@codexa/cli 9.0.6 → 9.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/decide.ts +120 -3
- package/commands/discover.ts +18 -9
- package/commands/integration.test.ts +754 -0
- package/commands/knowledge.test.ts +2 -6
- package/commands/knowledge.ts +20 -4
- package/commands/patterns.ts +8 -644
- package/commands/product.ts +41 -104
- package/commands/spec-resolver.test.ts +2 -13
- package/commands/standards.ts +33 -3
- package/commands/task.ts +21 -4
- package/commands/utils.test.ts +25 -87
- package/commands/utils.ts +20 -82
- package/context/assembly.ts +81 -0
- package/context/domains.test.ts +278 -0
- package/context/domains.ts +156 -0
- package/context/generator.ts +272 -0
- package/context/index.ts +21 -0
- package/context/scoring.ts +106 -0
- package/context/sections.ts +247 -0
- package/db/schema.ts +40 -5
- package/db/test-helpers.ts +33 -0
- package/gates/standards-validator.test.ts +447 -0
- package/gates/standards-validator.ts +164 -125
- package/gates/typecheck-validator.ts +296 -92
- package/gates/validator.ts +93 -8
- package/package.json +4 -2
- package/protocol/process-return.ts +39 -4
- package/templates/loader.ts +22 -0
- package/templates/subagent-context.md +34 -0
- package/templates/subagent-return-protocol.md +29 -0
- package/workflow.ts +54 -84
package/db/schema.ts
CHANGED
|
@@ -237,7 +237,6 @@ export function initSchema(): void {
|
|
|
237
237
|
relation TEXT NOT NULL, -- uses, implements, depends_on, modifies, contradicts, extracted_from
|
|
238
238
|
|
|
239
239
|
-- Metadados
|
|
240
|
-
strength REAL DEFAULT 1.0, -- 0-1, peso da relacao
|
|
241
240
|
metadata TEXT, -- JSON com informacoes adicionais
|
|
242
241
|
|
|
243
242
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
@@ -459,6 +458,28 @@ const MIGRATIONS: Migration[] = [
|
|
|
459
458
|
}
|
|
460
459
|
},
|
|
461
460
|
},
|
|
461
|
+
{
|
|
462
|
+
version: "9.5.0",
|
|
463
|
+
description: "Adicionar superseded_by na tabela decisions",
|
|
464
|
+
up: (db) => {
|
|
465
|
+
db.exec(`ALTER TABLE decisions ADD COLUMN superseded_by TEXT`);
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
{
|
|
469
|
+
version: "9.6.0",
|
|
470
|
+
description: "Adicionar semantic_query e expect na tabela standards para validacao semantica stack-agnostica",
|
|
471
|
+
up: (db) => {
|
|
472
|
+
db.exec(`ALTER TABLE standards ADD COLUMN semantic_query TEXT`);
|
|
473
|
+
db.exec(`ALTER TABLE standards ADD COLUMN expect TEXT DEFAULT 'no_match'`);
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
version: "9.7.0",
|
|
478
|
+
description: "Adicionar typecheck_command na tabela project para typecheck agnostico de stack",
|
|
479
|
+
up: (db) => {
|
|
480
|
+
db.exec(`ALTER TABLE project ADD COLUMN typecheck_command TEXT`);
|
|
481
|
+
},
|
|
482
|
+
},
|
|
462
483
|
];
|
|
463
484
|
|
|
464
485
|
export function runMigrations(): void {
|
|
@@ -521,6 +542,22 @@ export function claimTask(taskId: number): boolean {
|
|
|
521
542
|
return result.changes > 0;
|
|
522
543
|
}
|
|
523
544
|
|
|
545
|
+
const STUCK_THRESHOLD_MS = 30 * 60 * 1000; // 30 minutos
|
|
546
|
+
|
|
547
|
+
export function detectStuckTasks(specId: string): any[] {
|
|
548
|
+
const db = getDb();
|
|
549
|
+
const running = db.query(
|
|
550
|
+
"SELECT * FROM tasks WHERE spec_id = ? AND status = 'running'"
|
|
551
|
+
).all(specId) as any[];
|
|
552
|
+
|
|
553
|
+
const now = Date.now();
|
|
554
|
+
return running.filter((task: any) => {
|
|
555
|
+
if (!task.started_at) return true;
|
|
556
|
+
const elapsed = now - new Date(task.started_at).getTime();
|
|
557
|
+
return elapsed > STUCK_THRESHOLD_MS;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
524
561
|
export function getPatternsByScope(scope: string): any[] {
|
|
525
562
|
const db = getDb();
|
|
526
563
|
return db.query(
|
|
@@ -582,7 +619,6 @@ export interface GraphRelation {
|
|
|
582
619
|
targetType: string;
|
|
583
620
|
targetId: string;
|
|
584
621
|
relation: string;
|
|
585
|
-
strength?: number;
|
|
586
622
|
metadata?: Record<string, any>;
|
|
587
623
|
}
|
|
588
624
|
|
|
@@ -591,8 +627,8 @@ export function addGraphRelation(specId: string | null, relation: GraphRelation)
|
|
|
591
627
|
const now = new Date().toISOString();
|
|
592
628
|
|
|
593
629
|
db.run(
|
|
594
|
-
`INSERT INTO knowledge_graph (spec_id, source_type, source_id, target_type, target_id, relation,
|
|
595
|
-
VALUES (?, ?, ?, ?, ?, ?, ?,
|
|
630
|
+
`INSERT INTO knowledge_graph (spec_id, source_type, source_id, target_type, target_id, relation, metadata, created_at)
|
|
631
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
596
632
|
[
|
|
597
633
|
specId,
|
|
598
634
|
relation.sourceType,
|
|
@@ -600,7 +636,6 @@ export function addGraphRelation(specId: string | null, relation: GraphRelation)
|
|
|
600
636
|
relation.targetType,
|
|
601
637
|
relation.targetId,
|
|
602
638
|
relation.relation,
|
|
603
|
-
relation.strength || 1.0,
|
|
604
639
|
relation.metadata ? JSON.stringify(relation.metadata) : null,
|
|
605
640
|
now,
|
|
606
641
|
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { getDb } from "./connection";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Limpa todas as tabelas do DB respeitando ordem de FK constraints.
|
|
5
|
+
* Usa PRAGMA foreign_keys = OFF como safety net.
|
|
6
|
+
*
|
|
7
|
+
* Uso: beforeEach(() => { initSchema(); cleanDb(); });
|
|
8
|
+
*/
|
|
9
|
+
export function cleanDb() {
|
|
10
|
+
const db = getDb();
|
|
11
|
+
db.run("PRAGMA foreign_keys = OFF");
|
|
12
|
+
try {
|
|
13
|
+
// Folhas (sem dependentes) primeiro
|
|
14
|
+
db.run("DELETE FROM knowledge_acknowledgments");
|
|
15
|
+
db.run("DELETE FROM agent_performance");
|
|
16
|
+
db.run("DELETE FROM reasoning_log");
|
|
17
|
+
db.run("DELETE FROM knowledge_graph");
|
|
18
|
+
db.run("DELETE FROM gate_bypasses");
|
|
19
|
+
db.run("DELETE FROM snapshots");
|
|
20
|
+
db.run("DELETE FROM review");
|
|
21
|
+
db.run("DELETE FROM project_utilities");
|
|
22
|
+
db.run("DELETE FROM artifacts");
|
|
23
|
+
db.run("DELETE FROM decisions");
|
|
24
|
+
// Dependentes de knowledge (antes de knowledge)
|
|
25
|
+
db.run("DELETE FROM knowledge");
|
|
26
|
+
// Dependentes de tasks/specs
|
|
27
|
+
db.run("DELETE FROM tasks");
|
|
28
|
+
db.run("DELETE FROM context");
|
|
29
|
+
db.run("DELETE FROM specs");
|
|
30
|
+
} finally {
|
|
31
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test";
|
|
2
|
+
import { getDb } from "../db/connection";
|
|
3
|
+
import { initSchema, runMigrations } from "../db/schema";
|
|
4
|
+
import { writeFileSync, mkdirSync, rmSync, existsSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
|
|
7
|
+
// Mock grepai functions before importing validator
|
|
8
|
+
import * as patterns from "../commands/patterns";
|
|
9
|
+
|
|
10
|
+
import { validateAgainstStandards, printValidationResult } from "./standards-validator";
|
|
11
|
+
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════
|
|
13
|
+
// HELPERS
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════
|
|
15
|
+
|
|
16
|
+
const TMP_DIR = join(process.cwd(), ".codexa", "test-tmp-standards");
|
|
17
|
+
|
|
18
|
+
function ensureTmpDir() {
|
|
19
|
+
if (!existsSync(TMP_DIR)) {
|
|
20
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createTmpFile(name: string, content: string): string {
|
|
25
|
+
ensureTmpDir();
|
|
26
|
+
const path = join(TMP_DIR, name);
|
|
27
|
+
writeFileSync(path, content);
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function insertStandard(opts: {
|
|
32
|
+
category: string;
|
|
33
|
+
scope: string;
|
|
34
|
+
rule: string;
|
|
35
|
+
enforcement?: string;
|
|
36
|
+
anti_examples?: string;
|
|
37
|
+
semantic_query?: string | null;
|
|
38
|
+
expect?: string;
|
|
39
|
+
}) {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
db.run(
|
|
42
|
+
`INSERT INTO standards (category, scope, rule, anti_examples, enforcement, semantic_query, expect, source, created_at)
|
|
43
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'test', datetime('now'))`,
|
|
44
|
+
[
|
|
45
|
+
opts.category,
|
|
46
|
+
opts.scope,
|
|
47
|
+
opts.rule,
|
|
48
|
+
opts.anti_examples || null,
|
|
49
|
+
opts.enforcement || "required",
|
|
50
|
+
opts.semantic_query || null,
|
|
51
|
+
opts.expect || "no_match",
|
|
52
|
+
]
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ═══════════════════════════════════════════════════════════════
|
|
57
|
+
// TESTS
|
|
58
|
+
// ═══════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
describe("standards-validator", () => {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
initSchema();
|
|
63
|
+
runMigrations();
|
|
64
|
+
const db = getDb();
|
|
65
|
+
db.run("DELETE FROM standards");
|
|
66
|
+
|
|
67
|
+
// Clean tmp files
|
|
68
|
+
if (existsSync(TMP_DIR)) {
|
|
69
|
+
rmSync(TMP_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─────────────────────────────────────────────────────────
|
|
74
|
+
// BASE CASES
|
|
75
|
+
// ─────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
it("returns passed when no standards exist", () => {
|
|
78
|
+
const file = createTmpFile("test.ts", "export const x = 1;");
|
|
79
|
+
const result = validateAgainstStandards([file], "all");
|
|
80
|
+
expect(result.passed).toBe(true);
|
|
81
|
+
expect(result.violations).toHaveLength(0);
|
|
82
|
+
expect(result.warnings).toHaveLength(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("returns passed when files list is empty", () => {
|
|
86
|
+
insertStandard({ category: "code", scope: "all", rule: "No console.log" });
|
|
87
|
+
const result = validateAgainstStandards([], "all");
|
|
88
|
+
expect(result.passed).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ─────────────────────────────────────────────────────────
|
|
92
|
+
// TEXTUAL VALIDATION (anti_examples)
|
|
93
|
+
// ─────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
it("detects anti_examples violation in file content", () => {
|
|
96
|
+
const file = createTmpFile("api.ts", 'console.log("debug"); export function handle() {}');
|
|
97
|
+
insertStandard({
|
|
98
|
+
category: "code",
|
|
99
|
+
scope: "all",
|
|
100
|
+
rule: "Nao usar console.log",
|
|
101
|
+
anti_examples: JSON.stringify(["console.log"]),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const result = validateAgainstStandards([file], "all");
|
|
105
|
+
expect(result.passed).toBe(false);
|
|
106
|
+
expect(result.violations).toHaveLength(1);
|
|
107
|
+
expect(result.violations[0].detail).toContain("console.log");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("passes when anti_examples not found in file", () => {
|
|
111
|
+
const file = createTmpFile("api.ts", "export function handle() { return 'ok'; }");
|
|
112
|
+
insertStandard({
|
|
113
|
+
category: "code",
|
|
114
|
+
scope: "all",
|
|
115
|
+
rule: "Nao usar console.log",
|
|
116
|
+
anti_examples: JSON.stringify(["console.log"]),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const result = validateAgainstStandards([file], "all");
|
|
120
|
+
expect(result.passed).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("treats recommended enforcement as warning not violation", () => {
|
|
124
|
+
const file = createTmpFile("api.ts", 'console.log("test");');
|
|
125
|
+
insertStandard({
|
|
126
|
+
category: "code",
|
|
127
|
+
scope: "all",
|
|
128
|
+
rule: "Evitar console.log",
|
|
129
|
+
anti_examples: JSON.stringify(["console.log"]),
|
|
130
|
+
enforcement: "recommended",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = validateAgainstStandards([file], "all");
|
|
134
|
+
expect(result.passed).toBe(true); // recommended does NOT block
|
|
135
|
+
expect(result.warnings).toHaveLength(1);
|
|
136
|
+
expect(result.violations).toHaveLength(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("filters standards by scope", () => {
|
|
140
|
+
const file = createTmpFile("api.ts", 'console.log("test");');
|
|
141
|
+
|
|
142
|
+
// Standard for frontend scope only
|
|
143
|
+
insertStandard({
|
|
144
|
+
category: "code",
|
|
145
|
+
scope: "frontend",
|
|
146
|
+
rule: "Frontend: no console.log",
|
|
147
|
+
anti_examples: JSON.stringify(["console.log"]),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Validate with backend domain — should NOT match frontend standard
|
|
151
|
+
const result = validateAgainstStandards([file], "backend");
|
|
152
|
+
expect(result.passed).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("matches scope 'all' for any domain", () => {
|
|
156
|
+
const file = createTmpFile("api.ts", 'console.log("test");');
|
|
157
|
+
insertStandard({
|
|
158
|
+
category: "code",
|
|
159
|
+
scope: "all",
|
|
160
|
+
rule: "No console.log",
|
|
161
|
+
anti_examples: JSON.stringify(["console.log"]),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const result = validateAgainstStandards([file], "backend");
|
|
165
|
+
expect(result.passed).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("skips nonexistent files gracefully", () => {
|
|
169
|
+
insertStandard({
|
|
170
|
+
category: "code",
|
|
171
|
+
scope: "all",
|
|
172
|
+
rule: "No eval",
|
|
173
|
+
anti_examples: JSON.stringify(["eval("]),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const result = validateAgainstStandards(["/nonexistent/file.ts"], "all");
|
|
177
|
+
expect(result.passed).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("handles invalid anti_examples JSON gracefully", () => {
|
|
181
|
+
const file = createTmpFile("api.ts", "export const x = 1;");
|
|
182
|
+
const db = getDb();
|
|
183
|
+
db.run(
|
|
184
|
+
`INSERT INTO standards (category, scope, rule, anti_examples, enforcement, source, created_at)
|
|
185
|
+
VALUES ('code', 'all', 'Bad JSON', 'not-valid-json', 'required', 'test', datetime('now'))`
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const result = validateAgainstStandards([file], "all");
|
|
189
|
+
expect(result.passed).toBe(true); // Invalid JSON should not crash
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ─────────────────────────────────────────────────────────
|
|
193
|
+
// SEMANTIC VALIDATION (grepai)
|
|
194
|
+
// ─────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
it("detects no_match violation when grepai finds matching file", () => {
|
|
197
|
+
const file = createTmpFile("handler.ts", "export function handle() {}");
|
|
198
|
+
const resolvedPath = join(TMP_DIR, "handler.ts").replace(/\\/g, "/");
|
|
199
|
+
|
|
200
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
201
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
|
|
202
|
+
{ path: resolvedPath, score: 0.85 },
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
insertStandard({
|
|
206
|
+
category: "code",
|
|
207
|
+
scope: "all",
|
|
208
|
+
rule: "No .then() chains",
|
|
209
|
+
semantic_query: "promise .then() chain instead of async await",
|
|
210
|
+
expect: "no_match",
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const result = validateAgainstStandards([file], "all");
|
|
214
|
+
expect(result.passed).toBe(false);
|
|
215
|
+
expect(result.violations).toHaveLength(1);
|
|
216
|
+
expect(result.violations[0].detail).toContain("Violacao semantica");
|
|
217
|
+
expect(result.violations[0].detail).toContain("0.85");
|
|
218
|
+
|
|
219
|
+
availSpy.mockRestore();
|
|
220
|
+
searchSpy.mockRestore();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("passes no_match when grepai finds no matches", () => {
|
|
224
|
+
const file = createTmpFile("clean.ts", "export const x = 1;");
|
|
225
|
+
|
|
226
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
227
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([]);
|
|
228
|
+
|
|
229
|
+
insertStandard({
|
|
230
|
+
category: "code",
|
|
231
|
+
scope: "all",
|
|
232
|
+
rule: "No .then() chains",
|
|
233
|
+
semantic_query: "promise .then() chain",
|
|
234
|
+
expect: "no_match",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const result = validateAgainstStandards([file], "all");
|
|
238
|
+
expect(result.passed).toBe(true);
|
|
239
|
+
|
|
240
|
+
availSpy.mockRestore();
|
|
241
|
+
searchSpy.mockRestore();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("detects must_match violation when grepai finds no match", () => {
|
|
245
|
+
const file = createTmpFile("service.ts", "export function process() {}");
|
|
246
|
+
|
|
247
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
248
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([]);
|
|
249
|
+
|
|
250
|
+
insertStandard({
|
|
251
|
+
category: "practice",
|
|
252
|
+
scope: "all",
|
|
253
|
+
rule: "Must have error handling",
|
|
254
|
+
semantic_query: "try catch error handling",
|
|
255
|
+
expect: "must_match",
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
const result = validateAgainstStandards([file], "all");
|
|
259
|
+
expect(result.passed).toBe(false);
|
|
260
|
+
expect(result.violations).toHaveLength(1);
|
|
261
|
+
expect(result.violations[0].detail).toContain("Padrao obrigatorio nao encontrado");
|
|
262
|
+
|
|
263
|
+
availSpy.mockRestore();
|
|
264
|
+
searchSpy.mockRestore();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("passes must_match when grepai finds matching file", () => {
|
|
268
|
+
const file = createTmpFile("service.ts", "try { } catch (e) { }");
|
|
269
|
+
const resolvedPath = join(TMP_DIR, "service.ts").replace(/\\/g, "/");
|
|
270
|
+
|
|
271
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
272
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
|
|
273
|
+
{ path: resolvedPath, score: 0.9 },
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
insertStandard({
|
|
277
|
+
category: "practice",
|
|
278
|
+
scope: "all",
|
|
279
|
+
rule: "Must have error handling",
|
|
280
|
+
semantic_query: "try catch error handling",
|
|
281
|
+
expect: "must_match",
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const result = validateAgainstStandards([file], "all");
|
|
285
|
+
expect(result.passed).toBe(true);
|
|
286
|
+
|
|
287
|
+
availSpy.mockRestore();
|
|
288
|
+
searchSpy.mockRestore();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("ignores grepai results below score threshold", () => {
|
|
292
|
+
const file = createTmpFile("maybe.ts", "export const x = 1;");
|
|
293
|
+
const resolvedPath = join(TMP_DIR, "maybe.ts").replace(/\\/g, "/");
|
|
294
|
+
|
|
295
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
296
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
|
|
297
|
+
{ path: resolvedPath, score: 0.5 }, // Below 0.7 threshold
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
insertStandard({
|
|
301
|
+
category: "code",
|
|
302
|
+
scope: "all",
|
|
303
|
+
rule: "No console.log",
|
|
304
|
+
semantic_query: "console.log statement",
|
|
305
|
+
expect: "no_match",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const result = validateAgainstStandards([file], "all");
|
|
309
|
+
expect(result.passed).toBe(true); // Score too low = no match
|
|
310
|
+
|
|
311
|
+
availSpy.mockRestore();
|
|
312
|
+
searchSpy.mockRestore();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("ignores grepai results for files not in validation list", () => {
|
|
316
|
+
const file = createTmpFile("target.ts", "export const x = 1;");
|
|
317
|
+
|
|
318
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
319
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
|
|
320
|
+
{ path: "/some/other/file.ts", score: 0.95 }, // High score but wrong file
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
insertStandard({
|
|
324
|
+
category: "code",
|
|
325
|
+
scope: "all",
|
|
326
|
+
rule: "No eval",
|
|
327
|
+
semantic_query: "eval() usage",
|
|
328
|
+
expect: "no_match",
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const result = validateAgainstStandards([file], "all");
|
|
332
|
+
expect(result.passed).toBe(true); // Match is for a different file
|
|
333
|
+
|
|
334
|
+
availSpy.mockRestore();
|
|
335
|
+
searchSpy.mockRestore();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ─────────────────────────────────────────────────────────
|
|
339
|
+
// FALLBACK (grepai unavailable)
|
|
340
|
+
// ─────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
it("falls back to anti_examples when grepai not available", () => {
|
|
343
|
+
const file = createTmpFile("api.ts", 'eval("alert(1)");');
|
|
344
|
+
|
|
345
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(false);
|
|
346
|
+
|
|
347
|
+
insertStandard({
|
|
348
|
+
category: "code",
|
|
349
|
+
scope: "all",
|
|
350
|
+
rule: "No eval",
|
|
351
|
+
anti_examples: JSON.stringify(["eval("]),
|
|
352
|
+
semantic_query: "eval usage for dynamic code execution",
|
|
353
|
+
expect: "no_match",
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const result = validateAgainstStandards([file], "all");
|
|
357
|
+
expect(result.passed).toBe(false); // Falls back to anti_examples check
|
|
358
|
+
expect(result.violations).toHaveLength(1);
|
|
359
|
+
|
|
360
|
+
availSpy.mockRestore();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("skips must_match standards when grepai not available", () => {
|
|
364
|
+
const file = createTmpFile("service.ts", "export function process() {}");
|
|
365
|
+
|
|
366
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(false);
|
|
367
|
+
|
|
368
|
+
insertStandard({
|
|
369
|
+
category: "practice",
|
|
370
|
+
scope: "all",
|
|
371
|
+
rule: "Must have error handling",
|
|
372
|
+
semantic_query: "try catch error handling",
|
|
373
|
+
expect: "must_match",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const result = validateAgainstStandards([file], "all");
|
|
377
|
+
expect(result.passed).toBe(true); // Cannot validate must_match without grepai
|
|
378
|
+
|
|
379
|
+
availSpy.mockRestore();
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// ─────────────────────────────────────────────────────────
|
|
383
|
+
// CAP & MIXED
|
|
384
|
+
// ─────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
it("caps semantic standards at MAX_SEMANTIC_QUERIES (10)", () => {
|
|
387
|
+
const file = createTmpFile("test.ts", "export const x = 1;");
|
|
388
|
+
|
|
389
|
+
let callCount = 0;
|
|
390
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
391
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockImplementation(() => {
|
|
392
|
+
callCount++;
|
|
393
|
+
return [];
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Insert 15 semantic standards
|
|
397
|
+
for (let i = 0; i < 15; i++) {
|
|
398
|
+
insertStandard({
|
|
399
|
+
category: "code",
|
|
400
|
+
scope: "all",
|
|
401
|
+
rule: `Rule ${i}`,
|
|
402
|
+
semantic_query: `query ${i}`,
|
|
403
|
+
expect: "no_match",
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
validateAgainstStandards([file], "all");
|
|
408
|
+
expect(callCount).toBe(10); // Capped at 10
|
|
409
|
+
|
|
410
|
+
availSpy.mockRestore();
|
|
411
|
+
searchSpy.mockRestore();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("validates both semantic and textual standards in same run", () => {
|
|
415
|
+
const file = createTmpFile("mixed.ts", 'eval("code"); export const x = 1;');
|
|
416
|
+
const resolvedPath = join(TMP_DIR, "mixed.ts").replace(/\\/g, "/");
|
|
417
|
+
|
|
418
|
+
const availSpy = spyOn(patterns, "isGrepaiAvailable").mockReturnValue(true);
|
|
419
|
+
const searchSpy = spyOn(patterns, "searchWithGrepai").mockReturnValue([
|
|
420
|
+
{ path: resolvedPath, score: 0.9 },
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
// Semantic standard
|
|
424
|
+
insertStandard({
|
|
425
|
+
category: "code",
|
|
426
|
+
scope: "all",
|
|
427
|
+
rule: "No .then() chains",
|
|
428
|
+
semantic_query: "promise .then() chain",
|
|
429
|
+
expect: "no_match",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// Textual standard (no semantic_query)
|
|
433
|
+
insertStandard({
|
|
434
|
+
category: "code",
|
|
435
|
+
scope: "all",
|
|
436
|
+
rule: "No eval",
|
|
437
|
+
anti_examples: JSON.stringify(["eval("]),
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const result = validateAgainstStandards([file], "all");
|
|
441
|
+
expect(result.passed).toBe(false);
|
|
442
|
+
expect(result.violations).toHaveLength(2); // One semantic + one textual
|
|
443
|
+
|
|
444
|
+
availSpy.mockRestore();
|
|
445
|
+
searchSpy.mockRestore();
|
|
446
|
+
});
|
|
447
|
+
});
|