Skip to content

Commit eca0446

Browse files
committed
Accept ADR 0010; enforce the golden vector; decide R3, R4 and R15
Two strands from parallel work, landing together. ADR 0010 accepted, closing D6 (D26). Of D6's three gates, two were already met - C4's not-a-qualified-e-signature wording ships on both signing pages, and required-role semantics are ADR 0018 section 9. The third was not: D6 assumed the golden vector was a test, but it was a console.log that printed one line to a typically-unread server console and then kept sealing packages with hashes no independent verifier could reproduce. verifyGoldenVector() now lives beside the serializer, POST /api/turnover/build REFUSES on mismatch rather than sealing unverifiable evidence, and smoke_test.sh asserts the algo, digest shape, hash determinism across a rebuild, and supersession. The refusal path was verified by deliberately drifting the vector. Deliberately not a throw at load: a drifted turnover canonicalizer must not take down an instance serving QR scans, LOTO status and field checks. The D12 contract bump is explicitly not a precondition and stays M3 work. R3 -- a caveated pass is a derivation, not a state (section 4). No pass_with_exception; five states stay five. A pass renders as pass with exception when it carries a LINKED CONSEQUENCE - a punch item or a section 13 restriction - not merely a non-empty note. Two changes from the original recommendation: notes are common and mostly benign, so a badge firing on all of them is one nobody reads; and "pass + note + punch item" would have missed the clearest case in the source, where there is no punch item and never will be because the workaround is permanent and nobody owes work. Section 13 is what completes it. Residual risk named and accepted: a caveat written as prose with nothing linked stays invisible, and that is NOT mitigated by nudging on every noted pass, because a warning that fires on benign notes is one that gets clicked through. R4 -- not_performed carries a reason from a closed vocabulary (section 4): conditions_unavailable, access_blocked, equipment_unavailable, satisfied_by_other_evidence, out_of_scope, other. other requires free text; satisfied_by_other_evidence takes an optional satisfied_by relation, without which it is a nicer-sounding blank. out_of_scope complements section 6's scope-gap punch item rather than replacing it - the line says why the check was not done, the punch item says someone still owes the work. R15 absorbed into R4 and reclassified. It is not a third meaning of N/A: the check applies, so na was never right. It is not_performed with satisfied_by_other_evidence. That the source wrote N/A is the finding, not a mistake to correct past - people reach for N/A when nothing better exists, the same no-home-for-this-fact pattern as R9. D25 reconciled with D26: the fold decision is unaffected, but its precondition is now enforced by the build route rather than by a reviewer remembering to check. Both new R4 fields join the section 15 fold; the R3 derivation adds no hashed field.
1 parent c35b9bf commit eca0446

9 files changed

Lines changed: 560 additions & 79 deletions

