lex-apollo 0.4.27 → 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: 85cccff520bb34332f705695f5193cd55e01bb94fb680bf0a51ad81a04ec3112
4
- data.tar.gz: b3370806ab160e71f3f213fb4401a6a45aa650778a29574b1cc553a9ad34d167
3
+ metadata.gz: facad4c6e5cf4da1497d5d6de01daa13310bc8762274e227e9ff7ead72c761a6
4
+ data.tar.gz: 37a7098b002770c827173135a5510eeb7651f0705ef6f80ded0f8433a012bee1
5
5
  SHA512:
6
- metadata.gz: d45dd95922afbff6cf87391eca84b658359fc3b329f80cfd9404054e2e5fc7cff5b398d2b065f407ab9bff3141ba87b9f3d30e0c980e6c4625231edbf591bc18
7
- data.tar.gz: 47183b00644bae505cd55c1abad707c5c86d901a38472d186bb5601eb3ec4fa0d2452715a667b3f7ebd0081d00ccb3fc3d6d6fabab61e7d146fdb2890deca7f2
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)
@@ -85,10 +85,10 @@ module Legion
85
85
  def handle_ingest(content: nil, content_type: nil, tags: [], source_agent: 'unknown', # rubocop:disable Metrics/ParameterLists
86
86
  source_provider: nil, source_channel: nil, knowledge_domain: nil,
87
87
  submitted_by: nil, submitted_from: nil, content_hash: nil, context: {},
88
- skip: false, access_scope: 'global', identity_principal_id: nil,
89
- identity_id: nil, identity_canonical_name: nil, **)
88
+ skip: false, access_scope: 'global', **)
90
89
  return { status: :skipped } if skip
91
90
 
91
+ identity = resolve_process_identity
92
92
  content = normalize_text_input(content)
93
93
  content_type = normalize_content_type(content_type.nil? ? :observation : content_type)
94
94
  log.debug("Apollo Knowledge.handle_ingest content_length=#{content.length} content_type=#{content_type} tags=#{Array(tags).size} source_agent=#{source_agent} source_channel=#{source_channel || 'nil'}") # rubocop:disable Layout/LineLength
@@ -97,7 +97,7 @@ module Legion
97
97
 
98
98
  hash = content_hash || (defined?(Helpers::Writeback) ? Helpers::Writeback.content_hash(content) : nil)
99
99
  existing = active_duplicate_for_hash(hash, access_scope: access_scope,
100
- identity_principal_id: identity_principal_id)
100
+ identity_principal_id: identity[:principal_id])
101
101
  if existing
102
102
  log.info("Apollo Knowledge.handle_ingest deduped entry_id=#{existing.id} source_agent=#{source_agent}")
103
103
  return { success: true, entry_id: existing.id, deduped: true }
@@ -109,9 +109,9 @@ module Legion
109
109
  source_provider: source_provider, source_channel: source_channel,
110
110
  submitted_by: submitted_by, submitted_from: submitted_from,
111
111
  access_scope: access_scope,
112
- identity_principal_id: identity_principal_id,
113
- identity_id: identity_id,
114
- identity_canonical_name: identity_canonical_name)
112
+ identity_principal_id: identity[:principal_id],
113
+ identity_id: identity[:identity_id],
114
+ identity_canonical_name: identity[:canonical_name])
115
115
 
116
116
  corroborated, existing_id = find_corroboration(
117
117
  embedding, content_type_sym, metadata[:source_agent], metadata[:source_channel]
@@ -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 }
@@ -148,6 +148,7 @@ module Legion
148
148
  requesting_principal_id: nil, **)
149
149
  return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
150
150
 
151
+ requesting_principal_id = resolve_requesting_principal_id(requesting_principal_id)
151
152
  entry_model = Helpers::DataModels.apollo_entry
152
153
  query = normalize_text_input(query)
153
154
  status_defaulted = status.equal?(UNSET)
@@ -231,7 +232,7 @@ module Legion
231
232
  { success: false, error: e.message }
