doctrina-cli 0.16.0 → 0.18.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 +31 -73
- package/package.json +3 -3
- package/src/commands/adapter.js +3 -2
- package/src/commands/analyze.js +8 -0
- package/src/commands/change.js +86 -80
- package/src/commands/clarify.js +8 -1
- package/src/commands/close.js +136 -34
- package/src/commands/completion.js +8 -2
- package/src/commands/context.js +6 -4
- package/src/commands/decision.js +12 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/hooks.js +6 -1
- package/src/commands/index-rebuild.js +52 -2
- package/src/commands/init.js +19 -4
- package/src/commands/intake.js +55 -7
- package/src/commands/intent.js +14 -1
- package/src/commands/metrics.js +13 -5
- package/src/commands/next.js +2 -1
- package/src/commands/prime.js +4 -5
- package/src/commands/report.js +9 -20
- package/src/commands/review.js +47 -8
- package/src/commands/skill.js +8 -139
- package/src/commands/spec.js +13 -7
- package/src/commands/status.js +39 -20
- package/src/commands/templates.js +4 -10
- package/src/commands/upgrade.js +31 -6
- package/src/commands/validate.js +32 -8
- package/src/commands/verify.js +59 -6
- package/src/commands/work.js +63 -8
- package/src/index.js +34 -6
- package/src/lib/actions.js +38 -28
- package/src/lib/args.js +14 -0
- package/src/lib/change-ops.js +95 -1
- package/src/lib/commands.js +96 -15
- package/src/lib/coverage-model.js +40 -1
- package/src/lib/doc-model.js +47 -6
- package/src/lib/docs-impact.js +74 -0
- package/src/lib/ears.js +24 -1
- package/src/lib/gates.js +53 -9
- package/src/lib/git.js +59 -2
- package/src/lib/index-json.js +40 -5
- package/src/lib/intake-model.js +71 -2
- package/src/lib/lexicon.js +38 -1
- package/src/lib/metrics-model.js +30 -0
- package/src/lib/playbook.js +1 -1
- package/src/lib/project.js +49 -1
- package/src/lib/runtime.js +6 -0
- package/src/lib/scan.js +139 -14
- package/src/lib/signoff.js +24 -0
- package/src/lib/skill-candidates.js +162 -0
- package/src/lib/snapshot.js +61 -2
- package/src/lib/spec-ops.js +141 -6
- package/src/lib/templates-model.js +35 -8
- package/src/lib/validation-model.js +117 -35
- package/src/lib/verify-cache.js +76 -0
- package/src/lib/views.js +10 -2
- package/src/lib/work-model.js +57 -12
- package/templates/AGENTS.md.template +3 -4
- package/templates/README.md +2 -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 +15 -0
- package/templates/change/spec-delta.md.template +2 -0
- package/templates/change/tasks.md.template +3 -8
- package/templates/contract.md.template +2 -2
- package/templates/hooks/pre-commit.sample +10 -0
- package/templates/playbooks/bootstrap.md.template +6 -2
- package/templates/playbooks/chore.md.template +9 -4
- package/templates/playbooks/consolidate.md.template +28 -0
- package/templates/playbooks/work.md.template +13 -3
- package/src/commands/constitution.js +0 -68
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { git, GIT_STATE, worktreeFingerprint } from "./git.js";
|
|
6
|
+
import { cliVersion } from "./version.js";
|
|
7
|
+
|
|
8
|
+
// A green verify, remembered for the exact code it ran on (change 0203).
|
|
9
|
+
//
|
|
10
|
+
// A field review of 0.17 put the fixed cost of a small change at the close
|
|
11
|
+
// running the whole suite — 2.5 to 4 minutes — and an agent typically runs
|
|
12
|
+
// `doctrina verify` itself right before `doctrina close`, so the same suite
|
|
13
|
+
// ran twice on the same code. In a batch close it ran once per id, on a tree
|
|
14
|
+
// whose only difference between ids was the artifacts the previous close
|
|
15
|
+
// archived.
|
|
16
|
+
//
|
|
17
|
+
// The fingerprint is the CODE: the working tree (tracked, untracked, edits,
|
|
18
|
+
// .gitignore respected) minus `.doctrina/`, plus the verify.json that
|
|
19
|
+
// defines the checks and the CLI version that runs them. `.doctrina/` is
|
|
20
|
+
// left out on purpose — the close's apply and archive move it between the
|
|
21
|
+
// verify a person ran and the one the close would run — and the artifact
|
|
22
|
+
// tree is proven by the close's own steps (coverage, trace, index drift,
|
|
23
|
+
// validate), which always run.
|
|
24
|
+
//
|
|
25
|
+
// The record lives in the git directory, never in the tree: it is a fact
|
|
26
|
+
// about this checkout, not an artifact to commit or review.
|
|
27
|
+
|
|
28
|
+
const RECORD = "doctrina-verify-green.json";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} projectRoot
|
|
32
|
+
* @param {string} configText the verify.json the checks come from
|
|
33
|
+
* @returns {string | null} null when git cannot fingerprint the tree
|
|
34
|
+
*/
|
|
35
|
+
export function verifyFingerprint(projectRoot, configText) {
|
|
36
|
+
const tree = worktreeFingerprint(projectRoot, { exclude: [".doctrina"] });
|
|
37
|
+
if (tree === null) return null;
|
|
38
|
+
return createHash("sha256").update(`${tree}\n${configText}\n${cliVersion()}`).digest("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function recordPath(projectRoot) {
|
|
42
|
+
const dir = git(projectRoot, ["rev-parse", "--absolute-git-dir"]);
|
|
43
|
+
return dir.state === GIT_STATE.OK ? path.join(dir.stdout.trim(), RECORD) : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remember that every check passed on this fingerprint.
|
|
48
|
+
* @param {string} projectRoot
|
|
49
|
+
* @param {string} fingerprint
|
|
50
|
+
* @param {string[]} checks
|
|
51
|
+
*/
|
|
52
|
+
export function recordGreen(projectRoot, fingerprint, checks) {
|
|
53
|
+
const file = recordPath(projectRoot);
|
|
54
|
+
if (!file) return;
|
|
55
|
+
try {
|
|
56
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
57
|
+
writeFileSync(file, JSON.stringify({ fingerprint, checks, at: new Date().toISOString() }, null, 2) + "\n");
|
|
58
|
+
} catch { /* a cache that cannot be written is a cache miss, never an error */ }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The last green run, when it covered exactly this fingerprint.
|
|
63
|
+
* @param {string} projectRoot
|
|
64
|
+
* @param {string} fingerprint
|
|
65
|
+
* @returns {{ fingerprint: string, checks: string[], at: string } | null}
|
|
66
|
+
*/
|
|
67
|
+
export function reusableGreen(projectRoot, fingerprint) {
|
|
68
|
+
const file = recordPath(projectRoot);
|
|
69
|
+
if (!file) return null;
|
|
70
|
+
try {
|
|
71
|
+
const rec = JSON.parse(readFileSync(file, "utf8"));
|
|
72
|
+
return rec && rec.fingerprint === fingerprint ? rec : null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
package/src/lib/views.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { c } from "./colors.js";
|
|
3
3
|
import { today } from "./dates.js";
|
|
4
4
|
import { churnByCapability } from "./ledger.js";
|
|
5
|
+
import { STALE_CHANGE_DAYS } from "./snapshot.js";
|
|
5
6
|
|
|
6
7
|
// Executed proof and signed proof are different claims (change 0039). One
|
|
7
8
|
// phrasing, used by every view, so none of them can quietly add the two
|
|
@@ -182,7 +183,11 @@ export function prime(s) {
|
|
|
182
183
|
} else {
|
|
183
184
|
for (const ch of s.openChanges) {
|
|
184
185
|
const tasks = ch.tasksTotal > 0 ? ` · tasks ${ch.tasksDone}/${ch.tasksTotal}` : "";
|
|
185
|
-
|
|
186
|
+
// The age, once it is news (change 0201): a change opened today and
|
|
187
|
+
// one forgotten for three weeks used to read identically.
|
|
188
|
+
const age = ch.ageDays !== null && ch.ageDays !== undefined && ch.ageDays >= STALE_CHANGE_DAYS
|
|
189
|
+
? ` · open ${ch.ageDays}d` : "";
|
|
190
|
+
out.push(` ${c.cyan(ch.id)} ${ch.title ?? ""}${c.gray(` (${ch.status}${tasks}${age})`)}`);
|
|
186
191
|
}
|
|
187
192
|
}
|
|
188
193
|
|
|
@@ -222,6 +227,9 @@ export function handoff(s) {
|
|
|
222
227
|
out.push("## Open work");
|
|
223
228
|
out.push("");
|
|
224
229
|
out.push("- none — the tree is at rest; start with `doctrina work \"<prompt>\"`");
|
|
230
|
+
// The note is pasted into another session, so the section has to END like
|
|
231
|
+
// the loop below ends: a heading glued to a list item is not a heading.
|
|
232
|
+
out.push("");
|
|
225
233
|
} else {
|
|
226
234
|
for (const ch of s.openChanges) {
|
|
227
235
|
out.push(`## Open change \`${ch.id}\`${ch.title ? ` — ${ch.title}` : ""}`);
|
|
@@ -405,6 +413,6 @@ export function report(s, { days = 7, cutoffIso = "", git = null, gitState = nul
|
|
|
405
413
|
}
|
|
406
414
|
|
|
407
415
|
out.push("");
|
|
408
|
-
out.push(`*Generated read-only by \`doctrina report --since ${days}\`; deeper numbers: \`doctrina metrics\`.*`);
|
|
416
|
+
out.push(`*Generated read-only by \`doctrina status --view report --since ${days}\`; deeper numbers: \`doctrina metrics\`.*`);
|
|
409
417
|
return out;
|
|
410
418
|
}
|
package/src/lib/work-model.js
CHANGED
|
@@ -21,29 +21,74 @@ export function rankCapabilitiesByDiff(projectRoot, files, { limit = 3 } = {}) {
|
|
|
21
21
|
if (!files || files.length === 0 || !isDir(specsDir)) return [];
|
|
22
22
|
const norm = files.map((f) => f.replace(/\\/g, "/"));
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const specs = [];
|
|
25
25
|
for (const cap of readdirSync(specsDir).sort()) {
|
|
26
26
|
const specPath = path.join(specsDir, cap, "spec.md");
|
|
27
27
|
if (!isFile(specPath)) continue;
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
const text = read(specPath);
|
|
29
|
+
specs.push({ cap, text, declared: parseSourceGlobs(text).map((g) => globToRegExp(g)) });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Per file, the strongest answer the tree holds wins, and a weaker one is
|
|
33
|
+
// never added on top of it (change 0200, ADR 0027):
|
|
34
|
+
// 1. the specs whose `**Source:**` claims the file — a declaration;
|
|
35
|
+
// 2. else the specs citing the exact path (a proof or a pointer);
|
|
36
|
+
// 3. else the path/basename inferences, and only from specs that declare
|
|
37
|
+
// NO Source — a spec that stated what it owns has said all it owns.
|
|
38
|
+
// The old sum added every signal from every spec, so a file claimed by
|
|
39
|
+
// `health` still "belonged" to `cloud` because cloud's spec mentioned an
|
|
40
|
+
// `index.ts`, and every close warned about a spec the change never touched.
|
|
41
|
+
const scores = new Map();
|
|
42
|
+
const add = (cap, n) => scores.set(cap, (scores.get(cap) ?? 0) + n);
|
|
43
|
+
for (const f of norm) {
|
|
44
|
+
const claimed = specs.filter((s) => s.declared.some((re) => re.test(f)));
|
|
45
|
+
if (claimed.length > 0) {
|
|
46
|
+
for (const s of claimed) add(s.cap, 10);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const citing = specs.filter((s) => s.text.includes(f));
|
|
50
|
+
if (citing.length > 0) {
|
|
51
|
+
for (const s of citing) add(s.cap, 5);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const s of specs) {
|
|
55
|
+
if (s.declared.length > 0) continue;
|
|
56
|
+
if (f.split("/").includes(s.cap)) add(s.cap, 3); // src/<cap>/... etc.
|
|
57
|
+
if (s.text.includes(path.basename(f))) add(s.cap, 2); // filename cited
|
|
38
58
|
}
|
|
39
|
-
if (score > 0) ranked.push({ id: cap, score, path: `.doctrina/specs/${cap}/spec.md` });
|
|
40
59
|
}
|
|
60
|
+
const ranked = [...scores].map(([id, score]) => ({ id, score, path: `.doctrina/specs/${id}/spec.md` }));
|
|
41
61
|
// `limit` keeps the work-playbook hint short (top 3); review passes
|
|
42
62
|
// Infinity — truncating there is how it missed 5 of 8 touched capabilities
|
|
43
63
|
// in the 0.11.0 field session.
|
|
44
64
|
return ranked.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id)).slice(0, limit);
|
|
45
65
|
}
|
|
46
66
|
|
|
67
|
+
/**
|
|
68
|
+
* The capabilities whose `Source:` header CLAIMS a file — ADR 0027's question
|
|
69
|
+
* asked directly, with no inference in the answer.
|
|
70
|
+
*
|
|
71
|
+
* `rankCapabilitiesByDiff` deliberately falls back to inferences so a project
|
|
72
|
+
* that has not declared anything still gets a hint. That makes it the wrong
|
|
73
|
+
* function to enforce the declaration with: a file no spec names still comes
|
|
74
|
+
* back with an owner, scored 5 instead of 10, and a test asking "does this
|
|
75
|
+
* have an owner" cannot tell the two apart. `lib/names.js` sat undeclared
|
|
76
|
+
* behind exactly that.
|
|
77
|
+
*/
|
|
78
|
+
export function declaredOwners(projectRoot, file) {
|
|
79
|
+
const specsDir = path.join(projectRoot, ".doctrina", "specs");
|
|
80
|
+
if (!isDir(specsDir)) return [];
|
|
81
|
+
const norm = String(file).replace(/\\/g, "/");
|
|
82
|
+
const owners = [];
|
|
83
|
+
for (const cap of readdirSync(specsDir).sort()) {
|
|
84
|
+
const specPath = path.join(specsDir, cap, "spec.md");
|
|
85
|
+
if (!isFile(specPath)) continue;
|
|
86
|
+
const globs = parseSourceGlobs(read(specPath)).map((g) => globToRegExp(g));
|
|
87
|
+
if (globs.some((re) => re.test(norm))) owners.push(cap);
|
|
88
|
+
}
|
|
89
|
+
return owners;
|
|
90
|
+
}
|
|
91
|
+
|
|
47
92
|
// Deterministic term overlap: fold prompt and spec text with the SHARED
|
|
48
93
|
// lexicon, then score. This is a hint for the agent, never a decision
|
|
49
94
|
// (ADR 0005).
|
|
@@ -46,13 +46,12 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
46
46
|
- `doctrina triage` — classify the lane (product/runtime/chore) and check the declared runtime surface. *When:* a request arrives — BEFORE scaffolding, especially if it smells like an incident.
|
|
47
47
|
- `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)` — scaffold a change and print the playbook to execute. *When:* a request arrives that changes behaviour (triage says PRODUCT).
|
|
48
48
|
- `doctrina spec new|list|set` — create, list, and edit capability specs. *When:* a capability needs creating or its headers advancing.
|
|
49
|
-
- `doctrina change new|apply|archive|check|tick|abandon` —
|
|
49
|
+
- `doctrina change new|apply|archive|check|tick|abandon|delta` — drive a change by hand — check --verbose previews every delta. *When:* driving a change through its lifecycle by hand.
|
|
50
50
|
- `doctrina contract new|list|check` — own and verify the integration surface. *When:* the change touches ports, env vars, or public endpoints.
|
|
51
51
|
- `doctrina decision new|accept|land|supersede|list|scope` — record, accept, land, scope, and supersede ADRs. *When:* the change decides something a later session must not relitigate.
|
|
52
|
-
- `doctrina skill new|list|
|
|
52
|
+
- `doctrina skill new|list|suggest` — capture on-demand procedural memory. *When:* a lesson is worth not relearning.
|
|
53
53
|
- `doctrina intent add|list` — append and list product intent anchors. *When:* new product intent appears after the intake.
|
|
54
54
|
**Gate**
|
|
55
|
-
- `doctrina analyze` — structural pre-flight on a change folder. *When:* before applying a change.
|
|
56
55
|
- `doctrina clarify --all (--lang pt|en)` — smell-test Markdown for ambiguity. *When:* before applying, or before opening a PR.
|
|
57
56
|
- `doctrina validate (--fix)` — schema, structure, EARS, and index drift. *When:* after any artifact edit, and before considering work done.
|
|
58
57
|
- `doctrina coverage --strict` — acceptance criteria against cited evidence. *When:* before claiming a capability is proven.
|
|
@@ -61,7 +60,7 @@ canonical templates and syncs `index.json`. Flags: `doctrina <cmd> --help`.
|
|
|
61
60
|
- `doctrina verify` — the project's declared typecheck/test/build checks. *When:* the real build gate must run.
|
|
62
61
|
- `doctrina close <id...>` — the whole closing sequence in one attested pass. *When:* a change is implemented and ready to finish.
|
|
63
62
|
- `doctrina doctor` — aggregate diagnostic with per-finding remedies. *When:* something looks wrong and you do not know which gate to ask.
|
|
64
|
-
**Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list
|
|
63
|
+
**Maintain** (triggers: `doctrina <command> --help`) — `doctrina templates list` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina completion` · `doctrina upgrade --write` · `doctrina ci --emit github`
|
|
65
64
|
<!-- doctrina:surface:end -->
|
|
66
65
|
|
|
67
66
|
## Stack and tooling
|
package/templates/README.md
CHANGED
|
@@ -45,6 +45,7 @@ loops in it.
|
|
|
45
45
|
| `{{CAPABILITY_BLOCK}}` | The pinned capability, the ranked hints, or "no match" | required in `work` |
|
|
46
46
|
| `{{DIFF_MATCHES}}` | Capabilities the working tree touched | empty |
|
|
47
47
|
| `{{STEP3_INTRO}}` | The delta step's opening, pinned or not | required in `work` |
|
|
48
|
+
| `{{SHAPE}}` | The spec's measured size — lines, requirements per EARS subsection, criteria | required in `consolidate` |
|
|
48
49
|
| `{{FROM_DIFF_DELTA_NOTE}}` | The backfill caveat on writing the delta | empty |
|
|
49
50
|
| `{{MISSING_SPECS_WARNING}}` | Warning that `.doctrina/specs/` is absent | empty |
|
|
50
51
|
|
|
@@ -68,6 +69,7 @@ README.md this file
|
|
|
68
69
|
playbooks/work.md.template the work / backfill playbook
|
|
69
70
|
playbooks/chore.md.template the spec-less chore playbook
|
|
70
71
|
playbooks/bootstrap.md.template the intake -> specs playbook
|
|
72
|
+
playbooks/consolidate.md.template the spec consolidation playbook
|
|
71
73
|
AGENTS.md.template root rules for the target repo
|
|
72
74
|
doctrina/ the .doctrina/ skeleton
|
|
73
75
|
product.md.template
|
|
@@ -9,9 +9,12 @@ Drive the Doctrina spec-driven workflow for **{{PROJECT_NAME}}**.
|
|
|
9
9
|
(No prompt, but you already edited code? Use `doctrina work --from-diff`.)
|
|
10
10
|
2. Execute the printed playbook in order: read the pack with
|
|
11
11
|
`doctrina context <capability> --concat`, write the spec delta, fill
|
|
12
|
-
`tasks.md`, implement
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
`tasks.md`, implement. `doctrina change check <id>` previews everything
|
|
13
|
+
the close would refuse.
|
|
14
|
+
3. Close it: `doctrina close <id>` — one attested pass over every gate. The
|
|
15
|
+
sequence is declared in one place and printed by `doctrina close --help`;
|
|
16
|
+
read it there rather than from a copy. Resolve every `error:` before
|
|
17
|
+
reporting the work done.
|
|
15
18
|
|
|
16
19
|
Reach for the CLI instead of hand-authoring artifacts. Ask the human only on
|
|
17
20
|
genuine ambiguity.
|
|
@@ -5,8 +5,9 @@ Turn the request into a Doctrina change and drive it through the gates.
|
|
|
5
5
|
1. Run `doctrina work "<the request>"` (or `doctrina work --from-diff` if you
|
|
6
6
|
already edited code) and follow the printed playbook.
|
|
7
7
|
2. `doctrina context <capability> --concat` → write the spec delta → fill
|
|
8
|
-
`tasks.md` → implement → `doctrina
|
|
9
|
-
3. `doctrina close <id>`
|
|
10
|
-
|
|
8
|
+
`tasks.md` → implement → `doctrina change check <id>` (the close's preview).
|
|
9
|
+
3. `doctrina close <id>` — one attested pass over every gate; the sequence is
|
|
10
|
+
declared in one place and printed by `doctrina close --help`, so read it
|
|
11
|
+
there and not from a copy. Resolve every `error:` before reporting done.
|
|
11
12
|
|
|
12
13
|
Prefer the CLI over hand-authoring artifacts. Ask only on genuine ambiguity.
|
|
@@ -6,6 +6,21 @@
|
|
|
6
6
|
- **Lane:**
|
|
7
7
|
- **Affects specs:**
|
|
8
8
|
|
|
9
|
+
<!--
|
|
10
|
+
Optional, and usually absent. The closing docs gate reads COMMAND and FLAG
|
|
11
|
+
names out of the prose below and asks for documentation when it finds any.
|
|
12
|
+
It cannot tell a change from a mention: explaining an effect, or writing a
|
|
13
|
+
Scope boundaries line about what this deliberately does NOT touch, names
|
|
14
|
+
things just as loudly as changing them would.
|
|
15
|
+
|
|
16
|
+
When that happens, say so on the record instead of forcing the close:
|
|
17
|
+
|
|
18
|
+
- **Documented surface:** n/a — names two commands to explain an effect; alters neither
|
|
19
|
+
|
|
20
|
+
`none` reads the same as `n/a`, and a BARE one silences nothing — the
|
|
21
|
+
reason is the declaration.
|
|
22
|
+
-->
|
|
23
|
+
|
|
9
24
|
## Why
|
|
10
25
|
|
|
11
26
|
<!-- The user-visible or technical reason this change exists. -->
|
|
@@ -24,6 +24,8 @@ typical delta applies mechanically end to end; only free-prose rewrites
|
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Requirement sections: ubiquitous | event | state | unwanted | optional.
|
|
27
|
+
To take one out: `remove-requirement <section> <n>` / `remove-criterion <n>`
|
|
28
|
+
(the criteria after it renumber; write removals highest number first).
|
|
27
29
|
append-* ops resolve numbering/position at APPLY time, so several open
|
|
28
30
|
changes appending to the same spec never collide on numbers — order of
|
|
29
31
|
application decides.
|
|
@@ -2,16 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<!--
|
|
4
4
|
Each task is a single checkable item. Keep tasks small (under a few hours
|
|
5
|
-
of work). The change is done when every box is checked
|
|
6
|
-
|
|
5
|
+
of work). The change is done when every box is checked and
|
|
6
|
+
`doctrina close {{CHANGE_ID}}` succeeds — the close applies the deltas,
|
|
7
|
+
archives the change and updates the index, so those are not boxes here.
|
|
7
8
|
-->
|
|
8
9
|
|
|
9
10
|
- [ ]
|
|
10
11
|
- [ ]
|
|
11
12
|
- [ ]
|
|
12
|
-
|
|
13
|
-
## Closing steps
|
|
14
|
-
|
|
15
|
-
- [ ] Apply the change: merge each delta into the corresponding spec.
|
|
16
|
-
- [ ] Archive the change folder to `.doctrina/changes/archive/{{DATE}}-{{CHANGE_ID}}/`.
|
|
17
|
-
- [ ] Update `.doctrina/index.json` with new or modified artifacts.
|
|
@@ -107,8 +107,8 @@ Example (Behave): features/**/*.feature with pattern @([a-z0-9_-]+)
|
|
|
107
107
|
<!--
|
|
108
108
|
Ceilings that are a CONTRACT, not a preference: token limits, payload
|
|
109
109
|
caps, timeouts. Direction distinguishes what goes IN from what comes
|
|
110
|
-
OUT, because they fail differently — and `doctrina
|
|
111
|
-
change that "fixes" an overflow by raising an OUTPUT ceiling, which
|
|
110
|
+
OUT, because they fail differently — and `doctrina change check` and the
|
|
111
|
+
close refuse a change that "fixes" an overflow by raising an OUTPUT ceiling, which
|
|
112
112
|
buys headroom by truncating what mattered rather than sending less.
|
|
113
113
|
-->
|
|
114
114
|
|
|
@@ -26,3 +26,13 @@ $DOCTRINA validate --fix || exit 1
|
|
|
26
26
|
if [ -f .doctrina/index.json ]; then
|
|
27
27
|
git add .doctrina/index.json
|
|
28
28
|
fi
|
|
29
|
+
|
|
30
|
+
# ...and then check that the index this commit carries describes THIS COMMIT.
|
|
31
|
+
#
|
|
32
|
+
# Everything above reads the working tree. index.json is derived from the
|
|
33
|
+
# whole tree, so it cannot describe a subset of one: stage part of
|
|
34
|
+
# .doctrina/ and the index that just got staged names artifacts the commit
|
|
35
|
+
# does not contain. On your disk they are all still there, so `validate`,
|
|
36
|
+
# `index rebuild --check` and the close all pass — and the checkout in CI
|
|
37
|
+
# fails with "index.json references missing artifact".
|
|
38
|
+
$DOCTRINA index rebuild --check --staged || exit 1
|
|
@@ -35,8 +35,12 @@ Execute in order, in a single linear pass:
|
|
|
35
35
|
If something in the intake is genuinely ambiguous, ask the user
|
|
36
36
|
before assuming.
|
|
37
37
|
|
|
38
|
-
7. Mark the intake consumed
|
|
39
|
-
|
|
38
|
+
7. Mark the intake consumed — through the CLI, not by hand:
|
|
39
|
+
[[c]]doctrina intake --converted[[/c]]
|
|
40
|
+
[[g]] `next` branches on that header and `validate` refuses any other[[/g]]
|
|
41
|
+
[[g]] value, because every word but the two declared ones used to read[[/g]]
|
|
42
|
+
[[g]] as pending in silence. It refuses while `validate` reports an[[/g]]
|
|
43
|
+
[[g]] error — the step-6 gates are what "done" means.[[/g]]
|
|
40
44
|
|
|
41
45
|
8. [[c]]doctrina next[[/c]] — follow the recommendation. Implementation then
|
|
42
46
|
flows through [[c]]doctrina work "<brief prompt>"[[/c]], one change at a time.
|
|
@@ -6,14 +6,19 @@ Execute in order, in a single linear pass:
|
|
|
6
6
|
|
|
7
7
|
1. Replace the placeholder tasks in .doctrina/changes/{{CHANGE_ID}}/tasks.md
|
|
8
8
|
with small, checkable steps; implement them, checking each box.
|
|
9
|
-
[[g]] (Do it before implementing —
|
|
9
|
+
[[g]] (Do it before implementing — the close refuses leftover scaffold[[/g]]
|
|
10
10
|
[[g]] placeholders, and `change tick` will not tick an empty box.)[[/g]]
|
|
11
|
-
2. Prove it (
|
|
11
|
+
2. Prove it (the close refuses unchecked boxes):
|
|
12
12
|
[[c]]doctrina verify[[/c]] — the project's typecheck/test/build gate
|
|
13
13
|
[[c]]doctrina verify --clean[[/c]] — clean-checkout reproducibility lint
|
|
14
14
|
Then check the proposal's ## Verification boxes and every task.
|
|
15
15
|
3. This is a chore: no spec changes. If you find yourself needing a
|
|
16
16
|
spec delta, it is not a chore — reopen with [[c]]doctrina work "<prompt>"[[/c]].
|
|
17
|
-
4.
|
|
18
|
-
|
|
17
|
+
4. Close in one attested pass — the same close as any change; a chore
|
|
18
|
+
simply has no delta to apply:
|
|
19
|
+
[[c]]doctrina close {{CHANGE_ID}}[[/c]]
|
|
20
|
+
[[g]] It stops at the first failure with the exact rerun command, and the[[/g]]
|
|
21
|
+
[[g]] sequence is printed by the close itself. A chore that alters a[[/g]]
|
|
22
|
+
[[g]] documented surface still owes the docs and a changelog entry — or[[/g]]
|
|
23
|
+
[[g]] `- **Documented surface:** n/a — <why>` in the proposal.[[/g]]
|
|
19
24
|
[[g]] If this fix taught a lesson worth not relearning, capture it: [[/g]][[c]]doctrina skill new <slug>[[/c]][[g]].[[/g]]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[[b]]Consolidation playbook — change {{CHANGE_ID}}[[/b]][[g]] — agent-executed (ADR 0005); the spec shrinks, the behaviour does not change.[[/g]]
|
|
2
|
+
|
|
3
|
+
Capability: [[c]]{{CAPABILITY}}[[/c]] — {{SHAPE}}
|
|
4
|
+
|
|
5
|
+
Execute in order, in a single linear pass:
|
|
6
|
+
|
|
7
|
+
1. Read the spec and its history:
|
|
8
|
+
[[c]]doctrina context {{CAPABILITY}} --concat[[/c]]
|
|
9
|
+
[[c]]doctrina why {{CAPABILITY}}[[/c]]
|
|
10
|
+
2. Find what the spec says more than once: a requirement a later one
|
|
11
|
+
superseded, two requirements stating the same rule, a criterion proven
|
|
12
|
+
by another one, a bullet describing behaviour that no longer exists.
|
|
13
|
+
You decide; the CLI only measures. Keep every rule the code still
|
|
14
|
+
honours — consolidating removes words, never guarantees.
|
|
15
|
+
3. Write the delta with the ops (the scaffold is at
|
|
16
|
+
.doctrina/changes/{{CHANGE_ID}}/specs/{{CAPABILITY}}/delta.md):
|
|
17
|
+
[[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --op "remove-requirement <section> <n>: <why>"[[/c]]
|
|
18
|
+
[[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --op "replace-requirement <section> <n>: <merged text>"[[/c]]
|
|
19
|
+
[[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --op "remove-criterion <n>: <why>"[[/c]]
|
|
20
|
+
[[c]]doctrina change delta {{CHANGE_ID}} {{CAPABILITY}} --bump minor[[/c]]
|
|
21
|
+
[[g]] Ops run in order and a removal renumbers what follows it: write the[[/g]]
|
|
22
|
+
[[g]] removals of one section highest number first. Every kept criterion[[/g]]
|
|
23
|
+
[[g]] must still cite proof that exists — coverage gates the close.[[/g]]
|
|
24
|
+
[[g]] Preview the merged spec: [[/g]][[c]]doctrina change check {{CHANGE_ID}} --verbose[[/c]]
|
|
25
|
+
4. Fill the proposal's What (what was merged or removed, and why each) and
|
|
26
|
+
replace the placeholder tasks; no code changes.
|
|
27
|
+
5. Close in one attested pass:
|
|
28
|
+
[[c]]doctrina close {{CHANGE_ID}}[[/c]]
|
|
@@ -40,12 +40,15 @@ Execute in order, in a single linear pass:
|
|
|
40
40
|
[[g]] append-* ops number/position at APPLY time, so concurrent changes[[/g]]
|
|
41
41
|
[[g]] appending to the same spec never collide. Only free-prose rewrites[[/g]]
|
|
42
42
|
[[g]] (Purpose, Maturity, ...) stay a by-hand merge.[[/g]]
|
|
43
|
+
[[g]] Or let the CLI write the lines and dry-run them against the spec:[[/g]]
|
|
44
|
+
[[g]] doctrina change delta {{CHANGE_ID}} <capability> --requirement "event: ..." \[[/g]]
|
|
45
|
+
[[g]] --criterion "[unverified] ... — verified by `...`" --bump minor[[/g]]
|
|
43
46
|
{{FROM_DIFF_DELTA_NOTE}}
|
|
44
47
|
|
|
45
48
|
4. Replace the placeholder tasks in .doctrina/changes/{{CHANGE_ID}}/tasks.md
|
|
46
49
|
with small, checkable implementation tasks (a few hours each, max),
|
|
47
50
|
and record the change's What/Scope in its proposal.md. Do this BEFORE
|
|
48
|
-
implementing —
|
|
51
|
+
implementing — `change check` and close refuse a change whose scaffold
|
|
49
52
|
placeholders were never replaced, and `change tick` will not tick them.
|
|
50
53
|
|
|
51
54
|
5. Implement task by task, checking each box as it lands. Advance the
|
|
@@ -67,9 +70,16 @@ Execute in order, in a single linear pass:
|
|
|
67
70
|
[[g]] It self-reviews on the way through: the close runs `doctrina review`[[/g]]
|
|
68
71
|
[[g]] before the apply, where its findings can still change what gets[[/g]]
|
|
69
72
|
[[g]] written. Advisory — it reports and the close continues.[[/g]]
|
|
70
|
-
[[g]]
|
|
73
|
+
[[g]] The sequence is declared in one place and printed by the close itself,[[/g]]
|
|
74
|
+
[[g]] so read it there rather than from a copy that goes stale.[[/g]]
|
|
71
75
|
Before it: check the proposal's ## Verification boxes and every task,
|
|
72
|
-
|
|
76
|
+
and bump Implementation to verified.
|
|
77
|
+
Two gates ask for WRITING, and both refuse late if you leave them:
|
|
78
|
+
a change that alters a documented surface needs the documentation to
|
|
79
|
+
move with it, AND an entry in the project's changelog — one says how it
|
|
80
|
+
works, the other says that it changed. If the command and flag names in
|
|
81
|
+
your prose are only MENTIONED, declare it instead of forcing:
|
|
82
|
+
[[c]]- **Documented surface:** n/a — <why>[[/c]] in the proposal.
|
|
73
83
|
|
|
74
84
|
8. [[c]]doctrina next[[/c]] for the follow-up.
|
|
75
85
|
[[g]] If this change taught a reusable lesson (a fix you'd hate to relearn,[[/g]]
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// @ts-check
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import process from "node:process";
|
|
4
|
-
import { readdirSync } from "node:fs";
|
|
5
|
-
import { exists, isDir, isFile, read } from "../lib/fs-ops.js";
|
|
6
|
-
import { listHeader } from "../lib/scan.js";
|
|
7
|
-
import { c } from "../lib/colors.js";
|
|
8
|
-
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
9
|
-
import { acceptedDecisions, productSection } from "../lib/constitution-model.js";
|
|
10
|
-
import { collectSnapshot } from "../lib/snapshot.js";
|
|
11
|
-
import { renderView } from "../lib/views.js";
|
|
12
|
-
|
|
13
|
-
// Read by the project snapshot as well; see lib/constitution-model.js.
|
|
14
|
-
export { acceptedDecisions, productSection } from "../lib/constitution-model.js";
|
|
15
|
-
|
|
16
|
-
// The project's standing rules in one read — Spec Kit parity for its
|
|
17
|
-
// `constitution.md`, but ASSEMBLED, not a new home for facts. Doctrina's
|
|
18
|
-
// constitution is its accepted ADRs (the immutable decisions that govern how
|
|
19
|
-
// the codebase evolves) plus the product's declared non-goals. Both already
|
|
20
|
-
// own those facts elsewhere; this is a read-only digest that gathers them so a
|
|
21
|
-
// human or agent (especially one migrating from Spec Kit, which expects a
|
|
22
|
-
// single constitution) can see "the rules" without reading the whole tree.
|
|
23
|
-
//
|
|
24
|
-
// To change a principle, supersede its ADR; to change a non-goal, edit
|
|
25
|
-
// product.md. The command never writes — it has nothing of its own to own.
|
|
26
|
-
|
|
27
|
-
// Flags this command accepts. Declared HERE, with the command, so
|
|
28
|
-
// adding a command never requires editing the entrypoint — the gap that
|
|
29
|
-
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
30
|
-
export const flags = { boolean: ["json"], string: [] };
|
|
31
|
-
|
|
32
|
-
export async function run(_positional, _flags) {
|
|
33
|
-
const projectRoot = process.cwd();
|
|
34
|
-
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
35
|
-
throw notADoctrinaProject();
|
|
36
|
-
}
|
|
37
|
-
// Deprecated (change 0049): the standing rules are a VIEW of the shared
|
|
38
|
-
// collection, and `prime --rules` renders it. This command prints the same
|
|
39
|
-
// lines — not a similar set, the same ones — so the deprecation is a
|
|
40
|
-
// rename with a warning rather than a loss.
|
|
41
|
-
for (const line of renderView("rules", collectSnapshot(projectRoot))) console.log(line);
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function projectName(projectRoot) {
|
|
46
|
-
try {
|
|
47
|
-
return JSON.parse(read(path.join(projectRoot, ".doctrina", "index.json"))).project
|
|
48
|
-
?? path.basename(projectRoot);
|
|
49
|
-
} catch {
|
|
50
|
-
return path.basename(projectRoot);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
export const help = `
|
|
56
|
-
Usage: doctrina constitution
|
|
57
|
-
|
|
58
|
-
Print the project's standing rules in one read: the accepted ADRs (the
|
|
59
|
-
immutable decisions that govern how the codebase evolves) and the product's
|
|
60
|
-
declared non-goals. Read-only — it assembles facts the ADRs and product.md
|
|
61
|
-
already own; it never writes.
|
|
62
|
-
|
|
63
|
-
This is the Spec Kit \`constitution.md\` analogue: a single place to see the
|
|
64
|
-
non-negotiables. To change one, supersede the ADR or edit product.md.
|
|
65
|
-
|
|
66
|
-
DEPRECATED: use \`doctrina prime --rules\`, which prints exactly these lines.
|
|
67
|
-
This name keeps working and will be removed in a future minor.
|
|
68
|
-
`;
|