devflow-kit 2.4.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +229 -0
  2. package/README.md +111 -18
  3. package/dist/agents/git.md +822 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/ambient.js +160 -145
  6. package/dist/cli/commands/attribution-prompts.js +1 -1
  7. package/dist/cli/commands/capture.js +29 -55
  8. package/dist/cli/commands/compliance-prompts.js +1 -1
  9. package/dist/cli/commands/compliance.js +48 -55
  10. package/dist/cli/commands/context.js +17 -32
  11. package/dist/cli/commands/debug.js +65 -26
  12. package/dist/cli/commands/flags.js +3 -3
  13. package/dist/cli/commands/hud.js +34 -10
  14. package/dist/cli/commands/init-seed.js +61 -27
  15. package/dist/cli/commands/init.js +649 -240
  16. package/dist/cli/commands/install-report.js +200 -0
  17. package/dist/cli/commands/knowledge/index.js +2 -2
  18. package/dist/cli/commands/knowledge/toggle.js +35 -37
  19. package/dist/cli/commands/learning.js +79 -57
  20. package/dist/cli/commands/legacy-hooks.js +11 -14
  21. package/dist/cli/commands/memory.js +134 -135
  22. package/dist/cli/commands/prompt-io.js +4 -4
  23. package/dist/cli/commands/proxy.js +23 -41
  24. package/dist/cli/commands/security.js +81 -29
  25. package/dist/cli/commands/skills.js +71 -7
  26. package/dist/cli/commands/tracker-prompts.js +145 -0
  27. package/dist/cli/commands/tracker.js +277 -0
  28. package/dist/cli/commands/uninstall.js +520 -169
  29. package/dist/cli.js +2 -0
  30. package/dist/commands/bug-analysis.md +58 -14
  31. package/dist/commands/code-review.md +110 -32
  32. package/dist/commands/debug.md +55 -11
  33. package/dist/commands/dynamic-build.md +344 -73
  34. package/dist/commands/dynamic-plan.md +77 -27
  35. package/dist/commands/dynamic-profile.md +25 -11
  36. package/dist/commands/dynamic-tickets.md +76 -15
  37. package/dist/commands/explore.md +37 -7
  38. package/dist/commands/implement.md +314 -62
  39. package/dist/commands/plan.md +146 -32
  40. package/dist/commands/release.md +64 -17
  41. package/dist/commands/research.md +34 -8
  42. package/dist/commands/resolve.md +196 -68
  43. package/dist/commands/self-review.md +45 -9
  44. package/dist/core/agent-models.js +55 -12
  45. package/dist/core/assets.js +58 -2
  46. package/dist/core/compliance-compose.js +27 -27
  47. package/dist/core/evidence-policy.js +363 -0
  48. package/dist/core/feature-config.js +200 -65
  49. package/dist/core/feature-switch.js +112 -0
  50. package/dist/core/flags.js +34 -6
  51. package/dist/core/fs-atomic.js +27 -0
  52. package/dist/core/hook-log-dirs.js +104 -0
  53. package/dist/core/learning-tuning-config.js +5 -3
  54. package/dist/core/ledger-root.js +102 -0
  55. package/dist/core/manifest.js +38 -10
  56. package/dist/core/mds-variants.js +798 -0
  57. package/dist/core/migrations.js +49 -23
  58. package/dist/core/model-discovery.js +12 -1
  59. package/dist/core/plugins.js +361 -12
  60. package/dist/core/project-paths.js +1 -18
  61. package/dist/core/proxy-log.js +8 -6
  62. package/dist/core/proxy-state.js +11 -8
  63. package/dist/core/reference-sweep.js +136 -0
  64. package/dist/core/same-location.js +25 -0
  65. package/dist/core/tracker.js +494 -0
  66. package/dist/hud/components/config-counts.js +15 -4
  67. package/dist/hud/components/learning-counts.js +14 -0
  68. package/dist/hud/config.js +2 -1
  69. package/dist/hud/cost-history.js +2 -4
  70. package/dist/hud/git.js +52 -7
  71. package/dist/hud/index.js +7 -9
  72. package/dist/skills/git/references/decision-markers.md +19 -0
  73. package/dist/skills/git/references/learn-conventions.md +56 -0
  74. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  75. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  76. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  77. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  78. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  79. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  80. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  81. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  82. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  83. package/dist/skills/git/references/publication-gate.md +13 -0
  84. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  85. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  87. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  88. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  89. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  90. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  91. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  92. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  93. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  94. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  95. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  96. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  97. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  98. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  99. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  100. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  101. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  102. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  103. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  104. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  105. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  106. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  107. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  108. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  109. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  110. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  111. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  112. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  113. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  114. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  115. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  116. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  117. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  118. package/dist/skills/git/references/trust-rule.md +7 -0
  119. package/dist/targets/claude-code/claude-paths.js +59 -57
  120. package/dist/targets/claude-code/compliance-install.js +49 -65
  121. package/dist/targets/claude-code/hooks.js +108 -3
  122. package/dist/targets/claude-code/installer.js +1187 -32
  123. package/dist/targets/claude-code/legacy.js +5 -0
  124. package/dist/targets/claude-code/post-install.js +366 -151
  125. package/dist/targets/claude-code/tracker-install.js +134 -0
  126. package/package.json +8 -6
  127. package/src/assets/agents/code.md +45 -6
  128. package/src/assets/agents/design.md +2 -1
  129. package/src/assets/agents/git.mds +825 -0
  130. package/src/assets/agents/knowledge.md +3 -3
  131. package/src/assets/agents/learning.md +11 -0
  132. package/src/assets/agents/review.md +3 -1
  133. package/src/assets/agents/synthesize.md +1 -1
  134. package/src/assets/agents/test.md +16 -5
  135. package/src/assets/agents/tracker.md +474 -0
  136. package/src/assets/agents/validate.md +7 -5
  137. package/src/assets/commands/_partials/_compliance.mds +19 -1
  138. package/src/assets/commands/_partials/_decisions.mds +15 -3
  139. package/src/assets/commands/_partials/_docs_root.mds +35 -0
  140. package/src/assets/commands/_partials/_engine.mds +13 -11
  141. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  142. package/src/assets/commands/_partials/_factory.mds +1 -1
  143. package/src/assets/commands/_partials/_knowledge.mds +27 -9
  144. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  145. package/src/assets/commands/_partials/_preamble.mds +2 -2
  146. package/src/assets/commands/_partials/_publication.mds +8 -2
  147. package/src/assets/commands/_partials/_settings.mds +28 -0
  148. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  149. package/src/assets/commands/_partials/_tracker.mds +18 -0
  150. package/src/assets/commands/_partials/_wave.mds +16 -10
  151. package/src/assets/commands/bug-analysis.mds +31 -19
  152. package/src/assets/commands/code-review.mds +67 -41
  153. package/src/assets/commands/debug.mds +13 -7
  154. package/src/assets/commands/dynamic-build.mds +274 -66
  155. package/src/assets/commands/dynamic-plan.mds +50 -23
  156. package/src/assets/commands/dynamic-profile.mds +24 -11
  157. package/src/assets/commands/dynamic-tickets.mds +63 -16
  158. package/src/assets/commands/explore.mds +4 -5
  159. package/src/assets/commands/implement.mds +234 -67
  160. package/src/assets/commands/plan.mds +91 -33
  161. package/src/assets/commands/release.md +64 -17
  162. package/src/assets/commands/research.mds +11 -9
  163. package/src/assets/commands/resolve.mds +150 -78
  164. package/src/assets/commands/self-review.mds +24 -25
  165. package/src/assets/mds/git/_pr.mds +331 -0
  166. package/src/assets/mds/git/_references.mds +135 -0
  167. package/src/assets/mds/tracker/_common.mds +156 -0
  168. package/src/assets/mds/tracker/_github.mds +472 -0
  169. package/src/assets/mds/tracker/_jira.mds +407 -0
  170. package/src/assets/mds/tracker/_linear.mds +449 -0
  171. package/src/assets/mds/tracker/_mcp.mds +305 -0
  172. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  173. package/src/assets/scripts/hooks/background-memory-update +40 -19
  174. package/src/assets/scripts/hooks/capture-prompt +18 -8
  175. package/src/assets/scripts/hooks/capture-question +18 -8
  176. package/src/assets/scripts/hooks/capture-turn +27 -13
  177. package/src/assets/scripts/hooks/debug-trace +11 -6
  178. package/src/assets/scripts/hooks/ensure-devflow-init +33 -6
  179. package/src/assets/scripts/hooks/ensure-proxy +9 -8
  180. package/src/assets/scripts/hooks/ensure-root-gitignore +236 -60
  181. package/src/assets/scripts/hooks/git-marker +48 -0
  182. package/src/assets/scripts/hooks/hook-log-init +3 -1
  183. package/src/assets/scripts/hooks/json-helper.cjs +228 -5
  184. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -20
  185. package/src/assets/scripts/hooks/log-paths +80 -0
  186. package/src/assets/scripts/hooks/memory-worker +22 -13
  187. package/src/assets/scripts/hooks/pre-compact-memory +44 -15
  188. package/src/assets/scripts/hooks/preamble +1 -4
  189. package/src/assets/scripts/hooks/queue-append +146 -28
  190. package/src/assets/scripts/hooks/resolve-project-root +101 -7
  191. package/src/assets/scripts/hooks/session-start-context +534 -20
  192. package/src/assets/scripts/hooks/session-start-memory +38 -15
  193. package/src/assets/scripts/lib/project-config.cjs +633 -0
  194. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  195. package/src/assets/scripts/redact-secrets.cjs +490 -62
  196. package/src/assets/scripts/release-trace.cjs +1143 -0
  197. package/src/assets/scripts/resolve-evidence-policy.cjs +1145 -0
  198. package/src/assets/scripts/resolve-settings.cjs +1054 -0
  199. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  200. package/src/assets/skills/compliance/SKILL.md +4 -2
  201. package/src/assets/skills/docs-framework/SKILL.md +11 -10
  202. package/src/assets/skills/docs-framework/references/patterns.md +10 -17
  203. package/src/assets/skills/gap-analysis/SKILL.md +2 -2
  204. package/src/assets/skills/git/SKILL.md +8 -78
  205. package/src/assets/skills/git/references/github-api.md +179 -141
  206. package/src/assets/skills/git/references/patterns.md +11 -6
  207. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  208. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  209. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  210. package/src/assets/skills/worktree-support/SKILL.md +1 -1
  211. package/src/assets/skills/worktree-support/references/roots.md +29 -0
  212. package/src/targets/claude-code/templates/managed-settings.json +25 -9
  213. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,798 @@
