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 CHANGED
@@ -1,9 +1,10 @@
1
1
  # dw-sdlc
2
2
 
3
- The Gate and the agentic config of a repository. `.ai/agentic.config.json` declares how a repository
4
- works for an agent: its package manager, protected branches, env-file policy, commit convention, the
5
- Gate and who merges a pull request. The package ships the `dw-sdlc` bin that runs on that file, its JSON Schema, and the
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` in order from the repository root, then every Check
19
- under `gate.checks`. It prints one line per Step and per Check, and the last 50 lines of a command
20
- that fails; `--verbose` streams every command's output instead. A Step that creates, changes or
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
- Under `gate.checks`, a Check the package ships is `true` for its defaults or an object of its options.
47
- `comment-budget` holds each comment block in the JavaScript, TypeScript and shell (`.sh`) files under
48
- its `roots` to `maxProseLines` lines of prose, 3 by default.
49
- `disable-directives` fails every Disable directive in a comment that gives no reason: `oxlint-disable*`,
50
- `eslint-disable*`, `@ts-expect-error`, `@ts-ignore`, `rubocop:disable` and `noqa`. A reason follows `--`
51
- on the directive's line, follows an `@ts-` directive, or is a comment on the line above.
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
- answers the one Action it needs next, as a line or, with `--json`, one document. The exit code says
57
- whose turn it is: 0 done, 10 the agent's, 20 wait, 30 the dev's, 2 it cannot run. The script
58
- performs its own Actions: it updates a branch GitHub reports `BEHIND`, marks the head reviewed when the
59
- Review depth is `off`, marks a draft ready once nothing else holds it back, and squash merges a pull
60
- request that is green, reviewed and opened by the `gh` account, when the merge is its own. `--dry-run` performs nothing: it answers the first Action the
61
- script would perform as the dev's. It never deletes a branch; a merged pull request answers with
62
- the delete command for the dev.
63
-
64
- `pr.merge` in the committed `.ai/agentic.config.json` says who merges: the script under `auto`, the
65
- dev under `ask`, the default. `pr.review` says how closely a head no one has reviewed is read: the
66
- agent reviews it under `full`, one `sdlc-reviewer` subagent of the plugin does under `light`, and no
67
- one does under `off`. Under `auto`, the default, the script routes the depth from the changes:
68
- lockfiles alone `off`; a workflow file, more than 100 changed lines outside lockfiles, or a change
69
- too large to size `full`; the rest `light`. The local layer may tighten either and never loosen it;
70
- `--merge` acts as `auto` and `--review full|light|off` sets the depth for one call. A pull request
71
- the `gh` account did not open is always the dev's to merge.
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 ship` answers `review`, its depth under `details`, until the head carries the commit status
78
- `dw-sdlc/review`, which `dw-sdlc pr reviewed <n> --head <sha>` posts after a clean review. A Review
79
- that leaves a Finding open posts it with `--findings`, in state failure, "read, findings open": the
80
- read mark. After the next push, `details.since` names the last head a Review read, clean or marked,
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
- Three Reviews with no success between them end the loop: `pr ship` answers `park review-limit`
86
- instead of `review`, whose command, `dw-sdlc pr reviewed <n> -R <owner/name> --head <sha> --accept`,
87
- is the dev's. `--accept` posts the success described "accepted by the dev, findings open"; with
88
- `--findings` it exits 2. The limit is a constant, not a config key, and `--review <depth>` on a call
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, with their summaries and documents |
98
- | `setup` | this machine's local config layer, the git excludes and the permission rules |
99
- | `doctor` | one PASS / WARN / FAIL line per machine check the guardrails rely on |
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: clean, Findings open, or accepted |
105
- | `recap` | the facts a Recap rests on and whether the session can be closed |
106
-
107
- `dw-sdlc <command> --help` gives each one's options.
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
- that fails the schema; an invalid layer is dropped whole.
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/plugins/guardrails/config/agentic.config.schema.json",
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.merge` 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.",
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
@@ -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 at `file` under `root`, or why there is none to read. */
38
- export function readCatalogue(root, file) {
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.
@@ -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 === "") {
@@ -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[];
@@ -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, a known group, a one-line
2
- // summary, every code and docs path on disk with its anchor resolving, and every file under the roots
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) {
@@ -0,0 +1,5 @@
1
+ import { type Check } from "./check.ts";
2
+ export type SkillSectionsOptions = {
3
+ files: string[];
4
+ };
5
+ export declare const skillSections: Check<SkillSectionsOptions>;