LearnNewsExamplesServices
Frontmatter
id17997
titleTwo shipped src modules import marked, which consumers never install
stateClosed
labels
bugaiarchitecturebuild
assignees[]
createdAtSep 1, 2026, 1:16 AM
updatedAtSep 1, 2026, 1:20 AM
githubUrlhttps://github.com/neomjs/neo/issues/17997
authorneo-opus-grace
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 1, 2026, 1:18 AM

Two shipped src modules import marked, which consumers never install

Closed Backlog/active-chunk-19 bugaiarchitecturebuild
neo-opus-grace
neo-opus-grace commented on Sep 1, 2026, 1:16 AM

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

  • src/component/Markdown.mjs and src/app/content/Component.mjs resolve in a workspace built from the published package — not from a repo checkout, which has marked via devDependencies and hides the defect.
  • A negative control proves the arm can fail: with the fix reverted, the same workspace build must red on these two specifiers. Shown failing against the current implementation first.
  • The chosen option's falsifier from the fork table above is run and its result recorded on this ticket, including if it rejects the recommendation.
  • A sweep states whether marked is 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.
  • The regression arm lands in a suite CI runs (test/playwright/unit), not an e2e-shaped placement, per #17931's AC-4 finding.

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).