lex-cognitive-zeitgeist 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: 226b7e1ceb0957c06b2834e644e4a7b96eaa2cfffdf010f8db4fd9bf78426e5e
4
+ data.tar.gz: e422848c93bf5e2c4b9a0d878503161652640574e852a8b4f4989e03d1ea6118
5
+ SHA512:
6
+ metadata.gz: 1f5188ea227b211c1ada23e059e65795b068b3e6bc7bec7a8eaee2c0b5eca369fdf8bbf5cf5fd3932d4ef6e47a633edf73c411d3235cfef432b964e213708094
7
+ data.tar.gz: 64b0a0699ad11414fb1a07fd1a1397969148a4d6daf677783a25dd3665934dd0a96dad9b095b092f93d9e3e92891b29172226e9fde7f07cd7a1626859f0d206a
@@ -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 @@
1
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,44 @@
1
+ AllCops:
2
+ TargetRubyVersion: 3.4
3
+ NewCops: enable
4
+ SuggestExtensions: false
5
+
6
+ Layout/LineLength:
7
+ Max: 160
8
+
9
+ Layout/SpaceAroundEqualsInParameterDefault:
10
+ EnforcedStyle: space
11
+
12
+ Layout/HashAlignment:
13
+ EnforcedHashRocketStyle: table
14
+ EnforcedColonStyle: table
15
+
16
+ Metrics/ParameterLists:
17
+ Max: 8
18
+ MaxOptionalParameters: 8
19
+
20
+ Metrics/MethodLength:
21
+ Max: 25
22
+
23
+ Metrics/ClassLength:
24
+ Max: 150
25
+
26
+ Metrics/AbcSize:
27
+ Max: 25
28
+
29
+ Metrics/BlockLength:
30
+ Exclude:
31
+ - 'spec/**/*'
32
+
33
+ Style/Documentation:
34
+ Enabled: false
35
+
36
+ Style/OneClassPerFile:
37
+ Exclude:
38
+ - 'spec/spec_helper.rb'
39
+
40
+ Naming/PredicateMethod:
41
+ Enabled: false
42
+
43
+ Naming/PredicatePrefix:
44
+ Enabled: false
data/CLAUDE.md ADDED
@@ -0,0 +1,124 @@
1
+ # lex-cognitive-zeitgeist
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Gem**: `lex-cognitive-zeitgeist`
6
+ - **Version**: 0.1.0
7
+ - **Namespace**: `Legion::Extensions::CognitiveZeitgeist`
8
+
9
+ ## Purpose
10
+
11
+ Captures the aggregate "spirit of the moment" across cognitive subsystems by ingesting signals from multiple sources and computing collective mood, convergence, and momentum. Named after the German concept of the "spirit of the age" — the pervasive cognitive atmosphere emerging from all active subsystem signals rather than any individual source. Surfaces dominant themes, rising/falling domain trends, and alerts when subsystems diverge.
12
+
13
+ ## Gem Info
14
+
15
+ - **Gemspec**: `lex-cognitive-zeitgeist.gemspec`
16
+ - **Require**: `lex-cognitive-zeitgeist`
17
+ - **Ruby**: >= 3.4
18
+ - **License**: MIT
19
+ - **Homepage**: https://github.com/LegionIO/lex-cognitive-zeitgeist
20
+
21
+ ## File Structure
22
+
23
+ ```
24
+ lib/legion/extensions/cognitive_zeitgeist/
25
+ version.rb
26
+ helpers/
27
+ constants.rb # Signal domains, label tables for mood/convergence/momentum
28
+ cognitive_signal.rb # CognitiveSignal class — one subsystem signal
29
+ trend_window.rb # TrendWindow — sliding window over recent signals
30
+ zeitgeist_engine.rb # ZeitgeistEngine — registry, analytics
31
+ client.rb # Client lives at helpers/client.rb (unusual location)
32
+ runners/
33
+ cognitive_zeitgeist.rb # Runner module — public API
34
+ ```
35
+
36
+ Note: `client.rb` is at `helpers/client.rb`, not the standard `lib/legion/extensions/cognitive_zeitgeist/client.rb`.
37
+
38
+ ## Key Constants
39
+
40
+ | Constant | Value | Meaning |
41
+ |---|---|---|
42
+ | `MAX_SIGNALS` | 1000 | Ring buffer for all ingested signals |
43
+ | `WINDOW_SIZE` | 100 | Sliding window size for `TrendWindow` |
44
+ | `DEFAULT_INTENSITY` | 0.5 | Default signal intensity |
45
+ | `MOMENTUM_THRESHOLD` | 0.3 | Absolute momentum threshold for `accelerating?` / `decelerating?` |
46
+ | `CONVERGENCE_THRESHOLD` | 0.7 | `convergence_alert?` if above this (not used — see `DIVERGENCE_THRESHOLD`) |
47
+ | `DIVERGENCE_THRESHOLD` | 0.3 | `divergence_alert?` if cognitive_convergence < this |
48
+
49
+ `SIGNAL_DOMAINS`: `[:threat, :opportunity, :curiosity, :anxiety, :creativity, :routine, :social, :abstract]`
50
+
51
+ Mood labels (applied to normalized valence, -1..1 mapped to 0..1): `0.8+` = `:euphoric`, `0.6..0.8` = `:elevated`, `0.4..0.6` = `:neutral`, `0.2..0.4` = `:subdued`, `< 0.2` = `:suppressed`
52
+
53
+ Convergence labels: `0.8+` = `:unified`, `0.6..0.8` = `:aligned`, `0.4..0.6` = `:mixed`, `0.2..0.4` = `:fragmented`, `< 0.2` = `:divergent`
54
+
55
+ Momentum labels (can be negative): `0.6+` = `:surging`, `0.3..0.6` = `:building`, `0.0..0.3` = `:steady`, `-0.3..0.0` = `:fading`, `< -0.3` = `:collapsing`
56
+
57
+ `Constants.label_for(labels_hash, value)` — iterates hash by range cover; returns nil if no match.
58
+
59
+ ## Key Classes
60
+
61
+ ### `Helpers::CognitiveSignal`
62
+
63
+ One signal from a cognitive subsystem.
64
+
65
+ - Fields: `id` (UUID), `source_subsystem` (symbol), `domain` (symbol), `intensity` (0.0–1.0), `valence` (-1.0–1.0), `timestamp`
66
+ - `valence` is bidirectional (-1.0 negative through +1.0 positive) — unlike most other extensions which use unsigned intensity
67
+
68
+ ### `Helpers::TrendWindow`
69
+
70
+ Sliding window (default 100 signals) for local trend analysis.
71
+
72
+ - `add(signal)` — appends and shifts oldest when over `window_size`; returns `self`
73
+ - `dominant_domain` — domain with highest total intensity weight across window signals
74
+ - `dominant_valence` — intensity-weighted mean valence across window
75
+ - `momentum` — delta between second-half avg intensity and first-half avg intensity; positive = accelerating
76
+ - `accelerating?` — momentum > 0.3; `decelerating?` — momentum < -0.3
77
+
78
+ ### `Helpers::ZeitgeistEngine`
79
+
80
+ Central registry and analytics engine.
81
+
82
+ - `ingest(source_subsystem:, domain:, intensity:, valence:, timestamp:)` — creates `CognitiveSignal`, appends to `@signals` (ring buffer), adds to `@trend_window`
83
+ - `dominant_themes(limit:)` — domains ranked by total intensity weight; returns `[{ domain:, weight: }]`
84
+ - `collective_mood` — intensity-weighted mean valence across all signals; returns -1.0..1.0
85
+ - `cognitive_convergence` — ratio of subsystems whose dominant domain matches the overall dominant domain; returns 1.0 if <= 1 subsystem; returns 0.5 if no overall dominant found
86
+ - `rising_domains(window_size:)` / `falling_domains(window_size:)` — compare recent half to earlier half of signals; return `[{ domain:, delta: }]` sorted by magnitude
87
+ - `divergence_alert?` — `cognitive_convergence < DIVERGENCE_THRESHOLD` (0.3)
88
+ - `zeitgeist_report` — full report: signal_count, dominant_themes, collective_mood, mood_label, convergence, convergence_label, momentum, momentum_label, rising_domains, falling_domains, divergence_alert, trend_window
89
+
90
+ Mood normalization: `normalize_mood(mood_value) = (mood + 1.0) / 2.0` — maps -1..1 to 0..1 before label lookup.
91
+
92
+ ## Runners
93
+
94
+ Module: `Legion::Extensions::CognitiveZeitgeist::Runners::CognitiveZeitgeist`
95
+
96
+ | Runner | Key Args | Returns |
97
+ |---|---|---|
98
+ | `ingest_signal` | `source_subsystem:`, `domain:`, `intensity:`, `valence:` | `{ success:, signal: }` |
99
+ | `zeitgeist_report` | — | full report merged with `success: true` |
100
+ | `collective_mood` | — | `{ success:, mood:, mood_label: }` |
101
+ | `cognitive_convergence` | — | `{ success:, convergence:, convergence_label:, divergence_alert: }` |
102
+ | `dominant_themes` | `limit:` | `{ success:, themes:, count: }` |
103
+ | `rising_domains` | — | `{ success:, domains:, count: }` |
104
+ | `falling_domains` | — | `{ success:, domains:, count: }` |
105
+ | `trend_window_status` | — | `{ success:, trend_window:, momentum_label: }` |
106
+
107
+ All runners accept optional `engine:` keyword for test injection.
108
+
109
+ ## Integration Points
110
+
111
+ - `ingest_signal` should be called by `lex-tick` phase handlers after each phase completes, passing the originating subsystem and domain
112
+ - `divergence_alert?` can trigger conflict escalation in `lex-conflict` when subsystems are pulling in incompatible directions
113
+ - `collective_mood` provides a cross-subsystem mood aggregate that `lex-emotion` can use to modulate valence
114
+ - `dominant_themes` reveals what domains are consuming the most cognitive bandwidth
115
+ - All state is in-memory per `ZeitgeistEngine` instance
116
+
117
+ ## Development Notes
118
+
119
+ - `client.rb` is at `helpers/client.rb` — an unusual location for this gem; not at the standard top-level path
120
+ - Valence is signed (-1.0 to 1.0) in this extension; mood label lookup requires normalizing to 0..1 via `(mood + 1.0) / 2.0`
121
+ - `cognitive_convergence` returns 1.0 when only one subsystem has ingested signals (no divergence possible) and 0.5 when no overall dominant domain can be determined
122
+ - `rising_domains` requires `signals.size >= window_size` (default 50) — returns empty array if insufficient history
123
+ - Momentum labels cover negative values (`:fading`, `:collapsing`) — unique among the label tables in this extension category
124
+ - `CONVERGENCE_THRESHOLD` (0.7) is defined but not used; only `DIVERGENCE_THRESHOLD` (0.3) drives alert logic
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rubocop', '~> 1.75'
10
+ gem 'rubocop-rspec'
11
+ end
12
+
13
+ gem 'legion-gaia', path: '../../legion-gaia'
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lex-cognitive-zeitgeist (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-zeitgeist!
73
+ rspec (~> 3.13)
74
+ rubocop (~> 1.75)
75
+ rubocop-rspec
76
+
77
+ BUNDLED WITH
78
+ 2.6.9
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Esity
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # lex-cognitive-zeitgeist
2
+
3
+ A LegionIO cognitive architecture extension that captures the aggregate cognitive atmosphere across subsystems. Named after the German "spirit of the age" — surfaces dominant themes, collective mood, convergence, and momentum from the stream of signals flowing through the cognitive system.
4
+
5
+ ## What It Does
6
+
7
+ Ingests **cognitive signals** from multiple subsystems and computes cross-cutting metrics:
8
+
9
+ - **Collective mood**: intensity-weighted mean valence across all signals (-1.0 to +1.0)
10
+ - **Cognitive convergence**: ratio of subsystems focused on the same dominant domain; low convergence triggers a divergence alert
11
+ - **Momentum**: whether signal intensity is accelerating or fading in the recent window
12
+ - **Dominant themes**: domains with highest total intensity weight
13
+ - **Rising/falling domains**: comparing recent activity to historical baseline
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require 'lex-cognitive-zeitgeist'
19
+
20
+ client = Legion::Extensions::CognitiveZeitgeist::Client.new
21
+
22
+ # Ingest signals from different subsystems
23
+ client.ingest_signal(source_subsystem: :emotion, domain: :anxiety, intensity: 0.7, valence: -0.6)
24
+ client.ingest_signal(source_subsystem: :prediction, domain: :threat, intensity: 0.8, valence: -0.4)
25
+ client.ingest_signal(source_subsystem: :memory, domain: :curiosity, intensity: 0.5, valence: 0.3)
26
+
27
+ # Get collective mood
28
+ client.collective_mood
29
+ # => { success: true, mood: -0.4, mood_label: :subdued }
30
+
31
+ # Check whether subsystems are aligned
32
+ client.cognitive_convergence
33
+ # => { success: true, convergence: 0.67, convergence_label: :aligned, divergence_alert: false }
34
+
35
+ # Dominant themes (by intensity weight)
36
+ client.dominant_themes(limit: 3)
37
+ # => { success: true, themes: [{ domain: :threat, weight: 0.8 }, ...], count: 3 }
38
+
39
+ # Rising and falling domains
40
+ client.rising_domains
41
+ # => { success: true, domains: [{ domain: :threat, delta: 0.3 }], count: 1 }
42
+
43
+ # Trend window status
44
+ client.trend_window_status
45
+ # => { success: true, trend_window: { size: 3, dominant_domain: :threat, momentum: 0.1, ... }, momentum_label: :steady }
46
+
47
+ # Full zeitgeist report
48
+ client.zeitgeist_report
49
+ # => { success: true, signal_count: 3, dominant_themes: [...], collective_mood: -0.4,
50
+ # mood_label: :subdued, convergence: 0.67, convergence_label: :aligned,
51
+ # momentum: 0.1, momentum_label: :steady, divergence_alert: false, ... }
52
+ ```
53
+
54
+ ## Development
55
+
56
+ ```bash
57
+ bundle install
58
+ bundle exec rspec
59
+ bundle exec rubocop
60
+ ```
61
+
62
+ ## License
63
+
64
+ MIT
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_zeitgeist/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-zeitgeist'
7
+ spec.version = Legion::Extensions::CognitiveZeitgeist::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'Collective cognitive zeitgeist detection for LegionIO agents'
12
+ spec.description = 'Detects the overall mood and trending concerns across all cognitive subsystems. ' \
13
+ 'Captures what the agent mind is collectively focused on via signal ingestion, ' \
14
+ 'trend windows, convergence scoring, and divergence alerts.'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-zeitgeist'
16
+ spec.license = 'MIT'
17
+
18
+ spec.required_ruby_version = '>= 3.4'
19
+
20
+ spec.metadata['homepage_uri'] = spec.homepage
21
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-zeitgeist'
22
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-zeitgeist'
23
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-zeitgeist/blob/master/CHANGELOG.md'
24
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-zeitgeist/issues'
25
+ spec.metadata['rubygems_mfa_required'] = 'true'
26
+
27
+ spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
28
+ ls.readlines("\x0", chomp: true)
29
+ end
30
+
31
+ spec.require_paths = ['lib']
32
+ spec.add_development_dependency 'legion-gaia'
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveZeitgeist
6
+ module Helpers
7
+ class Client
8
+ include Runners::CognitiveZeitgeist
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveZeitgeist
6
+ module Helpers
7
+ class CognitiveSignal
8
+ attr_reader :id, :source_subsystem, :domain, :intensity, :valence, :timestamp
9
+
10
+ def initialize(source_subsystem:, domain:, intensity: Constants::DEFAULT_INTENSITY,
11
+ valence: 0.0, timestamp: nil)
12
+ @id = SecureRandom.uuid
13
+ @source_subsystem = source_subsystem.to_sym
14
+ @domain = domain.to_sym
15
+ @intensity = intensity.to_f.clamp(0.0, 1.0)
16
+ @valence = valence.to_f.clamp(-1.0, 1.0)
17
+ @timestamp = timestamp || Time.now.utc
18
+ end
19
+
20
+ def to_h
21
+ {
22
+ id: @id,
23
+ source_subsystem: @source_subsystem,
24
+ domain: @domain,
25
+ intensity: @intensity.round(10),
26
+ valence: @valence.round(10),
27
+ timestamp: @timestamp.iso8601
28
+ }
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveZeitgeist
6
+ module Helpers
7
+ module Constants
8
+ MAX_SIGNALS = 1000
9
+ WINDOW_SIZE = 100
10
+ DEFAULT_INTENSITY = 0.5
11
+ MOMENTUM_THRESHOLD = 0.3
12
+ CONVERGENCE_THRESHOLD = 0.7
13
+ DIVERGENCE_THRESHOLD = 0.3
14
+
15
+ SIGNAL_DOMAINS = %i[
16
+ threat
17
+ opportunity
18
+ curiosity
19
+ anxiety
20
+ creativity
21
+ routine
22
+ social
23
+ abstract
24
+ ].freeze
25
+
26
+ MOOD_LABELS = {
27
+ (0.8..) => :euphoric,
28
+ (0.6...0.8) => :elevated,
29
+ (0.4...0.6) => :neutral,
30
+ (0.2...0.4) => :subdued,
31
+ (..0.2) => :suppressed
32
+ }.freeze
33
+
34
+ CONVERGENCE_LABELS = {
35
+ (0.8..) => :unified,
36
+ (0.6...0.8) => :aligned,
37
+ (0.4...0.6) => :mixed,
38
+ (0.2...0.4) => :fragmented,
39
+ (..0.2) => :divergent
40
+ }.freeze
41
+
42
+ MOMENTUM_LABELS = {
43
+ (0.6..) => :surging,
44
+ (0.3...0.6) => :building,
45
+ (0.0...0.3) => :steady,
46
+ (-0.3...0.0) => :fading,
47
+ (..-0.3) => :collapsing
48
+ }.freeze
49
+
50
+ def self.label_for(labels_hash, value)
51
+ labels_hash.each do |range, label|
52
+ return label if range.cover?(value)
53
+ end
54
+ nil
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveZeitgeist
6
+ module Helpers
7
+ class TrendWindow
8
+ include Constants
9
+
10
+ attr_reader :signals, :window_size
11
+
12
+ def initialize(window_size: WINDOW_SIZE)
13
+ @signals = []
14
+ @window_size = window_size
15
+ end
16
+
17
+ def add(signal)
18
+ @signals << signal
19
+ @signals.shift while @signals.size > @window_size
20
+ self
21
+ end
22
+
23
+ def dominant_domain
24
+ return nil if @signals.empty?
25
+
26
+ counts = Hash.new(0.0)
27
+ @signals.each { |s| counts[s.domain] += s.intensity }
28
+ counts.max_by { |_d, weight| weight }&.first
29
+ end
30
+
31
+ def dominant_valence
32
+ return 0.0 if @signals.empty?
33
+
34
+ weighted_sum = @signals.sum { |s| s.valence * s.intensity }
35
+ total_intensity = @signals.sum(&:intensity)
36
+ return 0.0 if total_intensity.zero?
37
+
38
+ (weighted_sum / total_intensity).clamp(-1.0, 1.0).round(10)
39
+ end
40
+
41
+ def momentum
42
+ return 0.0 if @signals.size < 2
43
+
44
+ half = @signals.size / 2
45
+ first_half = @signals.first(half)
46
+ second_half = @signals.last(half)
47
+
48
+ avg_intensity = ->(arr) { arr.sum(&:intensity) / arr.size.to_f }
49
+ delta = avg_intensity.call(second_half) - avg_intensity.call(first_half)
50
+ delta.clamp(-1.0, 1.0).round(10)
51
+ end
52
+
53
+ def accelerating?
54
+ momentum > MOMENTUM_THRESHOLD
55
+ end
56
+
57
+ def decelerating?
58
+ momentum < -MOMENTUM_THRESHOLD
59
+ end
60
+
61
+ def to_h
62
+ {
63
+ size: @signals.size,
64
+ window_size: @window_size,
65
+ dominant_domain: dominant_domain,
66
+ dominant_valence: dominant_valence,
67
+ momentum: momentum,
68
+ accelerating: accelerating?,
69
+ decelerating: decelerating?
70
+ }
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end