doctrina-cli 0.20.0 → 0.22.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 +7 -7
- package/package.json +1 -1
- package/src/commands/adapter.js +10 -0
- package/src/commands/change.js +202 -27
- package/src/commands/clarify.js +3 -206
- package/src/commands/close.js +1 -1
- package/src/commands/context.js +33 -2
- package/src/commands/contract.js +1 -0
- package/src/commands/coverage.js +69 -6
- package/src/commands/delivery.js +135 -17
- package/src/commands/doctor.js +83 -5
- package/src/commands/handoff.js +11 -2
- package/src/commands/hooks.js +56 -11
- package/src/commands/index-rebuild.js +47 -1
- package/src/commands/init.js +64 -8
- package/src/commands/intake.js +16 -5
- package/src/commands/metrics.js +16 -1
- package/src/commands/next.js +41 -6
- package/src/commands/onboard.js +113 -0
- package/src/commands/prime.js +53 -4
- package/src/commands/review.js +40 -4
- package/src/commands/search.js +2 -2
- package/src/commands/skill.js +2 -1
- package/src/commands/status.js +28 -0
- package/src/commands/templates.js +2 -1
- package/src/commands/triage.js +16 -1
- package/src/commands/ui.js +512 -0
- package/src/commands/upgrade.js +62 -4
- package/src/commands/validate.js +18 -0
- package/src/commands/verify.js +35 -7
- package/src/commands/why.js +1 -1
- package/src/commands/work.js +90 -42
- package/src/commands/workspace.js +89 -66
- package/src/index.js +107 -62
- package/src/lib/actions.js +62 -3
- package/src/lib/adapters.js +83 -1
- package/src/lib/agent-bridge.js +400 -0
- package/src/lib/applied-deltas.js +82 -0
- package/src/lib/args.js +34 -2
- package/src/lib/change-numbers.js +115 -0
- package/src/lib/change-ops.js +2 -2
- package/src/lib/clarify-scan.js +221 -0
- package/src/lib/code-inventory.js +594 -104
- package/src/lib/commands.js +58 -4
- package/src/lib/config.js +27 -8
- package/src/lib/conflict-markers.js +61 -0
- package/src/lib/console-model.js +668 -0
- package/src/lib/console-prefs.js +67 -0
- package/src/lib/console-room.js +201 -0
- package/src/lib/console-state.js +150 -0
- package/src/lib/console-view.js +557 -0
- package/src/lib/context-slice.js +34 -3
- package/src/lib/coverage-model.js +29 -6
- package/src/lib/delivery.js +90 -12
- package/src/lib/docs-impact.js +37 -2
- package/src/lib/fs-ops.js +103 -12
- package/src/lib/gates.js +7 -1
- package/src/lib/git.js +18 -0
- package/src/lib/index-json.js +10 -1
- package/src/lib/index-merge.js +90 -0
- package/src/lib/install.js +80 -0
- package/src/lib/intake-model.js +5 -2
- package/src/lib/json-out.js +9 -1
- package/src/lib/ledger.js +2 -3
- package/src/lib/lock.js +58 -0
- package/src/lib/merge-setup.js +55 -0
- package/src/lib/metrics-model.js +2 -2
- package/src/lib/onboard.js +197 -0
- package/src/lib/preset.js +177 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/properties.js +149 -0
- package/src/lib/runtime.js +19 -7
- package/src/lib/scan.js +13 -8
- package/src/lib/shell.js +45 -0
- package/src/lib/snapshot.js +5 -1
- package/src/lib/spec-ops.js +2 -2
- package/src/lib/task-card.js +101 -8
- package/src/lib/templates-model.js +3 -2
- package/src/lib/triage-model.js +75 -0
- package/src/lib/validation-model.js +23 -5
- package/src/lib/views.js +5 -4
- package/src/lib/work-model.js +8 -2
- package/src/lib/workspace-agents.js +67 -20
- package/src/lib/workspace-guard.js +65 -0
- package/src/lib/workspace-locate.js +141 -0
- package/src/lib/workspace-tools.js +104 -17
- package/src/lib/workspace-view.js +391 -0
- package/src/lib/workspace.js +83 -131
- package/templates/AGENTS.md.template +9 -5
- package/templates/README.md +4 -2
- package/templates/adapters/claude/.claude/commands/doctrina-onboard.md.template +15 -0
- package/templates/adapters/cursor/.cursor/rules/00-doctrina.mdc.template +2 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-context.md.template → skills/doctrina-context/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-next.md.template → skills/doctrina-next/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/skills/doctrina-onboard/SKILL.md.template +18 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-status.md.template → skills/doctrina-status/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-why.md.template → skills/doctrina-why/SKILL.md.template} +6 -0
- package/templates/adapters/cursor/.cursor/{commands/doctrina-work.md.template → skills/doctrina-work/SKILL.md.template} +6 -0
- package/templates/adapters/kiro/README.md +22 -0
- package/templates/playbooks/absorb.md.template +39 -16
- package/templates/playbooks/bootstrap.md.template +18 -6
- package/templates/playbooks/delivery.md.template +12 -6
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> CLI for the Doctrina framework. The package is published as
|
|
4
4
|
> `doctrina-cli`; the executable it installs is `doctrina`.
|
|
5
5
|
|
|
6
|
-
**Status:** v0.
|
|
6
|
+
**Status:** v0.22.0 — current release. Zero runtime dependencies
|
|
7
7
|
(Node.js standard library only).
|
|
8
8
|
|
|
9
9
|
## Install
|
|
@@ -23,14 +23,14 @@ read `AGENTS.md` and run `doctrina next` — it drives the rest. The
|
|
|
23
23
|
surface, by the moment you reach for it:
|
|
24
24
|
|
|
25
25
|
```
|
|
26
|
-
Bootstrap init · intake · adapter list|add|remove
|
|
26
|
+
Bootstrap init · ui · onboard · intake · adapter list|add|remove
|
|
27
27
|
Orient prime · next · status · context · show · search · why · handoff
|
|
28
|
-
Change triage · work · spec new|list|set · change new|apply|archive|check|tick|abandon|delta|card
|
|
28
|
+
Change triage · work · spec new|list|set · change new|apply|archive|check|tick|abandon|renumber|delta|card
|
|
29
29
|
contract new|list|check · decision new|accept|land|supersede|list|scope
|
|
30
30
|
skill new|list|suggest · intent add|list
|
|
31
31
|
Gate clarify · validate · coverage · trace · review · verify · close · doctor
|
|
32
|
-
Orchestrate workspace init|add|attach|detach|list|tool|guard · delivery new|route|list|status|check|close
|
|
33
|
-
Maintain upgrade · index rebuild · hooks install · watch · metrics · completion · ci · templates list
|
|
32
|
+
Orchestrate workspace init|add|attach|detach|list|tool|guard · delivery new|route|list|status|tick|check|close
|
|
33
|
+
Maintain upgrade · index rebuild|merge · hooks install · watch · metrics · completion · ci · templates list
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
`doctrina --help` prints the same grouping with one line per command, and
|
|
@@ -44,7 +44,7 @@ code is in the
|
|
|
44
44
|
|------|---------|
|
|
45
45
|
| `--project-name <name>` | Override the project name (defaults to cwd basename) |
|
|
46
46
|
| `--project-description <text>` | One-sentence description, with no intake. Without it on a terminal, `init` asks you to describe the project once and keeps the answer as the intake |
|
|
47
|
-
| `--agent <name>` | Install the adapter for one of the
|
|
47
|
+
| `--agent <name>` | Install the adapter for one of the 13 supported agents (`claude`, `codex`, `cursor`, `copilot`, `gemini`, `aider`, `windsurf`, `continue`, `amp`, `devin`, `factory`, `jules`, `kiro`) or `all` |
|
|
48
48
|
| `--date <YYYY-MM-DD>` | Override the system date in artifacts |
|
|
49
49
|
| `--force` | Overwrite existing files |
|
|
50
50
|
| `--non-interactive` | Fail instead of prompting for missing required values |
|
|
@@ -57,7 +57,7 @@ code is in the
|
|
|
57
57
|
`alwaysApply: true` pointing at `AGENTS.md`. `copilot`, `gemini`,
|
|
58
58
|
`aider`, `windsurf`, and `continue` follow the same thin-pointer
|
|
59
59
|
pattern at each agent's native path.
|
|
60
|
-
- `--agent codex` (and `amp`, `devin`, `factory`, `jules`) installs
|
|
60
|
+
- `--agent codex` (and `amp`, `devin`, `factory`, `jules`, `kiro`) installs
|
|
61
61
|
nothing: these agents read `AGENTS.md` natively.
|
|
62
62
|
- `--agent all` installs every adapter.
|
|
63
63
|
|
package/package.json
CHANGED
package/src/commands/adapter.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { exists, isFile, mkdirp, pruneEmptyDirs, read, relPath, remove, write } from "../lib/fs-ops.js";
|
|
5
|
+
import { applyAdapterMigration, planAdapterMigration } from "../lib/adapters.js";
|
|
6
|
+
import { relinkShadow, shadowRootOf } from "../lib/workspace.js";
|
|
5
7
|
import {
|
|
6
8
|
ADAPTER_STATES, adapterFiles, describeAdapter, listAdapterNames, renderAdapterFile,
|
|
7
9
|
isMergedConfig, mergeJson, unmergeJson, containsJson, holdsAnyOf,
|
|
@@ -137,6 +139,14 @@ function adapterAdd(projectRoot, name, cmdFlags) {
|
|
|
137
139
|
written += 1;
|
|
138
140
|
}
|
|
139
141
|
|
|
142
|
+
// An adapter installed before its files moved (change 0238): the copies
|
|
143
|
+
// the CLI wrote under the old paths go, an edited one stays and is named.
|
|
144
|
+
const filesRoot = shadowRootOf(projectRoot) ?? projectRoot;
|
|
145
|
+
const moves = planAdapterMigration(projectRoot, tokens, filesRoot);
|
|
146
|
+
const gone = applyAdapterMigration(filesRoot, moves);
|
|
147
|
+
relinkShadow(projectRoot, gone);
|
|
148
|
+
for (const g of gone) console.log(c.green("removed") + ` ${g.rel} (replaced by its skill)`);
|
|
149
|
+
for (const rel of moves.kept) console.log(c.yellow("kept ") + ` ${rel} (edited since install — Cursor now reads the skill; remove it once your edits are moved)`);
|
|
140
150
|
console.log("");
|
|
141
151
|
console.log(`${c.bold("Adapter " + name)} — ${written} file${written === 1 ? "" : "s"} written` +
|
|
142
152
|
(skipped > 0 ? `, ${skipped} left alone` : "") +
|
package/src/commands/change.js
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
import { parseChecklist, getSection, getHeader, parseChangeTitle } from "../lib/doc-model.js";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
|
-
import { exists, isDir, isFile, lineCount, mkdirp, move, read, relPath, remove, walk, write } from "../lib/fs-ops.js";
|
|
5
|
+
import { eolOf, exists, isDir, isFile, lineCount, mkdirp, move, read, relPath, remove, walk, withEol, write } from "../lib/fs-ops.js";
|
|
6
6
|
import { diffLines, formatUnified } from "../lib/diff.js";
|
|
7
7
|
import { locateTemplatesDir, loadTemplateTree, materialiseEntry } from "../lib/templates.js";
|
|
8
8
|
import * as idx from "../lib/index-json.js";
|
|
9
|
-
import { archivedChangeEntry, deriveIndex } from "../lib/scan.js";
|
|
9
|
+
import { archivedChangeEntry, deriveIndex, listHeader } from "../lib/scan.js";
|
|
10
10
|
import { extractOps, applyOps } from "../lib/spec-ops.js";
|
|
11
11
|
import { printAdrCheckpoint } from "../lib/adr-guard.js";
|
|
12
12
|
import { GATES, TRANSITIONS, checkTransition, recordForcedGap } from "../lib/gates.js";
|
|
13
13
|
import { today } from "../lib/dates.js";
|
|
14
|
-
import { flagBool, flagString } from "../lib/args.js";
|
|
14
|
+
import { flagAll, flagBool, flagString } from "../lib/args.js";
|
|
15
15
|
import { c } from "../lib/colors.js";
|
|
16
16
|
import { appendLedgerLine, archivedLine, abandonedLine } from "../lib/ledger.js";
|
|
17
17
|
import { suggest } from "../lib/suggest.js";
|
|
@@ -19,15 +19,19 @@ import { confirm, isInteractive } from "../lib/prompt.js";
|
|
|
19
19
|
import { taskCard } from "../lib/task-card.js";
|
|
20
20
|
import { EXIT } from "../lib/exit-codes.js";
|
|
21
21
|
import { ensureDoctrinaProject, refuseChangeRef } from "../lib/project.js";
|
|
22
|
+
import { renumberChange } from "../lib/change-numbers.js";
|
|
23
|
+
import { appliedDeltas, deltaFingerprint, deltaKey, pendingOps, recordAppliedDeltas } from "../lib/applied-deltas.js";
|
|
22
24
|
import { parseOperation, parseCapabilityFromDelta, isUntouchedScaffold } from "../lib/doc-model.js";
|
|
23
25
|
import { changeNew, addDeltaOps } from "../lib/change-ops.js";
|
|
26
|
+
import { propertyShape } from "../lib/properties.js";
|
|
27
|
+
import { scanText } from "../lib/clarify-scan.js";
|
|
24
28
|
|
|
25
|
-
const SUBCOMMANDS = ["new", "apply", "archive", "check", "tick", "abandon", "delta", "card"];
|
|
29
|
+
const SUBCOMMANDS = ["new", "apply", "archive", "check", "tick", "abandon", "renumber", "delta", "card"];
|
|
26
30
|
|
|
27
31
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
28
32
|
// adding a command never requires editing the entrypoint — the gap that
|
|
29
33
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
30
|
-
export const flags = { boolean: ["json", "all", "chore", "design", "force", "no-spec", "verbose"], string: ["reason", "requirement", "criterion", "bump", "op"] };
|
|
34
|
+
export const flags = { boolean: ["json", "all", "chore", "design", "force", "no-spec", "verbose"], string: ["reason", "requirement", "criterion", "property", "bump", "op"] };
|
|
31
35
|
|
|
32
36
|
export async function run(positional, flags) {
|
|
33
37
|
const sub = positional[0];
|
|
@@ -45,6 +49,8 @@ export async function run(positional, flags) {
|
|
|
45
49
|
return changeTick(positional.slice(1), flags);
|
|
46
50
|
case "abandon":
|
|
47
51
|
return await changeAbandon(positional.slice(1), flags);
|
|
52
|
+
case "renumber":
|
|
53
|
+
return changeRenumber(positional.slice(1));
|
|
48
54
|
case "delta":
|
|
49
55
|
return changeDelta(positional.slice(1), flags);
|
|
50
56
|
case "card":
|
|
@@ -88,7 +94,7 @@ function changeCard(args) {
|
|
|
88
94
|
const { card, reason } = taskCard(projectRoot, id, ordinal);
|
|
89
95
|
if (!card) {
|
|
90
96
|
console.log(c.gray(reason));
|
|
91
|
-
return reason.startsWith("every task") ? EXIT.OK : EXIT.PRECONDITION;
|
|
97
|
+
return (reason ?? "").startsWith("every task") ? EXIT.OK : EXIT.PRECONDITION;
|
|
92
98
|
}
|
|
93
99
|
const L = [];
|
|
94
100
|
L.push(`# Task card — ${card.change}, task ${card.ordinal} of ${card.total}` + (card.checked ? " (already ticked)" : ""));
|
|
@@ -103,8 +109,26 @@ function changeCard(args) {
|
|
|
103
109
|
if (card.why) {
|
|
104
110
|
L.push("", "## Why the change exists", "", card.why);
|
|
105
111
|
}
|
|
112
|
+
if (card.story) {
|
|
113
|
+
L.push("", `## The story (delivery ${card.delivery})`, "", card.story);
|
|
114
|
+
}
|
|
115
|
+
// What the change does to the spec — the requirement the card implements
|
|
116
|
+
// is here, not in the spec as it stands (change 0231).
|
|
117
|
+
if (card.changes.length > 0) {
|
|
118
|
+
L.push("", "## What this change does to the spec", "");
|
|
119
|
+
for (const ch of card.changes) L.push(`- ${ch.capability} ${ch.line}`);
|
|
120
|
+
}
|
|
106
121
|
for (const s of card.slices) {
|
|
107
|
-
L.push("", `## Spec — ${s.capability} (cut to this task)`, "", s.text.trim());
|
|
122
|
+
L.push("", `## Spec — ${s.capability} as it stands (${s.whole ? "whole: nothing in it matched the task" : "cut to this task"})`, "", s.text.trim());
|
|
123
|
+
}
|
|
124
|
+
if (card.decisions.list.length > 0) {
|
|
125
|
+
L.push("", "## Decisions that bind it", "");
|
|
126
|
+
for (const d of card.decisions.list) {
|
|
127
|
+
L.push(`- ADR ${d.id} — ${d.title} (\`doctrina show ${d.id}\`)`);
|
|
128
|
+
if (d.decision) L.push(` ${d.decision}`);
|
|
129
|
+
}
|
|
130
|
+
if (card.decisions.more > 0) L.push(`- … and ${card.decisions.more} more accepted global decisions — \`doctrina prime --rules\``);
|
|
131
|
+
L.push("", "Where the story or the task contradicts one of these, stop and report it: the decision stands until the planner supersedes it.");
|
|
108
132
|
}
|
|
109
133
|
L.push("", "## When done", "");
|
|
110
134
|
L.push(card.fields.verify
|
|
@@ -223,21 +247,53 @@ function changeApply(args, flags) {
|
|
|
223
247
|
const proposalPath = path.join(changeDir, "proposal.md");
|
|
224
248
|
const alreadyApplied = isFile(proposalPath)
|
|
225
249
|
&& (getHeader(read(proposalPath), "Status") ?? "").trim().toLowerCase() === "applied";
|
|
250
|
+
const allDeltas = walk(path.join(changeDir, "specs")).filter((p) => p.endsWith("delta.md"));
|
|
251
|
+
// WHICH deltas the stamp covers (change 0247). The stamp is the change's,
|
|
252
|
+
// and a change is not finished when it is stamped: a close that applied
|
|
253
|
+
// and then stopped at a later gate leaves it `applied`, and the fix for
|
|
254
|
+
// that gate is often one more delta. The stamp said "nothing to do", the
|
|
255
|
+
// close went on, and the change was archived naming a spec it had never
|
|
256
|
+
// touched. The record names each delta that was applied, with what it held.
|
|
257
|
+
/** @type {Map<string, Set<number> | null> | null} delta → the ops still to apply (null: the whole delta) */
|
|
258
|
+
let lateOnly = null;
|
|
226
259
|
if (alreadyApplied && !flagBool(flags, "force", false)) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
260
|
+
const record = appliedDeltas(read(proposalPath));
|
|
261
|
+
const late = new Map();
|
|
262
|
+
const edited = [];
|
|
263
|
+
for (const p of record ? allDeltas : []) {
|
|
264
|
+
const recorded = record?.get(deltaKey(changeDir, p));
|
|
265
|
+
if (!recorded) {
|
|
266
|
+
late.set(p, null);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
const { fresh, gone } = pendingOps(recorded, deltaFingerprint(read(p)));
|
|
270
|
+
if (gone) edited.push(p);
|
|
271
|
+
else if (fresh.length > 0) late.set(p, new Set(fresh));
|
|
272
|
+
}
|
|
273
|
+
if (edited.length > 0) {
|
|
274
|
+
console.error(c.red("error:") + ` ${edited.map((p) => relPath(changeDir, p)).join(", ")}: an op that was already applied changed or was removed — the spec holds what it wrote, and applying the delta again would add to that, not replace it`);
|
|
275
|
+
console.error(c.gray("hint: ") + "restore the op as it was applied and add the correction as a new op (`replace-requirement`, `replace-criterion`, `doctrina change delta`), or revert the spec to its pre-apply state and run `doctrina change apply " + id + " --force`");
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
if (late.size === 0) {
|
|
279
|
+
console.log(c.gray("skip ") + `change "${id}" is already applied — nothing to do`);
|
|
280
|
+
console.log(c.gray("The ops verbs are additive: applying twice duplicates every"));
|
|
281
|
+
console.log(c.gray("requirement this delta carries. Re-apply anyway with --force"));
|
|
282
|
+
console.log(c.gray("only after reverting the specs to their pre-apply state."));
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
const what = [...late].map(([p, ops]) => `${relPath(changeDir, p)}${ops ? ` (${ops.size} new op${ops.size === 1 ? "" : "s"})` : ""}`).join(", ");
|
|
286
|
+
console.log(c.gray("note: ") + `change "${id}" was applied before this was added — applying only ${what}`);
|
|
287
|
+
lateOnly = late;
|
|
232
288
|
}
|
|
233
289
|
|
|
234
290
|
// The skip above comes FIRST (change 0221): the apply gate is a pre-apply
|
|
235
291
|
// question, and on an applied change its ADDED check reads the content the
|
|
236
292
|
// apply wrote as a conflict — which refused the resume of a killed close.
|
|
237
|
-
const applyGate = enforceTransition(projectRoot, id, changeDir, "apply", flags);
|
|
293
|
+
const applyGate = lateOnly ? { ok: true, forced: null } : enforceTransition(projectRoot, id, changeDir, "apply", flags);
|
|
238
294
|
if (!applyGate.ok) return 1;
|
|
239
295
|
|
|
240
|
-
const deltaFiles =
|
|
296
|
+
const deltaFiles = lateOnly ? [...lateOnly.keys()] : allDeltas;
|
|
241
297
|
const date = today();
|
|
242
298
|
let errors = 0;
|
|
243
299
|
let writes = 0;
|
|
@@ -291,7 +347,8 @@ function changeApply(args, flags) {
|
|
|
291
347
|
// `ops` block (set-header / bump-version / set-criterion / ...; F3);
|
|
292
348
|
// otherwise it falls back to the historical manual-merge pointer, so
|
|
293
349
|
// prose deltas keep working unchanged.
|
|
294
|
-
const
|
|
350
|
+
const only = lateOnly?.get(deltaPath) ?? null;
|
|
351
|
+
const ops = extractOps(text).filter((_op, i) => only === null || only.has(i));
|
|
295
352
|
if (ops.length === 0) {
|
|
296
353
|
console.log(c.yellow("manual[MODIFIED]") + ` merge ${relPath(projectRoot, deltaPath)} into ${relPath(projectRoot, targetPath)} by hand (no ops block)`);
|
|
297
354
|
manual += 1;
|
|
@@ -334,6 +391,10 @@ function changeApply(args, flags) {
|
|
|
334
391
|
console.log(c.green("status") + " proposal.md → applied");
|
|
335
392
|
flippedToApplied = true;
|
|
336
393
|
}
|
|
394
|
+
// Every delta the proposal's stamp now covers, with what it held.
|
|
395
|
+
const stamped = read(proposalPath);
|
|
396
|
+
const withRecord = recordAppliedDeltas(stamped, allDeltas.map((p) => [deltaKey(changeDir, p), deltaFingerprint(read(p))]));
|
|
397
|
+
if (withRecord !== stamped) write(proposalPath, withRecord, { force: true });
|
|
337
398
|
}
|
|
338
399
|
}
|
|
339
400
|
|
|
@@ -417,6 +478,13 @@ async function changeCheck(id, { verbose = false } = {}) {
|
|
|
417
478
|
opsFindings += 1;
|
|
418
479
|
} else {
|
|
419
480
|
console.log(c.green("✓ ") + `${rel}: ${result.applied.length} op${result.applied.length === 1 ? "" : "s"} would apply cleanly`);
|
|
481
|
+
// What clarify will say about the spec once this lands (change 0232):
|
|
482
|
+
// the smells the merged spec has and the current one does not.
|
|
483
|
+
// Advisory — clarify is not a close gate — but a project that runs
|
|
484
|
+
// `clarify --all` in its build learned about the word mid-close.
|
|
485
|
+
for (const smell of newSmells(read(targetPath), result.text, projectRoot)) {
|
|
486
|
+
console.log(c.yellow("! ") + `${rel}: writes ${smell.rule} "${smell.token}" into ${capability}'s spec — ${smell.hint} (\`clarify\` flags it after apply)`);
|
|
487
|
+
}
|
|
420
488
|
}
|
|
421
489
|
}
|
|
422
490
|
if (deltaFiles.length === 0) console.log(c.gray("- no spec deltas"));
|
|
@@ -484,14 +552,20 @@ function changeDelta(args, flags) {
|
|
|
484
552
|
}
|
|
485
553
|
// In the order a reader expects them: requirement, criterion, any raw op,
|
|
486
554
|
// and the bump last.
|
|
555
|
+
// `--requirement`, `--criterion` and `--op` repeat (change 0228): each
|
|
556
|
+
// occurrence is one op, in the order given. The parser keeps the last
|
|
557
|
+
// value of a repeated flag, so reading them with flagString wrote one op
|
|
558
|
+
// and reported success for a command line that asked for three.
|
|
487
559
|
const lines = [];
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
const raw = flagString(flags, "op") ?? (rest.length > 0 ? rest.join(" ") : undefined);
|
|
560
|
+
const raw = flagAll(flags, "op");
|
|
561
|
+
if (raw.length === 0 && rest.length > 0) raw.push(rest.join(" "));
|
|
491
562
|
const bump = flagString(flags, "bump");
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
563
|
+
for (const r of flagAll(flags, "requirement")) lines.push(`append-requirement ${r}`);
|
|
564
|
+
for (const r of flagAll(flags, "criterion")) lines.push(`append-criterion ${r}`);
|
|
565
|
+
// A criterion that claims something of every input (change 0254): the
|
|
566
|
+
// mark is what makes coverage ask the cited test to be a property.
|
|
567
|
+
for (const r of flagAll(flags, "property")) lines.push(`append-criterion [property] ${r.replace(/^\[property\]\s*/i, "")}`);
|
|
568
|
+
lines.push(...raw);
|
|
495
569
|
if (bump) lines.push(`bump-version ${bump}`);
|
|
496
570
|
|
|
497
571
|
const r = addDeltaOps(projectRoot, id, capability, lines);
|
|
@@ -502,7 +576,23 @@ function changeDelta(args, flags) {
|
|
|
502
576
|
return r.code;
|
|
503
577
|
}
|
|
504
578
|
console.log(c.green(r.created ? "created" : "updated") + ` ${r.deltaRel}` +
|
|
505
|
-
c.gray(` — ${lines.length} op${lines.length === 1 ? "" : "s"} added; the block now holds ${r.block.length} (${r.block.join(", ")}), and applies cleanly`));
|
|
579
|
+
c.gray(` — ${lines.length} op${lines.length === 1 ? "" : "s"} added; the block now holds ${(r.block ?? []).length} (${(r.block ?? []).join(", ")}), and applies cleanly`));
|
|
580
|
+
// The moment the requirement is written is the moment its proof is
|
|
581
|
+
// chosen (change 0254): one that claims something of every input is told
|
|
582
|
+
// so here, while the test for it does not exist yet.
|
|
583
|
+
if (flagAll(flags, "property").length === 0) {
|
|
584
|
+
let noted = 0;
|
|
585
|
+
for (const req of flagAll(flags, "requirement")) {
|
|
586
|
+
const shape = propertyShape(req.replace(/^[a-z-]+:\s*/i, ""));
|
|
587
|
+
if (!shape) continue;
|
|
588
|
+
noted++;
|
|
589
|
+
console.log(c.yellow("note: ") + `"${req.length > 70 ? req.slice(0, 69) + "…" : req}" reads as ${shape.id === "invariant" ? "an invariant" : shape.id} — ${shape.property}.`);
|
|
590
|
+
}
|
|
591
|
+
if (noted > 0) {
|
|
592
|
+
console.log(c.gray(" An example test proves the cases somebody thought of. If a failure here would be a bug, write a property test and claim it: ") +
|
|
593
|
+
c.cyan(`doctrina change delta ${id} ${capability} --property "<what holds for every input> — verified by \`<the property test>\`"`));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
506
596
|
console.log(c.gray("Preview the merge: ") + c.cyan(`doctrina change check ${id} --verbose`));
|
|
507
597
|
return EXIT.OK;
|
|
508
598
|
}
|
|
@@ -614,11 +704,12 @@ function changeTick(args, flags) {
|
|
|
614
704
|
byPath.get(box.file.path).push(box);
|
|
615
705
|
}
|
|
616
706
|
for (const [filePath, hits] of byPath) {
|
|
617
|
-
const
|
|
707
|
+
const before = read(filePath);
|
|
708
|
+
const lines = before.split(/\r?\n/);
|
|
618
709
|
for (const box of hits) {
|
|
619
710
|
lines[box.lineIndex] = lines[box.lineIndex].replace(/\[ \]/, "[x]");
|
|
620
711
|
}
|
|
621
|
-
write(filePath, lines.join("\n"), { force: true });
|
|
712
|
+
write(filePath, withEol(lines.join("\n"), eolOf(before)), { force: true });
|
|
622
713
|
}
|
|
623
714
|
console.log(c.green("ticked ") + `${picked.length} box${picked.length === 1 ? "" : "es"} in ${byPath.size} file${byPath.size === 1 ? "" : "s"}`);
|
|
624
715
|
return 0;
|
|
@@ -771,13 +862,22 @@ async function changeAbandon(args, flags) {
|
|
|
771
862
|
|
|
772
863
|
const reason = flagString(flags, "reason") ?? "";
|
|
773
864
|
const date = today();
|
|
865
|
+
// The delivery it belonged to stays on record (third audit, change 0234):
|
|
866
|
+
// the proposal that carried it is deleted, and the delivery's status went
|
|
867
|
+
// back to "no change yet", as if the planner had never decided anything.
|
|
868
|
+
const proposalFile = path.join(changeDir, "proposal.md");
|
|
869
|
+
const delivery = isFile(proposalFile) ? (listHeader(read(proposalFile), "Delivery") ?? "").trim() : "";
|
|
870
|
+
const recorded = delivery ? `${reason ? `${reason} ` : ""}(delivery ${delivery})` : reason;
|
|
774
871
|
|
|
775
872
|
remove(changeDir);
|
|
776
873
|
console.log(c.green("removed") + ` ${relPath(projectRoot, changeDir)}`);
|
|
777
874
|
|
|
778
875
|
// Ledger line so the abandonment is part of the visible history.
|
|
779
|
-
const ledgerFile = appendLedgerLine(projectRoot, abandonedLine(id,
|
|
876
|
+
const ledgerFile = appendLedgerLine(projectRoot, abandonedLine(id, recorded, date));
|
|
780
877
|
console.log(c.green("ledger") + ` +1 line in ${relPath(projectRoot, ledgerFile)}`);
|
|
878
|
+
if (delivery) {
|
|
879
|
+
console.log(c.gray("hint: ") + `it was this repository's part of delivery ${delivery} — from the workspace, re-route the delivery without this repository (\`doctrina delivery route ${delivery} <the others>\`) or open a new change here with \`doctrina work "<title>" --delivery ${delivery}\``);
|
|
880
|
+
}
|
|
781
881
|
|
|
782
882
|
// Drop the change entry; rebuild from the tree so nothing drifts.
|
|
783
883
|
const current = idx.load(projectRoot);
|
|
@@ -788,6 +888,34 @@ async function changeAbandon(args, flags) {
|
|
|
788
888
|
return 0;
|
|
789
889
|
}
|
|
790
890
|
|
|
891
|
+
// Give an open change the next free number (change 0242): two branches that
|
|
892
|
+
// each opened a change took the same one, and the merge holds both.
|
|
893
|
+
function changeRenumber(args) {
|
|
894
|
+
const id = args[0];
|
|
895
|
+
if (!id) {
|
|
896
|
+
console.error(c.red("error:") + " change renumber requires <id>");
|
|
897
|
+
return 2;
|
|
898
|
+
}
|
|
899
|
+
const refused = refuseChangeRef(id);
|
|
900
|
+
if (refused !== null) return refused;
|
|
901
|
+
const projectRoot = process.cwd();
|
|
902
|
+
ensureDoctrinaProject(projectRoot);
|
|
903
|
+
const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
|
|
904
|
+
if (!isDir(changeDir)) {
|
|
905
|
+
console.error(c.red("error:") + ` change "${id}" is not an open change (${relPath(projectRoot, changeDir)} not found) — an archived change keeps its number`);
|
|
906
|
+
return EXIT.USAGE;
|
|
907
|
+
}
|
|
908
|
+
const { to, rewritten } = renumberChange(projectRoot, id);
|
|
909
|
+
console.log(c.green("renumbered") + ` ${id} → ${to}`);
|
|
910
|
+
if (rewritten.length > 0) console.log(c.gray(` the id was rewritten in ${rewritten.join(", ")}`));
|
|
911
|
+
const derived = deriveIndex(projectRoot, idx.load(projectRoot));
|
|
912
|
+
derived.last_updated = today();
|
|
913
|
+
idx.save(projectRoot, derived);
|
|
914
|
+
console.log(c.green("indexed") + ` change "${to}" in index.json`);
|
|
915
|
+
console.log(c.gray("next: ") + `name ${to} wherever the old number was written outside the change — a commit message, a branch, a task card already handed out`);
|
|
916
|
+
return 0;
|
|
917
|
+
}
|
|
918
|
+
|
|
791
919
|
// The per-delta preview: what applying this delta would do to its target.
|
|
792
920
|
// ADDED reports the body it would write, REMOVED the spec it would delete,
|
|
793
921
|
// MODIFIED a line diff against the current spec.
|
|
@@ -830,6 +958,26 @@ export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
|
|
|
830
958
|
continue;
|
|
831
959
|
}
|
|
832
960
|
console.log(c.yellow("MODIFIED") + ` ${targetRel}`);
|
|
961
|
+
// A delta with an ops block is previewed as the spec it produces (change
|
|
962
|
+
// 0228): the ops run in memory, as apply runs them, and the diff is the
|
|
963
|
+
// spec before against the spec after. Diffing the spec against the
|
|
964
|
+
// delta FILE printed every line of the spec as a '-' — three hundred
|
|
965
|
+
// lines of noise around the four that change.
|
|
966
|
+
const ops = extractOps(text);
|
|
967
|
+
if (ops.length > 0) {
|
|
968
|
+
const current = read(targetPath);
|
|
969
|
+
const after = applyOps(current, ops);
|
|
970
|
+
if (after.errors.length > 0) {
|
|
971
|
+
for (const e of after.errors) console.error(c.red("error:") + ` ${rel}: ${e}`);
|
|
972
|
+
errors += 1;
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
console.log(formatUnified(diffLines(current, after.text), {
|
|
976
|
+
aLabel: targetRel,
|
|
977
|
+
bLabel: `${targetRel} (after apply)`,
|
|
978
|
+
}));
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
833
981
|
console.log(c.gray("note: the delta body is a fragment to merge — '-' lines are current"));
|
|
834
982
|
console.log(c.gray(" spec content absent from the delta, not necessarily removals."));
|
|
835
983
|
const out = formatUnified(diffLines(read(targetPath), body), {
|
|
@@ -850,6 +998,22 @@ function extractDeltaBody(text) {
|
|
|
850
998
|
}
|
|
851
999
|
|
|
852
1000
|
|
|
1001
|
+
// The smells `after` has that `before` does not, matched by rule, token and
|
|
1002
|
+
// the line's own text — line numbers shift when a delta inserts a line.
|
|
1003
|
+
function newSmells(before, after, projectRoot) {
|
|
1004
|
+
const key = (/** @type {string} */ text, /** @type {{ line: number, rule: string, token: string }} */ sm) =>
|
|
1005
|
+
`${sm.rule}|${sm.token.toLowerCase()}|${text.split("\n")[sm.line - 1] ?? ""}`;
|
|
1006
|
+
const seen = new Map();
|
|
1007
|
+
for (const sm of scanText(before, projectRoot)) seen.set(key(before, sm), (seen.get(key(before, sm)) ?? 0) + 1);
|
|
1008
|
+
const out = [];
|
|
1009
|
+
for (const sm of scanText(after, projectRoot)) {
|
|
1010
|
+
const k = key(after, sm);
|
|
1011
|
+
if ((seen.get(k) ?? 0) > 0) seen.set(k, seen.get(k) - 1);
|
|
1012
|
+
else out.push(sm);
|
|
1013
|
+
}
|
|
1014
|
+
return out;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
853
1017
|
export const help = `
|
|
854
1018
|
Usage: doctrina change <subcommand> [args]
|
|
855
1019
|
|
|
@@ -872,18 +1036,29 @@ Subcommands:
|
|
|
872
1036
|
its target + the archive gate preview + an advisory
|
|
873
1037
|
list of accepted ADRs citing the touched capabilities.
|
|
874
1038
|
Everything close would refuse, listed BEFORE it runs.
|
|
1039
|
+
--verbose also prints each delta as it would land.
|
|
875
1040
|
tick <id> [n... |--all] List the unchecked boxes (tasks.md + proposal
|
|
876
1041
|
## Verification) with ordinals; tick the given ones,
|
|
877
1042
|
or every one with --all. No args = list only.
|
|
878
1043
|
abandon <id> Delete an open change folder and its index entry, and
|
|
879
1044
|
record the abandonment in the ledger ([--reason "..."]).
|
|
1045
|
+
renumber <id> Give an open change the next free number: the folder
|
|
1046
|
+
moves and the id is rewritten in its own files. For
|
|
1047
|
+
the merge that brought together two changes opened
|
|
1048
|
+
on two branches under one number — \`validate\` names it.
|
|
880
1049
|
delta <id> <capability> [--requirement "<section>: <text>"] [--criterion "<text>"]
|
|
881
|
-
[--bump major|minor|patch] [--op "<op line>" | <op line>]
|
|
1050
|
+
[--property "<text>"] [--bump major|minor|patch] [--op "<op line>" | <op line>]
|
|
882
1051
|
Write ops into the change's MODIFIED delta for the
|
|
883
1052
|
capability — created from the template when absent —
|
|
884
1053
|
parsed by the grammar apply reads and dry-run against
|
|
885
1054
|
the spec first: nothing is written unless the whole
|
|
886
|
-
block applies.
|
|
1055
|
+
block applies. --requirement, --criterion and --op
|
|
1056
|
+
repeat, one op each, in order; a second --bump
|
|
1057
|
+
replaces the first. --property writes a criterion
|
|
1058
|
+
marked [property]: a claim about every input, which
|
|
1059
|
+
coverage counts only when the test it cites is
|
|
1060
|
+
property-based (\`doctrina coverage --properties\`
|
|
1061
|
+
lists the requirements that read as one).
|
|
887
1062
|
card <id> [<n>] Print task <n> (default: the next open one) as a
|
|
888
1063
|
self-contained card for an executor: the task and the
|
|
889
1064
|
lines indented under it (Files:, Requirement:, Verify:),
|