lex-synapse 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4373abd3a8ec4e508cd8d276b77fc185b667c51e7a54f389313c8b2aa3f353c4
4
- data.tar.gz: 97df6f4f7fbef9db59155ef5aeea11636ad0c58fb82bc30a38c1f71207db39bf
3
+ metadata.gz: 4a9d8a3b5e13c6470c2170ca639e04e1fc8aedc0d596ff709ec9324334374e51
4
+ data.tar.gz: ecc92ce72c7ec61129495e1b290dbfb1e5df7353c6ce8b9a9ef974f146fbfa9f
5
5
  SHA512:
6
- metadata.gz: e36c0ef72aa48a0315c12868ec48602ca0b91adf46c842e75da5279799daaefb49b4c80bf2d28f6af8c4ef8b6db334d491e98bf33c9877f32809bafdf0b97c56
7
- data.tar.gz: 8570e52cee80e41d5dfc0a1c2de4a642210c471d8a4f59386e87d687815804fc22e7d73c8de1d43c31a58263876d8f44f3e3515c12e4861b6dfd1447d4414eec
6
+ metadata.gz: bf1e6f5ffe926a283d8e5278cc12546e57e3183b8893604c7b20ca81a57e2b5b571b28ce6448370e4e24576ff62dc55916a5080fd864e67e3814951f93b5ed69
7
+ data.tar.gz: 7995dee982eda776e8f847ba73202131a5c5fb01f0198193ef9dd5f3bed7d45677be196a76a65b19d4518bd3f41265ddb5622c587cced160cd94164d59771793
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] - 2026-03-22
4
+
5
+ ### Fixed
6
+ - Challenge and Propose actors now include their runner modules and override `runner_class` to return `self.class`, fixing `NoMethodError: undefined method 'run_challenge_cycle'` at runtime
7
+
8
+ ## [0.4.0] - 2026-03-22
9
+
10
+ ### Added
11
+ - Adversarial challenge phase for proposals: conflict detection, LLM challenge, weighted aggregation
12
+ - `synapse_challenges` table (migration 005) for per-challenge verdict tracking
13
+ - `Runners::Challenge` with challenge_proposal, resolve_challenge_outcomes, run_challenge_cycle
14
+ - `Helpers::Challenge` with settings, constants, impact threshold helpers
15
+ - `Actors::Challenge` polling every 60s for pending proposals
16
+ - Challenger confidence tracking with outcome-based learning loop
17
+ - Auto-accept/auto-reject thresholds for unanimous verdicts
18
+ - Client methods: challenge_proposal, challenges, challenger_stats
19
+ - New proposal statuses: auto_accepted, auto_rejected
20
+ - Impact scoring gates LLM challenge (expensive calls only for high-impact proposals)
21
+
3
22
  ## [0.3.2] - 2026-03-21
4
23
 
5
24
  ### Fixed
data/CLAUDE.md CHANGED
@@ -10,7 +10,7 @@ Cognitive routing layer that wraps task chain relationships with observation, le
10
10
 
11
11
  **GitHub**: https://github.com/LegionIO/lex-synapse
12
12
  **License**: MIT
13
- **Version**: 0.3.0
13
+ **Version**: 0.4.0
14
14
 
15
15
  ## Architecture
16
16
 
@@ -22,7 +22,8 @@ Legion::Extensions::Synapse
22
22
  │ ├── Crystallize # Every 300s — emergent synapse detection
23
23
  │ ├── Homeostasis # Every 30s — spike/drought monitoring
24
24
  │ ├── Decay # Every 3600s — idle confidence decay
25
- └── Propose # Every 300s — proactive proposal analysis for AUTONOMOUS synapses
25
+ ├── Propose # Every 300s — proactive proposal analysis for AUTONOMOUS synapses
26
+ │ └── Challenge # Every 60s — adversarial challenge pipeline for pending proposals
26
27
  ├── Runners/
27
28
  │ ├── Evaluate # attention -> transform -> route -> record
28
29
  │ ├── Pain # failure recording, confidence hit, auto-revert
@@ -34,14 +35,16 @@ Legion::Extensions::Synapse
34
35
  │ ├── GaiaReport # GAIA tick hook: report confidence and health per synapse
35
36
  │ ├── Promote # Apollo integration: promote high-confidence synapse patterns to shared knowledge
