Skip to content

fix(build): keep the checkout path out of artifact bytes - #835

Merged
ScriptedAlchemy merged 8 commits into
mainfrom
fix/relative-generated-imports
Sep 25, 2026
Merged

ScriptedAlchemy merged 8 commits into
mainfrom
fix/relative-generated-imports

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #833

Why

The same commit built from two checkout paths emits different artifact bytes. Consumers that commit their artifact, such as cargo-hauler, get a dirty tree whenever they build outside the original directory.

Two leaks cause this. Generated wrappers imported project modules by absolute path. Rspack names each import binding after the request, so the root appeared in identifiers such as _tmp_ab_a_cargo_hauler_src_scripts_hauler_ts__rspack_import_1. canonicalizeNormalizedModel also skipped several model paths, so modelDigest changed with the root.

Every generator now emits a project-relative POSIX specifier from one helper. No later step rewrites generated source, so inspect --bundler shows exactly what Rspack compiles.

Scope

  • generatedModuleSpecifier(projectRoot, source) in build/meta.ts turns a project path into ../<posix path>. Generated modules always live one directory below the root, in .agent-bundle-virtual. Bare specifiers and paths outside the project pass through.
  • NormalizedPlugin.projectRoot is set by normalizeProject. canonicalizeNormalizedModel excludes it from model identity.
  • Every emitter calls the helper. The emitters in entry-shell.ts take projectRoot in their options. The hook wrappers in hook-contract.ts and amp.ts read TargetHookWrapper.projectRoot, which planHooks sets from the model. planHooksSurface and planMcpEntriesSurface take projectRoot, and eventHandlerStateSource and providerRegistrySource take it as their first argument.
  • The text rewrite in composeEntryLibConfig (build/rslib.ts) is deleted.
  • canonicalizeNormalizedModel canonicalizes mcpServers[].generatedRoutes, packageBuild.bins[].generatedCli routes and projectionSources, hooks[].eventRoute.handler, web.provenance, and state.
  • build-reproducibility.test.ts builds the fixture from two checkout paths. generated-module-specifier.test.ts pins literal POSIX and path.win32 cases. Test fixtures gain projectRoot through a tsc-driven codemod, and assertions that pinned absolute import strings now pin the relative specifier.

Blast Radius

Every generated executable changes bytes once, because import binding names change. Imports resolve to the same modules. The missing-main error text now names ../src/.... modelDigest changes once for projects that use the affected fields. Hand-built NormalizedPlugin values must now carry projectRoot.

Verification

Principles

  • Outcome-Oriented Execution. The generators emit the final specifier, and the post-hoc text rewrite in rslib.ts is deleted rather than kept beside them. Every caller and test fixture migrated in this PR, and nothing falls back to the absolute form.

Review

Verdict: PASS+NOTES. Independent review of head 34636aa41ba7715602919b2274de326ade49e82a. The stable patch-id of git diff origin/main...34636aa is a2cea75b99ba108726c9c0241f4486a86d0bac60.

Base vs head.

  • tests/build-reproducibility.test.ts from head fails on base 64492ddf43 with 11 differing files, and passes on head (measured).
  • End to end, I cloned cargo-conductor e9f7f2abc3 into /tmp/rev835-a/cc and /tmp/rev835-zz/deeper/nest/cargo-hauler-other. These paths differ in name and depth. Each clone installed the packed agent-bundle served over HTTP. On base, diff -rq artifact/ lists 14 differing files. On head it lists 0, and no checkout path appears in the head artifact (measured).
  • Building the second clone through a symlinked root on head also gives 0 differing files (measured).
  • Method caveat: installing the tarball as file: makes pnpm name the store directory by its relative path (file+..+.. vs file+..+..+..+..). That puts depth into module ids even on head. This comes from the install method, not the PR (measured).

Gate. I merged origin/main into head locally (not pushed), then ran pnpm check. It exited 0: build, unit 4487, route-unit 91, projection 197, integration 1183, lint, and typecheck (measured).

Findings.

  1. packages/agent-bundle/src/build/rslib.ts:181 rewrites the generated source as text after generation, rather than having the generators emit relative specifiers. Generators still produce absolute specifiers. inspect-bundler.ts:355 reports the unrewritten generatedEntry, so inspect shows different source from what was compiled. Per Outcome-Oriented Execution this is a post-hoc shim with two representations. It is not a correctness bug (inferred). A follow-up could emit project-relative specifiers in entry-shell.ts and hook-contract.ts.
  2. On Windows, rslib.ts:184 posix-normalises only the prefix. The specifier becomes "../src\\events\\tool\\before.ts", with mixed separators. I measured this with path.win32 on a copy of the function. Builds should still resolve and still reproduce within Windows, but bytes will differ between Windows and POSIX builds (inferred, untested on Windows).
  3. rslib.ts:181 rewrites every double-quoted string under the root, not only imports. For example, the error message at entry-shell.ts:149 is rewritten too. That is harmless and removes a leak (inferred). Sibling-prefix roots like repo-other are correctly left alone (measured).
  4. Generated modules always live at <root>/.agent-bundle-virtual, so the relative prefix is always ... Specifiers outside the root stay absolute. That is the known ceiling named in the test comment, where dependency symlinks resolving outside the root stay depth-dependent (inferred).
  5. packages/agent-bundle/src/core/project-context.ts:441-453 now runs route, handler, and view paths through resolvedProjectPath, which throws a RangeError for paths outside the root. A route source that is a symlink to outside the root would now fail modelDigest (inferred; route discovery probably rejects these earlier). The canonicalized fields cover generatedRoutes, the eventRoute.handler, and generatedCli routes and projectionSources, plus state and web provenance. The head manifests match end to end (measured).

