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,12 +1,186 @@
1
- // Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
2
- // A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
3
- // will not strip types, so it runs the self-contained bundle built for the package.
4
- import { spawnSync } from 'node:child_process'
5
- import { fileURLToPath } from 'node:url'
6
-
7
- const self = fileURLToPath(import.meta.url)
8
- const installed = /[\\/]node_modules[\\/]/.test(self)
9
- const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
10
- const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
11
- if (run.error) throw run.error
12
- process.exitCode = run.status ?? 1
1
+ #!/usr/bin/env node
2
+ // Compiled by `universal-plugin plugin build` from src/check-retired-terms/check-retired-terms.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/check-retired-terms/scripts/check-retired-terms.mjs","commands":{}};
4
+ import { execFileSync } from "node:child_process";
5
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ //#region src/check-retired-terms/check-retired-terms.mts
9
+ var RegistryParseError = class extends Error {};
10
+ const REGISTRY_PATH = ".agents/sdd/retired-terms.toml";
11
+ function isBlankOrComment(line) {
12
+ const t = line.trim();
13
+ return t === "" || t.startsWith("#");
14
+ }
15
+ function extractString(block, key) {
16
+ const m = new RegExp(`(^|\\n)\\s*${key}\\s*=\\s*"([^"]*)"[^\\n]*`).exec(block);
17
+ if (!m) throw new RegistryParseError(`[[retired]] entry is missing a string "${key}"`);
18
+ return m[2];
19
+ }
20
+ function extractArray(block, key) {
21
+ const openMatch = new RegExp(`(^|\\n)\\s*${key}\\s*=\\s*\\[`).exec(block);
22
+ if (!openMatch) return [];
23
+ const openIdx = openMatch.index + openMatch[0].length - 1;
24
+ const closeIdx = block.indexOf("]", openIdx);
25
+ if (closeIdx === -1) throw new RegistryParseError(`"${key}" array is never closed with "]"`);
26
+ const arrText = block.slice(openIdx, closeIdx);
27
+ const out = [];
28
+ for (const q of arrText.matchAll(/"([^"]*)"/g)) out.push(q[1]);
29
+ return out;
30
+ }
31
+ /** Parses the registry's minimal TOML subset: an array of `[[retired]]` tables carrying string
32
+ * keys (term/since/replacement, all required) and optional string-array keys (scope/allow). Throws
33
+ * `RegistryParseError` on anything that does not fit that shape — an absent registry is handled by
34
+ * the caller (`readRegistry`), not here; an empty/comment-only file parses to `[]`. */
35
+ function parseRegistryToml(text) {
36
+ const marker = "[[retired]]";
37
+ const firstIdx = text.indexOf(marker);
38
+ if (firstIdx === -1) {
39
+ if (text.split("\n").every(isBlankOrComment)) return [];
40
+ throw new RegistryParseError("no [[retired]] table found and the file is not empty/comment-only");
41
+ }
42
+ if (!text.slice(0, firstIdx).split("\n").every(isBlankOrComment)) throw new RegistryParseError("content found before the first [[retired]] table header");
43
+ return text.slice(firstIdx).split(marker).slice(1).map((block) => {
44
+ const term = extractString(block, "term");
45
+ const since = extractString(block, "since");
46
+ const replacement = extractString(block, "replacement");
47
+ const scope = extractArray(block, "scope");
48
+ const allow = extractArray(block, "allow");
49
+ const entry = {
50
+ term,
51
+ since,
52
+ replacement
53
+ };
54
+ if (scope.length) entry.scope = scope;
55
+ if (allow.length) entry.allow = allow;
56
+ return entry;
57
+ });
58
+ }
59
+ /** Reads and parses the registry at `<root>/.agents/sdd/retired-terms.toml`. An absent file yields
60
+ * `[]` (an "an absent registry sweeps clean" registry, not an error). A present-but-malformed file
61
+ * throws `RegistryParseError` — the caller must surface that loudly and exit non-zero. */
62
+ function readRegistry(root) {
63
+ const file = join(root, REGISTRY_PATH);
64
+ if (!existsSync(file)) return [];
65
+ return parseRegistryToml(readFileSync(file, "utf8"));
66
+ }
67
+ const DEFAULT_BUILTIN_EXCLUDED_FILES = [
68
+ REGISTRY_PATH,
69
+ "plugins/sdd/src/check-retired-terms/check-retired-terms.mts",
70
+ "plugins/sdd/src/check-retired-terms/check-retired-terms.test.mts",
71
+ ".agents/specs/sdd/corpus/retired-terms/README.md",
72
+ ".agents/specs/sdd/corpus/retired-terms/retired-terms.feature"
73
+ ];
74
+ const DEFAULT_BUILTIN_EXCLUDED_PREFIXES = [".agents/plans/"];
75
+ function isUnderLedgerDir(relPath) {
76
+ return relPath.split("/").slice(0, -1).includes("ledger");
77
+ }
78
+ function isBuiltinExcluded(relPath, excludedFiles, excludedPrefixes) {
79
+ if (excludedFiles.includes(relPath)) return true;
80
+ if (excludedPrefixes.some((p) => relPath.startsWith(p))) return true;
81
+ if (isUnderLedgerDir(relPath)) return true;
82
+ return false;
83
+ }
84
+ function inScope(relPath, scope) {
85
+ if (!scope || scope.length === 0) return true;
86
+ return scope.some((prefix) => relPath.startsWith(prefix));
87
+ }
88
+ function parseAllowEntry(raw) {
89
+ const idx = raw.indexOf("::");
90
+ if (idx === -1) return { file: raw.trim() };
91
+ return {
92
+ file: raw.slice(0, idx).trim(),
93
+ substring: raw.slice(idx + 2).trim()
94
+ };
95
+ }
96
+ function isAllowed(relPath, lineText, allow) {
97
+ if (!allow) return false;
98
+ for (const raw of allow) {
99
+ const parsed = parseAllowEntry(raw);
100
+ if (parsed.file !== relPath) continue;
101
+ if (parsed.substring === void 0) return true;
102
+ if (lineText.includes(parsed.substring)) return true;
103
+ }
104
+ return false;
105
+ }
106
+ /** The git-tracked file set, repo-relative, one per line. An untracked file is outside the sweep
107
+ * by construction — it is never in this list. */
108
+ function listTrackedFiles(root) {
109
+ return execFileSync("git", ["ls-files"], {
110
+ cwd: root,
111
+ encoding: "utf8"
112
+ }).split("\n").filter((l) => l !== "");
113
+ }
114
+ /** Sweeps `root` for survivors of every entry in `entries`. `options` lets callers (tests)
115
+ * substitute fixture-scoped config — the tracked-file source and the built-in exclusion lists —
116
+ * in place of the real repo and the real defaults, mirroring `findMetaphorViolations`'s
117
+ * `ScanOptions` (packages/cyberlegion/src/metaphor-free.ts). */
118
+ function sweep(root, entries, options = {}) {
119
+ const listFiles = options.listTrackedFiles ?? listTrackedFiles;
120
+ const excludedFiles = options.builtinExcludedFiles ?? DEFAULT_BUILTIN_EXCLUDED_FILES;
121
+ const excludedPrefixes = options.builtinExcludedPrefixes ?? DEFAULT_BUILTIN_EXCLUDED_PREFIXES;
122
+ const violations = [];
123
+ for (const relPath of listFiles(root)) {
124
+ if (isBuiltinExcluded(relPath, excludedFiles, excludedPrefixes)) continue;
125
+ let text;
126
+ try {
127
+ text = readFileSync(join(root, relPath), "utf8");
128
+ } catch {
129
+ continue;
130
+ }
131
+ const lines = text.split("\n");
132
+ for (const entry of entries) {
133
+ if (!inScope(relPath, entry.scope)) continue;
134
+ lines.forEach((lineText, i) => {
135
+ if (!lineText.includes(entry.term)) return;
136
+ if (isAllowed(relPath, lineText, entry.allow)) return;
137
+ violations.push({
138
+ file: relPath,
139
+ line: i + 1,
140
+ term: entry.term,
141
+ replacement: entry.replacement
142
+ });
143
+ });
144
+ }
145
+ }
146
+ return violations.sort((a, b) => a.file !== b.file ? a.file < b.file ? -1 : 1 : a.line - b.line);
147
+ }
148
+ /** Renders `--list` output: one line per registered term with its `since` and `replacement`, or a
149
+ * definitive empty-state line when nothing is registered — never silence. */
150
+ function formatList(entries) {
151
+ if (entries.length === 0) return "check-retired-terms: no term is registered\n";
152
+ return entries.map((e) => `${e.term} (since ${e.since}) -> ${e.replacement}\n`).join("");
153
+ }
154
+ /** Renders the sweep report: `file:line:term` plus the declared replacement, one per survivor
155
+ * (every survivor, not just the first), then a count summary. */
156
+ function formatViolations(violations) {
157
+ const lines = violations.map((v) => `${v.file}:${v.line}:${v.term} — replace with: ${v.replacement}\n`);
158
+ lines.push(`check-retired-terms: ${violations.length} survivor(s) found\n`);
159
+ return lines.join("");
160
+ }
161
+ function main(argv) {
162
+ const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
163
+ const list = argv.includes("--list");
164
+ let entries;
165
+ try {
166
+ entries = readRegistry(root);
167
+ } catch (err) {
168
+ const message = err instanceof Error ? err.message : String(err);
169
+ process.stderr.write(`check-retired-terms: malformed registry — ${message}\n`);
170
+ return 1;
171
+ }
172
+ if (list) {
173
+ process.stdout.write(formatList(entries));
174
+ return 0;
175
+ }
176
+ const violations = sweep(root, entries);
177
+ if (violations.length === 0) {
178
+ process.stdout.write("check-retired-terms: clean — no survivors found\n");
179
+ return 0;
180
+ }
181
+ process.stdout.write(formatViolations(violations));
182
+ return 1;
183
+ }
184
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
185
+ //#endregion
186
+ export { RegistryParseError, formatList, formatViolations, listTrackedFiles, main, parseRegistryToml, readRegistry, sweep };
@@ -12,8 +12,7 @@ The concrete engine for **cross-node scenario-overlap detection** — the intra-
12
12
  SSA** partner of the code-side collision ladder. It audits **across the nodes** of one project spec
