ai-maestro 1.0.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 (81) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +15 -0
  3. package/README.md +727 -0
  4. package/bin/maestro.mjs +246 -0
  5. package/package.json +29 -0
  6. package/src/checkpoint.mjs +102 -0
  7. package/src/codex-diagnostics.mjs +682 -0
  8. package/src/codex-home-inspect.mjs +252 -0
  9. package/src/codex-home.mjs +258 -0
  10. package/src/commands.mjs +809 -0
  11. package/src/config.mjs +11 -0
  12. package/src/debug.mjs +164 -0
  13. package/src/encoding-guard.mjs +125 -0
  14. package/src/engines/ai-router-engine.mjs +37 -0
  15. package/src/engines/codex-engine.mjs +21 -0
  16. package/src/engines/engine.mjs +21 -0
  17. package/src/engines/registry.mjs +29 -0
  18. package/src/files.mjs +65 -0
  19. package/src/format.mjs +132 -0
  20. package/src/lock.mjs +439 -0
  21. package/src/memory-log.mjs +18 -0
  22. package/src/memory.mjs +1 -0
  23. package/src/orchestration/attempt-chain-runtime.mjs +627 -0
  24. package/src/orchestration/budget-manager.mjs +121 -0
  25. package/src/orchestration/capability-registry.mjs +108 -0
  26. package/src/orchestration/consolidator.mjs +772 -0
  27. package/src/orchestration/delegation-executor.mjs +262 -0
  28. package/src/orchestration/delegation-manager.mjs +116 -0
  29. package/src/orchestration/deployment-intent.mjs +36 -0
  30. package/src/orchestration/engine-history.mjs +110 -0
  31. package/src/orchestration/engine-policy.mjs +73 -0
  32. package/src/orchestration/engine-selector.mjs +187 -0
  33. package/src/orchestration/execution-context.mjs +45 -0
  34. package/src/orchestration/failure-classifier.mjs +136 -0
  35. package/src/orchestration/failure-evidence.mjs +237 -0
  36. package/src/orchestration/fallback-chain-lock.mjs +217 -0
  37. package/src/orchestration/fallback-chain-trail.mjs +218 -0
  38. package/src/orchestration/fallback-executor.mjs +146 -0
  39. package/src/orchestration/fallback-graph.mjs +106 -0
  40. package/src/orchestration/fallback-recommendation.mjs +73 -0
  41. package/src/orchestration/file-conflict-detector.mjs +126 -0
  42. package/src/orchestration/host-validation.mjs +241 -0
  43. package/src/orchestration/orchestration-loop.mjs +1971 -0
  44. package/src/orchestration/orchestration-runtime.mjs +1019 -0
  45. package/src/orchestration/orchestration-scheduler.mjs +135 -0
  46. package/src/orchestration/orchestration-trail.mjs +192 -0
  47. package/src/orchestration/preflight.mjs +212 -0
  48. package/src/orchestration/provider-health.mjs +566 -0
  49. package/src/orchestration/provider-router.mjs +352 -0
  50. package/src/orchestration/rc-check-adapters.mjs +817 -0
  51. package/src/orchestration/rc-check.mjs +544 -0
  52. package/src/orchestration/rc-policy.mjs +200 -0
  53. package/src/orchestration/runtime-gate.mjs +146 -0
  54. package/src/orchestration/sector-managers.mjs +215 -0
  55. package/src/orchestration/self-hosting-canary.mjs +231 -0
  56. package/src/orchestration/self-hosting-readiness.mjs +244 -0
  57. package/src/orchestration/spec-planner.mjs +877 -0
  58. package/src/orchestration/subtask-planner.mjs +176 -0
  59. package/src/orchestration/task-classifier.mjs +241 -0
  60. package/src/orchestration/verifier.mjs +1608 -0
  61. package/src/orchestration-commands.mjs +279 -0
  62. package/src/planner.mjs +38 -0
  63. package/src/project.mjs +1 -0
  64. package/src/run-diagnostics.mjs +641 -0
  65. package/src/runner.mjs +243 -0
  66. package/src/safety.mjs +116 -0
  67. package/src/schema.mjs +371 -0
  68. package/src/session-commands.mjs +521 -0
  69. package/src/shell.mjs +93 -0
  70. package/src/smart-planner.mjs +249 -0
  71. package/src/task-commands.mjs +1182 -0
  72. package/src/tasks.mjs +134 -0
  73. package/src/ui/commands.mjs +76 -0
  74. package/src/ui/events.mjs +45 -0
  75. package/src/ui/public/app.js +600 -0
  76. package/src/ui/public/index.html +88 -0
  77. package/src/ui/public/style.css +460 -0
  78. package/src/ui/server.mjs +112 -0
  79. package/src/ui/state.mjs +504 -0
  80. package/src/usage.mjs +178 -0
  81. package/src/workspace-diff.mjs +228 -0