Review (delta)

Verdict: PASS. Independent re-review of head 7c9722c7acd3e8fc771b7e32661cf0763b98165b. The stable patch-id of git diff origin/main...7c9722c is ea9e44226716fdb406586acdd5b54c958748e1c8. The earlier findings 1 and 2 are resolved.

Old path removed. checkoutIndependentSource is gone, and rslib.ts has no diff against main. Every artifact import generator goes through generatedModuleSpecifier: entry-shell.ts, hook-contract.ts, and amp.ts. Only the test-time generators in rstest/setup-module.ts and test/script.ts keep their own specifiers, and those never reach artifact bytes (measured by grep). Every generator output is compiled as a virtual module at <root>/.agent-bundle-virtual/*.mjs, so the fixed ../ prefix holds for every caller (inferred from the callers).

Inspect equals compiled. I built a scratch copy of dist that dumps each source handed to the virtual-modules plugin. For audiobook-curator, hooks-and-scripts, and mcp-app, the generatedEntry sets from inspect --bundler --json equal the compiled -entry.mjs sources byte for byte, covering 4, 4, and 6 entries. None of the 84 virtual modules contains the project root (measured).

Tests.

  • generated-module-specifier.test.ts and inspect-bundler.test.ts pass, 8 tests in total. That includes the C:\repo Windows case (measured).
  • The two-checkout build-reproducibility.test.ts passes on head. It fails on merge base 18a913e with 11 differing files (measured).

cargo-hauler end to end, fixer's approach. The merge base 18a913e is #810 itself, and cargo-hauler master 27ef3cd fails AB4810 against it (measured). So I cherry-picked 261aaa8 and 7c9722c onto 64492ddf43 without conflicts, and compared against plain 64492ddf43. Each build ran in /tmp/rev835b-a/cc and /tmp/rev835b-zz/deeper/nest/cargo-hauler-other, with the tarball installed over HTTP.

  • Head gives 0 differing artifact/ files and base gives 14.
  • The head artifact names no checkout path.
  • A build through a symlinked root is byte-identical (measured).

Gate. I merged origin/main ff7421b (#842, #845) into head locally, not pushed, and ran pnpm check. It exited 0: unit 4434, route-unit 91, projection 197, integration 1170, then lint and typecheck (measured).

Note, non-blocking. NormalizedPlugin.projectRoot shows up in the public inspect result model as an absolute path. It is output-only, and public build does not accept a caller-built model, so nothing breaks (inferred from api.ts).

Local merge gate (head aef1975, contains origin/main 2a129e9)

  • pnpm build: pass
  • pnpm typecheck: pass
  • pnpm lint: pass
  • pnpm test:unit: pass (4433 passed)
  • pnpm test:route-unit, pnpm test:projection: pass (on the earlier base)
  • pnpm test:integration:run (whole pool, on main 3b667c8): 1154 passed. Three failures came from concurrent runs rewriting dist/example fixtures (host-install-proof, route-invocation-dev-server, examples-real Skills Starter). All three pass alone (25/25).
  • After merging main 2a129e9 (refactor(install): delete legacy receipt and state readers #841, fix(test): flag fs.promises.rm and wrapped or optional-chained rm callees #848): unit, typecheck, lint, host-install-proof + build-reproducibility integration pass (14/14)
  • pnpm test:packed: pass (16 files, 46 passed, 1 skipped), including packed-consumer; packed-deleted-source passed in the integration pool
  • pnpm docs:site:build: pass (0 broken links, language parity OK)

Review (change-risk-reviewer): no material risks. Low-severity items fixed: changeset wording, docs scoped to modules inside the project root, the executable-entry error names a project-relative path, and build/compiler-evidence/generated-module-evidence/hooks fixtures use the real temp root so they exercise ../ specifiers.

@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: aef1975

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
agent-bundle Patch
create-agent-bundle Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-25T00:08:37.401301Z ef5ecac PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

ScriptedAlchemy added a commit that referenced this pull request Sep 25, 2026
@pkg-pr-new

pkg-pr-new Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle@835
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/create-agent-bundle@835
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/rsc-markdown-stream@835
npm i https://pkg.pr.new/ScriptedAlchemy/agent-bundle/@agent-bundle/runtime@835

commit: 7c9722c

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ef5ecac934

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/agent-bundle/src/build/rslib.ts Outdated
Comment thread .changeset/checkout-independent-artifacts.md Outdated
@ScriptedAlchemy
ScriptedAlchemy force-pushed the fix/relative-generated-imports branch from 8ff772f to 34636aa Compare September 25, 2026 00:32
Every generated-module emitter names project modules through generatedModuleSpecifier, which returns a project-relative POSIX specifier. NormalizedPlugin carries projectRoot so hook wrappers and entry shells receive it; model identity excludes it. canonicalizeNormalizedModel now canonicalizes generated route, event handler, web, and state paths.

Fixes #833
@ScriptedAlchemy
ScriptedAlchemy force-pushed the fix/relative-generated-imports branch from 34636aa to 7c9722c Compare September 25, 2026 02:35
@ScriptedAlchemy
ScriptedAlchemy merged commit c74702b into main Sep 25, 2026
3 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 25, 2026
@ScriptedAlchemy
ScriptedAlchemy deleted the fix/relative-generated-imports branch September 25, 2026 20:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Generated wrapper imports and canonicalizeNormalizedModel leak the checkout path into artifact bytes

1 participant