mustflow 2.74.3 → 2.74.4
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/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 +8 -1
- package/schemas/generated-boundary-report.schema.json +148 -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
|
@@ -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',
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function isScriptPackSuggestionPhase(value) {
|
|
4
|
+
return ['before_change', 'during_change', 'after_change', 'review'].includes(value);
|
|
5
|
+
}
|
|
6
|
+
function uniqueSortedStrings(values) {
|
|
7
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
8
|
+
}
|
|
9
|
+
function uniqueSortedPhases(values) {
|
|
10
|
+
return uniqueSortedStrings(values);
|
|
11
|
+
}
|
|
12
|
+
function uniqueSortedSurfaces(values) {
|
|
13
|
+
return uniqueSortedStrings(values);
|
|
14
|
+
}
|
|
15
|
+
function normalizeReportPath(mustflowRoot, value) {
|
|
16
|
+
const absolute = path.resolve(mustflowRoot, value);
|
|
17
|
+
const relative = path.relative(mustflowRoot, absolute);
|
|
18
|
+
return relative.startsWith('..') || path.isAbsolute(relative)
|
|
19
|
+
? value.replace(/\\/gu, '/')
|
|
20
|
+
: (relative.replace(/\\/gu, '/') || '.');
|
|
21
|
+
}
|
|
22
|
+
export function classifyScriptPackPathSurface(relativePath) {
|
|
23
|
+
const normalized = relativePath.replace(/\\/gu, '/').replace(/^\.\/+/u, '');
|
|
24
|
+
const surfaces = [];
|
|
25
|
+
if (normalized === 'REPO_MAP.md' ||
|
|
26
|
+
normalized === '.mustflow/config/manifest.lock.toml' ||
|
|
27
|
+
normalized.startsWith('dist/') ||
|
|
28
|
+
normalized.startsWith('build/') ||
|
|
29
|
+
normalized.startsWith('coverage/') ||
|
|
30
|
+
normalized.startsWith('.mustflow/cache/') ||
|
|
31
|
+
normalized.startsWith('.mustflow/state/')) {
|
|
32
|
+
surfaces.push('generated');
|
|
33
|
+
}
|
|
34
|
+
if (normalized.startsWith('.mustflow/config/') || normalized.startsWith('config/')) {
|
|
35
|
+
surfaces.push('config');
|
|
36
|
+
}
|
|
37
|
+
if (normalized.startsWith('.mustflow/skills/') || normalized.includes('/.mustflow/skills/')) {
|
|
38
|
+
surfaces.push('skill');
|
|
39
|
+
}
|
|
40
|
+
if (normalized.startsWith('templates/')) {
|
|
41
|
+
surfaces.push('template');
|
|
42
|
+
}
|
|
43
|
+
if (normalized.startsWith('schemas/') || normalized.endsWith('.schema.json')) {
|
|
44
|
+
surfaces.push('schema');
|
|
45
|
+
}
|
|
46
|
+
if (normalized === 'README.md' ||
|
|
47
|
+
normalized === 'CHANGELOG.md' ||
|
|
48
|
+
normalized.endsWith('.md') ||
|
|
49
|
+
normalized.startsWith('docs/') ||
|
|
50
|
+
normalized.startsWith('docs-site/')) {
|
|
51
|
+
surfaces.push('docs');
|
|
52
|
+
}
|
|
53
|
+
if (normalized.startsWith('tests/') || normalized.endsWith('.test.js') || normalized.endsWith('.test.ts')) {
|
|
54
|
+
surfaces.push('test');
|
|
55
|
+
}
|
|
56
|
+
if (normalized === 'package.json' ||
|
|
57
|
+
normalized === 'bun.lock' ||
|
|
58
|
+
normalized === 'package-lock.json' ||
|
|
59
|
+
normalized.startsWith('.github/workflows/')) {
|
|
60
|
+
surfaces.push('package');
|
|
61
|
+
}
|
|
62
|
+
if (normalized.startsWith('src/') || normalized.endsWith('.ts') || normalized.endsWith('.tsx')) {
|
|
63
|
+
surfaces.push('source');
|
|
64
|
+
}
|
|
65
|
+
return surfaces.length > 0 ? uniqueSortedSurfaces(surfaces) : ['unknown'];
|
|
66
|
+
}
|
|
67
|
+
function readChangedPaths(mustflowRoot, issues) {
|
|
68
|
+
const result = spawnSync('git', ['status', '--short'], {
|
|
69
|
+
cwd: mustflowRoot,
|
|
70
|
+
encoding: 'utf8',
|
|
71
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
});
|
|
74
|
+
if (result.status !== 0) {
|
|
75
|
+
const detail = result.stderr.trim() || result.stdout.trim() || `git status exited with ${result.status}`;
|
|
76
|
+
issues.push(`Could not read changed paths: ${detail}`);
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
return result.stdout
|
|
80
|
+
.split(/\r?\n/u)
|
|
81
|
+
.map((line) => line.trimEnd())
|
|
82
|
+
.filter((line) => line.length > 0)
|
|
83
|
+
.map((line) => {
|
|
84
|
+
const renamed = /\s->\s(?<target>.+)$/u.exec(line);
|
|
85
|
+
return renamed?.groups?.target ?? line.slice(3).trim();
|
|
86
|
+
})
|
|
87
|
+
.filter((entry) => entry.length > 0);
|
|
88
|
+
}
|
|
89
|
+
function surfacesForScript(script) {
|
|
90
|
+
const surfaces = new Set();
|
|
91
|
+
const searchable = [
|
|
92
|
+
script.ref,
|
|
93
|
+
script.usage,
|
|
94
|
+
...script.useWhen,
|
|
95
|
+
...script.inputs,
|
|
96
|
+
...script.outputs,
|
|
97
|
+
...script.relatedSkills,
|
|
98
|
+
].join(' ');
|
|
99
|
+
const addIf = (surface, pattern) => {
|
|
100
|
+
if (pattern.test(searchable)) {
|
|
101
|
+
surfaces.add(surface);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
addIf('docs', /docs|readme|release|copy|text|prompt/u);
|
|
105
|
+
addIf('schema', /json|schema|contract/u);
|
|
106
|
+
addIf('template', /template|install|manifest/u);
|
|
107
|
+
addIf('skill', /skill|workflow/u);
|
|
108
|
+
addIf('generated', /generated|protected|vendor|cache|boundary/u);
|
|
109
|
+
addIf('config', /config|command/u);
|
|
110
|
+
addIf('package', /package|release/u);
|
|
111
|
+
addIf('source', /code|source|path/u);
|
|
112
|
+
return uniqueSortedSurfaces(surfaces);
|
|
113
|
+
}
|
|
114
|
+
function confidenceForScore(score) {
|
|
115
|
+
if (score >= 7) {
|
|
116
|
+
return 'high';
|
|
117
|
+
}
|
|
118
|
+
if (score >= 4) {
|
|
119
|
+
return 'medium';
|
|
120
|
+
}
|
|
121
|
+
return 'low';
|
|
122
|
+
}
|
|
123
|
+
export function createScriptPackSuggestionReport(mustflowRoot, options) {
|
|
124
|
+
const issues = [];
|
|
125
|
+
const changedPaths = options.changed ? readChangedPaths(mustflowRoot, issues) : [];
|
|
126
|
+
const inputPaths = uniqueSortedStrings([...options.paths, ...changedPaths].map((value) => normalizeReportPath(mustflowRoot, value)));
|
|
127
|
+
const analyzedPaths = inputPaths.map((entry) => ({ path: entry, surfaces: classifyScriptPackPathSurface(entry) }));
|
|
128
|
+
const requestedSurfaces = new Set(analyzedPaths.flatMap((entry) => entry.surfaces));
|
|
129
|
+
const suggestions = options.scripts
|
|
130
|
+
.map((script) => {
|
|
131
|
+
let score = 0;
|
|
132
|
+
const reasons = [];
|
|
133
|
+
const matchedPhases = options.phases.filter((phase) => script.phases.includes(phase));
|
|
134
|
+
if (matchedPhases.length > 0) {
|
|
135
|
+
score += matchedPhases.length * 3;
|
|
136
|
+
reasons.push(`Matches requested phase: ${matchedPhases.join(', ')}`);
|
|
137
|
+
}
|
|
138
|
+
const matchedSkills = options.skills.filter((skill) => script.relatedSkills.includes(skill));
|
|
139
|
+
if (matchedSkills.length > 0) {
|
|
140
|
+
score += matchedSkills.length * 4;
|
|
141
|
+
reasons.push(`Matches related skill: ${matchedSkills.join(', ')}`);
|
|
142
|
+
}
|
|
143
|
+
const scriptSurfaces = surfacesForScript(script);
|
|
144
|
+
const matchedSurfaces = scriptSurfaces.filter((surface) => requestedSurfaces.has(surface));
|
|
145
|
+
if (matchedSurfaces.length > 0) {
|
|
146
|
+
score += matchedSurfaces.length * 2;
|
|
147
|
+
reasons.push(`Matches changed surface: ${matchedSurfaces.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
if (inputPaths.length > 0 && script.inputs.includes('path')) {
|
|
150
|
+
score += 1;
|
|
151
|
+
reasons.push('Accepts explicit path inputs.');
|
|
152
|
+
}
|
|
153
|
+
if (script.readOnly && !script.mutates && !script.network) {
|
|
154
|
+
score += 1;
|
|
155
|
+
reasons.push('Read-only, non-mutating, offline helper.');
|
|
156
|
+
}
|
|
157
|
+
if (score === 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
script_ref: script.ref,
|
|
162
|
+
score,
|
|
163
|
+
confidence: confidenceForScore(score),
|
|
164
|
+
usage: script.usage,
|
|
165
|
+
phases: script.phases,
|
|
166
|
+
matched_phases: uniqueSortedPhases(matchedPhases),
|
|
167
|
+
matched_skills: uniqueSortedStrings(matchedSkills),
|
|
168
|
+
matched_surfaces: uniqueSortedSurfaces(matchedSurfaces),
|
|
169
|
+
reasons,
|
|
170
|
+
read_only: script.readOnly,
|
|
171
|
+
mutates: script.mutates,
|
|
172
|
+
network: script.network,
|
|
173
|
+
risk_level: script.riskLevel,
|
|
174
|
+
cost: script.cost,
|
|
175
|
+
report_schema_file: script.reportSchemaFile,
|
|
176
|
+
run_hint: script.usage,
|
|
177
|
+
};
|
|
178
|
+
})
|
|
179
|
+
.filter((suggestion) => suggestion !== null)
|
|
180
|
+
.sort((left, right) => right.score - left.score || left.script_ref.localeCompare(right.script_ref));
|
|
181
|
+
const status = issues.length > 0 ? 'partial' : suggestions.length > 0 ? 'suggested' : 'empty';
|
|
182
|
+
return {
|
|
183
|
+
schema_version: '1',
|
|
184
|
+
command: 'script-pack',
|
|
185
|
+
action: 'suggest',
|
|
186
|
+
status,
|
|
187
|
+
ok: true,
|
|
188
|
+
mustflow_root: mustflowRoot,
|
|
189
|
+
input: {
|
|
190
|
+
phases: uniqueSortedPhases(options.phases),
|
|
191
|
+
skills: uniqueSortedStrings(options.skills),
|
|
192
|
+
paths: inputPaths,
|
|
193
|
+
changed: options.changed,
|
|
194
|
+
},
|
|
195
|
+
analyzed_paths: analyzedPaths,
|
|
196
|
+
suggestions,
|
|
197
|
+
issues,
|
|
198
|
+
};
|
|
199
|
+
}
|
package/package.json
CHANGED
package/schemas/README.md
CHANGED
|
@@ -57,11 +57,18 @@ Current schemas:
|
|
|
57
57
|
quality-gaming risks such as line stuffing, validation suppressions, test bypass markers, type
|
|
58
58
|
escapes, generated/vendor logic, empty catch swallowing, and placeholder implementations
|
|
59
59
|
- `script-pack-catalog.schema.json`: output of `mf script-pack list --json`, containing bundled
|
|
60
|
-
script-pack ids, script refs, action names, usage strings,
|
|
60
|
+
script-pack ids, script refs, action names, usage strings, workflow phases, read-only and
|
|
61
|
+
side-effect flags, input and output capability labels, related skill names, cost and risk hints,
|
|
62
|
+
and associated report schemas
|
|
63
|
+
- `script-pack-suggestion-report.schema.json`: output of `mf script-pack suggest --json`, containing
|
|
64
|
+
path, skill, phase, and changed-file evidence used to recommend optional script-pack helpers
|
|
61
65
|
- `text-budget-report.schema.json`: output of
|
|
62
66
|
`mf script-pack run core/text-budget check <path...> --json`, containing
|
|
63
67
|
exact text-budget metrics, input content hashes, policy metadata, findings, and JSON Pointer field
|
|
64
68
|
checks for bounded user-facing strings and generated text
|
|
69
|
+
- `generated-boundary-report.schema.json`: output of
|
|
70
|
+
`mf script-pack run repo/generated-boundary check <path...> --json`, containing candidate path
|
|
71
|
+
classifications for generated, ignored, protected, vendor, and cache boundaries before or after edits
|
|
65
72
|
- `skill-route-report.schema.json`: output of `mf skill route --json`, containing compact route
|
|
66
73
|
candidates, selected main and adjunct skills, score breakdowns, route read plans, and source
|
|
67
74
|
route shards without granting command authority or replacing selected `SKILL.md` reads
|