doctrina-cli 0.15.1 → 0.17.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 +27 -72
- package/package.json +3 -3
- package/src/commands/adapter.js +27 -3
- package/src/commands/analyze.js +16 -212
- package/src/commands/change.js +144 -267
- package/src/commands/ci.js +182 -0
- package/src/commands/clarify.js +65 -21
- package/src/commands/close.js +349 -45
- package/src/commands/completion.js +8 -2
- package/src/commands/context.js +284 -111
- package/src/commands/contract.js +95 -75
- package/src/commands/coverage.js +58 -273
- package/src/commands/decision.js +66 -7
- package/src/commands/doctor.js +293 -130
- package/src/commands/handoff.js +10 -57
- package/src/commands/hooks.js +17 -3
- package/src/commands/index-rebuild.js +55 -44
- package/src/commands/init.js +88 -16
- package/src/commands/intake.js +85 -99
- package/src/commands/intent.js +14 -1
- package/src/commands/metrics.js +65 -88
- package/src/commands/next.js +105 -147
- package/src/commands/prime.js +23 -91
- package/src/commands/report.js +40 -109
- 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 +23 -39
- package/src/commands/spec.js +38 -11
- package/src/commands/status.js +69 -171
- package/src/commands/templates.js +35 -148
- package/src/commands/trace.js +36 -71
- package/src/commands/triage.js +4 -82
- package/src/commands/upgrade.js +31 -6
- package/src/commands/validate.js +44 -877
- package/src/commands/verify.js +60 -138
- package/src/commands/why.js +2 -2
- package/src/commands/work.js +243 -267
- package/src/index.js +139 -12
- package/src/lib/actions.js +490 -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/args.js +14 -0
- package/src/lib/change-ops.js +133 -0
- package/src/lib/commands.js +135 -10
- package/src/lib/config.js +206 -0
- package/src/lib/constitution-model.js +102 -0
- package/src/lib/coverage-model.js +431 -0
- package/src/lib/criteria.js +23 -0
- package/src/lib/doc-model.js +370 -3
- package/src/lib/docs-impact.js +270 -27
- package/src/lib/ears.js +24 -1
- package/src/lib/flag-catalog.js +0 -16
- package/src/lib/fs-ops.js +58 -1
- package/src/lib/gates.js +231 -29
- package/src/lib/git.js +132 -0
- package/src/lib/index-json.js +42 -24
- package/src/lib/intake-model.js +155 -0
- package/src/lib/json-out.js +100 -2
- package/src/lib/ledger.js +192 -0
- package/src/lib/lexicon.js +228 -0
- package/src/lib/metrics-model.js +189 -0
- package/src/lib/names.js +42 -0
- package/src/lib/playbook.js +97 -0
- package/src/lib/project.js +105 -0
- package/src/lib/reproducibility.js +137 -0
- package/src/lib/runtime.js +191 -5
- package/src/lib/scan.js +328 -25
- package/src/lib/signoff.js +147 -0
- package/src/lib/snapshot.js +231 -0
- package/src/lib/spec-ops.js +72 -8
- package/src/lib/templates-model.js +289 -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 +1381 -0
- package/src/lib/version.js +22 -0
- package/src/lib/views.js +413 -0
- package/src/lib/work-model.js +102 -0
- package/templates/AGENTS.md.template +21 -28
- package/templates/README.md +35 -0
- package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
- package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
- package/templates/change/proposal.md.template +16 -0
- package/templates/change/tasks.md.template +3 -8
- package/templates/contract.md.template +2 -2
- package/templates/hooks/pre-commit.sample +16 -2
- package/templates/playbooks/bootstrap.md.template +47 -0
- package/templates/playbooks/chore.md.template +24 -0
- package/templates/playbooks/work.md.template +83 -0
- package/src/commands/constitution.js +0 -145
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 { wantsJson } from "../lib/json-out.js";
|
|
5
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
|
}
|
|
@@ -205,7 +236,11 @@ export async function run(_positional, flags) {
|
|
|
205
236
|
// slower. Reading and showing are independent: the tee streams every
|
|
206
237
|
// chunk as it arrives AND accumulates it for the match.
|
|
207
238
|
const expectation = parseExpectation(ch);
|
|
208
|
-
|
|
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)
|
|
209
244
|
? await runTee(ch.run, ch.cwd ? path.resolve(projectRoot, ch.cwd) : projectRoot)
|
|
210
245
|
: spawnSync(ch.run, {
|
|
211
246
|
cwd: ch.cwd ? path.resolve(projectRoot, ch.cwd) : projectRoot,
|
|
@@ -346,62 +381,19 @@ export function judgeOutput(expectation, output) {
|
|
|
346
381
|
return { ok: true };
|
|
347
382
|
}
|
|
348
383
|
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
function reproducibilityLint(projectRoot) {
|
|
355
|
-
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);
|
|
356
389
|
console.log(c.bold("doctrina verify --clean") + c.gray(" — reproducibility lint (static, not a real fresh install)"));
|
|
357
390
|
console.log("");
|
|
358
|
-
|
|
359
|
-
if (pkgPaths.length === 0) {
|
|
391
|
+
if (packages === 0) {
|
|
360
392
|
console.log(c.gray("no package.json found — nothing this lint can check (it knows Node/npm footguns)"));
|
|
361
393
|
return 0;
|
|
362
394
|
}
|
|
363
|
-
|
|
364
|
-
const findings = [];
|
|
365
|
-
for (const pkgPath of pkgPaths) {
|
|
366
|
-
const rel = relPath(projectRoot, pkgPath);
|
|
367
|
-
let pkg;
|
|
368
|
-
try {
|
|
369
|
-
pkg = JSON.parse(read(pkgPath));
|
|
370
|
-
} catch (err) {
|
|
371
|
-
findings.push(`${rel}: not valid JSON (${err.message})`);
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const scripts = pkg.scripts ?? {};
|
|
375
|
-
const hasInstallBuild = typeof scripts.prepare === "string" || typeof scripts.prepack === "string";
|
|
376
|
-
|
|
377
|
-
// 1. Entry point into a build output with nothing to build it on install.
|
|
378
|
-
const built = entryIntoBuildDir(pkg);
|
|
379
|
-
if (built && !hasInstallBuild) {
|
|
380
|
-
findings.push(
|
|
381
|
-
`${rel}: ${built.field} → \`${built.value}\` is a build output, but no "prepare"/"prepack" ` +
|
|
382
|
-
`script builds it on install — a fresh install/clone won't have it (add a prepare script, or commit the output)`,
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// 2. Codegen dependency with no install hook that generates.
|
|
387
|
-
const deps = {
|
|
388
|
-
...pkg.dependencies, ...pkg.devDependencies,
|
|
389
|
-
...pkg.peerDependencies, ...pkg.optionalDependencies,
|
|
390
|
-
};
|
|
391
|
-
for (const [dep, gen] of Object.entries(CODEGEN_DEPS)) {
|
|
392
|
-
if (!(dep in deps)) continue;
|
|
393
|
-
const installHook = `${scripts.postinstall ?? ""} ${scripts.prepare ?? ""}`;
|
|
394
|
-
if (!gen.test(installHook)) {
|
|
395
|
-
findings.push(
|
|
396
|
-
`${rel}: depends on "${dep}" but no "postinstall"/"prepare" runs its codegen ` +
|
|
397
|
-
`(\`${gen.source.replace(/\\s\+/g, " ")}\`) — a fresh install has no generated output`,
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
395
|
if (findings.length === 0) {
|
|
404
|
-
console.log(c.green("ok") + ` ${
|
|
396
|
+
console.log(c.green("ok") + ` ${packages} package.json file${packages === 1 ? "" : "s"} — no reproducibility risks found`);
|
|
405
397
|
return 0;
|
|
406
398
|
}
|
|
407
399
|
for (const f of findings) console.log(` ${c.yellow("!")} ${f}`);
|
|
@@ -410,76 +402,6 @@ function reproducibilityLint(projectRoot) {
|
|
|
410
402
|
return 1;
|
|
411
403
|
}
|
|
412
404
|
|
|
413
|
-
// Known codegen dependencies → the command an install hook must run so a
|
|
414
|
-
// fresh install produces their generated output.
|
|
415
|
-
const CODEGEN_DEPS = {
|
|
416
|
-
"@prisma/client": /prisma\s+generate/,
|
|
417
|
-
"prisma": /prisma\s+generate/,
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
const BUILD_DIR_RE = /(?:^|\/)(?:dist|build|out)\//;
|
|
421
|
-
|
|
422
|
-
// The first entry-point field that points into a build-output directory, or
|
|
423
|
-
// null. Scans main/module/types/typings, bin (string or map), and exports
|
|
424
|
-
// (recursively, string leaves only).
|
|
425
|
-
function entryIntoBuildDir(pkg) {
|
|
426
|
-
for (const field of ["main", "module", "types", "typings"]) {
|
|
427
|
-
if (typeof pkg[field] === "string" && BUILD_DIR_RE.test(pkg[field])) {
|
|
428
|
-
return { field, value: pkg[field] };
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
if (typeof pkg.bin === "string" && BUILD_DIR_RE.test(pkg.bin)) return { field: "bin", value: pkg.bin };
|
|
432
|
-
if (pkg.bin && typeof pkg.bin === "object") {
|
|
433
|
-
for (const v of Object.values(pkg.bin)) {
|
|
434
|
-
if (typeof v === "string" && BUILD_DIR_RE.test(v)) return { field: "bin", value: v };
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
const fromExports = scanExports(pkg.exports);
|
|
438
|
-
if (fromExports) return { field: "exports", value: fromExports };
|
|
439
|
-
return null;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
function scanExports(node) {
|
|
443
|
-
if (typeof node === "string") return BUILD_DIR_RE.test(node) ? node : null;
|
|
444
|
-
if (node && typeof node === "object") {
|
|
445
|
-
for (const v of Object.values(node)) {
|
|
446
|
-
const hit = scanExports(v);
|
|
447
|
-
if (hit) return hit;
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return null;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Bounded walk for package.json files: skip dependency, build, and VCS
|
|
454
|
-
// directories so the lint stays fast and ignores vendored manifests.
|
|
455
|
-
const LINT_SKIP_DIRS = new Set([
|
|
456
|
-
".git", "node_modules", "dist", "build", "out", "vendor", ".next",
|
|
457
|
-
"coverage", ".venv", "venv", "__pycache__", "target", ".doctrina",
|
|
458
|
-
]);
|
|
459
|
-
|
|
460
|
-
function findPackageJsons(projectRoot) {
|
|
461
|
-
const found = [];
|
|
462
|
-
const stack = [projectRoot];
|
|
463
|
-
while (stack.length) {
|
|
464
|
-
const dir = stack.pop();
|
|
465
|
-
let entries;
|
|
466
|
-
try {
|
|
467
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
468
|
-
} catch {
|
|
469
|
-
continue;
|
|
470
|
-
}
|
|
471
|
-
for (const entry of entries) {
|
|
472
|
-
const full = path.join(dir, entry.name);
|
|
473
|
-
if (entry.isDirectory()) {
|
|
474
|
-
if (!LINT_SKIP_DIRS.has(entry.name) && !entry.name.startsWith(".")) stack.push(full);
|
|
475
|
-
} else if (entry.name === "package.json") {
|
|
476
|
-
found.push(full);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return found.sort();
|
|
481
|
-
}
|
|
482
|
-
|
|
483
405
|
export const help = `
|
|
484
406
|
Usage: doctrina verify [--list] [--init] [--clean] [--strict] [--signoff "<name>=<note>"] [--force]
|
|
485
407
|
|
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);
|