mustflow 2.70.0 → 2.74.1
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 +20 -6
- package/dist/cli/commands/api.js +17 -0
- package/dist/cli/commands/check.js +38 -26
- package/dist/cli/commands/doctor.js +17 -5
- package/dist/cli/commands/evidence.js +71 -0
- package/dist/cli/commands/index.js +24 -9
- package/dist/cli/commands/map.js +20 -7
- package/dist/cli/commands/run.js +2 -1
- package/dist/cli/commands/script-pack.js +124 -0
- package/dist/cli/commands/update.js +52 -39
- 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/active-command-lock.js +96 -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/active-run-locks.js +7 -1
- 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 +2 -2
- package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -2
- package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +19 -2
- package/templates/default/locales/en/.mustflow/skills/routes.toml +1 -1
- package/templates/default/manifest.toml +1 -1
|
@@ -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 {};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureInside, readFileInsideWithoutSymlinks } from './safe-filesystem.js';
|
|
4
|
+
export const TEXT_BUDGET_PACK_ID = 'core';
|
|
5
|
+
export const TEXT_BUDGET_SCRIPT_ID = 'text-budget';
|
|
6
|
+
export const TEXT_BUDGET_SCRIPT_REF = `${TEXT_BUDGET_PACK_ID}/${TEXT_BUDGET_SCRIPT_ID}`;
|
|
7
|
+
const DEFAULT_TEXT_BUDGET_UNIT = 'grapheme';
|
|
8
|
+
const DEFAULT_MAX_FILE_BYTES = 1024 * 1024;
|
|
9
|
+
const ERROR_FINDING_CODES = new Set([
|
|
10
|
+
'text_budget_unreadable',
|
|
11
|
+
'text_budget_json_parse_failed',
|
|
12
|
+
'text_budget_json_pointer_invalid',
|
|
13
|
+
'text_budget_json_pointer_missing',
|
|
14
|
+
'text_budget_json_pointer_not_string',
|
|
15
|
+
]);
|
|
16
|
+
export const TEXT_BUDGET_UNITS = [
|
|
17
|
+
'grapheme',
|
|
18
|
+
'code-point',
|
|
19
|
+
'utf16',
|
|
20
|
+
'utf8-byte',
|
|
21
|
+
'word',
|
|
22
|
+
'line',
|
|
23
|
+
];
|
|
24
|
+
function toPosixPath(value) {
|
|
25
|
+
return value.split(path.sep).join('/');
|
|
26
|
+
}
|
|
27
|
+
function sha256Hex(buffer) {
|
|
28
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
function sha256Tagged(buffer) {
|
|
31
|
+
return `sha256:${sha256Hex(buffer)}`;
|
|
32
|
+
}
|
|
33
|
+
function createSegmenter(granularity) {
|
|
34
|
+
const Segmenter = Intl.Segmenter;
|
|
35
|
+
return Segmenter ? new Segmenter(undefined, { granularity }) : null;
|
|
36
|
+
}
|
|
37
|
+
function countGraphemes(text) {
|
|
38
|
+
const segmenter = createSegmenter('grapheme');
|
|
39
|
+
return segmenter ? [...segmenter.segment(text)].length : [...text].length;
|
|
40
|
+
}
|
|
41
|
+
function countWords(text) {
|
|
42
|
+
const segmenter = createSegmenter('word');
|
|
43
|
+
if (segmenter) {
|
|
44
|
+
return [...segmenter.segment(text)].filter((segment) => segment.isWordLike === true).length;
|
|
45
|
+
}
|
|
46
|
+
return text.trim().length === 0 ? 0 : (text.trim().match(/\S+/gu) ?? []).length;
|
|
47
|
+
}
|
|
48
|
+
function countLines(text) {
|
|
49
|
+
return text.length === 0 ? 0 : text.split(/\r\n|\r|\n/u).length;
|
|
50
|
+
}
|
|
51
|
+
export function countTextBudgetUnits(text, unit) {
|
|
52
|
+
switch (unit) {
|
|
53
|
+
case 'grapheme':
|
|
54
|
+
return countGraphemes(text);
|
|
55
|
+
case 'code-point':
|
|
56
|
+
return [...text].length;
|
|
57
|
+
case 'utf16':
|
|
58
|
+
return text.length;
|
|
59
|
+
case 'utf8-byte':
|
|
60
|
+
return Buffer.byteLength(text, 'utf8');
|
|
61
|
+
case 'word':
|
|
62
|
+
return countWords(text);
|
|
63
|
+
case 'line':
|
|
64
|
+
return countLines(text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function decodeJsonPointerSegment(segment) {
|
|
68
|
+
return segment.replace(/~1/gu, '/').replace(/~0/gu, '~');
|
|
69
|
+
}
|
|
70
|
+
function resolveJsonPointer(value, pointer) {
|
|
71
|
+
if (pointer === '') {
|
|
72
|
+
return { ok: true, value };
|
|
73
|
+
}
|
|
74
|
+
if (!pointer.startsWith('/')) {
|
|
75
|
+
return { ok: false, error: 'invalid' };
|
|
76
|
+
}
|
|
77
|
+
let current = value;
|
|
78
|
+
for (const rawSegment of pointer.slice(1).split('/')) {
|
|
79
|
+
if (/~(?![01])/u.test(rawSegment)) {
|
|
80
|
+
return { ok: false, error: 'invalid' };
|
|
81
|
+
}
|
|
82
|
+
const segment = decodeJsonPointerSegment(rawSegment);
|
|
83
|
+
if (Array.isArray(current)) {
|
|
84
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(segment)) {
|
|
85
|
+
return { ok: false, error: 'missing' };
|
|
86
|
+
}
|
|
87
|
+
const index = Number(segment);
|
|
88
|
+
if (index >= current.length) {
|
|
89
|
+
return { ok: false, error: 'missing' };
|
|
90
|
+
}
|
|
91
|
+
current = current[index];
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!current || typeof current !== 'object' || !Object.hasOwn(current, segment)) {
|
|
95
|
+
return { ok: false, error: 'missing' };
|
|
96
|
+
}
|
|
97
|
+
current = current[segment];
|
|
98
|
+
}
|
|
99
|
+
return { ok: true, value: current };
|
|
100
|
+
}
|
|
101
|
+
function makeFinding(code, severity, pathValue, jsonPointer, message, actual = null, expected = null, metric = 'text_length') {
|
|
102
|
+
return {
|
|
103
|
+
code,
|
|
104
|
+
severity,
|
|
105
|
+
message,
|
|
106
|
+
path: pathValue,
|
|
107
|
+
json_pointer: jsonPointer,
|
|
108
|
+
metric,
|
|
109
|
+
actual,
|
|
110
|
+
expected,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function compareAgainstPolicy(relativePath, pointer, value, policy) {
|
|
114
|
+
const findings = [];
|
|
115
|
+
if (policy.exact !== null && value !== policy.exact) {
|
|
116
|
+
findings.push(makeFinding('text_budget_not_exact', 'medium', relativePath, pointer, `${relativePath} has ${value} ${policy.unit}; expected exactly ${policy.exact}.`, value, policy.exact));
|
|
117
|
+
}
|
|
118
|
+
if (policy.min !== null && value < policy.min) {
|
|
119
|
+
findings.push(makeFinding('text_budget_below_min', 'medium', relativePath, pointer, `${relativePath} has ${value} ${policy.unit}; expected at least ${policy.min}.`, value, policy.min));
|
|
120
|
+
}
|
|
121
|
+
if (policy.max !== null && value > policy.max) {
|
|
122
|
+
findings.push(makeFinding('text_budget_above_max', 'medium', relativePath, pointer, `${relativePath} has ${value} ${policy.unit}; expected at most ${policy.max}.`, value, policy.max));
|
|
123
|
+
}
|
|
124
|
+
return findings;
|
|
125
|
+
}
|
|
126
|
+
function createInputHash(policy, artifacts, findings) {
|
|
127
|
+
const inputState = {
|
|
128
|
+
policy,
|
|
129
|
+
artifacts: artifacts.map((artifact) => ({
|
|
130
|
+
path: artifact.path,
|
|
131
|
+
sha256: artifact.sha256,
|
|
132
|
+
size_bytes: artifact.size_bytes,
|
|
133
|
+
})),
|
|
134
|
+
input_errors: findings
|
|
135
|
+
.filter((finding) => ERROR_FINDING_CODES.has(finding.code))
|
|
136
|
+
.map((finding) => ({
|
|
137
|
+
code: finding.code,
|
|
138
|
+
path: finding.path,
|
|
139
|
+
json_pointer: finding.json_pointer,
|
|
140
|
+
})),
|
|
141
|
+
};
|
|
142
|
+
return sha256Tagged(JSON.stringify(inputState));
|
|
143
|
+
}
|
|
144
|
+
function normalizeTargetPath(projectRoot, targetPath) {
|
|
145
|
+
const absolutePath = path.resolve(process.cwd(), targetPath);
|
|
146
|
+
ensureInside(projectRoot, absolutePath);
|
|
147
|
+
const relativePath = toPosixPath(path.relative(projectRoot, absolutePath));
|
|
148
|
+
return {
|
|
149
|
+
absolutePath,
|
|
150
|
+
relativePath: relativePath.length === 0 ? '.' : relativePath,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function inspectTextBudget(projectRoot, options) {
|
|
154
|
+
const root = path.resolve(projectRoot);
|
|
155
|
+
const policy = {
|
|
156
|
+
min: options.min ?? null,
|
|
157
|
+
max: options.max ?? null,
|
|
158
|
+
exact: options.exact ?? null,
|
|
159
|
+
unit: options.unit ?? DEFAULT_TEXT_BUDGET_UNIT,
|
|
160
|
+
json_pointer: options.jsonPointer ?? null,
|
|
161
|
+
max_file_bytes: options.maxFileBytes ?? DEFAULT_MAX_FILE_BYTES,
|
|
162
|
+
};
|
|
163
|
+
const metrics = [];
|
|
164
|
+
const findings = [];
|
|
165
|
+
const artifacts = [];
|
|
166
|
+
const issues = [];
|
|
167
|
+
for (const targetPath of options.paths) {
|
|
168
|
+
let relativePath = targetPath;
|
|
169
|
+
let absolutePath;
|
|
170
|
+
try {
|
|
171
|
+
const normalized = normalizeTargetPath(root, targetPath);
|
|
172
|
+
absolutePath = normalized.absolutePath;
|
|
173
|
+
relativePath = normalized.relativePath;
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
177
|
+
issues.push(message);
|
|
178
|
+
findings.push(makeFinding('text_budget_unreadable', 'high', targetPath, policy.json_pointer, message, null, null, null));
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
let buffer;
|
|
182
|
+
try {
|
|
183
|
+
buffer = readFileInsideWithoutSymlinks(root, absolutePath, { maxBytes: policy.max_file_bytes });
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
187
|
+
issues.push(message);
|
|
188
|
+
findings.push(makeFinding('text_budget_unreadable', 'high', relativePath, policy.json_pointer, message, null, null, null));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const contentSha256 = sha256Tagged(buffer);
|
|
192
|
+
artifacts.push({
|
|
193
|
+
kind: 'input',
|
|
194
|
+
path: relativePath,
|
|
195
|
+
sha256: contentSha256,
|
|
196
|
+
size_bytes: buffer.byteLength,
|
|
197
|
+
});
|
|
198
|
+
const rawText = buffer.toString('utf8');
|
|
199
|
+
let selectedText = rawText;
|
|
200
|
+
if (policy.json_pointer !== null) {
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = JSON.parse(rawText);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
207
|
+
issues.push(`${relativePath}: ${message}`);
|
|
208
|
+
findings.push(makeFinding('text_budget_json_parse_failed', 'high', relativePath, policy.json_pointer, `${relativePath} could not be parsed as JSON: ${message}`, null, null, null));
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const resolution = resolveJsonPointer(parsed, policy.json_pointer);
|
|
212
|
+
if (!resolution.ok) {
|
|
213
|
+
const code = resolution.error === 'invalid' ? 'text_budget_json_pointer_invalid' : 'text_budget_json_pointer_missing';
|
|
214
|
+
const message = resolution.error === 'invalid'
|
|
215
|
+
? `Invalid JSON pointer: ${policy.json_pointer}`
|
|
216
|
+
: `JSON pointer not found: ${policy.json_pointer}`;
|
|
217
|
+
issues.push(`${relativePath}: ${message}`);
|
|
218
|
+
findings.push(makeFinding(code, 'high', relativePath, policy.json_pointer, message, null, null, null));
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (typeof resolution.value !== 'string') {
|
|
222
|
+
const message = `JSON pointer ${policy.json_pointer} does not resolve to a string.`;
|
|
223
|
+
issues.push(`${relativePath}: ${message}`);
|
|
224
|
+
findings.push(makeFinding('text_budget_json_pointer_not_string', 'high', relativePath, policy.json_pointer, message, null, null, null));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
selectedText = resolution.value;
|
|
228
|
+
}
|
|
229
|
+
const value = countTextBudgetUnits(selectedText, policy.unit);
|
|
230
|
+
metrics.push({
|
|
231
|
+
name: 'text_length',
|
|
232
|
+
value,
|
|
233
|
+
unit: policy.unit,
|
|
234
|
+
path: relativePath,
|
|
235
|
+
json_pointer: policy.json_pointer,
|
|
236
|
+
content_sha256: contentSha256,
|
|
237
|
+
});
|
|
238
|
+
findings.push(...compareAgainstPolicy(relativePath, policy.json_pointer, value, policy));
|
|
239
|
+
}
|
|
240
|
+
const status = findings.some((finding) => ERROR_FINDING_CODES.has(finding.code))
|
|
241
|
+
? 'error'
|
|
242
|
+
: findings.length > 0
|
|
243
|
+
? 'failed'
|
|
244
|
+
: 'passed';
|
|
245
|
+
return {
|
|
246
|
+
schema_version: '1',
|
|
247
|
+
command: 'script-pack',
|
|
248
|
+
pack_id: TEXT_BUDGET_PACK_ID,
|
|
249
|
+
script_id: TEXT_BUDGET_SCRIPT_ID,
|
|
250
|
+
script_ref: TEXT_BUDGET_SCRIPT_REF,
|
|
251
|
+
action: 'check',
|
|
252
|
+
status,
|
|
253
|
+
ok: status === 'passed',
|
|
254
|
+
mustflow_root: root,
|
|
255
|
+
policy,
|
|
256
|
+
input_hash: createInputHash(policy, artifacts, findings),
|
|
257
|
+
metrics,
|
|
258
|
+
findings,
|
|
259
|
+
artifacts,
|
|
260
|
+
issues,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createComplexityBudgetReport, createComplexityBudgetReportFromVerification, } from './complexity-budget.js';
|
|
2
|
+
import { createConflictLedger } from './conflict-ledger.js';
|
|
1
3
|
function uniqueSorted(values) {
|
|
2
4
|
return [...new Set([...values].filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
|
|
3
5
|
}
|
|
@@ -138,6 +140,18 @@ function externalEvidenceRemainingRisks(externalEvidenceRisks) {
|
|
|
138
140
|
detail: risk.detail,
|
|
139
141
|
}));
|
|
140
142
|
}
|
|
143
|
+
function riskPricedEvidenceRemainingRisks(assessment) {
|
|
144
|
+
if (!assessment?.manual_review_required) {
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
{
|
|
149
|
+
code: 'risk_priced_evidence_requires_review',
|
|
150
|
+
severity: assessment.level,
|
|
151
|
+
detail: `Verification risk is ${assessment.level}; required evidence: ${assessment.required_evidence.join(', ')}.`,
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}
|
|
141
155
|
export function createVerifyEvidenceModel(input) {
|
|
142
156
|
const requirements = input.report.requirements.map((requirement) => {
|
|
143
157
|
const candidates = input.report.candidates.filter((candidate) => candidate.reason === requirement.reason);
|
|
@@ -173,6 +187,12 @@ export function createVerifyEvidenceModel(input) {
|
|
|
173
187
|
const reproEvidenceRisks = input.reproEvidenceRisks ?? [];
|
|
174
188
|
const externalChecks = input.externalChecks ?? [];
|
|
175
189
|
const externalEvidenceRisks = input.externalEvidenceRisks ?? [];
|
|
190
|
+
const hasSpecificRemainingRisks = sourceAnchorRisks.length > 0 ||
|
|
191
|
+
scopeDiffRisks.length > 0 ||
|
|
192
|
+
repeatedFailureRisks.length > 0 ||
|
|
193
|
+
validationRatchetRisks.length > 0 ||
|
|
194
|
+
reproEvidenceRisks.length > 0 ||
|
|
195
|
+
externalEvidenceRisks.length > 0;
|
|
176
196
|
const gaps = input.report.gaps.map((gap) => ({
|
|
177
197
|
reason: gap.reason,
|
|
178
198
|
intent: null,
|
|
@@ -181,6 +201,15 @@ export function createVerifyEvidenceModel(input) {
|
|
|
181
201
|
files: [...gap.files],
|
|
182
202
|
surfaces: [...gap.surfaces],
|
|
183
203
|
}));
|
|
204
|
+
const remainingRisks = [
|
|
205
|
+
...sourceAnchorRemainingRisks(sourceAnchorRisks),
|
|
206
|
+
...scopeDiffRemainingRisks(scopeDiffRisks),
|
|
207
|
+
...repeatedFailureRemainingRisks(repeatedFailureRisks),
|
|
208
|
+
...validationRatchetRemainingRisks(validationRatchetRisks),
|
|
209
|
+
...reproEvidenceRemainingRisks(reproEvidenceRisks),
|
|
210
|
+
...externalEvidenceRemainingRisks(externalEvidenceRisks),
|
|
211
|
+
...(hasSpecificRemainingRisks ? [] : riskPricedEvidenceRemainingRisks(input.report.risk_assessment)),
|
|
212
|
+
];
|
|
184
213
|
return {
|
|
185
214
|
schema_version: '1',
|
|
186
215
|
source: 'mf_verify',
|
|
@@ -196,14 +225,15 @@ export function createVerifyEvidenceModel(input) {
|
|
|
196
225
|
receipts,
|
|
197
226
|
skipped_checks: skippedCheckEntries(input.results),
|
|
198
227
|
gaps,
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
228
|
+
complexity_budget: createComplexityBudgetReportFromVerification(input.report),
|
|
229
|
+
risk_assessment: input.report.risk_assessment,
|
|
230
|
+
...(input.failureReplayCapsule ? { failure_replay_capsule: input.failureReplayCapsule } : {}),
|
|
231
|
+
remaining_risks: remainingRisks,
|
|
232
|
+
conflict_ledger: createConflictLedger({
|
|
233
|
+
verdict: input.verdict,
|
|
234
|
+
gaps,
|
|
235
|
+
remainingRisks,
|
|
236
|
+
}),
|
|
207
237
|
...(reproEvidence ? { repro_evidence: reproEvidence } : {}),
|
|
208
238
|
...(externalChecks.length > 0 ? { external_checks: externalChecks } : {}),
|
|
209
239
|
explanation: explanationFromVerdict(input.verdict),
|
|
@@ -227,6 +257,11 @@ export function createDashboardEvidenceModel(input) {
|
|
|
227
257
|
const receipts = input.latestReceipt ? [input.latestReceipt] : [];
|
|
228
258
|
const sourceAnchorRisks = input.sourceAnchorRisks ?? [];
|
|
229
259
|
const scopeDiffRisks = input.scopeDiffRisks ?? [];
|
|
260
|
+
const remainingRisks = [
|
|
261
|
+
...input.remainingRisks,
|
|
262
|
+
...sourceAnchorRemainingRisks(sourceAnchorRisks),
|
|
263
|
+
...scopeDiffRemainingRisks(scopeDiffRisks),
|
|
264
|
+
];
|
|
230
265
|
return {
|
|
231
266
|
schema_version: '1',
|
|
232
267
|
source: 'dashboard_export',
|
|
@@ -242,11 +277,24 @@ export function createDashboardEvidenceModel(input) {
|
|
|
242
277
|
receipts,
|
|
243
278
|
skipped_checks: input.skippedChecks,
|
|
244
279
|
gaps: input.gaps,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
280
|
+
complexity_budget: createComplexityBudgetReport({
|
|
281
|
+
files: [],
|
|
282
|
+
summary: {
|
|
283
|
+
fileCount: 0,
|
|
284
|
+
publicSurfaceCount: input.changedSurfaces.length,
|
|
285
|
+
changeKinds: input.changedSurfaces.length > 0 ? ['unknown'] : [],
|
|
286
|
+
validationReasons: [],
|
|
287
|
+
updatePolicies: [],
|
|
288
|
+
driftChecks: [],
|
|
289
|
+
affectedContracts: [],
|
|
290
|
+
},
|
|
291
|
+
}),
|
|
292
|
+
remaining_risks: remainingRisks,
|
|
293
|
+
conflict_ledger: createConflictLedger({
|
|
294
|
+
verdict: input.verdict,
|
|
295
|
+
gaps: input.gaps,
|
|
296
|
+
remainingRisks,
|
|
297
|
+
}),
|
|
250
298
|
explanation: explanationFromVerdict(input.verdict),
|
|
251
299
|
};
|
|
252
300
|
}
|