@learningnodes/elen 0.1.1
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/client_test_output.txt +17 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +80 -0
- package/dist/id.d.ts +3 -0
- package/dist/id.js +20 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +51 -0
- package/dist/storage/index.d.ts +3 -0
- package/dist/storage/index.js +19 -0
- package/dist/storage/interface.d.ts +11 -0
- package/dist/storage/interface.js +2 -0
- package/dist/storage/memory.d.ts +14 -0
- package/dist/storage/memory.js +69 -0
- package/dist/storage/sqlite.d.ts +34 -0
- package/dist/storage/sqlite.js +334 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.js +2 -0
- package/package.json +22 -0
- package/src/client.ts +93 -0
- package/src/id.ts +18 -0
- package/src/index.ts +45 -0
- package/src/shims.d.ts +27 -0
- package/src/storage/index.ts +3 -0
- package/src/storage/interface.ts +12 -0
- package/src/storage/memory.ts +85 -0
- package/src/storage/sqlite.ts +397 -0
- package/src/types.ts +35 -0
- package/test_output.txt +78 -0
- package/tests/client.test.ts +147 -0
- package/tests/integration.test.ts +49 -0
- package/tests/storage.test.ts +100 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import type { CompetencyProfile, DecisionRecord, ConstraintSet } from '@learningnodes/elen-core';
|
|
3
|
+
import type { SearchOptions } from '../types';
|
|
4
|
+
import type { StorageAdapter } from './interface';
|
|
5
|
+
|
|
6
|
+
export interface ProjectRecord {
|
|
7
|
+
project_id: string;
|
|
8
|
+
display_name: string;
|
|
9
|
+
source_hint: string | null;
|
|
10
|
+
created_at: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ProjectSharingRecord {
|
|
14
|
+
source_project_id: string;
|
|
15
|
+
target_project_id: string;
|
|
16
|
+
direction: 'one-way' | 'bi-directional';
|
|
17
|
+
enabled: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class SQLiteStorage implements StorageAdapter {
|
|
21
|
+
private readonly db: Database.Database;
|
|
22
|
+
private readonly projectId: string;
|
|
23
|
+
|
|
24
|
+
constructor(path: string, projectId: string = 'default') {
|
|
25
|
+
this.db = new Database(path);
|
|
26
|
+
this.projectId = projectId;
|
|
27
|
+
this.init();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private init(): void {
|
|
31
|
+
const pragmaQuery = this.db.prepare('PRAGMA user_version').get() as { user_version: number };
|
|
32
|
+
const versionRow = pragmaQuery ? pragmaQuery.user_version : 0;
|
|
33
|
+
|
|
34
|
+
if (versionRow === 0) {
|
|
35
|
+
this.db.exec(`
|
|
36
|
+
CREATE TABLE IF NOT EXISTS constraint_sets (
|
|
37
|
+
constraint_set_id TEXT PRIMARY KEY,
|
|
38
|
+
atoms TEXT NOT NULL,
|
|
39
|
+
summary TEXT NOT NULL,
|
|
40
|
+
created_at TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
CREATE TABLE IF NOT EXISTS records (
|
|
44
|
+
record_id TEXT PRIMARY KEY,
|
|
45
|
+
decision_id TEXT NOT NULL,
|
|
46
|
+
q_id TEXT NOT NULL,
|
|
47
|
+
agent_id TEXT NOT NULL,
|
|
48
|
+
domain TEXT NOT NULL,
|
|
49
|
+
project_id TEXT NOT NULL DEFAULT 'default',
|
|
50
|
+
decision_text TEXT NOT NULL,
|
|
51
|
+
constraint_set_id TEXT NOT NULL,
|
|
52
|
+
refs TEXT NOT NULL,
|
|
53
|
+
status TEXT NOT NULL,
|
|
54
|
+
supersedes_id TEXT,
|
|
55
|
+
timestamp TEXT NOT NULL,
|
|
56
|
+
record_json TEXT NOT NULL,
|
|
57
|
+
FOREIGN KEY(constraint_set_id) REFERENCES constraint_sets(constraint_set_id)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
61
|
+
project_id TEXT PRIMARY KEY,
|
|
62
|
+
display_name TEXT NOT NULL,
|
|
63
|
+
source_hint TEXT,
|
|
64
|
+
created_at TEXT NOT NULL
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE IF NOT EXISTS project_sharing (
|
|
68
|
+
source_project_id TEXT NOT NULL,
|
|
69
|
+
target_project_id TEXT NOT NULL,
|
|
70
|
+
direction TEXT NOT NULL DEFAULT 'one-way',
|
|
71
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
72
|
+
PRIMARY KEY (source_project_id, target_project_id)
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
CREATE TABLE IF NOT EXISTS search_log (
|
|
76
|
+
search_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
77
|
+
query TEXT NOT NULL,
|
|
78
|
+
domain TEXT,
|
|
79
|
+
project_id TEXT NOT NULL,
|
|
80
|
+
hits INTEGER NOT NULL DEFAULT 0,
|
|
81
|
+
cross_project_hits INTEGER NOT NULL DEFAULT 0,
|
|
82
|
+
searched_at TEXT NOT NULL
|
|
83
|
+
);
|
|
84
|
+
`);
|
|
85
|
+
} else {
|
|
86
|
+
// Legacy migration: Database exists but lacks the v1 schema columns.
|
|
87
|
+
this.db.exec("BEGIN TRANSACTION;");
|
|
88
|
+
try {
|
|
89
|
+
this.db.exec(`
|
|
90
|
+
CREATE TABLE IF NOT EXISTS constraint_sets (
|
|
91
|
+
constraint_set_id TEXT PRIMARY KEY,
|
|
92
|
+
atoms TEXT NOT NULL,
|
|
93
|
+
summary TEXT NOT NULL,
|
|
94
|
+
created_at TEXT NOT NULL
|
|
95
|
+
);
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
// Alter legacy records table to inject the new columns gracefully
|
|
99
|
+
const queries = [
|
|
100
|
+
"ALTER TABLE records ADD COLUMN q_id TEXT NOT NULL DEFAULT 'legacy_q'",
|
|
101
|
+
"ALTER TABLE records ADD COLUMN constraint_set_id TEXT NOT NULL DEFAULT 'legacy_c'",
|
|
102
|
+
"ALTER TABLE records ADD COLUMN refs TEXT NOT NULL DEFAULT '[]'",
|
|
103
|
+
"ALTER TABLE records ADD COLUMN status TEXT NOT NULL DEFAULT 'active'",
|
|
104
|
+
"ALTER TABLE records ADD COLUMN supersedes_id TEXT",
|
|
105
|
+
"ALTER TABLE records ADD COLUMN timestamp TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP",
|
|
106
|
+
"ALTER TABLE records ADD COLUMN decision_text TEXT NOT NULL DEFAULT ''"
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
for (const query of queries) {
|
|
110
|
+
try {
|
|
111
|
+
this.db.exec(query);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// Ignore "duplicate column name" if partially migrated
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Migrate existing 'answer' data over to 'decision_text' if applicable
|
|
118
|
+
try {
|
|
119
|
+
this.db.exec("UPDATE records SET decision_text = answer WHERE decision_text = '' AND answer IS NOT NULL");
|
|
120
|
+
} catch (e) {
|
|
121
|
+
// 'answer' column might have been dropped or didn't exist
|
|
122
|
+
}
|
|
123
|
+
this.db.exec("COMMIT;");
|
|
124
|
+
} catch (e) {
|
|
125
|
+
this.db.exec("ROLLBACK;");
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.db.exec('PRAGMA user_version = 1');
|
|
131
|
+
|
|
132
|
+
// Ensure current project exists in projects table
|
|
133
|
+
this.ensureProject(this.projectId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private ensureProject(projectId: string, displayName?: string): void {
|
|
137
|
+
const existing = this.db
|
|
138
|
+
.prepare('SELECT project_id FROM projects WHERE project_id = ?')
|
|
139
|
+
.get(projectId);
|
|
140
|
+
|
|
141
|
+
if (!existing) {
|
|
142
|
+
this.db.prepare(`
|
|
143
|
+
INSERT INTO projects (project_id, display_name, source_hint, created_at)
|
|
144
|
+
VALUES (@project_id, @display_name, @source_hint, @created_at)
|
|
145
|
+
`).run({
|
|
146
|
+
project_id: projectId,
|
|
147
|
+
display_name: displayName || projectId.replace(/[-_]/g, ' ').replace(/\\b\\w/g, c => c.toUpperCase()),
|
|
148
|
+
source_hint: null,
|
|
149
|
+
created_at: new Date().toISOString()
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// --- Project & Sharing Management ---
|
|
155
|
+
|
|
156
|
+
getProjects(): ProjectRecord[] {
|
|
157
|
+
return this.db.prepare('SELECT * FROM projects ORDER BY created_at ASC').all() as ProjectRecord[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getSharing(): ProjectSharingRecord[] {
|
|
161
|
+
return this.db.prepare('SELECT * FROM project_sharing ORDER BY source_project_id').all() as ProjectSharingRecord[];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
upsertSharing(source: string, target: string, direction: 'one-way' | 'bi-directional', enabled: boolean): void {
|
|
165
|
+
this.db.prepare(`
|
|
166
|
+
INSERT INTO project_sharing (source_project_id, target_project_id, direction, enabled)
|
|
167
|
+
VALUES (@source, @target, @direction, @enabled)
|
|
168
|
+
ON CONFLICT(source_project_id, target_project_id)
|
|
169
|
+
DO UPDATE SET direction = @direction, enabled = @enabled
|
|
170
|
+
`).run({ source, target, direction: direction, enabled: enabled ? 1 : 0 });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
deleteSharing(source: string, target: string): void {
|
|
174
|
+
this.db.prepare('DELETE FROM project_sharing WHERE source_project_id = ? AND target_project_id = ?')
|
|
175
|
+
.run([source, target]);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private getAccessibleProjects(): string[] {
|
|
179
|
+
const hasRules = this.db.prepare(`
|
|
180
|
+
SELECT 1 FROM project_sharing
|
|
181
|
+
WHERE source_project_id = ? OR target_project_id = ?
|
|
182
|
+
LIMIT 1
|
|
183
|
+
`).get(this.projectId, this.projectId);
|
|
184
|
+
|
|
185
|
+
if (!hasRules) {
|
|
186
|
+
const all = this.db.prepare('SELECT project_id FROM projects').all() as Array<{ project_id: string }>;
|
|
187
|
+
const ids = new Set(all.map(r => r.project_id));
|
|
188
|
+
ids.add(this.projectId);
|
|
189
|
+
return [...ids];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const accessible = new Set<string>([this.projectId]);
|
|
193
|
+
|
|
194
|
+
const inbound = this.db.prepare(`
|
|
195
|
+
SELECT source_project_id FROM project_sharing
|
|
196
|
+
WHERE target_project_id = ? AND enabled = 1
|
|
197
|
+
`).all(this.projectId) as Array<{ source_project_id: string }>;
|
|
198
|
+
|
|
199
|
+
for (const row of inbound) {
|
|
200
|
+
accessible.add(row.source_project_id);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const bidir = this.db.prepare(`
|
|
204
|
+
SELECT source_project_id, target_project_id FROM project_sharing
|
|
205
|
+
WHERE direction = 'bi-directional' AND enabled = 1
|
|
206
|
+
AND (source_project_id = ? OR target_project_id = ?)
|
|
207
|
+
`).all([this.projectId, this.projectId]) as Array<{ source_project_id: string; target_project_id: string }>;
|
|
208
|
+
|
|
209
|
+
for (const row of bidir) {
|
|
210
|
+
accessible.add(row.source_project_id);
|
|
211
|
+
accessible.add(row.target_project_id);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return [...accessible];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// --- Core Storage Methods ---
|
|
218
|
+
|
|
219
|
+
async saveConstraintSet(constraintSet: ConstraintSet): Promise<void> {
|
|
220
|
+
const statement = this.db.prepare(`
|
|
221
|
+
INSERT OR IGNORE INTO constraint_sets (constraint_set_id, atoms, summary, created_at)
|
|
222
|
+
VALUES (@constraint_set_id, @atoms, @summary, @created_at)
|
|
223
|
+
`);
|
|
224
|
+
|
|
225
|
+
statement.run({
|
|
226
|
+
constraint_set_id: constraintSet.constraint_set_id,
|
|
227
|
+
atoms: JSON.stringify(constraintSet.atoms),
|
|
228
|
+
summary: constraintSet.summary,
|
|
229
|
+
created_at: new Date().toISOString()
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async getConstraintSet(id: string): Promise<ConstraintSet | null> {
|
|
234
|
+
const row = this.db
|
|
235
|
+
.prepare('SELECT constraint_set_id, atoms, summary FROM constraint_sets WHERE constraint_set_id = ?')
|
|
236
|
+
.get(id) as { constraint_set_id: string; atoms: string; summary: string } | undefined;
|
|
237
|
+
|
|
238
|
+
if (!row) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
constraint_set_id: row.constraint_set_id,
|
|
244
|
+
atoms: JSON.parse(row.atoms) as string[],
|
|
245
|
+
summary: row.summary
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async saveRecord(record: DecisionRecord): Promise<void> {
|
|
250
|
+
const statement = this.db.prepare(`
|
|
251
|
+
INSERT OR REPLACE INTO records (
|
|
252
|
+
record_id, decision_id, q_id, agent_id, domain, project_id, decision_text,
|
|
253
|
+
constraint_set_id, refs, status, supersedes_id, timestamp, record_json
|
|
254
|
+
)
|
|
255
|
+
VALUES (
|
|
256
|
+
@record_id, @decision_id, @q_id, @agent_id, @domain, @project_id, @decision_text,
|
|
257
|
+
@constraint_set_id, @refs, @status, @supersedes_id, @timestamp, @record_json
|
|
258
|
+
)
|
|
259
|
+
`);
|
|
260
|
+
|
|
261
|
+
const enrichedRecord = { ...record, project_id: this.projectId };
|
|
262
|
+
|
|
263
|
+
statement.run({
|
|
264
|
+
record_id: record.decision_id,
|
|
265
|
+
decision_id: record.decision_id,
|
|
266
|
+
q_id: record.q_id,
|
|
267
|
+
agent_id: record.agent_id,
|
|
268
|
+
domain: record.domain,
|
|
269
|
+
project_id: this.projectId,
|
|
270
|
+
decision_text: record.decision_text,
|
|
271
|
+
constraint_set_id: record.constraint_set_id,
|
|
272
|
+
refs: JSON.stringify(record.refs),
|
|
273
|
+
status: record.status,
|
|
274
|
+
supersedes_id: record.supersedes_id ?? null,
|
|
275
|
+
timestamp: record.timestamp,
|
|
276
|
+
record_json: JSON.stringify(enrichedRecord)
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async getRecord(recordId: string): Promise<DecisionRecord | null> {
|
|
281
|
+
const row = this.db
|
|
282
|
+
.prepare('SELECT record_json FROM records WHERE decision_id = ?')
|
|
283
|
+
.get(recordId) as { record_json: string } | undefined;
|
|
284
|
+
|
|
285
|
+
if (!row) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return JSON.parse(row.record_json) as DecisionRecord;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async searchRecords(opts: SearchOptions): Promise<DecisionRecord[]> {
|
|
293
|
+
const conditions: string[] = [];
|
|
294
|
+
const params: Record<string, unknown> = {};
|
|
295
|
+
|
|
296
|
+
if (opts.includeShared !== false) {
|
|
297
|
+
const accessible = this.getAccessibleProjects();
|
|
298
|
+
const placeholders = accessible.map((_, i) => `@proj${i}`);
|
|
299
|
+
conditions.push(`records.project_id IN (${placeholders.join(', ')})`);
|
|
300
|
+
accessible.forEach((id, i) => { params[`proj${i}`] = id; });
|
|
301
|
+
} else {
|
|
302
|
+
const projId = opts.projectId || this.projectId;
|
|
303
|
+
conditions.push('records.project_id = @projectId');
|
|
304
|
+
params.projectId = projId;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (opts.domain) {
|
|
308
|
+
conditions.push('records.domain = @domain');
|
|
309
|
+
params.domain = opts.domain;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (opts.query) {
|
|
313
|
+
conditions.push(`(
|
|
314
|
+
LOWER(records.decision_text) LIKE @query OR
|
|
315
|
+
LOWER(records.domain) LIKE @query OR
|
|
316
|
+
LOWER(records.q_id) LIKE @query
|
|
317
|
+
)`);
|
|
318
|
+
params.query = `%${opts.query.toLowerCase()}%`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
322
|
+
const limitClause = opts.limit ? `LIMIT ${Math.max(1, opts.limit)}` : '';
|
|
323
|
+
|
|
324
|
+
const rows = this.db
|
|
325
|
+
.prepare(
|
|
326
|
+
`SELECT records.record_json FROM records
|
|
327
|
+
${whereClause}
|
|
328
|
+
ORDER BY records.timestamp DESC
|
|
329
|
+
${limitClause}`
|
|
330
|
+
)
|
|
331
|
+
.all(params) as Array<{ record_json: string }>;
|
|
332
|
+
|
|
333
|
+
const results = rows.map((row) => JSON.parse(row.record_json) as DecisionRecord);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const crossProjectHits = results.filter((r: DecisionRecord & { project_id?: string }) => {
|
|
337
|
+
return r.project_id && r.project_id !== this.projectId;
|
|
338
|
+
}).length;
|
|
339
|
+
|
|
340
|
+
this.db.prepare(`
|
|
341
|
+
INSERT INTO search_log(query, domain, project_id, hits, cross_project_hits, searched_at)
|
|
342
|
+
VALUES(@query, @domain, @project_id, @hits, @cross_project_hits, @searched_at)
|
|
343
|
+
`).run({
|
|
344
|
+
query: opts.query || '',
|
|
345
|
+
domain: opts.domain || null,
|
|
346
|
+
project_id: this.projectId,
|
|
347
|
+
hits: results.length,
|
|
348
|
+
cross_project_hits: crossProjectHits,
|
|
349
|
+
searched_at: new Date().toISOString()
|
|
350
|
+
});
|
|
351
|
+
} catch {
|
|
352
|
+
// Non-critical: don't fail search if logging fails
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return results;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async getAgentDecisions(agentId: string, domain?: string): Promise<DecisionRecord[]> {
|
|
359
|
+
const statement = domain
|
|
360
|
+
? this.db.prepare(
|
|
361
|
+
'SELECT record_json FROM records WHERE agent_id = @agentId AND domain = @domain ORDER BY timestamp DESC'
|
|
362
|
+
)
|
|
363
|
+
: this.db.prepare('SELECT record_json FROM records WHERE agent_id = @agentId ORDER BY timestamp DESC');
|
|
364
|
+
|
|
365
|
+
const rows = statement.all({ agentId, domain }) as Array<{ record_json: string }>;
|
|
366
|
+
return rows.map((row) => JSON.parse(row.record_json) as DecisionRecord);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async getCompetencyProfile(agentId: string): Promise<CompetencyProfile> {
|
|
370
|
+
const records = await this.getAgentDecisions(agentId);
|
|
371
|
+
const domainCounts = new Map<string, number>();
|
|
372
|
+
|
|
373
|
+
for (const record of records) {
|
|
374
|
+
const count = domainCounts.get(record.domain) ?? 0;
|
|
375
|
+
domainCounts.set(record.domain, count + 1);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const domains = Array.from(domainCounts.keys());
|
|
379
|
+
const strengths: string[] = [];
|
|
380
|
+
const weaknesses: string[] = [];
|
|
381
|
+
|
|
382
|
+
// Simply map domain frequency to strengths
|
|
383
|
+
for (const [domain, count] of domainCounts.entries()) {
|
|
384
|
+
if (count >= 5) {
|
|
385
|
+
strengths.push(domain);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
agent_id: agentId,
|
|
391
|
+
domains,
|
|
392
|
+
strengths,
|
|
393
|
+
weaknesses,
|
|
394
|
+
updated_at: new Date().toISOString()
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CompetencyProfile, DecisionRecord, DecisionStatus, ConstraintSet } from '@learningnodes/elen-core';
|
|
2
|
+
|
|
3
|
+
export interface ElenConfig {
|
|
4
|
+
agentId: string;
|
|
5
|
+
projectId?: string;
|
|
6
|
+
storage?: 'memory' | 'sqlite';
|
|
7
|
+
sqlitePath?: string;
|
|
8
|
+
apiUrl?: string;
|
|
9
|
+
apiKey?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CommitDecisionInput {
|
|
13
|
+
question: string;
|
|
14
|
+
domain: string;
|
|
15
|
+
decisionText: string;
|
|
16
|
+
constraints: string[];
|
|
17
|
+
refs?: string[];
|
|
18
|
+
status?: DecisionStatus;
|
|
19
|
+
supersedesId?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SearchOptions {
|
|
23
|
+
domain?: string;
|
|
24
|
+
projectId?: string;
|
|
25
|
+
includeShared?: boolean;
|
|
26
|
+
query?: string;
|
|
27
|
+
limit?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SearchPrecedentsOptions {
|
|
31
|
+
limit?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DecisionRecordResult = DecisionRecord;
|
|
35
|
+
export type CompetencyProfileResult = CompetencyProfile;
|
package/test_output.txt
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
[1m[7m[36m RUN [39m[27m[22m [36mv2.1.9 [39m[90mC:/Users/ln_ni/OneDrive/Desktop/Desktop/ventures/learningnodes/git/marketplace-repos/Elen/packages/sdk-ts[39m
|
|
3
|
+
|
|
4
|
+
[31mΓ¥»[39m tests/integration.test.ts [2m([22m[2m2 tests[22m[2m | [22m[31m2 failed[39m[2m)[22m[90m 52[2mms[22m[39m
|
|
5
|
+
[31m [31m×[31m Elen integration[2m > [22msupports batch decision logging and precedent search in memory mode[90m 11[2mms[22m[31m[39m
|
|
6
|
+
[31m → expected 0 to be greater than 0[39m
|
|
7
|
+
[31m [31m×[31m Elen integration[2m > [22msupports sqlite-backed usage[90m 39[2mms[22m[31m[39m
|
|
8
|
+
[31m → EBUSY: resource busy or locked, unlink 'C:\Users\ln_ni\AppData\Local\Temp\elen-int-1771463766853.db'[39m
|
|
9
|
+
|
|
10
|
+
node.exe : [31m⎯⎯⎯⎯⎯⎯⎯[1m[7m
|
|
11
|
+
Failed Tests 2
|
|
12
|
+
[27m[22m⎯⎯⎯⎯⎯⎯⎯[39m
|
|
13
|
+
At C:\Users\ln_ni\AppData\Roaming\npm\npx.ps1:24
|
|
14
|
+
char:5
|
|
15
|
+
+ & "node$exe"
|
|
16
|
+
"$basedir/node_modules/npm/bin/npx-cli.js" $args
|
|
17
|
+
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
18
|
+
~~~~~~~~~~~~~~~~~~~
|
|
19
|
+
+ CategoryInfo : NotSpecified: ([3
|
|
20
|
+
1mΓÄ»ΓÄ»ΓÄ»Γ...»ΓÄ»ΓÄ»ΓÄ»[39m:String) [], R
|
|
21
|
+
emoteException
|
|
22
|
+
+ FullyQualifiedErrorId : NativeCommandError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
[31m[1m[7m FAIL [27m[22m[39m
|
|
26
|
+
tests/integration.test.ts[2m > [22mElen
|
|
27
|
+
integration[2m > [22msupports batch decision
|
|
28
|
+
logging and precedent search in memory mode
|
|
29
|
+
[31m[1mAssertionError[22m: expected 0 to be
|
|
30
|
+
greater than 0[39m
|
|
31
|
+
[36m [2mΓ¥»[22m
|
|
32
|
+
tests/integration.test.ts:[2m29:31[22m[39m
|
|
33
|
+
[90m 27| [39m
|
|
34
|
+
[90m 28| [39m [35mconst[39m
|
|
35
|
+
precedents [33m=[39m [35mawait[39m elen[33m.
|
|
36
|
+
[39m[34msearchPrecedents[39m([32m'high
|
|
37
|
+
concurrency d[39m…
|
|
38
|
+
[90m 29| [39m [34mexpect[39m(precedent
|
|
39
|
+
s[33m.[39mlength)[33m.[39m[34mtoBeGreaterTha
|
|
40
|
+
n[39m([34m0[39m)[33m;[39m
|
|
41
|
+
[90m | [39m
|
|
42
|
+
[31m^[39m
|
|
43
|
+
[90m 30| [39m })[33m;[39m
|
|
44
|
+
[90m 31| [39m
|
|
45
|
+
|
|
46
|
+
[31m[2mΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»Γ
|
|
47
|
+
Ä»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»[1/2]ΓÄ»[22m[39
|
|
48
|
+
m
|
|
49
|
+
|
|
50
|
+
[31m[1m[7m FAIL [27m[22m[39m
|
|
51
|
+
tests/integration.test.ts[2m > [22mElen
|
|
52
|
+
integration[2m > [22msupports sqlite-backed
|
|
53
|
+
usage
|
|
54
|
+
[31m[1mError[22m: EBUSY: resource busy or
|
|
55
|
+
locked, unlink 'C:\Users\ln_ni\AppData\Local\Temp
|
|
56
|
+
\elen-int-1771463766853.db'[39m
|
|
57
|
+
[36m [2mΓ¥»[22m
|
|
58
|
+
tests/integration.test.ts:[2m47:5[22m[39m
|
|
59
|
+
[90m 45| [39m [34mexpect[39m(records)
|
|
60
|
+
[33m.[39m[34mtoHaveLength[39m([34m1[39m)[33
|
|
61
|
+
m;[39m
|
|
62
|
+
[90m 46| [39m
|
|
63
|
+
[90m 47| [39m
|
|
64
|
+
[34mrmSync[39m(dbPath[33m,[39m {
|
|
65
|
+
force[33m:[39m [35mtrue[39m })[33m;[39m
|
|
66
|
+
[90m | [39m [31m^[39m
|
|
67
|
+
[90m 48| [39m })[33m;[39m
|
|
68
|
+
[90m 49| [39m})[33m;[39m
|
|
69
|
+
|
|
70
|
+
[31m[2mΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»Γ
|
|
71
|
+
Ä»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»ΓÄ»[2/2]ΓÄ»[22m[39
|
|
72
|
+
m
|
|
73
|
+
|
|
74
|
+
[2m Test Files [22m [1m[31m1 failed[39m[22m[90m (1)[39m
|
|
75
|
+
[2m Tests [22m [1m[31m2 failed[39m[22m[90m (2)[39m
|
|
76
|
+
[2m Start at [22m 12:16:06
|
|
77
|
+
[2m Duration [22m 619ms[2m (transform 125ms, setup 0ms, collect 193ms, tests 52ms, environment 0ms, prepare 118ms)[22m
|
|
78
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ElenClient } from '../src/client';
|
|
3
|
+
import { createId } from '../src/id';
|
|
4
|
+
import { InMemoryStorage } from '../src/storage';
|
|
5
|
+
|
|
6
|
+
describe('ElenClient', () => {
|
|
7
|
+
it('logDecision creates a valid DecisionRecord', async () => {
|
|
8
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
9
|
+
|
|
10
|
+
const record = await client.logDecision({
|
|
11
|
+
question: 'Which DB?',
|
|
12
|
+
domain: 'infrastructure',
|
|
13
|
+
constraints: ['Must be open-source'],
|
|
14
|
+
evidence: ['benchmark: PostgreSQL 3200 TPS'],
|
|
15
|
+
answer: 'PostgreSQL'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(record.record_id.startsWith('rec-')).toBe(true);
|
|
19
|
+
expect(record.validation_type).toBe('self');
|
|
20
|
+
expect(record.constraints_snapshot).toHaveLength(1);
|
|
21
|
+
expect(record.evidence_snapshot).toHaveLength(1);
|
|
22
|
+
expect(record.checks_snapshot).toHaveLength(1);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('logDecision throws if no constraints provided', async () => {
|
|
26
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
27
|
+
|
|
28
|
+
await expect(
|
|
29
|
+
client.logDecision({
|
|
30
|
+
question: 'Which DB?',
|
|
31
|
+
domain: 'infrastructure',
|
|
32
|
+
constraints: [],
|
|
33
|
+
evidence: ['benchmark evidence'],
|
|
34
|
+
answer: 'PostgreSQL'
|
|
35
|
+
})
|
|
36
|
+
).rejects.toThrow('at least one constraint');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('logDecision throws if no evidence provided', async () => {
|
|
40
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
41
|
+
|
|
42
|
+
await expect(
|
|
43
|
+
client.logDecision({
|
|
44
|
+
question: 'Which DB?',
|
|
45
|
+
domain: 'infrastructure',
|
|
46
|
+
constraints: ['Must scale'],
|
|
47
|
+
evidence: [],
|
|
48
|
+
answer: 'PostgreSQL'
|
|
49
|
+
})
|
|
50
|
+
).rejects.toThrow('at least one evidence');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('logDecision auto-classifies epistemic types', async () => {
|
|
54
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
55
|
+
|
|
56
|
+
const record = await client.logDecision({
|
|
57
|
+
question: 'Which DB?',
|
|
58
|
+
domain: 'infrastructure',
|
|
59
|
+
constraints: ['Scale'],
|
|
60
|
+
evidence: ['benchmark measured 3200 TPS'],
|
|
61
|
+
answer: 'PostgreSQL'
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(record.checks_snapshot[0].epistemic_type).toBe('empirical');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('searchRecords filters by domain, minConfidence and parentPrompt', async () => {
|
|
68
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
69
|
+
|
|
70
|
+
await client.logDecision({
|
|
71
|
+
question: 'Which DB?',
|
|
72
|
+
domain: 'infrastructure',
|
|
73
|
+
parentPrompt: 'Build auth system',
|
|
74
|
+
constraints: ['Scale'],
|
|
75
|
+
evidence: ['benchmark 3200 TPS'],
|
|
76
|
+
answer: 'PostgreSQL'
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await client.logDecision({
|
|
80
|
+
question: 'Which color?',
|
|
81
|
+
domain: 'design',
|
|
82
|
+
parentPrompt: 'Design system refresh',
|
|
83
|
+
constraints: ['Accessible'],
|
|
84
|
+
evidence: ['usually blue works'],
|
|
85
|
+
confidence: [0.5],
|
|
86
|
+
answer: 'Blue'
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const byDomain = await client.searchRecords({ domain: 'infrastructure' });
|
|
90
|
+
const byConfidence = await client.searchRecords({ minConfidence: 0.8 });
|
|
91
|
+
const byParent = await client.searchRecords({ parentPrompt: 'auth system' });
|
|
92
|
+
|
|
93
|
+
expect(byDomain).toHaveLength(1);
|
|
94
|
+
expect(byConfidence).toHaveLength(1);
|
|
95
|
+
expect(byParent).toHaveLength(1);
|
|
96
|
+
expect(byParent[0].domain).toBe('infrastructure');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('competency profile is computed from records', async () => {
|
|
100
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
101
|
+
|
|
102
|
+
await client.logDecision({
|
|
103
|
+
question: 'DB?',
|
|
104
|
+
domain: 'infrastructure',
|
|
105
|
+
constraints: ['Scale'],
|
|
106
|
+
evidence: ['benchmark 3200 TPS'],
|
|
107
|
+
confidence: [0.9],
|
|
108
|
+
answer: 'PostgreSQL'
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await client.logDecision({
|
|
112
|
+
question: 'UI color?',
|
|
113
|
+
domain: 'design',
|
|
114
|
+
constraints: ['Accessible'],
|
|
115
|
+
evidence: ['rule of thumb says blue'],
|
|
116
|
+
confidence: [0.5],
|
|
117
|
+
answer: 'Blue'
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const profile = await client.getCompetencyProfile();
|
|
121
|
+
|
|
122
|
+
expect(profile.domains).toEqual(expect.arrayContaining(['infrastructure', 'design']));
|
|
123
|
+
expect(profile.strengths).toContain('infrastructure');
|
|
124
|
+
expect(profile.weaknesses).toContain('design');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('stores linked precedents in evidence', async () => {
|
|
128
|
+
const client = new ElenClient('agent-a', new InMemoryStorage());
|
|
129
|
+
|
|
130
|
+
const record = await client.logDecision({
|
|
131
|
+
question: 'Which pooler?',
|
|
132
|
+
domain: 'infrastructure',
|
|
133
|
+
constraints: ['Must work with PostgreSQL'],
|
|
134
|
+
evidence: ['PostgreSQL selected per precedent'],
|
|
135
|
+
linkedPrecedents: ['rec-abc123'],
|
|
136
|
+
answer: 'PgBouncer'
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
expect(record.evidence_snapshot[0].linked_precedent).toBe('rec-abc123');
|
|
140
|
+
expect(record.evidence_snapshot[0].type).toBe('precedent');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('ID generation produces unique IDs', () => {
|
|
144
|
+
const ids = new Set(Array.from({ length: 300 }, () => createId('rec')));
|
|
145
|
+
expect(ids.size).toBe(300);
|
|
146
|
+
});
|
|
147
|
+
});
|