lex-cognitive-tessellation 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: e7f6f52f6c2c0a13ef9974ca5dd9d9868fe076e7c1a2ba6a3e23eae1e6c7c610
4
+ data.tar.gz: b70c6b251e6443bc7c13976cbe691709342baf8f373cf396f8f3bec227d58a74
5
+ SHA512:
6
+ metadata.gz: 593ba1d137e5c15f6d87284963efff612b2118b08d53b339cf6308f43c07178df2a8e49a98ad73f96de6235664aded53b0b72bd90a528409f139160c7f26041f
7
+ data.tar.gz: e3cde566be075711228beb30745ab180184125624ad6f3f79997d5af5e8a6332433163fa741116fab81ca265b5c6cb0debe565c7d062e31c42b98f46c028f989
@@ -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,2 @@
1
+ --require spec_helper
2
+ --format documentation
data/.rubocop.yml ADDED
@@ -0,0 +1,37 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ TargetRubyVersion: 3.4
4
+
5
+ Style/Documentation:
6
+ Enabled: false
7
+
8
+ Naming/PredicateMethod:
9
+ Enabled: false
10
+
11
+ Naming/PredicatePrefix:
12
+ Enabled: false
13
+
14
+ Metrics/ClassLength:
15
+ Max: 150
16
+
17
+ Metrics/MethodLength:
18
+ Max: 25
19
+
20
+ Metrics/AbcSize:
21
+ Max: 25
22
+
23
+ Metrics/ParameterLists:
24
+ Max: 8
25
+ MaxOptionalParameters: 8
26
+
27
+ Layout/HashAlignment:
28
+ EnforcedColonStyle: table
29
+ EnforcedHashRocketStyle: table
30
+
31
+ Metrics/BlockLength:
32
+ Exclude:
33
+ - spec/**/*
34
+
35
+ Style/OneClassPerFile:
36
+ Exclude:
37
+ - spec/spec_helper.rb
data/CLAUDE.md ADDED
@@ -0,0 +1,127 @@
1
+ # lex-cognitive-tessellation
2
+
3
+ **Level 3 Leaf Documentation**
4
+ - **Parent**: `/Users/miverso2/rubymine/legion/extensions-agentic/CLAUDE.md`
5
+ - **Gem**: `lex-cognitive-tessellation`
6
+ - **Version**: 0.1.0
7
+ - **Namespace**: `Legion::Extensions::CognitiveTessellation`
8
+
9
+ ## Purpose
10
+
11
+ Models knowledge coverage as a tessellation — a pattern of tiles fitting together without gaps or excessive overlap. Each tile represents a unit of cognitive content (knowledge, skill, pattern, belief, etc.) in a domain. Tiles expand or shrink in coverage, connect to adjacent tiles, and auto-organize into per-domain mosaics. The model surfaces gaps (under-covered tiles), seamless fits (well-integrated tiles), and overall coherence.
12
+
13
+ ## Gem Info
14
+
15
+ - **Gemspec**: `lex-cognitive-tessellation.gemspec`
16
+ - **Require**: `lex-cognitive-tessellation`
17
+ - **Ruby**: >= 3.4
18
+ - **License**: MIT
19
+ - **Homepage**: https://github.com/LegionIO/lex-cognitive-tessellation
20
+
21
+ ## File Structure
22
+
23
+ ```
24
+ lib/legion/extensions/cognitive_tessellation/
25
+ version.rb
26
+ helpers/
27
+ constants.rb # Shapes, domains, tile types, coverage/fit/density label tables
28
+ tile.rb # Tile class — one cognitive knowledge unit
29
+ mosaic.rb # Mosaic class — per-domain tile collection
30
+ tessellation_engine.rb # TessellationEngine — registry and aggregate metrics
31
+ runners/
32
+ cognitive_tessellation.rb # Runner module — public API
33
+ client.rb
34
+ ```
35
+
36
+ ## Key Constants
37
+
38
+ | Constant | Value | Meaning |
39
+ |---|---|---|
40
+ | `MAX_TILES` | 500 | Hard cap; lowest effective-coverage tiles pruned when exceeded |
41
+ | `MAX_MOSAICS` | 50 | Max per-domain mosaics (not enforced in engine) |
42
+ | `FULL_COVERAGE_THRESHOLD` | 0.95 | Coverage >= this = `full_coverage?` true |
43
+ | `GAP_THRESHOLD` | 0.3 | Fit score < this = `gapped?` true |
44
+ | `OVERLAP_THRESHOLD` | 0.7 | Fit + many adjacents triggers `overlapping?` |
45
+ | `FIT_TOLERANCE` | 0.05 | Reference tolerance (defined, not used) |
46
+ | `COVERAGE_GROWTH` | 0.08 | Coverage increase per `expand!` |
47
+ | `COVERAGE_DECAY` | 0.02 | Coverage decrease per `shrink!` |
48
+ | `OVERLAP_PENALTY` | 0.05 | Per-adjacent penalty when `overlapping?` |
49
+
50
+ `TILE_SHAPES`: `[:triangular, :square, :hexagonal, :pentagonal, :irregular, :fractal, :amorphous, :crystalline]`
51
+
52
+ `DOMAINS`: `[:cognitive, :emotional, :procedural, :semantic, :episodic, :social, :creative, :analytical]`
53
+
54
+ `TILE_TYPES`: `[:knowledge, :skill, :pattern, :belief, :heuristic, :schema, :model, :framework]`
55
+
56
+ Coverage labels: `0.9+` = `:complete`, `0.7..0.9` = `:substantial`, `0.5..0.7` = `:partial`, `0.3..0.5` = `:sparse`, `<0.3` = `:fragmented`
57
+
58
+ Fit labels: `0.8+` = `:seamless`, `0.6..0.8` = `:snug`, `0.4..0.6` = `:loose`, `0.2..0.4` = `:gapped`, `<0.2` = `:disjointed`
59
+
60
+ ## Key Classes
61
+
62
+ ### `Helpers::Tile`
63
+
64
+ One cognitive knowledge unit.
65
+
66
+ - `expand!(amount)` — increases coverage by amount (default `COVERAGE_GROWTH`)
67
+ - `shrink!(amount)` — decreases coverage by amount (default `COVERAGE_DECAY`)
68
+ - `adjust_fit!(new_fit)` — sets fit score
69
+ - `connect!(other_id)` — adds to `adjacent_ids` (no duplicates)
70
+ - `full_coverage?` — coverage >= 0.95; `gapped?` — fit_score < 0.3; `seamless?` — fit_score >= 0.8
71
+ - `overlapping?` — fit_score > 0.7 AND adjacent_ids.size > 3
72
+ - `isolated?` — no adjacent tiles; `well_connected?` — 3+ adjacent tiles
73
+ - `effective_coverage` — coverage minus overlap penalties
74
+
75
+ ### `Helpers::Mosaic`
76
+
77
+ A per-domain collection of tiles.
78
+
79
+ - `add_tile(tile)` — appends and auto-connects to last 2 tiles in same domain via `compute_fit`
80
+ - `total_coverage` — mean effective_coverage across tiles
81
+ - `coherence` — ratio of well-connected tiles (adjacency >= 3)
82
+ - `uniformity` — `1 - sqrt(variance_of_coverages)`; high = tiles are evenly covered
83
+ - `complete?` — total_coverage >= `FULL_COVERAGE_THRESHOLD`
84
+ - `auto_connect` computes fit: `0.5 + shape_match(0.2 if same) + type_match(0.1 if same) + rand(0..0.2)`
85
+
86
+ ### `Helpers::TessellationEngine`
87
+
88
+ Registry for all tiles and mosaics.
89
+
90
+ - `create_tile(tile_type:, shape:, domain:, coverage:, fit_score:)` — auto-creates mosaic for domain; prunes lowest-coverage if over `MAX_TILES`
91
+ - `expand_tile(tile_id:, amount:)` — delegates to tile
92
+ - `shrink_all!` — shrinks every tile by `COVERAGE_DECAY`
93
+ - `connect_tiles(tile_a_id:, tile_b_id:)` — bidirectional adjacency
94
+ - `gapped_tiles` / `seamless_tiles` / `isolated_tiles` / `full_coverage_tiles` — filtered tile lists
95
+ - `overall_coverage` / `overall_fit` / `overall_coherence` — aggregate scores
96
+ - `gap_density` — ratio of gapped tiles
97
+ - `tessellation_report` — full report with all counts and labels
98
+
99
+ ## Runners
100
+
101
+ Module: `Legion::Extensions::CognitiveTessellation::Runners::CognitiveTessellation`
102
+
103
+ | Runner | Key Args | Returns |
104
+ |---|---|---|
105
+ | `create_tile` | `tile_type:`, `shape:`, `domain:`, `coverage:`, `fit_score:` | `{ success:, tile: }` |
106
+ | `expand_tile` | `tile_id:`, `amount:` | `{ success:, tile: }` or `{ success: false, error: }` |
107
+ | `connect_tiles` | `tile_a_id:`, `tile_b_id:` | `{ success:, connected:, tile_a:, tile_b: }` or error |
108
+ | `shrink_all` | — | `{ success: true }` |
109
+ | `list_gaps` | — | `{ success:, count:, tiles: }` |
110
+ | `tessellation_status` | — | `{ success:, total_tiles:, overall_coverage:, gap_density:, mosaics:, ... }` |
111
+
112
+ Note: the runner uses `@default_engine` directly (not `engine || @default_engine` for all runners — `create_tile` uses the pattern but some others don't initialize it lazily).
113
+
114
+ ## Integration Points
115
+
116
+ - No actors defined; `shrink_all` should be called periodically as a decay tick
117
+ - `list_gaps` identifies knowledge domains needing expansion — can drive learning prioritization
118
+ - Mosaics are auto-created per domain when the first tile for a domain is created
119
+ - All state is in-memory per `TessellationEngine` instance
120
+
121
+ ## Development Notes
122
+
123
+ - The runner accesses `@default_engine` directly without the `||=` pattern in some methods — the engine must be initialized before those runners are called (via `create_tile` or another `||=` path)
124
+ - `compute_fit` in Mosaic includes `rand * 0.2` — fit scores have a random component introduced at tile connection time
125
+ - `overlapping?` requires BOTH high fit score AND more than 3 adjacents — fit score alone is not sufficient
126
+ - `auto_connect` connects new tiles to the last 2 tiles in the same domain only; tiles added to the same-domain mosaic but not via the engine are not connected
127
+ - `prune_tiles` removes the lowest-effective-coverage tiles when over `MAX_TILES`
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75'
9
+ gem 'rubocop-rspec'
10
+
11
+ gem 'legion-gaia', path: '../../legion-gaia'
data/Gemfile.lock ADDED
@@ -0,0 +1,78 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ lex-cognitive-tessellation (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-cognitive-tessellation!
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,56 @@
1
+ # lex-cognitive-tessellation
2
+
3
+ A LegionIO cognitive architecture extension that models knowledge coverage as a tessellation. Tiles represent cognitive units (knowledge, skills, patterns, beliefs) that fit together across domains, growing through expansion and fading through decay.
4
+
5
+ ## What It Does
6
+
7
+ Tracks **tiles** of eight types (`knowledge`, `skill`, `pattern`, `belief`, `heuristic`, `schema`, `model`, `framework`) across eight domains (`cognitive`, `emotional`, `procedural`, `semantic`, `episodic`, `social`, `creative`, `analytical`).
8
+
9
+ Each tile has coverage (how much of its domain it explains) and a fit score (how well it integrates with neighbors). Tiles auto-organize into per-domain mosaics when created. The system surfaces gaps, isolated tiles, and coherence across the knowledge landscape.
10
+
11
+ ## Usage
12
+
13
+ ```ruby
14
+ require 'lex-cognitive-tessellation'
15
+
16
+ client = Legion::Extensions::CognitiveTessellation::Client.new
17
+
18
+ # Create a knowledge tile
19
+ t1 = client.create_tile(tile_type: :knowledge, shape: :hexagonal, domain: :semantic, coverage: 0.4)
20
+ # => { success: true, tile: { id: "uuid...", coverage: 0.4, fit_score: 0.5, gapped: false, seamless: false, ... } }
21
+
22
+ t2 = client.create_tile(tile_type: :schema, shape: :hexagonal, domain: :semantic, coverage: 0.6)
23
+ t3 = client.create_tile(tile_type: :knowledge, shape: :square, domain: :procedural, coverage: 0.2)
24
+
25
+ # Expand a tile's coverage
26
+ client.expand_tile(tile_id: t1[:tile][:id], amount: 0.1)
27
+ # => { success: true, tile: { coverage: 0.5, ... } }
28
+
29
+ # Explicitly connect two tiles
30
+ client.connect_tiles(tile_a_id: t1[:tile][:id], tile_b_id: t2[:tile][:id])
31
+ # => { success: true, connected: true, tile_a: "uuid...", tile_b: "uuid..." }
32
+
33
+ # Find tiles with poor fit (gaps)
34
+ client.list_gaps
35
+ # => { success: true, count: 0, tiles: [] }
36
+
37
+ # Decay all tiles (periodic maintenance)
38
+ client.shrink_all
39
+ # => { success: true }
40
+
41
+ # Full tessellation report
42
+ client.tessellation_status
43
+ # => { success: true, total_tiles: 3, overall_coverage: 0.35, coverage_label: :sparse, gap_density: 0.0, mosaics: [...], ... }
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,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/cognitive_tessellation/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-cognitive-tessellation'
7
+ spec.version = Legion::Extensions::CognitiveTessellation::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+ spec.summary = 'Cognitive pattern tiling and coverage analysis for LegionIO agents'
11
+ spec.description = 'Models how cognitive patterns tile together to cover conceptual space ' \
12
+ 'detecting gaps, overlaps, and coherence in knowledge mosaics'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-cognitive-tessellation'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata = {
18
+ 'homepage_uri' => spec.homepage,
19
+ 'source_code_uri' => spec.homepage,
20
+ 'documentation_uri' => "#{spec.homepage}/blob/master/README.md",
21
+ 'changelog_uri' => "#{spec.homepage}/blob/master/CHANGELOG.md",
22
+ 'bug_tracker_uri' => "#{spec.homepage}/issues",
23
+ 'rubygems_mfa_required' => 'true'
24
+ }
25
+
26
+ spec.files = Dir.chdir(__dir__) { `git ls-files -z`.split("\x0") }
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTessellation
6
+ class Client
7
+ include Runners::CognitiveTessellation
8
+
9
+ def initialize
10
+ @default_engine = Helpers::TessellationEngine.new
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTessellation
6
+ module Helpers
7
+ module Constants
8
+ MAX_TILES = 500
9
+ MAX_MOSAICS = 50
10
+
11
+ DEFAULT_COVERAGE = 0.0
12
+ FULL_COVERAGE_THRESHOLD = 0.95
13
+ GAP_THRESHOLD = 0.3
14
+ OVERLAP_THRESHOLD = 0.7
15
+ FIT_TOLERANCE = 0.05
16
+
17
+ COVERAGE_GROWTH = 0.08
18
+ COVERAGE_DECAY = 0.02
19
+ OVERLAP_PENALTY = 0.05
20
+
21
+ TILE_SHAPES = %i[
22
+ triangular square hexagonal pentagonal
23
+ irregular fractal amorphous crystalline
24
+ ].freeze
25
+
26
+ DOMAINS = %i[
27
+ cognitive emotional procedural semantic
28
+ episodic social creative analytical
29
+ ].freeze
30
+
31
+ TILE_TYPES = %i[
32
+ knowledge skill pattern belief
33
+ heuristic schema model framework
34
+ ].freeze
35
+
36
+ COVERAGE_LABELS = {
37
+ (0.9..) => :complete,
38
+ (0.7...0.9) => :substantial,
39
+ (0.5...0.7) => :partial,
40
+ (0.3...0.5) => :sparse,
41
+ (..0.3) => :fragmented
42
+ }.freeze
43
+
44
+ FIT_LABELS = {
45
+ (0.8..) => :seamless,
46
+ (0.6...0.8) => :snug,
47
+ (0.4...0.6) => :loose,
48
+ (0.2...0.4) => :gapped,
49
+ (..0.2) => :disjointed
50
+ }.freeze
51
+
52
+ DENSITY_LABELS = {
53
+ (0.8..) => :saturated,
54
+ (0.6...0.8) => :dense,
55
+ (0.4...0.6) => :moderate,
56
+ (0.2...0.4) => :thin,
57
+ (..0.2) => :vacant
58
+ }.freeze
59
+
60
+ def self.label_for(labels, value)
61
+ labels.each { |range, label| return label if range.cover?(value) }
62
+ :unknown
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTessellation
6
+ module Helpers
7
+ class Mosaic
8
+ include Constants
9
+
10
+ attr_reader :id, :domain, :tiles, :created_at
11
+
12
+ def initialize(domain:)
13
+ @id = SecureRandom.uuid
14
+ @domain = domain.to_sym
15
+ @tiles = []
16
+ @created_at = Time.now.utc
17
+ end
18
+
19
+ def add_tile(tile)
20
+ @tiles << tile
21
+ auto_connect(tile)
22
+ tile
23
+ end
24
+
25
+ def total_coverage
26
+ return 0.0 if @tiles.empty?
27
+
28
+ raw = @tiles.sum(&:effective_coverage) / @tiles.size
29
+ raw.clamp(0.0, 1.0).round(10)
30
+ end
31
+
32
+ def average_fit
33
+ return 0.0 if @tiles.empty?
34
+
35
+ (@tiles.sum(&:fit_score) / @tiles.size).round(10)
36
+ end
37
+
38
+ def gap_count = @tiles.count(&:gapped?)
39
+ def seamless_count = @tiles.count(&:seamless?)
40
+ def isolated_count = @tiles.count(&:isolated?)
41
+ def complete? = total_coverage >= FULL_COVERAGE_THRESHOLD
42
+
43
+ def coherence
44
+ return 0.0 if @tiles.empty?
45
+
46
+ connected = @tiles.count(&:well_connected?)
47
+ (connected.to_f / @tiles.size).round(10)
48
+ end
49
+
50
+ def uniformity
51
+ return 1.0 if @tiles.size <= 1
52
+
53
+ coverages = @tiles.map(&:coverage)
54
+ mean = coverages.sum / coverages.size
55
+ variance = coverages.sum { |c| (c - mean)**2 } / coverages.size
56
+ (1.0 - Math.sqrt(variance)).clamp(0.0, 1.0).round(10)
57
+ end
58
+
59
+ def to_h
60
+ {
61
+ id: @id,
62
+ domain: @domain,
63
+ tile_count: @tiles.size,
64
+ total_coverage: total_coverage,
65
+ average_fit: average_fit,
66
+ coherence: coherence,
67
+ uniformity: uniformity,
68
+ gaps: gap_count,
69
+ seamless: seamless_count,
70
+ complete: complete?
71
+ }
72
+ end
73
+
74
+ private
75
+
76
+ def auto_connect(new_tile)
77
+ same_domain = @tiles.select { |t| t.domain == new_tile.domain && t.id != new_tile.id }
78
+ same_domain.last(2).each do |neighbor|
79
+ new_tile.connect!(neighbor.id)
80
+ neighbor.connect!(new_tile.id)
81
+ compute_fit(new_tile, neighbor)
82
+ end
83
+ end
84
+
85
+ def compute_fit(tile_a, tile_b)
86
+ shape_match = tile_a.shape == tile_b.shape ? 0.2 : 0.0
87
+ type_match = tile_a.tile_type == tile_b.tile_type ? 0.1 : 0.0
88
+ base_fit = 0.5 + shape_match + type_match + (rand * 0.2)
89
+ tile_a.adjust_fit!(base_fit)
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module CognitiveTessellation
6
+ module Helpers
7
+ class TessellationEngine
8
+ include Constants
9
+
10
+ def initialize
11
+ @tiles = {}
12
+ @mosaics = {}
13
+ end
14
+
15
+ def create_tile(tile_type:, shape:, domain:, coverage: nil, fit_score: nil)
16
+ tile = Tile.new(tile_type: tile_type, shape: shape, domain: domain,
17
+ coverage: coverage, fit_score: fit_score)
18
+ @tiles[tile.id] = tile
19
+ mosaic = find_or_create_mosaic(domain: domain)
20
+ mosaic.add_tile(tile)
21
+ prune_tiles
22
+ tile
23
+ end
24
+
25
+ def expand_tile(tile_id:, amount: COVERAGE_GROWTH)
26
+ tile = @tiles[tile_id]
27
+ return nil unless tile
28
+
29
+ tile.expand!(amount)
30
+ tile
31
+ end
32
+
33
+ def shrink_all!
34
+ @tiles.each_value { |t| t.shrink!(COVERAGE_DECAY) }
35
+ end
36
+
37
+ def connect_tiles(tile_a_id:, tile_b_id:)
38
+ a = @tiles[tile_a_id]
39
+ b = @tiles[tile_b_id]
40
+ return nil unless a && b
41
+
42
+ a.connect!(b.id)
43
+ b.connect!(a.id)
44
+ { connected: true, tile_a: a.id, tile_b: b.id }
45
+ end
46
+
47
+ def tiles_by_domain(domain:) = @tiles.values.select { |t| t.domain == domain.to_sym }
48
+ def tiles_by_type(tile_type:) = @tiles.values.select { |t| t.tile_type == tile_type.to_sym }
49
+ def tiles_by_shape(shape:) = @tiles.values.select { |t| t.shape == shape.to_sym }
50
+ def gapped_tiles = @tiles.values.select(&:gapped?)
51
+ def seamless_tiles = @tiles.values.select(&:seamless?)
52
+ def isolated_tiles = @tiles.values.select(&:isolated?)
53
+ def full_coverage_tiles = @tiles.values.select(&:full_coverage?)
54
+
55
+ def overall_coverage
56
+ return 0.0 if @mosaics.empty?
57
+
58
+ (@mosaics.values.sum(&:total_coverage) / @mosaics.size).round(10)
59
+ end
60
+
61
+ def overall_fit
62
+ return 0.0 if @tiles.empty?
63
+
64
+ (@tiles.values.sum(&:fit_score) / @tiles.size).round(10)
65
+ end
66
+
67
+ def overall_coherence
68
+ return 0.0 if @mosaics.empty?
69
+
70
+ (@mosaics.values.sum(&:coherence) / @mosaics.size).round(10)
71
+ end
72
+
73
+ def domain_coverage
74
+ @mosaics.transform_values(&:total_coverage)
75
+ end
76
+
77
+ def gap_density
78
+ return 0.0 if @tiles.empty?
79
+
80
+ (gapped_tiles.size.to_f / @tiles.size).round(10)
81
+ end
82
+
83
+ def most_covered(limit: 5)
84
+ @tiles.values.sort_by { |t| -t.effective_coverage }.first(limit)
85
+ end
86
+
87
+ def least_covered(limit: 5)
88
+ @tiles.values.sort_by(&:effective_coverage).first(limit)
89
+ end
90
+
91
+ def tessellation_report
92
+ {
93
+ total_tiles: @tiles.size,
94
+ total_mosaics: @mosaics.size,
95
+ overall_coverage: overall_coverage,
96
+ coverage_label: Constants.label_for(COVERAGE_LABELS, overall_coverage),
97
+ overall_fit: overall_fit,
98
+ fit_label: Constants.label_for(FIT_LABELS, overall_fit),
99
+ overall_coherence: overall_coherence,
100
+ gap_density: gap_density,
101
+ gapped_count: gapped_tiles.size,
102
+ seamless_count: seamless_tiles.size,
103
+ isolated_count: isolated_tiles.size,
104
+ domain_coverage: domain_coverage,
105
+ mosaics: @mosaics.values.map(&:to_h)
106
+ }
107
+ end
108
+
109
+ def to_h
110
+ {
111
+ total_tiles: @tiles.size,
112
+ total_mosaics: @mosaics.size,
113
+ coverage: overall_coverage,
114
+ fit: overall_fit,
115
+ coherence: overall_coherence
116
+ }
117
+ end
118
+
119
+ private
120
+
121
+ def find_or_create_mosaic(domain:)
122
+ key = domain.to_sym
123
+ @mosaics[key] ||= Mosaic.new(domain: key)
124
+ end
125
+
126
+ def prune_tiles
127
+ return if @tiles.size <= MAX_TILES
128
+
129
+ sorted = @tiles.values.sort_by(&:effective_coverage)
130
+ to_remove = sorted.first(@tiles.size - MAX_TILES)
131
+ to_remove.each { |t| @tiles.delete(t.id) }
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end