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 +7 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +64 -0
- data/CLAUDE.md +100 -0
- data/Gemfile +11 -0
- data/README.md +50 -0
- data/lex-cognitive-narrative-arc.gemspec +29 -0
- data/lib/legion/extensions/cognitive_narrative_arc/client.rb +25 -0
- data/lib/legion/extensions/cognitive_narrative_arc/helpers/arc.rb +133 -0
- data/lib/legion/extensions/cognitive_narrative_arc/helpers/arc_engine.rb +115 -0
- data/lib/legion/extensions/cognitive_narrative_arc/helpers/beat_event.rb +55 -0
- data/lib/legion/extensions/cognitive_narrative_arc/helpers/constants.rb +62 -0
- data/lib/legion/extensions/cognitive_narrative_arc/runners/narrative.rb +97 -0
- data/lib/legion/extensions/cognitive_narrative_arc/version.rb +9 -0
- data/lib/legion/extensions/cognitive_narrative_arc.rb +16 -0
- metadata +76 -0
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
data/.rspec
ADDED
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
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,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: []
|