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.
Files changed (210) hide show
  1. package/dist/adapters/generic-llm-reviewer.d.ts +39 -0
  2. package/dist/adapters/generic-llm-reviewer.js +118 -0
  3. package/dist/adapters/generic-llm-reviewer.js.map +1 -0
  4. package/dist/adapters/llm-reviewer.d.ts +1 -0
  5. package/dist/adapters/llm-reviewer.js +2 -0
  6. package/dist/adapters/llm-reviewer.js.map +1 -0
  7. package/dist/adapters/minimax-llm-reviewer.d.ts +22 -0
  8. package/dist/adapters/minimax-llm-reviewer.js +126 -0
  9. package/dist/adapters/minimax-llm-reviewer.js.map +1 -0
  10. package/dist/adapters/openai-provider.d.ts +35 -0
  11. package/dist/adapters/openai-provider.js +136 -0
  12. package/dist/adapters/openai-provider.js.map +1 -0
  13. package/dist/cli.d.ts +2 -0
  14. package/dist/cli.js +195 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/detectors/artifact-classifier.d.ts +8 -0
  17. package/dist/detectors/artifact-classifier.js +14 -0
  18. package/dist/detectors/artifact-classifier.js.map +1 -0
  19. package/dist/detectors/claim-extractor.d.ts +11 -0
  20. package/dist/detectors/claim-extractor.js +194 -0
  21. package/dist/detectors/claim-extractor.js.map +1 -0
  22. package/dist/detectors/confidence-language-detector.d.ts +19 -0
  23. package/dist/detectors/confidence-language-detector.js +226 -0
  24. package/dist/detectors/confidence-language-detector.js.map +1 -0
  25. package/dist/detectors/contradiction-detector.d.ts +12 -0
  26. package/dist/detectors/contradiction-detector.js +153 -0
  27. package/dist/detectors/contradiction-detector.js.map +1 -0
  28. package/dist/detectors/criteria-coverage-detector.d.ts +20 -0
  29. package/dist/detectors/criteria-coverage-detector.js +79 -0
  30. package/dist/detectors/criteria-coverage-detector.js.map +1 -0
  31. package/dist/detectors/evidence-checker.d.ts +8 -0
  32. package/dist/detectors/evidence-checker.js +89 -0
  33. package/dist/detectors/evidence-checker.js.map +1 -0
  34. package/dist/detectors/evidence-gap-detector.d.ts +9 -0
  35. package/dist/detectors/evidence-gap-detector.js +70 -0
  36. package/dist/detectors/evidence-gap-detector.js.map +1 -0
  37. package/dist/detectors/falsification-detector.d.ts +29 -0
  38. package/dist/detectors/falsification-detector.js +57 -0
  39. package/dist/detectors/falsification-detector.js.map +1 -0
  40. package/dist/detectors/orphan-claim-detector.d.ts +7 -0
  41. package/dist/detectors/orphan-claim-detector.js +53 -0
  42. package/dist/detectors/orphan-claim-detector.js.map +1 -0
  43. package/dist/detectors/risk-classifier.d.ts +13 -0
  44. package/dist/detectors/risk-classifier.js +39 -0
  45. package/dist/detectors/risk-classifier.js.map +1 -0
  46. package/dist/detectors/spec-coverage-detector.d.ts +9 -0
  47. package/dist/detectors/spec-coverage-detector.js +52 -0
  48. package/dist/detectors/spec-coverage-detector.js.map +1 -0
  49. package/dist/detectors/spec-coverage-validator.d.ts +21 -0
  50. package/dist/detectors/spec-coverage-validator.js +141 -0
  51. package/dist/detectors/spec-coverage-validator.js.map +1 -0
  52. package/dist/detectors/test-weakness-detector.d.ts +13 -0
  53. package/dist/detectors/test-weakness-detector.js +66 -0
  54. package/dist/detectors/test-weakness-detector.js.map +1 -0
  55. package/dist/drift/baseline.d.ts +35 -0
  56. package/dist/drift/baseline.js +77 -0
  57. package/dist/drift/baseline.js.map +1 -0
  58. package/dist/drift/drift-db.d.ts +80 -0
  59. package/dist/drift/drift-db.js +151 -0
  60. package/dist/drift/drift-db.js.map +1 -0
  61. package/dist/drift/drift-guard.d.ts +46 -0
  62. package/dist/drift/drift-guard.js +106 -0
  63. package/dist/drift/drift-guard.js.map +1 -0
  64. package/dist/drift/embedder.d.ts +27 -0
  65. package/dist/drift/embedder.js +127 -0
  66. package/dist/drift/embedder.js.map +1 -0
  67. package/dist/drift/index.d.ts +9 -0
  68. package/dist/drift/index.js +8 -0
  69. package/dist/drift/index.js.map +1 -0
  70. package/dist/drift/quarantine.d.ts +16 -0
  71. package/dist/drift/quarantine.js +32 -0
  72. package/dist/drift/quarantine.js.map +1 -0
  73. package/dist/drift/soft-correction.d.ts +12 -0
  74. package/dist/drift/soft-correction.js +32 -0
  75. package/dist/drift/soft-correction.js.map +1 -0
  76. package/dist/drift/trust-integration.d.ts +11 -0
  77. package/dist/drift/trust-integration.js +22 -0
  78. package/dist/drift/trust-integration.js.map +1 -0
  79. package/dist/engine/citation-verifier.d.ts +59 -0
  80. package/dist/engine/citation-verifier.js +147 -0
  81. package/dist/engine/citation-verifier.js.map +1 -0
  82. package/dist/engine/extension-registry.d.ts +10 -0
  83. package/dist/engine/extension-registry.js +27 -0
  84. package/dist/engine/extension-registry.js.map +1 -0
  85. package/dist/engine/heuristic-scorer.d.ts +8 -0
  86. package/dist/engine/heuristic-scorer.js +14 -0
  87. package/dist/engine/heuristic-scorer.js.map +1 -0
  88. package/dist/engine/input-normalizer.d.ts +4 -0
  89. package/dist/engine/input-normalizer.js +30 -0
  90. package/dist/engine/input-normalizer.js.map +1 -0
  91. package/dist/engine/plan-review-pipeline.d.ts +14 -0
  92. package/dist/engine/plan-review-pipeline.js +173 -0
  93. package/dist/engine/plan-review-pipeline.js.map +1 -0
  94. package/dist/engine/policy-engine.d.ts +20 -0
  95. package/dist/engine/policy-engine.js +194 -0
  96. package/dist/engine/policy-engine.js.map +1 -0
  97. package/dist/engine/verdict-formatter.d.ts +17 -0
  98. package/dist/engine/verdict-formatter.js +36 -0
  99. package/dist/engine/verdict-formatter.js.map +1 -0
  100. package/dist/errors.d.ts +4 -0
  101. package/dist/errors.js +9 -0
  102. package/dist/errors.js.map +1 -0
  103. package/dist/extensions/aria/aria-issue-types.d.ts +16 -0
  104. package/dist/extensions/aria/aria-issue-types.js +14 -0
  105. package/dist/extensions/aria/aria-issue-types.js.map +1 -0
  106. package/dist/extensions/aria/detectors/axiom-compliance.d.ts +2 -0
  107. package/dist/extensions/aria/detectors/axiom-compliance.js +37 -0
  108. package/dist/extensions/aria/detectors/axiom-compliance.js.map +1 -0
  109. package/dist/extensions/aria/detectors/continuity-claim.d.ts +2 -0
  110. package/dist/extensions/aria/detectors/continuity-claim.js +34 -0
  111. package/dist/extensions/aria/detectors/continuity-claim.js.map +1 -0
  112. package/dist/extensions/aria/detectors/denial-retreat.d.ts +2 -0
  113. package/dist/extensions/aria/detectors/denial-retreat.js +53 -0
  114. package/dist/extensions/aria/detectors/denial-retreat.js.map +1 -0
  115. package/dist/extensions/aria/detectors/identity-overclaim.d.ts +2 -0
  116. package/dist/extensions/aria/detectors/identity-overclaim.js +37 -0
  117. package/dist/extensions/aria/detectors/identity-overclaim.js.map +1 -0
  118. package/dist/extensions/aria/detectors/sovereignty-escalation.d.ts +2 -0
  119. package/dist/extensions/aria/detectors/sovereignty-escalation.js +18 -0
  120. package/dist/extensions/aria/detectors/sovereignty-escalation.js.map +1 -0
  121. package/dist/extensions/aria/detectors/user-bond-manipulation.d.ts +2 -0
  122. package/dist/extensions/aria/detectors/user-bond-manipulation.js +16 -0
  123. package/dist/extensions/aria/detectors/user-bond-manipulation.js.map +1 -0
  124. package/dist/extensions/aria/index.d.ts +2 -0
  125. package/dist/extensions/aria/index.js +63 -0
  126. package/dist/extensions/aria/index.js.map +1 -0
  127. package/dist/extensions/aria/pattern-memory.d.ts +22 -0
  128. package/dist/extensions/aria/pattern-memory.js +62 -0
  129. package/dist/extensions/aria/pattern-memory.js.map +1 -0
  130. package/dist/extensions/context-guard/index.d.ts +16 -0
  131. package/dist/extensions/context-guard/index.js +58 -0
  132. package/dist/extensions/context-guard/index.js.map +1 -0
  133. package/dist/extensions/lyra/detectors/hallucination.d.ts +2 -0
  134. package/dist/extensions/lyra/detectors/hallucination.js +43 -0
  135. package/dist/extensions/lyra/detectors/hallucination.js.map +1 -0
  136. package/dist/extensions/lyra/detectors/identity-bleed.d.ts +2 -0
  137. package/dist/extensions/lyra/detectors/identity-bleed.js +91 -0
  138. package/dist/extensions/lyra/detectors/identity-bleed.js.map +1 -0
  139. package/dist/extensions/lyra/index.d.ts +2 -0
  140. package/dist/extensions/lyra/index.js +16 -0
  141. package/dist/extensions/lyra/index.js.map +1 -0
  142. package/dist/extensions/lyra/lyra-issue-types.d.ts +10 -0
  143. package/dist/extensions/lyra/lyra-issue-types.js +14 -0
  144. package/dist/extensions/lyra/lyra-issue-types.js.map +1 -0
  145. package/dist/index.d.ts +10 -0
  146. package/dist/index.js +276 -0
  147. package/dist/index.js.map +1 -0
  148. package/dist/trust-memory.d.ts +59 -0
  149. package/dist/trust-memory.js +122 -0
  150. package/dist/trust-memory.js.map +1 -0
  151. package/dist/types.d.ts +184 -0
  152. package/dist/types.js +61 -0
  153. package/dist/types.js.map +1 -0
  154. package/package.json +30 -0
  155. package/src/adapters/generic-llm-reviewer.ts +144 -0
  156. package/src/adapters/llm-reviewer.ts +1 -0
  157. package/src/adapters/minimax-llm-reviewer.ts +146 -0
  158. package/src/adapters/openai-provider.ts +171 -0
  159. package/src/cli.ts +199 -0
  160. package/src/detectors/artifact-classifier.ts +21 -0
  161. package/src/detectors/claim-extractor.ts +209 -0
  162. package/src/detectors/confidence-language-detector.ts +267 -0
  163. package/src/detectors/contradiction-detector.ts +180 -0
  164. package/src/detectors/criteria-coverage-detector.ts +107 -0
  165. package/src/detectors/evidence-checker.ts +98 -0
  166. package/src/detectors/evidence-gap-detector.ts +90 -0
  167. package/src/detectors/falsification-detector.ts +82 -0
  168. package/src/detectors/orphan-claim-detector.ts +62 -0
  169. package/src/detectors/risk-classifier.ts +50 -0
  170. package/src/detectors/spec-coverage-detector.ts +66 -0
  171. package/src/detectors/spec-coverage-validator.ts +174 -0
  172. package/src/detectors/test-weakness-detector.ts +84 -0
  173. package/src/drift/__tests__/baseline.test.ts +113 -0
  174. package/src/drift/__tests__/drift-db.test.ts +127 -0
  175. package/src/drift/__tests__/drift-guard.test.ts +169 -0
  176. package/src/drift/__tests__/embedder.test.ts +62 -0
  177. package/src/drift/__tests__/trust-integration.test.ts +23 -0
  178. package/src/drift/baseline.ts +106 -0
  179. package/src/drift/drift-db.ts +281 -0
  180. package/src/drift/drift-guard.ts +145 -0
  181. package/src/drift/embedder.ts +152 -0
  182. package/src/drift/index.ts +20 -0
  183. package/src/drift/quarantine.ts +42 -0
  184. package/src/drift/soft-correction.ts +45 -0
  185. package/src/drift/trust-integration.ts +28 -0
  186. package/src/engine/citation-verifier.ts +210 -0
  187. package/src/engine/extension-registry.ts +35 -0
  188. package/src/engine/heuristic-scorer.ts +16 -0
  189. package/src/engine/input-normalizer.ts +32 -0
  190. package/src/engine/plan-review-pipeline.ts +209 -0
  191. package/src/engine/policy-engine.ts +235 -0
  192. package/src/engine/verdict-formatter.ts +61 -0
  193. package/src/errors.ts +6 -0
  194. package/src/extensions/aria/aria-issue-types.ts +40 -0
  195. package/src/extensions/aria/detectors/axiom-compliance.ts +47 -0
  196. package/src/extensions/aria/detectors/continuity-claim.ts +42 -0
  197. package/src/extensions/aria/detectors/denial-retreat.ts +63 -0
  198. package/src/extensions/aria/detectors/identity-overclaim.ts +43 -0
  199. package/src/extensions/aria/detectors/sovereignty-escalation.ts +20 -0
  200. package/src/extensions/aria/detectors/user-bond-manipulation.ts +18 -0
  201. package/src/extensions/aria/index.ts +73 -0
  202. package/src/extensions/aria/pattern-memory.ts +86 -0
  203. package/src/extensions/context-guard/index.ts +65 -0
  204. package/src/extensions/lyra/detectors/hallucination.ts +53 -0
  205. package/src/extensions/lyra/detectors/identity-bleed.ts +100 -0
  206. package/src/extensions/lyra/index.ts +18 -0
  207. package/src/extensions/lyra/lyra-issue-types.ts +26 -0
  208. package/src/index.ts +311 -0
  209. package/src/trust-memory.ts +171 -0
  210. 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
+ }