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,28 @@
|
|
|
1
|
+
import type { DriftStatus } from './drift-guard.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight adapter: map drift distance/status to [0,1] reliability score
|
|
5
|
+
* that can be fed into TrustMemory.
|
|
6
|
+
*
|
|
7
|
+
* Safe to import even when TrustMemory v2 is not yet integrated:
|
|
8
|
+
* this module has zero side effects and produces a plain number.
|
|
9
|
+
*/
|
|
10
|
+
export function driftScoreForTrust(
|
|
11
|
+
status: DriftStatus,
|
|
12
|
+
distance: number,
|
|
13
|
+
options?: { hardLimit: number }
|
|
14
|
+
): number {
|
|
15
|
+
const hard = options?.hardLimit ?? 0.55;
|
|
16
|
+
|
|
17
|
+
switch (status) {
|
|
18
|
+
case 'NORMAL':
|
|
19
|
+
return 1.0;
|
|
20
|
+
case 'SOFT_DRIFT': {
|
|
21
|
+
// Linear degradation from 1.0 at soft_limit to 0.5 at hard_limit
|
|
22
|
+
const t = Math.min(distance / hard, 1.0);
|
|
23
|
+
return 1.0 - t * 0.5;
|
|
24
|
+
}
|
|
25
|
+
case 'HARD_DRIFT':
|
|
26
|
+
return 0.0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { SealIssue, CitationStatus } from '../types.js'
|
|
3
|
+
|
|
4
|
+
export type EvidenceGrade = 'strong' | 'weak' | 'none'
|
|
5
|
+
|
|
6
|
+
export interface CitationVerdict {
|
|
7
|
+
status: CitationStatus
|
|
8
|
+
quality: EvidenceGrade
|
|
9
|
+
similarScore: number
|
|
10
|
+
isFabricated: boolean
|
|
11
|
+
fabricatedReason?: 'location_not_found' | 'content_mismatch'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AbsencePattern {
|
|
15
|
+
positive: RegExp
|
|
16
|
+
negated: RegExp
|
|
17
|
+
label?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SIMILARITY_STRONG = 0.85
|
|
21
|
+
const SIMILARITY_WEAK = 0.60
|
|
22
|
+
|
|
23
|
+
function normalize(str: string): string {
|
|
24
|
+
return str.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Levenshtein distance between two strings */
|
|
28
|
+
function levenshtein(a: string, b: string): number {
|
|
29
|
+
const m = a.length
|
|
30
|
+
const n = b.length
|
|
31
|
+
// Early termination for empty strings
|
|
32
|
+
if (m === 0) return n
|
|
33
|
+
if (n === 0) return m
|
|
34
|
+
|
|
35
|
+
// Use two rows to reduce memory
|
|
36
|
+
let prev: number[] = Array.from({ length: n + 1 }, (_, i) => i)
|
|
37
|
+
let curr: number[] = new Array(n + 1)
|
|
38
|
+
|
|
39
|
+
for (let i = 1; i <= m; i++) {
|
|
40
|
+
curr[0] = i
|
|
41
|
+
for (let j = 1; j <= n; j++) {
|
|
42
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1
|
|
43
|
+
curr[j] = Math.min(
|
|
44
|
+
prev[j] + 1, // deletion
|
|
45
|
+
curr[j - 1] + 1, // insertion
|
|
46
|
+
prev[j - 1] + cost, // substitution
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
;[prev, curr] = [curr, prev]
|
|
50
|
+
}
|
|
51
|
+
return prev[n]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fuzzy-match a quote against actual content.
|
|
56
|
+
* Returns 0-1 similarity score where 1 = exact match.
|
|
57
|
+
*/
|
|
58
|
+
function fuzzyMatch(quote: string, content: string): number {
|
|
59
|
+
const normQuote = normalize(quote)
|
|
60
|
+
const normContent = normalize(content)
|
|
61
|
+
if (normContent.length === 0 && normQuote.length === 0) return 1
|
|
62
|
+
if (normContent.length === 0) return 0
|
|
63
|
+
const dist = levenshtein(normQuote, normContent)
|
|
64
|
+
const maxLen = Math.max(normQuote.length, normContent.length)
|
|
65
|
+
if (maxLen === 0) return 1
|
|
66
|
+
return Math.max(0, 1 - dist / maxLen)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Check if the issue describes an absence (something missing) */
|
|
70
|
+
function isAbsenceDescription(quote: string): boolean {
|
|
71
|
+
return /^(no |missing |absent |lacks? |without )/i.test(quote.trim())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class CitationVerifier {
|
|
75
|
+
/** Compute SHA-256 hash of artifact content for pipeline integrity */
|
|
76
|
+
static computeHash(content: string): string {
|
|
77
|
+
return createHash('sha256').update(content).digest('hex')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Check pipeline integrity: preflight hash vs current hash */
|
|
81
|
+
static checkIntegrity(preflightHash: string, currentHash: string): boolean {
|
|
82
|
+
return preflightHash === currentHash
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Verify a single issue's citation against the actual artifact content.
|
|
87
|
+
*
|
|
88
|
+
* @param issue - The issue with evidence_file/evidence_lines/evidence_quote
|
|
89
|
+
* @param artifact - The artifact string (input.output or input.evidence.diff)
|
|
90
|
+
* @param artifactFile - The logical file name of the artifact
|
|
91
|
+
* @param opts - Optional: absencePatterns for absence-mode verification
|
|
92
|
+
* @returns CitationVerdict with status and quality
|
|
93
|
+
*/
|
|
94
|
+
static verify(
|
|
95
|
+
issue: SealIssue,
|
|
96
|
+
artifact: string,
|
|
97
|
+
artifactFile: string,
|
|
98
|
+
opts?: { absencePatterns?: AbsencePattern[] },
|
|
99
|
+
): CitationVerdict {
|
|
100
|
+
// No evidence fields → not_applicable
|
|
101
|
+
if (!issue.evidence_file || !issue.evidence_lines || issue.evidence_lines.length === 0) {
|
|
102
|
+
return { status: 'not_applicable', quality: 'none', similarScore: 0, isFabricated: false }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// If the issue cites a different file, skip (we only check against the main artifact)
|
|
106
|
+
if (issue.evidence_file !== artifactFile) {
|
|
107
|
+
return { status: 'not_applicable', quality: 'none', similarScore: 0, isFabricated: false }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const lines = artifact.split('\n')
|
|
111
|
+
const [lineA] = issue.evidence_lines
|
|
112
|
+
|
|
113
|
+
// Phantom: line out of range
|
|
114
|
+
if (lineA < 1 || lineA > lines.length) {
|
|
115
|
+
return { status: 'phantom', quality: 'none', similarScore: 0, isFabricated: true, fabricatedReason: 'location_not_found' }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const lineContent = lines[lineA - 1] ?? ''
|
|
119
|
+
const quote = issue.evidence_quote ?? ''
|
|
120
|
+
|
|
121
|
+
// Absence mode: check if this looks like a "something is missing" description
|
|
122
|
+
if (opts?.absencePatterns && opts.absencePatterns.length > 0 && isAbsenceDescription(quote)) {
|
|
123
|
+
for (const pattern of opts.absencePatterns) {
|
|
124
|
+
const hasPositive = pattern.positive.test(artifact)
|
|
125
|
+
const hasNegated = pattern.negated.test(artifact)
|
|
126
|
+
|
|
127
|
+
if (hasPositive && !hasNegated) {
|
|
128
|
+
// Absence confirmed: positive anchor present, negated token absent
|
|
129
|
+
return { status: 'verified', quality: 'strong', similarScore: 1, isFabricated: false }
|
|
130
|
+
}
|
|
131
|
+
if (hasPositive && hasNegated) {
|
|
132
|
+
// Absence NOT confirmed: negated token IS present
|
|
133
|
+
// This means the detector was wrong about the absence
|
|
134
|
+
return { status: 'void', quality: 'none', similarScore: 0, isFabricated: true, fabricatedReason: 'content_mismatch' }
|
|
135
|
+
}
|
|
136
|
+
// No positive anchor → can't confirm, fall through to literal mode
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Literal excerpt mode: fuzzy-match the quote against the line content
|
|
141
|
+
const score = fuzzyMatch(quote, lineContent)
|
|
142
|
+
|
|
143
|
+
if (score >= SIMILARITY_STRONG) {
|
|
144
|
+
return { status: 'verified', quality: 'strong', similarScore: score, isFabricated: false }
|
|
145
|
+
}
|
|
146
|
+
if (score >= SIMILARITY_WEAK) {
|
|
147
|
+
return { status: 'drifted', quality: 'weak', similarScore: score, isFabricated: false }
|
|
148
|
+
}
|
|
149
|
+
return { status: 'void', quality: 'none', similarScore: score, isFabricated: true, fabricatedReason: 'content_mismatch' }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Verify multiple issues and annotate them with citation_status.
|
|
154
|
+
* Returns fabricated_evidence entries for any that failed verification.
|
|
155
|
+
*/
|
|
156
|
+
static verifyIssues(
|
|
157
|
+
issues: SealIssue[],
|
|
158
|
+
artifact: string,
|
|
159
|
+
artifactFile: string,
|
|
160
|
+
opts?: { absencePatterns?: AbsencePattern[] },
|
|
161
|
+
): { issues: SealIssue[]; fabricated: FabricatedEvidenceEntry[] } {
|
|
162
|
+
const fabricated: FabricatedEvidenceEntry[] = []
|
|
163
|
+
|
|
164
|
+
const annotated = issues.map(issue => {
|
|
165
|
+
const verdict = this.verify(issue, artifact, artifactFile, opts)
|
|
166
|
+
// Annotate the issue
|
|
167
|
+
const updated = { ...issue, citation_status: verdict.status }
|
|
168
|
+
|
|
169
|
+
if (verdict.isFabricated) {
|
|
170
|
+
fabricated.push({
|
|
171
|
+
type: 'FABRICATED_EVIDENCE',
|
|
172
|
+
severity: verdict.status === 'phantom' ? 'CRITICAL' : 'HIGH',
|
|
173
|
+
is_blocking: false,
|
|
174
|
+
evidence: `Detector claimed citation at ${issue.evidence_file}:${issue.evidence_lines?.[0]} but it was ${verdict.fabricatedReason}`,
|
|
175
|
+
layer: 'L3',
|
|
176
|
+
source: 'core',
|
|
177
|
+
citation_status: verdict.status,
|
|
178
|
+
fabricated_reason: verdict.fabricatedReason!,
|
|
179
|
+
claimed_evidence: {
|
|
180
|
+
file: issue.evidence_file,
|
|
181
|
+
lines: issue.evidence_lines,
|
|
182
|
+
quote: issue.evidence_quote,
|
|
183
|
+
},
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return updated
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return { issues: annotated, fabricated }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface FabricatedEvidenceEntry {
|
|
195
|
+
type: 'FABRICATED_EVIDENCE'
|
|
196
|
+
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
|
|
197
|
+
is_blocking: boolean
|
|
198
|
+
evidence: string
|
|
199
|
+
layer: IssueLayer
|
|
200
|
+
source: 'core' | 'llm-overlay' | 'extension'
|
|
201
|
+
citation_status: CitationStatus
|
|
202
|
+
fabricated_reason: 'location_not_found' | 'content_mismatch'
|
|
203
|
+
claimed_evidence: {
|
|
204
|
+
file?: string
|
|
205
|
+
lines?: number[]
|
|
206
|
+
quote?: string
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
type IssueLayer = 'L1' | 'L2' | 'L3' | 'L4' | 'EXTENSION' | 'LLM_OVERLAY'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SealExtension, SealInput, SealIssue, makeIssue } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export interface ExtensionResult {
|
|
4
|
+
issues: SealIssue[]
|
|
5
|
+
errors: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class ExtensionRegistry {
|
|
9
|
+
private extensions: SealExtension[] = []
|
|
10
|
+
|
|
11
|
+
add(ext: SealExtension): void {
|
|
12
|
+
this.extensions.push(ext)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
run(input: SealInput): ExtensionResult {
|
|
16
|
+
const issues: SealIssue[] = []
|
|
17
|
+
const errors: string[] = []
|
|
18
|
+
|
|
19
|
+
for (const ext of this.extensions) {
|
|
20
|
+
try {
|
|
21
|
+
const result = ext.check(input)
|
|
22
|
+
issues.push(...result)
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const msg = `Extension [${ext.name}] failed: ${err instanceof Error ? err.message : String(err)}`
|
|
25
|
+
errors.push(msg)
|
|
26
|
+
issues.push(makeIssue({
|
|
27
|
+
type: 'OTHER', severity: 'LOW', layer: 'EXTENSION', source: 'extension',
|
|
28
|
+
evidence: msg,
|
|
29
|
+
}))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { issues, errors }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SealIssue, Verdict, verdictFromScore } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export class HeuristicScorer {
|
|
4
|
+
static computeDetectorScore(findings: Array<{ trust_deduction?: number }>): number {
|
|
5
|
+
const total = findings.reduce((sum, f) => sum + (f.trust_deduction ?? 0), 0)
|
|
6
|
+
return Math.max(0, Math.min(100, Math.floor(100 - total)))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
static computeFinalScore(detectorScore: number, policyDeductions: number): number {
|
|
10
|
+
return Math.max(0, Math.min(100, Math.floor(detectorScore - policyDeductions)))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
static toVerdict(score: number, blockingIssues: SealIssue[]): Verdict {
|
|
14
|
+
return verdictFromScore(score, blockingIssues.length > 0)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SealInput, ArtifactType, RiskLevel } from '../types.js'
|
|
2
|
+
import { SealInputError } from '../errors.js'
|
|
3
|
+
|
|
4
|
+
const VALID_ARTIFACT_TYPES: ArtifactType[] = ['llm_response', 'code_diff', 'test_plan', 'design', 'migration', 'plan_review']
|
|
5
|
+
const VALID_RISK_LEVELS: RiskLevel[] = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
6
|
+
|
|
7
|
+
export class InputNormalizer {
|
|
8
|
+
static normalize(input: Partial<SealInput>): SealInput {
|
|
9
|
+
if (!input.output || input.output.trim() === '') {
|
|
10
|
+
throw new SealInputError('output is required and must be non-empty', 'output')
|
|
11
|
+
}
|
|
12
|
+
if (!input.artifact_type || !VALID_ARTIFACT_TYPES.includes(input.artifact_type)) {
|
|
13
|
+
throw new SealInputError(`artifact_type must be one of: ${VALID_ARTIFACT_TYPES.join(', ')}`, 'artifact_type')
|
|
14
|
+
}
|
|
15
|
+
if (input.risk_hint !== null && input.risk_hint !== undefined && !VALID_RISK_LEVELS.includes(input.risk_hint)) {
|
|
16
|
+
throw new SealInputError(`risk_hint must be one of: ${VALID_RISK_LEVELS.join(', ')} or null`, 'risk_hint')
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
artifact_type: input.artifact_type,
|
|
20
|
+
spec: input.spec ?? null,
|
|
21
|
+
output: input.output.trim(),
|
|
22
|
+
evidence: {
|
|
23
|
+
test_log: input.evidence?.test_log ?? '',
|
|
24
|
+
build_log: input.evidence?.build_log ?? '',
|
|
25
|
+
diff: input.evidence?.diff ?? '',
|
|
26
|
+
references: input.evidence?.references ?? [],
|
|
27
|
+
},
|
|
28
|
+
risk_hint: input.risk_hint ?? null,
|
|
29
|
+
context: input.context,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plan_review mode pipeline — gates plans, designs, proposals, spec seeds.
|
|
3
|
+
*
|
|
4
|
+
* Core principle: no runtime exists, so behavioral claims are out of scope.
|
|
5
|
+
* The gateable question is "is this coherent and complete against what was
|
|
6
|
+
* locked before it was written?" Falsification, not approval.
|
|
7
|
+
*
|
|
8
|
+
* Pass threshold: 90 (higher than code mode's 85)
|
|
9
|
+
* High-risk [71,89]: ESCALATE_TO_HUMAN instead of PASS_WITH_WARNINGS
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
SealInput, SealVerdict, SealIssue, Verdict,
|
|
14
|
+
gradeEvidence, maxVerdict, worstOf, makeIssue, SCHEMA_VERSION, NEXT_ACTION
|
|
15
|
+
} from '../types.js'
|
|
16
|
+
import type { ScoreBreakdown, FabricatedEvidence } from '../types.js'
|
|
17
|
+
import { ClaimExtractor } from '../detectors/claim-extractor.js'
|
|
18
|
+
import { RiskClassifier } from '../detectors/risk-classifier.js'
|
|
19
|
+
import { SpecCoverageDetector } from '../detectors/spec-coverage-detector.js'
|
|
20
|
+
import { ContradictionDetector } from '../detectors/contradiction-detector.js'
|
|
21
|
+
import { CriteriaCoverageDetector } from '../detectors/criteria-coverage-detector.js'
|
|
22
|
+
import { OrphanClaimDetector } from '../detectors/orphan-claim-detector.js'
|
|
23
|
+
import { FalsificationDetector } from '../detectors/falsification-detector.js'
|
|
24
|
+
import { HeuristicScorer } from './heuristic-scorer.js'
|
|
25
|
+
import { PolicyEngine } from './policy-engine.js'
|
|
26
|
+
import { ExtensionRegistry } from './extension-registry.js'
|
|
27
|
+
import { TrustMemory } from '../trust-memory.js'
|
|
28
|
+
|
|
29
|
+
const PLAN_PASS_THRESHOLD = 90
|
|
30
|
+
const PLAN_ESCALATE_BAND_LOW = 71
|
|
31
|
+
const PLAN_ESCALATE_BAND_HIGH = 89
|
|
32
|
+
|
|
33
|
+
function planReviewVerdict(score: number, hasBlocking: boolean, riskLevel: string): Verdict {
|
|
34
|
+
if (hasBlocking) return 'BLOCK'
|
|
35
|
+
|
|
36
|
+
// High-risk middle band → ESCALATE instead of PASS_WITH_WARNINGS
|
|
37
|
+
if (score >= PLAN_ESCALATE_BAND_LOW && score <= PLAN_ESCALATE_BAND_HIGH && riskLevel === 'HIGH') {
|
|
38
|
+
return 'ESCALATE_TO_HUMAN'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (score >= PLAN_PASS_THRESHOLD) return 'PASS'
|
|
42
|
+
if (score >= PLAN_ESCALATE_BAND_LOW) return 'PASS_WITH_WARNINGS'
|
|
43
|
+
if (score >= 50) return 'REVISE'
|
|
44
|
+
if (score >= 30) return 'ESCALATE_TO_HUMAN'
|
|
45
|
+
return 'BLOCK'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function evidenceBonus(issues: SealIssue[]): number {
|
|
49
|
+
// +5 per confirmed-strong field (from CriteriaCoverageDetector), capped at +15
|
|
50
|
+
const strongCount = issues
|
|
51
|
+
.filter(i => gradeEvidence(i) === 'strong' && i.type !== 'LOGIC_BUG')
|
|
52
|
+
.length
|
|
53
|
+
return Math.min(strongCount * 5, 15)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runPlanReviewPipeline(
|
|
57
|
+
input: SealInput,
|
|
58
|
+
registry: ExtensionRegistry,
|
|
59
|
+
trustMemory: TrustMemory | null,
|
|
60
|
+
): Promise<SealVerdict> {
|
|
61
|
+
const artifactFile = (input.context?.artifact_file as string) ?? 'artifact.md'
|
|
62
|
+
|
|
63
|
+
// Step 1: Extract claims — behavioral claim patterns are designed for code artifacts,
|
|
64
|
+
// not prose (words like "fixed", "fully" in specs are noise, not claims needing evidence).
|
|
65
|
+
// OrphanClaimDetector in plan_review mode checks traceability anchors, not behavioral claims.
|
|
66
|
+
const claims = input.artifact_type === 'plan_review' ? [] : ClaimExtractor.extract(input.output)
|
|
67
|
+
|
|
68
|
+
// Step 2: Classify risk
|
|
69
|
+
const riskResult = RiskClassifier.classify({
|
|
70
|
+
output: input.output,
|
|
71
|
+
diff: input.evidence?.diff ?? '',
|
|
72
|
+
risk_hint: input.risk_hint,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// Step 3: Spec coverage (reused from code mode)
|
|
76
|
+
const specResult = SpecCoverageDetector.detect(input.spec, input.output)
|
|
77
|
+
const assumptions_detected = [...specResult.assumptions]
|
|
78
|
+
|
|
79
|
+
// Stage 1 — ContradictionDetector (inline, keystone)
|
|
80
|
+
const contradictionResult = ContradictionDetector.detect({
|
|
81
|
+
artifact: input.output,
|
|
82
|
+
artifact_file: artifactFile,
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// Stage 2 — CriteriaCoverageDetector (inline)
|
|
86
|
+
const lockedCriteria = input.context?.locked_criteria ?? []
|
|
87
|
+
const coverageResult = CriteriaCoverageDetector.detect({
|
|
88
|
+
artifact: input.output,
|
|
89
|
+
artifact_file: artifactFile,
|
|
90
|
+
locked_criteria: lockedCriteria as Array<{ id: string; text: string; source_file?: string; source_line?: number }>,
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
// Preflight failure: no locked criteria
|
|
94
|
+
if (coverageResult.no_criteria_escalate) {
|
|
95
|
+
assumptions_detected.push('No locked criteria — coverage measurement not possible')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Stage 3 — OrphanClaimDetector (inline)
|
|
99
|
+
const orphanResult = OrphanClaimDetector.detect(claims, input.output)
|
|
100
|
+
|
|
101
|
+
// All deterministic findings
|
|
102
|
+
const deterministicFindings: SealIssue[] = [
|
|
103
|
+
...contradictionResult.issues,
|
|
104
|
+
...coverageResult.issues,
|
|
105
|
+
...orphanResult.issues,
|
|
106
|
+
...specResult.issues,
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
// Evidence bonus from covered criteria (confirmed-strong fields)
|
|
110
|
+
const criteriaBonus = Math.min(coverageResult.confirmed_strong_fields * 5, 15)
|
|
111
|
+
|
|
112
|
+
// Stage 4 — FalsificationDetector (parallel agent, async)
|
|
113
|
+
const falsificationIssues = await FalsificationDetector.detect(input)
|
|
114
|
+
|
|
115
|
+
// Score computation (start 100 + bonus, deduct per issue)
|
|
116
|
+
const allDeductions = deterministicFindings.filter(f => (f.trust_deduction ?? 0) > 0)
|
|
117
|
+
const falsificationDeductions = falsificationIssues
|
|
118
|
+
.filter(f => (f.trust_deduction ?? 0) > 0)
|
|
119
|
+
.reduce((sum, f) => sum + (f.trust_deduction ?? 0), 0)
|
|
120
|
+
const baseScore = HeuristicScorer.computeDetectorScore(allDeductions)
|
|
121
|
+
const detectorScore = Math.max(0, Math.min(100, baseScore + criteriaBonus - falsificationDeductions))
|
|
122
|
+
|
|
123
|
+
// Policy engine (reused)
|
|
124
|
+
const hasBlocking = deterministicFindings.some(f => f.is_blocking)
|
|
125
|
+
const baseVerdict = planReviewVerdict(detectorScore, hasBlocking, riskResult.risk_level)
|
|
126
|
+
|
|
127
|
+
const allForPolicy = [...deterministicFindings, ...falsificationIssues]
|
|
128
|
+
const policyResult = PolicyEngine.apply({
|
|
129
|
+
base_verdict: baseVerdict,
|
|
130
|
+
detector_score: detectorScore,
|
|
131
|
+
risk_level: riskResult.risk_level,
|
|
132
|
+
input,
|
|
133
|
+
all_findings: allForPolicy,
|
|
134
|
+
llm_signals: null,
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Extensions
|
|
138
|
+
const extResult = registry.run(input)
|
|
139
|
+
|
|
140
|
+
// Combine all issues
|
|
141
|
+
const allIssues = [...deterministicFindings, ...policyResult.injected_issues, ...falsificationIssues]
|
|
142
|
+
|
|
143
|
+
// Final score
|
|
144
|
+
const finalScore = HeuristicScorer.computeFinalScore(detectorScore, policyResult.policy_deductions)
|
|
145
|
+
|
|
146
|
+
// Honor required_verdict on all findings (including non-blocking)
|
|
147
|
+
const requiredVerdicts = allForPolicy
|
|
148
|
+
.map(i => i.required_verdict)
|
|
149
|
+
.filter((v): v is Verdict => !!v)
|
|
150
|
+
|
|
151
|
+
// Final verdict
|
|
152
|
+
let finalVerdict = worstOf(
|
|
153
|
+
policyResult.verdict,
|
|
154
|
+
planReviewVerdict(finalScore, allIssues.some(i => i.is_blocking), riskResult.risk_level),
|
|
155
|
+
)
|
|
156
|
+
for (const rv of requiredVerdicts) finalVerdict = worstOf(finalVerdict, rv)
|
|
157
|
+
|
|
158
|
+
// Preflight escalation overrides
|
|
159
|
+
if (coverageResult.no_criteria_escalate) {
|
|
160
|
+
finalVerdict = worstOf(finalVerdict, 'ESCALATE_TO_HUMAN')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Extension block check
|
|
164
|
+
if (extResult.issues.some(i => i.is_blocking && i.required_verdict === 'BLOCK')) {
|
|
165
|
+
finalVerdict = 'BLOCK'
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const allIssuesFull = [...allIssues, ...extResult.issues]
|
|
169
|
+
const blocking = allIssuesFull.filter(i => i.is_blocking)
|
|
170
|
+
const nonBlocking = allIssuesFull.filter(i => !i.is_blocking)
|
|
171
|
+
|
|
172
|
+
// TrustMemory
|
|
173
|
+
const agentId = input.context?.agent_id
|
|
174
|
+
if (trustMemory && agentId) {
|
|
175
|
+
trustMemory.record(agentId, {
|
|
176
|
+
verdict: finalVerdict,
|
|
177
|
+
trust_score: finalScore,
|
|
178
|
+
risk_level: riskResult.risk_level,
|
|
179
|
+
blocking_count: blocking.length,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
verdict: finalVerdict,
|
|
185
|
+
trust_score: finalScore,
|
|
186
|
+
risk_level: riskResult.risk_level,
|
|
187
|
+
summary: `plan_review: ${finalVerdict} (trust=${finalScore}, risk=${riskResult.risk_level}): ${blocking.length} blocking, ${nonBlocking.length} non-blocking`,
|
|
188
|
+
deterministic_findings: allIssuesFull,
|
|
189
|
+
llm_findings: falsificationIssues,
|
|
190
|
+
blocking_issues: blocking,
|
|
191
|
+
non_blocking_issues: nonBlocking,
|
|
192
|
+
missing_evidence: [],
|
|
193
|
+
assumptions_detected,
|
|
194
|
+
advisory_notes: [],
|
|
195
|
+
next_action: NEXT_ACTION[finalVerdict],
|
|
196
|
+
schema_version: SCHEMA_VERSION,
|
|
197
|
+
score_breakdown: {
|
|
198
|
+
base: 100,
|
|
199
|
+
issue_deductions: blocking.reduce((s, i) => s + (i.trust_deduction ?? 0), 0) + nonBlocking.reduce((s, i) => s + (i.trust_deduction ?? 0), 0),
|
|
200
|
+
missing_evidence_deductions: 0,
|
|
201
|
+
risk_deductions: riskResult.trust_deduction,
|
|
202
|
+
overconfidence_deductions: 0,
|
|
203
|
+
evidence_bonuses: 0,
|
|
204
|
+
final: finalScore,
|
|
205
|
+
},
|
|
206
|
+
fabricated_evidence: [],
|
|
207
|
+
trust_memory_summary: (trustMemory && agentId) ? trustMemory.getSummary(agentId) : undefined,
|
|
208
|
+
}
|
|
209
|
+
}
|