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.
Files changed (150) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/CHANGELOG.md +320 -0
  4. package/agents/sdd-scanner.md +1 -1
  5. package/package.json +11 -12
  6. package/plugin.json +55 -0
  7. package/skills/align-spec/SKILL.md +1 -1
  8. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  9. package/skills/blast-estimate/README.md +3 -3
  10. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  11. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  12. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  13. package/skills/check-plan-safety/SKILL.md +2 -3
  14. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  15. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  16. package/skills/check-project-specs/SKILL.md +1 -1
  17. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  18. package/skills/check-retired-terms/SKILL.md +2 -3
  19. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  20. package/skills/check-scenario-overlap/SKILL.md +1 -2
  21. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  22. package/skills/check-spec-references/SKILL.md +2 -2
  23. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  24. package/skills/check-spec-structure/SKILL.md +1 -2
  25. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  26. package/skills/collision-ladder/README.md +2 -2
  27. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  28. package/skills/combat-log-governance/SKILL.md +1 -1
  29. package/skills/concept-index/SKILL.md +1 -1
  30. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  31. package/skills/discover-plans/README.md +3 -3
  32. package/skills/discover-plans/SKILL.md +1 -1
  33. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  34. package/skills/discover-specs/README.md +3 -3
  35. package/skills/discover-specs/SKILL.md +1 -1
  36. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  37. package/skills/doctrine-loop/SKILL.md +1 -1
  38. package/skills/gate-validation-governance/SKILL.md +1 -1
  39. package/skills/init/README.md +2 -2
  40. package/skills/init/scripts/wire-statusline.mjs +168 -12
  41. package/skills/manage-ignore/README.md +3 -3
  42. package/skills/manage-ignore/SKILL.md +1 -2
  43. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  44. package/skills/manage-scenario-bridge/README.md +3 -3
  45. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  46. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  47. package/skills/manage-spec-anchors/README.md +3 -3
  48. package/skills/manage-spec-anchors/SKILL.md +1 -1
  49. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  50. package/skills/mission-graph/README.md +2 -2
  51. package/skills/mission-graph/SKILL.md +1 -1
  52. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  53. package/skills/pause-mission/SKILL.md +1 -1
  54. package/skills/place-node/SKILL.md +2 -2
  55. package/skills/place-node/scripts/place-node.mjs +117 -12
  56. package/skills/plan-retirement/README.md +2 -2
  57. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  58. package/skills/resolve-governances/README.md +3 -3
  59. package/skills/resolve-governances/SKILL.md +1 -1
  60. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  61. package/skills/resolve-tracking/SKILL.md +1 -1
  62. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  63. package/skills/scaffold-project-spec/SKILL.md +1 -1
  64. package/skills/spec-gate/README.md +12 -9
  65. package/skills/spec-gate/SKILL.md +2 -2
  66. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  67. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  68. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  69. package/skills/spec-producer-governance/SKILL.md +2 -2
  70. package/skills/start-mission/SKILL.md +1 -1
  71. package/skills/suite-format-governance/README.md +1 -1
  72. package/skills/suite-format-governance/SKILL.md +1 -1
  73. package/skills/touch-set-correction/README.md +5 -5
  74. package/skills/touch-set-correction/SKILL.md +1 -1
  75. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  76. package/skills/verify-scenarios/README.md +6 -6
  77. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  78. package/.plugin/pins.json +0 -3
  79. package/.plugin/plugin.json +0 -19
  80. package/dist/align-spec/scripts/align-spec.js +0 -102
  81. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  82. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  83. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  84. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  85. package/dist/concept-index/scripts/concept-index.js +0 -181
  86. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  87. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  88. package/dist/spec-gate/scripts/check-suite.js +0 -317
  89. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  90. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  91. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  92. package/skills/align-spec/scripts/align-spec.mts +0 -188
  93. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  94. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  95. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  96. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  97. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  98. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  99. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  100. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  102. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  103. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  104. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  105. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  106. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  108. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  109. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  110. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  111. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  112. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  113. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  114. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  115. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  116. package/skills/concept-index/scripts/concept-index.mts +0 -246
  117. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  118. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  119. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  120. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  121. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  122. package/skills/init/scripts/wire-statusline.mts +0 -279
  123. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  124. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  126. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  128. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  129. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  130. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  131. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  132. package/skills/place-node/scripts/place-node.mts +0 -158
  133. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  134. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  135. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  136. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  137. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  138. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  139. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  140. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  141. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  142. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  143. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  144. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  145. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  146. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  147. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  148. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  149. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  150. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
