Skip to content

Commit c41905c

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 d6e41dc commit c41905c

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;
@@ -753,6 +754,28 @@ enum Commands {
753754
#[command(subcommand)]
754755
action: CampaignAction,
755756
},
757+
758+
/// Query persisted findings + campaign state with a small S-expression
759+
/// language (issue #33 S3). See `panic-attack query --help` for syntax.
760+
Query {
761+
/// Query expression, e.g. `(and (category UnsafeCode) (pr-state nil))`.
762+
#[arg(value_name = "EXPR")]
763+
expr: String,
764+
765+
/// VeriSimDB data directory (default: `verisimdb-data`).
766+
#[arg(long, value_name = "DIR", default_value = "verisimdb-data")]
767+
verisimdb_dir: PathBuf,
768+
769+
/// Output format.
770+
#[arg(long, value_enum, default_value_t = QueryFormatArg::Table)]
771+
format: QueryFormatArg,
772+
},
773+
}
774+
775+
#[derive(clap::ValueEnum, Clone, Debug)]
776+
enum QueryFormatArg {
777+
Table,
778+
Json,
756779
}
757780

758781
#[derive(Subcommand)]
@@ -2404,6 +2427,24 @@ fn run_main() -> Result<()> {
24042427
}
24052428
},
24062429

2430+
Commands::Query {
2431+
expr,
2432+
verisimdb_dir,
2433+
format,
2434+
} => {
2435+
let q = query::parse(&expr)?;
2436+
let hits = query::run(&q, &verisimdb_dir)?;
2437+
match format {
2438+
QueryFormatArg::Table => {
2439+
print!("{}", query::render_table(&hits));
2440+
}
2441+
QueryFormatArg::Json => {
2442+
println!("{}", serde_json::to_string_pretty(&hits)?);
2443+
}
2444+
}
2445+
return Ok(());
2446+
}
2447+
24072448
Commands::Campaign { action } => {
24082449
match action {
24092450
CampaignAction::RegisterPr {

0 commit comments

Comments
 (0)