doctrina-cli 0.15.0 → 0.16.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 +1 -1
- package/src/commands/adapter.js +24 -1
- package/src/commands/analyze.js +8 -199
- package/src/commands/change.js +143 -217
- package/src/commands/ci.js +182 -0
- package/src/commands/clarify.js +58 -21
- package/src/commands/close.js +291 -42
- package/src/commands/constitution.js +14 -91
- package/src/commands/context.js +278 -107
- package/src/commands/contract.js +95 -75
- package/src/commands/coverage.js +58 -273
- package/src/commands/decision.js +54 -7
- package/src/commands/doctor.js +292 -129
- package/src/commands/handoff.js +10 -57
- package/src/commands/hooks.js +11 -2
- package/src/commands/index-rebuild.js +9 -48
- package/src/commands/init.js +70 -13
- package/src/commands/intake.js +30 -92
- package/src/commands/metrics.js +53 -84
- package/src/commands/next.js +105 -147
- package/src/commands/prime.js +24 -91
- package/src/commands/report.js +50 -108
- package/src/commands/review.js +68 -36
- package/src/commands/search.js +18 -4
- package/src/commands/show.js +24 -9
- package/src/commands/skill.js +16 -25
- package/src/commands/spec.js +26 -5
- package/src/commands/status.js +51 -172
- package/src/commands/templates.js +31 -138
- package/src/commands/trace.js +36 -71
- package/src/commands/triage.js +7 -75
- package/src/commands/validate.js +14 -871
- package/src/commands/verify.js +121 -160
- package/src/commands/why.js +2 -2
- package/src/commands/work.js +234 -263
- package/src/index.js +106 -7
- package/src/lib/actions.js +484 -0
- package/src/lib/adapters.js +0 -11
- package/src/lib/adr-guard.js +19 -0
- package/src/lib/agent-changelog.js +133 -0
- package/src/lib/analysis.js +264 -0
- package/src/lib/change-ops.js +131 -0
- package/src/lib/commands.js +109 -6
- package/src/lib/config.js +206 -0
- package/src/lib/constitution-model.js +102 -0
- package/src/lib/coverage-model.js +392 -0
- package/src/lib/criteria.js +23 -0
- package/src/lib/doc-model.js +326 -0
- package/src/lib/docs-impact.js +196 -27
- package/src/lib/flag-catalog.js +0 -16
- package/src/lib/fs-ops.js +58 -1
- package/src/lib/gates.js +210 -24
- package/src/lib/git.js +132 -0
- package/src/lib/index-json.js +2 -19
- package/src/lib/intake-model.js +86 -0
- package/src/lib/json-out.js +100 -2
- package/src/lib/ledger.js +192 -0
- package/src/lib/lexicon.js +191 -0
- package/src/lib/metrics-model.js +159 -0
- package/src/lib/names.js +42 -0
- package/src/lib/playbook.js +97 -0
- package/src/lib/project.js +57 -0
- package/src/lib/reproducibility.js +137 -0
- package/src/lib/runtime.js +238 -11
- package/src/lib/scan.js +204 -13
- package/src/lib/signoff.js +147 -0
- package/src/lib/snapshot.js +209 -0
- package/src/lib/spec-ops.js +50 -5
- package/src/lib/templates-model.js +262 -0
- package/src/lib/trace-model.js +95 -0
- package/src/lib/triage-model.js +121 -0
- package/src/lib/usage.js +6 -4
- package/src/lib/validation-model.js +1299 -0
- package/src/lib/version.js +22 -0
- package/src/lib/views.js +410 -0
- package/src/lib/work-model.js +77 -0
- package/templates/AGENTS.md.template +20 -26
- package/templates/README.md +35 -0
- package/templates/change/proposal.md.template +1 -0
- package/templates/contract.md.template +7 -0
- package/templates/hooks/pre-commit.sample +6 -2
- package/templates/playbooks/bootstrap.md.template +43 -0
- package/templates/playbooks/chore.md.template +19 -0
- package/templates/playbooks/work.md.template +76 -0
package/src/commands/verify.js
CHANGED
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { readdirSync } from "node:fs";
|
|
5
|
-
import {
|
|
5
|
+
import { wantsJson } from "../lib/json-out.js";
|
|
6
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
6
7
|
import { exists, isDir, isFile, read, write, relPath } from "../lib/fs-ops.js";
|
|
7
8
|
import { flagBool, flagString } from "../lib/args.js";
|
|
8
9
|
import { today } from "../lib/dates.js";
|
|
9
10
|
import { c } from "../lib/colors.js";
|
|
11
|
+
import { loadSignoffs, saveSignoffs, recordSignoff, signoffState, short, SIGNOFF_REL } from "../lib/signoff.js";
|
|
10
12
|
import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
13
|
+
import { collectReproducibility } from "../lib/reproducibility.js";
|
|
11
14
|
|
|
12
15
|
// The build/verify gate `validate` is not. `validate` checks the shape of
|
|
13
16
|
// the artifact tree; it never runs the project, so a repo that does not
|
|
@@ -18,7 +21,7 @@ import { EXIT, notADoctrinaProject } from "../lib/exit-codes.js";
|
|
|
18
21
|
// `.doctrina/verify.json`, and `verify` runs exactly that, in order.
|
|
19
22
|
|
|
20
23
|
const CONFIG_REL = ".doctrina/verify.json";
|
|
21
|
-
|
|
24
|
+
|
|
22
25
|
|
|
23
26
|
const STARTER = {
|
|
24
27
|
$comment:
|
|
@@ -53,7 +56,7 @@ export async function run(_positional, flags) {
|
|
|
53
56
|
// fresh `npm install` differ from the dirty machine, without doing a real
|
|
54
57
|
// (slow, package-manager-specific) clean install.
|
|
55
58
|
if (flagBool(flags, "clean", false)) {
|
|
56
|
-
return
|
|
59
|
+
return renderReproducibility(projectRoot);
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
const configPath = path.join(projectRoot, CONFIG_REL);
|
|
@@ -117,12 +120,11 @@ export async function run(_positional, flags) {
|
|
|
117
120
|
return 1;
|
|
118
121
|
}
|
|
119
122
|
|
|
120
|
-
// Sign-off store for manual checks
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
123
|
+
// Sign-off store for manual checks. Since change 0039 a record carries what
|
|
124
|
+
// it covered and the commit it covered it at, so the signature can expire
|
|
125
|
+
// (lib/signoff.js); a pre-expiry record is read unchanged and reported as
|
|
126
|
+
// unverifiable rather than silently trusted.
|
|
127
|
+
const signoffs = loadSignoffs(projectRoot);
|
|
126
128
|
|
|
127
129
|
// --signoff "<name>=<note>" records a manual check's human/eval sign-off
|
|
128
130
|
// (date stamped today) and exits. The name must be a declared manual check.
|
|
@@ -140,9 +142,22 @@ export async function run(_positional, flags) {
|
|
|
140
142
|
: `add one: { "name": "${name || "quality"}", "type": "manual", "rubric": "<the question>" }`));
|
|
141
143
|
return 1;
|
|
142
144
|
}
|
|
143
|
-
signoffs[name] =
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
signoffs[name] = recordSignoff(projectRoot, target, note);
|
|
146
|
+
saveSignoffs(projectRoot, signoffs);
|
|
147
|
+
const stamped = signoffs[name];
|
|
148
|
+
console.log(c.green("signed off") + ` ${name} on ${stamped.date}${note ? `: ${note}` : ""}`);
|
|
149
|
+
// Say what the signature is anchored to, so its expiry is not a surprise
|
|
150
|
+
// later: an unanchored signature is one nothing can hold to the code.
|
|
151
|
+
if (stamped.sha && stamped.paths) {
|
|
152
|
+
console.log(c.gray(` covers ${stamped.paths.join(", ")} as of ${short(stamped.sha)}`));
|
|
153
|
+
console.log(c.gray(" expires when one of those paths changes"));
|
|
154
|
+
} else if (!stamped.paths) {
|
|
155
|
+
console.log(c.yellow("warn:") + ` "${name}" declares no "paths" — the sign-off cannot expire, ` +
|
|
156
|
+
"so `verify` will report it as unverifiable rather than passing");
|
|
157
|
+
console.log(c.gray(` fix: add "paths": ["src/..."] to the check in ${CONFIG_REL}`));
|
|
158
|
+
} else {
|
|
159
|
+
console.log(c.yellow("warn:") + " not a git repository — nothing can tell when this signature goes stale");
|
|
160
|
+
}
|
|
146
161
|
return 0;
|
|
147
162
|
}
|
|
148
163
|
|
|
@@ -178,13 +193,29 @@ export async function run(_positional, flags) {
|
|
|
178
193
|
// can require the human/eval sign-off without blocking local runs.
|
|
179
194
|
if (ch.type === "manual") {
|
|
180
195
|
console.log(c.gray(`──── ${name}: `) + c.gray(`manual${ch.rubric ? " — " + ch.rubric : ""}`));
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
196
|
+
// Four states, one rule: only a signature that still demonstrably
|
|
197
|
+
// covers the code passes. The other three warn by default and fail
|
|
198
|
+
// under --strict — which is what `pending` already did, so a manual
|
|
199
|
+
// check keeps ONE rule rather than gaining a second.
|
|
200
|
+
const verdict = signoffState(projectRoot, ch, signoffs[name]);
|
|
201
|
+
const so = verdict.record;
|
|
202
|
+
const resign = c.gray(` (doctrina verify --signoff "${name}=<note>")`);
|
|
203
|
+
if (verdict.state === "fresh") {
|
|
204
|
+
console.log(c.green(`✓ ${name}`) + c.gray(` — signed off ${so.date}${so.note ? `: ${so.note}` : ""}`) +
|
|
205
|
+
c.gray(` · still covers ${so.paths.join(", ")} at ${short(so.sha)}`));
|
|
206
|
+
results.push({ name, ok: true, kind: "manual", signoff: "fresh" });
|
|
207
|
+
} else if (verdict.state === "expired") {
|
|
208
|
+
console.log(c.yellow(`○ ${name} — sign-off expired`) +
|
|
209
|
+
c.gray(` (signed ${so.date} at ${short(so.sha)}; changed since: ${verdict.changed.slice(0, 3).join(", ")}` +
|
|
210
|
+
`${verdict.changed.length > 3 ? `, +${verdict.changed.length - 3} more` : ""})`) + resign);
|
|
211
|
+
results.push({ name, ok: false, kind: "manual", pending: true, signoff: "expired" });
|
|
212
|
+
} else if (verdict.state === "unverifiable") {
|
|
213
|
+
console.log(c.yellow(`○ ${name} — sign-off cannot be verified`) +
|
|
214
|
+
c.gray(` (signed ${so.date}; ${verdict.why})`) + resign);
|
|
215
|
+
results.push({ name, ok: false, kind: "manual", pending: true, signoff: "unverifiable" });
|
|
185
216
|
} else {
|
|
186
|
-
console.log(c.yellow(`○ ${name} — pending sign-off`) +
|
|
187
|
-
results.push({ name, ok: false, kind: "manual", pending: true });
|
|
217
|
+
console.log(c.yellow(`○ ${name} — pending sign-off`) + resign);
|
|
218
|
+
results.push({ name, ok: false, kind: "manual", pending: true, signoff: "pending" });
|
|
188
219
|
}
|
|
189
220
|
continue;
|
|
190
221
|
}
|
|
@@ -198,24 +229,24 @@ export async function run(_positional, flags) {
|
|
|
198
229
|
// monorepo target a sub-package, e.g. { run: "node --test", cwd:
|
|
199
230
|
// "packages/api" } — the multi-service shape the contract artifact
|
|
200
231
|
// also serves.
|
|
201
|
-
// A check that declares OUTPUT EXPECTATIONS must have its output
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
232
|
+
// A check that declares OUTPUT EXPECTATIONS must have its output READ,
|
|
233
|
+
// which is not the same as withholding it. The first cut ran such a
|
|
234
|
+
// check piped and echoed the whole buffer at the end — forty seconds of
|
|
235
|
+
// blank terminal on this project's own suite, and worse on anything
|
|
236
|
+
// slower. Reading and showing are independent: the tee streams every
|
|
237
|
+
// chunk as it arrives AND accumulates it for the match.
|
|
205
238
|
const expectation = parseExpectation(ch);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (res.stderr) process.stderr.write(res.stderr);
|
|
218
|
-
}
|
|
239
|
+
// Under --json the child is TEED as well (change 0114): with
|
|
240
|
+
// `stdio: "inherit"` its bytes go straight to the file descriptors,
|
|
241
|
+
// past the envelope's capture, so a failing check's diagnostics landed
|
|
242
|
+
// on the real stderr while the JSON said `"stderr": []`.
|
|
243
|
+
const res = expectation || wantsJson(flags)
|
|
244
|
+
? await runTee(ch.run, ch.cwd ? path.resolve(projectRoot, ch.cwd) : projectRoot)
|
|
245
|
+
: spawnSync(ch.run, {
|
|
246
|
+
cwd: ch.cwd ? path.resolve(projectRoot, ch.cwd) : projectRoot,
|
|
247
|
+
shell: true,
|
|
248
|
+
stdio: "inherit",
|
|
249
|
+
});
|
|
219
250
|
|
|
220
251
|
let ok = !res.error && res.status === 0;
|
|
221
252
|
/** @type {string | null} */
|
|
@@ -226,7 +257,9 @@ export async function run(_positional, flags) {
|
|
|
226
257
|
// matched nothing prints "0 scenarios" and exits 0, and every gate
|
|
227
258
|
// above this one calls that a pass. An expectation makes the run's
|
|
228
259
|
// own output part of the verdict.
|
|
229
|
-
|
|
260
|
+
// The tee already interleaves both streams in the order they arrived,
|
|
261
|
+
// which is the order a human read them in.
|
|
262
|
+
const output = res.output ?? `${res.stdout ?? ""}\n${res.stderr ?? ""}`;
|
|
230
263
|
const verdict = judgeOutput(expectation, output);
|
|
231
264
|
if (!verdict.ok) {
|
|
232
265
|
ok = false;
|
|
@@ -266,6 +299,47 @@ export async function run(_positional, flags) {
|
|
|
266
299
|
return 1;
|
|
267
300
|
}
|
|
268
301
|
|
|
302
|
+
// Run a command, streaming its output to the terminal AS IT ARRIVES while
|
|
303
|
+
// accumulating a copy for an expectation to match against.
|
|
304
|
+
//
|
|
305
|
+
// `spawnSync` cannot do this: it either inherits the streams (visible,
|
|
306
|
+
// unreadable) or pipes them (readable, invisible until it exits). The async
|
|
307
|
+
// spawn gives both, at the cost of this function existing.
|
|
308
|
+
//
|
|
309
|
+
// Both streams accumulate into ONE buffer in arrival order, so a pattern
|
|
310
|
+
// spanning them matches the same text the operator saw, and stderr is
|
|
311
|
+
// echoed to stderr so redirection still behaves.
|
|
312
|
+
//
|
|
313
|
+
// @returns {Promise<{status: number|null, output: string, error?: Error}>}
|
|
314
|
+
function runTee(command, cwd) {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
let child;
|
|
317
|
+
try {
|
|
318
|
+
child = spawn(command, { cwd, shell: true, stdio: ["inherit", "pipe", "pipe"] });
|
|
319
|
+
} catch (err) {
|
|
320
|
+
resolve({ status: null, output: "", error: /** @type {Error} */ (err) });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let output = "";
|
|
325
|
+
const tee = (stream, sink) => {
|
|
326
|
+
if (!stream) return;
|
|
327
|
+
stream.setEncoding("utf8");
|
|
328
|
+
stream.on("data", (chunk) => {
|
|
329
|
+
output += chunk;
|
|
330
|
+
sink.write(chunk);
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
tee(child.stdout, process.stdout);
|
|
334
|
+
tee(child.stderr, process.stderr);
|
|
335
|
+
|
|
336
|
+
child.on("error", (err) => resolve({ status: null, output, error: err }));
|
|
337
|
+
// "close" rather than "exit": it fires once the streams are drained, so
|
|
338
|
+
// no trailing output is dropped from the match or the terminal.
|
|
339
|
+
child.on("close", (status) => resolve({ status, output }));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
269
343
|
// An `expect` block, normalised and compiled, or null when the check does
|
|
270
344
|
// not declare one. Runner-agnostic by construction: Doctrina supplies no
|
|
271
345
|
// patterns of its own and knows nothing about what the output means — the
|
|
@@ -307,62 +381,19 @@ export function judgeOutput(expectation, output) {
|
|
|
307
381
|
return { ok: true };
|
|
308
382
|
}
|
|
309
383
|
|
|
310
|
-
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
function reproducibilityLint(projectRoot) {
|
|
316
|
-
const pkgPaths = findPackageJsons(projectRoot);
|
|
384
|
+
|
|
385
|
+
// The reproducibility lint, rendered. The lint itself is a collection in
|
|
386
|
+
// lib/reproducibility.js so `doctor` can ask for it in process.
|
|
387
|
+
function renderReproducibility(projectRoot) {
|
|
388
|
+
const { packages, findings } = collectReproducibility(projectRoot);
|
|
317
389
|
console.log(c.bold("doctrina verify --clean") + c.gray(" — reproducibility lint (static, not a real fresh install)"));
|
|
318
390
|
console.log("");
|
|
319
|
-
|
|
320
|
-
if (pkgPaths.length === 0) {
|
|
391
|
+
if (packages === 0) {
|
|
321
392
|
console.log(c.gray("no package.json found — nothing this lint can check (it knows Node/npm footguns)"));
|
|
322
393
|
return 0;
|
|
323
394
|
}
|
|
324
|
-
|
|
325
|
-
const findings = [];
|
|
326
|
-
for (const pkgPath of pkgPaths) {
|
|
327
|
-
const rel = relPath(projectRoot, pkgPath);
|
|
328
|
-
let pkg;
|
|
329
|
-
try {
|
|
330
|
-
pkg = JSON.parse(read(pkgPath));
|
|
331
|
-
} catch (err) {
|
|
332
|
-
findings.push(`${rel}: not valid JSON (${err.message})`);
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
const scripts = pkg.scripts ?? {};
|
|
336
|
-
const hasInstallBuild = typeof scripts.prepare === "string" || typeof scripts.prepack === "string";
|
|
337
|
-
|
|
338
|
-
// 1. Entry point into a build output with nothing to build it on install.
|
|
339
|
-
const built = entryIntoBuildDir(pkg);
|
|
340
|
-
if (built && !hasInstallBuild) {
|
|
341
|
-
findings.push(
|
|
342
|
-
`${rel}: ${built.field} → \`${built.value}\` is a build output, but no "prepare"/"prepack" ` +
|
|
343
|
-
`script builds it on install — a fresh install/clone won't have it (add a prepare script, or commit the output)`,
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// 2. Codegen dependency with no install hook that generates.
|
|
348
|
-
const deps = {
|
|
349
|
-
...pkg.dependencies, ...pkg.devDependencies,
|
|
350
|
-
...pkg.peerDependencies, ...pkg.optionalDependencies,
|
|
351
|
-
};
|
|
352
|
-
for (const [dep, gen] of Object.entries(CODEGEN_DEPS)) {
|
|
353
|
-
if (!(dep in deps)) continue;
|
|
354
|
-
const installHook = `${scripts.postinstall ?? ""} ${scripts.prepare ?? ""}`;
|
|
355
|
-
if (!gen.test(installHook)) {
|
|
356
|
-
findings.push(
|
|
357
|
-
`${rel}: depends on "${dep}" but no "postinstall"/"prepare" runs its codegen ` +
|
|
358
|
-
`(\`${gen.source.replace(/\\s\+/g, " ")}\`) — a fresh install has no generated output`,
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
395
|
if (findings.length === 0) {
|
|
365
|
-
console.log(c.green("ok") + ` ${
|
|
396
|
+
console.log(c.green("ok") + ` ${packages} package.json file${packages === 1 ? "" : "s"} — no reproducibility risks found`);
|
|
366
397
|
return 0;
|
|
367
398
|
}
|
|
368
399
|
for (const f of findings) console.log(` ${c.yellow("!")} ${f}`);
|
|
@@ -371,76 +402,6 @@ function reproducibilityLint(projectRoot) {
|
|
|
371
402
|
return 1;
|
|
372
403
|
}
|
|
373
404
|
|
|
374
|
-
// Known codegen dependencies → the command an install hook must run so a
|
|
375
|
-
// fresh install produces their generated output.
|
|
376
|
-
const CODEGEN_DEPS = {
|
|
377
|
-
"@prisma/client": /prisma\s+generate/,
|
|
378
|
-
"prisma": /prisma\s+generate/,
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
const BUILD_DIR_RE = /(?:^|\/)(?:dist|build|out)\//;
|
|
382
|
-
|
|
383
|
-
// The first entry-point field that points into a build-output directory, or
|
|
384
|
-
// null. Scans main/module/types/typings, bin (string or map), and exports
|
|
385
|
-
// (recursively, string leaves only).
|
|
386
|
-
function entryIntoBuildDir(pkg) {
|
|
387
|
-
for (const field of ["main", "module", "types", "typings"]) {
|
|
388
|
-
if (typeof pkg[field] === "string" && BUILD_DIR_RE.test(pkg[field])) {
|
|
389
|
-
return { field, value: pkg[field] };
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
if (typeof pkg.bin === "string" && BUILD_DIR_RE.test(pkg.bin)) return { field: "bin", value: pkg.bin };
|
|
393
|
-
if (pkg.bin && typeof pkg.bin === "object") {
|
|
394
|
-
for (const v of Object.values(pkg.bin)) {
|
|
395
|
-
if (typeof v === "string" && BUILD_DIR_RE.test(v)) return { field: "bin", value: v };
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
const fromExports = scanExports(pkg.exports);
|
|
399
|
-
if (fromExports) return { field: "exports", value: fromExports };
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
function scanExports(node) {
|
|
404
|
-
if (typeof node === "string") return BUILD_DIR_RE.test(node) ? node : null;
|
|
405
|
-
if (node && typeof node === "object") {
|
|
406
|
-
for (const v of Object.values(node)) {
|
|
407
|
-
const hit = scanExports(v);
|
|
408
|
-
if (hit) return hit;
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Bounded walk for package.json files: skip dependency, build, and VCS
|
|
415
|
-
// directories so the lint stays fast and ignores vendored manifests.
|
|
416
|
-
const LINT_SKIP_DIRS = new Set([
|
|
417
|
-
".git", "node_modules", "dist", "build", "out", "vendor", ".next",
|
|
418
|
-
"coverage", ".venv", "venv", "__pycache__", "target", ".doctrina",
|
|
419
|
-
]);
|
|
420
|
-
|
|
421
|
-
function findPackageJsons(projectRoot) {
|
|
422
|
-
const found = [];
|
|
423
|
-
const stack = [projectRoot];
|
|
424
|
-
while (stack.length) {
|
|
425
|
-
const dir = stack.pop();
|
|
426
|
-
let entries;
|
|
427
|
-
try {
|
|
428
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
429
|
-
} catch {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
for (const entry of entries) {
|
|
433
|
-
const full = path.join(dir, entry.name);
|
|
434
|
-
if (entry.isDirectory()) {
|
|
435
|
-
if (!LINT_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) stack.push(full);
|
|
436
|
-
} else if (entry.name === "package.json") {
|
|
437
|
-
found.push(full);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return found.sort();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
405
|
export const help = `
|
|
445
406
|
Usage: doctrina verify [--list] [--init] [--clean] [--strict] [--signoff "<name>=<note>"] [--force]
|
|
446
407
|
|
|
@@ -476,10 +437,10 @@ verdict:
|
|
|
476
437
|
|
|
477
438
|
Doctrina supplies no patterns of its own and knows nothing about what the
|
|
478
439
|
output means — the project declares the line that proves its run was real,
|
|
479
|
-
so this works for any runner in any language.
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
440
|
+
so this works for any runner in any language. An expect-carrying check
|
|
441
|
+
still streams: its output is teed to the terminal as it arrives while a
|
|
442
|
+
copy accumulates for the match. An "expect" pattern that is not a valid
|
|
443
|
+
regular expression fails at config time, not silently.
|
|
483
444
|
|
|
484
445
|
Flags:
|
|
485
446
|
--init Scaffold a starter ${CONFIG_REL} (refuses to overwrite without --force)
|
package/src/commands/why.js
CHANGED
|
@@ -9,7 +9,7 @@ import { parseAcceptanceCriteria, isVerified } from "../lib/criteria.js";
|
|
|
9
9
|
import * as idx from "../lib/index-json.js";
|
|
10
10
|
import { c } from "../lib/colors.js";
|
|
11
11
|
import { suggest } from "../lib/suggest.js";
|
|
12
|
-
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
12
|
+
import { notADoctrinaProject, EXIT } from "../lib/exit-codes.js";
|
|
13
13
|
|
|
14
14
|
// Provenance explainer (review 2026-06-27 passive-user feature #5): answer
|
|
15
15
|
// "why does this capability exist / why was it built this way?" by assembling
|
|
@@ -51,7 +51,7 @@ export async function run(positional, _flags) {
|
|
|
51
51
|
const guess = suggest(cap, known);
|
|
52
52
|
if (guess) console.error(c.gray("hint: ") + `did you mean "${guess}"?`);
|
|
53
53
|
else if (known.length) console.error(c.gray("known: ") + known.sort().join(", "));
|
|
54
|
-
return
|
|
54
|
+
return EXIT.USAGE;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const text = read(specPath);
|