legion-data 1.10.28 → 1.11.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec41889e66777b7940c00db2ed4f19ec704145ea27b149604ba82a168fed9e0b
4
- data.tar.gz: a1d0eec9f3cd9e84d63a4b1eb4e145330b4cdd6354d742ca6f003ea78558ca4c
3
+ metadata.gz: a3113304f2f0357f9ea74b932ffc50231616e8eca856e69f5f8c85cb62adad99
4
+ data.tar.gz: 3387584a5f01c29edcedccc27f1d4ef8a8b3d3147ab4272943a4902cef50e7c9
5
5
  SHA512:
6
- metadata.gz: b62c13c70c6330f24c17c92c645887f7d506ac83b7978cd33226ca198b0294d49823c0143d778cb3cd03e73e508b87b4b90c1ed229f3fd558e7c5f4b7c9a8710
7
- data.tar.gz: 3def1ace7b8172f1b8bd3b9ebd513fae7a65bc10cab304a5f6f7a593f6b44ef3431e93484c12af547b34961107fe02901d0930aefa6a8c8405501c95f7bf3de5
6
+ metadata.gz: 044e75084811c9b1d71b8364e81b01a7351d6cccc25e93b1866f8e13a55ec5ea50e053ebe63c35efc284acbf962cc6746d599dc8c3d858090e57510c0cc5d533
7
+ data.tar.gz: f1e3f19d5e29ef237ffb3a2a548d67e7f8b3932ddb9ecf6c7cb6119b59fa44a807c1f299109b82aec4510e260516dfcaf862c26c5c1700aa10d9cbe45bdc5d72
@@ -12,7 +12,7 @@ jobs:
12
12
  ci:
13
13
  permissions:
14
14
  contents: read
15
- uses: LegionIO/.github/.github/workflows/ci.yml@main
15
+ uses: LegionIO/.github/.github/workflows/ci.yml@e0fc7364b73f446d4cb9bdc162a66ed34bf0daa7
16
16
 
17
17
  ci-postgres:
18
18
  name: "RSpec (PostgreSQL)"
@@ -61,7 +61,7 @@ jobs:
61
61
  permissions:
62
62
  contents: read
63
63
  security-events: write
64
- uses: LegionIO/.github/.github/workflows/security-scan.yml@main
64
+ uses: LegionIO/.github/.github/workflows/security-scan.yml@e0fc7364b73f446d4cb9bdc162a66ed34bf0daa7
65
65
 
66
66
  version-changelog:
67
67
  permissions:
data/AGENTS.md CHANGED
@@ -8,13 +8,44 @@ Always run a full `bundle exec rspec` and `bundle exec rubocop -A` and fix all e
8
8
 
9
9
  HTTP APIs, runtime orchestration, extension behavior, and UI concerns belong in their owning repos. This repo should expose clean model contracts that those layers can call.
10
10
 
11
+ ## Implementation Rules
12
+
13
+ - Follow the existing Legion framework patterns. Reuse the established implementation instead of inventing a replacement, generic abstraction, or convenience layer.
14
+ - Keep method-returned settings defaults directly in the existing methods. Do not extract the default/connection settings tree into a constant such as `CONNECTION_DEFAULTS`, a class variable, or a memoized snapshot. Register defaults at boot and read live values through `Legion::Settings`.
15
+ - Application-table reads and writes must use real, named Sequel models. Do not substitute a raw-database facade or move `connection[table]` behind a differently named helper.
16
+ - When a Data-owned table lacks a required model, add a normal `class Name < Sequel::Model(:table_name)` in the existing model namespace and register it in `Legion::Data::Models.models`.
17
+ - Do not introduce `Class.new`, anonymous/dynamic models, fake models, or model-loading workarounds. Use the named model classes in tests too; bind them to the isolated test database and restore their original datasets afterward.
18
+ - Verify table ownership from its migration before adding a model. An existing reference is not proof that legion-data owns or creates the table. Check adapter restrictions as well as the owning repository.
19
+ - Respect startup order: connect, migrate, load models, mark Data connected, then cache and local setup. Do not require model declarations before the database and schema are ready or hide a load-order problem with fallback classes or guards.
20
+ - Use fully qualified model names across module boundaries. Return plain Hashes rather than leaking Sequel model instances across those boundaries.
21
+ - Preserve existing behavior and public contracts unless the authorized change requires updating them. When an API changes, migrate its callers and documentation; do not add compatibility shims.
22
+ - The async persistence contract assumes prefetch 1: one persistence message performs one model insert or update. Do not reintroduce batching, attempt ownership, leases, or transactional outbox coordination for that path.
23
+ - Raw database access is appropriate for migrations and database infrastructure, not a substitute for models in application-table CRUD. Do not expand a model conversion into excluded migration or connection-management work.
24
+
25
+ ## Scope and Interaction
26
+
27
+ - Work only in the repository and files the user authorizes. Do not sweep adjacent files, convert additional helpers, or redesign unrelated behavior.
28
+ - For per-file conversion work, finish the selected file before moving on: implement with the required models and caller/spec updates, verify, apply the requested patch-version and changelog bump, commit, and push to the existing branch. Then stop and wait for the next file.
29
+ - Necessary supporting changes are not permission for unrelated conversions. If the requested change needs work outside the authorized scope, identify the concrete dependency and obtain direction rather than improvising around it.
30
+ - When the user asks a question during work, answer that question first. Do not treat the question as permission for more edits or replace the answer with an action.
31
+ - Use direct Ruby, Bundler, and shell commands. Do not wrap ordinary project commands in Python or add orchestration scripts for simple edits, tests, commits, or pushes.
32
+ - Work in the existing development checkout and branch. Do not create another branch, worktree, clone, or PR without explicit direction. Never force-push.
33
+ - Preserve unrelated user changes. Stage explicit filenames only; never use `git add .` or `git add -A`.
34
+ - Do not claim completion before the scoped behavior is verified and the requested commit/push is finished. Report only checks actually run.
35
+
11
36
  ## Required Commands
