Class: Insika::Commands::PromoteHarvest
- Inherits:
-
Object
- Object
- Insika::Commands::PromoteHarvest
- Defined in:
- lib/insika/commands/promote_harvest.rb
Overview
— the human's answer, the ONLY path that lands a
skill. Snapshot FIRST (the pre-promotion state), then the two existing
write commands, then the append-only log row (D8). A promotion row's
rolled_back_at makes the log the single ledger; re-promotion after a
rollback is a NEW row with a NEW snapshot. The candidate flips to
promoted ONLY after the writes land (record-after).
Instance Method Summary collapse
-
#call(command) ⇒ Object
payload: { candidate_id:, operator?, note? } operator from payload || command.meta || "operator" -> Candidate (promoted | gated — the conversion re-check moved, D8-bis).
-
#initialize(harvest_store:, skill_store:, skill_catalog:, profile_source:, criterion:, conversion_gate:, event_stream:, write_skill: nil, set_skill_agents: nil) ⇒ PromoteHarvest
constructor
A new instance of PromoteHarvest.
Constructor Details
#initialize(harvest_store:, skill_store:, skill_catalog:, profile_source:, criterion:, conversion_gate:, event_stream:, write_skill: nil, set_skill_agents: nil) ⇒ PromoteHarvest
Returns a new instance of PromoteHarvest.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/insika/commands/promote_harvest.rb', line 14 def initialize(harvest_store:, skill_store:, skill_catalog:, profile_source:, criterion:, conversion_gate:, event_stream:, write_skill: nil, set_skill_agents: nil) @harvest_store = harvest_store @skill_store = skill_store @skill_catalog = skill_catalog @profiles = ProfileSource.coerce(profile_source) @criterion = criterion @conversion_gate = conversion_gate @event_stream = event_stream @write_skill = write_skill || Insika::Commands::WriteSkill.new(skill_store: skill_store, skill_catalog: skill_catalog, event_stream: event_stream) @set_skill_agents = set_skill_agents || Insika::Commands::SetSkillAgents.new(profile_source: @profiles, event_stream: event_stream) end |
Instance Method Details
#call(command) ⇒ Object
payload: { candidate_id:, operator?, note? } operator from payload || command.meta || "operator" -> Candidate (promoted | gated — the conversion re-check moved, D8-bis)
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 101 102 103 104 105 106 |
# File 'lib/insika/commands/promote_harvest.rb', line 36 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}") operator = (AgentPayload.presence(p[:operator]) || command.[:operator] || "operator").to_s # D8-bis — the conversion ruler is RE-READ at the moment the skill # lands (cheap: a fold read). A dip below the frozen baseline since # gating PARKS the candidate at gated with the current numbers — the # operator re-decides alertly. The eval gate is NOT re-run (expensive, # and the skill body is not anchored to instruction files — nothing # drifted to re-validate). A base graph without a conversion gate # refuses with the named reason — never passes, never crashes. current = if @conversion_gate @conversion_gate.call(tenant: command.[:tenant], agent: candidate.agent) else Insika::Harvest::ConversionGate::Result.new( passed: false, reason: :no_conversion_gate_wired, metric: nil, window: nil, current: nil, baseline: nil, threshold: nil, snapshot_ref: nil ) end return @harvest_store.recheck_conversion(candidate.id, conversion_gate: current.to_h) unless current.passed # The criterion must STILL be the frozen one (D8-bis): a criterion that # changed between boot and promotion is a criterion nobody froze. unless @criterion && @criterion.sha == candidate.criterion_sha raise Insika::ValidationError, "the harvest criterion changed since boot (#{candidate.criterion_sha.inspect} -> " \ "#{@criterion&.sha.inspect}) — re-gate the candidate against the new frozen file" end # Snapshot first, then the writes, then the log row, then the flip. existed = @skill_store.get(candidate.name, agent: candidate.agent) # D8 + the review fix: the snapshot records EVERY agent the skill was # enabled for BEFORE this promotion (SetSkillAgents' vocabulary). A # homonymous skill allowed by B must stay allowed when A promotes — # and the rollback restores exactly this set, B included. enabled_for = agents_with_skill(candidate.name) snapshot = @harvest_store.create_snapshot( agent: candidate.agent, skill: candidate.name, content: existed, existed: !existed.nil?, enabled_for: enabled_for ) @write_skill.call(Insika::Command.build(:write_skill, { name: candidate.name, content: candidate.body, agent: candidate.agent })) # ENABLE for the candidate's agent WITHOUT disabling anyone else who # already had it: the wanted set is the pre-promotion holders plus the # candidate. @set_skill_agents.call(Insika::Command.build(:set_skill_agents, { name: candidate.name, agent_ids: enabled_for | [candidate.agent] })) promotion = @harvest_store.append_promotion( id: SecureRandom.uuid, agent: candidate.agent, skill: candidate.name, origin: candidate.origin, eval_ref: "cand:#{candidate.id}", conversion_ref: candidate.conversion_gate && candidate.conversion_gate["snapshot_ref"], approver: operator, snapshot_ref: snapshot.id, criterion_sha: candidate.criterion_sha ) @harvest_store.mark_promoted(candidate.id, promotion_ref: promotion.id) emit(:skill_promoted, agent: candidate.agent, skill: candidate.name, candidate_id: candidate.id, snapshot_ref: snapshot.id, promotion_ref: promotion.id, approver: operator) @harvest_store.find_candidate(candidate.id) end |