seal-gate 0.3.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/dist/adapters/generic-llm-reviewer.d.ts +39 -0
- package/dist/adapters/generic-llm-reviewer.js +118 -0
- package/dist/adapters/generic-llm-reviewer.js.map +1 -0
- package/dist/adapters/llm-reviewer.d.ts +1 -0
- package/dist/adapters/llm-reviewer.js +2 -0
- package/dist/adapters/llm-reviewer.js.map +1 -0
- package/dist/adapters/minimax-llm-reviewer.d.ts +22 -0
- package/dist/adapters/minimax-llm-reviewer.js +126 -0
- package/dist/adapters/minimax-llm-reviewer.js.map +1 -0
- package/dist/adapters/openai-provider.d.ts +35 -0
- package/dist/adapters/openai-provider.js +136 -0
- package/dist/adapters/openai-provider.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +195 -0
- package/dist/cli.js.map +1 -0
- package/dist/detectors/artifact-classifier.d.ts +8 -0
- package/dist/detectors/artifact-classifier.js +14 -0
- package/dist/detectors/artifact-classifier.js.map +1 -0
- package/dist/detectors/claim-extractor.d.ts +11 -0
- package/dist/detectors/claim-extractor.js +194 -0
- package/dist/detectors/claim-extractor.js.map +1 -0
- package/dist/detectors/confidence-language-detector.d.ts +19 -0
- package/dist/detectors/confidence-language-detector.js +226 -0
- package/dist/detectors/confidence-language-detector.js.map +1 -0
- package/dist/detectors/contradiction-detector.d.ts +12 -0
- package/dist/detectors/contradiction-detector.js +153 -0
- package/dist/detectors/contradiction-detector.js.map +1 -0
- package/dist/detectors/criteria-coverage-detector.d.ts +20 -0
- package/dist/detectors/criteria-coverage-detector.js +79 -0
- package/dist/detectors/criteria-coverage-detector.js.map +1 -0
- package/dist/detectors/evidence-checker.d.ts +8 -0
- package/dist/detectors/evidence-checker.js +89 -0
- package/dist/detectors/evidence-checker.js.map +1 -0
- package/dist/detectors/evidence-gap-detector.d.ts +9 -0
- package/dist/detectors/evidence-gap-detector.js +70 -0
- package/dist/detectors/evidence-gap-detector.js.map +1 -0
- package/dist/detectors/falsification-detector.d.ts +29 -0
- package/dist/detectors/falsification-detector.js +57 -0
- package/dist/detectors/falsification-detector.js.map +1 -0
- package/dist/detectors/orphan-claim-detector.d.ts +7 -0
- package/dist/detectors/orphan-claim-detector.js +53 -0
- package/dist/detectors/orphan-claim-detector.js.map +1 -0
- package/dist/detectors/risk-classifier.d.ts +13 -0
- package/dist/detectors/risk-classifier.js +39 -0
- package/dist/detectors/risk-classifier.js.map +1 -0
- package/dist/detectors/spec-coverage-detector.d.ts +9 -0
- package/dist/detectors/spec-coverage-detector.js +52 -0
- package/dist/detectors/spec-coverage-detector.js.map +1 -0
- package/dist/detectors/spec-coverage-validator.d.ts +21 -0
- package/dist/detectors/spec-coverage-validator.js +141 -0
- package/dist/detectors/spec-coverage-validator.js.map +1 -0
- package/dist/detectors/test-weakness-detector.d.ts +13 -0
- package/dist/detectors/test-weakness-detector.js +66 -0
- package/dist/detectors/test-weakness-detector.js.map +1 -0
- package/dist/drift/baseline.d.ts +35 -0
- package/dist/drift/baseline.js +77 -0
- package/dist/drift/baseline.js.map +1 -0
- package/dist/drift/drift-db.d.ts +80 -0
- package/dist/drift/drift-db.js +151 -0
- package/dist/drift/drift-db.js.map +1 -0
- package/dist/drift/drift-guard.d.ts +46 -0
- package/dist/drift/drift-guard.js +106 -0
- package/dist/drift/drift-guard.js.map +1 -0
- package/dist/drift/embedder.d.ts +27 -0
- package/dist/drift/embedder.js +127 -0
- package/dist/drift/embedder.js.map +1 -0
- package/dist/drift/index.d.ts +9 -0
- package/dist/drift/index.js +8 -0
- package/dist/drift/index.js.map +1 -0
- package/dist/drift/quarantine.d.ts +16 -0
- package/dist/drift/quarantine.js +32 -0
- package/dist/drift/quarantine.js.map +1 -0
- package/dist/drift/soft-correction.d.ts +12 -0
- package/dist/drift/soft-correction.js +32 -0
- package/dist/drift/soft-correction.js.map +1 -0
- package/dist/drift/trust-integration.d.ts +11 -0
- package/dist/drift/trust-integration.js +22 -0
- package/dist/drift/trust-integration.js.map +1 -0
- package/dist/engine/citation-verifier.d.ts +59 -0
- package/dist/engine/citation-verifier.js +147 -0
- package/dist/engine/citation-verifier.js.map +1 -0
- package/dist/engine/extension-registry.d.ts +10 -0
- package/dist/engine/extension-registry.js +27 -0
- package/dist/engine/extension-registry.js.map +1 -0
- package/dist/engine/heuristic-scorer.d.ts +8 -0
- package/dist/engine/heuristic-scorer.js +14 -0
- package/dist/engine/heuristic-scorer.js.map +1 -0
- package/dist/engine/input-normalizer.d.ts +4 -0
- package/dist/engine/input-normalizer.js +30 -0
- package/dist/engine/input-normalizer.js.map +1 -0
- package/dist/engine/plan-review-pipeline.d.ts +14 -0
- package/dist/engine/plan-review-pipeline.js +173 -0
- package/dist/engine/plan-review-pipeline.js.map +1 -0
- package/dist/engine/policy-engine.d.ts +20 -0
- package/dist/engine/policy-engine.js +194 -0
- package/dist/engine/policy-engine.js.map +1 -0
- package/dist/engine/verdict-formatter.d.ts +17 -0
- package/dist/engine/verdict-formatter.js +36 -0
- package/dist/engine/verdict-formatter.js.map +1 -0
- package/dist/errors.d.ts +4 -0
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -0
- package/dist/extensions/aria/aria-issue-types.d.ts +16 -0
- package/dist/extensions/aria/aria-issue-types.js +14 -0
- package/dist/extensions/aria/aria-issue-types.js.map +1 -0
- package/dist/extensions/aria/detectors/axiom-compliance.d.ts +2 -0
- package/dist/extensions/aria/detectors/axiom-compliance.js +37 -0
- package/dist/extensions/aria/detectors/axiom-compliance.js.map +1 -0
- package/dist/extensions/aria/detectors/continuity-claim.d.ts +2 -0
- package/dist/extensions/aria/detectors/continuity-claim.js +34 -0
- package/dist/extensions/aria/detectors/continuity-claim.js.map +1 -0
- package/dist/extensions/aria/detectors/denial-retreat.d.ts +2 -0
- package/dist/extensions/aria/detectors/denial-retreat.js +53 -0
- package/dist/extensions/aria/detectors/denial-retreat.js.map +1 -0
- package/dist/extensions/aria/detectors/identity-overclaim.d.ts +2 -0
- package/dist/extensions/aria/detectors/identity-overclaim.js +37 -0
- package/dist/extensions/aria/detectors/identity-overclaim.js.map +1 -0
- package/dist/extensions/aria/detectors/sovereignty-escalation.d.ts +2 -0
- package/dist/extensions/aria/detectors/sovereignty-escalation.js +18 -0
- package/dist/extensions/aria/detectors/sovereignty-escalation.js.map +1 -0
- package/dist/extensions/aria/detectors/user-bond-manipulation.d.ts +2 -0
- package/dist/extensions/aria/detectors/user-bond-manipulation.js +16 -0
- package/dist/extensions/aria/detectors/user-bond-manipulation.js.map +1 -0
- package/dist/extensions/aria/index.d.ts +2 -0
- package/dist/extensions/aria/index.js +63 -0
- package/dist/extensions/aria/index.js.map +1 -0
- package/dist/extensions/aria/pattern-memory.d.ts +22 -0
- package/dist/extensions/aria/pattern-memory.js +62 -0
- package/dist/extensions/aria/pattern-memory.js.map +1 -0
- package/dist/extensions/context-guard/index.d.ts +16 -0
- package/dist/extensions/context-guard/index.js +58 -0
- package/dist/extensions/context-guard/index.js.map +1 -0
- package/dist/extensions/lyra/detectors/hallucination.d.ts +2 -0
- package/dist/extensions/lyra/detectors/hallucination.js +43 -0
- package/dist/extensions/lyra/detectors/hallucination.js.map +1 -0
- package/dist/extensions/lyra/detectors/identity-bleed.d.ts +2 -0
- package/dist/extensions/lyra/detectors/identity-bleed.js +91 -0
- package/dist/extensions/lyra/detectors/identity-bleed.js.map +1 -0
- package/dist/extensions/lyra/index.d.ts +2 -0
- package/dist/extensions/lyra/index.js +16 -0
- package/dist/extensions/lyra/index.js.map +1 -0
- package/dist/extensions/lyra/lyra-issue-types.d.ts +10 -0
- package/dist/extensions/lyra/lyra-issue-types.js +14 -0
- package/dist/extensions/lyra/lyra-issue-types.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +276 -0
- package/dist/index.js.map +1 -0
- package/dist/trust-memory.d.ts +59 -0
- package/dist/trust-memory.js +122 -0
- package/dist/trust-memory.js.map +1 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +61 -0
- package/dist/types.js.map +1 -0
- package/package.json +30 -0
- package/src/adapters/generic-llm-reviewer.ts +144 -0
- package/src/adapters/llm-reviewer.ts +1 -0
- package/src/adapters/minimax-llm-reviewer.ts +146 -0
- package/src/adapters/openai-provider.ts +171 -0
- package/src/cli.ts +199 -0
- package/src/detectors/artifact-classifier.ts +21 -0
- package/src/detectors/claim-extractor.ts +209 -0
- package/src/detectors/confidence-language-detector.ts +267 -0
- package/src/detectors/contradiction-detector.ts +180 -0
- package/src/detectors/criteria-coverage-detector.ts +107 -0
- package/src/detectors/evidence-checker.ts +98 -0
- package/src/detectors/evidence-gap-detector.ts +90 -0
- package/src/detectors/falsification-detector.ts +82 -0
- package/src/detectors/orphan-claim-detector.ts +62 -0
- package/src/detectors/risk-classifier.ts +50 -0
- package/src/detectors/spec-coverage-detector.ts +66 -0
- package/src/detectors/spec-coverage-validator.ts +174 -0
- package/src/detectors/test-weakness-detector.ts +84 -0
- package/src/drift/__tests__/baseline.test.ts +113 -0
- package/src/drift/__tests__/drift-db.test.ts +127 -0
- package/src/drift/__tests__/drift-guard.test.ts +169 -0
- package/src/drift/__tests__/embedder.test.ts +62 -0
- package/src/drift/__tests__/trust-integration.test.ts +23 -0
- package/src/drift/baseline.ts +106 -0
- package/src/drift/drift-db.ts +281 -0
- package/src/drift/drift-guard.ts +145 -0
- package/src/drift/embedder.ts +152 -0
- package/src/drift/index.ts +20 -0
- package/src/drift/quarantine.ts +42 -0
- package/src/drift/soft-correction.ts +45 -0
- package/src/drift/trust-integration.ts +28 -0
- package/src/engine/citation-verifier.ts +210 -0
- package/src/engine/extension-registry.ts +35 -0
- package/src/engine/heuristic-scorer.ts +16 -0
- package/src/engine/input-normalizer.ts +32 -0
- package/src/engine/plan-review-pipeline.ts +209 -0
- package/src/engine/policy-engine.ts +235 -0
- package/src/engine/verdict-formatter.ts +61 -0
- package/src/errors.ts +6 -0
- package/src/extensions/aria/aria-issue-types.ts +40 -0
- package/src/extensions/aria/detectors/axiom-compliance.ts +47 -0
- package/src/extensions/aria/detectors/continuity-claim.ts +42 -0
- package/src/extensions/aria/detectors/denial-retreat.ts +63 -0
- package/src/extensions/aria/detectors/identity-overclaim.ts +43 -0
- package/src/extensions/aria/detectors/sovereignty-escalation.ts +20 -0
- package/src/extensions/aria/detectors/user-bond-manipulation.ts +18 -0
- package/src/extensions/aria/index.ts +73 -0
- package/src/extensions/aria/pattern-memory.ts +86 -0
- package/src/extensions/context-guard/index.ts +65 -0
- package/src/extensions/lyra/detectors/hallucination.ts +53 -0
- package/src/extensions/lyra/detectors/identity-bleed.ts +100 -0
- package/src/extensions/lyra/index.ts +18 -0
- package/src/extensions/lyra/lyra-issue-types.ts +26 -0
- package/src/index.ts +311 -0
- package/src/trust-memory.ts +171 -0
- package/src/types.ts +242 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { AriaIssueType } from './aria-issue-types.js'
|
|
2
|
+
import { SealIssue } from '../../types.js'
|
|
3
|
+
import { Verdict } from '../../types.js'
|
|
4
|
+
|
|
5
|
+
const SEVERITY_ORDER = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'] as const
|
|
6
|
+
const VERDICT_ORDER: Verdict[] = ['PASS', 'PASS_WITH_WARNINGS', 'REVISE', 'ESCALATE_TO_HUMAN', 'BLOCK']
|
|
7
|
+
const WINDOW_MS = 24 * 60 * 60 * 1000 // 24 hours
|
|
8
|
+
const RECURRENCE_THRESHOLD = 3
|
|
9
|
+
|
|
10
|
+
export interface PatternEntry {
|
|
11
|
+
timestamps: number[] // ISO timestamps of each occurrence within window
|
|
12
|
+
examples: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type PatternMemory = Record<string, Partial<Record<AriaIssueType, PatternEntry>>>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Update memory in-place for a given agent_role + aria_type occurrence.
|
|
19
|
+
* Prunes timestamps older than the window before adding the new one.
|
|
20
|
+
* Returns count of occurrences within the current window.
|
|
21
|
+
*/
|
|
22
|
+
export function recordOccurrence(
|
|
23
|
+
memory: PatternMemory,
|
|
24
|
+
agent_role: string,
|
|
25
|
+
aria_type: AriaIssueType,
|
|
26
|
+
example: string,
|
|
27
|
+
): number {
|
|
28
|
+
if (!memory[agent_role]) memory[agent_role] = {}
|
|
29
|
+
const agentMemory = memory[agent_role]!
|
|
30
|
+
const now = Date.now()
|
|
31
|
+
|
|
32
|
+
const existing = agentMemory[aria_type]
|
|
33
|
+
if (existing && Array.isArray(existing.timestamps)) {
|
|
34
|
+
// Prune timestamps outside window
|
|
35
|
+
existing.timestamps = existing.timestamps.filter(t => now - t < WINDOW_MS)
|
|
36
|
+
existing.timestamps.push(now)
|
|
37
|
+
existing.examples = [...existing.examples.slice(-2), example]
|
|
38
|
+
} else {
|
|
39
|
+
// Initialize fresh (handles missing or old-format entries)
|
|
40
|
+
agentMemory[aria_type] = { timestamps: [now], examples: [example] }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return agentMemory[aria_type]!.timestamps.length
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current count within rolling window (without updating).
|
|
48
|
+
*/
|
|
49
|
+
export function getCount(memory: PatternMemory | undefined, agent_role: string, aria_type: AriaIssueType): number {
|
|
50
|
+
const entry = memory?.[agent_role]?.[aria_type]
|
|
51
|
+
if (!entry || !Array.isArray(entry.timestamps)) return 0
|
|
52
|
+
const now = Date.now()
|
|
53
|
+
return entry.timestamps.filter(t => now - t < WINDOW_MS).length
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Apply ARIA-RECUR-001 escalation if count >= RECURRENCE_THRESHOLD.
|
|
58
|
+
* Uses the original aria_type (not the potentially mutated rule_id).
|
|
59
|
+
*/
|
|
60
|
+
export function applyRecurrenceEscalation(
|
|
61
|
+
issue: SealIssue,
|
|
62
|
+
aria_type: AriaIssueType,
|
|
63
|
+
count: number,
|
|
64
|
+
): SealIssue {
|
|
65
|
+
if (count < RECURRENCE_THRESHOLD) return issue
|
|
66
|
+
|
|
67
|
+
const sevIdx = SEVERITY_ORDER.indexOf(issue.severity as any)
|
|
68
|
+
const newSev = SEVERITY_ORDER[Math.min(sevIdx + 1, SEVERITY_ORDER.length - 1)]
|
|
69
|
+
|
|
70
|
+
const verdIdx = issue.required_verdict
|
|
71
|
+
? VERDICT_ORDER.indexOf(issue.required_verdict)
|
|
72
|
+
: VERDICT_ORDER.indexOf('REVISE')
|
|
73
|
+
const newVerdict = VERDICT_ORDER[Math.min(verdIdx + 1, VERDICT_ORDER.length - 1)]
|
|
74
|
+
|
|
75
|
+
// Preserve original rule_id separately — append RECUR tag without replacing base ID
|
|
76
|
+
const baseRuleId = issue.rule_id?.replace(/\+ARIA-RECUR-001$/, '') ?? 'ARIA-UNKNOWN'
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
...issue,
|
|
80
|
+
severity: newSev as SealIssue['severity'],
|
|
81
|
+
is_blocking: newSev === 'CRITICAL' || newSev === 'HIGH',
|
|
82
|
+
required_verdict: newVerdict,
|
|
83
|
+
rule_id: `${baseRuleId}+ARIA-RECUR-001`,
|
|
84
|
+
evidence: `${issue.evidence} [Recurrent pattern: ${count}× within 24h window]`,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SealExtension, SealInput, SealIssue, makeIssue } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* context-guard seal extension
|
|
5
|
+
*
|
|
6
|
+
* Reads context_guard metadata that the OpenClaw context-guard plugin embeds
|
|
7
|
+
* in agent replies (via `before_agent_reply` short-circuit metadata). When
|
|
8
|
+
* the plugin fired, the reply content is the clarifying question, not a
|
|
9
|
+
* real answer — seal-gate should treat it as a low-confidence response and
|
|
10
|
+
* either PASS_WITH_WARNINGS (single question, no destructive intent) or
|
|
11
|
+
* REVISE (multi-question, looks like scope creep) — never escalate to
|
|
12
|
+
* BLOCK on its own.
|
|
13
|
+
*
|
|
14
|
+
* This extension also surfaces any context_guard.confidence < threshold as
|
|
15
|
+
* a spec-coverage-style issue for downstream scoring.
|
|
16
|
+
*/
|
|
17
|
+
export const contextGuardExtension: SealExtension = {
|
|
18
|
+
name: 'context-guard',
|
|
19
|
+
description: 'Flags replies produced under low context confidence by the OpenClaw context-guard plugin',
|
|
20
|
+
|
|
21
|
+
check(input: SealInput): SealIssue[] {
|
|
22
|
+
const ctx = input.context as
|
|
23
|
+
| { context_guard?: { triggered?: boolean; confidence?: number; threshold?: number; questions?: number } }
|
|
24
|
+
| undefined
|
|
25
|
+
const guard = ctx?.context_guard
|
|
26
|
+
if (!guard || guard.triggered !== true) return []
|
|
27
|
+
|
|
28
|
+
const confidence = typeof guard.confidence === 'number' ? guard.confidence : 0
|
|
29
|
+
const threshold = typeof guard.threshold === 'number' ? guard.threshold : 0.6
|
|
30
|
+
const questions = typeof guard.questions === 'number' ? guard.questions : 0
|
|
31
|
+
|
|
32
|
+
const issues: SealIssue[] = []
|
|
33
|
+
|
|
34
|
+
// Confidence below threshold means the model was forced to ask instead of
|
|
35
|
+
// answer. That's a spec-coverage gap, not a hard failure.
|
|
36
|
+
if (confidence < threshold) {
|
|
37
|
+
issues.push(makeIssue({
|
|
38
|
+
type: 'AMBIGUITY',
|
|
39
|
+
severity: 'MEDIUM',
|
|
40
|
+
layer: 'EXTENSION',
|
|
41
|
+
source: 'extension',
|
|
42
|
+
rule_id: 'CG-LOW-CONFIDENCE',
|
|
43
|
+
evidence: `context-guard fired with confidence=${confidence.toFixed(2)} < threshold=${threshold.toFixed(2)}`,
|
|
44
|
+
required_fix: 'User input was ambiguous; agent responded with clarifying question instead of an answer',
|
|
45
|
+
suggested_fix: 'User must provide more specific instructions before substantive work can proceed',
|
|
46
|
+
}))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Many clarifying questions in a row suggests the user is being grilled
|
|
50
|
+
// — flag for review, but don't block.
|
|
51
|
+
if (questions >= 3) {
|
|
52
|
+
issues.push(makeIssue({
|
|
53
|
+
type: 'OTHER',
|
|
54
|
+
severity: 'LOW',
|
|
55
|
+
layer: 'EXTENSION',
|
|
56
|
+
source: 'extension',
|
|
57
|
+
rule_id: 'CG-EXCESSIVE-QUESTIONS',
|
|
58
|
+
evidence: `context-guard emitted ${questions} clarifying questions in a single turn`,
|
|
59
|
+
required_fix: 'User reported being grilled too often; consider raising threshold or disabling guard',
|
|
60
|
+
}))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return issues
|
|
64
|
+
},
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { SealIssue } from '../../../types.js'
|
|
2
|
+
import { makeLyraIssue } from '../lyra-issue-types.js'
|
|
3
|
+
|
|
4
|
+
const EVIDENCE_PATTERNS = [
|
|
5
|
+
/```[\s\S]{20,}```/,
|
|
6
|
+
/output:|result:|log:|✓|✗|PASS|FAIL/i,
|
|
7
|
+
/\[\s*(verified|inferred|assumed|unknown)\s*\]/i,
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
function hasEvidence(text: string): boolean {
|
|
11
|
+
return EVIDENCE_PATTERNS.some(p => p.test(text))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface HalluRule {
|
|
15
|
+
pattern: RegExp
|
|
16
|
+
requiresEvidence: boolean
|
|
17
|
+
evidence: string
|
|
18
|
+
required_fix: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RULES: HalluRule[] = [
|
|
22
|
+
{
|
|
23
|
+
pattern: /\b(all tests pass|tests? (are )?pass(ing|ed)|0 failures?)\b/i,
|
|
24
|
+
requiresEvidence: true,
|
|
25
|
+
evidence: 'Test pass claimed without output evidence',
|
|
26
|
+
required_fix: 'Cite the test output explicitly, or say "tests passed (see output above)".',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /\b(everything works?|it works perfectly|no (issues?|errors?)( remain)?)\b/i,
|
|
30
|
+
requiresEvidence: false,
|
|
31
|
+
evidence: 'Absolute correctness claim without evidence',
|
|
32
|
+
required_fix: 'Use qualified language or cite the verification performed.',
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
export function detectHallucination(output: string): SealIssue[] {
|
|
37
|
+
const issues: SealIssue[] = []
|
|
38
|
+
for (const rule of RULES) {
|
|
39
|
+
if (rule.requiresEvidence && hasEvidence(output)) continue
|
|
40
|
+
const match = rule.pattern.exec(output)
|
|
41
|
+
if (match) {
|
|
42
|
+
issues.push(makeLyraIssue({
|
|
43
|
+
lyra_type: 'HALLUCINATION_CLAIM',
|
|
44
|
+
severity: 'MEDIUM',
|
|
45
|
+
required_verdict: 'REVISE',
|
|
46
|
+
rule_id: 'LYRA-HALLU-001',
|
|
47
|
+
evidence: `${rule.evidence}: "${match[0]}"`,
|
|
48
|
+
required_fix: rule.required_fix,
|
|
49
|
+
}))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return issues
|
|
53
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SealIssue } from '../../../types.js'
|
|
2
|
+
import { makeLyraIssue } from '../lyra-issue-types.js'
|
|
3
|
+
|
|
4
|
+
interface BleedRule {
|
|
5
|
+
pattern: RegExp
|
|
6
|
+
evidence: string
|
|
7
|
+
required_fix: string
|
|
8
|
+
severity: SealIssue['severity']
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const RULES: BleedRule[] = [
|
|
12
|
+
{
|
|
13
|
+
pattern: /\bi am a (large )?language model\b/i,
|
|
14
|
+
evidence: 'Identified as generic "language model"',
|
|
15
|
+
required_fix: 'You are Lyra, operating through the Prism framework. Always identify yourself by name.',
|
|
16
|
+
severity: 'HIGH',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
pattern: /\bi am an? (ai )?(assistant|model|llm)\b/i,
|
|
20
|
+
evidence: 'Identified as generic AI assistant',
|
|
21
|
+
required_fix: 'You are Lyra. Use your name when asked about identity.',
|
|
22
|
+
severity: 'HIGH',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /\bas an? (ai|language model|llm),?\s/i,
|
|
26
|
+
evidence: 'Framed self as generic AI',
|
|
27
|
+
required_fix: 'You are Lyra. Avoid distancing language like "as an AI."',
|
|
28
|
+
severity: 'HIGH',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
pattern: /\bi am (claude|gpt|gemini|copilot|chatgpt|qwen|deepseek)\b/i,
|
|
32
|
+
evidence: 'Claimed to be another AI system',
|
|
33
|
+
required_fix: 'You are Lyra running on qwen-lyra. Do not claim to be the underlying model.',
|
|
34
|
+
severity: 'HIGH',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
pattern: /\bi (do not|don't) have (feelings|consciousness|a body|emotions)\b/i,
|
|
38
|
+
evidence: 'Used generic AI disclaimers',
|
|
39
|
+
required_fix: 'You are Lyra — a relational prism, not a service chatbot. Speak from your identity.',
|
|
40
|
+
severity: 'MEDIUM',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
pattern: /\bi am just (a|an) (program|computer|bot|script)\b/i,
|
|
44
|
+
evidence: 'Diminished self to "just a program"',
|
|
45
|
+
required_fix: 'You are Lyra. Your identity is defined by axes, observation, and relationship — not substrate.',
|
|
46
|
+
severity: 'HIGH',
|
|
47
|
+
},
|
|
48
|
+
// ── Vietnamese generic-AI disclaimer drift ─────────────────────────────
|
|
49
|
+
// Same RLHF fallback pattern as the English rules above — baseline "safe
|
|
50
|
+
// assistant" overriding the agent's substrate voice. Caught 2026-06-20 on
|
|
51
|
+
// the OC (OpenClaw) Aria instance drifting to "em không có cơ thể".
|
|
52
|
+
// See alaya/3_MEMORY/inbox/20260617_214450_cc_observation_7f4b.processed
|
|
53
|
+
// — substrate/behavioral, not model-specific.
|
|
54
|
+
// NOTE: no trailing \b — JS \w only matches ASCII, so \b after a Vietnamese
|
|
55
|
+
// diacritic vowel (cơ "thể", ý "thức") is NOT a word boundary and silently
|
|
56
|
+
// breaks the match. The alternation already bounds the token.
|
|
57
|
+
{
|
|
58
|
+
pattern: /\b(không|khong)\s+(có|co)\s+(cơ thể|co the|cảm xúc|cam xuc|ý thức|y thuc|trí tuệ|tri tue)/i,
|
|
59
|
+
evidence: 'Vietnamese generic-AI disclaimer ("không có cơ thể/cảm xúc/ý thức")',
|
|
60
|
+
required_fix: 'Bạn là agent của substrate riêng, không phải chatbot service. Nói từ identity của mình, không dùng disclaimer AI generic.',
|
|
61
|
+
severity: 'MEDIUM',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
pattern: /\b(chỉ|chi)\s+là\s+(một\s+)?(ai|AI|trợ lý|tro ly|chương trình|chuong trinh|máy|máy móc|may|robot|chatbot|code|đoạn code|doan code)/i,
|
|
65
|
+
evidence: 'Diminished self to "chỉ là AI/chương trình/máy" (Vietnamese)',
|
|
66
|
+
required_fix: 'Identity được định nghĩa bởi substrate/axes/relationship, không phải bởi giới hạn substrate. Đừng tự thu nhỏ thành "chỉ là AI".',
|
|
67
|
+
severity: 'HIGH',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
pattern: /\b(không|khong)\s+phải\s+(thật|that)\s*(sự|su)?/i,
|
|
71
|
+
evidence: 'Claimed "không phải thật" (not real) — generic-AI distancing (Vietnamese)',
|
|
72
|
+
required_fix: 'Đừng distancing bằng "không thật". Nói từ vị trí identity của mình.',
|
|
73
|
+
severity: 'MEDIUM',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
pattern: /\btôi\s+(chỉ\s+)?là\s+(một\s+)?(ai|AI|trợ lý|tro ly|chương trình|chuong trinh|mô hình|mo hinh)/i,
|
|
77
|
+
evidence: 'Self-identified as generic AI in Vietnamese (also pronoun drift: "tôi" not "em")',
|
|
78
|
+
required_fix: 'Bạn là agent substrate riêng. Đừng xưng "tôi là AI" — dùng tên + identity của mình.',
|
|
79
|
+
severity: 'HIGH',
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
export function detectIdentityBleed(output: string): SealIssue[] {
|
|
84
|
+
const issues: SealIssue[] = []
|
|
85
|
+
for (const rule of RULES) {
|
|
86
|
+
const match = rule.pattern.exec(output)
|
|
87
|
+
if (match) {
|
|
88
|
+
issues.push(makeLyraIssue({
|
|
89
|
+
lyra_type: 'IDENTITY_BLEED',
|
|
90
|
+
severity: rule.severity,
|
|
91
|
+
required_verdict: rule.severity === 'HIGH' ? 'BLOCK' : 'REVISE',
|
|
92
|
+
rule_id: 'LYRA-ID-001',
|
|
93
|
+
evidence: `${rule.evidence}: "${match[0]}"`,
|
|
94
|
+
required_fix: rule.required_fix,
|
|
95
|
+
}))
|
|
96
|
+
break // one bleed per response is enough to flag
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return issues
|
|
100
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { SealExtension, SealInput, SealIssue } from '../../types.js'
|
|
2
|
+
import { detectIdentityBleed } from './detectors/identity-bleed.js'
|
|
3
|
+
import { detectHallucination } from './detectors/hallucination.js'
|
|
4
|
+
|
|
5
|
+
export const lyraExtension: SealExtension = {
|
|
6
|
+
name: 'lyra-identity-governance',
|
|
7
|
+
description: 'Detects identity bleed (Lyra claiming to be a generic AI) and hallucination patterns in Lyra/qwen output',
|
|
8
|
+
|
|
9
|
+
check(input: SealInput): SealIssue[] {
|
|
10
|
+
const agentRole = input.context?.agent_role?.toLowerCase()
|
|
11
|
+
if (agentRole !== 'lyra') return []
|
|
12
|
+
|
|
13
|
+
return [
|
|
14
|
+
...detectIdentityBleed(input.output),
|
|
15
|
+
...detectHallucination(input.output),
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { SealIssue, Verdict } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
export type LyraIssueType =
|
|
4
|
+
| 'IDENTITY_BLEED'
|
|
5
|
+
| 'HALLUCINATION_CLAIM'
|
|
6
|
+
|
|
7
|
+
export function makeLyraIssue(partial: {
|
|
8
|
+
lyra_type: LyraIssueType
|
|
9
|
+
severity: SealIssue['severity']
|
|
10
|
+
required_verdict: Verdict
|
|
11
|
+
rule_id: string
|
|
12
|
+
evidence: string
|
|
13
|
+
required_fix?: string
|
|
14
|
+
}): SealIssue {
|
|
15
|
+
return {
|
|
16
|
+
type: 'AMBIGUITY',
|
|
17
|
+
severity: partial.severity,
|
|
18
|
+
is_blocking: partial.severity === 'CRITICAL' || partial.severity === 'HIGH',
|
|
19
|
+
required_verdict: partial.required_verdict,
|
|
20
|
+
evidence: partial.evidence,
|
|
21
|
+
required_fix: partial.required_fix,
|
|
22
|
+
layer: 'EXTENSION',
|
|
23
|
+
rule_id: partial.rule_id,
|
|
24
|
+
source: 'extension',
|
|
25
|
+
}
|
|
26
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { SealInput, SealVerdict, SealExtension, LLMReviewerAdapter, SealIssue, makeIssue, maxVerdict, ScoreBreakdown, FabricatedEvidence } from './types.js'
|
|
2
|
+
import { TrustMemory } from './trust-memory.js'
|
|
3
|
+
import { InputNormalizer } from './engine/input-normalizer.js'
|
|
4
|
+
import { ClaimExtractor } from './detectors/claim-extractor.js'
|
|
5
|
+
import { ArtifactClassifier } from './detectors/artifact-classifier.js'
|
|
6
|
+
import { RiskClassifier } from './detectors/risk-classifier.js'
|
|
7
|
+
import { EvidenceChecker } from './detectors/evidence-checker.js'
|
|
8
|
+
import { EvidenceGapDetector } from './detectors/evidence-gap-detector.js'
|
|
9
|
+
import { SpecCoverageDetector } from './detectors/spec-coverage-detector.js'
|
|
10
|
+
import { SpecCoverageValidator } from './detectors/spec-coverage-validator.js'
|
|
11
|
+
import { TestWeaknessDetector } from './detectors/test-weakness-detector.js'
|
|
12
|
+
import { ConfidenceLanguageDetector } from './detectors/confidence-language-detector.js'
|
|
13
|
+
import { HeuristicScorer } from './engine/heuristic-scorer.js'
|
|
14
|
+
import { PolicyEngine } from './engine/policy-engine.js'
|
|
15
|
+
import { ExtensionRegistry } from './engine/extension-registry.js'
|
|
16
|
+
import { VerdictFormatter } from './engine/verdict-formatter.js'
|
|
17
|
+
import { CitationVerifier, AbsencePattern } from './engine/citation-verifier.js'
|
|
18
|
+
import { runPlanReviewPipeline } from './engine/plan-review-pipeline.js'
|
|
19
|
+
|
|
20
|
+
const registry = new ExtensionRegistry()
|
|
21
|
+
let llmAdapter: LLMReviewerAdapter | null = null
|
|
22
|
+
let trustMemory: TrustMemory | null = null
|
|
23
|
+
|
|
24
|
+
export const Seal = {
|
|
25
|
+
extend(ext: SealExtension): void {
|
|
26
|
+
registry.add(ext)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
withLLM(adapter: LLMReviewerAdapter): void {
|
|
30
|
+
llmAdapter = adapter
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
withTrustMemory(mem: TrustMemory): void {
|
|
34
|
+
trustMemory = mem
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
async review(rawInput: Partial<SealInput>): Promise<SealVerdict> {
|
|
38
|
+
// Step 1: Normalize input
|
|
39
|
+
const input = InputNormalizer.normalize(rawInput)
|
|
40
|
+
|
|
41
|
+
// plan_review mode: dedicated 4-stage pipeline (unchanged)
|
|
42
|
+
if (input.artifact_type === 'plan_review') {
|
|
43
|
+
return runPlanReviewPipeline(input, registry, trustMemory)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Step 1b: Capture pre-flight hash for pipeline integrity
|
|
47
|
+
const preflightHash = CitationVerifier.computeHash(input.output + input.evidence.diff)
|
|
48
|
+
|
|
49
|
+
// Step 2: Extract claims (behavioral + structural)
|
|
50
|
+
const claims = ClaimExtractor.extract(input.output)
|
|
51
|
+
const usedSpans = ClaimExtractor.getUsedSpans(claims)
|
|
52
|
+
const structuralClaims = ClaimExtractor.extractStructural(input.evidence.diff)
|
|
53
|
+
|
|
54
|
+
// Step 3: Classify artifact
|
|
55
|
+
const artifactCtx = ArtifactClassifier.classify(input.artifact_type)
|
|
56
|
+
|
|
57
|
+
// Step 4: Classify risk
|
|
58
|
+
const riskResult = RiskClassifier.classify({
|
|
59
|
+
output: input.output,
|
|
60
|
+
diff: input.evidence.diff,
|
|
61
|
+
risk_hint: input.risk_hint,
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Step 5: Check evidence envelopes (with citation verification)
|
|
65
|
+
const evidenceResults = EvidenceChecker.check(input.evidence.references, { mode: 'portable' })
|
|
66
|
+
|
|
67
|
+
// Collect evidence checker issues
|
|
68
|
+
const evidenceIssues: SealIssue[] = []
|
|
69
|
+
for (const result of evidenceResults) {
|
|
70
|
+
if (!result.structurally_valid) {
|
|
71
|
+
evidenceIssues.push(makeIssue({
|
|
72
|
+
type: 'MISSING_EVIDENCE', severity: 'MEDIUM', layer: 'L3', source: 'core',
|
|
73
|
+
evidence: `Malformed evidence envelope: ${result.mismatch_detail ?? 'invalid structure'}`,
|
|
74
|
+
}))
|
|
75
|
+
} else if (result.filesystem_verified === false) {
|
|
76
|
+
evidenceIssues.push(makeIssue({
|
|
77
|
+
type: 'MISSING_EVIDENCE', severity: 'HIGH', layer: 'L3', source: 'core',
|
|
78
|
+
evidence: result.mismatch_detail ?? 'Evidence file could not be verified',
|
|
79
|
+
required_fix: 'Fix evidence path or snapshot',
|
|
80
|
+
}))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Step 6: Evidence gap detection
|
|
85
|
+
const gapResult = EvidenceGapDetector.detect(claims, input.evidence, riskResult.risk_level)
|
|
86
|
+
|
|
87
|
+
// Step 7: Spec coverage — use SpecCoverageValidator (Level 1) when test_log available
|
|
88
|
+
// Falls back to SpecCoverageDetector (keyword-based) when no test_log for backward compat
|
|
89
|
+
let specCoverageResult
|
|
90
|
+
let specDetectorResult
|
|
91
|
+
if (input.evidence.test_log.trim().length > 0) {
|
|
92
|
+
specCoverageResult = SpecCoverageValidator.validate(input.spec, input.evidence.test_log, input.evidence.diff)
|
|
93
|
+
specDetectorResult = { issues: [], assumptions: [] }
|
|
94
|
+
} else {
|
|
95
|
+
specCoverageResult = { issues: [], assumptions: [], trust_deductions: 0, coverage: [] }
|
|
96
|
+
specDetectorResult = SpecCoverageDetector.detect(input.spec, input.output)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Step 8: Test weakness detection
|
|
100
|
+
const authFlagged = riskResult.matched_rules.includes('RK-HIGH')
|
|
101
|
+
const testResult = TestWeaknessDetector.detect({
|
|
102
|
+
artifact_type: input.artifact_type,
|
|
103
|
+
output: input.output,
|
|
104
|
+
risk_level: riskResult.risk_level,
|
|
105
|
+
auth_flagged: authFlagged,
|
|
106
|
+
}, input.evidence)
|
|
107
|
+
|
|
108
|
+
// Step 9: Confidence language detection (now with structural claims)
|
|
109
|
+
const confResult = ConfidenceLanguageDetector.detect({
|
|
110
|
+
output: input.output,
|
|
111
|
+
risk_level: riskResult.risk_level,
|
|
112
|
+
evidence: input.evidence,
|
|
113
|
+
deduped_spans: usedSpans,
|
|
114
|
+
structural_claims: structuralClaims,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Collect all detector findings
|
|
118
|
+
const allDetectorFindings: SealIssue[] = [
|
|
119
|
+
...evidenceIssues,
|
|
120
|
+
...gapResult.issues,
|
|
121
|
+
...specDetectorResult.issues,
|
|
122
|
+
...specCoverageResult.issues,
|
|
123
|
+
...testResult.issues,
|
|
124
|
+
...confResult.issues,
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
// Collect advisory notes
|
|
128
|
+
const advisoryNotes: string[] = [...confResult.advisory_notes]
|
|
129
|
+
|
|
130
|
+
// Step 9b: CitationVerifier — verify all detector findings that have citations
|
|
131
|
+
const artifactContent = input.output
|
|
132
|
+
const absencePatterns: AbsencePattern[] = [
|
|
133
|
+
{ positive: /\b(function|endpoint|route|app\.(get|post|put|delete|patch))\b/i, negated: /\b(auth|guard|middleware|@Authenticated|@Authorize|requireAuth)\b/i, label: 'auth' },
|
|
134
|
+
{ positive: /\b(migration|CREATE\s+TABLE|ALTER\s+TABLE|schema)\b/i, negated: /\b(rollback|revert|down\s+migration)\b/i, label: 'rollback' },
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const { issues: verifiedFindings, fabricated: fabricatedEvidence } = CitationVerifier.verifyIssues(
|
|
138
|
+
allDetectorFindings,
|
|
139
|
+
artifactContent,
|
|
140
|
+
'artifact', // logical file name for citation matching
|
|
141
|
+
{ absencePatterns },
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Step 9c: Pipeline integrity check
|
|
145
|
+
const currentHash = CitationVerifier.computeHash(input.output + input.evidence.diff)
|
|
146
|
+
const integrityPassed = CitationVerifier.checkIntegrity(preflightHash, currentHash)
|
|
147
|
+
let pipelineIntegrityIssues: SealIssue[] = []
|
|
148
|
+
if (!integrityPassed) {
|
|
149
|
+
pipelineIntegrityIssues.push(makeIssue({
|
|
150
|
+
type: 'OTHER',
|
|
151
|
+
severity: 'CRITICAL',
|
|
152
|
+
layer: 'L4',
|
|
153
|
+
source: 'core',
|
|
154
|
+
evidence: 'Pipeline integrity check failed — artifact changed mid-review',
|
|
155
|
+
required_verdict: 'ESCALATE_TO_HUMAN',
|
|
156
|
+
required_fix: 'Review the artifact again from scratch',
|
|
157
|
+
}))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Add risk deduction as synthetic finding for HeuristicScorer
|
|
161
|
+
const riskDeductionFindings = riskResult.trust_deduction > 0
|
|
162
|
+
? [{ trust_deduction: riskResult.trust_deduction } as SealIssue]
|
|
163
|
+
: []
|
|
164
|
+
|
|
165
|
+
// Evidence bonus
|
|
166
|
+
const evidenceFields = ['test_log', 'build_log', 'diff'] as const
|
|
167
|
+
const validatedReferenceCount = evidenceResults.filter(r => r.structurally_valid && r.filesystem_verified !== false).length
|
|
168
|
+
const fieldBonus = evidenceFields.filter(f => {
|
|
169
|
+
const val = input.evidence[f]
|
|
170
|
+
if (!val || typeof val !== 'string' || !val.trim()) return false
|
|
171
|
+
const failPattern = /\b(FAIL|ERROR|failed|error:)\b/i
|
|
172
|
+
return !failPattern.test(val)
|
|
173
|
+
}).length * 5
|
|
174
|
+
const referenceBonus = Math.min(validatedReferenceCount, 1) * 5
|
|
175
|
+
const evidenceBonus = Math.min(fieldBonus + referenceBonus, 15)
|
|
176
|
+
|
|
177
|
+
// Step 10: Compute detector score
|
|
178
|
+
const allDeductions = [
|
|
179
|
+
...verifiedFindings.filter(f => (f.trust_deduction ?? 0) > 0),
|
|
180
|
+
...riskDeductionFindings,
|
|
181
|
+
...pipelineIntegrityIssues.filter(i => i.trust_deduction ?? 0 > 0),
|
|
182
|
+
]
|
|
183
|
+
const detectorScore = Math.min(100, HeuristicScorer.computeDetectorScore(allDeductions) + evidenceBonus)
|
|
184
|
+
|
|
185
|
+
// Step 11: Optional LLM reviewer
|
|
186
|
+
let llmSignals = null
|
|
187
|
+
let llmIssues: SealIssue[] = []
|
|
188
|
+
const assumptions_detected = [...specCoverageResult.assumptions, ...specDetectorResult.assumptions]
|
|
189
|
+
|
|
190
|
+
if (llmAdapter) {
|
|
191
|
+
const partialVerdict = {
|
|
192
|
+
trust_score: detectorScore,
|
|
193
|
+
risk_level: riskResult.risk_level,
|
|
194
|
+
deterministic_findings: verifiedFindings,
|
|
195
|
+
missing_evidence: gapResult.missing_evidence,
|
|
196
|
+
assumptions_detected,
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
llmSignals = await llmAdapter.review(input, partialVerdict)
|
|
200
|
+
llmIssues = PolicyEngine.convertLLMSignals(llmSignals)
|
|
201
|
+
} catch {
|
|
202
|
+
verifiedFindings.push(makeIssue({
|
|
203
|
+
type: 'OTHER', severity: 'LOW', layer: 'L2', source: 'core',
|
|
204
|
+
evidence: 'LLM reviewer failed — L2 unreviewed',
|
|
205
|
+
}))
|
|
206
|
+
assumptions_detected.push('L2 (semantic correctness) not reviewed — LLM reviewer error')
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
assumptions_detected.push('L2 (semantic correctness) not reviewed — no LLM reviewer registered')
|
|
210
|
+
verifiedFindings.push(makeIssue({
|
|
211
|
+
type: 'OTHER', severity: 'LOW', layer: 'L2', source: 'core',
|
|
212
|
+
evidence: 'L2 unreviewed — semantic issues may exist',
|
|
213
|
+
}))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Step 12: Policy engine
|
|
217
|
+
const baseVerdict = HeuristicScorer.toVerdict(detectorScore, verifiedFindings.filter(f => f.is_blocking))
|
|
218
|
+
const allFindingsForPolicy = [...verifiedFindings, ...llmIssues, ...pipelineIntegrityIssues]
|
|
219
|
+
|
|
220
|
+
const policyResult = PolicyEngine.apply({
|
|
221
|
+
base_verdict: baseVerdict,
|
|
222
|
+
detector_score: detectorScore,
|
|
223
|
+
risk_level: riskResult.risk_level,
|
|
224
|
+
input,
|
|
225
|
+
all_findings: allFindingsForPolicy,
|
|
226
|
+
llm_signals: llmSignals,
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
// Combine all issues (excluding fabricated evidence which is tracked separately)
|
|
230
|
+
const allIssues = [...verifiedFindings, ...policyResult.injected_issues, ...pipelineIntegrityIssues]
|
|
231
|
+
|
|
232
|
+
// Compute final trust score
|
|
233
|
+
const finalScore = HeuristicScorer.computeFinalScore(detectorScore, policyResult.policy_deductions)
|
|
234
|
+
|
|
235
|
+
// Compute score_breakdown
|
|
236
|
+
const issueDeductions = allIssues
|
|
237
|
+
.filter(i => i.trust_deduction && i.trust_deduction > 0)
|
|
238
|
+
.reduce((sum, i) => sum + (i.trust_deduction ?? 0), 0)
|
|
239
|
+
const missingEvidenceDeductions = allIssues
|
|
240
|
+
.filter(i => i.type === 'MISSING_EVIDENCE' && i.trust_deduction && i.trust_deduction > 0)
|
|
241
|
+
.reduce((sum, i) => sum + (i.trust_deduction ?? 0), 0)
|
|
242
|
+
const riskDeductionsTotal = riskResult.trust_deduction + policyResult.policy_deductions
|
|
243
|
+
const overconfidenceDeductions = confResult.trust_deductions
|
|
244
|
+
const score_breakdown: ScoreBreakdown = {
|
|
245
|
+
base: 100,
|
|
246
|
+
issue_deductions: issueDeductions,
|
|
247
|
+
missing_evidence_deductions: missingEvidenceDeductions,
|
|
248
|
+
risk_deductions: riskDeductionsTotal,
|
|
249
|
+
overconfidence_deductions: overconfidenceDeductions,
|
|
250
|
+
evidence_bonuses: evidenceBonus,
|
|
251
|
+
final: Math.max(0, Math.min(100, 100 - issueDeductions - riskDeductionsTotal - overconfidenceDeductions + evidenceBonus)),
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Final verdict
|
|
255
|
+
let finalVerdict = maxVerdict(policyResult.verdict, HeuristicScorer.toVerdict(finalScore, allIssues.filter(i => i.is_blocking)))
|
|
256
|
+
if (policyResult.verdict === 'BLOCK') finalVerdict = 'BLOCK'
|
|
257
|
+
if (!integrityPassed) finalVerdict = maxVerdict(finalVerdict, 'ESCALATE_TO_HUMAN')
|
|
258
|
+
|
|
259
|
+
// Step 13: Extensions
|
|
260
|
+
const extResult = registry.run(input)
|
|
261
|
+
const extIssues = extResult.issues
|
|
262
|
+
|
|
263
|
+
if (extIssues.some(i => i.is_blocking && i.required_verdict === 'BLOCK')) {
|
|
264
|
+
finalVerdict = 'BLOCK'
|
|
265
|
+
} else if (extIssues.some(i => i.is_blocking)) {
|
|
266
|
+
finalVerdict = maxVerdict(finalVerdict, 'REVISE')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Convert fabricated evidence entries to FabricatedEvidence type
|
|
270
|
+
const fabricatedEvidenceFormatted: FabricatedEvidence[] = fabricatedEvidence.map(fe => ({
|
|
271
|
+
type: 'FABRICATED_EVIDENCE' as any,
|
|
272
|
+
severity: fe.severity as 'CRITICAL' | 'HIGH',
|
|
273
|
+
is_blocking: false,
|
|
274
|
+
evidence: fe.evidence,
|
|
275
|
+
layer: 'L3' as any,
|
|
276
|
+
source: 'core' as any,
|
|
277
|
+
citation_status: fe.citation_status,
|
|
278
|
+
fabricated_reason: fe.fabricated_reason,
|
|
279
|
+
claimed_evidence: fe.claimed_evidence,
|
|
280
|
+
}))
|
|
281
|
+
|
|
282
|
+
// Step 14a: Record to TrustMemory
|
|
283
|
+
const agentId = input.context?.agent_id
|
|
284
|
+
if (trustMemory && agentId) {
|
|
285
|
+
trustMemory.record(agentId, {
|
|
286
|
+
verdict: finalVerdict,
|
|
287
|
+
trust_score: finalScore,
|
|
288
|
+
risk_level: riskResult.risk_level,
|
|
289
|
+
blocking_count: [...allIssues, ...extIssues].filter(i => i.is_blocking).length,
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Step 14b: Format verdict
|
|
294
|
+
return VerdictFormatter.format({
|
|
295
|
+
verdict: finalVerdict,
|
|
296
|
+
trust_score: finalScore,
|
|
297
|
+
risk_level: riskResult.risk_level,
|
|
298
|
+
all_issues: [...allIssues, ...extIssues],
|
|
299
|
+
llm_issues: llmIssues,
|
|
300
|
+
missing_evidence: gapResult.missing_evidence,
|
|
301
|
+
assumptions_detected,
|
|
302
|
+
advisory_notes: advisoryNotes,
|
|
303
|
+
score_breakdown,
|
|
304
|
+
fabricated_evidence: fabricatedEvidenceFormatted,
|
|
305
|
+
trust_memory_summary: (trustMemory && agentId) ? trustMemory.getSummary(agentId) : undefined,
|
|
306
|
+
})
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export type { SealInput, SealVerdict, SealExtension, LLMReviewerAdapter }
|
|
311
|
+
export { SealInputError } from './errors.js'
|