@llblab/pi-kit 0.3.2 → 0.4.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/AGENTS.md +3 -3
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +19 -10
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +5 -0
- package/node_modules/@llblab/pi-clean-room/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-clean-room/README.md +61 -0
- package/node_modules/@llblab/pi-clean-room/banner.jpg +0 -0
- package/node_modules/@llblab/pi-clean-room/index.ts +178 -0
- package/node_modules/@llblab/pi-clean-room/package.json +53 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +3 -17
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +28 -0
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +79 -0
- package/node_modules/@llblab/pi-state-flow/README.md +197 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +238 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +78 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +58 -0
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +103 -0
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +20 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +148 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/validation.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/package.json +55 -0
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +2 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +17 -5
- package/node_modules/@llblab/pi-telegram/index.ts +9 -0
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +29 -18
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -6
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +89 -32
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +74 -15
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +32 -1
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +123 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +4 -2
- package/node_modules/@llblab/skills/abcd-context/AGENTS.md +40 -0
- package/node_modules/@llblab/skills/abcd-context/BACKLOG.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +19 -0
- package/node_modules/@llblab/skills/abcd-context/README.md +30 -0
- package/node_modules/@llblab/skills/abcd-context/SKILL.md +122 -0
- package/node_modules/@llblab/skills/abcd-context/docs/README.md +9 -0
- package/node_modules/@llblab/skills/abcd-context/docs/protocols.md +199 -0
- package/node_modules/@llblab/skills/abcd-context/docs/templates.md +194 -0
- package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +117 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/AGENTS.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/BACKLOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/CHANGELOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/README.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/README.md +7 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/overview.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/recipes/validate-context.json +7 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +244 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +497 -0
- package/node_modules/@llblab/skills/brain-storm/AGENTS.md +5 -0
- package/node_modules/@llblab/skills/brain-storm/SKILL.md +321 -0
- package/node_modules/@llblab/skills/coding-contract/SKILL.md +228 -0
- package/node_modules/@llblab/skills/domain-dag/AGENTS.md +6 -0
- package/node_modules/@llblab/skills/domain-dag/SKILL.md +257 -0
- package/node_modules/@llblab/skills/domain-dag/domain-dag.json +9 -0
- package/node_modules/@llblab/skills/domain-dag/recipes/validate-domain-dag.json +7 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.mjs +771 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.sh +4 -0
- package/node_modules/@llblab/skills/edge-tts/AGENTS.md +19 -0
- package/node_modules/@llblab/skills/edge-tts/SKILL.md +56 -0
- package/node_modules/@llblab/skills/edge-tts/recipes/say-edge.json +9 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.mjs +1548 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.sh +4 -0
- package/node_modules/@llblab/skills/extra-self/SKILL.md +229 -0
- package/node_modules/@llblab/skills/frontend-design/SKILL.md +78 -0
- package/node_modules/@llblab/skills/groq-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/groq-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/groq-stt/recipes/transcribe-groq.json +10 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.mjs +190 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/mistral-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/mistral-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/mistral-stt/recipes/transcribe-mistral.json +10 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.mjs +191 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/package.json +46 -0
- package/node_modules/@llblab/skills/re-review/SKILL.md +318 -0
- package/node_modules/@llblab/skills/release-flow/SKILL.md +456 -0
- package/node_modules/@llblab/skills/show-me/SKILL.md +144 -0
- package/package.json +20 -8
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +0 -143
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +0 -143
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
let explicitRoot;
|
|
8
|
+
const args = process.argv.slice(2);
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
if (arg === "--help" || arg === "-h") {
|
|
12
|
+
console.log(
|
|
13
|
+
[
|
|
14
|
+
"Usage: validate-context.mjs [project-root]",
|
|
15
|
+
"",
|
|
16
|
+
"Validates the current directory by default, VALIDATE_CONTEXT_ROOT when set,",
|
|
17
|
+
"or the explicit project-root argument when provided.",
|
|
18
|
+
"",
|
|
19
|
+
"Markdown table formatting is always checked. Rows over 120 characters warn.",
|
|
20
|
+
].join("\n"),
|
|
21
|
+
);
|
|
22
|
+
process.exit(0);
|
|
23
|
+
}
|
|
24
|
+
if (arg.startsWith("--")) {
|
|
25
|
+
console.error(`ERROR: Unknown option: ${arg}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
if (explicitRoot) {
|
|
29
|
+
console.error("ERROR: Multiple project roots provided");
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
explicitRoot = arg;
|
|
33
|
+
}
|
|
34
|
+
const root = path.resolve(
|
|
35
|
+
explicitRoot || process.env.VALIDATE_CONTEXT_ROOT || process.cwd(),
|
|
36
|
+
);
|
|
37
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
38
|
+
console.error(
|
|
39
|
+
`ERROR: Project root does not exist or is not a directory: ${root}`,
|
|
40
|
+
);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
const noColor = process.env.NO_COLOR;
|
|
44
|
+
const tableWidthWarnThreshold = 120;
|
|
45
|
+
const markdownLinkScanMaxBytes = Number(
|
|
46
|
+
process.env.ABCD_MARKDOWN_LINK_SCAN_MAX_BYTES || 262144,
|
|
47
|
+
);
|
|
48
|
+
if (
|
|
49
|
+
!Number.isInteger(markdownLinkScanMaxBytes) ||
|
|
50
|
+
markdownLinkScanMaxBytes <= 0
|
|
51
|
+
) {
|
|
52
|
+
console.error(
|
|
53
|
+
"ERROR: markdown link scan max bytes must be a positive integer",
|
|
54
|
+
);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const indexCandidates = [
|
|
59
|
+
"AGENTS.md",
|
|
60
|
+
"CLAUDE.md",
|
|
61
|
+
"CODEX.md",
|
|
62
|
+
"GEMINI.md",
|
|
63
|
+
"CONTEXT.md",
|
|
64
|
+
];
|
|
65
|
+
const planCandidates = ["BACKLOG.md", "TODO.md", "PLAN.md", "ROADMAP.md"];
|
|
66
|
+
const skipDirs = new Set([
|
|
67
|
+
"node_modules",
|
|
68
|
+
"target",
|
|
69
|
+
"build",
|
|
70
|
+
"dist",
|
|
71
|
+
"vendor",
|
|
72
|
+
"obj",
|
|
73
|
+
"out",
|
|
74
|
+
"bin",
|
|
75
|
+
"bower_components",
|
|
76
|
+
"site-packages",
|
|
77
|
+
"coverage",
|
|
78
|
+
"Pods",
|
|
79
|
+
".git",
|
|
80
|
+
".cache",
|
|
81
|
+
".github",
|
|
82
|
+
".idea",
|
|
83
|
+
".next",
|
|
84
|
+
".pytest_cache",
|
|
85
|
+
".tox",
|
|
86
|
+
".venv",
|
|
87
|
+
".vscode",
|
|
88
|
+
"venv",
|
|
89
|
+
"temp",
|
|
90
|
+
"tmp",
|
|
91
|
+
"_build",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
let errors = 0;
|
|
95
|
+
let warnings = 0;
|
|
96
|
+
const colors = noColor
|
|
97
|
+
? { g: "", y: "", r: "", c: "", n: "" }
|
|
98
|
+
: {
|
|
99
|
+
g: "\x1b[0;32m",
|
|
100
|
+
y: "\x1b[1;33m",
|
|
101
|
+
r: "\x1b[0;31m",
|
|
102
|
+
c: "\x1b[0;36m",
|
|
103
|
+
n: "\x1b[0m",
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function add(level, msg) {
|
|
107
|
+
if (level === "warn") warnings++;
|
|
108
|
+
if (level === "fail") errors++;
|
|
109
|
+
const tag =
|
|
110
|
+
level === "pass"
|
|
111
|
+
? `${colors.g}[PASS]${colors.n}`
|
|
112
|
+
: level === "warn"
|
|
113
|
+
? `${colors.y}[WARN]${colors.n}`
|
|
114
|
+
: level === "fail"
|
|
115
|
+
? `${colors.r}[FAIL]${colors.n}`
|
|
116
|
+
: `${colors.c}[INFO]${colors.n}`;
|
|
117
|
+
console.log(`${tag} ${msg}`);
|
|
118
|
+
}
|
|
119
|
+
const pass = (msg) => add("pass", msg);
|
|
120
|
+
const warn = (msg) => add("warn", msg);
|
|
121
|
+
const fail = (msg) => add("fail", msg);
|
|
122
|
+
const info = (msg) => add("info", msg);
|
|
123
|
+
const progress = (msg) => console.log(msg);
|
|
124
|
+
|
|
125
|
+
function exists(rel) {
|
|
126
|
+
return fs.existsSync(path.join(root, rel));
|
|
127
|
+
}
|
|
128
|
+
function read(file) {
|
|
129
|
+
return fs.readFileSync(file, "utf8");
|
|
130
|
+
}
|
|
131
|
+
function rel(file) {
|
|
132
|
+
return path.relative(root, file).replaceAll(path.sep, "/");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function walk(dir, out = []) {
|
|
136
|
+
if (!fs.existsSync(dir)) return out;
|
|
137
|
+
for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
138
|
+
const p = path.join(dir, ent.name);
|
|
139
|
+
if (ent.isDirectory()) {
|
|
140
|
+
if (!skipDirs.has(ent.name)) walk(p, out);
|
|
141
|
+
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
142
|
+
out.push(p);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function markdownLines(text) {
|
|
149
|
+
let fence = null;
|
|
150
|
+
return text.split(/\r?\n/).map((line) => {
|
|
151
|
+
if (fence) {
|
|
152
|
+
const closing = line.match(/^ {0,3}(`{3,}|~{3,})\s*$/);
|
|
153
|
+
if (
|
|
154
|
+
closing &&
|
|
155
|
+
closing[1][0] === fence.marker &&
|
|
156
|
+
closing[1].length >= fence.length
|
|
157
|
+
)
|
|
158
|
+
fence = null;
|
|
159
|
+
return { line, outsideFence: false };
|
|
160
|
+
}
|
|
161
|
+
const opening = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
162
|
+
if (opening && (opening[1][0] === "~" || !opening[2].includes("`"))) {
|
|
163
|
+
fence = { marker: opening[1][0], length: opening[1].length };
|
|
164
|
+
return { line, outsideFence: false };
|
|
165
|
+
}
|
|
166
|
+
return { line, outsideFence: true };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stripFences(text) {
|
|
171
|
+
return markdownLines(text)
|
|
172
|
+
.filter(({ outsideFence }) => outsideFence)
|
|
173
|
+
.map(({ line }) => line)
|
|
174
|
+
.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function headingAnchor(text) {
|
|
178
|
+
return text
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^\p{L}\p{N} _-]/gu, "")
|
|
181
|
+
.trim()
|
|
182
|
+
.replace(/\s+/g, "-")
|
|
183
|
+
.replace(/-+/g, "-");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
progress("--- ABCd CONTEXT PROTOCOL VALIDATOR ---\n");
|
|
187
|
+
|
|
188
|
+
let contextName = "";
|
|
189
|
+
let contextFile = "";
|
|
190
|
+
for (const c of indexCandidates)
|
|
191
|
+
if (exists(c)) {
|
|
192
|
+
contextName = c;
|
|
193
|
+
contextFile = path.join(root, c);
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
contextFile
|
|
197
|
+
? pass(`Index file detected: ${contextName}`)
|
|
198
|
+
: fail(`No index file found (checked: ${indexCandidates.join(" ")})`);
|
|
199
|
+
|
|
200
|
+
let planName = "";
|
|
201
|
+
let planFile = "";
|
|
202
|
+
for (const c of planCandidates)
|
|
203
|
+
if (exists(c)) {
|
|
204
|
+
planName = c;
|
|
205
|
+
planFile = path.join(root, c);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
const changelogFile = path.join(root, "CHANGELOG.md");
|
|
209
|
+
|
|
210
|
+
const readme = path.join(root, "README.md");
|
|
211
|
+
if (fs.existsSync(readme)) {
|
|
212
|
+
const txt = read(readme);
|
|
213
|
+
txt.includes("docs/README.md")
|
|
214
|
+
? pass("README links to docs/README.md")
|
|
215
|
+
: warn("README missing link to docs/README.md");
|
|
216
|
+
contextName && txt.includes(contextName)
|
|
217
|
+
? pass(`README references index file (${contextName})`)
|
|
218
|
+
: warn(`README missing reference to index file (${contextName})`);
|
|
219
|
+
if (planName)
|
|
220
|
+
txt.includes(planName)
|
|
221
|
+
? pass(`README references open-work file (${planName})`)
|
|
222
|
+
: warn(`README missing reference to open-work file (${planName})`);
|
|
223
|
+
if (fs.existsSync(changelogFile))
|
|
224
|
+
txt.includes("CHANGELOG.md")
|
|
225
|
+
? pass("README references CHANGELOG.md")
|
|
226
|
+
: warn("README missing reference to CHANGELOG.md");
|
|
227
|
+
} else warn("README.md missing");
|
|
228
|
+
|
|
229
|
+
if (contextFile) {
|
|
230
|
+
const txt = read(contextFile);
|
|
231
|
+
if (
|
|
232
|
+
/## 1\./.test(txt) ||
|
|
233
|
+
(txt.includes("Meta-Protocol Principles") &&
|
|
234
|
+
/Discovered Constraints|Operating Principles/.test(txt))
|
|
235
|
+
)
|
|
236
|
+
pass("Core structure sections present");
|
|
237
|
+
else warn("Core structure sections missing");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (planName)
|
|
241
|
+
planName === "BACKLOG.md"
|
|
242
|
+
? pass("Canonical open-work file detected: BACKLOG.md")
|
|
243
|
+
: warn(
|
|
244
|
+
`Canonical open-work file uses fallback alias: ${planName} (BACKLOG.md preferred)`,
|
|
245
|
+
);
|
|
246
|
+
else
|
|
247
|
+
warn(
|
|
248
|
+
`No canonical open-work file found (checked: ${planCandidates.join(" ")})`,
|
|
249
|
+
);
|
|
250
|
+
fs.existsSync(changelogFile)
|
|
251
|
+
? pass("CHANGELOG.md detected")
|
|
252
|
+
: warn("CHANGELOG.md missing");
|
|
253
|
+
|
|
254
|
+
if (
|
|
255
|
+
contextFile &&
|
|
256
|
+
fs.existsSync(changelogFile) &&
|
|
257
|
+
read(contextFile).includes("Change History")
|
|
258
|
+
)
|
|
259
|
+
warn(
|
|
260
|
+
"Index file contains Change History while CHANGELOG.md exists — consider separating durable rules from delivery history",
|
|
261
|
+
);
|
|
262
|
+
else pass("Root state split looks coherent");
|
|
263
|
+
|
|
264
|
+
if (planFile && fs.existsSync(changelogFile)) {
|
|
265
|
+
const backlog = read(planFile);
|
|
266
|
+
const changelog = read(changelogFile).toLowerCase();
|
|
267
|
+
let drift = 0;
|
|
268
|
+
for (const m of backlog.matchAll(/^- \[ \] `([^`]+)`/gm)) {
|
|
269
|
+
const label = m[1];
|
|
270
|
+
const search = label.replace(/:$/, "").toLowerCase();
|
|
271
|
+
if (search && changelog.includes(search)) {
|
|
272
|
+
warn(
|
|
273
|
+
`Possible root-state drift: open backlog slice also appears in CHANGELOG.md: ${label}`,
|
|
274
|
+
);
|
|
275
|
+
drift++;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!drift) pass("No obvious BACKLOG/CHANGELOG drift detected");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
progress("Checking documentation links...");
|
|
282
|
+
const mdFiles = walk(root);
|
|
283
|
+
let broken = false;
|
|
284
|
+
const targets = new Set();
|
|
285
|
+
for (const file of mdFiles) {
|
|
286
|
+
const sourceSize = fs.statSync(file).size;
|
|
287
|
+
if (sourceSize > markdownLinkScanMaxBytes) {
|
|
288
|
+
info(
|
|
289
|
+
`Skipped link validation for large Markdown file: ${rel(file)} (${sourceSize} bytes > ${markdownLinkScanMaxBytes})`,
|
|
290
|
+
);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const base = path.dirname(file);
|
|
294
|
+
const text = stripFences(read(file));
|
|
295
|
+
for (const m of text.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)) {
|
|
296
|
+
const raw = m[1];
|
|
297
|
+
if (/^(https?:|mailto:)/.test(raw)) continue;
|
|
298
|
+
let linkFile = raw;
|
|
299
|
+
let anchor = "";
|
|
300
|
+
if (raw.startsWith("#")) {
|
|
301
|
+
linkFile = "";
|
|
302
|
+
anchor = raw.slice(1);
|
|
303
|
+
} else if (raw.includes("#")) [linkFile, anchor] = raw.split("#", 2);
|
|
304
|
+
const target = linkFile ? path.resolve(base, linkFile) : file;
|
|
305
|
+
if (!fs.existsSync(target)) {
|
|
306
|
+
fail(`Broken link: ${raw} in ${rel(file)}`);
|
|
307
|
+
broken = true;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (target !== file) targets.add(path.resolve(target));
|
|
311
|
+
if (anchor && fs.statSync(target).isFile()) {
|
|
312
|
+
const lineRef = anchor.match(/^L([1-9]\d*)(?:-L([1-9]\d*))?$/);
|
|
313
|
+
if (lineRef) {
|
|
314
|
+
const startLine = Number(lineRef[1]);
|
|
315
|
+
const endLine = Number(lineRef[2] || lineRef[1]);
|
|
316
|
+
const targetText = read(target);
|
|
317
|
+
const totalLines = targetText
|
|
318
|
+
? targetText.split(/\r?\n/).length - (targetText.endsWith("\n") ? 1 : 0)
|
|
319
|
+
: 0;
|
|
320
|
+
if (startLine > totalLines || endLine > totalLines || endLine < startLine) {
|
|
321
|
+
fail(
|
|
322
|
+
`Line ref out of range: #${anchor} (file has ${totalLines} lines) in ${rel(target)} (from ${rel(file)})`,
|
|
323
|
+
);
|
|
324
|
+
broken = true;
|
|
325
|
+
}
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const anchors = read(target)
|
|
329
|
+
.split(/\r?\n/)
|
|
330
|
+
.filter((l) => /^#{1,6} /.test(l))
|
|
331
|
+
.map((l) => headingAnchor(l.replace(/^#{1,6} /, "")));
|
|
332
|
+
if (!anchors.includes(anchor)) {
|
|
333
|
+
fail(`Broken anchor: #${anchor} in ${rel(file)}`);
|
|
334
|
+
broken = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (!broken) pass("Documentation links validated");
|
|
340
|
+
|
|
341
|
+
let readmeOrphans = 0;
|
|
342
|
+
for (const file of mdFiles.filter(
|
|
343
|
+
(f) =>
|
|
344
|
+
path.basename(f) === "README.md" &&
|
|
345
|
+
path.resolve(f) !== path.resolve(readme),
|
|
346
|
+
)) {
|
|
347
|
+
if (!targets.has(path.resolve(file))) {
|
|
348
|
+
warn(
|
|
349
|
+
`Unreachable README entrypoint: ${rel(file)} (no inbound markdown links)`,
|
|
350
|
+
);
|
|
351
|
+
readmeOrphans++;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (!readmeOrphans) pass("README entrypoints are reachable");
|
|
355
|
+
|
|
356
|
+
if (contextFile)
|
|
357
|
+
read(contextFile).includes("Meta-Protocol Principles")
|
|
358
|
+
? pass("Meta-Protocol Principles found")
|
|
359
|
+
: warn("Meta-Protocol Principles missing");
|
|
360
|
+
|
|
361
|
+
if (contextFile) {
|
|
362
|
+
progress("Analyzing index file health...");
|
|
363
|
+
const lines = read(contextFile).split(/\r?\n/);
|
|
364
|
+
const total = lines.length;
|
|
365
|
+
const headings = lines.filter((l) => /^#{1,6} /.test(l)).length;
|
|
366
|
+
const listItems = lines.filter((l) => /^\s*[-*] /.test(l)).length;
|
|
367
|
+
const blanks = lines.filter((l) => /^\s*$/.test(l)).length;
|
|
368
|
+
const content = Math.max(1, total - blanks);
|
|
369
|
+
const density = Math.floor(((headings + listItems) * 100) / content);
|
|
370
|
+
info(
|
|
371
|
+
`Index metrics: ${total} lines, ${headings} headings, density ${density}%`,
|
|
372
|
+
);
|
|
373
|
+
let signals = 0;
|
|
374
|
+
if (density < 40) {
|
|
375
|
+
warn(
|
|
376
|
+
`Low information density (${density}%): index may contain verbose prose`,
|
|
377
|
+
);
|
|
378
|
+
signals++;
|
|
379
|
+
}
|
|
380
|
+
if (headings && Math.floor(total / headings) > 15) {
|
|
381
|
+
warn(
|
|
382
|
+
`Sparse structure (${Math.floor(total / headings)} lines/heading): consider splitting or consolidating`,
|
|
383
|
+
);
|
|
384
|
+
signals++;
|
|
385
|
+
}
|
|
386
|
+
signals === 0
|
|
387
|
+
? pass("Index file health: no bloat signals")
|
|
388
|
+
: warn(
|
|
389
|
+
`Index file health: ${signals} bloat signal(s) — consolidation recommended`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const docsDir = path.join(root, "docs");
|
|
394
|
+
|
|
395
|
+
progress("Checking Markdown tables...");
|
|
396
|
+
let tableIssue = false;
|
|
397
|
+
for (const file of mdFiles) {
|
|
398
|
+
const lines = markdownLines(read(file));
|
|
399
|
+
let table = null;
|
|
400
|
+
const flushTable = () => {
|
|
401
|
+
if (!table) return;
|
|
402
|
+
if (table.maxLen > tableWidthWarnThreshold) {
|
|
403
|
+
warn(
|
|
404
|
+
`Wide Markdown table: ${rel(file)}:${table.start}-${table.end} (max ${table.maxLen} chars, ${table.wideRows}/${table.rows} rows > ${tableWidthWarnThreshold})`,
|
|
405
|
+
);
|
|
406
|
+
tableIssue = true;
|
|
407
|
+
}
|
|
408
|
+
table = null;
|
|
409
|
+
};
|
|
410
|
+
for (let i = 0; i < lines.length; i++) {
|
|
411
|
+
const { line, outsideFence } = lines[i];
|
|
412
|
+
if (!outsideFence || !/^ *\|.*\| *$/.test(line)) {
|
|
413
|
+
flushTable();
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (!table)
|
|
417
|
+
table = {
|
|
418
|
+
start: i + 1,
|
|
419
|
+
end: i + 1,
|
|
420
|
+
rows: 0,
|
|
421
|
+
maxLen: 0,
|
|
422
|
+
wideRows: 0,
|
|
423
|
+
};
|
|
424
|
+
table.end = i + 1;
|
|
425
|
+
table.rows += 1;
|
|
426
|
+
table.maxLen = Math.max(table.maxLen, line.length);
|
|
427
|
+
if (line.length > tableWidthWarnThreshold) table.wideRows += 1;
|
|
428
|
+
|
|
429
|
+
const cells = line
|
|
430
|
+
.slice(line.indexOf("|") + 1, line.lastIndexOf("|"))
|
|
431
|
+
.split("|");
|
|
432
|
+
if (
|
|
433
|
+
cells.length &&
|
|
434
|
+
cells.every((cell) => /^\s*:?-{3,}:?\s*$/.test(cell))
|
|
435
|
+
) {
|
|
436
|
+
const compact = cells.every((cell) =>
|
|
437
|
+
/^ (?:---|---:|:---|:---:) $/.test(cell),
|
|
438
|
+
);
|
|
439
|
+
if (!compact) {
|
|
440
|
+
fail(
|
|
441
|
+
`Non-compact Markdown table delimiter: ${rel(file)}:${i + 1} (use exactly three hyphens and one space inside each pipe)`,
|
|
442
|
+
);
|
|
443
|
+
tableIssue = true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
flushTable();
|
|
448
|
+
}
|
|
449
|
+
if (!tableIssue) pass("Markdown table checks passed");
|
|
450
|
+
|
|
451
|
+
if (contextFile) {
|
|
452
|
+
const age = Math.floor(
|
|
453
|
+
(Date.now() - fs.statSync(contextFile).mtimeMs) / 86400000,
|
|
454
|
+
);
|
|
455
|
+
age < 30
|
|
456
|
+
? pass(`Context is fresh (${age} days old)`)
|
|
457
|
+
: warn(`Context is stale (${age} days old)`);
|
|
458
|
+
}
|
|
459
|
+
fs.existsSync(docsDir)
|
|
460
|
+
? pass("Documentation directory exists")
|
|
461
|
+
: warn("Documentation directory missing");
|
|
462
|
+
|
|
463
|
+
const docsIndex = path.join(docsDir, "README.md");
|
|
464
|
+
if (fs.existsSync(docsDir) && fs.existsSync(docsIndex)) {
|
|
465
|
+
const idx = read(docsIndex);
|
|
466
|
+
let orphan = 0,
|
|
467
|
+
phantom = 0;
|
|
468
|
+
for (const file of walk(docsDir).filter(
|
|
469
|
+
(f) => path.basename(f) !== "README.md",
|
|
470
|
+
)) {
|
|
471
|
+
const r = path.relative(docsDir, file).replaceAll(path.sep, "/");
|
|
472
|
+
if (!idx.includes(r)) {
|
|
473
|
+
warn(`Orphaned doc: docs/${r} (not in docs/README.md)`);
|
|
474
|
+
orphan++;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
for (const m of idx.matchAll(/\]\(\.\/([^)]+\.md)\)/g)) {
|
|
478
|
+
if (!fs.existsSync(path.join(docsDir, m[1]))) {
|
|
479
|
+
warn(`Phantom link in docs/README.md: ./${m[1]}`);
|
|
480
|
+
phantom++;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
if (!orphan && !phantom) pass("Docs index coverage: 0 orphans, 0 phantoms");
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
console.log("\n--- VALIDATION SUMMARY ---\n");
|
|
487
|
+
console.log(`Warnings: ${warnings}`);
|
|
488
|
+
console.log(`Errors: ${errors}\n`);
|
|
489
|
+
if (!errors)
|
|
490
|
+
console.log(
|
|
491
|
+
`${colors.g}✓ Context validation PASSED${colors.n}`,
|
|
492
|
+
);
|
|
493
|
+
else
|
|
494
|
+
console.log(
|
|
495
|
+
`${colors.r}✗ Context validation FAILED${colors.n}\nManual intervention required`,
|
|
496
|
+
);
|
|
497
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Brain Storm Maintenance
|
|
2
|
+
|
|
3
|
+
- [SKILL.md](./SKILL.md) owns the design method, operating principles, and handoff boundaries; do not restate them here.
|
|
4
|
+
- Keep reusable instructions project-neutral and self-contained. Do not hard-code sibling skill names, concrete project names, private repository names, or local stack mirrors.
|
|
5
|
+
- Express adaptation as generic lenses rather than dependencies on a particular workflow or context system.
|