robot_lab-document_store 0.2.1 → 0.2.6

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: 421276837702fa51fef2d7eb2e776634f7f5e9f937b85bc832afb1ba6d0da978
4
- data.tar.gz: 6ecb47f66596a40a8e36abe49880f0001b1bc6ed6b9f3fd976b4c0b36b6ede29
3
+ metadata.gz: c76daa301371b72ad1a128cc287b84a4b86962851d581807a3b4a86eea758d7a
4
+ data.tar.gz: 3dd0e51ac30391feb45ea59cede650a1d137b9fc54d02cbf63479a484036c6aa
5
5
  SHA512:
6
- metadata.gz: 6f56c72155401b3f79acbd3ed9c70ab8a62081b2a7b587d415a93a5143bc716f73a54c2eecaf785598ef923b271dfb7dad3e799cf7c8c06dbbb0b5a311b79a29
7
- data.tar.gz: 2643e466fefa4f97c2ce218e33e2904c3d40845b242276997e58475f46b59517e05cf810e5c43aab80e978b1b43990fe9c990990147e927a0776fb93c3c6fda9
6
+ metadata.gz: fb965431ab53192605471ec0b06f9664fc57bea6a97e53bd0956b074e53e95f34a4c689624a171bb655847cbb8d45b9417f6112daa2e89de23618cb2978f94fd
7
+ data.tar.gz: ee8900ebb335b40ffc7adc00db2562919a3d7fbb5eec492cda848b041a458154ca513ac1a8f0a3c8c9a665836d7739faa67e102710576d89d49c8f502115a3a0
data/.envrc CHANGED
@@ -1 +1,3 @@
1
+ source_up
1
2
  export RR=`pwd`