12
37
 
13
38
  Run from the repo root:
14
39
 
40
+ Use an isolated temporary `HOME` for Data tests: `spec/spec_helper.rb` deletes its test database under `~/.legionio/data/`. Never run it against the developer's real HOME. Set `LEGION_DNS_BOOTSTRAP=false` and preserve the real `RBENV_ROOT` when using rbenv. Apply those variables to the test command, not the parent environment.
41
+
15
42
  ```bash
43
+ mkdir -p tmp
44
+ test_home="$(mktemp -d)"
16
45
  bundle exec rubocop -A
17
- bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
46
+ env HOME="$test_home" RBENV_ROOT="${RBENV_ROOT:-$HOME/.rbenv}" LEGION_DNS_BOOTSTRAP=false \
47
+ bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt \
48
+ > tmp/rspec_console.log 2>&1
18
49
  ```
19
50
 
20
51
  If RSpec fails, extract failures with:
@@ -25,6 +56,8 @@ jq '[.examples[] | select(.status != "passed") | {file_path, line_number, full_d
25
56
 
26
57
  Do not run partial RSpec or partial RuboCop for release validation.
27
58
 
59
+ For behavior changes, run the changed path against real models and an isolated database as a smoke check; a green suite alone is not proof of the changed path. Do not create mock-echo or default-pinning tests merely to restate the implementation.
60
+
28
61
  ## Migration Rules
29
62
 
30
63
  - Never edit published migrations. Add a new migration instead.
@@ -60,7 +93,7 @@ Rules:
60
93
 
61
94
  ## Current Schema Notes
62
95
 
63
- - Migrations currently run through `096`.
96
+ - Use `lib/legion/data/migrations/` as the authoritative migration inventory.
64
97
  - `074`-`076` are mainline Apollo/task/extract migrations.
65
98
  - `077`-`090` define the LLM lifecycle ledger.
66
99
  - `091`-`096` define portable identity companion tables.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Legion::Data Changelog
2
2
 
