doctrina-cli 0.15.1 → 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 -212
- 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 +4 -82
- package/src/commands/validate.js +14 -871
- package/src/commands/verify.js +60 -138
- 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 +56 -4
- 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 +181 -23
- 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 +191 -5
- 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/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/trace.js
CHANGED
|
@@ -8,6 +8,11 @@ import { flagBool } from "../lib/args.js";
|
|
|
8
8
|
import { c } from "../lib/colors.js";
|
|
9
9
|
import { emitJson } from "../lib/json-out.js";
|
|
10
10
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
11
|
+
import { summarize, collectAnchors, collectAnchorDuplicates, collectSpecs } from "../lib/trace-model.js";
|
|
12
|
+
|
|
13
|
+
// The provenance arithmetic lives in lib/trace-model.js, which `status`,
|
|
14
|
+
// `review` and the project snapshot read too (audit finding F7).
|
|
15
|
+
export { summarize } from "../lib/trace-model.js";
|
|
11
16
|
|
|
12
17
|
// Intent-provenance report (ADR 0006). `coverage` proves a criterion has a
|
|
13
18
|
// test; `trace` proves a capability traces to a stated intent. Together they
|
|
@@ -48,9 +53,23 @@ export async function run(_positional, flags) {
|
|
|
48
53
|
|
|
49
54
|
const anchors = collectAnchors(projectRoot); // [{ id }], in document order
|
|
50
55
|
const specs = collectSpecs(projectRoot); // [{ cap, status, realizes: [id] }]
|
|
56
|
+
// An id declared twice is two intents under one name (change 0108): the
|
|
57
|
+
// collector keeps the first, so the second is invisible everywhere unless
|
|
58
|
+
// named here. A gap, like a dropped anchor — `validate` reports it as an
|
|
59
|
+
// error as well.
|
|
60
|
+
const duplicates = collectAnchorDuplicates(projectRoot); // [{ id, lines }]
|
|
51
61
|
|
|
52
62
|
const anchorIds = new Set(anchors.map((a) => a.id));
|
|
53
|
-
|
|
63
|
+
// A spec OPTED IN when it cites at least one anchor id. The header alone
|
|
64
|
+
// is not opting in: `spec new` scaffolds `**Realizes:** n/a — <reason>`,
|
|
65
|
+
// which parses to an empty id list and used to count as participation.
|
|
66
|
+
// One scaffolded spec was therefore enough to take a project out of the
|
|
67
|
+
// never-opted-in branch and into the normal report, where zero anchors
|
|
68
|
+
// rendered as `ok 0 of 0 intent anchors realized` — a green verdict over
|
|
69
|
+
// nothing, on the very first read a new project gets about itself, while
|
|
70
|
+
// `doctor` read the same collection and warned (change 0083). Absence is
|
|
71
|
+
// not approval — the half change 0057 fixed in `coverage` and not here.
|
|
72
|
+
const anyRealizes = specs.some((s) => s.realizes !== null && s.realizes.length > 0);
|
|
54
73
|
|
|
55
74
|
// The feature is unused: do not nag a project that never opted in.
|
|
56
75
|
if (anchors.length === 0 && !anyRealizes) {
|
|
@@ -88,8 +107,8 @@ export async function run(_positional, flags) {
|
|
|
88
107
|
|
|
89
108
|
if (json) {
|
|
90
109
|
const rows = anchors.map((a) => ({ id: a.id, realizedBy: (realizedBy.get(a.id) ?? []).sort() }));
|
|
91
|
-
const clean = rows.every((r) => r.realizedBy.length > 0) && dangling.length === 0 && untraceable.length === 0;
|
|
92
|
-
emitJson("trace", { anchors: rows, dangling, untraceable, summary: summarize(projectRoot) });
|
|
110
|
+
const clean = rows.every((r) => r.realizedBy.length > 0) && dangling.length === 0 && untraceable.length === 0 && duplicates.length === 0;
|
|
111
|
+
emitJson("trace", { anchors: rows, dangling, untraceable, duplicates, summary: summarize(projectRoot) });
|
|
93
112
|
return clean ? 0 : strict ? 1 : 0;
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -107,7 +126,10 @@ export async function run(_positional, flags) {
|
|
|
107
126
|
}
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
if (dangling.length > 0 || untraceable.length > 0) console.log("");
|
|
129
|
+
if (dangling.length > 0 || untraceable.length > 0 || duplicates.length > 0) console.log("");
|
|
130
|
+
for (const d of duplicates) {
|
|
131
|
+
console.log(` ${c.red("✗")} duplicate anchor [${d.id}] — declared at product.md lines ${d.lines.join(" and ")}; only the first is read, give the second its own id`);
|
|
132
|
+
}
|
|
111
133
|
for (const d of dangling) {
|
|
112
134
|
console.log(` ${c.yellow("!")} dangling: spec "${d.cap}" realizes ${d.id} (no such anchor in product.md)`);
|
|
113
135
|
}
|
|
@@ -117,10 +139,16 @@ export async function run(_positional, flags) {
|
|
|
117
139
|
|
|
118
140
|
const dropped = anchors.length - realized;
|
|
119
141
|
console.log("");
|
|
120
|
-
const summary =
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
const summary = anchors.length === 0
|
|
143
|
+
? `no intent anchors declared in product.md` +
|
|
144
|
+
`; ${dangling.length} dangling, ${untraceable.length} untraceable`
|
|
145
|
+
: `${realized} of ${anchors.length} intent anchor${anchors.length === 1 ? "" : "s"} realized` +
|
|
146
|
+
`; ${dropped} dropped, ${dangling.length} dangling, ${untraceable.length} untraceable` +
|
|
147
|
+
(duplicates.length > 0 ? `, ${duplicates.length} duplicate` : "");
|
|
148
|
+
// Reaching the report with no anchors at all is never clean: there is
|
|
149
|
+
// nothing to have realized, and a ratio over zero says nothing true.
|
|
150
|
+
const clean = anchors.length > 0
|
|
151
|
+
&& dropped === 0 && dangling.length === 0 && untraceable.length === 0 && duplicates.length === 0;
|
|
124
152
|
if (clean) {
|
|
125
153
|
console.log(c.green("ok") + " " + summary);
|
|
126
154
|
return 0;
|
|
@@ -130,69 +158,6 @@ export async function run(_positional, flags) {
|
|
|
130
158
|
return strict ? 1 : 0;
|
|
131
159
|
}
|
|
132
160
|
|
|
133
|
-
// Pure summary of intent provenance, for other commands (`status`, `review`)
|
|
134
|
-
// that need the numbers without the report output.
|
|
135
|
-
export function summarize(projectRoot) {
|
|
136
|
-
const anchors = collectAnchors(projectRoot);
|
|
137
|
-
const specs = collectSpecs(projectRoot);
|
|
138
|
-
const anchorIds = new Set(anchors.map((a) => a.id));
|
|
139
|
-
const realizedBy = new Map();
|
|
140
|
-
let dangling = 0;
|
|
141
|
-
for (const s of specs) {
|
|
142
|
-
if (s.realizes === null) continue;
|
|
143
|
-
for (const id of s.realizes) {
|
|
144
|
-
if (anchorIds.has(id)) {
|
|
145
|
-
if (!realizedBy.has(id)) realizedBy.set(id, []);
|
|
146
|
-
realizedBy.get(id).push(s.cap);
|
|
147
|
-
} else {
|
|
148
|
-
dangling += 1;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const untraceable = specs.filter((s) => s.realizes === null && s.status === "active").length;
|
|
153
|
-
let realized = 0;
|
|
154
|
-
for (const a of anchors) if ((realizedBy.get(a.id) ?? []).length > 0) realized += 1;
|
|
155
|
-
return {
|
|
156
|
-
anchors: anchors.length,
|
|
157
|
-
realized,
|
|
158
|
-
dropped: anchors.length - realized,
|
|
159
|
-
dangling,
|
|
160
|
-
untraceable,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Every "[A-Z]+\d+" tag at the head of a bullet in product.md is an intent
|
|
165
|
-
// anchor. Section-agnostic so Success-criteria and In-scope bullets both work.
|
|
166
|
-
function collectAnchors(projectRoot) {
|
|
167
|
-
const productPath = path.join(projectRoot, ".doctrina", "product.md");
|
|
168
|
-
if (!isFile(productPath)) return [];
|
|
169
|
-
const out = [];
|
|
170
|
-
const seen = new Set();
|
|
171
|
-
for (const line of read(productPath).split(/\r?\n/)) {
|
|
172
|
-
const m = line.match(/^\s*[-*]\s+\[([A-Z]+\d+)\]\s+/);
|
|
173
|
-
if (m && !seen.has(m[1])) {
|
|
174
|
-
seen.add(m[1]);
|
|
175
|
-
out.push({ id: m[1] });
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function collectSpecs(projectRoot) {
|
|
182
|
-
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
183
|
-
if (!isDir(specsDir)) return [];
|
|
184
|
-
const out = [];
|
|
185
|
-
for (const cap of readdirSync(specsDir).sort()) {
|
|
186
|
-
const specPath = path.join(specsDir, cap, "spec.md");
|
|
187
|
-
if (!isFile(specPath)) continue;
|
|
188
|
-
const text = read(specPath);
|
|
189
|
-
const realizesRaw = specHeader(text, "Realizes");
|
|
190
|
-
const realizes = realizesRaw === null ? null : (realizesRaw.match(ANCHOR_RE) ?? []);
|
|
191
|
-
const status = (specHeader(text, "Status") ?? "active").trim().toLowerCase();
|
|
192
|
-
out.push({ cap, status, realizes });
|
|
193
|
-
}
|
|
194
|
-
return out;
|
|
195
|
-
}
|
|
196
161
|
|
|
197
162
|
export const help = `
|
|
198
163
|
Usage: doctrina trace [--strict]
|
package/src/commands/triage.js
CHANGED
|
@@ -4,6 +4,10 @@ import process from "node:process";
|
|
|
4
4
|
import { exists } from "../lib/fs-ops.js";
|
|
5
5
|
import { flagBool } from "../lib/args.js";
|
|
6
6
|
import { c } from "../lib/colors.js";
|
|
7
|
+
import { classify } from "../lib/triage-model.js";
|
|
8
|
+
|
|
9
|
+
// The classifier is shared with `work`; see lib/triage-model.js.
|
|
10
|
+
export { classify } from "../lib/triage-model.js";
|
|
7
11
|
import { emitJson } from "../lib/json-out.js";
|
|
8
12
|
import { collectRuntimeFindings, checkLocalEnv } from "../lib/runtime.js";
|
|
9
13
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
@@ -35,96 +39,14 @@ export const jsonNative = true;
|
|
|
35
39
|
|
|
36
40
|
export const flags = { boolean: ["json", "env"], string: [] };
|
|
37
41
|
|
|
38
|
-
// Signals that a request is about the running system rather than about what
|
|
39
|
-
// the system should do. Weighted: a phrase that can only mean runtime counts
|
|
40
|
-
// for more than a word that merely leans that way.
|
|
41
|
-
const RUNTIME_SIGNALS = [
|
|
42
|
-
[3, /\b(workflow|pipeline|ci|github actions|actions run|job|runner)\b/i],
|
|
43
|
-
[3, /\b(env var|environment variable|dotenv|\.env|secret|secrets|vars)\b/i],
|
|
44
|
-
[3, /\b(0 (tests?|scenarios?|cases?)|no (tests?|scenarios?) ran|ran nothing|empty (run|suite|summary))\b/i],
|
|
45
|
-
[2, /\b(exit code|stack ?trace|traceback|logs?|step summary|artifact missing)\b/i],
|
|
46
|
-
[2, /\b(green but|passes but|silently|never (fires|applies|reaches|ran))\b/i],
|
|
47
|
-
[2, /\b(flaky|timeout|timed out|hangs?|crash(es|ed)?|broken build|failing build)\b/i],
|
|
48
|
-
[2, /\b(tag|selector|filter|dispatch)\b/i],
|
|
49
|
-
[1, /\b(config|configuration|wiring|wired|deploy(ment)?|docker|compose)\b/i],
|
|
50
|
-
[1, /\b(debug|diagnos(e|is)|investigate|why (is|does|did)|not working|doesn't work)\b/i],
|
|
51
|
-
];
|
|
52
42
|
|
|
53
|
-
const CHORE_SIGNALS = [
|
|
54
|
-
[3, /\b(bump|upgrade|pin|lockfile|dependency|dependencies|devdependenc)\b/i],
|
|
55
|
-
[2, /\b(rename|move|reorganis|reorganiz|refactor|tidy|cleanup|clean up|format(ting)?|lint)\b/i],
|
|
56
|
-
[2, /\b(typo|comment|readme|changelog|docs? only)\b/i],
|
|
57
|
-
[2, /\b(already (specified|in the spec)|wiring only|no behaviour change|no behavior change)\b/i],
|
|
58
|
-
];
|
|
59
43
|
|
|
60
|
-
const PRODUCT_SIGNALS = [
|
|
61
|
-
[3, /\b(add|introduce|support|allow|enable|implement|build)\b/i],
|
|
62
|
-
[3, /\b(should|shall|must|requirement|acceptance|criteri)\b/i],
|
|
63
|
-
// AUTHORING VERBS. Both lanes above match domain NOUNS, which turned out
|
|
64
|
-
// to misread work ON the machinery as an incident IN it: "declare the
|
|
65
|
-
// release workflow wiring in a contract" scored RUNTIME on "workflow" and
|
|
66
|
-
// "wiring", and "so an intentional rename stops warning" scored CHORE on
|
|
67
|
-
// "rename" — a noun, not the act. Neither prompt was a diagnosis; both
|
|
68
|
-
// were someone sitting down to write an artifact.
|
|
69
|
-
//
|
|
70
|
-
// These are the verbs of authoring. A prompt whose verb is "declare" or
|
|
71
|
-
// "document" is describing work on the tree, whatever nouns surround it.
|
|
72
|
-
[3, /\b(declare|declaring|document|documenting|record|recording|specify|specifying|define|defining)\b/i],
|
|
73
|
-
[2, /\b(new (feature|capability|command|endpoint|screen|flow))\b/i],
|
|
74
|
-
[2, /\b(users? (can|should|want)|so that|in order to)\b/i],
|
|
75
|
-
[1, /\b(change|behaviour|behavior|spec)\b/i],
|
|
76
|
-
];
|
|
77
44
|
|
|
78
45
|
/**
|
|
79
46
|
* Score a prompt against each lane. Deterministic, explainable, and cheap:
|
|
80
47
|
* the caller gets the winning lane AND the signals that produced it, so a
|
|
81
48
|
* wrong guess is arguable rather than mysterious.
|
|
82
49
|
*/
|
|
83
|
-
export function classify(prompt) {
|
|
84
|
-
const text = String(prompt ?? "");
|
|
85
|
-
const score = (signals) => {
|
|
86
|
-
let total = 0;
|
|
87
|
-
const hits = [];
|
|
88
|
-
for (const [weight, re] of signals) {
|
|
89
|
-
const m = text.match(re);
|
|
90
|
-
if (m) {
|
|
91
|
-
total += weight;
|
|
92
|
-
hits.push(m[0].toLowerCase());
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return { total, hits };
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const lanes = {
|
|
99
|
-
runtime: score(RUNTIME_SIGNALS),
|
|
100
|
-
chore: score(CHORE_SIGNALS),
|
|
101
|
-
product: score(PRODUCT_SIGNALS),
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// Product is the default: when nothing distinguishes the request, the
|
|
105
|
-
// ceremony that writes a spec delta is the safe one to fall into. Runtime
|
|
106
|
-
// must BEAT product to win, so an ordinary feature request mentioning a
|
|
107
|
-
// "job" does not get diverted into a diagnosis.
|
|
108
|
-
let lane = "product";
|
|
109
|
-
let best = lanes.product.total;
|
|
110
|
-
for (const name of ["runtime", "chore"]) {
|
|
111
|
-
if (lanes[name].total > best) {
|
|
112
|
-
lane = name;
|
|
113
|
-
best = lanes[name].total;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (best === 0) lane = "product";
|
|
117
|
-
|
|
118
|
-
// Confidence is the MARGIN over the runner-up, not the winner's own score:
|
|
119
|
-
// a prompt that scores 5 for runtime and 4 for product is a coin toss the
|
|
120
|
-
// agent should see as one, and a clear product request must be able to read
|
|
121
|
-
// as confident too (comparing a lane against itself never could).
|
|
122
|
-
const runnerUp = Object.entries(lanes)
|
|
123
|
-
.filter(([name]) => name !== lane)
|
|
124
|
-
.reduce((max, [, v]) => Math.max(max, v.total), 0);
|
|
125
|
-
|
|
126
|
-
return { lane, scores: lanes, confident: best > 0 && best - runnerUp >= 2 };
|
|
127
|
-
}
|
|
128
50
|
|
|
129
51
|
export async function run(positional, cmdFlags) {
|
|
130
52
|
const projectRoot = process.cwd();
|