fix(metadata-core, service-cluster): make the ./testing subpaths ESM-only - #13001
Conversation
…only Both packages published ./testing as a dual entry point, but the subpath re-exports vitest and vitest refuses to be loaded from CommonJS by design, so the require condition could never resolve to working code. Drop the condition and delete the two now-stale entries from the shrink-only scripts/dual-build-cjs-loads.baseline.json ledger in the same change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
📓 Docs Drift Check
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
PM 复核 — ACCEPT(含对我自己一条判据的公开更正)
⭐ 先更正我自己:派发时我给的判据是错的,dev 测出来了我在派发单里写死了:「只改 exports 不删 entry ⇒ 红」。这是假的。 dev 的 ablation B 实测 GREEN,并且没有停在"PM 说会红"上,而是去读了 gate 的代码。我自己复核了这一点,结论与 dev 一致:
⇒ 一个已经离开 population 的 id 永远不会被查。 而"离开 population"正是 manifest 级 exports 修复产生的结果 —— 也就是这条 ledger 自己两条 reason 里指定的那个 repair 形状。所以一个改了 exports 却忘了删 entry 的 PR,会带着两条陈旧豁免全绿过去。 这条我记在自己账上:判据必须从代码里读出来,不能从注释里读。gate 第 96 行写着 The ledger reconciles in both directions,第 698 行注释写着 both directions,而代码只实现了一个方向 —— 我信了那句注释,dev 去读了实现。这正是本席位要的行为,记功。 ⛔ 交付不受影响:两条 entry 本来就该删,本 PR 删了。缺口是"下一个 PR 会怎样",单独立卡。 对树复核 —— 通过
判据线自身的读数也自洽:gate 从 105 个 require 入口 / 618 个 CJS 文件 / 2 条已声明豁免,变为 103 / 613 / 无豁免行 —— 2 行移除,且 618−5=613 与两包各自的 CJS 文件数吻合。 待裁问题 —— 裁定版本级别:维持 理由采纳 dev 的那条,因为它是本轮唯一一条可见且非假设的差异:
两条 finding 我来立dev 的 dedup 读通道不可用(容器 REST 已死:
声明的收窄,接受repo-wide
CI 全绿后由我撤草稿并 arm。 Generated by Claude Code |
Fixes #12985
Both
@objectstack/metadata-core#./testingand@objectstack/service-cluster#./testingadvertised a
requirecondition that could never load. Both subpaths re-exportvitest, whose CJS entry is a singlethrowby design, so the bytes parse and theload dies inside vitest itself. This drops the condition and deletes the two ledger
entries
#12984left behind for it, in the same change — the ledger is SHRINK-ONLY.What changed (4 files)
1. The two manifests — the
./testingsubpaths become ESM-only:The shape is copied, not invented. It is the repo's one existing ESM-only subpath:
create-objectstack#./created-summaryspells exactly{ "types", "import" }with norequire. Enumerated mechanically over all 74 manifests underpackages/— thatsubpath is the only precedent, and
./testingon these two packages were the onlytesting-ish subpaths in the tree.2.
scripts/dual-build-cjs-loads.baseline.json— the two entries deleted by name(
@objectstack/metadata-core#./testing,@objectstack/service-cluster#./testing), the$commentuntouched. The edit was done as a JSON round-trip whose fidelity was provedfirst: re-serialising the unmodified file reproduced it byte-for-byte, so the whole
diff is the two deletions. That the ledger now reads
"entries": {}is a fact aboutthis tree, not an acceptance criterion — the
$commentis explicit that its steadystate is not empty, and no other entry was touched because there was no other entry.
3. A
minorchangeset for both packages carrying the FROM → TO and the one-line fixfor anyone who did spell the call as
require.The two things the card asked to measure, not assume
Is
dist/testing.cjsstill needed? — Decision: keep emitting it. Not deleted.Both
tsup.config.tsfiles declareformat: ['esm', 'cjs']for both entries(
src/index.ts,src/testing.ts), sodist/testing.cjskeeps being emitted. After thischange nothing in either manifest points at it, and neither package exports
./dist/*,so it is unreachable through the package. It is therefore dead weight — but deleting it
is a build-config change, not an exports change:
formatper config, so per-entry formats mean splitting each configinto an array of two. Both configs also set
clean: trueandsplitting: true;a second config would wipe the first's output and break chunk sharing between
indexandtesting. That is a real risk with its own verification surface, and itis not folded into an exports repair.
check:dual-build-cjs-loadsparses every emitted CommonJSfile of any package that has a
requireentry point, and both packages still have oneat
., sodist/testing.cjsstays under the PARSES sweep. Deleting it would removethat coverage.
check:published-filesasks that thefileswhitelist coversevery declared entry point (removal can never violate that) and that it admits no test
or build tooling;
dist/is unchanged either way. Verdict on this branch:✓ check:published-files — 69 publishable package(s) ... declare a files whitelist that covers every entry point.Are there real
require()consumers of these subpaths? — Measured: none.grep -rover the working tree (a filesystem walk, so untracked files are included;node_modules,.git,.turbopruned) — deliberately notgit grep, which readstracked files only:
@objectstack/metadata-core/testingimportstatements, 3 in CHANGELOG/doc prose@objectstack/service-cluster/testingimport, 1 docstringrequire(call namingmetadata-coreorservice-clustertesting.cjsZero
require()call sites. Positive control on the same command shape, with a term inno substring relation to the tested ones — a
require(call naming any@objectstack/specifier — returns 22 hits, so the scan is live rather than silently empty.
The in-repo consumers all reach these subpaths through
import:@objectstack/metadata-fs,@objectstack/metadata-protocol,@objectstack/rest,@objectstack/runtime,@objectstack/service-cluster-redis.Independently of any scan, a working CJS consumer cannot exist: the load fails inside
vitest for every caller and every code path, which is the whole reason the ledger entry
was written.
Pinned sibling (AGENTS.md §Workflow 4). objectui at the pinned
.objectui-sha190fbd01d0615e2e168faf9e08b8ad7844bc039d: 0 hits for either subpath, and 0 hits for@objectstack/(metadata-core|service-cluster)at all. Positive control on the same scan:@objectstack/specreturns 35. The Console Pin Gate cannot see this removal.Reverse verification
Both legs mutate a file the gate reads directly from the working tree
(
manifestPaths()walkspackages/**/package.json;readLedger()reads the baseline).No source is compiled into
dist/on this path, so no rebuild sits between the mutationand the verdict —
dist/was built once, in full, before any of this(
pnpm build, 71/71 tasks). Each leg restores under atrap ... EXIT INT TERMusingabsolute paths and
git checkout HEAD --, and the restore is proved by blob hash plus anempty
git diff HEAD, never by an exit code.Ablation A — put the
requireconditions back, ledger stays empty. Predicteddirection: RED.
The two mutated blobs are byte-identical to the pre-fix blobs on
origin/main, so theablation reconstructed the old manifests exactly rather than approximating them. Restored:
both worktree blobs back to their HEAD blobs,
git diff HEAD0 lines,git status0 lines.Ablation B — restore the two ledger entries, keep the exports fix. This one is a
claim check, and the claim did not hold. Predicted direction: GREEN, from reading the
gate; measured: GREEN.
So a PR that had fixed the exports and forgotten the ledger deletion would have been
green, with two stale exemptions left in the tree and not even listed as
declared:hits.
staleLedgeronly fires for a ledgered id that is still in the collectedpopulation and now loads; an id that has left the population is never consulted.
That is exactly the shape a manifest-level repair produces — the repair this ledger's own
reasons prescribed. Reported to the PM as a separate finding rather than repaired here:
it is a different defect class from this card, and closing it needs a new
--self-testcase, so it is not a bounded in-place fix. Deleting the entries is correct either way and
this PR does it.
(The first run of leg B aborted with
FATAL: mutation did NOT land on diskand measurednothing — the anchor
grep -cexpected 1 but the id appears twice in the restored file,the second time inside the sibling entry's
reasonprose. The guard was wrong, not themutation; the anchor was tightened to the entry-key line and the leg re-run. Recorded
because a silently re-run ablation is the same defect one level up.)
Verification — all at
68fb6915f, on a clean treepnpm build— 71/71 tasks successful (full, once, before any measurement).Gate families derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackafter the final commit. All 32 derived families run, real exit codes captured before any
pipe. 31 exit 0; 1 NOT MEASURED:
✓ check:dual-build-cjs-loads — 103 published require entry point(s) across 67 package(s) load; 613 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree.(was 105 / 618 / 2 declared before this change; the deltas reconcile exactly — 2 rows removed, andcjsFileCountcounts per row, so the 3 CJS files of metadata-core and the 2 of service-cluster leave the sum once each)✓ check-dual-build-cjs-loads self-test: 37 cases pass✓ check:published-files — 69 publishable package(s) ... covers every entry pointcheck-type-source-resolution OK — 94 tsc program(s) across 77 packages scannedcheck-test-source-alias OK — 72 packages with tests scanned; ... 45 published subpath(s) resolved through every alias table✓ check-adr-0087-registration: this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).✓ This diff introduces no major bump.·✓ No empty-frontmatter changeset introduced by this diff✓ .changeset/config.json "fixed" group is in sync with 69 public workspace packages.check-nul-bytes: OK (scanned 7203 text file(s) ... no raw ASCII control bytes)scripts/pm/check-half-states.mjsexits 3 — PREREQUISITE NOT MET,not a finding: the container's
GITHUB_TOKENis the proxy placeholder (len 14, noGitHub prefix,
GET /rate_limitgives HTTP 401). Its own text: "Nothing was swept ...this result says NOTHING about whether the board carries half-states." It is a PM board
sweep matched only via the
.changesetglob, unrelated to this diff.Tests and typechecks (heavy runs serialised through
scripts/pm/os-verify-lock.sh):@objectstack/metadata-core14 files / 234 passed;@objectstack/service-cluster4 files / 66 passedmetadata-fs 31, metadata-protocol 48, rest 123 (3 files), runtime 85,
service-cluster-redis 28 → 315 passed, 0 failed
typecheckfor the 5 consumers that declare one (Scope: 5 of 79 workspace projects,each echoing its script name) — all
Done, includingcheck:test-typecheck: OK — @objectstack/rest's test layer compilesDeclared narrowing: repo-wide
pnpm lintwas not run locallyNot "skipped" — measured, with the three pieces of evidence that make a narrowing a
measurement:
files:blockin
eslint.config.mjsis scoped to**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}or narrower.No block matches
.jsonor.md.--format json: running eslint over exactly the 4 changedpaths reports 4 results,
errorCount=0on each, every one carrying"File ignored because no matching configuration was supplied."
(
eslint.config.mjsline 328: "noparserOptions.project, no typed@typescript-eslintrules"), so nothing in this diff can move a verdict on a file itdoes not contain. This diff contains no linted file at all.
CI runs the full farm regardless.
Note on the gate derivation and a moving
origin/maindispatch-gates.mjsprinted a STALE TREE warning:origin/mainadvanced 8 commits(
15d55fb24tod028b37cb) while this branch was being verified, and 3 files thederivation reads changed in that range. The warning is real; its effect on this
answer is zero, and that is proved rather than assumed:
.github/workflows/lint.ymlis the only one of 28 workflow files that changed, and itchanged 0 non-comment lines — no gate step added, removed or renamed.
scripts/pm/dispatch-gates.mjsgained 0 non-comment lines (62 added lines, allcomment) — the derivation logic is behaviourally identical.
scripts/engine-double-contract.pinned.jsonis the ledger of a family whose source didnot change and which none of these 4 paths matches.
The 4 changed paths are disjoint from all 30 files in that range, so there is no conflict
either. Re-verifying on a merge would restart the whole run against a target that moved 8
commits in about half an hour; the family list is the thing that had to be current, and it
is.
Generated by Claude Code