dw-sdlc 0.5.1 → 0.6.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/references.schema.json +65 -51
- package/dist/commands/references-setup.js +57 -24
- package/dist/commands/references-sync.js +5 -5
- package/dist/json-text.d.ts +5 -0
- package/dist/json-text.js +74 -1
- package/dist/references-key.d.ts +22 -3
- package/dist/references-key.js +69 -19
- package/dist/router-block.d.ts +1 -1
- package/dist/router-block.js +4 -4
- package/package.json +1 -1
- package/templates/router.md +4 -4
|
@@ -1,56 +1,70 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
-
"$id": "https://raw.githubusercontent.com/dominikwozniak/dw-sdlc/main/
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/dominikwozniak/dw-sdlc/main/packages/dw-sdlc/config/references.schema.json",
|
|
4
4
|
"title": "references",
|
|
5
|
-
"description": "
|
|
6
|
-
"type": "
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]*$"
|
|
14
|
-
},
|
|
15
|
-
"repository": {
|
|
16
|
-
"description": "A URL `git` can fetch. A `https://github.com/` one fetches with the GitHub CLI's credentials. No whitespace, and no leading `-`, which `git` would read as an option.",
|
|
17
|
-
"type": "string",
|
|
18
|
-
"pattern": "^[^\\s-]\\S*$"
|
|
19
|
-
},
|
|
20
|
-
"ref": {
|
|
21
|
-
"description": "A branch, whose head the clone moves to, or a tag. `{name}` in it stands for the exact version the root `package.json` pins for package `name`, read through the default catalog of `pnpm-workspace.yaml` when the pin says `catalog:` or `catalog:default`. No whitespace, and no leading `-`.",
|
|
22
|
-
"type": "string",
|
|
23
|
-
"pattern": "^[^\\s-]\\S*$"
|
|
24
|
-
},
|
|
25
|
-
"only": {
|
|
26
|
-
"description": "Paths the sparse checkout holds beside the repository's top-level files, relative to the clone and inside it; a directory ends in `/`. Absent, the whole tree is checked out. Present, it names at least one path, and a path the ref's tree lacks fails the Reference.",
|
|
27
|
-
"type": "array",
|
|
28
|
-
"minItems": 1,
|
|
29
|
-
"items": {
|
|
30
|
-
"type": "string",
|
|
31
|
-
"pattern": "^(?!/)(?!(?:.*/)?\\.\\.(?:/|$)).+$"
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"skip": {
|
|
35
|
-
"description": "Paths the sparse checkout leaves out, relative to the clone and inside it. Empty or absent, nothing is left out. With `only`, each lies inside one of its paths, which this schema cannot say and the Sync refuses.",
|
|
36
|
-
"type": "array",
|
|
37
|
-
"items": {
|
|
38
|
-
"type": "string",
|
|
39
|
-
"pattern": "^(?!/)(?!(?:.*/)?\\.\\.(?:/|$)).+$"
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"what": {
|
|
43
|
-
"description": "What the Reference is.",
|
|
44
|
-
"type": "string",
|
|
45
|
-
"minLength": 1
|
|
46
|
-
},
|
|
47
|
-
"when": {
|
|
48
|
-
"description": "When an agent reaches for it.",
|
|
49
|
-
"type": "string",
|
|
50
|
-
"minLength": 1
|
|
51
|
-
}
|
|
5
|
+
"description": "`.ai/references.json`: the repositories an agent reads to see how a problem was solved before, each brought to its ref under `references/<dir>` by the Sync. Only the committed file is read. A `references` key in `.ai/agentic.config.json` is refused: `dw-sdlc references setup` moves it here. A file the Sync refuses syncs nothing.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"type": "string"
|
|
10
|
+
},
|
|
11
|
+
"version": {
|
|
12
|
+
"const": 1
|
|
52
13
|
},
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
14
|
+
"references": {
|
|
15
|
+
"description": "The References, in the order the Router lists them. Every `dir` is unique in the list, which this schema cannot say and the Sync refuses.",
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"type": "object",
|
|
19
|
+
"properties": {
|
|
20
|
+
"dir": {
|
|
21
|
+
"description": "The one directory under `references/` the clone lives in: letters, digits, `_`, `.` and `-`, not starting with `.` or `-`.",
|
|
22
|
+
"type": "string",
|
|
23
|
+
"pattern": "^[A-Za-z0-9_][A-Za-z0-9_.-]*$"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"description": "A URL `git` can fetch. A `https://github.com/` one fetches with the GitHub CLI's credentials. No whitespace, and no leading `-`, which `git` would read as an option.",
|
|
27
|
+
"type": "string",
|
|
28
|
+
"pattern": "^[^\\s-]\\S*$"
|
|
29
|
+
},
|
|
30
|
+
"ref": {
|
|
31
|
+
"description": "A branch, whose head the clone moves to, or a tag. `{name}` in it stands for the exact version the root `package.json` pins for package `name`, read through the default catalog of `pnpm-workspace.yaml` when the pin says `catalog:` or `catalog:default`. No whitespace, and no leading `-`.",
|
|
32
|
+
"type": "string",
|
|
33
|
+
"pattern": "^[^\\s-]\\S*$"
|
|
34
|
+
},
|
|
35
|
+
"only": {
|
|
36
|
+
"description": "Paths the sparse checkout holds beside the repository's top-level files, relative to the clone and inside it; a directory ends in `/`. Absent, the whole tree is checked out. Present, it names at least one path, and a path the ref's tree lacks fails the Reference.",
|
|
37
|
+
"type": "array",
|
|
38
|
+
"minItems": 1,
|
|
39
|
+
"items": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"pattern": "^(?!/)(?!(?:.*/)?\\.\\.(?:/|$)).+$"
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"skip": {
|
|
45
|
+
"description": "Paths the sparse checkout leaves out, relative to the clone and inside it. Empty or absent, nothing is left out. With `only`, each lies inside one of its paths, which this schema cannot say and the Sync refuses.",
|
|
46
|
+
"type": "array",
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"pattern": "^(?!/)(?!(?:.*/)?\\.\\.(?:/|$)).+$"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"what": {
|
|
53
|
+
"description": "What the Reference is.",
|
|
54
|
+
"type": "string",
|
|
55
|
+
"minLength": 1
|
|
56
|
+
},
|
|
57
|
+
"when": {
|
|
58
|
+
"description": "When an agent reaches for it.",
|
|
59
|
+
"type": "string",
|
|
60
|
+
"minLength": 1
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"required": ["dir", "repository", "ref", "what", "when"],
|
|
64
|
+
"additionalProperties": false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"required": ["version", "references"],
|
|
69
|
+
"additionalProperties": false
|
|
56
70
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// `dw-sdlc references setup` sets a repository up to declare References: the
|
|
2
|
-
// each written only when it would change.
|
|
3
|
-
//
|
|
1
|
+
// `dw-sdlc references setup` sets a repository up to declare References: the files its USAGE names,
|
|
2
|
+
// each written only when it would change. A list gains the entries it lacks at its end; the one removal
|
|
3
|
+
// is a `references` key moving out of the agentic config. A file it cannot read as expected is refused.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
5
5
|
import { existsSync, lstatSync, mkdirSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import {
|
|
9
|
-
import { CONFIG,
|
|
8
|
+
import { parseObject, removeKey, render } from "../json-text.js";
|
|
9
|
+
import { CONFIG, REFERENCES_FILE, leftoverKey, leftoverProblem, parseReferences, readFile } from "../references-key.js";
|
|
10
10
|
import { renderBlock, withBlock } from "../router-block.js";
|
|
11
11
|
const ROUTER = "references/AGENTS.md";
|
|
12
12
|
/** A symlink to the Router, so Claude Code loads it. */
|
|
@@ -14,6 +14,8 @@ const ROUTER_LINK = "references/CLAUDE.md";
|
|
|
14
14
|
const SETTINGS = ".claude/settings.json";
|
|
15
15
|
const IGNORE = ".gitignore";
|
|
16
16
|
const TEMPLATE = join(import.meta.dirname, "..", "..", "templates", "router.md");
|
|
17
|
+
/** Where a file the setup creates points `$schema`: the schema the pinned package installs. */
|
|
18
|
+
const SCHEMA = "../node_modules/dw-sdlc/config/references.schema.json";
|
|
17
19
|
/** A clone's own agent files at any depth; Claude Code matches the globs against absolute paths. */
|
|
18
20
|
const EXCLUDES = [
|
|
19
21
|
"**/references/*/CLAUDE.md",
|
|
@@ -33,12 +35,13 @@ const USAGE = `Usage: dw-sdlc references setup [--root <dir>] [--dry-run | --che
|
|
|
33
35
|
|
|
34
36
|
Sets the repository at --root, or the one the current directory sits in, up to declare References:
|
|
35
37
|
the Router in ${ROUTER} with its CLAUDE.md symlink and, between its two markers, the table of each
|
|
36
|
-
Reference's dir and when from
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
when
|
|
41
|
-
|
|
38
|
+
Reference's dir and when from ${REFERENCES_FILE}, claudeMdExcludes in ${SETTINGS} for the clones' own agent
|
|
39
|
+
files, the ${IGNORE} lines that keep every clone out of git but the Router, and ${REFERENCES_FILE} itself
|
|
40
|
+
with no References when it is missing. A references key in ${CONFIG} moves into a missing ${REFERENCES_FILE}
|
|
41
|
+
and leaves the config; with the file there too, both stay and the step is refused. Every file is
|
|
42
|
+
written only when it would change, and the text around the Router's table never; --dry-run reports
|
|
43
|
+
without writing, and --check does the same and exits 1 when a file would change. Prints one line
|
|
44
|
+
per file. Exit 1 when a file is refused, 2 on a bad argument.`;
|
|
42
45
|
function messageOf(error) {
|
|
43
46
|
return error instanceof Error ? error.message : String(error);
|
|
44
47
|
}
|
|
@@ -116,16 +119,43 @@ function withIgnoreLines(current) {
|
|
|
116
119
|
const gap = head !== "" && !ignored ? "\n" : "";
|
|
117
120
|
return { text: `${head}${gap}${missing.join("\n")}\n` };
|
|
118
121
|
}
|
|
119
|
-
/** The
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
/** The text of a References file the setup creates, declaring `references`. */
|
|
123
|
+
function fileOf(references) {
|
|
124
|
+
return render({ $schema: SCHEMA, version: 1, references });
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Where the References come from: a `references` key left in the config moves into a missing file,
|
|
128
|
+
* and a repository with neither gets the file with no References; the file and the key both there
|
|
129
|
+
* are refused, and so is a key the parser refuses.
|
|
130
|
+
*/
|
|
131
|
+
function source(root) {
|
|
132
|
+
const key = leftoverKey(root);
|
|
133
|
+
const path = join(root, REFERENCES_FILE);
|
|
134
|
+
if (key === undefined) {
|
|
135
|
+
const reading = readFile(root);
|
|
136
|
+
if ("problem" in reading) {
|
|
137
|
+
// The problem is the file's own, and the Router's line names it; the file stays as it is.
|
|
138
|
+
return { problem: reading.problem, file: { text: readFileSync(path, "utf8") } };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
references: reading.references,
|
|
142
|
+
file: { text: existsSync(path) ? readFileSync(path, "utf8") : fileOf([]) }
|
|
143
|
+
};
|
|
123
144
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
145
|
+
if (existsSync(path)) {
|
|
146
|
+
return { problem: leftoverProblem(true) };
|
|
147
|
+
}
|
|
148
|
+
const parsed = parseReferences(key);
|
|
149
|
+
if ("problem" in parsed) {
|
|
150
|
+
return { problem: `${CONFIG}: ${parsed.problem.path}: ${parsed.problem.message}; fix it by hand first` };
|
|
127
151
|
}
|
|
128
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
references: parsed.references,
|
|
154
|
+
file: { text: fileOf(key) },
|
|
155
|
+
config: (current) => current === undefined || parseObject(current) === undefined
|
|
156
|
+
? { refusal: "is not a JSON object; fix it by hand first" }
|
|
157
|
+
: { text: removeKey(current, "references") }
|
|
158
|
+
};
|
|
129
159
|
}
|
|
130
160
|
/** The repository `dir` sits in, or `dir` itself outside one. */
|
|
131
161
|
function toplevel(dir) {
|
|
@@ -135,12 +165,12 @@ function toplevel(dir) {
|
|
|
135
165
|
function setup(root, dryRun) {
|
|
136
166
|
const writer = new Writer(root, dryRun);
|
|
137
167
|
const template = readFileSync(TEMPLATE, "utf8");
|
|
138
|
-
const
|
|
168
|
+
const from = source(root);
|
|
139
169
|
writer.put(ROUTER, (current) => {
|
|
140
|
-
if ("problem" in
|
|
141
|
-
return { refusal: `${
|
|
170
|
+
if ("problem" in from) {
|
|
171
|
+
return { refusal: `${from.problem}; the table comes from ${REFERENCES_FILE}, so fix it first` };
|
|
142
172
|
}
|
|
143
|
-
const next = withBlock(current ?? template, renderBlock(
|
|
173
|
+
const next = withBlock(current ?? template, renderBlock(from.references ?? []));
|
|
144
174
|
return "problem" in next ? { refusal: next.problem } : next;
|
|
145
175
|
});
|
|
146
176
|
writer.create(ROUTER_LINK, (path) => {
|
|
@@ -148,7 +178,10 @@ function setup(root, dryRun) {
|
|
|
148
178
|
});
|
|
149
179
|
writer.put(SETTINGS, withExcludes);
|
|
150
180
|
writer.put(IGNORE, withIgnoreLines);
|
|
151
|
-
writer.put(
|
|
181
|
+
writer.put(REFERENCES_FILE, () => ("problem" in from ? (from.file ?? { refusal: from.problem }) : from.file));
|
|
182
|
+
if (!("problem" in from) && from.config !== undefined) {
|
|
183
|
+
writer.put(CONFIG, from.config);
|
|
184
|
+
}
|
|
152
185
|
return writer.steps;
|
|
153
186
|
}
|
|
154
187
|
export function run(argv) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `dw-sdlc references sync`, the Sync: every Reference
|
|
1
|
+
// `dw-sdlc references sync`, the Sync: every Reference `.ai/references.json` declares brought to its ref
|
|
2
2
|
// under references/<dir>, each clone shallow, blobless, detached and sparse where its entry says. An
|
|
3
3
|
// entry that fails fails alone, and a repository that declares no References gets no output at all.
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
@@ -7,7 +7,7 @@ import { extname, join } from "node:path";
|
|
|
7
7
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
8
|
import { parseArgs } from "node:util";
|
|
9
9
|
import { readPins, resolveRef } from "../pins.js";
|
|
10
|
-
import {
|
|
10
|
+
import { REFERENCES_FILE, readReferences } from "../references-key.js";
|
|
11
11
|
import { renderBlock, withBlock } from "../router-block.js";
|
|
12
12
|
const CLONES = "references";
|
|
13
13
|
const ROUTER = "references/AGENTS.md";
|
|
@@ -22,7 +22,7 @@ export const summary = "bring every declared Reference to its ref under referenc
|
|
|
22
22
|
const USAGE = `Usage: dw-sdlc references sync [dir...]
|
|
23
23
|
or: pnpm references:sync [dir...], in a checkout of dw-sdlc
|
|
24
24
|
|
|
25
|
-
Brings every Reference
|
|
25
|
+
Brings every Reference ${REFERENCES_FILE} declares to its ref under ${CLONES}/<dir>, or
|
|
26
26
|
only the given dirs: a missing one cloned shallow, an existing one moved to its branch's head or its
|
|
27
27
|
tag, holding the top-level files and the paths its only names when it has one, without the paths
|
|
28
28
|
its skip names. Prints one line per Reference. Exit 1 when any Reference fails, 2 on a bad key,
|
|
@@ -304,7 +304,7 @@ export async function run(argv) {
|
|
|
304
304
|
const references = reading.references ?? [];
|
|
305
305
|
const unknown = cli.dirs.filter((dir) => !references.some((reference) => reference.dir === dir));
|
|
306
306
|
if (unknown.length > 0) {
|
|
307
|
-
fail(`not in
|
|
307
|
+
fail(`not in ${REFERENCES_FILE}: ${unknown.join(", ")}`);
|
|
308
308
|
}
|
|
309
309
|
if (reading.references === undefined) {
|
|
310
310
|
return 0;
|
|
@@ -335,7 +335,7 @@ export async function run(argv) {
|
|
|
335
335
|
.map((entry) => `${CLONES}/${entry.name}`)
|
|
336
336
|
: [];
|
|
337
337
|
if (stray.length > 0) {
|
|
338
|
-
console.log(`Not listed in ${
|
|
338
|
+
console.log(`Not listed in ${REFERENCES_FILE}, left alone: ${stray.join(", ")}`);
|
|
339
339
|
}
|
|
340
340
|
return failed ? 1 : 0;
|
|
341
341
|
}
|
package/dist/json-text.d.ts
CHANGED
|
@@ -5,3 +5,8 @@ export declare function parseObject(text: string): JsonObject | undefined;
|
|
|
5
5
|
export declare function render(value: unknown): string;
|
|
6
6
|
/** `text`, a JSON object, with `key` set to `value` after its last key, two spaces in. */
|
|
7
7
|
export declare function appendKey(text: string, key: string, value: unknown): string;
|
|
8
|
+
/**
|
|
9
|
+
* `text`, a JSON object, without its top-level `key`, every copy of it when the key repeats; the
|
|
10
|
+
* members around it keep their layout.
|
|
11
|
+
*/
|
|
12
|
+
export declare function removeKey(text: string, key: string): string;
|
package/dist/json-text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// The text of a JSON object file as the setup commands read and write it: parsed only when it holds an
|
|
2
|
-
// object, rendered two spaces in, and a key appended so a hand-formatted file keeps its layout.
|
|
2
|
+
// object, rendered two spaces in, and a key appended or removed so a hand-formatted file keeps its layout.
|
|
3
3
|
export function isObject(value) {
|
|
4
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5
5
|
}
|
|
@@ -24,3 +24,76 @@ export function appendKey(text, key, value) {
|
|
|
24
24
|
const rendered = JSON.stringify(value, null, 2).replaceAll("\n", "\n ");
|
|
25
25
|
return `${body}${comma}\n ${JSON.stringify(key)}: ${rendered}\n${text.slice(close)}`;
|
|
26
26
|
}
|
|
27
|
+
/** The index just past the JSON string that opens at `start`, or the text's end when it never closes. */
|
|
28
|
+
function stringEnd(text, start) {
|
|
29
|
+
let index = start + 1;
|
|
30
|
+
while (index < text.length && text[index] !== '"') {
|
|
31
|
+
index += text[index] === "\\" ? 2 : 1;
|
|
32
|
+
}
|
|
33
|
+
return Math.min(index + 1, text.length);
|
|
34
|
+
}
|
|
35
|
+
/** The start and end of the top-level member `key` in `text`, a JSON object, from its name to its value's end. */
|
|
36
|
+
function memberSpan(text, key) {
|
|
37
|
+
let depth = 0;
|
|
38
|
+
let expectKey = false;
|
|
39
|
+
let start;
|
|
40
|
+
// Just past the last character that is not whitespace: where the member's value ends once a `,` or `}` follows.
|
|
41
|
+
let lastValue = 0;
|
|
42
|
+
for (let index = 0; index < text.length;) {
|
|
43
|
+
const char = text.charAt(index);
|
|
44
|
+
if (char === '"') {
|
|
45
|
+
const end = stringEnd(text, index);
|
|
46
|
+
if (depth === 1 && expectKey) {
|
|
47
|
+
expectKey = false;
|
|
48
|
+
if (start === undefined && JSON.parse(text.slice(index, end)) === key) {
|
|
49
|
+
start = index;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lastValue = end;
|
|
53
|
+
index = end;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (char === "{" || char === "[") {
|
|
57
|
+
depth += 1;
|
|
58
|
+
expectKey = char === "{" && depth === 1;
|
|
59
|
+
}
|
|
60
|
+
else if (char === "}" || char === "]") {
|
|
61
|
+
depth -= 1;
|
|
62
|
+
if (depth === 0 && start !== undefined) {
|
|
63
|
+
return { start, end: lastValue };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (char === "," && depth === 1) {
|
|
67
|
+
if (start !== undefined) {
|
|
68
|
+
return { start, end: lastValue };
|
|
69
|
+
}
|
|
70
|
+
expectKey = true;
|
|
71
|
+
}
|
|
72
|
+
if (!/\s/.test(char)) {
|
|
73
|
+
lastValue = index + 1;
|
|
74
|
+
}
|
|
75
|
+
index += 1;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `text`, a JSON object, without its top-level `key`, every copy of it when the key repeats; the
|
|
81
|
+
* members around it keep their layout.
|
|
82
|
+
*/
|
|
83
|
+
export function removeKey(text, key) {
|
|
84
|
+
const span = memberSpan(text, key);
|
|
85
|
+
if (span === undefined) {
|
|
86
|
+
return text;
|
|
87
|
+
}
|
|
88
|
+
return removeKey(withoutSpan(text, span), key);
|
|
89
|
+
}
|
|
90
|
+
/** `text` without the member at `span` and the comma that joins it to its neighbour. */
|
|
91
|
+
function withoutSpan(text, span) {
|
|
92
|
+
const after = text.slice(span.end).match(/^\s*,\s*/);
|
|
93
|
+
if (after !== null) {
|
|
94
|
+
return text.slice(0, span.start) + text.slice(span.end + after[0].length);
|
|
95
|
+
}
|
|
96
|
+
const before = text.slice(0, span.start).match(/,?\s*$/);
|
|
97
|
+
const cut = span.start - (before?.[0].length ?? 0);
|
|
98
|
+
return text.slice(0, cut) + text.slice(span.end);
|
|
99
|
+
}
|
package/dist/references-key.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
/** The committed
|
|
1
|
+
/** The committed file the References are read from; no local layer adds to it. */
|
|
2
|
+
export declare const REFERENCES_FILE = ".ai/references.json";
|
|
3
|
+
/** The committed agentic config: a `references` key in it is refused, so the file stays the one list. */
|
|
2
4
|
export declare const CONFIG = ".ai/agentic.config.json";
|
|
3
5
|
export type Reference = {
|
|
4
6
|
dir: string;
|
|
@@ -20,12 +22,29 @@ export declare function parseReferences(value: unknown): {
|
|
|
20
22
|
} | {
|
|
21
23
|
problem: KeyProblem;
|
|
22
24
|
};
|
|
25
|
+
/** The References the file's parsed `value` declares, or the first problem in it; `""` names its root. */
|
|
26
|
+
export declare function parseFile(value: unknown): {
|
|
27
|
+
references: Reference[];
|
|
28
|
+
} | {
|
|
29
|
+
problem: KeyProblem;
|
|
30
|
+
};
|
|
31
|
+
/** The `references` key the agentic config at `root` still holds, `undefined` without one or the config. */
|
|
32
|
+
export declare function leftoverKey(root: string): unknown;
|
|
33
|
+
/** Why a `references` key left in the config stops a read, by whether the file is there beside it. */
|
|
34
|
+
export declare function leftoverProblem(fileExists: boolean): string;
|
|
23
35
|
/**
|
|
24
|
-
* The References the
|
|
25
|
-
*
|
|
36
|
+
* The References the file at `root` declares, `undefined` without the file, or the problem that stops
|
|
37
|
+
* the read: a `references` key left in the agentic config, a file that is not JSON, or one the parser
|
|
38
|
+
* refuses. A config that is not JSON is the guards' to report, not the read's.
|
|
26
39
|
*/
|
|
27
40
|
export declare function readReferences(root: string): {
|
|
28
41
|
references: Reference[] | undefined;
|
|
29
42
|
} | {
|
|
30
43
|
problem: string;
|
|
31
44
|
};
|
|
45
|
+
/** The References the file at `root` declares, `undefined` without it, the agentic config unread. */
|
|
46
|
+
export declare function readFile(root: string): {
|
|
47
|
+
references: Reference[] | undefined;
|
|
48
|
+
} | {
|
|
49
|
+
problem: string;
|
|
50
|
+
};
|
package/dist/references-key.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
// The strict parser of
|
|
2
|
-
//
|
|
3
|
-
// `test/references-key.test.ts` runs one case list through both. The first problem stops the parse.
|
|
1
|
+
// The strict parser of `.ai/references.json`, the authority at runtime, and its reader. The file's
|
|
2
|
+
// `references` key holds the entries. `config/references.schema.json` documents the same contract,
|
|
3
|
+
// and `test/references-key.test.ts` runs one case list through both. The first problem stops the parse.
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
/** The committed
|
|
6
|
+
/** The committed file the References are read from; no local layer adds to it. */
|
|
7
|
+
export const REFERENCES_FILE = ".ai/references.json";
|
|
8
|
+
/** The committed agentic config: a `references` key in it is refused, so the file stays the one list. */
|
|
7
9
|
export const CONFIG = ".ai/agentic.config.json";
|
|
10
|
+
const FILE_FIELDS = ["$schema", "version", "references"];
|
|
8
11
|
const FIELDS = ["dir", "repository", "ref", "only", "skip", "what", "when"];
|
|
9
12
|
const REQUIRED = ["dir", "repository", "ref", "what", "when"];
|
|
10
13
|
const DIR_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
|
|
@@ -119,31 +122,78 @@ export function parseReferences(value) {
|
|
|
119
122
|
throw error;
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
125
|
+
/** The References the file's parsed `value` declares, or the first problem in it; `""` names its root. */
|
|
126
|
+
export function parseFile(value) {
|
|
127
|
+
if (!isObject(value)) {
|
|
128
|
+
return { problem: { path: "", message: `must be an object, not ${kind(value)}` } };
|
|
129
|
+
}
|
|
130
|
+
const unknown = Object.keys(value).find((field) => !FILE_FIELDS.includes(field));
|
|
131
|
+
if (unknown !== undefined) {
|
|
132
|
+
return { problem: { path: "", message: `unknown key ${JSON.stringify(unknown)}` } };
|
|
133
|
+
}
|
|
134
|
+
const missing = ["version", "references"].find((field) => !Object.hasOwn(value, field));
|
|
135
|
+
if (missing !== undefined) {
|
|
136
|
+
return { problem: { path: "", message: `missing key ${JSON.stringify(missing)}` } };
|
|
137
|
+
}
|
|
138
|
+
if (value.$schema !== undefined && typeof value.$schema !== "string") {
|
|
139
|
+
return { problem: { path: "$schema", message: `must be a string, not ${kind(value.$schema)}` } };
|
|
140
|
+
}
|
|
141
|
+
if (value.version !== 1) {
|
|
142
|
+
return { problem: { path: "version", message: `must be 1, not ${JSON.stringify(value.version)}` } };
|
|
143
|
+
}
|
|
144
|
+
return parseReferences(value.references);
|
|
145
|
+
}
|
|
146
|
+
/** The JSON object at `path`, `undefined` without the file, or why it is not one. */
|
|
147
|
+
function readObject(path, name) {
|
|
128
148
|
if (!existsSync(path)) {
|
|
129
|
-
return {
|
|
149
|
+
return { value: undefined };
|
|
130
150
|
}
|
|
131
|
-
let
|
|
151
|
+
let value;
|
|
132
152
|
try {
|
|
133
|
-
|
|
153
|
+
value = JSON.parse(readFileSync(path, "utf8"));
|
|
134
154
|
}
|
|
135
155
|
catch (error) {
|
|
136
|
-
return { problem: `${
|
|
156
|
+
return { problem: `${name}: not JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
157
|
+
}
|
|
158
|
+
return isObject(value) ? { value } : { problem: `${name}: not a JSON object` };
|
|
159
|
+
}
|
|
160
|
+
/** The `references` key the agentic config at `root` still holds, `undefined` without one or the config. */
|
|
161
|
+
export function leftoverKey(root) {
|
|
162
|
+
const read = readObject(join(root, CONFIG), CONFIG);
|
|
163
|
+
return "value" in read && read.value !== undefined && Object.hasOwn(read.value, "references")
|
|
164
|
+
? read.value.references
|
|
165
|
+
: undefined;
|
|
166
|
+
}
|
|
167
|
+
/** Why a `references` key left in the config stops a read, by whether the file is there beside it. */
|
|
168
|
+
export function leftoverProblem(fileExists) {
|
|
169
|
+
return fileExists
|
|
170
|
+
? `${CONFIG} has a references key beside ${REFERENCES_FILE}; merge it into the file and remove the key by hand`
|
|
171
|
+
: `${CONFIG}: the references key belongs in ${REFERENCES_FILE}; move it there with \`dw-sdlc references setup\``;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* The References the file at `root` declares, `undefined` without the file, or the problem that stops
|
|
175
|
+
* the read: a `references` key left in the agentic config, a file that is not JSON, or one the parser
|
|
176
|
+
* refuses. A config that is not JSON is the guards' to report, not the read's.
|
|
177
|
+
*/
|
|
178
|
+
export function readReferences(root) {
|
|
179
|
+
if (leftoverKey(root) !== undefined) {
|
|
180
|
+
return { problem: leftoverProblem(existsSync(join(root, REFERENCES_FILE))) };
|
|
137
181
|
}
|
|
138
|
-
|
|
139
|
-
|
|
182
|
+
return readFile(root);
|
|
183
|
+
}
|
|
184
|
+
/** The References the file at `root` declares, `undefined` without it, the agentic config unread. */
|
|
185
|
+
export function readFile(root) {
|
|
186
|
+
const read = readObject(join(root, REFERENCES_FILE), REFERENCES_FILE);
|
|
187
|
+
if ("problem" in read) {
|
|
188
|
+
return read;
|
|
140
189
|
}
|
|
141
|
-
if (
|
|
190
|
+
if (read.value === undefined) {
|
|
142
191
|
return { references: undefined };
|
|
143
192
|
}
|
|
144
|
-
const parsed =
|
|
193
|
+
const parsed = parseFile(read.value);
|
|
145
194
|
if ("problem" in parsed) {
|
|
146
|
-
|
|
195
|
+
const where = parsed.problem.path === "" ? "" : `${parsed.problem.path}: `;
|
|
196
|
+
return { problem: `${REFERENCES_FILE}: ${where}${parsed.problem.message}` };
|
|
147
197
|
}
|
|
148
198
|
return { references: parsed.references };
|
|
149
199
|
}
|
package/dist/router-block.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Reference } from "./references-key.ts";
|
|
2
|
-
/** The block the
|
|
2
|
+
/** The block the file gives: one row per Reference in the file's order, or one line when it has none. */
|
|
3
3
|
export declare function renderBlock(references: Reference[]): string;
|
|
4
4
|
/**
|
|
5
5
|
* The Router's text with `block` in place of the one between its markers: as it is when the two say
|
package/dist/router-block.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// The Router's block: a table of each Reference's `dir` and `when`, written from
|
|
1
|
+
// The Router's block: a table of each Reference's `dir` and `when`, written from `.ai/references.json`
|
|
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 block is in step when its rows say what the
|
|
4
|
-
const START = "<!-- references:start — written from .ai/
|
|
3
|
+
// author's. A block is in step when its rows say what the file says, not its bytes, which a formatter pads.
|
|
4
|
+
const START = "<!-- references:start — written from .ai/references.json by the setup; edit the file -->";
|
|
5
5
|
const END = "<!-- references:end -->";
|
|
6
6
|
const START_LINE = /^<!-- references:start\b.*-->\s*$/;
|
|
7
7
|
const END_LINE = /^<!-- references:end -->\s*$/;
|
|
8
8
|
const NONE = "No Reference is declared yet: `/sdlc-references-add <repository>` adds one.";
|
|
9
|
-
/** The block the
|
|
9
|
+
/** The block the file gives: one row per Reference in the file's order, or one line when it has none. */
|
|
10
10
|
export function renderBlock(references) {
|
|
11
11
|
const body = references.length === 0
|
|
12
12
|
? [NONE]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dw-sdlc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": {
|
package/templates/router.md
CHANGED
|
@@ -9,11 +9,11 @@ file routes reference material only.
|
|
|
9
9
|
|
|
10
10
|
## The list
|
|
11
11
|
|
|
12
|
-
The table below is written from
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
The table below is written from `.ai/references.json` by the setup, so edit the file, never the
|
|
13
|
+
table; the file also holds `what` each Reference is, its repository, its ref and the paths its clone
|
|
14
|
+
skips. Before you research, open the clones whose `when` matches the task.
|
|
15
15
|
|
|
16
|
-
<!-- references:start — written from .ai/
|
|
16
|
+
<!-- references:start — written from .ai/references.json by the setup; edit the file -->
|
|
17
17
|
|
|
18
18
|
No Reference is declared yet: `/sdlc-references-add <repository>` adds one.
|
|
19
19
|
<!-- references:end -->
|