mustflow 2.85.4 → 2.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/script-pack.js +10 -0
- package/dist/cli/i18n/en.js +183 -0
- package/dist/cli/i18n/es.js +183 -0
- package/dist/cli/i18n/fr.js +183 -0
- package/dist/cli/i18n/hi.js +183 -0
- package/dist/cli/i18n/ko.js +183 -0
- package/dist/cli/i18n/zh.js +183 -0
- package/dist/cli/lib/script-pack-registry.js +284 -1
- package/dist/cli/script-packs/code-change-impact.js +6 -0
- package/dist/cli/script-packs/code-import-cycle.js +193 -0
- package/dist/cli/script-packs/docs-link-integrity.js +145 -0
- package/dist/cli/script-packs/repo-approval-gate.js +100 -0
- package/dist/cli/script-packs/repo-git-ignore-audit.js +119 -0
- package/dist/cli/script-packs/repo-manifest-lock-drift.js +122 -0
- package/dist/cli/script-packs/repo-merge-conflict-scan.js +123 -0
- package/dist/cli/script-packs/repo-skill-route-audit.js +86 -0
- package/dist/cli/script-packs/repo-version-source.js +92 -0
- package/dist/cli/script-packs/test-performance-report.js +247 -0
- package/dist/cli/script-packs/test-regression-selector.js +167 -0
- package/dist/core/change-impact.js +23 -51
- package/dist/core/change-surface-classification.js +198 -0
- package/dist/core/docs-link-integrity.js +443 -0
- package/dist/core/import-cycle.js +152 -0
- package/dist/core/public-json-contracts.js +116 -0
- package/dist/core/repo-approval-gate.js +116 -0
- package/dist/core/repo-git-ignore-audit.js +302 -0
- package/dist/core/repo-manifest-lock-drift.js +321 -0
- package/dist/core/repo-merge-conflict-scan.js +335 -0
- package/dist/core/repo-version-source.js +82 -0
- package/dist/core/script-pack-suggestions.js +77 -1
- package/dist/core/skill-route-audit.js +354 -0
- package/dist/core/test-performance-report.js +697 -0
- package/dist/core/test-regression-selector.js +335 -0
- package/package.json +1 -1
- package/schemas/README.md +40 -2
- package/schemas/change-impact-report.schema.json +35 -1
- package/schemas/import-cycle-report.schema.json +157 -0
- package/schemas/link-integrity-report.schema.json +176 -0
- package/schemas/repo-approval-gate-report.schema.json +115 -0
- package/schemas/repo-git-ignore-audit-report.schema.json +201 -0
- package/schemas/repo-manifest-lock-drift-report.schema.json +202 -0
- package/schemas/repo-merge-conflict-scan-report.schema.json +169 -0
- package/schemas/repo-version-source-report.schema.json +127 -0
- package/schemas/skill-route-audit-report.schema.json +144 -0
- package/schemas/test-performance-report.schema.json +319 -0
- package/schemas/test-regression-selector-report.schema.json +187 -0
- package/templates/default/i18n.toml +66 -18
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +45 -8
- package/templates/default/locales/en/.mustflow/skills/api-access-control-review/SKILL.md +48 -27
- package/templates/default/locales/en/.mustflow/skills/api-failure-triage/SKILL.md +270 -0
- package/templates/default/locales/en/.mustflow/skills/auth-flow-triage/SKILL.md +192 -0
- package/templates/default/locales/en/.mustflow/skills/auth-permission-change/SKILL.md +59 -13
- package/templates/default/locales/en/.mustflow/skills/backend-log-evidence-review/SKILL.md +14 -5
- package/templates/default/locales/en/.mustflow/skills/cache-integrity-review/SKILL.md +30 -15
- package/templates/default/locales/en/.mustflow/skills/change-blast-radius-review/SKILL.md +45 -32
- package/templates/default/locales/en/.mustflow/skills/ci-pipeline-triage/SKILL.md +200 -0
- package/templates/default/locales/en/.mustflow/skills/clarifying-question-gate/SKILL.md +87 -13
- package/templates/default/locales/en/.mustflow/skills/docker-runtime-triage/SKILL.md +191 -0
- package/templates/default/locales/en/.mustflow/skills/go-code-change/SKILL.md +18 -13
- package/templates/default/locales/en/.mustflow/skills/line-ending-hygiene/SKILL.md +18 -10
- package/templates/default/locales/en/.mustflow/skills/llm-hallucination-control-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/motion-system-contract-review/SKILL.md +155 -0
- package/templates/default/locales/en/.mustflow/skills/next-action-menu/SKILL.md +177 -0
- package/templates/default/locales/en/.mustflow/skills/observability-debuggability-review/SKILL.md +15 -7
- package/templates/default/locales/en/.mustflow/skills/payment-integrity-review/SKILL.md +59 -35
- package/templates/default/locales/en/.mustflow/skills/powershell-code-change/SKILL.md +16 -6
- package/templates/default/locales/en/.mustflow/skills/prompt-contract-quality-review/SKILL.md +4 -1
- package/templates/default/locales/en/.mustflow/skills/python-code-change/SKILL.md +19 -10
- package/templates/default/locales/en/.mustflow/skills/rag-pipeline-triage/SKILL.md +206 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +54 -0
- package/templates/default/locales/en/.mustflow/skills/rust-code-change/SKILL.md +10 -4
- package/templates/default/locales/en/.mustflow/skills/search-index-integrity-review/SKILL.md +181 -0
- package/templates/default/locales/en/.mustflow/skills/service-boundary-architecture/SKILL.md +37 -23
- package/templates/default/locales/en/.mustflow/skills/test-suite-performance-review/SKILL.md +9 -0
- package/templates/default/locales/en/.mustflow/skills/typescript-code-change/SKILL.md +14 -9
- package/templates/default/locales/en/.mustflow/skills/vector-search-integrity-review/SKILL.md +209 -0
- package/templates/default/locales/en/.mustflow/skills/version-freshness-check/SKILL.md +16 -14
- package/templates/default/manifest.toml +64 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DEPENDENCY_GRAPH_SCRIPT_REF, inspectDependencyGraph, } from './dependency-graph.js';
|
|
4
|
+
export const IMPORT_CYCLE_PACK_ID = 'code';
|
|
5
|
+
export const IMPORT_CYCLE_SCRIPT_ID = 'import-cycle';
|
|
6
|
+
export const IMPORT_CYCLE_SCRIPT_REF = `${IMPORT_CYCLE_PACK_ID}/${IMPORT_CYCLE_SCRIPT_ID}`;
|
|
7
|
+
const DEFAULT_MAX_DEPTH = 20;
|
|
8
|
+
const DEFAULT_MAX_CYCLES = 50;
|
|
9
|
+
const MAX_ISSUES = 50;
|
|
10
|
+
function sha256Tagged(value) {
|
|
11
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
12
|
+
}
|
|
13
|
+
function cycleId(paths) {
|
|
14
|
+
return `cycle:${createHash('sha256').update(paths.join('\0')).digest('hex').slice(0, 12)}`;
|
|
15
|
+
}
|
|
16
|
+
function edgeKey(sourcePath, targetPath) {
|
|
17
|
+
return `${sourcePath}\0${targetPath}`;
|
|
18
|
+
}
|
|
19
|
+
function buildEdgeIndex(edges) {
|
|
20
|
+
const index = new Map();
|
|
21
|
+
for (const edge of edges) {
|
|
22
|
+
const key = edgeKey(edge.source_path, edge.target_path);
|
|
23
|
+
const existing = index.get(key) ?? [];
|
|
24
|
+
existing.push(edge);
|
|
25
|
+
index.set(key, existing);
|
|
26
|
+
}
|
|
27
|
+
for (const indexedEdges of index.values()) {
|
|
28
|
+
indexedEdges.sort((left, right) => left.line - right.line ||
|
|
29
|
+
left.kind.localeCompare(right.kind) ||
|
|
30
|
+
left.specifier.localeCompare(right.specifier));
|
|
31
|
+
}
|
|
32
|
+
return index;
|
|
33
|
+
}
|
|
34
|
+
function importCycleEdges(cyclePaths, edgeIndex) {
|
|
35
|
+
const edges = [];
|
|
36
|
+
let index = 0;
|
|
37
|
+
while (index < cyclePaths.length - 1) {
|
|
38
|
+
const sourcePath = cyclePaths[index];
|
|
39
|
+
const targetPath = cyclePaths[index + 1];
|
|
40
|
+
if (!sourcePath || !targetPath) {
|
|
41
|
+
index += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const [edge] = edgeIndex.get(edgeKey(sourcePath, targetPath)) ?? [];
|
|
45
|
+
if (edge) {
|
|
46
|
+
edges.push({
|
|
47
|
+
source_path: edge.source_path,
|
|
48
|
+
target_path: edge.target_path,
|
|
49
|
+
specifier: edge.specifier,
|
|
50
|
+
line: edge.line,
|
|
51
|
+
kind: edge.kind,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
return edges;
|
|
57
|
+
}
|
|
58
|
+
function makeFinding(code, severity, pathValue, message, cycle_id) {
|
|
59
|
+
return cycle_id
|
|
60
|
+
? { code, severity, path: pathValue, message, cycle_id }
|
|
61
|
+
: { code, severity, path: pathValue, message };
|
|
62
|
+
}
|
|
63
|
+
function normalizeGraphFinding(finding) {
|
|
64
|
+
return {
|
|
65
|
+
code: finding.code,
|
|
66
|
+
severity: finding.severity,
|
|
67
|
+
path: finding.path,
|
|
68
|
+
message: finding.message,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function pushIssue(issues, issue) {
|
|
72
|
+
if (issues.length < MAX_ISSUES) {
|
|
73
|
+
issues.push(issue);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function importCycleStatus(graphStatus, cycles, findings) {
|
|
77
|
+
if (graphStatus === 'error') {
|
|
78
|
+
return 'error';
|
|
79
|
+
}
|
|
80
|
+
return cycles.length > 0 || findings.length > 0 ? 'failed' : 'passed';
|
|
81
|
+
}
|
|
82
|
+
function createInputHash(policy, graph, cycles, findings, issues) {
|
|
83
|
+
return sha256Tagged(JSON.stringify({
|
|
84
|
+
policy,
|
|
85
|
+
graph_input_hash: graph.input_hash,
|
|
86
|
+
targets: graph.targets,
|
|
87
|
+
cycles: cycles.map((cycle) => ({ cycle_id: cycle.cycle_id, paths: cycle.paths })),
|
|
88
|
+
findings: findings.map((finding) => ({ code: finding.code, path: finding.path, cycle_id: finding.cycle_id })),
|
|
89
|
+
issues,
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
export function inspectImportCycles(projectRoot, options) {
|
|
93
|
+
const root = path.resolve(projectRoot);
|
|
94
|
+
const graph = inspectDependencyGraph(root, {
|
|
95
|
+
paths: options.paths,
|
|
96
|
+
maxFiles: options.maxFiles,
|
|
97
|
+
maxFileBytes: options.maxFileBytes,
|
|
98
|
+
maxDepth: options.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
99
|
+
maxNodes: options.maxNodes,
|
|
100
|
+
maxEdges: options.maxEdges,
|
|
101
|
+
});
|
|
102
|
+
const policy = {
|
|
103
|
+
...graph.policy,
|
|
104
|
+
max_cycles: options.maxCycles ?? DEFAULT_MAX_CYCLES,
|
|
105
|
+
};
|
|
106
|
+
const edgeIndex = buildEdgeIndex(graph.edges);
|
|
107
|
+
const cycles = graph.cycles.slice(0, policy.max_cycles).map((cyclePaths) => ({
|
|
108
|
+
cycle_id: cycleId(cyclePaths),
|
|
109
|
+
path_count: Math.max(0, cyclePaths.length - 1),
|
|
110
|
+
paths: cyclePaths,
|
|
111
|
+
edges: importCycleEdges(cyclePaths, edgeIndex),
|
|
112
|
+
}));
|
|
113
|
+
const issues = [...graph.issues];
|
|
114
|
+
const findings = graph.findings.map(normalizeGraphFinding);
|
|
115
|
+
for (const cycle of cycles) {
|
|
116
|
+
const [firstPath] = cycle.paths;
|
|
117
|
+
findings.push(makeFinding('import_cycle_detected', 'high', firstPath ?? '.', `Import cycle detected: ${cycle.paths.join(' -> ')}`, cycle.cycle_id));
|
|
118
|
+
}
|
|
119
|
+
if (graph.cycles.length > cycles.length) {
|
|
120
|
+
const message = `Import cycle scan found more than ${policy.max_cycles} cycles; remaining cycles were skipped.`;
|
|
121
|
+
pushIssue(issues, message);
|
|
122
|
+
findings.push(makeFinding('import_cycle_max_cycles_exceeded', 'medium', '.', message));
|
|
123
|
+
}
|
|
124
|
+
const status = importCycleStatus(graph.status, cycles, findings);
|
|
125
|
+
const truncated = graph.truncated || graph.cycles.length > cycles.length;
|
|
126
|
+
return {
|
|
127
|
+
schema_version: '1',
|
|
128
|
+
command: 'script-pack',
|
|
129
|
+
pack_id: IMPORT_CYCLE_PACK_ID,
|
|
130
|
+
script_id: IMPORT_CYCLE_SCRIPT_ID,
|
|
131
|
+
script_ref: IMPORT_CYCLE_SCRIPT_REF,
|
|
132
|
+
action: 'check',
|
|
133
|
+
status,
|
|
134
|
+
ok: status === 'passed',
|
|
135
|
+
mustflow_root: root,
|
|
136
|
+
policy,
|
|
137
|
+
input_hash: createInputHash(policy, graph, cycles, findings, issues),
|
|
138
|
+
targets: graph.targets,
|
|
139
|
+
graph: {
|
|
140
|
+
script_ref: DEPENDENCY_GRAPH_SCRIPT_REF,
|
|
141
|
+
status: graph.status,
|
|
142
|
+
node_count: graph.nodes.length,
|
|
143
|
+
edge_count: graph.edges.length,
|
|
144
|
+
cycle_hint_count: graph.cycles.length,
|
|
145
|
+
truncated: graph.truncated,
|
|
146
|
+
},
|
|
147
|
+
cycles,
|
|
148
|
+
truncated,
|
|
149
|
+
findings,
|
|
150
|
+
issues,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -264,6 +264,23 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
264
264
|
'--json',
|
|
265
265
|
],
|
|
266
266
|
},
|
|
267
|
+
{
|
|
268
|
+
id: 'import-cycle-report',
|
|
269
|
+
schemaFile: 'import-cycle-report.schema.json',
|
|
270
|
+
producer: 'mf script-pack run code/import-cycle check <path...> --json',
|
|
271
|
+
packaged: true,
|
|
272
|
+
documented: true,
|
|
273
|
+
installedCommand: [
|
|
274
|
+
'mf',
|
|
275
|
+
'script-pack',
|
|
276
|
+
'run',
|
|
277
|
+
'code/import-cycle',
|
|
278
|
+
'check',
|
|
279
|
+
'node_modules/mustflow/dist/cli/index.js',
|
|
280
|
+
'--json',
|
|
281
|
+
],
|
|
282
|
+
expectedExitCodes: [0, 1],
|
|
283
|
+
},
|
|
267
284
|
{
|
|
268
285
|
id: 'change-impact-report',
|
|
269
286
|
schemaFile: 'change-impact-report.schema.json',
|
|
@@ -342,6 +359,42 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
342
359
|
],
|
|
343
360
|
expectedExitCodes: [0, 1],
|
|
344
361
|
},
|
|
362
|
+
{
|
|
363
|
+
id: 'link-integrity-report',
|
|
364
|
+
schemaFile: 'link-integrity-report.schema.json',
|
|
365
|
+
producer: 'mf script-pack run docs/link-integrity check [path...] --json',
|
|
366
|
+
packaged: true,
|
|
367
|
+
documented: true,
|
|
368
|
+
installedCommand: [
|
|
369
|
+
'mf',
|
|
370
|
+
'script-pack',
|
|
371
|
+
'run',
|
|
372
|
+
'docs/link-integrity',
|
|
373
|
+
'check',
|
|
374
|
+
'node_modules/mustflow/README.md',
|
|
375
|
+
'node_modules/mustflow/schemas/README.md',
|
|
376
|
+
'--json',
|
|
377
|
+
],
|
|
378
|
+
expectedExitCodes: [0, 1],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: 'test-performance-report',
|
|
382
|
+
schemaFile: 'test-performance-report.schema.json',
|
|
383
|
+
producer: 'mf script-pack run test/performance-report summarize --json',
|
|
384
|
+
packaged: true,
|
|
385
|
+
documented: true,
|
|
386
|
+
installedCommand: ['mf', 'script-pack', 'run', 'test/performance-report', 'summarize', '--json'],
|
|
387
|
+
expectedExitCodes: [0, 1],
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
id: 'test-regression-selector-report',
|
|
391
|
+
schemaFile: 'test-regression-selector-report.schema.json',
|
|
392
|
+
producer: 'mf script-pack run test/regression-selector select --json',
|
|
393
|
+
packaged: true,
|
|
394
|
+
documented: true,
|
|
395
|
+
installedCommand: ['mf', 'script-pack', 'run', 'test/regression-selector', 'select', '--json'],
|
|
396
|
+
expectedExitCodes: [0, 1],
|
|
397
|
+
},
|
|
345
398
|
{
|
|
346
399
|
id: 'text-budget-report',
|
|
347
400
|
schemaFile: 'text-budget-report.schema.json',
|
|
@@ -379,6 +432,69 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
379
432
|
],
|
|
380
433
|
expectedExitCodes: [0, 1],
|
|
381
434
|
},
|
|
435
|
+
{
|
|
436
|
+
id: 'repo-merge-conflict-scan-report',
|
|
437
|
+
schemaFile: 'repo-merge-conflict-scan-report.schema.json',
|
|
438
|
+
producer: 'mf script-pack run repo/merge-conflict-scan check [path...] --json',
|
|
439
|
+
packaged: true,
|
|
440
|
+
documented: true,
|
|
441
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/merge-conflict-scan', 'check', '--json'],
|
|
442
|
+
expectedExitCodes: [0, 1],
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
id: 'repo-git-ignore-audit-report',
|
|
446
|
+
schemaFile: 'repo-git-ignore-audit-report.schema.json',
|
|
447
|
+
producer: 'mf script-pack run repo/git-ignore-audit audit [path...] --json',
|
|
448
|
+
packaged: true,
|
|
449
|
+
documented: true,
|
|
450
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/git-ignore-audit', 'audit', 'AGENTS.md', '--json'],
|
|
451
|
+
expectedExitCodes: [0, 1],
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: 'repo-manifest-lock-drift-report',
|
|
455
|
+
schemaFile: 'repo-manifest-lock-drift-report.schema.json',
|
|
456
|
+
producer: 'mf script-pack run repo/manifest-lock-drift check [path...] --json',
|
|
457
|
+
packaged: true,
|
|
458
|
+
documented: true,
|
|
459
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/manifest-lock-drift', 'check', 'AGENTS.md', '--json'],
|
|
460
|
+
expectedExitCodes: [0, 1],
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
id: 'skill-route-audit-report',
|
|
464
|
+
schemaFile: 'skill-route-audit-report.schema.json',
|
|
465
|
+
producer: 'mf script-pack run repo/skill-route-audit audit --json',
|
|
466
|
+
packaged: true,
|
|
467
|
+
documented: true,
|
|
468
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/skill-route-audit', 'audit', '--json'],
|
|
469
|
+
expectedExitCodes: [0, 1],
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
id: 'repo-version-source-report',
|
|
473
|
+
schemaFile: 'repo-version-source-report.schema.json',
|
|
474
|
+
producer: 'mf script-pack run repo/version-source inspect --json',
|
|
475
|
+
packaged: true,
|
|
476
|
+
documented: true,
|
|
477
|
+
installedCommand: ['mf', 'script-pack', 'run', 'repo/version-source', 'inspect', '--json'],
|
|
478
|
+
expectedExitCodes: [0, 1],
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
id: 'repo-approval-gate-report',
|
|
482
|
+
schemaFile: 'repo-approval-gate-report.schema.json',
|
|
483
|
+
producer: 'mf script-pack run repo/approval-gate check --action <type> --json',
|
|
484
|
+
packaged: true,
|
|
485
|
+
documented: true,
|
|
486
|
+
installedCommand: [
|
|
487
|
+
'mf',
|
|
488
|
+
'script-pack',
|
|
489
|
+
'run',
|
|
490
|
+
'repo/approval-gate',
|
|
491
|
+
'check',
|
|
492
|
+
'--action',
|
|
493
|
+
'git_commit',
|
|
494
|
+
'--json',
|
|
495
|
+
],
|
|
496
|
+
expectedExitCodes: [0, 1],
|
|
497
|
+
},
|
|
382
498
|
{
|
|
383
499
|
id: 'config-chain-report',
|
|
384
500
|
schemaFile: 'config-chain-report.schema.json',
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isRecord, MUSTFLOW_CONFIG_RELATIVE_PATH, readMustflowConfigIfExists, readStringArray, } from './config-loading.js';
|
|
4
|
+
export const REPO_APPROVAL_GATE_PACK_ID = 'repo';
|
|
5
|
+
export const REPO_APPROVAL_GATE_SCRIPT_ID = 'approval-gate';
|
|
6
|
+
export const REPO_APPROVAL_GATE_SCRIPT_REF = `${REPO_APPROVAL_GATE_PACK_ID}/${REPO_APPROVAL_GATE_SCRIPT_ID}`;
|
|
7
|
+
export const REPO_APPROVAL_GATE_POLICY_PATH = MUSTFLOW_CONFIG_RELATIVE_PATH;
|
|
8
|
+
function sha256(value) {
|
|
9
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
10
|
+
}
|
|
11
|
+
function uniqueActions(actions) {
|
|
12
|
+
const normalized = [];
|
|
13
|
+
const seen = new Set();
|
|
14
|
+
for (const action of actions) {
|
|
15
|
+
const trimmed = action.trim();
|
|
16
|
+
if (trimmed.length === 0 || seen.has(trimmed)) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
seen.add(trimmed);
|
|
20
|
+
normalized.push(trimmed);
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
function readApprovalPolicy(projectRoot, issues) {
|
|
25
|
+
let config;
|
|
26
|
+
try {
|
|
27
|
+
config = readMustflowConfigIfExists(projectRoot);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
issues.push(`Could not read ${REPO_APPROVAL_GATE_POLICY_PATH}: ${message}`);
|
|
32
|
+
}
|
|
33
|
+
if (!config) {
|
|
34
|
+
issues.push(`${REPO_APPROVAL_GATE_POLICY_PATH} is missing.`);
|
|
35
|
+
return { required_for: [], on_required: null };
|
|
36
|
+
}
|
|
37
|
+
if (!isRecord(config.approval)) {
|
|
38
|
+
issues.push(`[approval] in ${REPO_APPROVAL_GATE_POLICY_PATH} must be a TOML table.`);
|
|
39
|
+
return { required_for: [], on_required: null };
|
|
40
|
+
}
|
|
41
|
+
const approval = config.approval;
|
|
42
|
+
const requiredFor = readStringArray(approval, 'required_for');
|
|
43
|
+
if (!requiredFor) {
|
|
44
|
+
issues.push(`[approval].required_for in ${REPO_APPROVAL_GATE_POLICY_PATH} must be a string array.`);
|
|
45
|
+
}
|
|
46
|
+
const onRequired = approval.on_required;
|
|
47
|
+
if (onRequired !== undefined && typeof onRequired !== 'string') {
|
|
48
|
+
issues.push(`[approval].on_required in ${REPO_APPROVAL_GATE_POLICY_PATH} must be a string.`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
required_for: requiredFor ?? [],
|
|
52
|
+
on_required: typeof onRequired === 'string' ? onRequired : null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createDecisions(actionTypes, policy) {
|
|
56
|
+
const required = new Set(policy.required_for);
|
|
57
|
+
return actionTypes.map((actionType) => {
|
|
58
|
+
const approvalRequired = required.has(actionType);
|
|
59
|
+
return {
|
|
60
|
+
action_type: actionType,
|
|
61
|
+
approval_required: approvalRequired,
|
|
62
|
+
policy_source: approvalRequired ? `${REPO_APPROVAL_GATE_POLICY_PATH}#[approval].required_for` : null,
|
|
63
|
+
reason: approvalRequired
|
|
64
|
+
? `Action "${actionType}" is listed in [approval].required_for.`
|
|
65
|
+
: `Action "${actionType}" is not listed in [approval].required_for.`,
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function createFindings(decisions) {
|
|
70
|
+
return decisions
|
|
71
|
+
.filter((decision) => decision.approval_required)
|
|
72
|
+
.map((decision) => ({
|
|
73
|
+
code: 'approval_required_for_action',
|
|
74
|
+
severity: 'high',
|
|
75
|
+
path: REPO_APPROVAL_GATE_POLICY_PATH,
|
|
76
|
+
message: `Action "${decision.action_type}" requires explicit approval before proceeding.`,
|
|
77
|
+
json_pointer: '/approval/required_for',
|
|
78
|
+
metric: null,
|
|
79
|
+
actual: null,
|
|
80
|
+
expected: null,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
function createInputHash(reportInput) {
|
|
84
|
+
return sha256(JSON.stringify(reportInput));
|
|
85
|
+
}
|
|
86
|
+
export function checkRepoApprovalGate(projectRoot, actionTypes) {
|
|
87
|
+
const root = path.resolve(projectRoot);
|
|
88
|
+
const actions = uniqueActions(actionTypes);
|
|
89
|
+
const issues = [];
|
|
90
|
+
const policy = readApprovalPolicy(root, issues);
|
|
91
|
+
const decisions = issues.length > 0 ? [] : createDecisions(actions, policy);
|
|
92
|
+
const findings = createFindings(decisions);
|
|
93
|
+
const approvalRequired = findings.length > 0;
|
|
94
|
+
const status = issues.length > 0 ? 'error' : approvalRequired ? 'failed' : 'passed';
|
|
95
|
+
return {
|
|
96
|
+
schema_version: '1',
|
|
97
|
+
command: 'script-pack',
|
|
98
|
+
pack_id: REPO_APPROVAL_GATE_PACK_ID,
|
|
99
|
+
script_id: REPO_APPROVAL_GATE_SCRIPT_ID,
|
|
100
|
+
script_ref: REPO_APPROVAL_GATE_SCRIPT_REF,
|
|
101
|
+
action: 'check',
|
|
102
|
+
status,
|
|
103
|
+
ok: status === 'passed',
|
|
104
|
+
mustflow_root: root,
|
|
105
|
+
input: {
|
|
106
|
+
action_types: actions,
|
|
107
|
+
policy_path: REPO_APPROVAL_GATE_POLICY_PATH,
|
|
108
|
+
},
|
|
109
|
+
input_hash: createInputHash({ actionTypes: actions, policy, decisions, findings, issues }),
|
|
110
|
+
approval_required: approvalRequired,
|
|
111
|
+
policy,
|
|
112
|
+
decisions,
|
|
113
|
+
findings,
|
|
114
|
+
issues,
|
|
115
|
+
};
|
|
116
|
+
}
|