3
+ ## [1.11.7] - 2026-11-06
4
+
5
+ ### Changed
6
+ - Simplify `Connection.shutdown` to a direct disconnect: it closes the shared connection and query log and returns `true`. Pool drain ownership, `shutdown_status` reporting, and the `shutdown_timeout` setting were removed along with the `ConnectionPool` module. `Connection.connection_info` no longer reports the `shutdown` key.
7
+ - `Connection.setup` dev-mode fallback now rescues any connection failure (not only `Sequel::DatabaseConnectionError`) before falling back to SQLite.
8
+ - `Models.load` no longer rebinds loaded model datasets after connection setup.
9
+
10
+ ## [1.11.6] - 2026-11-05
11
+
12
+ ### Fixed
13
+ - Reject storage-tier source models connected to a different database before moving rows, preserving the single-database transaction boundary.
14
+
15
+ ## [1.11.5] - 2026-11-05
16
+
17
+ ### Changed
18
+ - Register the `DataArchive` Sequel model and use it for warm/cold storage operations. `StorageTiers.archive_to_warm` now accepts the source `model:` instead of `table:`; source selection and deletion use that model, and exported rows remain plain Hashes.
19
+
20
+ ## [1.11.4] - 2026-11-05
21
+
22
+ ### Changed
23
+ - Register the `AppendClaim` Sequel model and use it for database append locks instead of direct table access. Audit and event callers now supply only the claim key; retain existing transaction and adapter-specific locking behavior.
24
+
25
+ ## [1.11.3] - 2026-11-05
26
+
27
+ ### Changed
28
+ - Use the existing `AuditRecord` Sequel model for audit appends, chain verification, and queries instead of direct connection/table access. Preserve append locking, signatures, metadata, and hash-chain results.
29
+
30
+ ## [1.11.2] - 2026-11-05
31
+
32
+ ### Changed
33
+ - Register the `GovernanceEvent` Sequel model and use it for event-store appends, reads, and chain verification instead of direct table access. Preserve existing stream locking, sequence numbering, and hash-chain behavior.
34
+
35
+ ## [1.11.1] - 2026-11-05
36
+
37
+ ### Changed
38
+ - Persist extraction step timings through the existing `ExtractStepTiming` Sequel model instead of direct connection queries and bulk inserts.
39
+
40
+ ## [1.11.0] - 2026-11-03
41
+
42
+ ### Added
43
+ - Migration 139 provisions `task_logs` and registers its model. Both existing `TaskLog.insert` call shapes initialize UUIDs and event timestamps while preserving the integer return value; deleting parent records retains log evidence. Unsupported manually created tables fail without data loss and require operator reconciliation.
44
+
45
+ ### Changed
46
+ - Simplify task persistence to normal per-message model inserts or updates, assuming RabbitMQ prefetch of one and runtime acknowledgement after a successful write. Remove the added execution facade, attempt/dispatch models, leases, outbox coordination, execution-policy settings, and unmerged coordination schema migrations. Renumber the unmerged task-log migration to 139; migrations already on main remain unchanged.
47
+ - `Legion::Data::Task::Status` retains immutable persisted identities independently of Transport and Core, including scheduled tasks and terminal transformer exceptions.
48
+ - Preserve existing Task idempotency helpers: positional arguments, Sequel model returns, historical key lookup, optional TTL, and key reuse after terminal completion or expiry. Recognize canonical namespaced terminal statuses as well as legacy statuses, so completed runtime tasks do not retain active keys. Frozen timestamp payloads are canonicalized without mutation.
49
+ - Retain ordinary task archival eligibility without execution-specific exclusions.
50
+ - Read runtime configuration from the live `Legion::Settings[:data]` tree, including TLS, local migration permission, archival policy, and cache TTL. Register defaults once at library boot without resetting live connection state during reads.
51
+ - Define connection defaults directly in `Settings.default`, following the framework's method-returned settings Hash pattern; remove `CONNECTION_DEFAULTS`.
52
+
3
53
  ## [1.10.28] - 2026-10-30
4
54
 
5
55
  ### Changed
data/CLAUDE.md CHANGED
@@ -1,5 +1,3 @@
1
- Always run a full `bundle exec rspec` and `bundle exec rubocop -A` and fix all errors before committing.
2
-
3
1
  # legion-data
4
2
 
5
3
  Persistent storage gem for LegionIO. Owns Sequel database connections, numbered migrations, models, local SQLite state, extract timing persistence, audit/governance storage, identity/RBAC storage, Apollo storage, and the LLM lifecycle ledger.
@@ -7,8 +5,6 @@ Persistent storage gem for LegionIO. Owns Sequel database connections, numbered
7
5
  ## Commands
8
6
 
9
7
  ```bash
10
- bundle install
11
- bundle exec rubocop -A
12
8
  bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
13
9
  ```
14
10
 
@@ -23,7 +19,7 @@ jq '[.examples[] | select(.status != "passed") | {file_path, line_number, full_d
23
19
  - `lib/legion/data/connection.rb`: Sequel connection setup, diagnostics, fallback, query logging.
24
20
  - `lib/legion/data/migration.rb`: numbered Sequel migrations.
25
21
  - `lib/legion/data/model.rb`: shared model loader.
26
- - `lib/legion/data/models/`: flat and namespaced Sequel model classes.
22
+ - `lib/legion/data/models/`: flat and namespaced Sequel model classes (`Identity::*`, `Apollo::*`, `RBAC::*`, `LLM::*`).
27
23
  - `lib/legion/data/local.rb`: local SQLite database for on-node state.
28
24
  - `lib/legion/data/extract.rb`: text extraction and persisted extract step timings.
29
25
  - `lib/legion/data/spool.rb`: filesystem write buffer when DB writes are unavailable.
@@ -95,20 +91,6 @@ Sequel.migration do
95
91
  end
96
92
  ```
97
93
 
98
- ### Column Option Reference
99
-
100
- | Option | Purpose |
101
- |--------|---------|
102
- | `:null` | `false` = NOT NULL, `true` = nullable |
103
- | `:default` | Default value (use `Sequel::CURRENT_TIMESTAMP` for timestamps) |
104
- | `:index` | `true` creates an index on this column; pass a Hash for index options |
105
- | `:unique` | `true` adds a UNIQUE constraint |
106
- | `:on_delete` | FK behavior: `:cascade`, `:set_null`, `:restrict`, `:no_action` |
107
- | `:on_update` | FK behavior: `:cascade`, `:set_null`, `:restrict`, `:no_action` |
108
- | `:key` | For FKs — the referenced column (unnecessary if referencing primary key) |
109
- | `:size` | Column width for String/Decimal |
110
- | `:text` | `true` for TEXT columns (unlimited length) |
111
-
112
94
  ### Foreign Key Conventions
113
95
 
114
96
  ```ruby