3
+ export BUNDLE_GEMFILE=Gemfile
data/.loki ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ # robot_lab-document_store — vector embeddings & semantic search.
3
+
4
+ import_up "repo_dev.loki"
5
+
6
+ class Tasks
7
+ @@gem_name ||= "robot_lab-document_store".freeze
8
+
9
+ header "robot_lab-document_store v#{gem_version} — vector embeddings & semantic search"
10
+ end
@@ -0,0 +1 @@
1
+ 9 lib/robot_lab/document_store.rb
data/.rubocop.yml CHANGED
@@ -1,173 +1 @@
1
- AllCops:
2
- NewCops: enable
3
- SuggestExtensions: false
4
- TargetRubyVersion: 4.0
5
- Exclude:
6
- - 'examples/**/*'
7
- - 'vendor/**/*'
8
- - 'dead_code/**/*'
9
-
10
- # ── Style: disabled cops ───────────────────────────────────────────────────
11
- Style/StringLiterals:
12
- Enabled: false
13
-
14
- Style/StringLiteralsInInterpolation:
15
- Enabled: false
16
-
17
- Style/Documentation:
18
- Enabled: false
19
-
20
- # Ruby 4.0 freezes string literals by default
21
- Style/FrozenStringLiteralComment:
22
- Enabled: false
23
-
24
- Style/IfUnlessModifier:
25
- Enabled: false
26
-
27
- Style/RescueModifier:
28
- Enabled: false
29
-
30
- Style/TrivialAccessors:
31
- Enabled: false
32
-
33
- Style/MultilineTernaryOperator:
34
- Enabled: false
35
-
36
- Style/SafeNavigation:
37
- Enabled: false
38
-
39
- Style/EmptyClassDefinition:
40
- Enabled: false
41
-
42
- Style/ClassAndModuleChildren:
43
- Enabled: false
44
-
45
- Style/RescueStandardError:
46
- Enabled: false
47
-
48
- Style/OneClassPerFile:
49
- Enabled: false
50
-
51
- # Both % and format/sprintf are acceptable
52
- Style/FormatString:
53
- Enabled: false
54
-
55
- # String concatenation and interpolation are both acceptable
56
- Style/StringConcatenation:
57
- Enabled: false
58
-
59
- # ── Layout ─────────────────────────────────────────────────────────────────
60
- Layout/LineLength:
61
- Max: 140
62
-
63
- Layout/ExtraSpacing:
64
- Enabled: false
65
-
66
- Layout/HashAlignment:
67
- Enabled: false
68
-
69
- Layout/FirstHashElementIndentation:
70
- Enabled: false
71
-
72
- Layout/EmptyLineAfterGuardClause:
73
- Enabled: false
74
-
75
- # ── Naming ─────────────────────────────────────────────────────────────────
76
- # Single-char params (c, e, n) are acceptable throughout
77
- Naming/MethodParameterName:
78
- Enabled: false
79
-
80
- Naming/VariableNumber:
81
- Exclude:
82
- - 'test/**/*'
83
-
84
- Naming/RescuedExceptionsVariableName:
85
- Enabled: false
86
-
87
- # set_results and similar explicit setters are clear and conventional
88
- Naming/AccessorMethodName:
89
- Enabled: false
90
-
91
-
92
- # has_tool_calls? and similar are clear and conventional
93
- Naming/PredicatePrefix:
94
- Enabled: false
95
-
96
- # Test helper methods don't need to follow predicate naming rules
97
- Naming/PredicateMethod:
98
- Exclude:
99
- - 'test/**/*'
100
-
101
- # ── Lint: relax noisy cops on intentional patterns ─────────────────────────
102
- # Library and framework methods commonly accept args for API/documentation purposes
103
- Lint/UnusedMethodArgument:
104
- Enabled: false
105
-
106
-
107
- Lint/EmptyBlock:
108
- Exclude:
109
- - 'test/**/*'
110
-
111
- Lint/ConstantDefinitionInBlock:
112
- Exclude:
113
- - 'Rakefile'
114
- - 'test/**/*'
115
-
116
- # ── Gemspec ────────────────────────────────────────────────────────────────
117
- Gemspec/DevelopmentDependencies:
118
- EnforcedStyle: Gemfile
119
-
120
- Gemspec/RequiredRubyVersion:
121
- Enabled: false
122
-
123
- Gemspec/OrderedDependencies:
124
- Enabled: false
125
-
126
- # ── Metrics ────────────────────────────────────────────────────────────────
127
- # Framework-level code (routers, parsers, orchestrators) is inherently complex.
128
- # Flog is the primary complexity gate — these RuboCop thresholds catch only
129
- # egregious outliers without false-positiving every dispatch method.
130
-
131
- Metrics/MethodLength:
132
- Max: 35
133
- CountAsOne:
134
- - heredoc
135
- - array
136
- - hash
137
- Exclude:
138
- - 'test/**/*'
139
-
140
- Metrics/AbcSize:
141
- Max: 40
142
- Exclude:
143
- - 'test/**/*'
144
-
145
- Metrics/ClassLength:
146
- Max: 600
147
- Exclude:
148
- - 'test/**/*'
149
-
150
- Metrics/ModuleLength:
151
- Max: 200
152
- Exclude:
153
- - 'test/**/*'
154
-
155
- Metrics/CyclomaticComplexity:
156
- Max: 20
157
- Exclude:
158
- - 'test/**/*'
159
-
160
- Metrics/PerceivedComplexity:
161
- Max: 20
162
- Exclude:
163
- - 'test/**/*'
164
-
165
- # Long method signatures with keyword args are a Ruby framework idiom
166
- Metrics/ParameterLists:
167
- Enabled: false
168
-
169
- Metrics/BlockLength:
170
- Exclude:
171
- - 'Rakefile'
172
- - '*.gemspec'
173
- - 'test/**/*'
1
+ inherit_from: ../.rubocop-base.yml
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added
4
+ - `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, and `console` tasks via the Asgard task runner
5
+ - `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
6
+ - `flay` and `minitest-reporters` gems added to development dependencies
7
+ - `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
8
+
9
+ ### Changed
10
+ - `test/test_helper.rb`: test output redirected to `test_output.txt` via `$stdout` reassignment; `TerminalSummaryReporter` prints a single PASS/FAIL summary line to the terminal
11
+ - `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
12
+
3
13
  ## [0.2.1] - 2026-05-19
4
14
 
