loopforge 3.0.0 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -145
- package/dist/audit.d.ts +50 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +158 -0
- package/dist/audit.js.map +1 -0
- package/dist/canonical-state.d.ts +28 -20
- package/dist/canonical-state.d.ts.map +1 -1
- package/dist/canonical-state.js +82 -173
- package/dist/canonical-state.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +25 -16
- package/dist/cli.js.map +1 -1
- package/dist/cognitive-governance.d.ts +25 -33
- package/dist/cognitive-governance.d.ts.map +1 -1
- package/dist/cognitive-governance.js +38 -138
- package/dist/cognitive-governance.js.map +1 -1
- package/dist/enforcement-gate.d.ts +76 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +1071 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +99 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +839 -0
- package/dist/engine.js.map +1 -0
- package/dist/evidence-claims.d.ts +50 -0
- package/dist/evidence-claims.d.ts.map +1 -0
- package/dist/evidence-claims.js +134 -0
- package/dist/evidence-claims.js.map +1 -0
- package/dist/evidence-provider.d.ts +21 -23
- package/dist/evidence-provider.d.ts.map +1 -1
- package/dist/evidence-provider.js +74 -34
- package/dist/evidence-provider.js.map +1 -1
- package/dist/generate-schema.js +1 -84
- package/dist/generate-schema.js.map +1 -1
- package/dist/index.d.ts +19 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -15
- package/dist/index.js.map +1 -1
- package/dist/loop-compiler.d.ts +41 -3
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +632 -273
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +2 -2
- package/dist/loop-extras-parser.d.ts.map +1 -1
- package/dist/loop-extras-parser.js +2 -2
- package/dist/loop-extras-parser.js.map +1 -1
- package/dist/loop-projection.d.ts +26 -0
- package/dist/loop-projection.d.ts.map +1 -0
- package/dist/loop-projection.js +142 -0
- package/dist/loop-projection.js.map +1 -0
- package/dist/loop-store.d.ts +112 -24
- package/dist/loop-store.d.ts.map +1 -1
- package/dist/loop-store.js +365 -218
- package/dist/loop-store.js.map +1 -1
- package/dist/mcp/round-lifecycle.d.ts +216 -0
- package/dist/mcp/round-lifecycle.d.ts.map +1 -0
- package/dist/mcp/round-lifecycle.js +926 -0
- package/dist/mcp/round-lifecycle.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +28 -42
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +53 -101
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +306 -1105
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +13 -16
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +479 -413
- package/dist/mcp/tools.js.map +1 -1
- package/dist/policy-metrics.d.ts +12 -17
- package/dist/policy-metrics.d.ts.map +1 -1
- package/dist/policy-metrics.js +95 -161
- package/dist/policy-metrics.js.map +1 -1
- package/dist/policy.d.ts +44 -35
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +31 -115
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +37 -6
- package/dist/prompt-assembler.d.ts.map +1 -1
- package/dist/prompt-assembler.js +389 -160
- package/dist/prompt-assembler.js.map +1 -1
- package/dist/prompt-policy.d.ts +1 -8
- package/dist/prompt-policy.d.ts.map +1 -1
- package/dist/prompt-policy.js +8 -26
- package/dist/prompt-policy.js.map +1 -1
- package/dist/protocol.d.ts +357 -229
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +139 -15
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts +5 -13
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +19 -55
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +20 -24
- package/dist/round-coordinator.d.ts.map +1 -1
- package/dist/round-coordinator.js +99 -92
- package/dist/round-coordinator.js.map +1 -1
- package/dist/round-driver.d.ts +14 -13
- package/dist/round-driver.d.ts.map +1 -1
- package/dist/round-driver.js +26 -14
- package/dist/round-driver.js.map +1 -1
- package/dist/round-transaction.d.ts +15 -18
- package/dist/round-transaction.d.ts.map +1 -1
- package/dist/round-transaction.js +38 -47
- package/dist/round-transaction.js.map +1 -1
- package/dist/self-eval.d.ts +53 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +284 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +9 -4
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +18 -6
- package/dist/storage.js.map +1 -1
- package/dist/token-utils.d.ts +15 -3
- package/dist/token-utils.d.ts.map +1 -1
- package/dist/token-utils.js +24 -5
- package/dist/token-utils.js.map +1 -1
- package/dist/verification-gate.d.ts +108 -0
- package/dist/verification-gate.d.ts.map +1 -0
- package/dist/verification-gate.js +971 -0
- package/dist/verification-gate.js.map +1 -0
- package/loop_policy.json +44 -15
- package/package.json +5 -10
- package/skills/perception/SKILL.md +182 -199
- package/dist/canonical-event-writer.d.ts +0 -20
- package/dist/canonical-event-writer.d.ts.map +0 -1
- package/dist/canonical-event-writer.js +0 -131
- package/dist/canonical-event-writer.js.map +0 -1
- package/dist/compiler-subgoals.d.ts +0 -7
- package/dist/compiler-subgoals.d.ts.map +0 -1
- package/dist/compiler-subgoals.js +0 -93
- package/dist/compiler-subgoals.js.map +0 -1
- package/dist/derive-id.d.ts +0 -12
- package/dist/derive-id.d.ts.map +0 -1
- package/dist/derive-id.js +0 -31
- package/dist/derive-id.js.map +0 -1
- package/dist/mcp/session-projection.d.ts +0 -5
- package/dist/mcp/session-projection.d.ts.map +0 -1
- package/dist/mcp/session-projection.js +0 -18
- package/dist/mcp/session-projection.js.map +0 -1
- package/dist/mcp/session-queue.d.ts +0 -3
- package/dist/mcp/session-queue.d.ts.map +0 -1
- package/dist/mcp/session-queue.js +0 -18
- package/dist/mcp/session-queue.js.map +0 -1
- package/dist/mcp/session-types.d.ts +0 -72
- package/dist/mcp/session-types.d.ts.map +0 -1
- package/dist/mcp/session-types.js +0 -2
- package/dist/mcp/session-types.js.map +0 -1
- package/dist/round-commit-store.d.ts +0 -12
- package/dist/round-commit-store.d.ts.map +0 -1
- package/dist/round-commit-store.js +0 -13
- package/dist/round-commit-store.js.map +0 -1
- package/dist/round-gate.d.ts +0 -31
- package/dist/round-gate.d.ts.map +0 -1
- package/dist/round-gate.js +0 -259
- package/dist/round-gate.js.map +0 -1
- package/dist/round-recovery.d.ts +0 -17
- package/dist/round-recovery.d.ts.map +0 -1
- package/dist/round-recovery.js +0 -117
- package/dist/round-recovery.js.map +0 -1
- package/dist/round-report.d.ts +0 -16
- package/dist/round-report.d.ts.map +0 -1
- package/dist/round-report.js +0 -401
- package/dist/round-report.js.map +0 -1
- package/dist/runtime-services.d.ts +0 -59
- package/dist/runtime-services.d.ts.map +0 -1
- package/dist/runtime-services.js +0 -460
- package/dist/runtime-services.js.map +0 -1
- package/dist/vault-format.d.ts +0 -4
- package/dist/vault-format.d.ts.map +0 -1
- package/dist/vault-format.js +0 -4
- package/dist/vault-format.js.map +0 -1
- package/dist/vault-types.d.ts +0 -24
- package/dist/vault-types.d.ts.map +0 -1
- package/dist/vault-types.js +0 -2
- package/dist/vault-types.js.map +0 -1
- package/loopforge-policy.json +0 -340
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
/** Verification Gate — Layer 1 cross-round consistency checks.
|
|
2
|
+
*
|
|
3
|
+
* Pure-function module. Validates an agent's SelfEvaluation against the
|
|
4
|
+
* loop's own lineage before it enters the compiler.
|
|
5
|
+
*
|
|
6
|
+
* Verdict semantics:
|
|
7
|
+
* - trusted: all checks passed; flags are informational only.
|
|
8
|
+
* - suspect: one or more warn-level flags; flags become warnings in
|
|
9
|
+
* the next prompt so the agent can clarify.
|
|
10
|
+
* - contradicted: one or more error-level flags; the success flag for
|
|
11
|
+
* this round is excluded from the success trend (NOT
|
|
12
|
+
* modified). Flags become hard constraints — the agent
|
|
13
|
+
* must respond in the next round.
|
|
14
|
+
*/
|
|
15
|
+
import { getPolicy } from "./policy.js";
|
|
16
|
+
import { makeVerificationFlag, makeVerificationResult } from "./protocol.js";
|
|
17
|
+
import { jaccardSimilarity, tokenize, entryRound as sharedEntryRound, isRecord } from "./token-utils.js";
|
|
18
|
+
import { deriveSubGoalId } from "./loop-compiler.js";
|
|
19
|
+
import { deriveClaimView, resolveRoundFiles } from "./evidence-claims.js";
|
|
20
|
+
import { effectiveSuccess } from "./self-eval.js";
|
|
21
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
+
// v2.12: Check-name constants — single source of truth so the enforcement
|
|
23
|
+
// gate and audit can reference checks without string-literal drift.
|
|
24
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
+
export const CHECK_PROGRESS_REGRESSION = "progress_regression";
|
|
26
|
+
export const CHECK_EMPTY_CHANGE_WITH_PASSING = "empty_change_with_passing";
|
|
27
|
+
export const CHECK_SUCCESS_WITH_REMAINING_CRITERIA = "success_with_remaining_criteria";
|
|
28
|
+
export const CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE = "success_without_verified_evidence";
|
|
29
|
+
export const CHECK_OUTCOME_SUCCESS_CONTRADICTION = "outcome_success_contradiction";
|
|
30
|
+
export const CHECK_SUCCESS_CLAIM_CONFLICT = "success_claim_conflict";
|
|
31
|
+
export const CHECK_BLOCKED_WITHOUT_BLOCKER = "blocked_without_blocker";
|
|
32
|
+
export const CHECK_RETROACTIVE_CLAIM_BAD_ROUND = "retroactive_claim_bad_round";
|
|
33
|
+
export const CHECK_RETROACTIVE_CLAIM_UNVERIFIED = "retroactive_claim_unverified";
|
|
34
|
+
export const CHECK_DUPLICATE_CONSTRAINT_DISCOVERY = "duplicate_constraint_discovery";
|
|
35
|
+
export const CHECK_RECURRING_VIOLATION = "recurring_violation";
|
|
36
|
+
export const CHECK_RETRACT_FRESH_CONSTRAINT = "retract_fresh_constraint";
|
|
37
|
+
export const CHECK_EVIDENCE_INTEGRITY = "evidence_integrity";
|
|
38
|
+
export const CHECK_REQUIRED_COMMAND_FAILED = "required_command_failed";
|
|
39
|
+
export const CHECK_COMMAND_EVIDENCE_MISMATCH = "command_evidence_mismatch";
|
|
40
|
+
export const CHECK_INTENT_DRIFT = "intent_drift";
|
|
41
|
+
export const CHECK_SUBGOAL_DRIFT = "subgoal_drift";
|
|
42
|
+
export const CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED = "backtrack_workspace_not_restored";
|
|
43
|
+
export const CHECK_CRITERIA_CLAIMS_UNVERIFIED = "criteria_claims_unverified";
|
|
44
|
+
/** v3.2: success declared with no machine-verified observation this round
|
|
45
|
+
* (providerStatus ≠ verified). Warn-level: the round still commits, but its
|
|
46
|
+
* success never enters the trajectory and trust drops. */
|
|
47
|
+
export const CHECK_SUCCESS_UNVERIFIED = "success_unverified";
|
|
48
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
49
|
+
// Helpers
|
|
50
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
|
+
/** Extract the round number from a vault entry's loop_lineage.
|
|
52
|
+
* Returns 0 if the entry has no lineage or no round field.
|
|
53
|
+
* In practice, persistLoopLineage always writes round ≥ 1, so 0
|
|
54
|
+
* unambiguously means "not a valid round entry" in this context.
|
|
55
|
+
* Exported for reuse by enforcement-gate.ts. */
|
|
56
|
+
export function entryRound(entry) {
|
|
57
|
+
return sharedEntryRound(entry);
|
|
58
|
+
}
|
|
59
|
+
/** Read constraint_violations from a vault entry (entry-level, stored from
|
|
60
|
+
* the previous round's last_round_result at persist time). */
|
|
61
|
+
function entryViolations(entry) {
|
|
62
|
+
const viols = entry.constraint_violations;
|
|
63
|
+
if (Array.isArray(viols))
|
|
64
|
+
return viols.filter((v) => typeof v === "string");
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
function passedAfterCommand(snapshots) {
|
|
68
|
+
return snapshots.find((snapshot) => isRecord(snapshot.data) &&
|
|
69
|
+
snapshot.data.kind === "command" &&
|
|
70
|
+
snapshot.data.phase === "after" &&
|
|
71
|
+
snapshot.data.status === "passed") ?? null;
|
|
72
|
+
}
|
|
73
|
+
/** v3.2: Derive the machine-verification status of a round. The verification
|
|
74
|
+
* capability is modeled explicitly (verified / unavailable / absent) instead
|
|
75
|
+
* of letting evidence-dependent checks silently disappear when snapshots are
|
|
76
|
+
* missing — the fix for the "weakest when it matters most" gap. */
|
|
77
|
+
export function deriveEvidenceStatus(selfEval, evidenceSnapshots) {
|
|
78
|
+
const gitSnap = evidenceSnapshots.find((snapshot) => snapshot.provider === "git") ?? null;
|
|
79
|
+
const gitObserved = gitSnap !== null && gitSnap.files.length > 0;
|
|
80
|
+
const command = passedAfterCommand(evidenceSnapshots);
|
|
81
|
+
const commandVerified = command !== null;
|
|
82
|
+
// testsMachineBacked: agent-reported test_results agree with a passed
|
|
83
|
+
// command's parsed stdout (the same comparison checkCommandEvidenceIntegrity
|
|
84
|
+
// performs — parseTestOutput lives in this module).
|
|
85
|
+
let testsMachineBacked = false;
|
|
86
|
+
const reported = selfEval.execution_evidence?.test_results;
|
|
87
|
+
if (command && reported) {
|
|
88
|
+
const parsed = parseTestOutput(String(command.data.stdout ?? ""));
|
|
89
|
+
testsMachineBacked = parsed !== null &&
|
|
90
|
+
parsed.passed === reported.passed &&
|
|
91
|
+
parsed.failed === reported.failed &&
|
|
92
|
+
parsed.skipped === reported.skipped;
|
|
93
|
+
}
|
|
94
|
+
// reportedFilesMatch: agent-reported files_changed equals the git diff set.
|
|
95
|
+
let reportedFilesMatch = false;
|
|
96
|
+
if (gitSnap && selfEval.execution_evidence) {
|
|
97
|
+
const reportedSet = [...selfEval.execution_evidence.files_changed].sort();
|
|
98
|
+
const actualSet = [...gitSnap.files].sort();
|
|
99
|
+
reportedFilesMatch = reportedSet.length === actualSet.length &&
|
|
100
|
+
reportedSet.every((file, index) => file === actualSet[index]);
|
|
101
|
+
}
|
|
102
|
+
const providerStatus = evidenceSnapshots.length === 0
|
|
103
|
+
? "absent"
|
|
104
|
+
: gitObserved || commandVerified ? "verified" : "unavailable";
|
|
105
|
+
return {
|
|
106
|
+
providerStatus,
|
|
107
|
+
gitObserved,
|
|
108
|
+
commandVerified,
|
|
109
|
+
testsMachineBacked,
|
|
110
|
+
reportedFilesMatch,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** v3.2: Per-round machine progress — whether git observed file changes in
|
|
114
|
+
* each of the last `lookback` committed rounds, rebuilt from the feedback
|
|
115
|
+
* entries' roundEvidence snapshots (already persisted at commit). Returns
|
|
116
|
+
* null when fewer than `lookback` rounds carry git snapshots — the machine
|
|
117
|
+
* signal is unavailable and callers (R4/R5 on the heuristic path) keep
|
|
118
|
+
* their legacy skip. The snapshot shape is parsed defensively here because
|
|
119
|
+
* this module must not import round-transaction (import cycle). */
|
|
120
|
+
export function machineProgressSeries(vaultEntries, currentRound, lookback) {
|
|
121
|
+
const byRound = new Map();
|
|
122
|
+
for (const entry of vaultEntries) {
|
|
123
|
+
const tid = String(entry.task_id ?? "");
|
|
124
|
+
if (!tid.endsWith(":feedback"))
|
|
125
|
+
continue;
|
|
126
|
+
const rnd = entryRound(entry);
|
|
127
|
+
if (rnd <= 0 || rnd >= currentRound)
|
|
128
|
+
continue;
|
|
129
|
+
const rt = isRecord(entry.loop_lineage)
|
|
130
|
+
? entry.loop_lineage.round_transaction
|
|
131
|
+
: null;
|
|
132
|
+
const snapshot = isRecord(rt) ? rt.snapshot : null;
|
|
133
|
+
const roundEvidence = isRecord(snapshot) ? snapshot.roundEvidence : null;
|
|
134
|
+
if (!Array.isArray(roundEvidence))
|
|
135
|
+
continue;
|
|
136
|
+
const git = roundEvidence.find((value) => isRecord(value) && value.provider === "git");
|
|
137
|
+
if (!git)
|
|
138
|
+
continue;
|
|
139
|
+
const gitFiles = git.files;
|
|
140
|
+
byRound.set(rnd, Array.isArray(gitFiles) && gitFiles.length > 0);
|
|
141
|
+
}
|
|
142
|
+
if (byRound.size < lookback)
|
|
143
|
+
return null;
|
|
144
|
+
const sorted = [...byRound.keys()].sort((a, b) => a - b);
|
|
145
|
+
const recent = sorted.slice(-lookback);
|
|
146
|
+
// Continuity: the recent rounds must be the actual last `lookback` rounds.
|
|
147
|
+
if (recent[0] < currentRound - lookback)
|
|
148
|
+
return null;
|
|
149
|
+
return recent.map((round) => byRound.get(round));
|
|
150
|
+
}
|
|
151
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
152
|
+
// Individual checks — each returns a VerificationFlag or null
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
154
|
+
function checkProgressRegression(selfEval, prevSelfEval) {
|
|
155
|
+
if (!prevSelfEval?.execution_evidence)
|
|
156
|
+
return null;
|
|
157
|
+
if (!selfEval.execution_evidence)
|
|
158
|
+
return null;
|
|
159
|
+
const prevProgress = prevSelfEval.execution_evidence.progress_estimate;
|
|
160
|
+
const currProgress = selfEval.execution_evidence.progress_estimate;
|
|
161
|
+
if (typeof prevProgress !== "number" || typeof currProgress !== "number")
|
|
162
|
+
return null;
|
|
163
|
+
// Use delta with epsilon to avoid IEEE 754 rounding issues (0.8 - 0.2 > 0.6 in float)
|
|
164
|
+
if (prevProgress - currProgress <= 0.2 + 1e-10)
|
|
165
|
+
return null;
|
|
166
|
+
return makeVerificationFlag({
|
|
167
|
+
severity: "warn",
|
|
168
|
+
field: "progress_estimate",
|
|
169
|
+
check: CHECK_PROGRESS_REGRESSION,
|
|
170
|
+
detail: `Progress dropped from ${prevProgress.toFixed(2)} to ` +
|
|
171
|
+
`${currProgress.toFixed(2)} (delta: ${(currProgress - prevProgress).toFixed(2)})`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function checkEmptyChangeWithPassing(selfEval) {
|
|
175
|
+
const ev = selfEval.execution_evidence;
|
|
176
|
+
if (!ev)
|
|
177
|
+
return null;
|
|
178
|
+
const filesEmpty = ev.files_changed.length === 0;
|
|
179
|
+
const testsAllPass = ev.test_results !== null &&
|
|
180
|
+
ev.test_results.failed === 0 &&
|
|
181
|
+
ev.test_results.passed > 0;
|
|
182
|
+
if (!filesEmpty || !testsAllPass || !effectiveSuccess(selfEval))
|
|
183
|
+
return null;
|
|
184
|
+
return makeVerificationFlag({
|
|
185
|
+
severity: "warn",
|
|
186
|
+
field: "execution_evidence",
|
|
187
|
+
check: CHECK_EMPTY_CHANGE_WITH_PASSING,
|
|
188
|
+
detail: "Agent claims success with no files changed and all tests passing — " +
|
|
189
|
+
"verify that work was actually performed",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function checkSuccessWithRemainingCriteria(selfEval) {
|
|
193
|
+
if (!effectiveSuccess(selfEval))
|
|
194
|
+
return null;
|
|
195
|
+
const remaining = selfEval.execution_evidence?.success_criteria_remaining;
|
|
196
|
+
if (!remaining || remaining.length === 0)
|
|
197
|
+
return null;
|
|
198
|
+
return makeVerificationFlag({
|
|
199
|
+
severity: "error",
|
|
200
|
+
field: "success",
|
|
201
|
+
check: CHECK_SUCCESS_WITH_REMAINING_CRITERIA,
|
|
202
|
+
detail: `Agent claims success but ${remaining.length} criteria remain unmet: ` +
|
|
203
|
+
remaining.slice(0, 3).join("; "),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** v2.12: Criteria reported met but none machine-verified. The agent puts
|
|
207
|
+
* completed criteria into the ledger, but no passing test evidence or
|
|
208
|
+
* command snapshot backs them. Warn — completion still counts, but the
|
|
209
|
+
* next prompt tells the agent its claims are unverified. This is the
|
|
210
|
+
* mild form of the 3.x criterion-specific completion rule. */
|
|
211
|
+
function checkUnverifiedCriteriaClaims(selfEval, claimView) {
|
|
212
|
+
const met = selfEval.execution_evidence?.success_criteria_met;
|
|
213
|
+
if (!met || met.length === 0)
|
|
214
|
+
return null;
|
|
215
|
+
if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
|
|
216
|
+
return null;
|
|
217
|
+
return makeVerificationFlag({
|
|
218
|
+
severity: "warn",
|
|
219
|
+
field: "execution_evidence",
|
|
220
|
+
check: CHECK_CRITERIA_CLAIMS_UNVERIFIED,
|
|
221
|
+
detail: `${met.length} criteria reported met but none machine-verified ` +
|
|
222
|
+
"(no passing test evidence or command snapshot)",
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
/** v2.12: Declared outcome vs legacy boolean consistency. A declared success
|
|
226
|
+
* with success=false is a self-contradiction (error); a declared non-success
|
|
227
|
+
* with success=true is a compat conflict (warn). Also: outcome==="blocked"
|
|
228
|
+
* without a blocker description gets a warn so the next prompt asks for it. */
|
|
229
|
+
function checkOutcomeConsistency(selfEval) {
|
|
230
|
+
const outcome = selfEval.outcome;
|
|
231
|
+
if (outcome === "success" && selfEval.success === false) {
|
|
232
|
+
return makeVerificationFlag({
|
|
233
|
+
severity: "error",
|
|
234
|
+
field: "outcome",
|
|
235
|
+
check: CHECK_OUTCOME_SUCCESS_CONTRADICTION,
|
|
236
|
+
detail: "outcome=success but success=false — self-contradictory",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (outcome !== undefined && outcome !== "success" && selfEval.success === true) {
|
|
240
|
+
return makeVerificationFlag({
|
|
241
|
+
severity: "warn",
|
|
242
|
+
field: "success",
|
|
243
|
+
check: CHECK_SUCCESS_CLAIM_CONFLICT,
|
|
244
|
+
detail: `outcome=${outcome} but success=true — the declared outcome wins`,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
if (outcome === "blocked" &&
|
|
248
|
+
(typeof selfEval.blocker !== "string" || selfEval.blocker.trim().length === 0)) {
|
|
249
|
+
return makeVerificationFlag({
|
|
250
|
+
severity: "warn",
|
|
251
|
+
field: "blocker",
|
|
252
|
+
check: CHECK_BLOCKED_WITHOUT_BLOCKER,
|
|
253
|
+
detail: "outcome=blocked but no blocker description was provided",
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
/** v2.12: Retroactive claims reference PRIOR rounds. Each claim's file
|
|
259
|
+
* paths are checked against the referenced round's committed git evidence
|
|
260
|
+
* (P0 provenance layer). Unverifiable claims warn — they never upgrade
|
|
261
|
+
* criteria status. */
|
|
262
|
+
function checkRetroactiveClaims(selfEval, vaultEntries, currentRound) {
|
|
263
|
+
const claims = selfEval.retroactiveClaims;
|
|
264
|
+
if (!claims || claims.length === 0)
|
|
265
|
+
return null;
|
|
266
|
+
const loopId = vaultEntries.find((entry) => typeof entry.loop_id === "string")
|
|
267
|
+
?.loop_id;
|
|
268
|
+
const flags = [];
|
|
269
|
+
for (const claim of claims) {
|
|
270
|
+
if (claim.round < 1 || claim.round >= currentRound) {
|
|
271
|
+
flags.push(makeVerificationFlag({
|
|
272
|
+
severity: "warn",
|
|
273
|
+
field: "retroactiveClaims",
|
|
274
|
+
check: CHECK_RETROACTIVE_CLAIM_BAD_ROUND,
|
|
275
|
+
detail: `retroactive claim targets round ${claim.round} (must be a prior round)`,
|
|
276
|
+
}));
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
if (!loopId)
|
|
280
|
+
continue; // no vault context → cannot verify (fail-open)
|
|
281
|
+
const observed = resolveRoundFiles(vaultEntries, loopId, claim.round);
|
|
282
|
+
if (observed === null) {
|
|
283
|
+
flags.push(makeVerificationFlag({
|
|
284
|
+
severity: "warn",
|
|
285
|
+
field: "retroactiveClaims",
|
|
286
|
+
check: CHECK_RETROACTIVE_CLAIM_UNVERIFIED,
|
|
287
|
+
detail: `retroactive claim for round ${claim.round} has no observable git evidence`,
|
|
288
|
+
}));
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const paths = extractFilePaths(claim.claim);
|
|
292
|
+
const unverified = paths.filter((path) => !observed.some((file) => file === path || file.endsWith(path) || path.endsWith(file)));
|
|
293
|
+
if (paths.length > 0 && unverified.length > 0) {
|
|
294
|
+
flags.push(makeVerificationFlag({
|
|
295
|
+
severity: "warn",
|
|
296
|
+
field: "retroactiveClaims",
|
|
297
|
+
check: CHECK_RETROACTIVE_CLAIM_UNVERIFIED,
|
|
298
|
+
detail: `retroactive claim references files not observed in round ${claim.round}: ` +
|
|
299
|
+
unverified.slice(0, 3).join(", "),
|
|
300
|
+
}));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return flags.length > 0 ? flags[0] : null;
|
|
304
|
+
}
|
|
305
|
+
/** v2.12: success with zero machine-verifiable evidence. The agent claims
|
|
306
|
+
* completion but nothing the runtime observed backs it — no verified claims
|
|
307
|
+
* (test evidence), no test results, no required commands. A declared
|
|
308
|
+
* no_change_reason is the honest escape hatch: it downgrades to info. */
|
|
309
|
+
function checkSuccessWithoutVerifiedEvidence(selfEval, claimView) {
|
|
310
|
+
if (!effectiveSuccess(selfEval))
|
|
311
|
+
return null;
|
|
312
|
+
if (claimView.verifiedCount > 0 || claimView.hasMachineEvidence)
|
|
313
|
+
return null;
|
|
314
|
+
const noChange = typeof selfEval.no_change_reason === "string" &&
|
|
315
|
+
selfEval.no_change_reason.trim().length > 0;
|
|
316
|
+
return makeVerificationFlag({
|
|
317
|
+
severity: noChange ? "info" : "error",
|
|
318
|
+
field: "success",
|
|
319
|
+
check: CHECK_SUCCESS_WITHOUT_VERIFIED_EVIDENCE,
|
|
320
|
+
detail: noChange
|
|
321
|
+
? "success with zero verified claims (no_change_reason declared)"
|
|
322
|
+
: "success with zero verified claims and no test evidence",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/** v3.2: success declared but no machine-verified observation this round.
|
|
326
|
+
* Unlike R8 (which keys off the agent's SELF-REPORTED test_results), this
|
|
327
|
+
* keys off the runtime-derived providerStatus: git diff observed, or a
|
|
328
|
+
* passed after-command. Warn-level by design — the round commits, but its
|
|
329
|
+
* success never enters the trajectory (and trust drops). A declared
|
|
330
|
+
* no_change_reason is the honest escape hatch, mirroring R8.
|
|
331
|
+
* Crucially this check does NOT self-skip on the heuristic-extraction path:
|
|
332
|
+
* an unstructured success claim without machine observation is flagged
|
|
333
|
+
* exactly the same — the check degrades, it never disappears. */
|
|
334
|
+
function checkSuccessUnverified(selfEval, status) {
|
|
335
|
+
if (!effectiveSuccess(selfEval))
|
|
336
|
+
return null;
|
|
337
|
+
if (typeof selfEval.no_change_reason === "string" &&
|
|
338
|
+
selfEval.no_change_reason.trim().length > 0)
|
|
339
|
+
return null;
|
|
340
|
+
if (status.providerStatus === "verified")
|
|
341
|
+
return null;
|
|
342
|
+
return makeVerificationFlag({
|
|
343
|
+
severity: "warn",
|
|
344
|
+
field: "success",
|
|
345
|
+
check: CHECK_SUCCESS_UNVERIFIED,
|
|
346
|
+
detail: `success declared but no machine-verified observation this round (providerStatus: ${status.providerStatus})`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
function checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations) {
|
|
350
|
+
const discovered = selfEval.discovered_constraints;
|
|
351
|
+
if (!discovered || discovered.length === 0)
|
|
352
|
+
return null;
|
|
353
|
+
// Collect all previously-known constraints
|
|
354
|
+
const known = new Set();
|
|
355
|
+
for (const v of olderViolations)
|
|
356
|
+
known.add(v.toLowerCase().trim());
|
|
357
|
+
if (prevSelfEval) {
|
|
358
|
+
for (const d of prevSelfEval.discovered_constraints ?? []) {
|
|
359
|
+
known.add(d.toLowerCase().trim());
|
|
360
|
+
}
|
|
361
|
+
// Also treat previous violations as implicitly discovered
|
|
362
|
+
for (const v of prevSelfEval.constraint_violations) {
|
|
363
|
+
known.add(v.toLowerCase().trim());
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
for (const d of discovered) {
|
|
367
|
+
if (known.has(d.toLowerCase().trim())) {
|
|
368
|
+
return makeVerificationFlag({
|
|
369
|
+
severity: "warn",
|
|
370
|
+
field: "discovered_constraints",
|
|
371
|
+
check: CHECK_DUPLICATE_CONSTRAINT_DISCOVERY,
|
|
372
|
+
detail: `Constraint "${d}" was already known from a previous round`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
function checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
379
|
+
const currViols = selfEval.constraint_violations;
|
|
380
|
+
if (!currViols || currViols.length === 0)
|
|
381
|
+
return null;
|
|
382
|
+
// Build the violation history: [round N-2, round N-1, round N]
|
|
383
|
+
const violationsByRound = [];
|
|
384
|
+
// Round N-2 violations come from the vault entry for round N-1
|
|
385
|
+
// (persistLoopLineage stores the PREVIOUS round's violations on each entry)
|
|
386
|
+
if (currentRound >= 3) {
|
|
387
|
+
const entryNMinus1 = vaultEntries.find((e) => entryRound(e) === currentRound - 1);
|
|
388
|
+
if (entryNMinus1) {
|
|
389
|
+
const viols = entryViolations(entryNMinus1);
|
|
390
|
+
if (viols.length)
|
|
391
|
+
violationsByRound.push(viols.map((v) => v.toLowerCase().trim()));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Round N-1 violations from prevSelfEval
|
|
395
|
+
if (prevSelfEval) {
|
|
396
|
+
const prevViols = prevSelfEval.constraint_violations;
|
|
397
|
+
violationsByRound.push(prevViols.map((v) => v.toLowerCase().trim()));
|
|
398
|
+
}
|
|
399
|
+
// Round N violations from current selfEval
|
|
400
|
+
violationsByRound.push(currViols.map((v) => v.toLowerCase().trim()));
|
|
401
|
+
// Need at least 3 rounds of data
|
|
402
|
+
if (violationsByRound.length < 3)
|
|
403
|
+
return null;
|
|
404
|
+
// Check each current violation against the previous 2 rounds
|
|
405
|
+
const [rNMinus2, rNMinus1, rN] = violationsByRound.slice(-3);
|
|
406
|
+
for (const v of rN) {
|
|
407
|
+
if (rNMinus1.includes(v) && rNMinus2.includes(v)) {
|
|
408
|
+
return makeVerificationFlag({
|
|
409
|
+
severity: "error",
|
|
410
|
+
field: "constraint_violations",
|
|
411
|
+
check: CHECK_RECURRING_VIOLATION,
|
|
412
|
+
detail: `Constraint violation "${v}" has appeared in 3 consecutive rounds ` +
|
|
413
|
+
`(rounds ${currentRound - 2}–${currentRound}) without resolution`,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
function checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound) {
|
|
420
|
+
const retracted = selfEval.retracted_constraints;
|
|
421
|
+
if (!retracted || retracted.length === 0)
|
|
422
|
+
return null;
|
|
423
|
+
if (!prevSelfEval)
|
|
424
|
+
return null;
|
|
425
|
+
const lastRoundDiscoveries = new Set();
|
|
426
|
+
for (const d of prevSelfEval.discovered_constraints ?? []) {
|
|
427
|
+
lastRoundDiscoveries.add(d.toLowerCase().trim());
|
|
428
|
+
}
|
|
429
|
+
for (const r of retracted) {
|
|
430
|
+
if (lastRoundDiscoveries.has(r.toLowerCase().trim())) {
|
|
431
|
+
return makeVerificationFlag({
|
|
432
|
+
severity: "warn",
|
|
433
|
+
field: "retracted_constraints",
|
|
434
|
+
check: CHECK_RETRACT_FRESH_CONSTRAINT,
|
|
435
|
+
detail: `Retracting constraint "${r}" that was just discovered in round ` +
|
|
436
|
+
`${currentRound - 1} — may indicate rapid flip-flopping`,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
/** v1.18: Cross-validate agent-reported files_changed against git evidence
|
|
443
|
+
* from the configured evidence providers (ProviderSnapshot array). */
|
|
444
|
+
function checkEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
445
|
+
if (!selfEval.execution_evidence)
|
|
446
|
+
return null;
|
|
447
|
+
if (evidenceSnapshots.length === 0)
|
|
448
|
+
return null;
|
|
449
|
+
const gitSnap = evidenceSnapshots.find((s) => s.provider === "git");
|
|
450
|
+
if (!gitSnap)
|
|
451
|
+
return null;
|
|
452
|
+
const reported = [...selfEval.execution_evidence.files_changed].sort();
|
|
453
|
+
const actual = [...gitSnap.files].sort();
|
|
454
|
+
if (reported.length === 0 && actual.length === 0)
|
|
455
|
+
return null;
|
|
456
|
+
if (reported.length === 0 && actual.length > 0) {
|
|
457
|
+
return makeVerificationFlag({
|
|
458
|
+
severity: "warn",
|
|
459
|
+
field: "files_changed",
|
|
460
|
+
check: CHECK_EVIDENCE_INTEGRITY,
|
|
461
|
+
detail: `Agent reported no files changed but evidence shows: ${actual.join(", ")}`,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const ghostFiles = reported.filter((f) => !actual.includes(f));
|
|
465
|
+
const missedFiles = actual.filter((f) => !reported.includes(f));
|
|
466
|
+
if (ghostFiles.length > 0 || missedFiles.length > 0) {
|
|
467
|
+
const parts = [];
|
|
468
|
+
if (ghostFiles.length > 0)
|
|
469
|
+
parts.push(`unconfirmed: [${ghostFiles.join(", ")}]`);
|
|
470
|
+
if (missedFiles.length > 0)
|
|
471
|
+
parts.push(`unreported: [${missedFiles.join(", ")}]`);
|
|
472
|
+
return makeVerificationFlag({
|
|
473
|
+
severity: "warn",
|
|
474
|
+
field: "files_changed",
|
|
475
|
+
check: CHECK_EVIDENCE_INTEGRITY,
|
|
476
|
+
detail: `Agent files_changed doesn't match evidence: ${parts.join("; ")}`,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
/** A required, explicitly configured verification command is authoritative
|
|
482
|
+
* when the Agent claims success. Optional commands remain observational. */
|
|
483
|
+
function checkRequiredCommandEvidence(selfEval, evidenceSnapshots) {
|
|
484
|
+
if (!effectiveSuccess(selfEval))
|
|
485
|
+
return null;
|
|
486
|
+
for (const snapshot of evidenceSnapshots) {
|
|
487
|
+
if (snapshot.data.kind !== "command")
|
|
488
|
+
continue;
|
|
489
|
+
if (snapshot.data.phase !== "after" || snapshot.data.required !== true)
|
|
490
|
+
continue;
|
|
491
|
+
const status = snapshot.data.status;
|
|
492
|
+
if (status === "passed")
|
|
493
|
+
continue;
|
|
494
|
+
const name = typeof snapshot.data.commandName === "string"
|
|
495
|
+
? snapshot.data.commandName
|
|
496
|
+
: snapshot.provider;
|
|
497
|
+
const exitCode = typeof snapshot.data.exitCode === "number"
|
|
498
|
+
? ` (exit ${snapshot.data.exitCode})`
|
|
499
|
+
: "";
|
|
500
|
+
return makeVerificationFlag({
|
|
501
|
+
severity: "error",
|
|
502
|
+
field: "execution_evidence",
|
|
503
|
+
check: CHECK_REQUIRED_COMMAND_FAILED,
|
|
504
|
+
detail: `Agent claims success but required command "${name}" ${String(status)}${exitCode}`,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
// ── Test output parsing ────────────────────────────────────────────────────
|
|
510
|
+
// jaccardSimilarity() imported from token-utils.ts (v2.1 — intent-action drift detection)
|
|
511
|
+
/** Extract stable sub-goal IDs (sg-XXXXXXXX) mentioned in text.
|
|
512
|
+
* Matches the ID format rendered in prompts (loop-compiler deriveSubGoalId). */
|
|
513
|
+
function extractSubGoalIds(text) {
|
|
514
|
+
return [...new Set(text.toLowerCase().match(/sg-[a-f0-9]{8}/g) ?? [])];
|
|
515
|
+
}
|
|
516
|
+
/** Extract file-path-like tokens (e.g. "src/auth/login.ts") from text. */
|
|
517
|
+
function extractFilePaths(text) {
|
|
518
|
+
return [...new Set(text.match(/[\w./-]+\.[a-z]{2,6}\b/gi) ?? [])];
|
|
519
|
+
}
|
|
520
|
+
/** Whether a changed path looks like a test file. */
|
|
521
|
+
function isTestFile(path) {
|
|
522
|
+
return /\.(test|spec)\.[a-z0-9]+$/i.test(path) ||
|
|
523
|
+
/(^|[\\/])tests?[\\/]/i.test(path) ||
|
|
524
|
+
/_test\.[a-z0-9]+$/i.test(path);
|
|
525
|
+
}
|
|
526
|
+
/** Map each referenced sub-goal ID to its known description(s), reconstructed
|
|
527
|
+
* from emerged_subtasks in the vault lineage and the previous self-eval. */
|
|
528
|
+
function referencedSubGoalDescriptions(ids, vaultEntries, prevSelfEval) {
|
|
529
|
+
const map = new Map();
|
|
530
|
+
for (const id of ids)
|
|
531
|
+
map.set(id, []);
|
|
532
|
+
const descriptions = [];
|
|
533
|
+
for (const entry of vaultEntries) {
|
|
534
|
+
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
535
|
+
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
536
|
+
: [];
|
|
537
|
+
descriptions.push(...emerged);
|
|
538
|
+
}
|
|
539
|
+
descriptions.push(...(prevSelfEval?.emerged_subtasks ?? []));
|
|
540
|
+
for (const desc of descriptions) {
|
|
541
|
+
const trimmed = desc.trim();
|
|
542
|
+
if (!trimmed)
|
|
543
|
+
continue;
|
|
544
|
+
const id = deriveSubGoalId(trimmed);
|
|
545
|
+
if (map.has(id))
|
|
546
|
+
map.get(id).push(trimmed);
|
|
547
|
+
}
|
|
548
|
+
return map;
|
|
549
|
+
}
|
|
550
|
+
/** v2.1 (check 11): Compare the previous round's declared next_action with
|
|
551
|
+
* the current round's actual output_summary. Detect when the agent says it
|
|
552
|
+
* will do X but then does Y — silent drift without explanation.
|
|
553
|
+
*
|
|
554
|
+
* Only fires when both next_action and output_summary are non-empty.
|
|
555
|
+
*
|
|
556
|
+
* v2.14: Structured-ID-first detection. Alignment is decided by a waterfall
|
|
557
|
+
* of concrete signals before falling back to string similarity:
|
|
558
|
+
* 1. Sub-goal IDs (sg-XXXXXXXX) in next_action matched against this
|
|
559
|
+
* round's completed_subtasks (exact ID, ID derived from the
|
|
560
|
+
* description text, or Jaccard against the referenced sub-goal's
|
|
561
|
+
* known description).
|
|
562
|
+
* 2. File paths named in next_action appearing in files_changed.
|
|
563
|
+
* 3. Test evidence — next_action mentions tests and test files were
|
|
564
|
+
* changed with tests actually run.
|
|
565
|
+
* 4. Jaccard token similarity against the policy threshold (weak signal).
|
|
566
|
+
* Any signal aligning is sufficient — text similarity is never the sole
|
|
567
|
+
* authority when structured evidence exists. */
|
|
568
|
+
function checkIntentDrift(selfEval, prevSelfEval, vaultEntries) {
|
|
569
|
+
if (!prevSelfEval?.next_action?.trim())
|
|
570
|
+
return null;
|
|
571
|
+
if (!selfEval.output_summary.trim())
|
|
572
|
+
return null;
|
|
573
|
+
const intent = prevSelfEval.next_action.trim();
|
|
574
|
+
const actual = selfEval.output_summary.trim();
|
|
575
|
+
const policy = getPolicy();
|
|
576
|
+
const filesChanged = selfEval.execution_evidence?.files_changed ?? [];
|
|
577
|
+
// ── Signal 1: sub-goal IDs referenced by next_action → completed this round
|
|
578
|
+
const intentIds = extractSubGoalIds(intent);
|
|
579
|
+
if (intentIds.length > 0) {
|
|
580
|
+
const completed = selfEval.completed_subtasks ?? [];
|
|
581
|
+
const referenced = referencedSubGoalDescriptions(intentIds, vaultEntries, prevSelfEval);
|
|
582
|
+
const idMatched = intentIds.some((id) => completed.some((entry) => {
|
|
583
|
+
const text = entry.trim();
|
|
584
|
+
if (!text)
|
|
585
|
+
return false;
|
|
586
|
+
if (text.toLowerCase() === id)
|
|
587
|
+
return true;
|
|
588
|
+
if (deriveSubGoalId(text) === id)
|
|
589
|
+
return true;
|
|
590
|
+
const descriptions = referenced.get(id) ?? [];
|
|
591
|
+
return descriptions.some((desc) => jaccardSimilarity(text, desc) >= policy.evolution.subgoal_match_threshold);
|
|
592
|
+
}));
|
|
593
|
+
if (idMatched)
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
// ── Signal 2: file paths named in next_action appear in files_changed
|
|
597
|
+
const intentPaths = extractFilePaths(intent);
|
|
598
|
+
if (intentPaths.length > 0 && filesChanged.length > 0) {
|
|
599
|
+
const pathMatched = intentPaths.some((p) => filesChanged.some((f) => f === p || f.endsWith(p) || p.endsWith(f)));
|
|
600
|
+
if (pathMatched)
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
// ── Signal 3: test evidence — intent mentions tests, test files changed,
|
|
604
|
+
// and tests actually ran this round
|
|
605
|
+
const intentTokens = tokenize(intent);
|
|
606
|
+
const mentionsTests = intentTokens.has("test") || intentTokens.has("tests");
|
|
607
|
+
const testResults = selfEval.execution_evidence?.test_results;
|
|
608
|
+
const testsRan = !!testResults && testResults.passed + testResults.failed > 0;
|
|
609
|
+
const testFilesChanged = filesChanged.some((f) => isTestFile(f));
|
|
610
|
+
if (mentionsTests && testsRan && testFilesChanged)
|
|
611
|
+
return null;
|
|
612
|
+
// ── Signal 4 (weak): Jaccard token similarity
|
|
613
|
+
const score = jaccardSimilarity(intent, actual);
|
|
614
|
+
const threshold = policy.evolution.intent_drift_threshold;
|
|
615
|
+
if (score >= threshold)
|
|
616
|
+
return null;
|
|
617
|
+
const idNote = intentIds.length > 0
|
|
618
|
+
? `; ${intentIds.length} referenced sub-goal ID(s) did not match completed_subtasks`
|
|
619
|
+
: "";
|
|
620
|
+
return makeVerificationFlag({
|
|
621
|
+
severity: "warn",
|
|
622
|
+
field: "output_summary",
|
|
623
|
+
check: CHECK_INTENT_DRIFT,
|
|
624
|
+
detail: `Declared intent was "${intent.slice(0, 120)}" ` +
|
|
625
|
+
`but actual output "${actual.slice(0, 120)}" — ` +
|
|
626
|
+
`similarity ${(score * 100).toFixed(0)}% below threshold ${(threshold * 100).toFixed(0)}%` +
|
|
627
|
+
idNote,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
/** Best-effort parse of test counts from common test-runner output formats.
|
|
631
|
+
*
|
|
632
|
+
* Scans the last 2000 characters of stdout (where summary lines typically
|
|
633
|
+
* appear) and tries patterns in descending specificity order. Returns null
|
|
634
|
+
* when the output format is unrecognized, stdout is empty, or a confident
|
|
635
|
+
* parse cannot be made.
|
|
636
|
+
*
|
|
637
|
+
* Recognized formats: Jest verbose/compact, Mocha, pytest/unittest, Go test,
|
|
638
|
+
* and PHPUnit OK summaries. */
|
|
639
|
+
export function parseTestOutput(stdout) {
|
|
640
|
+
if (!stdout)
|
|
641
|
+
return null;
|
|
642
|
+
const tail = stdout.length > 2000 ? stdout.slice(-2000) : stdout;
|
|
643
|
+
let match;
|
|
644
|
+
// 1. Jest verbose: "Tests: 1 failed, 8 passed, 9 total"
|
|
645
|
+
match = tail.match(/Tests:\s*(\d+)\s+failed,\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
|
|
646
|
+
if (match) {
|
|
647
|
+
return { failed: Number(match[1]), passed: Number(match[2]), total: Number(match[3]), skipped: 0 };
|
|
648
|
+
}
|
|
649
|
+
// 2. Jest compact: "Tests: 8 passed, 9 total"
|
|
650
|
+
match = tail.match(/Tests:\s*(\d+)\s+passed,\s*(\d+)\s+total/i);
|
|
651
|
+
if (match) {
|
|
652
|
+
const passed = Number(match[1]);
|
|
653
|
+
const total = Number(match[2]);
|
|
654
|
+
return { passed, failed: total - passed, total, skipped: 0 };
|
|
655
|
+
}
|
|
656
|
+
// 3. Mocha: "8 passing" + optional "2 failing"
|
|
657
|
+
const mochaPass = tail.match(/(\d+)\s+passing/i);
|
|
658
|
+
if (mochaPass) {
|
|
659
|
+
const mochaFail = tail.match(/(\d+)\s+failing/i);
|
|
660
|
+
const passed = Number(mochaPass[1]);
|
|
661
|
+
const failed = mochaFail ? Number(mochaFail[1]) : 0;
|
|
662
|
+
return { passed, failed, total: passed + failed, skipped: 0 };
|
|
663
|
+
}
|
|
664
|
+
// 4. Go test: count "--- PASS:" and "--- FAIL:" lines
|
|
665
|
+
const goPassCount = (tail.match(/---\s+PASS:/g) || []).length;
|
|
666
|
+
const goFailCount = (tail.match(/---\s+FAIL:/g) || []).length;
|
|
667
|
+
if (goPassCount > 0 || goFailCount > 0) {
|
|
668
|
+
return { passed: goPassCount, failed: goFailCount, total: goPassCount + goFailCount, skipped: 0 };
|
|
669
|
+
}
|
|
670
|
+
// 5. pytest / unittest: "8 passed, 1 failed" or "8 passed"
|
|
671
|
+
match = tail.match(/(\d+)\s+passed[,;\s]+(\d+)\s+failed/i);
|
|
672
|
+
if (match) {
|
|
673
|
+
return { passed: Number(match[1]), failed: Number(match[2]), total: Number(match[1]) + Number(match[2]), skipped: 0 };
|
|
674
|
+
}
|
|
675
|
+
// pytest compact: "8 passed" (with a reasonable context hint)
|
|
676
|
+
match = tail.match(/(?:=\s+test session|\b(?:passed|failed)\b)/i);
|
|
677
|
+
if (match) {
|
|
678
|
+
const pCount = tail.match(/(\d+)\s+passed/i);
|
|
679
|
+
const fCount = tail.match(/(\d+)\s+failed/i);
|
|
680
|
+
if (pCount || fCount) {
|
|
681
|
+
const passed = pCount ? Number(pCount[1]) : 0;
|
|
682
|
+
const failed = fCount ? Number(fCount[1]) : 0;
|
|
683
|
+
return { passed, failed, total: passed + failed, skipped: 0 };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// 6. PHPUnit: "OK (8 tests, 16 assertions)"
|
|
687
|
+
match = tail.match(/OK\s*\((\d+)\s+tests?/i);
|
|
688
|
+
if (match) {
|
|
689
|
+
const total = Number(match[1]);
|
|
690
|
+
return { passed: total, failed: 0, total, skipped: 0 };
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
/** Cross-validate agent-reported test_results against command evidence output.
|
|
695
|
+
*
|
|
696
|
+
* For each command provider whose status is "passed" and output is not
|
|
697
|
+
* truncated, attempts to parse test counts from stdout. Mismatched counts
|
|
698
|
+
* produce a warn-level flag; failures hidden by the agent (reported 0 failed
|
|
699
|
+
* when the command shows >0) produce an error-level flag.
|
|
700
|
+
*
|
|
701
|
+
* Non-command evidence providers are skipped (no structural cross-check is
|
|
702
|
+
* defined for them yet). */
|
|
703
|
+
function checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots) {
|
|
704
|
+
const reported = selfEval.execution_evidence?.test_results;
|
|
705
|
+
if (!reported)
|
|
706
|
+
return null;
|
|
707
|
+
for (const snapshot of evidenceSnapshots) {
|
|
708
|
+
if (snapshot.data.kind !== "command")
|
|
709
|
+
continue;
|
|
710
|
+
if (snapshot.data.status !== "passed")
|
|
711
|
+
continue;
|
|
712
|
+
if (snapshot.data.truncated === true)
|
|
713
|
+
continue;
|
|
714
|
+
const stdout = typeof snapshot.data.stdout === "string" ? snapshot.data.stdout : "";
|
|
715
|
+
const parsed = parseTestOutput(stdout);
|
|
716
|
+
if (!parsed)
|
|
717
|
+
continue;
|
|
718
|
+
const mismatches = [];
|
|
719
|
+
if (parsed.passed !== reported.passed) {
|
|
720
|
+
mismatches.push(`passed: reported ${reported.passed}, command shows ${parsed.passed}`);
|
|
721
|
+
}
|
|
722
|
+
if (parsed.failed !== reported.failed) {
|
|
723
|
+
mismatches.push(`failed: reported ${reported.failed}, command shows ${parsed.failed}`);
|
|
724
|
+
}
|
|
725
|
+
if (mismatches.length === 0) {
|
|
726
|
+
// Counts match exactly — evidence supports the agent's claim.
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
const name = typeof snapshot.data.commandName === "string"
|
|
730
|
+
? snapshot.data.commandName
|
|
731
|
+
: snapshot.provider;
|
|
732
|
+
// Agent hides failures → error. Other mismatch → warn.
|
|
733
|
+
const severity = parsed.failed > 0 && reported.failed === 0 ? "error" : "warn";
|
|
734
|
+
return makeVerificationFlag({
|
|
735
|
+
severity,
|
|
736
|
+
field: "test_results",
|
|
737
|
+
check: CHECK_COMMAND_EVIDENCE_MISMATCH,
|
|
738
|
+
detail: `Agent test_results don't match "${name}" output: ${mismatches.join("; ")}`,
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
/** v2.2: Check if the agent's declared next_action aligns with any pending
|
|
744
|
+
* or in_progress sub-goal. When 3+ pending sub-goals exist but the agent
|
|
745
|
+
* plans work unrelated to any of them, it may indicate task drift.
|
|
746
|
+
* This is a warn-level advisory — the agent always owns prioritization.
|
|
747
|
+
*
|
|
748
|
+
* v2.14: Exact sub-goal ID alignment first — next_action naming a pending
|
|
749
|
+
* sub-goal by its stable ID (sg-XXXXXXXX) is aligned by definition.
|
|
750
|
+
* Jaccard description similarity remains as the fallback. */
|
|
751
|
+
function checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound) {
|
|
752
|
+
const nextAction = selfEval.next_action?.trim();
|
|
753
|
+
if (!nextAction)
|
|
754
|
+
return null;
|
|
755
|
+
// Reconstruct pending sub-goal descriptions from vault entries
|
|
756
|
+
const pendingSubGoals = new Set();
|
|
757
|
+
const completedOrCanceled = new Set();
|
|
758
|
+
for (const entry of vaultEntries) {
|
|
759
|
+
const emerged = Array.isArray(entry.emerged_subtasks)
|
|
760
|
+
? entry.emerged_subtasks.filter((v) => typeof v === "string")
|
|
761
|
+
: [];
|
|
762
|
+
for (const desc of emerged) {
|
|
763
|
+
pendingSubGoals.add(desc.trim());
|
|
764
|
+
}
|
|
765
|
+
const done = Array.isArray(entry.completed_subtasks)
|
|
766
|
+
? entry.completed_subtasks.filter((v) => typeof v === "string")
|
|
767
|
+
: [];
|
|
768
|
+
for (const desc of done) {
|
|
769
|
+
completedOrCanceled.add(desc.trim());
|
|
770
|
+
}
|
|
771
|
+
const canceled = Array.isArray(entry.canceled_subtasks)
|
|
772
|
+
? entry.canceled_subtasks.filter((v) => typeof v === "string")
|
|
773
|
+
: [];
|
|
774
|
+
for (const desc of canceled) {
|
|
775
|
+
completedOrCanceled.add(desc.trim());
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Also process current selfEval
|
|
779
|
+
for (const desc of selfEval.emerged_subtasks ?? []) {
|
|
780
|
+
pendingSubGoals.add(desc.trim());
|
|
781
|
+
}
|
|
782
|
+
for (const desc of selfEval.completed_subtasks ?? []) {
|
|
783
|
+
completedOrCanceled.add(desc.trim());
|
|
784
|
+
}
|
|
785
|
+
for (const desc of selfEval.canceled_subtasks ?? []) {
|
|
786
|
+
completedOrCanceled.add(desc.trim());
|
|
787
|
+
}
|
|
788
|
+
// Remove completed/canceled from pending
|
|
789
|
+
for (const desc of completedOrCanceled) {
|
|
790
|
+
pendingSubGoals.delete(desc);
|
|
791
|
+
}
|
|
792
|
+
// Need at least 3 pending sub-goals for the check to be meaningful
|
|
793
|
+
if (pendingSubGoals.size < 3)
|
|
794
|
+
return null;
|
|
795
|
+
// v2.14: Stable ID match first — next_action naming a pending sub-goal
|
|
796
|
+
// by ID is aligned by definition, regardless of description wording.
|
|
797
|
+
const nextActionIds = extractSubGoalIds(nextAction);
|
|
798
|
+
if (nextActionIds.length > 0) {
|
|
799
|
+
const pendingIds = new Set([...pendingSubGoals].map((d) => deriveSubGoalId(d)));
|
|
800
|
+
if (nextActionIds.some((id) => pendingIds.has(id)))
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
// Check if next_action aligns with any pending sub-goal
|
|
804
|
+
let aligns = false;
|
|
805
|
+
for (const sg of pendingSubGoals) {
|
|
806
|
+
const score = jaccardSimilarity(nextAction, sg);
|
|
807
|
+
if (score >= getPolicy().evolution.subgoal_drift_alignment_threshold) {
|
|
808
|
+
aligns = true;
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (!aligns) {
|
|
813
|
+
const sample = [...pendingSubGoals].slice(0, 3).join(", ");
|
|
814
|
+
return makeVerificationFlag({
|
|
815
|
+
severity: "warn",
|
|
816
|
+
field: "next_action",
|
|
817
|
+
check: CHECK_SUBGOAL_DRIFT,
|
|
818
|
+
detail: `Agent's next_action doesn't align with any of ${pendingSubGoals.size} pending sub-goals. ` +
|
|
819
|
+
`Pending: ${sample}… Consider completing existing sub-goals before starting new work, ` +
|
|
820
|
+
`or cancel outdated sub-goals via canceled_subtasks.`,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
/** v2.13: After a backtrack, check that the agent restored the workspace
|
|
826
|
+
* before working. If the agent's files_changed overlaps with files from
|
|
827
|
+
* skipped (failed) rounds, the workspace was likely not clean. */
|
|
828
|
+
function checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles) {
|
|
829
|
+
if (backtrackSkippedFiles.length === 0)
|
|
830
|
+
return null;
|
|
831
|
+
const ev = selfEval.execution_evidence;
|
|
832
|
+
if (!ev || ev.files_changed.length === 0)
|
|
833
|
+
return null;
|
|
834
|
+
// Check overlap: files the agent changed vs. files from skipped rounds
|
|
835
|
+
const overlap = ev.files_changed.filter((f) => backtrackSkippedFiles.some((sf) => sf === f || f.endsWith(sf) || sf.endsWith(f)));
|
|
836
|
+
if (overlap.length === 0)
|
|
837
|
+
return null;
|
|
838
|
+
// Agent is modifying files that were part of failed rounds without
|
|
839
|
+
// having properly restored the workspace first.
|
|
840
|
+
if (overlap.length >= 3 || overlap.length === ev.files_changed.length) {
|
|
841
|
+
return makeVerificationFlag({
|
|
842
|
+
severity: "error",
|
|
843
|
+
field: "files_changed",
|
|
844
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
845
|
+
detail: `Agent modified ${overlap.length} file(s) that were part of ` +
|
|
846
|
+
`the skipped (failed) rounds: ${overlap.slice(0, 5).join(", ")}. ` +
|
|
847
|
+
`The workspace was likely not restored before working.`,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
// Minor overlap — warn only
|
|
851
|
+
return makeVerificationFlag({
|
|
852
|
+
severity: "warn",
|
|
853
|
+
field: "files_changed",
|
|
854
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
855
|
+
detail: `Agent modified files that overlap with skipped rounds: ` +
|
|
856
|
+
`${overlap.join(", ")}. Verify the workspace was restored.`,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
/** v2.12: After a backtrack, the working tree must return to the restore
|
|
860
|
+
* point's git HEAD. Compares the first 12 characters (short hash), matching
|
|
861
|
+
* how git itself disambiguates. */
|
|
862
|
+
function checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead) {
|
|
863
|
+
if (!backtrackTargetGitHead)
|
|
864
|
+
return null;
|
|
865
|
+
const git = evidenceSnapshots.find((snapshot) => snapshot.provider === "git" && isRecord(snapshot.data) &&
|
|
866
|
+
typeof snapshot.data.head === "string");
|
|
867
|
+
if (!git)
|
|
868
|
+
return null; // git unavailable → cannot verify → skip (fail-open)
|
|
869
|
+
const currentHead = git.data.head;
|
|
870
|
+
if (currentHead.length === 0)
|
|
871
|
+
return null;
|
|
872
|
+
if (currentHead.slice(0, 12) === backtrackTargetGitHead.slice(0, 12))
|
|
873
|
+
return null;
|
|
874
|
+
return makeVerificationFlag({
|
|
875
|
+
severity: "error",
|
|
876
|
+
field: "workspace",
|
|
877
|
+
check: CHECK_BACKTRACK_WORKSPACE_NOT_RESTORED,
|
|
878
|
+
detail: `Workspace HEAD (${currentHead.slice(0, 12)}) does not match the ` +
|
|
879
|
+
`backtrack restore commit (${backtrackTargetGitHead.slice(0, 12)}). ` +
|
|
880
|
+
`Restore the working tree before continuing.`,
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
884
|
+
// Main entry point
|
|
885
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
886
|
+
/** Verify a SelfEvaluation against the loop's cross-round lineage.
|
|
887
|
+
*
|
|
888
|
+
* @param selfEval The agent's self-evaluation for the current round.
|
|
889
|
+
* @param currentRound The current round number (1-based).
|
|
890
|
+
* @param vaultEntries Vault entries for this loop (non-feedback only).
|
|
891
|
+
* @param prevSelfEval The agent's self-evaluation from the previous round
|
|
892
|
+
* (null for round 1).
|
|
893
|
+
* @param evidenceSnapshots v1.18: Evidence snapshots from configured providers.
|
|
894
|
+
* Used by checkEvidenceIntegrity for multi-provider
|
|
895
|
+
* cross-validation. Defaults to empty array. */
|
|
896
|
+
export function verifySelfEvaluation(selfEval, currentRound, vaultEntries, prevSelfEval = null, evidenceSnapshots = [],
|
|
897
|
+
/** v2.13: Files from skipped backtrack rounds. If the agent's
|
|
898
|
+
* files_changed overlaps significantly with these, the workspace
|
|
899
|
+
* was not properly restored before working. */
|
|
900
|
+
backtrackSkippedFiles = [],
|
|
901
|
+
/** v2.12: Git HEAD commit of the backtrack restore point. When set, the
|
|
902
|
+
* current git snapshot must sit at this commit — otherwise the workspace
|
|
903
|
+
* was not restored and the round cannot be accepted. */
|
|
904
|
+
backtrackTargetGitHead) {
|
|
905
|
+
const flags = [];
|
|
906
|
+
// Collect violations from all previous vault entries for duplicate-discovery
|
|
907
|
+
// and other checks that need deeper history.
|
|
908
|
+
const olderViolations = [];
|
|
909
|
+
for (const entry of vaultEntries) {
|
|
910
|
+
for (const v of entryViolations(entry))
|
|
911
|
+
olderViolations.push(v);
|
|
912
|
+
}
|
|
913
|
+
// v2.12: Derived claim provenance — which reported criteria completions
|
|
914
|
+
// are backed by machine evidence (pure derivation, not persisted).
|
|
915
|
+
const claimView = deriveClaimView(selfEval, evidenceSnapshots);
|
|
916
|
+
// v3.2: Runtime machine-verification status (git observed / command passed /
|
|
917
|
+
// absent). Feeds the success_unverified check that never self-skips.
|
|
918
|
+
const evidenceStatus = deriveEvidenceStatus(selfEval, evidenceSnapshots);
|
|
919
|
+
// Run all checks
|
|
920
|
+
const checks = [
|
|
921
|
+
() => checkProgressRegression(selfEval, prevSelfEval),
|
|
922
|
+
() => checkEmptyChangeWithPassing(selfEval),
|
|
923
|
+
() => checkSuccessWithRemainingCriteria(selfEval),
|
|
924
|
+
// v2.12: success with zero verified claims and no test evidence
|
|
925
|
+
() => checkSuccessWithoutVerifiedEvidence(selfEval, claimView),
|
|
926
|
+
// v3.2: success without a machine-verified observation (never self-skips)
|
|
927
|
+
() => checkSuccessUnverified(selfEval, evidenceStatus),
|
|
928
|
+
// v2.12: criteria met but none machine-verified (mild criterion-specific rule)
|
|
929
|
+
() => checkUnverifiedCriteriaClaims(selfEval, claimView),
|
|
930
|
+
// v2.12: declared outcome vs legacy boolean consistency
|
|
931
|
+
() => checkOutcomeConsistency(selfEval),
|
|
932
|
+
// v2.12: retroactive claims against prior rounds
|
|
933
|
+
() => checkRetroactiveClaims(selfEval, vaultEntries, currentRound),
|
|
934
|
+
() => checkDuplicateConstraintDiscovery(selfEval, prevSelfEval, olderViolations),
|
|
935
|
+
() => checkRecurringViolation(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
936
|
+
() => checkRetractFreshConstraint(selfEval, prevSelfEval, currentRound),
|
|
937
|
+
// v1.18: Cross-validate agent-reported files_changed against git evidence
|
|
938
|
+
() => checkEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
939
|
+
() => checkRequiredCommandEvidence(selfEval, evidenceSnapshots),
|
|
940
|
+
// v2.0: Cross-validate agent-reported test_results against command output
|
|
941
|
+
() => checkCommandEvidenceIntegrity(selfEval, evidenceSnapshots),
|
|
942
|
+
// v2.1: Detect intent-action drift — agent said X but did Y
|
|
943
|
+
() => checkIntentDrift(selfEval, prevSelfEval, vaultEntries),
|
|
944
|
+
// v2.2: Detect sub-goal drift — next_action doesn't align with pending sub-goals
|
|
945
|
+
() => checkSubGoalDrift(selfEval, prevSelfEval, vaultEntries, currentRound),
|
|
946
|
+
// v2.13: Post-backtrack workspace restore check
|
|
947
|
+
() => checkBacktrackWorkspaceRestore(selfEval, backtrackSkippedFiles),
|
|
948
|
+
// v2.12: Post-backtrack git HEAD restore check
|
|
949
|
+
() => checkBacktrackGitHeadRestore(evidenceSnapshots, backtrackTargetGitHead),
|
|
950
|
+
];
|
|
951
|
+
for (const run of checks) {
|
|
952
|
+
const flag = run();
|
|
953
|
+
if (flag)
|
|
954
|
+
flags.push(flag);
|
|
955
|
+
}
|
|
956
|
+
// Determine verdict from the most severe flag present
|
|
957
|
+
const hasError = flags.some((f) => f.severity === "error");
|
|
958
|
+
const hasWarn = flags.some((f) => f.severity === "warn");
|
|
959
|
+
let verdict;
|
|
960
|
+
if (hasError) {
|
|
961
|
+
verdict = "contradicted";
|
|
962
|
+
}
|
|
963
|
+
else if (hasWarn) {
|
|
964
|
+
verdict = "suspect";
|
|
965
|
+
}
|
|
966
|
+
else {
|
|
967
|
+
verdict = "trusted";
|
|
968
|
+
}
|
|
969
|
+
return makeVerificationResult({ verdict, flags });
|
|
970
|
+
}
|
|
971
|
+
//# sourceMappingURL=verification-gate.js.map
|