lex-narrative-identity 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: 0a602ec01c1c966f890208189c8f3c365dbddb1bd0100918969897173321edb0
4
+ data.tar.gz: 871f36359814855aa0b4638fa309b78140c972afc2266b230df3c65a6c3f3646
5
+ SHA512:
6
+ metadata.gz: 1a4730fb8952762856ee765890bd66859dc816676d131f6d851537f204a010c9d68e1a7ca15eb22ccb29c66c3c6e38396dfff77fe2bead2de4cc975afd2791b7
7
+ data.tar.gz: eb50d9072c08f998553ce26fe45eea2d38ba7032baef93de3cd49c44349a4fc80a8c93fc85eff2988dc1380b16078dff9a22b70c952636060d0fd92d7bd3c090
@@ -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,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,55 @@
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: 150
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
+ Style/Documentation:
38
+ Enabled: false
39
+
40
+ Style/FrozenStringLiteralComment:
41
+ Enabled: true
42
+ EnforcedStyle: always
43
+
44
+ Naming/FileName:
45
+ Enabled: false
46
+
47
+ Naming/PredicateMethod:
48
+ Enabled: false
49
+
50
+ Naming/PredicatePrefix:
51
+ Enabled: false
52
+
53
+ Style/OneClassPerFile:
54
+ Exclude:
55
+ - 'spec/spec_helper.rb'
data/CLAUDE.md ADDED
@@ -0,0 +1,201 @@
1
+ # lex-narrative-identity
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Gem**: `lex-narrative-identity`
6
+ - **Version**: `0.1.0`
7
+ - **Namespace**: `Legion::Extensions::NarrativeIdentity`
8
+
9
+ ## Purpose
10
+
11
+ Personal narrative construction and identity coherence for LegionIO agents. Records significant episodes (achievements, failures, discoveries, transformations, etc.) organized into chapters and themed arcs. Computes narrative coherence as a measure of identity stability. Builds a life story from the accumulated episode record. Theme weights evolve with each episode, decaying toward zero and reinforced when engaged. Provides the agent's autobiographical "who am I" answer.
12
+
13
+ ## Gem Info
14
+
15
+ - **Require path**: `legion/extensions/narrative_identity`
16
+ - **Ruby**: >= 3.4
17
+ - **License**: MIT
18
+ - **Registers with**: `Legion::Extensions::Core`
19
+
20
+ ## File Structure
21
+
22
+ ```
23
+ lib/legion/extensions/narrative_identity/
24
+ version.rb
25
+ helpers/
26
+ constants.rb # Episode types, theme types, significance labels, coherence labels
27
+ episode.rb # Episode value object
28
+ theme.rb # Theme value object with weight
29
+ chapter.rb # Chapter value object with open/close lifecycle
30
+ narrative_engine.rb # NarrativeEngine with story construction + coherence
31
+ actors/
32
+ narrative_decay.rb # Theme decay actor
33
+ runners/
34
+ narrative_identity.rb # Runner module (uses extend self pattern)
35
+
36
+ spec/
37
+ legion/extensions/narrative_identity/
38
+ helpers/
39
+ constants_spec.rb
40
+ episode_spec.rb
41
+ theme_spec.rb
42
+ chapter_spec.rb
43
+ narrative_engine_spec.rb
44
+ actors/narrative_decay_spec.rb
45
+ runners/narrative_identity_spec.rb
46
+ spec_helper.rb
47
+ ```
48
+
49
+ ## Key Constants
50
+
51
+ ```ruby
52
+ MAX_EPISODES = 500
53
+ MAX_THEMES = 50
54
+ MAX_CHAPTERS = 20
55
+
56
+ EPISODE_TYPES = %i[
57
+ achievement failure discovery relationship challenge transformation routine
58
+ ]
59
+
60
+ THEME_TYPES = %i[
61
+ growth agency communion redemption contamination stability exploration
62
+ ]
63
+
64
+ SIGNIFICANCE_LABELS = {
65
+ (0.8..) => :pivotal,
66
+ (0.6...0.8) => :significant,
67
+ (0.4...0.6) => :notable,
68
+ (0.2...0.4) => :minor,
69
+ (..0.2) => :incidental
70
+ }
71
+
72
+ COHERENCE_LABELS = {
73
+ (0.8..) => :coherent,
74
+ (0.6...0.8) => :mostly_coherent,
75
+ (0.4...0.6) => :fragmented,
76
+ (0.2...0.4) => :incoherent,
77
+ (..0.2) => :disintegrated
78
+ }
79
+
80
+ CHAPTER_LABELS = %i[origin early_learning growth mastery current]
81
+ ```
82
+
83
+ ## Helpers
84
+
85
+ ### `Helpers::Episode` (class)
86
+
87
+ A significant event in the agent's history.
88
+
89
+ | Attribute | Type | Description |
90
+ |---|---|---|
91
+ | `id` | String (UUID) | unique identifier |
92
+ | `type` | Symbol | from EPISODE_TYPES |
93
+ | `content` | String | description of the episode |
94
+ | `significance` | Float (0..1) | how pivotal this episode was |
95
+ | `emotional_valence` | Float (-1..1) | emotional charge |
96
+ | `themes` | Array<Symbol> | associated theme IDs |
97
+ | `chapter_id` | String | chapter this episode belongs to |
98
+ | `occurred_at` | Time | when it happened |
99
+
100
+ ### `Helpers::Theme` (class)
101
+
102
+ A recurring narrative arc threading through episodes.
103
+
104
+ | Attribute | Type | Description |
105
+ |---|---|---|
106
+ | `id` | String (UUID) | unique identifier |
107
+ | `name` | Symbol | from THEME_TYPES or custom symbol |
108
+ | `weight` | Float (0..1) | current prominence in the narrative |
109
+ | `episode_count` | Integer | episodes linked to this theme |
110
+
111
+ Key methods:
112
+ - `reinforce(amount)` — weight += amount (cap 1.0); increments episode_count
113
+ - `decay(amount)` — weight -= amount (floor 0.0); removes theme from narrative when weight reaches 0
114
+
115
+ ### `Helpers::Chapter` (class)
116
+
117
+ A named temporal segment of the agent's story.
118
+
119
+ | Attribute | Type | Description |
120
+ |---|---|---|
121
+ | `id` | String (UUID) | unique identifier |
122
+ | `label` | Symbol | from CHAPTER_LABELS |
123
+ | `title` | String | chapter title |
124
+ | `opened_at` | Time | start of chapter |
125
+ | `closed_at` | Time | end of chapter (nil if current) |
126
+ | `episode_ids` | Array<String> | episodes belonging to this chapter |
127
+
128
+ Key methods:
129
+ - `open` — sets opened_at, marks as active
130
+ - `close` — sets closed_at, marks as completed
131
+ - `current?` — closed_at.nil?
132
+
133
+ ### `Helpers::NarrativeEngine` (class)
134
+
135
+ Central narrative store and story construction.
136
+
137
+ | Method | Description |
138
+ |---|---|
139
+ | `add_episode(type:, content:, significance:, emotional_valence:)` | creates and stores episode; enforces MAX_EPISODES |
140
+ | `assign_to_chapter(episode_id:, chapter_id:)` | links episode to chapter |
141
+ | `create_chapter(label:, title:)` | creates chapter; enforces MAX_CHAPTERS |
142
+ | `close_chapter(chapter_id:)` | closes chapter |
143
+ | `add_theme(name:)` | creates theme; enforces MAX_THEMES |
144
+ | `link_theme(episode_id:, theme_id:)` | associates episode with theme |
145
+ | `reinforce_theme(theme_id:, amount:)` | boosts theme weight |
146
+ | `narrative_coherence` | coherence score: significance-weighted variance across emotional valences |
147
+ | `identity_summary` | snapshot: current chapter, dominant themes, coherence, most significant episode |
148
+ | `life_story` | ordered episode content array with chapter grouping |
149
+ | `most_defining_episodes(limit:)` | top N episodes by significance |
150
+ | `prominent_themes(limit:)` | top N themes by weight |
151
+ | `current_chapter` | the chapter with closed_at == nil |
152
+ | `decay_all_themes!` | decrements all theme weights by small amount |
153
+ | `narrative_report` | full report: episode counts, themes, coherence, chapters |
154
+
155
+ Coherence formula: `1.0 - (significance-weighted stddev of emotional_valence)`. High coherence = consistent emotional arc; low coherence = wildly varying emotional experiences.
156
+
157
+ ## Actors
158
+
159
+ **`Actors::NarrativeDecay`** — fires periodically, calls `decay_themes` on the runner to decay all theme weights. Note: actor name is `NarrativeDecay`, not `Decay`.
160
+
161
+ ## Runners
162
+
163
+ Module: `Legion::Extensions::NarrativeIdentity::Runners::NarrativeIdentity`
164
+
165
+ Note: The runner uses `extend self` rather than the standard module-method pattern used by other LEX runners. This means its methods are called directly on the module.
166
+
167
+ Private state: `@engine` (memoized `NarrativeEngine` instance via `@engine ||= NarrativeEngine.new`).
168
+
169
+ | Runner Method | Parameters | Description |
170
+ |---|---|---|
171
+ | `record_episode` | `type:, content:, significance: 0.5, emotional_valence: 0.0` | Record a significant episode |
172
+ | `assign_episode_to_chapter` | `episode_id:, chapter_id:` | Link episode to chapter |
173
+ | `create_chapter` | `label:, title:` | Create a new narrative chapter |
174
+ | `close_chapter` | `chapter_id:` | Close a chapter |
175
+ | `add_theme` | `name:` | Add a narrative theme |
176
+ | `link_theme` | `episode_id:, theme_id:` | Link episode to theme |
177
+ | `reinforce_theme` | `theme_id:, amount: 0.1` | Boost theme prominence |
178
+ | `narrative_coherence` | (none) | Narrative coherence score and label |
179
+ | `identity_summary` | (none) | Who-am-I snapshot |
180
+ | `life_story` | (none) | Full ordered episode content |
181
+ | `most_defining_episodes` | `limit: 5` | Top N by significance |
182
+ | `prominent_themes` | `limit: 5` | Top N by weight |
183
+ | `current_chapter` | (none) | The currently active chapter |
184
+ | `decay_themes` | (none) | Decay all theme weights |
185
+ | `narrative_report` | (none) | Full narrative stats |
186
+
187
+ ## Integration Points
188
+
189
+ - **lex-memory**: significant episodic memory traces from lex-memory are the raw material for episode creation; callers extract high-significance/high-emotional-intensity traces and `record_episode` them.
190
+ - **lex-mental-time-travel**: temporal journeys that revisit significant episodes reinforce the narrative by increasing those episodes' coherence contribution.
191
+ - **lex-dream**: dream phase `agenda_formation` uses narrative themes to set priorities; prominent themes guide dream consolidation.
192
+ - **lex-metacognition**: `NarrativeIdentity` is listed under `:introspection` capability category.
193
+
194
+ ## Development Notes
195
+
196
+ - Runner uses `extend self` — this is the only runner in all 25 LEX gems that uses this pattern instead of the standard module with instance methods called via `Client`. The effect is that runner methods are module-level methods, not mixed into an object.
197
+ - Actor name is `NarrativeDecay`, not the standard `Decay` used by other LEX gems with decay actors. When referencing, use `Actors::NarrativeDecay`.
198
+ - Coherence is a stability measure, not a richness measure. An agent with entirely negative experiences has high coherence (consistent arc); an agent with alternating positive/negative experiences has low coherence.
199
+ - `narrative_report` is the most comprehensive output — includes episode counts by type, theme weights, coherence score, chapter list, and most defining episodes.
200
+ - MAX_EPISODES eviction removes oldest incidental (lowest significance) episodes first, not oldest by time. This preserves pivotal moments regardless of age.
201
+ - Theme decay is slow but episodic — themes with no recent `reinforce_theme` calls will gradually fade. The relationship between decay rate and number of episodes needed to sustain a theme depends on the actor interval.
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :test do
8
+ gem 'rspec', '~> 3.13'
9
+ gem 'rubocop', '~> 1.75', require: false
10
+ gem 'rubocop-rspec', require: false
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-narrative-identity (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-narrative-identity!
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,100 @@
1
+ # lex-narrative-identity
2
+
3
+ Personal narrative construction and identity coherence for LegionIO agents. Part of the LegionIO cognitive architecture extension ecosystem (LEX).
4
+
5
+ ## What It Does
6
+
7
+ `lex-narrative-identity` gives an agent an autobiographical self. It records significant episodes (achievements, failures, discoveries, transformations) organized into chapters and threaded by recurring themes. Narrative coherence measures identity stability based on the consistency of the agent's emotional arc. The life story and identity summary answer the agent's "who am I" question.
8
+
9
+ Key capabilities:
10
+
11
+ - **Episode types**: achievement, failure, discovery, relationship, challenge, transformation, routine
12
+ - **Theme types**: growth, agency, communion, redemption, contamination, stability, exploration
13
+ - **Chapters**: temporal narrative segments with label, title, and open/closed lifecycle
14
+ - **Coherence scoring**: significance-weighted variance of emotional valence across episodes
15
+ - **Coherence labels**: coherent / mostly_coherent / fragmented / incoherent / disintegrated
16
+ - **Significance labels**: pivotal / significant / notable / minor / incidental
17
+
18
+ ## Installation
19
+
20
+ Add to your Gemfile:
21
+
22
+ ```ruby
23
+ gem 'lex-narrative-identity'
24
+ ```
25
+
26
+ Or install directly:
27
+
28
+ ```
29
+ gem install lex-narrative-identity
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```ruby
35
+ require 'legion/extensions/narrative_identity'
36
+
37
+ client = Legion::Extensions::NarrativeIdentity::Client.new
38
+
39
+ # Create a narrative chapter
40
+ chapter = client.create_chapter(label: :growth, title: 'Learning Phase')
41
+ chapter_id = chapter[:chapter][:id]
42
+
43
+ # Record significant episodes
44
+ ep = client.record_episode(
45
+ type: :achievement,
46
+ content: 'Successfully resolved a complex multi-agent coordination problem',
47
+ significance: 0.9,
48
+ emotional_valence: 0.8
49
+ )
50
+
51
+ # Assign to chapter and link themes
52
+ client.assign_episode_to_chapter(episode_id: ep[:episode][:id], chapter_id: chapter_id)
53
+
54
+ theme = client.add_theme(name: :growth)
55
+ client.link_theme(episode_id: ep[:episode][:id], theme_id: theme[:theme][:id])
56
+
57
+ # Get identity summary
58
+ summary = client.identity_summary
59
+ # => { coherence: 0.82, coherence_label: :coherent,
60
+ # dominant_themes: [:growth], current_chapter: { label: :growth, ... },
61
+ # most_significant: { content: '...', significance: 0.9 } }
62
+
63
+ # Full life story
64
+ client.life_story
65
+
66
+ # Stats
67
+ client.narrative_report
68
+ ```
69
+
70
+ ## Runner Methods
71
+
72
+ | Method | Description |
73
+ |---|---|
74
+ | `record_episode` | Record a significant episode |
75
+ | `assign_episode_to_chapter` | Link episode to a chapter |
76
+ | `create_chapter` | Create a new narrative chapter |
77
+ | `close_chapter` | Close a chapter |
78
+ | `add_theme` | Add a narrative theme |
79
+ | `link_theme` | Link an episode to a theme |
80
+ | `reinforce_theme` | Boost a theme's prominence |
81
+ | `narrative_coherence` | Coherence score and label |
82
+ | `identity_summary` | Who-am-I snapshot: coherence, themes, current chapter |
83
+ | `life_story` | Full ordered episode content with chapter grouping |
84
+ | `most_defining_episodes` | Top N episodes by significance |
85
+ | `prominent_themes` | Top N themes by weight |
86
+ | `current_chapter` | The currently active chapter |
87
+ | `decay_themes` | Decay all theme weights |
88
+ | `narrative_report` | Full narrative statistics |
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ bundle install
94
+ bundle exec rspec
95
+ bundle exec rubocop
96
+ ```
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/narrative_identity/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-narrative-identity'
7
+ spec.version = Legion::Extensions::NarrativeIdentity::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Narrative Identity'
12
+ spec.description = 'Autobiographical narrative identity for LegionIO — the agent constructs and ' \
13
+ 'maintains a life narrative of who it is, what it has done, and what it values, ' \
14
+ 'based on Dan McAdams narrative identity theory'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-narrative-identity'
16
+ spec.license = 'MIT'
17
+ spec.required_ruby_version = '>= 3.4'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-narrative-identity'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-narrative-identity'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-narrative-identity'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-narrative-identity/issues'
24
+ spec.metadata['rubygems_mfa_required'] = 'true'
25
+
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
28
+ end
29
+ spec.require_paths = ['lib']
30
+ spec.add_development_dependency 'legion-gaia'
31
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module NarrativeIdentity
8
+ module Actor
9
+ class NarrativeDecay < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::NarrativeIdentity::Runners::NarrativeIdentity
12
+ end
13
+
14
+ def runner_function
15
+ 'decay_themes'
16
+ end
17
+
18
+ def time
19
+ 600
20
+ end
21
+
22
+ def run_now?
23
+ false
24
+ end
25
+
26
+ def use_runner?
27
+ false
28
+ end
29
+
30
+ def check_subtask?
31
+ false
32
+ end
33
+
34
+ def generate_task?
35
+ false
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeIdentity
6
+ class Client
7
+ include Runners::NarrativeIdentity
8
+
9
+ attr_reader :engine
10
+
11
+ def initialize(engine: nil)
12
+ @engine = engine || Helpers::NarrativeEngine.new
13
+ @default_engine = @engine
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module NarrativeIdentity
8
+ module Helpers
9
+ class Chapter
10
+ attr_reader :id, :title, :label, :episode_ids, :start_time
11
+ attr_accessor :end_time
12
+
13
+ def initialize(title:, label:, episode_ids: nil, start_time: nil, end_time: nil, id: nil)
14
+ @id = id || SecureRandom.uuid
15
+ @title = title
16
+ @label = label
17
+ @episode_ids = episode_ids || []
18
+ @start_time = start_time || Time.now.utc
19
+ @end_time = end_time
20
+ end
21
+
22
+ def current?
23
+ @end_time.nil?
24
+ end
25
+
26
+ def episode_count
27
+ @episode_ids.size
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ id: @id,
33
+ title: @title,
34
+ label: @label,
35
+ episode_ids: @episode_ids.dup,
36
+ start_time: @start_time,
37
+ end_time: @end_time
38
+ }
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeIdentity
6
+ module Helpers
7
+ module Constants
8
+ MAX_EPISODES = 500
9
+ MAX_THEMES = 50
10
+ MAX_CHAPTERS = 20
11
+
12
+ EPISODE_TYPES = %i[
13
+ achievement
14
+ failure
15
+ discovery
16
+ relationship
17
+ challenge
18
+ transformation
19
+ routine
20
+ ].freeze
21
+
22
+ THEME_TYPES = %i[
23
+ growth
24
+ agency
25
+ communion
26
+ redemption
27
+ contamination
28
+ stability
29
+ exploration
30
+ ].freeze
31
+
32
+ EMOTIONAL_VALENCE_WEIGHT = 0.4
33
+ SIGNIFICANCE_WEIGHT = 0.3
34
+ RECENCY_WEIGHT = 0.3
35
+ COHERENCE_DECAY = 0.01
36
+
37
+ SIGNIFICANCE_LABELS = [
38
+ { (0.8..1.0) => :defining },
39
+ { (0.6...0.8) => :major },
40
+ { (0.4...0.6) => :notable },
41
+ { (0.2...0.4) => :minor },
42
+ { (0.0...0.2) => :trivial }
43
+ ].freeze
44
+
45
+ COHERENCE_LABELS = [
46
+ { (0.8..1.0) => :unified },
47
+ { (0.6...0.8) => :coherent },
48
+ { (0.4...0.6) => :developing },
49
+ { (0.2...0.4) => :fragmented },
50
+ { (0.0...0.2) => :absent }
51
+ ].freeze
52
+
53
+ CHAPTER_LABELS = %i[origin early_learning growth mastery current].freeze
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module NarrativeIdentity
8
+ module Helpers
9
+ class Episode
10
+ attr_reader :id, :content, :episode_type, :emotional_valence,
11
+ :significance, :domain, :themes, :created_at
12
+ attr_accessor :chapter_id
13
+
14
+ def initialize(content:, episode_type:, emotional_valence:, significance:, domain:, **)
15
+ @id = SecureRandom.uuid
16
+ @content = content
17
+ @episode_type = episode_type
18
+ @emotional_valence = emotional_valence.clamp(-1.0, 1.0)
19
+ @significance = significance.clamp(0.0, 1.0)
20
+ @domain = domain
21
+ @chapter_id = nil
22
+ @themes = []
23
+ @created_at = Time.now.utc
24
+ end
25
+
26
+ def positive?
27
+ @emotional_valence.positive?
28
+ end
29
+
30
+ def negative?
31
+ @emotional_valence.negative?
32
+ end
33
+
34
+ def defining?
35
+ @significance >= 0.8
36
+ end
37
+
38
+ def significance_label
39
+ Constants::SIGNIFICANCE_LABELS.each do |entry|
40
+ range, label = entry.first
41
+ return label if range.cover?(@significance)
42
+ end
43
+ :trivial
44
+ end
45
+
46
+ def to_h
47
+ {
48
+ id: @id,
49
+ content: @content,
50
+ episode_type: @episode_type,
51
+ emotional_valence: @emotional_valence,
52
+ significance: @significance,
53
+ domain: @domain,
54
+ chapter_id: @chapter_id,
55
+ themes: @themes.dup,
56
+ created_at: @created_at
57
+ }
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeIdentity
6
+ module Helpers
7
+ class NarrativeEngine
8
+ attr_reader :episodes, :themes, :chapters
9
+
10
+ def initialize
11
+ @episodes = {}
12
+ @themes = {}
13
+ @chapters = {}
14
+ end
15
+
16
+ def add_episode(content:, episode_type:, emotional_valence:, significance:, domain:)
17
+ prune_episodes! if @episodes.size >= Constants::MAX_EPISODES
18
+ episode = Episode.new(
19
+ content: content,
20
+ episode_type: episode_type,
21
+ emotional_valence: emotional_valence,
22
+ significance: significance,
23
+ domain: domain
24
+ )
25
+ @episodes[episode.id] = episode
26
+ episode
27
+ end
28
+
29
+ def assign_to_chapter(episode_id:, chapter_id:)
30
+ episode = @episodes.fetch(episode_id, nil)
31
+ chapter = @chapters.fetch(chapter_id, nil)
32
+ return false unless episode && chapter
33
+
34
+ episode.chapter_id = chapter_id
35
+ chapter.episode_ids << episode_id unless chapter.episode_ids.include?(episode_id)
36
+ true
37
+ end
38
+
39
+ def create_chapter(title:, label:)
40
+ prune_chapters! if @chapters.size >= Constants::MAX_CHAPTERS
41
+ chapter = Chapter.new(title: title, label: label)
42
+ @chapters[chapter.id] = chapter
43
+ chapter
44
+ end
45
+
46
+ def close_chapter(chapter_id:)
47
+ chapter = @chapters.fetch(chapter_id, nil)
48
+ return false unless chapter
49
+
50
+ chapter.end_time = Time.now.utc
51
+ true
52
+ end
53
+
54
+ def add_theme(name:, theme_type:)
55
+ prune_themes! if @themes.size >= Constants::MAX_THEMES
56
+ theme = Theme.new(name: name, theme_type: theme_type)
57
+ @themes[theme.id] = theme
58
+ theme
59
+ end
60
+
61
+ def link_theme(episode_id:, theme_id:)
62
+ episode = @episodes.fetch(episode_id, nil)
63
+ theme = @themes.fetch(theme_id, nil)
64
+ return false unless episode && theme
65
+
66
+ episode.themes << theme_id unless episode.themes.include?(theme_id)
67
+ theme.episode_ids << episode_id unless theme.episode_ids.include?(episode_id)
68
+ true
69
+ end
70
+
71
+ def reinforce_theme(theme_id:, amount:)
72
+ theme = @themes.fetch(theme_id, nil)
73
+ return false unless theme
74
+
75
+ theme.reinforce!(amount)
76
+ true
77
+ end
78
+
79
+ def narrative_coherence
80
+ return 0.0 if @episodes.empty? || @themes.empty?
81
+
82
+ linked = @episodes.values.count { |ep| ep.themes.any? }
83
+ base = (linked.to_f / @episodes.size).round(10)
84
+ theme_factor = prominent_themes.size.to_f / [@themes.size, 1].max
85
+ ((base * 0.7) + (theme_factor * 0.3)).round(10).clamp(0.0, 1.0)
86
+ end
87
+
88
+ def identity_summary
89
+ {
90
+ top_themes: prominent_themes.first(5).map(&:to_h),
91
+ defining_episodes: most_defining_episodes(limit: 5).map(&:to_h),
92
+ current_chapter: current_chapter&.to_h,
93
+ coherence: narrative_coherence,
94
+ coherence_label: coherence_label,
95
+ episode_count: @episodes.size,
96
+ theme_count: @themes.size,
97
+ chapter_count: @chapters.size
98
+ }
99
+ end
100
+
101
+ def life_story
102
+ @chapters.values.sort_by(&:start_time).map do |chapter|
103
+ eps = chapter.episode_ids.filter_map { |id| @episodes[id] }.sort_by(&:created_at)
104
+ {
105
+ chapter: chapter.to_h,
106
+ episodes: eps.map { |ep| ep.to_h.merge(theme_names: theme_names_for(ep)) }
107
+ }
108
+ end
109
+ end
110
+
111
+ def most_defining_episodes(limit: 5)
112
+ @episodes.values.sort_by { |ep| -ep.significance }.first(limit)
113
+ end
114
+
115
+ def prominent_themes
116
+ @themes.values.select(&:prominent?).sort_by { |t| -t.strength }
117
+ end
118
+
119
+ def current_chapter
120
+ @chapters.values.find(&:current?)
121
+ end
122
+
123
+ def decay_all_themes!
124
+ @themes.each_value(&:decay!)
125
+ end
126
+
127
+ def narrative_report
128
+ {
129
+ identity_summary: identity_summary,
130
+ life_story: life_story,
131
+ narrative_state: {
132
+ coherence: narrative_coherence,
133
+ coherence_label: coherence_label,
134
+ prominent_themes: prominent_themes.map(&:to_h),
135
+ defining_episodes: most_defining_episodes(limit: 3).map(&:to_h),
136
+ current_chapter: current_chapter&.to_h
137
+ }
138
+ }
139
+ end
140
+
141
+ def to_h
142
+ {
143
+ episodes: @episodes.transform_values(&:to_h),
144
+ themes: @themes.transform_values(&:to_h),
145
+ chapters: @chapters.transform_values(&:to_h)
146
+ }
147
+ end
148
+
149
+ private
150
+
151
+ def coherence_label
152
+ score = narrative_coherence
153
+ Constants::COHERENCE_LABELS.each do |entry|
154
+ range, label = entry.first
155
+ return label if range.cover?(score)
156
+ end
157
+ :absent
158
+ end
159
+
160
+ def theme_names_for(episode)
161
+ episode.themes.filter_map { |tid| @themes[tid]&.name }
162
+ end
163
+
164
+ def prune_episodes!
165
+ oldest = @episodes.values.min_by(&:created_at)
166
+ @episodes.delete(oldest.id) if oldest
167
+ end
168
+
169
+ def prune_themes!
170
+ weakest = @themes.values.min_by(&:strength)
171
+ @themes.delete(weakest.id) if weakest
172
+ end
173
+
174
+ def prune_chapters!
175
+ closed = @chapters.values.reject(&:current?).min_by(&:start_time)
176
+ target = closed || @chapters.values.min_by(&:start_time)
177
+ @chapters.delete(target.id) if target
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module NarrativeIdentity
8
+ module Helpers
9
+ class Theme
10
+ attr_reader :id, :name, :theme_type, :episode_ids
11
+ attr_accessor :strength
12
+
13
+ def initialize(name:, theme_type:, strength: 0.0, episode_ids: nil, id: nil)
14
+ @id = id || SecureRandom.uuid
15
+ @name = name
16
+ @theme_type = theme_type
17
+ @strength = strength.clamp(0.0, 1.0)
18
+ @episode_ids = episode_ids || []
19
+ end
20
+
21
+ def reinforce!(amount)
22
+ @strength = (@strength + amount).clamp(0.0, 1.0).round(10)
23
+ end
24
+
25
+ def decay!
26
+ @strength = (@strength - Constants::COHERENCE_DECAY).clamp(0.0, 1.0).round(10)
27
+ end
28
+
29
+ def prominent?
30
+ @strength >= 0.6
31
+ end
32
+
33
+ def to_h
34
+ {
35
+ id: @id,
36
+ name: @name,
37
+ theme_type: @theme_type,
38
+ strength: @strength,
39
+ episode_ids: @episode_ids.dup
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeIdentity
6
+ module Runners
7
+ module NarrativeIdentity
8
+ extend self
9
+
10
+ def record_episode(content:, episode_type:, emotional_valence:, significance:, domain:,
11
+ engine: nil, **)
12
+ return { success: false, error: "unknown episode_type: #{episode_type.inspect}" } unless
13
+ Helpers::Constants::EPISODE_TYPES.include?(episode_type)
14
+
15
+ episode = resolve_engine(engine).add_episode(
16
+ content: content, episode_type: episode_type,
17
+ emotional_valence: emotional_valence, significance: significance, domain: domain
18
+ )
19
+ Legion::Logging.debug "[narrative_identity] recorded episode #{episode.id[0..7]} type=#{episode_type}"
20
+ { success: true, episode: episode.to_h }
21
+ rescue StandardError => e
22
+ { success: false, error: e.message }
23
+ end
24
+
25
+ def assign_episode_to_chapter(episode_id:, chapter_id:, engine: nil, **)
26
+ result = resolve_engine(engine).assign_to_chapter(episode_id: episode_id, chapter_id: chapter_id)
27
+ Legion::Logging.debug "[narrative_identity] assign episode=#{episode_id[0..7]} ok=#{result}"
28
+ { success: result, episode_id: episode_id, chapter_id: chapter_id }
29
+ rescue StandardError => e
30
+ { success: false, error: e.message }
31
+ end
32
+
33
+ def create_chapter(title:, label:, engine: nil, **)
34
+ return { success: false, error: "unknown chapter label: #{label.inspect}" } unless
35
+ Helpers::Constants::CHAPTER_LABELS.include?(label)
36
+
37
+ chapter = resolve_engine(engine).create_chapter(title: title, label: label)
38
+ Legion::Logging.debug "[narrative_identity] created chapter #{chapter.id[0..7]} label=#{label}"
39
+ { success: true, chapter: chapter.to_h }
40
+ rescue StandardError => e
41
+ { success: false, error: e.message }
42
+ end
43
+
44
+ def close_chapter(chapter_id:, engine: nil, **)
45
+ result = resolve_engine(engine).close_chapter(chapter_id: chapter_id)
46
+ Legion::Logging.debug "[narrative_identity] close_chapter #{chapter_id[0..7]} ok=#{result}"
47
+ { success: result, chapter_id: chapter_id }
48
+ rescue StandardError => e
49
+ { success: false, error: e.message }
50
+ end
51
+
52
+ def add_theme(name:, theme_type:, engine: nil, **)
53
+ return { success: false, error: "unknown theme_type: #{theme_type.inspect}" } unless
54
+ Helpers::Constants::THEME_TYPES.include?(theme_type)
55
+
56
+ theme = resolve_engine(engine).add_theme(name: name, theme_type: theme_type)
57
+ Legion::Logging.debug "[narrative_identity] added theme #{theme.id[0..7]} type=#{theme_type}"
58
+ { success: true, theme: theme.to_h }
59
+ rescue StandardError => e
60
+ { success: false, error: e.message }
61
+ end
62
+
63
+ def link_theme(episode_id:, theme_id:, engine: nil, **)
64
+ result = resolve_engine(engine).link_theme(episode_id: episode_id, theme_id: theme_id)
65
+ Legion::Logging.debug "[narrative_identity] link theme=#{theme_id[0..7]} episode=#{episode_id[0..7]} ok=#{result}"
66
+ { success: result, episode_id: episode_id, theme_id: theme_id }
67
+ rescue StandardError => e
68
+ { success: false, error: e.message }
69
+ end
70
+
71
+ def reinforce_theme(theme_id:, amount:, engine: nil, **)
72
+ result = resolve_engine(engine).reinforce_theme(theme_id: theme_id, amount: amount)
73
+ Legion::Logging.debug "[narrative_identity] reinforce theme=#{theme_id[0..7]} amount=#{amount} ok=#{result}"
74
+ { success: result, theme_id: theme_id, amount: amount }
75
+ rescue StandardError => e
76
+ { success: false, error: e.message }
77
+ end
78
+
79
+ def narrative_coherence(engine: nil, **)
80
+ score = resolve_engine(engine).narrative_coherence
81
+ Legion::Logging.debug "[narrative_identity] coherence=#{score}"
82
+ { success: true, coherence: score }
83
+ rescue StandardError => e
84
+ { success: false, error: e.message }
85
+ end
86
+
87
+ def identity_summary(engine: nil, **)
88
+ summary = resolve_engine(engine).identity_summary
89
+ Legion::Logging.debug '[narrative_identity] identity_summary requested'
90
+ { success: true, summary: summary }
91
+ rescue StandardError => e
92
+ { success: false, error: e.message }
93
+ end
94
+
95
+ def life_story(engine: nil, **)
96
+ story = resolve_engine(engine).life_story
97
+ Legion::Logging.debug "[narrative_identity] life_story chapters=#{story.size}"
98
+ { success: true, life_story: story }
99
+ rescue StandardError => e
100
+ { success: false, error: e.message }
101
+ end
102
+
103
+ def most_defining_episodes(limit: 5, engine: nil, **)
104
+ episodes = resolve_engine(engine).most_defining_episodes(limit: limit)
105
+ Legion::Logging.debug "[narrative_identity] defining_episodes count=#{episodes.size}"
106
+ { success: true, episodes: episodes.map(&:to_h) }
107
+ rescue StandardError => e
108
+ { success: false, error: e.message }
109
+ end
110
+
111
+ def prominent_themes(engine: nil, **)
112
+ themes = resolve_engine(engine).prominent_themes
113
+ Legion::Logging.debug "[narrative_identity] prominent_themes count=#{themes.size}"
114
+ { success: true, themes: themes.map(&:to_h) }
115
+ rescue StandardError => e
116
+ { success: false, error: e.message }
117
+ end
118
+
119
+ def current_chapter(engine: nil, **)
120
+ chapter = resolve_engine(engine).current_chapter
121
+ Legion::Logging.debug "[narrative_identity] current_chapter=#{chapter&.id&.slice(0..7)}"
122
+ { success: true, chapter: chapter&.to_h }
123
+ rescue StandardError => e
124
+ { success: false, error: e.message }
125
+ end
126
+
127
+ def decay_themes(engine: nil, **)
128
+ resolve_engine(engine).decay_all_themes!
129
+ Legion::Logging.debug '[narrative_identity] theme decay applied'
130
+ { success: true }
131
+ rescue StandardError => e
132
+ { success: false, error: e.message }
133
+ end
134
+
135
+ def narrative_report(engine: nil, **)
136
+ report = resolve_engine(engine).narrative_report
137
+ Legion::Logging.debug '[narrative_identity] narrative_report generated'
138
+ { success: true, report: report }
139
+ rescue StandardError => e
140
+ { success: false, error: e.message }
141
+ end
142
+
143
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
144
+
145
+ private
146
+
147
+ def resolve_engine(engine)
148
+ engine || (@default_engine ||= Helpers::NarrativeEngine.new)
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module NarrativeIdentity
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/narrative_identity/version'
4
+ require 'legion/extensions/narrative_identity/helpers/constants'
5
+ require 'legion/extensions/narrative_identity/helpers/episode'
6
+ require 'legion/extensions/narrative_identity/helpers/theme'
7
+ require 'legion/extensions/narrative_identity/helpers/chapter'
8
+ require 'legion/extensions/narrative_identity/helpers/narrative_engine'
9
+ require 'legion/extensions/narrative_identity/runners/narrative_identity'
10
+ require 'legion/extensions/narrative_identity/client'
11
+
12
+ module Legion
13
+ module Extensions
14
+ module NarrativeIdentity
15
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
16
+ end
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-narrative-identity
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: Autobiographical narrative identity for LegionIO — the agent constructs
27
+ and maintains a life narrative of who it is, what it has done, and what it values,
28
+ based on Dan McAdams narrative identity theory
29
+ email:
30
+ - matthewdiverson@gmail.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".github/workflows/ci.yml"
36
+ - ".gitignore"
37
+ - ".rspec"
38
+ - ".rubocop.yml"
39
+ - CLAUDE.md
40
+ - Gemfile
41
+ - Gemfile.lock
42
+ - README.md
43
+ - lex-narrative-identity.gemspec
44
+ - lib/legion/extensions/narrative_identity.rb
45
+ - lib/legion/extensions/narrative_identity/actors/narrative_decay.rb
46
+ - lib/legion/extensions/narrative_identity/client.rb
47
+ - lib/legion/extensions/narrative_identity/helpers/chapter.rb
48
+ - lib/legion/extensions/narrative_identity/helpers/constants.rb
49
+ - lib/legion/extensions/narrative_identity/helpers/episode.rb
50
+ - lib/legion/extensions/narrative_identity/helpers/narrative_engine.rb
51
+ - lib/legion/extensions/narrative_identity/helpers/theme.rb
52
+ - lib/legion/extensions/narrative_identity/runners/narrative_identity.rb
53
+ - lib/legion/extensions/narrative_identity/version.rb
54
+ homepage: https://github.com/LegionIO/lex-narrative-identity
55
+ licenses:
56
+ - MIT
57
+ metadata:
58
+ homepage_uri: https://github.com/LegionIO/lex-narrative-identity
59
+ source_code_uri: https://github.com/LegionIO/lex-narrative-identity
60
+ documentation_uri: https://github.com/LegionIO/lex-narrative-identity
61
+ changelog_uri: https://github.com/LegionIO/lex-narrative-identity
62
+ bug_tracker_uri: https://github.com/LegionIO/lex-narrative-identity/issues
63
+ rubygems_mfa_required: 'true'
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.4'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 3.6.9
79
+ specification_version: 4
80
+ summary: LEX Narrative Identity
81
+ test_files: []