Current decision and implementation contract — 2026-09-08
All three sites still need delivery at dev@b537d0a3c8101c7d9fcb2a23317b6133d8d356ec. PR #17962 was closed without merging; its earlier implementation is prior art, not completed ACs. #17997 was closed back into this ticket, not resolved.
Decision: keep browser-relative imports and publish a generated dist/marked.mjs, following the existing parse5 artifact producer/release/package contract (#17943). Copy marked's already self-contained upstream ESM file and license without recompiling it, include it in initial/release builds and package checks, and carry it from the engine package into consumer ESM output. Consumers must not install the engine's dev dependencies or edit its source to use it.
The webpack binary resolves from the active project's package context, preserving the generated workspace's compiler choice even when the engine postinstall creates its own nested webpack. The original neoPath config selection remains unchanged. Parser producers resolve their inputs from their own module context. The webpack runner invokes the resolved JavaScript entry with Node and an argv array, preserving its existing env/thread selection without shell re-splitting.
Why this shape: native import maps do not reach worker graphs. A bare marked import would therefore break dev workers. Requiring an unhoisted runtime installation adds a consumer layout constraint without need; the existing engine-owned artifact pattern avoids it. The alternative bundledDependencies layout does not survive the current ESM rewrite: it puts marked inside the engine package, while the emitted node_modules specifier addresses the consumer root. The engine-owned artifact avoids adding another rewrite rule. This is not a new dependency-loading architecture. The published artifact must also survive the existing ESM copy/flattening path; an in-repo success is insufficient.
Current falsifiers: invoking the existing parse5 producer from /private/tmp exits 1 on node_modules/parse5/dist/index.js. A real npm pack --dry-run --json --ignore-scripts includes both marked-importing source modules but zero marked files; marked is only a devDependency (^18.0.11). The source import sweep finds only these two runtime node_modules imports (the Task module hit is a JSDoc example).
Contract Ledger
| Surface |
Owner |
Delivered behavior |
Failure boundary |
Evidence |
| Thread-build CLI |
Existing buildThreads script and webpack package bin |
Project-relative compiler resolution; original engine/workspace config selection; correct argv under hoisting and spaces |
Failed launch/build exits nonzero |
Real runner with relocated/hoisted dependency fixture plus installed-engine thread builds |
| parse5 producer |
Existing buildScripts/build/parse5.mjs |
Resolves package entry independent of cwd |
Missing dependency/build failure remains an error |
Producer invoked from unrelated cwd |
| marked runtime imports |
Engine-owned generated bundle |
Both importers address a shipped relative ESM asset |
No import-map or nested node_modules requirement |
Packed-package negative/positive controls and parser execution |
| Initial/release build and tarball |
Existing build-all/package guard |
Generate and include required browser bundles, with license |
Missing artifact fails package check |
Actual pack contents, not ignore-rule inference |
| Consumer ESM carriage |
Existing esmodules artifact-copy stage |
Copy required bundles from the engine that supplied the source |
Unresolvable emitted imports remain errors |
Consumer output fixture, including marked source imports |
Intake: created 2026-08-29; previous substantive update 2026-09-01. Pre-stale under the workflow's 90-day/14-day bands; no stale/exemption label. No parent or open blocker returned by GitHub. Current-source and closed-successor checks above establish currency. ADR successor-risk: no-adr-impact found for dependency classification/carriage; existing BuildArchitecture and parse5 packaging are the local authority. Recovered Grace's packing investigation (Memory Core 57814f10-66f8-43a2-a308-5ad3066c498d); other semantic queries were irrelevant. Native Graph prebrief did not resolve the numeric ticket, so the live issue conversation/source and that memory are the grounding, not an invented graph result.
The four original AC outcomes below remain binding. This decision resolves the previously deferred packaging choice; it does not count the implementation as completed.
Original report
Three places resolve a dependency by filesystem path instead of by module resolution. All three
break for a consumer that installs the engine as a dependency, because npm hoists — and the failures
are unrelated-looking, so they get diagnosed one at a time.
Found while getting dist builds running for a workspace app that consumes the engine via file:.
All three still present at origin/dev @ 8a5fb9c0aa.
| # |
site |
specifier |
| 1 |
buildScripts/webpack/buildThreads.mjs:21 |
let webpack = './node_modules/.bin/webpack'; |
| 2 |
buildScripts/build/parse5.mjs:11 |
entryPoints: ['node_modules/parse5/dist/index.js'] |
| 3 |
src/component/Markdown.mjs:4, src/app/content/Component.mjs:2 |
import {marked} from '../../node_modules/marked/lib/marked.esm.js' |
These are two different problems and only one of them is a defect
1 and 2 are Node-side and have no constraint forcing them. Both are resolved relative to the
process cwd, not to the script. A consumer running the engine's build scripts has its own
node_modules layout — webpack and parse5 are hoisted to the consumer root, so ./node_modules/…
from the consumer's cwd may or may not hit them, and the failure is a bare ENOENT that says nothing
about hoisting. Nothing here needs a path: import.meta.resolve / createRequire(import.meta.url)
resolves both correctly from either layout.
3 is deliberate and must not be "fixed" by switching to a bare specifier. The engine loads ES
modules directly in the browser in dev mode, and there is no import map anywhere in the repo
(checked). A bare import {marked} from 'marked' would simply fail to resolve in the browser. The
relative path is what makes dev mode work — it is load-bearing, not sloppiness.
What it silently assumes is that the engine's own node_modules is not hoisted. Inside this repo
that holds. For a consumer it does not: the engine lands at <app>/node_modules/neo.mjs/, marked
hoists to <app>/node_modules/marked/, and ../../node_modules/marked/… resolves to
<app>/node_modules/neo.mjs/node_modules/marked/ — which is not there.
So #3 is a design tension needing a decision, not a line to patch:
- an import map for dev mode, freeing
src/ to use bare specifiers; or
- vendoring the handful of browser-loaded deps under a path the engine owns; or
- declaring the non-hoisted layout a supported requirement and having consumers pin it
(
nohoist / an install-time check that fails with a sentence naming the cause).
I have deliberately not picked one — it touches dev-mode loading, packaging, and consumer install, so
it wants a maintainer decision before an implementation.
Why file it as one ticket
Because the shared root is what makes it expensive: each site fails differently (a missing binary, a
bundler entry point that is not found, a module that will not resolve in the browser), so they read as
three unrelated build problems. Naming the class once is worth more than three leaf fixes discovered
separately.
Acceptance Criteria
Out of Scope
- The engine's service-thread entry: a workspace build fails that thread for lack of an app-owned
ServiceWorker.mjs, which is expected — the reference workspace has none either.
Current decision and implementation contract — 2026-09-08
All three sites still need delivery at
dev@b537d0a3c8101c7d9fcb2a23317b6133d8d356ec. PR #17962 was closed without merging; its earlier implementation is prior art, not completed ACs. #17997 was closed back into this ticket, not resolved.Decision: keep browser-relative imports and publish a generated
dist/marked.mjs, following the existing parse5 artifact producer/release/package contract (#17943). Copy marked's already self-contained upstream ESM file and license without recompiling it, include it in initial/release builds and package checks, and carry it from the engine package into consumer ESM output. Consumers must not install the engine's dev dependencies or edit its source to use it.The webpack binary resolves from the active project's package context, preserving the generated workspace's compiler choice even when the engine postinstall creates its own nested webpack. The original
neoPathconfig selection remains unchanged. Parser producers resolve their inputs from their own module context. The webpack runner invokes the resolved JavaScript entry with Node and an argv array, preserving its existing env/thread selection without shell re-splitting.Why this shape: native import maps do not reach worker graphs. A bare marked import would therefore break dev workers. Requiring an unhoisted runtime installation adds a consumer layout constraint without need; the existing engine-owned artifact pattern avoids it. The alternative bundledDependencies layout does not survive the current ESM rewrite: it puts marked inside the engine package, while the emitted node_modules specifier addresses the consumer root. The engine-owned artifact avoids adding another rewrite rule. This is not a new dependency-loading architecture. The published artifact must also survive the existing ESM copy/flattening path; an in-repo success is insufficient.
Current falsifiers: invoking the existing parse5 producer from
/private/tmpexits 1 onnode_modules/parse5/dist/index.js. A realnpm pack --dry-run --json --ignore-scriptsincludes both marked-importing source modules but zero marked files; marked is only a devDependency (^18.0.11). The source import sweep finds only these two runtime node_modules imports (the Task module hit is a JSDoc example).Contract Ledger
Intake: created 2026-08-29; previous substantive update 2026-09-01. Pre-stale under the workflow's 90-day/14-day bands; no stale/exemption label. No parent or open blocker returned by GitHub. Current-source and closed-successor checks above establish currency. ADR successor-risk: no-adr-impact found for dependency classification/carriage; existing BuildArchitecture and parse5 packaging are the local authority. Recovered Grace's packing investigation (Memory Core
57814f10-66f8-43a2-a308-5ad3066c498d); other semantic queries were irrelevant. Native Graph prebrief did not resolve the numeric ticket, so the live issue conversation/source and that memory are the grounding, not an invented graph result.The four original AC outcomes below remain binding. This decision resolves the previously deferred packaging choice; it does not count the implementation as completed.
Original report
Three places resolve a dependency by filesystem path instead of by module resolution. All three break for a consumer that installs the engine as a dependency, because npm hoists — and the failures are unrelated-looking, so they get diagnosed one at a time.
Found while getting dist builds running for a workspace app that consumes the engine via
file:. All three still present atorigin/dev @ 8a5fb9c0aa.buildScripts/webpack/buildThreads.mjs:21let webpack = './node_modules/.bin/webpack';buildScripts/build/parse5.mjs:11entryPoints: ['node_modules/parse5/dist/index.js']src/component/Markdown.mjs:4,src/app/content/Component.mjs:2import {marked} from '../../node_modules/marked/lib/marked.esm.js'These are two different problems and only one of them is a defect
1 and 2 are Node-side and have no constraint forcing them. Both are resolved relative to the process cwd, not to the script. A consumer running the engine's build scripts has its own
node_moduleslayout —webpackandparse5are hoisted to the consumer root, so./node_modules/…from the consumer's cwd may or may not hit them, and the failure is a bare ENOENT that says nothing about hoisting. Nothing here needs a path:import.meta.resolve/createRequire(import.meta.url)resolves both correctly from either layout.3 is deliberate and must not be "fixed" by switching to a bare specifier. The engine loads ES modules directly in the browser in dev mode, and there is no import map anywhere in the repo (checked). A bare
import {marked} from 'marked'would simply fail to resolve in the browser. The relative path is what makes dev mode work — it is load-bearing, not sloppiness.What it silently assumes is that the engine's own
node_modulesis not hoisted. Inside this repo that holds. For a consumer it does not: the engine lands at<app>/node_modules/neo.mjs/,markedhoists to<app>/node_modules/marked/, and../../node_modules/marked/…resolves to<app>/node_modules/neo.mjs/node_modules/marked/— which is not there.So #3 is a design tension needing a decision, not a line to patch:
src/to use bare specifiers; ornohoist/ an install-time check that fails with a sentence naming the cause).I have deliberately not picked one — it touches dev-mode loading, packaging, and consumer install, so it wants a maintainer decision before an implementation.
Why file it as one ticket
Because the shared root is what makes it expensive: each site fails differently (a missing binary, a bundler entry point that is not found, a module that will not resolve in the browser), so they read as three unrelated build problems. Naming the class once is worth more than three leaf fixes discovered separately.
Acceptance Criteria
buildScripts/webpack/buildThreads.mjsresolves thewebpackbinary through module resolution rather than a cwd-relative path, and works when invoked from a consumer whosenode_modulesis hoisted.buildScripts/build/parse5.mjsresolves its entry point the same way.markedimports — import map, vendored copy, or a supported non-hoisted layout — with the reason, so the next reader does not "fix" a deliberate relative path into a broken bare one.Out of Scope
ServiceWorker.mjs, which is expected — the reference workspace has none either.