lex-apollo 0.4.28 → 0.4.30

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: d25745968940593d7d1ac3fba3f1ccd1b5d7ce553cca28c1a1fdfa4d3104af11
4
- data.tar.gz: 96ecdae2a460625d95cbe0e4605f2c60778d035ea3c3f93e19d6560df0a40e70
3
+ metadata.gz: facad4c6e5cf4da1497d5d6de01daa13310bc8762274e227e9ff7ead72c761a6
4
+ data.tar.gz: 37a7098b002770c827173135a5510eeb7651f0705ef6f80ded0f8433a012bee1
5
5
  SHA512:
6
- metadata.gz: c788a0ab4bd58e70ef2be2b7be88101bcccf4880fecd0ee4b8cd7a1be05cbe9b9e157c99fc7f5724a62a4d6fcdacf6278e8c902368fcc60324d966b204698b48
7
- data.tar.gz: 1a51dc48ec715adc969325e82f1c7bd81e23b35f716c0ee6c3ded05a9d38eb0b57261121892658fb8715a969f7ab22f1a06664af17aa990dc0e708272229039e
6
+ metadata.gz: 6f45fbaf1de91fd1676d94063f6b8d34663207d0f81de84805eca6203789f6864fe48f54137a0cd73178fc7972f309dec9a3ef8bc956070ec09fd5bbac74c4e9
7
+ data.tar.gz: ff8f92d7ffe8f69d040d00237227c14696453abe76b3bdd90163ae2ffdc681bd0059ca119c6216a2f454f862fdd4db80196c2befa8c1e07b0c73a430fee5a06e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.30] - 2026-07-31
4
+
5
+ ### Removed
6
+ - **`apollo_setting` wrapper method deleted.** All 40 call sites across 10 files converted to direct bracket access against registered defaults in `default_settings`. Single source of truth for all Apollo configuration per standards section 3.
7
+
8
+ ### Changed
9
+ - **Contradiction detection moved off the synchronous write path.** `handle_ingest` no longer blocks on `detect_contradictions`. When enabled, contradiction work is enqueued to the new `ContradictionScanner` interval actor (10-minute cadence, configurable via `apollo.actors.contradiction_interval`). The return payload retains a stable `contradictions: []` key.
10
+ - New setting `apollo.contradiction.enabled` (default: `false`) controls whether contradiction detection runs at all. When false, no LLM calls or vector scans are performed for contradiction purposes.
11
+ - Corrected CLAUDE.md backing store description from Azure to self-hosted PostgreSQL + pgvector.
12
+ - All helpers, runners, and actors now read settings via bracket access (`settings[:namespace][:key]`) instead of the removed `apollo_setting(*keys, default:)` proxy.
13
+
14
+ ### Added
15
+ - `Actor::ContradictionScanner` — interval actor that drains a thread-safe queue of pending entries and runs `detect_contradictions` out-of-band from the write path.
16
+ - `contradiction_interval` setting under `apollo.actors` (default: 600 seconds).
17
+
3
18
  ## [0.4.27] - 2026-05-15
4
19
 
5
20
  ### Added
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+ require_relative '../runners/knowledge'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Apollo
9
+ module Actor
10
+ class ContradictionScanner < Legion::Extensions::Actors::Every
11
+ include Legion::Settings::Helper
12
+ include Legion::Logging::Helper
13
+
14
+ @queue = Queue.new
15
+ @mutex = Mutex.new
16
+
17
+ class << self
18
+ attr_reader :queue, :mutex
19
+
20
+ def enqueue(entry_id:, embedding:, content:)
21
+ queue.push({ entry_id: entry_id, embedding: embedding, content: content })
22
+ end
23
+
24
+ def pending_count
25
+ queue.size
26
+ end
27
+
28
+ def drain
29
+ items = []
30
+ items << queue.pop until queue.empty?
31
+ items
32
+ end
33
+ end
34
+
35
+ def runner_class = Legion::Extensions::Apollo::Runners::Knowledge
36
+ def runner_function = 'scan_pending_contradictions'
37
+ def time = settings[:actors][:contradiction_interval]
38
+ def run_now? = false
39
+ def use_runner? = false
40
+ def check_subtask? = false
41
+ def generate_task? = false
42
+
43
+ def every
44
+ items = self.class.drain
45
+ return if items.empty?
46
+
47
+ log.debug("Apollo ContradictionScanner.every processing=#{items.size}")
48
+ runner = Object.new.extend(Legion::Extensions::Apollo::Runners::Knowledge)
49
+ items.each do |item|
50
+ runner.detect_contradictions(item[:entry_id], item[:embedding], item[:content])
51
+ end
52
+ log.info("Apollo ContradictionScanner.every completed=#{items.size}")
53
+ rescue StandardError => e
54
+ handle_exception(e, level: :error, operation: 'apollo.actor.contradiction_scanner')
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -89,7 +89,7 @@ module Legion
89
89
  result = retrieve_relevant(
90
90
  query: entity[:name].to_s,
91
91
  limit: 1,
92
- min_confidence: Helpers::Confidence.apollo_setting(:entity_watchdog, :exists_min_confidence, default: 0.1),
92
+ min_confidence: settings[:entity_watchdog][:exists_min_confidence],
93
93
  tags: [entity[:type].to_s]
94
94
  )
