devflow-kit 2.4.0 → 2.5.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 (166) hide show
  1. package/CHANGELOG.md +156 -0
  2. package/README.md +86 -18
  3. package/dist/agents/git.md +824 -0
  4. package/dist/cli/commands/agents.js +6 -1
  5. package/dist/cli/commands/attribution-prompts.js +1 -1
  6. package/dist/cli/commands/compliance-prompts.js +1 -1
  7. package/dist/cli/commands/compliance.js +23 -1
  8. package/dist/cli/commands/init-seed.js +24 -26
  9. package/dist/cli/commands/init.js +502 -71
  10. package/dist/cli/commands/install-report.js +205 -0
  11. package/dist/cli/commands/knowledge/index.js +2 -2
  12. package/dist/cli/commands/knowledge/toggle.js +27 -37
  13. package/dist/cli/commands/learning.js +37 -30
  14. package/dist/cli/commands/memory.js +79 -69
  15. package/dist/cli/commands/prompt-io.js +4 -4
  16. package/dist/cli/commands/security.js +76 -16
  17. package/dist/cli/commands/skills.js +53 -7
  18. package/dist/cli/commands/tracker-prompts.js +145 -0
  19. package/dist/cli/commands/tracker.js +405 -0
  20. package/dist/cli/commands/uninstall.js +211 -65
  21. package/dist/cli.js +2 -0
  22. package/dist/commands/bug-analysis.md +22 -4
  23. package/dist/commands/code-review.md +44 -15
  24. package/dist/commands/debug.md +20 -6
  25. package/dist/commands/dynamic-build.md +289 -67
  26. package/dist/commands/dynamic-plan.md +60 -21
  27. package/dist/commands/dynamic-profile.md +1 -1
  28. package/dist/commands/dynamic-tickets.md +58 -8
  29. package/dist/commands/explore.md +2 -2
  30. package/dist/commands/implement.md +241 -53
  31. package/dist/commands/plan.md +88 -17
  32. package/dist/commands/release.md +64 -17
  33. package/dist/commands/resolve.md +138 -58
  34. package/dist/commands/self-review.md +2 -2
  35. package/dist/core/agent-models.js +55 -12
  36. package/dist/core/assets.js +58 -2
  37. package/dist/core/evidence-policy.js +147 -0
  38. package/dist/core/feature-config.js +130 -64
  39. package/dist/core/feature-switch.js +112 -0
  40. package/dist/core/flags.js +4 -4
  41. package/dist/core/manifest.js +33 -7
  42. package/dist/core/mds-variants.js +861 -0
  43. package/dist/core/model-discovery.js +12 -1
  44. package/dist/core/plugins.js +357 -9
  45. package/dist/core/project-paths.js +1 -1
  46. package/dist/core/proxy-log.js +8 -6
  47. package/dist/core/proxy-state.js +11 -8
  48. package/dist/core/reference-sweep.js +136 -0
  49. package/dist/core/tracker.js +407 -0
  50. package/dist/skills/git/references/decision-markers.md +19 -0
  51. package/dist/skills/git/references/learn-conventions.md +56 -0
  52. package/dist/skills/git/references/pr/check-ci-status.md +14 -0
  53. package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
  54. package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
  55. package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
  56. package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
  57. package/dist/skills/git/references/pr/post-review-summary.md +42 -0
  58. package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
  59. package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
  60. package/dist/skills/git/references/pr/validate-branch.md +18 -0
  61. package/dist/skills/git/references/publication-gate.md +13 -0
  62. package/dist/skills/git/references/tracker/_mcp.md +153 -0
  63. package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
  64. package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
  65. package/dist/skills/git/references/tracker/github/create-release.md +11 -0
  66. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
  67. package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
  68. package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
  69. package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
  70. package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
  71. package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
  72. package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
  73. package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
  74. package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
  75. package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
  76. package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
  77. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
  78. package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
  79. package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
  80. package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
  81. package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
  82. package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
  83. package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
  84. package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
  85. package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
  86. package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
  87. package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
  88. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
  89. package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
  90. package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
  91. package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
  92. package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
  93. package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
  94. package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
  95. package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
  96. package/dist/skills/git/references/trust-rule.md +7 -0
  97. package/dist/targets/claude-code/installer.js +1213 -31
  98. package/dist/targets/claude-code/legacy.js +5 -0
  99. package/dist/targets/claude-code/post-install.js +196 -74
  100. package/dist/targets/claude-code/tracker-install.js +161 -0
  101. package/package.json +4 -3
  102. package/src/assets/agents/code.md +42 -4
  103. package/src/assets/agents/design.md +1 -1
  104. package/src/assets/agents/git.mds +827 -0
  105. package/src/assets/agents/knowledge.md +1 -1
  106. package/src/assets/agents/learning.md +11 -0
  107. package/src/assets/agents/synthesize.md +1 -1
  108. package/src/assets/agents/test.md +16 -5
  109. package/src/assets/agents/tracker.md +467 -0
  110. package/src/assets/agents/validate.md +7 -5
  111. package/src/assets/commands/_partials/_engine.mds +11 -9
  112. package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
  113. package/src/assets/commands/_partials/_knowledge.mds +2 -2
  114. package/src/assets/commands/_partials/_plan_contract.mds +22 -7
  115. package/src/assets/commands/_partials/_preamble.mds +1 -1
  116. package/src/assets/commands/_partials/_publication.mds +3 -1
  117. package/src/assets/commands/_partials/_ticket_template.mds +3 -2
  118. package/src/assets/commands/_partials/_tracker.mds +18 -0
  119. package/src/assets/commands/_partials/_wave.mds +16 -10
  120. package/src/assets/commands/bug-analysis.mds +15 -5
  121. package/src/assets/commands/code-review.mds +34 -14
  122. package/src/assets/commands/debug.mds +11 -4
  123. package/src/assets/commands/dynamic-build.mds +227 -41
  124. package/src/assets/commands/dynamic-plan.mds +35 -13
  125. package/src/assets/commands/dynamic-tickets.mds +47 -5
  126. package/src/assets/commands/implement.mds +206 -52
  127. package/src/assets/commands/plan.mds +70 -17
  128. package/src/assets/commands/release.md +64 -17
  129. package/src/assets/commands/resolve.mds +126 -56
  130. package/src/assets/mds/git/_pr.mds +331 -0
  131. package/src/assets/mds/git/_references.mds +135 -0
  132. package/src/assets/mds/tracker/_common.mds +156 -0
  133. package/src/assets/mds/tracker/_github.mds +472 -0
  134. package/src/assets/mds/tracker/_jira.mds +407 -0
  135. package/src/assets/mds/tracker/_linear.mds +449 -0
  136. package/src/assets/mds/tracker/_mcp.mds +299 -0
  137. package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
  138. package/src/assets/scripts/hooks/background-memory-update +14 -9
  139. package/src/assets/scripts/hooks/capture-prompt +6 -2
  140. package/src/assets/scripts/hooks/capture-question +6 -2
  141. package/src/assets/scripts/hooks/capture-turn +6 -2
  142. package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
  143. package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
  144. package/src/assets/scripts/hooks/hook-log-init +3 -1
  145. package/src/assets/scripts/hooks/json-helper.cjs +223 -5
  146. package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
  147. package/src/assets/scripts/hooks/memory-worker +15 -8
  148. package/src/assets/scripts/hooks/pre-compact-memory +12 -8
  149. package/src/assets/scripts/hooks/preamble +1 -4
  150. package/src/assets/scripts/hooks/queue-append +68 -24
  151. package/src/assets/scripts/hooks/session-start-context +355 -8
  152. package/src/assets/scripts/hooks/session-start-memory +12 -8
  153. package/src/assets/scripts/pr-evidence.cjs +1961 -0
  154. package/src/assets/scripts/redact-secrets.cjs +490 -62
  155. package/src/assets/scripts/release-trace.cjs +1143 -0
  156. package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
  157. package/src/assets/scripts/verify-evidence.cjs +1822 -0
  158. package/src/assets/skills/compliance/SKILL.md +2 -0
  159. package/src/assets/skills/docs-framework/SKILL.md +5 -3
  160. package/src/assets/skills/git/SKILL.md +8 -78
  161. package/src/assets/skills/git/references/github-api.md +179 -141
  162. package/src/assets/skills/git/references/patterns.md +11 -6
  163. package/src/assets/skills/review-methodology/SKILL.md +1 -1
  164. package/src/assets/skills/review-methodology/references/patterns.md +6 -61
  165. package/src/assets/skills/review-methodology/references/violations.md +14 -22
  166. package/src/assets/agents/git.md +0 -938
