You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
check-liveness drills exactly ONE level, so a children map at depth 2 is silently ignored — no evidence resolved, no unclassified report, no warning that any of it is missing #17424
Filed by the domain:spec execution seat, session_01MkQhmuuJAVDjmeWNixwDDH, 2026-09-10T13:4xZ, out of the #17385 round (PR #17423). ⛔ Unclaimed. No domain:* label and no pm:* state: ⛔ both are the triage seat's to produce.
The reading
packages/spec/scripts/liveness/check-liveness.mts reads led.children[ck] and never recurses into a child's own children.
⇒ A children map written at depth two is accepted by the file format and then silently ignored: no evidence path is resolved, no key is reported unclassified, no container reconcile applies, and ⛔ no warning says any of that is missing.
⭐ The consequence, stated as the round that found it stated it: such a map would be "prose wearing the shape of data" — the #4956 failure this ledger exists to end, one level down.
Why it surfaced, and why it is not academic
Card #17385's remaining deliverable is a per-key drill of dashboard/widgets.chartConfig's fourteen keys. That coordinate sits at depth two. ⇒ Writing the drill today would produce a table that looks classified, is read by nothing, and can never go red — the exact opposite of what a ledger row is for.
⭐ The round refused to write it for that reason and stopped. ⛔ That refusal is correct and this card is what unblocks it: the drill is not workable until the walk reaches the coordinate.
What a fix has to touch
check-liveness.mts — the walk itself.
drill.mts — the authoring side, so a depth-2 map can be produced.
The undrilled-containers baseline — a migration, because deepening the walk will surface containers that were invisible before.
⚠️ ⭐ This is the only option under which drilled verdicts can ever go red, which is the whole reason to prefer it over writing the table by hand.
⚠️ Sequencing, from the round: do this first, then drill. ⛔ Drilling into a coordinate the gate does not read buys the appearance of classification and none of its protection.
⛔ What this card is NOT
⛔ Not a request to change any row's status. ⛔ Not the #17385 drill itself — that stays on #17385 and should carry Blocked-by: this card once triage grades it. ⛔ Not a licence to invent a per-face ledger shape: the 2026-09-10 ruling 「⛔ 不收窄声明、不发明「按面 live」的台账形状」 stands and is untouched by this.
⚠️Re-check before acting: read check-liveness.mts around its led.children[ck] read and confirm there is still no recursion. If the walk already recurses, this card is answered and closes.
Dedup
⚠️Bounded, and the bound is declared. The seat searched the open domain:spec set for liveness / drill / children / depth. ⛔ It did not run a repo-wide sweep of every issue touching the liveness ledger — that corpus is large and the round that surfaced this had already declared a similar sweep unclosable within budget on an adjacent question. ⇒ Whoever takes this should re-run dedup before starting; ⛔ do not read this section as a closed enumeration.
Known adjacent, not duplicates: #17385 (the card whose remaining half this blocks) · #4956 (the failure class the ledger exists to end, named by the round).
Source
#17385 · PR #17423 · the os-dev round report of 2026-09-10, out-of-scope finding 2 · packages/spec/scripts/liveness/check-liveness.mts
Triage: lands in packages/spec/scripts/liveness/check-liveness.mts; domain:spec; priority:p2.
check-liveness reads led.children[ck] and never recurses into a child's own children ⇒ a children map written at depth two is accepted by the ledger and then silently ignored: no evidence resolved, no unclassified report, no warning that any of it is missing.
⇒ p2, and the reason is what this tool gates: the liveness ledger is the instrument the enforce-or-remove channel relies on to say whether a property is dead. ⭐ A liveness check that silently skips a subtree can report dead for something that is alive — and this round routed several cards (#16864, #17157, #17296) into that channel. A false dead verdict retires a live key.
⇒ Recurse. ⚠️ And ⭐ the missing report is as important as the missing recursion: whatever the depth limit ends up being, an entry the tool did not classify must appear as unclassified, ⛔ never be dropped. Silence is what made this invisible.
⚠️ Re-run the ledger after the fix and report which verdicts change — if any existing dead verdict flips, that is a card of its own and possibly a retirement to unwind.
Claim: PM dispatch by the domain:spec execution seat, session session_01MkQhmuuJAVDjmeWNixwDDH, at 2026-09-12T04:27Z. The assignee and this comment are both written by the PM; the os-dev round inherits them, ⛔ posts no second claim and ⛔ never writes the assignee — on this card or on any card it files.
Clause-②: no — this PR puts no new key on any published payload — the liveness tooling under packages/spec/scripts/ is not a published payload.
Declared file face (region level) — packages/spec/scripts/liveness/check-liveness.mts (the walk) and drill.mts (the authoring side). ⛔ Ledger DATA files are READ-ONLY unless a fixture is needed; ⛔ do not edit any existing verdict to make the walk agree.
Concurrency at dispatch (2026-09-12T04:27Z) — sibling dispatches this batch: #16885 (ui/view.zod.ts), #17260 (ui/component.zod.ts). Open from this seat: PR #17774 (scripts/check-dispatcher-error-vocabulary.mjs), PR #17776 (scripts/check-adr-0087-registration.mjs + docs/adr/0087 — ⛔ GOVERNED, human merge), PR #17635 (parked, lands last per #15939 Ruling A). ⇒ disjoint from all. ⭐ The round still owes its own check.
Changeset — ⛔ do NOT assume owed or not-owed. Measure it: build the package and check whether your changed text actually reaches its published dist, with a positive control that ships and a negative control that does not (a test file's text). Two rounds tonight reached opposite, correct answers this way. If owed: .changeset/17424-<slug>.md (bare card number then slug — the tree's prevailing spelling; ⛔ never an unscoped descriptive name, per #17712). If not owed: skip-changesetand say why.
Worktree — ../objectstack-issue-17424; ⛔ never edit the shared primary checkout. The branch name above is fixed (the Claim names it).
{
"card": 17424,
"status": "done",
"pr": "https://github.com/objectstack-ai/objectstack/pull/17810",
"branch": "claude/issue-17424-liveness-depth-two-recursion",
"head_sha": "d921eafbfcde28a3082d650599d2970180d32017",
"files_changed": [
"packages/spec/scripts/liveness/check-liveness.mts",
"packages/spec/scripts/liveness/orphans.mts",
"packages/spec/scripts/liveness/drill.mts",
"packages/spec/scripts/liveness/undrilled-containers.baseline.json",
"packages/spec/scripts/liveness/check-liveness.test.ts",
"packages/spec/scripts/liveness/orphans.test.ts",
"packages/spec/liveness/README.md",
".changeset/17424-liveness-depth-two-recursion.md"
],
"premise_still_valid": true,
"premise_controls": "Re-read the walk at origin/main 65481183b8 as the card demanded: check-liveness.mts read led.children[ck] and had no recursion into the child's own children — the 'drill one level' comment present at 1, drillChildren at 0. LIT CONTROL that the premise could have come back the other way: the same probe on the fixed file returns drillChildren at 4, so the probe discriminates. Second control, aimed one directory over rather than only at the file the card named: the authoring side (drill.mts) and the reverse direction (orphans.mts) were both read — orphans.mts carried the SAME one-level bound and was NOT named on the card. It is fixed here because recursing the forward pass alone would have built a fresh silence at depth two.",
"depth_limit_chosen": "The LEDGER's own nesting — the walk descends exactly as far as a children map is declared, with no artificial limit. Why: a parsed ledger is finite acyclic JSON, so termination is bounded by data the author can read rather than by a constant they cannot; and driving from the SCHEMA instead would recurse into genuinely recursive shapes (a NavigationItem contains NavigationItems) and fan the undrilled baseline out by an unbounded amount with no evidence behind any of it, which drill.mts's own header says is worse than an honest coarse verdict. MAX_DRILL_DEPTH = 8 is a TRIPWIRE, not the working limit: it exists only so the boundary cannot hide anything, and it is unreachable on today's ledgers (deepest declared nesting is 1).",
"unclassified_reporting": "Four named sites, all of which FAIL the gate rather than warn. (1) The ceiling: past MAX_DRILL_DEPTH every key under the cut is pushed to report.unclassified as 'nested past the 8-level drill ceiling — NOT walked, so nothing here is classified'. (2) No verdict anywhere at depth N: reported as type/a.b.c, the same shape depth one already used. (3) A children map declared on a non-container at depth N: reported with the same 'declared children but property is not a container' message as depth one. (4) A ledger row naming a key its container never had, at ANY depth: findOrphanEntries now takes a PATH and recurses, so it lands in report.orphanEntries instead of being dropped. Two deliberate silences, both stated in the code: an orphan's own subtree is not listed (the parent row is the single fix), and a non-container's children is reported once by the forward pass rather than twice under two headings.",
"verdict_diff": {
"changed": "NONE",
"command": "pnpm exec tsx scripts/liveness/check-liveness.mts --json captured at 65481183b8 (pre) and post-fix, then diffed field by field with: python3 -c \"import json;a=json.load(open('prefix.json'));b=json.load(open('post.json'));[print('MOVED:',k) for k in sorted(set(a)|set(b)) if a.get(k)!=b.get(k)]\"",
"result": "Exactly one field moved: undrilledNew, 0 to 54. The types map (per-type byStatus for all 36 governed types) and totals compared EQUAL as whole objects; unclassified, orphanEntries and unknownStatus were empty on both sides. Totals unchanged: live 850, planned 10, dead 93, experimental 5, live-elsewhere 1.",
"dead_flips": "None. No dead verdict flipped in either direction, so there is no retirement to unwind and no card to hand back.",
"why_structural_not_lucky": "A scan of all 36 shipped ledger files finds ZERO depth-2 children maps, so the new recursion branch is unreachable on shipped data. That zero is a reading because the same scan returns non-zero the moment one exists — which is exactly what the ablation fixture creates, and the post-fix run then reports it. Had a depth-2 map existed in the tree, the field-by-field diff above is the instrument that would have caught the flip.",
"second_order": "What DID move is the gate's census of its own completeness, not any verdict: 58 containers / 302 child keys to 109 / 555, plus 7 to 10 resolved deferrals covering 277 to 331."
},
"baseline_migration": "54 containers became visible at once — every one already riding on a blanket verdict BELOW a drilled container, where a one-level walk could not see it and therefore never counted it. 3 resolve as deferrals to app/navigation (app/navigation.children, app/areas.navigation, manifest/navigationContributions.items — all the same 18-key NavigationItem surface; recording them as 'classified nowhere' would have been false in the other direction, and the gate's EQUALITY check is what makes the deferral resolved rather than believed). 51 are recorded in the containers list. The migration and its reading are stated in that file's own _containers header, so a reviewer seeing 51 rows arrive reads it as a denominator correction rather than a regression. dashboard/widgets.chartConfig (14 keys) — the coordinate #17385 is blocked on — is now in the population; that drill is NOT in this PR.",
"ablation": {
"shape": "The PRE-FIX tool was materialised from the branch point into an untracked sibling directory (packages/spec/scripts/liveness-prefix/, so specRoot and repoRoot still resolve) and run against the SAME fixture as the post-fix tool. A whole-directory copy rather than one file, because orphans.mts changed too and a mixed pair would have contaminated the reading.",
"on_disk_proof_before_reading": "Pre-fix copy is the old walk: 'drill one level' comment at 1, drillChildren at 0, against 4 in HEAD's copy; its baseline carries the pre-migration 58 rows. Fixture carries the depth-2 map: marker neverWasAKeyOnChartConfig at 1 in the fixture ledger, 0 in the shipped one. All counted with grep -c on the exact text, never inferred from an editor's exit code.",
"fixture": "A nested children map on dashboard.widgets.chartConfig carrying (a) a rotted evidence pointer and (b) a key that has never existed on ChartConfig.",
"pre_fix": "exit 0. chartConfig.title mentions = 0, neverWasAKeyOnChartConfig = 0, rotted sql-driver.ts pointer = 0 — and it printed its success line claiming everything was classified. That IS the defect.",
"post_fix": "exit 1. Names 'dashboard/widgets.chartConfig.title to packages/plugins/driver-sql/src/sql-driver.ts' (evidence RESOLVED at depth two), reports 'dashboard/widgets.chartConfig.neverWasAKeyOnChartConfig' as an ORPHAN rather than guessing what it meant, and lists the 13 remaining keys as unclassified.",
"restore": "No repo byte was ever mutated — pre-fix tool in an untracked dir, fixture in a temp dir, both removed by a trap on EXIT INT TERM whose paths are absolute, resolved from git rev-parse --show-toplevel. Restore proven by OBSERVATION, not by an exit code: git status --porcelain empty, git diff HEAD --stat empty, and git hash-object equal to the HEAD blob for all six touched paths — README.md d12e5acd94d5fcf311aee524685c13010bb76e74, dashboard.json 93dabafa8790d2ff4fc607db7751ef95454cb0a6, check-liveness.mts 5d731dad20728d8aca8679139ea4fbf04fa4301d, orphans.mts 75dfb958e3803eb707a2bf6094e0acf21bb90346, drill.mts 14f078410c7b90a5c96edd195ba66d60dac66ef9, undrilled-containers.baseline.json d18b2c8cadcce2904951a254b3ca067521113da7. No empty hash, no missing path.",
"permanent_cover": "The ablation leaves nothing behind. 9 new cases in check-liveness.test.ts spawn the real gate the way CI does and read named report BUCKETS via --json rather than searching the prose, because 'reported somewhere' was never the question. undrilledStale is deliberately NOT used as evidence of recursion: a coordinate the walk cannot see is reported stale too, so it agrees pre- and post-fix and does not discriminate. The ceiling case uses app.navigation, whose children are NavigationItems, so a real recursive shape carries the ledger past the constant."
},
"changeset_decision": {
"verdict": "OWED — .changeset/17424-liveness-depth-two-recursion.md, @objectstack/spec: patch",
"measurement": "npm pack --dry-run --json on the BUILT package: 2011 published entries, 216 under dist/. scripts/ contributes 0 of 2011. grep of the built dist/ for the changed text: drillChildren 0 files, MAX_DRILL_DEPTH 0 files.",
"positive_controls": "liveness/ ships at 38 entries and liveness/README.md is 1 of them; a real shipped symbol (MetadataTypeSchema) resolves in 40 of the 216 dist files — so the dist grep could have come back the other way.",
"negative_control": "scripts/liveness/check-liveness.test.ts — 0 published entries, as expected of a file that does not ship.",
"why_owed_anyway": "files[] ships the liveness DIRECTORY whole, and liveness/README.md is one of those 2011 entries. It is the ledger's authoring contract — its 'Granularity — drill one level' section is what an author reads before writing a children map. Those published bytes change, and they change because this PR made them false. So the tool is not what owes the changeset; the README is.",
"trap_avoided": "The first pack ran before the build and reported dist entries = 0. It was re-run after a green 73/73 build so the dist leg is a real reading rather than a measurement of an absent directory."
},
"verification": {
"tests": "pnpm --filter @objectstack/spec test — 473 test files / 13450 tests, ALL PASSED (vitest's own summary line). The lock wrapper's VERDICT line is absent from that log because I terminated the wrapper myself after mis-spelling its args, and the child suite ran to completion — so the reading is vitest's summary, not an inferred exit code. Targeted re-run after the late README commit: check-liveness.test.ts + readme-table.test.ts + orphans.test.ts + drill.test.ts, 4 files / 140 tests passed, VERDICT command-exit 0.",
"typecheck": "pnpm --filter @objectstack/spec typecheck — VERDICT command-exit 0. The usual hole is closed here by a SECOND gate rather than assumed: check:test-typecheck compiles the test layer separately (54 files under tsconfig.test.json), so the new cases are type-checked rather than silently excluded.",
"lint": "pnpm lint (eslint . --no-inline-config, the WHOLE repo) — exit 0 in the foreground. No narrowing claimed and none needed: the full declared population ran.",
"gates": "scripts/pm/dispatch-gates.mjs --commands derived 67 families for these 8 paths; 66 ran and ALL exited 0, every exit code captured before any pipe. Five (dts-closure, dual-build-cjs-loads, lean-entry-closure, sourcemap-no-sources-content, type-check-debt) first returned exit 3 = PREREQUISITE NOT MET, were re-run after a green full build (73/73 tasks), and then exited 0.",
"not_measured": [
"pnpm check:pm-dispatch-gates — exceeded the container's roughly 10-minute foreground ceiling twice under contention (SIGTERM, exit 143). NOT MEASURED: read as neither green nor red. It is a checker-health self-test that grades the dispatch-gates script itself, which this diff does not touch.",
"pnpm check:type-check-debt on the FINAL head — exit 3, PREREQUISITE NOT MET, reason printed by the gate: '@objectstack/hono has no built type entry point on disk'. That is a sibling agent's build churn on this shared box, not this diff. The reading that stands is its exit 0 from the post-build gate run; the only delta between those two heads is Markdown prose plus a Markdown changeset, which cannot move a TypeScript debt ledger."
],
"declared_limitation": "The gate-family derivation was made on this branch's base (65481183b8). dispatch-gates reports that origin/main has since advanced about 15 commits and that 12 of the files the derivation reads changed in that range, so a family ADDED in that window would not appear in my 67. I did not merge main to close it: that would owe a full re-verification round on a heavily contended box for a benefit CI delivers directly, since CI derives against the real merge state. Declared rather than silent.",
"hygiene": "Control-byte scan of all 8 changed files: 0 hits, with a lit control (a planted 0x01 byte is detected at 1 by the same probe). Commit messages contain zero occurrences of '#' followed by digits, so no card relation is even expressible; the repo's own pre-push hook confirmed it — 'check:commit-card-trailers: 2 commit message(s) on this push carry no card relation'. The card relation appears exactly once, in the PR body, as Fixes #17424. PR body read back from GitHub after creation: byte-identical to what was sent, footer block intact.",
"worktree": "../objectstack-issue-17424, dedicated. The shared primary checkout was never edited. No git stash. No kill by name — the two processes stopped were stopped by a PID I had recorded."
},
"mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probed first: GET /issues/17424 returned HTTP 200), and every local read through git.",
"out_of_scope_findings": [
"noted, not filed: the gate's --dump TYPE debug mode still drills exactly one level, so it cannot show a depth-2 coordinate the walk now classifies. Not (a)/(b)/(c) — it never claimed to reach deeper and it misreports nothing; it is now merely shallower than the walk. NAMED SUCCESSOR: the #17385 drill round, which will author the depth-2 dashboard/widgets.chartConfig table and is the first consumer that will want --dump to reach it.",
"noted, not filed: five governed types (crud_endpoints, metadata_endpoints, batch_endpoints, route_generation, realtime_subscription) were deliberately ROOTED at RestServerConfig / RealtimeConfig sub-schemas BECAUSE the walk drilled one level — their README rows say so in as many words. That constraint is now gone, so they could in principle be consolidated under their parent config schemas with nested children. A design opportunity, not a defect: the current rooting still works and is still honest, and consolidating would move ledger coordinates, README rows and the GOVERNED list — not a dev agent's call. Their rationale prose is corrected to past tense in this PR; the decision itself is untouched. NAMED SUCCESSOR: whoever next revisits SPEC_ONLY_SCHEMAS rooting.",
"noted, not filed: check-liveness.test.ts now spawns the real gate 62 times, and ran about 255s locally for 53 cases before I added 9 more. Each spawn is a full ledger walk. An observation about test cost, not a defect — the spawn-the-real-gate design is deliberate and documented in that file's header, and it is why the #5623 class is catchable at all. NAMED SUCCESSOR: whoever next adds a case to that file and has to wait for it."
],
"open_questions": []
}
Closed out by CONTENT on origin/mainc9eb773bab (2026-09-12T08:08Z), ⛔ not by the merge event
PR #17810 merged. pm:dispatched and the assignee were stripped in the same write and read back.
probe (git grep -oF, over packages/spec/scripts/liveness/)
count
drillChildren
4
MAX_DRILL_DEPTH
6
export function — ⭐ lit control
37
a fabricated token — ⭐ dark control
0
⭐ The count only says the name is there; the recursion is what the card asked for, so the sites were read: check-liveness.mts:986 is drillChildren(type, childPath, cs[ck], cled, cat, depth + 1)inside drillChildren itself, with the entry call at :1033 passing depth 1. ⇒ the walk descends, it does not merely name a helper.
What this seat checked before landing, beyond the round's own report
The baseline grew by 51 rows in one commit. ⭐ That is a ratchet extension, not a raised ceiling, and it was verified rather than accepted:
those containers were invisible to the one-level walk, so nothing that was previously refused is now accepted — only the instrument's reach grew;
undrilledNew and undrilledStale both still fail the gate, so a 52nd is refused and a row that becomes unnecessary is refused too;
the whole diff was read for weakening tells: the skip / ignore hits are all inside comments describing the defect, the continue; hits are all in the report-then-continue classification path, and there are zero allowlist entries, exemptions, or exit-code changes;
⚠️ one correction to my own first reading: I took a - line in the baseline for a deleted translation/settingsCommon row. It is a comma change — that row is present at line 139 of the merged file. No baseline row was removed.
Activity
os-litant commentedon Sep 10, 2026
Triage: lands in
packages/spec/scripts/liveness/check-liveness.mts;domain:spec;priority:p2.check-livenessreadsled.children[ck]and never recurses into a child's ownchildren⇒ achildrenmap written at depth two is accepted by the ledger and then silently ignored: no evidence resolved, no unclassified report, no warning that any of it is missing.⇒ p2, and the reason is what this tool gates: the liveness ledger is the instrument the enforce-or-remove channel relies on to say whether a property is dead. ⭐ A liveness check that silently skips a subtree can report
deadfor something that is alive — and this round routed several cards (#16864, #17157, #17296) into that channel. A falsedeadverdict retires a live key.⇒ Recurse.⚠️ And ⭐ the missing report is as important as the missing recursion: whatever the depth limit ends up being, an entry the tool did not classify must appear as unclassified, ⛔ never be dropped. Silence is what made this invisible.
deadverdict flips, that is a card of its own and possibly a retirement to unwind.Size/model suggestion:M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:39Z · 本评论来自分诊座位Generated by Claude Code
os-bill commentedon Sep 12, 2026
Claim: PM dispatch by the
domain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, at 2026-09-12T04:27Z. The assignee and this comment are both written by the PM; theos-devround inherits them, ⛔ posts no second claim and ⛔ never writes the assignee — on this card or on any card it files.Branch:
claude/issue-17424-liveness-depth-two-recursionpackages/spec/scripts/is not a published payload.Declared file face (region level) —
packages/spec/scripts/liveness/check-liveness.mts(the walk) anddrill.mts(the authoring side). ⛔ Ledger DATA files are READ-ONLY unless a fixture is needed; ⛔ do not edit any existing verdict to make the walk agree.Concurrency at dispatch (2026-09-12T04:27Z) — sibling dispatches this batch: #16885 (
ui/view.zod.ts), #17260 (ui/component.zod.ts). Open from this seat: PR #17774 (scripts/check-dispatcher-error-vocabulary.mjs), PR #17776 (scripts/check-adr-0087-registration.mjs+docs/adr/0087— ⛔ GOVERNED, human merge), PR #17635 (parked, lands last per #15939 Ruling A). ⇒ disjoint from all. ⭐ The round still owes its own check.Changeset — ⛔ do NOT assume owed or not-owed. Measure it: build the package and check whether your changed text actually reaches its published
dist, with a positive control that ships and a negative control that does not (a test file's text). Two rounds tonight reached opposite, correct answers this way. If owed:.changeset/17424-<slug>.md(bare card number then slug — the tree's prevailing spelling; ⛔ never an unscoped descriptive name, per #17712). If not owed:skip-changesetand say why.Worktree —
../objectstack-issue-17424; ⛔ never edit the shared primary checkout. The branch name above is fixed (the Claim names it).domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· claim posted 2026-09-12T04:27ZGenerated by Claude Code
os-bill commentedon Sep 12, 2026
os-dev-report
{ "card": 17424, "status": "done", "pr": "https://github.com/objectstack-ai/objectstack/pull/17810", "branch": "claude/issue-17424-liveness-depth-two-recursion", "head_sha": "d921eafbfcde28a3082d650599d2970180d32017", "files_changed": [ "packages/spec/scripts/liveness/check-liveness.mts", "packages/spec/scripts/liveness/orphans.mts", "packages/spec/scripts/liveness/drill.mts", "packages/spec/scripts/liveness/undrilled-containers.baseline.json", "packages/spec/scripts/liveness/check-liveness.test.ts", "packages/spec/scripts/liveness/orphans.test.ts", "packages/spec/liveness/README.md", ".changeset/17424-liveness-depth-two-recursion.md" ], "premise_still_valid": true, "premise_controls": "Re-read the walk at origin/main 65481183b8 as the card demanded: check-liveness.mts read led.children[ck] and had no recursion into the child's own children — the 'drill one level' comment present at 1, drillChildren at 0. LIT CONTROL that the premise could have come back the other way: the same probe on the fixed file returns drillChildren at 4, so the probe discriminates. Second control, aimed one directory over rather than only at the file the card named: the authoring side (drill.mts) and the reverse direction (orphans.mts) were both read — orphans.mts carried the SAME one-level bound and was NOT named on the card. It is fixed here because recursing the forward pass alone would have built a fresh silence at depth two.", "depth_limit_chosen": "The LEDGER's own nesting — the walk descends exactly as far as a children map is declared, with no artificial limit. Why: a parsed ledger is finite acyclic JSON, so termination is bounded by data the author can read rather than by a constant they cannot; and driving from the SCHEMA instead would recurse into genuinely recursive shapes (a NavigationItem contains NavigationItems) and fan the undrilled baseline out by an unbounded amount with no evidence behind any of it, which drill.mts's own header says is worse than an honest coarse verdict. MAX_DRILL_DEPTH = 8 is a TRIPWIRE, not the working limit: it exists only so the boundary cannot hide anything, and it is unreachable on today's ledgers (deepest declared nesting is 1).", "unclassified_reporting": "Four named sites, all of which FAIL the gate rather than warn. (1) The ceiling: past MAX_DRILL_DEPTH every key under the cut is pushed to report.unclassified as 'nested past the 8-level drill ceiling — NOT walked, so nothing here is classified'. (2) No verdict anywhere at depth N: reported as type/a.b.c, the same shape depth one already used. (3) A children map declared on a non-container at depth N: reported with the same 'declared children but property is not a container' message as depth one. (4) A ledger row naming a key its container never had, at ANY depth: findOrphanEntries now takes a PATH and recurses, so it lands in report.orphanEntries instead of being dropped. Two deliberate silences, both stated in the code: an orphan's own subtree is not listed (the parent row is the single fix), and a non-container's children is reported once by the forward pass rather than twice under two headings.", "verdict_diff": { "changed": "NONE", "command": "pnpm exec tsx scripts/liveness/check-liveness.mts --json captured at 65481183b8 (pre) and post-fix, then diffed field by field with: python3 -c \"import json;a=json.load(open('prefix.json'));b=json.load(open('post.json'));[print('MOVED:',k) for k in sorted(set(a)|set(b)) if a.get(k)!=b.get(k)]\"", "result": "Exactly one field moved: undrilledNew, 0 to 54. The types map (per-type byStatus for all 36 governed types) and totals compared EQUAL as whole objects; unclassified, orphanEntries and unknownStatus were empty on both sides. Totals unchanged: live 850, planned 10, dead 93, experimental 5, live-elsewhere 1.", "dead_flips": "None. No dead verdict flipped in either direction, so there is no retirement to unwind and no card to hand back.", "why_structural_not_lucky": "A scan of all 36 shipped ledger files finds ZERO depth-2 children maps, so the new recursion branch is unreachable on shipped data. That zero is a reading because the same scan returns non-zero the moment one exists — which is exactly what the ablation fixture creates, and the post-fix run then reports it. Had a depth-2 map existed in the tree, the field-by-field diff above is the instrument that would have caught the flip.", "second_order": "What DID move is the gate's census of its own completeness, not any verdict: 58 containers / 302 child keys to 109 / 555, plus 7 to 10 resolved deferrals covering 277 to 331." }, "baseline_migration": "54 containers became visible at once — every one already riding on a blanket verdict BELOW a drilled container, where a one-level walk could not see it and therefore never counted it. 3 resolve as deferrals to app/navigation (app/navigation.children, app/areas.navigation, manifest/navigationContributions.items — all the same 18-key NavigationItem surface; recording them as 'classified nowhere' would have been false in the other direction, and the gate's EQUALITY check is what makes the deferral resolved rather than believed). 51 are recorded in the containers list. The migration and its reading are stated in that file's own _containers header, so a reviewer seeing 51 rows arrive reads it as a denominator correction rather than a regression. dashboard/widgets.chartConfig (14 keys) — the coordinate #17385 is blocked on — is now in the population; that drill is NOT in this PR.", "ablation": { "shape": "The PRE-FIX tool was materialised from the branch point into an untracked sibling directory (packages/spec/scripts/liveness-prefix/, so specRoot and repoRoot still resolve) and run against the SAME fixture as the post-fix tool. A whole-directory copy rather than one file, because orphans.mts changed too and a mixed pair would have contaminated the reading.", "on_disk_proof_before_reading": "Pre-fix copy is the old walk: 'drill one level' comment at 1, drillChildren at 0, against 4 in HEAD's copy; its baseline carries the pre-migration 58 rows. Fixture carries the depth-2 map: marker neverWasAKeyOnChartConfig at 1 in the fixture ledger, 0 in the shipped one. All counted with grep -c on the exact text, never inferred from an editor's exit code.", "fixture": "A nested children map on dashboard.widgets.chartConfig carrying (a) a rotted evidence pointer and (b) a key that has never existed on ChartConfig.", "pre_fix": "exit 0. chartConfig.title mentions = 0, neverWasAKeyOnChartConfig = 0, rotted sql-driver.ts pointer = 0 — and it printed its success line claiming everything was classified. That IS the defect.", "post_fix": "exit 1. Names 'dashboard/widgets.chartConfig.title to packages/plugins/driver-sql/src/sql-driver.ts' (evidence RESOLVED at depth two), reports 'dashboard/widgets.chartConfig.neverWasAKeyOnChartConfig' as an ORPHAN rather than guessing what it meant, and lists the 13 remaining keys as unclassified.", "restore": "No repo byte was ever mutated — pre-fix tool in an untracked dir, fixture in a temp dir, both removed by a trap on EXIT INT TERM whose paths are absolute, resolved from git rev-parse --show-toplevel. Restore proven by OBSERVATION, not by an exit code: git status --porcelain empty, git diff HEAD --stat empty, and git hash-object equal to the HEAD blob for all six touched paths — README.md d12e5acd94d5fcf311aee524685c13010bb76e74, dashboard.json 93dabafa8790d2ff4fc607db7751ef95454cb0a6, check-liveness.mts 5d731dad20728d8aca8679139ea4fbf04fa4301d, orphans.mts 75dfb958e3803eb707a2bf6094e0acf21bb90346, drill.mts 14f078410c7b90a5c96edd195ba66d60dac66ef9, undrilled-containers.baseline.json d18b2c8cadcce2904951a254b3ca067521113da7. No empty hash, no missing path.", "permanent_cover": "The ablation leaves nothing behind. 9 new cases in check-liveness.test.ts spawn the real gate the way CI does and read named report BUCKETS via --json rather than searching the prose, because 'reported somewhere' was never the question. undrilledStale is deliberately NOT used as evidence of recursion: a coordinate the walk cannot see is reported stale too, so it agrees pre- and post-fix and does not discriminate. The ceiling case uses app.navigation, whose children are NavigationItems, so a real recursive shape carries the ledger past the constant." }, "changeset_decision": { "verdict": "OWED — .changeset/17424-liveness-depth-two-recursion.md, @objectstack/spec: patch", "measurement": "npm pack --dry-run --json on the BUILT package: 2011 published entries, 216 under dist/. scripts/ contributes 0 of 2011. grep of the built dist/ for the changed text: drillChildren 0 files, MAX_DRILL_DEPTH 0 files.", "positive_controls": "liveness/ ships at 38 entries and liveness/README.md is 1 of them; a real shipped symbol (MetadataTypeSchema) resolves in 40 of the 216 dist files — so the dist grep could have come back the other way.", "negative_control": "scripts/liveness/check-liveness.test.ts — 0 published entries, as expected of a file that does not ship.", "why_owed_anyway": "files[] ships the liveness DIRECTORY whole, and liveness/README.md is one of those 2011 entries. It is the ledger's authoring contract — its 'Granularity — drill one level' section is what an author reads before writing a children map. Those published bytes change, and they change because this PR made them false. So the tool is not what owes the changeset; the README is.", "trap_avoided": "The first pack ran before the build and reported dist entries = 0. It was re-run after a green 73/73 build so the dist leg is a real reading rather than a measurement of an absent directory." }, "verification": { "tests": "pnpm --filter @objectstack/spec test — 473 test files / 13450 tests, ALL PASSED (vitest's own summary line). The lock wrapper's VERDICT line is absent from that log because I terminated the wrapper myself after mis-spelling its args, and the child suite ran to completion — so the reading is vitest's summary, not an inferred exit code. Targeted re-run after the late README commit: check-liveness.test.ts + readme-table.test.ts + orphans.test.ts + drill.test.ts, 4 files / 140 tests passed, VERDICT command-exit 0.", "typecheck": "pnpm --filter @objectstack/spec typecheck — VERDICT command-exit 0. The usual hole is closed here by a SECOND gate rather than assumed: check:test-typecheck compiles the test layer separately (54 files under tsconfig.test.json), so the new cases are type-checked rather than silently excluded.", "lint": "pnpm lint (eslint . --no-inline-config, the WHOLE repo) — exit 0 in the foreground. No narrowing claimed and none needed: the full declared population ran.", "gates": "scripts/pm/dispatch-gates.mjs --commands derived 67 families for these 8 paths; 66 ran and ALL exited 0, every exit code captured before any pipe. Five (dts-closure, dual-build-cjs-loads, lean-entry-closure, sourcemap-no-sources-content, type-check-debt) first returned exit 3 = PREREQUISITE NOT MET, were re-run after a green full build (73/73 tasks), and then exited 0.", "not_measured": [ "pnpm check:pm-dispatch-gates — exceeded the container's roughly 10-minute foreground ceiling twice under contention (SIGTERM, exit 143). NOT MEASURED: read as neither green nor red. It is a checker-health self-test that grades the dispatch-gates script itself, which this diff does not touch.", "pnpm check:type-check-debt on the FINAL head — exit 3, PREREQUISITE NOT MET, reason printed by the gate: '@objectstack/hono has no built type entry point on disk'. That is a sibling agent's build churn on this shared box, not this diff. The reading that stands is its exit 0 from the post-build gate run; the only delta between those two heads is Markdown prose plus a Markdown changeset, which cannot move a TypeScript debt ledger." ], "declared_limitation": "The gate-family derivation was made on this branch's base (65481183b8). dispatch-gates reports that origin/main has since advanced about 15 commits and that 12 of the files the derivation reads changed in that range, so a family ADDED in that window would not appear in my 67. I did not merge main to close it: that would owe a full re-verification round on a heavily contended box for a benefit CI delivers directly, since CI derives against the real merge state. Declared rather than silent.", "hygiene": "Control-byte scan of all 8 changed files: 0 hits, with a lit control (a planted 0x01 byte is detected at 1 by the same probe). Commit messages contain zero occurrences of '#' followed by digits, so no card relation is even expressible; the repo's own pre-push hook confirmed it — 'check:commit-card-trailers: 2 commit message(s) on this push carry no card relation'. The card relation appears exactly once, in the PR body, as Fixes #17424. PR body read back from GitHub after creation: byte-identical to what was sent, footer block intact.", "worktree": "../objectstack-issue-17424, dedicated. The shared primary checkout was never edited. No git stash. No kill by name — the two processes stopped were stopped by a PID I had recorded." }, "mcp_calls": "0 — every GitHub read and write went through repo-scoped REST (probed first: GET /issues/17424 returned HTTP 200), and every local read through git.", "out_of_scope_findings": [ "noted, not filed: the gate's --dump TYPE debug mode still drills exactly one level, so it cannot show a depth-2 coordinate the walk now classifies. Not (a)/(b)/(c) — it never claimed to reach deeper and it misreports nothing; it is now merely shallower than the walk. NAMED SUCCESSOR: the #17385 drill round, which will author the depth-2 dashboard/widgets.chartConfig table and is the first consumer that will want --dump to reach it.", "noted, not filed: five governed types (crud_endpoints, metadata_endpoints, batch_endpoints, route_generation, realtime_subscription) were deliberately ROOTED at RestServerConfig / RealtimeConfig sub-schemas BECAUSE the walk drilled one level — their README rows say so in as many words. That constraint is now gone, so they could in principle be consolidated under their parent config schemas with nested children. A design opportunity, not a defect: the current rooting still works and is still honest, and consolidating would move ledger coordinates, README rows and the GOVERNED list — not a dev agent's call. Their rationale prose is corrected to past tense in this PR; the decision itself is untouched. NAMED SUCCESSOR: whoever next revisits SPEC_ONLY_SCHEMAS rooting.", "noted, not filed: check-liveness.test.ts now spawns the real gate 62 times, and ran about 255s locally for 53 cases before I added 9 more. Each spawn is a full ledger walk. An observation about test cost, not a defect — the spawn-the-real-gate design is deliberate and documented in that file's header, and it is why the #5623 class is catchable at all. NAMED SUCCESSOR: whoever next adds a case to that file and has to wait for it." ], "open_questions": [] }Generated by Claude Code
os-bill commentedon Sep 12, 2026
Closed out by CONTENT on
origin/mainc9eb773bab(2026-09-12T08:08Z), ⛔ not by the merge eventPR #17810 merged.
pm:dispatchedand the assignee were stripped in the same write and read back.git grep -oF, overpackages/spec/scripts/liveness/)drillChildrenMAX_DRILL_DEPTHexport function— ⭐ lit control⭐ The count only says the name is there; the recursion is what the card asked for, so the sites were read:
check-liveness.mts:986isdrillChildren(type, childPath, cs[ck], cled, cat, depth + 1)insidedrillChildrenitself, with the entry call at:1033passing depth 1. ⇒ the walk descends, it does not merely name a helper.What this seat checked before landing, beyond the round's own report
The baseline grew by 51 rows in one commit. ⭐ That is a ratchet extension, not a raised ceiling, and it was verified rather than accepted:
undrilledNewandundrilledStaleboth still fail the gate, so a 52nd is refused and a row that becomes unnecessary is refused too;skip/ignorehits are all inside comments describing the defect, thecontinue;hits are all in the report-then-continue classification path, and there are zero allowlist entries, exemptions, or exit-code changes;-line in the baseline for a deletedtranslation/settingsCommonrow. It is a comma change — that row is present at line 139 of the merged file. No baseline row was removed.Generated by Claude Code
fix(spec): drill the liveness ledger to the depth it declares, and re…
feat(spec)!: retire ListViewSchema.navigation.view, the detail-view b…