95
95
  return false unless result[:success] && result[:count].positive?
@@ -24,25 +24,18 @@ module Legion
24
24
 
25
25
  module_function
26
26
 
27
- def apollo_setting(*keys, default:)
28
- settings.dig(*keys)
29
- rescue StandardError => e
30
- handle_exception(e, level: :warn, operation: 'apollo.confidence.apollo_setting', keys: keys)
31
- default
32
- end
33
-
34
- def initial_confidence = apollo_setting(:confidence, :initial, default: INITIAL_CONFIDENCE)
35
- def corroboration_boost = apollo_setting(:confidence, :corroboration_boost, default: CORROBORATION_BOOST)
36
- def retrieval_boost = apollo_setting(:confidence, :retrieval_boost, default: RETRIEVAL_BOOST)
37
- def power_law_alpha = apollo_setting(:power_law_alpha, default: POWER_LAW_ALPHA)
38
- def decay_threshold = apollo_setting(:decay_threshold, default: DECAY_THRESHOLD)
39
- def write_confidence_gate = apollo_setting(:confidence, :write_gate, default: WRITE_CONFIDENCE_GATE)
40
- def write_novelty_gate = apollo_setting(:confidence, :novelty_gate, default: WRITE_NOVELTY_GATE)
41
- def stale_days = apollo_setting(:stale_days, default: STALE_DAYS)
42
- def decay_min_age_hours = apollo_setting(:decay_min_age_hours, default: DECAY_MIN_AGE_HOURS)
27
+ def initial_confidence = settings[:confidence][:initial]
28
+ def corroboration_boost = settings[:confidence][:corroboration_boost]
29
+ def retrieval_boost = settings[:confidence][:retrieval_boost]
30
+ def power_law_alpha = settings[:power_law_alpha]
31
+ def decay_threshold = settings[:decay_threshold]
32
+ def write_confidence_gate = settings[:confidence][:write_gate]
33
+ def write_novelty_gate = settings[:confidence][:novelty_gate]
34
+ def stale_days = settings[:stale_days]
35
+ def decay_min_age_hours = settings[:decay_min_age_hours]
43
36
 
44
37
  def corroboration_similarity_threshold
45
- apollo_setting(:confidence, :corroboration_similarity, default: CORROBORATION_SIMILARITY_THRESHOLD)
38
+ settings[:confidence][:corroboration_similarity]
46
39
  end
47
40
 
48
41
  def apply_decay(confidence:, age_hours: nil, alpha: power_law_alpha, **)
@@ -28,7 +28,7 @@ module Legion
28
28
 
29
29
  text.scan(pattern).each do |match|
30
30
  entities << { type: entity_type, value: match.strip,
31
- confidence: Confidence.apollo_setting(:entity_watchdog, :detect_confidence, default: 0.5) }
31
+ confidence: settings[:entity_watchdog][:detect_confidence] }
32
32
  end
33
33
  end
34
34
 
@@ -5,18 +5,20 @@ module Legion
5
5
  module Apollo
6
6
  module Helpers
7
7
  module GraphQuery
8
+ extend Legion::Settings::Helper
9
+
8
10
  SPREAD_FACTOR = 0.6
