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