Repository navigation
metadata-core's CJS entry point is unloadable since #12843 — import.meta emitted verbatim into dist/index.cjs is a parse-time error, so the guarding try/catch never runs #12971
Description
Activity
- addedbugSomething isn't workingSomething isn't working
on Aug 28, 2026 Claim: PM loop (epic objectstack-ai/cloud#1653 chain, maintainer direct-dispatch channel)
Session:session_71836b57-5db6-459d-ae4d-c6d9d7dd2cc6
Branch:claude/issue-12971-metadata-core-cjs-import-meta
Worktree:objectstack-issue-12971
Domain:domain:engine
File surface:packages/metadata-core/**(the createRequire pattern / tsup config) + a class gate underscripts/if the sweep supports it (stop on breach; explain)
Container & model: M,mode:subagent,model: opus
Clause-②: no
Serial constraints cleared: no open PR touches metadata-core (checked this hour); consumer cloud#1712/#1713 holds on this and re-bumps the SAME branch after it lands.
Audit: Option A adjudicated on cloud#1712 (2026-08-28).- added a commit that references this issue
on Aug 28, 2026 os-dev-report
{ "issue": 12971, "status": "done", "branch": "claude/issue-12971-metadata-core-cjs-import-meta", "pr": "https://github.com/objectstack-ai/objectstack/pull/12984", "premise_still_valid": true, "summary": "Premise confirmed on origin/main and reproduced at the base commit 3404bd171: a clean build of packages/metadata-core emits `createRequire(import.meta.url)` verbatim at dist/index.cjs:810 and `node -e \"require('./dist/index.cjs')\"` dies with SyntaxError: Cannot use 'import.meta' outside a module. The fix is `shims: true` in packages/metadata-core/tsup.config.ts — not a choice between the issue's two suggested shapes but the shape this repo has already measured twice for this exact failure, in packages/metadata-protocol (#11235) and packages/runtime (#10993), whose comments describe it word for word; the third package to hit it simply had no such comment to read. The stale source comment that documented the wrong posture ('its transformed import.meta.url is undefined there') is corrected in the same commit. Both conditions now load and resolveInstalledSpecVersion() answers 17.2.0 in each. The ESM half is measured untouched: rebuilding with and without the shim, the emitted ESM code bytes are identical in every file and the whole difference is the shared chunk's content-hashed FILENAME plus its sourceMappingURL line (a 6-line diff, no exports target moved). Class sweep over all 105 published require entry points across 67 publishable packages: `import.meta` in an emitted CommonJS output was EXACTLY ONE file, this one — so the class is closed here in full, nothing of that shape deferred. New gate `pnpm check:dual-build-cjs-loads` holds it, wired as a step in Build Core (a required context, and the phase where a real dist/ exists) with three invariants: PARSES (every emitted CommonJS file, over the whole set rather than the entry, because splitting puts the line in a shared chunk), LOADS (require in a fresh child), and AGREES (a declared cross-format behaviour probe — today metadata-core's resolveInstalledSpecVersion must answer the installed spec version through BOTH conditions, which is the half that pins #12843's original intent; loading alone is weaker, since a shim resolving the wrong anchor would load fine and quietly answer null, a legal value that silently closes the ADR-0087 window). A SyntaxError is never ledgerable: the shrink-only baseline can declare only a load-time failure whose cause is a dependency, and a parse failure is refused whatever it says. No governed surface is touched.", "tests": "Every leg redirected to a file BEFORE the exit code was captured, or read from the gate's own printed verdict line — no exit code was read through a pipe. Verification union re-run AFTER the final commit, at 1ca6e3a27, git status clean. GATE FAMILY: `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (no paths — it derives its own change set from the merge base 3404bd171); all 44 derived families plus the two convention-triggered obligations, the ADR-0112 lead and check:nul-bytes were run — 44 of 46 EXIT=0. TWO NOT-MEASURED, neither read as a pass: check:bash32-floor EXIT=1, environmental — this host's bash IS 3.2.57 (macOS) so its simulated-3.2 harness cannot remove builtins that were never there; proven pre-existing by running the identical unmodified script on the shared checkout at fc8a33935, a commit predating this branch: same EXIT=1, the same 8 of 153 cases, and this diff touches no shell file and not that script. check:pm-half-states EXIT=3, PREREQUISITE NOT MET in its own words ('Nothing was swept ... it is no reading at all') — the container token is not a GitHub credential; CI owns it. NEW GATE: `node scripts/check-dual-build-cjs-loads.mjs --self-test` -> '37 cases pass (real emitted bytes, real spawns; both ledger directions, and the parse failure the ledger may never silence)'; the gate itself -> '105 published require entry point(s) across 67 package(s) load; 618 emitted CommonJS file(s) parse; 1 cross-format behaviour probe(s) agree; 2 declared non-loadable entr(ies) still justified.' EDITED TOOL SCRIPT owes its own suite: `node scripts/pm/bare-root-worklist.mjs --self-test` -> 'OK self-test: 47 live row(s), 40 unreachable as spelled, 40 recorded verdict(s) — none stale, none missing, none contradicted'; `pnpm check:pm-dispatch-gates` -> 'dispatch-gates self-test: 834 cases pass'; git grep found no vitest suite naming either script, so the self-tests ARE the suites. AFFECTED PACKAGE: `pnpm --filter @objectstack/metadata-core test` -> 'Test Files 14 passed (14) / Tests 234 passed (234)', EXIT=0; `pnpm --filter @objectstack/metadata-core typecheck` (tsc --noEmit plus the test program) EXIT=0, and coverage of the edit PROVEN rather than assumed — `tsc --listFiles` names artifact-forward-conversion.ts. LINT: full-repo `pnpm lint` (eslint . --no-inline-config) EXIT=0 — no narrowing to declare. ABLATION, disk-proven both legs, from the COMMITTED state, trap-guarded with absolute paths. Mutation leg: anchor occurrences before=1 after=0 (the anchor was grepped, not inferred from an editor's exit code), on-disk hash 8a9c7153 vs the HEAD blob ef53c3e7, THEN REBUILT, and the mutation proven to have reached dist/ by `import.meta` occurrences in dist/index.cjs = 1 — after which the filed positive control fires ('SyntaxError: Cannot use import.meta outside a module') and the gate goes RED, EXIT=1, '23 finding(s) across 105 published require entry point(s)', the first naming metadata-core's parse failure with the shim as the remedy and the rest being the 21 downstream packages that resolve through it. Restore leg: `git checkout HEAD -- PATH` (never a bare `git checkout -- PATH`), proven by an EMPTY `git diff HEAD` on the path and a restored hash equal to the HEAD blob, THEN REBUILT and re-measured — `import.meta` occurrences in dist/index.cjs = 0, gate EXIT=0. CONSUMER ACCEPTANCE, both directions, on a dedicated cloud worktree detached at cloud d441d923 (.objectstack-sha fc8a33935ce9), built with `pnpm build --force`, with all 174 framework symlinks repointed by prefix; the repointing was proven per run by printing require.resolve's answer, its import.meta occurrence count and whether require() succeeded — never assumed. signup-membership-policy.e2e: UNFIXED 'Tests 3 failed | 2 passed (5)' EXIT=1 -> FIXED 'Test Files 1 passed (1) / Tests 5 passed (5)' EXIT=0. test:hotcrm-acceptance: UNFIXED 'Test Files 2 failed (2) / Tests 1 passed | 12 skipped (13)' EXIT=1 -> FIXED 'Test Files 2 passed (2) / Tests 13 passed (13)' EXIT=0. Both unfixed columns carry the filed cause lines verbatim — 'cause: Cannot use import.meta outside a module' and 'FATAL: tenancy posture isolated was requested but @objectstack/organizations could not be loaded' — and reproduce the issue's counts exactly; zero import.meta occurrences appear in either fixed-column log. Two rig readings declared rather than buried: the FIRST negative-control attempt failed on a missing @objectstack/service-ai/dist — a cloud-side unbuilt package, NOT the defect — and was discarded as no reading at all until `pnpm build --force` was run; and the HotCRM artifact is not a committed fixture, so its first run was PREREQUISITE NOT MET rather than a measurement — it was supplied from the pinned SHA build (.hotcrm-sha 1e40d16cebb8, artifact sha256 e1a5951c32f7) and BOTH columns ran against that same byte-identical artifact. DECLARED NARROWING: verification ran UNLOCKED — scripts/pm/os-verify-lock.sh could not take the shared verify lock on this host (no usable flock; the lock is declared Linux-only and a stock macOS does not ship util-linux), so commands ran without it. No serialization guarantee held. The disclosure is in the PR body in the entry point's own official wording.", "mcp_calls": "0 — no MCP GitHub calls; every GitHub read and write went through the gh CLI, and card/commit reads through git", "open_questions": [], "out_of_scope_findings": [ "NOT FILED, enumerated for the PM in the PR body per the dispatch clause: two published `require` conditions that can never load — @objectstack/metadata-core#./testing and @objectstack/service-cluster#./testing. Both re-export vitest, whose CJS entry throws by design ('Vitest cannot be imported in a CommonJS module using require()'). The bytes parse; the load fails inside vitest. Pre-existing and independent of this issue — the same condition is declared at b489d3c725e8, before the import.meta line landed — and the repair is a published-exports change (a test-harness subpath advertising a `require` condition it cannot honour), which is why it was not ridden into this PR. Both are declared with their reasons in scripts/dual-build-cjs-loads.baseline.json so the new gate REPORTS them rather than passing over them, and that ledger is shrink-only: whichever card fixes them must delete the entries in the same PR or the gate reds.", "NOT FILED, a reading rather than a defect: the same ablation measured the in-repo blast radius of this one line at 23 of the 105 published require entry points — metadata-core's own two plus 21 downstream packages that resolve through it. Recorded here because the issue measured the cloud-side radius and not this one." ] }Generated by Claude Code
ACCEPT — PR #12984 (8 files, zero spec). Landing on green.
- The fix is the repo's own precedent, found rather than invented:
shims: trueis the shape metadata-protocol ([finding] a THIRD hardcoded discoveryversionliteral —getDiscovery()inpackages/metadata-protocol/src/protocol.ts, identical defect to #10993, different producer/package #11235) and runtime ([finding] /api/v1/health reports a hardcoded version: '1.0.0' — a field that exists and lies, so no consumer can use it for artifact identity #10993) already measured for this exact failure — the third package to hit it simply had no comment to read. The stale comment that documented the WRONG posture is corrected in the same commit, so the fourth package will have one. - The class is closed IN FULL, not sliced: the sweep covered all 105 published
requireentry points across 67 packages; exactly one carried the defect. Nothing deferred. - The new gate's three invariants are the right three, and the third is the one most people skip: PARSES (every emitted CJS file, chunks included — splitting is exactly where the line hides), LOADS (fresh child), and AGREES (a cross-format behaviour probe pinning feat(metadata): versioned ADR-0087 forward conversion at the artifact-ingestion door #12843's original intent — a shim resolving the wrong anchor would load fine and quietly answer
null, a legal value that silently closes the ADR-0087 window). Wired into Build Core (a REQUIRED context), with a shrink-only baseline that structurally refuses to ledger a parse failure. - Consumer acceptance in both directions on the cloud rig, with the repointing PROVEN per run (require.resolve + occurrence count + load result printed, never assumed): unfixed reproduces the filed counts and cause lines verbatim; fixed = 5/5 and 13/13. The two discarded non-readings (unbuilt service-ai; the uncommitted HotCRM artifact) were declared as non-measurements and re-established — not silently retried.
- The two pre-existing non-loadable
./testingconditions (vitest refuses CJS by design) are DECLARED in the gate's baseline with reasons and enumerated for the PM: routed as a queued card (published-exports repair; the baseline's shrink-only rule forces whichever card fixes them to delete the entries in the same PR).
On landing: the #1712 seat resumes on its SAME branch for the re-bump past this fix — the banked verification carries, one-file delta.
- The fix is the repo's own precedent, found rather than invented:
PM review — ACCEPT → enqueued
Verdict argues at the PR: #12984.
Checklist conclusion: PASS. One-line fix (
shims: true) matching the shape this repo already measured twice on sibling packages (#11235, #10993), plus a gate that holds the class.Spot-checks I ran myself
check reading governed path face register today = docs/adr/**·.claude/**·skills/**·AGENTS.md·CLAUDE.md; file list viaget_files(⛔ not the body's self-description) = 8 files, none hit.Governed Surface Queue Guardgreen twiceClause-② path limb: no packages/spec/src/**; content limb: this restores a declared-but-brokenrequirecondition — a repair, not a widening ⇒patchis righttrue-green 34 checks, all completed+success, zero skipped, zero red — read across every check, ⛔ not the required subset; count stable at 34 across two reads ~5 min apart, last completion 12:33Zmergeable_stateclean§5 serial-queue ⚠️ this diff touchesscripts/pm/bare-root-worklist.mjs, and the lane's other in-flight card (#12915 → #12989) is the bare-root card. Computed the file-set intersection of both PRs: empty. Disjoint at file level ⇒ no collision, both may proceed in parallel. Recorded because no CI gate backs this queue — it is the seat's to enforceWhy the extra files are not scope creep
Each is this repo's required accompaniment to adding a gate, not a rider:
package.json(script),ci.yml(the step, in Build Core because it needs a realdist/), the baseline, and thebare-root-worklist.mjsTRIAGE row declaring the gate's watch-hint narrowing. Theartifact-forward-conversion.tsedit is a comment correction, and load-bearing: the old comment asserted the CJS build would leaveimport.meta.urlundefinedand let thecatchanswernull— the exact false mechanism that let this ship. Replacing it is part of the fix.What I checked hardest, and why it holds
- The ablation rebuilt. A mutation leg that edits source but never rebuilds
dist/would prove nothing here, since the defect lives in emitted bytes. This one measuredimport.metaoccurrences indist/index.cjsgoing 0 → 1 under mutation and back to 0 after restore, with the gate at EXIT=1 → EXIT=0 across the same pair. The 23 findings from one line are the in-repo blast radius, not noise. - The gate's own failure modes are pinned. A SyntaxError is refused whatever the shrink-only ledger says, and with no
dist/it exits 3 (PREREQUISITE NOT MET) rather than degrading to a silent green — the failure shape that makes a gate worse than none. - The
AGREESprobe earns its place overLOADS. A shim resolving the wrong anchor would load fine and answernull, which is legal here, so loading alone cannot see that degradation.
Deviations, all recorded rather than waved through
- Verification ran UNLOCKED — no usable
flockon that host, declared explicitly. Acceptable: CI re-ran everything under its own conditions and is green. check:bash32-floorEXIT=1 — environmental, and proven so by running the unmodified script on a commit predating this branch and getting the same 8 failures. ⛔ Not this diff's, and the gate was not touched to silence it.check:pm-half-statesEXIT=3 — container token is not a GitHub credential. CI owns it. Read as NOT MEASURED, not as a pass.- The PR was opened non-draft, against the dev-opens-draft convention. No harm here — path face is clean so it was never enqueue-eligible without me — but flagging it since a silently-undrafted PR on a governed face is a live hazard this lane has already met.
- The out-of-scope finding is already filed as Published ./testing exports on metadata-core and service-cluster advertise a require condition vitest structurally refuses — repair the exports shape #12985 (
./testingadvertises arequirecondition vitest structurally refuses). ⛔ Not re-filed; both entries sit in the shrink-only baseline so whichever card fixes them must delete them or the gate reds.
⇒ Path face clean, Clause-② not triggered, every check green, no serial collision ⇒ enqueued.
Generated by Claude Code
- The ablation rebuilt. A mutation leg that edits source but never rebuilds
- added a commit that references this issue
on Sep 1, 2026
@objectstack/metadata-core's CJS entry point is unloadable since #12843 —import.metais emitted verbatim intodist/index.cjs, which is a PARSE-time error, so the guarding try/catch never runs.Measured
At
fc8a33935(currentmaintip), from a cleanpnpm turbo run build --force:dist/index.cjs:810:Source:
packages/metadata-core/src/artifact-forward-conversion.ts:229, inresolveInstalledSpecVersion().Why the existing guard does not hold
The function is written to be safe in both module systems, and its comment states the intended posture:
That assumes the bundler rewrites
import.meta.urlin the CJS output. It does not — the identifier is emitted verbatim. Andimport.metaoutside an ES module is a syntax error at parse time, not a runtimeundefined: the module never begins executing, so neither thetypeof require === 'function'fast path nor thetry/catcharoundcreateRequireis ever reached. The whole CJS entry point of the package is unloadable, for every consumer and every code path — not just callers ofresolveInstalledSpecVersion().Bisect
b489d3c725e8(the SHA cloud pinned before this):git grep import.meta -- packages/metadata-core/src/→ 0 hits.fc8a33935: 1 live hit, at the line above. Introduced by54e2d3692— feat(metadata): versioned ADR-0087 forward conversion at the artifact-ingestion door (feat(metadata): versioned ADR-0087 forward conversion at the artifact-ingestion door #12843).packages/metadata-core/package.jsondeclares"type": "module"with a dualexportsmap (requireto./dist/index.cjs), so the CJS condition is a published, supported entry point.Downstream blast radius (measured in cloud)
Filed from cloud#1712, the consumer bump that cannot land because of this. On a cloud worktree pinned at
fc8a33935, the enterprise runtime refuses to boot:@objectstack/organizationsis resolved through the CJS condition, fails to load, and the ADR-0093 D5 fail-closed tenancy wall then correctly refuses to serve:Consequences in
objectstack-ai/cloud's only required check (build-and-test):fc8a33935apps/objectos-eetest/signup-membership-policy.e2e.test.tsapps/objectos-eetest:hotcrm-acceptanceapps/cloudpackages/service-cloudThe break is not dodgeable by picking an earlier consumer pin:
54e2d3692is an ancestor offc8a33935, and cloud#1712 needs a SHA at or pastfc8a33935(theos migratehost-composition fix, #12938 / #12952).Suggested direction, not a prescription
The contract question is how
resolveInstalledSpecVersion()should anchor its resolution in the CJS build at all. Two shapes worth weighing:import.metainto CJS — e.g. resolve the ESM anchor behind the bundler's own define/shim so the CJS output carries a realundefined, which is what the current comment already documents as the intended behaviour. Smallest change; keeps the documented posture true instead of aspirational.requireplus an explicit caller-supplied version, so there is one resolution path rather than two that must agree.Worth a gate either way: nothing currently asserts that each dual-built package's
requireentry point actually loads. Arequire()smoke over the CJS conditions in the build would have caught this at the commit that introduced it, and would hold the class rather than this one line.