legion-data 1.10.17 → 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 +4 -4
- data/.github/workflows/ci.yml +2 -2
- data/AGENTS.md +35 -2
- data/CHANGELOG.md +109 -0
- data/CLAUDE.md +1 -30
- data/Gemfile +3 -0
- data/README.md +134 -17
- data/legion-data.gemspec +1 -1
- data/lib/legion/data/archival/policy.rb +1 -16
- data/lib/legion/data/archival.rb +49 -50
- data/lib/legion/data/archiver.rb +2 -1
- data/lib/legion/data/audit_log_hash_chain.rb +3 -1
- data/lib/legion/data/audit_record.rb +25 -31
- data/lib/legion/data/connection.rb +13 -46
- data/lib/legion/data/database_lock.rb +23 -0
- data/lib/legion/data/event_store.rb +14 -14
- data/lib/legion/data/extract.rb +5 -7
- data/lib/legion/data/helper.rb +1 -1
- data/lib/legion/data/local.rb +9 -21
- data/lib/legion/data/migration.rb +24 -0
- data/lib/legion/data/migrations/137_preserve_task_archive_fields.rb +37 -0
- data/lib/legion/data/migrations/138_add_append_claims.rb +11 -0
- data/lib/legion/data/migrations/139_create_task_logs.rb +27 -0
- data/lib/legion/data/model.rb +2 -2
- data/lib/legion/data/models/append_claim.rb +10 -0
- data/lib/legion/data/models/audit_log.rb +34 -2
- data/lib/legion/data/models/data_archive.rb +10 -0
- data/lib/legion/data/models/governance_event.rb +10 -0
- data/lib/legion/data/models/task.rb +5 -5
- data/lib/legion/data/models/task_log.rb +16 -0
- data/lib/legion/data/retention.rb +42 -11
- data/lib/legion/data/settings.rb +6 -9
- data/lib/legion/data/spool.rb +36 -16
- data/lib/legion/data/storage_tiers.rb +26 -20
- data/lib/legion/data/task/status.rb +36 -0
- data/lib/legion/data/version.rb +1 -1
- data/lib/legion/data.rb +47 -23
- metadata +11 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a3113304f2f0357f9ea74b932ffc50231616e8eca856e69f5f8c85cb62adad99
|
|
4
|
+
data.tar.gz: 3387584a5f01c29edcedccc27f1d4ef8a8b3d3147ab4272943a4902cef50e7c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 044e75084811c9b1d71b8364e81b01a7351d6cccc25e93b1866f8e13a55ec5ea50e053ebe63c35efc284acbf962cc6746d599dc8c3d858090e57510c0cc5d533
|
|
7
|
+
data.tar.gz: f1e3f19d5e29ef237ffb3a2a548d67e7f8b3932ddb9ecf6c7cb6119b59fa44a807c1f299109b82aec4510e260516dfcaf862c26c5c1700aa10d9cbe45bdc5d72
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
ci:
|
|
13
13
|
permissions:
|
|
14
14
|
contents: read
|
|
15
|
-
uses: LegionIO/.github/.github/workflows/ci.yml@
|
|
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@
|
|
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
|
-
|
|
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
|
-
-
|
|
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,114 @@
|
|
|
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
|
+
|
|
53
|
+
## [1.10.28] - 2026-10-30
|
|
54
|
+
|
|
55
|
+
### Changed
|
|
56
|
+
- Remove the unmerged task-idempotency claim migration and restore the existing task creation API without database claims. The concurrent-creation fix for #77 is withdrawn from this PR (#84).
|
|
57
|
+
- Renumber the unmerged append-claims migration from 139 to 138, leaving a contiguous migration sequence without adding a compensating migration. Migrations already on main are unchanged.
|
|
58
|
+
|
|
59
|
+
## [1.10.27] - 2026-10-30
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
- Remove unmerged migration 140 rather than conditionally taking ownership of a runtime-created archive. Verification handles hot-only history without creating schema and rechecks for an archive created during the first verification pass (#79, #84).
|
|
63
|
+
- Preserve database errors other than a genuinely missing archive, including permission failures. Existing hot/warm verification remains a single ordered statement; migrations already on main are unchanged.
|
|
64
|
+
|
|
65
|
+
## [1.10.26] - 2026-10-29
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Exercise concurrent SQLite audit/event appends with the same cooperative busy handler used by production connections, avoiding native busy-wait starvation in Ruby 3.4 CI while retaining lock-timeout behavior (#78).
|
|
69
|
+
- Initialize concurrency-spec cleanup state before resource setup and guard an absent release queue so early setup failures are not masked by cleanup errors (#84).
|
|
70
|
+
|
|
71
|
+
## [1.10.25] - 2026-10-29
|
|
72
|
+
|
|
73
|
+
### Fixed
|
|
74
|
+
- Archive generic retention backlogs in primary-key-ordered, bounded transactions. Copy locked rows and delete only their complete identities; verify deletion counts and roll back inconsistent batches. Independent workers cannot copy the same locked rows, and concurrent archive-table creation handles PostgreSQL catalog races (#82).
|
|
75
|
+
- Add an optional positive `batch_size` (default 1000), require a declared source primary key, and support composite keys without exceeding SQLite predicate-depth limits. Completed batches survive later failures or process interruption; reruns move only remaining eligible records and report their aggregate count.
|
|
76
|
+
|
|
77
|
+
## [1.10.24] - 2026-10-29
|
|
78
|
+
|
|
79
|
+
### Fixed
|
|
80
|
+
- Use the active adapter for privilege checks, including SQLite fallback. PostgreSQL uses native table privileges; MySQL uses nonmutating SELECT and EXPLAIN INSERT probes rather than PostgreSQL-only SQL. Failed queries return false without poisoning future checks (#81).
|
|
81
|
+
- Cache successful privilege lookups per physical connection, invalidate on connection replacement and credential rotation, and prevent in-flight old sessions from overwriting replacement permissions.
|
|
82
|
+
|
|
83
|
+
## [1.10.23] - 2026-10-29
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- Claim spool entries with nonblocking cross-process file locks before reading, replaying, quarantining, or clearing them. Recheck descriptor identity after acquisition so an already-open deleted file cannot replay again. Reads and clears skip in-flight entries (#80).
|
|
87
|
+
- Preserve callback exceptions and pending events for retry; operating-system lock release recovers crashed owners without stale claim files. Delivery remains at least once if a process exits after its side effect but before deleting the event.
|
|
88
|
+
|
|
89
|
+
## [1.10.22] - 2026-10-29
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
- Verify the audit log across retained hot and warm rows in one ordered database statement, preserving genesis, predecessor, content-hash, and duplicate detection after repeated retention boundaries. Explicit record collections remain strictly verified as supplied; verification counts consumed rows without building an additional result array (#79).
|
|
93
|
+
|
|
94
|
+
## [1.10.21] - 2026-10-29
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
- Serialize audit-chain and governance-stream appends with database claims, including first-entry races. Migration 138 adds scoped append claims. MySQL uses exclusive duplicate-key locking rather than shared-lock upgrades (#78).
|
|
98
|
+
- Read locked tails on network adapters, retain database lock-timeout errors, and verify/walk audit records by append ID rather than writer-clock order. Unrelated insert failures roll back their claims and remain visible to callers.
|
|
99
|
+
|
|
100
|
+
## [1.10.19] - 2026-10-29
|
|
101
|
+
|
|
102
|
+
### Fixed
|
|
103
|
+
- Drain database pools through tracked public checkout lifetimes across threaded and sharded pool implementations. Idle pools close immediately; shutdown returns false at its deadline while accepted work finishes, rejects new work, and closes connections and query logging only after the last checkout returns (#76).
|
|
104
|
+
- Rebind loaded models and association caches to the current database on setup so a restart does not reopen a closed pool.
|
|
105
|
+
|
|
106
|
+
## [1.10.18] - 2026-10-29
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
- Preserve task payloads, arguments, results, identities, links, and timestamps through scheduled/policy archival and restore. Copy and delete exact locked row IDs atomically; keep parents referenced by live tasks hot and roll back restores on missing dependencies or occupied IDs (#75).
|
|
110
|
+
- Add migration 137 for missing task archive columns; existing migrations are unchanged. PostgreSQL generated timestamps are excluded from restore inserts.
|
|
111
|
+
|
|
3
112
|
## [1.10.17] - 2026-10-29
|
|
4
113
|
|
|
5
114
|
### Fixed
|
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
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/),
|
|
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.
|
|
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 # Disconnect
|
|
100
|
+
│ └── .shutdown # Disconnect shared connection; close query log
|
|
77
101
|
│
|
|
78
|
-
├── Migration #
|
|
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,6 +175,10 @@ Legion::Data::Connection.connection_info
|
|
|
151
175
|
Legion::Data.shutdown
|
|
152
176
|
```
|
|
153
177
|
|
|
178
|
+
### Database Shutdown
|
|
179
|
+
|
|
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.
|
|
181
|
+
|
|
154
182
|
### Model Associations
|
|
155
183
|
|
|
156
184
|
Models use Sequel associations as the public object graph. Prefer association methods and association datasets over hand-written foreign-key lookups when the relationship is part of the schema contract.
|
|
@@ -207,30 +235,88 @@ Supported formats: `.txt`, `.md`, `.csv`, `.json`, `.jsonl`, `.html`, `.xlsx`, `
|
|
|
207
235
|
When migration 076 is present, Extract also persists the same per-step timing rows to `extract_step_timings`
|
|
208
236
|
under the returned `extract_id`.
|
|
209
237
|
|
|
210
|
-
### Task
|
|
238
|
+
### Task Persistence
|
|
239
|
+
|
|
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.
|
|
245
|
+
|
|
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.
|
|
259
|
+
|
|
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.
|
|
269
|
+
|
|
270
|
+
### Serialized Audit and Governance Appends
|
|
271
|
+
|
|
272
|
+
After migration 138, `AuditRecord.append` serializes each chain and `EventStore.append` serializes each stream with database-backed claims, including empty chains. Different audit/event namespaces remain independent. Claim rows do not accumulate after transactions finish; failed appends roll back both claim and record.
|
|
273
|
+
|
|
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.
|
|
211
275
|
|
|
212
|
-
|
|
213
|
-
returns an existing non-terminal task for the same key inside the optional TTL window, or creates a new task
|
|
214
|
-
with `idempotency_key` and `idempotency_expires_at` populated:
|
|
276
|
+
### Model-backed Storage Tiers
|
|
215
277
|
|
|
216
278
|
```ruby
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
payload: payload,
|
|
220
|
-
ttl: 300
|
|
279
|
+
Legion::Data::StorageTiers.archive_to_warm(
|
|
280
|
+
model: Legion::Data::Model::Task, age_days: 90, batch_size: 1000
|
|
221
281
|
)
|
|
222
282
|
```
|
|
223
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
|
+
|
|
286
|
+
### Bounded Retention
|
|
287
|
+
|
|
288
|
+
```ruby
|
|
289
|
+
Legion::Data::Retention.archive_old_records(
|
|
290
|
+
table: :audit_log, archive_after_days: 90, batch_size: 1000
|
|
291
|
+
) # => { archived: total_moved, table: :audit_log }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
Source tables need a declared primary key, including composite keys. `batch_size` must be a positive integer and bounds each materialized batch. Rows are selected in primary-key order; PostgreSQL/MySQL lock them and SQLite uses an immediate write transaction. Each batch copies and deletes exactly the same identities atomically, including when other rows become eligible during the copy.
|
|
295
|
+
|
|
296
|
+
Outside a caller-owned transaction, each batch commits independently. A later failure is raised without undoing earlier batches; rerun to archive the remaining eligible rows. A successful result sums that invocation's moved rows. If the caller wraps retention in its own transaction, that outer transaction still controls commit and lock lifetime.
|
|
297
|
+
|
|
298
|
+
### Audit Verification After Retention
|
|
299
|
+
|
|
300
|
+
`Legion::Data::Model::AuditLog.verify_chain` verifies hot `audit_log` rows together with retained `audit_log_archive` rows in one ordered SQL statement when the archive exists. Moving records between these tables does not discard their predecessors or create a mixed-snapshot boundary. Archived content is rehashed; missing predecessors, modified content, and duplicate copies remain verification failures.
|
|
301
|
+
|
|
302
|
+
Passing an explicit record collection still verifies exactly that collection from genesis. Cold storage and purged history are not silently trusted: missing archived predecessors remain invalid until the complete history is supplied. This does not change LegionIO's separate tier-verification API.
|
|
303
|
+
|
|
304
|
+
Before the first retention run, verification reads hot-only history without creating archive schema. It checks again afterward for concurrent archive creation and, if the archive appeared, verifies both tables together. Only an actual missing-table error means the archive is absent; permission, connection, and other database failures propagate.
|
|
305
|
+
|
|
224
306
|
### Filesystem Spool (Write Buffer)
|
|
225
307
|
|
|
226
308
|
When the database is unavailable, `Legion::Data::Spool` buffers writes to `~/.legionio/data/spool/` and replays once the connection is restored:
|
|
227
309
|
|
|
228
310
|
```ruby
|
|
229
311
|
spool = Legion::Data::Spool.for(Legion::Extensions::MyLex)
|
|
230
|
-
spool.write({ task_id: SecureRandom.uuid, data: payload })
|
|
231
|
-
spool.
|
|
312
|
+
spool.write(:events, { task_id: SecureRandom.uuid, data: payload })
|
|
313
|
+
spool.flush(:events) { |entry| process(entry) }
|
|
232
314
|
```
|
|
233
315
|
|
|
316
|
+
Replay claims each event with a nonblocking advisory file lock held through the callback and successful deletion. Concurrent processes cannot replay the same live claim. `read` and `clear` skip in-flight entries; `count` includes them until deletion. Corrupt files are quarantined under the same ownership protocol.
|
|
317
|
+
|
|
318
|
+
Callback failure retains the event and releases its lock. Process exit also releases the lock automatically, so the next drainer can recover it without stale claim markers. Delivery is **at least once**: a crash after an external side effect but before deletion can replay that effect. Callbacks must provide their own idempotency. Single-drainer filename order is preserved, but concurrent completion order is not guaranteed. The spool filesystem must support cooperative advisory locking.
|
|
319
|
+
|
|
234
320
|
### Row-Level Security (PostgreSQL)
|
|
235
321
|
|
|
236
322
|
`Legion::Data::Rls` provides tenant isolation via PostgreSQL session variables (migration 043):
|
|
@@ -251,6 +337,10 @@ Legion::Data.can_read?(:tasks) # => true
|
|
|
251
337
|
Legion::Data.reset_privileges! # clear cached privilege checks
|
|
252
338
|
```
|
|
253
339
|
|
|
340
|
+
Checks use the active database adapter, including SQLite fallback, and return false while disconnected. PostgreSQL checks table-level SELECT/INSERT grants. MySQL checks base tables with a zero-row SELECT and a nonexecuting `EXPLAIN INSERT`; checking a MySQL view also requires the privileges needed to explain that view, including SHOW VIEW for writes. Neither probe inserts data.
|
|
341
|
+
|
|
342
|
+
Successful lookups are cached per physical connection. Connection replacement and credential rotation invalidate caches; query or connection failures return false without caching a denial. Call `reset_privileges!` after changing grants or roles on existing sessions.
|
|
343
|
+
|
|
254
344
|
---
|
|
255
345
|
|
|
256
346
|
## Configuration
|
|
@@ -317,6 +407,8 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
317
407
|
|
|
318
408
|
### Full Configuration Reference
|
|
319
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
|
+
|
|
320
412
|
```json
|
|
321
413
|
{
|
|
322
414
|
"data": {
|
|
@@ -346,6 +438,15 @@ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
346
438
|
"replicas": [],
|
|
347
439
|
|
|
348
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
|
+
},
|
|
349
450
|
|
|
350
451
|
"migrations": {
|
|
351
452
|
"continue_on_fail": false,
|
|
@@ -415,6 +516,12 @@ Enable `data.query_log` (or `data.local.query_log`) to write SQL to a dedicated
|
|
|
415
516
|
|
|
416
517
|
Replica URLs initialize a sharded pool at startup. URL-specific options override primary connection options; omitted credentials and session timeouts are inherited. Duplicate URLs are registered once as `read_0`, `read_1`, and so on. Reads remain on the primary unless explicitly routed, for example `dataset.server(Legion::Data::Connection.read_server)`. With no replicas configured, `read_server` returns `:default`.
|
|
417
518
|
|
|
519
|
+
### Task Database Archival
|
|
520
|
+
|
|
521
|
+
`Archival.archive_completed_tasks` and task-policy archival retain every task field after migration 137. Copy/delete and restore are atomic; only copied task IDs are deleted. A task referenced as a parent or master by another live task remains hot, so linked task trees archive from leaves upward across batches.
|
|
522
|
+
|
|
523
|
+
Restore preserves original IDs and restores parent/master links after inserting the requested rows. Include archived ancestors in the same restore request or restore them first. Occupied IDs or missing referenced functions/relationships cause rollback and leave the archive intact. Fields already discarded by older archival versions cannot be reconstructed by this migration.
|
|
524
|
+
|
|
418
525
|
### Archive Objects
|
|
419
526
|
|
|
420
527
|
Each archive batch receives a UUID before upload. S3, Azure, and local object paths use `legion-archive/<table>/<year>/<month>/batch_<uuid>.jsonl.gz`; the same UUID is stored in `archive_manifest.batch_id`. Repeated runs do not reuse numbered object names. Existing manifest paths remain valid; no stored objects are renamed. Direct `Archiver.upload_batch` callers must supply `batch_id:` instead of `batch_n:`.
|
|
@@ -532,7 +639,7 @@ The `Legion::Data::Model::Identity::*`, `Apollo::*`, and `RBAC::*` namespaces pr
|
|
|
532
639
|
| `csv` (>= 3.2) | CSV extraction handler |
|
|
533
640
|
| `legion-json` | JSON serialization via Legion::JSON |
|
|
534
641
|
| `legion-logging` (>= 1.5.0) | Structured logging |
|
|
535
|
-
| `legion-settings` (>= 1.
|
|
642
|
+
| `legion-settings` (>= 1.4.5) | Configuration management |
|
|
536
643
|
| `mysql2` (>= 0.5.5) | MySQL adapter (optional) |
|
|
537
644
|
| `pg` (>= 1.5) | PostgreSQL adapter (optional) |
|
|
538
645
|
|
|
@@ -540,7 +647,7 @@ The `Legion::Data::Model::Identity::*`, `Apollo::*`, and `RBAC::*` namespaces pr
|
|
|
540
647
|
|
|
541
648
|
## Migrations
|
|
542
649
|
|
|
543
|
-
|
|
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:
|
|
544
651
|
|
|
545
652
|
| Range | What was added |
|
|
546
653
|
|-------|---------------|
|
|
@@ -616,10 +723,20 @@ bundle exec legionio_migrate
|
|
|
616
723
|
git clone https://github.com/LegionIO/legion-data
|
|
617
724
|
cd legion-data
|
|
618
725
|
bundle install
|
|
619
|
-
bundle exec rspec
|
|
726
|
+
bundle exec rspec --format json --out tmp/rspec_results.json --format progress --out tmp/rspec_progress.txt
|
|
620
727
|
bundle exec rubocop -A # zero offenses expected
|
|
621
728
|
```
|
|
622
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
|
+
|
|
623
740
|
This repo also includes a pre-commit configuration:
|
|
624
741
|
|
|
625
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.
|
|
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
|
-
|
|
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
|