Provider Research Factory
Turns a population query into a verified national dataset behind two human approval gates.
- Python 3.12
- Stdlib streaming CSV
- openpyxl, optional
- Claude Code subagents
- Status
- Version 1 delivered. Three national datasets built and signed off at GATE D, urology (the reference build, reproduced end to end), dentistry and cardiology, with a fourth run directory present as a second urology cut. Build-order steps 4 to 6 (single outbound channel with measurement, email enrichment with recorded consent basis, measurement feedback loop) are deliberately unbuilt; the repo contains no send path. Last pushed 1 September 2026.
By the numbers · 11
9,835,402
Registry rows streamed per national build
409,848 / 73,394 / 22,195
Records delivered across the three national datasets
11
assertion functions in the deterministic pipeline
286
Assertions in the synthetic end-to-end smoke suite
7 lenses x 3 verifiers, 2-of-3 to survive
Adversarial QA protocol
75
Committed QA artifacts across runs (64 for the cardiology run alone)
24 removed, 0 by the standalone report
Deactivated records caught by the corpus scrub that the official snapshot missed
16 of 16 absent, 7 of 7 control matched
Independent verification of scrubbed records against the live NPI Registry
30 codes admitted, 5 cardiology
Measured cost of a prefix filter over the controlled vocabulary
40,643 records (74.4%)
Headline records silently dropped by the most natural text filter
13,562 excluded, 273 surviving, 0 in headline
Enforced cost of the adjacency decision on the cardiology run
Summary
A query-to-dataset factory for professional-population research, built on the Puffo agent platform. A population query ("All urologists in the United States") is parsed into a frozen target spec, researched by parallel judgment agents, approved by a human at a source gate, then built by deterministic Python that streams the ~9.8M-row NPPES national registry, applies an exact-match taxonomy code set, joins CMS enrichment sources under hit-rate bands, normalizes, and packages CSV splits plus a styled workbook. An adversarial QA panel of seven lenses attacks the built dataset; each finding goes to three independent verifiers and survives only on two confirmations. A second human gate signs the deliverable before anything is called delivered. An optional marketing layer runs on an allowlist-derived view from which contact channels the compliance gate permanently blocks have been structurally stripped, and the system contains no send path at all, deliberately, and by build order.
The problem
Buying a professional contact list means buying someone else's undocumented selection rule. The failure modes are quiet: a specialty filter that substring-matches the wrong specialty, an enrichment join that silently returns empty, a status flag that misses providers who have since been deactivated. None of these announce themselves, the file still opens, the counts still look plausible, and the error only surfaces after the list has been used. For regulated outreach to clinicians the exposure compounds: contacting a wrong or dead record is a data-quality problem, contacting them on a legally blocked channel is a statutory one. The requirement was a dataset whose selection rule, provenance and limits are all reconstructible, and an outreach layer that cannot reach a blocked channel even by mistake.
Approach
Fix the doctrine before the code: METHODOLOGY.md is a literal record of one hand-built dataset, every command, every dead end, all three defects, and the system is derived from it. The README states that when the two disagree, the methodology wins and the README has the bug.
Split judgment from mechanics. Models are used in exactly four places (source discovery, ontology resolution, adversarial QA, marketing content); fetch, filter, join, normalize and package are deterministic Python, because a deterministic stage can be asserted against.
Two-layer agent definition: agents/*.md holds the doctrine brief, .claude/agents/*.md is thin Claude CLI wiring, puffo/profiles/*.md is the same brief plus a workspace contract for the hosted agent roster. Behavior changes go in the brief, never the wiring.
Place the human gates where they are cheapest. GATE R reviews the source manifest before any large download (a wrong source caught before download costs a review; after, it costs the run). GATE D reviews the finished deliverable before it is called delivered.
Encode the three defects the reference build found the hard way as standing guards: declared-column existence and minimum join hit rate, exact-match code sets only, and an independent cross-check against the authoritative deactivation list.
Turn QA into an adversarial protocol rather than a checklist: seven single-lens critics in parallel, each finding sent to three verifiers prompted to refute it, surviving only on 2-of-3, defaulting to refuted under uncertainty, looping until two consecutive clean rounds.
Make the QA panel own the blind ontology critic and run it by default, because a deriver who commissions their own critic chooses its framing, inputs and timing, every degree of freedom the check exists to remove.
Make the compliance boundary structural: marketing/extract.py builds an allowlist view, so the fields the gate permanently blocks do not exist in the namespace a send decision would run in.
Write no send path. Build order puts single-channel outbound at step 4, behind the compliance gate at step 3; steps 4-6 are unbuilt on purpose and the repo makes premature sending impossible by not containing it.
Commit run identity, not run payload: spec, sources, manifest, QA findings and documentation are versioned; the CSV deliverables, raw downloads, source cache and suppression store are not.
Architecture
A population query enters an orchestrator session, which parses it into a target spec and either reuses a proven spec or fans out three to four source-discovery agents with different framings, one ontology agent and one schema probe, merging their output into the run's spec.json and sources.json.The run halts at GATE R for human approval of sources, code set and disk budget before any large download.Deterministic pipeline stages then run in sequence, resumable fetch, bulk stream and exact-code filter, enrichment joins under declared hit-rate bands, normalization, with barriers only where a stage genuinely needs the complete prior output.Nine assertions can fail the build at any point, and none can be downgraded to a warning.Deterministic QA runs first, then an adversarial panel of seven single-lens critics in parallel; each finding goes to three verifiers prompted to refute it and survives only on two confirmations, looping until two consecutive clean rounds.Packaging emits CSV splits and a styled workbook, a documentation agent writes the deliverable README, and a deterministic output validator runs last.GATE D puts the finished deliverable in front of a human before it is marked delivered.Only afterwards, and only on request, does the marketing layer build an allowlist view, the step that structurally strips the channels the compliance gate blocks, and run segmentation and explainable prioritization on it.Every list build consults an append-only suppression store, and every channel decision calls the compliance gate, which can only permit or block and never sends.
| Component | Role |
|---|---|
| CLAUDE.md + agents/00-orchestrator.md | Orchestrator doctrine: runs the state machine, spawns judgment subagents by type, executes pipeline scripts rather than doing their work in-model, and stops at both human gates. Explicitly forbidden from weakening an assertion or phrasing a gate so that silence reads as consent. |
| agents/01-06 briefs | One model-tier brief per judgment role, source discovery, ontology, schema probe, QA panel, documentation, marketing. These are the doctrine layer; .claude/agents/*.md is thin wiring that points at them. |
| puffo/profiles/*.md | Seven hosted-agent profiles for the Puffo roster, each the repo brief plus a workspace contract (repo root, per-agent write scope, no push) and a use-case isolation rule. |
| pipeline/contracts.py + run.py | Typed stage hand-offs and the stage driver. run.py is the single entry point (python pipeline/run.py <slug> <stage>) and holds the provenance checks that need both the declared-source and fetch-result collections in scope. |
| pipeline/assertions.py | Nine build-failing invariants: declared columns exist, join hit rates inside a declared band, no duplicate keys, no records on any scrub list, expected geographies present, coverage thresholds, no unrendered placeholders, splits partition the master by membership rather than summing to it, inputs identified by sha256, extraction ratio sane. |
| pipeline/fetchkit.py + sourcecache.py | Resumable downloads with archive verification and a true-size probe (Windows reports stale sizes under an open write handle), plus a persistent cross-run cache of upstream archives so a rebuild does not re-download. |
| pipeline/filter_bulk.py + registries/nppes.py | Streams the national registry, selects on exact taxonomy codes across primary and secondary slots, applies newest-wins merging of weekly increments ordered by coverage end rather than filename. |
| pipeline/enrich.py + normalize.py + package.py + manifest.py | Hit-rate-guarded joins, phone/postal/name normalization and derived fields, CSV and workbook packaging with a re-parse cross-check of every written file, and a BuildManifest that is diffed against the previous run of the same spec. |
| pipeline/qa_checks.py | Deterministic QA, integrity, coverage, geography, scrub and placeholder checks, run before the adversarial panel, on the principle that deterministic findings are free. |
| compliance/channel_rules.py | The gate. One contact, one channel, one verdict, with the statute named on every block: fax and SMS and robocall blocked outright, email blocked without a recorded consent basis, phone and physical mail allowed with attached obligations, unknown channels blocked rather than assumed fine. No bypass parameter exists. |
| marketing/extract.py | The structural compliance boundary. Builds an allowlist marketing view, audits the written header against the forbidden-field set, and records provenance including the master and spec hashes it read. |
| marketing/segmentation.py + prioritization.py | Cohort cuts from signals already in the data, and explainable scoring that emits weighted components rather than opaque totals, with a read audit that distinguishes a suppressed axis from a column that silently failed to resolve. |
| marketing/suppression.py | Append-only opt-out / bounce / do-not-contact store that is its own audit log, consulted at list build, and reporting whether the store existed separately from how many keys it held. |
| tests/test_smoke.py | End-to-end run on a synthetic NPPES-shaped world including a substring decoy, exercising filter through marketing and proving the guards fire, a missing declared column, a sub-minimum hit rate, a decoy that must not be selected, a forbidden field that must not survive into the view. |
| runs/{slug}/qa/ | The committed evidence trail: per-finding files, per-round ledgers, pre-registrations written before a re-cut runs, a contamination register frozen before any blind critic is commissioned, and retractions of the panel's own confirmed findings. |
Trade-offs
Chose
Deterministic Python for fetch, filter, join, normalize and package
Over
Agent-driven data handling with an LLM inside the file-streaming loop
README and METHODOLOGY 2.1 state the governing principle directly: a script does mechanical work faster and more reliably than an agent, and a deterministic stage can be asserted against. The README calls an architecture that puts an LLM in the file-streaming loop a design defect.
Chose
Compliance rules as hard-coded functions with no bypass parameter
Over
Compliance stated as instructions in the agent prompts
channel_rules.py opens with the reasoning: a model can be argued out of a prompt, it cannot be argued out of a blocked function call. The module notes that adding a force flag or admin mode is itself the defect.
Chose
Structural stripping, an allowlist marketing view that omits blocked-channel fields
Over
Policy prohibition on using those fields downstream
extract.py states the requirement as architectural, not policy: the forbidden columns must be unable to reach the marketing layer, so no later code path, including one written by someone who never read the methodology, can route them. The gate's own DIRECT branch notes that reaching it means a boundary was already crossed.
Chose
A hit-rate band (declared floor and declared ceiling) on every enrichment join
Over
A minimum hit rate alone
assertions.py records the failure it was written for: a one-sided floor asks whether there is enough coverage, and a fabricated or wrongly-joined column always answers yes. medicare_enrolled read 100% against a source that never ran and defeated every missingness check by not being missing.
Chose
Exact-match code sets over a controlled vocabulary
Over
Name-substring filtering on the specialty term
Documented as Defect 2 and measured repeatedly: 'urolog' is a substring of 'neurology'; on cardiology a 207R prefix filter admits 30 codes of which only 5 are cardiology, and filtering the segment label on 'cardiolog' drops 40,643 records (74.4% of the headline) while the survivors still look like a plausible list.
Chose
Findings survive only on 2-of-3 independent confirmations, defaulting to refuted
Over
Accepting any single agent's finding
agents/04-qa-panel.md states that phantom defects cost rebuild time and trust, and that a claim without a reproducer is an opinion and is discarded. Refuted findings are still committed, as evidence the check ran.
Chose
Committing run identity (spec, sources, manifest, QA) and gitignoring the CSV payload
Over
Committing the delivered datasets alongside their provenance
.gitignore records both reasons: every payload byte is reproducible from committed files, the delivered dentist master alone is 120 MB and over GitHub's per-file limit, and a repository that leaves the machine should not carry the providers' personal data.
Chose
Reusing a proven spec from specs/ instead of re-running Tier 1 research
Over
Re-deriving the code set on every run of a solved query
agents/00-orchestrator.md: a proven spec's code set was already resolved, criticised and human-approved once, and rerunning research on a solved query only creates the chance to resolve it differently.
Chose
Constraining the blind ontology critic by allowlist (specialty string, vocabulary URL)
Over
Enumerating forbidden information channels as a denylist
The orchestrator brief argues a denylist has to anticipate every channel, and the channel that leaks will be the one nobody listed. The same argument drives the allowlist marketing view.
Chose
Python standard library plus openpyxl
Over
pandas
METHODOLOGY Phase 0.7 records the decision as deliberate, no reason to pull numpy for a streaming job. requirements.txt is openpyxl only, and the README notes even that is optional.
At scale
~9.8 million registry rows streamed per national build
409,848 records in the largest delivered dataset; 60-column output schema on the reference build
1.09 GB compressed source expanding to 11.0 GB, against a documented 10-12x disk budget rule
155 tracked files, 22 Python modules, 477,699 bytes of Python (corrected: 181 counted directories as paths)
75 committed QA artifacts across four run directories; 64 for the cardiology run alone
Seven-agent hosted roster plus seven Claude CLI project subagent definitions
Six upstream sources per build, one bulk registry, weekly increments, a deactivation report, a pinned taxonomy vocabulary, and three CMS enrichment datasets
1,986-line synthetic end-to-end test suite carrying 286 checks, running with no downloads (corrected: the 287th match is the check() definition itself)
My role
Architect and operator. The seven human-authored commits on 10 August 2026 are the founding set and they are the whole design: the initial Provider Research Factory (pipeline, compliance gate, marketing layer, agent briefs), the relocation and isolation rules, the wiring of those briefs as runnable Claude CLI project subagents, the Puffo provisioning pack of profiles and roster parameters, a persistent resumable source cache, corrected column declarations against the local corpus, and the first live end-to-end run producing 22,195 records with a population identical to the hand-built reference. The 314 subsequent commits were made by mso-orchestrator, an agent Muneeb defined in this repo (puffo/profiles/mso-orchestrator.md), provisioned into a space he created on his own machine, and pointed at a workspace contract he wrote. He owns the doctrine those agents follow, the deterministic pipeline they are forbidden to bypass, the gates they must stop at, and the human sign-off at GATE D on every delivered dataset. He did not hand-write the later runs.
