cyber-sdd 0.7.0 → 0.7.1

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 (149) hide show
  1. package/.claude-plugin/plugin.json +1 -2
  2. package/.codex-plugin/plugin.json +1 -2
  3. package/agents/sdd-scanner.md +1 -1
  4. package/package.json +11 -13
  5. package/plugin.json +55 -0
  6. package/skills/align-spec/SKILL.md +1 -1
  7. package/skills/align-spec/scripts/align-spec.mjs +6755 -12
  8. package/skills/blast-estimate/README.md +3 -3
  9. package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
  10. package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
  11. package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
  12. package/skills/check-plan-safety/SKILL.md +2 -3
  13. package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
  14. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
  15. package/skills/check-project-specs/SKILL.md +1 -1
  16. package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
  17. package/skills/check-retired-terms/SKILL.md +2 -3
  18. package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
  19. package/skills/check-scenario-overlap/SKILL.md +1 -2
  20. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
  21. package/skills/check-spec-references/SKILL.md +2 -2
  22. package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
  23. package/skills/check-spec-structure/SKILL.md +1 -2
  24. package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
  25. package/skills/collision-ladder/README.md +2 -2
  26. package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
  27. package/skills/combat-log-governance/SKILL.md +1 -1
  28. package/skills/concept-index/SKILL.md +1 -1
  29. package/skills/concept-index/scripts/concept-index.mjs +183 -12
  30. package/skills/discover-plans/README.md +3 -3
  31. package/skills/discover-plans/SKILL.md +1 -1
  32. package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
  33. package/skills/discover-specs/README.md +3 -3
  34. package/skills/discover-specs/SKILL.md +1 -1
  35. package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
  36. package/skills/doctrine-loop/SKILL.md +1 -1
  37. package/skills/gate-validation-governance/SKILL.md +1 -1
  38. package/skills/init/README.md +2 -2
  39. package/skills/init/scripts/wire-statusline.mjs +168 -12
  40. package/skills/manage-ignore/README.md +3 -3
  41. package/skills/manage-ignore/SKILL.md +1 -2
  42. package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
  43. package/skills/manage-scenario-bridge/README.md +3 -3
  44. package/skills/manage-scenario-bridge/SKILL.md +1 -1
  45. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mjs +239 -12
  46. package/skills/manage-spec-anchors/README.md +3 -3
  47. package/skills/manage-spec-anchors/SKILL.md +1 -1
  48. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
  49. package/skills/mission-graph/README.md +2 -2
  50. package/skills/mission-graph/SKILL.md +1 -1
  51. package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
  52. package/skills/pause-mission/SKILL.md +1 -1
  53. package/skills/place-node/SKILL.md +2 -2
  54. package/skills/place-node/scripts/place-node.mjs +117 -12
  55. package/skills/plan-retirement/README.md +2 -2
  56. package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
  57. package/skills/resolve-governances/README.md +3 -3
  58. package/skills/resolve-governances/SKILL.md +1 -1
  59. package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
  60. package/skills/resolve-tracking/SKILL.md +1 -1
  61. package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
  62. package/skills/scaffold-project-spec/SKILL.md +1 -1
  63. package/skills/spec-gate/README.md +12 -9
  64. package/skills/spec-gate/SKILL.md +2 -2
  65. package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
  66. package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
  67. package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
  68. package/skills/spec-producer-governance/SKILL.md +2 -2
  69. package/skills/start-mission/SKILL.md +1 -1
  70. package/skills/suite-format-governance/README.md +1 -1
  71. package/skills/suite-format-governance/SKILL.md +1 -1
  72. package/skills/touch-set-correction/README.md +5 -5
  73. package/skills/touch-set-correction/SKILL.md +1 -1
  74. package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
  75. package/skills/verify-scenarios/README.md +6 -6
  76. package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
  77. package/.plugin/pins.json +0 -3
  78. package/.plugin/plugin.json +0 -19
  79. package/dist/align-spec/scripts/align-spec.js +0 -102
  80. package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
  81. package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
  82. package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
  83. package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
  84. package/dist/concept-index/scripts/concept-index.js +0 -181
  85. package/dist/discover-specs/scripts/discover-specs.js +0 -318
  86. package/dist/spec-gate/scripts/check-spec-state.js +0 -420
  87. package/dist/spec-gate/scripts/check-suite.js +0 -317
  88. package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
  89. package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
  90. package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
  91. package/skills/align-spec/scripts/align-spec.mts +0 -188
  92. package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
  93. package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
  94. package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
  95. package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
  96. package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
  97. package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
  98. package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
  99. package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
  100. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
  101. package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
  102. package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
  103. package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
  104. package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
  105. package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
  106. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
  107. package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
  108. package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
  109. package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
  110. package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
  111. package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
  112. package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
  113. package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
  114. package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
  115. package/skills/concept-index/scripts/concept-index.mts +0 -246
  116. package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
  117. package/skills/discover-plans/scripts/discover-plans.mts +0 -215
  118. package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
  119. package/skills/discover-specs/scripts/discover-specs.mts +0 -420
  120. package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
  121. package/skills/init/scripts/wire-statusline.mts +0 -279
  122. package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
  123. package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
  124. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
  125. package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
  126. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
  127. package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
  128. package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
  129. package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
  130. package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
  131. package/skills/place-node/scripts/place-node.mts +0 -158
  132. package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
  133. package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
  134. package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
  135. package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
  136. package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
  137. package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
  138. package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
  139. package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
  140. package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
  141. package/skills/spec-gate/scripts/check-suite.mts +0 -538
  142. package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
  143. package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
  144. package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
  145. package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
  146. package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
  147. package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
  148. package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
  149. package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
