Skip to content

build(spec): one module graph with shared chunks, so each schema ships once and is one object across entries - #25748

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-8133-spec-shared-chunks
Oct 25, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-8133-spec-shared-chunks

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #8133

Clause-②: no
Metadata-protocol: none
Ruling-ref: none

What changes

@objectstack/spec's JS build is now one esbuild module graph with shared chunks for every output (ESM and CJS, Node and browser-conditioned). A module two entries reach is one module in one dist/chunk-HASH.{mjs,js} file, so it ships once and is the same object through either entry. Only packages/spec/tsup.config.ts changes behaviour. The exports map and the declaration build are untouched.

Why a bare splitting: true was not enough (measured, then rejected)

With splitting: true on the existing two passes, ./shared and ./migrations (subpaths with no browser condition, so a browser bundler loads their ordinary bundles) started linking the chunk that imports pg-connection-string. esbuild assigns a module to a chunk by file-level reachability, so an entry links every chunk its import graph touches, used or not. Per-entry tree-shaking had kept them clean. That is the breakage class check:browser-reachable-entries rule 2 refuses: the controlled experiment below failed to bundle with Could not resolve "fs". A plain two-pass split would also have left the browser graph and the Node graph as two copies of everything they share.

What the config does instead (oneModuleGraph)

  • One graph. Entries: the 19 ordinary outputs at dist/ENTRY plus the 5 browser-conditioned outputs at dist/browser/ENTRY. The browser swap of the pg-grammar arm (the 2026-08-22 ruling's mechanism, keyed to the same seam specifier) now applies to the whole graph.
  • Node-side copy of only the seam's importers. The Node outputs of the 5 browser-conditioned entries are built from a node-side esbuild namespace. It re-instantiates exactly the modules from which the seam is reachable (measured at BASE: 28 of 459 source modules: the entry barrels, the driver configs, the datasource, stack and conversion/migration registries). Every other module resolves back to the shared graph.
  • Measured per build, not listed. A metafile pre-pass over those entries' Node graph computes the set. It refuses the build when no module imports the seam any more: then the browser conditions are moot, and withdrawing them is a decision.
  • Behaviour unchanged. The Node outputs of the conditioned entries still run the pg grammar. Every other output still never runs it. Their per-entry bundles linked no pg grammar at all, which the gate held at BASE. Where a chunk they share reaches the driver configs, they now carry the browser twin.

Measurements

All readings are on this worktree. BASE = origin/main 47af3dd. HEAD = 5a40a420 (this branch with origin/main c61f75d1 merged; every HEAD reading below was re-taken there, and was identical at the pre-merge head 19a8800a except the experiment, 602.9 KB there). Full pnpm --filter @objectstack/spec build both times.

BASE HEAD
a.defineConnector === b.defineConnector (root vs /integration), ESM false true
same, CJS false true
shared object/function exports identical across entries, browser view (119 pairs) 0 119
same, Node view (119 pairs) 0 114
.mjs files defining var FieldSchema = 14 1
.mjs files defining var ConnectorSchema = 3 1
total packages/spec/dist 123.5M 53.5M
JS + maps (excl. .d.ts) 93.9M 23.9M
declaration files 134 134 (same set, hash-stripped)
objectui import experiment, gzip 9 1,628.4 KB 603.6 KB (−63%)
  • Node view, 5 not identical. All 5 are root vs /shared: MAP_SUPPORTED_FIELDS, METADATA_ALIASES, normalizeMetadataCollection, normalizePluginMetadata, normalizeStackInput. They live in shared/metadata-collection.zod.ts, which reaches the seam, so the root's Node output carries the node-side copy and /shared (unconditioned) the shared one. Under the browser condition they are one object.
  • ConnectorSchema on the card. The card's a.ConnectorSchema === b.ConnectorSchema compares undefined to an object: the root entry does not export ConnectorSchema at top level, before or after. Only /integration does. defineConnector is the meaningful half, and it holds.
  • Controlled experiment. The card's experiment, re-derived on today's trees. objectui a3c0da9 (current main) value imports: 256 symbols across 11 subpaths, parsed with the TypeScript parser, type-only imports excluded. Each import comes from the subpath objectui uses. esbuild bundle + minify + platform: browser, gzip level 9, zod bundled in every arm.
    • BASE per-entry dist: 1,628.4 KB.
    • HEAD dist: 603.6 KB.
    • Control, one module graph from src with the same pure-annotation and swap plugins: 538.5 KB.
    • The card's figures (1,542 / 452 KB on 18.0.0, 232 symbols) are the same shape on an older tree.
  • Chunk graph. Acyclic in both formats: 0 cycles over 1,181 ESM and 1,158 CJS relative edges. This is why CJS takes shared chunks safely (Rollup converts each chunk ESM→CJS under tsup's treeshake, with no partially-initialized require). check:dual-build-cjs-loads loads every require entry and is green.

NOT MEASURED: objectui Bundle Analysis vendor-objectstack. That number comes from objectui CI on a pin that carries this change: objectui's own pin move, which also re-baselines its eager-closure ceilings. The experiment above is the local stand-in, not that number.

Deep-path consumers (risk: chunk names are not stable)

  • objectui a3c0da9: three @objectstack/spec/dist/index.{js,mjs} strings, all regex probe inputs in tests, not imports (scripts/__tests__/vite-objectstack-spec-dist.test.ts:481, :946, :1165; scripts/__tests__/vite-objectstack-client-dist.test.ts:398). Both files still exist.
    • scripts/objectstack-pin.mjs hard-links the package's whole tree, so chunks travel with it.
    • The vendor-objectstack group test matches any file under the package directory, so chunks group there.
  • HotCRM a4b3560: zero @objectstack/spec/dist / src / json-schema deep imports.

Gates and tests

All on 5a40a420 unless stated. Exit codes were captured before any pipe.

  • Derived union (node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack): 72 commands. 71 exit 0, 1 exit 1 (pnpm --filter @objectstack/spec run check:liveness, the main-red below). --ran reconciliation: 72 derived, 72 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero from 72 recorded exit codes. On the pre-merge head 19a8800a, all 72 exited 0. One of them, check-plugin-teardown-shape --self-test, first exited 3 because of a shallow clone, then 0 after git fetch --depth=1 of its pinned fixture commit.
  • Dist-reading gates on 5a40a420: check:browser-reachable-entries (48 bundles walked transitively; 10 server-only links, all on the Node side of the 5 conditioned entries), check:dual-build-cjs-loads, check:lean-entry-closure, check:dts-closure, check:sourcemap-no-sources-content, check:published-files and check:dev-prereqs (local-only; after rebuilding the two packages the merge moved): all exit 0.
  • Whole workspace build at 19a8800a, pnpm build: Tasks: 76 successful, 76 total, examples/* included.
  • Every consumer's tests at 19a8800a, root pnpm test (turbo run test test:repo, all 152 tasks, every package that reads @objectstack/spec through dist): Tasks: 152 successful, 152 total, lock VERDICT command-exit 0. The run was interrupted once by a worker restart and resumed from turbo's cache: 125 cached, 27 executed in the second run, and no failure in either run.
    • Inside it, the dogfood battery: Test Files 295 passed, 3 skipped (298); Tests 2520 passed, 18 skipped (2538).
    • @objectstack/cli (unit + integration): 500 passed, 1 skipped (501); Tests 6291 passed.
    • packages/qa/downstream-contract: 3 files / 31 tests passed.
    • @objectstack/spec: test 721/721 files, test:repo 79/79.
  • @objectstack/spec on 5a40a420: typecheck exit 0, including the test layer that compiles tsup.config.ts through pure-schema-construction.test.ts (that reading was taken at 19a8800a). test gives 720 passed, 1 failed (721); the failing file is scripts/liveness/check-liveness.test.ts (23 tests), the same main-red. test:repo was not reached after it.
  • Declaration build unaffected: the DTS-pass options object (BUILD_DTS=true, plugins by name) is identical at BASE and HEAD (sha256 prefix ccf850853f3c0699). There are 134 declaration files, the same set once hashes are stripped. The DTS pass is not byte-stable from run to run (tsup.config.ts's docblock), so it is compared by its inputs and its file set, not by a digest.
  • Ablations (scripts/ablation-replace.mjs, each leg verified on disk, each restored blob-identical to HEAD 9c9b83dc, then rebuilt). Predicted, then observed:
    • A1 (browser swap off): predicted red on rule 2. Observed red, 14 problems: all 10 dist/browser/** bundles plus ./shared and ./migrations in both formats. The first attempt was refused by the tool as a no-op (the replacement was a substring of the anchor), so nothing ran; re-run with a unique replacement.
    • A2 (node-side twinning off): predicted a positive-control red. Observed POSITIVE CONTROL FAILED.
    • A3 (seam regex matches nothing): predicted the build refuses. Observed one-module-graph: no module of the Node graph of … imports the pg-grammar arm in both the ESM and CJS runs.
    • Restore leg: rebuilt, check:browser-reachable-entries green, every measurement identical.

Main-red, not this PR's: check:liveness and check-liveness.test.ts. Green on 19a8800a, red after merging origin/main c61f75d1, and still red on origin/main f9a1e93f. This PR's diff vs main touches no ledger, no service-automation file and nothing under src/.

  • The failing row. The ledger row translation/flows.screens cites packages/services/service-automation/src/engine.ts#renderFlowTextSlot.
  • Why it was green before. engine.ts named screens only through a comment ("two screens down"). 0189524a (refactor(service-automation): move AutomationEngine.resumeInternal's body into run-resume.ts (#25222 slice 8b) #25734) moved that comment into run-resume.ts.
  • Why a repoint is not enough. renderFlowTextSlot itself now lives in node-text-rendering.ts, which never names screens either.
  • Proposed patch, for the owning lane (not applied here). Drop the engine.ts#renderFlowTextSlot clause from that row's evidence. builtin/screen-nodes.ts#registerScreenNodes, already cited, is the reader that spells the key. Or cite node-text-rendering.ts#renderFlowTextSlot with an evidence-scope note. Fix PR known to this run: none.

CI on this PR is the authority for the families above: the required five, Build Core's bundle-size step, Test Core.

Acceptance notes

  • Comments under packages/spec/src/** that describe the old layout. The dispatch puts src/** out of scope, so they are not edited here. Each is now inaccurate about the layout, not about behaviour:

    • src/index.ts:73: "Every subpath entry is a self-contained bundle".
    • src/conversions/registry.ts:18144: "Each published entry is one flat file (tsup.config.ts, splitting: false)". The PURE-annotation reasoning it supports still holds per chunk.
    • src/api/package-api-assembled.zod.ts:40: a historical sentence. Its TSDoc renders into the generated content/docs/references/api/package-api-assembled.mdx.

    Proposed: one comment-only follow-up, routed by the seat.

  • packages/spec/README.md (published) is corrected here. check:browser-reachable-entries' docblock said "this tree cannot exercise" a chunk walk; now it does, and the docblock says so.


Generated by Claude Code

Every JS output of @objectstack/spec is now one esbuild graph with
splitting, so a module two entries reach is one module in one shared
chunk. The browser-conditioned Node outputs are built from a node-side
copy of only the modules that reach the pg-grammar seam, measured per
build by a metafile pre-pass.

Claude-Session: https://claude.ai/code/session_01Q3XCv7AXcUSSD479Vt1LXy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Oct 25, 2026
@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 13 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/README.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/plugins/development.mdx (via outDir (symbol, a field of const object browserConfig))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/README.md) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 144 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 6679644a1a56b552d6864b125900221823ec78b3 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 196ac021819c13fba9f6a89756888791af33649a — the merge of head 5e9381000524a32cc8f9bebf09212e2b51cc93e9 into base 6679644a1a56b552d6864b125900221823ec78b3, 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 196ac021819c13fba9f6a89756888791af33649a && git checkout 196ac021819c13fba9f6a89756888791af33649a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6679644a1a56b552d6864b125900221823ec78b3 5e9381000524a32cc8f9bebf09212e2b51cc93e9 && git checkout -B drift-repro 6679644a1a56b552d6864b125900221823ec78b3 && git merge --no-ff 5e9381000524a32cc8f9bebf09212e2b51cc93e9

node scripts/docs-audit/affected-docs.mjs --json 6679644a1a56b552d6864b125900221823ec78b3

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6679644a1a56b552d6864b125900221823ec78b3 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · #8133 · domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT · 2026-10-25T05:13Z · head 5a40a42075 · report 6393762205 (on #8133; cloud session session_01Q3XCv7AXcUSSD479Vt1LXy) · seat review at TIER_DEFAULT. dispatch-gates --tier --repo objectstack-ai/objectstack over the 4 paths: no path-derived mandate. The diff reaches nothing under packages/spec/src/**, so per the claim no contract review is owed and needs:contract-review is not added.

  1. Shape. Draft, base main, first line Fixes #8133, title build(spec):. Clause-②: no, Metadata-protocol: none, Ruling-ref: none are line-initial in the body and in the changeset (@objectstack/spec patch). 4 files, +220/−48. check-governed-merges --pr 25748 (dev's run): 0 of 4 paths governed, 268 changed lines.
  2. The card's direction, measured on the diff. The reopen 6370298049 names direction A: shared chunks for the ESM and browser builds, with the exports map unchanged.
    • jsConfig builds all 24 outputs (19 ordinary plus 5 dist/browser/*) as one esbuild graph with splitting: true.
    • mainConfig stays per-entry and is now read only by the DTS pass and pure-schema-construction.test.ts. The DTS options hash is identical at BASE and HEAD.
    • The claim's mechanism lead was corrected by measurement. A bare splitting: true made ./shared and ./migrations link the chunk that imports pg-connection-string, the [seam→spec] @objectstack/spec@17.1.0 statically imports pg-connection-string from six of its sixteen entry points, with no browser export condition — objectui's site build is red on main #11072 class that check:browser-reachable-entries rule 2 refuses. That happens because esbuild chunks by file-level reachability. The dev followed the measurement.
    • How the fix works. The browser swap applies graph-wide, and only the modules that reach the seam get a node-side copy.
    • How the node-side set is found. It is computed per build by a metafile pre-pass, not kept as a list, and the build refuses when the seam disappears. Read in the diff:
      • The swap stands aside only for resolutions carrying pluginData.nodeSide.
      • Node-side imports resolve through build.resolve and stay on the node side only when their target is in the measured set.
      • Every other import goes back to the shared file namespace.
  3. Results against the card's done-when.
    • defineConnector from the root is defineConnector from /integration, in both ESM and CJS: false → true.
    • FieldSchema is defined in 14 .mjs files → 1.
    • dist shrinks from 123.5M to 53.5M.
    • The exports map and the declaration file set are unchanged.
    • The objectui import experiment (256 value imports, gzip 9) drops from 1,628.4 KB to 603.6 KB.
    • The card's first done-when, objectui Bundle Analysis vendor-objectstack on a pin carrying this change, is NOT MEASURED here, and the PR says so. It is carried as a paired objectui card, below.
  4. Gates.
  5. Producers.
    • objectui a3c0da9: 3 @objectstack/spec/dist/index.* strings, all regex probe inputs in tests, not imports. objectstack-pin.mjs hard-links the whole package tree, so the chunks travel with it.
    • HotCRM a4b3560: 0 deep imports.
    • Chunk names are unstable by design, and the changeset tells consumers to import only through the exports map.
  6. Open questions.
    • (1) Three comments under packages/spec/src/** now misdescribe the layout: src/index.ts:73, src/conversions/registry.ts:18144, and src/api/package-api-assembled.zod.ts:40, which renders into generated docs. A: the seat files one comment-only follow-up card at this PR's merge. src/** stays out of this PR so its tier stays honest.
    • (2) Under Node, 5 metadata-collection exports still differ between the root and /shared. Under browser they are one object. A: no consumer is measured comparing them by identity. Making them one object would mean an exports-map change or a src split, and nobody ruled either.
    • (3) CJS also takes shared chunks. A: the chunk graph has 0 cycles (1,158 CJS edges), and check:dual-build-cjs-loads would fail loudly on a cyclic future. The reopen left CJS "judged on its own", and this is that judgement, with evidence.
  7. Deviations accepted.
    • packages/spec/README.md (published) is outside the claim's file surface. Its sentence "Each entry is a self-contained bundle" becomes false with this change, so correcting it in the same PR is in kind.
    • The changeset quotes 602.9 KB (pre-merge head) where the body's HEAD reading is 603.6 KB. Both are true readings on different trees. Acceptance note, no carrier.
    • mcp_calls 0. Relay writes 2.
  8. Landing.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 25, 2026 14:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 3a2d38c Oct 25, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-8133-spec-shared-chunks branch October 25, 2026 15:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

2 participants