Skip to content

Commit bc07d0f

Browse files
hyperpolymathclaude
andcommitted
feat(query): panic-attack query subcommand (issue #33 S3)
Adds the third slice of issue #33: a panic-attack query subcommand that evaluates a small S-expression query language over the persisted per-finding hexads (S1) and campaign-state hexads (S2), joined by finding_id. Supported forms in this initial S3: (category UnsafeCode) (rule-id PA004) (severity Critical) (repo <name-substring>) ; case-insensitive substring (file <path-substring>) ; case-insensitive substring (pr-state pr-filed|pr-merged|pr-closed|dismissed|nil) (and <expr> <expr> ...) (or <expr> <expr> ...) (not <expr>) `pr-state nil` matches any finding without a campaign hexad — i.e. the operationally important "open work not yet PR'd" view that the estate-sweep campaign needs most. CLI: panic-attack query "(and (category UnsafeCode) (pr-state nil))" panic-attack query "(severity Critical)" --format json panic-attack query "(repo alpha)" --verisimdb-dir verisimdb-data Output: fixed-width table by default, JSON via `--format json`. Deferred to S3 follow-ups (recorded in the module header): - (crosslang :from FFI :to ProofDrift) — needs integration with src/kanren/crosslang.rs. - (diff :since <date> :category <X>) — needs an explicit baseline-run cursor beyond created_at. Implementation notes: - Small hand-rolled S-expression tokenizer/parser in src/query/mod.rs (~170 lines including escape handling for quoted strings and `;` line comments). Doesn't depend on the a2ml parser since the query surface is narrower. - Evaluator pre-joins findings with their latest campaign event (newest-by-created_at wins per finding_id) before filtering. That keeps `(pr-state ...)` a free clause inside `and`/`or` rather than forcing a special-case in the loop. Tests: 19 new in src/query/ — 8 parser (positive + 3 rejection cases), 9 evaluator (each filter, and/or/not, pr-state nil/filed/excluded), 2 renderer. Full lib suite: 239 green. Clippy clean. CLI smoke validated: writing a hand-crafted finding hexad + invoking campaign register-pr + query returns the expected JSON with the pr-filed state joined in. Refs #33. Stacked on #56 (S2). Diff against main includes S1+S2 changes until they land; this PR rebases cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 016791c commit bc07d0f

3 files changed

Lines changed: 759 additions & 0 deletions

File tree

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub mod kanren;
3232
pub mod mass_panic;
3333
pub mod notify;
3434
pub mod panll;
35+
pub mod query;
3536
pub mod report;
3637
pub mod signatures;
3738
pub mod storage;

src/main.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mod kin;
2727
mod mass_panic;
2828
mod notify;
2929
mod panll;
30+
mod query;
3031
mod report;
3132
mod signatures;
3233
mod storage;
@@ -778,6 +779,28 @@ enum Commands {
778779
#[arg(long, group = "sweep_shape", default_value_t = false)]
779780
by_category: bool,
780781
},
782+
783+
/// Query persisted findings + campaign state with a small S-expression
784+
/// language (issue #33 S3). See `panic-attack query --help` for syntax.
785+
Query {
786+
/// Query expression, e.g. `(and (category UnsafeCode) (pr-state nil))`.
787+
#[arg(value_name = "EXPR")]
788+
expr: String,
789+
790+
/// VeriSimDB data directory (default: `verisimdb-data`).
791+
#[arg(long, value_name = "DIR", default_value = "verisimdb-data")]
792+
verisimdb_dir: PathBuf,
793+
794+
/// Output format.
795+
#[arg(long, value_enum, default_value_t = QueryFormatArg::Table)]
796+
format: QueryFormatArg,
797+
},
798+
}
799+
800+
#[derive(clap::ValueEnum, Clone, Debug)]
801+
enum QueryFormatArg {
802+
Table,
803+
Json,
781804
}
782805

783806
#[derive(Subcommand)]
@@ -2429,6 +2452,24 @@ fn run_main() -> Result<()> {
24292452
}
24302453
},
24312454

2455+
Commands::Query {
2456+
expr,
2457+
verisimdb_dir,
2458+
format,
2459+
} => {
2460+
let q = query::parse(&expr)?;
2461+
let hits = query::run(&q, &verisimdb_dir)?;
2462+
match format {
2463+
QueryFormatArg::Table => {
2464+
print!("{}", query::render_table(&hits));
2465+
}
2466+
QueryFormatArg::Json => {
2467+
println!("{}", serde_json::to_string_pretty(&hits)?);
2468+
}
2469+
}
2470+
return Ok(());
2471+
}
2472+
24322473
Commands::Campaign { action } => {
24332474
match action {
24342475
CampaignAction::RegisterPr {

0 commit comments

Comments
 (0)