package/.plugin/pins.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "cyberlegion": "0.3.1"
3
- }
@@ -1,19 +0,0 @@
1
- {
2
- "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
- "name": "sdd",
4
- "description": "Spec-Driven Development. Scaffold, validate, and maintain behavioral specs (spec.md + .feature files) for software features.",
5
- "author": {
6
- "name": "unional"
7
- },
8
- "homepage": "https://github.com/cyberuni/cyber-sdd",
9
- "repository": "https://github.com/cyberuni/cyber-sdd",
10
- "version": "0.7.0",
11
- "skills": "./skills",
12
- "agents": [
13
- "./agents/sdd-automaton.md",
14
- "./agents/sdd-impl-judge.md",
15
- "./agents/sdd-scanner.md",
16
- "./agents/sdd-spec-judge.md",
17
- "./agents/sdd-warden.md"
18
- ]
19
- }
@@ -1,102 +0,0 @@
1
- #!/usr/bin/env node
2
- import { scanProjectSpec } from "../../check-spec-structure/scripts/check-spec-structure.js";
3
- import { classifyFile } from "../../spec-gate/scripts/classify-edit-class.js";
4
- import { existsSync, readdirSync, realpathSync } from "node:fs";
5
- import { isAbsolute, join, relative } from "node:path";
6
- import { pathToFileURL } from "node:url";
7
- //#region skills/align-spec/scripts/align-spec.mts
8
- const DEFAULT_BASE = "HEAD";
9
- function selectNodes(records, only) {
10
- if (!only || only.length === 0) return records;
11
- const wanted = new Set(only.map(normalizeNodeRef));
12
- return records.filter((r) => wanted.has(normalizeNodeRef(r.display)) || wanted.has(normalizeNodeRef(r.relPath)));
13
- }
14
- function normalizeNodeRef(ref) {
15
- return ref.replace(/\\/g, "/").replace(/\/README\.md$/, "").replace(/\/+$/, "");
16
- }
17
- function findFeaturePath(specDir, node) {
18
- const nodeDir = join(specDir, node.relPath.replace(/README\.md$/, ""));
19
- if (!existsSync(nodeDir)) return null;
20
- const feature = readdirSync(nodeDir, { withFileTypes: true }).find((e) => e.isFile() && e.name.endsWith(".feature"));
21
- return feature ? join(nodeDir, feature.name) : null;
22
- }
23
- function detectNarrowing(node, specDir, base, cwd = ".") {
24
- const featurePath = findFeaturePath(specDir, node);
25
- if (!featurePath) return [];
26
- const relFeaturePath = isAbsolute(featurePath) ? relative(cwd, featurePath) : featurePath;
27
- const result = classifyFile(relFeaturePath, base, cwd);
28
- if (result.classification !== "narrowing" && result.classification !== "mixed") return [];
29
- const narrowed = result.scenarios.filter((s) => s.change === "modified" || s.change === "removed");
30
- return [{
31
- kind: "narrowing",
32
- severity: "clearance",
33
- node: node.display,
34
- detail: `scenario-diff vs ${base} narrows: ${narrowed.map((s) => s.name).join(", ")} — escalate a Clearance CR, do not silently rewrite`
35
- }];
36
- }
37
- function detect(records, specDir, base, cwd = ".") {
38
- return records.flatMap((node) => detectNarrowing(node, specDir, base, cwd));
39
- }
40
- function hasDrift(findings) {
41
- return findings.length > 0;
42
- }
43
- function splitFrontmatter(text) {
44
- const m = /^(---\r?\n[\s\S]*?\r?\n---\s*(?:\r?\n|$))([\s\S]*)$/.exec(text);
45
- if (!m) return {
46
- frontmatter: "",
47
- body: text
48
- };
49
- return {
50
- frontmatter: m[1],
51
- body: m[2]
52
- };
53
- }
54
- function trimProse(readmeText, proseToRemove) {
55
- const { frontmatter, body } = splitFrontmatter(readmeText);
56
- return frontmatter + body.split(proseToRemove).join("");
57
- }
58
- function appendScenario(featureText, scenarioBlock) {
59
- return `${featureText.replace(/\s+$/, "")}\n\n${scenarioBlock.trim()}\n`;
60
- }
61
- function renderFindings(findings, specDir) {
62
- const lines = [`align-spec: spec-dir=${specDir}`, `drift[${findings.length}]:`];
63
- for (const f of findings) lines.push(` ${f.node} — ${f.kind} (${f.severity}): ${f.detail}`);
64
- if (findings.length === 0) lines.push(" (none — mechanical scenario-diff found no narrowing)");
65
- lines.push("note: coverage gaps and prose/scenario contradictions are judge-only (no scenario IDs in prose) — this engine reports only the mechanical scenario-diff");
66
- return lines.join("\n");
67
- }
68
- function parseNodesArg(value) {
69
- if (!value) return void 0;
70
- const parts = value.split(",").map((s) => s.trim()).filter(Boolean);
71
- return parts.length > 0 ? parts : void 0;
72
- }
73
- function main(argv) {
74
- let specDir = ".";
75
- let base = DEFAULT_BASE;
76
- let mode = "detect";
77
- let format = "toon";
78
- let only;
79
- for (let i = 0; i < argv.length; i++) {
80
- const a = argv[i];
81
- if (a === "--spec-dir") specDir = argv[++i] ?? ".";
82
- else if (a === "--check") mode = "check";
83
- else if (a === "--base") base = argv[++i] ?? "HEAD";
84
- else if (a === "--format") format = argv[++i] ?? "toon";
85
- else if (a === "--nodes") only = parseNodesArg(argv[++i]);
86
- }
87
- const findings = detect(selectNodes(scanProjectSpec(specDir), only), specDir, base);
88
- if (mode === "check") {
89
- if (hasDrift(findings)) {
90
- process.stderr.write(`align-spec: ${findings.length} drift finding(s) (mechanical scenario-diff)\n`);
91
- return 1;
92
- }
93
- process.stdout.write("align-spec: no mechanical drift\n");
94
- return 0;
95
- }
96
- if (format === "json") process.stdout.write(`${JSON.stringify(findings)}\n`);
97
- else process.stdout.write(`${renderFindings(findings, specDir)}\n`);
98
- return 0;
99
- }
100
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
101
- //#endregion
102
- export { DEFAULT_BASE, appendScenario, detect, detectNarrowing, findFeaturePath, hasDrift, main, renderFindings, selectNodes, splitFrontmatter, trimProse };
@@ -1,312 +0,0 @@
1
- #!/usr/bin/env node
2
- import { collectSpecs, discoverSpecFiles, parseFrontmatter } from "../../discover-specs/scripts/discover-specs.js";
3
- import { execFileSync } from "node:child_process";
4
- import { existsSync, readFileSync, realpathSync } from "node:fs";
5
- import { dirname, join, relative, resolve } from "node:path";
6
- import { fileURLToPath, pathToFileURL } from "node:url";
7
- //#region skills/check-project-specs/scripts/check-project-specs.mts
8
- const HERE = fileURLToPath(import.meta.url);
9
- const ENGINES = [
10
- {
11
- name: "check-spec-state",
12
- script: "spec-gate/scripts/check-spec-state.mts",
13
- args: (d) => ["--root", d]
14
- },
15
- {
16
- name: "check-suite",
17
- script: "spec-gate/scripts/check-suite.mts",
18
- args: (d) => ["--root", d]
19
- },
20
- {
21
- name: "concept-index",
22
- script: "concept-index/scripts/concept-index.mts",
23
- args: (d) => [
24
- "--spec-dir",
25
- d,
26
- "--check"
27
- ]
28
- },
29
- {
30
- name: "check-spec-structure",
31
- script: "check-spec-structure/scripts/check-spec-structure.mts",
32
- args: (d) => [
33
- "--spec-dir",
34
- d,
35
- "--check"
36
- ]
37
- },
38
- {
39
- name: "align-spec",
40
- script: "align-spec/scripts/align-spec.mts",
41
- args: (d) => [
42
- "--spec-dir",
43
- d,
44
- "--check"
45
- ]
46
- },
47
- {
48
- name: "check-spec-references",
49
- script: "check-spec-references/scripts/check-spec-references.mts",
50
- args: (d) => ["--spec-dir", d]
51
- },
52
- {
53
- name: "check-scenario-overlap",
54
- script: "check-scenario-overlap/scripts/check-scenario-overlap.mts",
55
- args: (d) => [
56
- "--spec-dir",
57
- d,
58
- "--check"
59
- ]
60
- }
61
- ];
62
- /**
63
- * Where an engine runs from. The .mts source runs its siblings in the skills tree; the
64
- * published bin runs from dist/, which mirrors that tree compiled to .js (tsdown.config.ts)
65
- * — Node will not strip types under node_modules, so the bin must never reach back into
66
- * the .mts tree.
67
- */
68
- function enginePath(e, here = HERE) {
69
- const skillsDir = resolve(dirname(here), "../..");
70
- if (here.endsWith(".bundle.mjs")) return join(skillsDir, e.script.replace(/\.mts$/, ".bundle.mjs"));
71
- return join(skillsDir, here.endsWith(".mts") ? e.script : e.script.replace(/\.mts$/, ".js"));
72
- }
73
- /** Walk up from `start` for the workspace marker. Returns '' when not found. */
74
- function findRepoRoot(start) {
75
- let dir = resolve(start);
76
- for (;;) {
77
- if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir;
78
- const up = dirname(dir);
79
- if (up === dir) return "";
80
- dir = up;
81
- }
82
- }
83
- /**
84
- * Find the spec whose `project-path` names `projectRel` (a repo-relative dir).
85
- * A project with no spec is `none` — not an error: the script is uniform across
86
- * every workspace member, and some members are governed by no spec.
87
- */
88
- function resolveSpecFor(specs, projectRel) {
89
- const hits = specs.filter((s) => s.projectPath !== "" && s.projectPath === projectRel);
90
- if (hits.length === 0) return { kind: "none" };
91
- if (hits.length > 1) return {
92
- kind: "ambiguous",
93
- specs: hits
94
- };
95
- return {
96
- kind: "resolved",
97
- spec: hits[0]
98
- };
99
- }
100
- /**
101
- * Every spec must be reachable from a project that actually checks it. Without
102
- * this, a spec silently goes unchecked the moment its project drops the script —
103
- * which is exactly how the corpus ended up with one audited project out of ten.
104
- *
105
- * `specFiles` are the spec.md paths found at the recognized locations *before*
106
- * the lifecycle-status filter; `specs` are the ones that survived it. A file in
107
- * the first set and not the second is a spec whose status is not in the enum:
108
- * discovery drops it, so every engine silently skips it and it is checked by
109
- * nothing. That is a gap to escalate, not to exempt.
110
- */
111
- function findCoverageGaps(root, specFiles, specs, readPkg) {
112
- const gaps = [];
113
- const recognized = new Set(specs.map((s) => s.path === "" ? "spec.md" : `${s.path}/spec.md`));
114
- for (const f of specFiles) if (!recognized.has(f)) gaps.push({
115
- spec: f,
116
- projectPath: "",
117
- reason: "unrecognized"
118
- });
119
- for (const s of specs) {
120
- if (s.projectPath === "") {
121
- gaps.push({
122
- spec: s.path,
123
- projectPath: "",
124
- reason: "no-project-path"
125
- });
126
- continue;
127
- }
128
- const pkg = readPkg(join(root, s.projectPath, "package.json"));
129
- if (!pkg) {
130
- gaps.push({
131
- spec: s.path,
132
- projectPath: s.projectPath,
133
- reason: "no-manifest"
134
- });
135
- continue;
136
- }
137
- if (!pkg.scripts?.["check:spec"]) gaps.push({
138
- spec: s.path,
139
- projectPath: s.projectPath,
140
- reason: "no-check-script"
141
- });
142
- }
143
- return gaps;
144
- }
145
- /**
146
- * A spec.md sitting at a recognized location that discovery DROPPED (its status is not
147
- * in the lifecycle enum) and that belongs to `projectRel` — by the `project-path` it
148
- * declares, or by sitting inside the project (`<project>/.agents/spec/spec.md`, which
149
- * survives frontmatter corruption because it is location-derived).
150
- *
151
- * Without this, a per-project run resolves such a spec to `none` and prints "no spec
152
- * governs <project> — skipped" with exit 0: a status typo silently exempts the whole
153
- * project from every engine. A spec that exists but cannot be classified is escalated,
154
- * not exempted — the same call the corpus scope’s coverage guard makes.
155
- */
156
- function findDroppedSpecFor(specFiles, specs, projectRel, readText) {
157
- const recognized = new Set(specs.map((s) => s.path === "" ? "spec.md" : `${s.path}/spec.md`));
158
- const out = [];
159
- for (const f of specFiles) {
160
- if (recognized.has(f)) continue;
161
- const text = readText(f);
162
- const fm = text === null ? null : parseFrontmatter(text);
163
- const dir = f.replace(/(^|\/)spec\.md$/, "");
164
- const nested = /^(.+)\/\.agents\/spec$/.exec(dir)?.[1] ?? "";
165
- if (fm?.projectPath === projectRel || nested === projectRel) out.push({
166
- file: f,
167
- status: fm?.status ?? ""
168
- });
169
- }
170
- return out;
171
- }
172
- const REASON_TEXT = {
173
- unrecognized: "sits at a spec location but its status is not in the lifecycle enum, so discovery drops it and nothing checks it",
174
- "no-project-path": "declares no project-path, so no project can be resolved to check it",
175
- "no-manifest": "names a project with no package.json, so it is not a workspace member",
176
- "no-check-script": "names a project that defines no `check:spec` script"
177
- };
178
- /** Read a file, or null when it cannot be read. */
179
- function readTextOrNull(path) {
180
- try {
181
- return readFileSync(path, "utf8");
182
- } catch {
183
- return null;
184
- }
185
- }
186
- /**
187
- * Run the whole engine set against one project-spec dir. Every engine runs even
188
- * after one fails: a run that stops at the first failure reports one defect per
189
- * invocation, and the corpus scope exists to report the tree's defects in one go.
190
- */
191
- function runEngines(repoRoot, specDir) {
192
- let failed = 0;
193
- for (const e of ENGINES) try {
194
- execFileSync("node", [enginePath(e), ...e.args(specDir)], {
195
- cwd: repoRoot,
196
- stdio: "inherit"
197
- });
198
- process.stdout.write(` ok ${e.name}\n`);
199
- } catch {
200
- process.stderr.write(` FAIL ${e.name}\n`);
201
- failed++;
202
- }
203
- return failed;
204
- }
205
- function checkCoverage(root) {
206
- const gaps = findCoverageGaps(root, discoverSpecFiles(root), collectSpecs(root), (p) => {
207
- try {
208
- return JSON.parse(readFileSync(p, "utf8"));
209
- } catch {
210
- return null;
211
- }
212
- });
213
- if (gaps.length === 0) {
214
- process.stdout.write("check-project-specs: every spec is checked by its project\n");
215
- return 0;
216
- }
217
- for (const g of gaps) process.stderr.write(` ${g.spec} — ${REASON_TEXT[g.reason]}\n`);
218
- process.stderr.write(`check-project-specs: ${gaps.length} spec(s) no project checks\n`);
219
- return 1;
220
- }
221
- /** The flags the harness defines. Anything else is an error, never a default. */
222
- const KNOWN_FLAGS = ["--corpus", "--project"];
223
- /**
224
- * The scope is chosen by a flag, so an unrecognized one must not fall through to a
225
- * default. It used to: project scope at a repo root resolves no governing spec and
226
- * exits 0, so a misspelled or retired flag reported success having checked nothing.
227
- * That is how `--check-coverage` guarded commits and CI while running no engine.
228
- */
229
- function unknownFlags(argv) {
230
- const out = [];
231
- for (let i = 0; i < argv.length; i++) {
232
- const a = argv[i];
233
- if (!a.startsWith("-")) continue;
234
- if (a === "--project") {
235
- i++;
236
- continue;
237
- }
238
- if (!KNOWN_FLAGS.includes(a)) out.push(a);
239
- }
240
- return out;
241
- }
242
- function main(argv) {
243
- const unknown = unknownFlags(argv);
244
- if (unknown.length) {
245
- process.stderr.write(`check-project-specs: unrecognized flag(s) ${unknown.join(", ")} — the scope is ${KNOWN_FLAGS.join(" | ")} (or none, meaning the cwd's project)\n`);
246
- return 1;
247
- }
248
- if (argv.includes("--corpus") && argv.includes("--project")) {
249
- process.stderr.write("check-project-specs: --corpus and --project name contradictory scopes — pass one\n");
250
- return 1;
251
- }
252
- if (argv.includes("--corpus")) return checkCorpus();
253
- return checkProject(argv);
254
- }
255
- /**
256
- * Corpus scope — the commit floor. Total by definition: every project-spec the
257
- * corpus holds is checked, and a spec that cannot be classified fails rather than
258
- * being skipped.
259
- *
260
- * It runs BOTH sub-checks because neither subsumes the other. The sweep iterates
261
- * the specs discovery *recognizes*, so a spec whose lifecycle `status` is a typo is
262
- * invisible to it — and would report clean. The coverage guard sees that file on
263
- * disk and escalates it, but says nothing about whether the engines pass. Run either
264
- * alone and a whole project-spec leaves the floor silently.
265
- */
266
- function checkCorpus() {
267
- const root = findRepoRoot(process.cwd());
268
- if (!root) {
269
- process.stderr.write("check-project-specs: no pnpm-workspace.yaml found above the cwd\n");
270
- return 1;
271
- }
272
- let failed = checkCoverage(root) === 0 ? 0 : 1;
273
- const specs = collectSpecs(root);
274
- if (specs.length === 0) {
275
- process.stdout.write("check-project-specs: the corpus holds no project-spec\n");
276
- return failed;
277
- }
278
- for (const s of specs) {
279
- process.stdout.write(`check-project-specs: ${s.path}\n`);
280
- if (runEngines(root, join(root, s.path)) > 0) failed = 1;
281
- }
282
- return failed;
283
- }
284
- function checkProject(argv) {
285
- const projectArg = argv.includes("--project") ? argv[argv.indexOf("--project") + 1] : void 0;
286
- const projectDir = resolve(projectArg ?? process.cwd());
287
- const repoRoot = findRepoRoot(projectDir);
288
- if (!repoRoot) {
289
- process.stderr.write(`check-project-specs: no pnpm-workspace.yaml found above ${projectDir}\n`);
290
- return 1;
291
- }
292
- const projectRel = relative(repoRoot, projectDir) || ".";
293
- const specs = collectSpecs(repoRoot);
294
- const res = resolveSpecFor(specs, projectRel);
295
- if (res.kind === "none") {
296
- const dropped = findDroppedSpecFor(discoverSpecFiles(repoRoot), specs, projectRel, (rel) => readTextOrNull(join(repoRoot, rel)));
297
- for (const d of dropped) process.stderr.write(`check-project-specs: \`${d.file}\` sits at a spec location and governs \`${projectRel}\` but ` + (d.status === "" ? "declares no lifecycle status" : `its status \`${d.status}\` is not in the lifecycle enum`) + " (draft | approved | implemented | deprecated) — discovery drops it, so it is checked by nothing\n");
298
- if (dropped.length) return 1;
299
- process.stdout.write(`check-project-specs: no spec governs \`${projectRel}\` — skipped\n`);
300
- return 0;
301
- }
302
- if (res.kind === "ambiguous") {
303
- process.stderr.write(`check-project-specs: \`${projectRel}\` is claimed by ${res.specs.length} specs (${res.specs.map((s) => s.path).join(", ")}) — a project has exactly one spec\n`);
304
- return 1;
305
- }
306
- const specDir = join(repoRoot, res.spec.path);
307
- process.stdout.write(`check-project-specs: ${projectRel} -> ${res.spec.path}\n`);
308
- return runEngines(repoRoot, specDir) === 0 ? 0 : 1;
309
- }
310
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
311
- //#endregion
312
- export { ENGINES, KNOWN_FLAGS, enginePath, findCoverageGaps, findDroppedSpecFor, findRepoRoot, main, resolveSpecFor, unknownFlags };
@@ -1,175 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFileSync, readdirSync, realpathSync } from "node:fs";
3
- import { join } from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- //#region skills/check-scenario-overlap/scripts/check-scenario-overlap.mts
6
- const SKIP_DIRS = /* @__PURE__ */ new Set([
7
- "node_modules",
8
- ".git",
9
- "dist",
10
- ".turbo",
11
- ".next",
12
- "coverage"
13
- ]);
14
- function normalize(s) {
15
- return s.trim().replace(/\s+/g, " ").toLowerCase();
16
- }
17
- const STEP = /^\s*(Given|When|Then|And|But)\s+(.*\S)\s*$/;
18
- const SCENARIO = /^\s*Scenario(?: Outline)?:\s*(.*\S)\s*$/;
19
- const TABLE_ROW = /^\s*\|(.*)\|\s*$/;
20
- function normalizeRow(line) {
21
- return (TABLE_ROW.exec(line)?.[1] ?? line).split("|").map((cell) => normalize(cell)).join("|");
22
- }
23
- function parseFeature(featureText) {
24
- const scenarios = [];
25
- let title = null;
26
- let steps = [];
27
- let exampleRows = [];
28
- const flush = () => {
29
- if (title !== null) {
30
- const fingerprint = exampleRows.length === 0 ? steps.map(normalize).join("\n") : `${steps.map(normalize).join("\n")}\n EXAMPLES \n${exampleRows.join("\n")}`;
31
- scenarios.push({
32
- title,
33
- titleKey: normalize(title),
34
- fingerprint
35
- });
36
- }
37
- title = null;
38
- steps = [];
39
- exampleRows = [];
40
- };
41
- for (const line of featureText.split("\n")) {
42
- const sc = SCENARIO.exec(line);
43
- if (sc) {
44
- flush();
45
- title = sc[1];
46
- continue;
47
- }
48
- if (title === null) continue;
49
- const st = STEP.exec(line);
50
- if (st) {
51
- steps.push(st[2]);
52
- continue;
53
- }
54
- if (TABLE_ROW.test(line)) exampleRows.push(normalizeRow(line));
55
- }
56
- flush();
57
- return scenarios;
58
- }
59
- function displayPath(relPath) {
60
- const p = relPath.replace(/\\/g, "/");
61
- return p.endsWith("/README.md") ? p.slice(0, -9) : p;
62
- }
63
- function scanSuites(specDir) {
64
- const suites = [];
65
- walk(specDir, specDir, suites);
66
- return suites.sort((a, b) => a.node.localeCompare(b.node));
67
- }
68
- function walk(dir, specDir, out) {
69
- const entries = readdirSync(dir, { withFileTypes: true });
70
- for (const entry of entries) {
71
- if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) continue;
72
- const full = join(dir, entry.name);
73
- if (entry.isDirectory()) {
74
- walk(full, specDir, out);
75
- continue;
76
- }
77
- if (!entry.name.endsWith(".feature")) continue;
78
- const scenarios = parseFeature(readFileSync(full, "utf8"));
79
- if (scenarios.length === 0) continue;
80
- const relDir = dir.slice(specDir.length + 1).replace(/\\/g, "/");
81
- out.push({
82
- node: displayPath(`${relDir}/`),
83
- scenarios
84
- });
85
- }
86
- }
87
- function detect(suites) {
88
- const byFingerprint = /* @__PURE__ */ new Map();
89
- const byTitle = /* @__PURE__ */ new Map();
90
- for (const suite of suites) for (const sc of suite.scenarios) {
91
- if (sc.fingerprint !== "") {
92
- let m = byFingerprint.get(sc.fingerprint);
93
- if (!m) byFingerprint.set(sc.fingerprint, m = /* @__PURE__ */ new Map());
94
- if (!m.has(suite.node)) m.set(suite.node, sc.title);
95
- }
96
- let t = byTitle.get(sc.titleKey);
97
- if (!t) byTitle.set(sc.titleKey, t = /* @__PURE__ */ new Map());
98
- let e = t.get(suite.node);
99
- if (!e) t.set(suite.node, e = {
100
- fps: /* @__PURE__ */ new Set(),
101
- title: sc.title
102
- });
103
- e.fps.add(sc.fingerprint);
104
- }
105
- const candidates = [];
106
- for (const [, nodeMap] of byFingerprint) {
107
- if (nodeMap.size < 2) continue;
108
- const nodes = [...nodeMap.keys()].sort();
109
- for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) candidates.push({
110
- kind: "exact-duplicate",
111
- severity: "blocking",
112
- nodes: [nodes[i], nodes[j]],
113
- scenario: nodeMap.get(nodes[i]) ?? "",
114
- detail: "identical step fingerprint — the same behavior is specified in both nodes; one behavior = one owning node"
115
- });
116
- }
117
- for (const [, nodeMap] of byTitle) {
118
- if (nodeMap.size < 2) continue;
119
- const nodes = [...nodeMap.keys()].sort();
120
- for (let i = 0; i < nodes.length; i++) for (let j = i + 1; j < nodes.length; j++) {
121
- const a = nodeMap.get(nodes[i]);
122
- const b = nodeMap.get(nodes[j]);
123
- if (!a || !b) continue;
124
- if ([...a.fps].some((fp) => b.fps.has(fp))) continue;
125
- candidates.push({
126
- kind: "title-overlap",
127
- severity: "advisory",
128
- nodes: [nodes[i], nodes[j]],
129
- scenario: a.title,
130
- detail: "same scenario title, differing steps — a weaker overlap hint the Warden judges"
131
- });
132
- }
133
- }
134
- return candidates;
135
- }
136
- function hasBlocking(candidates) {
137
- return candidates.some((c) => c.severity === "blocking");
138
- }
139
- function renderCandidates(candidates, specDir) {
140
- const blocking = candidates.filter((c) => c.severity === "blocking");
141
- const advisory = candidates.filter((c) => c.severity === "advisory");
142
- const lines = [`check-scenario-overlap: spec-dir=${specDir}`];
143
- lines.push(`blocking[${blocking.length}]:`);
144
- for (const c of blocking) lines.push(` ${c.nodes[0]} <-> ${c.nodes[1]} — ${c.kind}: "${c.scenario}" — ${c.detail}`);
145
- lines.push(`advisory[${advisory.length}]:`);
146
- for (const c of advisory) lines.push(` ${c.nodes[0]} <-> ${c.nodes[1]} — ${c.kind}: "${c.scenario}" — ${c.detail}`);
147
- lines.push("note: advisory — candidates feed the Warden formation pass; the engine writes nothing");
148
- return lines.join("\n");
149
- }
150
- function main(argv) {
151
- let specDir = ".";
152
- let mode = "audit";
153
- let format = "toon";
154
- for (let i = 0; i < argv.length; i++) {
155
- const a = argv[i];
156
- if (a === "--spec-dir") specDir = argv[++i] ?? ".";
157
- else if (a === "--check") mode = "check";
158
- else if (a === "--format") format = argv[++i] ?? "toon";
159
- }
160
- const candidates = detect(scanSuites(specDir));
161
- if (mode === "check") {
162
- if (hasBlocking(candidates)) {
163
- process.stderr.write(`check-scenario-overlap: ${candidates.filter((c) => c.severity === "blocking").length} exact-duplicate candidate(s)\n`);
164
- return 1;
165
- }
166
- process.stdout.write("check-scenario-overlap: no exact-duplicate candidates\n");
167
- return 0;
168
- }
169
- if (format === "json") process.stdout.write(`${JSON.stringify(candidates)}\n`);
170
- else process.stdout.write(`${renderCandidates(candidates, specDir)}\n`);
171
- return 0;
172
- }
173
- if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
174
- //#endregion
175
- export { detect, hasBlocking, main, normalize, parseFeature, renderCandidates, scanSuites };