cyber-sdd 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -2
- package/.codex-plugin/plugin.json +1 -2
- package/CHANGELOG.md +320 -0
- package/agents/sdd-scanner.md +1 -1
- package/package.json +11 -12
- package/plugin.json +55 -0
- package/skills/align-spec/SKILL.md +1 -1
- package/skills/align-spec/scripts/align-spec.mjs +6755 -12
- package/skills/blast-estimate/README.md +3 -3
- package/skills/blast-estimate/scripts/blast-estimate.mjs +601 -12
- package/skills/check-field-mandates/scripts/check-field-mandates.mjs +253 -12
- package/skills/check-partition-quality/scripts/check-partition-quality.mjs +235 -12
- package/skills/check-plan-safety/SKILL.md +2 -3
- package/skills/check-plan-safety/scripts/check-plan-safety.mjs +109 -12
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mjs +250 -12
- package/skills/check-project-specs/SKILL.md +1 -1
- package/skills/check-project-specs/scripts/check-project-specs.mjs +593 -12
- package/skills/check-retired-terms/SKILL.md +2 -3
- package/skills/check-retired-terms/scripts/check-retired-terms.mjs +186 -12
- package/skills/check-scenario-overlap/SKILL.md +1 -2
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mjs +177 -12
- package/skills/check-spec-references/SKILL.md +2 -2
- package/skills/check-spec-references/scripts/check-spec-references.mjs +287 -12
- package/skills/check-spec-structure/SKILL.md +1 -2
- package/skills/check-spec-structure/scripts/check-spec-structure.mjs +243 -12
- package/skills/collision-ladder/README.md +2 -2
- package/skills/collision-ladder/scripts/collision-ladder.mjs +7047 -12
- package/skills/combat-log-governance/SKILL.md +1 -1
- package/skills/concept-index/SKILL.md +1 -1
- package/skills/concept-index/scripts/concept-index.mjs +183 -12
- package/skills/discover-plans/README.md +3 -3
- package/skills/discover-plans/SKILL.md +1 -1
- package/skills/discover-plans/scripts/discover-plans.mjs +137 -12
- package/skills/discover-specs/README.md +3 -3
- package/skills/discover-specs/SKILL.md +1 -1
- package/skills/discover-specs/scripts/discover-specs.mjs +321 -12
- package/skills/doctrine-loop/SKILL.md +1 -1
- package/skills/gate-validation-governance/SKILL.md +1 -1
- package/skills/init/README.md +2 -2
- package/skills/init/scripts/wire-statusline.mjs +168 -12
- package/skills/manage-ignore/README.md +3 -3
- package/skills/manage-ignore/SKILL.md +1 -2
- package/skills/manage-ignore/scripts/manage-ignore.mjs +285 -12
- 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.mjs +239 -12
- package/skills/manage-spec-anchors/README.md +3 -3
- package/skills/manage-spec-anchors/SKILL.md +1 -1
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mjs +310 -12
- package/skills/mission-graph/README.md +2 -2
- package/skills/mission-graph/SKILL.md +1 -1
- package/skills/mission-graph/scripts/mission-graph.mjs +1040 -12
- package/skills/pause-mission/SKILL.md +1 -1
- package/skills/place-node/SKILL.md +2 -2
- package/skills/place-node/scripts/place-node.mjs +117 -12
- package/skills/plan-retirement/README.md +2 -2
- package/skills/plan-retirement/scripts/retire-plans.mjs +123 -12
- package/skills/resolve-governances/README.md +3 -3
- package/skills/resolve-governances/SKILL.md +1 -1
- package/skills/resolve-governances/scripts/resolve-governances.mjs +352 -12
- package/skills/resolve-tracking/SKILL.md +1 -1
- package/skills/resolve-tracking/scripts/resolve-tracking.mjs +152 -12
- package/skills/scaffold-project-spec/SKILL.md +1 -1
- package/skills/spec-gate/README.md +12 -9
- package/skills/spec-gate/SKILL.md +2 -2
- package/skills/spec-gate/scripts/check-spec-state.mjs +422 -12
- package/skills/spec-gate/scripts/check-suite.mjs +6500 -12
- package/skills/spec-gate/scripts/classify-edit-class.mjs +6579 -12
- package/skills/spec-producer-governance/SKILL.md +2 -2
- package/skills/start-mission/SKILL.md +1 -1
- package/skills/suite-format-governance/README.md +1 -1
- package/skills/suite-format-governance/SKILL.md +1 -1
- package/skills/touch-set-correction/README.md +5 -5
- package/skills/touch-set-correction/SKILL.md +1 -1
- package/skills/touch-set-correction/scripts/touch-set-correction.mjs +6663 -12
- package/skills/verify-scenarios/README.md +6 -6
- package/skills/verify-scenarios/scripts/verify-scenarios.mjs +6539 -12
- package/.plugin/pins.json +0 -3
- package/.plugin/plugin.json +0 -19
- package/dist/align-spec/scripts/align-spec.js +0 -102
- package/dist/check-project-specs/scripts/check-project-specs.js +0 -312
- package/dist/check-scenario-overlap/scripts/check-scenario-overlap.js +0 -175
- package/dist/check-spec-references/scripts/check-spec-references.js +0 -285
- package/dist/check-spec-structure/scripts/check-spec-structure.js +0 -241
- package/dist/concept-index/scripts/concept-index.js +0 -181
- package/dist/discover-specs/scripts/discover-specs.js +0 -318
- package/dist/spec-gate/scripts/check-spec-state.js +0 -420
- package/dist/spec-gate/scripts/check-suite.js +0 -317
- package/dist/spec-gate/scripts/classify-edit-class.js +0 -263
- package/dist/spec-gate/vendor/gherkin-cli.js +0 -6369
- package/skills/align-spec/scripts/align-spec.bundle.mjs +0 -6753
- package/skills/align-spec/scripts/align-spec.mts +0 -188
- package/skills/blast-estimate/scripts/blast-estimate.bundle.mjs +0 -593
- package/skills/blast-estimate/scripts/blast-estimate.mts +0 -586
- package/skills/check-field-mandates/scripts/check-field-mandates.bundle.mjs +0 -251
- package/skills/check-field-mandates/scripts/check-field-mandates.mts +0 -337
- package/skills/check-partition-quality/scripts/check-partition-quality.bundle.mjs +0 -233
- package/skills/check-partition-quality/scripts/check-partition-quality.mts +0 -338
- package/skills/check-plan-safety/scripts/check-plan-safety.bundle.mjs +0 -107
- package/skills/check-plan-safety/scripts/check-plan-safety.mts +0 -148
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.bundle.mjs +0 -248
- package/skills/check-plugin-manifests/scripts/check-plugin-manifests.mts +0 -303
- package/skills/check-project-specs/scripts/check-project-specs.bundle.mjs +0 -584
- package/skills/check-project-specs/scripts/check-project-specs.mts +0 -370
- package/skills/check-retired-terms/scripts/check-retired-terms.bundle.mjs +0 -184
- package/skills/check-retired-terms/scripts/check-retired-terms.mts +0 -293
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.bundle.mjs +0 -175
- package/skills/check-scenario-overlap/scripts/check-scenario-overlap.mts +0 -250
- package/skills/check-spec-references/scripts/check-spec-references.bundle.mjs +0 -285
- package/skills/check-spec-references/scripts/check-spec-references.mts +0 -422
- package/skills/check-spec-structure/scripts/check-spec-structure.bundle.mjs +0 -241
- package/skills/check-spec-structure/scripts/check-spec-structure.mts +0 -347
- package/skills/collision-ladder/scripts/collision-ladder.bundle.mjs +0 -7041
- package/skills/collision-ladder/scripts/collision-ladder.mts +0 -662
- package/skills/concept-index/scripts/concept-index.bundle.mjs +0 -181
- package/skills/concept-index/scripts/concept-index.mts +0 -246
- package/skills/discover-plans/scripts/discover-plans.bundle.mjs +0 -135
- package/skills/discover-plans/scripts/discover-plans.mts +0 -215
- package/skills/discover-specs/scripts/discover-specs.bundle.mjs +0 -319
- package/skills/discover-specs/scripts/discover-specs.mts +0 -420
- package/skills/init/scripts/wire-statusline.bundle.mjs +0 -166
- package/skills/init/scripts/wire-statusline.mts +0 -279
- package/skills/manage-ignore/scripts/manage-ignore.bundle.mjs +0 -244
- package/skills/manage-ignore/scripts/manage-ignore.mts +0 -297
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.bundle.mjs +0 -237
- package/skills/manage-scenario-bridge/scripts/manage-scenario-bridge.mts +0 -159
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.bundle.mjs +0 -308
- package/skills/manage-spec-anchors/scripts/manage-spec-anchors.mts +0 -331
- package/skills/mission-graph/scripts/mission-graph.bundle.mjs +0 -1038
- package/skills/mission-graph/scripts/mission-graph.mts +0 -1333
- package/skills/place-node/scripts/place-node.bundle.mjs +0 -115
- package/skills/place-node/scripts/place-node.mts +0 -158
- package/skills/plan-retirement/scripts/retire-plans.bundle.mjs +0 -121
- package/skills/plan-retirement/scripts/retire-plans.mts +0 -205
- package/skills/resolve-governances/scripts/resolve-governances.bundle.mjs +0 -350
- package/skills/resolve-governances/scripts/resolve-governances.mts +0 -518
- package/skills/resolve-tracking/scripts/resolve-tracking.bundle.mjs +0 -150
- package/skills/resolve-tracking/scripts/resolve-tracking.mts +0 -214
- package/skills/spec-gate/scripts/check-spec-state.bundle.mjs +0 -420
- package/skills/spec-gate/scripts/check-spec-state.mts +0 -637
- package/skills/spec-gate/scripts/check-suite.bundle.mjs +0 -6498
- package/skills/spec-gate/scripts/check-suite.mts +0 -538
- package/skills/spec-gate/scripts/classify-edit-class.bundle.mjs +0 -6577
- package/skills/spec-gate/scripts/classify-edit-class.mts +0 -414
- package/skills/spec-gate/vendor/gherkin-cli.d.mts +0 -2
- package/skills/spec-gate/vendor/gherkin-cli.mjs +0 -6545
- package/skills/touch-set-correction/scripts/touch-set-correction.bundle.mjs +0 -6655
- package/skills/touch-set-correction/scripts/touch-set-correction.mts +0 -429
- package/skills/verify-scenarios/scripts/verify-scenarios.bundle.mjs +0 -6537
- package/skills/verify-scenarios/scripts/verify-scenarios.mts +0 -470
|
@@ -1,12 +1,253 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
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.2","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
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.2","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 `.
|
|
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.
|
|
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
|
-
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.2","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 };
|