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.
Files changed (55) hide show
  1. package/README.md +20 -6
  2. package/dist/cli/commands/api.js +17 -0
  3. package/dist/cli/commands/check.js +38 -26
  4. package/dist/cli/commands/doctor.js +17 -5
  5. package/dist/cli/commands/evidence.js +71 -0
  6. package/dist/cli/commands/index.js +24 -9
  7. package/dist/cli/commands/map.js +20 -7
  8. package/dist/cli/commands/run.js +2 -1
  9. package/dist/cli/commands/script-pack.js +124 -0
  10. package/dist/cli/commands/update.js +52 -39
  11. package/dist/cli/commands/verify.js +50 -15
  12. package/dist/cli/commands/workspace.js +2 -0
  13. package/dist/cli/i18n/en.js +38 -0
  14. package/dist/cli/i18n/es.js +38 -0
  15. package/dist/cli/i18n/fr.js +38 -0
  16. package/dist/cli/i18n/hi.js +38 -0
  17. package/dist/cli/i18n/ko.js +38 -0
  18. package/dist/cli/i18n/zh.js +38 -0
  19. package/dist/cli/index.js +1 -0
  20. package/dist/cli/lib/active-command-lock.js +96 -0
  21. package/dist/cli/lib/agent-context.js +179 -10
  22. package/dist/cli/lib/command-registry.js +6 -0
  23. package/dist/cli/lib/dashboard-export.js +1 -0
  24. package/dist/cli/lib/script-pack-registry.js +27 -0
  25. package/dist/cli/script-packs/core-text-budget.js +241 -0
  26. package/dist/core/active-run-locks.js +7 -1
  27. package/dist/core/change-verification.js +10 -0
  28. package/dist/core/completion-verdict.js +14 -1
  29. package/dist/core/complexity-budget.js +206 -0
  30. package/dist/core/conflict-ledger.js +122 -0
  31. package/dist/core/failure-replay-capsule.js +213 -0
  32. package/dist/core/public-json-contracts.js +27 -0
  33. package/dist/core/risk-priced-evidence.js +213 -0
  34. package/dist/core/script-check-result.js +1 -0
  35. package/dist/core/text-budget.js +262 -0
  36. package/dist/core/verification-evidence.js +61 -13
  37. package/package.json +1 -1
  38. package/schemas/README.md +23 -11
  39. package/schemas/change-verification-report.schema.json +29 -0
  40. package/schemas/context-report.schema.json +58 -2
  41. package/schemas/dashboard-export.schema.json +42 -1
  42. package/schemas/diff-risk.schema.json +6 -0
  43. package/schemas/evidence-report.schema.json +45 -0
  44. package/schemas/latest-run-pointer.schema.json +50 -1
  45. package/schemas/script-pack-catalog.schema.json +68 -0
  46. package/schemas/text-budget-report.schema.json +131 -0
  47. package/schemas/verification-plan.schema.json +32 -0
  48. package/schemas/verify-report.schema.json +360 -1
  49. package/schemas/verify-run-manifest.schema.json +50 -1
  50. package/schemas/workspace-verification-plan.schema.json +32 -0
  51. package/templates/default/i18n.toml +2 -2
  52. package/templates/default/locales/en/.mustflow/skills/INDEX.md +2 -2
  53. package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +19 -2
  54. package/templates/default/locales/en/.mustflow/skills/routes.toml +1 -1
  55. 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) => !staleRecordIds.has(record.run_id));
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
- : 'external_evidence_review_required',
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',