Skip to content

Commit 5e7d83c

Browse files
os-warrenclaude
andauthored
fix(gate): correct why check-published-files lets packages/spec ship its zod sources — a read route, not an import route (#19468)
Fixes #19009 Clause-②: yes ## The experiment the card said nobody had run The gate `scripts/check-published-files.mjs` lets `@objectstack/spec` ship `src/**/*.zod.ts` in `files[]`, and states why, verbatim: > The Zod schemas are themselves the contract (Prime Directive #1); downstream code imports them directly, so these sources are product rather than build input. The card measured both halves of that sentence statically and said plainly that the decisive experiment — pack the tarball, install it, import a shipped `.zod.ts` from a real consumer — had not been run. It is run here, first, before any edit. `npm pack` of `packages/spec` at this branch's base, installed into a scratch consumer, then every spelling of the deep import: | probe | result | |---|---| | ESM `@objectstack/spec/src/data/object.zod.ts` | `ERR_PACKAGE_PATH_NOT_EXPORTED` | | ESM, no extension, and `.js` extension | `ERR_PACKAGE_PATH_NOT_EXPORTED` | | CJS `require.resolve(...)` | `ERR_PACKAGE_PATH_NOT_EXPORTED` | | esbuild bundle of the deep specifier | `The path "./src/data/object.zod.ts" is not exported by package` | | `tsc`, `moduleResolution: bundler` | `TS2307` | | `tsc`, `moduleResolution: nodenext` | `TS2307` | | literal relative path into `node_modules` | `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING` | Lit controls, same install, same run: `@objectstack/spec/data` resolves with 490 exports, the root entry with 137, `require.resolve` of `./package.json` returns a path, and esbuild bundles the declared subpath to 1.4 MB. The zeros above are readings, not a dead harness. One resolver mode does reach the files, and it measures both halves of the card at once: `tsc --moduleResolution node10`, which ignores `exports` maps. It opens the shipped source and then fails — 120 errors, the first `Cannot find module '../shared/lazy-schema'` — and in that same mode the package's own declared subpath `@objectstack/spec/data` does not resolve either. It is not a mode in which this package is consumable at all. So the card's first half holds: no consumer can import these files. The stated reason's mechanism is false. ## What the files are actually for Re-measured on this branch: 190 of the 203 shipped `*.zod.ts` modules carry a relative import onto one of 42 `src/` modules the glob does not ship, and `src/shared/lazy-schema.ts` alone is named by 182. The card's second half holds too. But both halves only bite if the point is to IMPORT them, and it is not. The published skills catalog points agents at these files BY PATH, to read inside a consumer's `node_modules`. `skills/README.md` states the mechanism itself: > a `references/_index.md` that points into the authoritative Zod sources in `node_modules/@objectstack/spec/src/...` (the published `@objectstack/spec` package ships these `.zod.ts` sources, so the pointers resolve in consumer apps too) Those index files are generated from this very glob by `packages/spec/scripts/build-skill-references.ts`, whose own guard already states the dependency from the other end: > `resolveAll()` keeps only `*.zod.ts` from the closure, because the published package's `files` allowlist ships those sources and nothing else — a pointer to any other src file 404s in a consumer's `node_modules`. Measured with the tarball in hand: **170 pointers across 9 published index files, 0 of them outside the glob.** `check:skill-refs` is green, reporting 9 generated files in sync with `packages/spec`. That is also why the 190-of-203 count is a property of the glob rather than a defect in it: a reader never resolves those imports. ## The route taken, and the two that were not **Correct the reason.** The entry stays; the sentence justifying it now names the read route instead of an import route that has never been open. The long-form rationale sits in a comment above the entry so the next reader does not re-derive the two wrong repairs: - **Do not open the `exports` map** to make the old sentence true. It would advertise a route broken for 190 of 203 files — a machine-readable surface that lies (Route and surface ownership rule 4; Prime Directive #10). - **Do not drop the entry** as unreachable payload. It is reachable product, by read rather than by resolve, and dropping it makes every generated pointer in the shipped catalog 404 at once. Triage's boundary applies and is respected here: that would be removing a published capability, and the measurement above is what shows the capability is real rather than absent. **No downstream resolution result changes.** This diff edits a reason string and a comment in one repo-root gate script. Zero published bytes move — the tarball is byte-identical before and after — so triage's `pm:retriage` condition is not met. ## Cost of this glob, measured here The card pointed at #16045's 12,661,943-byte reading as the same question asked of a different `files[]` entry, explicitly not as this entry's cost. Measured for THIS glob, from the packed tarball: **5,557,289 bytes uncompressed across 203 files, 3.8% of the unpacked tree** (tarball 28,299,508 bytes compressed, 147,205,318 uncompressed). Recorded as a reading; it vetoes nothing. ## Hold #8133 — restart trigger `#8133` is open and `pm:on-hold`, carrying `Restart-when: any PR touches the packages/spec build/publish pipeline (tsup config, or the exports map in packages/spec/package.json)`. **This PR does not trip that trigger.** It touches neither the tsup config nor the `exports` map — `packages/spec/package.json` is not in this diff at all. What a reader of that hold needs from here: - the `exports` map was read in this work and is unchanged: 19 subpaths, none of them a `./src/*` and no wildcard; - one branch considered and rejected here WOULD have tripped it — opening the map to admit the `src` subpath. It was rejected on the measurement above, so the hold keeps its standing and its blast radius is untouched; - `#8133` remains open and is not addressed here. Stated by hand rather than left to the patrol: the half-state patrol's H17 trigger-file index does not list `#8133`, because that index carries only tracked file paths and this hold's trigger is written as prose. The index says of itself that it under-reports and never invents, so its silence on this file is a reach limit and not a clear. ## Gates Derived from the actual changed path with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, every exit code captured before any pipe, reconciled with `--ran`. Readings are labelled by which side of the edit they came from, because this PR edits the gate it also runs: - **pre-edit**: `node scripts/check-published-files.mjs` exit 0, and `--self-test` exit 0. - **post-edit**: all 31 derived families exit 0 at the merged head, `check:published-files` and its self-test among them. - `check:pm-dispatch-gates` and `check:watch-hint-literal` are both in that set and green. That is the guard that matters most here: the new prose lives in comments and in one long string, so it declares no watch hint and fabricates no lead for this gate — the failure mode its own `build-time script` entry warns about. - `--ran` reconciliation: 31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN. Lint, narrowed with the narrowing proven rather than assumed: `eslint --no-inline-config --format json` over the one changed file gives 1 file, 0 errors, 0 warnings, exit 0. The population is read from eslint's own config via `isPathIgnored` over the 9,096 tracked files: 6,946 files in the lint population. The config never enables type-aware linting — no `parserOptions.project`, no `projectService`, the only textual hit being the config's own comment saying so — so a one-file diff cannot move any untouched file's verdict. The repo-wide sweep stays CI's run. Consumer-package tests: - `@objectstack/downstream-contract` — 3 files, 31 tests, all passed. This is the harness that resolves named out-of-repo consumer specifiers out of a packed tarball, so it is the on-point consumer reading for a publish-surface card. Its first run refused with a stated prerequisite (`@objectstack/cli` not built), which is the harness working as designed; re-run after `pnpm --filter '@objectstack/cli...' build`. - `@objectstack/spec` — 506 files, 14,797 tests, all passed. - `pnpm --filter @objectstack/spec check:skill-refs` exit 0; `check:skill-docs` exit 0. ## Changeset `skip-changeset`. Nothing in this diff publishes: the only changed file is `scripts/check-published-files.mjs`, the root manifest is private, and this gate's own FORBIDDEN rule bars `scripts/` from every package tarball. Verified mechanically against the packed spec tarball — 0 hits for the changed file, with a positive control (`package/src/data/object.zod.ts`, 1 hit) proving the query was alive. For review: the claim declared `Clause-②: yes` because one live branch, opening the `exports` map, expands the public surface. That branch was measured and rejected, so the landed diff expands nothing and publishes nothing. The `yes` is kept as declared. ## Acceptance notes Observations from this work, deliberately not filed and not fixed here: - The glob-to-catalog dependency is one-directional in prose. The generator's guard says it filters to `*.zod.ts` because the `files` allowlist ships those and nothing else; after this PR the gate says the catalog is why the allowlist has them. Neither names the other by path, and nothing fails mechanically if one side moves. No PR and no person is carrying this today — successor: none. `packages/spec/scripts/**` is also held by an open sibling PR from this seat, so the back-pointer half was out of reach here regardless. - `files[]` carries a bare `README.md` entry, which npm matches at any depth, so `package/src/migrations/entries/README.md` rides into the tarball — the single non-`.zod.ts` file under `src/` in it. Observation only: one small file, and the entry is canonical. - The card's static readings drifted between filing and this branch — 188 of 202 over 35 modules there, 190 of 203 over 42 here. Direction unchanged; recorded so the counts in the gate comment are not read as contradicting the card. - My own first pass at the import measurement resolved relative specifiers without mapping the NodeNext `.js` spelling back to `.ts`, which counted five shipped files as unshipped targets. Corrected in a second commit on this branch before the numbers were relied on; the corrected run leaves 0 unresolved specifiers. --- _Generated by [Claude Code](https://claude.ai/code/session_01UDXER3sdqfeVYpEWZs5mZx)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 841ed38 commit 5e7d83c

1 file changed

Lines changed: 43 additions & 1 deletion

File tree

‎scripts/check-published-files.mjs‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,50 @@ const EXTRA_ENTRIES = {
242242
liveness: 'Per-metadata-type liveness ledgers, read by the ADR-0049 enforce-or-remove tooling.',
243243
prompts: 'Authoring prompts shipped for agents consuming the protocol.',
244244
'llms.txt': 'Protocol summary for LLM consumers.',
245+
// src/**/*.zod.ts -- a READ route, and never an import route (#19009)
246+
// ------------------------------------------------------------------
247+
//
248+
// This entry used to justify itself with "downstream code imports them
249+
// directly". That sentence named a route that has never been open, and
250+
// both of its halves were measured false against a real packed tarball
251+
// before this correction landed:
252+
//
253+
// - the exports map declares no ./src/* subpath and no wildcard, so
254+
// every spelling of the deep specifier is refused at RESOLUTION --
255+
// Node answers ERR_PACKAGE_PATH_NOT_EXPORTED under both ESM and CJS,
256+
// esbuild answers "not exported by package", and tsc answers TS2307
257+
// under both bundler and nodenext resolution. A literal file path
258+
// into node_modules does not rescue it either: Node refuses type
259+
// stripping under node_modules by design, so the file cannot load
260+
// even when resolution is bypassed entirely.
261+
// - measured when this landed, 190 of the 203 shipped modules carry a
262+
// relative import onto one of 42 src/ modules this glob does NOT
263+
// ship (src/shared/lazy-schema.ts alone is named by 182), so a route
264+
// that reached them still would not load them.
265+
//
266+
// What they are actually for is a READ route. The published skills
267+
// catalog tells an agent to open these paths inside a consumer's
268+
// node_modules and read the schema as the truth, and each skill's
269+
// reference index is GENERATED from this very set by the spec package's
270+
// own build-skill-references generator. That generator filters its
271+
// closure to the *.zod.ts shape for exactly this reason, in its own
272+
// words: a pointer to any other src file 404s in a consumer's
273+
// node_modules. Which is also why the unshipped-dependency count above
274+
// is a property of the glob and not a defect in it -- a reader never
275+
// resolves those imports. Measured with the tarball in hand: 170
276+
// pointers across 9 published index files, 0 of them outside this glob.
277+
//
278+
// So what #19009 corrects is the REASON, not the entry, and the two
279+
// repairs the false reason invites are both wrong:
280+
// - do NOT open the exports map to make the old sentence true. It
281+
// would advertise a route that is broken for 190 of 203 files, which
282+
// is a machine-readable surface that lies (Route & surface ownership
283+
// rule 4; Prime Directive #10).
284+
// - do NOT drop the entry as unreachable payload. It is reachable
285+
// product -- by read rather than by resolve -- and dropping it makes
286+
// every generated pointer in the shipped catalog 404 at once.
245287
'src/**/*.zod.ts':
246-
'The Zod schemas are themselves the contract (Prime Directive #1); downstream code imports them directly, so these sources are product rather than build input. Narrowed to *.zod.ts so no test or helper rides along.',
288+
'The Zod schemas are themselves the contract (Prime Directive #1), and the published skills catalog points agents at these files by PATH, to read inside a consumer node_modules -- never by specifier to import, a route the exports map does not offer. Product rather than build input, read rather than resolved. Narrowed to *.zod.ts so no test or helper rides along, and because the reference generator emits pointers to exactly that set.',
247289
'api-surface':
248290
'Export snapshot used by downstream compatibility checks — one file per published entry point since #5837.',
249291
'spec-changes.json': 'Machine-readable spec change log driving the upgrade guide.',

0 commit comments

Comments
 (0)