mustflow 2.69.0 → 2.74.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +19 -5
  2. package/dist/cli/commands/api.js +17 -0
  3. package/dist/cli/commands/evidence.js +71 -0
  4. package/dist/cli/commands/script-pack.js +124 -0
  5. package/dist/cli/commands/verify.js +50 -15
  6. package/dist/cli/commands/workspace.js +2 -0
  7. package/dist/cli/i18n/en.js +38 -0
  8. package/dist/cli/i18n/es.js +38 -0
  9. package/dist/cli/i18n/fr.js +38 -0
  10. package/dist/cli/i18n/hi.js +38 -0
  11. package/dist/cli/i18n/ko.js +38 -0
  12. package/dist/cli/i18n/zh.js +38 -0
  13. package/dist/cli/index.js +1 -0
  14. package/dist/cli/lib/agent-context.js +179 -10
  15. package/dist/cli/lib/command-registry.js +6 -0
  16. package/dist/cli/lib/dashboard-export.js +1 -0
  17. package/dist/cli/lib/script-pack-registry.js +27 -0
  18. package/dist/cli/script-packs/core-text-budget.js +241 -0
  19. package/dist/core/change-verification.js +10 -0
  20. package/dist/core/completion-verdict.js +14 -1
  21. package/dist/core/complexity-budget.js +206 -0
  22. package/dist/core/conflict-ledger.js +122 -0
  23. package/dist/core/failure-replay-capsule.js +213 -0
  24. package/dist/core/public-json-contracts.js +27 -0
  25. package/dist/core/risk-priced-evidence.js +213 -0
  26. package/dist/core/script-check-result.js +1 -0
  27. package/dist/core/text-budget.js +262 -0
  28. package/dist/core/verification-evidence.js +61 -13
  29. package/package.json +1 -1
  30. package/schemas/README.md +23 -11
  31. package/schemas/change-verification-report.schema.json +29 -0
  32. package/schemas/context-report.schema.json +58 -2
  33. package/schemas/dashboard-export.schema.json +42 -1
  34. package/schemas/diff-risk.schema.json +6 -0
  35. package/schemas/evidence-report.schema.json +45 -0
  36. package/schemas/latest-run-pointer.schema.json +50 -1
  37. package/schemas/script-pack-catalog.schema.json +68 -0
  38. package/schemas/text-budget-report.schema.json +131 -0
  39. package/schemas/verification-plan.schema.json +32 -0
  40. package/schemas/verify-report.schema.json +360 -1
  41. package/schemas/verify-run-manifest.schema.json +50 -1
  42. package/schemas/workspace-verification-plan.schema.json +32 -0
  43. package/templates/default/i18n.toml +8 -2
  44. package/templates/default/locales/en/.mustflow/skills/INDEX.md +3 -2
  45. package/templates/default/locales/en/.mustflow/skills/adapter-boundary/SKILL.md +19 -2
  46. package/templates/default/locales/en/.mustflow/skills/design-implementation-handoff/SKILL.md +250 -0
  47. package/templates/default/locales/en/.mustflow/skills/routes.toml +7 -1
  48. package/templates/default/manifest.toml +8 -1