36
37
  │ ├── Retrieve # Apollo integration: retrieve relevant synapse patterns from shared knowledge
37
- └── Propose # reactive (signal-driven) + proactive (periodic) proposal generation
38
+ ├── Propose # reactive (signal-driven) + proactive (periodic) proposal generation
39
+ │ └── Challenge # conflict detection, LLM challenge, weighted aggregation, outcome resolution
38
40
  ├── Helpers/
39
41
  │ ├── Confidence # scoring, adjustments, autonomy ranges, decay
40
42
  │ ├── Homeostasis # spike/drought detection, baseline tracking
41
- └── RelationshipWrapper # Layer 1 -> Layer 2 wrapping
43
+ ├── RelationshipWrapper # Layer 1 -> Layer 2 wrapping
44
+ │ └── Challenge # settings, constants, impact threshold helpers
42
45
  ├── Data/
43
- │ ├── Migrations/ # 001 synapses, 002 mutations, 003 signals
44
- │ └── Models/ # Synapse, SynapseMutation, SynapseSignal
46
+ │ ├── Migrations/ # 001 synapses, 002 mutations, 003 signals, 004 proposals, 005 challenges
47
+ │ └── Models/ # Synapse, SynapseMutation, SynapseSignal, SynapseProposal, SynapseChallenge
45
48
  ├── Transport/
46
49
  │ ├── Exchanges/Synapse
47
50
  │ ├── Queues/Evaluate, Pain
@@ -80,6 +83,8 @@ Legion::Extensions::Synapse
80
83
  - **synapses**: Core routing definition with confidence, status, version, baseline_throughput
81
84
  - **synapse_mutations**: Versioned change history with before/after JSON snapshots
82
85
  - **synapse_signals**: Per-signal outcome records (attention pass, transform success, latency, downstream outcome)
86
+ - **synapse_proposals**: Proposed changes with status lifecycle, challenge_state, challenge_score, impact_score
87
+ - **synapse_challenges**: Per-challenge verdicts (conflict/LLM), confidence tracking, outcome resolution
83
88
 
84
89
  ## Autonomous Observation Mode (v0.3.0)
85
90
 
@@ -91,6 +96,18 @@ Legion::Extensions::Synapse
91
96
  - **Data**: `synapse_proposals` table with status lifecycle (pending -> approved/rejected/applied/expired)
92
97
  - **Client methods**: `proposals(synapse_id:, status:)`, `review_proposal(proposal_id:, status:)`
93
98
 
99
+ ## Adversarial Challenge Phase (v0.4.0)
100
+
101
+ - **Challenge pipeline**: pending proposals go through conflict detection (mechanical) -> impact scoring -> LLM challenge (gated) -> weighted aggregation -> auto-accept/reject/await-review
102
+ - **Conflict detection**: queries sibling pending proposals on same synapse; conflicting types produce 'challenge' verdict
103
+ - **LLM challenge**: gated by `impact_score >= 0.3`; calls lex-transformer LLM engine; parses SUPPORT/CHALLENGE/ABSTAIN
104
+ - **Aggregation**: `support_weight / (support_weight + challenge_weight)`, abstains excluded; >= 0.85 auto-accepts, <= 0.15 auto-rejects
105
+ - **Challenger confidence**: starting 0.5, correct +0.05, incorrect -0.08, decay *0.998/hr; tracks via outcome resolution after observation window (50 signals post-application)
106
+ - **Settings**: `lex-synapse.challenge.*` — enabled, impact_threshold, auto_accept_threshold, auto_reject_threshold, llm_engine_options, outcome_observation_window, max_per_cycle
107
+ - **Data**: `synapse_challenges` table; `synapse_proposals` gains challenge_state, challenge_score, impact_score columns
108
+ - **Statuses**: `auto_accepted`, `auto_rejected` added to proposal lifecycle
109
+ - **Client methods**: `challenge_proposal(proposal_id:)`, `challenges(proposal_id:)`, `challenger_stats`
110
+
94
111
  ## GAIA / Apollo Integration (v0.2.2)
95
112
 
96
113
  - **GaiaReport runner**: Called during the GAIA tick cycle to report per-synapse confidence and health metrics.
@@ -110,11 +127,11 @@ Legion::Extensions::Synapse
110
127
 
