@learningnodes/elen 0.1.2 → 0.1.4
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/package.json +7 -6
- package/client_test_output.txt +0 -17
- package/src/client.ts +0 -172
- package/src/id.ts +0 -18
- package/src/index.ts +0 -57
- package/src/shims.d.ts +0 -27
- package/src/storage/index.ts +0 -3
- package/src/storage/interface.ts +0 -20
- package/src/storage/memory.ts +0 -63
- package/src/storage/sqlite.ts +0 -95
- package/src/types.ts +0 -50
- package/test_output.txt +0 -78
- package/tests/client.test.ts +0 -147
- package/tests/integration.test.ts +0 -49
- package/tests/storage.test.ts +0 -100
- package/tsconfig.json +0 -16
package/package.json
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learningnodes/elen",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"license": "AGPL-3.0",
|
|
5
|
-
"main": "
|
|
6
|
-
"types": "
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "tsc -p tsconfig.json",
|
|
9
9
|
"test": "vitest run",
|
|
10
10
|
"lint": "tsc -p tsconfig.json --noEmit"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@learningnodes/elen-core": "
|
|
13
|
+
"@learningnodes/elen-core": "^0.1.4",
|
|
14
14
|
"better-sqlite3": "^11.7.0",
|
|
15
15
|
"nanoid": "^5.1.4"
|
|
16
16
|
},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"vitest": "^2.1.8"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
|
-
".": "./
|
|
23
|
+
".": "./dist/index.js"
|
|
24
24
|
}
|
|
25
|
-
}
|
|
25
|
+
}
|
|
26
|
+
|
package/client_test_output.txt
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mlogDecision creates a valid DecisionRecord
|
|
5
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mlogDecision throws if no constraints provided
|
|
6
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mlogDecision throws if no evidence provided
|
|
7
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mlogDecision auto-classifies epistemic types
|
|
8
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22msearchRecords filters by domain, minConfidence and parentPrompt
|
|
9
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mcompetency profile is computed from records
|
|
10
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mstores linked precedents in evidence
|
|
11
|
-
[32mΓ£ô[39m tests/client.test.ts[2m > [22mElenClient[2m > [22mID generation produces unique IDs
|
|
12
|
-
|
|
13
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
14
|
-
[2m Tests [22m [1m[32m8 passed[39m[22m[90m (8)[39m
|
|
15
|
-
[2m Start at [22m 12:18:15
|
|
16
|
-
[2m Duration [22m 2.30s[2m (transform 471ms, setup 0ms, collect 666ms, tests 51ms, environment 1ms, prepare 546ms)[22m
|
|
17
|
-
|
package/src/client.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
minimalDecisionRecordSchema,
|
|
3
|
-
constraintSetSchema,
|
|
4
|
-
classifyEpistemicType,
|
|
5
|
-
type MinimalDecisionRecord,
|
|
6
|
-
type DecisionRecord,
|
|
7
|
-
type ConstraintSet,
|
|
8
|
-
type Constraint,
|
|
9
|
-
type Evidence,
|
|
10
|
-
type Check
|
|
11
|
-
} from '@learningnodes/elen-core';
|
|
12
|
-
import { createId, createDecisionId, createConstraintSetId } from './id';
|
|
13
|
-
import type { StorageAdapter } from './storage';
|
|
14
|
-
import type { CompetencyProfileResult, CommitDecisionInput, LogDecisionInput, SearchOptions } from './types';
|
|
15
|
-
|
|
16
|
-
export class ElenClient {
|
|
17
|
-
constructor(private readonly agentId: string, private readonly storage: StorageAdapter) { }
|
|
18
|
-
|
|
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> {
|
|
89
|
-
const now = new Date().toISOString();
|
|
90
|
-
|
|
91
|
-
const constraintSetId = createConstraintSetId(input.constraints);
|
|
92
|
-
const existingConstraints = await this.storage.getConstraintSet(constraintSetId);
|
|
93
|
-
|
|
94
|
-
if (!existingConstraints) {
|
|
95
|
-
const newSet: ConstraintSet = {
|
|
96
|
-
constraint_set_id: constraintSetId,
|
|
97
|
-
atoms: input.constraints,
|
|
98
|
-
summary: `Auto-generated summary for ${input.constraints.length} constraints`
|
|
99
|
-
};
|
|
100
|
-
constraintSetSchema.parse(newSet);
|
|
101
|
-
await this.storage.saveConstraintSet(newSet);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const record: MinimalDecisionRecord = {
|
|
105
|
-
decision_id: createDecisionId(input.domain),
|
|
106
|
-
q_id: createId('q'),
|
|
107
|
-
question_text: input.question,
|
|
108
|
-
decision_text: input.decisionText,
|
|
109
|
-
constraint_set_id: constraintSetId,
|
|
110
|
-
refs: input.refs ?? [],
|
|
111
|
-
status: input.status ?? 'active',
|
|
112
|
-
supersedes_id: input.supersedesId,
|
|
113
|
-
timestamp: now,
|
|
114
|
-
agent_id: this.agentId,
|
|
115
|
-
domain: input.domain
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
minimalDecisionRecordSchema.parse(record);
|
|
119
|
-
await this.storage.saveRecord(record);
|
|
120
|
-
|
|
121
|
-
return record;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async supersedeDecision(oldDecisionId: string, input: CommitDecisionInput): Promise<MinimalDecisionRecord> {
|
|
125
|
-
const oldRecord = await this.storage.getRecord(oldDecisionId);
|
|
126
|
-
if (oldRecord && 'status' in oldRecord) {
|
|
127
|
-
oldRecord.status = 'superseded';
|
|
128
|
-
await this.storage.saveRecord(oldRecord);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return this.commitDecision({ ...input, supersedesId: oldDecisionId });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async searchRecords(opts: SearchOptions) {
|
|
135
|
-
return this.storage.searchRecords(opts);
|
|
136
|
-
}
|
|
137
|
-
|
|
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
|
-
}));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async expand(decisionId: string): Promise<{ record: MinimalDecisionRecord, constraints: ConstraintSet } | null> {
|
|
160
|
-
const record = await this.storage.getRecord(decisionId);
|
|
161
|
-
if (!record || !('constraint_set_id' in record)) return null;
|
|
162
|
-
|
|
163
|
-
const constraints = await this.storage.getConstraintSet(record.constraint_set_id);
|
|
164
|
-
if (!constraints) return null;
|
|
165
|
-
|
|
166
|
-
return { record, constraints };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async getCompetencyProfile(): Promise<CompetencyProfileResult> {
|
|
170
|
-
return this.storage.getCompetencyProfile(this.agentId);
|
|
171
|
-
}
|
|
172
|
-
}
|
package/src/id.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { randomBytes, createHash } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
export function createId(prefix: string): string {
|
|
4
|
-
return `${prefix}-${randomBytes(8).toString('base64url').slice(0, 10)}`;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function createDecisionId(domain: string): string {
|
|
8
|
-
// Analytical Prefix: 3 chars domain + 'A' (Agent) + 'T3' (Routine Tier)
|
|
9
|
-
const d = domain.toUpperCase().replace(/[^A-Z]/g, '').padEnd(3, 'X').substring(0, 3);
|
|
10
|
-
const prefix = `${d}AT3`;
|
|
11
|
-
return `dec:${prefix}-${randomBytes(4).toString('base64url').slice(0, 6)}`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createConstraintSetId(atoms: string[]): string {
|
|
15
|
-
const sorted = [...atoms].sort().join('|');
|
|
16
|
-
const hash = createHash('sha256').update(sorted).digest('hex').substring(0, 8);
|
|
17
|
-
return `cs:${hash}`;
|
|
18
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { ElenClient } from './client';
|
|
2
|
-
import { InMemoryStorage, SQLiteStorage, type StorageAdapter } from './storage';
|
|
3
|
-
import type { ElenConfig, CommitDecisionInput, LogDecisionInput, SearchOptions } from './types';
|
|
4
|
-
|
|
5
|
-
export class Elen {
|
|
6
|
-
private readonly client: ElenClient;
|
|
7
|
-
|
|
8
|
-
constructor(config: ElenConfig) {
|
|
9
|
-
const storage = this.createStorage(config);
|
|
10
|
-
this.client = new ElenClient(config.agentId, storage);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
private createStorage(config: ElenConfig): StorageAdapter {
|
|
14
|
-
if (config.storage === 'sqlite') {
|
|
15
|
-
return new SQLiteStorage(config.sqlitePath ?? 'elen.db', config.projectId, config.defaultProjectIsolation ?? 'strict');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return new InMemoryStorage();
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async logDecision(input: LogDecisionInput) {
|
|
22
|
-
return this.client.logDecision(input);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async commitDecision(input: CommitDecisionInput) {
|
|
26
|
-
return this.client.commitDecision(input);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async supersedeDecision(oldDecisionId: string, input: CommitDecisionInput) {
|
|
30
|
-
return this.client.supersedeDecision(oldDecisionId, input);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async searchRecords(opts: SearchOptions) {
|
|
34
|
-
return this.client.searchRecords(opts);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async searchPrecedents(query: string, opts: SearchOptions = {}) {
|
|
38
|
-
return this.client.searchPrecedents(query, opts);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async suggest(opts: SearchOptions) {
|
|
42
|
-
return this.client.suggest(opts);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async expand(decisionId: string) {
|
|
46
|
-
return this.client.expand(decisionId);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async getCompetencyProfile() {
|
|
50
|
-
return this.client.getCompetencyProfile();
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export * from './client';
|
|
55
|
-
export * from './id';
|
|
56
|
-
export * from './storage';
|
|
57
|
-
export * from './types';
|
package/src/shims.d.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
declare module 'nanoid' {
|
|
2
|
-
export function nanoid(size?: number): string;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
declare module 'better-sqlite3' {
|
|
6
|
-
interface Statement {
|
|
7
|
-
run(params?: unknown): unknown;
|
|
8
|
-
get(...params: unknown[]): unknown;
|
|
9
|
-
all(params?: unknown): unknown[];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface DatabaseInstance {
|
|
13
|
-
exec(sql: string): void;
|
|
14
|
-
prepare(sql: string): Statement;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface DatabaseConstructor {
|
|
18
|
-
new (path: string): DatabaseInstance;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const Database: DatabaseConstructor;
|
|
22
|
-
namespace Database {
|
|
23
|
-
export type Database = DatabaseInstance;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export default Database;
|
|
27
|
-
}
|
package/src/storage/index.ts
DELETED
package/src/storage/interface.ts
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CompetencyProfile,
|
|
3
|
-
ConstraintSet,
|
|
4
|
-
DecisionContext,
|
|
5
|
-
DecisionRecord,
|
|
6
|
-
MinimalDecisionRecord
|
|
7
|
-
} from '@learningnodes/elen-core';
|
|
8
|
-
import type { SearchOptions } from '../types';
|
|
9
|
-
|
|
10
|
-
export interface StorageAdapter {
|
|
11
|
-
saveDecision?(decision: DecisionContext): Promise<void>;
|
|
12
|
-
saveConstraintSet(constraintSet: ConstraintSet): Promise<void>;
|
|
13
|
-
getConstraintSet(id: string): Promise<ConstraintSet | null>;
|
|
14
|
-
saveRecord(record: MinimalDecisionRecord | DecisionRecord): Promise<void>;
|
|
15
|
-
saveLegacyRecord?(record: DecisionRecord): Promise<void>;
|
|
16
|
-
getRecord(recordId: string): Promise<MinimalDecisionRecord | DecisionRecord | null>;
|
|
17
|
-
searchRecords(opts: SearchOptions): Promise<Array<MinimalDecisionRecord | DecisionRecord>>;
|
|
18
|
-
getAgentDecisions(agentId: string, domain?: string): Promise<Array<MinimalDecisionRecord | DecisionRecord>>;
|
|
19
|
-
getCompetencyProfile(agentId: string): Promise<CompetencyProfile>;
|
|
20
|
-
}
|
package/src/storage/memory.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CompetencyProfile,
|
|
3
|
-
ConstraintSet,
|
|
4
|
-
DecisionContext,
|
|
5
|
-
DecisionRecord,
|
|
6
|
-
MinimalDecisionRecord
|
|
7
|
-
} from '@learningnodes/elen-core';
|
|
8
|
-
import type { SearchOptions } from '../types';
|
|
9
|
-
import type { StorageAdapter } from './interface';
|
|
10
|
-
|
|
11
|
-
export class InMemoryStorage implements StorageAdapter {
|
|
12
|
-
private readonly constraintSets = new Map<string, ConstraintSet>();
|
|
13
|
-
private readonly records = new Map<string, MinimalDecisionRecord | DecisionRecord>();
|
|
14
|
-
private readonly decisions = new Map<string, DecisionContext>();
|
|
15
|
-
|
|
16
|
-
async saveDecision(decision: DecisionContext): Promise<void> { this.decisions.set(decision.decision_id, decision); }
|
|
17
|
-
async saveConstraintSet(constraintSet: ConstraintSet): Promise<void> { if (!this.constraintSets.has(constraintSet.constraint_set_id)) this.constraintSets.set(constraintSet.constraint_set_id, constraintSet); }
|
|
18
|
-
async getConstraintSet(id: string): Promise<ConstraintSet | null> { return this.constraintSets.get(id) ?? null; }
|
|
19
|
-
async saveRecord(record: MinimalDecisionRecord | DecisionRecord): Promise<void> { this.records.set(("record_id" in record ? record.record_id : record.decision_id), record); }
|
|
20
|
-
async saveLegacyRecord(record: DecisionRecord): Promise<void> { this.records.set(record.record_id, record); }
|
|
21
|
-
async getRecord(recordId: string): Promise<MinimalDecisionRecord | DecisionRecord | null> { return this.records.get(recordId) ?? null; }
|
|
22
|
-
|
|
23
|
-
async searchRecords(opts: SearchOptions): Promise<Array<MinimalDecisionRecord | DecisionRecord>> {
|
|
24
|
-
let results = Array.from(this.records.values());
|
|
25
|
-
if (opts.domain) results = results.filter((r) => r.domain === opts.domain);
|
|
26
|
-
if (typeof opts.minConfidence === 'number') results = results.filter((r) => ('confidence' in r ? r.confidence >= opts.minConfidence! : true));
|
|
27
|
-
if (opts.parentPrompt) {
|
|
28
|
-
const needle = opts.parentPrompt.toLowerCase();
|
|
29
|
-
results = results.filter((r) => {
|
|
30
|
-
const ctx = this.decisions.get(r.decision_id);
|
|
31
|
-
return ctx?.parent_prompt?.toLowerCase().includes(needle) ?? false;
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
if (opts.query) {
|
|
35
|
-
const needle = opts.query.toLowerCase();
|
|
36
|
-
results = results.filter((r) => {
|
|
37
|
-
const haystack = 'decision_text' in r ? `${r.question_text ?? ''} ${r.decision_text} ${r.domain}` : `${r.question} ${r.answer} ${r.domain} ${r.constraints_snapshot.map(c=>c.description).join(' ')} ${r.evidence_snapshot.map(e=>`${e.claim} ${e.proof}`).join(' ')}`;
|
|
38
|
-
return haystack.toLowerCase().includes(needle);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
const limit = opts.limit ?? results.length;
|
|
42
|
-
return results.slice(0, limit);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async getAgentDecisions(agentId: string, domain?: string): Promise<Array<MinimalDecisionRecord | DecisionRecord>> {
|
|
46
|
-
return Array.from(this.records.values()).filter((r) => r.agent_id === agentId && (domain ? r.domain === domain : true));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async getCompetencyProfile(agentId: string): Promise<CompetencyProfile> {
|
|
50
|
-
const records = await this.getAgentDecisions(agentId);
|
|
51
|
-
const stats = new Map<string, {count:number; conf:number}>();
|
|
52
|
-
for (const r of records) {
|
|
53
|
-
const cur = stats.get(r.domain) ?? {count:0, conf:0};
|
|
54
|
-
cur.count += 1;
|
|
55
|
-
cur.conf += ("confidence" in r ? r.confidence : 0.8);
|
|
56
|
-
stats.set(r.domain, cur);
|
|
57
|
-
}
|
|
58
|
-
const domains = [...stats.keys()];
|
|
59
|
-
const strengths = domains.filter((d)=>{ const s=stats.get(d)!; return s.count >= 1 && (s.conf/s.count) >= 0.7;});
|
|
60
|
-
const weaknesses = domains.filter((d)=>{ const s=stats.get(d)!; return (s.conf/s.count) < 0.7;});
|
|
61
|
-
return { agent_id: agentId, domains, strengths, weaknesses, updated_at: new Date().toISOString() };
|
|
62
|
-
}
|
|
63
|
-
}
|
package/src/storage/sqlite.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import Database from 'better-sqlite3';
|
|
2
|
-
import type { CompetencyProfile, ConstraintSet, DecisionContext, DecisionRecord, MinimalDecisionRecord } from '@learningnodes/elen-core';
|
|
3
|
-
import type { SearchOptions } from '../types';
|
|
4
|
-
import type { StorageAdapter } from './interface';
|
|
5
|
-
|
|
6
|
-
export class SQLiteStorage implements StorageAdapter {
|
|
7
|
-
private readonly db: Database.Database;
|
|
8
|
-
private readonly projectId: string;
|
|
9
|
-
private readonly defaultIsolation: 'strict' | 'open';
|
|
10
|
-
|
|
11
|
-
constructor(path: string, projectId: string = 'default', defaultIsolation: 'strict' | 'open' = 'strict') {
|
|
12
|
-
this.db = new Database(path);
|
|
13
|
-
this.projectId = projectId;
|
|
14
|
-
this.defaultIsolation = defaultIsolation;
|
|
15
|
-
this.init();
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
private init() {
|
|
19
|
-
this.db.exec(`
|
|
20
|
-
CREATE TABLE IF NOT EXISTS constraint_sets (constraint_set_id TEXT PRIMARY KEY, atoms TEXT NOT NULL, summary TEXT NOT NULL);
|
|
21
|
-
CREATE TABLE IF NOT EXISTS decisions (decision_id TEXT PRIMARY KEY, decision_json TEXT NOT NULL);
|
|
22
|
-
CREATE TABLE IF NOT EXISTS records (
|
|
23
|
-
record_id TEXT PRIMARY KEY,
|
|
24
|
-
decision_id TEXT,
|
|
25
|
-
agent_id TEXT NOT NULL,
|
|
26
|
-
domain TEXT NOT NULL,
|
|
27
|
-
project_id TEXT NOT NULL,
|
|
28
|
-
question_text TEXT,
|
|
29
|
-
decision_text TEXT,
|
|
30
|
-
confidence REAL,
|
|
31
|
-
payload_json TEXT NOT NULL
|
|
32
|
-
);
|
|
33
|
-
`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async saveDecision(decision: DecisionContext): Promise<void> {
|
|
37
|
-
this.db.prepare('INSERT OR REPLACE INTO decisions(decision_id, decision_json) VALUES (?,?)').run([decision.decision_id, JSON.stringify(decision)]);
|
|
38
|
-
}
|
|
39
|
-
async saveConstraintSet(constraintSet: ConstraintSet): Promise<void> {
|
|
40
|
-
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]);
|
|
41
|
-
}
|
|
42
|
-
async getConstraintSet(id: string): Promise<ConstraintSet | null> {
|
|
43
|
-
const row = this.db.prepare('SELECT * FROM constraint_sets WHERE constraint_set_id=?').get(id) as any;
|
|
44
|
-
return row ? { constraint_set_id: row.constraint_set_id, atoms: JSON.parse(row.atoms), summary: row.summary } : null;
|
|
45
|
-
}
|
|
46
|
-
async saveRecord(record: MinimalDecisionRecord | DecisionRecord): Promise<void> {
|
|
47
|
-
if ("record_id" in record) {
|
|
48
|
-
await this.saveLegacyRecord(record);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
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)]);
|
|
52
|
-
}
|
|
53
|
-
async saveLegacyRecord(record: DecisionRecord): Promise<void> {
|
|
54
|
-
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)]);
|
|
55
|
-
}
|
|
56
|
-
async getRecord(recordId: string): Promise<MinimalDecisionRecord | DecisionRecord | null> {
|
|
57
|
-
const row = this.db.prepare('SELECT payload_json FROM records WHERE record_id=? OR decision_id=?').get([recordId, recordId]) as any;
|
|
58
|
-
return row ? JSON.parse(row.payload_json) : null;
|
|
59
|
-
}
|
|
60
|
-
async searchRecords(opts: SearchOptions): Promise<Array<MinimalDecisionRecord | DecisionRecord>> {
|
|
61
|
-
let rows = this.db.prepare('SELECT payload_json, decision_id, project_id, confidence, question_text, decision_text, domain FROM records').all() as any[];
|
|
62
|
-
if (this.defaultIsolation === 'strict' || opts.includeShared === false) rows = rows.filter(r => r.project_id === this.projectId);
|
|
63
|
-
if (opts.domain) rows = rows.filter(r => r.domain === opts.domain);
|
|
64
|
-
if (typeof opts.minConfidence === 'number') rows = rows.filter(r => r.confidence == null || r.confidence >= opts.minConfidence!);
|
|
65
|
-
if (opts.query) {
|
|
66
|
-
const q=opts.query.toLowerCase();
|
|
67
|
-
rows=rows.filter(r => { const p = JSON.parse(r.payload_json); const extra = p.constraints_snapshot ? `${p.constraints_snapshot.map((c:any)=>c.description).join(' ')} ${p.evidence_snapshot.map((e:any)=>`${e.claim} ${e.proof}`).join(' ')}` : ''; return `${r.question_text ?? ''} ${r.decision_text ?? ''} ${r.domain ?? ''} ${extra}`.toLowerCase().includes(q); });
|
|
68
|
-
}
|
|
69
|
-
let parsed = rows.map(r => JSON.parse(r.payload_json));
|
|
70
|
-
if (opts.parentPrompt) {
|
|
71
|
-
const needle = opts.parentPrompt.toLowerCase();
|
|
72
|
-
parsed = parsed.filter((r: any) => {
|
|
73
|
-
const d = this.db.prepare('SELECT decision_json FROM decisions WHERE decision_id=?').get(r.decision_id) as any;
|
|
74
|
-
if (!d) return false;
|
|
75
|
-
const ctx = JSON.parse(d.decision_json) as DecisionContext;
|
|
76
|
-
return ctx.parent_prompt?.toLowerCase().includes(needle) ?? false;
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return opts.limit ? parsed.slice(0, opts.limit) : parsed;
|
|
80
|
-
}
|
|
81
|
-
async getAgentDecisions(agentId: string, domain?: string): Promise<Array<MinimalDecisionRecord | DecisionRecord>> {
|
|
82
|
-
const rows = this.db.prepare('SELECT payload_json FROM records WHERE agent_id=?').all([agentId]) as any[];
|
|
83
|
-
const parsed = rows.map(r => JSON.parse(r.payload_json));
|
|
84
|
-
return domain ? parsed.filter(r => r.domain === domain) : parsed;
|
|
85
|
-
}
|
|
86
|
-
async getCompetencyProfile(agentId: string): Promise<CompetencyProfile> {
|
|
87
|
-
const records = await this.getAgentDecisions(agentId);
|
|
88
|
-
const stats = new Map<string, {count:number; conf:number}>();
|
|
89
|
-
for (const r of records) { const c=stats.get(r.domain) ?? {count:0, conf:0}; c.count +=1; c.conf += ("confidence" in r ? r.confidence : 0.8); stats.set(r.domain,c); }
|
|
90
|
-
const domains=[...stats.keys()];
|
|
91
|
-
const strengths=domains.filter(d=>{const s=stats.get(d)!; return (s.conf/s.count)>=0.7;});
|
|
92
|
-
const weaknesses=domains.filter(d=>{const s=stats.get(d)!; return (s.conf/s.count)<0.7;});
|
|
93
|
-
return { agent_id: agentId, domains, strengths, weaknesses, updated_at: new Date().toISOString() };
|
|
94
|
-
}
|
|
95
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
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
|
-
defaultProjectIsolation?: 'strict' | 'open';
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface CommitDecisionInput {
|
|
14
|
-
question: string;
|
|
15
|
-
domain: string;
|
|
16
|
-
decisionText: string;
|
|
17
|
-
constraints: string[];
|
|
18
|
-
refs?: string[];
|
|
19
|
-
status?: DecisionStatus;
|
|
20
|
-
supersedesId?: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface LogDecisionInput {
|
|
24
|
-
question: string;
|
|
25
|
-
domain: string;
|
|
26
|
-
constraints: string[];
|
|
27
|
-
evidence: string[];
|
|
28
|
-
confidence?: number[];
|
|
29
|
-
answer: string;
|
|
30
|
-
parentPrompt?: string;
|
|
31
|
-
linkedPrecedents?: string[];
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface SearchOptions {
|
|
35
|
-
domain?: string;
|
|
36
|
-
projectId?: string;
|
|
37
|
-
includeShared?: boolean;
|
|
38
|
-
query?: string;
|
|
39
|
-
limit?: number;
|
|
40
|
-
minConfidence?: number;
|
|
41
|
-
parentPrompt?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface SearchPrecedentsOptions {
|
|
45
|
-
limit?: number;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type DecisionRecordResult = DecisionRecord;
|
|
49
|
-
export type CompetencyProfileResult = CompetencyProfile;
|
|
50
|
-
export type ExpandedDecision = { record: DecisionRecord; constraints: ConstraintSet };
|
package/test_output.txt
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
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
|
-
|
package/tests/client.test.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
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
|
-
});
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { rmSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import { Elen } from '../src';
|
|
6
|
-
|
|
7
|
-
describe('Elen integration', () => {
|
|
8
|
-
it('supports batch decision logging and precedent search in memory mode', async () => {
|
|
9
|
-
const elen = new Elen({ agentId: 'agent-int', storage: 'memory' });
|
|
10
|
-
|
|
11
|
-
const first = await elen.logDecision({
|
|
12
|
-
question: 'Which database for sessions?',
|
|
13
|
-
domain: 'infrastructure',
|
|
14
|
-
constraints: ['Must support >1000 writes'],
|
|
15
|
-
evidence: ['pgbench: PostgreSQL 3200 TPS vs SQLite 280 TPS'],
|
|
16
|
-
answer: 'PostgreSQL 16'
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
await elen.logDecision({
|
|
20
|
-
question: 'Which pooler?',
|
|
21
|
-
domain: 'infrastructure',
|
|
22
|
-
constraints: ['Must work with PostgreSQL'],
|
|
23
|
-
evidence: ['PostgreSQL selected per precedent'],
|
|
24
|
-
linkedPrecedents: [first.record_id],
|
|
25
|
-
answer: 'PgBouncer'
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const precedents = await elen.searchPrecedents('high concurrency database selection');
|
|
29
|
-
expect(precedents.length).toBeGreaterThan(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('supports sqlite-backed usage', async () => {
|
|
33
|
-
const dbPath = join(tmpdir(), `elen-int-${Date.now()}.db`);
|
|
34
|
-
const elen = new Elen({ agentId: 'agent-sql', storage: 'sqlite', sqlitePath: dbPath });
|
|
35
|
-
|
|
36
|
-
await elen.logDecision({
|
|
37
|
-
question: 'Which cache?',
|
|
38
|
-
domain: 'infrastructure',
|
|
39
|
-
constraints: ['Must be open-source'],
|
|
40
|
-
evidence: ['documentation recommends Redis for this workload'],
|
|
41
|
-
answer: 'Redis'
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const records = await elen.searchRecords({ query: 'redis' });
|
|
45
|
-
expect(records).toHaveLength(1);
|
|
46
|
-
|
|
47
|
-
rmSync(dbPath, { force: true });
|
|
48
|
-
});
|
|
49
|
-
});
|
package/tests/storage.test.ts
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import { rmSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { describe, expect, it } from 'vitest';
|
|
5
|
-
import type { DecisionContext, DecisionRecord } from '@learningnodes/elen-core';
|
|
6
|
-
import { InMemoryStorage, SQLiteStorage } from '../src/storage';
|
|
7
|
-
|
|
8
|
-
function fixtures(): { context: DecisionContext; record: DecisionRecord } {
|
|
9
|
-
const decisionId = 'dec-1';
|
|
10
|
-
const context: DecisionContext = {
|
|
11
|
-
decision_id: decisionId,
|
|
12
|
-
agent_id: 'agent-a',
|
|
13
|
-
question: 'Which DB?',
|
|
14
|
-
domain: 'infrastructure',
|
|
15
|
-
status: 'validated',
|
|
16
|
-
constraints: [
|
|
17
|
-
{
|
|
18
|
-
constraint_id: 'con-1',
|
|
19
|
-
decision_id: decisionId,
|
|
20
|
-
type: 'requirement',
|
|
21
|
-
description: 'Must scale',
|
|
22
|
-
locked: false
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
evidence: [
|
|
26
|
-
{
|
|
27
|
-
evidence_id: 'evd-1',
|
|
28
|
-
decision_id: decisionId,
|
|
29
|
-
type: 'benchmark',
|
|
30
|
-
claim: 'Postgres faster',
|
|
31
|
-
proof: 'benchmark 3200 TPS',
|
|
32
|
-
confidence: 0.9
|
|
33
|
-
}
|
|
34
|
-
],
|
|
35
|
-
checks: [
|
|
36
|
-
{
|
|
37
|
-
check_id: 'chk-1',
|
|
38
|
-
decision_id: decisionId,
|
|
39
|
-
claim: 'Postgres faster',
|
|
40
|
-
result: 'pass',
|
|
41
|
-
evidence_ids: ['evd-1'],
|
|
42
|
-
epistemic_type: 'empirical',
|
|
43
|
-
confidence: 0.9
|
|
44
|
-
}
|
|
45
|
-
],
|
|
46
|
-
created_at: new Date().toISOString(),
|
|
47
|
-
parent_prompt: 'Build auth system'
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
const record: DecisionRecord = {
|
|
51
|
-
record_id: 'rec-1',
|
|
52
|
-
decision_id: decisionId,
|
|
53
|
-
agent_id: 'agent-a',
|
|
54
|
-
question: 'Which DB?',
|
|
55
|
-
answer: 'PostgreSQL',
|
|
56
|
-
constraints_snapshot: context.constraints,
|
|
57
|
-
evidence_snapshot: context.evidence,
|
|
58
|
-
checks_snapshot: context.checks,
|
|
59
|
-
confidence: 0.9,
|
|
60
|
-
validation_type: 'self',
|
|
61
|
-
domain: 'infrastructure',
|
|
62
|
-
tags: ['database'],
|
|
63
|
-
published_at: new Date().toISOString(),
|
|
64
|
-
expires_at: null
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
return { context, record };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe('Storage adapters', () => {
|
|
71
|
-
it('InMemoryStorage supports CRUD/search/profile', async () => {
|
|
72
|
-
const storage = new InMemoryStorage();
|
|
73
|
-
const { context, record } = fixtures();
|
|
74
|
-
|
|
75
|
-
await storage.saveDecision(context);
|
|
76
|
-
await storage.saveRecord(record);
|
|
77
|
-
|
|
78
|
-
expect(await storage.getRecord('rec-1')).not.toBeNull();
|
|
79
|
-
expect(await storage.searchRecords({ domain: 'infrastructure' })).toHaveLength(1);
|
|
80
|
-
expect(await storage.searchRecords({ parentPrompt: 'auth' })).toHaveLength(1);
|
|
81
|
-
expect((await storage.getCompetencyProfile('agent-a')).strengths).toContain('infrastructure');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('SQLiteStorage supports CRUD/search/profile', async () => {
|
|
85
|
-
const dbPath = join(tmpdir(), `elen-sdk-${Date.now()}.db`);
|
|
86
|
-
const storage = new SQLiteStorage(dbPath);
|
|
87
|
-
const { context, record } = fixtures();
|
|
88
|
-
|
|
89
|
-
await storage.saveDecision(context);
|
|
90
|
-
await storage.saveRecord(record);
|
|
91
|
-
|
|
92
|
-
expect(await storage.getRecord('rec-1')).not.toBeNull();
|
|
93
|
-
expect(await storage.searchRecords({ domain: 'infrastructure' })).toHaveLength(1);
|
|
94
|
-
expect(await storage.searchRecords({ minConfidence: 0.8 })).toHaveLength(1);
|
|
95
|
-
expect(await storage.searchRecords({ parentPrompt: 'auth system' })).toHaveLength(1);
|
|
96
|
-
expect((await storage.getCompetencyProfile('agent-a')).domains).toContain('infrastructure');
|
|
97
|
-
|
|
98
|
-
rmSync(dbPath, { force: true });
|
|
99
|
-
});
|
|
100
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "CommonJS",
|
|
5
|
-
"moduleResolution": "Node",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"outDir": "dist",
|
|
8
|
-
"rootDir": "src",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"forceConsistentCasingInFileNames": true,
|
|
13
|
-
"baseUrl": "."
|
|
14
|
-
},
|
|
15
|
-
"include": ["src"]
|
|
16
|
-
}
|