fix(tooling): grade a documented call reached through a property path, so the client namespace tour stops being invisible to both halves - #16837
Merged
Conversation
…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
baozhoutao
marked this pull request as ready for review
September 8, 2026 10:18
baozhoutao
enabled auto-merge
September 8, 2026 10:18
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 #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'smainbefore anything was changed, that mechanism already exists — it isextractLocalBindings(#9870), andclientwas already one of thename(s) built from onethe green line counts.The card also states that every call in
packages/client's namespace tour sits in theNOT readbucket. It does not. That document contributed three unread sites, and all three areconsole.log,console.errorandMath.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:extractMemberCallskeeps the sites whose receiver is typeable,unreadCallSiteskeeps 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.memberAtPathon the type surface — walks the path from a receiver type the gate already resolves, onepropertyHopper 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— theviaresolution (instanceconstruct-signature,returnawaited call-signature, no-viastatic side) lifted into one place, so the three member questions cannot disagree about whatviameans.hasMemberViais now three lines over it.NOT readline, 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
hasMemberViabut notmemberAtPathbinds 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
mainhas moved.NOT readcallsNOT readreceivers⭐ 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
newfrom a class the fence did not import 6, parameters 3, call returns 2, destructured 1. ⇒new X(…)off an import-boundXcontributes 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 frommainand had to be re-created. The three lines are the exact bytes #16212 removed.Exit 0 before, exit 1 after, naming
ai.nlq. The gate's own--self-testwas 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 emptygit 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.
packages/client/README.mdpackages/services/service-i18n/README.mdpackages/services/service-storage/README.mdpackages/runtime/README.mdpackages/types/README.mdpackages/core/README.mdThe tour's whole namespace surface is now graded against the published
.d.ts,client.ai.conversations.listandclient.ai.agents.chatincluded (depth-3 paths).验收备注
client.data.findwith 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, sofindis 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.approvals.approve(requestId, 'LGTM'), TS2559 — stays out of reach by construction.client.approvals.approveis 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-reportrows now print a site by its full key rather thanreceiver.member, so a chained unread site is not reported asres.json(…)when the document saysres.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 symbolmemberAtPathacross the 195 existing paths named by every published package'sfiles[]— 0 hits; positive controlObjectStackClienton the same paths — 42 hits. Nothing published moves.Verification
node scripts/check-published-readme-exports.mjs --self-test— exit 0. New batteryTHE PROPERTY PATH, and the partition it must preserve (#16209), 10 cases, registered inSELF_TEST_BATTERIESwith 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),memberAtPathin 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/objectstackderived 31 gate families from the actual changed set; all 31 run, all exit 0;--ranreconciles 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, at17ebeb7b8d.pnpm test/pnpm typecheckowed: the diff touches no package.Generated by Claude Code