Class: Insika::Commands::GateHarvest

Inherits:
Object
  • Object
show all
Defined in:
lib/insika/commands/gate_harvest.rb

Overview

— the double gate on ONE candidate, in order: eval first (expensive — a full golden replay), conversion second (cheap — a fold read). Synchronous control command (the Studio dispatches it; the gate is minutes of real replay, exactly like gate_refinement). It records both reports on the candidate and moves it: both passed -> awaiting_approval; either failed -> rejected with the reason (a failed candidate is terminal — the same finding must re-surface with new evidence, no silent retry).

Instance Method Summary collapse

Constructor Details

#initialize(harvest_store:, gate:, conversion_gate:, criterion: nil, event_stream:) ⇒ GateHarvest

Returns a new instance of GateHarvest.



16
17
18
19
20
21
22
# File 'lib/insika/commands/gate_harvest.rb', line 16

def initialize(harvest_store:, gate:, conversion_gate:, criterion: nil, event_stream:)
  @harvest_store = harvest_store
  @gate = gate
  @conversion_gate = conversion_gate
  @criterion = criterion
  @event_stream = event_stream
end

Instance Method Details

#call(command) ⇒ Object

payload: { candidate_id: } (tenant from command.meta — WS1) -> Candidate (awaiting_approval | rejected | gated-parked; both reports attached)



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/insika/commands/gate_harvest.rb', line 27

def call(command)
  p = AgentPayload.symbolize(command.payload)
  candidate_id = AgentPayload.presence(p[:candidate_id])
  raise Insika::ValidationError, "candidate_id is required" if candidate_id.nil?

  candidate = @harvest_store.find_candidate(candidate_id) ||
              (raise Insika::NotFoundError, "harvest candidate not found: #{candidate_id}")

  # The base graph wires NO gates (the deployment owns the eval surface):
  # the candidate parks at gated with the named reason — refuse-with-a-
  # named-reason, never pass, never crash.
  if @gate.nil? || @conversion_gate.nil?
    parked = @harvest_store.attach_gate(
      candidate.id,
      eval_gate: { "passed" => false, "reason" => "no_eval_gate_wired" },
      conversion_gate: { "passed" => false, "reason" => "no_conversion_gate_wired" },
      criterion_sha: nil
    )
    return parked
  end

  # The mining budget is read HERE too (the review fix): once the mining
  # pass spent the pack's cap, the expensive golden replay is refused
  # with the named reason — the Refinement::Budget discipline ("not
  # gated — the budget was spent"), applied before the provider bill.
  run = @harvest_store.find_run(candidate.run_id)
  cap = run && run.budget && run.budget["tokens"].to_i
  spent = run && run.cost && run.cost["spent"].to_i
  if cap&.positive? && spent.to_i >= cap
    parked = @harvest_store.attach_gate(
      candidate.id,
      eval_gate: { "passed" => false, "reason" => "budget_exceeded",
                   "spent" => spent, "tokens" => cap },
      conversion_gate: { "passed" => false, "reason" => "budget_exceeded" },
      criterion_sha: nil
    )
    return parked
  end

  skill = { "name" => candidate.name, "description" => candidate.description,
            "body" => candidate.body, "triggers" => candidate.triggers }
  eval_report = @gate.score(agent_id: candidate.agent, skill: skill,
                            run_id: candidate.run_id)
  conversion_report = @conversion_gate.call(tenant: command.meta[:tenant],
                                            agent: candidate.agent)
  criterion_sha = @criterion ? @criterion.sha : nil

  # A conversion REFUSAL (missing data — :no_frozen_baseline etc.) is NOT
  # a candidate failure: it parks the candidate at gated with both
  # reports and the named reason — the page shows the ruler's hole.
  # Only a conversion FAILURE (current worse than baseline) rejects.
  if refused?(conversion_report)
    gated = @harvest_store.attach_gate(candidate.id,
                                       eval_gate: eval_report.to_h,
                                       conversion_gate: conversion_report.to_h,
                                       criterion_sha: criterion_sha)
    emit(:harvest_gated, candidate, gated, eval_report, conversion_report)
    return gated
  end

  passed = eval_report.passed && conversion_report.passed
  @harvest_store.attach_gate(candidate.id,
                             eval_gate: eval_report.to_h,
                             conversion_gate: conversion_report.to_h,
                             criterion_sha: criterion_sha)
  result = if passed
             @harvest_store.mark_awaiting(candidate.id)
           else
             reason = eval_report.passed ? conversion_report.reason : eval_report.reason
             @harvest_store.mark_rejected(candidate.id, operator: "gate", note: reason)
           end
  emit(:harvest_gated, candidate, result, eval_report, conversion_report)
  result
end