Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .changeset/field-conditional-required-ledger-dead.md
Original file line numberDiff line numberDiff line change
@@ -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.
2 changes: 1 addition & 1 deletion packages/spec/liveness/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand Down
6 changes: 3 additions & 3 deletions packages/spec/liveness/field.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/spec/liveness/state-counts.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 |
Expand DownExpand Up@@ -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** |
40 changes: 18 additions & 22 deletions packages/spec/scripts/liveness/check-liveness.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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');
});

Expand Down
Loading
Loading