1
+ /**
2
+ * MDS host output validation and variant expansion.
3
+ *
4
+ * Pure module — zero I/O. Every question a caller asks about a HOST is answered
5
+ * with a Result; callers own every filesystem call and every process exit.
6
+ *
7
+ * applies ADR-013: pure core-layer module, no build-script or adapter concerns.
8
+ * The registries below are agent-neutral, so what is DERIVED from them is derived
9
+ * here rather than inside an install target — a target adapter computing a build
10
+ * fact, with tests importing that adapter to learn it, is the seam inverting.
11
+ * avoids PF-014: no process.exit(); every fallible path returns Result. The
12
+ * exiting shell is scripts/build-mds.ts, which renders these errors into its
13
+ * pre-existing messages.
14
+ *
15
+ * Scope guarantee: this module answers exactly five questions —
16
+ * 1. Is the filename a host will emit safe? (validateOutputName)
17
+ * 2. Is the directory it declares one the build may write into, and which host
18
+ * variant does that directory select? (resolveOutputDir)
19
+ * 3. Which files does a reference module fan out into? (expandVariants)
20
+ * 4. Which slice of its compiled body belongs to each? (splitVariantSections)
21
+ * 5. Which files does the shipped registry produce, flattened into the manifest
22
+ * an installer converges to? (generatedReferenceManifest — the one answer
23
+ * that asserts instead of returning a Result; see the function for why.)
24
+ * It still performs no I/O and no iteration over the filesystem.
25
+ *
26
+ * The `-variants` in the filename names the variant-expansion entry point below
27
+ * (DR-16), which lives next to the validation it depends on.
28
+ */
29
+ import * as path from 'path';
30
+ import { isContainedIn } from './paths.js';
31
+ function Ok(value) {
32
+ return { ok: true, value };
33
+ }
34
+ function Err(error) {
35
+ return { ok: false, error };
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // Output filename validation
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Charset an emitted output basename must satisfy before it is joined onto a
42
+ * build destination directory.
43
+ *
44
+ * Rules (same anchored, bounded, alternation-free shape as MODEL_NAME_RE in
45
+ * agent-frontmatter.ts):
46
+ * - Start with a lowercase alphanumeric character.
47
+ * - Remaining characters: lowercase alphanumeric, dot, underscore, hyphen.
48
+ * - Total length: 1–64 characters.
49
+ *
50
+ * Accepts every basename the repo ships (`implement`, `code-review`,
51
+ * `dynamic-build`, `git`, …) and refuses uppercase, whitespace, and shell
52
+ * metacharacters outright.
53
+ */
54
+ const OUTPUT_NAME_RE = /^[a-z0-9][a-z0-9._-]{0,63}$/;
55
+ /**
56
+ * Validate the basename an MDS host will emit (before `.md` is appended).
57
+ *
58
+ * Traversal is reported ahead of the separator check so `../x` is diagnosed as
59
+ * traversal rather than as a generic slash, and `a/b` is diagnosed as nesting.
60
+ * Both are refused; the distinction only shapes the build's error message.
61
+ */
62
+ export function validateOutputName(name) {
63
+ if (name === '')
64
+ return Err({ kind: 'empty' });
65
+ const segments = name.split(/[\\/]/);
66
+ if (segments.some(segment => segment === '..' || segment === '.')) {
67
+ return Err({ kind: 'dot-segment', name });
68
+ }
69
+ if (segments.length > 1) {
70
+ return Err({ kind: 'path-separator', name });
71
+ }
72
+ if (!OUTPUT_NAME_RE.test(name)) {
73
+ return Err({ kind: 'invalid-charset', name });
74
+ }
75
+ return Ok(name);
76
+ }
77
+ /**
78
+ * Validate the emitted basename of a CONTRACT document: one leading underscore,
79
+ * then the ordinary name rule.
80
+ *
81
+ * A SECOND function rather than a relaxed OUTPUT_NAME_RE, and the distinction is
82
+ * not stylistic. The underscore is MANDATORY here and FORBIDDEN there, because it
83
+ * is what tells a reader of the references tree which entries are providers:
84
+ * `tracker/_mcp.md` sits beside the provider DIRECTORIES `tracker/github/` and
85
+ * `tracker/jira/`, and `tracker/mcp.md` would read as a third provider.
86
+ * Relaxing the shared rule instead would have admitted `_anything.md`
87
+ * as a command or an agent basename too — a widening across all three build
88
+ * destinations to buy a property only this one needs (ADR-025: classify the case,
89
+ * never blanket-widen).
90
+ *
91
+ * Every other guarantee is inherited by delegation, so the dot-segment,
92
+ * separator, charset and length refusals cannot drift apart from their originals.
93
+ * The refusal reports the name AS WRITTEN — a reader of the build's error needs
94
+ * the string they typed, not its underscore-stripped remainder.
95
+ */
96
+ export function validateContractOutputName(name) {
97
+ if (!name.startsWith('_'))
98
+ return Err({ kind: 'invalid-charset', name });
99
+ const inner = validateOutputName(name.slice(1));
100
+ if (inner.ok)
101
+ return Ok(name);
102
+ return Err(inner.error.kind === 'empty' ? { kind: 'empty' } : { ...inner.error, name });
103
+ }
104
+ /**
105
+ * The only directories the MDS build may write into, each tagged with the host
106
+ * variant it selects.
107
+ *
108
+ * `dist/commands` holds compiled slash commands; `dist/agents` holds agents
109
+ * compiled from generator hosts; `dist/skills/git/references` holds the generated
110
+ * `devflow:git` skill references. Adding an entry here is the single place a new
111
+ * build destination becomes legal — and `satisfies` forces that entry to declare
112
+ * a HostVariant, so no destination can arrive without saying how it is treated.
113
+ */
114
+ /**
115
+ * Repo-relative destination for `agents` hosts.
116
+ *
117
+ * Exported because the build's orphan prune must name this directory even when
118
+ * no generator host is planned — which is exactly the case where every file in
119
+ * it is an orphan, so the directory cannot be derived from the plan. Reading it
120
+ * from here keeps the table below the only place a destination is spelled.
121
+ */
122
+ export const AGENTS_OUTPUT_DIR = 'dist/agents';
123
+ /**
124
+ * The bare (unprefixed) skill that OWNS the generated references.
125
+ *
126
+ * One fact, three derivations: SKILL_REFS_OUTPUT_DIR below is composed from it,
127
+ * the installer decides which skill install triggers the reference overlay from
128
+ * it, and the init summary renders `prefixSkillName()` of it. Retyped at each of
129
+ * those three sites, moving the references to another skill would mean finding
130
+ * all three spellings with nothing failing if only two were found — the PF-013
131
+ * shape, a hardcoded spelling that still resolves.
132
+ *
133
+ * Bare, not `devflow:`-prefixed: the build writes to `dist/skills/git/…` while
134
+ * the install target is `skills/devflow:git/`. prefixSkillName is what spans that
135
+ * gap, and it is applied at the install sites rather than baked in here.
136
+ */
137
+ export const SKILL_REFS_SKILL_NAME = 'git';
138
+ /**
139
+ * Repo-relative destination for `skill-refs` hosts — the generated `devflow:git`
140
+ * skill references.
141
+ *
142
+ * D-SKILLREFS-ALLOWLIST: the third allowlist entry is deliberate, not incidental.
143
+ * The alternative was to let the build write these files through a path composed
144
+ * outside resolveOutputDir, which would have made the allowlist a partial gate —
145
+ * true for two destinations and bypassed for the third. Routing them through the
146
+ * same table keeps ONE answer to "where may the build write", so a future
147
+ * destination is added in one place and inherits containment, the backslash and
148
+ * canonical-spelling checks, and the exhaustive-dispatch friction that
149
+ * HostVariant imposes on every consumer.
150
+ *
151
+ * Exported because the build's orphan prune must name this directory even when
152
+ * no reference module is planned — the same reason AGENTS_OUTPUT_DIR is exported.
153
+ */
154
+ export const SKILL_REFS_OUTPUT_DIR = `dist/skills/${SKILL_REFS_SKILL_NAME}/references`;
155
+ const ALLOWED_OUTPUT_DIRS = [
156
+ { dir: 'dist/commands', variant: 'commands' },
157
+ { dir: AGENTS_OUTPUT_DIR, variant: 'agents' },
158
+ { dir: SKILL_REFS_OUTPUT_DIR, variant: 'skill-refs' },
159
+ ];
160
+ /**
161
+ * The allowlisted directory names, in declaration order, for error rendering.
162
+ *
163
+ * Exported so guards assert the build's refusal text against the table itself
164
+ * rather than against a retyped literal: adding a destination then rewrites both
165
+ * the message and its assertion from one edit (PF-018 — the expectation must
166
+ * come from the thing under test, not a copy of it).
167
+ */
168
+ export const ALLOWED_OUTPUT_DIR_NAMES = ALLOWED_OUTPUT_DIRS.map(entry => entry.dir);
169
+ /**
170
+ * Resolve a host's declared `output-dir:` against `root` and check it against
171
+ * the allowlist.
172
+ *
173
+ * Four refusals, in order:
174
+ * 1. `escapes-root` — the declaration resolves outside `root`
175
+ * (`dist/../..`, an absolute path elsewhere). Containment is decided by
176
+ * isContainedIn, which compares resolved paths rather than string prefixes.
177
+ * 2. `backslash-separator` — the declaration contains a backslash. Declarations
178
+ * are POSIX-spelled by contract; the canonical check below normalises as
179
+ * POSIX, where a backslash is an ordinary character, so a win32-style
180
+ * spelling would otherwise slip through as canonical on win32 only.
181
+ * 3. `non-canonical` — the declaration resolves onto an allowlisted target
182
+ * but is not spelled canonically (`dist/commands/`, `./dist/agents`,
183
+ * `dist/skills/../commands`). One target must have exactly one spelling.
184
+ * 4. `not-allowlisted` — the resolved target is not an allowlisted directory.
185
+ *
186
+ * On success the resolved absolute directory is returned together with the host
187
+ * variant the matching allowlist entry declares, so callers dispatch on a value
188
+ * they were handed rather than one they re-derive.
189
+ */
190
+ export function resolveOutputDir(root, declared) {
191
+ if (!isContainedIn(root, declared)) {
192
+ return Err({ kind: 'escapes-root', declared });
193
+ }
194
+ if (declared.includes('\\')) {
195
+ return Err({ kind: 'backslash-separator', declared, allowed: ALLOWED_OUTPUT_DIR_NAMES });
196
+ }
197
+ // Canonical spelling: POSIX-normalised, no trailing separator. The frontmatter
198
+ // value is always written with forward slashes, so normalise as POSIX and
199
+ // resolve with the platform resolver.
200
+ const canonical = path.posix.normalize(declared).replace(/\/+$/, '');
201
+ if (canonical !== declared) {
202
+ return Err({ kind: 'non-canonical', declared, canonical, allowed: ALLOWED_OUTPUT_DIR_NAMES });
203
+ }
204
+ const abs = path.resolve(root, declared);
205
+ const match = ALLOWED_OUTPUT_DIRS.find(entry => path.resolve(root, entry.dir) === abs);
206
+ if (match === undefined) {
207
+ return Err({ kind: 'not-allowlisted', declared, allowed: ALLOWED_OUTPUT_DIR_NAMES });
208
+ }
209
+ return Ok({ variant: match.variant, abs });
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Variant expansion — one reference module fans out into many op files
213
+ // ---------------------------------------------------------------------------
214
+ /**
215
+ * The 11 tracker operations whose provider mechanics are generated as separate
216
+ * skill reference files.
217
+ *
218
+ * Bidirectional parity, the COMPLIANCE_SKILL_TOKENS model
219
+ * (src/core/compliance-compose.ts): every op named here must have a section in
220
+ * the module that declares it, and every section in that module must be named
221
+ * here. splitVariantSections enforces both directions; neither alone is enough —
222
+ * the forward direction alone lets a stray section ship unreferenced, and the
223
+ * reverse alone lets a listed op silently emit nothing.
224
+ *
225
+ * The list is long from its first commit on purpose. A one- or two-element list
226
+ * makes every parity assertion over it vacuous (GAP-42, the PF-018 trap) and is
227
+ * structurally identical to the single-arm conditional AC-1.2 forbids, so
228
+ * expandVariants refuses a pair list below MIN_VARIANT_PAIRS.
229
+ */
230
+ export const TRACKER_OPS = [
231
+ 'setup-task',
232
+ 'fetch-issue',
233
+ 'fetch-issues-batch',
234
+ 'manage-debt',
235
+ 'create-release',
236
+ 'gather-release-evidence',
237
+ 'backlink-shipped-issues',
238
+ 'associate-release',
239
+ 'ensure-traceable-issue',
240
+ 'post-wave-report',
241
+ 'ensure-pr-ready',
242
+ ];
243
+ /**
244
+ * The GitHub provider's operation set — the SAME list, under the name that reads
245
+ * correctly at a GitHub-scoped call site.
246
+ *
247
+ * An alias, not a copy, and both names are load-bearing:
248
+ *
249
+ * - {@link TRACKER_OPS} is the ROSTER. Every provider row in VARIANT_MODULES
250
+ * reads it, which is what makes AC-3.8's file-set parity a compile-time
251
+ * property instead of an assertion two hand-listed arrays have to keep
252
+ * agreeing on.
253
+ * - `TRACKER_GITHUB_OPS` is a PROVIDER SCOPE. Several guards genuinely mean
254
+ * "the ops of the GitHub path" rather than "the roster" — the byte budget's
255
+ * GitHub-scoped loaded-set row (D-LOADED-SET-SCOPE), the re-scoped AC-2.7
256
+ * arm that proves no github op file names the tool-call contract, and the
257
+ * containment oracle's github corpus. Reading the roster's name at those
258
+ * sites would say something subtly different from what they check.
259
+ *
260
+ * The two sets are identical today and identity is asserted by `toBe` at the
261
+ * registration sites, so this is one list with two readings rather than a
262
+ * synonym nobody maintains. If a provider ever needs an op the others do not,
263
+ * this alias is where that divergence becomes visible.
264
+ */
265
+ export const TRACKER_GITHUB_OPS = TRACKER_OPS;
266
+ /**
267
+ * The 9 PR/review operations whose mechanics are generated once, for every
268
+ * provider, under `pr/`.
269
+ *
270
+ * A PR-HOST roster, not a tracker roster, and the distinction is the whole
271
+ * reason this list exists separately from {@link TRACKER_OPS}: pull requests, PR
272
+ * reviews and PR checks stay on GitHub under every issue-tracker provider, so
273
+ * these steps are the SAME file whatever `TRACKER_PROVIDER` resolves to. Filing
274
+ * them under `tracker/github/` would make a jira user's PR mechanics read as
275
+ * their tracker's, and fanning them across the three provider directories would
276
+ * ship three identical trees.
277
+ *
278
+ * `ensure-pr-ready` is a member of BOTH rosters by design, and the two halves do
279
+ * not overlap: the PR skeleton (branch/commit/push, create, retitle, and step
280
+ * 4b's open-PR lookup and body edit) is a PR-host fact and lives here; step 4b
281
+ * itself — the issue-number lookup and the link line it renders — is a tracker
282
+ * fact and lives in `tracker/{provider}/ensure-pr-ready.md`. The operation
283
+ * carries one pointer to each.
284
+ *
285
+ * 9 entries, one above {@link MIN_VARIANT_PAIRS}, which is a floor and not a
286
+ * target: a shorter roster makes every parity assertion over it vacuous (GAP-42,
287
+ * the PF-018 trap) and `expandVariants` refuses the build, so the roster can grow
288
+ * but never drop below 8. `update-pr-evidence` (#363) is the ninth — it edits the
289
+ * PR body and comments on the PR, both GitHub whatever the tracker is.
290
+ */
291
+ export const PR_HOST_OPS = [
292
+ 'ensure-pr-ready',
293
+ 'validate-branch',
294
+ 'post-review-summary',
295
+ 'check-ci-status',
296
+ 'fetch-review-threads',
297
+ 'resolve-review-threads',
298
+ 'post-resolution-summary',
299
+ 'check-merge-readiness',
300
+ 'update-pr-evidence',
301
+ ];
302
+ /**
303
+ * The destination directory the PR-host module lands under.
304
+ *
305
+ * Stated, exactly as {@link TRACKER_DESTINATION_ROOT} is, rather than derived
306
+ * from the module that writes there: it is a fact about where PR mechanics live
307
+ * in the reference tree, not something the registry can work out.
308
+ *
309
+ * It is the provider-independent `pr/` directory, a sibling of the `tracker/`
310
+ * tree rather than a directory inside it: it is under no provider, and every
311
+ * install carries it because a jira or linear user still opens pull requests on
312
+ * GitHub.
313
+ */
314
+ export const PR_HOST_DESTINATION_ROOT = 'pr';
315
+ /**
316
+ * The cross-cutting `devflow:git` reference documents — provider-independent, so
317
+ * they land at the root of the references directory rather than under
318
+ * `tracker/{provider}/`.
319
+ *
320
+ * `decision-markers` holds the D1–D3 / D5–D10 rows of the agent's Decision Marker
321
+ * Legend. The D4 and D11 rows are the ONLY definitions of labels whose controls
322
+ * are always-loaded, so they stay inline in the agent (E10 / AC-2.13); the rest
323
+ * are glossary entries a reader consults, not rules a spawn must have.
324
+ *
325
+ * `learn-conventions` holds that operation's bounded scan and its untrusted-string
326
+ * discipline. It is GENERATED rather than hand-authored on purpose [DR-15]: the
327
+ * Phase-3 Tracker agent NAMES this file instead of copying the block, so the
328
+ * bounded-scan literals and the post-composition verbatim-match check never exist
329
+ * in a second, independently maintained copy outside the single-authority corpus.
330
+ *
331
+ * `publication-gate` holds the D10 step order. It is named from the two summary
332
+ * operations and from nowhere else, which is the scope property [DR-20] asserts:
333
+ * an operation that can load the gate is an operation that probes repo visibility.
334
+ *
335
+ * `trust-rule` is the ONE prose statement of who counts as a trusted author of a
336
+ * PR comment, review thread or review (#363). `pr-evidence.cjs`'s `trust()` is its
337
+ * one implementation, and a parity test holds the two to the same terms. It is the
338
+ * only document here named from a PR-HOST reference rather than from the agent:
339
+ * `references/pr/fetch-review-threads.md` applies it and names it, while an op that
340
+ * runs the evidence scripts gets the rule from `trust()` and never loads the
341
+ * document — naming it from the agent would bill every spawn for a rule one
342
+ * operation reads.
343
+ */
344
+ export const GIT_CROSS_CUTTING_DOCS = [
345
+ 'decision-markers',
346
+ 'learn-conventions',
347
+ 'publication-gate',
348
+ 'trust-rule',
349
+ ];
350
+ /**
351
+ * Every reference module the build knows about — a closed registry, read the
352
+ * same way ALLOWED_OUTPUT_DIRS is read.
353
+ *
354
+ * A `skill-refs` host whose source path is absent from this table is refused by
355
+ * the build rather than guessed at: the emitted filenames come from the op list,
356
+ * not from the module's own basename, so there is nothing to fall back to.
357
+ *
358
+ * Every provider row reads the ONE shared {@link TRACKER_OPS} roster, so the three
359
+ * providers below emit the same file set by construction — file-set parity is a
360
+ * compile-time property rather than an assertion two hand-listed arrays have to
361
+ * keep agreeing on.
362
+ *
363
+ * Registering a provider whose `subdir` is one of MCP_BACKED_PROVIDER_SUBDIRS is
364
+ * also what opens the generation gate on the tool-call contract; see
365
+ * {@link mcpContractIsGenerated}. There is no second edit and no flag.
366
+ */
367
+ export const VARIANT_MODULES = [
368
+ {
369
+ source: 'src/assets/mds/tracker/_github.mds',
370
+ subdir: 'tracker/github',
371
+ kind: 'fanout',
372
+ ops: TRACKER_GITHUB_OPS,
373
+ },
374
+ {
375
+ source: 'src/assets/mds/tracker/_jira.mds',
376
+ subdir: 'tracker/jira',
377
+ kind: 'fanout',
378
+ ops: TRACKER_OPS,
379
+ },
380
+ {
381
+ source: 'src/assets/mds/tracker/_linear.mds',
382
+ subdir: 'tracker/linear',
383
+ kind: 'fanout',
384
+ ops: TRACKER_OPS,
385
+ },
386
+ {
387
+ source: 'src/assets/mds/git/_pr.mds',
388
+ subdir: PR_HOST_DESTINATION_ROOT,
389
+ kind: 'fanout',
390
+ ops: PR_HOST_OPS,
391
+ },
392
+ {
393
+ source: 'src/assets/mds/git/_references.mds',
394
+ subdir: '',
395
+ kind: 'named',
396
+ ops: GIT_CROSS_CUTTING_DOCS,
397
+ },
398
+ ];
399
+ // ---------------------------------------------------------------------------
400
+ // The tool-call contract module, and the gate on its generation
401
+ // (hazard H7, conflict C5)
402
+ // ---------------------------------------------------------------------------
403
+ /**
404
+ * The tracker provider destinations whose mechanics reach the tracker through a
405
+ * TOOL CALL rather than through a CLI — the condition the contract document's
406
+ * generation is keyed on.
407
+ *
408
+ * `tracker/github` is deliberately absent: GitHub's mechanics are `gh` commands,
409
+ * and a gate keyed on "any tracker module is registered" would already be open.
410
+ *
411
+ * Spelled as DESTINATIONS rather than provider names so the gate is a fact about
412
+ * the registry: a provider module is registered with the subdir its files land
413
+ * in, so opening the gate and shipping the provider are the same edit. A boolean
414
+ * field on VariantModule would have been a flag someone has to remember to flip,
415
+ * which is the same class of defect as a floor nobody raises.
416
+ */
417
+ export const MCP_BACKED_PROVIDER_SUBDIRS = ['tracker/jira', 'tracker/linear'];
418
+ /** The destination directory every tracker provider module lands under. */
419
+ export const TRACKER_DESTINATION_ROOT = 'tracker';
420
+ /**
421
+ * The provider-independent tool-call contract document.
422
+ *
423
+ * GENERATED only while {@link mcpContractIsGenerated} is true — that is, only
424
+ * while a provider that reaches its tracker through a tool call is registered.
425
+ * The gate is not a phase marker; it is the answer to "does anyone load this?",
426
+ * and it stays answerable in both directions:
427
+ *
428
+ * - Open, as it is whenever a tool-call provider is registered: every such
429
+ * provider's per-operation mechanics NAME this document, so it must exist or
430
+ * those references point at a file the install does not carry.
431
+ * - Shut, as it is for a registry with GitHub alone: no reachable consumer
432
+ * exists, and generating it anyway would bill every GitHub user for a
433
+ * reference nothing they can reach ever loads (GAP-02). The byte-budget
434
+ * formula carries it as a term that is 0 on the GitHub path for exactly that
435
+ * reason, and the re-scoped AC-2.7 arm proves no github op file names it.
436
+ *
437
+ * It lands at the `tracker/` ROOT rather than inside a provider directory: it is
438
+ * provider-independent, and a copy per provider is the duplication it exists to
439
+ * remove. The `_` prefix is what distinguishes it from the provider directories
440
+ * beside it (validateContractOutputName).
441
+ */
442
+ export const MCP_CONTRACT_MODULE = {
443
+ source: 'src/assets/mds/tracker/_mcp.mds',
444
+ subdir: 'tracker',
445
+ kind: 'contract',
446
+ ops: ['_mcp'],
447
+ };
448
+ /**
449
+ * Does this registry contain a provider that needs the tool-call contract?
450
+ *
451
+ * The whole gate, in one derived predicate: registering a provider module in an
452
+ * MCP-backed sub-directory is what starts the contract being generated, with no
453
+ * second edit anywhere and no declaration to keep in step.
454
+ */
455
+ export function mcpContractIsGenerated(modules = VARIANT_MODULES) {
456
+ const gated = MCP_BACKED_PROVIDER_SUBDIRS;
457
+ return modules.some(mod => gated.includes(mod.subdir));
458
+ }
459
+ /**
460
+ * Every reference module whose GENERATION is conditional, each beside the
461
+ * predicate that answers for it.
462
+ *
463
+ * ONE table, read by both halves of the mechanism: {@link resolveVariantModules}
464
+ * appends the modules whose predicate says yes, and
465
+ * {@link GATED_REFERENCE_MODULE_SOURCES} is this table's source column. A module
466
+ * added here therefore reaches the resolver and the gated roster in the same
467
+ * edit. Naming the module inline in the resolver and again in the roster is how
468
+ * a roster and the code that produces it come to disagree the first time a
469
+ * second one is added — the same defect {@link deferredReferenceModuleSources}
470
+ * exists to keep out of its two callers.
471
+ */
472
+ export const GATED_REFERENCE_MODULES = [
473
+ { module: MCP_CONTRACT_MODULE, isGenerated: mcpContractIsGenerated },
474
+ ];
475
+ /**
476
+ * The registry the build actually expands: {@link VARIANT_MODULES} plus every
477
+ * gated module whose own gate is open.
478
+ *
479
+ * Idempotent — resolving an already-resolved list appends nothing. Without that,
480
+ * a caller that resolved twice would hand expandVariants two rows for one source
481
+ * and get a `duplicate-output` refusal describing a bug it could not locate.
482
+ *
483
+ * Each predicate is asked about the registry AS PASSED, never about the list the
484
+ * loop is building, so a gate can never be opened by a module an earlier gate
485
+ * appended.
486
+ *
487
+ * @param modules - Registry to resolve (defaults to VARIANT_MODULES). Injectable
488
+ * so both sides of every gate are provable against a registry that never has to
489
+ * exist on disk.
490
+ */
491
+ export function resolveVariantModules(modules = VARIANT_MODULES) {
492
+ let resolved = modules;
493
+ for (const gated of GATED_REFERENCE_MODULES) {
494
+ if (!gated.isGenerated(modules))
495
+ continue;
496
+ if (resolved.some(mod => mod.source === gated.module.source))
497
+ continue;
498
+ resolved = [...resolved, gated.module];
499
+ }
500
+ return resolved;
501
+ }
502
+ /**
503
+ * Every reference-module source whose GENERATION is conditional — the sources
504
+ * {@link resolveVariantModules} may or may not include.
505
+ *
506
+ * The build reads this to tell the two reasons a module is absent from the
507
+ * resolved registry apart: an UNREGISTERED reference module is an authoring
508
+ * mistake and is refused with a message naming the registry, while one listed
509
+ * here is authored-but-gated and is reported as deferred. Without the
510
+ * distinction the gated case would take the refusal path and no gated module
511
+ * could ever exist.
512
+ *
513
+ * Derived from {@link GATED_REFERENCE_MODULES} — the same table
514
+ * {@link resolveVariantModules} loops over — rather than hand-listed beside it: a
515
+ * second module added to that table is on this roster by construction, and there
516
+ * is no second place to remember.
517
+ */
518
+ export const GATED_REFERENCE_MODULE_SOURCES = GATED_REFERENCE_MODULES.map(gated => gated.module.source);
519
+ /**
520
+ * The gated reference modules this registry does NOT generate — the build's
521
+ * "deferred" bucket, as a derived set.
522
+ *
523
+ * ONE authority for a question two callers ask. `scripts/build-mds.ts` asks it
524
+ * per walked file to decide whether to defer or compile; the packaging and
525
+ * printed-count guards ask it for the whole registry to know what the build must
526
+ * have reported. Both spelled the predicate inline while there was exactly one
527
+ * gated module and exactly one answer, which is how a roster and the code that
528
+ * produces it come to disagree the first time the answer changes.
529
+ *
530
+ * With a tool-call provider registered the set is EMPTY, and that is the honest
531
+ * reading rather than a missing roster: the one gated module has a consumer, so
532
+ * nothing is held back. The guards therefore assert the build printed zero
533
+ * deferred modules, and prove the predicate still has teeth by asking it about a
534
+ * registry with every such provider removed.
535
+ *
536
+ * @param modules - Registry to measure (defaults to VARIANT_MODULES). Injectable
537
+ * so the non-empty arm is provable without unregistering a shipped provider.
538
+ */
539
+ export function deferredReferenceModuleSources(modules = VARIANT_MODULES) {
540
+ const active = new Set(resolveVariantModules(modules).map(mod => mod.source));
541
+ return GATED_REFERENCE_MODULE_SOURCES.filter(source => !active.has(source));
542
+ }
543
+ /**
544
+ * The floor a FAN-OUT module's pair list must clear.
545
+ *
546
+ * 8 is not a tuning knob: below it the "every op has a file and every file has
547
+ * an op" parity assertions stop discriminating, because a list short enough to
548
+ * be enumerated by hand is satisfied by any implementation that returns
549
+ * something (GAP-42). Raising it is allowed; lowering it is the exact evasion
550
+ * §14.5's no-threshold-lowered rule exists to prevent.
551
+ *
552
+ * It applies per module, and only to `kind: 'fanout'` modules — see
553
+ * VariantModuleKind for why a count proves nothing about a named document set.
554
+ */
555
+ export const MIN_VARIANT_PAIRS = 8;
556
+ /**
557
+ * Expand reference modules into the flat `(module, op)` pair list the build
558
+ * writes.
559
+ *
560
+ * Pure and total: every refusal is a Result, so the build shell keeps its single
561
+ * exit (avoids PF-014). The expansion is deliberately flat rather than nested —
562
+ * one list of destinations is what the plan pass needs to detect two hosts
563
+ * claiming one file, and a nested shape would have to be flattened there anyway.
564
+ *
565
+ * Every segment of every emitted path goes through validateOutputName, so the
566
+ * destination cannot be escaped by a subdir or an op name, only by editing the
567
+ * registry above.
568
+ *
569
+ * @param modules - Registry to expand (defaults to VARIANT_MODULES). Injectable
570
+ * so the refusal branches are provable without inventing a module on disk.
571
+ */
572
+ export function expandVariants(modules = resolveVariantModules()) {
573
+ if (modules.length === 0)
574
+ return Err({ kind: 'no-modules' });
575
+ const pairs = [];
576
+ const claimedBy = new Map();
577
+ for (const mod of modules) {
578
+ if (mod.ops.length === 0)
579
+ return Err({ kind: 'empty-module', module: mod.source });
580
+ // `''` means "land in the destination directory itself" — there is no segment
581
+ // to validate, and splitting it would produce one empty segment that every
582
+ // name rule rejects. Any other value is validated segment by segment.
583
+ if (mod.subdir !== '') {
584
+ for (const segment of mod.subdir.split('/')) {
585
+ if (!validateOutputName(segment).ok) {
586
+ return Err({
587
+ kind: 'invalid-subdir-segment',
588
+ module: mod.source,
589
+ subdir: mod.subdir,
590
+ segment,
591
+ });
592
+ }
593
+ }
594
+ }
595
+ if (mod.kind === 'fanout' && mod.ops.length < MIN_VARIANT_PAIRS) {
596
+ // `module` like every sibling arm: the floor is PER MODULE, so a bare count
597
+ // leaves a reader of the refusal with no way to tell which registry entry is
598
+ // short — the omission typescript-02 names.
599
+ return Err({
600
+ kind: 'too-few-pairs',
601
+ module: mod.source,
602
+ count: mod.ops.length,
603
+ minimum: MIN_VARIANT_PAIRS,
604
+ });
605
+ }
606
+ for (const op of mod.ops) {
607
+ // A 'contract' module's basename carries a mandatory leading underscore;
608
+ // every other kind's is refused one. Dispatching on the kind keeps ONE name
609
+ // rule per kind, rather than one relaxed rule that both kinds share and
610
+ // neither is fully described by.
611
+ const nameResult = mod.kind === 'contract'
612
+ ? validateContractOutputName(op)
613
+ : validateOutputName(op);
614
+ if (!nameResult.ok) {
615
+ return Err({ kind: 'invalid-op-name', module: mod.source, op, cause: nameResult.error });
616
+ }
617
+ const relPath = mod.subdir === '' ? `${op}.md` : `${mod.subdir}/${op}.md`;
618
+ const claimants = claimedBy.get(relPath);
619
+ if (claimants === undefined) {
620
+ claimedBy.set(relPath, [mod.source]);
621
+ }
622
+ else {
623
+ claimants.push(mod.source);
624
+ return Err({ kind: 'duplicate-output', relPath, modules: [...claimants] });
625
+ }
626
+ pairs.push({ module: mod.source, op, relPath });
627
+ }
628
+ }
629
+ return Ok(pairs);
630
+ }
631
+ /**
632
+ * Every reference file the build generates, as POSIX paths relative to
633
+ * {@link SKILL_REFS_OUTPUT_DIR} — the manifest an installer converges to.
634
+ *
635
+ * Derived from the resolved registry above (VARIANT_MODULES plus the gated
636
+ * contract module, carrying TRACKER_OPS once per provider and
637
+ * GIT_CROSS_CUTTING_DOCS) through the same expandVariants the build plan uses.
638
+ * Hand-listing the operations here would create a second
639
+ * roster that drifts silently the moment one is added — the bidirectional-registry
640
+ * rule compliance-compose.ts states for its token tables.
641
+ *
642
+ * Lives beside the registry it reads rather than in the Claude Code installer that
643
+ * consumes it: nothing about the answer is Claude-Code-specific, and the packaging
644
+ * and containment tests that read it are asking the BUILD what it emits, not
645
+ * asking an install target (applies ADR-013).
646
+ *
647
+ * Asserts where its siblings return a Result. The registry is a compile-time
648
+ * constant, so a refusal is a programming error rather than an install-time
649
+ * degradation: no caller could sensibly continue, and every caller would otherwise
650
+ * carry the same impossible branch. The full refusal is rendered and not just its
651
+ * `kind` — the payload is what names the offending module and op, and a payload
652
+ * nothing reads is a payload nothing maintains (avoids PF-041). Same rendering the
653
+ * build's own refusal sinks use (scripts/build-mds.ts).
654
+ */
655
+ export function generatedReferenceManifest() {
656
+ return expandedManifest(resolveVariantModules());
657
+ }
658
+ /**
659
+ * The references ONE install carries — the manifest the overlay converges to.
660
+ *
661
+ * D-INSTALL-ALL-PROVIDERS: an install carries exactly what the build emits
662
+ * ({@link generatedReferenceManifest}): every provider's tree, the PR-host tree,
663
+ * the cross-cutting documents and `tracker/_mcp.md`, whatever the machine selected.
664
+ * The provider is no longer a property of the install. A repository selects its
665
+ * own tracker in its committed `.devflow/project.json`, so one machine meets more
666
+ * than one provider and a Git spawn must find the mechanics of whichever one the
667
+ * repository resolves — an install scoped to the machine's selection would leave
668
+ * every such repository degraded until someone re-ran init for a provider that is
669
+ * not theirs. The trees are INERT until a spawn resolves a provider and names
670
+ * one of its files, so carrying all of them costs disk, never context.
671
+ *
672
+ * The overlay still PRUNES everything under its converged subtrees this manifest
673
+ * does not name, so a retired generated document leaves on the next install.
674
+ *
675
+ * Asserts rather than degrades on a registry that does not expand, exactly as
676
+ * its sibling does (design review M3): the registry is a compile-time constant,
677
+ * so a refusal is a programming error rather than an install-time degradation.
678
+ *
679
+ * @param opts.modules - Registry to expand (defaults to the shipped one, with the
680
+ * gated contract module resolved). Injectable so the refusal arm is provable
681
+ * without editing the registry.
682
+ */
683
+ export function installedReferenceManifest(opts = {}) {
684
+ return expandedManifest(opts.modules ?? resolveVariantModules());
685
+ }
686
+ /** Every relPath a registry expands to, or the assertion both manifests share. */
687
+ function expandedManifest(modules) {
688
+ const expanded = expandVariants(modules);
689
+ if (!expanded.ok) {
690
+ throw new Error(`Reference module registry does not expand — ${JSON.stringify(expanded.error)}. ` +
691
+ `VARIANT_MODULES in src/core/mds-variants.ts is invalid.`);
692
+ }
693
+ return expanded.value.map(pair => pair.relPath);
694
+ }
695
+ // ---------------------------------------------------------------------------
696
+ // Section splitting — which slice of a module's compiled body belongs to which op
697
+ // ---------------------------------------------------------------------------
698
+ /**
699
+ * The delimiter a reference module writes before each operation's section.
700
+ *
701
+ * An HTML comment rather than a heading: the splitter CONSUMES these lines, so
702
+ * the emitted reference starts with its own content and carries no build
703
+ * plumbing. A heading would have to survive into the file and would then be
704
+ * load-bearing for two unrelated readers at once.
705
+ *
706
+ * No `g`/`y` flag on the shared object — callers construct their own scanner
707
+ * rather than inherit a lastIndex (the same rule LEADING_BLOCK_RE follows in
708
+ * scripts/build-mds.ts).
709
+ *
710
+ * The optional leading `_` mirrors validateContractOutputName, and widening the
711
+ * capture here costs nothing: this regex is NOT a containment gate. It recognises
712
+ * a plumbing comment inside a source file, and the name it captures is then
713
+ * checked against the caller's own registry (`unknown-section`), so a marker
714
+ * naming something unregistered is refused whatever its spelling. The gate on
715
+ * what may become a PATH is validateOutputName / validateContractOutputName,
716
+ * which run over the registry, not over the file.
717
+ */
718
+ export const VARIANT_SECTION_MARKER_RE = /^<!-- op: (_?[a-z0-9][a-z0-9._-]{0,63}) -->[ \t]*$/;
719
+ /**
720
+ * Split a reference module's compiled body into one document per operation.
721
+ *
722
+ * Bidirectional, and both directions are load-bearing:
723
+ * - unknown-section — the body carries a section for an op the registry does
724
+ * not name, so a file would ship that nothing loads (ADR-003);
725
+ * - missing-section — the registry names an op the body does not cover, so the
726
+ * preamble's load instruction resolves to nothing at runtime.
727
+ * A forward-only check passes on either half of that pair.
728
+ *
729
+ * empty-section is the third arm, and it exists because the other two cannot see
730
+ * it: an op with a marker and no body compiles cleanly and emits a zero-byte
731
+ * reference, which the agent then loads as an operation with no instructions,
732
+ * with no build signal at all (the GAP-44 shape — omission is caught, emptiness
733
+ * is not).
734
+ *
735
+ * Total on success, and immutable: the caller gets back a readonly array of its
736
+ * OWN records, in its own order, each carrying its section. Nothing is looked up
737
+ * afterwards, so no consumer can be handed `undefined` for an operation the
738
+ * registry declared, and no consumer holds a handle it could write through.
739
+ *
740
+ * @param body - The module's compiled output, steering block already stripped.
741
+ * @param entries - The caller's records, one per operation the registry says this
742
+ * module emits, each naming its operation in `op`. Taking the caller's records
743
+ * rather than a bare op list is what lets the result carry each operation's
744
+ * destination back to it structurally, with no index correspondence to trust.
745
+ */
746
+ export function splitVariantSections(body, entries) {
747
+ const ops = entries.map(entry => entry.op);
748
+ const lines = body.split('\n');
749
+ const sections = new Map();
750
+ const expected = new Set(ops);
751
+ let current = null;
752
+ for (const line of lines) {
753
+ const match = VARIANT_SECTION_MARKER_RE.exec(line);
754
+ if (match !== null) {
755
+ const op = match[1];
756
+ if (!expected.has(op))
757
+ return Err({ kind: 'unknown-section', op, expected: ops });
758
+ if (sections.has(op))
759
+ return Err({ kind: 'duplicate-section', op });
760
+ // The buffer itself is what the scan carries forward, not the op name it is
761
+ // filed under, so appending a line is never a second partial lookup.
762
+ current = [];
763
+ sections.set(op, current);
764
+ continue;
765
+ }
766
+ // Text before the first marker is module-level preamble and is dropped: it
767
+ // belongs to no operation, so shipping it would duplicate it into every file.
768
+ if (current === null)
769
+ continue;
770
+ current.push(line);
771
+ }
772
+ if (sections.size === 0)
773
+ return Err({ kind: 'no-sections', expected: ops });
774
+ // Pair every entry with its collected section, recording the entries the body
775
+ // never covered. Parity is decided in full before any content is judged, so a
776
+ // body that is both short and empty-in-places still reports missing-section —
777
+ // the omission, which is the larger fact.
778
+ const paired = [];
779
+ const missing = [];
780
+ for (const entry of entries) {
781
+ const collected = sections.get(entry.op);
782
+ if (collected === undefined)
783
+ missing.push(entry.op);
784
+ else
785
+ paired.push({ entry, collected });
786
+ }
787
+ if (missing.length > 0)
788
+ return Err({ kind: 'missing-section', ops: missing });
789
+ const out = [];
790
+ for (const { entry, collected } of paired) {
791
+ const trimmed = collected.join('\n').trim();
792
+ if (trimmed.length === 0)
793
+ return Err({ kind: 'empty-section', op: entry.op });
794
+ out.push({ ...entry, content: `${trimmed}\n` });
795
+ }
796
+ return Ok(out);
797
+ }
798
+ //# sourceMappingURL=mds-variants.js.map