@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.
- package/dist/storage/sqlite.js +142 -21
- package/package.json +3 -4
package/dist/storage/sqlite.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
58
|
-
|
|
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
|
-
|
|
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 =>
|
|
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.
|
|
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.
|
|
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
|
+
}
|