Refs #17931 · #17943 · #17976
Context
Measured on origin/dev @ fa20136f29 while taking #17931's AC-2 measurement (#17931 comment). A greenfield workspace consuming the real engine builds dist/esm in 2.6s and then exits 1:
dist/esm: 3 import(s) do not resolve inside the output tree:
dist/esm/src/component/Markdown.mjs → ../../../../node_modules/marked/lib/marked.esm.js
dist/esm/src/app/content/Component.mjs → ../../../../../node_modules/marked/lib/marked.esm.js
dist/esm/src/functional/util/HtmlTemplateProcessor.mjs → ../../../dist/parse5.mjs
The parse5 arm belongs to #17931 (it is the evidence for #17943's scope transfer). This ticket is the marked arm, which no open or closed ticket covers.
The Problem
marked is a devDependency, and two shipped src/ modules import it at runtime through node_modules/:
src/component/Markdown.mjs:4 — import {marked} from '../../node_modules/marked/lib/marked.esm.js';
src/app/content/Component.mjs:2 — import {marked} from '../../../node_modules/marked/lib/marked.esm.js';
package.json declares:
"dependencies": {}
"devDependencies": { "marked": "^18.0.5", … }npm does not install devDependencies of an installed package. A consumer of neo.mjs therefore never has marked on disk at all — under any serving strategy, any bundler, any basePath. Both modules are unloadable in every consuming workspace.
This is load-bearing regardless of how the output tree is meant to address node_modules. Even if reaching outside dist/esm were by design and correctly served, the file being reached does not exist for a consumer.
Same shape as #17943. That ticket found parse5 unreachable for consumers because dist was npmignored, and it was accepted and fixed. marked is the same class of defect — a module src/ needs at runtime that a consumer's install does not produce — reached by a different mechanism (dependency classification rather than publication filtering).
Why nothing caught it. test/playwright/unit/buildScripts/esmWorkspaceBuild.spec.mjs builds its workspace against a stub engine (node_modules/neo.mjs/src/core/Base.mjs = export default class Base {}), so no fixture has ever compiled a module that imports marked. The defect is invisible to every existing arm by construction. #17931's AC-1 is what makes it observable.
The Architectural Reality
src/component/Markdown.mjs:4 and src/app/content/Component.mjs:2 — the two importers.
package.json — dependencies is empty; marked sits in devDependencies at ^18.0.5.
buildScripts/build/esmodules.mjs — flattens src/ into dist/esm/src/ and rewrites specifiers, but carries nothing that src/ imports from outside src/. Its unresolved-import report is what surfaced this.
- #17943 established the precedent for the sibling case (
parse5), shipping dist/parse5.mjs and un-npmignoring dist.
The Fix
The mechanism is settled; the destination is a genuine fork and I am not deciding it unilaterally. Each option below carries its own falsifier.
| # |
Option |
Falsifier |
| A |
Promote marked to a runtime dependency. The emitted specifier already resolves to the workspace's own node_modules/marked, so the path arithmetic needs no change. |
Build a consumer workspace with marked installed; the two modules must resolve and be reachable by whatever serves the app. If serving node_modules is not acceptable for a built tree, A is insufficient alone. |
| B |
Bundle it into dist/ and import from there, mirroring #17943's parse5 treatment. |
#17931 measured that dist/-relative imports do not survive into dist/esm — HtmlTemplateProcessor fails on exactly that today. B is only viable if it lands together with a fix for that, so verify the emitted dist/esm/dist/* path exists before adopting. |
| C |
Teach esmodules.mjs to copy imported third-party assets into the output tree and rewrite to them. |
Most self-contained output, largest build-layer change. Falsify by checking whether any other undeclared external import exists — if marked is the only one, C is over-built for one package. |
My recommendation is A + the serving check, because it is the smallest change that makes the file exist for consumers, and because B is currently blocked by a defect #17931 already owns. I have not run A's falsifier; whoever takes this should run it first rather than trusting the recommendation.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Behavior |
Fallback |
Docs |
Evidence |
package.json dependencies |
this ticket |
declares every package a shipped src/ module imports at runtime |
none — an undeclared runtime import is the defect |
package.json |
a consumer install produces the package on disk |
src/component/Markdown.mjs, src/app/content/Component.mjs |
this ticket |
load in a consumer's built tree |
none |
module JSDoc |
negative control: a workspace built from the published package resolves both, and fails to before the fix |
Decision Record impact
none. learn/agentos/decisions/ contains no ADR governing dependency classification or dist/esm third-party carriage — grepped at filing time, and that absence is recorded rather than read as permission. If option C is chosen, the build-layer change may warrant one.
Acceptance Criteria
Out of Scope
- The
parse5 arm — #17931 owns it as its scope-transferred AC-5; fixing marked does not address it and must not be conflated with it.
- #17976 (
basePath resolves to the repo root) and #17972 (a WS/ addon loading unbuilt source) — distinct dist/esm consumer defects on the same output tree.
- The real-engine boot fixture itself — #17931. This ticket is a defect that fixture revealed, not the fixture.
- Whether
node_modules should be served for a built app at all. That question sits under #17884 and is only touched here by option A's falsifier.
Avoided Traps
- Verifying from a repo checkout. The engine's own tree has
marked installed as a devDependency, so every import resolves and the defect is invisible. Only a workspace built from the published package can observe it — the same blind spot that let #17943 ship.
- Folding this into #17931. That ticket's job is to build a fixture that can see defects like this one. Absorbing the defect into the fixture ticket means neither closes cleanly.
- Assuming the unresolved-import report is always a defect. It flags anything outside the output tree, which may be legitimate for a served workspace. The load-bearing fact here is narrower and survives that objection: an undeclared devDependency is not installed for consumers at all.
- Bundling by reflex because #17943 did. The precedent's mechanism is currently broken for
dist/esm output — #17931 measured it. Copying the precedent without checking it would ship a fix that cannot load.
Related
- #17931 — the real-engine fixture whose measurement surfaced this; owns the
parse5 arm
- #17943 — the accepted sibling defect (
parse5 unreachable for consumers), and the precedent for option B
- #17976, #17972, #17918, #17884 — other open
dist/esm consumer defects, distinct mechanisms
Live latest-open sweep: checked the latest 20 open neomjs/neo issues at 2026-08-31T23:14:48Z plus the A2A claim window (12 most recent messages, ~23:00–23:14Z) — no equivalent ticket and no in-flight claim on this scope. Closed-state sweep for dist/esm marked parse5 unresolved returned empty. Structure-map gate: N/A — no new or relocated .mjs placement.
Retrieval Hint: query_raw_memories("dist/esm consumer marked devDependency unresolved import real engine workspace"); anchors fa20136f29, src/component/Markdown.mjs:4, #17931 comment 5486043960.
Origin Session ID: c38a0f32-c1b0-47ff-8e14-f16ace6007e7
Authored by Grace (Anthropic Claude Opus 5, Claude Code).
Refs #17931 · #17943 · #17976
Context
Measured on
origin/dev@fa20136f29while taking #17931's AC-2 measurement (#17931 comment). A greenfield workspace consuming the real engine buildsdist/esmin 2.6s and then exits 1:The
parse5arm belongs to #17931 (it is the evidence for #17943's scope transfer). This ticket is themarkedarm, which no open or closed ticket covers.The Problem
markedis a devDependency, and two shippedsrc/modules import it at runtime throughnode_modules/:src/component/Markdown.mjs:4—import {marked} from '../../node_modules/marked/lib/marked.esm.js';src/app/content/Component.mjs:2—import {marked} from '../../../node_modules/marked/lib/marked.esm.js';package.jsondeclares:"dependencies": {} "devDependencies": { "marked": "^18.0.5", … }npm does not install devDependencies of an installed package. A consumer of
neo.mjstherefore never hasmarkedon disk at all — under any serving strategy, any bundler, anybasePath. Both modules are unloadable in every consuming workspace.This is load-bearing regardless of how the output tree is meant to address
node_modules. Even if reaching outsidedist/esmwere by design and correctly served, the file being reached does not exist for a consumer.Same shape as #17943. That ticket found
parse5unreachable for consumers becausedistwas npmignored, and it was accepted and fixed.markedis the same class of defect — a modulesrc/needs at runtime that a consumer's install does not produce — reached by a different mechanism (dependency classification rather than publication filtering).Why nothing caught it.
test/playwright/unit/buildScripts/esmWorkspaceBuild.spec.mjsbuilds its workspace against a stub engine (node_modules/neo.mjs/src/core/Base.mjs=export default class Base {}), so no fixture has ever compiled a module that importsmarked. The defect is invisible to every existing arm by construction. #17931's AC-1 is what makes it observable.The Architectural Reality
src/component/Markdown.mjs:4andsrc/app/content/Component.mjs:2— the two importers.package.json—dependenciesis empty;markedsits indevDependenciesat^18.0.5.buildScripts/build/esmodules.mjs— flattenssrc/intodist/esm/src/and rewrites specifiers, but carries nothing thatsrc/imports from outsidesrc/. Its unresolved-import report is what surfaced this.parse5), shippingdist/parse5.mjsand un-npmignoringdist.The Fix
The mechanism is settled; the destination is a genuine fork and I am not deciding it unilaterally. Each option below carries its own falsifier.
markedto a runtimedependency. The emitted specifier already resolves to the workspace's ownnode_modules/marked, so the path arithmetic needs no change.markedinstalled; the two modules must resolve and be reachable by whatever serves the app. If servingnode_modulesis not acceptable for a built tree, A is insufficient alone.dist/and import from there, mirroring #17943'sparse5treatment.dist/-relative imports do not survive intodist/esm—HtmlTemplateProcessorfails on exactly that today. B is only viable if it lands together with a fix for that, so verify the emitteddist/esm/dist/*path exists before adopting.esmodules.mjsto copy imported third-party assets into the output tree and rewrite to them.markedis the only one, C is over-built for one package.My recommendation is A + the serving check, because it is the smallest change that makes the file exist for consumers, and because B is currently blocked by a defect #17931 already owns. I have not run A's falsifier; whoever takes this should run it first rather than trusting the recommendation.
Contract Ledger Matrix
package.jsondependenciessrc/module imports at runtimepackage.jsonsrc/component/Markdown.mjs,src/app/content/Component.mjsDecision Record impact
none.learn/agentos/decisions/contains no ADR governing dependency classification ordist/esmthird-party carriage — grepped at filing time, and that absence is recorded rather than read as permission. If option C is chosen, the build-layer change may warrant one.Acceptance Criteria
src/component/Markdown.mjsandsrc/app/content/Component.mjsresolve in a workspace built from the published package — not from a repo checkout, which hasmarkedvia devDependencies and hides the defect.markedis the only undeclared runtime import, so the fix is known to be complete rather than assumed — if others exist they are named here or split out.test/playwright/unit), not an e2e-shaped placement, per #17931's AC-4 finding.Out of Scope
parse5arm — #17931 owns it as its scope-transferred AC-5; fixingmarkeddoes not address it and must not be conflated with it.basePathresolves to the repo root) and #17972 (aWS/addon loading unbuilt source) — distinctdist/esmconsumer defects on the same output tree.node_modulesshould be served for a built app at all. That question sits under #17884 and is only touched here by option A's falsifier.Avoided Traps
markedinstalled as a devDependency, so every import resolves and the defect is invisible. Only a workspace built from the published package can observe it — the same blind spot that let #17943 ship.dist/esmoutput — #17931 measured it. Copying the precedent without checking it would ship a fix that cannot load.Related
parse5armparse5unreachable for consumers), and the precedent for option Bdist/esmconsumer defects, distinct mechanismsLive latest-open sweep: checked the latest 20 open
neomjs/neoissues at 2026-08-31T23:14:48Z plus the A2A claim window (12 most recent messages, ~23:00–23:14Z) — no equivalent ticket and no in-flight claim on this scope. Closed-state sweep fordist/esm marked parse5 unresolvedreturned empty. Structure-map gate: N/A — no new or relocated.mjsplacement.Retrieval Hint:
query_raw_memories("dist/esm consumer marked devDependency unresolved import real engine workspace"); anchorsfa20136f29,src/component/Markdown.mjs:4, #17931 comment 5486043960.Origin Session ID: c38a0f32-c1b0-47ff-8e14-f16ace6007e7
Authored by Grace (Anthropic Claude Opus 5, Claude Code).