Skip to content

fix(build): give each exports condition its own types target in 27 dual-build packages - #13299

Draft
os-elon wants to merge 8 commits into
mainfrom
claude/issue-13112-unreachable-dcts
Draft

fix(build): give each exports condition its own types target in 27 dual-build packages#13299
os-elon wants to merge 8 commits into
mainfrom
claude/issue-13112-unreachable-dcts

Conversation

@os-elon

@os-elon os-elon commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Part of #13112

Implements the maintainer ruling of 2026-08-29 (decision batch #3, verbatim
「同意」): declare the files, do not delete them. 27 of the 28 dual-build
packages now give each exports condition its own types target. The 28th,
@objectstack/core, is held back at a measured boundary described below —
that is the one place this PR does not discharge the ruling, and it is the
reason this needs contract review rather than a rubber stamp.

⚠️ Published-surface declaration (clause ②) — NOT self-cleared

This PR changes the published surface of 27 packages. Concretely, per
package: the exports map is rewritten so import and require each carry
their own nested types. What that changes for a consumer:

consumer before after
ESM, node16/nodenext/bundler dist/index.d.ts dist/index.d.tsunchanged, byte for byte
CJS, node16/nodenext dist/index.d.ts (an ES-module declaration) → TS1479 dist/index.d.cts — the CommonJS declaration
node10 / moduleResolution: node root types field root types field — untouched
runtime (import/require of JS) unchanged unchanged — no emitted byte moves

Nothing is removed and no path that resolved before stops resolving. No package
version, entry point, or runtime artifact changes. 33 entry points across 27
packages; 34 .d.cts files totalling 5,296,847 B move from unreachable to
reachable. This is a widening of what the manifests name, not of what the
tarballs contain.

The defect is not the wasted bytes

The card measured 5.2 MiB of .d.cts files that no types condition points at.
Re-derived on this branch's build at 74049254d4: 28 packages · 36 files ·
5,517,701 B (5.26 MiB)
— the card's figures reproduce (it measured 5,497,247 B
at 2fde235c84; the drift is intervening source changes).

But the bytes are the symptom. Measured with tsc --traceResolution against a
real "type": "commonjs" fixture at moduleResolution: node16, resolving
@objectstack/metadata-core:

BEFORE  cjs consumer -> packages/metadata-core/dist/index.d.ts   tsc exit 2
        error TS1479: The current file is a CommonJS module whose imports will
        produce 'require' calls; however, the referenced file is an ECMAScript
        module and cannot be imported with 'require'.
BEFORE  esm consumer -> packages/metadata-core/dist/index.d.ts   tsc exit 0

AFTER   cjs consumer -> packages/metadata-core/dist/index.d.cts  tsc exit 0
AFTER   esm consumer -> packages/metadata-core/dist/index.d.ts   tsc exit 0   (unchanged control)

So the published require entry point was unusable from TypeScript, while
the JavaScript behind it loaded perfectly — check:dual-build-cjs-loads has
asserted that for months. The types said the supported entry point could not be
required. The ESM leg is the positive control: same harness, same run, verdict
unchanged, which is what makes the CJS leg a reading rather than a coincidence.

The triage comment's confidence gap — "does any consumer reach .d.cts by file
adjacency?" — is answered and the answer is no: TypeScript matches the
sibling types condition first, so it never falls back to adjacency. The
sibling types was answering for both conditions.

@objectstack/core is deliberately NOT changed — the measured boundary

Splitting one declaration into two makes TypeScript compare it nominally
when it carries a private member. ObjectKernel has private plugins and
travels into every plugin's init via PluginContext.getKernel(). Three
whole-repo pnpm build runs on this branch:

tree result
28 packages split (core included) RED@objectstack/verify, 5 × TS2345
27 packages split (core held back) GREEN — 71/71 tasks
28 + the 37-package ESM mirror RED@objectstack/plugin-dev, TS2345
error TS2345: Argument of type 'ObjectQLPlugin' is not assignable to parameter of type 'Plugin'.
  ...
  Types have separate declarations of a private property 'plugins'.

The third row is the informative one. It moves the failure instead of
removing it: splitting @objectstack/objectql is what fixes verify and what
breaks plugin-dev, because a split dependency resolves core one way from its
.d.mts and the other way from its .d.ts. This is TypeScript's dual-package
hazard on the type axis, and it is a property of the dependency, never of the
consumer that trips over it.

