lex-cognitive-fermentation 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ea50c03dfbb3796b24114e8395d00e9a70bbc9efd2801912528f909f931ea39
4
+ data.tar.gz: ee361053997fc4d64fdcd522abb6ff406a621d190b8c7002c8b7ac2cdaf11fc5
5
+ SHA512:
6
+ metadata.gz: e35f786a95a757afb400bc8827ee11fe685f37514a657afe4c05132cb18d6a987bdba670690f5afff19896a6d0b0d5b3d02f87f9aa2dc9fd0ed704eeee9abf1a
7
+ data.tar.gz: 55444692dc3361b8c1ef6272d59a94767bc69ad603215d0fd1feaf2d6622ad2d7e85ee5894ec01c7aeb6e895ae71744fc4649ccb51900d281187535d4066159f
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .rspec_status
2
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.4
4
+
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ Naming/PredicateMethod:
9
+ Enabled: false
10
+
11
+ Naming/PredicatePrefix:
12
+ Enabled: false
13
+
14
+ Metrics/ClassLength:
15
+ Max: 150
16
+
17
+ Metrics/MethodLength:
18
+ Max: 25
19
+
20
+ Metrics/AbcSize:
21
+ Max: 25
22
+
23
+ Metrics/ParameterLists:
24
+ Max: 8
25
+ MaxOptionalParameters: 8
26
+
27
+ Layout/HashAlignment:
28
+ EnforcedColonStyle: table
29
+ EnforcedHashRocketStyle: table
30
+
31
+ Metrics/BlockLength:
32
+ Exclude:
33
+ - spec/**/*
34
+
35
+ Style/OneClassPerFile:
36
+ Exclude:
37
+ - spec/spec_helper.rb
data/CLAUDE.md ADDED
@@ -0,0 +1,117 @@
1
+ # lex-cognitive-fermentation
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Gem**: `lex-cognitive-fermentation`
6
+
7
+ ## Purpose
8
+
9
+ Models slow, non-linear cognitive maturation using a fermentation metaphor. Raw, unresolved, or partial cognitive material (substrates) ferments over time, developing potency and maturity through natural processing and catalysis. When substrates reach peak stage, they are ready for use or harvest. Over-fermented substrates decay. Domain-grouped batches track collective fermentation progress.
10
+
11
+ ## Gem Info
12
+
13
+ | Field | Value |
14
+ |---|---|
15
+ | Gem name | `lex-cognitive-fermentation` |
16
+ | Version | `0.1.0` |
17
+ | Namespace | `Legion::Extensions::CognitiveFermentation` |
18
+ | Ruby | `>= 3.4` |
19
+ | License | MIT |
20
+ | GitHub | https://github.com/LegionIO/lex-cognitive-fermentation |
21
+
22
+ ## File Structure
23
+
24
+ ```
25
+ lib/legion/extensions/cognitive_fermentation/
26
+ cognitive_fermentation.rb # Top-level require
27
+ version.rb # VERSION = '0.1.0'
28
+ client.rb # Client class
29
+ helpers/
30
+ constants.rb # Types, stages, rates, label hashes
31
+ substrate.rb # Substrate value object
32
+ batch.rb # Batch (domain group) value object
33
+ fermentation_engine.rb # Engine: substrates + batches
34
+ runners/
35
+ cognitive_fermentation.rb # Runner module
36
+ ```
37
+
38
+ ## Key Constants
39
+
40
+ | Constant | Value | Meaning |
41
+ |---|---|---|
42
+ | `MAX_SUBSTRATES` | 500 | Hard cap; evicts spoiled then lowest-potency |
43
+ | `MAX_BATCHES` | 50 | Batch cap |
44
+ | `MATURATION_RATE` | 0.05 | Default fermentation step size |
45
+ | `CATALYSIS_BOOST` | 0.12 | Potency boost from catalyst application |
46
+ | `SPOILAGE_THRESHOLD` | 0.1 | Potency below this = spoiled |
47
+ | `RIPE_THRESHOLD` | 0.7 | Potency above this = ripe |
48
+ | `PEAK_THRESHOLD` | 0.9 | Potency above this = peak |
49
+ | `OVER_FERMENTED_DECAY` | 0.03 | Post-peak potency loss rate |
50
+ | `SUBSTRATE_TYPES` | array | `raw_idea`, `unresolved_problem`, `partial_pattern`, etc. |
51
+ | `FERMENTATION_STAGES` | array | `inoculation` through `over_fermented` |
52
+ | `CATALYST_TYPES` | array | `analogy`, `contrast`, `emotional_charge`, `dream_residue`, etc. |
53
+
54
+ ## Helpers
55
+
56
+ ### `Substrate`
57
+
58
+ Tracks the fermentation state of a single cognitive material unit.
59
+
60
+ - `initialize(substrate_type:, domain:, content:, potency: nil, volatility: nil)`
61
+ - `ferment!(rate)` — advances maturity and potency by rate; transitions stage
62
+ - `catalyze!(catalyst_type)` — boosts potency by `CATALYSIS_BOOST` if valid catalyst
63
+ - `ripe?`, `peak?`, `spoiled?`, `raw?` — state predicates
64
+ - `to_h` — includes stage, potency, maturity, volatility, labels
65
+
66
+ ### `Batch`
67
+
68
+ Groups substrates by domain.
69
+
70
+ - `add_substrate(substrate)`
71
+ - `ferment_all!(rate)` — delegates to each substrate
72
+
73
+ ### `FermentationEngine`
74
+
75
+ Manages substrates and batches.
76
+
77
+ - `create_substrate(substrate_type:, domain:, content:, potency: nil, volatility: nil)` — auto-assigns to batch; prunes when at cap
78
+ - `ferment(substrate_id:, rate: MATURATION_RATE)` — single substrate advance
79
+ - `catalyze(substrate_id:, catalyst_type:)` — catalyzes single substrate
80
+ - `ferment_all!(rate: MATURATION_RATE)` — advances all batches
81
+ - `substrates_by_domain(domain:)`, `substrates_by_type(type:)`, `substrates_by_stage(stage:)`
82
+ - `ripe_substrates`, `peak_substrates`, `spoiled_substrates`, `raw_substrates`
83
+ - `most_potent(limit: 5)`, `most_mature(limit: 5)`
84
+ - `overall_potency`, `overall_maturity`, `overall_volatility`, `yield_rate`, `stage_distribution`
85
+ - `fermentation_report` — full stats with labels and top substrates
86
+
87
+ ## Runners
88
+
89
+ **Module**: `Legion::Extensions::CognitiveFermentation::Runners::CognitiveFermentation`
90
+
91
+ | Method | Key Args | Returns |
92
+ |---|---|---|
93
+ | `create_substrate` | `substrate_type:`, `domain:`, `content:` | `{ success:, substrate: }` |
94
+ | `ferment` | `substrate_id:`, `rate: nil` | `{ success:, substrate: }` |
95
+ | `catalyze` | `substrate_id:`, `catalyst_type:` | `{ success:, substrate: }` |
96
+ | `ferment_all` | `rate: nil` | `{ success: }` |
97
+ | `list_ripe` | — | `{ count:, substrates: }` |
98
+ | `fermentation_status` | — | Full fermentation report |
99
+
100
+ Private: `@default_engine` — memoized `FermentationEngine`. Runner uses `engine || @default_engine` pattern.
101
+
102
+ ## Integration Points
103
+
104
+ - **`lex-dream`**: Dream cycle is a natural catalyst source. The `dream_residue` catalyst type in the constants is specifically for materials that surface during the dream cycle and catalyze substrate development.
105
+ - **`lex-memory`**: Ripe substrates could be promoted to memory traces after maturation.
106
+ - **`lex-genesis`**: Fermentation produces potent substrates ready for concept birth.
107
+
108
+ ## Development Notes
109
+
110
+ - Pruning order on cap: first removes `spoiled?` substrates, then lowest-potency if still over cap.
111
+ - `ferment_all!` works through the batch grouping, not directly over substrates. Batch-level iteration.
112
+ - `CATALYST_TYPES` includes `sleep` and `dream_residue` — these are valid symbols recognized by `catalyze!`.
113
+ - In-memory only. No persistence.
114
+
115
+ ---
116
+
117
+ **Maintained By**: Matthew Iverson (@Esity)
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75'
9
+ gem 'rubocop-rspec'
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lex-cognitive-fermentation (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ addressable (2.8.9)
10
+ public_suffix (>= 2.0.2, < 8.0)
11
+ ast (2.4.3)
12
+ bigdecimal (4.0.1)
13
+ diff-lcs (1.6.2)
14
+ json (2.19.1)
15
+ json-schema (6.2.0)
16
+ addressable (~> 2.8)
17
+ bigdecimal (>= 3.1, < 5)
18
+ language_server-protocol (3.17.0.5)
19
+ lint_roller (1.1.0)
20
+ mcp (0.8.0)
21
+ json-schema (>= 4.1)
22
+ parallel (1.27.0)
23
+ parser (3.3.10.2)
24
+ ast (~> 2.4.1)
25
+ racc
26
+ prism (1.9.0)
27
+ public_suffix (7.0.5)
28
+ racc (1.8.1)
29
+ rainbow (3.1.1)
30
+ regexp_parser (2.11.3)
31
+ rspec (3.13.2)
32
+ rspec-core (~> 3.13.0)
33
+ rspec-expectations (~> 3.13.0)
34
+ rspec-mocks (~> 3.13.0)
35
+ rspec-core (3.13.6)
36
+ rspec-support (~> 3.13.0)
37
+ rspec-expectations (3.13.5)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.13.0)
40
+ rspec-mocks (3.13.8)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.13.0)
43
+ rspec-support (3.13.7)
44
+ rubocop (1.85.1)
45
+ json (~> 2.3)
46
+ language_server-protocol (~> 3.17.0.2)
47
+ lint_roller (~> 1.1.0)
48
+ mcp (~> 0.6)
49
+ parallel (~> 1.10)
50
+ parser (>= 3.3.0.2)
51
+ rainbow (>= 2.2.2, < 4.0)
52
+ regexp_parser (>= 2.9.3, < 3.0)
53
+ rubocop-ast (>= 1.49.0, < 2.0)
54
+ ruby-progressbar (~> 1.7)
55
+ unicode-display_width (>= 2.4.0, < 4.0)
56
+ rubocop-ast (1.49.1)
57
+ parser (>= 3.3.7.2)
58
+ prism (~> 1.7)
59
+ rubocop-rspec (3.9.0)
60
+ lint_roller (~> 1.1)
61
+ rubocop (~> 1.81)
62
+ ruby-progressbar (1.13.0)
63
+ unicode-display_width (3.2.0)
64
+ unicode-emoji (~> 4.1)
65
+ unicode-emoji (4.2.0)
66
+
67
+ PLATFORMS
68
+ arm64-darwin-25
69
+ ruby
70
+
71
+ DEPENDENCIES
72
+ lex-cognitive-fermentation!
73
+ rspec (~> 3.13)
74
+ rubocop (~> 1.75)
75
+ rubocop-rspec
76
+
77
+ BUNDLED WITH
78
+ 2.6.9
data/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # lex-cognitive-fermentation
2
+
3
+ Slow non-linear cognitive maturation model for brain-modeled agentic AI in the LegionIO ecosystem.
4
+
5
+ ## What It Does
6
+
7
+ Not all cognitive processing is immediate. Raw ideas, unresolved problems, partial patterns, and lingering questions sometimes need time to develop before they yield useful output. This extension models that incubation process using a fermentation metaphor: substrates move through stages from `inoculation` to `peak` maturity, developing potency along the way. Catalysts (analogies, emotional charge, dream residue) accelerate the process. Over-fermented substrates decay.
8
+
9
+ Substrates are grouped into domain-based batches for collective tracking. The engine surfaces which substrates are ripe or at peak, and provides an overall yield rate.
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require 'legion/extensions/cognitive_fermentation'
15
+
16
+ client = Legion::Extensions::CognitiveFermentation::Client.new
17
+
18
+ # Add a substrate
19
+ result = client.create_substrate(
20
+ substrate_type: :unresolved_problem,
21
+ domain: :analytical,
22
+ content: 'Why do requests cluster in the morning?'
23
+ )
24
+ id = result[:substrate][:id]
25
+
26
+ # Advance fermentation naturally
27
+ client.ferment(substrate_id: id)
28
+
29
+ # Apply a catalyst to boost potency
30
+ client.catalyze(substrate_id: id, catalyst_type: :analogy)
31
+
32
+ # Run a full fermentation cycle on all substrates
33
+ client.ferment_all
34
+
35
+ # See what's ready
36
+ client.list_ripe
37
+ # => { count: 0, substrates: [] }
38
+
39
+ # Full status report
40
+ client.fermentation_status
41
+ ```
42
+
43
+ ## Development
44
+
45
+ ```bash
46
+ bundle install
47
+ bundle exec rspec
48
+ bundle exec rubocop
49
+ ```
50
+
51
+ ## License
52
+
53
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_fermentation/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-fermentation'
7
+ spec.version = Legion::Extensions::CognitiveFermentation::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+ spec.summary = 'Slow unconscious cognitive processing for LegionIO agents'
11
+ spec.description = 'Models cognitive fermentation — the slow transformation of raw ideas ' \
12
+ 'into refined insights through unconscious incubation and catalysis'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-fermentation'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata = {
18
+ 'homepage_uri' => spec.homepage,
19
+ 'source_code_uri' => spec.homepage,
20
+ 'documentation_uri' => "#{spec.homepage}/blob/master/README.md",
21
+ 'changelog_uri' => "#{spec.homepage}/blob/master/CHANGELOG.md",
22
+ 'bug_tracker_uri' => "#{spec.homepage}/issues",
23
+ 'rubygems_mfa_required' => 'true'
24
+ }
25
+
26
+ spec.files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFermentation
6
+ class Client
7
+ include Runners::CognitiveFermentation
8
+
9
+ def initialize
10
+ @default_engine = Helpers::FermentationEngine.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFermentation
6
+ module Helpers
7
+ class Batch
8
+ include Constants
9
+
10
+ attr_reader :id, :domain, :substrates, :created_at
11
+
12
+ def initialize(domain:)
13
+ @id = SecureRandom.uuid
14
+ @domain = domain.to_sym
15
+ @substrates = []
16
+ @created_at = Time.now.utc
17
+ end
18
+
19
+ def add_substrate(substrate)
20
+ @substrates << substrate
21
+ substrate
22
+ end
23
+
24
+ def ferment_all!(rate = MATURATION_RATE)
25
+ @substrates.each { |s| s.ferment!(rate) }
26
+ end
27
+
28
+ def average_potency
29
+ return 0.0 if @substrates.empty?
30
+
31
+ (@substrates.sum(&:potency) / @substrates.size).round(10)
32
+ end
33
+
34
+ def average_maturity
35
+ return 0.0 if @substrates.empty?
36
+
37
+ (@substrates.sum(&:maturity) / @substrates.size).round(10)
38
+ end
39
+
40
+ def ripe_count = @substrates.count(&:ripe?)
41
+ def peak_count = @substrates.count(&:peak?)
42
+ def spoiled_count = @substrates.count(&:spoiled?)
43
+ def raw_count = @substrates.count(&:raw?)
44
+
45
+ def yield_rate
46
+ return 0.0 if @substrates.empty?
47
+
48
+ (ripe_count.to_f / @substrates.size).round(10)
49
+ end
50
+
51
+ def ready_to_harvest? = yield_rate >= 0.5
52
+
53
+ def to_h
54
+ {
55
+ id: @id,
56
+ domain: @domain,
57
+ substrate_count: @substrates.size,
58
+ average_potency: average_potency,
59
+ average_maturity: average_maturity,
60
+ ripe_count: ripe_count,
61
+ peak_count: peak_count,
62
+ spoiled_count: spoiled_count,
63
+ yield_rate: yield_rate,
64
+ ready_to_harvest: ready_to_harvest?
65
+ }
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFermentation
6
+ module Helpers
7
+ module Constants
8
+ MAX_SUBSTRATES = 500
9
+ MAX_BATCHES = 50
10
+
11
+ DEFAULT_POTENCY = 0.3
12
+ MATURATION_RATE = 0.05
13
+ VOLATILITY_DECAY = 0.02
14
+ CATALYSIS_BOOST = 0.12
15
+ SPOILAGE_THRESHOLD = 0.1
16
+ RIPE_THRESHOLD = 0.7
17
+ PEAK_THRESHOLD = 0.9
18
+ OVER_FERMENTED_DECAY = 0.03
19
+
20
+ SUBSTRATE_TYPES = %i[
21
+ raw_idea unresolved_problem partial_pattern
22
+ dormant_association vague_intuition half_formed_belief
23
+ contradictory_evidence lingering_question
24
+ ].freeze
25
+
26
+ FERMENTATION_STAGES = %i[
27
+ inoculation primary_fermentation secondary_fermentation
28
+ conditioning maturation aging peak over_fermented
29
+ ].freeze
30
+
31
+ CATALYST_TYPES = %i[
32
+ analogy contrast juxtaposition
33
+ emotional_charge sleep dream_residue
34
+ environmental_stimulus social_interaction
35
+ ].freeze
36
+
37
+ DOMAINS = %i[
38
+ cognitive emotional procedural semantic
39
+ episodic social creative analytical
40
+ ].freeze
41
+
42
+ POTENCY_LABELS = {
43
+ (0.8..) => :transcendent,
44
+ (0.6...0.8) => :potent,
45
+ (0.4...0.6) => :developing,
46
+ (0.2...0.4) => :mild,
47
+ (..0.2) => :inert
48
+ }.freeze
49
+
50
+ MATURITY_LABELS = {
51
+ (0.8..) => :peak,
52
+ (0.6...0.8) => :mature,
53
+ (0.4...0.6) => :developing,
54
+ (0.2...0.4) => :young,
55
+ (..0.2) => :raw
56
+ }.freeze
57
+
58
+ VOLATILITY_LABELS = {
59
+ (0.8..) => :explosive,
60
+ (0.6...0.8) => :volatile,
61
+ (0.4...0.6) => :active,
62
+ (0.2...0.4) => :stable,
63
+ (..0.2) => :dormant
64
+ }.freeze
65
+
66
+ def self.label_for(labels, value)
67
+ labels.each { |range, label| return label if range.cover?(value) }
68
+ :unknown
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveFermentation
6
+ module Helpers
7
+ class FermentationEngine
8
+ include Constants
9
+
10
+ def initialize
11
+ @substrates = {}
12
+ @batches = {}
13
+ end
14
+
15
+ def create_substrate(substrate_type:, domain:, content: '', potency: nil, volatility: nil)
16
+ sub = Substrate.new(substrate_type: substrate_type, domain: domain, content: content,
17
+ potency: potency, volatility: volatility)
18
+ @substrates[sub.id] = sub
19
+ batch = find_or_create_batch(domain: domain)
20
+ batch.add_substrate(sub)
21
+ prune_substrates
22
+ sub
23
+ end
24
+
25
+ def ferment(substrate_id:, rate: MATURATION_RATE)
26
+ sub = @substrates[substrate_id]
27
+ return nil unless sub
28
+
29
+ sub.ferment!(rate)
30
+ sub
31
+ end
32
+
33
+ def catalyze(substrate_id:, catalyst_type:)
34
+ sub = @substrates[substrate_id]
35
+ return nil unless sub
36
+
37
+ sub.catalyze!(catalyst_type)
38
+ sub
39
+ end
40
+
41
+ def ferment_all!(rate: MATURATION_RATE)
42
+ @batches.each_value { |b| b.ferment_all!(rate) }
43
+ end
44
+
45
+ def substrates_by_domain(domain:) = @substrates.values.select { |s| s.domain == domain.to_sym }
46
+ def substrates_by_type(type:) = @substrates.values.select { |s| s.substrate_type == type.to_sym }
47
+ def substrates_by_stage(stage:) = @substrates.values.select { |s| s.stage == stage.to_sym }
48
+ def ripe_substrates = @substrates.values.select(&:ripe?)
49
+ def peak_substrates = @substrates.values.select(&:peak?)
50
+ def spoiled_substrates = @substrates.values.select(&:spoiled?)
51
+ def raw_substrates = @substrates.values.select(&:raw?)
52
+
53
+ def most_potent(limit: 5)
54
+ @substrates.values.sort_by { |s| -s.potency }.first(limit)
55
+ end
56
+
57
+ def most_mature(limit: 5)
58
+ @substrates.values.sort_by { |s| -s.maturity }.first(limit)
59
+ end
60
+
61
+ def overall_potency
62
+ return 0.0 if @substrates.empty?
63
+
64
+ (@substrates.values.sum(&:potency) / @substrates.size).round(10)
65
+ end
66
+
67
+ def overall_maturity
68
+ return 0.0 if @substrates.empty?
69
+
70
+ (@substrates.values.sum(&:maturity) / @substrates.size).round(10)
71
+ end
72
+
73
+ def overall_volatility
74
+ return 0.0 if @substrates.empty?
75
+
76
+ (@substrates.values.sum(&:volatility) / @substrates.size).round(10)
77
+ end
78
+
79
+ def yield_rate
80
+ return 0.0 if @substrates.empty?
81
+
82
+ (ripe_substrates.size.to_f / @substrates.size).round(10)
83
+ end
84
+
85
+ def stage_distribution
86
+ dist = Hash.new(0)
87
+ @substrates.each_value { |s| dist[s.stage] += 1 }
88
+ dist
89
+ end
90
+
91
+ def fermentation_report
92
+ {
93
+ total_substrates: @substrates.size,
94
+ total_batches: @batches.size,
95
+ overall_potency: overall_potency,
96
+ potency_label: Constants.label_for(POTENCY_LABELS, overall_potency),
97
+ overall_maturity: overall_maturity,
98
+ maturity_label: Constants.label_for(MATURITY_LABELS, overall_maturity),
99
+ overall_volatility: overall_volatility,
100
+ volatility_label: Constants.label_for(VOLATILITY_LABELS, overall_volatility),
101
+ yield_rate: yield_rate,
102
+ ripe_count: ripe_substrates.size,
103
+ peak_count: peak_substrates.size,
104
+ spoiled_count: spoiled_substrates.size,
105
+ stage_distribution: stage_distribution,
106
+ batches: @batches.values.map(&:to_h),
107
+ most_potent: most_potent(limit: 3).map(&:to_h)
108
+ }
109
+ end
110
+
111
+ def to_h
112
+ {
113
+ total_substrates: @substrates.size,
114
+ total_batches: @batches.size,
115
+ potency: overall_potency,
116
+ maturity: overall_maturity,
117
+ volatility: overall_volatility
118
+ }
119
+ end
120
+
121
+ private
122
+
123
+ def find_or_create_batch(domain:)
124
+ key = domain.to_sym
125
+ @batches[key] ||= Batch.new(domain: key)
126
+ end
127
+
128
+ def prune_substrates
129
+ return if @substrates.size <= MAX_SUBSTRATES
130
+
131
+ spoiled = @substrates.values.select(&:spoiled?)
132
+ spoiled.each { |s| @substrates.delete(s.id) }
133
+ return if @substrates.size <= MAX_SUBSTRATES
134
+
135
+ sorted = @substrates.values.sort_by(&:potency)
136
+ to_remove = sorted.first(@substrates.size - MAX_SUBSTRATES)
137
+ to_remove.each { |s| @substrates.delete(s.id) }
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end