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