Skip to content

fix(metadata-core,service-cluster): stop emitting and publishing the CJS half of ./testing - #13114

Merged
os-elon merged 4 commits into
mainfrom
claude/issue-13013-dead-testing-cjs
Aug 29, 2026
Merged

fix(metadata-core,service-cluster): stop emitting and publishing the CJS half of ./testing#13114
os-elon merged 4 commits into
mainfrom
claude/issue-13013-dead-testing-cjs

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#13013

#13001 made both ./testing subpaths ESM-only, dropping the require condition that pointed at dist/testing.cjs. The build kept emitting those files and files: ["dist"] kept packing them, so every release shipped bytes no exports condition could reach. This prunes them without moving anything reachable.

The route, chosen on measurement

The card left three routes open and did not commit. All three were measured.

routeverdict
narrow the files whitelistinadmissible.scripts/check-published-files.mjs:473 fails closed on any negated files pattern ("which this guard does not model"). A positive re-spelling would have to enumerate build output by glob, and every entry beyond dist/README.md/CHANGELOG.md needs a registered reason. It also leaves the build still emitting the dead bytes.
one config per entrybreaks behaviour. See below.
one config per formattaken.

Why per-entry splitting is a silent breaking change

index and testing share a chunk, and the chunk carries the error classes:

$ python3 -c "import json;print(json.load(open('dist/chunk-46MG4YHS.js.map'))['sources'])"
['../src/errors.ts', '../src/canonicalize.ts']

src/errors.ts exports MetadataError, ConflictError, NotFoundError, SchemaValidationError, BranchError. One config per entry gives testing.js its own copies of them, so ConflictError reached through @objectstack/metadata-core/testing stops being the class thrown by @objectstack/metadata-core — and the contract suite this entry point exists to publish asserts exactly that identity (src/contract-suite.ts:344, .rejects.toBeInstanceOf(ConflictError)). Every downstream driver package running the suite would fail, on a change that reads as a build-config tidy-up.

Splitting by format avoids it entirely: the ESM half keeps both entries, so chunk sharing is untouched; the CJS half takes src/index.ts alone, and . is its only entry point, so there is exactly one copy either way.

The clean hazard is a race, not an ordering

