robot_lab-durable 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: b576c8ce4dc5ca5b4741cf6b7d9a95b0b653b9f54ab15aa02e0114a2e7cf43d4
4
- data.tar.gz: 5079f8bd4f6a63fdcc9938da446db749ca1e93ab844aa5d47c4a8fbd025d189f
3
+ metadata.gz: 54a17c8e6e00177f47175470d75d785d95aa4322ee89f1dd89702b218491b2e8
4
+ data.tar.gz: 0d87cba5d51a23813f7592058daf4f4f3e12822d6545d03bbcb363cf8a9013b0
5
5
  SHA512:
6
- metadata.gz: 7834ab27785b8f43f52e67dad700dd9a5a886cb0fb265d84e46b4cc79c0e123b5181847d17e87260400d6c3db3ac2212507ae7205186f684a6b8962270f49b57
7
- data.tar.gz: cc57c77b81eaf328a07d167dc5d053dff56599935e51ada19fc50ac39bb73a73c1f63c28d387d77e3380693871b20e4e33c787cd9a2c39cd6d8734add3e8f19f
6
+ metadata.gz: bb2cd6073cbd35a76fed4d8a741171b40109d5c0739297879460b37d7a9141e2d4391247c2ff80118d661cc648380fbca5fa54f54afb34dce833f4ae2e6f1eee
7
+ data.tar.gz: ec9d1af50d43b74ef6826e2216173d990101028394b4960414c7ff64bded29301b13879c6816e1c37a257a03fcf4e3950d47fc499fd06c0d035cb323400a794d
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-durable — cross-session persistent robot memory and learned behaviors.
3
+
4
+ import_up "repo_dev.loki"
5
+
6
+ class Tasks
7
+ @@gem_name ||= "robot_lab-durable".freeze
8
+
9
+ header "robot_lab-durable v#{gem_version} — cross-session persistent learning"
10
+ end
File without changes
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,47 @@
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
+
13
+ ## [0.3.0] - 2026-05-24
14
+
15
+ ### Changed (breaking)
16
+ - **Backend replaced**: YAML file store replaced entirely by the `htm` gem (PostgreSQL + pgvector).
17
+ `robot_lab-durable` is now a thin adapter between `robot_lab` and `htm`.
18
+ - `Durable::Entry` is now a presenter built from HTM node hashes via `Entry.from_node(node)`.
19
+ Removed fields: `domain`, `confidence`, `use_count`. Added: `node_id`.
20
+ - `Durable::Hook` no longer manages a domain or seeds past knowledge into session memory.
21
+ `around_run` initialises a thread-local `Adapter` from `ctx.robot.name`; `on_learn` persists
22
+ new learnings via `adapter.record`. `Hook.current_adapter` returns the active adapter.
23
+ - `RecallKnowledge` and `RecordKnowledge` now delegate to `Hook.current_adapter` instead of a
24
+ YAML store. The `domain:` parameter is removed from both tools.
25
+ - `Gemfile` updated: depends on published `htm` gem and local `robot_lab` path.
26
+
27
+ ### Added
28
+ - `Durable::Adapter` — wraps `HTM.new(robot_name:)`, exposes `recall(query:, ...)` and
29
+ `record(content:, reasoning:, category:)`. HTM deduplicates by content hash, eliminating
30
+ the need for double-write prevention.
31
+ - Full test suite for `Durable::Adapter` (`test/robot_lab/durable/adapter_test.rb`) using a
32
+ `FakeHTM` stand-in — no PostgreSQL required.
33
+ - `examples/01_day_trader.rb` — demo launcher that spawns the generator and predictor as
34
+ subprocesses and relays their output with colour-coded `[GEN ]`/`[PRED]` prefixes.
35
+ - `examples/day_trader_lib/generator.rb` — XYZZY fake stock price stream via Redis (GBM model).
36
+ - `examples/day_trader_lib/predictor.rb` — SMA+EMA ensemble tuned by a durable agent;
37
+ prints `✓ GOOD / ✗ MISS` verdict after each prediction window.
38
+
39
+ ### Removed
40
+ - `Durable::Store` (YAML-backed per-domain file store).
41
+ - `Durable::Reflector` (end-of-run confidence promoter).
42
+ - `domain` concept throughout — HTM uses `robot_name` for scoping.
43
+ - `skip_persist` mechanism — HTM SHA-256 content deduplication makes it unnecessary.
44
+
3
45
  ## [0.2.1] - 2026-05-19
