Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 67 additions & 20 deletions decisions/manifest-address.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,36 +49,76 @@ with the file it promises. Not once the name is registered, and not once the
host resolves. The test is a request for the address itself, and the evidence is
that request's output recorded where the address is argued.

The reason for the stricter form is in the current state of the tree. The name
resolves and the site answers, and the printed install address still does not:
The reason for the stricter form was in the state of the tree the day the rule
was written. The name resolved and the site answered, and the printed install
address did not:

curl -sS -o /dev/null -w "%{http_code}\n" https://flowfin.dev/
200
curl -sS -o /dev/null -w "%{http_code}\n" https://flowfin.dev/manifest.json
404

Both run 2026-08-08 against the tree at 6a98de6. A rule written against DNS
alone would read that pair as satisfied.
alone would have read that pair as satisfied, which is why this one is not.

## The reading that fills the list

The address answers now, and this is the request the entry rests on:

curl -sS -o /dev/null -w "%{http_code}\n" https://flowfin.dev/manifest.json
200

curl -sS https://flowfin.dev/manifest.json \
| python -c "import json,sys; d=json.load(sys.stdin); print(len(d),'entry')"
1 entry

curl -sS -o served.json https://flowfin.dev/manifest.json
git show origin/main:docs/manifest.json | cmp - served.json && echo identical
identical

All three run 2026-08-23 against the tree at 514c771, which is the commit that
carried the generated catalogue into `docs/`.

The third request is the one that decides it rather than the first. A holding
page, a redirect and a rewritten error page all answer 200, and a Jellyfin server
renders each of them the same way it renders an empty repository, so a status
code alone records that something is at the address rather than that the
catalogue is. What is written down here is that the bytes served are the bytes
this tree holds.

What was read is one entry and one version, which is the scope #65 settled, and
the address is now recorded in `internal/address.Answered`. That does not make
the catalogue complete, and nothing here says it is: the ten plugins that have
published nothing and the declaration switched off for the first publication are
`decisions/first-release.md`, and they are absent by decision rather than by
failure.

Two tracked files printed the address, and #35 removed both:

grep -rno 'https://[a-zA-Z0-9./_-]*manifest.json' -- README.md docs/ ; echo "exit=$?"
exit=1

Run 2026-08-08. Nothing is printed and the grep exits 1. What the operator-facing
files say instead is that installation is not available yet, which is true and is
what an address that answers 404 leaves them able to say.
Run 2026-08-08. Nothing was printed and the grep exited 1. What the
operator-facing files said instead was that installation is not available yet,
which was what an address answering 404 left them able to say. Now that the entry
exists they may print the address, and whether they do is the instruction in #55
rather than this rule.

The pair of requests above stays here because this is where the address is
argued, and recording a 404 is not the same act as printing an install
instruction.
The 404 pair stays here because this is where the address is argued, and a
superseded reading is worth more in place than deleted: it is the pair that
decided the shape of the rule, and a reader who meets only the reading that
succeeded cannot see why a request rather than a DNS lookup is what the rule
asks for. Recording a 404 was never the same act as printing an install
instruction, which is why holding it here refused nothing.

`Gate: install-address-is-answered` is what refuses the next file to print one.
It reads every tracked file, recognises an address on the host `docs/CNAME`
declares whose last path segment ends in `manifest.json`, and refuses it unless
that exact address is recorded in `internal/address.Answered` as having been read
and found to answer. That list is empty, so the leg refuses every printed install
address today, which is the sentence above with a machine behind it.
and found to answer. That list holds one address, so the leg admits that one and
goes on refusing every other, which is the sentence above with a machine behind
it. It matched nothing for as long as the list was empty, and what it refuses is
narrower now rather than gone.

Two things sit outside it, each for its own reason. `decisions/` is not read,
because this directory is where an address is argued and therefore where the
Expand All @@ -90,20 +130,27 @@ evidence for being evidence. Where a third party's address rots, that is a link,
and `internal/links` holds it under the harness.

Adding an entry to that list is the act this rule turns on, and it costs the
request, its output written here, and the list changed in the same commit. What
keeps the entry honest afterwards is the harness check
request, its output written here, and the list changed in the same commit. That
price was paid on 2026-08-23 in the section above. What keeps the entry honest
afterwards is the harness check
`TestEveryRecordedInstallAddressStillAnswers`, which re-reads every recorded
address and refuses one that has stopped answering, or that answers with
something a server cannot read as a catalogue. An address that answered once and
stopped is silent on every server that already has it.

The operator instruction that prints the address is the other half of #34 and
cannot be written until that list is not empty.
That check returned early for as long as the list was empty, and it says so on
its own output rather than passing quietly. With an entry it makes the request,
so the day the entry landed is the day the check began reading anything at all.

The operator instruction that prints the address is the other half of #34. The
list is no longer what stands in front of it.

## What this costs

