Skip to content

fix(tooling): grade a documented call reached through a property path, so the client namespace tour stops being invisible to both halves - #16837

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16209-readme-new-receiver-calls
Sep 8, 2026
Merged

fix(tooling): grade a documented call reached through a property path, so the client namespace tour stops being invisible to both halves#16837
baozhoutao merged 1 commit into
mainfrom
claude/issue-16209-readme-new-receiver-calls

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #16209

The card's premise is false, and the correction is the fix

The card asked for a receiver bound by new X(…) to be resolved by the call-site half. Re-measured on today's main before anything was changed, that mechanism already exists — it is extractLocalBindings (#9870), and client was already one of the name(s) built from one the green line counts.

The card also states that every call in packages/client's namespace tour sits in the NOT read bucket. It does not. That document contributed three unread sites, and all three are console.log, console.error and Math.pow:

   3 unread call(s) /  2 unread receiver(s)  packages/client/README.md
      line  51  console.log(…)
      line 149  console.error(…)
      line 190  Math.pow(…)

The tour's calls were in neither half. Every one is client.NAMESPACE.METHOD(, and the matcher took exactly one dot between receiver and member — so the second dot put the member behind the leading-boundary lookbehind and the site matched nothing at all. Invisible to the checked half, and invisible to the half whose entire job is to publish what the checked half missed. That is strictly worse than an unread site, and it is why adding an import to that fence moved the import half by one and the call-site half by zero: exactly as the card measured, and exactly not for the reason it gave.

What changed

scripts/check-published-readme-exports.mjs, and nothing else.

  • memberCallSites — one walk, carrying the property path between receiver and member. Both halves are now filters over it: extractMemberCalls keeps the sites whose receiver is typeable, unreadCallSites keeps the rest. They previously carried a byte-identical regex, fence skip and dedup in two places, so the split the green line prints was held together by nothing but both copies being edited together.
  • memberAtPath on the type surface — walks the path from a receiver type the gate already resolves, one propertyHop per segment. A hop that is not knowable stops the walk with no finding; a hop that is knowably absent is the finding, and it names the segment that failed rather than the member at the end.
  • receiverType — the via resolution (instance construct-signature, return awaited call-signature, no-via static side) lifted into one place, so the three member questions cannot disagree about what via means. hasMemberVia is now three lines over it.
  • ⛔ The NOT read line, its wording, and what it reports are untouched. Nothing was narrowed and nothing was silenced.

