diff --git a/00-META/checks/records.py b/00-META/checks/records.py index 83e3d1d..0484dd8 100644 --- a/00-META/checks/records.py +++ b/00-META/checks/records.py @@ -284,6 +284,59 @@ def check_numbering(failures, records): ) +def check_progressive_insights(failures, records): + """A correction made inside a record is marked and dated, or it is a silent rewrite. + + A record may be corrected in place when a *fact* in it went stale and the decision still + stands (`02-DECISIONS/README.md`, "Progressive insight"). The whole safety of that allowance + is that the correction is legible in the record rather than only in a diff nobody reads, so + the form is what is checked here: every mention of an insight is the marker, the marker + carries an ISO date, and that date is not earlier than the decision's own — an insight + predating the decision it corrects is a copied marker, not a correction. + + What this cannot check is an edit made with no marker at all. Nothing mechanical can; that + one is the reviewer's, reading the diff. The check keeps the *marked* path honest so that an + unmarked change stands out as the anomaly it is. + """ + phrase = re.compile(r"progressive insight", re.I) + marker = re.compile(r"\*\*Progressive insights?\s*[\u2014\u2013-]\s*(\d{4}-\d{2}-\d{2})\.?\*\*") + loose = re.compile(r"\*\*[^*]*[Pp]rogressive insights?[^*]*\*\*") + iso = re.compile(r"^\d{4}-\d{2}-\d{2}$") + + for number, record in sorted(records.items()): + text = record["text"] + if not phrase.search(text): + continue + decided = str(record["front"].get("date", "")) + good = [(m.start(), m.end(), m.group(1)) for m in marker.finditer(text)] + + for m in loose.finditer(text): + if any(s <= m.start() and m.end() <= e for s, e, _ in good): + continue + failures.add("insights", rel(record["path"]), + "a progressive insight is not in the dated marked form " + "'**Progressive insight \u2014 YYYY-MM-DD.**': %s" % m.group(0)) + + for _, _, stamp in good: + if decided and iso.match(decided) and stamp < decided: + failures.add("insights", rel(record["path"]), + "a progressive insight dated %s predates the decision (%s)" + % (stamp, decided)) + + covered = [(s, e) for s, e, _ in good] + for m in phrase.finditer(text): + if any(s <= m.start() and m.end() <= e for s, e in covered): + continue + line = text.rfind("\n", 0, m.start()) + 1 + if text[line:m.start()].lstrip().startswith("#"): + continue + if loose.search(text, line, text.find("\n", m.end()) + 1 or len(text)): + continue + failures.add("insights", rel(record["path"]), + "'progressive insight' appears unmarked; a correction is marked and " + "dated, or it is a silent rewrite") + + def check_status_against_code(failures): """A design document naming specific code may not still call itself `designed`. @@ -325,6 +378,7 @@ def main(): check_numbering(failures, records) check_topics(failures, records) check_status_against_code(failures) + check_progressive_insights(failures, records) print(f"records: {len(records)} decision records checked") return failures.report() diff --git a/00-META/process/02-graduation.md b/00-META/process/02-graduation.md index e135f16..487c293 100644 --- a/00-META/process/02-graduation.md +++ b/00-META/process/02-graduation.md @@ -33,7 +33,10 @@ A design changes only through a decision. 1. Write the decision record. If it reverses an earlier one, the earlier record's `status:` - becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its text is never edited**. + becomes `superseded-by: 02-DECISIONS/NNNN-....md` — **its reasoning is never rewritten**. If the + earlier record is sound and only a *fact* in it went stale, that is a **progressive insight**, + corrected in place and marked in the record rather than superseded + ([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)). 2. Edit the to-be design document and set `updated:` to today. 3. If the amendment came from an issue, set that issue's `amended-design:` to the document path. @@ -54,4 +57,5 @@ Implementation state is a third axis, independent of both design and decision. - Do not move a to-be document into `00-as-is/`. Write the as-is document; both stand. - Do not edit an as-is document to describe an intention. That is what the to-be layer is for. -- Do not change a decision record's meaning. Supersede it. +- Do not change a decision record's meaning. Supersede it. Correcting a fact it got wrong, while + the decision stands, is a progressive insight — marked and dated in the record, never silent. diff --git a/02-DECISIONS/0115-the-bus-is-built-in-five-steps.md b/02-DECISIONS/0115-the-bus-is-built-in-five-steps.md index 2d4c9ed..b980838 100644 --- a/02-DECISIONS/0115-the-bus-is-built-in-five-steps.md +++ b/02-DECISIONS/0115-the-bus-is-built-in-five-steps.md @@ -61,8 +61,9 @@ may not disagree about* still describes the bus being replaced. waits for the one after it. The cutover remains a single rollout** — dividing the build does not divide the bus. -**Step 1 — genesis raises the broker.** The `nats` module and a mesh raised on it from nothing. -This is built and proven even though the mesh it is for will never travel this path, because +**Step 1 — genesis raises the broker.** The `nats` module, and genesis placing it in the +foundation. This is built and proven even though the mesh it is for will never travel this path, +because genesis is where the foundation is *defined*: the only place the mesh comes from nothing, and the definition every other path is measured against. A genesis path that exists only on paper is one nobody discovers is wrong until there is a second mesh. @@ -78,8 +79,9 @@ renamed by every change the seat exists to survive. protocol, an SDK is an implementation of it in one language and nothing more, the protocol is split per capability, and conformance is executable fixtures per capability rather than prose. What changes is what the specification specifies. Design 19's wire section is rewritten from exchanges -and queues to subjects and streams; the fixtures are recaptured on NATS; every SDK claims the -capabilities it passes, and a language may arrive with connection and events alone. +and queues to subjects and streams; the conformance suite is built — first against the bus the +mesh has, then restated on NATS; every SDK claims the capabilities it passes, and a language may +arrive with connection and events alone. **The SDK gains no conveniences in the process.** [ADR 0039](0039-what-the-sdk-holds-and-refuses.md) refuses frequent-and-cascading code in the shared library, and ADR 0074 restates it — an SDK is @@ -119,17 +121,48 @@ is still on AMQP throughout. The bus moves on one day, at the end, once. ## How it is checked - **Per step, a bed, and the bed named in design 25 §10 against the step it belongs to.** Step 1: - a mesh raised on NATS from genesis. Step 2: the broker adopted into a mesh already running, and - a second holder of `mesh-broker` refused at resolution. Step 3: the conformance suite passing per - capability, on NATS, for every SDK that claims it — and a module built before the binding serving - its tools unchanged. Step 4: each converted flow proved against the behaviour it replaced. Step 5: - the cutover bed, then the rollout with every node reporting. + a mesh raised from nothing has the server standing, its streams asserted and every account + composed from the manifests — no mesh traffic on it yet. Step 2: the broker adopted into a mesh + already running, a second holder of `mesh-broker` refused at resolution, and nothing routed to it. + Step 3: the conformance suite passing per capability, on NATS, for every implementation that + claims it — and a module built before the binding serving its tools unchanged. Step 4: each + converted flow proved against the behaviour it replaced, **and the full genesis bed** — a mesh + enrolling, holding a push, and rolling out an upgrade on NATS. Step 5: the cutover bed, then the + rollout with every node reporting. - **Step 3 is not done when the code runs.** It is done when the fixtures match byte for byte across implementations, which is ADR 0074's own test and the only one that catches two SDKs quietly ignoring each other. - **A step that cannot name what its bed proves is not a step**, and is divided further before it is started. +## Progressive insights + +Corrections of fact made in this record after it was accepted, under the rule in +[`README.md`](README.md). The decision — five steps, each proved, one rollout — is untouched by +both; each corrects something this record asserted about the *state of the code*, which nobody had +measured when it was written. + +> **Progressive insight — 2026-09-26.** *There is no conformance suite to recapture.* This record +> said step 3's "fixtures are recaptured on NATS", and design 19's wire was described as specified +> and conformed. Measuring the four repositories found no conformance fixtures in any of them: +> [design 22](../03-DESIGN/01-to-be/22-the-work-ahead.md)'s Phase 1.2, which would have built the +> suite, is still open. Step 3 therefore **builds** it, and builds it first against the bus the +> mesh has — a suite born on the new bus certifies whatever the new bus happens to do. The step's +> place in the order, and ADR 0074's model it implements, are unchanged. + +> **Progressive insight — 2026-09-26.** *The full genesis bed belongs to step 4, not step 1.* This +> record attributed "a mesh raised on NATS from genesis" to step 1, and described that step as "the +> `nats` module and a mesh raised on it from nothing". A bed that enrols a node, holds a push and +> rolls out an upgrade needs the controller and the host to speak NATS — which is step 3's +> implementations and step 4's flows. A step whose proof cannot run is exactly the failure this +> record was written to prevent, so step 1 now ends at the server standing from genesis, correctly +> configured and carrying nothing, and the full bed is named under step 4. The five steps, their +> names, their order and the single rollout are unchanged; only where two beds run has moved. +> +> Recorded here rather than superseded because the decision this record makes — that the work is +> divided and each division is proved — is what *produced* the correction: the dependency was +> invisible while the build was one item. + ## References - [ADR 0106](0106-the-bus-is-nats.md) — the decision this divides. diff --git a/02-DECISIONS/README.md b/02-DECISIONS/README.md index a54f2ac..09145ed 100644 --- a/02-DECISIONS/README.md +++ b/02-DECISIONS/README.md @@ -8,8 +8,45 @@ the decision is recorded here, and only then is the design written. Following th numbers walks the process in the order it happens. One file per decision, numbered, never deleted. A superseded record has its `status:` changed -and gains a pointer to what replaced it — **its text is never edited**. The reasoning that was -rejected is the expensive half to rediscover. +and gains a pointer to what replaced it — **its reasoning is never rewritten**. The reasoning that +was rejected is the expensive half to rediscover. + +## Progressive insight + +A record is a decision, not a snapshot of everything that was true the day it was written, and +those two fail differently. **A fact a record asserted can turn out to be wrong while the decision +it supports stays right** — a count taken before anyone measured, a file named that does not +exist, a proof attributed to a step that cannot run it. Superseding a record for that buries a +correct decision under a second one, and teaches every reader to first work out which of two +records is live. Done a few times, the reading order stops being one. + +So: **a correction of fact that leaves the decision standing is made in the record, in place, +marked and dated.** + +> **Progressive insight — YYYY-MM-DD.** What was found, what the record said before, and what it +> says instead. + +Three conditions, all of which hold: + +- **It corrects a fact, not a judgement.** That a suite does not exist is a fact. That building it + is the wrong order is a judgement, and judgements supersede. +- **It adds; it never quietly replaces.** Where body text changes, the note says what stood there + before, so a reader who followed a citation to the old wording can find out what happened to it. + A correction nobody can see is indistinguishable from a record that was always right, which is + the failure the immutability rule exists to prevent. +- **The decision, the options weighed and the consequences stand untouched.** If the correction + changes what was decided, which alternatives were rejected, or a consequence another record + relies on, it is not an insight — write the superseding record. + +**What still supersedes**, without exception: reversing a decision, changing its scope, rejecting +an option it accepted, or making a consequence false that a later record cites. The test is not +how large the edit looks in a diff; it is whether a reader who acted on the old text would now be +wrong about *what was decided* rather than about *a detail the decision did not rest on*. + +**How this is checked.** `00-META/checks/records.py` requires every insight to be marked in the +form above and dated no earlier than the record's own `date:` — an unmarked edit is a rule +violation the reviewer looks for in the diff, and a marked one is legible in the record itself. +The git history is the backstop, not the record of intent; the note is the record of intent. The records run in the order the decisions were taken, oldest first. diff --git a/03-DESIGN/01-to-be/25-the-bus-on-nats.md b/03-DESIGN/01-to-be/25-the-bus-on-nats.md index 0b0fc45..77f70ca 100644 --- a/03-DESIGN/01-to-be/25-the-bus-on-nats.md +++ b/03-DESIGN/01-to-be/25-the-bus-on-nats.md @@ -316,26 +316,20 @@ including the two places their dependencies put a bed later than the step that n real if the proofs divide with it. A step that cannot name what its bed proves is not a step, and is divided further before it is started. -**Step 1 — the genesis bed**, a mesh raised on NATS from nothing: -- a node enrols over TLS with a claimed token, and the enrolment user cannot read a declaration; -- a push composes; the store is stopped; the push is held (nak with delay), the store returns, the - push applies, nothing was lost or duplicated; -- a node that was away gets exactly the newest declaration, and a replayed older one is refused - by sequence; -- an upgrade rolls out to two nodes; -- a module's tool is invoked from another node and from a person's client, each with an account - that can invoke it, and refused from one that cannot; -- a module's account cannot publish outside its `emits` nor subscribe outside its `consumes` — - refused by the server; -- a module acks a delivery from its own durable consumer, and is refused acking another module's; -- a user subscribes another module's or person's inbox prefix and is refused by the server, not - by the client's own good behaviour; -- an event whose consumer keeps failing dead-letters after `max-deliver`; -- an enrolment request held by a `nak`-with-delay cycle still reaches the enrolling node's inbox - once the controller answers — proving the reply travels in the payload and not the transport - field a consumer's ack has already claimed; -- the `nats` container is not recreated when only its composed configuration file changes, and - a change to that file is live (a new user can connect, a revoked one cannot) within one +**Step 1 — the genesis-broker bed**, a mesh raised from nothing, the server standing on it. The +mesh does not yet *live* on this bus — nothing speaks it until step 3's implementations exist — so +what this bed proves is the server, its configuration and the permissions, each of which the server +itself enforces and a plain client can therefore check: +- the four streams exist, asserted idempotently on a second start, with the retention of §3; +- every account and permission in the composed file is derived from the manifests' `emits` and + `consumes` and nothing else, with each user's own ack subject and its own inbox prefix; +- a user cannot publish outside its `emits` nor subscribe outside its `consumes` — refused by the + server, not by convention; +- a user cannot ack another user's delivery, and cannot subscribe another's inbox prefix — refused + by the server, not by the client's own good behaviour; +- the monitoring port is refused from anything but the private network; +- the `nats` container is not recreated when only its composed configuration file changes, and a + change to that file is live (a new user can connect, a revoked one cannot) within one watcher-poll interval, without a restart. **Step 2 — the adoption bed**, a mesh already running that has never had this server: @@ -358,14 +352,24 @@ divided further before it is started. - the shared library gained nothing but the binding: its surface is the protocol and the primitives, and a helper that arrived with the transport is a review failure, not a detail. -**Step 4 — each converted flow against the behaviour it replaced:** +**Step 4 — the mesh living on it.** The implementations exist from step 3, so this is where a mesh +can first be raised on NATS and run: +- a node enrols over TLS with a claimed token, and the enrolment user cannot read a declaration; +- a push composes; the store is stopped; the push is held (nak with delay), the store returns, the + push applies, nothing was lost or duplicated; +- a node that was away gets exactly the newest declaration, and a replayed older one is refused + by sequence; +- an upgrade rolls out to two nodes; +- an enrolment request held by a `nak`-with-delay cycle still reaches the enrolling node's inbox + once the controller answers — proving the reply travels in the payload and not the transport + field a consumer's ack has already claimed; +- an event whose consumer keeps failing dead-letters after `max-deliver`; +- a module's tool is invoked from another node and from a person's client, each with an account + that can invoke it, and refused from one that cannot; - a build source's change reaches the builder over the bus, and the build that follows is the one the change asked for; - an installation completes over the bus, with the same outcome the path it replaces produced; -- a module's reports arrive, and a node that was unreachable catches up rather than losing them; -- nothing of research 017's observation work is present — no heartbeat semantics, no condition - store — because that design is not written yet and a flow built ahead of it would have to be - rebuilt. +- a node that was unreachable catches up on its reports rather than losing them. **Step 5 — the cutover bed:** a mesh on AMQP with a predecessor stand-in on the compatibility broker moves its bus in one rollout; every node reports on NATS afterwards; the stand-in's client diff --git a/03-DESIGN/01-to-be/28-building-the-bus.md b/03-DESIGN/01-to-be/28-building-the-bus.md index ca5f952..d96aebb 100644 --- a/03-DESIGN/01-to-be/28-building-the-bus.md +++ b/03-DESIGN/01-to-be/28-building-the-bus.md @@ -65,12 +65,12 @@ mirror rather than share (the host imports nothing, by host's link, and the runtime's client. And a search for conformance fixtures finds none anywhere in the four repositories: design 22's Phase 1.2 — the suite — has not been built. -> **A correction to ADR 0115, found in the measuring.** That record says step 3's fixtures are -> *recaptured* on NATS. There is nothing to recapture: the suite does not exist. Step 3 **builds** -> it, and its first job is to pin the wire the mesh has before changing it, because a suite written -> only against the new bus certifies whatever the new bus happens to do. The record's decision is -> unaffected; the word was wrong, and is corrected here rather than edited there -> ([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md): a record's meaning is never edited). +> **This corrected the record.** ADR 0115 said step 3's fixtures were *recaptured* on NATS. There +> is nothing to recapture, so step 3 **builds** the suite, and its first job is to pin the wire the +> mesh has before changing it — a suite written only against the new bus certifies whatever the new +> bus happens to do. A fact went stale while the decision stood, which is a **progressive insight** +> ([`02-DECISIONS/README.md`](../../02-DECISIONS/README.md)): it is marked and dated in ADR 0115 +> itself rather than left to be discovered here. ## The order the work actually allows @@ -91,12 +91,12 @@ otherwise would put two beds where they cannot run.** Three edges decide it: So step 1's bed proves *the server, from genesis, configured* — not a mesh living on it. The full genesis bed is step 4's, where it can first run. -> **This differs from ADR 0115's check list, deliberately.** That record attributes "a mesh raised -> on NATS from genesis" to step 1. Measuring the dependency showed that bed cannot run until the -> links exist, and a step whose proof cannot run is the exact failure the record was written to -> prevent. The five steps, their names and the single rollout are unchanged; only where two beds -> run has moved. If that reads as a change of meaning rather than a correction of fact, the fix is -> a superseding record, not an edit. +> **This corrected the record too.** ADR 0115 first attributed "a mesh raised on NATS from genesis" +> to step 1. That bed cannot run until the links exist, and a step whose proof cannot run is the +> exact failure the record was written to prevent — so step 1 now ends at the server standing, +> correctly configured and carrying nothing, and the full bed is named under step 4. The five +> steps, their names, their order and the single rollout are unchanged; only where two beds run has +> moved. Marked and dated in ADR 0115 as a progressive insight, with what the record said before. ``` step 1 module, genesis places it ──┐ @@ -132,10 +132,15 @@ paper is wrong until there is a second mesh to find out. - [ ] 1.6 the genesis-broker bed **Done when.** A mesh raised from nothing has the server standing with the streams asserted and -every account and permission composed from the manifests; the monitoring port is refused from -anything but the private network; a change to the composed file is live within one watcher interval -without a restart, and the container is not recreated by it. No mesh traffic is on it yet — that is -step 4's bed, not this one's. +every account and permission composed from the manifests; a user cannot publish outside its +`emits`, subscribe outside its `consumes`, ack another user's delivery or subscribe another's inbox +prefix; the monitoring port is refused from anything but the private network; a change to the +composed file is live within one watcher interval without a restart, and the container is not +recreated by it. + +**The permission checks belong here rather than later** because the server enforces them itself — a +plain client proves them, no link required — and they are the whole of what ADR 0043 asks for. No +mesh traffic is on the bus yet; that is step 4's bed, not this one's. ## Step 2 — adoption puts it in the seat @@ -191,14 +196,14 @@ module. **Why here.** The links exist from step 3, so the flows that are not on the bus at all can move onto it, and the beds that need a mesh living on NATS can finally run. -- [ ] 4.1 **the full genesis bed** — a mesh raised on NATS from nothing: a node enrols over TLS with - a claimed token and the enrolment user cannot read a declaration; a push is held while the - store restarts and applies after, nothing lost or duplicated; a node that was away gets - exactly the newest declaration and refuses a replayed older one by sequence; an upgrade rolls - out to two nodes; an event dead-letters after `max-deliver`; a module cannot publish outside - its `emits`, ack another module's delivery, or subscribe another's inbox prefix; and an - enrolment held by a `nak`-with-delay cycle still reaches the enrolling node, proving the reply - travels in the payload and not the transport field the consumer's ack has claimed +- [ ] 4.1 **the full genesis bed** — a mesh raised on NATS from nothing and living on it: a node + enrols over TLS with a claimed token and the enrolment user cannot read a declaration; a push + is held while the store restarts and applies after, nothing lost or duplicated; a node that + was away gets exactly the newest declaration and refuses a replayed older one by sequence; an + upgrade rolls out to two nodes; an event dead-letters after `max-deliver`; and an enrolment + held by a `nak`-with-delay cycle still reaches the enrolling node, proving the reply travels + in the payload and not the transport field the consumer's ack has claimed. The server-enforced + permissions were proved at step 1 and are not re-proved here - [ ] 4.2 a build source's change reaches the builder over the bus, and the build that follows is the one the change asked for - [ ] 4.3 an installation completes over the bus, with the same outcome as the path it replaces diff --git a/AGENTS.md b/AGENTS.md index 82d7069..71762df 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,8 +52,12 @@ vocabulary — *controller* (not "control plane"), *foundation* (not "substrate" docs (`layer`, `status`, `code`, `updated`), issue reports (`status`, `located-in`, `fixed-by`, `amended-design`) and decision records (`status`, `date`, `deciders`). Never create a central status file; cross-cutting views are generated from frontmatter. -- **`02-DECISIONS/` records are immutable.** Supersede with a new record; never edit meaning. Fixing a - broken link or path is allowed. +- **`02-DECISIONS/` records hold their meaning.** Supersede with a new record rather than rewriting + what was decided, the options weighed, or a consequence another record relies on. Fixing a broken + link or path is allowed, and so is a **progressive insight** — a correction of *fact* that leaves + the decision standing, made in place, marked and dated in the record's own words + ([`02-DECISIONS/README.md`](02-DECISIONS/README.md)). A fact going stale is not the decision going + wrong, and superseding a sound record for one buries it. - **Design docs are prose and diagrams only** — no code. A manifest field may be named; a manifest may not be pasted. - **Two layers, never mixed.** [`03-DESIGN/00-as-is/`](03-DESIGN/00-as-is/) describes the mesh