Module: Insika::Refinement::CandidateBuilder
- Defined in:
- lib/insika/refinement/candidate.rb
Class Method Summary collapse
-
.build(raw, allowlist:, contents:, limits: {}, id: nil) ⇒ Object
raw: { "proposer" =>, "rationale" =>, "edits" => [ … ] } (string keys) allowlist: the agent's
refinement.files. - .edit_field(edit, key) ⇒ Object
-
.validate(raw, allow, contents, bounds, grown, kept_count) ⇒ Object
-> [Edit, nil] | [nil, reason].
Class Method Details
.build(raw, allowlist:, contents:, limits: {}, id: nil) ⇒ Object
raw: { "proposer" =>, "rationale" =>, "edits" => [ … ] } (string keys)
allowlist: the agent's refinement.files. EMPTY MEANS NOTHING IS WRITABLE —
report-only, so every edit drops. Not "no restriction":
an unset allowlist that meant "anything" would turn a missing
config into the most permissive setting there is.
contents: name => current content, for staleness and growth.
-> Candidate (possibly empty; empty? never reaches the gate).
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/insika/refinement/candidate.rb', line 83 def build(raw, allowlist:, contents:, limits: {}, id: nil) raw = Coercion.deep_stringify(raw.is_a?(Hash) ? raw : {}) bounds = DEFAULT_LIMITS.merge(limits.is_a?(Hash) ? Coercion.deep_stringify(limits) : {}) allow = Array(allowlist).map(&:to_s) kept = [] dropped = [] # Growth is measured per FILE across the whole candidate, so three edits that # are each under the cap cannot add up to a rewritten file. grown = Hash.new(0) Array(raw["edits"]).each do |edit| built, reason = validate(Coercion.deep_stringify(edit), allow, contents, bounds, grown, kept.size) if built kept << built grown[built.file] += built.after.to_s.bytesize - (built.replace? ? built.before.to_s.bytesize : 0) else dropped << Dropped.new(file: edit_field(edit, "file"), op: edit_field(edit, "op"), reason: reason) end end Candidate.new(id: (id || SecureRandom.uuid).to_s, proposer: Coercion.presence(raw["proposer"]) || "operator", rationale: raw["rationale"].to_s, edits: kept, dropped: dropped) end |
.edit_field(edit, key) ⇒ Object
152 153 154 155 156 |
# File 'lib/insika/refinement/candidate.rb', line 152 def edit_field(edit, key) return nil unless edit.is_a?(Hash) (edit[key] || edit[key.to_sym]).to_s end |
.validate(raw, allow, contents, bounds, grown, kept_count) ⇒ Object
-> [Edit, nil] | [nil, reason]. One reason per edit, in the order an operator would ask: is it allowed, is it well-formed, does it still apply, is it small.
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/insika/refinement/candidate.rb', line 111 def validate(raw, allow, contents, bounds, grown, kept_count) file = raw["file"].to_s op = raw["op"].to_s after = raw["after"].to_s return [nil, "candidate is over max_edits (#{bounds['max_edits']})"] if kept_count >= bounds["max_edits"].to_i return [nil, "'#{file}' is not on the refinement allowlist"] unless allow.include?(file) return [nil, "unknown op '#{op}' (#{OPS.join('|')})"] unless OPS.include?(op) current = contents[file] return [nil, "'#{file}' does not exist for this agent"] if current.nil? return [nil, "'after' is empty"] if after.strip.empty? if after.bytesize > bounds["max_bytes"].to_i return [nil, "edit is #{after.bytesize}B, over max_bytes (#{bounds['max_bytes']})"] end edit = Edit.new(file: file, op: op, anchor: Coercion.presence(raw["anchor"]), before: raw["before"].to_s, after: after, addresses: Array(raw["addresses"]).map(&:to_s)) if edit.replace? return [nil, "'before' is empty — a replace with no anchor text is a rewrite"] if edit.before.empty? # STALE: the file changed since the proposal was built (or the model # hallucinated the text it claims to be replacing). Either way the edit # describes a file that does not exist, and applying it by fuzzy match is # how a refinement loop silently clobbers a human's edit. return [nil, "'before' no longer matches '#{file}' (stale or invented)"] unless current.include?(edit.before) if current.scan(edit.before).length > 1 return [nil, "'before' matches '#{file}' in #{current.scan(edit.before).length} places — ambiguous"] end end growth = grown[file] + after.bytesize - (edit.replace? ? edit.before.bytesize : 0) cap = (current.bytesize * bounds["max_total_growth"].to_f).ceil return [nil, "would grow '#{file}' by #{growth}B, over max_total_growth (#{cap}B)"] if growth > cap [edit, nil] end |