@codexa/cli 9.0.30 → 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 +49 -47
- 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 +304 -298
- package/db/schema.ts +302 -392
- 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,
|
|
@@ -494,11 +477,10 @@ describe("Migration System", () => {
|
|
|
494
477
|
content TEXT NOT NULL,
|
|
495
478
|
severity TEXT DEFAULT 'info',
|
|
496
479
|
broadcast_to TEXT DEFAULT 'all',
|
|
497
|
-
acknowledged_by TEXT,
|
|
498
480
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
499
481
|
)
|
|
500
482
|
`);
|
|
501
|
-
|
|
483
|
+
await dbExec(`
|
|
502
484
|
CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
|
|
503
485
|
knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
504
486
|
task_id INTEGER NOT NULL,
|
|
@@ -506,118 +488,141 @@ describe("Migration System", () => {
|
|
|
506
488
|
PRIMARY KEY (knowledge_id, task_id)
|
|
507
489
|
)
|
|
508
490
|
`);
|
|
509
|
-
|
|
491
|
+
await dbExec("CREATE INDEX IF NOT EXISTS idx_ka_task ON knowledge_acknowledgments(task_id)");
|
|
510
492
|
}
|
|
511
493
|
|
|
512
|
-
it("migration 9.4.0 should create knowledge_acknowledgments table", () => {
|
|
513
|
-
createKnowledgeTables(
|
|
494
|
+
it("migration 9.4.0 should create knowledge_acknowledgments table", async () => {
|
|
495
|
+
await createKnowledgeTables();
|
|
514
496
|
|
|
515
|
-
const columns =
|
|
516
|
-
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);
|
|
517
499
|
expect(colNames).toContain("knowledge_id");
|
|
518
500
|
expect(colNames).toContain("task_id");
|
|
519
501
|
expect(colNames).toContain("acknowledged_at");
|
|
520
502
|
});
|
|
521
503
|
|
|
522
|
-
it("should insert and query acknowledgment", () => {
|
|
523
|
-
createKnowledgeTables(
|
|
524
|
-
|
|
525
|
-
|
|
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(
|
|
526
508
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
527
509
|
["SPEC-001", 1, "discovery", "Test knowledge", "critical"]
|
|
528
510
|
);
|
|
529
511
|
|
|
530
|
-
const
|
|
512
|
+
const kRow = await dbGet<{ id: number }>("SELECT id FROM knowledge LIMIT 1");
|
|
513
|
+
const kid = kRow!.id;
|
|
531
514
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
)
|
|
515
|
+
const before = await dbGet(
|
|
516
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?",
|
|
517
|
+
[kid, 2]
|
|
518
|
+
);
|
|
536
519
|
expect(before).toBeNull();
|
|
537
520
|
|
|
538
|
-
|
|
539
|
-
db.run(
|
|
521
|
+
await dbRun(
|
|
540
522
|
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
541
523
|
[kid, 2]
|
|
542
524
|
);
|
|
543
525
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
)
|
|
526
|
+
const after = await dbGet(
|
|
527
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?",
|
|
528
|
+
[kid, 2]
|
|
529
|
+
);
|
|
548
530
|
expect(after).not.toBeNull();
|
|
549
531
|
});
|
|
550
532
|
|
|
551
|
-
it("should be idempotent (INSERT OR IGNORE)", () => {
|
|
552
|
-
createKnowledgeTables(
|
|
553
|
-
|
|
554
|
-
|
|
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(
|
|
555
537
|
"INSERT INTO knowledge (spec_id, task_origin, category, content) VALUES (?, ?, ?, ?)",
|
|
556
538
|
["SPEC-001", 1, "discovery", "Test"]
|
|
557
539
|
);
|
|
558
540
|
|
|
559
|
-
const
|
|
541
|
+
const kRow = await dbGet<{ id: number }>("SELECT id FROM knowledge LIMIT 1");
|
|
542
|
+
const kid = kRow!.id;
|
|
560
543
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
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]);
|
|
564
546
|
|
|
565
|
-
const count = (
|
|
566
|
-
"SELECT COUNT(*) as c FROM knowledge_acknowledgments WHERE knowledge_id = ?"
|
|
567
|
-
|
|
568
|
-
|
|
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);
|
|
569
552
|
});
|
|
570
553
|
|
|
571
|
-
it("should migrate data from JSON acknowledged_by", () => {
|
|
572
|
-
|
|
573
|
-
|
|
554
|
+
it("should migrate data from JSON acknowledged_by", async () => {
|
|
555
|
+
await createBaseTables();
|
|
556
|
+
await dbExec(`
|
|
557
|
+
CREATE TABLE IF NOT EXISTS knowledge (
|
|
558
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
559
|
+
spec_id TEXT NOT NULL,
|
|
560
|
+
task_origin INTEGER NOT NULL,
|
|
561
|
+
category TEXT NOT NULL,
|
|
562
|
+
content TEXT NOT NULL,
|
|
563
|
+
severity TEXT DEFAULT 'info',
|
|
564
|
+
broadcast_to TEXT DEFAULT 'all',
|
|
565
|
+
acknowledged_by TEXT,
|
|
566
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
567
|
+
)
|
|
568
|
+
`);
|
|
569
|
+
await dbExec(`
|
|
570
|
+
CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
|
|
571
|
+
knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
572
|
+
task_id INTEGER NOT NULL,
|
|
573
|
+
acknowledged_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
574
|
+
PRIMARY KEY (knowledge_id, task_id)
|
|
575
|
+
)
|
|
576
|
+
`);
|
|
577
|
+
await dbRun("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
574
578
|
|
|
575
|
-
|
|
576
|
-
db.run(
|
|
579
|
+
await dbRun(
|
|
577
580
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, acknowledged_by) VALUES (?, ?, ?, ?, ?)",
|
|
578
581
|
["SPEC-001", 1, "discovery", "Test", JSON.stringify([2, 3, 5])]
|
|
579
582
|
);
|
|
580
583
|
|
|
581
|
-
const
|
|
584
|
+
const kRow = await dbGet<{ id: number }>("SELECT id FROM knowledge LIMIT 1");
|
|
585
|
+
const kid = kRow!.id;
|
|
582
586
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
587
|
+
const rows = await dbAll<{ id: number; acknowledged_by: string }>(
|
|
588
|
+
"SELECT id, acknowledged_by FROM knowledge WHERE acknowledged_by IS NOT NULL"
|
|
589
|
+
);
|
|
586
590
|
for (const row of rows) {
|
|
587
591
|
const taskIds = JSON.parse(row.acknowledged_by) as number[];
|
|
588
592
|
for (const taskId of taskIds) {
|
|
589
|
-
|
|
593
|
+
await dbRun(
|
|
594
|
+
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
595
|
+
[row.id, taskId]
|
|
596
|
+
);
|
|
590
597
|
}
|
|
591
598
|
}
|
|
592
599
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
)
|
|
597
|
-
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]);
|
|
598
605
|
});
|
|
599
606
|
|
|
600
|
-
it("should find unacknowledged critical knowledge via NOT EXISTS", () => {
|
|
601
|
-
createKnowledgeTables(
|
|
602
|
-
|
|
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')");
|
|
603
610
|
|
|
604
|
-
|
|
605
|
-
db.run(
|
|
611
|
+
await dbRun(
|
|
606
612
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
607
613
|
["SPEC-001", 1, "discovery", "Critical A", "critical"]
|
|
608
614
|
);
|
|
609
|
-
|
|
615
|
+
await dbRun(
|
|
610
616
|
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
611
617
|
["SPEC-001", 1, "discovery", "Critical B", "critical"]
|
|
612
618
|
);
|
|
613
619
|
|
|
614
|
-
const
|
|
620
|
+
const allK = await dbAll<{ id: number }>("SELECT id FROM knowledge ORDER BY id");
|
|
621
|
+
const kids = allK.map((r) => r.id);
|
|
615
622
|
|
|
616
|
-
|
|
617
|
-
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]);
|
|
618
624
|
|
|
619
|
-
|
|
620
|
-
const unacked = db.query(`
|
|
625
|
+
const unacked = await dbAll<any>(`
|
|
621
626
|
SELECT k.* FROM knowledge k
|
|
622
627
|
WHERE k.spec_id = ?
|
|
623
628
|
AND k.severity = 'critical'
|
|
@@ -626,21 +631,17 @@ describe("Migration System", () => {
|
|
|
626
631
|
SELECT 1 FROM knowledge_acknowledgments ka
|
|
627
632
|
WHERE ka.knowledge_id = k.id AND ka.task_id = ?
|
|
628
633
|
)
|
|
629
|
-
|
|
634
|
+
`, ["SPEC-001", 2, 2]);
|
|
630
635
|
|
|
631
636
|
expect(unacked).toHaveLength(1);
|
|
632
637
|
expect(unacked[0].content).toBe("Critical B");
|
|
633
638
|
});
|
|
634
639
|
});
|
|
635
640
|
|
|
636
|
-
// ═══════════════════════════════════════════════════════════════
|
|
637
|
-
// P1-2: Review Score
|
|
638
|
-
// ═══════════════════════════════════════════════════════════════
|
|
639
|
-
|
|
640
641
|
describe("calculateReviewScore()", () => {
|
|
641
|
-
function createReviewTables(
|
|
642
|
-
createBaseTables(
|
|
643
|
-
|
|
642
|
+
async function createReviewTables() {
|
|
643
|
+
await createBaseTables();
|
|
644
|
+
await dbExec(`
|
|
644
645
|
CREATE TABLE IF NOT EXISTS artifacts (
|
|
645
646
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
646
647
|
spec_id TEXT NOT NULL,
|
|
@@ -649,7 +650,9 @@ describe("Migration System", () => {
|
|
|
649
650
|
action TEXT NOT NULL,
|
|
650
651
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
651
652
|
UNIQUE(spec_id, path)
|
|
652
|
-
)
|
|
653
|
+
)
|
|
654
|
+
`);
|
|
655
|
+
await dbExec(`
|
|
653
656
|
CREATE TABLE IF NOT EXISTS gate_bypasses (
|
|
654
657
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
655
658
|
spec_id TEXT NOT NULL,
|
|
@@ -657,55 +660,60 @@ describe("Migration System", () => {
|
|
|
657
660
|
gate_name TEXT NOT NULL,
|
|
658
661
|
reason TEXT,
|
|
659
662
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
660
|
-
)
|
|
663
|
+
)
|
|
664
|
+
`);
|
|
665
|
+
await dbExec(`
|
|
661
666
|
CREATE TABLE IF NOT EXISTS review (
|
|
662
667
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
663
668
|
spec_id TEXT NOT NULL,
|
|
664
669
|
planned_vs_done TEXT,
|
|
665
670
|
deviations TEXT,
|
|
666
|
-
pattern_violations TEXT,
|
|
667
671
|
status TEXT DEFAULT 'pending',
|
|
668
|
-
resolution TEXT,
|
|
669
672
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
670
|
-
)
|
|
673
|
+
)
|
|
671
674
|
`);
|
|
672
675
|
}
|
|
673
676
|
|
|
674
|
-
function calcScore(
|
|
675
|
-
const
|
|
676
|
-
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;
|
|
677
682
|
const tasksCompleted = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 25) : 25;
|
|
678
683
|
|
|
679
684
|
const totalGateEvents = totalTasks * 7;
|
|
680
|
-
const
|
|
685
|
+
const bypassRow = await dbGet<{ c: number }>("SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ?", [specId]);
|
|
686
|
+
const bypassCount = bypassRow!.c;
|
|
681
687
|
const cleanGateEvents = Math.max(0, totalGateEvents - bypassCount);
|
|
682
688
|
const gatesPassedClean = totalGateEvents > 0 ? Math.round((cleanGateEvents / totalGateEvents) * 25) : 25;
|
|
683
689
|
|
|
684
|
-
const plannedFiles =
|
|
690
|
+
const plannedFiles = await dbAll<{ files: string }>("SELECT files FROM tasks WHERE spec_id = ? AND files IS NOT NULL", [specId]);
|
|
685
691
|
const allPlannedFiles = new Set<string>();
|
|
686
692
|
for (const t of plannedFiles) {
|
|
687
693
|
try { for (const f of JSON.parse(t.files) as string[]) allPlannedFiles.add(f); } catch {}
|
|
688
694
|
}
|
|
689
|
-
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));
|
|
690
697
|
let filesDelivered: number;
|
|
691
698
|
if (allPlannedFiles.size === 0) { filesDelivered = deliveredFiles.size > 0 ? 25 : 0; }
|
|
692
699
|
else { let m = 0; for (const f of allPlannedFiles) { if (deliveredFiles.has(f)) m++; } filesDelivered = Math.round((m / allPlannedFiles.size) * 25); }
|
|
693
700
|
|
|
694
|
-
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;
|
|
695
703
|
const standardsFollowed = totalTasks > 0 ? Math.round(((totalTasks - standardsBypasses) / totalTasks) * 25) : 25;
|
|
696
704
|
|
|
697
705
|
return { total: tasksCompleted + gatesPassedClean + filesDelivered + standardsFollowed, breakdown: { tasksCompleted, gatesPassedClean, filesDelivered, standardsFollowed } };
|
|
698
706
|
}
|
|
699
707
|
|
|
700
|
-
it("should return perfect score for clean implementation", () => {
|
|
701
|
-
createReviewTables(
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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"]);
|
|
707
715
|
|
|
708
|
-
const score = calcScore(
|
|
716
|
+
const score = await calcScore("SPEC-001");
|
|
709
717
|
expect(score.total).toBe(100);
|
|
710
718
|
expect(score.breakdown.tasksCompleted).toBe(25);
|
|
711
719
|
expect(score.breakdown.gatesPassedClean).toBe(25);
|
|
@@ -713,48 +721,46 @@ describe("Migration System", () => {
|
|
|
713
721
|
expect(score.breakdown.standardsFollowed).toBe(25);
|
|
714
722
|
});
|
|
715
723
|
|
|
716
|
-
it("should penalize incomplete tasks", () => {
|
|
717
|
-
createReviewTables(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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"]);
|
|
721
729
|
|
|
722
|
-
const score = calcScore(
|
|
723
|
-
expect(score.breakdown.tasksCompleted).toBe(13);
|
|
730
|
+
const score = await calcScore("SPEC-001");
|
|
731
|
+
expect(score.breakdown.tasksCompleted).toBe(13);
|
|
724
732
|
});
|
|
725
733
|
|
|
726
|
-
it("should penalize gate bypasses", () => {
|
|
727
|
-
createReviewTables(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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"]);
|
|
732
740
|
|
|
733
|
-
const score = calcScore(
|
|
734
|
-
// 1 task * 7 gates = 7 events, 2 bypasses = 5/7 clean
|
|
741
|
+
const score = await calcScore("SPEC-001");
|
|
735
742
|
expect(score.breakdown.gatesPassedClean).toBe(Math.round(5 / 7 * 25));
|
|
736
743
|
});
|
|
737
744
|
|
|
738
|
-
it("should penalize missing files", () => {
|
|
739
|
-
createReviewTables(
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
// 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"]);
|
|
744
750
|
|
|
745
|
-
const score = calcScore(
|
|
746
|
-
expect(score.breakdown.filesDelivered).toBe(13);
|
|
751
|
+
const score = await calcScore("SPEC-001");
|
|
752
|
+
expect(score.breakdown.filesDelivered).toBe(13);
|
|
747
753
|
});
|
|
748
754
|
|
|
749
|
-
it("should penalize standards bypasses specifically", () => {
|
|
750
|
-
createReviewTables(
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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"]);
|
|
755
761
|
|
|
756
|
-
const score = calcScore(
|
|
757
|
-
expect(score.breakdown.standardsFollowed).toBe(13);
|
|
762
|
+
const score = await calcScore("SPEC-001");
|
|
763
|
+
expect(score.breakdown.standardsFollowed).toBe(13);
|
|
758
764
|
});
|
|
759
765
|
});
|
|
760
766
|
});
|