@rungs/cli 0.1.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/LICENSE +21 -0
- package/README.md +196 -0
- package/modules/README.md +116 -0
- package/modules/adr/files/{{path}}/README.md +52 -0
- package/modules/adr/files/{{path}}/TEMPLATE.md +55 -0
- package/modules/adr/fragments/AGENTS.md +8 -0
- package/modules/adr/gates/adr.toml +65 -0
- package/modules/adr/module.toml +67 -0
- package/modules/audit/files/{{criteria_path}} +43 -0
- package/modules/audit/fragments/AGENTS.md +8 -0
- package/modules/audit/gates/audit.toml +48 -0
- package/modules/audit/module.toml +72 -0
- package/modules/audit/skills/assess/SKILL.md +62 -0
- package/modules/backlog/files/docs/{{root}}/BACKLOG.md +52 -0
- package/modules/backlog/files/docs/{{root}}/README.md +110 -0
- package/modules/backlog/files/docs/{{root}}/TEMPLATE.md +56 -0
- package/modules/backlog/files/docs/{{root}}/archive/README.md +13 -0
- package/modules/backlog/files/docs/{{root}}/items/README.md +13 -0
- package/modules/backlog/fragments/AGENTS.md +9 -0
- package/modules/backlog/gates/ids.toml +106 -0
- package/modules/backlog/module.toml +150 -0
- package/modules/backlog/rules/work-items.md +44 -0
- package/modules/backlog/skills/backlog-summary/SKILL.md +65 -0
- package/modules/backlog/skills/work-item/SKILL.md +96 -0
- package/modules/ci/files/{{workflow_path}} +43 -0
- package/modules/ci/gates/ci.toml +44 -0
- package/modules/ci/module.toml +74 -0
- package/modules/concurrency/files/docs/concurrent-sessions.md +87 -0
- package/modules/concurrency/fragments/AGENTS.md +10 -0
- package/modules/concurrency/fragments/gitattributes +15 -0
- package/modules/concurrency/gates/concurrency.toml +54 -0
- package/modules/concurrency/module.toml +113 -0
- package/modules/design-sync/fragments/AGENTS.md +8 -0
- package/modules/design-sync/gates/design.toml +70 -0
- package/modules/design-sync/module.toml +82 -0
- package/modules/design-sync/skills/design-align/SKILL.md +51 -0
- package/modules/design-sync/skills/design-pull/SKILL.md +50 -0
- package/modules/doc-authority/files/{{registry_path}} +59 -0
- package/modules/doc-authority/files/{{rules_path}} +51 -0
- package/modules/doc-authority/fragments/AGENTS.md +9 -0
- package/modules/doc-authority/gates/authority.toml +127 -0
- package/modules/doc-authority/module.toml +111 -0
- package/modules/findings/files/docs/{{backlog.root}}/FINDINGS.md +56 -0
- package/modules/findings/fragments/AGENTS.md +7 -0
- package/modules/findings/gates/findings.toml +68 -0
- package/modules/findings/module.toml +100 -0
- package/modules/findings/skills/record-finding/SKILL.md +63 -0
- package/modules/gates/files/.ai/gates.toml +49 -0
- package/modules/gates/fragments/AGENTS.md +8 -0
- package/modules/gates/fragments/gitignore +4 -0
- package/modules/gates/gates/structural.toml +127 -0
- package/modules/gates/module.toml +143 -0
- package/modules/gates/skills/harden-rule/SKILL.md +82 -0
- package/modules/instructions/files/.ai/rules/README.md +53 -0
- package/modules/instructions/files/AGENTS.md +104 -0
- package/modules/instructions/files/CLAUDE.md +11 -0
- package/modules/instructions/gates/core.toml +114 -0
- package/modules/instructions/module.toml +127 -0
- package/modules/release/fragments/AGENTS.md +8 -0
- package/modules/release/gates/release.toml +85 -0
- package/modules/release/module.toml +97 -0
- package/modules/release/skills/cut-release/SKILL.md +81 -0
- package/modules/session/files/{{archive}}/README.md +28 -0
- package/modules/session/files/{{path}} +56 -0
- package/modules/session/fragments/AGENTS.md +6 -0
- package/modules/session/gates/session.toml +63 -0
- package/modules/session/module.toml +72 -0
- package/modules/session/skills/close-session/SKILL.md +59 -0
- package/modules/skills/fragments/AGENTS.md +8 -0
- package/modules/skills/gates/skills.toml +94 -0
- package/modules/skills/module.toml +89 -0
- package/modules/skills/rules/skill-authoring.md +71 -0
- package/modules/specs/files/{{path}}/README.md +63 -0
- package/modules/specs/files/{{path}}/TEMPLATE.md +62 -0
- package/modules/specs/fragments/AGENTS.md +8 -0
- package/modules/specs/gates/specs.toml +119 -0
- package/modules/specs/module.toml +107 -0
- package/modules/workflows/fragments/AGENTS.md +8 -0
- package/modules/workflows/gates/workflows.toml +65 -0
- package/modules/workflows/module.toml +75 -0
- package/modules/workflows/rules/planning-tiers.md +46 -0
- package/modules/workflows/rules/reuse-decision.md +50 -0
- package/modules/workflows/skills/decompose/SKILL.md +75 -0
- package/package.json +42 -0
- package/src/add.ts +331 -0
- package/src/check.ts +216 -0
- package/src/cli.ts +411 -0
- package/src/detect.ts +246 -0
- package/src/engines.ts +260 -0
- package/src/engines2.ts +297 -0
- package/src/engines3.ts +194 -0
- package/src/glob.ts +101 -0
- package/src/lifecycle.ts +268 -0
- package/src/manifest.ts +108 -0
- package/src/render.ts +222 -0
- package/src/substitute.ts +78 -0
- package/src/types.ts +110 -0
package/src/render.ts
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { walk } from './glob.ts';
|
|
4
|
+
|
|
5
|
+
export type Harness = 'claude' | 'copilot' | 'cursor' | 'agents-md';
|
|
6
|
+
|
|
7
|
+
export interface Rule {
|
|
8
|
+
file: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
paths: string[];
|
|
11
|
+
enforcement?: string;
|
|
12
|
+
body: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface RenderEntry {
|
|
16
|
+
rule: string;
|
|
17
|
+
harness: Harness;
|
|
18
|
+
target?: string;
|
|
19
|
+
degraded?: string;
|
|
20
|
+
dropped?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const DO_NOT_EDIT = (source: string) =>
|
|
24
|
+
`Generated by \`rungs render\` from ${source}. Do not edit — your changes are overwritten.`;
|
|
25
|
+
|
|
26
|
+
/** Parse `.ai/rules/*.md`: the neutral source ADR-0001 renders from. */
|
|
27
|
+
export function readRules(repoRoot: string): Rule[] {
|
|
28
|
+
const dir = join(repoRoot, '.ai', 'rules');
|
|
29
|
+
const rules: Rule[] = [];
|
|
30
|
+
let files: string[];
|
|
31
|
+
try {
|
|
32
|
+
files = walk(dir).filter((f) => f.endsWith('.md') && f !== 'README.md');
|
|
33
|
+
} catch {
|
|
34
|
+
return rules;
|
|
35
|
+
}
|
|
36
|
+
for (const rel of files) {
|
|
37
|
+
const raw = readFileSync(join(dir, rel), 'utf8');
|
|
38
|
+
const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
39
|
+
if (!m) continue;
|
|
40
|
+
const [, fm, body] = m;
|
|
41
|
+
rules.push({
|
|
42
|
+
file: rel,
|
|
43
|
+
description: scalar(fm, 'description'),
|
|
44
|
+
paths: list(fm, 'paths'),
|
|
45
|
+
enforcement: scalar(fm, 'enforcement'),
|
|
46
|
+
body: body.trim(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return rules;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function scalar(fm: string, key: string): string | undefined {
|
|
53
|
+
const folded = fm.match(new RegExp(`^${key}:\\s*>-?\\s*\\n([\\s\\S]*?)(?=\\n\\S|$)`, 'm'));
|
|
54
|
+
if (folded) return folded[1].split('\n').map((l) => l.trim()).filter(Boolean).join(' ');
|
|
55
|
+
const plain = fm.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'));
|
|
56
|
+
return plain?.[1].trim().replace(/^["']|["']$/g, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function list(fm: string, key: string): string[] {
|
|
60
|
+
const block = fm.match(new RegExp(`^${key}:\\s*\\n((?:\\s*-\\s*.+\\n?)+)`, 'm'));
|
|
61
|
+
if (!block) return [];
|
|
62
|
+
return [...block[1].matchAll(/^\s*-\s*(.+)$/gm)].map((m) => m[1].trim().replace(/^["']|["']$/g, ''));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Emit one rule into one harness's dialect. Full bodies, never pointers: a
|
|
67
|
+
* wrapper that references a shared file relies on the harness following the
|
|
68
|
+
* reference, and only some do. A rule that does not load is worth nothing.
|
|
69
|
+
*/
|
|
70
|
+
export function renderRule(rule: Rule, harness: Harness): { target: string; content: string; dropped: string[] } | { degraded: string } {
|
|
71
|
+
const stem = rule.file.replace(/\.md$/, '');
|
|
72
|
+
const source = `.ai/rules/${rule.file}`;
|
|
73
|
+
const dropped: string[] = [];
|
|
74
|
+
|
|
75
|
+
if (harness === 'claude') {
|
|
76
|
+
// No description field in a Claude rule; the routing is done by `paths`.
|
|
77
|
+
if (rule.description) dropped.push('description');
|
|
78
|
+
const fm = rule.paths.length ? `paths:\n${rule.paths.map((p) => ` - "${p}"`).join('\n')}\n` : '';
|
|
79
|
+
return {
|
|
80
|
+
target: `.claude/rules/${stem}.md`,
|
|
81
|
+
content: `---\n${fm}---\n\n<!-- ${DO_NOT_EDIT(source)} -->\n\n${rule.body}\n`,
|
|
82
|
+
dropped,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (harness === 'copilot') {
|
|
87
|
+
const applyTo = rule.paths.length ? rule.paths.join(', ') : '**/*';
|
|
88
|
+
const desc = rule.description ? `description: '${rule.description.replace(/'/g, "''")}'\n` : '';
|
|
89
|
+
return {
|
|
90
|
+
target: `.github/instructions/${stem}.instructions.md`,
|
|
91
|
+
content: `---\n${desc}applyTo: '${applyTo}'\n---\n\n<!-- ${DO_NOT_EDIT(source)} -->\n\n${rule.body}\n`,
|
|
92
|
+
dropped,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (harness === 'cursor') {
|
|
97
|
+
// `.mdc` is required — a plain .md in .cursor/rules is ignored entirely.
|
|
98
|
+
const desc = rule.description ? `description: ${rule.description}\n` : '';
|
|
99
|
+
const globs = rule.paths.length ? `globs: ${rule.paths.join(',')}\n` : '';
|
|
100
|
+
return {
|
|
101
|
+
target: `.cursor/rules/${stem}.mdc`,
|
|
102
|
+
content: `---\n${desc}${globs}alwaysApply: ${rule.paths.length === 0}\n---\n\n<!-- ${DO_NOT_EDIT(source)} -->\n\n${rule.body}\n`,
|
|
103
|
+
dropped,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// AGENTS.md-only harnesses have no glob scoping at all. Degrade explicitly
|
|
108
|
+
// and report it — never drop a rule silently.
|
|
109
|
+
const prefix = commonDirPrefix(rule.paths);
|
|
110
|
+
if (prefix) {
|
|
111
|
+
return {
|
|
112
|
+
target: `${prefix}/AGENTS.md`,
|
|
113
|
+
content: `<!-- ${DO_NOT_EDIT(source)} -->\n\n${rule.body}\n`,
|
|
114
|
+
dropped: ['description', 'paths (directory-scoped instead of glob)'],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
degraded: `routing-only: globs do not share a directory prefix, so root AGENTS.md gets a pointer to ${source}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function commonDirPrefix(paths: string[]): string | null {
|
|
123
|
+
if (!paths.length) return null;
|
|
124
|
+
const dirs = paths.map((p) => p.split('/').filter((s) => !s.includes('*')).join('/')).filter(Boolean);
|
|
125
|
+
if (dirs.length !== paths.length) return null;
|
|
126
|
+
const first = dirs[0];
|
|
127
|
+
return dirs.every((d) => d === first) && first.includes('/') ? first : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function render(repoRoot: string, harnesses: Harness[]): RenderEntry[] {
|
|
131
|
+
const rules = readRules(repoRoot);
|
|
132
|
+
const entries: RenderEntry[] = [];
|
|
133
|
+
const routingOnly: Rule[] = [];
|
|
134
|
+
|
|
135
|
+
for (const rule of rules) {
|
|
136
|
+
for (const harness of harnesses) {
|
|
137
|
+
const out = renderRule(rule, harness);
|
|
138
|
+
if ('degraded' in out) {
|
|
139
|
+
entries.push({ rule: rule.file, harness, degraded: out.degraded });
|
|
140
|
+
if (harness === 'agents-md') routingOnly.push(rule);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const full = join(repoRoot, out.target);
|
|
144
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
145
|
+
writeFileSync(full, out.content);
|
|
146
|
+
entries.push({ rule: rule.file, harness, target: out.target, dropped: out.dropped });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// The report said root AGENTS.md "gets a pointer" and nothing wrote one — a
|
|
151
|
+
// degradation notice that was itself a silent drop, in the function whose
|
|
152
|
+
// whole job is not to have those. Written now, as a managed block.
|
|
153
|
+
writeRoutingBlock(repoRoot, routingOnly, harnesses);
|
|
154
|
+
return entries;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function writeRoutingBlock(repoRoot: string, rules: Rule[], harnesses: Harness[]) {
|
|
158
|
+
if (!harnesses.includes('agents-md')) return;
|
|
159
|
+
const target = join(repoRoot, 'AGENTS.md');
|
|
160
|
+
if (!existsSync(target)) return;
|
|
161
|
+
const begin = '<!-- rungs:begin rules-routing -->';
|
|
162
|
+
const end = '<!-- rungs:end rules-routing -->';
|
|
163
|
+
|
|
164
|
+
const body = rules.length
|
|
165
|
+
? [
|
|
166
|
+
begin,
|
|
167
|
+
'## Rules for specific paths',
|
|
168
|
+
'',
|
|
169
|
+
'This harness has no glob scoping, so these load only if you open them. **Read the one that',
|
|
170
|
+
'matches what you are editing before editing broadly.**',
|
|
171
|
+
'',
|
|
172
|
+
...rules.map((r) => `- \`${r.paths.join('\`, \`')}\` → [\`.ai/rules/${r.file}\`](.ai/rules/${r.file})`),
|
|
173
|
+
end,
|
|
174
|
+
].join('\n')
|
|
175
|
+
: '';
|
|
176
|
+
|
|
177
|
+
const existing = readFileSync(target, 'utf8');
|
|
178
|
+
const beginRe = /^[ \t]*<!--\s*rungs:begin rules-routing\s*-->[ \t]*$/m;
|
|
179
|
+
const endRe = /^[ \t]*<!--\s*rungs:end rules-routing\s*-->[ \t]*$/m;
|
|
180
|
+
const b = existing.match(beginRe);
|
|
181
|
+
const e = existing.match(endRe);
|
|
182
|
+
if (b && e && b.index !== undefined && e.index !== undefined) {
|
|
183
|
+
const next = existing.slice(0, b.index) + body.trim() + existing.slice(e.index + e[0].length);
|
|
184
|
+
writeFileSync(target, body ? next : next.replace(/\n{3,}/g, '\n\n'));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (body) writeFileSync(target, `${existing.replace(/\n+$/, '\n')}\n${body}\n`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* ADR-0001: a render that quietly dropped a rule reads identically to one that
|
|
192
|
+
* had nothing to drop, so every degradation lands here.
|
|
193
|
+
*/
|
|
194
|
+
export function writeReport(repoRoot: string, entries: RenderEntry[], harnesses: Harness[], stamp: string): string {
|
|
195
|
+
const lines = [
|
|
196
|
+
'# Render report',
|
|
197
|
+
'',
|
|
198
|
+
`> Generated by \`rungs render\` on ${stamp}. Do not edit.`,
|
|
199
|
+
'',
|
|
200
|
+
`Harnesses: ${harnesses.join(', ')}`,
|
|
201
|
+
'',
|
|
202
|
+
'| Rule | Harness | Emitted | Dropped / degraded |',
|
|
203
|
+
'| --- | --- | --- | --- |',
|
|
204
|
+
];
|
|
205
|
+
for (const e of entries) {
|
|
206
|
+
const lost = e.degraded ?? (e.dropped?.length ? e.dropped.join(', ') : '—');
|
|
207
|
+
lines.push(`| \`${e.rule}\` | ${e.harness} | ${e.target ? `\`${e.target}\`` : '**not emitted**'} | ${lost} |`);
|
|
208
|
+
}
|
|
209
|
+
const degraded = entries.filter((e) => e.degraded).length;
|
|
210
|
+
const lossy = entries.filter((e) => e.dropped?.length).length;
|
|
211
|
+
lines.push(
|
|
212
|
+
'',
|
|
213
|
+
`${entries.length} renderings · ${lossy} lost a field · ${degraded} degraded.`,
|
|
214
|
+
'',
|
|
215
|
+
'A field listed as dropped is one the target harness has no way to express. It is recorded',
|
|
216
|
+
'here rather than silently discarded, so a repo can see what its harness choice costs it.',
|
|
217
|
+
'',
|
|
218
|
+
);
|
|
219
|
+
const content = lines.join('\n');
|
|
220
|
+
writeFileSync(join(repoRoot, '.ai', 'render-report.md'), content);
|
|
221
|
+
return content;
|
|
222
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { Manifest } from './types.ts';
|
|
2
|
+
|
|
3
|
+
export type Params = Record<string, Record<string, unknown>>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `{{param}}` substitution, in file contents and in path segments. No
|
|
7
|
+
* conditionals, no loops — ADR-0003. A module that needs a conditional is two
|
|
8
|
+
* modules, or reaches file content through a managed block.
|
|
9
|
+
*
|
|
10
|
+
* `${{ … }}` is never substituted: GitHub Actions expressions share the
|
|
11
|
+
* delimiter, and without the passthrough the `ci` module corrupts its own
|
|
12
|
+
* workflow file at install — a broken file rather than an error.
|
|
13
|
+
*/
|
|
14
|
+
export function substitute(text: string, module: string, params: Params): string {
|
|
15
|
+
return text.replace(/(^|[^$])\{\{([a-z_.]+)\}\}/g, (whole, lead: string, ref: string) => {
|
|
16
|
+
const [a, b] = ref.includes('.') ? ref.split('.') : [module, ref];
|
|
17
|
+
const value = params[a]?.[b];
|
|
18
|
+
if (value === undefined) return whole; // leave it visible rather than emitting an empty string
|
|
19
|
+
return lead + format(value);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function format(v: unknown): string {
|
|
24
|
+
if (Array.isArray(v)) return `[${v.map((x) => JSON.stringify(x)).join(', ')}]`;
|
|
25
|
+
if (typeof v === 'boolean' || typeof v === 'number') return String(v);
|
|
26
|
+
return String(v);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Defaults from every manifest, with explicit overrides applied on top. */
|
|
30
|
+
export function resolveParams(mods: Manifest[], overrides: Params = {}): Params {
|
|
31
|
+
const out: Params = {};
|
|
32
|
+
for (const m of mods) {
|
|
33
|
+
out[m.name] = {};
|
|
34
|
+
for (const [k, spec] of Object.entries(m.params)) out[m.name][k] = spec.default;
|
|
35
|
+
}
|
|
36
|
+
// A default may itself reference another module's parameter, e.g. findings'
|
|
37
|
+
// register living at `docs/{{backlog.root}}/FINDINGS.md`. Resolve after all
|
|
38
|
+
// defaults are in place, and only one level — a chain would be a template
|
|
39
|
+
// language arriving through the back door.
|
|
40
|
+
for (const m of mods) {
|
|
41
|
+
for (const [k, v] of Object.entries(out[m.name])) {
|
|
42
|
+
if (typeof v === 'string' && v.includes('{{')) out[m.name][k] = substitute(v, m.name, out);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
for (const [mod, vals] of Object.entries(overrides)) {
|
|
46
|
+
out[mod] = { ...(out[mod] ?? {}), ...vals };
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Comment syntax for a managed block, chosen by the target file. */
|
|
52
|
+
export function markers(targetPath: string, module: string, version: string) {
|
|
53
|
+
const hash = /\.(toml|ya?ml|gitignore|gitattributes|sh|ps1|conf|properties)$|(^|\/)\.(gitignore|gitattributes)$/.test(
|
|
54
|
+
targetPath,
|
|
55
|
+
);
|
|
56
|
+
return hash
|
|
57
|
+
? { begin: `# rungs:begin ${module}@${version}`, end: `# rungs:end ${module}` }
|
|
58
|
+
: { begin: `<!-- rungs:begin ${module}@${version} -->`, end: `<!-- rungs:end ${module} -->` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Replace an existing managed block, or append one. Content outside every block
|
|
63
|
+
* is the user's and is never touched — that is what makes the upgrade story
|
|
64
|
+
* mechanical and divergence a decision rather than an error.
|
|
65
|
+
*/
|
|
66
|
+
export function mergeBlock(existing: string, fragment: string, module: string): string {
|
|
67
|
+
const beginRe = new RegExp(`^[ \\t]*(?:<!--|#)\\s*rungs:begin ${module}(?:@[\\w.\\-]+)?\\s*(?:-->)?[ \\t]*$`, 'm');
|
|
68
|
+
const endRe = new RegExp(`^[ \\t]*(?:<!--|#)\\s*rungs:end ${module}\\s*(?:-->)?[ \\t]*$`, 'm');
|
|
69
|
+
const b = existing.match(beginRe);
|
|
70
|
+
const e = existing.match(endRe);
|
|
71
|
+
if (b && e && b.index !== undefined && e.index !== undefined && e.index > b.index) {
|
|
72
|
+
const before = existing.slice(0, b.index);
|
|
73
|
+
const after = existing.slice(e.index + e[0].length);
|
|
74
|
+
return `${before}${fragment.trim()}${after}`;
|
|
75
|
+
}
|
|
76
|
+
const sep = existing.endsWith('\n\n') ? '' : existing.endsWith('\n') ? '\n' : '\n\n';
|
|
77
|
+
return `${existing}${sep}${fragment.trim()}\n`;
|
|
78
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Shapes read from a module's `module.toml`. See ADR-0003 for the format. */
|
|
2
|
+
|
|
3
|
+
export interface ParamSpec {
|
|
4
|
+
description?: string;
|
|
5
|
+
default?: unknown;
|
|
6
|
+
allowed?: unknown[];
|
|
7
|
+
pattern?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Marks a *behavioural* parameter: it changes what the CLI does rather than
|
|
11
|
+
* being substituted into a template, so it never appears as `{{name}}`.
|
|
12
|
+
* Without this the dead-parameter check reports it, and the obvious "fix"
|
|
13
|
+
* deletes the parameter that decides which harnesses exist.
|
|
14
|
+
*/
|
|
15
|
+
consumed_by?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GateSpec {
|
|
19
|
+
id: string;
|
|
20
|
+
kind: 'declared' | 'command';
|
|
21
|
+
engine?: string;
|
|
22
|
+
table?: string;
|
|
23
|
+
command?: string;
|
|
24
|
+
tier?: string;
|
|
25
|
+
/** A hook is a gate with a lifecycle trigger rather than a runner trigger. */
|
|
26
|
+
trigger?: string;
|
|
27
|
+
matcher?: string;
|
|
28
|
+
why?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ParadigmSpec {
|
|
32
|
+
id: string;
|
|
33
|
+
paths?: string[];
|
|
34
|
+
compare?: string;
|
|
35
|
+
note?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface InferSpec {
|
|
39
|
+
param: string;
|
|
40
|
+
pattern?: string;
|
|
41
|
+
min?: number;
|
|
42
|
+
scope?: string[];
|
|
43
|
+
exclude?: string[];
|
|
44
|
+
paths?: Record<string, string>;
|
|
45
|
+
/**
|
|
46
|
+
* A regex that settles the value outright, ahead of frequency. Frequency
|
|
47
|
+
* alone made `findings` propose the backlog's prefix, because a register
|
|
48
|
+
* cites work items more often than it defines its own ids.
|
|
49
|
+
*/
|
|
50
|
+
anchor?: string;
|
|
51
|
+
anchor_name?: string;
|
|
52
|
+
/** Values that share the id shape but are never ids — `UTF-8`, `SHA-256`. */
|
|
53
|
+
exclude_values?: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface DetectSpec {
|
|
57
|
+
paths?: string[];
|
|
58
|
+
markers?: string[];
|
|
59
|
+
/** Files to scan for markers when path existence is not discriminating. */
|
|
60
|
+
marker_paths?: string[];
|
|
61
|
+
paradigm?: ParadigmSpec[];
|
|
62
|
+
infer?: InferSpec[];
|
|
63
|
+
adopt_as?: { kind: string; paths?: string[]; note?: string }[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface Provenance {
|
|
67
|
+
sources: string[];
|
|
68
|
+
patterns: string[];
|
|
69
|
+
incident: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface Manifest {
|
|
73
|
+
name: string;
|
|
74
|
+
version: string;
|
|
75
|
+
rung: number;
|
|
76
|
+
summary: string;
|
|
77
|
+
requires: string[];
|
|
78
|
+
conflicts: string[];
|
|
79
|
+
params: Record<string, ParamSpec>;
|
|
80
|
+
gates: GateSpec[];
|
|
81
|
+
detect: DetectSpec;
|
|
82
|
+
provenance: Provenance;
|
|
83
|
+
threshold?: { metric: string; minimum: number; confirm?: boolean };
|
|
84
|
+
/** Absolute path to the module directory. */
|
|
85
|
+
dir: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One of ADR-0004's six states, for one module in one repo. */
|
|
89
|
+
export type DetectState =
|
|
90
|
+
| 'absent'
|
|
91
|
+
| 'ours-current'
|
|
92
|
+
| 'ours-diverged'
|
|
93
|
+
| 'theirs'
|
|
94
|
+
| 'paradigm'
|
|
95
|
+
| 'unknown';
|
|
96
|
+
|
|
97
|
+
export interface DetectResult {
|
|
98
|
+
module: string;
|
|
99
|
+
state: DetectState;
|
|
100
|
+
/** Detect globs that matched, with a sample of what they hit. */
|
|
101
|
+
matchedPaths: { pattern: string; count: number; sample: string[] }[];
|
|
102
|
+
matchedMarkers: string[];
|
|
103
|
+
paradigm?: { id: string; note?: string; compare?: string; matched: string[] };
|
|
104
|
+
/** Parameters detection *proposes*. Never used to conclude presence. */
|
|
105
|
+
proposals: { param: string; value: string; evidence: string }[];
|
|
106
|
+
/** Set when the repo installed this module: what we wrote, and whether it still says so. */
|
|
107
|
+
ours?: { version: string; current: string[]; stale: string[]; diverged: string[]; missing: string[]; kept: string[] };
|
|
108
|
+
/** Existing artifacts that would adopt as `command` gates or similar. */
|
|
109
|
+
adoptable: { kind: string; count: number; sample: string[]; note?: string }[];
|
|
110
|
+
}
|