omegon-pi 0.14.3 → 0.15.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.
|
@@ -156,6 +156,12 @@ export interface Fact {
|
|
|
156
156
|
/** jj change ID at fact creation — permanent provenance anchor.
|
|
157
157
|
* Survives rebase/squash. Null for facts created outside jj repos. */
|
|
158
158
|
jj_change_id: string | null;
|
|
159
|
+
/** Persona that owns this fact. Null = project-level fact. */
|
|
160
|
+
persona_id: string | null;
|
|
161
|
+
/** Memory layer: 'project' | 'persona' | 'working'. */
|
|
162
|
+
layer: string;
|
|
163
|
+
/** JSON array of domain tags (e.g. ["pcb","thermal"]). Null = untagged. */
|
|
164
|
+
tags: string | null;
|
|
159
165
|
}
|
|
160
166
|
|
|
161
167
|
export interface MindRecord {
|
|
@@ -320,7 +326,7 @@ export class FactStore {
|
|
|
320
326
|
}
|
|
321
327
|
|
|
322
328
|
/** Current schema version — bump when adding migrations */
|
|
323
|
-
static readonly SCHEMA_VERSION =
|
|
329
|
+
static readonly SCHEMA_VERSION = 6;
|
|
324
330
|
|
|
325
331
|
private getSchemaVersion(): number {
|
|
326
332
|
try {
|
|
@@ -452,29 +458,49 @@ export class FactStore {
|
|
|
452
458
|
this.setSchemaVersion(4);
|
|
453
459
|
}
|
|
454
460
|
|
|
455
|
-
// Migration 4→5: schema alignment
|
|
461
|
+
// Migration 4→5: full Rust↔TS schema alignment
|
|
456
462
|
//
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
// 2. jj_change_id column for provenance tracking (new in v5).
|
|
463
|
+
// The Rust omegon-memory crate owns the canonical schema but may produce
|
|
464
|
+
// DBs missing columns/tables that TS requires. This migration idempotently
|
|
465
|
+
// brings ANY DB (Rust v4, Rust v5, or TS-created) to a state where all
|
|
466
|
+
// TS code paths work. Every ALTER TABLE is wrapped in try/catch because
|
|
467
|
+
// the column may already exist.
|
|
463
468
|
if (current < 5) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// for DBs created by TS (which always had these columns).
|
|
467
|
-
const addColumnIfMissing = (table: string, col: string, type: string = "TEXT") => {
|
|
468
|
-
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${type}`).run(); } catch { /* exists */ }
|
|
469
|
+
const addCol = (table: string, col: string, typedef: string = "TEXT") => {
|
|
470
|
+
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${typedef}`).run(); } catch { /* exists */ }
|
|
469
471
|
};
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
//
|
|
472
|
+
|
|
473
|
+
// --- facts table ---
|
|
474
|
+
addCol("facts", "created_session");
|
|
475
|
+
addCol("facts", "superseded_at");
|
|
476
|
+
addCol("facts", "archived_at");
|
|
477
|
+
addCol("facts", "jj_change_id");
|
|
478
|
+
|
|
479
|
+
// --- edges table ---
|
|
480
|
+
// Rust edges is minimal: id, source_fact_id, target_fact_id, relation,
|
|
481
|
+
// description, weight, created_at. TS needs these additional columns:
|
|
482
|
+
addCol("edges", "confidence", "REAL NOT NULL DEFAULT 1.0");
|
|
483
|
+
addCol("edges", "last_reinforced", "TEXT NOT NULL DEFAULT ''");
|
|
484
|
+
addCol("edges", "reinforcement_count", "INTEGER NOT NULL DEFAULT 1");
|
|
485
|
+
addCol("edges", "decay_rate", `REAL NOT NULL DEFAULT ${DECAY.baseRate}`);
|
|
486
|
+
addCol("edges", "status", "TEXT NOT NULL DEFAULT 'active'");
|
|
487
|
+
addCol("edges", "created_session");
|
|
488
|
+
addCol("edges", "source_mind");
|
|
489
|
+
addCol("edges", "target_mind");
|
|
490
|
+
|
|
491
|
+
// --- minds table ---
|
|
492
|
+
// Rust minds is minimal: name, description, status, origin_type, created_at.
|
|
493
|
+
addCol("minds", "origin_path");
|
|
494
|
+
addCol("minds", "origin_url");
|
|
495
|
+
addCol("minds", "readonly", "INTEGER NOT NULL DEFAULT 0");
|
|
496
|
+
addCol("minds", "parent");
|
|
497
|
+
addCol("minds", "last_sync");
|
|
498
|
+
|
|
499
|
+
// --- episodes table ---
|
|
500
|
+
addCol("episodes", "session_id");
|
|
501
|
+
addCol("episodes", "jj_change_id");
|
|
502
|
+
|
|
503
|
+
// --- tables that may not exist in Rust-created DBs ---
|
|
478
504
|
this.db.exec(`
|
|
479
505
|
CREATE TABLE IF NOT EXISTS episode_facts (
|
|
480
506
|
episode_id TEXT NOT NULL,
|
|
@@ -493,9 +519,27 @@ export class FactStore {
|
|
|
493
519
|
);
|
|
494
520
|
`);
|
|
495
521
|
|
|
496
|
-
// Indexes on
|
|
522
|
+
// Indexes on migration-added columns are created by ensureIndexes().
|
|
497
523
|
this.setSchemaVersion(5);
|
|
498
524
|
}
|
|
525
|
+
|
|
526
|
+
// Migration 5→6: Persona system
|
|
527
|
+
//
|
|
528
|
+
// Adds persona ownership and memory layering to facts:
|
|
529
|
+
// persona_id — which persona owns the fact (NULL = project-level)
|
|
530
|
+
// layer — memory layer: 'project' | 'persona' | 'working'
|
|
531
|
+
// tags — JSON array of domain tags (e.g. ["pcb","thermal"])
|
|
532
|
+
// All additive. Existing data reads cleanly with defaults.
|
|
533
|
+
if (current < 6) {
|
|
534
|
+
const addCol = (table: string, col: string, typedef: string = "TEXT") => {
|
|
535
|
+
try { this.db.prepare(`ALTER TABLE ${table} ADD COLUMN ${col} ${typedef}`).run(); } catch { /* exists */ }
|
|
536
|
+
};
|
|
537
|
+
addCol("facts", "persona_id");
|
|
538
|
+
addCol("facts", "layer", "TEXT NOT NULL DEFAULT 'project'");
|
|
539
|
+
addCol("facts", "tags");
|
|
540
|
+
// Indexes created by ensureIndexes().
|
|
541
|
+
this.setSchemaVersion(6);
|
|
542
|
+
}
|
|
499
543
|
}
|
|
500
544
|
|
|
501
545
|
private initSchema(): void {
|
|
@@ -578,12 +622,8 @@ export class FactStore {
|
|
|
578
622
|
FOREIGN KEY (target_fact_id) REFERENCES facts(id) ON DELETE CASCADE
|
|
579
623
|
);
|
|
580
624
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
CREATE INDEX IF NOT EXISTS idx_edges_target
|
|
584
|
-
ON edges(target_fact_id) WHERE status = 'active';
|
|
585
|
-
CREATE INDEX IF NOT EXISTS idx_edges_relation
|
|
586
|
-
ON edges(relation) WHERE status = 'active';
|
|
625
|
+
-- Edge indexes that reference 'status' are created in ensureIndexes()
|
|
626
|
+
-- after migrations add the status column (missing in Rust-created DBs).
|
|
587
627
|
`);
|
|
588
628
|
|
|
589
629
|
// FTS5 virtual table for full-text search
|
|
@@ -642,17 +682,30 @@ export class FactStore {
|
|
|
642
682
|
}
|
|
643
683
|
}
|
|
644
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Create indexes on columns that may not exist until after migrations.
|
|
687
|
+
* Called AFTER runMigrations() so Rust-created DBs have had their
|
|
688
|
+
* missing columns added before we try to index them.
|
|
689
|
+
*/
|
|
645
690
|
/**
|
|
646
691
|
* Create indexes on columns that may not exist until after migrations.
|
|
647
692
|
* Called AFTER runMigrations() so Rust-created DBs have had their
|
|
648
693
|
* missing columns added before we try to index them.
|
|
649
694
|
*/
|
|
650
695
|
private ensureIndexes(): void {
|
|
651
|
-
const
|
|
652
|
-
try { this.db.prepare(sql).run(); } catch { /* column or index
|
|
696
|
+
const idx = (sql: string) => {
|
|
697
|
+
try { this.db.prepare(sql).run(); } catch { /* column or index already exists */ }
|
|
653
698
|
};
|
|
654
|
-
|
|
655
|
-
|
|
699
|
+
// facts indexes on migration-added columns
|
|
700
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_facts_session ON facts(created_session)`);
|
|
701
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_facts_version ON facts(version DESC)`);
|
|
702
|
+
// edges indexes that reference 'status' (missing in Rust-created DBs before migration)
|
|
703
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_fact_id) WHERE status = 'active'`);
|
|
704
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_fact_id) WHERE status = 'active'`);
|
|
705
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_edges_relation ON edges(relation) WHERE status = 'active'`);
|
|
706
|
+
// v6: persona system indexes
|
|
707
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_facts_persona ON facts(persona_id) WHERE persona_id IS NOT NULL`);
|
|
708
|
+
idx(`CREATE INDEX IF NOT EXISTS idx_facts_layer ON facts(mind, layer) WHERE status = 'active'`);
|
|
656
709
|
}
|
|
657
710
|
|
|
658
711
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Canonical memory DB schema. Generated from Rust omegon-memory. Do not edit — regenerate with: cargo test -p omegon-memory schema_contract_generate -- --ignored",
|
|
3
|
+
"schema_version": 6,
|
|
4
|
+
"tables": {
|
|
5
|
+
"edges": ["id", "source_fact_id", "target_fact_id", "relation", "description", "confidence", "last_reinforced", "reinforcement_count", "decay_rate", "status", "created_at", "created_session", "source_mind", "target_mind"],
|
|
6
|
+
"embedding_metadata": ["model_name", "dims", "inserted_at"],
|
|
7
|
+
"episode_facts": ["episode_id", "fact_id"],
|
|
8
|
+
"episodes": ["id", "mind", "title", "narrative", "date", "session_id", "created_at", "jj_change_id"],
|
|
9
|
+
"episodes_vec": ["episode_id", "embedding", "model_name", "dims", "created_at"],
|
|
10
|
+
"facts": ["id", "mind", "section", "content", "status", "created_at", "created_session", "supersedes", "superseded_at", "archived_at", "source", "content_hash", "confidence", "last_reinforced", "reinforcement_count", "decay_rate", "decay_profile", "version", "last_accessed", "jj_change_id", "persona_id", "layer", "tags"],
|
|
11
|
+
"facts_vec": ["fact_id", "embedding", "model_name", "dims", "created_at"],
|
|
12
|
+
"minds": ["name", "description", "status", "origin_type", "origin_path", "origin_url", "readonly", "parent", "created_at", "last_sync"],
|
|
13
|
+
"schema_version": ["version", "applied_at"]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omegon-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Omegon — an opinionated distribution of pi (by Mario Zechner) with extensions for lifecycle management, memory, orchestration, and visualization",
|
|
5
5
|
"bin": {
|
|
6
6
|
"omegon-pi": "bin/omegon-pi.mjs",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* validate-schema-contract.mjs
|
|
4
|
+
*
|
|
5
|
+
* Validates that the TS factstore migration produces a schema that is a
|
|
6
|
+
* superset of the Rust omegon-memory schema contract.
|
|
7
|
+
*
|
|
8
|
+
* Usage: node scripts/validate-schema-contract.mjs <path-to-schema-contract.json>
|
|
9
|
+
*
|
|
10
|
+
* The script:
|
|
11
|
+
* 1. Creates a minimal Rust-shaped DB at the contract's stated schema_version
|
|
12
|
+
* (simulating what the Rust binary would create)
|
|
13
|
+
* 2. Opens it with FactStore (triggering TS migrations)
|
|
14
|
+
* 3. Verifies every table and column from the contract exists in the result
|
|
15
|
+
*
|
|
16
|
+
* Exit 0 = TS is compatible. Exit 1 = drift detected.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { readFileSync } from "node:fs";
|
|
20
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
23
|
+
import Database from "better-sqlite3";
|
|
24
|
+
|
|
25
|
+
const contractPath = process.argv[2];
|
|
26
|
+
if (!contractPath) {
|
|
27
|
+
console.error("Usage: node validate-schema-contract.mjs <schema-contract.json>");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const contract = JSON.parse(readFileSync(contractPath, "utf-8"));
|
|
32
|
+
console.log(`Schema contract: version ${contract.schema_version}, ${Object.keys(contract.tables).length} tables`);
|
|
33
|
+
|
|
34
|
+
// Step 1: Create a DB with ONLY the Rust contract's tables and columns.
|
|
35
|
+
// This simulates what the latest Rust binary would produce.
|
|
36
|
+
const dir = mkdtempSync(join(tmpdir(), "schema-validate-"));
|
|
37
|
+
const dbPath = join(dir, "facts.db");
|
|
38
|
+
const db = new Database(dbPath);
|
|
39
|
+
db.pragma("journal_mode = WAL");
|
|
40
|
+
|
|
41
|
+
for (const [table, columns] of Object.entries(contract.tables)) {
|
|
42
|
+
// Skip FTS virtual tables — they're created by TS initSchema
|
|
43
|
+
if (table.endsWith("_fts")) continue;
|
|
44
|
+
|
|
45
|
+
const colDefs = columns.map((col, i) => {
|
|
46
|
+
// First column is PRIMARY KEY
|
|
47
|
+
if (i === 0) return `${col} TEXT PRIMARY KEY`;
|
|
48
|
+
// embedding columns are BLOB
|
|
49
|
+
if (col === "embedding") return `${col} BLOB`;
|
|
50
|
+
// integer columns
|
|
51
|
+
if (["dims", "reinforcement_count", "readonly"].includes(col)) return `${col} INTEGER NOT NULL DEFAULT 0`;
|
|
52
|
+
// real columns
|
|
53
|
+
if (["confidence", "decay_rate"].includes(col)) return `${col} REAL NOT NULL DEFAULT 0`;
|
|
54
|
+
// version column
|
|
55
|
+
if (col === "version" && table === "facts") return `${col} INTEGER NOT NULL DEFAULT 0`;
|
|
56
|
+
if (col === "version" && table === "schema_version") return `${col} INTEGER PRIMARY KEY`;
|
|
57
|
+
// everything else is TEXT
|
|
58
|
+
return `${col} TEXT`;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// episode_facts has composite PK
|
|
62
|
+
if (table === "episode_facts") {
|
|
63
|
+
const cols = columns.map(c => `${c} TEXT NOT NULL`).join(", ");
|
|
64
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${table} (${cols}, PRIMARY KEY (${columns.join(", ")}))`);
|
|
65
|
+
} else {
|
|
66
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${table} (${colDefs.join(", ")})`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Insert schema version
|
|
71
|
+
db.exec(`INSERT OR REPLACE INTO schema_version (version, applied_at) VALUES (${contract.schema_version}, datetime('now'))`);
|
|
72
|
+
|
|
73
|
+
// Insert default mind (required by TS)
|
|
74
|
+
try {
|
|
75
|
+
db.exec(`INSERT INTO minds (name, created_at) VALUES ('default', datetime('now'))`);
|
|
76
|
+
} catch { /* might already exist */ }
|
|
77
|
+
|
|
78
|
+
db.close();
|
|
79
|
+
console.log(`Created Rust-shaped DB at ${dbPath}`);
|
|
80
|
+
|
|
81
|
+
// Step 2: Open with TS FactStore — this runs migrations
|
|
82
|
+
const { FactStore } = await import("../extensions/project-memory/factstore.ts");
|
|
83
|
+
let store;
|
|
84
|
+
try {
|
|
85
|
+
store = new FactStore(dir);
|
|
86
|
+
console.log("✓ FactStore opened Rust-shaped DB successfully");
|
|
87
|
+
} catch (e) {
|
|
88
|
+
console.error(`✗ FactStore FAILED to open Rust-shaped DB: ${e.message}`);
|
|
89
|
+
rmSync(dir, { recursive: true, force: true });
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Step 3: Verify every contract table+column exists after migration
|
|
94
|
+
const verifyDb = store.db ?? (store)._db ?? (() => { throw new Error("Cannot access DB handle"); })();
|
|
95
|
+
let errors = 0;
|
|
96
|
+
|
|
97
|
+
for (const [table, expectedCols] of Object.entries(contract.tables)) {
|
|
98
|
+
if (table.endsWith("_fts")) continue;
|
|
99
|
+
|
|
100
|
+
let actualCols;
|
|
101
|
+
try {
|
|
102
|
+
actualCols = verifyDb.prepare(`PRAGMA table_info(${table})`).all().map(r => r.name);
|
|
103
|
+
} catch {
|
|
104
|
+
console.error(`✗ Table '${table}' does not exist after TS migration`);
|
|
105
|
+
errors++;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
for (const col of expectedCols) {
|
|
110
|
+
if (!actualCols.includes(col)) {
|
|
111
|
+
console.error(`✗ Column '${table}.${col}' required by Rust contract but missing after TS migration`);
|
|
112
|
+
errors++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
store.close();
|
|
118
|
+
rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
|
|
120
|
+
if (errors > 0) {
|
|
121
|
+
console.error(`\n${errors} schema drift error(s) detected. Update the TS v5 migration in factstore.ts.`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
} else {
|
|
124
|
+
console.log(`✓ All ${Object.keys(contract.tables).length} tables and columns verified — TS is compatible with Rust schema v${contract.schema_version}`);
|
|
125
|
+
}
|