@learningnodes/elen 0.1.4 → 0.1.5

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.
@@ -16,22 +16,93 @@ class SQLiteStorage {
16
16
  this.db.exec(`
17
17
  CREATE TABLE IF NOT EXISTS constraint_sets (constraint_set_id TEXT PRIMARY KEY, atoms TEXT NOT NULL, summary TEXT NOT NULL);
18
18
  CREATE TABLE IF NOT EXISTS decisions (decision_id TEXT PRIMARY KEY, decision_json TEXT NOT NULL);
19
- CREATE TABLE IF NOT EXISTS records (
20
- record_id TEXT PRIMARY KEY,
21
- decision_id TEXT,
22
- agent_id TEXT NOT NULL,
23
- domain TEXT NOT NULL,
24
- project_id TEXT NOT NULL,
25
- question_text TEXT,
26
- decision_text TEXT,
27
- confidence REAL,
28
- payload_json TEXT NOT NULL
29
- );
30
19
  `);
20
+ // Check if records table exists and what schema it has
21
+ const tableExists = this.db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='records'").get();
22
+ if (!tableExists) {
23
+ // Fresh DB: create spec-compliant table
24
+ this.db.exec(`
25
+ CREATE TABLE records (
26
+ record_id TEXT PRIMARY KEY,
27
+ decision_id TEXT NOT NULL,
28
+ q_id TEXT NOT NULL,
29
+ agent_id TEXT NOT NULL,
30
+ domain TEXT NOT NULL,
31
+ project_id TEXT NOT NULL DEFAULT 'default',
32
+ question_text TEXT,
33
+ decision_text TEXT NOT NULL,
34
+ constraint_set_id TEXT NOT NULL,
35
+ refs TEXT NOT NULL DEFAULT '[]',
36
+ status TEXT NOT NULL DEFAULT 'active',
37
+ supersedes_id TEXT,
38
+ timestamp TEXT NOT NULL,
39
+ payload_json TEXT
40
+ );
41
+ `);
42
+ return;
43
+ }
44
+ // Table exists — check if it needs migration
45
+ const cols = this.db.pragma('table_info(records)');
46
+ const colNames = new Set(cols.map(c => c.name));
47
+ const needsRebuild = colNames.has('record_json') || !colNames.has('payload_json');
48
+ if (needsRebuild) {
49
+ // Old schema detected — rebuild table to fix NOT NULL constraints
50
+ this.db.exec('BEGIN TRANSACTION');
51
+ try {
52
+ this.db.exec('ALTER TABLE records RENAME TO _records_old');
53
+ this.db.exec(`
54
+ CREATE TABLE records (
55
+ record_id TEXT PRIMARY KEY,
56
+ decision_id TEXT NOT NULL,
57
+ q_id TEXT NOT NULL,
58
+ agent_id TEXT NOT NULL,
59
+ domain TEXT NOT NULL,
60
+ project_id TEXT NOT NULL DEFAULT 'default',
61
+ question_text TEXT,
62
+ decision_text TEXT NOT NULL,
63
+ constraint_set_id TEXT NOT NULL,
64
+ refs TEXT NOT NULL DEFAULT '[]',
65
+ status TEXT NOT NULL DEFAULT 'active',
66
+ supersedes_id TEXT,
67
+ timestamp TEXT NOT NULL,
68
+ payload_json TEXT
69
+ );
70
+ `);
71
+ // Copy data, mapping old columns to new
72
+ const hasRecordJson = colNames.has('record_json');
73
+ const hasQuestionText = colNames.has('question_text');
74
+ const payloadCol = hasRecordJson ? 'record_json' : (colNames.has('payload_json') ? 'payload_json' : 'NULL');
75
+ const questionCol = hasQuestionText ? 'question_text' : 'NULL';
76
+ this.db.exec(`
77
+ INSERT INTO records (
78
+ record_id, decision_id, q_id, agent_id, domain, project_id,
79
+ question_text, decision_text, constraint_set_id,
80
+ refs, status, supersedes_id, timestamp, payload_json
81
+ )
82
+ SELECT
83
+ record_id, decision_id, q_id, agent_id, domain, project_id,
84
+ ${questionCol}, decision_text, constraint_set_id,
85
+ refs, status, supersedes_id, timestamp, ${payloadCol}
86
+ FROM _records_old
87
+ `);
88
+ this.db.exec('DROP TABLE _records_old');
89
+ this.db.exec('COMMIT');
90
+ }
91
+ catch (err) {
92
+ this.db.exec('ROLLBACK');
93
+ throw err;
94
+ }
95
+ }
96
+ else if (!colNames.has('question_text')) {
97
+ // Partial migration: just add missing columns
98
+ this.db.exec('ALTER TABLE records ADD COLUMN question_text TEXT');
99
+ }
31
100
  }
