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
@@ -1,12 +1,253 @@
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-field-mandates/check-field-mandates.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/check-field-mandates/scripts/check-field-mandates.mjs","commands":{}};
4
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
5
+ import { join, relative, sep } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/check-field-mandates/check-field-mandates.mts
8
+ /** UPPER_CASE, or snake_case with at least one underscore — a bare lowercase word is prose. */
9
+ const TOKEN = "(?:[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*|[a-z][a-z0-9]*(?:_[a-z0-9]+)+)";
10
+ const TOKEN_ITEM = `${TOKEN}(?:\\(s\\))?`;
11
+ /** A block line opening with a comma list of tokens, then a colon, a trailing comment, or nothing. */
12
+ const BLOCK_DECLARATION_RE = new RegExp(`^\\s*(${TOKEN_ITEM}(?:\\s*,\\s*${TOKEN_ITEM})*)\\s*(:.*|#.*)?$`);
13
+ /** A list item opening with one or more bold tokens joined by `+`. */
14
+ const LIST_DECLARATION_RE = new RegExp(`^\\s*[-*+]\\s+(\\*\\*${TOKEN}\\*\\*(?:\\s*\\+\\s*\\*\\*${TOKEN}\\*\\*)*)(.*)$`);
15
+ const BOLD_TOKEN_RE = new RegExp(`\\*\\*(${TOKEN})\\*\\*`, "g");
16
+ const WORD_RE = new RegExp(`(?<![A-Za-z0-9_])${TOKEN}(?![A-Za-z0-9_])`, "g");
17
+ const CODE_SPAN_RE = /`([^`\n]+)`/g;
18
+ const MANDATE_LEAD_RE = new RegExp(`^<?(${TOKEN})(?=$|[\\s.:\\[\\]>(,=])`);
19
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
20
+ /** The marker must carry a reason — a bare marker excuses nothing. */
21
+ const IGNORE_MARKER_RE = /<!--\s*field-mandate-ignore\s*:\s*[^\s>-][^>]*-->/;
22
+ /** A field token is two or more characters: a lone capital is a word, not a field. */
23
+ function isField(token) {
24
+ return token.length >= 2;
25
+ }
26
+ /** A token's case-folded form: the spelling-independent name of the field. */
27
+ function fold(token) {
28
+ return token.toUpperCase();
29
+ }
30
+ /** The folded forms a token may take and still be the same field — equal, or one trailing `S` apart. */
31
+ function sameFieldForms(token) {
32
+ const t = fold(token);
33
+ const forms = [t, `${t}S`];
34
+ if (t.endsWith("S") && t.length > 2) forms.push(t.slice(0, -1));
35
+ return forms;
36
+ }
37
+ /** Does the set of folded tokens hold the same field as `token`? */
38
+ function hasSameField(folded, token) {
39
+ return sameFieldForms(token).some((f) => folded.has(f));
40
+ }
41
+ /** Is the token spelled lowercase (snake_case) rather than uppercase? */
42
+ function isLower(token) {
43
+ return token !== fold(token);
44
+ }
45
+ function parseDefinition(text) {
46
+ const lines = text.split(/\r?\n/);
47
+ const declarations = [];
48
+ const mandates = [];
49
+ const proseWords = /* @__PURE__ */ new Set();
50
+ let i = 0;
51
+ if (lines[0]?.trim() === "---") {
52
+ const end = lines.findIndex((l, n) => n > 0 && l.trim() === "---");
53
+ if (end > 0) i = end + 1;
54
+ }
55
+ let fence = null;
56
+ let group = [];
57
+ /** Record a span of text as prose: its field-shaped words, and any code-span mandate. */
58
+ function scanProse(text, lineNo) {
59
+ for (const m of text.matchAll(WORD_RE)) if (isField(m[0])) proseWords.add(m[0]);
60
+ if (IGNORE_MARKER_RE.test(text)) return;
61
+ for (const m of text.matchAll(CODE_SPAN_RE)) {
62
+ const token = (m[1] ?? "").trim().match(MANDATE_LEAD_RE)?.[1];
63
+ if (token && isField(token)) mandates.push({
64
+ token,
65
+ line: lineNo
66
+ });
67
+ }
68
+ }
69
+ for (; i < lines.length; i++) {
70
+ const line = lines[i] ?? "";
71
+ const lineNo = i + 1;
72
+ const fenceMatch = line.match(FENCE_RE);
73
+ if (fence) {
74
+ if (fenceMatch && fenceMatch[1]?.[0] === fence.char && (fenceMatch[1]?.length ?? 0) >= fence.length && fenceMatch[2]?.trim() === "") {
75
+ fence = null;
76
+ group = [];
77
+ continue;
78
+ }
79
+ if (!fence.structured) continue;
80
+ const decl = line.match(BLOCK_DECLARATION_RE);
81
+ const tokens = decl ? (decl[1] ?? "").split(",").map((t) => t.trim().replace(/\(s\)$/, "")) : [];
82
+ const rest = decl?.[2];
83
+ if (decl !== null && (rest?.startsWith(":") || tokens.length >= 2)) {
84
+ const glossed = rest?.startsWith(":") === true && rest.slice(1).trim() !== "";
85
+ group = tokens.filter(isField).map((token) => ({
86
+ token,
87
+ line: lineNo,
88
+ glossed
89
+ }));
90
+ declarations.push(...group);
91
+ } else if (line.trim() !== "") for (const d of group) d.glossed = true;
92
+ continue;
93
+ }
94
+ if (fenceMatch) {
95
+ const info = fenceMatch[2]?.trim() ?? "";
96
+ fence = {
97
+ char: fenceMatch[1]?.[0] ?? "`",
98
+ length: fenceMatch[1]?.length ?? 3,
99
+ structured: info === "" || info === "text"
100
+ };
101
+ group = [];
102
+ continue;
103
+ }
104
+ const listDecl = line.match(LIST_DECLARATION_RE);
105
+ if (listDecl) {
106
+ const rest = listDecl[2] ?? "";
107
+ const glossed = rest.trim() !== "";
108
+ for (const m of (listDecl[1] ?? "").matchAll(BOLD_TOKEN_RE)) {
109
+ const token = m[1] ?? "";
110
+ if (isField(token)) declarations.push({
111
+ token,
112
+ line: lineNo,
113
+ glossed
114
+ });
115
+ }
116
+ scanProse(rest, lineNo);
117
+ continue;
118
+ }
119
+ scanProse(line, lineNo);
120
+ }
121
+ return {
122
+ declarations,
123
+ mandates,
124
+ proseWords
125
+ };
126
+ }
127
+ function walk(dir, out) {
128
+ let entries;
129
+ try {
130
+ entries = readdirSync(dir, { withFileTypes: true });
131
+ } catch {
132
+ return;
133
+ }
134
+ for (const e of entries) {
135
+ const full = join(dir, e.name);
136
+ if (e.isDirectory()) walk(full, out);
137
+ else if (e.isFile() && e.name.endsWith(".md")) out.push(full);
138
+ }
139
+ }
140
+ /** Is this root-relative path a skill (`plugins/<p>/skills/**\/SKILL.md`) or agent (`plugins/<p>/agents/*.md`)? */
141
+ function isDefinitionPath(rel) {
142
+ const parts = rel.split("/");
143
+ if (parts[0] !== "plugins" || parts.length < 4) return false;
144
+ if (parts[2] === "agents") return parts.length === 4 && (parts[3] ?? "").endsWith(".md");
145
+ if (parts[2] === "skills") return parts.length >= 5 && parts[parts.length - 1] === "SKILL.md";
146
+ return false;
147
+ }
148
+ /** Every definition under `root`, root-relative, `/`-separated, path-sorted. */
149
+ function discoverDefinitions(root) {
150
+ const pluginsDir = join(root, "plugins");
151
+ if (!existsSync(pluginsDir)) return [];
152
+ const files = [];
153
+ walk(pluginsDir, files);
154
+ return files.map((f) => relative(root, f).split(sep).join("/")).filter(isDefinitionPath).sort();
155
+ }
156
+ function check(parsed) {
157
+ const known = /* @__PURE__ */ new Set();
158
+ for (const p of parsed.values()) for (const d of p.declarations) known.add(fold(d.token));
159
+ const findings = [];
160
+ for (const [file, p] of parsed) {
161
+ if (p.declarations.length === 0) continue;
162
+ const declared = new Set(p.declarations.map((d) => fold(d.token)));
163
+ const proseWords = new Set([...p.proseWords].map(fold));
164
+ const seen = /* @__PURE__ */ new Set();
165
+ for (const d of p.declarations) {
166
+ if (seen.has(d.token)) continue;
167
+ seen.add(d.token);
168
+ if (!p.declarations.some((o) => o.token === d.token && o.glossed) && !hasSameField(proseWords, d.token)) findings.push({
169
+ kind: "unexplained",
170
+ file,
171
+ line: d.line,
172
+ token: d.token
173
+ });
174
+ }
175
+ const reported = /* @__PURE__ */ new Set();
176
+ for (const m of p.mandates) {
177
+ if (!hasSameField(known, m.token)) continue;
178
+ const key = `${m.line}:${m.token}`;
179
+ if (reported.has(key)) continue;
180
+ if (!hasSameField(declared, m.token)) {
181
+ reported.add(key);
182
+ findings.push({
183
+ kind: "undeclared",
184
+ file,
185
+ line: m.line,
186
+ token: m.token
187
+ });
188
+ continue;
189
+ }
190
+ const forms = sameFieldForms(m.token);
191
+ const same = p.declarations.filter((d) => forms.includes(fold(d.token)));
192
+ if (same.some((d) => isLower(d.token) === isLower(m.token))) continue;
193
+ reported.add(key);
194
+ findings.push({
195
+ kind: "miscased",
196
+ file,
197
+ line: m.line,
198
+ token: m.token,
199
+ declared: same[0]?.token
200
+ });
201
+ }
202
+ }
203
+ return findings.sort((a, b) => a.file === b.file ? a.line - b.line : a.file < b.file ? -1 : 1);
204
+ }
205
+ const DETAIL = {
206
+ unexplained: "declared in a block, but no gloss and no prose explains it",
207
+ undeclared: "mandated in the prose, but no block in this file declares it",
208
+ miscased: "mandated in the prose, but this file declares it in the other case"
209
+ };
210
+ function formatReport(definitions, findings) {
211
+ if (definitions.length === 0) return "check-field-mandates: no skill or agent definition found\n";
212
+ if (findings.length === 0) return `check-field-mandates: ${definitions.length} definition(s) OK\n`;
213
+ return `${findings.map((f) => ` ${f.kind.padEnd(11)} ${f.file}:${f.line} ${f.token} — ${DETAIL[f.kind]}${f.declared ? ` (${f.declared})` : ""}`).join("\n")}\ncheck-field-mandates: ${findings.length} finding(s) across ${definitions.length} definition(s)\n`;
214
+ }
215
+ function isReadableDirectory(path) {
216
+ try {
217
+ if (!statSync(path).isDirectory()) return false;
218
+ readdirSync(path);
219
+ return true;
220
+ } catch {
221
+ return false;
222
+ }
223
+ }
224
+ function main(argv) {
225
+ let root = ".";
226
+ for (let i = 0; i < argv.length; i++) {
227
+ const a = argv[i];
228
+ if (a === "--root") {
229
+ const next = argv[++i];
230
+ if (next === void 0) {
231
+ process.stderr.write("check-field-mandates: --root needs a directory\n");
232
+ return 1;
233
+ }
234
+ root = next;
235
+ continue;
236
+ }
237
+ process.stderr.write(`check-field-mandates: unrecognized flag ${a}\n`);
238
+ return 1;
239
+ }
240
+ if (!isReadableDirectory(root)) {
241
+ process.stderr.write(`check-field-mandates: root ${root} is not a readable directory\n`);
242
+ return 1;
243
+ }
244
+ const definitions = discoverDefinitions(root);
245
+ const parsed = /* @__PURE__ */ new Map();
246
+ for (const rel of definitions) parsed.set(rel, parseDefinition(readFileSync(join(root, rel), "utf8")));
247
+ const findings = check(parsed);
248
+ process.stdout.write(formatReport(definitions, findings));
249
+ return findings.length === 0 ? 0 : 1;
250
+ }
251
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
252
+ //#endregion
253
+ export { check, discoverDefinitions, formatReport, isDefinitionPath, main, parseDefinition };
@@ -1,12 +1,235 @@
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-partition-quality/check-partition-quality.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/check-partition-quality/scripts/check-partition-quality.mjs","commands":{}};
4
+ import { execFileSync } from "node:child_process";
5
+ import { realpathSync } from "node:fs";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/check-partition-quality/check-partition-quality.mts
8
+ /** Below this many usable multi-file commits, a rate would be noise — report, never score. */
9
+ const DEFAULT_FLOOR = 20;
10
+ function parseGitLog(stdout, inScope) {
11
+ const out = [];
12
+ let cur = null;
13
+ for (const line of stdout.split("\n")) {
14
+ const t = line.trimEnd();
15
+ if (t === "") {
16
+ if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] });
17
+ cur = null;
18
+ continue;
19
+ }
20
+ if (/^[0-9a-f]{40}$/.test(t)) {
21
+ if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] });
22
+ cur = [];
23
+ continue;
24
+ }
25
+ if (cur !== null && inScope(t)) cur.push(t);
26
+ }
27
+ if (cur && cur.length > 1) out.push({ files: [...new Set(cur)] });
28
+ return out.filter((c) => c.files.length > 1);
29
+ }
30
+ function readHistory(repo, inScope, limit = 4e3) {
31
+ return parseGitLog(execFileSync("git", [
32
+ "-C",
33
+ repo,
34
+ "log",
35
+ `-${limit}`,
36
+ "--name-only",
37
+ "--pretty=format:%H"
38
+ ], {
39
+ encoding: "utf8",
40
+ maxBuffer: 268435456
41
+ }), inScope);
42
+ }
43
+ /** The set of nodes one change touches. A change touching none drops out of the measure. */
44
+ function nodesOf(c, p) {
45
+ const s = /* @__PURE__ */ new Set();
46
+ for (const f of c.files) {
47
+ const n = p(f);
48
+ if (n !== void 0) s.add(n);
49
+ }
50
+ return s;
51
+ }
52
+ function collisionRate(changes, p) {
53
+ const sets = changes.map((c) => nodesOf(c, p)).filter((s) => s.size > 0);
54
+ let pairs = 0;
55
+ let collided = 0;
56
+ for (let i = 0; i < sets.length; i++) for (let j = i + 1; j < sets.length; j++) {
57
+ pairs++;
58
+ const a = sets[i];
59
+ const b = sets[j];
60
+ for (const n of a) if (b.has(n)) {
61
+ collided++;
62
+ break;
63
+ }
64
+ }
65
+ return {
66
+ rate: pairs === 0 ? 0 : collided / pairs,
67
+ pairs
68
+ };
69
+ }
70
+ function mulberry32(seed) {
71
+ let a = seed >>> 0;
72
+ return () => {
73
+ a = a + 1831565813 >>> 0;
74
+ let t = Math.imul(a ^ a >>> 15, 1 | a);
75
+ t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
76
+ return ((t ^ t >>> 14) >>> 0) / 4294967296;
77
+ };
78
+ }
79
+ function shuffledControl(changes, p, seed = 7) {
80
+ const files = [...new Set(changes.flatMap((c) => c.files))].filter((f) => p(f) !== void 0).sort();
81
+ const labels = files.map((f) => p(f));
82
+ const rnd = mulberry32(seed);
83
+ for (let i = labels.length - 1; i > 0; i--) {
84
+ const j = Math.floor(rnd() * (i + 1));
85
+ const li = labels[i];
86
+ const lj = labels[j];
87
+ labels[i] = lj;
88
+ labels[j] = li;
89
+ }
90
+ const m = new Map(files.map((f, i) => [f, labels[i]]));
91
+ return collisionRate(changes, (f) => m.get(f)).rate;
92
+ }
93
+ /**
94
+ * Rename `diagnostics` to `confoundedDiagnostics` on the way out to JSON.
95
+ *
96
+ * The text render carries the warning inline; a JSON consumer sees only keys, so a plain
97
+ * `diagnostics` object hands over two numbers indistinguishable from the headline. Renaming the key
98
+ * beats adding a note field: a consumer cannot read these numbers without typing the word
99
+ * "confounded", whereas a sibling note is trivially skipped.
100
+ */
101
+ function toJson([name, m]) {
102
+ if (!("diagnostics" in m)) return [name, m];
103
+ const { diagnostics, ...rest } = m;
104
+ return [name, {
105
+ ...rest,
106
+ confoundedDiagnostics: diagnostics
107
+ }];
108
+ }
109
+ function withinNodeCoChangeRatio(changes, p) {
110
+ let within = 0;
111
+ let total = 0;
112
+ for (const c of changes) {
113
+ const ns = c.files.map((f) => p(f)).filter((n) => n !== void 0);
114
+ for (let i = 0; i < ns.length; i++) for (let j = i + 1; j < ns.length; j++) {
115
+ total++;
116
+ if (ns[i] === ns[j]) within++;
117
+ }
118
+ }
119
+ return total === 0 ? 0 : within / total;
120
+ }
121
+ function meanNodesTouched(changes, p) {
122
+ const counts = changes.map((c) => nodesOf(c, p).size).filter((n) => n > 0);
123
+ return counts.length === 0 ? 0 : counts.reduce((a, b) => a + b, 0) / counts.length;
124
+ }
125
+ function measure(changes, p, floor = 20) {
126
+ const usable = changes.filter((c) => nodesOf(c, p).size > 0);
127
+ if (usable.length < floor) return {
128
+ thin: true,
129
+ usableCommits: usable.length,
130
+ floor
131
+ };
132
+ const { rate, pairs } = collisionRate(usable, p);
133
+ const control = shuffledControl(usable, p);
134
+ const margin = control - rate;
135
+ return {
136
+ nodes: new Set(usable.flatMap((c) => [...nodesOf(c, p)])).size,
137
+ pairs,
138
+ collisionRate: rate,
139
+ parallelizableShare: 1 - rate,
140
+ control,
141
+ marginOverControl: margin,
142
+ explainsNothing: margin <= 0,
143
+ diagnostics: {
144
+ withinNodeCoChangeRatio: withinNodeCoChangeRatio(usable, p),
145
+ meanNodesTouched: meanNodesTouched(usable, p)
146
+ }
147
+ };
148
+ }
149
+ function isThin(m) {
150
+ return m.thin === true;
151
+ }
152
+ const PARTITIONS = {
153
+ /** Top-level folder under the scope — the capability cut for a screaming layout. */
154
+ "top-folder": (scope) => (f) => {
155
+ const rel = f.startsWith(scope) ? f.slice(scope.length).replace(/^\//, "") : void 0;
156
+ if (rel === void 0) return void 0;
157
+ const seg = rel.split("/")[0];
158
+ return seg === void 0 || seg === "" || !rel.includes("/") ? void 0 : seg;
159
+ },
160
+ /** Second-level folder — a finer cut, for a capability/unit tree. */
161
+ "second-folder": (scope) => (f) => {
162
+ const rel = f.startsWith(scope) ? f.slice(scope.length).replace(/^\//, "") : void 0;
163
+ if (rel === void 0) return void 0;
164
+ const p = rel.split("/");
165
+ return p.length > 2 ? `${p[0]}/${p[1]}` : void 0;
166
+ },
167
+ /** File extension / artifact role — the layered analogue, useful as a contrast candidate. */
168
+ role: (scope) => (f) => {
169
+ if (!f.startsWith(scope)) return void 0;
170
+ const n = f.split("/").pop() ?? "";
171
+ const dot = n.indexOf(".");
172
+ return dot === -1 ? n : n.slice(dot + 1);
173
+ },
174
+ /** Everything in one node — the degenerate floor: no parallel work is possible. */
175
+ single: (scope) => (f) => f.startsWith(scope) ? "all" : void 0
176
+ };
177
+ function pct(n) {
178
+ return `${(n * 100).toFixed(1)}%`;
179
+ }
180
+ function renderOne(label, m) {
181
+ if (isThin(m)) return ` ${label}: history too thin to measure — ${m.usableCommits} usable multi-file commits, floor ${m.floor}. No rate emitted.`;
182
+ const lines = [
183
+ ` ${label}`,
184
+ ` parallelizable share : ${pct(m.parallelizableShare)} ← the headline`,
185
+ ` collision rate : ${pct(m.collisionRate)} over ${m.pairs} change pairs, ${m.nodes} nodes`,
186
+ ` shuffled control : ${pct(m.control)} (margin ${m.marginOverControl >= 0 ? "+" : ""}${pct(m.marginOverControl)})`
187
+ ];
188
+ if (m.collisionRate >= 1) lines.push(" ⚠ every change pair collides — this partition permits no parallel work");
189
+ if (m.explainsNothing) lines.push(" ⚠ this partition explains no more than chance — do not trust the headline");
190
+ lines.push(` diagnostics (CONFOUNDED by node count — not the headline): within-node co-change ${pct(m.diagnostics.withinNodeCoChangeRatio)}, mean nodes touched ${m.diagnostics.meanNodesTouched.toFixed(2)}`);
191
+ return lines.join("\n");
192
+ }
193
+ function render(results, repo, scope) {
194
+ const out = [`check-partition-quality: repo=${repo} scope=${scope || "(whole repo)"}`];
195
+ for (const [label, m] of results) out.push(renderOne(label, m));
196
+ if (results.length > 1) {
197
+ const scored = results.filter(([, m]) => !isThin(m));
198
+ if (scored.length > 1) {
199
+ const best = scored.reduce((a, b) => b[1].parallelizableShare > a[1].parallelizableShare ? b : a);
200
+ out.push(`\nOn this history, "${best[0]}" permits the most parallel work (${pct(best[1].parallelizableShare)}).`);
201
+ }
202
+ }
203
+ out.push("note: a measurement, not a verdict — it moves no file and gates nothing; layout is the owner’s call");
204
+ return out.join("\n");
205
+ }
206
+ function main(argv, context = { readHistory }) {
207
+ let repo = ".";
208
+ let scope = "";
209
+ let floor = 20;
210
+ let limit = 4e3;
211
+ let format = "text";
212
+ const candidates = [];
213
+ for (let i = 0; i < argv.length; i++) {
214
+ const a = argv[i];
215
+ if (a === "--repo") repo = argv[++i] ?? ".";
216
+ else if (a === "--scope") scope = argv[++i] ?? "";
217
+ else if (a === "--floor") floor = Number(argv[++i] ?? 20);
218
+ else if (a === "--limit") limit = Number(argv[++i] ?? 4e3);
219
+ else if (a === "--format") format = argv[++i] ?? "text";
220
+ else if (a === "--partition") candidates.push(argv[++i] ?? "");
221
+ }
222
+ if (candidates.length === 0) candidates.push("top-folder", "role");
223
+ for (const c of candidates) if (!(c in PARTITIONS)) {
224
+ process.stderr.write(`unknown --partition "${c}" — known: ${Object.keys(PARTITIONS).join(", ")}\n`);
225
+ return 1;
226
+ }
227
+ const changes = context.readHistory(repo, (f) => scope ? f.startsWith(scope) : true, limit);
228
+ const results = candidates.map((c) => [c, measure(changes, PARTITIONS[c](scope), floor)]);
229
+ if (format === "json") process.stdout.write(`${JSON.stringify(Object.fromEntries(results.map(toJson)), null, 2)}\n`);
230
+ else process.stdout.write(`${render(results, repo, scope)}\n`);
231
+ return 0;
232
+ }
233
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
234
+ //#endregion
235
+ export { DEFAULT_FLOOR, PARTITIONS, collisionRate, isThin, main, meanNodesTouched, measure, parseGitLog, readHistory, render, renderOne, shuffledControl, toJson, withinNodeCoChangeRatio };
@@ -12,8 +12,7 @@ The concrete engine for the **plan brief's safe-to-publish floor**. The plan bri
12
12
  (`.agents/plans/<cr-ref>.plan.md`) and its sibling design docs are **tracked, portable** handoff
13
13
  artifacts — committed at handoff and read by any later session, agent, or checkout. This engine
14
14
  guards that floor mechanically: it scans those `*.md` files for **machine-local references** that
15
- must never enter git history. It carries a self-contained `.mts` script (the repo's node-≥23.6 /
16
- no-deps convention), and is the plan-brief analog of the combat log's safe-to-publish floor
15
+ must never enter git history. It carries a self-contained `.mjs` script (compiled from `src/`; no runtime deps, Node ≥22.11), and is the plan-brief analog of the combat log's safe-to-publish floor
17
16
  (`combat-log-governance`) — the same "never committed: absolute paths, OS usernames" rule, extended
18
17
  from the ledger to the brief.
19
18
 
@@ -47,7 +46,7 @@ node "<skill>/scripts/check-plan-safety.mjs" [--root .] [--path <file>]... [--ch
47
46
  `pause-mission` to check the one brief it is about to commit.
48
47
  - **Audit mode** (default) emits the finding set (`file, line, kind, token`) and exits **0**.
49
48
  - **`--check`** (CI guard) exits **non-zero** iff any leak is found and **writes nothing** else.
50
- Wired into `verify:specs` (`node …/check-plan-safety.mts --root . --check`).
49
+ Wired into `verify:specs` (`node …/check-plan-safety.mjs --root . --check`).
51
50
 
52
51
  When `node` is absent, an agent performs the same derivation by hand: grep the plan `*.md` files for
53
52
  `/home/`, `/Users/`, `C:\Users\`, `$HOME`, and `$USER`.
@@ -1,12 +1,109 @@
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-plan-safety/check-plan-safety.mts. Edit the source, not this file.
3
+ globalThis.__SKILL_SCRIPT__ = {"package":null,"bin":null,"version":"0.7.1","entry":"skills/check-plan-safety/scripts/check-plan-safety.mjs","commands":{}};
4
+ import { existsSync, readFileSync, readdirSync, realpathSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ //#region src/check-plan-safety/check-plan-safety.mts
8
+ const LEAK_PATTERNS = [
9
+ {
10
+ kind: "home-abs-path",
11
+ re: /\/(?:home|Users)\/[^\s'"`)\]]+/g
12
+ },
13
+ {
14
+ kind: "home-abs-path",
15
+ re: /[A-Za-z]:\\Users\\[^\s'"`)\]]+/g
16
+ },
17
+ {
18
+ kind: "env-home",
19
+ re: /\$\{?HOME\}?(?![A-Za-z])|%USERPROFILE%|%HOMEPATH%/g
20
+ },
21
+ {
22
+ kind: "env-user",
23
+ re: /\$\{?USER\}?(?![A-Za-z])|%USERNAME%/g
24
+ }
25
+ ];
26
+ /** Cap a matched token so a pathological long path does not blow up the report. */
27
+ function capToken(t) {
28
+ return t.length > 80 ? `${t.slice(0, 77)}...` : t;
29
+ }
30
+ function scanText(file, text) {
31
+ const out = [];
32
+ const lines = text.split("\n");
33
+ for (let i = 0; i < lines.length; i++) {
34
+ const line = lines[i].replace(/\r$/, "");
35
+ for (const { kind, re } of LEAK_PATTERNS) {
36
+ re.lastIndex = 0;
37
+ let m = re.exec(line);
38
+ while (m !== null) {
39
+ out.push({
40
+ file,
41
+ line: i + 1,
42
+ kind,
43
+ token: capToken(m[0])
44
+ });
45
+ if (m.index === re.lastIndex) re.lastIndex++;
46
+ m = re.exec(line);
47
+ }
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+ function planFiles(root) {
53
+ const dir = join(root, ".agents", "plans");
54
+ if (!existsSync(dir)) return [];
55
+ let entries;
56
+ try {
57
+ entries = readdirSync(dir, { withFileTypes: true });
58
+ } catch {
59
+ return [];
60
+ }
61
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name).sort();
62
+ }
63
+ function collectLeaks(root, paths) {
64
+ const targets = paths ? paths.map((p) => ({
65
+ rel: p,
66
+ abs: p
67
+ })) : planFiles(root).map((name) => ({
68
+ rel: join(".agents", "plans", name),
69
+ abs: join(root, ".agents", "plans", name)
70
+ }));
71
+ const out = [];
72
+ for (const { rel, abs } of targets) {
73
+ let text;
74
+ try {
75
+ text = readFileSync(abs, "utf8");
76
+ } catch {
77
+ continue;
78
+ }
79
+ out.push(...scanText(rel, text));
80
+ }
81
+ return out.sort((a, b) => a.file !== b.file ? a.file < b.file ? -1 : 1 : a.line - b.line);
82
+ }
83
+ const COLUMNS = [
84
+ "file",
85
+ "line",
86
+ "kind",
87
+ "token"
88
+ ];
89
+ function toonField(v) {
90
+ if (v === "" || /[",]/.test(v) || v !== v.trim()) return `"${v.replace(/"/g, "\"\"")}"`;
91
+ return v;
92
+ }
93
+ function toToon(leaks) {
94
+ return [`leaks[${leaks.length}]{${COLUMNS.join(",")}}:`, ...leaks.map((l) => ` ${COLUMNS.map((c) => toonField(String(l[c]))).join(",")}`)].join("\n");
95
+ }
96
+ function main(argv) {
97
+ const root = argv.includes("--root") ? argv[argv.indexOf("--root") + 1] ?? "." : ".";
98
+ const format = argv.includes("--format") ? argv[argv.indexOf("--format") + 1] : "toon";
99
+ const check = argv.includes("--check");
100
+ const paths = [];
101
+ for (let i = 0; i < argv.length; i++) if (argv[i] === "--path" && argv[i + 1]) paths.push(argv[++i]);
102
+ const leaks = collectLeaks(root, paths.length ? paths : void 0);
103
+ const out = format === "json" ? JSON.stringify(leaks, null, 2) : toToon(leaks);
104
+ process.stdout.write(`${out}\n`);
105
+ return check && leaks.length > 0 ? 1 : 0;
106
+ }
107
+ if (process.argv[1] && import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) process.exit(main(process.argv.slice(2)));
108
+ //#endregion
109
+ export { LEAK_PATTERNS, collectLeaks, main, scanText, toToon };