13
13
  and returns the candidates where the **same behavior lives in more than one node's `.feature`**, for
14
14
  the formation **Warden**. It is the **cross-node** sibling of `check-spec-structure` (intra-node
15
- node-shape), whose per-node scope leaves this cross-node axis uncovered. Self-contained `.mts` (the
16
- repo's node-≥23.6 / no-deps convention).
15
+ node-shape), whose per-node scope leaves this cross-node axis uncovered. Self-contained `.mjs` (compiled from `src/`; no runtime deps, Node ≥22.11).
17
16
 
18
17
  ## Why this rung exists
19
18
 
@@ -1,12 +1,177 @@
1
- // Launcher for the engine of the same name. Run it as `node <skill>/scripts/<engine>.mjs`.
2
- // A checkout runs the .mts source. An npm install keeps the skill under node_modules, where Node
3
- // will not strip types, so it runs the self-contained bundle built for the package.
4
- import { spawnSync } from 'node:child_process'
5
- import { fileURLToPath } from 'node:url'
6
-
7
- const self = fileURLToPath(import.meta.url)
8
- const installed = /[\\/]node_modules[\\/]/.test(self)
9
- const target = self.replace(/\.mjs$/, installed ? '.bundle.mjs' : '.mts')
10
- const run = spawnSync(process.execPath, [...process.execArgv, target, ...process.argv.slice(2)], { stdio: 'inherit' })
11
- if (run.error) throw run.error
12
- process.exitCode = run.status ?? 1
1
+ #!/usr/bin/env node
2
+ // Compiled by `universal-plugin plugin build` from src/check-scenario-overlap/check-scenario-overlap.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.2","entry":"skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs","commands":{}};
4
+ import { readFileSync, readdirSync, realpathSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/check-scenario-overlap/check-scenario-overlap.mts
8
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
9
+ "node_modules",
10
+ ".git",
11
+ "dist",
12
+ ".turbo",
13
+ ".next",
14
+ "coverage"
15
+ ]);
16
+ function normalize(s) {
17
+ return s.trim().replace(/\s+/g, " ").toLowerCase();
18
+ }
19
+ const STEP = /^\s*(Given|When|Then|And|But)\s+(.*\S)\s*$/;
20
+ const SCENARIO = /^\s*Scenario(?: Outline)?:\s*(.*\S)\s*$/;
21
+ const TABLE_ROW = /^\s*\|(.*)\|\s*$/;
22
+ function normalizeRow(line) {
23
+ return (TABLE_ROW.exec(line)?.[1] ?? line).split("|").map((cell) => normalize(cell)).join("|");
24
+ }
25
+ function parseFeature(featureText) {
26
+ const scenarios = [];
27
+ let title = null;
28
+ let steps = [];
29
+ let exampleRows = [];
30
+ const flush = () => {
31
+ if (title !== null) {
32
+ const fingerprint = exampleRows.length === 0 ? steps.map(normalize).join("\n") : `${steps.map(normalize).join("\n")}\n EXAMPLES \n${exampleRows.join("\n")}`;
33
+ scenarios.push({
34
+ title,
35
+ titleKey: normalize(title),
36
+ fingerprint
37
+ });
38
+ }
39
+ title = null;
40
+ steps = [];
41
+ exampleRows = [];
42
+ };
43
+ for (const line of featureText.split("\n")) {
44
+ const sc = SCENARIO.exec(line);
45
+ if (sc) {
46
+ flush();
47
+ title = sc[1];
48
+ continue;
49
+ }
50
+ if (title === null) continue;
51
+ const st = STEP.exec(line);
52
+ if (st) {
53
+ steps.push(st[2]);
54
+ continue;
55
+ }
56
+ if (TABLE_ROW.test(line)) exampleRows.push(normalizeRow(line));
57
+ }
58
+ flush();
59
+ return scenarios;
60
+ }
61
+ function displayPath(relPath) {
62
+ const p = relPath.replace(/\\/g, "/");
63
+ return p.endsWith("/README.md") ? p.slice(0, -9) : p;
64
+ }
65
+ function scanSuites(specDir) {
66
+ const suites = [];
67
+ walk(specDir, specDir, suites);
68
+ return suites.sort((a, b) => a.node.localeCompare(b.node));
69
+ }
70
+ function walk(dir, specDir, out) {
71
+ const entries = readdirSync(dir, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
74
+ const full = join(dir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ walk(full, specDir, out);
77
+ continue;
78
+ }
79
+ if (!entry.name.endsWith(".feature")) continue;
80
+ const scenarios = parseFeature(readFileSync(full, "utf8"));
81
+ if (scenarios.length === 0) continue;
82
+ const relDir = dir.slice(specDir.length + 1).replace(/\\/g, "/");
83
+ out.push({
84
+ node: displayPath(`${relDir}/`),
85
+ scenarios
86
+ });
87
+ }
88
+ }
89
+ function detect(suites) {
90
+ const byFingerprint = /* @__PURE__ */ new Map();
91
+ const byTitle = /* @__PURE__ */ new Map();
92
+ for (const suite of suites) for (const sc of suite.scenarios) {
93
+ if (sc.fingerprint !== "") {
94
+ let m = byFingerprint.get(sc.fingerprint);
95
+ if (!m) byFingerprint.set(sc.fingerprint, m = /* @__PURE__ */ new Map());
96
+ if (!m.has(suite.node)) m.set(suite.node, sc.title);
97
+ }
98
+ let t = byTitle.get(sc.titleKey);
99
+ if (!t) byTitle.set(sc.titleKey, t = /* @__PURE__ */ new Map());
100
+ let e = t.get(suite.node);
101
+ if (!e) t.set(suite.node, e = {
102
+ fps: /* @__PURE__ */ new Set(),
103
+ title: sc.title
104
+ });
105
+ e.fps.add(sc.fingerprint);
106
+ }
107
+ const candidates = [];
108
+ for (const [, nodeMap] of byFingerprint) {
109
+ if (nodeMap.size < 2) continue;
110
+ const nodes = [...nodeMap.keys()].sort();
111
+ for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) candidates.push({
112
+ kind: "exact-duplicate",
113
+ severity: "blocking",
114
+ nodes: [nodes[i], nodes[j]],
115
+ scenario: nodeMap.get(nodes[i]) ?? "",
116
+ detail: "identical step fingerprint — the same behavior is specified in both nodes; one behavior = one owning node"
117
+ });
118
+ }
119
+ for (const [, nodeMap] of byTitle) {
120
+ if (nodeMap.size < 2) continue;
121
+ const nodes = [...nodeMap.keys()].sort();
122
+ for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
123
+ const a = nodeMap.get(nodes[i]);
124
+ const b = nodeMap.get(nodes[j]);
125
+ if (!a || !b) continue;
126
+ if ([...a.fps].some((fp) => b.fps.has(fp))) continue;
127
+ candidates.push({
128
+ kind: "title-overlap",
129
+ severity: "advisory",
130
+ nodes: [nodes[i], nodes[j]],
131
+ scenario: a.title,
132
+ detail: "same scenario title, differing steps — a weaker overlap hint the Warden judges"
133
+ });
134
+ }
135
+ }
136
+ return candidates;
137
+ }
138
+ function hasBlocking(candidates) {
139
+ return candidates.some((c) => c.severity === "blocking");
140
+ }
141
+ function renderCandidates(candidates, specDir) {
142
+ const blocking = candidates.filter((c) => c.severity === "blocking");
143
+ const advisory = candidates.filter((c) => c.severity === "advisory");
144
+ const lines = [`check-scenario-overlap: spec-dir=${specDir}`];
145
+ lines.push(`blocking[${blocking.length}]:`);
146
+ for (const c of blocking) lines.push(` ${c.nodes[0]} <-> ${c.nodes[1]} — ${c.kind}: "${c.scenario}" — ${c.detail}`);
147
+ lines.push(`advisory[${advisory.length}]:`);
148
+ for (const c of advisory) lines.push(` ${c.nodes[0]} <-> ${c.nodes[1]} — ${c.kind}: "${c.scenario}" — ${c.detail}`);
149
+ lines.push("note: advisory — candidates feed the Warden formation pass; the engine writes nothing");
150
+ return lines.join("\n");
151
+ }
152
+ function main(argv) {
153
+ let specDir = ".";
154
+ let mode = "audit";
155
+ let format = "toon";
156
+ for (let i = 0; i < argv.length; i++) {
157
+ const a = argv[i];
158
+ if (a === "--spec-dir") specDir = argv[++i] ?? ".";
159
+ else if (a === "--check") mode = "check";
160
+ else if (a === "--format") format = argv[++i] ?? "toon";
161
+ }
162
+ const candidates = detect(scanSuites(specDir));
163
+ if (mode === "check") {
164
+ if (hasBlocking(candidates)) {
165
+ process.stderr.write(`check-scenario-overlap: ${candidates.filter((c) => c.severity === "blocking").length} exact-duplicate candidate(s)\n`);
166
+ return 1;
167
+ }
168
+ process.stdout.write("check-scenario-overlap: no exact-duplicate candidates\n");
169
+ return 0;
170
+ }
171
+ if (format === "json") process.stdout.write(`${JSON.stringify(candidates)}\n`);
172
+ else process.stdout.write(`${renderCandidates(candidates, specDir)}\n`);
173
+ return 0;
174
+ }
175
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
176
+ //#endregion
177
+ export { detect, hasBlocking, main, normalize, parseFeature, renderCandidates, scanSuites };
@@ -12,8 +12,8 @@ The concrete engine for the **spec-reference resolution check**. It walks every
12
12
  project spec, extracts each **explicitly-relative** reference, resolves it **against the file's own
13
13
  directory**, and fails on every one that resolves to nothing. Read-only, deterministic, and — unlike
14
14
  its `check-spec-structure` sibling — single-severity: a reference that does not resolve is always a
15
- defect, so there is one mode, not a report mode and a gate mode. It carries a self-contained `.mts`
16
- script (the repo's node-≥23.6 / no-deps convention).
15
+ defect, so there is one mode, not a report mode and a gate mode. It carries a self-contained `.mjs`
16
+ script (compiled from `src/`; no runtime deps, Node ≥22.11).
17
17
 
18
18
  It closes a failure **review cannot catch**: a consistent off-by-one reads as entirely plausible —
19
19
  right filename, right-looking depth, wrong level — so re-reading a reference by eye is not a check