doctrina-cli 0.13.0 → 0.15.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/README.md +1 -1
- package/package.json +3 -2
- package/src/commands/adapter.js +205 -0
- package/src/commands/analyze.js +120 -17
- package/src/commands/change.js +100 -42
- package/src/commands/clarify.js +8 -1
- package/src/commands/close.js +57 -2
- package/src/commands/completion.js +6 -0
- package/src/commands/constitution.js +8 -1
- package/src/commands/context.js +391 -58
- package/src/commands/contract.js +76 -43
- package/src/commands/coverage.js +126 -12
- package/src/commands/decision.js +172 -40
- package/src/commands/doctor.js +92 -11
- package/src/commands/handoff.js +8 -1
- package/src/commands/hooks.js +6 -0
- package/src/commands/index-rebuild.js +8 -1
- package/src/commands/init.js +139 -6
- package/src/commands/intake.js +11 -2
- package/src/commands/intent.js +8 -1
- package/src/commands/metrics.js +80 -7
- package/src/commands/next.js +38 -3
- package/src/commands/prime.js +8 -1
- package/src/commands/report.js +8 -1
- package/src/commands/review.js +8 -1
- package/src/commands/search.js +8 -1
- package/src/commands/show.js +8 -1
- package/src/commands/skill.js +201 -8
- package/src/commands/spec.js +13 -6
- package/src/commands/status.js +14 -2
- package/src/commands/templates.js +193 -59
- package/src/commands/trace.js +15 -3
- package/src/commands/triage.js +248 -0
- package/src/commands/upgrade.js +8 -1
- package/src/commands/validate.js +148 -5
- package/src/commands/verify.js +126 -5
- package/src/commands/watch.js +8 -1
- package/src/commands/why.js +10 -24
- package/src/commands/work.js +44 -3
- package/src/index.js +61 -6
- package/src/lib/adapters.js +140 -0
- package/src/lib/adr-guard.js +1 -0
- package/src/lib/args.js +10 -0
- package/src/lib/clarity.js +1 -0
- package/src/lib/colors.js +1 -0
- package/src/lib/commands.js +252 -27
- package/src/lib/criteria.js +1 -0
- package/src/lib/dates.js +1 -0
- package/src/lib/diff.js +1 -0
- package/src/lib/doc-model.js +287 -0
- package/src/lib/docs-impact.js +167 -0
- package/src/lib/ears.js +1 -0
- package/src/lib/exit-codes.js +103 -0
- package/src/lib/flag-catalog.js +106 -0
- package/src/lib/fs-ops.js +1 -0
- package/src/lib/gates.js +123 -0
- package/src/lib/git.js +97 -0
- package/src/lib/index-json.js +14 -6
- package/src/lib/json-out.js +83 -0
- package/src/lib/pipeline.js +161 -0
- package/src/lib/prompt.js +18 -1
- package/src/lib/runtime.js +683 -0
- package/src/lib/scan.js +81 -33
- package/src/lib/spec-ops.js +94 -9
- package/src/lib/suggest.js +1 -0
- package/src/lib/templates.js +89 -0
- package/src/lib/usage.js +89 -0
- package/src/lib/version.js +1 -0
- package/templates/AGENTS.md.template +59 -53
- package/templates/contract.md.template +74 -3
- package/templates/spec.md.template +22 -0
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { exists, isDir, isFile, lineCount, read, relPath, walk, write } from "../lib/fs-ops.js";
|
|
4
|
-
import { locateTemplatesDir,
|
|
5
|
-
import {
|
|
5
|
+
import { locateTemplatesDir, listResolvedTemplates } from "../lib/templates.js";
|
|
6
|
+
import { adapterFiles, isHubPointer, listAdapterNames } from "../lib/adapters.js";
|
|
7
|
+
import {
|
|
8
|
+
surfaceBlock, findSurfaceBlock, surfaceAnchors, placeSurfaceBlock,
|
|
9
|
+
COMMAND_META, COMMAND_NAMES, SURFACE_LINE_BUDGET, surfaceMarkdown,
|
|
10
|
+
agentChangelogBlock, findAgentChangelogBlock,
|
|
11
|
+
} from "../lib/commands.js";
|
|
12
|
+
import { cliVersion } from "../lib/version.js";
|
|
13
|
+
import { ARTIFACT_CATEGORIES } from "../lib/index-json.js";
|
|
6
14
|
import { flagBool } from "../lib/args.js";
|
|
7
15
|
import { c } from "../lib/colors.js";
|
|
8
16
|
import { suggest } from "../lib/suggest.js";
|
|
@@ -28,6 +36,11 @@ const PRODUCT_SECTIONS = [
|
|
|
28
36
|
];
|
|
29
37
|
const INDEX_FIELDS = ["$schema_version", "project", "artifacts"];
|
|
30
38
|
|
|
39
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
40
|
+
// adding a command never requires editing the entrypoint — the gap that
|
|
41
|
+
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
42
|
+
export const flags = { boolean: ["json", "write"], string: [] };
|
|
43
|
+
|
|
31
44
|
export async function run(positional, flags) {
|
|
32
45
|
const sub = positional[0];
|
|
33
46
|
if (!SUBCOMMANDS.includes(sub)) {
|
|
@@ -75,6 +88,7 @@ function updateTemplates(flags) {
|
|
|
75
88
|
if (normalizeBlock(text.slice(block.start, block.end)) !== normalizeBlock(fresh)) {
|
|
76
89
|
plan.push({
|
|
77
90
|
describe: ["AGENTS.md: regenerate the doctrina:surface block (stale vs installed CLI)"],
|
|
91
|
+
preview: () => diffPreview(text.slice(block.start, block.end), fresh),
|
|
78
92
|
apply() {
|
|
79
93
|
const cur = read(agentsMdPath);
|
|
80
94
|
const b = findSurfaceBlock(cur);
|
|
@@ -84,17 +98,56 @@ function updateTemplates(flags) {
|
|
|
84
98
|
}
|
|
85
99
|
} else {
|
|
86
100
|
const legacy = findLegacySurfaceSection(text);
|
|
101
|
+
const anchors = templateAnchors();
|
|
87
102
|
plan.push({
|
|
88
103
|
describe: [legacy
|
|
89
104
|
? "AGENTS.md: replace the hand-written \"## Doctrina command surface\" section with the generated doctrina:surface block"
|
|
90
|
-
:
|
|
105
|
+
: `AGENTS.md: insert the generated doctrina:surface command block${anchors.after ? ` after "${anchors.after}"` : ""} (agents discover commands through this file)`],
|
|
106
|
+
preview: () => fresh,
|
|
91
107
|
apply() {
|
|
92
108
|
const cur = read(agentsMdPath);
|
|
93
109
|
const l = findLegacySurfaceSection(cur);
|
|
94
110
|
if (l) {
|
|
111
|
+
// The legacy section already sits where the CLI put it, so
|
|
112
|
+
// replacing it in place preserves the layout.
|
|
95
113
|
write(agentsMdPath, cur.slice(0, l.start) + fresh + "\n\n" + cur.slice(l.end), { force: true });
|
|
96
114
|
} else {
|
|
97
|
-
|
|
115
|
+
// Neither block nor legacy section: PLACE it at the canonical
|
|
116
|
+
// position instead of appending, so `upgrade` and `init` produce
|
|
117
|
+
// the same layout (C4).
|
|
118
|
+
write(agentsMdPath, placeSurfaceBlock(cur, fresh, anchors), { force: true });
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// The agent-facing changelog (M2): three to six lines stating only what
|
|
126
|
+
// alters agent behaviour in this version. An agent reading AGENTS.md after
|
|
127
|
+
// an upgrade learns what is new without being told to look — reported from
|
|
128
|
+
// real use, where a new command shipped and the driving LLM never knew.
|
|
129
|
+
if (isFile(agentsMdPath)) {
|
|
130
|
+
const cur = read(agentsMdPath);
|
|
131
|
+
const fresh = agentChangelogBlock(cliVersion());
|
|
132
|
+
const existing = findAgentChangelogBlock(cur);
|
|
133
|
+
if (fresh && (!existing || normalizeBlock(cur.slice(existing.start, existing.end)) !== normalizeBlock(fresh))) {
|
|
134
|
+
plan.push({
|
|
135
|
+
describe: [`AGENTS.md: ${existing ? "refresh" : "add"} the "What changed in ${cliVersion()}" block for agents`],
|
|
136
|
+
preview: () => fresh,
|
|
137
|
+
apply() {
|
|
138
|
+
const now = read(agentsMdPath);
|
|
139
|
+
const found = findAgentChangelogBlock(now);
|
|
140
|
+
if (found) {
|
|
141
|
+
write(agentsMdPath, now.slice(0, found.start) + fresh + now.slice(found.end), { force: true });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Place it immediately after the surface block, so "what exists"
|
|
145
|
+
// and "what just changed" are read together.
|
|
146
|
+
const sb = findSurfaceBlock(now);
|
|
147
|
+
if (sb) {
|
|
148
|
+
write(agentsMdPath, now.slice(0, sb.end) + "\n\n" + fresh + now.slice(sb.end), { force: true });
|
|
149
|
+
} else {
|
|
150
|
+
write(agentsMdPath, now.replace(/\s+$/, "") + "\n\n" + fresh + "\n", { force: true });
|
|
98
151
|
}
|
|
99
152
|
},
|
|
100
153
|
});
|
|
@@ -102,10 +155,12 @@ function updateTemplates(flags) {
|
|
|
102
155
|
}
|
|
103
156
|
|
|
104
157
|
// Markdown files: append stub sections for missing recommended headings.
|
|
105
|
-
|
|
158
|
+
/** @type {Array<[string, string[]]>} */
|
|
159
|
+
const markdownTargets = [
|
|
106
160
|
["AGENTS.md", AGENTS_SECTIONS],
|
|
107
161
|
[path.join(".doctrina", "product.md"), PRODUCT_SECTIONS],
|
|
108
|
-
]
|
|
162
|
+
];
|
|
163
|
+
for (const [rel, sections] of markdownTargets) {
|
|
109
164
|
const filePath = path.join(projectRoot, rel);
|
|
110
165
|
if (!isFile(filePath)) continue;
|
|
111
166
|
const text = read(filePath);
|
|
@@ -134,7 +189,7 @@ function updateTemplates(flags) {
|
|
|
134
189
|
if (idx.$schema_version === undefined) describe.push('index.json: add "$schema_version": "0.1.0"');
|
|
135
190
|
if (idx.project === undefined) describe.push(`index.json: add "project": "${path.basename(projectRoot)}"`);
|
|
136
191
|
if (idx.artifacts === undefined) describe.push('index.json: add empty "artifacts" object');
|
|
137
|
-
const categories =
|
|
192
|
+
const categories = ARTIFACT_CATEGORIES;
|
|
138
193
|
for (const cat of categories) {
|
|
139
194
|
if (idx.artifacts && idx.artifacts[cat] === undefined) {
|
|
140
195
|
describe.push(`index.json: add empty artifact category "${cat}"`);
|
|
@@ -165,8 +220,16 @@ function updateTemplates(flags) {
|
|
|
165
220
|
return 0;
|
|
166
221
|
}
|
|
167
222
|
|
|
168
|
-
for (const
|
|
169
|
-
|
|
223
|
+
for (const p of plan) {
|
|
224
|
+
for (const s of p.describe) {
|
|
225
|
+
console.log((writeMode ? c.green("update ") : c.yellow("would ")) + s);
|
|
226
|
+
}
|
|
227
|
+
// A one-line "would append" told the operator nothing about what was
|
|
228
|
+
// about to land in the file they read first. In preview, show the
|
|
229
|
+
// content and where it goes (C4).
|
|
230
|
+
if (!writeMode && p.preview) {
|
|
231
|
+
for (const line of p.preview().split("\n")) console.log(c.gray(" │ ") + line);
|
|
232
|
+
}
|
|
170
233
|
}
|
|
171
234
|
console.log("");
|
|
172
235
|
if (!writeMode) {
|
|
@@ -180,27 +243,32 @@ function updateTemplates(flags) {
|
|
|
180
243
|
}
|
|
181
244
|
|
|
182
245
|
function listTemplates() {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
246
|
+
// Resolution is a CHAIN (M1): a template under the project's
|
|
247
|
+
// `.doctrina/templates/` wins over the bundled copy, per file. Printing
|
|
248
|
+
// WHERE each one resolved from is what turns the override from a hidden
|
|
249
|
+
// behaviour into a usable one.
|
|
250
|
+
const projectRoot = process.cwd();
|
|
251
|
+
const rows = listResolvedTemplates(isDir(path.join(projectRoot, ".doctrina")) ? projectRoot : null);
|
|
252
|
+
if (rows.length === 0) {
|
|
253
|
+
console.error(c.red("error:") + " no templates found");
|
|
187
254
|
return 1;
|
|
188
255
|
}
|
|
189
|
-
|
|
256
|
+
|
|
257
|
+
console.log(c.bold("Templates resolved for this project:"));
|
|
190
258
|
console.log("");
|
|
191
|
-
|
|
192
|
-
const rows = [];
|
|
193
|
-
for (const f of files) {
|
|
194
|
-
const rel = relPath(dir, f);
|
|
195
|
-
const lines = lineCount(f);
|
|
196
|
-
rows.push({ rel, lines });
|
|
197
|
-
if (rel.length > maxLen) maxLen = rel.length;
|
|
198
|
-
}
|
|
259
|
+
const width = Math.max(...rows.map((r) => r.relativePath.length));
|
|
199
260
|
for (const r of rows) {
|
|
200
|
-
|
|
261
|
+
const source = r.source === "project"
|
|
262
|
+
? c.cyan("project") + (r.overrides ? c.gray(" (overrides bundled)") : "")
|
|
263
|
+
: c.gray("bundled");
|
|
264
|
+
console.log(` ${r.relativePath.padEnd(width + 2)}${String(lineCount(r.path)).padStart(4)} lines ${source}`);
|
|
201
265
|
}
|
|
202
266
|
console.log("");
|
|
203
|
-
|
|
267
|
+
const overridden = rows.filter((r) => r.source === "project").length;
|
|
268
|
+
console.log(c.gray(`${rows.length} templates · ${overridden} from this project's .doctrina/templates/`));
|
|
269
|
+
if (overridden === 0) {
|
|
270
|
+
console.log(c.gray("Drop a file there with the same relative path to override one — see docs/en/templates.md."));
|
|
271
|
+
}
|
|
204
272
|
return 0;
|
|
205
273
|
}
|
|
206
274
|
|
|
@@ -211,7 +279,16 @@ function checkTemplates() {
|
|
|
211
279
|
console.error(c.gray("hint: ") + "run `doctrina init` first");
|
|
212
280
|
return 1;
|
|
213
281
|
}
|
|
282
|
+
const { findings, ok } = collectFindings(projectRoot);
|
|
283
|
+
|
|
284
|
+
return report(findings, ok);
|
|
285
|
+
}
|
|
214
286
|
|
|
287
|
+
// The findings `templates check` reports, as structured records with an
|
|
288
|
+
// executable remedy. Exported so a test can seed each finding, run the
|
|
289
|
+
// remedy it names, and assert the finding clears — a remedy that cannot
|
|
290
|
+
// resolve its own finding is not a remedy (C2).
|
|
291
|
+
export function collectFindings(projectRoot) {
|
|
215
292
|
const findings = [];
|
|
216
293
|
const ok = [];
|
|
217
294
|
|
|
@@ -221,19 +298,19 @@ function checkTemplates() {
|
|
|
221
298
|
const text = read(agentsPath);
|
|
222
299
|
for (const heading of AGENTS_SECTIONS) {
|
|
223
300
|
if (hasHeading(text, heading)) ok.push(`AGENTS.md: ${heading}`);
|
|
224
|
-
else findings.push(`AGENTS.md missing recommended section "${heading}"
|
|
301
|
+
else findings.push({ message: `AGENTS.md missing recommended section "${heading}"`, remedy: "doctrina templates update --write" });
|
|
225
302
|
}
|
|
226
303
|
// Command-surface block: present and current vs the installed catalog.
|
|
227
304
|
const block = findSurfaceBlock(text);
|
|
228
305
|
if (!block) {
|
|
229
|
-
findings.push("AGENTS.md has no doctrina:surface block — agents discover commands through this file
|
|
306
|
+
findings.push({ message: "AGENTS.md has no doctrina:surface block — agents discover commands through this file", remedy: "doctrina templates update --write" });
|
|
230
307
|
} else if (normalizeBlock(text.slice(block.start, block.end)) !== normalizeBlock(surfaceBlock())) {
|
|
231
|
-
findings.push("AGENTS.md doctrina:surface block is stale vs the installed CLI
|
|
308
|
+
findings.push({ message: "AGENTS.md doctrina:surface block is stale vs the installed CLI", remedy: "doctrina templates update --write" });
|
|
232
309
|
} else {
|
|
233
310
|
ok.push("AGENTS.md: doctrina:surface block current");
|
|
234
311
|
}
|
|
235
312
|
} else {
|
|
236
|
-
findings.push("AGENTS.md missing at project root");
|
|
313
|
+
findings.push({ message: "AGENTS.md missing at project root", remedy: "doctrina init --force" });
|
|
237
314
|
}
|
|
238
315
|
|
|
239
316
|
// product.md sections
|
|
@@ -242,37 +319,59 @@ function checkTemplates() {
|
|
|
242
319
|
const text = read(productPath);
|
|
243
320
|
for (const heading of PRODUCT_SECTIONS) {
|
|
244
321
|
if (hasHeading(text, heading)) ok.push(`product.md: ${heading}`);
|
|
245
|
-
else findings.push(`.doctrina/product.md missing recommended section "${heading}"
|
|
322
|
+
else findings.push({ message: `.doctrina/product.md missing recommended section "${heading}"`, remedy: "doctrina templates update --write" });
|
|
246
323
|
}
|
|
247
324
|
} else {
|
|
248
|
-
findings.push(".doctrina/product.md missing");
|
|
325
|
+
findings.push({ message: ".doctrina/product.md missing", remedy: "doctrina init --force" });
|
|
249
326
|
}
|
|
250
327
|
|
|
251
|
-
// Installed agent adapters:
|
|
252
|
-
// (that is why one `upgrade --write` refresh of the hub reaches
|
|
253
|
-
// installed agent).
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
328
|
+
// Installed agent adapters: a HUB POINTER file must still reference
|
|
329
|
+
// AGENTS.md (that is why one `upgrade --write` refresh of the hub reaches
|
|
330
|
+
// every installed agent). Only files whose template declares the
|
|
331
|
+
// {{AGENTS_MD_PATH}} token are pointers; a slash-command shim reaches the
|
|
332
|
+
// hub through its parent pointer file and is not checked (C2).
|
|
333
|
+
for (const name of listAdapterNames(projectRoot)) {
|
|
334
|
+
const spec = adapterFiles(projectRoot, name);
|
|
335
|
+
if (!spec) continue;
|
|
336
|
+
for (const file of spec.files) {
|
|
337
|
+
const installed = path.join(projectRoot, file.relativePath);
|
|
338
|
+
if (!isFile(installed)) continue; // not installed — nothing to check
|
|
339
|
+
if (!isHubPointer(file)) continue; // command shim, not a pointer
|
|
340
|
+
if (read(installed).includes("AGENTS.md")) {
|
|
341
|
+
ok.push(`adapter ${file.relativePath}: points at AGENTS.md`);
|
|
342
|
+
} else {
|
|
343
|
+
findings.push({
|
|
344
|
+
message: `adapter ${file.relativePath} no longer references AGENTS.md — agents loading it will miss the hub`,
|
|
345
|
+
// Executable and verified: `adapter add --force` rewrites exactly
|
|
346
|
+
// this file from its template. A remedy that cannot resolve the
|
|
347
|
+
// finding it is attached to is not a remedy (C2).
|
|
348
|
+
remedy: `doctrina adapter add ${name} --force`,
|
|
349
|
+
});
|
|
273
350
|
}
|
|
274
351
|
}
|
|
275
|
-
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// M2: a command that cannot state its trigger has not earned a place on
|
|
355
|
+
// the surface. The block is an agent's only discovery surface, so both
|
|
356
|
+
// fields are required and the block has a declared size budget.
|
|
357
|
+
for (const name of COMMAND_NAMES) {
|
|
358
|
+
const meta = COMMAND_META[name];
|
|
359
|
+
if (!meta || !meta.purpose || !meta.when) {
|
|
360
|
+
findings.push({
|
|
361
|
+
message: `command "${name}" declares no ${!meta ? "purpose or when" : (!meta.purpose ? "purpose" : "when")} — the surface block cannot say when to reach for it`,
|
|
362
|
+
remedy: null,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const surfaceLines = surfaceMarkdown().split("\n").length;
|
|
367
|
+
if (surfaceLines > SURFACE_LINE_BUDGET) {
|
|
368
|
+
findings.push({
|
|
369
|
+
message: `the generated surface block is ${surfaceLines} lines (budget ${SURFACE_LINE_BUDGET}) — cut commands rather than raising the budget`,
|
|
370
|
+
remedy: null,
|
|
371
|
+
});
|
|
372
|
+
} else {
|
|
373
|
+
ok.push(`surface block within budget (${surfaceLines}/${SURFACE_LINE_BUDGET} lines)`);
|
|
374
|
+
}
|
|
276
375
|
|
|
277
376
|
// index.json schema fields
|
|
278
377
|
const indexPath = path.join(projectRoot, ".doctrina", "index.json");
|
|
@@ -281,21 +380,31 @@ function checkTemplates() {
|
|
|
281
380
|
const idx = JSON.parse(read(indexPath));
|
|
282
381
|
for (const field of INDEX_FIELDS) {
|
|
283
382
|
if (idx[field] !== undefined) ok.push(`index.json: ${field}`);
|
|
284
|
-
else findings.push(`.doctrina/index.json missing field "${field}"
|
|
383
|
+
else findings.push({ message: `.doctrina/index.json missing field "${field}"`, remedy: "doctrina templates update --write" });
|
|
285
384
|
}
|
|
286
385
|
if (idx.$schema_version && idx.$schema_version !== "0.1.0") {
|
|
287
|
-
findings.push(`.doctrina/index.json $schema_version is "${idx.$schema_version}" (expected "0.1.0")
|
|
386
|
+
findings.push({ message: `.doctrina/index.json $schema_version is "${idx.$schema_version}" (expected "0.1.0")`, remedy: null });
|
|
288
387
|
}
|
|
289
388
|
} catch (err) {
|
|
290
|
-
findings.push(`.doctrina/index.json failed to parse: ${err.message}
|
|
389
|
+
findings.push({ message: `.doctrina/index.json failed to parse: ${err.message}`, remedy: null });
|
|
291
390
|
}
|
|
292
391
|
} else {
|
|
293
|
-
findings.push(".doctrina/index.json missing");
|
|
392
|
+
findings.push({ message: ".doctrina/index.json missing", remedy: "doctrina index rebuild" });
|
|
294
393
|
}
|
|
295
394
|
|
|
296
395
|
// Output
|
|
396
|
+
return { findings, ok };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function report(findings, ok) {
|
|
297
400
|
for (const o of ok) console.log(c.green("✓ ") + o);
|
|
298
|
-
for (const f of findings)
|
|
401
|
+
for (const f of findings) {
|
|
402
|
+
console.log(c.yellow("✗ ") + f.message);
|
|
403
|
+
// Every finding names the command that RESOLVES it, or says plainly
|
|
404
|
+
// that repair is manual. A remedy the CLI cannot execute and verify is
|
|
405
|
+
// not a remedy — a test runs each one and asserts the finding clears.
|
|
406
|
+
console.log(c.gray(" fix: ") + (f.remedy ? c.cyan(f.remedy) : c.gray("manual repair — no command can fix this")));
|
|
407
|
+
}
|
|
299
408
|
console.log("");
|
|
300
409
|
|
|
301
410
|
if (findings.length === 0) {
|
|
@@ -303,12 +412,37 @@ function checkTemplates() {
|
|
|
303
412
|
return 0;
|
|
304
413
|
}
|
|
305
414
|
console.log(c.red("fail") + ` ${findings.length} recommendation${findings.length === 1 ? "" : "s"}`);
|
|
306
|
-
console.log(c.gray("hint: ") + "this command is read-only; review and add the missing sections by hand");
|
|
307
415
|
return 1;
|
|
308
416
|
}
|
|
309
417
|
|
|
418
|
+
// Line-level preview of what regenerating the block changes: only the
|
|
419
|
+
// lines that differ, so a stale-catalog refresh reads as the two or three
|
|
420
|
+
// commands that moved rather than as twenty unchanged lines.
|
|
421
|
+
function diffPreview(before, after) {
|
|
422
|
+
const a = before.split("\n");
|
|
423
|
+
const b = after.split("\n");
|
|
424
|
+
const aSet = new Set(a);
|
|
425
|
+
const bSet = new Set(b);
|
|
426
|
+
const out = [
|
|
427
|
+
...a.filter((line) => !bSet.has(line)).map((line) => `- ${line}`),
|
|
428
|
+
...b.filter((line) => !aSet.has(line)).map((line) => `+ ${line}`),
|
|
429
|
+
];
|
|
430
|
+
return out.length > 0 ? out.join("\n") : "(no textual change)";
|
|
431
|
+
}
|
|
432
|
+
|
|
310
433
|
// Whitespace/EOL-insensitive comparison for the surface block, so a CRLF
|
|
311
434
|
// checkout is not eternally "stale".
|
|
435
|
+
|
|
436
|
+
// The canonical position, read from the shipped template — the single
|
|
437
|
+
// place the intended layout is defined.
|
|
438
|
+
function templateAnchors() {
|
|
439
|
+
try {
|
|
440
|
+
return surfaceAnchors(read(path.join(locateTemplatesDir(), "AGENTS.md.template")));
|
|
441
|
+
} catch {
|
|
442
|
+
return { after: null, before: null };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
312
446
|
function normalizeBlock(s) {
|
|
313
447
|
return s.replace(/\r\n/g, "\n").replace(/[ \t]+$/gm, "").trim();
|
|
314
448
|
}
|
package/src/commands/trace.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import process from "node:process";
|
|
3
4
|
import { readdirSync } from "node:fs";
|
|
@@ -5,6 +6,8 @@ import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
|
|
|
5
6
|
import { specHeader } from "../lib/scan.js";
|
|
6
7
|
import { flagBool } from "../lib/args.js";
|
|
7
8
|
import { c } from "../lib/colors.js";
|
|
9
|
+
import { emitJson } from "../lib/json-out.js";
|
|
10
|
+
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
8
11
|
|
|
9
12
|
// Intent-provenance report (ADR 0006). `coverage` proves a criterion has a
|
|
10
13
|
// test; `trace` proves a capability traces to a stated intent. Together they
|
|
@@ -26,10 +29,19 @@ import { c } from "../lib/colors.js";
|
|
|
26
29
|
|
|
27
30
|
const ANCHOR_RE = /[A-Z]+\d+/g;
|
|
28
31
|
|
|
32
|
+
// Flags this command accepts. Declared HERE, with the command, so
|
|
33
|
+
// adding a command never requires editing the entrypoint — the gap that
|
|
34
|
+
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
35
|
+
// This command builds its own JSON payload; the entrypoint must not
|
|
36
|
+
// wrap it in the generic envelope.
|
|
37
|
+
export const jsonNative = true;
|
|
38
|
+
|
|
39
|
+
export const flags = { boolean: ["json", "strict"], string: [] };
|
|
40
|
+
|
|
29
41
|
export async function run(_positional, flags) {
|
|
30
42
|
const projectRoot = process.cwd();
|
|
31
43
|
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
32
|
-
throw
|
|
44
|
+
throw notADoctrinaProject();
|
|
33
45
|
}
|
|
34
46
|
const strict = flagBool(flags, "strict", false);
|
|
35
47
|
const json = flagBool(flags, "json", false);
|
|
@@ -43,7 +55,7 @@ export async function run(_positional, flags) {
|
|
|
43
55
|
// The feature is unused: do not nag a project that never opted in.
|
|
44
56
|
if (anchors.length === 0 && !anyRealizes) {
|
|
45
57
|
if (json) {
|
|
46
|
-
|
|
58
|
+
emitJson("trace", { anchors: [], dangling: [], untraceable: [], summary: summarize(projectRoot) });
|
|
47
59
|
return 0;
|
|
48
60
|
}
|
|
49
61
|
console.log(
|
|
@@ -77,7 +89,7 @@ export async function run(_positional, flags) {
|
|
|
77
89
|
if (json) {
|
|
78
90
|
const rows = anchors.map((a) => ({ id: a.id, realizedBy: (realizedBy.get(a.id) ?? []).sort() }));
|
|
79
91
|
const clean = rows.every((r) => r.realizedBy.length > 0) && dangling.length === 0 && untraceable.length === 0;
|
|
80
|
-
|
|
92
|
+
emitJson("trace", { anchors: rows, dangling, untraceable, summary: summarize(projectRoot) });
|
|
81
93
|
return clean ? 0 : strict ? 1 : 0;
|
|
82
94
|
}
|
|
83
95
|
|