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.
- package/CHANGELOG.md +156 -0
- package/README.md +86 -18
- package/dist/agents/git.md +824 -0
- package/dist/cli/commands/agents.js +6 -1
- package/dist/cli/commands/attribution-prompts.js +1 -1
- package/dist/cli/commands/compliance-prompts.js +1 -1
- package/dist/cli/commands/compliance.js +23 -1
- package/dist/cli/commands/init-seed.js +24 -26
- package/dist/cli/commands/init.js +502 -71
- package/dist/cli/commands/install-report.js +205 -0
- package/dist/cli/commands/knowledge/index.js +2 -2
- package/dist/cli/commands/knowledge/toggle.js +27 -37
- package/dist/cli/commands/learning.js +37 -30
- package/dist/cli/commands/memory.js +79 -69
- package/dist/cli/commands/prompt-io.js +4 -4
- package/dist/cli/commands/security.js +76 -16
- package/dist/cli/commands/skills.js +53 -7
- package/dist/cli/commands/tracker-prompts.js +145 -0
- package/dist/cli/commands/tracker.js +405 -0
- package/dist/cli/commands/uninstall.js +211 -65
- package/dist/cli.js +2 -0
- package/dist/commands/bug-analysis.md +22 -4
- package/dist/commands/code-review.md +44 -15
- package/dist/commands/debug.md +20 -6
- package/dist/commands/dynamic-build.md +289 -67
- package/dist/commands/dynamic-plan.md +60 -21
- package/dist/commands/dynamic-profile.md +1 -1
- package/dist/commands/dynamic-tickets.md +58 -8
- package/dist/commands/explore.md +2 -2
- package/dist/commands/implement.md +241 -53
- package/dist/commands/plan.md +88 -17
- package/dist/commands/release.md +64 -17
- package/dist/commands/resolve.md +138 -58
- package/dist/commands/self-review.md +2 -2
- package/dist/core/agent-models.js +55 -12
- package/dist/core/assets.js +58 -2
- package/dist/core/evidence-policy.js +147 -0
- package/dist/core/feature-config.js +130 -64
- package/dist/core/feature-switch.js +112 -0
- package/dist/core/flags.js +4 -4
- package/dist/core/manifest.js +33 -7
- package/dist/core/mds-variants.js +861 -0
- package/dist/core/model-discovery.js +12 -1
- package/dist/core/plugins.js +357 -9
- package/dist/core/project-paths.js +1 -1
- package/dist/core/proxy-log.js +8 -6
- package/dist/core/proxy-state.js +11 -8
- package/dist/core/reference-sweep.js +136 -0
- package/dist/core/tracker.js +407 -0
- package/dist/skills/git/references/decision-markers.md +19 -0
- package/dist/skills/git/references/learn-conventions.md +56 -0
- package/dist/skills/git/references/pr/check-ci-status.md +14 -0
- package/dist/skills/git/references/pr/check-merge-readiness.md +28 -0
- package/dist/skills/git/references/pr/ensure-pr-ready.md +24 -0
- package/dist/skills/git/references/pr/fetch-review-threads.md +22 -0
- package/dist/skills/git/references/pr/post-resolution-summary.md +40 -0
- package/dist/skills/git/references/pr/post-review-summary.md +42 -0
- package/dist/skills/git/references/pr/resolve-review-threads.md +35 -0
- package/dist/skills/git/references/pr/update-pr-evidence.md +14 -0
- package/dist/skills/git/references/pr/validate-branch.md +18 -0
- package/dist/skills/git/references/publication-gate.md +13 -0
- package/dist/skills/git/references/tracker/_mcp.md +153 -0
- package/dist/skills/git/references/tracker/github/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/github/backlink-shipped-issues.md +40 -0
- package/dist/skills/git/references/tracker/github/create-release.md +11 -0
- package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +16 -0
- package/dist/skills/git/references/tracker/github/ensure-traceable-issue.md +69 -0
- package/dist/skills/git/references/tracker/github/fetch-issue.md +32 -0
- package/dist/skills/git/references/tracker/github/fetch-issues-batch.md +17 -0
- package/dist/skills/git/references/tracker/github/gather-release-evidence.md +19 -0
- package/dist/skills/git/references/tracker/github/manage-debt.md +101 -0
- package/dist/skills/git/references/tracker/github/post-wave-report.md +28 -0
- package/dist/skills/git/references/tracker/github/setup-task.md +26 -0
- package/dist/skills/git/references/tracker/jira/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/jira/backlink-shipped-issues.md +49 -0
- package/dist/skills/git/references/tracker/jira/create-release.md +17 -0
- package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/jira/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/jira/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/jira/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/jira/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/jira/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/jira/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/jira/setup-task.md +31 -0
- package/dist/skills/git/references/tracker/linear/associate-release.md +18 -0
- package/dist/skills/git/references/tracker/linear/backlink-shipped-issues.md +53 -0
- package/dist/skills/git/references/tracker/linear/create-release.md +17 -0
- package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +22 -0
- package/dist/skills/git/references/tracker/linear/ensure-traceable-issue.md +53 -0
- package/dist/skills/git/references/tracker/linear/fetch-issue.md +14 -0
- package/dist/skills/git/references/tracker/linear/fetch-issues-batch.md +15 -0
- package/dist/skills/git/references/tracker/linear/gather-release-evidence.md +18 -0
- package/dist/skills/git/references/tracker/linear/manage-debt.md +37 -0
- package/dist/skills/git/references/tracker/linear/post-wave-report.md +33 -0
- package/dist/skills/git/references/tracker/linear/setup-task.md +32 -0
- package/dist/skills/git/references/trust-rule.md +7 -0
- package/dist/targets/claude-code/installer.js +1213 -31
- package/dist/targets/claude-code/legacy.js +5 -0
- package/dist/targets/claude-code/post-install.js +196 -74
- package/dist/targets/claude-code/tracker-install.js +161 -0
- package/package.json +4 -3
- package/src/assets/agents/code.md +42 -4
- package/src/assets/agents/design.md +1 -1
- package/src/assets/agents/git.mds +827 -0
- package/src/assets/agents/knowledge.md +1 -1
- package/src/assets/agents/learning.md +11 -0
- package/src/assets/agents/synthesize.md +1 -1
- package/src/assets/agents/test.md +16 -5
- package/src/assets/agents/tracker.md +467 -0
- package/src/assets/agents/validate.md +7 -5
- package/src/assets/commands/_partials/_engine.mds +11 -9
- package/src/assets/commands/_partials/_evidence_policy.mds +30 -0
- package/src/assets/commands/_partials/_knowledge.mds +2 -2
- package/src/assets/commands/_partials/_plan_contract.mds +22 -7
- package/src/assets/commands/_partials/_preamble.mds +1 -1
- package/src/assets/commands/_partials/_publication.mds +3 -1
- package/src/assets/commands/_partials/_ticket_template.mds +3 -2
- package/src/assets/commands/_partials/_tracker.mds +18 -0
- package/src/assets/commands/_partials/_wave.mds +16 -10
- package/src/assets/commands/bug-analysis.mds +15 -5
- package/src/assets/commands/code-review.mds +34 -14
- package/src/assets/commands/debug.mds +11 -4
- package/src/assets/commands/dynamic-build.mds +227 -41
- package/src/assets/commands/dynamic-plan.mds +35 -13
- package/src/assets/commands/dynamic-tickets.mds +47 -5
- package/src/assets/commands/implement.mds +206 -52
- package/src/assets/commands/plan.mds +70 -17
- package/src/assets/commands/release.md +64 -17
- package/src/assets/commands/resolve.mds +126 -56
- package/src/assets/mds/git/_pr.mds +331 -0
- package/src/assets/mds/git/_references.mds +135 -0
- package/src/assets/mds/tracker/_common.mds +156 -0
- package/src/assets/mds/tracker/_github.mds +472 -0
- package/src/assets/mds/tracker/_jira.mds +407 -0
- package/src/assets/mds/tracker/_linear.mds +449 -0
- package/src/assets/mds/tracker/_mcp.mds +299 -0
- package/src/assets/scripts/hooks/assets/orchestrator-charter.md +5 -8
- package/src/assets/scripts/hooks/background-memory-update +14 -9
- package/src/assets/scripts/hooks/capture-prompt +6 -2
- package/src/assets/scripts/hooks/capture-question +6 -2
- package/src/assets/scripts/hooks/capture-turn +6 -2
- package/src/assets/scripts/hooks/ensure-devflow-init +1 -1
- package/src/assets/scripts/hooks/ensure-root-gitignore +161 -60
- package/src/assets/scripts/hooks/hook-log-init +3 -1
- package/src/assets/scripts/hooks/json-helper.cjs +223 -5
- package/src/assets/scripts/hooks/lib/project-paths.cjs +1 -1
- package/src/assets/scripts/hooks/memory-worker +15 -8
- package/src/assets/scripts/hooks/pre-compact-memory +12 -8
- package/src/assets/scripts/hooks/preamble +1 -4
- package/src/assets/scripts/hooks/queue-append +68 -24
- package/src/assets/scripts/hooks/session-start-context +355 -8
- package/src/assets/scripts/hooks/session-start-memory +12 -8
- package/src/assets/scripts/pr-evidence.cjs +1961 -0
- package/src/assets/scripts/redact-secrets.cjs +490 -62
- package/src/assets/scripts/release-trace.cjs +1143 -0
- package/src/assets/scripts/resolve-evidence-policy.cjs +1065 -0
- package/src/assets/scripts/verify-evidence.cjs +1822 -0
- package/src/assets/skills/compliance/SKILL.md +2 -0
- package/src/assets/skills/docs-framework/SKILL.md +5 -3
- package/src/assets/skills/git/SKILL.md +8 -78
- package/src/assets/skills/git/references/github-api.md +179 -141
- package/src/assets/skills/git/references/patterns.md +11 -6
- package/src/assets/skills/review-methodology/SKILL.md +1 -1
- package/src/assets/skills/review-methodology/references/patterns.md +6 -61
- package/src/assets/skills/review-methodology/references/violations.md +14 -22
- 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
|