111
128
  ```bash
112
129
  bundle install
113
- bundle exec rspec # 353 specs, 0 failures
130
+ bundle exec rspec # 412 specs, 0 failures
114
131
  bundle exec rubocop # 0 offenses
115
132
  ```
116
133
 
117
- 353 specs, 95%+ coverage. Uses in-memory SQLite for model/runner tests.
134
+ 412 specs, 94%+ coverage. Uses in-memory SQLite for model/runner tests.
118
135
 
119
136
  ---
120
137
 
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../runners/challenge'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Synapse
8
+ module Actor
9
+ class Challenge < Legion::Extensions::Actors::Every
10
+ include Legion::Extensions::Synapse::Runners::Challenge
11
+
12
+ def runner_class = self.class
13
+
14
+ def runner_function
15
+ 'run_challenge_cycle'
16
+ end
17
+
18
+ def time
19
+ 60
20
+ end
21
+
22
+ def use_runner?
23
+ false
24
+ end
25
+
26
+ def check_subtask?
27
+ false
28
+ end
29
+
30
+ def generate_task?
31
+ false
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,10 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../runners/propose'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Synapse
6
8
  module Actor
7
9
  class Propose < Legion::Extensions::Actors::Every
10
+ include Legion::Extensions::Synapse::Runners::Propose
11
+
12
+ def runner_class = self.class
13
+
8
14
  def runner_function
9
15
  'propose_proactive'
10
16
  end
@@ -11,6 +11,7 @@ require_relative 'runners/dream'
11
11
  require_relative 'runners/promote'
12
12
  require_relative 'runners/retrieve'
13
13
  require_relative 'runners/propose'
14
+ require_relative 'runners/challenge'
14
15
  require_relative 'data/models/synapse_proposal'
15
16
  require_relative 'helpers/proposals'
16
17
 
@@ -29,6 +30,7 @@ module Legion
29
30
  include Runners::Promote
30
31
  include Runners::Retrieve
31
32
  include Runners::Propose
33
+ include Runners::Challenge
32
34
 
33
35
  attr_reader :conditioner_client, :transformer_client
34
36
 
@@ -79,6 +81,25 @@ module Legion
79
81
  proposal.update(status: status, reviewed_at: Time.now)
80
82
  { success: true, proposal_id: proposal_id, status: status }
81
83
  end
84
+
85
+ def challenge_proposal(proposal_id:)
86
+ super(proposal_id: proposal_id, transformer_client: @transformer_client)
87
+ end
88
+
89
+ def challenges(proposal_id:)
90
+ Data::Model.define_synapse_challenge_model
91
+ Data::Model::SynapseChallenge.where(proposal_id: proposal_id).order(Sequel.desc(:id)).all
92
+ end
93
+
94
+ def challenger_stats
95
+ Data::Model.define_synapse_challenge_model
96
+ resolved = Data::Model::SynapseChallenge.exclude(outcome: nil)
97
+ {
98
+ total: resolved.count,
99
+ correct: resolved.where(outcome: 'correct').count,
100
+ by_type: resolved.group_and_count(:challenger_type).to_h { |r| [r[:challenger_type], r[:count]] }
101
+ }
102
+ end
82
103
  end
83
104
  end
