lex-cognitive-labyrinth 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: 8e80874f5d806cabd4e0b5a3873fc35e6ceffda2e32268105fd2ae694fcdc3d0
4
+ data.tar.gz: ac099ce9a1c5365b456d09933fb5d09183ac4526ac0b362c7bd2972b1cca7117
5
+ SHA512:
6
+ metadata.gz: 2662fd410758e942642ee4a9aeac9d01c1d2512706c6aac86442ed7a11e000a443fa6bcc780c838dc17fb66c2fe48f38d0b898e3c34f6c18bc14078940b7d83c
7
+ data.tar.gz: c4c1114b70ba860416567281e4123193d7194f4edad034feb42c127015907bf5f795d51b8a96dfbdfd532279e4fd749b65fad3c410ef170fbf2dbce1fa8fe288
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+
7
+ jobs:
8
+ ci:
9
+ uses: LegionIO/.github/.github/workflows/ci.yml@main
10
+
11
+ release:
12
+ needs: ci
13
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
14
+ uses: LegionIO/.github/.github/workflows/release.yml@main
15
+ secrets:
16
+ rubygems-api-key: ${{ secrets.RUBYGEMS_API_KEY }}
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
10
+ .rspec_status
11
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --require spec_helper
2
+ --format documentation
3
+ --color
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/CyclomaticComplexity:
34
+ Max: 15
35
+
36
+ Metrics/PerceivedComplexity:
37
+ Max: 17
38
+
39
+ Metrics/ParameterLists:
40
+ Max: 8
41
+ MaxOptionalParameters: 8
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,129 @@
1
+ # lex-cognitive-labyrinth
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+
6
+ ## Purpose
7
+
8
+ Maze-like problem space navigation engine. Models a cognitive labyrinth as a directed graph of typed nodes (corridors, junctions, dead ends, entrances, exits, minotaur lairs). The agent explores by moving between nodes, dropping breadcrumbs for backtracking, following Ariadne's thread toward the exit, and encountering the Minotaur (dangerous misconceptions) at specially typed nodes.
9
+
10
+ ## Gem Info
11
+
12
+ - **Gem name**: `lex-cognitive-labyrinth`
13
+ - **Module**: `Legion::Extensions::CognitiveLabyrinth`
14
+ - **Version**: `0.1.0`
15
+ - **Ruby**: `>= 3.4`
16
+ - **License**: MIT
17
+
18
+ ## File Structure
19
+
20
+ ```
21
+ lib/legion/extensions/cognitive_labyrinth/
22
+ version.rb
23
+ client.rb
24
+ helpers/
25
+ constants.rb
26
+ node.rb
27
+ labyrinth.rb
28
+ labyrinth_engine.rb
29
+ runners/
30
+ cognitive_labyrinth.rb
31
+ actors/
32
+ thread_walker.rb
33
+ ```
34
+
35
+ ## Key Constants
36
+
37
+ | Constant | Value | Purpose |
38
+ |---|---|---|
39
+ | `NODE_TYPES` | `%i[corridor junction dead_end entrance exit minotaur_lair]` | Valid node type values |
40
+ | `MAX_LABYRINTHS` | `20` | Per-engine labyrinth capacity |
41
+ | `MAX_NODES` | `200` | Per-engine node capacity |
42
+ | `DEFAULT_DANGER_LEVEL` | `0.0` | Starting danger for non-minotaur nodes |
43
+ | `MINOTAUR_DANGER_LEVEL` | `0.9` | Danger applied to `minotaur_lair` nodes |
44
+ | `DEAD_END_COMPLEXITY_HIT` | `0.1` | Complexity added when entering a dead end |
45
+ | `COMPLEXITY_LABELS` | range hash | Labels from `:trivial` to `:impossible` |
46
+ | `DANGER_LABELS` | range hash | Labels from `:safe` to `:lethal` |
47
+
48
+ ## Helpers
49
+
50
+ ### `Helpers::Node`
51
+ Graph node with `id`, `label`, `node_type`, `connections` (array of neighbor IDs), and `danger_level`.
52
+
53
+ - `connect!(other_node_id)` — adds neighbor (idempotent)
54
+ - `disconnect!(other_node_id)` — removes neighbor
55
+ - `dead_end?` — true if `node_type == :dead_end`
56
+ - `junction?` — true if `node_type == :junction`
57
+ - `dangerous?` — true if `danger_level > 0.5`
58
+ - `to_h` — serializes to hash
59
+
60
+ ### `Helpers::Labyrinth`
61
+ Single labyrinth instance. Holds nodes hash, current position, breadcrumb trail, and Ariadne's thread.
62
+
63
+ - `add_node(node)` — registers node
64
+ - `move_to!(node_id)` — changes current position, drops breadcrumb, increments dead-end complexity
65
+ - `backtrack!` — pops breadcrumb stack to return to previous node
66
+ - `drop_breadcrumb` — records current position in trail
67
+ - `follow_thread` — returns path toward exit node (thread set on entrance)
68
+ - `solved?` — current node is the exit
69
+ - `lost?` — no breadcrumbs and not at entrance
70
+ - `path_length` — breadcrumb count
71
+ - `complexity` / `complexity_label`
72
+ - `to_h`
73
+
74
+ ### `Helpers::LabyrinthEngine`
75
+ Top-level in-memory store. Enforces `MAX_LABYRINTHS` and `MAX_NODES`.
76
+
77
+ - `create_labyrinth(name:)` → labyrinth or `{error: :capacity_exceeded}`
78
+ - `add_node_to(labyrinth_id:, label:, node_type:, danger_level:)` → node or error
79
+ - `connect_nodes(labyrinth_id:, from_id:, to_id:)` → status hash
80
+ - `move(labyrinth_id:, node_id:)` → position hash
81
+ - `backtrack(labyrinth_id:)` → position hash or `:at_entrance`
82
+ - `follow_thread(labyrinth_id:)` → thread path
83
+ - `check_minotaur(labyrinth_id:)` → danger result for current node
84
+ - `labyrinth_report(labyrinth_id:)` → full status hash
85
+ - `list_labyrinths` → array of labyrinth summaries
86
+ - `delete_labyrinth(labyrinth_id:)` → boolean
87
+
88
+ ## Runners
89
+
90
+ Module: `Runners::CognitiveLaby rinth` (included via `Client`)
91
+
92
+ | Runner Method | Description |
93
+ |---|---|
94
+ | `create_labyrinth(name:)` | Create a new labyrinth |
95
+ | `add_node(labyrinth_id:, label:, node_type:, danger_level:)` | Add a node |
96
+ | `connect_nodes(labyrinth_id:, from_id:, to_id:)` | Connect two nodes |
97
+ | `move(labyrinth_id:, node_id:)` | Move current position |
98
+ | `backtrack(labyrinth_id:)` | Backtrack along breadcrumbs |
99
+ | `follow_thread(labyrinth_id:)` | Follow Ariadne's thread toward exit |
100
+ | `check_minotaur(labyrinth_id:)` | Check danger at current node |
101
+ | `labyrinth_report(labyrinth_id:)` | Full labyrinth state |
102
+ | `list_labyrinths` | All labyrinths summary |
103
+ | `delete_labyrinth(labyrinth_id:)` | Remove labyrinth |
104
+
105
+ All runners return `{success: true/false, ...}` hashes.
106
+
107
+ ## Actors
108
+
109
+ ### `Actors::ThreadWalker`
110
+ - Subclass of `Legion::Extensions::Actors::Every`
111
+ - Interval: `600` seconds
112
+ - Calls `Runners::CognitiveLaby rinth#list_labyrinths`
113
+ - `run_now? = false`, `use_runner? = false`
114
+ - Passive periodic survey of all open labyrinths
115
+
116
+ ## Integration Points
117
+
118
+ - No direct dependencies on other agentic LEX gems
119
+ - Can integrate with `lex-tick` via `action_selection` phase handler to gate actions when the agent is "lost" in a problem space
120
+ - Works alongside `lex-memory` for persisting explored paths as semantic traces
121
+ - Minotaur encounters (high danger) can feed into `lex-emotion` arousal signal
122
+
123
+ ## Development Notes
124
+
125
+ - `Client` instantiates `@labyrinth_engine = Helpers::LabyrinthEngine.new` — all state is per-client-instance
126
+ - Ariadne's thread is set at entrance node creation and points toward the exit node ID
127
+ - `backtrack!` returns `:at_entrance` when the breadcrumb stack is empty
128
+ - Node connections are directed (one-way unless connected in both directions)
129
+ - `MAX_LABYRINTHS` and `MAX_NODES` are enforced at engine level; runners receive error hashes on overflow
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,56 @@
1
+ # lex-cognitive-labyrinth
2
+
3
+ Maze-like problem space navigation for LegionIO cognitive agents. Models complex reasoning challenges as labyrinths with typed nodes, directional connections, breadcrumb backtracking, and Ariadne's thread guidance toward the exit.
4
+
5
+ ## What It Does
6
+
7
+ - Creates named labyrinths as directed graphs of cognitive nodes
8
+ - Node types: `corridor`, `junction`, `dead_end`, `entrance`, `exit`, `minotaur_lair`
9
+ - Navigation: move to nodes, drop breadcrumbs, backtrack along the crumb trail
10
+ - Ariadne's thread: pre-computed guidance path toward the exit node
11
+ - Minotaur detection: checks danger level at current position (minotaur lairs default to 0.9 danger)
12
+ - Complexity tracking: increases when entering dead ends
13
+ - Periodic survey via `ThreadWalker` actor (every 600s)
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+ # Create a labyrinth
19
+ result = runner.create_labyrinth(name: 'problem_analysis')
20
+ labyrinth_id = result[:labyrinth][:id]
21
+
22
+ # Add nodes
23
+ entrance = runner.add_node(labyrinth_id: labyrinth_id, label: 'start', node_type: :entrance, danger_level: 0.0)
24
+ junction = runner.add_node(labyrinth_id: labyrinth_id, label: 'fork', node_type: :junction, danger_level: 0.0)
25
+ danger = runner.add_node(labyrinth_id: labyrinth_id, label: 'false assumption', node_type: :minotaur_lair, danger_level: 0.9)
26
+ exit_n = runner.add_node(labyrinth_id: labyrinth_id, label: 'solution', node_type: :exit, danger_level: 0.0)
27
+
28
+ # Connect and navigate
29
+ runner.connect_nodes(labyrinth_id: labyrinth_id, from_id: entrance[:node][:id], to_id: junction[:node][:id])
30
+ runner.move(labyrinth_id: labyrinth_id, node_id: junction[:node][:id])
31
+
32
+ # Check for danger
33
+ runner.check_minotaur(labyrinth_id: labyrinth_id)
34
+ # => { success: true, danger_level: 0.0, dangerous: false, ... }
35
+
36
+ # Backtrack if stuck
37
+ runner.backtrack(labyrinth_id: labyrinth_id)
38
+
39
+ # Follow the thread toward exit
40
+ runner.follow_thread(labyrinth_id: labyrinth_id)
41
+
42
+ # Full status
43
+ runner.labyrinth_report(labyrinth_id: labyrinth_id)
44
+ ```
45
+
46
+ ## Development
47
+
48
+ ```bash
49
+ bundle install
50
+ bundle exec rspec
51
+ bundle exec rubocop
52
+ ```
53
+
54
+ ## License
55
+
56
+ MIT
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_labyrinth/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-labyrinth'
7
+ spec.version = Legion::Extensions::CognitiveLabyrinth::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Cognitive Labyrinth'
12
+ spec.description = 'Maze-like problem spaces for agentic reasoning — paths, dead ends, backtracking, ' \
13
+ 'breadcrumb trails, Ariadne\'s thread (guiding heuristic), and Minotaur encounters ' \
14
+ '(dangerous misconceptions)'
15
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-labyrinth'
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-cognitive-labyrinth'
21
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-cognitive-labyrinth'
22
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-cognitive-labyrinth'
23
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-cognitive-labyrinth/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 CognitiveLabyrinth
8
+ module Actor
9
+ class ThreadWalker < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::CognitiveLabyrinth::Runners::CognitiveLabyrinth
12
+ end
13
+
14
+ def runner_function
15
+ 'list_labyrinths'
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,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
6
+ class Client
7
+ include Runners::CognitiveLabyrinth
8
+
9
+ def initialize(**)
10
+ @labyrinth_engine = Helpers::LabyrinthEngine.new
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :labyrinth_engine
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
6
+ module Helpers
7
+ module Constants
8
+ NODE_TYPES = %i[corridor junction dead_end entrance exit minotaur_lair].freeze
9
+
10
+ MAX_LABYRINTHS = 20
11
+ MAX_NODES = 200
12
+
13
+ COMPLEXITY_LABELS = {
14
+ (0.0...0.2) => :trivial,
15
+ (0.2...0.4) => :simple,
16
+ (0.4...0.6) => :moderate,
17
+ (0.6...0.8) => :complex,
18
+ (0.8..1.0) => :labyrinthine
19
+ }.freeze
20
+
21
+ DANGER_LABELS = {
22
+ (0.0...0.25) => :safe,
23
+ (0.25...0.5) => :uncertain,
24
+ (0.5...0.75) => :hazardous,
25
+ (0.75..1.0) => :lethal
26
+ }.freeze
27
+
28
+ DEFAULT_DANGER_LEVEL = 0.0
29
+ MINOTAUR_DANGER_LEVEL = 0.9
30
+ DEAD_END_COMPLEXITY_HIT = 0.1
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
6
+ module Helpers
7
+ class Labyrinth
8
+ attr_reader :labyrinth_id, :name, :domain, :nodes, :breadcrumbs, :current_node_id
9
+ attr_accessor :solved
10
+
11
+ def initialize(labyrinth_id:, name:, domain: nil)
12
+ @labyrinth_id = labyrinth_id
13
+ @name = name
14
+ @domain = domain
15
+ @nodes = {}
16
+ @breadcrumbs = []
17
+ @current_node_id = nil
18
+ @solved = false
19
+ end
20
+
21
+ def add_node(node)
22
+ raise ArgumentError, "nodes must be a #{Node}" unless node.is_a?(Node)
23
+ raise ArgumentError, "max nodes (#{Constants::MAX_NODES}) reached" if @nodes.size >= Constants::MAX_NODES
24
+
25
+ @nodes[node.node_id] = node
26
+ @current_node_id ||= node.node_id if node.node_type == :entrance
27
+ node
28
+ end
29
+
30
+ def move_to!(node_id)
31
+ node = @nodes.fetch(node_id) { raise ArgumentError, "node #{node_id.inspect} not found in labyrinth" }
32
+
33
+ current = current_node
34
+ unless current.nil? || current.connections.include?(node_id)
35
+ raise ArgumentError, "cannot move to #{node_id.inspect}: not connected from current node #{@current_node_id.inspect}"
36
+ end
37
+
38
+ drop_breadcrumb
39
+ node.visited = true
40
+ @current_node_id = node_id
41
+ @solved = true if node.node_type == :exit
42
+ node
43
+ end
44
+
45
+ def backtrack!
46
+ return nil if @breadcrumbs.empty?
47
+
48
+ target_id = @breadcrumbs.pop
49
+ @current_node_id = target_id
50
+ current_node
51
+ end
52
+
53
+ def drop_breadcrumb
54
+ return unless @current_node_id
55
+
56
+ current = @nodes[@current_node_id]
57
+ current.visited = true if current
58
+ @breadcrumbs << @current_node_id if @breadcrumbs.last != @current_node_id
59
+ end
60
+
61
+ def follow_thread
62
+ return nil if current_node.nil?
63
+
64
+ unvisited = current_node.connections.find do |conn_id|
65
+ node = @nodes[conn_id]
66
+ node && !node.visited
67
+ end
68
+
69
+ return nil unless unvisited
70
+
71
+ move_to!(unvisited)
72
+ end
73
+
74
+ def solved?
75
+ @solved
76
+ end
77
+
78
+ def lost?
79
+ return false if @current_node_id.nil?
80
+ return false if solved?
81
+
82
+ current = current_node
83
+ return true if current.nil?
84
+
85
+ unvisited_exits = current.connections.any? do |conn_id|
86
+ node = @nodes[conn_id]
87
+ node && !node.visited
88
+ end
89
+
90
+ !unvisited_exits && !current.dead_end? && @breadcrumbs.empty?
91
+ end
92
+
93
+ def path_length
94
+ @breadcrumbs.size
95
+ end
96
+
97
+ def current_node
98
+ @nodes[@current_node_id]
99
+ end
100
+
101
+ def complexity
102
+ return 0.0 if @nodes.empty?
103
+
104
+ dead_end_count = @nodes.values.count(&:dead_end?)
105
+ junction_count = @nodes.values.count(&:junction?)
106
+ minotaur_count = @nodes.values.count { |n| n.node_type == :minotaur_lair }
107
+
108
+ raw = ((dead_end_count * 0.3) + (junction_count * 0.2) + (minotaur_count * 0.5)) / @nodes.size.to_f
109
+ raw.clamp(0.0, 1.0).round(10)
110
+ end
111
+
112
+ def complexity_label
113
+ score = complexity
114
+ Constants::COMPLEXITY_LABELS.find { |range, _| range.cover?(score) }&.last || :labyrinthine
115
+ end
116
+
117
+ def to_h
118
+ {
119
+ labyrinth_id: @labyrinth_id,
120
+ name: @name,
121
+ domain: @domain,
122
+ node_count: @nodes.size,
123
+ current_node_id: @current_node_id,
124
+ path_length: path_length,
125
+ solved: @solved,
126
+ complexity: complexity,
127
+ complexity_label: complexity_label
128
+ }
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
6
+ module Helpers
7
+ class LabyrinthEngine
8
+ attr_reader :labyrinths
9
+
10
+ def initialize
11
+ @labyrinths = {}
12
+ end
13
+
14
+ def create_labyrinth(name:, domain: nil, labyrinth_id: nil)
15
+ raise ArgumentError, "max labyrinths (#{Constants::MAX_LABYRINTHS}) reached" if @labyrinths.size >= Constants::MAX_LABYRINTHS
16
+
17
+ id = labyrinth_id || SecureRandom.uuid
18
+ labyrinth = Labyrinth.new(labyrinth_id: id, name: name, domain: domain)
19
+ @labyrinths[id] = labyrinth
20
+ Legion::Logging.debug "[cognitive_labyrinth] created labyrinth id=#{id[0..7]} name=#{name}"
21
+ labyrinth
22
+ end
23
+
24
+ def add_node_to(labyrinth_id:, node_type:, content: nil, danger_level: nil, node_id: nil)
25
+ labyrinth = fetch_labyrinth!(labyrinth_id)
26
+ id = node_id || SecureRandom.uuid
27
+ level = (danger_level || default_danger_for(node_type)).clamp(0.0, 1.0)
28
+
29
+ node = Node.new(node_id: id, node_type: node_type, content: content, danger_level: level)
30
+ labyrinth.add_node(node)
31
+ Legion::Logging.debug "[cognitive_labyrinth] added node #{id[0..7]} type=#{node_type} to labyrinth #{labyrinth_id[0..7]}"
32
+ node
33
+ end
34
+
35
+ def connect_nodes(labyrinth_id:, from_id:, to_id:, bidirectional: true)
36
+ labyrinth = fetch_labyrinth!(labyrinth_id)
37
+ from_node = fetch_node!(labyrinth, from_id)
38
+ to_node = fetch_node!(labyrinth, to_id)
39
+
40
+ from_node.connect!(to_id)
41
+ to_node.connect!(from_id) if bidirectional
42
+ { connected: true, from: from_id, to: to_id, bidirectional: bidirectional }
43
+ end
44
+
45
+ def move(labyrinth_id:, node_id:, **)
46
+ labyrinth = fetch_labyrinth!(labyrinth_id)
47
+ node = labyrinth.move_to!(node_id)
48
+
49
+ minotaur_result = check_minotaur(labyrinth_id: labyrinth_id)
50
+ Legion::Logging.debug "[cognitive_labyrinth] moved to #{node_id[0..7]} type=#{node.node_type}"
51
+
52
+ {
53
+ success: true,
54
+ node_id: node.node_id,
55
+ node_type: node.node_type,
56
+ content: node.content,
57
+ danger_level: node.danger_level,
58
+ solved: labyrinth.solved?,
59
+ minotaur: minotaur_result,
60
+ path_length: labyrinth.path_length
61
+ }
62
+ end
63
+
64
+ def backtrack(labyrinth_id:, **)
65
+ labyrinth = fetch_labyrinth!(labyrinth_id)
66
+ node = labyrinth.backtrack!
67
+
68
+ if node
69
+ Legion::Logging.debug "[cognitive_labyrinth] backtracked to #{node.node_id[0..7]}"
70
+ { success: true, node_id: node.node_id, node_type: node.node_type, path_length: labyrinth.path_length }
71
+ else
72
+ Legion::Logging.debug '[cognitive_labyrinth] backtrack failed: no breadcrumbs'
73
+ { success: false, reason: :no_breadcrumbs }
74
+ end
75
+ end
76
+
77
+ def follow_thread(labyrinth_id:, **)
78
+ labyrinth = fetch_labyrinth!(labyrinth_id)
79
+ node = labyrinth.follow_thread
80
+
81
+ if node
82
+ minotaur_result = check_minotaur(labyrinth_id: labyrinth_id)
83
+ Legion::Logging.debug "[cognitive_labyrinth] thread followed to #{node.node_id[0..7]}"
84
+ {
85
+ success: true,
86
+ node_id: node.node_id,
87
+ node_type: node.node_type,
88
+ content: node.content,
89
+ danger_level: node.danger_level,
90
+ solved: labyrinth.solved?,
91
+ minotaur: minotaur_result
92
+ }
93
+ else
94
+ Legion::Logging.debug '[cognitive_labyrinth] thread exhausted: no unvisited nodes from current'
95
+ { success: false, reason: :thread_exhausted }
96
+ end
97
+ end
98
+
99
+ def check_minotaur(labyrinth_id:, **)
100
+ labyrinth = fetch_labyrinth!(labyrinth_id)
101
+ node = labyrinth.current_node
102
+ return { encountered: false } unless node
103
+
104
+ if node.node_type == :minotaur_lair
105
+ Legion::Logging.debug "[cognitive_labyrinth] MINOTAUR ENCOUNTERED at #{node.node_id[0..7]}"
106
+ {
107
+ encountered: true,
108
+ node_id: node.node_id,
109
+ danger_level: node.danger_level,
110
+ danger_label: danger_label_for(node.danger_level),
111
+ misconception: node.content
112
+ }
113
+ elsif node.dangerous?
114
+ {
115
+ encountered: false,
116
+ warning: true,
117
+ danger_level: node.danger_level,
118
+ danger_label: danger_label_for(node.danger_level)
119
+ }
120
+ else
121
+ { encountered: false }
122
+ end
123
+ end
124
+
125
+ def labyrinth_report(labyrinth_id:, **)
126
+ labyrinth = fetch_labyrinth!(labyrinth_id)
127
+ nodes_by_type = labyrinth.nodes.values.group_by(&:node_type).transform_values(&:count)
128
+
129
+ labyrinth.to_h.merge(
130
+ nodes_by_type: nodes_by_type,
131
+ visited_count: labyrinth.nodes.values.count(&:visited),
132
+ breadcrumb_trail: labyrinth.breadcrumbs.dup,
133
+ lost: labyrinth.lost?
134
+ )
135
+ end
136
+
137
+ def list_labyrinths(**)
138
+ @labyrinths.values.map(&:to_h)
139
+ end
140
+
141
+ def delete_labyrinth(labyrinth_id:, **)
142
+ raise ArgumentError, "labyrinth #{labyrinth_id.inspect} not found" unless @labyrinths.key?(labyrinth_id)
143
+
144
+ @labyrinths.delete(labyrinth_id)
145
+ { deleted: true, labyrinth_id: labyrinth_id }
146
+ end
147
+
148
+ private
149
+
150
+ def fetch_labyrinth!(labyrinth_id)
151
+ @labyrinths.fetch(labyrinth_id) do
152
+ raise ArgumentError, "labyrinth #{labyrinth_id.inspect} not found"
153
+ end
154
+ end
155
+
156
+ def fetch_node!(labyrinth, node_id)
157
+ labyrinth.nodes.fetch(node_id) do
158
+ raise ArgumentError, "node #{node_id.inspect} not found in labyrinth #{labyrinth.labyrinth_id.inspect}"
159
+ end
160
+ end
161
+
162
+ def default_danger_for(node_type)
163
+ node_type == :minotaur_lair ? Constants::MINOTAUR_DANGER_LEVEL : Constants::DEFAULT_DANGER_LEVEL
164
+ end
165
+
166
+ def danger_label_for(level)
167
+ Constants::DANGER_LABELS.find { |range, _| range.cover?(level) }&.last || :lethal
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
6
+ module Helpers
7
+ class Node
8
+ attr_reader :node_id, :node_type, :content, :connections, :danger_level
9
+ attr_accessor :visited
10
+
11
+ def initialize(node_id:, node_type:, content: nil, danger_level: Constants::DEFAULT_DANGER_LEVEL)
12
+ unless Constants::NODE_TYPES.include?(node_type)
13
+ raise ArgumentError, "unknown node_type: #{node_type.inspect}; must be one of #{Constants::NODE_TYPES.inspect}"
14
+ end
15
+
16
+ @node_id = node_id
17
+ @node_type = node_type
18
+ @content = content
19
+ @connections = []
20
+ @visited = false
21
+ @danger_level = danger_level.clamp(0.0, 1.0)
22
+ end
23
+
24
+ def connect!(other_id)
25
+ @connections << other_id unless @connections.include?(other_id)
26
+ self
27
+ end
28
+
29
+ def disconnect!(other_id)
30
+ @connections.delete(other_id)
31
+ self
32
+ end
33
+
34
+ def dead_end?
35
+ @node_type == :dead_end || (@connections.empty? && @node_type != :entrance && @node_type != :exit)
36
+ end
37
+
38
+ def junction?
39
+ @node_type == :junction || @connections.size >= 3
40
+ end
41
+
42
+ def dangerous?
43
+ @node_type == :minotaur_lair || @danger_level >= 0.5
44
+ end
45
+
46
+ def to_h
47
+ {
48
+ node_id: @node_id,
49
+ node_type: @node_type,
50
+ content: @content,
51
+ connections: @connections.dup,
52
+ visited: @visited,
53
+ danger_level: @danger_level
54
+ }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
6
+ module Runners
7
+ module CognitiveLabyrinth
8
+ extend self
9
+
10
+ include Legion::Extensions::Helpers::Lex if defined?(Legion::Extensions::Helpers::Lex)
11
+
12
+ def create_labyrinth(name:, domain: nil, labyrinth_id: nil, engine: nil, **)
13
+ raise ArgumentError, 'name is required' if name.nil? || name.to_s.strip.empty?
14
+
15
+ result = resolve_engine(engine).create_labyrinth(name: name, domain: domain, labyrinth_id: labyrinth_id)
16
+ Legion::Logging.debug "[cognitive_labyrinth] runner: created labyrinth #{result.labyrinth_id[0..7]}"
17
+ { success: true, labyrinth_id: result.labyrinth_id, name: result.name, domain: result.domain }
18
+ rescue ArgumentError => e
19
+ Legion::Logging.debug "[cognitive_labyrinth] create_labyrinth error: #{e.message}"
20
+ { success: false, error: e.message }
21
+ end
22
+
23
+ def add_node(labyrinth_id:, node_type:, content: nil, danger_level: nil, node_id: nil, engine: nil, **)
24
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
25
+ raise ArgumentError, 'node_type is required' if node_type.nil?
26
+
27
+ node = resolve_engine(engine).add_node_to(
28
+ labyrinth_id: labyrinth_id, node_type: node_type,
29
+ content: content, danger_level: danger_level, node_id: node_id
30
+ )
31
+ { success: true, node_id: node.node_id, node_type: node.node_type }
32
+ rescue ArgumentError => e
33
+ Legion::Logging.debug "[cognitive_labyrinth] add_node error: #{e.message}"
34
+ { success: false, error: e.message }
35
+ end
36
+
37
+ def connect_nodes(labyrinth_id:, from_id:, to_id:, bidirectional: true, engine: nil, **)
38
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
39
+
40
+ resolve_engine(engine).connect_nodes(
41
+ labyrinth_id: labyrinth_id, from_id: from_id, to_id: to_id, bidirectional: bidirectional
42
+ )
43
+ rescue ArgumentError => e
44
+ Legion::Logging.debug "[cognitive_labyrinth] connect_nodes error: #{e.message}"
45
+ { success: false, error: e.message }
46
+ end
47
+
48
+ def move(labyrinth_id:, node_id:, engine: nil, **)
49
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
50
+ raise ArgumentError, 'node_id is required' if node_id.nil?
51
+
52
+ resolve_engine(engine).move(labyrinth_id: labyrinth_id, node_id: node_id)
53
+ rescue ArgumentError => e
54
+ Legion::Logging.debug "[cognitive_labyrinth] move error: #{e.message}"
55
+ { success: false, error: e.message }
56
+ end
57
+
58
+ def backtrack(labyrinth_id:, engine: nil, **)
59
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
60
+
61
+ resolve_engine(engine).backtrack(labyrinth_id: labyrinth_id)
62
+ rescue ArgumentError => e
63
+ Legion::Logging.debug "[cognitive_labyrinth] backtrack error: #{e.message}"
64
+ { success: false, error: e.message }
65
+ end
66
+
67
+ def follow_thread(labyrinth_id:, engine: nil, **)
68
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
69
+
70
+ resolve_engine(engine).follow_thread(labyrinth_id: labyrinth_id)
71
+ rescue ArgumentError => e
72
+ Legion::Logging.debug "[cognitive_labyrinth] follow_thread error: #{e.message}"
73
+ { success: false, error: e.message }
74
+ end
75
+
76
+ def check_minotaur(labyrinth_id:, engine: nil, **)
77
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
78
+
79
+ resolve_engine(engine).check_minotaur(labyrinth_id: labyrinth_id)
80
+ rescue ArgumentError => e
81
+ Legion::Logging.debug "[cognitive_labyrinth] check_minotaur error: #{e.message}"
82
+ { success: false, error: e.message }
83
+ end
84
+
85
+ def labyrinth_report(labyrinth_id:, engine: nil, **)
86
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
87
+
88
+ result = resolve_engine(engine).labyrinth_report(labyrinth_id: labyrinth_id)
89
+ { success: true }.merge(result)
90
+ rescue ArgumentError => e
91
+ Legion::Logging.debug "[cognitive_labyrinth] labyrinth_report error: #{e.message}"
92
+ { success: false, error: e.message }
93
+ end
94
+
95
+ def list_labyrinths(engine: nil, **)
96
+ result = resolve_engine(engine).list_labyrinths
97
+ { success: true, labyrinths: result, count: result.size }
98
+ rescue ArgumentError => e
99
+ { success: false, error: e.message }
100
+ end
101
+
102
+ def delete_labyrinth(labyrinth_id:, engine: nil, **)
103
+ raise ArgumentError, 'labyrinth_id is required' if labyrinth_id.nil?
104
+
105
+ resolve_engine(engine).delete_labyrinth(labyrinth_id: labyrinth_id)
106
+ rescue ArgumentError => e
107
+ Legion::Logging.debug "[cognitive_labyrinth] delete_labyrinth error: #{e.message}"
108
+ { success: false, error: e.message }
109
+ end
110
+
111
+ private
112
+
113
+ def labyrinth_engine
114
+ @labyrinth_engine ||= Helpers::LabyrinthEngine.new
115
+ end
116
+
117
+ def resolve_engine(engine)
118
+ engine || labyrinth_engine
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveLabyrinth
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_relative 'cognitive_labyrinth/version'
6
+ require_relative 'cognitive_labyrinth/helpers/constants'
7
+ require_relative 'cognitive_labyrinth/helpers/node'
8
+ require_relative 'cognitive_labyrinth/helpers/labyrinth'
9
+ require_relative 'cognitive_labyrinth/helpers/labyrinth_engine'
10
+ require_relative 'cognitive_labyrinth/runners/cognitive_labyrinth'
11
+ require_relative 'cognitive_labyrinth/client'
12
+
13
+ module Legion
14
+ module Extensions
15
+ module CognitiveLabyrinth
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-labyrinth
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: Maze-like problem spaces for agentic reasoning — paths, dead ends, backtracking,
27
+ breadcrumb trails, Ariadne's thread (guiding heuristic), and Minotaur encounters
28
+ (dangerous misconceptions)
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-labyrinth.gemspec
43
+ - lib/legion/extensions/cognitive_labyrinth.rb
44
+ - lib/legion/extensions/cognitive_labyrinth/actors/thread_walker.rb
45
+ - lib/legion/extensions/cognitive_labyrinth/client.rb
46
+ - lib/legion/extensions/cognitive_labyrinth/helpers/constants.rb
47
+ - lib/legion/extensions/cognitive_labyrinth/helpers/labyrinth.rb
48
+ - lib/legion/extensions/cognitive_labyrinth/helpers/labyrinth_engine.rb
49
+ - lib/legion/extensions/cognitive_labyrinth/helpers/node.rb
50
+ - lib/legion/extensions/cognitive_labyrinth/runners/cognitive_labyrinth.rb
51
+ - lib/legion/extensions/cognitive_labyrinth/version.rb
52
+ homepage: https://github.com/LegionIO/lex-cognitive-labyrinth
53
+ licenses:
54
+ - MIT
55
+ metadata:
56
+ homepage_uri: https://github.com/LegionIO/lex-cognitive-labyrinth
57
+ source_code_uri: https://github.com/LegionIO/lex-cognitive-labyrinth
58
+ documentation_uri: https://github.com/LegionIO/lex-cognitive-labyrinth
59
+ changelog_uri: https://github.com/LegionIO/lex-cognitive-labyrinth
60
+ bug_tracker_uri: https://github.com/LegionIO/lex-cognitive-labyrinth/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 Labyrinth
79
+ test_files: []