lex-cognitive-tectonics 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: 6beef28cd3264b3978f5e982e00fca770a6b80b5d63a29d8155b5208ae8d399a
4
+ data.tar.gz: 9a9c401d00654e2473066da9be054972920e2f257cda552d18f1a1c51fef03bb
5
+ SHA512:
6
+ metadata.gz: 10e34cf80558cac6f3a5fe4e06c05bc7153b68d2501145451f592964d8902d84e1c383fcbe894fd05daea10093da39cf9fe4ce015dc1191b4d901a282a221de2
7
+ data.tar.gz: 4afbe3e1eb7bcc4360f8e5609de40fc58bd597347189a4d6945a299b298f58dfd93bcca364aad017e416dbe533dc10b38b3d0b362af822fa7d77cd43e82b24af
@@ -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
+ Gemfile.lock
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ --format documentation
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: 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
+ Metrics/CyclomaticComplexity:
38
+ Max: 15
39
+
40
+ Metrics/PerceivedComplexity:
41
+ Max: 17
42
+
43
+ Style/Documentation:
44
+ Enabled: false
45
+
46
+ Style/SymbolArray:
47
+ Enabled: true
48
+
49
+ Style/FrozenStringLiteralComment:
50
+ Enabled: true
51
+ EnforcedStyle: always
52
+
53
+ Style/OneClassPerFile:
54
+ Exclude:
55
+ - 'spec/spec_helper.rb'
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,137 @@
1
+ # lex-cognitive-tectonics
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Gem**: `lex-cognitive-tectonics`
6
+ - **Version**: 0.1.0
7
+ - **Namespace**: `Legion::Extensions::CognitiveTectonics`
8
+
9
+ ## Purpose
10
+
11
+ Models belief revision as plate tectonics. Cognitive beliefs are represented as plates with mass, position, and drift vectors in a 2D space. Plates drift over time, collide, and interact via three boundary types: convergent (beliefs merge), divergent (beliefs split and drift apart), or transform (friction accumulates stress that eventually triggers earthquakes). Seismic events model sudden, cascading belief shifts. A periodic `DriftTick` actor moves all plates each tick.
12
+
13
+ ## Gem Info
14
+
15
+ - **Gemspec**: `lex-cognitive-tectonics.gemspec`
16
+ - **Require**: `lex-cognitive-tectonics`
17
+ - **Ruby**: >= 3.4
18
+ - **License**: MIT
19
+ - **Homepage**: https://github.com/LegionIO/lex-cognitive-tectonics
20
+
21
+ ## File Structure
22
+
23
+ ```
24
+ lib/legion/extensions/cognitive_tectonics/
25
+ version.rb
26
+ helpers/
27
+ constants.rb # Boundary types, magnitude labels, stress/collision thresholds
28
+ belief_plate.rb # BeliefPlate class — one belief with position, mass, stress
29
+ seismic_event.rb # SeismicEvent class — earthquake/tremor/aftershock record
30
+ tectonic_engine.rb # TectonicBoundaries module + TectonicEngine class
31
+ runners/
32
+ cognitive_tectonics.rb # Runner module — public API (extend self)
33
+ actors/
34
+ drift_tick.rb # Actor::DriftTick — fires drift_tick every 60s
35
+ client.rb
36
+ ```
37
+
38
+ ## Key Constants
39
+
40
+ | Constant | Value | Meaning |
41
+ |---|---|---|
42
+ | `MAX_PLATES` | 50 | Hard cap on belief plates (raises if exceeded) |
43
+ | `MAX_QUAKES` | 200 | Seismic history ring size |
44
+ | `COLLISION_THRESHOLD` | 0.2 | Distance below which two plates collide |
45
+ | `SUBDUCTION_RATIO` | 0.7 | Mass below which a plate is subductable |
46
+ | `AFTERSHOCK_DECAY` | 0.3 | Magnitude multiplier reduction for aftershocks |
47
+ | `STRESS_QUAKE_TRIGGER` | 1.0 | Stress accumulation that auto-triggers earthquake |
48
+ | `MIN_DRIFT_RATE` | 0.001 | Minimum allowed drift component (reference) |
49
+ | `MAX_DRIFT_RATE` | 0.05 | Maximum allowed drift component (reference) |
50
+
51
+ `BOUNDARY_TYPES`: `[:convergent, :divergent, :transform]`
52
+
53
+ `PLATE_STATES`: `[:active, :subducted, :dormant]`
54
+
55
+ Magnitude labels: `[0,1)` = `:micro`, `[1,2)` = `:minor`, `[2,3)` = `:light`, `[3,4)` = `:moderate`, `[4,5)` = `:strong`, `5+` = `:great`
56
+
57
+ ## Key Classes
58
+
59
+ ### `Helpers::BeliefPlate`
60
+
61
+ One cognitive belief with spatial position, mass, drift, and stress.
62
+
63
+ - `drift!(delta_t)` — advances position by `drift_vector * delta_t`; only active plates drift
64
+ - `accumulate_stress!(amount)` — adds to `@stress_accumulation`
65
+ - `release_stress!` — resets stress to 0.0; returns the released amount
66
+ - `subducted?` — mass < `SUBDUCTION_RATIO`
67
+ - `subduct!` / `dormant!` — transitions state
68
+ - `active?` — `state == :active`
69
+ - `distance_to(other_plate)` — Euclidean distance between positions
70
+ - Position is initialized with random `x,y` in `[-10.0, 10.0]` if not provided
71
+
72
+ ### `Helpers::SeismicEvent`
73
+
74
+ A seismic event record for the history log.
75
+
76
+ - `EVENT_TYPES`: `[:earthquake, :tremor, :aftershock]`
77
+ - `label` — magnitude label from `Constants#label_for`
78
+ - `aftershock?` — type == `:aftershock`
79
+ - Fields: `id`, `type`, `magnitude`, `epicenter_plate_id`, `affected_plate_ids`, `parent_event_id`, `timestamp`
80
+
81
+ ### `Helpers::TectonicBoundaries` (module)
82
+
83
+ Private collision resolution methods mixed into `TectonicEngine`.
84
+
85
+ - `resolve_convergent` — averages mass and drift vectors; outcome `:merged`
86
+ - `resolve_divergent` — halves mass; reverses x-drift for A, y-drift for B; outcome `:split`
87
+ - `resolve_transform` — adds `mass_a * mass_b * 0.5` stress to both; calls `check_stress_quake`; outcome `:friction`
88
+ - `check_stress_quake` — auto-triggers earthquake if plate stress >= `STRESS_QUAKE_TRIGGER`
89
+
90
+ ### `Helpers::TectonicEngine`
91
+
92
+ Registry and event processing.
93
+
94
+ - `create_plate(domain:, content:, mass:, drift_vector:, position:)` — raises if at `MAX_PLATES`
95
+ - `drift_tick!(delta_t)` — drifts all active plates; detects collisions afterward
96
+ - `detect_collisions` — all active plate pairs with distance < `COLLISION_THRESHOLD`
97
+ - `resolve_collision(plate_a_id:, plate_b_id:, boundary_type:)` — dispatches to `TectonicBoundaries`; updates fault registry
98
+ - `subduct(weaker_plate_id:, stronger_plate_id:)` — absorbs 50% of weaker plate's mass into stronger; marks weaker as subducted
99
+ - `trigger_earthquake(plate_id:, magnitude:)` — releases stress; propagates 30% magnitude to nearby plates (< 5.0 distance)
100
+ - `aftershock_cascade(event_id:)` — creates aftershock at `magnitude * (1 - AFTERSHOCK_DECAY)`
101
+ - `tectonic_report` — aggregate with plate counts, high-stress count, recent quakes
102
+
103
+ ## Runners
104
+
105
+ Module: `Legion::Extensions::CognitiveTectonics::Runners::CognitiveTectonics` (uses `extend self`)
106
+
107
+ | Runner | Key Args | Returns |
108
+ |---|---|---|
109
+ | `create_plate` | `domain:`, `content:`, `mass:`, `drift_vector:`, `position:` | `{ success:, plate_id:, plate: }` |
110
+ | `drift_tick` | `delta_t:` | `{ success:, plates_moved:, collisions_detected:, collisions: }` |
111
+ | `resolve_collision` | `plate_a_id:`, `plate_b_id:`, `boundary_type:` | boundary-specific result hash |
112
+ | `trigger_earthquake` | `plate_id:`, `magnitude:` | `{ success:, event_id:, event: }` |
113
+ | `tectonic_status` | — | `{ success:, total_plates:, active_plates:, high_stress_count:, recent_quakes:, ... }` |
114
+
115
+ ## Actors
116
+
117
+ `Actor::DriftTick` — extends `Legion::Extensions::Actors::Every`
118
+
119
+ - Fires `drift_tick` every **60 seconds**
120
+ - `run_now?: false`, `use_runner?: false`, `check_subtask?: false`, `generate_task?: false`
121
+ - Advances all plate positions and surfaces new collisions each minute
122
+
123
+ ## Integration Points
124
+
125
+ - `drift_tick` is called automatically every 60s by `Actor::DriftTick`
126
+ - Can be triggered manually via runner for faster simulation
127
+ - Collision detection returns plate ID pairs — caller decides the boundary type for `resolve_collision`
128
+ - Transform boundary is the only type that accumulates stress; high-stress states can be read via `tectonic_report`
129
+ - All state is in-memory per `TectonicEngine` instance; reset is not built in (replace `@tectonic_engine`)
130
+
131
+ ## Development Notes
132
+
133
+ - `MIN_DRIFT_RATE` and `MAX_DRIFT_RATE` are defined but not enforced; drift components in `drift_vector` are caller-specified
134
+ - `nearby_plates` uses a fixed radius of 5.0 (hardcoded in `TectonicBoundaries`)
135
+ - The runner raises `ArgumentError` directly for missing required args, then rescues it into `{ success: false, error: }`
136
+ - Fault registry (`@active_faults`) is a plain array of hashes, not indexed — O(n) for lookup
137
+ - Seismic history is a ring buffer capped at `MAX_QUAKES`: `@seismic_history.shift if size > MAX_QUAKES`
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/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # lex-cognitive-tectonics
2
+
3
+ A LegionIO cognitive architecture extension that models belief revision as plate tectonics. Beliefs drift through conceptual space, collide, and interact via convergent, divergent, or transform boundaries. Stress accumulates and releases as earthquakes — sudden, cascading belief shifts.
4
+
5
+ ## What It Does
6
+
7
+ Manages **belief plates** — cognitive beliefs with position, mass, and drift velocity in a 2D space. A background actor moves all plates every 60 seconds. When plates collide, the caller resolves the interaction by choosing a boundary type:
8
+
9
+ - **Convergent**: beliefs merge; masses and drifts average together
10
+ - **Divergent**: beliefs split and drift apart; mass halves
11
+ - **Transform**: friction builds stress; at threshold stress triggers an earthquake
12
+
13
+ Earthquakes release stress and propagate to nearby plates.
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ require 'lex-cognitive-tectonics'
19
+
20
+ client = Legion::Extensions::CognitiveTectonics::Client.new
21
+
22
+ # Create belief plates
23
+ r1 = client.create_plate(domain: :ethics, content: 'harm prevention is primary', mass: 0.8,
24
+ drift_vector: { x: 0.01, y: 0.0 })
25
+ # => { success: true, plate_id: "uuid...", plate: { mass: 0.8, state: :active, position: {...}, ... } }
26
+
27
+ r2 = client.create_plate(domain: :ethics, content: 'autonomy is primary', mass: 0.7,
28
+ drift_vector: { x: -0.01, y: 0.0 })
29
+
30
+ # Advance one tick (also fires automatically every 60s via Actor::DriftTick)
31
+ client.drift_tick(delta_t: 1.0)
32
+ # => { success: true, plates_moved: 2, collisions_detected: 0, collisions: [] }
33
+
34
+ # If plates collide, resolve the interaction
35
+ # (plates with distance < 0.2 are flagged as collisions)
36
+ client.resolve_collision(
37
+ plate_a_id: r1[:plate_id],
38
+ plate_b_id: r2[:plate_id],
39
+ boundary_type: :transform
40
+ )
41
+ # => { success: true, boundary_type: :transform, outcome: :friction, stress_added: 0.28 }
42
+
43
+ # Manually trigger an earthquake at a plate
44
+ client.trigger_earthquake(plate_id: r1[:plate_id], magnitude: 2.5)
45
+ # => { success: true, event_id: "uuid...", event: { type: :earthquake, magnitude: 2.5, label: :light, ... } }
46
+
47
+ # System report
48
+ client.tectonic_status
49
+ # => { success: true, total_plates: 2, active_plates: 2, high_stress_count: 0, seismic_events: 1, ... }
50
+ ```
51
+
52
+ ## Development
53
+
54
+ ```bash
55
+ bundle install
56
+ bundle exec rspec
57
+ bundle exec rubocop
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_tectonics/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-tectonics'
7
+ spec.version = Legion::Extensions::CognitiveTectonics::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Tectonics'
12
+ spec.description = 'Tectonic belief-plate model for LegionIO — conviction as mass, drift vectors, ' \
13
+ 'convergent/divergent/transform boundaries, seismic belief shifts, and aftershock cascades'
14
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-tectonics'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>= 3.4'
17
+
18
+ spec.metadata['homepage_uri'] = spec.homepage
19
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-cognitive-tectonics'
20
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-tectonics'
21
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-tectonics'
22
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-tectonics/issues'
23
+ spec.metadata['rubygems_mfa_required'] = 'true'
24
+
25
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
26
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
27
+ end
28
+ spec.require_paths = ['lib']
29
+ spec.add_development_dependency 'legion-gaia'
30
+ 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 CognitiveTectonics
8
+ module Actor
9
+ class DriftTick < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::CognitiveTectonics::Runners::CognitiveTectonics
12
+ end
13
+
14
+ def runner_function
15
+ 'drift_tick'
16
+ end
17
+
18
+ def time
19
+ 60
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 CognitiveTectonics
6
+ class Client
7
+ include Runners::CognitiveTectonics
8
+
9
+ attr_reader :engine
10
+
11
+ def initialize(engine: nil, **)
12
+ @engine = engine || Helpers::TectonicEngine.new
13
+ @tectonic_engine = @engine
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveTectonics
8
+ module Helpers
9
+ class BeliefPlate
10
+ include Constants
11
+
12
+ attr_reader :id, :domain, :content, :created_at, :state
13
+ attr_accessor :mass, :drift_vector, :position, :velocity, :stress_accumulation
14
+
15
+ def initialize(domain:, content:, mass: 0.5, drift_vector: nil, position: nil, **)
16
+ @id = SecureRandom.uuid
17
+ @domain = domain
18
+ @content = content
19
+ @mass = mass.clamp(0.0, 1.0)
20
+ @drift_vector = drift_vector || { x: 0.0, y: 0.0 }
21
+ @position = position || { x: rand(-10.0..10.0).round(10), y: rand(-10.0..10.0).round(10) }
22
+ @velocity = { x: 0.0, y: 0.0 }
23
+ @stress_accumulation = 0.0
24
+ @state = :active
25
+ @created_at = Time.now.utc
26
+ end
27
+
28
+ def drift!(delta_t = 1.0)
29
+ return if @state != :active
30
+
31
+ @position[:x] = (@position[:x] + (@drift_vector.fetch(:x, 0.0) * delta_t)).round(10)
32
+ @position[:y] = (@position[:y] + (@drift_vector.fetch(:y, 0.0) * delta_t)).round(10)
33
+ end
34
+
35
+ def accumulate_stress!(amount)
36
+ @stress_accumulation = (@stress_accumulation + amount.abs).round(10)
37
+ end
38
+
39
+ def release_stress!
40
+ released = @stress_accumulation
41
+ @stress_accumulation = 0.0
42
+ released
43
+ end
44
+
45
+ def subducted?
46
+ @mass < Constants::SUBDUCTION_RATIO
47
+ end
48
+
49
+ def subduct!
50
+ @state = :subducted
51
+ end
52
+
53
+ def dormant!
54
+ @state = :dormant
55
+ end
56
+
57
+ def active?
58
+ @state == :active
59
+ end
60
+
61
+ def distance_to(other_plate)
62
+ dx = @position[:x] - other_plate.position[:x]
63
+ dy = @position[:y] - other_plate.position[:y]
64
+ Math.sqrt((dx**2) + (dy**2)).round(10)
65
+ end
66
+
67
+ def to_h
68
+ {
69
+ id: @id,
70
+ domain: @domain,
71
+ content: @content,
72
+ mass: @mass,
73
+ drift_vector: @drift_vector,
74
+ position: @position,
75
+ velocity: @velocity,
76
+ stress_accumulation: @stress_accumulation,
77
+ state: @state,
78
+ created_at: @created_at
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTectonics
6
+ module Helpers
7
+ module Constants
8
+ MAX_PLATES = 50
9
+ MAX_QUAKES = 200
10
+ BOUNDARY_TYPES = %i[convergent divergent transform].freeze
11
+ MIN_DRIFT_RATE = 0.001
12
+ MAX_DRIFT_RATE = 0.05
13
+ COLLISION_THRESHOLD = 0.2
14
+ SUBDUCTION_RATIO = 0.7
15
+ AFTERSHOCK_DECAY = 0.3
16
+ STRESS_QUAKE_TRIGGER = 1.0
17
+
18
+ PLATE_STATES = %i[active subducted dormant].freeze
19
+
20
+ MAGNITUDE_LABELS = {
21
+ (0.0...1.0) => :micro,
22
+ (1.0...2.0) => :minor,
23
+ (2.0...3.0) => :light,
24
+ (3.0...4.0) => :moderate,
25
+ (4.0...5.0) => :strong,
26
+ (5.0...Float::INFINITY) => :great
27
+ }.freeze
28
+
29
+ def label_for(magnitude)
30
+ MAGNITUDE_LABELS.find { |range, _| range.cover?(magnitude) }&.last || :unknown
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module CognitiveTectonics
8
+ module Helpers
9
+ class SeismicEvent
10
+ include Constants
11
+
12
+ EVENT_TYPES = %i[earthquake tremor aftershock].freeze
13
+
14
+ attr_reader :id, :type, :magnitude, :epicenter_plate_id,
15
+ :affected_plate_ids, :timestamp, :parent_event_id
16
+
17
+ def initialize(type:, magnitude:, epicenter_plate_id:, affected_plate_ids: [], parent_event_id: nil, **)
18
+ raise ArgumentError, "unknown event type: #{type.inspect}" unless EVENT_TYPES.include?(type)
19
+
20
+ @id = SecureRandom.uuid
21
+ @type = type
22
+ @magnitude = magnitude.clamp(0.0, Float::INFINITY)
23
+ @epicenter_plate_id = epicenter_plate_id
24
+ @affected_plate_ids = Array(affected_plate_ids)
25
+ @parent_event_id = parent_event_id
26
+ @timestamp = Time.now.utc
27
+ end
28
+
29
+ def aftershock?
30
+ @type == :aftershock
31
+ end
32
+
33
+ def label
34
+ label_for(@magnitude)
35
+ end
36
+
37
+ def to_h
38
+ {
39
+ id: @id,
40
+ type: @type,
41
+ magnitude: @magnitude,
42
+ label: label,
43
+ epicenter_plate_id: @epicenter_plate_id,
44
+ affected_plate_ids: @affected_plate_ids,
45
+ parent_event_id: @parent_event_id,
46
+ timestamp: @timestamp
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTectonics
6
+ module Helpers
7
+ module TectonicBoundaries
8
+ private
9
+
10
+ def resolve_convergent(plate_a, plate_b)
11
+ combined = ((plate_a.mass + plate_b.mass) / 2.0).clamp(0.0, 1.0)
12
+ plate_a.mass = combined
13
+ plate_b.mass = combined
14
+ avg_drift = {
15
+ x: ((plate_a.drift_vector[:x] + plate_b.drift_vector[:x]) / 2.0).round(10),
16
+ y: ((plate_a.drift_vector[:y] + plate_b.drift_vector[:y]) / 2.0).round(10)
17
+ }
18
+ plate_a.drift_vector = avg_drift
19
+ plate_b.drift_vector = avg_drift
20
+ { success: true, boundary_type: :convergent, outcome: :merged, new_mass: combined }
21
+ end
22
+
23
+ def resolve_divergent(plate_a, plate_b)
24
+ split_mass = (plate_a.mass * 0.5).round(10)
25
+ plate_a.mass = split_mass
26
+ plate_b.mass = split_mass
27
+ plate_a.drift_vector = { x: -plate_a.drift_vector.fetch(:x, 0.0), y: plate_a.drift_vector.fetch(:y, 0.0) }
28
+ plate_b.drift_vector = { x: plate_b.drift_vector.fetch(:x, 0.0), y: -plate_b.drift_vector.fetch(:y, 0.0) }
29
+ { success: true, boundary_type: :divergent, outcome: :split, new_mass: split_mass }
30
+ end
31
+
32
+ def resolve_transform(plate_a, plate_b)
33
+ stress = (plate_a.mass * plate_b.mass * 0.5).round(10)
34
+ plate_a.accumulate_stress!(stress)
35
+ plate_b.accumulate_stress!(stress)
36
+ check_stress_quake(plate_a)
37
+ check_stress_quake(plate_b)
38
+ { success: true, boundary_type: :transform, outcome: :friction, stress_added: stress }
39
+ end
40
+
41
+ def check_stress_quake(plate)
42
+ return unless plate.stress_accumulation >= Constants::STRESS_QUAKE_TRIGGER
43
+
44
+ trigger_earthquake(plate_id: plate.id, magnitude: plate.stress_accumulation)
45
+ end
46
+
47
+ def nearby_plates(plate, exclude_id:)
48
+ @plates.values.select do |p|
49
+ p.active? && p.id != exclude_id && plate.distance_to(p) < 5.0
50
+ end
51
+ end
52
+
53
+ def record_seismic_event(event)
54
+ @seismic_history << event
55
+ @seismic_history.shift if @seismic_history.size > Constants::MAX_QUAKES
56
+ end
57
+
58
+ def update_active_faults(plate_a_id, plate_b_id, boundary_type)
59
+ existing = @active_faults.find { |f| fault_matches?(f, plate_a_id, plate_b_id) }
60
+ if existing
61
+ existing[:boundary_type] = boundary_type
62
+ existing[:last_activity] = Time.now.utc
63
+ else
64
+ @active_faults << { plate_a_id: plate_a_id, plate_b_id: plate_b_id,
65
+ boundary_type: boundary_type, last_activity: Time.now.utc }
66
+ end
67
+ end
68
+
69
+ def fault_matches?(fault, id_a, id_b)
70
+ (fault[:plate_a_id] == id_a && fault[:plate_b_id] == id_b) ||
71
+ (fault[:plate_a_id] == id_b && fault[:plate_b_id] == id_a)
72
+ end
73
+
74
+ def remove_faults_for(plate_id)
75
+ @active_faults.reject! { |f| f[:plate_a_id] == plate_id || f[:plate_b_id] == plate_id }
76
+ end
77
+
78
+ def avg_mass(plates)
79
+ return 0.0 if plates.empty?
80
+
81
+ (plates.sum(&:mass) / plates.size.to_f).round(10)
82
+ end
83
+
84
+ def recent_earthquakes(count)
85
+ @seismic_history.last(count).map(&:to_h)
86
+ end
87
+ end
88
+
89
+ class TectonicEngine
90
+ include Constants
91
+ include TectonicBoundaries
92
+
93
+ attr_reader :plates, :seismic_history, :active_faults
94
+
95
+ def initialize
96
+ @plates = {}
97
+ @seismic_history = []
98
+ @active_faults = []
99
+ end
100
+
101
+ def create_plate(domain:, content:, mass: 0.5, drift_vector: nil, position: nil, **)
102
+ raise ArgumentError, 'plate limit reached' if @plates.size >= Constants::MAX_PLATES
103
+
104
+ plate = BeliefPlate.new(domain: domain, content: content,
105
+ mass: mass, drift_vector: drift_vector, position: position)
106
+ @plates[plate.id] = plate
107
+ { success: true, plate_id: plate.id, plate: plate.to_h }
108
+ rescue ArgumentError => e
109
+ { success: false, error: e.message }
110
+ end
111
+
112
+ def drift_tick!(delta_t = 1.0, **)
113
+ moved = 0
114
+ @plates.each_value do |plate|
115
+ next unless plate.active?
116
+
117
+ plate.drift!(delta_t)
118
+ moved += 1
119
+ end
120
+ collisions = detect_collisions
121
+ { success: true, plates_moved: moved, collisions_detected: collisions.size, collisions: collisions }
122
+ end
123
+
124
+ def detect_collisions
125
+ active = @plates.values.select(&:active?)
126
+ pairs = active.combination(2).select { |a, b| a.distance_to(b) < Constants::COLLISION_THRESHOLD }
127
+ pairs.map { |a, b| { plate_a_id: a.id, plate_b_id: b.id, distance: a.distance_to(b) } }
128
+ end
129
+
130
+ def resolve_collision(plate_a_id:, plate_b_id:, boundary_type:, **)
131
+ raise ArgumentError, "unknown boundary type: #{boundary_type}" unless Constants::BOUNDARY_TYPES.include?(boundary_type)
132
+
133
+ plate_a = @plates[plate_a_id]
134
+ plate_b = @plates[plate_b_id]
135
+ raise ArgumentError, "plate not found: #{plate_a_id}" unless plate_a
136
+ raise ArgumentError, "plate not found: #{plate_b_id}" unless plate_b
137
+
138
+ result = send(:"resolve_#{boundary_type}", plate_a, plate_b)
139
+ update_active_faults(plate_a_id, plate_b_id, boundary_type)
140
+ result
141
+ rescue ArgumentError => e
142
+ { success: false, error: e.message }
143
+ end
144
+
145
+ def subduct(weaker_plate_id:, stronger_plate_id:, **)
146
+ weaker = @plates[weaker_plate_id]
147
+ stronger = @plates[stronger_plate_id]
148
+ raise ArgumentError, "plate not found: #{weaker_plate_id}" unless weaker
149
+ raise ArgumentError, "plate not found: #{stronger_plate_id}" unless stronger
150
+
151
+ mass_absorbed = weaker.mass * 0.5
152
+ stronger.mass = (stronger.mass + mass_absorbed).clamp(0.0, 1.0)
153
+ weaker.subduct!
154
+ remove_faults_for(weaker_plate_id)
155
+ { success: true, subducted_plate_id: weaker_plate_id, mass_absorbed: mass_absorbed.round(10) }
156
+ rescue ArgumentError => e
157
+ { success: false, error: e.message }
158
+ end
159
+
160
+ def trigger_earthquake(plate_id:, magnitude:, **)
161
+ plate = @plates[plate_id]
162
+ raise ArgumentError, "plate not found: #{plate_id}" unless plate
163
+
164
+ nearby = nearby_plates(plate, exclude_id: plate_id)
165
+ event = SeismicEvent.new(type: :earthquake, magnitude: magnitude,
166
+ epicenter_plate_id: plate_id, affected_plate_ids: nearby.map(&:id))
167
+ record_seismic_event(event)
168
+ plate.release_stress!
169
+ nearby.each { |p| p.accumulate_stress!(magnitude * 0.3) }
170
+ { success: true, event_id: event.id, event: event.to_h }
171
+ rescue ArgumentError => e
172
+ { success: false, error: e.message }
173
+ end
174
+
175
+ def aftershock_cascade(event_id:, **)
176
+ parent = @seismic_history.find { |e| e.id == event_id }
177
+ raise ArgumentError, "event not found: #{event_id}" unless parent
178
+
179
+ decayed_magnitude = (parent.magnitude * (1.0 - Constants::AFTERSHOCK_DECAY)).round(10)
180
+ return { success: true, aftershocks: [], reason: :magnitude_too_low } if decayed_magnitude < 0.1
181
+
182
+ aftershock = SeismicEvent.new(type: :aftershock, magnitude: decayed_magnitude,
183
+ epicenter_plate_id: parent.epicenter_plate_id,
184
+ affected_plate_ids: parent.affected_plate_ids,
185
+ parent_event_id: event_id)
186
+ record_seismic_event(aftershock)
187
+ { success: true, aftershocks: [aftershock.to_h] }
188
+ rescue ArgumentError => e
189
+ { success: false, error: e.message }
190
+ end
191
+
192
+ def all_plates
193
+ @plates.values.map(&:to_h)
194
+ end
195
+
196
+ def tectonic_report
197
+ active_plates, subducted_plates = @plates.values.partition(&:active?)
198
+ high_stress = active_plates.select { |p| p.stress_accumulation > 0.5 }
199
+ {
200
+ total_plates: @plates.size,
201
+ active_plates: active_plates.size,
202
+ subducted_plates: subducted_plates.size,
203
+ high_stress_count: high_stress.size,
204
+ seismic_events: @seismic_history.size,
205
+ active_faults: @active_faults.size,
206
+ avg_mass: avg_mass(active_plates),
207
+ recent_quakes: recent_earthquakes(5)
208
+ }
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTectonics
6
+ module Runners
7
+ module CognitiveTectonics
8
+ extend self
9
+
10
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
11
+
12
+ def create_plate(domain: nil, content: nil, mass: 0.5, drift_vector: nil, position: nil, engine: nil, **)
13
+ raise ArgumentError, 'domain is required' if domain.nil?
14
+ raise ArgumentError, 'content is required' if content.nil?
15
+
16
+ tectonic_engine(engine).create_plate(
17
+ domain: domain,
18
+ content: content,
19
+ mass: mass,
20
+ drift_vector: drift_vector,
21
+ position: position
22
+ )
23
+ rescue ArgumentError => e
24
+ { success: false, error: e.message }
25
+ end
26
+
27
+ def drift_tick(delta_t: 1.0, engine: nil, **)
28
+ tectonic_engine(engine).drift_tick!(delta_t)
29
+ rescue ArgumentError => e
30
+ { success: false, error: e.message }
31
+ end
32
+
33
+ def resolve_collision(plate_a_id: nil, plate_b_id: nil, boundary_type: :convergent, engine: nil, **)
34
+ raise ArgumentError, 'plate_a_id is required' if plate_a_id.nil?
35
+ raise ArgumentError, 'plate_b_id is required' if plate_b_id.nil?
36
+
37
+ tectonic_engine(engine).resolve_collision(
38
+ plate_a_id: plate_a_id,
39
+ plate_b_id: plate_b_id,
40
+ boundary_type: boundary_type
41
+ )
42
+ rescue ArgumentError => e
43
+ { success: false, error: e.message }
44
+ end
45
+
46
+ def trigger_earthquake(plate_id: nil, magnitude: 1.0, engine: nil, **)
47
+ raise ArgumentError, 'plate_id is required' if plate_id.nil?
48
+
49
+ tectonic_engine(engine).trigger_earthquake(plate_id: plate_id, magnitude: magnitude)
50
+ rescue ArgumentError => e
51
+ { success: false, error: e.message }
52
+ end
53
+
54
+ def tectonic_status(engine: nil, **)
55
+ eng = tectonic_engine(engine)
56
+ report = eng.tectonic_report
57
+ { success: true }.merge(report)
58
+ rescue ArgumentError => e
59
+ { success: false, error: e.message }
60
+ end
61
+
62
+ private
63
+
64
+ def tectonic_engine(engine)
65
+ engine || @tectonic_engine ||= Helpers::TectonicEngine.new
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTectonics
6
+ VERSION = '0.1.0'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ require 'legion/extensions/cognitive_tectonics/version'
6
+ require 'legion/extensions/cognitive_tectonics/helpers/constants'
7
+ require 'legion/extensions/cognitive_tectonics/helpers/belief_plate'
8
+ require 'legion/extensions/cognitive_tectonics/helpers/seismic_event'
9
+ require 'legion/extensions/cognitive_tectonics/helpers/tectonic_engine'
10
+ require 'legion/extensions/cognitive_tectonics/runners/cognitive_tectonics'
11
+ require 'legion/extensions/cognitive_tectonics/client'
12
+
13
+ module Legion
14
+ module Extensions
15
+ module CognitiveTectonics
16
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-cognitive-tectonics
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: Tectonic belief-plate model for LegionIO — conviction as mass, drift
27
+ vectors, convergent/divergent/transform boundaries, seismic belief shifts, and aftershock
28
+ cascades
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
+ - README.md
42
+ - lex-cognitive-tectonics.gemspec
43
+ - lib/legion/extensions/cognitive_tectonics.rb
44
+ - lib/legion/extensions/cognitive_tectonics/actors/drift_tick.rb
45
+ - lib/legion/extensions/cognitive_tectonics/client.rb
46
+ - lib/legion/extensions/cognitive_tectonics/helpers/belief_plate.rb
47
+ - lib/legion/extensions/cognitive_tectonics/helpers/constants.rb
48
+ - lib/legion/extensions/cognitive_tectonics/helpers/seismic_event.rb
49
+ - lib/legion/extensions/cognitive_tectonics/helpers/tectonic_engine.rb
50
+ - lib/legion/extensions/cognitive_tectonics/runners/cognitive_tectonics.rb
51
+ - lib/legion/extensions/cognitive_tectonics/version.rb
52
+ homepage: https://github.com/LegionIO/lex-cognitive-tectonics
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/LegionIO/lex-cognitive-tectonics
57
+ source_code_uri: https://github.com/LegionIO/lex-cognitive-tectonics
58
+ documentation_uri: https://github.com/LegionIO/lex-cognitive-tectonics
59
+ changelog_uri: https://github.com/LegionIO/lex-cognitive-tectonics
60
+ bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-tectonics/issues
61
+ rubygems_mfa_required: 'true'
62
+ rdoc_options: []
63
+ require_paths:
64
+ - lib
65
+ required_ruby_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '3.4'
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ requirements: []
76
+ rubygems_version: 3.6.9
77
+ specification_version: 4
78
+ summary: LEX Cognitive Tectonics
79
+ test_files: []