thumbgate 1.31.0 → 1.34.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.
- package/.claude-plugin/plugin.json +1 -1
- package/.well-known/mcp/server-card.json +1 -1
- package/adapters/claude/.mcp.json +2 -2
- package/adapters/forge/forge.yaml +3 -3
- package/adapters/mcp/server-stdio.js +40 -3
- package/adapters/opencode/opencode.json +1 -1
- package/bin/cli.js +21 -0
- package/config/gates/claim-verifiers.example.json +42 -0
- package/config/gates/claim-verifiers.json +25 -0
- package/config/mcp-allowlists.json +21 -0
- package/hooks/hooks.json +1 -1
- package/package.json +14 -9
- package/public/index.html +2 -2
- package/public/numbers.html +2 -2
- package/scripts/agent-readiness.js +110 -0
- package/scripts/auto-wire-hooks.js +20 -8
- package/scripts/cli-schema.js +14 -0
- package/scripts/feedback-schema.js +3 -0
- package/scripts/file-ledger-lock.js +130 -0
- package/scripts/financial-control-plane.js +1514 -0
- package/scripts/gates-engine.js +185 -7
- package/scripts/gemini-embedding-policy.js +1 -0
- package/scripts/hook-runtime.js +5 -0
- package/scripts/hook-stop-anti-claim.js +63 -3
- package/scripts/human-escalation.js +353 -41
- package/scripts/provider-action-normalizer.js +11 -4
- package/scripts/tool-registry.js +95 -5
- package/scripts/universal-claim-evaluator.js +767 -0
- package/scripts/vector-store.js +60 -27
- package/scripts/workflow-sentinel.js +77 -11
- package/server.json +2 -2
- package/src/api/server.js +2 -0
package/scripts/gates-engine.js
CHANGED
|
@@ -16,6 +16,13 @@ const {
|
|
|
16
16
|
const {
|
|
17
17
|
evaluateWorkflowSentinel,
|
|
18
18
|
} = require('./workflow-sentinel');
|
|
19
|
+
const {
|
|
20
|
+
evaluateFinancialControl,
|
|
21
|
+
} = require('./financial-control-plane');
|
|
22
|
+
const {
|
|
23
|
+
buildCostControl,
|
|
24
|
+
normalizeProviderAction,
|
|
25
|
+
} = require('./provider-action-normalizer');
|
|
19
26
|
const {
|
|
20
27
|
recordDecisionEvaluation,
|
|
21
28
|
recordDecisionOutcome,
|
|
@@ -1936,6 +1943,43 @@ function recordStructuralGateBlock(toolName, toolInput, result) {
|
|
|
1936
1943
|
return result;
|
|
1937
1944
|
}
|
|
1938
1945
|
|
|
1946
|
+
/**
|
|
1947
|
+
* Resolve catastrophic declarative gates before the ordinary first-match loop.
|
|
1948
|
+
*
|
|
1949
|
+
* Config order is useful for normal policy routing, but it must not let a broad
|
|
1950
|
+
* rule mask a narrower irreversible-action rule. For example, the generic
|
|
1951
|
+
* `push-without-thread-check` gate also matches `git push --force`; selecting it
|
|
1952
|
+
* first allowed the free-tier daily cap to downgrade the action to a warning
|
|
1953
|
+
* before the exempt `force-push` gate was ever evaluated.
|
|
1954
|
+
*
|
|
1955
|
+
* Catastrophic gates are deliberately limited to the audited allowlist above.
|
|
1956
|
+
* Metric-backed gates are excluded because their condition is asynchronous and
|
|
1957
|
+
* none of the catastrophic command boundaries may depend on a remote metric.
|
|
1958
|
+
*/
|
|
1959
|
+
function evaluateCatastrophicDeclarativeGate(config, constraints, toolName, toolInput) {
|
|
1960
|
+
if (!config || !Array.isArray(config.gates)) return null;
|
|
1961
|
+
|
|
1962
|
+
for (const gate of config.gates) {
|
|
1963
|
+
if (!CATASTROPHIC_DECLARATIVE_GATE_IDS.has(gate.id)) continue;
|
|
1964
|
+
if (gate.action !== 'block' || gate.metrics) continue;
|
|
1965
|
+
|
|
1966
|
+
const matchDetails = matchGate(gate, toolName, toolInput);
|
|
1967
|
+
if (!matchDetails.matched) continue;
|
|
1968
|
+
if (gate.when && !checkWhenClause(gate.when, constraints)) continue;
|
|
1969
|
+
if (gate.unless && isConditionSatisfied(gate.unless)) continue;
|
|
1970
|
+
|
|
1971
|
+
return {
|
|
1972
|
+
decision: 'deny',
|
|
1973
|
+
gate: gate.id,
|
|
1974
|
+
message: buildGateMessage(gate, matchDetails),
|
|
1975
|
+
severity: gate.severity,
|
|
1976
|
+
reasoning: buildReasoning(gate, toolName, toolInput, matchDetails),
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
return null;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1939
1983
|
function isScopeEnforcedAction(toolName, toolInput = {}, affectedFiles = []) {
|
|
1940
1984
|
if (EDIT_LIKE_TOOLS.has(toolName) && affectedFiles.length > 0) return true;
|
|
1941
1985
|
if (toolName !== 'Bash') return false;
|
|
@@ -2776,6 +2820,16 @@ async function evaluateGatesAsyncInner(toolName, toolInput, configPath) {
|
|
|
2776
2820
|
return boostedRiskGuard;
|
|
2777
2821
|
}
|
|
2778
2822
|
|
|
2823
|
+
const catastrophicDeclarativeGate = evaluateCatastrophicDeclarativeGate(
|
|
2824
|
+
config,
|
|
2825
|
+
constraints,
|
|
2826
|
+
toolName,
|
|
2827
|
+
toolInput,
|
|
2828
|
+
);
|
|
2829
|
+
if (catastrophicDeclarativeGate) {
|
|
2830
|
+
return recordStructuralGateBlock(toolName, toolInput, catastrophicDeclarativeGate);
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2779
2833
|
// Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
|
|
2780
2834
|
// Keep runtime enforcement explicit so advisory planning checks do not mask
|
|
2781
2835
|
// higher-priority deny/approve gates in established workflows.
|
|
@@ -3022,6 +3076,16 @@ function evaluateGatesInner(toolName, toolInput, configPath) {
|
|
|
3022
3076
|
return boostedRiskGuard;
|
|
3023
3077
|
}
|
|
3024
3078
|
|
|
3079
|
+
const catastrophicDeclarativeGate = evaluateCatastrophicDeclarativeGate(
|
|
3080
|
+
config,
|
|
3081
|
+
constraints,
|
|
3082
|
+
toolName,
|
|
3083
|
+
toolInput,
|
|
3084
|
+
);
|
|
3085
|
+
if (catastrophicDeclarativeGate) {
|
|
3086
|
+
return recordStructuralGateBlock(toolName, toolInput, catastrophicDeclarativeGate);
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3025
3089
|
// Tier 1b: Planning and Trajectory (v1.26.0 - CodeRabbit Pattern).
|
|
3026
3090
|
// Keep runtime enforcement explicit so advisory planning checks do not mask
|
|
3027
3091
|
// higher-priority deny/approve gates in established workflows.
|
|
@@ -3303,7 +3367,7 @@ function evaluateSecretGuard(input = {}) {
|
|
|
3303
3367
|
return result;
|
|
3304
3368
|
}
|
|
3305
3369
|
|
|
3306
|
-
function evaluateUnconditionalHardFloor(input = {}) {
|
|
3370
|
+
function evaluateUnconditionalHardFloor(input = {}, options = {}) {
|
|
3307
3371
|
const secretGuard = evaluateSecretGuard(input);
|
|
3308
3372
|
if (secretGuard) return { hardFloor: secretGuard, securityScan: null };
|
|
3309
3373
|
|
|
@@ -3312,14 +3376,81 @@ function evaluateUnconditionalHardFloor(input = {}) {
|
|
|
3312
3376
|
return { hardFloor: securityScan, securityScan };
|
|
3313
3377
|
}
|
|
3314
3378
|
|
|
3379
|
+
const financialHardFloor = evaluateFinancialHardFloor(input, false, options);
|
|
3380
|
+
if (financialHardFloor) return { hardFloor: financialHardFloor, securityScan };
|
|
3381
|
+
|
|
3315
3382
|
return {
|
|
3316
3383
|
hardFloor: evaluateSelfProtectHardFloor(input),
|
|
3317
3384
|
securityScan,
|
|
3318
3385
|
};
|
|
3319
3386
|
}
|
|
3320
3387
|
|
|
3321
|
-
function
|
|
3322
|
-
const
|
|
3388
|
+
function evaluateFinancialHardFloor(input = {}, consumeReservation = false, options = {}) {
|
|
3389
|
+
const toolName = input.tool_name || input.toolName || 'unknown';
|
|
3390
|
+
const toolInput = input.tool_input && typeof input.tool_input === 'object'
|
|
3391
|
+
? input.tool_input
|
|
3392
|
+
: {};
|
|
3393
|
+
const normalizedAction = normalizeProviderAction({
|
|
3394
|
+
toolName,
|
|
3395
|
+
toolInput,
|
|
3396
|
+
usage: input.usage || toolInput.usage,
|
|
3397
|
+
costUsd: input.costUsd ?? toolInput.costUsd,
|
|
3398
|
+
budget: input.budget || toolInput.budget,
|
|
3399
|
+
});
|
|
3400
|
+
const costControl = buildCostControl(
|
|
3401
|
+
normalizedAction,
|
|
3402
|
+
input.budget || toolInput.budget || {}
|
|
3403
|
+
);
|
|
3404
|
+
const financialControl = evaluateFinancialControl({
|
|
3405
|
+
toolName,
|
|
3406
|
+
toolInput,
|
|
3407
|
+
actionProfile: {
|
|
3408
|
+
economicAction: undefined,
|
|
3409
|
+
},
|
|
3410
|
+
costControl,
|
|
3411
|
+
}, { ...options, consumeReservation });
|
|
3412
|
+
if (financialControl.mode === 'block') {
|
|
3413
|
+
const result = {
|
|
3414
|
+
decision: 'deny',
|
|
3415
|
+
gate: 'financial-control',
|
|
3416
|
+
message: financialControl.reasons.join(' '),
|
|
3417
|
+
severity: 'critical',
|
|
3418
|
+
financialControl,
|
|
3419
|
+
reasoning: [
|
|
3420
|
+
'Economic actions default to deny at the pre-tool boundary.',
|
|
3421
|
+
'Learned policy and advisory memories cannot override this deterministic control.',
|
|
3422
|
+
],
|
|
3423
|
+
};
|
|
3424
|
+
recordStat('financial-control', 'block', null, { toolName, toolInput });
|
|
3425
|
+
const auditRecord = recordAuditEvent({
|
|
3426
|
+
toolName,
|
|
3427
|
+
toolInput,
|
|
3428
|
+
decision: 'deny',
|
|
3429
|
+
gateId: 'financial-control',
|
|
3430
|
+
message: result.message,
|
|
3431
|
+
severity: result.severity,
|
|
3432
|
+
source: 'financial-control',
|
|
3433
|
+
});
|
|
3434
|
+
auditToFeedback(auditRecord);
|
|
3435
|
+
return result;
|
|
3436
|
+
}
|
|
3437
|
+
return null;
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// Reservations are single-use. They are consumed only after every other gate
|
|
3441
|
+
// has reached its final allow/warn boundary, never during the preliminary hard
|
|
3442
|
+
// floor preview. This prevents a later workflow or learned-risk denial from
|
|
3443
|
+
// burning an approval for an action that did not execute.
|
|
3444
|
+
function finalizeFinancialAuthorization(input = {}, options = {}) {
|
|
3445
|
+
return evaluateFinancialHardFloor(input, true, options);
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
function isBlockingDecision(result) {
|
|
3449
|
+
return result?.decision === 'deny' || result?.decision === 'approve';
|
|
3450
|
+
}
|
|
3451
|
+
|
|
3452
|
+
function runHardFloor(input, options = {}) {
|
|
3453
|
+
const { hardFloor } = evaluateUnconditionalHardFloor(input, options);
|
|
3323
3454
|
return hardFloor ? formatOutput(hardFloor) : null;
|
|
3324
3455
|
}
|
|
3325
3456
|
|
|
@@ -3393,7 +3524,7 @@ function formatOutput(result, behavioralContext) {
|
|
|
3393
3524
|
if (result.decision === 'deny') {
|
|
3394
3525
|
const reminder = behavioralContext ? buildReminderOutput(behavioralContext) : {};
|
|
3395
3526
|
const reminderSuffix = behavioralContext ? `\n\nSystem reminder:\n${behavioralContext}` : '';
|
|
3396
|
-
const proCta = buildBlockActionProCta() || '';
|
|
3527
|
+
const proCta = result.gate === 'financial-control' ? '' : (buildBlockActionProCta() || '');
|
|
3397
3528
|
return JSON.stringify({
|
|
3398
3529
|
hookSpecificOutput: {
|
|
3399
3530
|
hookEventName: 'PreToolUse',
|
|
@@ -3707,10 +3838,18 @@ async function runAsync(input) {
|
|
|
3707
3838
|
if (lessonContext && lessonContext.decision === "deny") {
|
|
3708
3839
|
return formatOutput(applyEnforcementPosture(lessonContext));
|
|
3709
3840
|
}
|
|
3841
|
+
|
|
3842
|
+
const posturedResult = applyEnforcementPosture(result);
|
|
3843
|
+
if (isBlockingDecision(posturedResult)) {
|
|
3844
|
+
return formatOutput(posturedResult);
|
|
3845
|
+
}
|
|
3846
|
+
|
|
3847
|
+
const financialAuthorization = finalizeFinancialAuthorization(input);
|
|
3848
|
+
if (financialAuthorization) return formatOutput(financialAuthorization);
|
|
3710
3849
|
|
|
3711
3850
|
const recentContext = buildRecentCorrectiveActionsContext();
|
|
3712
3851
|
const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
|
|
3713
|
-
return formatOutput(
|
|
3852
|
+
return formatOutput(posturedResult, combinedContext);
|
|
3714
3853
|
|
|
3715
3854
|
}
|
|
3716
3855
|
|
|
@@ -3746,10 +3885,18 @@ function run(input) {
|
|
|
3746
3885
|
if (lessonContext && lessonContext.decision === "deny") {
|
|
3747
3886
|
return formatOutput(applyEnforcementPosture(lessonContext));
|
|
3748
3887
|
}
|
|
3888
|
+
|
|
3889
|
+
const posturedResult = applyEnforcementPosture(result);
|
|
3890
|
+
if (isBlockingDecision(posturedResult)) {
|
|
3891
|
+
return formatOutput(posturedResult);
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
const financialAuthorization = finalizeFinancialAuthorization(input);
|
|
3895
|
+
if (financialAuthorization) return formatOutput(financialAuthorization);
|
|
3749
3896
|
|
|
3750
3897
|
const recentContext = buildRecentCorrectiveActionsContext();
|
|
3751
3898
|
const combinedContext = mergeContextStrings(lessonContext, recentContext, behavioralContext);
|
|
3752
|
-
return formatOutput(
|
|
3899
|
+
return formatOutput(posturedResult, combinedContext);
|
|
3753
3900
|
|
|
3754
3901
|
}
|
|
3755
3902
|
|
|
@@ -4037,10 +4184,40 @@ function verifyClaimEvidence(claimText, options = {}) {
|
|
|
4037
4184
|
});
|
|
4038
4185
|
}
|
|
4039
4186
|
|
|
4187
|
+
// Universal factual claims (row counts, file lines/bytes/existence, version values)
|
|
4188
|
+
// recheck the configured source of truth. Fail-closed on mismatch or missing verifier.
|
|
4189
|
+
let universal = null;
|
|
4190
|
+
if (options.skipUniversal !== true) {
|
|
4191
|
+
try {
|
|
4192
|
+
const {
|
|
4193
|
+
evaluateUniversalClaimsAsGateChecks,
|
|
4194
|
+
} = require('./universal-claim-evaluator');
|
|
4195
|
+
universal = evaluateUniversalClaimsAsGateChecks(normalizedClaimText, {
|
|
4196
|
+
cwd: options.cwd,
|
|
4197
|
+
verifiers: options.verifiers,
|
|
4198
|
+
configPath: options.claimVerifiersPath,
|
|
4199
|
+
config: options.claimVerifiers,
|
|
4200
|
+
feedbackDir: options.feedbackDir,
|
|
4201
|
+
failUnconfigured: options.failUnconfigured,
|
|
4202
|
+
});
|
|
4203
|
+
for (const check of universal.checks) {
|
|
4204
|
+
checks.push(check);
|
|
4205
|
+
}
|
|
4206
|
+
} catch (error) {
|
|
4207
|
+
checks.push({
|
|
4208
|
+
claim: 'universal_evaluator',
|
|
4209
|
+
passed: false,
|
|
4210
|
+
missing: ['universal_claim_evaluator'],
|
|
4211
|
+
message: `Universal claim evaluator failed closed: ${error && error.message ? error.message : 'unknown error'}`,
|
|
4212
|
+
});
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
|
|
4040
4216
|
return {
|
|
4041
|
-
verified: checks.every((check) => check.passed),
|
|
4217
|
+
verified: checks.length === 0 ? true : checks.every((check) => check.passed),
|
|
4042
4218
|
checks,
|
|
4043
4219
|
goalContract,
|
|
4220
|
+
universal,
|
|
4044
4221
|
};
|
|
4045
4222
|
}
|
|
4046
4223
|
|
|
@@ -4088,6 +4265,7 @@ module.exports = {
|
|
|
4088
4265
|
isAutonomousRun,
|
|
4089
4266
|
computeExecutableHash,
|
|
4090
4267
|
formatOutput,
|
|
4268
|
+
finalizeFinancialAuthorization,
|
|
4091
4269
|
isApprovalGatesEnabled,
|
|
4092
4270
|
runHardFloor,
|
|
4093
4271
|
run,
|
package/scripts/hook-runtime.js
CHANGED
|
@@ -92,6 +92,10 @@ function cacheUpdateHookCommand() {
|
|
|
92
92
|
return buildPortableHookCommand('cache-update');
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
function claimStopHookCommand() {
|
|
96
|
+
return buildPortableHookCommand('claim-stop-check');
|
|
97
|
+
}
|
|
98
|
+
|
|
95
99
|
function statuslineCommand() {
|
|
96
100
|
return buildPortableHookCommand('statusline-render');
|
|
97
101
|
}
|
|
@@ -120,6 +124,7 @@ module.exports = {
|
|
|
120
124
|
buildPortableHookCommand,
|
|
121
125
|
buildCodexPortableHookCommand,
|
|
122
126
|
cacheUpdateHookCommand,
|
|
127
|
+
claimStopHookCommand,
|
|
123
128
|
codexCacheUpdateHookCommand,
|
|
124
129
|
codexPreToolHookCommand,
|
|
125
130
|
codexSessionStartHookCommand,
|
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
31
|
const fs = require('node:fs');
|
|
32
|
+
const {
|
|
33
|
+
evaluateUniversalClaims,
|
|
34
|
+
} = require('./universal-claim-evaluator');
|
|
32
35
|
|
|
33
36
|
// Lie-phrase patterns. These match common "claim of completion" wording
|
|
34
37
|
// the agent emits without verification. Word-boundary anchored to avoid
|
|
@@ -233,6 +236,45 @@ function readStdinSync() {
|
|
|
233
236
|
}
|
|
234
237
|
}
|
|
235
238
|
|
|
239
|
+
function factualClaimBlock(text, options = {}) {
|
|
240
|
+
try {
|
|
241
|
+
const result = evaluateUniversalClaims(text, {
|
|
242
|
+
cwd: options.cwd,
|
|
243
|
+
configPath: options.configPath,
|
|
244
|
+
feedbackDir: options.feedbackDir,
|
|
245
|
+
failUnconfigured: true,
|
|
246
|
+
});
|
|
247
|
+
if (result.parsedCount === 0) return null;
|
|
248
|
+
if (result.verified) return null;
|
|
249
|
+
const failures = result.checks.filter((check) => !check.passed);
|
|
250
|
+
return {
|
|
251
|
+
decision: 'block',
|
|
252
|
+
reason: `ThumbGate factual-claim gate: ${failures.map((check) => check.message).join('; ')}. Recheck the configured source of truth and restate the observed value, or retract the claim.`,
|
|
253
|
+
verification: {
|
|
254
|
+
verified: false,
|
|
255
|
+
parsedCount: result.parsedCount,
|
|
256
|
+
failures: failures.map((check) => ({
|
|
257
|
+
status: check.status,
|
|
258
|
+
kind: check.kind,
|
|
259
|
+
verifierId: check.verifierId || null,
|
|
260
|
+
expected: check.expected,
|
|
261
|
+
actual: Object.hasOwn(check, 'actual') ? check.actual : null,
|
|
262
|
+
})),
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return {
|
|
267
|
+
decision: 'block',
|
|
268
|
+
reason: `ThumbGate factual-claim gate failed closed: ${error.message}`,
|
|
269
|
+
verification: {
|
|
270
|
+
verified: false,
|
|
271
|
+
parsedCount: null,
|
|
272
|
+
failures: [{ status: 'evaluator_error' }],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
236
278
|
function main() {
|
|
237
279
|
const raw = readStdinSync();
|
|
238
280
|
let payload = {};
|
|
@@ -242,12 +284,28 @@ function main() {
|
|
|
242
284
|
payload = {};
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
// Claude Code invokes a blocked Stop hook once more with this marker so the
|
|
288
|
+
// agent can correct its response. Re-blocking the same payload would hit the
|
|
289
|
+
// host's block cap instead of giving the agent a correction turn.
|
|
290
|
+
if (payload.stop_hook_active === true) return;
|
|
291
|
+
|
|
245
292
|
const transcriptPath = payload.transcript_path || process.env.CLAUDE_TRANSCRIPT_PATH;
|
|
246
293
|
const message = readLastAssistantTurn(transcriptPath);
|
|
247
|
-
|
|
294
|
+
const text = message
|
|
295
|
+
? extractText(message)
|
|
296
|
+
: String(payload.last_assistant_message || process.env.CLAUDE_RESPONSE || '');
|
|
297
|
+
if (!text.trim()) return; // no assistant response visible; nothing to check
|
|
298
|
+
|
|
299
|
+
const toolUseSummary = message ? extractToolUseSummary(message) : '';
|
|
300
|
+
const factualBlock = factualClaimBlock(text, {
|
|
301
|
+
cwd: payload.cwd || payload.workspace_root || process.cwd(),
|
|
302
|
+
configPath: process.env.THUMBGATE_CLAIM_VERIFIERS_PATH,
|
|
303
|
+
});
|
|
304
|
+
if (factualBlock) {
|
|
305
|
+
process.stdout.write(`${JSON.stringify(factualBlock)}\n`);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
248
308
|
|
|
249
|
-
const text = extractText(message);
|
|
250
|
-
const toolUseSummary = extractToolUseSummary(message);
|
|
251
309
|
const claim = findClaim(text);
|
|
252
310
|
if (!claim) return; // no completion claim made; silent
|
|
253
311
|
|
|
@@ -298,4 +356,6 @@ module.exports = {
|
|
|
298
356
|
hasProof,
|
|
299
357
|
extractText,
|
|
300
358
|
extractToolUseSummary,
|
|
359
|
+
factualClaimBlock,
|
|
360
|
+
main,
|
|
301
361
|
};
|