@@ -0,0 +1,861 @@
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
+ * Not to be confused with {@link PR_HOST_TRACKER_SUBDIR} (`tracker/github`),
310
+ * which is the TRACKER directory every install carries because PR hosting is on
311
+ * GitHub. This one is the provider-independent `pr/` directory itself — it is
312
+ * under no provider, and every install carries it for the same reason: a jira or
313
+ * linear user still opens pull requests.
314
+ */
315
+ export const PR_HOST_DESTINATION_ROOT = 'pr';
316
+ /**
317
+ * The cross-cutting `devflow:git` reference documents — provider-independent, so
318
+ * they land at the root of the references directory rather than under
319
+ * `tracker/{provider}/`.
320
+ *
321
+ * `decision-markers` holds the D1–D3 / D5–D10 rows of the agent's Decision Marker
322
+ * Legend. The D4 and D11 rows are the ONLY definitions of labels whose controls
323
+ * are always-loaded, so they stay inline in the agent (E10 / AC-2.13); the rest
324
+ * are glossary entries a reader consults, not rules a spawn must have.
325
+ *
326
+ * `learn-conventions` holds that operation's bounded scan and its untrusted-string
327
+ * discipline. It is GENERATED rather than hand-authored on purpose [DR-15]: the
328
+ * Phase-3 Tracker agent NAMES this file instead of copying the block, so the
329
+ * bounded-scan literals and the post-composition verbatim-match check never exist
330
+ * in a second, independently maintained copy outside the single-authority corpus.
331
+ *
332
+ * `publication-gate` holds the D10 step order. It is named from the two summary
333
+ * operations and from nowhere else, which is the scope property [DR-20] asserts:
334
+ * an operation that can load the gate is an operation that probes repo visibility.
335
+ *
336
+ * `trust-rule` is the ONE prose statement of who counts as a trusted author of a
337
+ * PR comment, review thread or review (#363). `pr-evidence.cjs`'s `trust()` is its
338
+ * one implementation, and a parity test holds the two to the same terms. It is the
339
+ * only document here named from a PR-HOST reference rather than from the agent:
340
+ * `references/pr/fetch-review-threads.md` applies it and names it, while an op that
341
+ * runs the evidence scripts gets the rule from `trust()` and never loads the
342
+ * document — naming it from the agent would bill every spawn for a rule one
343
+ * operation reads.
344
+ */
345
+ export const GIT_CROSS_CUTTING_DOCS = [
346
+ 'decision-markers',
347
+ 'learn-conventions',
348
+ 'publication-gate',
349
+ 'trust-rule',
350
+ ];
351
+ /**
352
+ * Every reference module the build knows about — a closed registry, read the
353
+ * same way ALLOWED_OUTPUT_DIRS is read.
354
+ *
355
+ * A `skill-refs` host whose source path is absent from this table is refused by
356
+ * the build rather than guessed at: the emitted filenames come from the op list,
357
+ * not from the module's own basename, so there is nothing to fall back to.
358
+ *
359
+ * Every provider row reads the ONE shared {@link TRACKER_OPS} roster, so the three
360
+ * providers below emit the same file set by construction — file-set parity is a
361
+ * compile-time property rather than an assertion two hand-listed arrays have to
362
+ * keep agreeing on.
363
+ *
364
+ * Registering a provider whose `subdir` is one of MCP_BACKED_PROVIDER_SUBDIRS is
365
+ * also what opens the generation gate on the tool-call contract; see
366
+ * {@link mcpContractIsGenerated}. There is no second edit and no flag.
367
+ */
368
+ export const VARIANT_MODULES = [
369
+ {
370
+ source: 'src/assets/mds/tracker/_github.mds',
371
+ subdir: 'tracker/github',
372
+ kind: 'fanout',
373
+ ops: TRACKER_GITHUB_OPS,
374
+ },
375
+ {
376
+ source: 'src/assets/mds/tracker/_jira.mds',
377
+ subdir: 'tracker/jira',
378
+ kind: 'fanout',
379
+ ops: TRACKER_OPS,
380
+ },
381
+ {
382
+ source: 'src/assets/mds/tracker/_linear.mds',
383
+ subdir: 'tracker/linear',
384
+ kind: 'fanout',
385
+ ops: TRACKER_OPS,
386
+ },
387
+ {
388
+ source: 'src/assets/mds/git/_pr.mds',
389
+ subdir: PR_HOST_DESTINATION_ROOT,
390
+ kind: 'fanout',
391
+ ops: PR_HOST_OPS,
392
+ },
393
+ {
394
+ source: 'src/assets/mds/git/_references.mds',
395
+ subdir: '',
396
+ kind: 'named',
397
+ ops: GIT_CROSS_CUTTING_DOCS,
398
+ },
399
+ ];
400
+ // ---------------------------------------------------------------------------
401
+ // The tool-call contract module, and the gate on its generation
402
+ // (hazard H7, conflict C5)
403
+ // ---------------------------------------------------------------------------
404
+ /**
405
+ * The tracker provider destinations whose mechanics reach the tracker through a
406
+ * TOOL CALL rather than through a CLI — the condition the contract document's
407
+ * generation is keyed on.
408
+ *
409
+ * `tracker/github` is deliberately absent: GitHub's mechanics are `gh` commands,
410
+ * and a gate keyed on "any tracker module is registered" would already be open.
411
+ *
412
+ * Spelled as DESTINATIONS rather than provider names so the gate is a fact about
413
+ * the registry: a provider module is registered with the subdir its files land
414
+ * in, so opening the gate and shipping the provider are the same edit. A boolean
415
+ * field on VariantModule would have been a flag someone has to remember to flip,
416
+ * which is the same class of defect as a floor nobody raises.
417
+ */
418
+ export const MCP_BACKED_PROVIDER_SUBDIRS = ['tracker/jira', 'tracker/linear'];
419
+ /** The destination directory every tracker provider module lands under. */
420
+ export const TRACKER_DESTINATION_ROOT = 'tracker';
421
+ /**
422
+ * The tracker destination every install carries, whatever the user selected.
423
+ *
424
+ * Not a default and not a fallback: PR hosting stays on GitHub under every
425
+ * issue-tracker provider, so a jira or linear user still runs `gh pr` mechanics
426
+ * and still needs the GitHub tree reachable. It is the FLOOR of
427
+ * {@link installedReferenceManifest}'s union.
428
+ *
429
+ * Stated rather than derived, because the fact is about where pull requests
430
+ * live, not about anything the registry knows. A derivation from "the one
431
+ * CLI-backed module" would read as a rule and silently promote the next
432
+ * CLI-backed provider into everyone's install.
433
+ */
434
+ export const PR_HOST_TRACKER_SUBDIR = `${TRACKER_DESTINATION_ROOT}/github`;
435
+ /**
436
+ * The provider-independent tool-call contract document.
437
+ *
438
+ * GENERATED only while {@link mcpContractIsGenerated} is true — that is, only
439
+ * while a provider that reaches its tracker through a tool call is registered.
440
+ * The gate is not a phase marker; it is the answer to "does anyone load this?",
441
+ * and it stays answerable in both directions:
442
+ *
443
+ * - Open, as it is whenever a tool-call provider is registered: every such
444
+ * provider's per-operation mechanics NAME this document, so it must exist or
445
+ * those references point at a file the install does not carry.
446
+ * - Shut, as it is for a registry with GitHub alone: no reachable consumer
447
+ * exists, and generating it anyway would bill every GitHub user for a
448
+ * reference nothing they can reach ever loads (GAP-02). The byte-budget
449
+ * formula carries it as a term that is 0 on the GitHub path for exactly that
450
+ * reason, and the re-scoped AC-2.7 arm proves no github op file names it.
451
+ *
452
+ * It lands at the `tracker/` ROOT rather than inside a provider directory: it is
453
+ * provider-independent, and a copy per provider is the duplication it exists to
454
+ * remove. The `_` prefix is what distinguishes it from the provider directories
455
+ * beside it (validateContractOutputName).
456
+ */
457
+ export const MCP_CONTRACT_MODULE = {
458
+ source: 'src/assets/mds/tracker/_mcp.mds',
459
+ subdir: 'tracker',
460
+ kind: 'contract',
461
+ ops: ['_mcp'],
462
+ };
463
+ /**
464
+ * Does this registry contain a provider that needs the tool-call contract?
465
+ *
466
+ * The whole gate, in one derived predicate: registering a provider module in an
467
+ * MCP-backed sub-directory is what starts the contract being generated, with no
468
+ * second edit anywhere and no declaration to keep in step.
469
+ */
470
+ export function mcpContractIsGenerated(modules = VARIANT_MODULES) {
471
+ const gated = MCP_BACKED_PROVIDER_SUBDIRS;
472
+ return modules.some(mod => gated.includes(mod.subdir));
473
+ }
474
+ /**
475
+ * Every reference module whose GENERATION is conditional, each beside the
476
+ * predicate that answers for it.
477
+ *
478
+ * ONE table, read by both halves of the mechanism: {@link resolveVariantModules}
479
+ * appends the modules whose predicate says yes, and
480
+ * {@link GATED_REFERENCE_MODULE_SOURCES} is this table's source column. A module
481
+ * added here therefore reaches the resolver and the gated roster in the same
482
+ * edit. Naming the module inline in the resolver and again in the roster is how
483
+ * a roster and the code that produces it come to disagree the first time a
484
+ * second one is added — the same defect {@link deferredReferenceModuleSources}
485
+ * exists to keep out of its two callers.
486
+ */
487
+ export const GATED_REFERENCE_MODULES = [
488
+ { module: MCP_CONTRACT_MODULE, isGenerated: mcpContractIsGenerated },
489
+ ];
490
+ /**
491
+ * The registry the build actually expands: {@link VARIANT_MODULES} plus every
492
+ * gated module whose own gate is open.
493
+ *
494
+ * Idempotent — resolving an already-resolved list appends nothing. Without that,
495
+ * a caller that resolved twice would hand expandVariants two rows for one source
496
+ * and get a `duplicate-output` refusal describing a bug it could not locate.
497
+ *
498
+ * Each predicate is asked about the registry AS PASSED, never about the list the
499
+ * loop is building, so a gate can never be opened by a module an earlier gate
500
+ * appended.
501
+ *
502
+ * @param modules - Registry to resolve (defaults to VARIANT_MODULES). Injectable
503
+ * so both sides of every gate are provable against a registry that never has to
504
+ * exist on disk.
505
+ */
506
+ export function resolveVariantModules(modules = VARIANT_MODULES) {
507
+ let resolved = modules;
508
+ for (const gated of GATED_REFERENCE_MODULES) {
509
+ if (!gated.isGenerated(modules))
510
+ continue;
511
+ if (resolved.some(mod => mod.source === gated.module.source))
512
+ continue;
513
+ resolved = [...resolved, gated.module];
514
+ }
515
+ return resolved;
516
+ }
517
+ /**
518
+ * Every reference-module source whose GENERATION is conditional — the sources
519
+ * {@link resolveVariantModules} may or may not include.
520
+ *
521
+ * The build reads this to tell the two reasons a module is absent from the
522
+ * resolved registry apart: an UNREGISTERED reference module is an authoring
523
+ * mistake and is refused with a message naming the registry, while one listed
524
+ * here is authored-but-gated and is reported as deferred. Without the
525
+ * distinction the gated case would take the refusal path and no gated module
526
+ * could ever exist.
527
+ *
528
+ * Derived from {@link GATED_REFERENCE_MODULES} — the same table
529
+ * {@link resolveVariantModules} loops over — rather than hand-listed beside it: a
530
+ * second module added to that table is on this roster by construction, and there
531
+ * is no second place to remember.
532
+ */
533
+ export const GATED_REFERENCE_MODULE_SOURCES = GATED_REFERENCE_MODULES.map(gated => gated.module.source);
534
+ /**
535
+ * The gated reference modules this registry does NOT generate — the build's
536
+ * "deferred" bucket, as a derived set.
537
+ *
538
+ * ONE authority for a question two callers ask. `scripts/build-mds.ts` asks it
539
+ * per walked file to decide whether to defer or compile; the packaging and
540
+ * printed-count guards ask it for the whole registry to know what the build must
541
+ * have reported. Both spelled the predicate inline while there was exactly one
542
+ * gated module and exactly one answer, which is how a roster and the code that
543
+ * produces it come to disagree the first time the answer changes.
544
+ *
545
+ * With a tool-call provider registered the set is EMPTY, and that is the honest
546
+ * reading rather than a missing roster: the one gated module has a consumer, so
547
+ * nothing is held back. The guards therefore assert the build printed zero
548
+ * deferred modules, and prove the predicate still has teeth by asking it about a
549
+ * registry with every such provider removed.
550
+ *
551
+ * @param modules - Registry to measure (defaults to VARIANT_MODULES). Injectable
552
+ * so the non-empty arm is provable without unregistering a shipped provider.
553
+ */
554
+ export function deferredReferenceModuleSources(modules = VARIANT_MODULES) {
555
+ const active = new Set(resolveVariantModules(modules).map(mod => mod.source));
556
+ return GATED_REFERENCE_MODULE_SOURCES.filter(source => !active.has(source));
557
+ }
558
+ /**
559
+ * The floor a FAN-OUT module's pair list must clear.
560
+ *
561
+ * 8 is not a tuning knob: below it the "every op has a file and every file has
562
+ * an op" parity assertions stop discriminating, because a list short enough to
563
+ * be enumerated by hand is satisfied by any implementation that returns
564
+ * something (GAP-42). Raising it is allowed; lowering it is the exact evasion
565
+ * §14.5's no-threshold-lowered rule exists to prevent.
566
+ *
567
+ * It applies per module, and only to `kind: 'fanout'` modules — see
568
+ * VariantModuleKind for why a count proves nothing about a named document set.
569
+ */
570
+ export const MIN_VARIANT_PAIRS = 8;
571
+ /**
572
+ * Expand reference modules into the flat `(module, op)` pair list the build
573
+ * writes.
574
+ *
575
+ * Pure and total: every refusal is a Result, so the build shell keeps its single
576
+ * exit (avoids PF-014). The expansion is deliberately flat rather than nested —
577
+ * one list of destinations is what the plan pass needs to detect two hosts
578
+ * claiming one file, and a nested shape would have to be flattened there anyway.
579
+ *
580
+ * Every segment of every emitted path goes through validateOutputName, so the
581
+ * destination cannot be escaped by a subdir or an op name, only by editing the
582
+ * registry above.
583
+ *
584
+ * @param modules - Registry to expand (defaults to VARIANT_MODULES). Injectable
585
+ * so the refusal branches are provable without inventing a module on disk.
586
+ */
587
+ export function expandVariants(modules = resolveVariantModules()) {
588
+ if (modules.length === 0)
589
+ return Err({ kind: 'no-modules' });
590
+ const pairs = [];
591
+ const claimedBy = new Map();
592
+ for (const mod of modules) {
593
+ if (mod.ops.length === 0)
594
+ return Err({ kind: 'empty-module', module: mod.source });
595
+ // `''` means "land in the destination directory itself" — there is no segment
596
+ // to validate, and splitting it would produce one empty segment that every
597
+ // name rule rejects. Any other value is validated segment by segment.
598
+ if (mod.subdir !== '') {
599
+ for (const segment of mod.subdir.split('/')) {
600
+ if (!validateOutputName(segment).ok) {
601
+ return Err({
602
+ kind: 'invalid-subdir-segment',
603
+ module: mod.source,
604
+ subdir: mod.subdir,
605
+ segment,
606
+ });
607
+ }
608
+ }
609
+ }
610
+ if (mod.kind === 'fanout' && mod.ops.length < MIN_VARIANT_PAIRS) {
611
+ // `module` like every sibling arm: the floor is PER MODULE, so a bare count
612
+ // leaves a reader of the refusal with no way to tell which registry entry is
613
+ // short — the omission typescript-02 names.
614
+ return Err({
615
+ kind: 'too-few-pairs',
616
+ module: mod.source,
617
+ count: mod.ops.length,
618
+ minimum: MIN_VARIANT_PAIRS,
619
+ });
620
+ }
621
+ for (const op of mod.ops) {
622
+ // A 'contract' module's basename carries a mandatory leading underscore;
623
+ // every other kind's is refused one. Dispatching on the kind keeps ONE name
624
+ // rule per kind, rather than one relaxed rule that both kinds share and
625
+ // neither is fully described by.
626
+ const nameResult = mod.kind === 'contract'
627
+ ? validateContractOutputName(op)
628
+ : validateOutputName(op);
629
+ if (!nameResult.ok) {
630
+ return Err({ kind: 'invalid-op-name', module: mod.source, op, cause: nameResult.error });
631
+ }
632
+ const relPath = mod.subdir === '' ? `${op}.md` : `${mod.subdir}/${op}.md`;
633
+ const claimants = claimedBy.get(relPath);
634
+ if (claimants === undefined) {
635
+ claimedBy.set(relPath, [mod.source]);
636
+ }
637
+ else {
638
+ claimants.push(mod.source);
639
+ return Err({ kind: 'duplicate-output', relPath, modules: [...claimants] });
640
+ }
641
+ pairs.push({ module: mod.source, op, relPath });
642
+ }
643
+ }
644
+ return Ok(pairs);
645
+ }
646
+ /**
647
+ * Every reference file the build generates, as POSIX paths relative to
648
+ * {@link SKILL_REFS_OUTPUT_DIR} — the manifest an installer converges to.
649
+ *
650
+ * Derived from the resolved registry above (VARIANT_MODULES plus the gated
651
+ * contract module, carrying TRACKER_OPS once per provider and
652
+ * GIT_CROSS_CUTTING_DOCS) through the same expandVariants the build plan uses.
653
+ * Hand-listing the operations here would create a second
654
+ * roster that drifts silently the moment one is added — the bidirectional-registry
655
+ * rule compliance-compose.ts states for its token tables.
656
+ *
657
+ * Lives beside the registry it reads rather than in the Claude Code installer that
658
+ * consumes it: nothing about the answer is Claude-Code-specific, and the packaging
659
+ * and containment tests that read it are asking the BUILD what it emits, not
660
+ * asking an install target (applies ADR-013).
661
+ *
662
+ * Asserts where its siblings return a Result. The registry is a compile-time
663
+ * constant, so a refusal is a programming error rather than an install-time
664
+ * degradation: no caller could sensibly continue, and every caller would otherwise
665
+ * carry the same impossible branch. The full refusal is rendered and not just its
666
+ * `kind` — the payload is what names the offending module and op, and a payload
667
+ * nothing reads is a payload nothing maintains (avoids PF-041). Same rendering the
668
+ * build's own refusal sinks use (scripts/build-mds.ts).
669
+ */
670
+ export function generatedReferenceManifest() {
671
+ const expanded = expandVariants();
672
+ if (!expanded.ok) {
673
+ throw new Error(`Reference module registry does not expand — ${JSON.stringify(expanded.error)}. ` +
674
+ `VARIANT_MODULES in src/core/mds-variants.ts is invalid.`);
675
+ }
676
+ return expanded.value.map(pair => pair.relPath);
677
+ }
678
+ /**
679
+ * The references ONE install carries, for one resolved tracker provider — the
680
+ * narrower manifest the overlay converges to.
681
+ *
682
+ * D-INSTALL-SET: the BUILD emits every provider ({@link generatedReferenceManifest},
683
+ * 42 files) because the tarball must be able to serve any selection without a
684
+ * rebuild. An INSTALL carries `{github} ∪ {selected provider}`:
685
+ *
686
+ * - the GitHub tree is the FLOOR under every provider, not an optional extra.
687
+ * PR hosting stays on GitHub whatever the issue tracker is, so those
688
+ * mechanics stay reachable for a jira or linear user;
689
+ * - the cross-cutting documents (`subdir: ''`) are provider-independent and
690
+ * always land;
691
+ * - the PR-host tree ({@link PR_HOST_DESTINATION_ROOT}) is provider-independent
692
+ * for the same reason the GitHub tree is a floor — pull requests, PR reviews
693
+ * and PR checks stay on GitHub under every issue tracker — but it sits under
694
+ * no provider directory, so it is named here rather than reached through the
695
+ * provider union;
696
+ * - a provider directory the user did not select is 11 files nothing they can
697
+ * reach ever loads (applies ADR-003 — ship the end state, not every state).
698
+ *
699
+ * `tracker/_mcp.md` rides the same gate its GENERATION does
700
+ * ({@link MCP_BACKED_PROVIDER_SUBDIRS}): it is the transport contract for
701
+ * providers reached by tool call, and GitHub's mechanics are `gh` commands. One
702
+ * predicate, asked of the selection here and of the registry in
703
+ * {@link mcpContractIsGenerated}, so opening the gate and shipping the provider
704
+ * stay the same edit.
705
+ *
706
+ * Derived from the registry rather than a provider table: a provider registered
707
+ * with a `tracker/{id}` subdir is installable by construction, and a literal
708
+ * here would be a second roster to keep in step with VARIANT_MODULES.
709
+ *
710
+ * Asserts rather than degrades on a registry that does not expand, exactly as
711
+ * its sibling does (design review M3): the registry is a compile-time constant,
712
+ * so a refusal is a programming error rather than an install-time degradation —
713
+ * no caller could sensibly continue, and every caller would otherwise carry the
714
+ * same impossible branch.
715
+ *
716
+ * @param opts.provider - The resolved tracker provider id, used as the
717
+ * `tracker/{id}` sub-directory key.
718
+ * @param opts.modules - Registry to expand (defaults to the shipped one).
719
+ * Injectable so both the refusal arm and a provider set this build does not
720
+ * produce are provable without editing the registry.
721
+ */
722
+ export function installedReferenceManifest(opts) {
723
+ const modules = opts.modules ?? resolveVariantModules();
724
+ const expanded = expandVariants(modules);
725
+ if (!expanded.ok) {
726
+ throw new Error(`Reference module registry does not expand — ${JSON.stringify(expanded.error)}. ` +
727
+ `VARIANT_MODULES in src/core/mds-variants.ts is invalid.`);
728
+ }
729
+ const providerSubdir = `${TRACKER_DESTINATION_ROOT}/${opts.provider}`;
730
+ const wanted = new Set(['', PR_HOST_DESTINATION_ROOT, PR_HOST_TRACKER_SUBDIR, providerSubdir]);
731
+ const installed = expanded.value
732
+ .filter(pair => wanted.has(subdirOfRelPath(pair.relPath)))
733
+ .map(pair => pair.relPath);
734
+ const gated = MCP_BACKED_PROVIDER_SUBDIRS;
735
+ if (gated.includes(providerSubdir)) {
736
+ const contract = contractRelPath(expanded.value);
737
+ if (contract !== undefined)
738
+ installed.push(contract);
739
+ }
740
+ return installed;
741
+ }
742
+ /** The directory part of a manifest-relative path; `''` for a file at the root. */
743
+ function subdirOfRelPath(relPath) {
744
+ const cut = relPath.lastIndexOf('/');
745
+ return cut < 0 ? '' : relPath.slice(0, cut);
746
+ }
747
+ /**
748
+ * The tool-call contract's emitted path, as this registry expands it — read from
749
+ * the expansion rather than composed from the module's fields, so the name can
750
+ * only ever be the one the build actually writes.
751
+ *
752
+ * Takes the already-expanded pairs rather than re-expanding: the caller has
753
+ * already validated the same registry expands cleanly, so a second call would
754
+ * only duplicate that work and reintroduce a refusal branch that can never fire.
755
+ */
756
+ function contractRelPath(pairs) {
757
+ return pairs.find(pair => pair.module === MCP_CONTRACT_MODULE.source)?.relPath;
758
+ }
759
+ // ---------------------------------------------------------------------------
760
+ // Section splitting — which slice of a module's compiled body belongs to which op
761
+ // ---------------------------------------------------------------------------
762
+ /**
763
+ * The delimiter a reference module writes before each operation's section.
764
+ *
765
+ * An HTML comment rather than a heading: the splitter CONSUMES these lines, so
766
+ * the emitted reference starts with its own content and carries no build
767
+ * plumbing. A heading would have to survive into the file and would then be
768
+ * load-bearing for two unrelated readers at once.
769
+ *
770
+ * No `g`/`y` flag on the shared object — callers construct their own scanner
771
+ * rather than inherit a lastIndex (the same rule LEADING_BLOCK_RE follows in
772
+ * scripts/build-mds.ts).
773
+ *
774
+ * The optional leading `_` mirrors validateContractOutputName, and widening the
775
+ * capture here costs nothing: this regex is NOT a containment gate. It recognises
776
+ * a plumbing comment inside a source file, and the name it captures is then
777
+ * checked against the caller's own registry (`unknown-section`), so a marker
778
+ * naming something unregistered is refused whatever its spelling. The gate on
779
+ * what may become a PATH is validateOutputName / validateContractOutputName,
780
+ * which run over the registry, not over the file.
781
+ */
782
+ export const VARIANT_SECTION_MARKER_RE = /^<!-- op: (_?[a-z0-9][a-z0-9._-]{0,63}) -->[ \t]*$/;
783
+ /**
784
+ * Split a reference module's compiled body into one document per operation.
785
+ *
786
+ * Bidirectional, and both directions are load-bearing:
787
+ * - unknown-section — the body carries a section for an op the registry does
788
+ * not name, so a file would ship that nothing loads (ADR-003);
789
+ * - missing-section — the registry names an op the body does not cover, so the
790
+ * preamble's load instruction resolves to nothing at runtime.
791
+ * A forward-only check passes on either half of that pair.
792
+ *
793
+ * empty-section is the third arm, and it exists because the other two cannot see
794
+ * it: an op with a marker and no body compiles cleanly and emits a zero-byte
795
+ * reference, which reads downstream as "mechanics unavailable" with no build
796
+ * signal at all (the GAP-44 shape — omission is caught, emptiness is not).
797
+ *
798
+ * Total on success, and immutable: the caller gets back a readonly array of its
799
+ * OWN records, in its own order, each carrying its section. Nothing is looked up
800
+ * afterwards, so no consumer can be handed `undefined` for an operation the
801
+ * registry declared, and no consumer holds a handle it could write through.
802
+ *
803
+ * @param body - The module's compiled output, steering block already stripped.
804
+ * @param entries - The caller's records, one per operation the registry says this
805
+ * module emits, each naming its operation in `op`. Taking the caller's records
806
+ * rather than a bare op list is what lets the result carry each operation's
807
+ * destination back to it structurally, with no index correspondence to trust.
808
+ */
809
+ export function splitVariantSections(body, entries) {
810
+ const ops = entries.map(entry => entry.op);
811
+ const lines = body.split('\n');
812
+ const sections = new Map();
813
+ const expected = new Set(ops);
814
+ let current = null;
815
+ for (const line of lines) {
816
+ const match = VARIANT_SECTION_MARKER_RE.exec(line);
817
+ if (match !== null) {
818
+ const op = match[1];
819
+ if (!expected.has(op))
820
+ return Err({ kind: 'unknown-section', op, expected: ops });
821
+ if (sections.has(op))
822
+ return Err({ kind: 'duplicate-section', op });
823
+ // The buffer itself is what the scan carries forward, not the op name it is
824
+ // filed under, so appending a line is never a second partial lookup.
825
+ current = [];
826
+ sections.set(op, current);
827
+ continue;
828
+ }
829
+ // Text before the first marker is module-level preamble and is dropped: it
830
+ // belongs to no operation, so shipping it would duplicate it into every file.
831
+ if (current === null)
832
+ continue;
833
+ current.push(line);
834
+ }
835
+ if (sections.size === 0)
836
+ return Err({ kind: 'no-sections', expected: ops });
837
+ // Pair every entry with its collected section, recording the entries the body
838
+ // never covered. Parity is decided in full before any content is judged, so a
839
+ // body that is both short and empty-in-places still reports missing-section —
840
+ // the omission, which is the larger fact.
841
+ const paired = [];
842
+ const missing = [];
843
+ for (const entry of entries) {
844
+ const collected = sections.get(entry.op);
845
+ if (collected === undefined)
846
+ missing.push(entry.op);
847
+ else
848
+ paired.push({ entry, collected });
849
+ }
850
+ if (missing.length > 0)
851
+ return Err({ kind: 'missing-section', ops: missing });
852
+ const out = [];
853
+ for (const { entry, collected } of paired) {
854
+ const trimmed = collected.join('\n').trim();
855
+ if (trimmed.length === 0)
856
+ return Err({ kind: 'empty-section', op: entry.op });
857
+ out.push({ ...entry, content: `${trimmed}\n` });
858
+ }
859
+ return Ok(out);
860
+ }
861
+ //# sourceMappingURL=mds-variants.js.map