Redesign Space UX for guided story-first public flow
Browse files
.gitattributes
CHANGED
|
@@ -36,3 +36,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 36 |
dataset_bundle/evidence_audit/claim_supporting_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
dataset_bundle/evidence_audit/scored_event_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
|
| 38 |
dataset_bundle/evidence_audit/source_artifact_index.csv filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 36 |
dataset_bundle/evidence_audit/claim_supporting_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
|
| 37 |
dataset_bundle/evidence_audit/scored_event_provenance.jsonl filter=lfs diff=lfs merge=lfs -text
|
| 38 |
dataset_bundle/evidence_audit/source_artifact_index.csv filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
__pycache__/public_space_app.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
|
__pycache__/public_space_app.cpython-311.pyc
CHANGED
|
Binary files a/__pycache__/public_space_app.cpython-311.pyc and b/__pycache__/public_space_app.cpython-311.pyc differ
|
|
|
dataset_bundle/evidence_audit/consistency_report.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"generated_at": "2026-04-
|
| 3 |
"event_provenance": {
|
| 4 |
"event_count": 3918,
|
| 5 |
"events_with_artifacts": 3878,
|
|
|
|
| 1 |
{
|
| 2 |
+
"generated_at": "2026-04-19T09:21:59-04:00",
|
| 3 |
"event_provenance": {
|
| 4 |
"event_count": 3918,
|
| 5 |
"events_with_artifacts": 3878,
|
dataset_bundle/network_graph/graph_config.json
CHANGED
|
@@ -20,10 +20,9 @@
|
|
| 20 |
"default_filters": {
|
| 21 |
"relationship_family": "sector",
|
| 22 |
"review_status": "stronger",
|
| 23 |
-
"max_edges":
|
| 24 |
"hide_unresolved_only": true,
|
| 25 |
-
"overview_member_limit":
|
| 26 |
-
"default_member_search": "Josh Gottheimer"
|
| 27 |
},
|
| 28 |
"example_member_searches": [
|
| 29 |
"Josh Gottheimer",
|
|
|
|
| 20 |
"default_filters": {
|
| 21 |
"relationship_family": "sector",
|
| 22 |
"review_status": "stronger",
|
| 23 |
+
"max_edges": 60,
|
| 24 |
"hide_unresolved_only": true,
|
| 25 |
+
"overview_member_limit": 8
|
|
|
|
| 26 |
},
|
| 27 |
"example_member_searches": [
|
| 28 |
"Josh Gottheimer",
|
dataset_bundle/public_release_manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
{
|
| 2 |
"public_version": "congress-public-records-slice-2026-04-v1",
|
| 3 |
"title": "Congress Public Records Slice",
|
| 4 |
-
"release_date": "2026-04-
|
| 5 |
"slice_description": "A neutral, review-oriented slice of House public-record linkages across financial disclosures, sector overlap, and community project funding recipient relationships.",
|
| 6 |
"source_run_name": "house_all_baseline_20260418_v21_recipienthardening",
|
| 7 |
"dataset_repo_id": "cjc0013/cmp-data",
|
|
|
|
| 1 |
{
|
| 2 |
"public_version": "congress-public-records-slice-2026-04-v1",
|
| 3 |
"title": "Congress Public Records Slice",
|
| 4 |
+
"release_date": "2026-04-19T09:22:53-04:00",
|
| 5 |
"slice_description": "A neutral, review-oriented slice of House public-record linkages across financial disclosures, sector overlap, and community project funding recipient relationships.",
|
| 6 |
"source_run_name": "house_all_baseline_20260418_v21_recipienthardening",
|
| 7 |
"dataset_repo_id": "cjc0013/cmp-data",
|
public_copy.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"public_version": "congress-public-records-slice-2026-04-v1",
|
| 3 |
"title": "Congress Public Records Slice",
|
| 4 |
"subtitle": "Neutral Records explorer for a public-record slice of congressional money-and-power linkages.",
|
| 5 |
"dataset_repo_id": "cjc0013/cmp-data",
|
|
|
|
| 1 |
{
|
| 2 |
+
"public_version": "congress-public-records-slice-2026-04-v1-private-spacefix",
|
| 3 |
"title": "Congress Public Records Slice",
|
| 4 |
"subtitle": "Neutral Records explorer for a public-record slice of congressional money-and-power linkages.",
|
| 5 |
"dataset_repo_id": "cjc0013/cmp-data",
|
public_space_app.py
CHANGED
|
@@ -239,6 +239,339 @@ def _fictional_example_markdown() -> str:
|
|
| 239 |
)
|
| 240 |
|
| 241 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
def _plain_status_label(value: str) -> str:
|
| 243 |
normalized = str(value or "").strip()
|
| 244 |
mapping = {
|
|
@@ -552,12 +885,82 @@ def _overview_summary_markdown(
|
|
| 552 |
return "\n".join(lines)
|
| 553 |
|
| 554 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
def _relationship_options(ranked: pd.DataFrame) -> list[tuple[str, str]]:
|
| 556 |
if ranked.empty:
|
| 557 |
return []
|
| 558 |
options: list[tuple[str, str]] = []
|
| 559 |
for row in ranked.to_dict("records"):
|
| 560 |
-
label =
|
|
|
|
|
|
|
|
|
|
| 561 |
options.append((label, str(row["relationship_id"])))
|
| 562 |
return options
|
| 563 |
|
|
@@ -588,6 +991,7 @@ def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) ->
|
|
| 588 |
f"- Supporting relationship rows: `{int(row.get('link_count', 0) or 0)}`",
|
| 589 |
f"- Stronger-support rows: `{int(row.get('linked_count', 0) or 0) if family == 'recipient' else int(row.get('strong_event_count', 0) or 0)}`",
|
| 590 |
f"- Caution / weaker rows: `{int(row.get('review_count', 0) or 0) if family == 'recipient' else int(row.get('weak_event_count', 0) or 0)}`",
|
|
|
|
| 591 |
f"- Unresolved source refs still counted: `{int(row.get('unresolved_source_ref_count', 0) or 0)}`",
|
| 592 |
f"- Evidence signals: `{', '.join(chips) if chips else 'published source support'}`",
|
| 593 |
f"- Time-window overlap: `{_window_overlap_text(row)}`",
|
|
@@ -598,6 +1002,14 @@ def _relationship_detail_markdown(edges: pd.DataFrame, relationship_id: str) ->
|
|
| 598 |
if urls:
|
| 599 |
lines.extend(["", "#### Example published source URLs", ""])
|
| 600 |
lines.extend(f"- {item}" for item in urls)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 601 |
return "\n".join(lines)
|
| 602 |
|
| 603 |
|
|
@@ -742,12 +1154,12 @@ def _consistency_summary_markdown(consistency: Dict[str, Any]) -> str:
|
|
| 742 |
"### Audit Summary",
|
| 743 |
"",
|
| 744 |
f"- Event rows in the audit index: `{int(event_payload.get('event_count', 0) or 0)}`",
|
| 745 |
-
f"- Event rows with
|
| 746 |
f"- Stored-versus-lookup provenance mismatches: `{int(event_payload.get('stored_lookup_mismatch_count', 0) or 0)}`",
|
| 747 |
f"- Claim-supporting rows in the audit index: `{int(claim_payload.get('row_count', 0) or 0)}`",
|
| 748 |
-
f"- Claim-supporting rows with
|
| 749 |
"",
|
| 750 |
-
"Use the tables below to inspect the public source URLs and
|
| 751 |
]
|
| 752 |
)
|
| 753 |
|
|
@@ -877,7 +1289,7 @@ def _render_graph(nodes: pd.DataFrame, edges: pd.DataFrame) -> str:
|
|
| 877 |
|
| 878 |
def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str) -> Tuple[str, pd.DataFrame]:
|
| 879 |
if not event_id or event_id not in set(events["event_id"]):
|
| 880 |
-
return "Select an event id to inspect source URLs and
|
| 881 |
event_row = events[events["event_id"] == event_id].head(1).to_dict("records")[0]
|
| 882 |
prov_rows = provenance[provenance["row_key"] == event_id]
|
| 883 |
member_name = str(event_row.get("member_name") or event_row.get("member_slug") or "Unknown member")
|
|
@@ -895,7 +1307,7 @@ def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str)
|
|
| 895 |
"This panel summarizes one released event row from the public slice.",
|
| 896 |
"",
|
| 897 |
f"- Event id: `{event_id}`",
|
| 898 |
-
|
| 899 |
]
|
| 900 |
if score_label:
|
| 901 |
lines.append(f"- Score label: `{score_label}`")
|
|
@@ -909,7 +1321,7 @@ def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str)
|
|
| 909 |
lines.extend(
|
| 910 |
[
|
| 911 |
f"- Attached source URLs in this row: `{int(event_row.get('source_ref_count', 0) or 0)}`",
|
| 912 |
-
f"-
|
| 913 |
f"- Unresolved source references still counted: `{int(event_row.get('unresolved_source_ref_count', 0) or 0)}`",
|
| 914 |
f"- Matching provenance rows shown below: `{len(prov_rows)}`",
|
| 915 |
]
|
|
@@ -962,140 +1374,259 @@ def build_app(copy_path: str | Path):
|
|
| 962 |
overview_member_limit = int(graph_defaults.get("overview_member_limit", 8))
|
| 963 |
default_member_search = str(graph_defaults.get("default_member_search", "") or "")
|
| 964 |
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
|
| 971 |
-
|
| 972 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 973 |
gr.Markdown(
|
| 974 |
-
"###
|
| 975 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
)
|
|
|
|
| 977 |
with gr.Row():
|
| 978 |
-
|
| 979 |
-
|
| 980 |
-
|
| 981 |
-
|
| 982 |
-
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 987 |
overview_detail_md = gr.Markdown()
|
| 988 |
overview_timeline_html = gr.HTML()
|
| 989 |
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
| 993 |
-
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
| 998 |
-
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
|
| 1002 |
-
|
| 1003 |
-
def _update_overview(member_query: str, family: str, only_strong: bool, top_n: int):
|
| 1004 |
-
filtered_edges = _overview_edges(member_query, family, only_strong, top_n)
|
| 1005 |
-
ranked = _rank_relationships(filtered_edges)
|
| 1006 |
-
options = _relationship_options(ranked)
|
| 1007 |
-
selected = options[0][1] if options else None
|
| 1008 |
-
display = ranked.drop(columns=["relationship_id", "source_examples"], errors="ignore")
|
| 1009 |
-
return (
|
| 1010 |
-
_overview_summary_markdown(
|
| 1011 |
-
ranked,
|
| 1012 |
-
member_query=member_query,
|
| 1013 |
-
family=family,
|
| 1014 |
-
only_strong_links=only_strong,
|
| 1015 |
-
top_n=top_n,
|
| 1016 |
-
),
|
| 1017 |
-
display,
|
| 1018 |
-
gr.update(choices=options, value=selected),
|
| 1019 |
-
_relationship_detail_markdown(filtered_edges, selected or ""),
|
| 1020 |
-
_relationship_timeline_html(filtered_edges, selected or ""),
|
| 1021 |
-
)
|
| 1022 |
-
|
| 1023 |
-
def _update_overview_detail(member_query: str, family: str, only_strong: bool, top_n: int, relationship_id: str):
|
| 1024 |
-
filtered_edges = _overview_edges(member_query, family, only_strong, top_n)
|
| 1025 |
-
return _relationship_detail_markdown(filtered_edges, relationship_id), _relationship_timeline_html(filtered_edges, relationship_id)
|
| 1026 |
-
|
| 1027 |
-
for control in (overview_member, overview_family, overview_only_strong, overview_top_n):
|
| 1028 |
-
control.change(
|
| 1029 |
-
_update_overview,
|
| 1030 |
-
[overview_member, overview_family, overview_only_strong, overview_top_n],
|
| 1031 |
-
[overview_summary_md, overview_df, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1032 |
-
)
|
| 1033 |
-
relationship_choice.change(
|
| 1034 |
-
_update_overview_detail,
|
| 1035 |
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1036 |
-
[overview_detail_md, overview_timeline_html],
|
| 1037 |
)
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
-
|
| 1041 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
)
|
| 1043 |
-
with gr.Tab("Explore Graph (optional)"):
|
| 1044 |
gr.Markdown(_graph_intro_markdown(data["graph_config"]))
|
| 1045 |
with gr.Row():
|
| 1046 |
-
|
| 1047 |
-
|
| 1048 |
-
|
| 1049 |
-
|
| 1050 |
-
|
| 1051 |
-
|
| 1052 |
-
gr.
|
| 1053 |
-
|
| 1054 |
-
hide_unresolved_only = gr.Checkbox(label="Hide unresolved links", value=bool(graph_defaults.get("hide_unresolved_only", True)))
|
| 1055 |
-
max_edges = gr.Slider(label="Show top relationships", minimum=25, maximum=300, step=25, value=int(graph_defaults.get("max_edges", 60)))
|
| 1056 |
graph_summary_md = gr.Markdown()
|
| 1057 |
graph_html = gr.HTML()
|
| 1058 |
-
gr.
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
member_query=member_graph_query,
|
| 1067 |
-
target_query=target_query,
|
| 1068 |
-
review_status=review_status,
|
| 1069 |
-
max_edges=max_edges,
|
| 1070 |
)
|
| 1071 |
-
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
-
|
| 1075 |
-
|
| 1076 |
-
|
| 1077 |
-
|
| 1078 |
-
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
|
| 1084 |
-
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
| 1088 |
-
|
| 1089 |
-
|
| 1090 |
-
|
| 1091 |
-
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1101 |
return app
|
|
|
|
| 239 |
)
|
| 240 |
|
| 241 |
|
| 242 |
+
def _space_css() -> str:
|
| 243 |
+
return """
|
| 244 |
+
.gradio-container {
|
| 245 |
+
max-width: 1180px !important;
|
| 246 |
+
margin: 0 auto !important;
|
| 247 |
+
padding-bottom: 48px !important;
|
| 248 |
+
}
|
| 249 |
+
.hero-panel {
|
| 250 |
+
background: linear-gradient(135deg, #fff8ed 0%, #f5eee2 100%);
|
| 251 |
+
border: 1px solid #e6d8bf;
|
| 252 |
+
border-radius: 24px;
|
| 253 |
+
padding: 28px;
|
| 254 |
+
margin: 6px 0 20px 0;
|
| 255 |
+
box-shadow: 0 10px 30px rgba(62, 46, 14, 0.08);
|
| 256 |
+
}
|
| 257 |
+
.hero-eyebrow {
|
| 258 |
+
font-size: 0.82rem;
|
| 259 |
+
font-weight: 700;
|
| 260 |
+
letter-spacing: 0.08em;
|
| 261 |
+
text-transform: uppercase;
|
| 262 |
+
color: #8a6220;
|
| 263 |
+
margin-bottom: 8px;
|
| 264 |
+
}
|
| 265 |
+
.hero-title {
|
| 266 |
+
font-size: 2.2rem;
|
| 267 |
+
line-height: 1.1;
|
| 268 |
+
font-weight: 800;
|
| 269 |
+
color: #1f2b2d;
|
| 270 |
+
margin: 0 0 12px 0;
|
| 271 |
+
}
|
| 272 |
+
.hero-lede {
|
| 273 |
+
font-size: 1.05rem;
|
| 274 |
+
line-height: 1.6;
|
| 275 |
+
color: #334244;
|
| 276 |
+
margin: 0 0 10px 0;
|
| 277 |
+
max-width: 900px;
|
| 278 |
+
}
|
| 279 |
+
.hero-note {
|
| 280 |
+
font-size: 0.98rem;
|
| 281 |
+
line-height: 1.5;
|
| 282 |
+
color: #5f4e2a;
|
| 283 |
+
background: rgba(255,255,255,0.72);
|
| 284 |
+
border: 1px solid #eadcbf;
|
| 285 |
+
border-radius: 14px;
|
| 286 |
+
padding: 12px 14px;
|
| 287 |
+
margin-top: 14px;
|
| 288 |
+
}
|
| 289 |
+
.stat-grid, .story-grid {
|
| 290 |
+
display: grid;
|
| 291 |
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
| 292 |
+
gap: 14px;
|
| 293 |
+
margin-top: 18px;
|
| 294 |
+
}
|
| 295 |
+
.story-grid {
|
| 296 |
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
| 297 |
+
margin: 10px 0 22px 0;
|
| 298 |
+
}
|
| 299 |
+
.stat-card, .story-card, .source-card, .glossary-card, .result-card {
|
| 300 |
+
background: #fffdf8;
|
| 301 |
+
border: 1px solid #e7dcc8;
|
| 302 |
+
border-radius: 18px;
|
| 303 |
+
padding: 16px 18px;
|
| 304 |
+
box-shadow: 0 6px 18px rgba(40, 31, 9, 0.05);
|
| 305 |
+
}
|
| 306 |
+
.stat-label {
|
| 307 |
+
font-size: 0.82rem;
|
| 308 |
+
font-weight: 700;
|
| 309 |
+
text-transform: uppercase;
|
| 310 |
+
letter-spacing: 0.06em;
|
| 311 |
+
color: #8b6b2e;
|
| 312 |
+
margin-bottom: 8px;
|
| 313 |
+
}
|
| 314 |
+
.stat-value {
|
| 315 |
+
font-size: 1.9rem;
|
| 316 |
+
font-weight: 800;
|
| 317 |
+
color: #1f2b2d;
|
| 318 |
+
line-height: 1;
|
| 319 |
+
margin-bottom: 6px;
|
| 320 |
+
}
|
| 321 |
+
.stat-help {
|
| 322 |
+
font-size: 0.92rem;
|
| 323 |
+
color: #5b5b5b;
|
| 324 |
+
line-height: 1.45;
|
| 325 |
+
}
|
| 326 |
+
.story-title, .source-title, .glossary-title {
|
| 327 |
+
font-size: 1rem;
|
| 328 |
+
font-weight: 800;
|
| 329 |
+
color: #1f2b2d;
|
| 330 |
+
margin-bottom: 6px;
|
| 331 |
+
}
|
| 332 |
+
.story-body, .source-body, .glossary-body {
|
| 333 |
+
font-size: 0.95rem;
|
| 334 |
+
line-height: 1.55;
|
| 335 |
+
color: #435153;
|
| 336 |
+
}
|
| 337 |
+
.source-table {
|
| 338 |
+
width: 100%;
|
| 339 |
+
border-collapse: collapse;
|
| 340 |
+
margin-top: 8px;
|
| 341 |
+
font-size: 0.95rem;
|
| 342 |
+
}
|
| 343 |
+
.source-table th, .source-table td {
|
| 344 |
+
border-top: 1px solid #eadfcf;
|
| 345 |
+
padding: 12px 10px;
|
| 346 |
+
text-align: left;
|
| 347 |
+
vertical-align: top;
|
| 348 |
+
}
|
| 349 |
+
.source-table th {
|
| 350 |
+
color: #7a5b20;
|
| 351 |
+
font-size: 0.82rem;
|
| 352 |
+
text-transform: uppercase;
|
| 353 |
+
letter-spacing: 0.06em;
|
| 354 |
+
width: 32%;
|
| 355 |
+
}
|
| 356 |
+
.glossary-list {
|
| 357 |
+
display: grid;
|
| 358 |
+
gap: 10px;
|
| 359 |
+
margin-top: 8px;
|
| 360 |
+
}
|
| 361 |
+
.glossary-item strong {
|
| 362 |
+
display: block;
|
| 363 |
+
color: #1f2b2d;
|
| 364 |
+
margin-bottom: 2px;
|
| 365 |
+
}
|
| 366 |
+
.section-kicker {
|
| 367 |
+
color: #8a6220;
|
| 368 |
+
font-size: 0.84rem;
|
| 369 |
+
font-weight: 700;
|
| 370 |
+
letter-spacing: 0.06em;
|
| 371 |
+
text-transform: uppercase;
|
| 372 |
+
margin-bottom: 6px;
|
| 373 |
+
}
|
| 374 |
+
.result-list {
|
| 375 |
+
display: flex;
|
| 376 |
+
flex-direction: column;
|
| 377 |
+
gap: 12px;
|
| 378 |
+
margin-top: 10px;
|
| 379 |
+
}
|
| 380 |
+
.result-head {
|
| 381 |
+
display: flex;
|
| 382 |
+
justify-content: space-between;
|
| 383 |
+
align-items: flex-start;
|
| 384 |
+
gap: 12px;
|
| 385 |
+
}
|
| 386 |
+
.result-rank {
|
| 387 |
+
font-size: 0.78rem;
|
| 388 |
+
font-weight: 700;
|
| 389 |
+
color: #8b6b2e;
|
| 390 |
+
text-transform: uppercase;
|
| 391 |
+
letter-spacing: 0.06em;
|
| 392 |
+
margin-bottom: 4px;
|
| 393 |
+
}
|
| 394 |
+
.result-title {
|
| 395 |
+
font-size: 1.12rem;
|
| 396 |
+
font-weight: 800;
|
| 397 |
+
color: #1f2b2d;
|
| 398 |
+
line-height: 1.2;
|
| 399 |
+
margin-bottom: 4px;
|
| 400 |
+
}
|
| 401 |
+
.result-subtitle {
|
| 402 |
+
color: #546365;
|
| 403 |
+
font-size: 0.93rem;
|
| 404 |
+
}
|
| 405 |
+
.metric-stack {
|
| 406 |
+
display: flex;
|
| 407 |
+
gap: 8px;
|
| 408 |
+
flex-wrap: wrap;
|
| 409 |
+
justify-content: flex-end;
|
| 410 |
+
}
|
| 411 |
+
.score-pill, .strength-pill, .chip {
|
| 412 |
+
display: inline-block;
|
| 413 |
+
border-radius: 999px;
|
| 414 |
+
padding: 5px 10px;
|
| 415 |
+
font-size: 0.82rem;
|
| 416 |
+
font-weight: 700;
|
| 417 |
+
white-space: nowrap;
|
| 418 |
+
}
|
| 419 |
+
.score-pill {
|
| 420 |
+
background: #1f5f5b;
|
| 421 |
+
color: white;
|
| 422 |
+
}
|
| 423 |
+
.strength-pill {
|
| 424 |
+
background: #f3e4b9;
|
| 425 |
+
color: #5a470d;
|
| 426 |
+
}
|
| 427 |
+
.chip-row {
|
| 428 |
+
display: flex;
|
| 429 |
+
flex-wrap: wrap;
|
| 430 |
+
gap: 8px;
|
| 431 |
+
margin: 12px 0 10px 0;
|
| 432 |
+
}
|
| 433 |
+
.chip {
|
| 434 |
+
background: #f2ede2;
|
| 435 |
+
color: #4a4a4a;
|
| 436 |
+
}
|
| 437 |
+
.meta-grid {
|
| 438 |
+
display: grid;
|
| 439 |
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
| 440 |
+
gap: 10px;
|
| 441 |
+
margin-top: 10px;
|
| 442 |
+
font-size: 0.9rem;
|
| 443 |
+
color: #465558;
|
| 444 |
+
}
|
| 445 |
+
.meta-grid strong {
|
| 446 |
+
display: block;
|
| 447 |
+
color: #1f2b2d;
|
| 448 |
+
margin-bottom: 2px;
|
| 449 |
+
font-size: 0.82rem;
|
| 450 |
+
text-transform: uppercase;
|
| 451 |
+
letter-spacing: 0.04em;
|
| 452 |
+
}
|
| 453 |
+
.result-hint {
|
| 454 |
+
margin-top: 12px;
|
| 455 |
+
font-size: 0.88rem;
|
| 456 |
+
color: #7a5b20;
|
| 457 |
+
}
|
| 458 |
+
.panel-note {
|
| 459 |
+
background: #fffdf8;
|
| 460 |
+
border: 1px solid #e7dcc8;
|
| 461 |
+
border-radius: 18px;
|
| 462 |
+
padding: 14px 16px;
|
| 463 |
+
color: #4b4b4b;
|
| 464 |
+
margin-bottom: 12px;
|
| 465 |
+
}
|
| 466 |
+
"""
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
def _hero_html(manifest: Dict[str, Any]) -> str:
|
| 470 |
+
counts = manifest.get("counts") or {}
|
| 471 |
+
cards = [
|
| 472 |
+
("House members", int(counts.get("members", 0) or 0), "Members included in this released slice."),
|
| 473 |
+
("Scored events", int(counts.get("scored_events", 0) or 0), "Row-level public-record overlaps that survived into the release."),
|
| 474 |
+
("Relationship rows", int(counts.get("graph_links", 0) or 0), "Member-to-sector or member-to-recipient links in the public package."),
|
| 475 |
+
("Source records", int(counts.get("source_artifacts", 0) or 0), "Published source artifacts in the verification layer."),
|
| 476 |
+
]
|
| 477 |
+
card_html = "".join(
|
| 478 |
+
f"""
|
| 479 |
+
<div class="stat-card">
|
| 480 |
+
<div class="stat-label">{html.escape(label)}</div>
|
| 481 |
+
<div class="stat-value">{value:,}</div>
|
| 482 |
+
<div class="stat-help">{html.escape(help_text)}</div>
|
| 483 |
+
</div>
|
| 484 |
+
"""
|
| 485 |
+
for label, value, help_text in cards
|
| 486 |
+
)
|
| 487 |
+
return f"""
|
| 488 |
+
<section class="hero-panel">
|
| 489 |
+
<div class="hero-eyebrow">Public-record overlap explorer</div>
|
| 490 |
+
<div class="hero-title">{html.escape(str(manifest.get("title") or "Congress Public Records Slice"))}</div>
|
| 491 |
+
<div class="hero-lede">Quickly check whether a House member's disclosed financial or funding relationships line up with public legislative activity in the same area.</div>
|
| 492 |
+
<div class="hero-lede">Built for journalists, researchers, and curious citizens who want a faster path from a vague hunch to inspectable public records.</div>
|
| 493 |
+
<div class="hero-note"><strong>What this does not claim:</strong> this tool does not prove corruption, illegality, intent, or causality. It shows public-record overlap and evidence strength so people can inspect the records themselves.</div>
|
| 494 |
+
<div class="stat-grid">{card_html}</div>
|
| 495 |
+
</section>
|
| 496 |
+
"""
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
def _start_here_cards_html() -> str:
|
| 500 |
+
cards = [
|
| 501 |
+
(
|
| 502 |
+
"What this helps answer",
|
| 503 |
+
"Do a member's disclosed financial or funding relationships line up with public legislative activity in the same area?"
|
| 504 |
+
),
|
| 505 |
+
(
|
| 506 |
+
"Why someone might care",
|
| 507 |
+
"It helps move from a vague suspicion to a concrete set of records worth checking, without pulling multiple public sources by hand."
|
| 508 |
+
),
|
| 509 |
+
(
|
| 510 |
+
"What it does not mean",
|
| 511 |
+
"A visible relationship here is not a verdict. It is a signal that enough public records line up to justify closer reporting or review."
|
| 512 |
+
),
|
| 513 |
+
]
|
| 514 |
+
return "<div class=\"story-grid\">" + "".join(
|
| 515 |
+
f"""
|
| 516 |
+
<div class="story-card">
|
| 517 |
+
<div class="story-title">{html.escape(title)}</div>
|
| 518 |
+
<div class="story-body">{html.escape(body)}</div>
|
| 519 |
+
</div>
|
| 520 |
+
"""
|
| 521 |
+
for title, body in cards
|
| 522 |
+
) + "</div>"
|
| 523 |
+
|
| 524 |
+
|
| 525 |
+
def _source_table_html(manifest: Dict[str, Any]) -> str:
|
| 526 |
+
summary = manifest.get("methodology_summary") or {}
|
| 527 |
+
present_sources = set(_split_source_group_lines(summary.get("source_groups")))
|
| 528 |
+
source_pairs = [
|
| 529 |
+
("House Clerk financial disclosures and PTRs", "Show trades or financial holdings disclosed by House members."),
|
| 530 |
+
("House Clerk member directory and committee list", "Identify members and show committee context."),
|
| 531 |
+
("GovInfo BILLSTATUS bulk data", "Show bill activity tied to the same policy area."),
|
| 532 |
+
("House Clerk roll-call vote XML", "Show vote activity tied to the same policy area."),
|
| 533 |
+
("FEC public bulk downloads", "Add campaign-finance context where it is used in this release."),
|
| 534 |
+
("LDA public search pages", "Add lobbying visibility around the same issue areas."),
|
| 535 |
+
("House member community project funding disclosure pages", "Show member-published funding-request disclosures."),
|
| 536 |
+
("USAspending award pages used for some recipient matching", "Show public award records used to support some funding-recipient links."),
|
| 537 |
+
]
|
| 538 |
+
rows = "".join(
|
| 539 |
+
f"<tr><th>{html.escape(source)}</th><td>{html.escape(purpose)}</td></tr>"
|
| 540 |
+
for source, purpose in source_pairs
|
| 541 |
+
if source in present_sources
|
| 542 |
+
)
|
| 543 |
+
return f"""
|
| 544 |
+
<div class="source-card">
|
| 545 |
+
<div class="section-kicker">What data is in here</div>
|
| 546 |
+
<div class="source-title">Public source families used in this release</div>
|
| 547 |
+
<table class="source-table">
|
| 548 |
+
<thead><tr><th>Source</th><th>What it adds</th></tr></thead>
|
| 549 |
+
<tbody>{rows}</tbody>
|
| 550 |
+
</table>
|
| 551 |
+
</div>
|
| 552 |
+
"""
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def _glossary_html() -> str:
|
| 556 |
+
items = [
|
| 557 |
+
("Stronger support", "The released slice has clearer public support for this relationship."),
|
| 558 |
+
("Needs review", "There is some support, but it should still be read with caution."),
|
| 559 |
+
("Integrity-checked record", "The release includes a cryptographic fingerprint to help show the published record has not been altered."),
|
| 560 |
+
("Evidence window", "A coarse view of when the published records line up; it is not exact chronology."),
|
| 561 |
+
]
|
| 562 |
+
rows = "".join(
|
| 563 |
+
f"<div class=\"glossary-item\"><strong>{html.escape(term)}</strong><div>{html.escape(body)}</div></div>"
|
| 564 |
+
for term, body in items
|
| 565 |
+
)
|
| 566 |
+
return f"""
|
| 567 |
+
<div class="glossary-card">
|
| 568 |
+
<div class="section-kicker">Sticky terms</div>
|
| 569 |
+
<div class="glossary-title">Plain-English glossary</div>
|
| 570 |
+
<div class="glossary-list">{rows}</div>
|
| 571 |
+
</div>
|
| 572 |
+
"""
|
| 573 |
+
|
| 574 |
+
|
| 575 |
def _plain_status_label(value: str) -> str:
|
| 576 |
normalized = str(value or "").strip()
|
| 577 |
mapping = {
|
|
|
|
| 885 |
return "\n".join(lines)
|
| 886 |
|
| 887 |
|
| 888 |
+
def _overview_cards_html(
|
| 889 |
+
ranked: pd.DataFrame,
|
| 890 |
+
*,
|
| 891 |
+
member_query: str,
|
| 892 |
+
family: str,
|
| 893 |
+
only_strong_links: bool,
|
| 894 |
+
top_n: int,
|
| 895 |
+
) -> str:
|
| 896 |
+
if ranked.empty:
|
| 897 |
+
return (
|
| 898 |
+
"<div class=\"panel-note\">"
|
| 899 |
+
"<strong>No relationships match the current filters.</strong><br>"
|
| 900 |
+
"Try a different House member, switch from sectors to funding recipients, or turn off the stronger-links-only filter."
|
| 901 |
+
"</div>"
|
| 902 |
+
)
|
| 903 |
+
focus_names = [str(value) for value in ranked["member"].dropna().unique().tolist() if str(value).strip()]
|
| 904 |
+
focus_label = ", ".join(focus_names[:3]) or "this view"
|
| 905 |
+
intro = (
|
| 906 |
+
"<div class=\"panel-note\">"
|
| 907 |
+
f"<strong>Showing the top {min(int(top_n), len(ranked))} {_plain_family_label(family).lower()}</strong> "
|
| 908 |
+
f"for <strong>{html.escape(focus_label)}</strong>. "
|
| 909 |
+
f"Filtered to stronger links only: <strong>{'yes' if bool(only_strong_links) else 'no'}</strong>. "
|
| 910 |
+
"Pick one relationship below to open the plain-English explanation and evidence window."
|
| 911 |
+
"</div>"
|
| 912 |
+
)
|
| 913 |
+
cards: list[str] = []
|
| 914 |
+
for row in ranked.head(int(top_n)).to_dict("records"):
|
| 915 |
+
evidence_chips = [item.strip() for item in str(row.get("evidence", "") or "").split("|") if item.strip()]
|
| 916 |
+
chip_html = "".join(f"<span class=\"chip\">{html.escape(chip)}</span>" for chip in evidence_chips[:6])
|
| 917 |
+
supporting_rows = int(row.get("supporting rows", 0) or 0)
|
| 918 |
+
stronger_support = int(row.get("stronger support", 0) or 0)
|
| 919 |
+
needs_caution = int(row.get("needs caution", 0) or 0)
|
| 920 |
+
unresolved_refs = int(row.get("unresolved refs", 0) or 0)
|
| 921 |
+
cards.append(
|
| 922 |
+
f"""
|
| 923 |
+
<div class="result-card">
|
| 924 |
+
<div class="result-head">
|
| 925 |
+
<div>
|
| 926 |
+
<div class="result-rank">Rank #{int(row.get("rank", 0) or 0)}</div>
|
| 927 |
+
<div class="result-title">{html.escape(str(row.get("counterparty / sector", "") or ""))}</div>
|
| 928 |
+
<div class="result-subtitle">For {html.escape(str(row.get("member", "") or ""))} in the {_plain_family_label(family).lower()} view.</div>
|
| 929 |
+
</div>
|
| 930 |
+
<div class="metric-stack">
|
| 931 |
+
<span class="score-pill">Score {int(row.get("overall score", 0) or 0)}</span>
|
| 932 |
+
<span class="strength-pill">{html.escape(str(row.get("strength", "") or ""))}</span>
|
| 933 |
+
</div>
|
| 934 |
+
</div>
|
| 935 |
+
<div class="chip-row">{chip_html or '<span class="chip">published source support</span>'}</div>
|
| 936 |
+
<div class="meta-grid">
|
| 937 |
+
<div><strong>Evidence window</strong>{html.escape(str(row.get("time-window overlap", "") or ""))}</div>
|
| 938 |
+
<div><strong>Supporting rows</strong>{supporting_rows}</div>
|
| 939 |
+
<div><strong>Stronger support</strong>{stronger_support}</div>
|
| 940 |
+
<div><strong>Needs caution</strong>{needs_caution}</div>
|
| 941 |
+
<div><strong>Unresolved refs</strong>{unresolved_refs}</div>
|
| 942 |
+
</div>
|
| 943 |
+
<div class="result-hint">Use “Explain this link” below to open the detailed breakdown for this relationship.</div>
|
| 944 |
+
</div>
|
| 945 |
+
"""
|
| 946 |
+
)
|
| 947 |
+
if not str(member_query or "").strip():
|
| 948 |
+
cards.insert(
|
| 949 |
+
0,
|
| 950 |
+
"<div class=\"panel-note\"><strong>Tip:</strong> Type one House member name above for the clearest first read.</div>",
|
| 951 |
+
)
|
| 952 |
+
return intro + "<div class=\"result-list\">" + "".join(cards) + "</div>"
|
| 953 |
+
|
| 954 |
+
|
| 955 |
def _relationship_options(ranked: pd.DataFrame) -> list[tuple[str, str]]:
|
| 956 |
if ranked.empty:
|
| 957 |
return []
|
| 958 |
options: list[tuple[str, str]] = []
|
| 959 |
for row in ranked.to_dict("records"):
|
| 960 |
+
label = (
|
| 961 |
+
f"#{int(row['rank'])} {row['counterparty / sector']} "
|
| 962 |
+
f"— {row['strength']} (score {row['overall score']})"
|
| 963 |
+
)
|
| 964 |
options.append((label, str(row["relationship_id"])))
|
| 965 |
return options
|
| 966 |
|
|
|
|
| 991 |
f"- Supporting relationship rows: `{int(row.get('link_count', 0) or 0)}`",
|
| 992 |
f"- Stronger-support rows: `{int(row.get('linked_count', 0) or 0) if family == 'recipient' else int(row.get('strong_event_count', 0) or 0)}`",
|
| 993 |
f"- Caution / weaker rows: `{int(row.get('review_count', 0) or 0) if family == 'recipient' else int(row.get('weak_event_count', 0) or 0)}`",
|
| 994 |
+
f"- Integrity-checked source records attached: `{int(row.get('sha_backed_source_artifact_count', 0) or 0)}`",
|
| 995 |
f"- Unresolved source refs still counted: `{int(row.get('unresolved_source_ref_count', 0) or 0)}`",
|
| 996 |
f"- Evidence signals: `{', '.join(chips) if chips else 'published source support'}`",
|
| 997 |
f"- Time-window overlap: `{_window_overlap_text(row)}`",
|
|
|
|
| 1002 |
if urls:
|
| 1003 |
lines.extend(["", "#### Example published source URLs", ""])
|
| 1004 |
lines.extend(f"- {item}" for item in urls)
|
| 1005 |
+
lines.extend(
|
| 1006 |
+
[
|
| 1007 |
+
"",
|
| 1008 |
+
"#### Integrity note",
|
| 1009 |
+
"",
|
| 1010 |
+
"- `Integrity-checked` means the release includes a cryptographic fingerprint to help show a published record has not been altered.",
|
| 1011 |
+
]
|
| 1012 |
+
)
|
| 1013 |
return "\n".join(lines)
|
| 1014 |
|
| 1015 |
|
|
|
|
| 1154 |
"### Audit Summary",
|
| 1155 |
"",
|
| 1156 |
f"- Event rows in the audit index: `{int(event_payload.get('event_count', 0) or 0)}`",
|
| 1157 |
+
f"- Event rows with integrity-checked source records: `{int(event_payload.get('events_with_artifacts', 0) or 0)}`",
|
| 1158 |
f"- Stored-versus-lookup provenance mismatches: `{int(event_payload.get('stored_lookup_mismatch_count', 0) or 0)}`",
|
| 1159 |
f"- Claim-supporting rows in the audit index: `{int(claim_payload.get('row_count', 0) or 0)}`",
|
| 1160 |
+
f"- Claim-supporting rows with integrity-checked source records: `{int(claim_payload.get('rows_with_artifacts', 0) or 0)}`",
|
| 1161 |
"",
|
| 1162 |
+
"Use the tables below to inspect the public source URLs and integrity-checked source records that support the released rows.",
|
| 1163 |
]
|
| 1164 |
)
|
| 1165 |
|
|
|
|
| 1289 |
|
| 1290 |
def _event_detail(events: pd.DataFrame, provenance: pd.DataFrame, event_id: str) -> Tuple[str, pd.DataFrame]:
|
| 1291 |
if not event_id or event_id not in set(events["event_id"]):
|
| 1292 |
+
return "Select an event id to inspect source URLs and integrity-checked source records.", pd.DataFrame()
|
| 1293 |
event_row = events[events["event_id"] == event_id].head(1).to_dict("records")[0]
|
| 1294 |
prov_rows = provenance[provenance["row_key"] == event_id]
|
| 1295 |
member_name = str(event_row.get("member_name") or event_row.get("member_slug") or "Unknown member")
|
|
|
|
| 1307 |
"This panel summarizes one released event row from the public slice.",
|
| 1308 |
"",
|
| 1309 |
f"- Event id: `{event_id}`",
|
| 1310 |
+
f"- Event type: `{event_type}`",
|
| 1311 |
]
|
| 1312 |
if score_label:
|
| 1313 |
lines.append(f"- Score label: `{score_label}`")
|
|
|
|
| 1321 |
lines.extend(
|
| 1322 |
[
|
| 1323 |
f"- Attached source URLs in this row: `{int(event_row.get('source_ref_count', 0) or 0)}`",
|
| 1324 |
+
f"- Integrity-checked source records attached: `{int(event_row.get('sha_backed_source_artifact_count', 0) or 0)}`",
|
| 1325 |
f"- Unresolved source references still counted: `{int(event_row.get('unresolved_source_ref_count', 0) or 0)}`",
|
| 1326 |
f"- Matching provenance rows shown below: `{len(prov_rows)}`",
|
| 1327 |
]
|
|
|
|
| 1374 |
overview_member_limit = int(graph_defaults.get("overview_member_limit", 8))
|
| 1375 |
default_member_search = str(graph_defaults.get("default_member_search", "") or "")
|
| 1376 |
|
| 1377 |
+
def _overview_edges(member_query: str, family: str, only_strong: bool, top_n: int) -> pd.DataFrame:
|
| 1378 |
+
return _filter_graph(
|
| 1379 |
+
edges,
|
| 1380 |
+
family,
|
| 1381 |
+
member_query,
|
| 1382 |
+
"",
|
| 1383 |
+
"all",
|
| 1384 |
+
"stronger" if only_strong else "all",
|
| 1385 |
+
True,
|
| 1386 |
+
int(top_n),
|
| 1387 |
+
overview_member_limit,
|
| 1388 |
+
)
|
| 1389 |
+
|
| 1390 |
+
def _update_overview(member_query: str, family: str, only_strong: bool, top_n: int, relationship_id: str | None = None):
|
| 1391 |
+
filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
|
| 1392 |
+
ranked = _rank_relationships(filtered_edges)
|
| 1393 |
+
options = _relationship_options(ranked)
|
| 1394 |
+
valid_ids = {value for _, value in options}
|
| 1395 |
+
selected = relationship_id if relationship_id in valid_ids else (options[0][1] if options else None)
|
| 1396 |
+
return (
|
| 1397 |
+
_overview_summary_markdown(
|
| 1398 |
+
ranked,
|
| 1399 |
+
member_query=member_query,
|
| 1400 |
+
family=family,
|
| 1401 |
+
only_strong_links=only_strong,
|
| 1402 |
+
top_n=int(top_n),
|
| 1403 |
+
),
|
| 1404 |
+
_overview_cards_html(
|
| 1405 |
+
ranked,
|
| 1406 |
+
member_query=member_query,
|
| 1407 |
+
family=family,
|
| 1408 |
+
only_strong_links=only_strong,
|
| 1409 |
+
top_n=int(top_n),
|
| 1410 |
+
),
|
| 1411 |
+
gr.update(choices=options, value=selected),
|
| 1412 |
+
_relationship_detail_markdown(filtered_edges, selected or ""),
|
| 1413 |
+
_relationship_timeline_html(filtered_edges, selected or ""),
|
| 1414 |
+
)
|
| 1415 |
+
|
| 1416 |
+
def _update_overview_detail(member_query: str, family: str, only_strong: bool, top_n: int, relationship_id: str):
|
| 1417 |
+
filtered_edges = _overview_edges(member_query, family, only_strong, int(top_n))
|
| 1418 |
+
return _relationship_detail_markdown(filtered_edges, relationship_id), _relationship_timeline_html(filtered_edges, relationship_id)
|
| 1419 |
+
|
| 1420 |
+
def _update_graph(member_query: str, family: str, only_strong: bool, top_n: int):
|
| 1421 |
+
review_status = "stronger" if only_strong else "all"
|
| 1422 |
+
filtered_edges = _filter_graph(
|
| 1423 |
+
edges,
|
| 1424 |
+
family,
|
| 1425 |
+
member_query,
|
| 1426 |
+
"",
|
| 1427 |
+
"all",
|
| 1428 |
+
review_status,
|
| 1429 |
+
True,
|
| 1430 |
+
int(top_n),
|
| 1431 |
+
overview_member_limit,
|
| 1432 |
+
)
|
| 1433 |
+
filtered_nodes = nodes[
|
| 1434 |
+
nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))
|
| 1435 |
+
]
|
| 1436 |
+
summary = _graph_view_summary_markdown(
|
| 1437 |
+
filtered_edges,
|
| 1438 |
+
family=family,
|
| 1439 |
+
member_query=member_query,
|
| 1440 |
+
target_query="",
|
| 1441 |
+
review_status=review_status,
|
| 1442 |
+
max_edges=int(top_n),
|
| 1443 |
+
)
|
| 1444 |
+
return summary, _render_graph(filtered_nodes, filtered_edges), _graph_table(filtered_edges)
|
| 1445 |
+
|
| 1446 |
+
def _reset_graph(member_query: str):
|
| 1447 |
+
default_family = str(graph_defaults.get("relationship_family", "sector"))
|
| 1448 |
+
default_top_n = min(max(int(graph_defaults.get("max_edges", 20) or 20), 10), 30)
|
| 1449 |
+
filtered_edges = _filter_graph(
|
| 1450 |
+
edges,
|
| 1451 |
+
default_family,
|
| 1452 |
+
member_query,
|
| 1453 |
+
"",
|
| 1454 |
+
"all",
|
| 1455 |
+
"stronger",
|
| 1456 |
+
True,
|
| 1457 |
+
int(default_top_n),
|
| 1458 |
+
overview_member_limit,
|
| 1459 |
+
)
|
| 1460 |
+
filtered_nodes = nodes[
|
| 1461 |
+
nodes["node_id"].isin(set(filtered_edges["source_node_id"]).union(set(filtered_edges["target_node_id"])))
|
| 1462 |
+
]
|
| 1463 |
+
summary = _graph_view_summary_markdown(
|
| 1464 |
+
filtered_edges,
|
| 1465 |
+
family=default_family,
|
| 1466 |
+
member_query=member_query,
|
| 1467 |
+
target_query="",
|
| 1468 |
+
review_status="stronger",
|
| 1469 |
+
max_edges=int(default_top_n),
|
| 1470 |
+
)
|
| 1471 |
+
return (
|
| 1472 |
+
gr.update(value=default_family),
|
| 1473 |
+
gr.update(value=True),
|
| 1474 |
+
gr.update(value=int(default_top_n)),
|
| 1475 |
+
summary,
|
| 1476 |
+
_render_graph(filtered_nodes, filtered_edges),
|
| 1477 |
+
_graph_table(filtered_edges),
|
| 1478 |
+
)
|
| 1479 |
+
|
| 1480 |
+
def _update_events(member_query: str, event_type: str, score_label: str, text_query: str):
|
| 1481 |
+
return _filter_events(events, member_query, event_type, score_label, text_query)
|
| 1482 |
+
|
| 1483 |
+
with gr.Blocks(title=copy_payload.get("title", "Congress Public Records Slice"), css=_space_css()) as app:
|
| 1484 |
+
gr.HTML(_hero_html(manifest))
|
| 1485 |
+
gr.HTML(_start_here_cards_html())
|
| 1486 |
+
|
| 1487 |
+
with gr.Accordion("Start here: what this is and how to use it", open=True):
|
| 1488 |
gr.Markdown(
|
| 1489 |
+
"### What you can do in 30 seconds\n\n"
|
| 1490 |
+
"1. Search one House member.\n"
|
| 1491 |
+
"2. Read the ranked sectors or funding recipients.\n"
|
| 1492 |
+
"3. Pick one relationship in **Explain this link**.\n"
|
| 1493 |
+
"4. Open the example source URLs if you want to verify it yourself.\n\n"
|
| 1494 |
+
"Treat this as a lead generator for public-record review, not a conclusion machine."
|
| 1495 |
)
|
| 1496 |
+
gr.Markdown(_fictional_example_markdown())
|
| 1497 |
with gr.Row():
|
| 1498 |
+
gr.HTML(_source_table_html(manifest))
|
| 1499 |
+
gr.HTML(_glossary_html())
|
| 1500 |
+
|
| 1501 |
+
gr.Markdown("## Overview")
|
| 1502 |
+
gr.Markdown(
|
| 1503 |
+
"Search one House member, choose sectors or funding recipients, and start with the ranked list. "
|
| 1504 |
+
"This is the main reading path."
|
| 1505 |
+
)
|
| 1506 |
+
with gr.Row():
|
| 1507 |
+
overview_member = gr.Textbox(label="House member", value=default_member_search, scale=3)
|
| 1508 |
+
search_button = gr.Button("Search a House member", variant="primary", scale=1)
|
| 1509 |
+
with gr.Row():
|
| 1510 |
+
overview_family = gr.Radio(
|
| 1511 |
+
label="Show",
|
| 1512 |
+
choices=[("Sectors", "sector"), ("Funding recipients", "recipient")],
|
| 1513 |
+
value="sector",
|
| 1514 |
+
)
|
| 1515 |
+
overview_only_strong = gr.Checkbox(label="Only stronger links", value=True)
|
| 1516 |
+
overview_top_n = gr.Dropdown(label="Show top results", choices=[5, 10, 15, 20], value=10)
|
| 1517 |
+
if example_member_choices:
|
| 1518 |
+
gr.Examples(examples=example_member_choices, inputs=[overview_member], label="Try one of these example members")
|
| 1519 |
+
overview_summary_md = gr.Markdown()
|
| 1520 |
+
overview_cards = gr.HTML()
|
| 1521 |
+
|
| 1522 |
+
gr.Markdown("## Explain Link")
|
| 1523 |
+
relationship_choice = gr.Dropdown(label="Explain this link", choices=[], value=None)
|
| 1524 |
+
with gr.Row():
|
| 1525 |
overview_detail_md = gr.Markdown()
|
| 1526 |
overview_timeline_html = gr.HTML()
|
| 1527 |
|
| 1528 |
+
search_button.click(
|
| 1529 |
+
_update_overview,
|
| 1530 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1531 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1532 |
+
)
|
| 1533 |
+
overview_member.submit(
|
| 1534 |
+
_update_overview,
|
| 1535 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1536 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1537 |
+
)
|
| 1538 |
+
for control in (overview_family, overview_only_strong, overview_top_n):
|
| 1539 |
+
control.change(
|
| 1540 |
+
_update_overview,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1541 |
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1542 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1543 |
)
|
| 1544 |
+
relationship_choice.change(
|
| 1545 |
+
_update_overview_detail,
|
| 1546 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1547 |
+
[overview_detail_md, overview_timeline_html],
|
| 1548 |
+
)
|
| 1549 |
+
|
| 1550 |
+
with gr.Accordion("Explore the network map (optional)", open=False):
|
| 1551 |
+
gr.Markdown(
|
| 1552 |
+
"The ranked list above is the clearest way to read this release. "
|
| 1553 |
+
"Use the map below only if you want a visual view of the same relationships."
|
| 1554 |
)
|
|
|
|
| 1555 |
gr.Markdown(_graph_intro_markdown(data["graph_config"]))
|
| 1556 |
with gr.Row():
|
| 1557 |
+
graph_family = gr.Radio(
|
| 1558 |
+
label="Show",
|
| 1559 |
+
choices=graph_family_choices,
|
| 1560 |
+
value=str(graph_defaults.get("relationship_family", "sector")),
|
| 1561 |
+
)
|
| 1562 |
+
graph_only_strong = gr.Checkbox(label="Only stronger links", value=True)
|
| 1563 |
+
graph_top_n = gr.Dropdown(label="Show top", choices=[10, 20, 30], value=min(max(int(graph_defaults.get("max_edges", 20) or 20), 10), 30))
|
| 1564 |
+
graph_reset = gr.Button("Reset view")
|
|
|
|
|
|
|
| 1565 |
graph_summary_md = gr.Markdown()
|
| 1566 |
graph_html = gr.HTML()
|
| 1567 |
+
with gr.Accordion("Current relationships in this map", open=False):
|
| 1568 |
+
graph_df = gr.Dataframe(interactive=False)
|
| 1569 |
+
|
| 1570 |
+
for control in (graph_family, graph_only_strong, graph_top_n):
|
| 1571 |
+
control.change(
|
| 1572 |
+
_update_graph,
|
| 1573 |
+
[overview_member, graph_family, graph_only_strong, graph_top_n],
|
| 1574 |
+
[graph_summary_md, graph_html, graph_df],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1575 |
)
|
| 1576 |
+
graph_reset.click(
|
| 1577 |
+
_reset_graph,
|
| 1578 |
+
[overview_member],
|
| 1579 |
+
[graph_family, graph_only_strong, graph_top_n, graph_summary_md, graph_html, graph_df],
|
| 1580 |
+
)
|
| 1581 |
+
search_button.click(
|
| 1582 |
+
_update_graph,
|
| 1583 |
+
[overview_member, graph_family, graph_only_strong, graph_top_n],
|
| 1584 |
+
[graph_summary_md, graph_html, graph_df],
|
| 1585 |
+
)
|
| 1586 |
+
overview_member.submit(
|
| 1587 |
+
_update_graph,
|
| 1588 |
+
[overview_member, graph_family, graph_only_strong, graph_top_n],
|
| 1589 |
+
[graph_summary_md, graph_html, graph_df],
|
| 1590 |
+
)
|
| 1591 |
+
|
| 1592 |
+
with gr.Accordion("Audit & downloads", open=False):
|
| 1593 |
+
gr.Markdown(
|
| 1594 |
+
"Use these lower sections if you want the raw released event rows, the verification layer, or the download notes. "
|
| 1595 |
+
"Most people can start and stop with the overview above."
|
| 1596 |
+
)
|
| 1597 |
+
with gr.Accordion("Search released event rows", open=False):
|
| 1598 |
+
with gr.Row():
|
| 1599 |
+
member_query = gr.Textbox(label="Member name or slug")
|
| 1600 |
+
event_type = gr.Dropdown(label="Event type", choices=event_type_choices, value="all")
|
| 1601 |
+
score_label = gr.Dropdown(label="Score label", choices=score_label_choices, value="all")
|
| 1602 |
+
text_query = gr.Textbox(label="Issuer or sector search")
|
| 1603 |
+
explore_df = gr.Dataframe(value=events.head(100), interactive=False)
|
| 1604 |
+
for control in (member_query, event_type, score_label, text_query):
|
| 1605 |
+
control.change(_update_events, [member_query, event_type, score_label, text_query], explore_df)
|
| 1606 |
+
|
| 1607 |
+
with gr.Accordion("Inspect one released event row", open=False):
|
| 1608 |
+
event_id = gr.Dropdown(label="Event id", choices=event_id_choices, value=event_id_choices[0] if event_id_choices else None)
|
| 1609 |
+
event_detail_md = gr.Markdown()
|
| 1610 |
+
event_detail_df = gr.Dataframe(interactive=False)
|
| 1611 |
+
event_id.change(_event_detail, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
|
| 1612 |
+
app.load(_event_detail, [gr.State(events), gr.State(provenance), event_id], [event_detail_md, event_detail_df])
|
| 1613 |
+
|
| 1614 |
+
with gr.Accordion("Integrity-checked source records and audit summary", open=False):
|
| 1615 |
+
gr.Markdown(_consistency_summary_markdown(data["consistency"]))
|
| 1616 |
+
gr.Dataframe(value=data["artifact_index"].head(200), interactive=False)
|
| 1617 |
+
|
| 1618 |
+
with gr.Accordion("Methodology, limits, and downloads", open=False):
|
| 1619 |
+
gr.Markdown(copy_payload.get("landing_markdown", ""))
|
| 1620 |
+
gr.Markdown(copy_payload.get("downloads_markdown", ""))
|
| 1621 |
+
|
| 1622 |
+
app.load(
|
| 1623 |
+
_update_overview,
|
| 1624 |
+
[overview_member, overview_family, overview_only_strong, overview_top_n, relationship_choice],
|
| 1625 |
+
[overview_summary_md, overview_cards, relationship_choice, overview_detail_md, overview_timeline_html],
|
| 1626 |
+
)
|
| 1627 |
+
app.load(
|
| 1628 |
+
_update_graph,
|
| 1629 |
+
[overview_member, graph_family, graph_only_strong, graph_top_n],
|
| 1630 |
+
[graph_summary_md, graph_html, graph_df],
|
| 1631 |
+
)
|
| 1632 |
return app
|