diff --git a/decisions/manifest-address.md b/decisions/manifest-address.md index 6590618..801b2a2 100644 --- a/decisions/manifest-address.md +++ b/decisions/manifest-address.md @@ -49,8 +49,9 @@ 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 @@ -58,27 +59,66 @@ resolves and the site answers, and the printed install address still does not: 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 @@ -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. diff --git a/internal/address/address.go b/internal/address/address.go index 966f8ae..a471264 100644 --- a/internal/address/address.go +++ b/internal/address/address.go @@ -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 ( @@ -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 { diff --git a/internal/address/address_test.go b/internal/address/address_test.go index 96560db..22a7684 100644 --- a/internal/address/address_test.go +++ b/internal/address/address_test.go @@ -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 := "
Paste this into the repositories list:
\n" + "" + at(t, "/manifest.json") + "\n"
@@ -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"),
@@ -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