84
105
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ create_table(:synapse_challenges) do
6
+ primary_key :id
7
+ foreign_key :proposal_id, :synapse_proposals, null: false, index: true
8
+ String :challenger_type, null: false, size: 50
9
+ String :verdict, null: false, size: 50
10
+ String :reasoning, text: true
11
+ Float :challenger_confidence, default: 0.5
12
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
13
+ DateTime :resolved_at
14
+ String :outcome, size: 50
15
+ end
16
+
17
+ alter_table(:synapse_proposals) do
18
+ add_column :challenge_state, String, size: 50
19
+ add_column :challenge_score, Float
20
+ add_column :impact_score, Float
21
+ end
22
+ end
23
+
24
+ down do
25
+ drop_table :synapse_challenges
26
+
27
+ alter_table(:synapse_proposals) do
28
+ drop_column :challenge_state
29
+ drop_column :challenge_score
30
+ drop_column :impact_score
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Synapse
6
+ module Data
7
+ module Model
8
+ def self.define_synapse_challenge_model
9
+ return if const_defined?(:SynapseChallenge, false)
10
+ return unless defined?(Legion::Data) && Legion::Settings.dig(:data, :connected)
11
+
12
+ db = Sequel::Model.db
13
+ return unless db&.table_exists?(:synapse_challenges)
14
+
15
+ klass = Class.new(Sequel::Model(:synapse_challenges)) do
16
+ many_to_one :proposal, class: 'Legion::Extensions::Synapse::Data::Model::SynapseProposal',
17
+ key: :proposal_id
18
+ end
19
+ klass.set_primary_key :id
20
+ const_set(:SynapseChallenge, klass)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Synapse
6
+ module Helpers
7
+ module Challenge
8
+ VALID_VERDICTS = %w[support challenge abstain].freeze
9
+ VALID_CHALLENGER_TYPES = %w[conflict llm].freeze
10
+ VALID_OUTCOMES = %w[correct incorrect].freeze
11
+ VALID_CHALLENGE_STATES = %w[challenging challenged].freeze
12
+
13
+ IMPACT_WEIGHTS = {
14
+ 'llm_transform' => 0.7,
15
+ 'transform_mutation' => 0.5,
16
+ 'attention_mutation' => 0.6,
17
+ 'route_change' => 0.8
18
+ }.freeze
19
+
20
+ DEFAULT_SETTINGS = {
21
+ enabled: true,
22
+ impact_threshold: 0.3,
23
+ auto_accept_threshold: 0.85,
24
+ auto_reject_threshold: 0.15,
25
+ llm_engine_options: { temperature: 0.2, max_tokens: 512 },
26
+ outcome_observation_window: 50,
27
+ max_per_cycle: 5,
28
+ challenger_starting_confidence: 0.5,
29
+ challenger_correct_adjustment: 0.05,
30
+ challenger_incorrect_adjustment: -0.08
31
+ }.freeze
32
+
33
+ class << self
34
+ def settings
35
+ raw = Legion::Settings.dig('lex-synapse', 'challenge')
36
+ return DEFAULT_SETTINGS.dup unless raw.is_a?(Hash)
37
+
38
+ merged = DEFAULT_SETTINGS.dup
39
+ raw.each { |k, v| merged[k.to_sym] = v unless v.nil? }
40
+ merged
41
+ end
42
+
43
+ def enabled?
44
+ settings[:enabled] == true
45
+ end
46
+
47
+ def above_impact_threshold?(impact_score)
48
+ impact_score >= settings[:impact_threshold]
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -7,7 +7,7 @@ module Legion
7
7
  module Proposals
8
8
  VALID_PROPOSAL_TYPES = %w[llm_transform attention_mutation transform_mutation route_change].freeze
9
9
  VALID_TRIGGERS = %w[reactive proactive].freeze
10
- VALID_STATUSES = %w[pending approved rejected applied expired].freeze
10
+ VALID_STATUSES = %w[pending challenging challenged approved rejected applied expired auto_accepted auto_rejected].freeze
11
11
 