9
11
  DEFAULT_DEPTH = 2
10
12
  MIN_ACTIVATION = 0.1
11
13
 
12
14
  module_function
13
15
 
14
- def spread_factor = Confidence.apollo_setting(:graph, :spread_factor, default: SPREAD_FACTOR)
15
- def default_depth = Confidence.apollo_setting(:graph, :default_depth, default: DEFAULT_DEPTH)
16
- def min_activation = Confidence.apollo_setting(:graph, :min_activation, default: MIN_ACTIVATION)
16
+ def spread_factor = settings[:graph][:spread_factor]
17
+ def default_depth = settings[:graph][:default_depth]
18
+ def min_activation = settings[:graph][:min_activation]
17
19
 
18
- def default_query_limit = Confidence.apollo_setting(:query, :default_limit, default: 10)
19
- def default_query_min_confidence = Confidence.apollo_setting(:query, :default_min_confidence, default: 0.3)
20
+ def default_query_limit = settings[:query][:default_limit]
21
+ def default_query_min_confidence = settings[:query][:default_min_confidence]
20
22
 
21
23
  def build_traversal_sql(depth: default_depth, relation_types: nil, min_activation: self.min_activation, **)
22
24
  sf = spread_factor
@@ -8,7 +8,7 @@ module Legion
8
8
  DEFAULT_ENTITY_TYPES = %w[person service repository concept].freeze
9
9
  DEFAULT_MIN_CONFIDENCE = 0.7
10
10
 
11
- def extract_entities(text:, entity_types: nil, min_confidence: Helpers::Confidence.apollo_setting(:entity_extractor, :min_confidence, default: DEFAULT_MIN_CONFIDENCE), **) # rubocop:disable Layout/LineLength
11
+ def extract_entities(text:, entity_types: nil, min_confidence: Legion::Extensions::Apollo.settings[:entity_extractor][:min_confidence], **)
12
12
  if text.to_s.strip.empty?
13
13
  log.debug('Apollo EntityExtractor.extract_entities skipped reason=empty_text')
14
14
  return { success: true, entities: [], source: :empty }
@@ -7,11 +7,11 @@ module Legion
7
7
  module Apollo
8
8
  module Runners
9
9
  module Expertise
10
- def get_expertise(domain:, min_proficiency: Helpers::Confidence.apollo_setting(:expertise, :initial_proficiency, default: 0.0), **)
10
+ def get_expertise(domain:, min_proficiency: Legion::Extensions::Apollo.settings[:expertise][:initial_proficiency], **)
11
11
  { action: :expertise_query, domain: domain, min_proficiency: min_proficiency }
12
12
  end
13
13
 
14
- def domains_at_risk(min_agents: Helpers::Confidence.apollo_setting(:expertise, :min_agents_at_risk, default: 2), **)
14
+ def domains_at_risk(min_agents: Legion::Extensions::Apollo.settings[:expertise][:min_agents_at_risk], **)
15
15
  { action: :domains_at_risk, min_agents: min_agents }
16
16
  end
17
17
 
@@ -75,7 +75,7 @@ module Legion
75
75
 
76
76
  def expertise_proficiency(confidences)
77
77
  avg = confidences.sum / confidences.size
78
- cap = Helpers::Confidence.apollo_setting(:expertise, :proficiency_cap, default: 1.0)
78
+ cap = Legion::Extensions::Apollo.settings[:expertise][:proficiency_cap]
79
79
  [avg * Math.log2(confidences.size + 1), cap].min
80
80
  end
81
81
 
@@ -27,11 +27,11 @@ module Legion
27
27
  json_parse(str)
28
28
  end
29
29
 
