@codexa/cli 9.0.3 → 9.0.5
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 +113 -61
package/commands/architect.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema } from "../db/schema";
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
|
|
4
|
+
import { CodexaError } from "../errors";
|
|
4
5
|
import { join } from "path";
|
|
5
6
|
|
|
6
7
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -364,7 +365,7 @@ export function architectStart(description: string, options: { json?: boolean }
|
|
|
364
365
|
console.log(` Criada: ${pending.created_at}`);
|
|
365
366
|
console.log("\nUse 'architect show' para ver detalhes ou 'architect cancel' para cancelar.\n");
|
|
366
367
|
}
|
|
367
|
-
|
|
368
|
+
throw new CodexaError("Ja existe uma analise pendente. Use 'architect show' ou 'architect cancel'.");
|
|
368
369
|
}
|
|
369
370
|
|
|
370
371
|
// Criar nova analise
|
|
@@ -566,7 +567,7 @@ export function architectSave(options: { file?: string; json?: boolean }): void
|
|
|
566
567
|
} else {
|
|
567
568
|
console.log("\n[ERRO] Nenhuma analise pendente encontrada.\n");
|
|
568
569
|
}
|
|
569
|
-
|
|
570
|
+
throw new CodexaError("Nenhuma analise pendente encontrada.");
|
|
570
571
|
}
|
|
571
572
|
|
|
572
573
|
// v8.4: Resolver caminho do arquivo .md
|
|
@@ -587,7 +588,7 @@ export function architectSave(options: { file?: string; json?: boolean }): void
|
|
|
587
588
|
console.log(`\n[ERRO] Arquivo nao encontrado: ${filePath}`);
|
|
588
589
|
console.log("O agente architect deve escrever o .md antes de chamar 'architect save'.\n");
|
|
589
590
|
}
|
|
590
|
-
|
|
591
|
+
throw new CodexaError(`Arquivo nao encontrado: ${filePath}`);
|
|
591
592
|
}
|
|
592
593
|
|
|
593
594
|
const content = readFileSync(filePath, "utf-8");
|
|
@@ -697,7 +698,7 @@ export function architectApprove(options: { id?: string; json?: boolean }): void
|
|
|
697
698
|
} else {
|
|
698
699
|
console.log("\n[ERRO] Analise nao encontrada.\n");
|
|
699
700
|
}
|
|
700
|
-
|
|
701
|
+
throw new CodexaError("Analise nao encontrada.");
|
|
701
702
|
}
|
|
702
703
|
|
|
703
704
|
if (analysis.status !== "pending") {
|
|
@@ -706,7 +707,7 @@ export function architectApprove(options: { id?: string; json?: boolean }): void
|
|
|
706
707
|
} else {
|
|
707
708
|
console.log(`\n[ERRO] Analise ja esta com status '${analysis.status}'.\n`);
|
|
708
709
|
}
|
|
709
|
-
|
|
710
|
+
throw new CodexaError(`Analise ja esta com status '${analysis.status}'.`);
|
|
710
711
|
}
|
|
711
712
|
|
|
712
713
|
db.run(
|
|
@@ -750,7 +751,7 @@ export function architectExport(options: { id?: string; json?: boolean }): void
|
|
|
750
751
|
console.log("\n[ERRO] Nenhuma analise aprovada encontrada.");
|
|
751
752
|
console.log("Use 'architect approve' primeiro.\n");
|
|
752
753
|
}
|
|
753
|
-
|
|
754
|
+
throw new CodexaError("Nenhuma analise aprovada encontrada. Use 'architect approve' primeiro.");
|
|
754
755
|
}
|
|
755
756
|
|
|
756
757
|
const babySteps: BabyStep[] = analysis.baby_steps ? JSON.parse(analysis.baby_steps) : [];
|
package/commands/check.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema } from "../db/schema";
|
|
3
3
|
import { enforceGate } from "../gates/validator";
|
|
4
|
+
import { resolveSpec } from "./spec-resolver";
|
|
4
5
|
|
|
5
|
-
export function checkRequest(): void {
|
|
6
|
+
export function checkRequest(specId?: string): void {
|
|
6
7
|
initSchema();
|
|
7
8
|
enforceGate("check-request");
|
|
8
9
|
|
|
9
10
|
const db = getDb();
|
|
10
|
-
const spec =
|
|
11
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
12
|
-
.get() as any;
|
|
11
|
+
const spec = resolveSpec(specId, ["planning"]);
|
|
13
12
|
|
|
14
13
|
const tasks = db
|
|
15
14
|
.query("SELECT * FROM tasks WHERE spec_id = ? ORDER BY number")
|
|
@@ -68,16 +67,14 @@ export function checkRequest(): void {
|
|
|
68
67
|
console.log(`Para ajustar: plan task-add (adiciona mais tasks)\n`);
|
|
69
68
|
}
|
|
70
69
|
|
|
71
|
-
export function checkApprove(): void {
|
|
70
|
+
export function checkApprove(specId?: string): void {
|
|
72
71
|
initSchema();
|
|
73
72
|
enforceGate("check-approve");
|
|
74
73
|
|
|
75
74
|
const db = getDb();
|
|
76
75
|
const now = new Date().toISOString();
|
|
77
76
|
|
|
78
|
-
const spec =
|
|
79
|
-
.query("SELECT * FROM specs WHERE phase = 'checking' ORDER BY created_at DESC LIMIT 1")
|
|
80
|
-
.get() as any;
|
|
77
|
+
const spec = resolveSpec(specId, ["checking"]);
|
|
81
78
|
|
|
82
79
|
// Atualizar para aprovado
|
|
83
80
|
db.run(
|
|
@@ -100,20 +97,13 @@ export function checkApprove(): void {
|
|
|
100
97
|
console.log(`3. Complete uma task com: task done <id> --checkpoint "..."\n`);
|
|
101
98
|
}
|
|
102
99
|
|
|
103
|
-
export function checkReject(reason: string): void {
|
|
100
|
+
export function checkReject(reason: string, specId?: string): void {
|
|
104
101
|
initSchema();
|
|
105
102
|
|
|
106
103
|
const db = getDb();
|
|
107
104
|
const now = new Date().toISOString();
|
|
108
105
|
|
|
109
|
-
const spec =
|
|
110
|
-
.query("SELECT * FROM specs WHERE phase = 'checking' ORDER BY created_at DESC LIMIT 1")
|
|
111
|
-
.get() as any;
|
|
112
|
-
|
|
113
|
-
if (!spec) {
|
|
114
|
-
console.error("\nNenhum plano aguardando aprovacao.\n");
|
|
115
|
-
process.exit(1);
|
|
116
|
-
}
|
|
106
|
+
const spec = resolveSpec(specId, ["checking"]);
|
|
117
107
|
|
|
118
108
|
// Voltar para planning
|
|
119
109
|
db.run("UPDATE specs SET phase = 'planning', updated_at = ? WHERE id = ?", [now, spec.id]);
|
package/commands/clear.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { join, dirname } from "path";
|
|
|
5
5
|
|
|
6
6
|
interface ClearOptions {
|
|
7
7
|
force?: boolean;
|
|
8
|
+
specId?: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -64,6 +65,45 @@ export function clearTasks(options: ClearOptions = {}): void {
|
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// Per-spec clear or clear all
|
|
69
|
+
if (options.specId) {
|
|
70
|
+
console.log("\n" + "═".repeat(60));
|
|
71
|
+
console.log(`LIMPANDO SPEC: ${options.specId}`);
|
|
72
|
+
console.log("═".repeat(60) + "\n");
|
|
73
|
+
|
|
74
|
+
const tablesWithSpecId = [
|
|
75
|
+
"reasoning_log",
|
|
76
|
+
"knowledge_graph",
|
|
77
|
+
"knowledge",
|
|
78
|
+
"gate_bypasses",
|
|
79
|
+
"snapshots",
|
|
80
|
+
"review",
|
|
81
|
+
"artifacts",
|
|
82
|
+
"decisions",
|
|
83
|
+
"tasks",
|
|
84
|
+
"context",
|
|
85
|
+
"specs",
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
for (const table of tablesWithSpecId) {
|
|
89
|
+
try {
|
|
90
|
+
const result = db.run(`DELETE FROM ${table} WHERE spec_id = ?`, [options.specId]);
|
|
91
|
+
if (result.changes > 0) {
|
|
92
|
+
console.log(` Limpo: ${table} (${result.changes} registros)`);
|
|
93
|
+
}
|
|
94
|
+
} catch (err: any) {
|
|
95
|
+
if (!err.message.includes("no such table") && !err.message.includes("no such column")) {
|
|
96
|
+
console.error(` Erro ao limpar ${table}: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log("\n" + "═".repeat(60));
|
|
102
|
+
console.log(`SPEC ${options.specId} LIMPO`);
|
|
103
|
+
console.log("═".repeat(60) + "\n");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
67
107
|
console.log("\n" + "═".repeat(60));
|
|
68
108
|
console.log("LIMPANDO TASKS...");
|
|
69
109
|
console.log("═".repeat(60) + "\n");
|
package/commands/decide.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { getDb } from "../db/connection";
|
|
2
2
|
import { initSchema, getNextDecisionId } from "../db/schema";
|
|
3
|
+
import { resolveSpec } from "./spec-resolver";
|
|
4
|
+
import { CodexaError } from "../errors";
|
|
3
5
|
|
|
4
6
|
interface ConflictAnalysis {
|
|
5
7
|
hasConflict: boolean;
|
|
@@ -124,20 +126,13 @@ function detectConflicts(
|
|
|
124
126
|
};
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
export function decide(title: string, decision: string, options: { rationale?: string; force?: boolean }): void {
|
|
129
|
+
export function decide(title: string, decision: string, options: { rationale?: string; force?: boolean; specId?: string }): void {
|
|
128
130
|
initSchema();
|
|
129
131
|
|
|
130
132
|
const db = getDb();
|
|
131
133
|
const now = new Date().toISOString();
|
|
132
134
|
|
|
133
|
-
const spec =
|
|
134
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
135
|
-
.get() as any;
|
|
136
|
-
|
|
137
|
-
if (!spec) {
|
|
138
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
139
|
-
process.exit(1);
|
|
140
|
-
}
|
|
135
|
+
const spec = resolveSpec(options.specId);
|
|
141
136
|
|
|
142
137
|
// Verificar conflitos com decisoes existentes
|
|
143
138
|
const existingDecisions = db
|
|
@@ -159,9 +154,9 @@ export function decide(title: string, decision: string, options: { rationale?: s
|
|
|
159
154
|
}
|
|
160
155
|
|
|
161
156
|
console.warn(`${"─".repeat(50)}`);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
throw new CodexaError(
|
|
158
|
+
`Potencial conflito detectado.\nPara registrar mesmo assim, use: decide "${title}" "${decision}" --force\nOu revise as decisoes existentes com: decisions`
|
|
159
|
+
);
|
|
165
160
|
}
|
|
166
161
|
}
|
|
167
162
|
|
|
@@ -207,19 +202,12 @@ export function decide(title: string, decision: string, options: { rationale?: s
|
|
|
207
202
|
console.log();
|
|
208
203
|
}
|
|
209
204
|
|
|
210
|
-
export function listDecisions(json: boolean = false): void {
|
|
205
|
+
export function listDecisions(json: boolean = false, specId?: string): void {
|
|
211
206
|
initSchema();
|
|
212
207
|
|
|
213
208
|
const db = getDb();
|
|
214
209
|
|
|
215
|
-
const spec =
|
|
216
|
-
.query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
|
|
217
|
-
.get() as any;
|
|
218
|
-
|
|
219
|
-
if (!spec) {
|
|
220
|
-
console.error("\nNenhuma feature ativa.\n");
|
|
221
|
-
process.exit(1);
|
|
222
|
-
}
|
|
210
|
+
const spec = resolveSpec(specId);
|
|
223
211
|
|
|
224
212
|
const decisions = db
|
|
225
213
|
.query("SELECT * FROM decisions WHERE spec_id = ? ORDER BY created_at")
|
package/commands/discover.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getDetailedTechnologies,
|
|
12
12
|
type UnifiedDetectionResult,
|
|
13
13
|
} from "../detectors/loader";
|
|
14
|
+
import { CodexaError } from "../errors";
|
|
14
15
|
|
|
15
16
|
interface StackDetection {
|
|
16
17
|
frontend?: string;
|
|
@@ -172,9 +173,7 @@ export function discoverConfirm(): void {
|
|
|
172
173
|
|
|
173
174
|
const pending = db.query("SELECT * FROM project WHERE id = 'pending'").get() as any;
|
|
174
175
|
if (!pending) {
|
|
175
|
-
|
|
176
|
-
console.error("Execute: discover start primeiro\n");
|
|
177
|
-
process.exit(1);
|
|
176
|
+
throw new CodexaError("Nenhuma descoberta pendente.\nExecute: discover start primeiro");
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
const data = JSON.parse(pending.stack);
|
|
@@ -283,9 +282,7 @@ export function discoverShow(json: boolean = false): void {
|
|
|
283
282
|
const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
|
|
284
283
|
|
|
285
284
|
if (!project) {
|
|
286
|
-
|
|
287
|
-
console.error("Execute: discover start\n");
|
|
288
|
-
process.exit(1);
|
|
285
|
+
throw new CodexaError("Projeto nao descoberto.\nExecute: discover start");
|
|
289
286
|
}
|
|
290
287
|
|
|
291
288
|
const standards = db.query("SELECT * FROM standards ORDER BY category, scope").all() as any[];
|
|
@@ -381,9 +378,7 @@ export async function discoverRefresh(options: { force?: boolean } = {}): Promis
|
|
|
381
378
|
|
|
382
379
|
const currentProject = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
|
|
383
380
|
if (!currentProject) {
|
|
384
|
-
|
|
385
|
-
console.error("Execute: discover start primeiro\n");
|
|
386
|
-
process.exit(1);
|
|
381
|
+
throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
|
|
387
382
|
}
|
|
388
383
|
|
|
389
384
|
const currentStack = JSON.parse(currentProject.stack);
|
|
@@ -466,9 +461,7 @@ export async function discoverIncremental(): Promise<void> {
|
|
|
466
461
|
|
|
467
462
|
const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
|
|
468
463
|
if (!project) {
|
|
469
|
-
|
|
470
|
-
console.error("Execute: discover start primeiro\n");
|
|
471
|
-
process.exit(1);
|
|
464
|
+
throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
|
|
472
465
|
}
|
|
473
466
|
|
|
474
467
|
// Data de referencia: last_discover_at > discovered_at > fallback 30 dias
|
|
@@ -774,9 +767,7 @@ export function discoverPatternsShow(name: string, json: boolean = false): void
|
|
|
774
767
|
const pattern = db.query("SELECT * FROM implementation_patterns WHERE name = ?").get(name) as any;
|
|
775
768
|
|
|
776
769
|
if (!pattern) {
|
|
777
|
-
|
|
778
|
-
console.error("Use: discover patterns para listar todos\n");
|
|
779
|
-
process.exit(1);
|
|
770
|
+
throw new CodexaError(`Pattern '${name}' nao encontrado.\nUse: discover patterns para listar todos`);
|
|
780
771
|
}
|
|
781
772
|
|
|
782
773
|
const structure = JSON.parse(pattern.structure || "{}");
|
|
@@ -876,9 +867,7 @@ export function discoverPatternAdd(options: {
|
|
|
876
867
|
// Verificar se pattern ja existe
|
|
877
868
|
const existing = db.query("SELECT id FROM implementation_patterns WHERE name = ?").get(options.name);
|
|
878
869
|
if (existing) {
|
|
879
|
-
|
|
880
|
-
console.error("Use: discover pattern-edit para modificar\n");
|
|
881
|
-
process.exit(1);
|
|
870
|
+
throw new CodexaError(`Pattern '${options.name}' ja existe.\nUse: discover pattern-edit para modificar`);
|
|
882
871
|
}
|
|
883
872
|
|
|
884
873
|
const now = new Date().toISOString();
|
|
@@ -925,9 +914,7 @@ export function discoverPatternEdit(name: string, options: {
|
|
|
925
914
|
|
|
926
915
|
const existing = db.query("SELECT * FROM implementation_patterns WHERE name = ?").get(name) as any;
|
|
927
916
|
if (!existing) {
|
|
928
|
-
|
|
929
|
-
console.error("Use: discover patterns para listar todos\n");
|
|
930
|
-
process.exit(1);
|
|
917
|
+
throw new CodexaError(`Pattern '${name}' nao encontrado.\nUse: discover patterns para listar todos`);
|
|
931
918
|
}
|
|
932
919
|
|
|
933
920
|
const updates: string[] = [];
|
|
@@ -967,8 +954,7 @@ export function discoverPatternEdit(name: string, options: {
|
|
|
967
954
|
}
|
|
968
955
|
|
|
969
956
|
if (updates.length === 0) {
|
|
970
|
-
|
|
971
|
-
process.exit(1);
|
|
957
|
+
throw new CodexaError("Nenhuma opcao de atualizacao fornecida.");
|
|
972
958
|
}
|
|
973
959
|
|
|
974
960
|
updates.push("updated_at = ?");
|
|
@@ -993,8 +979,7 @@ export function discoverPatternRemove(name: string): void {
|
|
|
993
979
|
|
|
994
980
|
const existing = db.query("SELECT id FROM implementation_patterns WHERE name = ?").get(name);
|
|
995
981
|
if (!existing) {
|
|
996
|
-
|
|
997
|
-
process.exit(1);
|
|
982
|
+
throw new CodexaError(`Pattern '${name}' nao encontrado.`);
|
|
998
983
|
}
|
|
999
984
|
|
|
1000
985
|
db.run("DELETE FROM implementation_patterns WHERE name = ?", [name]);
|
|
@@ -1013,9 +998,7 @@ export function discoverRefreshPatterns(): void {
|
|
|
1013
998
|
// Verificar se projeto foi descoberto
|
|
1014
999
|
const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
|
|
1015
1000
|
if (!project) {
|
|
1016
|
-
|
|
1017
|
-
console.error("Execute: discover start primeiro\n");
|
|
1018
|
-
process.exit(1);
|
|
1001
|
+
throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
|
|
1019
1002
|
}
|
|
1020
1003
|
|
|
1021
1004
|
console.log("\n⚠ Refresh de patterns requer analise manual do codigo.");
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { jaccardSimilarity, compactKnowledge } from "./knowledge";
|
|
3
|
+
import { getDb } from "../db/connection";
|
|
4
|
+
import { initSchema } from "../db/schema";
|
|
5
|
+
|
|
6
|
+
describe("jaccardSimilarity", () => {
|
|
7
|
+
it("returns 1 for identical strings", () => {
|
|
8
|
+
expect(jaccardSimilarity("hello world test", "hello world test")).toBe(1);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns 0 for completely different strings", () => {
|
|
12
|
+
expect(jaccardSimilarity("apple banana cherry", "delta echo foxtrot")).toBe(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns high similarity for near-duplicates", () => {
|
|
16
|
+
const sim = jaccardSimilarity(
|
|
17
|
+
"Implemented user authentication with JWT tokens",
|
|
18
|
+
"Implemented user authentication using JWT tokens"
|
|
19
|
+
);
|
|
20
|
+
expect(sim).toBeGreaterThan(0.7);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns 1 for two empty strings", () => {
|
|
24
|
+
expect(jaccardSimilarity("", "")).toBe(1);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("returns 0 when one string is empty", () => {
|
|
28
|
+
expect(jaccardSimilarity("hello world test", "")).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("is case insensitive", () => {
|
|
32
|
+
expect(jaccardSimilarity("Hello World Test", "hello world test")).toBe(1);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("ignores short words (<=2 chars)", () => {
|
|
36
|
+
// "a" and "is" are filtered out, only "cat" and "dog" matter
|
|
37
|
+
const sim = jaccardSimilarity("a cat is here", "a dog is here");
|
|
38
|
+
// "cat" vs "dog" + "here" in common
|
|
39
|
+
expect(sim).toBeGreaterThan(0);
|
|
40
|
+
expect(sim).toBeLessThan(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("strips punctuation", () => {
|
|
44
|
+
const sim = jaccardSimilarity("hello, world! test.", "hello world test");
|
|
45
|
+
expect(sim).toBe(1);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("compactKnowledge", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
initSchema();
|
|
52
|
+
const db = getDb();
|
|
53
|
+
db.run("DELETE FROM knowledge_graph");
|
|
54
|
+
db.run("DELETE FROM knowledge");
|
|
55
|
+
db.run("DELETE FROM tasks");
|
|
56
|
+
db.run("DELETE FROM context");
|
|
57
|
+
db.run("DELETE FROM specs");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function createSpec(id: string, phase: string) {
|
|
61
|
+
const db = getDb();
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
db.run(
|
|
64
|
+
"INSERT INTO specs (id, name, phase, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
|
|
65
|
+
[id, `Feature ${id}`, phase, now, now]
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function addKnowledgeEntry(specId: string, content: string, category: string, severity: string, createdAt?: string) {
|
|
70
|
+
const db = getDb();
|
|
71
|
+
const now = createdAt || new Date().toISOString();
|
|
72
|
+
db.run(
|
|
73
|
+
`INSERT INTO knowledge (spec_id, task_origin, category, content, severity, broadcast_to, created_at)
|
|
74
|
+
VALUES (?, 0, ?, ?, ?, 'all', ?)`,
|
|
75
|
+
[specId, category, content, severity, now]
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
it("merges similar entries in same category and spec", () => {
|
|
80
|
+
createSpec("spec-a", "implementing");
|
|
81
|
+
addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the login flow", "discovery", "info");
|
|
82
|
+
addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for login flow", "discovery", "info");
|
|
83
|
+
|
|
84
|
+
// Dry run first
|
|
85
|
+
compactKnowledge({ dryRun: true, json: true });
|
|
86
|
+
|
|
87
|
+
// Actual compact
|
|
88
|
+
compactKnowledge({ json: true });
|
|
89
|
+
|
|
90
|
+
const db = getDb();
|
|
91
|
+
const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
|
|
92
|
+
expect(archived.c).toBe(1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps higher severity entry when merging", () => {
|
|
96
|
+
createSpec("spec-a", "implementing");
|
|
97
|
+
addKnowledgeEntry("spec-a", "Database connection timeout causing failures in production", "blocker", "critical");
|
|
98
|
+
addKnowledgeEntry("spec-a", "Database connection timeout causing failures in production system", "blocker", "info");
|
|
99
|
+
|
|
100
|
+
compactKnowledge({ json: true });
|
|
101
|
+
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const remaining = db.query("SELECT * FROM knowledge WHERE severity != 'archived'").all() as any[];
|
|
104
|
+
expect(remaining.length).toBe(1);
|
|
105
|
+
expect(remaining[0].severity).toBe("critical");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("archives old info entries (>7 days)", () => {
|
|
109
|
+
createSpec("spec-a", "implementing");
|
|
110
|
+
const oldDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
|
|
111
|
+
addKnowledgeEntry("spec-a", "Some old discovery that is not referenced anywhere in the graph", "discovery", "info", oldDate);
|
|
112
|
+
addKnowledgeEntry("spec-a", "A recent discovery that should not be archived by this compaction", "discovery", "info");
|
|
113
|
+
|
|
114
|
+
compactKnowledge({ json: true });
|
|
115
|
+
|
|
116
|
+
const db = getDb();
|
|
117
|
+
const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
|
|
118
|
+
expect(archived.c).toBe(1);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("archives entries from completed specs", () => {
|
|
122
|
+
createSpec("spec-done", "completed");
|
|
123
|
+
addKnowledgeEntry("spec-done", "Something learned during completed feature implementation", "discovery", "info");
|
|
124
|
+
addKnowledgeEntry("spec-done", "A critical blocker that was found during the implementation phase", "blocker", "critical");
|
|
125
|
+
|
|
126
|
+
compactKnowledge({ json: true });
|
|
127
|
+
|
|
128
|
+
const db = getDb();
|
|
129
|
+
const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
|
|
130
|
+
expect(archived.c).toBe(2);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("dry-run mode does not modify DB", () => {
|
|
134
|
+
createSpec("spec-done", "completed");
|
|
135
|
+
addKnowledgeEntry("spec-done", "Something that would be archived in a real compaction run", "discovery", "info");
|
|
136
|
+
|
|
137
|
+
compactKnowledge({ dryRun: true, json: true });
|
|
138
|
+
|
|
139
|
+
const db = getDb();
|
|
140
|
+
const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
|
|
141
|
+
expect(archived.c).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("respects --spec filter", () => {
|
|
145
|
+
createSpec("spec-a", "implementing");
|
|
146
|
+
createSpec("spec-b", "implementing");
|
|
147
|
+
addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the system", "discovery", "info");
|
|
148
|
+
addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the system flow", "discovery", "info");
|
|
149
|
+
addKnowledgeEntry("spec-b", "Implemented user authentication with JWT tokens for the login", "discovery", "info");
|
|
150
|
+
addKnowledgeEntry("spec-b", "Implemented user authentication with JWT tokens for the login flow", "discovery", "info");
|
|
151
|
+
|
|
152
|
+
compactKnowledge({ specId: "spec-a", json: true });
|
|
153
|
+
|
|
154
|
+
const db = getDb();
|
|
155
|
+
const archivedA = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived' AND spec_id = 'spec-a'").get() as any;
|
|
156
|
+
const archivedB = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived' AND spec_id = 'spec-b'").get() as any;
|
|
157
|
+
expect(archivedA.c).toBe(1);
|
|
158
|
+
expect(archivedB.c).toBe(0);
|
|
159
|
+
});
|
|
160
|
+
});
|