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.
Files changed (93) hide show
  1. package/README.md +27 -72
  2. package/package.json +3 -3
  3. package/src/commands/adapter.js +27 -3
  4. package/src/commands/analyze.js +16 -212
  5. package/src/commands/change.js +144 -267
  6. package/src/commands/ci.js +182 -0
  7. package/src/commands/clarify.js +65 -21
  8. package/src/commands/close.js +349 -45
  9. package/src/commands/completion.js +8 -2
  10. package/src/commands/context.js +284 -111
  11. package/src/commands/contract.js +95 -75
  12. package/src/commands/coverage.js +58 -273
  13. package/src/commands/decision.js +66 -7
  14. package/src/commands/doctor.js +293 -130
  15. package/src/commands/handoff.js +10 -57
  16. package/src/commands/hooks.js +17 -3
  17. package/src/commands/index-rebuild.js +55 -44
  18. package/src/commands/init.js +88 -16
  19. package/src/commands/intake.js +85 -99
  20. package/src/commands/intent.js +14 -1
  21. package/src/commands/metrics.js +65 -88
  22. package/src/commands/next.js +105 -147
  23. package/src/commands/prime.js +23 -91
  24. package/src/commands/report.js +40 -109
  25. package/src/commands/review.js +68 -36
  26. package/src/commands/search.js +18 -4
  27. package/src/commands/show.js +24 -9
  28. package/src/commands/skill.js +23 -39
  29. package/src/commands/spec.js +38 -11
  30. package/src/commands/status.js +69 -171
  31. package/src/commands/templates.js +35 -148
  32. package/src/commands/trace.js +36 -71
  33. package/src/commands/triage.js +4 -82
  34. package/src/commands/upgrade.js +31 -6
  35. package/src/commands/validate.js +44 -877
  36. package/src/commands/verify.js +60 -138
  37. package/src/commands/why.js +2 -2
  38. package/src/commands/work.js +243 -267
  39. package/src/index.js +139 -12
  40. package/src/lib/actions.js +490 -0
  41. package/src/lib/adapters.js +0 -11
  42. package/src/lib/adr-guard.js +19 -0
  43. package/src/lib/agent-changelog.js +133 -0
  44. package/src/lib/analysis.js +264 -0
  45. package/src/lib/args.js +14 -0
  46. package/src/lib/change-ops.js +133 -0
  47. package/src/lib/commands.js +135 -10
  48. package/src/lib/config.js +206 -0
  49. package/src/lib/constitution-model.js +102 -0
  50. package/src/lib/coverage-model.js +431 -0
  51. package/src/lib/criteria.js +23 -0
  52. package/src/lib/doc-model.js +370 -3
  53. package/src/lib/docs-impact.js +270 -27
  54. package/src/lib/ears.js +24 -1
  55. package/src/lib/flag-catalog.js +0 -16
  56. package/src/lib/fs-ops.js +58 -1
  57. package/src/lib/gates.js +231 -29
  58. package/src/lib/git.js +132 -0
  59. package/src/lib/index-json.js +42 -24
  60. package/src/lib/intake-model.js +155 -0
  61. package/src/lib/json-out.js +100 -2
  62. package/src/lib/ledger.js +192 -0
  63. package/src/lib/lexicon.js +228 -0
  64. package/src/lib/metrics-model.js +189 -0
  65. package/src/lib/names.js +42 -0
  66. package/src/lib/playbook.js +97 -0
  67. package/src/lib/project.js +105 -0
  68. package/src/lib/reproducibility.js +137 -0
  69. package/src/lib/runtime.js +191 -5
  70. package/src/lib/scan.js +328 -25
  71. package/src/lib/signoff.js +147 -0
  72. package/src/lib/snapshot.js +231 -0
  73. package/src/lib/spec-ops.js +72 -8
  74. package/src/lib/templates-model.js +289 -0
  75. package/src/lib/trace-model.js +95 -0
  76. package/src/lib/triage-model.js +121 -0
  77. package/src/lib/usage.js +6 -4
  78. package/src/lib/validation-model.js +1381 -0
  79. package/src/lib/version.js +22 -0
  80. package/src/lib/views.js +413 -0
  81. package/src/lib/work-model.js +102 -0
  82. package/templates/AGENTS.md.template +21 -28
  83. package/templates/README.md +35 -0
  84. package/templates/adapters/claude/.claude/commands/doctrina-work.md.template +6 -3
  85. package/templates/adapters/cursor/.cursor/commands/doctrina-work.md.template +4 -3
  86. package/templates/change/proposal.md.template +16 -0
  87. package/templates/change/tasks.md.template +3 -8
  88. package/templates/contract.md.template +2 -2
  89. package/templates/hooks/pre-commit.sample +16 -2
  90. package/templates/playbooks/bootstrap.md.template +47 -0
  91. package/templates/playbooks/chore.md.template +24 -0
  92. package/templates/playbooks/work.md.template +83 -0
  93. 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
- // The surface is 36 commands and 59 operations. The audit's charge is that
5
- // some of them exist because they were easy to add, not because anybody
6
- // reaches for them — and the honest way to find out which is to MEASURE,
7
- // not to guess from the outside. Guessing is how `prime`, `handoff` and
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
  //