30
- def relate_confidence_gate = Helpers::Confidence.apollo_setting(:gas, :relate_confidence_gate, default: RELATE_CONFIDENCE_GATE)
31
- def synthesis_confidence_cap = Helpers::Confidence.apollo_setting(:gas, :synthesis_confidence_cap, default: SYNTHESIS_CONFIDENCE_CAP)
32
- def max_anticipations = Helpers::Confidence.apollo_setting(:gas, :max_anticipations, default: MAX_ANTICIPATIONS)
33
- def similar_entries_limit = Helpers::Confidence.apollo_setting(:gas, :similar_entries_limit, default: 3)
34
- def fallback_confidence = Helpers::Confidence.apollo_setting(:gas, :fallback_confidence, default: 0.5)
30
+ def relate_confidence_gate = Legion::Extensions::Apollo.settings[:gas][:relate_confidence_gate]
31
+ def synthesis_confidence_cap = Legion::Extensions::Apollo.settings[:gas][:synthesis_confidence_cap]
32
+ def max_anticipations = Legion::Extensions::Apollo.settings[:gas][:max_anticipations]
33
+ def similar_entries_limit = Legion::Extensions::Apollo.settings[:gas][:similar_entries_limit]
34
+ def fallback_confidence = Legion::Extensions::Apollo.settings[:gas][:fallback_confidence]
35
35
 
36
36
  def process(audit_event)
37
37
  unless processable?(audit_event)
@@ -132,11 +132,11 @@ module Legion
132
132
  entry_id: existing_id, agent_id: metadata[:source_agent], action: 'ingest'
133
133
  )
134
134
 
135
- contradictions = detect_contradictions(existing_id, embedding, content)
136
- log.debug("Apollo Knowledge.handle_ingest complete entry_id=#{existing_id} corroborated=#{corroborated} contradictions=#{contradictions.size}")
135
+ schedule_contradiction_detection(entry_id: existing_id, embedding: embedding, content: content)
136
+ log.debug("Apollo Knowledge.handle_ingest complete entry_id=#{existing_id} corroborated=#{corroborated}")
137
137
 
138
138
  { success: true, entry_id: existing_id, status: corroborated ? 'corroborated' : 'candidate',
139
- corroborated: corroborated, contradictions: contradictions }
139
+ corroborated: corroborated, contradictions: [] }
140
140
  rescue Sequel::Error => e
141
141
  handle_exception(e, level: :error, operation: 'apollo.knowledge.handle_ingest')
142
142
  { success: false, error: e.message }
@@ -232,7 +232,7 @@ module Legion
232
232
  { success: false, error: e.message }
233
233
  end
234
234
 
235
- def redistribute_knowledge(agent_id:, min_confidence: Helpers::Confidence.apollo_setting(:query, :redistribute_min_confidence, default: 0.5), **)
235
+ def redistribute_knowledge(agent_id:, min_confidence: settings[:query][:redistribute_min_confidence], **)
236
236
  return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
237
237
 
238
238
  log.debug("Apollo Knowledge.redistribute_knowledge agent_id=#{agent_id} min_confidence=#{min_confidence}")
@@ -267,7 +267,7 @@ module Legion
267
267
  { success: false, error: e.message }
268
268
  end
269
269
 
