@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.
- package/dist/client.d.ts +9 -6
- package/dist/client.js +81 -13
- package/dist/index.d.ts +8 -5
- package/dist/index.js +10 -1
- package/dist/storage/interface.d.ts +7 -5
- package/dist/storage/memory.d.ts +8 -5
- package/dist/storage/memory.js +34 -45
- package/dist/storage/sqlite.d.ts +9 -24
- package/dist/storage/sqlite.js +66 -299
- package/dist/types.d.ts +18 -1
- package/package.json +6 -3
- package/src/client.ts +108 -29
- package/src/index.ts +14 -2
- package/src/storage/interface.ts +13 -5
- package/src/storage/memory.ts +40 -62
- package/src/storage/sqlite.ts +62 -364
- package/src/types.ts +15 -0
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ElenClient } from './client';
|
|
2
2
|
import { InMemoryStorage, SQLiteStorage, type StorageAdapter } from './storage';
|
|
3
|
-
import type { ElenConfig, CommitDecisionInput, SearchOptions } from './types';
|
|
3
|
+
import type { ElenConfig, CommitDecisionInput, LogDecisionInput, SearchOptions } from './types';
|
|
4
4
|
|
|
5
5
|
export class Elen {
|
|
6
6
|
private readonly client: ElenClient;
|
|
@@ -12,12 +12,16 @@ export class Elen {
|
|
|
12
12
|
|
|
13
13
|
private createStorage(config: ElenConfig): StorageAdapter {
|
|
14
14
|
if (config.storage === 'sqlite') {
|
|
15
|
-
return new SQLiteStorage(config.sqlitePath ?? 'elen.db', config.projectId);
|
|
15
|
+
return new SQLiteStorage(config.sqlitePath ?? 'elen.db', config.projectId, config.defaultProjectIsolation ?? 'strict');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
return new InMemoryStorage();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
async logDecision(input: LogDecisionInput) {
|
|
22
|
+
return this.client.logDecision(input);
|
|
23
|
+
}
|
|
24
|
+
|
|
21
25
|
async commitDecision(input: CommitDecisionInput) {
|
|
22
26
|
return this.client.commitDecision(input);
|
|
23
27
|
}
|
|
@@ -26,6 +30,14 @@ export class Elen {
|
|
|
26
30
|
return this.client.supersedeDecision(oldDecisionId, input);
|
|
27
31
|
}
|
|
28
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
|
+
|
|
29
41
|
async suggest(opts: SearchOptions) {
|
|
30
42
|
return this.client.suggest(opts);
|
|
31
43
|
}
|
package/src/storage/interface.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CompetencyProfile,
|
|
3
|
+
ConstraintSet,
|
|
4
|
+
DecisionContext,
|
|
5
|
+
DecisionRecord,
|
|
6
|
+
MinimalDecisionRecord
|
|
7
|
+
} from '@learningnodes/elen-core';
|
|
2
8
|
import type { SearchOptions } from '../types';
|
|
3
9
|
|
|
4
10
|
export interface StorageAdapter {
|
|
11
|
+
saveDecision?(decision: DecisionContext): Promise<void>;
|
|
5
12
|
saveConstraintSet(constraintSet: ConstraintSet): Promise<void>;
|
|
6
13
|
getConstraintSet(id: string): Promise<ConstraintSet | null>;
|
|
7
|
-
saveRecord(record: DecisionRecord): Promise<void>;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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>>;
|
|
11
19
|
getCompetencyProfile(agentId: string): Promise<CompetencyProfile>;
|
|
12
20
|
}
|
package/src/storage/memory.ts
CHANGED
|
@@ -1,85 +1,63 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
CompetencyProfile,
|
|
3
|
+
ConstraintSet,
|
|
4
|
+
DecisionContext,
|
|
5
|
+
DecisionRecord,
|
|
6
|
+
MinimalDecisionRecord
|
|
7
|
+
} from '@learningnodes/elen-core';
|
|
2
8
|
import type { SearchOptions } from '../types';
|
|
3
9
|
import type { StorageAdapter } from './interface';
|
|
4
10
|
|
|
5
11
|
export class InMemoryStorage implements StorageAdapter {
|
|
6
12
|
private readonly constraintSets = new Map<string, ConstraintSet>();
|
|
7
|
-
private readonly records = new Map<string, DecisionRecord>();
|
|
13
|
+
private readonly records = new Map<string, MinimalDecisionRecord | DecisionRecord>();
|
|
14
|
+
private readonly decisions = new Map<string, DecisionContext>();
|
|
8
15
|
|
|
9
|
-
async
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async getConstraintSet(id: string): Promise<ConstraintSet | null> {
|
|
16
|
-
return this.constraintSets.get(id) ?? null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async saveRecord(record: DecisionRecord): Promise<void> {
|
|
20
|
-
this.records.set(record.decision_id, record);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async getRecord(recordId: string): Promise<DecisionRecord | null> {
|
|
24
|
-
return this.records.get(recordId) ?? null;
|
|
25
|
-
}
|
|
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; }
|
|
26
22
|
|
|
27
|
-
async searchRecords(opts: SearchOptions): Promise<DecisionRecord
|
|
23
|
+
async searchRecords(opts: SearchOptions): Promise<Array<MinimalDecisionRecord | DecisionRecord>> {
|
|
28
24
|
let results = Array.from(this.records.values());
|
|
29
|
-
|
|
30
|
-
if (opts.
|
|
31
|
-
|
|
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
|
+
});
|
|
32
33
|
}
|
|
33
|
-
|
|
34
34
|
if (opts.query) {
|
|
35
35
|
const needle = opts.query.toLowerCase();
|
|
36
|
-
results = results.filter((
|
|
37
|
-
const haystack =
|
|
38
|
-
|
|
39
|
-
record.domain,
|
|
40
|
-
record.q_id
|
|
41
|
-
].join(' ').toLowerCase();
|
|
42
|
-
|
|
43
|
-
return haystack.includes(needle);
|
|
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);
|
|
44
39
|
});
|
|
45
40
|
}
|
|
46
|
-
|
|
47
41
|
const limit = opts.limit ?? results.length;
|
|
48
|
-
|
|
49
|
-
return results.sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, limit);
|
|
42
|
+
return results.slice(0, limit);
|
|
50
43
|
}
|
|
51
44
|
|
|
52
|
-
async getAgentDecisions(agentId: string, domain?: string): Promise<DecisionRecord
|
|
53
|
-
return Array.from(this.records.values()).filter(
|
|
54
|
-
(record) => record.agent_id === agentId && (domain ? record.domain === domain : true)
|
|
55
|
-
);
|
|
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));
|
|
56
47
|
}
|
|
57
48
|
|
|
58
49
|
async getCompetencyProfile(agentId: string): Promise<CompetencyProfile> {
|
|
59
50
|
const records = await this.getAgentDecisions(agentId);
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
65
57
|
}
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for (const [domain, count] of domainCounts.entries()) {
|
|
72
|
-
if (count >= 5) {
|
|
73
|
-
strengths.push(domain);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
agent_id: agentId,
|
|
79
|
-
domains,
|
|
80
|
-
strengths,
|
|
81
|
-
weaknesses,
|
|
82
|
-
updated_at: new Date().toISOString()
|
|
83
|
-
};
|
|
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() };
|
|
84
62
|
}
|
|
85
63
|
}
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -1,397 +1,95 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
|
-
import type { CompetencyProfile, DecisionRecord,
|
|
2
|
+
import type { CompetencyProfile, ConstraintSet, DecisionContext, DecisionRecord, MinimalDecisionRecord } from '@learningnodes/elen-core';
|
|
3
3
|
import type { SearchOptions } from '../types';
|
|
4
4
|
import type { StorageAdapter } from './interface';
|
|
5
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
6
|
export class SQLiteStorage implements StorageAdapter {
|
|
21
7
|
private readonly db: Database.Database;
|
|
22
8
|
private readonly projectId: string;
|
|
9
|
+
private readonly defaultIsolation: 'strict' | 'open';
|
|
23
10
|
|
|
24
|
-
constructor(path: string, projectId: string = 'default') {
|
|
11
|
+
constructor(path: string, projectId: string = 'default', defaultIsolation: 'strict' | 'open' = 'strict') {
|
|
25
12
|
this.db = new Database(path);
|
|
26
13
|
this.projectId = projectId;
|
|
14
|
+
this.defaultIsolation = defaultIsolation;
|
|
27
15
|
this.init();
|
|
28
16
|
}
|
|
29
17
|
|
|
30
|
-
private init()
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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]);
|
|
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
|
+
`);
|
|
176
34
|
}
|
|
177
35
|
|
|
178
|
-
|
|
179
|
-
|
|
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];
|
|
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)]);
|
|
215
38
|
}
|
|
216
|
-
|
|
217
|
-
// --- Core Storage Methods ---
|
|
218
|
-
|
|
219
39
|
async saveConstraintSet(constraintSet: ConstraintSet): Promise<void> {
|
|
220
|
-
|
|
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
|
-
});
|
|
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]);
|
|
231
41
|
}
|
|
232
|
-
|
|
233
42
|
async getConstraintSet(id: string): Promise<ConstraintSet | null> {
|
|
234
|
-
const row = this.db
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
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;
|
|
240
50
|
}
|
|
241
|
-
|
|
242
|
-
return {
|
|
243
|
-
constraint_set_id: row.constraint_set_id,
|
|
244
|
-
atoms: JSON.parse(row.atoms) as string[],
|
|
245
|
-
summary: row.summary
|
|
246
|
-
};
|
|
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)]);
|
|
247
52
|
}
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
});
|
|
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)]);
|
|
278
55
|
}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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;
|
|
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;
|
|
290
59
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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!);
|
|
312
65
|
if (opts.query) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
LOWER(records.domain) LIKE @query OR
|
|
316
|
-
LOWER(records.q_id) LIKE @query
|
|
317
|
-
)`);
|
|
318
|
-
params.query = `%${opts.query.toLowerCase()}%`;
|
|
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); });
|
|
319
68
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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()
|
|
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;
|
|
350
77
|
});
|
|
351
|
-
} catch {
|
|
352
|
-
// Non-critical: don't fail search if logging fails
|
|
353
78
|
}
|
|
354
|
-
|
|
355
|
-
return results;
|
|
79
|
+
return opts.limit ? parsed.slice(0, opts.limit) : parsed;
|
|
356
80
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
|
|
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);
|
|
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;
|
|
367
85
|
}
|
|
368
|
-
|
|
369
86
|
async getCompetencyProfile(agentId: string): Promise<CompetencyProfile> {
|
|
370
87
|
const records = await this.getAgentDecisions(agentId);
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
};
|
|
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() };
|
|
396
94
|
}
|
|
397
95
|
}
|