Class: Insika::Safety::GroundingEnforcer

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/safety/grounding_enforcer.rb

Overview

— the :enforce half of grounding: the claim's SENTENCE is cut from the content the turn persists and delivers. A NEW stage-8 boundary step (a plain executor call, NOT an after_task hook — after_task fires too late to change what is persisted).

The cut removes the SENTENCE that contains an ungrounded claim (never a surgical span — removing half a sentence produces broken prose). What the cut CANNOT do is unsay a streamed delta — for a streaming surface :enforce still delivers the cut text via channel delivery and flags the turn; that honesty is why the own default is :flag (ship :enforce only after the matcher audit). Runs ONLY in :enforce mode — inert otherwise.

Instance Method Summary collapse

Instance Method Details

#call(task, state, content) ⇒ Object

-> [new_content, state]. The stage-8 boundary call. Fail-open: grounding must never break a completed turn.



19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/insika/safety/grounding_enforcer.rb', line 19

def call(task, state, content)
  grounding = grounding_of(state)
  return [content, state] unless grounding&.enforce?

  ledger = state.respond_to?(:evidence_ledger) ? state.evidence_ledger : nil
  return [content, state] unless ledger

  text = content.to_s
  refs = grounding.matcher.references(text)
  claims = grounding.matcher.ungrounded(refs, evidence_ids: ledger.ids)
  return [content, state] if claims.empty?

  cut = cut_sentences(text, claims)
  claims.each { |c| ledger.ungrounded_count(c) }
  state.guardrail_flags = Array(state.guardrail_flags) + [{
    category: "ungrounded", source: "evidence", action: "cut",
    detail: "cut claim(s): #{claims.join(', ')}" }]
  [cut, state]
rescue StandardError
  [content, state] # fail-open: grounding must never break a completed turn
end