lex-coldstart 0.1.0
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 +7 -0
- data/Gemfile +8 -0
- data/lex-coldstart.gemspec +30 -0
- data/lib/legion/extensions/coldstart/actors/imprint.rb +33 -0
- data/lib/legion/extensions/coldstart/client.rb +23 -0
- data/lib/legion/extensions/coldstart/helpers/bootstrap.rb +102 -0
- data/lib/legion/extensions/coldstart/helpers/claude_parser.rb +231 -0
- data/lib/legion/extensions/coldstart/helpers/imprint.rb +48 -0
- data/lib/legion/extensions/coldstart/local_migrations/20260316000050_create_bootstrap_state.rb +13 -0
- data/lib/legion/extensions/coldstart/runners/coldstart.rb +65 -0
- data/lib/legion/extensions/coldstart/runners/ingest.rb +174 -0
- data/lib/legion/extensions/coldstart/version.rb +9 -0
- data/lib/legion/extensions/coldstart.rb +23 -0
- data/spec/fixtures/sample_claude.md +25 -0
- data/spec/fixtures/sample_memory.md +23 -0
- data/spec/legion/extensions/coldstart/actors/imprint_spec.rb +50 -0
- data/spec/legion/extensions/coldstart/client_spec.rb +14 -0
- data/spec/legion/extensions/coldstart/helpers/claude_parser_spec.rb +205 -0
- data/spec/legion/extensions/coldstart/runners/coldstart_spec.rb +66 -0
- data/spec/legion/extensions/coldstart/runners/ingest_spec.rb +77 -0
- data/spec/local_persistence_spec.rb +139 -0
- data/spec/spec_helper.rb +20 -0
- metadata +95 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Coldstart
|
|
6
|
+
module Runners
|
|
7
|
+
module Ingest
|
|
8
|
+
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
|
|
9
|
+
Legion::Extensions::Helpers.const_defined?(:Lex)
|
|
10
|
+
|
|
11
|
+
# Ingest a single Claude memory or CLAUDE.md file into lex-memory traces.
|
|
12
|
+
# If lex-memory is not loaded, returns the parsed traces without storing.
|
|
13
|
+
#
|
|
14
|
+
# @param file_path [String] absolute path to the markdown file
|
|
15
|
+
# @param store_traces [Boolean] whether to store into lex-memory (default: true)
|
|
16
|
+
# @return [Hash] { file:, file_type:, traces_parsed:, traces_stored:, traces: }
|
|
17
|
+
def ingest_file(file_path:, store_traces: true, **)
|
|
18
|
+
unless File.exist?(file_path)
|
|
19
|
+
Legion::Logging.warn "[coldstart:ingest] file not found: #{file_path}"
|
|
20
|
+
return { file: file_path, error: 'file not found' }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
candidates = Helpers::ClaudeParser.parse_file(file_path)
|
|
24
|
+
file_type = Helpers::ClaudeParser.detect_file_type(file_path)
|
|
25
|
+
Legion::Logging.info "[coldstart:ingest] parsed #{candidates.size} traces from #{file_path} (#{file_type})"
|
|
26
|
+
|
|
27
|
+
stored = store_traces ? store_candidates(candidates) : []
|
|
28
|
+
|
|
29
|
+
{
|
|
30
|
+
file: file_path,
|
|
31
|
+
file_type: file_type,
|
|
32
|
+
traces_parsed: candidates.size,
|
|
33
|
+
traces_stored: stored.size,
|
|
34
|
+
traces: stored.empty? ? candidates : stored
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Ingest all CLAUDE.md and MEMORY.md files under a directory.
|
|
39
|
+
#
|
|
40
|
+
# @param dir_path [String] absolute path to the directory
|
|
41
|
+
# @param pattern [String] glob pattern (default: '**/{CLAUDE,MEMORY}.md')
|
|
42
|
+
# @param store_traces [Boolean] whether to store into lex-memory (default: true)
|
|
43
|
+
# @return [Hash] { directory:, files_found:, total_parsed:, total_stored:, files: }
|
|
44
|
+
def ingest_directory(dir_path:, pattern: '**/{CLAUDE,MEMORY}.md', store_traces: true, **)
|
|
45
|
+
unless Dir.exist?(dir_path)
|
|
46
|
+
Legion::Logging.warn "[coldstart:ingest] directory not found: #{dir_path}"
|
|
47
|
+
return { directory: dir_path, error: 'directory not found' }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
candidates = Helpers::ClaudeParser.parse_directory(dir_path, pattern: pattern)
|
|
51
|
+
files = candidates.map { |c| c[:source_file] }.uniq
|
|
52
|
+
Legion::Logging.info "[coldstart:ingest] parsed #{candidates.size} traces from #{files.size} files in #{dir_path}"
|
|
53
|
+
|
|
54
|
+
stored = store_traces ? store_candidates(candidates) : []
|
|
55
|
+
|
|
56
|
+
{
|
|
57
|
+
directory: dir_path,
|
|
58
|
+
files_found: files.size,
|
|
59
|
+
total_parsed: candidates.size,
|
|
60
|
+
total_stored: stored.size,
|
|
61
|
+
files: files
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Preview what traces would be created from a file without storing them.
|
|
66
|
+
#
|
|
67
|
+
# @param file_path [String] absolute path to the markdown file
|
|
68
|
+
# @return [Hash] { file:, file_type:, traces: }
|
|
69
|
+
def preview_ingest(file_path:, **)
|
|
70
|
+
return { file: file_path, error: 'file not found' } unless File.exist?(file_path)
|
|
71
|
+
|
|
72
|
+
candidates = Helpers::ClaudeParser.parse_file(file_path)
|
|
73
|
+
file_type = Helpers::ClaudeParser.detect_file_type(file_path)
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
file: file_path,
|
|
77
|
+
file_type: file_type,
|
|
78
|
+
traces: candidates
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def store_candidates(candidates)
|
|
85
|
+
return [] unless memory_available?
|
|
86
|
+
|
|
87
|
+
imprint = imprint_active_now?
|
|
88
|
+
runner = memory_runner
|
|
89
|
+
stored = []
|
|
90
|
+
|
|
91
|
+
candidates.each do |candidate|
|
|
92
|
+
result = runner.store_trace(
|
|
93
|
+
type: candidate[:trace_type],
|
|
94
|
+
content_payload: candidate[:content_payload],
|
|
95
|
+
domain_tags: candidate[:domain_tags],
|
|
96
|
+
origin: candidate[:origin],
|
|
97
|
+
confidence: candidate[:confidence],
|
|
98
|
+
imprint_active: imprint,
|
|
99
|
+
emotional_valence: candidate[:emotional_valence] || 0.0,
|
|
100
|
+
emotional_intensity: candidate[:emotional_intensity] || (candidate[:trace_type] == :firmware ? 0.8 : 0.3)
|
|
101
|
+
)
|
|
102
|
+
stored << result if result
|
|
103
|
+
rescue StandardError => e
|
|
104
|
+
Legion::Logging.warn "[coldstart:ingest] failed to store trace: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Flush the cache-backed store if it supports it
|
|
108
|
+
store = runner.send(:default_store)
|
|
109
|
+
store.flush if store.respond_to?(:flush)
|
|
110
|
+
|
|
111
|
+
Legion::Logging.info "[coldstart:ingest] stored #{stored.size} traces (imprint_active=#{imprint})"
|
|
112
|
+
|
|
113
|
+
# Co-activate traces from the same section to form Hebbian links
|
|
114
|
+
coactivate_section_traces(stored, candidates, runner)
|
|
115
|
+
|
|
116
|
+
stored
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def memory_available?
|
|
120
|
+
Legion::Extensions.const_defined?(:Memory) &&
|
|
121
|
+
Legion::Extensions::Memory.const_defined?(:Runners) &&
|
|
122
|
+
Legion::Extensions::Memory::Runners.const_defined?(:Traces)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def memory_runner
|
|
126
|
+
@memory_runner ||= Object.new.extend(Legion::Extensions::Memory::Runners::Traces)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def imprint_active_now?
|
|
130
|
+
bootstrap.imprint_active?
|
|
131
|
+
rescue StandardError
|
|
132
|
+
false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def bootstrap
|
|
136
|
+
@bootstrap ||= Helpers::Bootstrap.new
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def coactivate_section_traces(stored, candidates, runner)
|
|
140
|
+
return if stored.size < 2
|
|
141
|
+
|
|
142
|
+
store = runner.send(:default_store)
|
|
143
|
+
|
|
144
|
+
# Group stored trace IDs by their heading_slug domain tag
|
|
145
|
+
groups = {}
|
|
146
|
+
stored.each_with_index do |result, idx|
|
|
147
|
+
candidate = candidates[idx]
|
|
148
|
+
next unless candidate && result
|
|
149
|
+
|
|
150
|
+
slug = candidate[:domain_tags]&.find { |t| t.is_a?(String) && !%w[memory claude_md markdown].include?(t) && !t.include?('/') }
|
|
151
|
+
next unless slug
|
|
152
|
+
|
|
153
|
+
groups[slug] ||= []
|
|
154
|
+
groups[slug] << result[:trace_id]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
coactivations = 0
|
|
158
|
+
groups.each_value do |trace_ids|
|
|
159
|
+
# Limit to first 10 traces per section to avoid O(n^2) explosion
|
|
160
|
+
trace_ids.first(10).combination(2).each do |id_a, id_b|
|
|
161
|
+
store.record_coactivation(id_a, id_b)
|
|
162
|
+
coactivations += 1
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
Legion::Logging.debug "[coldstart:ingest] co-activated #{coactivations} trace pairs across #{groups.size} sections"
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
Legion::Logging.warn "[coldstart:ingest] co-activation failed: #{e.message}"
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/coldstart/version'
|
|
4
|
+
require 'legion/extensions/coldstart/helpers/imprint'
|
|
5
|
+
require 'legion/extensions/coldstart/helpers/bootstrap'
|
|
6
|
+
require 'legion/extensions/coldstart/helpers/claude_parser'
|
|
7
|
+
require 'legion/extensions/coldstart/runners/coldstart'
|
|
8
|
+
require 'legion/extensions/coldstart/runners/ingest'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Coldstart
|
|
13
|
+
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
if defined?(Legion::Data::Local)
|
|
19
|
+
Legion::Data::Local.register_migrations(
|
|
20
|
+
name: :coldstart,
|
|
21
|
+
path: File.join(__dir__, 'coldstart', 'local_migrations')
|
|
22
|
+
)
|
|
23
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# lex-example: Test Extension
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
A test extension for validating the Claude parser.
|
|
6
|
+
|
|
7
|
+
## What is This?
|
|
8
|
+
|
|
9
|
+
An async job processing engine built on RabbitMQ.
|
|
10
|
+
|
|
11
|
+
## Development
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bundle install
|
|
15
|
+
bundle exec rspec
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Use `bundle exec` for all commands
|
|
19
|
+
- Run rubocop before committing
|
|
20
|
+
|
|
21
|
+
## Key Concepts
|
|
22
|
+
|
|
23
|
+
- **Runner**: A function that processes a task
|
|
24
|
+
- **Actor**: An execution mode (subscription, polling, interval)
|
|
25
|
+
- **Extension**: A gem that plugs into the framework
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Test Project Memory
|
|
2
|
+
|
|
3
|
+
## Hard Rules
|
|
4
|
+
- Never delete production data without confirmation
|
|
5
|
+
- Always use snake_case for Ruby methods
|
|
6
|
+
|
|
7
|
+
## Key Architecture Facts
|
|
8
|
+
- Ruby gem ecosystem with auto-discovered extensions
|
|
9
|
+
- RabbitMQ for task distribution via `legion-transport`
|
|
10
|
+
- MySQL via `Sequel` ORM for persistence
|
|
11
|
+
|
|
12
|
+
## CLI Gotchas
|
|
13
|
+
- Thor reserves `run` as a method name
|
|
14
|
+
- `::Process` must be explicit inside `Legion::` namespace
|
|
15
|
+
- `::JSON` must be explicit inside `Legion::` namespace
|
|
16
|
+
|
|
17
|
+
## Identity Auth Pattern
|
|
18
|
+
- Digital Worker = Entra ID **Application** (service principal)
|
|
19
|
+
- Dual-layer: OIDC client credentials + behavioral entropy
|
|
20
|
+
|
|
21
|
+
## Project Structure
|
|
22
|
+
- Workspace: `/tmp/test`
|
|
23
|
+
- 10 repos total
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Stub the framework actor base class since legionio gem is not available in test
|
|
4
|
+
module Legion
|
|
5
|
+
module Extensions
|
|
6
|
+
module Actors
|
|
7
|
+
class Once # rubocop:disable Lint/EmptyClass
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Intercept the require in the actor file so it doesn't fail
|
|
14
|
+
$LOADED_FEATURES << 'legion/extensions/actors/once'
|
|
15
|
+
|
|
16
|
+
require 'legion/extensions/coldstart/actors/imprint'
|
|
17
|
+
|
|
18
|
+
RSpec.describe Legion::Extensions::Coldstart::Actor::Imprint do
|
|
19
|
+
subject(:actor) { described_class.new }
|
|
20
|
+
|
|
21
|
+
describe '#runner_class' do
|
|
22
|
+
it 'returns the Coldstart runner module' do
|
|
23
|
+
expect(actor.runner_class).to eq(Legion::Extensions::Coldstart::Runners::Coldstart)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '#runner_function' do
|
|
28
|
+
it 'returns begin_imprint' do
|
|
29
|
+
expect(actor.runner_function).to eq('begin_imprint')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
describe '#use_runner?' do
|
|
34
|
+
it 'returns false' do
|
|
35
|
+
expect(actor.use_runner?).to be false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe '#check_subtask?' do
|
|
40
|
+
it 'returns false' do
|
|
41
|
+
expect(actor.check_subtask?).to be false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#generate_task?' do
|
|
46
|
+
it 'returns false' do
|
|
47
|
+
expect(actor.generate_task?).to be false
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/coldstart/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Coldstart::Client do
|
|
6
|
+
it 'responds to coldstart runner methods' do
|
|
7
|
+
client = described_class.new
|
|
8
|
+
expect(client).to respond_to(:begin_imprint)
|
|
9
|
+
expect(client).to respond_to(:record_observation)
|
|
10
|
+
expect(client).to respond_to(:coldstart_progress)
|
|
11
|
+
expect(client).to respond_to(:imprint_active?)
|
|
12
|
+
expect(client).to respond_to(:current_multiplier)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Coldstart::Helpers::ClaudeParser do
|
|
6
|
+
let(:fixture_dir) { File.expand_path('../../../../fixtures', __dir__) }
|
|
7
|
+
let(:memory_path) { File.join(fixture_dir, 'sample_memory.md') }
|
|
8
|
+
let(:claude_path) { File.join(fixture_dir, 'sample_claude.md') }
|
|
9
|
+
|
|
10
|
+
describe '.detect_file_type' do
|
|
11
|
+
it 'detects MEMORY.md files' do
|
|
12
|
+
expect(described_class.detect_file_type('/some/path/MEMORY.md')).to eq(:memory)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
it 'detects CLAUDE.md files' do
|
|
16
|
+
expect(described_class.detect_file_type('/some/path/CLAUDE.md')).to eq(:claude_md)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'returns :markdown for other files' do
|
|
20
|
+
expect(described_class.detect_file_type('/some/path/README.md')).to eq(:markdown)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
describe '.classify_section' do
|
|
25
|
+
it 'maps Hard Rules to firmware' do
|
|
26
|
+
expect(described_class.classify_section('Hard Rules')).to eq(:firmware)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'maps architecture sections to semantic' do
|
|
30
|
+
expect(described_class.classify_section('Key Architecture Facts')).to eq(:semantic)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'maps gotchas to procedural' do
|
|
34
|
+
expect(described_class.classify_section('CLI Gotchas')).to eq(:procedural)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'maps identity sections to identity' do
|
|
38
|
+
expect(described_class.classify_section('Identity Auth Pattern')).to eq(:identity)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'defaults to semantic for unknown sections' do
|
|
42
|
+
expect(described_class.classify_section('Random Stuff')).to eq(:semantic)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'maps development sections to procedural' do
|
|
46
|
+
expect(described_class.classify_section('Development')).to eq(:procedural)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'maps API sections to procedural' do
|
|
50
|
+
expect(described_class.classify_section('REST API Routes')).to eq(:procedural)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '.split_sections' do
|
|
55
|
+
it 'splits on ## headers' do
|
|
56
|
+
content = "## First\nline1\n## Second\nline2\n"
|
|
57
|
+
sections = described_class.split_sections(content)
|
|
58
|
+
expect(sections.size).to eq(2)
|
|
59
|
+
expect(sections[0][:heading]).to eq('First')
|
|
60
|
+
expect(sections[1][:heading]).to eq('Second')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'creates slugs from headings' do
|
|
64
|
+
content = "## Key Architecture Facts\nstuff\n"
|
|
65
|
+
sections = described_class.split_sections(content)
|
|
66
|
+
expect(sections[0][:heading_slug]).to eq('key-architecture-facts')
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'skips empty sections' do
|
|
70
|
+
content = "## Empty\n\n## HasContent\nreal stuff\n"
|
|
71
|
+
sections = described_class.split_sections(content)
|
|
72
|
+
expect(sections.size).to eq(1)
|
|
73
|
+
expect(sections[0][:heading]).to eq('HasContent')
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
describe '.extract_items' do
|
|
78
|
+
it 'extracts bullet points as individual items' do
|
|
79
|
+
body = "- first item\n- second item\n- third item\n"
|
|
80
|
+
items = described_class.extract_items(body)
|
|
81
|
+
expect(items).to eq(['first item', 'second item', 'third item'])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'merges continuation lines into bullets' do
|
|
85
|
+
body = "- first item\n continued here\n- second item\n"
|
|
86
|
+
items = described_class.extract_items(body)
|
|
87
|
+
expect(items.size).to eq(2)
|
|
88
|
+
expect(items[0]).to include('continued here')
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it 'keeps code blocks as single items' do
|
|
92
|
+
body = "- before\n```ruby\ncode here\nmore code\n```\n- after\n"
|
|
93
|
+
items = described_class.extract_items(body)
|
|
94
|
+
expect(items.any? { |i| i.include?('code here') }).to be true
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'handles paragraphs' do
|
|
98
|
+
body = "This is a paragraph\nwith continuation.\n\nAnother paragraph.\n"
|
|
99
|
+
items = described_class.extract_items(body)
|
|
100
|
+
expect(items.size).to eq(2)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
describe '.extract_inline_tags' do
|
|
105
|
+
it 'extracts backtick terms' do
|
|
106
|
+
tags = described_class.extract_inline_tags('Uses `legion-transport` for messaging')
|
|
107
|
+
expect(tags).to include('legion-transport')
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'extracts bold terms' do
|
|
111
|
+
tags = described_class.extract_inline_tags('The **Runner** processes tasks')
|
|
112
|
+
expect(tags).to include('runner')
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'rejects very short or very long tags' do
|
|
116
|
+
tags = described_class.extract_inline_tags("`x` and `a_very_long_tag_#{'x' * 60}`")
|
|
117
|
+
expect(tags).to be_empty
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
describe '.parse_file' do
|
|
122
|
+
it 'parses a MEMORY.md file into traces' do
|
|
123
|
+
traces = described_class.parse_file(memory_path)
|
|
124
|
+
expect(traces).to be_an(Array)
|
|
125
|
+
expect(traces).not_to be_empty
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'assigns firmware type to Hard Rules' do
|
|
129
|
+
traces = described_class.parse_file(memory_path)
|
|
130
|
+
firmware = traces.select { |t| t[:trace_type] == :firmware }
|
|
131
|
+
expect(firmware).not_to be_empty
|
|
132
|
+
expect(firmware.first[:content_payload]).to include('production data')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
it 'assigns procedural type to gotchas' do
|
|
136
|
+
traces = described_class.parse_file(memory_path)
|
|
137
|
+
procedural = traces.select { |t| t[:trace_type] == :procedural }
|
|
138
|
+
expect(procedural).not_to be_empty
|
|
139
|
+
expect(procedural.any? { |t| t[:content_payload].include?('Thor') }).to be true
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'assigns identity type to identity sections' do
|
|
143
|
+
traces = described_class.parse_file(memory_path)
|
|
144
|
+
identity = traces.select { |t| t[:trace_type] == :identity }
|
|
145
|
+
expect(identity).not_to be_empty
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'sets origin to :firmware for MEMORY.md files' do
|
|
149
|
+
traces = described_class.parse_file(memory_path)
|
|
150
|
+
# All traces from MEMORY.md get :firmware origin (imprint source)
|
|
151
|
+
expect(traces.all? { |t| t[:origin] == :firmware }).to be true
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'sets origin to :direct_experience for CLAUDE.md files' do
|
|
155
|
+
traces = described_class.parse_file(claude_path)
|
|
156
|
+
expect(traces.all? { |t| t[:origin] == :direct_experience }).to be true
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
it 'sets confidence 1.0 for firmware traces' do
|
|
160
|
+
traces = described_class.parse_file(memory_path)
|
|
161
|
+
firmware = traces.select { |t| t[:trace_type] == :firmware }
|
|
162
|
+
expect(firmware.all? { |t| t[:confidence] == 1.0 }).to be true # rubocop:disable Lint/FloatComparison
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'includes source_file in each trace' do
|
|
166
|
+
traces = described_class.parse_file(memory_path)
|
|
167
|
+
expect(traces.all? { |t| t[:source_file] == memory_path }).to be true
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'includes domain tags' do
|
|
171
|
+
traces = described_class.parse_file(memory_path)
|
|
172
|
+
expect(traces.all? { |t| t[:domain_tags].is_a?(Array) && !t[:domain_tags].empty? }).to be true
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'parses a CLAUDE.md file into traces' do
|
|
176
|
+
traces = described_class.parse_file(claude_path)
|
|
177
|
+
expect(traces).not_to be_empty
|
|
178
|
+
semantic = traces.select { |t| t[:trace_type] == :semantic }
|
|
179
|
+
expect(semantic).not_to be_empty
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
describe '.parse_directory' do
|
|
184
|
+
it 'finds and parses all matching files' do
|
|
185
|
+
traces = described_class.parse_directory(fixture_dir, pattern: '*.md')
|
|
186
|
+
expect(traces).not_to be_empty
|
|
187
|
+
source_files = traces.map { |t| t[:source_file] }.uniq
|
|
188
|
+
expect(source_files.size).to be >= 2
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
describe '.skip_path?' do
|
|
193
|
+
it 'skips _deprecated paths' do
|
|
194
|
+
expect(described_class.skip_path?('/foo/_deprecated/CLAUDE.md')).to be true
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'skips references paths' do
|
|
198
|
+
expect(described_class.skip_path?('/foo/references/CLAUDE.md')).to be true
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'allows normal paths' do
|
|
202
|
+
expect(described_class.skip_path?('/foo/lex-memory/CLAUDE.md')).to be false
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/coldstart/client'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Coldstart::Runners::Coldstart do
|
|
6
|
+
let(:client) { Legion::Extensions::Coldstart::Client.new }
|
|
7
|
+
|
|
8
|
+
describe '#begin_imprint' do
|
|
9
|
+
it 'starts the imprint window' do
|
|
10
|
+
result = client.begin_imprint
|
|
11
|
+
expect(result[:started]).to be true
|
|
12
|
+
expect(result[:multiplier]).to eq(3.0)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
describe '#record_observation' do
|
|
17
|
+
it 'increments observation count' do
|
|
18
|
+
client.begin_imprint
|
|
19
|
+
result = client.record_observation
|
|
20
|
+
expect(result[:observation_count]).to eq(1)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'transitions calibration state at baseline threshold' do
|
|
24
|
+
client.begin_imprint
|
|
25
|
+
50.times { client.record_observation }
|
|
26
|
+
result = client.record_observation
|
|
27
|
+
expect(result[:calibration_state]).to eq(:baseline_established)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#coldstart_progress' do
|
|
32
|
+
it 'reports progress' do
|
|
33
|
+
client.begin_imprint
|
|
34
|
+
result = client.coldstart_progress
|
|
35
|
+
expect(result[:firmware_loaded]).to be true
|
|
36
|
+
expect(result[:imprint_active]).to be true
|
|
37
|
+
expect(result[:current_layer]).to eq(:imprint_window)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#imprint_active?' do
|
|
42
|
+
it 'returns false before begin' do
|
|
43
|
+
result = client.imprint_active?
|
|
44
|
+
expect(result[:active]).to be false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'returns true after begin' do
|
|
48
|
+
client.begin_imprint
|
|
49
|
+
result = client.imprint_active?
|
|
50
|
+
expect(result[:active]).to be true
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe '#current_multiplier' do
|
|
55
|
+
it 'returns 1.0 when not imprinting' do
|
|
56
|
+
result = client.current_multiplier
|
|
57
|
+
expect(result[:multiplier]).to eq(1.0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'returns 3.0 during imprint' do
|
|
61
|
+
client.begin_imprint
|
|
62
|
+
result = client.current_multiplier
|
|
63
|
+
expect(result[:multiplier]).to eq(3.0)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Legion::Extensions::Coldstart::Runners::Ingest do
|
|
6
|
+
let(:runner) { Object.new.extend(described_class) }
|
|
7
|
+
let(:fixture_dir) { File.expand_path('../../../../fixtures', __dir__) }
|
|
8
|
+
let(:memory_path) { File.join(fixture_dir, 'sample_memory.md') }
|
|
9
|
+
let(:claude_path) { File.join(fixture_dir, 'sample_claude.md') }
|
|
10
|
+
|
|
11
|
+
describe '#ingest_file' do
|
|
12
|
+
it 'parses a MEMORY.md file' do
|
|
13
|
+
result = runner.ingest_file(file_path: memory_path, store_traces: false)
|
|
14
|
+
expect(result[:file]).to eq(memory_path)
|
|
15
|
+
expect(result[:file_type]).to eq(:memory)
|
|
16
|
+
expect(result[:traces_parsed]).to be > 0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
it 'parses a CLAUDE.md file' do
|
|
20
|
+
result = runner.ingest_file(file_path: claude_path, store_traces: false)
|
|
21
|
+
expect(result[:file]).to eq(claude_path)
|
|
22
|
+
expect(result[:file_type]).to eq(:claude_md)
|
|
23
|
+
expect(result[:traces_parsed]).to be > 0
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'returns error for missing file' do
|
|
27
|
+
result = runner.ingest_file(file_path: '/nonexistent/file.md')
|
|
28
|
+
expect(result[:error]).to eq('file not found')
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'returns traces when store_traces is false' do
|
|
32
|
+
result = runner.ingest_file(file_path: memory_path, store_traces: false)
|
|
33
|
+
expect(result[:traces]).to be_an(Array)
|
|
34
|
+
expect(result[:traces]).not_to be_empty
|
|
35
|
+
expect(result[:traces].first).to have_key(:trace_type)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'includes firmware traces from Hard Rules' do
|
|
39
|
+
result = runner.ingest_file(file_path: memory_path, store_traces: false)
|
|
40
|
+
firmware = result[:traces].select { |t| t[:trace_type] == :firmware }
|
|
41
|
+
expect(firmware.size).to eq(2)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
describe '#ingest_directory' do
|
|
46
|
+
it 'finds and parses files in a directory' do
|
|
47
|
+
result = runner.ingest_directory(dir_path: fixture_dir, pattern: '*.md', store_traces: false)
|
|
48
|
+
expect(result[:files_found]).to be >= 2
|
|
49
|
+
expect(result[:total_parsed]).to be > 0
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns error for missing directory' do
|
|
53
|
+
result = runner.ingest_directory(dir_path: '/nonexistent/dir')
|
|
54
|
+
expect(result[:error]).to eq('directory not found')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'lists processed files' do
|
|
58
|
+
result = runner.ingest_directory(dir_path: fixture_dir, pattern: '*.md', store_traces: false)
|
|
59
|
+
expect(result[:files]).to be_an(Array)
|
|
60
|
+
expect(result[:files].any? { |f| f.include?('sample_memory') }).to be true
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
describe '#preview_ingest' do
|
|
65
|
+
it 'returns traces without storing' do
|
|
66
|
+
result = runner.preview_ingest(file_path: memory_path)
|
|
67
|
+
expect(result[:traces]).to be_an(Array)
|
|
68
|
+
expect(result[:traces]).not_to be_empty
|
|
69
|
+
expect(result[:file_type]).to eq(:memory)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'returns error for missing file' do
|
|
73
|
+
result = runner.preview_ingest(file_path: '/nonexistent/file.md')
|
|
74
|
+
expect(result[:error]).to eq('file not found')
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|