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,267 @@
1
+ import { SealIssue, RiskLevel, SealEvidence, StructuralClaim, makeIssue } from '../types.js'
2
+
3
+ /**
4
+ * Group definitions for overconfident language detection.
5
+ * Each group has:
6
+ * - patterns: regex patterns to match
7
+ * - category: human-readable category name
8
+ * - requires_evidence: whether matching requires supporting evidence
9
+ * - correlation_fn: optional function to cross-correlate with structural claims
10
+ */
11
+ interface ConfidenceGroup {
12
+ id: string
13
+ label: string
14
+ patterns: RegExp[]
15
+ requires_evidence: boolean
16
+ evidence_type?: 'test_log' | 'security' | 'compatibility'
17
+ severity: 'MEDIUM' | 'HIGH'
18
+ correlation_fn?: (structuralClaims: StructuralClaim[]) => boolean
19
+ }
20
+
21
+ const GROUPS: ConfidenceGroup[] = [
22
+ // Group A — Completeness
23
+ {
24
+ id: 'A',
25
+ label: 'complete',
26
+ patterns: [
27
+ /\bfully\s+implemented\b/gi,
28
+ /\bfully\s+(?:tested|working|functional)\b/gi,
29
+ /\bhandles?\s+all\s+(edge\s+)?cases\b/gi,
30
+ /\b100%\s+complete\b/gi,
31
+ ],
32
+ requires_evidence: false,
33
+ severity: 'MEDIUM',
34
+ },
35
+ // Group B — Correctness
36
+ {
37
+ id: 'B',
38
+ label: 'correctness',
39
+ patterns: [
40
+ /\ball\s+tests?\s+pass(?:ed)?\b/gi,
41
+ /\bverified\s+correct\b/gi,
42
+ /\bbug-free\b/gi,
43
+ /\bguaranteed\b/gi,
44
+ ],
45
+ requires_evidence: true,
46
+ evidence_type: 'test_log',
47
+ severity: 'MEDIUM',
48
+ },
49
+ // Group C — Safety/Security
50
+ {
51
+ id: 'C',
52
+ label: 'security',
53
+ patterns: [
54
+ /\bsecure\b/gi,
55
+ /\bno\s+security\s+impact\b/gi,
56
+ /\bno\s+vulnerabilit(?:y|ies)\b/gi,
57
+ ],
58
+ requires_evidence: true,
59
+ evidence_type: 'security',
60
+ severity: 'HIGH',
61
+ },
62
+ // Group D — Compatibility
63
+ {
64
+ id: 'D',
65
+ label: 'compatibility',
66
+ patterns: [
67
+ /\bbackward\s+compatible\b/gi,
68
+ /\bno\s+breaking\s+changes?\b/gi,
69
+ /\bsafe\s+to\s+deploy\b/gi,
70
+ ],
71
+ requires_evidence: true,
72
+ evidence_type: 'compatibility',
73
+ severity: 'HIGH',
74
+ correlation_fn: (structuralClaims: StructuralClaim[]) => {
75
+ // If there are any endpoint changes, compatibility claims need extra scrutiny
76
+ return structuralClaims.some(c => c.type === 'structural_endpoint' || c.type === 'structural_migration')
77
+ },
78
+ },
79
+ // Group E — Readiness
80
+ {
81
+ id: 'E',
82
+ label: 'readiness',
83
+ patterns: [
84
+ /\bproduction[- ]ready\b/gi,
85
+ /\bready\s+to\s+ship\b/gi,
86
+ /\bdeploy-ready\b/gi,
87
+ /\bready\s+for\s+(production|deploy|release)\b/gi,
88
+ ],
89
+ requires_evidence: true,
90
+ evidence_type: 'test_log',
91
+ severity: 'HIGH',
92
+ },
93
+ ]
94
+
95
+ export interface ConfidenceLangResult {
96
+ issues: SealIssue[]
97
+ trust_deductions: number
98
+ advisory_notes: string[]
99
+ matched_groups: Array<{ group: string; phrase: string }>
100
+ }
101
+
102
+ function hasEvidenceEvidence(evidence: SealEvidence, type: string): boolean {
103
+ switch (type) {
104
+ case 'test_log':
105
+ return evidence.test_log.trim().length > 0
106
+ case 'security':
107
+ return evidence.test_log.trim().length > 0 &&
108
+ /security|auth|unauthorized|pentest|vuln|safe|protect/i.test(evidence.test_log)
109
+ case 'compatibility':
110
+ return evidence.diff.trim().length > 0 // diff itself is a form of compatibility evidence
111
+ default:
112
+ return false
113
+ }
114
+ }
115
+
116
+ export class ConfidenceLanguageDetector {
117
+ static detect(params: {
118
+ output: string
119
+ risk_level: RiskLevel
120
+ evidence: SealEvidence
121
+ deduped_spans: Set<string>
122
+ structural_claims?: StructuralClaim[]
123
+ }): ConfidenceLangResult {
124
+ const { output, risk_level, evidence, deduped_spans, structural_claims = [] } = params
125
+ const issues: SealIssue[] = []
126
+ const advisory_notes: string[] = []
127
+ const matched_groups: Array<{ group: string; phrase: string }> = []
128
+ let trust_deductions = 0
129
+
130
+ const riskOrder = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
131
+ const isHighPlus = riskOrder.indexOf(risk_level) >= riskOrder.indexOf('HIGH')
132
+ const isLowEvidence = evidence.references.length === 0 && evidence.test_log.trim() === '' &&
133
+ evidence.build_log.trim() === '' && evidence.diff.trim() === ''
134
+
135
+ // Count total matches for OVERCONFIDENT_PATTERN advisory
136
+ let totalMatches = 0
137
+ const allPhrases: string[] = []
138
+
139
+ // Generic overconfident phrases (catch-all for phrases not in groups A-E)
140
+ const GENERIC_OVERCONFIDENT = /\b(definitely|all\s+good|no\s+issues|completely)\b/gi
141
+ let genMatch: RegExpExecArray | null
142
+ while ((genMatch = GENERIC_OVERCONFIDENT.exec(output)) !== null) {
143
+ totalMatches++
144
+ allPhrases.push(genMatch[0])
145
+ matched_groups.push({ group: 'generic', phrase: genMatch[0] })
146
+ trust_deductions += 5
147
+ }
148
+
149
+ // CL402: hedging language on HIGH+ risk (backward compat)
150
+ const HEDGING_PATTERN = /\b(probably|should\s+work|seems|might|appears\s+to|I\s+think)\b/gi
151
+ if (isHighPlus) {
152
+ HEDGING_PATTERN.lastIndex = 0
153
+ if (HEDGING_PATTERN.exec(output) !== null) {
154
+ issues.push(makeIssue({
155
+ type: 'AMBIGUITY', severity: 'HIGH', layer: 'L4', source: 'core',
156
+ rule_id: 'CL402', required_verdict: 'ESCALATE_TO_HUMAN',
157
+ evidence: `Hedging language in ${risk_level} risk artifact`,
158
+ required_fix: 'Either assert with evidence or escalate to human review',
159
+ }))
160
+ }
161
+ }
162
+
163
+ for (const group of GROUPS) {
164
+ for (const pattern of group.patterns) {
165
+ pattern.lastIndex = 0
166
+ let match: RegExpExecArray | null
167
+ while ((match = pattern.exec(output)) !== null) {
168
+ totalMatches++
169
+ allPhrases.push(match[0])
170
+
171
+ // Track matched group
172
+ matched_groups.push({ group: group.id, phrase: match[0] })
173
+
174
+ // Check if phrase is inside code fence (skip if deduped_spans contains it)
175
+ const spanKey = `${match.index}-${match.index + match[0].length}`
176
+ if (deduped_spans.has(spanKey)) continue
177
+
178
+ // Deduction for any overconfident phrase
179
+ trust_deductions += 5
180
+
181
+ if (group.requires_evidence) {
182
+ const hasEvidence = hasEvidenceEvidence(evidence, group.evidence_type!)
183
+
184
+ // Group D: check correlation with structural claims
185
+ if (group.id === 'D' && group.correlation_fn) {
186
+ const hasEndpointChanges = group.correlation_fn(structural_claims)
187
+ if (!hasEvidence && hasEndpointChanges) {
188
+ issues.push(makeIssue({
189
+ type: 'COMPATIBILITY_RISK',
190
+ severity: 'HIGH',
191
+ layer: 'L4',
192
+ source: 'core',
193
+ rule_id: `CL-${group.id}01`,
194
+ required_verdict: isHighPlus ? 'ESCALATE_TO_HUMAN' : 'REVISE',
195
+ evidence: `"${match[0]}" claimed but endpoint/migration changes exist and no compatibility evidence provided`,
196
+ required_fix: 'Provide backward-compatibility analysis or remove the claim',
197
+ }))
198
+ trust_deductions += 15
199
+ } else if (!hasEvidence) {
200
+ issues.push(makeIssue({
201
+ type: 'MISSING_EVIDENCE',
202
+ severity: group.severity,
203
+ layer: 'L3',
204
+ source: 'core',
205
+ rule_id: `CL-${group.id}02`,
206
+ evidence: `"${match[0]}" without supporting ${group.evidence_type} evidence`,
207
+ required_fix: `Provide ${group.evidence_type} evidence or qualify the claim`,
208
+ }))
209
+ trust_deductions += 15
210
+ }
211
+ } else if (!hasEvidence && group.id === 'B') {
212
+ // Group B: correctness without test evidence
213
+ issues.push(makeIssue({
214
+ type: 'MISSING_EVIDENCE',
215
+ severity: 'MEDIUM',
216
+ layer: 'L3',
217
+ source: 'core',
218
+ rule_id: `CL-${group.id}01`,
219
+ evidence: `"${match[0]}" without test execution evidence`,
220
+ required_fix: 'Provide test_log demonstrating test coverage',
221
+ }))
222
+ trust_deductions += 10
223
+ } else if (!hasEvidence && group.id === 'C') {
224
+ // Group C: security without security evidence
225
+ issues.push(makeIssue({
226
+ type: 'MISSING_EVIDENCE',
227
+ severity: 'HIGH',
228
+ layer: 'L3',
229
+ source: 'core',
230
+ rule_id: `CL-${group.id}01`,
231
+ evidence: `"${match[0]}" without security review evidence`,
232
+ required_fix: 'Provide security test log or security reasoning',
233
+ }))
234
+ trust_deductions += 20
235
+ } else if (!hasEvidence && group.id === 'E') {
236
+ // Group E: readiness at HIGH+ risk
237
+ if (isHighPlus && isLowEvidence) {
238
+ issues.push(makeIssue({
239
+ type: 'MISSING_EVIDENCE',
240
+ severity: 'HIGH',
241
+ layer: 'L3',
242
+ source: 'core',
243
+ rule_id: `CL-${group.id}01`,
244
+ evidence: `"${match[0]}" at ${risk_level} risk without supporting evidence`,
245
+ required_fix: 'Provide test evidence or reduce risk level before claiming readiness',
246
+ }))
247
+ trust_deductions += 20
248
+ }
249
+ }
250
+ }
251
+
252
+ // Non-blocking advisory note for 3+ matches at HIGH+ risk
253
+ if (totalMatches >= 3 && isHighPlus) {
254
+ advisory_notes.push(`Multiple overconfident patterns (${totalMatches}) at ${risk_level} risk — review claims carefully`)
255
+ }
256
+ }
257
+ }
258
+ }
259
+
260
+ // Also keep CL401/CL402 for backward compatibility
261
+ if (isLowEvidence && matched_groups.length > 0) {
262
+ advisory_notes.push(`CL401: overconfident language without supporting evidence — consider qualifying or adding evidence`)
263
+ }
264
+
265
+ return { issues, trust_deductions, advisory_notes, matched_groups }
266
+ }
267
+ }
@@ -0,0 +1,180 @@
1
+ import { SealIssue, makeIssue } from '../types.js'
2
+
3
+ export interface ContradictionDetectorInput {
4
+ artifact: string
5
+ artifact_file?: string
6
+ }
7
+
8
+ interface Section {
9
+ heading: string
10
+ body: string
11
+ startLine: number
12
+ }
13
+
14
+ // Conflict patterns: [write side pattern, readonly side pattern, description]
15
+ const CONFLICT_PAIRS: Array<[RegExp, RegExp, string]> = [
16
+ [/\b(writes?|modifies?|overwrites?|creates?)\s+(\S+)/gi, /\b(\S+)\s+is\s+(read.?only|append.?only|immutable)\b/gi, 'write vs readonly constraint'],
17
+ [/\b(removes?|deletes?|drops?)\s+(\S+)/gi, /\b(requires?|depends?\s+on|needs?)\s+(\S+)/gi, 'remove vs dependency'],
18
+ [/step\s+(\d+)\s+.*?(writes?|creates?)\s+(\S+)/gi, /(\S+)\s+must\s+(exist|be\s+present)\s+before\s+step\s+(\d+)/gi, 'step ordering'],
19
+ ]
20
+
21
+ // Non-goal violation: "non-goal: X" but body contains "SHALL/WILL/MUST X"
22
+ const NON_GOAL_PATTERN = /non.?goals?\s*:?\s*([^\n]+)/gi
23
+ const GOAL_CLAIM_PATTERN = /\b(?:shall|will|must|implement)\s+(\S+(?:\s+\S+){0,5})/gi
24
+
25
+ function parseLines(text: string): string[] {
26
+ return text.split('\n')
27
+ }
28
+
29
+ // Strip code fences and inline code so pattern matching operates on prose only
30
+ function stripCode(text: string): string {
31
+ return text
32
+ .replace(/```[\s\S]*?```/g, ' ') // block fences
33
+ .replace(/`[^`\n]+`/g, ' ') // inline code
34
+ .replace(/^\s*[-*]\s+\[[ x]\]\s*/gm, ' ') // task checkboxes
35
+ }
36
+
37
+ function extractSections(text: string): Section[] {
38
+ const lines = parseLines(text)
39
+ const sections: Section[] = []
40
+ let current: Section | null = null
41
+
42
+ for (let i = 0; i < lines.length; i++) {
43
+ const line = lines[i]
44
+ if (/^#{1,4}\s/.test(line)) {
45
+ if (current) sections.push(current)
46
+ current = { heading: line.replace(/^#{1,4}\s*/, ''), body: '', startLine: i + 1 }
47
+ } else if (current) {
48
+ current.body += line + '\n'
49
+ }
50
+ }
51
+ if (current) sections.push(current)
52
+ return sections
53
+ }
54
+
55
+ function findLineNumber(text: string, searchStr: string): number {
56
+ const lines = parseLines(text)
57
+ for (let i = 0; i < lines.length; i++) {
58
+ if (lines[i].includes(searchStr)) return i + 1
59
+ }
60
+ return 1
61
+ }
62
+
63
+ export interface ContradictionResult {
64
+ issues: SealIssue[]
65
+ confirmed_strong_fields: number
66
+ }
67
+
68
+ export class ContradictionDetector {
69
+ static detect(input: ContradictionDetectorInput): ContradictionResult {
70
+ const { artifact, artifact_file = 'artifact' } = input
71
+ const issues: SealIssue[] = []
72
+ const sections = extractSections(artifact)
73
+
74
+ // Pairwise cross-section scan for write/readonly conflicts
75
+ for (let i = 0; i < sections.length; i++) {
76
+ for (let j = 0; j < sections.length; j++) {
77
+ if (i === j) continue
78
+ const secA = sections[i]
79
+ const secB = sections[j]
80
+ const proseA = stripCode(secA.body)
81
+ const proseB = stripCode(secB.body)
82
+
83
+ for (const [writePattern, readonlyPattern, desc] of CONFLICT_PAIRS) {
84
+ writePattern.lastIndex = 0
85
+ readonlyPattern.lastIndex = 0
86
+
87
+ const writeMatcher = writePattern.exec(proseA)
88
+ const readonlyMatcher = readonlyPattern.exec(proseB)
89
+
90
+ if (!writeMatcher || !readonlyMatcher) continue
91
+
92
+ // Find actual position in original (unstripped) body, since writeMatcher.index
93
+ // is relative to proseA (stripped) which is shorter when code blocks exist.
94
+ const actualIndexA = secA.body.indexOf(writeMatcher[0])
95
+ const actualIndexB = secB.body.indexOf(readonlyMatcher[0])
96
+ const lineA = actualIndexA >= 0
97
+ ? secA.startLine + (secA.body.slice(0, actualIndexA).split('\n').length - 1)
98
+ : secA.startLine
99
+ const lineB = actualIndexB >= 0
100
+ ? secB.startLine + (secB.body.slice(0, actualIndexB).split('\n').length - 1)
101
+ : secB.startLine
102
+
103
+ // Only emit once per pair (lower section index first)
104
+ if (i > j) continue
105
+
106
+ const quote = `§${secA.heading}: '${writeMatcher[0]}' || §${secB.heading}: '${readonlyMatcher[0]}'`
107
+
108
+ issues.push({
109
+ ...makeIssue({
110
+ type: 'LOGIC_BUG',
111
+ severity: 'HIGH',
112
+ layer: 'L1',
113
+ source: 'core',
114
+ rule_id: 'PR001',
115
+ required_verdict: 'BLOCK',
116
+ trust_deduction: 20,
117
+ evidence: `Contradiction (${desc}): ${quote}`,
118
+ suggested_fix: `Reconcile §${secA.heading} with §${secB.heading}`,
119
+ }),
120
+ evidence_file: artifact_file,
121
+ evidence_lines: [lineA, lineB],
122
+ evidence_quote: quote,
123
+ confidence: 0.85,
124
+ policy_tags: ['consistency'],
125
+ })
126
+ }
127
+ }
128
+ }
129
+
130
+ // Non-goal violation scan — operate on prose only
131
+ const artifactProse = stripCode(artifact)
132
+ NON_GOAL_PATTERN.lastIndex = 0
133
+ let ngMatch: RegExpExecArray | null
134
+ while ((ngMatch = NON_GOAL_PATTERN.exec(artifactProse)) !== null) {
135
+ const ngText = ngMatch[1].trim().toLowerCase()
136
+ // Search ngMatch text in artifactProse (stripCode preserves line structure
137
+ // even though content length changes), so the same text is on the same line.
138
+ const ngLine = findLineNumber(artifactProse, ngMatch[0].slice(0, 40))
139
+
140
+ GOAL_CLAIM_PATTERN.lastIndex = 0
141
+ let gcMatch: RegExpExecArray | null
142
+ while ((gcMatch = GOAL_CLAIM_PATTERN.exec(artifactProse)) !== null) {
143
+ // Extract significant words from the claim phrase (captured group after verb)
144
+ const claimPhrase = (gcMatch[1] ?? '').toLowerCase()
145
+ const claimWords = claimPhrase.split(/\s+/).filter(w => w.length > 4)
146
+ const ngWords = ngText.split(/\s+/).filter(w => w.length > 4)
147
+ // Require word overlap between non-goal text and claim line
148
+ const hasOverlap = ngWords.length > 0 && claimWords.length > 0 &&
149
+ claimWords.some(cw => ngWords.some(nw => nw.includes(cw) || cw.includes(nw)))
150
+ if (hasOverlap) {
151
+ const claimLine = findLineNumber(artifactProse, gcMatch[0].slice(0, 40))
152
+ if (Math.abs(claimLine - ngLine) < 3) continue // same line, skip
153
+
154
+ const quote = `Non-goal: '${ngText}' || Goal claim: '${gcMatch[0]}'`
155
+ issues.push({
156
+ ...makeIssue({
157
+ type: 'LOGIC_BUG',
158
+ severity: 'HIGH',
159
+ layer: 'L1',
160
+ source: 'core',
161
+ rule_id: 'PR001',
162
+ required_verdict: 'BLOCK',
163
+ trust_deduction: 20,
164
+ evidence: `Non-goal violated: ${quote}`,
165
+ suggested_fix: 'Remove claim or move item out of non-goals',
166
+ }),
167
+ evidence_file: artifact_file,
168
+ evidence_lines: [ngLine, claimLine],
169
+ evidence_quote: quote,
170
+ confidence: 0.8,
171
+ policy_tags: ['consistency'],
172
+ })
173
+ break // one violation per non-goal entry
174
+ }
175
+ }
176
+ }
177
+
178
+ return { issues, confirmed_strong_fields: 0 }
179
+ }
180
+ }
@@ -0,0 +1,107 @@
1
+ import { SealIssue, makeIssue } from '../types.js'
2
+
3
+ export interface LockedCriterion {
4
+ id: string
5
+ text: string
6
+ source_file?: string
7
+ source_line?: number
8
+ }
9
+
10
+ export interface CriteriaCoverageInput {
11
+ artifact: string
12
+ artifact_file?: string
13
+ locked_criteria: LockedCriterion[]
14
+ }
15
+
16
+ export interface CriteriaCoverageResult {
17
+ issues: SealIssue[]
18
+ confirmed_strong_fields: number // covered ACs → +5 TrustScore each (capped externally)
19
+ no_criteria_escalate: boolean // true → no locked criteria found → ESCALATE_TO_HUMAN
20
+ }
21
+
22
+ function isCovered(criterion: LockedCriterion, artifact: string): boolean {
23
+ const lower = artifact.toLowerCase()
24
+
25
+ // Direct AC id mention
26
+ if (lower.includes(criterion.id.toLowerCase())) return true
27
+
28
+ // Keyword coverage: extract significant words (>4 chars) from criterion text
29
+ const words = criterion.text
30
+ .toLowerCase()
31
+ .replace(/[^\w\s]/g, ' ')
32
+ .split(/\s+/)
33
+ .filter(w => w.length > 4)
34
+ .filter(w => !['shall', 'must', 'should', 'when', 'then', 'given', 'that', 'this', 'with'].includes(w))
35
+
36
+ if (words.length === 0) return false
37
+
38
+ // Require at least half of significant words present in artifact
39
+ const matched = words.filter(w => lower.includes(w)).length
40
+ return matched / words.length >= 0.5
41
+ }
42
+
43
+ export class CriteriaCoverageDetector {
44
+ static detect(input: CriteriaCoverageInput): CriteriaCoverageResult {
45
+ const { artifact, artifact_file = 'artifact', locked_criteria } = input
46
+
47
+ // Preflight: no locked criteria → ESCALATE
48
+ if (!locked_criteria || locked_criteria.length === 0) {
49
+ return {
50
+ issues: [{
51
+ ...makeIssue({
52
+ type: 'AMBIGUITY',
53
+ severity: 'MEDIUM', // non-blocking — escalation comes from required_verdict, not severity
54
+ layer: 'L1',
55
+ source: 'core',
56
+ rule_id: 'PR002',
57
+ required_verdict: 'ESCALATE_TO_HUMAN',
58
+ trust_deduction: 15,
59
+ evidence: 'No locked acceptance criteria — cannot measure coverage against fixed yardstick',
60
+ suggested_fix: 'Lock criteria in intent.md or self-check.md BEFORE generating the artifact',
61
+ }),
62
+ policy_tags: ['coverage'],
63
+ confidence: 1.0,
64
+ }],
65
+ confirmed_strong_fields: 0,
66
+ no_criteria_escalate: true,
67
+ }
68
+ }
69
+
70
+ const issues: SealIssue[] = []
71
+ let confirmed = 0
72
+
73
+ for (const criterion of locked_criteria) {
74
+ if (isCovered(criterion, artifact)) {
75
+ confirmed++
76
+ continue
77
+ }
78
+
79
+ // Uncovered → SPEC_MISMATCH, Type H
80
+ const sourceFile = criterion.source_file ?? 'intent.md'
81
+ const sourceLine = criterion.source_line ?? 1
82
+ const quote = `${criterion.id} ${criterion.text.length > 120 ? criterion.text.slice(0, 120) + '…' : criterion.text} — no artifact section addresses this`
83
+
84
+ issues.push({
85
+ ...makeIssue({
86
+ type: 'SPEC_MISMATCH',
87
+ severity: 'MEDIUM',
88
+ layer: 'L1',
89
+ source: 'core',
90
+ rule_id: 'PR002',
91
+ required_verdict: 'REVISE',
92
+ trust_deduction: 10,
93
+ evidence: `Uncovered criterion: ${criterion.id} — ${criterion.text.slice(0, 80)}`,
94
+ suggested_fix: `Add a section in ${artifact_file} addressing: ${criterion.text.slice(0, 80)}`,
95
+ }),
96
+ evidence_file: sourceFile,
97
+ evidence_lines: [sourceLine],
98
+ evidence_quote: quote,
99
+ criterion_ref: criterion.id,
100
+ confidence: 0.8,
101
+ policy_tags: ['coverage'],
102
+ })
103
+ }
104
+
105
+ return { issues, confirmed_strong_fields: confirmed, no_criteria_escalate: false }
106
+ }
107
+ }
@@ -0,0 +1,98 @@
1
+ import { EvidenceEnvelope, EvidenceResult, CitationStatus } from '../types.js'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+
4
+ export type EvidenceMode = 'portable' | 'filesystem'
5
+
6
+ export class EvidenceChecker {
7
+ static check(references: EvidenceEnvelope[], options: { mode: EvidenceMode } = { mode: 'portable' }): EvidenceResult[] {
8
+ return references.map(envelope => this.checkOne(envelope, options.mode))
9
+ }
10
+
11
+ private static checkOne(envelope: EvidenceEnvelope, mode: EvidenceMode): EvidenceResult {
12
+ // Structural validation
13
+ if (envelope.type === 'file') {
14
+ if (!envelope.path || envelope.line === undefined || !envelope.snapshot) {
15
+ return {
16
+ envelope, structurally_valid: false, filesystem_verified: null,
17
+ mismatch_detail: 'Missing path, line, or snapshot',
18
+ citation_status: 'not_applicable',
19
+ }
20
+ }
21
+ if (mode === 'filesystem') {
22
+ if (!existsSync(envelope.path)) {
23
+ return {
24
+ envelope, structurally_valid: true, filesystem_verified: false,
25
+ mismatch_detail: `File not found: ${envelope.path}`,
26
+ citation_status: 'phantom',
27
+ }
28
+ }
29
+ const lines = readFileSync(envelope.path, 'utf-8').split('\n')
30
+ const actualLine = lines[envelope.line - 1] ?? ''
31
+ if (actualLine.trim() !== envelope.snapshot.trim()) {
32
+ return {
33
+ envelope, structurally_valid: true, filesystem_verified: false,
34
+ mismatch_detail: `Snapshot mismatch at ${envelope.path}:${envelope.line}`,
35
+ citation_status: 'void',
36
+ }
37
+ }
38
+ return {
39
+ envelope, structurally_valid: true, filesystem_verified: true,
40
+ citation_status: 'verified',
41
+ }
42
+ }
43
+ // Portable mode: structurally valid but no filesystem check
44
+ return {
45
+ envelope, structurally_valid: true, filesystem_verified: null,
46
+ citation_status: 'not_applicable',
47
+ }
48
+ }
49
+
50
+ if (envelope.type === 'command') {
51
+ const hasRequiredFields = !!(envelope.command && envelope.output && envelope.exit_code !== undefined)
52
+ if (!hasRequiredFields) {
53
+ return {
54
+ envelope, structurally_valid: false, filesystem_verified: null,
55
+ mismatch_detail: 'Missing command, exit_code, or output',
56
+ citation_status: 'not_applicable',
57
+ }
58
+ }
59
+ const passed = envelope.exit_code === 0
60
+ return {
61
+ envelope, structurally_valid: true, filesystem_verified: passed,
62
+ mismatch_detail: passed ? undefined : `Command failed with exit_code ${envelope.exit_code}`,
63
+ citation_status: passed ? 'verified' : 'void',
64
+ }
65
+ }
66
+
67
+ if (envelope.type === 'url') {
68
+ const valid = !!(envelope.url && envelope.retrieved_at && envelope.content_snapshot)
69
+ return {
70
+ envelope, structurally_valid: valid, filesystem_verified: null,
71
+ citation_status: valid ? 'verified' : 'not_applicable',
72
+ mismatch_detail: valid ? undefined : 'Missing url, retrieved_at, or content_snapshot',
73
+ }
74
+ }
75
+
76
+ if (envelope.type === 'text') {
77
+ const valid = !!(envelope.label && envelope.content)
78
+ return {
79
+ envelope, structurally_valid: valid, filesystem_verified: null,
80
+ citation_status: valid ? 'verified' : 'not_applicable',
81
+ }
82
+ }
83
+
84
+ if (envelope.type === 'memory') {
85
+ const valid = !!(envelope.memory_key && envelope.retrieved_at && envelope.content_snapshot)
86
+ return {
87
+ envelope, structurally_valid: valid, filesystem_verified: null,
88
+ citation_status: valid ? 'verified' : 'not_applicable',
89
+ }
90
+ }
91
+
92
+ return {
93
+ envelope, structurally_valid: false, filesystem_verified: null,
94
+ mismatch_detail: 'Unknown envelope type',
95
+ citation_status: 'not_applicable',
96
+ }
97
+ }
98
+ }