From 21b610d73a895d1cff2b8075827ea2773263e9e7 Mon Sep 17 00:00:00 2001 From: Trump Date: Sat, 29 Aug 2026 02:13:30 +0000 Subject: [PATCH] chore(spec): re-classify the field.conditionalRequired liveness entry dead (#13043) The entry was ledgered `live` on a FieldSchema `.transform` lowering conditionalRequired -> requiredWhen and dropping the alias. No such transform exists: field.zod.ts has ZERO `.transform` calls, and its two occurrences of the token are comments recording that `.overwrite()` was chosen instead (#6926). The note's second leg - that objectql rule-validator.ts keeps a `requiredWhen ?? conditionalRequired` fallback on purpose - was retired by #3903, which replays the ADR-0087 conversion chain at rehydration so a stored pre-17 row arrives already lowered; its absence is pinned by that module's own test. Status flips to `dead` with the ROW STAYING - the rls.priority precedent, a retiredKey tombstone keeps the key in the walked shape. The falsified `evidence` goes, the #3754 / #3713 lowering history is preserved in the note. This was the LAST `path:NNN` citation in any ledger. The gate's line-citation counter goes 1 -> 0, which is the end of the #13003 anchor-adoption worklist and the trigger for the delete-at-zero instruction the two non-vacuity floors carried in their own guard comments: both floors, both guard comments and both named companions (`outOfRange`; the equality check) are deleted here. The two sibling floors on non-draining populations (`local` paths, `evidence paths`) stay at `> 100`, verified by grep. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4 --- .../field-conditional-required-ledger-dead.md | 32 +++++++++ packages/spec/liveness/README.md | 2 +- packages/spec/liveness/field.json | 6 +- packages/spec/liveness/state-counts.md | 4 +- .../scripts/liveness/check-liveness.test.ts | 40 +++++------ .../spec/scripts/liveness/evidence.test.ts | 68 ++++++------------- 6 files changed, 75 insertions(+), 77 deletions(-) create mode 100644 .changeset/field-conditional-required-ledger-dead.md diff --git a/.changeset/field-conditional-required-ledger-dead.md b/.changeset/field-conditional-required-ledger-dead.md new file mode 100644 index 0000000000..efb6c129f4 --- /dev/null +++ b/.changeset/field-conditional-required-ledger-dead.md @@ -0,0 +1,32 @@ +--- +'@objectstack/spec': patch +--- + +liveness ledger: re-classify `field.conditionalRequired` `live` → `dead` (#13043) + +Both halves of the entry's evidence were falsified, not just its citation. It +claimed a `FieldSchema .transform` lowering `conditionalRequired` → `requiredWhen` +that drops the alias: `packages/spec/src/data/field.zod.ts` has **zero** +`.transform` calls — its only two occurrences of that token are comments recording +that `.overwrite()` was chosen instead (#6926). And its note leaned on +`packages/objectql/src/validation/rule-validator.ts` keeping a +`requiredWhen ?? conditionalRequired` fallback "on purpose"; #3903 retired that +fallback, because every path that hands the validator a stored field definition now +replays the ADR-0087 conversion chain at rehydration, so a pre-17 row arrives with +the alias already lowered. The absence is pinned by that module's own test ("does +NOT read the retired conditionalRequired alias — PD #12, no dialect fallback"). + +The alias and its lowering were removed together in protocol 17 (#3855, PR #3883, +landed 2026-07-28); the key has been a `retiredKey` tombstone ever since. + +Data-only: no schema, no runtime, no authoring-surface change — authoring +`conditionalRequired` already failed `tsc` and the parse before this, and still +does. The row STAYS: per the `rls.priority` precedent a `retiredKey()` tombstone +keeps the key in the walked shape, so deleting the row would report UNCLASSIFIED. +`liveness/` is in this package's `files` array, so these ledgers ship in the npm +tarball and this is published data. + +This was the last `path:NNN` citation in any ledger. Retiring it takes the gate's +line-citation counter to zero and closes #13003's symbol-anchor adoption worklist, +which is why the same change deletes the two non-vacuity floors that guarded that +population (and their guard comments) exactly as those comments instructed. diff --git a/packages/spec/liveness/README.md b/packages/spec/liveness/README.md index fc23866259..9422b50402 100644 --- a/packages/spec/liveness/README.md +++ b/packages/spec/liveness/README.md @@ -810,7 +810,7 @@ marker where the Notes cell goes, never a guess at what belongs there. | Type | Notes | |---|---| | object | aspirational tier (versioning/softDelete/search/recordName/keyPrefix) + tags/active/abstract REMOVED (#2377) — tombstoned in UNKNOWN_KEY_GUIDANCE; `enable.trash`/`mru` REMOVED (#2377 close-out) — tombstoned in the now-`.strict()` ObjectCapabilities; `isSystem` + `enable.searchable` CORRECTED to live (#2377 — sharing default-model + global-search opt-out; 2026-06 audit missed both readers); `tenancy.strategy`/`crossTenantAccess` REMOVED post-15.0 (#2763) | -| field | healthy — full dead set (vectorConfig/fileAttachmentConfig/dependencies, then referenceFilters/columnName/index) REMOVED (#2377); columnName also dropped the ADR-0062 D7 lint + StorageNameMapping column helpers | +| field | full dead set (vectorConfig/fileAttachmentConfig/dependencies, then referenceFilters/columnName/index) REMOVED (#2377); columnName also dropped the ADR-0062 D7 lint + StorageNameMapping column helpers. **#13043** ends the empty dead column this type had carried since that sweep — the reason the cell said "healthy" until 2026-08-29: `conditionalRequired` is re-classified `live` → `dead` with no key added or removed. It has been a `retiredKey` tombstone since 2026-07-28 (protocol 17, #3855), so the row stays (the `rls.priority` precedent) while the verdict does not. BOTH halves of its evidence were falsified, not just the citation: the `.transform` lowering `conditionalRequired` → `requiredWhen` that the row credited does not exist (field.zod.ts has zero `.transform` calls), and the objectql rule-validator `requiredWhen ?? conditionalRequired` fallback its note leaned on was retired by #3903, which replays the ADR-0087 conversion chain at rehydration instead — so a stored pre-17 row reaches the validator already lowered. The rot was invisible to every citation check (pointer in range, right file, file names the key) and the entry carried no `verifiedAt`, so nothing ever re-asked — the #12516 class, the same shape `action.execute` turned out to have. It was also the ledger's LAST `path:NNN` citation, so retiring it took #13003's line-citation counter to zero | | flow | dead count = **5 tombstone entries** + the kept docs field: `active`/`template`/nodes.`outputSchema`/errorHandling.`fallbackNodeId` REMOVED 2026-07-30 (#3896 close-out sweep — `active: false` never stopped a flow, `status` is the enforced lifecycle; faults route via per-node fault edges), plus errorHandling.`retryDelayMs` RENAMED to `backoffMs` 2026-08-04 (#4964). The rename is why the dead column moved while live did not: a rename is a removal on this ledger, so the old spelling is tombstoned (`retiredKey` keeps it in the walked shape) and the new spelling enters as its own `live` row. Read it beside the four above as the one entry here that cost an author nothing — the block was a THIRD encoding of the retry policy #4661 converged, invisible to that pass because it is an anonymous inline block with no exported name, and #4964 spelled its base delay `backoffMs` to match `job.retryPolicy` and a `try_catch` node's `retry`. Remaining dead = `description`, KEPT deliberately: docs-shaped, exempt from enforce-or-remove | | action | `type:'form'` CORRECTED to live (objectui ActionRunner.executeForm, #2377); dead `timeout` REMOVED (#2377); `disabled` live since objectui#2863; `undoable` CORRECTED to live (#3714); `shortcut` + `bulkEnabled` REMOVED 2026-07-30 (#3896 close-out sweep — no keydown path dispatches shortcuts; the multi-select toolbar reads the view's bulkActions). **#7367** (PR #7430) adds `description` as an authorable key, `live` on arrival — the only row this type has gained since that sweep. **#13036** makes the dead set three: `execute` joins it, re-classified `live` → `dead` 2026-08-29 with no key added or removed. Its `live` verdict rested on a `.transform` lowering `execute` → `target` that protocol 17 (#3855) removed along with the alias; the key has been a `retiredKey` tombstone since 2026-07-28, so the row stays (the `rls.priority` precedent) while the verdict does not. The rot was invisible to every citation check — the pointer was in range, in the right file, and the file names the key — and the entry carried no `verifiedAt`, so nothing ever re-asked | | hook | model-healthy; label/description dead but KEPT deliberately (2026-07-30 sweep) — docs-shaped annotation fields, exempt from enforce-or-remove | diff --git a/packages/spec/liveness/field.json b/packages/spec/liveness/field.json index f3a611238f..f77613b827 100644 --- a/packages/spec/liveness/field.json +++ b/packages/spec/liveness/field.json @@ -91,9 +91,9 @@ "note": "renderer CEL." }, "conditionalRequired": { - "status": "live", - "evidence": "packages/spec/src/data/field.zod.ts:639 — the FieldSchema .transform lowers conditionalRequired -> requiredWhen and DROPS the alias, so authoring it changes what the validator enforces", - "note": "#3754: folded at parse time and removed from the parsed output (same shape as action.execute -> target in #3742, agent.knowledge.topics -> sources in #1891). Previously BOTH keys stayed live in the output with no canonicalization, so each consumer re-derived the precedence — the condition that produced the #3713 divergence. objectql rule-validator.ts keeps its `requiredWhen ?? conditionalRequired` fallback on purpose: raw (unparsed) metadata still reaches evaluateValidationRules. Removal of the input key is still the endgame." + "status": "dead", + "verifiedAt": "2026-08-29", + "note": "REMOVED 2026-07-28 in protocol 17 (#3855, PR #3883 — the commit that retired the last three deprecated authorable aliases) — tombstoned at the schema (retiredKey carries the prescription; authoring it is a tsc error and a parse error) and renamed out of sources by the protocol-17 conversion `field-conditionalRequired-to-requiredWhen`. The entry stays because retiredKey keeps the key in the walked shape (the rls.priority precedent); use `requiredWhen`, the only predicate slot — rename the key, the value (a CEL predicate) is unchanged, and `os migrate meta --from 16` lists the mechanical edits. The tombstone is packages/spec/src/data/field.zod.ts#conditionalRequired. The consumer-side refusal is packages/objectql/src/validation/rule-validator.ts#ConditionalFieldDef, which declares `requiredWhen` THE ONLY predicate slot and states PD #12 — no consumer-side dialect fallback returns here; that absence is PINNED by the case named `does NOT read the retired conditionalRequired alias (PD #12 — no dialect fallback)` in packages/objectql/src/validation/rule-validator.test.ts, which sits beside a positive case asserting the rule DOES fire once the stored-conversion chain has lowered the alias. LEDGER CORRECTED 2026-08-29 (#13043) — the VERDICT was falsified, not the citation, and in BOTH halves of its evidence. (a) This row read `live` on the claim that the FieldSchema .transform lowers conditionalRequired -> requiredWhen and DROPS the alias. No such transform exists: field.zod.ts contains ZERO .transform calls, its only two occurrences of that token are comments recording that `.overwrite()` was chosen over `.transform()` per the measured #6926 precedent, and the describe() on `requiredWhen` says out loud that the alias was removed in protocol 17. (b) The note's own stated reason for the verdict — that objectql rule-validator.ts keeps its `requiredWhen ?? conditionalRequired` fallback on purpose because raw (unparsed) metadata still reaches evaluateValidationRules — was retired by #3903: every path that hands the validator a STORED field definition now replays the ADR-0087 conversion chain at rehydration (applyConversionsToStoredItem), so a pre-17 row arrives with the alias already lowered, and a caller handing raw unconverted legacy input is off-contract. WHY NOTHING SAW THE ROT: the old pointer packages/spec/src/data/field.zod.ts:639 was IN RANGE in a 2,132-line file that names the key five times, so the existence check, the line bound and the key-mention check were all green on it — what :639 holds today is the tail of FieldMaskingRuleSchema, unrelated in every respect — and the entry carried no verifiedAt, so the re-verification clock never asked (the #12516 class). It was also the LAST `path:NNN` citation left in any ledger, which is why retiring it here closes #13003's anchor-adoption worklist. HISTORY PRESERVED, because it is the argument for `requiredWhen` being the single slot: #3754 folded the alias at parse time and removed it from the parsed output (same shape as action.execute -> target in #3742, agent.knowledge.topics -> sources in #1891); before that fix BOTH keys stayed live in the output with no canonicalization, so each consumer re-derived the precedence — the condition that produced the #3713 divergence. That note called removal of the input key the endgame, and protocol 17 is where the endgame landed. RETIREMENT CHAIN, COMPLETE — the close-out machinery is what remains, and it is not a consumer: the ADR-0087 conversion `field-conditionalRequired-to-requiredWhen` in packages/spec/src/conversions/registry.ts, its `RETIRED_KEYS_BY_MAJOR` entry and D3 chain step in packages/spec/src/migrations/registry.ts, and the explicit `NOTE: no entry for conditionalRequired` in packages/spec/src/data/authoring-key-lint.ts. Two INERT survivors are recorded so the next reader does not mistake either for a reader that gives the key meaning: packages/lint/src/validate-expressions.ts still walks the key, and its own docblock calls that branch inert on the parsed compile path because strict FieldSchema rejects the key by name (it gets a slot-agnostic consequence clause rather than a fabricated fourth measurement); packages/drivers/driver-sql/src/builtin-column-collision.ts lists it in a key-classification map as `presentation`, which classifies a name, not a value anyone reads. Re-closed by hand against cae2169cf." }, "readonly": { "status": "live", diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 72a4515b58..2024a7e7f3 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -28,7 +28,7 @@ for both corollaries. | Type | live | exp | dead | planned | classified | |---|---|---|---|---|---| | `object` | 51 | 0 | 0 | 1 | 52 | -| `field` | 89 | 0 | 0 | 2 | 91 | +| `field` | 88 | 0 | 1 | 2 | 91 | | `flow` | 34 | 0 | 6 | 0 | 40 | | `action` | 41 | 0 | 3 | 2 | 46 | | `hook` | 18 | 0 | 2 | 0 | 20 | @@ -58,4 +58,4 @@ for both corollaries. | `capability` | 12 | 0 | 0 | 0 | 12 | | `qa` | 4 | 0 | 5 | 0 | 9 | | `manifest` | 22 | 0 | 21 | 0 | 43 | -| **total** | **820** | **5** | **79** | **10** | **914** | +| **total** | **819** | **5** | **80** | **10** | **914** | diff --git a/packages/spec/scripts/liveness/check-liveness.test.ts b/packages/spec/scripts/liveness/check-liveness.test.ts index e25fdd7450..92d3daecd7 100644 --- a/packages/spec/scripts/liveness/check-liveness.test.ts +++ b/packages/spec/scripts/liveness/check-liveness.test.ts @@ -183,33 +183,29 @@ describe('check:liveness — evidence pointers (#5623)', () => { expect(output).not.toContain('name a line the cited file does not have'); }); - it('prints the citation count and how many are in range, equal on a green run', () => { - // The #5623 lesson applied to the new counter: printing only "in range" - // would read as a pass on a run where the parser extracted no citations. - // Hence two numbers, and a floor under the first — see the floor's own note - // at the assertion below for why that floor is `> 0` and when to delete it. + it('prints the citation count and how many are in range, in the documented two-number shape', () => { + // The #5623 lesson applied to the counter: printing only "in range" would + // read as a pass on a run where the parser extracted no citations. Hence two + // numbers, and the gate prints them on every run — including this one, where + // the population is zero. + // + // The non-vacuity FLOOR that stood here (`> 0`, ruled 2026-08-28 on #13003, + // comment 5458356183) and the equality check beside it were DELETED + // 2026-08-29 (#13043), by the standing instruction the floor's own guard + // comment carried, at the moment that instruction names: #13003 retired the + // last line citation and the population legitimately reached zero. The floor + // reds at exactly that moment BY DESIGN, so that reaching zero is a conscious + // decision rather than a silent pass; the equality check went with it because + // it now compares two zeroes. What the case still pins is real and is what + // the deletion could otherwise cost: the gate must keep EMITTING the line, in + // the shape this regex documents — drop the line, rename it, or collapse it + // to one number and this reds. A `path:NNN` citation written again gives this + // case a population back; restore a floor with it. const { status, output } = runGate(path.join(tmp, 'liveness')); expect(status, output).toBe(0); const line = output.split('\n').find((l) => l.startsWith('line citations:')) ?? ''; const m = /line citations: (\d+) pointer\(s\) written .*?, (\d+) inside the cited file/.exec(line); expect(m, line).not.toBeNull(); - // Non-vacuity. The failure mode this guards is the parser silently - // extracting NOTHING (#5623), and that failure mode is exactly zero — so - // zero is exactly what the floor tests. It was 100 until #13003, which is - // the migration adopting #12516's `path#symbol` grammar and therefore - // RETIRING line citations by design (300 at that card's filing, 175 after - // batch 2, 82 after batch 3). Any floor above zero reds on legitimate - // drainage and re-opens the same escalation one batch later; `> 0` never - // lies during the migration and still catches extracts-nothing at full - // strength. Ruled 2026-08-28 on #13003, comment 5458356183. - // - // ⛔ WHEN THIS POPULATION LEGITIMATELY REACHES ZERO — the last line citation - // retired — DELETE this assertion AND this comment IN THE SAME PR that - // retires it, along with the equality check below, which then compares two - // zeroes and asserts nothing. A conscious decision at zero, never a silent - // one; that is the whole reason the floor sits at `> 0` rather than gone. - expect(Number(m![1])).toBeGreaterThan(0); - expect(m![2]).toBe(m![1]); expect(line).not.toContain('PAST EOF'); }); diff --git a/packages/spec/scripts/liveness/evidence.test.ts b/packages/spec/scripts/liveness/evidence.test.ts index 874776e31c..ef2daaa44b 100644 --- a/packages/spec/scripts/liveness/evidence.test.ts +++ b/packages/spec/scripts/liveness/evidence.test.ts @@ -289,8 +289,10 @@ describe('countLines — what a citation can address', () => { // inputs, and @objectstack/spec's globs cover neither package — so naming them // reds that gate, and the only way to satisfy it would be to declare an input // this file does not have. The counts below come from a stub, not from those -// files: a citation whose real line count matters is asserted against the real -// ledgers in the contract test at the bottom of this file. +// files. Until 2026-08-29 (#13043) the shipped ledgers were re-checked against +// their real line counts by a contract test at the bottom of this file; that +// population is now zero and the test is gone (see the note where it stood), so +// these unit tests are the whole of the bound's coverage. describe('checkCitationLines', () => { const lines = (n: number) => () => n; @@ -344,53 +346,21 @@ describe('shipped ledgers', () => { expect(local).toBeGreaterThan(100); }); - it('every local `path:NNN` citation names a line that file has', () => { - const outOfRange: string[] = []; - let citations = 0; - const lineCount = (p: string): number | null => { - const f = join(repoRoot, p); - if (!existsSync(f)) return null; - return countLines(readFileSync(f, 'utf8')); - }; - for (const f of readdirSync(ledgerRoot).filter((x) => x.endsWith('.json'))) { - const ledger = JSON.parse(readFileSync(join(ledgerRoot, f), 'utf8')); - const visit = (key: string, entry: any) => { - if (entry?.status !== 'live' || typeof entry?.evidence !== 'string') return; - const scan = checkEvidence(entry.evidence, () => true); - citations += scan.localCitations.length; - for (const c of checkCitationLines(scan, lineCount)) { - outOfRange.push(`${ledger.type}/${key} → ${c.path}:${c.line} (file has ${c.lines})`); - } - }; - for (const [key, entry] of Object.entries(ledger.props || {})) { - visit(key, entry); - for (const [ck, centry] of Object.entries(entry?.children || {})) visit(`${key}.${ck}`, centry); - } - } - expect(outOfRange).toEqual([]); - // Same non-vacuity guard as above, one level down: a parser that stopped - // retaining lines would satisfy the assertion above by extracting nothing - // (the #5623 lesson — "all in range" over zero citations is what a degraded - // parser prints too). The GUARDED FAILURE MODE IS EXACTLY ZERO, so zero is - // exactly what the floor tests, and the sibling floor above keeps its own - // number because the `local` PATH population it guards is not draining. - // - // Why not a bigger number here, when this one used to be 100 (#13003): the - // symbol-anchor migration (#12516's grammar, adopted batch by batch under - // #13003) RETIRES line citations by design — 300 at that card's filing, 175 - // after batch 2, 82 after batch 3 — so any floor above zero reds on - // legitimate drainage and re-opens the same escalation one batch later. - // Ruled 2026-08-28 on #13003 (comment 5458356183): lower to `> 0`, the only - // floor that never lies during the migration while still catching - // extracts-nothing at full strength. - // - // ⛔ WHEN THIS POPULATION LEGITIMATELY REACHES ZERO — the last line citation - // retired — DELETE this assertion AND this comment IN THE SAME PR that - // retires it, together with the `outOfRange` assertion above, which has - // nothing left to check. That is the conscious decision at zero the `> 0` - // floor exists to force. Never let it pass silently on an empty population. - expect(citations).toBeGreaterThan(0); - }); + // The `path:NNN` contract test stood here until 2026-08-29 (#13043), when the + // standing instruction in its own guard comment came due: #13003 retired the + // last line citation (`field.conditionalRequired` re-classified `dead`, so its + // `evidence` string and the citation in it are gone) and the gate now prints + // `line citations: 0 pointer(s) written`. The `> 0` floor ruled on 2026-08-28 + // (#13003, comment 5458356183) reds at exactly that moment BY DESIGN, to force + // a conscious decision rather than a silent pass on an empty population; the + // `outOfRange` assertion it named as its companion had nothing left to check. + // The `it` went with its two assertions because nothing else was in it, and a + // body that walks every ledger and asserts nothing is the vacuous shape this + // file exists to refuse (#5623). No coverage is lost: the ARITHMETIC of the + // line bound stays unit-tested in `checkCitationLines` above, and the gate's + // two-number line stays pinned in check-liveness.test.ts. If a `path:NNN` + // citation is ever written again the ledger has a new population — restore a + // contract test here rather than trusting the unit tests to cover shipped data. it('every local `path#symbol` anchor names a symbol its file contains (#12516)', () => { const bad: string[] = [];