The card expected "the second config wipes the first's output". It is worse than that: tsup runs an array config through Promise.all (tsup/dist/index.js, the Array.isArray(configData) map), so the halves build concurrently and a clean in either one races the other's writes in either direction. clean therefore moves out of tsup and into the build script (rm -rf dist && tsup). That is also a stronger clean than tsup's own, which unshifts !**/*.d.{ts,cts,mts} and so preserves stale declarations — it would have left dist/testing.d.cts on disk through every rebuild of an existing worktree, which is precisely the file this change exists to stop emitting.

Verification

Union re-run on the final commit, 2fde235c84.

1. Nothing reachable changed — sha256 over both dist/ trees, before vs after. Byte-for-byte identical: index.js, testing.js, both maps, chunk-46MG4YHS.js + map, index.d.ts, testing.d.ts, repository-*.d.ts (and the service-cluster equivalents). index.cjs changes only because the shared CJS chunk is now inlined into the sole remaining CJS entry; index.d.cts likewise. This is the load-bearing evidence for the class-identity claim above — the ESM output is not merely equivalent, it is the same bytes.

2. The published tarball, measured both ways with a positive control. The pre-fix side is a real measurement, not an inference: the fix was committed first, then the two configs were reverted to their pre-fix bytes with the mutation confirmed on disk by anchored grep -c counts (pre-fix marker 2, post-fix marker 0), rebuilt, packed, and restored via git checkout HEAD -- ... with git diff HEAD proved empty. A trap held the restore on every exit path.

packagefilesunpackedgone from the tarball
@objectstack/metadata-core22 → 163.3 MB → 3.2 MBtesting.cjs, testing.cjs.map, testing.d.cts, chunk-H2D6OJ76.cjs, chunk-H2D6OJ76.cjs.map, repository-Dl3EudaY.d.cts
@objectstack/service-cluster15 → 12364.1 kB → 336.9 kBtesting.cjs, testing.cjs.map, testing.d.cts

Positive control: the same npm pack --dry-run invocation still lists dist/testing.js (24.6 kB) and dist/testing.js.map in the after run, so the absences are readings and not a broken instrument.

3. check:dual-build-cjs-loads still reads a non-empty, honest population. This was the card's stated risk — a gate that greens because it no longer reads anything is the vacuous-pass class of #13014.

before ✓ 103 published require entry point(s) across 67 package(s) load; 613 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree.
after ✓ 103 published require entry point(s) across 67 package(s) load; 610 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree.

Entry points, packages and probes are unchanged; the PARSES population falls by exactly 3, which is exactly the three CJS files that stopped existing (metadata-core/dist/testing.cjs, metadata-core/dist/chunk-H2D6OJ76.cjs, service-cluster/dist/testing.cjs). Two independent readings agree on the 613: this branch measured it at db39dfc1c9, and #13050's own MEASURED snapshot records it at 8cb96ec41b.

MEASURED.cjsFiles is deliberately not updated — it is a provenance record attributed to a named commit, and it is still true of that commit. The gating values are the floors, and MIN_CJS_FILES = 520 clears with margin. The gate's own self-test (53 cases, including "every vacuity floor driven to zero with its green control") ran in the same command, so the instrument is proved able to return non-zero in the same run that returned this pass.

4. check:published-files cannot object, verified by reading rather than by a green run. Its five invariants ask that the whitelist covers declared entry points (SUFFICIENT); its walk() never enters dist/ at all (SKIP_DIRS), so an extra shipped file is outside every question it asks, in both directions. It is green, but that green is not the evidence.

Gates. Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (re-derived after the changeset existed and after each sync, never off the stale list): 28 families, all green. Two returned non-zero and are NOT MEASURED rather than red — check-test-completeness printed its usage banner (it needs a turbo test log argument, so it never entered the gate body) and check-half-states exited 3, PREREQUISITE NOT MET, because this container's token is the 14-byte proxy placeholder; it swept nothing.

Tests. The two changed packages plus every direct consumer of the ./testing subpaths: metadata-core (260), service-cluster (66), metadata-fs (70), metadata-protocol (2010 passed, 10 skipped), service-cluster-redis (28) — 2434 passed, 0 failed. metadata-coretypecheck green. These suites resolve the subpath through the package exports to the built dist/testing.js (no vitest alias in packages/metadata-fs/vitest.config.ts; a direct ESM import from inside the package resolves and binds real exports), so they exercise the built artefact rather than source. Independently, require.resolve('@objectstack/metadata-core/testing') answers ERR_PACKAGE_PATH_NOT_EXPORTED, which re-confirms the card's premise from the consumer side.

Out of scope, filed

#13112 — 28 published packages ship 36 .d.cts declarations (5.2 MiB) that no types condition points at. Same defect class, measured ~48x larger than this card. Deliberately not fixed here: unlike testing.cjs, those files have a plausible reader (a require-side consumer resolving index.cjs is where an adjacent index.d.cts would be looked for), and the remedy could equally be declaring them rather than deleting them — a published-surface decision across 28 packages, not a mechanical prune.

Routing note

Triage routed this to domain:devx by subject (build/publish pipeline shape, read by two scripts/ gates) while the fix lands in packages/metadata-core (lane: engine) and packages/services/service-cluster (lane: services), both outside the devx tree — and wrote its reasoning down expressly so it can be overturned. If either owning seat objects to this shape, this PR should be re-routed or split by package ownership rather than pushed through.

Also worth recording for the next reader: the card writes the second package as packages/service-cluster; it actually lives at packages/services/service-cluster. That is the path-precision false-zero shape the card itself warned about.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 2 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d48929efe794b467c169825ecb68a2d015f09d8dpackageMentionDocs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mtooling

Projects

None yet

2 participants

@os-elon@claude