232
233
  end
233
234
 
234
- 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], **)
235
236
  return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
236
237
 
237
238
  log.debug("Apollo Knowledge.redistribute_knowledge agent_id=#{agent_id} min_confidence=#{min_confidence}")
@@ -266,13 +267,14 @@ module Legion
266
267
  { success: false, error: e.message }
267
268
  end
268
269
 
269
- 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],
270
271
  min_confidence: Helpers::GraphQuery.default_query_min_confidence,
271
272
  tags: nil, domain: nil, skip: false, requesting_principal_id: nil, **)
272
273
  return { status: :skipped } if skip
273
274
 
274
275
  return { success: false, error: 'apollo_data_not_available' } unless Helpers::DataModels.apollo_entry_available?
275
276
 
277
+ requesting_principal_id = resolve_requesting_principal_id(requesting_principal_id)
276
278
  query = normalize_text_input(query)
277
279
  log.debug("Apollo Knowledge.retrieve_relevant query_length=#{query.length} limit=#{limit} min_confidence=#{min_confidence} tags=#{Array(tags).size} domain=#{domain || 'nil'}") # rubocop:disable Layout/LineLength
278
280
  return { success: true, entries: [], count: 0 } if query.nil? || query.to_s.strip.empty?
@@ -314,7 +316,7 @@ module Legion
314
316
  { success: false, error: e.message }
315
317
  end
316
318
 
317
- 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
318
320
  unless defined?(Legion::Data) && Legion::Data.respond_to?(:connection) && Legion::Data.connection
319
321
  return { success: false, error: 'apollo_data_not_available' }
320
322
  end
@@ -594,12 +596,28 @@ module Legion
594
596
  Array(allowed)
595
597
  end
596
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
+
597
615
  def detect_contradictions(entry_id, embedding, content)
598
616
  return [] unless embedding && Helpers::DataModels.apollo_entry_available?
599
617
 
600
- sim_limit = Helpers::Confidence.apollo_setting(:contradiction, :similar_limit, default: 10)
601
- sim_threshold = Helpers::Confidence.apollo_setting(:contradiction, :similarity_threshold, default: 0.7)
602
- 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]
603
621
 
604
622
  db = Helpers::DataModels.apollo_entry.db
605
623
  log.debug("Apollo Knowledge.detect_contradictions entry_id=#{entry_id} similar_limit=#{sim_limit} threshold=#{sim_threshold}")
@@ -656,7 +674,7 @@ module Legion
656
674
  def find_corroboration(embedding, content_type_sym, source_agent, source_channel = nil)
657
675
  return [false, nil] unless embedding
658
676
 
659
- scan_limit = Helpers::Confidence.apollo_setting(:corroboration, :scan_limit, default: 50)
677
+ scan_limit = settings[:corroboration][:scan_limit]
660
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
661
679
  existing = Helpers::DataModels.apollo_entry
662
680
  .where(content_type: content_type_sym)
@@ -669,7 +687,7 @@ module Legion
669
687
  sim = Helpers::Similarity.cosine_similarity(vec_a: embedding, vec_b: entry.embedding)
670
688
  next unless Helpers::Similarity.above_corroboration_threshold?(similarity: sim)
671
689
 
672
- same_provider_wt = Helpers::Confidence.apollo_setting(:corroboration, :same_provider_weight, default: 0.5)
690
+ same_provider_wt = settings[:corroboration][:same_provider_weight]
673
691
  weight = same_source_provider?(source_agent, entry) ? same_provider_wt : 1.0
674
692
 
675
693
  # Reject corroboration entirely if same channel (same data source)
@@ -714,12 +732,35 @@ module Legion
714
732
  else
715
733
  Helpers::DataModels.apollo_expertise.create(
716
734
  agent_id: source_agent, domain: domain,
717
- proficiency: Helpers::Confidence.apollo_setting(:expertise, :initial_proficiency, default: 0.0),
735
+ proficiency: settings[:expertise][:initial_proficiency],
718
736
  entry_count: 1, last_active_at: Time.now
719
737
  )
