@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.
Files changed (115) hide show
  1. package/AGENTS.md +3 -3
  2. package/BACKLOG.md +1 -1
  3. package/CHANGELOG.md +6 -0
  4. package/README.md +19 -10
  5. package/node_modules/@llblab/pi-actors/AGENTS.md +1 -0
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
  7. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +16 -6
  8. package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +3 -3
  9. package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +6 -4
  10. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +85 -18
  11. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +2 -6
  12. package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +2 -31
  13. package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
  14. package/node_modules/@llblab/pi-actors/package.json +1 -1
  15. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +16 -6
  16. package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +3 -3
  17. package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +6 -4
  18. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +85 -18
  19. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +2 -6
  20. package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +2 -31
  21. package/node_modules/@llblab/pi-clean-room/AGENTS.md +5 -0
  22. package/node_modules/@llblab/pi-clean-room/BACKLOG.md +3 -0
  23. package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +15 -0
  24. package/node_modules/@llblab/pi-clean-room/README.md +61 -0
  25. package/node_modules/@llblab/pi-clean-room/banner.jpg +0 -0
  26. package/node_modules/@llblab/pi-clean-room/index.ts +178 -0
  27. package/node_modules/@llblab/pi-clean-room/package.json +53 -0
  28. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
  29. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
  30. package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
  31. package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +3 -17
  32. package/node_modules/@llblab/pi-state-flow/AGENTS.md +28 -0
  33. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -0
  34. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +79 -0
  35. package/node_modules/@llblab/pi-state-flow/README.md +197 -0
  36. package/node_modules/@llblab/pi-state-flow/index.ts +4 -0
  37. package/node_modules/@llblab/pi-state-flow/lib/context.ts +88 -0
  38. package/node_modules/@llblab/pi-state-flow/lib/episode.ts +29 -0
  39. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +238 -0
  40. package/node_modules/@llblab/pi-state-flow/lib/json.ts +78 -0
  41. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +38 -0
  42. package/node_modules/@llblab/pi-state-flow/lib/session.ts +49 -0
  43. package/node_modules/@llblab/pi-state-flow/lib/skills.ts +58 -0
  44. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +103 -0
  45. package/node_modules/@llblab/pi-state-flow/lib/state.ts +20 -0
  46. package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -0
  47. package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +148 -0
  48. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +47 -0
  49. package/node_modules/@llblab/pi-state-flow/lib/validation.ts +27 -0
  50. package/node_modules/@llblab/pi-state-flow/package.json +55 -0
  51. package/node_modules/@llblab/pi-telegram/BACKLOG.md +2 -0
  52. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +12 -0
  53. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +17 -5
  54. package/node_modules/@llblab/pi-telegram/index.ts +9 -0
  55. package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +29 -18
  56. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -6
  57. package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -4
  58. package/node_modules/@llblab/pi-telegram/lib/routing.ts +89 -32
  59. package/node_modules/@llblab/pi-telegram/lib/sync.ts +74 -15
  60. package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +32 -1
  61. package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +17 -0
  62. package/node_modules/@llblab/pi-telegram/lib/threads.ts +123 -15
  63. package/node_modules/@llblab/pi-telegram/package.json +1 -1
  64. package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +4 -2
  65. package/node_modules/@llblab/skills/abcd-context/AGENTS.md +40 -0
  66. package/node_modules/@llblab/skills/abcd-context/BACKLOG.md +3 -0
  67. package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +19 -0
  68. package/node_modules/@llblab/skills/abcd-context/README.md +30 -0
  69. package/node_modules/@llblab/skills/abcd-context/SKILL.md +122 -0
  70. package/node_modules/@llblab/skills/abcd-context/docs/README.md +9 -0
  71. package/node_modules/@llblab/skills/abcd-context/docs/protocols.md +199 -0
  72. package/node_modules/@llblab/skills/abcd-context/docs/templates.md +194 -0
  73. package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +117 -0
  74. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/AGENTS.md +10 -0
  75. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/BACKLOG.md +5 -0
  76. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/CHANGELOG.md +5 -0
  77. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/README.md +10 -0
  78. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/README.md +7 -0
  79. package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/overview.md +3 -0
  80. package/node_modules/@llblab/skills/abcd-context/recipes/validate-context.json +7 -0
  81. package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +244 -0
  82. package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +497 -0
  83. package/node_modules/@llblab/skills/brain-storm/AGENTS.md +5 -0
  84. package/node_modules/@llblab/skills/brain-storm/SKILL.md +321 -0
  85. package/node_modules/@llblab/skills/coding-contract/SKILL.md +228 -0
  86. package/node_modules/@llblab/skills/domain-dag/AGENTS.md +6 -0
  87. package/node_modules/@llblab/skills/domain-dag/SKILL.md +257 -0
  88. package/node_modules/@llblab/skills/domain-dag/domain-dag.json +9 -0
  89. package/node_modules/@llblab/skills/domain-dag/recipes/validate-domain-dag.json +7 -0
  90. package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.mjs +771 -0
  91. package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.sh +4 -0
  92. package/node_modules/@llblab/skills/edge-tts/AGENTS.md +19 -0
  93. package/node_modules/@llblab/skills/edge-tts/SKILL.md +56 -0
  94. package/node_modules/@llblab/skills/edge-tts/recipes/say-edge.json +9 -0
  95. package/node_modules/@llblab/skills/edge-tts/scripts/say.mjs +1548 -0
  96. package/node_modules/@llblab/skills/edge-tts/scripts/say.sh +4 -0
  97. package/node_modules/@llblab/skills/extra-self/SKILL.md +229 -0
  98. package/node_modules/@llblab/skills/frontend-design/SKILL.md +78 -0
  99. package/node_modules/@llblab/skills/groq-stt/AGENTS.md +15 -0
  100. package/node_modules/@llblab/skills/groq-stt/SKILL.md +35 -0
  101. package/node_modules/@llblab/skills/groq-stt/recipes/transcribe-groq.json +10 -0
  102. package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.mjs +190 -0
  103. package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.sh +4 -0
  104. package/node_modules/@llblab/skills/mistral-stt/AGENTS.md +15 -0
  105. package/node_modules/@llblab/skills/mistral-stt/SKILL.md +35 -0
  106. package/node_modules/@llblab/skills/mistral-stt/recipes/transcribe-mistral.json +10 -0
  107. package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.mjs +191 -0
  108. package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.sh +4 -0
  109. package/node_modules/@llblab/skills/package.json +46 -0
  110. package/node_modules/@llblab/skills/re-review/SKILL.md +318 -0
  111. package/node_modules/@llblab/skills/release-flow/SKILL.md +456 -0
  112. package/node_modules/@llblab/skills/show-me/SKILL.md +144 -0
  113. package/package.json +20 -8
  114. package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +0 -143
  115. package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +0 -143
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Domain DAG project validator
4
+ * Domains: source graph, architecture validation, command-line diagnostics
5
+ * Validates local import acyclicity, composition-root boundaries, shared-bucket drift, headers, and optional configured rules
6
+ */
7
+
8
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
+ import {
10
+ basename,
11
+ dirname,
12
+ extname,
13
+ join,
14
+ relative,
15
+ resolve,
16
+ sep,
17
+ } from "node:path";
18
+
19
+ const DEFAULT_CONFIG = {
20
+ sourceRoots: ["src", "lib"],
21
+ entrypoints: [
22
+ "index.ts",
23
+ "index.tsx",
24
+ "index.js",
25
+ "index.mjs",
26
+ "src/index.ts",
27
+ "src/index.tsx",
28
+ "src/main.ts",
29
+ "src/main.tsx",
30
+ "lib/index.ts",
31
+ ],
32
+ sourceExtensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".svelte"],
33
+ ignoreDirs: [
34
+ ".git",
35
+ ".hg",
36
+ ".svn",
37
+ "node_modules",
38
+ "dist",
39
+ "build",
40
+ "coverage",
41
+ ".next",
42
+ ".nuxt",
43
+ "target",
44
+ "vendor",
45
+ ],
46
+ excludePatterns: [
47
+ "**/*.d.ts",
48
+ "**/*.test.ts",
49
+ "**/*.test.tsx",
50
+ "**/*.test.js",
51
+ "**/*.spec.ts",
52
+ "**/*.spec.tsx",
53
+ "**/*.spec.js",
54
+ "**/__tests__/**",
55
+ ],
56
+ importAliases: {},
57
+ requireHeaders: true,
58
+ headerPattern: "\\b(?:Domain|Domains|Zone|Zones|Owns)" + ":\\s*\\S",
59
+ headerSeverity: "warn",
60
+ headerRequiredClauses: [],
61
+ headerRequiredClausesSeverity: "warn",
62
+ flatRoots: false,
63
+ flatRootSeverity: "warn",
64
+ sharedBucketNames: [
65
+ "types",
66
+ "constants",
67
+ "utils",
68
+ "helpers",
69
+ "shared",
70
+ "common",
71
+ ],
72
+ allowedSharedBuckets: [],
73
+ sharedBucketSeverity: "warn",
74
+ forbiddenEdges: [],
75
+ layers: [],
76
+ surfaceRules: [],
77
+ };
78
+ const DEFAULT_CONFIG_NAMES = ["domain-dag.json", ".domain-dag.json"];
79
+ const MAX_LISTED_ITEMS = 30;
80
+
81
+ function toPosixPath(path) {
82
+ return path.split(sep).join("/");
83
+ }
84
+
85
+ function escapeRegExp(text) {
86
+ return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
87
+ }
88
+
89
+ function globToRegExp(glob) {
90
+ const pattern = toPosixPath(glob);
91
+ let output = "^";
92
+ for (let index = 0; index < pattern.length; index += 1) {
93
+ const char = pattern[index];
94
+ if (
95
+ char === "*" &&
96
+ pattern[index + 1] === "*" &&
97
+ pattern[index + 2] === "/"
98
+ ) {
99
+ output += "(?:.*/)?";
100
+ index += 2;
101
+ continue;
102
+ }
103
+ if (char === "*" && pattern[index + 1] === "*") {
104
+ output += ".*";
105
+ index += 1;
106
+ continue;
107
+ }
108
+ if (char === "*") {
109
+ output += "[^/]*";
110
+ continue;
111
+ }
112
+ if (char === "?") {
113
+ output += "[^/]";
114
+ continue;
115
+ }
116
+ output += escapeRegExp(char);
117
+ }
118
+ return new RegExp(`${output}$`);
119
+ }
120
+
121
+ function matchesAnyGlob(path, patterns) {
122
+ const normalized = toPosixPath(path);
123
+ return patterns.some((pattern) => globToRegExp(pattern).test(normalized));
124
+ }
125
+
126
+ function parseArgs(argv) {
127
+ const args = {
128
+ json: false,
129
+ strict: false,
130
+ root: undefined,
131
+ config: undefined,
132
+ };
133
+ for (let index = 0; index < argv.length; index += 1) {
134
+ const arg = argv[index];
135
+ if (arg === "--json") {
136
+ args.json = true;
137
+ continue;
138
+ }
139
+ if (arg === "--strict") {
140
+ args.strict = true;
141
+ continue;
142
+ }
143
+ if (arg === "--root") {
144
+ args.root = argv[index + 1];
145
+ index += 1;
146
+ continue;
147
+ }
148
+ if (arg === "--config") {
149
+ args.config = argv[index + 1];
150
+ index += 1;
151
+ continue;
152
+ }
153
+ if (arg === "--help" || arg === "-h") {
154
+ args.help = true;
155
+ continue;
156
+ }
157
+ if (!arg.startsWith("-") && !args.root) {
158
+ args.root = arg;
159
+ continue;
160
+ }
161
+ throw new Error(`Unknown argument: ${arg}`);
162
+ }
163
+ return args;
164
+ }
165
+
166
+ function printHelp() {
167
+ console.log(
168
+ [
169
+ "Domain DAG validator",
170
+ "",
171
+ "Usage:",
172
+ " validate-domain-dag.sh [--root <path>] [--config <path>] [--strict] [--json]",
173
+ "",
174
+ "Checks local source import cycles, composition-root reach-through, shared buckets, headers, and configured layer/edge rules.",
175
+ ].join("\n"),
176
+ );
177
+ }
178
+
179
+ function readJsonFile(path) {
180
+ return JSON.parse(readFileSync(path, "utf8"));
181
+ }
182
+
183
+ function findConfigPath(root, explicitConfig) {
184
+ if (explicitConfig) return resolve(root, explicitConfig);
185
+ for (const name of DEFAULT_CONFIG_NAMES) {
186
+ const candidate = join(root, name);
187
+ if (existsSync(candidate)) return candidate;
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ function loadConfig(root, explicitConfig) {
193
+ const configPath = findConfigPath(root, explicitConfig);
194
+ if (!configPath)
195
+ return {
196
+ config: { ...DEFAULT_CONFIG },
197
+ configPath: undefined,
198
+ };
199
+ const loadedConfig = readJsonFile(configPath);
200
+ return { config: { ...DEFAULT_CONFIG, ...loadedConfig }, configPath };
201
+ }
202
+
203
+ function createReporter({ json, strict }) {
204
+ const state = {
205
+ errors: 0,
206
+ warnings: 0,
207
+ items: [],
208
+ };
209
+ const color = (code, text) =>
210
+ json || process.env.NO_COLOR || !process.stdout.isTTY
211
+ ? text
212
+ : `\x1b[${code}m${text}\x1b[0m`;
213
+ const emit = (level, message, details = {}) => {
214
+ const effectiveLevel = strict && level === "warn" ? "error" : level;
215
+ if (effectiveLevel === "error") state.errors += 1;
216
+ if (effectiveLevel === "warn") state.warnings += 1;
217
+ state.items.push({ level: effectiveLevel, message, details });
218
+ if (json) return;
219
+ const label =
220
+ effectiveLevel === "error"
221
+ ? color("0;31", "[FAIL]")
222
+ : effectiveLevel === "warn"
223
+ ? color("1;33", "[WARN]")
224
+ : effectiveLevel === "pass"
225
+ ? color("0;32", "[PASS]")
226
+ : color("0;36", "[INFO]");
227
+ console.log(`${label} ${message}`);
228
+ };
229
+ return {
230
+ state,
231
+ info: (message, details) => emit("info", message, details),
232
+ pass: (message, details) => emit("pass", message, details),
233
+ warn: (message, details) => emit("warn", message, details),
234
+ error: (message, details) => emit("error", message, details),
235
+ bySeverity: (severity, message, details) => {
236
+ if (severity === "off") return;
237
+ if (severity === "error") emit("error", message, details);
238
+ else emit("warn", message, details);
239
+ },
240
+ };
241
+ }
242
+
243
+ function isIgnoredDirectory(name, config) {
244
+ return config.ignoreDirs.includes(name);
245
+ }
246
+
247
+ function isSourceFile(path, root, config) {
248
+ const relativePath = toPosixPath(relative(root, path));
249
+ const extension = extname(path);
250
+ if (!config.sourceExtensions.includes(extension)) return false;
251
+ if (relativePath.endsWith(".d.ts")) return false;
252
+ return !matchesAnyGlob(relativePath, config.excludePatterns);
253
+ }
254
+
255
+ function walkSourceFiles(directory, root, config, files) {
256
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
257
+ const absolutePath = join(directory, entry.name);
258
+ if (entry.isDirectory()) {
259
+ if (!isIgnoredDirectory(entry.name, config))
260
+ walkSourceFiles(absolutePath, root, config, files);
261
+ continue;
262
+ }
263
+ if (entry.isFile() && isSourceFile(absolutePath, root, config))
264
+ files.add(resolve(absolutePath));
265
+ }
266
+ }
267
+
268
+ function collectSourceFiles(root, config) {
269
+ const files = new Set();
270
+ for (const sourceRoot of config.sourceRoots) {
271
+ const absoluteRoot = resolve(root, sourceRoot);
272
+ if (!existsSync(absoluteRoot)) continue;
273
+ const stats = statSync(absoluteRoot);
274
+ if (stats.isFile() && isSourceFile(absoluteRoot, root, config))
275
+ files.add(resolve(absoluteRoot));
276
+ if (stats.isDirectory()) walkSourceFiles(absoluteRoot, root, config, files);
277
+ }
278
+ for (const entrypoint of config.entrypoints) {
279
+ const absolutePath = resolve(root, entrypoint);
280
+ if (
281
+ existsSync(absolutePath) &&
282
+ statSync(absolutePath).isFile() &&
283
+ isSourceFile(absolutePath, root, config)
284
+ )
285
+ files.add(resolve(absolutePath));
286
+ }
287
+ return [...files].sort((left, right) => left.localeCompare(right));
288
+ }
289
+
290
+ function stripSourceComments(source) {
291
+ return source
292
+ .replace(/\/\*[\s\S]*?\*\//g, "")
293
+ .replace(/(^|[^:])\/\/[^\n\r]*/gm, "$1");
294
+ }
295
+
296
+ function getImportSpecifiers(source) {
297
+ const cleaned = stripSourceComments(source);
298
+ const specifiers = new Set();
299
+ const patterns = [
300
+ /(import|export)\s+(type\s+)?[^"']*?\s+from\s*["']([^"']+)["']/g,
301
+ /import\s*["']([^"']+)["']/g,
302
+ /import\s*\(\s*["']([^"']+)["']\s*\)/g,
303
+ /require\s*\(\s*["']([^"']+)["']\s*\)/g,
304
+ ];
305
+ for (const pattern of patterns) {
306
+ for (const match of cleaned.matchAll(pattern)) {
307
+ specifiers.add(match[match.length - 1]);
308
+ }
309
+ }
310
+ return [...specifiers].filter(Boolean);
311
+ }
312
+
313
+ function getImportResolutionCandidates(basePath, config) {
314
+ const candidates = [basePath];
315
+ for (const extension of config.sourceExtensions)
316
+ candidates.push(`${basePath}${extension}`);
317
+ for (const extension of config.sourceExtensions)
318
+ candidates.push(join(basePath, `index${extension}`));
319
+ return candidates;
320
+ }
321
+
322
+ function getImportAliasBase(root, specifier, config) {
323
+ const aliases = config.importAliases ?? {};
324
+ for (const [aliasPattern, targetPattern] of Object.entries(aliases)) {
325
+ if (aliasPattern.endsWith("/*") && targetPattern.endsWith("/*")) {
326
+ const aliasPrefix = aliasPattern.slice(0, -1);
327
+ if (specifier.startsWith(aliasPrefix)) {
328
+ const suffix = specifier.slice(aliasPrefix.length);
329
+ return resolve(root, targetPattern.slice(0, -1), suffix);
330
+ }
331
+ continue;
332
+ }
333
+ if (specifier === aliasPattern) return resolve(root, targetPattern);
334
+ }
335
+ return undefined;
336
+ }
337
+
338
+ function resolveLocalImport(fromFile, specifier, fileSet, config, root) {
339
+ const basePath = specifier.startsWith(".")
340
+ ? resolve(dirname(fromFile), specifier)
341
+ : getImportAliasBase(root, specifier, config);
342
+ if (!basePath) return undefined;
343
+ for (const candidate of getImportResolutionCandidates(basePath, config)) {
344
+ const normalized = resolve(candidate);
345
+ if (fileSet.has(normalized)) return normalized;
346
+ }
347
+ return undefined;
348
+ }
349
+
350
+ function buildImportGraph(root, files, config) {
351
+ const fileSet = new Set(files.map((file) => resolve(file)));
352
+ const graph = new Map();
353
+ for (const file of files) {
354
+ const deps = [];
355
+ const source = readFileSync(file, "utf8");
356
+ for (const specifier of getImportSpecifiers(source)) {
357
+ const resolved = resolveLocalImport(
358
+ file,
359
+ specifier,
360
+ fileSet,
361
+ config,
362
+ root,
363
+ );
364
+ if (resolved) deps.push(toPosixPath(relative(root, resolved)));
365
+ }
366
+ graph.set(toPosixPath(relative(root, file)), [...new Set(deps)].sort());
367
+ }
368
+ return graph;
369
+ }
370
+
371
+ function findCycles(graph) {
372
+ const cycles = [];
373
+ const visited = new Set();
374
+ const active = [];
375
+ const visit = (file) => {
376
+ const activeIndex = active.indexOf(file);
377
+ if (activeIndex !== -1) {
378
+ cycles.push([...active.slice(activeIndex), file]);
379
+ return;
380
+ }
381
+ if (visited.has(file)) return;
382
+ visited.add(file);
383
+ active.push(file);
384
+ for (const dep of graph.get(file) ?? []) visit(dep);
385
+ active.pop();
386
+ };
387
+ for (const file of graph.keys()) visit(file);
388
+ return cycles;
389
+ }
390
+
391
+ function getEntrypointSet(root, files, config) {
392
+ const fileSet = new Set(files.map((file) => resolve(file)));
393
+ const entrypoints = new Set();
394
+ for (const entrypoint of config.entrypoints) {
395
+ const absolutePath = resolve(root, entrypoint);
396
+ if (fileSet.has(absolutePath))
397
+ entrypoints.add(toPosixPath(relative(root, absolutePath)));
398
+ }
399
+ return entrypoints;
400
+ }
401
+
402
+ function validateEntrypointReachThrough(graph, entrypoints, reporter) {
403
+ const violations = [];
404
+ for (const [file, deps] of graph.entries()) {
405
+ if (entrypoints.has(file)) continue;
406
+ for (const dep of deps)
407
+ if (entrypoints.has(dep)) violations.push(`${file} -> ${dep}`);
408
+ }
409
+ if (violations.length === 0) {
410
+ reporter.pass("Domain modules do not import configured entrypoints");
411
+ return;
412
+ }
413
+ for (const violation of violations.slice(0, MAX_LISTED_ITEMS))
414
+ reporter.error(`Domain module imports composition root: ${violation}`);
415
+ if (violations.length > MAX_LISTED_ITEMS)
416
+ reporter.error(
417
+ `Additional entrypoint reach-through violations: ${violations.length - MAX_LISTED_ITEMS}`,
418
+ );
419
+ }
420
+
421
+ function validateHeaders(root, files, config, reporter) {
422
+ if (!config.requireHeaders) return;
423
+ const pattern = new RegExp(config.headerPattern, "im");
424
+ const missing = [];
425
+ const missingClauses = [];
426
+ const requiredClauses = Array.isArray(config.headerRequiredClauses)
427
+ ? config.headerRequiredClauses
428
+ : [];
429
+ for (const file of files) {
430
+ const relativePath = toPosixPath(relative(root, file));
431
+ const sourceHead = readFileSync(file, "utf8").slice(0, 1600);
432
+ const hasHeader = pattern.test(sourceHead);
433
+ if (!hasHeader) {
434
+ missing.push(relativePath);
435
+ continue;
436
+ }
437
+ for (const clause of requiredClauses) {
438
+ if (clause && !sourceHead.includes(clause))
439
+ missingClauses.push(`${relativePath} missing '${clause}'`);
440
+ }
441
+ }
442
+ if (missing.length === 0) reporter.pass("Domain headers are present");
443
+ else {
444
+ for (const file of missing.slice(0, MAX_LISTED_ITEMS))
445
+ reporter.bySeverity(
446
+ config.headerSeverity,
447
+ `Missing domain header: ${file}`,
448
+ );
449
+ if (missing.length > MAX_LISTED_ITEMS)
450
+ reporter.bySeverity(
451
+ config.headerSeverity,
452
+ `Additional files without domain headers: ${missing.length - MAX_LISTED_ITEMS}`,
453
+ );
454
+ }
455
+ if (requiredClauses.length === 0) return;
456
+ if (missingClauses.length === 0) {
457
+ reporter.pass("Required domain-header clauses are present");
458
+ return;
459
+ }
460
+ for (const item of missingClauses.slice(0, MAX_LISTED_ITEMS))
461
+ reporter.bySeverity(
462
+ config.headerRequiredClausesSeverity,
463
+ `Domain header clause missing: ${item}`,
464
+ );
465
+ if (missingClauses.length > MAX_LISTED_ITEMS)
466
+ reporter.bySeverity(
467
+ config.headerRequiredClausesSeverity,
468
+ `Additional missing domain-header clauses: ${missingClauses.length - MAX_LISTED_ITEMS}`,
469
+ );
470
+ }
471
+
472
+ function validateSharedBuckets(root, files, config, reporter) {
473
+ const violations = [];
474
+ const names = new Set(config.sharedBucketNames);
475
+ for (const file of files) {
476
+ const relativePath = toPosixPath(relative(root, file));
477
+ if (matchesAnyGlob(relativePath, config.allowedSharedBuckets)) continue;
478
+ const parts = relativePath.split("/");
479
+ const baseName = basename(relativePath, extname(relativePath));
480
+ if (names.has(baseName)) violations.push(`file ${relativePath}`);
481
+ for (const part of parts.slice(0, -1))
482
+ if (names.has(part)) violations.push(`folder ${part} in ${relativePath}`);
483
+ }
484
+ if (violations.length === 0) {
485
+ reporter.pass("No shared-bucket candidates found");
486
+ return;
487
+ }
488
+ for (const violation of violations.slice(0, MAX_LISTED_ITEMS))
489
+ reporter.bySeverity(
490
+ config.sharedBucketSeverity,
491
+ `Shared-bucket candidate: ${violation}`,
492
+ );
493
+ if (violations.length > MAX_LISTED_ITEMS)
494
+ reporter.bySeverity(
495
+ config.sharedBucketSeverity,
496
+ `Additional shared-bucket candidates: ${violations.length - MAX_LISTED_ITEMS}`,
497
+ );
498
+ }
499
+
500
+ function validateFlatRoots(root, files, config, reporter) {
501
+ if (!config.flatRoots) return;
502
+ const violations = [];
503
+ const sourceRoots = config.sourceRoots
504
+ .map((sourceRoot) => resolve(root, sourceRoot))
505
+ .filter(
506
+ (sourceRoot) =>
507
+ existsSync(sourceRoot) && statSync(sourceRoot).isDirectory(),
508
+ );
509
+ for (const file of files) {
510
+ for (const sourceRoot of sourceRoots) {
511
+ const inside = relative(sourceRoot, file);
512
+ if (
513
+ inside.startsWith("..") ||
514
+ resolve(sourceRoot, inside) !== resolve(file)
515
+ )
516
+ continue;
517
+ if (toPosixPath(inside).includes("/"))
518
+ violations.push(toPosixPath(relative(root, file)));
519
+ }
520
+ }
521
+ if (violations.length === 0) {
522
+ reporter.pass("Configured flat roots have no nested source files");
523
+ return;
524
+ }
525
+ for (const violation of violations.slice(0, MAX_LISTED_ITEMS))
526
+ reporter.bySeverity(
527
+ config.flatRootSeverity,
528
+ `Nested source file in flat root: ${violation}`,
529
+ );
530
+ if (violations.length > MAX_LISTED_ITEMS)
531
+ reporter.bySeverity(
532
+ config.flatRootSeverity,
533
+ `Additional nested flat-root files: ${violations.length - MAX_LISTED_ITEMS}`,
534
+ );
535
+ }
536
+
537
+ function getLayerForFile(file, layers) {
538
+ for (let index = 0; index < layers.length; index += 1) {
539
+ const layer = layers[index];
540
+ const rank = Number.isFinite(layer.rank) ? layer.rank : index;
541
+ if (matchesAnyGlob(file, layer.files ?? []))
542
+ return { name: layer.name ?? `layer-${index}`, rank };
543
+ }
544
+ return undefined;
545
+ }
546
+
547
+ function validateLayers(graph, config, reporter) {
548
+ if (!Array.isArray(config.layers) || config.layers.length === 0) return;
549
+ const violations = [];
550
+ for (const [file, deps] of graph.entries()) {
551
+ const fromLayer = getLayerForFile(file, config.layers);
552
+ if (!fromLayer) continue;
553
+ for (const dep of deps) {
554
+ const toLayer = getLayerForFile(dep, config.layers);
555
+ if (toLayer && fromLayer.rank < toLayer.rank)
556
+ violations.push(
557
+ `${file} (${fromLayer.name}) -> ${dep} (${toLayer.name})`,
558
+ );
559
+ }
560
+ }
561
+ if (violations.length === 0) {
562
+ reporter.pass("Configured layer direction holds");
563
+ return;
564
+ }
565
+ for (const violation of violations.slice(0, MAX_LISTED_ITEMS))
566
+ reporter.error(`Layer direction violation: ${violation}`);
567
+ if (violations.length > MAX_LISTED_ITEMS)
568
+ reporter.error(
569
+ `Additional layer violations: ${violations.length - MAX_LISTED_ITEMS}`,
570
+ );
571
+ }
572
+
573
+ function collectConfiguredGlobs(config) {
574
+ const globs = [];
575
+ const add = (owner, value) => {
576
+ if (!value) return;
577
+ const values = Array.isArray(value) ? value : [value];
578
+ for (const item of values) {
579
+ if (typeof item === "string") globs.push({ owner, value: item });
580
+ }
581
+ };
582
+ add("sourceRoots", config.sourceRoots);
583
+ add("entrypoints", config.entrypoints);
584
+ add("allowedSharedBuckets", config.allowedSharedBuckets);
585
+ for (const edge of config.forbiddenEdges ?? []) {
586
+ add("forbiddenEdges.from", edge.from);
587
+ add("forbiddenEdges.to", edge.to);
588
+ }
589
+ for (const layer of config.layers ?? [])
590
+ add(`layers.${layer.name ?? "unnamed"}`, layer.files);
591
+ for (const rule of config.surfaceRules ?? [])
592
+ add(`surfaceRules.${rule.name ?? "unnamed"}`, rule.files);
593
+ return globs;
594
+ }
595
+
596
+ function validateConfigSyntax(config, reporter) {
597
+ const unsupportedBraceGlobs = collectConfiguredGlobs(config).filter(
598
+ ({ value }) => /[{}]/.test(value),
599
+ );
600
+ for (const glob of unsupportedBraceGlobs.slice(0, MAX_LISTED_ITEMS)) {
601
+ reporter.warn(
602
+ `Unsupported brace glob syntax in ${glob.owner}: ${glob.value}; expand it into separate glob entries`,
603
+ );
604
+ }
605
+ if (unsupportedBraceGlobs.length > MAX_LISTED_ITEMS)
606
+ reporter.warn(
607
+ `Additional unsupported brace globs: ${unsupportedBraceGlobs.length - MAX_LISTED_ITEMS}`,
608
+ );
609
+ }
610
+
611
+ function validateSurfaceRules(root, files, config, reporter) {
612
+ if (!Array.isArray(config.surfaceRules) || config.surfaceRules.length === 0)
613
+ return;
614
+ for (const rule of config.surfaceRules) {
615
+ const max = Number.isFinite(rule.max) ? rule.max : undefined;
616
+ if (max === undefined) continue;
617
+ const filesGlob = Array.isArray(rule.files)
618
+ ? rule.files
619
+ : rule.files
620
+ ? [rule.files]
621
+ : ["**/*"];
622
+ const violations = [];
623
+ for (const file of files) {
624
+ const relativePath = toPosixPath(relative(root, file));
625
+ if (!matchesAnyGlob(relativePath, filesGlob)) continue;
626
+ const source = readFileSync(file, "utf8");
627
+ let count = 0;
628
+ if (rule.metric === "lines") {
629
+ count = source.split(/\r?\n/).length;
630
+ } else {
631
+ if (!rule.pattern) continue;
632
+ const pattern = new RegExp(rule.pattern, rule.flags ?? "g");
633
+ const matches = new Set();
634
+ for (const match of source.matchAll(pattern)) {
635
+ matches.add(match[1] ?? match[0]);
636
+ }
637
+ count = matches.size;
638
+ }
639
+ if (count > max)
640
+ violations.push(`${relativePath} measured ${count}/${max}`);
641
+ }
642
+ if (violations.length === 0) {
643
+ reporter.pass(
644
+ rule.passMessage ??
645
+ `Surface rule holds: ${rule.name ?? rule.pattern ?? rule.metric}`,
646
+ );
647
+ continue;
648
+ }
649
+ for (const violation of violations.slice(0, MAX_LISTED_ITEMS))
650
+ reporter.bySeverity(
651
+ rule.severity ?? "warn",
652
+ `${rule.message ?? "Wide interface surface"}: ${violation}`,
653
+ );
654
+ if (violations.length > MAX_LISTED_ITEMS)
655
+ reporter.bySeverity(
656
+ rule.severity ?? "warn",
657
+ `Additional surface-rule violations: ${violations.length - MAX_LISTED_ITEMS}`,
658
+ );
659
+ }
660
+ }
661
+
662
+ function validateForbiddenEdges(graph, config, reporter) {
663
+ if (
664
+ !Array.isArray(config.forbiddenEdges) ||
665
+ config.forbiddenEdges.length === 0
666
+ )
667
+ return;
668
+ const violations = [];
669
+ for (const [file, deps] of graph.entries()) {
670
+ for (const dep of deps) {
671
+ for (const rule of config.forbiddenEdges) {
672
+ const fromMatches =
673
+ !rule.from ||
674
+ matchesAnyGlob(
675
+ file,
676
+ Array.isArray(rule.from) ? rule.from : [rule.from],
677
+ );
678
+ const toMatches =
679
+ !rule.to ||
680
+ matchesAnyGlob(dep, Array.isArray(rule.to) ? rule.to : [rule.to]);
681
+ if (fromMatches && toMatches)
682
+ violations.push({
683
+ severity: rule.severity ?? "error",
684
+ message: rule.message ?? "Forbidden edge",
685
+ edge: `${file} -> ${dep}`,
686
+ });
687
+ }
688
+ }
689
+ }
690
+ if (violations.length === 0) {
691
+ reporter.pass("Configured forbidden edges are absent");
692
+ return;
693
+ }
694
+ for (const violation of violations.slice(0, MAX_LISTED_ITEMS))
695
+ reporter.bySeverity(
696
+ violation.severity,
697
+ `${violation.message}: ${violation.edge}`,
698
+ );
699
+ if (violations.length > MAX_LISTED_ITEMS)
700
+ reporter.error(
701
+ `Additional forbidden-edge violations: ${violations.length - MAX_LISTED_ITEMS}`,
702
+ );
703
+ }
704
+
705
+ function countEdges(graph) {
706
+ let count = 0;
707
+ for (const deps of graph.values()) count += deps.length;
708
+ return count;
709
+ }
710
+
711
+ function runValidation(args) {
712
+ const root = resolve(
713
+ args.root ?? process.env.DOMAIN_DAG_ROOT ?? process.cwd(),
714
+ );
715
+ const { config, configPath } = loadConfig(root, args.config);
716
+ const reporter = createReporter(args);
717
+ const files = collectSourceFiles(root, config);
718
+ const graph = buildImportGraph(root, files, config);
719
+ const cycles = findCycles(graph);
720
+ if (!args.json) console.log("--- DOMAIN DAG VALIDATOR ---\n");
721
+ reporter.info(`Root: ${root}`);
722
+ if (configPath) reporter.info(`Config: ${configPath}`);
723
+ validateConfigSyntax(config, reporter);
724
+ reporter.info(
725
+ `Source files ${files.length}, local edges ${countEdges(graph)}`,
726
+ );
727
+ if (files.length === 0)
728
+ reporter.warn("No source files found in configured roots");
729
+ if (cycles.length === 0)
730
+ reporter.pass("Local source import graph is acyclic");
731
+ for (const cycle of cycles.slice(0, MAX_LISTED_ITEMS))
732
+ reporter.error(`Import cycle: ${cycle.join(" -> ")}`);
733
+ if (cycles.length > MAX_LISTED_ITEMS)
734
+ reporter.error(
735
+ `Additional import cycles: ${cycles.length - MAX_LISTED_ITEMS}`,
736
+ );
737
+ validateEntrypointReachThrough(
738
+ graph,
739
+ getEntrypointSet(root, files, config),
740
+ reporter,
741
+ );
742
+ validateHeaders(root, files, config, reporter);
743
+ validateSharedBuckets(root, files, config, reporter);
744
+ validateFlatRoots(root, files, config, reporter);
745
+ validateLayers(graph, config, reporter);
746
+ validateSurfaceRules(root, files, config, reporter);
747
+ validateForbiddenEdges(graph, config, reporter);
748
+ if (args.json) console.log(JSON.stringify(reporter.state, null, 2));
749
+ else
750
+ console.log(
751
+ `\nResult: ${reporter.state.errors} error(s), ${reporter.state.warnings} warning(s)`,
752
+ );
753
+ return reporter.state.errors === 0 ? 0 : 1;
754
+ }
755
+
756
+ function main() {
757
+ const args = parseArgs(process.argv.slice(2));
758
+ if (args.help) {
759
+ printHelp();
760
+ return 0;
761
+ }
762
+ return runValidation(args);
763
+ }
764
+
765
+ try {
766
+ process.exitCode = main();
767
+ } catch (error) {
768
+ const message = error instanceof Error ? error.message : String(error);
769
+ console.error(`RUNTIME ERROR: ${message}`);
770
+ process.exitCode = 1;
771
+ }