cyber-sdd 0.7.0 → 0.7.2
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/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- package/skills/manage-scenario-bridge/README.md +3 -3
- package/skills/manage-scenario-bridge/SKILL.md +1 -1
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -1,12 +1,593 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Compiled by `universal-plugin plugin build` from src/check-project-specs/check-project-specs.mts. Edit the source, not this file.
|
|
3
|
+
globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/check-project-specs/scripts/check-project-specs.mjs","commands":{}};
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
6
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
|
+
//#region src/discover-specs/discover-specs.mts
|
|
9
|
+
const LIFECYCLE_STATUSES = /* @__PURE__ */ new Set([
|
|
10
|
+
"draft",
|
|
11
|
+
"approved",
|
|
12
|
+
"implemented",
|
|
13
|
+
"deprecated"
|
|
14
|
+
]);
|
|
15
|
+
const ROOT_PROJECT_NAME = "repo";
|
|
16
|
+
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
17
|
+
"node_modules",
|
|
18
|
+
".git",
|
|
19
|
+
"dist",
|
|
20
|
+
".turbo",
|
|
21
|
+
".next",
|
|
22
|
+
"coverage"
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* A directory that is itself a checkout is a DIFFERENT repository's tree, not part of this corpus.
|
|
26
|
+
*
|
|
27
|
+
* A name blocklist cannot express this: it can only say "this is called X", never "this is a
|
|
28
|
+
* boundary". `SKIP_DIRS` already holds `.git`, so the walk skipped the METADATA directory while
|
|
29
|
+
* descending straight into the checkout that directory marks — one level off from where the guard
|
|
30
|
+
* was needed. Agent-harness worktree isolation checks this repo out inside itself, and the scan then
|
|
31
|
+
* found the whole corpus once per worktree: 38 spec files where 10 exist, and every corpus-wide
|
|
32
|
+
* guard silently auditing a tree nobody has.
|
|
33
|
+
*
|
|
34
|
+
* `.git` is a DIRECTORY in a clone and a FILE in a worktree or submodule, so both forms count.
|
|
35
|
+
*/
|
|
36
|
+
function isNestedCheckout(abs) {
|
|
37
|
+
return existsSync(join(abs, ".git"));
|
|
38
|
+
}
|
|
39
|
+
const ANCHORS_CONFIG = ".agents/sdd/spec-anchors.toml";
|
|
40
|
+
function parseFrontmatter(text) {
|
|
41
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)/.exec(text);
|
|
42
|
+
if (!m) return null;
|
|
43
|
+
const fm = { approval: {} };
|
|
44
|
+
let gate = null;
|
|
45
|
+
for (const raw of m[1].split("\n")) {
|
|
46
|
+
const line = raw.replace(/\r$/, "");
|
|
47
|
+
if (line.trim() === "" || line.trim().startsWith("#")) continue;
|
|
48
|
+
const indent = line.length - line.trimStart().length;
|
|
49
|
+
const [key, ...rest] = line.trim().split(":");
|
|
50
|
+
const value = rest.join(":").trim();
|
|
51
|
+
if (indent === 0) {
|
|
52
|
+
gate = null;
|
|
53
|
+
if (key === "status") fm.status = unquote(value);
|
|
54
|
+
else if (key === "project-path") fm.projectPath = unquote(value);
|
|
55
|
+
else if (key === "name") fm.name = unquote(value);
|
|
56
|
+
else if (key === "approval") gate = null;
|
|
57
|
+
} else if (indent === 2 && value === "" && insideApproval(m[1], line)) gate = key;
|
|
58
|
+
else if (indent >= 4 && key === "verdict" && gate) fm.approval[gate] = unquote(value);
|
|
59
|
+
}
|
|
60
|
+
return fm;
|
|
61
|
+
}
|
|
62
|
+
function insideApproval(block, line) {
|
|
63
|
+
const lines = block.split("\n");
|
|
64
|
+
const idx = lines.indexOf(line);
|
|
65
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
66
|
+
const l = lines[i].replace(/\r$/, "");
|
|
67
|
+
if (l.trim() === "" || l.trim().startsWith("#")) continue;
|
|
68
|
+
if (l.length - l.trimStart().length === 0) return l.trim().replace(/:.*$/, "") === "approval";
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
function unquote(v) {
|
|
73
|
+
return v.replace(/^["']|["']$/g, "");
|
|
74
|
+
}
|
|
75
|
+
function classifyLocation(relPath) {
|
|
76
|
+
const p = relPath.replace(/\\/g, "/");
|
|
77
|
+
if (p === ".agents/spec/spec.md") return {
|
|
78
|
+
pattern: "root-single",
|
|
79
|
+
locationDir: ""
|
|
80
|
+
};
|
|
81
|
+
const multi = /^\.agents\/specs\/([^/]+)\/spec\.md$/.exec(p);
|
|
82
|
+
if (multi) return {
|
|
83
|
+
pattern: "root-multi",
|
|
84
|
+
locationDir: multi[1]
|
|
85
|
+
};
|
|
86
|
+
const nested = /^(.+)\/\.agents\/spec\/spec\.md$/.exec(p);
|
|
87
|
+
if (nested) return {
|
|
88
|
+
pattern: "nested",
|
|
89
|
+
locationDir: nested[1]
|
|
90
|
+
};
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function deriveName(loc, fm) {
|
|
94
|
+
if (fm.name) return {
|
|
95
|
+
name: fm.name,
|
|
96
|
+
nameSource: "declared"
|
|
97
|
+
};
|
|
98
|
+
if (loc.pattern === "root-single") return {
|
|
99
|
+
name: ROOT_PROJECT_NAME,
|
|
100
|
+
nameSource: "derived"
|
|
101
|
+
};
|
|
102
|
+
if (loc.pattern === "root-multi") return {
|
|
103
|
+
name: loc.locationDir,
|
|
104
|
+
nameSource: "derived"
|
|
105
|
+
};
|
|
106
|
+
if (loc.pattern === "extra" && loc.capturedName) return {
|
|
107
|
+
name: loc.capturedName,
|
|
108
|
+
nameSource: "derived"
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
name: loc.locationDir.split("/").pop() ?? loc.locationDir,
|
|
112
|
+
nameSource: "guessed"
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function discoverSpecFiles(root) {
|
|
116
|
+
const found = [];
|
|
117
|
+
const walk = (relDir) => {
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = readdirSync(join(root, relDir), { withFileTypes: true });
|
|
121
|
+
} catch {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
for (const e of entries) {
|
|
125
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
126
|
+
const childRel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
127
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
128
|
+
if (e.name === ".agents") {
|
|
129
|
+
probeAgents(root, childRel, found);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
walk(childRel);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
walk("");
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
function probeAgents(root, agentsRel, found) {
|
|
139
|
+
const single = `${agentsRel}/spec/spec.md`;
|
|
140
|
+
if (existsSync(join(root, single))) found.push(single);
|
|
141
|
+
if (agentsRel === ".agents") {
|
|
142
|
+
const specsDir = join(root, ".agents", "specs");
|
|
143
|
+
let projects;
|
|
144
|
+
try {
|
|
145
|
+
projects = readdirSync(specsDir, { withFileTypes: true });
|
|
146
|
+
} catch {
|
|
147
|
+
projects = [];
|
|
148
|
+
}
|
|
149
|
+
for (const p of projects) {
|
|
150
|
+
if (!p.isDirectory()) continue;
|
|
151
|
+
const rel = `.agents/specs/${p.name}/spec.md`;
|
|
152
|
+
if (existsSync(join(root, rel))) found.push(rel);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function parseAnchorsToml(text) {
|
|
157
|
+
const m = /(^|\n)\s*anchors\s*=\s*\[([\s\S]*?)\]/.exec(text);
|
|
158
|
+
if (!m) return [];
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const q of m[2].matchAll(/"([^"]*)"|'([^']*)'/g)) out.push((q[1] ?? q[2]).trim());
|
|
161
|
+
return out.filter((s) => s !== "");
|
|
162
|
+
}
|
|
163
|
+
function readAnchors(root) {
|
|
164
|
+
const file = join(root, ANCHORS_CONFIG);
|
|
165
|
+
if (!existsSync(file)) return [];
|
|
166
|
+
try {
|
|
167
|
+
return parseAnchorsToml(readFileSync(file, "utf8"));
|
|
168
|
+
} catch {
|
|
169
|
+
process.stderr.write(`discover-specs: ignoring unreadable ${ANCHORS_CONFIG}\n`);
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function collectDescendants(root, startDir) {
|
|
174
|
+
const out = [startDir];
|
|
175
|
+
let entries;
|
|
176
|
+
try {
|
|
177
|
+
entries = readdirSync(join(root, startDir), { withFileTypes: true });
|
|
178
|
+
} catch {
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
for (const e of entries) {
|
|
182
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
183
|
+
const childRel = startDir ? `${startDir}/${e.name}` : e.name;
|
|
184
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
185
|
+
out.push(...collectDescendants(root, childRel));
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
function expandAnchor(root, pattern) {
|
|
190
|
+
const segs = pattern.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "").split("/").filter(Boolean);
|
|
191
|
+
let frontier = [{ dir: "" }];
|
|
192
|
+
for (const seg of segs) {
|
|
193
|
+
const next = [];
|
|
194
|
+
if (seg === "**") {
|
|
195
|
+
for (const node of frontier) for (const dir of collectDescendants(root, node.dir)) next.push({
|
|
196
|
+
dir,
|
|
197
|
+
capturedName: node.capturedName
|
|
198
|
+
});
|
|
199
|
+
frontier = next;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const isGlob = seg === "*" || seg === "<project>";
|
|
203
|
+
for (const node of frontier) if (isGlob) {
|
|
204
|
+
let entries;
|
|
205
|
+
try {
|
|
206
|
+
entries = readdirSync(join(root, node.dir), { withFileTypes: true });
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
for (const e of entries) {
|
|
211
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
212
|
+
const childRel = node.dir ? `${node.dir}/${e.name}` : e.name;
|
|
213
|
+
if (isNestedCheckout(join(root, childRel))) continue;
|
|
214
|
+
next.push({
|
|
215
|
+
dir: childRel,
|
|
216
|
+
capturedName: seg === "<project>" ? e.name : node.capturedName
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
} else next.push({
|
|
220
|
+
dir: node.dir ? `${node.dir}/${seg}` : seg,
|
|
221
|
+
capturedName: node.capturedName
|
|
222
|
+
});
|
|
223
|
+
frontier = next;
|
|
224
|
+
}
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const node of frontier) {
|
|
227
|
+
const rel = node.dir ? `${node.dir}/spec.md` : "spec.md";
|
|
228
|
+
if (existsSync(join(root, rel))) out.push({
|
|
229
|
+
rel,
|
|
230
|
+
capturedName: node.capturedName
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
}
|
|
235
|
+
function recordFor(root, rel, loc) {
|
|
236
|
+
let fm;
|
|
237
|
+
try {
|
|
238
|
+
fm = parseFrontmatter(readFileSync(join(root, rel), "utf8"));
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
if (!fm?.status || !LIFECYCLE_STATUSES.has(fm.status)) return null;
|
|
243
|
+
const { name, nameSource } = deriveName(loc, fm);
|
|
244
|
+
return {
|
|
245
|
+
path: rel.replace(/\/spec\.md$/, ""),
|
|
246
|
+
name,
|
|
247
|
+
nameSource,
|
|
248
|
+
status: fm.status,
|
|
249
|
+
projectPath: fm.projectPath ?? "",
|
|
250
|
+
approvals: Object.entries(fm.approval).map(([gate, verdict]) => `${gate}:${verdict}`).join(";")
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function collectSpecs(root) {
|
|
254
|
+
const out = [];
|
|
255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
256
|
+
for (const rel of discoverSpecFiles(root)) {
|
|
257
|
+
const loc = classifyLocation(rel);
|
|
258
|
+
if (!loc) continue;
|
|
259
|
+
const rec = recordFor(root, rel, loc);
|
|
260
|
+
if (rec) {
|
|
261
|
+
out.push(rec);
|
|
262
|
+
seen.add(rel);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
for (const pattern of readAnchors(root)) for (const { rel, capturedName } of expandAnchor(root, pattern)) {
|
|
266
|
+
if (seen.has(rel)) continue;
|
|
267
|
+
const rec = recordFor(root, rel, {
|
|
268
|
+
pattern: "extra",
|
|
269
|
+
locationDir: rel.replace(/\/spec\.md$/, ""),
|
|
270
|
+
capturedName
|
|
271
|
+
});
|
|
272
|
+
if (rec) {
|
|
273
|
+
out.push(rec);
|
|
274
|
+
seen.add(rel);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
278
|
+
}
|
|
279
|
+
if (process.argv[1] && false);
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/sibling-engine.mts
|
|
282
|
+
/** The path of `<skill>/<engine>` for an engine running from the file `here`. */
|
|
283
|
+
function siblingEngine(here, skill, engine) {
|
|
284
|
+
if (here.endsWith(".mts")) return join(dirname(here), "..", skill, `${engine}.mts`);
|
|
285
|
+
return join(dirname(here), "..", "..", skill, "scripts", `${engine}.mjs`);
|
|
286
|
+
}
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/check-project-specs/check-project-specs.mts
|
|
289
|
+
const HERE = fileURLToPath(import.meta.url);
|
|
290
|
+
const ENGINES = [
|
|
291
|
+
{
|
|
292
|
+
name: "check-spec-state",
|
|
293
|
+
skill: "spec-gate",
|
|
294
|
+
engine: "check-spec-state",
|
|
295
|
+
args: (d) => ["--root", d]
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
name: "check-suite",
|
|
299
|
+
skill: "spec-gate",
|
|
300
|
+
engine: "check-suite",
|
|
301
|
+
args: (d) => ["--root", d]
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "concept-index",
|
|
305
|
+
skill: "concept-index",
|
|
306
|
+
engine: "concept-index",
|
|
307
|
+
args: (d) => [
|
|
308
|
+
"--spec-dir",
|
|
309
|
+
d,
|
|
310
|
+
"--check"
|
|
311
|
+
]
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "check-spec-structure",
|
|
315
|
+
skill: "check-spec-structure",
|
|
316
|
+
engine: "check-spec-structure",
|
|
317
|
+
args: (d) => [
|
|
318
|
+
"--spec-dir",
|
|
319
|
+
d,
|
|
320
|
+
"--check"
|
|
321
|
+
]
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: "align-spec",
|
|
325
|
+
skill: "align-spec",
|
|
326
|
+
engine: "align-spec",
|
|
327
|
+
args: (d) => [
|
|
328
|
+
"--spec-dir",
|
|
329
|
+
d,
|
|
330
|
+
"--check"
|
|
331
|
+
]
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
name: "check-spec-references",
|
|
335
|
+
skill: "check-spec-references",
|
|
336
|
+
engine: "check-spec-references",
|
|
337
|
+
args: (d) => ["--spec-dir", d]
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
name: "check-scenario-overlap",
|
|
341
|
+
skill: "check-scenario-overlap",
|
|
342
|
+
engine: "check-scenario-overlap",
|
|
343
|
+
args: (d) => [
|
|
344
|
+
"--spec-dir",
|
|
345
|
+
d,
|
|
346
|
+
"--check"
|
|
347
|
+
]
|
|
348
|
+
}
|
|
349
|
+
];
|
|
350
|
+
/** Where an engine runs from: beside this one, in the same form (see sibling-engine.mts). */
|
|
351
|
+
function enginePath(e, here = HERE) {
|
|
352
|
+
return siblingEngine(here, e.skill, e.engine);
|
|
353
|
+
}
|
|
354
|
+
/** Walk up from `start` for the workspace marker. Returns '' when not found. */
|
|
355
|
+
function findRepoRoot(start) {
|
|
356
|
+
let dir = resolve(start);
|
|
357
|
+
for (;;) {
|
|
358
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
|
|
359
|
+
const up = dirname(dir);
|
|
360
|
+
if (up === dir) return "";
|
|
361
|
+
dir = up;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Find the spec whose `project-path` names `projectRel` (a repo-relative dir).
|
|
366
|
+
* A project with no spec is `none` — not an error: the script is uniform across
|
|
367
|
+
* every workspace member, and some members are governed by no spec.
|
|
368
|
+
*/
|
|
369
|
+
function resolveSpecFor(specs, projectRel) {
|
|
370
|
+
const hits = specs.filter((s) => s.projectPath !== "" && s.projectPath === projectRel);
|
|
371
|
+
if (hits.length === 0) return { kind: "none" };
|
|
372
|
+
if (hits.length > 1) return {
|
|
373
|
+
kind: "ambiguous",
|
|
374
|
+
specs: hits
|
|
375
|
+
};
|
|
376
|
+
return {
|
|
377
|
+
kind: "resolved",
|
|
378
|
+
spec: hits[0]
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Every spec must be reachable from a project that actually checks it. Without
|
|
383
|
+
* this, a spec silently goes unchecked the moment its project drops the script —
|
|
384
|
+
* which is exactly how the corpus ended up with one audited project out of ten.
|
|
385
|
+
*
|
|
386
|
+
* `specFiles` are the spec.md paths found at the recognized locations *before*
|
|
387
|
+
* the lifecycle-status filter; `specs` are the ones that survived it. A file in
|
|
388
|
+
* the first set and not the second is a spec whose status is not in the enum:
|
|
389
|
+
* discovery drops it, so every engine silently skips it and it is checked by
|
|
390
|
+
* nothing. That is a gap to escalate, not to exempt.
|
|
391
|
+
*/
|
|
392
|
+
function findCoverageGaps(root, specFiles, specs, readPkg) {
|
|
393
|
+
const gaps = [];
|
|
394
|
+
const recognized = new Set(specs.map((s) => s.path === "" ? "spec.md" : `${s.path}/spec.md`));
|
|
395
|
+
for (const f of specFiles) if (!recognized.has(f)) gaps.push({
|
|
396
|
+
spec: f,
|
|
397
|
+
projectPath: "",
|
|
398
|
+
reason: "unrecognized"
|
|
399
|
+
});
|
|
400
|
+
for (const s of specs) {
|
|
401
|
+
if (s.projectPath === "") {
|
|
402
|
+
gaps.push({
|
|
403
|
+
spec: s.path,
|
|
404
|
+
projectPath: "",
|
|
405
|
+
reason: "no-project-path"
|
|
406
|
+
});
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const pkg = readPkg(join(root, s.projectPath, "package.json"));
|
|
410
|
+
if (!pkg) {
|
|
411
|
+
gaps.push({
|
|
412
|
+
spec: s.path,
|
|
413
|
+
projectPath: s.projectPath,
|
|
414
|
+
reason: "no-manifest"
|
|
415
|
+
});
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (!pkg.scripts?.["check:spec"]) gaps.push({
|
|
419
|
+
spec: s.path,
|
|
420
|
+
projectPath: s.projectPath,
|
|
421
|
+
reason: "no-check-script"
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return gaps;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* A spec.md sitting at a recognized location that discovery DROPPED (its status is not
|
|
428
|
+
* in the lifecycle enum) and that belongs to `projectRel` — by the `project-path` it
|
|
429
|
+
* declares, or by sitting inside the project (`<project>/.agents/spec/spec.md`, which
|
|
430
|
+
* survives frontmatter corruption because it is location-derived).
|
|
431
|
+
*
|
|
432
|
+
* Without this, a per-project run resolves such a spec to `none` and prints "no spec
|
|
433
|
+
* governs <project> — skipped" with exit 0: a status typo silently exempts the whole
|
|
434
|
+
* project from every engine. A spec that exists but cannot be classified is escalated,
|
|
435
|
+
* not exempted — the same call the corpus scope’s coverage guard makes.
|
|
436
|
+
*/
|
|
437
|
+
function findDroppedSpecFor(specFiles, specs, projectRel, readText) {
|
|
438
|
+
const recognized = new Set(specs.map((s) => s.path === "" ? "spec.md" : `${s.path}/spec.md`));
|
|
439
|
+
const out = [];
|
|
440
|
+
for (const f of specFiles) {
|
|
441
|
+
if (recognized.has(f)) continue;
|
|
442
|
+
const text = readText(f);
|
|
443
|
+
const fm = text === null ? null : parseFrontmatter(text);
|
|
444
|
+
const dir = f.replace(/(^|\/)spec\.md$/, "");
|
|
445
|
+
const nested = /^(.+)\/\.agents\/spec$/.exec(dir)?.[1] ?? "";
|
|
446
|
+
if (fm?.projectPath === projectRel || nested === projectRel) out.push({
|
|
447
|
+
file: f,
|
|
448
|
+
status: fm?.status ?? ""
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
const REASON_TEXT = {
|
|
454
|
+
unrecognized: "sits at a spec location but its status is not in the lifecycle enum, so discovery drops it and nothing checks it",
|
|
455
|
+
"no-project-path": "declares no project-path, so no project can be resolved to check it",
|
|
456
|
+
"no-manifest": "names a project with no package.json, so it is not a workspace member",
|
|
457
|
+
"no-check-script": "names a project that defines no `check:spec` script"
|
|
458
|
+
};
|
|
459
|
+
/** Read a file, or null when it cannot be read. */
|
|
460
|
+
function readTextOrNull(path) {
|
|
461
|
+
try {
|
|
462
|
+
return readFileSync(path, "utf8");
|
|
463
|
+
} catch {
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Run the whole engine set against one project-spec dir. Every engine runs even
|
|
469
|
+
* after one fails: a run that stops at the first failure reports one defect per
|
|
470
|
+
* invocation, and the corpus scope exists to report the tree's defects in one go.
|
|
471
|
+
*/
|
|
472
|
+
function runEngines(repoRoot, specDir) {
|
|
473
|
+
let failed = 0;
|
|
474
|
+
for (const e of ENGINES) try {
|
|
475
|
+
execFileSync("node", [enginePath(e), ...e.args(specDir)], {
|
|
476
|
+
cwd: repoRoot,
|
|
477
|
+
stdio: "inherit"
|
|
478
|
+
});
|
|
479
|
+
process.stdout.write(` ok ${e.name}\n`);
|
|
480
|
+
} catch {
|
|
481
|
+
process.stderr.write(` FAIL ${e.name}\n`);
|
|
482
|
+
failed++;
|
|
483
|
+
}
|
|
484
|
+
return failed;
|
|
485
|
+
}
|
|
486
|
+
function checkCoverage(root) {
|
|
487
|
+
const gaps = findCoverageGaps(root, discoverSpecFiles(root), collectSpecs(root), (p) => {
|
|
488
|
+
try {
|
|
489
|
+
return JSON.parse(readFileSync(p, "utf8"));
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
if (gaps.length === 0) {
|
|
495
|
+
process.stdout.write("check-project-specs: every spec is checked by its project\n");
|
|
496
|
+
return 0;
|
|
497
|
+
}
|
|
498
|
+
for (const g of gaps) process.stderr.write(` ${g.spec} — ${REASON_TEXT[g.reason]}\n`);
|
|
499
|
+
process.stderr.write(`check-project-specs: ${gaps.length} spec(s) no project checks\n`);
|
|
500
|
+
return 1;
|
|
501
|
+
}
|
|
502
|
+
/** The flags the harness defines. Anything else is an error, never a default. */
|
|
503
|
+
const KNOWN_FLAGS = ["--corpus", "--project"];
|
|
504
|
+
/**
|
|
505
|
+
* The scope is chosen by a flag, so an unrecognized one must not fall through to a
|
|
506
|
+
* default. It used to: project scope at a repo root resolves no governing spec and
|
|
507
|
+
* exits 0, so a misspelled or retired flag reported success having checked nothing.
|
|
508
|
+
* That is how `--check-coverage` guarded commits and CI while running no engine.
|
|
509
|
+
*/
|
|
510
|
+
function unknownFlags(argv) {
|
|
511
|
+
const out = [];
|
|
512
|
+
for (let i = 0; i < argv.length; i++) {
|
|
513
|
+
const a = argv[i];
|
|
514
|
+
if (!a.startsWith("-")) continue;
|
|
515
|
+
if (a === "--project") {
|
|
516
|
+
i++;
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
if (!KNOWN_FLAGS.includes(a)) out.push(a);
|
|
520
|
+
}
|
|
521
|
+
return out;
|
|
522
|
+
}
|
|
523
|
+
function main(argv) {
|
|
524
|
+
const unknown = unknownFlags(argv);
|
|
525
|
+
if (unknown.length) {
|
|
526
|
+
process.stderr.write(`check-project-specs: unrecognized flag(s) ${unknown.join(", ")} — the scope is ${KNOWN_FLAGS.join(" | ")} (or none, meaning the cwd's project)\n`);
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
if (argv.includes("--corpus") && argv.includes("--project")) {
|
|
530
|
+
process.stderr.write("check-project-specs: --corpus and --project name contradictory scopes — pass one\n");
|
|
531
|
+
return 1;
|
|
532
|
+
}
|
|
533
|
+
if (argv.includes("--corpus")) return checkCorpus();
|
|
534
|
+
return checkProject(argv);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Corpus scope — the commit floor. Total by definition: every project-spec the
|
|
538
|
+
* corpus holds is checked, and a spec that cannot be classified fails rather than
|
|
539
|
+
* being skipped.
|
|
540
|
+
*
|
|
541
|
+
* It runs BOTH sub-checks because neither subsumes the other. The sweep iterates
|
|
542
|
+
* the specs discovery *recognizes*, so a spec whose lifecycle `status` is a typo is
|
|
543
|
+
* invisible to it — and would report clean. The coverage guard sees that file on
|
|
544
|
+
* disk and escalates it, but says nothing about whether the engines pass. Run either
|
|
545
|
+
* alone and a whole project-spec leaves the floor silently.
|
|
546
|
+
*/
|
|
547
|
+
function checkCorpus() {
|
|
548
|
+
const root = findRepoRoot(process.cwd());
|
|
549
|
+
if (!root) {
|
|
550
|
+
process.stderr.write("check-project-specs: no pnpm-workspace.yaml found above the cwd\n");
|
|
551
|
+
return 1;
|
|
552
|
+
}
|
|
553
|
+
let failed = checkCoverage(root) === 0 ? 0 : 1;
|
|
554
|
+
const specs = collectSpecs(root);
|
|
555
|
+
if (specs.length === 0) {
|
|
556
|
+
process.stdout.write("check-project-specs: the corpus holds no project-spec\n");
|
|
557
|
+
return failed;
|
|
558
|
+
}
|
|
559
|
+
for (const s of specs) {
|
|
560
|
+
process.stdout.write(`check-project-specs: ${s.path}\n`);
|
|
561
|
+
if (runEngines(root, join(root, s.path)) > 0) failed = 1;
|
|
562
|
+
}
|
|
563
|
+
return failed;
|
|
564
|
+
}
|
|
565
|
+
function checkProject(argv) {
|
|
566
|
+
const projectArg = argv.includes("--project") ? argv[argv.indexOf("--project") + 1] : void 0;
|
|
567
|
+
const projectDir = resolve(projectArg ?? process.cwd());
|
|
568
|
+
const repoRoot = findRepoRoot(projectDir);
|
|
569
|
+
if (!repoRoot) {
|
|
570
|
+
process.stderr.write(`check-project-specs: no pnpm-workspace.yaml found above ${projectDir}\n`);
|
|
571
|
+
return 1;
|
|
572
|
+
}
|
|
573
|
+
const projectRel = relative(repoRoot, projectDir) || ".";
|
|
574
|
+
const specs = collectSpecs(repoRoot);
|
|
575
|
+
const res = resolveSpecFor(specs, projectRel);
|
|
576
|
+
if (res.kind === "none") {
|
|
577
|
+
const dropped = findDroppedSpecFor(discoverSpecFiles(repoRoot), specs, projectRel, (rel) => readTextOrNull(join(repoRoot, rel)));
|
|
578
|
+
for (const d of dropped) process.stderr.write(`check-project-specs: \`${d.file}\` sits at a spec location and governs \`${projectRel}\` but ` + (d.status === "" ? "declares no lifecycle status" : `its status \`${d.status}\` is not in the lifecycle enum`) + " (draft | approved | implemented | deprecated) — discovery drops it, so it is checked by nothing\n");
|
|
579
|
+
if (dropped.length) return 1;
|
|
580
|
+
process.stdout.write(`check-project-specs: no spec governs \`${projectRel}\` — skipped\n`);
|
|
581
|
+
return 0;
|
|
582
|
+
}
|
|
583
|
+
if (res.kind === "ambiguous") {
|
|
584
|
+
process.stderr.write(`check-project-specs: \`${projectRel}\` is claimed by ${res.specs.length} specs (${res.specs.map((s) => s.path).join(", ")}) — a project has exactly one spec\n`);
|
|
585
|
+
return 1;
|
|
586
|
+
}
|
|
587
|
+
const specDir = join(repoRoot, res.spec.path);
|
|
588
|
+
process.stdout.write(`check-project-specs: ${projectRel} -> ${res.spec.path}\n`);
|
|
589
|
+
return runEngines(repoRoot, specDir) === 0 ? 0 : 1;
|
|
590
|
+
}
|
|
591
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
592
|
+
//#endregion
|
|
593
|
+
export { ENGINES, KNOWN_FLAGS, enginePath, findCoverageGaps, findDroppedSpecFor, findRepoRoot, main, resolveSpecFor, unknownFlags };
|
|
@@ -13,8 +13,7 @@ retires a path, directory, or naming convention, and records it once in
|
|
|
13
13
|
`.agents/sdd/retired-terms.toml`. This engine guards that the retirement holds: it scans every
|
|
14
14
|
git-tracked file in the repo — not only the node someone happened to touch — for a literal,
|
|
15
15
|
case-sensitive occurrence of a registered term, and reports every survivor as `file:line:term` with
|
|
16
|
-
the replacement to use. It carries a self-contained `.
|
|
17
|
-
convention), parsing the registry with the same minimal hand-rolled TOML subset `discover-specs`
|
|
16
|
+
the replacement to use. It carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11), parsing the registry with the same minimal hand-rolled TOML subset `discover-specs`
|
|
18
17
|
already uses for `spec-anchors.toml`.
|
|
19
18
|
|
|
20
19
|
It is the corpus-wide, declared-data sibling of `check:metaphor-free`
|
|
@@ -67,7 +66,7 @@ node "<skill>/scripts/check-retired-terms.mjs" [--root .] --list # what is regi
|
|
|
67
66
|
non-zero, because the registry *is* the check.
|
|
68
67
|
- **`--list`** prints each registered term with its `since` and `replacement`, and exits 0. With no
|
|
69
68
|
registry, it states plainly that nothing is registered — a definitive empty state, not silence.
|
|
70
|
-
- Wired into `check:specs` (`node …/check-retired-terms.
|
|
69
|
+
- Wired into `check:specs` (`node …/check-retired-terms.mjs --root .`), so it runs on every
|
|
71
70
|
`pnpm verify` and in CI.
|
|
72
71
|
|
|
73
72
|
When `node` is absent, an agent performs the same derivation by hand: read
|