270
- def retrieve_relevant(query: nil, limit: Helpers::Confidence.apollo_setting(:query, :retrieval_limit, default: 5),
270
+ def retrieve_relevant(query: nil, limit: settings[:query][:retrieval_limit],
271
271
  min_confidence: Helpers::GraphQuery.default_query_min_confidence,
272
272
  tags: nil, domain: nil, skip: false, requesting_principal_id: nil, **)
273
273
  return { status: :skipped } if skip
@@ -316,7 +316,7 @@ module Legion
316
316
  { success: false, error: e.message }
317
317
  end
318
318
 
319
- def prepare_mesh_export(target_domain:, min_confidence: Helpers::Confidence.apollo_setting(:query, :mesh_export_min_confidence, default: 0.5), limit: Helpers::Confidence.apollo_setting(:query, :mesh_export_limit, default: 100), **) # rubocop:disable Layout/LineLength
319
+ def prepare_mesh_export(target_domain:, min_confidence: settings[:query][:mesh_export_min_confidence], limit: settings[:query][:mesh_export_limit], **) # rubocop:disable Layout/LineLength
320
320
  unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
321
321
  return { success: false, error: 'apollo_data_not_available' }
322
322
  end
@@ -596,12 +596,28 @@ module Legion
596
596
  Array(allowed)
597
597
  end
598
598
 
599
+ def contradiction_detection_enabled?
600
+ settings[:contradiction][:enabled] == true
601
+ end
602
+
603
+ def schedule_contradiction_detection(entry_id:, embedding:, content:)
604
+ return unless contradiction_detection_enabled?
605
+ return unless embedding && Helpers::DataModels.apollo_entry_available?
606
+
607
+ Legion::Extensions::Apollo::Actor::ContradictionScanner.enqueue(
608
+ entry_id: entry_id, embedding: embedding, content: content
609
+ )
610
+ log.debug("Apollo Knowledge.schedule_contradiction_detection enqueued entry_id=#{entry_id}")
611
+ rescue StandardError => e
612
+ handle_exception(e, level: :warn, operation: 'apollo.knowledge.schedule_contradiction_detection')
613
+ end
614
+
599
615
  def detect_contradictions(entry_id, embedding, content)
600
616
  return [] unless embedding && Helpers::DataModels.apollo_entry_available?
601
617
 
602
- sim_limit = Helpers::Confidence.apollo_setting(:contradiction, :similar_limit, default: 10)
603
- sim_threshold = Helpers::Confidence.apollo_setting(:contradiction, :similarity_threshold, default: 0.7)
604
- rel_weight = Helpers::Confidence.apollo_setting(:contradiction, :relation_weight, default: 0.8)
618
+ sim_limit = settings[:contradiction][:similar_limit]
619
+ sim_threshold = settings[:contradiction][:similarity_threshold]
620
+ rel_weight = settings[:contradiction][:relation_weight]
605
621
 
606
622
  db = Helpers::DataModels.apollo_entry.db
607
623
  log.debug("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} similar_limit=#{sim_limit} threshold=#{sim_threshold}")
@@ -658,7 +674,7 @@ module Legion
658
674
  def find_corroboration(embedding, content_type_sym, source_agent, source_channel = nil)
659
675
  return [false, nil] unless embedding
660
676
 
661
- scan_limit = Helpers::Confidence.apollo_setting(:corroboration, :scan_limit, default: 50)
677
+ scan_limit = settings[:corroboration][:scan_limit]
662
678
  log.debug("Apollo Knowledge.find_corroboration content_type=#{content_type_sym} source_agent=#{source_agent} source_channel=#{source_channel || 'nil'} scan_limit=#{scan_limit}") # rubocop:disable Layout/LineLength
663
679
  existing = Helpers::DataModels.apollo_entry
664
680
  .where(content_type: content_type_sym)
@@ -671,7 +687,7 @@ module Legion
671
687
  sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: entry.embedding)
672
688
  next unless Helpers::Similarity.above_corroboration_threshold?(similarity: sim)
673
689
 
674
- same_provider_wt = Helpers::Confidence.apollo_setting(:corroboration, :same_provider_weight, default: 0.5)
690
+ same_provider_wt = settings[:corroboration][:same_provider_weight]
675
691
  weight = same_source_provider?(source_agent, entry) ? same_provider_wt : 1.0
676
692
 
677
693
  # Reject corroboration entirely if same channel (same data source)
@@ -716,7 +732,7 @@ module Legion
716
732
  else
717
733
  Helpers::DataModels.apollo_expertise.create(
718
734
  agent_id: source_agent, domain: domain,
719
- proficiency: Helpers::Confidence.apollo_setting(:expertise, :initial_proficiency, default: 0.0),
735
+ proficiency: settings[:expertise][:initial_proficiency],
720
736
  entry_count: 1, last_active_at: Time.now
721
737
  )
722
738
  end
@@ -9,7 +9,7 @@ module Legion
9
9
  module Apollo
10
10
  module Runners
11
11
  module Maintenance
12
- def force_decay(factor: Helpers::Confidence.apollo_setting(:maintenance, :force_decay_factor, default: 0.5), **)
12
+ def force_decay(factor: Legion::Extensions::Apollo.settings[:maintenance][:force_decay_factor], **)
13
13
  { action: :force_decay, factor: factor }
14
14
  end
15
15
 
@@ -66,7 +66,7 @@ module Legion
66
66
  { decayed: 0, archived: 0, error: e.message }
67
67
  end
68
68
 
69
- def check_corroboration(**) # rubocop:disable Metrics/CyclomaticComplexity
69
+ def check_corroboration(**) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
70
70
  unless Helpers::DataModels.apollo_entry_available?