4
46
 
5
47
  ### Added
data/CLAUDE.md ADDED
@@ -0,0 +1,65 @@
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-durable` gives RobotLab robots cross-session persistent memory backed by the `htm` gem (Hierarchical Temporal Memory). Knowledge is recorded by robots during a session and recalled in future sessions using hybrid semantic search (vector + full-text).
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
+ All source lives under `lib/robot_lab/durable/` plus two tool files at `lib/robot_lab/`.
19
+
20
+ **`Adapter`** (`durable/adapter.rb`) — Wraps an `HTM` instance for one robot. Two operations:
21
+ - `recall(query:, limit:, strategy:)` — calls `htm.recall` and maps nodes to `Entry` structs. Strategies: `:vector`, `:fulltext`, `:hybrid` (default)
22
+ - `record(content:, reasoning:, category:)` — calls `htm.remember` with metadata hash
23
+
24
+ **`Entry`** (`durable/entry.rb`) — `Data.define(:node_id, :content, :reasoning, :category, :created_at)`. Immutable value object. `category` is always a Symbol. Built via `Entry.from_node(node)` from raw HTM node hashes.
25
+
26
+ **`Hook`** (`durable/hook.rb`) — Subclass of `RobotLab::Hook` with `namespace = :durable`. Manages a per-thread `Adapter` instance:
27
+ - `around_run` — creates an `Adapter` for the robot, stores it in `Thread.current`, tears it down in `ensure`
28
+ - `on_learn` — called by the `:learn` hook family when the robot learns something; persists to HTM if `ctx.stored` is true
29
+
30
+ **`RecallKnowledge`** (`recall_knowledge.rb`) — `RobotLab::Tool` subclass. Takes a `query` param, calls `Hook.current_adapter.recall`, formats results for the LLM. Returns "No relevant past knowledge found" when empty — instructs the robot to skip rather than guess.
31
+
32
+ **`RecordKnowledge`** (`record_knowledge.rb`) — `RobotLab::Tool` subclass. Takes `content`, `reasoning`, `category` params. Calls `adapter.record` then `robot.learn(content)` to update in-session memory immediately. HTM deduplicates by content hash so double-writes are harmless.
33
+
34
+ ## Enabling
35
+
36
+ ```ruby
37
+ require 'robot_lab'
38
+ require 'robot_lab/durable'
39
+
40
+ robot = RobotLab.build(
41
+ name: "advisor",
42
+ local_tools: [RobotLab::RecallKnowledge, RobotLab::RecordKnowledge]
43
+ )
44
+ robot.on(RobotLab::Durable::Hook)
45
+ result = robot.run("Remember my preference for concise answers.")
46
+ ```
47
+
48
+ ## Valid Categories
49
+
50
+ `fact`, `preference`, `pattern`, `correction`, `observation`
51
+
52
+ The LLM is free to choose — these are stored as metadata strings and returned as Symbols in `Entry#category`.
53
+
54
+ ## Key Constraints
55
+
56
+ - The `Adapter` is scoped to `Thread.current` — it is NOT available across Ractors or Fibers.
57
+ - `RecordKnowledge` calls `robot.learn` internally; this triggers the `:learn` hook which calls `on_learn`. HTM deduplication prevents a double-write.
58
+ - `Hook` loads only when `RobotLab::Hook` is defined; `RecallKnowledge`/`RecordKnowledge` load only when `RobotLab::Tool` is defined. Always `require 'robot_lab'` before `require 'robot_lab/durable'`.
59
+ - The `htm` gem manages its own storage backend (PostgreSQL with vector extensions). Ensure the HTM database is configured before use.
60
+
61
+ ## Testing
62
+
63
+ - Minitest with SimpleCov (branch coverage tracked, no minimum threshold enforced yet)
64
+ - Tests stub `HTM` — do not use a real database in unit tests
65
+ - Coverage baseline: ~83% line / ~44% branch
data/README.md CHANGED
@@ -1,18 +1,23 @@
1
1
  # robot_lab-durable
