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
|
@@ -6,6 +6,7 @@ import { readUtf8FileInsideWithoutSymlinks, writeJsonFileInsideWithoutSymlinks,
|
|
|
6
6
|
const ACTIVE_LOCK_SCHEMA_VERSION = '1';
|
|
7
7
|
const ACTIVE_LOCK_KIND = 'active_run_lock';
|
|
8
8
|
const LOCK_ROOT_RELATIVE_PATH = '.mustflow/state/locks';
|
|
9
|
+
export const ACTIVE_RUN_LOCK_ID_ENV = 'MUSTFLOW_ACTIVE_RUN_LOCK_ID';
|
|
9
10
|
const LOCK_MUTEX_STALE_MS = 30_000;
|
|
10
11
|
const LOCK_MUTEX_WAIT_MS = 1_000;
|
|
11
12
|
const LOCK_MUTEX_SLEEP_MS = 25;
|
|
@@ -462,7 +463,12 @@ export function acquireActiveRunLock(projectRoot, contract, intentName, options
|
|
|
462
463
|
}
|
|
463
464
|
}
|
|
464
465
|
const staleRecordIds = new Set(staleRecords.map((stale) => stale.runId));
|
|
465
|
-
const liveRecords = records.filter((record) =>
|
|
466
|
+
const liveRecords = records.filter((record) => {
|
|
467
|
+
if (staleRecordIds.has(record.run_id)) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
return record.run_id !== options.ignoreRunId || record.pid !== options.ignorePid;
|
|
471
|
+
});
|
|
466
472
|
const conflicts = findConflicts(intentName, effects, liveRecords);
|
|
467
473
|
if (conflicts.length > 0) {
|
|
468
474
|
return { ok: false, conflicts, recoveredStaleRecords: staleRecords };
|
|
@@ -4,6 +4,7 @@ import { classifyVerificationCandidate, createVerificationPlan, } from './verifi
|
|
|
4
4
|
import { createVerificationDecisionGraph, } from './verification-decision-graph.js';
|
|
5
5
|
import { createVerificationSchedule, } from './verification-scheduler.js';
|
|
6
6
|
import { createProjectTestSelectionPlan, } from './test-selection.js';
|
|
7
|
+
import { createVerificationRiskAssessment, } from './risk-priced-evidence.js';
|
|
7
8
|
export const CHANGE_VERIFICATION_SCHEMA_VERSION = '1';
|
|
8
9
|
function uniqueSorted(values) {
|
|
9
10
|
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
@@ -328,6 +329,14 @@ export function createChangeVerificationReport(classificationReport, commandCont
|
|
|
328
329
|
const gaps = requirements
|
|
329
330
|
.map((requirement) => gapForRequirement(requirement, candidates))
|
|
330
331
|
.filter((gap) => gap !== null);
|
|
332
|
+
const riskAssessment = createVerificationRiskAssessment({
|
|
333
|
+
classificationSummary: classificationReport.summary,
|
|
334
|
+
requirements,
|
|
335
|
+
candidates,
|
|
336
|
+
gaps,
|
|
337
|
+
commandContract,
|
|
338
|
+
selectedIntents: schedule.entries.map((entry) => entry.intent),
|
|
339
|
+
});
|
|
331
340
|
return {
|
|
332
341
|
schema_version: CHANGE_VERIFICATION_SCHEMA_VERSION,
|
|
333
342
|
source: classificationReport.source,
|
|
@@ -336,6 +345,7 @@ export function createChangeVerificationReport(classificationReport, commandCont
|
|
|
336
345
|
requirements,
|
|
337
346
|
candidates,
|
|
338
347
|
gaps,
|
|
348
|
+
risk_assessment: riskAssessment,
|
|
339
349
|
schedule,
|
|
340
350
|
decision_graph: createVerificationDecisionGraph(commandContract, requirements, candidates, gaps, schedule),
|
|
341
351
|
test_selection: testSelectionPlan.report,
|
|
@@ -10,6 +10,7 @@ function createRiskEvidence(input) {
|
|
|
10
10
|
receipt_binding: input.receiptBindingRiskCount ?? 0,
|
|
11
11
|
stale_receipt: input.staleReceiptCount ?? 0,
|
|
12
12
|
plan_mismatch: input.planMismatchCount ?? 0,
|
|
13
|
+
risk_priced_evidence: input.riskPricedEvidenceRiskCount ?? 0,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
16
|
function emptyReceiptBindingEvidence() {
|
|
@@ -158,6 +159,9 @@ function verifyStatus(input) {
|
|
|
158
159
|
if ((input.externalEvidenceRiskCount ?? 0) > 0) {
|
|
159
160
|
downgradeLimitations.push('external_evidence_requires_review');
|
|
160
161
|
}
|
|
162
|
+
if ((input.riskPricedEvidenceRiskCount ?? 0) > 0) {
|
|
163
|
+
downgradeLimitations.push('risk_priced_evidence_requires_review');
|
|
164
|
+
}
|
|
161
165
|
if (downgradeLimitations.length > 0) {
|
|
162
166
|
return {
|
|
163
167
|
status: 'partially_verified',
|
|
@@ -175,7 +179,9 @@ function verifyStatus(input) {
|
|
|
175
179
|
? 'stale_receipt_review_required'
|
|
176
180
|
: (input.reproEvidenceRiskCount ?? 0) > 0
|
|
177
181
|
? 'repro_evidence_missing'
|
|
178
|
-
:
|
|
182
|
+
: (input.externalEvidenceRiskCount ?? 0) > 0
|
|
183
|
+
? 'external_evidence_review_required'
|
|
184
|
+
: 'risk_priced_evidence_review_required',
|
|
179
185
|
blockers: [],
|
|
180
186
|
contradictions: [],
|
|
181
187
|
limitations: downgradeLimitations,
|
|
@@ -230,6 +236,7 @@ export function createVerifyCompletionVerdict(input) {
|
|
|
230
236
|
receipt_binding_risk_count: normalizedInput.receiptBindingRiskCount ?? 0,
|
|
231
237
|
stale_receipt_count: normalizedInput.staleReceiptCount ?? 0,
|
|
232
238
|
plan_mismatch_count: normalizedInput.planMismatchCount ?? 0,
|
|
239
|
+
risk_priced_evidence_risk_count: normalizedInput.riskPricedEvidenceRiskCount ?? 0,
|
|
233
240
|
risks,
|
|
234
241
|
receipt_binding: receiptBinding,
|
|
235
242
|
latest_run_status: null,
|
|
@@ -269,6 +276,11 @@ export function createDashboardCompletionVerdict(input) {
|
|
|
269
276
|
primaryReason = 'scope_diff_review_required';
|
|
270
277
|
limitations.push('scope_diff_risk_requires_review');
|
|
271
278
|
}
|
|
279
|
+
else if ((input.riskPricedEvidenceRiskCount ?? 0) > 0) {
|
|
280
|
+
status = 'partially_verified';
|
|
281
|
+
primaryReason = 'risk_priced_evidence_review_required';
|
|
282
|
+
limitations.push('risk_priced_evidence_requires_review');
|
|
283
|
+
}
|
|
272
284
|
else if (input.gapCount > 0) {
|
|
273
285
|
status = 'blocked';
|
|
274
286
|
primaryReason = 'verification_gaps_present';
|
|
@@ -331,6 +343,7 @@ export function createDashboardCompletionVerdict(input) {
|
|
|
331
343
|
receipt_binding_risk_count: input.receiptBindingRiskCount ?? 0,
|
|
332
344
|
stale_receipt_count: input.staleReceiptCount ?? 0,
|
|
333
345
|
plan_mismatch_count: input.planMismatchCount ?? 0,
|
|
346
|
+
risk_priced_evidence_risk_count: input.riskPricedEvidenceRiskCount ?? 0,
|
|
334
347
|
risks,
|
|
335
348
|
receipt_binding: receiptBinding,
|
|
336
349
|
latest_run_status: input.latestRunStatus,
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
export const COMPLEXITY_BUDGET_DEFAULTS = {
|
|
2
|
+
scoreBudget: 12,
|
|
3
|
+
maxConfigSurfaces: 2,
|
|
4
|
+
maxSchemaSurfaces: 2,
|
|
5
|
+
maxPathsPerRisk: 8,
|
|
6
|
+
};
|
|
7
|
+
const CHANGE_KIND_FAMILY_BY_KIND = {
|
|
8
|
+
documentation: 'documentation',
|
|
9
|
+
example: 'documentation',
|
|
10
|
+
translation: 'documentation',
|
|
11
|
+
workflow: 'workflow',
|
|
12
|
+
host_instruction: 'workflow',
|
|
13
|
+
installed_template: 'template',
|
|
14
|
+
package_metadata: 'release',
|
|
15
|
+
schema: 'contract',
|
|
16
|
+
test: 'test',
|
|
17
|
+
test_fixture: 'test',
|
|
18
|
+
implementation: 'implementation',
|
|
19
|
+
unknown: 'unknown',
|
|
20
|
+
};
|
|
21
|
+
const DEPENDENCY_SURFACE_FILENAMES = new Set([
|
|
22
|
+
'package.json',
|
|
23
|
+
'package-lock.json',
|
|
24
|
+
'pnpm-lock.yaml',
|
|
25
|
+
'bun.lock',
|
|
26
|
+
'bun.lockb',
|
|
27
|
+
'yarn.lock',
|
|
28
|
+
'Cargo.toml',
|
|
29
|
+
'Cargo.lock',
|
|
30
|
+
'go.mod',
|
|
31
|
+
'go.sum',
|
|
32
|
+
'pyproject.toml',
|
|
33
|
+
'pom.xml',
|
|
34
|
+
'build.gradle',
|
|
35
|
+
'deno.json',
|
|
36
|
+
'deno.jsonc',
|
|
37
|
+
]);
|
|
38
|
+
const CONFIG_SURFACE_FILENAMES = new Set(['Makefile', 'justfile', 'Dockerfile']);
|
|
39
|
+
const CONFIG_FILE_PATTERN = /(?:config|rc)\.(?:cjs|cts|js|json|mjs|mts|toml|ts|yaml|yml)$/u;
|
|
40
|
+
const HELPER_FILE_PATTERN = /.*(?:helper|helpers|util|utils|manager|common|misc).*\.(?:cjs|cts|go|java|js|jsx|kt|mjs|mts|php|ps1|py|rb|rs|swift|ts|tsx)$/u;
|
|
41
|
+
const REQUIREMENTS_FILE_PATTERN = /^requirements(?:-[^/]*)?\.txt$/u;
|
|
42
|
+
const TSCONFIG_FILE_PATTERN = /^tsconfig[^/]*\.json$/u;
|
|
43
|
+
function uniqueSorted(values) {
|
|
44
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
45
|
+
}
|
|
46
|
+
function changeKindFamily(kind) {
|
|
47
|
+
return CHANGE_KIND_FAMILY_BY_KIND[kind] ?? kind;
|
|
48
|
+
}
|
|
49
|
+
function baseName(file) {
|
|
50
|
+
const slashIndex = file.lastIndexOf('/');
|
|
51
|
+
return slashIndex === -1 ? file : file.slice(slashIndex + 1);
|
|
52
|
+
}
|
|
53
|
+
function isDependencySurface(file) {
|
|
54
|
+
const name = baseName(file);
|
|
55
|
+
return DEPENDENCY_SURFACE_FILENAMES.has(name) || REQUIREMENTS_FILE_PATTERN.test(name);
|
|
56
|
+
}
|
|
57
|
+
function isConfigurationSurface(file) {
|
|
58
|
+
const name = baseName(file);
|
|
59
|
+
return (file.startsWith('.github/') ||
|
|
60
|
+
file.startsWith('.mustflow/config/') ||
|
|
61
|
+
CONFIG_SURFACE_FILENAMES.has(name) ||
|
|
62
|
+
CONFIG_FILE_PATTERN.test(name) ||
|
|
63
|
+
TSCONFIG_FILE_PATTERN.test(name));
|
|
64
|
+
}
|
|
65
|
+
function isHelperContainer(file) {
|
|
66
|
+
return HELPER_FILE_PATTERN.test(baseName(file));
|
|
67
|
+
}
|
|
68
|
+
function isSchemaSurface(file) {
|
|
69
|
+
return file.startsWith('schemas/') && file.endsWith('.schema.json');
|
|
70
|
+
}
|
|
71
|
+
function matchingPaths(files, predicate) {
|
|
72
|
+
return files.filter(predicate);
|
|
73
|
+
}
|
|
74
|
+
function firstPaths(paths) {
|
|
75
|
+
return paths.slice(0, COMPLEXITY_BUDGET_DEFAULTS.maxPathsPerRisk);
|
|
76
|
+
}
|
|
77
|
+
function metric(name, value, weight) {
|
|
78
|
+
return {
|
|
79
|
+
name,
|
|
80
|
+
value,
|
|
81
|
+
weight,
|
|
82
|
+
weighted_score: value * weight,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function scoreMetrics(input) {
|
|
86
|
+
const families = uniqueSorted(input.summary.changeKinds.map(changeKindFamily));
|
|
87
|
+
const dependencySurfaces = matchingPaths(input.files, isDependencySurface);
|
|
88
|
+
const configSurfaces = matchingPaths(input.files, isConfigurationSurface);
|
|
89
|
+
const helperContainers = matchingPaths(input.files, isHelperContainer);
|
|
90
|
+
const schemaSurfaces = matchingPaths(input.files, isSchemaSurface);
|
|
91
|
+
return [
|
|
92
|
+
metric('changed_files', input.summary.fileCount, 1),
|
|
93
|
+
metric('public_surfaces', input.summary.publicSurfaceCount, 2),
|
|
94
|
+
metric('change_kind_families_beyond_one', Math.max(0, families.length - 1), 2),
|
|
95
|
+
metric('dependency_surfaces', dependencySurfaces.length, 5),
|
|
96
|
+
metric('configuration_surfaces', configSurfaces.length, 3),
|
|
97
|
+
metric('helper_containers', helperContainers.length, 3),
|
|
98
|
+
metric('schema_surfaces', schemaSurfaces.length, 2),
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
function risk(input) {
|
|
102
|
+
return {
|
|
103
|
+
code: input.code,
|
|
104
|
+
severity: 'warning',
|
|
105
|
+
detail: input.detail,
|
|
106
|
+
count: input.count,
|
|
107
|
+
limit: input.limit,
|
|
108
|
+
paths: firstPaths(input.paths),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function createComplexityBudgetRisks(input, score) {
|
|
112
|
+
const dependencySurfaces = matchingPaths(input.files, isDependencySurface);
|
|
113
|
+
const configSurfaces = matchingPaths(input.files, isConfigurationSurface);
|
|
114
|
+
const helperContainers = matchingPaths(input.files, isHelperContainer);
|
|
115
|
+
const schemaSurfaces = matchingPaths(input.files, isSchemaSurface);
|
|
116
|
+
const risks = [];
|
|
117
|
+
if (score > COMPLEXITY_BUDGET_DEFAULTS.scoreBudget) {
|
|
118
|
+
risks.push(risk({
|
|
119
|
+
code: 'complexity_score_budget_exceeded',
|
|
120
|
+
detail: `Complexity score ${score} exceeds the review budget of ${COMPLEXITY_BUDGET_DEFAULTS.scoreBudget}.`,
|
|
121
|
+
count: score,
|
|
122
|
+
limit: COMPLEXITY_BUDGET_DEFAULTS.scoreBudget,
|
|
123
|
+
paths: input.files,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
if (dependencySurfaces.length > 0) {
|
|
127
|
+
risks.push(risk({
|
|
128
|
+
code: 'dependency_surface_requires_justification',
|
|
129
|
+
detail: [
|
|
130
|
+
'Dependency or package manifest changes add operational cost;',
|
|
131
|
+
'justify the dependency boundary and rollback path.',
|
|
132
|
+
].join(' '),
|
|
133
|
+
count: dependencySurfaces.length,
|
|
134
|
+
limit: 0,
|
|
135
|
+
paths: dependencySurfaces,
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
if (helperContainers.length > 0) {
|
|
139
|
+
risks.push(risk({
|
|
140
|
+
code: 'helper_container_requires_justification',
|
|
141
|
+
detail: [
|
|
142
|
+
'Helper, util, common, manager, or misc files tend to hide broad abstractions;',
|
|
143
|
+
'justify why the new surface reduces net complexity.',
|
|
144
|
+
].join(' '),
|
|
145
|
+
count: helperContainers.length,
|
|
146
|
+
limit: 0,
|
|
147
|
+
paths: helperContainers,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
if (configSurfaces.length > COMPLEXITY_BUDGET_DEFAULTS.maxConfigSurfaces) {
|
|
151
|
+
risks.push(risk({
|
|
152
|
+
code: 'configuration_surface_budget_exceeded',
|
|
153
|
+
detail: [
|
|
154
|
+
`Configuration surface count ${configSurfaces.length}`,
|
|
155
|
+
`exceeds the review budget of ${COMPLEXITY_BUDGET_DEFAULTS.maxConfigSurfaces}.`,
|
|
156
|
+
].join(' '),
|
|
157
|
+
count: configSurfaces.length,
|
|
158
|
+
limit: COMPLEXITY_BUDGET_DEFAULTS.maxConfigSurfaces,
|
|
159
|
+
paths: configSurfaces,
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
if (schemaSurfaces.length > COMPLEXITY_BUDGET_DEFAULTS.maxSchemaSurfaces) {
|
|
163
|
+
risks.push(risk({
|
|
164
|
+
code: 'schema_surface_budget_exceeded',
|
|
165
|
+
detail: [
|
|
166
|
+
`Schema surface count ${schemaSurfaces.length}`,
|
|
167
|
+
`exceeds the review budget of ${COMPLEXITY_BUDGET_DEFAULTS.maxSchemaSurfaces}.`,
|
|
168
|
+
].join(' '),
|
|
169
|
+
count: schemaSurfaces.length,
|
|
170
|
+
limit: COMPLEXITY_BUDGET_DEFAULTS.maxSchemaSurfaces,
|
|
171
|
+
paths: schemaSurfaces,
|
|
172
|
+
}));
|
|
173
|
+
}
|
|
174
|
+
return risks;
|
|
175
|
+
}
|
|
176
|
+
function reviewPrompts(risks) {
|
|
177
|
+
if (risks.length === 0) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
return [
|
|
181
|
+
'Explain why this added complexity is necessary for the requested outcome.',
|
|
182
|
+
'Name the simpler alternative that was rejected and why it is not enough.',
|
|
183
|
+
'Identify the rollback or removal path if the added surface proves unnecessary.',
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
export function createComplexityBudgetReport(input) {
|
|
187
|
+
const metrics = scoreMetrics(input);
|
|
188
|
+
const score = metrics.reduce((total, entry) => total + entry.weighted_score, 0);
|
|
189
|
+
const risks = createComplexityBudgetRisks(input, score);
|
|
190
|
+
return {
|
|
191
|
+
schema_version: '1',
|
|
192
|
+
source: 'change_classification',
|
|
193
|
+
status: risks.length > 0 ? 'review_required' : 'within_budget',
|
|
194
|
+
score,
|
|
195
|
+
budget: COMPLEXITY_BUDGET_DEFAULTS.scoreBudget,
|
|
196
|
+
metrics,
|
|
197
|
+
risks,
|
|
198
|
+
review_prompts: reviewPrompts(risks),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
export function createComplexityBudgetReportFromVerification(report) {
|
|
202
|
+
return createComplexityBudgetReport({
|
|
203
|
+
files: report.files,
|
|
204
|
+
summary: report.classification_summary,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
function hashConflictIdentity(value) {
|
|
3
|
+
return `conflict:${createHash('sha256').update(JSON.stringify(value)).digest('hex').slice(0, 16)}`;
|
|
4
|
+
}
|
|
5
|
+
function uniqueSorted(values) {
|
|
6
|
+
return [...new Set([...values].filter((value) => typeof value === 'string' && value.length > 0))].sort((left, right) => left.localeCompare(right));
|
|
7
|
+
}
|
|
8
|
+
function remainingRiskSeverity(severity) {
|
|
9
|
+
if (severity === 'critical') {
|
|
10
|
+
return 'critical';
|
|
11
|
+
}
|
|
12
|
+
if (severity === 'high' || severity === 'error') {
|
|
13
|
+
return 'error';
|
|
14
|
+
}
|
|
15
|
+
if (severity === 'medium' || severity === 'warning') {
|
|
16
|
+
return 'warning';
|
|
17
|
+
}
|
|
18
|
+
return 'info';
|
|
19
|
+
}
|
|
20
|
+
function createConflictItem(input) {
|
|
21
|
+
return {
|
|
22
|
+
id: hashConflictIdentity({
|
|
23
|
+
kind: input.kind,
|
|
24
|
+
summary: input.summary,
|
|
25
|
+
detail: input.detail,
|
|
26
|
+
sources: input.sources,
|
|
27
|
+
}),
|
|
28
|
+
status: 'open',
|
|
29
|
+
owner: 'user_or_maintainer',
|
|
30
|
+
expires_at: null,
|
|
31
|
+
...input,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function verdictItem(kind, key) {
|
|
35
|
+
const contradiction = kind === 'contradiction';
|
|
36
|
+
return createConflictItem({
|
|
37
|
+
kind,
|
|
38
|
+
severity: contradiction ? 'critical' : 'error',
|
|
39
|
+
summary: key,
|
|
40
|
+
detail: contradiction
|
|
41
|
+
? `Completion evidence is contradicted by ${key}.`
|
|
42
|
+
: `Completion is blocked by ${key}.`,
|
|
43
|
+
sources: [
|
|
44
|
+
{
|
|
45
|
+
kind: 'completion_verdict',
|
|
46
|
+
authority: 'verdict',
|
|
47
|
+
key,
|
|
48
|
+
paths: [],
|
|
49
|
+
surfaces: [],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
affected_paths: [],
|
|
53
|
+
affected_surfaces: [],
|
|
54
|
+
blocks_completion: true,
|
|
55
|
+
resolution: contradiction
|
|
56
|
+
? 'Resolve the contradictory evidence and rerun verification before claiming completion.'
|
|
57
|
+
: 'Resolve the blocker and provide fresh verification evidence before claiming completion.',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function gapItem(gap) {
|
|
61
|
+
const key = gap.reason ?? gap.intent ?? gap.status ?? 'verification_gap';
|
|
62
|
+
return createConflictItem({
|
|
63
|
+
kind: 'verification_gap',
|
|
64
|
+
severity: 'error',
|
|
65
|
+
summary: key,
|
|
66
|
+
detail: gap.detail ?? 'Verification coverage is missing for this requirement.',
|
|
67
|
+
sources: [
|
|
68
|
+
{
|
|
69
|
+
kind: 'verification_gap',
|
|
70
|
+
authority: 'verification_plan',
|
|
71
|
+
key,
|
|
72
|
+
paths: uniqueSorted(gap.files),
|
|
73
|
+
surfaces: uniqueSorted(gap.surfaces),
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
affected_paths: uniqueSorted(gap.files),
|
|
77
|
+
affected_surfaces: uniqueSorted(gap.surfaces),
|
|
78
|
+
blocks_completion: true,
|
|
79
|
+
resolution: 'Add or run an allowed verification intent that covers this gap, then refresh the evidence model.',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function riskItem(risk) {
|
|
83
|
+
const severity = remainingRiskSeverity(risk.severity);
|
|
84
|
+
return createConflictItem({
|
|
85
|
+
kind: 'remaining_risk',
|
|
86
|
+
severity,
|
|
87
|
+
summary: risk.code,
|
|
88
|
+
detail: risk.detail,
|
|
89
|
+
sources: [
|
|
90
|
+
{
|
|
91
|
+
kind: 'remaining_risk',
|
|
92
|
+
authority: 'evidence_model',
|
|
93
|
+
key: risk.code,
|
|
94
|
+
paths: [],
|
|
95
|
+
surfaces: [],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
affected_paths: [],
|
|
99
|
+
affected_surfaces: [],
|
|
100
|
+
blocks_completion: severity === 'error' || severity === 'critical',
|
|
101
|
+
resolution: 'Review the remaining risk and attach stronger evidence or an explicit acceptance decision.',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
export function createConflictLedger(input) {
|
|
105
|
+
const items = [
|
|
106
|
+
...input.verdict.blockers.map((blocker) => verdictItem('blocker', blocker)),
|
|
107
|
+
...input.verdict.contradictions.map((contradiction) => verdictItem('contradiction', contradiction)),
|
|
108
|
+
...input.gaps.map(gapItem),
|
|
109
|
+
...input.remainingRisks.map(riskItem),
|
|
110
|
+
];
|
|
111
|
+
const blockingCount = items.filter((item) => item.blocks_completion).length;
|
|
112
|
+
const contradictionCount = items.filter((item) => item.kind === 'contradiction').length;
|
|
113
|
+
return {
|
|
114
|
+
schema_version: '1',
|
|
115
|
+
source: 'verification_evidence_model',
|
|
116
|
+
status: items.length > 0 ? 'open' : 'clear',
|
|
117
|
+
item_count: items.length,
|
|
118
|
+
blocking_count: blockingCount,
|
|
119
|
+
contradiction_count: contradictionCount,
|
|
120
|
+
items,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -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',
|