Refusals kept, each with its population stated in the source: a call expression anywhere in the path stays out (#9610's fence, unchanged); a namespace receiver refuses the chain rather than defaulting through it (#10367 measured that population at zero); a target that answers hasMemberVia but not memberAtPath binds no derived receiver at all, the same both-or-neither rule #9870 paid for once.

Measurements

Whole-repo summary line, this tree, before and after. ⛔ Not the card's figures — the card's are from 2026-09-06 and main has moved.

before after
published documents 61 61
import half 326 326
call-site half, checked 78 120
import-bound names 244 244
names built from one 49 49
NOT read calls 122 133
NOT read receivers 74 75

The blind-spot pair goes UP, and that is the point. The rise of 11 is chained sites on untyped roots (this.connections.set(…), ctx.logger.info(…), kernel.ql.find(…)) that were previously invisible to both halves. Nothing about what the bucket reports was narrowed.

Receiver-shape census of the pre-change bucket, 122 calls on 74 receivers: free variables 24, globals 19, other initialisers 19, built with new from a class the fence did not import 6, parameters 3, call returns 2, destructured 1. ⇒ new X(…) off an import-bound X contributes zero to that bucket — it was never the blind spot.

Chained-site census, 53 repo-wide: 42 on a root the gate already types, 11 on roots it does not.

Red before green — the card's own executable criterion

#16142 is closed completed, so its defect is gone from main and had to be re-created. The three lines are the exact bytes #16212 removed.

HEAD blob            = 63c0e95c14e4377ccd98e994560e828654c509bd
nlq count BEFORE     = 0        (expect 0)
nlq count AFTER      = 1        (expect 1)
suggest AFTER        = 1    (expect 1)
insights AFTER       = 1   (expect 1)
mutated blob         = 98af2c4c16a4764f9626c7c8fa23f99026ce5fca
GATE EXIT ON MUTATED TREE, BEFORE THIS PR = 0
  lines naming ai.nlq (before) = 0
GATE EXIT ON MUTATED TREE, WITH THIS PR   = 1
    line 268: documents `client.ai.nlq(…)` on a value the fence builds with `new ObjectStackClient(…)`, but `client.ai` has no `nlq` member in the published types ObjectStackClient (from '@objectstack/client') reaches.
    line 269: documents `client.ai.suggest(…)` … no `suggest` member …
    line 270: documents `client.ai.insights(…)` … no `insights` member …
restored blob        = 63c0e95c14e4377ccd98e994560e828654c509bd
equals HEAD blob     = YES
nlq count RESTORED   = 0        (expect 0)
git diff HEAD        = []

Exit 0 before, exit 1 after, naming ai.nlq. The gate's own --self-test was rerun on the pristine copy for leg A rather than trusted from prose; the mutation was proven on disk by blob hash before either leg ran, and the restore by blob equality plus an empty git diff HEAD.

The fallout, audited here as triage required

Turning this on newly grades 42 sites. Every one passes — the gate exits 0 on the clean tree.

document newly checked newly visible as unread
packages/client/README.md 36 0
packages/services/service-i18n/README.md 3 0
packages/services/service-storage/README.md 3 0
packages/runtime/README.md 0 8
packages/types/README.md 0 2
packages/core/README.md 0 1

The tour's whole namespace surface is now graded against the published .d.ts, client.ai.conversations.list and client.ai.agents.chat included (depth-3 paths).

验收备注

  • One call in that README remains ungraded, by construction: line 60 writes client.data.find with explicit type arguments, and the matcher's path is identifiers and dots only, so the angle brackets end it. The same member is graded in its plain form at line 233, so find is verified to exist; only that one spelling is unreached. Noted, not filed — it is a known fence of the matcher, not a defect, and no PR or person is queued on that file.
  • The second defect the card cites — approvals.approve(requestId, 'LGTM'), TS2559 — stays out of reach by construction. client.approvals.approve is now checked for member existence and passes; argument types are a question this gate deliberately does not parse (it does not read arguments anywhere). That card is unaffected either way.
  • --unread-report rows now print a site by its full key rather than receiver.member, so a chained unread site is not reported as res.json(…) when the document says res.body.json(…).
  • skip-changeset, measured rather than assumed: the one changed file is a root-level dev script in the private root package. Grepped the symbol memberAtPath across the 195 existing paths named by every published package's files[]0 hits; positive control ObjectStackClient on the same paths — 42 hits. Nothing published moves.

Verification

  • node scripts/check-published-readme-exports.mjs --self-test — exit 0. New battery THE PROPERTY PATH, and the partition it must preserve (#16209), 10 cases, registered in SELF_TEST_BATTERIES with the roster floor raised 25 to 26. It pins the matcher's new admission and its unchanged refusals, ⭐ the partition (every matched site lands in exactly one half — measured, not read off the source), memberAtPath in both directions, the intermediate-hop case that decides which token an author is sent at, the both-or-neither refusal, and the two zero-population arms (static-root chain, namespace-root chain) over fixtures.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 31 gate families from the actual changed set; all 31 run, all exit 0; --ran reconciles 31 derived / 31 run / 0 NOT-MEASURED / 0 UNRUN.
  • npx eslint . --no-inline-config --format json — the full repo-wide union, not a narrowing: exit 0 over 6356 files, 0 errors, 0 warnings, at 17ebeb7b8d.
  • No pnpm test / pnpm typecheck owed: the diff touches no package.

Generated by Claude Code

…client namespace tour is graded (#16209)

`check:published-readme-exports` matched exactly one dot between receiver and
member, so `client.ai.chat(…)` matched nothing at all — not the checked half,
and not the `NOT read` half either. `packages/client`'s namespace tour, the npm
front page of `@objectstack/client`, contributed three unread sites, all of them
`console.log`/`console.error`/`Math.pow`, and none of its ~30 documented calls.

The matcher now carries the property path between receiver and member, and
`memberAtPath` walks it from a receiver type the gate already resolves, naming
the segment that fails rather than the member at the end. Both halves became
filters over one walk (`memberCallSites`), so the split the green line prints
cannot stop being a split.

Measured on this tree: call-site half 78 -> 120, `NOT read` 122 -> 133. The pair
RISES by the 11 chained sites on untyped roots that were previously invisible to
both halves; nothing about what the bucket reports is narrowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@github-actions github-actions Bot added the size/l label Sep 8, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 8, 2026 10:18
@baozhoutao
baozhoutao enabled auto-merge September 8, 2026 10:18
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 8, 2026
Merged via the queue into main with commit 1692157 Sep 8, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-16209-readme-new-receiver-calls branch September 8, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants