Skip to content

feat(campaign): finding-lifecycle CLI + state hexads (issue #33 S2)#56

Closed
hyperpolymath wants to merge 2 commits into
mainfrom
feat/issue-33-s2-campaign-state
Closed

feat(campaign): finding-lifecycle CLI + state hexads (issue #33 S2)#56
hyperpolymath wants to merge 2 commits into
mainfrom
feat/issue-33-s2-campaign-state

Conversation

@hyperpolymath
Copy link
Copy Markdown
Owner

Summary

Second slice of #33. Adds a `panic-attack campaign` subcommand that tracks the lifecycle of individual findings produced by the assemblyline per-finding hexad path landed in #55 (S1).

State is persisted as campaign-facet hexads under `

/hexads/campaign/`, indexed by `finding_id`, append-only — the current state per finding is the newest campaign hexad with that finding_id as subject.

Stacked on #55 — diff against `main` includes the S1 changes until S1 lands; this PR rebases cleanly when S1 merges.

What's new

  • `HexadSemantic.campaign: Option` — additive, `skip_serializing_if`.
  • `CampaignSemantic { finding_id, state, pr_url?, reason?, last_polled? }` — `state` is a free-form String (forward-compatible).
  • Storage helpers: `build_campaign_hexad` / `write_campaign_hexad` / `load_{finding,campaign,aggregate}_hexads`.
  • New module `src/campaign/`: `register_pr`, `dismiss`, `current_state`, `status_markdown`.
  • CLI: `panic-attack campaign register-pr | dismiss | status`.

`status` renders a Markdown tracker matching the shape of the issue #32 manual checklist: summary line, table with finding-id, repo, rule_id, location, state, PR link (or dismissal reason), last-event timestamp, and a checkbox column (`[x]` for merged/closed/dismissed, `[ ]` otherwise).

Out of scope (S2b)

  • `panic-attack campaign poll` — queries GitHub for PR-state transitions. The data path is in place; the polling logic ships once the rate-limit / pagination shape is settled. Deferred to a follow-up to keep this PR focused.

Test plan

  • `cargo test --lib` — 220 green (5 new in `src/campaign/`).
  • `cargo clippy --all-targets -- -D warnings` — clean.
  • `cargo fmt --all` — clean.
  • End-to-end CLI smoke: `campaign register-pr` → `campaign dismiss` → `campaign status` round-trip prints the expected Markdown tracker.

Refs #33. Stacked on #55 (S1).

🤖 Generated with Claude Code

hyperpolymath and others added 2 commits May 26, 2026 12:30
Adds a per-WeakPoint hexad path to persist_assemblyline_report so a
batch scan can persist one hexad per finding in addition to the existing
aggregate hexad. Subject identity is `finding:<repo>:<file>:<line>:<category>`,
chosen for cross-run stability so the upcoming S2 (campaign register-pr)
and S3 (query) slices can join on it without diffing JSON.

New public surface:
- HexadSemantic gains an optional `finding: Option<FindingSemantic>`
  (additive, skip_serializing_if = none → existing consumers unaffected).
- FindingSemantic carries finding_id / repo / file / line / category /
  rule_id / rule_name / severity / description / first_seen_run /
  last_seen_run / framework. rule_id and rule_name reuse the canonical
  SARIF mapping (sarif.rs::rule_id / rule_name now pub(crate)).
- build_finding_hexads(report) -> Vec<PanicAttackHexad>.
- STORE_FINDING_HEXADS_ENV = "PANIC_ATTACK_STORE_FINDING_HEXADS" — when
  set non-empty AND StorageMode::VerisimDb is configured,
  persist_assemblyline_report writes one file per finding under
  `<dir>/hexads/findings/`.

Behaviour preserved:
- Default path unchanged (env var off → no per-finding writes).
- Aggregate hexad still emitted in every VerisimDb run.
- Suppressed WeakPoints are skipped, keeping the store aligned with
  fleet/CI counts.

S1 sets first_seen_run == last_seen_run; back-stamping from a prior
hexad is S2's job (per the issue), not S1's.

Tests: 7 new (id stability, category discrimination, count per WP,
suppression skip, canonical rule_id/name, file write + round-trip,
env-var default-off). Full suite: 215 lib + 13 + 16 + 6 + 12 + 3 + 7
+ 12 + 14 + 20 + 10 + 8 + 22 + 22 + 12 + 2 doc — all green. Clippy
clean with -D warnings.

Refs #33.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the second slice of issue #33: a panic-attack campaign subcommand
that tracks the lifecycle of individual findings produced by the
assemblyline per-finding hexad path (S1). State is persisted as
campaign-facet hexads written under <dir>/hexads/campaign/, indexed by
finding_id, append-only — the current state per finding is the newest
campaign hexad with that finding_id as subject.

New surface:

- HexadSemantic gains `campaign: Option<CampaignSemantic>` (additive,
  skip_serializing_if = none).
- CampaignSemantic { finding_id, state, pr_url?, reason?, last_polled? }
  — state is a free-form String so future labels can be added without
  a schema bump.
- storage: build_campaign_hexad / write_campaign_hexad /
  load_{finding,campaign,aggregate}_hexads helpers.
- src/campaign/ module — register_pr, dismiss, current_state,
  status_markdown.
- panic-attack campaign register-pr|dismiss|status — CLI surface.

`status` renders a Markdown tracker matching the shape of the issue #32
manual checklist: summary line, table with finding-id, repo, rule_id,
location, state, PR link (or dismissal reason), last-event timestamp,
checkbox column.

Out of scope (S2b): poll subcommand that queries GitHub for PR-state
transitions. The data path is in place — the polling logic lands once
the rate-limit / pagination shape is settled.

Tests: 5 new in src/campaign/ (register, dismiss-overrides-open,
empty-arg rejection, empty-store status, two-row render). Full lib
suite: 220 green. Clippy clean with -D warnings. End-to-end CLI smoke
test green: register-pr + dismiss + status round-trip prints the
expected markdown.

Refs #33. Stacked on #55 (S1) — diff against main includes the S1
changes until S1 lands; this PR will rebase clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath hyperpolymath enabled auto-merge (squash) May 26, 2026 11:44
hyperpolymath added a commit that referenced this pull request May 27, 2026
## Summary

Adds `panic-attack sweep-tracker` subcommand — an issue-#32-shaped sweep
tracker derived from the per-finding (issue #33 S1) and campaign-state
(issue #33 S2) hexad stores. Complements (does not replace) the existing
per-finding `campaign status` table.

Distinct from `campaign status`:
- **Hierarchical**, not flat — grouped by repo and/or by category.
- **Estate summary** header: total findings, repos, criticals, highs,
  PR-filed, dismissed, open-no-PR.
- **Always sourced from the finding store**: a finding with no campaign
  hexad still appears (state `open`); `campaign status` shows only
  rows with a campaign event.

### CLI

```
panic-attack sweep-tracker [--verisimdb-dir DIR] [--output FILE]
                          [--by-repo | --by-category]
```

No flag = both sections. `--by-repo` / `--by-category` select one
section only (mutually exclusive via clap arg group).

### Output shape

```
# Estate sweep tracker

_Generated <ISO>_

**Estate summary**: N findings across R repos (C critical, H high).
M PR-filed, D dismissed, U open (no PR).

## By repo

### alpha (2 findings, 1 critical)
- [x] PA001 src/lib.rs:23 — pr-merged ([#42](https://github.com/...))
- [ ] PA004 src/ffi.rs:7  — open
...
```

### Determinism

- Repos sorted alphabetically.
- Findings within each repo sorted by `(rule_id, file, line,
finding_id)`.
- Categories sorted by `rule_id`.

## Implementation

- New module `src/sweep_tracker/` with public `render_report(base_dir,
shape)` and `ReportShape::{ByRepo, ByCategory, Both}` (default `Both`).
- Reuses `storage::load_finding_hexads` /
`storage::load_campaign_hexads`
  — no new I/O paths.
- New CLI variant `Commands::SweepTracker` wired in `src/main.rs`.
- 7 unit tests (1 extra above the spec floor of 5):
  empty-store, by-repo grouping, by-category grouping, campaign-state
  join (open / pr-merged / dismissed), deterministic ordering,
  both-shape ordering, PR-number label parser.

## Notes on base

This PR depends on issue-#33 S1 (`feat/issue-33-s1-finding-hexads`,
PR #55) and S2 (`feat/issue-33-s2-campaign-state`, PR #56) for the
loaders and `CampaignSemantic` type. Branched off the S2 tip so the
diff is minimal; base is `main` as standing policy. Once #55 and #56
land this PR's diff will narrow to just `src/sweep_tracker/` plus the
small wiring delta in `src/lib.rs` + `src/main.rs`.

## Test plan

- [x] `cargo test --lib` — 227 tests pass, including 7 new sweep_tracker
tests
- [x] `cargo clippy --all-targets -- -D warnings` clean
- [x] `cargo fmt --all -- --check` clean
- [ ] Smoke-test against a real `verisimdb-data/` produced by an
  assemblyline run with `PANIC_ATTACK_STORE_FINDING_HEXADS=1`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 27, 2026
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>
hyperpolymath added a commit that referenced this pull request May 27, 2026
## Summary

Third slice of
[#33](#33). Adds a
\`panic-attack query\` subcommand that evaluates a small S-expression
query language over the per-finding hexads from #55 (S1) joined with the
campaign-state hexads from #56 (S2).

> Stacked on #56 — diff against \`main\` includes the S1+S2 changes
until those land; this PR rebases cleanly.

## Supported forms (S3 initial)

\`\`\`scheme
(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
\`\`\`

Default output: fixed-width table. JSON via \`--format json\`.

## Deferred to S3 follow-ups

Three follow-ups will land in the next PRs in this stack:
- \`(crosslang :from FFI :to ProofDrift)\` — needs integration with
\`src/kanren/crosslang.rs\`.
- \`(diff :since <date> :category <X>)\` — needs an explicit
baseline-run cursor.
- \`panic-attack campaign poll\` (was S2 scope cut) — GitHub PR-state
polling.

## Implementation notes

- Small hand-rolled S-expression tokenizer/parser (~170 LOC) — 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.
\`(pr-state ...)\` is a free clause inside \`and\`/\`or\` rather than a
special case.

## Test plan

- [x] \`cargo test --lib\` — 239 green (19 new in \`src/query/\`).
- [x] \`cargo clippy --all-targets -- -D warnings\` — clean.
- [x] \`cargo fmt --all\` — clean.
- [x] End-to-end CLI smoke: hand-crafted finding hexad + \`campaign
register-pr\` + \`query (and (category UnsafeCode) (pr-state pr-filed))
--format json\` returns the expected JSON with the joined campaign
state.

Refs #33. Stacked on #56 (S2) → #55 (S1).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 27, 2026
…ssue #33 S2b)

Closes the S2 follow-up called out in #56. Adds a new subcommand:

  panic-attack campaign poll

For each finding whose latest campaign state is `pr-filed`, the
poller:

1. Parses the stored PR URL into (owner, repo, number).
2. Calls GET https://api.github.com/repos/<o>/<r>/pulls/<n>.
3. Maps the response to RemotePrState::{Open, Merged, Closed}.
4. If the new state differs from `pr-filed`, writes a new campaign
   hexad stamping `last_polled` to the current timestamp.

Auth: reads `GH_TOKEN` or `GITHUB_TOKEN` from the environment. Falls
back to unauthenticated requests (60/hour). Output: one line per
transition.

Implementation:

- New campaign primitives: `transition(finding_id, new_state, pr_url,
  reason, base_dir)` — lower-level than `register_pr`/`dismiss`,
  stamps `last_polled`.
- `parse_pr_url(url) -> ParsedPrUrl` — pure, accepts canonical
  `https://github.com/<o>/<r>/pull/<n>` plus trailing slash and
  fragment.
- `RemotePrState` + `should_transition` — pure mapping helpers that
  decide whether a fetched state warrants a new hexad.
- `poll(base_dir) -> Vec<PollOutcome>` — orchestrator.
- `fetch_remote_pr_state` — single ureq GET with correct headers
  (Accept, User-Agent, X-GitHub-Api-Version).

The whole new section is gated on `#[cfg(feature = "http")]`; the
default `cargo build` is unaffected, and `cargo build --features http`
picks up the new subcommand.

Tests (added under http feature): 11 new — 6 parse_pr_url cases
(canonical, trailing slash, fragment, non-github reject, issue-url
reject, missing-number reject); 4 should_transition cases (open→filed
no-op, filed→merged, filed→closed, merged→merged no-op); 1
transition() writes a new hexad and the latest-state fold sees it.

Full lib suite: 222 green default, 260 green with --features http.
Clippy clean with --features http -D warnings.

Refs #33. Stacked on #56 (S2). Diff against main includes the S2
changes until S2 lands; this PR rebases cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 27, 2026
…ssue #33 S2b) (#60)

## Summary

Closes the S2 follow-up called out in #56. Adds a new subcommand under
\`panic-attack campaign\`:

\`\`\`
panic-attack campaign poll [--verisimdb-dir DIR]
\`\`\`

For each finding whose latest campaign state is \`pr-filed\`, the
poller:
1. Parses the stored PR URL into \`(owner, repo, number)\`.
2. Calls \`GET https://api.github.com/repos/<o>/<r>/pulls/<n>\`.
3. Maps the response to \`RemotePrState::{Open, Merged, Closed}\`.
4. If the new state differs from \`pr-filed\`, writes a new campaign
hexad stamping \`last_polled\` to the current timestamp.

Auth: reads \`GH_TOKEN\` or \`GITHUB_TOKEN\` from the environment. Falls
back to unauthenticated (GitHub 60/hour cap). Output: one line per
transition.

> Stacked on #56 — diff against \`main\` includes the S2 changes until
S2 lands; this PR rebases cleanly.

## New surface

| Item | Purpose |
|---|---|
| \`transition(finding_id, new_state, pr_url, reason, dir)\` |
Lower-level than \`register_pr\`/\`dismiss\`; stamps \`last_polled\`. |
| \`parse_pr_url(url) -> ParsedPrUrl\` | Pure parser; accepts canonical
PR URL + trailing slash + fragment. |
| \`RemotePrState\` + \`should_transition\` | Pure helpers — decide
whether a fetched state warrants a new hexad. |
| \`poll(base_dir) -> Vec<PollOutcome>\` | Orchestrator. |
| \`fetch_remote_pr_state\` | Single ureq GET with correct headers
(\`Accept\`, \`User-Agent\`, \`X-GitHub-Api-Version\`). |

## Feature gating

The whole new section is \`#[cfg(feature = "http")]\`. Default \`cargo
build\` is unaffected. \`cargo build --features http\` picks up the new
subcommand.

## Test plan

- [x] \`cargo test --lib\` — 222 green (default).
- [x] \`cargo test --lib --features http\` — 260 green (11 new poll
tests).
- [x] \`cargo clippy --all-targets --features http -- -D warnings\` —
clean.
- [x] \`cargo fmt --all\` — clean.

11 new tests cover:
- 6 \`parse_pr_url\` cases (canonical, trailing slash, fragment,
non-github, issue-url, missing-number).
- 4 \`should_transition\` cases (open→filed no-op, filed→merged,
filed→closed, merged→merged no-op).
- 1 end-to-end \`transition()\` + latest-state fold.

Refs #33. Stacked on #56 (S2).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hyperpolymath added a commit that referenced this pull request May 27, 2026
… follow-up) (#61)

## Summary
- Adds a `HexadSemantic.crosslang: Option<CrosslangSemantic>` facet and
a
  `build_crosslang_hexads(...)` helper that drives the kanren
`CrossLangAnalyzer` per repo (ingest → extract → load_rules → analyze →
  query_interactions) and emits one hexad per derived
  `CrossLangInteraction`.
- New env var `PANIC_ATTACK_STORE_CROSSLANG_HEXADS` (separate from
  `PANIC_ATTACK_STORE_FINDING_HEXADS`) opts a run into emission;
  `persist_assemblyline_report` writes to `<dir>/hexads/crosslang/`
  file-side only.
- Adds `load_crosslang_hexads(base_dir)` so the paired query-evaluator
PR
  can match against persisted facts; falls back to empty `Vec` when the
dir is missing (the evaluator treats that as "use co-occurrence proxy").

## Why
Tightens the `(crosslang :from :to)` query from a same-repo
co-occurrence
proxy to a true FFI/cross-language reachability check against
persisted kanren-derived facts. PR 1 of a 2-PR stack; PR 2 switches the
evaluator over while preserving fall-back semantics.

## Test plan
- [x] `cargo test --lib` — 252 tests pass, including 4 new
      `storage::tests::*crosslang*` cases (build-empty, build-from-FFI,
      write/read roundtrip + missing-dir, env-var default-off + opt-in).
- [x] `cargo clippy --all-targets -- -D warnings` clean.
- [x] `cargo fmt --all` no diff.

Stacks under: issue #33 S1/S2/S3 PRs (#55, #56, #57, #58). Filed against
`main` per orphan-trap rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hyperpolymath
Copy link
Copy Markdown
Owner Author

Superseded — content already absorbed into main via #62 (sweep-tracker PR branched off S2 tip and carried the S2 campaign scaffold into main directly). CampaignSemantic / build_campaign_hexad / register_pr / dismiss / status_markdown are all present in main as of 09b80f4. The S2b poll subcommand landed separately via #60. Closing as no-op rebase target.

auto-merge was automatically disabled May 27, 2026 13:30

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant