Skip to content

fix(core): one declaration serves both module formats, so exports give types per condition - #26279

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-l5hgva
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-l5hgva

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #26205

Clause-②: no
Metadata-protocol: none

@objectstack/core was the one published package still handing a require consumer its ESM-kind declaration (attw FalseESM on . and ./logger). Serving the .d.cts per condition, as #26198 did for 38 other packages, broke consumers, because dist/index.d.ts and dist/index.d.cts were two separate declarations of the same classes and ObjectKernel's private plugins makes the two copies incompatible types.

What changed

  • packages/core/tsup.config.ts: the declaration pass runs once, for ESM only (a dts: { only: true } half). The JS half (ESM + CJS, unchanged options) writes dist/index.d.cts and dist/logger.d.cts in its onSuccess as one line each, export * from './index.js'; / './logger.js'. The declaration half never emits a .d.cts, so the two halves tsup runs concurrently never write the same file, and clean stays on the JS half only (tsup's clean keeps declaration files).
  • packages/core/package.json: both subpaths become { import: { types, default }, require: { types, default } }, the shape chore(packages): publishing metadata for the 73 published packages — per-format types, sideEffects, publishConfig, and 18 missing READMEs #26198 used. No subpath added or removed, no JavaScript target moved; the top-level main/types are unchanged.
  • scripts/check-dual-build-cjs-loads.mjs: the two @objectstack/core rows leave TYPED_EXEMPTIONS. The table is shrink-only and the scan reports a row whose entry now resolves a CommonJS declaration as stale, so they go in the same PR; the header paragraph that described core's block now says where the cure lives. This file was added to the claim's file surface by the seat before the edit.
  • .changeset/26205-core-one-declaration-per-condition.md: @objectstack/core patch.

Why this direction and not the reverse

Tried first the other way round: declarations in the .d.cts, index.d.ts re-exporting ./index.cjs (ESM may import CJS in every TypeScript mode). That made core's own imports of @objectstack/spec resolve through spec's require condition, and spec's own twin declarations then failed @objectstack/driver-memory's build: TS2345 at src/memory-driver.ts:1469, ComposedReadableIds not assignable to itself. Keeping the declarations in the ESM .d.ts means everything core's types import resolves exactly as it did when both conditions read the .d.ts.

The cost of this direction is TS1479 inside core's .d.cts for a CommonJS consumer on module: node16 (or on TypeScript 5.6 under nodenext). A consumer CJS file plus ESM file importing core in both resolution modes and assigning ObjectKernel across them, compiled against this branch's build and against origin/main's:

TypeScript · module origin/main, skipLibCheck: true this PR, skipLibCheck: true this PR, skipLibCheck: false
6.0.3 · nodenext 0 errors 0 errors 0 errors
6.0.3 · node16 2 × TS1479 in the consumer's own file 0 errors 2 × TS1479 in core's .d.cts
5.6.1-rc · node16 2 × TS1479 in the consumer's own file 0 errors 2 × TS1479 in core's .d.cts
5.6.1-rc · nodenext 2 × TS1479 in the consumer's own file 0 errors 2 × TS1479 in core's .d.cts

So the diagnostic that origin/main raises at the consumer's import, where no option silences it, moves into the package, where skipLibCheck does. The cross-mode ObjectKernel assignments type-check in every cell. (The 5.6 runs also print errors from ai/@ai-sdk and @objectstack/spec declarations with skipLibCheck: false; those are unchanged by this PR and not counted above.)

Acceptance

  • Premise reproduced on origin/main (f2e70836): with only the export map split and the declaration build unchanged, pnpm --filter @objectstack/plugin-dev build exits 1 with src/dev-plugin.ts(523,32): error TS2345: Argument of type 'ObjectQLPlugin' is not assignable to parameter of type 'Plugin'.; unsplit, exit 0.
  • attw (@arethetypeswrong/cli 0.18.5) on the pnpm pack tarball of core: before 3 problems (FalseESM × 2, NoResolution on ./logger under node10), after 1 (NoResolution on ./logger under node10). FalseESM: 2 → 0. The remaining node10 row is the known no-exports-support class, left out of scope as in chore(packages): publishing metadata for the 73 published packages — per-format types, sideEffects, publishConfig, and 18 missing READMEs #26198.
  • pnpm --filter @objectstack/plugin-dev build: exit 0 at 01ec8f2e (inside the force build below).
  • dist file names: packages/core/dist lists the same 12 file names before and after (plus its two .build-input-hash* stamps), and the packed tarball the same 18 paths, diff clean. What changed is content only: index.d.cts 579,364 → 28 bytes and logger.d.cts 3,963 → 29 bytes (now re-exports); index.d.ts and logger.d.ts are the same declarations as before.

Verification

All at head 01ec8f2e, which merges origin/main at 0453fabe (that brought in #26203's tsconfig base change to core and plugin-dev):

  • turbo run build --force --filter='./packages/**': 75 of 75 tasks successful, including @objectstack/verify, @objectstack/plugin-dev and @objectstack/driver-memory, the three builds that met this hazard before.
  • pnpm --filter @objectstack/core typecheck exit 0; pnpm --filter @objectstack/core test: 121 files, 2,778 tests passed.
  • dispatch-gates --commands derived 93 families for this diff; every one exits 0, and --ran reconciles at 93 derived, 93 run, 0 NOT-MEASURED (exit codes recorded). check-plugin-teardown-shape.mjs --self-test first refused on the shallow clone (exit 3) and passed after git fetch --unshallow. Named for this diff: check:dual-build-cjs-loads (self-test 93 cases; scan: 112 require entry points across 69 packages, 112 require conditions resolve a CommonJS-flavoured types that exists), check:dts-closure, check:lean-entry-closure, check:published-files, check-dts-emitted.mjs --self-test, check-empty-changeset.mjs, check-adr-0087-registration.mjs.
  • Repo-wide scans (pnpm lint first) and the type-check lanes are CI's.

Acceptance notes

  • No path under packages/plugins/plugin-dev/**, ui/** or any governed surface is touched.

Generated by Claude Code

…e types per condition

Core's declaration pass runs once, for CJS; the ESM .d.ts files are
one-line re-exports of the .d.cts ones, so ObjectKernel has one identity
whichever resolution mode reaches it. exports now hands each condition its
own types, and the two core rows leave TYPED_EXEMPTIONS in
check-dual-build-cjs-loads (shrink-only: the entries are now correct).

Claude-Session: https://claude.ai/code/session_01VPkxx6ackwXchvtVNidtum
Co-authored-by: Claude <noreply@anthropic.com>
…from the .d.cts

Putting the declarations in the .d.cts made core's own imports of
@objectstack/spec resolve through spec's require condition, and spec's twin
declarations then failed driver-memory's build (TS2345). With the ESM .d.ts
as the one declaration, everything core's types import resolves as before.

Claude-Session: https://claude.ai/code/session_01VPkxx6ackwXchvtVNidtum
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tooling labels Oct 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 1 changed file(s) yielded no anchor (packages/core/package.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/core/package.json) — pages documenting those are invisible to this run
  • 1 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 29 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 4f7e0414a8d44b139cf5c9bc7e37f207f1db1327 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b1c3eff61833c9b01c6c0854622cb00c215def33 — the merge of head 01ec8f2e9e6fd8fcd658552ecec8ede97dae1e8d into base 4f7e0414a8d44b139cf5c9bc7e37f207f1db1327, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b1c3eff61833c9b01c6c0854622cb00c215def33 && git checkout b1c3eff61833c9b01c6c0854622cb00c215def33
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4f7e0414a8d44b139cf5c9bc7e37f207f1db1327 01ec8f2e9e6fd8fcd658552ecec8ede97dae1e8d && git checkout -B drift-repro 4f7e0414a8d44b139cf5c9bc7e37f207f1db1327 && git merge --no-ff 01ec8f2e9e6fd8fcd658552ecec8ede97dae1e8d

node scripts/docs-audit/affected-docs.mjs --json 4f7e0414a8d44b139cf5c9bc7e37f207f1db1327

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · devx seat 2 · 2026-10-26T18:37Z

Seat domain:devx#2 (#20163) · session_01Q4MDXRbEJCNdPAP4SiXwp9 · reviewed head 01ec8f2e · card #26205, claim 6422211308 (delta N, widened by the seat to check-dual-build-cjs-loads.mjs). PR carries Fixes #26205.

  • Scope: 4 files, +101/−38: packages/core/tsup.config.ts, packages/core/package.json, scripts/check-dual-build-cjs-loads.mjs and one @objectstack/core patch changeset. Not governed. Nothing under ui/.
  • The design follows the card's first option.
    • The declaration pass runs once, ESM only (dts: { only: true }).
    • The JS half's onSuccess writes dist/{index,logger}.d.cts as one-line export * from './<name>.js', so both conditions hand out the same ObjectKernel.
    • The two halves never write the same file, and clean sits only on the JS half.
  • The export * re-export loses nothing. I checked that neither src/index.ts nor src/logger.ts has a default export.
  • The reverse design was measured and correctly rejected. Putting the declarations in .d.cts reddens driver-memory (TS2345 through spec's own twin declarations). The config header records it.
  • exports: both existing subpaths gain the per-condition {import:{types,default}, require:{types,default}} shape from chore(packages): publishing metadata for the 73 published packages — per-format types, sideEffects, publishConfig, and 18 missing READMEs #26198, and no subpath is added or removed. TYPED_EXEMPTIONS loses its two core rows, as the scan requires once require resolves to .d.cts. The comment now points at the cure rather than the old blocker.
  • Acceptance, from the dev's report on this head:
  • Consumer note, as the changeset states: a module: node16 CommonJS consumer's TS1479 moves from their own import into core's .d.cts, where skipLibCheck covers it. That is strictly better than today.
  • Landing: ready + auto-merge once every check on this head is green or skipped.

Generated by Claude Code

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/m tooling

Projects

None yet

2 participants