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,251 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import { join, relative, sep } from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
//#region skills/check-field-mandates/scripts/check-field-mandates.mts
|
|
6
|
-
/** UPPER_CASE, or snake_case with at least one underscore — a bare lowercase word is prose. */
|
|
7
|
-
const TOKEN = "(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)";
|
|
8
|
-
const TOKEN_ITEM = `${TOKEN}(?:\\(s\\))?`;
|
|
9
|
-
/** A block line opening with a comma list of tokens, then a colon, a trailing comment, or nothing. */
|
|
10
|
-
const BLOCK_DECLARATION_RE = new RegExp(`^\\s*(${TOKEN_ITEM}(?:\\s*,\\s*${TOKEN_ITEM})*)\\s*(:.*|#.*)?$`);
|
|
11
|
-
/** A list item opening with one or more bold tokens joined by `+`. */
|
|
12
|
-
const LIST_DECLARATION_RE = new RegExp(`^\\s*[-*+]\\s+(\\*\\*${TOKEN}\\*\\*(?:\\s*\\+\\s*\\*\\*${TOKEN}\\*\\*)*)(.*)$`);
|
|
13
|
-
const BOLD_TOKEN_RE = new RegExp(`\\*\\*(${TOKEN})\\*\\*`, "g");
|
|
14
|
-
const WORD_RE = new RegExp(`(?<![A-Za-z0-9_])${TOKEN}(?![A-Za-z0-9_])`, "g");
|
|
15
|
-
const CODE_SPAN_RE = /`([^`\n]+)`/g;
|
|
16
|
-
const MANDATE_LEAD_RE = new RegExp(`^<?(${TOKEN})(?=$|[\\s.:\\[\\]>(,=])`);
|
|
17
|
-
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
|
|
18
|
-
/** The marker must carry a reason — a bare marker excuses nothing. */
|
|
19
|
-
const IGNORE_MARKER_RE = /<!--\s*field-mandate-ignore\s*:\s*[^\s>-][^>]*-->/;
|
|
20
|
-
/** A field token is two or more characters: a lone capital is a word, not a field. */
|
|
21
|
-
function isField(token) {
|
|
22
|
-
return token.length >= 2;
|
|
23
|
-
}
|
|
24
|
-
/** A token's case-folded form: the spelling-independent name of the field. */
|
|
25
|
-
function fold(token) {
|
|
26
|
-
return token.toUpperCase();
|
|
27
|
-
}
|
|
28
|
-
/** The folded forms a token may take and still be the same field — equal, or one trailing `S` apart. */
|
|
29
|
-
function sameFieldForms(token) {
|
|
30
|
-
const t = fold(token);
|
|
31
|
-
const forms = [t, `${t}S`];
|
|
32
|
-
if (t.endsWith("S") && t.length > 2) forms.push(t.slice(0, -1));
|
|
33
|
-
return forms;
|
|
34
|
-
}
|
|
35
|
-
/** Does the set of folded tokens hold the same field as `token`? */
|
|
36
|
-
function hasSameField(folded, token) {
|
|
37
|
-
return sameFieldForms(token).some((f) => folded.has(f));
|
|
38
|
-
}
|
|
39
|
-
/** Is the token spelled lowercase (snake_case) rather than uppercase? */
|
|
40
|
-
function isLower(token) {
|
|
41
|
-
return token !== fold(token);
|
|
42
|
-
}
|
|
43
|
-
function parseDefinition(text) {
|
|
44
|
-
const lines = text.split(/\r?\n/);
|
|
45
|
-
const declarations = [];
|
|
46
|
-
const mandates = [];
|
|
47
|
-
const proseWords = /* @__PURE__ */ new Set();
|
|
48
|
-
let i = 0;
|
|
49
|
-
if (lines[0]?.trim() === "---") {
|
|
50
|
-
const end = lines.findIndex((l, n) => n > 0 && l.trim() === "---");
|
|
51
|
-
if (end > 0) i = end + 1;
|
|
52
|
-
}
|
|
53
|
-
let fence = null;
|
|
54
|
-
let group = [];
|
|
55
|
-
/** Record a span of text as prose: its field-shaped words, and any code-span mandate. */
|
|
56
|
-
function scanProse(text, lineNo) {
|
|
57
|
-
for (const m of text.matchAll(WORD_RE)) if (isField(m[0])) proseWords.add(m[0]);
|
|
58
|
-
if (IGNORE_MARKER_RE.test(text)) return;
|
|
59
|
-
for (const m of text.matchAll(CODE_SPAN_RE)) {
|
|
60
|
-
const token = (m[1] ?? "").trim().match(MANDATE_LEAD_RE)?.[1];
|
|
61
|
-
if (token && isField(token)) mandates.push({
|
|
62
|
-
token,
|
|
63
|
-
line: lineNo
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
for (; i < lines.length; i++) {
|
|
68
|
-
const line = lines[i] ?? "";
|
|
69
|
-
const lineNo = i + 1;
|
|
70
|
-
const fenceMatch = line.match(FENCE_RE);
|
|
71
|
-
if (fence) {
|
|
72
|
-
if (fenceMatch && fenceMatch[1]?.[0] === fence.char && (fenceMatch[1]?.length ?? 0) >= fence.length && fenceMatch[2]?.trim() === "") {
|
|
73
|
-
fence = null;
|
|
74
|
-
group = [];
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (!fence.structured) continue;
|
|
78
|
-
const decl = line.match(BLOCK_DECLARATION_RE);
|
|
79
|
-
const tokens = decl ? (decl[1] ?? "").split(",").map((t) => t.trim().replace(/\(s\)$/, "")) : [];
|
|
80
|
-
const rest = decl?.[2];
|
|
81
|
-
if (decl !== null && (rest?.startsWith(":") || tokens.length >= 2)) {
|
|
82
|
-
const glossed = rest?.startsWith(":") === true && rest.slice(1).trim() !== "";
|
|
83
|
-
group = tokens.filter(isField).map((token) => ({
|
|
84
|
-
token,
|
|
85
|
-
line: lineNo,
|
|
86
|
-
glossed
|
|
87
|
-
}));
|
|
88
|
-
declarations.push(...group);
|
|
89
|
-
} else if (line.trim() !== "") for (const d of group) d.glossed = true;
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (fenceMatch) {
|
|
93
|
-
const info = fenceMatch[2]?.trim() ?? "";
|
|
94
|
-
fence = {
|
|
95
|
-
char: fenceMatch[1]?.[0] ?? "`",
|
|
96
|
-
length: fenceMatch[1]?.length ?? 3,
|
|
97
|
-
structured: info === "" || info === "text"
|
|
98
|
-
};
|
|
99
|
-
group = [];
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const listDecl = line.match(LIST_DECLARATION_RE);
|
|
103
|
-
if (listDecl) {
|
|
104
|
-
const rest = listDecl[2] ?? "";
|
|
105
|
-
const glossed = rest.trim() !== "";
|
|
106
|
-
for (const m of (listDecl[1] ?? "").matchAll(BOLD_TOKEN_RE)) {
|
|
107
|
-
const token = m[1] ?? "";
|
|
108
|
-
if (isField(token)) declarations.push({
|
|
109
|
-
token,
|
|
110
|
-
line: lineNo,
|
|
111
|
-
glossed
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
scanProse(rest, lineNo);
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
scanProse(line, lineNo);
|
|
118
|
-
}
|
|
119
|
-
return {
|
|
120
|
-
declarations,
|
|
121
|
-
mandates,
|
|
122
|
-
proseWords
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function walk(dir, out) {
|
|
126
|
-
let entries;
|
|
127
|
-
try {
|
|
128
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
129
|
-
} catch {
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
for (const e of entries) {
|
|
133
|
-
const full = join(dir, e.name);
|
|
134
|
-
if (e.isDirectory()) walk(full, out);
|
|
135
|
-
else if (e.isFile() && e.name.endsWith(".md")) out.push(full);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/** Is this root-relative path a skill (`plugins/<p>/skills/**\/SKILL.md`) or agent (`plugins/<p>/agents/*.md`)? */
|
|
139
|
-
function isDefinitionPath(rel) {
|
|
140
|
-
const parts = rel.split("/");
|
|
141
|
-
if (parts[0] !== "plugins" || parts.length < 4) return false;
|
|
142
|
-
if (parts[2] === "agents") return parts.length === 4 && (parts[3] ?? "").endsWith(".md");
|
|
143
|
-
if (parts[2] === "skills") return parts.length >= 5 && parts[parts.length - 1] === "SKILL.md";
|
|
144
|
-
return false;
|
|
145
|
-
}
|
|
146
|
-
/** Every definition under `root`, root-relative, `/`-separated, path-sorted. */
|
|
147
|
-
function discoverDefinitions(root) {
|
|
148
|
-
const pluginsDir = join(root, "plugins");
|
|
149
|
-
if (!existsSync(pluginsDir)) return [];
|
|
150
|
-
const files = [];
|
|
151
|
-
walk(pluginsDir, files);
|
|
152
|
-
return files.map((f) => relative(root, f).split(sep).join("/")).filter(isDefinitionPath).sort();
|
|
153
|
-
}
|
|
154
|
-
function check(parsed) {
|
|
155
|
-
const known = /* @__PURE__ */ new Set();
|
|
156
|
-
for (const p of parsed.values()) for (const d of p.declarations) known.add(fold(d.token));
|
|
157
|
-
const findings = [];
|
|
158
|
-
for (const [file, p] of parsed) {
|
|
159
|
-
if (p.declarations.length === 0) continue;
|
|
160
|
-
const declared = new Set(p.declarations.map((d) => fold(d.token)));
|
|
161
|
-
const proseWords = new Set([...p.proseWords].map(fold));
|
|
162
|
-
const seen = /* @__PURE__ */ new Set();
|
|
163
|
-
for (const d of p.declarations) {
|
|
164
|
-
if (seen.has(d.token)) continue;
|
|
165
|
-
seen.add(d.token);
|
|
166
|
-
if (!p.declarations.some((o) => o.token === d.token && o.glossed) && !hasSameField(proseWords, d.token)) findings.push({
|
|
167
|
-
kind: "unexplained",
|
|
168
|
-
file,
|
|
169
|
-
line: d.line,
|
|
170
|
-
token: d.token
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
const reported = /* @__PURE__ */ new Set();
|
|
174
|
-
for (const m of p.mandates) {
|
|
175
|
-
if (!hasSameField(known, m.token)) continue;
|
|
176
|
-
const key = `${m.line}:${m.token}`;
|
|
177
|
-
if (reported.has(key)) continue;
|
|
178
|
-
if (!hasSameField(declared, m.token)) {
|
|
179
|
-
reported.add(key);
|
|
180
|
-
findings.push({
|
|
181
|
-
kind: "undeclared",
|
|
182
|
-
file,
|
|
183
|
-
line: m.line,
|
|
184
|
-
token: m.token
|
|
185
|
-
});
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const forms = sameFieldForms(m.token);
|
|
189
|
-
const same = p.declarations.filter((d) => forms.includes(fold(d.token)));
|
|
190
|
-
if (same.some((d) => isLower(d.token) === isLower(m.token))) continue;
|
|
191
|
-
reported.add(key);
|
|
192
|
-
findings.push({
|
|
193
|
-
kind: "miscased",
|
|
194
|
-
file,
|
|
195
|
-
line: m.line,
|
|
196
|
-
token: m.token,
|
|
197
|
-
declared: same[0]?.token
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
return findings.sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1);
|
|
202
|
-
}
|
|
203
|
-
const DETAIL = {
|
|
204
|
-
unexplained: "declared in a block, but no gloss and no prose explains it",
|
|
205
|
-
undeclared: "mandated in the prose, but no block in this file declares it",
|
|
206
|
-
miscased: "mandated in the prose, but this file declares it in the other case"
|
|
207
|
-
};
|
|
208
|
-
function formatReport(definitions, findings) {
|
|
209
|
-
if (definitions.length === 0) return "check-field-mandates: no skill or agent definition found\n";
|
|
210
|
-
if (findings.length === 0) return `check-field-mandates: ${definitions.length} definition(s) OK\n`;
|
|
211
|
-
return `${findings.map((f) => ` ${f.kind.padEnd(11)} ${f.file}:${f.line} ${f.token} — ${DETAIL[f.kind]}${f.declared ? ` (${f.declared})` : ""}`).join("\n")}\ncheck-field-mandates: ${findings.length} finding(s) across ${definitions.length} definition(s)\n`;
|
|
212
|
-
}
|
|
213
|
-
function isReadableDirectory(path) {
|
|
214
|
-
try {
|
|
215
|
-
if (!statSync(path).isDirectory()) return false;
|
|
216
|
-
readdirSync(path);
|
|
217
|
-
return true;
|
|
218
|
-
} catch {
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
function main(argv) {
|
|
223
|
-
let root = ".";
|
|
224
|
-
for (let i = 0; i < argv.length; i++) {
|
|
225
|
-
const a = argv[i];
|
|
226
|
-
if (a === "--root") {
|
|
227
|
-
const next = argv[++i];
|
|
228
|
-
if (next === void 0) {
|
|
229
|
-
process.stderr.write("check-field-mandates: --root needs a directory\n");
|
|
230
|
-
return 1;
|
|
231
|
-
}
|
|
232
|
-
root = next;
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
process.stderr.write(`check-field-mandates: unrecognized flag ${a}\n`);
|
|
236
|
-
return 1;
|
|
237
|
-
}
|
|
238
|
-
if (!isReadableDirectory(root)) {
|
|
239
|
-
process.stderr.write(`check-field-mandates: root ${root} is not a readable directory\n`);
|
|
240
|
-
return 1;
|
|
241
|
-
}
|
|
242
|
-
const definitions = discoverDefinitions(root);
|
|
243
|
-
const parsed = /* @__PURE__ */ new Map();
|
|
244
|
-
for (const rel of definitions) parsed.set(rel, parseDefinition(readFileSync(join(root, rel), "utf8")));
|
|
245
|
-
const findings = check(parsed);
|
|
246
|
-
process.stdout.write(formatReport(definitions, findings));
|
|
247
|
-
return findings.length === 0 ? 0 : 1;
|
|
248
|
-
}
|
|
249
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
|
|
250
|
-
//#endregion
|
|
251
|
-
export { check, discoverDefinitions, formatReport, isDefinitionPath, main, parseDefinition };
|
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// check-field-mandates — a definition's declared fields and its prose must agree.
|
|
3
|
-
//
|
|
4
|
-
// Skill and agent definitions ship structured blocks (an Input, an Output, a dispatch payload)
|
|
5
|
-
// whose fields the surrounding prose also mandates. Nothing else compares the two, so a field can be
|
|
6
|
-
// mandated and never carried, or carried and never explained, and both read as complete. This
|
|
7
|
-
// engine diffs the token sets (spec: .agents/specs/sdd/plugin/check-field-mandates/):
|
|
8
|
-
// unexplained — a block declares a field that carries no gloss and the prose never names
|
|
9
|
-
// undeclared — the prose mandates a known field (a code span) that none of the file's blocks declare
|
|
10
|
-
// miscased — the prose mandates a field this file declares, but spelled in the other case
|
|
11
|
-
//
|
|
12
|
-
// A field is explained by a gloss OR by the prose — requiring the prose to repeat a glossed
|
|
13
|
-
// declaration would demand filler in every definition. A mandate is a code span naming a KNOWN field
|
|
14
|
-
// (declared somewhere in the tree); that vocabulary is what separates a field from `TODO`.
|
|
15
|
-
//
|
|
16
|
-
// A field token is UPPER_CASE or snake_case. A lowercase word needs an underscore to be a token, so
|
|
17
|
-
// ordinary prose words (`owner`, `summary`) never become fields. Case decides whether two tokens are the
|
|
18
|
-
// same field only for the miscased finding: `governances_loaded` and `GOVERNANCES_LOADED` are one
|
|
19
|
-
// field, spelled two ways, and a file that does both is reported.
|
|
20
|
-
//
|
|
21
|
-
// Pure functions are exported for node:test; running the file directly drives the CLI. No
|
|
22
|
-
// dependencies (the repo's node-≥23.6 / no-deps convention).
|
|
23
|
-
|
|
24
|
-
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs'
|
|
25
|
-
import { join, relative, sep } from 'node:path'
|
|
26
|
-
import { pathToFileURL } from 'node:url'
|
|
27
|
-
|
|
28
|
-
export type FindingKind = 'unexplained' | 'undeclared' | 'miscased'
|
|
29
|
-
|
|
30
|
-
export interface Finding {
|
|
31
|
-
kind: FindingKind
|
|
32
|
-
/** Root-relative path of the definition, `/`-separated. */
|
|
33
|
-
file: string
|
|
34
|
-
/** 1-based line: the declaration for `unexplained`, the prose line for `undeclared`. */
|
|
35
|
-
line: number
|
|
36
|
-
token: string
|
|
37
|
-
/** For `miscased`: the spelling this file's declaration uses. */
|
|
38
|
-
declared?: string
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface Declaration {
|
|
42
|
-
token: string
|
|
43
|
-
line: number
|
|
44
|
-
glossed: boolean
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
interface Mandate {
|
|
48
|
-
token: string
|
|
49
|
-
line: number
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export interface ParsedDefinition {
|
|
53
|
-
declarations: Declaration[]
|
|
54
|
-
mandates: Mandate[]
|
|
55
|
-
/** Every field-shaped word the prose names, code span or not. */
|
|
56
|
-
proseWords: Set<string>
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** UPPER_CASE, or snake_case with at least one underscore — a bare lowercase word is prose. */
|
|
60
|
-
const TOKEN = '(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)'
|
|
61
|
-
const TOKEN_ITEM = `${TOKEN}(?:\\(s\\))?`
|
|
62
|
-
/** A block line opening with a comma list of tokens, then a colon, a trailing comment, or nothing. */
|
|
63
|
-
const BLOCK_DECLARATION_RE = new RegExp(`^\\s*(${TOKEN_ITEM}(?:\\s*,\\s*${TOKEN_ITEM})*)\\s*(:.*|#.*)?$`)
|
|
64
|
-
/** A list item opening with one or more bold tokens joined by `+`. */
|
|
65
|
-
const LIST_DECLARATION_RE = new RegExp(`^\\s*[-*+]\\s+(\\*\\*${TOKEN}\\*\\*(?:\\s*\\+\\s*\\*\\*${TOKEN}\\*\\*)*)(.*)$`)
|
|
66
|
-
const BOLD_TOKEN_RE = new RegExp(`\\*\\*(${TOKEN})\\*\\*`, 'g')
|
|
67
|
-
const WORD_RE = new RegExp(`(?<![A-Za-z0-9_])${TOKEN}(?![A-Za-z0-9_])`, 'g')
|
|
68
|
-
const CODE_SPAN_RE = /`([^`\n]+)`/g
|
|
69
|
-
const MANDATE_LEAD_RE = new RegExp(`^<?(${TOKEN})(?=$|[\\s.:\\[\\]>(,=])`)
|
|
70
|
-
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/
|
|
71
|
-
/** The marker must carry a reason — a bare marker excuses nothing. */
|
|
72
|
-
const IGNORE_MARKER_RE = /<!--\s*field-mandate-ignore\s*:\s*[^\s>-][^>]*-->/
|
|
73
|
-
|
|
74
|
-
/** A field token is two or more characters: a lone capital is a word, not a field. */
|
|
75
|
-
function isField(token: string): boolean {
|
|
76
|
-
return token.length >= 2
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** A token's case-folded form: the spelling-independent name of the field. */
|
|
80
|
-
function fold(token: string): string {
|
|
81
|
-
return token.toUpperCase()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** The folded forms a token may take and still be the same field — equal, or one trailing `S` apart. */
|
|
85
|
-
function sameFieldForms(token: string): string[] {
|
|
86
|
-
const t = fold(token)
|
|
87
|
-
const forms = [t, `${t}S`]
|
|
88
|
-
if (t.endsWith('S') && t.length > 2) forms.push(t.slice(0, -1))
|
|
89
|
-
return forms
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Does the set of folded tokens hold the same field as `token`? */
|
|
93
|
-
function hasSameField(folded: Set<string>, token: string): boolean {
|
|
94
|
-
return sameFieldForms(token).some((f) => folded.has(f))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Is the token spelled lowercase (snake_case) rather than uppercase? */
|
|
98
|
-
function isLower(token: string): boolean {
|
|
99
|
-
return token !== fold(token)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// ── Parse ──
|
|
103
|
-
|
|
104
|
-
export function parseDefinition(text: string): ParsedDefinition {
|
|
105
|
-
const lines = text.split(/\r?\n/)
|
|
106
|
-
const declarations: Declaration[] = []
|
|
107
|
-
const mandates: Mandate[] = []
|
|
108
|
-
const proseWords = new Set<string>()
|
|
109
|
-
|
|
110
|
-
let i = 0
|
|
111
|
-
// YAML frontmatter is metadata, not prose.
|
|
112
|
-
if (lines[0]?.trim() === '---') {
|
|
113
|
-
const end = lines.findIndex((l, n) => n > 0 && l.trim() === '---')
|
|
114
|
-
if (end > 0) i = end + 1
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
let fence: { char: string; length: number; structured: boolean } | null = null
|
|
118
|
-
// The declarations a continuation line glosses: the most recent group in the open block.
|
|
119
|
-
let group: Declaration[] = []
|
|
120
|
-
|
|
121
|
-
/** Record a span of text as prose: its field-shaped words, and any code-span mandate. */
|
|
122
|
-
function scanProse(text: string, lineNo: number): void {
|
|
123
|
-
for (const m of text.matchAll(WORD_RE)) if (isField(m[0])) proseWords.add(m[0])
|
|
124
|
-
if (IGNORE_MARKER_RE.test(text)) return
|
|
125
|
-
for (const m of text.matchAll(CODE_SPAN_RE)) {
|
|
126
|
-
const lead = (m[1] ?? '').trim().match(MANDATE_LEAD_RE)
|
|
127
|
-
const token = lead?.[1]
|
|
128
|
-
if (token && isField(token)) mandates.push({ token, line: lineNo })
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
for (; i < lines.length; i++) {
|
|
133
|
-
const line = lines[i] ?? ''
|
|
134
|
-
const lineNo = i + 1
|
|
135
|
-
const fenceMatch = line.match(FENCE_RE)
|
|
136
|
-
|
|
137
|
-
if (fence) {
|
|
138
|
-
if (
|
|
139
|
-
fenceMatch &&
|
|
140
|
-
fenceMatch[1]?.[0] === fence.char &&
|
|
141
|
-
(fenceMatch[1]?.length ?? 0) >= fence.length &&
|
|
142
|
-
fenceMatch[2]?.trim() === ''
|
|
143
|
-
) {
|
|
144
|
-
fence = null
|
|
145
|
-
group = []
|
|
146
|
-
continue
|
|
147
|
-
}
|
|
148
|
-
if (!fence.structured) continue
|
|
149
|
-
const decl = line.match(BLOCK_DECLARATION_RE)
|
|
150
|
-
const tokens = decl ? (decl[1] ?? '').split(',').map((t) => t.trim().replace(/\(s\)$/, '')) : []
|
|
151
|
-
const rest = decl?.[2]
|
|
152
|
-
const isDeclaration = decl !== null && (rest?.startsWith(':') || tokens.length >= 2)
|
|
153
|
-
if (isDeclaration) {
|
|
154
|
-
const glossed = rest?.startsWith(':') === true && rest.slice(1).trim() !== ''
|
|
155
|
-
group = tokens.filter(isField).map((token) => ({ token, line: lineNo, glossed }))
|
|
156
|
-
declarations.push(...group)
|
|
157
|
-
} else if (line.trim() !== '') {
|
|
158
|
-
for (const d of group) d.glossed = true
|
|
159
|
-
}
|
|
160
|
-
continue
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (fenceMatch) {
|
|
164
|
-
const info = fenceMatch[2]?.trim() ?? ''
|
|
165
|
-
fence = {
|
|
166
|
-
char: fenceMatch[1]?.[0] ?? '`',
|
|
167
|
-
length: fenceMatch[1]?.length ?? 3,
|
|
168
|
-
structured: info === '' || info === 'text',
|
|
169
|
-
}
|
|
170
|
-
group = []
|
|
171
|
-
continue
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const listDecl = line.match(LIST_DECLARATION_RE)
|
|
175
|
-
if (listDecl) {
|
|
176
|
-
const rest = listDecl[2] ?? ''
|
|
177
|
-
const glossed = rest.trim() !== ''
|
|
178
|
-
for (const m of (listDecl[1] ?? '').matchAll(BOLD_TOKEN_RE)) {
|
|
179
|
-
const token = m[1] ?? ''
|
|
180
|
-
if (isField(token)) declarations.push({ token, line: lineNo, glossed })
|
|
181
|
-
}
|
|
182
|
-
// The text after a list item's bold lead is that declaration's gloss AND prose: a code
|
|
183
|
-
// span in it mandates like any other (it is scanned the same as a full prose line).
|
|
184
|
-
scanProse(rest, lineNo)
|
|
185
|
-
continue
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Prose.
|
|
189
|
-
scanProse(line, lineNo)
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return { declarations, mandates, proseWords }
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── Discovery ──
|
|
196
|
-
|
|
197
|
-
function walk(dir: string, out: string[]): void {
|
|
198
|
-
let entries: import('node:fs').Dirent[]
|
|
199
|
-
try {
|
|
200
|
-
entries = readdirSync(dir, { withFileTypes: true })
|
|
201
|
-
} catch {
|
|
202
|
-
return
|
|
203
|
-
}
|
|
204
|
-
for (const e of entries) {
|
|
205
|
-
const full = join(dir, e.name)
|
|
206
|
-
if (e.isDirectory()) walk(full, out)
|
|
207
|
-
else if (e.isFile() && e.name.endsWith('.md')) out.push(full)
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
/** Is this root-relative path a skill (`plugins/<p>/skills/**\/SKILL.md`) or agent (`plugins/<p>/agents/*.md`)? */
|
|
212
|
-
export function isDefinitionPath(rel: string): boolean {
|
|
213
|
-
const parts = rel.split('/')
|
|
214
|
-
if (parts[0] !== 'plugins' || parts.length < 4) return false
|
|
215
|
-
if (parts[2] === 'agents') return parts.length === 4 && (parts[3] ?? '').endsWith('.md')
|
|
216
|
-
if (parts[2] === 'skills') return parts.length >= 5 && parts[parts.length - 1] === 'SKILL.md'
|
|
217
|
-
return false
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/** Every definition under `root`, root-relative, `/`-separated, path-sorted. */
|
|
221
|
-
export function discoverDefinitions(root: string): string[] {
|
|
222
|
-
const pluginsDir = join(root, 'plugins')
|
|
223
|
-
if (!existsSync(pluginsDir)) return []
|
|
224
|
-
const files: string[] = []
|
|
225
|
-
walk(pluginsDir, files)
|
|
226
|
-
return files
|
|
227
|
-
.map((f) => relative(root, f).split(sep).join('/'))
|
|
228
|
-
.filter(isDefinitionPath)
|
|
229
|
-
.sort()
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// ── The check ──
|
|
233
|
-
|
|
234
|
-
export function check(parsed: Map<string, ParsedDefinition>): Finding[] {
|
|
235
|
-
const known = new Set<string>()
|
|
236
|
-
for (const p of parsed.values()) for (const d of p.declarations) known.add(fold(d.token))
|
|
237
|
-
|
|
238
|
-
const findings: Finding[] = []
|
|
239
|
-
for (const [file, p] of parsed) {
|
|
240
|
-
if (p.declarations.length === 0) continue
|
|
241
|
-
const declared = new Set(p.declarations.map((d) => fold(d.token)))
|
|
242
|
-
const proseWords = new Set([...p.proseWords].map(fold))
|
|
243
|
-
|
|
244
|
-
const seen = new Set<string>()
|
|
245
|
-
for (const d of p.declarations) {
|
|
246
|
-
if (seen.has(d.token)) continue
|
|
247
|
-
seen.add(d.token)
|
|
248
|
-
const glossed = p.declarations.some((o) => o.token === d.token && o.glossed)
|
|
249
|
-
if (!glossed && !hasSameField(proseWords, d.token)) {
|
|
250
|
-
findings.push({ kind: 'unexplained', file, line: d.line, token: d.token })
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const reported = new Set<string>()
|
|
255
|
-
for (const m of p.mandates) {
|
|
256
|
-
if (!hasSameField(known, m.token)) continue
|
|
257
|
-
const key = `${m.line}:${m.token}`
|
|
258
|
-
if (reported.has(key)) continue
|
|
259
|
-
if (!hasSameField(declared, m.token)) {
|
|
260
|
-
reported.add(key)
|
|
261
|
-
findings.push({ kind: 'undeclared', file, line: m.line, token: m.token })
|
|
262
|
-
continue
|
|
263
|
-
}
|
|
264
|
-
// Declared here — but is any same-field declaration spelled in the mandate's case?
|
|
265
|
-
const forms = sameFieldForms(m.token)
|
|
266
|
-
const same = p.declarations.filter((d) => forms.includes(fold(d.token)))
|
|
267
|
-
if (same.some((d) => isLower(d.token) === isLower(m.token))) continue
|
|
268
|
-
reported.add(key)
|
|
269
|
-
findings.push({ kind: 'miscased', file, line: m.line, token: m.token, declared: same[0]?.token })
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
return findings.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1))
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// ── Report ──
|
|
276
|
-
|
|
277
|
-
const DETAIL: Record<FindingKind, string> = {
|
|
278
|
-
unexplained: 'declared in a block, but no gloss and no prose explains it',
|
|
279
|
-
undeclared: 'mandated in the prose, but no block in this file declares it',
|
|
280
|
-
miscased: 'mandated in the prose, but this file declares it in the other case',
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
export function formatReport(definitions: string[], findings: Finding[]): string {
|
|
284
|
-
if (definitions.length === 0) return 'check-field-mandates: no skill or agent definition found\n'
|
|
285
|
-
if (findings.length === 0) return `check-field-mandates: ${definitions.length} definition(s) OK\n`
|
|
286
|
-
const rows = findings
|
|
287
|
-
.map(
|
|
288
|
-
(f) =>
|
|
289
|
-
` ${f.kind.padEnd(11)} ${f.file}:${f.line} ${f.token} — ${DETAIL[f.kind]}${f.declared ? ` (${f.declared})` : ''}`,
|
|
290
|
-
)
|
|
291
|
-
.join('\n')
|
|
292
|
-
return `${rows}\ncheck-field-mandates: ${findings.length} finding(s) across ${definitions.length} definition(s)\n`
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// ── CLI ──
|
|
296
|
-
|
|
297
|
-
function isReadableDirectory(path: string): boolean {
|
|
298
|
-
try {
|
|
299
|
-
if (!statSync(path).isDirectory()) return false
|
|
300
|
-
readdirSync(path)
|
|
301
|
-
return true
|
|
302
|
-
} catch {
|
|
303
|
-
return false
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function main(argv: string[]): number {
|
|
308
|
-
let root = '.'
|
|
309
|
-
for (let i = 0; i < argv.length; i++) {
|
|
310
|
-
const a = argv[i]
|
|
311
|
-
if (a === '--root') {
|
|
312
|
-
const next = argv[++i]
|
|
313
|
-
if (next === undefined) {
|
|
314
|
-
process.stderr.write('check-field-mandates: --root needs a directory\n')
|
|
315
|
-
return 1
|
|
316
|
-
}
|
|
317
|
-
root = next
|
|
318
|
-
continue
|
|
319
|
-
}
|
|
320
|
-
process.stderr.write(`check-field-mandates: unrecognized flag ${a}\n`)
|
|
321
|
-
return 1
|
|
322
|
-
}
|
|
323
|
-
if (!isReadableDirectory(root)) {
|
|
324
|
-
process.stderr.write(`check-field-mandates: root ${root} is not a readable directory\n`)
|
|
325
|
-
return 1
|
|
326
|
-
}
|
|
327
|
-
const definitions = discoverDefinitions(root)
|
|
328
|
-
const parsed = new Map<string, ParsedDefinition>()
|
|
329
|
-
for (const rel of definitions) parsed.set(rel, parseDefinition(readFileSync(join(root, rel), 'utf8')))
|
|
330
|
-
const findings = check(parsed)
|
|
331
|
-
process.stdout.write(formatReport(definitions, findings))
|
|
332
|
-
return findings.length === 0 ? 0 : 1
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
|
|
336
|
-
process.exit(main(process.argv.slice(2)))
|
|
337
|
-
}
|