@@ -0,0 +1,1608 @@
1
+ import { evaluateBudget } from './budget-manager.mjs';
2
+ import { isNestedSandboxProcessFailure } from './failure-classifier.mjs';
3
+
4
+ const VERIFIER_SCHEMA_VERSION = 1;
5
+ const NEW_VERIFIER_OUTCOMES = ['verified', 'verified_with_warnings', 'rejected', 'inconclusive', 'blocked', 'needs_human'];
6
+ const EVIDENCE_SOURCE_TYPES = [
7
+ 'run-status',
8
+ 'syntax-validation',
9
+ 'workspace-diff',
10
+ 'test-result',
11
+ 'host-validation',
12
+ 'failure-classification',
13
+ 'verifier-result',
14
+ 'diagnostics',
15
+ 'worker-report',
16
+ 'other'
17
+ ];
18
+ const ACCEPTANCE_STATES = ['satisfied', 'partially_satisfied', 'not_satisfied', 'not_verifiable', 'invalid'];
19
+ const ACCEPTANCE_MODES = [
20
+ 'file_exists',
21
+ 'file_changed',
22
+ 'file_not_changed',
23
+ 'syntax_passed',
24
+ 'tests_passed',
25
+ 'no_forbidden_files',
26
+ 'no_unexpected_changes',
27
+ 'evidence_present',
28
+ 'host_validation_passed',
29
+ 'manual',
30
+ 'text_only'
31
+ ];
32
+ const RECOMMENDATION_ACTIONS = ['complete', 'verify', 'fallback', 'fix', 'ask_human', 'block', 'none'];
33
+
34
+ export const VERIFIER_OUTCOMES = NEW_VERIFIER_OUTCOMES;
35
+
36
+ // Deterministic layer: reuses the same signals src/task-commands.mjs already
37
+ // computes today (syntax validation, workspace-diff guards, tool-call
38
+ // detection) — this is not a new detection mechanism, it's a shared verdict
39
+ // format over the existing ones so risk>=medium tasks get a structured
40
+ // verified/rejected/inconclusive call instead of just a task status string.
41
+ export function runDeterministicVerification({
42
+ exitCode,
43
+ syntaxValidationFailed,
44
+ analysisTaskChangedCode,
45
+ deletedOutOfScope,
46
+ unsupportedToolCall,
47
+ suspectedFalsePositive,
48
+ windowsShellCommandMismatch,
49
+ testsRun,
50
+ testsPassed
51
+ } = {}) {
52
+ const failures = [];
53
+ const warnings = [];
54
+ const signal = arguments[0] || {};
55
+ const output = [signal.output, signal.stdout, signal.stderr].filter(Boolean).join('\\n');
56
+ const failureCategory = signal.failureCategory || signal.validationFailureCategory;
57
+ if (failureCategory === 'nested_sandbox_process_failure' || isNestedSandboxProcessFailure(output)) {
58
+ return {
59
+ outcome: 'inconclusive',
60
+ status: 'inconclusive',
61
+ reason: 'Tests could not run because the sandbox blocked process creation.',
62
+ infrastructureFailure: true,
63
+ failures,
64
+ warnings: ['worker validation blocked by nested sandbox'],
65
+ source: 'deterministic'
66
+ };
67
+ }
68
+
69
+ if (typeof exitCode === 'number' && exitCode !== 0) failures.push('worker exit code was ' + exitCode);
70
+ if (syntaxValidationFailed) failures.push('changed file(s) failed syntax validation');
71
+ if (analysisTaskChangedCode) failures.push('analysis-only task changed code outside .memory/.maestro');
72
+ if (deletedOutOfScope) failures.push('worker deleted file(s) outside .memory/.maestro');
73
+ if (testsRun === true && testsPassed === false) failures.push('test run failed');
74
+
75
+ if (unsupportedToolCall) warnings.push('worker attempted an unsupported tool call');
76
+ if (suspectedFalsePositive) warnings.push('worker may have claimed success without a matching file change');
77
+ if (windowsShellCommandMismatch) warnings.push('worker used a Unix command under PowerShell');
78
+
79
+ if (failures.length > 0) {
80
+ return { outcome: 'rejected', status: 'rejected', failures, warnings, source: 'deterministic' };
81
+ }
82
+ if (warnings.length > 0) {
83
+ return { outcome: 'verified_with_warnings', status: 'verified_with_warnings', failures, warnings, source: 'deterministic' };
84
+ }
85
+ return { outcome: 'verified', status: 'verified', failures, warnings, source: 'deterministic' };
86
+ }
87
+
88
+ // Order from the spec: deterministic first, cheap model second, strong model
89
+ // only if truly necessary. This function decides whether step 2 is even
90
+ // warranted — a clean 'rejected' from deterministic checks needs no second
91
+ // opinion (the failure is already conclusive), but 'verified_with_warnings'
92
+ // or any high/critical-risk task benefits from one.
93
+ export function needsIndependentModelCheck(classification, deterministicResult) {
94
+ if (!deterministicResult || deterministicResult.outcome === 'rejected') {
95
+ return false;
96
+ }
97
+ if (deterministicResult.outcome === 'verified_with_warnings') {
98
+ return true;
99
+ }
100
+ if (classification && (classification.risk === 'high' || classification.risk === 'critical')) {
101
+ return true;
102
+ }
103
+ if (classification && classification.verificationRequired === true && deterministicResult.outcome !== 'verified') {
104
+ return true;
105
+ }
106
+ return false;
107
+ }
108
+
109
+ // Ties the deterministic verdict together with an OPTIONAL model-backed
110
+ // second opinion. This module never calls a real engine on its own — that
111
+ // would violate "não chamar workers reais desnecessariamente" for anything
112
+ // that merely imports this file (including tests). A second opinion only
113
+ // happens if the caller explicitly passes `options.runEngineFn`, and even
114
+ // then only after budget/policy allows it.
115
+ export async function verifyTaskResult({ task, classification, runSignals = {}, options = {} } = {}) {
116
+ const deterministic = runDeterministicVerification(runSignals);
117
+
118
+ if (options.skipModelVerification || !needsIndependentModelCheck(classification, deterministic)) {
119
+ return { ...deterministic, modelVerification: null };
120
+ }
121
+
122
+ if (typeof options.runEngineFn !== 'function') {
123
+ return {
124
+ outcome: deterministic.outcome === 'verified' ? 'inconclusive' : deterministic.outcome,
125
+ failures: deterministic.failures,
126
+ warnings: [...deterministic.warnings, 'independent model verification recommended but no runEngineFn was provided'],
127
+ source: 'deterministic',
128
+ modelVerification: null
129
+ };
130
+ }
131
+
132
+ const budgetResult = await evaluateBudget({
133
+ engineRecord: options.verifierEngineRecord || null,
134
+ ...(options.budgetContext || {})
135
+ });
136
+ if (!budgetResult.allowed) {
137
+ return {
138
+ outcome: deterministic.outcome === 'verified' ? 'inconclusive' : deterministic.outcome,
139
+ failures: deterministic.failures,
140
+ warnings: [...deterministic.warnings, 'independent model verification skipped: ' + budgetResult.reason],
141
+ source: 'deterministic',
142
+ modelVerification: null
143
+ };
144
+ }
145
+
146
+ const modelResult = await options.runEngineFn({ task, classification, deterministic });
147
+ const outcome = modelResult && VERIFIER_OUTCOMES.includes(modelResult.outcome) ? modelResult.outcome : 'inconclusive';
148
+ return {
149
+ outcome,
150
+ failures: deterministic.failures,
151
+ warnings: deterministic.warnings,
152
+ source: 'deterministic+model',
153
+ modelVerification: modelResult || null
154
+ };
155
+ }
156
+
157
+ export function verifySubtaskResult(input) {
158
+ assertPlainApiObject(input, 'input');
159
+ if (!isNonBlankString(input.subtaskId)) {
160
+ throw new TypeError('subtaskId must be a non-empty string');
161
+ }
162
+ if (!Object.prototype.hasOwnProperty.call(input, 'runId')) {
163
+ throw new TypeError('runId key is required');
164
+ }
165
+ if (!(input.runId === null || isNonBlankString(input.runId))) {
166
+ throw new TypeError('runId must be a non-empty string or null');
167
+ }
168
+ for (const field of ['expectedFiles', 'forbiddenFiles', 'warnings', 'acceptanceCriteria']) {
169
+ if (input[field] !== undefined && !Array.isArray(input[field])) {
170
+ throw new TypeError(`${field} must be an array`);
171
+ }
172
+ }
173
+
174
+ const ctx = createVerificationContext('subtask');
175
+ const subtaskId = input.subtaskId;
176
+ const runId = input.runId;
177
+ const run = isPlainObject(input.run) ? input.run : null;
178
+ const runStatus = stringOrNull(input.runStatus) || stringOrNull(run && run.status);
179
+ const task = isPlainObject(input.task) ? input.task : null;
180
+ const subtask = isPlainObject(input.subtask) ? input.subtask : null;
181
+ const expectedFiles = normalizePathArray(input.expectedFiles || task && task.files || subtask && subtask.expectedFiles || []);
182
+ const forbiddenFiles = normalizePathArray(input.forbiddenFiles || []);
183
+ const noChangeExpected = input.noChangeExpected === true ||
184
+ (task && task.noChangeExpected === true) ||
185
+ (subtask && subtask.noChangeExpected === true);
186
+
187
+ for (const warning of input.warnings || []) {
188
+ if (typeof warning === 'string') ctx.warnings.push(warning);
189
+ }
190
+
191
+ analyzeRunAssociation(ctx, { subtaskId, runId, runStatus, noExecutionReason: input.noExecutionReason });
192
+ analyzeRunStatus(ctx, { subtaskId, runId, runStatus });
193
+ analyzeWorkspaceDiff(ctx, {
194
+ subtaskId,
195
+ runId,
196
+ workspaceDiff: input.workspaceDiff,
197
+ expectedFiles,
198
+ forbiddenFiles,
199
+ noChangeExpected,
200
+ task
201
+ });
202
+ analyzeSyntaxChecks(ctx, { subtaskId, runId, syntaxChecks: input.syntaxChecks });
203
+ analyzeTestResults(ctx, { subtaskId, runId, testResults: input.testResults });
204
+ analyzeHostValidation(ctx, { subtaskId, runId, hostValidation: input.hostValidation });
205
+ analyzeDiagnostics(ctx, { subtaskId, runId, diagnostics: input.diagnostics });
206
+ analyzeFailureClassification(ctx, { subtaskId, runId, failureClassification: input.failureClassification });
207
+ analyzeExecutionContext(ctx, { subtaskId, runId, executionContext: input.executionContext });
208
+
209
+ const acceptance = verifyAcceptanceCriteria({
210
+ criteria: input.acceptanceCriteria || [],
211
+ evidenceBundle: buildAcceptanceEvidenceBundle({
212
+ acceptedEvidence: ctx.acceptedEvidence,
213
+ rejectedEvidence: ctx.rejectedEvidence,
214
+ missingEvidence: ctx.missingEvidence,
215
+ contradictoryEvidence: ctx.contradictoryEvidence,
216
+ workspaceDiff: input.workspaceDiff,
217
+ expectedFiles,
218
+ forbiddenFiles,
219
+ syntaxChecks: input.syntaxChecks,
220
+ testResults: input.testResults,
221
+ hostValidation: input.hostValidation
222
+ }),
223
+ options: cloneData(input.options || {})
224
+ });
225
+ ctx.acceptanceCriteriaResults.push(...acceptance.results);
226
+ ctx.errors.push(...acceptance.errors);
227
+ ctx.warnings.push(...acceptance.warnings);
228
+ applyAcceptanceOutcome(ctx, acceptance);
229
+
230
+ if (acceptedNonRunEvidenceCount(ctx) === 0 && ctx.acceptanceCriteriaResults.length === 0 &&
231
+ !ctx.blocked && !ctx.needsHuman && !ctx.hasCodeFailure && !ctx.hasInfrastructureFailure) {
232
+ ctx.missingEvidence.push(evidenceDecision({
233
+ evidenceId: `missing:${subtaskId}:${runId ?? 'none'}:deterministic-evidence`,
234
+ sourceType: 'other',
235
+ subtaskId,
236
+ runId,
237
+ rule: 'deterministic-evidence-required',
238
+ reason: 'No deterministic evidence was provided for the subtask result.',
239
+ originalEvidence: null
240
+ }));
241
+ }
242
+
243
+ return finalizeVerificationResult(ctx, {
244
+ scope: 'subtask',
245
+ subtaskId,
246
+ runId,
247
+ recommendedAction: defaultRecommendationForContext(ctx)
248
+ });
249
+ }
250
+
251
+ export function verifyConsolidatedResult(input) {
252
+ assertPlainApiObject(input, 'input');
253
+ if (!isPlainObject(input.consolidatedResult)) {
254
+ throw new TypeError('consolidatedResult must be an object');
255
+ }
256
+ const consolidated = input.consolidatedResult;
257
+ const ctx = createVerificationContext('consolidated');
258
+
259
+ const taskId = typeof consolidated.taskId === 'string' ? consolidated.taskId : null;
260
+ const completeness = stringOrNull(consolidated.consolidationCompleteness);
261
+ const status = stringOrNull(consolidated.status);
262
+ if (!['complete', 'partial', 'invalid'].includes(completeness)) {
263
+ ctx.errors.push('consolidationCompleteness is missing or invalid');
264
+ ctx.structuralInvalid = true;
265
+ }
266
+ if (!['accepted', 'accepted_with_warnings', 'rejected', 'inconclusive', 'needs_human'].includes(status)) {
267
+ ctx.errors.push('consolidated status is missing or unknown');
268
+ ctx.structuralInvalid = true;
269
+ }
270
+
271
+ ctx.warnings.push(...stringArrayOrError(ctx, consolidated.warnings, 'warnings'));
272
+ ctx.errors.push(...stringArrayOrError(ctx, consolidated.inputErrors, 'inputErrors'));
273
+ ctx.conflicts.push(...stringArrayOrError(ctx, consolidated.conflicts, 'conflicts'));
274
+ ctx.inconsistencies.push(...stringArrayOrError(ctx, consolidated.inconsistencies, 'inconsistencies'));
275
+ ctx.invalidReferences.push(...stringArrayOrError(ctx, consolidated.invalidReferences, 'invalidReferences'));
276
+ ctx.failures.push(...stringArrayOrError(ctx, consolidated.failures, 'failures'));
277
+ ctx.codeFailures.push(...stringArrayOrError(ctx, consolidated.codeFailures, 'codeFailures'));
278
+ ctx.infrastructureFailures.push(...stringArrayOrError(ctx, consolidated.infrastructureFailures, 'infrastructureFailures'));
279
+
280
+ if (ctx.codeFailures.length > 0) {
281
+ ctx.hasCodeFailure = true;
282
+ }
283
+ if (ctx.infrastructureFailures.length > 0) {
284
+ ctx.hasInfrastructureFailure = true;
285
+ }
286
+ if (completeness === 'invalid' || countArray(consolidated.inputErrors) > 0) {
287
+ ctx.structuralInvalid = true;
288
+ }
289
+ if (completeness === 'partial') {
290
+ ctx.partial = true;
291
+ }
292
+ if (ctx.conflicts.length > 0 || ctx.inconsistencies.length > 0) {
293
+ ctx.contradictory = true;
294
+ }
295
+ if (ctx.invalidReferences.length > 0) {
296
+ ctx.structuralInvalid = true;
297
+ }
298
+
299
+ const consolidatedEvidence = objectArrayOrError(ctx, consolidated.evidence, 'evidence');
300
+ const consolidatedMissingEvidence = objectArrayOrError(ctx, consolidated.missingEvidence, 'missingEvidence');
301
+ const consolidatedPerSubtask = objectArrayOrError(ctx, consolidated.perSubtask, 'perSubtask');
302
+
303
+ for (const item of consolidatedEvidence) {
304
+ ctx.acceptedEvidence.push(evidenceDecisionFromRaw(item, {
305
+ rule: 'consolidated-evidence-preserved',
306
+ reason: 'Evidence produced by the Consolidator was preserved for verification.',
307
+ relatedCriterionId: null
308
+ }));
309
+ }
310
+ for (const item of consolidatedMissingEvidence) {
311
+ ctx.missingEvidence.push(evidenceDecisionFromRaw(item, {
312
+ rule: 'consolidated-missing-evidence-preserved',
313
+ reason: objectReason(item, 'Missing evidence reported by the Consolidator.'),
314
+ relatedCriterionId: stringOrNull(item && item.criterionId)
315
+ }));
316
+ }
317
+
318
+ for (const item of consolidatedPerSubtask) {
319
+ const subtaskId = stringOrNull(item.subtaskId);
320
+ const runId = normalizeRunId(item.runId);
321
+ const observedState = stringOrNull(item.observedState);
322
+ if (observedState === 'blocked') {
323
+ ctx.blocked = true;
324
+ ctx.failures.push(`Subtask ${subtaskId ?? '(unknown)'} is blocked`);
325
+ } else if (observedState === 'needs_human') {
326
+ ctx.needsHuman = true;
327
+ ctx.failures.push(`Subtask ${subtaskId ?? '(unknown)'} needs human decision`);
328
+ } else if (observedState === 'failed') {
329
+ if (item.infrastructureFailure === true) {
330
+ ctx.hasInfrastructureFailure = true;
331
+ ctx.infrastructureFailures.push(`Subtask ${subtaskId ?? '(unknown)'} failed due to infrastructure`);
332
+ } else {
333
+ ctx.hasCodeFailure = true;
334
+ ctx.codeFailures.push(`Subtask ${subtaskId ?? '(unknown)'} failed`);
335
+ }
336
+ } else if (observedState === 'missing') {
337
+ ctx.missingEvidence.push(evidenceDecision({
338
+ evidenceId: `missing:${subtaskId ?? 'unknown'}:${runId ?? 'none'}:subtask-result`,
339
+ sourceType: 'verifier-result',
340
+ subtaskId,
341
+ runId,
342
+ rule: 'subtask-result-required',
343
+ reason: 'Subtask result is missing.',
344
+ originalEvidence: item
345
+ }));
346
+ } else if (observedState === 'contradictory') {
347
+ ctx.contradictory = true;
348
+ ctx.contradictoryEvidence.push(evidenceDecision({
349
+ evidenceId: `contradictory:${subtaskId ?? 'unknown'}:${runId ?? 'none'}:subtask-result`,
350
+ sourceType: 'verifier-result',
351
+ subtaskId,
352
+ runId,
353
+ rule: 'subtask-signals-contradict',
354
+ reason: 'Subtask execution signals are contradictory.',
355
+ originalEvidence: item
356
+ }));
357
+ } else if (observedState === 'invalid') {
358
+ ctx.structuralInvalid = true;
359
+ ctx.invalidReferences.push(`Subtask ${subtaskId ?? '(unknown)'} has invalid execution association`);
360
+ }
361
+ pushStrings(ctx.warnings, item.warnings);
362
+ pushStrings(ctx.failures, item.failures);
363
+ }
364
+
365
+ const acceptance = verifyAcceptanceCriteria({
366
+ criteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
367
+ evidenceBundle: {
368
+ acceptedEvidence: ctx.acceptedEvidence,
369
+ missingEvidence: ctx.missingEvidence,
370
+ evidence: consolidatedEvidence
371
+ },
372
+ options: cloneData(input.options || {})
373
+ });
374
+ ctx.acceptanceCriteriaResults.push(...acceptance.results);
375
+ ctx.errors.push(...acceptance.errors);
376
+ ctx.warnings.push(...acceptance.warnings);
377
+ applyAcceptanceOutcome(ctx, acceptance);
378
+
379
+ if (isNonBlankString(consolidated.recommendedNextAction)) {
380
+ ctx.recommendations.push({
381
+ action: normalizeRecommendationAction(consolidated.recommendedNextAction),
382
+ reason: 'Recommendation copied from Consolidator as consultative metadata.',
383
+ consultative: true
384
+ });
385
+ }
386
+
387
+ return finalizeVerificationResult(ctx, {
388
+ scope: 'consolidated',
389
+ taskId,
390
+ consolidatedStatus: status,
391
+ consolidationCompleteness: completeness,
392
+ recommendedAction: defaultRecommendationForContext(ctx)
393
+ });
394
+ }
395
+
396
+ export function verifyAcceptanceCriteria(input) {
397
+ assertPlainApiObject(input, 'input');
398
+ if (input.criteria !== undefined && !Array.isArray(input.criteria)) {
399
+ throw new TypeError('criteria must be an array');
400
+ }
401
+ if (input.evidenceBundle !== undefined && !isPlainObject(input.evidenceBundle)) {
402
+ throw new TypeError('evidenceBundle must be an object');
403
+ }
404
+ const criteria = input.criteria || [];
405
+ const evidenceBundle = input.evidenceBundle || {};
406
+ const evidence = normalizeEvidenceBundle(evidenceBundle);
407
+ const errors = [];
408
+ const warnings = [];
409
+ const seenIds = new Set();
410
+ const results = [];
411
+
412
+ criteria.forEach((criterion, index) => {
413
+ const normalized = normalizeCriterion(criterion, index);
414
+ if (!normalized.valid) {
415
+ errors.push(normalized.reason);
416
+ results.push(criterionResult({
417
+ id: normalized.id,
418
+ description: normalized.description,
419
+ required: normalized.required,
420
+ verificationMode: normalized.verificationMode,
421
+ state: 'invalid',
422
+ reason: normalized.reason
423
+ }));
424
+ return;
425
+ }
426
+ if (seenIds.has(normalized.id)) {
427
+ const reason = `duplicate criterion id: ${normalized.id}`;
428
+ errors.push(reason);
429
+ results.push(criterionResult({
430
+ ...normalized,
431
+ state: 'invalid',
432
+ reason
433
+ }));
434
+ return;
435
+ }
436
+ seenIds.add(normalized.id);
437
+
438
+ const evaluated = evaluateCriterion(normalized, evidence, evidenceBundle);
439
+ results.push(evaluated);
440
+ if (evaluated.required !== true && evaluated.state !== 'satisfied') {
441
+ warnings.push(`optional criterion ${evaluated.id} is ${evaluated.state}`);
442
+ }
443
+ });
444
+
445
+ const sortedResults = [...results].sort(compareCriterionResults);
446
+ const summary = {
447
+ satisfied: sortedResults.filter(item => item.state === 'satisfied').length,
448
+ partiallySatisfied: sortedResults.filter(item => item.state === 'partially_satisfied').length,
449
+ notSatisfied: sortedResults.filter(item => item.state === 'not_satisfied').length,
450
+ notVerifiable: sortedResults.filter(item => item.state === 'not_verifiable').length,
451
+ invalid: sortedResults.filter(item => item.state === 'invalid').length,
452
+ requiredUnsatisfied: sortedResults.filter(item => item.required === true && item.state !== 'satisfied').length
453
+ };
454
+
455
+ let verificationOutcome = 'verified';
456
+ if (sortedResults.some(item => item.required === true && item.state === 'not_satisfied')) {
457
+ verificationOutcome = 'rejected';
458
+ } else if (sortedResults.some(item => item.required === true && item.verificationMode === 'manual')) {
459
+ verificationOutcome = 'needs_human';
460
+ } else if (sortedResults.some(item => item.required === true && item.state !== 'satisfied')) {
461
+ verificationOutcome = 'inconclusive';
462
+ } else if (warnings.length > 0) {
463
+ verificationOutcome = 'verified_with_warnings';
464
+ }
465
+
466
+ return {
467
+ schemaVersion: VERIFIER_SCHEMA_VERSION,
468
+ scope: 'acceptance',
469
+ verificationOutcome,
470
+ outcome: verificationOutcome,
471
+ results: sortedResults,
472
+ summary,
473
+ errors: uniqueSorted(errors),
474
+ warnings: uniqueSorted(warnings)
475
+ };
476
+ }
477
+
478
+ function createVerificationContext(scope) {
479
+ return {
480
+ scope,
481
+ errors: [],
482
+ warnings: [],
483
+ acceptedEvidence: [],
484
+ rejectedEvidence: [],
485
+ missingEvidence: [],
486
+ contradictoryEvidence: [],
487
+ acceptanceCriteriaResults: [],
488
+ failures: [],
489
+ codeFailures: [],
490
+ infrastructureFailures: [],
491
+ failureClassifications: [],
492
+ conflicts: [],
493
+ inconsistencies: [],
494
+ invalidReferences: [],
495
+ recommendations: [],
496
+ hasForbiddenChange: false,
497
+ hasCodeFailure: false,
498
+ hasInfrastructureFailure: false,
499
+ hostValidationPassed: false,
500
+ needsHuman: false,
501
+ blocked: false,
502
+ structuralInvalid: false,
503
+ contradictory: false,
504
+ partial: false
505
+ };
506
+ }
507
+
508
+ function assertPlainApiObject(value, name) {
509
+ if (!isPlainObject(value)) {
510
+ throw new TypeError(`${name} must be an object`);
511
+ }
512
+ }
513
+
514
+ function analyzeRunAssociation(ctx, { subtaskId, runId, runStatus, noExecutionReason }) {
515
+ if (runId === null) {
516
+ if (runStatus === 'blocked' && noExecutionReason === 'blocked_before_execution') {
517
+ return;
518
+ }
519
+ if (runStatus === 'needs_human' && noExecutionReason === 'needs_human_before_execution') {
520
+ return;
521
+ }
522
+ if ((runStatus === null || runStatus === undefined) &&
523
+ (noExecutionReason === 'not_executed' || noExecutionReason === 'result_missing')) {
524
+ ctx.missingEvidence.push(evidenceDecision({
525
+ evidenceId: `missing:${subtaskId}:none:${noExecutionReason}`,
526
+ sourceType: 'run-status',
527
+ subtaskId,
528
+ runId,
529
+ rule: 'no-execution-declared',
530
+ reason: `No execution occurred: ${noExecutionReason}.`,
531
+ originalEvidence: { runStatus: runStatus ?? null, noExecutionReason }
532
+ }));
533
+ return;
534
+ }
535
+ ctx.invalidReferences.push(`invalid run association for ${subtaskId}: runId null requires a coherent noExecutionReason`);
536
+ ctx.structuralInvalid = true;
537
+ }
538
+ }
539
+
540
+ function analyzeRunStatus(ctx, { subtaskId, runId, runStatus }) {
541
+ if (!runStatus) return;
542
+ ctx.acceptedEvidence.push(evidenceDecision({
543
+ evidenceId: `run-status:${subtaskId}:${runId ?? 'none'}`,
544
+ sourceType: 'run-status',
545
+ subtaskId,
546
+ runId,
547
+ sourceId: runId,
548
+ sourceIdentifier: runStatus,
549
+ rule: 'run-status-preserved',
550
+ reason: `Runtime reported status ${runStatus}.`,
551
+ originalEvidence: { status: runStatus }
552
+ }));
553
+ if (runStatus === 'failed') {
554
+ ctx.hasCodeFailure = true;
555
+ ctx.codeFailures.push(`Subtask ${subtaskId} run failed`);
556
+ } else if (runStatus === 'blocked') {
557
+ ctx.blocked = true;
558
+ ctx.failures.push(`Subtask ${subtaskId} is blocked`);
559
+ } else if (runStatus === 'needs_human') {
560
+ ctx.needsHuman = true;
561
+ ctx.failures.push(`Subtask ${subtaskId} needs human decision`);
562
+ } else if (runStatus === 'completed_with_warnings') {
563
+ ctx.warnings.push(`Subtask ${subtaskId} completed with warnings`);
564
+ } else if (!['completed', 'completed_with_warnings', 'failed', 'blocked', 'needs_human'].includes(runStatus)) {
565
+ ctx.invalidReferences.push(`unknown runStatus ${runStatus}`);
566
+ ctx.structuralInvalid = true;
567
+ }
568
+ }
569
+
570
+ function analyzeWorkspaceDiff(ctx, { subtaskId, runId, workspaceDiff, expectedFiles, forbiddenFiles, noChangeExpected, task }) {
571
+ if (workspaceDiff === null || workspaceDiff === undefined) {
572
+ if (expectedFiles.length > 0) {
573
+ ctx.missingEvidence.push(evidenceDecision({
574
+ evidenceId: `missing:${subtaskId}:${runId ?? 'none'}:workspace-diff`,
575
+ sourceType: 'workspace-diff',
576
+ subtaskId,
577
+ runId,
578
+ rule: 'workspace-diff-required',
579
+ reason: 'Workspace diff evidence is required for expected files.',
580
+ originalEvidence: null
581
+ }));
582
+ }
583
+ return;
584
+ }
585
+ if (!isPlainObject(workspaceDiff)) {
586
+ ctx.errors.push('workspaceDiff must be an object when provided');
587
+ ctx.structuralInvalid = true;
588
+ return;
589
+ }
590
+ const changed = normalizePathArray(workspaceDiff.changedFiles || []);
591
+ const created = normalizePathArray(workspaceDiff.createdFiles || []);
592
+ const deleted = normalizePathArray(workspaceDiff.deletedFiles || []);
593
+ const outOfScope = normalizePathArray(workspaceDiff.outOfScopeChanges || []);
594
+ const allChanged = uniqueSorted([...changed, ...created, ...deleted, ...outOfScope]);
595
+ const expectedSet = new Set(expectedFiles);
596
+ const forbiddenSet = new Set(forbiddenFiles);
597
+
598
+ for (const file of allChanged) {
599
+ const kind = deleted.includes(file) ? 'deleted' : created.includes(file) ? 'created' : 'changed';
600
+ const decision = evidenceDecision({
601
+ evidenceId: `workspace:${kind}:${file}`,
602
+ sourceType: 'workspace-diff',
603
+ subtaskId,
604
+ runId,
605
+ sourceId: file,
606
+ sourceIdentifier: file,
607
+ provenance: { field: `${kind}Files` },
608
+ rule: `workspace-file-${kind}`,
609
+ reason: `Workspace file was ${kind}: ${file}.`,
610
+ originalEvidence: { file, kind }
611
+ });
612
+ if (forbiddenSet.has(file) || outOfScope.includes(file) || isSensitiveOrOutsidePath(file)) {
613
+ ctx.rejectedEvidence.push({
614
+ ...decision,
615
+ rule: 'forbidden-or-out-of-scope-change',
616
+ reason: `File change is forbidden or outside scope: ${file}.`
617
+ });
618
+ ctx.errors.push(`forbidden or out-of-scope file changed: ${file}`);
619
+ ctx.hasForbiddenChange = true;
620
+ } else {
621
+ ctx.acceptedEvidence.push(decision);
622
+ if (!expectedSet.has(file) && expectedFiles.length > 0) {
623
+ ctx.warnings.push(`unexpected file changed: ${file}`);
624
+ }
625
+ }
626
+ }
627
+
628
+ for (const file of expectedFiles) {
629
+ if (changed.includes(file) || created.includes(file)) {
630
+ continue;
631
+ }
632
+ if (deleted.includes(file)) {
633
+ ctx.rejectedEvidence.push(evidenceDecision({
634
+ evidenceId: `workspace:deleted-expected:${file}`,
635
+ sourceType: 'workspace-diff',
636
+ subtaskId,
637
+ runId,
638
+ sourceId: file,
639
+ sourceIdentifier: file,
640
+ rule: 'expected-file-deleted',
641
+ reason: `Expected file was deleted: ${file}.`,
642
+ originalEvidence: { file, kind: 'deleted' }
643
+ }));
644
+ ctx.hasCodeFailure = true;
645
+ ctx.codeFailures.push(`expected file was deleted: ${file}`);
646
+ } else {
647
+ ctx.missingEvidence.push(evidenceDecision({
648
+ evidenceId: `missing:expected-file:${file}`,
649
+ sourceType: 'workspace-diff',
650
+ subtaskId,
651
+ runId,
652
+ sourceId: file,
653
+ sourceIdentifier: file,
654
+ rule: 'expected-file-not-changed',
655
+ reason: `Expected file has no verified change: ${file}.`,
656
+ originalEvidence: { file }
657
+ }));
658
+ }
659
+ }
660
+
661
+ const changedFlag = workspaceDiff.workspaceChanged === true || allChanged.length > 0;
662
+ if (!changedFlag && !noChangeExpected && expectedFiles.length === 0 && taskLooksLikeWrite(task)) {
663
+ ctx.missingEvidence.push(evidenceDecision({
664
+ evidenceId: `missing:${subtaskId}:${runId ?? 'none'}:write-without-files`,
665
+ sourceType: 'workspace-diff',
666
+ subtaskId,
667
+ runId,
668
+ rule: 'write-task-needs-diff',
669
+ reason: 'Task appears to require writing but no changed files or expected files were provided.',
670
+ originalEvidence: cloneData(workspaceDiff)
671
+ }));
672
+ }
673
+ }
674
+
675
+ function analyzeSyntaxChecks(ctx, { subtaskId, runId, syntaxChecks }) {
676
+ const entries = normalizeCheckEntries(syntaxChecks, 'syntaxValidation');
677
+ for (const entry of entries) {
678
+ const sourceId = stringOrNull(entry.path) || stringOrNull(entry.file) || stringOrNull(entry.sourceId) || 'syntax';
679
+ const status = checkStatus(entry);
680
+ const decision = evidenceDecision({
681
+ evidenceId: `syntax:${sourceId}`,
682
+ sourceType: 'syntax-validation',
683
+ subtaskId,
684
+ runId,
685
+ sourceId,
686
+ sourceIdentifier: sourceId,
687
+ rule: `syntax-${status}`,
688
+ reason: `Syntax check ${status} for ${sourceId}.`,
689
+ originalEvidence: entry
690
+ });
691
+ if (status === 'passed' || status === 'not_applicable') {
692
+ ctx.acceptedEvidence.push(decision);
693
+ } else if (status === 'failed') {
694
+ ctx.rejectedEvidence.push(decision);
695
+ ctx.hasCodeFailure = true;
696
+ ctx.codeFailures.push(`syntax check failed: ${sourceId}`);
697
+ } else if (status === 'blocked_by_infrastructure') {
698
+ ctx.hasInfrastructureFailure = true;
699
+ ctx.infrastructureFailures.push(`syntax check blocked by infrastructure: ${sourceId}`);
700
+ ctx.missingEvidence.push({ ...decision, reason: `Syntax check blocked by infrastructure for ${sourceId}.` });
701
+ } else {
702
+ ctx.missingEvidence.push({ ...decision, reason: `Syntax check was not run for ${sourceId}.` });
703
+ }
704
+ }
705
+ }
706
+
707
+ function analyzeTestResults(ctx, { subtaskId, runId, testResults }) {
708
+ const entries = normalizeCheckEntries(testResults, 'tests');
709
+ for (const entry of entries) {
710
+ const sourceId = stringOrNull(entry.commandId) || stringOrNull(entry.name) || stringOrNull(entry.sourceId) || 'tests';
711
+ const status = testStatus(entry);
712
+ const decision = evidenceDecision({
713
+ evidenceId: `test:${sourceId}`,
714
+ sourceType: 'test-result',
715
+ subtaskId,
716
+ runId,
717
+ sourceId,
718
+ sourceIdentifier: sourceId,
719
+ rule: `tests-${status}`,
720
+ reason: `Test result ${status} for ${sourceId}.`,
721
+ originalEvidence: entry
722
+ });
723
+ if (status === 'passed' || status === 'not_applicable') {
724
+ ctx.acceptedEvidence.push(decision);
725
+ } else if (status === 'failed') {
726
+ ctx.rejectedEvidence.push(decision);
727
+ ctx.hasCodeFailure = true;
728
+ ctx.codeFailures.push(`test run failed: ${sourceId}`);
729
+ } else if (status === 'blocked_by_infrastructure') {
730
+ ctx.hasInfrastructureFailure = true;
731
+ ctx.infrastructureFailures.push(`test run blocked by infrastructure: ${sourceId}`);
732
+ ctx.missingEvidence.push({ ...decision, reason: `Test did not start because infrastructure blocked it: ${sourceId}.` });
733
+ } else {
734
+ ctx.missingEvidence.push({ ...decision, reason: `Test did not run: ${sourceId}.` });
735
+ }
736
+ }
737
+ }
738
+
739
+ function analyzeHostValidation(ctx, { subtaskId, runId, hostValidation }) {
740
+ const entries = normalizeCheckEntries(hostValidation, 'results');
741
+ for (const entry of entries) {
742
+ const sourceId = stringOrNull(entry.commandId) || stringOrNull(entry.sourceId) || 'host-validation';
743
+ const result = hostValidationStatus(entry);
744
+ const decision = evidenceDecision({
745
+ evidenceId: `host:${sourceId}`,
746
+ sourceType: 'host-validation',
747
+ subtaskId,
748
+ runId,
749
+ sourceId,
750
+ sourceIdentifier: sourceId,
751
+ provenance: entry.metadata ? { metadata: cloneData(entry.metadata) } : null,
752
+ rule: `host-validation-${result}`,
753
+ reason: `Host validation ${result} for ${sourceId}.`,
754
+ originalEvidence: entry
755
+ });
756
+ if (result === 'passed') {
757
+ ctx.hostValidationPassed = true;
758
+ ctx.acceptedEvidence.push(decision);
759
+ if (ctx.hasInfrastructureFailure) {
760
+ ctx.warnings.push(`host validation passed for ${sourceId}; original infrastructure failure preserved`);
761
+ }
762
+ } else if (result === 'failed') {
763
+ ctx.rejectedEvidence.push(decision);
764
+ ctx.hasHostValidationFailure = true;
765
+ } else if (result === 'blocked') {
766
+ ctx.blocked = true;
767
+ ctx.rejectedEvidence.push(decision);
768
+ } else {
769
+ ctx.missingEvidence.push(decision);
770
+ }
771
+ }
772
+ }
773
+
774
+ function analyzeDiagnostics(ctx, { subtaskId, runId, diagnostics }) {
775
+ if (!isPlainObject(diagnostics)) return;
776
+ pushStrings(ctx.warnings, diagnostics.warnings);
777
+ if (diagnostics.suspectedFalsePositive === true) {
778
+ ctx.contradictory = true;
779
+ ctx.contradictoryEvidence.push(evidenceDecision({
780
+ evidenceId: `diagnostics:${subtaskId}:${runId ?? 'none'}:suspected-false-positive`,
781
+ sourceType: 'diagnostics',
782
+ subtaskId,
783
+ runId,
784
+ rule: 'diagnostics-false-positive',
785
+ reason: 'Diagnostics indicate the worker claimed success without matching deterministic evidence.',
786
+ originalEvidence: diagnostics
787
+ }));
788
+ }
789
+ if (diagnostics.explicitWorkerFailure === true) {
790
+ ctx.hasCodeFailure = true;
791
+ ctx.codeFailures.push('worker explicitly reported failure');
792
+ ctx.rejectedEvidence.push(evidenceDecision({
793
+ evidenceId: `diagnostics:${subtaskId}:${runId ?? 'none'}:explicit-worker-failure`,
794
+ sourceType: 'diagnostics',
795
+ subtaskId,
796
+ runId,
797
+ rule: 'diagnostics-explicit-worker-failure',
798
+ reason: 'Diagnostics indicate the worker explicitly reported it could not complete the task.',
799
+ originalEvidence: diagnostics
800
+ }));
801
+ }
802
+ if (diagnostics.unsupportedToolCall === true) {
803
+ ctx.warnings.push('worker used an unsupported tool call');
804
+ }
805
+ if (diagnostics.windowsShellCommandMismatch === true) {
806
+ ctx.warnings.push('worker used a Unix command under PowerShell');
807
+ }
808
+ }
809
+
810
+ function analyzeFailureClassification(ctx, { subtaskId, runId, failureClassification }) {
811
+ if (!isPlainObject(failureClassification)) return;
812
+ ctx.failureClassifications.push(cloneData(failureClassification));
813
+ const category = stringOrNull(failureClassification.category);
814
+ ctx.acceptedEvidence.push(evidenceDecision({
815
+ evidenceId: `failure-classification:${category ?? 'unknown'}:${subtaskId}:${runId ?? 'none'}`,
816
+ sourceType: 'failure-classification',
817
+ subtaskId,
818
+ runId,
819
+ sourceIdentifier: category,
820
+ rule: 'failure-classification-preserved',
821
+ reason: `Failure classification preserved: ${category ?? 'unknown'}.`,
822
+ originalEvidence: failureClassification
823
+ }));
824
+ if (category === 'syntax_failure' || category === 'test_failure') {
825
+ ctx.hasCodeFailure = true;
826
+ ctx.codeFailures.push(`failure classification: ${category}`);
827
+ } else if (category === 'needs_human' || failureClassification.shouldAskHuman === true) {
828
+ ctx.needsHuman = true;
829
+ } else if (['policy_violation', 'budget_exceeded', 'safety_blocked', 'insufficient_capability', 'missing_tool'].includes(category)) {
830
+ ctx.blocked = true;
831
+ } else if (failureClassification.infrastructureFailure === true ||
832
+ category === 'nested_sandbox_process_failure' ||
833
+ category === 'windows_sandbox_acl_failure') {
834
+ ctx.hasInfrastructureFailure = true;
835
+ ctx.infrastructureFailures.push(`infrastructure failure classification: ${category ?? 'unknown'}`);
836
+ } else if (category === 'suspected_false_positive') {
837
+ ctx.contradictory = true;
838
+ }
839
+ pushStrings(ctx.failures, failureClassification.reason);
840
+ }
841
+
842
+ function analyzeExecutionContext(ctx, { subtaskId, runId, executionContext }) {
843
+ if (!isPlainObject(executionContext)) return;
844
+ const runtimeGate = executionContext.runtimeGate;
845
+ if (isPlainObject(runtimeGate)) {
846
+ if (runtimeGate.decision === 'NEEDS_HUMAN' || runtimeGate.status === 'NEEDS_HUMAN') {
847
+ ctx.needsHuman = true;
848
+ } else if (runtimeGate.decision === 'BLOCK' || runtimeGate.status === 'BLOCK') {
849
+ ctx.blocked = true;
850
+ }
851
+ }
852
+ if (isPlainObject(executionContext.policy) && executionContext.policy.ok === false) {
853
+ ctx.blocked = true;
854
+ ctx.rejectedEvidence.push(evidenceDecision({
855
+ evidenceId: `execution-context:${subtaskId}:${runId ?? 'none'}:policy`,
856
+ sourceType: 'other',
857
+ subtaskId,
858
+ runId,
859
+ rule: 'policy-block',
860
+ reason: 'Execution context policy blocks automatic approval.',
861
+ originalEvidence: executionContext.policy
862
+ }));
863
+ }
864
+ }
865
+
866
+ function applyAcceptanceOutcome(ctx, acceptance) {
867
+ if (acceptance.outcome === 'needs_human') {
868
+ ctx.needsHuman = true;
869
+ } else if (acceptance.outcome === 'rejected') {
870
+ ctx.hasCodeFailure = true;
871
+ ctx.codeFailures.push('required acceptance criterion was not satisfied');
872
+ } else if (acceptance.outcome === 'inconclusive') {
873
+ ctx.partial = true;
874
+ } else if (acceptance.outcome === 'verified_with_warnings') {
875
+ ctx.warnings.push('optional acceptance criterion is not fully satisfied');
876
+ }
877
+ }
878
+
879
+ function finalizeVerificationResult(ctx, extra) {
880
+ const failureInformation = {
881
+ failures: uniqueSorted([...ctx.failures, ...ctx.codeFailures, ...ctx.infrastructureFailures]),
882
+ codeFailures: uniqueSorted(ctx.codeFailures),
883
+ infrastructureFailures: uniqueSorted(ctx.infrastructureFailures),
884
+ failureClassifications: cloneData(ctx.failureClassifications)
885
+ };
886
+ let verificationOutcome = decideVerificationOutcome(ctx, failureInformation);
887
+ verificationOutcome = applyConsolidatedStatusCeiling(verificationOutcome, extra.consolidatedStatus, ctx);
888
+
889
+ const recommendations = [
890
+ ...ctx.recommendations,
891
+ {
892
+ action: normalizeRecommendationAction(extra.recommendedAction || recommendationForOutcome(verificationOutcome)),
893
+ reason: recommendationReason(verificationOutcome),
894
+ consultative: true
895
+ }
896
+ ];
897
+
898
+ return {
899
+ schemaVersion: VERIFIER_SCHEMA_VERSION,
900
+ scope: extra.scope,
901
+ ...(extra.taskId !== undefined ? { taskId: extra.taskId } : {}),
902
+ ...(extra.subtaskId !== undefined ? { subtaskId: extra.subtaskId } : {}),
903
+ ...(Object.prototype.hasOwnProperty.call(extra, 'runId') ? { runId: extra.runId } : {}),
904
+ verificationOutcome,
905
+ outcome: verificationOutcome,
906
+ verified: verificationOutcome === 'verified',
907
+ errors: uniqueSorted(ctx.errors),
908
+ warnings: uniqueSorted(ctx.warnings),
909
+ acceptedEvidence: sortEvidenceDecisions(ctx.acceptedEvidence),
910
+ rejectedEvidence: sortEvidenceDecisions(ctx.rejectedEvidence),
911
+ missingEvidence: sortEvidenceDecisions(ctx.missingEvidence),
912
+ contradictoryEvidence: sortEvidenceDecisions(ctx.contradictoryEvidence),
913
+ acceptanceCriteriaResults: [...ctx.acceptanceCriteriaResults].sort(compareCriterionResults).map(item => cloneData(item)),
914
+ failureInformation,
915
+ conflicts: uniqueSorted(ctx.conflicts),
916
+ inconsistencies: uniqueSorted(ctx.inconsistencies),
917
+ invalidReferences: uniqueSorted(ctx.invalidReferences),
918
+ recommendations: sortRecommendations(recommendations),
919
+ summary: summarizeVerification({ scope: extra.scope, verificationOutcome, ctx, failureInformation }),
920
+ source: 'verifier'
921
+ };
922
+ }
923
+
924
+ function decideVerificationOutcome(ctx, failureInformation) {
925
+ if (ctx.hasForbiddenChange) return 'rejected';
926
+ if (ctx.hasCodeFailure || failureInformation.codeFailures.length > 0) return 'rejected';
927
+ if (ctx.needsHuman) return 'needs_human';
928
+ if (ctx.blocked) return 'blocked';
929
+ if (failureInformation.failures.length > 0 && !ctx.hasInfrastructureFailure && failureInformation.infrastructureFailures.length === 0) {
930
+ return 'inconclusive';
931
+ }
932
+ if (ctx.structuralInvalid || ctx.invalidReferences.length > 0 || ctx.errors.length > 0) return 'inconclusive';
933
+ if (ctx.contradictory || ctx.conflicts.length > 0 || ctx.inconsistencies.length > 0 || ctx.contradictoryEvidence.length > 0) {
934
+ return 'inconclusive';
935
+ }
936
+ if (ctx.missingEvidence.length > 0 || ctx.partial || ctx.hasHostValidationFailure) {
937
+ return 'inconclusive';
938
+ }
939
+ if (ctx.hasInfrastructureFailure || failureInformation.infrastructureFailures.length > 0) {
940
+ return ctx.hostValidationPassed ? 'verified_with_warnings' : 'inconclusive';
941
+ }
942
+ if (ctx.warnings.length > 0) return 'verified_with_warnings';
943
+ return 'verified';
944
+ }
945
+
946
+ function applyConsolidatedStatusCeiling(outcome, status, ctx) {
947
+ if (!status) return outcome;
948
+ if (outcome === 'blocked' && ctx.blocked && !ctx.needsHuman) {
949
+ return outcome;
950
+ }
951
+ if (status === 'needs_human') {
952
+ return outcome === 'rejected' ? 'rejected' : 'needs_human';
953
+ }
954
+ if (status === 'rejected') {
955
+ return 'rejected';
956
+ }
957
+ if (status === 'inconclusive' && (outcome === 'verified' || outcome === 'verified_with_warnings')) {
958
+ return 'inconclusive';
959
+ }
960
+ if (status === 'accepted_with_warnings' && outcome === 'verified') {
961
+ return 'verified_with_warnings';
962
+ }
963
+ return outcome;
964
+ }
965
+
966
+ function normalizeCriterion(criterion, index) {
967
+ if (typeof criterion === 'string') {
968
+ return {
969
+ valid: true,
970
+ id: `criterion:${index}`,
971
+ description: criterion,
972
+ required: true,
973
+ evidenceRequirements: [],
974
+ verificationMode: 'text_only',
975
+ metadata: {},
976
+ legacyText: true
977
+ };
978
+ }
979
+ if (!isPlainObject(criterion)) {
980
+ return {
981
+ valid: false,
982
+ id: `criterion:${index}`,
983
+ description: '',
984
+ required: true,
985
+ verificationMode: 'text_only',
986
+ reason: `criterion ${index} must be a string or object`
987
+ };
988
+ }
989
+ const id = stringOrNull(criterion.id);
990
+ const description = stringOrNull(criterion.description);
991
+ const verificationMode = stringOrNull(criterion.verificationMode) || 'text_only';
992
+ if (!id) {
993
+ return invalidCriterion(index, criterion, 'criterion object must include a non-empty id');
994
+ }
995
+ if (!description) {
996
+ return invalidCriterion(index, criterion, `criterion ${id} must include a non-empty description`);
997
+ }
998
+ if (!ACCEPTANCE_MODES.includes(verificationMode)) {
999
+ return {
1000
+ valid: false,
1001
+ id,
1002
+ description,
1003
+ required: criterion.required !== false,
1004
+ verificationMode,
1005
+ reason: `unknown verification mode for ${id}: ${verificationMode}`
1006
+ };
1007
+ }
1008
+ if (criterion.evidenceRequirements !== undefined && !Array.isArray(criterion.evidenceRequirements)) {
1009
+ return {
1010
+ valid: false,
1011
+ id,
1012
+ description,
1013
+ required: criterion.required !== false,
1014
+ verificationMode,
1015
+ reason: `criterion ${id} evidenceRequirements must be an array`
1016
+ };
1017
+ }
1018
+ return {
1019
+ valid: true,
1020
+ id,
1021
+ description,
1022
+ required: criterion.required !== false,
1023
+ evidenceRequirements: cloneData(criterion.evidenceRequirements || []),
1024
+ verificationMode,
1025
+ metadata: isPlainObject(criterion.metadata) ? cloneData(criterion.metadata) : {}
1026
+ };
1027
+ }
1028
+
1029
+ function invalidCriterion(index, criterion, reason) {
1030
+ return {
1031
+ valid: false,
1032
+ id: stringOrNull(criterion && criterion.id) || `criterion:${index}`,
1033
+ description: stringOrNull(criterion && criterion.description) || '',
1034
+ required: criterion && criterion.required === false ? false : true,
1035
+ verificationMode: stringOrNull(criterion && criterion.verificationMode) || 'text_only',
1036
+ reason
1037
+ };
1038
+ }
1039
+
1040
+ function evaluateCriterion(criterion, evidence, evidenceBundle) {
1041
+ if (criterion.verificationMode === 'manual') {
1042
+ return criterionResult({
1043
+ ...criterion,
1044
+ state: 'not_verifiable',
1045
+ reason: 'Manual verification requires a human decision.'
1046
+ });
1047
+ }
1048
+ if (criterion.verificationMode === 'text_only') {
1049
+ const rule = evidenceBundle.criteriaRules && evidenceBundle.criteriaRules[criterion.id];
1050
+ if (isPlainObject(rule) && ACCEPTANCE_STATES.includes(rule.state)) {
1051
+ return criterionResult({
1052
+ ...criterion,
1053
+ state: rule.state,
1054
+ evidenceRefs: normalizeStringArray(rule.evidenceRefs || []),
1055
+ reason: stringOrNull(rule.reason) || 'Structured criteria rule supplied the result.'
1056
+ });
1057
+ }
1058
+ return criterionResult({
1059
+ ...criterion,
1060
+ state: 'not_verifiable',
1061
+ reason: 'Text-only criterion has no deterministic verification rule.'
1062
+ });
1063
+ }
1064
+
1065
+ const relatedEvidence = evidence.items.filter(item => criterionEvidenceMatches(item, criterion.id));
1066
+ const wrongRelated = evidence.items.filter(item => item.relatedCriterionId && item.relatedCriterionId !== criterion.id);
1067
+ const requirementValues = criterion.evidenceRequirements.flatMap(requirementValuesFrom);
1068
+
1069
+ if (criterion.verificationMode === 'file_exists') {
1070
+ return evaluateFileCriterion(criterion, evidence.filesExisting, evidence.filesDeleted, requirementValues, 'exists');
1071
+ }
1072
+ if (criterion.verificationMode === 'file_changed') {
1073
+ return evaluateFileCriterion(criterion, evidence.filesChanged, evidence.filesDeleted, requirementValues, 'changed');
1074
+ }
1075
+ if (criterion.verificationMode === 'file_not_changed') {
1076
+ return evaluateFileNotChangedCriterion(criterion, evidence, requirementValues);
1077
+ }
1078
+ if (criterion.verificationMode === 'syntax_passed') {
1079
+ return evaluateStatusCriterion(criterion, evidence.syntax, 'syntax');
1080
+ }
1081
+ if (criterion.verificationMode === 'tests_passed') {
1082
+ return evaluateStatusCriterion(criterion, evidence.tests, 'tests');
1083
+ }
1084
+ if (criterion.verificationMode === 'host_validation_passed') {
1085
+ return evaluateStatusCriterion(criterion, evidence.hostValidation, 'host validation');
1086
+ }
1087
+ if (criterion.verificationMode === 'no_forbidden_files') {
1088
+ const changed = evidence.forbiddenChanged;
1089
+ return criterionResult({
1090
+ ...criterion,
1091
+ state: changed.length > 0 ? 'not_satisfied' : 'satisfied',
1092
+ evidenceRefs: changed,
1093
+ reason: changed.length > 0 ? `Forbidden files changed: ${changed.join(', ')}` : 'No forbidden file changes were reported.'
1094
+ });
1095
+ }
1096
+ if (criterion.verificationMode === 'no_unexpected_changes') {
1097
+ const unexpected = evidence.unexpectedChanged;
1098
+ return criterionResult({
1099
+ ...criterion,
1100
+ state: unexpected.length > 0 ? 'not_satisfied' : 'satisfied',
1101
+ evidenceRefs: unexpected,
1102
+ reason: unexpected.length > 0 ? `Unexpected files changed: ${unexpected.join(', ')}` : 'No unexpected changes were reported.'
1103
+ });
1104
+ }
1105
+ if (criterion.verificationMode === 'evidence_present') {
1106
+ if (wrongRelated.length > 0 && relatedEvidence.length === 0 && requirementValues.length > 0) {
1107
+ return criterionResult({
1108
+ ...criterion,
1109
+ state: 'not_satisfied',
1110
+ missingEvidence: requirementValues,
1111
+ reason: 'Evidence exists only for a different criterion.'
1112
+ });
1113
+ }
1114
+ const presentIds = new Set(relatedEvidence.map(item => item.id));
1115
+ const missing = requirementValues.filter(id => !presentIds.has(id));
1116
+ return criterionResult({
1117
+ ...criterion,
1118
+ state: missing.length === 0 && relatedEvidence.length > 0 ? 'satisfied' :
1119
+ (missing.length < requirementValues.length && relatedEvidence.length > 0 ? 'partially_satisfied' : 'not_verifiable'),
1120
+ evidenceRefs: [...presentIds],
1121
+ missingEvidence: missing,
1122
+ reason: missing.length === 0 && relatedEvidence.length > 0 ? 'Required evidence is present.' : 'Required evidence is missing.'
1123
+ });
1124
+ }
1125
+ return criterionResult({
1126
+ ...criterion,
1127
+ state: 'invalid',
1128
+ reason: `unsupported verification mode: ${criterion.verificationMode}`
1129
+ });
1130
+ }
1131
+
1132
+ function evaluateFileCriterion(criterion, positiveFiles, deletedFiles, requirements, verb) {
1133
+ const files = requirements.length > 0 ? requirements : positiveFiles;
1134
+ const present = files.filter(file => positiveFiles.includes(file));
1135
+ const deleted = files.filter(file => deletedFiles.includes(file));
1136
+ const missing = files.filter(file => !positiveFiles.includes(file));
1137
+ if (deleted.length > 0) {
1138
+ return criterionResult({
1139
+ ...criterion,
1140
+ state: 'not_satisfied',
1141
+ evidenceRefs: deleted,
1142
+ missingEvidence: missing,
1143
+ reason: `Required file was deleted: ${deleted.join(', ')}.`
1144
+ });
1145
+ }
1146
+ if (files.length === 0) {
1147
+ return criterionResult({ ...criterion, state: 'not_verifiable', reason: `No file evidence is available for ${verb}.` });
1148
+ }
1149
+ return criterionResult({
1150
+ ...criterion,
1151
+ state: missing.length === 0 ? 'satisfied' : present.length > 0 ? 'partially_satisfied' : 'not_satisfied',
1152
+ evidenceRefs: present,
1153
+ missingEvidence: missing,
1154
+ reason: missing.length === 0 ? `Required file ${verb} evidence is present.` : `Missing file ${verb} evidence: ${missing.join(', ')}.`
1155
+ });
1156
+ }
1157
+
1158
+ function evaluateFileNotChangedCriterion(criterion, evidence, requirements) {
1159
+ const files = requirements.length > 0 ? requirements : evidence.filesExisting;
1160
+ const changed = files.filter(file => evidence.filesChanged.includes(file) || evidence.filesDeleted.includes(file));
1161
+ if (files.length === 0) {
1162
+ return criterionResult({ ...criterion, state: 'not_verifiable', reason: 'No file evidence is available for unchanged check.' });
1163
+ }
1164
+ return criterionResult({
1165
+ ...criterion,
1166
+ state: changed.length === 0 ? 'satisfied' : 'not_satisfied',
1167
+ evidenceRefs: files.filter(file => !changed.includes(file)),
1168
+ missingEvidence: changed,
1169
+ reason: changed.length === 0 ? 'Required files were not changed.' : `Files changed but should not have: ${changed.join(', ')}.`
1170
+ });
1171
+ }
1172
+
1173
+ function evaluateStatusCriterion(criterion, statuses, label) {
1174
+ if (statuses.failed.length > 0) {
1175
+ return criterionResult({
1176
+ ...criterion,
1177
+ state: 'not_satisfied',
1178
+ evidenceRefs: statuses.failed,
1179
+ reason: `${label} failed.`
1180
+ });
1181
+ }
1182
+ if (statuses.passed.length > 0) {
1183
+ return criterionResult({
1184
+ ...criterion,
1185
+ state: statuses.missing.length > 0 ? 'partially_satisfied' : 'satisfied',
1186
+ evidenceRefs: statuses.passed,
1187
+ missingEvidence: statuses.missing,
1188
+ reason: statuses.missing.length > 0 ? `${label} passed partially; some evidence is missing.` : `${label} passed.`
1189
+ });
1190
+ }
1191
+ return criterionResult({
1192
+ ...criterion,
1193
+ state: statuses.blocked.length > 0 ? 'not_verifiable' : 'not_verifiable',
1194
+ evidenceRefs: statuses.blocked,
1195
+ missingEvidence: statuses.missing.length > 0 ? statuses.missing : [`${label}:missing`],
1196
+ reason: statuses.blocked.length > 0 ? `${label} was blocked by infrastructure.` : `${label} evidence is missing.`
1197
+ });
1198
+ }
1199
+
1200
+ function normalizeEvidenceBundle(bundle) {
1201
+ const workspaceDiff = isPlainObject(bundle.workspaceDiff) ? bundle.workspaceDiff : {};
1202
+ const changed = normalizePathArray([...(workspaceDiff.changedFiles || []), ...(workspaceDiff.createdFiles || [])]);
1203
+ const deleted = normalizePathArray(workspaceDiff.deletedFiles || []);
1204
+ const expectedFiles = normalizePathArray(bundle.expectedFiles || []);
1205
+ const forbiddenFiles = normalizePathArray(bundle.forbiddenFiles || []);
1206
+ const accepted = arrayOrEmpty(bundle.acceptedEvidence);
1207
+ const rejected = arrayOrEmpty(bundle.rejectedEvidence);
1208
+ const rawEvidence = arrayOrEmpty(bundle.evidence);
1209
+ const items = [...accepted, ...rejected, ...rawEvidence].map((item, index) => normalizeEvidenceItem(item, index));
1210
+ const forbiddenChanged = changed.concat(deleted).filter(file => forbiddenFiles.includes(file) || isSensitiveOrOutsidePath(file));
1211
+ const unexpectedChanged = expectedFiles.length > 0
1212
+ ? changed.concat(deleted).filter(file => !expectedFiles.includes(file) && !forbiddenFiles.includes(file))
1213
+ : normalizePathArray(bundle.unexpectedFiles || []);
1214
+
1215
+ return {
1216
+ items,
1217
+ filesChanged: uniqueSorted([...changed, ...items.filter(item => item.sourceType === 'workspace-diff').map(item => normalizePath(stringOrNull(item.sourceId) || '')).filter(Boolean)]),
1218
+ filesDeleted: uniqueSorted(deleted),
1219
+ filesExisting: uniqueSorted([...changed, ...normalizePathArray(bundle.existingFiles || [])]),
1220
+ forbiddenChanged: uniqueSorted([...(bundle.forbiddenChanged || []), ...forbiddenChanged].map(normalizePath).filter(Boolean)),
1221
+ unexpectedChanged: uniqueSorted(unexpectedChanged),
1222
+ syntax: collectStatuses(bundle.syntaxChecks, 'syntaxValidation'),
1223
+ tests: collectStatuses(bundle.testResults, 'tests'),
1224
+ hostValidation: collectHostValidationStatuses(bundle.hostValidation)
1225
+ };
1226
+ }
1227
+
1228
+ function normalizeEvidenceItem(item, index) {
1229
+ const raw = isPlainObject(item) ? item : { value: item };
1230
+ const id = stringOrNull(raw.evidenceId) || stringOrNull(raw.id) || stringOrNull(raw.sourceId) || `evidence:${index}`;
1231
+ return {
1232
+ id,
1233
+ sourceType: normalizeSourceType(raw.sourceType || raw.type),
1234
+ relatedCriterionId: stringOrNull(raw.relatedCriterionId) || stringOrNull(raw.criterionId),
1235
+ sourceId: stringOrNull(raw.sourceId) || stringOrNull(raw.sourceIdentifier),
1236
+ raw
1237
+ };
1238
+ }
1239
+
1240
+ function collectStatuses(value, nestedField) {
1241
+ const entries = normalizeCheckEntries(value, nestedField);
1242
+ const statuses = { passed: [], failed: [], missing: [], blocked: [] };
1243
+ entries.forEach((entry, index) => {
1244
+ const id = stringOrNull(entry.evidenceId) || stringOrNull(entry.id) || stringOrNull(entry.path) || stringOrNull(entry.commandId) || `${nestedField}:${index}`;
1245
+ const status = nestedField === 'tests' ? testStatus(entry) : checkStatus(entry);
1246
+ if (status === 'passed' || status === 'not_applicable') statuses.passed.push(id);
1247
+ else if (status === 'failed') statuses.failed.push(id);
1248
+ else if (status === 'blocked_by_infrastructure') statuses.blocked.push(id);
1249
+ else statuses.missing.push(id);
1250
+ });
1251
+ return mapStatusArrays(statuses);
1252
+ }
1253
+
1254
+ function collectHostValidationStatuses(value) {
1255
+ const entries = normalizeCheckEntries(value, 'results');
1256
+ const statuses = { passed: [], failed: [], missing: [], blocked: [] };
1257
+ entries.forEach((entry, index) => {
1258
+ const id = stringOrNull(entry.commandId) || stringOrNull(entry.evidenceId) || `host:${index}`;
1259
+ const status = hostValidationStatus(entry);
1260
+ if (status === 'passed') statuses.passed.push(id);
1261
+ else if (status === 'failed') statuses.failed.push(id);
1262
+ else if (status === 'blocked') statuses.blocked.push(id);
1263
+ else statuses.missing.push(id);
1264
+ });
1265
+ return mapStatusArrays(statuses);
1266
+ }
1267
+
1268
+ function mapStatusArrays(statuses) {
1269
+ return {
1270
+ passed: uniqueSorted(statuses.passed),
1271
+ failed: uniqueSorted(statuses.failed),
1272
+ missing: uniqueSorted(statuses.missing),
1273
+ blocked: uniqueSorted(statuses.blocked)
1274
+ };
1275
+ }
1276
+
1277
+ function criterionResult(input) {
1278
+ return {
1279
+ id: String(input.id),
1280
+ description: String(input.description || ''),
1281
+ required: input.required !== false,
1282
+ verificationMode: String(input.verificationMode || 'text_only'),
1283
+ state: ACCEPTANCE_STATES.includes(input.state) ? input.state : 'invalid',
1284
+ evidenceRefs: uniqueSorted(input.evidenceRefs || []),
1285
+ missingEvidence: uniqueSorted(input.missingEvidence || []),
1286
+ reason: String(input.reason || '')
1287
+ };
1288
+ }
1289
+
1290
+ function evidenceDecisionFromRaw(raw, { rule, reason, relatedCriterionId }) {
1291
+ const item = isPlainObject(raw) ? raw : { value: raw };
1292
+ return evidenceDecision({
1293
+ evidenceId: stringOrNull(item.evidenceId) || stringOrNull(item.id) || stringOrNull(item.sourceId) || canonicalStringify(item),
1294
+ sourceType: normalizeSourceType(item.sourceType || item.type || item.kind),
1295
+ subtaskId: stringOrNull(item.subtaskId),
1296
+ runId: normalizeRunId(item.runId),
1297
+ sourceId: stringOrNull(item.sourceId),
1298
+ sourceIdentifier: stringOrNull(item.sourceIdentifier) || stringOrNull(item.field),
1299
+ provenance: isPlainObject(item.provenance) ? item.provenance : null,
1300
+ rule,
1301
+ reason,
1302
+ relatedCriterionId,
1303
+ originalEvidence: Object.prototype.hasOwnProperty.call(item, 'value') ? item.value : item
1304
+ });
1305
+ }
1306
+
1307
+ function evidenceDecision(input) {
1308
+ return {
1309
+ evidenceId: String(input.evidenceId || 'evidence'),
1310
+ sourceType: normalizeSourceType(input.sourceType),
1311
+ subtaskId: input.subtaskId === undefined ? null : stringOrNull(input.subtaskId),
1312
+ runId: input.runId === undefined ? null : normalizeRunId(input.runId),
1313
+ sourceId: input.sourceId === undefined ? null : stringOrNull(input.sourceId),
1314
+ sourceIdentifier: input.sourceIdentifier === undefined ? null : stringOrNull(input.sourceIdentifier),
1315
+ provenance: input.provenance === undefined ? null : cloneData(input.provenance),
1316
+ rule: String(input.rule || 'unspecified'),
1317
+ reason: String(input.reason || ''),
1318
+ relatedCriterionId: input.relatedCriterionId === undefined ? null : stringOrNull(input.relatedCriterionId),
1319
+ originalEvidence: input.originalEvidence === undefined ? null : cloneData(input.originalEvidence)
1320
+ };
1321
+ }
1322
+
1323
+ function buildAcceptanceEvidenceBundle({ acceptedEvidence, rejectedEvidence, missingEvidence, contradictoryEvidence, workspaceDiff, expectedFiles, forbiddenFiles, syntaxChecks, testResults, hostValidation }) {
1324
+ return {
1325
+ acceptedEvidence: cloneData(acceptedEvidence),
1326
+ rejectedEvidence: cloneData(rejectedEvidence),
1327
+ missingEvidence: cloneData(missingEvidence),
1328
+ contradictoryEvidence: cloneData(contradictoryEvidence),
1329
+ workspaceDiff: cloneData(workspaceDiff),
1330
+ expectedFiles: cloneData(expectedFiles),
1331
+ forbiddenFiles: cloneData(forbiddenFiles),
1332
+ syntaxChecks: cloneData(syntaxChecks),
1333
+ testResults: cloneData(testResults),
1334
+ hostValidation: cloneData(hostValidation)
1335
+ };
1336
+ }
1337
+
1338
+ function normalizeCheckEntries(value, nestedField) {
1339
+ if (value === null || value === undefined) return [];
1340
+ if (Array.isArray(value)) return value.filter(isPlainObject).map(item => cloneData(item));
1341
+ if (isPlainObject(value)) {
1342
+ if (Array.isArray(value[nestedField])) return value[nestedField].filter(isPlainObject).map(item => cloneData(item));
1343
+ if (Array.isArray(value.results)) return value.results.filter(isPlainObject).map(item => cloneData(item));
1344
+ return [cloneData(value)];
1345
+ }
1346
+ return [];
1347
+ }
1348
+
1349
+ function checkStatus(entry) {
1350
+ if (entry.ok === true || entry.passed === true || entry.status === 'passed' || entry.result === 'passed') return 'passed';
1351
+ if (entry.ok === false || entry.passed === false || entry.status === 'failed' || entry.result === 'failed') return 'failed';
1352
+ if (entry.status === 'not_applicable' || entry.result === 'not_applicable' || entry.skipped === true) return 'not_applicable';
1353
+ if (entry.status === 'blocked_by_infrastructure' || entry.infrastructureFailure === true) return 'blocked_by_infrastructure';
1354
+ return 'not_run';
1355
+ }
1356
+
1357
+ function testStatus(entry) {
1358
+ if (entry.ran === true && entry.passed === true) return 'passed';
1359
+ if (entry.ran === true && entry.passed === false) return 'failed';
1360
+ if (entry.status === 'passed' || entry.result === 'passed') return 'passed';
1361
+ if (entry.status === 'failed' || entry.result === 'failed') return 'failed';
1362
+ if (entry.status === 'not_applicable' || entry.result === 'not_applicable') return 'not_applicable';
1363
+ if (entry.ran === false && (entry.infrastructureFailure === true || entry.status === 'blocked_by_infrastructure')) return 'blocked_by_infrastructure';
1364
+ if (entry.ran === false || entry.status === 'not_run') return 'not_run';
1365
+ if (entry.infrastructureFailure === true) return 'blocked_by_infrastructure';
1366
+ return 'not_run';
1367
+ }
1368
+
1369
+ function hostValidationStatus(entry) {
1370
+ if (entry.result === 'passed' || entry.ok === true) return 'passed';
1371
+ if (entry.result === 'failed' || entry.ok === false) return 'failed';
1372
+ if (entry.result === 'blocked' || entry.blocked === true || entry.allowed === false) return 'blocked';
1373
+ return 'not_run';
1374
+ }
1375
+
1376
+ function requirementValuesFrom(requirement) {
1377
+ if (typeof requirement === 'string') return [normalizePath(requirement) || requirement];
1378
+ if (!isPlainObject(requirement)) return [];
1379
+ return [
1380
+ requirement.id,
1381
+ requirement.evidenceId,
1382
+ requirement.sourceId,
1383
+ requirement.path,
1384
+ requirement.file
1385
+ ].filter(value => typeof value === 'string').map(value => normalizePath(value) || value);
1386
+ }
1387
+
1388
+ function criterionEvidenceMatches(item, criterionId) {
1389
+ return !item.relatedCriterionId || item.relatedCriterionId === criterionId;
1390
+ }
1391
+
1392
+ function defaultRecommendationForContext(ctx) {
1393
+ const outcome = decideVerificationOutcome(ctx, {
1394
+ failures: uniqueSorted([...ctx.failures, ...ctx.codeFailures, ...ctx.infrastructureFailures]),
1395
+ codeFailures: ctx.codeFailures,
1396
+ infrastructureFailures: ctx.infrastructureFailures
1397
+ });
1398
+ return recommendationForOutcome(outcome);
1399
+ }
1400
+
1401
+ function recommendationForOutcome(outcome) {
1402
+ if (outcome === 'verified') return 'complete';
1403
+ if (outcome === 'verified_with_warnings' || outcome === 'inconclusive') return 'verify';
1404
+ if (outcome === 'rejected') return 'fix';
1405
+ if (outcome === 'needs_human') return 'ask_human';
1406
+ if (outcome === 'blocked') return 'block';
1407
+ return 'none';
1408
+ }
1409
+
1410
+ function recommendationReason(outcome) {
1411
+ if (outcome === 'verified') return 'Verification evidence is sufficient.';
1412
+ if (outcome === 'verified_with_warnings') return 'Verification evidence is sufficient with warnings preserved.';
1413
+ if (outcome === 'rejected') return 'Deterministic verification found a blocking failure.';
1414
+ if (outcome === 'needs_human') return 'A human decision is required.';
1415
+ if (outcome === 'blocked') return 'An objective blocker prevents automatic approval.';
1416
+ return 'Additional verification is required.';
1417
+ }
1418
+
1419
+ function normalizeRecommendationAction(action) {
1420
+ return RECOMMENDATION_ACTIONS.includes(action) ? action : 'verify';
1421
+ }
1422
+
1423
+ function summarizeVerification({ scope, verificationOutcome, ctx, failureInformation }) {
1424
+ const issueCount = uniqueSorted([
1425
+ ...ctx.errors,
1426
+ ...ctx.invalidReferences,
1427
+ ...ctx.conflicts,
1428
+ ...ctx.inconsistencies,
1429
+ ...failureInformation.failures,
1430
+ ...ctx.missingEvidence.map(item => item.reason)
1431
+ ]).length;
1432
+ return `Verification ${verificationOutcome}: ${scope}, ${ctx.acceptedEvidence.length} accepted evidence item(s), ${issueCount} issue(s).`;
1433
+ }
1434
+
1435
+ function acceptedNonRunEvidenceCount(ctx) {
1436
+ return ctx.acceptedEvidence.filter(item => item.sourceType !== 'run-status').length;
1437
+ }
1438
+
1439
+ function sortEvidenceDecisions(values) {
1440
+ return values.map(item => cloneData(item)).sort((a, b) =>
1441
+ compareStrings(a.subtaskId ?? '', b.subtaskId ?? '') ||
1442
+ compareStrings(a.runId ?? '', b.runId ?? '') ||
1443
+ compareStrings(a.evidenceId, b.evidenceId) ||
1444
+ compareStrings(a.relatedCriterionId ?? '', b.relatedCriterionId ?? '') ||
1445
+ compareStrings(a.rule, b.rule)
1446
+ );
1447
+ }
1448
+
1449
+ function compareCriterionResults(a, b) {
1450
+ return compareStrings(a.id, b.id) ||
1451
+ compareStrings(a.verificationMode, b.verificationMode) ||
1452
+ compareStrings(a.reason, b.reason);
1453
+ }
1454
+
1455
+ function sortRecommendations(values) {
1456
+ return values.map(item => cloneData(item)).sort((a, b) =>
1457
+ compareStrings(a.action, b.action) ||
1458
+ compareStrings(a.reason, b.reason)
1459
+ );
1460
+ }
1461
+
1462
+ function stringArrayOrError(ctx, value, field) {
1463
+ if (value === undefined) return [];
1464
+ if (!Array.isArray(value)) {
1465
+ ctx.errors.push(`${field} must be an array`);
1466
+ ctx.structuralInvalid = true;
1467
+ return [];
1468
+ }
1469
+ return normalizeStringArray(value);
1470
+ }
1471
+
1472
+ function objectArrayOrError(ctx, value, field) {
1473
+ if (value === undefined) return [];
1474
+ if (!Array.isArray(value)) {
1475
+ ctx.errors.push(`${field} must be an array`);
1476
+ ctx.structuralInvalid = true;
1477
+ return [];
1478
+ }
1479
+ const entries = [];
1480
+ value.forEach((item, index) => {
1481
+ if (!isPlainObject(item)) {
1482
+ ctx.errors.push(`${field}[${index}] must be an object`);
1483
+ ctx.structuralInvalid = true;
1484
+ return;
1485
+ }
1486
+ entries.push(item);
1487
+ });
1488
+ return entries;
1489
+ }
1490
+
1491
+ function pushStrings(target, values) {
1492
+ if (!Array.isArray(values)) return;
1493
+ for (const value of values) {
1494
+ if (typeof value === 'string') target.push(value);
1495
+ }
1496
+ }
1497
+
1498
+ function normalizeStringArray(values) {
1499
+ return Array.isArray(values) ? values.filter(value => typeof value === 'string') : [];
1500
+ }
1501
+
1502
+ function normalizePathArray(values) {
1503
+ return Array.isArray(values) ? uniqueSorted(values.map(normalizePath).filter(Boolean)) : [];
1504
+ }
1505
+
1506
+ function normalizePath(value) {
1507
+ if (typeof value !== 'string') return null;
1508
+ const normalized = value.replace(/\\/g, '/').replace(/^\.\//, '').trim();
1509
+ return normalized || null;
1510
+ }
1511
+
1512
+ function isSensitiveOrOutsidePath(file) {
1513
+ const normalized = normalizePath(file) || '';
1514
+ const lower = normalized.toLowerCase();
1515
+ return lower === '.env' ||
1516
+ lower.endsWith('/.env') ||
1517
+ lower.includes('/.env/') ||
1518
+ lower.includes('secret') ||
1519
+ lower.includes('token') ||
1520
+ normalized.startsWith('../') ||
1521
+ normalized.includes('/../') ||
1522
+ normalized.startsWith('/') ||
1523
+ /^[A-Za-z]:\//.test(normalized);
1524
+ }
1525
+
1526
+ function taskLooksLikeWrite(task) {
1527
+ if (!isPlainObject(task)) return false;
1528
+ const text = `${task.title || ''} ${task.description || ''}`.toLowerCase();
1529
+ return /\b(implement|create|update|modify|fix|add|write|editar|alterar|criar|corrigir|implementar)\b/.test(text);
1530
+ }
1531
+
1532
+ function objectReason(item, fallback) {
1533
+ return isPlainObject(item) && typeof item.reason === 'string' ? item.reason : fallback;
1534
+ }
1535
+
1536
+ function normalizeRunId(value) {
1537
+ return isNonBlankString(value) ? value : null;
1538
+ }
1539
+
1540
+ function stringOrNull(value) {
1541
+ return typeof value === 'string' && value.trim().length > 0 ? value : null;
1542
+ }
1543
+
1544
+ function normalizeSourceType(value) {
1545
+ return EVIDENCE_SOURCE_TYPES.includes(value) ? value : 'other';
1546
+ }
1547
+
1548
+ function uniqueSorted(values) {
1549
+ return [...new Set((values || []).filter(value => typeof value === 'string'))].sort(compareStrings);
1550
+ }
1551
+
1552
+ function arrayOrEmpty(value) {
1553
+ return Array.isArray(value) ? value : [];
1554
+ }
1555
+
1556
+ function countArray(value) {
1557
+ return Array.isArray(value) ? value.length : 0;
1558
+ }
1559
+
1560
+ function isPlainObject(value) {
1561
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
1562
+ }
1563
+
1564
+ function isNonBlankString(value) {
1565
+ return typeof value === 'string' && value.trim().length > 0;
1566
+ }
1567
+
1568
+ function cloneData(value, seen = new WeakMap()) {
1569
+ if (Array.isArray(value)) {
1570
+ if (seen.has(value)) return '[Circular]';
1571
+ seen.set(value, true);
1572
+ return value.map(item => cloneData(item, seen));
1573
+ }
1574
+ if (isPlainObject(value)) {
1575
+ if (seen.has(value)) return '[Circular]';
1576
+ seen.set(value, true);
1577
+ return Object.fromEntries(Object.keys(value).sort(compareStrings).map(key => [key, cloneData(value[key], seen)]));
1578
+ }
1579
+ if (typeof value === 'function' || typeof value === 'symbol') {
1580
+ return String(value);
1581
+ }
1582
+ return value;
1583
+ }
1584
+
1585
+ function canonicalStringify(value, seen = new WeakMap()) {
1586
+ if (Array.isArray(value)) {
1587
+ if (seen.has(value)) return '"[Circular]"';
1588
+ seen.set(value, true);
1589
+ return `[${value.map(item => canonicalStringify(item, seen)).join(',')}]`;
1590
+ }
1591
+ if (isPlainObject(value)) {
1592
+ if (seen.has(value)) return '"[Circular]"';
1593
+ seen.set(value, true);
1594
+ return `{${Object.keys(value).sort(compareStrings).map(key => `${JSON.stringify(key)}:${canonicalStringify(value[key], seen)}`).join(',')}}`;
1595
+ }
1596
+ if (typeof value === 'bigint') {
1597
+ return JSON.stringify(`${value.toString()}n`);
1598
+ }
1599
+ return JSON.stringify(value);
1600
+ }
1601
+
1602
+ function compareStrings(a, b) {
1603
+ const left = String(a);
1604
+ const right = String(b);
1605
+ if (left < right) return -1;
1606
+ if (left > right) return 1;
1607
+ return 0;
1608
+ }