-
Notifications
You must be signed in to change notification settings - Fork 1
feat: evidence category validation + gap collection + CLI arg validation (#174, #175) #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
7dd73cd
8db735c
6183ef5
47ff8f2
ebb9b8a
3b338a2
43d0d47
43572a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,9 +1,13 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { join, resolve } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { CAC } from "cac"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { loadDiscoveryEvidence } from "../../../agents/evidence-collector.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { loadDiscoveryEvidence, appendDiscoveryEvidence } from "../../../agents/evidence-collector.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { DiscoveryEvidenceEntry } from "../../../agents/evidence-collector.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { DecisionFileSchema } from "../../../agents/contracts/evidence.js"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import { z } from "zod"; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const RunDirArgSchema = z.string().trim().min(1, "runDir is required"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const KeywordArgSchema = z.string().trim().min(1, "keyword is required"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── discovery-filter-evidence ────────────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -39,8 +43,10 @@ export function registerFilterDiscoveryEvidence(cli: CAC): void { | |||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .option("--run-dir <path>", "Write filtered evidence to run directory") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .action((keyword: string, options: { runDir?: string }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const kParsed = KeywordArgSchema.safeParse(keyword); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!kParsed.success) { console.log(`Invalid keyword: ${kParsed.error.issues[0]?.message}`); return; } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const filtered = filterDiscoveryEvidence(keyword); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const filtered = filterDiscoveryEvidence(kParsed.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (options.runDir) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dir = resolve(options.runDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -110,7 +116,9 @@ export function registerApplyDecision(cli: CAC): void { | |||||||||||||||||||||||||||||||||||||||||||||||||
| "Read decision.json and output the action (commit/revert/adjust)" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .action((runDir: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dir = resolve(runDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = RunDirArgSchema.safeParse(runDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!parsed.success) { console.log(`Invalid runDir: ${parsed.error.issues[0]?.message}`); return; } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dir = resolve(parsed.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(dir) || !statSync(dir).isDirectory()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Run directory not found or is not a directory: ${runDir}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -125,3 +133,91 @@ export function registerApplyDecision(cli: CAC): void { | |||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(JSON.stringify(result)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ─── calibrate-collect-gap-evidence ───────────────────────────────────────── | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const GapSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| category: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: z.string(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| actionable: z.boolean(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| coveredByRule: z.unknown().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }).passthrough(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Require an explicit The selection contract here is “actionable and ♻️ Suggested contract tightening const GapSchema = z.object({
category: z.string(),
description: z.string(),
actionable: z.boolean(),
- coveredByRule: z.unknown().optional(),
+ coveredByRule: z.unknown().nullable(),
}).passthrough();
...
- if (gap.coveredByRule != null) continue;
+ if (gap.coveredByRule !== null) continue;Also applies to: 171-175 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| const GapsFileSchema = z.object({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fixture: z.string().optional(), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| gaps: z.array(GapSchema), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }).passthrough(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+129
to
+139
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major Rename the schema constants to SCREAMING_SNAKE_CASE.
♻️ Proposed rename-const GapSchema = z.object({
+const GAP_SCHEMA = z.object({
category: z.string(),
description: z.string(),
actionable: z.boolean(),
coveredByRule: z.unknown().default(null),
}).passthrough();
-const GapsFileSchema = z.object({
+const GAPS_FILE_SCHEMA = z.object({
fixture: z.string().optional(),
- gaps: z.array(GapSchema),
+ gaps: z.array(GAP_SCHEMA),
}).passthrough();
@@
- const parsed = GapsFileSchema.safeParse(raw);
+ const parsed = GAPS_FILE_SCHEMA.safeParse(raw);Also applies to: 155-156 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Extract uncovered actionable gaps from gaps.json and append to discovery evidence. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| * Deterministic — no LLM needed. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| export function collectGapEvidence(runDir: string, fixture: string): DiscoveryEvidenceEntry[] { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const gapsPath = join(runDir, "gaps.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(gapsPath)) return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| let raw: unknown; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| raw = JSON.parse(readFileSync(gapsPath, "utf-8")); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = GapsFileSchema.safeParse(raw); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!parsed.success) return []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const timestamp = new Date().toISOString(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries: DiscoveryEvidenceEntry[] = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const gap of parsed.data.gaps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Only actionable gaps not covered by existing rules | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!gap.actionable) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Skip when coveredByRule is present (non-nullish); empty string counts as "marked covered" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (gap.coveredByRule != null) continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| entries.push({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| description: gap.description, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| category: gap.category, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| impact: "medium", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| fixture, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| timestamp, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| source: "gap-analysis", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return entries; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export function registerCollectGapEvidence(cli: CAC): void { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| cli | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .command( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "calibrate-collect-gap-evidence <runDir>", | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "Collect uncovered actionable gaps from gaps.json into discovery evidence" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .action((runDir: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = RunDirArgSchema.safeParse(runDir); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!parsed.success) { console.log(`Invalid runDir: ${parsed.error.issues[0]?.message}`); return; } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dir = resolve(parsed.data); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!existsSync(dir) || !statSync(dir).isDirectory()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Run directory not found or is not a directory: ${runDir}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract fixture name from run dir | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const dirName = dir.split(/[/\\]/).pop() ?? ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const idx = dirName.lastIndexOf("--"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const fixture = idx === -1 ? dirName : dirName.slice(0, idx); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const entries = collectGapEvidence(dir, fixture); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (entries.length === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("No uncovered actionable gaps found"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| appendDiscoveryEvidence(entries); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Collected ${entries.length} gap evidence entries for fixture "${fixture}"`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Failed to collect gap evidence: ${err instanceof Error ? err.message : String(err)}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+195
to
+205
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mark append failures as command failures.
♻️ Suggested failure signaling } catch (err) {
console.log(`Failed to collect gap evidence: ${err instanceof Error ? err.message : String(err)}`);
+ process.exitCode = 1;
}Based on learnings, the stdout-and-exit-0 convention in 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.