@kodrunhq/opencode-autopilot 1.16.0 → 1.18.0
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/assets/commands/oc-doctor.md +17 -0
- package/bin/configure-tui.ts +1 -1
- package/bin/inspect.ts +2 -2
- package/package.json +1 -1
- package/src/config/index.ts +29 -0
- package/src/config/migrations.ts +196 -0
- package/src/config/v7.ts +45 -0
- package/src/config.ts +108 -24
- package/src/health/checks.ts +165 -0
- package/src/health/runner.ts +8 -2
- package/src/health/types.ts +1 -1
- package/src/index.ts +25 -2
- package/src/kernel/transaction.ts +48 -0
- package/src/kernel/types.ts +1 -2
- package/src/logging/domains.ts +39 -0
- package/src/logging/forensic-writer.ts +177 -0
- package/src/logging/index.ts +4 -0
- package/src/logging/logger.ts +44 -0
- package/src/logging/performance.ts +59 -0
- package/src/logging/rotation.ts +261 -0
- package/src/logging/types.ts +33 -0
- package/src/memory/capture-utils.ts +149 -0
- package/src/memory/capture.ts +16 -197
- package/src/memory/decay.ts +11 -2
- package/src/memory/injector.ts +4 -1
- package/src/memory/lessons.ts +85 -0
- package/src/memory/observations.ts +177 -0
- package/src/memory/preferences.ts +718 -0
- package/src/memory/projects.ts +83 -0
- package/src/memory/repository.ts +46 -1001
- package/src/memory/retrieval.ts +5 -1
- package/src/observability/context-display.ts +8 -0
- package/src/observability/event-handlers.ts +44 -6
- package/src/observability/forensic-log.ts +10 -2
- package/src/observability/forensic-schemas.ts +9 -1
- package/src/observability/log-reader.ts +20 -1
- package/src/orchestrator/error-context.ts +24 -0
- package/src/orchestrator/handlers/build-utils.ts +118 -0
- package/src/orchestrator/handlers/build.ts +13 -148
- package/src/orchestrator/handlers/retrospective.ts +0 -1
- package/src/orchestrator/lesson-memory.ts +7 -2
- package/src/orchestrator/orchestration-logger.ts +46 -31
- package/src/orchestrator/progress.ts +63 -0
- package/src/review/memory.ts +11 -3
- package/src/review/parse-findings.ts +116 -0
- package/src/review/pipeline.ts +3 -107
- package/src/review/selection.ts +38 -4
- package/src/scoring/time-provider.ts +23 -0
- package/src/tools/configure.ts +1 -1
- package/src/tools/doctor.ts +2 -2
- package/src/tools/logs.ts +32 -6
- package/src/tools/orchestrate.ts +11 -9
- package/src/tools/replay.ts +42 -0
- package/src/tools/review.ts +8 -2
- package/src/tools/summary.ts +43 -0
- package/src/types/background.ts +51 -0
- package/src/types/mcp.ts +27 -0
- package/src/types/recovery.ts +39 -0
- package/src/types/routing.ts +39 -0
- package/src/utils/random.ts +33 -0
- package/src/ux/session-summary.ts +56 -0
package/src/memory/repository.ts
CHANGED
|
@@ -1,17 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "./
|
|
14
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Memory repository - unified exports for observations, preferences, projects, and lessons.
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports all public functions from focused sub-modules:
|
|
5
|
+
* - observations.ts: Observation CRUD and search
|
|
6
|
+
* - preferences.ts: Preference records with evidence tracking
|
|
7
|
+
* - projects.ts: Project metadata and path resolution
|
|
8
|
+
* - lessons.ts: Extracted lessons retrieval
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export { listRelevantLessons } from "./lessons";
|
|
14
|
+
// Re-export all public functions from sub-modules
|
|
15
|
+
export {
|
|
16
|
+
deleteObservation,
|
|
17
|
+
getObservationsByProject,
|
|
18
|
+
getRecentFailureObservations,
|
|
19
|
+
insertObservation,
|
|
20
|
+
searchObservations,
|
|
21
|
+
updateAccessCount,
|
|
22
|
+
} from "./observations";
|
|
23
|
+
export {
|
|
24
|
+
deletePreferenceRecord,
|
|
25
|
+
deletePreferencesByKey,
|
|
26
|
+
getAllPreferences,
|
|
27
|
+
getConfirmedPreferencesForProject,
|
|
28
|
+
getPreferenceRecordById,
|
|
29
|
+
type ListPreferenceRecordOptions,
|
|
30
|
+
listPreferenceEvidence,
|
|
31
|
+
listPreferenceRecords,
|
|
32
|
+
type PreferenceEvidencePruneOptions,
|
|
33
|
+
type PreferenceMutationResult,
|
|
34
|
+
type PreferencePruneOptions,
|
|
35
|
+
type PreferencePruneStatus,
|
|
36
|
+
type PreferenceUpsertInput,
|
|
37
|
+
prunePreferenceEvidence,
|
|
38
|
+
prunePreferences,
|
|
39
|
+
type UpsertPreferenceRecordInput,
|
|
40
|
+
upsertPreference,
|
|
41
|
+
upsertPreferenceRecord,
|
|
42
|
+
} from "./preferences";
|
|
43
|
+
export { getProjectByPath, upsertProject } from "./projects";
|
|
44
|
+
|
|
45
|
+
// Re-export types for convenience
|
|
46
|
+
export type {
|
|
15
47
|
Observation,
|
|
16
48
|
ObservationType,
|
|
17
49
|
Preference,
|
|
@@ -19,990 +51,3 @@ import type {
|
|
|
19
51
|
PreferenceRecord,
|
|
20
52
|
Project,
|
|
21
53
|
} from "./types";
|
|
22
|
-
|
|
23
|
-
interface PreferenceRecordRow {
|
|
24
|
-
readonly id: string;
|
|
25
|
-
readonly key: string;
|
|
26
|
-
readonly value: string;
|
|
27
|
-
readonly scope: string;
|
|
28
|
-
readonly project_id: string | null;
|
|
29
|
-
readonly status: string;
|
|
30
|
-
readonly confidence: number;
|
|
31
|
-
readonly source_session: string | null;
|
|
32
|
-
readonly created_at: string;
|
|
33
|
-
readonly last_updated: string;
|
|
34
|
-
readonly evidence_count?: number;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface PreferenceEvidenceRow {
|
|
38
|
-
readonly id: string;
|
|
39
|
-
readonly preference_id: string;
|
|
40
|
-
readonly session_id: string | null;
|
|
41
|
-
readonly run_id: string | null;
|
|
42
|
-
readonly statement: string;
|
|
43
|
-
readonly statement_hash: string;
|
|
44
|
-
readonly confidence: number;
|
|
45
|
-
readonly confirmed: number;
|
|
46
|
-
readonly created_at: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
interface ProjectLessonRow {
|
|
50
|
-
readonly content: string;
|
|
51
|
-
readonly domain: Lesson["domain"];
|
|
52
|
-
readonly extracted_at: string;
|
|
53
|
-
readonly source_phase: Lesson["sourcePhase"];
|
|
54
|
-
readonly last_updated_at: string | null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Resolve optional db parameter to singleton fallback. */
|
|
58
|
-
function resolveDb(db?: Database): Database {
|
|
59
|
-
return db ?? getMemoryDb();
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function withWriteTransaction<T>(db: Database, callback: () => T): T {
|
|
63
|
-
const row = db.query("PRAGMA transaction_state").get() as { transaction_state?: string } | null;
|
|
64
|
-
if (row?.transaction_state === "TRANSACTION") {
|
|
65
|
-
return callback();
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
db.run("BEGIN IMMEDIATE");
|
|
69
|
-
try {
|
|
70
|
-
const result = callback();
|
|
71
|
-
db.run("COMMIT");
|
|
72
|
-
return result;
|
|
73
|
-
} catch (error: unknown) {
|
|
74
|
-
try {
|
|
75
|
-
db.run("ROLLBACK");
|
|
76
|
-
} catch {
|
|
77
|
-
// Ignore rollback failures so the original error wins.
|
|
78
|
-
}
|
|
79
|
-
throw error;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildPlaceholders(count: number): string {
|
|
84
|
-
return Array.from({ length: count }, () => "?").join(", ");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function normalizePreferenceProjectId(
|
|
88
|
-
scope: PreferenceRecord["scope"],
|
|
89
|
-
projectId: string | null,
|
|
90
|
-
): string | null {
|
|
91
|
-
return scope === "project" ? projectId : null;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function makePreferenceId(
|
|
95
|
-
key: string,
|
|
96
|
-
scope: PreferenceRecord["scope"],
|
|
97
|
-
projectId: string | null,
|
|
98
|
-
): string {
|
|
99
|
-
const normalizedProjectId = normalizePreferenceProjectId(scope, projectId) ?? "global";
|
|
100
|
-
return `pref-${createHash("sha1").update(`${scope}:${normalizedProjectId}:${key}`).digest("hex")}`;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function makeEvidenceId(preferenceId: string, statementHash: string): string {
|
|
104
|
-
return `evidence-${createHash("sha1").update(`${preferenceId}:${statementHash}`).digest("hex")}`;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function makeStatementHash(statement: string): string {
|
|
108
|
-
return createHash("sha1").update(statement).digest("hex");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function tableExists(db: Database, tableName: string): boolean {
|
|
112
|
-
const row = db
|
|
113
|
-
.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
114
|
-
.get(tableName) as { name?: string } | null;
|
|
115
|
-
return row?.name === tableName;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Validate observation type at runtime. */
|
|
119
|
-
function parseObservationType(value: unknown): ObservationType {
|
|
120
|
-
if (typeof value === "string" && (OBSERVATION_TYPES as readonly string[]).includes(value)) {
|
|
121
|
-
return value as ObservationType;
|
|
122
|
-
}
|
|
123
|
-
return "context";
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Map a snake_case DB row to camelCase Observation. */
|
|
127
|
-
function rowToObservation(row: Record<string, unknown>): Observation {
|
|
128
|
-
return {
|
|
129
|
-
id: row.id as number,
|
|
130
|
-
projectId: (row.project_id as string) ?? null,
|
|
131
|
-
sessionId: row.session_id as string,
|
|
132
|
-
type: parseObservationType(row.type),
|
|
133
|
-
content: row.content as string,
|
|
134
|
-
summary: row.summary as string,
|
|
135
|
-
confidence: row.confidence as number,
|
|
136
|
-
accessCount: row.access_count as number,
|
|
137
|
-
createdAt: row.created_at as string,
|
|
138
|
-
lastAccessed: row.last_accessed as string,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/** Map a snake_case DB row to camelCase Project. */
|
|
143
|
-
function rowToProject(row: Record<string, unknown>): Project {
|
|
144
|
-
return {
|
|
145
|
-
id: row.id as string,
|
|
146
|
-
path: row.path as string,
|
|
147
|
-
name: row.name as string,
|
|
148
|
-
firstSeenAt: ((row.first_seen_at as string) ?? (row.last_updated as string)) as string,
|
|
149
|
-
lastUpdated: row.last_updated as string,
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function rowToPreferenceRecord(row: PreferenceRecordRow): PreferenceRecord {
|
|
154
|
-
return preferenceRecordSchema.parse({
|
|
155
|
-
id: row.id,
|
|
156
|
-
key: row.key,
|
|
157
|
-
value: row.value,
|
|
158
|
-
scope: row.scope,
|
|
159
|
-
projectId: row.project_id,
|
|
160
|
-
status: row.status,
|
|
161
|
-
confidence: row.confidence,
|
|
162
|
-
sourceSession: row.source_session,
|
|
163
|
-
createdAt: row.created_at,
|
|
164
|
-
lastUpdated: row.last_updated,
|
|
165
|
-
evidenceCount: row.evidence_count ?? 0,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function rowToPreferenceEvidence(row: PreferenceEvidenceRow): PreferenceEvidence {
|
|
170
|
-
return preferenceEvidenceSchema.parse({
|
|
171
|
-
id: row.id,
|
|
172
|
-
preferenceId: row.preference_id,
|
|
173
|
-
sessionId: row.session_id,
|
|
174
|
-
runId: row.run_id,
|
|
175
|
-
statement: row.statement,
|
|
176
|
-
statementHash: row.statement_hash,
|
|
177
|
-
confidence: row.confidence,
|
|
178
|
-
confirmed: row.confirmed === 1,
|
|
179
|
-
createdAt: row.created_at,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/** Map a normalized preference record to the compatibility shape. */
|
|
184
|
-
function recordToPreference(record: PreferenceRecord): Preference {
|
|
185
|
-
return preferenceSchema.parse({
|
|
186
|
-
id: record.id,
|
|
187
|
-
key: record.key,
|
|
188
|
-
value: record.value,
|
|
189
|
-
confidence: record.confidence,
|
|
190
|
-
scope: record.scope,
|
|
191
|
-
projectId: record.projectId,
|
|
192
|
-
status: record.status,
|
|
193
|
-
evidenceCount: record.evidenceCount,
|
|
194
|
-
sourceSession: record.sourceSession,
|
|
195
|
-
createdAt: record.createdAt,
|
|
196
|
-
lastUpdated: record.lastUpdated,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function syncCompatibilityPreference(record: PreferenceRecord, db: Database): void {
|
|
201
|
-
if (record.scope !== "global" || record.status !== "confirmed") {
|
|
202
|
-
db.run("DELETE FROM preferences WHERE id = ?", [record.id]);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
db.run(
|
|
207
|
-
`INSERT INTO preferences (id, key, value, confidence, source_session, created_at, last_updated)
|
|
208
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
209
|
-
ON CONFLICT(key) DO UPDATE SET
|
|
210
|
-
id = excluded.id,
|
|
211
|
-
key = excluded.key,
|
|
212
|
-
value = excluded.value,
|
|
213
|
-
confidence = excluded.confidence,
|
|
214
|
-
source_session = excluded.source_session,
|
|
215
|
-
created_at = excluded.created_at,
|
|
216
|
-
last_updated = excluded.last_updated`,
|
|
217
|
-
[
|
|
218
|
-
record.id,
|
|
219
|
-
record.key,
|
|
220
|
-
record.value,
|
|
221
|
-
record.confidence,
|
|
222
|
-
record.sourceSession,
|
|
223
|
-
record.createdAt,
|
|
224
|
-
record.lastUpdated,
|
|
225
|
-
],
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function listPreferenceRecordsSql(baseWhere = ""): string {
|
|
230
|
-
return `SELECT
|
|
231
|
-
pr.*,
|
|
232
|
-
COALESCE(evidence_counts.evidence_count, 0) AS evidence_count
|
|
233
|
-
FROM preference_records pr
|
|
234
|
-
LEFT JOIN (
|
|
235
|
-
SELECT preference_id, COUNT(*) AS evidence_count
|
|
236
|
-
FROM preference_evidence
|
|
237
|
-
GROUP BY preference_id
|
|
238
|
-
) AS evidence_counts ON evidence_counts.preference_id = pr.id
|
|
239
|
-
${baseWhere}
|
|
240
|
-
ORDER BY pr.last_updated DESC, pr.key ASC, pr.id ASC`;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function listLegacyLessons(projectId: string, db: Database): readonly Lesson[] {
|
|
244
|
-
if (!tableExists(db, "project_lesson_memory")) {
|
|
245
|
-
return Object.freeze([]);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const row = db
|
|
249
|
-
.query("SELECT state_json FROM project_lesson_memory WHERE project_id = ?")
|
|
250
|
-
.get(projectId) as { state_json?: string } | null;
|
|
251
|
-
if (row?.state_json === undefined) {
|
|
252
|
-
return Object.freeze([]);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const parsed = lessonMemorySchema.parse(JSON.parse(row.state_json));
|
|
257
|
-
return Object.freeze(parsed.lessons);
|
|
258
|
-
} catch {
|
|
259
|
-
return Object.freeze([]);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function buildLessonsFromRows(rows: readonly ProjectLessonRow[]): readonly Lesson[] {
|
|
264
|
-
return Object.freeze(
|
|
265
|
-
rows.map((row) =>
|
|
266
|
-
Object.freeze({
|
|
267
|
-
content: row.content,
|
|
268
|
-
domain: row.domain,
|
|
269
|
-
extractedAt: row.extracted_at,
|
|
270
|
-
sourcePhase: row.source_phase,
|
|
271
|
-
}),
|
|
272
|
-
),
|
|
273
|
-
);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Insert an observation. Validates via Zod before writing.
|
|
278
|
-
* Returns the observation with the generated id.
|
|
279
|
-
*/
|
|
280
|
-
export function insertObservation(obs: Omit<Observation, "id">, db?: Database): Observation {
|
|
281
|
-
const validated = observationSchema.omit({ id: true }).parse(obs);
|
|
282
|
-
const d = resolveDb(db);
|
|
283
|
-
|
|
284
|
-
d.run(
|
|
285
|
-
`INSERT INTO observations (project_id, session_id, type, content, summary, confidence, access_count, created_at, last_accessed)
|
|
286
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
287
|
-
[
|
|
288
|
-
validated.projectId,
|
|
289
|
-
validated.sessionId,
|
|
290
|
-
validated.type,
|
|
291
|
-
validated.content,
|
|
292
|
-
validated.summary,
|
|
293
|
-
validated.confidence,
|
|
294
|
-
validated.accessCount,
|
|
295
|
-
validated.createdAt,
|
|
296
|
-
validated.lastAccessed,
|
|
297
|
-
],
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const row = d.query("SELECT last_insert_rowid() as id").get() as { id: number };
|
|
301
|
-
return { ...validated, id: row.id };
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Search observations using FTS5 MATCH with BM25 ranking.
|
|
306
|
-
* Filters by projectId (null for user-level observations).
|
|
307
|
-
*/
|
|
308
|
-
export function searchObservations(
|
|
309
|
-
query: string,
|
|
310
|
-
projectId: string | null,
|
|
311
|
-
limit = 20,
|
|
312
|
-
db?: Database,
|
|
313
|
-
): Array<Observation & { ftsRank: number }> {
|
|
314
|
-
const d = resolveDb(db);
|
|
315
|
-
|
|
316
|
-
const projectFilter = projectId === null ? "AND o.project_id IS NULL" : "AND o.project_id = ?";
|
|
317
|
-
const safeFtsQuery = `"${query.replace(/"/g, '""')}"`;
|
|
318
|
-
const params: Array<string | number> =
|
|
319
|
-
projectId === null ? [safeFtsQuery, limit] : [safeFtsQuery, projectId, limit];
|
|
320
|
-
|
|
321
|
-
const rows = d
|
|
322
|
-
.query(
|
|
323
|
-
`SELECT o.*, bm25(observations_fts) as fts_rank
|
|
324
|
-
FROM observations_fts f
|
|
325
|
-
JOIN observations o ON o.id = f.rowid
|
|
326
|
-
WHERE observations_fts MATCH ?
|
|
327
|
-
${projectFilter}
|
|
328
|
-
ORDER BY fts_rank
|
|
329
|
-
LIMIT ?`,
|
|
330
|
-
)
|
|
331
|
-
.all(...params) as Array<Record<string, unknown>>;
|
|
332
|
-
|
|
333
|
-
return rows.map((row) => ({
|
|
334
|
-
...rowToObservation(row),
|
|
335
|
-
ftsRank: row.fts_rank as number,
|
|
336
|
-
}));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Create or replace a project record.
|
|
341
|
-
*/
|
|
342
|
-
export function upsertProject(project: Project, db?: Database): void {
|
|
343
|
-
const validated = projectSchema.parse(project);
|
|
344
|
-
const d = resolveDb(db);
|
|
345
|
-
const firstSeenAt = validated.firstSeenAt ?? validated.lastUpdated;
|
|
346
|
-
d.run(
|
|
347
|
-
`INSERT INTO projects (id, path, name, first_seen_at, last_updated)
|
|
348
|
-
VALUES (?, ?, ?, ?, ?)
|
|
349
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
350
|
-
path = excluded.path,
|
|
351
|
-
name = excluded.name,
|
|
352
|
-
first_seen_at = COALESCE(projects.first_seen_at, excluded.first_seen_at),
|
|
353
|
-
last_updated = excluded.last_updated`,
|
|
354
|
-
[validated.id, validated.path, validated.name, firstSeenAt, validated.lastUpdated],
|
|
355
|
-
);
|
|
356
|
-
d.run("UPDATE project_paths SET is_current = 0, last_updated = ? WHERE project_id = ?", [
|
|
357
|
-
validated.lastUpdated,
|
|
358
|
-
validated.id,
|
|
359
|
-
]);
|
|
360
|
-
d.run(
|
|
361
|
-
`INSERT INTO project_paths (project_id, path, first_seen_at, last_updated, is_current)
|
|
362
|
-
VALUES (?, ?, ?, ?, 1)
|
|
363
|
-
ON CONFLICT(project_id, path) DO UPDATE SET
|
|
364
|
-
last_updated = excluded.last_updated,
|
|
365
|
-
is_current = 1`,
|
|
366
|
-
[validated.id, validated.path, firstSeenAt, validated.lastUpdated],
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/**
|
|
371
|
-
* Get a project by its filesystem path. Returns null if not found.
|
|
372
|
-
*/
|
|
373
|
-
export function getProjectByPath(path: string, db?: Database): Project | null {
|
|
374
|
-
const d = resolveDb(db);
|
|
375
|
-
const row = d
|
|
376
|
-
.query(
|
|
377
|
-
`SELECT p.*
|
|
378
|
-
FROM projects p
|
|
379
|
-
WHERE p.path = ?
|
|
380
|
-
UNION ALL
|
|
381
|
-
SELECT p.*
|
|
382
|
-
FROM project_paths pp
|
|
383
|
-
JOIN projects p ON p.id = pp.project_id
|
|
384
|
-
WHERE pp.path = ?
|
|
385
|
-
AND NOT EXISTS (SELECT 1 FROM projects p2 WHERE p2.path = ?)
|
|
386
|
-
ORDER BY last_updated DESC
|
|
387
|
-
LIMIT 1`,
|
|
388
|
-
)
|
|
389
|
-
.get(path, path, path) as Record<string, unknown> | null;
|
|
390
|
-
return row ? rowToProject(row) : null;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Get observations filtered by project_id, ordered by created_at DESC.
|
|
395
|
-
*/
|
|
396
|
-
export function getObservationsByProject(
|
|
397
|
-
projectId: string | null,
|
|
398
|
-
limit = 50,
|
|
399
|
-
db?: Database,
|
|
400
|
-
): readonly Observation[] {
|
|
401
|
-
const d = resolveDb(db);
|
|
402
|
-
|
|
403
|
-
const whereClause = projectId === null ? "WHERE project_id IS NULL" : "WHERE project_id = ?";
|
|
404
|
-
const params: Array<string | number> = projectId === null ? [limit] : [projectId, limit];
|
|
405
|
-
|
|
406
|
-
const rows = d
|
|
407
|
-
.query(`SELECT * FROM observations ${whereClause} ORDER BY created_at DESC LIMIT ?`)
|
|
408
|
-
.all(...params) as Array<Record<string, unknown>>;
|
|
409
|
-
|
|
410
|
-
return rows.map(rowToObservation);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
export interface UpsertPreferenceRecordInput {
|
|
414
|
-
readonly id?: string;
|
|
415
|
-
readonly key: string;
|
|
416
|
-
readonly value: string;
|
|
417
|
-
readonly scope?: PreferenceRecord["scope"];
|
|
418
|
-
readonly projectId?: string | null;
|
|
419
|
-
readonly status?: PreferenceRecord["status"];
|
|
420
|
-
readonly confidence?: number;
|
|
421
|
-
readonly sourceSession?: string | null;
|
|
422
|
-
readonly createdAt: string;
|
|
423
|
-
readonly lastUpdated: string;
|
|
424
|
-
readonly evidence?: readonly {
|
|
425
|
-
readonly sessionId?: string | null;
|
|
426
|
-
readonly runId?: string | null;
|
|
427
|
-
readonly statement: string;
|
|
428
|
-
readonly confidence?: number;
|
|
429
|
-
readonly confirmed?: boolean;
|
|
430
|
-
readonly createdAt?: string;
|
|
431
|
-
}[];
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export interface ListPreferenceRecordOptions {
|
|
435
|
-
readonly scope?: PreferenceRecord["scope"];
|
|
436
|
-
readonly projectId?: string | null;
|
|
437
|
-
readonly status?: PreferenceRecord["status"];
|
|
438
|
-
readonly onlyConfirmed?: boolean;
|
|
439
|
-
readonly limit?: number;
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
export type PreferenceUpsertInput = Omit<
|
|
443
|
-
Preference,
|
|
444
|
-
"scope" | "projectId" | "status" | "evidenceCount"
|
|
445
|
-
> &
|
|
446
|
-
Partial<Pick<Preference, "scope" | "projectId" | "status" | "evidenceCount">>;
|
|
447
|
-
|
|
448
|
-
export type PreferencePruneStatus = PreferenceRecord["status"] | "unconfirmed" | "any";
|
|
449
|
-
|
|
450
|
-
export interface PreferenceMutationResult {
|
|
451
|
-
readonly deletedPreferences: number;
|
|
452
|
-
readonly deletedEvidence: number;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
export interface PreferencePruneOptions {
|
|
456
|
-
readonly olderThanDays: number;
|
|
457
|
-
readonly scope?: PreferenceRecord["scope"];
|
|
458
|
-
readonly projectId?: string | null;
|
|
459
|
-
readonly status?: PreferencePruneStatus;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export interface PreferenceEvidencePruneOptions {
|
|
463
|
-
readonly olderThanDays: number;
|
|
464
|
-
readonly keepLatestPerPreference?: number;
|
|
465
|
-
readonly scope?: PreferenceRecord["scope"];
|
|
466
|
-
readonly projectId?: string | null;
|
|
467
|
-
readonly status?: PreferencePruneStatus;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
function isPreferenceStatusMatch(record: PreferenceRecord, status: PreferencePruneStatus): boolean {
|
|
471
|
-
if (status === "any") {
|
|
472
|
-
return true;
|
|
473
|
-
}
|
|
474
|
-
if (status === "unconfirmed") {
|
|
475
|
-
return record.status !== "confirmed";
|
|
476
|
-
}
|
|
477
|
-
return record.status === status;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Create or replace a structured preference record and its supporting evidence.
|
|
482
|
-
*/
|
|
483
|
-
export function upsertPreferenceRecord(
|
|
484
|
-
input: UpsertPreferenceRecordInput,
|
|
485
|
-
db?: Database,
|
|
486
|
-
): PreferenceRecord {
|
|
487
|
-
const d = resolveDb(db);
|
|
488
|
-
const scope = input.scope ?? "global";
|
|
489
|
-
const normalizedProjectId = normalizePreferenceProjectId(scope, input.projectId ?? null);
|
|
490
|
-
if (scope === "project" && normalizedProjectId === null) {
|
|
491
|
-
throw new Error("project-scoped preferences require a projectId");
|
|
492
|
-
}
|
|
493
|
-
const validated = preferenceRecordSchema.parse({
|
|
494
|
-
id: input.id ?? makePreferenceId(input.key, scope, normalizedProjectId),
|
|
495
|
-
key: input.key,
|
|
496
|
-
value: input.value,
|
|
497
|
-
scope,
|
|
498
|
-
projectId: normalizedProjectId,
|
|
499
|
-
status: input.status ?? "confirmed",
|
|
500
|
-
confidence: input.confidence ?? 0.5,
|
|
501
|
-
sourceSession: input.sourceSession ?? null,
|
|
502
|
-
createdAt: input.createdAt,
|
|
503
|
-
lastUpdated: input.lastUpdated,
|
|
504
|
-
evidenceCount: input.evidence?.length ?? 0,
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
d.run("BEGIN IMMEDIATE");
|
|
508
|
-
try {
|
|
509
|
-
d.run(
|
|
510
|
-
`INSERT INTO preference_records (
|
|
511
|
-
id,
|
|
512
|
-
key,
|
|
513
|
-
value,
|
|
514
|
-
scope,
|
|
515
|
-
project_id,
|
|
516
|
-
status,
|
|
517
|
-
confidence,
|
|
518
|
-
source_session,
|
|
519
|
-
created_at,
|
|
520
|
-
last_updated
|
|
521
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
522
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
523
|
-
key = excluded.key,
|
|
524
|
-
value = excluded.value,
|
|
525
|
-
scope = excluded.scope,
|
|
526
|
-
project_id = excluded.project_id,
|
|
527
|
-
status = excluded.status,
|
|
528
|
-
confidence = excluded.confidence,
|
|
529
|
-
source_session = excluded.source_session,
|
|
530
|
-
created_at = excluded.created_at,
|
|
531
|
-
last_updated = excluded.last_updated`,
|
|
532
|
-
[
|
|
533
|
-
validated.id,
|
|
534
|
-
validated.key,
|
|
535
|
-
validated.value,
|
|
536
|
-
validated.scope,
|
|
537
|
-
validated.projectId,
|
|
538
|
-
validated.status,
|
|
539
|
-
validated.confidence,
|
|
540
|
-
validated.sourceSession,
|
|
541
|
-
validated.createdAt,
|
|
542
|
-
validated.lastUpdated,
|
|
543
|
-
],
|
|
544
|
-
);
|
|
545
|
-
|
|
546
|
-
for (const evidence of input.evidence ?? []) {
|
|
547
|
-
const statementHash = makeStatementHash(evidence.statement);
|
|
548
|
-
const validatedEvidence = preferenceEvidenceSchema.parse({
|
|
549
|
-
id: makeEvidenceId(validated.id, statementHash),
|
|
550
|
-
preferenceId: validated.id,
|
|
551
|
-
sessionId: evidence.sessionId ?? validated.sourceSession,
|
|
552
|
-
runId: evidence.runId ?? null,
|
|
553
|
-
statement: evidence.statement,
|
|
554
|
-
statementHash,
|
|
555
|
-
confidence: evidence.confidence ?? validated.confidence,
|
|
556
|
-
confirmed: evidence.confirmed ?? validated.status === "confirmed",
|
|
557
|
-
createdAt: evidence.createdAt ?? validated.lastUpdated,
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
d.run(
|
|
561
|
-
`INSERT INTO preference_evidence (
|
|
562
|
-
id,
|
|
563
|
-
preference_id,
|
|
564
|
-
session_id,
|
|
565
|
-
run_id,
|
|
566
|
-
statement,
|
|
567
|
-
statement_hash,
|
|
568
|
-
confidence,
|
|
569
|
-
confirmed,
|
|
570
|
-
created_at
|
|
571
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
572
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
573
|
-
session_id = excluded.session_id,
|
|
574
|
-
run_id = excluded.run_id,
|
|
575
|
-
statement = excluded.statement,
|
|
576
|
-
confidence = excluded.confidence,
|
|
577
|
-
confirmed = excluded.confirmed,
|
|
578
|
-
created_at = excluded.created_at`,
|
|
579
|
-
[
|
|
580
|
-
validatedEvidence.id,
|
|
581
|
-
validatedEvidence.preferenceId,
|
|
582
|
-
validatedEvidence.sessionId,
|
|
583
|
-
validatedEvidence.runId,
|
|
584
|
-
validatedEvidence.statement,
|
|
585
|
-
validatedEvidence.statementHash,
|
|
586
|
-
validatedEvidence.confidence,
|
|
587
|
-
validatedEvidence.confirmed ? 1 : 0,
|
|
588
|
-
validatedEvidence.createdAt,
|
|
589
|
-
],
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
syncCompatibilityPreference(validated, d);
|
|
594
|
-
d.run("COMMIT");
|
|
595
|
-
} catch (error: unknown) {
|
|
596
|
-
try {
|
|
597
|
-
d.run("ROLLBACK");
|
|
598
|
-
} catch {
|
|
599
|
-
// Ignore rollback failures so the original error wins.
|
|
600
|
-
}
|
|
601
|
-
throw error;
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
return getPreferenceRecordById(validated.id, d) ?? validated;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
export function getPreferenceRecordById(id: string, db?: Database): PreferenceRecord | null {
|
|
608
|
-
const d = resolveDb(db);
|
|
609
|
-
if (!tableExists(d, "preference_records")) {
|
|
610
|
-
return null;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const row = d
|
|
614
|
-
.query(`${listPreferenceRecordsSql("WHERE pr.id = ?")} LIMIT 1`)
|
|
615
|
-
.get(id) as PreferenceRecordRow | null;
|
|
616
|
-
return row ? rowToPreferenceRecord(row) : null;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
export function listPreferenceRecords(
|
|
620
|
-
options: ListPreferenceRecordOptions = {},
|
|
621
|
-
db?: Database,
|
|
622
|
-
): readonly PreferenceRecord[] {
|
|
623
|
-
const d = resolveDb(db);
|
|
624
|
-
if (!tableExists(d, "preference_records")) {
|
|
625
|
-
return getAllPreferences(d).map((preference) =>
|
|
626
|
-
preferenceRecordSchema.parse({
|
|
627
|
-
...preference,
|
|
628
|
-
scope: preference.scope,
|
|
629
|
-
projectId: preference.projectId,
|
|
630
|
-
status: preference.status,
|
|
631
|
-
evidenceCount: preference.evidenceCount,
|
|
632
|
-
}),
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const conditions: string[] = [];
|
|
637
|
-
const params: Array<string | number> = [];
|
|
638
|
-
|
|
639
|
-
if (options.scope) {
|
|
640
|
-
conditions.push("pr.scope = ?");
|
|
641
|
-
params.push(options.scope);
|
|
642
|
-
}
|
|
643
|
-
if (Object.hasOwn(options, "projectId")) {
|
|
644
|
-
if (options.projectId === null) {
|
|
645
|
-
conditions.push("pr.project_id IS NULL");
|
|
646
|
-
} else if (typeof options.projectId === "string") {
|
|
647
|
-
conditions.push("pr.project_id = ?");
|
|
648
|
-
params.push(options.projectId);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
if (options.onlyConfirmed === true) {
|
|
652
|
-
conditions.push("pr.status = 'confirmed'");
|
|
653
|
-
} else if (options.status) {
|
|
654
|
-
conditions.push("pr.status = ?");
|
|
655
|
-
params.push(options.status);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
let sql = listPreferenceRecordsSql(
|
|
659
|
-
conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "",
|
|
660
|
-
);
|
|
661
|
-
if (typeof options.limit === "number") {
|
|
662
|
-
sql = `${sql} LIMIT ?`;
|
|
663
|
-
params.push(Math.max(1, options.limit));
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const rows = d.query(sql).all(...params) as PreferenceRecordRow[];
|
|
667
|
-
return Object.freeze(rows.map(rowToPreferenceRecord));
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
export function listPreferenceEvidence(
|
|
671
|
-
preferenceId: string,
|
|
672
|
-
db?: Database,
|
|
673
|
-
): readonly PreferenceEvidence[] {
|
|
674
|
-
const d = resolveDb(db);
|
|
675
|
-
if (!tableExists(d, "preference_evidence")) {
|
|
676
|
-
return Object.freeze([]);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
const rows = d
|
|
680
|
-
.query(
|
|
681
|
-
`SELECT *
|
|
682
|
-
FROM preference_evidence
|
|
683
|
-
WHERE preference_id = ?
|
|
684
|
-
ORDER BY created_at DESC, id DESC`,
|
|
685
|
-
)
|
|
686
|
-
.all(preferenceId) as PreferenceEvidenceRow[];
|
|
687
|
-
return Object.freeze(rows.map(rowToPreferenceEvidence));
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function selectPrunablePreferenceRecords(
|
|
691
|
-
options: PreferencePruneOptions,
|
|
692
|
-
db: Database,
|
|
693
|
-
): readonly PreferenceRecord[] {
|
|
694
|
-
const cutoff = new Date(
|
|
695
|
-
Date.now() - Math.max(1, options.olderThanDays) * 24 * 60 * 60 * 1000,
|
|
696
|
-
).toISOString();
|
|
697
|
-
const status = options.status ?? "unconfirmed";
|
|
698
|
-
const records = listPreferenceRecords(
|
|
699
|
-
{
|
|
700
|
-
scope: options.scope,
|
|
701
|
-
projectId: options.projectId,
|
|
702
|
-
status:
|
|
703
|
-
status === "candidate" || status === "confirmed" || status === "rejected"
|
|
704
|
-
? status
|
|
705
|
-
: undefined,
|
|
706
|
-
},
|
|
707
|
-
db,
|
|
708
|
-
);
|
|
709
|
-
return Object.freeze(
|
|
710
|
-
records.filter(
|
|
711
|
-
(record) => isPreferenceStatusMatch(record, status) && record.lastUpdated < cutoff,
|
|
712
|
-
),
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
function deletePreferenceRecordsByIds(
|
|
717
|
-
ids: readonly string[],
|
|
718
|
-
db: Database,
|
|
719
|
-
): PreferenceMutationResult {
|
|
720
|
-
if (ids.length === 0) {
|
|
721
|
-
return Object.freeze({ deletedPreferences: 0, deletedEvidence: 0 });
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
if (!tableExists(db, "preference_records")) {
|
|
725
|
-
const placeholders = buildPlaceholders(ids.length);
|
|
726
|
-
const deletedPreferences =
|
|
727
|
-
(
|
|
728
|
-
db
|
|
729
|
-
.query(`SELECT COUNT(*) AS cnt FROM preferences WHERE id IN (${placeholders})`)
|
|
730
|
-
.get(...ids) as { cnt?: number } | null
|
|
731
|
-
)?.cnt ?? 0;
|
|
732
|
-
if (deletedPreferences > 0) {
|
|
733
|
-
db.run(`DELETE FROM preferences WHERE id IN (${placeholders})`, [...ids]);
|
|
734
|
-
}
|
|
735
|
-
return Object.freeze({ deletedPreferences, deletedEvidence: 0 });
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
const placeholders = buildPlaceholders(ids.length);
|
|
739
|
-
const records = db
|
|
740
|
-
.query(`SELECT * FROM preference_records WHERE id IN (${placeholders})`)
|
|
741
|
-
.all(...ids) as PreferenceRecordRow[];
|
|
742
|
-
if (records.length === 0) {
|
|
743
|
-
return Object.freeze({ deletedPreferences: 0, deletedEvidence: 0 });
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
const deletedEvidence = tableExists(db, "preference_evidence")
|
|
747
|
-
? ((
|
|
748
|
-
db
|
|
749
|
-
.query(
|
|
750
|
-
`SELECT COUNT(*) AS cnt FROM preference_evidence WHERE preference_id IN (${placeholders})`,
|
|
751
|
-
)
|
|
752
|
-
.get(...ids) as { cnt?: number } | null
|
|
753
|
-
)?.cnt ?? 0)
|
|
754
|
-
: 0;
|
|
755
|
-
|
|
756
|
-
withWriteTransaction(db, () => {
|
|
757
|
-
for (const record of records) {
|
|
758
|
-
if (record.scope === "global") {
|
|
759
|
-
db.run("DELETE FROM preferences WHERE id = ? OR key = ?", [record.id, record.key]);
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
db.run(`DELETE FROM preference_records WHERE id IN (${placeholders})`, [...ids]);
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
return Object.freeze({
|
|
766
|
-
deletedPreferences: records.length,
|
|
767
|
-
deletedEvidence,
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
export function deletePreferenceRecord(id: string, db?: Database): PreferenceMutationResult {
|
|
772
|
-
return deletePreferenceRecordsByIds([id], resolveDb(db));
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
export function deletePreferencesByKey(
|
|
776
|
-
key: string,
|
|
777
|
-
options: { readonly scope?: PreferenceRecord["scope"]; readonly projectId?: string | null } = {},
|
|
778
|
-
db?: Database,
|
|
779
|
-
): PreferenceMutationResult {
|
|
780
|
-
const d = resolveDb(db);
|
|
781
|
-
const records = listPreferenceRecords(
|
|
782
|
-
{
|
|
783
|
-
scope: options.scope,
|
|
784
|
-
projectId: options.projectId,
|
|
785
|
-
},
|
|
786
|
-
d,
|
|
787
|
-
).filter((record) => record.key === key);
|
|
788
|
-
return deletePreferenceRecordsByIds(
|
|
789
|
-
records.map((record) => record.id),
|
|
790
|
-
d,
|
|
791
|
-
);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
export function prunePreferences(
|
|
795
|
-
options: PreferencePruneOptions,
|
|
796
|
-
db?: Database,
|
|
797
|
-
): PreferenceMutationResult {
|
|
798
|
-
const d = resolveDb(db);
|
|
799
|
-
const records = selectPrunablePreferenceRecords(options, d);
|
|
800
|
-
return deletePreferenceRecordsByIds(
|
|
801
|
-
records.map((record) => record.id),
|
|
802
|
-
d,
|
|
803
|
-
);
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
export function prunePreferenceEvidence(
|
|
807
|
-
options: PreferenceEvidencePruneOptions,
|
|
808
|
-
db?: Database,
|
|
809
|
-
): PreferenceMutationResult {
|
|
810
|
-
const d = resolveDb(db);
|
|
811
|
-
if (!tableExists(d, "preference_evidence") || !tableExists(d, "preference_records")) {
|
|
812
|
-
return Object.freeze({ deletedPreferences: 0, deletedEvidence: 0 });
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
const cutoff = new Date(
|
|
816
|
-
Date.now() - Math.max(1, options.olderThanDays) * 24 * 60 * 60 * 1000,
|
|
817
|
-
).toISOString();
|
|
818
|
-
const keepLatestPerPreference = Math.max(0, options.keepLatestPerPreference ?? 1);
|
|
819
|
-
const status = options.status ?? "any";
|
|
820
|
-
const records = listPreferenceRecords(
|
|
821
|
-
{
|
|
822
|
-
scope: options.scope,
|
|
823
|
-
projectId: options.projectId,
|
|
824
|
-
status:
|
|
825
|
-
status === "candidate" || status === "confirmed" || status === "rejected"
|
|
826
|
-
? status
|
|
827
|
-
: undefined,
|
|
828
|
-
},
|
|
829
|
-
d,
|
|
830
|
-
).filter((record) => isPreferenceStatusMatch(record, status));
|
|
831
|
-
|
|
832
|
-
let deletedEvidence = 0;
|
|
833
|
-
withWriteTransaction(d, () => {
|
|
834
|
-
for (const record of records) {
|
|
835
|
-
const evidence = listPreferenceEvidence(record.id, d);
|
|
836
|
-
const removable = evidence
|
|
837
|
-
.slice(keepLatestPerPreference)
|
|
838
|
-
.filter((entry) => entry.createdAt < cutoff);
|
|
839
|
-
if (removable.length === 0) {
|
|
840
|
-
continue;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
const placeholders = buildPlaceholders(removable.length);
|
|
844
|
-
d.run(
|
|
845
|
-
`DELETE FROM preference_evidence WHERE id IN (${placeholders})`,
|
|
846
|
-
removable.map((entry) => entry.id),
|
|
847
|
-
);
|
|
848
|
-
deletedEvidence += removable.length;
|
|
849
|
-
}
|
|
850
|
-
});
|
|
851
|
-
|
|
852
|
-
return Object.freeze({ deletedPreferences: 0, deletedEvidence });
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
export function listRelevantLessons(
|
|
856
|
-
projectId: string,
|
|
857
|
-
limit = 5,
|
|
858
|
-
db?: Database,
|
|
859
|
-
): readonly Lesson[] {
|
|
860
|
-
const d = resolveDb(db);
|
|
861
|
-
if (tableExists(d, "project_lessons")) {
|
|
862
|
-
const rows = d
|
|
863
|
-
.query(
|
|
864
|
-
`SELECT content, domain, extracted_at, source_phase, last_updated_at
|
|
865
|
-
FROM project_lessons
|
|
866
|
-
WHERE project_id = ?
|
|
867
|
-
ORDER BY extracted_at DESC, lesson_id DESC
|
|
868
|
-
LIMIT ?`,
|
|
869
|
-
)
|
|
870
|
-
.all(projectId, limit) as ProjectLessonRow[];
|
|
871
|
-
if (rows.length > 0) {
|
|
872
|
-
return buildLessonsFromRows(rows);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
return listLegacyLessons(projectId, d).slice(0, limit);
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
/**
|
|
880
|
-
* Create or replace a preference by its id.
|
|
881
|
-
* Compatibility wrapper that stores a confirmed global preference record.
|
|
882
|
-
*/
|
|
883
|
-
export function upsertPreference(pref: PreferenceUpsertInput, db?: Database): void {
|
|
884
|
-
const validated = preferenceSchema.parse({
|
|
885
|
-
scope: "global",
|
|
886
|
-
projectId: null,
|
|
887
|
-
status: "confirmed",
|
|
888
|
-
evidenceCount: 0,
|
|
889
|
-
...pref,
|
|
890
|
-
});
|
|
891
|
-
upsertPreferenceRecord(
|
|
892
|
-
{
|
|
893
|
-
id: validated.id,
|
|
894
|
-
key: validated.key,
|
|
895
|
-
value: validated.value,
|
|
896
|
-
scope: validated.scope,
|
|
897
|
-
projectId: validated.projectId,
|
|
898
|
-
status: validated.status,
|
|
899
|
-
confidence: validated.confidence,
|
|
900
|
-
sourceSession: validated.sourceSession,
|
|
901
|
-
createdAt: validated.createdAt,
|
|
902
|
-
lastUpdated: validated.lastUpdated,
|
|
903
|
-
evidence:
|
|
904
|
-
validated.sourceSession === null
|
|
905
|
-
? []
|
|
906
|
-
: [
|
|
907
|
-
{
|
|
908
|
-
sessionId: validated.sourceSession,
|
|
909
|
-
statement: `${validated.key}: ${validated.value}`,
|
|
910
|
-
confidence: validated.confidence,
|
|
911
|
-
confirmed: validated.status === "confirmed",
|
|
912
|
-
createdAt: validated.lastUpdated,
|
|
913
|
-
},
|
|
914
|
-
],
|
|
915
|
-
},
|
|
916
|
-
db,
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
/**
|
|
921
|
-
* Get all compatibility preferences.
|
|
922
|
-
*/
|
|
923
|
-
export function getAllPreferences(db?: Database): readonly Preference[] {
|
|
924
|
-
const d = resolveDb(db);
|
|
925
|
-
if (!tableExists(d, "preference_records")) {
|
|
926
|
-
const rows = d
|
|
927
|
-
.query("SELECT * FROM preferences ORDER BY last_updated DESC, key ASC")
|
|
928
|
-
.all() as Array<Record<string, unknown>>;
|
|
929
|
-
return rows.map((row) =>
|
|
930
|
-
preferenceSchema.parse({
|
|
931
|
-
id: row.id as string,
|
|
932
|
-
key: row.key as string,
|
|
933
|
-
value: row.value as string,
|
|
934
|
-
confidence: row.confidence as number,
|
|
935
|
-
scope: "global",
|
|
936
|
-
projectId: null,
|
|
937
|
-
status: "confirmed",
|
|
938
|
-
evidenceCount: 0,
|
|
939
|
-
sourceSession: (row.source_session as string) ?? null,
|
|
940
|
-
createdAt: row.created_at as string,
|
|
941
|
-
lastUpdated: row.last_updated as string,
|
|
942
|
-
}),
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
const projected: Preference[] = [];
|
|
947
|
-
const seen = new Set<string>();
|
|
948
|
-
for (const record of listPreferenceRecords({ onlyConfirmed: true }, d)) {
|
|
949
|
-
const uniquenessKey = `${record.scope}:${record.projectId ?? "global"}:${record.key}`;
|
|
950
|
-
if (seen.has(uniquenessKey)) {
|
|
951
|
-
continue;
|
|
952
|
-
}
|
|
953
|
-
seen.add(uniquenessKey);
|
|
954
|
-
projected.push(recordToPreference(record));
|
|
955
|
-
}
|
|
956
|
-
return Object.freeze(projected);
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
export function getConfirmedPreferencesForProject(
|
|
960
|
-
projectId: string,
|
|
961
|
-
db?: Database,
|
|
962
|
-
): readonly Preference[] {
|
|
963
|
-
const d = resolveDb(db);
|
|
964
|
-
const globalPrefs = listPreferenceRecords({ scope: "global", onlyConfirmed: true, limit: 5 }, d);
|
|
965
|
-
const projectPrefs = listPreferenceRecords(
|
|
966
|
-
{ scope: "project", projectId, onlyConfirmed: true, limit: 5 },
|
|
967
|
-
d,
|
|
968
|
-
);
|
|
969
|
-
return Object.freeze([...projectPrefs, ...globalPrefs].map(recordToPreference));
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
export function getRecentFailureObservations(
|
|
973
|
-
projectId: string,
|
|
974
|
-
limit = 5,
|
|
975
|
-
db?: Database,
|
|
976
|
-
): readonly Observation[] {
|
|
977
|
-
const d = resolveDb(db);
|
|
978
|
-
const rows = d
|
|
979
|
-
.query(
|
|
980
|
-
`SELECT *
|
|
981
|
-
FROM observations
|
|
982
|
-
WHERE project_id = ?
|
|
983
|
-
AND type = 'error'
|
|
984
|
-
ORDER BY created_at DESC, id DESC
|
|
985
|
-
LIMIT ?`,
|
|
986
|
-
)
|
|
987
|
-
.all(projectId, limit) as Array<Record<string, unknown>>;
|
|
988
|
-
return Object.freeze(rows.map(rowToObservation));
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
/**
|
|
992
|
-
* Delete an observation by id.
|
|
993
|
-
*/
|
|
994
|
-
export function deleteObservation(id: number, db?: Database): void {
|
|
995
|
-
const d = resolveDb(db);
|
|
996
|
-
d.run("DELETE FROM observations WHERE id = ?", [id]);
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
/**
|
|
1000
|
-
* Increment access_count and update last_accessed for an observation.
|
|
1001
|
-
*/
|
|
1002
|
-
export function updateAccessCount(id: number, db?: Database): void {
|
|
1003
|
-
const d = resolveDb(db);
|
|
1004
|
-
d.run("UPDATE observations SET access_count = access_count + 1, last_accessed = ? WHERE id = ?", [
|
|
1005
|
-
new Date().toISOString(),
|
|
1006
|
-
id,
|
|
1007
|
-
]);
|
|
1008
|
-
}
|