lex-apollo 0.4.20 → 0.4.22

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: f197e46e616eb71f939175480496d17775c67985bf70d631ac8d089724c7ac7d
4
- data.tar.gz: c8f6ee951339eda218647c3bee95bfda3f32c6ee62a44abcaad8940b40214bd9
3
+ metadata.gz: 9a3e0ff5897e31c3f8e91f5b71eb96fd9ee90a15a041a5034ae936e2188806ae
4
+ data.tar.gz: c3aba4e4e156c940298dde9c54da46b77e87ffa05db60b7f4b3877307fa20c1c
5
5
  SHA512:
6
- metadata.gz: 5b4822965e47e806bf21b1e07e3a675fff365bdea474514278ab61efc1be2e3fc557756f2ad8ab32e63b2d1eb24286dfe25c44ceacbba723bd877d1402d7a5ab
7
- data.tar.gz: b67b970d6cf8734abd96c5de5e993e802ba924fd966307e60fd7911f7879559e3835d262ed653a269b740bbffd2574df9e156afe4e4c9dfd6a15b70afce6d658
6
+ metadata.gz: e8d3000ec6c34b23c5f49eea11b2a03d8ef117d395f9134782cf1c2fe91650b569b036571aaafd023d36d3fbd74725d492e44b98ed701348717b3221a7e45986
7
+ data.tar.gz: 811fd837e8370dde5d998daadad819f648da2f4b1aae3258c82573efb773a4901c6aff8b6cbcbe60eb9f964cee9146b81288d923429321cc7e70e046cd14371b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.22] - 2026-04-28
4
+
5
+ ### Fixed
6
+ - `/api/apollo/stats` now returns the health UI metrics Interlink expects: `recent_24h`, `avg_confidence`, and a synthesized `by_status["active"]` count for non-archived entries. This keeps the Knowledge Health cards populated instead of rendering missing values. Fixes #16.
7
+
8
+ ## [0.4.21] - 2026-04-27
9
+
10
+ ### Changed
11
+ - `Apollo::Runners::Knowledge#handle_ingest` now emits warn-level logs on the three early-return failure paths (nil/blank content, nil content_type, apollo_data_not_available). Companion to PR #15: that PR added `handle_exception` to the rescue paths; this PR closes the silent-failure window for the early-return paths that fire BEFORE any rescue would. Tag values in the log line are sanitized via `gsub(/[\r\n]+/, ' ')` to prevent log-line injection from caller-controlled tags.
12
+
3
13
  ## [0.4.20] - 2026-04-25
4
14
 
5
15
  ### Fixed
@@ -9,6 +9,37 @@ module Legion
9
9
  class Api < Sinatra::Base
10
10
  set :host_authorization, permitted: :any
11
11
 
