dw-sdlc 0.1.0 → 0.2.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/config/agentic.config.schema.json +163 -0
- package/dist/agentic-config.js +3 -5
- package/dist/checks/agents-map.js +3 -10
- package/dist/checks/check.d.ts +10 -0
- package/dist/checks/comment-budget.d.ts +6 -0
- package/dist/checks/comment-budget.js +141 -0
- package/dist/checks/decision-records.d.ts +6 -0
- package/dist/checks/decision-records.js +105 -0
- package/dist/checks/file-size-ratchet.d.ts +10 -0
- package/dist/checks/file-size-ratchet.js +194 -0
- package/dist/checks/index.d.ts +5 -1
- package/dist/checks/index.js +25 -2
- package/dist/checks/markdown-budget.d.ts +15 -0
- package/dist/checks/markdown-budget.js +42 -0
- package/dist/checks/markdown-refs.d.ts +6 -0
- package/dist/checks/markdown-refs.js +73 -0
- package/dist/checks/primitives.js +5 -30
- package/dist/checks/temporal-language.d.ts +6 -0
- package/dist/checks/temporal-language.js +58 -0
- package/dist/checks/tree.d.ts +8 -0
- package/dist/checks/tree.js +30 -0
- package/dist/commands/doctor.js +7 -7
- package/dist/commands/gate.js +73 -17
- package/dist/commands/references-setup.js +4 -15
- package/dist/commands/references-sync.js +3 -9
- package/dist/commands/setup.d.ts +3 -4
- package/dist/commands/setup.js +22 -39
- package/dist/json-schema.js +3 -4
- package/dist/markdown.d.ts +12 -0
- package/dist/markdown.js +40 -2
- package/dist/pins.js +2 -3
- package/dist/references-key.js +1 -2
- package/dist/router-block.js +1 -3
- package/package.json +1 -1
package/dist/commands/gate.js
CHANGED
|
@@ -6,7 +6,7 @@ import { CannotRun } from "../checks/check.js";
|
|
|
6
6
|
import { enabledChecks } from "../checks/index.js";
|
|
7
7
|
import { annotation, failureJson, failureLine } from "../report.js";
|
|
8
8
|
export const summary = "run the Gate: the Steps of gate.steps in order, then every Check under gate.checks";
|
|
9
|
-
const USAGE = `Usage: dw-sdlc gate [--json] [--root <dir>]
|
|
9
|
+
const USAGE = `Usage: dw-sdlc gate [--json] [--root <dir>] [--update-baseline]
|
|
10
10
|
|
|
11
11
|
Runs the Gate of the repository holding --root (default: the current directory): each Step of
|
|
12
12
|
gate.steps in order from the repository root, its output passed through, stopping at the first that
|
|
@@ -15,6 +15,10 @@ none stopping the next. Prints one line per failure: the file where there is one
|
|
|
15
15
|
remedy; under GitHub Actions each line is also a workflow annotation. --json prints one document
|
|
16
16
|
instead and sends the commands' own output to stderr.
|
|
17
17
|
|
|
18
|
+
--update-baseline then rewrites the baseline of each Ratchet, only when every Step and every other
|
|
19
|
+
Check passed, and only tighter: a file now within its limit leaves it and no file joins. Where there
|
|
20
|
+
is no baseline, it records one. The Ratchet is judged again on what it wrote.
|
|
21
|
+
|
|
18
22
|
Exit 0 when everything passes, 1 when a Step or a Check fails, 2 when the Gate cannot run: an
|
|
19
23
|
invalid config layer, a Step or Check whose command the shell cannot find or execute, or a Check
|
|
20
24
|
missing an input it reads.`;
|
|
@@ -36,9 +40,19 @@ function parseCli(argv) {
|
|
|
36
40
|
try {
|
|
37
41
|
const { values } = parseArgs({
|
|
38
42
|
args: argv,
|
|
39
|
-
options: {
|
|
43
|
+
options: {
|
|
44
|
+
help: { type: "boolean", short: "h" },
|
|
45
|
+
json: { type: "boolean" },
|
|
46
|
+
root: { type: "string" },
|
|
47
|
+
"update-baseline": { type: "boolean" }
|
|
48
|
+
}
|
|
40
49
|
});
|
|
41
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
help: values.help === true,
|
|
52
|
+
json: values.json === true,
|
|
53
|
+
root: values.root,
|
|
54
|
+
updateBaseline: values["update-baseline"] === true
|
|
55
|
+
};
|
|
42
56
|
}
|
|
43
57
|
catch (error) {
|
|
44
58
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -69,28 +83,66 @@ function runCommand(command, root, json) {
|
|
|
69
83
|
}
|
|
70
84
|
return { status: "fail", exitCode: result.status, detail: `exited ${result.status}` };
|
|
71
85
|
}
|
|
86
|
+
/** What a Check that threw reports: the input it lacks, or what stopped it. */
|
|
87
|
+
function erred(check, error) {
|
|
88
|
+
const finding = error instanceof CannotRun
|
|
89
|
+
? error.finding
|
|
90
|
+
: { message: `stopped: ${error instanceof Error ? error.message : String(error)}`, remedy: REMEDIES.checkStopped };
|
|
91
|
+
return {
|
|
92
|
+
check,
|
|
93
|
+
outcome: { status: "error", exitCode: EXIT_CODES.error },
|
|
94
|
+
failures: [{ rule: check.name, ...finding }]
|
|
95
|
+
};
|
|
96
|
+
}
|
|
72
97
|
/** Runs one Check the package ships, its failures named by the Check; one it cannot judge is an error. */
|
|
73
98
|
function runShipped(check, root) {
|
|
74
99
|
try {
|
|
75
100
|
const failures = check.run(root).map((finding) => ({ rule: check.name, ...finding }));
|
|
76
101
|
const status = failures.length === 0 ? "pass" : "fail";
|
|
77
|
-
return { status, exitCode: EXIT_CODES[status], failures };
|
|
102
|
+
return { check, outcome: { status, exitCode: EXIT_CODES[status] }, failures };
|
|
78
103
|
}
|
|
79
104
|
catch (error) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
return erred(check, error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Rewrites the baseline of each Ratchet while every Step, every run Check in `others` and every other
|
|
110
|
+
* shipped Check passed, and judges the Ratchet again on what it wrote.
|
|
111
|
+
*/
|
|
112
|
+
function refreshBaselines(judged, others, root, log) {
|
|
113
|
+
if (!judged.some(({ check }) => check.baseline !== undefined)) {
|
|
114
|
+
log.write("gate: no Ratchet is turned on, so no baseline to refresh\n");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
for (const [index, { check }] of judged.entries()) {
|
|
118
|
+
if (check.baseline === undefined) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const { file, refresh } = check.baseline;
|
|
122
|
+
const rest = [...others, ...judged.filter((_, at) => at !== index).map(({ outcome }) => outcome)];
|
|
123
|
+
if (rest.some(({ status }) => status !== "pass")) {
|
|
124
|
+
log.write(`gate: baseline ${file} not refreshed — a Step or another Check did not pass\n`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
log.write(`gate: baseline ${file} ${refresh(root)}\n`);
|
|
129
|
+
judged[index] = runShipped(check, root);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
if (error instanceof CannotRun) {
|
|
133
|
+
log.write(`gate: baseline ${file} not refreshed — ${error.finding.message}; ${error.finding.remedy}\n`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
judged[index] = erred(check, error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
87
139
|
}
|
|
88
140
|
}
|
|
89
141
|
function worst(outcomes) {
|
|
90
142
|
const statuses = new Set(outcomes.map(({ status }) => status));
|
|
91
143
|
return statuses.has("error") ? "error" : statuses.has("fail") ? "fail" : "pass";
|
|
92
144
|
}
|
|
93
|
-
function runGate(root, json) {
|
|
145
|
+
function runGate(root, json, updateBaseline) {
|
|
94
146
|
const log = json ? process.stderr : process.stdout;
|
|
95
147
|
const { config, problems } = resolveConfig(root);
|
|
96
148
|
const gateCommand = config.gate.command ?? null;
|
|
@@ -128,13 +180,17 @@ function runGate(root, json) {
|
|
|
128
180
|
log.write(`gate: check ${check.name}\n`);
|
|
129
181
|
checks.push({ ...check, ...runOne(check.command, check.name, REMEDIES.checkNotRunnable) });
|
|
130
182
|
}
|
|
131
|
-
|
|
183
|
+
const judged = enabledChecks(config.gate.checks).map((check) => {
|
|
132
184
|
if (stopped) {
|
|
133
|
-
|
|
134
|
-
continue;
|
|
185
|
+
return { check, outcome: SKIPPED, failures: [] };
|
|
135
186
|
}
|
|
136
187
|
log.write(`gate: check ${check.name}\n`);
|
|
137
|
-
|
|
188
|
+
return runShipped(check, root);
|
|
189
|
+
});
|
|
190
|
+
if (updateBaseline) {
|
|
191
|
+
refreshBaselines(judged, [...steps, ...checks], root, log);
|
|
192
|
+
}
|
|
193
|
+
for (const { check, outcome, failures: found } of judged) {
|
|
138
194
|
failures.push(...found);
|
|
139
195
|
checks.push({ name: check.name, command: null, ...outcome });
|
|
140
196
|
}
|
|
@@ -157,7 +213,7 @@ export function run(argv) {
|
|
|
157
213
|
process.stdout.write(`${USAGE}\n`);
|
|
158
214
|
return 0;
|
|
159
215
|
}
|
|
160
|
-
const report = runGate(repositoryRoot(cli.root ?? process.cwd()), cli.json);
|
|
216
|
+
const report = runGate(repositoryRoot(cli.root ?? process.cwd()), cli.json, cli.updateBaseline);
|
|
161
217
|
const actions = process.env.GITHUB_ACTIONS === "true";
|
|
162
218
|
if (cli.json) {
|
|
163
219
|
const document = { ...report, failures: report.failures.map(failureJson) };
|
|
@@ -1,18 +1,6 @@
|
|
|
1
|
-
// `dw-sdlc references setup` sets a repository up to declare References
|
|
2
|
-
// when it would change
|
|
3
|
-
//
|
|
4
|
-
// references/AGENTS.md the Router, from templates/router.md when there is none; on every run the
|
|
5
|
-
// block between its two markers, a table of each Reference's dir and when
|
|
6
|
-
// from the key, while the text around the block stays the author's
|
|
7
|
-
// references/CLAUDE.md a symlink to AGENTS.md, so Claude Code loads the Router; written once
|
|
8
|
-
// .claude/settings.json claudeMdExcludes for the clones' own CLAUDE.md and AGENTS.md
|
|
9
|
-
// .gitignore references/* and the two exceptions that keep the Router tracked
|
|
10
|
-
// .ai/agentic.config.json an empty references key when the config has none
|
|
11
|
-
//
|
|
12
|
-
// Existing content stays: a list gains the entries it lacks at its end, and nothing is removed or
|
|
13
|
-
// reordered. A settings file or a config that is not a JSON object is refused, not rewritten, and so
|
|
14
|
-
// is an ignore file that ignores references/ itself and a Router with one marker and not the other,
|
|
15
|
-
// or whose key fails the parser. --check writes nothing and exits 1 when any file would change.
|
|
1
|
+
// `dw-sdlc references setup` sets a repository up to declare References: the five files its USAGE names,
|
|
2
|
+
// each written only when it would change. Existing content stays: a list gains the entries it lacks at
|
|
3
|
+
// its end, nothing is removed or reordered, and a file the step cannot read as it expects is refused.
|
|
16
4
|
import { spawnSync } from "node:child_process";
|
|
17
5
|
import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
18
6
|
import { dirname, join } from "node:path";
|
|
@@ -20,6 +8,7 @@ import { parseArgs } from "node:util";
|
|
|
20
8
|
import { CONFIG, isObject, readReferences } from "../references-key.js";
|
|
21
9
|
import { renderBlock, withBlock } from "../router-block.js";
|
|
22
10
|
const ROUTER = "references/AGENTS.md";
|
|
11
|
+
/** A symlink to the Router, so Claude Code loads it. */
|
|
23
12
|
const ROUTER_LINK = "references/CLAUDE.md";
|
|
24
13
|
const SETTINGS = ".claude/settings.json";
|
|
25
14
|
const IGNORE = ".gitignore";
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
// `dw-sdlc references sync`, the Sync:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// Each clone is shallow and blobless, detached at what was fetched, and sparse when its entry names
|
|
5
|
-
// only some paths or skips some. Only the committed config is read. A key the parser refuses syncs
|
|
6
|
-
// nothing; an entry that fails to resolve fails alone. One Sync runs in a checkout at a time: a
|
|
7
|
-
// second one waits on the lock under references/. When the Router or its table is missing or behind
|
|
8
|
-
// the key, one line names the setup to run. A repository that declares no References gets no output
|
|
9
|
-
// at all.
|
|
1
|
+
// `dw-sdlc references sync`, the Sync: every Reference the committed config declares brought to its ref
|
|
2
|
+
// under references/<dir>, each clone shallow, blobless, detached and sparse where its entry says. An
|
|
3
|
+
// entry that fails fails alone, and a repository that declares no References gets no output at all.
|
|
10
4
|
import { spawnSync } from "node:child_process";
|
|
11
5
|
import { existsSync, linkSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
12
6
|
import { extname, join } from "node:path";
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -21,10 +21,9 @@ export type SetupOptions = {
|
|
|
21
21
|
};
|
|
22
22
|
export declare const summary = "write this machine's local config layer, the git excludes and the permission rules";
|
|
23
23
|
/**
|
|
24
|
-
* What the machine's layer points `$schema` at: the
|
|
25
|
-
*
|
|
26
|
-
* across plugin updates
|
|
27
|
-
* holding the schema falls back to the `$id`.
|
|
24
|
+
* What the machine's layer points `$schema` at: the marketplace clone's schema as a `file://` URL, as an
|
|
25
|
+
* editor cannot fetch the `$id` from a private repository and the clone, unlike the install cache, keeps
|
|
26
|
+
* its path across plugin updates. Without a registered clone holding the schema, the `$id`.
|
|
28
27
|
*/
|
|
29
28
|
export declare function schemaReference(claudeDir: string): string;
|
|
30
29
|
export declare function setup(options: SetupOptions): Step[];
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,28 +1,6 @@
|
|
|
1
|
-
// `dw-sdlc setup` sets a repository and this machine up for the guardrails plugin
|
|
2
|
-
// written only when its content would change, so a
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// .ai/agentic.config.local.json the machine's layer, from inference: the Node package manager when
|
|
6
|
-
// the committed config leaves it unset, $schema at the marketplace
|
|
7
|
-
// clone's schema, commit.convention when --convention names one
|
|
8
|
-
// or the file already pins one, and the guardrails block the file
|
|
9
|
-
// holds, as it is. Refused when one exists with other content than
|
|
10
|
-
// those, unless --force.
|
|
11
|
-
// .git/info/exclude keeps that file and .claude/settings.local.json out of git without
|
|
12
|
-
// touching the repository's tracked .gitignore
|
|
13
|
-
// .claude/settings.local.json permissions.deny as the jq-less backstop for the hooks: the
|
|
14
|
-
// destructive git and gh sets in plain and rtk form, and Read/Edit
|
|
15
|
-
// rules for every env file the resolved guardrails.envFiles refuses:
|
|
16
|
-
// listed in deny, or ignored by git and not listed in allow
|
|
17
|
-
// ~/.claude/settings.json autoMode.allow plus the lease-push exception ("$defaults" opens a
|
|
18
|
-
// list this run creates), and permissions.deny with the irreversible set;
|
|
19
|
-
// under $CLAUDE_CONFIG_DIR when it is set, as Claude Code reads it
|
|
20
|
-
//
|
|
21
|
-
// Existing content is merged, never replaced: lists grow by the entries they lack, the repository's
|
|
22
|
-
// deny list loses the rules RETIRED_DENY names, other keys stay. A settings file that is not JSON is
|
|
23
|
-
// refused, not rewritten. From a linked worktree all three repository files go to the main checkout:
|
|
24
|
-
// every worktree reads the local config from there when it has none of its own, and Claude Code reads
|
|
25
|
-
// the local settings there. The only thing run in the repository is read-only git.
|
|
1
|
+
// `dw-sdlc setup` sets a repository and this machine up for the guardrails plugin: the four files its USAGE
|
|
2
|
+
// names, each written only when its content would change, so a run is safe to repeat. Existing content is
|
|
3
|
+
// merged, never replaced: other keys stay, and a settings file that is not JSON is refused.
|
|
26
4
|
import { spawnSync } from "node:child_process";
|
|
27
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
6
|
import { homedir } from "node:os";
|
|
@@ -63,6 +41,7 @@ const PACKAGE_LINK = "dw-sdlc";
|
|
|
63
41
|
function isObject(value) {
|
|
64
42
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
65
43
|
}
|
|
44
|
+
/** A read-only git command, the one thing the setup runs in the repository. */
|
|
66
45
|
function git(root, args) {
|
|
67
46
|
const result = spawnSync("git", ["-C", root, ...args], { encoding: "utf8" });
|
|
68
47
|
return result.status === 0 ? result.stdout.trim() : undefined;
|
|
@@ -77,10 +56,9 @@ function committedConfig(root) {
|
|
|
77
56
|
return "value" in result ? result.value : {};
|
|
78
57
|
}
|
|
79
58
|
/**
|
|
80
|
-
* What the machine's layer points `$schema` at: the
|
|
81
|
-
*
|
|
82
|
-
* across plugin updates
|
|
83
|
-
* holding the schema falls back to the `$id`.
|
|
59
|
+
* What the machine's layer points `$schema` at: the marketplace clone's schema as a `file://` URL, as an
|
|
60
|
+
* editor cannot fetch the `$id` from a private repository and the clone, unlike the install cache, keeps
|
|
61
|
+
* its path across plugin updates. Without a registered clone holding the schema, the `$id`.
|
|
84
62
|
*/
|
|
85
63
|
export function schemaReference(claudeDir) {
|
|
86
64
|
const registry = readJson(join(claudeDir, "plugins", "known_marketplaces.json"));
|
|
@@ -157,12 +135,8 @@ function sameContent(existing, config) {
|
|
|
157
135
|
return isObject(existing) && render(unowned(existing)) === render(unowned(config));
|
|
158
136
|
}
|
|
159
137
|
/**
|
|
160
|
-
* The machine's layer: what inference finds, minus every key the committed config already sets,
|
|
161
|
-
* the `guardrails` block the file
|
|
162
|
-
* inferred key written here would override it. The commit convention is written only when one is
|
|
163
|
-
* asked for, by `--convention` or by the pin the file already holds: inference always gives the
|
|
164
|
-
* `conventional` default, and pinning it here overrides a convention the repository commits later,
|
|
165
|
-
* so that is a choice, never a guess.
|
|
138
|
+
* The machine's layer: what inference finds, minus every key the committed config already sets, which an
|
|
139
|
+
* inferred key here would override, plus the `guardrails` block the file holds and the convention asked for.
|
|
166
140
|
*/
|
|
167
141
|
function localConfig(root, claudeDir, convention, guardrails) {
|
|
168
142
|
const inferred = inferConfig(root);
|
|
@@ -178,11 +152,14 @@ function localConfig(root, claudeDir, convention, guardrails) {
|
|
|
178
152
|
if (guardrails !== undefined) {
|
|
179
153
|
config.guardrails = guardrails;
|
|
180
154
|
}
|
|
155
|
+
// Only one asked for, by `--convention` or the file's pin: inference always gives `conventional`, and
|
|
156
|
+
// pinning it here would override a convention the repository commits later.
|
|
181
157
|
if (convention !== undefined) {
|
|
182
158
|
config.commit = { convention };
|
|
183
159
|
}
|
|
184
160
|
return config;
|
|
185
161
|
}
|
|
162
|
+
/** `existing` with the entries of `additions` it lacks appended: a list grows and never loses one. */
|
|
186
163
|
function union(existing, additions) {
|
|
187
164
|
const list = Array.isArray(existing) ? existing.filter((item) => typeof item === "string") : [];
|
|
188
165
|
for (const rule of additions) {
|
|
@@ -234,7 +211,10 @@ class Writer {
|
|
|
234
211
|
this.steps.push({ file: label, outcome: "refused", detail });
|
|
235
212
|
}
|
|
236
213
|
}
|
|
237
|
-
/**
|
|
214
|
+
/**
|
|
215
|
+
* Where the repository's files live: the main checkout for a linked worktree, since every worktree reads
|
|
216
|
+
* the local config from there when it has none of its own, and Claude Code reads the local settings there.
|
|
217
|
+
*/
|
|
238
218
|
function mainCheckout(root) {
|
|
239
219
|
const common = git(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
|
|
240
220
|
if (common === undefined) {
|
|
@@ -266,7 +246,8 @@ export function setup(options) {
|
|
|
266
246
|
writer.put(LOCAL_FILE, localPath, render(local));
|
|
267
247
|
}
|
|
268
248
|
}
|
|
269
|
-
// 2. The exclude entries, appended to whatever the file holds
|
|
249
|
+
// 2. The exclude entries, appended to whatever the file holds: they keep both local files out of git
|
|
250
|
+
// without touching the repository's tracked .gitignore.
|
|
270
251
|
const excludePath = join(gitDir, "info", "exclude");
|
|
271
252
|
const excludeText = existsSync(excludePath) ? readFileSync(excludePath, "utf8") : "";
|
|
272
253
|
const lines = excludeText.split("\n");
|
|
@@ -279,7 +260,8 @@ export function setup(options) {
|
|
|
279
260
|
const separator = excludeText === "" || excludeText.endsWith("\n") ? "" : "\n";
|
|
280
261
|
writer.put(excludeLabel, excludePath, excludeText + separator + missing.join("\n") + "\n");
|
|
281
262
|
}
|
|
282
|
-
// 3. The repository's deny rules, merged into the main
|
|
263
|
+
// 3. The repository's deny rules, the backstop for the hooks where jq is missing, merged into the main
|
|
264
|
+
// checkout's local settings.
|
|
283
265
|
const localSettingsPath = join(checkout, ".claude", "settings.local.json");
|
|
284
266
|
const localSettings = readJson(localSettingsPath);
|
|
285
267
|
if (localSettings === "invalid") {
|
|
@@ -292,7 +274,8 @@ export function setup(options) {
|
|
|
292
274
|
settings.permissions = { ...permissions, deny };
|
|
293
275
|
writer.put(".claude/settings.local.json", localSettingsPath, render(settings));
|
|
294
276
|
}
|
|
295
|
-
// 4. The user file: the lease exception, "$defaults" kept, and
|
|
277
|
+
// 4. The user file, under $CLAUDE_CONFIG_DIR when it is set: the lease exception, "$defaults" kept, and
|
|
278
|
+
// the irreversible non-git set.
|
|
296
279
|
const userPath = join(claudeDir, "settings.json");
|
|
297
280
|
const userLabel = options.claudeDir === undefined ? "~/.claude/settings.json" : userPath;
|
|
298
281
|
const userSettings = readJson(userPath);
|
package/dist/json-schema.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
// The subset of JSON Schema draft 2020-12 the shipped schemas use, validated without a dependency: Node
|
|
2
2
|
// has no validator built in, and a config file does not justify pulling one in. A keyword outside the
|
|
3
3
|
// subset throws instead of passing silently, so a schema never promises more than this file enforces.
|
|
4
|
-
// `format` is enforced, not annotative: `regex` is the one format, compiled with `RegExp` and refused
|
|
5
|
-
// when it holds a construct POSIX ERE lacks, because `grep -E` runs the pattern and BSD and GNU grep
|
|
6
|
-
// read those differently. `enum` and `const` compare by JSON text, exact for the scalars and arrays
|
|
7
|
-
// the schemas use.
|
|
8
4
|
export class SchemaError extends Error {
|
|
9
5
|
}
|
|
10
6
|
const ANNOTATIONS = new Set(["$schema", "$id", "$comment", "title", "description", "default", "examples"]);
|
|
@@ -14,6 +10,7 @@ function isObject(value) {
|
|
|
14
10
|
function show(value) {
|
|
15
11
|
return value === undefined ? "undefined" : JSON.stringify(value);
|
|
16
12
|
}
|
|
13
|
+
/** Equal by JSON text, as `enum` and `const` compare: exact for the scalars and arrays the schemas use. */
|
|
17
14
|
function sameJson(a, b) {
|
|
18
15
|
return JSON.stringify(a) === JSON.stringify(b);
|
|
19
16
|
}
|
|
@@ -194,6 +191,8 @@ function checkKeyword(schema, keyword, operand, value, path, problems) {
|
|
|
194
191
|
}
|
|
195
192
|
return;
|
|
196
193
|
}
|
|
194
|
+
// Enforced, not annotative: `regex` compiles with `RegExp` and refuses a construct POSIX ERE lacks,
|
|
195
|
+
// because `grep -E` runs the pattern and BSD and GNU grep read those differently.
|
|
197
196
|
case "format": {
|
|
198
197
|
operandMust(operand === "regex", path, keyword, '"regex", the one format enforced here');
|
|
199
198
|
if (typeof value !== "string") {
|
package/dist/markdown.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export type Row = {
|
|
|
8
8
|
cells: string[];
|
|
9
9
|
line: number;
|
|
10
10
|
};
|
|
11
|
+
export type Link = {
|
|
12
|
+
target: string;
|
|
13
|
+
line: number;
|
|
14
|
+
};
|
|
11
15
|
/** Every ATX heading in `text`, a repeated anchor numbered `-1`, `-2` as GitHub numbers it. */
|
|
12
16
|
export declare function headings(text: string): Heading[];
|
|
13
17
|
/**
|
|
@@ -17,3 +21,11 @@ export declare function headings(text: string): Heading[];
|
|
|
17
21
|
export declare function tableRows(text: string, title: string): Row[];
|
|
18
22
|
/** Every backticked span in `text`, without its backticks. */
|
|
19
23
|
export declare function codeSpans(text: string): string[];
|
|
24
|
+
/** `text` with each fenced line and each code span blanked to spaces, so every offset and line is kept. */
|
|
25
|
+
export declare function prose(text: string): string;
|
|
26
|
+
/** Every link target in the prose of `text`, its angle brackets dropped, with its 1-based line. */
|
|
27
|
+
export declare function links(text: string): Link[];
|
|
28
|
+
/** The anchors of each markdown file read so far, by path. */
|
|
29
|
+
export type Anchors = Map<string, Set<string>>;
|
|
30
|
+
/** The anchors of the headings of `path` under `root`; none when it is not a file. */
|
|
31
|
+
export declare function anchorsOf(root: string, path: string, cache: Anchors): Set<string>;
|
package/dist/markdown.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// The parts of a markdown file the Checks read: its ATX headings with the anchors GitHub gives them,
|
|
2
|
-
//
|
|
3
|
-
|
|
2
|
+
// the rows of a table under a heading, its links, and its prose with the code masked; and the anchors
|
|
3
|
+
// of a file on disk. A line inside a fenced code block is never any of them.
|
|
4
|
+
import { readFileSync, statSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
/** A code fence at any indent, so one inside a list item counts. */
|
|
7
|
+
const FENCE = /^[ \t]*(`{3,}|~{3,})/;
|
|
4
8
|
const ATX = /^ {0,3}(#{1,6})[ \t]+(.*?)(?:[ \t]+#+)?[ \t]*$/;
|
|
5
9
|
/** Each line with its 1-based number, `fenced` when a code fence opens, closes or encloses it. */
|
|
6
10
|
function scan(text) {
|
|
@@ -75,3 +79,37 @@ export function tableRows(text, title) {
|
|
|
75
79
|
export function codeSpans(text) {
|
|
76
80
|
return Array.from(text.matchAll(/`([^`]+)`/g), ([, span = ""]) => span);
|
|
77
81
|
}
|
|
82
|
+
/** A code span: a run of backticks, then the text up to the next run of as many. */
|
|
83
|
+
const CODE_SPAN = /(?<!`)(`+)(?!`).*?(?<!`)\1(?!`)/g;
|
|
84
|
+
/** `text` with each fenced line and each code span blanked to spaces, so every offset and line is kept. */
|
|
85
|
+
export function prose(text) {
|
|
86
|
+
return scan(text)
|
|
87
|
+
.map(({ text: line, fenced }) => fenced ? " ".repeat(line.length) : line.replace(CODE_SPAN, (span) => " ".repeat(span.length)))
|
|
88
|
+
.join("\n");
|
|
89
|
+
}
|
|
90
|
+
/** The target of an inline link or image, `<…>` or up to a space or `)`, and of a link definition, `[label]: target`. */
|
|
91
|
+
const INLINE_TARGET = /\]\(\s*(<[^>]*>|[^\s)]+)/g;
|
|
92
|
+
const DEFINITION = /^ {0,3}\[(?!\^)[^\]]+\]:[ \t]*(<[^>]*>|\S+)/;
|
|
93
|
+
/** Every link target in the prose of `text`, its angle brackets dropped, with its 1-based line. */
|
|
94
|
+
export function links(text) {
|
|
95
|
+
return prose(text)
|
|
96
|
+
.split("\n")
|
|
97
|
+
.flatMap((line, index) => {
|
|
98
|
+
const definition = DEFINITION.exec(line)?.[1];
|
|
99
|
+
const targets = [
|
|
100
|
+
...Array.from(line.matchAll(INLINE_TARGET), ([, target = ""]) => target),
|
|
101
|
+
...(definition === undefined ? [] : [definition])
|
|
102
|
+
];
|
|
103
|
+
return targets.map((target) => ({ target: target.replace(/^<(.*)>$/, "$1"), line: index + 1 }));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** The anchors of the headings of `path` under `root`; none when it is not a file. */
|
|
107
|
+
export function anchorsOf(root, path, cache) {
|
|
108
|
+
let anchors = cache.get(path);
|
|
109
|
+
if (anchors === undefined) {
|
|
110
|
+
const full = join(root, path);
|
|
111
|
+
anchors = new Set(statSync(full).isFile() ? headings(readFileSync(full, "utf8")).map(({ anchor }) => anchor) : []);
|
|
112
|
+
cache.set(path, anchors);
|
|
113
|
+
}
|
|
114
|
+
return anchors;
|
|
115
|
+
}
|
package/dist/pins.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// The exact version a repository pins for a package, for a ref that names one: the root package.json's
|
|
2
|
-
// dependencies and devDependencies, a
|
|
3
|
-
//
|
|
4
|
-
// fails only the References that name a package.
|
|
2
|
+
// dependencies and devDependencies, a catalog pin read through pnpm-workspace.yaml's default catalog. A
|
|
3
|
+
// ref without `{name}` reads neither file, so a broken manifest fails only the References that name one.
|
|
5
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
const EXACT_VERSION = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/;
|
package/dist/references-key.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// The strict parser of the `references` key, the authority at runtime, and the reader of the committed
|
|
2
2
|
// config that holds it. `config/references.schema.json` documents the same contract, and
|
|
3
|
-
// `test/references-key.test.ts` runs one case list through both. The first problem stops the
|
|
4
|
-
// parse: a key that fails it syncs nothing.
|
|
3
|
+
// `test/references-key.test.ts` runs one case list through both. The first problem stops the parse.
|
|
5
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
/** The committed config, the only file the key is read from. */
|
package/dist/router-block.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// The Router's block: a table of each Reference's `dir` and `when`, written from the `references` key
|
|
2
2
|
// between two markers. The setup writes it and the Sync checks it; the rest of the Router is the
|
|
3
|
-
// author's. A
|
|
4
|
-
// step when its rows say what the key says, not when its bytes match: cells are compared trimmed, and
|
|
5
|
-
// a `when` that differs only in the spaces at a cell's edge reads as the same row.
|
|
3
|
+
// author's. A block is in step when its rows say what the key says, not its bytes, which a formatter pads.
|
|
6
4
|
const START = "<!-- references:start — written from .ai/agentic.config.json by the setup; edit the key -->";
|
|
7
5
|
const END = "<!-- references:end -->";
|
|
8
6
|
const START_LINE = /^<!-- references:start\b.*-->\s*$/;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dw-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The Gate and the agentic config of a repository: the dw-sdlc bin with the Gate, the setup, the doctor, the config check, and the Sync and setup of References; the resolver of the config's layers, its JSON Schema and the validator that reads it.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|