Files
msd-core/docs/adr/1508-runtime-artifact-conversion-module.md
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

128 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Runtime Artifact Conversion Module owns per-runtime content rewriting
- **Status:** Accepted
- **Date:** 2026-06-20
- **Issue:** #1508
- **Epic:** #1507
- **Implementation:** Phase 1 (helper relocation, no behavior change) → Phase 2 (engine move + relay deletion)
The **Runtime Surface Module** (`src/surface.cts` → `surface.cjs`) re-materializes a resolved skill surface to disk via `applySurface`. For `skills` kinds it must rewrite staged `SKILL.md` bodies so their `@`-ref paths point at the install target (`pathPrefix`) instead of the converter's default `~/.claude` paths (#813). To do that it reaches **up** into the 12,289-line hand-authored `bin/install.js` via `getInstallExports()` (`src/runtime-artifact-layout.cts:53-69`) — a lazy `require('../../../bin/install.js')` guarded by a save/set/restore of `MSD_TEST_MODE` — to borrow `computePathPrefix` and `applyRuntimeContentRewritesInPlace`.
This is the **last upward dependency from the `.cts` source tree into the hand-authored installer**. It forces an env-var dance at a test seam, and it leaks: `applySurface` (and `bin/install.js`'s own three call sites) each re-derive the same five path-prefix inputs (`scope→isGlobal`, `runtime==='opencode'`, `process.platform`, normalized `resolvedTarget`, normalized `homeDir`) before calling `computePathPrefix`. The prefix-derivation knowledge is duplicated across `surface.cts` and `install.js`.
`CONTEXT.md` already names the **Runtime Artifact Conversion Module** (`src/runtime-artifact-conversion.cts`) as the `[Planned]` sibling of the Layout Module — placement vs. content. ADR-3660 *§Initial Scope* deferred exactly this consolidation: *"A future ADR may consolidate them into a Skill Conversion Module if a second consumer emerges."* `surface.cts` is that second consumer. This is that future ADR.
## Decision
- Promote the `[Planned]` **Runtime Artifact Conversion Module** (`src/runtime-artifact-conversion.cts`) to the single owner of per-runtime **content rewriting**: the per-runtime converters (already relocated as ADR-3660's "first slice", #1099), **plus** the rewrite engine `_applyRuntimeRewrites`, the staged-content walkers, path-prefix derivation, and commit attribution. The **Runtime Artifact Layout Module** keeps owning **placement** only. **Exception:** opencode and kilo path-prefix rewriting remains a deliberate `bin/install.js`-owned pre-conversion step (see `applyOpencodeFamilyPathPrefix`); this is intentional per #784 and is not a violation of the single-owner rule.
- **Public seam** — two deep calls; the caller passes only what it has, the module derives the rest:
- `rewriteStagedSkillBodies(stagedDir, { runtime, configDir, scope }, env?)` — in-place walk (skills / kimi-agents).
- `rewriteStagedCommandBodies(stagedDir, { runtime, configDir, scope }, env?) → tempDir` — copy-to-temp (commands).
- The module internally derives `isGlobal`/`isOpencode`/`isWindowsHost`/`resolvedTarget`/`homeDir` and the path prefix. `env = { homedir = os.homedir, platform = process.platform } = {}` is an injected test seam (the clock-seam analog, `RULESET.TESTS.clock-seam`).
- `computePathPrefix` becomes **private** to the module, exported as `_computePathPrefix` for direct unit + `fast-check` property tests (`RULESET.TESTS.property-based-testing`). The hand-reimplemented copy in `tests/path-replacement.test.cjs` is deleted so the **real** function is what's tested (it is effectively untested today).
- **Dependency direction:** `bin/install.js` and `runtime-artifact-layout.cts` import the conversion module; the conversion module imports **nothing upward** (not `install.js`, not `layout`) — only deeper leaves.
- `getDirName(runtime)` relocates to `src/runtime-name-policy.cts` (a clean `fs`/`path`-only leaf), so the conversion module can consume it **without** dragging in `capability-registry.cjs` (which `runtime-homes.cjs` requires). `processAttribution` / `getCommitAttribution` move **into** the conversion module (attribution is content transformation).
- The duplicate `convertClaudeToAugmentMarkdown` (verified **byte-identical** in `install.js:2584` and `conversion.cts:976`) collapses to the conversion-module copy; `install.js`'s local copy is deleted (it already re-exports `...runtimeArtifactConversion`).
- `getInstallExports` / `loadInstallExports` / the `InstallExports` interface **and the `MSD_TEST_MODE` require of `bin/install.js`** are deleted from `runtime-artifact-layout.cts`. `surface.cts` (the sole consumer) calls the conversion module's deep functions directly — removing the last upward `.cts → install.js` dependency.
## Initial Scope
### Phase 1 — helper relocation (no behavior change)
1. Move `getDirName` → `runtime-name-policy.cts`; re-point its 13 `install.js` call sites.
2. Move `processAttribution` + `getCommitAttribution` → `conversion.cts`; re-point their 21 `install.js` call sites.
3. Delete `install.js`'s local `convertClaudeToAugmentMarkdown` (copies confirmed byte-identical); rely on the conversion-module copy via the existing `...runtimeArtifactConversion` export spread. Add a **characterization test** snapshotting current augment skills-rewrite output as insurance — it should pass unchanged.
4. No public-interface change; `install.js` and `surface.cts` behavior unchanged.
### Phase 2 — engine move + deepen + delete relay
1. Move `_applyRuntimeRewrites`, `applyRuntimeContentRewritesInPlace`, `applyRuntimeContentRewritesForCommandsInPlace`, and `computePathPrefix` into `conversion.cts`.
2. Expose `rewriteStagedSkillBodies` / `rewriteStagedCommandBodies`; privatize `computePathPrefix` (`_computePathPrefix` for tests).
3. `surface.cts:applySurface` and `install.js`'s three internal sites (`7261`/`7276`, `9475`) call the deep functions; delete the per-site prefix derivation.
4. Delete `getInstallExports` / `loadInstallExports` / `InstallExports` + the `MSD_TEST_MODE` `bin/install.js` require from `runtime-artifact-layout.cts`.
5. Tests: `fast-check` property test for the rewrite engine (`$HOME`-collapse invariant; path-rewrite idempotency), direct `_computePathPrefix` unit tests, delete the `path-replacement.test.cjs` reimplementation, and a `DEFECT.GENERATIVE-FIX` parity guard ensuring no second converter copy reappears.
### These phases should NOT
- Bundle ADR-3660 **Phase 2** (install/uninstall `layout.kinds` loop collapse, ~250 lines, separate issue #3664).
- Relocate `getConfigDirFromHome` or other general install helpers the rewrite engine does not need.
## Migration Inventory
### New files
- `docs/adr/1508-runtime-artifact-conversion-module.md` (this ADR) + README index row.
- `CONTEXT.md` glossary: flip **Runtime Artifact Conversion Module** `[Planned]` → shipped, and update the Runtime Artifact Layout Module entry (the `getInstallExports` seam sentence is removed). *(lands with Phase 2)*
### Phase 1 modified
- `src/runtime-name-policy.cts` — `+getDirName`.
- `src/runtime-artifact-conversion.cts` — `+processAttribution`, `+getCommitAttribution`.
- `bin/install.js` — re-point 13 (`getDirName`) + 21 (attribution) call sites; delete local `convertClaudeToAugmentMarkdown`.
- tests — augment characterization test.
### Phase 2 modified
- `src/runtime-artifact-conversion.cts` — `+_applyRuntimeRewrites`, `+`both walkers, `+computePathPrefix` (private) + deep seam.
- `src/surface.cts` — deep-call cutover; drop the `getInstallExports` import + prefix math.
- `src/runtime-artifact-layout.cts` — delete `getInstallExports`/`loadInstallExports`/`InstallExports` + the `install.js` require.
- `bin/install.js` — three sites call the deep functions; import them back from the conversion module.
- tests — engine property test, `_computePathPrefix` unit tests, delete `path-replacement.test.cjs` reimplementation, parity guard.
## Consequences
- **+** `surface.cts` and `install.js` stop re-deriving the path prefix — one owner, leak dissolved at both sites.
- **+** The `.cts` source tree no longer reaches into hand-authored `bin/install.js`; `runtime-artifact-layout.cts` no longer requires `install.js` or toggles `MSD_TEST_MODE`.
- **+** `computePathPrefix` gains real unit + property coverage it lacks today.
- **−** `bin/install.js` stays hand-authored JS; it now imports the rewrite engine back from the generated `conversion.cjs` — the same pattern it already uses for `hooksSurface` and `...runtimeArtifactConversion`. Only the moved functions become TypeScript; `install.js` itself is not converted.
- **−** Two-phase sequence; CONTEXT.md glossary, ADR README index, and `lint:ci` (ADR-HEADER) updates required at merge.
## Relationship to other ADRs and issues
- **ADR-3660 (Runtime Artifact Layout Module):** resolves its *§Initial Scope* deferral ("A future ADR may consolidate them … if a second consumer emerges"). Layout owns placement; this module owns content. Independent of ADR-3660 **Phase 2** (#3664).
- **ADR-457 (generated-CJS single source):** the moved engine is authored in `src/*.cts` and consumed as generated `bin/lib/*.cjs`, consistent with the single-source rule.
- **ADR-1235 (descriptor-driven agent conversion):** complementary — both narrow `bin/install.js`'s ownership of conversion concerns.
- **Epic #1507** tracks the phases. **Distinct from epic #1258** (cross-runtime skill mapping + plugin skill provision/consumption): #1258 Phase A documents the converter *transform-contract catalog*; this ADR decides *module ownership + dependency direction + engine relocation*. Continues **#1099** (closed first slice that created the module) and is a sibling of **#1173** (agent-converter wiring).
## Amendment — 2026-06-24: Implementation complete (epic #1507 closed)
The decision recorded above is **implemented** on `next`. The ADR `Status` stays **Accepted** — per this directory's append-only convention there is no "Implemented" status; this dated amendment records that the decision is realized.
Landed:
- **Phase 1 (#1512):** `getDirName` → `src/runtime-name-policy.cts`; `processAttribution` → `src/runtime-artifact-conversion.cts`. (`getCommitAttribution` stays in `bin/install.js` — a documented scope refinement: it is impure install-time config I/O, not a content-transformation helper, so it cannot move into the pure conversion module. Phase 2 injects the resolved attribution value instead.)
- **Phase 2 (#1513):** the content-rewrite engine (`_applyRuntimeRewrites`), both staged-content walkers, and `computePathPrefix` (private; `_computePathPrefix` for tests) live in `src/runtime-artifact-conversion.cts` behind the deep seam `rewriteStagedSkillBodies` / `rewriteStagedCommandBodies({runtime, configDir, scope, homedir?, platform?, resolveAttribution?})`. The `getInstallExports` / `loadInstallExports` / `InstallExports` relay and the `MSD_TEST_MODE` install.js `require` are **deleted** from `src/runtime-artifact-layout.cts` — the last upward `.cts → bin/install.js` dependency is gone. `CONTEXT.md` marks the module **SHIPPED**. (The install-side cutover lands one indirection deeper than the literal issue text — `install.js` delegates to `createRuntimeArtifactInstallPlan`, which performs the deep calls in `src/runtime-artifact-install-plan.cts` — satisfying the same dependency-direction intent with a cleaner owner.)
Two deferred follow-ups were spun out as **sub-issues of #1507** and have since been **delivered** (neither was a blocker; the architectural goal — single ownership, downward dependency direction, relay deletion — was already met by the merged slices above):
- **#1675** (PR #1685) — deduped the byte-identical `convertClaudeToAugmentMarkdown` / `convertSlashCommandsToAugmentSkillMentions` between `bin/install.js` and the conversion module (the Phase 1 → Phase 2 deferred cleanup; install.js now binds them from the conversion module, single-sourced).
- **#1676** (PR #1686) — added the `fast-check` property test (`$HOME`-collapse invariant + path-rewrite idempotency) promised in #1511's test scope.
Delivery verified by a Codex (`gpt-5.4`, high-effort, read-only) review against the epic's stated deliverables, cross-checked against the indexed code graph and live source.
## Amendment — 2026-08-17 (#2876): the re-export mandate's stated beneficiary was the test suite
This ADR's relocation slices, and ADR-857's before them, kept a re-export spine in `bin/install.js`
so that *"existing consumers that do `require('../bin/install.js').writeCursorHooksJson` (etc.)
continue to work unchanged"* — the wording carried in `src/runtime-hooks-surface.cts`'s own header.
**Epic #2866 Phase 7 measured the consumers. There are none.**
`bin/install.js` exported **197** names. Every reference to it outside `tests/` — in `src/`,
`hooks/`, `scripts/` and `bin/` — sits **inside a comment**, including the one in
`runtime-hooks-surface.cts` that named the case. Not one line of production code has ever required
the file. The 44 consumers are test files.
So the pattern was real and the reasoning behind it was sound; the beneficiary was simply
misidentified. The compatibility spine was preserving access for a caller that did not exist, while
the actual dependency — the test suite reaching implementation through the installer rather than
through the modules that own it — went unnamed and therefore unmanaged.
**What Phase 7 changed.** 9 dead exports and 61 pass-throughs are gone; their tests now import the
extracted modules directly. `bin/install.js` exports **127** — 197 − 9 − 61 — and every one of those
127 is substantive local implementation. They are not a compatibility wall; they are code with no
other home yet, and extracting them is a different piece of work.
**What this does not change.** The dependency direction this ADR established — installer and layout
import the conversion module, the conversion module imports nothing upward — is untouched, and so is
ADR-857's capability-registry contract. Relocation still requires the moved symbol to keep working
for its real callers; the correction is only about who those callers are.
**The generalizable point.** A back-compat re-export is a claim about consumers, and a claim about
consumers is checkable. This one went four ADRs and several epics without being checked, which let
an interface be shaped by test access while everyone believed it was shaped by callers. Before
adding a compatibility layer, enumerate what it is compatible *for*; if the answer is "the tests",
the honest fix is to point the tests at the owning module.