101
+ /* ── Decisions (context objects) ──────────────────── */
32
102
  async saveDecision(decision) {
33
103
  this.db.prepare('INSERT OR REPLACE INTO decisions(decision_id, decision_json) VALUES (?,?)').run([decision.decision_id, JSON.stringify(decision)]);
34
104
  }
105
+ /* ── Constraint Sets ─────────────────────────────── */
35
106
  async saveConstraintSet(constraintSet) {
36
107
  this.db.prepare('INSERT OR IGNORE INTO constraint_sets(constraint_set_id, atoms, summary) VALUES (?,?,?)').run([constraintSet.constraint_set_id, JSON.stringify(constraintSet.atoms), constraintSet.summary]);
37
108
  }
@@ -39,33 +110,82 @@ class SQLiteStorage {
39
110
  const row = this.db.prepare('SELECT * FROM constraint_sets WHERE constraint_set_id=?').get(id);
40
111
  return row ? { constraint_set_id: row.constraint_set_id, atoms: JSON.parse(row.atoms), summary: row.summary } : null;
41
112
  }
113
+ /* ── Records ─────────────────────────────────────── */
42
114
  async saveRecord(record) {
43
115
  if ("record_id" in record) {
44
116
  await this.saveLegacyRecord(record);
45
117
  return;
46
118
  }
47
- this.db.prepare('INSERT OR REPLACE INTO records(record_id, decision_id, agent_id, domain, project_id, question_text, decision_text, payload_json) VALUES (?,?,?,?,?,?,?,?)').run([record.decision_id, record.decision_id, record.agent_id, record.domain, this.projectId, record.question_text ?? null, record.decision_text, JSON.stringify(record)]);
119
+ // Spec-compliant MinimalDecisionRecord all columns populated
120
+ this.db.prepare(`
121
+ INSERT OR REPLACE INTO records(
122
+ record_id, decision_id, q_id, agent_id, domain, project_id,
123
+ question_text, decision_text, constraint_set_id,
124
+ refs, status, supersedes_id, timestamp, payload_json
125
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
126
+ `).run([
127
+ record.decision_id, // record_id = decision_id for minimal records
128
+ record.decision_id,
129
+ record.q_id,
130
+ record.agent_id,
131
+ record.domain,
132
+ this.projectId,
133
+ record.question_text ?? null,
134
+ record.decision_text,
135
+ record.constraint_set_id,
136
+ JSON.stringify(record.refs),
137
+ record.status,
138
+ record.supersedes_id ?? null,
139
+ record.timestamp,
140
+ JSON.stringify(record)
141
+ ]);
48
142
  }
49
143
  async saveLegacyRecord(record) {
50
- this.db.prepare('INSERT OR REPLACE INTO records(record_id, decision_id, agent_id, domain, project_id, question_text, decision_text, confidence, payload_json) VALUES (?,?,?,?,?,?,?,?,?)').run([record.record_id, record.decision_id, record.agent_id, record.domain, this.projectId, record.question, record.answer, record.confidence, JSON.stringify(record)]);
144
+ // Legacy DecisionRecord (v0) map old fields to spec columns
145
+ this.db.prepare(`
146
+ INSERT OR REPLACE INTO records(
147
+ record_id, decision_id, q_id, agent_id, domain, project_id,
148
+ question_text, decision_text, constraint_set_id,
149
+ refs, status, supersedes_id, timestamp, payload_json
150
+ ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
151
+ `).run([
152
+ record.record_id,
153
+ record.decision_id,
154
+ '', // q_id not available in legacy format
155
+ record.agent_id,
156
+ record.domain,
157
+ this.projectId,
158
+ record.question, // question → question_text
159
+ record.answer, // answer → decision_text
160
+ '', // no constraint_set_id in legacy
161
+ JSON.stringify([]), // no refs in legacy
162
+ 'active', // default status
163
+ null, // no supersedes_id in legacy
164
+ record.published_at, // published_at → timestamp
165
+ JSON.stringify(record)
166
+ ]);
51
167
  }