12
+ class << self
13
+ def stats_payload(now: Time.now)
14
+ return { error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
15
+
16
+ entries = Legion::Data::Model::ApolloEntry
17
+ by_status = grouped_counts(entries, :status)
18
+ by_status['active'] = entries.exclude(status: 'archived').count
19
+
20
+ stats = {
21
+ total_entries: entries.count,
22
+ recent_24h: entries.where { created_at >= (now - 86_400) }.count,
23
+ avg_confidence: average_confidence(entries),
24
+ by_status: by_status,
25
+ by_content_type: grouped_counts(entries, :content_type)
26
+ }
27
+ stats[:total_relations] = Legion::Data::Model::ApolloRelation.count if defined?(Legion::Data::Model::ApolloRelation)
28
+ stats
29
+ end
30
+
31
+ private
32
+
33
+ def grouped_counts(entries, column)
34
+ entries.group_and_count(column).all.to_h { |row| [row[column].to_s, row[:count]] }
35
+ end
36
+
37
+ def average_confidence(entries)
38
+ avg = entries.avg(:confidence)
39
+ avg&.to_f&.round(3)
40
+ end
41
+ end
42
+
12
43
  before do
13
44
  content_type :json
14
45
  end
@@ -140,18 +171,7 @@ module Legion
140
171
 
141
172
  # Statistics
142
173
  get '/api/apollo/stats' do
143
- stats = {}
144
- if defined?(Legion::Data::Model::ApolloEntry)
145
- stats[:total_entries] = Legion::Data::Model::ApolloEntry.count
146
- stats[:by_status] = Legion::Data::Model::ApolloEntry.group_and_count(:status).all
147
- .to_h { |r| [r[:status], r[:count]] }
148
- stats[:by_content_type] = Legion::Data::Model::ApolloEntry.group_and_count(:content_type).all
149
- .to_h { |r| [r[:content_type], r[:count]] }
150
- stats[:total_relations] = Legion::Data::Model::ApolloRelation.count if defined?(Legion::Data::Model::ApolloRelation)
151
- else
152
- stats[:error] = 'apollo_data_not_available'
153
- end
154
- stats.to_json
174
+ self.class.stats_payload.to_json
155
175
  end
156
176
  end
157
177
  end
@@ -85,9 +85,8 @@ module Legion
85
85
 
86
86
  content = normalize_text_input(content)
87
87
  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
88
- return { success: false, error: 'content is required' } if content.strip.empty?
89
- return { success: false, error: 'content_type is required' } if content_type.nil?
90
- return { success: false, error: 'apollo_data_not_available' } unless defined?(Legion::Data::Model::ApolloEntry)
88
+ early_error = ingest_early_return_error(content: content, content_type: content_type, tags: tags)
89
+ return early_error if early_error
91
90
 
92
91
  hash = content_hash || (defined?(Helpers::Writeback) ? Helpers::Writeback.content_hash(content) : nil)
93
92
  existing = active_duplicate_for_hash(hash)
@@ -353,6 +352,27 @@ module Legion
353
352
 
354
353
  private
355
354
 
355
+ def ingest_early_return_error(content:, content_type:, tags:)
356
+ if content.strip.empty?
357
+ safe_tags = Array(tags).map(&:to_s).map { |t| t.gsub(/[\r\n]+/, ' ') }
358
+ log.warn('[apollo][handle_ingest] early-return: content is required ' \
359
+ "content_type=#{content_type} tags=#{safe_tags.inspect}")
360
+ return { success: false, error: 'content is required' }
361
+ end
362
+
363
+ if content_type.nil?
364
+ log.warn('[apollo][handle_ingest] early-return: content_type is required ' \
365
+ "content_length=#{content.to_s.length}")
366
+ return { success: false, error: 'content_type is required' }
367
+ end
368
+
369
+ return nil if defined?(Legion::Data::Model::ApolloEntry)
370
+
371
+ log.warn('[apollo][handle_ingest] early-return: apollo_data_not_available ' \
372
+ "content_type=#{content_type}")
373
+ { success: false, error: 'apollo_data_not_available' }
374
+ end
375
+
356
376
  def normalize_content_type(raw)
357
377
  sym = raw.to_s.delete_prefix(':').gsub(%r{[/\s]}, '_').strip.downcase.to_sym
358
378
  sym = CONTENT_TYPE_ALIASES.fetch(sym, sym)
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Apollo
6
- VERSION = '0.4.20'
6
+ VERSION = '0.4.22'
7
7
  end
8
8
  end
9
9
  end
@@ -25,4 +25,68 @@ RSpec.describe Legion::Extensions::Apollo::Api do
25
25
  it 'is defined as a Sinatra app' do
26
26
  expect(described_class.superclass).to eq(Sinatra::Base)
27
27
  end
28
+
29
+ describe '.stats_payload' do
30
+ let(:entry_model) { class_double('Legion::Data::Model::ApolloEntry') }
31
+ let(:relation_model) { class_double('Legion::Data::Model::ApolloRelation', count: 4) }
32
+ let(:status_counts) do
33
+ instance_double(
34
+ 'StatusCounts',
35
+ all: [
36
+ { status: 'candidate', count: 2 },
37
+ { status: 'confirmed', count: 3 },
38
+ { status: 'archived', count: 1 }
39
+ ]
40
+ )
41
+ end
42
+ let(:content_type_counts) do
43
+ instance_double(
44
+ 'ContentTypeCounts',
45
+ all: [
46
+ { content_type: 'document_chunk', count: 5 },
47
+ { content_type: 'observation', count: 1 }
48
+ ]
49
+ )
50
+ end
51
+ let(:active_entries) { instance_double('ActiveEntries', count: 5) }
52
+ let(:recent_entries) { instance_double('RecentEntries', count: 2) }
53
+
54
+ before do
55
+ stub_const('Legion::Data::Model::ApolloEntry', entry_model)
56
+ stub_const('Legion::Data::Model::ApolloRelation', relation_model)
57
+ allow(entry_model).to receive(:count).and_return(6)
58
+ allow(entry_model).to receive(:avg).with(:confidence).and_return(0.81234)
59
+ allow(entry_model).to receive(:exclude).with(status: 'archived').and_return(active_entries)
60
+ allow(entry_model).to receive(:where).and_return(recent_entries)
61
+ allow(entry_model).to receive(:group_and_count).with(:status).and_return(status_counts)
62
+ allow(entry_model).to receive(:group_and_count).with(:content_type).and_return(content_type_counts)
63
+ end
64
+
65
+ it 'returns the health UI metrics expected by Interlink' do
66
+ payload = described_class.stats_payload(now: Time.utc(2026, 4, 28, 12, 0, 0))
67
+
68
+ expect(payload).to include(
69
+ total_entries: 6,
70
+ recent_24h: 2,
71
+ avg_confidence: 0.812,
72
+ total_relations: 4
73
+ )
74
+ expect(payload[:by_status]).to include(
75
+ 'candidate' => 2,
76
+ 'confirmed' => 3,
77
+ 'archived' => 1,
78
+ 'active' => 5
79
+ )
80
+ expect(payload[:by_content_type]).to eq(
81
+ 'document_chunk' => 5,
82
+ 'observation' => 1
83
+ )
84
+ end
85
+
86
+ it 'returns an apollo data error when the entry model is unavailable' do
87
+ hide_const('Legion::Data::Model::ApolloEntry')
88
+
89
+ expect(described_class.stats_payload).to eq(error: 'apollo_data_not_available')
90
+ end
91
+ end
28
92
  end
@@ -337,6 +337,36 @@ RSpec.describe Legion::Extensions::Apollo::Runners::Knowledge do
337
337
  )
