fix(check-adr-0087): walk a dotted member path through a class body - #17776
Draft
os-bill wants to merge 4 commits into
Draft
fix(check-adr-0087): walk a dotted member path through a class body#17776os-bill wants to merge 4 commits into
os-bill wants to merge 4 commits into
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 12, 2026
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