Skip to content

Commit feec46c

Browse files
committed
feat(schema): shutdown runbook collections (ADR 0019)
Migration S creates the seven collections ADR 0019 defines, and nothing else. The runbook JSON stays unloaded (task 110) and no UI is built (tasks 120-140). Committed schema goes 24 -> 31 application collections. Open question 1 is settled in favour of the ADR's own recommendation: shutdown_contingencies stays its OWN collection rather than folding into shutdown_template_steps behind a discriminator. A contingency has no seq position, no offset, no duration and no hold point, so folding would leave half the columns null for half the rows, and the two are read at different moments -- a step list is walked, a contingency is consulted when something has already gone wrong. Shape of the split: - Template side is project-agnostic, so responsible_role_key and tag_ref_hint are TEXT. The instance side resolves them to real shutdown_roles and tags relations at planning time. - shutdown_step_events is append-only and PUBLIC to create: planning is an office act, but logging what happened is a gloved hand at 2 AM in the rain (hard constraint #1). A login wall there is the one thing guaranteed to get the ledger written afterwards from memory. - Because the ledger is append-only a malformed record is permanent, so the createRule is the gate, not the UI: contingency_invoked must name a contingency, not_performed must carry a reason from ADR 0018 §4's vocabulary. There is no `skipped` kind. - No status or current_step column anywhere (constraint #4). client_token is deliberately NOT `hidden`, which is where it differs from calibrations.client_token: a hidden field is stripped from client input, so a required hidden field can never be satisfied by a client POST at all. Calibrations only gets away with it because a privileged pb_hooks route writes those records; these events are posted directly by the accountless field client. Verified: applies to a fresh DB, `migrate down` removes all seven cleanly and re-applies; smoke test 94 -> 112 assertions, green; browser field-path suite still 7/7. Docs: ARCHITECTURE.md §2 (tree, mutability table, two new rejected-list entries for go/no-go authority and stored status), collection count and v1/delta split updated across CLAUDE.md, ROADMAP, current-state, architecture-status and open-questions. Task 100 DONE; M2 migrations checkbox ticked.
1 parent 3b69640 commit feec46c

9 files changed

Lines changed: 560 additions & 30 deletions

‎ARCHITECTURE.md‎

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ the same origin it was served from. PocketBase serves `pb_public/`, applies
2121

2222
## 2. Data model
2323

24-
Twenty-four committed application collections. Relations are drawn
24+
Thirty-one committed application collections. Relations are drawn
2525
`child → parent`; PocketBase system/auth collections are not counted.
2626

2727
```
@@ -58,6 +58,15 @@ loto_events (project, tag; applies_to → loto_events) APPEND-ONL
5858
5959
turnover_packages (project, system?; supersedes → turnover_packages) APPEND-ONLY frozen snapshot
6060
signatures (subject_collection, subject_id → package XOR check) APPEND-ONLY attestation
61+
62+
shutdown_templates (library header) the runbook, project-agnostic
63+
├── shutdown_template_steps (template) ordered steps, role/tag as TEXT
64+
└── shutdown_contingencies (template) trigger/action pairs, not steps
65+
shutdown_roles (project) per-project role + contact registry
66+
67+
shutdowns (project, system?, template?) one planned outage; editable plan
68+
└── shutdown_steps (shutdown, responsible_role?, tag?) instantiated list; editable
69+
└── shutdown_step_events (shutdown, step?, contingency?) APPEND-ONLY execution ledger
6170
```
6271

6372
### Collections
@@ -88,6 +97,13 @@ signatures (subject_collection, subject_id → package XOR check)
8897
| `service_visits` | Manufacturer-representative visits against a tag XOR system, with report evidence. | **append-only** |
8998
| `turnover_packages` | A frozen, sealed snapshot of a scope's checkout — the signed turnover deliverable (built server-side). | **append-only** |
9099
| `signatures` | A recorded attestation (name/role/statement) bound to a package or check by content hash. | **append-only** |
100+
| `shutdown_templates` | A named shutdown runbook in the library (seeded from `seed/`). | editable |
101+
| `shutdown_template_steps` | Ordered library steps: group, offsets, hold point, role/tag as project-agnostic **text**. | editable |
102+
| `shutdown_contingencies` | Library trigger/action pairs. Not steps — no seq position, offset, or hold point. | editable |
103+
| `shutdown_roles` | Per-project role registry: `role_key`, title, contact, `notify_on` (**stored, never acted on**). | editable |
104+
| `shutdowns` | One planned outage. `planned_window_start` is T-0. A planning record, deliberately **not** append-only. | editable |
105+
| `shutdown_steps` | The instantiated step list; role and tag are real **relations** here. Editable while planning. | editable |
106+
| `shutdown_step_events` | What actually happened: started/completed/held/released/aborted/resumed/not_performed/contingency_invoked, with frozen step text. | **append-only** |
91107

92108
### Field notes worth knowing
93109

@@ -544,9 +560,24 @@ re-imports are safe.
544560
hook serves the file in place instead.
545561
- **Global tag-number uniqueness.** Rejected — real programs run multiple plants
546562
that reuse tag numbers. Uniqueness is per project.
547-
- **Building shutdown/compliance collections now.** Rejected as building ahead.
548-
The domain content lives in `seed/` so it is not lost; the collections and
549-
UIs come in Phases 4–5.
563+
- **Building shutdown/compliance collections now.** Rejected as building ahead
564+
*while Phases 1–3 were the work*. The domain content lived in `seed/` so it
565+
was not lost. **Superseded for the shutdown half on 2026-09-07**: Phase 4 / M2
566+
arrived, ADR 0019 was accepted, and migration S created the seven shutdown
567+
collections (task `100`). The compliance half still stands — the Phase 5
568+
templates remain data with no execution UI.
569+
- **Anything that makes the shutdown module part of the go/no-go decision.**
570+
Rejected permanently (ADR 0019, the LOTO doctrine of ADR 0007 applied to the
571+
second module that could plausibly claim authority): no interlocks, no
572+
permission-to-proceed, no "safe to cut" indicator, no green all-clear, and no
573+
rule that blocks logging past an unsatisfied hold point. A hold point warns
574+
and enters the derived standing; it never stands in the doorway at 2 AM when
575+
the authorizer is unreachable. The app records; the written energy-control
576+
procedure and the people on site authorize. The app must never be usable as
577+
evidence that it is safe to cut a live main.
578+
- **A `status` or `current_step` column on `shutdowns`.** Rejected — hard
579+
constraint #4. A step's state is its latest event, a shutdown's standing is a
580+
fold over its events, and schedule variance is computed at render.
550581
- **Seeding the shutdown runbook as flat checklist templates.** Rejected — a
551582
flattened copy loses the offsets, hold points, and roles, and would diverge
552583
from the real runbook in `seed/`. One representation, loaded when Phase 4
@@ -742,11 +773,14 @@ Mechanics worth knowing:
742773
`subject_kind='service'` templates (ADR 0006).
743774
- `recurrence` blank = a one-time checkout checklist; `daily` / `per_shift` /
744775
`per_event` are the compliance cadences (Phase 5).
745-
- **Not loaded:** `seed/shutdown_runbook_tie_in.json`. The runbook's structure
746-
(time offsets, hold points, roles with notification triggers) cannot be
747-
represented as a flat checklist without losing what makes it a runbook. It
748-
stays as captured domain knowledge until Phase 4 builds real shutdown
749-
collections.
776+
- **Not loaded (yet):** `seed/shutdown_runbook_tie_in.json`. The runbook's
777+
structure (time offsets, hold points, roles with notification triggers) cannot
778+
be represented as a flat checklist without losing what makes it a runbook —
779+
which is why it was never flattened into `checklist_templates`. The
780+
collections that *can* hold it now exist (migration S, ADR 0019), so the
781+
condition this note set has been met; the loader that reads the JSON into
782+
them is task `110` and has not landed. Until it does, the file remains
783+
captured domain knowledge and the shutdown collections are empty.
750784

751785
*(Resolved 2026-07-09 — this section previously documented a schema divergence
752786
between the migration seed and `seed/`; the migrations were reconciled to the

‎CLAUDE.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,8 @@ block (grey).
266266
- Design tokens in `app.css`: one action accent (safety orange), system fonts
267267
for zero webfont bytes, monospace for tag numbers, severity color language.
268268
- **The collections are a published API contract** (`docs/API.md`, ADR 0003).
269-
Sixteen are published as **v1**; the committed schema now has **24**, the
270-
extra eight marked *delta* pending a contract version bump (an M3 decision).
269+
Sixteen are published as **v1**; the committed schema now has **31**, the
270+
extra fifteen marked *delta* pending a contract version bump (an M3 decision).
271271
Breaking changes to any of their shapes, semantics, or rules need an ADR and a
272272
contract version bump — not just a migration. Premium is a sidecar on that
273273
contract; capabilities it needs land in core first.

‎docs/ROADMAP.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ first ([ADR 0003](adr/0003-core-premium-sidecar-boundary.md)).
4747
## Shipped (the product today)
4848

4949
Feature-broad and working; never yet tagged, hardened, or validated. A fresh
50-
database from committed migrations has **24 application collections** plus
50+
database from committed migrations has **31 application collections** plus
5151
PocketBase's `users`. Authoritative descriptions live in `ARCHITECTURE.md`;
5252
this is the index.
5353

@@ -131,8 +131,10 @@ validated — all gated by CI. No new features
131131
- [x] `grep -ri "linecheck" docs/ *.md` returns only historical/superseded
132132
references explicitly labeled as such (ADR 0014's own record); no doc names
133133
LineCheck as the *current or future owner* of any scope.
134-
- [x] Every doc that states a collection count states **24** (application
135-
collections); none says 16 or 18 as the current total.
134+
- [x] Every doc that states a collection count states the same current total
135+
(application collections); none says 16 or 18. That total was **24** when
136+
M1 closed and is **31** since migration S added the seven shutdown
137+
collections (ADR 0019, task 100).
136138
- [x] `scripts/smoke_test.sh` passes with new assertions for: `segments` in the
137139
existence loop, a tank leak-test seed/workflow assertion, and an
138140
open→resolve behavior assertion; run count is documented in the script
@@ -194,7 +196,7 @@ shutdown runbook execution UI — plus the on-mission Punch List report.
194196
execution model **before** the migration (the design task's output).
195197
*([ADR 0019](adr/0019-shutdown-runbook-execution.md), accepted
196198
2026-08-04.)*
197-
- [ ] Migrations create the shutdown collections; a fresh DB + smoke test pass.
199+
- [x] Migrations create the shutdown collections; a fresh DB + smoke test pass.
198200
- [ ] An operator can execute the seeded tie-in runbook end to end from the UI,
199201
producing append-only step records; executed steps are never edited.
200202
- [x] The Punch List report renders print-friendly and exports CSV for a system

‎docs/architecture-status.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ implementation and decision state differ.
1818
| Record locking/supersession | `CURRENT` for immutable checks; `PROPOSED` for packages | Check records are born immutable; warranty claims freeze on close. | Frozen packages supersede rather than update; signatures do not carry forward. | Historical signoff can be misread as current. | Define current-package derivation, voids, and supersession UX. |
1919
| Evidence hashing | `PROPOSED`, `UNKNOWN` active work | No committed hash contract; staged canonicalization/hook files exist. | Versioned canonical bytes, file inclusion policy, SHA-256 identifier, golden vectors. | A non-reproducible hash creates misleading assurance. | Review allowlists, Unicode/time/number rules, file hashing, and test vectors. |
2020
| PDF generation | `CURRENT` browser print; `DECIDED` paid boundary | Free print-friendly HTML; no committed PDF renderer. | Basic record/index remains free; advanced compiled binder may run in paid sidecar. | Paywalling evidence would violate constraints; server PDF may burden Pi. | Define minimum free turnover artifact and whether deterministic core PDF is required. |
21-
| API versioning | `DECIDED`, `CURRENT` drift | `docs/API.md` calls 16 collections v1; committed schema has **24** (16 v1 + 8 delta) and selective auth. Auth/PII semantics are settled (ADR 0011); the collection-set version bump is not. | Reconcile contract, define additive vs breaking policy, publish compatibility fixtures. | Existing clients/sidecar may silently mis-handle rules or fields. | Decide the version bump for the 8 delta collections — sequenced to M3 (D12). |
21+
| API versioning | `DECIDED`, `CURRENT` drift | `docs/API.md` calls 16 collections v1; committed schema has **31** (16 v1 + 15 delta) and selective auth. Auth/PII semantics are settled (ADR 0011); the collection-set version bump is not. | Reconcile contract, define additive vs breaking policy, publish compatibility fixtures. | Existing clients/sidecar may silently mis-handle rules or fields. | Decide the version bump for the 8 delta collections — sequenced to M3 (D12). |
2222
| Cross-product handoffs | `PROPOSED` | No implemented manifest/event exchange found. | Versioned exports/events with provenance; no direct DB coupling. | Premature shapes create permanent identifiers and duplicate masters. | Approve bounded contexts, stable ids, first concrete handoff, and ownership. |
2323
| Paid/core boundary | `DECIDED`, `CURRENT` sidecar | ADR 0003; LoopCheck-Lookahead has separate DB and read-only HTTP consumption. | Core owns field facts/basic export; paid owns optional aggregation and compilation. | High legal/product risk if premium gains private access or required field features. | Clarify whether sidecar service-account role needs mechanical read-only enforcement. |
2424
| Service-cutover ownership | `RESOLVED` — stays in LoopCheck (2026-07-18) | Complete LoopCheck workflow from notice through restoration. | **Settled:** service cutover is LoopCheck's permanently; it does not move to a sibling. | — | **Resolved** — [DECISIONS.md D9](DECISIONS.md#d9--service-cutover-stays-in-loopcheck-semantics-frozen); no migration planned. |

‎docs/current-state.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ workflow**. See migrations `1789000023`, `1789000024`, `1789000025`,
6363

6464
## Current collections
6565

66-
**CURRENT (revised 2026-07-28).** A fresh database from committed migrations
67-
contains **24 application collections**, excluding PocketBase system/auth
66+
**CURRENT (revised 2026-09-07).** A fresh database from committed migrations
67+
contains **31 application collections**, excluding PocketBase system/auth
6868
collections (`users` is the auth collection and is additional):
6969

7070
- Project/equipment: `projects`, `systems`, `tags`, `segments`
@@ -78,10 +78,13 @@ collections (`users` is the auth collection and is additional):
7878
`calibration_points`
7979
- Vendor service visits: `service_visits`
8080
- Signatures and turnover: `turnover_packages`, `signatures`
81+
- Shutdown runbooks (ADR 0019, schema only — no UI yet): `shutdown_templates`,
82+
`shutdown_template_steps`, `shutdown_contingencies`, `shutdown_roles`,
83+
`shutdowns`, `shutdown_steps`, `shutdown_step_events`
8184

8285
The authoritative schema is the ordered migration set in `pb_migrations/`; the
8386
count above is verifiable with `grep -c "new Collection(" pb_migrations/*.js`.
84-
Of these, **16 are published as API contract v1 and 8 are marked delta**
87+
Of these, **16 are published as API contract v1 and 15 are marked delta**
8588
pending a version bump — that bump is a deliberate M3 decision, not an
8689
oversight. See [`API.md`](API.md).
8790

@@ -173,8 +176,9 @@ PII/auth boundaries, and checks the `/t/` and `/s/` routes. It asserts the
173176
ADR 0011 access matrix across all three tiers — guest denial,
174177
authenticated-user reads/writes, the user-vs-superuser delete boundary,
175178
segment-create in both directions, and the field-level PII back-relation
176-
expand. As of the 2026-07-28 revision it also covers all 24 collections in the
177-
existence loop, the tank leak-test seed, the ADR 0012 open→resolve pair
179+
expand. As of the 2026-09-07 revision it also covers all 31 collections in the
180+
existence loop, the shutdown planning/execution auth asymmetry and its
181+
append-only event shape rules (ADR 0019), the tank leak-test seed, the ADR 0012 open→resolve pair
178182
(asserting the open record is **not** mutated by its resolution), and the
179183
newer collections' contract rules — capture-route-only creates for
180184
`calibrations`/`calibration_points`, the auth boundary on `test_standards`, and

‎docs/open-questions.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ safe.
116116
## 7. How should API v1 be reconciled?
117117

118118
- **Why it matters:** published contract v1 originally covered sixteen broadly
119-
permissive collections; committed code has **24** (16 v1 + 8 delta) and
119+
permissive collections; committed code has **31** (16 v1 + 15 delta) and
120120
selective auth. The API document now exposes that delta but cannot approve its
121121
own version. The auth/PII half is settled (ADR 0011, Accepted); what remains
122122
open is the **collection-set** version bump, sequenced to M3 (D12).

‎docs/tasks/100-shutdown-collections-migration.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 100 — Shutdown collections migration (schema only, no UI)
22

3-
Status: TODO
3+
Status: DONE (2026-09-07 — migration `1789000034_migration_s_shutdowns.js`; open question 1 settled as the ADR recommended, contingencies stay a separate collection; smoke test 94 → 112 assertions, green)
44
Milestone: M2 Shutdown Runbooks + Punch List (v0.10.0)
55
Depends on: 080 (ADR 0019, accepted 2026-08-04)
66

@@ -103,17 +103,17 @@ variant**): `conditions_unavailable` · `access_blocked` ·
103103

104104
## Acceptance criteria
105105

106-
- [ ] Open question 1 settled and the choice stated in the commit message.
107-
- [ ] The migration applies to a **fresh** database (delete `pb_data/`, run
106+
- [x] Open question 1 settled and the choice stated in the commit message.
107+
- [x] The migration applies to a **fresh** database (delete `pb_data/`, run
108108
`./pocketbase serve`) and `migrate down` rolls it back cleanly.
109-
- [ ] `scripts/smoke_test.sh` gains assertions, and its header assertion count
109+
- [x] `scripts/smoke_test.sh` gains assertions, and its header assertion count
110110
is updated: all seven collections exist; a `shutdown_step_events` create
111111
succeeds **without a token**; a PATCH and a DELETE of one are refused; a
112112
`contingency_invoked` event with no `contingency` is refused; a
113113
`not_performed` event with no reason is refused; an unauthenticated
114114
`shutdowns` create is refused and an authenticated one succeeds.
115-
- [ ] `scripts/smoke_test.sh` passes end to end; the browser test still passes.
116-
- [ ] No `pb_public/` file and no `seed/` file changed.
115+
- [x] `scripts/smoke_test.sh` passes end to end; the browser test still passes.
116+
- [x] No `pb_public/` file and no `seed/` file changed.
117117

118118
## Guardrails
119119

0 commit comments

Comments
 (0)