feat(devx): accept a dotted member path in ADR-0087 type-surface-only references - #15724
Merged
Merged
Conversation
… references A `type-surface-only` reference was a bare identifier resolved to the FIRST same-named definition in the file. On `packages/client/src/index.ts` that is not a symbol identity: the file declares `get` 14 times and `delete` 10 times, so the members two PRs actually narrowed had no addressable spelling at all, and the gate answered a true sentence about a member the diff never touched. One card's whole category was closed by the reference grammar rather than by a judgement about the claim. The grammar now also accepts a dotted member path, and the READER was widened in the same commit: a dotted reference is resolved STRUCTURALLY through the object-literal nesting it names, walked from the top of the file over the shared comment- and literal-masked projection with offsets preserved, and the member's definition is taken from inside the resolved body. Zero candidates, or more than one, is reported by name and refused -- never guessed at. A line number is never the disambiguator; this file's line numbers were measured to rot within one day. Bare identifiers keep their exact previous behaviour, including the first-same-name reading, which is what every marker written before this means. `runtime-interface-only` refuses a dotted path by name: it reads a top-level type declaration, which has no nesting to walk. ADR-0087's marker grammar and this gate's accepted grammar are pinned to each other, so both example blocks and the predicate-4 prose move in the same commit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
This was referenced Sep 5, 2026
os-zhuang
approved these changes
Sep 5, 2026
os-zhuang
marked this pull request as ready for review
September 5, 2026 10:23
os-zhuang
enabled auto-merge
September 5, 2026 10:23
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 #15627
Unblocks #15451
A
type-surface-onlyreference was a bare identifier resolved to the first same-named definition in the file. Onpackages/client/src/index.tsthat is not a symbol identity: the file declaresget14 times anddelete10 times. So the members PR #15445 and PR #15451 actually narrowed had no addressable spelling at all, and the gate answered a true sentence about a member the diff never touched. On #15451 that closed the category outright — there was no other member to name — so a p1 declaration was blocked by the reference grammar rather than by any judgement about the claim.Grammar
parseSymbolRefnow acceptsPATH+#+ a dotted member path (IDENT, then any number of.IDENT), in addition to the bare identifier. It returnssymbol(the last segment — the member's own name),segments(the whole path) anddotted. An empty segment (a..b,a.,.a) is still refused: this is a dotted IDENT chain, not "anything with a dot in it". Bare identifiers keep their exact previous behaviour, first-same-name reading included — that is what every marker written before this means.Reader — structural, never a line number
⛔ A wider grammar over the old first-same-name reader would be writable but wrong, which is strictly worse than the refusal it replaces. So the reader moved in the same commit.
A dotted reference is resolved by walking the object-literal nesting it names, from the top of the file, on the shared comment- and literal-masked projection (
scripts/js-comment-mask.mjs, offsets preserved) withmatchBracketfor the bodies:oauthtoapplicationsnarrows the search to the bytes between that literal's braces, and the member is looked for there.Parent segments are matched as
name: {(an object-literal property) orname = {(a class field holding one) — both spellings this SDK client actually uses. A leading dot is excluded, sothis.oauth = {reached through a receiver is not claimed.The member's definition is read by
memberDefinitionsIn, the extracted core ofmemberReturnAnnotation, which reads exactly these declaration shapes and no others:followed, after the balanced parameter list, by a return annotation, an arrow or a body brace. A
(...)with none of those after it is a call, not a declaration.Zero candidates, or more than one, is reported by name and refused — never guessed at. ⛔ A line number is never the disambiguator: the two measurements on the card, one day apart, disagree about every line number in that file.
runtime-interface-onlyrefuses a dotted path by name: it reads a top-level exported type declaration, which has no nesting to walk, and silently readinga.b.ResultasResultwould be the same defect one category over.Red/green on the REAL victims
packages/client/src/index.ts(read-only here), driven through the module's exported functions.Before, on
origin/mainatcd555587e— refused by the grammar, before any reading happens:Both bare readings are about members the two PRs never touched (the first of 14
gets and the first of 10deletes).After — the dotted path reads the annotation at the nested definition:
oauth.applications.deleteis unannotated at this tip — that is what it reads, quoted as measured; #15451 is the PR that annotates it, and its base-side reading is exactly this. Note the verdict now names the dotted spelling: a message aboutdeletewhen the marker saidoauth.applications.deleteis the reading this card was filed on.The negative control — bare behaviour is unchanged:
still the first definition in the file, byte-identical to before.
And the named refusals, on the same real file:
Not a one-off for
oauth.applications.*Any depth, and same-named members under different parents resolve separately. On the real file,
organizations.invitations.list(line 2947 today) andorganizations.teams.list(3062) each resolve to their own definition — the shape #14313 (auth.*) and #14314 (organizations.*) bind. The self-test pins it on a fixture where the two carry different annotations, so "both resolved" is distinguishable from "both landed on the same one":ADR and gate move together
documentedCategoriesreads ADR-0087's own example lines, so the two are pinned to each other. Bothtype-surface-onlyexample blocks indocs/adr/0087-metadata-protocol-upgrade-contract.mdgain the dotted spelling as an alternative line (pure insertions — no existing line is rewritten), and predicate 4's prose gains a paragraph stating the grammar, the structural resolution, the line-number prohibition and the zero/several refusal.check-adr-0087-registration --self-testV3 (real ADR againstCATEGORIES, both directions) stays green, andcheck:adr-links/check:adr-symbol-anchors/check:adr-anchorsare green.Self-test
New battery
TSO-D (#15627), 21 assertions: the grammar half (D1-D4), the reader half with the negative control (D5-D8), the two-parents case (D9), the four refusals (D10-D13), and four end-to-end legs through the shippingscan()— the dotted marker green (D14), the bare spelling still landing on the untouched first definition and refusing (D15), an unresolvable dotted path refused by predicate 4 (D16), andruntime-interface-onlyrefusing a dotted path (D17).Roster floor raised 48 to 49, with the new battery declared at 21.
Ablation
Trap-guarded, absolute paths, both legs proven on disk. Mutation: keep the widened grammar, revert the resolver to the first-same-name reader — the exact shape the card forbids.
The ablated victim readings are the point: they are not errors, they are well-formed wrong answers.
Gates
Derived on the final head
0a3b91735withnode scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack(family byte-identical to the earlier derivation): 40 commands, all exit 0, exit codes captured before any pipe. Includescheck:adr-0087-registration(self-test and--base origin/main),check:changeset-gate-self-tests,check:adr-anchors,check:adr-links,check:adr-symbol-anchors,check:comment-mask-corpus,check:self-test-wired,check:parse-guard,check:entry-guard,check:doc-authoring,check:pm-governed-merges,check:nul-bytes,check:pm-dispatch-gates.Verdict lines quoted from the final-head runs:
Declared narrowings
pnpm lint: narrowed to the changed files. Receiving population read from eslint's own config, not guessed: this repo runs oneeslint.config.mjs, which never enables type-aware linting for any file (noparserOptions.project, no typed rules — stated in that config's own header). File count read from--format json: 1 file linted forscripts/check-adr-0087-registration.mjs, 0 errors 0 warnings; the ADR reportsFile ignored because no matching configuration was supplied, so eslint's own config excludes it. Invariance: with no type-aware linting, this diff cannot move the verdict on any file it does not touch. CI runs the full sweep regardless.os-verify-lock.sh—VERDICT command-exit 0 · held the lock 592s. Two later attempts hitVERDICT queue-timeout (exit 99) · never acquired · waited 540sbehind another card's holder (recorded as NOT MEASURED, not as red). The remaining ten single-file node gates and the ablation were then run unlocked — the lock excludes only other locked runs, never thecheck:*gates themselves, and what is read here is exit codes rather than wall-clock.NOT MEASURED
PREREQUISITE NOT MET(fresh worktree, nonode_modules). All nine were re-run afterpnpm install(and, forcheck:doc-formula-expressions, after building@objectstack/formulaand@objectstack/lint) and are counted only from those runs.check-cross-package-test-inputs,check-shard-attestation,check-test-completenesswith$RUNNER_TEMPargv) and cannot run outside CI.silentfor every card in the tree; 6 of them keep their roster underscripts/. Their silence is not a clearance in either direction.Scope
No changeset:
scripts/anddocs/adr/publish nothing from any package, soskip-changesetis applied.packages/client/src/index.tsis read-only here. Adjacent but deliberately not folded in: #15489 (landed) and #14502.Governed surface (
docs/adr/**): draft, human merge — review requested from os-zhuang and hotlong🤖 Generated with Claude Code
https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
Generated by Claude Code