2
2
 
3
- Cross-session durable learning for the [RobotLab](https://github.com/MadBomber/robot_lab) LLM agent framework.
3
+ Cross-session durable memory for the [RobotLab](https://github.com/MadBomber/robot_lab) LLM agent framework, backed by the [htm gem](https://madbomber.github.io/htm) (PostgreSQL + pgvector).
4
4
 
5
5
  > [!CAUTION]
6
6
  > This gem is under active development. APIs may change without notice.
7
7
 
8
8
  ## What it provides
9
9
 
10
- - **`Durable::Entry`** — immutable, confidence-tracked knowledge record
11
- - **`Durable::Store`** — YAML-backed, file-locked per-domain knowledge persistence in `~/.robot_lab/durable/`
12
- - **`Durable::Reflector`** — promotes session-level learnings into the durable store at end-of-run
13
- - **`Durable::Learning`** — mixin included into `RobotLab::Robot`; enabled via `learn: true, learn_domain:` constructor params
14
- - **`RecallKnowledge`** tool — lets robots query the durable store before making decisions
15
- - **`RecordKnowledge`** tool — lets robots write new knowledge during a session
10
+ - **`Durable::Adapter`** — thin wrapper around an `HTM` instance; scoped to a robot by name. Exposes `record` and `recall` methods.
11
+ - **`Durable::Entry`** — presenter over raw HTM node hashes returned by recall. Provides typed fields: `node_id`, `content`, `reasoning`, `category`, `created_at`.
12
+ - **`Durable::Hook`** — RobotLab hook handler that wires the `Adapter` lifecycle into `around_run` and `on_learn` automatically.
13
+ - **`RecallKnowledge`** tool lets a robot query its durable memory before answering.
14
+ - **`RecordKnowledge`** tool — lets a robot persist new knowledge during a session.
15
+
16
+ ## Prerequisites
17
+
18
+ - PostgreSQL with the pgvector extension enabled.
19
+ - The `htm` gem — see documentation at https://madbomber.github.io/htm.
20
+ - The `robot_lab` gem.
16
21
 
17
22
  ## Installation
18
23
 
@@ -23,6 +28,12 @@ gem "robot_lab"
23
28
  gem "robot_lab-durable"
24
29
  ```
25
30
 
31
+ Then run:
32
+
33
+ ```
34
+ bundle install
35
+ ```
36
+
26
37
  ## Quick Example
27
38
 
28
39
  ```ruby
@@ -30,53 +41,77 @@ require "robot_lab"
30
41
  require "robot_lab/durable"
31
42
 
32
43
  robot = RobotLab.build(
33
- name: "advisor",
34
- system_prompt: "You are a financial advisor that learns from each session.",
35
- learn: true,
36
- learn_domain: "finance"
44
+ name: "advisor",
45
+ system_prompt: "You are a financial advisor.",
46
+ local_tools: [RobotLab::RecallKnowledge, RobotLab::RecordKnowledge]
37
47
  )
48
+ robot.on(RobotLab::Durable::Hook)
38
49
 
39
- # RecallKnowledge and RecordKnowledge tools are automatically available.
40
- # At the end of each run, the Reflector promotes learned facts to
41
- # ~/.robot_lab/durable/finance.yml for use in future sessions.
42
50
  result = robot.run("What do you know about my risk tolerance?")
43
51
  puts result.last_text_content
44
52
  ```
45
53
 
54
+ No `domain:` argument is required. HTM scopes all storage to the robot's `name` automatically.
55
+
46
56
  ## How It Works
47
57
 
48
- When `learn: true` and `learn_domain:` are set, the robot gains two built-in tools:
58
+ ### Hook lifecycle
49
59
 
50
- - **`RecallKnowledge`** queries the domain's YAML store for relevant past knowledge before responding
51
- - **`RecordKnowledge`** — writes new knowledge entries during a session
60
+ `RobotLab::Durable::Hook` is registered via `robot.on(...)` and plugs into two lifecycle events:
52
61
 
53
- At the end of each run, `Durable::Reflector` promotes session-level learnings into the persistent store with confidence scoring and deduplication.
62
+ - **`around_run`** — creates a thread-local `Adapter` instance keyed to `ctx.robot.name`, makes it available to tools for the duration of the run, and clears it in `ensure` so state never leaks across run boundaries.
63
+ - **`on_learn`** — fires after each new session learning is stored in memory. When a durable session is active, persists the text via `adapter.record(content:, category: :observation)`.
54
64
 
55
- ## Knowledge Persistence
65
+ ### Adapter
56
66
 
57
- ```
58
- ~/.robot_lab/durable/
59
- finance.yml # per-domain YAML store
60
- support.yml
61
- ...
62
- ```
67
+ `Durable::Adapter` wraps an `HTM` instance and provides two methods:
68
+
69
+ | Method | Description |
70
+ |--------|-------------|
71
+ | `record(content:, reasoning: nil, category: 'fact')` | Stores a memory entry. HTM deduplicates by SHA-256 content hash, so calling `record` with identical content is safe. |
72
+ | `recall(query:, limit: 20, strategy: :hybrid)` | Searches stored memory and returns an array of `Entry` objects. |
73
+ | `htm` | Direct access to the underlying `HTM` instance for advanced use. |
63
74
 
64
- Each entry records: `content`, `confidence`, `category`, `domain`, `use_count`, `created_at`, and `updated_at`.
75
+ ### Entry
65
76
 
66
- Knowledge confidence grows as the same fact is recalled and confirmed across sessions. Low-confidence entries are pruned automatically over time.
77
+ `Durable::Entry` is a presenter built via `Entry.from_node(node)` over the raw hashes returned by `HTM#recall`. It exposes:
67
78
 
68
- ## Relationship to `robot.learn()`
79
+ | Field | Type | Description |
80
+ |-------|------|-------------|
81
+ | `node_id` | String | Unique HTM node identifier |
82
+ | `content` | String | The stored knowledge text |
83
+ | `reasoning` | String / nil | Optional rationale recorded at write time |
84
+ | `category` | Symbol | `:fact`, `:observation`, or any custom value |
85
+ | `created_at` | Time | When the entry was first stored |
69
86
 
70
- `robot.learn()` is a core RobotLab method that accumulates observations within a single session in memory. `robot_lab-durable` extends this by persisting those observations to disk across sessions, making the robot's learning accumulate over its lifetime rather than resetting each run.
87
+ ## Tools
88
+
89
+ ### RecallKnowledge
90
+
91
+ Calls `Hook.current_adapter.recall(query:)` with the query text provided by the LLM and returns formatted entries. Uses the `:hybrid` strategy by default.
92
+
93
+ ### RecordKnowledge
94
+
95
+ Calls `adapter.record(content:, reasoning:, category:)` to persist a new memory, then calls `robot.learn(content)` to update session context. Because HTM deduplicates by content hash, no explicit double-write prevention is necessary.
96
+
97
+ ## Day Trader Demo
98
+
99
+ `examples/01_day_trader.rb` demonstrates cross-session durable memory in action. It spawns a GBM stock price generator and an SMA+EMA ensemble predictor as subprocesses. Output is colour-coded `[GEN ]` / `[PRED]` per process, with a GOOD/MISS verdict printed for each prediction window. The durable agent tunes its prediction parameters across sessions, accumulating knowledge about what works.
100
+
101
+ ```
102
+ ruby examples/01_day_trader.rb
103
+ ```
71
104
 
72
105
  ## Links
73
106
 
74
107
  - [RobotLab Core](https://github.com/MadBomber/robot_lab)
108
+ - [HTM gem docs](https://madbomber.github.io/htm)
75
109
  - [RubyGems](https://rubygems.org/gems/robot_lab-durable)
110
+ - [GitHub](https://github.com/MadBomber/robot_lab-durable)
76
111
 
77
112
  ## License
78
113
 
79
- MIT License - Copyright (c) 2025 Dewayne VanHoozer
114
+ MIT License Copyright (c) 2025 Dewayne VanHoozer
80
115
 
81
116
  ## Contributing
82
117
 
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
@@ -1,42 +1,9 @@
1
1
  # Delegate Storage to robot_lab-document_store — Design Discussion
2
2
 
3
3
  **Date:** 2026-05-14
4
- **Status:** Parked — resume when time allows
4
+ **Original status:** Parked — resume when time allows
5
+ **Current status:** Superseded — see [docs/index.md](index.md)
5
6
 
6
- ## The Problem
7
+ The document_store delegation path was abandoned. In v0.3.0, `robot_lab-durable` became a direct adapter over the [`htm` gem](https://madbomber.github.io/htm) (PostgreSQL + pgvector), replacing the YAML store, the `Durable::Store` class, `Durable::Reflector`, and all domain/confidence concepts entirely. The open questions raised here — search semantics, structured metadata, backend versioning — are resolved by HTM's native hybrid recall and metadata hash support.
7
8
 
8
- `robot_lab-durable` currently maintains its own `Store` class: a YAML-backed, file-locked, keyword-search storage layer in `lib/robot_lab/durable/store.rb`. This is duplicated effort — `robot_lab-document_store` is intended to be the canonical pluggable storage abstraction for the robot_lab ecosystem.
9
-
10
- ## The Vision
11
-
12
- Once `robot_lab-document_store` gains a `DocumentStore::FileSystem` backend, durable should drop its custom `Store` class and delegate physical storage to it. Durable retains its own concerns:
13
-
14
- - `Entry` — immutable value object with confidence scoring, category, domain, use_count
15
- - `Reflector` — end-of-session promoter that pushes session learnings into the store
16
- - `Learning` — mixin included into `Robot` when robot_lab is present
17
- - `RecallKnowledge` / `RecordKnowledge` — LLM tools that interact with the store
18
-
19
- ## What Changes in This Gem
20
-
21
- 1. Add `robot_lab-document_store` as a runtime dependency in the gemspec.
22
- 2. Remove `lib/robot_lab/durable/store.rb`.
23
- 3. Wire `Learning#setup_durable_learning` to instantiate a `DocumentStore::FileSystem` instead of `Durable::Store`.
24
- 4. Adapt `Reflector` and the two tools to call the `DocumentStore` interface (`store`, `search`, `delete`, etc.) rather than the old `Store` API.
25
- 5. Handle `Entry` serialization — since `DocumentStore` stores raw text by key, durable will serialize `Entry` fields into text (or use a metadata hash if `FileSystem` supports it — see open questions).
26
-
27
- ## Open Questions (shared with robot_lab-document_store)
28
-
29
- 1. **Search semantics.** `DocumentStore::Memory` uses embedding-based cosine similarity; `DocumentStore::FileSystem` would use keyword matching. Should the interface declare its search capability, or do callers accept whatever the backend provides?
30
-
31
- 2. **Structured vs raw text.** `Entry` carries structured fields (confidence, category, domain, use_count). Options:
32
- - Durable serializes all fields into the stored text string; deserializes on recall.
33
- - `DocumentStore::FileSystem` supports an optional `meta:` Hash alongside text, which durable populates with `Entry` fields.
34
-
35
- ## Versioning
36
-
37
- - `robot_lab-document_store` must ship `DocumentStore::FileSystem` first — that is a v0.3.0 breaking change for that gem (v0.2.1 is the current release).
38
- - This gem (`robot_lab-durable`) then bumps to v0.3.0 once it drops `Store` and depends on document_store.
39
-
40
- ## See Also
41
-
42
- `robot_lab-document_store/docs/pluggable_backends_design.md` — the full backend architecture design including the `DocumentStore` abstract interface and implementation plan.
9
+ For the current architecture, see [docs/index.md](index.md) and the [HTM gem documentation](https://madbomber.github.io/htm).