5
15
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,72 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What This Gem Does
6
+
7
+ `robot_lab-document_store` provides a thread-safe, in-memory semantic document store for RobotLab. Documents are embedded and retrieved by cosine similarity. It uses `fastembed` (BAAI/bge-small-en-v1.5) when available, and falls back to a lightweight TF-IDF word-frequency embedder when fastembed is not installed.
8
+
9
+ ## Commands
10
+
11
+ ```bash
12
+ bundle exec rake test # Run full test suite
13
+ ruby -Ilib:test test/<file> # Run a single test file
14
+ ```
15
+
16
+ ## Architecture
17
+
18
+ The gem is a single class: `RobotLab::DocumentStore` (`lib/robot_lab/document_store.rb`).
19
+
20
+ ### Key Methods
21
+
22
+ ```ruby
23
+ store = RobotLab::DocumentStore.new # fastembed model chosen automatically
24
+ store = RobotLab::DocumentStore.new(model_name: "...") # override fastembed model
25
+
26
+ store.store(:key, "text") # embed and store; replaces existing key
27
+ store.search("query", limit: 5) # returns Array<{key:, text:, score:}>
28
+ store.size # Integer
29
+ store.keys # Array<Symbol>
30
+ store.empty? # Boolean
31
+ store.delete(:key) # remove one document
32
+ store.clear # remove all documents
33
+ ```
34
+
35
+ Search results are sorted by score descending (0.0–1.0). The fastembed model is initialised lazily on the first `store` or `search` call — the ONNX model file is downloaded then cached locally.
36
+
37
+ ### Embedding Paths
38
+
39
+ **Fastembed path** (when `fastembed` gem is installed):
40
+ - `passage_embed` for stored documents, `query_embed` for search queries — asymmetric embedding per the BGE model's design
41
+ - Dense float vectors, cosine similarity computed inline
42
+
43
+ **Fallback TF-IDF path** (when fastembed is absent):
44
+ - Stop-word filtered, Porter-style stemmed word frequency vectors
45
+ - Sparse `Hash{String => Float}` L2-normalised vectors
46
+ - `sparse_cosine` for similarity — no semantic understanding, lexical overlap only
47
+ - Good for development and testing without downloading ONNX models
48
+
49
+ ### Thread Safety
50
+
51
+ All reads and writes to `@documents` are protected by a `Mutex`. The fastembed model is not Mutex-protected because `Fastembed::TextEmbedding` is itself thread-safe.
52
+
53
+ ### Integration with robot_lab Memory
54
+
55
+ When `robot_lab-document_store` is loaded alongside `robot_lab`, the Memory class gains:
56
+ ```ruby
57
+ memory.store_document(:key, text)
58
+ memory.search_documents("query", limit: 5)
59
+ ```
60
+
61
+ ## Key Constraints
62
+
63
+ - Documents are stored entirely in memory — the store does not persist across process restarts.
64
+ - Keys are coerced to `Symbol`. Storing under the same key replaces the previous document.
65
+ - The TF-IDF fallback has no semantic understanding — "car" and "automobile" will not match. Use fastembed for production.
66
+ - `FASTEMBED_AVAILABLE` is set at load time and cannot be changed at runtime.
67
+
68
+ ## Testing
69
+
70
+ - Minitest with SimpleCov (`minimum_coverage line: 95, branch: 75` enforced)
71
+ - Tests stub fastembed to avoid downloading the ONNX model — do not call the real fastembed model in unit tests
72
+ - Coverage: 98.89% line / 95.83% branch
data/Rakefile CHANGED
@@ -24,16 +24,6 @@ task :test_file, [:file] do |_t, args|
24
24
  ruby "test/#{args[:file]}"
25
25
  end
26
26
 
27
- desc 'Check code style with RuboCop'
28
- task :rubocop do
29
- sh 'bundle exec rubocop'
30
- end
31
-
32
- desc 'Auto-correct RuboCop offenses'
33
- task :rubocop_fix do
34
- sh 'bundle exec rubocop -a'
35
- end
36
-
37
27
  desc 'Check code complexity with Flog (warn >=20, fail >=50)'
38
28
  task :flog_check do
39
29
  require 'flog'
@@ -71,36 +61,62 @@ task :flog_check do
71
61
  end
72
62
  end
73
63
 
74
- desc 'Run all quality checks: tests (with coverage), RuboCop, and Flog'
75
- task :quality do
76
- results = {}
64
+ desc 'Check for structural code duplication with Flay (mass >= 50)'
65
+ task :flay_check do
66
+ require 'flay'
77
67
 
78
- puts "\n#{'=' * 60}"
79
- puts 'Quality Gate: Tests + Coverage'
80
- puts '=' * 60
81
- results[:tests] = system('bundle exec rake test') ? :pass : :fail
68
+ mass_threshold = 50
82
69
 
83
- puts "\n#{'=' * 60}"
84
- puts 'Quality Gate: RuboCop'
85
- puts '=' * 60
86
- results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
70
+ flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
71
+ flay.process(*Dir.glob('lib/**/*.rb'))
72
+ flay.analyze
87
73
 
