@aletheia-labs/store-sqlite 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.
- package/LICENSE +176 -0
- package/README.md +119 -0
- package/dist/codec.d.ts +68 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +112 -0
- package/dist/codec.js.map +1 -0
- package/dist/connection.d.ts +37 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +44 -0
- package/dist/connection.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/migrations.d.ts +22 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +154 -0
- package/dist/migrations.js.map +1 -0
- package/dist/sqlite-conflict-registry.d.ts +68 -0
- package/dist/sqlite-conflict-registry.d.ts.map +1 -0
- package/dist/sqlite-conflict-registry.js +173 -0
- package/dist/sqlite-conflict-registry.js.map +1 -0
- package/dist/sqlite-event-ledger.d.ts +58 -0
- package/dist/sqlite-event-ledger.d.ts.map +1 -0
- package/dist/sqlite-event-ledger.js +154 -0
- package/dist/sqlite-event-ledger.js.map +1 -0
- package/dist/sqlite-memory-store.d.ts +76 -0
- package/dist/sqlite-memory-store.d.ts.map +1 -0
- package/dist/sqlite-memory-store.js +189 -0
- package/dist/sqlite-memory-store.js.map +1 -0
- package/package.json +48 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migrations for @aletheia-labs/store-sqlite.
|
|
3
|
+
*
|
|
4
|
+
* Migrations are embedded as strings so the package works the same when
|
|
5
|
+
* bundled. Each migration runs once and is recorded in `schema_migrations`.
|
|
6
|
+
*
|
|
7
|
+
* To add a migration: append to the MIGRATIONS array with the next version
|
|
8
|
+
* number. NEVER edit an existing migration — write a new one.
|
|
9
|
+
*/
|
|
10
|
+
import type Database from 'better-sqlite3';
|
|
11
|
+
export interface Migration {
|
|
12
|
+
readonly version: number;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly sql: string;
|
|
15
|
+
}
|
|
16
|
+
export declare const MIGRATIONS: readonly Migration[];
|
|
17
|
+
/**
|
|
18
|
+
* Apply all pending migrations in order, inside a single transaction per migration.
|
|
19
|
+
* Idempotent — already-applied migrations are skipped.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyMigrations(db: Database.Database): readonly Migration[];
|
|
22
|
+
//# sourceMappingURL=migrations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,EA+G1C,CAAC;AAEF;;;GAGG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,GAAG,SAAS,SAAS,EAAE,CAoC3E"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema migrations for @aletheia-labs/store-sqlite.
|
|
3
|
+
*
|
|
4
|
+
* Migrations are embedded as strings so the package works the same when
|
|
5
|
+
* bundled. Each migration runs once and is recorded in `schema_migrations`.
|
|
6
|
+
*
|
|
7
|
+
* To add a migration: append to the MIGRATIONS array with the next version
|
|
8
|
+
* number. NEVER edit an existing migration — write a new one.
|
|
9
|
+
*/
|
|
10
|
+
export const MIGRATIONS = [
|
|
11
|
+
{
|
|
12
|
+
version: 1,
|
|
13
|
+
name: 'initial-schema',
|
|
14
|
+
sql: `
|
|
15
|
+
-- Schema versioning ----------------------------------------------------------
|
|
16
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
17
|
+
version INTEGER PRIMARY KEY,
|
|
18
|
+
name TEXT NOT NULL,
|
|
19
|
+
applied_at TEXT NOT NULL
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
-- Events: append-only --------------------------------------------------------
|
|
23
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
24
|
+
event_id TEXT PRIMARY KEY,
|
|
25
|
+
kind TEXT NOT NULL,
|
|
26
|
+
agent_id TEXT,
|
|
27
|
+
occurred_at TEXT NOT NULL,
|
|
28
|
+
payload_json TEXT NOT NULL,
|
|
29
|
+
scope_json TEXT NOT NULL,
|
|
30
|
+
visibility_json TEXT NOT NULL,
|
|
31
|
+
scope_key TEXT NOT NULL,
|
|
32
|
+
visibility_key TEXT NOT NULL,
|
|
33
|
+
inserted_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE INDEX IF NOT EXISTS idx_events_occurred_at ON events(occurred_at);
|
|
37
|
+
CREATE INDEX IF NOT EXISTS idx_events_agent ON events(agent_id);
|
|
38
|
+
CREATE INDEX IF NOT EXISTS idx_events_visibility_key ON events(visibility_key);
|
|
39
|
+
CREATE INDEX IF NOT EXISTS idx_events_scope_key ON events(scope_key);
|
|
40
|
+
|
|
41
|
+
-- Memory atoms ---------------------------------------------------------------
|
|
42
|
+
CREATE TABLE IF NOT EXISTS memory_atoms (
|
|
43
|
+
memory_id TEXT PRIMARY KEY,
|
|
44
|
+
memory_type TEXT NOT NULL,
|
|
45
|
+
content TEXT NOT NULL,
|
|
46
|
+
source_agent_id TEXT NOT NULL,
|
|
47
|
+
source_event_ids_json TEXT NOT NULL,
|
|
48
|
+
source_memory_ids_json TEXT NOT NULL,
|
|
49
|
+
scope_json TEXT NOT NULL,
|
|
50
|
+
visibility_json TEXT NOT NULL,
|
|
51
|
+
status TEXT NOT NULL,
|
|
52
|
+
scores_json TEXT NOT NULL,
|
|
53
|
+
valid_from TEXT NOT NULL,
|
|
54
|
+
valid_until TEXT,
|
|
55
|
+
last_confirmed_at TEXT,
|
|
56
|
+
links_json TEXT NOT NULL,
|
|
57
|
+
scope_key TEXT NOT NULL,
|
|
58
|
+
visibility_key TEXT NOT NULL,
|
|
59
|
+
inserted_at TEXT NOT NULL
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_atoms_status ON memory_atoms(status);
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_atoms_visibility_key ON memory_atoms(visibility_key);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_atoms_scope_key ON memory_atoms(scope_key);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_atoms_valid_from ON memory_atoms(valid_from);
|
|
66
|
+
|
|
67
|
+
-- Status history -------------------------------------------------------------
|
|
68
|
+
CREATE TABLE IF NOT EXISTS memory_status_history (
|
|
69
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
+
memory_id TEXT NOT NULL REFERENCES memory_atoms(memory_id),
|
|
71
|
+
from_status TEXT,
|
|
72
|
+
to_status TEXT NOT NULL,
|
|
73
|
+
rationale TEXT NOT NULL,
|
|
74
|
+
actor TEXT NOT NULL,
|
|
75
|
+
conflict_id TEXT,
|
|
76
|
+
at TEXT NOT NULL
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_msh_memory_at ON memory_status_history(memory_id, at);
|
|
80
|
+
|
|
81
|
+
-- Conflicts ------------------------------------------------------------------
|
|
82
|
+
CREATE TABLE IF NOT EXISTS conflicts (
|
|
83
|
+
conflict_id TEXT PRIMARY KEY,
|
|
84
|
+
topic TEXT NOT NULL,
|
|
85
|
+
scope_json TEXT NOT NULL,
|
|
86
|
+
scope_key TEXT NOT NULL,
|
|
87
|
+
claims_json TEXT NOT NULL,
|
|
88
|
+
status TEXT NOT NULL,
|
|
89
|
+
decision_policy TEXT NOT NULL,
|
|
90
|
+
recorded_at TEXT NOT NULL,
|
|
91
|
+
resolved_at TEXT
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_status ON conflicts(status);
|
|
95
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_scope_key ON conflicts(scope_key);
|
|
96
|
+
|
|
97
|
+
-- Conflict → atoms (many-to-many) for "touchingMemoryIds" queries -----------
|
|
98
|
+
CREATE TABLE IF NOT EXISTS conflict_claim_atoms (
|
|
99
|
+
conflict_id TEXT NOT NULL REFERENCES conflicts(conflict_id),
|
|
100
|
+
memory_id TEXT NOT NULL,
|
|
101
|
+
PRIMARY KEY (conflict_id, memory_id)
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_cca_memory ON conflict_claim_atoms(memory_id);
|
|
105
|
+
|
|
106
|
+
-- Conflict resolution history ------------------------------------------------
|
|
107
|
+
CREATE TABLE IF NOT EXISTS conflict_resolution_history (
|
|
108
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
109
|
+
conflict_id TEXT NOT NULL REFERENCES conflicts(conflict_id),
|
|
110
|
+
from_status TEXT NOT NULL,
|
|
111
|
+
to_status TEXT NOT NULL,
|
|
112
|
+
rationale TEXT NOT NULL,
|
|
113
|
+
actor TEXT NOT NULL,
|
|
114
|
+
preferred_memory_id TEXT,
|
|
115
|
+
at TEXT NOT NULL
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_crh_conflict_at ON conflict_resolution_history(conflict_id, at);
|
|
119
|
+
`.trim(),
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
/**
|
|
123
|
+
* Apply all pending migrations in order, inside a single transaction per migration.
|
|
124
|
+
* Idempotent — already-applied migrations are skipped.
|
|
125
|
+
*/
|
|
126
|
+
export function applyMigrations(db) {
|
|
127
|
+
// Bootstrap the schema_migrations table if it doesn't exist yet.
|
|
128
|
+
// Done outside the migration loop so the loop can rely on its presence.
|
|
129
|
+
db.exec(`
|
|
130
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
131
|
+
version INTEGER PRIMARY KEY,
|
|
132
|
+
name TEXT NOT NULL,
|
|
133
|
+
applied_at TEXT NOT NULL
|
|
134
|
+
);
|
|
135
|
+
`);
|
|
136
|
+
const applied = new Set(db
|
|
137
|
+
.prepare('SELECT version FROM schema_migrations')
|
|
138
|
+
.all()
|
|
139
|
+
.map((r) => r.version));
|
|
140
|
+
const newlyApplied = [];
|
|
141
|
+
const insertMigration = db.prepare('INSERT INTO schema_migrations (version, name, applied_at) VALUES (?, ?, ?)');
|
|
142
|
+
for (const migration of MIGRATIONS) {
|
|
143
|
+
if (applied.has(migration.version))
|
|
144
|
+
continue;
|
|
145
|
+
const tx = db.transaction(() => {
|
|
146
|
+
db.exec(migration.sql);
|
|
147
|
+
insertMigration.run(migration.version, migration.name, new Date().toISOString());
|
|
148
|
+
});
|
|
149
|
+
tx();
|
|
150
|
+
newlyApplied.push(migration);
|
|
151
|
+
}
|
|
152
|
+
return newlyApplied;
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAUH,MAAM,CAAC,MAAM,UAAU,GAAyB;IAC9C;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,gBAAgB;QACtB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAyGJ,CAAC,IAAI,EAAE;KACT;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,EAAqB;IACnD,iEAAiE;IACjE,wEAAwE;IACxE,EAAE,CAAC,IAAI,CAAC;;;;;;GAMP,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,IAAI,GAAG,CACrB,EAAE;SACC,OAAO,CAA0B,uCAAuC,CAAC;SACzE,GAAG,EAAE;SACL,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CACzB,CAAC;IAEF,MAAM,YAAY,GAAgB,EAAE,CAAC;IAErC,MAAM,eAAe,GAAG,EAAE,CAAC,OAAO,CAChC,4EAA4E,CAC7E,CAAC;IAEF,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,IAAI,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC;YAAE,SAAS;QAE7C,MAAM,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE;YAC7B,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YACvB,eAAe,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;QACnF,CAAC,CAAC,CAAC;QACH,EAAE,EAAE,CAAC;QACL,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,YAAY,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed ConflictRegistry.
|
|
3
|
+
*
|
|
4
|
+
* Conflicts are append-only by ID; the only allowed mutation is `resolve`,
|
|
5
|
+
* which updates `status` + `resolved_at` and records an entry in
|
|
6
|
+
* `conflict_resolution_history`.
|
|
7
|
+
*/
|
|
8
|
+
import { type ConflictId, type ConflictQuery, type ConflictRecord, type ConflictRegistry, type ConflictStatus, type IsoTimestamp, type ResolveReason, type Visibility } from '@aletheia-labs/core';
|
|
9
|
+
import type Database from 'better-sqlite3';
|
|
10
|
+
export declare class SqliteConflictRegistry implements ConflictRegistry {
|
|
11
|
+
private readonly db;
|
|
12
|
+
private readonly insertConflict;
|
|
13
|
+
private readonly insertClaimLink;
|
|
14
|
+
private readonly getConflictByID;
|
|
15
|
+
private readonly updateConflictStatus;
|
|
16
|
+
private readonly insertResolution;
|
|
17
|
+
private readonly resolutionHistoryStmt;
|
|
18
|
+
/**
|
|
19
|
+
* Create a ConflictRegistry backed by an existing SQLite connection.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* The constructor prepares all statements up front. Use `openSqliteStores()`
|
|
23
|
+
* unless a host needs direct control over connection ownership.
|
|
24
|
+
*/
|
|
25
|
+
constructor(db: Database.Database);
|
|
26
|
+
/**
|
|
27
|
+
* Record a new first-class conflict and index all touched memory IDs.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* Conflict rows are append-only by ID. The join table is maintained inside
|
|
31
|
+
* the same transaction so `touchingMemoryIds` queries stay deterministic.
|
|
32
|
+
*/
|
|
33
|
+
record(conflict: ConflictRecord): Promise<ConflictRecord>;
|
|
34
|
+
/**
|
|
35
|
+
* Retrieve one conflict if conflict access is permitted.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* Phase 1 conflict visibility is coarse: an empty permitted set fails closed;
|
|
39
|
+
* action paths still re-check the visibility of cited memory atoms before use.
|
|
40
|
+
*/
|
|
41
|
+
get(conflictId: ConflictId, permittedVisibilities: readonly Visibility[]): Promise<ConflictRecord | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Query conflicts by status, scope, and touched memory IDs.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* Returns an empty list when no visibility planes are permitted. This mirrors
|
|
47
|
+
* the fail-closed behavior of the other stores.
|
|
48
|
+
*/
|
|
49
|
+
query(filter: ConflictQuery): Promise<readonly ConflictRecord[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Resolve a conflict and append a resolution-history row.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Resolution is the only mutation path for conflicts. A repeated transition
|
|
55
|
+
* to the current status returns the existing row without adding history.
|
|
56
|
+
*/
|
|
57
|
+
resolve(conflictId: ConflictId, nextStatus: Exclude<ConflictStatus, 'unresolved'>, reason: ResolveReason): Promise<ConflictRecord | null>;
|
|
58
|
+
/**
|
|
59
|
+
* Return the audited resolution history for one conflict.
|
|
60
|
+
*/
|
|
61
|
+
resolutionHistory(conflictId: ConflictId): Promise<readonly {
|
|
62
|
+
at: IsoTimestamp;
|
|
63
|
+
fromStatus: ConflictStatus;
|
|
64
|
+
toStatus: ConflictStatus;
|
|
65
|
+
reason: ResolveReason;
|
|
66
|
+
}[]>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=sqlite-conflict-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-conflict-registry.d.ts","sourceRoot":"","sources":["../src/sqlite-conflict-registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,cAAc,EAEnB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,YAAY,EAEjB,KAAK,aAAa,EAClB,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAY3C,qBAAa,sBAAuB,YAAW,gBAAgB;IAejD,OAAO,CAAC,QAAQ,CAAC,EAAE;IAd/B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAqB;IACpD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAqB;IACrD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAA+B;IAC/D,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAqB;IAC1D,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAqB;IACtD,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA+B;IAErE;;;;;;OAMG;gBAC0B,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAkClD;;;;;;OAMG;IACG,MAAM,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IAuB/D;;;;;;OAMG;IACG,GAAG,CACP,UAAU,EAAE,UAAU,EACtB,qBAAqB,EAAE,SAAS,UAAU,EAAE,GAC3C,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAWjC;;;;;;OAMG;IACG,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC;IAoCtE;;;;;;OAMG;IACG,OAAO,CACX,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,OAAO,CAAC,cAAc,EAAE,YAAY,CAAC,EACjD,MAAM,EAAE,aAAa,GACpB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IA8BjC;;OAEG;IACG,iBAAiB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CACtD,SAAS;QACP,EAAE,EAAE,YAAY,CAAC;QACjB,UAAU,EAAE,cAAc,CAAC;QAC3B,QAAQ,EAAE,cAAc,CAAC;QACzB,MAAM,EAAE,aAAa,CAAC;KACvB,EAAE,CACJ;CAaF"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed ConflictRegistry.
|
|
3
|
+
*
|
|
4
|
+
* Conflicts are append-only by ID; the only allowed mutation is `resolve`,
|
|
5
|
+
* which updates `status` + `resolved_at` and records an entry in
|
|
6
|
+
* `conflict_resolution_history`.
|
|
7
|
+
*/
|
|
8
|
+
import { ConflictRecordSchema, scopeKey, } from '@aletheia-labs/core';
|
|
9
|
+
import { conflictToRow, rowToConflict } from './codec.js';
|
|
10
|
+
export class SqliteConflictRegistry {
|
|
11
|
+
db;
|
|
12
|
+
insertConflict;
|
|
13
|
+
insertClaimLink;
|
|
14
|
+
getConflictByID;
|
|
15
|
+
updateConflictStatus;
|
|
16
|
+
insertResolution;
|
|
17
|
+
resolutionHistoryStmt;
|
|
18
|
+
/**
|
|
19
|
+
* Create a ConflictRegistry backed by an existing SQLite connection.
|
|
20
|
+
*
|
|
21
|
+
* @remarks
|
|
22
|
+
* The constructor prepares all statements up front. Use `openSqliteStores()`
|
|
23
|
+
* unless a host needs direct control over connection ownership.
|
|
24
|
+
*/
|
|
25
|
+
constructor(db) {
|
|
26
|
+
this.db = db;
|
|
27
|
+
this.insertConflict = db.prepare(`
|
|
28
|
+
INSERT INTO conflicts (
|
|
29
|
+
conflict_id, topic, scope_json, scope_key,
|
|
30
|
+
claims_json, status, decision_policy, recorded_at, resolved_at
|
|
31
|
+
) VALUES (
|
|
32
|
+
@conflict_id, @topic, @scope_json, @scope_key,
|
|
33
|
+
@claims_json, @status, @decision_policy, @recorded_at, @resolved_at
|
|
34
|
+
)
|
|
35
|
+
`);
|
|
36
|
+
this.insertClaimLink = db.prepare('INSERT OR IGNORE INTO conflict_claim_atoms (conflict_id, memory_id) VALUES (?, ?)');
|
|
37
|
+
this.getConflictByID = db.prepare('SELECT * FROM conflicts WHERE conflict_id = ?');
|
|
38
|
+
this.updateConflictStatus = db.prepare('UPDATE conflicts SET status = ?, resolved_at = ? WHERE conflict_id = ?');
|
|
39
|
+
this.insertResolution = db.prepare(`
|
|
40
|
+
INSERT INTO conflict_resolution_history (
|
|
41
|
+
conflict_id, from_status, to_status, rationale, actor, preferred_memory_id, at
|
|
42
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
43
|
+
`);
|
|
44
|
+
this.resolutionHistoryStmt = db.prepare('SELECT * FROM conflict_resolution_history WHERE conflict_id = ? ORDER BY at ASC, id ASC');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Record a new first-class conflict and index all touched memory IDs.
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* Conflict rows are append-only by ID. The join table is maintained inside
|
|
51
|
+
* the same transaction so `touchingMemoryIds` queries stay deterministic.
|
|
52
|
+
*/
|
|
53
|
+
async record(conflict) {
|
|
54
|
+
const validated = ConflictRecordSchema.parse(conflict);
|
|
55
|
+
const row = conflictToRow(validated);
|
|
56
|
+
const tx = this.db.transaction(() => {
|
|
57
|
+
try {
|
|
58
|
+
this.insertConflict.run(row);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
if (err instanceof Error && err.message.includes('UNIQUE')) {
|
|
62
|
+
throw new Error(`ConflictRegistry.record: duplicate conflict_id "${row.conflict_id}"`);
|
|
63
|
+
}
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
// Mirror claim atoms into the join table for "touchingMemoryIds" queries.
|
|
67
|
+
for (const claim of validated.claims) {
|
|
68
|
+
this.insertClaimLink.run(validated.conflictId, claim.memoryId);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
tx();
|
|
72
|
+
return validated;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Retrieve one conflict if conflict access is permitted.
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* Phase 1 conflict visibility is coarse: an empty permitted set fails closed;
|
|
79
|
+
* action paths still re-check the visibility of cited memory atoms before use.
|
|
80
|
+
*/
|
|
81
|
+
async get(conflictId, permittedVisibilities) {
|
|
82
|
+
// Conflicts don't carry their own visibility; permission inherits from the
|
|
83
|
+
// most-restrictive scope of their claim atoms. For Phase 1 we treat
|
|
84
|
+
// conflict visibility as "any atom involved is visible to the caller".
|
|
85
|
+
// The caller must still respect the touching atoms' visibility when acting.
|
|
86
|
+
const row = this.getConflictByID.get(conflictId);
|
|
87
|
+
if (!row)
|
|
88
|
+
return null;
|
|
89
|
+
if (permittedVisibilities.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return rowToConflict(row);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Query conflicts by status, scope, and touched memory IDs.
|
|
95
|
+
*
|
|
96
|
+
* @remarks
|
|
97
|
+
* Returns an empty list when no visibility planes are permitted. This mirrors
|
|
98
|
+
* the fail-closed behavior of the other stores.
|
|
99
|
+
*/
|
|
100
|
+
async query(filter) {
|
|
101
|
+
if (filter.permittedVisibilities.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const where = [];
|
|
105
|
+
const params = [];
|
|
106
|
+
if (filter.statuses !== undefined && filter.statuses.length > 0) {
|
|
107
|
+
where.push(`status IN (${filter.statuses.map(() => '?').join(', ')})`);
|
|
108
|
+
params.push(...filter.statuses);
|
|
109
|
+
}
|
|
110
|
+
if (filter.scope !== undefined) {
|
|
111
|
+
where.push('scope_key = ?');
|
|
112
|
+
params.push(scopeKey(filter.scope));
|
|
113
|
+
}
|
|
114
|
+
if (filter.touchingMemoryIds !== undefined && filter.touchingMemoryIds.length > 0) {
|
|
115
|
+
where.push(`conflict_id IN (
|
|
116
|
+
SELECT conflict_id FROM conflict_claim_atoms
|
|
117
|
+
WHERE memory_id IN (${filter.touchingMemoryIds.map(() => '?').join(', ')})
|
|
118
|
+
)`);
|
|
119
|
+
params.push(...filter.touchingMemoryIds);
|
|
120
|
+
}
|
|
121
|
+
const whereClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
122
|
+
const sql = `
|
|
123
|
+
SELECT * FROM conflicts
|
|
124
|
+
${whereClause}
|
|
125
|
+
ORDER BY recorded_at DESC, conflict_id ASC
|
|
126
|
+
`;
|
|
127
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
128
|
+
return rows.map(rowToConflict);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Resolve a conflict and append a resolution-history row.
|
|
132
|
+
*
|
|
133
|
+
* @remarks
|
|
134
|
+
* Resolution is the only mutation path for conflicts. A repeated transition
|
|
135
|
+
* to the current status returns the existing row without adding history.
|
|
136
|
+
*/
|
|
137
|
+
async resolve(conflictId, nextStatus, reason) {
|
|
138
|
+
const row = this.getConflictByID.get(conflictId);
|
|
139
|
+
if (!row)
|
|
140
|
+
return null;
|
|
141
|
+
const current = row.status;
|
|
142
|
+
if (current === nextStatus) {
|
|
143
|
+
// Already in this status — no-op, but record the attempted resolution.
|
|
144
|
+
return rowToConflict(row);
|
|
145
|
+
}
|
|
146
|
+
const now = new Date().toISOString();
|
|
147
|
+
// The TS type already excludes 'unresolved' — `now` is always the resolution timestamp.
|
|
148
|
+
const tx = this.db.transaction(() => {
|
|
149
|
+
this.updateConflictStatus.run(nextStatus, now, conflictId);
|
|
150
|
+
this.insertResolution.run(conflictId, current, nextStatus, reason.rationale, reason.actor, reason.preferredMemoryId, now);
|
|
151
|
+
});
|
|
152
|
+
tx();
|
|
153
|
+
const updated = this.getConflictByID.get(conflictId);
|
|
154
|
+
return rowToConflict(updated);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Return the audited resolution history for one conflict.
|
|
158
|
+
*/
|
|
159
|
+
async resolutionHistory(conflictId) {
|
|
160
|
+
const rows = this.resolutionHistoryStmt.all(conflictId);
|
|
161
|
+
return rows.map((r) => ({
|
|
162
|
+
at: r.at,
|
|
163
|
+
fromStatus: r.from_status,
|
|
164
|
+
toStatus: r.to_status,
|
|
165
|
+
reason: {
|
|
166
|
+
rationale: r.rationale,
|
|
167
|
+
actor: r.actor,
|
|
168
|
+
preferredMemoryId: r.preferred_memory_id,
|
|
169
|
+
},
|
|
170
|
+
}));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=sqlite-conflict-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-conflict-registry.js","sourceRoot":"","sources":["../src/sqlite-conflict-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed EventLedger.
|
|
3
|
+
*
|
|
4
|
+
* Enforces append-only at the storage layer:
|
|
5
|
+
* - INSERT only. Duplicate event_id throws (no OR REPLACE, no OR IGNORE).
|
|
6
|
+
* - No DELETE or UPDATE path exposed.
|
|
7
|
+
*
|
|
8
|
+
* Permission filtering is done in SQL via the `visibility_key` index.
|
|
9
|
+
*/
|
|
10
|
+
import { type Event, type EventId, type EventLedger, type EventQuery, type Visibility } from '@aletheia-labs/core';
|
|
11
|
+
import type Database from 'better-sqlite3';
|
|
12
|
+
export declare class SqliteEventLedger implements EventLedger {
|
|
13
|
+
private readonly db;
|
|
14
|
+
private readonly insertStmt;
|
|
15
|
+
private readonly getStmt;
|
|
16
|
+
/**
|
|
17
|
+
* Create an EventLedger backed by an existing `better-sqlite3` connection.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Callers normally use `openSqliteStores()` instead. Direct construction is
|
|
21
|
+
* useful when a host owns connection lifecycle or wants to compose stores
|
|
22
|
+
* manually.
|
|
23
|
+
*/
|
|
24
|
+
constructor(db: Database.Database);
|
|
25
|
+
/**
|
|
26
|
+
* Append one validated event to SQLite.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* Implementation uses plain INSERT and lets the primary key enforce
|
|
30
|
+
* append-only semantics. Duplicate IDs throw instead of replacing or ignoring
|
|
31
|
+
* existing evidence.
|
|
32
|
+
*/
|
|
33
|
+
append(event: Event): Promise<EventId>;
|
|
34
|
+
/**
|
|
35
|
+
* Load one event if the caller can see its visibility plane.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* A missing row and a hidden row both return `null` so callers cannot infer
|
|
39
|
+
* the existence of inaccessible evidence.
|
|
40
|
+
*/
|
|
41
|
+
get(eventId: EventId, permittedVisibilities: readonly Visibility[]): Promise<Event | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Query visible events in deterministic chronological order.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* The SQL WHERE clause always starts with the permission predicate generated
|
|
47
|
+
* by `permittedClause()`, preserving permission-before-selection.
|
|
48
|
+
*/
|
|
49
|
+
query(filter: EventQuery): Promise<readonly Event[]>;
|
|
50
|
+
/**
|
|
51
|
+
* Count visible events matching the same filters as `query()`.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* Use this for coverage/audit summaries without materializing rows.
|
|
55
|
+
*/
|
|
56
|
+
count(filter: EventQuery): Promise<number>;
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=sqlite-event-ledger.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-event-ledger.d.ts","sourceRoot":"","sources":["../src/sqlite-event-ledger.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,KAAK,KAAK,EACV,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,UAAU,EAEf,KAAK,UAAU,EAEhB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAG3C,qBAAa,iBAAkB,YAAW,WAAW;IAYvC,OAAO,CAAC,QAAQ,CAAC,EAAE;IAX/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IAEvD;;;;;;;OAOG;gBAC0B,EAAE,EAAE,QAAQ,CAAC,QAAQ;IAgBlD;;;;;;;OAOG;IACG,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC;IAkB5C;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,qBAAqB,EAAE,SAAS,UAAU,EAAE,GAAG,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC;IAahG;;;;;;OAMG;IACG,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,KAAK,EAAE,CAAC;IAsC1D;;;;;OAKG;IACG,KAAK,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;CA6BjD"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite-backed EventLedger.
|
|
3
|
+
*
|
|
4
|
+
* Enforces append-only at the storage layer:
|
|
5
|
+
* - INSERT only. Duplicate event_id throws (no OR REPLACE, no OR IGNORE).
|
|
6
|
+
* - No DELETE or UPDATE path exposed.
|
|
7
|
+
*
|
|
8
|
+
* Permission filtering is done in SQL via the `visibility_key` index.
|
|
9
|
+
*/
|
|
10
|
+
import { EventSchema, scopeKey, } from '@aletheia-labs/core';
|
|
11
|
+
import { eventToRow, permittedClause, rowToEvent } from './codec.js';
|
|
12
|
+
export class SqliteEventLedger {
|
|
13
|
+
db;
|
|
14
|
+
insertStmt;
|
|
15
|
+
getStmt;
|
|
16
|
+
/**
|
|
17
|
+
* Create an EventLedger backed by an existing `better-sqlite3` connection.
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* Callers normally use `openSqliteStores()` instead. Direct construction is
|
|
21
|
+
* useful when a host owns connection lifecycle or wants to compose stores
|
|
22
|
+
* manually.
|
|
23
|
+
*/
|
|
24
|
+
constructor(db) {
|
|
25
|
+
this.db = db;
|
|
26
|
+
this.insertStmt = db.prepare(`
|
|
27
|
+
INSERT INTO events (
|
|
28
|
+
event_id, kind, agent_id, occurred_at,
|
|
29
|
+
payload_json, scope_json, visibility_json,
|
|
30
|
+
scope_key, visibility_key, inserted_at
|
|
31
|
+
) VALUES (
|
|
32
|
+
@event_id, @kind, @agent_id, @occurred_at,
|
|
33
|
+
@payload_json, @scope_json, @visibility_json,
|
|
34
|
+
@scope_key, @visibility_key, @inserted_at
|
|
35
|
+
)
|
|
36
|
+
`);
|
|
37
|
+
this.getStmt = db.prepare('SELECT * FROM events WHERE event_id = ?');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Append one validated event to SQLite.
|
|
41
|
+
*
|
|
42
|
+
* @remarks
|
|
43
|
+
* Implementation uses plain INSERT and lets the primary key enforce
|
|
44
|
+
* append-only semantics. Duplicate IDs throw instead of replacing or ignoring
|
|
45
|
+
* existing evidence.
|
|
46
|
+
*/
|
|
47
|
+
async append(event) {
|
|
48
|
+
// Validate before persisting. The schema enforces shape; SQLite enforces uniqueness.
|
|
49
|
+
const validated = EventSchema.parse(event);
|
|
50
|
+
const row = eventToRow(validated, new Date().toISOString());
|
|
51
|
+
try {
|
|
52
|
+
this.insertStmt.run(row);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// better-sqlite3 throws SqliteError with code SQLITE_CONSTRAINT_PRIMARYKEY on duplicate
|
|
56
|
+
if (err instanceof Error && err.message.includes('UNIQUE')) {
|
|
57
|
+
throw new Error(`EventLedger.append: duplicate event_id "${row.event_id}"`);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
return validated.eventId;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Load one event if the caller can see its visibility plane.
|
|
65
|
+
*
|
|
66
|
+
* @remarks
|
|
67
|
+
* A missing row and a hidden row both return `null` so callers cannot infer
|
|
68
|
+
* the existence of inaccessible evidence.
|
|
69
|
+
*/
|
|
70
|
+
async get(eventId, permittedVisibilities) {
|
|
71
|
+
const row = this.getStmt.get(eventId);
|
|
72
|
+
if (!row)
|
|
73
|
+
return null;
|
|
74
|
+
// Permission filter: never leak existence of events the caller can't see.
|
|
75
|
+
const allowed = permittedClause(permittedVisibilities);
|
|
76
|
+
if (allowed.params.length === 0 || !allowed.params.includes(row.visibility_key)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return rowToEvent(row);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Query visible events in deterministic chronological order.
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* The SQL WHERE clause always starts with the permission predicate generated
|
|
86
|
+
* by `permittedClause()`, preserving permission-before-selection.
|
|
87
|
+
*/
|
|
88
|
+
async query(filter) {
|
|
89
|
+
const where = [];
|
|
90
|
+
const params = [];
|
|
91
|
+
// Permission filter ALWAYS first.
|
|
92
|
+
const allowed = permittedClause(filter.permittedVisibilities);
|
|
93
|
+
where.push(allowed.clause);
|
|
94
|
+
params.push(...allowed.params);
|
|
95
|
+
if (filter.agentId !== undefined) {
|
|
96
|
+
where.push('agent_id = ?');
|
|
97
|
+
params.push(filter.agentId);
|
|
98
|
+
}
|
|
99
|
+
if (filter.since !== undefined) {
|
|
100
|
+
where.push('occurred_at >= ?');
|
|
101
|
+
params.push(filter.since);
|
|
102
|
+
}
|
|
103
|
+
if (filter.until !== undefined) {
|
|
104
|
+
where.push('occurred_at <= ?');
|
|
105
|
+
params.push(filter.until);
|
|
106
|
+
}
|
|
107
|
+
if (filter.scope !== undefined) {
|
|
108
|
+
where.push('scope_key = ?');
|
|
109
|
+
params.push(scopeKey(filter.scope));
|
|
110
|
+
}
|
|
111
|
+
const limitClause = filter.limit !== undefined ? `LIMIT ${Math.floor(filter.limit)}` : '';
|
|
112
|
+
const sql = `
|
|
113
|
+
SELECT * FROM events
|
|
114
|
+
WHERE ${where.join(' AND ')}
|
|
115
|
+
ORDER BY occurred_at ASC, event_id ASC
|
|
116
|
+
${limitClause}
|
|
117
|
+
`;
|
|
118
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
119
|
+
return rows.map(rowToEvent);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Count visible events matching the same filters as `query()`.
|
|
123
|
+
*
|
|
124
|
+
* @remarks
|
|
125
|
+
* Use this for coverage/audit summaries without materializing rows.
|
|
126
|
+
*/
|
|
127
|
+
async count(filter) {
|
|
128
|
+
const where = [];
|
|
129
|
+
const params = [];
|
|
130
|
+
const allowed = permittedClause(filter.permittedVisibilities);
|
|
131
|
+
where.push(allowed.clause);
|
|
132
|
+
params.push(...allowed.params);
|
|
133
|
+
if (filter.agentId !== undefined) {
|
|
134
|
+
where.push('agent_id = ?');
|
|
135
|
+
params.push(filter.agentId);
|
|
136
|
+
}
|
|
137
|
+
if (filter.since !== undefined) {
|
|
138
|
+
where.push('occurred_at >= ?');
|
|
139
|
+
params.push(filter.since);
|
|
140
|
+
}
|
|
141
|
+
if (filter.until !== undefined) {
|
|
142
|
+
where.push('occurred_at <= ?');
|
|
143
|
+
params.push(filter.until);
|
|
144
|
+
}
|
|
145
|
+
if (filter.scope !== undefined) {
|
|
146
|
+
where.push('scope_key = ?');
|
|
147
|
+
params.push(scopeKey(filter.scope));
|
|
148
|
+
}
|
|
149
|
+
const sql = `SELECT COUNT(*) as n FROM events WHERE ${where.join(' AND ')}`;
|
|
150
|
+
const row = this.db.prepare(sql).get(...params);
|
|
151
|
+
return row.n;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//# sourceMappingURL=sqlite-event-ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-event-ledger.js","sourceRoot":"","sources":["../src/sqlite-event-ledger.ts"],"names":[],"mappings":"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"}
|