Uh oh!
There was an error while loading. Please reload this page.
ci: a published README's symbol claims are checked against the package's built exports - #9546
Merged
os-project-manager merged 2 commits intoAug 18, 2026
Conversation
…ackage's built exports (#9532) Six published service READMEs documented a `.configure()` entry point on classes that exist nowhere in the repo. #9517 fixed one by hand; a single grep found five more. Six instances of one defect is a missing gate, not six mistakes -- nothing read a published README against its package's exports, so the drift was free in both directions. check:published-readme-exports resolves every symbol a published markdown document tells a reader to import, through the `exports` map, to the built `.d.ts` a consumer installs. Two halves: the import clause, and `X.member(` call sites on names that import bound (which is where `.configure()` lives when the class itself is real). Scope was set by measurement, not taste. Of the 145 lines across 50 published READMEs carrying both `import` and an `@objectstack/` specifier, the 3 outside code fences are all prose false positives; `diff` fences read their `+` side only; CHANGELOG.md is excluded because a changelog documents the past. The result on this tree is 16 findings across 10 files and zero false positives, each hand-verified. Those 16 seed a shrink-only baseline reconciled in both directions, so fixing a README forces its entry to be deleted in the same PR. Every entry pins one symbol, never a file, and records the real export -- the baseline is the worklist for the per-package rewrites. plugin-audit is deliberately absent: it is the negative control. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
Reverse-verifying the missing-dist path showed it reporting under the "undocumented symbol claim" heading with a remedy telling the reader to fix the README -- when the actual fix is `pnpm build`. A gate whose failure sends the reader at the wrong file is one push away from being muted, which is the failure this whole gate exists to prevent. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
This was referenced Aug 18, 2026
os-project-manager
marked this pull request as ready for review
August 18, 2026 12:03
Uh oh!
There was an error while loading. Please reload this page.
os-project-manager
deleted the
claude/issue-9532-published-readme-export-gate
branch
August 18, 2026 12:18
os-project-manager pushed a commit
that referenced
this pull request
Aug 18, 2026
… not a `.configure()` that never existed (#9532) `service-analytics`, `service-automation`, `service-cache`, `service-i18n` and `service-job` each told a reader to import a `Service…` class from its own package and call a static `.configure({...})` on it. Neither has ever existed: no class in this repo exposes a static `configure`, and none of those five identifiers is exported by anything. All five READMEs are in their packages' `files` arrays with `private` unset, so they are the pages npm renders. Each README is rewritten against the package's BUILT type surface — the entry point is a kernel plugin constructed with `new` in every case — and each package's entry is deleted from `scripts/published-readme-exports.baseline.json` in the same commit (that baseline is reconciled in both directions, so a stale entry fails too): 16 entries -> 10. A name swap alone would not have gone green, which is the point of the gate landed in #9546: substituting the genuine class while keeping `.configure(...)` turns the import finding into a call-site finding rather than into silence. Also removed as fabricated: nine analytics REST endpoints of which none exists, a five-endpoint automation REST list matching no mounted route, fourteen `ICacheService` methods on a six-member contract, an i18n dialect with namespaces/plurals/formatters over a synchronous `t(key, locale, params?)`, and ten `IJobService` methods on a three-required-member contract. Two capability claims are corrected rather than deleted, because the source decides: `RedisCacheAdapter` throws from every method and `adapter: 'redis'` throws at init, and `JobServicePlugin`'s `adapter: 'interval'` stores cron registrations that never fire. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza
This was referenced Aug 18, 2026
This was referenced Aug 18, 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 freeto 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 #9532 — piece 2 only, the gate. The five per-package README rewrites are the other half of that card and are deliberately untouched here, so the card stays open for them.
Verified at
466fe1394(the head of this branch; the gate union below was run on that tree).Premise, re-measured before building
Both of the card's measurements still resolve to zero on
origin/main(02ebb6f5b): nostatic configureanywhere underpackages/**/*.ts, and none ofServiceAnalytics/ServiceAutomation/ServiceCache/ServiceJobis exported by any file. The fixture set stands as dispatched.One correction worth recording:
plugin-audit's README only became honest onorigin/main(PR #9531). A checkout predating that merge still showsPluginAudit, whichsrc/index.tshas never exported — so the negative control has to be measured againstorigin/main, not a localmain.What landed
scripts/check-published-readme-exports.mjsresolves every symbol a published markdown document tells a reader to import — through the package'sexportsmap, to the built.d.tsa consumer actually installs — and fails when the symbol is not there.Two halves:
import { … } fromclause naming a workspace package must be an export of that package's type entry. This catches all six measured instances.Name.member(must name a real property of that symbol's type. This is where.configure()lives in the worse version of the defect, where the class itself is real.Type resolution is the TypeScript compiler's, over one shared program — not a regex over declaration text — so cross-package re-exports resolve the way a consumer's compiler resolves them.
The call-site half is not decorative — it was kept on evidence
The dispatch said to ship the import half alone rather than ship something noisy. Both halves shipped, because both measured clean and the second one found defects the first structurally cannot see:
packages/mcp/README.md:40—MCPServerPluginis a real exported class; its.configure()is invented.packages/objectql/README.md:66—SchemaRegistry.registerObjectis an instance method called on the class.It is also what stops a careless rewrite of the five service READMEs. Measured directly: substituting the genuine
AnalyticsServicePlugininto service-analytics' README turns the import finding into anAnalyticsServicePlugin.configurecall-site finding — not into silence.Scope was set by measurement, not taste
The card warned a naive version "will drown in false positives on prose and pseudo-code", and a muted gate is worse than none because it still reads as coverage. Every boundary below was measured on the real tree first:
importand an@objectstack/specifier, 142 are inside fences. All 3 outside are false positives — two markdown links inpackages/core("import@objectstack/spec") and one table row about an admin user import feature.typescript/ts/tsx/diff.bash(51 fences),json(12),sql,share excluded; untagged fences are included (zero import lines today, so it is free coverage).difffences read the+side only-lines document the old imports. Scanning them would report the deletion the fence exists to announce.CHANGELOG.mdexcludedfiles, but a changelog documents the past: an entry naming a v1 export that v3 removed is correct text this gate would redden permanently, unfixably. Any other published.mdis in scope automatically.react,@objectql/coreand friends are not this repo's business.Result on this tree: 16 findings across 10 files, and zero false positives. Every one was hand-verified against the built
.d.tsbefore it was accepted.Why the built
.d.ts, and why an unbuilt package is an errorThe question is "can a consumer write this line?", and a consumer resolves through
exportsto a.d.ts. Readingsrc/index.tswould answer a similar-looking question about a file no consumer sees, and would miss a subpath the README imports thatexportsnever declares (which is exactlydriver-sql's finding).That makes it build-dependent, and build-dependent gates fail characteristically: on a fresh checkout
dist/is absent, the scan reads nothing, and green means "not measured" while looking like "measured and clean". So a missing type entry is a hard error with its own heading and remedy, never a skip. The second commit exists only because reverse-verifying that path showed it reporting under the wrong heading with a remedy pointing at the README instead of atpnpm build.Baseline
All 16 findings are pre-existing on
main, so they seedscripts/published-readme-exports.baseline.json:--fixflag, and adding an entry is not offered as an author remedy — this gate takes the refusal arm of thecheck:engine-double-contractfires at CI time, not authoring time — four independent PRs tripped it on brand-new test files in one shift, and a pre-warning in the brief did not prevent it #8435 convention;plugin-auditis deliberately absent — it is the negative control.Self-test and reverse verification
--self-testdrives the whole pipeline offline in both directions, over the shapes measured above: the three prose false positives, the bash fence, thedifffence's two sides, multi-line clauses, aliases,import type, bothexports-map shapes, and end-to-end analysis of a fabricated import, a fabricated static, and an honest README. A text scanner over prose fails by quietly matching nothing, and only a positive control tells that apart from a clean tree.Four ablations, each predicted before it was run, each restored afterwards:
service-analyticsline 27AnalyticsServicePlugin.configureplugin-audit's README to its pre-#9531 textPluginAuditPluginAudit; green again on restoredist/index.d.tspnpm --filter @objectstack/service-cache buildThe third is the strongest control: on the exact bytes of the README that shipped, this gate would have caught #9517 before it went out.
CI wiring
One step added to the existing
TypeScript Type Checkjob inlint.yml, after its build steps, beside the other dist-reading gate (check:api-surface). No new job, so no new required context — nothing waits on a check name no head has ever reported. Confirmed bycheck:required-contexts, which still pins the same 8 names across 2 workflows.Gate union — all green at
466fe1394Derived from the actual changed paths via
node scripts/pm/dispatch-gates.mjs(paths taken fromgit merge-base origin/main HEAD, per #9320), then run:eslintis clean on the new script. Prettier is not wired into CI and the existing gate scripts fail bareprettier --checkidentically, so the file follows its siblings' convention.Changeset
None — new tooling, nothing published moves. Labelled
skip-changeset.Filed, not fixed
Eleven of the 16 findings sit outside this card's five-package rewrite scope. One of them (
service-i18n's cross-packageuseTranslationclaim) falls inside #9532's own rewrite of that README, so it needs no card. The other ten are filed unassigned:packages/spec/prompts/import four spec symbols that do not exist — and one of them resolves to the JS global instead of failing #9545 — the agent-authoring prompts inpackages/spec/prompts/, whereimport { Object } from '@objectstack/spec/data'binds to the JS global instead of failing loudly.Generated by Claude Code