chore(spec): re-anchor six liveness ledgers to consuming symbols (#13003 batch 3) - #13039
Merged
Conversation
…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>
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 126 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Aug 28, 2026
…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>
os-trump
marked this pull request as ready for review
August 28, 2026 23:06
os-trump
enabled auto-merge
August 28, 2026 23:06
This was referenced Aug 28, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 mergesorigin/mainincl. 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:NNNcitations across six ledgers becomepath#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.Two tests in
packages/spec/scripts/liveness/failed atdb2d9ff39. They were never a defect in these ledgers —check:livenessitself 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:evidence.test.ts:373"every localpath:NNNcitation names a line that file has"expect(citations).toBeGreaterThan(100)check-liveness.test.ts:194"prints the citation count and how many are in range"expect(Number(m[1])).toBeGreaterThan(100)The two numbers differ because they count different populations, which I measured rather than assumed: 78 live-entry
evidencecitations + 4producercitations = 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_decisionon #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 thee.g. 10I 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.> 0is 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:
packages/spec/scripts/liveness/evidence.test.tsexpect(citations).toBeGreaterThan(100)→toBeGreaterThan(0)packages/spec/scripts/liveness/check-liveness.test.tsexpect(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:
localpaths (evidence.test.ts:344) andevidence 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 atdb2d9ff39(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.check:cross-package-test-inputs,check:objectql-double-limit,check:pm-governed-merges,check-ci-filter-parity,check-cross-package-test-inputs.mjs.check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:where-matcher,check:cross-package-test-inputs, andcheck:type-check-debt.check:type-check-debtdeserves 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 aslint.ymldoes (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.check-dev-prereqsNOT 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 typecheckEXIT=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 --listFilesnames both edited files (count 2), while the package's basetsc --noEmitprogram names neither (count 0). A green typecheck that excluded the edited files would have said nothing about them.pnpm check:nul-bytesEXIT=0, plus agrep -naPself-scan over the C0 class plus DEL on both edited files: clean.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 rottednamebootstrap-declared-email-templates.ts:78—name: tpl.name#mapTemplateToRownameemail-service.ts:411— aTemplateLoaderdocblock line#resolveAndRenderTemplatelabel:79—label: tpl.label#mapTemplateToRowcategory:80—category: tpl.category#mapTemplateToRowlocale:81—locale: tpl.locale#mapTemplateToRowlocaleemail-service.ts:411-416— the same docblock#resolveAndRenderTemplate+#DEFAULT_TEMPLATE_LOCALEsubject:82—subject: tpl.subject#mapTemplateToRowsubjectemail-service.ts:444— anEmailServiceOptionsdocblock about sys_email PERSISTENCE#resolveAndRenderTemplatebodyHtml:83—body_html: tpl.bodyHtml#mapTemplateToRowbodyHtmlemail-service.ts:445—persistence?: EmailPersistence;#resolveAndRenderTemplatebodyText:84— thebody_textspread#mapTemplateToRowbodyTextemail-service.ts:446-448— thetemplateLoader/retriesoption docblocks#resolveAndRenderTemplatevariables:91—is_system: tpl.isSystem, the lineisSystemcites for itself#mapTemplateToRowvariablesemail-service.ts:427-431— theEmailTemplateRowinterface (the row SHAPE, not the gate)#resolveAndRenderTemplatefromOverride:85-88— the fromOverride spread#mapTemplateToRowfromOverrideemail-service.ts:450-453—logger/defaultTemplateContextoptions#sendTemplate+#rowEnvelopereplyTo:89— the reply_to spread#mapTemplateToRowreplyToemail-service.ts:139— a docblock on the sys_email outbox id contract, ~1,150 lines away#sendTemplate+#rowEnvelopeactive:90—active: tpl.active#mapTemplateToRowactiveemail-service.ts:418-419— the bare*/closing a docblock#resolveAndRenderTemplate+#deactivateDeclaredEmailTemplateisSystem:91—is_system: tpl.isSystem#mapTemplateToRowisSystememail-plugin.ts:544—await target.sendInline({in the operator TEST-MAIL button#upsertTemplatedescription:92— the description spread#mapTemplateToRowAll nine
email-service.tspointers rotted together and by one cause: #9225 split the resolver out ofsendTemplateintoresolveAndRenderTemplate, 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 rottednameendpoint-matcher.ts:210— a real read ofendpoint.name, but the publish-gate exclusion MESSAGE, ~22 lines above the tiebreaker this entry is about#buildEndpointIndexpath:199— a barecontinue;ending the parse-failure branch#endpointIndexKey+#buildEndpointIndexmethod:115— a module docblock about the store-read contract#normalizeEndpointMethod+#endpointIndexKeysummaryopenapi-endpoints.ts:214—successStatus: 501in the unsupported-type branch#buildEndpointOperationdescription:215—requiresRecordId: falsein the same branch#buildEndpointOperationtypeendpoint-executor.ts:216— the docblock ABOVEplanEndpointTarget#planEndpointTargettarget:233— a bare};closing the sibling branch's refusal literal#planEndpointTargetobjectParams.object:217— a docblock line#planEndpointTarget+#executeObjectOperationobjectParams.operation:218— a docblock line#planEndpointTarget+#executeObjectOperation+#requireRecordIdinputMapping.sourceapi-mapping.ts:318—const projected = {}, the accumulator two lines above the read#project+#applyInputMappinginputMapping.target:322— a comment two lines above the write#project+#mappingDeclarationRejectioninputMapping.transform:259— the diagnostic-label line two above the refusal#mappingDeclarationRejectionoutputMapping.source:318— same as its twin#project+#applyOutputMappingoutputMapping.target:322— same as its twin#project+#applyOutputMappingoutputMapping.transform:259— same as its twin#mappingDeclarationRejectionauthRequiredendpoint-policy.ts:354—message: 'Rate limit exceeded…', step ①'s 429 body: a DIFFERENT key's enforcement#applyEndpointPolicies+#anonymousDenialrateLimit.enabledinbound-rate-limit.ts:88—if (!budget?.enabled) return null#deriveBucketConfigrateLimit.enabledendpoint-policy.ts:148— a BLANK LINE#createEndpointRateLimiterRegistryrateLimit.windowMs:91—const windowMs = budget.windowMs ?? 60_000#deriveBucketConfigrateLimit.maxRequests:90—const maxRequests = budget.maxRequests ?? 100#deriveBucketConfigcacheTtlendpoint-policy.ts:252— the function's own declaration line#computeCacheControl+#applyEndpointPoliciescacheTtlis the sharpest case in the batch for what an anchor buys: the LINE was accurate and the parenthetical namedcacheControlHeader, which is not a symbol anywhere inpackages/**(it iscomputeCacheControl). 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%)namebook.zod.ts:225— a BLANK LINE inside theResolvedBookinterface#matchesInclude+#entryFromDocnamerest-server.ts:3129—upsertMany: batch.operations?.upsertMany ?? true#resolveDocAudienceslabelbook.zod.ts:198—description?: string;(another key's declaration)#byOrderThenLabel+#entryFromDoclabelbook.zod.ts:202— thetagsdocblockdoc.zod.ts#resolveDocLocaledescriptionbook.zod.ts:202— thetagsdocblock#entryFromDocdescriptionrest-server.ts:3131—defaultAtomic: batch.defaultAtomic ?? true#resolveBookTreecontentrest-server.ts:3007— a docblock aboutenableProjectScoping#registerMetadataEndpointsInnercontentdoc.zod.ts:120— the middle of thetagsdocblock#resolveDocLocaleorderbook.zod.ts:198—description?: string;#byOrderThenLabelgroupbook.zod.ts:238— theResolvedEntrySchemadocblock#resolveBookTreetranslationsdoc.zod.ts:110— the/**that OPENS thetagsdocblock#resolveDocLocaletranslationsrest-server.ts:2996— a comment aboutos serveconfig#resolveDocLocaletranslationsrest-server.ts:3388— a comment aboutapi.version#resolveDocLocaletagsbook.zod.ts:213—description?: string;inResolvedEntry#matchesInclude+#resolveBookTreetagsrest-server.ts:3218—this.registerSecurityEndpoints(bp);#resolveBookTreeTwo whole-file moves under one ledger. In
book.zod.tsthe pointers had come to rest inside theResolverDoc/ResolvedEntryINTERFACES — 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 adeadkey has a declaration too. Inrest-server.tsthe entire doc-serving block moved ~1,600 lines down.book.json— 13 local citations, 0 accurate, 13 rotted (100%)namerest-server.ts:3098—patterns: crud.patternsin the CRUD-endpoint config#deriveImplicitPackageBook+book.zod.ts#resolveBookTreelabelbook.zod.ts:303—.map((g, i) => …), the GROUP sort (a sibling key's read)#resolveBookTreeaudiencerest-server.ts:3113— the comment// config to read).#audienceAllowsaudiencerest-server.ts:2969— a docblock about'v1/beta'splicing a path segment#audienceAllowsaudiencebook.zod.ts:351—entries.push(entryFromDoc(d)), tree ASSEMBLY not the gate#audienceAllows+#resolveDocAudiencesgroups.keybook.zod.ts:238/:290— a docblock, and a bare*continuation line#resolveBookTreegroups.labelbook.zod.ts:290— the same bare*line#resolveBookTreegroups.orderbook.zod.ts:221—key: string;in theResolvedGroupinterface#resolveBookTreegroups.includebook.zod.ts:236/:193— a docblock, and a BLANK LINE#matchesInclude+#resolveBookTreegroups.packagebook.zod.ts:232— the/**opening a docblock#resolveBookTree+#matchesIncludegroups.pagesbook.zod.ts:248-286— a RANGE now spanning the response-contract schemas#resolveBookTreeaudienceis 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 allobjectprotocol.ts:780—if (seen.has(key)) return;in an unrelated walker (QUERY_AST_KEYSis at ~:2426)engine.ts#fillQueryAstDefaults+protocol.ts#QUERY_AST_KEYSfieldssql-driver.ts:1354— an error-constructor helper, ~4,200 lines above the projection#findRows+engine.ts#planFormulaProjectionwheresql-driver.ts:1331— a docblock aboutUNSUPPORTED_QUERY_PARAM#findRows+#applyFilters+predicate-guard.ts#collectQueryFieldssearchengine.ts:2911— the comment// this row sees. Never a copy.#expandSearchOnAst+search-filter.ts#expandSearchToFiltersearchFieldsengine.ts:2898—const row = rows[index];#expandSearchOnAst+search-filter.ts#resolveSearchFieldsorderBysql-driver.ts:1342— a docblock about a throw site#orderKeysFor+#findRowslimitsql-driver.ts:1345— a bare*/#findRowsoffsetsql-driver.ts:1344— the docblock line* log.#findRowstopengine.ts:2853andprotocol.ts:3486— both docblocks#ENGINE_QUERY_SLOTS+#foldEngineOptionAliases+protocol.ts#WIRE_DOLLAR_ALIASESaggregations.functionin-memory-aggregation.ts:167—return v ?? null;insideprojectGroupValue, a DIFFERENT function three lines above#aggregateBucketaggregations.fieldin-memory-aggregation.ts:206—if (!field) {, the branch taken when the key is ABSENT#aggregateBucket+#collectValuesquery.whereis the largest name-vs-position gap in the batch: the parenthetical namedapplyFilters, 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.jsonclass from batch 2 reproduced twice more:expandwas writtenpackages/objectql/src/engine.ts:2519+. The trailing+stops the token matchingPATH_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, toexpandRelatedRecords.)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), whichPATH_REcannot 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 ofobject/searchFields/fields) were PATH-ONLY — a form the line bound cannot falsify by construction.havingis 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%)nameapp-plugin.ts:815— a bare} else {;:833— a baretry {#startscheduleapp-plugin.ts:834—const actions = collectBundleActions(this.bundle), a different metadata kind's registrationjob-schedule.ts#toBoundaryJobSchedule+#startschedulecron-job-adapter.ts:88,db-job-adapter.ts:83— both docblocks#CronJobAdapter,#DbJobAdapterhandlerapp-plugin.ts:824-830— a comment about registering actions onPOST /api/v1/actions/...#start+#collectBundleFunctionsretryPolicyapp-plugin.ts:838-841—if (actions.length > 0 && …), actions again#startretryPolicyrun-with-policy.ts:58-65— theJobAttemptRecorderinterface#runWithPolicy+#RETRY_DEFAULTStimeoutrun-with-policy.ts:25-33—jitter: falseinsideRETRY_DEFAULTS, a retryPolicy default#runWithPolicy+#withTimeout+#JobTimeoutErrortimeout[producer]app-plugin.ts:875-876— a bare}#startenabledapp-plugin.ts:820— a bare}#startThis file is the worklist's own argument, stated by the ledger before I got there. Its 2026-08-02
_noterecords 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
timeoutproducer pointer is worth its own line:producerresolves through the same resolver asevidenceprecisely 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/mainat9ee2dcfbd(incl. batches 1 and 2). Head =db2d9ff39.path:NNNpath#symbollive)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-28already carried averifiedAt, so the undated backlog is untouched by batch 3.check:livenessverdict line at head, quoted from the gate:Ablation — the new anchors are CHECKED, not merely present
Gate-green alone would not prove it: an anchor token that stops matching
PATH_REdegrades 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 anEXIT INT TERMtrap.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.
isSymbolNamedis 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'sevidenceparenthetical so a reviewer can check the claim without trusting the pointer.job.json'sname/enabled/ part ofschedule,handler,retryPolicyanchorapp-plugin.ts#start— the scheduling loop is inline inAppPlugin.start'skernel:readyhook with no enclosing named helper (themanifest.data→#seedDatasetssituation batch 2 recorded, minus the local const).startis 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'scontenthas the same shape withrest-server.ts#registerMetadataEndpointsInner, a ~2,800-line registrar; its narrow half is the exactdoc.zod.ts#resolveDocLocaleanchor.book.json'slabelcitedbook.zod.ts:303, which is insideresolveBookTreebut 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.plannedentries' anchors are not checked by the gate.api.json'sinputMapping.transform/outputMapping.transformcarry anchors, butcheck-liveness.mtsscansevidenceonly whenstatus === 'live'(produceris scanned at any status). So those two anchors are documentation, not enforcement. They are migrated anyway because aplannedverdict's whole content is "this key is REFUSED, not ignored", and the refusal disappearing is exactly what should go red — but today nothing would.book.description/.slug/.icon/.order(pinnedobjectui @940ba24) andjob.label/.description(pinnedobjectui @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 — thetool.jsonprecedent from batch 2.Gates
Family derived at this head by
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackwith no paths passed (the script takes its own change set from the merge base — 7 paths vs9ee2dcfbd, three-dot semantics): 25 families matched. Exit codes captured BEFORE any pipe (redirect-then-read).23 of 25 EXIT=0, plus
pnpm check:nul-bytesEXIT=0 run in addition because AGENTS.md mandates it for any edit (scanned 7245 text file(s) … no raw ASCII control bytes), plus agrep -naPself-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.mjsEXIT=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.mjsEXIT=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 was254 passed (254)on the pre-merge commit6c21c51f0, where the line-citation population was 187 rather than 82: the failure was a function of the population, not of the tree's correctness. Atfb3a27f07it is254 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.json10 ·validation.json10 ·translation.json9 ·field.json8 ·hook.json8 ·mapping.json7 ·capability.json6 ·seed.json6 ·view.json5 ·dashboard.json4 ·flow.json4 ·qa.json4 ·action.json1Two notes for whoever takes it.
action.jsonstill 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.jsonremains out of reach and unchanged (all six entries citepackages/services/service-ai/…, absent from this checkout); per the batch-2 ACCEPT it spins out as its own card before#13003can be closed.Generated by Claude Code