From ed0b597717ca3df062e458735516bb5eac7dec75 Mon Sep 17 00:00:00 2001 From: Nils Lehnen <30603423+iderex@users.noreply.github.com> Date: Sun, 23 Aug 2026 05:02:54 +0200 Subject: [PATCH] Record the reading that the install address answers (#133) The address answers with the catalogue it promises now, and nothing in the tree said so, so `internal/address.Answered` stayed empty and the merge gate went on refusing every tracked file that printed the address. That refusal was correct while the address answered 404 and it had started refusing a true instruction, which is the opposite failure and the more expensive one: it keeps an operator from being told how to install anything. The entry rests on three requests rather than a status code. A holding page, a redirect and a rewritten error page all answer 200, and a Jellyfin server renders each of them the way it renders an empty repository, so what is written down is that the bytes served are byte-identical with the ones this tree holds. decisions/manifest-address.md carries the requests and their output, and the two places that stated the 404 as the current state now read as superseded rather than as a description of the world. Three tests in the suite were reading the live list and planting the published address as the thing nobody had read. They went green together the moment the list took its entry, which is a suite passing for the wrong reason rather than a guard holding. Each one now pins the list empty for its own length, so it goes on measuring the refusal instead of the state of the tree, and the leg against the real tree with the real list is unchanged and still reads both. Signed-off-by: Nils Lehnen <30603423+iderex@users.noreply.github.com> --- decisions/manifest-address.md | 87 ++++++++++++++++++++++++-------- internal/address/address.go | 36 ++++++++----- internal/address/address_test.go | 36 +++++++++++-- 3 files changed, 122 insertions(+), 37 deletions(-) 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