Reported FEC relationships
transforms.fec_relationships maps retained source rows into a local relationship
observation table. It covers candidate–committee links, principal and other
authorized committees, connected organizations, affiliation names, leadership
PAC sponsors and relationships stated on selected original Forms 1/2 and their
supplements. It does not publish a dataset or change fec_committees columns.
The build-fec-observations command now delivers these observations with their
complete metadata companions through the existing rollup upload path. Local
builds are the default. A successful local build does not establish that these
tables are published on the public MCP server.
For GitHub Actions, use the explicit retained-input workflow with pinned transfer bytes and portable manifest paths. Successful build-only runs retain their complete generation and input audit.
Build selected retained inputs
uv run --frozen build-fec-observations \
--manifest /path/to/retained-inputs.json \
--output-dir /path/to/output
This creates a new fec-observations-<run-id> directory containing:
| Table | Row meaning |
|---|---|
fec_source_records |
One selected source observation, identified by collection_id and source_record_id; complete provider record and native metadata, source digest, URL, observation time and exact source coordinates. |
fec_collections |
One selected collection or explicit caller coverage disposition. Parsed collections preserve requested scope, verified counts, coverage limits and source outcome; metadata-only rows have NULL provider fields. |
fec_relationships |
Source-reported assertions and absence states from supported committee API, original statement and explicitly mapped bulk inputs. |
The manifest is JSON with version: 1 and a nonempty collections list. Each
collection names a unique collection_id, a source_family ID from the official
FEC source catalog, a profile, and a blob_root. Relative filesystem paths
resolve beside the manifest. Choose one input mode:
- Retained API query:
profileiscommittee,candidate,filing,legaloraudit;capturescontains the exact SpicyDocs capture descriptors in request order. Existing provider parsers verify original digests, selected membership, pagination, counts and native identifiers. Missing pages refuse. - Retained original or ZIP member:
profileispositional;scopeis the result of SpicyDocspositional_row_scope. This pins the original and explicitly selects format, encoding, delimiter, quoting and optional ZIP member ordinal and name. Complete source bytes are verified before parsing. Header rows, blank rows, duplicates, signed amounts and amendment flags survive. - Retained agency original:
profileisagency;source_familyisfec_agency_reports;scopeis the result of SpicyDocsagency_report_scope. The selected FOIA XML or Oversight HTML original must match its digest and size. Every native report record, field, body and asset remains insource_record_jsonandmetadata_json, with its ordinal and format insource_locator_json. Agency records produce no inferred candidate/committee relationships. - Agency archive or Word original:
profileisagency-document;scopecomes fromagency_document_scope. FOIA.gov ZIP inputs select an exact member ordinal and name. Word Flat OPC inputs retain package parts, XML elements and literal text/control tokens. Their tokens are not rendered pages or OCR. - PostgreSQL committee history:
profileispostgres;scopecomes frompostgres_row_scope. The source reader regenerates the selected COPY text throughpg_restoreand checks the retained tool, arguments, dump, schema and derived-stream pins. Raw COPY fields, decoded SQL NULLs, empty strings and array text stay distinct.metadata_json.named_fieldsuses the verified column names; historical rows do not use the current-committee relationship mapper. - One retained API response:
profileisdocument;scopecomes fromdocument_scopewithformat: api-jsonand an explicit response mode. Complete top-level fields, source row metadata, attachment links and embedded body text remain queryable. Every record statesquery_completeness: not-asserted; this input never establishes a complete API traversal. Scalar, list and NULL source values survive without inventing identity fields. - Existing source release:
release_path,artifact_sha256andverifier_implementation_idpin an existing SpicyDocs release. Supported profiles additionally includebulk;agencyalso accepts a pinned release. The source reader checks artifact membership and replays source evidence. Releases with rejected or discarded observations refuse this complete companion delivery.
An existing release is optional. No input mode downloads source data. Explicit source query scope remains visible; none establishes a complete historical FEC population, complete legal case files or a current amendment view.
Caller coverage facts use the same metadata table without inventing a source
profile or a successful empty result. A metadata-only manifest entry contains
only collection_id, source_family and this disposition:
{
"status": "unresolved",
"reason": "The retained inventory references an original whose bytes are unavailable",
"context": {"path": "coverage/object.json", "sha256": "sha256:<64 lowercase hex digits>"}
}
The allowed statuses are retained_unparsed, refused, unresolved,
inventory_only and selection_context. These rows emit zero records; their
unknown source count remains NULL. They have NULL profile, source_system_id,
source_state_scope, requested_scope_json and release pin. Their
collection_outcome_json.receiverDisposition holds the caller's status, reason
and verified context; providerOutcome is NULL. A normal parsed entry may also
include an optional context pin, stored separately as callerContext.
Context is a retained JSON object with version: 1, limited to 1 MiB. Its allowed
facts are source_capture, source_family, bulk_group, object_key, member,
related_collection_ids, source_authority, selection, parsing,
relationships, evidence and coverage_limits. Unknown top-level fields,
named credentials, credential-bearing URLs and changed bytes refuse the build.
Context rows have their own unique IDs and can refer to prior collection IDs
without changing those prior rows. retained_unparsed remains open work; it is
not evidence of adoption. A PDF deferred by the user uses inventory_only with
parsing.status: deferred_by_user in its context. Already retained output pins
can remain discoverable without performing another PDF extraction. A parse-requested input still fails the complete build
on malformed bytes or incomplete traversal.
The rollup retains the exact input manifest in its sealed source-evidence artifact. Original capture timestamps remain unchanged. A session-local source reader check reuses verified ZIP inventories across selected members while hash-checking each opened original; no pathname grants trust.
For positional data, an optional field_mapping associates a verified header
observation with its rows:
{
"header_collection_id": "linkage-header-2024",
"header_row_ordinal": 0,
"data_has_header": false,
"relationship_family": "linkage",
"cycle": 2024
}
The selected header collection must appear earlier in the manifest. An embedded
CSV header instead names its own collection and uses data_has_header: true.
Its exact nonempty, unique field names must match every nonblank data row's
field count. metadata_json then contains the named source fields, including
empty strings and literal numbers; source_record_json still contains every
original positional field and coordinate. Header provenance is retained in
source_locator_json.field_mapping, and collection metadata records the chosen
mapping. Header and blank rows keep their positional metadata.
relationship_family and cycle are optional together. The supported existing
maps are linkage, candidate_master, committee_master, form1_bulk and
leadership. Source candidate election year and selected bulk cycle remain
separate. Named source fields do not establish that a summary row represents an
individual entity; publisher aggregate and sentinel identifiers remain literal.
Mapped source ID fields also populate committee_id and candidate_id for
direct joins. Recognized publisher spellings include CMTE_ID, COMMITTEE_ID,
Committee_Id, CAND_ID, CANDIDATE_ID, Cand_Id and cand_id, alongside the
lowercase API spellings. These columns retain literal source roles and empty
strings. A source's aggregate placeholder remains an aggregate placeholder;
the column does not validate a person, committee or affiliation. Conflicting
aliases in one row refuse delivery instead of choosing one or filling a blank.
Every original alias and field remains in the named metadata and literal record.
Some summary ZIPs, including weball, webl and webk, publish their ordered
field definitions on an official HTML description page. Select that retained
dictionary directly instead of manufacturing a CSV header:
{
"dictionary": {
"blob_root": "/path/to/retained-dictionaries/blobs",
"capture": {
"requestUrl": "https://www.fec.gov/campaign-finance-data/all-candidates-file-description/",
"responseSha256": "sha256:<exact retained digest>",
"byteSize": 32602,
"observedAt": "2026-09-12T13:42:59.888472+00:00",
"representation": "opaque"
}
},
"data_has_header": false
}
Use this object as field_mapping; dictionary and header-collection modes are
mutually exclusive. SpicyDocs verifies the HTML and reads one explicit
Column name / Field name / Position table. Positions must run from one
without gaps, names must be unique and nonempty, and every data row must match
the dictionary's width. Surrounding HTML layout whitespace is removed from
field-name keys; literal cells, fragments and source spelling remain available.
Each data row retains the dictionary digest, URL and table locator. Complete
field definitions, cell coordinates and original text fragments appear once in
fec_collections.collection_outcome_json.tableFieldDefinitions; the row locator
identifies that collection and JSON location. definitions_pointer is relative
to the decoded collection_outcome_json value. Exact input rows remain in
source_record_json. Selecting a description for a historical file is explicit;
matching field counts does not prove historical semantic compatibility, establish
financial types or choose an amendment policy.
For API records, metadata_json follows SpicyDocs' metadata/body separation and
exact decimal-string representation. Original JSON numbers and embedded bodies
remain in the pinned original; assets_json and embedded_bodies_json retain
their source references. Full nested fields are accessible through DuckDB's JSON
functions without adding a new table schema for each source family.
The local builder stages and validates all three outputs, then installs the new
directory together. Any late input failure leaves earlier generations intact.
The Python build_fec_observations(manifest, output_dir) API requires a fresh
destination. The rollup creates one automatically and seals its complete output
set through the generation publisher. Optional
--no-skip-upload checks shrink limits, uploads immutable members, verifies their
remote bytes and conditionally switches one publication pointer. A failed member
upload leaves the prior published family selected. Retain the input manifest and
pinned originals with each generation; the generation artifact verifies output
membership and bytes, not complete source coverage. Overlapping selections remain
separate observations.
Relationship locators include their exact companion identity. A consumer can join without guessing from names or choosing a current amendment:
SELECT r.relationship_type, r.value_status, m.metadata_json
FROM fec_relationships AS r
JOIN fec_source_records AS m
ON m.collection_id = json_extract_string(r.source_locator_json, '$.collection_id')
AND m.source_record_id = json_extract_string(r.source_locator_json, '$.source_record_id')
AND m.source_sha256 = r.source_sha256;
The native relationships mapper also remains available directly:
Use bulk_relationships, api_relationships or statement_relationships for
each verified input row, then stream their results to
write_fec_relationship_rows(records, destination, batch_size=2000). Evidence
requires the original SHA-256, URL, observation time and exact source locator:
CSV data-row number and ZIP member, API JSON Pointer, or filing byte range.
The caller verifies each source once and retains the originals. The shared
Arrow writer keeps one bounded batch and replaces the destination after full
consumption; a late error preserves the previous output.
value_status=reported marks a populated source value. It does not establish
that the relationship is true or identify a name-only target. reported_none,
empty_string, empty_list, null and missing_field are observations, not
relationship edges. source_id_shape validates syntax only. Invalid candidate
IDs remain in their source role; a committee-shaped value is not retyped as a
committee. The subject_type and object_type columns name the source field's
identifier or target role, not a legal entity classification. In particular,
committee covers FEC's C-prefixed filer role, including independent-expenditure
filers whose source label explicitly says "not a committee". API observations
retain committee_type, committee_type_full, organization_type and
organization_type_full when present, including a raw code with a null label.
Keep these qualifications with the observation. No name matcher runs and no row
enters org_committee_links.
Bulk cycle and candidate election year remain separate. Current API rows have
cycle=NULL; cycles stays in the complete source record, alongside fields
outside this narrow table. Candidate IDs alone are labeled committee_candidate;
authorization requires an explicit source designation or statement role.
Each reported array element and duplicate survives, including separate sponsor
ID and sponsor-name assertions. These arrays are independent: neither position
nor a null entry authorizes pairing or filling from the other array. Missing,
null and empty lists each emit an explicit observation for all three API arrays.
There is no deduplication or latest-wins policy across sources or amendments.
For API arrays, source_locator_json.array_field names the array and
array_index selects the element within the exact parent JSON Pointer.
source_fields_json[array_field] contains that literal element, including all
keys in a sponsor object. A null element retains its nonnull index; missing,
null or empty parent arrays have array_index=null and retain their original
absence/value state. Scalar observations have no array_field. This differs
from the earlier local output, which repeated the entire array on every row.
The top-level table columns are unchanged.
The original digest plus parent pointer locates the complete record. Keep its
complete metadata companion with the delivery so consumers can join directly to
cycles, detailed attributes and complete arrays. Do not treat the relationship
table as a complete entity record. The mapper copies selected array elements
once, for linear selected-field bytes as array length grows; it does not copy
cycles into every observation. It does not itself verify or package companions:
input completeness and their admission remain the caller's responsibility.
For statements, the supported versions are 8.3 and 8.4. The positions come
from the FEC's FEC_Format_v8.3.xlsx and FEC_Format_v8.4.xlsx in the official
format archive.
F1 line 6 moves two fields in 8.4; F1S supplies additional affiliations and
joint-fundraising participants. The literal ORG/AFF/JFR/LPS code determines the
reported role. Unknown codes retain reported_affiliation; unsupported versions
refuse. F2 and F2S provide principal and other authorized committees. The original
byte ranges and source fields preserve spelling, dates, blank values and both
targets when committee and candidate IDs coexist. Other filing versions remain
available through SpicyDocs' literal reader without an assumed relationship map.
The repaired selected output and verification cover the selected bulk relationship rows, current cycle-filtered census and retained gap observations, plus the selected original statements. Required input membership and independent source-role/state checks now qualify that output. The selected census delivery now admits current API metadata through a local SpicyDocs release, the existing committee writer and a complete DocSpec metadata catalog. The retained-input builder above delivers selected relationships and their companions without requiring a new immutable release. Full historical acquisition and remote publication remain separate work. The retained committee-history dump stops at 2022; its recent object date does not make it a substitute for the selected cycles.