fix(scripts): a dotted member path's last segment resolves at the region's top depth - #17001
Merged
Merged
Conversation
…on's top depth `check-adr-0087-registration`'s dotted `type-surface-only` walker narrowed the search to a nesting region's braces and then counted EVERY same-named definition inside it, at any depth. A DIRECT member of an object literal whose name recurs in a nested literal was therefore counted against itself, refused as AMBIGUOUS, and told to "name a deeper path that resolves to exactly one" -- an instruction that cannot be carried out, because the member already sits at the depth the path names. Such a member had no addressable spelling at all and could only be left out of the marker and described in unchecked prose, which is the failure the dotted grammar was widened to abolish. The last segment now resolves at the region's TOP DEPTH only: a definition whose name starts inside the region but inside a nested object literal opened within it belongs to the deeper path. This is a NARROWING of the candidate set, never a tie-break -- the direct member resolves because the nested one stopped being a candidate for its path, so "take the first" stays unreachable and every refusal stays loud. A collision at the path's own depth is still refused, now without prescribing a deeper path that does not exist. Measured on `packages/client/src/index.ts`: 5 of the 19 `organizations.*` refs went refused -> resolved, each to its own annotation; the other 14 did not move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
…tted-path-top-depth
baozhoutao
marked this pull request as ready for review
September 9, 2026 02:00
baozhoutao
enabled auto-merge
September 9, 2026 02:00
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.
Part of #16571
The card is fully implemented here; the relation is
Part ofand not a closing keyword becauselanding and the card's own closure are the PM seat's, per its dispatch. Nothing of the card's
acceptance is left over for a follow-up.
The defect: the gate broke its own declared grammar
parseSymbolRef's header declares that a member which is not unique in a file is named"as a DOTTED MEMBER PATH through the object-literal nesting the member sits in", and the refusal
in
readTypeSurfaceReftold the author to "Name a deeper path that resolves to exactly one."resolveMemberPathnarrowed the search to the named nesting's braces, andmemberDefinitionsInthen collected every same-named definition inside that region at any depth. A direct member
whose name recurs in a nested literal was therefore counted against itself:
The two definitions are
organizations.create(direct) andorganizations.teams.create(nested).Only the nested one was addressable. For the direct one the printed remedy is unsatisfiable — it
already sits at the depth the path names, and no deeper path exists to write. Such a member could
only be left out of the marker and described in unchecked prose, which is exactly the failure mode
the dotted grammar was widened to abolish.
The fix: the last segment resolves at the region's TOP DEPTH
New
definitionsAtTopDepth(structural, defs, from, to)— a definition whose name starts inside[from, to)but inside a nested object literal opened within that region belongs to the deeperpath, not to this one.
objectLiteralBodiesForalready computes every named literal's[open, close], so it is reused withIDENTas the name: every named literal counts, and the spansof literals nested inside those are contained in them, so one pass suffices.
Two properties this deliberately keeps:
the nested one stopped being a candidate for its path, not because ties are now broken by
position. "Take the first" stays unreachable from here.
(
resolveMemberPath, TSO-D12, untouched). A name with no top-depth definition and several nestedones is still refused with its count and still told to name a deeper path — because there a deeper
path does exist (TSO-D13, untouched).
One refusal changed on purpose: a collision at the path's own depth is still refused, but no
longer prescribes a deeper path that cannot be written —
`X.y` is AMBIGUOUS: N `y` definitions sit at the TOP DEPTH of `X`, so no deeper path distinguishes them.This is the same contract breach the card was filed on, at the branch thecard did not reach; it is repaired in the same pass rather than left as a second card.
Acceptance
Both directions pinned, in one battery (TSO-D18 .. TSO-D23)
Placed beside TSO-D12/TSO-D13, on a fixture where
organizationsholds a directcreateand anested
teams.create.listis left alone so TSO-D13 keeps its own fixture.organizations.createresolves to the direct member (Promise[Organization])organizations.teams.createresolves to the nested member (Promise[Team])organizations.list(0 top-depth, 2 nested) is still refused with its countorganizations.nosuchmemberis still refused by the same named findingorganizations's own depth are still refused, and the message does not say "Name a deeper path"Battery floor ratcheted
21 -> 27(measured, not assumed: the floor was temporarily set to 999 andthe gate printed
registered 27 case(s)).SELF_TEST_BATTERY_FLOOR(roster size, 50) unchanged.Re-measured by symbol on the current tree — NOT the card's number
The card measured 5-of-19 at
b66530f33c; that SHA and its line numbers have drifted. Re-measuredby symbol against
packages/client/src/index.tsat merge basefe2b7554a5, over #14314's nineteenorganizations.*members (the 14 its marker spells plus the 5 its prose names as unspellable):The card's 5-of-19 reproduces exactly. The five are
organizations.create,.update,.listMembers,.delete,.removeMember; after the fix each resolves to its own annotation —OrganizationCreateResult,OrganizationEchoWire,OrganizationMembersPage,OrganizationWire,OrganizationRemoveMemberResult— matching #14314's own table row for row, and the other fourteendid not move.
organizations.teams.createstill readsOrganizationTeamWire, so the pair isdistinct on the real file too, not only in the fixture.
Controls beside that zero, on the same real file and the same run:
Both refuse identically before and after, so the
0above is a reading and not a reader thatresolves everything.
Ablation — the pins can fail, proved on disk
Both legs mutate the committed file, prove the mutation reached the disk by grep-counting the
anchor, run the gate, and restore with
git checkout HEAD -- PATHverified by blob-hash equalityagainst
git rev-parse HEAD:PATHplus an emptygit diff HEAD. Both scripts carrytrap RESTORE EXIT INT TERMwith absolute paths. Neither leg is left in the tree.Ablation 1 — remove the narrowing (
const here = definitionsAtTopDepth(...)->const here = defs):Ablation 2 — a "collapse" reader (
return defs.filter(...)->return defs.slice(-1), i.e.every path takes the deepest same-named definition):
Ablation 2 is the one that earns TSO-D20 its place: under a collapse reader TSO-D19 stays green
while TSO-D18/D20 go red. A one-direction pin really would have passed a reader in which every
spelling lands on the same definition — the acceptance criterion, measured rather than asserted.
Verification
Gate families derived mechanically from the change set rather than listed by hand:
All 34 run locally, all exit 0 — including
check:adr-0087-registration --base origin/main,check:scripts-symbol-anchors,check:comment-mask-corpus,check:declaration-mirrors,check:self-test-wired,check:changeset-gate-self-tests,check:ratchet-remedy-authority,check:nul-bytesandcheck:parse-guard. The tool's own caveats are carried, not swallowed: 10families declare a population too wide to place, 2 take a value from the workflow
(
NOT MEASURED— shard attestation and test completeness), 8 apply only once a changeset pathexists, and 45 artifact rosters score
silentfor every card. None of those is read as a clearance;CI owns them.
origin/mainmoved while this ran, so the branch was merged forward and the derivation re-taken onthe merged tree: the stale-tree warning clears, the change set is still the same single path, and the
34-command list is byte-identical to the one that was run. Every ratchet-bearing and self-test family
was then re-run on the final head
6c4b549123and stayed green.Lint is the WHOLE repo, not a narrowing. The full
pnpm lintsurface fits in one foreground runhere, so no scoping argument is needed:
eslint . --no-inline-config --format jsonat6c4b549123linted 6,392 files, 0 errors, 0 warnings, exit 0, and the changed file is present in that
population (verified by filtering the JSON report for its path, exactly one entry, 0/0).
No control characters:
pnpm check:nul-bytesexits 0, and an independent scan of the changed filefinds none (
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', exit 1, zero bytes of output).No changeset: measured, not assumed
scripts/**at the repo root is on the fast track — the root package isprivate: truewith nofiles[], and no package underpackages/**namescheck-adr-0087-registrationin itsfiles[](grep over every
package.json, zero hits). Nothing published moves, so the PR carriesskip-changesetrather than a.changeset/*.md.pnpm check:objectui-changesetexits 0.This PR also does not touch #14314's changeset, which is where the defect was hit and is not
part of this card. With this fix landed, the five members it describes in prose become spellable and
whoever owns that card may move them into the marker — that is a separate edit on a separate card.
验收备注
Out-of-scope observations, filed nowhere and recorded here for the accepting seat:
card names, the
defs.length > 1refusal also printed "Name a deeper path" for a collision at thepath's own depth, where none exists. It is fixed in the same pass because it is the same defect at
the same call site, not a separate finding. Noted, not filed. Carrier: this PR.
check-partof-closing-keyword's guidance is unsatisfiable on an already-pushed branch — it asks for reworded commits AND forbids history rewrites, and its red is permanent for that branch however the author responds #16653 is the same class and this is the second instance, not a third. Read once as dispatched.Its shape is identical (
check-partof-closing-keyword's diagnosis is exact, its remedy asks for aforbidden history rewrite); it is a different file, a different lane and a different fix, and I did
not widen this PR toward it. The extra data point above — the same gate carrying the same defect at
two branches — argues the rule the triage seat floated ("a gate's remedy text must be executable,
and a case must pin it") is worth more than two instances suggest. Noted, not filed; filing is the
PM's. Carrier: whoever takes [finding]
check-partof-closing-keyword's guidance is unsatisfiable on an already-pushed branch — it asks for reworded commits AND forbids history rewrites, and its red is permanent for that branch however the author responds #16653.What a reviewer should check
definitionsAtTopDepthnarrows, it does not tie-break — read it beside TSO-D21, which is theassertion that would go green if it ever did.
origin/main; the diff adds cases beside them andchanges neither.
here.length > 1message intentionally drops the "Name a deeper path" remedy. TSO-D23 pinsits absence, not merely the refusal.
Generated by Claude Code