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/src/lib/commands.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// Keep this sorted by the workflow order used in `--help`, not alphabetically.
|
|
13
13
|
export const COMMAND_NAMES = [
|
|
14
14
|
// bootstrap / day-to-day
|
|
15
|
-
"init", "intake", "triage", "work",
|
|
15
|
+
"init", "ui", "onboard", "intake", "triage", "work",
|
|
16
16
|
// authoring
|
|
17
17
|
"spec", "change", "contract", "decision", "skill", "intent", "adapter",
|
|
18
18
|
// read / orient
|
|
@@ -37,6 +37,8 @@ export const COMMAND_NAMES = [
|
|
|
37
37
|
// every op.
|
|
38
38
|
export const OPERATIONS = [
|
|
39
39
|
["init", "Scaffold AGENTS.md and .doctrina/ in the current directory"],
|
|
40
|
+
["ui", "The console: type requests, run commands, watch the agent work"],
|
|
41
|
+
["onboard", "Walk the whole cycle once on this repository, step by step"],
|
|
40
42
|
["intake", "Store the full project description; print the bootstrap playbook"],
|
|
41
43
|
["triage", "Classify a request into a lane, and check the runtime surface"],
|
|
42
44
|
["work", "Brief prompt -> scaffolded change + guided work playbook"],
|
|
@@ -49,6 +51,7 @@ export const OPERATIONS = [
|
|
|
49
51
|
["change check", "Pre-close dry-run: everything close would refuse, listed first"],
|
|
50
52
|
["change tick", "List/tick the unchecked boxes (tasks + verification; --all)"],
|
|
51
53
|
["change abandon", "Discard an open change cleanly (recorded in the ledger)"],
|
|
54
|
+
["change renumber", "Give an open change the next free number (two branches took the same one)"],
|
|
52
55
|
["change delta", "Add a requirement/criterion/bump to a change's delta (ops written for you)"],
|
|
53
56
|
["change card", "One task as an executor receives it: the task, its why, the spec cut to it"],
|
|
54
57
|
["contract new", "Own the integration surface (ports, env, interfaces)"],
|
|
@@ -96,6 +99,7 @@ export const OPERATIONS = [
|
|
|
96
99
|
["delivery route", "Choose the projects a delivery touches, dependencies first"],
|
|
97
100
|
["delivery list", "Deliveries with status, ref and closed/total changes"],
|
|
98
101
|
["delivery status", "Per project: the changes carrying a delivery and their tasks"],
|
|
102
|
+
["delivery tick", "List or tick a delivery's Release claims"],
|
|
99
103
|
["delivery check", "What still keeps a delivery from closing (exit 1 while anything does)"],
|
|
100
104
|
["delivery close", "Close a delivery once every project's change is closed"],
|
|
101
105
|
["templates list", "List the templates shipped by the installed CLI"],
|
|
@@ -103,6 +107,7 @@ export const OPERATIONS = [
|
|
|
103
107
|
["templates update", "Additive fixer for check findings (preview; --write applies)"],
|
|
104
108
|
["hooks install", "Install the pre-commit hook"],
|
|
105
109
|
["index rebuild", "Regenerate index.json from the artifacts on disk"],
|
|
110
|
+
["index merge", "The merge driver git runs for index.json (set up by hooks install)"],
|
|
106
111
|
["next", "Print the recommended next workflow actions"],
|
|
107
112
|
["watch", "Re-run validate --fix + next on every change (--once for one pass)"],
|
|
108
113
|
["metrics", "Local git-derived adoption metrics (no network)"],
|
|
@@ -131,6 +136,8 @@ export const OPERATIONS = [
|
|
|
131
136
|
// `templates check` fails when either is missing.
|
|
132
137
|
export const COMMAND_META = {
|
|
133
138
|
init: { moment: "Bootstrap", when: "starting a project that has no AGENTS.md yet", purpose: "scaffold AGENTS.md and .doctrina/" },
|
|
139
|
+
ui: { moment: "Bootstrap", when: "a human wants to drive the project from one window instead of typing commands", purpose: "the interactive console: requests to the agent, commands, the project's state" },
|
|
140
|
+
onboard: { moment: "Bootstrap", when: "the first session in a project, or showing it to someone new", purpose: "walk the whole cycle once, step by step, on this repository" },
|
|
134
141
|
intake: { moment: "Bootstrap", when: "you have a full project description and no specs yet", purpose: "store the intent and print the bootstrap playbook" },
|
|
135
142
|
adapter: { moment: "Bootstrap", when: "adding or removing an agent's pointer files", purpose: "install/remove agent adapters (additive)" },
|
|
136
143
|
|
|
@@ -350,7 +357,40 @@ const COMPACT_MOMENTS = new Set(["Orchestrate", "Maintain"]);
|
|
|
350
357
|
// trigger: an agent scanning it learns WHEN to reach for a command, which
|
|
351
358
|
// a name index never told it. Same shape `context` prints for skills —
|
|
352
359
|
// description plus `when:`, body on demand.
|
|
353
|
-
|
|
360
|
+
// TWO PROFILES OF THE SAME CATALOG (change 0251).
|
|
361
|
+
//
|
|
362
|
+
// `full` names every command, by moment. It is what this block has always
|
|
363
|
+
// been, and it costs 37 lines of a file capped at 150 that an agent reads
|
|
364
|
+
// on every turn — for 39 commands, most of which a session never reaches
|
|
365
|
+
// for. `core` names the loop a session actually runs and defers the rest to
|
|
366
|
+
// `doctrina --help`, which is where the playbook a command prints already
|
|
367
|
+
// sends the agent for the next step. A project chooses in
|
|
368
|
+
// `.doctrina/config.json` ("surface"); `init` writes `core` for a new one.
|
|
369
|
+
export const SURFACE_PROFILES = Object.freeze(["full", "core"]);
|
|
370
|
+
export const CORE_SURFACE = Object.freeze(["prime", "work", "context", "close", "next", "validate", "doctor", "handoff"]);
|
|
371
|
+
|
|
372
|
+
function coreSurfaceMarkdown() {
|
|
373
|
+
const lines = [
|
|
374
|
+
"## Doctrina command surface (generated — reach for these, don't hand-author)",
|
|
375
|
+
"",
|
|
376
|
+
"The loop a session runs. Each command prints the next step; the CLI",
|
|
377
|
+
"scaffolds from canonical templates and syncs `index.json`.",
|
|
378
|
+
];
|
|
379
|
+
for (const cmd of CORE_SURFACE) {
|
|
380
|
+
const meta = COMMAND_META[cmd];
|
|
381
|
+
const hint = SURFACE_HINTS[cmd] ? ` ${SURFACE_HINTS[cmd]}` : "";
|
|
382
|
+
lines.push(`- \`doctrina ${cmd}${hint}\` — ${meta.purpose}. *When:* ${meta.when}.`);
|
|
383
|
+
}
|
|
384
|
+
lines.push("**Everything else** — specs, changes by hand, decisions, contracts, skills, the single gates, " +
|
|
385
|
+
"workspaces and deliveries: `doctrina --help` lists every command with its moment, and " +
|
|
386
|
+
"`doctrina <command> --help` its flags. (`\"surface\": \"full\"` in `.doctrina/config.json`, then " +
|
|
387
|
+
"`doctrina upgrade --write`, lists them all here.)");
|
|
388
|
+
return lines.join("\n");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** @param {string} [profile] "full" (every command) or "core" (the session loop) */
|
|
392
|
+
export function surfaceMarkdown(profile = "full") {
|
|
393
|
+
if (profile === "core") return coreSurfaceMarkdown();
|
|
354
394
|
// A deprecated operation stays in the catalog — `--help` and the shell
|
|
355
395
|
// completions must still know the name someone types — but leaves this
|
|
356
396
|
// block: it is the list of commands to REACH FOR, and pointing an agent at
|
|
@@ -399,8 +439,8 @@ export function surfaceMarkdown() {
|
|
|
399
439
|
}
|
|
400
440
|
|
|
401
441
|
// The block as written to AGENTS.md: markers wrapping the generated section.
|
|
402
|
-
export function surfaceBlock() {
|
|
403
|
-
return `${SURFACE_BEGIN}\n${surfaceMarkdown()}\n${SURFACE_END}`;
|
|
442
|
+
export function surfaceBlock(profile = "full") {
|
|
443
|
+
return `${SURFACE_BEGIN}\n${surfaceMarkdown(profile)}\n${SURFACE_END}`;
|
|
404
444
|
}
|
|
405
445
|
|
|
406
446
|
// The block's CANONICAL POSITION, defined once — by the shipped template,
|
|
@@ -542,6 +582,20 @@ export const AGENT_CHANGELOG = {
|
|
|
542
582
|
// Each entry lists only ITS OWN delta. The series carries the rest
|
|
543
583
|
// forward (see agentChangelogEntries), so a patch never has to restate
|
|
544
584
|
// the minor that introduced the commands, and never silently erases it.
|
|
585
|
+
"0.22.0": [
|
|
586
|
+
"An edit that changes nothing a spec says opens no change: `doctrina work` answers `direct` and exits 0 — edit, run `doctrina verify`, commit. `--force` only when a sentence of a spec does change.",
|
|
587
|
+
"Run `doctrina hooks install` once in each clone and commit the `.gitattributes` it writes: `index.json` and the ledger then merge across branches. Two open changes with one number: `doctrina change renumber <id>`.",
|
|
588
|
+
"A requirement that claims something of every input is proven by a property-based test: claim it with `doctrina change delta <id> <cap> --property \"…\"`; an example test cited for `[property]` fails `coverage --strict`. `doctrina coverage --properties` lists the candidates.",
|
|
589
|
+
"A request \"sent by the Doctrina console <name>\" is done in this repository and your last message is its report — never hand it on. Told other consoles are open, `doctrina ui --send <name> \"<self-contained task>\"` hands one a task and waits for the report.",
|
|
590
|
+
"A new project's AGENTS.md lists the session loop only (`surface: core`); `doctrina --help` has every command. `doctrina onboard` walks a newcomer through the cycle; `init --preset <dir>` lays an organisation's preset.",
|
|
591
|
+
],
|
|
592
|
+
"0.21.0": [
|
|
593
|
+
"At a workspace root, `next`, `handoff`, `status`, `validate`, `context`, `doctor` and `metrics` answer for the workspace; never `doctrina init` there or in a shadowed repository's clone — the commands name `doctrina workspace attach` instead.",
|
|
594
|
+
"A task card carries the delta, the delivery's story and every binding decision, global ones included: as an executor, stop and report when the story or the task contradicts one; a card on placeholder tasks means the planner writes them first (`doctrina work --resume <id>`).",
|
|
595
|
+
"Run the command lines the CLI prints as printed: paths and titles are quoted for the shell, and tracker text stays data.",
|
|
596
|
+
"Cursor: the core loop is skills in `.cursor/skills/doctrina-*/`; run `doctrina upgrade --write` to move an older install. With a tool declared the guard fails closed — a refused call means fix the install or ask the human.",
|
|
597
|
+
"Absorb: after writing specs by hand run `doctrina validate --fix`; a stored code inventory resumes with a bare `doctrina intake`, never `--from-code` again.",
|
|
598
|
+
],
|
|
545
599
|
"0.20.0": [
|
|
546
600
|
"Work across repositories is a delivery: from the workspace, `doctrina delivery new \"<title>\" --ref <id>`, then follow its playbook — route, one change per repository with `doctrina work \"<title>\" --delivery <id>`, close each, then `doctrina delivery close <id>`.",
|
|
547
601
|
"Write tasks as self-contained cards (indented `Files:`, `Requirement:`, `Verify:` lines) and hand an executor `doctrina change card <id>`; one writer per repository, and only the planner closes (ADR 0030).",
|
package/src/lib/config.js
CHANGED
|
@@ -20,6 +20,8 @@ import { isFile, read } from "./fs-ops.js";
|
|
|
20
20
|
|
|
21
21
|
export const CONFIG_REL = ".doctrina/config.json";
|
|
22
22
|
export const RULES_REL = ".doctrina/rules.json";
|
|
23
|
+
/** The values `surface` takes — the profiles of lib/commands.js, named here so the config has no import to cycle on. */
|
|
24
|
+
const SURFACE_VALUES = ["full", "core"];
|
|
23
25
|
|
|
24
26
|
// The built-in default for every option. `language: null` means "detect per
|
|
25
27
|
// file", which is what the absence of a declaration has always meant.
|
|
@@ -27,6 +29,7 @@ export const DEFAULTS = Object.freeze({
|
|
|
27
29
|
language: null,
|
|
28
30
|
context_budget: 15000,
|
|
29
31
|
rules: [],
|
|
32
|
+
surface: "full",
|
|
30
33
|
});
|
|
31
34
|
|
|
32
35
|
// Where a value came from, most authoritative first. The strings are printed
|
|
@@ -53,8 +56,8 @@ export function configPath(projectRoot) {
|
|
|
53
56
|
*
|
|
54
57
|
* @param {string} projectRoot
|
|
55
58
|
* @returns {{
|
|
56
|
-
* language: string|null, context_budget: number, rules: any[],
|
|
57
|
-
* sources: {language: string, context_budget: string, rules: string},
|
|
59
|
+
* language: string|null, context_budget: number, rules: any[], surface: string,
|
|
60
|
+
* sources: {language: string, context_budget: string, rules: string, surface: string},
|
|
58
61
|
* errors: string[],
|
|
59
62
|
* unknown: string[]
|
|
60
63
|
* }}
|
|
@@ -65,12 +68,12 @@ export function loadConfig(projectRoot) {
|
|
|
65
68
|
// Keys the CLI does not know (change 0115). `{"context_budjet": 10}` was
|
|
66
69
|
// silence at every door, and the author believed the budget was set.
|
|
67
70
|
// Reported, never rejected: the effective values are unchanged.
|
|
68
|
-
const KNOWN_KEYS = new Set(["language", "context_budget", "rules", "$comment", "$schema"]);
|
|
71
|
+
const KNOWN_KEYS = new Set(["language", "context_budget", "rules", "surface", "preset", "$comment", "$schema"]);
|
|
69
72
|
const unknown = cfg && typeof cfg === "object" && !Array.isArray(cfg)
|
|
70
73
|
? Object.keys(cfg).filter((k) => !KNOWN_KEYS.has(k))
|
|
71
74
|
: [];
|
|
72
|
-
/** @type {{language: string, context_budget: string, rules: string}} */
|
|
73
|
-
const sources = { language: SOURCES.default, context_budget: SOURCES.default, rules: SOURCES.default };
|
|
75
|
+
/** @type {{language: string, context_budget: string, rules: string, surface: string}} */
|
|
76
|
+
const sources = { language: SOURCES.default, context_budget: SOURCES.default, rules: SOURCES.default, surface: SOURCES.default };
|
|
74
77
|
|
|
75
78
|
// language — config.json has always been its only home.
|
|
76
79
|
//
|
|
@@ -79,6 +82,7 @@ export function loadConfig(projectRoot) {
|
|
|
79
82
|
// "default" would tell a reader the file does not say what it plainly
|
|
80
83
|
// says. Only a key that is absent, or one whose value is rejected, falls
|
|
81
84
|
// back to the default.
|
|
85
|
+
/** @type {string | null} */
|
|
82
86
|
let language = DEFAULTS.language;
|
|
83
87
|
if (cfg && "language" in cfg) {
|
|
84
88
|
const rawLang = cfg.language;
|
|
@@ -130,7 +134,18 @@ export function loadConfig(projectRoot) {
|
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
// surface — which commands AGENTS.md lists (change 0251).
|
|
138
|
+
let surface = DEFAULTS.surface;
|
|
139
|
+
if (cfg && "surface" in cfg) {
|
|
140
|
+
if (SURFACE_VALUES.includes(cfg.surface)) {
|
|
141
|
+
surface = cfg.surface;
|
|
142
|
+
sources.surface = SOURCES.config;
|
|
143
|
+
} else {
|
|
144
|
+
errors.push(`${CONFIG_REL}: unknown surface ${JSON.stringify(cfg.surface)} (use "core" or "full")`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { language, context_budget, rules, surface, sources, errors, unknown };
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
/**
|
|
@@ -148,6 +163,7 @@ export function configRows(projectRoot) {
|
|
|
148
163
|
{ option: "language", value: show(cfg.language), source: cfg.sources.language, configured: cfg.sources.language !== SOURCES.default },
|
|
149
164
|
{ option: "context_budget", value: `${cfg.context_budget} tokens`, source: cfg.sources.context_budget, configured: cfg.sources.context_budget !== SOURCES.default },
|
|
150
165
|
{ option: "rules", value: `${cfg.rules.length} rule${cfg.rules.length === 1 ? "" : "s"}`, source: cfg.sources.rules, configured: cfg.sources.rules !== SOURCES.default },
|
|
166
|
+
{ option: "surface", value: cfg.surface === "core" ? "core (the session loop)" : "full (every command)", source: cfg.sources.surface, configured: cfg.sources.surface !== SOURCES.default },
|
|
151
167
|
];
|
|
152
168
|
}
|
|
153
169
|
|
|
@@ -193,14 +209,17 @@ function readJson(file, rel, errors) {
|
|
|
193
209
|
*
|
|
194
210
|
* @returns {string} the file's contents, newline-terminated
|
|
195
211
|
*/
|
|
196
|
-
export function scaffoldConfig() {
|
|
212
|
+
export function scaffoldConfig({ surface = /** @type {string | null} */ (null) } = {}) {
|
|
197
213
|
return JSON.stringify({
|
|
198
214
|
$comment:
|
|
199
|
-
"Doctrina project configuration
|
|
215
|
+
"Doctrina project configuration — add a key to " +
|
|
200
216
|
"change it. Options: \"language\" (\"en\" | \"pt\", or null to detect per file); " +
|
|
217
|
+
"\"surface\" (\"core\": AGENTS.md lists the session loop and defers the rest to `doctrina --help`; " +
|
|
218
|
+
"\"full\": it lists every command; then `doctrina upgrade --write`); " +
|
|
201
219
|
`"context_budget" (positive integer, default ${DEFAULTS.context_budget}); ` +
|
|
202
220
|
"\"rules\" (array of { id, forbid, paths, message } — permanent project " +
|
|
203
221
|
"constraints enforced by `doctrina validate`). Run `doctrina doctor` to see the " +
|
|
204
222
|
"effective value of each option and where it came from.",
|
|
223
|
+
...(surface ? { surface } : {}),
|
|
205
224
|
}, null, 2) + "\n";
|
|
206
225
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readdirSync } from "node:fs";
|
|
4
|
+
import { isDir, isFile, read, walk } from "./fs-ops.js";
|
|
5
|
+
|
|
6
|
+
// A MERGE THAT STOPPED INSIDE AN ARTIFACT (seventh audit, change 0250).
|
|
7
|
+
//
|
|
8
|
+
// Two branches each added a requirement to the same section of a spec, the
|
|
9
|
+
// merge stopped with `<<<<<<<` / `=======` / `>>>>>>>` in the file, and
|
|
10
|
+
// `validate` answered "0 errors": the markers are not headers, bullets or
|
|
11
|
+
// criteria, so no check had a reason to look at them. A spec is read as
|
|
12
|
+
// written — by the agent, by `context`, by `apply` — markers and both
|
|
13
|
+
// halves included.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The line of the first conflict marker, or null. A file is conflicted when
|
|
17
|
+
* it holds an opening AND a closing marker at the start of a line, outside a
|
|
18
|
+
* fenced block (a document may show the markers as an example).
|
|
19
|
+
*/
|
|
20
|
+
export function conflictMarkerLine(text) {
|
|
21
|
+
let fenced = false;
|
|
22
|
+
let opened = null;
|
|
23
|
+
const lines = String(text).split(/\r?\n/);
|
|
24
|
+
for (let i = 0; i < lines.length; i++) {
|
|
25
|
+
if (/^[ \t]*(```|~~~)/.test(lines[i])) fenced = !fenced;
|
|
26
|
+
if (fenced) continue;
|
|
27
|
+
if (opened === null && /^<{7}( |$)/.test(lines[i])) opened = i + 1;
|
|
28
|
+
else if (opened !== null && /^>{7}( |$)/.test(lines[i])) return opened;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The Markdown a session reads as current truth: the hub, the product, specs, decisions, skills, contracts, open changes. */
|
|
34
|
+
export function livingArtifacts(projectRoot) {
|
|
35
|
+
const root = path.join(projectRoot, ".doctrina");
|
|
36
|
+
const out = [path.join(projectRoot, "AGENTS.md"), path.join(root, "product.md")].filter(isFile);
|
|
37
|
+
for (const dir of ["specs", "decisions", "skills", "contracts"]) {
|
|
38
|
+
out.push(...walk(path.join(root, dir)).filter((f) => f.endsWith(".md")));
|
|
39
|
+
}
|
|
40
|
+
const changes = path.join(root, "changes");
|
|
41
|
+
if (isDir(changes)) {
|
|
42
|
+
for (const entry of readdirSync(changes)) {
|
|
43
|
+
if (entry === "archive" || entry.startsWith(".")) continue;
|
|
44
|
+
out.push(...walk(path.join(changes, entry)).filter((f) => f.endsWith(".md")));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every living artifact a merge stopped in.
|
|
52
|
+
* @returns {{ file: string, line: number }[]}
|
|
53
|
+
*/
|
|
54
|
+
export function conflictedArtifacts(projectRoot) {
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const file of livingArtifacts(projectRoot)) {
|
|
57
|
+
const line = conflictMarkerLine(read(file));
|
|
58
|
+
if (line !== null) out.push({ file, line });
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|