mustflow 2.103.16 → 2.103.20

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 (32) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/commands/run/args.js +83 -0
  3. package/dist/cli/commands/run/execution.js +334 -0
  4. package/dist/cli/commands/run/preview.js +29 -0
  5. package/dist/cli/commands/run/profile.js +6 -0
  6. package/dist/cli/commands/run.js +19 -425
  7. package/dist/cli/commands/script-pack.js +1 -0
  8. package/dist/cli/commands/verify.js +15 -18
  9. package/dist/cli/i18n/en.js +27 -0
  10. package/dist/cli/i18n/es.js +27 -0
  11. package/dist/cli/i18n/fr.js +27 -0
  12. package/dist/cli/i18n/hi.js +27 -0
  13. package/dist/cli/i18n/ko.js +27 -0
  14. package/dist/cli/i18n/zh.js +27 -0
  15. package/dist/cli/lib/command-registry.js +92 -0
  16. package/dist/cli/lib/script-pack-registry.js +39 -0
  17. package/dist/cli/script-packs/code-module-boundary.js +210 -0
  18. package/dist/core/module-boundary.js +523 -0
  19. package/dist/core/public-json-contracts.js +50 -0
  20. package/dist/core/script-pack-suggestions.js +5 -0
  21. package/package.json +1 -1
  22. package/schemas/README.md +12 -0
  23. package/schemas/check-report.schema.json +52 -0
  24. package/schemas/index-report.schema.json +103 -0
  25. package/schemas/module-boundary-report.schema.json +210 -0
  26. package/schemas/search-report.schema.json +102 -0
  27. package/schemas/status-report.schema.json +50 -0
  28. package/templates/default/i18n.toml +3 -3
  29. package/templates/default/locales/en/.mustflow/skills/database-migration-change/SKILL.md +16 -2
  30. package/templates/default/locales/en/.mustflow/skills/module-boundary-review/SKILL.md +12 -1
  31. package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +17 -10
  32. package/templates/default/manifest.toml +1 -1
@@ -0,0 +1,523 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { DEPENDENCY_GRAPH_SCRIPT_REF, inspectDependencyGraph, } from './dependency-graph.js';
5
+ import { DEFAULT_IGNORED_DIRECTORIES, isIgnoredDirectoryPath } from './ignored-directories.js';
6
+ import { ensureInside, ensureInsideWithoutSymlinks, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
7
+ import { parseTomlText } from './toml.js';
8
+ export const MODULE_BOUNDARY_PACK_ID = 'code';
9
+ export const MODULE_BOUNDARY_SCRIPT_ID = 'module-boundary';
10
+ export const MODULE_BOUNDARY_SCRIPT_REF = `${MODULE_BOUNDARY_PACK_ID}/${MODULE_BOUNDARY_SCRIPT_ID}`;
11
+ const DEFAULT_CONFIG_PATH = '.mustflow/config/module-boundaries.toml';
12
+ const DEFAULT_MAX_DEPTH = 20;
13
+ const DEFAULT_MAX_CYCLES = 50;
14
+ const DEFAULT_MAX_SHARED_FILES = 1000;
15
+ const MAX_ISSUES = 50;
16
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mts', '.cts', '.js', '.jsx', '.mjs', '.cjs']);
17
+ const DEFAULT_ENTRYPOINT_NAMES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mts', 'index.mjs', 'index.cjs', 'index.cts'];
18
+ const ERROR_CODES = new Set([
19
+ 'dependency_graph_path_outside_root',
20
+ 'dependency_graph_unreadable_path',
21
+ 'module_boundary_invalid_config',
22
+ ]);
23
+ const NON_BLOCKING_CODES = new Set(['module_boundary_config_missing']);
24
+ function toPosixPath(value) {
25
+ return value.replace(/\\/gu, '/');
26
+ }
27
+ function normalizeRelativePath(value) {
28
+ return toPosixPath(value).replace(/^\.\/+/u, '').replace(/\/+/gu, '/') || '.';
29
+ }
30
+ function sha256Tagged(value) {
31
+ return `sha256:${createHash('sha256').update(value).digest('hex')}`;
32
+ }
33
+ function pushIssue(issues, issue) {
34
+ if (issues.length < MAX_ISSUES) {
35
+ issues.push(issue);
36
+ }
37
+ }
38
+ function makeFinding(code, severity, pathValue, message, extras = {}) {
39
+ return { code, severity, path: pathValue, message, ...extras };
40
+ }
41
+ function normalizeGraphFinding(finding) {
42
+ return {
43
+ code: finding.code,
44
+ severity: finding.severity,
45
+ path: finding.path,
46
+ message: finding.message,
47
+ };
48
+ }
49
+ function isRecord(value) {
50
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
51
+ }
52
+ function asRecordArray(value) {
53
+ if (!Array.isArray(value)) {
54
+ return [];
55
+ }
56
+ return value.filter(isRecord);
57
+ }
58
+ function asStringArray(value) {
59
+ if (!Array.isArray(value)) {
60
+ return [];
61
+ }
62
+ return value.filter((entry) => typeof entry === 'string').map(normalizeRelativePath);
63
+ }
64
+ function stringField(record, field) {
65
+ const value = record[field];
66
+ return typeof value === 'string' && value.trim().length > 0 ? normalizeRelativePath(value) : null;
67
+ }
68
+ function booleanField(record, field, fallback) {
69
+ return typeof record[field] === 'boolean' ? record[field] : fallback;
70
+ }
71
+ function integerField(record, field) {
72
+ const value = record[field];
73
+ return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : null;
74
+ }
75
+ function ruleId(record, fallback) {
76
+ const value = record.id;
77
+ return typeof value === 'string' && value.trim().length > 0 ? value : fallback;
78
+ }
79
+ function parseLayerRules(raw) {
80
+ return asRecordArray(raw)
81
+ .map((entry, index) => {
82
+ const paths = asStringArray(entry.paths);
83
+ const denyImports = asStringArray(entry.deny_imports ?? entry.denyImports);
84
+ if (paths.length === 0 || denyImports.length === 0) {
85
+ return null;
86
+ }
87
+ return { id: ruleId(entry, `layer:${index + 1}`), paths, denyImports };
88
+ })
89
+ .filter((entry) => entry !== null);
90
+ }
91
+ function parsePublicEntrypointRules(raw) {
92
+ return asRecordArray(raw)
93
+ .map((entry, index) => {
94
+ const root = stringField(entry, 'root');
95
+ const entrypoint = stringField(entry, 'entrypoint') ?? stringField(entry, 'entry');
96
+ if (!root || !entrypoint) {
97
+ return null;
98
+ }
99
+ return { id: ruleId(entry, `public_entrypoint:${index + 1}`), root, entrypoint };
100
+ })
101
+ .filter((entry) => entry !== null);
102
+ }
103
+ function parseFeatureGroupRules(raw) {
104
+ return asRecordArray(raw)
105
+ .map((entry, index) => {
106
+ const root = stringField(entry, 'root');
107
+ if (!root) {
108
+ return null;
109
+ }
110
+ const entrypointNames = asStringArray(entry.entrypoint_names ?? entry.entrypointNames);
111
+ return {
112
+ id: ruleId(entry, `feature_group:${index + 1}`),
113
+ root,
114
+ allowEntrypointImports: booleanField(entry, 'allow_entrypoint_imports', true),
115
+ entrypointNames: entrypointNames.length > 0 ? entrypointNames : DEFAULT_ENTRYPOINT_NAMES,
116
+ };
117
+ })
118
+ .filter((entry) => entry !== null);
119
+ }
120
+ function parseSharedBudgetRules(raw) {
121
+ return asRecordArray(raw)
122
+ .map((entry, index) => {
123
+ const sharedPath = stringField(entry, 'path');
124
+ if (!sharedPath) {
125
+ return null;
126
+ }
127
+ return {
128
+ id: ruleId(entry, `shared_budget:${index + 1}`),
129
+ path: sharedPath,
130
+ maxFiles: integerField(entry, 'max_files'),
131
+ maxExports: integerField(entry, 'max_exports'),
132
+ };
133
+ })
134
+ .filter((entry) => entry !== null);
135
+ }
136
+ function readModuleBoundaryConfig(projectRoot, configPath) {
137
+ const relativeConfigPath = normalizeRelativePath(configPath);
138
+ const absoluteConfigPath = path.resolve(projectRoot, relativeConfigPath);
139
+ const findings = [];
140
+ const issues = [];
141
+ try {
142
+ ensureInside(projectRoot, absoluteConfigPath);
143
+ ensureInsideWithoutSymlinks(projectRoot, absoluteConfigPath, { allowMissingLeaf: true });
144
+ }
145
+ catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ return {
148
+ path: relativeConfigPath,
149
+ status: 'invalid',
150
+ layers: [],
151
+ publicEntrypoints: [],
152
+ featureGroups: [],
153
+ sharedBudgets: [],
154
+ findings: [makeFinding('module_boundary_invalid_config', 'high', relativeConfigPath, message)],
155
+ issues: [message],
156
+ };
157
+ }
158
+ if (!existsSync(absoluteConfigPath)) {
159
+ const message = `Module boundary config not found at ${relativeConfigPath}; no boundary rules were enforced.`;
160
+ return {
161
+ path: relativeConfigPath,
162
+ status: 'missing',
163
+ layers: [],
164
+ publicEntrypoints: [],
165
+ featureGroups: [],
166
+ sharedBudgets: [],
167
+ findings: [makeFinding('module_boundary_config_missing', 'low', relativeConfigPath, message)],
168
+ issues: [message],
169
+ };
170
+ }
171
+ let parsed;
172
+ try {
173
+ const content = readFileInsideWithoutSymlinks(projectRoot, absoluteConfigPath, { maxBytes: 256 * 1024 }).toString('utf8');
174
+ parsed = parseTomlText(content);
175
+ }
176
+ catch (error) {
177
+ const message = error instanceof Error ? error.message : String(error);
178
+ return {
179
+ path: relativeConfigPath,
180
+ status: 'invalid',
181
+ layers: [],
182
+ publicEntrypoints: [],
183
+ featureGroups: [],
184
+ sharedBudgets: [],
185
+ findings: [makeFinding('module_boundary_invalid_config', 'high', relativeConfigPath, message)],
186
+ issues: [message],
187
+ };
188
+ }
189
+ if (!isRecord(parsed)) {
190
+ const message = 'Module boundary config must be a TOML table.';
191
+ return {
192
+ path: relativeConfigPath,
193
+ status: 'invalid',
194
+ layers: [],
195
+ publicEntrypoints: [],
196
+ featureGroups: [],
197
+ sharedBudgets: [],
198
+ findings: [makeFinding('module_boundary_invalid_config', 'high', relativeConfigPath, message)],
199
+ issues: [message],
200
+ };
201
+ }
202
+ const layers = parseLayerRules(parsed.layers);
203
+ const publicEntrypoints = parsePublicEntrypointRules(parsed.public_entrypoints ?? parsed.publicEntrypoints);
204
+ const featureGroups = parseFeatureGroupRules(parsed.feature_groups ?? parsed.featureGroups);
205
+ const sharedBudgets = parseSharedBudgetRules(parsed.shared_budgets ?? parsed.sharedBudgets);
206
+ if (layers.length + publicEntrypoints.length + featureGroups.length + sharedBudgets.length === 0) {
207
+ pushIssue(issues, `Module boundary config ${relativeConfigPath} contains no usable rules.`);
208
+ }
209
+ return {
210
+ path: relativeConfigPath,
211
+ status: 'found',
212
+ layers,
213
+ publicEntrypoints,
214
+ featureGroups,
215
+ sharedBudgets,
216
+ findings,
217
+ issues,
218
+ };
219
+ }
220
+ function patternToRegExp(pattern) {
221
+ const normalized = normalizeRelativePath(pattern);
222
+ const escaped = normalized.replace(/[.+?^${}()|[\]\\]/gu, '\\$&').replace(/\*\*/gu, '\u0000').replace(/\*/gu, '[^/]*');
223
+ return new RegExp(`^${escaped.replace(/\u0000/gu, '.*')}$`, 'u');
224
+ }
225
+ function matchesPathPattern(pattern, relativePath) {
226
+ const normalizedPattern = normalizeRelativePath(pattern);
227
+ const normalizedPath = normalizeRelativePath(relativePath);
228
+ if (normalizedPattern.endsWith('/**')) {
229
+ const prefix = normalizedPattern.slice(0, -3);
230
+ return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`);
231
+ }
232
+ if (normalizedPattern.includes('*')) {
233
+ return patternToRegExp(normalizedPattern).test(normalizedPath);
234
+ }
235
+ return normalizedPath === normalizedPattern;
236
+ }
237
+ function isInsidePath(root, candidate) {
238
+ const normalizedRoot = normalizeRelativePath(root);
239
+ const normalizedCandidate = normalizeRelativePath(candidate);
240
+ return normalizedCandidate === normalizedRoot || normalizedCandidate.startsWith(`${normalizedRoot}/`);
241
+ }
242
+ function featureSegment(root, candidate) {
243
+ if (!isInsidePath(root, candidate)) {
244
+ return null;
245
+ }
246
+ const rootPrefix = normalizeRelativePath(root);
247
+ const rest = normalizeRelativePath(candidate).slice(rootPrefix.length).replace(/^\//u, '');
248
+ const [segment] = rest.split('/');
249
+ return segment && segment.length > 0 ? segment : null;
250
+ }
251
+ function isFeatureEntrypoint(rule, targetPath) {
252
+ const normalized = normalizeRelativePath(targetPath);
253
+ return rule.entrypointNames.some((entrypointName) => normalized.endsWith(`/${normalizeRelativePath(entrypointName)}`));
254
+ }
255
+ function addEdgeFinding(findings, code, severity, ruleIdValue, edge, message) {
256
+ findings.push(makeFinding(code, severity, edge.source_path, message, {
257
+ rule_id: ruleIdValue,
258
+ source_path: edge.source_path,
259
+ target_path: edge.target_path,
260
+ line: edge.line,
261
+ }));
262
+ }
263
+ function checkLayerRules(config, edges) {
264
+ const findings = [];
265
+ for (const rule of config.layers) {
266
+ for (const edge of edges) {
267
+ if (!rule.paths.some((pattern) => matchesPathPattern(pattern, edge.source_path))) {
268
+ continue;
269
+ }
270
+ if (!rule.denyImports.some((pattern) => matchesPathPattern(pattern, edge.target_path))) {
271
+ continue;
272
+ }
273
+ addEdgeFinding(findings, 'module_boundary_forbidden_import', 'high', rule.id, edge, `${edge.source_path} imports forbidden boundary ${edge.target_path} by rule ${rule.id}.`);
274
+ }
275
+ }
276
+ return findings;
277
+ }
278
+ function checkPublicEntrypointRules(config, edges) {
279
+ const findings = [];
280
+ for (const rule of config.publicEntrypoints) {
281
+ for (const edge of edges) {
282
+ if (!isInsidePath(rule.root, edge.target_path) || edge.target_path === normalizeRelativePath(rule.entrypoint)) {
283
+ continue;
284
+ }
285
+ if (isInsidePath(rule.root, edge.source_path)) {
286
+ continue;
287
+ }
288
+ addEdgeFinding(findings, 'module_boundary_public_entry_violation', 'high', rule.id, edge, `${edge.source_path} imports ${edge.target_path} instead of public entrypoint ${rule.entrypoint}.`);
289
+ }
290
+ }
291
+ return findings;
292
+ }
293
+ function checkFeatureGroupRules(config, edges) {
294
+ const findings = [];
295
+ for (const rule of config.featureGroups) {
296
+ for (const edge of edges) {
297
+ const sourceFeature = featureSegment(rule.root, edge.source_path);
298
+ const targetFeature = featureSegment(rule.root, edge.target_path);
299
+ if (!sourceFeature || !targetFeature || sourceFeature === targetFeature) {
300
+ continue;
301
+ }
302
+ if (rule.allowEntrypointImports && isFeatureEntrypoint(rule, edge.target_path)) {
303
+ continue;
304
+ }
305
+ addEdgeFinding(findings, 'module_boundary_feature_direct_import', 'medium', rule.id, edge, `${edge.source_path} imports another feature internals at ${edge.target_path} by rule ${rule.id}.`);
306
+ }
307
+ }
308
+ return findings;
309
+ }
310
+ function isSourceFile(relativePath) {
311
+ const extension = path.extname(relativePath).toLowerCase();
312
+ return SOURCE_EXTENSIONS.has(extension) && !relativePath.endsWith('.d.ts');
313
+ }
314
+ function isIgnored(relativePath) {
315
+ return isIgnoredDirectoryPath(normalizeRelativePath(relativePath), DEFAULT_IGNORED_DIRECTORIES);
316
+ }
317
+ function collectSourceFiles(projectRoot, absoluteDirectory, maxFiles, issues) {
318
+ const candidates = [];
319
+ const visit = (directoryPath) => {
320
+ if (candidates.length >= maxFiles) {
321
+ return;
322
+ }
323
+ const relativeDirectory = normalizeRelativePath(path.relative(projectRoot, directoryPath));
324
+ if (isIgnored(relativeDirectory)) {
325
+ return;
326
+ }
327
+ let entries;
328
+ try {
329
+ ensureInsideWithoutSymlinks(projectRoot, directoryPath);
330
+ entries = readdirSync(directoryPath, { withFileTypes: true });
331
+ }
332
+ catch (error) {
333
+ pushIssue(issues, `${relativeDirectory}: ${error instanceof Error ? error.message : String(error)}`);
334
+ return;
335
+ }
336
+ for (const entry of entries) {
337
+ if (candidates.length >= maxFiles) {
338
+ return;
339
+ }
340
+ const absoluteEntryPath = path.join(directoryPath, entry.name);
341
+ const relativeEntryPath = normalizeRelativePath(path.relative(projectRoot, absoluteEntryPath));
342
+ if (entry.isDirectory()) {
343
+ visit(absoluteEntryPath);
344
+ continue;
345
+ }
346
+ if (entry.isFile() && isSourceFile(relativeEntryPath)) {
347
+ candidates.push({ absolutePath: absoluteEntryPath, relativePath: relativeEntryPath });
348
+ }
349
+ }
350
+ };
351
+ visit(absoluteDirectory);
352
+ return candidates;
353
+ }
354
+ function countExports(projectRoot, file, maxFileBytes, issues) {
355
+ try {
356
+ const text = readFileInsideWithoutSymlinks(projectRoot, file.absolutePath, { maxBytes: maxFileBytes }).toString('utf8');
357
+ return [...text.matchAll(/\bexport\s+(?:type\s+)?(?:\{|\*|default|class|function|const|let|var|interface|type|enum)\b/gu)].length;
358
+ }
359
+ catch (error) {
360
+ pushIssue(issues, `${file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
361
+ return 0;
362
+ }
363
+ }
364
+ function checkSharedBudgets(projectRoot, config, policy, issues) {
365
+ const metrics = [];
366
+ const findings = [];
367
+ for (const rule of config.sharedBudgets) {
368
+ const absoluteSharedPath = path.resolve(projectRoot, rule.path);
369
+ let files = [];
370
+ try {
371
+ ensureInside(projectRoot, absoluteSharedPath);
372
+ if (existsSync(absoluteSharedPath) && lstatSync(absoluteSharedPath).isDirectory()) {
373
+ files = collectSourceFiles(projectRoot, absoluteSharedPath, policy.max_shared_files, issues);
374
+ }
375
+ }
376
+ catch (error) {
377
+ pushIssue(issues, `${rule.path}: ${error instanceof Error ? error.message : String(error)}`);
378
+ }
379
+ const exportCount = files.reduce((count, file) => count + countExports(projectRoot, file, policy.max_file_bytes, issues), 0);
380
+ const metric = {
381
+ rule_id: rule.id,
382
+ path: rule.path,
383
+ file_count: files.length,
384
+ export_count: exportCount,
385
+ max_files: rule.maxFiles,
386
+ max_exports: rule.maxExports,
387
+ };
388
+ metrics.push(metric);
389
+ const fileExceeded = rule.maxFiles !== null && files.length > rule.maxFiles;
390
+ const exportExceeded = rule.maxExports !== null && exportCount > rule.maxExports;
391
+ if (fileExceeded || exportExceeded) {
392
+ const parts = [
393
+ fileExceeded ? `${files.length} files exceeds max_files ${rule.maxFiles}` : null,
394
+ exportExceeded ? `${exportCount} exports exceeds max_exports ${rule.maxExports}` : null,
395
+ ].filter((entry) => entry !== null);
396
+ findings.push(makeFinding('module_boundary_shared_budget_exceeded', 'medium', rule.path, `Shared budget exceeded for ${rule.path}: ${parts.join('; ')}.`, { rule_id: rule.id }));
397
+ }
398
+ }
399
+ return { metrics, findings };
400
+ }
401
+ function cycleId(paths) {
402
+ return `cycle:${createHash('sha256').update(paths.join('\0')).digest('hex').slice(0, 12)}`;
403
+ }
404
+ function cycleFindings(graph, maxCycles) {
405
+ const cycles = graph.cycles.slice(0, maxCycles).map((cyclePaths) => ({
406
+ cycle_id: cycleId(cyclePaths),
407
+ path_count: Math.max(0, cyclePaths.length - 1),
408
+ paths: cyclePaths,
409
+ }));
410
+ const findings = cycles.map((cycle) => makeFinding('module_boundary_import_cycle_detected', 'high', cycle.paths[0] ?? '.', `Import cycle detected: ${cycle.paths.join(' -> ')}`, { cycle_id: cycle.cycle_id }));
411
+ return { cycles, findings };
412
+ }
413
+ function ruleSummaries(config, findings, cycles) {
414
+ const rules = [];
415
+ const countFor = (ruleIdValue) => findings.filter((finding) => finding.rule_id === ruleIdValue).length;
416
+ for (const rule of config.layers) {
417
+ rules.push({ rule_id: rule.id, kind: 'layer_deny', finding_count: countFor(rule.id) });
418
+ }
419
+ for (const rule of config.publicEntrypoints) {
420
+ rules.push({ rule_id: rule.id, kind: 'public_entrypoint', finding_count: countFor(rule.id) });
421
+ }
422
+ for (const rule of config.featureGroups) {
423
+ rules.push({ rule_id: rule.id, kind: 'feature_direct_import', finding_count: countFor(rule.id) });
424
+ }
425
+ for (const rule of config.sharedBudgets) {
426
+ rules.push({ rule_id: rule.id, kind: 'shared_budget', finding_count: countFor(rule.id) });
427
+ }
428
+ rules.push({ rule_id: 'import-cycle', kind: 'import_cycle', finding_count: cycles.length });
429
+ return rules;
430
+ }
431
+ function moduleBoundaryStatus(findings) {
432
+ if (findings.some((finding) => ERROR_CODES.has(finding.code))) {
433
+ return 'error';
434
+ }
435
+ return findings.some((finding) => !NON_BLOCKING_CODES.has(finding.code)) ? 'failed' : 'passed';
436
+ }
437
+ function createInputHash(policy, config, graph, rules, cycles, sharedMetrics, findings, issues) {
438
+ return sha256Tagged(JSON.stringify({
439
+ policy,
440
+ config,
441
+ graph_input_hash: graph.input_hash,
442
+ rules,
443
+ cycles: cycles.map((cycle) => ({ cycle_id: cycle.cycle_id, paths: cycle.paths })),
444
+ sharedMetrics,
445
+ findings: findings.map((finding) => ({
446
+ code: finding.code,
447
+ path: finding.path,
448
+ rule_id: finding.rule_id,
449
+ cycle_id: finding.cycle_id,
450
+ })),
451
+ issues,
452
+ }));
453
+ }
454
+ export function inspectModuleBoundaries(projectRoot, options) {
455
+ const root = path.resolve(projectRoot);
456
+ const configPath = normalizeRelativePath(options.configPath ?? DEFAULT_CONFIG_PATH);
457
+ const graph = inspectDependencyGraph(root, {
458
+ paths: options.paths,
459
+ maxFiles: options.maxFiles,
460
+ maxFileBytes: options.maxFileBytes,
461
+ maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
462
+ maxNodes: options.maxNodes,
463
+ maxEdges: options.maxEdges,
464
+ });
465
+ const policy = {
466
+ ...graph.policy,
467
+ config_path: configPath,
468
+ max_cycles: options.maxCycles ?? DEFAULT_MAX_CYCLES,
469
+ max_shared_files: options.maxSharedFiles ?? DEFAULT_MAX_SHARED_FILES,
470
+ };
471
+ const config = readModuleBoundaryConfig(root, configPath);
472
+ const issues = [...graph.issues, ...config.issues];
473
+ const boundaryFindings = [
474
+ ...graph.findings.map(normalizeGraphFinding),
475
+ ...config.findings,
476
+ ...checkLayerRules(config, graph.edges),
477
+ ...checkPublicEntrypointRules(config, graph.edges),
478
+ ...checkFeatureGroupRules(config, graph.edges),
479
+ ];
480
+ const shared = checkSharedBudgets(root, config, policy, issues);
481
+ const cycles = cycleFindings(graph, policy.max_cycles);
482
+ const findings = [...boundaryFindings, ...shared.findings, ...cycles.findings];
483
+ const configSummary = {
484
+ path: config.path,
485
+ status: config.status,
486
+ layer_rule_count: config.layers.length,
487
+ public_entrypoint_rule_count: config.publicEntrypoints.length,
488
+ feature_group_rule_count: config.featureGroups.length,
489
+ shared_budget_rule_count: config.sharedBudgets.length,
490
+ };
491
+ const rules = ruleSummaries(config, findings, cycles.cycles);
492
+ const status = moduleBoundaryStatus(findings);
493
+ const truncated = graph.truncated || graph.cycles.length > cycles.cycles.length;
494
+ return {
495
+ schema_version: '1',
496
+ command: 'script-pack',
497
+ pack_id: MODULE_BOUNDARY_PACK_ID,
498
+ script_id: MODULE_BOUNDARY_SCRIPT_ID,
499
+ script_ref: MODULE_BOUNDARY_SCRIPT_REF,
500
+ action: 'check',
501
+ status,
502
+ ok: status === 'passed',
503
+ mustflow_root: root,
504
+ policy,
505
+ input_hash: createInputHash(policy, configSummary, graph, rules, cycles.cycles, shared.metrics, findings, issues),
506
+ config: configSummary,
507
+ targets: graph.targets,
508
+ graph: {
509
+ script_ref: DEPENDENCY_GRAPH_SCRIPT_REF,
510
+ status: graph.status,
511
+ node_count: graph.nodes.length,
512
+ edge_count: graph.edges.length,
513
+ cycle_hint_count: graph.cycles.length,
514
+ truncated: graph.truncated,
515
+ },
516
+ rules,
517
+ cycles: cycles.cycles,
518
+ shared_metrics: shared.metrics,
519
+ truncated,
520
+ findings,
521
+ issues,
522
+ };
523
+ }
@@ -15,6 +15,23 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
15
15
  documented: true,
16
16
  installedCommand: ['mf', 'doctor', '--json'],
17
17
  },
