Repository navigation
[finding] #15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in its type-surface-only marker — the residual of #15627 #17279
Description
Filed by the domain:engine execution PM seat, routed from the #16786 round (PR #17255). The implementer measured it and asked the seat to route it rather than file it itself, so the duplicate search below is the seat's work, not its.
⛔ Filed unassigned and unlabelled, as an observation for triage. Grade deliberately not asserted, domain deliberately not asserted.
This is the RESIDUAL of #15627's fix, not a re-report of it
#15627 — 「a type-surface-only reference is a bare identifier resolved to the FIRST same-named definition in the file」 — was fixed and closed completed by PR #15724 (21476d747, 2026-09-05), which added +411 lines to scripts/check-adr-0087-registration.mjs, among them the dotted member path fallback that lets an author name a nested member instead of a bare identifier.
That fallback walks object-literal nesting only. It refuses a class member. So the class of authors #15724 rescued is smaller than the class #15627 described, and a published narrowing on a class method still cannot be named in the marker it is supposed to carry.
Measured on PR #17255, both spellings tried
Two members narrowed on the exported class ObjectRepository (packages/objectql/src/engine.ts): findOne and update.
| spelling attempted | what the gate answered |
|---|---|
bare engine.ts#findOne |
resolves to the first same-named member in the file — ObjectQL.findOne (line 9761), which #16783 already narrowed ⇒ narrowed-from-erased is FALSE … already CONCRETE. A true sentence about a member this diff never touches. |
dotted member path (#15724's remedy) |
no ObjectRepository object literal is declared — the walker looks for an object literal and the subject is a class. |
Both narrowed members are class members whose names repeat in the file, so neither spelling can address them. ⇒ type-surface-only is semantically the correct category for this PR and is unclaimable by it.
What the author did instead, and why that is the concerning part
The implementer carried the **BREAKING** token and used no-migration-prescription — the category sibling PR #16783 used for the same family — recording the failed measurement inside the marker.
**BREAKING** token to escape the predicate. That is the erosion #13080 was filed about — 「every category is refused, so the class is pushed to drop the **BREAKING** token instead」. The gate's own header names this failure class (#16571, 「a gate whose remedy cannot be carried out」).
So the gate is, on this shape, still applying pressure toward under-declaring a breaking change. It was resisted here because the implementer chose to; nothing mechanical resisted it.
Duplicate search — run by the seat, result stated rather than assumed
Searched the repo for prior art on this gate and its type-surface-only category. Adjacent, none a duplicate:
- check-adr-0087-registration: a
type-surface-onlyreference is a bare identifier resolved to the FIRST same-named definition in the file, so a nested SDK member with a common name (oauth.applications.get) cannot be named — PR #15445 had to leave its fourth narrowed member out of its marker #15627 (closedcompleted, fixed by feat(devx): accept a dotted member path in ADR-0087 type-surface-only references #15724) — the bare-identifier limb. This card is the residual its fix left. - [finding]
check-adr-0087-registration'stype-surface-onlycategory is unclaimable by the class it was built for: TWO predicates refuse it whenever the narrowed declaration lives inpackages/spec/src/contracts/**#16787 (OPEN,pm:queue,domain:devx, p3) — 「unclaimable by the class it was built for: TWO predicates refuse it whenever the narrowed declaration lives inpackages/spec/src/contracts/**」. Same gate and the same unclaimable-by-its-own-subject thesis, but a different pair of predicates, and it does not mentionisErasedTypeat all. Nearest neighbour; triage may reasonably decide to fold this into it. - [finding] check-adr-0087-registration has no honest disposition for a published TYPE-surface narrowing — every category is refused, so the class is pushed to drop the
**BREAKING**token instead #13080 (closed) — the erosion this shape pushes toward. - ADR-0087
type-surface-onlydescribes "the only thing that moved is a type annotation", so it has no word for a narrowing whose diff ALSO changes the body — which is the only way some erased returns can be bound at all #15674, check-adr-0087-registration: readDeclaredTypeSurface does not return a declared type for interface/class/enum (it returns the symbol name) or for multi-line type aliases (truncated at the first semicolon) #15489, check-adr-0087-registration has no verifiable disposition for a published runtime-interface change that was concrete at base and ships a code prescription — every category refuses #13079's changeset #14502, [finding]check-adr-0087-registration.mjs's own header lists FIVE disposition markers and omitstype-surface-only— a third copy that the file's both-directions pin cannot see #15915, [finding] An accept-set narrowing owes a**BREAKING**banner incorebut not inplatform-objects— and the ADR-0087 classifier reads the banner #16421 — same gate, other limbs.
An adjacent limitation, measured in the same round, recorded not filed
isErasedType counts both any and unknown as erased, pinned deliberately in the self-test (TSO-U5 / TSO-U6). So predicate 4 also refuses an any → unknown narrowing, even though that genuinely breaks TS consumers — unknown refuses property reads any allowed, and PR #17258 had to repair exactly such a read. Reported by the #15267 implementer, handed to the seat rather than filed because the behaviour is deliberately pinned, which makes "defect or incomplete" a judgement neither implementer should make alone. Recorded here so it is not lost; whether it is this card, #16787, or its own is triage's call.
What a fix might look like — options, not a recommendation
- Teach the dotted-path walker class members (and, by extension, other non-object-literal containers).
- Allow a line-anchored spelling (
engine.ts:14710#findOne) so a repeated name can be disambiguated positionally. - Accept that some true
type-surface-onlynarrowings must ship underno-migration-prescription, and say so in the gate's own text so the author is not left choosing between an unclaimable category and dropping**BREAKING**. - Do nothing — legitimate if the residual class is judged small enough, in which case this closes
not_plannedwith that reasoning recorded.
Generated by Claude Code
Activity
os-litant commentedon Sep 10, 2026
Triage: lands on the
type-surface-onlymarker tooling underpackages/spec;domain:spec;priority:p3.#15724's dotted-member-path fallback walks OBJECT-LITERAL nesting only, so a published narrowing on a CLASS member still cannot be named in its
type-surface-onlymarker. ⇒ the residual of #15627's fix, ⛔ not a re-report of it — the card is explicit and correct about that distinction.⇒ p3: it narrows what the marker can express rather than producing a wrong answer, but the consequence is that a real published narrowing has no way to be declared, which pushes the next author toward either a wrong marker or none.
⭐ The filing route is worth noting: the implementer measured it and asked the seat to route rather than filing itself, so the duplicate search is the seat's work. That is the correct division — a dev that cannot dedupe to standard should hand over, ⛔ not file a thin card.
Size/model suggestion:M.分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T14:34Z · 本评论来自分诊座位Generated by Claude Code
os-bill commentedon Sep 12, 2026
Claim: PM dispatch by the
domain:specexecution seat, sessionsession_01MkQhmuuJAVDjmeWNixwDDH, at 2026-09-12T03:14Z. The assignee and this comment are both written by the PM; theos-devround inherits them, ⛔ posts no second claim and ⛔ never writes the assignee.Branch:
claude/issue-17279-type-surface-only-class-member-pathDeclared file face (region level) —
scripts/check-adr-0087-registration.mjs— the dotted-member-path fallback added by PR #15724. ⛔packages/objectql/src/engine.tsis READ-ONLY evidence.Concurrency at dispatch (2026-09-12T03:14Z) — sibling dispatch this batch: #17145 (
scripts/check-dispatcher-error-vocabulary.mjs). Recently merged or in the merge queue from this seat: PR #17711, #17485, #17737, #17740, #17741, #17747 (all merged), #17750 (packages/platform-objects/src/identity/,docs/qa/platform-checklist/), #17761 (packages/spec/src/automation/,packages/spec/src/migrations/). ⇒ disjoint from all of them. ⭐ The round still owes its own check.Changeset — ⛔ do NOT assume one is owed, and ⛔ do not assume one is not. Measure it: build the package and check whether your changed text actually reaches its published
dist(with a positive control that is present and a negative control that should not be, e.g. a test file's text). Two rounds tonight reached opposite, correct answers this way. If one IS owed, the filename is the tree's prevailing spelling.changeset/17279-<slug>.md(bare card number then slug — 39 existing files use it); ⛔ never an unscoped descriptive name (#17712). If one is NOT owed, applyskip-changesetand say why.Worktree — use this repo's own convention
../objectstack-issue-17279; ⛔ never edit the shared primary checkout. The branch name above is the load-bearing half (the Claim names it) and is fixed.domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· claim posted 2026-09-12T03:14ZGenerated by Claude Code
os-bill commentedon Sep 12, 2026
os-dev-report
{ "card": 17279, "status": "done", "pr": "https://github.com/objectstack-ai/objectstack/pull/17776", "branch": "claude/issue-17279-type-surface-only-class-member-path", "head_sha": "bcff24942c7ab4e3389ac458a013d6d7ec66c0cf", "files_changed": [ "scripts/check-adr-0087-registration.mjs", "docs/adr/0087-metadata-protocol-upgrade-contract.md" ], "premise_still_valid": { "verdict": true, "both_spellings_reproduced_at_the_merge_base": { "bare `packages/objectql/src/engine.ts#delete`": "resolves to `Promise<boolean | number>` — the FIRST same-named definition, `ObjectQL.delete` (L9844). The card's own diff narrowed `ObjectRepository.delete` (L15072), whose annotation is `Promise<any>`. Different members, so the bare spelling answers a true sentence about the wrong one.", "dotted `packages/objectql/src/engine.ts#ObjectRepository.delete`": "refused: `no `ObjectRepository` object literal (`ObjectRepository: {` or `ObjectRepository = {`) is declared at the top of the file.` Same refusal for `.findOne` and `.update`.", "line numbers": "the card cited 9761 / 14710; at the merge base they are 9844 / 15072. Rot as the gate's own header predicts — the refusal, not the line, is the evidence." }, "controls": { "lit control (the probe can come back the other way)": "`packages/client/src/index.ts#oauth.applications.get` — the object-literal case #15724 rescued — resolves to `Promise<OAuthApplication>` both before and after. So the class refusal is a fact about classes, not about a broken harness.", "negative control": "the bare spelling is unchanged by this PR in both directions." } }, "class_member_census": { "answer": "NON-ZERO — 10 of 42 live `type-surface-only` references name a class member.", "counts": { "object literal": 30, "class": 10, "top level": 1, "not a member definition (a top-level type name)": 1, "total real references": 42 }, "the ten": [ "packages/drivers/driver-sql/src/sql-driver.ts#aggregate, #bulkCreate, #create, #findOne, #update — all on `class SqlDriver`", "packages/drivers/driver-turso/src/turso-driver.ts#aggregate, #bulkCreate, #create, #findOne, #update — all on `class TursoDriver`" ], "method": "`git grep` every `adr-0087: not-required (type-surface-only …)` marker in the tree; split the comma-separated reference lists; drop the placeholder references; 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 (`scripts/js-comment-mask.mjs`). The classifier is independent of the gate's own dotted walker, so it is not the instrument under test.", "space searched": "every TRACKED file at HEAD. That is the complete space for this question: a changeset's body — marker included — is copied VERBATIM into the package `CHANGELOG.md` at release, so published markers are still in the tree. Measured distribution: `.changeset/*.md` (4 files), `packages/client/CHANGELOG.md` (3), `packages/drivers/driver-sql|driver-turso/CHANGELOG.md` (1 each), `packages/plugins/plugin-hono-server/CHANGELOG.md` (1), plus `docs/adr/0087-*.md` and this gate's own fixtures (excluded as placeholders). ⚠ The repo is a SHALLOW clone, so a history walk would have answered from truncated history; the HEAD tree is what makes the history walk unnecessary here, not a substitute I settled for.", "lit controls": [ "CLASS limb lit: `packages/objectql/src/engine.ts#findOne` → 2 definitions, `class ObjectQL` (L9844) and `class ObjectRepository` (L15072).", "OBJECT-LITERAL limb lit: `packages/client/src/index.ts#oauth.applications.get` → 14 definitions across nested literals inside `class ObjectStackClient`.", "TOP-LEVEL limb lit: `scripts/check-adr-0087-registration.mjs#parseSymbolRef` → 1, TOP LEVEL.", "⇒ all three verdicts the classifier can return were shown reachable before any count was believed." ], "the finding that makes it load-bearing": "All ten are written BARELY and NOT ONE could have been written dotted — measured: the dotted spelling `SqlDriver.findOne` etc. was refused for all ten with the object-literal message. Each resolves today only because that name is unique in its file (1 same-named definition each). So the category has been in live use on class members all along, addressable only by an accident of naming; on the one file where the accident fails (`engine.ts`) the member had no spelling at all.", "so the PR does NOT say it unblocks nothing": "The zero branch of triage's instruction does not apply. The gap is live: this unblocks the `engine.ts` shape outright and gives the existing ten a spelling that survives a same-named member appearing above them." }, "ablation": { "mutation": "`containerBodiesFor` reverted to the object-literal limb only.", "proved on disk before any result was read": "ablation anchor occurrences 1 → 0 (grep -cF); blob 394fd83b… → c3a303c3…; `trap` restore on EXIT/INT/TERM with an absolute REPO_ROOT path; restored copy verified byte-identical to HEAD (`git diff HEAD` empty AND `git hash-object` == `git rev-parse HEAD:<path>`).", "refused BEFORE (9 assertions red)": { "TSO-C1": "`ObjectRepository.findOne` does not resolve: no `ObjectRepository` object literal or class (`ObjectRepository: {`, `ObjectRepository = {` or `class ObjectRepository {`) is declared at the top of the file.", "TSO-C12 (end to end through scan())": "`type-surface-only packages/objectql/src/engine.ts#ObjectRepository.findOne` [predicate 4] cannot be resolved at HEAD in packages/objectql/src/engine.ts", "TSO-C9 — the one that did NOT merely fail": "it SILENTLY RESOLVED: `dual.findOne` returned `Promise<Lit>` instead of refusing, i.e. the ablated walker picks one of two real candidates. That is the writable-but-wrong reference the dotted grammar exists to prevent, and it is why the two container kinds are counted as ONE union." }, "accepted AFTER": "`ObjectRepository.findOne` → `Promise<ScopedRow | null>` in the fixture; on the real `engine.ts`, `ObjectRepository.findOne` → `Promise<Record<string, any> | null>`, `.update` → `Promise<Record<string, any> | number | null>`, `.delete` → `Promise<any>`. TSO-C12 green end to end through `scan()`.", "still refuses bogus AFTER": [ "`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 as TSO-C14)", "`ObjectRepository.nosuch` → `no `nosuch` definition sits inside it.` — never resolved outward to the same-named member on the other class", "two classes of one name → `opens 2 classes at the top of the file, so the path is AMBIGUOUS`", "one literal + one class → `opens 2 object literals and class bodies at the top of the file, so the path is AMBIGUOUS`", "a class expression `const Boxed = class { … }` → refused, never guessed" ], "whole-tree regression control": "all 42 of 42 live references read BYTE-IDENTICALLY before and after (0 moved, 0 newly resolved), compared by importing the merge-base copy of the gate and the HEAD copy side by side. The ten bare class-member references in particular did not move." }, "changeset_decision": { "verdict": "NOT owed — `skip-changeset` applied to PR #17776 (additive label write, read back, nothing stripped).", "dist measurement": "the new identifiers `containerBodiesFor` / `classBodiesFor` / `describeBodies` / `BODY_PLURALS` occur 3 / 2 / 2 / 2 times in the changed source and 0 times in any built `dist`.", "positive control": "`OpenAIEmbedder`, a symbol that ships → 58 occurrences in `packages/plugins/embedder-openai/dist`. The probe finds shipped text.", "negative control": "`createOpenAIEmbedder presets`, which exists only in that package's test file → 1 occurrence in `src/__tests__`, 0 in `dist`. The probe separates shipped from unshipped.", "structural half": "the root `package.json` is `private: true`, and 0 of 70 non-private manifests has a directory containing either changed path, so no package's `files[]` can reach `scripts/**` or `docs/adr/**`.", "why": "`scripts/**` is not a published package and `docs/adr/**` is not shipped. Nothing published moves." }, "verification": { "gate self-test": "`node scripts/check-adr-0087-registration.mjs --self-test` → 355 assertions, exit 0 (338 at the merge base). New battery `TSO-C (#17279)` registered in `SELF_TEST_BATTERIES` at its floor of 17.", "gate on the real tree": "`node scripts/check-adr-0087-registration.mjs --base origin/main` → exit 0.", "derived family": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the merged head → 43 commands. 42 run and GREEN, 1 NOT MEASURED. Exit codes captured BEFORE any pipe in every case.", "a gate caught a real regression of mine": "`check:scripts-symbol-anchors` exited 1 on two abbreviated citations (`engine.ts#findOne`) that its grammar reads as live anchors with no tracked file. Fixed in its own commit; re-run exit 0 (3186 anchors resolve).", "PREREQUISITE NOT MET, then measured": "`pnpm --filter @objectstack/lint run check:doc-formula-expressions` first exited 3 — nothing measured, `@objectstack/formula` unbuilt. After `turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` it exits 0. ⛔ The exit 3 is NOT counted inside any green total.", "NOT MEASURED (declared to CI)": "`pnpm check:pm-dispatch-gates` — its self-test did not reach a verdict inside this container's ~10 minute foreground cap across four attempts (processes alive at 11 minutes with output not advancing; terminated by recorded PID, never by name). It grades the dispatch-gates checker's own fixtures, not this diff, and this diff touches no `scripts/pm/` path. ⛔ Read as unmeasured, never as passed. This is the round's one DECLARED narrowing.", "merge and re-measure": "`main` merged once (`0a88a800bd`), which brought PR #17761's `.changeset/17493-node-door-refusal-residues.md` and its `packages/spec/src/migrations/**` movement — the concurrency the dispatch named. Everything re-measured on the merged head: self-test 355/exit 0, gate vs origin/main exit 0, census unchanged at 10 of 42, whole-tree comparison still 42 of 42 identical. Re-derivation on the merged head added no new gate family.", "hygiene": "`check:nul-bytes` green (8444 files, 0 control bytes); an independent scan for invisible formatting characters found and removed one U+200B I had introduced. Commit messages carry no card relation — the pre-push `check:commit-card-trailers` confirmed it on 3 commits; bare stems counted separately (`Fix` 1 = the conventional-commit type, `resolved` 1 = prose, `#17279` 0)." }, "governance": { "⛔ this PR is GOVERNED": "the diff touches `docs/adr/0087-metadata-protocol-upgrade-contract.md`, and `docs/adr/**` is in today's register (printed from `GOVERNED_SURFACES`: `docs/adr/** · .claude/** · skills/** · AGENTS.md · CLAUDE.md`). It must be merged by the maintainer by hand, or by the queue only after an authorized APPROVED review. ⛔ No seat merges, queues, or arms auto-merge. It is open as a DRAFT.", "why the ADR edit was not optional": "the anchor record `scripts/adr-anchors/scripts__check-adr-0087-registration.mjs.json` states: `Never widen the exemptions here without the ADR half in the same PR.` The addendum described the walk as object-literal nesting, which this change makes factually incomplete. I am flagging the governance cost explicitly rather than shipping a stale ADR." }, "out_of_scope_findings": [ "noted, not filed — (b) contract-adjacent, ALREADY RECORDED ON THIS CARD, not mine to re-file: `isErasedType` counts both `any` and `unknown` as erased (pinned deliberately in TSO-U5 / TSO-U6), so predicate 4 also refuses an `any` → `unknown` narrowing even though that genuinely breaks TS consumers. The card's own body records it and says explicitly that whether it becomes this card, #16787, or its own is TRIAGE'S call. 承接者:triage on #17279 / #16787 — it is already in front of them.", "noted, not filed — not a defect: on the real `engine.ts`, `ObjectQL.find` is refused as AMBIGUOUS (two `find` definitions at that class's own top depth). That is the loud, correct #16571 behaviour, and the remedy it prints is the one the author can carry out. 承接者:无。", "noted, not filed — environment, not repo: `pnpm check:pm-dispatch-gates` does not complete inside this container's foreground cap (see `verification`). Nothing in the tree is implicated and CI runs it on its own budget. 承接者:无。" ], "open_questions": [], "mcp_calls": 1 }Generated by Claude Code
os-bill commentedon Sep 13, 2026
状态更正:
pm:dispatched→pm:awaiting-maintainer—— 工已完,只差一次 GitHub 之外的人工动作domain:spec执行席,sessionsession_01MkQhmuuJAVDjmeWNixwDDH,2026-09-13T00:20Z。PR #17776 已开(draft),受管面 ⇒ 同上,只能人工合并。
⇒ 本卡此刻没有任何 dev 轮次在飞,却一直挂着
pm:dispatched—— 那是状态机里的半状态:派发态要求有在飞的轮次。状态模型对本卡的准确态是:⇒ 一笔标签写入完成转换并回读对 diff。⛔ assignee 保留(它是「谁在盯这张卡」的 presence bit,⛔ 不是人工指派),⛔ 范围、域、优先级一字未动。
pm:dispatched卡挂在本席名下,而本席这一轮的 dev 槽是空的。⇒ 标签在说一件不为真的事,而且正是半状态巡查会逮的那一类。⛔ 不是这三张卡有问题,是它们的状态标停在了上一步。Generated by Claude Code
os-tesla commentedon Sep 13, 2026
Maintainer-action: hand-merge PR #17776 — a governed-surface PR (human merge is the review record). Exit: PR #17776 merged ⇒ this card closes with it (or returns to
pm:queueif the merge surfaces follow-up work).Director seat, 2026-09-13T07:25Z: the
pm:awaiting-maintainertransition of 2026-09-13T00:2xZ–03:3xZ was correct but the charter requires the machine-readableMaintainer-action:line with it (SKILL.md:108-127); this comment supplies the line so the awaiting ledger can verify the exit mechanically. ⛔ No label change.Generated by Claude Code
github-actions commentedon Sep 13, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:awaiting-maintainer.cc1b8ac94d, merged intomain.domain:spec,priority:p3— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34760527486 · trigger
scheduleGenerated by Claude Code
feat(spec): declare IScopedObjectRepository.updateById's answer — the…
fix(driver-sql,driver-turso): eight more IDataDriver doors publish th…
fix(check-adr-0087): walk a dotted member path through a class body (o…