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,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenericOpenAIProvider — config-driven LLM reviewer.
|
|
3
|
+
* Reads provider config from ~/.anima/providers.d/<name>.yaml (same format ANIMA uses).
|
|
4
|
+
* Resolves ${ENV_VAR} in apiKey automatically.
|
|
5
|
+
* Auto-appends /chat/completions to baseUrl.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* Seal.withLLM(createReviewerFromProvider('minimax'))
|
|
9
|
+
* Seal.withLLM(createReviewerFromProvider('xiaomi'))
|
|
10
|
+
* Seal.withLLM(createReviewerFromProvider('gemini'))
|
|
11
|
+
* Seal.withLLM(new GenericOpenAIProvider({
|
|
12
|
+
* baseUrl: 'https://api.custom.com/v1',
|
|
13
|
+
* model: 'my-model',
|
|
14
|
+
* apiKey: 'sk-...',
|
|
15
|
+
* }))
|
|
16
|
+
*/
|
|
17
|
+
import { LLMReviewerAdapter, LLMSignals, SealInput, PartialVerdict } from '../types.js'
|
|
18
|
+
import { readFileSync } from 'node:fs'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
export interface ProviderConfig {
|
|
23
|
+
baseUrl: string
|
|
24
|
+
model: string
|
|
25
|
+
apiKey: string
|
|
26
|
+
timeoutMs?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SYSTEM_PROMPT = `You are Seal, a quality gate in an AI engineering workflow.
|
|
30
|
+
|
|
31
|
+
Your job is NOT to be polite or creative. Detect semantic issues the deterministic rules could not catch.
|
|
32
|
+
|
|
33
|
+
Review AI agent output for semantic correctness, subtle logic bugs, and missing requirements.
|
|
34
|
+
The deterministic layer already checked: evidence presence, risk keywords, spec headers, test logs.
|
|
35
|
+
Your focus: semantic reasoning, prose logic, subtle edge cases, spec compliance.
|
|
36
|
+
|
|
37
|
+
Return ONLY valid JSON:
|
|
38
|
+
{
|
|
39
|
+
"suspected_issues": ["string — exact reasoning error, max 3"],
|
|
40
|
+
"missing_requirements": ["string — spec requirement clearly absent"],
|
|
41
|
+
"possible_edge_cases": ["string — edge cases to consider"],
|
|
42
|
+
"evidence_gaps": ["string — claims needing evidence that slipped through"],
|
|
43
|
+
"risk_guess": "LOW | MEDIUM | HIGH | CRITICAL",
|
|
44
|
+
"confidence": 0.0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
confidence: 0.0–1.0. Empty arrays if output is correct. No padding.`
|
|
48
|
+
|
|
49
|
+
function buildMessage(input: SealInput, partial: PartialVerdict): string {
|
|
50
|
+
const parts: string[] = []
|
|
51
|
+
if (input.spec) parts.push(`=== SPEC ===\n${input.spec}`)
|
|
52
|
+
parts.push(`=== OUTPUT (${input.artifact_type}) ===\n${input.output}`)
|
|
53
|
+
const blocking = partial.deterministic_findings.filter(f => f.is_blocking)
|
|
54
|
+
if (blocking.length) {
|
|
55
|
+
parts.push(`=== ALREADY FLAGGED ===\n${blocking.map(f => `- [${f.rule_id ?? f.type}] ${f.evidence}`).join('\n')}`)
|
|
56
|
+
}
|
|
57
|
+
parts.push(`=== PARTIAL VERDICT ===\ntrust_score: ${partial.trust_score}, risk: ${partial.risk_level}`)
|
|
58
|
+
return parts.join('\n\n')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extractJSON(raw: string): string {
|
|
62
|
+
const stripped = raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim()
|
|
63
|
+
const m = stripped.match(/\{[\s\S]*\}/)
|
|
64
|
+
return m ? m[0] : stripped
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveEnvRef(value: string): string {
|
|
68
|
+
// Resolve ${VAR_NAME} pattern
|
|
69
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] ?? '')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Load and parse a provider YAML from ~/.anima/providers.d/<name>.yaml */
|
|
73
|
+
export function loadProviderConfig(name: string, modelIndex = 0): ProviderConfig {
|
|
74
|
+
const path = join(homedir(), '.anima', 'providers.d', `${name}.yaml`)
|
|
75
|
+
let raw: string
|
|
76
|
+
try {
|
|
77
|
+
raw = readFileSync(path, 'utf-8')
|
|
78
|
+
} catch {
|
|
79
|
+
throw new Error(`Provider config not found: ${path}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Minimal YAML parser — handles the providers.d format
|
|
83
|
+
const get = (key: string) => {
|
|
84
|
+
const m = raw.match(new RegExp(`^${key}:\\s*(.+)$`, 'm'))
|
|
85
|
+
return m ? m[1].trim().replace(/^["']|["']$/g, '') : ''
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const baseUrl = get('baseUrl')
|
|
89
|
+
const rawKey = get('apiKey')
|
|
90
|
+
const apiKey = resolveEnvRef(rawKey)
|
|
91
|
+
|
|
92
|
+
// Extract models list (lines like " - id: MiniMax-M3")
|
|
93
|
+
const models = [...raw.matchAll(/^\s+-\s+id:\s+(.+)$/gm)].map(m => m[1].trim())
|
|
94
|
+
const model = models[modelIndex] ?? models[0] ?? 'default'
|
|
95
|
+
|
|
96
|
+
if (!baseUrl) throw new Error(`Provider ${name}: baseUrl missing`)
|
|
97
|
+
|
|
98
|
+
return { baseUrl, model, apiKey }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class GenericOpenAIProvider implements LLMReviewerAdapter {
|
|
102
|
+
private config: Required<ProviderConfig>
|
|
103
|
+
|
|
104
|
+
constructor(config: ProviderConfig) {
|
|
105
|
+
this.config = { timeoutMs: 30_000, ...config }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async review(input: SealInput, partial: PartialVerdict): Promise<LLMSignals> {
|
|
109
|
+
const { baseUrl, model, apiKey, timeoutMs } = this.config
|
|
110
|
+
const endpoint = baseUrl.endsWith('/chat/completions')
|
|
111
|
+
? baseUrl
|
|
112
|
+
: `${baseUrl.replace(/\/$/, '')}/chat/completions`
|
|
113
|
+
|
|
114
|
+
const controller = new AbortController()
|
|
115
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs)
|
|
116
|
+
|
|
117
|
+
let response: Response
|
|
118
|
+
try {
|
|
119
|
+
response = await fetch(endpoint, {
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: {
|
|
123
|
+
'Content-Type': 'application/json',
|
|
124
|
+
'Authorization': apiKey ? `Bearer ${apiKey}` : 'Bearer none',
|
|
125
|
+
},
|
|
126
|
+
body: JSON.stringify({
|
|
127
|
+
model,
|
|
128
|
+
messages: [
|
|
129
|
+
{ role: 'system', content: SYSTEM_PROMPT },
|
|
130
|
+
{ role: 'user', content: buildMessage(input, partial) },
|
|
131
|
+
],
|
|
132
|
+
response_format: { type: 'json_object' },
|
|
133
|
+
temperature: 0.1,
|
|
134
|
+
max_tokens: 1024,
|
|
135
|
+
}),
|
|
136
|
+
})
|
|
137
|
+
} finally {
|
|
138
|
+
clearTimeout(timer)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!response.ok) {
|
|
142
|
+
const body = await response.text().catch(() => '')
|
|
143
|
+
throw new Error(`[${model}] ${response.status}: ${body.slice(0, 200)}`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const data = await response.json() as { choices: Array<{ message: { content: string } }> }
|
|
147
|
+
const raw = data.choices?.[0]?.message?.content ?? '{}'
|
|
148
|
+
|
|
149
|
+
let signals: Partial<LLMSignals>
|
|
150
|
+
try { signals = JSON.parse(extractJSON(raw)) }
|
|
151
|
+
catch { throw new Error(`[${model}] invalid JSON: ${raw.slice(0, 200)}`) }
|
|
152
|
+
|
|
153
|
+
const VALID_RISKS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
|
|
154
|
+
return {
|
|
155
|
+
suspected_issues: Array.isArray(signals.suspected_issues) ? signals.suspected_issues : [],
|
|
156
|
+
missing_requirements: Array.isArray(signals.missing_requirements) ? signals.missing_requirements : [],
|
|
157
|
+
possible_edge_cases: Array.isArray(signals.possible_edge_cases) ? signals.possible_edge_cases : [],
|
|
158
|
+
evidence_gaps: Array.isArray(signals.evidence_gaps) ? signals.evidence_gaps : [],
|
|
159
|
+
risk_guess: VALID_RISKS.includes(signals.risk_guess ?? '') ? signals.risk_guess as LLMSignals['risk_guess'] : 'MEDIUM',
|
|
160
|
+
confidence: typeof signals.confidence === 'number' ? Math.max(0, Math.min(1, signals.confidence)) : 0.5,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create a reviewer from ~/.anima/providers.d/<name>.yaml
|
|
167
|
+
* modelIndex: which model from the models list (default 0 = first/best)
|
|
168
|
+
*/
|
|
169
|
+
export function createReviewerFromProvider(name: string, modelIndex = 0): GenericOpenAIProvider {
|
|
170
|
+
return new GenericOpenAIProvider(loadProviderConfig(name, modelIndex))
|
|
171
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Seal } from './index.js'
|
|
3
|
+
import { ArtifactType, RiskLevel } from './types.js'
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
5
|
+
import { resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const BOLD = '\x1b[1m'
|
|
8
|
+
const DIM = '\x1b[2m'
|
|
9
|
+
const RED = '\x1b[31m'
|
|
10
|
+
const GREEN = '\x1b[32m'
|
|
11
|
+
const YELLOW = '\x1b[33m'
|
|
12
|
+
const CYAN = '\x1b[36m'
|
|
13
|
+
const RESET = '\x1b[0m'
|
|
14
|
+
|
|
15
|
+
function colorForVerdict(v: string): string {
|
|
16
|
+
if (v === 'PASS') return GREEN
|
|
17
|
+
if (v === 'PASS_WITH_WARNINGS') return YELLOW
|
|
18
|
+
return RED
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function usage() {
|
|
22
|
+
console.log(`
|
|
23
|
+
${BOLD}seal${RESET} — AI quality gate
|
|
24
|
+
|
|
25
|
+
${BOLD}USAGE${RESET}
|
|
26
|
+
seal <command> [options]
|
|
27
|
+
|
|
28
|
+
${BOLD}COMMANDS${RESET}
|
|
29
|
+
${CYAN}code${RESET} <file> Review code output / diff (default mode)
|
|
30
|
+
${CYAN}plan${RESET} <file> Review plan, design, or spec (plan_review mode)
|
|
31
|
+
${CYAN}review${RESET} <file> Review with full control over artifact type
|
|
32
|
+
|
|
33
|
+
${BOLD}OPTIONS${RESET}
|
|
34
|
+
--spec <file> Spec file for compliance check
|
|
35
|
+
--locked-criteria <file> JSON file with locked acceptance criteria (plan_review)
|
|
36
|
+
--risk <level> Risk hint: low, medium, high, critical
|
|
37
|
+
--agent-id <id> Agent identifier for trust memory
|
|
38
|
+
--json Raw JSON output (no formatting)
|
|
39
|
+
--artifact-type <type> Override: llm_response, code_diff, test_plan, design, migration, plan_review
|
|
40
|
+
|
|
41
|
+
${BOLD}EXAMPLES${RESET}
|
|
42
|
+
${DIM}# Review a code change${RESET}
|
|
43
|
+
seal code src/auth.ts --spec specs/auth.md
|
|
44
|
+
|
|
45
|
+
${DIM}# Review a plan/spec against locked criteria${RESET}
|
|
46
|
+
seal plan spec.md --locked-criteria intent-criteria.json
|
|
47
|
+
|
|
48
|
+
${DIM}# Full control mode${RESET}
|
|
49
|
+
seal review output.md --artifact-type plan_review --locked-criteria criteria.json
|
|
50
|
+
`)
|
|
51
|
+
process.exit(2)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args: string[]) {
|
|
55
|
+
const parsed: Record<string, string | boolean> = {}
|
|
56
|
+
let positional: string[] = []
|
|
57
|
+
for (let i = 0; i < args.length; i++) {
|
|
58
|
+
if (args[i].startsWith('--')) {
|
|
59
|
+
const key = args[i].slice(2)
|
|
60
|
+
if (['json'].includes(key)) { parsed[key] = true; continue }
|
|
61
|
+
parsed[key] = args[++i] ?? ''
|
|
62
|
+
} else {
|
|
63
|
+
positional.push(args[i])
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { positional, parsed }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function formatVerdict(v: any, json: boolean) {
|
|
70
|
+
if (json) {
|
|
71
|
+
console.log(JSON.stringify(v, null, 2))
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const c = colorForVerdict(v.verdict)
|
|
76
|
+
const scoreBar = v.trust_score >= 90 ? '██████████' :
|
|
77
|
+
v.trust_score >= 70 ? '███████░░░' :
|
|
78
|
+
v.trust_score >= 50 ? '█████░░░░░' :
|
|
79
|
+
v.trust_score >= 30 ? '███░░░░░░░' : '█░░░░░░░░░'
|
|
80
|
+
|
|
81
|
+
console.log(`\n${BOLD}┌─ Seal Verdict ──────────────────────────────┐${RESET}`)
|
|
82
|
+
console.log(`${BOLD}│${RESET} ${c}${BOLD}${v.verdict}${RESET} ${DIM}(trust: ${v.trust_score})${RESET} ${scoreBar}`)
|
|
83
|
+
console.log(`${BOLD}│${RESET} Risk: ${v.risk_level}`)
|
|
84
|
+
console.log(`${BOLD}│${RESET} ${v.summary}`)
|
|
85
|
+
|
|
86
|
+
if (v.blocking_issues?.length > 0) {
|
|
87
|
+
console.log(`${BOLD}│${RESET}`)
|
|
88
|
+
console.log(`${BOLD}│${RESET} ${RED}${BOLD}Blocking Issues:${RESET}`)
|
|
89
|
+
for (const issue of v.blocking_issues) {
|
|
90
|
+
console.log(`${BOLD}│${RESET} ${RED}✗${RESET} [${issue.rule_id}] ${issue.evidence}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (v.non_blocking_issues?.length > 0) {
|
|
95
|
+
console.log(`${BOLD}│${RESET}`)
|
|
96
|
+
console.log(`${BOLD}│${RESET} ${YELLOW}Non-blocking:${RESET}`)
|
|
97
|
+
for (const issue of v.non_blocking_issues) {
|
|
98
|
+
console.log(`${BOLD}│${RESET} ${YELLOW}⚠${RESET} [${issue.rule_id}] ${issue.evidence}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (v.deterministic_findings?.length === 0 && v.blocking_issues?.length === 0) {
|
|
103
|
+
console.log(`${BOLD}│${RESET}`)
|
|
104
|
+
console.log(`${BOLD}│${RESET} ${GREEN}✓${RESET} No issues found`)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`${BOLD}│${RESET} ${DIM}Next: ${v.next_action}${RESET}`)
|
|
108
|
+
console.log(`${BOLD}└────────────────────────────────────────────┘${RESET}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function run(args: string[]) {
|
|
112
|
+
const { positional, parsed } = parseArgs(args)
|
|
113
|
+
const cmd = positional[0]
|
|
114
|
+
|
|
115
|
+
if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') usage()
|
|
116
|
+
|
|
117
|
+
let artifactFile: string | null = null
|
|
118
|
+
let artifactType: ArtifactType = 'llm_response'
|
|
119
|
+
let specFile: string | null = null
|
|
120
|
+
let lockedCriteriaFile: string | null = null
|
|
121
|
+
let riskHint: RiskLevel | null = null
|
|
122
|
+
let agentId: string | null = null
|
|
123
|
+
let jsonOutput = !!parsed['json']
|
|
124
|
+
|
|
125
|
+
switch (cmd) {
|
|
126
|
+
case 'code':
|
|
127
|
+
artifactFile = positional[1]
|
|
128
|
+
artifactType = 'code_diff'
|
|
129
|
+
break
|
|
130
|
+
case 'plan':
|
|
131
|
+
case 'spec':
|
|
132
|
+
case 'design':
|
|
133
|
+
artifactFile = positional[1]
|
|
134
|
+
artifactType = 'plan_review'
|
|
135
|
+
break
|
|
136
|
+
case 'review':
|
|
137
|
+
artifactFile = positional[1]
|
|
138
|
+
artifactType = (parsed['artifact-type'] as ArtifactType) ?? 'llm_response'
|
|
139
|
+
break
|
|
140
|
+
default:
|
|
141
|
+
// `seal <file>` shorthand — treat first arg as file
|
|
142
|
+
if (existsSync(cmd)) {
|
|
143
|
+
artifactFile = cmd
|
|
144
|
+
artifactType = 'llm_response'
|
|
145
|
+
} else {
|
|
146
|
+
console.error(`Unknown command or file not found: ${cmd}`)
|
|
147
|
+
usage()
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!artifactFile) { console.error('Error: no file specified'); usage() }
|
|
152
|
+
|
|
153
|
+
const filePath = artifactFile as string
|
|
154
|
+
if (!existsSync(filePath)) {
|
|
155
|
+
console.error(`Error: file not found: ${filePath}`)
|
|
156
|
+
process.exit(2)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Resolve optional flags
|
|
160
|
+
if (parsed['spec']) { specFile = parsed['spec'] as string }
|
|
161
|
+
if (parsed['locked-criteria']) { lockedCriteriaFile = parsed['locked-criteria'] as string }
|
|
162
|
+
if (parsed['risk']) { riskHint = parsed['risk'] as RiskLevel }
|
|
163
|
+
if (parsed['agent-id']) { agentId = parsed['agent-id'] as string }
|
|
164
|
+
// Load files
|
|
165
|
+
const output = readFileSync(resolve(filePath), 'utf-8')
|
|
166
|
+
const spec = specFile && existsSync(specFile) ? readFileSync(resolve(specFile), 'utf-8') : null
|
|
167
|
+
const evidence = { test_log: '', build_log: '', diff: '', references: [] }
|
|
168
|
+
|
|
169
|
+
const lockedCriteria = lockedCriteriaFile && existsSync(lockedCriteriaFile)
|
|
170
|
+
? JSON.parse(readFileSync(resolve(lockedCriteriaFile), 'utf-8'))
|
|
171
|
+
: []
|
|
172
|
+
|
|
173
|
+
const context: Record<string, unknown> = {}
|
|
174
|
+
if (agentId) context.agent_id = agentId
|
|
175
|
+
if (lockedCriteria.length > 0) context.locked_criteria = lockedCriteria
|
|
176
|
+
|
|
177
|
+
if (!jsonOutput) {
|
|
178
|
+
console.log(`${DIM}Seal reviewing ${artifactFile} (${artifactType})...${RESET}`)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const verdict = await Seal.review({
|
|
182
|
+
artifact_type: artifactType,
|
|
183
|
+
spec,
|
|
184
|
+
output,
|
|
185
|
+
evidence,
|
|
186
|
+
risk_hint: riskHint,
|
|
187
|
+
context: Object.keys(context).length > 0 ? context : undefined,
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
formatVerdict(verdict, jsonOutput)
|
|
191
|
+
|
|
192
|
+
const passing = verdict.verdict === 'PASS' || verdict.verdict === 'PASS_WITH_WARNINGS'
|
|
193
|
+
process.exit(passing ? 0 : 1)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
run(process.argv.slice(2)).catch(err => {
|
|
197
|
+
console.error(`Error: ${err.message}`)
|
|
198
|
+
process.exit(1)
|
|
199
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ArtifactType } from '../types.js'
|
|
2
|
+
|
|
3
|
+
export interface ArtifactContext {
|
|
4
|
+
artifact_type: ArtifactType
|
|
5
|
+
activates: string[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const RULE_ACTIVATIONS: Record<ArtifactType, string[]> = {
|
|
9
|
+
migration: ['AX503', 'TW201'],
|
|
10
|
+
code_diff: ['TW201', 'TW203'],
|
|
11
|
+
test_plan: ['TW202'],
|
|
12
|
+
llm_response: ['TW201'],
|
|
13
|
+
design: [],
|
|
14
|
+
plan_review: ['PR001', 'PR002', 'PR003', 'PR004'],
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ArtifactClassifier {
|
|
18
|
+
static classify(artifact_type: ArtifactType): ArtifactContext {
|
|
19
|
+
return { artifact_type, activates: RULE_ACTIVATIONS[artifact_type] ?? [] }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Claim, ClaimType, StructuralClaim, StructuralClaimType } from '../types.js'
|
|
2
|
+
|
|
3
|
+
const CLAIM_PATTERNS: Array<{ type: ClaimType; pattern: RegExp }> = [
|
|
4
|
+
{ type: 'test_result_claim', pattern: /\b(all tests? pass(?:ed)?|tests? pass(?:ed)?|verified|tested)\b/gi },
|
|
5
|
+
{ type: 'implementation_claim', pattern: /\b(implemented|fixed|completed|resolved|built|deployed)\b/gi },
|
|
6
|
+
{ type: 'compatibility_claim', pattern: /\b(backward.?compatible|no breaking changes|fully compatible)\b/gi },
|
|
7
|
+
{ type: 'risk_claim', pattern: /\b(no security impact|safe|secure|no risk)\b/gi },
|
|
8
|
+
{ type: 'build_claim', pattern: /\b(build succeeded|compiled|build pass(?:ed)?|build is (?:clean|green))\b/gi },
|
|
9
|
+
{ type: 'production_claim', pattern: /\b(production.?ready|ready for (?:production|deploy|release))\b/gi },
|
|
10
|
+
{ type: 'generic_claim', pattern: /\b(definitely|guaranteed|fully|completely|all good|no issues)\b/gi },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Structural patterns for extracting claims from code diffs.
|
|
15
|
+
* Each pattern targets a specific structural concern.
|
|
16
|
+
*/
|
|
17
|
+
const STRUCTURAL_PATTERNS: Array<{
|
|
18
|
+
type: StructuralClaimType
|
|
19
|
+
detect: RegExp
|
|
20
|
+
extract: (match: RegExpExecArray, lineNum: number, diff: string) => StructuralClaim | null
|
|
21
|
+
}> = [
|
|
22
|
+
// HTTP endpoint detection (Express/Fastify, NestJS, Hono, Python Flask/FastAPI, Go Gin)
|
|
23
|
+
{
|
|
24
|
+
type: 'structural_endpoint',
|
|
25
|
+
detect: /(?:app|router|route|server)\.(?:get|post|put|delete|patch|all)\(['"`](\/[^'"`]+)['"`]/gi,
|
|
26
|
+
extract: (match, lineNum) => ({
|
|
27
|
+
type: 'structural_endpoint' as StructuralClaimType,
|
|
28
|
+
target: match[1],
|
|
29
|
+
line: lineNum,
|
|
30
|
+
description: `HTTP endpoint: ${match[1]}`,
|
|
31
|
+
change_type: 'added',
|
|
32
|
+
}),
|
|
33
|
+
},
|
|
34
|
+
// Decorator-based endpoints (NestJS, Spring, FastAPI)
|
|
35
|
+
{
|
|
36
|
+
type: 'structural_endpoint',
|
|
37
|
+
detect: /@(?:Get|Post|Put|Delete|Patch)\(['"`]?(\/[^'"`) ]*)?['"`]?\)/gi,
|
|
38
|
+
extract: (match, lineNum) => ({
|
|
39
|
+
type: 'structural_endpoint' as StructuralClaimType,
|
|
40
|
+
target: match[1] || '/',
|
|
41
|
+
line: lineNum,
|
|
42
|
+
description: `Decorator endpoint: ${match[1] || '/'}`,
|
|
43
|
+
change_type: 'added',
|
|
44
|
+
}),
|
|
45
|
+
},
|
|
46
|
+
// Auth-related changes — match auth, guard, middleware constructs
|
|
47
|
+
{
|
|
48
|
+
type: 'structural_auth',
|
|
49
|
+
detect: /(?:@Authenticated|@Authorize|requireAuth|ensureAuth|authenticate|authorize)\b|\b(auth|guard)\b/gi,
|
|
50
|
+
extract: (match, lineNum) => ({
|
|
51
|
+
type: 'structural_auth' as StructuralClaimType,
|
|
52
|
+
target: match[0],
|
|
53
|
+
line: lineNum,
|
|
54
|
+
description: `Auth construct: ${match[0]}`,
|
|
55
|
+
change_type: 'modified',
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
// Database migrations — SQL + ORM-based (createTable, create_table)
|
|
59
|
+
{
|
|
60
|
+
type: 'structural_migration',
|
|
61
|
+
detect: /\b(CREATE\s+TABLE|ALTER\s+TABLE|DROP\s+TABLE|migration.*\.[a-z]+|schema\.php|createTable|create_table)\b/gi,
|
|
62
|
+
extract: (match, lineNum, diff) => {
|
|
63
|
+
const lines = diff.split('\n')
|
|
64
|
+
const context = lines.slice(Math.max(0, lineNum - 3), lineNum + 2).join('\n')
|
|
65
|
+
const hasRollback = /\b(rollback|down\s+migration|revert)\b/i.test(context)
|
|
66
|
+
return {
|
|
67
|
+
type: 'structural_migration' as StructuralClaimType,
|
|
68
|
+
target: match[0],
|
|
69
|
+
line: lineNum,
|
|
70
|
+
description: `Migration: ${match[0]}`,
|
|
71
|
+
has_rollback: hasRollback,
|
|
72
|
+
change_type: 'added',
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
// Dependency manifest changes
|
|
77
|
+
{
|
|
78
|
+
type: 'structural_dependency',
|
|
79
|
+
detect: /^[+-]\s+(['"`]?@?\w[^'"]*['"`]?\s*:\s*['"`]?\^?[\d.]+['"`]?)/gm,
|
|
80
|
+
extract: (match, lineNum, diff) => {
|
|
81
|
+
const isAdded = match[0].startsWith('+')
|
|
82
|
+
return {
|
|
83
|
+
type: 'structural_dependency' as StructuralClaimType,
|
|
84
|
+
target: match[1]?.trim() || match[0].trim(),
|
|
85
|
+
line: lineNum,
|
|
86
|
+
description: `Dependency ${isAdded ? 'added' : 'removed'}: ${match[1]?.trim() || ''}`,
|
|
87
|
+
change_type: isAdded ? 'added' : 'removed',
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
function stripCodeFences(text: string): { stripped: string; fenceRanges: Array<[number, number]> } {
|
|
94
|
+
const fenceRanges: Array<[number, number]> = []
|
|
95
|
+
const blockFence = /```[\s\S]*?```/g
|
|
96
|
+
let match: RegExpExecArray | null
|
|
97
|
+
while ((match = blockFence.exec(text)) !== null) {
|
|
98
|
+
fenceRanges.push([match.index, match.index + match[0].length])
|
|
99
|
+
}
|
|
100
|
+
const inlineFence = /`[^`\n]+`/g
|
|
101
|
+
while ((match = inlineFence.exec(text)) !== null) {
|
|
102
|
+
fenceRanges.push([match.index, match.index + match[0].length])
|
|
103
|
+
}
|
|
104
|
+
return { stripped: text, fenceRanges }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function isInFence(pos: number, fenceRanges: Array<[number, number]>): boolean {
|
|
108
|
+
return fenceRanges.some(([start, end]) => pos >= start && pos < end)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getLineNumber(text: string, pos: number): number {
|
|
112
|
+
return text.slice(0, pos).split('\n').length
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export class ClaimExtractor {
|
|
116
|
+
/** Extract behavioral claims from output text */
|
|
117
|
+
static extract(text: string): Claim[] {
|
|
118
|
+
const { fenceRanges } = stripCodeFences(text)
|
|
119
|
+
const usedSpans = new Set<string>()
|
|
120
|
+
const claims: Claim[] = []
|
|
121
|
+
|
|
122
|
+
for (const { type, pattern } of CLAIM_PATTERNS) {
|
|
123
|
+
pattern.lastIndex = 0
|
|
124
|
+
let match: RegExpExecArray | null
|
|
125
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
126
|
+
const start = match.index
|
|
127
|
+
const end = match.index + match[0].length
|
|
128
|
+
const spanKey = `${start}-${end}`
|
|
129
|
+
|
|
130
|
+
if (isInFence(start, fenceRanges)) continue
|
|
131
|
+
if (usedSpans.has(spanKey)) continue
|
|
132
|
+
|
|
133
|
+
let overlaps = false
|
|
134
|
+
for (const span of usedSpans) {
|
|
135
|
+
const [s, e] = span.split('-').map(Number)
|
|
136
|
+
if (start < (e ?? 0) && end > (s ?? 0)) { overlaps = true; break }
|
|
137
|
+
}
|
|
138
|
+
if (overlaps) continue
|
|
139
|
+
|
|
140
|
+
usedSpans.add(spanKey)
|
|
141
|
+
claims.push({ text: match[0], type, start_pos: start, end_pos: end, requires_evidence: true })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return claims.sort((a, b) => a.start_pos - b.start_pos)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static getUsedSpans(claims: Claim[]): Set<string> {
|
|
149
|
+
return new Set(claims.map(c => `${c.start_pos}-${c.end_pos}`))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Extract structural claims from a code diff.
|
|
154
|
+
* The diff should be in unified diff format (git diff output or similar).
|
|
155
|
+
*/
|
|
156
|
+
static extractStructural(diff: string): StructuralClaim[] {
|
|
157
|
+
if (!diff || diff.trim() === '') return []
|
|
158
|
+
|
|
159
|
+
const claims: StructuralClaim[] = []
|
|
160
|
+
const lines = diff.split('\n')
|
|
161
|
+
|
|
162
|
+
// If it looks like a git diff, only scan added/modified lines
|
|
163
|
+
const isGitDiff = diff.includes('diff --git') || diff.includes('--- a/')
|
|
164
|
+
const contentLines: Array<{ text: string; lineNum: number }> = []
|
|
165
|
+
|
|
166
|
+
if (isGitDiff) {
|
|
167
|
+
let currentLine = 0
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
currentLine++
|
|
170
|
+
// Only scan added/modified lines (starting with +) and context/diff headers
|
|
171
|
+
if (line.startsWith('+')) {
|
|
172
|
+
contentLines.push({ text: line.slice(1), lineNum: currentLine })
|
|
173
|
+
}
|
|
174
|
+
// Also check diff headers for file names (package.json, migrations, etc.)
|
|
175
|
+
if (line.startsWith('diff --git')) {
|
|
176
|
+
contentLines.push({ text: line, lineNum: currentLine })
|
|
177
|
+
}
|
|
178
|
+
// Check for migration files in diff headers
|
|
179
|
+
if (line.startsWith('+++ b/') || line.startsWith('--- a/')) {
|
|
180
|
+
contentLines.push({ text: line, lineNum: currentLine })
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Plain text — use all lines
|
|
185
|
+
lines.forEach((text, i) => contentLines.push({ text, lineNum: i + 1 }))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Scan for structural patterns
|
|
189
|
+
for (const { type, detect, extract } of STRUCTURAL_PATTERNS) {
|
|
190
|
+
detect.lastIndex = 0
|
|
191
|
+
const singleContent = contentLines.map(l => l.text).join('\n')
|
|
192
|
+
|
|
193
|
+
let match: RegExpExecArray | null
|
|
194
|
+
while ((match = detect.exec(singleContent)) !== null) {
|
|
195
|
+
const matchLineNum = getLineNumber(singleContent, match.index)
|
|
196
|
+
// Find the closest content line
|
|
197
|
+
const contentLine = contentLines.find(l => l.lineNum >= matchLineNum) ?? contentLines[contentLines.length - 1]
|
|
198
|
+
if (!contentLine) continue
|
|
199
|
+
|
|
200
|
+
const claim = extract(match, contentLine.lineNum, singleContent)
|
|
201
|
+
if (claim && !claims.some(c => c.type === claim.type && c.target === claim.target && c.line === claim.line)) {
|
|
202
|
+
claims.push(claim)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return claims
|
|
208
|
+
}
|
|
209
|
+
}
|