Core cannot be split until ObjectKernel's identity stops being nominal.
That is a decision about core's public types, not about an exports map, so it is
not taken here. Core keeps the sibling-types shape; its two .d.cts files
(220,854 B) stay unreachable, declared as such in the gate with the
measurement above attached, never silently skipped.

The gate (ruling point 2)

check:dual-build-cjs-loads gains a fourth invariant beside PARSES / LOADS /
AGREES:

TYPED — the declaration a require consumer resolves is CommonJS-flavoured
and is really on disk.

Three properties worth reviewing:

  • It judges module KIND, not the .d.cts extension. dist/index.d.ts is the
    correct CommonJS declaration in a CJS-first package and the wrong one in a
    "type": "module" package. An extension rule would red 50 correctly-spelled
    entries in this repo. The self-test carries that case as an explicit green
    control.
  • The resolver reads types at any depth, sibling included — otherwise the
    defective shape and a legitimately CJS-first package both report "no types"
    and cannot be told apart.
  • The vacuity floor counts entries JUDGED, never entries clean. Found by
    ablation: with the floor on the clean count, restoring the 28 defective
    manifests drove it 102 → 67 and the gate refused (exit 2, "nothing was
    read") instead of reporting its 35 findings — a real regression rendered as a
    broken instrument. Pinned.

Ablation, on the committed fix, mutation confirmed on disk before the reading
and restoration proven after (git diff HEAD empty, blob hash matches HEAD):

nested import branches BEFORE mutation: 35 -> AFTER: 0     (mutation confirmed)
gate exit under mutation: 1
✗ check:dual-build-cjs-loads — 35 finding(s) across 102 published require entry point(s)
ESM-flavoured findings: 35

35 findings at the base commit, 0 at HEAD, with 67 entries already correct at
base as the built-in control that the invariant is not vacuously true.

Verification (local; CI has a large queue backlog, so this is the evidence)

At 23fc0d875c, everything under the shared verify lock:

pnpm build                        71/71 tasks   (forced rebuild of core, verify,
                                                 metadata-core, plugin-dev, objectql:
                                                 38/38, 0 cached)
pnpm typecheck                    130/130 tasks
check:dual-build-cjs-loads        ✓ 102 require entry point(s) across 66 package(s) load;
                                    610 emitted CommonJS file(s) parse; 1 probe agrees;
                                    100 require condition(s) resolve a CommonJS-flavoured
                                    `types` that exists
                                    · 2 declared UNREACHABLE (core, with the measurement)
  --self-test                     ✓ 87 cases

All 20 gate families derived by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack on the final HEAD are green, plus check:nul-bytes
and check:empty-changeset. Re-deriving after the changeset landed added three
families the first derivation did not name (changeset-gate-self-tests,
objectui-changeset, pm-half-states); those are included.

Out of scope, deliberately

  • Ruling point 4 — the converse gate ("packed into the tarball, named by no
    manifest") does not ride along, per the ruling.
  • The ESM mirror. The same defect on the other side is larger: 38
    CJS-first packages ship 97 undeclared .d.mts files, 15,357,733 B (14.65
    MiB)
    , because their sibling types hands ESM consumers a CJS-flavoured
    .d.ts. It is measured here and not fixed here — it is a published-surface
    change in 38 more packages, and the third build row above shows it is not
    independently safe.
  • ObjectKernel's nominal identity, which is what actually blocks core.

The dedupe channel refused on both routes while this ran (repo-scoped REST 403,
gh absent, MCP search_issues rate-limited), so ⛔ nothing was filed blind;
the three findings are handed to the PM in the dev report for filing.

Needs needs:contract-review: 27 published manifests move at once, and the
@objectstack/core boundary is a maintainer call.

Generated by Claude Code


Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 27 changed file(s) yielded no anchor (packages/cloud-connection/package.json, packages/lint/package.json, packages/mcp/package.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 27 changed package(s)).

What this run could not see
  • 27 changed file(s) yielded no anchor (packages/cloud-connection/package.json, packages/lint/package.json, packages/mcp/package.json, …) — pages documenting those are invisible to this run
  • 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 — 56 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 74049254d47bd0edd2a2fcd732dcc01c91504f10packageMentionDocs.

@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants