Commit c9eb773
fix(spec): drill the liveness ledger to the depth it declares, and report what the walk did not reach (#17810)
Fixes #17424
`packages/spec/scripts/liveness/check-liveness.mts` read
`led.children[ck]` and never recursed into a child's own `children`. A
`children` map written at **depth two** was therefore accepted by the
file format and then ignored: no evidence path resolved, no key reported
unclassified, no container reconcile, and **no line of output saying any
of it was missing**. Because the enforce-or-remove channel acts on this
gate's `dead` verdicts, a silently skipped subtree could retire a key
that was alive.
Triage asked for two things and said the second matters as much as the
first. Both are here.
## 1. The walk recurses
`drillChildren` descends wherever the ledger declares a nested
`children` map. The working depth limit is therefore the **ledger's own
nesting** — a bound the author can read in the file — not a constant in
the gate. Two things follow it down:
- **The reverse (orphan) direction.** `findOrphanEntries` now takes a
PATH rather than a key and recurses with the walk. A reverse direction
that only ever asked about depth one would have stopped asking exactly
where the forward pass started looking.
- **The container reconcile.** A drilled child that is itself a
container carries a blanket verdict over its own subtree, so it now owes
the same declared disposition its top-level peers already owed: drilled,
deferred, or recorded.
## 2. Nothing the walk does not reach is silent
`MAX_DRILL_DEPTH` (8) is a **tripwire, not the working limit**. Every
key below it is reported UNCLASSIFIED, which fails the gate. A depth
limit the instrument does not announce would rebuild this card's own
defect one level lower, at the one place a check can least afford it.
Each of the three ways an entry goes unclassified at depth one is now
pinned at depth two as well: no verdict anywhere, a `children` map on a
non-container, and a row naming a key its container never had.
## Which verdicts changed: none
The full report was captured before and after and diffed field by field.
```
pnpm exec tsx scripts/liveness/check-liveness.mts --json (pre-fix, at 6548118)
pnpm exec tsx scripts/liveness/check-liveness.mts --json (post-fix)
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)]"
```
**Exactly one field moved: `undrilledNew`, 0 to 54.** `types` (the
per-type `byStatus` map for all 36 governed types) and `totals` are
byte-identical objects, and `unclassified`, `orphanEntries` and
`unknownStatus` are empty on both sides.
```
live 850 · planned 10 · dead 93 · experimental 5 · live-elsewhere 1 (before)
live 850 · planned 10 · dead 93 · experimental 5 · live-elsewhere 1 (after)
```
**No `dead` verdict flipped, so no retirement is in question** and there
is no card to hand back. The reason is structural rather than lucky, and
the control is lit: a scan of all 36 shipped ledger files finds **zero**
depth-2 `children` maps today, so the new recursion branch is
unreachable on shipped data. Had any existed, that count would have been
non-zero and the diff above is what would have caught it.
## The baseline migration
54 containers became visible at once, and the jump is a **denominator
correction, not new debt**. Every one of them was 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 are genuinely classified elsewhere** and resolve as deferrals to
`app/navigation`: `app/navigation.children`, `app/areas.navigation`,
`manifest/navigationContributions.items` are all the same NavigationItem
surface, 18 keys each. Recording them as "classified nowhere" would have
been the baseline's own false claim in the other direction. The gate's
equality check is what makes the deferral hold.
- **51 are recorded** in the `containers` list, with the migration and
its reading stated in that file's own `_containers` header.
- Census: 58 containers / 302 child keys, to 109 containers / 555 child
keys, plus 7 to 10 resolved deferrals covering 277 to 331.
`dashboard/widgets.chartConfig` — the coordinate #17385's per-key drill
is blocked on — is now in that population. **That drill is not in this
PR.** A previous round correctly refused to write it because the table
would have looked classified and been read by nothing; this unblocks it.
## Ablation, not a green
A gate that would have passed either way is not evidence, so the pre-fix
tool was materialised from the branch point and run against the same
fixture.
**On-disk proof before reading either run.** The pre-fix copy was
verified to be the old walk (`drill one level` comment present at 1,
`drillChildren` at 0, against 4 in HEAD's copy, and its baseline
carrying the pre-migration 58 rows). The fixture was verified to carry
the depth-2 map (`neverWasAKeyOnChartConfig` at 1 in the fixture, 0 in
the shipped ledger). Both legs ran under `trap ... EXIT INT TERM`.
The fixture nests a `children` map on `dashboard.widgets.chartConfig`
carrying a rotted evidence pointer and a key that has never existed on
ChartConfig.
```
PRE-FIX exit 0
mentions chartConfig.title ................. 0
mentions neverWasAKeyOnChartConfig ......... 0
mentions the rotted sql-driver.ts pointer .. 0
and printed: "all governed-type properties ... are classified"
POST-FIX exit 1
dashboard/widgets.chartConfig.title -> packages/plugins/driver-sql/src/sql-driver.ts
dashboard/widgets.chartConfig.neverWasAKeyOnChartConfig (orphan, not guessed)
dashboard/widgets.chartConfig.type ... .series ... (13 unclassified)
```
**Restore proven, not assumed.** No repo byte was mutated — the pre-fix
tool lived in an untracked sibling directory and the fixture in a temp
dir — and the restore was verified by `git hash-object` on every touched
path against its HEAD blob, with `git status --porcelain` and `git diff
HEAD --stat` both empty. The six hashes are in the round report.
The ablation is one-time and leaves nothing behind. The permanent
regression cover is 9 new cases in `check-liveness.test.ts`, each
spawning the real gate the way CI does and reading a named report BUCKET
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
does not discriminate. The ceiling case uses `app.navigation`, whose
`children` are NavigationItems, so the ledger can be nested past the
constant against a real recursive shape.
## Changeset: owed, and measured
`packages/spec/scripts/**` — the tool, its tests and the baseline —
contributes **0 of 2011 published entries** in the packed tarball, and
neither `drillChildren` nor `MAX_DRILL_DEPTH` appears in any of the 216
built `dist/` files. Positive controls both lit: `liveness/` ships at 38
entries, and a real shipped symbol resolves in 40 dist files. Negative
control: `scripts/liveness/check-liveness.test.ts`, 0.
But `files[]` ships the `liveness` directory **whole**, and
`liveness/README.md` is one of those 2011 entries. It is the ledger's
authoring contract, and its "Granularity — drill one level" section is
what an author reads before writing a `children` map. So the published
bytes that change are documentation, and they change because this PR
made them false — which is why five per-type rooting rationales and two
present-tense claims elsewhere in that file are corrected in place
rather than left. The rooting decisions themselves are untouched and
still stand; only their tense moved.
`.changeset/17424-liveness-depth-two-recursion.md`, `@objectstack/spec:
patch`. No ledger verdict file was edited.
## Verification
- `pnpm --filter @objectstack/spec test` — **473 test files, 13450
tests, all passed**.
- `pnpm --filter @objectstack/spec typecheck` — exit 0, and
`check:test-typecheck` compiles the test layer separately (54 files
under `tsconfig.test.json`), so the usual "typecheck excludes the tests"
hole is closed here by a second gate rather than assumed away.
- `pnpm lint` (`eslint . --no-inline-config`, the whole repo) — exit 0.
No narrowing claimed; the full declared population ran.
- **66 of the 67 gate families** derived by
`scripts/pm/dispatch-gates.mjs` for these 8 paths — all exit 0. The one
exception and the two NOT MEASURED readings are named in the round
report rather than folded into that count.
- `pnpm check:liveness` green on the final head; the success line now
reads "at every depth the ledger drills".
## Notes for review
- **Clause-②: no** — this PR puts no new key on any published payload.
- No governed surface is touched (`docs/adr/**`, `.claude/**`,
`skills/**`, `AGENTS.md`, `CLAUDE.md` — 0 path hits, checked against the
`GOVERNED_SURFACES` register).
- Ledger verdict data is untouched: the only file under
`packages/spec/liveness/` in this diff is `README.md`.
- The derivation of the gate list was made on this branch's base;
`origin/main` has advanced since, so a family added in that range would
not appear in it. CI derives against the real merge state.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 48203ff commit c9eb773
8 files changed
Lines changed: 584 additions & 61 deletions
File tree
- .changeset
- packages/spec
- liveness
- scripts/liveness
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
18 | | - | |
| 18 | + | |
19 | 19 | | |
20 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
21 | 29 | | |
22 | 30 | | |
23 | 31 | | |
| |||
904 | 912 | | |
905 | 913 | | |
906 | 914 | | |
| 915 | + | |
| 916 | + | |
| 917 | + | |
| 918 | + | |
| 919 | + | |
| 920 | + | |
| 921 | + | |
| 922 | + | |
| 923 | + | |
| 924 | + | |
| 925 | + | |
| 926 | + | |
| 927 | + | |
| 928 | + | |
| 929 | + | |
| 930 | + | |
| 931 | + | |
| 932 | + | |
| 933 | + | |
| 934 | + | |
| 935 | + | |
| 936 | + | |
| 937 | + | |
| 938 | + | |
| 939 | + | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
| 945 | + | |
| 946 | + | |
| 947 | + | |
| 948 | + | |
| 949 | + | |
| 950 | + | |
| 951 | + | |
| 952 | + | |
| 953 | + | |
| 954 | + | |
| 955 | + | |
| 956 | + | |
| 957 | + | |
| 958 | + | |
| 959 | + | |
| 960 | + | |
| 961 | + | |
| 962 | + | |
| 963 | + | |
| 964 | + | |
| 965 | + | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
| 975 | + | |
| 976 | + | |
| 977 | + | |
| 978 | + | |
| 979 | + | |
| 980 | + | |
| 981 | + | |
| 982 | + | |
| 983 | + | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
907 | 999 | | |
908 | 1000 | | |
909 | 1001 | | |
| |||
912 | 1004 | | |
913 | 1005 | | |
914 | 1006 | | |
915 | | - | |
| 1007 | + | |
| 1008 | + | |
916 | 1009 | | |
917 | 1010 | | |
918 | 1011 | | |
919 | 1012 | | |
920 | 1013 | | |
921 | | - | |
922 | | - | |
| 1014 | + | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
| 1018 | + | |
| 1019 | + | |
| 1020 | + | |
| 1021 | + | |
| 1022 | + | |
| 1023 | + | |
923 | 1024 | | |
924 | 1025 | | |
925 | 1026 | | |
| |||
929 | 1030 | | |
930 | 1031 | | |
931 | 1032 | | |
932 | | - | |
933 | | - | |
934 | | - | |
935 | | - | |
936 | | - | |
937 | | - | |
938 | | - | |
939 | | - | |
940 | | - | |
| 1033 | + | |
941 | 1034 | | |
942 | 1035 | | |
943 | 1036 | | |
944 | 1037 | | |
945 | 1038 | | |
946 | 1039 | | |
947 | | - | |
948 | | - | |
949 | | - | |
| 1040 | + | |
950 | 1041 | | |
951 | 1042 | | |
952 | 1043 | | |
| |||
970 | 1061 | | |
971 | 1062 | | |
972 | 1063 | | |
| 1064 | + | |
| 1065 | + | |
| 1066 | + | |
| 1067 | + | |
973 | 1068 | | |
974 | 1069 | | |
975 | 1070 | | |
| |||
981 | 1076 | | |
982 | 1077 | | |
983 | 1078 | | |
984 | | - | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
985 | 1083 | | |
986 | 1084 | | |
987 | 1085 | | |
| |||
1626 | 1724 | | |
1627 | 1725 | | |
1628 | 1726 | | |
| 1727 | + | |
| 1728 | + | |
| 1729 | + | |
| 1730 | + | |
1629 | 1731 | | |
1630 | | - | |
| 1732 | + | |
1631 | 1733 | | |
1632 | 1734 | | |
1633 | 1735 | | |
| |||
0 commit comments