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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
|
+
import { isDir, isFile, read } from "./fs-ops.js";
|
|
5
|
+
import { specHeader } from "./scan.js";
|
|
6
|
+
|
|
7
|
+
// The trace MODEL: product intent anchors, the specs that realize them, and
|
|
8
|
+
// the provenance arithmetic over the two. Lifted out of `commands/trace.js`
|
|
9
|
+
// so `status`, `review` and the snapshot read it directly instead of
|
|
10
|
+
// importing out of a command module (audit finding F7). Nothing here prints.
|
|
11
|
+
|
|
12
|
+
const ANCHOR_RE = /[A-Z]+\d+/g;
|
|
13
|
+
|
|
14
|
+
// Pure summary of intent provenance, for other commands (`status`, `review`)
|
|
15
|
+
// that need the numbers without the report output.
|
|
16
|
+
export function summarize(projectRoot) {
|
|
17
|
+
const anchors = collectAnchors(projectRoot);
|
|
18
|
+
const specs = collectSpecs(projectRoot);
|
|
19
|
+
const anchorIds = new Set(anchors.map((a) => a.id));
|
|
20
|
+
const realizedBy = new Map();
|
|
21
|
+
let dangling = 0;
|
|
22
|
+
for (const s of specs) {
|
|
23
|
+
if (s.realizes === null) continue;
|
|
24
|
+
for (const id of s.realizes) {
|
|
25
|
+
if (anchorIds.has(id)) {
|
|
26
|
+
if (!realizedBy.has(id)) realizedBy.set(id, []);
|
|
27
|
+
realizedBy.get(id).push(s.cap);
|
|
28
|
+
} else {
|
|
29
|
+
dangling += 1;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const untraceable = specs.filter((s) => s.realizes === null && s.status === "active").length;
|
|
34
|
+
let realized = 0;
|
|
35
|
+
for (const a of anchors) if ((realizedBy.get(a.id) ?? []).length > 0) realized += 1;
|
|
36
|
+
return {
|
|
37
|
+
anchors: anchors.length,
|
|
38
|
+
realized,
|
|
39
|
+
dropped: anchors.length - realized,
|
|
40
|
+
dangling,
|
|
41
|
+
untraceable,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Every "[A-Z]+\d+" tag at the head of a bullet in product.md is an intent
|
|
46
|
+
// anchor. Section-agnostic so Success-criteria and In-scope bullets both work.
|
|
47
|
+
export function collectAnchors(projectRoot) {
|
|
48
|
+
const productPath = path.join(projectRoot, ".doctrina", "product.md");
|
|
49
|
+
if (!isFile(productPath)) return [];
|
|
50
|
+
const out = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
for (const line of read(productPath).split(/\r?\n/)) {
|
|
53
|
+
const m = line.match(/^\s*[-*]\s+\[([A-Z]+\d+)\]\s+/);
|
|
54
|
+
if (m && !seen.has(m[1])) {
|
|
55
|
+
seen.add(m[1]);
|
|
56
|
+
out.push({ id: m[1] });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Anchor ids declared MORE THAN ONCE in product.md (change 0108). The
|
|
63
|
+
// collector above keeps the first bullet per id, which is right for the
|
|
64
|
+
// graph and wrong as the only reading: the second intent under the same id
|
|
65
|
+
// vanished from trace, why, intent list and every pack, with `trace
|
|
66
|
+
// --strict` green. Returned as {id, lines} so trace can name both bullets.
|
|
67
|
+
export function collectAnchorDuplicates(projectRoot) {
|
|
68
|
+
const productPath = path.join(projectRoot, ".doctrina", "product.md");
|
|
69
|
+
if (!isFile(productPath)) return [];
|
|
70
|
+
const lines = new Map();
|
|
71
|
+
const rows = read(productPath).split(/\r?\n/);
|
|
72
|
+
for (let i = 0; i < rows.length; i++) {
|
|
73
|
+
const m = rows[i].match(/^\s*[-*]\s+\[([A-Z]+\d+)\]\s+/);
|
|
74
|
+
if (!m) continue;
|
|
75
|
+
if (!lines.has(m[1])) lines.set(m[1], []);
|
|
76
|
+
lines.get(m[1]).push(i + 1);
|
|
77
|
+
}
|
|
78
|
+
return [...lines].filter(([, at]) => at.length > 1).map(([id, at]) => ({ id, lines: at }));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function collectSpecs(projectRoot) {
|
|
82
|
+
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
83
|
+
if (!isDir(specsDir)) return [];
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const cap of readdirSync(specsDir).sort()) {
|
|
86
|
+
const specPath = path.join(specsDir, cap, "spec.md");
|
|
87
|
+
if (!isFile(specPath)) continue;
|
|
88
|
+
const text = read(specPath);
|
|
89
|
+
const realizesRaw = specHeader(text, "Realizes");
|
|
90
|
+
const realizes = realizesRaw === null ? null : (realizesRaw.match(ANCHOR_RE) ?? []);
|
|
91
|
+
const status = (specHeader(text, "Status") ?? "active").trim().toLowerCase();
|
|
92
|
+
out.push({ cap, status, realizes });
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { fold } from "./lexicon.js";
|
|
3
|
+
// The lane CLASSIFIER (ADR 0024).
|
|
4
|
+
//
|
|
5
|
+
// `work` asks the same question `triage` answers — is this request PRODUCT,
|
|
6
|
+
// RUNTIME or CHORE — and imported the answer out of the command module
|
|
7
|
+
// (audit finding F7). One classifier, read by both.
|
|
8
|
+
|
|
9
|
+
// The prompt is FOLDED before matching (change 0107): accents stripped,
|
|
10
|
+
// lower-cased — the same `fold` `work` and `context --for` retrieve with
|
|
11
|
+
// (ADR 0040). Every pattern below is written in folded (ASCII) form, and
|
|
12
|
+
// the Portuguese half of each list sits next to the English half: the
|
|
13
|
+
// lexicon was English-only, so "the build is broken in CI, DATABASE_URL
|
|
14
|
+
// never reaches the process" was held as RUNTIME with exit 3 while the
|
|
15
|
+
// same sentence in Portuguese opened a change. A project whose prompts
|
|
16
|
+
// arrive in Portuguese (this one) had no guard at all.
|
|
17
|
+
export const CHORE_SIGNALS = [
|
|
18
|
+
[3, /\b(bump|upgrade|pin|lockfile|dependency|dependencies|devdependenc)\b/i],
|
|
19
|
+
[2, /\b(rename|move|reorganis|reorganiz|refactor|tidy|cleanup|clean up|format(ting)?|lint)\b/i],
|
|
20
|
+
[2, /\b(typo|comment|readme|changelog|docs? only)\b/i],
|
|
21
|
+
[2, /\b(already (specified|in the spec)|wiring only|no behaviour change|no behavior change)\b/i],
|
|
22
|
+
// pt
|
|
23
|
+
[3, /\b(atualizar (a |as )?dependencia|dependencias|fixar (a )?versao)\b/i],
|
|
24
|
+
[2, /\b(renomear|mover|reorganizar|refatorar|arrumar|limpeza|limpar|formatar|formatacao)\b/i],
|
|
25
|
+
[2, /\b(erro de digitacao|comentario|so (a )?doc(umentacao)?)\b/i],
|
|
26
|
+
[2, /\b(ja (esta )?(especificad[oa]|na spec)|so (a )?fiacao|sem mudanca de comportamento)\b/i],
|
|
27
|
+
];
|
|
28
|
+
export const PRODUCT_SIGNALS = [
|
|
29
|
+
[3, /\b(add|introduce|support|allow|enable|implement|build)\b/i],
|
|
30
|
+
[3, /\b(should|shall|must|requirement|acceptance|criteri)\b/i],
|
|
31
|
+
// AUTHORING VERBS. Both lanes above match domain NOUNS, which turned out
|
|
32
|
+
// to misread work ON the machinery as an incident IN it: "declare the
|
|
33
|
+
// release workflow wiring in a contract" scored RUNTIME on "workflow" and
|
|
34
|
+
// "wiring", and "so an intentional rename stops warning" scored CHORE on
|
|
35
|
+
// "rename" — a noun, not the act. Neither prompt was a diagnosis; both
|
|
36
|
+
// were someone sitting down to write an artifact.
|
|
37
|
+
//
|
|
38
|
+
// These are the verbs of authoring. A prompt whose verb is "declare" or
|
|
39
|
+
// "document" is describing work on the tree, whatever nouns surround it.
|
|
40
|
+
[3, /\b(declare|declaring|document|documenting|record|recording|specify|specifying|define|defining)\b/i],
|
|
41
|
+
[2, /\b(new (feature|capability|command|endpoint|screen|flow))\b/i],
|
|
42
|
+
[2, /\b(users? (can|should|want)|so that|in order to)\b/i],
|
|
43
|
+
[1, /\b(change|behaviour|behavior|spec)\b/i],
|
|
44
|
+
// pt
|
|
45
|
+
[3, /\b(adicionar|introduzir|suportar|permitir|habilitar|implementar|construir|criar)\b/i],
|
|
46
|
+
[3, /\b(deve|devera|requisito|aceite|aceitacao|criterio)\b/i],
|
|
47
|
+
[3, /\b(declarar|declarando|documentar|documentando|registrar|registrando|especificar|especificando|definir|definindo)\b/i],
|
|
48
|
+
[2, /\b(nov[oa] (feature|funcionalidade|capability|capacidade|comando|endpoint|tela|fluxo))\b/i],
|
|
49
|
+
[2, /\b(usuari[oa]s? (pode|podem|deve|devem|quer|querem)|para que|de modo que|a fim de)\b/i],
|
|
50
|
+
[1, /\b(mudanca|comportamento)\b/i],
|
|
51
|
+
];
|
|
52
|
+
// Signals that a request is about the running system rather than about what
|
|
53
|
+
// the system should do. Weighted: a phrase that can only mean runtime counts
|
|
54
|
+
// for more than a word that merely leans that way.
|
|
55
|
+
export const RUNTIME_SIGNALS = [
|
|
56
|
+
[3, /\b(workflow|pipeline|ci|github actions|actions run|job|runner)\b/i],
|
|
57
|
+
[3, /\b(env var|environment variable|dotenv|\.env|secret|secrets|vars)\b/i],
|
|
58
|
+
[3, /\b(0 (tests?|scenarios?|cases?)|no (tests?|scenarios?) ran|ran nothing|empty (run|suite|summary))\b/i],
|
|
59
|
+
[2, /\b(exit code|stack ?trace|traceback|logs?|step summary|artifact missing)\b/i],
|
|
60
|
+
[2, /\b(green but|passes but|silently|never (fires|applies|reaches|ran))\b/i],
|
|
61
|
+
[2, /\b(flaky|timeout|timed out|hangs?|crash(es|ed)?|broken build|failing build)\b/i],
|
|
62
|
+
[2, /\b(tag|selector|filter|dispatch)\b/i],
|
|
63
|
+
[1, /\b(config|configuration|wiring|wired|deploy(ment)?|docker|compose)\b/i],
|
|
64
|
+
[1, /\b(debug|diagnos(e|is)|investigate|why (is|does|did)|not working|doesn't work)\b/i],
|
|
65
|
+
// pt
|
|
66
|
+
[3, /\b(execucao d[oa]s? actions|esteira)\b/i],
|
|
67
|
+
[3, /\b(variavel de ambiente|variaveis de ambiente|segredo|segredos)\b/i],
|
|
68
|
+
[3, /\b(0 (testes?|cenarios?|casos?)|nenhum (teste|cenario|caso) (rodou|correu|executou)|(rodou|correu|executou) nada|(execucao|suite|resumo) vazi[oa])\b/i],
|
|
69
|
+
[2, /\b(codigo de saida|resumo d[oa] (step|etapa)|artefato (ausente|faltando|sumiu))\b/i],
|
|
70
|
+
[2, /\b(verde mas|passa mas|silenciosamente|em silencio|nunca (dispara|aplica|chega|rodou|correu|executou))\b/i],
|
|
71
|
+
[2, /\b(intermitente|estourou o tempo|trava|travou|crashou|build (quebrad[oa]|falhando|falhou))\b/i],
|
|
72
|
+
[2, /\b(seletor|filtro|despacho)\b/i],
|
|
73
|
+
[1, /\b(configuracao|fiacao|implantacao)\b/i],
|
|
74
|
+
[1, /\b(depurar|diagnostic(o|ar)|investigar|por que (esta|e|nao)|nao funciona|nao esta funcionando)\b/i],
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export function classify(prompt) {
|
|
78
|
+
const text = fold(prompt);
|
|
79
|
+
const score = (signals) => {
|
|
80
|
+
let total = 0;
|
|
81
|
+
const hits = [];
|
|
82
|
+
for (const [weight, re] of signals) {
|
|
83
|
+
const m = text.match(re);
|
|
84
|
+
if (m) {
|
|
85
|
+
total += weight;
|
|
86
|
+
hits.push(m[0].toLowerCase());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return { total, hits };
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const lanes = {
|
|
93
|
+
runtime: score(RUNTIME_SIGNALS),
|
|
94
|
+
chore: score(CHORE_SIGNALS),
|
|
95
|
+
product: score(PRODUCT_SIGNALS),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Product is the default: when nothing distinguishes the request, the
|
|
99
|
+
// ceremony that writes a spec delta is the safe one to fall into. Runtime
|
|
100
|
+
// must BEAT product to win, so an ordinary feature request mentioning a
|
|
101
|
+
// "job" does not get diverted into a diagnosis.
|
|
102
|
+
let lane = "product";
|
|
103
|
+
let best = lanes.product.total;
|
|
104
|
+
for (const name of ["runtime", "chore"]) {
|
|
105
|
+
if (lanes[name].total > best) {
|
|
106
|
+
lane = name;
|
|
107
|
+
best = lanes[name].total;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (best === 0) lane = "product";
|
|
111
|
+
|
|
112
|
+
// Confidence is the MARGIN over the runner-up, not the winner's own score:
|
|
113
|
+
// a prompt that scores 5 for runtime and 4 for product is a coin toss the
|
|
114
|
+
// agent should see as one, and a clear product request must be able to read
|
|
115
|
+
// as confident too (comparing a lane against itself never could).
|
|
116
|
+
const runnerUp = Object.entries(lanes)
|
|
117
|
+
.filter(([name]) => name !== lane)
|
|
118
|
+
.reduce((max, [, v]) => Math.max(max, v.total), 0);
|
|
119
|
+
|
|
120
|
+
return { lane, scores: lanes, confident: best > 0 && best - runnerUp >= 2 };
|
|
121
|
+
}
|
package/src/lib/usage.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
// Audit item M8: instrument the command surface before shrinking it.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// How big the surface is, this module does not say: `COMMAND_NAMES` and
|
|
5
|
+
// `OPERATIONS` in lib/commands.js own that number, and a copy of it here
|
|
6
|
+
// would be one more count to rot (change 0059 found four, all disagreeing).
|
|
7
|
+
// The audit's charge is that some of those commands exist because they were
|
|
8
|
+
// easy to add, not because anybody reaches for them — and the honest way to
|
|
9
|
+
// find out which is to MEASURE, not to guess from the outside. Guessing is how `prime`, `handoff` and
|
|
8
10
|
// `doctor` were nearly cut in 0.13.0, right before the operator review
|
|
9
11
|
// found they had gone unused only because AGENTS.md never named them.
|
|
10
12
|
//
|