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
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# dw-sdlc
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
works for an agent: its package manager, protected
|
|
5
|
-
Gate and who merges a pull request. The package ships
|
|
6
|
-
resolver of its layers.
|
|
3
|
+
The bin and the config behind the [dw-sdlc](https://github.com/dominikwozniak/dw-sdlc) Claude Code plugins.
|
|
4
|
+
`.ai/agentic.config.json` declares how a repository works for an agent: its package manager, protected
|
|
5
|
+
branches, env-file policy, commit convention, the Gate and who merges a pull request. The package ships
|
|
6
|
+
the `dw-sdlc` bin that runs on that file, the file's JSON Schema and the resolver of its layers. The Gate
|
|
7
|
+
and the Ship script work in any repository, with or without the plugins.
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -13,17 +14,17 @@ Node 24 or later.
|
|
|
13
14
|
pnpm add -D dw-sdlc
|
|
14
15
|
```
|
|
15
16
|
|
|
17
|
+
Every release goes to npm from GitHub Actions by trusted publishing, and every version after 0.17.0
|
|
18
|
+
carries provenance too. Trust evidence never drops from one version to the next, so a repository on
|
|
19
|
+
pnpm's `trustPolicy: no-downgrade` takes each release. Dependabot offers one after the seven days of
|
|
20
|
+
`minimumReleaseAge`; a repository that cannot wait names `dw-sdlc` in `minimumReleaseAgeExclude` under
|
|
21
|
+
its own rule for exclusions.
|
|
22
|
+
|
|
16
23
|
## The Gate
|
|
17
24
|
|
|
18
|
-
`dw-sdlc gate` runs the Steps under `gate.steps
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
deletes a file git tracks or would track fails the Gate as Drift, unless `gate.drift` is `"allow"`.
|
|
22
|
-
It exits 0 when everything passes, 1 when a Step or a Check fails or a Step drifts, and 2 when the
|
|
23
|
-
Gate cannot run. `--json` prints the result as one document. `--cache` skips the run when the
|
|
24
|
-
working tree and the resolved Gate match the last green run in that checkout, so a pre-push hook
|
|
25
|
-
costs nothing on an unchanged tree. `dw-sdlc check <name>` runs one Check alone, with the same
|
|
26
|
-
lines, exit codes and document.
|
|
25
|
+
`dw-sdlc gate` proves a change. It runs the Steps under `gate.steps`, the repository's own commands, in
|
|
26
|
+
order from the repository root, stopping at the first that fails. Then it runs every Check under
|
|
27
|
+
`gate.checks`, a named rule the package ships, none stopping the next.
|
|
27
28
|
|
|
28
29
|
```json
|
|
29
30
|
{
|
|
@@ -35,76 +36,113 @@ lines, exit codes and document.
|
|
|
35
36
|
}
|
|
36
37
|
```
|
|
37
38
|
|
|
38
|
-
A Step is a command string, or `{ "command": "…", "hint": "…" }`; a `gate.checks.run` entry takes a
|
|
39
|
-
`hint` beside `name` and `command`. When the Step or script fails, the Gate prints `hint: <text>` under
|
|
40
|
-
its failure line, and `--json` carries `hint` on every Step and Check, `null` where there is none.
|
|
41
|
-
|
|
42
39
|
```json
|
|
43
40
|
{ "scripts": { "check": "dw-sdlc gate" } }
|
|
44
41
|
```
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
It prints one line per Step and per Check, and the last 50 lines of a command that fails. A Step that
|
|
44
|
+
creates, changes or deletes a file git tracks or would track fails as Drift, unless `gate.drift` is
|
|
45
|
+
`"allow"`. A Step written as `{ "command": "…", "hint": "…" }` prints `hint: <text>` under its failure
|
|
46
|
+
line; a `gate.checks.run` entry takes a `hint` beside `name` and `command`.
|
|
47
|
+
|
|
48
|
+
| flag | does |
|
|
49
|
+
| ------------------- | --------------------------------------------------------------------------------- |
|
|
50
|
+
| `--json` | prints the result as one document; each Step and Check carries `tail` and `hint` |
|
|
51
|
+
| `--verbose` | streams every command's output instead of the tail |
|
|
52
|
+
| `--cache` | skips the run when the tree and the Gate match the last green run in the checkout |
|
|
53
|
+
| `--update-baseline` | after a green run, tightens the baseline of each Ratchet |
|
|
54
|
+
|
|
55
|
+
| exit | means |
|
|
56
|
+
| ---- | ---------------------------------------------------------------------------------- |
|
|
57
|
+
| 0 | everything passes |
|
|
58
|
+
| 1 | a Step or a Check fails, or a Step drifts |
|
|
59
|
+
| 2 | the Gate cannot run: an invalid config layer, a command not found, a missing input |
|
|
60
|
+
|
|
61
|
+
`dw-sdlc check <name>` runs one Check alone, with the same lines, exit codes and document.
|
|
62
|
+
|
|
63
|
+
### Checks
|
|
64
|
+
|
|
65
|
+
Under `gate.checks`, a Check is `true` for its defaults, an object of its options, or `false`. The
|
|
66
|
+
schema's description of each one names its options and defaults.
|
|
67
|
+
|
|
68
|
+
| Check | fails | reads first |
|
|
69
|
+
| -------------------- | --------------------------------------------------------------------------- | ------------------------- |
|
|
70
|
+
| `markdown-refs` | a relative link or image whose path or heading anchor does not resolve | nothing |
|
|
71
|
+
| `temporal-language` | a changelog phrase in a durable document, outside code | nothing |
|
|
72
|
+
| `comment-budget` | a comment block over 3 lines of prose in JS, TS or shell | nothing |
|
|
73
|
+
| `disable-directives` | a linter or type-checker disable comment with no reason | nothing |
|
|
74
|
+
| `agents-map` | an `AGENTS.md` over budget, missing the Gate command or pointing at nothing | `AGENTS.md` |
|
|
75
|
+
| `primitives` | a catalogue entry out of step with the tree, or a file no entry owns | `.ai/primitives.json` |
|
|
76
|
+
| `routed-docs` | a document under `docs/` no Task Router row or Primitive leads to | the map, the catalogue |
|
|
77
|
+
| `markdown-budget` | a markdown file over the byte budget of its glob | the files its globs match |
|
|
78
|
+
| `decision-records` | a decision record misnamed, misnumbered or without a valid status | a records directory |
|
|
79
|
+
| `file-size-ratchet` | a code file over its line limit that the baseline does not allow | its baseline |
|
|
80
|
+
| `skill-sections` | a README section a skill copies that differs from its source | the catalogue |
|
|
81
|
+
|
|
82
|
+
Each Check's options and defaults:
|
|
83
|
+
[its description in the schema](https://github.com/dominikwozniak/dw-sdlc/blob/main/packages/dw-sdlc/config/agentic.config.schema.json).
|
|
52
84
|
|
|
53
85
|
## The Ship script
|
|
54
86
|
|
|
55
|
-
`dw-sdlc pr ship [<n>] [-R owner/name]` reads one pull request through an authenticated `gh` and
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
87
|
+
`dw-sdlc pr ship [<n>] [-R owner/name]` reads one pull request through an authenticated `gh` and answers
|
|
88
|
+
the one Action it needs next, as a line or, with `--json`, one document. It performs its own Actions:
|
|
89
|
+
it updates a branch GitHub reports `BEHIND`, marks the head reviewed at Review depth `off`, marks a draft
|
|
90
|
+
ready once nothing else holds it back, and squash merges when the merge is its own. It never deletes a
|
|
91
|
+
branch. `--dry-run` performs nothing.
|
|
92
|
+
|
|
93
|
+
| exit | whose turn |
|
|
94
|
+
| ---- | ------------------ |
|
|
95
|
+
| 0 | done |
|
|
96
|
+
| 10 | the agent's: act |
|
|
97
|
+
| 20 | nobody's yet: wait |
|
|
98
|
+
| 30 | the dev's: park |
|
|
99
|
+
| 2 | it cannot run |
|
|
100
|
+
|
|
101
|
+
| key | sets |
|
|
102
|
+
| ------------------ | ---------------------------------------------------------------------------------------------- |
|
|
103
|
+
| `pr.merge` | who merges: the script under `auto`, the dev under `ask`, the default |
|
|
104
|
+
| `pr.review` | the Review depth of an unreviewed head: `full`, `light`, `off`, or `auto` from the change size |
|
|
105
|
+
| `pr.reviewCommand` | the slash command a `full` Review runs, `/code-review` by default |
|
|
72
106
|
|
|
73
107
|
```json
|
|
74
108
|
{ "pr": { "merge": "auto", "review": "full" } }
|
|
75
109
|
```
|
|
76
110
|
|
|
77
|
-
`pr
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
so the Review after a fix reads the fix alone. `pr reviewed` posts nothing and exits 2 once a push
|
|
82
|
-
moved the head, so a new head needs a fresh review. Under `off` the script posts that status
|
|
83
|
-
itself, its description saying why, and goes on.
|
|
111
|
+
The local layer may tighten `pr.merge` and `pr.review`, never loosen them. `--merge` acts as `auto` and
|
|
112
|
+
`--review full|light|off` sets the depth for one call. A pull request the `gh` account did not open is
|
|
113
|
+
always the dev's to merge. `light` spawns the `sdlc-reviewer` subagent of the
|
|
114
|
+
[`sdlc` plugin](https://github.com/dominikwozniak/dw-sdlc/blob/main/plugins/sdlc/README.md#the-light-review).
|
|
84
115
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
buys one more Review past it.
|
|
116
|
+
`dw-sdlc pr reviewed <n> --head <sha>` pins a Review to the head it read, as the commit status
|
|
117
|
+
`dw-sdlc/review`: clean, Findings open with `--findings`, or accepted by the dev with `--accept`. The
|
|
118
|
+
routing rules, the read mark and the limit of three unclean Reviews are in
|
|
119
|
+
[How the Ship script merges a pull request](https://github.com/dominikwozniak/dw-sdlc/blob/main/docs/overview.md#how-the-ship-script-merges-a-pull-request).
|
|
90
120
|
|
|
91
121
|
## Commands
|
|
92
122
|
|
|
93
|
-
| command | what it does
|
|
94
|
-
| ------------------ |
|
|
95
|
-
| `gate` | the Steps of `gate.steps` in order, then every Check under `gate.checks`
|
|
96
|
-
| `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks
|
|
97
|
-
| `primitives` | the Primitives that own each path,
|
|
98
|
-
| `setup` | this machine's local config layer,
|
|
99
|
-
| `doctor` | one PASS / WARN / FAIL line per machine check the
|
|
100
|
-
| `config` | the resolved agentic config, or one file validated against the schema
|
|
101
|
-
| `references sync` | every declared Reference brought to its ref under `references/`
|
|
102
|
-
| `references setup` | the Router, its table and the ignore rules a repository needs to declare References
|
|
103
|
-
| `pr ship [<n>]` | the Action one pull request needs next, performing the script's own
|
|
104
|
-
| `pr reviewed <n>` | a Review pinned to the head it read, as a commit status
|
|
105
|
-
| `recap` | the facts a Recap rests on
|
|
106
|
-
|
|
107
|
-
`
|
|
123
|
+
| command | what it does |
|
|
124
|
+
| ------------------ | ------------------------------------------------------------------------------------------------- |
|
|
125
|
+
| `gate` | the Gate: the Steps of `gate.steps` in order, then every Check under `gate.checks` |
|
|
126
|
+
| `check <name>` | one Check of the Gate by its name, without the Steps and the other Checks |
|
|
127
|
+
| `primitives` | the Primitives that own each path, or that a change removes, adds or extends |
|
|
128
|
+
| `setup` | the Gate adopted where asked, this machine's local config layer, git excludes and permissions |
|
|
129
|
+
| `doctor` | one PASS / WARN / FAIL line per machine check the `sdlc` plugin relies on |
|
|
130
|
+
| `config` | the resolved agentic config, or one file validated against the schema |
|
|
131
|
+
| `references sync` | every declared Reference, a repository cloned for reading, brought to its ref under `references/` |
|
|
132
|
+
| `references setup` | the Router, its table and the ignore rules a repository needs to declare References |
|
|
133
|
+
| `pr ship [<n>]` | the Action one pull request needs next, performing the script's own |
|
|
134
|
+
| `pr reviewed <n>` | a Review pinned to the head it read, as a commit status |
|
|
135
|
+
| `recap` | the facts a Recap rests on, whether the session can be closed and what could come next |
|
|
136
|
+
| `sweep` | Landed branches and clean worktrees removed, and what it keeps named |
|
|
137
|
+
| `worker start` | a Worker started on a Brief in a herdr terminal |
|
|
138
|
+
| `worker ls` | the Workers this Lead started and their states |
|
|
139
|
+
| `worker report` | a Recap delivered to the Lead, retried while it is blocked |
|
|
140
|
+
| `lead start` | the next session started as a Lead in a herdr terminal of its own |
|
|
141
|
+
|
|
142
|
+
`dw-sdlc --help` lists them, and `dw-sdlc <command> --help` gives each one's options and exit codes. A
|
|
143
|
+
Primitive is one unit a repository exposes to an agent, a module, a hook, a script, a config key or a
|
|
144
|
+
skill, listed in `.ai/primitives.json`. A Recap, a Brief, a Worker and a Lead belong to the
|
|
145
|
+
[`sdlc-sessions` plugin](https://github.com/dominikwozniak/dw-sdlc/blob/main/plugins/sdlc-sessions/README.md).
|
|
108
146
|
|
|
109
147
|
## The resolver
|
|
110
148
|
|
|
@@ -115,8 +153,8 @@ const { config, problems } = resolveConfig(process.cwd())
|
|
|
115
153
|
```
|
|
116
154
|
|
|
117
155
|
`resolveConfig` merges the committed `.ai/agentic.config.json`, the machine's
|
|
118
|
-
`.ai/agentic.config.local.json` and what it infers from the repository. `problems` names each layer
|
|
119
|
-
|
|
156
|
+
`.ai/agentic.config.local.json` and what it infers from the repository. `problems` names each layer that
|
|
157
|
+
fails the schema; an invalid layer is dropped whole, and the guards then run the strictest set.
|
|
120
158
|
|
|
121
159
|
## License
|
|
122
160
|
|
|
@@ -1,8 +1,8 @@
|
|
|
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/agentic.config.schema.json",
|
|
4
4
|
"title": "agentic.config",
|
|
5
|
-
"description": "How a repository works for an agent. `.ai/agentic.config.json` is committed; `.ai/agentic.config.local.json` overrides it on one machine and may tighten `guardrails` and `pr.
|
|
5
|
+
"description": "How a repository works for an agent. `.ai/agentic.config.json` is committed; `.ai/agentic.config.local.json` overrides it on one machine and may tighten `guardrails`, `pr.merge` and `pr.review` but never loosen them. A key neither file sets is inferred: the Node package manager from the lockfile, the `conventional` commit preset, and `gate.command` from the manifest's `check` script. Unknown top-level keys are tolerated so other tool families can share the file; the keys named here refuse unknown children.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"properties": {
|
|
8
8
|
"$schema": {
|
|
@@ -428,6 +428,22 @@
|
|
|
428
428
|
}
|
|
429
429
|
},
|
|
430
430
|
"additionalProperties": false
|
|
431
|
+
},
|
|
432
|
+
"skill-sections": {
|
|
433
|
+
"description": "Every README section a skill copies matches its source. A part is the section of the README under the heading with GitHub's anchor `<anchor>`, up to the next heading of its level or above, copied whole between `<!-- README.md#<anchor> -->` and `<!-- /README.md#<anchor> -->`, each on its own line, with a blank line inside each. The README is the `README.md` of the directory that holds the `skills` directory the file is in, or of the file's own directory outside one. A part fails when it differs from its section, when the README has no heading with its anchor, and when no Primitive in the catalogue `primitives` reads names `<README>#<anchor>` among its docs; a marker without its pair fails too. A marker inside code is none, and a code fence that never closes above a marker fails, since it hides that marker. `dw-sdlc check skill-sections --write` writes each part from its section. A repository with no part passes without reading the catalogue; one with a part and no valid catalogue cannot run.",
|
|
434
|
+
"type": ["boolean", "object"],
|
|
435
|
+
"properties": {
|
|
436
|
+
"files": {
|
|
437
|
+
"description": "Globs of the skill files, against the files git tracks or would track; `*` and `**` skip a name that starts with a dot. Default `**/skills/**/*.md`.",
|
|
438
|
+
"type": "array",
|
|
439
|
+
"items": {
|
|
440
|
+
"type": "string",
|
|
441
|
+
"minLength": 1
|
|
442
|
+
},
|
|
443
|
+
"uniqueItems": true
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
"additionalProperties": false
|
|
431
447
|
}
|
|
432
448
|
},
|
|
433
449
|
"additionalProperties": false
|
package/dist/catalogue.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type CatalogueRead = {
|
|
|
27
27
|
catalogue: Catalogue;
|
|
28
28
|
text: string;
|
|
29
29
|
};
|
|
30
|
+
/** The catalogue `text` holds, or why it is not one. */
|
|
31
|
+
export declare function parseCatalogue(text: string): Exclude<CatalogueRead, {
|
|
32
|
+
state: "missing";
|
|
33
|
+
}>;
|
|
30
34
|
/** The catalogue at `file` under `root`, or why there is none to read. */
|
|
31
35
|
export declare function readCatalogue(root: string, file: string): CatalogueRead;
|
|
32
36
|
/**
|
package/dist/catalogue.js
CHANGED
|
@@ -34,13 +34,8 @@ const SCHEMA = {
|
|
|
34
34
|
},
|
|
35
35
|
additionalProperties: false
|
|
36
36
|
};
|
|
37
|
-
/** The catalogue
|
|
38
|
-
export function
|
|
39
|
-
const path = join(root, file);
|
|
40
|
-
if (!existsSync(path)) {
|
|
41
|
-
return { state: "missing" };
|
|
42
|
-
}
|
|
43
|
-
const text = readFileSync(path, "utf8");
|
|
37
|
+
/** The catalogue `text` holds, or why it is not one. */
|
|
38
|
+
export function parseCatalogue(text) {
|
|
44
39
|
let parsed;
|
|
45
40
|
try {
|
|
46
41
|
parsed = JSON.parse(text);
|
|
@@ -55,6 +50,11 @@ export function readCatalogue(root, file) {
|
|
|
55
50
|
// The schema above is the runtime twin of `Catalogue`: a document it accepts has this shape.
|
|
56
51
|
return { state: "valid", catalogue: parsed, text };
|
|
57
52
|
}
|
|
53
|
+
/** The catalogue at `file` under `root`, or why there is none to read. */
|
|
54
|
+
export function readCatalogue(root, file) {
|
|
55
|
+
const path = join(root, file);
|
|
56
|
+
return existsSync(path) ? parseCatalogue(readFileSync(path, "utf8")) : { state: "missing" };
|
|
57
|
+
}
|
|
58
58
|
/**
|
|
59
59
|
* How near the `code` entry `entry`, a file or a directory, names `path`: the length of the entry when
|
|
60
60
|
* it is the path or a directory above it, -1 when it is neither.
|
package/dist/checks/check.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export type Check<Options extends object> = {
|
|
|
22
22
|
run: (root: string, options: Options) => Finding[];
|
|
23
23
|
/** Present on a Ratchet. */
|
|
24
24
|
baseline?: Baseline<Options>;
|
|
25
|
+
/** Present on a Check that derives files from their sources: writes them, says what it wrote. */
|
|
26
|
+
write?: (root: string, options: Options) => string;
|
|
25
27
|
};
|
|
26
28
|
/** The options `setting` turns the Check on with: the defaults under `true`, an object over them, else off. */
|
|
27
29
|
export declare function optionsOf<Options extends object>(check: Check<Options>, setting: unknown): Options | undefined;
|
|
@@ -7,5 +7,20 @@ export type DecisionRecordsOptions = {
|
|
|
7
7
|
statuses: string[];
|
|
8
8
|
fields: DecisionField[];
|
|
9
9
|
};
|
|
10
|
+
/** The first words of a status that say a record once governed and no longer does. */
|
|
11
|
+
export declare const OUT_OF_FORCE: Set<string>;
|
|
12
|
+
/**
|
|
13
|
+
* The `status` of the frontmatter `text` opens with, its quotes dropped, and its 1-based line. It keeps a
|
|
14
|
+
* reader apart from `frontmatter`, which a status never goes through: a ` #` is part of it.
|
|
15
|
+
*/
|
|
16
|
+
export declare function statusOf(text: string): {
|
|
17
|
+
value: string;
|
|
18
|
+
line: number;
|
|
19
|
+
} | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* The status of the decision record `path` names, whole, or "" when it has none; undefined when `path` is
|
|
22
|
+
* no record of `dirs` or is not on disk. A record is a file `<number>-<slug>.md` directly under a directory.
|
|
23
|
+
*/
|
|
24
|
+
export declare function recordStatus(root: string, dirs: string[], path: string): string | undefined;
|
|
10
25
|
export declare const decisionRecords: Check<DecisionRecordsOptions>;
|
|
11
26
|
export {};
|
|
@@ -12,6 +12,8 @@ const FIELDS = ["date", "touches", "superseded-by", "amended-by"];
|
|
|
12
12
|
const RECORD = /^(\d+)-[^/]+\.md$/;
|
|
13
13
|
/** The first words of a status that put a record in force, so it governs the paths under its `touches`. */
|
|
14
14
|
const IN_FORCE = new Set(["accepted", "active"]);
|
|
15
|
+
/** The first words of a status that say a record once governed and no longer does. */
|
|
16
|
+
export const OUT_OF_FORCE = new Set(["deprecated", "superseded"]);
|
|
15
17
|
/** A character that makes a path under `touches` a glob. */
|
|
16
18
|
const GLOB = /[*?[\]{}]/;
|
|
17
19
|
const REMEDIES = {
|
|
@@ -37,7 +39,7 @@ function isDirectory(path) {
|
|
|
37
39
|
* The `status` of the frontmatter `text` opens with, its quotes dropped, and its 1-based line. It keeps a
|
|
38
40
|
* reader apart from `frontmatter`, which a status never goes through: a ` #` is part of it.
|
|
39
41
|
*/
|
|
40
|
-
function statusOf(text) {
|
|
42
|
+
export function statusOf(text) {
|
|
41
43
|
const lines = text.split("\n");
|
|
42
44
|
if (lines[0]?.trim() !== "---") {
|
|
43
45
|
return undefined;
|
|
@@ -55,6 +57,18 @@ function isDay(value) {
|
|
|
55
57
|
const date = new Date(`${value}T00:00:00Z`);
|
|
56
58
|
return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(date.getTime()) && date.toISOString().startsWith(value);
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* The status of the decision record `path` names, whole, or "" when it has none; undefined when `path` is
|
|
62
|
+
* no record of `dirs` or is not on disk. A record is a file `<number>-<slug>.md` directly under a directory.
|
|
63
|
+
*/
|
|
64
|
+
export function recordStatus(root, dirs, path) {
|
|
65
|
+
const inDirs = dirs.some((dir) => {
|
|
66
|
+
const prefix = `${dir.replace(/\/+$/, "")}/`;
|
|
67
|
+
return path.startsWith(prefix) && RECORD.test(path.slice(prefix.length));
|
|
68
|
+
});
|
|
69
|
+
const file = join(root, path);
|
|
70
|
+
return inDirs && existsSync(file) ? (statusOf(readFileSync(file, "utf8"))?.value ?? "") : undefined;
|
|
71
|
+
}
|
|
58
72
|
function judgeDate({ file, keys }) {
|
|
59
73
|
const date = keys.get("date");
|
|
60
74
|
if (date === undefined || date.value === "") {
|
package/dist/checks/index.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type EnabledCheck = {
|
|
|
7
7
|
file: string;
|
|
8
8
|
refresh: (root: string) => string;
|
|
9
9
|
};
|
|
10
|
+
write?: (root: string) => string;
|
|
10
11
|
};
|
|
11
12
|
/** The name of every Check the package ships, in the order the Gate runs them. */
|
|
12
13
|
export declare const CHECK_NAMES: readonly string[];
|
package/dist/checks/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { markdownBudget } from "./markdown-budget.js";
|
|
|
10
10
|
import { markdownRefs } from "./markdown-refs.js";
|
|
11
11
|
import { primitives } from "./primitives.js";
|
|
12
12
|
import { routedDocs } from "./routed-docs.js";
|
|
13
|
+
import { skillSections } from "./skill-sections.js";
|
|
13
14
|
import { temporalLanguage } from "./temporal-language.js";
|
|
14
15
|
function ship(check) {
|
|
15
16
|
return {
|
|
@@ -24,6 +25,10 @@ function ship(check) {
|
|
|
24
25
|
if (baseline !== undefined) {
|
|
25
26
|
enabled.baseline = { file: baseline.file, refresh: (root) => baseline.refresh(root, options) };
|
|
26
27
|
}
|
|
28
|
+
const { write } = check;
|
|
29
|
+
if (write !== undefined) {
|
|
30
|
+
enabled.write = (root) => write(root, options);
|
|
31
|
+
}
|
|
27
32
|
return enabled;
|
|
28
33
|
}
|
|
29
34
|
};
|
|
@@ -38,7 +43,8 @@ const SHIPPED = [
|
|
|
38
43
|
ship(decisionRecords),
|
|
39
44
|
ship(fileSizeRatchet),
|
|
40
45
|
ship(commentBudget),
|
|
41
|
-
ship(disableDirectives)
|
|
46
|
+
ship(disableDirectives),
|
|
47
|
+
ship(skillSections)
|
|
42
48
|
];
|
|
43
49
|
/** The name of every Check the package ships, in the order the Gate runs them. */
|
|
44
50
|
export const CHECK_NAMES = SHIPPED.map(({ name }) => name);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// `primitives`: the catalogue says what is true of the tree. Unique ids
|
|
2
|
-
// summary,
|
|
3
|
-
// named by a Primitive.
|
|
1
|
+
// `primitives`: the catalogue says what is true of the tree. Unique ids that start with their group, a
|
|
2
|
+
// one-line summary, real code and docs paths, no decision record out of force, every file named.
|
|
4
3
|
import { existsSync } from "node:fs";
|
|
5
4
|
import { join } from "node:path";
|
|
5
|
+
import { resolveConfig } from "../agentic-config.js";
|
|
6
6
|
import { CATALOGUE_FILE, primitivesNaming, readCatalogue } from "../catalogue.js";
|
|
7
7
|
import { anchorsOf } from "../markdown.js";
|
|
8
8
|
import { CannotRun, optionsOf } from "./check.js";
|
|
9
|
+
import { decisionRecords, OUT_OF_FORCE, recordStatus } from "./decision-records.js";
|
|
9
10
|
import { treeFiles } from "./tree.js";
|
|
10
11
|
const MAX_SUMMARY = 120;
|
|
11
12
|
/** A file a test runs, never shipped: under a test directory, or named as a test. */
|
|
@@ -14,6 +15,8 @@ const REMEDIES = {
|
|
|
14
15
|
missing: "create it, or point gate.checks.primitives.file at the catalogue",
|
|
15
16
|
shape: "give it the catalogue's shape: version 1, groups of id and name, primitives of id, name, group, summary, code and docs",
|
|
16
17
|
duplicate: "give each Primitive an id of its own",
|
|
18
|
+
prefix: "start the id with its group and a . or a -, or move the Primitive into the group its id starts with",
|
|
19
|
+
record: "name the record that replaces it, or drop the entry",
|
|
17
20
|
group: "add the group to groups, or move the Primitive into one listed there",
|
|
18
21
|
summary: `keep the summary to one line of ${MAX_SUMMARY} characters or fewer`,
|
|
19
22
|
path: "fix the path or drop it",
|
|
@@ -30,10 +33,13 @@ function lineOf(text, id, nth) {
|
|
|
30
33
|
return match?.index === undefined ? undefined : text.slice(0, start + match.index).split("\n").length;
|
|
31
34
|
}
|
|
32
35
|
/** What is wrong with one Primitive, declared at `at`: its group, its summary, each code and docs entry. */
|
|
33
|
-
function judge(root, catalogue, primitive, at, cache) {
|
|
36
|
+
function judge(root, catalogue, primitive, at, cache, records) {
|
|
34
37
|
const { id, group, summary, code, docs } = primitive;
|
|
35
38
|
const findings = [];
|
|
36
39
|
const fail = (message, remedy) => findings.push({ ...at, message, remedy });
|
|
40
|
+
if (!id.startsWith(`${group}.`) && !id.startsWith(`${group}-`)) {
|
|
41
|
+
fail(`the id ${show(id)} does not start with its group ${show(group)} and a "." or a "-"`, REMEDIES.prefix);
|
|
42
|
+
}
|
|
37
43
|
if (!catalogue.groups.some((known) => known.id === group)) {
|
|
38
44
|
fail(`${show(id)} is in group ${show(group)}, which groups does not list`, REMEDIES.group);
|
|
39
45
|
}
|
|
@@ -55,6 +61,10 @@ function judge(root, catalogue, primitive, at, cache) {
|
|
|
55
61
|
else if (anchor !== undefined && !anchorsOf(root, path, cache).has(anchor)) {
|
|
56
62
|
fail(`${show(id)} names the doc ${show(entry)}, which has no heading with that anchor`, REMEDIES.anchor);
|
|
57
63
|
}
|
|
64
|
+
const status = recordStatus(root, records, path);
|
|
65
|
+
if (status !== undefined && OUT_OF_FORCE.has(status.split(/\s+/)[0] ?? "")) {
|
|
66
|
+
fail(`${show(id)} names the decision record ${show(entry)}, whose status ${show(status)} is no longer in force`, REMEDIES.record);
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
return findings;
|
|
60
70
|
}
|
|
@@ -67,6 +77,7 @@ function run(root, { file, roots }) {
|
|
|
67
77
|
return read.problems.map((message) => ({ file, message, remedy: REMEDIES.shape }));
|
|
68
78
|
}
|
|
69
79
|
const { catalogue, text } = read;
|
|
80
|
+
const records = (optionsOf(decisionRecords, resolveConfig(root).config.gate.checks["decision-records"]) ?? decisionRecords.defaults).dirs;
|
|
70
81
|
const findings = [];
|
|
71
82
|
const seen = new Map();
|
|
72
83
|
const cache = new Map();
|
|
@@ -82,7 +93,7 @@ function run(root, { file, roots }) {
|
|
|
82
93
|
remedy: REMEDIES.duplicate
|
|
83
94
|
});
|
|
84
95
|
}
|
|
85
|
-
findings.push(...judge(root, catalogue, primitive, at, cache));
|
|
96
|
+
findings.push(...judge(root, catalogue, primitive, at, cache, records));
|
|
86
97
|
}
|
|
87
98
|
for (const path of treeFiles(root, roots)) {
|
|
88
99
|
if (!TEST_FILE.test(path) && primitivesNaming(catalogue, path).length === 0) {
|