Skip to content

chore(spec): re-anchor six liveness ledgers to consuming symbols (#13003 batch 3) - #13039

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-13003-anchor-adoption-b3
Aug 28, 2026
Merged

chore(spec): re-anchor six liveness ledgers to consuming symbols (#13003 batch 3)#13039
os-trump merged 3 commits into
mainfrom
claude/issue-13003-anchor-adoption-b3

Conversation

@os-trump

@os-trump os-trump commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Part of #13003

Adoption batch 3 of the #12516 symbol-anchor citation grammar, file-disjoint from batches 1 (#13035) and 2 (#13038). Every ledger counter, gate verdict and the ablation transcript below is at db2d9ff39, which merges origin/main incl. batch 2 — one tree. Patch round 1 (fb3a27f07) then executed the floor ruling; its own readings are in its section and are quoted at that head.

Ninety-five path:NNN citations across six ledgers become path#symbol, together with every path-only pointer in the same entries. Seventy-nine of the ninety-five were already wrong, every one IN RANGE and therefore invisible to the existence check, the line bound and the key-mention check alike.

⚠️ READ FIRST — this batch crossed a pin-test floor (escalated, ruled, executed in patch round 1)

Two tests in packages/spec/scripts/liveness/ failed at db2d9ff39. They were never a defect in these ledgerscheck:liveness itself is green there, 82 of 82 citations in range and 383 of 383 anchors resolving. They are a non-vacuity floor calibrated to a population this worklist is chartered to drain:

Test Assertion Measured here Was
evidence.test.ts:373 "every local path:NNN citation names a line that file has" expect(citations).toBeGreaterThan(100) 78 171 on main
check-liveness.test.ts:194 "prints the citation count and how many are in range" expect(Number(m[1])).toBeGreaterThan(100) 82 175 on main

The two numbers differ because they count different populations, which I measured rather than assumed: 78 live-entry evidence citations + 4 producer citations = the 82 the gate prints.

Both floors carry the same comment — "Guard against the parser silently degrading to 'extracts nothing' — that would make the assertion above vacuously true" (the #5623 lesson). The guard is right. Its NUMBER was chosen when line citations were the only grammar and the population could only grow. The sibling guard that #12516 added for the anchor population is toBeGreaterThanOrEqual(2), commented "the two #12516 repoints are the day-one anchored population" — a floor, deliberately not a high-water mark.

I did not fix it in the first round, on purpose: it was outside the file surface this batch declared, the closing batch hits it again near zero, and the right number is a judgment the endgame should settle once. The four-axis analysis with options went up as needs_decision on #13003, and the ruling came back — see the next section.

Nothing else in the derived gate family was red at db2d9ff39 — 23 of 25 EXIT=0, two NOT MEASURED.

Patch round 1 — the ruling, executed (fb3a27f07)

Ruled on #13003 (comment 5458356183): option A, with the constant set to toBeGreaterThan(0) rather than the e.g. 10 I had suggested. The reasoning in the ruling is the part worth carrying: any constant above zero can red again mid-batch-4 on legitimate drainage — the measured tail is 82 across 13 files and the endgame is near-zero — which would re-create this same escalation one batch later. > 0 is the only floor that never lies during the migration, still catches the named failure mode (the parser extracting NOTHING, #5623) at full strength, and reds at precisely the moment the endgame decision is due.

Surface widening authorized for exactly two files. The diff is two constants plus their guard comments:

File Change
packages/spec/scripts/liveness/evidence.test.ts expect(citations).toBeGreaterThan(100)toBeGreaterThan(0)
packages/spec/scripts/liveness/check-liveness.test.ts expect(Number(m[1])).toBeGreaterThan(100)toBeGreaterThan(0)

Both guard comments rewritten to carry three things: the #5623 lesson stays (the guarded failure mode is the parser silently extracting nothing, and zero catches that exactly); #13003 named as the migration that drains this population by design, with the measured trajectory 300 → 175 → 82 and the ruling comment cited; and the instruction that when the population legitimately reaches zero, the assertion AND its guard comment are DELETED in the same PR that retires the last line citation — a conscious decision at zero, never a silent one.

The two sibling floors are deliberately untouched and stay at 100: local paths (evidence.test.ts:344) and evidence paths (check-liveness.test.ts:501), both guarding populations that are not draining (464 today). Two constants changed, two constants left — verified by grep, not by intent.

Evidence at fb3a27f07. The pair of reds was reproduced first at db2d9ff39 (78 and 82 against > 100), so this is a measured before/after rather than an assumed one.

  • pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/liveness/ through the shared verify lock: Test Files 10 passed (10) / Tests 254 passed (254), EXIT=0.
  • Gate family re-derived for the widened surface: 9 paths, 30 families matched (up from 25). The five new path-derived ones all EXIT=0: check:cross-package-test-inputs, check:objectql-double-limit, check:pm-governed-merges, check-ci-filter-parity, check-cross-package-test-inputs.mjs.
  • The derivation also fired its convention trigger ("adds or edits a test file"), which no path derivation can name. All six run, all EXIT=0: check:query-options-erasure, check:type-check-coverage, check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, and check:type-check-debt.
  • check:type-check-debt deserves its own line, because it is the one gate here that refuses rather than guesses. On the unbuilt worktree it threw — "--re-measure cannot run: 56 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk … measuring now would not fail, it would silently measure a DIFFERENT WORLD" — which is NOT MEASURED, never a pass. So the closure was built exactly as lint.yml does (turbo run build --filter='./packages/*' --filter='./packages/*/*', 70/70 tasks, 5m20s) and it was re-run for a real reading: EXIT=0, 31 ledger entr(ies) re-measured in 189.0s, 1570 raw tsc error(s) total, none above its recorded number · surplus: none — every entry sits exactly at its measurement, so any new error is red.
  • Building the closure also converted the standing check-dev-prereqs NOT MEASURED into a real green: EXIT=0, ✓ 67 package build artifacts present · ✓ @objectstack/spec built from the sources on disk. Only one NOT MEASURED is left in this PR, scripts/pm/check-half-states.mjs (EXIT=3, no GitHub credential in the container).
  • pnpm --filter @objectstack/spec typecheck EXIT=0 across all three legs, and the leg that actually covers the edit was identified by measurement rather than assumed: tsc -p tsconfig.scripts.json --listFiles names both edited files (count 2), while the package's base tsc --noEmit program names neither (count 0). A green typecheck that excluded the edited files would have said nothing about them.
  • pnpm check:nul-bytes EXIT=0, plus a grep -naP self-scan over the C0 class plus DEL on both edited files: clean.
  • Ratchet families re-run on the committed head after the commit (check:liveness, check:engine-double-contract, check:where-matcher, check:query-options-erasure, check:type-check-coverage): all EXIT=0.

Per-entry verdicts

= the cited line was still accurate, so the change is a pure grammar migration. = the line had rotted and the entry was REPOINTED; the FROM column says what that line lands on today.

email_template.json — 23 citations, 12 accurate, 11 rotted

Entry FROM (what that line is today) TO
name bootstrap-declared-email-templates.ts:78name: tpl.name #mapTemplateToRow
name email-service.ts:411 — a TemplateLoader docblock line #resolveAndRenderTemplate
label :79label: tpl.label #mapTemplateToRow
category :80category: tpl.category #mapTemplateToRow
locale :81locale: tpl.locale #mapTemplateToRow
locale email-service.ts:411-416 — the same docblock #resolveAndRenderTemplate + #DEFAULT_TEMPLATE_LOCALE
subject :82subject: tpl.subject #mapTemplateToRow
subject email-service.ts:444 — an EmailServiceOptions docblock about sys_email PERSISTENCE #resolveAndRenderTemplate
bodyHtml :83body_html: tpl.bodyHtml #mapTemplateToRow
bodyHtml email-service.ts:445persistence?: EmailPersistence; #resolveAndRenderTemplate
bodyText :84 — the body_text spread #mapTemplateToRow
bodyText email-service.ts:446-448 — the templateLoader / retries option docblocks #resolveAndRenderTemplate
variables :91is_system: tpl.isSystem, the line isSystem cites for itself #mapTemplateToRow
variables email-service.ts:427-431 — the EmailTemplateRow interface (the row SHAPE, not the gate) #resolveAndRenderTemplate
fromOverride :85-88 — the fromOverride spread #mapTemplateToRow
fromOverride email-service.ts:450-453logger / defaultTemplateContext options #sendTemplate + #rowEnvelope
replyTo :89 — the reply_to spread #mapTemplateToRow
replyTo email-service.ts:139 — a docblock on the sys_email outbox id contract, ~1,150 lines away #sendTemplate + #rowEnvelope
active :90active: tpl.active #mapTemplateToRow
active email-service.ts:418-419 — the bare */ closing a docblock #resolveAndRenderTemplate + #deactivateDeclaredEmailTemplate
isSystem :91is_system: tpl.isSystem #mapTemplateToRow
isSystem email-plugin.ts:544await target.sendInline({ in the operator TEST-MAIL button #upsertTemplate
description :92 — the description spread #mapTemplateToRow

All nine email-service.ts pointers rotted together and by one cause: #9225 split the resolver out of sendTemplate into resolveAndRenderTemplate, moving the reads from the 400s into the 1,170-1,310 band. The file exists, every cited line is still inside it, and the file still names each key — so all three existing checks pass on pointers that are ~750 lines wrong.

api.json — 21 citations, 4 accurate, 17 rotted

Entry FROM (what that line is today) TO
name endpoint-matcher.ts:210 — a real read of endpoint.name, but the publish-gate exclusion MESSAGE, ~22 lines above the tiebreaker this entry is about #buildEndpointIndex
path :199 — a bare continue; ending the parse-failure branch #endpointIndexKey + #buildEndpointIndex
method :115 — a module docblock about the store-read contract #normalizeEndpointMethod + #endpointIndexKey
summary openapi-endpoints.ts:214successStatus: 501 in the unsupported-type branch #buildEndpointOperation
description :215requiresRecordId: false in the same branch #buildEndpointOperation
type endpoint-executor.ts:216 — the docblock ABOVE planEndpointTarget #planEndpointTarget
target :233 — a bare }; closing the sibling branch's refusal literal #planEndpointTarget
objectParams.object :217 — a docblock line #planEndpointTarget + #executeObjectOperation
objectParams.operation :218 — a docblock line #planEndpointTarget + #executeObjectOperation + #requireRecordId
inputMapping.source api-mapping.ts:318const projected = {}, the accumulator two lines above the read #project + #applyInputMapping
inputMapping.target :322 — a comment two lines above the write #project + #mappingDeclarationRejection
inputMapping.transform :259 — the diagnostic-label line two above the refusal #mappingDeclarationRejection
outputMapping.source :318 — same as its twin #project + #applyOutputMapping
outputMapping.target :322 — same as its twin #project + #applyOutputMapping
outputMapping.transform :259 — same as its twin #mappingDeclarationRejection
authRequired endpoint-policy.ts:354message: 'Rate limit exceeded…', step ①'s 429 body: a DIFFERENT key's enforcement #applyEndpointPolicies + #anonymousDenial
rateLimit.enabled inbound-rate-limit.ts:88if (!budget?.enabled) return null #deriveBucketConfig
rateLimit.enabled endpoint-policy.ts:148 — a BLANK LINE #createEndpointRateLimiterRegistry
rateLimit.windowMs :91const windowMs = budget.windowMs ?? 60_000 #deriveBucketConfig
rateLimit.maxRequests :90const maxRequests = budget.maxRequests ?? 100 #deriveBucketConfig
cacheTtl endpoint-policy.ts:252 — the function's own declaration line #computeCacheControl + #applyEndpointPolicies ✓ position, ✗ PROSE

cacheTtl is the sharpest case in the batch for what an anchor buys: the LINE was accurate and the parenthetical named cacheControlHeader, which is not a symbol anywhere in packages/** (it is computeCacheControl). Line-accurate and name-wrong is invisible to every check the ledger has, because nothing has ever compared a citation's prose against its position — and it is the combination an anchor removes by construction, since the name IS the pointer now.

doc.json — 15 citations, 0 accurate, 15 rotted (100%)

Entry FROM (what that line is today) TO
name book.zod.ts:225 — a BLANK LINE inside the ResolvedBook interface #matchesInclude + #entryFromDoc
name rest-server.ts:3129upsertMany: batch.operations?.upsertMany ?? true #resolveDocAudiences
label book.zod.ts:198description?: string; (another key's declaration) #byOrderThenLabel + #entryFromDoc
label book.zod.ts:202 — the tags docblock doc.zod.ts#resolveDocLocale
description book.zod.ts:202 — the tags docblock #entryFromDoc
description rest-server.ts:3131defaultAtomic: batch.defaultAtomic ?? true #resolveBookTree
content rest-server.ts:3007 — a docblock about enableProjectScoping #registerMetadataEndpointsInner
content doc.zod.ts:120 — the middle of the tags docblock #resolveDocLocale
order book.zod.ts:198description?: string; #byOrderThenLabel
group book.zod.ts:238 — the ResolvedEntrySchema docblock #resolveBookTree
translations doc.zod.ts:110 — the /** that OPENS the tags docblock #resolveDocLocale
translations rest-server.ts:2996 — a comment about os serve config #resolveDocLocale
translations rest-server.ts:3388 — a comment about api.version #resolveDocLocale
tags book.zod.ts:213description?: string; in ResolvedEntry #matchesInclude + #resolveBookTree
tags rest-server.ts:3218this.registerSecurityEndpoints(bp); #resolveBookTree

Two whole-file moves under one ledger. In book.zod.ts the pointers had come to rest inside the ResolverDoc / ResolvedEntry INTERFACES — type declarations of the very fields whose consumers they claimed to cite, which is the most convincing wrong place a citation can land and proves nothing at all, since a dead key has a declaration too. In rest-server.ts the entire doc-serving block moved ~1,600 lines down.

book.json — 13 local citations, 0 accurate, 13 rotted (100%)

Entry FROM (what that line is today) TO
name rest-server.ts:3098patterns: crud.patterns in the CRUD-endpoint config #deriveImplicitPackageBook + book.zod.ts#resolveBookTree
label book.zod.ts:303.map((g, i) => …), the GROUP sort (a sibling key's read) #resolveBookTree
audience rest-server.ts:3113 — the comment // config to read). #audienceAllows
audience rest-server.ts:2969 — a docblock about 'v1/beta' splicing a path segment #audienceAllows
audience book.zod.ts:351entries.push(entryFromDoc(d)), tree ASSEMBLY not the gate #audienceAllows + #resolveDocAudiences
groups.key book.zod.ts:238 / :290 — a docblock, and a bare * continuation line #resolveBookTree
groups.label book.zod.ts:290 — the same bare * line #resolveBookTree
groups.order book.zod.ts:221key: string; in the ResolvedGroup interface #resolveBookTree
groups.include book.zod.ts:236 / :193 — a docblock, and a BLANK LINE #matchesInclude + #resolveBookTree
groups.package book.zod.ts:232 — the /** opening a docblock #resolveBookTree + #matchesInclude
groups.pages book.zod.ts:248-286 — a RANGE now spanning the response-contract schemas #resolveBookTree

audience is the one worth stopping on: the only security-shaped property on this type, three pointers, all three drifted, one of them onto a comment. The verdict was right the whole time and nothing cited could have shown it.

query.json — 12 citations, 0 accurate, 12 rotted (100%) — plus two entries that were never checkable at all

Entry FROM (what that line is today) TO
object protocol.ts:780if (seen.has(key)) return; in an unrelated walker (QUERY_AST_KEYS is at ~:2426) engine.ts#fillQueryAstDefaults + protocol.ts#QUERY_AST_KEYS
fields sql-driver.ts:1354 — an error-constructor helper, ~4,200 lines above the projection #findRows + engine.ts#planFormulaProjection
where sql-driver.ts:1331 — a docblock about UNSUPPORTED_QUERY_PARAM #findRows + #applyFilters + predicate-guard.ts#collectQueryFields
search engine.ts:2911 — the comment // this row sees. Never a copy. #expandSearchOnAst + search-filter.ts#expandSearchToFilter
searchFields engine.ts:2898const row = rows[index]; #expandSearchOnAst + search-filter.ts#resolveSearchFields
orderBy sql-driver.ts:1342 — a docblock about a throw site #orderKeysFor + #findRows
limit sql-driver.ts:1345 — a bare */ #findRows
offset sql-driver.ts:1344 — the docblock line * log. #findRows
top engine.ts:2853 and protocol.ts:3486 — both docblocks #ENGINE_QUERY_SLOTS + #foldEngineOptionAliases + protocol.ts#WIRE_DOLLAR_ALIASES
aggregations.function in-memory-aggregation.ts:167return v ?? null; inside projectGroupValue, a DIFFERENT function three lines above #aggregateBucket
aggregations.field in-memory-aggregation.ts:206if (!field) {, the branch taken when the key is ABSENT #aggregateBucket + #collectValues

query.where is the largest name-vs-position gap in the batch: the parenthetical named applyFilters, which is real and sits ~11,250 lines from the position cited beside it.

Two entries in this file were not merely stale but structurally unaskable, which is the webhook.json class from batch 2 reproduced twice more:

  • expand was written packages/objectql/src/engine.ts:2519+. The trailing + stops the token matching PATH_RE, so the scanner classified the whole thing as PROSE — never resolved, never bounded, never key-checked, while reading like the most precise citation in the file. One character. (Its consumer had also moved ~5,520 lines, to expandRelatedRecords.)
  • aggregations.filter — the file's YOUNGEST entry, hand-verified 2026-08-21 — wrote five of its eight consumers as bare package+filename prose (driver-sql sql-driver.ts, plugin-security predicate-guard.ts:89), which PATH_RE cannot parse, and the other three as path-only. Nothing had rotted; nothing was checkable either. All eight are now repo-rooted anchors, including the four driver refusal faces (#unsupportedAggregationFilterError, #refusePerAggregationFilter ×2, #performAggregation).

Six further entries (aggregations.alias, groupBy, having, and the second legs of object / searchFields / fields) were PATH-ONLY — a form the line bound cannot falsify by construction. having is the pointed one: its whole history is a key that was declared, forwarded, then dropped one layer before it could act, and four unbounded file pointers are exactly the evidence shape that could not have caught that.

job.json — 11 local citations, 0 accurate, 11 rotted (100%)

Entry FROM (what that line is today) TO
name app-plugin.ts:815 — a bare } else {; :833 — a bare try { #start
schedule app-plugin.ts:834const actions = collectBundleActions(this.bundle), a different metadata kind's registration job-schedule.ts#toBoundaryJobSchedule + #start
schedule cron-job-adapter.ts:88, db-job-adapter.ts:83 — both docblocks #CronJobAdapter, #DbJobAdapter
handler app-plugin.ts:824-830 — a comment about registering actions on POST /api/v1/actions/... #start + #collectBundleFunctions
retryPolicy app-plugin.ts:838-841if (actions.length > 0 && …), actions again #start
retryPolicy run-with-policy.ts:58-65 — the JobAttemptRecorder interface #runWithPolicy + #RETRY_DEFAULTS
timeout run-with-policy.ts:25-33jitter: false inside RETRY_DEFAULTS, a retryPolicy default #runWithPolicy + #withTimeout + #JobTimeoutError
timeout [producer] app-plugin.ts:875-876 — a bare } #start
enabled app-plugin.ts:820 — a bare } #start

This file is the worklist's own argument, stated by the ledger before I got there. Its 2026-08-02 _note records that the SEEDED lines had already drifted ~25 lines and were restamped with fresh numbers. Twenty-six days later every one of those fresh numbers had drifted again, by ~70-100 lines. Restamping a line is the same claim with a newer date.

The timeout producer pointer is worth its own line: producer resolves through the same resolver as evidence precisely so a call-site claim cannot be unfalsifiable — and a line-only producer pointer decays exactly as fast as a consumer one.

Census, from the gate's own counters

Base = origin/main at 9ee2dcfbd (incl. batches 1 and 2). Head = db2d9ff39.

Counter Base Head
line citations path:NNN 175, all in range 82, all in range
symbol anchors path#symbol 232, all resolving 383, all resolving
evidence paths (repo-local, declared by live) 444 declared / 444 resolved 464 / 464
key-mention pairs 444 asked, 443 anchored, 1 exempt 464 asked, 463 anchored, 1 exempt
re-verification clock 427 dated / 277 undated 427 / 277

The clock is unchanged on purpose and is worth stating rather than glossing: this batch adds zero new dates. All 72 entries it re-stamps to 2026-08-28 already carried a verifiedAt, so the undated backlog is untouched by batch 3.

check:liveness verdict line at head, quoted from the gate:

✓ every governed-type property at the walk's one-level granularity is classified, … every path:NNN citation names a line that file actually has, every path#symbol anchor names a symbol its file contains, and every cited file names the property it is evidence for (or is a recorded exemption), …

Ablation — the new anchors are CHECKED, not merely present

Gate-green alone would not prove it: an anchor token that stops matching PATH_RE degrades to prose SILENTLY, taking the citation's existence check with it. Committed first, then one anchor mutated to a symbol its file does not contain. Absolute paths under an EXIT INT TERM trap.

HEAD blob for packages/spec/liveness/email_template.json : 782c46534cc0147062d00f3d80e3d781b5366a96
pre-mutation blob  : 782c46534cc0147062d00f3d80e3d781b5366a96
--- pre-mutation grep counts
  ORIG '#upsertTemplate' : 1
  INJ  '#upsertEmailTemplate'  : 0
--- MUTATE: #upsertTemplate -> #upsertEmailTemplate  (entry email_template/isSystem)
--- post-mutation grep counts
  ORIG '#upsertTemplate' : 0   (expect 0)
  INJ  '#upsertEmailTemplate'  : 1   (expect 1)
  blob         : e44261405ae4a93c12bfd157cc078644b0748dad  (expect != 782c465…)

--- PREDICTION (written before the run): RED, naming entry email_template/isSystem;
    the anchor TOTAL must be UNCHANGED — that is what proves the new anchors parse
    as anchors rather than as prose.

MUTATED RUN EXIT=1
symbol anchors: 383 pointer(s) written `path#symbol`, 382 naming a symbol the cited file contains, 1 UNRESOLVED.
✗ 1 anchored citation(s) name a symbol the cited file does not contain:
    email_template/isSystem → packages/plugins/plugin-email/src/email-plugin.ts#upsertEmailTemplate

--- RESTORE PROOF   (git checkout HEAD -- ABSOLUTE_PATH, never a bare checkout)
  git diff HEAD is empty : YES
  blob == HEAD blob      : YES (782c46534cc0147062d00f3d80e3d781b5366a96)
  ORIG '#upsertTemplate' count     : 1   (expect 1)
  INJ  '#upsertEmailTemplate'  count     : 0   (expect 0)
RESTORED RUN EXIT=0
line citations: 82 pointer(s) written `path:NNN`, 82 inside the cited file.
symbol anchors: 383 pointer(s) written `path#symbol`, 383 naming a symbol the cited file contains.

Direction and named entry matched the prediction exactly. The mutated run still counted 383 pointers, which independently proves the 151 new anchors parse as anchors rather than prose. No build leg is owed or claimed: the gate runs from source through tsx and reads the JSON directly, so no dist/ sits between the mutation and the reading. The prediction text was authored pre-merge and names the pre-merge total (198); the merged-head total is 383 — the property under test ("total unchanged between mutated and restored runs") held on both trees, and the ablation was run on each.

Honest residuals

Stated, not smoothed over.

  1. isSymbolNamed is word-presence, not extent. The gate asks whether the cited file contains the symbol as a word. A symbol that SURVIVES while its body stops reading the key is out of reach at text level — locating a symbol's extent needs a parser, and the [finding] A liveness citation can rot WITHIN its file — 14 measured candidates the new line bound structurally cannot see, incl. permission.objects.allowExport citing a symbol that moved repos-internally #11457 precedent is not to switch on a matcher whose false-positive class has not been measured. The hand reads are the proof here, not the gate. Every anchor in this PR was placed by opening the file and finding the expression, and the expression is quoted in the entry's evidence parenthetical so a reviewer can check the claim without trusting the pointer.
  2. Two anchors are the true enclosing symbol and still weak. job.json's name / enabled / part of schedule, handler, retryPolicy anchor app-plugin.ts#start — the scheduling loop is inline in AppPlugin.start's kernel:ready hook with no enclosing named helper (the manifest.data#seedDatasets situation batch 2 recorded, minus the local const). start is a common word, so its falsification power is close to nil. Each such entry cites a distinctive downstream symbol beside it wherever one genuinely participates (#toBoundaryJobSchedule, #collectBundleFunctions, #runWithPolicy, #RETRY_DEFAULTS). doc.json's content has the same shape with rest-server.ts#registerMetadataEndpointsInner, a ~2,800-line registrar; its narrow half is the exact doc.zod.ts#resolveDocLocale anchor.
  3. Right-function-wrong-read survives anchoring. book.json's label cited book.zod.ts:303, which is inside resolveBookTree but on the GROUP sort — a sibling key's read. An anchor is honest about which function and cannot narrow further; the parenthetical now carries the expression, which is the part a reader can check.
  4. The two planned entries' anchors are not checked by the gate. api.json's inputMapping.transform / outputMapping.transform carry anchors, but check-liveness.mts scans evidence only when status === 'live' (producer is scanned at any status). So those two anchors are documentation, not enforcement. They are migrated anyway because a planned verdict's whole content is "this key is REFUSED, not ignored", and the refusal disappearing is exactly what should go red — but today nothing would.
  5. Foreign-realm entries are untouched and NOT re-stamped. book.description / .slug / .icon / .order (pinned objectui @940ba24) and job.label / .description (pinned objectui @aeb8424b) are left byte-for-byte alone. Their commits are not reproducible from this container, foreign anchors are never collected by the scanner by design, and dating a call graph nobody re-closed is the false confidence this ledger exists to prevent — the tool.json precedent from batch 2.

Gates

Family derived at this head by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with no paths passed (the script takes its own change set from the merge base — 7 paths vs 9ee2dcfbd, three-dot semantics): 25 families matched. Exit codes captured BEFORE any pipe (redirect-then-read).

23 of 25 EXIT=0, plus pnpm check:nul-bytes EXIT=0 run in addition because AGENTS.md mandates it for any edit (scanned 7245 text file(s) … no raw ASCII control bytes), plus a grep -naP self-scan over the C0 class plus DEL across every file in the diff: clean.

Green: check:changeset-gate-self-tests, check:empty-state, check:liveness, check:merge-driver, check:objectui-changeset, check:page-declaration-shape, check:pm-half-states, check:published-files, check:slot-lookup, check:strictness-ledger, check:test-source-alias, check:type-source-resolution, check:variant-docs, check-adr-0087-registration, check-changeset-no-major, check-comment-mask-adoption, check-empty-changeset, check-keyed-text-bounds, check-plugin-teardown-shape, check-undeclared-dep-imports, check-affected-docs, check-drift-comment, release-rehearsal-clone --self-test.

Two NOT MEASURED at db2d9ff39, neither a red — both the standing container prerequisites:

  • check-dev-prereqs.mjs EXIT=1: "✗ The workspace is not built — 1 unmet precondition, not a list of problems." Resolved in patch round 1: the closure was built and it now reads EXIT=0.
  • scripts/pm/check-half-states.mjs EXIT=3: "PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential … Nothing was swept … It is not a clean board and it is not a dirty one — it is no reading at all." Still NOT MEASURED; CI owns it.

Vitest at db2d9ff39, through the shared verify lock: pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 scripts/liveness/252 passed, 2 failed — the two non-vacuity floors, and only those. The same command was 254 passed (254) on the pre-merge commit 6c21c51f0, where the line-citation population was 187 rather than 82: the failure was a function of the population, not of the tree's correctness. At fb3a27f07 it is 254 passed (254) again, with the floors now measuring the failure mode they name.

Remaining inventory for the closing batch

Recounted from the gate's own scanner at this head, so the next batch is planned from measurement rather than recall. 82 line citations remain across 13 files:

app.json 10 · validation.json 10 · translation.json 9 · field.json 8 · hook.json 8 · mapping.json 7 · capability.json 6 · seed.json 6 · view.json 5 · dashboard.json 4 · flow.json 4 · qa.json 4 · action.json 1

Two notes for whoever takes it. action.json still carries one line citation after batch 1 — worth a look, it may be a foreign-adjacent leg rather than an oversight. And the closing batch drives the line population toward zero: with patch round 1 landed the floors no longer obstruct that, but reaching exactly zero is the moment the two assertions and their guard comments are to be deleted, in the same PR that retires the last line citation. That instruction now lives in the comments themselves, so batch 4 does not have to remember it.

tool.json remains out of reach and unchanged (all six entries cite packages/services/service-ai/…, absent from this checkout); per the batch-2 ACCEPT it spins out as its own card before #13003 can be closed.


Generated by Claude Code

claude added 2 commits August 28, 2026 22:01
…ch 3)

Adoption batch 3 of the #12516 symbol-anchor citation grammar (#13003),
file-disjoint from batches 1 and 2.

Ninety-five `path:NNN` evidence citations across liveness/email_template.json,
api.json, doc.json, book.json, query.json and job.json become `path#symbol`,
together with every path-only pointer in the same entries — each re-closed by
reading the code on the current tree rather than by shifting a line.

Seventy-nine of the ninety-five were already wrong, every one IN RANGE and so
invisible to the existence check, the line bound and the key-mention check.
Four files were 100% rotted (doc 15/15, book 13/13, query 12/12, job 11/11).
doc.json's book.zod.ts pointers had come to rest inside the ResolverDoc /
ResolvedEntry INTERFACES; email_template.json's nine email-service.ts pointers
moved together when #9225 split resolveAndRenderTemplate out of sendTemplate.
Two security-shaped keys cited a DIFFERENT key's enforcement: api.authRequired
cited the rate limiter's 429 body, email_template.variables cited the exact
line its sibling isSystem cites for itself.

Two pointers were falsified in prose as well as position (api.cacheTtl named
cacheControlHeader, which is no symbol in packages/**; query.where named
applyFilters ~11,250 lines from the position beside it). Two were not checkable
at all: query.expand was written `engine.ts:2519+`, whose trailing `+` stops
the token matching PATH_RE so it degraded silently to prose, and
query.aggregations.filter wrote five of its eight consumers as bare
package+filename prose.

Nothing is re-classified. The objectui-only entries (book description/slug/
icon/order, job label/description) are left byte-for-byte untouched and NOT
re-stamped: their evidence is pinned at a commit this checkout cannot
reproduce, on the tool.json precedent.

Part of #13003

Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
File-disjoint merge: batch 2 touched manifest/datasource/permission/dataset/
webhook, batch 3 touches email_template/api/doc/book/query/job. Merged so every
counter, gate reading and the ablation transcript in this PR come from the same
tree the reviewer will see.

Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 6 changed file(s) yielded no anchor (packages/spec/liveness/api.json, packages/spec/liveness/book.json, packages/spec/liveness/doc.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 6 changed file(s) yielded no anchor (packages/spec/liveness/api.json, packages/spec/liveness/book.json, packages/spec/liveness/doc.json, …) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9ee2dcfbd9bd28430398caf61e1bab64f94d7dcepackageMentionDocs.

…13003 ruling)

Patch round 1 on batch 3. Executes the maintainer-seat ruling recorded as
comment 5458356183 on #13003: option A, constant `toBeGreaterThan(0)`.

Both floors guard the parser silently extracting NOTHING (the #5623 lesson),
and that failure mode is exactly zero — so zero is exactly what the floor
tests. The number was 100, chosen when line citations were the only citation
grammar and the population could only grow. #13003 adopts #12516's
`path#symbol` grammar and therefore RETIRES 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.

Both guard comments are rewritten to carry the ruling, the migration that
drains the population, and the instruction that when the population
legitimately reaches zero the assertion AND its guard comment are DELETED in
the same PR that retires the last line citation — a conscious decision at
zero, never a silent one.

The two sibling floors are deliberately untouched: `local` paths
(evidence.test.ts) and `evidence paths` (check-liveness.test.ts) both guard
populations that are not draining (464 today).

Part of #13003

Claude-Session: https://claude.ai/code/session_01LpRNHxWZgSUgVnFT9mQQo4

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants