mustflow 2.74.3 → 2.74.5
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/dist/cli/commands/next.js +50 -0
- package/dist/cli/commands/script-pack.js +132 -3
- package/dist/cli/i18n/en.js +30 -3
- package/dist/cli/i18n/es.js +30 -3
- package/dist/cli/i18n/fr.js +30 -3
- package/dist/cli/i18n/hi.js +30 -3
- package/dist/cli/i18n/ko.js +30 -3
- package/dist/cli/i18n/zh.js +30 -3
- package/dist/cli/lib/script-pack-registry.js +53 -0
- package/dist/cli/script-packs/repo-generated-boundary.js +94 -0
- package/dist/core/generated-boundary.js +231 -0
- package/dist/core/public-json-contracts.js +35 -0
- package/dist/core/script-pack-suggestions.js +199 -0
- package/package.json +1 -1
- package/schemas/README.md +10 -2
- package/schemas/generated-boundary-report.schema.json +148 -0
- package/schemas/next-report.schema.json +140 -0
- package/schemas/script-pack-catalog.schema.json +32 -0
- package/schemas/script-pack-suggestion-report.schema.json +159 -0
- package/templates/default/common/.mustflow/config/commands.toml +32 -0
- package/templates/default/i18n.toml +4 -4
- package/templates/default/locales/en/.mustflow/skills/completion-evidence-gate/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/public-json-contract-change/SKILL.md +5 -2
- package/templates/default/locales/en/.mustflow/skills/repo-improvement-loop/SKILL.md +6 -2
- package/templates/default/locales/en/.mustflow/skills/template-install-surface-sync/SKILL.md +6 -3
- package/templates/default/manifest.toml +1 -1
package/dist/cli/i18n/zh.js
CHANGED
|
@@ -40,7 +40,7 @@ export const zhMessages = {
|
|
|
40
40
|
"command.map.summary": "生成 REPO_MAP.md",
|
|
41
41
|
"command.lineEndings.summary": "检查并规范化换行符策略",
|
|
42
42
|
"command.quality.summary": "Inspect changed files for quality-gaming patterns",
|
|
43
|
-
"command.scriptPack.summary": "List and run bundled mustflow script packs",
|
|
43
|
+
"command.scriptPack.summary": "List, suggest, and run bundled mustflow script packs",
|
|
44
44
|
"command.run.summary": "运行已配置的一次性命令",
|
|
45
45
|
"command.context.summary": "输出机器可读的代理上下文",
|
|
46
46
|
"command.tech.summary": "管理代理使用的技术偏好",
|
|
@@ -749,19 +749,33 @@ export const zhMessages = {
|
|
|
749
749
|
"quality.clean": "No quality-gaming risks found.",
|
|
750
750
|
"quality.error.missingAction": "Specify a quality action: check",
|
|
751
751
|
"quality.error.unknownAction": "Unknown quality action: {action}",
|
|
752
|
-
"scriptPack.help.summary": "List and run bundled mustflow script-pack utilities through one stable command namespace.",
|
|
752
|
+
"scriptPack.help.summary": "List, suggest, and run bundled mustflow script-pack utilities through one stable command namespace.",
|
|
753
753
|
"scriptPack.help.exit.ok": "The script-pack command completed successfully",
|
|
754
754
|
"scriptPack.help.exit.fail": "The script-pack command received invalid input or the selected script failed",
|
|
755
|
+
"scriptPack.help.option.changed": "Suggest scripts for current Git working-tree changes",
|
|
756
|
+
"scriptPack.help.option.phase": "Limit suggestions to before_change, during_change, after_change, or review",
|
|
757
|
+
"scriptPack.help.option.skill": "Prefer scripts related to a mustflow skill name",
|
|
758
|
+
"scriptPack.help.option.path": "Add a candidate path to classify for script suggestions",
|
|
755
759
|
"scriptPack.title": "mustflow script packs",
|
|
760
|
+
"scriptPack.suggest.title": "mustflow script suggestions",
|
|
761
|
+
"scriptPack.suggest.label.suggestions": "Suggestions",
|
|
762
|
+
"scriptPack.suggest.label.analyzedPaths": "Analyzed paths",
|
|
763
|
+
"scriptPack.suggest.label.recommendations": "Recommendations",
|
|
764
|
+
"scriptPack.suggest.label.runHint": "Run hint",
|
|
765
|
+
"scriptPack.suggest.empty": "No script-pack suggestions matched the supplied paths, skills, or phases.",
|
|
756
766
|
"scriptPack.pack.core.summary": "Core built-in utility scripts",
|
|
767
|
+
"scriptPack.pack.repo.summary": "Repository-boundary utility scripts",
|
|
757
768
|
"scriptPack.script.textBudget.summary": "Check exact text length budgets for files or JSON string fields",
|
|
769
|
+
"scriptPack.script.generatedBoundary.summary": "Check whether candidate paths cross generated, ignored, protected, vendor, or cache boundaries",
|
|
758
770
|
"scriptPack.label.script": "Script",
|
|
759
771
|
"scriptPack.label.actions": "actions",
|
|
760
772
|
"scriptPack.label.schema": "schema",
|
|
761
|
-
"scriptPack.error.missingAction": "Specify a script-pack action: list or run",
|
|
773
|
+
"scriptPack.error.missingAction": "Specify a script-pack action: list, suggest, or run",
|
|
762
774
|
"scriptPack.error.unknownAction": "Unknown script-pack action: {action}",
|
|
763
775
|
"scriptPack.error.missingScript": "Specify a script ref such as core/text-budget",
|
|
764
776
|
"scriptPack.error.unknownScript": "Unknown script-pack script: {script}",
|
|
777
|
+
"scriptPack.error.missingSuggestInput": "Provide at least one suggestion input: --path <path>, --changed, --phase <phase>, or --skill <skill>",
|
|
778
|
+
"scriptPack.error.unknownPhase": "Unknown script-pack phase: {phase}",
|
|
765
779
|
"textBudget.help.summary": "Check exact text length budgets for files or JSON string fields using grapheme counts by default.",
|
|
766
780
|
"textBudget.help.option.min": "Require at least this many units",
|
|
767
781
|
"textBudget.help.option.max": "Require at most this many units",
|
|
@@ -785,6 +799,19 @@ export const zhMessages = {
|
|
|
785
799
|
"textBudget.error.invalidNumber": "{option} must be a non-negative safe integer: {value}",
|
|
786
800
|
"textBudget.error.invalidUnit": "Unknown text-budget unit: {unit}. Use one of: {allowed}",
|
|
787
801
|
"textBudget.error.minGreaterThanMax": "--min must be less than or equal to --max",
|
|
802
|
+
"generatedBoundary.help.summary": "Check whether candidate edit paths cross generated, ignored, protected, vendor, or cache boundaries.",
|
|
803
|
+
"generatedBoundary.help.exit.ok": "Every checked target stayed within ordinary editable project paths",
|
|
804
|
+
"generatedBoundary.help.exit.fail": "A target crossed a generated, ignored, protected, vendor, cache, or root boundary",
|
|
805
|
+
"generatedBoundary.title": "mustflow generated boundary",
|
|
806
|
+
"generatedBoundary.label.checkedTargets": "Checked targets",
|
|
807
|
+
"generatedBoundary.label.findings": "Findings",
|
|
808
|
+
"generatedBoundary.label.targets": "Targets",
|
|
809
|
+
"generatedBoundary.label.boundaries": "boundaries",
|
|
810
|
+
"generatedBoundary.label.issues": "Issues",
|
|
811
|
+
"generatedBoundary.clean": "No generated, ignored, protected, vendor, or cache boundaries matched.",
|
|
812
|
+
"generatedBoundary.error.missingAction": "Specify a generated-boundary action: check",
|
|
813
|
+
"generatedBoundary.error.unknownAction": "Unknown generated-boundary action: {action}",
|
|
814
|
+
"generatedBoundary.error.missingPath": "Provide at least one path to check",
|
|
788
815
|
"run.help.summary": "从 .mustflow/config/commands.toml 运行已配置的一次性命令。",
|
|
789
816
|
"run.help.option.dryRun": "输出命令计划但不执行",
|
|
790
817
|
"run.help.option.planOnly": "--dry-run 的别名",
|
|
@@ -13,11 +13,64 @@ export const SCRIPT_PACKS = [
|
|
|
13
13
|
usage: 'mf script-pack run core/text-budget check <path...> [options]',
|
|
14
14
|
summaryKey: 'scriptPack.script.textBudget.summary',
|
|
15
15
|
actions: ['check'],
|
|
16
|
+
useWhen: [
|
|
17
|
+
'Check exact text budgets for docs, package metadata, prompts, release notes, or user-facing copy.',
|
|
18
|
+
'Inspect a JSON string field by JSON Pointer when a public description, label, or summary has a length contract.',
|
|
19
|
+
],
|
|
20
|
+
phases: ['before_change', 'after_change', 'review'],
|
|
21
|
+
readOnly: true,
|
|
22
|
+
mutates: false,
|
|
23
|
+
network: false,
|
|
24
|
+
inputs: ['path', 'json_pointer', 'budget', 'unit'],
|
|
25
|
+
outputs: ['human_summary', 'json_report'],
|
|
26
|
+
relatedSkills: [
|
|
27
|
+
'docs-prose-review',
|
|
28
|
+
'public-json-contract-change',
|
|
29
|
+
'readme-authoring',
|
|
30
|
+
'release-notes-authoring',
|
|
31
|
+
],
|
|
32
|
+
riskLevel: 'low',
|
|
33
|
+
cost: 'low',
|
|
16
34
|
reportSchemaFile: 'text-budget-report.schema.json',
|
|
17
35
|
loadRunner: async () => (await import('../script-packs/core-text-budget.js')).runCoreTextBudgetScript,
|
|
18
36
|
},
|
|
19
37
|
],
|
|
20
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: 'repo',
|
|
41
|
+
summaryKey: 'scriptPack.pack.repo.summary',
|
|
42
|
+
scripts: [
|
|
43
|
+
{
|
|
44
|
+
packId: 'repo',
|
|
45
|
+
id: 'generated-boundary',
|
|
46
|
+
ref: scriptRef('repo', 'generated-boundary'),
|
|
47
|
+
usage: 'mf script-pack run repo/generated-boundary check <path...> [options]',
|
|
48
|
+
summaryKey: 'scriptPack.script.generatedBoundary.summary',
|
|
49
|
+
actions: ['check'],
|
|
50
|
+
useWhen: [
|
|
51
|
+
'Check candidate edit paths before changing files that may be generated, ignored, protected, vendor, or cache output.',
|
|
52
|
+
'Review changed paths after implementation when generated or protected-file drift would make completion evidence misleading.',
|
|
53
|
+
],
|
|
54
|
+
phases: ['before_change', 'after_change', 'review'],
|
|
55
|
+
readOnly: true,
|
|
56
|
+
mutates: false,
|
|
57
|
+
network: false,
|
|
58
|
+
inputs: ['path'],
|
|
59
|
+
outputs: ['human_summary', 'json_report'],
|
|
60
|
+
relatedSkills: [
|
|
61
|
+
'completion-evidence-gate',
|
|
62
|
+
'proactive-risk-surfacing',
|
|
63
|
+
'quality-gaming-guard',
|
|
64
|
+
'repo-improvement-loop',
|
|
65
|
+
'template-install-surface-sync',
|
|
66
|
+
],
|
|
67
|
+
riskLevel: 'low',
|
|
68
|
+
cost: 'low',
|
|
69
|
+
reportSchemaFile: 'generated-boundary-report.schema.json',
|
|
70
|
+
loadRunner: async () => (await import('../script-packs/repo-generated-boundary.js')).runRepoGeneratedBoundaryScript,
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
21
74
|
];
|
|
22
75
|
export function listScriptPackScripts() {
|
|
23
76
|
return SCRIPT_PACKS.flatMap((pack) => pack.scripts);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { printUsageError, renderHelp } from '../lib/cli-output.js';
|
|
2
|
+
import { t } from '../lib/i18n.js';
|
|
3
|
+
import { formatCliOptionParseError, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
|
|
4
|
+
import { resolveMustflowRoot } from '../lib/project-root.js';
|
|
5
|
+
import { GENERATED_BOUNDARY_SCRIPT_REF, inspectGeneratedBoundary, } from '../../core/generated-boundary.js';
|
|
6
|
+
const GENERATED_BOUNDARY_OPTIONS = [{ name: '--json', kind: 'boolean' }];
|
|
7
|
+
export function getRepoGeneratedBoundaryHelp(lang = 'en') {
|
|
8
|
+
return renderHelp({
|
|
9
|
+
usage: 'mf script-pack run repo/generated-boundary check <path...> [options]',
|
|
10
|
+
summary: t(lang, 'generatedBoundary.help.summary'),
|
|
11
|
+
options: [
|
|
12
|
+
{ label: '--json', description: t(lang, 'cli.option.json') },
|
|
13
|
+
{ label: '-h, --help', description: t(lang, 'cli.option.help') },
|
|
14
|
+
],
|
|
15
|
+
examples: [
|
|
16
|
+
'mf script-pack run repo/generated-boundary check src/cli/index.ts',
|
|
17
|
+
'mf script-pack run repo/generated-boundary check dist/cli/index.js --json',
|
|
18
|
+
'mf script-pack run repo/generated-boundary check AGENTS.md .mustflow/config/manifest.lock.toml',
|
|
19
|
+
],
|
|
20
|
+
exitCodes: [
|
|
21
|
+
{ label: '0', description: t(lang, 'generatedBoundary.help.exit.ok') },
|
|
22
|
+
{ label: '1', description: t(lang, 'generatedBoundary.help.exit.fail') },
|
|
23
|
+
],
|
|
24
|
+
}, lang);
|
|
25
|
+
}
|
|
26
|
+
function parseGeneratedBoundaryOptions(args, lang) {
|
|
27
|
+
const [action, ...rest] = args;
|
|
28
|
+
const parsed = parseCliOptions(rest, GENERATED_BOUNDARY_OPTIONS, { allowPositionals: true });
|
|
29
|
+
const json = hasParsedCliOption(parsed, '--json');
|
|
30
|
+
if (action !== 'check') {
|
|
31
|
+
return {
|
|
32
|
+
action: 'check',
|
|
33
|
+
json,
|
|
34
|
+
paths: parsed.positionals,
|
|
35
|
+
error: action
|
|
36
|
+
? t(lang, 'generatedBoundary.error.unknownAction', { action })
|
|
37
|
+
: t(lang, 'generatedBoundary.error.missingAction'),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (parsed.error) {
|
|
41
|
+
return { action, json, paths: parsed.positionals, error: formatCliOptionParseError(parsed.error, lang) };
|
|
42
|
+
}
|
|
43
|
+
if (parsed.positionals.length === 0) {
|
|
44
|
+
return { action, json, paths: parsed.positionals, error: t(lang, 'generatedBoundary.error.missingPath') };
|
|
45
|
+
}
|
|
46
|
+
return { action, json, paths: parsed.positionals };
|
|
47
|
+
}
|
|
48
|
+
function renderGeneratedBoundarySummary(report, lang) {
|
|
49
|
+
const lines = [
|
|
50
|
+
t(lang, 'generatedBoundary.title'),
|
|
51
|
+
`${t(lang, 'scriptPack.label.script')}: ${GENERATED_BOUNDARY_SCRIPT_REF}`,
|
|
52
|
+
`${t(lang, 'label.status')}: ${report.status}`,
|
|
53
|
+
`${t(lang, 'generatedBoundary.label.checkedTargets')}: ${report.targets.length}`,
|
|
54
|
+
`${t(lang, 'generatedBoundary.label.findings')}: ${report.findings.length}`,
|
|
55
|
+
];
|
|
56
|
+
if (report.targets.length > 0) {
|
|
57
|
+
lines.push(t(lang, 'generatedBoundary.label.targets'));
|
|
58
|
+
for (const target of report.targets) {
|
|
59
|
+
const boundaries = target.matched_boundaries.length === 0 ? t(lang, 'value.none') : target.matched_boundaries.join(', ');
|
|
60
|
+
lines.push(`- ${target.path}: ${target.kind}, ${t(lang, 'generatedBoundary.label.boundaries')}: ${boundaries}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (report.findings.length > 0) {
|
|
64
|
+
lines.push(t(lang, 'generatedBoundary.label.findings'));
|
|
65
|
+
for (const finding of report.findings) {
|
|
66
|
+
lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (report.issues.length > 0) {
|
|
70
|
+
lines.push(t(lang, 'generatedBoundary.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
|
|
71
|
+
}
|
|
72
|
+
if (report.findings.length === 0 && report.issues.length === 0) {
|
|
73
|
+
lines.push(t(lang, 'generatedBoundary.clean'));
|
|
74
|
+
}
|
|
75
|
+
return lines.join('\n');
|
|
76
|
+
}
|
|
77
|
+
export function runRepoGeneratedBoundaryScript(args, reporter, lang = 'en') {
|
|
78
|
+
if (hasCliOptionToken(args, '--help', ['-h'])) {
|
|
79
|
+
reporter.stdout(getRepoGeneratedBoundaryHelp(lang));
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
const options = parseGeneratedBoundaryOptions(args, lang);
|
|
83
|
+
if (options.error) {
|
|
84
|
+
printUsageError(reporter, options.error, 'mf script-pack run repo/generated-boundary --help', getRepoGeneratedBoundaryHelp(lang), lang);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
const report = inspectGeneratedBoundary(resolveMustflowRoot(), { paths: options.paths });
|
|
88
|
+
if (options.json) {
|
|
89
|
+
reporter.stdout(JSON.stringify(report, null, 2));
|
|
90
|
+
return report.ok ? 0 : 1;
|
|
91
|
+
}
|
|
92
|
+
reporter.stdout(renderGeneratedBoundarySummary(report, lang));
|
|
93
|
+
return report.ok ? 0 : 1;
|
|
94
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, lstatSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isRecord, readMustflowConfigIfExists, readStringArray } from './config-loading.js';
|
|
5
|
+
import { ensureInside } from './safe-filesystem.js';
|
|
6
|
+
export const GENERATED_BOUNDARY_PACK_ID = 'repo';
|
|
7
|
+
export const GENERATED_BOUNDARY_SCRIPT_ID = 'generated-boundary';
|
|
8
|
+
export const GENERATED_BOUNDARY_SCRIPT_REF = `${GENERATED_BOUNDARY_PACK_ID}/${GENERATED_BOUNDARY_SCRIPT_ID}`;
|
|
9
|
+
const BUILTIN_GENERATED_PATTERNS = [
|
|
10
|
+
'REPO_MAP.md',
|
|
11
|
+
'.mustflow/config/manifest.lock.toml',
|
|
12
|
+
'.mustflow/state/**',
|
|
13
|
+
'.mustflow/cache/**',
|
|
14
|
+
'dist/**',
|
|
15
|
+
'build/**',
|
|
16
|
+
'coverage/**',
|
|
17
|
+
'.next/**',
|
|
18
|
+
'.turbo/**',
|
|
19
|
+
];
|
|
20
|
+
const BUILTIN_VENDOR_PATTERNS = ['node_modules/**', 'vendor/**', 'third_party/**'];
|
|
21
|
+
const BUILTIN_CACHE_PATTERNS = ['.cache/**', '.parcel-cache/**', '.pytest_cache/**'];
|
|
22
|
+
const BUILTIN_PROTECTED_PATTERNS = ['.git/**', '**/*.pem', '**/*.key', '**/.env'];
|
|
23
|
+
const CATEGORY_FINDING = {
|
|
24
|
+
generated: { code: 'generated_boundary_generated_path', severity: 'medium' },
|
|
25
|
+
ignored: { code: 'generated_boundary_ignored_path', severity: 'medium' },
|
|
26
|
+
protected: { code: 'generated_boundary_protected_path', severity: 'critical' },
|
|
27
|
+
vendor: { code: 'generated_boundary_vendor_path', severity: 'high' },
|
|
28
|
+
cache: { code: 'generated_boundary_cache_path', severity: 'medium' },
|
|
29
|
+
outside_root: { code: 'generated_boundary_path_outside_root', severity: 'high' },
|
|
30
|
+
};
|
|
31
|
+
function toPosixPath(value) {
|
|
32
|
+
return value.replace(/\\/gu, '/');
|
|
33
|
+
}
|
|
34
|
+
function normalizeRelativePath(value) {
|
|
35
|
+
return toPosixPath(value).replace(/^\.\/+/u, '').replace(/\/+$/u, '') || '.';
|
|
36
|
+
}
|
|
37
|
+
function uniqueSorted(values) {
|
|
38
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
39
|
+
}
|
|
40
|
+
function escapeRegExp(value) {
|
|
41
|
+
return value.replace(/[\\^$+?.()|[\]{}]/gu, '\\$&');
|
|
42
|
+
}
|
|
43
|
+
function globToRegExp(pattern) {
|
|
44
|
+
const normalized = normalizeRelativePath(pattern).replace(/^\/+/u, '');
|
|
45
|
+
let source = '^';
|
|
46
|
+
for (let index = 0; index < normalized.length;) {
|
|
47
|
+
if (normalized.startsWith('**', index)) {
|
|
48
|
+
source += '.*';
|
|
49
|
+
index += 2;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const character = normalized[index];
|
|
53
|
+
if (character === '*') {
|
|
54
|
+
source += '[^/]*';
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (character === '?') {
|
|
59
|
+
source += '[^/]';
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
source += escapeRegExp(character ?? '');
|
|
64
|
+
index += 1;
|
|
65
|
+
}
|
|
66
|
+
return new RegExp(`${source}$`, 'u');
|
|
67
|
+
}
|
|
68
|
+
function patternMatches(pattern, relativePath) {
|
|
69
|
+
const normalizedPattern = normalizeRelativePath(pattern);
|
|
70
|
+
const normalizedPath = normalizeRelativePath(relativePath);
|
|
71
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
72
|
+
const base = normalizedPattern.slice(0, -3);
|
|
73
|
+
if (normalizedPath === base || normalizedPath.startsWith(`${base}/`)) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (globToRegExp(normalizedPattern).test(normalizedPath)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return normalizedPattern.startsWith('**/') && globToRegExp(normalizedPattern.slice(3)).test(normalizedPath);
|
|
81
|
+
}
|
|
82
|
+
function readPatternTable(config, tableName, key) {
|
|
83
|
+
const table = config?.[tableName];
|
|
84
|
+
return isRecord(table) ? (readStringArray(table, key) ?? []) : [];
|
|
85
|
+
}
|
|
86
|
+
function createPolicy(projectRoot, issues) {
|
|
87
|
+
let config;
|
|
88
|
+
try {
|
|
89
|
+
config = readMustflowConfigIfExists(projectRoot);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
93
|
+
issues.push(`Could not read .mustflow/config/mustflow.toml: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
config_path: '.mustflow/config/mustflow.toml',
|
|
97
|
+
config_loaded: config !== undefined,
|
|
98
|
+
generated_patterns: readPatternTable(config, 'document_roots', 'generated'),
|
|
99
|
+
ignored_patterns: readPatternTable(config, 'document_roots', 'ignored'),
|
|
100
|
+
protected_patterns: readPatternTable(config, 'edit_policy', 'protected'),
|
|
101
|
+
builtin_generated_patterns: [...BUILTIN_GENERATED_PATTERNS],
|
|
102
|
+
builtin_vendor_patterns: [...BUILTIN_VENDOR_PATTERNS],
|
|
103
|
+
builtin_cache_patterns: [...BUILTIN_CACHE_PATTERNS],
|
|
104
|
+
builtin_protected_patterns: [...BUILTIN_PROTECTED_PATTERNS],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function createBoundaryPatterns(policy) {
|
|
108
|
+
return [
|
|
109
|
+
...policy.generated_patterns.map((pattern) => ({ category: 'generated', pattern, source: 'mustflow_config' })),
|
|
110
|
+
...policy.ignored_patterns.map((pattern) => ({ category: 'ignored', pattern, source: 'mustflow_config' })),
|
|
111
|
+
...policy.protected_patterns.map((pattern) => ({ category: 'protected', pattern, source: 'mustflow_config' })),
|
|
112
|
+
...policy.builtin_generated_patterns.map((pattern) => ({ category: 'generated', pattern, source: 'builtin' })),
|
|
113
|
+
...policy.builtin_vendor_patterns.map((pattern) => ({ category: 'vendor', pattern, source: 'builtin' })),
|
|
114
|
+
...policy.builtin_cache_patterns.map((pattern) => ({ category: 'cache', pattern, source: 'builtin' })),
|
|
115
|
+
...policy.builtin_protected_patterns.map((pattern) => ({ category: 'protected', pattern, source: 'builtin' })),
|
|
116
|
+
];
|
|
117
|
+
}
|
|
118
|
+
function targetKind(absolutePath) {
|
|
119
|
+
if (!existsSync(absolutePath)) {
|
|
120
|
+
return { exists: false, kind: 'missing' };
|
|
121
|
+
}
|
|
122
|
+
const stats = lstatSync(absolutePath);
|
|
123
|
+
if (stats.isFile()) {
|
|
124
|
+
return { exists: true, kind: 'file' };
|
|
125
|
+
}
|
|
126
|
+
if (stats.isDirectory()) {
|
|
127
|
+
return { exists: true, kind: 'directory' };
|
|
128
|
+
}
|
|
129
|
+
return { exists: true, kind: 'other' };
|
|
130
|
+
}
|
|
131
|
+
function makeFinding(code, severity, targetPath, boundary, message, pattern, source) {
|
|
132
|
+
return { code, severity, message, path: targetPath, boundary, pattern, source };
|
|
133
|
+
}
|
|
134
|
+
function createInputHash(policy, targets, issues) {
|
|
135
|
+
const inputState = {
|
|
136
|
+
policy,
|
|
137
|
+
targets: targets.map((target) => ({
|
|
138
|
+
input: target.input,
|
|
139
|
+
path: target.path,
|
|
140
|
+
exists: target.exists,
|
|
141
|
+
kind: target.kind,
|
|
142
|
+
matched_patterns: target.matched_patterns,
|
|
143
|
+
})),
|
|
144
|
+
issues,
|
|
145
|
+
};
|
|
146
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(inputState)).digest('hex')}`;
|
|
147
|
+
}
|
|
148
|
+
export function inspectGeneratedBoundary(projectRoot, options) {
|
|
149
|
+
const root = path.resolve(projectRoot);
|
|
150
|
+
const issues = [];
|
|
151
|
+
const policy = createPolicy(root, issues);
|
|
152
|
+
const patterns = createBoundaryPatterns(policy);
|
|
153
|
+
const targets = [];
|
|
154
|
+
const findings = [];
|
|
155
|
+
for (const targetPath of options.paths) {
|
|
156
|
+
let relativePath = targetPath;
|
|
157
|
+
let absolutePath;
|
|
158
|
+
try {
|
|
159
|
+
absolutePath = path.resolve(process.cwd(), targetPath);
|
|
160
|
+
ensureInside(root, absolutePath);
|
|
161
|
+
relativePath = normalizeRelativePath(path.relative(root, absolutePath));
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
165
|
+
issues.push(message);
|
|
166
|
+
targets.push({
|
|
167
|
+
input: targetPath,
|
|
168
|
+
path: targetPath,
|
|
169
|
+
exists: null,
|
|
170
|
+
kind: 'unknown',
|
|
171
|
+
matched_boundaries: ['outside_root'],
|
|
172
|
+
matched_patterns: [],
|
|
173
|
+
});
|
|
174
|
+
findings.push(makeFinding('generated_boundary_path_outside_root', 'high', targetPath, 'outside_root', message, null, null));
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
let existence;
|
|
178
|
+
try {
|
|
179
|
+
existence = targetKind(absolutePath);
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
issues.push(`${relativePath}: ${message}`);
|
|
184
|
+
targets.push({
|
|
185
|
+
input: targetPath,
|
|
186
|
+
path: relativePath,
|
|
187
|
+
exists: null,
|
|
188
|
+
kind: 'unknown',
|
|
189
|
+
matched_boundaries: [],
|
|
190
|
+
matched_patterns: [],
|
|
191
|
+
});
|
|
192
|
+
findings.push(makeFinding('generated_boundary_unreadable_path', 'high', relativePath, 'protected', message, null, null));
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const matchedPatterns = patterns.filter((candidate) => patternMatches(candidate.pattern, relativePath));
|
|
196
|
+
const matchedBoundaries = uniqueSorted(matchedPatterns.map((candidate) => candidate.category));
|
|
197
|
+
targets.push({
|
|
198
|
+
input: targetPath,
|
|
199
|
+
path: relativePath,
|
|
200
|
+
exists: existence.exists,
|
|
201
|
+
kind: existence.kind,
|
|
202
|
+
matched_boundaries: matchedBoundaries,
|
|
203
|
+
matched_patterns: matchedPatterns,
|
|
204
|
+
});
|
|
205
|
+
for (const match of matchedPatterns) {
|
|
206
|
+
const finding = CATEGORY_FINDING[match.category];
|
|
207
|
+
findings.push(makeFinding(finding.code, finding.severity, relativePath, match.category, `${relativePath} matches ${match.category} boundary pattern ${match.pattern}.`, match.pattern, match.source));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const status = findings.some((finding) => ['generated_boundary_path_outside_root', 'generated_boundary_unreadable_path'].includes(finding.code))
|
|
211
|
+
? 'error'
|
|
212
|
+
: findings.length > 0 || issues.length > 0
|
|
213
|
+
? 'failed'
|
|
214
|
+
: 'passed';
|
|
215
|
+
return {
|
|
216
|
+
schema_version: '1',
|
|
217
|
+
command: 'script-pack',
|
|
218
|
+
pack_id: GENERATED_BOUNDARY_PACK_ID,
|
|
219
|
+
script_id: GENERATED_BOUNDARY_SCRIPT_ID,
|
|
220
|
+
script_ref: GENERATED_BOUNDARY_SCRIPT_REF,
|
|
221
|
+
action: 'check',
|
|
222
|
+
status,
|
|
223
|
+
ok: status === 'passed',
|
|
224
|
+
mustflow_root: root,
|
|
225
|
+
policy,
|
|
226
|
+
input_hash: createInputHash(policy, targets, issues),
|
|
227
|
+
targets,
|
|
228
|
+
findings,
|
|
229
|
+
issues,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -215,6 +215,23 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
215
215
|
documented: true,
|
|
216
216
|
installedCommand: ['mf', 'script-pack', 'list', '--json'],
|
|
217
217
|
},
|
|
218
|
+
{
|
|
219
|
+
id: 'script-pack-suggestion-report',
|
|
220
|
+
schemaFile: 'script-pack-suggestion-report.schema.json',
|
|
221
|
+
producer: 'mf script-pack suggest --json',
|
|
222
|
+
packaged: true,
|
|
223
|
+
documented: true,
|
|
224
|
+
installedCommand: [
|
|
225
|
+
'mf',
|
|
226
|
+
'script-pack',
|
|
227
|
+
'suggest',
|
|
228
|
+
'--path',
|
|
229
|
+
'AGENTS.md',
|
|
230
|
+
'--phase',
|
|
231
|
+
'before_change',
|
|
232
|
+
'--json',
|
|
233
|
+
],
|
|
234
|
+
},
|
|
218
235
|
{
|
|
219
236
|
id: 'text-budget-report',
|
|
220
237
|
schemaFile: 'text-budget-report.schema.json',
|
|
@@ -234,6 +251,24 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
234
251
|
],
|
|
235
252
|
expectedExitCodes: [0, 1],
|
|
236
253
|
},
|
|
254
|
+
{
|
|
255
|
+
id: 'generated-boundary-report',
|
|
256
|
+
schemaFile: 'generated-boundary-report.schema.json',
|
|
257
|
+
producer: 'mf script-pack run repo/generated-boundary check <path...> --json',
|
|
258
|
+
packaged: true,
|
|
259
|
+
documented: true,
|
|
260
|
+
installedCommand: [
|
|
261
|
+
'mf',
|
|
262
|
+
'script-pack',
|
|
263
|
+
'run',
|
|
264
|
+
'repo/generated-boundary',
|
|
265
|
+
'check',
|
|
266
|
+
'AGENTS.md',
|
|
267
|
+
'.mustflow/config/manifest.lock.toml',
|
|
268
|
+
'--json',
|
|
269
|
+
],
|
|
270
|
+
expectedExitCodes: [0, 1],
|
|
271
|
+
},
|
|
237
272
|
{
|
|
238
273
|
id: 'skill-route-report',
|
|
239
274
|
schemaFile: 'skill-route-report.schema.json',
|