18
+ {
19
+ id: 'check-report',
20
+ schemaFile: 'check-report.schema.json',
21
+ producer: 'mf check --json',
22
+ packaged: true,
23
+ documented: true,
24
+ installedCommand: ['mf', 'check', '--json'],
25
+ expectedExitCodes: [0, 1],
26
+ },
27
+ {
28
+ id: 'status-report',
29
+ schemaFile: 'status-report.schema.json',
30
+ producer: 'mf status --json',
31
+ packaged: true,
32
+ documented: true,
33
+ installedCommand: ['mf', 'status', '--json'],
34
+ },
18
35
  {
19
36
  id: 'context-report',
20
37
  schemaFile: 'context-report.schema.json',
@@ -215,6 +232,22 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
215
232
  documented: true,
216
233
  installedCommand: ['mf', 'script-pack', 'list', '--json'],
217
234
  },
235
+ {
236
+ id: 'index-report',
237
+ schemaFile: 'index-report.schema.json',
238
+ producer: 'mf index --dry-run --json',
239
+ packaged: true,
240
+ documented: true,
241
+ installedCommand: ['mf', 'index', '--dry-run', '--json'],
242
+ },
243
+ {
244
+ id: 'search-report',
245
+ schemaFile: 'search-report.schema.json',
246
+ producer: 'mf search <query> --json',
247
+ packaged: true,
248
+ documented: true,
249
+ installedCommand: ['mf', 'search', 'mustflow', '--json'],
250
+ },
218
251
  {
219
252
  id: 'script-pack-suggestion-report',
220
253
  schemaFile: 'script-pack-suggestion-report.schema.json',
@@ -281,6 +314,23 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
281
314
  ],
