@learningnodes/elen 0.1.1 → 0.1.3

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.
@@ -6,329 +6,96 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.SQLiteStorage = void 0;
7
7
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
8
8
  class SQLiteStorage {
9
- constructor(path, projectId = 'default') {
9
+ constructor(path, projectId = 'default', defaultIsolation = 'strict') {
10
10
  this.db = new better_sqlite3_1.default(path);
11
11
  this.projectId = projectId;
12
+ this.defaultIsolation = defaultIsolation;
12
13
  this.init();
13
14
  }
14
15
  init() {
15
- const pragmaQuery = this.db.prepare('PRAGMA user_version').get();
16
- const versionRow = pragmaQuery ? pragmaQuery.user_version : 0;
17
- if (versionRow === 0) {
18
- this.db.exec(`
19
- CREATE TABLE IF NOT EXISTS constraint_sets (
20
- constraint_set_id TEXT PRIMARY KEY,
21
- atoms TEXT NOT NULL,
22
- summary TEXT NOT NULL,
23
- created_at TEXT NOT NULL
24
- );
25
-
26
- CREATE TABLE IF NOT EXISTS records (
27
- record_id TEXT PRIMARY KEY,
28
- decision_id TEXT NOT NULL,
29
- q_id TEXT NOT NULL,
30
- agent_id TEXT NOT NULL,
31
- domain TEXT NOT NULL,
32
- project_id TEXT NOT NULL DEFAULT 'default',
33
- decision_text TEXT NOT NULL,
34
- constraint_set_id TEXT NOT NULL,
35
- refs TEXT NOT NULL,
36
- status TEXT NOT NULL,
37
- supersedes_id TEXT,
38
- timestamp TEXT NOT NULL,
39
- record_json TEXT NOT NULL,
40
- FOREIGN KEY(constraint_set_id) REFERENCES constraint_sets(constraint_set_id)
41
- );
42
-
43
- CREATE TABLE IF NOT EXISTS projects (
44
- project_id TEXT PRIMARY KEY,
45
- display_name TEXT NOT NULL,
46
- source_hint TEXT,
47
- created_at TEXT NOT NULL
48
- );
49
-
50
- CREATE TABLE IF NOT EXISTS project_sharing (
51
- source_project_id TEXT NOT NULL,
52
- target_project_id TEXT NOT NULL,
53
- direction TEXT NOT NULL DEFAULT 'one-way',
54
- enabled INTEGER NOT NULL DEFAULT 1,
55
- PRIMARY KEY (source_project_id, target_project_id)
56
- );
57
-
58
- CREATE TABLE IF NOT EXISTS search_log (
59
- search_id INTEGER PRIMARY KEY AUTOINCREMENT,
60
- query TEXT NOT NULL,
61
- domain TEXT,
62
- project_id TEXT NOT NULL,
63
- hits INTEGER NOT NULL DEFAULT 0,
64
- cross_project_hits INTEGER NOT NULL DEFAULT 0,
65
- searched_at TEXT NOT NULL
66
- );
67
- `);
68
- }
69
- else {
70
- // Legacy migration: Database exists but lacks the v1 schema columns.
71
- this.db.exec("BEGIN TRANSACTION;");
72
- try {
73
- this.db.exec(`
74
- CREATE TABLE IF NOT EXISTS constraint_sets (
75
- constraint_set_id TEXT PRIMARY KEY,
76
- atoms TEXT NOT NULL,
77
- summary TEXT NOT NULL,
78
- created_at TEXT NOT NULL
79
- );
80
- `);
81
- // Alter legacy records table to inject the new columns gracefully
82
- const queries = [
83
- "ALTER TABLE records ADD COLUMN q_id TEXT NOT NULL DEFAULT 'legacy_q'",
84
- "ALTER TABLE records ADD COLUMN constraint_set_id TEXT NOT NULL DEFAULT 'legacy_c'",
85
- "ALTER TABLE records ADD COLUMN refs TEXT NOT NULL DEFAULT '[]'",
86
- "ALTER TABLE records ADD COLUMN status TEXT NOT NULL DEFAULT 'active'",
87
- "ALTER TABLE records ADD COLUMN supersedes_id TEXT",
88
- "ALTER TABLE records ADD COLUMN timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
89
- "ALTER TABLE records ADD COLUMN decision_text TEXT NOT NULL DEFAULT ''"
90
- ];
91
- for (const query of queries) {
92
- try {
93
- this.db.exec(query);
94
- }
95
- catch (e) {
96
- // Ignore "duplicate column name" if partially migrated
97
- }
98
- }
99
- // Migrate existing 'answer' data over to 'decision_text' if applicable
100
- try {
101
- this.db.exec("UPDATE records SET decision_text = answer WHERE decision_text = '' AND answer IS NOT NULL");
102
- }
103
- catch (e) {
104
- // 'answer' column might have been dropped or didn't exist
105
- }
106
- this.db.exec("COMMIT;");
107
- }
108
- catch (e) {
109
- this.db.exec("ROLLBACK;");
110
- throw e;
111
- }
112
- }
113
- this.db.exec('PRAGMA user_version = 1');
114
- // Ensure current project exists in projects table
115
- this.ensureProject(this.projectId);
116
- }
117
- ensureProject(projectId, displayName) {
118
- const existing = this.db
119
- .prepare('SELECT project_id FROM projects WHERE project_id = ?')
120
- .get(projectId);
121
- if (!existing) {
122
- this.db.prepare(`
123
- INSERT INTO projects (project_id, display_name, source_hint, created_at)
124
- VALUES (@project_id, @display_name, @source_hint, @created_at)
125
- `).run({
126
- project_id: projectId,
127
- display_name: displayName || projectId.replace(/[-_]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),
128
- source_hint: null,
129
- created_at: new Date().toISOString()
130
- });
131
- }
132
- }
133
- // --- Project & Sharing Management ---
134
- getProjects() {
135
- return this.db.prepare('SELECT * FROM projects ORDER BY created_at ASC').all();
136
- }
137
- getSharing() {
138
- return this.db.prepare('SELECT * FROM project_sharing ORDER BY source_project_id').all();
139
- }
140
- upsertSharing(source, target, direction, enabled) {
141
- this.db.prepare(`
142
- INSERT INTO project_sharing (source_project_id, target_project_id, direction, enabled)
143
- VALUES (@source, @target, @direction, @enabled)
144
- ON CONFLICT(source_project_id, target_project_id)
145
- DO UPDATE SET direction = @direction, enabled = @enabled
146
- `).run({ source, target, direction: direction, enabled: enabled ? 1 : 0 });
147
- }
148
- deleteSharing(source, target) {
149
- this.db.prepare('DELETE FROM project_sharing WHERE source_project_id = ? AND target_project_id = ?')
150
- .run([source, target]);
16
+ this.db.exec(`
17
+ CREATE TABLE IF NOT EXISTS constraint_sets (constraint_set_id TEXT PRIMARY KEY, atoms TEXT NOT NULL, summary TEXT NOT NULL);
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
+ `);
151
31
  }
152
- getAccessibleProjects() {
153
- const hasRules = this.db.prepare(`
154
- SELECT 1 FROM project_sharing
155
- WHERE source_project_id = ? OR target_project_id = ?
156
- LIMIT 1
157
- `).get(this.projectId, this.projectId);
158
- if (!hasRules) {
159
- const all = this.db.prepare('SELECT project_id FROM projects').all();
160
- const ids = new Set(all.map(r => r.project_id));
161
- ids.add(this.projectId);
162
- return [...ids];
163
- }
164
- const accessible = new Set([this.projectId]);
165
- const inbound = this.db.prepare(`
166
- SELECT source_project_id FROM project_sharing
167
- WHERE target_project_id = ? AND enabled = 1
168
- `).all(this.projectId);
169
- for (const row of inbound) {
170
- accessible.add(row.source_project_id);
171
- }
172
- const bidir = this.db.prepare(`
173
- SELECT source_project_id, target_project_id FROM project_sharing
174
- WHERE direction = 'bi-directional' AND enabled = 1
175
- AND (source_project_id = ? OR target_project_id = ?)
176
- `).all([this.projectId, this.projectId]);
177
- for (const row of bidir) {
178
- accessible.add(row.source_project_id);
179
- accessible.add(row.target_project_id);
180
- }
181
- return [...accessible];
32
+ async saveDecision(decision) {
33
+ this.db.prepare('INSERT OR REPLACE INTO decisions(decision_id, decision_json) VALUES (?,?)').run([decision.decision_id, JSON.stringify(decision)]);
182
34
  }
183
- // --- Core Storage Methods ---
184
35
  async saveConstraintSet(constraintSet) {
185
- const statement = this.db.prepare(`
186
- INSERT OR IGNORE INTO constraint_sets (constraint_set_id, atoms, summary, created_at)
187
- VALUES (@constraint_set_id, @atoms, @summary, @created_at)
188
- `);
189
- statement.run({
190
- constraint_set_id: constraintSet.constraint_set_id,
191
- atoms: JSON.stringify(constraintSet.atoms),
192
- summary: constraintSet.summary,
193
- created_at: new Date().toISOString()
194
- });
36
+ 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]);
195
37
  }
196
38
  async getConstraintSet(id) {
197
- const row = this.db
198
- .prepare('SELECT constraint_set_id, atoms, summary FROM constraint_sets WHERE constraint_set_id = ?')
199
- .get(id);
200
- if (!row) {
201
- return null;
202
- }
203
- return {
204
- constraint_set_id: row.constraint_set_id,
205
- atoms: JSON.parse(row.atoms),
206
- summary: row.summary
207
- };
39
+ const row = this.db.prepare('SELECT * FROM constraint_sets WHERE constraint_set_id=?').get(id);
40
+ return row ? { constraint_set_id: row.constraint_set_id, atoms: JSON.parse(row.atoms), summary: row.summary } : null;
208
41
  }
209
42
  async saveRecord(record) {
210
- const statement = this.db.prepare(`
211
- INSERT OR REPLACE INTO records (
212
- record_id, decision_id, q_id, agent_id, domain, project_id, decision_text,
213
- constraint_set_id, refs, status, supersedes_id, timestamp, record_json
214
- )
215
- VALUES (
216
- @record_id, @decision_id, @q_id, @agent_id, @domain, @project_id, @decision_text,
217
- @constraint_set_id, @refs, @status, @supersedes_id, @timestamp, @record_json
218
- )
219
- `);
220
- const enrichedRecord = { ...record, project_id: this.projectId };
221
- statement.run({
222
- record_id: record.decision_id,
223
- decision_id: record.decision_id,
224
- q_id: record.q_id,
225
- agent_id: record.agent_id,
226
- domain: record.domain,
227
- project_id: this.projectId,
228
- decision_text: record.decision_text,
229
- constraint_set_id: record.constraint_set_id,
230
- refs: JSON.stringify(record.refs),
231
- status: record.status,
232
- supersedes_id: record.supersedes_id ?? null,
233
- timestamp: record.timestamp,
234
- record_json: JSON.stringify(enrichedRecord)
235
- });
43
+ if ("record_id" in record) {
44
+ await this.saveLegacyRecord(record);
45
+ return;
46
+ }
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)]);
48
+ }
49
+ 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)]);
236
51
  }
