Campaigns

Create and run full, incremental, and retest campaigns in TheSpider

A campaign is one audit run against a specific git state: a branch, a commit, and a remote. Campaigns are created by a developer's run, by a forge webhook (PR opened/synchronized), or as a retest of existing findings. Each campaign pins a config_version so in-flight work renders from the methodology it started with.

Choose a campaign type

TypeWhen to useScope
fullfirst scan of a repo, or a periodic re-baselineevery bucket × every pass × every model
incrementala PR / branch — only what changedbuckets/passes whose scope intersects changed_paths + risk-trigger files
retestverifying a fix landeda single finding's bucket + the passes whose scope matches its files

Create a campaign

From the agent (full)

thespider-agent run without --campaign creates a full campaign from local git detect — the client detects branch/commit/remote_url/dirty and the server records it.

From the API (any type)

POST /v1/projects/{id}/campaigns
     { type, git:{branch,commit,remote_url,dirty}, base_commit?, changed_paths?, config_version? }
     → { campaign_id, config_version_id, status, reused }
The server has no source tree. For incremental scans the client must send changed_paths (file paths, not contents). The server intersects them with pass-scope globs + risk triggers — paths travel, contents never do.

Dedup identity is keyed by (project, branch, commit, type), so re-creating the same campaign reuses the existing one instead of duplicating work.

Scope an incremental campaign

Incremental targeting is decided server-side in campaign planning, fed by client-supplied changed_paths. It ports two rules verbatim from the original engine:

  1. Pass-scope glob matching — a pass only runs on a changed file if the pass's scope globs match it.
  2. Global risk triggersDockerfile, .env*, lockfiles, *.config.*, and similar risk-pattern files expand scope to all passes, regardless of the pass's own scope.

This is why a one-line .env change can fan out into a broad re-scan, while a docs-only change runs nothing.

Understand finding identity across campaigns

Fingerprint is the fast path, not the identity. Ingest resolves identity in three steps:

  1. Alias-table hit on (project_id, fingerprint)KNOWN: record a sighting, inherit current triage status (a won't-fix on main stays suppressed on a PR branch).
  2. Miss → candidate dedupe project-scoped, with bounded candidates and threshold gates. If adjudicated duplicate (heuristic or AI) → insert an alias row mapping this fingerprint to the canonical finding + record a sighting. This absorbs renames and wording drift, which change the fingerprint. For entitled orgs, AI adjudication batches the finding's eligible candidate pairs into model requests of up to AI_DEDUPE_BATCH_SIZE pairs (defective entries are retried individually); each pair's decision is cached individually and reused forever.
  3. Still no match → NEW finding + its birth alias row.

Merges repoint alias rows to the keeper and set merged_into_finding_id (a soft merge — never DELETE), so a merged-away fingerprint re-imported later lands on the keeper deterministically.

Run a retest

Retest is a targeted verification campaign, not a naive re-scan. It is scoped to the finding's bucket + the passes whose scope matches its files, gated on HEAD matching the expected fix commit.

# server-created retest campaign; the agent claims it like any other work
thespider-agent run --campaign <retest-campaign-id> --once

Known nuance: fixed/regressed is decided by reappearance. Near-matches route through the dedupe layer before concluding "fixed" (so wording drift doesn't masquerade as a fix), and for high-severity findings you may require N consecutive clean runs. Absence only counts as evidence when the retest scope actually covered the finding — which the bucket/pass targeting guarantees.

Next steps

  • Triage — work with findings in the web UI.
  • PR gate — turn campaigns into merge gates.