@@ -0,0 +1,241 @@
1
+ import { printUsageError, renderHelp } from '../lib/cli-output.js';
2
+ import { t } from '../lib/i18n.js';
3
+ import { formatCliOptionParseError, getParsedCliStringOption, hasCliOptionToken, hasParsedCliOption, parseCliOptions, } from '../lib/option-parser.js';
4
+ import { resolveMustflowRoot } from '../lib/project-root.js';
5
+ import { inspectTextBudget, TEXT_BUDGET_SCRIPT_REF, TEXT_BUDGET_UNITS, } from '../../core/text-budget.js';
6
+ const TEXT_BUDGET_OPTIONS = [
7
+ { name: '--json', kind: 'boolean' },
8
+ { name: '--min', kind: 'string' },
9
+ { name: '--max', kind: 'string' },
10
+ { name: '--exact', kind: 'string' },
11
+ { name: '--unit', kind: 'string' },
12
+ { name: '--json-pointer', kind: 'string' },
13
+ ];
14
+ export function getCoreTextBudgetHelp(lang = 'en') {
15
+ return renderHelp({
16
+ usage: 'mf script-pack run core/text-budget check <path...> [options]',
17
+ summary: t(lang, 'textBudget.help.summary'),
18
+ options: [
19
+ { label: '--min <count>', description: t(lang, 'textBudget.help.option.min') },
20
+ { label: '--max <count>', description: t(lang, 'textBudget.help.option.max') },
21
+ { label: '--exact <count>', description: t(lang, 'textBudget.help.option.exact') },
22
+ { label: '--unit <unit>', description: t(lang, 'textBudget.help.option.unit') },
23
+ { label: '--json-pointer <pointer>', description: t(lang, 'textBudget.help.option.jsonPointer') },
24
+ { label: '--json', description: t(lang, 'cli.option.json') },
25
+ { label: '-h, --help', description: t(lang, 'cli.option.help') },
26
+ ],
27
+ examples: [
28
+ 'mf script-pack run core/text-budget check README.md --max 5000',
29
+ 'mf script-pack run core/text-budget check package.json --json-pointer /description --max 80 --json',
30
+ 'mf script-pack run core/text-budget check docs/intro.md --min 20 --max 120 --unit word',
31
+ ],
32
+ exitCodes: [
33
+ { label: '0', description: t(lang, 'textBudget.help.exit.ok') },
34
+ { label: '1', description: t(lang, 'textBudget.help.exit.fail') },
35
+ ],
36
+ }, lang);
37
+ }
38
+ function parseNonNegativeInteger(value, option, lang) {
39
+ if (value === null) {
40
+ return { value: null };
41
+ }
42
+ if (!/^(?:0|[1-9]\d*)$/u.test(value)) {
43
+ return { value: null, error: t(lang, 'textBudget.error.invalidNumber', { option, value }) };
44
+ }
45
+ const parsed = Number(value);
46
+ if (!Number.isSafeInteger(parsed)) {
47
+ return { value: null, error: t(lang, 'textBudget.error.invalidNumber', { option, value }) };
48
+ }
49
+ return { value: parsed };
50
+ }
51
+ function parseTextBudgetUnit(value, lang) {
52
+ if (value === null) {
53
+ return { value: 'grapheme' };
54
+ }
55
+ if (TEXT_BUDGET_UNITS.includes(value)) {
56
+ return { value: value };
57
+ }
58
+ return {
59
+ value: 'grapheme',
60
+ error: t(lang, 'textBudget.error.invalidUnit', { unit: value, allowed: TEXT_BUDGET_UNITS.join(', ') }),
61
+ };
62
+ }
63
+ function parseTextBudgetOptions(args, lang) {
64
+ const [action, ...rest] = args;
65
+ const parsed = parseCliOptions(rest, TEXT_BUDGET_OPTIONS, {
66
+ allowPositionals: true,
67
+ allowEmptyStringValues: true,
68
+ });
69
+ const json = hasParsedCliOption(parsed, '--json');
70
+ const min = parseNonNegativeInteger(getParsedCliStringOption(parsed, '--min'), '--min', lang);
71
+ const max = parseNonNegativeInteger(getParsedCliStringOption(parsed, '--max'), '--max', lang);
72
+ const exact = parseNonNegativeInteger(getParsedCliStringOption(parsed, '--exact'), '--exact', lang);
73
+ const unit = parseTextBudgetUnit(getParsedCliStringOption(parsed, '--unit'), lang);
74
+ const jsonPointer = getParsedCliStringOption(parsed, '--json-pointer');
75
+ if (action !== 'check') {
76
+ return {
77
+ action: 'check',
78
+ json,
79
+ paths: parsed.positionals,
80
+ min: min.value,
81
+ max: max.value,
82
+ exact: exact.value,
83
+ unit: unit.value,
84
+ jsonPointer,
85
+ error: action ? t(lang, 'textBudget.error.unknownAction', { action }) : t(lang, 'textBudget.error.missingAction'),
86
+ };
87
+ }
88
+ if (parsed.error) {
89
+ return {
90
+ action,
91
+ json,
92
+ paths: parsed.positionals,
93
+ min: min.value,
94
+ max: max.value,
95
+ exact: exact.value,
96
+ unit: unit.value,
97
+ jsonPointer,
98
+ error: formatCliOptionParseError(parsed.error, lang),
99
+ };
100
+ }
101
+ for (const candidate of [min, max, exact, unit]) {
102
+ if (candidate.error) {
103
+ return {
104
+ action,
105
+ json,
106
+ paths: parsed.positionals,
107
+ min: min.value,
108
+ max: max.value,
109
+ exact: exact.value,
110
+ unit: unit.value,
111
+ jsonPointer,
112
+ error: candidate.error,
113
+ };
114
+ }
115
+ }
116
+ if (parsed.positionals.length === 0) {
117
+ return {
118
+ action,
119
+ json,
120
+ paths: parsed.positionals,
121
+ min: min.value,
122
+ max: max.value,
123
+ exact: exact.value,
124
+ unit: unit.value,
125
+ jsonPointer,
126
+ error: t(lang, 'textBudget.error.missingPath'),
127
+ };
128
+ }
129
+ if (exact.value !== null && (min.value !== null || max.value !== null)) {
130
+ return {
131
+ action,
132
+ json,
133
+ paths: parsed.positionals,
134
+ min: min.value,
135
+ max: max.value,
136
+ exact: exact.value,
137
+ unit: unit.value,
138
+ jsonPointer,
139
+ error: t(lang, 'textBudget.error.exactConflict'),
140
+ };
141
+ }
142
+ if (exact.value === null && min.value === null && max.value === null) {
143
+ return {
144
+ action,
145
+ json,
146
+ paths: parsed.positionals,
147
+ min: min.value,
148
+ max: max.value,
149
+ exact: exact.value,
150
+ unit: unit.value,
151
+ jsonPointer,
152
+ error: t(lang, 'textBudget.error.missingBudget'),
153
+ };
154
+ }
155
+ if (min.value !== null && max.value !== null && min.value > max.value) {
156
+ return {
157
+ action,
158
+ json,
159
+ paths: parsed.positionals,
160
+ min: min.value,
161
+ max: max.value,
162
+ exact: exact.value,
163
+ unit: unit.value,
164
+ jsonPointer,
165
+ error: t(lang, 'textBudget.error.minGreaterThanMax'),
166
+ };
167
+ }
168
+ return {
169
+ action,
170
+ json,
171
+ paths: parsed.positionals,
172
+ min: min.value,
173
+ max: max.value,
174
+ exact: exact.value,
175
+ unit: unit.value,
176
+ jsonPointer,
177
+ };
178
+ }
179
+ function renderBudget(report) {
180
+ if (report.policy.exact !== null) {
181
+ return `exact ${report.policy.exact} ${report.policy.unit}`;
182
+ }
183
+ const lower = report.policy.min === null ? null : `min ${report.policy.min}`;
184
+ const upper = report.policy.max === null ? null : `max ${report.policy.max}`;
185
+ return [lower, upper].filter((part) => part !== null).join(', ') + ` ${report.policy.unit}`;
186
+ }
187
+ function renderTextBudgetSummary(report, lang) {
188
+ const lines = [
189
+ t(lang, 'textBudget.title'),
190
+ `${t(lang, 'scriptPack.label.script')}: ${TEXT_BUDGET_SCRIPT_REF}`,
191
+ `${t(lang, 'label.status')}: ${report.status}`,
192
+ `${t(lang, 'textBudget.label.budget')}: ${renderBudget(report)}`,
193
+ `${t(lang, 'textBudget.label.checkedTargets')}: ${report.metrics.length}`,
194
+ `${t(lang, 'textBudget.label.findings')}: ${report.findings.length}`,
195
+ ];
196
+ if (report.metrics.length > 0) {
197
+ lines.push(t(lang, 'textBudget.label.metrics'));
198
+ for (const metric of report.metrics) {
199
+ const pointer = metric.json_pointer === null ? '' : ` ${metric.json_pointer}`;
200
+ lines.push(`- ${metric.path}${pointer}: ${metric.value} ${metric.unit}`);
201
+ }
202
+ }
203
+ if (report.findings.length > 0) {
204
+ lines.push(t(lang, 'textBudget.label.findings'));
205
+ for (const finding of report.findings) {
206
+ lines.push(`- ${finding.path}: ${finding.code} (${finding.message})`);
207
+ }
208
+ }
209
+ if (report.issues.length > 0) {
210
+ lines.push(t(lang, 'textBudget.label.issues'), ...report.issues.map((issue) => `- ${issue}`));
211
+ }
212
+ if (report.findings.length === 0 && report.issues.length === 0) {
213
+ lines.push(t(lang, 'textBudget.clean'));
214
+ }
215
+ return lines.join('\n');
216
+ }
217
+ export function runCoreTextBudgetScript(args, reporter, lang = 'en') {
218
+ if (hasCliOptionToken(args, '--help', ['-h'])) {
219
+ reporter.stdout(getCoreTextBudgetHelp(lang));
220
+ return 0;
221
+ }
222
+ const options = parseTextBudgetOptions(args, lang);
223
+ if (options.error) {
224
+ printUsageError(reporter, options.error, 'mf script-pack run core/text-budget --help', getCoreTextBudgetHelp(lang), lang);
225
+ return 1;
226
+ }
227
+ const report = inspectTextBudget(resolveMustflowRoot(), {
228
+ paths: options.paths,
229
+ min: options.min,
230
+ max: options.max,
231
+ exact: options.exact,
232
+ unit: options.unit,
233
+ jsonPointer: options.jsonPointer,
234
+ });
235
+ if (options.json) {
236
+ reporter.stdout(JSON.stringify(report, null, 2));
237
+ return report.ok ? 0 : 1;
238
+ }
239
+ reporter.stdout(renderTextBudgetSummary(report, lang));
240
+ return report.ok ? 0 : 1;
241
+ }
@@ -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
+ }