@@ -1,317 +0,0 @@
1
- #!/usr/bin/env node
2
- import { validate } from "../vendor/gherkin-cli.js";
3
- import { readFileSync, readdirSync, realpathSync } from "node:fs";
4
- import { basename, dirname, join } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- //#region skills/spec-gate/scripts/check-suite.mts
7
- const ADVERB_PATTERNS = [
8
- "sometimes",
9
- "usually",
10
- "often",
11
- "occasionally"
12
- ].map((w) => new RegExp(`\\b${w}\\b`, "i"));
13
- const RUBRIC_PATTERNS = [...[
14
- "score",
15
- "threshold",
16
- "rubric"
17
- ].map((w) => new RegExp(`\\b${w}\\b`, "i")), /\b1[-–]5\b/];
18
- const ASSERTION_RE = /^(Then|And|But)\b/i;
19
- const RUBRIC_EXEMPT_RE = /\b(no|not|never|without|nor|passing|failing|pass|fail|boolean|true|false|verdict)\b/i;
20
- function parseSuite(text) {
21
- const lines = text.split("\n");
22
- let hasFeatureLine = false;
23
- let sectionCommentCount = 0;
24
- const scenarios = [];
25
- let current = null;
26
- let pendingTags = [];
27
- let inDocString = false;
28
- let docStringLines = [];
29
- for (const raw of lines) {
30
- const line = raw.trimStart();
31
- if (inDocString) {
32
- if (line.startsWith("\"\"\"")) {
33
- inDocString = false;
34
- if (current) current.docStrings.push(docStringLines.join("\n"));
35
- docStringLines = [];
36
- } else docStringLines.push(raw);
37
- continue;
38
- }
39
- if (current && line.startsWith("\"\"\"")) {
40
- inDocString = true;
41
- continue;
42
- }
43
- if (/^Feature:/i.test(line)) {
44
- hasFeatureLine = true;
45
- continue;
46
- }
47
- if (/^#/.test(line) && (/──/.test(line) || /--/.test(line))) {
48
- sectionCommentCount++;
49
- continue;
50
- }
51
- if (/^@/.test(line)) {
52
- pendingTags.push(...line.split(/\s+/).filter((t) => t.startsWith("@")));
53
- continue;
54
- }
55
- if (/^Scenario:/i.test(line) || /^Scenario Outline:/i.test(line)) {
56
- if (current) scenarios.push(current);
57
- const isOutline = /^Scenario Outline:/i.test(line);
58
- current = {
59
- name: line.replace(/^Scenario(?: Outline)?:/i, "").trim(),
60
- steps: [],
61
- tags: pendingTags,
62
- isOutline,
63
- placeholders: [],
64
- examples: null,
65
- docStrings: []
66
- };
67
- pendingTags = [];
68
- continue;
69
- }
70
- if (current && /^Examples:/i.test(line)) {
71
- current.examples = {
72
- header: [],
73
- rows: []
74
- };
75
- continue;
76
- }
77
- if (current?.examples && line.startsWith("|")) {
78
- const cells = line.split("|").slice(1, -1).map((c) => c.trim());
79
- if (current.examples.header.length === 0) current.examples.header = cells;
80
- else current.examples.rows.push(cells);
81
- continue;
82
- }
83
- if (current && /^(Given|When|Then|And|But)\b/i.test(line)) {
84
- current.steps.push(line);
85
- for (const m of line.matchAll(/<([^>]+)>/g)) current.placeholders.push(m[1]);
86
- }
87
- }
88
- if (current) scenarios.push(current);
89
- return {
90
- hasFeatureLine,
91
- scenarios,
92
- sectionCommentCount
93
- };
94
- }
95
- function runGherkinValidate(paths) {
96
- const { files } = validate(paths);
97
- const out = /* @__PURE__ */ new Map();
98
- for (const f of files) out.set(f.file, f.errors.map((e) => ({
99
- line: e.line,
100
- message: e.message
101
- })));
102
- return out;
103
- }
104
- function findDeadRubric(docString) {
105
- let dimensionsTotal = 0;
106
- let sawMax = false;
107
- let threshold = null;
108
- for (const raw of docString.split("\n")) {
109
- const line = raw.trim();
110
- const maxMatch = /^max:\s*(-?\d+(?:\.\d+)?)\s*$/.exec(line);
111
- if (maxMatch) {
112
- sawMax = true;
113
- dimensionsTotal += Number(maxMatch[1]);
114
- continue;
115
- }
116
- const thresholdMatch = /^threshold:\s*(-?\d+(?:\.\d+)?)\s*$/.exec(line);
117
- if (thresholdMatch) threshold = Number(thresholdMatch[1]);
118
- }
119
- if (!sawMax || threshold === null) return null;
120
- if (dimensionsTotal >= threshold) return null;
121
- return {
122
- dimensionsTotal,
123
- threshold
124
- };
125
- }
126
- function checkSuite(slug, file, text, parseErrors = []) {
127
- const tag = (msg) => `${slug}/${file}: ${msg}`;
128
- if (parseErrors.length > 0) return parseErrors.map((e) => tag(`cannot parse as Gherkin at line ${e.line} — ${e.message}`));
129
- const v = [];
130
- const ref = parseSuite(text);
131
- if (!ref.hasFeatureLine) v.push(tag("missing Feature: line"));
132
- for (const scenario of ref.scenarios) {
133
- const steps = scenario.steps;
134
- const label = scenario.name ? `Scenario "${scenario.name}"` : "unnamed scenario";
135
- const isRubric = scenario.tags.includes("@rubric");
136
- if (isRubric) for (const docString of scenario.docStrings) {
137
- const dead = findDeadRubric(docString);
138
- if (dead) v.push(tag(`${label}: rubric cannot be passed — dimensions total ${dead.dimensionsTotal}, threshold ${dead.threshold}`));
139
- }
140
- if (steps.length === 0) {
141
- v.push(tag(`${label}: has no steps`));
142
- continue;
143
- }
144
- if (!steps.some((s) => /^(Given|When)\b/i.test(s))) v.push(tag(`${label}: missing Given or When step`));
145
- if (!steps.some((s) => /^Then\b/i.test(s))) v.push(tag(`${label}: missing Then step — scenario has no assertion`));
146
- for (const step of steps) {
147
- const adverb = ADVERB_PATTERNS.find((p) => p.test(step));
148
- if (adverb) {
149
- v.push(tag(`${label}: step contains non-boolean hedge — "${step.trim()}" (matched ${adverb.source})`));
150
- continue;
151
- }
152
- if (isRubric || !ASSERTION_RE.test(step) || RUBRIC_EXEMPT_RE.test(step)) continue;
153
- const rubric = RUBRIC_PATTERNS.find((p) => p.test(step));
154
- if (rubric) v.push(tag(`${label}: step embeds a rubric/score in its assertion — "${step.trim()}" (matched ${rubric.source})`));
155
- }
156
- if (scenario.isOutline) {
157
- const ex = scenario.examples;
158
- if (!ex || ex.header.length === 0 || ex.rows.length === 0) v.push(tag(`${label}: Scenario Outline has no non-empty Examples table`));
159
- else {
160
- const missing = [...new Set(scenario.placeholders)].filter((p) => !ex.header.includes(p));
161
- if (missing.length) v.push(tag(`${label}: Examples table missing column(s) for placeholder(s): ${missing.join(", ")}`));
162
- }
163
- }
164
- }
165
- if (ref.scenarios.length > 6 && ref.sectionCommentCount === 0) v.push(tag(`${ref.scenarios.length} scenarios but no section comments (add # ── Stage comments to group lifecycle stages)`));
166
- return v;
167
- }
168
- function discoverSuiteDirs(root) {
169
- const out = [];
170
- const walk = (dir, rel) => {
171
- let entries;
172
- try {
173
- entries = readdirSync(dir, { withFileTypes: true });
174
- } catch {
175
- return;
176
- }
177
- const files = entries.filter((e) => e.isFile() && e.name.endsWith(".feature")).map((e) => e.name);
178
- if (files.length) out.push({
179
- slug: rel,
180
- files
181
- });
182
- for (const e of entries) {
183
- if (!e.isDirectory() || e.name === "node_modules" || e.name.startsWith(".")) continue;
184
- walk(join(dir, e.name), rel ? join(rel, e.name) : e.name);
185
- }
186
- };
187
- walk(root, "");
188
- return out;
189
- }
190
- function checkFilePaths(paths, validate = runGherkinValidate) {
191
- const violations = [];
192
- const readable = [];
193
- for (const p of paths) try {
194
- readable.push({
195
- path: p,
196
- text: readFileSync(p, "utf8")
197
- });
198
- } catch {
199
- violations.push(`${p}: cannot read file`);
200
- }
201
- if (readable.length === 0) return violations;
202
- let parseErrorsByPath;
203
- try {
204
- parseErrorsByPath = validate(readable.map((r) => r.path));
205
- } catch (err) {
206
- for (const { path } of readable) violations.push(`${path}: cannot verify Gherkin validity — ${err.message}`);
207
- return violations;
208
- }
209
- for (const { path: p, text } of readable) {
210
- const errs = parseErrorsByPath.get(p);
211
- if (errs === void 0) {
212
- violations.push(`${p}: the Gherkin parser returned no result for this file`);
213
- continue;
214
- }
215
- violations.push(...checkSuite(dirname(p), basename(p), text, errs));
216
- if (errs.length === 0) {
217
- const specPath = join(dirname(p), "README.md");
218
- let specText;
219
- try {
220
- specText = readFileSync(specPath, "utf8");
221
- } catch {
222
- specText = void 0;
223
- }
224
- if (specText !== void 0) violations.push(...checkScenarioMap(dirname(p), basename(p), text, specText));
225
- }
226
- }
227
- return violations;
228
- }
229
- function parseScenarioMap(specText) {
230
- const heading = /^## Scenario map[ \t]*$/m.exec(specText);
231
- if (heading === null) return void 0;
232
- const after = specText.slice(heading.index + heading[0].length);
233
- const nextHeading = after.search(/\n## /);
234
- const body = nextHeading === -1 ? after : after.slice(0, nextHeading);
235
- const rows = [];
236
- const unparseable = [];
237
- let blockRow = -1;
238
- for (const line of body.split("\n")) {
239
- const t = line.trim();
240
- if (!t.startsWith("|")) {
241
- blockRow = -1;
242
- continue;
243
- }
244
- blockRow++;
245
- if (blockRow < 2) continue;
246
- const cells = t.split("|").slice(1, -1).map((c) => c.trim());
247
- const m = (cells.length === 3 ? cells[2] ?? "" : "").match(/^`(.+)`$/);
248
- if (m === null) {
249
- unparseable.push(t);
250
- continue;
251
- }
252
- rows.push({
253
- edge: cells[0] ?? "",
254
- path: cells[1] ?? "",
255
- scenario: m[1] ?? ""
256
- });
257
- }
258
- return {
259
- rows,
260
- unparseable
261
- };
262
- }
263
- function checkScenarioMap(slug, file, featureText, specText) {
264
- const map = parseScenarioMap(specText);
265
- if (map === void 0) return [];
266
- const tag = (msg) => `${slug}/${file}: ${msg}`;
267
- const v = [];
268
- for (const raw of map.unparseable) v.push(tag(`scenario-map data row has no backtick-wrapped Scenario cell — ${raw}`));
269
- const titles = [...featureText.matchAll(/^\s*Scenario(?: Outline)?:\s*(.+?)\s*$/gm)].map((m) => m[1] ?? "");
270
- const mapped = new Set(map.rows.map((r) => r.scenario));
271
- for (const t of titles) if (!mapped.has(t)) v.push(tag(`scenario is not on the scenario map — "${t}"`));
272
- const titleSet = new Set(titles);
273
- for (const r of map.rows) if (!titleSet.has(r.scenario)) v.push(tag(`scenario map row names no such scenario — "${r.scenario}"`));
274
- const seen = /* @__PURE__ */ new Map();
275
- for (const r of map.rows) {
276
- const key = `${r.edge}\u0000${r.path}`;
277
- const prior = seen.get(key);
278
- if (prior !== void 0) v.push(tag(`duplicate map pair — edge "${r.edge}" and path "${r.path}" cover both "${prior}" and "${r.scenario}"; a repeated edge needs a DIFFERENT path class`));
279
- else seen.set(key, r.scenario);
280
- }
281
- return v;
282
- }
283
- function parseFilesArg(argv) {
284
- const idx = argv.indexOf("--files");
285
- if (idx === -1) return [];
286
- const paths = [];
287
- for (let i = idx + 1; i < argv.length; i++) {
288
- if (argv[i].startsWith("--")) break;
289
- paths.push(argv[i]);
290
- }
291
- return paths;
292
- }
293
- function main(argv) {
294
- let violations = [];
295
- if (argv.includes("--files")) {
296
- const paths = parseFilesArg(argv);
297
- if (paths.length === 0) {
298
- console.error("✗ --files requires at least one .feature path");
299
- return 1;
300
- }
301
- violations = checkFilePaths(paths);
302
- } else {
303
- const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] : ".agents/specs";
304
- const paths = [];
305
- for (const { slug, files } of discoverSuiteDirs(root)) for (const file of files) paths.push(join(root, slug, file));
306
- violations = checkFilePaths(paths);
307
- }
308
- if (violations.length) {
309
- for (const line of violations) console.error(`✗ ${line}`);
310
- return 1;
311
- }
312
- process.stdout.write("suite checks OK\n");
313
- return 0;
314
- }
315
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
316
- //#endregion
317
- export { checkFilePaths, checkScenarioMap, checkSuite, discoverSuiteDirs, findDeadRubric, main, parseFilesArg, parseScenarioMap, parseSuite, runGherkinValidate };
@@ -1,263 +0,0 @@
1
- import { GitError, diff } from "../vendor/gherkin-cli.js";
2
- import { execFileSync } from "node:child_process";
3
- import { readFileSync, realpathSync } from "node:fs";
4
- import { dirname, join, relative, resolve, sep } from "node:path";
5
- import { pathToFileURL } from "node:url";
6
- //#region skills/spec-gate/scripts/classify-edit-class.mts
7
- function hasFeatureFrozenTag(text) {
8
- const lines = text.split("\n");
9
- const featureIdx = lines.findIndex((l) => /^\s*Feature:/i.test(l));
10
- if (featureIdx === -1) return false;
11
- const tags = [];
12
- for (let i = featureIdx - 1; i >= 0; i--) {
13
- const line = lines[i].trim();
14
- if (line === "") continue;
15
- if (line.startsWith("@")) {
16
- tags.push(...line.split(/\s+/).filter(Boolean));
17
- continue;
18
- }
19
- break;
20
- }
21
- return tags.includes("@frozen");
22
- }
23
- function classifyFromDiff(fileResult) {
24
- const added = fileResult.scenarios.some((s) => s.change === "added");
25
- const narrowed = fileResult.scenarios.some((s) => s.change === "modified" || s.change === "removed");
26
- if (added && narrowed) return "mixed";
27
- if (narrowed) return "narrowing";
28
- if (added) return "additive";
29
- return "no-content-change";
30
- }
31
- function classifyFromFileResult(fileResult) {
32
- if (fileResult === void 0) return {
33
- classification: "unclassifiable",
34
- reason: "the structural differ returned no result for this file"
35
- };
36
- if (fileResult.error) return {
37
- classification: "unclassifiable",
38
- reason: `cannot parse (${fileResult.error.code}): ${fileResult.error.message}`
39
- };
40
- return { classification: classifyFromDiff(fileResult) };
41
- }
42
- function parseRenameStatus(nameStatusOutput) {
43
- const out = [];
44
- for (const line of nameStatusOutput.split("\n")) {
45
- const m = /^R(\d+)\t([^\t]+)\t([^\t]+)$/.exec(line);
46
- if (m) out.push({
47
- score: Number(m[1]),
48
- oldPath: m[2],
49
- newPath: m[3]
50
- });
51
- }
52
- return out;
53
- }
54
- function detectPureRename(base, path, cwd) {
55
- try {
56
- return parseRenameStatus(execFileSync("git", [
57
- "diff",
58
- "--name-status",
59
- "-M",
60
- base
61
- ], {
62
- encoding: "utf8",
63
- cwd,
64
- stdio: [
65
- "ignore",
66
- "pipe",
67
- "ignore"
68
- ]
69
- })).some((r) => r.newPath === path && r.score === 100);
70
- } catch {
71
- return false;
72
- }
73
- }
74
- function readGitShow(base, path, cwd) {
75
- try {
76
- return execFileSync("git", ["show", `${base}:${path}`], {
77
- encoding: "utf8",
78
- cwd,
79
- stdio: [
80
- "ignore",
81
- "pipe",
82
- "ignore"
83
- ]
84
- });
85
- } catch {
86
- return "";
87
- }
88
- }
89
- function makeCwdReader(cwd) {
90
- return (file, base) => {
91
- const abs = resolve(cwd, file);
92
- const dir = dirname(abs);
93
- let head;
94
- try {
95
- head = readFileSync(abs, "utf8");
96
- } catch {
97
- head = void 0;
98
- }
99
- const gitIo = {
100
- cwd: dir,
101
- encoding: "utf8",
102
- stdio: [
103
- "ignore",
104
- "pipe",
105
- "ignore"
106
- ]
107
- };
108
- let rel;
109
- try {
110
- rel = execFileSync("git", [
111
- "ls-files",
112
- "--full-name",
113
- "--",
114
- abs
115
- ], gitIo).trim();
116
- } catch (err) {
117
- throw new GitError(`git ls-files failed for ${file}: ${err.message}`);
118
- }
119
- if (rel === "") {
120
- const top = execFileSync("git", ["rev-parse", "--show-toplevel"], gitIo).trim();
121
- rel = relative(top, abs).split(sep).join("/");
122
- }
123
- let baseText;
124
- try {
125
- baseText = execFileSync("git", ["show", `${base}:${rel}`], gitIo);
126
- } catch {
127
- try {
128
- execFileSync("git", [
129
- "rev-parse",
130
- "--verify",
131
- "--quiet",
132
- `${base}^{commit}`
133
- ], {
134
- cwd: dir,
135
- stdio: "ignore"
136
- });
137
- baseText = void 0;
138
- } catch {
139
- throw new GitError(`git could not resolve base ref '${base}'`);
140
- }
141
- }
142
- return {
143
- head,
144
- base: baseText
145
- };
146
- };
147
- }
148
- const runGherkinDiff = (base, paths, cwd) => diff(paths, {
149
- base,
150
- full: true
151
- }, { readDiff: makeCwdReader(cwd) });
152
- function preCheckFile(path, base, cwd) {
153
- let currentText;
154
- try {
155
- currentText = readFileSync(join(cwd, path), "utf8");
156
- } catch {
157
- currentText = "";
158
- }
159
- const baselineText = readGitShow(base, path, cwd);
160
- if (!hasFeatureFrozenTag(currentText) && !hasFeatureFrozenTag(baselineText)) return {
161
- path,
162
- early: {
163
- file: path,
164
- classification: "unfrozen-skip",
165
- scenarios: []
166
- }
167
- };
168
- if (detectPureRename(base, path, cwd)) return {
169
- path,
170
- early: {
171
- file: path,
172
- classification: "no-content-change",
173
- scenarios: []
174
- }
175
- };
176
- return { path };
177
- }
178
- function classifyFromDiffOutput(path, diff) {
179
- const fileResult = diff.files.find((f) => f.file === path) ?? (diff.files.length === 1 ? diff.files[0] : void 0);
180
- const { classification, reason } = classifyFromFileResult(fileResult);
181
- return {
182
- file: path,
183
- classification,
184
- scenarios: fileResult?.scenarios ?? [],
185
- reason
186
- };
187
- }
188
- function classifyFile(path, base, cwd = ".", diffWith = runGherkinDiff) {
189
- const pre = preCheckFile(path, base, cwd);
190
- if (pre.early) return pre.early;
191
- let diff;
192
- try {
193
- diff = diffWith(base, [path], cwd);
194
- } catch {
195
- return {
196
- file: path,
197
- classification: "unclassifiable",
198
- scenarios: [],
199
- reason: "the structural differ produced no readable result"
200
- };
201
- }
202
- return classifyFromDiffOutput(path, diff);
203
- }
204
- function classifyFiles(paths, base, cwd = ".", diffWith = runGherkinDiff) {
205
- const preChecked = paths.filter((p) => p.endsWith(".feature")).map((path) => preCheckFile(path, base, cwd));
206
- const needsDiff = preChecked.filter((r) => r.early === void 0).map((r) => r.path);
207
- let diff;
208
- if (needsDiff.length > 0) try {
209
- diff = diffWith(base, needsDiff, cwd);
210
- } catch {
211
- diff = void 0;
212
- }
213
- return preChecked.map(({ path, early }) => {
214
- if (early) return early;
215
- if (diff === void 0) return {
216
- file: path,
217
- classification: "unclassifiable",
218
- scenarios: [],
219
- reason: "the structural differ produced no readable result"
220
- };
221
- return classifyFromDiffOutput(path, diff);
222
- });
223
- }
224
- function formatText(results) {
225
- const lines = [];
226
- for (const r of results) {
227
- lines.push(`${r.classification.toUpperCase().padEnd(18)} ${r.file}`);
228
- if (r.reason) lines.push(` ${r.reason}`);
229
- for (const s of r.scenarios) if (s.change !== "unchanged") lines.push(` ${s.change.padEnd(10)} ${s.name}`);
230
- }
231
- return lines.join("\n");
232
- }
233
- function parseFilesArg(argv) {
234
- const idx = argv.indexOf("--files");
235
- if (idx === -1) return [];
236
- const paths = [];
237
- for (let i = idx + 1; i < argv.length; i++) {
238
- if (argv[i].startsWith("--")) break;
239
- paths.push(argv[i]);
240
- }
241
- return paths;
242
- }
243
- function main(argv) {
244
- const paths = parseFilesArg(argv);
245
- if (paths.length === 0) {
246
- console.error("✗ --files requires at least one .feature path");
247
- return 1;
248
- }
249
- const base = argv.includes("--base") ? argv[argv.indexOf("--base") + 1] : "HEAD";
250
- const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "text";
251
- const results = classifyFiles(paths, base);
252
- if (format === "json") process.stdout.write(`${JSON.stringify(results, null, 2)}\n`);
253
- else process.stdout.write(`${formatText(results)}\n`);
254
- const unclassifiable = results.filter((r) => r.classification === "unclassifiable");
255
- if (unclassifiable.length) {
256
- for (const r of unclassifiable) console.error(`✗ ${r.file}: unclassifiable — ${r.reason}`);
257
- return 1;
258
- }
259
- return 0;
260
- }
261
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
262
- //#endregion
263
- export { classifyFile, classifyFiles, classifyFromDiff, classifyFromFileResult, formatText, hasFeatureFrozenTag, main, parseRenameStatus, runGherkinDiff };