@codexa/cli 9.0.3 → 9.0.4
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 +7 -6
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -0
- package/commands/decide.ts +9 -21
- package/commands/discover.ts +11 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +190 -75
- package/commands/patterns.ts +6 -13
- package/commands/plan.ts +14 -64
- package/commands/product.ts +8 -17
- package/commands/research.ts +4 -3
- package/commands/review.ts +190 -28
- package/commands/spec-resolver.test.ts +119 -0
- package/commands/spec-resolver.ts +90 -0
- package/commands/standards.ts +7 -15
- package/commands/sync.ts +2 -3
- package/commands/task.ts +30 -9
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -708
- package/db/schema.test.ts +475 -48
- package/db/schema.ts +136 -12
- package/gates/validator.test.ts +58 -0
- package/gates/validator.ts +83 -30
- package/package.json +1 -1
- package/workflow.ts +102 -51
package/db/schema.test.ts
CHANGED
|
@@ -209,63 +209,37 @@ describe("Migration System", () => {
|
|
|
209
209
|
// ═══════════════════════════════════════════════════════════════
|
|
210
210
|
|
|
211
211
|
describe("getNextDecisionId()", () => {
|
|
212
|
+
// Reimplemented: uses timestamp+random instead of sequential count
|
|
212
213
|
function getNextDecisionId(specId: string): string {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const nextNum = (result?.max_num || 0) + 1;
|
|
219
|
-
return `DEC-${nextNum.toString().padStart(3, "0")}`;
|
|
214
|
+
const slug = specId.split("-").slice(1, 3).join("-");
|
|
215
|
+
const ts = Date.now().toString(36);
|
|
216
|
+
const rand = Math.random().toString(36).substring(2, 6);
|
|
217
|
+
return `DEC-${slug}-${ts}-${rand}`;
|
|
220
218
|
}
|
|
221
219
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it("should return DEC-001 for empty decisions", () => {
|
|
228
|
-
expect(getNextDecisionId("SPEC-001")).toBe("DEC-001");
|
|
220
|
+
it("should start with DEC- prefix and contain spec slug", () => {
|
|
221
|
+
const id = getNextDecisionId("SPEC-001");
|
|
222
|
+
expect(id.startsWith("DEC-001-")).toBe(true);
|
|
229
223
|
});
|
|
230
224
|
|
|
231
|
-
it("should
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
["DEC-001", "SPEC-001", "test", "test", now]
|
|
236
|
-
);
|
|
237
|
-
expect(getNextDecisionId("SPEC-001")).toBe("DEC-002");
|
|
238
|
-
|
|
239
|
-
db.run(
|
|
240
|
-
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
241
|
-
["DEC-002", "SPEC-001", "test2", "test2", now]
|
|
242
|
-
);
|
|
243
|
-
expect(getNextDecisionId("SPEC-001")).toBe("DEC-003");
|
|
225
|
+
it("should generate unique IDs on consecutive calls", () => {
|
|
226
|
+
const id1 = getNextDecisionId("SPEC-001");
|
|
227
|
+
const id2 = getNextDecisionId("SPEC-001");
|
|
228
|
+
expect(id1).not.toBe(id2);
|
|
244
229
|
});
|
|
245
230
|
|
|
246
|
-
it("should
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
250
|
-
["DEC-001", "SPEC-001", "test", "test", now]
|
|
251
|
-
);
|
|
252
|
-
db.run(
|
|
253
|
-
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
254
|
-
["DEC-003", "SPEC-001", "test3", "test3", now]
|
|
255
|
-
);
|
|
256
|
-
expect(getNextDecisionId("SPEC-001")).toBe("DEC-004");
|
|
231
|
+
it("should include slug from multi-part spec IDs", () => {
|
|
232
|
+
const id = getNextDecisionId("2026-02-11-feature-name");
|
|
233
|
+
expect(id.startsWith("DEC-02-11-")).toBe(true);
|
|
257
234
|
});
|
|
258
235
|
|
|
259
|
-
it("should
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
);
|
|
266
|
-
|
|
267
|
-
expect(getNextDecisionId("SPEC-001")).toBe("DEC-006");
|
|
268
|
-
expect(getNextDecisionId("SPEC-002")).toBe("DEC-001");
|
|
236
|
+
it("should not require DB access (no race condition)", () => {
|
|
237
|
+
// Generate 100 IDs rapidly — all should be unique
|
|
238
|
+
const ids = new Set<string>();
|
|
239
|
+
for (let i = 0; i < 100; i++) {
|
|
240
|
+
ids.add(getNextDecisionId("SPEC-001"));
|
|
241
|
+
}
|
|
242
|
+
expect(ids.size).toBe(100);
|
|
269
243
|
});
|
|
270
244
|
});
|
|
271
245
|
|
|
@@ -330,4 +304,457 @@ describe("Migration System", () => {
|
|
|
330
304
|
expect(claim2).toBe(false);
|
|
331
305
|
});
|
|
332
306
|
});
|
|
307
|
+
|
|
308
|
+
// ═══════════════════════════════════════════════════════════════
|
|
309
|
+
// v9.3: Agent Performance (P3.2)
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════
|
|
311
|
+
|
|
312
|
+
describe("agent_performance", () => {
|
|
313
|
+
function createPerformanceTables(db: Database) {
|
|
314
|
+
createBaseTables(db);
|
|
315
|
+
db.exec(`
|
|
316
|
+
CREATE TABLE IF NOT EXISTS agent_performance (
|
|
317
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
318
|
+
agent_type TEXT NOT NULL,
|
|
319
|
+
spec_id TEXT NOT NULL,
|
|
320
|
+
task_id INTEGER NOT NULL,
|
|
321
|
+
gates_passed_first_try INTEGER DEFAULT 0,
|
|
322
|
+
gates_total INTEGER DEFAULT 0,
|
|
323
|
+
bypasses_used INTEGER DEFAULT 0,
|
|
324
|
+
files_created INTEGER DEFAULT 0,
|
|
325
|
+
files_modified INTEGER DEFAULT 0,
|
|
326
|
+
context_size_bytes INTEGER DEFAULT 0,
|
|
327
|
+
execution_duration_ms INTEGER DEFAULT 0,
|
|
328
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
329
|
+
)
|
|
330
|
+
`);
|
|
331
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_perf_type ON agent_performance(agent_type)`);
|
|
332
|
+
|
|
333
|
+
db.exec(`
|
|
334
|
+
CREATE TABLE IF NOT EXISTS gate_bypasses (
|
|
335
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
336
|
+
spec_id TEXT,
|
|
337
|
+
task_id INTEGER,
|
|
338
|
+
gate_name TEXT NOT NULL,
|
|
339
|
+
reason TEXT,
|
|
340
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
341
|
+
)
|
|
342
|
+
`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
it("migration 9.3.0 should create agent_performance table", () => {
|
|
346
|
+
createPerformanceTables(db);
|
|
347
|
+
|
|
348
|
+
const columns = db.query("PRAGMA table_info(agent_performance)").all() as any[];
|
|
349
|
+
const colNames = columns.map((c: any) => c.name);
|
|
350
|
+
expect(colNames).toContain("agent_type");
|
|
351
|
+
expect(colNames).toContain("spec_id");
|
|
352
|
+
expect(colNames).toContain("task_id");
|
|
353
|
+
expect(colNames).toContain("gates_passed_first_try");
|
|
354
|
+
expect(colNames).toContain("gates_total");
|
|
355
|
+
expect(colNames).toContain("bypasses_used");
|
|
356
|
+
expect(colNames).toContain("execution_duration_ms");
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("should insert and retrieve performance data", () => {
|
|
360
|
+
createPerformanceTables(db);
|
|
361
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
362
|
+
db.run(
|
|
363
|
+
"INSERT INTO tasks (spec_id, number, name, agent, status) VALUES (?, ?, ?, ?, ?)",
|
|
364
|
+
["SPEC-001", 1, "Test task", "frontend-next", "done"]
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
const now = new Date().toISOString();
|
|
368
|
+
db.run(
|
|
369
|
+
`INSERT INTO agent_performance
|
|
370
|
+
(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
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
372
|
+
["frontend-next", "SPEC-001", 1, 7, 7, 0, 3, 1, 4096, 15000, now]
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
const rows = db.query("SELECT * FROM agent_performance WHERE agent_type = ?").all("frontend-next") as any[];
|
|
376
|
+
expect(rows).toHaveLength(1);
|
|
377
|
+
expect(rows[0].gates_passed_first_try).toBe(7);
|
|
378
|
+
expect(rows[0].bypasses_used).toBe(0);
|
|
379
|
+
expect(rows[0].execution_duration_ms).toBe(15000);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("should compute hints: no data returns empty", () => {
|
|
383
|
+
createPerformanceTables(db);
|
|
384
|
+
|
|
385
|
+
const recent = db.query(
|
|
386
|
+
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5"
|
|
387
|
+
).all("nonexistent") as any[];
|
|
388
|
+
expect(recent).toHaveLength(0);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("should detect high bypass rate from performance data", () => {
|
|
392
|
+
createPerformanceTables(db);
|
|
393
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
394
|
+
|
|
395
|
+
const now = new Date().toISOString();
|
|
396
|
+
// Insert 3 records with bypasses
|
|
397
|
+
for (let i = 1; i <= 3; i++) {
|
|
398
|
+
db.run(
|
|
399
|
+
`INSERT INTO agent_performance
|
|
400
|
+
(agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, created_at)
|
|
401
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
402
|
+
["backend-go", "SPEC-001", i, 5, 7, 2, now]
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const recent = db.query(
|
|
407
|
+
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5"
|
|
408
|
+
).all("backend-go") as any[];
|
|
409
|
+
|
|
410
|
+
const avgBypass = recent.reduce((sum: number, r: any) => sum + r.bypasses_used, 0) / recent.length;
|
|
411
|
+
expect(avgBypass).toBe(2);
|
|
412
|
+
expect(avgBypass > 0.5).toBe(true);
|
|
413
|
+
|
|
414
|
+
const avgGateRate = recent.reduce((sum: number, r: any) => {
|
|
415
|
+
return sum + (r.gates_total > 0 ? r.gates_passed_first_try / r.gates_total : 1);
|
|
416
|
+
}, 0) / recent.length;
|
|
417
|
+
expect(avgGateRate).toBeCloseTo(5 / 7, 2);
|
|
418
|
+
expect(avgGateRate < 0.7).toBe(false); // 5/7 ≈ 0.71, just above threshold
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("should detect low gate pass rate", () => {
|
|
422
|
+
createPerformanceTables(db);
|
|
423
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
424
|
+
|
|
425
|
+
const now = new Date().toISOString();
|
|
426
|
+
for (let i = 1; i <= 3; i++) {
|
|
427
|
+
db.run(
|
|
428
|
+
`INSERT INTO agent_performance
|
|
429
|
+
(agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, created_at)
|
|
430
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
431
|
+
["backend-csharp", "SPEC-001", i, 3, 7, 4, now]
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const recent = db.query(
|
|
436
|
+
"SELECT * FROM agent_performance WHERE agent_type = ? ORDER BY created_at DESC LIMIT 5"
|
|
437
|
+
).all("backend-csharp") as any[];
|
|
438
|
+
|
|
439
|
+
const avgGateRate = recent.reduce((sum: number, r: any) => {
|
|
440
|
+
return sum + (r.gates_total > 0 ? r.gates_passed_first_try / r.gates_total : 1);
|
|
441
|
+
}, 0) / recent.length;
|
|
442
|
+
expect(avgGateRate).toBeCloseTo(3 / 7, 2);
|
|
443
|
+
expect(avgGateRate < 0.7).toBe(true); // 3/7 ≈ 0.43 — should trigger hint
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("should track frequent gate bypass types", () => {
|
|
447
|
+
createPerformanceTables(db);
|
|
448
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
449
|
+
db.run(
|
|
450
|
+
"INSERT INTO tasks (spec_id, number, name, agent, status) VALUES (?, ?, ?, ?, ?)",
|
|
451
|
+
["SPEC-001", 1, "Task 1", "frontend-next", "done"]
|
|
452
|
+
);
|
|
453
|
+
db.run(
|
|
454
|
+
"INSERT INTO tasks (spec_id, number, name, agent, status) VALUES (?, ?, ?, ?, ?)",
|
|
455
|
+
["SPEC-001", 2, "Task 2", "frontend-next", "done"]
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const taskId1 = (db.query("SELECT id FROM tasks WHERE number = 1").get() as any).id;
|
|
459
|
+
const taskId2 = (db.query("SELECT id FROM tasks WHERE number = 2").get() as any).id;
|
|
460
|
+
|
|
461
|
+
// Add bypasses for same gate
|
|
462
|
+
db.run("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, ?, ?, ?)",
|
|
463
|
+
["SPEC-001", taskId1, "standards-follow", "test"]);
|
|
464
|
+
db.run("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, ?, ?, ?)",
|
|
465
|
+
["SPEC-001", taskId2, "standards-follow", "test"]);
|
|
466
|
+
|
|
467
|
+
const bypassTypes = db.query(
|
|
468
|
+
`SELECT gb.gate_name, COUNT(*) as cnt FROM gate_bypasses gb
|
|
469
|
+
JOIN tasks t ON gb.task_id = t.id
|
|
470
|
+
WHERE t.agent = ?
|
|
471
|
+
GROUP BY gb.gate_name
|
|
472
|
+
ORDER BY cnt DESC LIMIT 3`
|
|
473
|
+
).all("frontend-next") as any[];
|
|
474
|
+
|
|
475
|
+
expect(bypassTypes).toHaveLength(1);
|
|
476
|
+
expect(bypassTypes[0].gate_name).toBe("standards-follow");
|
|
477
|
+
expect(bypassTypes[0].cnt).toBe(2);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// ═══════════════════════════════════════════════════════════════
|
|
482
|
+
// v9.4: Knowledge Acknowledgments (P1-5)
|
|
483
|
+
// ═══════════════════════════════════════════════════════════════
|
|
484
|
+
|
|
485
|
+
describe("knowledge_acknowledgments", () => {
|
|
486
|
+
function createKnowledgeTables(db: Database) {
|
|
487
|
+
createBaseTables(db);
|
|
488
|
+
db.exec(`
|
|
489
|
+
CREATE TABLE IF NOT EXISTS knowledge (
|
|
490
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
491
|
+
spec_id TEXT NOT NULL,
|
|
492
|
+
task_origin INTEGER NOT NULL,
|
|
493
|
+
category TEXT NOT NULL,
|
|
494
|
+
content TEXT NOT NULL,
|
|
495
|
+
severity TEXT DEFAULT 'info',
|
|
496
|
+
broadcast_to TEXT DEFAULT 'all',
|
|
497
|
+
acknowledged_by TEXT,
|
|
498
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
499
|
+
)
|
|
500
|
+
`);
|
|
501
|
+
db.exec(`
|
|
502
|
+
CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
|
|
503
|
+
knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
|
|
504
|
+
task_id INTEGER NOT NULL,
|
|
505
|
+
acknowledged_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
506
|
+
PRIMARY KEY (knowledge_id, task_id)
|
|
507
|
+
)
|
|
508
|
+
`);
|
|
509
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_ka_task ON knowledge_acknowledgments(task_id)`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
it("migration 9.4.0 should create knowledge_acknowledgments table", () => {
|
|
513
|
+
createKnowledgeTables(db);
|
|
514
|
+
|
|
515
|
+
const columns = db.query("PRAGMA table_info(knowledge_acknowledgments)").all() as any[];
|
|
516
|
+
const colNames = columns.map((c: any) => c.name);
|
|
517
|
+
expect(colNames).toContain("knowledge_id");
|
|
518
|
+
expect(colNames).toContain("task_id");
|
|
519
|
+
expect(colNames).toContain("acknowledged_at");
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("should insert and query acknowledgment", () => {
|
|
523
|
+
createKnowledgeTables(db);
|
|
524
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
525
|
+
db.run(
|
|
526
|
+
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
527
|
+
["SPEC-001", 1, "discovery", "Test knowledge", "critical"]
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const kid = (db.query("SELECT id FROM knowledge LIMIT 1").get() as any).id;
|
|
531
|
+
|
|
532
|
+
// Not acknowledged yet
|
|
533
|
+
const before = db.query(
|
|
534
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
|
|
535
|
+
).get(kid, 2);
|
|
536
|
+
expect(before).toBeNull();
|
|
537
|
+
|
|
538
|
+
// Acknowledge
|
|
539
|
+
db.run(
|
|
540
|
+
"INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
|
|
541
|
+
[kid, 2]
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
// Now acknowledged
|
|
545
|
+
const after = db.query(
|
|
546
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
|
|
547
|
+
).get(kid, 2);
|
|
548
|
+
expect(after).not.toBeNull();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("should be idempotent (INSERT OR IGNORE)", () => {
|
|
552
|
+
createKnowledgeTables(db);
|
|
553
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
554
|
+
db.run(
|
|
555
|
+
"INSERT INTO knowledge (spec_id, task_origin, category, content) VALUES (?, ?, ?, ?)",
|
|
556
|
+
["SPEC-001", 1, "discovery", "Test"]
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
const kid = (db.query("SELECT id FROM knowledge LIMIT 1").get() as any).id;
|
|
560
|
+
|
|
561
|
+
// Insert twice — no error
|
|
562
|
+
db.run("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kid, 2]);
|
|
563
|
+
db.run("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kid, 2]);
|
|
564
|
+
|
|
565
|
+
const count = (db.query(
|
|
566
|
+
"SELECT COUNT(*) as c FROM knowledge_acknowledgments WHERE knowledge_id = ?"
|
|
567
|
+
).get(kid) as any).c;
|
|
568
|
+
expect(count).toBe(1);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it("should migrate data from JSON acknowledged_by", () => {
|
|
572
|
+
createKnowledgeTables(db);
|
|
573
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
574
|
+
|
|
575
|
+
// Insert with old JSON format
|
|
576
|
+
db.run(
|
|
577
|
+
"INSERT INTO knowledge (spec_id, task_origin, category, content, acknowledged_by) VALUES (?, ?, ?, ?, ?)",
|
|
578
|
+
["SPEC-001", 1, "discovery", "Test", JSON.stringify([2, 3, 5])]
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
const kid = (db.query("SELECT id FROM knowledge LIMIT 1").get() as any).id;
|
|
582
|
+
|
|
583
|
+
// Simulate migration
|
|
584
|
+
const rows = db.query("SELECT id, acknowledged_by FROM knowledge WHERE acknowledged_by IS NOT NULL").all() as any[];
|
|
585
|
+
const insert = db.prepare("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)");
|
|
586
|
+
for (const row of rows) {
|
|
587
|
+
const taskIds = JSON.parse(row.acknowledged_by) as number[];
|
|
588
|
+
for (const taskId of taskIds) {
|
|
589
|
+
insert.run(row.id, taskId);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Verify migrated data
|
|
594
|
+
const acks = db.query(
|
|
595
|
+
"SELECT task_id FROM knowledge_acknowledgments WHERE knowledge_id = ? ORDER BY task_id"
|
|
596
|
+
).all(kid) as any[];
|
|
597
|
+
expect(acks.map((a: any) => a.task_id)).toEqual([2, 3, 5]);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
it("should find unacknowledged critical knowledge via NOT EXISTS", () => {
|
|
601
|
+
createKnowledgeTables(db);
|
|
602
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
603
|
+
|
|
604
|
+
// Insert critical knowledge from task 1
|
|
605
|
+
db.run(
|
|
606
|
+
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
607
|
+
["SPEC-001", 1, "discovery", "Critical A", "critical"]
|
|
608
|
+
);
|
|
609
|
+
db.run(
|
|
610
|
+
"INSERT INTO knowledge (spec_id, task_origin, category, content, severity) VALUES (?, ?, ?, ?, ?)",
|
|
611
|
+
["SPEC-001", 1, "discovery", "Critical B", "critical"]
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
const kids = (db.query("SELECT id FROM knowledge ORDER BY id").all() as any[]).map((r: any) => r.id);
|
|
615
|
+
|
|
616
|
+
// Acknowledge only the first one by task 2
|
|
617
|
+
db.run("INSERT INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)", [kids[0], 2]);
|
|
618
|
+
|
|
619
|
+
// Query unacknowledged for task 2
|
|
620
|
+
const unacked = db.query(`
|
|
621
|
+
SELECT k.* FROM knowledge k
|
|
622
|
+
WHERE k.spec_id = ?
|
|
623
|
+
AND k.severity = 'critical'
|
|
624
|
+
AND k.task_origin != ?
|
|
625
|
+
AND NOT EXISTS (
|
|
626
|
+
SELECT 1 FROM knowledge_acknowledgments ka
|
|
627
|
+
WHERE ka.knowledge_id = k.id AND ka.task_id = ?
|
|
628
|
+
)
|
|
629
|
+
`).all("SPEC-001", 2, 2) as any[];
|
|
630
|
+
|
|
631
|
+
expect(unacked).toHaveLength(1);
|
|
632
|
+
expect(unacked[0].content).toBe("Critical B");
|
|
633
|
+
});
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// ═══════════════════════════════════════════════════════════════
|
|
637
|
+
// P1-2: Review Score
|
|
638
|
+
// ═══════════════════════════════════════════════════════════════
|
|
639
|
+
|
|
640
|
+
describe("calculateReviewScore()", () => {
|
|
641
|
+
function createReviewTables(db: Database) {
|
|
642
|
+
createBaseTables(db);
|
|
643
|
+
db.exec(`
|
|
644
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
645
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
646
|
+
spec_id TEXT NOT NULL,
|
|
647
|
+
task_ref INTEGER NOT NULL,
|
|
648
|
+
path TEXT NOT NULL,
|
|
649
|
+
action TEXT NOT NULL,
|
|
650
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
651
|
+
UNIQUE(spec_id, path)
|
|
652
|
+
);
|
|
653
|
+
CREATE TABLE IF NOT EXISTS gate_bypasses (
|
|
654
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
655
|
+
spec_id TEXT NOT NULL,
|
|
656
|
+
task_id INTEGER NOT NULL,
|
|
657
|
+
gate_name TEXT NOT NULL,
|
|
658
|
+
reason TEXT,
|
|
659
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
660
|
+
);
|
|
661
|
+
CREATE TABLE IF NOT EXISTS review (
|
|
662
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
663
|
+
spec_id TEXT NOT NULL,
|
|
664
|
+
planned_vs_done TEXT,
|
|
665
|
+
deviations TEXT,
|
|
666
|
+
pattern_violations TEXT,
|
|
667
|
+
status TEXT DEFAULT 'pending',
|
|
668
|
+
resolution TEXT,
|
|
669
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
670
|
+
);
|
|
671
|
+
`);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function calcScore(db: Database, specId: string) {
|
|
675
|
+
const totalTasks = (db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?").get(specId) as any).c;
|
|
676
|
+
const completedTasks = (db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ? AND status = 'done'").get(specId) as any).c;
|
|
677
|
+
const tasksCompleted = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 25) : 25;
|
|
678
|
+
|
|
679
|
+
const totalGateEvents = totalTasks * 7;
|
|
680
|
+
const bypassCount = (db.query("SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ?").get(specId) as any).c;
|
|
681
|
+
const cleanGateEvents = Math.max(0, totalGateEvents - bypassCount);
|
|
682
|
+
const gatesPassedClean = totalGateEvents > 0 ? Math.round((cleanGateEvents / totalGateEvents) * 25) : 25;
|
|
683
|
+
|
|
684
|
+
const plannedFiles = db.query("SELECT files FROM tasks WHERE spec_id = ? AND files IS NOT NULL").all(specId) as any[];
|
|
685
|
+
const allPlannedFiles = new Set<string>();
|
|
686
|
+
for (const t of plannedFiles) {
|
|
687
|
+
try { for (const f of JSON.parse(t.files) as string[]) allPlannedFiles.add(f); } catch {}
|
|
688
|
+
}
|
|
689
|
+
const deliveredFiles = new Set((db.query("SELECT DISTINCT path FROM artifacts WHERE spec_id = ?").all(specId) as any[]).map(a => a.path));
|
|
690
|
+
let filesDelivered: number;
|
|
691
|
+
if (allPlannedFiles.size === 0) { filesDelivered = deliveredFiles.size > 0 ? 25 : 0; }
|
|
692
|
+
else { let m = 0; for (const f of allPlannedFiles) { if (deliveredFiles.has(f)) m++; } filesDelivered = Math.round((m / allPlannedFiles.size) * 25); }
|
|
693
|
+
|
|
694
|
+
const standardsBypasses = (db.query("SELECT COUNT(*) as c FROM gate_bypasses WHERE spec_id = ? AND gate_name = 'standards-follow'").get(specId) as any).c;
|
|
695
|
+
const standardsFollowed = totalTasks > 0 ? Math.round(((totalTasks - standardsBypasses) / totalTasks) * 25) : 25;
|
|
696
|
+
|
|
697
|
+
return { total: tasksCompleted + gatesPassedClean + filesDelivered + standardsFollowed, breakdown: { tasksCompleted, gatesPassedClean, filesDelivered, standardsFollowed } };
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
it("should return perfect score for clean implementation", () => {
|
|
701
|
+
createReviewTables(db);
|
|
702
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
703
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status, files) VALUES (?, 1, 'Task 1', 'done', ?)", ["SPEC-001", '["src/a.ts"]']);
|
|
704
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status, files) VALUES (?, 2, 'Task 2', 'done', ?)", ["SPEC-001", '["src/b.ts"]']);
|
|
705
|
+
db.run("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, 1, 'src/a.ts', 'created')", ["SPEC-001"]);
|
|
706
|
+
db.run("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, 2, 'src/b.ts', 'created')", ["SPEC-001"]);
|
|
707
|
+
|
|
708
|
+
const score = calcScore(db, "SPEC-001");
|
|
709
|
+
expect(score.total).toBe(100);
|
|
710
|
+
expect(score.breakdown.tasksCompleted).toBe(25);
|
|
711
|
+
expect(score.breakdown.gatesPassedClean).toBe(25);
|
|
712
|
+
expect(score.breakdown.filesDelivered).toBe(25);
|
|
713
|
+
expect(score.breakdown.standardsFollowed).toBe(25);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it("should penalize incomplete tasks", () => {
|
|
717
|
+
createReviewTables(db);
|
|
718
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
719
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 1, 'Task 1', 'done')", ["SPEC-001"]);
|
|
720
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 2, 'Task 2', 'pending')", ["SPEC-001"]);
|
|
721
|
+
|
|
722
|
+
const score = calcScore(db, "SPEC-001");
|
|
723
|
+
expect(score.breakdown.tasksCompleted).toBe(13); // round(1/2 * 25) = 13
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("should penalize gate bypasses", () => {
|
|
727
|
+
createReviewTables(db);
|
|
728
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
729
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 1, 'Task 1', 'done')", ["SPEC-001"]);
|
|
730
|
+
db.run("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, 1, 'standards-follow', 'test')", ["SPEC-001"]);
|
|
731
|
+
db.run("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, 1, 'dry-check', 'test')", ["SPEC-001"]);
|
|
732
|
+
|
|
733
|
+
const score = calcScore(db, "SPEC-001");
|
|
734
|
+
// 1 task * 7 gates = 7 events, 2 bypasses = 5/7 clean
|
|
735
|
+
expect(score.breakdown.gatesPassedClean).toBe(Math.round(5 / 7 * 25));
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it("should penalize missing files", () => {
|
|
739
|
+
createReviewTables(db);
|
|
740
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
741
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status, files) VALUES (?, 1, 'Task 1', 'done', ?)", ["SPEC-001", '["src/a.ts","src/b.ts"]']);
|
|
742
|
+
db.run("INSERT INTO artifacts (spec_id, task_ref, path, action) VALUES (?, 1, 'src/a.ts', 'created')", ["SPEC-001"]);
|
|
743
|
+
// src/b.ts missing
|
|
744
|
+
|
|
745
|
+
const score = calcScore(db, "SPEC-001");
|
|
746
|
+
expect(score.breakdown.filesDelivered).toBe(13); // round(1/2 * 25) = 13
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("should penalize standards bypasses specifically", () => {
|
|
750
|
+
createReviewTables(db);
|
|
751
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'reviewing')");
|
|
752
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 1, 'Task 1', 'done')", ["SPEC-001"]);
|
|
753
|
+
db.run("INSERT INTO tasks (spec_id, number, name, status) VALUES (?, 2, 'Task 2', 'done')", ["SPEC-001"]);
|
|
754
|
+
db.run("INSERT INTO gate_bypasses (spec_id, task_id, gate_name, reason) VALUES (?, 1, 'standards-follow', 'test')", ["SPEC-001"]);
|
|
755
|
+
|
|
756
|
+
const score = calcScore(db, "SPEC-001");
|
|
757
|
+
expect(score.breakdown.standardsFollowed).toBe(13); // round(1/2 * 25) = 13
|
|
758
|
+
});
|
|
759
|
+
});
|
|
333
760
|
});
|