dw-sdlc 0.17.0 → 0.19.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 +107 -69
- package/config/agentic.config.schema.json +18 -2
- package/dist/catalogue.d.ts +4 -0
- package/dist/catalogue.js +7 -7
- package/dist/checks/check.d.ts +2 -0
- package/dist/checks/decision-records.d.ts +15 -0
- package/dist/checks/decision-records.js +15 -1
- package/dist/checks/index.d.ts +1 -0
- package/dist/checks/index.js +7 -1
- package/dist/checks/primitives.js +16 -5
- package/dist/checks/skill-sections.d.ts +5 -0
- package/dist/checks/skill-sections.js +163 -0
- package/dist/commands/check.js +50 -5
- package/dist/commands/doctor.js +5 -1
- package/dist/commands/primitives.d.ts +1 -1
- package/dist/commands/primitives.js +38 -7
- package/dist/commands/recap.js +22 -6
- package/dist/commands/setup.d.ts +2 -2
- package/dist/commands/setup.js +2 -2
- package/dist/commands/sweep.js +5 -8
- package/dist/markdown.d.ts +2 -0
- package/dist/markdown.js +11 -2
- package/dist/primitives-diff.d.ts +31 -0
- package/dist/primitives-diff.js +123 -0
- package/dist/recap-primitives.d.ts +12 -0
- package/dist/recap-primitives.js +26 -0
- package/dist/run-command.js +4 -2
- package/dist/sweep-trees.d.ts +10 -0
- package/dist/sweep-trees.js +22 -2
- package/package.json +2 -2
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// `skill-sections`: each README section a skill copies, a part, matches its source. A part sits between
|
|
2
|
+
// `<!-- README.md#<anchor> -->` and `<!-- /README.md#<anchor> -->` and repeats only a section a Primitive's
|
|
3
|
+
// docs name. `dw-sdlc check skill-sections --write` writes each part from its section.
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join, posix } from "node:path";
|
|
6
|
+
import { resolveConfig } from "../agentic-config.js";
|
|
7
|
+
import { readCatalogue } from "../catalogue.js";
|
|
8
|
+
import { headings, openFence, prose } from "../markdown.js";
|
|
9
|
+
import { CannotRun } from "./check.js";
|
|
10
|
+
import { catalogueFile } from "./primitives.js";
|
|
11
|
+
import { matching, treeFiles } from "./tree.js";
|
|
12
|
+
/** Either marker of a part: `/` before `README.md` closes it. */
|
|
13
|
+
const MARKER = /<!-- (\/?)README\.md#(\S+) -->/g;
|
|
14
|
+
const WRITE = "dw-sdlc check skill-sections --write";
|
|
15
|
+
const REMEDIES = {
|
|
16
|
+
stale: `edit the README, not the part, then run ${WRITE}`,
|
|
17
|
+
readme: "write the README, or drop the part",
|
|
18
|
+
section: "point it at a heading the README has",
|
|
19
|
+
unpaired: "open a part with <!-- README.md#<anchor> --> and close it with <!-- /README.md#<anchor> -->, each on its own line",
|
|
20
|
+
missing: "create it, or point gate.checks.primitives.file at the catalogue",
|
|
21
|
+
invalid: "fix what dw-sdlc check primitives names",
|
|
22
|
+
fence: "close the fence"
|
|
23
|
+
};
|
|
24
|
+
/** The markers outside code in `text`, in order. */
|
|
25
|
+
function markers(text) {
|
|
26
|
+
return Array.from(prose(text).matchAll(MARKER), (match) => ({
|
|
27
|
+
close: match[1] === "/",
|
|
28
|
+
anchor: match[2] ?? "",
|
|
29
|
+
start: match.index,
|
|
30
|
+
end: match.index + match[0].length
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
/** The README a skill file copies from: the one beside the `skills` directory it is in, else beside it. */
|
|
34
|
+
function readmeOf(path) {
|
|
35
|
+
const directories = path.split("/").slice(0, -1);
|
|
36
|
+
const at = directories.lastIndexOf("skills");
|
|
37
|
+
const unit = at === -1 ? directories : directories.slice(0, at);
|
|
38
|
+
return posix.join(...unit, "README.md");
|
|
39
|
+
}
|
|
40
|
+
/** The section under the heading with `anchor`, up to the next heading of its level or above, its end trimmed. */
|
|
41
|
+
function sectionOf(markdown, anchor) {
|
|
42
|
+
const all = headings(markdown);
|
|
43
|
+
const start = all.find((heading) => heading.anchor === anchor);
|
|
44
|
+
if (start === undefined) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const end = all.find((heading) => heading.line > start.line && heading.level <= start.level);
|
|
48
|
+
return markdown
|
|
49
|
+
.split("\n")
|
|
50
|
+
.slice(start.line - 1, end === undefined ? undefined : end.line - 1)
|
|
51
|
+
.join("\n")
|
|
52
|
+
.trimEnd();
|
|
53
|
+
}
|
|
54
|
+
function lineAt(text, offset) {
|
|
55
|
+
return text.slice(0, offset).split("\n").length;
|
|
56
|
+
}
|
|
57
|
+
function readmeText(root, path, sources) {
|
|
58
|
+
if (!sources.readmes.has(path)) {
|
|
59
|
+
const full = join(root, path);
|
|
60
|
+
sources.readmes.set(path, existsSync(full) ? readFileSync(full, "utf8") : undefined);
|
|
61
|
+
}
|
|
62
|
+
return sources.readmes.get(path);
|
|
63
|
+
}
|
|
64
|
+
function render(root, path, sources) {
|
|
65
|
+
const text = readFileSync(join(root, path), "utf8");
|
|
66
|
+
const readme = readmeOf(path);
|
|
67
|
+
const found = markers(text);
|
|
68
|
+
const findings = [];
|
|
69
|
+
const pieces = [];
|
|
70
|
+
let copied = 0;
|
|
71
|
+
let rewritten = 0;
|
|
72
|
+
for (let index = 0, open = found[0]; open !== undefined; open = found[++index]) {
|
|
73
|
+
const close = found[index + 1];
|
|
74
|
+
const line = lineAt(text, open.start);
|
|
75
|
+
const fail = (message, remedy) => findings.push({ file: path, line, message, remedy });
|
|
76
|
+
if (open.close || close === undefined || !close.close || close.anchor !== open.anchor) {
|
|
77
|
+
const marker = `README.md#${open.anchor}`;
|
|
78
|
+
const message = open.close
|
|
79
|
+
? `the closing marker of ${marker} has no opening marker`
|
|
80
|
+
: `the opening marker of ${marker} has no closing marker`;
|
|
81
|
+
fail(message, REMEDIES.unpaired);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
index++;
|
|
85
|
+
const part = `the part from README.md#${open.anchor}`;
|
|
86
|
+
const source = readmeText(root, readme, sources);
|
|
87
|
+
const section = source === undefined ? undefined : sectionOf(source, open.anchor);
|
|
88
|
+
if (!sources.listed().has(`${readme}#${open.anchor}`)) {
|
|
89
|
+
const remedy = `name ${readme}#${open.anchor} in a Primitive's docs, or point the part at a section they name`;
|
|
90
|
+
fail(`${part} copies a section no Primitive's docs name`, remedy);
|
|
91
|
+
}
|
|
92
|
+
else if (source === undefined) {
|
|
93
|
+
fail(`${part} reads ${readme}, which does not exist`, REMEDIES.readme);
|
|
94
|
+
}
|
|
95
|
+
else if (section === undefined) {
|
|
96
|
+
fail(`${part} names no heading of ${readme}`, REMEDIES.section);
|
|
97
|
+
}
|
|
98
|
+
else if (text.slice(open.end, close.start) !== `\n\n${section}\n\n`) {
|
|
99
|
+
fail(`${part} differs from that section of ${readme}`, REMEDIES.stale);
|
|
100
|
+
pieces.push(text.slice(copied, open.end), `\n\n${section}\n\n`);
|
|
101
|
+
copied = close.start;
|
|
102
|
+
rewritten++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const fence = openFence(text);
|
|
106
|
+
const hidden = fence === undefined ? "" : text.split("\n").slice(fence).join("\n");
|
|
107
|
+
if (fence !== undefined && hidden.search(MARKER) !== -1) {
|
|
108
|
+
const message = "a code fence opens here and never closes, so the markers after it are not read";
|
|
109
|
+
findings.push({ file: path, line: fence, message, remedy: REMEDIES.fence });
|
|
110
|
+
}
|
|
111
|
+
pieces.push(text.slice(copied));
|
|
112
|
+
return { text: pieces.join(""), findings, rewritten };
|
|
113
|
+
}
|
|
114
|
+
/** The docs entries the catalogue names, each read from the root; the catalogue is read on the first call. */
|
|
115
|
+
function listedDocs(root) {
|
|
116
|
+
let listed;
|
|
117
|
+
return () => {
|
|
118
|
+
if (listed === undefined) {
|
|
119
|
+
const file = catalogueFile(resolveConfig(root).config.gate.checks);
|
|
120
|
+
const read = readCatalogue(root, file);
|
|
121
|
+
if (read.state === "missing") {
|
|
122
|
+
throw new CannotRun({
|
|
123
|
+
file,
|
|
124
|
+
message: "the catalogue does not exist, and a skill holds a part",
|
|
125
|
+
remedy: REMEDIES.missing
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (read.state === "invalid") {
|
|
129
|
+
throw new CannotRun({
|
|
130
|
+
file,
|
|
131
|
+
message: "the catalogue is not valid, and a skill holds a part",
|
|
132
|
+
remedy: REMEDIES.invalid
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
listed = new Set(read.catalogue.primitives.flatMap(({ docs }) => docs).map((entry) => posix.join(".", entry)));
|
|
136
|
+
}
|
|
137
|
+
return listed;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function renderAll(root, { files }) {
|
|
141
|
+
const sources = { listed: listedDocs(root), readmes: new Map() };
|
|
142
|
+
return matching(root, treeFiles(root), files).map((path) => ({ path, rendered: render(root, path, sources) }));
|
|
143
|
+
}
|
|
144
|
+
function plural(count, noun) {
|
|
145
|
+
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
146
|
+
}
|
|
147
|
+
function write(root, options) {
|
|
148
|
+
const changed = renderAll(root, options).filter(({ rendered }) => rendered.rewritten > 0);
|
|
149
|
+
for (const { path, rendered } of changed) {
|
|
150
|
+
writeFileSync(join(root, path), rendered.text);
|
|
151
|
+
}
|
|
152
|
+
if (changed.length === 0) {
|
|
153
|
+
return "every part matches its section, so nothing was written";
|
|
154
|
+
}
|
|
155
|
+
const parts = changed.reduce((sum, { rendered }) => sum + rendered.rewritten, 0);
|
|
156
|
+
return `wrote ${plural(parts, "part")} in ${plural(changed.length, "file")}`;
|
|
157
|
+
}
|
|
158
|
+
export const skillSections = {
|
|
159
|
+
name: "skill-sections",
|
|
160
|
+
defaults: { files: ["**/skills/**/*.md"] },
|
|
161
|
+
run: (root, options) => renderAll(root, options).flatMap(({ rendered }) => rendered.findings),
|
|
162
|
+
write
|
|
163
|
+
};
|
package/dist/commands/check.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// `dw-sdlc check <name>`: one Check of the Gate by its name, without the Steps and the other Checks.
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { repositoryRoot } from "../agentic-config.js";
|
|
4
|
-
import {
|
|
3
|
+
import { repositoryRoot, resolveConfig } from "../agentic-config.js";
|
|
4
|
+
import { CannotRun } from "../checks/check.js";
|
|
5
|
+
import { CHECK_NAMES, shippedCheck } from "../checks/index.js";
|
|
5
6
|
import { TAIL_LINES } from "../run-command.js";
|
|
6
7
|
import { printReport, runGate } from "./gate.js";
|
|
7
8
|
export const summary = "run one Check of the Gate by its name, without the Steps and the other Checks";
|
|
8
|
-
const USAGE = `Usage: dw-sdlc check <name> [--json] [--root <dir>]
|
|
9
|
+
const USAGE = `Usage: dw-sdlc check <name> [--json] [--write] [--root <dir>]
|
|
9
10
|
|
|
10
11
|
Runs the Check named <name> in the Gate of the repository holding --root (default: the current
|
|
11
12
|
directory): a script under gate.checks.run, or a Check the package ships, one of
|
|
@@ -15,6 +16,10 @@ false or leaves it out. No Step runs. A run script prints nothing of its own unl
|
|
|
15
16
|
pass: then its last ${TAIL_LINES} lines, both streams in the order written. Prints one line per failure,
|
|
16
17
|
as the Gate does; --json prints the Gate's document alone, with no Step.
|
|
17
18
|
|
|
19
|
+
--write first writes what the Check derives from its sources, then judges the tree it wrote, and
|
|
20
|
+
prints a line on what it wrote, to stderr under --json. Only a Check that derives files takes it:
|
|
21
|
+
skill-sections writes each part of a skill from its README section.
|
|
22
|
+
|
|
18
23
|
A Ratchet's baseline is refreshed by dw-sdlc gate --update-baseline, which needs every other Check to
|
|
19
24
|
pass.
|
|
20
25
|
|
|
@@ -29,18 +34,50 @@ function parseCli(argv) {
|
|
|
29
34
|
options: {
|
|
30
35
|
help: { type: "boolean", short: "h" },
|
|
31
36
|
json: { type: "boolean" },
|
|
37
|
+
write: { type: "boolean" },
|
|
32
38
|
root: { type: "string" }
|
|
33
39
|
}
|
|
34
40
|
});
|
|
35
41
|
if (positionals.length > 1) {
|
|
36
42
|
return { error: `one Check at a time, not ${positionals.map((name) => JSON.stringify(name)).join(" and ")}` };
|
|
37
43
|
}
|
|
38
|
-
|
|
44
|
+
const flags = { help: values.help === true, json: values.json === true, write: values.write === true };
|
|
45
|
+
return { ...flags, root: values.root, name: positionals[0] };
|
|
39
46
|
}
|
|
40
47
|
catch (error) {
|
|
41
48
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
42
49
|
}
|
|
43
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Writes what the shipped Check `name` derives, on the setting the config gives it, and prints what it
|
|
53
|
+
* wrote; why it cannot when it takes no --write. An invalid config or a missing input writes nothing,
|
|
54
|
+
* and the run after it reports why.
|
|
55
|
+
*/
|
|
56
|
+
function writeDerived(root, name, json) {
|
|
57
|
+
const { config, problems } = resolveConfig(root);
|
|
58
|
+
if (problems.length > 0) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const shipped = shippedCheck(name, config.gate.checks[name]);
|
|
62
|
+
if (shipped === undefined && !config.gate.checks.run.some((check) => check.name === name)) {
|
|
63
|
+
// No Check of that name: the run after it says so, with the names there are.
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const write = shipped?.write;
|
|
67
|
+
if (write === undefined) {
|
|
68
|
+
return `${JSON.stringify(name)} takes no --write: it derives no file`;
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const out = json ? process.stderr : process.stdout;
|
|
72
|
+
out.write(`check ${name}: ${write(root)}\n`);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(error instanceof CannotRun)) {
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
44
81
|
export async function run(argv) {
|
|
45
82
|
const cli = parseCli(argv);
|
|
46
83
|
if ("error" in cli) {
|
|
@@ -56,6 +93,14 @@ export async function run(argv) {
|
|
|
56
93
|
return 2;
|
|
57
94
|
}
|
|
58
95
|
const { json, name } = cli;
|
|
59
|
-
const
|
|
96
|
+
const root = repositoryRoot(cli.root ?? process.cwd());
|
|
97
|
+
if (cli.write) {
|
|
98
|
+
const refused = writeDerived(root, name, json);
|
|
99
|
+
if (refused !== undefined) {
|
|
100
|
+
process.stderr.write(`dw-sdlc check: ${refused}\n`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const report = await runGate(root, { json, only: name });
|
|
60
105
|
return printReport(report, json, `check ${name}`);
|
|
61
106
|
}
|
package/dist/commands/doctor.js
CHANGED
|
@@ -47,7 +47,11 @@ function checkJq(probe) {
|
|
|
47
47
|
if (status === 0) {
|
|
48
48
|
return { name: "jq", level: "PASS", detail: stdout.trim() };
|
|
49
49
|
}
|
|
50
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
name: "jq",
|
|
52
|
+
level: "FAIL",
|
|
53
|
+
detail: "missing: every guard is silent without it. https://jqlang.org/download/"
|
|
54
|
+
};
|
|
51
55
|
}
|
|
52
56
|
/** Every hook command string in a settings file, whatever the event and matcher. */
|
|
53
57
|
function hookCommands(settings) {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const summary = "name the Primitives that own each path,
|
|
1
|
+
export declare const summary = "name the Primitives that own each path, or that a change removes, adds or extends";
|
|
2
2
|
export declare function run(argv: string[]): number;
|
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
// `dw-sdlc primitives
|
|
2
|
-
// with their summaries and the documents to read, or that none does
|
|
1
|
+
// `dw-sdlc primitives`: with `--paths <files>`, for each path the Primitives of the catalogue that name
|
|
2
|
+
// it, with their summaries and the documents to read, or that none does; with `--diff <base>`, the
|
|
3
|
+
// Primitives a change removes, adds or extends.
|
|
3
4
|
import { existsSync, realpathSync } from "node:fs";
|
|
4
5
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
6
|
import { parseArgs } from "node:util";
|
|
6
7
|
import { repositoryRoot, resolveConfig } from "../agentic-config.js";
|
|
7
8
|
import { primitivesNaming, readCatalogue } from "../catalogue.js";
|
|
8
9
|
import { catalogueFile } from "../checks/primitives.js";
|
|
9
|
-
|
|
10
|
+
import { blockOf, primitivesDiff } from "../primitives-diff.js";
|
|
11
|
+
export const summary = "name the Primitives that own each path, or that a change removes, adds or extends";
|
|
10
12
|
const USAGE = `Usage: dw-sdlc primitives --paths <file>... [--json] [--root <dir>]
|
|
13
|
+
dw-sdlc primitives --diff <base> [--json] [--root <dir>]
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
nearest directory above it: the id, the name, the summary and the documents to read; or that no
|
|
15
|
+
--paths prints, for each path, the Primitives of the catalogue whose code names it, the file itself or
|
|
16
|
+
the nearest directory above it: the id, the name, the summary and the documents to read; or that no
|
|
14
17
|
Primitive names it. A path is read from the current directory. The catalogue is the one the
|
|
15
18
|
primitives Check reads, in the repository holding --root (default: the current directory). --json
|
|
16
19
|
prints one document instead.
|
|
17
20
|
|
|
18
|
-
|
|
21
|
+
--diff prints the Primitives the committed change from the merge-base of <base> and HEAD to HEAD
|
|
22
|
+
removes (an id gone from the catalogue), adds (a new id) and extends (a changed file in the code of a
|
|
23
|
+
Primitive that was there): a markdown block between <!-- primitives:start --> and <!-- primitives:end -->
|
|
24
|
+
for a pull request body, which a later run replaces. --json prints instead, per Primitive, its id, name,
|
|
25
|
+
class, file count, summary and docs, each doc with whether the change touches its file, and the count of
|
|
26
|
+
changed files outside every Primitive. It takes no --paths.
|
|
27
|
+
|
|
28
|
+
Exit 0 with an answer, 2 when there is no catalogue to read or it is not one, and for --diff when <base>
|
|
29
|
+
does not resolve.`;
|
|
19
30
|
const REMEDIES = {
|
|
20
31
|
missing: "create it, or point gate.checks.primitives.file at the catalogue",
|
|
21
32
|
invalid: "fix the catalogue; dw-sdlc gate names each problem under the primitives Check"
|
|
@@ -29,6 +40,7 @@ function parseCli(argv) {
|
|
|
29
40
|
help: { type: "boolean", short: "h" },
|
|
30
41
|
json: { type: "boolean" },
|
|
31
42
|
paths: { type: "boolean" },
|
|
43
|
+
diff: { type: "string" },
|
|
32
44
|
root: { type: "string" }
|
|
33
45
|
}
|
|
34
46
|
});
|
|
@@ -36,10 +48,17 @@ function parseCli(argv) {
|
|
|
36
48
|
help: values.help === true,
|
|
37
49
|
json: values.json === true,
|
|
38
50
|
paths: values.paths === true,
|
|
51
|
+
diff: values.diff,
|
|
39
52
|
files: positionals,
|
|
40
53
|
root: values.root
|
|
41
54
|
};
|
|
42
|
-
|
|
55
|
+
if (cli.help) {
|
|
56
|
+
return cli;
|
|
57
|
+
}
|
|
58
|
+
if (cli.diff !== undefined && (cli.paths || cli.files.length > 0)) {
|
|
59
|
+
return { error: "--diff and --paths exclude each other — ask one question at a time" };
|
|
60
|
+
}
|
|
61
|
+
return cli.paths || cli.diff !== undefined ? cli : { error: "name the paths with --paths, or a base with --diff" };
|
|
43
62
|
}
|
|
44
63
|
catch (error) {
|
|
45
64
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -68,6 +87,15 @@ function plain(path, primitives) {
|
|
|
68
87
|
` read: ${docs.join(", ")}`
|
|
69
88
|
]);
|
|
70
89
|
}
|
|
90
|
+
function runDiff(root, base, json) {
|
|
91
|
+
const answer = primitivesDiff(root, base);
|
|
92
|
+
if ("problems" in answer) {
|
|
93
|
+
process.stderr.write(answer.problems.map((problem) => `dw-sdlc primitives: ${problem}\n`).join(""));
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
process.stdout.write(json ? `${JSON.stringify(answer, null, 2)}\n` : `${blockOf(answer)}\n`);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
71
99
|
export function run(argv) {
|
|
72
100
|
const cli = parseCli(argv);
|
|
73
101
|
if ("error" in cli) {
|
|
@@ -79,6 +107,9 @@ export function run(argv) {
|
|
|
79
107
|
return 0;
|
|
80
108
|
}
|
|
81
109
|
const root = repositoryRoot(cli.root ?? process.cwd());
|
|
110
|
+
if (cli.diff !== undefined) {
|
|
111
|
+
return runDiff(root, cli.diff, cli.json);
|
|
112
|
+
}
|
|
82
113
|
const file = catalogueFile(resolveConfig(root).config.gate.checks);
|
|
83
114
|
const read = readCatalogue(root, file);
|
|
84
115
|
if (read.state !== "valid") {
|
package/dist/commands/recap.js
CHANGED
|
@@ -8,6 +8,7 @@ import { decide, declaredPolicy, labelOf, lineOf } from "../pr/decide.js";
|
|
|
8
8
|
import { CannotRun, defaultBranch, now, readBranch } from "../pr/read.js";
|
|
9
9
|
import { holds, upstreamName } from "../pr/upstream.js";
|
|
10
10
|
import { READY_LABEL, readNext } from "../recap-next.js";
|
|
11
|
+
import { rollup } from "../recap-primitives.js";
|
|
11
12
|
import { isLive, Refusal, workersOf, workingFolders } from "../worker/herdr.js";
|
|
12
13
|
import { asked, git } from "../working-tree.js";
|
|
13
14
|
export const summary = "the facts a Recap rests on, whether the session can be closed and what could come next";
|
|
@@ -32,6 +33,10 @@ A pull request closed without merging merges nothing, and a detached HEAD names
|
|
|
32
33
|
commit counts as pushed only on a remote branch, and the command asks the remote which branches still
|
|
33
34
|
stand. A branch force-pushed since the last fetch can still read as holding it, so fetch first to be exact.
|
|
34
35
|
|
|
36
|
+
Beside the facts it lists the Primitives the session's branch changed, as primitives --diff classes them
|
|
37
|
+
from its merge-base with origin/<default>: ids and classes, none on the default branch or in a repository
|
|
38
|
+
with no catalogue. A catalogue it cannot read lists none, with a warning.
|
|
39
|
+
|
|
35
40
|
A linked worktree is a reason beside the verdict, never against it: the session cannot remove the
|
|
36
41
|
directory it runs in.
|
|
37
42
|
|
|
@@ -192,11 +197,11 @@ function orphanedOf(root, read, shipped, held) {
|
|
|
192
197
|
* The commits on top of the base that no pull request holds. A branch the config protects takes changes
|
|
193
198
|
* through pull requests rather than making one, and so does the default branch: neither needs one.
|
|
194
199
|
*/
|
|
195
|
-
function aheadOf(root, branch,
|
|
200
|
+
function aheadOf(root, branch, baseOf) {
|
|
196
201
|
if (resolveConfig(root).config.guardrails.protectedBranches.includes(branch)) {
|
|
197
202
|
return null;
|
|
198
203
|
}
|
|
199
|
-
const base =
|
|
204
|
+
const base = baseOf();
|
|
200
205
|
if (branch === base) {
|
|
201
206
|
return null;
|
|
202
207
|
}
|
|
@@ -285,7 +290,11 @@ function nextLine(next) {
|
|
|
285
290
|
const named = next.candidates.map(({ number, parent }) => `#${number}${parent === null ? "" : ` (sub-issue of #${parent})`}`);
|
|
286
291
|
return named.length === 0 ? "none" : named.join(", ");
|
|
287
292
|
}
|
|
288
|
-
function
|
|
293
|
+
function primitivesLine(primitives) {
|
|
294
|
+
const named = primitives.map(({ id, class: kind }) => `${kind} ${id}`);
|
|
295
|
+
return named.length === 0 ? [] : [`primitives: ${named.join(", ")}`];
|
|
296
|
+
}
|
|
297
|
+
function linesOf(local, { shipped, askedGh, workers, next }, primitives, line) {
|
|
289
298
|
const none = askedGh ? "none" : "gh not asked";
|
|
290
299
|
return [
|
|
291
300
|
`branch: ${local.branch ?? "detached HEAD"}`,
|
|
@@ -295,6 +304,7 @@ function linesOf(local, { shipped, askedGh, workers, next }, line) {
|
|
|
295
304
|
`worktree: ${local.worktree.linked ? `linked at ${local.worktree.path}` : "main checkout"}`,
|
|
296
305
|
`workers: ${workersLine(workers)}`,
|
|
297
306
|
`next: ${nextLine(next)}`,
|
|
307
|
+
...primitivesLine(primitives),
|
|
298
308
|
line
|
|
299
309
|
].join("\n");
|
|
300
310
|
}
|
|
@@ -328,7 +338,9 @@ function report(json) {
|
|
|
328
338
|
const local = standing(root, read, shipped, held);
|
|
329
339
|
// An open pull request can still take the head, and a commit-less repository has nothing to merge.
|
|
330
340
|
const nothingToMerge = held || shipped?.pr.state === "OPEN" || head === null;
|
|
331
|
-
|
|
341
|
+
let named;
|
|
342
|
+
const baseOf = () => (named ??= shipped?.pr.base ?? defaultBranch());
|
|
343
|
+
const ahead = asksGh && !nothingToMerge ? aheadOf(root, branch, baseOf) : null;
|
|
332
344
|
const facts = {
|
|
333
345
|
shipped,
|
|
334
346
|
ahead,
|
|
@@ -340,14 +352,18 @@ function report(json) {
|
|
|
340
352
|
if (facts.next !== null && "unread" in facts.next) {
|
|
341
353
|
process.stderr.write(`dw-sdlc recap: warning: the backlog was not read: ${facts.next.unread}\n`);
|
|
342
354
|
}
|
|
355
|
+
const rolled = asksGh ? rollup(root, branch, defaultBranch) : { primitives: [] };
|
|
356
|
+
if ("unread" in rolled) {
|
|
357
|
+
process.stderr.write(`dw-sdlc recap: warning: the Primitives were not read: ${rolled.unread}\n`);
|
|
358
|
+
}
|
|
343
359
|
const next = facts.next !== null && "candidates" in facts.next ? facts.next.candidates : null;
|
|
344
360
|
const close = closeOf(reasonsOf(local, facts));
|
|
345
361
|
const pullRequest = shipped === null
|
|
346
362
|
? null
|
|
347
363
|
: { url: shipped.pr.url, number: shipped.pr.number, state: shipped.pr.state, ...shipped.answer };
|
|
348
364
|
const output = json
|
|
349
|
-
? JSON.stringify({ ...local, askedGh: asksGh, pullRequest, workers: facts.workers, close, next }, null, 2)
|
|
350
|
-
: linesOf(local, facts, close.line);
|
|
365
|
+
? JSON.stringify({ ...local, askedGh: asksGh, pullRequest, workers: facts.workers, primitives: rolled.primitives, close, next }, null, 2)
|
|
366
|
+
: linesOf(local, facts, rolled.primitives, close.line);
|
|
351
367
|
process.stdout.write(`${output}\n`);
|
|
352
368
|
return close.verdict === "yes" ? 0 : 1;
|
|
353
369
|
}
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export type SetupOptions = {
|
|
|
27
27
|
export declare const summary = "adopt the Gate where asked, and write this machine's local config layer, the git excludes and the permission rules";
|
|
28
28
|
/**
|
|
29
29
|
* What the machine's layer points `$schema` at: the marketplace clone's schema as a `file://` URL, as an
|
|
30
|
-
* editor
|
|
31
|
-
*
|
|
30
|
+
* editor reads it offline at the installed version and the clone, unlike the install cache, keeps its
|
|
31
|
+
* path across plugin updates. Without a registered clone holding the schema, the `$id`.
|
|
32
32
|
*/
|
|
33
33
|
export declare function schemaReference(claudeDir: string): string;
|
|
34
34
|
export declare function setup(options: SetupOptions): Step[];
|
package/dist/commands/setup.js
CHANGED
|
@@ -69,8 +69,8 @@ function committedConfig(root) {
|
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
71
|
* What the machine's layer points `$schema` at: the marketplace clone's schema as a `file://` URL, as an
|
|
72
|
-
* editor
|
|
73
|
-
*
|
|
72
|
+
* editor reads it offline at the installed version and the clone, unlike the install cache, keeps its
|
|
73
|
+
* path across plugin updates. Without a registered clone holding the schema, the `$id`.
|
|
74
74
|
*/
|
|
75
75
|
export function schemaReference(claudeDir) {
|
|
76
76
|
const registry = readJson(join(claudeDir, "plugins", "known_marketplaces.json"));
|
package/dist/commands/sweep.js
CHANGED
|
@@ -9,7 +9,7 @@ import { isCommitLanded, isLanded } from "../landed.js";
|
|
|
9
9
|
import { shellLine } from "../pr/decide.js";
|
|
10
10
|
import { CannotRun } from "../pr/read.js";
|
|
11
11
|
import { byHand, holdersOf, keptIn, lockAfterStop, lockHolder, outsideWorktrees, readTerminals, removeTerminal, stopsOf, sweepingTerminal, whyHeld } from "../sweep-terminals.js";
|
|
12
|
-
import {
|
|
12
|
+
import { branchesOf, dirtyFiles, pausedOperation, switchOff, worktreesOf } from "../sweep-trees.js";
|
|
13
13
|
import { asked, git } from "../working-tree.js";
|
|
14
14
|
import { lockVerdict } from "../worktree-lock.js";
|
|
15
15
|
export const summary = "removes Landed branches and clean worktrees whose work has landed, names what it keeps";
|
|
@@ -26,7 +26,8 @@ config, else the branch origin/HEAD names) and removes, never forcing:
|
|
|
26
26
|
It keeps, and names with the reason and the command to run yourself:
|
|
27
27
|
a worktree that fails the set: the files that hold it with the forced removal, the paused operation, the
|
|
28
28
|
lock's reason with git worktree unlock, a live Claude Code lock named by its pid, a HEAD that is not Landed;
|
|
29
|
-
the default branch, the base branch, a protected branch and a branch a kept worktree has checked out;
|
|
29
|
+
the default branch, the base branch, a protected branch and a branch a kept worktree has checked out; a
|
|
30
|
+
Landed branch the main checkout has checked out, clean and with nothing paused, with git switch to the base;
|
|
30
31
|
a branch whose work is on no commit of the base, with the forced delete.
|
|
31
32
|
It never changes anything on a remote: the fetch only writes remote-tracking refs. A removal git refuses is
|
|
32
33
|
kept with git's words, and the sweep goes on.
|
|
@@ -91,11 +92,6 @@ function baseOf(root, head) {
|
|
|
91
92
|
}
|
|
92
93
|
return { name, ref };
|
|
93
94
|
}
|
|
94
|
-
function branchesOf(root) {
|
|
95
|
-
return must(root, ["for-each-ref", "--format=%(refname:lstrip=2)", "refs/heads"])
|
|
96
|
-
.split("\n")
|
|
97
|
-
.filter((name) => name !== "");
|
|
98
|
-
}
|
|
99
95
|
/** Why the worktree stays, with the dev's command when there is one, or null when its whole set passes. */
|
|
100
96
|
function whyTreeStays(root, baseRef, tree, lock, holders) {
|
|
101
97
|
const { path } = tree;
|
|
@@ -194,8 +190,9 @@ function judge(root, terminals) {
|
|
|
194
190
|
}
|
|
195
191
|
judged.add(name);
|
|
196
192
|
const why = stays(name, footing);
|
|
193
|
+
const off = why?.dev === true && held.get(name) === checkout.path ? switchOff(root, base, name) : null;
|
|
197
194
|
if (why !== null) {
|
|
198
|
-
kept.push({ kind: "branch", name, ...why });
|
|
195
|
+
kept.push({ kind: "branch", name, ...why, ...off });
|
|
199
196
|
}
|
|
200
197
|
else if (isLanded(root, base.ref, name)) {
|
|
201
198
|
removed.push({ kind: "branch", name, reason: `Landed on ${baseName}` });
|
package/dist/markdown.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export type Link = {
|
|
|
12
12
|
target: string;
|
|
13
13
|
line: number;
|
|
14
14
|
};
|
|
15
|
+
/** The 1-based line of the code fence `text` leaves open at its end; none when every fence closes. */
|
|
16
|
+
export declare function openFence(text: string): number | undefined;
|
|
15
17
|
/** Every ATX heading in `text`, a repeated anchor numbered `-1`, `-2` as GitHub numbers it. */
|
|
16
18
|
export declare function headings(text: string): Heading[];
|
|
17
19
|
/**
|
package/dist/markdown.js
CHANGED
|
@@ -13,15 +13,24 @@ function scan(text) {
|
|
|
13
13
|
const marker = FENCE.exec(line)?.[1];
|
|
14
14
|
if (fence === undefined && marker !== undefined) {
|
|
15
15
|
fence = marker;
|
|
16
|
-
return { text: line, line: index + 1, fenced: true };
|
|
16
|
+
return { text: line, line: index + 1, fenced: true, edge: "open" };
|
|
17
17
|
}
|
|
18
18
|
if (fence !== undefined && marker !== undefined && marker[0] === fence[0] && marker.length >= fence.length) {
|
|
19
19
|
fence = undefined;
|
|
20
|
-
return { text: line, line: index + 1, fenced: true };
|
|
20
|
+
return { text: line, line: index + 1, fenced: true, edge: "close" };
|
|
21
21
|
}
|
|
22
22
|
return { text: line, line: index + 1, fenced: fence !== undefined };
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
|
+
/** The 1-based line of the code fence `text` leaves open at its end; none when every fence closes. */
|
|
26
|
+
export function openFence(text) {
|
|
27
|
+
const lines = scan(text);
|
|
28
|
+
const last = lines.at(-1);
|
|
29
|
+
if (last === undefined || !last.fenced || last.edge === "close") {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return lines.findLast(({ edge }) => edge === "open")?.line;
|
|
33
|
+
}
|
|
25
34
|
/**
|
|
26
35
|
* GitHub's anchor for a heading: its text as rendered, lowercased, every character but a letter, a
|
|
27
36
|
* digit, a space, `-` and `_` dropped, each space a hyphen.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PrimitiveClass = "removes" | "adds" | "extends";
|
|
2
|
+
export type Change = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
class: PrimitiveClass;
|
|
6
|
+
/** The changed files the Primitive owns. */
|
|
7
|
+
files: number;
|
|
8
|
+
summary: string;
|
|
9
|
+
/** Each `docs` entry, and whether the diff changes its file, whatever its anchor. */
|
|
10
|
+
docs: {
|
|
11
|
+
entry: string;
|
|
12
|
+
touched: boolean;
|
|
13
|
+
}[];
|
|
14
|
+
};
|
|
15
|
+
export type PrimitivesDiff = {
|
|
16
|
+
base: string;
|
|
17
|
+
primitives: Change[];
|
|
18
|
+
outside: number;
|
|
19
|
+
};
|
|
20
|
+
/** Why a diff cannot be answered: each problem with its remedy. */
|
|
21
|
+
export type Refusal = {
|
|
22
|
+
problems: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The Primitives `base...HEAD` removes, adds and extends. The catalogue's path comes from the config of
|
|
26
|
+
* the checkout; the catalogue is read at `HEAD` and at the merge-base, from the commits and not the tree.
|
|
27
|
+
* A file's owners are the nearest entries of the catalogue it is read against, as `--paths` answers.
|
|
28
|
+
*/
|
|
29
|
+
export declare function primitivesDiff(root: string, base: string): PrimitivesDiff | Refusal;
|
|
30
|
+
/** The markdown block between its markers, which a later run replaces whole. */
|
|
31
|
+
export declare function blockOf({ primitives, outside }: PrimitivesDiff): string;
|