88
- puts "\n#{'=' * 60}"
89
- puts 'Quality Gate: Flog Complexity'
90
- puts '=' * 60
91
- results[:flog] = system('bundle exec rake flog_check') ? :pass : :fail
74
+ if flay.hashes.empty?
75
+ puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
76
+ else
77
+ puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
78
+ flay.report
79
+ abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
80
+ end
81
+ end
82
+
83
+ desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
84
+ task :quality do
85
+ gates = [
86
+ ['Tests + Coverage', 'bundle exec rake test'],
87
+ ['RuboCop', 'bundle exec rubocop'],
88
+ ['Flog Complexity', 'bundle exec rake flog_check'],
89
+ ['Flay Duplication', 'bundle exec rake flay_check']
90
+ ]
91
+
92
+ results = gates.map do |label, command|
93
+ puts "\n#{'=' * 60}"
94
+ puts "Quality Gate: #{label}"
95
+ puts '=' * 60
96
+ [label, system(command) ? :pass : :fail]
97
+ end
98
+
99
+ green = ->(s) { "\e[32m#{s}\e[0m" }
100
+ red = ->(s) { "\e[31m#{s}\e[0m" }
101
+ width = results.map { |label, _| label.length }.max
92
102
 
93
103
  puts "\n#{'=' * 60}"
94
- puts 'Quality Summary'
104
+ puts 'Quality Gate Summary'
95
105
  puts '=' * 60
96
- results.each do |gate, status|
97
- icon = status == :pass ? 'PASS' : 'FAIL'
98
- puts " [#{icon}] #{gate}"
106
+ results.each do |label, status|
107
+ badge = status == :pass ? green.call('PASS') : red.call('FAIL')
108
+ puts " [#{badge}] #{label.ljust(width)}"
99
109
  end
110
+ puts '-' * 60
111
+
112
+ passed = results.count { |_, s| s == :pass }
113
+ failed = results.count { |_, s| s == :fail }
114
+ tally = "#{passed} passed, #{failed} failed"
115
+ puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
100
116
  puts '=' * 60
101
117
 
102
- abort "\nQuality gate failed" if results.values.any?(:fail)
103
- puts "\nAll quality gates passed."
118
+ abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
119
+ puts "\n#{green.call('All quality gates passed.')}"
104
120
  end
105
121
 
106
122
  namespace :docs do
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Prerequisites
4
4
 
5
- - Ruby 3.1+
5
+ - Ruby 3.2+
6
6
  - **fastembed** (recommended) — requires a platform that can run ONNX Runtime (x86_64 and ARM64 macOS/Linux). On first use the ~23 MB `BAAI/bge-small-en-v1.5` model file is downloaded and cached in `~/.cache/fastembed`.
7
7
  - Without fastembed the store still works using the built-in TF-IDF fallback (see [Fallback Mode](#fallback-mode) below).
8
8
 
data/docs/how_it_works.md CHANGED
@@ -64,7 +64,7 @@ term frequencies.
64
64
  raw text
65
65
  → downcase
66
66
  → tokenise /[a-z]+/ (ASCII only — Unicode letters are dropped)
67
- → remove STOP_WORDS (38 common English words: a, an, the, is, are, …)
67
+ → remove STOP_WORDS (45 common English words: a, an, the, is, are, …)
68
68
  → Porter-style stem (strips: -ies, -ness, -ment, -tion, -ing, -ed, -er, -ly, -s)
69
69
  → count term frequencies
70
70
  → L2-normalise counts (divide each count by the Euclidean norm of the count vector)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RobotLab
4
4
  class DocumentStore
5
- VERSION = '0.2.1'
5
+ VERSION = '0.2.6'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robot_lab-document_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -50,8 +50,11 @@ extra_rdoc_files: []
50
50
  files:
51
51
  - ".envrc"
52
52
  - ".github/workflows/deploy-github-pages.yml"
53
+ - ".loki"
54
+ - ".quality/reek_baseline.txt"
53
55
  - ".rubocop.yml"
54
56
  - CHANGELOG.md
57
+ - CLAUDE.md
55
58
  - LICENSE.txt
56
59
  - README.md
57
60
  - Rakefile
@@ -93,7 +96,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
96
  - !ruby/object:Gem::Version
94
97
  version: '0'
95
98
  requirements: []
96
- rubygems_version: 4.0.11
99
+ rubygems_version: 4.0.17
97
100
  specification_version: 4
98
101
  summary: Embedding-based semantic document store for RobotLab agents
99
102
  test_files: []