mustflow 2.69.0 → 2.74.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/README.md +19 -5
- package/dist/cli/commands/api.js +17 -0
- package/dist/cli/commands/evidence.js +71 -0
- package/dist/cli/commands/script-pack.js +124 -0
- package/dist/cli/commands/verify.js +50 -15
- package/dist/cli/commands/workspace.js +2 -0
- package/dist/cli/i18n/en.js +38 -0
- package/dist/cli/i18n/es.js +38 -0
- package/dist/cli/i18n/fr.js +38 -0
- package/dist/cli/i18n/hi.js +38 -0
- package/dist/cli/i18n/ko.js +38 -0
- package/dist/cli/i18n/zh.js +38 -0
- package/dist/cli/index.js +1 -0
- package/dist/cli/lib/agent-context.js +179 -10
- package/dist/cli/lib/command-registry.js +6 -0
- package/dist/cli/lib/dashboard-export.js +1 -0
- package/dist/cli/lib/script-pack-registry.js +27 -0
- package/dist/cli/script-packs/core-text-budget.js +241 -0
- package/dist/core/change-verification.js +10 -0
- package/dist/core/completion-verdict.js +14 -1
- package/dist/core/complexity-budget.js +206 -0
- package/dist/core/conflict-ledger.js +122 -0
- package/dist/core/failure-replay-capsule.js +213 -0
- package/dist/core/public-json-contracts.js +27 -0
- package/dist/core/risk-priced-evidence.js +213 -0
- package/dist/core/script-check-result.js +1 -0
- package/dist/core/text-budget.js +262 -0
- package/dist/core/verification-evidence.js +61 -13
- package/package.json +1 -1
- package/schemas/README.md +23 -11
- package/schemas/change-verification-report.schema.json +29 -0
- package/schemas/context-report.schema.json +58 -2
- package/schemas/dashboard-export.schema.json +42 -1
- package/schemas/diff-risk.schema.json +6 -0
- package/schemas/evidence-report.schema.json +45 -0
- package/schemas/latest-run-pointer.schema.json +50 -1
- package/schemas/script-pack-catalog.schema.json +68 -0
- package/schemas/text-budget-report.schema.json +131 -0
- package/schemas/verification-plan.schema.json +32 -0
- package/schemas/verify-report.schema.json +360 -1
- package/schemas/verify-run-manifest.schema.json +50 -1
- package/schemas/workspace-verification-plan.schema.json +32 -0
- package/templates/default/i18n.toml +8 -2
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +3 -2
- package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +19 -2
- package/templates/default/locales/en/.mustflow/skills/design-implementation-handoff/SKILL.md +250 -0
- package/templates/default/locales/en/.mustflow/skills/routes.toml +7 -1
- package/templates/default/manifest.toml +8 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
function stableJson(value) {
|
|
5
|
+
if (Array.isArray(value)) {
|
|
6
|
+
return `[${value.map((entry) => stableJson(entry)).join(',')}]`;
|
|
7
|
+
}
|
|
8
|
+
if (value && typeof value === 'object') {
|
|
9
|
+
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
10
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`).join(',')}}`;
|
|
11
|
+
}
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
function sha256(value) {
|
|
15
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
16
|
+
}
|
|
17
|
+
function sha256Json(value) {
|
|
18
|
+
return sha256(stableJson(value));
|
|
19
|
+
}
|
|
20
|
+
function uniqueSorted(values) {
|
|
21
|
+
return [...new Set([...values].filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
|
|
22
|
+
}
|
|
23
|
+
function numberArray(values) {
|
|
24
|
+
return [...new Set([...values].filter((value) => typeof value === 'number' && Number.isFinite(value)))]
|
|
25
|
+
.sort((left, right) => left - right);
|
|
26
|
+
}
|
|
27
|
+
function objectField(value) {
|
|
28
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
29
|
+
}
|
|
30
|
+
function stringField(record, key) {
|
|
31
|
+
const value = record?.[key];
|
|
32
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
33
|
+
}
|
|
34
|
+
function numberField(record, key) {
|
|
35
|
+
const value = record?.[key];
|
|
36
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
37
|
+
}
|
|
38
|
+
function booleanField(record, key) {
|
|
39
|
+
return record?.[key] === true;
|
|
40
|
+
}
|
|
41
|
+
function stringArrayField(record, key) {
|
|
42
|
+
const value = record?.[key];
|
|
43
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string' && entry.length > 0) : [];
|
|
44
|
+
}
|
|
45
|
+
function hashTail(output) {
|
|
46
|
+
const tail = stringField(output, 'tail');
|
|
47
|
+
return tail === null ? null : sha256(tail);
|
|
48
|
+
}
|
|
49
|
+
function isFailedResult(result) {
|
|
50
|
+
return (!result.skipped &&
|
|
51
|
+
result.intent !== null &&
|
|
52
|
+
(result.status === 'failed' ||
|
|
53
|
+
result.status === 'timed_out' ||
|
|
54
|
+
result.status === 'start_failed' ||
|
|
55
|
+
result.status === 'output_limit_exceeded'));
|
|
56
|
+
}
|
|
57
|
+
function createFailedResult(result) {
|
|
58
|
+
const receipt = result.receipt;
|
|
59
|
+
const performance = objectField(receipt?.performance);
|
|
60
|
+
const resultSummary = objectField(performance?.result_summary);
|
|
61
|
+
const stdout = objectField(receipt?.stdout);
|
|
62
|
+
const stderr = objectField(receipt?.stderr);
|
|
63
|
+
const stdoutTruncated = booleanField(stdout, 'truncated');
|
|
64
|
+
const stderrTruncated = booleanField(stderr, 'truncated');
|
|
65
|
+
const intent = result.intent;
|
|
66
|
+
return {
|
|
67
|
+
intent,
|
|
68
|
+
status: result.status,
|
|
69
|
+
exit_code: result.exit_code,
|
|
70
|
+
exit_code_class: stringField(resultSummary, 'exit_code_class'),
|
|
71
|
+
error_kind: stringField(resultSummary, 'error_kind'),
|
|
72
|
+
timed_out: result.status === 'timed_out' || booleanField(resultSummary, 'timed_out'),
|
|
73
|
+
receipt_path: result.receipt_path,
|
|
74
|
+
receipt_sha256: result.receipt_sha256,
|
|
75
|
+
command_fingerprint: stringField(performance, 'command_fingerprint'),
|
|
76
|
+
contract_fingerprint: stringField(performance, 'contract_fingerprint'),
|
|
77
|
+
stdout_tail_hash: hashTail(stdout),
|
|
78
|
+
stderr_tail_hash: hashTail(stderr),
|
|
79
|
+
output_truncated: stdoutTruncated || stderrTruncated,
|
|
80
|
+
replay_command: `mf run ${intent}`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function fileFingerprint(projectRoot, relativePath) {
|
|
84
|
+
const normalized = relativePath.split(path.sep).join('/');
|
|
85
|
+
const absolute = path.resolve(projectRoot, ...normalized.split('/'));
|
|
86
|
+
const relativeToRoot = path.relative(projectRoot, absolute);
|
|
87
|
+
if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
|
|
88
|
+
return {
|
|
89
|
+
path: normalized,
|
|
90
|
+
status: 'outside_root',
|
|
91
|
+
sha256: null,
|
|
92
|
+
bytes: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
if (!existsSync(absolute)) {
|
|
96
|
+
return {
|
|
97
|
+
path: normalized,
|
|
98
|
+
status: 'missing',
|
|
99
|
+
sha256: null,
|
|
100
|
+
bytes: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const stats = statSync(absolute);
|
|
105
|
+
if (!stats.isFile()) {
|
|
106
|
+
return {
|
|
107
|
+
path: normalized,
|
|
108
|
+
status: 'unreadable',
|
|
109
|
+
sha256: null,
|
|
110
|
+
bytes: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const content = readFileSync(absolute);
|
|
114
|
+
return {
|
|
115
|
+
path: normalized,
|
|
116
|
+
status: 'present',
|
|
117
|
+
sha256: sha256(content),
|
|
118
|
+
bytes: content.byteLength,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return {
|
|
123
|
+
path: normalized,
|
|
124
|
+
status: 'unreadable',
|
|
125
|
+
sha256: null,
|
|
126
|
+
bytes: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function changedFileFingerprints(projectRoot, report) {
|
|
131
|
+
return uniqueSorted(report.files)
|
|
132
|
+
.map((filePath) => fileFingerprint(projectRoot, filePath))
|
|
133
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
134
|
+
}
|
|
135
|
+
function redactionSummary(results) {
|
|
136
|
+
const redactionRecords = results
|
|
137
|
+
.map((result) => objectField(result.receipt?.redaction))
|
|
138
|
+
.filter((record) => record !== null);
|
|
139
|
+
const redactionCount = redactionRecords.reduce((sum, record) => sum + (numberField(record, 'redaction_count') ?? 0), 0);
|
|
140
|
+
return {
|
|
141
|
+
raw_output_included: false,
|
|
142
|
+
env_values_included: false,
|
|
143
|
+
receipt_paths_only: true,
|
|
144
|
+
redacted: redactionRecords.some((record) => booleanField(record, 'redacted')),
|
|
145
|
+
redaction_count: redactionCount,
|
|
146
|
+
redaction_kinds: uniqueSorted(redactionRecords.flatMap((record) => stringArrayField(record, 'redaction_kinds'))),
|
|
147
|
+
redaction_fields: uniqueSorted(redactionRecords.flatMap((record) => stringArrayField(record, 'fields'))),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function environmentSummary(results) {
|
|
151
|
+
const receipts = results.map((result) => result.receipt).filter((receipt) => receipt !== null);
|
|
152
|
+
const runners = receipts
|
|
153
|
+
.map((receipt) => objectField(objectField(receipt.performance)?.runner))
|
|
154
|
+
.filter((runner) => runner !== null);
|
|
155
|
+
return {
|
|
156
|
+
platform_family: uniqueSorted(runners.map((runner) => stringField(runner, 'platform_family')))[0] ?? null,
|
|
157
|
+
arch_family: uniqueSorted(runners.map((runner) => stringField(runner, 'arch_family')))[0] ?? null,
|
|
158
|
+
runtimes: uniqueSorted(runners.map((runner) => {
|
|
159
|
+
const runtime = stringField(runner, 'runtime');
|
|
160
|
+
const major = numberField(runner, 'runtime_major');
|
|
161
|
+
return runtime === null ? null : `${runtime}${major === null ? '' : `@${major}`}`;
|
|
162
|
+
})),
|
|
163
|
+
env_policies: uniqueSorted(receipts.map((receipt) => stringField(receipt, 'env_policy'))),
|
|
164
|
+
env_allowlist: uniqueSorted(receipts.flatMap((receipt) => stringArrayField(receipt, 'env_allowlist'))),
|
|
165
|
+
timeout_seconds: numberArray(receipts.map((receipt) => numberField(receipt, 'timeout_seconds'))),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
function riskCodes(report) {
|
|
169
|
+
return uniqueSorted([
|
|
170
|
+
...report.risk_assessment.reasons,
|
|
171
|
+
...report.risk_assessment.blocking_gaps,
|
|
172
|
+
...report.gaps.map((gap) => gap.reason),
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
export function createFailureReplayCapsule(input) {
|
|
176
|
+
const failedResults = input.results.filter(isFailedResult).map(createFailedResult);
|
|
177
|
+
if (failedResults.length === 0) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const affectedFiles = changedFileFingerprints(input.projectRoot, input.report);
|
|
181
|
+
const affectedSurfaces = uniqueSorted(input.report.requirements.flatMap((requirement) => requirement.surfaces));
|
|
182
|
+
const replayCommands = uniqueSorted([
|
|
183
|
+
...failedResults.map((result) => result.replay_command),
|
|
184
|
+
`mf verify --reason ${input.reasons[0] ?? 'unknown'} --json`,
|
|
185
|
+
]);
|
|
186
|
+
const base = {
|
|
187
|
+
schema_version: '1',
|
|
188
|
+
source: 'mf_verify_failure',
|
|
189
|
+
authority: 'replay_supporting_evidence',
|
|
190
|
+
created_at: (input.createdAt ?? new Date()).toISOString(),
|
|
191
|
+
verification_plan_id: input.verificationPlanId,
|
|
192
|
+
failure_fingerprint: input.failureFingerprint?.fingerprint ?? null,
|
|
193
|
+
reasons: uniqueSorted(input.reasons),
|
|
194
|
+
status: input.status,
|
|
195
|
+
replay_commands: replayCommands,
|
|
196
|
+
failed_results: failedResults,
|
|
197
|
+
affected_files: affectedFiles,
|
|
198
|
+
affected_surfaces: affectedSurfaces,
|
|
199
|
+
risk_codes: riskCodes(input.report),
|
|
200
|
+
environment: environmentSummary(input.results),
|
|
201
|
+
privacy: redactionSummary(input.results),
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
...base,
|
|
205
|
+
hashes: {
|
|
206
|
+
capsule_fingerprint: sha256Json(base),
|
|
207
|
+
changed_files_hash: sha256Json(affectedFiles),
|
|
208
|
+
failed_results_hash: sha256Json(failedResults),
|
|
209
|
+
command_fingerprints_hash: sha256Json(failedResults.map((result) => result.command_fingerprint)),
|
|
210
|
+
output_tails_hash: sha256Json(failedResults.flatMap((result) => [result.stdout_tail_hash, result.stderr_tail_hash])),
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -207,6 +207,33 @@ const PUBLIC_JSON_SCHEMA_CONTRACTS = [
|
|
|
207
207
|
installedCommand: ['mf', 'quality', 'check', '--json'],
|
|
208
208
|
expectedExitCodes: [0, 1],
|
|
209
209
|
},
|
|
210
|
+
{
|
|
211
|
+
id: 'script-pack-catalog',
|
|
212
|
+
schemaFile: 'script-pack-catalog.schema.json',
|
|
213
|
+
producer: 'mf script-pack list --json',
|
|
214
|
+
packaged: true,
|
|
215
|
+
documented: true,
|
|
216
|
+
installedCommand: ['mf', 'script-pack', 'list', '--json'],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: 'text-budget-report',
|
|
220
|
+
schemaFile: 'text-budget-report.schema.json',
|
|
221
|
+
producer: 'mf script-pack run core/text-budget check <path...> --json',
|
|
222
|
+
packaged: true,
|
|
223
|
+
documented: true,
|
|
224
|
+
installedCommand: [
|
|
225
|
+
'mf',
|
|
226
|
+
'script-pack',
|
|
227
|
+
'run',
|
|
228
|
+
'core/text-budget',
|
|
229
|
+
'check',
|
|
230
|
+
'AGENTS.md',
|
|
231
|
+
'--max',
|
|
232
|
+
'1000000',
|
|
233
|
+
'--json',
|
|
234
|
+
],
|
|
235
|
+
expectedExitCodes: [0, 1],
|
|
236
|
+
},
|
|
210
237
|
{
|
|
211
238
|
id: 'skill-route-report',
|
|
212
239
|
schemaFile: 'skill-route-report.schema.json',
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { isRecord, readString, readStringArray } from './config-loading.js';
|
|
2
|
+
const LEVEL_RANK = {
|
|
3
|
+
low: 0,
|
|
4
|
+
medium: 1,
|
|
5
|
+
high: 2,
|
|
6
|
+
critical: 3,
|
|
7
|
+
};
|
|
8
|
+
const HIGH_RISK_REASONS = new Set([
|
|
9
|
+
'public_api_change',
|
|
10
|
+
'package_metadata_change',
|
|
11
|
+
'template_version_change',
|
|
12
|
+
'packaging_change',
|
|
13
|
+
'release_risk',
|
|
14
|
+
'before_publish',
|
|
15
|
+
'security_change',
|
|
16
|
+
'privacy_change',
|
|
17
|
+
'data_change',
|
|
18
|
+
'mustflow_config_change',
|
|
19
|
+
'mustflow_docs_change',
|
|
20
|
+
'auth_permission_change',
|
|
21
|
+
'deployment_change',
|
|
22
|
+
]);
|
|
23
|
+
const CRITICAL_RISK_REASONS = new Set([
|
|
24
|
+
'database_migration',
|
|
25
|
+
'database_migration_change',
|
|
26
|
+
'migration_change',
|
|
27
|
+
'secret_exposure',
|
|
28
|
+
'secret_change',
|
|
29
|
+
'destructive_change',
|
|
30
|
+
]);
|
|
31
|
+
const MEDIUM_RISK_REASONS = new Set([
|
|
32
|
+
'code_change',
|
|
33
|
+
'behavior_change',
|
|
34
|
+
'test_change',
|
|
35
|
+
'unknown_change',
|
|
36
|
+
'performance_change',
|
|
37
|
+
'ui_change',
|
|
38
|
+
'build_config_change',
|
|
39
|
+
'dependency_change',
|
|
40
|
+
]);
|
|
41
|
+
const HIGH_RISK_TEXT = [
|
|
42
|
+
'auth',
|
|
43
|
+
'authorization',
|
|
44
|
+
'permission',
|
|
45
|
+
'security',
|
|
46
|
+
'privacy',
|
|
47
|
+
'pii',
|
|
48
|
+
'token',
|
|
49
|
+
'credential',
|
|
50
|
+
'payment',
|
|
51
|
+
'refund',
|
|
52
|
+
'ledger',
|
|
53
|
+
'billing',
|
|
54
|
+
'public api',
|
|
55
|
+
'json schema',
|
|
56
|
+
'machine-readable output',
|
|
57
|
+
'package',
|
|
58
|
+
'release',
|
|
59
|
+
'deployment',
|
|
60
|
+
'production',
|
|
61
|
+
'data contract',
|
|
62
|
+
];
|
|
63
|
+
const CRITICAL_RISK_TEXT = [
|
|
64
|
+
'destructive',
|
|
65
|
+
'delete',
|
|
66
|
+
'drop table',
|
|
67
|
+
'secret',
|
|
68
|
+
'credential',
|
|
69
|
+
'private key',
|
|
70
|
+
'database migration',
|
|
71
|
+
'payment settlement',
|
|
72
|
+
'ledger mutation',
|
|
73
|
+
];
|
|
74
|
+
function uniqueSorted(values) {
|
|
75
|
+
return [...new Set([...values].filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
|
|
76
|
+
}
|
|
77
|
+
function maxLevel(left, right) {
|
|
78
|
+
return LEVEL_RANK[right] > LEVEL_RANK[left] ? right : left;
|
|
79
|
+
}
|
|
80
|
+
function includesAnySignal(values, signals) {
|
|
81
|
+
const normalizedValues = values.map((value) => value.toLowerCase());
|
|
82
|
+
return signals.some((signal) => normalizedValues.some((value) => value.includes(signal)));
|
|
83
|
+
}
|
|
84
|
+
function readBoolean(record, key) {
|
|
85
|
+
return typeof record[key] === 'boolean' ? record[key] : null;
|
|
86
|
+
}
|
|
87
|
+
function selectedIntentRecords(input) {
|
|
88
|
+
return input.selectedIntents
|
|
89
|
+
.map((intent) => input.commandContract.intents[intent])
|
|
90
|
+
.filter((intent) => isRecord(intent));
|
|
91
|
+
}
|
|
92
|
+
function intentHasDeleteEffect(intent) {
|
|
93
|
+
if (!Array.isArray(intent.effects)) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return intent.effects.some((effect) => isRecord(effect) && readString(effect, 'mode') === 'delete_recreate');
|
|
97
|
+
}
|
|
98
|
+
function classifyReasonSignals(input) {
|
|
99
|
+
let level = 'low';
|
|
100
|
+
const reasons = [];
|
|
101
|
+
const validationReasons = input.classificationSummary.validationReasons;
|
|
102
|
+
const signalText = uniqueSorted([
|
|
103
|
+
...validationReasons,
|
|
104
|
+
...input.classificationSummary.changeKinds,
|
|
105
|
+
...input.classificationSummary.affectedContracts,
|
|
106
|
+
...input.classificationSummary.driftChecks,
|
|
107
|
+
...input.requirements.flatMap((requirement) => [
|
|
108
|
+
requirement.reason,
|
|
109
|
+
...requirement.surfaces,
|
|
110
|
+
...requirement.affectedContracts,
|
|
111
|
+
...requirement.driftChecks,
|
|
112
|
+
]),
|
|
113
|
+
]);
|
|
114
|
+
for (const reason of validationReasons) {
|
|
115
|
+
if (CRITICAL_RISK_REASONS.has(reason)) {
|
|
116
|
+
level = maxLevel(level, 'critical');
|
|
117
|
+
reasons.push(`critical validation reason: ${reason}`);
|
|
118
|
+
}
|
|
119
|
+
else if (HIGH_RISK_REASONS.has(reason)) {
|
|
120
|
+
level = maxLevel(level, 'high');
|
|
121
|
+
reasons.push(`high validation reason: ${reason}`);
|
|
122
|
+
}
|
|
123
|
+
else if (MEDIUM_RISK_REASONS.has(reason)) {
|
|
124
|
+
level = maxLevel(level, 'medium');
|
|
125
|
+
reasons.push(`medium validation reason: ${reason}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (includesAnySignal(signalText, CRITICAL_RISK_TEXT)) {
|
|
129
|
+
level = maxLevel(level, 'critical');
|
|
130
|
+
reasons.push('critical contract or surface signal');
|
|
131
|
+
}
|
|
132
|
+
else if (includesAnySignal(signalText, HIGH_RISK_TEXT)) {
|
|
133
|
+
level = maxLevel(level, 'high');
|
|
134
|
+
reasons.push('high contract or surface signal');
|
|
135
|
+
}
|
|
136
|
+
if (input.gaps.length > 0) {
|
|
137
|
+
level = maxLevel(level, 'high');
|
|
138
|
+
reasons.push('verification gaps exist');
|
|
139
|
+
}
|
|
140
|
+
if (level === 'low' && input.classificationSummary.publicSurfaceCount > 0 && validationReasons.length > 0) {
|
|
141
|
+
reasons.push('public surface changed but no high-risk trigger matched');
|
|
142
|
+
}
|
|
143
|
+
return { level, reasons: uniqueSorted(reasons) };
|
|
144
|
+
}
|
|
145
|
+
function classifyCommandSignals(input) {
|
|
146
|
+
let level = 'low';
|
|
147
|
+
const reasons = [];
|
|
148
|
+
for (const intent of selectedIntentRecords(input)) {
|
|
149
|
+
const intentName = readString(intent, 'name') ?? null;
|
|
150
|
+
const label = intentName ?? 'selected intent';
|
|
151
|
+
if (readBoolean(intent, 'destructive') === true) {
|
|
152
|
+
level = maxLevel(level, 'critical');
|
|
153
|
+
reasons.push(`${label} is destructive`);
|
|
154
|
+
}
|
|
155
|
+
if (readBoolean(intent, 'network') === true) {
|
|
156
|
+
level = maxLevel(level, 'high');
|
|
157
|
+
reasons.push(`${label} uses network`);
|
|
158
|
+
}
|
|
159
|
+
if (intentHasDeleteEffect(intent)) {
|
|
160
|
+
level = maxLevel(level, 'medium');
|
|
161
|
+
reasons.push(`${label} rewrites declared output paths`);
|
|
162
|
+
}
|
|
163
|
+
if ((readStringArray(intent, 'writes') ?? []).length > 0) {
|
|
164
|
+
level = maxLevel(level, 'medium');
|
|
165
|
+
reasons.push(`${label} writes files`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return { level, reasons: uniqueSorted(reasons) };
|
|
169
|
+
}
|
|
170
|
+
function requiredEvidence(level) {
|
|
171
|
+
if (level === 'critical') {
|
|
172
|
+
return [
|
|
173
|
+
'configured_mf_run_receipt',
|
|
174
|
+
'receipt_bound_to_current_plan',
|
|
175
|
+
'synchronized_contract_surfaces',
|
|
176
|
+
'rollback_or_recovery_plan',
|
|
177
|
+
'manual_approval_record',
|
|
178
|
+
'remaining_risk_review',
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
if (level === 'high') {
|
|
182
|
+
return [
|
|
183
|
+
'configured_mf_run_receipt',
|
|
184
|
+
'receipt_bound_to_current_plan',
|
|
185
|
+
'synchronized_contract_surfaces',
|
|
186
|
+
'remaining_risk_review',
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
if (level === 'medium') {
|
|
190
|
+
return ['configured_mf_run_receipt', 'related_or_declared_verification'];
|
|
191
|
+
}
|
|
192
|
+
return ['changed_file_review'];
|
|
193
|
+
}
|
|
194
|
+
export function riskPricedEvidenceRiskCount(assessment) {
|
|
195
|
+
return assessment.level === 'high' || assessment.level === 'critical' ? 1 : 0;
|
|
196
|
+
}
|
|
197
|
+
export function createVerificationRiskAssessment(input) {
|
|
198
|
+
const reasonSignals = classifyReasonSignals(input);
|
|
199
|
+
const commandSignals = classifyCommandSignals(input);
|
|
200
|
+
const level = maxLevel(reasonSignals.level, commandSignals.level);
|
|
201
|
+
const blockingGaps = input.gaps.map((gap) => `${gap.reason}: ${gap.detail}`);
|
|
202
|
+
return {
|
|
203
|
+
schema_version: '1',
|
|
204
|
+
source: 'change_classification_and_command_contract',
|
|
205
|
+
level,
|
|
206
|
+
reasons: uniqueSorted([...reasonSignals.reasons, ...commandSignals.reasons]),
|
|
207
|
+
required_evidence: requiredEvidence(level),
|
|
208
|
+
blocking_gaps: blockingGaps,
|
|
209
|
+
rollback_required: level === 'critical',
|
|
210
|
+
human_approval_required: level === 'critical',
|
|
211
|
+
manual_review_required: level === 'high' || level === 'critical',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|