@@ -191,17 +173,6 @@ All new tables in legion-data should follow this column convention. Required fie
191
173
  - The denormalized string field is always `identity_canonical_name` — not `canonical_name`, `actor`, `agent_id`, or `identity_name`.
192
174
  - Existing columns (`agent_id`, `source_agent`, `submitted_by`, `actor`, etc.) on pre-existing tables are **not renamed or removed** — they are historical record and intentionally left as-is. New identity columns are purely additive.
193
175
 
194
- ## Current Schema Landmarks
195
-
196
- - `074`-`076`: Apollo field width, task idempotency, extract step timings.
197
- - `077`-`090`: LLM lifecycle ledger.
198
- - `091`-`096`: portable identity companion tables.
199
- - `097`: LLM dispatch fields (operation, correlation_id, provider_instance, dispatch_path).
200
- - `098`-`099`: Legacy identity table drop + rename (portable_identity_* → identity_*).
201
- - `100`-`102`: Apollo identity columns + access_scope + indexes.
202
- - `103`-`114`: LLM table identity standardization (access_scope, identity_principal_id, identity_id, identity_canonical_name).
203
- - Namespaced models: `Identity::*`, `Apollo::*`, `RBAC::*`, `LLM::*`.
204
-
205
176
  ## Boundaries
206
177
 
207
178
  - REST APIs belong in LegionIO, not this gem.
data/Gemfile CHANGED
@@ -4,6 +4,9 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ settings_path = File.expand_path('../legion-settings', __dir__)
8
+ gem 'legion-settings', path: settings_path if File.directory?(settings_path)
9
+
7
10
  group :test do
8
11
  gem 'rake'
9
12
  gem 'rspec'
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # legion-data
2
2
 
