@axiom-lattice/local-stores 2.0.7 → 2.0.8
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +11 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +46 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/LocalTaskStore.test.ts +84 -0
- package/src/stores/LocalEvalStore.ts +6 -3
- package/src/stores/LocalTaskStore.ts +47 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/local-stores",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Local SQLite-based stores for Axiom Lattice framework",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"@types/sql.js": "^1.4.11",
|
|
26
26
|
"sql.js": "^1.14.1",
|
|
27
27
|
"uuid": "^14.0.1",
|
|
28
|
-
"@axiom-lattice/core": "3.0
|
|
29
|
-
"@axiom-lattice/protocols": "3.0.
|
|
28
|
+
"@axiom-lattice/core": "3.1.0",
|
|
29
|
+
"@axiom-lattice/protocols": "3.0.3"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/jest": "^29.5.14",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LocalTaskStore metadata filter tests
|
|
3
|
+
*
|
|
4
|
+
* The frontend queries training rounds with stringified JSON values
|
|
5
|
+
* (`metadata={"trainingRound":"true"}`), tuned for PostgreSQL's `metadata->>`
|
|
6
|
+
* text comparison. SQLite's json_extract returns INTEGER 1 for JSON true,
|
|
7
|
+
* so a naive text comparison filters the round out — the learning card
|
|
8
|
+
* shows no data.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, it, beforeEach, afterEach } from "@jest/globals";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { initDatabase, closeDatabase } from "../database";
|
|
15
|
+
import { LocalTaskStore } from "../stores/LocalTaskStore";
|
|
16
|
+
|
|
17
|
+
// sql.js persists to disk via DatabaseWrapper.save(), so each test needs a
|
|
18
|
+
// unique file path (":memory:" is treated as a plain relative path).
|
|
19
|
+
const nextDbPath = () =>
|
|
20
|
+
path.join(tmpdir(), `lt-task-test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
|
21
|
+
|
|
22
|
+
describe("LocalTaskStore metadata filter", () => {
|
|
23
|
+
let store: LocalTaskStore;
|
|
24
|
+
|
|
25
|
+
beforeEach(async () => {
|
|
26
|
+
const db = await initDatabase({ dbPath: nextDbPath() });
|
|
27
|
+
store = new LocalTaskStore(db);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
closeDatabase();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("matches a boolean metadata value queried as the string 'true'", async () => {
|
|
35
|
+
await store.create({
|
|
36
|
+
tenantId: "t1",
|
|
37
|
+
ownerType: "agent",
|
|
38
|
+
ownerId: "agent-architect",
|
|
39
|
+
title: "round",
|
|
40
|
+
status: "in_progress",
|
|
41
|
+
metadata: { trainingRound: true, targetAgentId: "agent-x" },
|
|
42
|
+
} as never);
|
|
43
|
+
|
|
44
|
+
const byBoolean = await store.list({
|
|
45
|
+
tenantId: "t1",
|
|
46
|
+
ownerId: "agent-architect",
|
|
47
|
+
metadata: { trainingRound: "true", targetAgentId: "agent-x" },
|
|
48
|
+
} as never);
|
|
49
|
+
expect(byBoolean).toHaveLength(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("still matches string metadata values", async () => {
|
|
53
|
+
await store.create({
|
|
54
|
+
tenantId: "t1",
|
|
55
|
+
ownerType: "agent",
|
|
56
|
+
ownerId: "agent-architect",
|
|
57
|
+
title: "round",
|
|
58
|
+
status: "pending",
|
|
59
|
+
metadata: { trainingRound: true, targetAgentId: "agent-x" },
|
|
60
|
+
} as never);
|
|
61
|
+
|
|
62
|
+
const byString = await store.list({
|
|
63
|
+
tenantId: "t1",
|
|
64
|
+
metadata: { targetAgentId: "agent-x" },
|
|
65
|
+
} as never);
|
|
66
|
+
expect(byString).toHaveLength(1);
|
|
67
|
+
const miss = await store.list({ tenantId: "t1", metadata: { targetAgentId: "agent-y" } } as never);
|
|
68
|
+
expect(miss).toHaveLength(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("matches boolean false queried as the string 'false'", async () => {
|
|
72
|
+
await store.create({
|
|
73
|
+
tenantId: "t1",
|
|
74
|
+
ownerType: "agent",
|
|
75
|
+
ownerId: "agent-architect",
|
|
76
|
+
title: "t",
|
|
77
|
+
status: "pending",
|
|
78
|
+
metadata: { archived: false },
|
|
79
|
+
} as never);
|
|
80
|
+
|
|
81
|
+
const rows = await store.list({ tenantId: "t1", metadata: { archived: "false" } } as never);
|
|
82
|
+
expect(rows).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -79,6 +79,7 @@ CREATE TABLE IF NOT EXISTS lt_eval_runs (
|
|
|
79
79
|
holdout INTEGER NOT NULL DEFAULT 0,
|
|
80
80
|
env_project_id TEXT,
|
|
81
81
|
env_workspace_id TEXT,
|
|
82
|
+
task_id TEXT,
|
|
82
83
|
created_at TEXT NOT NULL,
|
|
83
84
|
started_at TEXT,
|
|
84
85
|
completed_at TEXT,
|
|
@@ -133,6 +134,7 @@ export class LocalEvalStore implements EvalStore {
|
|
|
133
134
|
this.ensureColumn("lt_eval_runs", "interrupted_cases", "interrupted_cases INTEGER NOT NULL DEFAULT 0");
|
|
134
135
|
this.ensureColumn("lt_eval_run_results", "interrupted", "interrupted INTEGER NOT NULL DEFAULT 0");
|
|
135
136
|
this.ensureColumn("lt_eval_cases", "interrupt_policy", "interrupt_policy TEXT");
|
|
137
|
+
this.ensureColumn("lt_eval_runs", "task_id", "task_id TEXT");
|
|
136
138
|
// Project name must be unique per tenant (eval-{agent-id} convention).
|
|
137
139
|
// Repoint child rows (suites, runs) of duplicate projects onto the kept
|
|
138
140
|
// project (newest by created_at, tie-break rowid — mirrors the pg
|
|
@@ -398,9 +400,9 @@ export class LocalEvalStore implements EvalStore {
|
|
|
398
400
|
async createRun(tenantId: string, projectId: string, id: string, data: CreateEvalRunRequest): Promise<EvalRun> {
|
|
399
401
|
const actualId = id || randomUUID();
|
|
400
402
|
this.db.prepare(
|
|
401
|
-
`INSERT INTO lt_eval_runs (id, project_id, tenant_id, status, concurrency, total_cases, passed_cases, failed_cases, avg_score, holdout, env_project_id, env_workspace_id, created_at)
|
|
402
|
-
VALUES (?, ?, ?, 'running', ?, ?, 0, 0, 0, ?, ?, ?, ?)`,
|
|
403
|
-
).run(actualId, projectId, tenantId, data.concurrency, data.totalCases, data.holdout ? 1 : 0, data.envProjectId || null, data.envWorkspaceId || null, nowISO());
|
|
403
|
+
`INSERT INTO lt_eval_runs (id, project_id, tenant_id, status, concurrency, total_cases, passed_cases, failed_cases, avg_score, holdout, env_project_id, env_workspace_id, task_id, created_at)
|
|
404
|
+
VALUES (?, ?, ?, 'running', ?, ?, 0, 0, 0, ?, ?, ?, ?, ?)`,
|
|
405
|
+
).run(actualId, projectId, tenantId, data.concurrency, data.totalCases, data.holdout ? 1 : 0, data.envProjectId || null, data.envWorkspaceId || null, data.taskId || null, nowISO());
|
|
404
406
|
return (await this.getRunById(tenantId, actualId))!;
|
|
405
407
|
}
|
|
406
408
|
|
|
@@ -587,6 +589,7 @@ export class LocalEvalStore implements EvalStore {
|
|
|
587
589
|
holdout: row.holdout ? Boolean(row.holdout) : undefined,
|
|
588
590
|
envProjectId: row.env_project_id as string | undefined,
|
|
589
591
|
envWorkspaceId: row.env_workspace_id as string | undefined,
|
|
592
|
+
taskId: row.task_id as string | undefined,
|
|
590
593
|
createdAt: parseISO(row.created_at as string),
|
|
591
594
|
startedAt: row.started_at ? parseISO(row.started_at as string) : undefined,
|
|
592
595
|
completedAt: row.completed_at ? parseISO(row.completed_at as string) : undefined,
|
|
@@ -6,6 +6,7 @@ import { DatabaseWrapper } from "../database";
|
|
|
6
6
|
import type {
|
|
7
7
|
TaskStore,
|
|
8
8
|
TaskItem,
|
|
9
|
+
TaskFileRef,
|
|
9
10
|
CreateTaskRequest,
|
|
10
11
|
UpdateTaskRequest,
|
|
11
12
|
TaskListFilter,
|
|
@@ -34,6 +35,7 @@ CREATE TABLE IF NOT EXISTS lt_tasks (
|
|
|
34
35
|
failure_reason TEXT,
|
|
35
36
|
workspace_id TEXT,
|
|
36
37
|
project_id TEXT,
|
|
38
|
+
files TEXT,
|
|
37
39
|
created_at TEXT NOT NULL,
|
|
38
40
|
updated_at TEXT NOT NULL,
|
|
39
41
|
PRIMARY KEY (tenant_id, id)
|
|
@@ -60,10 +62,22 @@ interface TaskRow {
|
|
|
60
62
|
failure_reason: string | null;
|
|
61
63
|
workspace_id: string | null;
|
|
62
64
|
project_id: string | null;
|
|
65
|
+
files: string | null;
|
|
63
66
|
created_at: string;
|
|
64
67
|
updated_at: string;
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
function parseTaskFiles(raw: unknown): TaskFileRef[] | undefined {
|
|
71
|
+
if (!raw) return undefined;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
74
|
+
if (!Array.isArray(parsed)) return undefined;
|
|
75
|
+
return parsed as TaskFileRef[];
|
|
76
|
+
} catch {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
function mapRowToTask(row: TaskRow): TaskItem {
|
|
68
82
|
return {
|
|
69
83
|
id: row.id,
|
|
@@ -85,6 +99,7 @@ function mapRowToTask(row: TaskRow): TaskItem {
|
|
|
85
99
|
failureReason: row.failure_reason || undefined,
|
|
86
100
|
workspaceId: row.workspace_id ?? undefined,
|
|
87
101
|
projectId: row.project_id ?? undefined,
|
|
102
|
+
files: parseTaskFiles(row.files),
|
|
88
103
|
createdAt: parseISO(row.created_at),
|
|
89
104
|
updatedAt: parseISO(row.updated_at),
|
|
90
105
|
};
|
|
@@ -96,12 +111,24 @@ export class LocalTaskStore implements TaskStore {
|
|
|
96
111
|
constructor(db: DatabaseWrapper) {
|
|
97
112
|
this.db = db;
|
|
98
113
|
ensureTable(db, DDL);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
104
|
-
this.
|
|
114
|
+
// Existing installs: add columns shipped after the original schema.
|
|
115
|
+
// sql.js bundles an older SQLite that does not support
|
|
116
|
+
// `ADD COLUMN IF NOT EXISTS`, so check via PRAGMA instead.
|
|
117
|
+
this.ensureColumn("lt_tasks", "require_review", "require_review INTEGER DEFAULT 0");
|
|
118
|
+
this.ensureColumn("lt_tasks", "dependencies", "dependencies TEXT");
|
|
119
|
+
this.ensureColumn("lt_tasks", "result", "result TEXT");
|
|
120
|
+
this.ensureColumn("lt_tasks", "failure_reason", "failure_reason TEXT");
|
|
121
|
+
this.ensureColumn("lt_tasks", "workspace_id", "workspace_id TEXT");
|
|
122
|
+
this.ensureColumn("lt_tasks", "project_id", "project_id TEXT");
|
|
123
|
+
this.ensureColumn("lt_tasks", "files", "files TEXT");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Add a column if it does not exist (SQLite version compatible). */
|
|
127
|
+
private ensureColumn(table: string, column: string, ddl: string): void {
|
|
128
|
+
const cols = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
129
|
+
if (!cols.some((c) => c.name === column)) {
|
|
130
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
131
|
+
}
|
|
105
132
|
}
|
|
106
133
|
|
|
107
134
|
async create(
|
|
@@ -111,7 +138,7 @@ export class LocalTaskStore implements TaskStore {
|
|
|
111
138
|
const now = nowISO();
|
|
112
139
|
|
|
113
140
|
this.db.prepare(
|
|
114
|
-
`INSERT INTO lt_tasks (id, tenant_id, owner_type, owner_id, title, description, status, priority, due_date, metadata, parent_id, source_id, context, require_review, dependencies, result, failure_reason, workspace_id, project_id, created_at, updated_at)
|
|
141
|
+
`INSERT INTO lt_tasks (id, tenant_id, owner_type, owner_id, title, description, status, priority, due_date, metadata, parent_id, source_id, context, require_review, dependencies, result, failure_reason, workspace_id, project_id, files, created_at, updated_at)
|
|
115
142
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
116
143
|
).run(
|
|
117
144
|
id, params.tenantId, params.ownerType, params.ownerId, params.title,
|
|
@@ -123,6 +150,7 @@ export class LocalTaskStore implements TaskStore {
|
|
|
123
150
|
params.dependencies ? JSON.stringify(params.dependencies) : null,
|
|
124
151
|
params.result || null, params.failureReason || null,
|
|
125
152
|
params.workspaceId || null, params.projectId || null,
|
|
153
|
+
params.files ? JSON.stringify(params.files) : null,
|
|
126
154
|
now, now,
|
|
127
155
|
);
|
|
128
156
|
|
|
@@ -157,8 +185,17 @@ export class LocalTaskStore implements TaskStore {
|
|
|
157
185
|
for (const [key, value] of Object.entries(filter.metadata)) {
|
|
158
186
|
const safeKey = key.replace(/[^a-zA-Z0-9_]/g, '');
|
|
159
187
|
if (safeKey) {
|
|
160
|
-
|
|
161
|
-
|
|
188
|
+
// Callers pass stringified JSON values ("true"/"false") following
|
|
189
|
+
// the PostgreSQL metadata->> text convention, but SQLite's
|
|
190
|
+
// json_extract returns INTEGER 1/0 for JSON booleans — match both.
|
|
191
|
+
const strVal = String(value);
|
|
192
|
+
if (strVal === 'true' || strVal === 'false') {
|
|
193
|
+
conditions.push(`(json_extract(metadata, '$.${safeKey}') = ? OR json_extract(metadata, '$.${safeKey}') = ?)`);
|
|
194
|
+
params.push(strVal, strVal === 'true' ? 1 : 0);
|
|
195
|
+
} else {
|
|
196
|
+
conditions.push(`json_extract(metadata, '$.${safeKey}') = ?`);
|
|
197
|
+
params.push(strVal);
|
|
198
|
+
}
|
|
162
199
|
}
|
|
163
200
|
}
|
|
164
201
|
}
|
|
@@ -202,6 +239,7 @@ export class LocalTaskStore implements TaskStore {
|
|
|
202
239
|
if (updates.failureReason !== undefined) { setClauses.push('failure_reason = ?'); values.push(updates.failureReason); }
|
|
203
240
|
if (updates.workspaceId !== undefined) { setClauses.push('workspace_id = ?'); values.push(updates.workspaceId); }
|
|
204
241
|
if (updates.projectId !== undefined) { setClauses.push('project_id = ?'); values.push(updates.projectId); }
|
|
242
|
+
if (updates.files !== undefined) { setClauses.push('files = ?'); values.push(JSON.stringify(updates.files)); }
|
|
205
243
|
|
|
206
244
|
if (setClauses.length === 1) return existing;
|
|
207
245
|
|