Skip to content

feat(build): compiler service, Rspack evidence adapter, external policy (#619 steps 1–3) - #623

Merged
ScriptedAlchemy merged 13 commits into
mainfrom
feat/619-compiler-evidence-service
Sep 5, 2026
Merged

feat(build): compiler service, Rspack evidence adapter, external policy (#619 steps 1–3)#623
ScriptedAlchemy merged 13 commits into
mainfrom
feat/619-compiler-evidence-service

Conversation

@ScriptedAlchemy

@ScriptedAlchemy ScriptedAlchemy commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Implements steps 1–3 of #619 (central compiler service, Rspack compilation evidence adapter, external dependency policy). Steps 4–5 (Artifact IR compile provenance, AB6005 evaluated from the module graph for every surface) and step 6 (delete the generated-JS load scanners, rewrite the AGENTS.md "Proof is bytes and processes" sentence) follow in two more PRs. Nothing from #602 is reused as code; its behavioural bullets are re-proven here from compiler evidence.

What changes

  • src/build/compile-result.ts (new leaf): CompilationEvidence (what one Rspack compilation reported: externals with issuers, bundled modules), ExternalIR (kind: 'builtin' | 'artifact-relative' | 'package', request = run-time load target, userRequest = authored specifier), ModuleIR, AssetIR (the former BundledOutputEvidence), CompileResult.
  • src/build/dependency-audit-plugin.ts (new): ArtifactDependencyAuditPlugin taps thisCompilationafterOptimizeModules, records every ExternalModule (type and run-time request parsed from the module identifier — for { 'left-pad': 'lp' } the emitted code loads lp, and lp is what gets judged) with its issuers from the module graph, and every bundled NormalModule. Appended by the framework invariant layer in composeEntryLibConfig; the consumer's tools hatch runs before it and cannot remove it.
  • src/build/external-policy.ts (new): isAllowedExternalRequest (Node built-ins and pnpapi — the one allowlist, now also used by the pre-existing emitted-module walk), classifyExternal (built-in / emitted sibling of the same artifact / package), selfContainmentDiagnosticsAB6005, externalizedSpecifiers (static string/object externalsAB4725; RegExp, function, mutator, and relative declarations are left to compile evidence).
  • src/build/compiler.ts (new): the service. compileRslibSurfaces / buildWithRslib move here from rslib.ts and apply the policy to every CompileResult before any caller trusts an asset; violations throw DiagnosticError.
  • src/build/rslib.ts: buildRslibSurfaces returns CompileResult[] (one per surface, evidence matched to entries by the Rslib lib id the compiler is named after); externals mapped to ExternalIR with project-relative issuers; modules to ModuleIR with kind and package.
  • src/build/compose-layers.ts: the invariant layer pins output.autoExternal: false after the hatch merge, so tools.rsbuild.output.autoExternal: true cannot externalize a dependencies entry.
  • src/config/validate.ts: new AB4725tools.rsbuild.output.autoExternal other than false, or a static string/object externals entry naming a package, in tools.rsbuild.output or object-form tools.rspack.
  • src/build/package-build.ts: dist bundles are compiled through the service and judged from evidence (diagnosticPathPrefix: 'dist'). The feat(build): hold the package build's dist bundles to AB6005 #588 emitted-JS walk stays as defense in depth until PR 3.
  • Docs: docs/diagnostics.md (AB472x, AB4725, AB60xx, AB6005 rows), docs/entry-conventions.md (tools paragraph), website/docs/{en,zh}/guide/distribution/validation.mdx, guide/authoring/package-entries.mdx, reference/configuration.mdx. One minor changeset.

Behavioural tests (the #619 list)

Bullet Test
Externalized package must fail compiler-evidence.test.ts (host pack: array mutator, externalsType: 'node-commonjs', object map { 'left-pad': 'lp' } judged on lp); package-build.test.ts / prepack.test.ts (dist, module and node-commonjs shim forms)
Built-in must pass dependency-audit-plugin.test.ts (node:fs, node:path, CJS dependency's require('fs') recorded as node-commonjs and allowed); external-policy.test.ts
Relative emitted module must pass compiler-evidence.test.ts "artifact-relative externals": function-form external redirecting ./helper.ts./helper.mjs (emitted sibling) passes; ./missing.mjs and ../outside.mjs fail AB6005
Opaque prebuilt remains opaque unchanged: definePrebuilt is PR 2 scope; prebuilt files never enter a compilation, so no evidence is produced for them (existing prebuilt tests untouched)
tools.rspack cannot escape bundling policy package-conventions.test.ts (AB4725, static forms); compiler-evidence.test.ts (autoExternal: true hatch still inlines a dependencies entry; mutator-installed externals fail at compile time)

Decisions recorded on #619 and kept here: no unresolved evidence class (Rslib's profile sets importDynamic: false / requireDynamic: false, so an expression request is neither a module nor an external — the plugin test "leaves an expression import verbatim" proves it); no composition-time rejection of mutator externals (anything the config validator cannot see is judged from compile evidence, where the spelling is irrelevant).

Verification

Run on the merged tree (origin/main at 9b02115 merged in):

  • pnpm build && pnpm typecheck && pnpm lint && pnpm test:unit — build ok (publint passed); typecheck ok; lint 1340 files, 88 rules; unit pass, 0 failed.
  • pnpm test:integration:runpass, 0 failed (includes compiler-evidence.test.ts 8/8, dependency-audit-plugin.test.ts, package-build.test.ts, prepack.test.ts).
  • pnpm docs:site:build — typecheck, build, dead-link/anchor/image and language-parity checks pass.
  • Reachability: every added module has a production importer (compile-result.tsrslib.ts/compiler.ts/external-policy.ts; dependency-audit-plugin.tsrslib.ts; external-policy.tscompiler.ts/config/validate.ts/validate-artifact-modules.ts; compiler.tsbuild.ts/package-build.ts).
  • Probe behind the object-map fix (/tmp, @rspack/core 2.2.2, externals: [{ './helper.ts': 'module ./helper.mjs', 'left-pad': 'lp' }]): identifiers external module "./helper.mjs"|javascript/esm|…/src.mjs and external module "lp"|… with userRequest ./helper.ts / left-pad; external node-commonjs "node:fs" for the built-in. The identifier carries the run-time target; userRequest does not.

Deslop: gpt-5.6-sol-medium, 12 edits (removed comments restating code, a redundant ?? '' fallback and a resource ?? '' test fallback that could pass vacuously, duplicated allowlist checks; no behaviour change).

Self-review

Pass 1 — claude-fable-5-1-thinking-high on the deslopped diff. Findings and disposition:

  1. Should-fix — pnpapi allowed by the policy but rejected by the retained emitted-module walk (isBuiltin only) while docs say it passes. Fixed: the walk now uses isAllowedExternalRequest (one allowlist).
  2. Should-fix — missing behavioural tests: host-pack node-commonjs shim; artifact-relative external end to end (pass on emitted sibling, fail on ./missing.mjs / ../outside.mjs); autoExternal re-pin under a real build. Fixed: all added to compiler-evidence.test.ts. Writing the artifact-relative test exposed a real bug — the plugin recorded userRequest (./helper.ts) rather than the run-time target (./helper.mjs), so an object-map or function-form redirect was judged on the wrong string. Fixed by parsing the request from the module identifier; userRequest kept for the message; plugin test "records the run-time target of an object-map external" added.
  3. Nit — AB4725 rejected a static relative external that compile time would accept. Fixed: externalizedSpecifiers skips relative requests; docs (diagnostics, configuration en/zh, validation en/zh) and changeset updated; relative-target diagnostics now say it names no module emitted by this artifact. instead of the package sentence.
  4. Nit — two packageNameOf with different contracts. Fixed: renamed packageNameOfResource.
  5. Nit — defensive throws on trusted paths (result === undefined, asset === undefined). Fixed: non-null assertions; collectBundledOutputEvidence already throws for a missing expected asset.
  6. Nit — duplicated sort helpers in the plugin. Fixed: removed (sort() / localeCompare inline).
  7. Nit — artifactDependencyAuditPluginName exported with no importer. Fixed: module-private.
  8. Nit — docs/entry-conventions.md dropped the reserved-specifier sentence. Fixed: restored.

Pass 2 — gpt-5.6-sol-medium on the fixed diff: three findings.

  1. Blocker — the lazy identifier regex could truncate a JSON request containing an escaped quote or ] followed by |. Fixed: replaced by a depth-counting JSON scanner (jsonPrefixLength) that honours backslash escapes; the remainder must be empty, |… (layer/issuer segments), or (import attributes / phase). Plugin test "reads a run-time target containing a quote and a pipe" added (real compile, array request ['lp|"x', 'default']).
  2. Should-fix — evidence grouping omitted externalType, so one request redirected to different types per issuer merged under the first-seen type. Fixed: key is [externalType, request, userRequest]; sort tiebreaks on all three.
  3. Should-fix — docs/entry-conventions.md and package-entries.mdx (en/zh) still said AB4725 rejects "non-built-in" externals. Fixed: "package externals"; relative, function-form, and mutator externals deferred to AB6005.

Pass 3 — claude-fable-5-1-thinking-high (confirmation of the pass-2 fixes): verified the scanner (29 edge-case probes), grouping key, and docs parity; two further should-fixes and one nit, both verified reachable with an @rspack/core 2.2.2 probe:

  1. Should-fix — an import … with { type: 'json' } kept external produces external module "./data.json" {"type":"json"}|… (space-separated attributes; phase=defer likewise), which the parser rejected, crashing the build with a raw Error instead of recording evidence. Fixed: a remainder starting with a space is accepted.
  2. Should-fix — a per-type map request (callback(undefined, { module: 'lp', 'node-commonjs': 'left-pad' }), reachable through function-form or mutator externals) produces external module {"module":"lp",…}; the lookahead rejected {. Fixed: the scanner depth-counts [/{, and an object request is read at the resolved external type (array → first element). Plugin test "reads a per-type map request at the resolved type and tolerates import attributes" added (real compile covering both).
  3. Nit — the AB4725 prose said "neither a Node built-in nor a relative path" but the code also exempts pnpapi. Fixed in docs/diagnostics.md and reference/configuration.mdx en/zh.

Codex (GitHub review bot), two P2 threads on the pass-3 diff:

  1. External type ignored — fs kept under externalsType: 'global'/'var' passed as a built-in although the emitted code reads a variable, not a module. Fixed: classifyExternal judges externalType too; only module-loading types (module, import, module-import, commonjs*, node-commonjs) can be builtin/artifact-relative; unit + end-to-end tests added (prepended { 'node:fs': 'global fs' } mutator); AB6005 row updated.
  2. MCP App builds (compileMcpApps, Rsbuild) do not go through the service, so function-form externals there escape AB4725 and evidence. Deferred to PR 2 with the rule stated (a browser view has no allowable external; the audit plugin joins composeMcpAppsRsbuildConfig's invariant layer and fails a view on any recorded ExternalModule) — replied on the thread and recorded on Compiler evidence: prove self-containment from the Rspack module graph and Artifact IR; delete the generated-JS load scanners (replaces #591/#602) #619.

Verified fine by the reviewers and not changed: evidence→entry mapping via entryLibId/compilation.compiler.name with assertDistinctLibIds; classifyExternal root-escape handling; pnpm .pnpm/<name>@<ver>/node_modules/<name> package names; Windows issuer relativisation; evidence discarded when rslib.build() throws; the compilationEvidence test seam only seeds from dependencies, which production never passes; mergeRslibConfig lets the invariant autoExternal: false win; no public export of buildWithRslib/RslibSurfacePlan/compileRslibSurfaces; every added module has a production importer; externalMessage switch exhaustive; no new as-casts or inline imports.

@changeset-bot

changeset-bot Bot commented Sep 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 2c96b56

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

This PR includes changesets to release 1 package
Name Type
agent-bundle Minor

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

@pkg-pr-new

pkg-pr-new Bot commented Sep 5, 2026

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

commit: 2c96b56

@ScriptedAlchemy
ScriptedAlchemy marked this pull request as ready for review September 5, 2026 10:24
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 5, 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-05T10:29:22.772090Z 39d6cc5 Draft marked ready
ℹ️ 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.

@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: 39d6cc53c0

ℹ️ 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".

request: string,
options: { readonly asset: string; readonly emittedAssets: ReadonlySet<string> },
): ExternalKind => {
if (isAllowedExternalRequest(request)) return 'builtin';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject external types that do not load Node modules

When the tools hatch combines an allowed request such as fs with externalsType: 'global' or 'var', this request-only classification marks it as a permitted builtin even though Rspack emits a global-variable lookup rather than a Node module load. Static validation also exempts fs, so the build can publish an executable that passes AB6005 but fails at runtime because the expected global is absent; include externalType in the policy and allow only types that actually load modules.

AGENTS.md reference: AGENTS.md:L69-L78

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed. classifyExternal now takes the externalType with the request: only Rspack types whose emitted code loads a module (module, import, module-import, commonjs, commonjs2, commonjs-module, commonjs-static, node-commonjs) can be builtin or artifact-relative; anything else (var, global, this, window, assign, umd, amd, system, jsonp, promise, script, …) is an AB6005 even for a Node built-in, with the message ending external type <type> reads a variable instead of loading a module. Covered by external-policy.test.ts (unit) and compiler-evidence.test.ts "rejects a built-in kept under an external type that reads a variable" (a prepended { "node:fs": "global fs" } mutator, since Rspack takes the first matching declaration and Rslib declares the built-ins first). docs/diagnostics.md AB6005 row updated.

export const externalizedSpecifiers = (externals: unknown): readonly string[] => {
if (Array.isArray(externals)) return externals.flatMap(externalizedSpecifiers);
if (typeof externals === 'string') return isPackageRequest(externals) ? [externals] : [];
if (externals instanceof RegExp || typeof externals === 'function' || !isRecord(externals)) return [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Audit function-form externals in MCP App builds

When a compiled MCP App supplies function-form externals—for example, redirecting react to a browser global—this branch deliberately emits no AB4725 because such declarations are supposed to be judged from compilation evidence. However, compileMcpApps neither installs ArtifactDependencyAuditPlugin nor goes through the compiler service, and its final check only looks for extra files and external script/link URLs, so the build succeeds with an inline HTML app that references an unavailable external. Either collect and enforce evidence on the MCP App path or reject these external declarations there.

AGENTS.md reference: AGENTS.md:L82-L92

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Deferred to PR 2 of #619 with a precise scope, not fixed here. PR 1 is the compiler service for the Rslib surfaces (host-pack scripts/hooks/servers and the package build's dist) — the artifacts the #619 invariant names as compiler-owned executables. compileMcpApps is a separate Rsbuild compiler with its own diagnostic namespace (AB477x), whose views are browser HTML documents where no external is ever legitimate (there are no Node built-ins to allow), so the rule there is "the compilation has zero externals", not the executable policy in this PR. PR 2 (#619 steps 4–5: every surface's Artifact IR records compile provenance and is judged from evidence) installs ArtifactDependencyAuditPlugin in composeMcpAppsRsbuildConfig's invariant layer and fails the view when any ExternalModule is recorded, replacing the current assertSelfContainedViews heuristic for scripts/links. Recorded on #619.

@cursor

cursor Bot commented Sep 5, 2026

Copy link
Copy Markdown

Verified the three ChatGPT design-review follow-ups against current main (02b89727, which includes this PR and #627). All three are already satisfied; no code, docs, or test changes are needed, so no follow-up PR is opened.

1. Audit the actual external target + type, not only userRequest — done on main.

  • src/build/dependency-audit-plugin.ts:43-58 (runtimeRequest) parses the external type and the run-time load target from the Rspack module identifier (string, array, and per-type-map requests; import attributes and phase=defer tolerated), keeping userRequest only as the authored spelling; evidence is grouped by [externalType, request, userRequest] (line 74).
  • src/build/external-policy.ts:21-49: classifyExternal judges externalType first — only module-loading types (module, import, commonjs*, node-commonjs, …) can classify as builtin/artifact-relative; a built-in kept under var/global etc. is rejected.
  • Tests: tests/dependency-audit-plugin.test.ts:154-234 (object-map external records lp, not left-pad; quote/pipe identifier; per-type map read at the resolved type; CJS require('fs') recorded as node-commonjs) and tests/compiler-evidence.test.ts:168-191 ({ 'node:fs': 'global fs' } fails: "external type global reads a variable instead of loading a module").

2. Dynamic-import coverage limitation kept explicit — done on main.

  • docs/diagnostics.md:1803 (AB6005 row): "An expression request (import(expr), require(expr)) is outside the compiler's view: Rslib's profile leaves it verbatim without parsing it, so it is neither bundled nor external."
  • Same statement in website/docs/en/guide/distribution/validation.mdx:36-37 and the zh mirror (zh/.../validation.mdx:30).
  • docs/diagnostics.md:432-448 and validation.mdx:207-271 spell out that createRequire(…)(…) / import.meta.resolve(…) calls the compiler does not resolve are outside AB6005 and are instead read as AB7014/AB7015 dependency evidence.
  • In code: the comment above the test tests/dependency-audit-plugin.test.ts:236-241 ("has nothing to record for an expression import, which the profile leaves verbatim") and the retained walk's has a non-literal dynamic import diagnostic (src/build/validate-artifact-modules.ts:166-168).

3. Scanners not deleted early — done on main.

  • src/build/validate-artifact-modules.ts (validateJavaScriptModules) is retained with both production callers: the artifact walk (src/build/validate-artifact.ts:612) and the staged-dist walk (src/build/package-build.ts:385), plus readModuleImports consumers in rslib.ts:322 and pack-dependencies.ts:528.
  • The docs describe it as intentionally retained: "The emitted-module walk remains as defense in depth" (docs/diagnostics.md:1803, also line 35 and 435; validation.mdx:39-43). Compiler evidence: prove self-containment from the Rspack module graph and Artifact IR; delete the generated-JS load scanners (replaces #591/#602) #619 step 6 (deletion) has not landed, and its inventory already commits to keeping the relative-target manifest/file-table checks as defense in depth when it does.

CI on main is green (CI, Docs, Package preview, Release packages all successful on 02b89727).

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.

1 participant