lex-cognitive-narrative-arc 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: 2274e2972537f554c592cd9128575d6bc006b3a2d8675cb4eb74b6ed9759d6f4
4
+ data.tar.gz: cb53f27b356b02768b53e099f646e07bea79d6a367456f14e1f756b9014a4e7d
5
+ SHA512:
6
+ metadata.gz: f9e0d5b050adae938413b2dad6bd836f6d78e80189599ecf7ce08a38d192f77ecf8ab85515476af2e770d68a15afd06a09f63e9d7ed788f8501c02069be48a29
7
+ data.tar.gz: 6ff13f7dee6c859469bef93ac99d85bfd8bc91a1434ae056d607c1fdd3eab3758cc50a119b9747655f4a141c96eaca5e318d0c24eaac357e4a4f6df7afe42f97
@@ -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,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
10
+ .rspec_status
11
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,64 @@
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/MethodLength:
17
+ Max: 25
18
+
19
+ Metrics/ClassLength:
20
+ Max: 150
21
+
22
+ Metrics/ModuleLength:
23
+ Max: 1500
24
+
25
+ Metrics/BlockLength:
26
+ Max: 40
27
+ Exclude:
28
+ - 'spec/**/*'
29
+
30
+ Metrics/AbcSize:
31
+ Max: 25
32
+
33
+ Metrics/ParameterLists:
34
+ Max: 8
35
+ MaxOptionalParameters: 8
36
+
37
+ Metrics/CyclomaticComplexity:
38
+ Max: 15
39
+
40
+ Metrics/PerceivedComplexity:
41
+ Max: 17
42
+
43
+ Style/Documentation:
44
+ Enabled: false
45
+
46
+ Style/OneClassPerFile:
47
+ Exclude:
48
+ - 'spec/spec_helper.rb'
49
+
50
+ Style/SymbolArray:
51
+ Enabled: true
52
+
53
+ Style/FrozenStringLiteralComment:
54
+ Enabled: true
55
+ EnforcedStyle: always
56
+
57
+ Naming/FileName:
58
+ Enabled: false
59
+
60
+ Naming/PredicateMethod:
61
+ Enabled: false
62
+
63
+ Naming/PredicatePrefix:
64
+ Enabled: false
data/CLAUDE.md ADDED
@@ -0,0 +1,100 @@
1
+ # lex-cognitive-narrative-arc
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+
6
+ ## Purpose
7
+
8
+ Narrative arc tracking engine for cognitive processing. Models ongoing cognitive situations as narrative arcs with beats that drive tension through phases. Beat events (exposition, rising_action, complication, crisis, climax, falling_action, resolution, denouement) each carry intensity and emotional_charge and adjust the arc's tension level. Phases (building, peak, resolving, complete) transition automatically based on tension thresholds. A dramatic score synthesizes tension, beat count, and average intensity.
9
+
10
+ ## Gem Info
11
+
12
+ - **Gem name**: `lex-cognitive-narrative-arc`
13
+ - **Module**: `Legion::Extensions::CognitiveNarrativeArc`
14
+ - **Version**: `0.1.0`
15
+ - **Ruby**: `>= 3.4`
16
+ - **License**: MIT
17
+
18
+ ## File Structure
19
+
20
+ ```
21
+ lib/legion/extensions/cognitive_narrative_arc/
22
+ version.rb
23
+ client.rb
24
+ helpers/
25
+ constants.rb
26
+ arc.rb
27
+ beat_event.rb
28
+ runners/
29
+ narrative.rb
30
+ ```
31
+
32
+ ## Key Constants
33
+
34
+ | Constant | Value | Purpose |
35
+ |---|---|---|
36
+ | `MAX_ARCS` | `100` | Per-engine arc capacity |
37
+ | `MAX_BEATS_PER_ARC` | `50` | Max beats per arc |
38
+ | `DEFAULT_TENSION` | `0.3` | Starting tension for new arcs |
39
+ | `TENSION_RISE` | `0.1` | Tension increase for escalating beats |
40
+ | `TENSION_FALL` | `0.08` | Tension decrease for resolving beats |
41
+ | `CLIMAX_THRESHOLD` | `0.8` | Tension above which arc enters peak phase |
42
+ | `RESOLUTION_THRESHOLD` | `0.2` | Tension below which arc enters resolved phase |
43
+ | `BEAT_TYPES` | `%i[exposition rising_action complication crisis climax falling_action resolution denouement]` | Valid beat types |
44
+ | `ARC_PHASES` | `%i[building peak resolving complete]` | Arc lifecycle phases |
45
+ | `TENSION_LABELS` | range hash | From `:calm` to `:explosive` |
46
+ | `DRAMA_LABELS` | range hash | From `:mundane` to `:epic` |
47
+ | `PHASE_LABELS` | hash | Human-readable phase names |
48
+
49
+ ## Helpers
50
+
51
+ ### `Helpers::BeatEvent`
52
+ Immutable (frozen) record of a single narrative beat. Has `id`, `beat_type`, `content`, `intensity` (0.0–1.0), `emotional_charge` (-1.0 to 1.0), and `occurred_at`.
53
+
54
+ - `climactic?` — `beat_type == :climax`
55
+ - `resolving?` — `beat_type` is `:resolution` or `:denouement`
56
+ - `emotional_charge` — pre-computed from beat type: crisis/climax are negative, resolution/denouement are positive
57
+
58
+ ### `Helpers::Arc`
59
+ Active narrative arc. Has `id`, `title`, `domain`, `genre`, `tension`, `phase`, `beats` (array of `BeatEvent`), and `created_at`.
60
+
61
+ - `add_beat!(beat_type:, content:, intensity:)` — creates `BeatEvent`, adjusts tension (escalating beats raise tension, resolving beats lower it), appends to beats array
62
+ - `advance_phase!` — checks tension vs thresholds and transitions phase
63
+ - `tension_rise!(amount)` — direct tension increase
64
+ - `tension_fall!(amount)` — direct tension decrease
65
+ - `climaxed?` — phase is `:peak` or later
66
+ - `resolved?` — phase is `:resolving` or `:complete`
67
+ - `complete?` — phase is `:complete`
68
+ - `dramatic_score` — composite: `tension * 0.4 + (beat_count / MAX_BEATS) * 0.3 + avg_intensity * 0.3`
69
+
70
+ ## Runners
71
+
72
+ Module: `Runners::Narrative`
73
+
74
+ | Runner Method | Description |
75
+ |---|---|
76
+ | `create_arc(title:, domain:, genre:)` | Start a new narrative arc |
77
+ | `add_beat(arc_id:, beat_type:, content:, intensity:)` | Add a beat to an arc |
78
+ | `get_arc(arc_id:)` | Retrieve arc details |
79
+ | `active_arcs` | All arcs not yet complete |
80
+ | `completed_arcs` | All resolved/complete arcs |
81
+ | `most_dramatic_arc` | Arc with highest dramatic score |
82
+ | `arc_report` | Aggregate stats across all arcs |
83
+
84
+ All runners return `{success: true/false, ...}` hashes.
85
+
86
+ ## Integration Points
87
+
88
+ - `lex-emotion`: beat emotional_charge directly feeds into `lex-emotion` valence evaluation
89
+ - `lex-tick` `action_selection`: high-tension arcs (peak phase) should trigger cautious behavior; resolved arcs can close open actions
90
+ - `lex-conflict`: rising_action/complication/crisis beats parallel conflict escalation in `lex-conflict`
91
+ - `lex-memory`: arcs serve as episodic containers — complete arcs can be consolidated as episodic memory traces
92
+
93
+ ## Development Notes
94
+
95
+ - `Client` instantiates `@narrative_engine = Helpers::NarrativeEngine.new`
96
+ - `add_beat!` drives both the events log and phase transitions atomically
97
+ - `TENSION_RISE` is applied for: `rising_action`, `complication`, `crisis`, `climax` beats
98
+ - `TENSION_FALL` is applied for: `falling_action`, `resolution`, `denouement` beats
99
+ - `exposition` beats are neutral (no tension change)
100
+ - `dramatic_score` normalizes beat_count to `MAX_BEATS_PER_ARC` so all three components are [0.0, 1.0]
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', require: false
9
+ gem 'rubocop-rspec', require: false
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # lex-cognitive-narrative-arc
2
+
3
+ Narrative arc tracking for LegionIO cognitive agents. Models ongoing situations as story arcs with beats that drive tension through building, peak, resolving, and complete phases. Dramatic score synthesizes tension, event count, and intensity.
4
+
5
+ ## What It Does
6
+
7
+ - Eight beat types: exposition, rising_action, complication, crisis, climax, falling_action, resolution, denouement
8
+ - Tension adjusts automatically per beat type (crisis/climax raise; resolution/denouement lower)
9
+ - Four arc phases: building → peak (tension >= 0.8) → resolving (tension <= 0.2) → complete
10
+ - Each beat has intensity (0.0–1.0) and emotional_charge (-1.0 to 1.0)
11
+ - Dramatic score: composite of tension (40%), beat count ratio (30%), average intensity (30%)
12
+ - Track active, completed, and most-dramatic arcs
13
+
14
+ ## Usage
15
+
16
+ ```ruby
17
+ # Create an arc
18
+ result = runner.create_arc(title: 'deployment_incident', domain: :operations, genre: :crisis)
19
+ arc_id = result[:arc][:id]
20
+
21
+ # Add beats
22
+ runner.add_beat(arc_id: arc_id, beat_type: :rising_action,
23
+ content: 'error rate climbing', intensity: 0.6)
24
+ runner.add_beat(arc_id: arc_id, beat_type: :crisis,
25
+ content: 'service unavailable', intensity: 0.9)
26
+ # => { success: true, arc: { tension: 0.5, phase: :building, ... } }
27
+
28
+ runner.add_beat(arc_id: arc_id, beat_type: :climax,
29
+ content: 'root cause identified', intensity: 1.0)
30
+ # tension reaches peak phase
31
+
32
+ runner.add_beat(arc_id: arc_id, beat_type: :resolution,
33
+ content: 'hotfix deployed', intensity: 0.7)
34
+
35
+ # Check most dramatic
36
+ runner.most_dramatic_arc
37
+ # => { success: true, arc: { title: 'deployment_incident', dramatic_score: 0.73, ... } }
38
+ ```
39
+
40
+ ## Development
41
+
42
+ ```bash
43
+ bundle install
44
+ bundle exec rspec
45
+ bundle exec rubocop
46
+ ```
47
+
48
+ ## License
49
+
50
+ MIT
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_narrative_arc/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-narrative-arc'
7
+ spec.version = Legion::Extensions::CognitiveNarrativeArc::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Narrative Arc'
12
+ spec.description = 'Narrative arc detection in cognitive experience for LegionIO'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-narrative-arc'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-narrative-arc'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-narrative-arc'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-narrative-arc'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-narrative-arc/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
25
+ f.match(%r{\A(?:test|spec|features)/})
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_narrative_arc/helpers/constants'
4
+ require 'legion/extensions/cognitive_narrative_arc/helpers/beat_event'
5
+ require 'legion/extensions/cognitive_narrative_arc/helpers/arc'
6
+ require 'legion/extensions/cognitive_narrative_arc/helpers/arc_engine'
7
+ require 'legion/extensions/cognitive_narrative_arc/runners/narrative'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module CognitiveNarrativeArc
12
+ class Client
13
+ include Runners::Narrative
14
+
15
+ def initialize(**)
16
+ @arc_engine = Helpers::ArcEngine.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :arc_engine
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveNarrativeArc
8
+ module Helpers
9
+ class Arc
10
+ attr_reader :arc_id, :title, :domain, :beats, :arc_phase, :tension_level,
11
+ :created_at, :resolved_at
12
+
13
+ def initialize(title:, domain: :general, initial_tension: Constants::DEFAULT_TENSION)
14
+ @arc_id = SecureRandom.uuid
15
+ @title = title
16
+ @domain = domain
17
+ @beats = []
18
+ @arc_phase = :building
19
+ @tension_level = initial_tension.clamp(0.0, 1.0)
20
+ @created_at = Time.now.utc
21
+ @resolved_at = nil
22
+ end
23
+
24
+ def add_beat!(beat)
25
+ return false if @beats.size >= Constants::MAX_BEATS_PER_ARC
26
+ return false if complete?
27
+
28
+ @beats << beat
29
+ adjust_tension_for_beat(beat)
30
+ advance_phase!
31
+ true
32
+ end
33
+
34
+ def advance_phase!
35
+ new_phase = detect_phase
36
+ @arc_phase = new_phase if new_phase != @arc_phase
37
+ @resolved_at = Time.now.utc if @arc_phase == :complete
38
+ @arc_phase
39
+ end
40
+
41
+ def tension_rise!(amount = Constants::TENSION_RISE)
42
+ @tension_level = (@tension_level + amount).round(10).clamp(0.0, 1.0)
43
+ end
44
+
45
+ def tension_fall!(amount = Constants::TENSION_FALL)
46
+ @tension_level = (@tension_level - amount).round(10).clamp(0.0, 1.0)
47
+ end
48
+
49
+ def climaxed?
50
+ @tension_level >= Constants::CLIMAX_THRESHOLD || @arc_phase == :peak
51
+ end
52
+
53
+ def resolved?
54
+ @arc_phase == :complete
55
+ end
56
+
57
+ def complete?
58
+ @arc_phase == :complete
59
+ end
60
+
61
+ def dramatic_score
62
+ return 0.0 if @beats.empty?
63
+
64
+ tension_contrib = @tension_level * 0.4
65
+ beat_count_contrib = [@beats.size.to_f / Constants::MAX_BEATS_PER_ARC, 1.0].min * 0.3
66
+ intensity_contrib = average_beat_intensity * 0.3
67
+ (tension_contrib + beat_count_contrib + intensity_contrib).round(10)
68
+ end
69
+
70
+ def tension_label
71
+ Constants.label_for(Constants::TENSION_LABELS, @tension_level)
72
+ end
73
+
74
+ def drama_label
75
+ Constants.label_for(Constants::DRAMA_LABELS, dramatic_score)
76
+ end
77
+
78
+ def to_h
79
+ {
80
+ arc_id: @arc_id,
81
+ title: @title,
82
+ domain: @domain,
83
+ arc_phase: @arc_phase,
84
+ tension_level: @tension_level,
85
+ beat_count: @beats.size,
86
+ dramatic_score: dramatic_score,
87
+ tension_label: tension_label,
88
+ drama_label: drama_label,
89
+ created_at: @created_at,
90
+ resolved_at: @resolved_at
91
+ }
92
+ end
93
+
94
+ private
95
+
96
+ def average_beat_intensity
97
+ return 0.0 if @beats.empty?
98
+
99
+ @beats.sum(&:intensity) / @beats.size.to_f
100
+ end
101
+
102
+ def adjust_tension_for_beat(beat)
103
+ case beat.beat_type
104
+ when :rising_action, :complication, :crisis
105
+ tension_rise!(beat.intensity * Constants::TENSION_RISE)
106
+ when :climax
107
+ @tension_level = [@tension_level, Constants::CLIMAX_THRESHOLD].max.clamp(0.0, 1.0)
108
+ when :falling_action, :resolution, :denouement
109
+ tension_fall!(beat.intensity * Constants::TENSION_FALL)
110
+ end
111
+ end
112
+
113
+ def detect_phase
114
+ return :complete if has_resolution_beat?
115
+ return :resolving if has_climax_beat? && @tension_level < Constants::CLIMAX_THRESHOLD
116
+ return :peak if @tension_level >= Constants::CLIMAX_THRESHOLD
117
+ return :building if @tension_level < Constants::CLIMAX_THRESHOLD
118
+
119
+ @arc_phase
120
+ end
121
+
122
+ def has_climax_beat?
123
+ @beats.any?(&:climactic?)
124
+ end
125
+
126
+ def has_resolution_beat?
127
+ @beats.any? { |b| %i[resolution denouement].include?(b.beat_type) }
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveNarrativeArc
6
+ module Helpers
7
+ class ArcEngine
8
+ attr_reader :arcs
9
+
10
+ def initialize
11
+ @arcs = {}
12
+ end
13
+
14
+ def create_arc(title:, domain: :general, initial_tension: Constants::DEFAULT_TENSION)
15
+ return nil if @arcs.size >= Constants::MAX_ARCS
16
+
17
+ arc = Arc.new(title: title, domain: domain, initial_tension: initial_tension)
18
+ @arcs[arc.arc_id] = arc
19
+ arc
20
+ end
21
+
22
+ def add_beat(arc_id:, content:, intensity: 0.5, beat_type: :rising_action,
23
+ domain: :general, emotional_charge: 0.0)
24
+ arc = @arcs[arc_id]
25
+ return { success: false, reason: :arc_not_found } unless arc
26
+
27
+ beat = BeatEvent.new(
28
+ content: content,
29
+ intensity: intensity,
30
+ beat_type: beat_type,
31
+ domain: domain,
32
+ emotional_charge: emotional_charge
33
+ )
34
+
35
+ added = arc.add_beat!(beat)
36
+ return { success: false, reason: :arc_full_or_complete } unless added
37
+
38
+ { success: true, beat_id: beat.beat_id, arc_phase: arc.arc_phase,
39
+ tension_level: arc.tension_level }
40
+ end
41
+
42
+ def get_arc(arc_id)
43
+ @arcs[arc_id]
44
+ end
45
+
46
+ def active_arcs
47
+ @arcs.values.reject(&:complete?)
48
+ end
49
+
50
+ def completed_arcs
51
+ @arcs.values.select(&:complete?)
52
+ end
53
+
54
+ def most_dramatic_arc
55
+ return nil if @arcs.empty?
56
+
57
+ @arcs.values.max_by(&:dramatic_score)
58
+ end
59
+
60
+ def tension_distribution
61
+ return {} if @arcs.empty?
62
+
63
+ counts = Hash.new(0)
64
+ @arcs.each_value do |arc|
65
+ label = arc.tension_label
66
+ counts[label] += 1
67
+ end
68
+ counts
69
+ end
70
+
71
+ def detect_narrative_patterns
72
+ return [] if @arcs.size < 2
73
+
74
+ patterns = []
75
+ patterns << :recurring_crisis if recurring_beat_pattern?(:crisis)
76
+ patterns << :recurring_climax if recurring_beat_pattern?(:climax)
77
+ patterns << :unresolved_tension if unresolved_high_tension?
78
+ patterns << :rapid_resolution if rapid_resolution_pattern?
79
+ patterns
80
+ end
81
+
82
+ def arc_report
83
+ {
84
+ total_arcs: @arcs.size,
85
+ active: active_arcs.size,
86
+ completed: completed_arcs.size,
87
+ patterns: detect_narrative_patterns,
88
+ tension_dist: tension_distribution,
89
+ most_dramatic: most_dramatic_arc&.to_h
90
+ }
91
+ end
92
+
93
+ private
94
+
95
+ def recurring_beat_pattern?(beat_type)
96
+ arc_with_beat_type_count = @arcs.values.count do |arc|
97
+ arc.beats.any? { |b| b.beat_type == beat_type }
98
+ end
99
+ arc_with_beat_type_count >= 2
100
+ end
101
+
102
+ def unresolved_high_tension?
103
+ active_arcs.any? { |arc| arc.tension_level >= Constants::CLIMAX_THRESHOLD }
104
+ end
105
+
106
+ def rapid_resolution_pattern?
107
+ @arcs.values.any? do |arc|
108
+ arc.complete? && arc.beats.size <= 3
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveNarrativeArc
8
+ module Helpers
9
+ class BeatEvent
10
+ attr_reader :beat_id, :content, :intensity, :beat_type, :domain,
11
+ :emotional_charge, :created_at
12
+
13
+ def initialize(content:, intensity: 0.5, beat_type: :rising_action,
14
+ domain: :general, emotional_charge: 0.0)
15
+ @beat_id = SecureRandom.uuid
16
+ @content = content
17
+ @intensity = intensity.clamp(0.0, 1.0)
18
+ @beat_type = validate_beat_type(beat_type)
19
+ @domain = domain
20
+ @emotional_charge = emotional_charge.clamp(-1.0, 1.0)
21
+ @created_at = Time.now.utc
22
+ end
23
+
24
+ def to_h
25
+ {
26
+ beat_id: @beat_id,
27
+ content: @content,
28
+ intensity: @intensity,
29
+ beat_type: @beat_type,
30
+ domain: @domain,
31
+ emotional_charge: @emotional_charge,
32
+ created_at: @created_at
33
+ }
34
+ end
35
+
36
+ def climactic?
37
+ @beat_type == :climax || @intensity >= Constants::CLIMAX_THRESHOLD
38
+ end
39
+
40
+ def resolving?
41
+ %i[falling_action resolution denouement].include?(@beat_type)
42
+ end
43
+
44
+ private
45
+
46
+ def validate_beat_type(type)
47
+ return type if Constants::BEAT_TYPES.include?(type)
48
+
49
+ Constants::BEAT_TYPES.first
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveNarrativeArc
6
+ module Helpers
7
+ module Constants
8
+ MAX_ARCS = 100
9
+ MAX_BEATS_PER_ARC = 50
10
+ DEFAULT_TENSION = 0.3
11
+ TENSION_RISE = 0.1
12
+ TENSION_FALL = 0.08
13
+ CLIMAX_THRESHOLD = 0.8
14
+ RESOLUTION_THRESHOLD = 0.2
15
+
16
+ BEAT_TYPES = %i[
17
+ exposition
18
+ rising_action
19
+ complication
20
+ crisis
21
+ climax
22
+ falling_action
23
+ resolution
24
+ denouement
25
+ ].freeze
26
+
27
+ ARC_PHASES = %i[building peak resolving complete].freeze
28
+
29
+ TENSION_LABELS = {
30
+ (0.0..0.2) => :calm,
31
+ (0.2..0.5) => :developing,
32
+ (0.5..0.8) => :tense,
33
+ (0.8..1.0) => :critical
34
+ }.freeze
35
+
36
+ DRAMA_LABELS = {
37
+ (0.0..0.25) => :mundane,
38
+ (0.25..0.5) => :engaging,
39
+ (0.5..0.75) => :compelling,
40
+ (0.75..1.0) => :gripping
41
+ }.freeze
42
+
43
+ PHASE_LABELS = {
44
+ building: 'Rising action building toward climax',
45
+ peak: 'At or near peak tension — climax active',
46
+ resolving: 'Falling action moving toward resolution',
47
+ complete: 'Arc resolved and closed'
48
+ }.freeze
49
+
50
+ module_function
51
+
52
+ def label_for(labels_hash, value)
53
+ labels_hash.each do |range, label|
54
+ return label if range.cover?(value)
55
+ end
56
+ labels_hash.values.last
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveNarrativeArc
6
+ module Runners
7
+ module Narrative
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def create_arc(title:, domain: :general, initial_tension: Helpers::Constants::DEFAULT_TENSION,
12
+ engine: nil, **)
13
+ eng = engine || arc_engine
14
+ arc = eng.create_arc(title: title, domain: domain, initial_tension: initial_tension)
15
+
16
+ unless arc
17
+ Legion::Logging.warn "[narrative_arc] create_arc failed: engine at capacity (#{Helpers::Constants::MAX_ARCS})"
18
+ return { success: false, reason: :engine_at_capacity }
19
+ end
20
+
21
+ Legion::Logging.debug "[narrative_arc] arc created: #{arc.arc_id[0..7]} title=#{title} domain=#{domain}"
22
+ { success: true, arc_id: arc.arc_id, title: arc.title, arc_phase: arc.arc_phase,
23
+ tension_level: arc.tension_level }
24
+ end
25
+
26
+ def add_beat(arc_id:, content:, intensity: 0.5, beat_type: :rising_action,
27
+ domain: :general, emotional_charge: 0.0, engine: nil, **)
28
+ eng = engine || arc_engine
29
+ result = eng.add_beat(
30
+ arc_id: arc_id,
31
+ content: content,
32
+ intensity: intensity,
33
+ beat_type: beat_type,
34
+ domain: domain,
35
+ emotional_charge: emotional_charge
36
+ )
37
+
38
+ if result[:success]
39
+ arc = eng.get_arc(arc_id)
40
+ Legion::Logging.debug "[narrative_arc] beat added: arc=#{arc_id[0..7]} type=#{beat_type} " \
41
+ "phase=#{result[:arc_phase]} tension=#{result[:tension_level].round(2)}"
42
+ result[:dramatic_score] = arc.dramatic_score if arc
43
+ else
44
+ Legion::Logging.debug "[narrative_arc] add_beat failed: #{result[:reason]} arc=#{arc_id[0..7]}"
45
+ end
46
+
47
+ result
48
+ end
49
+
50
+ def get_arc(arc_id:, engine: nil, **)
51
+ eng = engine || arc_engine
52
+ arc = eng.get_arc(arc_id)
53
+ return { found: false, arc_id: arc_id } unless arc
54
+
55
+ { found: true, arc: arc.to_h, beats: arc.beats.map(&:to_h) }
56
+ end
57
+
58
+ def active_arcs(engine: nil, **)
59
+ eng = engine || arc_engine
60
+ arcs = eng.active_arcs
61
+ Legion::Logging.debug "[narrative_arc] active arcs count=#{arcs.size}"
62
+ { arcs: arcs.map(&:to_h), count: arcs.size }
63
+ end
64
+
65
+ def completed_arcs(engine: nil, **)
66
+ eng = engine || arc_engine
67
+ arcs = eng.completed_arcs
68
+ Legion::Logging.debug "[narrative_arc] completed arcs count=#{arcs.size}"
69
+ { arcs: arcs.map(&:to_h), count: arcs.size }
70
+ end
71
+
72
+ def most_dramatic_arc(engine: nil, **)
73
+ eng = engine || arc_engine
74
+ arc = eng.most_dramatic_arc
75
+ return { found: false } unless arc
76
+
77
+ Legion::Logging.debug "[narrative_arc] most dramatic: #{arc.arc_id[0..7]} score=#{arc.dramatic_score.round(2)}"
78
+ { found: true, arc: arc.to_h }
79
+ end
80
+
81
+ def arc_report(engine: nil, **)
82
+ eng = engine || arc_engine
83
+ report = eng.arc_report
84
+ Legion::Logging.debug "[narrative_arc] arc_report total=#{report[:total_arcs]} patterns=#{report[:patterns].inspect}"
85
+ { success: true, report: report }
86
+ end
87
+
88
+ private
89
+
90
+ def arc_engine
91
+ @arc_engine ||= Helpers::ArcEngine.new
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveNarrativeArc
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/cognitive_narrative_arc/version'
4
+ require 'legion/extensions/cognitive_narrative_arc/helpers/constants'
5
+ require 'legion/extensions/cognitive_narrative_arc/helpers/beat_event'
6
+ require 'legion/extensions/cognitive_narrative_arc/helpers/arc'
7
+ require 'legion/extensions/cognitive_narrative_arc/helpers/arc_engine'
8
+ require 'legion/extensions/cognitive_narrative_arc/runners/narrative'
9
+
10
+ module Legion
11
+ module Extensions
12
+ module CognitiveNarrativeArc
13
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
14
+ end
15
+ end
16
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-cognitive-narrative-arc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Narrative arc detection in cognitive experience for LegionIO
27
+ email:
28
+ - matthewdiverson@gmail.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - ".github/workflows/ci.yml"
34
+ - ".gitignore"
35
+ - ".rspec"
36
+ - ".rubocop.yml"
37
+ - CLAUDE.md
38
+ - Gemfile
39
+ - README.md
40
+ - lex-cognitive-narrative-arc.gemspec
41
+ - lib/legion/extensions/cognitive_narrative_arc.rb
42
+ - lib/legion/extensions/cognitive_narrative_arc/client.rb
43
+ - lib/legion/extensions/cognitive_narrative_arc/helpers/arc.rb
44
+ - lib/legion/extensions/cognitive_narrative_arc/helpers/arc_engine.rb
45
+ - lib/legion/extensions/cognitive_narrative_arc/helpers/beat_event.rb
46
+ - lib/legion/extensions/cognitive_narrative_arc/helpers/constants.rb
47
+ - lib/legion/extensions/cognitive_narrative_arc/runners/narrative.rb
48
+ - lib/legion/extensions/cognitive_narrative_arc/version.rb
49
+ homepage: https://github.com/LegionIO/lex-cognitive-narrative-arc
50
+ licenses:
51
+ - MIT
52
+ metadata:
53
+ homepage_uri: https://github.com/LegionIO/lex-cognitive-narrative-arc
54
+ source_code_uri: https://github.com/LegionIO/lex-cognitive-narrative-arc
55
+ documentation_uri: https://github.com/LegionIO/lex-cognitive-narrative-arc
56
+ changelog_uri: https://github.com/LegionIO/lex-cognitive-narrative-arc
57
+ bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-narrative-arc/issues
58
+ rubygems_mfa_required: 'true'
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '3.4'
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubygems_version: 3.6.9
74
+ specification_version: 4
75
+ summary: LEX Cognitive Narrative Arc
76
+ test_files: []