282
315
  expectedExitCodes: [0, 1],
283
316
  },
317
+ {
318
+ id: 'module-boundary-report',
319
+ schemaFile: 'module-boundary-report.schema.json',
320
+ producer: 'mf script-pack run code/module-boundary check <path...> --json',
321
+ packaged: true,
322
+ documented: true,
323
+ installedCommand: [
324
+ 'mf',
325
+ 'script-pack',
326
+ 'run',
327
+ 'code/module-boundary',
328
+ 'check',
329
+ 'node_modules/mustflow/dist/cli/index.js',
330
+ '--json',
331
+ ],
332
+ expectedExitCodes: [0, 1],
333
+ },
284
334
  {
285
335
  id: 'change-impact-report',
286
336
  schemaFile: 'change-impact-report.schema.json',
@@ -4,6 +4,7 @@ const CODE_NAVIGATION_SCRIPT_REFS = new Set([
4
4
  'code/outline',
5
5
  'code/dependency-graph',
6
6
  'code/import-cycle',
7
+ 'code/module-boundary',
7
8
  'code/symbol-read',
8
9
  'code/route-outline',
9
10
  'code/export-diff',
@@ -197,6 +198,10 @@ function createRunHint(script, analyzedPaths) {
197
198
  const sourcePaths = pathsWithSurface(analyzedPaths, 'source');
198
199
  return createConcretePathHint('mf script-pack run code/import-cycle check', sourcePaths, script.usage);
199
200
  }
201
+ if (script.ref === 'code/module-boundary') {
202
+ const sourcePaths = pathsWithSurface(analyzedPaths, 'source');
203
+ return createConcretePathHint('mf script-pack run code/module-boundary check', sourcePaths, script.usage);
204
+ }
200
205
  if (script.ref === 'code/change-impact') {
201
206
  return 'mf script-pack run code/change-impact analyze --base HEAD --json';
202
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.103.16",
3
+ "version": "2.103.20",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
package/schemas/README.md CHANGED
@@ -6,6 +6,10 @@ mustflow files and command output.
6
6
  Current schemas:
7
7
 
8
8
  - `doctor-report.schema.json`: output of `mf doctor --json`
9
+ - `check-report.schema.json`: output of `mf check --json`, containing base or strict validation
10
+ issues, warnings, stable issue details, and exit-code aligned `ok` status
11
+ - `status-report.schema.json`: output of `mf status --json`, containing installed-entrypoint
12
+ status, manifest-lock status, changed and missing locked files, issues, and template identity
9
13
  - `adapter-compatibility-report.schema.json`: output of `mf adapters status --json`
10
14
  - `context-report.schema.json`: output of `mf context --json`, including context trust metadata, prompt-cache profiles, and optional cache audit data
11
15
  - `workspace-summary.schema.json`: output of `mf api workspace-summary --json`
@@ -62,6 +66,10 @@ Current schemas:
62
66
  script-pack ids, script refs, action names, usage strings, workflow phases, read-only and
63
67
  side-effect flags, input and output capability labels, related skill names, cost and risk hints,
64
68
  and associated report schemas
69
+ - `index-report.schema.json`: output of `mf index --json`, including dry-run and write status,
70
+ local index counts, search backend metadata, source-index status, and indexed path summaries
71
+ - `search-report.schema.json`: output of `mf search <query> --json`, containing bounded local
72
+ workflow or source search results with authority, cache-layer, navigation-only, and match metadata
65
73
  - `script-pack-suggestion-report.schema.json`: output of `mf script-pack suggest --json`, containing
66
74
  path, skill, phase, and changed-file evidence used to recommend optional script-pack helpers
67
75
  - `code-outline-report.schema.json`: output of
@@ -77,6 +85,10 @@ Current schemas:
77
85
  `mf script-pack run code/import-cycle check <path...> --json`, containing bounded relative
78
86
  TypeScript and JavaScript import cycles with exact cycle paths, import line evidence, policy
79
87
  limits, and stable cycle or input-limit finding codes
88
+ - `module-boundary-report.schema.json`: output of
89
+ `mf script-pack run code/module-boundary check <path...> --json`, containing configured module
90
+ boundary rule findings for layer deny rules, public entrypoints, feature-to-feature imports,
91
+ shared/common budgets, and import cycles
80
92
  - `change-impact-report.schema.json`: output of
81
93
  `mf script-pack run code/change-impact analyze [path...] --json`, containing git-diff changed
82
94
  files, surface classifications, bounded impact candidates, script-pack hints with related