237
52
  async getRecord(recordId) {
238
- const row = this.db
239
- .prepare('SELECT record_json FROM records WHERE decision_id = ?')
240
- .get(recordId);
241
- if (!row) {
242
- return null;
243
- }
244
- return JSON.parse(row.record_json);
53
+ 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;
245
55
  }
246
56
  async searchRecords(opts) {
247
- const conditions = [];
248
- const params = {};
249
- if (opts.includeShared !== false) {
250
- const accessible = this.getAccessibleProjects();
251
- const placeholders = accessible.map((_, i) => `@proj${i}`);
252
- conditions.push(`records.project_id IN (${placeholders.join(', ')})`);
253
- accessible.forEach((id, i) => { params[`proj${i}`] = id; });
254
- }
255
- else {
256
- const projId = opts.projectId || this.projectId;
257
- conditions.push('records.project_id = @projectId');
258
- params.projectId = projId;
259
- }
260
- if (opts.domain) {
261
- conditions.push('records.domain = @domain');
262
- params.domain = opts.domain;
263
- }
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)
59
+ rows = rows.filter(r => r.project_id === this.projectId);
60
+ if (opts.domain)
61
+ 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);
264
64
  if (opts.query) {
265
- conditions.push(`(
266
- LOWER(records.decision_text) LIKE @query OR
267
- LOWER(records.domain) LIKE @query OR
268
- LOWER(records.q_id) LIKE @query
269
- )`);
270
- params.query = `%${opts.query.toLowerCase()}%`;
271
- }
272
- const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
273
- const limitClause = opts.limit ? `LIMIT ${Math.max(1, opts.limit)}` : '';
274
- const rows = this.db
275
- .prepare(`SELECT records.record_json FROM records
276
- ${whereClause}
277
- ORDER BY records.timestamp DESC
278
- ${limitClause}`)
279
- .all(params);
280
- const results = rows.map((row) => JSON.parse(row.record_json));
281
- try {
282
- const crossProjectHits = results.filter((r) => {
283
- return r.project_id && r.project_id !== this.projectId;
284
- }).length;
285
- this.db.prepare(`
286
- INSERT INTO search_log(query, domain, project_id, hits, cross_project_hits, searched_at)
287
- VALUES(@query, @domain, @project_id, @hits, @cross_project_hits, @searched_at)
288
- `).run({
289
- query: opts.query || '',
290
- domain: opts.domain || null,
291
- project_id: this.projectId,
292
- hits: results.length,
293
- cross_project_hits: crossProjectHits,
294
- searched_at: new Date().toISOString()
65
+ 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); });
67
+ }
68
+ let parsed = rows.map(r => JSON.parse(r.payload_json));
69
+ if (opts.parentPrompt) {
70
+ const needle = opts.parentPrompt.toLowerCase();
71
+ parsed = parsed.filter((r) => {
72
+ const d = this.db.prepare('SELECT decision_json FROM decisions WHERE decision_id=?').get(r.decision_id);
73
+ if (!d)
74
+ return false;
75
+ const ctx = JSON.parse(d.decision_json);
76
+ return ctx.parent_prompt?.toLowerCase().includes(needle) ?? false;
295
77
  });
296
78
  }