‎docs/DECISIONS.md‎

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -312,10 +312,16 @@ gate is a check).
312312
**Decision.** ADR 0018's eight hash-affecting manifest additions land in **one
313313
pass, into `sha256-canon-v1`**, before any real package is signed — not behind a
314314
`sha256-canon-v2`. D6 is **amended, not reversed**: the canonical form and
315-
manifest allowlist are unfrozen for this fold, while ADR 0010's *acceptance*
316-
stays blocked on its own gates (the "not a qualified e-signature" wording C4,
317-
required-role semantics, and the D12 contract bump). ADR 0010 stays **Proposed**
318-
with a completed allowlist. Partial folds are forbidden — each addition alone
315+
manifest allowlist are unfrozen for this fold, while ADR 0010's *acceptance* is a
316+
separate act on separate gates (the "not a qualified e-signature" wording C4,
317+
required-role semantics, and the D12 contract bump). ~~ADR 0010 stays
318+
**Proposed** with a completed allowlist.~~ **Superseded the same day by
319+
[D26](#d26--adr-0010-accepted-the-golden-vector-was-not-a-test-until-it-was-made-one):
320+
ADR 0010 was accepted** — C4 and required-role semantics were already met, and
321+
D12 is explicitly not a precondition. **The fold decision is unaffected**; what
322+
changed is that D26 also made the golden vector enforceable at the build route,
323+
so this entry's precondition is now checked by code rather than by a reviewer
324+
remembering. Partial folds are forbidden — each addition alone
319325
changes the canonical bytes, so piecemeal shipping means several silent breaks
320326
instead of one deliberate one. The golden vector is regenerated as the
321327
acceptance test.
@@ -335,3 +341,67 @@ only one of them gets more expensive with delay.
335341
**Rejected.** Accepting ADR 0010 wholesale (its legal and contract gates are
336342
untouched by this work); minting `sha256-canon-v2` (no history to protect).
337343
**Cross-ref.** ADR 0018 §15 and open question 5; ADR 0010; D6, D12.
344+
345+
### D26 — ADR 0010 accepted; the golden vector was not a test until it was made one
346+
**Decision.** **ADR 0010 (Signatures & the turnover package) is Accepted**
347+
(2026-08-04), closing [D6](#d6--adr-0010-signatures--turnover-stays-proposed-and-frozen)'s
348+
freeze and resolving open question #5. Acceptance covers the *design*: the
349+
frozen snapshot, the canonical hash, supersession, and the attestation model.
350+
It explicitly does **not** perform the D12 contract bump — `turnover_packages`
351+
and `signatures` stay `delta; ADR 0010` in `docs/API.md` until M3.
352+
**What the gates turned out to be.** D6 named three. Two were already met and
353+
merely unrecorded: the "not a qualified e-signature" wording ships on both
354+
`sign.html` and `turnover.html`, and required-role semantics are ADR 0018 §9.
355+
The third was **not met, and D6 assumed it was** — the golden vector was a
356+
`console.log` at hook load, not a test. On canonicalizer drift it printed one
357+
line to a server console (on a trailer Pi, an unread one) and kept sealing
358+
packages with hashes no independent verifier could reproduce. That is worse than
359+
having no seal: an unverifiable hash on a signed turnover package looks like
360+
evidence and is not.
361+
**Fixed as part of accepting, not deferred.** `verifyGoldenVector()` moved beside
362+
the serializer in `turnover_canon.js`; `POST /api/turnover/build` **refuses**
363+
with an explicit message on mismatch rather than sealing unverifiable evidence;
364+
`scripts/smoke_test.sh` asserts the pinned algo, the digest shape, **hash
365+
determinism across a rebuild of an unchanged scope**, and supersession (89 → 94
366+
assertions). The refusal path was verified by deliberately drifting the vector
367+
and confirming the build fails.
368+
**Deliberately not a throw at load.** A drifted *turnover* canonicalizer must
369+
never take down an instance whose first job is QR scans, LOTO status, and field
370+
checks. "Never cripple the field tier" outranks failing fast about an office
371+
deliverable, so the blast radius stays scoped to builds. This is the same
372+
asymmetry ADR 0007 applies to LOTO, pointed the other way.
373+
**Rationale.** This work is test hardening, not feature work, so it does not
374+
violate [D2](#d2--trust-before-features-m1-comes-first) — it is exactly M1's
375+
goal (prove the invariants) applied to the one invariant that had been asserted
376+
in prose and nowhere else.
377+
**Cross-ref.** ADR 0010; ADR 0018 §§9, 15; D6, D12, D25; open questions #5, #13.
378+
379+
### D27 — The asset behind a position is an appended event, not a second identity
380+
**Decision.** Open question #12 (asset vs. position) is **resolved** by a small
381+
append-only **`equipment_events`** ledger on the tag — `kind` (replaced /
382+
rebuilt / reinstalled / relocated / nameplate_corrected), nullable
383+
`serial_before` / `serial_after`, occurred-at, who, note — plus a **derived**
384+
re-verify marker: any phase whose standing rests entirely on checks older than
385+
the tag's latest event renders as "passed before the {date} {kind} event." A
386+
marker, never a downgrade; the pass stays in the ledger. No `equipment_assets`
387+
collection, no second identity, no identity scheme. It is a scope item inside
388+
**M5.3**, not its own milestone or ADR (ADR 0018 §16).
389+
**What decided it.** The obvious design — an asset keyed on serial number —
390+
fails on contact with the source: **actuator serial number reads "N/A" on all
391+
six valve reports.** A whole equipment class has no identity to key on. The real
392+
requirement was never asset lifecycle; it is "a phase passed by a machine that
393+
was later pulled apart must not read as current," which is a derivation over
394+
dates. The source has the case live: a pump found with a frozen impeller,
395+
removed, rebuilt, and re-installed **between the clean-water and seeding
396+
phases**.
397+
**What this gives up, deliberately.** You cannot ask "where has serial X been
398+
installed over time," or follow one machine between positions. That is asset
399+
management and belongs in an ERP or a materials product, consistent with the
400+
standing refusal to become an ERP. Recorded as a conscious trade; if spares
401+
pools or rotating equipment ever matter, these events are the substrate the
402+
heavier model would be built on.
403+
**Rejected.** An `equipment_assets` collection with an install/remove ledger
404+
(holed on day one by unserialised assets, and it would touch the identity of
405+
every record pointing at a tag); a stored "needs re-verification" flag (status
406+
that drifts).
407+
**Cross-ref.** ADR 0018 §§11, 16; open question #12; ROADMAP M5.3.

‎docs/ROADMAP.md‎

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,16 @@ this is the index.
7373
append-only apply/release, derived lockout, staleness asymmetry, no green
7474
state. A visibility layer, never part of the energy-control decision. Free
7575
permanently — safety visibility is never paywalled.
76-
- **Signatures & Turnover** ([ADR 0010](adr/0010-signatures-and-turnover-package.md), **Proposed/frozen** — [DECISIONS.md D6](DECISIONS.md#d6--adr-0010-signatures--turnover-stays-proposed-and-frozen)) —
76+
- **Signatures & Turnover** ([ADR 0010](adr/0010-signatures-and-turnover-package.md), **Accepted 2026-08-04** — [DECISIONS.md D26](DECISIONS.md#d26--adr-0010-accepted-the-golden-vector-was-not-a-test-until-it-was-made-one)) —
7777
server-built frozen turnover snapshot + content hash, `sign.html`,
78-
`turnover.html`, shared `lc-auth.js`/`login.html`. Implemented but not
79-
formally accepted; no further work this cycle.
78+
`turnover.html`, shared `lc-auth.js`/`login.html`. The golden vector is now
79+
**enforced at the build route and asserted by the smoke test** (it was a
80+
`console.log` until acceptance). The API v1 contract bump is **not** included
81+
and remains M3 work ([D12](DECISIONS.md#d12--api-v1-reconciliation--version-bump-sequenced-to-m3-v10)).
8082
- **Selective auth + PII split** ([ADR 0011](adr/0011-auth-lockdown.md), **Accepted**) —
8183
public scan/reads and the deliberate accountless flag/check/LOTO/calibration
8284
creates survive; office writes and PII reads require an account; the
83-
89-assertion smoke test asserts the matrix across guest/user/superuser.
85+
94-assertion smoke test asserts the matrix across guest/user/superuser.
8486

8587
**Shipped schema without UI (deliberately parked, not roadmap items):** segment
8688
acceptance (owned by MainLine — [D10](DECISIONS.md#d10--segment-acceptance-is-owned-by-mainline-correction-of-record)),
@@ -121,8 +123,9 @@ validated — all gated by CI. No new features
121123
all wait for M2+.
122124
- The formal API v1 version bump (that is M3 — [D12](DECISIONS.md#d12--api-v1-reconciliation--version-bump-sequenced-to-m3-v10)).
123125
M1 only makes the *documentation* of the current collection set truthful.
124-
- Accepting ADR 0010, building any parked schema's UI, or a general offline
125-
layer.
126+
- ~~Accepting ADR 0010~~ (**done 2026-08-04, D26** — the code half was
127+
golden-vector test hardening, which *is* M1's goal; the decision half cost no
128+
build work), building any parked schema's UI, or a general offline layer.
126129

127130
**Definition of done (testable)**
128131
- [x] `grep -ri "linecheck" docs/ *.md` returns only historical/superseded
@@ -568,8 +571,23 @@ One tag number appeared for two different valves in a vendor report —
568571
undetectable on paper. Equipment also got pulled, rebuilt, and reinstalled
569572
mid-startup with nothing prompting re-verification.
570573

571-
- **Data model (this milestone).** None. All three checks below are
572-
derivations over data that already exists.
574+
- **Data model.** One small append-only collection, **`equipment_events`**
575+
(ADR 0018 §16, resolving open question #12): `project` · `tag` · `occurred_at`
576+
· `kind` (`replaced` / `rebuilt` / `reinstalled` / `relocated` /
577+
`nameplate_corrected`) · `serial_before` / `serial_after` (**both nullable**)
578+
· `performed_by` · `vendor_company` · `note` · `client_token`.
579+
`updateRule: null`, `deleteRule: null`, public create. The four validation
580+
checks below need no schema at all.
581+
- **The re-verify marker is derived.** Any phase whose standing rests entirely
582+
on checks **older than the tag's latest event** renders as "passed before the
583+
{date} {kind} event." A marker, never a downgrade — the pass stays in the
584+
ledger, and nothing is stored. The frozen turnover package carries the same
585+
marker. **No `equipment_assets` collection and no second identity:** the
586+
source's actuators carry no serial number at all, so a serial-keyed asset
587+
model is holed on day one, and the real requirement ("a phase passed by a
588+
machine later pulled apart must not read as current") is a derivation over
589+
dates. Asset-history tracking — "where has serial X been installed" — is a
590+
**conscious loss**, recorded in [D27](DECISIONS.md#d27--the-asset-behind-a-position-is-an-appended-event-not-a-second-identity).
573591
- **UI — four checks, not three.** (a) **Unknown-tag warning** wherever a tag
574592
number is *typed* rather than picked — vendor visit capture, vendor tag-list
575593
import, open-items text — resolved against the project's tag register,
@@ -590,13 +608,11 @@ mid-startup with nothing prompting re-verification.
590608
- **Validation.** Deliberately **no unique index on `serial_number`**: blanks
591609
are the norm and a duplicate is a finding to investigate, not a write to
592610
reject.
593-
- **Deferred to its own ADR.** The true **asset-vs-position split** — `tag` as
594-
a P&ID position, the serial-numbered machine as an asset with an append-only
595-
install/remove ledger, and the re-verification prompt ("this position's asset
596-
changed on {date}; phases passed before that date were passed by a different
597-
machine"). It changes the identity of every record pointing at a tag and must
598-
not be improvised inside a validation task. See
599-
[open-questions #12](open-questions.md#12-does-loopcheck-model-the-asset-separately-from-the-position).
611+
- ~~**Deferred to its own ADR.** The true asset-vs-position split…~~
612+
**Resolved 2026-08-04** — see the `equipment_events` bullets above. The heavy
613+
`equipment_assets` model was rejected, not postponed:
614+
[open-questions #12](open-questions.md#12-does-loopcheck-model-the-asset-separately-from-the-position),
615+
[D27](DECISIONS.md#d27--the-asset-behind-a-position-is-an-appended-event-not-a-second-identity).
600616

601617
**M5 definition of done (testable)**
602618
- [ ] No `check_item` can be created with `result = "na"` and no reason; the
@@ -844,15 +860,15 @@ Status of each, per the 2026-07-21 planning session:
844860
| 2 | Continue Phase 6 segments? | **Decided** — no; MainLine owns ([D10](DECISIONS.md#d10--segment-acceptance-is-owned-by-mainline-correction-of-record)) |
845861
| 3 | Preservation ownership | **Open** — resolve boundary vs TrenchNote before any preservation UI ([D7](DECISIONS.md#d7--preservation-adr-0015-and-vendor-visits-adr-0017-uis-are-parked)) |
846862
| 4 | Portable identity scheme | **Deferred** — gate before first handoff ([D11](DECISIONS.md#d11--portable-identity-scheme-deferred)) |
847-
| 5 | Accept ADR 0010? | **Deferred** — stays Proposed/frozen ([D6](DECISIONS.md#d6--adr-0010-signatures--turnover-stays-proposed-and-frozen)) |
863+
| 5 | Accept ADR 0010? | **Resolved** (2026-08-04) — **Accepted**; the golden vector was a `console.log` and is now enforced + tested ([D26](DECISIONS.md#d26--adr-0010-accepted-the-golden-vector-was-not-a-test-until-it-was-made-one)) |
848864
| 6 | Auth access matrix | **Resolved** — ADR 0011 Accepted |
849865
| 7 | API v1 reconciliation | **Scheduled** — M3 ([D12](DECISIONS.md#d12--api-v1-reconciliation--version-bump-sequenced-to-m3-v10)) |
850866
| 8 | Offline guarantee | **Doc now, code deferred** ([D13](DECISIONS.md#d13--offline-document-guarantees-now-build-no-second-queue)) |
851-
| 9 | Min free turnover artifact | **Deferred** with #5; principle recorded (B5) |
867+
| 9 | Min free turnover artifact | **Open** — no longer *blocked* by #5 (now resolved), but not decided by it either; principle recorded (B5) |
852868
| 10 | Deployment topology | **Doc now** (label honestly), Pi validation M3 ([D14](DECISIONS.md#d14--deployment-topology-labeling)) |
853869
| 11 | Startup phase vocabulary | **Open, likely closeable** — ADR 0018 §1's two additive phases match the source sequence exactly; the dry-check and I/O-check reports map cleanly onto the existing four |
854-
| 12 | Asset vs. position identity | **Open, and live** — the source has a pump removed, rebuilt with new seals, and re-installed *between* the clean-water and seeding phases. Not hypothetical; needs its own ADR |
855-
| 13 | Turnover hash versioning | **Open** (2026-08-04) — fold ADR 0018's manifest additions into `sha256-canon-v1` before the first signed package, or plan a v2. Decide **with** #5 |
870+
| 12 | Asset vs. position identity | **Resolved** (2026-08-04) — an appended `equipment_events` fact + derived re-verify marker; no second identity. Asset-history tracking is a conscious loss ([D27](DECISIONS.md#d27--the-asset-behind-a-position-is-an-appended-event-not-a-second-identity)) |
871+
| 13 | Turnover hash versioning | **Resolved** (2026-08-04) — one fold inside `sha256-canon-v1`, no v2; partial folds forbidden ([D25](DECISIONS.md#d25--d6-amended--the-turnover-manifest-folds-into-sha256-canon-v1-now)). Precondition still live: no signed package may exist when it ships |
856872

857873
**Unresolved for the maintainer to answer eventually** (not blocking M1): #3
858874
(preservation ownership) and #4 (identity scheme) both need a maintainer/domain

‎docs/adr/0010-signatures-and-turnover-package.md‎

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,39 @@
11
# ADR 0010 — Signatures & the turnover package: a signed, frozen snapshot over the append-only ledger
22

3-
**Status:** proposed · **Date:** 2026-07-10
3+
**Status:** **accepted** (2026-08-04) · **Date:** 2026-07-10
4+
5+
> **Accepted 2026-08-04** by maintainer decision, resolving
6+
> [open-questions #5](../open-questions.md) and closing
7+
> [D6](../DECISIONS.md#d6--adr-0010-signatures--turnover-stays-proposed-and-frozen)'s
8+
> freeze. D6 named three gates; here is where each landed:
9+
>
10+
> 1. **Golden-vector / canonicalization test — NOW MET, and it was not before.**
11+
> D6 assumed the fixture was a test. It was a `console.log`: on drift it
12+
> printed one line to a server console (a trailer Pi's, typically unread) and
13+
> kept sealing packages with hashes no independent verifier could reproduce.
14+
> Fixed as part of this acceptance — `verifyGoldenVector()` moved beside the
15+
> serializer, `POST /api/turnover/build` **refuses** on mismatch rather than
16+
> sealing unverifiable evidence, and `scripts/smoke_test.sh` asserts the algo,
17+
> the digest shape, **hash determinism across a rebuild**, and supersession.
18+
> The refusal path was verified by deliberately drifting the vector and
19+
> confirming the build fails. Deliberately *not* a throw at load: a turnover
20+
> canonicalizer must never take down an instance serving QR scans, LOTO
21+
> status, and field checks — the blast radius stays scoped to builds.
22+
> 2. **Required-role semantics — MET by design.**
23+
> [ADR 0018 §9](0018-multi-vendor-startup-evidence-model.md) defines them:
24+
> `required_signature_roles` is template data frozen onto the check, the
25+
> missing-countersignature list is derived, and the roles come from the GC's
26+
> template **never** from whatever the vendor's paper happened to provide.
27+
> 3. **Legal wording (C4) — MET in shipped code.** Both `sign.html` and
28+
> `turnover.html` carry the "records an attestation, not a qualified
29+
> electronic signature" caveat on the page, not only in this ADR.
30+
>
31+
> **What acceptance does NOT do.** It does not perform the D12 contract bump —
32+
> `turnover_packages` and `signatures` stay marked `delta; ADR 0010` in
33+
> [docs/API.md](../API.md) until M3 reconciles the whole collection set. An
34+
> accepted ADR is a settled *design*; the published *contract version* is a
35+
> separate act with its own sequencing. Do not read this Status line as a
36+
> contract guarantee.
437
538
> **The manifest allowlist and canonical form are unfrozen for one fold**
639
> (2026-08-04, DECISIONS **D25** amending D6). ADR 0018 adds eight
@@ -16,13 +49,18 @@
1649
> regenerated as the acceptance test; **partial folds are forbidden**, since
1750
> each addition alone changes the canonical bytes.
1851
>
19-
> **This does not accept ADR 0010.** C4's on-page wording, required-role
20-
> semantics, and the D12 contract bump still gate acceptance.
52+
> ~~**This does not accept ADR 0010.** C4's on-page wording, required-role
53+
> semantics, and the D12 contract bump still gate acceptance.~~
54+
> **Superseded 2026-08-04** — see the acceptance block at the top of this file.
55+
> C4 and required-role semantics are met; the D12 contract bump is explicitly
56+
> *not* a precondition of acceptance and remains M3 work.
2157
2258
**Implementation status (committed + review-hardened, 2026-07-12):** the
2359
migration (`turnover_packages` + `signatures`, `1789000026`), the server
2460
builder/canonicalizer (`pb_hooks/turnover.pb.js` + `turnover_canon.js`, with a
25-
golden-vector self-check on load), the auth helper (`lc-auth.js` +
61+
golden-vector check enforced at build time and asserted by the smoke test —
62+
see the acceptance block; it was load-time-log-only until 2026-08-04), the auth
63+
helper (`lc-auth.js` +
2664
`login.html`), and the signing/turnover pages (`sign.html`, `turnover.html`)
2765
are in the committed baseline and were hardened by a code-review pass
2866
(single scoped `check_items` query, `//`+`/\` open-redirect guard,

0 commit comments

Comments
 (0)