12
12
  DEFAULT_SETTINGS = {
13
13
  enabled: true,
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/challenge'
4
+ require_relative '../helpers/confidence'
5
+ require_relative '../data/models/synapse'
6
+ require_relative '../data/models/synapse_proposal'
7
+ require_relative '../data/models/synapse_challenge'
8
+ require_relative '../data/models/synapse_signal'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module Synapse
13
+ module Runners
14
+ module Challenge
15
+ def pending_challenges
16
+ Data::Model.define_synapse_proposal_model
17
+ Data::Model::SynapseProposal.where(status: 'pending', challenge_state: nil)
18
+ .order(Sequel.asc(:id)).all
19
+ end
20
+
21
+ def challenge_proposal(proposal_id:, transformer_client: nil)
22
+ Data::Model.define_synapse_proposal_model
23
+ Data::Model.define_synapse_challenge_model
24
+ Data::Model.define_synapse_model
25
+ Data::Model.define_synapse_signal_model
26
+
27
+ return { success: true, skipped: true } unless Helpers::Challenge.enabled?
28
+
29
+ proposal = Data::Model::SynapseProposal[proposal_id]
30
+ return { success: false, error: 'proposal not found' } unless proposal
31
+ return { success: false, error: 'proposal not challengeable' } unless proposal.status == 'pending' && proposal.challenge_state.nil?
32
+
33
+ synapse = Data::Model::Synapse[proposal.synapse_id]
34
+ return { success: false, error: 'synapse not found' } unless synapse
35
+
36
+ proposal.update(challenge_state: 'challenging')
37
+
38
+ impact = calculate_impact_score(proposal, synapse)
39
+ proposal.update(impact_score: impact)
40
+
41
+ conflict_check(proposal)
42
+
43
+ llm_challenge(proposal, synapse, transformer_client) if Helpers::Challenge.above_impact_threshold?(impact) && transformer_client
44
+
45
+ aggregate_challenges(proposal)
46
+ end
47
+
48
+ def resolve_challenge_outcomes(proposal_id:)
49
+ Data::Model.define_synapse_proposal_model
50
+ Data::Model.define_synapse_challenge_model
51
+ Data::Model.define_synapse_signal_model
52
+
53
+ proposal = Data::Model::SynapseProposal[proposal_id]
54
+ return { success: false, error: 'proposal not found' } unless proposal
55
+ return { success: false, error: 'proposal not applied' } unless proposal.status == 'applied'
56
+
57
+ settings = Helpers::Challenge.settings
58
+ window = settings[:outcome_observation_window] || 50
59
+
60
+ signals = Data::Model::SynapseSignal.where(synapse_id: proposal.synapse_id)
61
+ .order(Sequel.desc(:id)).limit(window).all
62
+ return { success: false, error: 'insufficient signals' } if signals.size < window
63
+
64
+ success_rate = signals.count(&:transform_success).to_f / signals.size
65
+ proposal_succeeded = success_rate >= 0.7
66
+
67
+ challenges = Data::Model::SynapseChallenge.where(proposal_id: proposal_id)
68
+ .exclude(verdict: 'abstain').all
69
+
70
+ challenges.each do |challenge|
71
+ supported = challenge.verdict == 'support'
72
+ correct = (supported && proposal_succeeded) || (!supported && !proposal_succeeded)
73
+ outcome = correct ? 'correct' : 'incorrect'
74
+
75
+ adj = correct ? settings[:challenger_correct_adjustment] : settings[:challenger_incorrect_adjustment]
76
+ new_conf = (challenge.challenger_confidence + adj).clamp(0.0, 1.0)
77
+
78
+ challenge.update(outcome: outcome, challenger_confidence: new_conf, resolved_at: Time.now)
79
+ end
80
+
81
+ { success: true, proposal_id: proposal_id, success_rate: success_rate, resolved: challenges.size }
82
+ end
83
+
84
+ def run_challenge_cycle(transformer_client: nil)
85
+ Data::Model.define_synapse_proposal_model
86
+ Data::Model.define_synapse_challenge_model
87
+ return { challenged: 0, resolved: 0 } unless Helpers::Challenge.enabled?
88
+
89
+ settings = Helpers::Challenge.settings
90
+ max = settings[:max_per_cycle] || 5
91
+
92
+ challenged = 0
93
+ pending_challenges.first(max).each do |proposal|
94
+ challenge_proposal(proposal_id: proposal.id, transformer_client: transformer_client)
95
+ challenged += 1
96
+ end
97
+
98
+ resolved = 0
99
+ window = settings[:outcome_observation_window] || 50
100
+ Data::Model.define_synapse_signal_model
101
+ Data::Model::SynapseProposal.where(status: 'applied').each do |proposal|
102
+ cutoff = proposal.respond_to?(:reviewed_at) && proposal.reviewed_at ? proposal.reviewed_at : proposal.created_at
103
+ post_signals = Data::Model::SynapseSignal.where(synapse_id: proposal.synapse_id)
104
+ .where { created_at >= cutoff }.count
105
+ next unless post_signals >= window
106
+
107
+ unresolved = Data::Model::SynapseChallenge.where(proposal_id: proposal.id, outcome: nil)
108
+ .exclude(verdict: 'abstain')
109
+ next unless unresolved.any?
110
+
111
+ resolve_challenge_outcomes(proposal_id: proposal.id)
112
+ resolved += 1
113
+ end
114
+
115
+ { challenged: challenged, resolved: resolved }
116
+ end
117
+
118
+ private
119
+
120
+ def conflict_check(proposal)
121
+ conflicts = Data::Model::SynapseProposal.where(
122
+ synapse_id: proposal.synapse_id,
123
+ proposal_type: proposal.proposal_type,
124
+ status: 'pending'
125
+ ).exclude(id: proposal.id)
126
+
127
+ if conflicts.any?
128
+ Data::Model::SynapseChallenge.create(
129
+ proposal_id: proposal.id, challenger_type: 'conflict',
130
+ verdict: 'challenge',
131
+ reasoning: "#{conflicts.count} conflicting #{proposal.proposal_type} proposal(s) pending on same synapse",
132
+ challenger_confidence: Helpers::Challenge.settings[:challenger_starting_confidence]
133
+ )
134
+ else
135
+ Data::Model::SynapseChallenge.create(
136
+ proposal_id: proposal.id, challenger_type: 'conflict',
137
+ verdict: 'support', reasoning: 'no conflicting proposals',
138
+ challenger_confidence: Helpers::Challenge.settings[:challenger_starting_confidence]
139
+ )
140
+ end
141
+ end
142
+
143
+ def llm_challenge(proposal, synapse, transformer_client)
144
+ prompt = build_challenge_prompt(proposal, synapse)
145
+ engine_options = Helpers::Challenge.settings[:llm_engine_options]
146
+
147
+ result = transformer_client.transform(
148
+ transformation: prompt, payload: {}, engine: :llm, engine_options: engine_options
149
+ )
150
+
151
+ verdict, reasoning = parse_llm_verdict(result[:success] ? result[:result] : nil)
152
+
153
+ llm_confidence = rolling_llm_confidence
154
+ Data::Model::SynapseChallenge.create(
155
+ proposal_id: proposal.id, challenger_type: 'llm',
156
+ verdict: verdict, reasoning: reasoning,
157
+ challenger_confidence: llm_confidence
158
+ )
159
+ rescue StandardError => e
160
+ Legion::Logging.warn("Challenge LLM call failed: #{e.message}") if defined?(Legion::Logging)
161
+ Data::Model::SynapseChallenge.create(
162
+ proposal_id: proposal.id, challenger_type: 'llm',
163
+ verdict: 'abstain', reasoning: "LLM error: #{e.message}",
164
+ challenger_confidence: Helpers::Challenge.settings[:challenger_starting_confidence]
165
+ )
166
+ end
167
+
168
+ def aggregate_challenges(proposal)
169
+ challenges = Data::Model::SynapseChallenge.where(proposal_id: proposal.id)
170
+ .exclude(verdict: 'abstain').all
171
+
172
+ if challenges.empty?
173
+ proposal.update(challenge_state: 'challenged', challenge_score: 0.5)
174
+ return { success: true, challenge_score: 0.5, decision: 'challenged' }
175
+ end
176
+
177
+ support_weight = challenges.select { |c| c.verdict == 'support' }.sum(&:challenger_confidence)
178
+ challenge_weight = challenges.select { |c| c.verdict == 'challenge' }.sum(&:challenger_confidence)
179
+ total = support_weight + challenge_weight
180
+
181
+ score = total.zero? ? 0.5 : support_weight / total
182
+
183
+ settings = Helpers::Challenge.settings
184
+ decision = if score >= settings[:auto_accept_threshold]
185
+ proposal.update(status: 'auto_accepted', challenge_state: 'challenged', challenge_score: score)
186
+ 'auto_accepted'
187
+ elsif score <= settings[:auto_reject_threshold]
188
+ proposal.update(status: 'auto_rejected', challenge_state: 'challenged', challenge_score: score)
189
+ 'auto_rejected'
190
+ else
191
+ proposal.update(challenge_state: 'challenged', challenge_score: score)
192
+ 'challenged'
193
+ end
194
+
195
+ { success: true, challenge_score: score, decision: decision }
196
+ end
197
+
198
+ def calculate_impact_score(proposal, synapse)
199
+ base = Helpers::Challenge::IMPACT_WEIGHTS.fetch(proposal.proposal_type, 0.5)
200
+ recent_signals = Data::Model::SynapseSignal.where(synapse_id: synapse.id).count
201
+ baseline = [synapse.respond_to?(:baseline_throughput) && synapse.baseline_throughput ? synapse.baseline_throughput : 1.0, 1.0].max
202
+ throughput_factor = [recent_signals.to_f / baseline, 2.0].min
203
+
204
+ (base * synapse.confidence * throughput_factor).clamp(0.0, 1.0)
205
+ end
206
+
207
+ def build_challenge_prompt(proposal, synapse)
208
+ "Evaluate this proposed change to a cognitive routing synapse.\n\n" \
209
+ "Synapse confidence: #{synapse.confidence}\n" \
210
+ "Proposal type: #{proposal.proposal_type}\n" \
211
+ "Rationale: #{proposal.rationale}\n" \
212
+ "Proposed inputs: #{proposal.inputs}\n" \
213
+ "Proposed output: #{proposal.output}\n\n" \
214
+ "Is this change sound? Respond in exactly this format:\n" \
215
+ "VERDICT: SUPPORT or CHALLENGE or ABSTAIN\n" \
216
+ 'REASONING: one sentence explanation'
217
+ end
218
+
219
+ def parse_llm_verdict(response)
220
+ return ['abstain', 'no LLM response'] unless response.is_a?(String)
221
+
222
+ text = response.to_s.strip
223
+ verdict = if text.match?(/VERDICT:\s*SUPPORT/i)
224
+ 'support'
225
+ elsif text.match?(/VERDICT:\s*CHALLENGE/i)
226
+ 'challenge'
227
+ else
228
+ 'abstain'
229
+ end
230
+
231
+ reasoning_match = text.match(/REASONING:\s*(.+)/i)
232
+ reasoning = reasoning_match ? reasoning_match[1].strip : text.slice(0, 200)
233
+
234
+ [verdict, reasoning]
235
+ end
236
+
237
+ def rolling_llm_confidence
238
+ recent = Data::Model::SynapseChallenge.where(challenger_type: 'llm')
239
+ .exclude(outcome: nil)
240
+ .order(Sequel.desc(:id)).limit(20).all
241
+ return Helpers::Challenge.settings[:challenger_starting_confidence] if recent.empty?
242
+
243
+ recent.first.challenger_confidence
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Synapse
6
- VERSION = '0.3.2'
6
+ VERSION = '0.4.1'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-synapse
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -56,6 +56,7 @@ files:
56
56
  - Rakefile
57
57
  - lex-synapse.gemspec
58
58
  - lib/legion/extensions/synapse.rb
59
+ - lib/legion/extensions/synapse/actors/challenge.rb
59
60
  - lib/legion/extensions/synapse/actors/crystallize.rb
60
61
  - lib/legion/extensions/synapse/actors/decay.rb
61
62
  - lib/legion/extensions/synapse/actors/evaluate.rb
@@ -67,14 +68,18 @@ files:
67
68
  - lib/legion/extensions/synapse/data/migrations/002_create_synapse_mutations.rb
68
69
  - lib/legion/extensions/synapse/data/migrations/003_create_synapse_signals.rb
69
70
  - lib/legion/extensions/synapse/data/migrations/004_create_synapse_proposals.rb
71
+ - lib/legion/extensions/synapse/data/migrations/005_add_synapse_challenges.rb
70
72
  - lib/legion/extensions/synapse/data/models/synapse.rb
73
+ - lib/legion/extensions/synapse/data/models/synapse_challenge.rb
71
74
  - lib/legion/extensions/synapse/data/models/synapse_mutation.rb
72
75
  - lib/legion/extensions/synapse/data/models/synapse_proposal.rb
73
76
  - lib/legion/extensions/synapse/data/models/synapse_signal.rb
77
+ - lib/legion/extensions/synapse/helpers/challenge.rb
74
78
  - lib/legion/extensions/synapse/helpers/confidence.rb
75
79
  - lib/legion/extensions/synapse/helpers/homeostasis.rb
76
80
  - lib/legion/extensions/synapse/helpers/proposals.rb
77
81
  - lib/legion/extensions/synapse/helpers/relationship_wrapper.rb
82
+ - lib/legion/extensions/synapse/runners/challenge.rb
78
83
  - lib/legion/extensions/synapse/runners/crystallize.rb
79
84
  - lib/legion/extensions/synapse/runners/dream.rb
80
85
  - lib/legion/extensions/synapse/runners/evaluate.rb