297
- catch {
298
- // Non-critical: don't fail search if logging fails
299
- }
300
- return results;
79
+ return opts.limit ? parsed.slice(0, opts.limit) : parsed;
301
80
  }
302
81
  async getAgentDecisions(agentId, domain) {
303
- const statement = domain
304
- ? this.db.prepare('SELECT record_json FROM records WHERE agent_id = @agentId AND domain = @domain ORDER BY timestamp DESC')
305
- : this.db.prepare('SELECT record_json FROM records WHERE agent_id = @agentId ORDER BY timestamp DESC');
306
- const rows = statement.all({ agentId, domain });
307
- return rows.map((row) => JSON.parse(row.record_json));
82
+ 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));
84
+ return domain ? parsed.filter(r => r.domain === domain) : parsed;
308
85
  }
309
86
  async getCompetencyProfile(agentId) {
310
87
  const records = await this.getAgentDecisions(agentId);
311
- const domainCounts = new Map();
312
- for (const record of records) {
313
- const count = domainCounts.get(record.domain) ?? 0;
314
- domainCounts.set(record.domain, count + 1);
315
- }
316
- const domains = Array.from(domainCounts.keys());
317
- const strengths = [];
318
- const weaknesses = [];
319
- // Simply map domain frequency to strengths
320
- for (const [domain, count] of domainCounts.entries()) {
321
- if (count >= 5) {
322
- strengths.push(domain);
323
- }
324
- }
325
- return {
326
- agent_id: agentId,
327
- domains,
328
- strengths,
329
- weaknesses,
330
- updated_at: new Date().toISOString()
331
- };
88
+ const stats = new Map();
89
+ for (const r of records) {
90
+ const c = stats.get(r.domain) ?? { count: 0, conf: 0 };
91
+ c.count += 1;
92
+ c.conf += ("confidence" in r ? r.confidence : 0.8);
93
+ stats.set(r.domain, c);
94
+ }
95
+ const domains = [...stats.keys()];
96
+ const strengths = domains.filter(d => { const s = stats.get(d); return (s.conf / s.count) >= 0.7; });
97
+ const weaknesses = domains.filter(d => { const s = stats.get(d); return (s.conf / s.count) < 0.7; });
98
+ return { agent_id: agentId, domains, strengths, weaknesses, updated_at: new Date().toISOString() };
332
99
  }
