Repository navigation
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
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
os-steve
added a commit
that referenced
this pull request
Aug 18, 2026
…tract, not a seed count (#9649) The block claimed the file was "seeded from the five instances #9532 measured". It shipped with 16 entries (PR #9546) and has since shrunk to 0 through #9602, #9615 and #9581, so the sentence was wrong on day one and the gap kept changing meaning as the ledger shrank. Rewritten to state the contract rather than a number: what an entry is (one known instance still awaiting repair, debt not exemption), that the count is read from `entries` and never asserted in prose, that `entries: []` is the success state rather than a corrupt or deletable file, and that absence from the file means measured-and-clean rather than unscanned -- which is what the plugin-audit negative control now says for every package. No baseline entry is added, removed or edited. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 23, 2026
…in entry point, not a `.configure()` that never existed (objectstack-ai#9602) * docs(services): five published READMEs document the real entry point, not a `.configure()` that never existed (objectstack-ai#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 objectstack-ai#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 * docs(service-automation): the Exports section covers the whole published surface (objectstack-ai#9532) The value exports were listed but the 30 type exports were not, so the README's export list was a subset rather than the surface. A rerunnable set-equality check over the built `.d.ts` now reports 50/50 for this package (104/104 across all five), in BOTH directions — the gate proves every documented name resolves, it cannot prove nothing was omitted. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y26DJEHSBhhAQ6wwfsHNza --------- Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Aug 23, 2026
…tract, not a seed count (objectstack-ai#9649) (objectstack-ai#9764) The block claimed the file was "seeded from the five instances objectstack-ai#9532 measured". It shipped with 16 entries (PR objectstack-ai#9546) and has since shrunk to 0 through objectstack-ai#9602, objectstack-ai#9615 and objectstack-ai#9581, so the sentence was wrong on day one and the gap kept changing meaning as the ledger shrank. Rewritten to state the contract rather than a number: what an entry is (one known instance still awaiting repair, debt not exemption), that the count is read from `entries` and never asserted in prose, that `entries: []` is the success state rather than a corrupt or deletable file, and that absence from the file means measured-and-clean rather than unscanned -- which is what the plugin-audit negative control now says for every package. No baseline entry is added, removed or edited. Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…that decided it (objectstack-ai#20866) Part of objectstack-ai#20596 Clause-②: no ## What changed This is the seventeenth stage of the `domain:services` lane of the dead-citation sweep. It covers `packages/services/service-job/src/**` and nothing else. By the seat's claim (`5911377566`), it is the next of the two single-site packages left in the lane, taken one package per stage. Later stages cover the other packages, so this PR says `Part of` and the card stays open. Every comment or docblock site in scope that cited a tracker number answering 404 has been rewritten in ruling C+D's form C (comment 5749154545 on objectstack-ai#19123), by the method of stages 1 to 16 (the latest is PR objectstack-ai#20850, landed as `40d6c5fbd`, the base here). That is **2 sites on 2 lines in 2 files, covering 1 number**: - `src/never-fires.fixture.ts:30`, the census's one site in this package at the base (a docblock); - `src/db-job-adapter.timeout.test.ts:55`, a test-comment site the census defers, found by the supplementary reading below and rewritten too, as stages 1 to 15 did. No test string, source string or gate-invisible spelling in this package carries a dead number, so these two sites are the whole population. Both rewritten lines now cite the commit in this repository that decided what they describe: **1 commit sha**. No ADR or ruling record records that decision (see the per-number table), so ruling C's commit rung applies. No number was dropped. Only comments changed. Each file keeps its line count (2 lines out, 2 in), so no line citation into either file moves. No code token moves (see the guard below). **No citation number is added**: the only tracker number on the added lines is the live `objectstack-ai#8628`, which already stood on its line. **No changeset.** Neither rewritten line reaches `dist`, and `dist` is byte-identical with and without the rewrite (see Changeset below), so this PR takes `skip-changeset`. ## Census: `service-job`, before and after **Instrument (A1).** The gate's own `node scripts/check-issue-citations.mjs --census --json`, read-only and unchanged. The count below is its `allocated-but-absent` findings under `packages/services/service-job/`. Each run counts as a reading only because its board frontier equals the newest issue or pull-request number, read by a separate request just before and just after the run. | reading | tree | board | whole-repo `allocated-but-absent` | service-job sites | lines | files | numbers | |---|---|---|---|---|---|---|---| | before | base `40d6c5fbd`, run 2026-09-30T12:36:32Z to 12:40:56Z | enumerated, 187 pages, frontier objectstack-ai#20855 (newest objectstack-ai#20854 before; objectstack-ai#20855 after, an issue opened 12:40:06Z inside the run, so the frontier equals the newest at the run's end) | 745 | **1** | 1 | 1 | 1 | | after | head `6ac866f86`, run 12:46:10Z to 12:49:55Z | enumerated, 187 pages, frontier objectstack-ai#20856 (newest objectstack-ai#20856 before and after) | 744 | **0** | 0 | 0 | 0 | The whole-repo drop is 1, and the finding sets of the two runs differ by exactly one row, `never-fires.fixture.ts:30 objectstack-ai#8748`, removed; none was added. The `resolves` tally is 33,221 in both runs, and `resolves-as-pull-request` (1,985) and `cross-repo-unjudged` (1,018) did not move either. Neither run was truncated or discarded: both enumerations read 187 pages at the newest frontier. The seat's census counted 1 here at `6bff748b`, and the base agrees: `6bff748b` is an ancestor of the base, and no commit between them touches this package's `src`. **Supplementary instrument, the whole scope.** The census does not read test files or strings. So a second reading runs the gate's own exported `extractCitations` (whole-file and comment-prose projections) and `namesThisRepository` over every `.ts` file under `service-job/src` (18 files). It takes its verdicts from the before census's own board reading rather than from a second enumeration: a number is dead when that census reported it `allocated-but-absent`, and alive when the gate's own census-scope extraction (`collectCitations`) judged it and the census did not report it. 4 numbers are covered by neither, because they stand only in test files. Each was read on its own through the read-only tools, and each answers as an issue: `objectstack-ai#8628`, `objectstack-ai#9611`, `objectstack-ai#9631` and `objectstack-ai#9633`. The probe's control: `objectstack-ai#8748` answers 404 on the issues endpoint and on the pull-request endpoint. | reading | citations | dead | src comment | test comment | src string | test string | |---|---|---|---|---|---|---| | before, `40d6c5fbd` | 95 | **2** | 1 | 1 | 0 | 0 | | after, `6ac866f86` | 93 | **0** | 0 | 0 | 0 | 0 | Its src-comment column equals the census's 1, which is the control on the second instrument. The 93 live citations are the same in both readings (81 by the census's judgement, 12 by the single reads), none is cross-repo, none is unjudged, and the drop of 2 citations is exactly the two rewritten sites. A third, raw reading (every `#` followed by 2 to 6 digits, whatever surrounds it) finds 95 occurrences before and 93 after, the same drop of 2, and no token beyond the gate's grammar. ## Per-number table Sites and files count every dead occurrence in scope at the base (comments and strings, tests included). | number | sites / files | rewritten / left | anchor: what it decided | |---|---|---|---| | `objectstack-ai#8748` | 2/2 | 2/0 | `226cb71d5` (PR objectstack-ai#8774): every exact-count case in this package moves onto the inert `NEVER_FIRES` cron fixture (`'0 0 30 2 *'`, February 30th, which never occurs), and the fixture moves out of the timeout suite into the shared `never-fires.fixture.ts`. Its message records the measurement `:30` describes: only `Date` faked, the registration placed 2/5/10/15 ms before the expression's instant, one extra handler run per case under the firing spellings, and two of the three no longer reproducing at a 2 ms lead. That is what `:30` says, and what `:55`'s 「now shared with the sibling job suites rather than spelled a second time here」 says. Its subject names `objectstack-ai#8748`, and its own diff names it on 2 added lines; `git blame` puts both rewritten lines in this commit | The sha matches exactly one commit (`git rev-parse --disambiguate`, count 1) and is an ancestor of the base (`merge-base --is-ancestor`, exit 0; reverse leg, base against the anchor, exit 1; control legs exit 0: `7246d631d`, 5,451 commits behind the base, and the repository's root commit `1598cabe4`, 15,209 behind, against the anchor's 5,436; the history is complete, `--is-shallow-repository` false, 15,210 commits at the base). `objectstack-ai#8748` answers 404 on the issues endpoint and on the pull-request endpoint, read one by one. No ADR, `scripts/adr-anchors/` file or ruling record names `objectstack-ai#8748`, PR objectstack-ai#8774, `NEVER_FIRES`, `never-fires`, February 30th or an inert cron (`git grep` over `docs/adr` and `scripts/adr-anchors`: 0 hits), so ruling C's first rung is empty. The repository's other `objectstack-ai#8748` tokens are `objectui#8748`, a cross-repo reference the gate never judges. ## Wordings to check - **`never-fires.fixture.ts:30`, tag swap in place.** 「Measured (objectstack-ai#8748) by faking ONLY `Date`」 became 「Measured (commit 226cb71) by faking ONLY `Date`」. The form is the one `packages/lint/src/lint-flow-patterns.ts:1468` already carries on `main` (「Measured (commit 8ed9c54)」). - **`db-job-adapter.timeout.test.ts:55`, tag swap in place.** 「(objectstack-ai#8628, objectstack-ai#8748)」 became 「(objectstack-ai#8628, commit 226cb71)」. The live `objectstack-ai#8628` stays: it is the report of the wall-clock flake, whose own change, `7246d631d` (PR objectstack-ai#8750), first made this suite's cron unable to self-fire. The form is the one stages 7 and 9 landed (for example `(objectstack-ai#5928, commit 69787f0)` in `plugin-audit`). - Neither line was reflowed, so each is 11 characters longer than before (70 to 81, and 72 to 83), a little past its block's wrap. `eslint.config.mjs` declares no line-length rule, and reflowing would have moved neighbouring lines. ## Sites left - **In `src`: none.** No test title, assertion message, operator log string, runtime refusal, quoted maintainer ruling or generated file in this package carries a dead number. - **Outside `src`, listed and left, not edited in this stage:** - The release-owned `CHANGELOG.md` names the dead `objectstack-ai#13112` on 1 line (`:705`; 404 on the issue and pull-request endpoints). Its other 27 numbers are live: 19 by the census's judgement, and by single reads `objectstack-ai#18715`, `objectstack-ai#14619`, `objectstack-ai#9517`, `objectstack-ai#1893`, `objectstack-ai#9611`, `objectstack-ai#9631` and `objectstack-ai#9633` as issues and `objectstack-ai#9546` as a pull request. - `vitest.config.ts` names the live `objectstack-ai#10374`. - `README.md`, `package.json` and `tsconfig.json` name none. ## Mechanical guard: no code token moves The guard compares, base `40d6c5fbd` against head, over both touched `.ts` files: - **Reading 1**, the TypeScript parser's leaf nodes (a `forEachChild` walk, so comments are trivia and JSDoc nodes are never visited). String and template literals are therefore read in full. - **Reading 2**, the full token stream in parser context (a `getChildren` walk, so punctuation and keywords are included; JSDoc nodes skipped). Results: - Real run at the head `6ac866f86`: 755 base leaf tokens, **0 files with a token change** on either reading (exit 0). - Comment control (「croner resolves Feb 29 forward」 to 「croner rolls Feb 29 forward」): 0 files changed, as expected (exit 0). - Positive control, a code identifier renamed (`expression: NEVER_FIRES }` to `expression: NEVER_FIRESX }`): DIFFER on the identifier in both readings (exit 1). - Positive control, a string literal changed (`'0 0 30 2 *'` to `'0 0 31 2 *'`): DIFFER on the string literal in both readings (exit 1). - Positive control in the second file, a numeric literal changed (`TIMEOUT_MS = 20` to `21`): DIFFER on the literal in both readings (exit 1). Every mutation went through `scripts/ablation-replace.mjs` (wrap mode) under a shell trap that restores by absolute path, and each landed (anchor 1 to 0, blob changed). Each restore was proven byte-identical to the HEAD blobs (`1fd5e1b3f612`, `ce37b1050c78`), with `git diff HEAD` empty and a clean tree afterwards. ## Changeset: none (`skip-changeset`) `files[]` is `dist`, `README.md` and `CHANGELOG.md`, and the package is not private. `never-fires.fixture.ts` is a test-only support module (nothing in `src/index.ts` re-exports it, and the tsup entry is `src/index.ts` alone), and the other touched file is a test. Measured on the built package (A3), after `pnpm --filter '@objectstack/service-job...' build` at `6ac866f86`, where the package's own `tsup` ran and cleaned its output folder: - Neither rewritten line appears in any `dist` file (0 in all 6, source maps included). Nor do the old number, `objectstack-ai#8628`, the anchor sha, the neighbouring lines `:28`, `:31`, `:54` and `:56`, `NEVER_FIRES`, the `'0 0 30 2 *'` expression, or a never-written negative phrase. - Positive controls, text that does ship: `CronJobAdapter` (45 hits), `DbJobAdapter` (73) and `sys_job_run` (52) across the entries, and two docblock lines of the `DbJobAdapter` class JSDoc in `db-job-adapter.ts` (「`{@link DbJobAdapterOptions.recordRuns}` is `false`, the on/off switch for」 and the `recordRuns` gating line below it), each in `index.d.ts` and `index.d.cts`. - **Byte identity.** The package was rebuilt with the base text put back in both files (through nested `scripts/ablation-replace.mjs` legs, with each file proven equal to its base blob, `919a2e444f23` and `eb00f6eb1978`, during the leg), and again after the proven restore. The sha256 of all 6 `dist` files is identical in all three builds: JS entries, declaration files and source maps. So nothing this PR rewrites is published, and a changeset would announce a change no consumer can observe. No `.changeset/*` file is added or touched. ## Gates (head `6ac866f86`) - **Citation judging, as CI runs it:** `node scripts/check-issue-citations.mjs` exits 0 (「no issue citations added against 40d6c5f」). `pnpm check:issue-citations` exits 0 (self-test, 114 cases, 8 batteries). - **Doc authoring:** `pnpm check:doc-authoring` exits 0 (the sibling-package prose-id baseline holds, no growth). - **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `6ac866f86` (change set derived from git: 2 paths against the merge base `40d6c5fbd`) derived 54 commands, all among the dispatch list's 63. - All 63 of the dispatch list ran, each with its exit code captured before any pipe, and all 63 exit 0; none exited 3. - `--ran`, fed each command with its exit code, reports 54 derived, 54 run, 0 NOT MEASURED (a derived zero), 0 unrun, and exits 0. Of the other 9, it classifies 7 as pending-changeset families and 2 as outside this derivation. - A full `turbo run build` over `./packages/*` and `./packages/*/*` ran first under the shared verify lock (71 of 71 tasks), so no gate hit an unbuilt workspace. - **Roster families the derivation lists outside its commands** (their rosters sit in a directory this diff touches): `pnpm check:authz-resolver`, `pnpm check:error-code-casing` and `pnpm check:filter-alias-parity`, plus `node scripts/check-changeset-fixed.mjs`, which the dispatch named. Each exits 0. - **Tests and typecheck, under the verify lock, at `6ac866f86`:** - `pnpm --filter @objectstack/service-job test`: 11 files pass and 117 tests pass, which is every tracked test file under `src/`. - `pnpm --filter @objectstack/service-job typecheck` exits 0, and `tsc --noEmit --listFiles` puts both touched files in the program (the config includes `src` and excludes no test file). - **Lint, as a proven narrowing:** eslint with inline config disabled, over the 2 touched `.ts` files, gives 2 files, 0 errors and 0 warnings (its `--format json` output). Both files are in eslint's own population (not reported ignored; `dist/index.js`, the control, reads ignored). `eslint.config.mjs` never enables type-aware linting (no `parserOptions.project`, as its own lines 327-328 state), so a comment edit here cannot move the verdict on any untouched file. The repo-wide `pnpm lint` is CI's run. - **Control bytes:** `pnpm check:nul-bytes` exits 0, and a raw scan of the changed files for control bytes finds none. ## Acceptance notes - **The gate-invisible spellings, grepped as the claim asked** (objectstack-ai#20636, including the `clause #N` position). At the base and at the head, `#N-word`, `#A/#B`, `option #N`, `clause #N` and URL-spelled links are each on 0 lines, so the claim's 0 / 0 / 0 / 0 / 0 hold. The one prefix-joined token, `pre-objectstack-ai#6617` in a test title of `db-job-adapter.degraded-outcome.test.ts`, is inside the gate's grammar and names a live number. - **Wording only:** 「the card」 / 「this card」 stands on 4 comment lines in 3 files under `service-job/src`. It carries no number, neither instrument sees it, and this diff removes no antecedent. - **Outside-`src` residue.** `CHANGELOG.md:705` names the dead `objectstack-ai#13112`; the file is release-owned and is left. - **The census instrument did not truncate in this stage.** Both enumerations read 187 pages at the newest frontier. - **Base.** The branch is on `main` at `40d6c5fbd`. `main` has since moved two commits (to `660a9b247`). Neither touches `packages/services/service-job`, `scripts/check-issue-citations.mjs`, `scripts/pm/dispatch-gates.mjs` or `.changeset/config.json`, so no merge was taken; the merge queue rebuilds on the merged generation. The one derivation input that moved is `scripts/doc-authoring-prose-id.baseline.json`, where the second commit removed only `packages/objectql` entries; this package has no entry there before or after, so the merged tree cannot move that gate's verdict on this diff. --- _Generated by [Claude Code](https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H)_ Co-authored-by: Claude <noreply@anthropic.com>
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 #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