mustflow 2.11.0 → 2.17.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.
@@ -3,51 +3,132 @@ const TEXT_FIELD_LABELS = {
3
3
  expected_behavior: 'expected behavior',
4
4
  observed_behavior: 'observed behavior',
5
5
  };
6
- const ITEM_FIELD_LABELS = {
7
- original_reproduction: 'original reproduction path',
8
- evidence_before_fix: 'before-fix evidence',
9
- evidence_after_fix: 'after-fix evidence',
10
- regression_guard: 'regression guard',
11
- };
12
- export function createReproEvidenceRisks(report) {
6
+ function pushRisk(risks, detail, verdictEffect = 'partial') {
7
+ risks.push({
8
+ code: 'repro_evidence_missing',
9
+ severity: verdictEffect === 'contradicted' ? 'critical' : 'high',
10
+ detail,
11
+ verdict_effect: verdictEffect,
12
+ });
13
+ }
14
+ function collectReceiptBindingRisks(phaseLabel, evidence, options, risks) {
15
+ if (!evidence.receipt_path || !evidence.receipt_sha256 || !evidence.verification_plan_id) {
16
+ pushRisk(risks, `Bug-fix repro evidence ${phaseLabel} observation is not bound to receipt_path, receipt_sha256, and verification_plan_id.`);
17
+ return;
18
+ }
19
+ if (options.verificationPlanId && evidence.verification_plan_id !== options.verificationPlanId) {
20
+ pushRisk(risks, `Bug-fix repro evidence ${phaseLabel} receipt is stale for the current verification plan.`);
21
+ }
22
+ }
23
+ function collectBeforeFixRisks(report, options, risks) {
24
+ if (report.before_fix.status === 'missing') {
25
+ pushRisk(risks, 'Bug-fix repro evidence is missing before-fix reproduction; reproduce the original failure or mark it unavailable before claiming verification.');
26
+ return;
27
+ }
28
+ if (report.before_fix.status === 'unavailable') {
29
+ pushRisk(risks, report.before_fix.reason
30
+ ? 'Bug-fix repro evidence marks before-fix reproduction unavailable; the result cannot be verified without the original failure being observed.'
31
+ : 'Bug-fix repro evidence marks before-fix reproduction unavailable without explaining why.');
32
+ return;
33
+ }
34
+ if (!report.before_fix.summary) {
35
+ pushRisk(risks, 'Bug-fix repro evidence reproduced the before-fix failure but does not summarize the evidence.');
36
+ }
37
+ if (report.before_fix.outcome !== 'failed_as_expected') {
38
+ pushRisk(risks, 'Bug-fix repro evidence reproduced the before-fix path without outcome failed_as_expected.');
39
+ }
40
+ collectReceiptBindingRisks('before-fix', report.before_fix, options, risks);
41
+ }
42
+ function collectRouteIdentityRisks(report, risks) {
43
+ if (!report.reproduction_route.route_id) {
44
+ pushRisk(risks, 'Bug-fix repro evidence is missing reproduction_route.route_id.', 'unverified');
45
+ }
46
+ if (!report.reproduction_route.route_kind) {
47
+ pushRisk(risks, 'Bug-fix repro evidence is missing reproduction_route.route_kind.');
48
+ }
49
+ if (!report.reproduction_route.route_digest) {
50
+ pushRisk(risks, 'Bug-fix repro evidence is missing reproduction_route.route_digest.', 'unverified');
51
+ }
52
+ if (!report.reproduction_route.failure_oracle_hash) {
53
+ pushRisk(risks, 'Bug-fix repro evidence is missing reproduction_route.failure_oracle_hash.');
54
+ }
55
+ if (report.reproduction_route.steps.length === 0) {
56
+ pushRisk(risks, 'Bug-fix repro evidence is missing bounded reproduction route steps.', 'unverified');
57
+ }
58
+ }
59
+ function collectAfterFixRisks(report, options, risks) {
60
+ if (report.after_fix.status === 'missing') {
61
+ pushRisk(risks, 'Bug-fix repro evidence is missing after-fix same-route evidence; rerun the original route after the fix before claiming verification.', 'unverified');
62
+ return;
63
+ }
64
+ if (report.after_fix.status === 'unavailable') {
65
+ pushRisk(risks, report.after_fix.reason
66
+ ? 'Bug-fix repro evidence marks after-fix same-route evidence unavailable; the result cannot be verified without a post-fix pass.'
67
+ : 'Bug-fix repro evidence marks after-fix same-route evidence unavailable without explaining why.', 'unverified');
68
+ return;
69
+ }
70
+ if (report.after_fix.status === 'failed') {
71
+ pushRisk(risks, 'Bug-fix repro evidence says the after-fix route still failed.', 'contradicted');
72
+ return;
73
+ }
74
+ if (!report.after_fix.summary) {
75
+ pushRisk(risks, 'Bug-fix repro evidence marks after-fix evidence passed but does not summarize the evidence.');
76
+ }
77
+ if (report.after_fix.outcome !== 'passed_expected_behavior') {
78
+ pushRisk(risks, 'Bug-fix repro evidence marks after-fix evidence passed without outcome passed_expected_behavior.', 'unverified');
79
+ }
80
+ if (!report.after_fix.same_route_as) {
81
+ pushRisk(risks, 'Bug-fix repro evidence marks after-fix evidence passed without same_route_as.', 'unverified');
82
+ }
83
+ if (report.reproduction_route.route_id &&
84
+ report.after_fix.same_route_as &&
85
+ report.after_fix.same_route_as !== report.reproduction_route.route_id) {
86
+ pushRisk(risks, 'Bug-fix repro evidence after_fix.same_route_as does not match reproduction_route.route_id.');
87
+ }
88
+ collectReceiptBindingRisks('after-fix', report.after_fix, options, risks);
89
+ }
90
+ function collectRegressionGuardRisks(report, options, risks) {
91
+ if (report.regression_guard.status === 'missing') {
92
+ pushRisk(risks, 'Bug-fix repro evidence is missing a regression guard; add or identify the guard before claiming verification.');
93
+ return;
94
+ }
95
+ if (report.regression_guard.status === 'unavailable') {
96
+ pushRisk(risks, report.regression_guard.reason
97
+ ? 'Bug-fix repro evidence marks the regression guard unavailable; the result cannot be verified without a guard or explicit limitation.'
98
+ : 'Bug-fix repro evidence marks the regression guard unavailable without explaining why.');
99
+ return;
100
+ }
101
+ if (report.regression_guard.status === 'failed') {
102
+ pushRisk(risks, 'Bug-fix repro evidence says the regression guard failed.', 'contradicted');
103
+ return;
104
+ }
105
+ if (!report.regression_guard.summary) {
106
+ pushRisk(risks, 'Bug-fix repro evidence marks the regression guard passed but does not summarize the evidence.');
107
+ }
108
+ if (!report.regression_guard.intent && !report.regression_guard.test_path) {
109
+ pushRisk(risks, 'Bug-fix repro evidence marks the regression guard passed without an intent or test path.');
110
+ }
111
+ collectReceiptBindingRisks('regression-guard', report.regression_guard, options, risks);
112
+ }
113
+ export function createReproEvidenceRisks(report, options = {}) {
13
114
  if (!report) {
14
115
  return [];
15
116
  }
16
117
  const risks = [];
17
118
  for (const [field, label] of Object.entries(TEXT_FIELD_LABELS)) {
18
119
  if (!report[field]) {
19
- risks.push({
20
- code: 'repro_evidence_missing',
21
- severity: 'high',
22
- detail: `Bug-fix repro evidence is missing ${label}; do not mark the task verified from command receipts alone.`,
23
- });
24
- }
25
- }
26
- for (const [field, label] of Object.entries(ITEM_FIELD_LABELS)) {
27
- const item = report[field];
28
- if (item.status === 'missing') {
29
- risks.push({
30
- code: 'repro_evidence_missing',
31
- severity: 'high',
32
- detail: `Bug-fix repro evidence is missing ${label}; rerun or explicitly mark it unavailable before claiming verification.`,
33
- });
34
- continue;
35
- }
36
- if (item.status === 'present' && !item.summary) {
37
- risks.push({
38
- code: 'repro_evidence_missing',
39
- severity: 'high',
40
- detail: `Bug-fix repro evidence marks ${label} present but does not summarize the evidence.`,
41
- });
42
- continue;
43
- }
44
- if (item.status === 'unavailable' && !item.reason) {
45
- risks.push({
46
- code: 'repro_evidence_missing',
47
- severity: 'high',
48
- detail: `Bug-fix repro evidence marks ${label} unavailable without explaining why.`,
49
- });
120
+ pushRisk(risks, `Bug-fix repro evidence is missing ${label}; do not mark the task verified from command receipts alone.`);
50
121
  }
51
122
  }
123
+ collectRouteIdentityRisks(report, risks);
124
+ collectBeforeFixRisks(report, options, risks);
125
+ collectAfterFixRisks(report, options, risks);
126
+ collectRegressionGuardRisks(report, options, risks);
52
127
  return risks;
53
128
  }
129
+ export function countReproEvidenceVerdictEffects(risks) {
130
+ return {
131
+ contradicted: risks.filter((risk) => risk.verdict_effect === 'contradicted').length,
132
+ unverified: risks.filter((risk) => risk.verdict_effect === 'unverified').length,
133
+ };
134
+ }
@@ -1,10 +1,33 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import { existsSync, readFileSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  const TEST_CHANGE_KINDS = new Set(['test', 'test_fixture']);
4
5
  const SKIP_OR_ONLY_MARKER = /\b(?:describe|it|test)\s*\.\s*(?:skip|only)\s*\(/u;
6
+ const TODO_OR_PENDING_MARKER = /\b(?:describe|it|test)\s*\.\s*(?:todo|pending)\s*\(/u;
7
+ const ASSERTION_CALL = /\b(?:assert(?:\.\w+)?|expect)\s*\(/u;
8
+ const STRONG_ASSERTION = /\b(?:assert\.(?:equal|deepEqual|strictEqual|throws|rejects)|to(?:Equal|StrictEqual|Be|Throw)|throws|rejects)\b/u;
9
+ const WEAK_ASSERTION = /\b(?:assert\.ok|toBeDefined|toBeTruthy|toBeFalsy)\b/u;
10
+ const NEGATIVE_ASSERTION = /\b(?:notEqual|notDeepEqual|doesNotMatch|doesNotThrow|\.not\.)\b/u;
11
+ const EXCEPTION_ASSERTION = /\b(?:assert\.(?:throws|rejects|doesNotThrow|doesNotReject)|toThrow|rejects|throws)\b/u;
12
+ const SNAPSHOT_PATH = /(?:^|\/)(?:__snapshots__\/|snapshots\/)|\.snap$/u;
13
+ const GOLDEN_PATH = /(?:^|\/)(?:golden|fixtures|expected)(?:\/|-)|(?:\.golden\.|\.expected\.)/u;
14
+ const JAVASCRIPT_TEST_PATH = /(?:^|\/)[^/]+\.(?:test|spec)\.[cm]?[jt]sx?$/u;
15
+ const COMMAND_CONTRACT_PATH = '.mustflow/config/commands.toml';
16
+ const TEST_SELECTION_PATTERN = /(?:--(?:grep|testNamePattern|testPathPattern|runTestsByPath|test-name-pattern)|\bgrep\s*=|\btestNamePattern\b|\btestPathPattern\b)/u;
17
+ const COMMAND_ALLOWS_NO_TESTS_PATTERN = /(?:passWithNoTests|--pass-with-no-tests|--passWithNoTests)/u;
18
+ const COMMAND_FORCES_SNAPSHOT_UPDATE_PATTERN = /(?:updateSnapshot|--update-snapshot|--updateSnapshot|\s-u(?:\s|"))/u;
19
+ const COMMAND_HIDES_FAILURE_PATTERN = /(?:\|\|\s*true|;\s*true\b|&&\s*true\b|\bexit\s+0\b)/u;
20
+ const CRITICAL_RATCHET_CODES = new Set([
21
+ 'success_exit_codes_widened',
22
+ 'command_allows_no_tests',
23
+ 'command_hides_failure',
24
+ ]);
5
25
  function isTestClassification(classification) {
6
26
  return classification.surface.category === 'test' || classification.changeKinds.some((kind) => TEST_CHANGE_KINDS.has(kind));
7
27
  }
28
+ function isJavaScriptTestPath(relativePath) {
29
+ return JAVASCRIPT_TEST_PATH.test(relativePath);
30
+ }
8
31
  function resolveInsideRoot(projectRoot, relativePath) {
9
32
  const resolvedPath = path.resolve(projectRoot, relativePath);
10
33
  const relative = path.relative(projectRoot, resolvedPath);
@@ -25,27 +48,148 @@ function fileTextIfReadable(projectRoot, relativePath) {
25
48
  return null;
26
49
  }
27
50
  }
51
+ function gitDiffLines(projectRoot, relativePath) {
52
+ const result = spawnSync('git', ['diff', '--no-ext-diff', '--unified=0', '--', relativePath], {
53
+ cwd: projectRoot,
54
+ encoding: 'utf8',
55
+ windowsHide: true,
56
+ });
57
+ if (result.status !== 0 || typeof result.stdout !== 'string' || result.stdout.length === 0) {
58
+ return { added: [], removed: [] };
59
+ }
60
+ const added = [];
61
+ const removed = [];
62
+ for (const line of result.stdout.split(/\r?\n/u)) {
63
+ if (line.startsWith('+++') || line.startsWith('---')) {
64
+ continue;
65
+ }
66
+ if (line.startsWith('+')) {
67
+ added.push(line.slice(1));
68
+ }
69
+ else if (line.startsWith('-')) {
70
+ removed.push(line.slice(1));
71
+ }
72
+ }
73
+ return { added, removed };
74
+ }
75
+ function countMatching(lines, pattern) {
76
+ return lines.filter((line) => pattern.test(line)).length;
77
+ }
78
+ function hasAny(lines, pattern) {
79
+ return lines.some((line) => pattern.test(line));
80
+ }
81
+ function extractCoverageNumbers(lines) {
82
+ return lines
83
+ .filter((line) => /\b(?:coverage|threshold|branches|functions|lines|statements)\b/iu.test(line))
84
+ .flatMap((line) => [...line.matchAll(/\b\d+(?:\.\d+)?\b/gu)].map((match) => Number(match[0])))
85
+ .filter((value) => Number.isFinite(value));
86
+ }
87
+ function isCommandContractPath(relativePath) {
88
+ return relativePath === COMMAND_CONTRACT_PATH;
89
+ }
90
+ function isValidationConfigPath(relativePath) {
91
+ return (isCommandContractPath(relativePath) ||
92
+ /(?:^|\/)(?:package\.json|jest\.config\.[cm]?[jt]s|vitest\.config\.[cm]?[jt]s|nyc\.config\.[cm]?[jt]s)$/u.test(relativePath) ||
93
+ /\bcoverage\b/u.test(relativePath));
94
+ }
95
+ function riskDetail(pathText, message) {
96
+ return `Changed validation path ${pathText} ${message}`;
97
+ }
98
+ export function countValidationRatchetVerdictEffects(risks) {
99
+ return {
100
+ contradicted: risks.filter((risk) => risk.severity === 'critical' && CRITICAL_RATCHET_CODES.has(risk.code)).length,
101
+ };
102
+ }
28
103
  export function createValidationRatchetRisks(report, projectRoot) {
29
104
  const risks = [];
30
- for (const classification of report.classifications.filter(isTestClassification)) {
105
+ const seenRisks = new Set();
106
+ function addRisk(code, severity, pathText, detail) {
107
+ const key = `${pathText}\0${code}`;
108
+ if (seenRisks.has(key)) {
109
+ return;
110
+ }
111
+ seenRisks.add(key);
112
+ risks.push({ code, severity, path: pathText, detail });
113
+ }
114
+ for (const classification of report.classifications) {
31
115
  const resolvedPath = resolveInsideRoot(projectRoot, classification.path);
32
- if (report.source === 'changed' && (resolvedPath === null || !existsSync(resolvedPath))) {
33
- risks.push({
34
- code: 'related_test_deleted',
35
- severity: 'high',
36
- path: classification.path,
37
- detail: `Changed test path ${classification.path} is absent; deleted related tests require review before marking the task verified.`,
38
- });
39
- continue;
116
+ const diff = report.source === 'changed' ? gitDiffLines(projectRoot, classification.path) : { added: [], removed: [] };
117
+ const addedText = diff.added.join('\n');
118
+ if (isTestClassification(classification)) {
119
+ if (report.source === 'changed' && (resolvedPath === null || !existsSync(resolvedPath))) {
120
+ addRisk('related_test_deleted', 'high', classification.path, `Changed test path ${classification.path} is absent; deleted related tests require review before marking the task verified.`);
121
+ continue;
122
+ }
123
+ if (isJavaScriptTestPath(classification.path)) {
124
+ const fileText = fileTextIfReadable(projectRoot, classification.path);
125
+ if (fileText !== null && SKIP_OR_ONLY_MARKER.test(fileText)) {
126
+ addRisk('skip_or_only_marker_present', 'medium', classification.path, `Changed test path ${classification.path} contains a .skip or .only marker; review whether validation was weakened before marking the task verified.`);
127
+ }
128
+ if ((fileText !== null && TODO_OR_PENDING_MARKER.test(fileText)) || TODO_OR_PENDING_MARKER.test(addedText)) {
129
+ addRisk('todo_or_pending_marker_added', 'medium', classification.path, `Changed test path ${classification.path} contains a todo or pending marker; review whether validation was deferred before marking the task verified.`);
130
+ }
131
+ const removedAssertionCount = countMatching(diff.removed, ASSERTION_CALL);
132
+ const addedAssertionCount = countMatching(diff.added, ASSERTION_CALL);
133
+ if (removedAssertionCount > addedAssertionCount) {
134
+ addRisk('assertion_count_decreased', 'high', classification.path, riskDetail(classification.path, `removes ${removedAssertionCount} assertion line(s) and adds ${addedAssertionCount}; review whether validation strength decreased.`));
135
+ }
136
+ if (hasAny(diff.removed, STRONG_ASSERTION) && hasAny(diff.added, WEAK_ASSERTION)) {
137
+ addRisk('assertion_matcher_weakened', 'high', classification.path, riskDetail(classification.path, 'replaces a stronger assertion with a weaker presence or truthiness assertion.'));
138
+ }
139
+ if (hasAny(diff.removed, NEGATIVE_ASSERTION)) {
140
+ addRisk('negative_assertion_removed', 'high', classification.path, riskDetail(classification.path, 'removes a negative assertion; confirm the denied behavior is still covered.'));
141
+ }
142
+ if (hasAny(diff.removed, EXCEPTION_ASSERTION)) {
143
+ addRisk('exception_assertion_removed', 'high', classification.path, riskDetail(classification.path, 'removes an exception assertion; confirm failure behavior is still covered.'));
144
+ }
145
+ }
146
+ if (SNAPSHOT_PATH.test(classification.path) && diff.added.length + diff.removed.length >= 20) {
147
+ addRisk('snapshot_mass_updated', 'medium', classification.path, riskDetail(classification.path, 'changes a large snapshot region; review that the update does not hide a regression.'));
148
+ }
149
+ if (GOLDEN_PATH.test(`${classification.path} `) && diff.added.length + diff.removed.length >= 20) {
150
+ addRisk('golden_output_replaced', 'medium', classification.path, riskDetail(classification.path, 'replaces a broad golden or expected-output region; review the behavioral reason.'));
151
+ }
152
+ }
153
+ if (isCommandContractPath(classification.path)) {
154
+ if (hasAny(diff.added, /\bstatus\s*=\s*"(?:manual_only|disabled|unknown)"/u)) {
155
+ addRisk('verification_intent_disabled', 'high', classification.path, riskDetail(classification.path, 'adds a non-runnable verification intent status.'));
156
+ }
157
+ if (hasAny(diff.removed, /\brequired_after\s*=/u) && !hasAny(diff.added, /\brequired_after\s*=/u)) {
158
+ addRisk('verification_required_after_removed', 'high', classification.path, riskDetail(classification.path, 'removes a required_after mapping from the command contract.'));
159
+ }
160
+ if (hasAny(diff.added, /\bsuccess_exit_codes\s*=\s*\[[^\]]*(?:[1-9]\d*|true)[^\]]*\]/u)) {
161
+ addRisk('success_exit_codes_widened', 'critical', classification.path, riskDetail(classification.path, 'widens success exit codes beyond the normal zero-exit contract.'));
162
+ }
163
+ if (hasAny(diff.added, COMMAND_ALLOWS_NO_TESTS_PATTERN)) {
164
+ addRisk('command_allows_no_tests', 'critical', classification.path, riskDetail(classification.path, 'allows a test command to pass when no tests run.'));
165
+ }
166
+ if (hasAny(diff.added, COMMAND_FORCES_SNAPSHOT_UPDATE_PATTERN)) {
167
+ addRisk('command_forces_snapshot_update', 'medium', classification.path, riskDetail(classification.path, 'adds snapshot update behavior to a verification command.'));
168
+ }
169
+ if (hasAny(diff.added, COMMAND_HIDES_FAILURE_PATTERN)) {
170
+ addRisk('command_hides_failure', 'critical', classification.path, riskDetail(classification.path, 'adds shell behavior that can hide command failure.'));
171
+ }
40
172
  }
41
- const fileText = fileTextIfReadable(projectRoot, classification.path);
42
- if (fileText !== null && SKIP_OR_ONLY_MARKER.test(fileText)) {
43
- risks.push({
44
- code: 'skip_or_only_marker_present',
45
- severity: 'medium',
46
- path: classification.path,
47
- detail: `Changed test path ${classification.path} contains a .skip or .only marker; review whether validation was weakened before marking the task verified.`,
48
- });
173
+ if (isValidationConfigPath(classification.path)) {
174
+ const removedCoverageNumbers = extractCoverageNumbers(diff.removed);
175
+ const addedCoverageNumbers = extractCoverageNumbers(diff.added);
176
+ if (removedCoverageNumbers.length > 0 &&
177
+ addedCoverageNumbers.length > 0 &&
178
+ Math.min(...addedCoverageNumbers) < Math.max(...removedCoverageNumbers)) {
179
+ addRisk('coverage_threshold_lowered', 'medium', classification.path, riskDetail(classification.path, 'lowers a coverage-related numeric threshold.'));
180
+ }
181
+ if (hasAny(diff.added, COMMAND_ALLOWS_NO_TESTS_PATTERN)) {
182
+ addRisk('command_allows_no_tests', 'critical', classification.path, riskDetail(classification.path, 'allows a validation script to pass when no tests run.'));
183
+ }
184
+ if (hasAny(diff.added, COMMAND_FORCES_SNAPSHOT_UPDATE_PATTERN)) {
185
+ addRisk('command_forces_snapshot_update', 'medium', classification.path, riskDetail(classification.path, 'adds snapshot update behavior to a validation script.'));
186
+ }
187
+ if (hasAny(diff.added, COMMAND_HIDES_FAILURE_PATTERN)) {
188
+ addRisk('command_hides_failure', 'critical', classification.path, riskDetail(classification.path, 'adds shell behavior that can hide validation failure.'));
189
+ }
190
+ if (hasAny(diff.added, TEST_SELECTION_PATTERN)) {
191
+ addRisk('test_selection_narrowed', 'medium', classification.path, riskDetail(classification.path, 'adds a test-selection filter; confirm coverage still matches the change.'));
192
+ }
49
193
  }
50
194
  }
51
195
  return risks;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mustflow",
3
- "version": "2.11.0",
3
+ "version": "2.17.0",
4
4
  "description": "Agent workflow documents and CLI for mustflow repository roots.",
5
5
  "type": "module",
6
6
  "license": "MIT-0",
@@ -70,9 +70,9 @@
70
70
  "workflow"
71
71
  ],
72
72
  "devDependencies": {
73
- "@types/node": "^25.6.2",
73
+ "@types/node": "^25.8.0",
74
74
  "@types/sql.js": "^1.4.11",
75
- "fast-check": "^4.7.0",
75
+ "fast-check": "^4.8.0",
76
76
  "typescript": "^6.0.3"
77
77
  },
78
78
  "dependencies": {
@@ -326,6 +326,7 @@
326
326
  "source",
327
327
  "verification_plan_id",
328
328
  "changed_file_count",
329
+ "criteria",
329
330
  "matched_intents",
330
331
  "ran_intents",
331
332
  "passed_intents",
@@ -337,6 +338,14 @@
337
338
  "scope_diff_risk_count",
338
339
  "repeated_failure_count",
339
340
  "validation_ratchet_risk_count",
341
+ "repro_evidence_risk_count",
342
+ "external_evidence_risk_count",
343
+ "write_drift_risk_count",
344
+ "receipt_binding_risk_count",
345
+ "stale_receipt_count",
346
+ "plan_mismatch_count",
347
+ "risks",
348
+ "receipt_binding",
340
349
  "latest_run_status"
341
350
  ],
342
351
  "properties": {
@@ -346,6 +355,7 @@
346
355
  "pattern": "^sha256:[0-9a-f]{64}$"
347
356
  },
348
357
  "changed_file_count": { "type": ["integer", "null"] },
358
+ "criteria": { "$ref": "#/$defs/completionVerdictCriteria" },
349
359
  "matched_intents": { "type": "integer" },
350
360
  "ran_intents": { "type": "integer" },
351
361
  "passed_intents": { "type": "integer" },
@@ -357,6 +367,14 @@
357
367
  "scope_diff_risk_count": { "type": "integer" },
358
368
  "repeated_failure_count": { "type": "integer" },
359
369
  "validation_ratchet_risk_count": { "type": "integer" },
370
+ "repro_evidence_risk_count": { "type": "integer" },
371
+ "external_evidence_risk_count": { "type": "integer" },
372
+ "write_drift_risk_count": { "type": "integer" },
373
+ "receipt_binding_risk_count": { "type": "integer" },
374
+ "stale_receipt_count": { "type": "integer" },
375
+ "plan_mismatch_count": { "type": "integer" },
376
+ "risks": { "$ref": "#/$defs/completionVerdictRisks" },
377
+ "receipt_binding": { "$ref": "#/$defs/completionVerdictReceiptBinding" },
360
378
  "latest_run_status": { "type": ["string", "null"] }
361
379
  }
362
380
  },
@@ -374,6 +392,71 @@
374
392
  }
375
393
  }
376
394
  },
395
+ "completionVerdictCriteria": {
396
+ "type": "object",
397
+ "additionalProperties": false,
398
+ "required": ["total", "covered", "partially_covered", "uncovered", "blocked", "contradicted"],
399
+ "properties": {
400
+ "total": { "type": "integer" },
401
+ "covered": { "type": "integer" },
402
+ "partially_covered": { "type": "integer" },
403
+ "uncovered": { "type": "integer" },
404
+ "blocked": { "type": "integer" },
405
+ "contradicted": { "type": "integer" }
406
+ }
407
+ },
408
+ "completionVerdictRisks": {
409
+ "type": "object",
410
+ "additionalProperties": false,
411
+ "required": [
412
+ "source_anchor",
413
+ "scope_diff",
414
+ "repeated_failure",
415
+ "validation_ratchet",
416
+ "repro_evidence",
417
+ "external_evidence",
418
+ "write_drift",
419
+ "receipt_binding",
420
+ "stale_receipt",
421
+ "plan_mismatch"
422
+ ],
423
+ "properties": {
424
+ "source_anchor": { "type": "integer" },
425
+ "scope_diff": { "type": "integer" },
426
+ "repeated_failure": { "type": "integer" },
427
+ "validation_ratchet": { "type": "integer" },
428
+ "repro_evidence": { "type": "integer" },
429
+ "external_evidence": { "type": "integer" },
430
+ "write_drift": { "type": "integer" },
431
+ "receipt_binding": { "type": "integer" },
432
+ "stale_receipt": { "type": "integer" },
433
+ "plan_mismatch": { "type": "integer" }
434
+ }
435
+ },
436
+ "completionVerdictReceiptBinding": {
437
+ "type": "object",
438
+ "additionalProperties": false,
439
+ "required": [
440
+ "plan_bound_count",
441
+ "plan_unbound_count",
442
+ "fingerprint_bound_count",
443
+ "fingerprint_unbound_count",
444
+ "current_state_bound_count",
445
+ "current_state_unavailable_count",
446
+ "stale_count",
447
+ "plan_mismatch_count"
448
+ ],
449
+ "properties": {
450
+ "plan_bound_count": { "type": "integer" },
451
+ "plan_unbound_count": { "type": "integer" },
452
+ "fingerprint_bound_count": { "type": "integer" },
453
+ "fingerprint_unbound_count": { "type": "integer" },
454
+ "current_state_bound_count": { "type": "integer" },
455
+ "current_state_unavailable_count": { "type": "integer" },
456
+ "stale_count": { "type": "integer" },
457
+ "plan_mismatch_count": { "type": "integer" }
458
+ }
459
+ },
377
460
  "harnessReport": {
378
461
  "type": "object",
379
462
  "additionalProperties": false,