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
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.18.0 — current release. Zero runtime dependencies
|
|
7
7
|
(Node.js standard library only).
|
|
8
8
|
|
|
9
9
|
## Install
|
|
@@ -18,59 +18,31 @@ Requires Node.js 20.12 or later.
|
|
|
18
18
|
|
|
19
19
|
## Commands
|
|
20
20
|
|
|
21
|
+
Start with `doctrina init` in your repository, then tell your AI agent to
|
|
22
|
+
read `AGENTS.md` and run `doctrina next` — it drives the rest. The
|
|
23
|
+
surface, by the moment you reach for it:
|
|
24
|
+
|
|
21
25
|
```
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
doctrina spec list list specs with version, status, size
|
|
30
|
-
doctrina spec set <cap> [--bump ...] edit spec headers/criterion + resync index (no lockstep)
|
|
31
|
-
doctrina change new <id> "<title>" open a change proposal
|
|
32
|
-
doctrina change apply <id> apply spec deltas (ADDED/REMOVED auto; MODIFIED ops block applies)
|
|
33
|
-
doctrina change archive <id> archive an applied change
|
|
34
|
-
doctrina change abandon <id> delete an open change + its index entry (ledgered)
|
|
35
|
-
doctrina change diff <id> preview spec deltas (line diff for MODIFIED)
|
|
36
|
-
doctrina contract new <name> own the integration surface (ports, env, interfaces)
|
|
37
|
-
doctrina contract check verify port collisions, env drift, referenced specs
|
|
38
|
-
doctrina decision new "<title>" create the next sequential ADR
|
|
39
|
-
doctrina decision accept <num> flip a proposed ADR to accepted
|
|
40
|
-
doctrina decision land <num> [path...] stamp an accepted ADR as implemented (non-mutating Landed:)
|
|
41
|
-
doctrina decision supersede <num> "<t>" supersede an existing ADR
|
|
42
|
-
doctrina decision list list ADRs with status, date, title
|
|
43
|
-
doctrina skill new <name> scaffold an on-demand procedural-memory skill
|
|
44
|
-
doctrina skill list list skills with descriptions (read-only)
|
|
45
|
-
doctrina skill sync mirror skill frontmatter descriptions into index.json
|
|
46
|
-
doctrina analyze <change-id> inspect a change folder before applying
|
|
47
|
-
doctrina clarify <path> smell-test a Markdown file for ambiguity (--all: tree)
|
|
48
|
-
doctrina context [<capability>] print the context pack in read order (--concat)
|
|
49
|
-
doctrina search <term> [...] search artifacts, grouped by category (ranked)
|
|
50
|
-
doctrina validate schema + artifact-existence + index-drift checks (--fix rebuilds)
|
|
51
|
-
doctrina coverage acceptance criteria with linked evidence (--strict gates; skips = conditional)
|
|
52
|
-
doctrina trace intent provenance: product intent -> specs (--strict gates)
|
|
53
|
-
doctrina verify run project typecheck/test/build checks (the real gate)
|
|
54
|
-
doctrina verify --clean reproducibility lint: clean-checkout footguns (dist/codegen)
|
|
55
|
-
doctrina templates list enumerate shipped templates
|
|
56
|
-
doctrina templates check compare project against recommended template shape
|
|
57
|
-
doctrina templates update additive fixer for check findings (preview; --write applies)
|
|
58
|
-
doctrina hooks install install pre-commit hook
|
|
59
|
-
doctrina index rebuild regenerate index.json from the files (--check for CI)
|
|
60
|
-
doctrina next print the recommended next workflow actions
|
|
61
|
-
doctrina metrics local git-derived adoption metrics (no network)
|
|
62
|
-
doctrina --help | -h print the command surface
|
|
63
|
-
doctrina --version | -v print the package version
|
|
26
|
+
Bootstrap init · intake · adapter list|add|remove
|
|
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
|
|
29
|
+
contract new|list|check · decision new|accept|land|supersede|list|scope
|
|
30
|
+
skill new|list|suggest · intent add|list
|
|
31
|
+
Gate clarify · validate · coverage · trace · review · verify · close · doctor
|
|
32
|
+
Maintain upgrade · index rebuild · hooks install · watch · metrics · completion · ci · templates list
|
|
64
33
|
```
|
|
65
34
|
|
|
66
|
-
|
|
35
|
+
`doctrina --help` prints the same grouping with one line per command, and
|
|
36
|
+
`doctrina <command> --help` the flags of each. Every command, flag and exit
|
|
37
|
+
code is in the
|
|
38
|
+
[CLI reference](https://github.com/GCarin1/Doctrina/blob/main/docs/en/cli-reference.md).
|
|
67
39
|
|
|
68
40
|
## `init` flags
|
|
69
41
|
|
|
70
42
|
| Flag | Purpose |
|
|
71
43
|
|------|---------|
|
|
72
44
|
| `--project-name <name>` | Override the project name (defaults to cwd basename) |
|
|
73
|
-
| `--project-description <text>` | One-sentence description
|
|
45
|
+
| `--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 |
|
|
74
46
|
| `--agent <name>` | Install the adapter for one of the twelve supported agents (`claude`, `codex`, `cursor`, `copilot`, `gemini`, `aider`, `windsurf`, `continue`, `amp`, `devin`, `factory`, `jules`) or `all` |
|
|
75
47
|
| `--date <YYYY-MM-DD>` | Override the system date in artifacts |
|
|
76
48
|
| `--force` | Overwrite existing files |
|
|
@@ -100,43 +72,29 @@ For each spec delta found under `.doctrina/changes/<id>/specs/`:
|
|
|
100
72
|
- **REMOVED:** deletes the target spec.
|
|
101
73
|
- **MODIFIED:** when the delta carries a fenced ` ```ops ` block
|
|
102
74
|
(`set-header` / `bump-version` / `set-criterion` / `replace-criterion`
|
|
103
|
-
/ `append-criterion`
|
|
75
|
+
/ `append-criterion` / `append-requirement` / `replace-requirement` /
|
|
76
|
+
`remove-requirement` / `remove-criterion`),
|
|
77
|
+
applies it mechanically to the target spec — all
|
|
104
78
|
ops or none — and regenerates the index (ADR 0007). Without an ops
|
|
105
79
|
block it prints a manual-merge pointer and does not write: auto-merging
|
|
106
80
|
arbitrary prose is the kind of implicit decision Doctrina avoids.
|
|
107
81
|
|
|
108
82
|
On any spec write, the index is rebuilt from the tree so it never drifts
|
|
109
|
-
from the applied spec. `doctrina change
|
|
83
|
+
from the applied spec. `doctrina change delta <id> <capability>` writes the
|
|
84
|
+
ops lines for you (`--requirement`, `--criterion`, `--bump`), dry-run
|
|
85
|
+
against the spec before anything is written. `doctrina change abandon <id>` is the inverse of
|
|
110
86
|
`change new`: it deletes an open change and its index entry and ledgers
|
|
111
|
-
the abandonment.
|
|
87
|
+
the abandonment. `doctrina close <id>` runs apply together with verify,
|
|
88
|
+
coverage, archive and validate in one attested pass — the definition of
|
|
89
|
+
done.
|
|
112
90
|
|
|
113
91
|
## `validate` checks
|
|
114
92
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
6. Every adapter template under `.doctrina/templates/adapters/` is ≤ 30 lines.
|
|
121
|
-
7. Every open change has a `proposal.md`.
|
|
122
|
-
8. Capability specs are ≤ 400 lines (warns above).
|
|
123
|
-
9. ADRs are ≤ 300 lines (warns above).
|
|
124
|
-
10. Spec/ADR files on disk are present in `index.json` (orphan warn).
|
|
125
|
-
11. Markdown links inside specs/ADRs resolve to real files (stale ref warn).
|
|
126
|
-
12. Skills carry the required frontmatter triple (`name`, `description`, `when`).
|
|
127
|
-
13. Skills are ≤ 200 lines (warns at > 150).
|
|
128
|
-
14. Each skill's `name:` matches its filename slug.
|
|
129
|
-
15. Indexed artifact metadata (spec/decision/change/contract) matches its
|
|
130
|
-
file — a mismatch is an **error** so a green `validate` can't hide the
|
|
131
|
-
drift `index rebuild --check` would catch (G5; `validate --fix` rebuilds).
|
|
132
|
-
16. Each skill's frontmatter description matches `index.json` (drift warn; `skill sync` fixes).
|
|
133
|
-
17. EARS grammar shape per section in specs declaring `## Requirements (EARS)` (warn).
|
|
134
|
-
18. Nested `AGENTS.md` files obey the root size caps (warn > 150, error > 200).
|
|
135
|
-
19. No two ADR files share the same `NNNN` number (merge-collision error).
|
|
136
|
-
20. `index.json` records a `framework_version` matching the running CLI
|
|
137
|
-
(stamp-divergence warn; `index rebuild` migrates it).
|
|
138
|
-
21. Accepted ADRs anchor to reality via `Evidence:` and/or `Landed:`; cited
|
|
139
|
-
paths must exist on disk (drift warn).
|
|
93
|
+
Structure, schema, EARS grammar, index drift, size caps, links, skills,
|
|
94
|
+
ADR anchoring and more — the full, numbered list with the reason for each
|
|
95
|
+
check is in the
|
|
96
|
+
[CLI reference](https://github.com/GCarin1/Doctrina/blob/main/docs/en/cli-reference.md#doctrina-validate).
|
|
97
|
+
A second copy here fell behind it once; there is now one list.
|
|
140
98
|
|
|
141
99
|
Exit code: 0 if no errors (warnings allowed), 1 otherwise. `--fix`
|
|
142
100
|
regenerates `index.json` from the tree before validating.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doctrina-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Doctrina CLI — spec-driven, AGENTS.md-native framework for multi-agent AI development.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
17
|
"bin": {
|
|
18
|
-
"doctrina": "
|
|
18
|
+
"doctrina": "src/index.js"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
21
|
"test": "node --test",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"repository": {
|
|
34
34
|
"type": "git",
|
|
35
|
-
"url": "https://github.com/
|
|
35
|
+
"url": "git+https://github.com/GCarin1/Doctrina.git",
|
|
36
36
|
"directory": "packages/doctrina-cli"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
package/src/commands/adapter.js
CHANGED
|
@@ -10,6 +10,7 @@ import { today } from "../lib/dates.js";
|
|
|
10
10
|
import { c } from "../lib/colors.js";
|
|
11
11
|
import { suggest } from "../lib/suggest.js";
|
|
12
12
|
import { notADoctrinaProject } from "../lib/exit-codes.js";
|
|
13
|
+
import { projectName } from "../lib/project.js";
|
|
13
14
|
|
|
14
15
|
// `doctrina adapter` — add, remove, and inventory agent adapters (audit
|
|
15
16
|
// item C1).
|
|
@@ -96,7 +97,7 @@ function adapterAdd(projectRoot, name, cmdFlags) {
|
|
|
96
97
|
|
|
97
98
|
const force = flagBool(cmdFlags, "force", false);
|
|
98
99
|
const tokens = {
|
|
99
|
-
PROJECT_NAME:
|
|
100
|
+
PROJECT_NAME: projectName(projectRoot),
|
|
100
101
|
PROJECT_DESCRIPTION: "",
|
|
101
102
|
DATE: today(),
|
|
102
103
|
};
|
|
@@ -145,7 +146,7 @@ function adapterRemove(projectRoot, name, cmdFlags) {
|
|
|
145
146
|
// Only delete files this adapter created AND that still match what it
|
|
146
147
|
// would write. A file the user edited is theirs now; --force overrides.
|
|
147
148
|
const force = flagBool(cmdFlags, "force", false);
|
|
148
|
-
const tokens = { PROJECT_NAME:
|
|
149
|
+
const tokens = { PROJECT_NAME: projectName(projectRoot), PROJECT_DESCRIPTION: "", DATE: today() };
|
|
149
150
|
let removed = 0;
|
|
150
151
|
let kept = 0;
|
|
151
152
|
// Directories a removed file lived in, innermost first. Only these are
|
package/src/commands/analyze.js
CHANGED
|
@@ -5,6 +5,7 @@ import { isDir, relPath } from "../lib/fs-ops.js";
|
|
|
5
5
|
import { c } from "../lib/colors.js";
|
|
6
6
|
import { collectAnalysis } from "../lib/analysis.js";
|
|
7
7
|
import { EXIT } from "../lib/exit-codes.js";
|
|
8
|
+
import { ensureDoctrinaProject, refuseChangeRef } from "../lib/project.js";
|
|
8
9
|
|
|
9
10
|
// The findings themselves live in lib/analysis.js, where the gate map reads
|
|
10
11
|
// them; this command is their renderer (audit finding F7).
|
|
@@ -23,6 +24,13 @@ export async function run(positional, _flags) {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const projectRoot = process.cwd();
|
|
27
|
+
// The project first, then the reference. Asked the other way round, a
|
|
28
|
+
// directory that is not a project at all answered `change "x" not found`
|
|
29
|
+
// — which points at creating a change somewhere there is nothing to
|
|
30
|
+
// create it in.
|
|
31
|
+
ensureDoctrinaProject(projectRoot);
|
|
32
|
+
const refused = refuseChangeRef(id);
|
|
33
|
+
if (refused !== null) return refused;
|
|
26
34
|
const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
|
|
27
35
|
if (!isDir(changeDir)) {
|
|
28
36
|
console.error(c.red("error:") + ` change "${id}" not found at ${relPath(projectRoot, changeDir)}`);
|
package/src/commands/change.js
CHANGED
|
@@ -6,7 +6,7 @@ import { exists, isDir, isFile, lineCount, mkdirp, move, read, relPath, remove,
|
|
|
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 { deriveIndex } from "../lib/scan.js";
|
|
9
|
+
import { archivedChangeEntry, deriveIndex } 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";
|
|
@@ -17,16 +17,16 @@ import { appendLedgerLine, archivedLine, abandonedLine } from "../lib/ledger.js"
|
|
|
17
17
|
import { suggest } from "../lib/suggest.js";
|
|
18
18
|
import { confirm, isInteractive } from "../lib/prompt.js";
|
|
19
19
|
import { EXIT } from "../lib/exit-codes.js";
|
|
20
|
-
import {
|
|
20
|
+
import { ensureDoctrinaProject, refuseChangeRef } from "../lib/project.js";
|
|
21
21
|
import { parseOperation, parseCapabilityFromDelta, isUntouchedScaffold } from "../lib/doc-model.js";
|
|
22
|
-
import { changeNew } from "../lib/change-ops.js";
|
|
22
|
+
import { changeNew, addDeltaOps } from "../lib/change-ops.js";
|
|
23
23
|
|
|
24
|
-
const SUBCOMMANDS = ["new", "apply", "archive", "check", "tick", "
|
|
24
|
+
const SUBCOMMANDS = ["new", "apply", "archive", "check", "tick", "abandon", "delta"];
|
|
25
25
|
|
|
26
26
|
// Flags this command accepts. Declared HERE, with the command, so
|
|
27
27
|
// adding a command never requires editing the entrypoint — the gap that
|
|
28
28
|
// let six flags ship undeclared and silently swallow a positional (C3).
|
|
29
|
-
export const flags = { boolean: ["json", "all", "chore", "design", "force", "no-spec", "verbose"], string: ["reason"] };
|
|
29
|
+
export const flags = { boolean: ["json", "all", "chore", "design", "force", "no-spec", "verbose"], string: ["reason", "requirement", "criterion", "bump", "op"] };
|
|
30
30
|
|
|
31
31
|
export async function run(positional, flags) {
|
|
32
32
|
const sub = positional[0];
|
|
@@ -42,10 +42,10 @@ export async function run(positional, flags) {
|
|
|
42
42
|
changeCheck(id, { verbose: flagBool(flags, "verbose", false) }));
|
|
43
43
|
case "tick":
|
|
44
44
|
return changeTick(positional.slice(1), flags);
|
|
45
|
-
case "diff":
|
|
46
|
-
return changeDiff(positional.slice(1), flags);
|
|
47
45
|
case "abandon":
|
|
48
46
|
return await changeAbandon(positional.slice(1), flags);
|
|
47
|
+
case "delta":
|
|
48
|
+
return changeDelta(positional.slice(1), flags);
|
|
49
49
|
default:
|
|
50
50
|
console.error(c.red("error:") + ` unknown change subcommand "${sub ?? ""}"`);
|
|
51
51
|
const guess = suggest(sub, SUBCOMMANDS);
|
|
@@ -100,6 +100,8 @@ async function forEachId(ids, name, one) {
|
|
|
100
100
|
}
|
|
101
101
|
let worst = 0;
|
|
102
102
|
for (const id of ids) {
|
|
103
|
+
const refused = refuseChangeRef(id);
|
|
104
|
+
if (refused !== null) { worst = Math.max(worst, refused); continue; }
|
|
103
105
|
if (ids.length > 1) {
|
|
104
106
|
console.log("");
|
|
105
107
|
console.log(c.bold(`──── change ${name} ${id}`));
|
|
@@ -108,6 +110,14 @@ async function forEachId(ids, name, one) {
|
|
|
108
110
|
try {
|
|
109
111
|
code = await one(id);
|
|
110
112
|
} catch (err) {
|
|
113
|
+
// A TYPED error is a condition of the RUN, not a verdict on this id:
|
|
114
|
+
// "not a Doctrina project" is equally true of every id, and the class
|
|
115
|
+
// it carries is the whole point of the contract. Flattening it to 1
|
|
116
|
+
// here made `change apply|archive|check` answer "fix your work and
|
|
117
|
+
// retry" where `change new`, one switch arm away, answered "run the
|
|
118
|
+
// setup command first" — same condition, same sentence, two codes.
|
|
119
|
+
// The entrypoint owns the mapping and prints the remedy; let it.
|
|
120
|
+
if (err && typeof err.exitCode === "number") throw err;
|
|
111
121
|
console.error(c.red("error:") + ` ${err.message}`);
|
|
112
122
|
code = 1;
|
|
113
123
|
}
|
|
@@ -308,7 +318,7 @@ async function changeCheck(id, { verbose = false } = {}) {
|
|
|
308
318
|
let failures = 0;
|
|
309
319
|
|
|
310
320
|
// 1. Structural analysis — same checks analyze runs before an apply.
|
|
311
|
-
console.log(c.gray("──── 1/3 structure
|
|
321
|
+
console.log(c.gray("──── 1/3 structure"));
|
|
312
322
|
const analyze = await import("./analyze.js");
|
|
313
323
|
if ((await analyze.run([id], new Map())) !== 0) failures += 1;
|
|
314
324
|
|
|
@@ -348,7 +358,7 @@ async function changeCheck(id, { verbose = false } = {}) {
|
|
|
348
358
|
if (deltaFiles.length === 0) console.log(c.gray("- no spec deltas"));
|
|
349
359
|
if (opsFindings > 0) failures += 1;
|
|
350
360
|
|
|
351
|
-
// --verbose: the
|
|
361
|
+
// --verbose: the per-delta preview the removed `change diff` printed. The dry-run
|
|
352
362
|
// above says whether the ops WOULD apply; this says what the file would
|
|
353
363
|
// look like afterwards, which is the question the separate command existed
|
|
354
364
|
// to answer (change 0049).
|
|
@@ -390,12 +400,57 @@ async function changeCheck(id, { verbose = false } = {}) {
|
|
|
390
400
|
// `tick <id> --all` checks everything (tasks.md + the proposal's
|
|
391
401
|
// ## Verification). Ticking is a claim of completion — the honest gate is
|
|
392
402
|
// still archive/verify; this only removes the mechanical friction.
|
|
403
|
+
// `change delta <id> <capability> [ops]` — write ops lines into the change's
|
|
404
|
+
// MODIFIED delta through the grammar `apply` reads (change 0205). Rendering
|
|
405
|
+
// only; lib/change-ops.js decides.
|
|
406
|
+
function changeDelta(args, flags) {
|
|
407
|
+
const [id, capability, ...rest] = args;
|
|
408
|
+
if (!id || !capability) {
|
|
409
|
+
console.error(c.red("error:") + " change delta requires <id> <capability> and at least one operation");
|
|
410
|
+
console.error(c.gray("hint: ") + "doctrina change delta <id> <capability> --requirement \"event: When <trigger>, the system shall <action>.\" --criterion \"[unverified] <signal> — verified by `<test>`\" --bump minor");
|
|
411
|
+
return EXIT.USAGE;
|
|
412
|
+
}
|
|
413
|
+
const refused = refuseChangeRef(id);
|
|
414
|
+
if (refused !== null) return refused;
|
|
415
|
+
const projectRoot = process.cwd();
|
|
416
|
+
ensureDoctrinaProject(projectRoot);
|
|
417
|
+
if (!isDir(path.join(projectRoot, ".doctrina", "changes", id))) {
|
|
418
|
+
console.error(c.red("error:") + ` change "${id}" not found at .doctrina/changes/${id}`);
|
|
419
|
+
return EXIT.USAGE;
|
|
420
|
+
}
|
|
421
|
+
// In the order a reader expects them: requirement, criterion, any raw op,
|
|
422
|
+
// and the bump last.
|
|
423
|
+
const lines = [];
|
|
424
|
+
const requirement = flagString(flags, "requirement");
|
|
425
|
+
const criterion = flagString(flags, "criterion");
|
|
426
|
+
const raw = flagString(flags, "op") ?? (rest.length > 0 ? rest.join(" ") : undefined);
|
|
427
|
+
const bump = flagString(flags, "bump");
|
|
428
|
+
if (requirement) lines.push(`append-requirement ${requirement}`);
|
|
429
|
+
if (criterion) lines.push(`append-criterion ${criterion}`);
|
|
430
|
+
if (raw) lines.push(raw);
|
|
431
|
+
if (bump) lines.push(`bump-version ${bump}`);
|
|
432
|
+
|
|
433
|
+
const r = addDeltaOps(projectRoot, id, capability, lines);
|
|
434
|
+
if (r.code !== EXIT.OK) {
|
|
435
|
+
console.error(c.red("error:") + ` ${r.error}`);
|
|
436
|
+
for (const d of r.details ?? []) console.error(` - ${d}`);
|
|
437
|
+
if (r.hint) console.error(c.gray("hint: ") + r.hint);
|
|
438
|
+
return r.code;
|
|
439
|
+
}
|
|
440
|
+
console.log(c.green(r.created ? "created" : "updated") + ` ${r.deltaRel}` +
|
|
441
|
+
c.gray(` — ${lines.length} op${lines.length === 1 ? "" : "s"} added; the block now holds ${r.block.length} (${r.block.join(", ")}), and applies cleanly`));
|
|
442
|
+
console.log(c.gray("Preview the merge: ") + c.cyan(`doctrina change check ${id} --verbose`));
|
|
443
|
+
return EXIT.OK;
|
|
444
|
+
}
|
|
445
|
+
|
|
393
446
|
function changeTick(args, flags) {
|
|
394
447
|
const id = args[0];
|
|
395
448
|
if (!id) {
|
|
396
449
|
console.error(c.red("error:") + " change tick requires <id> [ordinals... | --all]");
|
|
397
450
|
return 2;
|
|
398
451
|
}
|
|
452
|
+
const refused = refuseChangeRef(id);
|
|
453
|
+
if (refused !== null) return refused;
|
|
399
454
|
const projectRoot = process.cwd();
|
|
400
455
|
ensureDoctrinaProject(projectRoot);
|
|
401
456
|
const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
|
|
@@ -522,7 +577,7 @@ function changeArchive(args, flags) {
|
|
|
522
577
|
|
|
523
578
|
// Verification gate. "Done" is a claim until it is checked. Archiving is
|
|
524
579
|
// the act of declaring a change finished, so it must refuse while any
|
|
525
|
-
// task
|
|
580
|
+
// task or any declared verification item is
|
|
526
581
|
// still unchecked. This is the difference between "boxes marked" and
|
|
527
582
|
// "verification passed" the framework was faulted for collapsing.
|
|
528
583
|
// Preconditions from the shared map (C6): structure AND verification.
|
|
@@ -594,14 +649,10 @@ function changeArchive(args, flags) {
|
|
|
594
649
|
console.log(c.green("ledger") + ` +1 line in ${relPath(projectRoot, ledgerFile)}`);
|
|
595
650
|
|
|
596
651
|
const index = idx.load(projectRoot);
|
|
597
|
-
idx.moveChangeToArchive(index, id,
|
|
598
|
-
|
|
599
|
-
title,
|
|
600
|
-
|
|
601
|
-
status: "applied",
|
|
602
|
-
applied: date,
|
|
603
|
-
specs_affected: specsAffected,
|
|
604
|
-
});
|
|
652
|
+
idx.moveChangeToArchive(index, id, archivedChangeEntry(
|
|
653
|
+
exists(proposal) ? read(proposal) : "",
|
|
654
|
+
{ id, archiveName, applied: date, specsAffected, title },
|
|
655
|
+
));
|
|
605
656
|
idx.touch(index, date);
|
|
606
657
|
idx.save(projectRoot, index);
|
|
607
658
|
console.log(c.green("indexed") + " change archived");
|
|
@@ -618,6 +669,8 @@ async function changeAbandon(args, flags) {
|
|
|
618
669
|
console.error(c.red("error:") + " change abandon requires <id>");
|
|
619
670
|
return 2;
|
|
620
671
|
}
|
|
672
|
+
const refused = refuseChangeRef(id);
|
|
673
|
+
if (refused !== null) return refused;
|
|
621
674
|
const projectRoot = process.cwd();
|
|
622
675
|
ensureDoctrinaProject(projectRoot);
|
|
623
676
|
|
|
@@ -675,9 +728,9 @@ async function changeAbandon(args, flags) {
|
|
|
675
728
|
// ADDED reports the body it would write, REMOVED the spec it would delete,
|
|
676
729
|
// MODIFIED a line diff against the current spec.
|
|
677
730
|
//
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
// is what
|
|
731
|
+
// `change check --verbose` prints this after its ops dry-run. It was also
|
|
732
|
+
// the whole of `change diff`, until that alias was retired (changes 0049,
|
|
733
|
+
// 0175) — the one renderer is what made the merge honest. Returns the number of
|
|
681
734
|
// deltas it could not read.
|
|
682
735
|
export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
|
|
683
736
|
let errors = 0;
|
|
@@ -725,33 +778,6 @@ export function printDeltaPreview(projectRoot, changeDir, deltaFiles) {
|
|
|
725
778
|
return errors;
|
|
726
779
|
}
|
|
727
780
|
|
|
728
|
-
function changeDiff(args, _flags) {
|
|
729
|
-
const id = args[0];
|
|
730
|
-
if (!id) {
|
|
731
|
-
console.error(c.red("error:") + " change diff requires <id>");
|
|
732
|
-
return 2;
|
|
733
|
-
}
|
|
734
|
-
const projectRoot = process.cwd();
|
|
735
|
-
ensureDoctrinaProject(projectRoot);
|
|
736
|
-
|
|
737
|
-
const changeDir = path.join(projectRoot, ".doctrina", "changes", id);
|
|
738
|
-
if (!isDir(changeDir)) {
|
|
739
|
-
console.error(c.red("error:") + ` change "${id}" not found at ${relPath(projectRoot, changeDir)}`);
|
|
740
|
-
return EXIT.USAGE;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const deltaFiles = walk(path.join(changeDir, "specs")).filter((p) => p.endsWith("delta.md"));
|
|
744
|
-
if (deltaFiles.length === 0) {
|
|
745
|
-
console.log(c.gray("no spec deltas in this change; nothing to diff"));
|
|
746
|
-
return 0;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
const errors = printDeltaPreview(projectRoot, changeDir, deltaFiles);
|
|
750
|
-
console.log("");
|
|
751
|
-
return errors > 0 ? 1 : 0;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
|
|
755
781
|
function extractDeltaBody(text) {
|
|
756
782
|
// The delta separates headers from the spec body with a `---` line.
|
|
757
783
|
const idxSep = text.indexOf("\n---\n");
|
|
@@ -760,34 +786,6 @@ function extractDeltaBody(text) {
|
|
|
760
786
|
}
|
|
761
787
|
|
|
762
788
|
|
|
763
|
-
// Reasons a change is not finished enough to archive. Counts unchecked
|
|
764
|
-
// GitHub-style checkboxes (`- [ ]`) in tasks.md (every task, including the
|
|
765
|
-
// closing steps) and in the proposal's "## Verification" section. Returns
|
|
766
|
-
// a list of human-readable blocker strings; empty means clear to archive.
|
|
767
|
-
function collectArchiveBlockers(changeDir) {
|
|
768
|
-
const blockers = [];
|
|
769
|
-
const countUnchecked = (s) => (s.match(/^\s*-\s*\[ \]/gm) ?? []).length;
|
|
770
|
-
|
|
771
|
-
const tasksPath = path.join(changeDir, "tasks.md");
|
|
772
|
-
if (exists(tasksPath)) {
|
|
773
|
-
const n = countUnchecked(read(tasksPath));
|
|
774
|
-
if (n > 0) blockers.push(`${n} unchecked task${n === 1 ? "" : "s"} in tasks.md (closing steps count)`);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
const proposalPath = path.join(changeDir, "proposal.md");
|
|
778
|
-
if (exists(proposalPath)) {
|
|
779
|
-
const n = countUnchecked(getSection(read(proposalPath), "Verification"));
|
|
780
|
-
if (n > 0) blockers.push(`${n} unmet verification item${n === 1 ? "" : "s"} in proposal.md (## Verification)`);
|
|
781
|
-
}
|
|
782
|
-
return blockers;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function ensureDoctrinaProject(projectRoot) {
|
|
786
|
-
if (!exists(path.join(projectRoot, ".doctrina"))) {
|
|
787
|
-
throw notADoctrinaProject();
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
789
|
export const help = `
|
|
792
790
|
Usage: doctrina change <subcommand> [args]
|
|
793
791
|
|
|
@@ -805,7 +803,7 @@ Subcommands:
|
|
|
805
803
|
MODIFIED without one prints a manual-merge pointer.
|
|
806
804
|
On any spec write the index is rebuilt from the tree.
|
|
807
805
|
archive <id...> Move the change to .doctrina/changes/archive/YYYY-MM-DD-<id>/
|
|
808
|
-
check <id...> Pre-close dry-run, read-only:
|
|
806
|
+
check <id...> Pre-close dry-run, read-only: the structural
|
|
809
807
|
checks + every ops block executed in memory against
|
|
810
808
|
its target + the archive gate preview + an advisory
|
|
811
809
|
list of accepted ADRs citing the touched capabilities.
|
|
@@ -815,8 +813,13 @@ Subcommands:
|
|
|
815
813
|
or every one with --all. No args = list only.
|
|
816
814
|
abandon <id> Delete an open change folder and its index entry, and
|
|
817
815
|
record the abandonment in the ledger ([--reason "..."]).
|
|
818
|
-
|
|
819
|
-
|
|
816
|
+
delta <id> <capability> [--requirement "<section>: <text>"] [--criterion "<text>"]
|
|
817
|
+
[--bump major|minor|patch] [--op "<op line>" | <op line>]
|
|
818
|
+
Write ops into the change's MODIFIED delta for the
|
|
819
|
+
capability — created from the template when absent —
|
|
820
|
+
parsed by the grammar apply reads and dry-run against
|
|
821
|
+
the spec first: nothing is written unless the whole
|
|
822
|
+
block applies. A second --bump replaces the first.
|
|
820
823
|
|
|
821
824
|
apply / archive / check accept multiple ids (batch close of a backlog); the
|
|
822
825
|
exit code is the worst per-id result.
|
|
@@ -829,9 +832,12 @@ A MODIFIED delta may carry a fenced operations block that \`apply\` executes:
|
|
|
829
832
|
append-criterion [unverified] new signal — verified by \`test/x.test.ts\`
|
|
830
833
|
append-requirement event: When <trigger>, the system shall <action>.
|
|
831
834
|
replace-requirement ubiquitous 2: The system shall <action>.
|
|
835
|
+
remove-requirement state 1: superseded by event 3
|
|
836
|
+
remove-criterion 4: proven by criterion 2 since 0.9
|
|
832
837
|
\`\`\`
|
|
833
838
|
append-* ops resolve numbering/position at apply time, so concurrent open
|
|
834
|
-
changes appending to the same spec cannot collide.
|
|
839
|
+
changes appending to the same spec cannot collide. \`change delta\` writes
|
|
840
|
+
these lines for you, one call per addition.
|
|
835
841
|
|
|
836
842
|
Options:
|
|
837
843
|
--force Overwrite existing files (where applicable)
|
package/src/commands/clarify.js
CHANGED
|
@@ -37,7 +37,14 @@ const RULES_EN = [
|
|
|
37
37
|
// hid the one real finding among them ("name some of them", which said
|
|
38
38
|
// nothing about how many). A gate that cries wolf teaches people to
|
|
39
39
|
// ignore it.
|
|
40
|
-
|
|
40
|
+
//
|
|
41
|
+
// The quality adjectives sit here for the same reason the quantifiers do:
|
|
42
|
+
// "robust" names no threshold, so it cannot be met or missed. They were
|
|
43
|
+
// in the Portuguese lexicon and nowhere in this one, so the SAME sentence
|
|
44
|
+
// translated came back with three smells in one language and none in the
|
|
45
|
+
// other — and this project keeps its own pages in both. A smell test that
|
|
46
|
+
// answers differently per language is not testing the writing.
|
|
47
|
+
re: /(?<!\bhow\s+)\b(many|few|some|several|adequate|robust|scalable)\b(?!\s+\d)/gi,
|
|
41
48
|
hint: "quantify (use a number or a precise scope)",
|
|
42
49
|
},
|
|
43
50
|
{
|