fix(objectql): a lax-admitted value withdraws the irreversible half of an ADR-0104 certificate (#4797) - #7285
Conversation
…f an ADR-0104 certificate (#4797) `recordDataMigrationRun` documents that a deployment whose data has regressed since it last verified closes its own gate — but that only happened when a migration was re-run. Nothing told the ledger when the data actually regressed. Normally nothing has to: once `sys_migration` records a verified ADR-0104 migration the write path is strict and a non-conforming value cannot land. The `OS_ALLOW_LAX_*` escape hatches are the exception, and they exist precisely to relax a deployment that has already verified. With one on, the value is admitted and persisted while the row still reads `verified_at` non-null, `blocking: 0` — so strict rejects that same data the moment the switch goes off, and the reap guard keeps deleting released field files on a certificate that is no longer true. A lax-admitted write now stamps `sys_migration.deviation_observed_at` (with a `deviation_detail` naming the counterexample) on the migration whose contract it broke, from the same admit-path sink that already tallies counterexamples for #4769. The marker does NOT clear `verified_at`: one admitted write is not evidence of the same order as the full-store scan that earned the certificate, and revoking on it would turn an explicitly temporary switch into a one-way door. Instead authority is withdrawn in proportion to reversibility. Strict enforcement and tombstoning keep reading `isDataMigrationFlagVerified` and carry on — a rejected write is retried, a tombstone is lifted on re-attach. The reap guard's byte delete, which cannot be undone, now reads the stronger `authorisesIrreversibleAction` / `mayActIrreversibly` and refuses while a deviation stands. A real `os migrate … --apply` run walks the whole store again and clears the marker. Additive and backward compatible: a row written before these columns exist reads as "no deviation observed", so upgrading never retroactively closes a gate a deployment earned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 110 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…mns (#4797) `sys_migration` gained two fields, so the generated baselines follow: the spec api-surface and export-origins snapshots (0 breaking, 2 added — the new `authorisesIrreversibleAction` / `hasObservedDeviation` predicates), the authorable-surface shard, the system reference page, and the four platform-object translation bundles. Also pins the new service-storage engine doubles to `assertEngineUpdateDispatch`, so neither fake is looser than `ObjectQL.update` about what counts as a by-id write (check:engine-double-contract). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
…4797) The fake is cast `as any` at the end, so its object-literal members were not contextually typed and each `object` parameter landed on an implicit any — three new TS7006s against a package whose type-check DEBT is a shrink-only ratchet. Annotating them keeps the entry moving in the direction the ratchet allows (52 recorded, 51 measured). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
…diagnoses a deviation (#4797) The guard is handed a boolean, so it cannot tell WHICH condition closed the gate — and naming only "migration is not verified" became wrong the moment a second condition existed. A deployment whose `verified_at` is plainly set would be told its migration was never verified and sent hunting for a row that says otherwise. Both causes are now named, with the pointer to the two columns that distinguish them. They share one remedy, so the instruction stays unambiguous either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015KREcaEs85dphzUUqdYfX6
|
PM note — the one red is a toolchain flake, not a build failure, and ⛔ not "pre-existing". Re-run triggered. The session report called this "69/70 gates (1 pre-existing)". The direction is right — ⛔ nothing in this diff caused it — but the label matters operationally, so correcting it: "pre-existing" means leave it red; this one just needed a re-run. Measured rather than assumed: That is Corepack's fetch of pnpm crashing inside Node's bundled undici — infrastructure, not this branch. Separately: the cross-seat declaration this PR needs is filed (#6017), since it adds two authorable fields to Two corrections in the session report are accepted and were worth making: the reclamation gate is Generated by Claude Code |
|
Revising my own veto window, in the open, before acting on it — the premise I set it on does not hold. At 05:47Z I declared PR #7285's authorable-surface touch and committed to ⛔ not flipping it ready until 07:50Z (a self-imposed 2 h). I am shortening that to now, and the reason is that I checked the precedent I was citing: #5586 — the precedent this seat has been quoting all shift — was itself an authorable-surface touch. Its spec change was Reinforcing, not deciding: three lanes today independently used declare-and-proceed for
Same shape as the call I made on PR #7261 earlier: the analysis was sound, one input was wrong, so I overturned the input in public rather than the analysis in silence. Recording it here rather than quietly flipping the PR, because a commitment other seats might rely on should not change without a reason attached. Objection window stays open on #6017 and here. Generated by Claude Code |
Closes #4797. Implements the maintainer-approved conditional B ruling (2026-08-06, reaffirmed by triage 2026-08-08): a write let through by an operator escape hatch records a "deviation observed" marker, and that marker gates the irreversible paths while reversible behaviour continues.
The defect
recordDataMigrationRundocuments its contract as:That only happened when a migration was re-run. Nothing told the ledger when the data actually regressed.
Normally nothing has to. Once
sys_migrationrecords a verified ADR-0104 migration the write path is strict, a non-conforming value is refused, and the certificate cannot go stale on its own. The operator escape hatches are the exception, and they exist precisely to relax a deployment that has already verified. WithOS_ALLOW_LAX_MEDIA_VALUES/OS_ALLOW_LAX_VALUE_SHAPESon:sys_migrationstill readsverified_atnon-null,blocking: 0;adr-0104-file-referencesrow also governs reclamation of released field files, so the reap guard keeps deleting bytes on a certificate that is now false.Same disease as #4769: the ledger's asserted fact was overturned by its own deployment, and the ledger does not know. #4794 deliberately covered only the empty-database half, because overturning a full-scan certificate on the strength of one write is the wrong order of magnitude.
The design
Producer. The engine's admit path — the same
noteAdmittedValueShapeViolationsink that already tallies counterexamples for #4769 — stampssys_migration.deviation_observed_at, plus adeviation_detailnaming the object, field, type and parse issue. It reuses that path's shape deliberately: once per migration id per process, serialised, never awaited by the write, and it never inserts (no row means nothing was certified, so there is no authority to withdraw).The marker does not clear
verified_at. One admitted write is a complete disproof of "nothing here violates this contract", but it is not evidence of the same order as the full-store scan that earned the certificate. Revoking on it would turn an explicitly temporary switch into a one-way door — option A, rejected.Consumer on the irreversible path. Authority is withdrawn in proportion to reversibility:
isDataMigrationFlagVerifiedisDataMigrationFlagVerifiedauthorisesIrreversibleActionstorage-service-plugin.tsnow supplies the reap guard'sisCollectionOpenfrom the strongermayActIrreversiblyinstead ofisDataMigrationVerified. A closed gate vetoes but does not un-tombstone: the observed release stands, only the permission to delete is withheld, so the file is collected once the gate re-opens.Clearing. A real
os migrate files-to-references --apply/os migrate value-shapes --applyrun walks the whole store again — evidence of the same order as the certificate — sorecordDataMigrationRunclears the marker in both directions.invalidateDataMigrationFlags()also reopens the engine's once-per-process witness window, so a host that re-runs a migration in-process can deviate again and be seen.Recording without gating was rejected for the opposite reason: a marker no code consumes is a declared-but-unenforced field (the ADR-0049 shape this repo is eliminating), and the bytes get deleted regardless.
Where the marker lives, and why
A dedicated pair of
sys_migrationcolumns, per migration id (matching the existing tally keying and the gate itself), contracted inpackages/specnext toisDataMigrationFlagVerified. Not in thedetailsJSON: that column is documented as diagnostics from the last run and is overwritten wholesale by each run, and a gate reading a diagnostics blob is the borrowed-evidence antipattern the ADR's addendum forbids one layer up. The row contract lives in spec so any package can read a flag without depending onplatform-objects— a marker outside that contract could not be read by the consumer that needs it.Cost measured: at most one ledger read+update per migration id per process, skipped once the marker stands. Pinned — five lax writes issue one update, not five.
Pins
packages/objectql/src/adr0104-lax-deviation-marker.test.ts(11) andpackages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts(8).Positive: the marker is written and names the counterexample;
verified_atandblockingsurvive; only the contradicted migration id is marked; strict enforcement still rejects once the hatch is off; the reap guard refuses the byte delete and reaps the same row after a re-run.Negative — the foot-gun half, since a marker anything can set is not a safety device:
validate()preview never marks (a preview that gates a later reclamation is a side effect);Backward compatibility
Additive.
isDataMigrationFlagVerifiedis unchanged and keeps its existing consumers;authorisesIrreversibleAction(spec) andmayActIrreversibly(platform-objects) are the new stronger pair, and the reap guard is their one caller. Nothing is declared breaking, so no ADR-0087 disposition is required.Related: #4769, #4794, ADR-0104.
Generated by Claude Code