diff --git a/.changeset/liveness-symbol-anchors-batch-3.md b/.changeset/liveness-symbol-anchors-batch-3.md new file mode 100644 index 0000000000..16d1c2eed2 --- /dev/null +++ b/.changeset/liveness-symbol-anchors-batch-3.md @@ -0,0 +1,66 @@ +--- +"@objectstack/spec": patch +--- + +chore(spec): re-anchor the `email_template` / `api` / `doc` / `book` / `query` / `job` liveness ledgers to consuming symbols (#13003) + +Adoption batch 3 of the symbol-anchor citation grammar landed by #12516, +file-disjoint from batches 1 and 2. The `liveness/` ledgers ship inside this +package's npm tarball (they are named in `files`), so this is a published-data +change even though no runtime behaviour moves and no schema key changes. + +Ninety-five `path:NNN` evidence citations across `liveness/email_template.json`, +`api.json`, `doc.json`, `book.json`, `query.json` and `job.json` are now written +`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 +number. A symbol moves with its consumer, so the pointer survives the in-file +drift that rots a line, and goes red when the consumer is renamed or deleted — a +direction a stale line can never produce. + +What the re-closure found. Seventy-nine of the ninety-five citations were already +pointing at the wrong place, every one of them IN RANGE and so invisible to the +existence check, the line bound and the key-mention check alike. Four whole files +were 100% rotted — `doc.json` 15 of 15, `book.json` 13 of 13, `query.json` 12 of +12, `job.json` 11 of 11 — and the sixteen accurate citations that survive are +concentrated in two places: twelve of the thirteen row-mapping pointers in +`email_template.json`, and four pointers into short, stable policy helpers in +`api.json`. In `doc.json`, its `book.zod.ts` pointers had come to rest inside +the `ResolverDoc` / `ResolvedEntry` INTERFACES — type declarations of the very +fields whose consumers they claimed to cite, which is what a `dead` key has too — +while the whole doc-serving block of `rest-server.ts` moved roughly 1,600 lines +out from under the rest. `email_template.json`'s nine `email-service.ts` pointers +rotted together when #9225 split the resolver out of `sendTemplate` into +`resolveAndRenderTemplate`, moving the reads from the 400s to the 1,170-1,310 +band; one of them landed 1,150 lines away on a docblock about the `sys_email` +outbox id contract. Two security-shaped keys had their only pointer land on a +DIFFERENT key's enforcement: `api.authRequired` cited the rate limiter's 429 +body, and `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` cited a +line that was still accurate and named `cacheControlHeader`, which is not a +symbol anywhere in `packages/**` (the function is `computeCacheControl`); +`query.where` named `applyFilters`, which is real and sits roughly 11,250 lines +from the position cited beside it. + +Two entries were not checkable at all rather than merely stale. +`query.expand`'s pointer was written `engine.ts:2519+`, and the trailing `+` +stops the token matching the scanner's path pattern — so it degraded silently to +prose and was never resolved, bounded or key-checked, while reading like the most +precise citation in the file. `query.aggregations.filter`, the youngest entry in +that ledger, wrote five of its eight consumers as bare package+filename prose +(`driver-sql sql-driver.ts`), which the same pattern cannot parse. Both are now +repo-rooted anchors — the `webhook.json` class from batch 2, reproduced here once +by a single character and once by a missing prefix. + +`job.json` is the file that argues the case most directly: 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, onto `} else {`, a bare `try {`, a bare `}` and a line about +registering actions. Restamping a line is the same claim with a newer date. + +Nothing is re-classified here. Entries whose evidence lives only in `objectui` at +a pinned commit this checkout cannot reproduce — `book.description` / `.slug` / +`.icon` / `.order` and `job.label` / `.description` — are left byte-for-byte +untouched and are NOT re-stamped, on the `tool.json` precedent: dating a call +graph nobody re-closed is the false confidence this ledger exists to prevent. diff --git a/packages/spec/liveness/api.json b/packages/spec/liveness/api.json index 2fd1984617..30dab7b444 100644 --- a/packages/spec/liveness/api.json +++ b/packages/spec/liveness/api.json @@ -1,62 +1,62 @@ { "type": "api", - "_note": "ApiEndpointSchema (packages/spec/src/api/endpoint.zod.ts). Seeded 2026-08-04 (#5271, part of #5206) in the same change that made `api` a REGISTERED metadata type — governance and registration land together, which is the whole lesson of `datasource` (#4487): it went ungoverned long enough to accumulate six inert keys that had to be found by hand. Consumers, by layer: the MATCHER (`packages/metadata/src/endpoint-matcher.ts`) indexes `name`/`path`/`method`; the EXECUTOR (`packages/runtime/src/endpoint-executor.ts`) dispatches on `type` and reads `target` / `objectParams`; the POLICY chain (`packages/runtime/src/endpoint-policy.ts` + `security/inbound-rate-limit.ts`) enforces `authRequired` / `rateLimit` / `cacheTtl`; the MAPPING layer (`packages/runtime/src/api-mapping.ts`) applies `inputMapping` / `outputMapping`; and the OpenAPI enrichment (`packages/rest/src/openapi-endpoints.ts`) emits `summary` / `description`. This is the cheapest possible moment to govern the type: #5040's E-series built every one of those consumers and all of it is on main, so each key has a real evidence path today rather than a promise. Nothing here is `dead`. The two `transform` keys are `planned`, not `dead`, and the distinction is load-bearing — see their notes. No `api` property is a bound HIGH_RISK class in proof-registry.mts, so no entry carries a `proof`; none is invented to look thorough. The protection-envelope keys (`_lock*`, `_provenance`, `_packageId/Version`) are auto-classified live by the gate (ADR-0010) and are deliberately absent from this file.", + "_note": "ApiEndpointSchema (packages/spec/src/api/endpoint.zod.ts). Seeded 2026-08-04 (#5271, part of #5206) in the same change that made `api` a REGISTERED metadata type — governance and registration land together, which is the whole lesson of `datasource` (#4487): it went ungoverned long enough to accumulate six inert keys that had to be found by hand. Consumers, by layer: the MATCHER (`packages/metadata/src/endpoint-matcher.ts`) indexes `name`/`path`/`method`; the EXECUTOR (`packages/runtime/src/endpoint-executor.ts`) dispatches on `type` and reads `target` / `objectParams`; the POLICY chain (`packages/runtime/src/endpoint-policy.ts` + `security/inbound-rate-limit.ts`) enforces `authRequired` / `rateLimit` / `cacheTtl`; the MAPPING layer (`packages/runtime/src/api-mapping.ts`) applies `inputMapping` / `outputMapping`; and the OpenAPI enrichment (`packages/rest/src/openapi-endpoints.ts`) emits `summary` / `description`. This is the cheapest possible moment to govern the type: #5040's E-series built every one of those consumers and all of it is on main, so each key has a real evidence path today rather than a promise. Nothing here is `dead`. The two `transform` keys are `planned`, not `dead`, and the distinction is load-bearing — see their notes. No `api` property is a bound HIGH_RISK class in proof-registry.mts, so no entry carries a `proof`; none is invented to look thorough. The protection-envelope keys (`_lock*`, `_provenance`, `_packageId/Version`) are auto-classified live by the gate (ADR-0010) and are deliberately absent from this file. 2026-08-28 (#13003): every citation in this file was re-anchored to its consuming symbol. Worth recording about THIS file specifically — the layer map above stayed true for 24 days while nearly every line number under it went stale: the files did not move, the functions did not move, the LINES did, and the ledger cited lines.", "props": { "name": { "status": "live", - "evidence": "packages/metadata/src/endpoint-matcher.ts:210", - "verifiedAt": "2026-08-04", - "note": "The metadata item key, and the tiebreaker with real consequences: when two stored declarations claim the same METHOD + normalized path, the lexicographically-first `name` keeps the route and the loser is named at `error` level. Also the subject of every publish-gate and matcher message, so a rename changes what an author is told." + "evidence": "packages/metadata/src/endpoint-matcher.ts#buildEndpointIndex (`endpoint.name < incumbent.name` — the duplicate-claim tiebreaker, plus the two exclusion messages that name the offending item)", + "verifiedAt": "2026-08-28", + "note": "The metadata item key, and the tiebreaker with real consequences: when two stored declarations claim the same METHOD + normalized path, the lexicographically-first `name` keeps the route and the loser is named at `error` level. Also the subject of every publish-gate and matcher message, so a rename changes what an author is told. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `endpoint-matcher.ts:210` is a real read of `endpoint.name`, but of the WRONG one: it is the publish-gate exclusion message, ~22 lines above the tiebreaker this note is about. A citation that lands on a different read of the same key in the same file is the least visible rot there is — it survives the existence check, the line bound and the key-mention check by construction, and it reads correct to a human skimming for the key. Anchored to the function that holds both reads. Re-closed by hand against 93ea19bca." }, "path": { "status": "live", - "evidence": "packages/metadata/src/endpoint-matcher.ts:199 (endpointIndexKey → normalizeEndpointPath)", - "verifiedAt": "2026-08-04", - "note": "Half the index key. Compared as a WHOLE STRING with exactly one trailing slash trimmed — no percent-decoding, no Unicode normalization, no case folding in 17.x. ADR-0121 D1 additionally confines it to the stack's `apps//` carve-out at publish; that is a gate, not a second consumer." + "evidence": "packages/metadata/src/endpoint-matcher.ts#endpointIndexKey (`normalizeEndpointPath(path)` — half the index key); packages/metadata/src/endpoint-matcher.ts#buildEndpointIndex (`endpointIndexKey(endpoint.method, endpoint.path)` — the call site that puts a declaration on a route)", + "verifiedAt": "2026-08-28", + "note": "Half the index key. Compared as a WHOLE STRING with exactly one trailing slash trimmed — no percent-decoding, no Unicode normalization, no case folding in 17.x. ADR-0121 D1 additionally confines it to the stack's `apps//` carve-out at publish; that is a gate, not a second consumer. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `endpoint-matcher.ts:199` had rotted onto the bare `continue;` that ends the schema-parse-failure branch. The evidence's own parenthetical named `endpointIndexKey`, which sits at ~:151 and never moved; only the number was wrong, and nothing in the ledger compares a citation's line against the symbol its prose names. Re-closed by hand against 93ea19bca." }, "method": { "status": "live", - "evidence": "packages/metadata/src/endpoint-matcher.ts:115 (normalizeEndpointMethod), :199", - "verifiedAt": "2026-08-04", - "note": "The other half of the index key, upper-cased before comparison. Also read by the policy layer: `cacheTtl` is GET-only, so `method` decides whether a Cache-Control header can be emitted at all." + "evidence": "packages/metadata/src/endpoint-matcher.ts#normalizeEndpointMethod (upper-cased before comparison); packages/metadata/src/endpoint-matcher.ts#endpointIndexKey (the other half of the index key)", + "verifiedAt": "2026-08-28", + "note": "The other half of the index key, upper-cased before comparison. Also read by the policy layer: `cacheTtl` is GET-only, so `method` decides whether a Cache-Control header can be emitted at all (`endpoint-policy.ts#computeCacheControl`, cited under `cacheTtl`). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `endpoint-matcher.ts:115` had rotted onto a module docblock line about the store-read contract, ~19 lines above `normalizeEndpointMethod`; the `:199` leg rotted with `path`'s onto the same `continue;`. Re-closed by hand against 93ea19bca." }, "summary": { "status": "live", - "evidence": "packages/rest/src/openapi-endpoints.ts:214", - "verifiedAt": "2026-08-04", - "note": "Emitted as the OpenAPI operation's `summary` (#5040 E6). Documentation-shaped rather than behavioural, but it genuinely reaches a served artifact — /openapi.json — so it is live, not decorative." + "evidence": "packages/rest/src/openapi-endpoints.ts#buildEndpointOperation (`if (endpoint.summary) operation.summary = endpoint.summary`)", + "verifiedAt": "2026-08-28", + "note": "Emitted as the OpenAPI operation's `summary` (#5040 E6). Documentation-shaped rather than behavioural, but it genuinely reaches a served artifact — /openapi.json — so it is live, not decorative. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `openapi-endpoints.ts:214` had rotted onto `successStatus: 501` inside `httpFactsFor`, the unsupported-type branch: a line about what the runtime REFUSES to execute, cited as evidence that a documentation key is emitted. ~26 lines from the read. Re-closed by hand against 93ea19bca." }, "description": { "status": "live", - "evidence": "packages/rest/src/openapi-endpoints.ts:215", - "verifiedAt": "2026-08-04", - "note": "Emitted as the OpenAPI operation's `description` (#5040 E6). Same reasoning as `summary`." + "evidence": "packages/rest/src/openapi-endpoints.ts#buildEndpointOperation (`if (endpoint.description) operation.description = endpoint.description`)", + "verifiedAt": "2026-08-28", + "note": "Emitted as the OpenAPI operation's `description` (#5040 E6). Same reasoning as `summary`. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `openapi-endpoints.ts:215` had rotted onto `requiresRecordId: false` in the same unsupported-type branch as `summary`'s, one line below it. The pair rotted together and by the same distance, which is what a citation pair written on one day against one function does when the function moves. Re-closed by hand against 93ea19bca." }, "type": { "status": "live", - "evidence": "packages/runtime/src/endpoint-executor.ts:216 (object_operation), :232 (flow)", - "verifiedAt": "2026-08-04", - "note": "The executor's dispatch key. NOTE the enum is wider than the runtime: only 'object_operation' and 'flow' execute; 'script' and 'proxy' are REFUSED at publish (endpoint-publish-gate.ts targetGate) and again by planEndpointTarget at :246. The KEY is live — it is the two rejected VALUES that are not, which is why this is a `live` row and not a qualified one." + "evidence": "packages/runtime/src/endpoint-executor.ts#planEndpointTarget (`endpoint.type === 'object_operation'` / `=== 'flow'`, and the trailing `unsupported` plan that answers 501 for everything else)", + "verifiedAt": "2026-08-28", + "note": "The executor's dispatch key. NOTE the enum is wider than the runtime: only 'object_operation' and 'flow' execute; 'script' and 'proxy' are REFUSED at publish (endpoint-publish-gate.ts targetGate) and again by `planEndpointTarget`. The KEY is live — it is the two rejected VALUES that are not, which is why this is a `live` row and not a qualified one. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:216` had rotted onto the docblock ABOVE `planEndpointTarget` (a sentence about SSRF/egress policy), and `:232` onto a hint string inside it. Both were inside the right neighbourhood and neither named a read — the docblock case is the one worth naming: a citation onto the prose that EXPLAINS a function is indistinguishable, to every check the ledger has, from a citation onto the function. Re-closed by hand against 93ea19bca." }, "target": { "status": "live", - "evidence": "packages/runtime/src/endpoint-executor.ts:233", - "verifiedAt": "2026-08-04", - "note": "The flow name a `type: 'flow'` endpoint triggers, delegated through the same automation pipeline as POST /automation//trigger (ADR-0121 D5 — no second execution dialect). For `object_operation` the routing comes from `objectParams` instead, so an empty `target` is only refused on the flow branch." + "evidence": "packages/runtime/src/endpoint-executor.ts#planEndpointTarget (`const flow = endpoint.target` on the flow branch; an empty one returns an `unsupported` plan naming the endpoint)", + "verifiedAt": "2026-08-28", + "note": "The flow name a `type: 'flow'` endpoint triggers, delegated through the same automation pipeline as POST /automation//trigger (ADR-0121 D5 — no second execution dialect). For `object_operation` the routing comes from `objectParams` instead, so an empty `target` is only refused on the flow branch. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `endpoint-executor.ts:233` had rotted onto a bare `};` closing the object_operation refusal literal, i.e. onto punctuation inside the sibling branch. Re-closed by hand against 93ea19bca." }, "objectParams": { "children": { "object": { "status": "live", - "evidence": "packages/runtime/src/endpoint-executor.ts:217, :394", - "verifiedAt": "2026-08-04", - "note": "The object an `object_operation` endpoint reads or writes. Line 394 is the one that matters for security: `object` comes from the DECLARATION, never from the request, so a caller cannot redirect a declared endpoint at another object." + "evidence": "packages/runtime/src/endpoint-executor.ts#planEndpointTarget (`const object = endpoint.objectParams?.object` — read from the DECLARATION and carried on the plan); packages/runtime/src/endpoint-executor.ts#executeObjectOperation (`const { object, operation } = plan` — every delegated callData argument takes it from there, never from the request)", + "verifiedAt": "2026-08-28", + "note": "The object an `object_operation` endpoint reads or writes. What matters for security: `object` comes from the DECLARATION, never from the request, so a caller cannot redirect a declared endpoint at another object (#3946 was the mirror-image bug on /data). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — BOTH legs were docblock lines: `:217` in `planEndpointTarget`'s header, `:394` in `executeObjectOperation`'s. The note's own claim (\"Line 394 is the one that matters for security\") was true of the PROSE at :394 and false of any read — the security property is real and is enforced ~15 lines lower, in the destructure. An entry whose evidence cites the documentation of its own claim is the purest form of the unfalsifiable pointer this migration exists to remove. Re-closed by hand against 93ea19bca." }, "operation": { "status": "live", - "evidence": "packages/runtime/src/endpoint-executor.ts:218, :368", - "verifiedAt": "2026-08-04", - "note": "find / get / create / update / delete, delegated to the same callData pipeline the built-in /data route uses. Also decides whether a request body is read at all, which is what makes `inputMapping` inert on the bodyless operations (publish refuses that combination — #5111)." + "evidence": "packages/runtime/src/endpoint-executor.ts#planEndpointTarget (`const operation = endpoint.objectParams?.operation`; an incomplete pair is refused before anything runs); packages/runtime/src/endpoint-executor.ts#executeObjectOperation (the find/get/create/update/delete branch, each delegating to the callData shape /data uses); packages/runtime/src/endpoint-executor.ts#requireRecordId (names the operation in the 400 when `?id=` is missing on get/update/delete)", + "verifiedAt": "2026-08-28", + "note": "find / get / create / update / delete, delegated to the same callData pipeline the built-in /data route uses. Also decides whether a request body is read at all, which is what makes `inputMapping` inert on the bodyless operations (publish refuses that combination — #5111). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:218` was a docblock line, `:368` a comment inside `requireRecordId` about the error constructor rather than the read two lines below it. Re-closed by hand against 93ea19bca." } } }, @@ -64,21 +64,21 @@ "children": { "source": { "status": "live", - "evidence": "packages/runtime/src/api-mapping.ts:318 (readPath)", - "verifiedAt": "2026-08-04", - "note": "Dot path read out of the request body, before delegation — so a mapping can never buy a caller past `authRequired` or the rate limiter." + "evidence": "packages/runtime/src/api-mapping.ts#project (`readPath(source, splitPath(entry.source)!)` — the dot-path read); packages/runtime/src/api-mapping.ts#applyInputMapping (the caller: no entries ⇒ the caller's own body rides through by reference)", + "verifiedAt": "2026-08-28", + "note": "Dot path read out of the request body, before delegation — so a mapping can never buy a caller past `authRequired` or the rate limiter. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `api-mapping.ts:318` landed on `const projected: … = {}`, the accumulator two lines above the read, and the evidence's parenthetical named `readPath`, which is a DIFFERENT function ~138 lines up. `project` is the symbol that reads `entry.source`; `readPath` only walks segments it is handed. Anchoring makes the distinction the pointer already claimed. Re-closed by hand against 93ea19bca." }, "target": { "status": "live", - "evidence": "packages/runtime/src/api-mapping.ts:322 (writePath)", - "verifiedAt": "2026-08-04", - "note": "Dot path written into the params the executor sees. Prototype keys and colliding targets are refused rather than silently discarded (:291)." + "evidence": "packages/runtime/src/api-mapping.ts#project (`writePath(projected, splitPath(entry.target)!, value)`); packages/runtime/src/api-mapping.ts#mappingDeclarationRejection (prototype keys and colliding/prefixing targets are REFUSED at declaration rather than silently discarded)", + "verifiedAt": "2026-08-28", + "note": "Dot path written into the params the executor sees. Prototype keys and colliding targets are refused rather than silently discarded. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `api-mapping.ts:322` landed on the comment \"a mapping projects, it does not assert that a field was supplied\", two lines above the write. The note's `:291` collision pointer was never in `evidence`, so nothing bounded it; it is now an anchor on the function that performs the refusal. Re-closed by hand against 93ea19bca." }, "transform": { "status": "planned", - "evidence": "packages/runtime/src/api-mapping.ts:259", - "verifiedAt": "2026-08-04", - "note": "PLANNED, deliberately not `dead`, and the difference is the point. `dead` in this ledger means 'parsed, no consumer' — a silent no-op. `transform` is the opposite: it is parsed and then LOUDLY REFUSED, at publish (endpoint-publish-gate.ts mappingGate) and again at runtime (:259), because there is no transformation-function registry anywhere in the platform. An author who writes it is told so and told what to do instead. It stays in the vocabulary rather than being removed because admitting it needs a function registry AND a sandbox ruling of its own (#5040 §3.4) — that is a design decision to make, not a key to quietly delete. Nothing enforce-or-remove has to chase: it is already refused." + "evidence": "packages/runtime/src/api-mapping.ts#mappingDeclarationRejection (`if (entry.transform !== undefined) return reject(…)` — parsed, then LOUDLY refused, naming the entry and what to do instead)", + "verifiedAt": "2026-08-28", + "note": "PLANNED, deliberately not `dead`, and the difference is the point. `dead` in this ledger means 'parsed, no consumer' — a silent no-op. `transform` is the opposite: it is parsed and then LOUDLY REFUSED, at publish (endpoint-publish-gate.ts mappingGate) and again at runtime, because there is no transformation-function registry anywhere in the platform. An author who writes it is told so and told what to do instead. It stays in the vocabulary rather than being removed because admitting it needs a function registry AND a sandbox ruling of its own (#5040 §3.4) — that is a design decision to make, not a key to quietly delete. Nothing enforce-or-remove has to chase: it is already refused. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `api-mapping.ts:259` landed on `const at = …`, the diagnostic-label line two above the refusal. For a `planned` entry the anchor earns more than for a `live` one: the whole verdict is \"this key is refused, not ignored\", and the refusal disappearing is exactly the change that must go red. Re-closed by hand against 93ea19bca." } } }, @@ -86,57 +86,57 @@ "children": { "source": { "status": "live", - "evidence": "packages/runtime/src/api-mapping.ts:318 (readPath, via applyOutputMapping at :358)", - "verifiedAt": "2026-08-04", - "note": "Dot path read out of the SUCCESS body only. An error answer is never remapped — a projection able to reshape a 401/429/500 into data could disguise a failure as a result." + "evidence": "packages/runtime/src/api-mapping.ts#project (the same dot-path read — one code path serves both mapping keys); packages/runtime/src/api-mapping.ts#applyOutputMapping (projects `data` out of the `{ success, data, meta }` envelope, or the body itself when it is not one)", + "verifiedAt": "2026-08-28", + "note": "Dot path read out of the SUCCESS body only. An error answer is never remapped — a projection able to reshape a 401/429/500 into data could disguise a failure as a result (the caller applies it on success only, `api-endpoint-step.ts`). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:318` rotted with its `inputMapping` twin, and the parenthetical's `applyOutputMapping at :358` named a docblock line, not the function (which begins at ~:360). Re-closed by hand against 93ea19bca." }, "target": { "status": "live", - "evidence": "packages/runtime/src/api-mapping.ts:322 (writePath, via applyOutputMapping at :358)", - "verifiedAt": "2026-08-04", - "note": "Dot path written into the response body. Validated BEFORE delegation, so a broken projection cannot let a `create` insert a record and then fail to answer." + "evidence": "packages/runtime/src/api-mapping.ts#project (the same dot-path write); packages/runtime/src/api-mapping.ts#applyOutputMapping (validated BEFORE the envelope is rebuilt, and every non-`data` member rides through untouched)", + "verifiedAt": "2026-08-28", + "note": "Dot path written into the response body. Validated BEFORE delegation, so a broken projection cannot let a `create` insert a record and then fail to answer. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:322` rotted with its `inputMapping` twin onto a comment, and `applyOutputMapping at :358` named the docblock rather than the function. Re-closed by hand against 93ea19bca." }, "transform": { "status": "planned", - "evidence": "packages/runtime/src/api-mapping.ts:259", - "verifiedAt": "2026-08-04", - "note": "Same key, same gate, same reasoning as `inputMapping.transform` — the mapping validator walks both arrays through one code path. See that entry." + "evidence": "packages/runtime/src/api-mapping.ts#mappingDeclarationRejection (the same walk, called once per mapping key — `outputMapping[i].transform` is refused by the identical branch)", + "verifiedAt": "2026-08-28", + "note": "Same key, same gate, same reasoning as `inputMapping.transform` — the mapping validator walks both arrays through one code path. See that entry. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — same `:259` rot as its twin. Anchoring both to the one function is now literally true of the code rather than an assertion in prose. Re-closed by hand against 93ea19bca." } } }, "authRequired": { "status": "live", - "evidence": "packages/runtime/src/endpoint-policy.ts:354", - "verifiedAt": "2026-08-04", - "note": "The default-deny gate, and the key whose history is the reason this whole surface was refused for a release: #4936 measured it parsing green and gating NOTHING (no route was mounted, no matcher existed) — declared security that enforced nothing, i.e. false compliance. It is enforced now. It DEFAULTS to true, so `false` is the only way to open an anonymous entry point, and ADR-0121 D6 pairs that with a mandatory armed `rateLimit`. Note the deliberate omission at the call site: no anonymous-exemption argument is passed, so a declared path can never exempt itself from its own default deny." + "evidence": "packages/runtime/src/endpoint-policy.ts#applyEndpointPolicies (`if (endpoint.authRequired !== false) { if (shouldDenyAnonymous(…)) return anonymousDenial(); }` — step ② of the chain); packages/runtime/src/endpoint-policy.ts#anonymousDenial (the 401 envelope it returns)", + "verifiedAt": "2026-08-28", + "note": "The default-deny gate, and the key whose history is the reason this whole surface was refused for a release: #4936 measured it parsing green and gating NOTHING (no route was mounted, no matcher existed) — declared security that enforced nothing, i.e. false compliance. It is enforced now. It DEFAULTS to true, so `false` is the only way to open an anonymous entry point, and ADR-0121 D6 pairs that with a mandatory armed `rateLimit`. Note the deliberate omission at the call site: no path argument is passed to `shouldDenyAnonymous`, so a declared `/…/auth/…` path can never exempt itself from its own default deny. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `endpoint-policy.ts:354` had rotted onto `message: 'Rate limit exceeded…'`, i.e. onto step ①'s 429 body: a SECURITY key whose only pointer named a DIFFERENT key's enforcement, ~21 lines above its own. This is the shape batch 2 recorded on `permission.writeScope` and it is worth naming twice — inside one policy chain every branch reads as policy code, so nothing about the wrong line looks wrong. Re-closed by hand against 93ea19bca." }, "rateLimit": { "children": { "enabled": { "status": "live", - "evidence": "packages/runtime/src/security/inbound-rate-limit.ts:88 (deriveBucketConfig), consumed via packages/runtime/src/endpoint-policy.ts:148", - "verifiedAt": "2026-08-04", - "note": "ARMS the budget, and it defaults to FALSE — so `rateLimit: { windowMs, maxRequests }` written without it parses into a budget that meters nothing. That is exactly why ADR-0121 D6's gate tests `enabled === true` rather than the presence of `rateLimit`: a presence check would be vacuous." + "evidence": "packages/runtime/src/security/inbound-rate-limit.ts#deriveBucketConfig (`if (!budget?.enabled) return null` — an unarmed budget yields NO limiter at all); consumed via packages/runtime/src/endpoint-policy.ts#createEndpointRateLimiterRegistry (`deriveBucketConfig(endpoint.rateLimit)` in `limiterFor`, rethrowing with the authoring noun the reader edits)", + "verifiedAt": "2026-08-28", + "note": "ARMS the budget, and it defaults to FALSE — so `rateLimit: { windowMs, maxRequests }` written without it parses into a budget that meters nothing. That is exactly why ADR-0121 D6's gate tests `enabled === true` rather than the presence of `rateLimit`: a presence check would be vacuous. 2026-08-28: RE-ANCHORED (#13003), one leg REPOINTED — the `inbound-rate-limit.ts:88` leg was ACCURATE and is a grammar migration; the `endpoint-policy.ts:148` leg had rotted onto a BLANK LINE. A blank line is the one rot a reader can catch instantly and no check can: it exists, it is in range, and the file names the key elsewhere. Re-closed by hand against 93ea19bca." }, "windowMs": { "status": "live", - "evidence": "packages/runtime/src/security/inbound-rate-limit.ts:91", - "verifiedAt": "2026-08-04", - "note": "Budget window in milliseconds; with `maxRequests` it sizes the token bucket (refillPerSec = maxRequests / (windowMs / 1000)). A zero-or-negative window is an armed-but-unusable budget and fails CLOSED (:102) rather than silently disabling metering." + "evidence": "packages/runtime/src/security/inbound-rate-limit.ts#deriveBucketConfig (`const windowMs = budget.windowMs ?? 60_000`, then `refillPerSec = maxRequests / (windowMs / 1000)`; a zero-or-negative window THROWS rather than silently disabling metering)", + "verifiedAt": "2026-08-28", + "note": "Budget window in milliseconds; with `maxRequests` it sizes the token bucket. An armed-but-unusable budget fails CLOSED rather than silently disabling metering. 2026-08-28: RE-ANCHORED (#13003) — `:91` was ACCURATE, so this is a grammar migration only. Re-closed by hand against 93ea19bca." }, "maxRequests": { "status": "live", - "evidence": "packages/runtime/src/security/inbound-rate-limit.ts:90", - "verifiedAt": "2026-08-04", - "note": "Bucket capacity. Same fail-closed rule as `windowMs` (:95): 'the author asked for metering and got none' is the worse outcome, so an impossible budget errors rather than passing traffic." + "evidence": "packages/runtime/src/security/inbound-rate-limit.ts#deriveBucketConfig (`const maxRequests = budget.maxRequests ?? 100` → the bucket's `capacity`; zero or negative THROWS, naming `enabled: false` as the way to turn metering off)", + "verifiedAt": "2026-08-28", + "note": "Bucket capacity. Same fail-closed rule as `windowMs`: 'the author asked for metering and got none' is the worse outcome, so an impossible budget errors rather than passing traffic. 2026-08-28: RE-ANCHORED (#13003) — `:90` was ACCURATE, so this is a grammar migration only. Re-closed by hand against 93ea19bca." } } }, "cacheTtl": { "status": "live", - "evidence": "packages/runtime/src/endpoint-policy.ts:252 (cacheControlHeader)", - "verifiedAt": "2026-08-04", - "note": "Seconds, emitted as a Cache-Control response header and nothing more (#5091 narrowed the original design to header semantics only — there is no response store). Applied to SUCCESSFUL answers only: telling a client to reuse a 401/429/5xx for half a minute is worse than saying nothing. GET-only — on any other method publish refuses it (#5040 §3.3) and the runtime warns instead of emitting." + "evidence": "packages/runtime/src/endpoint-policy.ts#computeCacheControl (`const ttl = endpoint.cacheTtl`; absent ⇒ no header, non-GET ⇒ no header plus a warn naming the endpoint); packages/runtime/src/endpoint-policy.ts#applyEndpointPolicies (step ③ — the returned header is attached to the SUCCESS answer only)", + "verifiedAt": "2026-08-28", + "note": "Seconds, emitted as a Cache-Control response header and nothing more (#5091 narrowed the original design to header semantics only — there is no response store). Applied to SUCCESSFUL answers only: telling a client to reuse a 401/429/5xx for half a minute is worse than saying nothing. GET-only — on any other method publish refuses it (#5040 §3.3) and the runtime warns instead of emitting. 2026-08-28: RE-ANCHORED (#13003) and PROSE CORRECTED — the line `:252` was ACCURATE (it is the function's own declaration line), but the parenthetical named `cacheControlHeader`, which is not a symbol anywhere in `packages/**` — the function is `computeCacheControl`, and the only other occurrence of the old spelling is a docblock table in `endpoint-publish-gate.ts` that names it and `endpointRateLimiterRegistry` (also stale — `createEndpointRateLimiterRegistry`) as this file's exports, i.e. the same rename went unpropagated in two places. Line-accurate and name-wrong is the combination nothing in the ledger could flag, because no check 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. Re-closed by hand against 93ea19bca." } } } diff --git a/packages/spec/liveness/book.json b/packages/spec/liveness/book.json index c0a54745f9..5f252430c5 100644 --- a/packages/spec/liveness/book.json +++ b/packages/spec/liveness/book.json @@ -1,18 +1,18 @@ { "type": "book", - "_note": "BookSchema (ADR-0046 §6 documentation spine). Consumers: the REST `/meta/book/:name/tree` endpoint (packages/rest/src/rest-server.ts:3078-3169) driving the spec's pure resolveBookTree/audienceAllows (packages/spec/src/system/book.zod.ts), and objectui's console docs portal (apps/console/src/pages/book-nav.ts @940ba24 — a faithful resolver port rendering the reader UI, plus portal-only consumers for slug/icon/order). 15 of 17 live; the two dead entries are both inline `translations` maps that LOOK like the doc-level mechanism that works (`doc.translations`, resolveDocLocale) but have no resolver anywhere. Seeded 2026-08-01 (#4488).", + "_note": "BookSchema (ADR-0046 §6 documentation spine). Consumers: the REST `/meta/book/:name/tree` endpoint (`packages/rest/src/rest-server.ts`, the `book/:name/tree` branch of `registerMetadataEndpointsInner`) driving the spec's pure `resolveBookTree` / `audienceAllows` (packages/spec/src/system/book.zod.ts), and objectui's console docs portal (apps/console/src/pages/book-nav.ts @940ba24 — a faithful resolver port rendering the reader UI, plus portal-only consumers for slug/icon/order). 15 of 17 live; the two dead entries are both inline `translations` maps that LOOK like the doc-level mechanism that works (`doc.translations`, resolveDocLocale) but have no resolver anywhere. Seeded 2026-08-01 (#4488). 2026-08-28 (#13003): every LOCAL citation in this file was re-anchored to its consuming symbol. The four objectui-only entries (`description`, `slug`, `icon`, `order`) are left BYTE-FOR-BYTE UNTOUCHED and undated-forward on purpose: their evidence is pinned at `objectui @940ba24`, a commit this container cannot reproduce, and the anchor grammar deliberately never collects foreign anchors — re-stamping `verifiedAt` on a call graph nobody re-closed is exactly the false confidence this ledger exists to prevent (the `tool.json` precedent).", "props": { "name": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/rest/src/rest-server.ts:3098", - "note": "tree-route identity; an unknown name is treated as a package id and resolved as the implicit per-package book (§6.4)." + "verifiedAt": "2026-08-28", + "evidence": "packages/rest/src/rest-server.ts#deriveImplicitPackageBook (the tree route matches `b.name === req.params.name` and, failing that, synthesizes the implicit per-package book from the same segment — §6.4); packages/spec/src/system/book.zod.ts#resolveBookTree (`return { name: book.name, … }` — the resolved tree carries it back out)", + "note": "tree-route identity; an unknown name is treated as a package id and resolved as the implicit per-package book (§6.4). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `rest-server.ts:3098` had rotted onto `patterns: crud.patterns` in the CRUD-endpoint config, ~1,800 lines from the book route. The `_note`'s file-level range (`:3078-3169`) had rotted with it and is replaced by a symbol too. Re-closed by hand against 93ea19bca." }, "label": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:303", - "note": "carried into the resolved tree; portal cards fall back to `name`." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (`return { name: book.name, label: book.label, … }`)", + "note": "carried into the resolved tree; portal cards fall back to `name`. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:303` lands on `.map((g, i) => ({ g, i }))`, the GROUP sort decoration at the top of `resolveBookTree`: the right function, the wrong read, and about a sibling key (`groups.order`) rather than this one. Right-function-wrong-read is the residual class an anchor cannot remove — the anchor is honest about which function, and the parenthetical now carries the expression, which is the part a reader can check. Re-closed by hand against 93ea19bca." }, "description": { "status": "live", @@ -40,23 +40,23 @@ }, "audience": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/rest/src/rest-server.ts:3113, packages/rest/src/rest-server.ts:2969, packages/spec/src/system/book.zod.ts:351", - "note": "ENFORCED access gate (§6.7), fail-closed: gates the whole tree (401 anonymous / 403 non-holder), and every doc's effective audience is the union over the books that claim it (resolveDocAudiences) — applied to both doc lists and tree entries. The one security-shaped property on this type, and it is real." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#audienceAllows (the gate itself: `public` always, `org`/unset any authenticated principal, `{ permissionSet }` only a holder, unknown shape → false); packages/spec/src/system/book.zod.ts#resolveDocAudiences (`const audience = book.audience ?? 'org'` — every doc's effective audience is the UNION over the books that claim it, unclaimed docs falling back to `org`); packages/rest/src/rest-server.ts#audienceAllows (the tree gate: `if (!audienceAllows(book.audience, caller))` → 401 anonymous / 403 non-holder, before any tree is built)", + "note": "ENFORCED access gate (§6.7), fail-closed: gates the whole tree (401 anonymous / 403 non-holder), and every doc's effective audience is the union over the books that claim it (resolveDocAudiences) — applied to both doc lists and tree entries. The one security-shaped property on this type, and it is real. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — ALL THREE legs were wrong. `rest-server.ts:3113` lands on the comment `// config to read).` and `:2969` on a docblock sentence about `'v1/beta'` splicing a path segment; `book.zod.ts:351` lands on `entries.push(entryFromDoc(d))` inside `resolveBookTree`'s `...` rest-expansion — tree ASSEMBLY, not the gate, ~83 lines from `audienceAllows`. A security-shaped property whose every pointer had drifted, one of them onto a comment, is the case this whole worklist is justified by: the verdict was right, and nothing cited could have shown it. Re-closed by hand against 93ea19bca." }, "groups": { "children": { "key": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:238, packages/spec/src/system/book.zod.ts:290", - "note": "group identity: explicit `doc.group` placement matches on it, and it keys the resolved tree." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (`d.group === group.key` places an explicit member, `derivedMembers.set(group.key, members)` buckets them, and `{ key: group.key, label: group.label, entries }` keys the resolved group)", + "note": "group identity: explicit `doc.group` placement matches on it, and it keys the resolved tree. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:238` is the docblock of `ResolvedEntrySchema` and `:290` a bare `*` continuation line inside `resolveBookTree`'s own docblock. Both sit close enough to be believable and neither is a read. Re-closed by hand against 93ea19bca." }, "label": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:290", - "note": "section title in the resolved tree." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (`resolvedGroups.push({ key: group.key, label: group.label, entries })` — the section title of the resolved group)", + "note": "section title in the resolved tree. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:290` is a bare `*` line in `resolveBookTree`'s docblock, shared with `groups.key`'s rotted second leg: one docblock line doing duty as evidence for two different keys. Re-closed by hand against 93ea19bca." }, "translations": { "status": "dead", @@ -66,27 +66,27 @@ }, "order": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:221", - "note": "orders groups within the book (0 default, then declaration order)." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (`.sort((a, b) => (a.g.order ?? 0) - (b.g.order ?? 0) || a.i - b.i)` — 0 when absent, declaration index as the stable tiebreak)", + "note": "orders groups within the book (0 default, then declaration order). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:221` is `key: string;` in the `ResolvedGroup` INTERFACE: a citation for `order` landing on the type declaration of a sibling key, in a block that describes the RESOLVED shape rather than the authored one. Re-closed by hand against 93ea19bca." }, "include": { "status": "live", - "verifiedAt": "2026-08-17", - "evidence": "packages/spec/src/system/book.zod.ts:236, packages/spec/src/system/book.zod.ts:193", - "note": "the derived-membership rule — the heart of the §6.2.1 design. Both forms are live: the GLOB variant matches on name/group, and the `{ tag }` variant (matchesInclude reads doc.tags) started matching the moment DocSchema DECLARED `tags` in 17.0.0 (#4509, ADR-0049) — see doc.json's `tags` entry for the fix's full history." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#matchesInclude (both forms — a string is a name glob via `globToRegExp`, `{ tag }` tests `doc.tags`, and `scopePackage` short-circuits a foreign doc first); packages/spec/src/system/book.zod.ts#resolveBookTree (`group.include != null && matchesInclude(d, group.include, scope)` — called on the derived-membership pass AND again inside a `pages` group's `...` rest-expansion)", + "note": "the derived-membership rule — the heart of the §6.2.1 design. Both forms are live: the GLOB variant matches on name/group, and the `{ tag }` variant (matchesInclude reads doc.tags) started matching the moment DocSchema DECLARED `tags` in 17.0.0 (#4509, ADR-0049) — see doc.json's `tags` entry for the fix's full history. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `:236` is a docblock line about #12038's describe-only transcription and `:193` a BLANK LINE in the resolver's section banner. The entry's prose already named `matchesInclude` correctly; it is only the positions that had rotted, which is the whole shape of this migration. Re-closed by hand against 93ea19bca." }, "package": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:232", - "note": "scopes the rule to a package id (cross-package books, ADR-0048)." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (`const scope = group.package ?? bookPackage`, on both the derived pass and the `pages` rest-expansion); packages/spec/src/system/book.zod.ts#matchesInclude (`if (scopePackage && doc.packageId && doc.packageId !== scopePackage) return false` — where the scope is actually enforced)", + "note": "scopes the rule to a package id (cross-package books, ADR-0048). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:232` is the `/**` that OPENS `ResolvedEntrySchema`'s docblock. The entry now cites both halves separately: the read that computes the scope and the comparison that enforces it, which were one line apart when this was written and are ~40 lines apart today. Re-closed by hand against 93ea19bca." }, "pages": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:248-286", - "note": "explicit curated-order override; `---` separators and `...` rest-expansion both implemented, node label/badge/icon overrides carried into entries." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (`if (group.pages) continue` excludes the group from the derived pass, then the override walk: `'---'` → a separator entry, `'...'` → rest-expansion filtered by the same `include`/`group` rules, a string → `byName` lookup with a not-found placeholder, and a node object → `entryFromDoc` overlaid with `label`/`badge`/`icon`)", + "note": "explicit curated-order override; `---` separators and `...` rest-expansion both implemented, node label/badge/icon overrides carried into entries. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:248-286` was a RANGE, and the line bound checks only its END (a start inside the file with an end past EOF is the case that rule exists for), so the whole span was unbounded on one side by design; today it spans `ResolvedEntrySchema`'s field list into `ResolvedBookSchema`, i.e. the response contract rather than the resolver. A range citation is the weakest form the grammar allows for exactly this reason — it names more lines and therefore fewer things. Re-closed by hand against 93ea19bca." } }, "note": "Drilled because `translations` diverges (dead) from its six live siblings." diff --git a/packages/spec/liveness/doc.json b/packages/spec/liveness/doc.json index e0e471c521..a89338c4e6 100644 --- a/packages/spec/liveness/doc.json +++ b/packages/spec/liveness/doc.json @@ -1,54 +1,54 @@ { "type": "doc", - "_note": "DocSchema (ADR-0046 flat Markdown package docs). Fully live. The schema header calls docs 'inert data' — true of the KERNEL (it stores `content` unparsed), but every property has a real runtime consumer in the delivery layer: the REST read layer localizes, audience-gates and serves docs (packages/rest/src/rest-server.ts:2944-3022 list, :3384-3390 single item), and the `/meta/book/:name/tree` endpoint resolves book membership from doc headers via the spec's own resolveBookTree (packages/spec/src/system/book.zod.ts:218). objectui's console docs portal is a faithful port of the same resolver (apps/console/src/pages/book-nav.ts @940ba24) rendering the reader UI — a delivery surface for readers, NOT an authoring preview. Seeded 2026-08-01 (#4488). NOTE: `tags` was DECLARED in 17.0.0 (#4509, ADR-0049) — the enforce half of enforce-or-remove: the book-side `include: { tag }` rule and the REST corpus (`d.tags`, rest-server.ts:2965) both already expected the key, so declaring it made the previously-inert `{ tag }` include variant live; see the `tags` entry below for the fix's full history.", + "_note": "DocSchema (ADR-0046 flat Markdown package docs). Fully live. The schema header calls docs 'inert data' — true of the KERNEL (it stores `content` unparsed), but every property has a real runtime consumer in the delivery layer: the REST read layer localizes, audience-gates and serves docs (`packages/rest/src/rest-server.ts`, inside `registerMetadataEndpointsInner` — the `doc` list branch, the `/meta/book/:name/tree` branch and the single-item branch), and the tree endpoint resolves book membership from doc headers via the spec's own `resolveBookTree` (packages/spec/src/system/book.zod.ts). objectui's console docs portal is a faithful port of the same resolver (apps/console/src/pages/book-nav.ts @940ba24) rendering the reader UI — a delivery surface for readers, NOT an authoring preview. Seeded 2026-08-01 (#4488). NOTE: `tags` was DECLARED in 17.0.0 (#4509, ADR-0049) — the enforce half of enforce-or-remove: the book-side `include: { tag }` rule and the REST corpus (`tags: d.tags`) both already expected the key, so declaring it made the previously-inert `{ tag }` include variant live; see the `tags` entry below for the fix's full history. 2026-08-28 (#13003): every citation in this file was re-anchored to its consuming symbol, and this file needed it most in the batch — ALL FIFTEEN of its line citations had rotted, because both cited files were reorganized under it. In `book.zod.ts` the pointers had come to rest inside the `ResolverDoc` / `ResolvedEntry` INTERFACES, i.e. on type declarations of the very fields whose consumers they claimed to cite; in `rest-server.ts` the entire doc-serving block moved ~1,600 lines down (the old 2,9xx-3,3xx pointers now land in the batch-endpoints and server-registration regions). Neither move is visible to the existence check, the line bound or the key-mention check.", "props": { "name": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:225, packages/rest/src/rest-server.ts:3129", - "note": "identity: the single-doc route key, the resolver's membership key (glob `include` matches over names), and the audience map key." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#matchesInclude (`globToRegExp(include).test(doc.name)` — the glob `include` rule matches over names); packages/spec/src/system/book.zod.ts#entryFromDoc (`{ doc: doc.name, … }` — the name is what a rendered tree entry points at); packages/rest/src/rest-server.ts#resolveDocAudiences (`audiences.get(d.name)` — the effective-audience map is keyed by name, on both the list and the tree branch)", + "note": "identity: the single-doc route key, the resolver's membership key (glob `include` matches over names), and the audience map key. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — BOTH legs were wrong. `book.zod.ts:225` is a BLANK LINE inside the `ResolvedBook` interface; `rest-server.ts:3129` lands on `upsertMany: batch.operations?.upsertMany ?? true` in the batch-endpoints config, ~1,480 lines from any doc read. Re-closed by hand against 93ea19bca." }, "label": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:198, packages/spec/src/system/book.zod.ts:202", - "note": "tree entry title + the order tiebreak sort key (byOrderThenLabel)." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#byOrderThenLabel (`(a.label ?? a.name).localeCompare(b.label ?? b.name)` — the order tiebreak); packages/spec/src/system/book.zod.ts#entryFromDoc (`label: doc.label` onto the rendered entry); packages/spec/src/system/doc.zod.ts#resolveDocLocale (`label: variant.label ?? base.label` — the per-locale swap, with per-field fallback to the base doc)", + "note": "tree entry title + the order tiebreak sort key (byOrderThenLabel). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — both legs were wrong and both were wrong in the same instructive way: `book.zod.ts:198` is `description?: string;` and `:202` is the `tags` docblock, i.e. two INTERFACE FIELD DECLARATIONS, one of them another key's. A field declaration is the most convincing wrong place a citation can land — the key is right there, in the right file, spelled correctly — and it proves nothing at all, because a declaration is what a `dead` key has too. Re-closed by hand against 93ea19bca." }, "description": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:202, packages/rest/src/rest-server.ts:3131", - "note": "carried into tree entries and kept on the list response (which strips `content`) so portals can show summaries without fetching bodies." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#entryFromDoc (`description: doc.description` — carried onto the rendered tree entry); packages/spec/src/system/doc.zod.ts#resolveDocLocale (`description: variant.description ?? base.description`); packages/rest/src/rest-server.ts#resolveBookTree (the tree corpus projection `{ name, label, description, order, group, tags, packageId }` the route hands the resolver)", + "note": "carried into tree entries and kept on the list response (which strips `content`) so portals can show summaries without fetching bodies. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:202` is the `tags` docblock (a DIFFERENT key's documentation) and `rest-server.ts:3131` is `defaultAtomic: batch.defaultAtomic ?? true` in the batch-endpoints config. Re-closed by hand against 93ea19bca." }, "content": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/rest/src/rest-server.ts:3007, packages/spec/src/system/doc.zod.ts:120", - "note": "the document body: served whole on single-doc GET, deliberately stripped from list responses unless `?include=content`, locale-swapped by resolveDocLocale." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/doc.zod.ts#resolveDocLocale (`content: variant.content` — the locale swap, and the ONLY field with no per-field fallback: a variant that omits it yields undefined by design); packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (the `doc` list branch destructures `const { content: _content, ...rest } = it` unless `?include=content`; the single-item branch returns the collapsed doc whole)", + "note": "the document body: served whole on single-doc GET, deliberately stripped from list responses unless `?include=content`, locale-swapped by resolveDocLocale. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `rest-server.ts:3007` had rotted onto a docblock about `enableProjectScoping`, and `doc.zod.ts:120` onto the middle of the `tags` docblock — the sentence \"the consumer already existed, so this is the enforce half of enforce-or-remove\", which is about `tags`, cited as evidence for `content`. HONEST RESIDUAL on the rest-server leg: the `?include=content` strip is inline in a ~2,800-line route registrar with no enclosing named helper, so the anchor is the registrar itself. That is coarse, and it is stated rather than dressed up — the narrow half of this entry is the `doc.zod.ts` anchor, which is exact. Re-closed by hand against 93ea19bca." }, "order": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:198", - "note": "sort key within a book group (0 when absent, then label)." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#byOrderThenLabel (`(a.order ?? 0) - (b.order ?? 0)` — the primary sort, absent reads as 0); packages/rest/src/rest-server.ts#resolveDocAudiences (`order: d.order` in the list-branch corpus projection)", + "note": "sort key within a book group (0 when absent, then label). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:198` is `description?: string;` in the `ResolverDoc` interface: a citation for `order` landing on the DECLARATION of a different key. `order`'s own declaration is the next line but one, which is how close a wrong pointer can sit to a right one and still say nothing. Re-closed by hand against 93ea19bca." }, "group": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/book.zod.ts:238", - "note": "explicit book-group membership; a doc joins the group whose `key` equals it when no `include` rule claims it first." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#resolveBookTree (a doc joins the first group whose `include` matches it OR whose `key` equals `doc.group`); packages/rest/src/rest-server.ts#resolveDocAudiences (`group: d.group` in the list-branch corpus projection)", + "note": "explicit book-group membership; a doc joins the group whose `key` equals it when no `include` rule claims it first. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:238` had rotted onto the docblock of `ResolvedEntrySchema` (a sentence about #12038's describe-only transcription), ~63 lines above `resolveBookTree`. Re-closed by hand against 93ea19bca." }, "translations": { "status": "live", - "verifiedAt": "2026-08-01", - "evidence": "packages/spec/src/system/doc.zod.ts:110, packages/rest/src/rest-server.ts:2996, packages/rest/src/rest-server.ts:3388", - "note": "per-locale {label,description,content} variants collapsed by resolveDocLocale on every read path (list, tree corpus, single item); the map itself is stripped from responses. This is the doc's OWN i18n mechanism — the generic bundle translator does not cover `doc`." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/doc.zod.ts#resolveDocLocale (`const { translations, ...base } = doc` then `translations[want] ?? translations[want.split('-')[0]]` — exact locale, then primary subtag, then the base doc; the map itself never survives into the return value); packages/rest/src/rest-server.ts#resolveDocLocale (called on every doc read path — the list branch maps it over the items, the tree branch over the corpus, the single-item branch over the one doc)", + "note": "per-locale {label,description,content} variants collapsed by resolveDocLocale on every read path (list, tree corpus, single item); the map itself is stripped from responses. This is the doc's OWN i18n mechanism — the generic bundle translator does not cover `doc`. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — all three legs were wrong: `doc.zod.ts:110` is the `/**` that OPENS the `tags` docblock, and `rest-server.ts:2996` / `:3388` are comments about `os serve` config and `api.version` respectively. The rest-server leg is anchored to the imported symbol it calls rather than to the registrar, because that is what actually has to survive for this claim to hold: drop the import and the three call sites go with it, and the anchor reds. Re-closed by hand against 93ea19bca." }, "tags": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/spec/src/system/book.zod.ts:213 (matchesInclude tag branch), reached from :255 and :281; transported by packages/rest/src/rest-server.ts:3218", - "note": "DECLARED 2026-08-02 (#4509, ADR-0049) — the enforce half of enforce-or-remove. The consumer, the transport and the resolver-side interface all predated this key: matchesInclude compared `doc.tags` against a group's `include: { tag }`, the book route already forwarded `tags: d.tags`, and ResolverDoc declared `tags?: string[]` marked '(P3d; absent today)'. What was missing was one line HERE — DocSchema is strict, so authoring `tags:` was a parse error, every doc reached the resolver with tags undefined, and the `{ tag }` include variant could never match. Removing the variant was the alternative and was rejected: a union member has no clean tombstone (retiredKey covers object keys), so authors would have gotten a bare union error, and it would have discarded working matcher code. Live on arrival — the branch it feeds is reachable the moment a doc carries a tag." + "verifiedAt": "2026-08-28", + "evidence": "packages/spec/src/system/book.zod.ts#matchesInclude (`Array.isArray(doc.tags) && doc.tags.includes(include.tag)` — the tag branch of `include`); packages/spec/src/system/book.zod.ts#resolveBookTree (the caller that decides membership from it); packages/rest/src/rest-server.ts#resolveBookTree (`tags: d.tags` in the tree corpus the route hands the resolver — the transport half)", + "note": "DECLARED 2026-08-02 (#4509, ADR-0049) — the enforce half of enforce-or-remove. The consumer, the transport and the resolver-side interface all predated this key: matchesInclude compared `doc.tags` against a group's `include: { tag }`, the book route already forwarded `tags: d.tags`, and ResolverDoc declared `tags?: string[]` marked '(P3d; absent today)'. What was missing was one line HERE — DocSchema is strict, so authoring `tags:` was a parse error, every doc reached the resolver with tags undefined, and the `{ tag }` include variant could never match. Removing the variant was the alternative and was rejected: a union member has no clean tombstone (retiredKey covers object keys), so authors would have gotten a bare union error, and it would have discarded working matcher code. Live on arrival — the branch it feeds is reachable the moment a doc carries a tag. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `book.zod.ts:213` is `description?: string;` in the `ResolvedEntry` interface, and the parenthetical claiming it was \"matchesInclude tag branch\" named the right function ~61 lines away; the \"reached from :255 and :281\" hops were prose the ledger never bounded. `rest-server.ts:3218` had rotted onto `this.registerSecurityEndpoints(bp)` — a route-registration call, ~1,700 lines from the corpus that forwards the tags. Re-closed by hand against 93ea19bca." } } } diff --git a/packages/spec/liveness/email_template.json b/packages/spec/liveness/email_template.json index e81289aa64..782c46534c 100644 --- a/packages/spec/liveness/email_template.json +++ b/packages/spec/liveness/email_template.json @@ -1,85 +1,85 @@ { "type": "email_template", - "_note": "EmailTemplateDefinitionSchema. THE WHOLE SURFACE WENT LIVE with the #4509 materializer bridge — this file previously recorded the webhook (#3461) disconnect shape, all 13 props dead, and it is kept as the worked example that a dead verdict is a worklist entry, not a tombstone: enforce-or-remove resolved this one by ENFORCING. What closed it: `bootstrapDeclaredEmailTemplates` (packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts) validates each declared item through `EmailTemplateDefinitionSchema.parse()` (:148 — the spec schema finally has a real consumer, defaults and all) and materializes it into the `sys_email_template` ROW that the execution point reads, via `mapTemplateToRow` (:76) — the SAME mapping the built-in seeder uses (email-plugin.ts:533), shared deliberately so the two doors cannot drift. The execution end is unchanged: `IEmailService.sendTemplate` resolves (name, locale) with an en-US fallback (email-service.ts:402-416) and honors active/variables/fromOverride/replyTo off the row. THREE breaks had to close, not one: the engine never registered authored `emailTemplates:` into the registry at all (objectql/src/engine.ts metadataArrayKeys — the key was simply missing, so the bridge's source was empty); built-in seeds left `managed_by` at the column default 'admin', which made them masquerade as admin-authored and permanently outrank a declared template (email-plugin.ts:533 now stamps 'platform'); and only then did the bridge's write land. The ADR-0054 proof pins all three by asserting the AUTHORED wording is what sendTemplate actually renders. Runtime authoring is covered too: email_template is allowRuntimeCreate:true (unlike webhook), so a boot-only sweep would leave a Studio save inert until restart — the plugin also subscribes to `email_template` metadata changes and re-materializes the single changed item (email-plugin.ts:428-455). Seed-not-clobber mirrors sys_webhook (#3489): declared rows seed as `managed_by:'package'` and re-seed every boot, but an admin-authored ('admin') or admin-edited ('customized', stamped by email-template-provenance.ts) row is never overwritten — a reworded transactional mail survives redeploys. Nothing carries an authorWarn any more: authoring is no longer a no-op. `protection`/_lock*/_provenance are framework overlay fields, auto-live.", + "_note": "EmailTemplateDefinitionSchema. THE WHOLE SURFACE WENT LIVE with the #4509 materializer bridge — this file previously recorded the webhook (#3461) disconnect shape, all 13 props dead, and it is kept as the worked example that a dead verdict is a worklist entry, not a tombstone: enforce-or-remove resolved this one by ENFORCING. What closed it: `bootstrapDeclaredEmailTemplates` (packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts) validates each declared item through `EmailTemplateDefinitionSchema.parse()` (in `upsertDeclaredEmailTemplate` — the spec schema finally has a real consumer, defaults and all) and materializes it into the `sys_email_template` ROW that the execution point reads, via `mapTemplateToRow` — the SAME mapping the built-in seeder uses (`EmailServicePlugin.upsertTemplate`), shared deliberately so the two doors cannot drift. The execution end is unchanged in mechanism but NOT in shape: `IEmailService.sendTemplate` resolves (name, locale) with an en-US fallback and honors active/variables/fromOverride/replyTo off the row — except that since #9225 the resolve-and-render half lives in `EmailService.resolveAndRenderTemplate`, shared with the render-only `renderTemplate`, and `sendTemplate` keeps only the envelope reads. THREE breaks had to close, not one: the engine never registered authored `emailTemplates:` into the registry at all (objectql/src/engine.ts metadataArrayKeys — the key was simply missing, so the bridge's source was empty); built-in seeds left `managed_by` at the column default 'admin', which made them masquerade as admin-authored and permanently outrank a declared template (`upsertTemplate` now stamps 'platform'); and only then did the bridge's write land. The ADR-0054 proof pins all three by asserting the AUTHORED wording is what sendTemplate actually renders. Runtime authoring is covered too: email_template is allowRuntimeCreate:true (unlike webhook), so a boot-only sweep would leave a Studio save inert until restart — the plugin also subscribes to `email_template` metadata changes and re-materializes the single changed item (`EmailServicePlugin.wireTemplateMutationProjection`). Seed-not-clobber mirrors sys_webhook (#3489): declared rows seed as `managed_by:'package'` and re-seed every boot, but an admin-authored ('admin') or admin-edited ('customized', stamped by email-template-provenance.ts) row is never overwritten — a reworded transactional mail survives redeploys. Nothing carries an authorWarn any more: authoring is no longer a no-op. `protection`/_lock*/_provenance are framework overlay fields, auto-live. 2026-08-28 (#13003): every citation in this file was re-anchored to its consuming symbol; the file-level pointers above were carrying line numbers of their own and are now named by symbol for the same reason.", "props": { "name": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:78 (row mapping); packages/plugins/plugin-email/src/email-service.ts:411 (the sendTemplate lookup key)", - "note": "The (name, locale) resolution key at both ends of the bridge." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`name: tpl.name` — the row projection, and half the (name, locale) upsert key `upsertDeclaredEmailTemplate` matches on); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (`loader.load(input.template, wanted)` — the send-end lookup key, and `TEMPLATE_NOT_FOUND: ${input.template}` when it misses)", + "note": "The (name, locale) resolution key at both ends of the bridge. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE (`:78` still names the column), so that half is a pure grammar migration; the send-end leg had rotted. `email-service.ts:411` lands on a docblock line of the `TemplateLoader` interface (\"or `null` when none exists / inactive\"), ~780 lines above the read. ALL NINE of this file's `email-service.ts` citations rotted together and by the same cause: #9225 split the resolver out of `sendTemplate` into `resolveAndRenderTemplate`, and the reads moved with it from the 400s to the 1170-1310 band. That is the drift no existing check can see — the file exists, every cited line is still inside it, and the file still names each key, so the existence check, the line bound and the key-mention check all pass on a pointer that is ~750 lines wrong. Re-closed by hand against 93ea19bca." }, "label": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:79", - "note": "Materialized into the `label` column — the Studio row title (sys_email_template nameField)." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`label: tpl.label`)", + "note": "Materialized into the `label` column — the Studio row title (sys_email_template nameField). 2026-08-28: RE-ANCHORED (#13003) — `:79` was ACCURATE, so this is a grammar migration only. Re-closed by hand against 93ea19bca." }, "category": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:80", - "note": "Materialized into `category`; a Studio filter facet on the row, never send behavior." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`category: tpl.category`)", + "note": "Materialized into `category`; a Studio filter facet on the row, never send behavior. 2026-08-28: RE-ANCHORED (#13003) — `:80` was ACCURATE, so this is a grammar migration only. Re-closed by hand against 93ea19bca." }, "locale": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:81 (row mapping, and half the upsert key); packages/plugins/plugin-email/src/email-service.ts:411-416 ((name, locale) resolution with en-US fallback)", - "note": "Load-bearing in both directions: the same template in two locales materializes as two rows, and the recipient's locale picks between them." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`locale: tpl.locale` — the row projection, and the other half of the upsert key); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (the (name, locale) ladder: the caller's locale, then the documented default, then the loader's own no-locale answer); packages/plugins/plugin-email/src/email-service.ts#DEFAULT_TEMPLATE_LOCALE (the `en-US` the ladder's second rung asks for BY NAME — #7731 is what happened while that rung passed `undefined`)", + "note": "Load-bearing in both directions: the same template in two locales materializes as two rows, and the recipient's locale picks between them. The resolved row's locale is also the render locale for ADR-0053 format filters. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:411-416` had rotted onto the `TemplateLoader` docblock, the same #9225 resolver split that moved every `email-service.ts` citation in this file. The ladder is now anchored to the function that runs it plus the constant it names, so a rung deleted or the default renamed goes red instead of drifting. Re-closed by hand against 93ea19bca." }, "subject": { "status": "live", - "verifiedAt": "2026-08-02", + "verifiedAt": "2026-08-28", "proof": "packages/qa/dogfood/test/email-template-materialization.dogfood.test.ts#email-template-materialization", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:82 (row mapping); packages/plugins/plugin-email/src/email-service.ts:444 (rendered with {{path}} holes by renderTemplate)", - "note": "ADR-0054 high-risk class (email-template-materialization): the authored subject is the representative check for the whole authoring→send pipeline, which crosses manifest-decomposition → the ObjectQL registry → the materializer → the sys_email_template row → the TemplateLoader → sendTemplate. Exactly the multi-layer seam that was broken here, and in three places at once. The proof authors a stack `emailTemplates:` entry overriding a BUILT-IN auth template and asserts the authored wording is what reaches the transport — the ADR-0078 false-compliance case #4509 named (an admin 'fixes' the password-reset mail and users keep receiving the built-in copy)." + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`subject: tpl.subject`); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (`renderTemplate(row.subject, data, renderOpts)` — the {{path}} holes filled with the resolved row's locale)", + "note": "ADR-0054 high-risk class (email-template-materialization): the authored subject is the representative check for the whole authoring→send pipeline, which crosses manifest-decomposition → the ObjectQL registry → the materializer → the sys_email_template row → the TemplateLoader → sendTemplate. Exactly the multi-layer seam that was broken here, and in three places at once. The proof authors a stack `emailTemplates:` entry overriding a BUILT-IN auth template and asserts the authored wording is what reaches the transport — the ADR-0078 false-compliance case #4509 named (an admin 'fixes' the password-reset mail and users keep receiving the built-in copy). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:444` had rotted onto an `EmailServiceOptions` docblock about sys_email PERSISTENCE, which is the most misleading rot in this file because it still reads as email-service configuration. The render moved with the #9225 resolver split. Re-closed by hand against 93ea19bca." }, "bodyHtml": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:83 (bodyHtml→body_html); packages/plugins/plugin-email/src/email-service.ts:445", - "note": "The camelCase→snake_case remap the proof asserts alongside the subject." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`body_html: tpl.bodyHtml` — the camelCase→snake_case remap); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (`renderTemplate(row.body_html, data, renderOpts)`)", + "note": "The camelCase→snake_case remap the proof asserts alongside the subject. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:445` had rotted onto `persistence?: EmailPersistence;`, an options field, ~800 lines above the render. Same #9225 resolver split. Re-closed by hand against 93ea19bca." }, "bodyText": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:84 (bodyText→body_text); packages/plugins/plugin-email/src/email-service.ts:446-448", - "note": "Omitted from the row rather than nulled when unset, so a re-seed never blanks it; the send path then auto-derives text from HTML (htmlToText)." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`...(tpl.bodyText ? { body_text: tpl.bodyText } : {})` — omitted, never nulled); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (`row.body_text ? renderTemplate(row.body_text, …) : htmlToText(html)` — the auto-derive fallback)", + "note": "Omitted from the row rather than nulled when unset, so a re-seed never blanks it; the send path then auto-derives text from HTML (htmlToText). 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:446-448` had rotted onto the `templateLoader` / `retries` option docblocks. Same #9225 resolver split. Re-closed by hand against 93ea19bca." }, "variables": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:91 (variables→variables_json); packages/plugins/plugin-email/src/email-service.ts:427-431 (requireVars fails the send fast on a missing required var)", - "note": "All four child keys share this verdict — not drilled. `required` is a real runtime gate, not a hint." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`...(tpl.variables?.length ? { variables_json: JSON.stringify(tpl.variables) } : {})`); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (parses `row.variables_json`, filters to the `required` ones and calls `requireVars` — a missing one throws MISSING_VARIABLES and the send never happens)", + "note": "All four child keys share this verdict — not drilled. `required` is a real runtime gate, not a hint. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — BOTH legs were wrong, and the bootstrap one is the sharpest rot in this file: `bootstrap-declared-email-templates.ts:91` lands on `is_system: tpl.isSystem`, i.e. on the line `isSystem` cites for ITSELF. Two entries citing one line for two different keys is a state the ledger cannot detect from the inside — the file names both keys, so key-mention anchors both, and the row projection really is 15 lines of near-identical shape. The real mapping is the last line of the same object literal. `email-service.ts:427-431` had rotted onto the `EmailTemplateRow` interface — the row SHAPE rather than the gate that reads it, which is the plausible-sibling failure key-mention was built for and cannot catch inside one file. Re-closed by hand against 93ea19bca." }, "fromOverride": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:85-88 (fromOverride.address/.name → from_address/from_name); packages/plugins/plugin-email/src/email-service.ts:450-453", - "note": "Both child keys share this verdict." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (fromOverride.address/.name → from_address/from_name, address-gated); packages/plugins/plugin-email/src/email-service.ts#sendTemplate (`input.from ?? (row.from_address ? { address: row.from_address, … } : undefined)` — the caller still wins); packages/plugins/plugin-email/src/email-service.ts#rowEnvelope (the generic row→message path, which REFUSES a row with no from_address)", + "note": "Both child keys share this verdict. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:450-453` had rotted onto the `logger` / `defaultTemplateContext` option fields. The send-end read stayed in `sendTemplate` across the #9225 split (it is an envelope read, not a render read), which is exactly why anchoring is worth doing here: the two halves of this entry moved apart and only the symbol names say which is which. `rowEnvelope` is added as the second local consumer — it was never cited, and it is the one that turns a missing from_address into a refusal rather than a silent send. Re-closed by hand against 93ea19bca." }, "replyTo": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:89 (replyTo→reply_to); packages/plugins/plugin-email/src/email-service.ts:139", - "note": "Honored on the outbound message when the caller passes no explicit replyTo." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`...(tpl.replyTo ? { reply_to: tpl.replyTo } : {})`); packages/plugins/plugin-email/src/email-service.ts#sendTemplate (`input.replyTo ?? row.reply_to` — the row's value used only when the caller passes none); packages/plugins/plugin-email/src/email-service.ts#rowEnvelope (`if (row.reply_to) msg.replyTo = String(row.reply_to)` — the persisted row's own projection onto the outbound message)", + "note": "Honored on the outbound message when the caller passes no explicit replyTo. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:139` had rotted onto a docblock about the sys_email outbox id contract, ~1150 lines above the read and about a completely different mechanism. That is the largest single drift measured in this batch, and it passed every check for the usual reason. Re-closed by hand against 93ea19bca." }, "active": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:90; packages/plugins/plugin-email/src/email-service.ts:418-419 (active:false → TEMPLATE_INACTIVE)", - "note": "Also the withdrawal mechanism: deleting a declared template deactivates its rows rather than destroying them (bootstrap-declared-email-templates.ts:201)." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`active: tpl.active`); packages/plugins/plugin-email/src/email-service.ts#resolveAndRenderTemplate (`if (row.active === false) throw new Error('TEMPLATE_INACTIVE: …')` — the send refuses before rendering); packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#deactivateDeclaredEmailTemplate (the withdrawal path: a deleted declared template flips `active: false` on its package-managed rows rather than destroying them)", + "note": "Also the withdrawal mechanism: deleting a declared template deactivates its rows rather than destroying them. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-service.ts:418-419` had rotted onto the closing lines of the `TemplateLoader` docblock (`:419` is the bare `*/`), and the note's `bootstrap-declared-email-templates.ts:201` pointer — never in `evidence`, so nothing ever bounded it — lands on `if (row?.id) {` inside the seed-not-clobber branch, a different mechanism in the same file. The withdrawal path is now cited in `evidence` under its own symbol, where the checks can reach it. Re-closed by hand against 93ea19bca." }, "isSystem": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:91 (isSystem→is_system); packages/plugins/plugin-email/src/email-plugin.ts:544 (gates built-in re-seeding)", - "note": "The BUILT-IN seeder's provenance axis, distinct from the declared bridge's managed_by/customized pair — a declared template lands is_system:false, which is what stops the built-in auth seed reclaiming the row on the next boot." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`is_system: tpl.isSystem`); packages/plugins/plugin-email/src/email-plugin.ts#upsertTemplate (`if (existingRow.is_system === false) return;` — the built-in seeder stands down rather than reclaiming the row)", + "note": "The BUILT-IN seeder's provenance axis, distinct from the declared bridge's managed_by/customized pair — a declared template lands is_system:false, which is what stops the built-in auth seed reclaiming the row on the next boot. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — the bootstrap leg was ACCURATE; `email-plugin.ts:544` had rotted onto `const result = await target.sendInline({` inside the operator's TEST-MAIL button, ~980 lines from the gate and about sending rather than seeding. `email-plugin.ts` is 1550 lines and names `is_system` on exactly four of them, three of which are prose — a file that large is where a line citation is worth least and an anchor most. Re-closed by hand against 93ea19bca." }, "description": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts:92", - "note": "Materialized into `description`; docs-shaped — read by humans in Studio, never by the send path." + "verifiedAt": "2026-08-28", + "evidence": "packages/plugins/plugin-email/src/bootstrap-declared-email-templates.ts#mapTemplateToRow (`...(tpl.description ? { description: tpl.description } : {})`)", + "note": "Materialized into `description`; docs-shaped — read by humans in Studio, never by the send path. 2026-08-28: RE-ANCHORED (#13003) — `:92` was ACCURATE, so this is a grammar migration only. Re-closed by hand against 93ea19bca." } } } diff --git a/packages/spec/liveness/job.json b/packages/spec/liveness/job.json index 99b0e3938a..4c71a93655 100644 --- a/packages/spec/liveness/job.json +++ b/packages/spec/liveness/job.json @@ -1,12 +1,12 @@ { "type": "job", - "_note": "JobSchema. The file-authored path is healthy: `defineStack({ jobs })` → app-plugin kernel:ready → IJobService.schedule (packages/runtime/src/app-plugin.ts:790-855) → the service-job adapters honor every schedule shape (packages/services/service-job/src/cron-job-adapter.ts:71-88) and runWithPolicy enforces retryPolicy/timeout (#3494 — these used to be parsed-but-ignored). `retryPolicy` here is the ENFORCED spelling ({maxRetries, backoffMs, backoffMultiplier}); do not confuse it with the datasource `retryPolicy`, which is dead and spells its delay differently. TYPE-LEVEL GAP CLOSED 2026-08-02 (#4509) by CLOSING THE DOOR, not building a bridge: `job` was registered `allowRuntimeCreate: true` while only the compiled bundle's `jobs` ever reached the scheduler, so a Studio-created job saved cleanly and never ran. Unlike the webhook (#3461) and email_template (#4509 item 1) disconnects, this one could not be bridged: `handler` names a function in the compiled bundle's function table (`collectBundleFunctions`, app-plugin.ts:812), which a runtime writer does not have and cannot name — the missing piece is a handler-binding design, not an ingestion path. So `allowRuntimeCreate` AND `allowOrgOverride` are now both false (metadata-plugin.zod.ts, with the rationale block), leaving `*.job.ts` / `defineStack({ jobs })` as the supported doors. The kind stays registered: its file loader is genuinely consumed, so it still passes the ADR-0088 admission test. Evidence lines restamped 2026-08-02 — the seeded set pointed at app-plugin.ts:767-791, which had drifted ~25 lines (the gate resolves paths, not line numbers, so nothing failed; this is the rot mode `verifiedAt` exists to catch). Seeded 2026-08-01.", + "_note": "JobSchema. The file-authored path is healthy: `defineStack({ jobs })` → `AppPlugin.start`'s `kernel:ready` hook → IJobService.schedule → the service-job adapters honor every schedule shape (`CronJobAdapter.schedule` / `DbJobAdapter.schedule`) and `runWithPolicy` enforces retryPolicy/timeout (#3494 — these used to be parsed-but-ignored). `retryPolicy` here is the ENFORCED spelling ({maxRetries, backoffMs, backoffMultiplier}); do not confuse it with the datasource `retryPolicy`, which is dead and spells its delay differently. TYPE-LEVEL GAP CLOSED 2026-08-02 (#4509) by CLOSING THE DOOR, not building a bridge: `job` was registered `allowRuntimeCreate: true` while only the compiled bundle's `jobs` ever reached the scheduler, so a Studio-created job saved cleanly and never ran. Unlike the webhook (#3461) and email_template (#4509 item 1) disconnects, this one could not be bridged: `handler` names a function in the compiled bundle's function table (`collectBundleFunctions`), which a runtime writer does not have and cannot name — the missing piece is a handler-binding design, not an ingestion path. So `allowRuntimeCreate` AND `allowOrgOverride` are now both false (metadata-plugin.zod.ts, with the rationale block), leaving `*.job.ts` / `defineStack({ jobs })` as the supported doors. The kind stays registered: its file loader is genuinely consumed, so it still passes the ADR-0088 admission test. Seeded 2026-08-01. 2026-08-28 (#13003): every LOCAL citation re-anchored to its consuming symbol — and this file is its own best argument for doing so. The 2026-08-02 note recorded 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, this time by ~70-100 lines, onto `} else {`, `try {`, a bare `}` and a line about registering ACTIONS. Restamping is not a fix for line rot, it is the same claim with a newer date — which is the case this whole worklist rests on. The two objectui-cited entries (`label`, `description`) are left BYTE-FOR-BYTE UNTOUCHED: their evidence and producer are pinned at `objectui @aeb8424b`, a commit this container cannot reproduce, and foreign anchors are never collected by the scanner.", "props": { "name": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/runtime/src/app-plugin.ts:815, packages/runtime/src/app-plugin.ts:833", - "note": "scheduling identity; a job without one is skipped loudly." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/app-plugin.ts#start (`const jobName: string = job?.name` — a job without one is skipped with a warn before anything else is read, and the name is the scheduling key passed to `svc.schedule` plus the subject of every diagnostic in the loop)", + "note": "scheduling identity; a job without one is skipped loudly. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `app-plugin.ts:815` had rotted onto a bare `} else {` and `:833` onto a bare `try {`, ~98 and ~80 lines above the reads. HONEST RESIDUAL: the scheduling loop is inline in `AppPlugin.start`'s `kernel:ready` hook with no enclosing named helper, so `start` is the anchor — a weak one at text level, since almost any file contains the word. It is the true enclosing symbol and it is named as such rather than dressed up; the entries below that have a distinctive downstream consumer cite it beside `start` for exactly this reason. Re-closed by hand against 93ea19bca." }, "label": { "status": "live", @@ -26,35 +26,35 @@ }, "schedule": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/runtime/src/app-plugin.ts:834, packages/services/service-job/src/cron-job-adapter.ts:71-88, packages/services/service-job/src/db-job-adapter.ts:83", - "note": "all three variants enforced: cron `expression` + per-job `timezone` (cron-job-adapter.ts:76-77), interval `intervalMs` (:82), once `at` (:87); the db adapter persists the shape onto sys_job (db-job-adapter.ts:233-245). WALK BOUNDARY: a discriminated union — the gate classifies it as one property; the per-variant keys are covered by the adapter evidence above, not by ledger rows." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/job-schedule.ts#toBoundaryJobSchedule (#4567 authoring tier → boundary tier: the parsed `Schedule`'s cron `expression` is the ADR expression envelope `{dialect,source}` and is lowered to the bare string the adapters take, refusing by name rather than scheduling a wrong shape); packages/runtime/src/app-plugin.ts#start (`toBoundaryJobSchedule(job.schedule, jobName)` — the call site, and `job.schedule` again in the FAILED-TO-SCHEDULE error); packages/services/service-job/src/cron-job-adapter.ts#CronJobAdapter (`schedule()` branches all three variants: `schedule.expression` + the per-job timezone, `schedule.type === 'interval' && schedule.intervalMs`, `schedule.type === 'once' && schedule.at`); packages/services/service-job/src/db-job-adapter.ts#DbJobAdapter (`schedule()` routes the cron variant to the cron adapter and persists the shape onto sys_job via `upsertJobRow`)", + "note": "all three variants enforced: cron `expression` + per-job `timezone`, interval `intervalMs`, once `at`; the db adapter persists the shape onto sys_job. WALK BOUNDARY: a discriminated union — the gate classifies it as one property; the per-variant keys are covered by the adapter evidence above, not by ledger rows. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — all three legs were wrong. `app-plugin.ts:834` had rotted onto `const actions = collectBundleActions(this.bundle)` — a DIFFERENT metadata kind's registration, which is the most misleading landing in this file because it still reads as bundle-wiring code; `cron-job-adapter.ts:71-88` and `db-job-adapter.ts:83` had both rotted onto docblocks. The entry also gains `toBoundaryJobSchedule`, the #4567 lowering seam that did not exist when this was written and is now the first thing that reads the key. Re-closed by hand against 93ea19bca." }, "handler": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/runtime/src/app-plugin.ts:824-830", - "note": "resolved against the bundle's function map (`collectBundleFunctions`, app-plugin.ts:812); a missing handler skips the job with a warning rather than scheduling a no-op. This resolution is ALSO why the type is closed to runtime creation (#4509): the function table is a bundle artifact, so a handler string authored at runtime has nothing to resolve against." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/app-plugin.ts#start (`const handler = fnMap[job.handler]` — a miss warns and SKIPS the job rather than scheduling a no-op); packages/runtime/src/app-plugin.ts#collectBundleFunctions (the bundle function table the string resolves against — the same registry hooks and actions use)", + "note": "resolved against the bundle's function map; a missing handler skips the job with a warning rather than scheduling a no-op. This resolution is ALSO why the type is closed to runtime creation (#4509): the function table is a bundle artifact, so a handler string authored at runtime has nothing to resolve against. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `app-plugin.ts:824-830` had rotted onto a comment about registering actions on `POST /api/v1/actions/...`, ~95 lines above the lookup. The `collectBundleFunctions` half was named in the note with a line (`:812`) that the ledger never bounded because notes are prose; it is now in `evidence` under its own symbol, which is the leg that carries the runtime-creation argument. Re-closed by hand against 93ea19bca." }, "retryPolicy": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/runtime/src/app-plugin.ts:838-841, packages/services/service-job/src/run-with-policy.ts:58-65", - "note": "maxRetries/backoffMs/backoffMultiplier all drive the exponential-backoff retry loop (delay = backoffMs * multiplier^(retry-1)). Enforced since #3494. This is the `retryPolicy` the datasource ledger warns about confusing with its dead namesake." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/app-plugin.ts#start (`(job.retryPolicy || job.timeout) ? { retryPolicy: job.retryPolicy, timeout: job.timeout } : undefined` — threaded into `svc.schedule` only when the author set one); packages/services/service-job/src/run-with-policy.ts#runWithPolicy (`const policy = options?.retryPolicy` → maxRetries / backoffMs / backoffMultiplier / maxRetryDelayMs / jitter drive the retry loop); packages/services/service-job/src/run-with-policy.ts#RETRY_DEFAULTS (what an OMITTED member means since 17.0.0 — maxRetries 0, i.e. no retry unless asked for, #4661)", + "note": "maxRetries/backoffMs/backoffMultiplier all drive the exponential-backoff retry loop (delay = min(backoffMs * multiplier^(retry-1), maxRetryDelayMs), jittered when asked). Enforced since #3494. This is the `retryPolicy` the datasource ledger warns about confusing with its dead namesake. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `app-plugin.ts:838-841` had rotted onto `if (actions.length > 0 && typeof ql.registerAction === 'function')`, another ACTIONS line, and `run-with-policy.ts:58-65` onto the `JobAttemptRecorder` interface — a neighbouring type rather than the policy loop. The `RETRY_DEFAULTS` leg is new and is the one that decides what an author's silence means, which is the half a reader of this entry most needs. Re-closed by hand against 93ea19bca." }, "timeout": { "status": "live", - "verifiedAt": "2026-08-09", + "verifiedAt": "2026-08-28", "evidenceScope": "in-repo", - "evidence": "packages/services/service-job/src/run-with-policy.ts:25-33", - "producer": "packages/runtime/src/app-plugin.ts:875-876 — the scheduler threads `{ retryPolicy: job.retryPolicy, timeout: job.timeout }` into svc.schedule, and only when the author set one of them", - "note": "per-attempt limit; an over-limit run records execution status 'timeout' (JobTimeoutError). The in-flight handler is abandoned, not cancelled — as documented. Producer side re-verified 2026-08-09 (#4837 slice): the seeded row cited the consumer only, which is the shape that carried `Seed.env` — here it holds, because #3494 wired the threading at the same time as the enforcement." + "evidence": "packages/services/service-job/src/run-with-policy.ts#runWithPolicy (`const timeoutMs = options?.timeout` — applied PER ATTEMPT, and a timed-out attempt retries like any other failure); packages/services/service-job/src/run-with-policy.ts#withTimeout (the race itself); packages/services/service-job/src/run-with-policy.ts#JobTimeoutError (what an over-limit attempt rejects with — the in-flight handler is abandoned, not cancelled)", + "producer": "packages/runtime/src/app-plugin.ts#start — the scheduler threads `{ retryPolicy: job.retryPolicy, timeout: job.timeout }` into `svc.schedule`, and only when the author set one of them", + "note": "per-attempt limit; an over-limit run records execution status 'timeout' (JobTimeoutError). The in-flight handler is abandoned, not cancelled — as documented. Producer side re-verified 2026-08-09 (#4837 slice): the seeded row cited the consumer only, which is the shape that carried `Seed.env` — here it holds, because #3494 wired the threading at the same time as the enforcement. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED, BOTH LEGS — `run-with-policy.ts:25-33` had rotted onto `jitter: false` inside `RETRY_DEFAULTS`, a retryPolicy default rather than anything about `timeout`, and the PRODUCER pointer `app-plugin.ts:875-876` onto a bare `}`. The producer leg is worth naming separately: `producer` is checked by 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. Re-closed by hand against 93ea19bca." }, "enabled": { "status": "live", - "verifiedAt": "2026-08-02", - "evidence": "packages/runtime/src/app-plugin.ts:820", - "note": "`enabled: false` skips scheduling entirely at registration — genuinely enforced, unlike the retired flow.active/tool.active." + "verifiedAt": "2026-08-28", + "evidence": "packages/runtime/src/app-plugin.ts#start (`if (job.enabled === false) { … continue; }` — the job is skipped at registration, before its handler is even resolved, so nothing is scheduled to no-op later)", + "note": "`enabled: false` skips scheduling entirely at registration — genuinely enforced, unlike the retired flow.active/tool.active. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `app-plugin.ts:820` had rotted onto a bare `}`. Same `start` residual as `name`: the read is inline in the `kernel:ready` hook and there is no narrower named symbol to anchor to. Re-closed by hand against 93ea19bca." } } } diff --git a/packages/spec/liveness/query.json b/packages/spec/liveness/query.json index fcff03ea75..c7e0735da6 100644 --- a/packages/spec/liveness/query.json +++ b/packages/spec/liveness/query.json @@ -1,53 +1,60 @@ { "type": "query", - "_note": "QuerySchema (BaseQuerySchema + expand) — packages/spec/src/data/query.zod.ts. NOT a registered metadata type: QueryAST is the REQUEST surface (the client SDK QueryBuilder's output; the POST /data/:object/query body), governed via the gate's SPEC_ONLY_SCHEMAS override like webhook (#4286). It is authorable by every API caller yet never stored as stack metadata, which is why no other gate reached it: this ledger read the metadata-type registry, check:authorable-surface walks the same registry, and check:exported-any is a type-resolution gate. Seeded 2026-07-31 from the #4286 sweep; closed out same day — joins/windowFunctions/cursor/distinct REMOVED (tombstoned; protocol-17 semantic migrations; the cursor/distinct SDK producers deleted with their keys), having ENFORCED engine-side, search sub-flags + aggregations.filter marked experimental at the schema. Re-opened 2026-08-09 for ONE key the #4286 sweep did not reach: `aggregations[].distinct` (#6815) — dispositioned REMOVED. #4286 swept the query's own members plus `aggregations.filter`; the per-aggregation `distinct` was neither a top-level key nor an unread one, so it passed both filters of that sweep while diverging by BACKEND rather than by declaration. The main executor evidence below: objectql engine find/aggregate and the drivers' find paths.", + "_note": "QuerySchema (BaseQuerySchema + expand) — packages/spec/src/data/query.zod.ts. NOT a registered metadata type: QueryAST is the REQUEST surface (the client SDK QueryBuilder's output; the POST /data/:object/query body), governed via the gate's SPEC_ONLY_SCHEMAS override like webhook (#4286). It is authorable by every API caller yet never stored as stack metadata, which is why no other gate reached it: this ledger read the metadata-type registry, check:authorable-surface walks the same registry, and check:exported-any is a type-resolution gate. Seeded 2026-07-31 from the #4286 sweep; closed out same day — joins/windowFunctions/cursor/distinct REMOVED (tombstoned; protocol-17 semantic migrations; the cursor/distinct SDK producers deleted with their keys), having ENFORCED engine-side, search sub-flags + aggregations.filter marked experimental at the schema. Re-opened 2026-08-09 for ONE key the #4286 sweep did not reach: `aggregations[].distinct` (#6815) — dispositioned REMOVED. #4286 swept the query's own members plus `aggregations.filter`; the per-aggregation `distinct` was neither a top-level key nor an unread one, so it passed both filters of that sweep while diverging by BACKEND rather than by declaration. The main executor evidence below: objectql engine find/aggregate and the drivers' find paths. 2026-08-28 (#13003): every citation in this file was re-anchored, and this file carried a second defect class besides drift — most of its `live` entries cited a path with NO line at all (unfalsifiable by the line bound by construction), and `expand`'s pointer was written `engine.ts:2519+`, whose trailing `+` stops the token matching PATH_RE entirely, so it degraded to PROSE and was never resolved, bounded or key-checked by anything. Both are now repo-rooted anchors.", "props": { "object": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/engine.ts (find/aggregate resolve the target object from ast.object); packages/metadata-protocol/src/protocol.ts:780 (QUERY_AST_KEYS reserves it at the REST boundary so a POST body's `object` is never read as a field filter)" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/engine.ts#fillQueryAstDefaults (`const ast: QueryAST = { ...this.fillQueryAstDefaults(findQuery), object }` — the resolved name is spread LAST so a stray body `object` cannot split `ast.object` from the table actually read); packages/metadata-protocol/src/protocol.ts#QUERY_AST_KEYS (reserves it at the REST boundary so a POST body's `object` is never read as a field filter, and a body/route mismatch is refused rather than resolved by picking a winner)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `protocol.ts:780` had rotted onto `if (seen.has(key)) return;` in an unrelated walker; `QUERY_AST_KEYS` is at ~:2426 today, ~1,650 lines away, in a 20,255-line file. The engine leg was PATH-ONLY (`engine.ts` with no line), which the line bound cannot falsify at all — it is now the narrowest symbol that assembles the AST. Re-closed by hand against 93ea19bca." }, "fields": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1353-1354 (select projection); driver-memory projectFields; objectql formula projection + known-field filters (closed end-to-end in the #4196 verification)" + "verifiedAt": "2026-08-28", + "evidence": "packages/drivers/driver-sql/src/sql-driver.ts#findRows (`builder.select(query.fields.map(...))`, else `select('*')`, with the unknown-column ladder that retries all-columns rather than answering an empty page); packages/objectql/src/engine.ts#planFormulaProjection (rewrites `ast.fields` to drop virtual formula fields before the driver sees them, and adds back the stored fields they read)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sql-driver.ts:1353-1354` had rotted onto `) as Error & { code?: string; status?: number };` inside an error-constructor helper, ~4,200 lines above the projection. sql-driver.ts is 16,289 lines; the whole find path moved into `findRows` under a citation nothing re-read. The `driver-memory projectFields` / `objectql formula projection` halves were prose with no path at all and are now a repo-rooted anchor on the formula side. Re-closed by hand against 93ea19bca." }, "where": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1330-1331 (applyFilters); every driver's find() applies it" + "verifiedAt": "2026-08-28", + "evidence": "packages/drivers/driver-sql/src/sql-driver.ts#findRows (`if (query.where) this.applyFilters(b, query.where)` — re-applied on every rung of the recovery ladder, which is why the ladder can drop a projection or a sort but never the predicate); packages/drivers/driver-sql/src/sql-driver.ts#applyFilters (the compiler); packages/plugins/plugin-security/src/predicate-guard.ts#collectQueryFields (`collectConditionFields(ast.where, out)` — the FLS guard walks it so a predicate cannot read a field the caller may not)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sql-driver.ts:1330-1331` had rotted onto a docblock about `UNSUPPORTED_QUERY_PARAM` being a 400 on the client-mistake list. The parenthetical named `applyFilters`, which is real and ~11,250 lines from the cited position — the largest name-vs-position gap measured in this batch. Re-closed by hand against 93ea19bca." }, "search": { "status": "live", - "verifiedAt": "2026-07-31", - "note": "Union since #3899's search-contract repair: the canonical bare query STRING (ADR-0061 D1 — what every surface sends and the dogfood HTTP proof pins) | the structured FullTextSearch form. A LEAF here, no longer a container: the walker does not descend into union members, so the object form's six engine-affordance flags (fuzzy/operator/boost/minScore/language/highlight) are audited where they are declared — FullTextSearchSchema's own `[EXPERIMENTAL — not enforced]` describe markers (#4286 step 1). The previously-listed children `query`/`fields` remain live at the same executor site.", - "evidence": "packages/objectql/src/engine.ts:2885-2911 (ADR-0061: string or object expanded into a server-resolved cross-field $or via expandSearchToFilter; object-form `fields` scopes the expansion); proof showcase-search.dogfood.test.ts" + "verifiedAt": "2026-08-28", + "note": "Union since #3899's search-contract repair: the canonical bare query STRING (ADR-0061 D1 — what every surface sends and the dogfood HTTP proof pins) | the structured FullTextSearch form. A LEAF here, no longer a container: the walker does not descend into union members, so the object form's six engine-affordance flags (fuzzy/operator/boost/minScore/language/highlight) are audited where they are declared — FullTextSearchSchema's own `[EXPERIMENTAL — not enforced]` describe markers (#4286 step 1). The previously-listed children `query`/`fields` remain live at the same executor site. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `engine.ts:2885-2911` had rotted onto the comment `// this row sees. Never a copy.`, ~5,650 lines above `expandSearchOnAst`. engine.ts is 13,593 lines and the cited range sat in the low 2,000s; a range citation into a file that size ages out of usefulness long before it ages out of RANGE, which is why nothing flagged it. Re-closed by hand against 93ea19bca.", + "evidence": "packages/objectql/src/engine.ts#expandSearchOnAst (ADR-0061: the string or object form is expanded on the AST before the driver sees it); packages/objectql/src/search-filter.ts#expandSearchToFilter (builds the server-resolved cross-field `$or`; the object form's `fields` scopes the expansion); proof showcase-search.dogfood.test.ts" }, "searchFields": { "status": "live", - "verifiedAt": "2026-07-31", - "note": "Formally declared by #3899 (ADR-0061 P1, 'formalize $searchFields'): the per-query narrowing override the executor has served since the ADR landed — server-intersected with the object's allowed searchable set, can only narrow, never widen. The schema-side declaration caught up with the executor when #3899's entry validation started rejecting bodies the schema did not describe.", - "evidence": "packages/objectql/src/engine.ts:2893-2898 (requestedFields hand-off) → packages/objectql/src/search-filter.ts resolveSearchFields (intersection with the allowed set); proof showcase-search.dogfood.test.ts + search-conformance.ledger.ts row search-fields-override" + "verifiedAt": "2026-08-28", + "note": "Formally declared by #3899 (ADR-0061 P1, 'formalize $searchFields'): the per-query narrowing override the executor has served since the ADR landed — server-intersected with the object's allowed searchable set, can only narrow, never widen. The schema-side declaration caught up with the executor when #3899's entry validation started rejecting bodies the schema did not describe. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `engine.ts:2893-2898` had rotted onto `const row = rows[index];`, and the `search-filter.ts` leg was PATH-ONLY. Both halves of a two-hop claim (`requestedFields hand-off` → `resolveSearchFields`) were unfalsifiable in different ways: one bounded to a line that had stopped meaning anything, the other bounded to nothing. Re-closed by hand against 93ea19bca.", + "evidence": "packages/objectql/src/engine.ts#expandSearchOnAst (the hand-off — the requested fields ride into the expansion); packages/objectql/src/search-filter.ts#resolveSearchFields (the intersection with the object's allowed searchable set: narrows only, never widens); proof showcase-search.dogfood.test.ts + search-conformance.ledger.ts row search-fields-override" }, "orderBy": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1335-1342; every driver's find() sorts by it" + "verifiedAt": "2026-08-28", + "evidence": "packages/drivers/driver-sql/src/sql-driver.ts#orderKeysFor (the whole ORDER BY decided once — the caller's keys plus what a deterministic paged read requires, #4363); packages/drivers/driver-sql/src/sql-driver.ts#findRows (`b.orderBy(this.remoteColumn(...), key.direction)`, droppable by the unknown-column ladder so an unsortable column returns rows unordered rather than none); packages/plugins/plugin-security/src/predicate-guard.ts#collectQueryFields (the FLS guard walks each sort key's field)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sql-driver.ts:1335-1342` had rotted onto a docblock sentence about a throw site's job, and `every driver's find() sorts by it` was prose naming no path. Re-closed by hand against 93ea19bca." }, "limit": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1345" + "verifiedAt": "2026-08-28", + "evidence": "packages/drivers/driver-sql/src/sql-driver.ts#findRows (`if (query.limit !== undefined) b.limit(query.limit)` — inside `buildBase`, so every rung of the recovery ladder re-applies it)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sql-driver.ts:1345` had rotted onto a bare `*/` closing a docblock. Re-closed by hand against 93ea19bca." }, "offset": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/drivers/driver-sql/src/sql-driver.ts:1344" + "verifiedAt": "2026-08-28", + "evidence": "packages/drivers/driver-sql/src/sql-driver.ts#findRows (`if (query.offset !== undefined) b.offset(query.offset)` — applied beside `limit` in the same pagination block)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `sql-driver.ts:1344` had rotted onto the docblock line `* log.`, one line above `limit`'s rotted `*/`. The pair moved together and landed together, which is what makes a whole-file drift read as coherent right up to the moment someone opens the file. Re-closed by hand against 93ea19bca." }, "top": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/engine.ts:2850-2853 (top → limit normalization, OData compatibility); packages/metadata-protocol/src/protocol.ts:3484-3486 (same fold on the REST list path)" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/engine.ts#ENGINE_QUERY_SLOTS (the engine's slot set — the `where` and `limit` canonicals, of which `top` is the OData alias); packages/objectql/src/engine.ts#foldEngineOptionAliases (applied on `find`/`findOne` before anything reads the AST, so the driver only ever sees `limit`); packages/metadata-protocol/src/protocol.ts#WIRE_DOLLAR_ALIASES (`['$top', 'top']` — the wire spelling folded on the REST list path)", + "note": "2026-08-28: RE-ANCHORED (#13003), REPOINTED, and the MECHANISM had moved too — this is the one entry in the batch where re-closing the call graph changed the story rather than just the coordinates. `engine.ts:2850-2853` claimed an inline `top → limit` normalization; both citations had rotted onto docblocks (`:2853` in engine.ts, `:3486` in protocol.ts), and the inline fold no longer exists: the alias now lives in the spec's ONE alias table (`RPC_QUERY_ALIAS_SLOTS`), filtered into `ENGINE_QUERY_SLOTS` and applied by `foldEngineOptionAliases` at every engine entry point (#4346/#4371). The verdict `live` was never in doubt; the pointer had stopped describing how. Re-closed by hand against 93ea19bca." }, "cursor": { "status": "dead", @@ -64,24 +71,27 @@ "children": { "filter": { "status": "live", - "verifiedAt": "2026-08-21", - "evidence": "packages/objectql/src/in-memory-aggregation.ts aggregateBucket narrows the source rows per aggregation via matchesAggregationFilter (having-filter.ts); packages/objectql/src/engine.ts aggregate() forces the in-memory lowering when any aggregation carries a non-empty filter; the four native driver faces refuse the key NOT_IMPLEMENTED/501 when reached directly (driver-sql sql-driver.ts, driver-turso remote-transport.ts, driver-mongodb mongodb-aggregation.ts, driver-memory memory-driver.ts performAggregation); the FLS predicate guard walks it (plugin-security predicate-guard.ts:89); pinned by packages/objectql/src/engine-aggregate-filter.test.ts", - "note": "ENFORCED 2026-08-21 (#10576, the contract half of #10413's ruling — maintainer 「其他接受」 accepting option A 「给引擎聚合契约加逐聚合过滤,一次修对所有驱动」). SQL FILTER (WHERE …) semantics: the predicate narrows the SOURCE rows the one aggregation reads while siblings keep the full group. Engine-owned via the in-memory lowering (the dateGranularity/having correct-first two-tier shape — a driver that grows native conditional aggregation must advertise a capability flag); a driver face reached directly refuses rather than silently aggregating the unfiltered rows, which was #10413's measured defect on the ObjectQL analytics path." + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/in-memory-aggregation.ts#aggregateBucket (`const aggFilter = agg.filter` — a non-vacuous predicate narrows the rows THIS aggregation reads while its siblings keep the full bucket); packages/objectql/src/having-filter.ts#matchesAggregationFilter (the per-row evaluation); packages/objectql/src/engine.ts#aggregate (forces the in-memory lowering when any aggregation carries a non-empty filter); packages/drivers/driver-sql/src/sql-driver.ts#unsupportedAggregationFilterError, packages/drivers/driver-turso/src/remote-transport.ts#refusePerAggregationFilter, packages/drivers/driver-mongodb/src/mongodb-aggregation.ts#refusePerAggregationFilter and packages/drivers/driver-memory/src/memory-driver.ts#performAggregation (the four native faces REFUSE the key NOT_IMPLEMENTED/501 when reached directly, rather than silently aggregating the unfiltered rows); packages/plugins/plugin-security/src/predicate-guard.ts#collectQueryFields (the FLS guard walks the predicate's fields); pinned by packages/objectql/src/engine-aggregate-filter.test.ts", + "note": "ENFORCED 2026-08-21 (#10576, the contract half of #10413's ruling — maintainer 「其他接受」 accepting option A 「给引擎聚合契约加逐聚合过滤,一次修对所有驱动」). SQL FILTER (WHERE …) semantics: the predicate narrows the SOURCE rows the one aggregation reads while siblings keep the full group. Engine-owned via the in-memory lowering (the dateGranularity/having correct-first two-tier shape — a driver that grows native conditional aggregation must advertise a capability flag); a driver face reached directly refuses rather than silently aggregating the unfiltered rows, which was #10413's measured defect on the ObjectQL analytics path. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — nothing here had ROTTED (this entry was re-verified 2026-08-21, the youngest in the file), but nothing here was CHECKABLE either: the three repo-rooted legs were path-only, and the four driver faces plus the FLS guard were written as bare package+filename prose (`driver-sql sql-driver.ts`, `plugin-security predicate-guard.ts:89`), which PATH_RE cannot parse — so the ledger contributed no resolvable pointer for five of this entry's eight consumers and the gate was never asked about them. That is the webhook.json class, on the file's best-verified entry. All eight are now repo-rooted anchors. Re-closed by hand against 93ea19bca." }, "function": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/engine.ts aggregate() forwards aggregations to the driver or the fallback; packages/objectql/src/in-memory-aggregation.ts:167 applies each function" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/in-memory-aggregation.ts#aggregateBucket (`const fn = agg.function` then the count / count_distinct / sum / avg / min / max arms, each with the platform's ruled empty-group value); packages/objectql/src/engine.ts#aggregate (forwards the aggregations to the driver or to the in-memory fallback)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `in-memory-aggregation.ts:167` had rotted onto `return v ?? null;` inside `projectGroupValue`, a DIFFERENT function three lines above `aggregateBucket`, and the engine leg was path-only. Three lines is the smallest miss in this batch and it is still a miss: the function it landed in projects a GROUP value, not an aggregate. Re-closed by hand against 93ea19bca." }, "field": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/in-memory-aggregation.ts:204-206 (collectValues reads rows[field]); SQL builders render FUNC(field)" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/in-memory-aggregation.ts#aggregateBucket (`const field = agg.field` → `collectValues(rows, field)`, with the `'*'` count-all sentinel handled before it); packages/objectql/src/in-memory-aggregation.ts#collectValues (the per-row read); packages/plugins/plugin-security/src/predicate-guard.ts#collectQueryFields (the FLS guard adds each aggregation's field, skipping `'*'`)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `in-memory-aggregation.ts:204-206` had rotted onto `if (!field) {`, the fieldless guard rather than the read; `SQL builders render FUNC(field)` was prose naming no path. The near-miss here is instructive in the other direction from `function`'s: the cited line genuinely mentions `field`, in the right function, and still describes the branch taken when the key is ABSENT. Re-closed by hand against 93ea19bca." }, "alias": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "result column naming in both the SQL builders and packages/objectql/src/in-memory-aggregation.ts" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/in-memory-aggregation.ts#aggregateBucket (`const alias = agg.alias` — every arm writes its result to `out[alias]`, so the alias IS the emitted column name); packages/objectql/src/in-memory-aggregation.ts#applyInMemoryAggregation (merges those aliased values onto each bucket's group key)", + "note": "2026-08-28: RE-ANCHORED (#13003) — the citation was PATH-ONLY (`result column naming in both the SQL builders and packages/…/in-memory-aggregation.ts`), so no line bound could ever apply to it and the only check it faced was that the file exists. Now anchored to the two symbols that name the column. The `SQL builders` half stays prose deliberately: it names no path today and inventing one would be a guess. Re-closed by hand against 93ea19bca." }, "distinct": { "status": "dead", @@ -92,14 +102,15 @@ }, "groupBy": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/engine.ts aggregate() forwards groupBy; packages/objectql/src/in-memory-aggregation.ts buckets by it (including dateGranularity fallback)" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/in-memory-aggregation.ts#applyInMemoryAggregation (`const groupBy = (ast.groupBy ?? [])` then the bucketing loop — one bucket per type-preserving key tuple); packages/objectql/src/in-memory-aggregation.ts#projectGroupValue (each group key's value, including the dateGranularity lowering); packages/objectql/src/engine.ts#aggregate (forwards it to the driver or the fallback); packages/plugins/plugin-security/src/predicate-guard.ts#collectQueryFields (the FLS guard walks the string and object group forms alike)", + "note": "2026-08-28: RE-ANCHORED (#13003) — both citations were PATH-ONLY, so this entry has never faced any check but file existence. Re-closed by hand against 93ea19bca." }, "having": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/having-filter.ts (the evaluator); packages/objectql/src/engine.ts aggregate() applies it after BOTH the native-driver path and the in-memory fallback; packages/metadata-protocol/src/protocol.ts findData aggregate branch forwards it; packages/spec/src/data/data-engine.zod.ts EngineAggregateOptionsSchema declares it", - "note": "ENFORCED 2026-07-31 (#4286 step 3, resolved to enforce): engine-owned, applied AFTER aggregation over the aggregated row's namespace (aggregation aliases + groupBy projections); an unknown operator rejects loudly instead of being ignored. Was #4286 finding 1 — aggregate() rebuilt the driver AST without it, so even a HAVING-implementing driver could never receive it. Native SQL pushdown can come later behind a driver capability flag (the dateGranularity two-tier pattern) without changing semantics. The FLS predicate guard walks it (predicate-guard.ts:64)." + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/having-filter.ts#applyHaving (the evaluator entry point) and packages/objectql/src/having-filter.ts#matchesHaving (the per-row predicate over the AGGREGATED row's namespace — aliases plus groupBy projections; an unknown operator throws rather than being ignored); packages/objectql/src/engine.ts#aggregate (applies it after BOTH the native-driver path and the in-memory fallback); packages/metadata-protocol/src/protocol.ts#findData (the aggregate branch forwards it, and strips it off the find-path bag because that path cannot serve it); packages/spec/src/data/data-engine.zod.ts#EngineAggregateOptionsSchema (the declaration leg — this one is the option's DECLARATION, not a consumer, and is cited as such); packages/plugins/plugin-security/src/predicate-guard.ts#collectQueryFields (`collectConditionFields(ast.having, out)` — the FLS guard walks it beside `where`)", + "note": "ENFORCED 2026-07-31 (#4286 step 3, resolved to enforce): engine-owned, applied AFTER aggregation over the aggregated row's namespace (aggregation aliases + groupBy projections); an unknown operator rejects loudly instead of being ignored. Was #4286 finding 1 — aggregate() rebuilt the driver AST without it, so even a HAVING-implementing driver could never receive it. Native SQL pushdown can come later behind a driver capability flag (the dateGranularity two-tier pattern) without changing semantics. The FLS predicate guard walks it. 2026-08-28: RE-ANCHORED (#13003) — all four citations were PATH-ONLY. This is the entry where that mattered most: `having`'s whole history is a key that was declared, forwarded and then dropped one layer before it could act, and four unbounded file pointers are exactly the evidence shape that could not have caught it. Re-closed by hand against 93ea19bca." }, "windowFunctions": { "status": "dead", @@ -113,8 +124,9 @@ }, "expand": { "status": "live", - "verifiedAt": "2026-07-31", - "evidence": "packages/objectql/src/engine.ts:2519+ (post-process: batch $in resolution of lookup/master_detail expands, default max depth 3)" + "verifiedAt": "2026-08-28", + "evidence": "packages/objectql/src/engine.ts#expandRelatedRecords (the post-process: batch `$in` resolution of lookup / master_detail expands, default max depth 3 — no driver reads `ast.expand`, the engine expands after the fetch)", + "note": "2026-08-28: RE-ANCHORED (#13003) and REPOINTED, and this pointer was WORSE than rotted — it was written `packages/objectql/src/engine.ts:2519+`, and the trailing `+` stops the token matching PATH_RE, so the scanner classified the whole thing as PROSE. The path was never resolved, the line never bounded, the key-mention never asked: `expand` contributed ZERO checkable pointers while looking like the most precise citation in the file. That is the webhook.json defect (batch 2) reproduced by a single character rather than by a missing prefix, and it argues the same way — a citation grammar that degrades silently to prose needs the anchor to be the load-bearing half. The consumer itself had also moved ~5,520 lines, to `expandRelatedRecords`. Re-closed by hand against 93ea19bca." } } } diff --git a/packages/spec/scripts/liveness/check-liveness.test.ts b/packages/spec/scripts/liveness/check-liveness.test.ts index 14ed1cafb3..e25fdd7450 100644 --- a/packages/spec/scripts/liveness/check-liveness.test.ts +++ b/packages/spec/scripts/liveness/check-liveness.test.ts @@ -186,12 +186,29 @@ describe('check:liveness — evidence pointers (#5623)', () => { it('prints the citation count and how many are in range, equal on a green run', () => { // The #5623 lesson applied to the new counter: printing only "in range" // would read as a pass on a run where the parser extracted no citations. + // Hence two numbers, and a floor under the first — see the floor's own note + // at the assertion below for why that floor is `> 0` and when to delete it. const { status, output } = runGate(path.join(tmp, 'liveness')); expect(status, output).toBe(0); const line = output.split('\n').find((l) => l.startsWith('line citations:')) ?? ''; const m = /line citations: (\d+) pointer\(s\) written .*?, (\d+) inside the cited file/.exec(line); expect(m, line).not.toBeNull(); - expect(Number(m![1])).toBeGreaterThan(100); + // Non-vacuity. The failure mode this guards is the parser silently + // extracting NOTHING (#5623), and that failure mode is exactly zero — so + // zero is exactly what the floor tests. It was 100 until #13003, which is + // the migration adopting #12516's `path#symbol` grammar and therefore + // RETIRING line citations by design (300 at that card's filing, 175 after + // batch 2, 82 after batch 3). Any floor above zero reds on legitimate + // drainage and re-opens the same escalation one batch later; `> 0` never + // lies during the migration and still catches extracts-nothing at full + // strength. Ruled 2026-08-28 on #13003, comment 5458356183. + // + // ⛔ WHEN THIS POPULATION LEGITIMATELY REACHES ZERO — the last line citation + // retired — DELETE this assertion AND this comment IN THE SAME PR that + // retires it, along with the equality check below, which then compares two + // zeroes and asserts nothing. A conscious decision at zero, never a silent + // one; that is the whole reason the floor sits at `> 0` rather than gone. + expect(Number(m![1])).toBeGreaterThan(0); expect(m![2]).toBe(m![1]); expect(line).not.toContain('PAST EOF'); }); diff --git a/packages/spec/scripts/liveness/evidence.test.ts b/packages/spec/scripts/liveness/evidence.test.ts index ef9d37f7c7..874776e31c 100644 --- a/packages/spec/scripts/liveness/evidence.test.ts +++ b/packages/spec/scripts/liveness/evidence.test.ts @@ -369,8 +369,27 @@ describe('shipped ledgers', () => { } expect(outOfRange).toEqual([]); // Same non-vacuity guard as above, one level down: a parser that stopped - // retaining lines would satisfy the assertion above by extracting nothing. - expect(citations).toBeGreaterThan(100); + // retaining lines would satisfy the assertion above by extracting nothing + // (the #5623 lesson — "all in range" over zero citations is what a degraded + // parser prints too). The GUARDED FAILURE MODE IS EXACTLY ZERO, so zero is + // exactly what the floor tests, and the sibling floor above keeps its own + // number because the `local` PATH population it guards is not draining. + // + // Why not a bigger number here, when this one used to be 100 (#13003): the + // symbol-anchor migration (#12516's grammar, adopted batch by batch under + // #13003) RETIRES line citations by design — 300 at that card's filing, 175 + // after batch 2, 82 after batch 3 — so any floor above zero reds on + // legitimate drainage and re-opens the same escalation one batch later. + // Ruled 2026-08-28 on #13003 (comment 5458356183): lower to `> 0`, the only + // floor that never lies during the migration while still catching + // extracts-nothing at full strength. + // + // ⛔ WHEN THIS POPULATION LEGITIMATELY REACHES ZERO — the last line citation + // retired — DELETE this assertion AND this comment IN THE SAME PR that + // retires it, together with the `outOfRange` assertion above, which has + // nothing left to check. That is the conscious decision at zero the `> 0` + // floor exists to force. Never let it pass silently on an empty population. + expect(citations).toBeGreaterThan(0); }); it('every local `path#symbol` anchor names a symbol its file contains (#12516)', () => {