Module: Insika::Refinement::CandidateBuilder

Defined in:
lib/insika/refinement/candidate.rb

Class Method Summary collapse

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 (§3.1/§3.8), 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