Skip to content

Commit 06be54e

Browse files
os-zhuangclaude
andauthored
fix(objectql): a lax-admitted value withdraws the irreversible half of an ADR-0104 certificate (#4797) (#7285)
* fix(objectql): a lax-admitted value withdraws the irreversible half of 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 * chore: regenerate spec + i18n artifacts for the deviation-marker columns (#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 * test(service-storage): annotate the reap-engine double's parameters (#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 * fix(service-storage): the reap guard's kept-files line no longer mis-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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 88154be commit 06be54e

19 files changed

Lines changed: 1088 additions & 17 deletions
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/platform-objects": minor
4+
"@objectstack/objectql": minor
5+
"@objectstack/service-storage": minor
6+
---
7+
8+
fix(objectql): a value admitted by an `OS_ALLOW_LAX_*` escape hatch stops released field files from being collected (#4797)
9+
10+
`recordDataMigrationRun`'s contract says a deployment whose data has regressed
11+
since it last verified closes its own gate. That only happened when a migration
12+
was re-run — nothing told the ledger when the data actually regressed.
13+
14+
Normally nothing has to. Once `sys_migration` records a verified ADR-0104
15+
migration the write path is strict, a non-conforming value is refused, and the
16+
certificate cannot go stale. **The operator escape hatches are the exception,
17+
and they exist precisely to relax a deployment that has already verified.** With
18+
`OS_ALLOW_MEDIA_VALUES` / `OS_ALLOW_LAX_MEDIA_VALUES` / `OS_ALLOW_LAX_VALUE_SHAPES`
19+
on, a non-conforming value is admitted and persisted while the row still reads
20+
`verified_at` non-null, `blocking: 0`. Turn the switch off — or let any other
21+
process or machine run without it — and strict returns to reject the very data
22+
this deployment stored. Meanwhile the `adr-0104-file-references` row also governs
23+
reclamation of released field files, so the reap guard kept **deleting bytes** on
24+
the strength of a certificate that was no longer true, with nothing in the ledger
25+
saying so.
26+
27+
**A lax-admitted write now records a deviation.** The engine's admit path — the
28+
same sink that already tallies counterexamples for #4769 — stamps
29+
`sys_migration.deviation_observed_at` (plus a `deviation_detail` naming the
30+
object, field, type and parse issue) on the migration whose contract the value
31+
broke.
32+
33+
**The marker gates the irreversible path, and only that.** Authority is withdrawn
34+
in proportion to reversibility:
35+
36+
| behaviour | reversible? | predicate | while a deviation stands |
37+
| --- | --- | --- | --- |
38+
| strict value-shape enforcement (#3438) | a rejected write is retried | `isDataMigrationFlagVerified` | continues |
39+
| tombstoning a released file (#3459 PR-5b) | lifted on re-attach | `isDataMigrationFlagVerified` | continues |
40+
| reap guard's byte delete | **never** | `authorisesIrreversibleAction` | **refuses** |
41+
42+
A certificate is not a boolean; it is authority over a set of behaviours, and the
43+
two halves are withdrawn on different evidence. One admitted write is a complete
44+
disproof of "nothing here violates this contract" — enough to stop deleting data
45+
forever. It is *not* evidence of the same order as the full-store scan that
46+
earned the certificate, so it does not revoke it: doing that would turn an
47+
explicitly temporary switch into a one-way door, forcing a full re-migration on
48+
anyone who used the escape hatch once.
49+
50+
Recording without gating was rejected for the opposite reason — a marker no code
51+
consumes is a declared-but-unenforced field, and the bytes get deleted regardless.
52+
53+
**Getting back to full authority is the documented route.** A real
54+
`os migrate files-to-references --apply` / `os migrate value-shapes --apply` run
55+
walks the whole store again, which *is* evidence of the same order, and clears
56+
the marker.
57+
58+
Additive and backward compatible. A `sys_migration` row written before these
59+
columns existed reads as "no deviation observed", so upgrading never retroactively
60+
closes a gate a deployment earned — the marker only ever closes it on an observed
61+
deviation. `isDataMigrationFlagVerified` is unchanged and keeps its existing
62+
consumers; the new `authorisesIrreversibleAction` (spec) and `mayActIrreversibly`
63+
(platform-objects) are the stronger pair, and the reap guard is their one caller.

content/docs/references/system/migration.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ Deployment-level record that a data migration ran here and its self-check passed
100100
| **blocking** | `integer` || Blocking discrepancies reported by the last self-check. The gate requires 0 |
101101
| **advisory** | `integer` | optional | Advisory findings from the last run (external URLs, stale owners, …) — cost storage or need a modelling decision, never block the gate |
102102
| **details** | `string` | optional | JSON-encoded counts from the last run, for diagnostics |
103+
| **deviation_observed_at** | `string \| null` | optional | When this deployment last ADMITTED a value the verified contract rejects, via an OS_ALLOW_LAX_* escape hatch. Does not clear verified_at — it withdraws the irreversible half of what the certificate authorises (#4797) |
104+
| **deviation_detail** | `string \| null` | optional | JSON-encoded first counterexample behind deviation_observed_at (object, field, type, parse issue), for diagnostics |
103105

104106

105107
---

packages/objectql/src/adr0104-attestation-evidence.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,12 @@ describe('ADR-0104 fresh-datastore attestation vs. the boot that seeds (#4769)',
277277
* A store this boot did NOT create carries history that is not ours to
278278
* vouch for either way: a verified row there is evidence by scan, and a
279279
* single write's observation must not overturn a walk of the whole store.
280+
*
281+
* That is still true, and it is the reason #4797 answers this case with a
282+
* deviation marker instead of a revocation — the certificate survives and
283+
* only the irreversible authority it carried is withheld. This test owns
284+
* the revocation half; the marker half is pinned in
285+
* `adr0104-lax-deviation-marker.test.ts`.
280286
*/
281287
it('never revokes a flag on a store this boot did not create', async () => {
282288
const store = newStore();
@@ -288,10 +294,11 @@ describe('ADR-0104 fresh-datastore attestation vs. the boot that seeds (#4769)',
288294
await expect(
289295
engine.insert('showcase_task', { id: 't1', title: 'Lax', cover: OFF_SHAPE_COVER }),
290296
).resolves.toBeDefined();
291-
// Counted (the fact is true), but the ledger is left alone.
297+
// Counted (the fact is true), and the CERTIFICATE is left alone.
292298
expect(engine.valueShapeViolationsAdmitted()[FILE_REFERENCES_MIGRATION_ID]?.count).toBe(1);
293299
const row = rowsOf(store, 'sys_migration').find((r) => r.id === FILE_REFERENCES_MIGRATION_ID);
294300
expect(row?.verified_at).not.toBeNull();
301+
expect(row?.blocking).toBe(0);
295302
} finally {
296303
vi.unstubAllEnvs();
297304
}

0 commit comments

Comments
 (0)