What this file is. A lightweight, append-only log of roadmap, scope,
sequencing, and cut decisions — the questions a future agent should not
relitigate. Newest decisions carry the highest D number.
What this file is NOT. It does not replace or duplicate the ADRs. Structural
decisions (schema shape, enforcement rules, library mechanics) live in
docs/adr/ and are the authority for how something is built. This log records
what we chose to build, in what order, and what we deliberately are not
building, and cross-links the relevant ADR where one exists. Unresolved
architecture questions live in open-questions.md; this
log records the ones that have been decided.
Precedence. When any doc disagrees with reality, the code is truth and the
doc is a bug (the docs-as-code rule in CLAUDE.md). Among docs: CLAUDE.md
holds the constraints, docs/adr/ holds structural decisions, this file holds
scope/sequencing decisions, docs/ROADMAP.md holds the sequence.
Decisions D1–D19 were made in a planning session with the maintainer on
2026-07-21. They set up the roadmap in docs/ROADMAP.md and the task files in
docs/tasks/.
Decision. Milestones are named by goal, each mapped to a semver tag. The
first tagged release is v0.9.0 (the product is feature-broad but has never
been tagged, hardened, or validated). v1.0.0 is the "a stranger can adopt and
self-host this" release. Milestones: M1 Harden & Reconcile (v0.9.0) → M2
Shutdown Runbooks + Punch List (v0.10.0) → M3 Stranger-Ready (v1.0.0), then
post-1.0 planned modules and a Later/Ideas parking lot.
Rationale. Calling the first milestone "v0.1" would misrepresent a nearly
feature-complete product; the maintainer already proposed v0.9.0 in the auth
work (SESSION-NOTES). Naming by goal keeps definitions-of-done testable.
Rejected. Starting the version line at v0.1.0.
Decision. No new feature work lands before M1 (Harden & Reconcile) is done. M1 = expand the smoke test, add a browser/UI test, reconcile the docs to a single source of truth, and validate deployment — gated by CI. Rationale. The maintainer will not deploy on a real job until the existing breadth is trustworthy. The features exist; the trust plumbing does not. Rejected. Shipping features (Phase 4) before hardening.
Decision. This file logs roadmap/scope/sequencing/cut decisions only, and
cross-links ADRs rather than duplicating them. The daily-driver workflow reads
CLAUDE.md, docs/ROADMAP.md, and this file.
Rationale. A second heavyweight decision store would drift against
docs/adr/. Single source of truth (M1 goal) applies to our own process too.
Decision. The root ARCHITECTURE.md stays the single authoritative
architecture document. We do not create docs/ARCHITECTURE.md.
Rationale. A parallel architecture doc is exactly the drift M1 exists to
eliminate. ROADMAP and CLAUDE point at the root file.
Decision. AGENTS.md (currently a full, and now stale, parallel copy of
CLAUDE.md for Codex sessions) is reduced to a short pointer that directs any
agent to CLAUDE.md as the single source, keeping only genuinely
Codex-specific notes if any remain.
Rationale. Two full copies of the constraints guarantee drift; AGENTS.md
already lags (it still calls Phase 6 a LoopCheck module, still says "sixteen
collections", references a .Codex/ path). Executed as a task in M1.
Cross-ref. Task 010.
Decision. The shipped signature/turnover code keeps running, but ADR 0010 stays Proposed; no new signature/turnover work happens this cycle. The open gates are recorded so a future agent does not casually "finish" it: golden-vector/canonicalization tests, required-role semantics, and legal wording ("not a qualified e-signature"). Revisit before external users. Rationale. Accepting it now hardens hash/attestation semantics that are hard to change once real records exist; there is no user pressure to decide. Cross-ref. ADR 0010; open-questions #5 and #9 stay open.
Decision. Both ADRs are Accepted and their schema shipped, but no UI is built for either until a real job requests it. The schema stays frozen (maintenance-only). They are not roadmap milestones. Rationale. Accepted-ADR-plus-shipped-schema-plus-no-UI is latent scope that dilutes focus; parking it explicitly stops an agent from building it unprompted. Preservation additionally has an unresolved ownership boundary vs TrenchNote. Cross-ref. ADR 0015, ADR 0017; open-questions #3 (preservation ownership).
Decision. The pending→resolve execution flow (ADR 0012) and the tank leak-test UI stay unbuilt until a real tank/duration-test job needs them. Schema stays frozen. The open→resolve pattern remains a general-purpose, accepted part of the schema. Rationale. Same as D7 — proven in schema, no demand yet. Cross-ref. ADR 0012.
Decision. The Service Cutover Tracker remains LoopCheck-authoritative and fully supported. Its semantics are frozen: no new service phases, no customer-notification automation, no second cross-product master. A future move to a sibling requires feature parity there plus a lossless migration; no dual-write. Rationale. It works and has real value; no sibling is ready to take it; moving it now would strand users and evidence. Cross-ref. ADR 0006; open-questions #1.
Decision. Linear/segment acceptance is not LoopCheck's bounded context;
it belongs to MainLine as a future module (maintainer decision 2026-07-18,
ADR 0014 superseded). The shipped LoopCheck segment schema slice (migrations
…23–…25, …33) stays frozen, maintenance-only. No segment execution UI,
import, board, QR route, or acceptance package is built in LoopCheck. Several
docs still incorrectly name "LineCheck" as the owner; correcting them is an M1
task.
Rationale. MainLine already models alignments, stations, and append-only
test ledgers with derived acceptance. LineCheck is archived.
Cross-ref. ADR 0014 (superseded); task 010.
Decision. Do not choose a cross-product portable identity/deployment/project
id scheme yet. The roadmap flags it as the gate before the first real
cross-product handoff or import.
Rationale. No real users and no cross-product handoff exists; choosing now
mints permanent identifiers that cannot be taken back.
Cross-ref. open-questions #4; docs/ecosystem-contracts.md (stays PROPOSED,
non-binding).
Decision. The formal API contract version bump for the added collections and
the auth/PII semantics is done in M3 (Stranger-Ready), not M1. M1 only makes
the documentation of the current collection set truthful.
Rationale. It is the backbone of stable release discipline and the sidecar
contract, but it needs care and is not a field-trust blocker. docs/API.md
already documents the delta honestly.
Cross-ref. ADR 0003; open-questions #7.
Decision. Publish honest, per-workflow offline guarantees as part of the M1 docs reconciliation (only LOTO is durable; batch-notice is tab-local; everything else is online-required). Do not build a general offline app-shell or a second durable write queue this cycle. Rationale. A half-built offline layer is more dangerous than honest "online-required" labeling; a durable queue needs an idempotency decision first. Cross-ref. ADR 0007; open-questions #8.
Decision. Label LAN and single-VPS as supported documented options and Pi + primary/replica as unvalidated until exercised. The M1 deploy validation proves the ARM binary path on a cloud VM; real Pi validation is an M3 item. Rationale. Cheap honesty fix; avoids presenting aspirational topology as tested. Cross-ref. open-questions #10; ROADMAP Raspberry Pi track.
Decision. The automated gate is both a hardened .githooks/pre-push
local gate (fast, offline) and a GitHub Actions workflow (remote backstop) that
fetches PocketBase and runs the smoke + browser tests.
Cross-ref. task 050.
Decision. The browser/UI smoke test may use a headless-browser tool
(Playwright is the default choice) as a dev-only, git-ignored dependency,
invoked from scripts/, never vendored into pb_public/, never part of the
app runtime, and never affecting the no-build-step field tier. If any objection
arises to introducing a Node/dev dependency at all, the task stops and asks the
maintainer before proceeding.
Rationale. Hard constraint #3 (no build step) governs the app, not dev
tooling; but the boundary must be explicit so the tooling never leaks into the
shipped product.
Cross-ref. task 040.
Decision. A project/system Punch List report (print-friendly board + free
CSV, across A/B/C severity) is adopted into M2. The implementing task must first
confirm the roll-up/export does not already exist before building it.
Rationale. It directly serves LoopCheck's second core question ("what punch
items are blocking this system?") and is high-value per effort. It must stay
free-tier (never paywalled).
Gap confirmed, built (2026-08-02). system.html and project.html carried
open A/B/C counts only — no item listing, no print, no export — and
readiness.html did not read punch_items at all, so all three deliverables
were missing. Built as a new pb_public/punchlist.html taking ?project= or
?system=, rather than a report mode bolted onto two navigation pages.
Cross-ref. task 090 (DONE); ARCHITECTURE.md UI section.
- B2 "Walk the system" batch-check mode — PARKED to Later/Ideas. Useful, but overlaps the Phase 4 glove-sized UI ergonomics; revisit after M2 proves the pattern.
- B3 Field-tier offline app-shell (service worker) — REJECTED this cycle, parked behind the offline-guarantee decision (D13). Half-built offline is worse than honest online-required; TrenchNote hit painful SW/IndexedDB issues.
- B4 Ledger-integrity / provenance view — PARKED. Overlaps the frozen signatures/hash work (D6); building it pre-commits hash semantics.
- B5 Free JSON turnover manifest export — ADOPTED in principle but DEFERRED with ADR 0010 (D6); it is the open-core keystone (paid PDF becomes presentation-only) but is coupled to the frozen turnover work. Rationale. Recorded so future ideation passes don't re-surface them as new.
Decision. M3/v1.0 requires all of: a getting-started onboarding path with screenshots; stable release discipline (semver tags, CHANGELOG, documented upgrade path, API v1 reconciled and versioned per D12); Raspberry Pi validation on real hardware; and an accessibility + field-device + offline-honesty polish pass across all pages (not just LOTO). Rationale. These are the gaps between "the maintainer can run it" and "a stranger can adopt and self-host it."
Decisions D20–D24 come from reviewing a full compiled vendor startup report package (equipment vendor forms, field service reports, I/O check reports, clean water test checklists) from a multi-vendor plant startup against what LoopCheck can currently record. Ten gaps came out of that review; the structural answers are in ADR 0018 (proposed) and the sequenced work is the M4–M6 track in ROADMAP.md.
Decision. The ten field observations become M4 (v1.1.0), M5
(v1.2.0), and M6 (v1.3.0), sequenced after M3/v1.0 — they do not
preempt or reorder M1–M3. Priority within the track follows observed pain:
staged per-tag-per-phase lifecycle, ball-in-court punch items, and vendor
evidence first (M4); explicit line resolution, typed values, and tag validation
second (M5); instrument configuration, signature QC, phase gates, and
continuation chains last (M6).
Rationale. Three reasons, in order of weight. (1) D2
already fixed trust-before-features and M1–M3 are the trust work. (2) Three of
these items make breaking changes to v1-published collections
(punch_items.assigned_party most sharply), and
D12 sequences
the API v1 reconciliation to M3 — doing the reconciliation first makes the
breaks clean instead of compounding. (3) v1.0's bar
(D19) is "a stranger can adopt this," and a
stranger adopting a schema mid-revision is the worse outcome.
Rejected. Interleaving M4 items into M2/M3 to get the vendor evidence
ledger sooner — it would put a breaking enum change ahead of the contract
reconciliation that makes it survivable.
Cross-ref. ADR 0018; ROADMAP "The multi-vendor startup track".
Decision. D7 parked the vendor field service visit UI (ADR 0017) and D8 parked the open→resolve UI (ADR 0012) until a real job asked for them. A real multi-vendor startup asked for both. Both are unparked and scheduled: vendor visits as M4.3, the continuation-chain half of open→resolve as M6.4. The rest of D7 and D8 stands — equipment preservation (ADR 0015) stays parked (its ownership boundary vs TrenchNote is still open, open-questions #3), and the tank leak-test execution UI stays parked until a real tank/duration-test job needs it. Rationale. D7/D8 were not "never"; they named a trigger condition precisely so a future session would know when to build. Amending them on the stated trigger is the mechanism working, not a relitigation. Cross-ref. ADR 0017, ADR 0012, ADR 0018 §§5, 10.
Decision. A checklist line may still be submitted unanswered — the record of a crew getting pulled off is worth more than no record. But a check may not be signed, and a scope may not be frozen into a turnover package, while any included line carries a blank result. The gate lives at signature and freeze, not at capture. Rationale. This is the one requirement that pulls directly against a shipped, deliberate behavior (ARCHITECTURE §6 records unanswered lines on purpose). Both concerns are legitimate and they apply at different moments: hard constraint #1 owns the capture moment; the evidence promise owns the deliverable. Blocking submission would trade a recoverable partial record for no record at all — the worse failure on a dead-zone phone. Rejected. Requiring every line before submit; leaving blanks legal all the way into a frozen package. Cross-ref. ADR 0018 §4; ROADMAP M5.1.
Decision. Three requirements are answered inside existing constraints rather than by relaxing them:
- Typed values with expected ranges are metadata on the existing
valueresponse type (expected_min/expected_max/expected_text/comparisonontemplate_items, frozen ontocheck_items), not a fifth response type. Hard constraint #5 stands. - Per-project configurable phases are configured by the project's template
library, not by a per-project
phasescollection. A stored phase map is the same driftARCHITECTURE.md§7 already rejects for required phases; the shared enum gainswet_commissioningandprocess_seedingadditively. - Vendor participation is never solved by giving vendors accounts, a portal, or an integration. Vendors use their own paper; LoopCheck is the GC's master ledger that indexes it. ADR 0017 already rejected the adjacent vendor-scheduling/contract/billing scope. Rationale. Each of the three had an obvious constraint-relaxing answer and a slightly less obvious constraint-preserving one that is as good or better. Recording the reasoning stops a future session from taking the easy version. Cross-ref. ADR 0018 §§1, 5, 7; CLAUDE.md constraints #3, #5.
Decision. A phase-entry prerequisite checklist is an ordinary check with a
system subject against a template flagged gate_for_phase. An unsatisfied gate
shows prominently and does not prevent anyone from running the phase.
Rationale. ADR 0007's principle — the app records, it never authorizes —
generalizes beyond safety. A crew that runs a phase over an unsatisfied gate
has made a decision, and the ledger's job is to record visibly that they did.
A hard block also strands the field tier the moment a gate item is owed by
someone who is not on site, and it is the first step toward the workflow-
engine scope LoopCheck refuses to become.
Rejected. Hard-blocking phase entry; a dedicated gates collection (a
gate is a check).
Cross-ref. ADR 0018 §12; ADR 0007.
Decision. ADR 0018's eight hash-affecting manifest additions land in one
pass, into sha256-canon-v1, before any real package is signed — not behind a
sha256-canon-v2. D6 is amended, not reversed: the canonical form and
manifest allowlist are unfrozen for this fold, while ADR 0010's acceptance is a
separate act on separate gates (the "not a qualified e-signature" wording C4,
required-role semantics, and the D12 contract bump). ADR 0010 stays
Proposed with a completed allowlist. Superseded the same day by
D26:
ADR 0010 was accepted — C4 and required-role semantics were already met, and
D12 is explicitly not a precondition. The fold decision is unaffected; what
changed is that D26 also made the golden vector enforceable at the build route,
so this entry's precondition is now checked by code rather than by a reviewer
remembering. Partial folds are forbidden — each addition alone
changes the canonical bytes, so piecemeal shipping means several silent breaks
instead of one deliberate one. The golden vector is regenerated as the
acceptance test.
Precondition. The builder and canonicalizer are already shipped and running,
so this is a code change. Confirm no turnover_packages record carrying real
signatures exists on any live instance before the fold lands. If one does, the
decision inverts and the additions go behind sha256-canon-v2; a signed package
that stops verifying is the outcome the ADR exists to prevent. Test packages are
discarded, not migrated.
Rationale. A hash version exists to protect history, and there is no history
to protect. A v2 carrying additions no v1 package ever saw would leave a version
boundary that records nothing. D6's own reasoning — that hash semantics are hard
to change once real records exist — argues for completing the allowlist now,
because the allowlist is exactly the thing that becomes unchangeable. D6
conflated the canonical form with acceptance of the ADR; they are separable, and
only one of them gets more expensive with delay.
Rejected. Accepting ADR 0010 wholesale (its legal and contract gates are
untouched by this work); minting sha256-canon-v2 (no history to protect).
Cross-ref. ADR 0018 §15 and open question 5; ADR 0010; D6, D12.
Decision. ADR 0010 (Signatures & the turnover package) is Accepted
(2026-08-04), closing D6's
freeze and resolving open question #5. Acceptance covers the design: the
frozen snapshot, the canonical hash, supersession, and the attestation model.
It explicitly does not perform the D12 contract bump — turnover_packages
and signatures stay delta; ADR 0010 in docs/API.md until M3.
What the gates turned out to be. D6 named three. Two were already met and
merely unrecorded: the "not a qualified e-signature" wording ships on both
sign.html and turnover.html, and required-role semantics are ADR 0018 §9.
The third was not met, and D6 assumed it was — the golden vector was a
console.log at hook load, not a test. On canonicalizer drift it printed one
line to a server console (on a trailer Pi, an unread one) and kept sealing
packages with hashes no independent verifier could reproduce. That is worse than
having no seal: an unverifiable hash on a signed turnover package looks like
evidence and is not.
Fixed as part of accepting, not deferred. verifyGoldenVector() moved beside
the serializer in turnover_canon.js; POST /api/turnover/build refuses
with an explicit message on mismatch rather than sealing unverifiable evidence;
scripts/smoke_test.sh asserts the pinned algo, the digest shape, hash
determinism across a rebuild of an unchanged scope, and supersession (89 → 94
assertions). The refusal path was verified by deliberately drifting the vector
and confirming the build fails.
Deliberately not a throw at load. A drifted turnover canonicalizer must
never take down an instance whose first job is QR scans, LOTO status, and field
checks. "Never cripple the field tier" outranks failing fast about an office
deliverable, so the blast radius stays scoped to builds. This is the same
asymmetry ADR 0007 applies to LOTO, pointed the other way.
Rationale. This work is test hardening, not feature work, so it does not
violate D2 — it is exactly M1's
goal (prove the invariants) applied to the one invariant that had been asserted
in prose and nowhere else.
Cross-ref. ADR 0010; ADR 0018 §§9, 15; D6, D12, D25; open questions #5, #13.
Decision. Open question #12 (asset vs. position) is resolved by a small
append-only equipment_events ledger on the tag — kind (replaced /
rebuilt / reinstalled / relocated / nameplate_corrected), nullable
serial_before / serial_after, occurred-at, who, note — plus a derived
re-verify marker: any phase whose standing rests entirely on checks older than
the tag's latest event renders as "passed before the {date} {kind} event." A
marker, never a downgrade; the pass stays in the ledger. No equipment_assets
collection, no second identity, no identity scheme. It is a scope item inside
M5.3, not its own milestone or ADR (ADR 0018 §16).
What decided it. The obvious design — an asset keyed on serial number —
fails on contact with the source: actuator serial number reads "N/A" on all
six valve reports. A whole equipment class has no identity to key on. The real
requirement was never asset lifecycle; it is "a phase passed by a machine that
was later pulled apart must not read as current," which is a derivation over
dates. The source has the case live: a pump found with a frozen impeller,
removed, rebuilt, and re-installed between the clean-water and seeding
phases.
What this gives up, deliberately. You cannot ask "where has serial X been
installed over time," or follow one machine between positions. That is asset
management and belongs in an ERP or a materials product, consistent with the
standing refusal to become an ERP. Recorded as a conscious trade; if spares
pools or rotating equipment ever matter, these events are the substrate the
heavier model would be built on.
Rejected. An equipment_assets collection with an install/remove ledger
(holed on day one by unserialised assets, and it would touch the identity of
every record pointing at a tag); a stored "needs re-verification" flag (status
that drifts).
Cross-ref. ADR 0018 §§11, 16; open question #12; ROADMAP M5.3.
Decision. Both are Accepted (2026-08-04). ADR 0018 (multi-vendor
startup evidence model) settles the design behind the post-1.0 M4–M6 track after
four readings of the source package and eighteen of its twenty-two review
comments decided. ADR 0019 (shutdown runbook execution) settles Phase 4,
from a domain interview with the maintainer. Build tasks 100–140 are queued
against ADR 0019 only.
What acceptance does and does not authorize. ADR 0019 authorizes building —
it is the current milestone. ADR 0018 authorizes nothing to be built: M4–M6
remain post-1.0 (D20),
and neither acceptance performs the D12 API contract bump. An accepted ADR is a
settled design, not a published contract version and not a licence to migrate.
Why accept 0018 now rather than at M4. It was proposed while carrying
eighteen resolved comments, and ADR 0019 already cites it as settled (its
not_performed reason vocabulary). A stale status line on a heavily-decided ADR
is a live trap: a future agent reads "proposed" and either declines to build
against it or re-opens questions already answered. Acceptance consolidates the
work rather than authorizing any of it.
Left open deliberately. ADR 0018: review comments R14, R17, R19, R20 and its
own questions 1 (phase vocabulary) and 2 (deferred at check vs. line level) —
all additive QC refinements or build-time choices for the milestone that
implements them. ADR 0019: questions 1 (trim the template side), 2 (subject
scope) and 4 (T-0 anchoring); question 1 is the only one that changes the
migration and task 100 calls it out first.
Resolved by the pairing. ADR 0019's question 3 asked whether a skipped
event kind should exist or reuse ADR 0018 §4's not_performed vocabulary,
noting the coupling risk while 0018 was Proposed. Accepting both removes the
risk: there is no skipped kind — one vocabulary, not two ways to say the
same thing.
Cross-ref. ADR 0018, ADR 0019; D20, D12; tasks 080, 100–140.