Publishing nothing until the address answers means the project is not
installable in the meantime, which is the state it is in anyway. The cost lands
later instead: once the address is published it cannot be tidied, consolidated
or moved to a shorter name, and a lapsed renewal is a silent outage for every
installation rather than an error anybody sees.
Publishing nothing until the address answered meant the project was not
installable in the meantime, which was the state it was in anyway. That half of
the cost is spent. What is left is the half this rule was always going to hand
forward: the address is published, so it cannot be tidied, consolidated or moved
to a shorter name, and a lapsed renewal is a silent outage for every installation
rather than an error anybody sees. Both obligations start on the day the entry
lands rather than on the day somebody first pastes the address.
36 changes: 23 additions & 13 deletions internal/address/address.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
// waited on it would be a merge waiting on a certificate, a DNS record and
// somebody else's uptime.
//
// The Answered list below is what joins the two halves, and it is empty. Adding
// an entry is the act that requires the measurement, and the harness is what
// keeps the entry honest afterwards.
// The Answered list below is what joins the two halves, and it carries one
// address. The entry is what lets a tracked file print it, adding one is the act
// that requires the measurement, and the harness is what keeps the entry honest
// afterwards.
package address

import (
Expand All @@ -36,22 +37,31 @@ import (
// with the manifest they promise. An address here may appear in a tracked file;
// one that is not here may not.
//
// It is empty, and that is the state of the world rather than a placeholder:
// It carries one, and this is the reading behind it:
//
// curl -sS -o /dev/null -w "%{http_code}\n" https://flowfin.dev/
// 200
// curl -sS -o /dev/null -w "%{http_code}\n" https://flowfin.dev/manifest.json
// 404
// 200
//
// curl -sS -o served.json https://flowfin.dev/manifest.json
// git show origin/main:docs/manifest.json | cmp - served.json && echo identical
// identical
//
// Recorded in decisions/manifest-address.md and run 2026-08-23 against the tree
// at 514c771. A status code on its own would not carry the entry: a holding page
// answers 200 as readily as a catalogue does, and a Jellyfin server renders both
// as an empty repository, so what is written down is that the bytes served are
// the catalogue this tree holds.
//
// Recorded in decisions/manifest-address.md and run 2026-08-08 against the tree
// at 6a98de6. The name resolves and the site answers; the address an operator
// would paste does not, and the decision refuses a rule written against DNS
// alone for exactly that reason.
// This comment said the list was empty and that the address answered 404. Both
// were the state of the world on 2026-08-08 at 6a98de6, and both stopped being it
// when the generated catalogue was landed and the host rebuilt from it. The pair
// of requests that recorded the 404 stays in decisions/manifest-address.md, where
// a superseded reading is argued rather than deleted.
//
// What adding an entry costs: the request above, its output written where the
// What adding an entry cost: the requests above, their output written where the
// address is argued, and this list changed in the same commit. What it buys is
// that the instruction may then be written, which is the other half of #34.
var Answered = []string{}
var Answered = []string{"https://flowfin.dev/manifest.json"}

// Finding is one refusal.
type Finding struct {
Expand Down
36 changes: 32 additions & 4 deletions internal/address/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,28 @@ func at(t *testing.T, path string) string {
return "https://" + ourHost(t)[0] + path
}

// nothingRecorded pins the recorded list to empty for the length of one test.
//
// Each test that calls it is about an address nobody has read, and the published
// address is recorded now, so a test reading the live list stops biting on the
// day an entry lands rather than on the day the rule changes. That is measured
// rather than supposed: the three tests below went green together the moment
// Answered took its first entry, and they were the reason it was noticed.
// Pinning is what keeps them measuring the refusal instead of the state of the
// tree, and the leg against the real tree with the real list is
// TestNoTrackedFilePrintsAnUnansweredInstallAddress at the bottom of this file.
func nothingRecorded(t *testing.T) {
t.Helper()
restore := Answered
Answered = nil
t.Cleanup(func() { Answered = restore })
}

func TestRefusesAnInstallAddressNobodyHasReadYet(t *testing.T) {
nothingRecorded(t)

// The bite, and the exact shape the tree carried before #35 removed it: an
// install instruction printed against an address that answers 404.
// install instruction printed against an address nobody had read.
page := "<p>Paste this into the repositories list:</p>\n" +
"<code>" + at(t, "/manifest.json") + "</code>\n"

Expand All @@ -51,13 +70,20 @@ func TestRefusesAnInstallAddressNobodyHasReadYet(t *testing.T) {
}

func TestRefusesANearMissOfThePublishedNameToo(t *testing.T) {
nothingRecorded(t)

// Four near misses of the published address: another file name, a deeper
// path, another scheme and port, another case. The first two are why the
// predicate matches a last segment ending in manifest.json rather than one
// that is exactly manifest.json. Each of the four is a string somebody
// pastes into a repositories list, and none of them is recorded as
// answering, so a file printing one promises a manifest just as loudly as
// the published name would.
// pastes into a repositories list, and with nothing recorded a file printing
// one promises a manifest just as loudly as the published name would.
//
// The fourth stops being a near miss once the published address is recorded,
// because answered() compares case-insensitively and that is the comparison
// the rule wants: an operator who pastes the address in another case reaches
// the same file. So it is a near miss of a name nobody has read, which is
// what the pinned list makes it here.
for _, printed := range []string{
at(t, "/prerelease-manifest.json"),
at(t, "/channels/stable-manifest.json"),
Expand Down Expand Up @@ -132,6 +158,8 @@ func TestSparesTextThatIsNotAnInstallAddress(t *testing.T) {
}

func TestWhatMayQuoteTheAddressIsNamedAndSmall(t *testing.T) {
nothingRecorded(t)

// The skip list is the part of this check most likely to grow quietly, one
// file at a time as each new one is discovered. It is a scope of two
// entries instead: the directory where an address is argued, and the check
Expand Down
Loading