333
100
  }
334
101
  exports.SQLiteStorage = SQLiteStorage;
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { CompetencyProfile, DecisionRecord, DecisionStatus } from '@learningnodes/elen-core';
1
+ import type { CompetencyProfile, DecisionRecord, DecisionStatus, ConstraintSet } from '@learningnodes/elen-core';
2
2
  export interface ElenConfig {
3
3
  agentId: string;
4
4
  projectId?: string;
@@ -6,6 +6,7 @@ export interface ElenConfig {
6
6
  sqlitePath?: string;
7
7
  apiUrl?: string;
8
8
  apiKey?: string;
9
+ defaultProjectIsolation?: 'strict' | 'open';
9
10
  }
10
11
  export interface CommitDecisionInput {
11
12
  question: string;
@@ -16,15 +17,31 @@ export interface CommitDecisionInput {
16
17
  status?: DecisionStatus;
17
18
  supersedesId?: string;
18
19
  }
20
+ export interface LogDecisionInput {
21
+ question: string;
22
+ domain: string;
23
+ constraints: string[];
24
+ evidence: string[];
25
+ confidence?: number[];
26
+ answer: string;
27
+ parentPrompt?: string;
28
+ linkedPrecedents?: string[];
29
+ }
19
30
  export interface SearchOptions {
20
31
  domain?: string;
21
32
  projectId?: string;
22
33
  includeShared?: boolean;
23
34
  query?: string;
24
35
  limit?: number;
36
+ minConfidence?: number;
37
+ parentPrompt?: string;
25
38
  }
26
39
  export interface SearchPrecedentsOptions {
27
40
  limit?: number;
28
41
  }
29
42
  export type DecisionRecordResult = DecisionRecord;
30
43
  export type CompetencyProfileResult = CompetencyProfile;
44
+ export type ExpandedDecision = {
45
+ record: DecisionRecord;
46
+ constraints: ConstraintSet;
47
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@learningnodes/elen",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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": "file:../core",
13
+ "@learningnodes/elen-core": "^0.1.3",
14
14
  "better-sqlite3": "^11.7.0",
15
15
  "nanoid": "^5.1.4"
16
16
  },
@@ -18,5 +18,8 @@
18
18
  "@types/better-sqlite3": "^7.6.12",
19
19
  "typescript": "^5.6.3",
20
20
  "vitest": "^2.1.8"
21
+ },
22
+ "exports": {
23
+ ".": "./dist/index.js"
21
24
  }
22
- }
25
+ }
package/src/client.ts CHANGED
@@ -1,20 +1,93 @@
1
1
  import {
2
- decisionRecordSchema,
2
+ minimalDecisionRecordSchema,
3
3
  constraintSetSchema,
4
+ classifyEpistemicType,
5
+ type MinimalDecisionRecord,
4
6
  type DecisionRecord,
5
- type ConstraintSet
7
+ type ConstraintSet,
8
+ type Constraint,
9
+ type Evidence,
10
+ type Check
6
11
  } from '@learningnodes/elen-core';