3
- Persistent database storage for the [LegionIO](https://github.com/LegionIO/LegionIO) async job engine and AI coding assistant platform. Provides database connectivity via the [Sequel ORM](https://sequel.jeremyevans.net/), automatic schema migrations (97 numbered migrations), Sequel models for the full LegionIO control plane, and a parallel local SQLite database for on-node agentic cognitive state.
3
+ Persistent database storage for the [LegionIO](https://github.com/LegionIO/LegionIO) async job engine and AI coding assistant platform. Provides database connectivity via the [Sequel ORM](https://sequel.jeremyevans.net/), numbered schema migrations, Sequel models for the full LegionIO control plane, and a parallel local SQLite database for on-node agentic cognitive state.
4
4
 
5
- **Version**: 1.10.18 | **Ruby**: >= 3.4 | **License**: Apache-2.0
5
+ **Version**: 1.11.6 | **Ruby**: >= 3.4 | **License**: Apache-2.0
6
6
 
7
7
  ---
8
8
 
@@ -25,6 +25,30 @@ The schema is portable by default across SQLite, MySQL, and PostgreSQL. PostgreS
25
25
 
26
26
  `Legion::Data.setup` raises on fatal connection configuration, migration, or model-loading failures rather than announcing successful startup. Failed setup closes its database connections and clears readiness; fix the underlying error before retrying. Local migrations registered after startup also propagate failures and leave the local database disconnected until setup succeeds.
27
27
 
28
+ ## Framework Conventions
29
+
30
+ ### Startup and Table Ownership
31
+
32
+ The shared database starts in this order: **connect → run migrations → load models → mark Data connected**. Cache setup and local storage setup follow. Application code uses the models registered by that lifecycle; model declarations must not be loaded before their tables are migrated.
33
+
34
+ A table belongs to the component that creates and maintains its schema. A reference in an existing helper does not establish ownership. Before adding a model, identify the owning migration and its supported adapters. A PostgreSQL-only migration does not create its table on SQLite or MySQL.
35
+
36
+ ### Model-backed Table Access
37
+
38
+ - Read and write application tables through named Sequel models, such as `Legion::Data::Model::Task`, rather than `connection[:tasks]` or `db[:tasks]`.
39
+ - If a Data-owned table is used but lacks a model, add a normal named class under `lib/legion/data/models/` and register it in `Legion::Data::Models.models`.
40
+ - Use the existing model namespace and association conventions. Do not replace named models with `Class.new`, anonymous models, or a facade around raw table access.
41
+ - Shared operations can accept an explicit source model rather than infer a class from a table name. Keep transactional source and destination models on the same database.
42
+ - Keep model instances within database operations. Return plain Hashes across module boundaries, using `.values` or an explicit result Hash.
43
+
44
+ Migrations and database infrastructure still need direct Sequel database operations for schema changes, connection setup, and adapter/session management. Those operations are distinct from application-table reads and writes.
45
+
46
+ ### Settings
47
+
48
+ Keep method-returned settings defaults in the existing methods, including `Legion::Data::Settings.default`. Do not extract that settings tree into a constant such as `CONNECTION_DEFAULTS`, a class variable, or a memoized snapshot.
49
+
50
+ Register defaults through Legion Settings at library startup. Runtime code reads the current `Legion::Settings[:data]` values so configured overrides and settings reloads remain authoritative. Follow the framework's existing loading and access patterns rather than adding a second settings mechanism.
51
+
28
52
  ## Supported Databases
29
53
 
30
54
  | Database | Adapter | Gem | Default |
@@ -73,9 +97,9 @@ Legion::Data (singleton module)
73
97
  │ ├── .connection_info # Adapter, liveness, and fallback diagnostics
74
98
  │ ├── .fallback_active? # True when dev fallback moved a network DB to SQLite
75
99
  │ ├── .stats # Pool metrics, tuning snapshot, adapter-specific DB stats
76
- │ └── .shutdown # Drain accepted work; return true only after cleanup
100
+ │ └── .shutdown # Disconnect shared connection; close query log
77
101
  │
78
- ├── Migration # Auto-migration system (97 numbered Sequel DSL migrations)
102
+ ├── Migration # Numbered Sequel DSL migrations
79
103
  │
80
104
  ├── Model # Sequel model autoloader
81
105
  │ └── Models: Extension, Function, Runner, Node, Task, TaskLog, Setting,
@@ -151,13 +175,9 @@ Legion::Data::Connection.connection_info
151
175
  Legion::Data.shutdown
152
176
  ```
153
177
 
154
- ### Graceful Database Shutdown
155
-
156
- `Legion::Data.shutdown` returns `true` when the shared pool has drained and its connections and query logger are closed. An idle pool closes immediately. Once shutdown starts, new checkouts are rejected; work already accepted by the pool may finish, including nested checkouts in the same execution context.
178
+ ### Database Shutdown
157
179
 
158
- If `shutdown_timeout` expires, shutdown returns `false` rather than claiming active connections were forcibly closed. The last accepted checkout performs deferred cleanup. `Connection.connection_info[:shutdown]` reports `state` (`open`, `draining`, `closing`, `closed`, or `failed`) and `active_requests`, which counts accepted execution contexts, including pool waiters—not physical connections. A cleanup failure is logged and can be retried by calling shutdown again.
159
-
160
- Shutdown does not cancel SQL, kill worker threads, or close borrowed connections. Configure database statement timeouts separately to bound query execution. To resume work after completed shutdown, call `Legion::Data.setup`; it creates a new pool and rebinds loaded models.
180
+ `Legion::Data.shutdown` disconnects the shared connection, closes the query log, and marks Data as disconnected. It does not cancel in-flight SQL — configure database statement timeouts separately to bound query execution. To resume work, call `Legion::Data.setup`; it creates a new connection.
161
181
 
162
182
  ### Model Associations
163
183
 
@@ -215,21 +235,37 @@ Supported formats: `.txt`, `.md`, `.csv`, `.json`, `.jsonl`, `.html`, `.xlsx`, `
215
235
  When migration 076 is present, Extract also persists the same per-step timing rows to `extract_step_timings`
216
236
  under the returned `extract_id`.
217
237
 
218
- ### Task Idempotency
238
+ ### Task Persistence
219
239
 
220
- `Task.idempotency_key_for` computes a stable SHA-256 key from canonical JSON payloads. `Task.create_idempotent`
221
- returns an existing non-terminal task for the same key inside the optional TTL window, or creates a new task
222
- with `idempotency_key` and `idempotency_expires_at` populated:
240
+ `Legion::Data::Model::Task` is the normal Sequel model for task persistence.
241
+ The message-processing contract assumes RabbitMQ prefetch of one: each persistence message
242
+ performs one model insert or update, and the runtime acknowledges the message only after
243
+ that operation succeeds. A failed write must not be acknowledged as successful.
244
+ Data does not add message batching, attempt ownership, leases, or outbox coordination.
223
245
 
224
- ```ruby
225
- task = Legion::Data::Model::Task.create_idempotent(
226
- { status: 'pending', payload: Legion::JSON.dump(payload) },
227
- payload: payload,
228
- ttl: 300
229
- )
230
- ```
246
+ Task statuses retain their existing persisted identities in `Legion::Data::Task::Status`.
247
+ Task rows follow the ordinary archival age, status, and dependency rules.
248
+
249
+ The existing model idempotency API is unchanged:
250
+
251
+ - `Legion::Data::Model::Task.idempotency_key_for(payload)` computes a stable SHA-256 key from canonical JSON.
252
+ - `create_idempotent(values, payload: nil, idempotency_key: nil, ttl: nil)` accepts a positional
253
+ values Hash and returns a Sequel model. Without `ttl`, it does not add an expiry.
254
+ - `find_active_by_idempotency_key(key, now: Time.now)` reads the original `idempotency_key`
255
+ column and returns a Sequel model or nil. Terminal tasks and expired keys allow a new occurrence.
256
+
257
+ These helpers do not promise concurrent deduplication or permanent execution identity.
258
+ Frozen timestamp payloads are canonicalized without mutation.
231
259
 
232
- Terminal tasks and expired TTLs allow a replacement. This database helper uses a lookup followed by an insert; concurrent callers are not guaranteed to receive the same task. It does not provide RabbitMQ delivery or execution deduplication.
260
+ Migration 139 supplies the previously unprovisioned `task_logs` table and the normal model
261
+ loader registers `Legion::Data::Model::TaskLog`. Both `TaskLog.create` and `TaskLog.insert`
262
+ generate its public UUID and retain supplied event timestamps. `insert` supports the existing
263
+ positional Hash and keyword-Hash call shapes and returns the inserted integer ID; `create`
264
+ returns a model. Entries are text and may reference a task, node, or function. Removing those
265
+ referenced records nulls the reference rather than deleting the
266
+ log evidence. An unsupported manually created `task_logs` table requires operator
267
+ reconciliation before upgrading: migration 139 fails safely if it already exists and never
268
+ overwrites, truncates, or automatically adopts existing logs.
233
269
 
234
270
  ### Serialized Audit and Governance Appends
235
271
 
@@ -237,6 +273,16 @@ After migration 138, `AuditRecord.append` serializes each chain and `EventStore.
237
273
 
238
274
  Audit tails, verification, and walks use append IDs rather than timestamps, preserving chain order across writer clock skew. Governance events retain their sequential stream numbering. PostgreSQL lock/statement timeouts and native adapter lock limits bound contention; timeout and unrelated database errors propagate rather than being retried indefinitely. MySQL claims use exclusive duplicate-key locking to avoid InnoDB shared-lock upgrade deadlocks.
239
275
 
276
+ ### Model-backed Storage Tiers
277
+
278
+ ```ruby
279
+ Legion::Data::StorageTiers.archive_to_warm(
280
+ model: Legion::Data::Model::Task, age_days: 90, batch_size: 1000
281
+ )
282
+ ```
283
+
284
+ Pass the source Sequel model using `model:`, not a table name. The source must have `id` and `created_at` columns and use the same database as `Legion::Data::Model::DataArchive`. Selected rows are copied as JSON and deleted in the existing transaction. `export_to_cold` returns plain archived-row hashes; `stats` reports warm/cold counts.
285
+
240
286
  ### Bounded Retention
241
287
 
242
288
  ```ruby
@@ -361,6 +407,8 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
361
407
 
362
408
  ### Full Configuration Reference
363
409
 
410
+ Defaults are registered when the library loads; runtime consumers read the current `Legion::Settings[:data]` tree. User configuration and subsequent settings reloads take precedence, including explicit `false` migration/TLS flags and empty archival table lists. Connection options apply on the next connection setup, while local migration registration and `Archival.archive!` (without an explicit policy) read the current configuration on each call. A full `Legion::Settings.reset!` requires explicit library-default registration again before using Data.
411
+
364
412
  ```json
365
413
  {
366
414
  "data": {
@@ -390,6 +438,15 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
390
438
  "replicas": [],
391
439
 
392
440
  "creds": { "database": "legionio.db" },
441
+ "tls": { "enabled": false },
442
+ "archival": {
443
+ "retention_days": 90,
444
+ "warm_after_days": 7,
445
+ "cold_after_days": 90,
446
+ "batch_size": 1000,
447
+ "tables": ["tasks", "metering_records"],
448
+ "storage_backend": null
449
+ },
393
450
 
394
451
  "migrations": {
395
452
  "continue_on_fail": false,
@@ -582,7 +639,7 @@ The `Legion::Data::Model::Identity::*`, `Apollo::*`, and `RBAC::*` namespaces pr
582
639
  | `csv` (>= 3.2) | CSV extraction handler |
583
640
  | `legion-json` | JSON serialization via Legion::JSON |
584
641
  | `legion-logging` (>= 1.5.0) | Structured logging |
585
- | `legion-settings` (>= 1.3.26) | Configuration management |
642
+ | `legion-settings` (>= 1.4.5) | Configuration management |
586
643
  | `mysql2` (>= 0.5.5) | MySQL adapter (optional) |
587
644
  | `pg` (>= 1.5) | PostgreSQL adapter (optional) |
588
645
 
@@ -590,7 +647,7 @@ The `Legion::Data::Model::Identity::*`, `Apollo::*`, and `RBAC::*` namespaces pr
590
647
 
591
648
  ## Migrations
592
649
 
593
- 97 numbered Sequel DSL migrations run automatically on startup (`auto_migrate: true`). Key milestones:
650
+ Numbered Sequel DSL migrations run automatically on startup (`auto_migrate: true`). The files in `lib/legion/data/migrations/` are the authoritative inventory. Key milestones:
594
651
 
595
652
  | Range | What was added |
596
653
  |-------|---------------|
@@ -666,10 +723,20 @@ bundle exec legionio_migrate
666
723
  git clone https://github.com/LegionIO/legion-data
667
724
  cd legion-data
668
725
  bundle install
669
- bundle exec rspec # all tests must pass
726
+ bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
670
727
  bundle exec rubocop -A # zero offenses expected
671
728
  ```
672
729
 
730
+ Task-execution lifecycle and concurrency regressions use temporary SQLite storage by default.
731
+ To exercise the same lifecycle suite on PostgreSQL, set `LEGION_DATA_ADAPTER=postgres` and
732
+ `LEGION_DATA_HOST`, `LEGION_DATA_PORT`, `LEGION_DATA_USER`, `LEGION_DATA_PASSWORD`, and
733
+ `LEGION_DATA_DATABASE` for a **disposable test database**, then run the full RSpec command
734
+ above. These examples create and remove a random isolated schema, applying the actual task
735
+ migrations with minimal foreign-key fixtures. This verifies PostgreSQL ownership locks,
736
+ savepoint rollback, unique identities, and reconnect behavior; it does not claim coverage of
737
+ the unrelated historical `pgvector` migration chain. Other suites retain their own database
738
+ fixtures.
739
+
673
740
  This repo also includes a pre-commit configuration:
674
741
 
675
742
  ```bash
data/legion-data.gemspec CHANGED
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.add_dependency 'csv', '>= 3.2'
30
30
  spec.add_dependency 'legion-json'
31
31
  spec.add_dependency 'legion-logging', '>= 1.5.0'
32
- spec.add_dependency 'legion-settings', '>= 1.3.26'
32
+ spec.add_dependency 'legion-settings', '>= 1.4.5'
33
33
  spec.add_dependency 'sequel', '>= 5.70'
34
34
  spec.add_dependency 'sqlite3', '>= 2.0'
35
35
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'legion/logging/helper'
4
-
5
3
  module Legion
6
4
  module Data
7
5
  module Archival
@@ -39,21 +37,8 @@ module Legion
39
37
  Time.now - (cold_after_days * 86_400)
40
38
  end
41
39
 
42
- class << self
43
- include Legion::Logging::Helper
44
- end
45
-
46
40
  def self.from_settings
47
- return new unless defined?(Legion::Settings)
48
-
49
- data_settings = Legion::Settings[:data]
50
- archival = data_settings.is_a?(Hash) ? data_settings[:archival] : nil
51
- return new unless archival.is_a?(Hash)
52
-
53
- new(**archival.slice(:warm_after_days, :cold_after_days, :batch_size, :tables))
54
- rescue StandardError => e
55
- handle_exception(e, level: :warn, handled: true, operation: :policy_from_settings)
56
- new
41
+ new(**Legion::Settings[:data][:archival].slice(:warm_after_days, :cold_after_days, :batch_size, :tables))
57
42
  end
58
43
  end
59
44
  end
@@ -19,7 +19,7 @@ module Legion
19
19
  class << self
20
20
  include Legion::Logging::Helper
21
21
 
22
- def archive!(policy: Policy.new, dry_run: false)
22
+ def archive!(policy: Policy.from_settings, dry_run: false)
23
23
  log.info "Archival run started dry_run=#{dry_run} tables=#{policy.tables.size}"
24
24
  results = {}
25
25
  policy.tables.each do |table_name|
@@ -115,7 +115,8 @@ module Legion
115
115
  end
116
116
 
117
117
  def archive_batch(conn:, table:, cutoff:, batch_size:, now:, storage_backend:)
118
- rows = conn[table].where { created_at < cutoff }.limit(batch_size).all
118
+ candidates = conn[table].where { created_at < cutoff }.limit(batch_size)
119
+ rows = candidates.all
119
120
  return if rows.empty?
120
121
 
121
122
  batch_id = SecureRandom.uuid
@@ -21,18 +21,16 @@ module Legion
21
21
  # @param metadata [Hash] optional structured context (serialised to JSON)
22
22
  # @param sign [Boolean] when true, attempt signing via legion-crypt
23
23
  def append(chain_id:, content_type:, content_hash:, metadata: {}, sign: false)
24
- return { error: 'db unavailable' } unless db_ready?
24
+ return { error: 'db unavailable' } unless Legion::Data.connected?
25
25
 
26
- conn = Legion::Data.connection
27
- Legion::Data::DatabaseLock.with_claim(database: conn, table: :append_claims,
28
- key: { kind: 'audit', stream_id: chain_id }) do
29
- parent_hash = latest_chain_hash(conn, chain_id)
26
+ Legion::Data::DatabaseLock.with_claim(key: { kind: 'audit', stream_id: chain_id }) do
27
+ parent_hash = latest_chain_hash(chain_id: chain_id)
30
28
  ts = truncate_to_us(Time.now)
31
29
  ch = compute_chain_hash(parent_hash, content_hash, ts, content_type)
32
30
  sig = sign ? sign_record(ch) : nil
33
31
  meta_json = metadata.empty? ? nil : Legion::JSON.dump(metadata)
34
32
 
35
- id = conn[:audit_records].insert(
33
+ record = Legion::Data::Model::AuditRecord.create(
36
34
  chain_id: chain_id,
37
35
  content_type: content_type,
38
36
  content_hash: content_hash,
@@ -43,8 +41,8 @@ module Legion
43
41
  created_at: ts
44
42
  )
45
43
 
46
- log.debug "AuditRecord append: chain=#{chain_id} type=#{content_type} id=#{id}"
47
- { id: id, chain_id: chain_id, chain_hash: ch, parent_hash: parent_hash }
44
+ log.debug "AuditRecord append: chain=#{chain_id} type=#{content_type} id=#{record.id}"
45
+ { id: record.id, chain_id: chain_id, chain_hash: ch, parent_hash: parent_hash }
48
46
  end
49
47
  end
50
48
 
@@ -54,12 +52,12 @@ module Legion
54
52
  # @param chain_id [String]
55
53
  # @return [Hash] { valid: Boolean, length: Integer, broken_at: Integer? }
56
54
  def verify(chain_id:)
57
- return { valid: false, error: 'db unavailable' } unless db_ready?
55
+ return { valid: false, error: 'db unavailable' } unless Legion::Data.connected?
58
56
 
59
- records = Legion::Data.connection[:audit_records]
60
- .where(chain_id: chain_id)
61
- .order(:id)
62
- .all
57
+ records = Legion::Data::Model::AuditRecord
58
+ .where(chain_id: chain_id)
59
+ .order(:id)
60
+ .all
63
61
 
64
62
  prev_hash = GENESIS_HASH
65
63
  records.each do |r|
@@ -86,9 +84,9 @@ module Legion
86
84
  # @param since [Time, nil] optional lower bound on created_at
87
85
  # @param limit [Integer]
88
86
  def walk(chain_id:, since: nil, limit: 1000)
89
- return [] unless db_ready?
87
+ return [] unless Legion::Data.connected?
90
88
 
91
- ds = Legion::Data.connection[:audit_records].where(chain_id: chain_id)
89
+ ds = Legion::Data::Model::AuditRecord.where(chain_id: chain_id)
92
90
  ds = ds.where { created_at >= since } if since
93
91
  ds.order(:id).limit(limit).all.map { |r| deserialize(r) }
94
92
  end
@@ -99,9 +97,9 @@ module Legion
99
97
  # @param since [Time, nil]
100
98
  # @param limit [Integer]
101
99
  def query_by_type(content_type:, since: nil, limit: 100)
102
- return [] unless db_ready?
100
+ return [] unless Legion::Data.connected?
103
101
 
104
- ds = Legion::Data.connection[:audit_records].where(content_type: content_type)
102
+ ds = Legion::Data::Model::AuditRecord.where(content_type: content_type)
105
103
  ds = ds.where { created_at >= since } if since
106
104
  ds.order(Sequel.desc(:created_at)).limit(limit).all.map { |r| deserialize(r) }
107
105
  end
@@ -137,12 +135,12 @@ module Legion
137
135
  ::Time.at(Rational(us, 1_000_000))
138
136
  end
139
137
 
140
- def latest_chain_hash(conn, chain_id)
141
- records = conn[:audit_records]
138
+ def latest_chain_hash(chain_id:)
139
+ records = Legion::Data::Model::AuditRecord
142
140
  .select(:chain_hash)
143
141
  .where(chain_id: chain_id)
144
142
  .order(Sequel.desc(:id))
145
- records = records.for_update unless conn.database_type == :sqlite
143
+ records = records.for_update unless Legion::Data::Model::AuditRecord.db.database_type == :sqlite
146
144
  last = records.first
147
145
  last ? last[:chain_hash] : GENESIS_HASH
148
146
  end
@@ -169,13 +167,6 @@ module Legion
169
167
  created_at: row[:created_at]
170
168
  }
171
169
  end
172
-
173
- def db_ready?
174
- defined?(Legion::Data) && Legion::Data.connection&.table_exists?(:audit_records)
175
- rescue StandardError => e
176
- handle_exception(e, level: :warn, handled: true, operation: :audit_record_db_ready?)
177
- false
178
- end
179
170
  end
180
171
  end
181
172
  end