52
168
  async getRecord(recordId) {
53
169
  const row = this.db.prepare('SELECT payload_json FROM records WHERE record_id=? OR decision_id=?').get([recordId, recordId]);
54
- return row ? JSON.parse(row.payload_json) : null;
170
+ return row?.payload_json ? JSON.parse(row.payload_json) : null;
55
171
  }
172
+ /* ── Search ──────────────────────────────────────── */
56
173
  async searchRecords(opts) {
57
- let rows = this.db.prepare('SELECT payload_json, decision_id, project_id, confidence, question_text, decision_text, domain FROM records').all();
58
- if (this.defaultIsolation === 'strict' || opts.includeShared === false)
174
+ let rows = this.db.prepare('SELECT payload_json, decision_id, project_id, question_text, decision_text, domain, status FROM records WHERE status != ?').all(['withdrawn']);
175
+ // Project isolation
176
+ if (this.defaultIsolation === 'strict' || opts.includeShared === false) {
59
177
  rows = rows.filter(r => r.project_id === this.projectId);
178
+ }
179
+ // Domain filter
60
180
  if (opts.domain)
61
181
  rows = rows.filter(r => r.domain === opts.domain);
62
- if (typeof opts.minConfidence === 'number')
63
- rows = rows.filter(r => r.confidence == null || r.confidence >= opts.minConfidence);
182
+ // Text search across question_text + decision_text + domain
64
183
  if (opts.query) {
65
184
  const q = opts.query.toLowerCase();
66
- rows = rows.filter(r => { const p = JSON.parse(r.payload_json); const extra = p.constraints_snapshot ? `${p.constraints_snapshot.map((c) => c.description).join(' ')} ${p.evidence_snapshot.map((e) => `${e.claim} ${e.proof}`).join(' ')}` : ''; return `${r.question_text ?? ''} ${r.decision_text ?? ''} ${r.domain ?? ''} ${extra}`.toLowerCase().includes(q); });
185
+ rows = rows.filter(r => `${r.question_text ?? ''} ${r.decision_text ?? ''} ${r.domain ?? ''}`.toLowerCase().includes(q));
67
186
  }
68
- let parsed = rows.map(r => JSON.parse(r.payload_json));
187
+ let parsed = rows.map(r => r.payload_json ? JSON.parse(r.payload_json) : null).filter(Boolean);
188
+ // Parent prompt filter (searches decision context)
69
189
  if (opts.parentPrompt) {
70
190
  const needle = opts.parentPrompt.toLowerCase();
71
191
  parsed = parsed.filter((r) => {
@@ -78,9 +198,10 @@ class SQLiteStorage {
78
198
  }
79
199
  return opts.limit ? parsed.slice(0, opts.limit) : parsed;
80
200
  }
201
+ /* ── Agent queries ───────────────────────────────── */
81
202
  async getAgentDecisions(agentId, domain) {
82
203
  const rows = this.db.prepare('SELECT payload_json FROM records WHERE agent_id=?').all([agentId]);
83
- const parsed = rows.map(r => JSON.parse(r.payload_json));
204
+ const parsed = rows.map(r => r.payload_json ? JSON.parse(r.payload_json) : null).filter(Boolean);
84
205
  return domain ? parsed.filter(r => r.domain === domain) : parsed;
85
206
  }
86
207
  async getCompetencyProfile(agentId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learningnodes/elen",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "license": "AGPL-3.0",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,7 +10,7 @@
10
10
  "lint": "tsc -p tsconfig.json --noEmit"
11
11
  },
12
12
  "dependencies": {
13
- "@learningnodes/elen-core": "^0.1.4",
13
+ "@learningnodes/elen-core": "^0.1.5",
14
14
  "better-sqlite3": "^11.7.0",
15
15
  "nanoid": "^5.1.4"
16
16
  },
@@ -22,5 +22,4 @@
22
22
  "exports": {
23
23
  ".": "./dist/index.js"
24
24
  }
25
- }
26
-
25
+ }