7
12
  import { createId, createDecisionId, createConstraintSetId } from './id';
8
13
  import type { StorageAdapter } from './storage';
9
- import type { CompetencyProfileResult, CommitDecisionInput, SearchOptions } from './types';
14
+ import type { CompetencyProfileResult, CommitDecisionInput, LogDecisionInput, SearchOptions } from './types';
10
15
 
11
16
  export class ElenClient {
12
17
  constructor(private readonly agentId: string, private readonly storage: StorageAdapter) { }
13
18
 
14
- async commitDecision(input: CommitDecisionInput): Promise<DecisionRecord> {
19
+ async logDecision(input: LogDecisionInput): Promise<DecisionRecord> {
20
+ if (input.constraints.length === 0) throw new Error('Decision must include at least one constraint');
21
+ if (input.evidence.length === 0) throw new Error('Decision must include at least one evidence');
22
+
23
+ const decisionId = createDecisionId(input.domain);
24
+ const now = new Date().toISOString();
25
+
26
+ const constraintsSnapshot: Constraint[] = input.constraints.map((description, i) => ({
27
+ constraint_id: createId(`con${i}`),
28
+ decision_id: decisionId,
29
+ type: 'requirement',
30
+ description,
31
+ locked: false
32
+ }));
33
+
34
+ const evidenceSnapshot: Evidence[] = input.evidence.map((e, i) => ({
35
+ evidence_id: createId(`evd${i}`),
36
+ decision_id: decisionId,
37
+ type: input.linkedPrecedents?.[i] ? 'precedent' : 'observation',
38
+ claim: e,
39
+ proof: e,
40
+ confidence: input.confidence?.[i] ?? 0.8,
41
+ linked_precedent: input.linkedPrecedents?.[i]
42
+ }));
43
+
44
+ const checksSnapshot: Check[] = evidenceSnapshot.map((e, i) => ({
45
+ check_id: createId(`chk${i}`),
46
+ decision_id: decisionId,
47
+ claim: e.claim,
48
+ result: 'pass',
49
+ evidence_ids: [e.evidence_id],
50
+ epistemic_type: classifyEpistemicType(e),
51
+ confidence: e.confidence
52
+ }));
53
+
54
+ const record: DecisionRecord = {
55
+ record_id: createId('rec'),
56
+ decision_id: decisionId,
57
+ agent_id: this.agentId,
58
+ question: input.question,
59
+ answer: input.answer,
60
+ constraints_snapshot: constraintsSnapshot,
61
+ evidence_snapshot: evidenceSnapshot,
62
+ checks_snapshot: checksSnapshot,
63
+ confidence: evidenceSnapshot.reduce((a, e) => a + e.confidence, 0) / evidenceSnapshot.length,
64
+ validation_type: 'self',
65
+ domain: input.domain,
66
+ tags: [],
67
+ published_at: now,
68
+ expires_at: null
69
+ };
70
+
71
+ await this.storage.saveDecision?.({
72
+ decision_id: decisionId,
73
+ agent_id: this.agentId,
74
+ question: input.question,
75
+ domain: input.domain,
76
+ status: 'validated',
77
+ constraints: constraintsSnapshot,
78
+ evidence: evidenceSnapshot,
79
+ checks: checksSnapshot,
80
+ created_at: now,
81
+ parent_prompt: input.parentPrompt
82
+ });
83
+
84
+ await this.storage.saveLegacyRecord?.(record);
85
+ return record;
86
+ }
87
+
88
+ async commitDecision(input: CommitDecisionInput): Promise<MinimalDecisionRecord> {
15
89
  const now = new Date().toISOString();
16
90
 
17
- // 1. Resolve Constraints (Deterministic Hashing server-side)
18
91
  const constraintSetId = createConstraintSetId(input.constraints);
19
92
  const existingConstraints = await this.storage.getConstraintSet(constraintSetId);
20
93
 
@@ -28,10 +101,10 @@ export class ElenClient {
28
101
  await this.storage.saveConstraintSet(newSet);
29
102
  }
30
103
 
31
- // 2. Build the Minimal Decision Atom
32
- const record: DecisionRecord = {
104
+ const record: MinimalDecisionRecord = {
33
105
  decision_id: createDecisionId(input.domain),
34
106
  q_id: createId('q'),
107
+ question_text: input.question,
35
108
  decision_text: input.decisionText,
36
109
  constraint_set_id: constraintSetId,
37
110
  refs: input.refs ?? [],
@@ -42,44 +115,50 @@ export class ElenClient {
42
115
  domain: input.domain
43
116
  };
44
117
 
45
- decisionRecordSchema.parse(record);
118
+ minimalDecisionRecordSchema.parse(record);
46
119
  await this.storage.saveRecord(record);
47
120
 
48
121
  return record;
49
122
  }
50
123
 
51
- async supersedeDecision(oldDecisionId: string, input: CommitDecisionInput): Promise<DecisionRecord> {
52
- // 1. Mark old as superseded
124
+ async supersedeDecision(oldDecisionId: string, input: CommitDecisionInput): Promise<MinimalDecisionRecord> {
53
125
  const oldRecord = await this.storage.getRecord(oldDecisionId);
54
- if (oldRecord) {
126
+ if (oldRecord && 'status' in oldRecord) {
55
127
  oldRecord.status = 'superseded';
56
128
  await this.storage.saveRecord(oldRecord);
57
129
  }
58
130
 
59
- // 2. Commit new
60
- return this.commitDecision({
61
- ...input,
62
- supersedesId: oldDecisionId
63
- });
131
+ return this.commitDecision({ ...input, supersedesId: oldDecisionId });
64
132
  }
65
133
 
66
- async suggest(opts: SearchOptions): Promise<Partial<DecisionRecord>[]> {
67
- const fullRecords = await this.storage.searchRecords(opts);
134
+ async searchRecords(opts: SearchOptions) {
135
+ return this.storage.searchRecords(opts);
136
+ }
68
137
 
69
- // Pointer-first retrieval (minimal payload)
70
- return fullRecords.map(r => ({
71
- decision_id: r.decision_id,
72
- status: r.status,
73
- decision_text: r.decision_text,
74
- constraint_set_id: r.constraint_set_id,
75
- refs: r.refs,
76
- supersedes_id: r.supersedes_id
77
- }));
138
+ async searchPrecedents(query: string, opts: SearchOptions = {}) {
139
+ const direct = await this.storage.searchRecords({ ...opts, query });
140
+ if (direct.length > 0) return direct;
141
+ return this.storage.searchRecords({ ...opts, limit: opts.limit ?? 5 });
142
+ }
143
+
144
+ async suggest(opts: SearchOptions): Promise<Array<Partial<MinimalDecisionRecord>>> {
145
+ const fullRecords = await this.storage.searchRecords(opts);
146
+ return fullRecords
147
+ .filter((r): r is MinimalDecisionRecord => 'decision_text' in r)
148
+ .map((r) => ({
149
+ decision_id: r.decision_id,
150
+ status: r.status,
151
+ decision_text: r.decision_text,
152
+ question_text: r.question_text,
153
+ constraint_set_id: r.constraint_set_id,
154
+ refs: r.refs,
155
+ supersedes_id: r.supersedes_id
156
+ }));
78
157
  }
79
158
 
80
- async expand(decisionId: string): Promise<{ record: DecisionRecord, constraints: ConstraintSet } | null> {
159
+ async expand(decisionId: string): Promise<{ record: MinimalDecisionRecord, constraints: ConstraintSet } | null> {
81
160
  const record = await this.storage.getRecord(decisionId);
82
- if (!record) return null;
161
+ if (!record || !('constraint_set_id' in record)) return null;
83
162
 
84
163
  const constraints = await this.storage.getConstraintSet(record.constraint_set_id);
85
164
  if (!constraints) return null;