71
71
  log.warn('Apollo Maintenance.check_corroboration skipped: apollo_data_not_available')
72
72
  return { success: false, error: 'apollo_data_not_available' }
@@ -111,7 +111,7 @@ module Legion
111
111
  to_entry_id: match.id,
112
112
  relation_type: 'similar_to',
113
113
  source_agent: 'system:corroboration',
114
- weight: Helpers::Confidence.apollo_setting(:corroboration, :relation_weight, default: 1.0)
114
+ weight: Legion::Extensions::Apollo.settings[:corroboration][:relation_weight]
115
115
  )
116
116
 
117
117
  promoted += 1
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.4.28'
6
+ VERSION = '0.4.30'
7
7
  end
8
8
  end
9
9
  end
@@ -82,6 +82,7 @@ module Legion
82
82
  force_decay_factor: 0.5
83
83
  },
84
84
  contradiction: {
85
+ enabled: false,
85
86
  similar_limit: 10,
86
87
  similarity_threshold: 0.7,
87
88
  relation_weight: 0.8
@@ -129,6 +130,7 @@ module Legion
129
130
  decay_interval: 3600,
130
131
  expertise_interval: 1800,
131
132
  corroboration_interval: 900,
133
+ contradiction_interval: 600,
132
134
  entity_watchdog_interval: 120
133
135
  }
134
136
  }
@@ -7,7 +7,7 @@ unless defined?(Legion::Extensions::Actors::Every)
7
7
  module Extensions
8
8
  module Actors
9
9
  class Every
10
- def initialize(**_opts); end
10
+ def initialize(**); end
11
11
  end
12
12
  end
13
13
  end
@@ -102,7 +102,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Writeback do
102
102
  end
103
103
 
104
104
  it 'does not call Runners::Knowledge.handle_ingest directly' do
105
- knowledge_mod = Module.new { def self.handle_ingest(**_); end }
105
+ knowledge_mod = Module.new { def self.handle_ingest(**); end }
106
106
  stub_const('Legion::Extensions::Apollo::Runners', Module.new)
107
107
  stub_const('Legion::Extensions::Apollo::Runners::Knowledge', knowledge_mod)
108
108
  allow(knowledge_mod).to receive(:handle_ingest)
@@ -112,7 +112,7 @@ RSpec.describe Legion::Extensions::Apollo::Helpers::Writeback do
112
112
  end
113
113
 
114
114
  context 'when Legion::Apollo is not defined' do
115
- let(:knowledge_mod) { Module.new { def self.handle_ingest(**_); end } }
115
+ let(:knowledge_mod) { Module.new { def self.handle_ingest(**); end } }
116
116
 
117
117
  before do
118
118
  hide_const('Legion::Apollo') if defined?(Legion::Apollo)
@@ -42,7 +42,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::EntityExtractor do
42
42
  stub_const('Legion::LLM', Module.new do
43
43
  def self.started? = true
44
44
 
45
- def self.structured(**_opts) = { data: { entities: [] } }
45
+ def self.structured(**) = { data: { entities: [] } }
46
46
  end)
47
47
  allow(Legion::LLM).to receive(:structured).and_return(llm_result)
48
48
  end
@@ -77,7 +77,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::EntityExtractor do
77
77
  stub_const('Legion::LLM', Module.new do
78
78
  def self.started? = true
79
79
 
80
- def self.structured(**_opts) = raise(StandardError, 'timeout')
80
+ def self.structured(**) = raise(StandardError, 'timeout')
81
81
  end)
82
82
  end
83
83
 
@@ -492,6 +492,88 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
492
492
  end
493
493
  end
494
494
  end
