doctrina-cli 0.11.0 → 0.13.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/package.json +1 -1
- package/src/commands/analyze.js +18 -2
- package/src/commands/change.js +322 -12
- package/src/commands/clarify.js +97 -11
- package/src/commands/close.js +99 -13
- package/src/commands/context.js +10 -1
- package/src/commands/coverage.js +157 -41
- package/src/commands/init.js +15 -2
- package/src/commands/intent.js +173 -0
- package/src/commands/review.js +25 -4
- package/src/commands/skill.js +46 -7
- package/src/commands/spec.js +13 -3
- package/src/commands/templates.js +113 -10
- package/src/commands/upgrade.js +133 -0
- package/src/commands/validate.js +152 -3
- package/src/commands/why.js +11 -1
- package/src/commands/work.js +105 -21
- package/src/index.js +11 -2
- package/src/lib/adr-guard.js +46 -0
- package/src/lib/commands.js +105 -4
- package/src/lib/scan.js +16 -0
- package/src/lib/spec-ops.js +143 -3
- package/templates/.gitkeep +0 -0
- package/templates/AGENTS.md.template +25 -31
- package/templates/change/spec-delta.md.template +13 -7
- package/templates/spec.md.template +9 -1
package/src/lib/commands.js
CHANGED
|
@@ -13,13 +13,13 @@ export const COMMAND_NAMES = [
|
|
|
13
13
|
// bootstrap / day-to-day
|
|
14
14
|
"init", "intake", "work",
|
|
15
15
|
// authoring
|
|
16
|
-
"spec", "change", "contract", "decision", "skill",
|
|
16
|
+
"spec", "change", "contract", "decision", "skill", "intent",
|
|
17
17
|
// read / orient
|
|
18
18
|
"prime", "context", "show", "search", "status", "next", "why", "handoff", "constitution",
|
|
19
19
|
// gates
|
|
20
20
|
"analyze", "clarify", "validate", "coverage", "trace", "review", "verify", "close", "doctor",
|
|
21
21
|
// maintenance
|
|
22
|
-
"templates", "hooks", "index", "watch", "metrics", "report", "completion",
|
|
22
|
+
"templates", "hooks", "index", "watch", "metrics", "report", "completion", "upgrade",
|
|
23
23
|
];
|
|
24
24
|
|
|
25
25
|
// The full operation surface — every `doctrina <command> [<subcommand>]` a
|
|
@@ -40,8 +40,10 @@ export const OPERATIONS = [
|
|
|
40
40
|
["spec list", "List specs with version, status, and size"],
|
|
41
41
|
["spec set", "Edit spec headers / a criterion mark and resync the index"],
|
|
42
42
|
["change new", "Open a change proposal"],
|
|
43
|
-
["change apply", "Apply spec deltas (
|
|
44
|
-
["change archive", "Archive an applied change"],
|
|
43
|
+
["change apply", "Apply spec deltas (ops blocks mechanically; batch ids ok)"],
|
|
44
|
+
["change archive", "Archive an applied change (batch ids ok)"],
|
|
45
|
+
["change check", "Pre-close dry-run: everything close would refuse, listed first"],
|
|
46
|
+
["change tick", "List/tick the unchecked boxes (tasks + verification; --all)"],
|
|
45
47
|
["change diff", "Preview spec deltas (line diff for MODIFIED)"],
|
|
46
48
|
["change abandon", "Discard an open change cleanly (recorded in the ledger)"],
|
|
47
49
|
["contract new", "Own the integration surface (ports, env, interfaces)"],
|
|
@@ -56,6 +58,8 @@ export const OPERATIONS = [
|
|
|
56
58
|
["skill list", "List skills with their descriptions"],
|
|
57
59
|
["skill sync", "Mirror skill frontmatter descriptions into index.json"],
|
|
58
60
|
["skill suggest", "Surface fix-shaped lessons worth a skill (--write scaffolds)"],
|
|
61
|
+
["intent add", "Append a new product intent anchor post-intake (- [SC5] ...)"],
|
|
62
|
+
["intent list", "List product.md intent anchors in document order"],
|
|
59
63
|
["analyze", "Inspect a change folder before applying"],
|
|
60
64
|
["clarify", "Smell-test a Markdown file for ambiguity (--all for the tree)"],
|
|
61
65
|
["prime", "Session primer: gates, rules, open work, next steps in one read"],
|
|
@@ -83,6 +87,7 @@ export const OPERATIONS = [
|
|
|
83
87
|
["metrics", "Local git-derived adoption metrics (no network)"],
|
|
84
88
|
["report", "Markdown digest for a period: changes, gates, git summary"],
|
|
85
89
|
["completion", "Print bash/zsh/pwsh completions (generated from the catalog)"],
|
|
90
|
+
["upgrade", "Bring an existing project up to the installed CLI (--write applies)"],
|
|
86
91
|
];
|
|
87
92
|
|
|
88
93
|
// The Commands block of `doctrina --help`, generated from OPERATIONS so the
|
|
@@ -91,6 +96,102 @@ export function surfaceHelp() {
|
|
|
91
96
|
return OPERATIONS.map(([op, summary]) => ` ${op.padEnd(21)}${summary}`).join("\n");
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// The CLI-owned AGENTS.md command-surface block (operator review 2026-07-19,
|
|
101
|
+
// meta-conclusion): for an agent, AGENTS.md IS the discovery interface — it
|
|
102
|
+
// never runs `--help` spontaneously — so every command absent from the hub
|
|
103
|
+
// was invisible for ~35 changes (`prime`, `handoff`, `doctor`, `show`, ...).
|
|
104
|
+
// The fix is a marker-delimited section GENERATED from OPERATIONS, so the
|
|
105
|
+
// hub's catalog is the CLI's catalog by construction: `init` scaffolds it,
|
|
106
|
+
// `templates check` flags it stale, and `templates update --write` (hence
|
|
107
|
+
// `doctrina upgrade --write`) regenerates exactly the marked span and
|
|
108
|
+
// nothing else. Content between the markers is CLI-owned; everything
|
|
109
|
+
// outside remains the adopter's, under the additive-only guarantee.
|
|
110
|
+
|
|
111
|
+
export const SURFACE_BEGIN =
|
|
112
|
+
"<!-- doctrina:surface:begin — CLI-owned block, generated from the installed command catalog. Refreshed by `doctrina upgrade --write`; edits inside are overwritten. -->";
|
|
113
|
+
export const SURFACE_END = "<!-- doctrina:surface:end -->";
|
|
114
|
+
|
|
115
|
+
// Workflow grouping for the generated block. A test asserts the flattened
|
|
116
|
+
// names equal COMMAND_NAMES, so a new command cannot ship ungrouped (and
|
|
117
|
+
// therefore cannot ship invisible to AGENTS.md).
|
|
118
|
+
export const SURFACE_GROUPS = [
|
|
119
|
+
["Start", ["init", "intake", "work"]],
|
|
120
|
+
["Author", ["spec", "change", "contract", "decision", "skill", "intent"]],
|
|
121
|
+
["Read / orient", ["prime", "context", "show", "search", "status", "next", "why", "handoff", "constitution"]],
|
|
122
|
+
["Gates", ["analyze", "clarify", "validate", "coverage", "trace", "review", "verify", "close", "doctor"]],
|
|
123
|
+
["Maintain", ["templates", "hooks", "index", "watch", "metrics", "report", "completion", "upgrade"]],
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
// Per-command usage hints appended to the generated reference — arguments and
|
|
127
|
+
// the flags an agent reaches for daily. Cosmetic only; the operation list
|
|
128
|
+
// itself always comes from OPERATIONS.
|
|
129
|
+
const SURFACE_HINTS = {
|
|
130
|
+
work: '"<prompt>" (--capability · --chore · --from-diff · --quiet)',
|
|
131
|
+
context: "[<cap>] --concat",
|
|
132
|
+
validate: "(--fix)",
|
|
133
|
+
coverage: "--strict",
|
|
134
|
+
trace: "--strict",
|
|
135
|
+
clarify: "--all (--lang pt|en)",
|
|
136
|
+
close: "<id...>",
|
|
137
|
+
upgrade: "--write",
|
|
138
|
+
prime: "(session start)",
|
|
139
|
+
handoff: "(before compaction/handover)",
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// The full managed section, heading included, between the markers.
|
|
143
|
+
export function surfaceMarkdown() {
|
|
144
|
+
const subsByCmd = new Map();
|
|
145
|
+
for (const [op] of OPERATIONS) {
|
|
146
|
+
const [cmd, sub] = op.split(" ");
|
|
147
|
+
if (!subsByCmd.has(cmd)) subsByCmd.set(cmd, []);
|
|
148
|
+
if (sub) subsByCmd.get(cmd).push(sub);
|
|
149
|
+
}
|
|
150
|
+
const renderCmd = (cmd) => {
|
|
151
|
+
const subs = subsByCmd.get(cmd) ?? [];
|
|
152
|
+
const hint = SURFACE_HINTS[cmd] ? ` ${SURFACE_HINTS[cmd]}` : "";
|
|
153
|
+
return subs.length > 0 ? `\`doctrina ${cmd} ${subs.join("|")}\`` : `\`doctrina ${cmd}${hint}\``;
|
|
154
|
+
};
|
|
155
|
+
const lines = [
|
|
156
|
+
"## Doctrina command surface (generated — reach for these, don't hand-author)",
|
|
157
|
+
"",
|
|
158
|
+
"Every operation of the installed CLI, grouped by workflow. The CLI",
|
|
159
|
+
"scaffolds from canonical templates and keeps `index.json` in sync, so",
|
|
160
|
+
"prefer it over writing artifacts by hand. Details: `doctrina --help`.",
|
|
161
|
+
"",
|
|
162
|
+
];
|
|
163
|
+
for (const [label, cmds] of SURFACE_GROUPS) {
|
|
164
|
+
lines.push(`- **${label}:** ${cmds.map(renderCmd).join(" · ")}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push("Session bookends: `doctrina prime` to orient at session start;");
|
|
168
|
+
lines.push("`doctrina handoff` before compaction or handover. Continuous:");
|
|
169
|
+
lines.push("`doctrina watch`. Capture lessons: `doctrina skill suggest`.");
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The block as written to AGENTS.md: markers wrapping the generated section.
|
|
174
|
+
export function surfaceBlock() {
|
|
175
|
+
return `${SURFACE_BEGIN}\n${surfaceMarkdown()}\n${SURFACE_END}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Locate the managed block in an AGENTS.md text. Returns
|
|
179
|
+
// { start, end, inner } (string offsets; inner excludes the marker lines)
|
|
180
|
+
// or null when either marker is absent.
|
|
181
|
+
export function findSurfaceBlock(text) {
|
|
182
|
+
const begin = text.match(/<!--\s*doctrina:surface:begin[\s\S]*?-->/);
|
|
183
|
+
if (!begin) return null;
|
|
184
|
+
const endRe = /<!--\s*doctrina:surface:end\s*-->/;
|
|
185
|
+
const end = endRe.exec(text.slice(begin.index + begin[0].length));
|
|
186
|
+
if (!end) return null;
|
|
187
|
+
const innerStart = begin.index + begin[0].length;
|
|
188
|
+
return {
|
|
189
|
+
start: begin.index,
|
|
190
|
+
end: innerStart + end.index + end[0].length,
|
|
191
|
+
inner: text.slice(innerStart, innerStart + end.index).replace(/^\r?\n|\r?\n$/g, ""),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
94
195
|
// Extract the set of top-level `doctrina <command>` references that appear in
|
|
95
196
|
// CODE context (fenced blocks or inline backtick spans) of a Markdown file —
|
|
96
197
|
// AGENTS.md, typically. Restricting to code context avoids matching prose like
|
package/src/lib/scan.js
CHANGED
|
@@ -25,6 +25,15 @@ export function listHeader(text, name) {
|
|
|
25
25
|
return m ? m[1].trim() : null;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// The machine-readable "**Depends on:** defects, decisions" spec header: the
|
|
29
|
+
// sibling capabilities this spec builds on. Returns [] when absent, "n/a", or
|
|
30
|
+
// empty. One parser so index, why, context, and review agree.
|
|
31
|
+
export function parseDependsOn(text) {
|
|
32
|
+
const raw = specHeader(text, "Depends on");
|
|
33
|
+
if (!raw || /^n\/a\b/i.test(raw.trim()) || raw.trim() === "—") return [];
|
|
34
|
+
return raw.match(/[a-z][a-z0-9][a-z0-9-]*/g) ?? [];
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
function dirEntries(dir) {
|
|
29
38
|
if (!isDir(dir)) return [];
|
|
30
39
|
return readdirSync(dir).filter((e) => !e.startsWith(".")).sort();
|
|
@@ -87,6 +96,13 @@ export function deriveIndex(projectRoot, current) {
|
|
|
87
96
|
const realizesRaw = specHeader(text, "Realizes");
|
|
88
97
|
const realizes = realizesRaw ? (realizesRaw.match(/[A-Z]+\d+/g) ?? []) : prev?.realizes;
|
|
89
98
|
if (realizes && realizes.length) entry.realizes = realizes;
|
|
99
|
+
// Depends on: sibling capabilities this spec builds on (0.11.0 field
|
|
100
|
+
// review — specs cited each other only in prose, unreadable by machine).
|
|
101
|
+
// Stored as a capability-id list so `why` can show the graph, `context`
|
|
102
|
+
// can pull dependencies into the read pack, and `review` can flag
|
|
103
|
+
// dependents of a touched capability.
|
|
104
|
+
const depends = specHeader(text, "Depends on") !== null ? parseDependsOn(text) : prev?.depends_on;
|
|
105
|
+
if (depends && depends.length) entry.depends_on = depends;
|
|
90
106
|
out.artifacts.specs.push(entry);
|
|
91
107
|
}
|
|
92
108
|
|
package/src/lib/spec-ops.js
CHANGED
|
@@ -17,8 +17,17 @@
|
|
|
17
17
|
// set-criterion 1: verified
|
|
18
18
|
// replace-criterion 2: [verified] new text — verified by `test/x.test.ts`
|
|
19
19
|
// append-criterion [unverified] new signal — verified by `test/y.test.ts`
|
|
20
|
+
// append-requirement event: When X occurs, the system shall do Y.
|
|
21
|
+
// replace-requirement ubiquitous 2: The system shall do Z.
|
|
20
22
|
// ```
|
|
21
23
|
//
|
|
24
|
+
// The requirement verbs (operator review 2026-07-19 §4.1) cover the dominant
|
|
25
|
+
// MODIFIED case — inserting or rewording EARS bullets in a `## Requirements
|
|
26
|
+
// (EARS)` subsection — so a typical delta no longer falls back to a manual
|
|
27
|
+
// merge. Because append-* verbs resolve their position/number at APPLY time,
|
|
28
|
+
// concurrent open changes appending to the same spec cannot collide on
|
|
29
|
+
// numbering: order of application decides, mechanically.
|
|
30
|
+
//
|
|
22
31
|
// A `# comment` or blank line inside the block is ignored. When a delta has
|
|
23
32
|
// no ops block, apply falls back to the manual-merge pointer (backward
|
|
24
33
|
// compatible).
|
|
@@ -48,9 +57,13 @@ export function extractOps(deltaText) {
|
|
|
48
57
|
|
|
49
58
|
// Find the body of the first ```ops fenced block, or null if absent. The
|
|
50
59
|
// info string must be exactly "ops" (case-insensitive) so a normal code
|
|
51
|
-
// fence in the prose is never mistaken for operations.
|
|
60
|
+
// fence in the prose is never mistaken for operations. HTML comments are
|
|
61
|
+
// stripped first: the scaffolded delta template carries an EXAMPLE ops block
|
|
62
|
+
// inside its instructional comment, and executing an example against a real
|
|
63
|
+
// spec is exactly the surprise this guard removes.
|
|
52
64
|
function matchOpsBlock(text) {
|
|
53
|
-
const
|
|
65
|
+
const visible = text.replace(/<!--[\s\S]*?-->/g, "");
|
|
66
|
+
const m = visible.match(/^[ \t]*```[ \t]*ops[ \t]*\r?\n([\s\S]*?)^[ \t]*```[ \t]*$/m);
|
|
54
67
|
return m ? m[1] : null;
|
|
55
68
|
}
|
|
56
69
|
|
|
@@ -92,11 +105,50 @@ function parseOpLine(line) {
|
|
|
92
105
|
if (!rest) return { verb, error: "append-criterion is missing its text" };
|
|
93
106
|
return { verb, value: rest };
|
|
94
107
|
}
|
|
108
|
+
case "append-requirement": {
|
|
109
|
+
// append-requirement <section>: <text>
|
|
110
|
+
const colon = rest.indexOf(":");
|
|
111
|
+
if (colon < 0) return { verb, error: `append-requirement needs "<section>: <text>" (got "${rest}")` };
|
|
112
|
+
const section = normalizeSection(rest.slice(0, colon).trim());
|
|
113
|
+
const value = rest.slice(colon + 1).trim();
|
|
114
|
+
if (!section) return { verb, error: `append-requirement: unknown section "${rest.slice(0, colon).trim()}" (use ${SECTION_TOKENS})` };
|
|
115
|
+
if (!value) return { verb, error: "append-requirement is missing the requirement text" };
|
|
116
|
+
return { verb, section, value };
|
|
117
|
+
}
|
|
118
|
+
case "replace-requirement": {
|
|
119
|
+
// replace-requirement <section> <n>: <text>
|
|
120
|
+
const colon = rest.indexOf(":");
|
|
121
|
+
if (colon < 0) return { verb, error: `replace-requirement needs "<section> <n>: <text>" (got "${rest}")` };
|
|
122
|
+
const head = rest.slice(0, colon).trim().split(/\s+/);
|
|
123
|
+
const value = rest.slice(colon + 1).trim();
|
|
124
|
+
const n = Number(head[head.length - 1]);
|
|
125
|
+
const section = normalizeSection(head.slice(0, -1).join(" "));
|
|
126
|
+
if (!section) return { verb, error: `replace-requirement: unknown section "${head.slice(0, -1).join(" ")}" (use ${SECTION_TOKENS})` };
|
|
127
|
+
if (!Number.isInteger(n) || n < 1) return { verb, error: `replace-requirement needs a positive bullet number (got "${rest}")` };
|
|
128
|
+
if (!value) return { verb, error: `replace-requirement ${n} is missing the requirement text` };
|
|
129
|
+
return { verb, section, n, value };
|
|
130
|
+
}
|
|
95
131
|
default:
|
|
96
132
|
return { verb: null, error: `unknown operation "${verb}"` };
|
|
97
133
|
}
|
|
98
134
|
}
|
|
99
135
|
|
|
136
|
+
// EARS subsection aliases -> the canonical token used to match the spec's
|
|
137
|
+
// `### ` heading under `## Requirements (EARS)`. Deterministic: first word of
|
|
138
|
+
// the heading, folded ("Unwanted-behavior (must-not)" -> "unwanted").
|
|
139
|
+
const SECTIONS = {
|
|
140
|
+
ubiquitous: "ubiquitous",
|
|
141
|
+
event: "event", "event-driven": "event",
|
|
142
|
+
state: "state", "state-driven": "state",
|
|
143
|
+
unwanted: "unwanted", "unwanted-behavior": "unwanted", "must-not": "unwanted",
|
|
144
|
+
optional: "optional",
|
|
145
|
+
};
|
|
146
|
+
const SECTION_TOKENS = "ubiquitous|event|state|unwanted|optional";
|
|
147
|
+
|
|
148
|
+
function normalizeSection(raw) {
|
|
149
|
+
return SECTIONS[raw.toLowerCase()] ?? null;
|
|
150
|
+
}
|
|
151
|
+
|
|
100
152
|
// Apply a parsed op list to a spec's Markdown text. Pure: returns
|
|
101
153
|
// { text, applied: string[], errors: string[] } and never throws. A failed
|
|
102
154
|
// op is recorded in `errors` and leaves the text unchanged for that op, so
|
|
@@ -132,6 +184,10 @@ function applyOne(text, op) {
|
|
|
132
184
|
return replaceCriterion(text, op.n, op.value);
|
|
133
185
|
case "append-criterion":
|
|
134
186
|
return appendCriterion(text, op.value);
|
|
187
|
+
case "append-requirement":
|
|
188
|
+
return appendRequirement(text, op.section, op.value);
|
|
189
|
+
case "replace-requirement":
|
|
190
|
+
return replaceRequirement(text, op.section, op.n, op.value);
|
|
135
191
|
default:
|
|
136
192
|
return { error: `unknown operation "${op.verb}"` };
|
|
137
193
|
}
|
|
@@ -221,6 +277,90 @@ export function replaceCriterion(text, n, value) {
|
|
|
221
277
|
return { text: loc.lines.join("\n"), summary: `replace-criterion ${n}` };
|
|
222
278
|
}
|
|
223
279
|
|
|
280
|
+
// The `### <Section>` blocks under `## Requirements (EARS)`. Returns
|
|
281
|
+
// { lines, sections: { <token>: { headingIndex, endIndex, bullets: [lineIndex] } } }
|
|
282
|
+
// or null when the Requirements section is absent. A section's token is the
|
|
283
|
+
// first word of its heading, folded through SECTIONS ("Unwanted-behavior
|
|
284
|
+
// (must-not)" -> "unwanted"). endIndex is exclusive: the line index of the
|
|
285
|
+
// next heading (### or ##) after the section, or lines.length.
|
|
286
|
+
function locateRequirements(text) {
|
|
287
|
+
const lines = text.split(/\r?\n/);
|
|
288
|
+
let inRequirements = false;
|
|
289
|
+
let current = null;
|
|
290
|
+
const sections = {};
|
|
291
|
+
for (let i = 0; i < lines.length; i++) {
|
|
292
|
+
const line = lines[i];
|
|
293
|
+
if (/^##\s+/.test(line) && !/^###/.test(line)) {
|
|
294
|
+
if (current) { current.endIndex = i; current = null; }
|
|
295
|
+
inRequirements = /^##\s+Requirements\b/i.test(line);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (!inRequirements) continue;
|
|
299
|
+
const h = line.match(/^###\s+(\S+)/);
|
|
300
|
+
if (h) {
|
|
301
|
+
if (current) current.endIndex = i;
|
|
302
|
+
const token = SECTIONS[h[1].toLowerCase().replace(/[^a-z-]/g, "")] ?? null;
|
|
303
|
+
current = { headingIndex: i, endIndex: lines.length, bullets: [] };
|
|
304
|
+
if (token) sections[token] = current;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (current && /^\s*-(\s|$)/.test(line)) current.bullets.push(i);
|
|
308
|
+
}
|
|
309
|
+
return Object.keys(sections).length > 0 || /^##\s+Requirements\b/im.test(text)
|
|
310
|
+
? { lines, sections }
|
|
311
|
+
: null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// A list item may wrap over indented continuation lines ("- When X,\n
|
|
315
|
+
// the system shall ..."). Inserting right after the item's FIRST line
|
|
316
|
+
// splits it from its continuation — the bug the 0.13.0 dogfood close
|
|
317
|
+
// caught on this very repo's cli spec. From the item's first line, the
|
|
318
|
+
// item ends after every following line that is indented content.
|
|
319
|
+
function endOfItem(lines, startIndex) {
|
|
320
|
+
let i = startIndex;
|
|
321
|
+
while (i + 1 < lines.length && /^\s+\S/.test(lines[i + 1])) i += 1;
|
|
322
|
+
return i;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// append-requirement <section>: <text> — add "- <text>" at the end of the
|
|
326
|
+
// EARS subsection. A lone "-" placeholder bullet (the untouched scaffold) is
|
|
327
|
+
// replaced instead of appended after, so the first real requirement does not
|
|
328
|
+
// land under an empty stub.
|
|
329
|
+
export function appendRequirement(text, section, value) {
|
|
330
|
+
const loc = locateRequirements(text);
|
|
331
|
+
if (!loc) return { error: "append-requirement: spec has no '## Requirements (EARS)' section" };
|
|
332
|
+
const sec = loc.sections[section];
|
|
333
|
+
if (!sec) return { error: `append-requirement: no '### ${section}...' subsection found under Requirements` };
|
|
334
|
+
const placeholder = sec.bullets.length === 1 && /^\s*-\s*$/.test(loc.lines[sec.bullets[0]]);
|
|
335
|
+
if (placeholder) {
|
|
336
|
+
loc.lines[sec.bullets[0]] = `- ${value}`;
|
|
337
|
+
} else if (sec.bullets.length > 0) {
|
|
338
|
+
loc.lines.splice(endOfItem(loc.lines, sec.bullets[sec.bullets.length - 1]) + 1, 0, `- ${value}`);
|
|
339
|
+
} else {
|
|
340
|
+
// Empty section: insert after the heading (skipping one blank line if present).
|
|
341
|
+
let at = sec.headingIndex + 1;
|
|
342
|
+
while (at < sec.endIndex && loc.lines[at].trim() === "") at += 1;
|
|
343
|
+
loc.lines.splice(at, 0, `- ${value}`);
|
|
344
|
+
}
|
|
345
|
+
return { text: loc.lines.join("\n"), summary: `append-requirement ${section}: ${value.slice(0, 60)}` };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// replace-requirement <section> <n>: <text> — replace the nth bullet (1-based)
|
|
349
|
+
// of the EARS subsection.
|
|
350
|
+
export function replaceRequirement(text, section, n, value) {
|
|
351
|
+
const loc = locateRequirements(text);
|
|
352
|
+
if (!loc) return { error: "replace-requirement: spec has no '## Requirements (EARS)' section" };
|
|
353
|
+
const sec = loc.sections[section];
|
|
354
|
+
if (!sec) return { error: `replace-requirement: no '### ${section}...' subsection found under Requirements` };
|
|
355
|
+
const real = sec.bullets.filter((i) => !/^\s*-\s*$/.test(loc.lines[i]));
|
|
356
|
+
if (n > real.length) {
|
|
357
|
+
return { error: `replace-requirement: section "${section}" has ${real.length} requirement${real.length === 1 ? "" : "s"}, no #${n}` };
|
|
358
|
+
}
|
|
359
|
+
const indent = loc.lines[real[n - 1]].match(/^(\s*)/)[1];
|
|
360
|
+
loc.lines[real[n - 1]] = `${indent}- ${value}`;
|
|
361
|
+
return { text: loc.lines.join("\n"), summary: `replace-requirement ${section} ${n}` };
|
|
362
|
+
}
|
|
363
|
+
|
|
224
364
|
// append-criterion <text> — add a new numbered criterion after the last one
|
|
225
365
|
// in the section.
|
|
226
366
|
export function appendCriterion(text, value) {
|
|
@@ -231,6 +371,6 @@ export function appendCriterion(text, value) {
|
|
|
231
371
|
}
|
|
232
372
|
const last = loc.items[loc.items.length - 1];
|
|
233
373
|
const nextN = Math.max(...loc.items.map((it) => it.n)) + 1;
|
|
234
|
-
loc.lines.splice(last.lineIndex + 1, 0, `${last.indent}${nextN}. ${value}`);
|
|
374
|
+
loc.lines.splice(endOfItem(loc.lines, last.lineIndex) + 1, 0, `${last.indent}${nextN}. ${value}`);
|
|
235
375
|
return { text: loc.lines.join("\n"), summary: `append-criterion ${nextN}` };
|
|
236
376
|
}
|
|
File without changes
|
|
@@ -22,37 +22,31 @@ prompt and approves; do not make them run the CLI or hand-author artifacts.
|
|
|
22
22
|
spec each, `doctrina clarify --all` + `doctrina validate`, then flip the
|
|
23
23
|
intake to `Status: converted`). After conversion the specs are the only
|
|
24
24
|
source of truth — never edit `intake.md` to change requirements.
|
|
25
|
-
- **Day-to-day.**
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **
|
|
42
|
-
|
|
43
|
-
- **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`doctrina verify` · `doctrina review` (conformance vs specs/ADRs) ·
|
|
51
|
-
`doctrina coverage --strict` · `doctrina trace --strict` · `doctrina
|
|
52
|
-
contract check` · `doctrina clarify --all` · `doctrina analyze <id>`
|
|
53
|
-
|
|
54
|
-
Continuous: `doctrina watch` re-syncs and re-orients on every save.
|
|
55
|
-
Capture lessons: `doctrina skill suggest` surfaces skills worth writing.
|
|
25
|
+
- **Day-to-day.** Start every session with `doctrina prime` (gates, rules,
|
|
26
|
+
open work in one read). Turn any request into a change with `doctrina
|
|
27
|
+
work "<prompt>"` and follow the printed playbook (spec delta → tasks →
|
|
28
|
+
implement → close). Preview a close with `doctrina change check <id>`;
|
|
29
|
+
close with `doctrina close <id>`. Unsure what's next? `doctrina next`.
|
|
30
|
+
Before compaction or handover: `doctrina handoff`. Ask the human only on
|
|
31
|
+
genuine ambiguity.
|
|
32
|
+
|
|
33
|
+
<!-- doctrina:surface:begin — CLI-owned block, generated from the installed command catalog. Refreshed by `doctrina upgrade --write`; edits inside are overwritten. -->
|
|
34
|
+
## Doctrina command surface (generated — reach for these, don't hand-author)
|
|
35
|
+
|
|
36
|
+
Every operation of the installed CLI, grouped by workflow. The CLI
|
|
37
|
+
scaffolds from canonical templates and keeps `index.json` in sync, so
|
|
38
|
+
prefer it over writing artifacts by hand. Details: `doctrina --help`.
|
|
39
|
+
|
|
40
|
+
- **Start:** `doctrina init` · `doctrina intake` · `doctrina work "<prompt>" (--capability · --chore · --from-diff · --quiet)`
|
|
41
|
+
- **Author:** `doctrina spec new|list|set` · `doctrina change new|apply|archive|check|tick|diff|abandon` · `doctrina contract new|list|check` · `doctrina decision new|accept|land|supersede|list` · `doctrina skill new|list|sync|suggest` · `doctrina intent add|list`
|
|
42
|
+
- **Read / orient:** `doctrina prime (session start)` · `doctrina context [<cap>] --concat` · `doctrina show` · `doctrina search` · `doctrina status` · `doctrina next` · `doctrina why` · `doctrina handoff (before compaction/handover)` · `doctrina constitution`
|
|
43
|
+
- **Gates:** `doctrina analyze` · `doctrina clarify --all (--lang pt|en)` · `doctrina validate (--fix)` · `doctrina coverage --strict` · `doctrina trace --strict` · `doctrina review` · `doctrina verify` · `doctrina close <id...>` · `doctrina doctor`
|
|
44
|
+
- **Maintain:** `doctrina templates list|check|update` · `doctrina hooks install` · `doctrina index rebuild` · `doctrina watch` · `doctrina metrics` · `doctrina report` · `doctrina completion` · `doctrina upgrade --write`
|
|
45
|
+
|
|
46
|
+
Session bookends: `doctrina prime` to orient at session start;
|
|
47
|
+
`doctrina handoff` before compaction or handover. Continuous:
|
|
48
|
+
`doctrina watch`. Capture lessons: `doctrina skill suggest`.
|
|
49
|
+
<!-- doctrina:surface:end -->
|
|
56
50
|
|
|
57
51
|
## Stack and tooling
|
|
58
52
|
|
|
@@ -7,21 +7,27 @@
|
|
|
7
7
|
For ADDED: include the full new spec body below. On apply, the body is
|
|
8
8
|
written verbatim to the target path.
|
|
9
9
|
|
|
10
|
-
For MODIFIED:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
manual-merge pointer.
|
|
10
|
+
For MODIFIED: prefer a fenced `ops` block — on apply the CLI executes it
|
|
11
|
+
against the target spec, all ops or none (ADR 0007). The verbs cover
|
|
12
|
+
headers, acceptance criteria, AND the EARS requirement bullets, so a
|
|
13
|
+
typical delta applies mechanically end to end; only free-prose rewrites
|
|
14
|
+
(Purpose, Maturity, ...) stay a by-hand merge. A MODIFIED delta with no
|
|
15
|
+
`ops` block prints a manual-merge pointer.
|
|
17
16
|
|
|
18
17
|
```ops
|
|
19
18
|
set-header Implementation: verified — durable adapter (`src/db.ts`)
|
|
20
19
|
bump-version minor
|
|
21
20
|
set-criterion 1: verified
|
|
22
21
|
append-criterion [unverified] new signal — verified by `test/x.test.ts`
|
|
22
|
+
append-requirement event: When <trigger>, the system shall <action>.
|
|
23
|
+
replace-requirement ubiquitous 2: The system shall <action>.
|
|
23
24
|
```
|
|
24
25
|
|
|
26
|
+
Requirement sections: ubiquitous | event | state | unwanted | optional.
|
|
27
|
+
append-* ops resolve numbering/position at APPLY time, so several open
|
|
28
|
+
changes appending to the same spec never collide on numbers — order of
|
|
29
|
+
application decides.
|
|
30
|
+
|
|
25
31
|
For REMOVED: the body may be empty; on apply, the target spec file is
|
|
26
32
|
deleted and the capability is recorded in the change archive only.
|
|
27
33
|
-->
|
|
@@ -24,7 +24,15 @@ ADR 0006). Tag product.md success-criteria bullets with an anchor —
|
|
|
24
24
|
"- [SC1] ..." — and list the ones this spec delivers above. An "active"
|
|
25
25
|
spec with no Realizes: header warns under `doctrina validate`; if the
|
|
26
26
|
capability genuinely maps to no product anchor (internal tooling), keep
|
|
27
|
-
the header and write "n/a — <why>" to record that on purpose.
|
|
27
|
+
the header and write "n/a — <why>" to record that on purpose. Need a NEW
|
|
28
|
+
anchor for intent born after the intake? `doctrina intent add "<text>"`.
|
|
29
|
+
|
|
30
|
+
Optional: declare the sibling capabilities this spec builds on with a
|
|
31
|
+
machine-readable header — "**Depends on:** defects, decisions". It feeds
|
|
32
|
+
`doctrina why` (the dependency graph), `doctrina context <cap>` (the
|
|
33
|
+
dependencies join the read pack), and `doctrina review` (dependents of a
|
|
34
|
+
touched capability are flagged). Citing them only in prose keeps every
|
|
35
|
+
tool blind to the link.
|
|
28
36
|
-->
|
|
29
37
|
|
|
30
38
|
## Purpose
|