Skip to content

Commit 64de4c5

Browse files
os-steveclaude
andauthored
fix(check-adr-symbol-anchors): the verdict may not claim a census wider than the vocabulary it read (#18925)
Fixes #18899 Clause-②: no `check-adr-symbol-anchors` printed `0 line anchors survive.` and that sentence was false. This change deletes the surviving pointers inside the declared file surface — every one of them sampled stale before it was deleted — and makes the verdict stop claiming a census it does not take. ## Caliber first, then the count ⛔ No count is inherited here, mine included. A **line pointer** is defined as a token in an authored document that designates a position in a source file by LINE NUMBER. The class was then enumerated by SPELLING — six of them — and each regex was lit on a literal it must see before any corpus was read; the two path-less spellings additionally got a negative control on the neighbours they must refuse (an HTTP status, a URL port, a clock time, a lane label, an identifier ending in a capital L before digits). Fenced blocks are skipped, exactly as the extractor skips them, so quoted material is not counted as authored. `docs/adr/**` at the branch point, 139 records: | spelling | hits | records | this gate reads it? | |:---|---:|---:|:---| | A path-anchored (a path, a colon, digits) | 6 | 3 | yes — and all 6 carry `anchor-exempt` markers, which is why the gate was green | | B backticked bare continuation | 0 | 0 | yes | | C tilde-backticked number | 0 | 0 | yes | | D path-less bare colon continuation | 5 | 2 | ⛔ no | | E the `L` pin as a markdown link-target fragment | 2 | 1 | ⛔ no | | F the `L` pin bare in prose (E is a subset of F) | 12 | 4 | ⛔ no | Two of D's five are ADR-0047's dev-server ports, which are not line pointers at all, so the caliber's verdict is: **15 real line pointers across 5 records were invisible to this gate** — not 5, and not 2. ### The card's 「未復現」 section, reproduced The handed-over report claimed **12** fragments across ADR-0020×6, ADR-0021×2, ADR-0034×2, ADR-0077×1, and the filing seat could neither reproduce the 12 nor make the parts sum to it. At spelling **F** the measurement is 12 across exactly those four records, distributed 0020×6 · 0021×2 · **0034×3** · 0077×1. So the **total was right**, the **0034 subtotal was one short**, and the class it counted was the `L` pin rather than the narrower `#L` fragment the filing seat measured at 2. ⛔ Neither reading was wrong; they were two calibers. Both are reproduced above, from one instrument, with the light on. ## The reading that decided deletion, not correction #13556 ruled this class deleted with no transition period, on a 4/4 sample of pointers landing on unrelated code. The card states plainly that it did not take that sample. Taken here, on every pointer the caliber found — **15/15 stale**: | pointer | claims to point at | line N is, today | |:---|:---|:---| | `security-plugin.ts` 626 (ADR-0096 ×3) | the `getReadFilter` read-filter fall-open | a JSDoc closing line inside the `SecurityReportSink` logging-sink interface | | `record-validator.ts` 198 (ADR-0020 ×2) | `validateRecord` | a comment inside the `ValidationError` constructor; `validateRecord` is declared at 1185 — 987 lines away | | `object.zod.ts` 354 / 371 / 410 / 765 (ADR-0020 ×4) | "managed-by buckets and toolbar lifecycle actions" | a closed-batch comment, a `where`-guidance comment, `const nearMiss =`, an author-time-rejection comment. The file's `lifecycle` block now sits at 818-1010 as an ADR-0057 retention/ttl/storage/archive policy | | `engine.ts` 806 + 961 (ADR-0021 ×2) | the two `metadataArrayKeys` lists | a blank docblank line and a comment about answering one mistake two ways. `metadataArrayKeys` is discussed at 2327, where a comment says it USED to be two lists | | `engine.ts` 1811, `sql-driver.ts` 561-563, `sql-driver.ts` 548-600 (ADR-0034 ×3) | a deadlock warning, a savepoint, the sequences table | a docblock tail, a datetime-canonicalisation docblock, a presentation-convergence comment. `SEQUENCES_TABLE` is at 181 | | `engine.ts` 3217 (ADR-0077 ×1) | the sandbox-runner ALS limitation | a comment about callers probing a private guard. The `setImmediate` discussion is at 4572 / 15700 / 15866 | The structural clue the card asked to be verified **holds**: ADR-0096's inherited path does sit in the same table row as the bare number, in a different cell. That is why the path-anchored pass cannot see it — there is no path adjacent to the number to anchor on. ## What this change does, and what it deliberately does not Three deletion commits, one per distinct stale target, each carrying its own sample in the message; then one commit on the gate. **ADR-0096** — deleting a path-less number leaves the sentence with no referent, so two of the three take the symbol anchor the record's own table row already uses for that site, and the third points at the row above it. Both substitutions resolve: the gate's anchor count rises 2093 to 2095 and 2 of those are new CHECKED pointers rather than merely shorter prose. **ADR-0020** — the path half of the links is correct and survives, so these are pure fragment deletions dropping to the file-level target the gate's own remedy text names as its second exit. ⛔ The prose is not repaired: whether ADR-0020's parenthetical still describes `object.zod.ts` is a separate defect and takes its own card, exactly as this gate's header instructs. **The gate** — `summaryLine()` now names the vocabulary it judged and states that the two path-less spellings are opt-in and off here. The self-test holds the claim in three parts, and the middle one is the control that makes the battery worth running: the corpus DECLARES the narrower vocabulary; the shared grammar really does see the wider one on the same text when asked; and the sentence names both halves and can no longer end in the unqualified form. Battery floor 17 to 22. ### ⛔ Route (c), widening the vocabulary, is MEASURED and NOT taken `pathlessLineCitations: true` on this corpus yields **17 findings across 6 records**. Two of them are ADR-0047's dev-server ports: correct prose no author can repair, whose only remedy is the maintainer-only exemption marker. Four of the six records are outside this card's declared file surface. The alternative route is worse: `unspannedAnchors: true` yields **8 findings, 6 of them fabrications** — correct relative ADR-to-ADR markdown links reported as missing FILES because a heading fragment reads as a symbol. So the price tag is real and the decision is not this sentence's to make. What the sentence owed was to stop reading as though the decision had already gone the other way. ⛔ It also cannot be taken inside this fence at all: the vocabulary lives in `scripts/symbol-anchors.mjs`, shared by five corpora, and that file is outside the declared surface. **This change does lower the bill**: the 17 becomes 8 once these nine pointers are gone. ## Evidence **Gates** — `dispatch-gates --repo objectstack-ai/objectstack --commands` derived **37** families from the real three-path change set; all 37 run, all exit 0, reconciled with `--ran` carrying exit codes: *"37 derived families accounted for — 37 run, 0 NOT-MEASURED (a DERIVED zero — all 37 recorded an exit code and none of them is 3)"*. One needed a second pass: `@objectstack/lint check:doc-formula-expressions` first exited **3 — PREREQUISITE NOT MET**, which its own text declares is not a finding; after `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` it exits 0. **Ablation, 4 legs, all on the final tree at `c38eead`.** Every leg proved its mutation reached disk (anchor count dropped, blob hash moved) before the run was read, and every restore was verified by blob-equals-HEAD plus an empty `git diff HEAD` — not by an exit code: | leg | mutation | the case that fired | |:---|:---|:---| | a | corpus declaration flipped to `pathlessLineCitations: true` | *the narrower vocabulary must be this corpus's DECLARATION* | | b | the probe literal stripped of both spellings | *the shared grammar must SEE both path-less spellings when asked … got 0* | | c1 | the unqualified sentence restored wholesale | *the verdict must name BOTH the vocabulary it judged and the one it did not* | | c2 | both halves named, the unqualified claim re-appended | *the verdict must not end in the unqualified claim* | Leg **b** is the one that matters: without it, legs a and c would hold just as well on a day when the wider grammar had stopped matching anything, and the new caveat would be documenting a dead rule. **Lint, a declared narrowing rather than the farm run.** Three readings, all at `c38eead`: ① the examined population comes from eslint's own config, not a guess — `ESLint#isPathIgnored` answers `true` for both `docs/adr/**` records (they are outside eslint's population entirely) and `false` for the script; ② `--format json` reports **1 file linted, 0 errors, 0 warnings**; ③ invariance — `eslint.config.mjs` states in its own words that this repo *"runs one `eslint.config.mjs`, which never enables type-aware linting (no `parserOptions.project`, no typed `@typescript-eslint` rules) for ANY file"*, and a grep for that configuration finds only the comment saying it is absent, so no verdict on an untouched file can move as a function of this diff. The repo-wide `eslint . --no-inline-config` remains CI's run. **Control bytes** — `check:nul-bytes` green, plus a direct scan of the touched files for the non-NUL control bytes it does not cover: no matches. ## Changeset: `skip-changeset`, measured Nothing published moves. The root package is `private: true`, and **0 of the 70 public packages** has a `files[]` entry that escapes its own directory or names `docs/adr`. Measured at the byte level against the built output with a matched positive control: a grep for the exact text this diff changed (`record-validator.ts` plus the fragment, the four-pin list, the two `:626` sentences, the old verdict phrase) across every built `dist/` exits **1**, while the same grep shape fires on 168 files for a symbol that is published. One near-miss worth naming: published `.d.ts` files DO cite ADR-0020 by URL, so the record's *filename* ships — its *text* does not. ## Acceptance notes ⛔ Out of the declared file surface, so measured and reported rather than touched: - **Six line pointers of this same class survive in three records this card's fence does not name** — ADR-0021 ×2, ADR-0034 ×3, ADR-0077 ×1. All six sampled stale above. Editing them would breach the fence and would widen the file surface the claim comment declared, so they are left for a follow-up card; the staleness sampling is already done above. Dedupe words: `L pin pathless ADR line pointer` · `0021 0034 0077 stale L citation` · `adr-anchors blind form remainder` · `pathlessLineCitations adr corpus`. - **ADR-0047's two dev-server ports are the standing obstacle to route (c)** — correct prose whose only remedy is a maintainer-only marker. Any widening card has to decide that first. Dedupe words: `dev-server port read as line citation` · `pathless colon false positive 0047`. - noted, not filed: whether ADR-0020's parenthetical ("managed-by buckets and toolbar lifecycle actions") still describes `object.zod.ts`, whose `lifecycle` block is now an ADR-0057 retention policy. The record's *claim* that `lifecycle` is overloaded there is still true, so this is prose drift in the specifics, not a false statement — and this gate's header forbids repairing prose to clear it. Carrier: the next author of ADR-0020's naming section; no PR is queued on it. - #18494 and #18493 are held out of this round at the seat and are not addressed here. #13556 · #14279 · #15809 · #18101 are near neighbours and none folds in. ## 维护者速读(草稿) **改了什么.** 两件事。一是删掉九个指向源码行号的旧指针——三个在 ADR-0096、六个在 ADR-0020,删前逐个抽样,九个全部指向无关代码(最离谱的一个偏了 987 行,落在一个错误类的 构造函数里)。二是把门禁那句结语从「0 line anchors survive.」改成说清它究竟量了哪几种 写法、哪两种没量。门禁逻辑一行没动,只动了它对外的说法,以及三条防止这句话退回去的自测。 **为什么改.** #13556 已裁决「ADR 行号锚」这一类无过渡期删除,依据是 4/4 抽样全烂。但门禁 的词汇表比那一类窄,于是两种写法活了下来:一种把行号藏在 markdown 链接的 fragment 里, 一种把行号与路径拆在同一表格行的不同单元格里。门禁读不到它们,却仍然印「0 … survive」。 **读数是真的,那句话不是**——这就是本卡判据 (b)。一句印在绿灯里的假零,下一个人会据它认定 这一类已经清完。 **风险与代价(含回滚).** 风险很低。① 不发布任何东西:根包是私有包,70 个公开包里没有一个 的 `files[]` 能伸到 `docs/adr` 或根 `scripts/`,已用字节级 grep 配正控实测,故声明 `skip-changeset`。② 本卡故意**没有**去扩宽门禁词汇表,因为实测代价摆在这里:打开那个开关 会在 6 份 ADR 上红 17 条,其中 2 条是 ADR-0047 的开发服务器端口号——那是完全正确的散文, 作者无法修,唯一出路是维护者专属的豁免标记;另有 4 份 ADR 在本卡申报的文件面之外。换一条 路更糟:`unspannedAnchors` 会红 8 条,其中 6 条是把正确的 ADR 互链误报成「文件不存在」。 ⇒ **扩词汇表是一个带价签的决定,不该由一句结语替您做。** 本卡把账单从 17 降到 8。 ③ 回滚成本近乎零:四个 commit 各自独立,`git revert` 任意一个都不影响其余,删 ADR 指针 那三个纯属文本删除。④ 唯一需要您过目的判断:ADR-0096 有两句删掉数字后会没有指代对象, 本卡让它们改用该 ADR **自己那张表里已有的**符号锚(`#getReadFilter`),没有新造事实; 这两处因此从「不可检查的数字」变成了「门禁会检查的锚点」。 **席位意见.** **你要做的.** ① 这是受管面(`docs/adr/**`),CI 绿也不由 AI 座位合并——需要您亲手确认与 合并。② 请重点看 ADR-0096 那两句的指代替换读起来对不对(是否仍然说的是同一个站点);其余 七处是纯删除,不改变任何论断。③ 如果您认为剩下那三份 ADR(0021 / 0034 / 0077)的六个指针 应当一并清掉,或者愿意为扩宽门禁词汇表付那 8 条红灯的账,请说一声——两件都已量好,另立卡 即可动手。 --- _Generated by [Claude Code](https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b84a847 commit 64de4c5

3 files changed

Lines changed: 83 additions & 13 deletions

File tree

‎docs/adr/0020-state-machine-converge-and-enforce.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ The design intent is a **runtime guardrail**: declare which `status` transitions
4242

4343
- `IWorkflowService` (`packages/spec/src/contracts/workflow-service.ts:58` <!-- anchor-exempt: HISTORICAL --> — unlinked: the contract file was deleted on 2026-08-01 by #4451 / #4473, which retired the `workflow` service slot outright, closing the follow-up this record left open below) has **no concrete implementation**.
4444
- There is **no XState interpreter** anywhere (no `createMachine` / `interpret` / transition engine).
45-
- The write-path validator [`validateRecord`](../../packages/objectql/src/validation/record-validator.ts#L198) reads only `objectSchema.fields` and validates **field data types** (string/number/date/…). It **never reads `objectSchema.validations`** at all — so *not one* of the nine validation-rule types (`state_machine`, `cross_field`, `script`, `unique`, `format`, `json_schema`, `async`, `custom`, `conditional`) is enforced by it.
45+
- The write-path validator [`validateRecord`](../../packages/objectql/src/validation/record-validator.ts) reads only `objectSchema.fields` and validates **field data types** (string/number/date/…). It **never reads `objectSchema.validations`** at all — so *not one* of the nine validation-rule types (`state_machine`, `cross_field`, `script`, `unique`, `format`, `json_schema`, `async`, `custom`, `conditional`) is enforced by it.
4646
- **Nothing reads `object.stateMachines`.**
4747

4848
So the guardrail goal is currently unmet at runtime. The only artefacts that exist are declarations — e.g. `examples/app-crm/src/workflows/stale-opportunity.workflow.ts:19` <!-- anchor-exempt: HISTORICAL --> (`StateMachineConfig`; unlinked — this file describes the pre-ADR state and was itself removed by this record's own implementation, see the checklist below), which additionally **mixes orchestration into the machine** (it carries `email_alert` / `task_creation` actions that no engine executes — that orchestration belongs to a record-triggered Flow per ADR-0019).
@@ -58,7 +58,7 @@ Future automation is **AI-generated, human-previewed** (ADR-0010 / ADR-0011). Th
5858
- The audience for the *name* is the **model**, not a non-technical admin. The right heuristic is **"meet the model where its priors are"**: use the term that is densest in training data for this concept.
5959
- "**state machine**" is that term — Rails `state_machine`, AWS Step Functions "State Machine", XState, Spring Statemachine. An AI given a field named `state_machine` with a `{ from: [to] }` transition table hits its priors and produces correct code. A coined term (e.g. `lifecycle`) forces the model off its priors onto local docs alone.
6060
- `state_machine` also reads as **maximally distinct from `flow`** — eliminating the `flow` / `workflow` near-synonym ambiguity that makes an AI pick the wrong type.
61-
- `lifecycle` is additionally **already overloaded** in this codebase (managed-by buckets and toolbar "lifecycle actions" in [`object.zod.ts`](../../packages/spec/src/data/object.zod.ts) at L354/L371/L410/L765), so reusing it would create a *new* ambiguity.
61+
- `lifecycle` is additionally **already overloaded** in this codebase (managed-by buckets and toolbar "lifecycle actions" in [`object.zod.ts`](../../packages/spec/src/data/object.zod.ts)), so reusing it would create a *new* ambiguity.
6262

6363
Corollary (a trap to avoid): if we name it `state_machine`, the **shape must also match the well-known shape**. A conventional name on a bespoke structure is the worst case — the model's priors fire on the name and mislead on the structure. Keep the shape textbook FSM.
6464

@@ -105,7 +105,7 @@ The surviving guardrail is named **`state_machine`** (rule type, already so name
105105

106106
Wire the `validations` union into the write path — today nothing evaluates it (see §prior-state plumbing gap). Concretely:
107107

108-
1. **Plumb the prior/merged record in.** Extend the rule-evaluation entry point (today [`validateRecord(schema, data, mode)`](../../packages/objectql/src/validation/record-validator.ts#L198)) to receive the prior record on update — e.g. `validateRecord(schema, data, mode, previous?)`, or run the rule pass from a `beforeUpdate` step that already holds both old and new. This unblocks `state_machine` **and** the currently-crippled `cross_field` / `script` rules in one move; do it union-wide, not `state_machine`-only.
108+
1. **Plumb the prior/merged record in.** Extend the rule-evaluation entry point (today [`validateRecord(schema, data, mode)`](../../packages/objectql/src/validation/record-validator.ts)) to receive the prior record on update — e.g. `validateRecord(schema, data, mode, previous?)`, or run the rule pass from a `beforeUpdate` step that already holds both old and new. This unblocks `state_machine` **and** the currently-crippled `cross_field` / `script` rules in one move; do it union-wide, not `state_machine`-only.
109109
2. **Transition check.** On update: if `old[field] !== new[field]` and `new[field] ∉ transitions[old[field]]`, **reject** with the rule's `message`. On insert: validate `new[field]` is the declared initial state — derived from the `Field.select` option marked `default: true` (no separate `initial` key needed; showcase relies on this).
110110
3. **Introspection endpoint (follow-on).** Expose `legalNext(object, field, currentState)` so UI/Agents read the legal set instead of re-deriving it.
111111

‎docs/adr/0096-execution-surface-identity-admission.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ All trace to the same `positions==0 && permissions==0 && !userId → skip` predi
177177
| Site | Surface | Note |
178178
|:---|:---|:---|
179179
| `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` | find/write middleware | the original seam (#2849) |
180-
| `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` | `getReadFilter` (analytics / reports / raw-SQL RLS compile) | returns `undefined` = *no filter*; the analytics mirror of :626 |
181-
| `objectql/engine.ts` Layer-0 + `driver-sql` `applyTenantScope` (opt-in on `tenantId`) | tenant scoping | Layer-0 is computed **after** the :626 skip, and the driver scope is opt-in → a principal-less context gets **no tenant filter at either layer** (cross-tenant read/write) |
180+
| `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` | `getReadFilter` (analytics / reports / raw-SQL RLS compile) | returns `undefined` = *no filter*; the analytics mirror of the middleware seam above |
181+
| `objectql/engine.ts` Layer-0 + `driver-sql` `applyTenantScope` (opt-in on `tenantId`) | tenant scoping | Layer-0 is computed **after** the `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` skip, and the driver scope is opt-in → a principal-less context gets **no tenant filter at either layer** (cross-tenant read/write) |
182182
| `packages/rest/src/rest-server.ts#enforceAuth` (+ lookup) | guest / public-form routes | bypass `enforceAuth`; fall open when `guest_portal` unregistered (partly mitigated by the `publicFormGrant`) |
183183
| `packages/runtime/src/http-dispatcher.ts#executionContext` | custom `object_operation` API endpoints | **accidental** — `callData` invoked with no `executionContext` (every sibling threads it) |
184184
| `packages/runtime/src/sandbox/body-runner.ts` | authored action/hook **body** interior facade | the inside of #2849 — only the *invoke* is gated (#2964); the body's `api.object().find/insert/...` run context-less |
@@ -194,7 +194,7 @@ All trace to the same `positions==0 && permissions==0 && !userId → skip` predi
194194
| Scheduled reports (`packages/plugins/plugin-reports/src/report-service.ts#isSystem` `dispatchDue`) run `executeReport(..., {isSystem:true})` → a member-owned schedule emails the target object's **entire** table, RLS bypassed | CONFIRMED exploitable | #2980 |
195195
| Knowledge/RAG `applyPermissionFilter` (`packages/services/service-knowledge/src/knowledge-service.ts#applyPermissionFilter`) returns **all** hits when `ctx` is missing/system; `chatWithTools`'s `ToolExecutionContext.actor` is optional with a system fallback → agent retrieval escapes the data ceiling | CONFIRMED (framework); exposure gated on cloud impl | #2981 |
196196

197-
These are *not* the :626 fall-open (they use an unconditional `SYSTEM_CTX`), but they are exactly what a D4 conformance row (`caller-scoped?` proof) + the D2 audit would have flagged. Fixed independently of the mechanism, tracked as the mechanism's motivating evidence.
197+
These are *not* the `packages/plugins/plugin-security/src/security-plugin.ts#getReadFilter` fall-open (they use an unconditional `SYSTEM_CTX`), but they are exactly what a D4 conformance row (`caller-scoped?` proof) + the D2 audit would have flagged. Fixed independently of the mechanism, tracked as the mechanism's motivating evidence.
198198

199199
### E3 — Structural: paths that never enter the security middleware (Class B)
200200

‎scripts/check-adr-symbol-anchors.mjs‎

Lines changed: 77 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ import { dirname, join } from 'node:path';
8585

8686
import { gitFreeEnv } from './git-env.mjs';
8787
import { isEntrypoint } from './invoked-as.mjs';
88-
import { ANCHOR_GRAMMAR, defineCorpus, sweepCorpus } from './symbol-anchors.mjs';
88+
import { ANCHOR_GRAMMAR, defineCorpus, extractAnchors, sweepCorpus } from './symbol-anchors.mjs';
8989

9090
/* ── The declared path population (#13519 / check-declared-population-live) ───
9191
* These literals ARE the population this gate reads, and the self-test holds
@@ -137,11 +137,50 @@ export function runCheck(root = process.cwd()) {
137137
process.exit(1);
138138
}
139139

140-
console.log(
141-
`✅ check-adr-symbol-anchors: ${counts.anchors} anchors across ${counts.docs} records resolve — ` +
142-
`${counts.symbol} symbol (${counts.declaration} declaration, ${counts.literal} literal), ` +
143-
`${counts.fileLevel} file-level, ${counts.crossRepo} cross-repo, ${counts.exempt} exempt, ` +
144-
`${counts.continuation} continuation. 0 line anchors survive.`,
140+
console.log(summaryLine(counts));
141+
}
142+
143+
/* ── The verdict may not claim a census wider than the vocabulary it read ──
144+
*
145+
* This line used to end `0 line anchors survive.` -- full stop, no
146+
* qualifier -- and that sentence was measured FALSE while it printed green.
147+
* Nine line pointers were live in `docs/adr/**` at the time, in spellings this
148+
* corpus does not judge: the path-less bare colon continuation (three in one
149+
* record, where the path sat in a different table cell from the number) and the
150+
* `L` pin, both bare in prose and written as a markdown link-target fragment
151+
* (six across two records). The measurement was true, the SENTENCE was not: the
152+
* gate's vocabulary is narrower than the class the #13556 ruling deleted, and
153+
* the verdict spoke for the class.
154+
*
155+
* ⭐ So the repair is the CLAIM, not the reading. The two path-less spellings
156+
* live in the shared grammar already and are OPT-IN per corpus
157+
* (`scripts/symbol-anchors.mjs#defineCorpus`, `pathlessLineCitations`), and
158+
* turning them on HERE was measured before this was written: 17 findings across
159+
* 6 records, of which 2 are an ADR's dev-server ports -- correct prose no
160+
* author can repair, whose only remedy is the maintainer-only exemption marker.
161+
* Widening is therefore a decision with a price tag and not this sentence's to
162+
* make; what this sentence owes is to stop reading as though the decision had
163+
* already gone the other way.
164+
*
165+
* ⛔ Do NOT restore the unqualified form. The self-test pins all three halves
166+
* -- that the corpus DECLARES the narrower vocabulary, that the shared grammar
167+
* really does see the wider one when asked (so the caveat is a measurement and
168+
* not a hedge), and that this sentence names both -- so a future author who
169+
* flips the declaration is made to re-state the claim in the same edit.
170+
*/
171+
export const JUDGED_LINE_SPELLINGS =
172+
'path-anchored, backticked continuation, fenced comment header, tilde';
173+
export const UNJUDGED_LINE_SPELLINGS =
174+
'the two path-less spellings, which continue a filename named earlier in the sentence';
175+
176+
export function summaryLine(counts) {
177+
return (
178+
`✅ check-adr-symbol-anchors: ${counts.anchors} anchors across ${counts.docs} records resolve — `
179+
+ `${counts.symbol} symbol (${counts.declaration} declaration, ${counts.literal} literal), `
180+
+ `${counts.fileLevel} file-level, ${counts.crossRepo} cross-repo, ${counts.exempt} exempt, `
181+
+ `${counts.continuation} continuation. 0 line anchors survive in the ${JUDGED_LINE_SPELLINGS} `
182+
+ `spellings this corpus judges — ⚠️ NOT a census of the deleted class: ${UNJUDGED_LINE_SPELLINGS}, `
183+
+ 'are opt-in and are OFF here (#18899).'
145184
);
146185
}
147186

@@ -177,7 +216,7 @@ function assert(cond, msg) { if (!cond) { console.error(`❌ check-adr-symbol-an
177216
// not red. A battery BELOW its floor means cases stopped running; the remedy is
178217
// to find what stopped registering.
179218
const SELF_TEST_BATTERIES = Object.freeze({
180-
'check-adr-symbol-anchors self-test': 17,
219+
'check-adr-symbol-anchors self-test': 22,
181220
});
182221

183222
// DELETING an entry silences that battery's floor exactly as effectively as
@@ -300,6 +339,37 @@ export function selfTest() {
300339
check(CENSUS_13556.rotRateIsLowerBound === true, 'the 72.1% figure is a LOWER bound and must be declared as one');
301340
check(CENSUS_13556.totalSurface === CENSUS_13556.distinctLineAnchors + CENSUS_13556.continuationAnchors, 'the declared surface must be the sum of its parts');
302341

342+
// 6. ⭐ The verdict's CLAIM, held against the vocabulary it actually read
343+
// (#18899). The three cases are one argument in three parts, and none of
344+
// them is worth anything without the other two:
345+
//
346+
// (a) the narrower vocabulary is this corpus's DECLARATION, not an
347+
// accident of the grammar -- so the caveat names a choice;
348+
// (b) the shared grammar really does see the wider vocabulary when it
349+
// is asked to, on the SAME text -- so (a) is a measurement and the
350+
// caveat is not a hedge against a rule that matches nothing;
351+
// (c) the printed sentence names both halves, and ⛔ no longer ends in
352+
// the unqualified census claim that was measured false.
353+
//
354+
// ⚠️ (b) is the light-on-a-literal control, and it is the case that makes
355+
// this battery worth running: without it, (a) and (c) would both hold just
356+
// as well on a day when `pathlessLineCitations` had stopped matching
357+
// anything at all, and the caveat would be documenting a dead rule.
358+
const pathlessProbe = 'The mirror of :626 and the pin L198 both continue a filename named earlier.';
359+
const asDeclared = extractAnchors(pathlessProbe, { pathlessLineCitations: CORPUS.pathlessLineCitations });
360+
const widened = extractAnchors(pathlessProbe, { pathlessLineCitations: true });
361+
check(CORPUS.pathlessLineCitations === false,
362+
'the narrower vocabulary must be this corpus\'s DECLARATION — flip it and the verdict below must be re-stated in the same edit');
363+
check(widened.lineAnchors.length === 2,
364+
`the shared grammar must SEE both path-less spellings when asked, or the verdict's caveat documents a dead rule — got ${widened.lineAnchors.length}`);
365+
check(asDeclared.lineAnchors.length === 0,
366+
`and must not see them under this corpus's declaration, or the caveat is simply wrong — got ${asDeclared.lineAnchors.length}`);
367+
const verdict = summaryLine({ anchors: 1, docs: 1, symbol: 1, declaration: 1, literal: 0, fileLevel: 0, crossRepo: 0, exempt: 0, continuation: 0 });
368+
check(verdict.includes(JUDGED_LINE_SPELLINGS) && verdict.includes(UNJUDGED_LINE_SPELLINGS),
369+
'the verdict must name BOTH the vocabulary it judged and the one it did not');
370+
check(!/line anchors survive\.\s*$/.test(verdict),
371+
'the verdict must not end in the unqualified claim `0 line anchors survive.` — that sentence printed green while nine pointers of the deleted class were live');
372+
303373
// ── The floor: every declared battery RAN, and ran its cases (#13489) ────
304374
//
305375
// Evaluated after every battery has had its chance and BEFORE the verdict, so

0 commit comments

Comments
 (0)