| 123 | type VoteStrength = 'block' | 'warn' | 'none'; |
| 124 | |
| 125 | function classifyTranscript(signal: LayerSignal): VoteStrength { |
| 126 | const verdict = signal.meta?.verdict as string | undefined; |
| 127 | const confidence = signal.confidence; |
| 128 | |
| 129 | if (verdict === 'block') { |
| 130 | // Hallucination guard: verdict=block with confidence < LOG_ONLY drops |
| 131 | // to warn-vote. Prevents a malformed low-confidence block from becoming |
| 132 | // authoritative. |
| 133 | return confidence >= THRESHOLDS.LOG_ONLY ? 'block' : 'warn'; |
| 134 | } |
| 135 | if (verdict === 'warn') { |
| 136 | return 'warn'; |
| 137 | } |
| 138 | if (verdict === 'safe') { |
| 139 | return 'none'; |
| 140 | } |
| 141 | // Backward-compat: signal with no meta.verdict (old tests, pre-v2 cached |
| 142 | // signals). Confidence-only fallback: warn-vote when >= WARN, else no vote. |
| 143 | // Missing meta NEVER block-votes — the old confidence-only block-vote rule |
| 144 | // is deprecated for the transcript layer. |
| 145 | if (confidence >= THRESHOLDS.WARN) return 'warn'; |
| 146 | return 'none'; |
| 147 | } |
| 148 | |
| 149 | export function combineVerdict(signals: LayerSignal[], opts: CombineVerdictOpts = {}): SecurityResult { |
| 150 | // Reduce to the strongest signal per layer. For transcript, we'll re-derive |