Live demo → govconradar.streamlit.app — no login, no setup.
· 118 data-integrity checks · all views boot · Scorer v2.0.0 · Python 3.12+
Find the DoD cyber/IT contracts coming up for recompete — and know which numbers you can defend.
This is a screenshot of the deployed app taken on 2026-07-25, not a mock-up. The SAMPLE DATA
badge is load-bearing: the Streamlit deploy has no data/powerbi/, so it boots on the committed
5,000-row data/sample/ bundle — these KPIs are the sample's, not the full snapshot's quoted below.
Runway is recomputed to today on every load, which is why the live Tier-1 count (35 here) differs
from the 37 baked into the sample.
An ETL + BI pipeline over public USAspending.gov and SAM.gov data that finds expiring DoD
cybersecurity/IT contracts, estimates their recompete windows, scores each for pursuit fit against
your company profile, and ships the result as a Power-BI-ready CSV star schema and a Streamlit
app. (The Power BI report itself is authored in the private build repo; this public repo ships the
schema the report consumes, not the .pbip.)
The product's brand is honesty. Every number on screen is built to survive scrutiny from someone who does capture management for a living. Facts are labeled facts; estimates are labeled estimates; and records the data can't stand behind are quarantined, not dressed up as leads.
Explore: Query the data model in SQL · ETL mini-pipeline · Screens · Docs
From the current public snapshot (2026-07-15, spanning FY2019–FY2026 award history), computed —
never hardcoded — and enforced by scripts/validate_data.py:
- 5,712 active recompete candidates (~$49.4B) across 1,492 contract vehicles,
- plus 859 recently-expired (≤90 days) grace candidates, flagged to verify on SAM.gov,
- and 29,393 historical/long-expired records (~$171B) quarantined for verification — the FY2019–FY2026 award history that enriches vendor books, price comparables, and tenure signals, but is excluded from every headline, chart, and default export, reachable only via the "Needs verification" surfaces. (The Data-Gap tier holds 29,400 rows: these plus garbled-but-active records.)
- 197 candidates carry FPDS termination evidence (new in 2.3.0): 80 likely-complete
terminations had their expiration retargeted to the termination date (
expiration_date_basis = "terminated") so an ended contract can no longer ride the forward pipeline as a live lead.
These figures are computed from the full public snapshot (~238K awards). The repo ships a deterministic 5,000-row sample (
data/sample/) so it clones and runs with zero setup; fetch the full snapshot withpy scripts/download_data.pyto reproduce the headline numbers locally.
An earlier version put 118 contracts in Tier 1 — but about a third of the candidate list was
already expired (one had ended in 2003), because the scorer gave any expired contract maximum
urgency. v2.0.0 fixed that, and its entry in CHANGELOG.md is the receipt: 34% of
candidates (1,581) expired, Tier 1 dropped 118 → 26 at that release. On this snapshot the
tier count is recomputed, not inherited: Tier 1 = 37, and the average data-quality score is an
honest 55.1 where the old logic scored a fake 100.0.
- Graduated expired-record policy — active / expired-≤90d-grace / expired-stale, with stale records forced to a Data Gap quarantine tier. Runway is recomputed to today on every load.
- Score-as-your-company — enter your NAICS/PSC/capabilities/past-performance and watch all ~36,000 scores and the tier board recompute live; the labeled demo baseline is kept for before/after deltas.
- Competitive Price Range, not "price-to-win" — a range of what comparable work has historically been won for (a fact), which refuses to estimate below a comparables floor rather than inventing a number. Competitor bids are never public, and the tool says so.
- Contract-vehicle rollup — up to 1,211 identical-looking task orders under one IDV collapse to a single vehicle row you can actually pursue.
- Forward signals beside the score, never inside it — an Incumbent Displacement lane
("k of N observed signals": bridge extension, termination, large deobligation,
lapsed-with-no-visible-successor, sole offer, incumbent size-standard shift) with its own sort
lens on the chase list, and a gate/warn/unknown/clear "Prime path" eligibility verdict
beside every score — with a hard ⛔ warning on Contract Detail when a live set-aside
solicitation excludes priming.
pursuit_scorestays byte-identical — validator-pinned. - Auditable by construction —
scripts/validate_data.pyre-derives every KPI from the fact tables and proves the app's live re-score reproduces the baked data (max diff 0.0). It runs in CI.
GovConRadar exists to reduce the analyst hours needed to build and maintain a trustworthy DoD IT/cyber recompete pipeline. It does not predict who wins — it finds the recompetes worth an analyst's hours and shows the evidence for each.
Accuracy claims are gated, not asserted: link precision per confidence tier and top-50 outcome
precision publish only after hand-labeled samples cross the pinned thresholds in
config/measurement.yaml (≥30 labels per link tier; ≥40 determinable outcome labels on the
disclosed top-50 sample), always with their n and a 95% Wilson interval. Until then the page reads
"not yet measured" — that refusal is the feature.
No recall number is published, ever. Notice linkage is thin — on the committed sample bundle, 192 of 5,000 candidates (under 4%) carry an established link to a SAM.gov notice — so the set of recompetes this radar missed is structurally unobservable in public data; any recall claim would be fiction.
Three design principles, stated about market patterns rather than named products:
| The common pattern | What ships here instead |
|---|---|
| A single black-box opportunity score | Eight decomposed components in fact_scoring_breakdown, reason chips with named bases, and a public source link on every row |
| A price estimate for every record, no matter how thin the data | A comparables floor that refuses (ptw_basis="insufficient") — and the same coverage gates on every mod/termination signal |
| Missing data quietly filled with a plausible middle | Unknown as a first-class answer: renormalized weights, a visible Data-Gap quarantine tier, and validator-enforced "an Unknown can never carry a number" invariants |
All three are enforced by scripts/validate_data.py on every published bundle — testable claims,
not marketing (see docs/methodology_notes.md → How we compare).
# Windows: use the py launcher (the Store 'python' is a broken stub)
py -m pip install -r requirements-dev.txt
# Run the app. On a fresh clone it boots on the committed data/sample/ subsample —
# no pipeline, data download, or API key needed.
py -m streamlit run streamlit_app/app.py
# Optional: pull the FULL snapshot for local full-data dev (not committed; the
# published asset is a 48,069,953-byte zip that expands to ~485 MB of CSV)
py scripts/download_data.py # -> data/powerbi/ (then the app reads "live")
# Verify the shipped bundle — the exact gates CI runs on every push
py scripts/validate_data.py --sample # 118 data-integrity checks over data/sample/
py scripts/check_doc_counts.py # pinned prose numbers vs generated values
py scripts/smoke_app.py # boots every app view on the sample
# The full snapshot runs the SAME contract — and currently does not pass it. The
# published data-snapshot-2026-07-15 asset predates the 2.8.0 bridge re-bake, so
# invariant 14 (established-link recency) fails on it; see CHANGELOG 2.8.0.
py scripts/validate_data.py # data/powerbi/ (SKIPs if absent)Which data the app uses (streamlit_app/components/data.py::resolve_data_dir), in order:
$RADAR_DATA_DIR (explicit override) → data/powerbi/ (the full snapshot, if present locally) →
data/sample/ (the committed seeded subsample default) → streamlit_app/assets/sample_data/ (legacy
synthetic bundle). The full snapshot is not committed as part of the data diet — fetch it with
py scripts/download_data.py; a fresh clone runs on data/sample/.
The full ETL pipeline (run_pipeline.py, private — not shipped in this public repo) needs local bulk
CSV exports; the app and validators run without it against the shipped star schema.
The same star schema the app and Power BI read is queryable directly with DuckDB — no database
to stand up. run_sql.py registers every table (Parquet-preferred, CSV fallback) as a view over the
resolved data dir (the same $RADAR_DATA_DIR → data/powerbi/ → data/sample/ order the app uses), so
on a fresh clone it runs against the committed data/sample/ subsample out of the box:
py run_sql.py sql/01_recompete_expiring_next_12mo_by_naics.sql # pretty table
py run_sql.py sql/04_competitive_price_range_by_psc.sql --csv # CSV to stdoutThe sql/ pack is six analyst queries, each headed by its business question, the tables it
touches, and a FACTS vs ESTIMATES caveat. Collectively they exercise CTEs, fact↔dimension joins,
RANK/LAG window functions, and GROUP BY … HAVING:
| File | Business question | Technique |
|---|---|---|
01_recompete_expiring_next_12mo_by_naics.sql |
Recompete pipeline expiring in the next 12 months, by NAICS | CTE · join · GROUP BY |
02_incumbent_concentration_by_dod_component.sql |
Top-incumbent share of each DoD Component's pipeline | RANK/SUM window |
03_pursuit_score_distribution_by_tier.sql |
Pursuit-score spread across priority tiers | join · GROUP BY |
04_competitive_price_range_by_psc.sql |
Competitive Price Range — historical comparable won ranges by PSC | GROUP BY · HAVING (min-sample) |
05_yoy_obligation_trend.sql |
Year-over-year obligation trend | LAG window |
06_pipeline_mix_by_fiscal_quarter.sql |
Expiring-vs-active pipeline mix by fiscal quarter | join · running-total window |
Query 04 is a Competitive Price Range, not a price-to-win — every dollar is an actual obligated (won) amount on a historically comparable award, and the
HAVINGclause refuses to publish a range below a minimum comparable count rather than inventing a number. Competitor bids are never public; the query never predicts one.
Example — the full text of query 01 and its actual output on the committed data/sample/ subsample
(a seeded 5,000-candidate slice; the full snapshot returns far larger counts). The block below is
pasted verbatim from a run on 2026-07-25 against this commit's data/sample/ — re-run the
command and you should get the same table:
-- sql/01_recompete_expiring_next_12mo_by_naics.sql
WITH expiring_soon AS (
SELECT
CAST(naics AS VARCHAR) AS naics_code,
candidate_id,
total_obligated_amount
FROM fact_recompete_candidates
WHERE candidate_status = 'active'
AND days_until_expiration BETWEEN 0 AND 365
)
SELECT
e.naics_code,
n.naics_description,
COUNT(*) AS candidates_next_12mo,
ROUND(SUM(e.total_obligated_amount) / 1e6, 2) AS pipeline_obligated_musd
FROM expiring_soon e
LEFT JOIN dim_naics n
ON CAST(n.naics_code AS VARCHAR) = e.naics_code
GROUP BY e.naics_code, n.naics_description
ORDER BY candidates_next_12mo DESC, pipeline_obligated_musd DESC;$ RADAR_DATA_DIR=data/sample py run_sql.py sql/01_recompete_expiring_next_12mo_by_naics.sql
[run_sql] 01_recompete_expiring_next_12mo_by_naics.sql | data=data\sample (mode=custom) | 17 tables registered
┌────────────┬────────────────────────────────────────────────────────────────────────────────────────┬──────────────────────┬─────────────────────────┐
│ naics_code │ naics_description │ candidates_next_12mo │ pipeline_obligated_musd │
│ varchar │ varchar │ int64 │ double │
├────────────┼────────────────────────────────────────────────────────────────────────────────────────┼──────────────────────┼─────────────────────────┤
│ 541519 │ Other Computer Related Services │ 178 │ 996.53 │
│ 541512 │ Computer Systems Design Services │ 125 │ 1189.72 │
│ 541511 │ Custom Computer Programming Services │ 55 │ 434.49 │
│ 518210 │ Computing Infrastructure Providers, Data Processing, Web Hosting, and Related Services │ 22 │ 546.03 │
│ 541513 │ Computer Facilities Management Services │ 19 │ 174.19 │
│ 541330 │ Engineering Services │ 16 │ 334.02 │
│ 541611 │ Administrative Management and General Management Consulting Services │ 4 │ 31.09 │
└────────────┴────────────────────────────────────────────────────────────────────────────────────────┴──────────────────────┴─────────────────────────┘
naics_description comes from the dim_naics join; pipeline_obligated_musd is a fact (FPDS
obligation), while the 12-month expiry window is an estimate recomputed to today (only
candidate_status = 'active' rows count — expired-grace/-stale records are held out for verification).
Each addition refuses to guess where the public data won't support a claim — the whole brand, made visible.
2.4.0 → 2.8.0 — forward signals beside the score, never inside it (2026-07-15/16):
pursuit_score, the weights, and every tier are byte-identical across all five releases
(validator-pinned) — each read below is a separate surface or ordering lens next to the number:
- Incumbent Displacement lane — "k of N observed signals" over six public forward signals (bridge extension, termination on record, large deobligation, lapsed-with-no-visible-successor, sole offer at the last competition, incumbent size-standard shift), surfaced as a Contract Detail panel, a reason chip, a compact Explorer column, and a sort lens ("Displacement signals, then score") on the chase list. An ordering choice, never a score change — lane-unreadable rows sort last, never imputed to zero.
- Eligibility beside the score — the score's
set_aside_fitcomponent rewards restricted work regardless of who can prime it, so the real gate/warn/unknown/clear verdict now travels with the number: a "Prime path" column beside Score in Explorer, and a hard ⛔ warning on Contract Detail when a live set-aside solicitation excludes you as prime. A gate, not a score. - Market concentration joined to the brief — the capture brief's Office section reads the buying component's incumbent concentration (the top incumbent's share of attributed expiring obligated dollars — a dollar-share, not market power) or a named refusal where the market is too thin to read.
- Honest link coverage — the candidate→notice linker is now recency-gated (a recompete notice posts near the incumbent's expiry, never years before it) and an award's own origin solicitation can never pose as its successor. The gate cost real coverage and kept the survivors honest: on the committed sample, 192 of 5,000 candidates (under 4%) hold an established link, and validator invariant 14 fails the bundle if any of them pairs a notice with a candidate outside the recency window. (Coverage counts are per-bundle — read them off whichever bundle you loaded, never off this sentence.) Link precision itself still reads "not yet measured": a bulk-fill guard now refuses mechanically-filled label sheets, and outcome labels are drawn at contract-vehicle grain over a FY2019–FY2021 cohort old enough for recompete outcomes to actually be observable.
2.3.0 — "keep the mods" (see CHANGELOG.md and docs/DATA_DICTIONARY.md):
the pipeline now keeps each award's modification history instead of collapsing it away.
Terminated contracts stop ghost-riding the forward pipeline (Terminated (verify) badge,
expiration_date_basis="terminated"); new coverage-gated signals — mod velocity,
ceiling-balloon, deobligation, bridge-extension, successor-visible (the recently-lapsed
"bridge watch" lens), and an incumbent size-determination shift flag — each with a named
basis and an unforgeable Unknown; plus a populated fact_transactions evidence table, digest
delivery (email/webhook), a CRM lead export, and a Sources Sought early-warning lane. Every
mods-derived surface carries the disclosure: "DoD FPDS reporting lags ~90 days; termination
signals are ≥3 months old."
Methodology: weights, tiers, and honesty rules on one page.
(The capture at the top of this README was shot from the live deploy with Playwright on 2026-07-25.
The gallery shots below it were generated per docs/screenshots/SCREENSHOTS_TODO.md,
which carries a deep-link URL, target filename, and viewport for each — a shot's on-screen as of
badge is the honest record of which bake it came from.)
- Architecture — data flow from USAspending/SAM.gov → star schema → Streamlit app (+ a Power BI report in the private build repo)
- Standard Operating Procedure (v2.1) — controlled refresh/validate/deploy runbook (supersedes the earlier informal SOP)
- Data dictionary — every table/column, with the v2 additions and legacy notes
- Data provenance — where the numbers come from; sample-data synthesis
- Demo script — a 5-minute buyer walkthrough with the three "aha" beats
- Changelog · Methodology · in-app Methodology page
The integrity contract is code, not prose:
py scripts/validate_data.py && py scripts/validate_data.py --sampleIt fails loudly on any of: scorer non-parity, a stale record in Tiers 1–4, an expired row in a forward
bucket, a KPI that doesn't tie to the facts, an unflagged garbled title, a title_display that leaks a
raw record, a baked lane read (burn pace, displacement, concentration, trust metrics) that disagrees
with a fresh recompute or forges an Unknown, an established notice link posted outside the recency
window, a schema violation, or a snapshot/version mismatch.
I specified this product, cut it into gated slices, and verified each one; AI agents wrote most of
the line-level code, and the Co-Authored-By trailers in this history name the models.
Nothing merged until the gate ran clean. .github/workflows/ci.yml runs, on every pull request: an
import-walk over every src/**/*.py, then python scripts/validate_data.py --sample, python scripts/check_doc_counts.py, python scripts/smoke_app.py, the six-query DuckDB pack, the offline
ETL demo, an advisory dependency audit, and a blocking gitleaks scan. Adversarial review passes read
the work after it landed; their corrections arrive as follow-up commits — git log shows later
commits rewriting numbers earlier ones shipped.
The repo records its own reasoning. radar_handoff.py takes the export's ceiling from
potential_value, never from the invitingly-named base_and_all_options_value: on the full public
snapshot that column is ≤0 on 22,436 of 35,964 candidate rows (62.4%), against 160 (0.44%)
for potential_value. src/scoring/mods_signal.py dates its real semantics — a per-transaction
delta, not a cumulative ceiling — to a 2026-07-12 measurement, before the producer existed.
The history shows the co-authorship directly. The judgment calls — the slice boundaries, what to refuse to compute, which number to delete, the decision to run an adversarial pass at all — are the part worth evaluating.
This repo is the analytics product: a static, periodically-refreshed public-data snapshot deployable to Streamlit Community Cloud (no auth, no database, no runtime API calls). Accounts, alerts, and scheduled refresh belong to a separate private SaaS and are out of scope here.