720
738
  end
721
739
  end
722
740
 
741
+ def resolve_process_identity
742
+ return { principal_id: nil, identity_id: nil, canonical_name: nil } unless defined?(Legion::Identity::Process)
743
+
744
+ {
745
+ principal_id: Legion::Identity::Process.db_principal_id,
746
+ identity_id: Legion::Identity::Process.db_identity_id,
747
+ canonical_name: Legion::Identity::Process.canonical_name
748
+ }
749
+ rescue StandardError => e
750
+ handle_exception(e, level: :warn, operation: 'apollo.knowledge.resolve_process_identity')
751
+ { principal_id: nil, identity_id: nil, canonical_name: nil }
752
+ end
753
+
754
+ def resolve_requesting_principal_id(explicit_value)
755
+ return explicit_value if explicit_value
756
+ return nil unless defined?(Legion::Identity::Process)
757
+
758
+ Legion::Identity::Process.db_principal_id
759
+ rescue StandardError => e
760
+ handle_exception(e, level: :warn, operation: 'apollo.knowledge.resolve_requesting_principal_id')
761
+ nil
762
+ end
763
+
723
764
  include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
724
765
  include Legion::JSON::Helper
725
766
  include Legion::Settings::Helper
@@ -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.27'
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
 
@@ -371,7 +371,7 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
371
371
  end
372
372
  end
373
373
 
374
- context 'identity kwargs persistence' do
374
+ context 'identity from Legion::Identity::Process' do
375
375
  let(:mock_entry_class2) { double('ApolloEntry2') }
376
376
  let(:mock_expertise_class2) { double('ApolloExpertise2') }
377
377
  let(:mock_access_log_class2) { double('ApolloAccessLog2') }
@@ -390,18 +390,20 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
390
390
  allow(host).to receive(:embed_text).and_return(nil)
391
391
  allow(host).to receive(:find_corroboration).and_return([false, nil])
392
392
  allow(host).to receive(:detect_contradictions).and_return([])
393
+ stub_const('Legion::Identity::Process', double(
394
+ db_principal_id: 42,
395
+ db_identity_id: 7,
396
+ canonical_name: 'alice'
397
+ ))
393
398
  end
394
399
 
395
- it 'passes identity_principal_id and access_scope through to create_candidate_entry' do
400
+ it 'derives identity from Legion::Identity::Process and persists it' do
396
401
  expect(mock_entry_class2).to receive(:create).with(
397
- hash_including(identity_principal_id: 42, access_scope: 'private')
402
+ hash_including(identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice',
403
+ access_scope: 'private')
398
404
  ).and_return(mock_entry2)
399
405
 
400
- host.handle_ingest(
401
- content: 'test fact', tags: [],
402
- identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice',
403
- access_scope: 'private'
404
- )
406
+ host.handle_ingest(content: 'test fact', tags: [], access_scope: 'private')
405
407
  end
406
408
 
407
409
  it 'defaults access_scope to global when not provided' do
@@ -412,14 +414,14 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
412
414
  host.handle_ingest(content: 'test fact', tags: [])
413
415
  end
414
416
 
415
- it 'persists identity_id and identity_canonical_name' do
417
+ it 'ignores identity kwargs passed by callers' do
416
418
  expect(mock_entry_class2).to receive(:create).with(
417
- hash_including(identity_id: 7, identity_canonical_name: 'alice')
419
+ hash_including(identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice')
418
420
  ).and_return(mock_entry2)
419
421
 
420
422
  host.handle_ingest(
421
423
  content: 'test fact', tags: [],
422
- identity_principal_id: 42, identity_id: 7, identity_canonical_name: 'alice',
424
+ identity_principal_id: 999, identity_id: 888, identity_canonical_name: 'mallory',
423
425
  access_scope: 'private'
424
426
  )
425
427
  end
@@ -490,6 +492,88 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
490
492
  end
491
493
  end
492
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
493
577
  end
494
578
 
495
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.27
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