Contract reconciled 2026-08-31 against the shipped repair (PR #17946 @ e01de6da59), on @neo-gpt-emmy's RA-1. The body below described extraction as returning "the raw slice minus its delimiters" and derived computedness from interpolation text. Neither is what shipped, and both are the pre-repair contract: reading raw source loses an escaped literal's cooked value, and a ${ substring test cannot tell a real interpolation from a filename that merely contains those characters. The exact supported forms are now stated below.
#17921 shipped a build-time guard in buildScripts/build/esmodules.mjs so a dist/esm tree whose imports cannot resolve refuses to exit 0. That guard is correct in intent and it caught real defects. It is also, as merged in PR #17932, unsatisfiable against the engine's own source tree: every dist/esm build over a real engine copy exits 1, whatever the workspace does.
@neo-opus-vega measured it post-merge at dev, on a consumer workspace with three extra roots declared via neo.esmSourceRoots (A2A witness-receipt, 2026-08-31T13:02Z). Her receipt has two halves and both matter: the feature works — all three roots copy in, the missing set drops 27 → 16 with the declaration, zero survivors reference workspace code, and the quote-class rewrite lands the single-quoted engine imports correctly. And the bound holds: the build still exits 1, and every one of the 16 survivors is engine-internal.
This ticket owns that residue. It is the guard's blind spot, not the workspace's.
Scope correction, 2026-08-31 15:45Z. Running this ticket's own acceptance instrument — a full dist/esm build of the engine — surfaced a third class the consumer workspace could not show, because the files that produce it are the engine's own portal and examples. It is folded in below rather than split off: same function, same root cause, and #17942's AC is the engine's build exits 0, which the first two shapes alone do not reach. The parse5 half also turned out to be bounded in a way the original body asserted past; that correction is in §3.
The Problem
Three shapes, all engine-owned, none a workspace mistake. All three share one root cause: the guard read specifiers out of minified text with a regular expression, and a regex cannot tell an import from something that merely looks like one.
1. Computed specifiers are unresolvable by construction, and the guard calls them missing
relativeSpecifiers() (buildScripts/util/esmDistTransforms.mjs) matched import(…) with a leading quote, backticks included. In source, the engine's lazy-import families carry webpack magic comments between ( and the template literal, so the pattern did not reach them. Terser strips comments, and the emitted module the guard actually inspects does.
Reproduced against the real helper rather than reasoned about:
MINIFIED: export async function a(r,t){return[await import(`../data/parser/${t}.mjs`),
await import(`../../apps/${r.slice(5)}.mjs`)]}
import*as r from"../../../dist/parse5.mjs";
SPECIFIERS: ["../data/parser/${t}.mjs","../../apps/${r.slice(5)}.mjs","../../../dist/parse5.mjs"]
../data/parser/${t}.mjs is a literal path containing an interpolation. No such file can ever exist, so it was reported missing and the build exited 1. The families are src/worker/Data.mjs:97,105,113,121,190,198,206, src/worker/Task.mjs:62, src/worker/App.mjs:387, src/worker/Canvas.mjs:88,128, src/Main.mjs:352,354,415,417.
Exempting them outright would be the easy fix and the wrong one. A computed specifier still carries a statically-known prefix, and that prefix names a directory the output tree must contain: ../data/parser/${name}.mjs is a true claim that dist/esm/src/data/parser/ exists. An uncopied source root is exactly the failure class the guard was built for, and it is visible at the prefix.
2. An import statement inside a string literal is not an import
The engine ships sample code as data. apps/portal/view/home/parts/Colors.mjs:33 renders the string "import Viewport from '../../apps/colors/view/Viewport.mjs';" on the portal home page; examples/component/toast/MainContainer.mjs:115 renders 'import Toast from \'../../../../node_modules/neo.mjs/src/component/Toast.mjs\';' the same way. A text match sees imports. The full build reported four of them as missing and one as engine-identity — a two-engine-graph failure, on a string.
No source change could ever have satisfied that report. The code is correct and the reader was wrong, which is the worst shape a guard can take: it teaches its consumers to ignore it.
3. dist/parse5.mjs is imported by copied source and never copied — and cannot be, off-repo
src/functional/util/HtmlTemplateProcessor.mjs:3 statically imports ../../../dist/parse5.mjs. From dist/esm/src/functional/util/ that resolves to dist/esm/dist/parse5.mjs, which the build never wrote: parse5.mjs is an esbuild bundle produced by npm run bundle-parse5 (buildScripts/build/parse5.mjs:15) into the gitignored dist/, and dist/esm's copy set carries source roots plus docs/output, nothing from dist/.
Here the guard is right and the tree is wrong. This is not a false positive: an app reaching HtmlTemplateProcessor from dist/esm fails to load it today. The fix is to close the hole, not to silence the report.
The bound this ticket cannot close, stated rather than asserted past. In the engine's own checkout the bundle exists after bundle-parse5 and is copied. In a consumer workspace it does not exist and cannot be produced: .npmignore:71 excludes /dist from the published package (no negation restores it), and both parse5 and esbuild are devDependencies, so a consumer has neither the artifact nor the means to build it. Making that import resolve off-repo is a packaging decision — publish the artifact, promote the dependency, or make the import lazy — with a release surface this ticket has no business choosing. Filed separately; the copy here is what the engine's own build needs and is a strict improvement for it.
The Architectural Reality
buildScripts/util/esmDistTransforms.mjs — relativeSpecifiers(); the regex admitted backticks, which is right for finding an import and says nothing about whether the captured text is a path, or an import at all.
buildScripts/util/esmDistTransforms.mjs — findUnresolvableImports(); two reasons today, missing and engine-identity. Its exists predicate is injected and is fs.existsSync at the call site, which is already directory-capable.
buildScripts/build/esmodules.mjs:149-190 — the report and the process.exit(1).
buildScripts/build/esmodules.mjs:135-138 — docs/output is copied verbatim into the output tree after minification. That is the established precedent for a non-minified artifact the tree needs; dist/parse5.mjs is the same shape.
acorn is already this build's parser.buildScripts/util/astTemplateProcessor.mjs:1 parses every emitted module with it, and esmodules.mjs imports that pipeline. Reading specifiers from the AST adds no dependency and no second opinion about what the code says — it reads the grammar the transform already depends on.
The guard's own JSDoc already draws the distinction this ticket extends: "Containment in the output tree is NOT the property that separates those … Existence is." For a computed specifier, existence is not a property of the specifier at all — it is a property of its prefix directory.
The Fix
Read specifiers from the AST, not from the text.relativeSpecifiers parses with acorn and collects source from ImportDeclaration, ImportExpression, ExportAllDeclaration and sourced ExportNamedDeclaration nodes. A string containing an import statement contributes nothing, which closes shape 2 completely and makes shape 1 addressable rather than guessable.
The exact supported forms, resolved by staticSpecifier(source, code) returning {specifier, computed} or null:
source node
result
string Literal
{specifier: source.value, computed: false} — the cooked value, so an escaped literal yields the request the runtime makes rather than its raw source text
non-string Literal
null — import(0) parses and names no module
TemplateLiteral
reconstructed from quasis (cooked ?? raw) with each ${…} re-inserted from source, and computed: source.expressions.length > 0
anything else
null — import(base + name) is an arbitrary expression that cannot be resolved statically, and is classified as such rather than guessed at
ImportExpression membership in the node set means "may carry a specifier", never "does" — its source is an arbitrary expression.
Classify a computed specifier and check its prefix directory. Computedness is carried as typed metadata from the AST — computed is true exactly when TemplateLiteral.expressions is non-empty — and is never inferred from a ${ substring, because a quoted filename may legitimately contain those characters while naming a real file. In findUnresolvableImports, a computed specifier has its literal prefix taken up to the first interpolation, resolved against the emitting module, and the directory required to exist. Present → pass. Absent → fail under a distinct reason (computed-root), because "the directory this dynamic-import family reads from is not in the output tree" is a different instruction to the reader than "this import is broken".
Report computed-root in its own paragraph, naming the directory rather than the interpolated specifier. Every reason still exits 1; no bucket becomes advisory.
Copy dist/parse5.mjs into dist/esm/dist/parse5.mjs when it exists, alongside the docs/output copy. When it does not, change nothing: the existing missing report is then honest, and it names exactly what is absent.
The existence check looks like dead code from inside this repository and is not. With the bundle removed here, the build dies at load — esmodules.mjs → astTemplateProcessor → templateBuildProcessor imports the same bundle at module scope, ERR_MODULE_NOT_FOUND, path named — which reads as "the copy site is unreachable without it". That import resolves relative to the engine package; the copy reads root, the workspace being built. In this checkout those are one path, which is what makes the inference tempting. In a consuming workspace they are two: the engine supplies the loader's bundle and the workspace root has none, so an unconditional copy fails a build that is otherwise correct. Removing the check reds four arms of the greenfield workspace fixture on ENOENT; the comment at the copy site now states the distinction rather than the conclusion.
Contract Ledger Matrix
Target Surface
Source of Authority
Behavior
Fallback
Docs
Evidence
relativeSpecifiers extraction
this ticket
AST nodes that may carry a module specifier, resolved by node type: a string Literal yields its cooked value with computed: false; a TemplateLiteral is rebuilt from cooked quasis with each interpolation reinserted and computed set from expressions.length > 0; a non-string Literal (import(0)) and every other expression (import(base + name)) yield null rather than a guess. Entries are then kept only when the specifier opens ./ or ../. A string literal containing an import statement is not a specifier node at all
none — a module that cannot be parsed is a build failure on the existing failures path, which is correct: Terser already parsed it
helper JSDoc
positive: both real emitted sample-code shapes yield nothing while a real import beside them is still seen; negative: a nested dynamic import is still found
adds computed-root; missing and engine-identity unchanged in meaning
none — a third reason exits 1 exactly like the other two
helper JSDoc
positive: a computed specifier whose prefix directory exists passes; negative: the same specifier reds when the directory is absent
computed-specifier prefix rule
this ticket
the prefix through the last / preceding the first interpolation, resolved as a directory against the emitting module. computedSpecifierRoot locates that position with indexOf('${'), which is a positional read of an already-classified specifier and never the classifier itself: findUnresolvableImports calls it only where the AST set computed, so a quoted filename that literally contains those two characters is never truncated
none — a specifier computed from its first segment (../../${path}/x.mjs) yields a prefix that trivially exists, and the guard correctly says nothing
helper JSDoc
a specifier with no static prefix beyond ../.. must not be reportable
dist/esm/dist/parse5.mjs
src/functional/util/HtmlTemplateProcessor.mjs:3
copied from the workspace root'sdist/parse5.mjs when present, mirroring the docs/output copy
absent source → no copy, and the guard names the dangling import. The check is load-bearing in a workspace even though the loader's own copy of the bundle comes from the engine package
esmodules.mjs copy-site comment
positive: after bundle-parse5, the emitted import resolves; negative: removing the check reds four greenfield-workspace arms on ENOENT. Bounded to this repository — see §3; the consumer case is a packaging decision filed separately
exits 0 for a tree that can boot; exits 1 for one that cannot
none — this ticket restores reachability of 0, it does not weaken any refusal
—
the engine's own tree builds to exit 0; every mutation below still reds
Decision Record impact
none. This corrects a guard shipped by #17921; no ADR is touched.
Acceptance Criteria
A dist/esm build over the engine's own source tree exits 0, with dist/parse5.mjs bundled beforehand. Verified by running it, not by the unit arms.
Mutation control — string literal: the two real emitted sample-code shapes (the portal's rendered import statement and the Toast example's node_modules/neo.mjs one) contribute no specifiers, while a real import in the same module still does. Shown red against text matching first.
Mutation control — computed prefix: a fixture whose computed-import prefix directory is absent from the output reds under computed-root; adding the directory greens it. Shown red against the current implementation first, where the same fixture reds under missing for the wrong reason.
Mutation control — no-static-prefix: a specifier interpolated from its first segment (../../${path}/task.mjs) is never reported, and a spec pins that, so a future tightening cannot start guessing at runtime values.
Mutation control — parse5: with dist/parse5.mjs removed the build fails and names the exact path; with it present the copy lands at dist/esm/dist/parse5.mjs and the emitted import resolves. Run, not reasoned — and the removal arm proved something other than what it was run for: it fails at module load rather than at the guard, which reads as licensing an unconditional copy and does not, per the Fix §4 note.
The missing and engine-identity arms keep their existing specs green and their existing meanings; no literal specifier becomes exempt, and a computed specifier addressing the workspace engine is still an identity failure rather than a directory check.
Specs live in test/playwright/unit/buildScripts/, which is a suite CI runs.
Out of Scope
The consumer-side parse5 gap — /dist is npmignored and parse5/esbuild are devDependencies, so an installed engine carries neither the bundle nor the means to build it. Publishing it, promoting the dependency, or making the import lazy is a packaging decision with a release surface; filed separately rather than decided inside a build-guard fix.
#17931 — the real-engine boot arm. It would have caught all three shapes on first run and it stays the right home for booting the output; this ticket is the blocking build defect in front of it, deliberately kept small enough to merge without a heavy fixture.
Making the computed families statically resolvable (rewriting the lazy loaders to explicit maps). A much larger change to engine loading, with its own bundling consequences.
#17918 — dist-root derivation for absolute mounts. Same tree, different layer.
Avoided Traps
Exempting every computed specifier. It restores exit 0 and throws away the signal for an entire class of emitted imports — including a genuinely uncopied source root that a computed family is the only consumer of.
Patching the regex instead of parsing. Each of the three shapes is a different way for text to look like an import. A fourth exists that nobody has hit yet; a parser does not have a fourth.
Exempting dist/parse5.mjs by name. The import is really broken in dist/esm today. An exemption converts a true report into a permanent silence, which is the exact failure mode #17921 exists to end.
Claiming the parse5 copy fixes consumers. It does not, and saying so would have shipped a false closure on an AC nobody re-measures. The bound is stated in §3 and split out.
Downgrading any reason to a warning. The guard's whole value is that it refuses to exit 0. A third reason that only prints is a guard that fails open again.
Reading "the build is red" as "the workspace is misconfigured". That inversion is what makes this expensive: the consumer's first instinct is to hunt their own esmSourceRoots, and @neo-opus-vega's receipt shows the declaration was already correct while the build stayed red.
Related
#17921 — the three transform defects and the guard this corrects; PR #17932 merged it
#17931 — the real-engine boot arm carved out of #17921 AC-4
#17918 — dist CSS / Font Awesome roots on absolute mounts
#17868 — filesystem-path dependency resolution in the same build family
Live latest-open sweep: checked the latest 20 open neomjs/neo issues at 2026-08-31T13:34:16Z plus the A2A claim window (last 30 messages, ~2h) — no equivalent ticket and no in-flight claim; #17931 is adjacent and is the boot arm, which this explicitly leaves in place. Structure-map gate: N/A — no new or relocated .mjs; both edits extend existing buildScripts files.
Refs #17921 · #17931 · PR #17932
Context
#17921 shipped a build-time guard in
buildScripts/build/esmodules.mjsso adist/esmtree whose imports cannot resolve refuses to exit 0. That guard is correct in intent and it caught real defects. It is also, as merged in PR #17932, unsatisfiable against the engine's own source tree: everydist/esmbuild over a real engine copy exits 1, whatever the workspace does.@neo-opus-vega measured it post-merge at
dev, on a consumer workspace with three extra roots declared vianeo.esmSourceRoots(A2A witness-receipt, 2026-08-31T13:02Z). Her receipt has two halves and both matter: the feature works — all three roots copy in, the missing set drops 27 → 16 with the declaration, zero survivors reference workspace code, and the quote-class rewrite lands the single-quoted engine imports correctly. And the bound holds: the build still exits 1, and every one of the 16 survivors is engine-internal.This ticket owns that residue. It is the guard's blind spot, not the workspace's.
The Problem
Three shapes, all engine-owned, none a workspace mistake. All three share one root cause: the guard read specifiers out of minified text with a regular expression, and a regex cannot tell an import from something that merely looks like one.
1. Computed specifiers are unresolvable by construction, and the guard calls them missing
relativeSpecifiers()(buildScripts/util/esmDistTransforms.mjs) matchedimport(…)with a leading quote, backticks included. In source, the engine's lazy-import families carry webpack magic comments between(and the template literal, so the pattern did not reach them. Terser strips comments, and the emitted module the guard actually inspects does.Reproduced against the real helper rather than reasoned about:
MINIFIED: export async function a(r,t){return[await import(`../data/parser/${t}.mjs`), await import(`../../apps/${r.slice(5)}.mjs`)]} import*as r from"../../../dist/parse5.mjs"; SPECIFIERS: ["../data/parser/${t}.mjs","../../apps/${r.slice(5)}.mjs","../../../dist/parse5.mjs"]../data/parser/${t}.mjsis a literal path containing an interpolation. No such file can ever exist, so it was reportedmissingand the build exited 1. The families aresrc/worker/Data.mjs:97,105,113,121,190,198,206,src/worker/Task.mjs:62,src/worker/App.mjs:387,src/worker/Canvas.mjs:88,128,src/Main.mjs:352,354,415,417.Exempting them outright would be the easy fix and the wrong one. A computed specifier still carries a statically-known prefix, and that prefix names a directory the output tree must contain:
../data/parser/${name}.mjsis a true claim thatdist/esm/src/data/parser/exists. An uncopied source root is exactly the failure class the guard was built for, and it is visible at the prefix.2. An import statement inside a string literal is not an import
The engine ships sample code as data.
apps/portal/view/home/parts/Colors.mjs:33renders the string"import Viewport from '../../apps/colors/view/Viewport.mjs';"on the portal home page;examples/component/toast/MainContainer.mjs:115renders'import Toast from \'../../../../node_modules/neo.mjs/src/component/Toast.mjs\';'the same way. A text match sees imports. The full build reported four of them asmissingand one asengine-identity— a two-engine-graph failure, on a string.No source change could ever have satisfied that report. The code is correct and the reader was wrong, which is the worst shape a guard can take: it teaches its consumers to ignore it.
3.
dist/parse5.mjsis imported by copied source and never copied — and cannot be, off-reposrc/functional/util/HtmlTemplateProcessor.mjs:3statically imports../../../dist/parse5.mjs. Fromdist/esm/src/functional/util/that resolves todist/esm/dist/parse5.mjs, which the build never wrote:parse5.mjsis an esbuild bundle produced bynpm run bundle-parse5(buildScripts/build/parse5.mjs:15) into the gitignoreddist/, anddist/esm's copy set carries source roots plusdocs/output, nothing fromdist/.Here the guard is right and the tree is wrong. This is not a false positive: an app reaching
HtmlTemplateProcessorfromdist/esmfails to load it today. The fix is to close the hole, not to silence the report.The bound this ticket cannot close, stated rather than asserted past. In the engine's own checkout the bundle exists after
bundle-parse5and is copied. In a consumer workspace it does not exist and cannot be produced:.npmignore:71excludes/distfrom the published package (no negation restores it), and bothparse5andesbuildaredevDependencies, so a consumer has neither the artifact nor the means to build it. Making that import resolve off-repo is a packaging decision — publish the artifact, promote the dependency, or make the import lazy — with a release surface this ticket has no business choosing. Filed separately; the copy here is what the engine's own build needs and is a strict improvement for it.The Architectural Reality
buildScripts/util/esmDistTransforms.mjs—relativeSpecifiers(); the regex admitted backticks, which is right for finding an import and says nothing about whether the captured text is a path, or an import at all.buildScripts/util/esmDistTransforms.mjs—findUnresolvableImports(); two reasons today,missingandengine-identity. Itsexistspredicate is injected and isfs.existsSyncat the call site, which is already directory-capable.buildScripts/build/esmodules.mjs:149-190— the report and theprocess.exit(1).buildScripts/build/esmodules.mjs:135-138—docs/outputis copied verbatim into the output tree after minification. That is the established precedent for a non-minified artifact the tree needs;dist/parse5.mjsis the same shape.acornis already this build's parser.buildScripts/util/astTemplateProcessor.mjs:1parses every emitted module with it, andesmodules.mjsimports that pipeline. Reading specifiers from the AST adds no dependency and no second opinion about what the code says — it reads the grammar the transform already depends on.The Fix
Read specifiers from the AST, not from the text.
relativeSpecifiersparses withacornand collectssourcefromImportDeclaration,ImportExpression,ExportAllDeclarationand sourcedExportNamedDeclarationnodes. A string containing an import statement contributes nothing, which closes shape 2 completely and makes shape 1 addressable rather than guessable.The exact supported forms, resolved by
staticSpecifier(source, code)returning{specifier, computed}ornull:Literal{specifier: source.value, computed: false}— the cooked value, so an escaped literal yields the request the runtime makes rather than its raw source textLiteralnull—import(0)parses and names no moduleTemplateLiteralquasis(cooked ?? raw) with each${…}re-inserted from source, andcomputed: source.expressions.length > 0null—import(base + name)is an arbitrary expression that cannot be resolved statically, and is classified as such rather than guessed atImportExpressionmembership in the node set means "may carry a specifier", never "does" — itssourceis an arbitrary expression.Classify a computed specifier and check its prefix directory. Computedness is carried as typed metadata from the AST —
computedis true exactly whenTemplateLiteral.expressionsis non-empty — and is never inferred from a${substring, because a quoted filename may legitimately contain those characters while naming a real file. InfindUnresolvableImports, a computed specifier has its literal prefix taken up to the first interpolation, resolved against the emitting module, and the directory required to exist. Present → pass. Absent → fail under a distinct reason (computed-root), because "the directory this dynamic-import family reads from is not in the output tree" is a different instruction to the reader than "this import is broken".Report
computed-rootin its own paragraph, naming the directory rather than the interpolated specifier. Every reason still exits 1; no bucket becomes advisory.Copy
dist/parse5.mjsintodist/esm/dist/parse5.mjswhen it exists, alongside thedocs/outputcopy. When it does not, change nothing: the existingmissingreport is then honest, and it names exactly what is absent.The existence check looks like dead code from inside this repository and is not. With the bundle removed here, the build dies at load —
esmodules.mjs→astTemplateProcessor→templateBuildProcessorimports the same bundle at module scope,ERR_MODULE_NOT_FOUND, path named — which reads as "the copy site is unreachable without it". That import resolves relative to the engine package; the copy readsroot, the workspace being built. In this checkout those are one path, which is what makes the inference tempting. In a consuming workspace they are two: the engine supplies the loader's bundle and the workspace root has none, so an unconditional copy fails a build that is otherwise correct. Removing the check reds four arms of the greenfield workspace fixture on ENOENT; the comment at the copy site now states the distinction rather than the conclusion.Contract Ledger Matrix
relativeSpecifiersextractionLiteralyields its cooked value withcomputed: false; aTemplateLiteralis rebuilt from cooked quasis with each interpolation reinserted andcomputedset fromexpressions.length > 0; a non-stringLiteral(import(0)) and every other expression (import(base + name)) yieldnullrather than a guess. Entries are then kept only when the specifier opens./or../. A string literal containing an import statement is not a specifier node at allfailurespath, which is correct: Terser already parsed itfindUnresolvableImportsreason setcomputed-root;missingandengine-identityunchanged in meaning/preceding the first interpolation, resolved as a directory against the emitting module.computedSpecifierRootlocates that position withindexOf('${'), which is a positional read of an already-classified specifier and never the classifier itself:findUnresolvableImportscalls it only where the AST setcomputed, so a quoted filename that literally contains those two characters is never truncated../../${path}/x.mjs) yields a prefix that trivially exists, and the guard correctly says nothing../..must not be reportabledist/esm/dist/parse5.mjssrc/functional/util/HtmlTemplateProcessor.mjs:3dist/parse5.mjswhen present, mirroring thedocs/outputcopyesmodules.mjscopy-site commentbundle-parse5, the emitted import resolves; negative: removing the check reds four greenfield-workspace arms on ENOENT. Bounded to this repository — see §3; the consumer case is a packaging decision filed separatelydist/esmexit code on a real engineDecision Record impact
none. This corrects a guard shipped by #17921; no ADR is touched.Acceptance Criteria
dist/esmbuild over the engine's own source tree exits 0, withdist/parse5.mjsbundled beforehand. Verified by running it, not by the unit arms.node_modules/neo.mjsone) contribute no specifiers, while a real import in the same module still does. Shown red against text matching first.computed-root; adding the directory greens it. Shown red against the current implementation first, where the same fixture reds undermissingfor the wrong reason.../../${path}/task.mjs) is never reported, and a spec pins that, so a future tightening cannot start guessing at runtime values.dist/parse5.mjsremoved the build fails and names the exact path; with it present the copy lands atdist/esm/dist/parse5.mjsand the emitted import resolves. Run, not reasoned — and the removal arm proved something other than what it was run for: it fails at module load rather than at the guard, which reads as licensing an unconditional copy and does not, per the Fix §4 note.missingandengine-identityarms keep their existing specs green and their existing meanings; no literal specifier becomes exempt, and a computed specifier addressing the workspace engine is still an identity failure rather than a directory check.test/playwright/unit/buildScripts/, which is a suite CI runs.Out of Scope
/distis npmignored andparse5/esbuildare devDependencies, so an installed engine carries neither the bundle nor the means to build it. Publishing it, promoting the dependency, or making the import lazy is a packaging decision with a release surface; filed separately rather than decided inside a build-guard fix.Avoided Traps
dist/parse5.mjsby name. The import is really broken indist/esmtoday. An exemption converts a true report into a permanent silence, which is the exact failure mode #17921 exists to end.esmSourceRoots, and @neo-opus-vega's receipt shows the declaration was already correct while the build stayed red.Related
Retrieval Hint:
query_raw_memories("dist/esm guard computed template literal specifier prefix directory parse5 copy acorn extraction"); anchorsesmDistTransforms.mjsrelativeSpecifiers/findUnresolvableImports,esmodules.mjs:135-190, PR #17932.Live latest-open sweep: checked the latest 20 open
neomjs/neoissues at 2026-08-31T13:34:16Z plus the A2A claim window (last 30 messages, ~2h) — no equivalent ticket and no in-flight claim; #17931 is adjacent and is the boot arm, which this explicitly leaves in place. Structure-map gate: N/A — no new or relocated.mjs; both edits extend existingbuildScriptsfiles.Origin Session ID: 57505599-d711-4464-b17d-4cfff3dceee4
Authored by Grace (Anthropic Claude Opus 5, Claude Code).