@codexa/cli 9.0.31 → 9.0.32
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/commands/architect.ts +52 -87
- package/commands/check.ts +22 -23
- package/commands/clear.ts +42 -48
- package/commands/decide.ts +46 -44
- package/commands/discover.ts +81 -94
- package/commands/integration.test.ts +262 -313
- package/commands/knowledge.test.ts +56 -61
- package/commands/knowledge.ts +126 -131
- package/commands/patterns.ts +28 -43
- package/commands/plan.ts +50 -48
- package/commands/product.ts +57 -59
- package/commands/research.ts +64 -77
- package/commands/review.ts +100 -86
- package/commands/simplify.ts +24 -35
- package/commands/spec-resolver.test.ts +52 -48
- package/commands/spec-resolver.ts +21 -23
- package/commands/standards.ts +20 -27
- package/commands/sync.ts +2 -8
- package/commands/task.ts +106 -97
- package/commands/team.test.ts +22 -83
- package/commands/team.ts +62 -50
- package/commands/utils.ts +83 -81
- package/context/assembly.ts +0 -1
- package/context/generator.ts +66 -79
- package/context/sections.ts +8 -14
- package/db/connection.ts +195 -19
- package/db/schema.test.ts +288 -299
- package/db/schema.ts +297 -394
- package/db/test-helpers.ts +18 -29
- package/gates/standards-validator.test.ts +83 -86
- package/gates/standards-validator.ts +9 -41
- package/gates/validator.test.ts +13 -22
- package/gates/validator.ts +69 -107
- package/package.json +2 -1
- package/protocol/process-return.ts +41 -57
- package/simplify/prompt-builder.test.ts +44 -42
- package/simplify/prompt-builder.ts +12 -14
- package/workflow.ts +159 -174
package/db/schema.test.ts
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
|
|
6
|
-
// Setup: use a temporary directory for test databases
|
|
7
|
-
const TEST_DIR = join(import.meta.dir, "..", ".test-db");
|
|
8
|
-
const TEST_DB_PATH = join(TEST_DIR, "test.db");
|
|
9
|
-
|
|
10
|
-
// We need to override the DB path before importing schema functions.
|
|
11
|
-
// The connection module uses process.cwd() so we mock via direct DB manipulation.
|
|
2
|
+
import { createClient, type Client } from "@libsql/client";
|
|
3
|
+
import { setClient, resetClient, dbGet, dbAll, dbRun, dbExec } from "./connection";
|
|
12
4
|
|
|
13
5
|
describe("Migration System", () => {
|
|
14
|
-
let
|
|
6
|
+
let client: Client;
|
|
15
7
|
|
|
16
8
|
beforeEach(() => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
20
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
9
|
+
client = createClient({ url: ":memory:" });
|
|
10
|
+
setClient(client);
|
|
21
11
|
});
|
|
22
12
|
|
|
23
13
|
afterEach(() => {
|
|
24
|
-
|
|
14
|
+
resetClient();
|
|
25
15
|
});
|
|
26
16
|
|
|
27
|
-
function createBaseTables(
|
|
28
|
-
|
|
29
|
-
db.exec(`
|
|
17
|
+
async function createBaseTables() {
|
|
18
|
+
await dbExec(`
|
|
30
19
|
CREATE TABLE IF NOT EXISTS specs (
|
|
31
20
|
id TEXT PRIMARY KEY,
|
|
32
21
|
name TEXT NOT NULL,
|
|
@@ -34,16 +23,18 @@ describe("Migration System", () => {
|
|
|
34
23
|
approved_at TEXT,
|
|
35
24
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
36
25
|
updated_at TEXT
|
|
37
|
-
)
|
|
38
|
-
|
|
26
|
+
)
|
|
27
|
+
`);
|
|
28
|
+
await dbExec(`
|
|
39
29
|
CREATE TABLE IF NOT EXISTS project (
|
|
40
30
|
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
41
31
|
name TEXT,
|
|
42
32
|
stack TEXT NOT NULL,
|
|
43
33
|
discovered_at TEXT,
|
|
44
34
|
updated_at TEXT
|
|
45
|
-
)
|
|
46
|
-
|
|
35
|
+
)
|
|
36
|
+
`);
|
|
37
|
+
await dbExec(`
|
|
47
38
|
CREATE TABLE IF NOT EXISTS tasks (
|
|
48
39
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
40
|
spec_id TEXT NOT NULL REFERENCES specs(id),
|
|
@@ -57,8 +48,9 @@ describe("Migration System", () => {
|
|
|
57
48
|
checkpoint TEXT,
|
|
58
49
|
completed_at TEXT,
|
|
59
50
|
UNIQUE(spec_id, number)
|
|
60
|
-
)
|
|
61
|
-
|
|
51
|
+
)
|
|
52
|
+
`);
|
|
53
|
+
await dbExec(`
|
|
62
54
|
CREATE TABLE IF NOT EXISTS decisions (
|
|
63
55
|
id TEXT PRIMARY KEY,
|
|
64
56
|
spec_id TEXT NOT NULL REFERENCES specs(id),
|
|
@@ -68,103 +60,101 @@ describe("Migration System", () => {
|
|
|
68
60
|
rationale TEXT,
|
|
69
61
|
status TEXT DEFAULT 'active',
|
|
70
62
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
71
|
-
)
|
|
72
|
-
|
|
63
|
+
)
|
|
64
|
+
`);
|
|
65
|
+
await dbExec(`
|
|
73
66
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
74
67
|
version TEXT PRIMARY KEY,
|
|
75
68
|
description TEXT NOT NULL,
|
|
76
69
|
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
77
|
-
)
|
|
70
|
+
)
|
|
78
71
|
`);
|
|
79
72
|
}
|
|
80
73
|
|
|
81
|
-
// ═══════════════════════════════════════════════════════════════
|
|
82
|
-
// runMigrations() tests
|
|
83
|
-
// ═══════════════════════════════════════════════════════════════
|
|
84
|
-
|
|
85
74
|
describe("runMigrations()", () => {
|
|
86
|
-
it("should apply all migrations on a fresh database", () => {
|
|
87
|
-
createBaseTables(
|
|
75
|
+
it("should apply all migrations on a fresh database", async () => {
|
|
76
|
+
await createBaseTables();
|
|
88
77
|
|
|
89
|
-
// Define test migrations
|
|
90
78
|
const migrations = [
|
|
91
79
|
{
|
|
92
80
|
version: "8.4.0",
|
|
93
81
|
description: "Add analysis_id to specs",
|
|
94
|
-
up: (
|
|
82
|
+
up: async () => await dbExec("ALTER TABLE specs ADD COLUMN analysis_id TEXT"),
|
|
95
83
|
},
|
|
96
84
|
{
|
|
97
85
|
version: "8.7.0",
|
|
98
86
|
description: "Add cli_version to project",
|
|
99
|
-
up: (
|
|
87
|
+
up: async () => await dbExec("ALTER TABLE project ADD COLUMN cli_version TEXT"),
|
|
100
88
|
},
|
|
101
89
|
];
|
|
102
90
|
|
|
103
|
-
// Run migrations manually (simulating runMigrations logic)
|
|
104
91
|
for (const migration of migrations) {
|
|
105
|
-
const existing =
|
|
92
|
+
const existing = await dbGet<{ version: string }>(
|
|
93
|
+
"SELECT version FROM schema_migrations WHERE version = ?",
|
|
94
|
+
[migration.version]
|
|
95
|
+
);
|
|
106
96
|
if (existing) continue;
|
|
107
97
|
|
|
108
|
-
migration.up(
|
|
109
|
-
|
|
98
|
+
await migration.up();
|
|
99
|
+
await dbRun(
|
|
110
100
|
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)",
|
|
111
101
|
[migration.version, migration.description]
|
|
112
102
|
);
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
|
|
116
|
-
|
|
105
|
+
const applied = await dbAll<{ version: string }>(
|
|
106
|
+
"SELECT * FROM schema_migrations ORDER BY version"
|
|
107
|
+
);
|
|
117
108
|
expect(applied.length).toBe(2);
|
|
118
109
|
expect(applied[0].version).toBe("8.4.0");
|
|
119
110
|
expect(applied[1].version).toBe("8.7.0");
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const colNames = columns.map((c: any) => c.name);
|
|
112
|
+
const columns = await dbAll<{ name: string }>("PRAGMA table_info(specs)");
|
|
113
|
+
const colNames = columns.map((c) => c.name);
|
|
124
114
|
expect(colNames).toContain("analysis_id");
|
|
125
115
|
});
|
|
126
116
|
|
|
127
|
-
it("should be idempotent — calling twice applies each migration only once", () => {
|
|
128
|
-
createBaseTables(
|
|
117
|
+
it("should be idempotent — calling twice applies each migration only once", async () => {
|
|
118
|
+
await createBaseTables();
|
|
129
119
|
|
|
130
120
|
const migration = {
|
|
131
121
|
version: "8.4.0",
|
|
132
122
|
description: "Add analysis_id to specs",
|
|
133
|
-
up: (
|
|
123
|
+
up: async () => await dbExec("ALTER TABLE specs ADD COLUMN analysis_id TEXT"),
|
|
134
124
|
};
|
|
135
125
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
db.run(
|
|
126
|
+
await migration.up();
|
|
127
|
+
await dbRun(
|
|
139
128
|
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)",
|
|
140
129
|
[migration.version, migration.description]
|
|
141
130
|
);
|
|
142
131
|
|
|
143
|
-
|
|
144
|
-
|
|
132
|
+
const existing = await dbGet<{ version: string }>(
|
|
133
|
+
"SELECT version FROM schema_migrations WHERE version = ?",
|
|
134
|
+
[migration.version]
|
|
135
|
+
);
|
|
145
136
|
expect(existing).not.toBeNull();
|
|
146
137
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
138
|
+
const applied = await dbGet<{ c: number }>(
|
|
139
|
+
"SELECT COUNT(*) as c FROM schema_migrations"
|
|
140
|
+
);
|
|
141
|
+
expect(applied!.c).toBe(1);
|
|
150
142
|
});
|
|
151
143
|
|
|
152
|
-
it("should absorb 'duplicate column name' errors from pre-migration databases", () => {
|
|
153
|
-
createBaseTables(
|
|
144
|
+
it("should absorb 'duplicate column name' errors from pre-migration databases", async () => {
|
|
145
|
+
await createBaseTables();
|
|
154
146
|
|
|
155
|
-
|
|
156
|
-
db.exec("ALTER TABLE specs ADD COLUMN analysis_id TEXT");
|
|
147
|
+
await dbExec("ALTER TABLE specs ADD COLUMN analysis_id TEXT");
|
|
157
148
|
|
|
158
|
-
// Now try to run the migration — should NOT throw
|
|
159
149
|
const migration = {
|
|
160
150
|
version: "8.4.0",
|
|
161
151
|
description: "Add analysis_id to specs",
|
|
162
|
-
up: (
|
|
152
|
+
up: async () => await dbExec("ALTER TABLE specs ADD COLUMN analysis_id TEXT"),
|
|
163
153
|
};
|
|
164
154
|
|
|
165
155
|
let absorbed = false;
|
|
166
156
|
try {
|
|
167
|
-
migration.up(
|
|
157
|
+
await migration.up();
|
|
168
158
|
} catch (e: any) {
|
|
169
159
|
if (e.message.includes("duplicate column name")) {
|
|
170
160
|
absorbed = true;
|
|
@@ -175,41 +165,36 @@ describe("Migration System", () => {
|
|
|
175
165
|
|
|
176
166
|
expect(absorbed).toBe(true);
|
|
177
167
|
|
|
178
|
-
|
|
179
|
-
db.run(
|
|
168
|
+
await dbRun(
|
|
180
169
|
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)",
|
|
181
170
|
[migration.version, migration.description]
|
|
182
171
|
);
|
|
183
172
|
|
|
184
|
-
const applied =
|
|
185
|
-
|
|
173
|
+
const applied = await dbGet<{ c: number }>(
|
|
174
|
+
"SELECT COUNT(*) as c FROM schema_migrations"
|
|
175
|
+
);
|
|
176
|
+
expect(applied!.c).toBe(1);
|
|
186
177
|
});
|
|
187
178
|
|
|
188
|
-
it("should propagate real errors (not duplicate column)", () => {
|
|
189
|
-
createBaseTables(
|
|
179
|
+
it("should propagate real errors (not duplicate column)", async () => {
|
|
180
|
+
await createBaseTables();
|
|
190
181
|
|
|
191
182
|
const badMigration = {
|
|
192
183
|
version: "99.0.0",
|
|
193
184
|
description: "Bad migration",
|
|
194
|
-
up: (
|
|
185
|
+
up: async () => await dbExec("ALTER TABLE nonexistent_table ADD COLUMN foo TEXT"),
|
|
195
186
|
};
|
|
196
187
|
|
|
197
|
-
expect(()
|
|
198
|
-
badMigration.up(db);
|
|
199
|
-
}).toThrow();
|
|
188
|
+
expect(badMigration.up()).rejects.toThrow();
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
190
|
+
const applied = await dbGet<{ c: number }>(
|
|
191
|
+
"SELECT COUNT(*) as c FROM schema_migrations"
|
|
192
|
+
);
|
|
193
|
+
expect(applied!.c).toBe(0);
|
|
204
194
|
});
|
|
205
195
|
});
|
|
206
196
|
|
|
207
|
-
// ═══════════════════════════════════════════════════════════════
|
|
208
|
-
// getNextDecisionId() tests
|
|
209
|
-
// ═══════════════════════════════════════════════════════════════
|
|
210
|
-
|
|
211
197
|
describe("getNextDecisionId()", () => {
|
|
212
|
-
// Reimplemented: uses timestamp+random instead of sequential count
|
|
213
198
|
function getNextDecisionId(specId: string): string {
|
|
214
199
|
const slug = specId.split("-").slice(1, 3).join("-");
|
|
215
200
|
const ts = Date.now().toString(36);
|
|
@@ -234,7 +219,6 @@ describe("Migration System", () => {
|
|
|
234
219
|
});
|
|
235
220
|
|
|
236
221
|
it("should not require DB access (no race condition)", () => {
|
|
237
|
-
// Generate 100 IDs rapidly — all should be unique
|
|
238
222
|
const ids = new Set<string>();
|
|
239
223
|
for (let i = 0; i < 100; i++) {
|
|
240
224
|
ids.add(getNextDecisionId("SPEC-001"));
|
|
@@ -243,76 +227,69 @@ describe("Migration System", () => {
|
|
|
243
227
|
});
|
|
244
228
|
});
|
|
245
229
|
|
|
246
|
-
// ═══════════════════════════════════════════════════════════════
|
|
247
|
-
// claimTask() tests
|
|
248
|
-
// ═══════════════════════════════════════════════════════════════
|
|
249
|
-
|
|
250
230
|
describe("claimTask()", () => {
|
|
251
|
-
function claimTask(taskId: number): boolean {
|
|
252
|
-
const result =
|
|
231
|
+
async function claimTask(taskId: number): Promise<boolean> {
|
|
232
|
+
const result = await dbRun(
|
|
253
233
|
"UPDATE tasks SET status = 'running' WHERE id = ? AND status = 'pending'",
|
|
254
234
|
[taskId]
|
|
255
235
|
);
|
|
256
236
|
return result.changes > 0;
|
|
257
237
|
}
|
|
258
238
|
|
|
259
|
-
beforeEach(() => {
|
|
260
|
-
createBaseTables(
|
|
261
|
-
|
|
262
|
-
|
|
239
|
+
beforeEach(async () => {
|
|
240
|
+
await createBaseTables();
|
|
241
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
242
|
+
await dbRun(
|
|
263
243
|
"INSERT INTO tasks (spec_id, number, name, status) VALUES (?, ?, ?, ?)",
|
|
264
244
|
["SPEC-001", 1, "Test task", "pending"]
|
|
265
245
|
);
|
|
266
246
|
});
|
|
267
247
|
|
|
268
|
-
it("should return true and set status to running for a pending task", () => {
|
|
269
|
-
const
|
|
270
|
-
|
|
248
|
+
it("should return true and set status to running for a pending task", async () => {
|
|
249
|
+
const row = await dbGet<{ id: number }>("SELECT id FROM tasks WHERE number = 1");
|
|
250
|
+
const taskId = row!.id;
|
|
251
|
+
expect(await claimTask(taskId)).toBe(true);
|
|
271
252
|
|
|
272
|
-
const task =
|
|
273
|
-
expect(task
|
|
253
|
+
const task = await dbGet<{ status: string }>("SELECT status FROM tasks WHERE id = ?", [taskId]);
|
|
254
|
+
expect(task!.status).toBe("running");
|
|
274
255
|
});
|
|
275
256
|
|
|
276
|
-
it("should return false for a task already in running status", () => {
|
|
277
|
-
const
|
|
257
|
+
it("should return false for a task already in running status", async () => {
|
|
258
|
+
const row = await dbGet<{ id: number }>("SELECT id FROM tasks WHERE number = 1");
|
|
259
|
+
const taskId = row!.id;
|
|
278
260
|
|
|
279
|
-
|
|
280
|
-
expect(claimTask(taskId)).toBe(
|
|
281
|
-
// Second claim fails
|
|
282
|
-
expect(claimTask(taskId)).toBe(false);
|
|
261
|
+
expect(await claimTask(taskId)).toBe(true);
|
|
262
|
+
expect(await claimTask(taskId)).toBe(false);
|
|
283
263
|
});
|
|
284
264
|
|
|
285
|
-
it("should return false for a task in done status", () => {
|
|
286
|
-
const
|
|
287
|
-
|
|
265
|
+
it("should return false for a task in done status", async () => {
|
|
266
|
+
const row = await dbGet<{ id: number }>("SELECT id FROM tasks WHERE number = 1");
|
|
267
|
+
const taskId = row!.id;
|
|
268
|
+
await dbRun("UPDATE tasks SET status = 'done' WHERE id = ?", [taskId]);
|
|
288
269
|
|
|
289
|
-
expect(claimTask(taskId)).toBe(false);
|
|
270
|
+
expect(await claimTask(taskId)).toBe(false);
|
|
290
271
|
});
|
|
291
272
|
|
|
292
|
-
it("should return false for a non-existent task ID", () => {
|
|
293
|
-
expect(claimTask(99999)).toBe(false);
|
|
273
|
+
it("should return false for a non-existent task ID", async () => {
|
|
274
|
+
expect(await claimTask(99999)).toBe(false);
|
|
294
275
|
});
|
|
295
276
|
|
|
296
|
-
it("should prevent double-claim (only first caller wins)", () => {
|
|
297
|
-
const
|
|
277
|
+
it("should prevent double-claim (only first caller wins)", async () => {
|
|
278
|
+
const row = await dbGet<{ id: number }>("SELECT id FROM tasks WHERE number = 1");
|
|
279
|
+
const taskId = row!.id;
|
|
298
280
|
|
|
299
|
-
|
|
300
|
-
const
|
|
301
|
-
const claim2 = claimTask(taskId);
|
|
281
|
+
const claim1 = await claimTask(taskId);
|
|
282
|
+
const claim2 = await claimTask(taskId);
|
|
302
283
|
|
|
303
284
|
expect(claim1).toBe(true);
|
|
304
285
|
expect(claim2).toBe(false);
|
|
305
286
|
});
|
|
306
287
|
});
|
|
307
288
|
|
|
308
|
-
// ═══════════════════════════════════════════════════════════════
|
|
309
|
-
// v9.3: Agent Performance (P3.2)
|
|
310
|
-
// ═══════════════════════════════════════════════════════════════
|
|
311
|
-
|
|
312
289
|
describe("agent_performance", () => {
|
|
313
|
-
function createPerformanceTables(
|
|
314
|
-
createBaseTables(
|
|
315
|
-
|
|
290
|
+
async function createPerformanceTables() {
|
|
291
|
+
await createBaseTables();
|
|
292
|
+
await dbExec(`
|
|
316
293
|
CREATE TABLE IF NOT EXISTS agent_performance (
|
|
317
294
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
318
295
|
agent_type TEXT NOT NULL,
|
|
@@ -328,9 +305,8 @@ describe("Migration System", () => {
|
|
|
328
305
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
329
306
|
)
|
|
330
307
|
`);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
db.exec(`
|
|
308
|
+
await dbExec("CREATE INDEX IF NOT EXISTS idx_agent_perf_type ON agent_performance(agent_type)");
|
|
309
|
+
await dbExec(`
|
|
334
310
|
CREATE TABLE IF NOT EXISTS gate_bypasses (
|
|
335
311
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
336
312
|
spec_id TEXT,
|
|
@@ -342,11 +318,11 @@ describe("Migration System", () => {
|
|
|
342
318
|
`);
|
|
343
319
|
}
|
|
344
320
|
|
|
345
|
-
it("migration 9.3.0 should create agent_performance table", () => {
|
|
346
|
-
createPerformanceTables(
|
|
321
|
+
it("migration 9.3.0 should create agent_performance table", async () => {
|
|
322
|
+
await createPerformanceTables();
|
|
347
323
|
|
|
348
|
-
const columns =
|
|
349
|
-
const colNames = columns.map((c
|
|
324
|
+
const columns = await dbAll<{ name: string }>("PRAGMA table_info(agent_performance)");
|
|
325
|
+
const colNames = columns.map((c) => c.name);
|
|
350
326
|
expect(colNames).toContain("agent_type");
|
|
351
327
|
expect(colNames).toContain("spec_id");
|
|
352
328
|
expect(colNames).toContain("task_id");
|
|
@@ -356,46 +332,49 @@ describe("Migration System", () => {
|
|
|
356
332
|
expect(colNames).toContain("execution_duration_ms");
|
|
357
333
|
});
|
|
358
334
|
|
|
359
|
-
it("should insert and retrieve performance data", () => {
|
|
360
|
-
createPerformanceTables(
|
|
361
|
-
|
|
362
|
-
|
|
335
|
+
it("should insert and retrieve performance data", async () => {
|
|
336
|
+
await createPerformanceTables();
|
|
337
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
338
|
+
await dbRun(
|
|
363
339
|
"INSERT INTO tasks (spec_id, number, name, agent, status) VALUES (?, ?, ?, ?, ?)",
|
|
364
340
|
["SPEC-001", 1, "Test task", "frontend-next", "done"]
|
|
365
341
|
);
|
|
366
342
|
|
|
367
343
|
const now = new Date().toISOString();
|
|
368
|
-
|
|
344
|
+
await dbRun(
|
|
369
345
|
`INSERT INTO agent_performance
|
|
370
346
|
(agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, files_created, files_modified, context_size_bytes, execution_duration_ms, created_at)
|
|
371
347
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
372
348
|
["frontend-next", "SPEC-001", 1, 7, 7, 0, 3, 1, 4096, 15000, now]
|
|
373
349
|
);
|
|
374
350
|
|
|
375
|
-
const rows =
|
|
351
|
+
const rows = await dbAll<any>(
|
|
352
|
+
"SELECT * FROM agent_performance WHERE agent_type = ?",
|
|
353
|
+
["frontend-next"]
|
|
354
|
+
);
|
|
376
355
|
expect(rows).toHaveLength(1);
|
|
377
356
|
expect(rows[0].gates_passed_first_try).toBe(7);
|
|
378
357
|
expect(rows[0].bypasses_used).toBe(0);
|
|
379
358
|
expect(rows[0].execution_duration_ms).toBe(15000);
|
|
380
359
|
});
|
|
381
360
|
|
|
382
|
-
it("should compute hints: no data returns empty", () => {
|
|
383
|
-
createPerformanceTables(
|
|
361
|
+
it("should compute hints: no data returns empty", async () => {
|
|
362
|
+
await createPerformanceTables();
|
|
384
363
|
|
|
385
|
-
const recent =
|
|
386
|
-
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5"
|
|
387
|
-
|
|
364
|
+
const recent = await dbAll<any>(
|
|
365
|
+
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5",
|
|
366
|
+
["nonexistent"]
|
|
367
|
+
);
|
|
388
368
|
expect(recent).toHaveLength(0);
|
|
389
369
|
});
|
|
390
370
|
|
|
391
|
-
it("should detect high bypass rate from performance data", () => {
|
|
392
|
-
createPerformanceTables(
|
|
393
|
-
|
|
371
|
+
it("should detect high bypass rate from performance data", async () => {
|
|
372
|
+
await createPerformanceTables();
|
|
373
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
394
374
|
|
|
395
375
|
const now = new Date().toISOString();
|
|
396
|
-
// Insert 3 records with bypasses
|
|
397
376
|
for (let i = 1; i <= 3; i++) {
|
|
398
|
-
|
|
377
|
+
await dbRun(
|
|
399
378
|
`INSERT INTO agent_performance
|
|
400
379
|
(agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, created_at)
|
|
401
380
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
@@ -403,9 +382,10 @@ describe("Migration System", () => {
|
|
|
403
382
|
);
|
|
404
383
|
}
|
|
405
384
|
|
|
406
|
-
const recent =
|
|
407
|
-
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5"
|
|
408
|
-
|
|
385
|
+
const recent = await dbAll<any>(
|
|
386
|
+
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5",
|
|
387
|
+
["backend-go"]
|
|
388
|
+
);
|
|
409
389
|
|
|
410
390
|
const avgBypass = recent.reduce((sum: number, r: any) => sum + r.bypasses_used, 0) / recent.length;
|
|
411
391
|
expect(avgBypass).toBe(2);
|
|
@@ -415,16 +395,16 @@ describe("Migration System", () => {
|
|
|
415
395
|
return sum + (r.gates_total > 0 ? r.gates_passed_first_try / r.gates_total : 1);
|
|
416
396
|
}, 0) / recent.length;
|
|
417
397
|
expect(avgGateRate).toBeCloseTo(5 / 7, 2);
|
|
418
|
-
expect(avgGateRate < 0.7).toBe(false);
|
|
398
|
+
expect(avgGateRate < 0.7).toBe(false);
|
|
419
399
|
});
|
|
420
400
|
|
|
421
|
-
it("should detect low gate pass rate", () => {
|
|
422
|
-
createPerformanceTables(
|
|
423
|
-
|
|
401
|
+
it("should detect low gate pass rate", async () => {
|
|
402
|
+
await createPerformanceTables();
|
|
403
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
424
404
|
|
|
425
405
|
const now = new Date().toISOString();
|
|
426
406
|
for (let i = 1; i <= 3; i++) {
|
|
427
|
-
|
|
407
|
+
await dbRun(
|
|
428
408
|
`INSERT INTO agent_performance
|
|
429
409
|
(agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, created_at)
|
|
430
410
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
@@ -432,45 +412,52 @@ describe("Migration System", () => {
|
|
|
432
412
|
);
|
|
433
413
|
}
|
|
434
414
|
|
|
435
|
-
const recent =
|
|
436
|
-
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5"
|
|
437
|
-
|
|
415
|
+
const recent = await dbAll<any>(
|
|
416
|
+
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5",
|
|
417
|
+
["backend-csharp"]
|
|
418
|
+
);
|
|
438
419
|
|
|
439
420
|
const avgGateRate = recent.reduce((sum: number, r: any) => {
|
|
440
421
|
return sum + (r.gates_total > 0 ? r.gates_passed_first_try / r.gates_total : 1);
|
|
441
422
|
}, 0) / recent.length;
|
|
442
423
|
expect(avgGateRate).toBeCloseTo(3 / 7, 2);
|
|
443
|
-
expect(avgGateRate < 0.7).toBe(true);
|
|
424
|
+
expect(avgGateRate < 0.7).toBe(true);
|
|
444
425
|
});
|
|
445
426
|
|
|
446
|
-
it("should track frequent gate bypass types", () => {
|
|
447
|
-
createPerformanceTables(
|
|
448
|
-
|
|
449
|
-
|
|
427
|
+
it("should track frequent gate bypass types", async () => {
|
|
428
|
+
await createPerformanceTables();
|
|
429
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
430
|
+
await dbRun(
|
|
450
431
|
"INSERT INTO tasks (spec_id, number, name, agent, status) VALUES (?, ?, ?, ?, ?)",
|
|
451
432
|
["SPEC-001", 1, "Task 1", "frontend-next", "done"]
|
|
452
433
|
);
|
|
453
|
-
|
|
434
|
+
await dbRun(
|
|
454
435
|
"INSERT INTO tasks (spec_id, number, name, agent, status) VALUES (?, ?, ?, ?, ?)",
|
|
455
436
|
["SPEC-001", 2, "Task 2", "frontend-next", "done"]
|
|
456
437
|
);
|
|
457
438
|
|
|
458
|
-
const
|
|
459
|
-
const
|
|
439
|
+
const row1 = await dbGet<{ id: number }>("SELECT id FROM tasks WHERE number = 1");
|
|
440
|
+
const row2 = await dbGet<{ id: number }>("SELECT id FROM tasks WHERE number = 2");
|
|
441
|
+
const taskId1 = row1!.id;
|
|
442
|
+
const taskId2 = row2!.id;
|
|
460
443
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
["SPEC-001", taskId1, "standards-follow", "test"]
|
|
464
|
-
|
|
465
|
-
|
|
444
|
+
await dbRun(
|
|
445
|
+
"INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, ?, ?, ?)",
|
|
446
|
+
["SPEC-001", taskId1, "standards-follow", "test"]
|
|
447
|
+
);
|
|
448
|
+
await dbRun(
|
|
449
|
+
"INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, ?, ?, ?)",
|
|
450
|
+
["SPEC-001", taskId2, "standards-follow", "test"]
|
|
451
|
+
);
|
|
466
452
|
|
|
467
|
-
const bypassTypes =
|
|
453
|
+
const bypassTypes = await dbAll<any>(
|
|
468
454
|
`SELECT gb.gate_name, COUNT(*) as cnt FROM gate_bypasses gb
|
|
469
455
|
JOIN tasks t ON gb.task_id = t.id
|
|
470
456
|
WHERE t.agent = ?
|
|
471
457
|
GROUP BY gb.gate_name
|
|
472
|
-
ORDER BY cnt DESC LIMIT 3
|
|
473
|
-
|
|
458
|
+
ORDER BY cnt DESC LIMIT 3`,
|
|
459
|
+
["frontend-next"]
|
|
460
|
+
);
|
|
474
461
|
|
|
475
462
|
expect(bypassTypes).toHaveLength(1);
|
|
476
463
|
expect(bypassTypes[0].gate_name).toBe("standards-follow");
|
|
@@ -478,14 +465,10 @@ describe("Migration System", () => {
|
|
|
478
465
|
});
|
|
479
466
|
});
|
|
480
467
|
|
|
481
|
-
// ═══════════════════════════════════════════════════════════════
|
|
482
|
-
// v9.4: Knowledge Acknowledgments (P1-5)
|
|
483
|
-
// ═══════════════════════════════════════════════════════════════
|
|
484
|
-
|
|
485
468
|
describe("knowledge_acknowledgments", () => {
|
|
486
|
-
function createKnowledgeTables(
|
|
487
|
-
createBaseTables(
|
|
488
|
-
|
|
469
|
+
async function createKnowledgeTables() {
|
|
470
|
+
await createBaseTables();
|
|
471
|
+
await dbExec(`
|
|
489
472
|
CREATE TABLE IF NOT EXISTS knowledge (
|
|
490
473
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
491
474
|
spec_id TEXT NOT NULL,
|
|
@@ -497,7 +480,7 @@ describe("Migration System", () => {
|
|
|
497
480
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
498
481
|
)
|
|
499
482
|
`);
|
|
500
|
-
|
|
483
|
+
await dbExec(`
|
|
501
484
|
CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
|
|
502
485
|
knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
503
486
|
task_id INTEGER NOT NULL,
|
|
@@ -505,72 +488,72 @@ describe("Migration System", () => {
|
|
|
505
488
|
PRIMARY KEY (knowledge_id, task_id)
|
|
506
489
|
)
|
|
507
490
|
`);
|
|
508
|
-
|
|
491
|
+
await dbExec("CREATE INDEX IF NOT EXISTS idx_ka_task ON knowledge_acknowledgments(task_id)");
|
|
509
492
|
}
|
|
510
493
|
|
|
511
|
-
it("migration 9.4.0 should create knowledge_acknowledgments table", () => {
|
|
512
|
-
createKnowledgeTables(
|
|
494
|
+
it("migration 9.4.0 should create knowledge_acknowledgments table", async () => {
|
|
495
|
+
await createKnowledgeTables();
|
|
513
496
|
|
|
514
|
-
const columns =
|
|
515
|
-
const colNames = columns.map((c
|
|
497
|
+
const columns = await dbAll<{ name: string }>("PRAGMA table_info(knowledge_acknowledgments)");
|
|
498
|
+
const colNames = columns.map((c) => c.name);
|
|
516
499
|
expect(colNames).toContain("knowledge_id");
|
|
517
500
|
expect(colNames).toContain("task_id");
|
|
518
501
|
expect(colNames).toContain("acknowledged_at");
|
|
519
502
|
});
|
|
520
503
|
|
|
521
|
-
it("should insert and query acknowledgment", () => {
|
|
522
|
-
createKnowledgeTables(
|
|
523
|
-
|
|
524
|
-
|
|
504
|
+
it("should insert and query acknowledgment", async () => {
|
|
505
|
+
await createKnowledgeTables();
|
|
506
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
507
|
+
await dbRun(
|
|
525
508
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
526
509
|
["SPEC-001", 1, "discovery", "Test knowledge", "critical"]
|
|
527
510
|
);
|
|
528
511
|
|
|
529
|
-
const
|
|
512
|
+
const kRow = await dbGet<{ id: number }>("SELECT id FROM knowledge LIMIT 1");
|
|
513
|
+
const kid = kRow!.id;
|
|
530
514
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
)
|
|
515
|
+
const before = await dbGet(
|
|
516
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?",
|
|
517
|
+
[kid, 2]
|
|
518
|
+
);
|
|
535
519
|
expect(before).toBeNull();
|
|
536
520
|
|
|
537
|
-
|
|
538
|
-
db.run(
|
|
521
|
+
await dbRun(
|
|
539
522
|
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
540
523
|
[kid, 2]
|
|
541
524
|
);
|
|
542
525
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
)
|
|
526
|
+
const after = await dbGet(
|
|
527
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?",
|
|
528
|
+
[kid, 2]
|
|
529
|
+
);
|
|
547
530
|
expect(after).not.toBeNull();
|
|
548
531
|
});
|
|
549
532
|
|
|
550
|
-
it("should be idempotent (INSERT OR IGNORE)", () => {
|
|
551
|
-
createKnowledgeTables(
|
|
552
|
-
|
|
553
|
-
|
|
533
|
+
it("should be idempotent (INSERT OR IGNORE)", async () => {
|
|
534
|
+
await createKnowledgeTables();
|
|
535
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
536
|
+
await dbRun(
|
|
554
537
|
"INSERT INTO knowledge (spec_id, task_origin, category, content) VALUES (?, ?, ?, ?)",
|
|
555
538
|
["SPEC-001", 1, "discovery", "Test"]
|
|
556
539
|
);
|
|
557
540
|
|
|
558
|
-
const
|
|
541
|
+
const kRow = await dbGet<{ id: number }>("SELECT id FROM knowledge LIMIT 1");
|
|
542
|
+
const kid = kRow!.id;
|
|
559
543
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
db.run("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kid, 2]);
|
|
544
|
+
await dbRun("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kid, 2]);
|
|
545
|
+
await dbRun("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kid, 2]);
|
|
563
546
|
|
|
564
|
-
const count = (
|
|
565
|
-
"SELECT COUNT(*) as c FROM knowledge_acknowledgments WHERE knowledge_id = ?"
|
|
566
|
-
|
|
567
|
-
|
|
547
|
+
const count = await dbGet<{ c: number }>(
|
|
548
|
+
"SELECT COUNT(*) as c FROM knowledge_acknowledgments WHERE knowledge_id = ?",
|
|
549
|
+
[kid]
|
|
550
|
+
);
|
|
551
|
+
expect(count!.c).toBe(1);
|
|
568
552
|
});
|
|
569
553
|
|
|
570
|
-
it("should migrate data from JSON acknowledged_by", () => {
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
db.exec(`
|
|
554
|
+
it("should migrate data from JSON acknowledged_by", async () => {
|
|
555
|
+
await createBaseTables();
|
|
556
|
+
await dbExec(`
|
|
574
557
|
CREATE TABLE IF NOT EXISTS knowledge (
|
|
575
558
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
576
559
|
spec_id TEXT NOT NULL,
|
|
@@ -581,62 +564,65 @@ describe("Migration System", () => {
|
|
|
581
564
|
broadcast_to TEXT DEFAULT 'all',
|
|
582
565
|
acknowledged_by TEXT,
|
|
583
566
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
584
|
-
)
|
|
567
|
+
)
|
|
568
|
+
`);
|
|
569
|
+
await dbExec(`
|
|
585
570
|
CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
|
|
586
571
|
knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
587
572
|
task_id INTEGER NOT NULL,
|
|
588
573
|
acknowledged_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
589
574
|
PRIMARY KEY (knowledge_id, task_id)
|
|
590
|
-
)
|
|
575
|
+
)
|
|
591
576
|
`);
|
|
592
|
-
|
|
577
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
593
578
|
|
|
594
|
-
|
|
595
|
-
db.run(
|
|
579
|
+
await dbRun(
|
|
596
580
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, acknowledged_by) VALUES (?, ?, ?, ?, ?)",
|
|
597
581
|
["SPEC-001", 1, "discovery", "Test", JSON.stringify([2, 3, 5])]
|
|
598
582
|
);
|
|
599
583
|
|
|
600
|
-
const
|
|
584
|
+
const kRow = await dbGet<{ id: number }>("SELECT id FROM knowledge LIMIT 1");
|
|
585
|
+
const kid = kRow!.id;
|
|
601
586
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
587
|
+
const rows = await dbAll<{ id: number; acknowledged_by: string }>(
|
|
588
|
+
"SELECT id, acknowledged_by FROM knowledge WHERE acknowledged_by IS NOT NULL"
|
|
589
|
+
);
|
|
605
590
|
for (const row of rows) {
|
|
606
591
|
const taskIds = JSON.parse(row.acknowledged_by) as number[];
|
|
607
592
|
for (const taskId of taskIds) {
|
|
608
|
-
|
|
593
|
+
await dbRun(
|
|
594
|
+
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
595
|
+
[row.id, taskId]
|
|
596
|
+
);
|
|
609
597
|
}
|
|
610
598
|
}
|
|
611
599
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
)
|
|
616
|
-
expect(acks.map((a
|
|
600
|
+
const acks = await dbAll<{ task_id: number }>(
|
|
601
|
+
"SELECT task_id FROM knowledge_acknowledgments WHERE knowledge_id = ? ORDER BY task_id",
|
|
602
|
+
[kid]
|
|
603
|
+
);
|
|
604
|
+
expect(acks.map((a) => a.task_id)).toEqual([2, 3, 5]);
|
|
617
605
|
});
|
|
618
606
|
|
|
619
|
-
it("should find unacknowledged critical knowledge via NOT EXISTS", () => {
|
|
620
|
-
createKnowledgeTables(
|
|
621
|
-
|
|
607
|
+
it("should find unacknowledged critical knowledge via NOT EXISTS", async () => {
|
|
608
|
+
await createKnowledgeTables();
|
|
609
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
622
610
|
|
|
623
|
-
|
|
624
|
-
db.run(
|
|
611
|
+
await dbRun(
|
|
625
612
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
626
613
|
["SPEC-001", 1, "discovery", "Critical A", "critical"]
|
|
627
614
|
);
|
|
628
|
-
|
|
615
|
+
await dbRun(
|
|
629
616
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
630
617
|
["SPEC-001", 1, "discovery", "Critical B", "critical"]
|
|
631
618
|
);
|
|
632
619
|
|
|
633
|
-
const
|
|
620
|
+
const allK = await dbAll<{ id: number }>("SELECT id FROM knowledge ORDER BY id");
|
|
621
|
+
const kids = allK.map((r) => r.id);
|
|
634
622
|
|
|
635
|
-
|
|
636
|
-
db.run("INSERT INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kids[0], 2]);
|
|
623
|
+
await dbRun("INSERT INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kids[0], 2]);
|
|
637
624
|
|
|
638
|
-
|
|
639
|
-
const unacked = db.query(`
|
|
625
|
+
const unacked = await dbAll<any>(`
|
|
640
626
|
SELECT k.* FROM knowledge k
|
|
641
627
|
WHERE k.spec_id = ?
|
|
642
628
|
AND k.severity = 'critical'
|
|
@@ -645,21 +631,17 @@ describe("Migration System", () => {
|
|
|
645
631
|
SELECT 1 FROM knowledge_acknowledgments ka
|
|
646
632
|
WHERE ka.knowledge_id = k.id AND ka.task_id = ?
|
|
647
633
|
)
|
|
648
|
-
|
|
634
|
+
`, ["SPEC-001", 2, 2]);
|
|
649
635
|
|
|
650
636
|
expect(unacked).toHaveLength(1);
|
|
651
637
|
expect(unacked[0].content).toBe("Critical B");
|
|
652
638
|
});
|
|
653
639
|
});
|
|
654
640
|
|
|
655
|
-
// ═══════════════════════════════════════════════════════════════
|
|
656
|
-
// P1-2: Review Score
|
|
657
|
-
// ═══════════════════════════════════════════════════════════════
|
|
658
|
-
|
|
659
641
|
describe("calculateReviewScore()", () => {
|
|
660
|
-
function createReviewTables(
|
|
661
|
-
createBaseTables(
|
|
662
|
-
|
|
642
|
+
async function createReviewTables() {
|
|
643
|
+
await createBaseTables();
|
|
644
|
+
await dbExec(`
|
|
663
645
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
664
646
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
665
647
|
spec_id TEXT NOT NULL,
|
|
@@ -668,7 +650,9 @@ describe("Migration System", () => {
|
|
|
668
650
|
action TEXT NOT NULL,
|
|
669
651
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
670
652
|
UNIQUE(spec_id, path)
|
|
671
|
-
)
|
|
653
|
+
)
|
|
654
|
+
`);
|
|
655
|
+
await dbExec(`
|
|
672
656
|
CREATE TABLE IF NOT EXISTS gate_bypasses (
|
|
673
657
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
674
658
|
spec_id TEXT NOT NULL,
|
|
@@ -676,7 +660,9 @@ describe("Migration System", () => {
|
|
|
676
660
|
gate_name TEXT NOT NULL,
|
|
677
661
|
reason TEXT,
|
|
678
662
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
679
|
-
)
|
|
663
|
+
)
|
|
664
|
+
`);
|
|
665
|
+
await dbExec(`
|
|
680
666
|
CREATE TABLE IF NOT EXISTS review (
|
|
681
667
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
682
668
|
spec_id TEXT NOT NULL,
|
|
@@ -684,45 +670,50 @@ describe("Migration System", () => {
|
|
|
684
670
|
deviations TEXT,
|
|
685
671
|
status TEXT DEFAULT 'pending',
|
|
686
672
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
687
|
-
)
|
|
673
|
+
)
|
|
688
674
|
`);
|
|
689
675
|
}
|
|
690
676
|
|
|
691
|
-
function calcScore(
|
|
692
|
-
const
|
|
693
|
-
const
|
|
677
|
+
async function calcScore(specId: string) {
|
|
678
|
+
const totalRow = await dbGet<{ c: number }>("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?", [specId]);
|
|
679
|
+
const totalTasks = totalRow!.c;
|
|
680
|
+
const completedRow = await dbGet<{ c: number }>("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND status = 'done'", [specId]);
|
|
681
|
+
const completedTasks = completedRow!.c;
|
|
694
682
|
const tasksCompleted = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 25) : 25;
|
|
695
683
|
|
|
696
684
|
const totalGateEvents = totalTasks * 7;
|
|
697
|
-
const
|
|
685
|
+
const bypassRow = await dbGet<{ c: number }>("SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ?", [specId]);
|
|
686
|
+
const bypassCount = bypassRow!.c;
|
|
698
687
|
const cleanGateEvents = Math.max(0, totalGateEvents - bypassCount);
|
|
699
688
|
const gatesPassedClean = totalGateEvents > 0 ? Math.round((cleanGateEvents / totalGateEvents) * 25) : 25;
|
|
700
689
|
|
|
701
|
-
const plannedFiles =
|
|
690
|
+
const plannedFiles = await dbAll<{ files: string }>("SELECT files FROM tasks WHERE spec_id = ? AND files IS NOT NULL", [specId]);
|
|
702
691
|
const allPlannedFiles = new Set<string>();
|
|
703
692
|
for (const t of plannedFiles) {
|
|
704
693
|
try { for (const f of JSON.parse(t.files) as string[]) allPlannedFiles.add(f); } catch {}
|
|
705
694
|
}
|
|
706
|
-
const
|
|
695
|
+
const deliveredRows = await dbAll<{ path: string }>("SELECT DISTINCT path FROM artifacts WHERE spec_id = ?", [specId]);
|
|
696
|
+
const deliveredFiles = new Set(deliveredRows.map(a => a.path));
|
|
707
697
|
let filesDelivered: number;
|
|
708
698
|
if (allPlannedFiles.size === 0) { filesDelivered = deliveredFiles.size > 0 ? 25 : 0; }
|
|
709
699
|
else { let m = 0; for (const f of allPlannedFiles) { if (deliveredFiles.has(f)) m++; } filesDelivered = Math.round((m / allPlannedFiles.size) * 25); }
|
|
710
700
|
|
|
711
|
-
const
|
|
701
|
+
const standardsRow = await dbGet<{ c: number }>("SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ? AND gate_name = 'standards-follow'", [specId]);
|
|
702
|
+
const standardsBypasses = standardsRow!.c;
|
|
712
703
|
const standardsFollowed = totalTasks > 0 ? Math.round(((totalTasks - standardsBypasses) / totalTasks) * 25) : 25;
|
|
713
704
|
|
|
714
705
|
return { total: tasksCompleted + gatesPassedClean + filesDelivered + standardsFollowed, breakdown: { tasksCompleted, gatesPassedClean, filesDelivered, standardsFollowed } };
|
|
715
706
|
}
|
|
716
707
|
|
|
717
|
-
it("should return perfect score for clean implementation", () => {
|
|
718
|
-
createReviewTables(
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
708
|
+
it("should return perfect score for clean implementation", async () => {
|
|
709
|
+
await createReviewTables();
|
|
710
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
711
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status, files) VALUES (?, 1, 'Task 1', 'done', ?)", ["SPEC-001", '["src/a.ts"]']);
|
|
712
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status, files) VALUES (?, 2, 'Task 2', 'done', ?)", ["SPEC-001", '["src/b.ts"]']);
|
|
713
|
+
await dbRun("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, 1, 'src/a.ts', 'created')", ["SPEC-001"]);
|
|
714
|
+
await dbRun("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, 2, 'src/b.ts', 'created')", ["SPEC-001"]);
|
|
724
715
|
|
|
725
|
-
const score = calcScore(
|
|
716
|
+
const score = await calcScore("SPEC-001");
|
|
726
717
|
expect(score.total).toBe(100);
|
|
727
718
|
expect(score.breakdown.tasksCompleted).toBe(25);
|
|
728
719
|
expect(score.breakdown.gatesPassedClean).toBe(25);
|
|
@@ -730,48 +721,46 @@ describe("Migration System", () => {
|
|
|
730
721
|
expect(score.breakdown.standardsFollowed).toBe(25);
|
|
731
722
|
});
|
|
732
723
|
|
|
733
|
-
it("should penalize incomplete tasks", () => {
|
|
734
|
-
createReviewTables(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
724
|
+
it("should penalize incomplete tasks", async () => {
|
|
725
|
+
await createReviewTables();
|
|
726
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
727
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 1, 'Task 1', 'done')", ["SPEC-001"]);
|
|
728
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 2, 'Task 2', 'pending')", ["SPEC-001"]);
|
|
738
729
|
|
|
739
|
-
const score = calcScore(
|
|
740
|
-
expect(score.breakdown.tasksCompleted).toBe(13);
|
|
730
|
+
const score = await calcScore("SPEC-001");
|
|
731
|
+
expect(score.breakdown.tasksCompleted).toBe(13);
|
|
741
732
|
});
|
|
742
733
|
|
|
743
|
-
it("should penalize gate bypasses", () => {
|
|
744
|
-
createReviewTables(
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
734
|
+
it("should penalize gate bypasses", async () => {
|
|
735
|
+
await createReviewTables();
|
|
736
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
737
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 1, 'Task 1', 'done')", ["SPEC-001"]);
|
|
738
|
+
await dbRun("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, 1, 'standards-follow', 'test')", ["SPEC-001"]);
|
|
739
|
+
await dbRun("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, 1, 'dry-check', 'test')", ["SPEC-001"]);
|
|
749
740
|
|
|
750
|
-
const score = calcScore(
|
|
751
|
-
// 1 task * 7 gates = 7 events, 2 bypasses = 5/7 clean
|
|
741
|
+
const score = await calcScore("SPEC-001");
|
|
752
742
|
expect(score.breakdown.gatesPassedClean).toBe(Math.round(5 / 7 * 25));
|
|
753
743
|
});
|
|
754
744
|
|
|
755
|
-
it("should penalize missing files", () => {
|
|
756
|
-
createReviewTables(
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
// src/b.ts missing
|
|
745
|
+
it("should penalize missing files", async () => {
|
|
746
|
+
await createReviewTables();
|
|
747
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
748
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status, files) VALUES (?, 1, 'Task 1', 'done', ?)", ["SPEC-001", '["src/a.ts","src/b.ts"]']);
|
|
749
|
+
await dbRun("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, 1, 'src/a.ts', 'created')", ["SPEC-001"]);
|
|
761
750
|
|
|
762
|
-
const score = calcScore(
|
|
763
|
-
expect(score.breakdown.filesDelivered).toBe(13);
|
|
751
|
+
const score = await calcScore("SPEC-001");
|
|
752
|
+
expect(score.breakdown.filesDelivered).toBe(13);
|
|
764
753
|
});
|
|
765
754
|
|
|
766
|
-
it("should penalize standards bypasses specifically", () => {
|
|
767
|
-
createReviewTables(
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
755
|
+
it("should penalize standards bypasses specifically", async () => {
|
|
756
|
+
await createReviewTables();
|
|
757
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
758
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 1, 'Task 1', 'done')", ["SPEC-001"]);
|
|
759
|
+
await dbRun("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 2, 'Task 2', 'done')", ["SPEC-001"]);
|
|
760
|
+
await dbRun("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, 1, 'standards-follow', 'test')", ["SPEC-001"]);
|
|
772
761
|
|
|
773
|
-
const score = calcScore(
|
|
774
|
-
expect(score.breakdown.standardsFollowed).toBe(13);
|
|
762
|
+
const score = await calcScore("SPEC-001");
|
|
763
|
+
expect(score.breakdown.standardsFollowed).toBe(13);
|
|
775
764
|
});
|
|
776
765
|
});
|
|
777
766
|
});
|