fix(check-adr-0087): walk a dotted member path through a class body - #17776
Merged
os-zhuang merged 4 commits intoSep 13, 2026
Merged
Conversation
`resolveMemberPath` narrowed each leading segment through an object literal only, so a member declared on a class had no dotted spelling: the walk answered "no `X` object literal is declared" and the bare fallback resolved to the first same-named definition in the file. Segments now resolve through an object literal OR a class declaration, counted as one union so a name opening both is refused as AMBIGUOUS rather than picked between. The top-depth rule and every bare reference are unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…or a class The anchored invariant on `scripts/check-adr-0087-registration.mjs` requires the ADR half of any widening in the same PR. The addendum described the walk as object-literal nesting; it now names both containers, and the author-facing remedy the gate prints shows the class spelling alongside the literal one. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
…pe-surface-only-class-member-path
…w prose `check:scripts-symbol-anchors` reads `engine.ts#findOne` as a real anchor and finds no tracked file at that path. The two new citations now use the full repo-relative path, or an elided one that is not anchor-shaped. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH
This was referenced Sep 12, 2026
This was referenced Sep 12, 2026
This was referenced Sep 13, 2026
os-zhuang
approved these changes
Sep 13, 2026
os-zhuang
marked this pull request as ready for review
September 13, 2026 08:07
os-zhuang
enabled auto-merge
September 13, 2026 08:07
os-zhuang
deleted the
claude/issue-17279-type-surface-only-class-member-path
branch
September 13, 2026 08:41
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…rding it (objectstack-ai#17766) Fixes objectstack-ai#17149 A `Claim:` comment that parses to ZERO branches is now reported as a MALFORMED claim instead of being discarded. The deliverable is triage 5620657752's, verbatim: 「⛔ do not fix the third spelling. **A parser that silently yields zero must say so** — an unparsed claim is an *unclassified* result, ⛔ never a *no*.」 Option 1 on the card (widening `claimedBranches` for the inline spelling) is **not** done here, and the branch reader's accept set is byte-identical — pinned as a case, because that is what keeps the NEXT unrecognised spelling loud on its first occurrence. ## The mechanism `governingClaim` picked the newest claim comment from which at least one branch parses, and `if (branches.length === 0) return;` threw the rest away. A discard is indistinguishable from an absence, so governance fell back to an older claim and said nothing. `claimGovernance(commentRows)` is now the two-part reading: - `governing` — what `governingClaim` has always returned. `governingClaim` is a one-line wrapper over it, so the ~18 readers that legitimately need only branches are untouched (proved below: the self-test's 3616 pre-existing cases pass unchanged on the refactor alone). - `malformed` — the NEWEST claim-shaped comment when it parses to zero branches, carrying its comment id, its timestamp, and the `created_at` of whatever governance fell back to. Recency is written once and both halves call it, so the two readings can never disagree about which comment is current. ## Every reader says so | reader | before | after | |---|---|---| | H20 / H27 dispatch liveness | probed the superseded branch, or (nothing parses) went silent as "no claim" | **H60**, a new `state`-band row, fires for every open `pm:dispatched` card, naming the comment id, what governance did instead, and the remedy | | `check-clause2-carriers` declaration limb | read the superseded claim's `Clause-②` line as if current | `cardDeclaration` returns `claim-branch-unparsed` **before** any line is read, from any comment (including the objectstack-ai#17366 correction path) | | `--pair N` | exit 4 with a verdict taken from the wrong comment | **exit 2 (UNJUDGED)**, with the whole reading printed | | the fold/lane rosters (H37/H38) | built on `governing` | unchanged — they need branches only, and H60 is the row that says the roster may rest on a superseded claim | **The exit code is the file's own, not a new one.** Its table already reads 2 as UNJUDGED — "an unread carrier is NOT a bare carrier and an unread thread is NOT an absent declaration (objectstack-ai#4690)" — and an unresolvable carrier is exactly that. Rendering it as 4 would make an unclassified result an adverse verdict, which is the reading the card refuses by name. Rendering it as 0-with-a-message is what the whole file exists against. ## The three questions the card asked ### 1. Where does the guard belong — carriers, half-states, or both? **Both, from one source.** The state is produced once in `check-half-states.mjs` (which owns `CLAIM_COMMENT_MARKER` and `claimedBranches`) and consumed in both files. A second detector in the carriers file is the drift that file's own docblocks refuse by name ("imported rather than restated ... so the two readers cannot drift"). ### 2. Which end? Option 2 only. See the four-axis reading below. ### 3. Is the fleet's claim template the cause? — MEASURED on the live board Read 2026-09-12T03:4xZ, REST, repo-scoped, over every open `pm:dispatched` card: - **28** open `pm:dispatched` cards. - **5** of them (18%) have a newest claim-shaped comment that parses to ZERO branches: **objectstack-ai#16310, objectstack-ai#16268, objectstack-ai#16251, objectstack-ai#16175, objectstack-ai#15234**. - 22 parse; 1 (objectstack-ai#13597) carries no claim-shaped comment at all. Four of the five claims were posted by ONE session (`session_012GKcPZbMoGq7WPzKLfRBTU`, the `domain:devx` execution PM seat) within **three seconds** of each other, 02:53:05Z–02:53:08Z, all in the same shape: ``` Claim: session_012GKcPZbMoGq7WPzKLfRBTU · claude/issue-16310-orphan-locale-key-gateable Clause-②: yes ``` That is not a run of typos; it is a template emitting a carrier no reader accepts. **objectstack-ai#16175 is the silent-fallback shape, live today**: its newest claim (5642984850, 2026-09-12) parses to zero, so governance falls back to its 2026-09-06 claim (5557414924) — which names a **different** branch, `claude/issue-16175-regen-sibling-stale-rules` against the current `claude/issue-16175-staleness-mtime-false-refusal`. Every downstream reader is probing the wrong ref, and the two `Clause-②` values happen to AGREE, which is the objectstack-ai#16589 near-miss recorded on objectstack-ai#16322 reproducing itself. ⛔ No `.claude/**` edit is made from this card, per the dispatch. The template finding is handed to the skills-lane seat; after this PR lands, those five cards are visible rather than silent, which is the point. ## Four-axis reading of option 1 vs option 2 - **实际业务需求** — measured, not speculative: 5 live carriers today, plus the objectstack-ai#16322 cost (two rounds, a director re-review, a re-issued claim) and its near-miss sibling. Option 1 addresses the one spelling in front of us; option 2 addresses the population that produces them. - **项目长远合理性** — option 1 is the treadmill the card names and objectstack-ai#16170 already bought once; each widening buys one spelling and leaves the next silent. Contract-first says the defect is at the reader's CONTRACT (a two-valued answer where three states exist), not in its accept set. - **防 AI 写代码犯错** — decisive here. Option 1 is consumer-side tolerance — the exact shape the axis forbids, and its failure mode is the silent one. Option 2 is a loud refusal at read time: a claim written in a shape the protocol does not accept is refused with the remedy named, and the seat cannot declare a claim the tooling does not honour. - **创业阶段不扩散需求** — option 2 adds no capability surface: one new reading of data already in hand, no new request, no new exit code, no new label written. Option 1 would grow the accept set permanently for a spelling the standing rule already forbids. ⇒ Option 2, on all four. Option 1 is deliberately NOT also done: doing both would let the accept set absorb the measured spelling and leave the new state unexercised on the live board, which is the one way to ship this fix and still not know whether it works. ## Verification All exit codes captured before any pipe (`cmd > log 2>&1; EXIT=$?`). | command | verdict line | exit | |---|---|---| | `pnpm check:pm-half-states` | `✓ check-half-states self-test: 3656 cases pass.` (was 3616) | 0 | | `pnpm check:pm-clause2-carriers` | `✓ check-clause2-carriers self-test: 493 cases pass (...)` (was 465) | 0 | | `pnpm check:pm-dispatch-gates` | `✓ dispatch-gates self-test: 1678 cases pass.` | 0 | | the derived union, 41 commands | `✓ dispatch-gates --ran: 41 derived famil(ies) accounted for — 41 run, 0 NOT-MEASURED (a DERIVED zero — all 41 recorded an exit code and none of them is 3).` | all 0 | The union was re-derived and re-run **after** the final commit, on head `b2b55c2b4` (`git rev-parse --short HEAD`), with `--repo objectstack-ai/objectstack` asserted. The first derivation printed a STALE TREE clause naming `check-skill-line-ratchet.mjs` and `check-widening-tells.mjs` (PR objectstack-ai#17760 had landed); `origin/main` was merged in, the list re-derived byte-identically at 41, and every command re-run on the merged head. Reconciliation was fed `command :: exit N` lines so the zero is derived, not claimed.⚠️ NOT MEASURED, and named rather than implied: the 47 artifact-roster families, the 11 declared-wide-population families, the 4 families taking a value from the workflow, and the 1 path-scheduled CI job are each outside the derived total — CI's, not this run's. ### Ablation — the new state can actually fail Committed first, then mutated on disk, then restored; the mutation and the restore are both proved by `git hash-object` against the HEAD blob rather than by an exit code. - Mutation: the pre-fix silent discard restored (`malformed` never populated). Anchor occurrences 1 → 0, file hash `f9f869cd` → `013b485b`. - Result: **20 of 3656** half-states cases and **14** carriers cases turn RED, every one of them in the new batteries. Direction: turns red, as pre-registered. - Restore: `git checkout HEAD -- scripts/pm/check-half-states.mjs`; `git diff HEAD` empty, hash back to `f9f869cd`. The script carries `trap ... EXIT INT TERM`. ## Live verification — the fix, run against the real board Both sweeps were run on the merged head, 2026-09-12T04:0xZ. **`check-half-states.mjs` (sweep, exit 0)** emits exactly **5** H60 rows — the same five cards the independent REST census found, arrived at through the fix's own code path: objectstack-ai#15234, objectstack-ai#16175, objectstack-ai#16251, objectstack-ai#16268, objectstack-ai#16310. Both sentence variants fire live: objectstack-ai#15234 gets the "NOTHING governs this card" reading, objectstack-ai#16175 gets "governance SILENTLY FELL BACK to an OLDER claim (2026-09-06...)". **`check-clause2-carriers.mjs` (sweep, exit 2)** reports **5 of 18 card/PR pairs UNJUDGED** — and two of them (objectstack-ai#15627 via PR objectstack-ai#17776, objectstack-ai#16565 via PR objectstack-ai#17310) are cards the `pm:dispatched` census could not see, because they are not in that population. Before this change the sweep exited 0 on all of them. **The before/after, measured rather than reasoned** — the base tree at `813f8e9f` materialised with `git archive` and its `cardDeclaration` run against the same live comment rows: | card | BEFORE | AFTER | |---|---|---| | objectstack-ai#16565 | `declared` `no` (`"Clause-②: no"`) | `claim-branch-unparsed` | | objectstack-ai#16175 | `declared` `no` (`"Clause-②: no"`) | `claim-branch-unparsed` | | objectstack-ai#15627 | `missing` | `claim-branch-unparsed` | Two of the three read as a confident `Clause-②: no` taken off a comment whose governance was never established — the objectstack-ai#16589 near-miss shape, twice, on today's board. The third read as `missing`, which sends the seat looking for a declaration line that is already written. Neither was a reading anybody could have found without opening the card by hand. ## Fixtures The measured bodies are quoted, never paraphrased, so a future widening cannot make these batteries pass by accident: - objectstack-ai#16322's two claims (5593513389 `Clause-②: no`, 5594909614 `Clause-②: yes`) — the inline spelling, and the two values DISAGREE, which is why reading the wrong one was a wrong answer and not merely an unlucky one.⚠️ Re-measured: the OLDER claim is branchless too, so on that card nothing parsed at all and `cardDeclaration`'s `pool = claimRows` fallback read the FIRST claim in thread order — a second silent fallback the card did not name, and the same fix reaches it. - The live 2026-09-12 specimen on objectstack-ai#16175, with its different-branch fallback. - objectstack-ai#16170's bulleted directive still parsing (its pin stays green, untouched at the H20 branch battery). - Controls: a well-formed newest claim still governs; an older branchless claim beside a well-formed newest one raises nothing; no claim comment at all still reads `absent`; an unreadable thread still reads `unreadable`. - The negative pins that keep this a STATE: the inline spelling still parses to ZERO, and `CLAIM_COMMENT_MARKER` still matches it. ## Acceptance notes - `skip-changeset` applies: `scripts/pm/**` ships in no package (fast-track path, no measurement owed). - `Clause-②: no`, as the claim declares — nothing published moves. - Scope held: no H22 docblock prose (objectstack-ai#17626 owns it), no widening tells (objectstack-ai#17618), no `.md`, no `.claude/**`. - H60 takes the `state` band, beside H34 — H34 reads a claim whose SEPARATOR the marker refuses, this one reads a claim whose BRANCH line the directive reader refuses; both are a live card contradicting itself, repaired on the board. ⛔ Not `stall`: the row does not claim the card is stopped, and the dev may be working perfectly well — what is broken is the READING. - ⛔ No age gate on H60, unlike H20's 60 minutes: a branchless claim is wrong at the instant it is posted and no later sweep frees it, because the protocol forbids a second `Claim:`. Pinned in both directions. - noted, not filed: `cardDeclaration`'s `pool = claimRows` path (used when nothing on the thread parses) picks the FIRST claim comment in thread order rather than the newest. This PR makes that path unreachable for the branchless case, so the residue is a recency question on a path no live shape now reaches. Successor: whoever next touches `cardDeclaration` — objectstack-ai#17098 is open against that function's neighbourhood. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…eir declared return type (objectstack-ai#17876) Part of objectstack-ai#17690 Clause-②: yes⚠️ **Deviation from the dispatch order, flagged rather than taken silently.** The order said to open this with a closing reference to the card. It opens with `Part of` instead, because **one of the card's nine doors is not the defect the card describes** and is deliberately left un-narrowed here (details below). A closing reference would take the card out of every open-state filter along with that remainder. This is the reversible direction: the PM seat can edit one word if it decides the remainder belongs on a new card. > **Notation.** Type arguments are written with **square** brackets in this body — `Promise[any[]]`, `Promise[Record[string, any]]` — because GitHub's body sanitizer eats short angle-bracket fragments. The code and the changeset carry the real spelling. ## 1. The ordered first act: the number, and the choice Triage ordered the pricing typecheck before any edit. Taken, on the whole consumer closure of both packages at the final tree: ``` turbo run typecheck --filter '...@objectstack/driver-sql' --filter '...@objectstack/driver-turso' --concurrency=2 --continue Tasks: 115 successful, 115 total TS errors: 0 ``` **Consumer-site count: 11.** Nine in `@objectstack/driver-sql`, two in `@objectstack/driver-sqlite-wasm` (which overrides none of these doors and reaches them through `driver-sql`'s declarations), and **zero anywhere else** — no package outside the three driver packages held a concrete driver value whose narrowed result it had to narrow. 11 is below objectstack-ai#17277's 18 ⇒ **one PR**, as the three predecessors did. ⛔ Not split. ## 2. The census — the predicate, and both controls ⛔ Not a grep. The predicate parses `IDataDriver` out of `packages/spec/src/contracts/data-driver.ts` with the TypeScript compiler API, enumerates every member, then parses each driver class and compares its **published** return annotation against the declaration. The `any` test walks the annotation's type AST for `AnyKeyword` nodes, so it sees a nested `any` exactly as well as a flat one — which is the one thing objectstack-ai#15267's literal-string predicate could not do. | | before | after | |---|---|---| | population | 35 members (32 methods, 3 properties) | same | | classes / cells | 6 / 210 | same | | tally | EXACT 103 · **MASKED 11** · DIFFERENT 4 · INFERRED 20 · ABSENT 72 | EXACT 112 · **MASKED 2** · DIFFERENT 4 · INFERRED 20 · ABSENT 72 | The `MASKED 11 → 2` and the `EXACT 103 · DIFFERENT 4` columns reproduce the card's tally independently. My INFERRED/ABSENT split is `20/72` against the card's `18/74`: a two-cell boundary difference in how `InMemoryDriver`'s `name` / `version` accessors are classified, ⛔ not a disagreement about any flagged door. - **COVERED control (fired):** the honest doors appear as `EXACT` rows in the same table — `MongoDBDriver.find` / `upsert` / `aggregate` / `bulkUpdate`, `InMemoryDriver.find` / `upsert`, `RemoteTransport.find` / `upsert` / `aggregate` / `bulkUpdate`, `SqlDriver.aggregate`, `TursoDriver.aggregate`. The predicate is shown to have covered them, not skipped them. - **DISCRIMINATION counter-control (fired):** `RemoteTransport.find` / `upsert` / `bulkUpdate` come back `EXACT` and are **not** flagged, while `RemoteTransport.beginTransaction` in the same class **is**. A predicate that flagged everything would have flagged all four. - **The two remaining MASKED rows after this change are exactly `InMemoryDriver.aggregate` and `InMemoryDriver.bulkCreate`** — the two doors under objectstack-ai#5499's freeze that triage declined to escalate. ⛔ Untouched, and the census is the receipt that they were not touched. The seat's re-measured line table was re-verified in my worktree and **holds on every one of the nine rows**, including both drifts (`SqlDriver.temporalFilterValue` 13825, `TursoDriver.bulkUpdate` 1606) and both seat-supplied numbers (`TursoDriver.beginTransaction` 1662, `RemoteTransport.beginTransaction` 1835). ## 3. The eight doors that landed | class | door | published | now | |---|---|---|---| | `SqlDriver` | `find` | `Promise[any[]]` | `Promise[Record[string, unknown][]]` | | `SqlDriver` | `upsert` | `Promise[Record[string, any]]` | `Promise[Record[string, unknown]]` | | `SqlDriver` | `bulkUpdate` | `Promise[Record[string, any][]]` | `Promise[Record[string, unknown][]]` | | `SqlDriver` | `temporalFilterValue` | `any` | `unknown` | | `TursoDriver` | `find` (override) | `Promise[any[]]` | `Promise[Record[string, unknown][]]` | | `TursoDriver` | `upsert` (override) | `Promise[Record[string, any]]` | `Promise[Record[string, unknown]]` | | `TursoDriver` | `bulkUpdate` (override) | `Promise[Record[string, any][]]` | `Promise[Record[string, unknown][]]` | | `RemoteTransport` | `beginTransaction` | `Promise[any]` | `Promise[unknown]` | Only the signature lines moved in `sql-driver.ts` — four annotations plus their docblocks, no read/write body — which is the footprint the dispatch drew against the concurrent round on that file (objectstack-ai#17859). ## 4. ⛔ The ninth door: a falsified premise, stated plainly **`TursoDriver.beginTransaction` is NOT narrowed here, and it cannot be by an annotation swap.** The card reads its declared type off `IDataDriver` (`Promise[unknown]`, `data-driver.ts:322`). For an **override** that is the wrong declaration: `TursoDriver extends SqlDriver`, and `SqlDriver.beginTransaction()` publishes `Promise[Knex.Transaction]` — **narrower** than the contract, the honest direction, and listed on the card itself under "Not findings". The base class is the binding declaration, so swapping the override onto the contract's own type does not compile. Measured, by doing it: ``` src/turso-driver.ts(1662,18): error TS2416: Property 'beginTransaction' in type 'TursoDriver' is not assignable to the same property in base type 'SqlDriver'. Type 'Promise[unknown]' is not assignable to type 'Promise[Transaction[any, any[]]]'. ``` So that `any` is **not masking an un-narrowed door**. It is masking a genuine LSP violation: in remote mode the override returns a libsql transaction while the inherited declaration promises a knex one. Closing it needs one of two things, neither of which is an annotation swap: - **widen `SqlDriver.beginTransaction` to the contract's `Promise[unknown]`** — measured by doing that too: **+14 further consumer sites** in these three driver packages alone (11 → 25), and it is a type-safety **regression** for every `driver-sql` consumer, reversing exactly the honest narrowing this card's own "Not findings" section protects; - **restructure the remote transaction handle** — a runtime change, and the dispatch says to stop and report rather than edit bodies. ⇒ Left named in the code, not re-masked and not forced with a cast, and carried to the PM seat as an open question. This is why the header says `Part of`. ## 5. The type-level pins — and a phantom leg the ablation caught Both halves per door, in the owning package's own tsc program (`tsconfig.json` selects `src/**/*`; neither package carries a DEBT / TEST_DEBT entry). Extended the two existing files of this family rather than adding new ones. **Reverse verification, direction predicted before it was run (turn red, exactly two errors per door, nothing else).** All eight doors put back to their masked annotations: ``` TS error count: 16 (8 doors x 2 halves) sql-driver-doors-declared-types.test.ts 192-197, 201-202 turso-driver-doors-declared-types.test.ts 185-190, 195-196 ``` The restore leg is proven by `git hash-object` equality on all three mutated files and an empty `git diff HEAD`. ⭐ **The first ablation run caught this card's own lesson inside the instrument.** With `find()` back at `Promise[any[]]`, the file red **once**: `Equals` fired and `IsAny` stayed green — because `IsAny[T]` asks about `T` itself, and the door resolves to `any[]`, not to `any`. The mandated "a regression reds the file twice" was one half. I then checked whether the **landed** doors carry it, rather than assuming they do not. They do: with `aggregate()` put back to its own historical `Promise[any[]]`, the shipped pin red **once**. Fixed in place by extending one detector, `ContainsAny`, across every per-door leg in both files — it asks `IsAny` first and then looks at the row and the cell, so it is a strict superset and the doors whose regression shape is a bare `any` lose nothing. Same `aggregate` ablation after the change: **2 errors**, both halves. That in-place fix is bounded and declared: same defect class as this card, mechanical, both files already in this diff, no new verification surface, and no other open PR claims either file (checked against the 19 open PRs). ## 6. Consumer-site narrowings — 11, ⛔ never a re-mask, ⛔ never a `!` | file | sites | shape | |---|--:|---| | `driver-sql/src/sql-driver.test.ts` | 2 | an id read off a row is `assert`-narrowed to `string` or `number` before it is passed as one | | `driver-sql/src/sql-driver-external-remote-name.test.ts` | 4 | `Array.prototype.find` now answers "or undefined"; the absent arm is narrowed away before four field reads that used to compile against nothing | | `driver-sql/src/sql-driver-13973-canonical-iso-read-door.test.ts` | 3 | a timestamp typed before `Date.parse`; a row id converted before it is used as a key; the absent arm of a lookup narrowed | | `driver-sqlite-wasm/src/sqlite-wasm-driver.test.ts` | 2 | the same id narrowing, reached through the subclass | ## 7. Changeset `minor` on `@objectstack/driver-sql` and `@objectstack/driver-turso`, carrying **BREAKING** under the launch-window convention. No runtime behaviour changes. **The ADR-0087 marker is derived from this diff, and it is NOT the one the order predicted.** The prescribed `type-surface-only` is **unavailable on all eight doors**, refused on two independent legs — measured by driving the gate, not read off the source: - **Leg 1, the six record-shaped doors.** Naming `sql-driver.ts#find`: *"[predicate 4: narrowed-from-erased] is FALSE: at the merge base the return annotation of `find` was already CONCRETE (`Promise[any[]]`), not any / unknown / unannotated."* `isErasedType` draws the line at "the type **is** `any`", never "the type **contains** `any`" — this card's subject, one layer up. - **Leg 2, the two unknown-destination doors.** Naming `remote-transport.ts#beginTransaction`: *"[predicate 4] is false at HEAD: the return annotation of `beginTransaction` is still `Promise[unknown]`. This category is for a surface that MOVED OFF an erased type. One that is still erased narrowed nothing."* `unknown` is a real narrowing to a consumer — it admits no property read — but the predicate groups it with `any`. This is the erased-destination wrinkle triage named. The two legs are each other's control: same citation form, same marker grammar, two **different** refusals naming two different revs ⇒ the probe discriminates rather than rejecting whatever it is handed. Disposition used: `not-required (no-migration-prescription)`, with both measurements written into the marker. **BREAKING** is carried, ⛔ not dropped. ## 8. Verification | what | result | |---|---| | `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` | 62 families derived; **all 62 run, every one exit 0** | | `--ran` reconciliation | `62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN` — a DERIVED zero, every family carrying its recorded exit code | | `check:dual-build-cjs-loads` | first run **exit 3, PREREQUISITE NOT MET** (three packages had no `dist`) — ⛔ not counted as a pass; re-run **exit 0** after a full `turbo run build` | | `driver-sql` test | 183 files (172 passed, 11 skipped), 2711 tests (2550 passed, 161 skipped) | | `driver-turso` test | 52 files, 1248 tests, all passed | | `driver-sqlite-wasm` test | 29 files, 518 tests, all passed | | typecheck | the three driver packages: 0 errors; whole consumer closure: 115/115 tasks, 0 errors | | lint | `eslint . --no-inline-config` over the **whole** population — **6656 files, 0 errors, 0 warnings**, exit 0. Not a narrowed run, so no narrowing needs proving | | control characters | `check:nul-bytes` exit 0, plus a direct scan of all 10 changed paths for the wider control-byte class: no match | **Tiers.** All three packages spell `test` as a bare `vitest run` and their `vitest.config.ts` declares no projects — **one tier each, run unnarrowed**. ⛔ No `--project` filter anywhere in this round (objectstack-ai#17853). The 11 skipped `driver-sql` files are its live-dialect Postgres/MySQL matrix, which skips without a live URL — the same lane CI's non-live job takes. The new cases were confirmed to have actually executed, by name: the `driver-sql` pin file runs 13 cases (was 8), the `driver-turso` pin file 10 (was 7). All readings were taken against this branch at `1c479a33fe`. The lint and gate figures are from that same tree; nothing has been committed since. ## Acceptance notes Findings from this round that are **not** repaired here: - **`TursoDriver.beginTransaction` masks an LSP violation, not an annotation.** Section 4. In scope of this card by the card's reckoning, out of reach of its repair shape. Carried to the PM seat as an open question with both options priced. - **noted, not filed — `type-surface-only` is unclaimable by this class, on two legs; both belong to a family already collected.** Section 7. Dedup run over the 500 most recently updated cards (numbers 3739-17876, open and closed), keyword-scanned for `type-surface-only` / `isErasedType` / `narrowed-from-erased`; control fired — objectstack-ai#17279 came back and is the card that names both terms. - **Leg 2 is already on the record, verbatim.** objectstack-ai#17279 carries it under "An adjacent limitation, measured in the same round, recorded not filed": `isErasedType` counts both `any` and `unknown` as erased (pinned on purpose as TSO-U5 / TSO-U6), so predicate 4 refuses an `any` to `unknown` narrowing. My measurement is a second, independent confirmation of it, ⛔ not a new finding. Its author handed it to their PM seat rather than filing, on the grounds that deliberately-pinned behaviour makes "defect or incomplete" a call no implementer should take alone. Same reasoning applies here, so it is handed over the same way. - **Leg 1 is a new limb of that same family, and the gate declares it deliberate.** The nested-`any` base reading is NOT what objectstack-ai#17279 reports: that card's identical-looking "already CONCRETE" row comes from resolving the WRONG same-named member, while leg 1 resolves the right one and is still refused. But the gate's own header states the line and its price: *"The line is 'the type IS `any`', never 'the type CONTAINS `any`' … admitting the broader question costs those gates their zero-false-positive property … `{ rows: any[] }` at base is a concrete object type, and a change that alters its members is not this category's class."* So this is a documented decision, ⛔ not a contract violation and ⛔ not a defect to file against — it is the same judgement call, and it goes to the PM seat. - 承接者: objectstack-ai#17279 (`pm:dispatched`, PR objectstack-ai#17776 in flight, which repairs symbol RESOLUTION rather than what predicate 4 then reads) and objectstack-ai#16787 (open, the sibling "unclaimable by its own subject" card on a different predicate pair). Which card absorbs which leg is triage's call, as objectstack-ai#17279 itself says. ⛔ No comment posted on either from this round. - **noted, not filed: eleven further files carry per-door `IsAny` legs of this family** (`driver-memory` x2, `driver-sqlite-wasm` x3, `driver-turso` x3, `driver-sql` x2, `objectql` x1). Whether each is a phantom leg depends on that door's declared shape and was not measured — only the two files in this diff were. 承接者: the next round of this pin family, or whoever takes the `ContainsAny` finding; the detector is now written down in both files here for it to copy. - **noted, not filed: the parameter side of these same doors is still `any`** (`data: Record[string, any]` on `upsert` and `bulkUpdate`, `value: any` on `temporalFilterValue`). Deliberately not in this family and recorded in the pin file's prose: a parameter typed `any` accepts exactly what one typed `unknown` accepts, so it erodes nothing on the **caller's** side, which is the axis this card is about. 承接者: no one — it is recorded as a boundary, not as a defect. ⛔ Not ready for review-flip or auto-merge by any agent seat: landing is the PM seat's act. Authored by Claude Code in session `01RuoNSXUbBoWHkNS4AknTrM` (https://claude.ai/code/session_01RuoNSXUbBoWHkNS4AknTrM), dispatched by the `domain:engine` execution PM seat. Attribution is stated here in prose because this surface APPENDS its own footer block on every body edit — the measured behaviour AGENTS.md records — so a footer written into the text would simply accumulate. --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Sep 17, 2026
…correct the residue and D3's pointer (objectstack-ai#17808) Fixes objectstack-ai#16270 **Why `Fixes` and not `Part of`.** This is the card's last residue. Its provenance half landed in PR objectstack-ai#17750 (the three documents that asserted the tab strip now name the record that declares it); the seat's merge comment recorded that the card stayed open carrying exactly one correction — this ADR. With it landed, nothing of objectstack-ai#16270 remains. - **Clause-②: no** — this PR puts no new key on any published payload.⚠️ **This PR is GOVERNED and stays draft.** `docs/adr/**` is a governed surface (AGENTS.md Prime Directive 14). ⛔ No seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. That is the expected end state, not a problem to route around. --- ## What was wrong ADR-0133's "What this record does not decide", item 1, asserted **in the present tense** that "Opens on tab-0 Members" is *"declared by none of its metadata"*, and that the tab ordering is *"either an emergent property of the renderer or a claim that has gone stale."* Both halves are false, and were already false on the day they were written. **The tab strip is declared metadata.** `packages/platform-objects/src/pages/sys-organization.page.ts` exports `SysOrganizationDetailPage` — `type: 'record'`, `kind: 'slotted'`, `isDefault: true`, for `sys_organization` — whose `slots.tabs` override carries exactly three `record:related_list` tabs: | order | label | objectName | |:--|:--|:--| | 0 | Members | `sys_member` | | 1 | Invitations | `sys_invitation` | | 2 | Teams | `sys_team` | plugin-auth hands it to the runtime — `auth-plugin.ts` imports it and declares `pages: [SysOrganizationDetailPage, SysUserDetailPage]`. It is registered, not a dead export. **It already existed at `77781151d`, the very commit the ADR cites.** The contents API for that path at that ref answers **HTTP 200**, blob `2f56173ff2c84b1ee9fb3324577df71fd78d9b3b`, 4606 bytes. **Lit control:** the same call for a fabricated sibling path at the same ref answers **Not Found**. So the 200 is a reading. ## Why a correct measurement produced a wrong conclusion ⭐ **This is the reusable part, and it is what the correction carries.** The ADR's two greps reproduce **exactly** on my own head, with their own lit control — they were never the error: - `git grep -rn relatedList -- packages/platform-objects/src/identity/` — the two hits on today's tree are the *provenance comments* PR objectstack-ai#17750 added; **zero declarations**, which is what the sentence claimed. Read the sites, not the count. - Lit control, same sweep: `packages/drivers/driver-sql/src/builtin-column-collision.ts` still hits, so the pattern matches something. - `relatedLayout` — 7 hits, all prose (3 in ADR-0085, 1 here, 1 in `field.zod.ts`'s doc comment, 2 in a SKILL.md). Zero object declarations. The search was **exhaustive over the wrong space**: scoped to `src/identity/` and to the `relatedList` key, while the declaration lives one directory over in `src/pages/`, written in a different vocabulary — an **assigned Page**, not a field prominence key. A control proves a probe reaches; it cannot prove the probe is aimed at the right place.⚠️ **Neither branch of the disjunction was even available**, measured at the objectui sha this repo pins (`.objectui-sha` = `53ded82bf7a494f54e344e19099dbf00854b8694`, read with `git show`, ⛔ nothing there edited): - `buildDefaultTabs` seeds `items[0]` with `{ label: 'Details', value: 'details', … }` **unconditionally**, so a promoted related list could never be tab-0. - `buildDefaultPageSchema` never calls it at all when an assigned page supplies a `tabs` slot: `if ('tabs' in slots && slots.tabs !== undefined) { components.push(...toNodeArray(slots.tabs)); }`. ⇒ adding `relatedList: 'primary'` would have been **inert on this page**, not corrective. "An emergent property of the renderer" is not merely unproven; it is measurably not what happens. ## Item disposition — it STAYS, as a corrected residue Asked to choose whether the item stays in "What this record does not decide" or moves. **It stays**, for three reasons: 1. **The disposition is still true.** This record does not decide the tab set or its order — that was never the false part. What was false was the *ground* stated for it. Moving the item would imply the placement was the error. 2. **The section's preamble stays honest.** It says "⛔ None of it is resolved here." Still true: this record decides nothing new. A correction of fact recorded in place is not a decision this ADR now makes. 3. **A correction belongs beside the reading it corrects.** ⛔ The item is not deleted — it records a real measurement, and the honest edit says what corrected it. The original reading is preserved verbatim under "The reading as originally recorded", including the "emergent property / gone stale" conclusion, quoted so a reader sees exactly what was overturned. The item now reads: recorded reading → what corrected it → what the measurement missed → why neither branch was available → the unchanged disposition. ##⚠️ Declared scope expansion — one sentence in D3 The dispatch fenced this round to item 1. **A whole-file sweep found the same falsehood at a second site**, and I corrected it too rather than leave a governed record contradicting itself one section away: >⚠️ **The tab set and its ordering are NOT declared by this repository, and this record does not decide them.** See [What this record does not decide](#what-this-record-does-not-decide). That sentence is D3's **pointer into the very item being corrected**. Leaving it would have made the ADR assert the falsehood in its *decision* section while refuting it in its residue section. It is corrected minimally: the "not declared" clause is replaced with the declaring record and its registration, the true half ("this record does not decide them") is kept word for word, and the pointer is kept. This is the bounded in-place fix, and all four conditions were measured before taking it: ① same defect class as the card; ② mechanical, in a form already pinned by item 1's correction; ③ no other claim holds this file — all 16 open PRs were enumerated and their file lists read; two touch `docs/adr/`, namely PR objectstack-ai#17776 (ADR-0087) and PR objectstack-ai#17756 (ADR-0025), and **zero** touch ADR-0133; ④ same gate family, no new verification surface. ⭐ The sweep that found it is the same discipline this card is about: `NOT declared` → 1, `declared by none` → 1, `emergent` → 1, `tab order` → 2, on a whitespace-flattened, indent-stripped stream. Exactly two sites; both corrected. ⛔ Items 2, 3 and 4 of that section are **byte-untouched** (they appear in the diff only as context lines). No code, no metadata, no other ADR, nothing under `content/docs/releases/`. ## Verification⚠️ **Prose probes must be flattened before matching.** On the raw file, three of the four quotes I needed returned **0** — they wrap across lines. On a flattened, indent-stripped stream all four return **1**, with `relatedList` → **2** on the same stream as the lit control proving the probe reaches. | probe | raw file | flattened | |:--|--:|--:| | `declared by none of its metadata` | 1 | 1 | | `emergent property of the renderer` | **0** | 1 | | `no object in ... declares the relatedList prominence key` | **0** | 1 | | the deep-link-contract sentence | **0** | 1 | | **lit control** `relatedList` | — | **2** | **On-disk proof of the edit** (⛔ not the editor's exit code): replaced text → 0 occurrences each; injected text → present each; the preserved original reading → still 1 each; `relatedLayout` → 1 as the control on the same after-stream. **Gates — all run with the exit code captured BEFORE any pipe** (`cmd > file 2>&1; EXIT=$?`), the set derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (18 commands; the stderr provenance line names this repo and commit `310760d225`): | gate | exit | |:--|--:| | `check-adr-symbol-anchors` (+ `--self-test`) | 0 | | `check-adr-links` (+ `--self-test`) | 0 | | `check:adr-anchors` | 0 | | `check:doc-authoring` | 0 | | `check:nul-bytes` | 0 | | `check:pm-governed-merges` | 0 | | `check-closing-keyword-parity` (+ `--self-test`) | 0 | | `check-ci-filter-parity` | 0 | | `check-comment-mask-corpus` | 0 | | `check:cross-package-test-inputs` | 0 | | `check:driver-memory-census` · `check:refd-timer-probe` · `check:watch-hint-literal` | 0 | | `report-test-timings --self-test` | 0 | ⭐ **The new anchors are genuinely checked — proven by ablation, not by the green.** `check-adr-symbol-anchors` passes 2079 anchors across 139 records. Renaming the symbol inside my new anchor on disk drove it to **exit 1** with `[unresolved-symbol]` at **both** new sites; restoring reproduced the file **byte-identical** (`git hash-object` `e085c37a…` before and after, with a `trap … EXIT INT TERM` and absolute paths), and the gate returned to exit 0. On-disk proof preceded reading any result: injected marker → 2, original spelling → 0.⚠️ **The two `objectui:` anchors are only judged when a checkout is available.** Default run: 27 cross-repo anchors reported-not-judged. With `OBJECTUI_CHECKOUT=/home/user/objectui`: skipped drops to 11 and mine are not among them ⇒ judged and resolved. Both symbols were independently confirmed at the pinned sha with `git show`. ⛔ **One gate was NOT MEASURED at first and is now measured.** `check:doc-formula-expressions` returned **exit 3 — PREREQUISITE NOT MET** (`@objectstack/formula` and `@objectstack/lint` not built). Exit 3 is not a failure and ⛔ never counts inside a green tally. My file **is** in its corpus (`ROOTS` includes `docs`; `docs/adr` is not in `SKIP_PATHS`), so the prerequisite was satisfied and the gate re-run rather than waived. **Repo-wide scans are CI's.** No local narrowing is claimed for them. ## Changeset — `skip-changeset`, and this is why ⛔ Measured, not assumed. - **Target:** `docs/adr/0133-org-management-open-basics.md` resolves to **no owning package at all** — it sits outside every package directory, so no `files[]` can reach it. - Across the workspace: **70 published packages** (12 private skipped, 0 published without a `files[]`). The **union** of every `files[]` entry is `CHANGELOG.md | README.md | api-surface | dist | json-schema | liveness | llms.txt | prompts | spec-changes.json | src/**/*.zod.ts`. Entries naming `docs/adr` or escaping the package dir with `..`: **0**. - All 139 ADR files live at repo-root `docs/adr/`; **none** is nested inside a package. - No build or copy step pulls them in: `git grep docs/adr` over every `package.json`, the tsup configs and `turbo.json` → **no hits, exit 1**. **Lit control** on the same probe space: `dist` in those same `package.json` files → hits, exit 0. So the zero is a reading. - **Positive control (ships):** `packages/platform-objects/README.md` → direct `files[]` hit **true**. - **Negative control (does not):** `packages/platform-objects/src/identity/invite-entry-toolbar.test.ts` → direct `files[]` hit **false**. ⇒ nothing published moves, so `skip-changeset` applies — the label's own documented case in `lint.yml` is a PR that releases nothing.⚠️ Note for whoever lands this: the size-labeler's whole-set `PUT` has erased a seat-applied `skip-changeset` before; the label was applied additively and read back. ⭐ Deliberately the **opposite** call from PR objectstack-ai#17750 on the same card, and both are right — because both were measured. There, a comment-only diff moved published bytes (the new text appeared 4 times inside `packages/platform-objects/dist`), so a changeset was owed. Here the file cannot reach a tarball at all. ## 维护者速读(草稿) **改了什么** — ADR-0133「本记录不决定什么」第 1 条,以及 D3 里指向该条的那一句。原文用现在时断言「组织记录页开在 tab-0 Members」这件事「没有任何元数据声明它」,并推论 tab 顺序要么是「渲染器的涌现属性」,要么是「已经过时的说法」。两半都是假的,而且写下它的那天就已经是假的。 **为什么改** — tab 条本来就是声明出来的:`SysOrganizationDetailPage` 是 `sys_organization` 的 slotted 记录页,`slots.tabs` 里正好三个 related_list —— Members、Invitations、Teams,顺序如此,由 plugin-auth 注册进运行时。它在 ADR 自己引用的那个 commit `77781151d` 上就已经存在(HTTP 200,带「伪造路径回 Not Found」的对照)。⭐ ADR 的**测量是对的,推论是错的**:搜索被限定在 `src/identity/` 和 `relatedList` 这个键上,而声明在隔壁 `src/pages/`,用的是另一套词汇 —— 一次「把错误的空间穷尽搜索」必然回一个理直气壮的零。这条教训是本次修正真正要留下的东西。原读数**没有删**,原样保留并标注是什么推翻了它。 **风险与代价(含回滚)** — 风险很低:改的是散文,不动任何代码、元数据或其他 ADR;第 2、3、4 条逐字节未动。新引用全部是受门禁校验的符号锚,并用消融证明了它们真的会变红(改名后 exit 1,还原后字节一致、exit 0)。⛔ 不发布任何东西,因此 `skip-changeset`(已实测,带正负对照)。回滚 = 直接 revert 这一个 commit,单文件,无下游依赖。⚠️ 需要您注意的只有一处:派发把范围钉在第 1 条,我按「有界就地修」把 D3 里同一句假话也改了 —— 否则同一份受管记录会在决策节说 A、在残留节说非 A。若您认为不该扩,删掉 D3 那一段即可,第 1 条的修正独立成立。 **席位意见** — (留空,待席位补) **你要做的** — 这是**受管面**,按 Prime Directive 14,只有您手工合并;⛔ 任何 AI 席位都不会把它翻 ready、不入队、不挂 auto-merge。请确认两件事:① D3 的那处扩范围要不要保留;② 第 1 条留在「本记录不决定什么」作为「已更正的残留」是否合您的意 —— 我给的理由是:该条的处置本身没错(本记录确实不决定 tab 顺序),错的是它给出的理由,所以位置不动、理由更正。 ## 验收备注 - **noted, not filed** — 观察类,不立卡:`docs/qa/platform-checklist/runs/` 里仍然只有 `README.md`,`identity-auth.org-membership-team-management`(P1)从未对真实 console 执行过。它的 tab 断言是从 cloud ADR-0081 写出来的,没有被观察过。这是清单计划自己的 backlog,不是本树的缺陷。**承接者:** checklist-test lane / 下一次 platform-checklist 运行。 - **noted, not filed** — 命名撞车,不是缺陷:objectui 另有一个手写的 console 区域 `/organizations/:slug`(`OrganizationLayout.tsx`),tabs 是 Members / Invitations / **Settings**,没有 Teams,且确实 index-redirect 到 members。任何靠点界面来复测本卡的人都可能落到那个界面,然后在任一方向上得出一个很自信的错误答案。**承接者:** 本节本身 —— 下一个读这些文档的人在这里遇到它;PR objectstack-ai#17750 的验收备注已记过一次,这里是它在受管记录这一侧的对应位置。 - **noted, not filed** — 边界记录,不扩类:ADR-0133 的状态行仍是 `Proposed (2026-09-06)`,等待维护者手工合并这一「受管面的接受动作」。本 PR 不动状态行 —— 那是维护者的动作,不是更正的一部分。**承接者:** 维护者,在合并本 PR 时一并判断。 --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH --- _Generated by [Claude Code](https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH)_ Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #17279
The defect
PR #15724 closed #15627 by widening a
type-surface-onlyreference to a dotted member path, so an author can name a nested member instead of a bare identifier. The walker it added narrows each leading segment through an object literal only. A member declared on a class therefore has no dotted spelling, and the bare fallback is "the first same-named definition in the file" — which is the defect #15627 was filed on, surviving its own fix on the container kind that fix did not cover.Reproduced at the merge base on the card's own evidence,
packages/objectql/src/engine.ts(read-only here, not edited):packages/objectql/src/engine.ts#delete(bare)Promise<boolean | number>—ObjectQL.delete, which the card's diff never touchedpackages/objectql/src/engine.ts#ObjectRepository.deleteno `ObjectRepository` object literal (`ObjectRepository: {` or `ObjectRepository = {`) is declared at the top of the filePromise<any>packages/objectql/src/engine.ts#ObjectRepository.findOnePromise<Record<string, any> | null>packages/objectql/src/engine.ts#ObjectRepository.updatePromise<Record<string, any> | number | null>The first two rows are the load-bearing pair: the two spellings reach different members (
Promise<boolean | number>vsPromise<any>), so the bare one is not a usable substitute — it answers a true sentence about the wrong member.Lit control for that probe: the object-literal case #15724 rescued,
packages/client/src/index.ts#oauth.applications.get, resolves toPromise<OAuthApplication>both before and after. A probe that found nothing for classes was one that could find something for literals.⭐ The population, established before fixing (triage's gate)
Triage asked how many published narrowings sit on class members today, and made the answer decide how this PR is written. It is not zero.
Method: enumerate every
adr-0087: not-required (type-surface-only …)marker in the tree withgit grep(markers are copied verbatim from.changeset/*.mdinto each package'sCHANGELOG.md, so the tree at HEAD carries the published ones too), split the comma-separated reference lists, then classify each named symbol's innermost container by walking class / interface / enum bodies and named object literals over the repo's own comment- and literal-masked projection.Space searched: every tracked file at HEAD — which is where markers live:
.changeset/*.md(4 files),packages/*/CHANGELOG.md(4 files),docs/adr/0087-*.mdand this gate's own fixtures. Placeholder references (path/to/file.ts#Symboland the angle-bracket forms) were excluded.Result — 42 real references:
The ten class members:
packages/drivers/driver-sql/src/sql-driver.ts#aggregate,#bulkCreate,#create,#findOne,#update— all onclass SqlDriverpackages/drivers/driver-turso/src/turso-driver.ts#aggregate,#bulkCreate,#create,#findOne,#update— all onclass TursoDriver⭐ Every one of the ten is written BARELY, and not one of them could have been written dotted. Each resolves today only because that name happens to be unique in its file (measured: 1 same-named definition each); the dotted spelling
SqlDriver.findOnewas refused for all ten with the object-literal message. So the category has been in live use on class members all along, addressable only by an accident of naming — and on the one file where the accident does not hold,engine.ts, the member had no spelling at all.Lit controls for the census probe (a zero is a reading only if the instrument could have come back the other way):
packages/objectql/src/engine.ts#findOne→ 2 definitions,class ObjectQL(L9844) andclass ObjectRepository(L15072). Lit.packages/client/src/index.ts#oauth.applications.get→ 14 definitions across nested literals insideclass ObjectStackClient. Lit.scripts/check-adr-0087-registration.mjs#parseSymbolRef→ 1, TOP LEVEL. Lit.⇒ The gap is live, not latent. This PR unblocks the
engine.tsshape outright, and gives the ten existing references a spelling that survives a same-named member being added above them.The change
resolveMemberPathwalks each leading segment throughcontainerBodiesFor, the union ofobjectLiteralBodiesForand the newclassBodiesFor. A union, not two passes: a segment naming one literal and one class is AMBIGUOUS exactly as two literals are, and counting the kinds separately would let it through as "one of each".class,export class,export default class,declareandabstractprefixes; the body is found with the existingdeclarationBodyStart, soextends Base<{ a: 1 }>does not send the walk into a type argument.const X = class { … }) is deliberately not walked — that name belongs to the binding, not the class.definitionsAtTopDepthnow treats any nested container as deeper nesting, so thecheck-adr-0087-registration's dottedtype-surface-onlywalker cannot name a DIRECT member of an object literal whose name recurs in a nested literal —organizations.createis refused as AMBIGUOUS withorganizations.teams.create, and no deeper path exists for the direct one #16571 top-depth rule holds inside a class body too.fix:remedy name both container shapes, and the remedy now prints a class example beside the literal one. An author who cannot see the spelling in the remedy cannot write it.Bare references are untouched. Nothing about what the marker means changed, and nothing beyond naming a class member was widened.
⭐ Ablation — it can fail, and it still refuses what it should
Removing the class limb from the union (
containerBodiesForback to object literals only), proven on disk before reading any result — anchor occurrences 1 → 0, blob394fd83b→c3a303c3, restored afterwards and verified byte-identical toHEAD— turns 9 assertions red:TSO-C1reads back the exact original refusal:no `ObjectRepository` object literal or class (…) is declared at the top of the file.TSO-C12(end to end, throughscan()) fails as[predicate 4] cannot be resolved at HEAD in packages/objectql/src/engine.ts.TSO-C9, the union pin, does not merely fail — it silently resolves:dual.findOnereturnsPromise<Lit>instead of refusing, i.e. the ablated walker picks one of two real candidates. That is the "writable but wrong" reference the whole dotted grammar exists to prevent, and it is why the union is counted as one set.Still refused after the change, so the gate did not simply become permissive:
NoSuchClass.findOne→no `NoSuchClass` object literal or class (`NoSuchClass: {`, `NoSuchClass = {` or `class NoSuchClass {`) is declared at the top of the file(and end to end,TSO-C14).ObjectRepository.nosuch→no `nosuch` definition sits inside it— never resolved outward to the same-named member on the other class.opens 2 classes …, so the path is AMBIGUOUS.opens 2 object literals and class bodies …, so the path is AMBIGUOUS.Whole-tree regression control: all 42 of 42 live references read byte-identically before and after — 0 moved, 0 newly resolved. The ten bare class-member references in particular did not move.
Changeset — measured, not assumed
skip-changeset. Measurement, with both controls, on a real build:containerBodiesFor/classBodiesFor/describeBodies/BODY_PLURALSoccur 3 / 2 / 2 / 2 times in the changed source and 0 times in any builtdist.OpenAIEmbedder, a symbol that ships → 58 occurrences inpackages/plugins/embedder-openai/dist. The probe finds shipped text.createOpenAIEmbedder presets, which exists only in that package's test file → 1 insrc/__tests__, 0 indist. The probe separates shipped from unshipped.package.jsonisprivate: true, and 0 of 70 non-private manifests has a directory containing either changed path, so nofiles[]can reachscripts/**ordocs/adr/**.⇒ Nothing published moves.
⛔ Governed surface — the maintainer merges this by hand
The diff touches
docs/adr/0087-metadata-protocol-upgrade-contract.md, anddocs/adr/**is in today's register (docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md). The ADR edit is not optional: the anchor record for this script states "Never widen the exemptions here without the ADR half in the same PR", and the addendum described the walk as object-literal nesting. ⛔ No seat merges, queues, or arms auto-merge on this PR.维护者速读(草稿)
改了什么 —
type-surface-only标记里的点分成员路径,原先只能穿过对象字面量;现在也能穿过类体,于是声明在类方法上的类型收窄终于能在它自己的标记里被点名。裸标识符的含义一个字节都没动。为什么改 — 这是 #15627 的残留:它的修复只覆盖了一种容器。树上已有 10 条活的引用落在类成员上(
SqlDriver/TursoDriver各 5 条),全部靠"这个名字在该文件里恰好唯一"才能解析;一旦不唯一(engine.ts就是),作者只剩两条路——写一个指向别的成员的错标记,或者丢掉**BREAKING**去躲开判据。后者正是 #13080 记录的侵蚀。风险与代价(含回滚) — 风险面是这一个门禁脚本的解析行为。全树 42 条现存引用改动前后逐条比对完全一致,消融显示去掉类分支会让 9 条断言转红、其中 1 条会变成"静默挑一个"。回滚 = revert 本 PR,门禁回到今天的行为,无数据迁移、无发布物变动。
席位意见 — (留空,待维护者)
你要做的 — 本 PR 触及
docs/adr/**(受管面),⛔ 不进合并队列、不开自动合并,需要你手动合并或给出授权的 APPROVED 评审。Verification
node scripts/check-adr-0087-registration.mjs --self-test→ 355 assertions, exit 0 (338 at the merge base; +17 in the newTSO-Cbattery, registered at its floor inSELF_TEST_BATTERIES).node scripts/check-adr-0087-registration.mjs --base origin/main→ exit 0.node scripts/pm/dispatch-gates.mjs --commandson the merged head: 43 commands — 42 run and green, 1 NOT MEASURED.check:scripts-symbol-anchorscaught a real regression of mine (abbreviatedengine.ts#findOneprose read as a live anchor) — fixed in its own commit and re-run green.check:doc-formula-expressionsfirst exited 3 (PREREQUISITE NOT MET — nothing measured) because@objectstack/formulawas unbuilt; afterturbo run build --filter=@objectstack/formula --filter=@objectstack/lintit exits 0.pnpm check:pm-dispatch-gates— its self-test did not reach a verdict inside this container's foreground window across four attempts (still running after 11 minutes, no output advancing), so it is declared to CI rather than reported as green. It grades the dispatch-gates checker's own fixtures, not this diff, and this diff touches noscripts/pm/path. ⛔ Read this as unmeasured, not as passed.mainmerged once (0a88a800bd, which brought PR docs(spec): name the node slot in the structural-condition ruling and its ADR-0087 entry #17761'spackages/spec/src/migrations/**changeset) and everything above re-measured on the merged head: self-test 355/0, census unchanged at 10 of 42, whole-tree comparison still 42 of 42 identical.Generated by Claude Code