495
+
496
+ context 'contradiction detection flag' do
497
+ let(:mock_entry_class) { double('ApolloEntry') }
498
+ let(:mock_relation_class) { double('ApolloRelation') }
499
+ let(:mock_expertise_class) { double('ApolloExpertise') }
500
+ let(:mock_access_log_class) { double('ApolloAccessLog') }
501
+ let(:mock_entry) { double('entry', id: 'uuid-new', embedding: nil) }
502
+ let(:empty_dataset) { double('dataset', each: nil) }
503
+ let(:mock_db) { double('db') }
504
+
505
+ before do
506
+ stub_const('Legion::Data::Model::ApolloEntry', mock_entry_class)
507
+ stub_const('Legion::Data::Model::ApolloRelation', mock_relation_class)
508
+ stub_const('Legion::Data::Model::ApolloExpertise', mock_expertise_class)
509
+ stub_const('Legion::Data::Model::ApolloAccessLog', mock_access_log_class)
510
+ allow(Legion::LLM::Call::Embeddings).to receive(:generate)
511
+ .and_return({ vector: Array.new(1024, 0.1), model: 'test', provider: :ollama, dimensions: 1024, tokens: 0 })
512
+ allow(mock_entry_class).to receive(:where).and_return(double(exclude: double(limit: empty_dataset)))
513
+ dedup_chain = double('dedup_chain')
514
+ allow(mock_entry_class).to receive(:where).with(content_hash: anything).and_return(dedup_chain)
515
+ allow(dedup_chain).to receive(:exclude).with(status: 'archived').and_return(double(first: nil))
516
+ allow(mock_entry_class).to receive(:db).and_return(mock_db)
517
+ allow(mock_db).to receive(:fetch).and_return(double(all: []))
518
+ allow(mock_entry_class).to receive(:create).and_return(mock_entry)
519
+ allow(mock_expertise_class).to receive(:where).and_return(double(first: nil))
520
+ allow(mock_expertise_class).to receive(:create)
521
+ allow(mock_access_log_class).to receive(:create)
522
+ end
523
+
524
+ it 'defaults contradiction detection to disabled' do
525
+ expect(Legion::Extensions::Apollo.settings[:contradiction][:enabled]).to be false
526
+ end
527
+
528
+ it 'does not call detect_contradictions when flag is false' do
529
+ expect(host).not_to receive(:detect_contradictions)
530
+ result = host.handle_ingest(content: 'test fact', content_type: 'fact', source_agent: 'agent-1')
531
+ expect(result[:success]).to be true
532
+ expect(result[:contradictions]).to eq([])
533
+ end
534
+
535
+ it 'returns stable contradictions key as empty array when disabled' do
536
+ result = host.handle_ingest(content: 'test content', content_type: 'fact', source_agent: 'agent-1')
537
+ expect(result).to have_key(:contradictions)
538
+ expect(result[:contradictions]).to eq([])
539
+ end
540
+
541
+ context 'when enabled' do
542
+ before do
543
+ Legion::Settings[:extensions][:apollo][:contradiction][:enabled] = true
544
+ stub_const('Legion::Extensions::Apollo::Actor::ContradictionScanner',
545
+ Class.new do
546
+ @queue = Queue.new
547
+ class << self
548
+ attr_reader :queue
549
+
550
+ def enqueue(entry_id:, embedding:, content:)
551
+ queue.push({ entry_id: entry_id, embedding: embedding, content: content })
552
+ end
553
+
554
+ def pending_count = queue.size
555
+
556
+ def drain
557
+ items = []
558
+ items << queue.pop until queue.empty?
559
+ items
560
+ end
561
+ end
562
+ end)
563
+ end
564
+
565
+ after do
566
+ Legion::Settings[:extensions][:apollo][:contradiction][:enabled] = false
567
+ end
568
+
569
+ it 'enqueues contradiction work to the actor instead of running inline' do
570
+ result = host.handle_ingest(content: 'enabled test', content_type: 'fact', source_agent: 'agent-1')
571
+ expect(result[:success]).to be true
572
+ expect(result[:contradictions]).to eq([])
573
+ expect(Legion::Extensions::Apollo::Actor::ContradictionScanner.pending_count).to eq(1)
574
+ end
575
+ end
576
+ end
495
577
  end
496
578
 
497
579
  describe '#handle_query' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-apollo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.28
4
+ version: 0.4.30
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -160,6 +160,7 @@ files:
160
160
  - CHANGELOG.md
161
161
  - README.md
162
162
  - lib/legion/extensions/apollo.rb
163
+ - lib/legion/extensions/apollo/actors/contradiction_scanner.rb
163
164
  - lib/legion/extensions/apollo/actors/corroboration_checker.rb
164
165
  - lib/legion/extensions/apollo/actors/decay.rb
165
166
  - lib/legion/extensions/apollo/actors/entity_watchdog.rb