338
338
  end
339
339
  end
340
+
341
+ context 'early-return warn logs' do
342
+ let(:logger) { instance_double('Logger', debug: nil, info: nil, warn: nil) }
343
+
344
+ before { allow(host).to receive(:log).and_return(logger) }
345
+
346
+ it 'emits a warn log when content is nil' do
347
+ host.handle_ingest(content: nil, content_type: 'fact')
348
+ expect(logger).to have_received(:warn).with(/early-return: content is required/)
349
+ end
350
+
351
+ it 'emits a warn log when content_type is nil' do
352
+ host.handle_ingest(content: 'something', content_type: nil)
353
+ expect(logger).to have_received(:warn).with(/early-return: content_type is required/)
354
+ end
355
+
356
+ it 'emits a warn log when apollo_data_not_available' do
357
+ hide_const('Legion::Data::Model::ApolloEntry') if defined?(Legion::Data::Model::ApolloEntry)
358
+ host.handle_ingest(content: 'something', content_type: 'fact')
359
+ expect(logger).to have_received(:warn).with(/early-return: apollo_data_not_available/)
360
+ end
361
+
362
+ it 'sanitizes newline-bearing tags in the warn log' do
363
+ host.handle_ingest(content: nil, content_type: 'fact', tags: ["evil\nFAKE LOG LINE", 'normal'])
364
+ expect(logger).to have_received(:warn) do |msg|
365
+ expect(msg).to include('evil FAKE LOG LINE')
366
+ expect(msg).not_to include("\n")
367
+ end
368
+ end
369
+ end
340
370
  end
341
371
 
342
372
  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.20
4
+ version: 0.4.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity