Skip to content

feat: Add Explore Data page with interactive dashboard#15

Merged
William-Hill merged 6 commits into
mainfrom
feature/explore-data-page
Mar 2, 2026
Merged

feat: Add Explore Data page with interactive dashboard#15
William-Hill merged 6 commits into
mainfrom
feature/explore-data-page

Conversation

@William-Hill

@William-Hill William-Hill commented Mar 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add /explore page with race-disaggregated Census ACS indicators and state policy bills dashboard
  • Three new FastAPI endpoints: GET /api/explore/indicators, GET /api/explore/policies, GET /api/explore/states
  • Pydantic schemas for all explore endpoint request/response models
  • Four new Next.js components: StateMap (interactive choropleth), RacialGapChart (recharts bar chart), MetricFilterPanel (indicator/state selectors), PolicyTable (sortable bill listing)
  • Two standalone ingestion scripts: ingest_census_acs.py (Census Bureau API) and ingest_openstates.py (OpenStates API) with --dry-run support
  • CensusIndicator and PolicyBill SQLAlchemy models (committed earlier on this branch)
  • CodeRabbit config for automated PR reviews with path-specific review instructions
  • 52 tests passing, Next.js build clean

Test plan

  • pytest tests/ -v — all 52 tests pass
  • cd ui-nextjs && npm run build — TypeScript build clean, /explore route generated
  • python scripts/ingest_census_acs.py --help — prints usage
  • python scripts/ingest_openstates.py --help — prints usage
  • Manual: dev server shows "Explore Data" nav link alongside "Research"
  • Manual: /explore page renders map, filter panel, chart, and policy table

Summary by CodeRabbit

  • New Features

    • Explore Data page: interactive US choropleth, metric/race/year filters, state selection, racial gap charts, and policy tracker with topic/status filters.
    • UI navigation updated to include Explore Data.
  • Documentation

    • Detailed design and architecture docs for the Explore Data feature.
  • Tests

    • Integration and unit tests covering Explore APIs, schemas, and models.
  • Chores

    • Code reorganization for maintainability; Langfuse made optional.

William-Hill and others added 5 commits February 17, 2026 21:25
…it codes, Redis) (#13)

* fix: Correct import paths in migration scripts (d4bl.database → d4bl.infra.database)

* fix: Fix remaining broken import paths in test_db_connection and add_trace_id scripts

* fix: Correct import path in crew.py (d4bl.tools → d4bl.agents.tools)

* fix: Return True from archive_and_wipe_db.py main() on success so exit code is 0

* fix: Log original db_host before overwriting it in Docker detection

* fix: Authenticate redis-cli in healthcheck when requirepass is set

* fix: Preserve sequential task order when filtering by selected_agents in crew.py

* fix: Create d4bl.evals.runner with Langfuse-backed run_evals_and_log so run_evals.py imports correctly

* fix: Address CodeRabbit PR #13 review issues

- Fix import-time binding in test_db_connection.py by accessing
  async_session_maker via module reference after init_db() runs
- Wrap synchronous run_comprehensive_evaluation in asyncio.to_thread
  to avoid blocking the event loop in evals/runner.py
- Add return_exceptions=True to asyncio.gather and log per-job failures
- Remove spurious f-prefix from non-interpolated print in database.py
- Remove unnecessary # noqa: E402 comments from migration scripts
- Move sys.path.insert and D4Bl import to module level in test file

* fix: Address CodeRabbit PR #13 second-round review issues

- Refactor evals/runner.py: hoist sem and _evaluate_job outside the
  session block, replace async-for-get_db()+break with direct
  async_session_maker context manager (fixes Ruff B023 closure capture)
- Fix max_rows=0 treated as "no limit": use `if max_rows is not None:`
- Add strict=True to zip(jobs, results) for invariant enforcement
- Simplify database.py guard: replace redundant 3-part boolean with
  `db_host in ("localhost", "127.0.0.1")`
- Break long warning print strings to stay within 100-char line limit
… interactive dashboard

Implement the full /explore feature: three new API endpoints (indicators,
policies, states), Pydantic schemas, Census ACS and OpenStates ingestion
scripts, and a Next.js dashboard with StateMap, RacialGapChart,
MetricFilterPanel, and PolicyTable components. Add CodeRabbit config
for automated PR reviews.
@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

📝 Walkthrough

Walkthrough

Adds an Explore Data feature: new ORM models and indexes, three /api/explore endpoints and Pydantic schemas, Next.js explore page and UI components, two ingestion scripts (Census ACS, OpenStates), import path shifts to d4bl.infra.database, task-ordering enforcement, eval runner, tests, and config updates.

Changes

Cohort / File(s) Summary
Configuration & Infra
\.coderabbit\.yaml, docker-compose.observability.yml
New CodeRabbit config and review rules; Redis healthcheck updated to use CMD-SHELL with REDIS_AUTH for auth.
Database Models & Infra
src/d4bl/infra/database.py
Added CensusIndicator and PolicyBill ORM models with indexes and unique constraints; minor DB URL messaging tweak.
API & Schemas
src/d4bl/app/api.py, src/d4bl/app/schemas.py, src/d4bl/app/__init__.py
Three new endpoints (/api/explore/indicators, /api/explore/policies, /api/explore/states) and Pydantic response models; api submodule re-exported.
Ingestion Scripts
scripts/ingest_census_acs.py, scripts/ingest_openstates.py
New CLI ingestion scripts with dry-run, pagination, rate computation, status mapping, deduplication, and upsert (on_conflict_do_update) logic.
Import Path Updates
scripts/..., src/d4bl/crew.py
Multiple scripts updated to import d4bl.infra.database (e.g., init_db, engine, model imports); archive_and_wipe_db.py now returns bool.
Agent Task Ordering
src/d4bl/agents/crew.py
Added TASK_ORDER and deterministic task selection logic in crew().
Eval Runner
src/d4bl/evals/__init__.py, src/d4bl/evals/runner.py
New evals package and run_evals_and_log for concurrent, semaphore-controlled evaluation execution using thread pool.
Optional Langfuse
src/d4bl/services/langfuse/client.py
Made Langfuse SDK optional with try/except import and early-return when missing.
Frontend Explore UI
ui-nextjs/app/explore/page.tsx, ui-nextjs/app/layout.tsx, ui-nextjs/components/explore/*, ui-nextjs/package.json
New Explore page and four components (MetricFilterPanel, StateMap, RacialGapChart, PolicyTable); added react-simple-maps, recharts, d3-scale and type defs; layout nav updated.
Tests
tests/test_explore_*, tests/test_crew_task_ordering.py
Added unit/integration tests for models, schemas, API endpoints (mocked DB), and deterministic task ordering.
Docs & Plans
docs/plans/2026-02-19-explore-data-*.md
Design and implementation plan documents describing endpoints, DB schema, frontend layout, ingestion workflow, and test strategy.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ExplorePage as Explore Page<br/>(Next.js)
    participant API as Backend API<br/>(/api/explore/*)
    participant DB as PostgreSQL<br/>Database

    User->>ExplorePage: Visit /explore
    activate ExplorePage
    ExplorePage->>API: GET /api/explore/states
    activate API
    API->>DB: Aggregate metrics & bill counts
    DB-->>API: StateSummaryItem[]
    API-->>ExplorePage: StateSummaryItem[]
    deactivate API
    ExplorePage->>ExplorePage: Render map + filters

    User->>ExplorePage: Select metric/race/year
    ExplorePage->>API: GET /api/explore/indicators (filters)
    activate API
    API->>DB: Query census_indicators (filtered)
    DB-->>API: IndicatorItem[]
    API-->>ExplorePage: IndicatorItem[]
    deactivate API
    ExplorePage->>ExplorePage: Update map colors

    User->>ExplorePage: Click state
    ExplorePage->>API: GET /api/explore/policies (state)
    activate API
    API->>DB: Query policy_bills (by state)
    DB-->>API: PolicyBillItem[]
    API-->>ExplorePage: PolicyBillItem[]
    deactivate API
    ExplorePage->>ExplorePage: Render policy table + chart
    deactivate ExplorePage
Loading
sequenceDiagram
    participant Script as Ingest Script
    participant ExtAPI as External API
    participant DB as PostgreSQL<br/>Database

    Script->>Script: Parse CLI (--year, --state, --dry-run)
    Script->>DB: init_db / create_tables
    alt Census ACS ingest
        Script->>ExtAPI: Fetch ACS variables
        ExtAPI-->>Script: Rows
        Script->>Script: compute_rate / transform
    else OpenStates ingest
        Script->>ExtAPI: GraphQL bills (paginated)
        ExtAPI-->>Script: Bill nodes
        Script->>Script: map_status / dedupe
    end
    Script->>DB: Upsert records (on_conflict_do_update)
    DB-->>Script: ACK
    alt dry-run
        Script->>Script: Skip commit
    else
        Script->>DB: Commit
    end
    Script->>Script: Print summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped through code and dirt tonight,

I stitched new maps with data bright,
Census counts and policy bills,
Colors, charts, and gentle thrills—
Hooray, D4BL, the world looks right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding an Explore Data page with an interactive dashboard, which aligns with the primary feature introduced across the entire changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/explore-data-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (11)
tests/test_crew_task_ordering.py (1)

36-39: Remove redundant in-test imports/path mutation.

sys.path.insert and D4Bl import are already done at module scope; repeating them here adds noise and side effects.

🧹 Proposed cleanup
 def test_all_agents_full_canonical_order():
-    import sys
-    sys.path.insert(0, 'src')
-    from d4bl.agents.crew import D4Bl
     all_agents = list(D4Bl.AGENT_TASK_MAP.keys())
     names = _get_ordered_task_names(all_agents)
     assert names == D4Bl.TASK_ORDER
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_crew_task_ordering.py` around lines 36 - 39, Remove the redundant
in-test imports and path mutation by deleting the repeated sys.path.insert(0,
'src') and the duplicate from d4bl.agents.crew import D4Bl in the test block;
rely on the module-scope imports already present and use the existing D4Bl
symbol to compute all_agents = list(D4Bl.AGENT_TASK_MAP.keys()) without
re-importing or mutating sys.path in the test body.
src/d4bl/agents/crew.py (1)

65-74: Make TASK_ORDER immutable and typed.

Using a mutable class-level list here makes accidental runtime mutation possible and can break determinism across tests/runs. Convert to an immutable tuple with explicit Final type hint.

♻️ Proposed refactor
-from typing import List, Optional
+from typing import Final, List, Optional
@@
-    TASK_ORDER = [
+    TASK_ORDER: Final[tuple[str, ...]] = (
         "research_task",
         "analysis_task",
         "writing_task",
         "fact_checker_task",
         "citation_task",
         "bias_detection_task",
         "editor_task",
         "data_visualization_task",
-    ]
+    )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/d4bl/agents/crew.py` around lines 65 - 74, TASK_ORDER is currently a
mutable class-level list; change it to an immutable tuple and add an explicit
Final type hint to prevent runtime mutation and signal intent. Replace the list
literal assigned to TASK_ORDER with a tuple literal (use parentheses) and
annotate it with typing.Final (e.g., from typing import Final) so the symbol
TASK_ORDER is typed as a Final[tuple[str, ...]] (or Final[Tuple[str, ...]]
depending on imports). Ensure imports are updated to include Final (and Tuple if
needed).
tests/test_explore_models.py (1)

61-72: Align the test name with the asserted topic_tags behavior.

Line 61 says “defaults to empty list”, but Line 72 allows None.
Please either rename the test to nullable behavior or assert one exact default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_explore_models.py` around lines 61 - 72, The test
test_topic_tags_defaults_to_empty_list is misnamed because the assertion allows
PolicyBill.topic_tags to be None or a list; rename the test to reflect nullable
behavior (e.g., test_topic_tags_is_nullable_or_list) or change the assertion to
enforce a single expectation; update either the test function name or the
assertion around PolicyBill(topic_tags) so the name and behavior match
(reference: test_topic_tags_defaults_to_empty_list, PolicyBill, topic_tags).
ui-nextjs/components/explore/PolicyTable.tsx (2)

51-73: Expose pressed state on topic toggle buttons.

These controls behave as toggles but currently do not announce selected state to assistive tech.

♿ Suggested fix
         <button
           onClick={() => setActiveTopic(null)}
+          aria-pressed={activeTopic === null}
           className={`px-3 py-1 rounded-full text-xs font-medium transition-colors ${
             activeTopic === null
               ? 'bg-[`#00ff32`] text-black'
               : 'bg-[`#2a2a2a`] text-gray-400 border border-[`#404040`] hover:border-[`#00ff32`]'
           }`}
         >
@@
           <button
             key={topic}
             onClick={() => setActiveTopic(activeTopic === topic ? null : topic)}
+            aria-pressed={activeTopic === topic}
             className={`px-3 py-1 rounded-full text-xs font-medium capitalize transition-colors ${
               activeTopic === topic
                 ? 'bg-[`#00ff32`] text-black'
                 : 'bg-[`#2a2a2a`] text-gray-400 border border-[`#404040`] hover:border-[`#00ff32`]'
             }`}
           >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui-nextjs/components/explore/PolicyTable.tsx` around lines 51 - 73, The topic
buttons (including the "All" button) are toggle controls but do not expose their
pressed/selected state to assistive tech; update the buttons in PolicyTable (the
button rendering that uses activeTopic, setActiveTopic, and ALL_TOPICS) to
include an explicit ARIA state such as aria-pressed={activeTopic === topic} for
each topic button and aria-pressed={activeTopic === null} for the "All" button
(or aria-current="true"/"false" if you prefer current semantics) so screen
readers can announce which toggle is selected; ensure the attribute value
changes when onClick updates activeTopic.

83-85: Use a stable key for bill rows instead of array index.

Line 84 uses key={i}, which can cause stale row reconciliation when filters change.

♻️ Suggested fix
-          {filtered.map((bill, i) => (
-            <div key={i} className="py-3 flex items-start justify-between gap-4">
+          {filtered.map((bill) => (
+            <div key={`${bill.state}-${bill.bill_number}`} className="py-3 flex items-start justify-between gap-4">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui-nextjs/components/explore/PolicyTable.tsx` around lines 83 - 85, The
mapped rows in PolicyTable use the array index (key={i}) which can break React
reconciliation when filtering; replace the unstable index key with a stable
unique identifier from each bill (e.g., bill.id, bill.slug, bill.billId or a
deterministic composite like `${bill.year}-${bill.number}`) in the map over
filtered so the <div key=...> uses that stable property instead of i; ensure the
chosen property is unique and present on the bill objects returned to avoid
fallback to indexing.
ui-nextjs/components/explore/RacialGapChart.tsx (1)

91-93: Use a stable key for chart cells.

Line 92 uses array index as key. Prefer a deterministic key from the mapped data item.

♻️ Suggested fix
           <Bar dataKey="value" radius={[4, 4, 0, 0]} barSize={40}>
             {data.map((entry, index) => (
-              <Cell key={index} fill={entry.fill} />
+              <Cell key={entry.race} fill={entry.fill} />
             ))}
           </Bar>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui-nextjs/components/explore/RacialGapChart.tsx` around lines 91 - 93, The
Cell elements in the data.map inside RacialGapChart use the array index as the
key; replace the index key with a stable, deterministic identifier from each
entry (e.g., entry.id, entry.label, or a composed key like
`${entry.label}-${entry.fill}`), or add a unique id to the data items before
mapping, and set that property as the key on <Cell> instead of using the index.
scripts/ingest_census_acs.py (3)

74-75: Add strict=True to zip() to catch length mismatches.

If the Census API returns malformed data where row lengths don't match header length, this would silently truncate data. Adding strict=True will raise a ValueError if lengths differ, making debugging easier.

🛡️ Suggested fix
     headers = rows[0]
-    return [dict(zip(headers, row)) for row in rows[1:]]
+    return [dict(zip(headers, row, strict=True)) for row in rows[1:]]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ingest_census_acs.py` around lines 74 - 75, The list comprehension
that builds records from headers and rows uses zip(headers, row) which silently
truncates when lengths mismatch; change the zip call in the return expression to
zip(headers, row, strict=True) so a ValueError is raised on length mismatches,
and wrap the comprehension (or the calling function) in a try/except to catch
ValueError, log the offending headers and row for debugging, and re-raise or
handle appropriately; update the symbols headers, rows and the zip(...) usage in
ingest_census_acs.py.

104-110: Clarify the tuple order comment.

The comment says "(denominator, numerator)" but vars is defined as (total, owner-occupied) in METRIC_VARS. The code correctly passes vars[1] (numerator) and vars[0] (denominator) to compute_rate, so the logic is correct, but the comment could be clearer.

📝 Suggested clarification
-                # For rate metrics, vars is a tuple (denominator, numerator)
+                # For rate metrics, vars is a tuple (denominator, numerator) where
+                # vars[0] = denominator (e.g., total households), vars[1] = numerator (e.g., owner-occupied)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ingest_census_acs.py` around lines 104 - 110, The inline comment
about tuple order is misleading; clarify that for rate metrics the tuple in
METRIC_VARS is (denominator, numerator) but defined as (total, owner-occupied)
so the code intentionally uses vars[1] as numerator and vars[0] as denominator
when calling compute_rate; update the comment above the tuple handling in the
block that calls fetch_acs and compute_rate (referencing vars, METRIC_VARS,
fetch_acs, compute_rate, rows, fips) to explicitly state the tuple element
meaning and that compute_rate receives (numerator, denominator) as
compute_rate(vars[1], vars[0]) to avoid confusion.

64-64: Consider using unpacking syntax for list concatenation.

Using spread operator is more idiomatic and slightly cleaner.

✨ Suggested refactor
-    get_str = ",".join(["NAME"] + vars)
+    get_str = ",".join(["NAME", *vars])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/ingest_census_acs.py` at line 64, Replace the explicit list
concatenation when building get_str with list unpacking for clarity: instead of
creating a new list via ["NAME"] + vars inside the ",".join call, construct the
list using unpacking (e.g., ["NAME", *vars]) before joining; update the
expression that assigns get_str to use this unpacked list form to achieve the
same output more idiomatically.
src/d4bl/infra/database.py (1)

9-9: Line exceeds 100 character limit.

This import line is 103 characters. Consider splitting it across multiple lines for readability.

As per coding guidelines: "Enforce 100 character maximum line length"

✨ Suggested refactor
-from sqlalchemy import JSON, Text, Column, String, DateTime, Float, Integer, Date, Index, UniqueConstraint
+from sqlalchemy import (
+    JSON, Text, Column, String, DateTime, Float, Integer, Date, Index, UniqueConstraint
+)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/d4bl/infra/database.py` at line 9, The long import statement importing
JSON, Text, Column, String, DateTime, Float, Integer, Date, Index,
UniqueConstraint exceeds the 100-character limit; break this single line into
multiple shorter import lines (for example grouping related symbols or
one-per-line) so the line length is under 100 characters while keeping the same
imported names (JSON, Text, Column, String, DateTime, Float, Integer, Date,
Index, UniqueConstraint).
src/d4bl/app/api.py (1)

549-553: Topic filter may produce unexpected matches.

The JSON-to-string cast with contains() could match partial strings within the JSON serialization (e.g., searching for "health" could match ["mental health", "public health"] but also potentially match keys or other JSON artifacts). Consider using PostgreSQL's native JSON containment operator (@>) if precision is critical.

💡 Alternative using JSON containment (PostgreSQL-specific)
# For exact array element matching:
from sqlalchemy.dialects.postgresql import JSONB
# If topic_tags were JSONB:
# query = query.where(PolicyBill.topic_tags.contains([topic]))

# Current approach works for substring matching which may be acceptable
# for the use case (matching "housing" in ["affordable housing"])

For this dashboard use case, the current approach is likely sufficient since topic strings are controlled via FOCUS_SUBJECTS in the ingestion script.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/d4bl/app/api.py` around lines 549 - 553, The current filter uses
PolicyBill.topic_tags.cast(String).contains(topic) which can produce
partial/false matches; replace it with PostgreSQL JSON containment to match
exact array elements by using PolicyBill.topic_tags.contains([topic]) (ensure
topic_tags is JSONB or cast to postgresql.JSONB) and import JSONB from
sqlalchemy.dialects.postgresql as needed; keep a fallback to the substring
approach only if JSONB is not available or portability is required.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.coderabbit.yaml:
- Line 11: The setting auto_incremental_review is currently at top-level and
must be moved under the reviews.auto_review object to match the documented
schema; update the .coderabbit.yaml by removing the top-level
auto_incremental_review entry and adding it as reviews:\n  auto_review:\n   
auto_incremental_review: <value> so the tool will pick up the option (refer to
the auto_incremental_review and reviews.auto_review keys to locate where to
change).

In `@docs/plans/2026-02-19-explore-data-design.md`:
- Around line 134-160: The fenced ASCII layout block (the NAV/HERO/CHOROPLETH
map diagram) lacks a language tag and triggers MD040; fix it by adding a
language identifier (e.g., "text") after the opening triple backticks of that
fenced code block so it reads ```text, keeping the block content unchanged;
locate the block by its unique ASCII header (the "NAV: D4BL  |  Research  | 
Explore Data" line) or the choropleth/map diagram to update the fence.

In `@scripts/ingest_openstates.py`:
- Around line 212-240: The upsert for PolicyBill (the stmt using
insert(PolicyBill) and .on_conflict_do_update) currently omits the model's
introduced_date and last_action_date columns; update the .values(...) and the
.on_conflict_do_update(..., set={...}) maps to include these two fields (e.g.,
mapping introduced_date to bill.get("createdAt") and last_action_date to
bill.get("updatedAt") or whatever keys the GraphQL response provides), or if
those keys don't exist add them to the GraphQL query and then use bill.get(...)
to populate both the initial insert and the conflict update.

In `@src/d4bl/app/schemas.py`:
- Around line 85-116: Add short class-level docstrings for the public Pydantic
models IndicatorItem, PolicyBillItem, and StateSummaryItem in schemas.py: for
each class (IndicatorItem, PolicyBillItem, StateSummaryItem) add a one- or
two-sentence docstring at top of the class describing its purpose and the key
fields it represents (e.g., IndicatorItem represents a single indicator
observation with fips_code, year, metric, value; PolicyBillItem represents a
legislative bill summary with state, bill_number, title, status;
StateSummaryItem summarizes per-state metadata like state_fips,
available_metrics, bill_count, latest_year). Ensure docstrings are simple,
placed immediately under the class definition, and follow existing docstring
style used in the codebase.

In `@src/d4bl/evals/runner.py`:
- Around line 43-45: The code currently does research_output =
str(result_dict.get("result") or "") which stringifies the entire result object;
instead, extract the actual answer text from the result payload (e.g., check
result_dict.get("result") for common keys like "answer", "text", "output" or
nested result["response"] and use that value) and assign that string to
research_output so evaluators get the human-readable answer; replace the
str(...) usage in the block that defines research_output (and the similar block
around lines 56-60) to prefer result_dict["result"]["answer"] or fall back to
other text keys, handling missing keys by using an empty string.
- Around line 61-62: The current call in runner.py sets trace_id=str(job.job_id)
which ignores an existing ResearchJob.trace_id and breaks correlation; update
the code that constructs the trace (where trace_id is passed—look for the block
using trace_id=str(job.job_id)) to use job.trace_id when present, e.g. set
trace_id = str(job.trace_id) if job.trace_id else str(job.job_id) (or equivalent
null-coalescing) so evaluations use persisted trace IDs while falling back to
job_id.

In `@tests/test_crew_task_ordering.py`:
- Around line 8-16: The current helper _get_ordered_task_names merely recomputes
order from D4Bl.AGENT_TASK_MAP and D4Bl.TASK_ORDER; update the tests to exercise
D4Bl.crew() directly: instantiate D4Bl, call its crew(...) method with
selected_agents set, then assert the returned crew object's crew.tasks (or
.tasks attribute on the object returned by D4Bl.crew) yields the expected task
order (compare task.name or str representation) instead of using
AGENT_TASK_MAP/TASK_ORDER directly; keep the helper only for expected-name
computation if needed but switch assertions to verify D4Bl.crew() behavior
(referencing D4Bl.crew, crew.tasks, _get_ordered_task_names,
D4Bl.AGENT_TASK_MAP, D4Bl.TASK_ORDER).

In `@tests/test_explore_schemas.py`:
- Around line 28-33: Update the test_metric_required unit test so it only
validates the absence of the metric field: instantiate IndicatorItem with valid
values for all other required attributes (e.g., fips_code, geography_name, and
any other mandatory fields on the model) but intentionally omit metric, then
assert that creating IndicatorItem raises a pydantic.ValidationError; modify the
test function name/test body (test_metric_required and the IndicatorItem
constructor call) to pass concrete valid values for all required fields except
metric so the failure isolates metric being required.

In `@ui-nextjs/app/explore/page.tsx`:
- Around line 75-77: Replace the permissive catch typing "catch (e: any)" with
"catch (e: unknown)" and narrow the type before using e.message; for example in
the async blocks around setError in app/explore/page.tsx (the catch blocks that
currently call setError(e.message)), check "if (e instanceof Error)
setError(e.message) else setError(String(e))" (or similar explicit narrowing) so
you handle non-Error throwables safely; apply the same change to the other two
catch sites referenced (lines ~96–98 and ~117–119).
- Around line 64-128: Old async fetches (fetchMapData, fetchChartData,
fetchBills) can be overwritten by stale responses and use catch(e: any) which
breaks strict TS; fix by making each fetch use an AbortController (create a
controller inside each fetch callback and pass controller.signal to fetch) and
ensure responses only update setMapIndicators, setChartIndicators, setBills,
setError if the request was not aborted, and wire a cleanup in the useEffect (or
return a cancel function) to abort pending controllers when filters change; also
change all catch blocks to catch(e: unknown) and convert to a safe message (e
instanceof Error ? e.message : String(e)) before calling setError.

In `@ui-nextjs/app/layout.tsx`:
- Around line 18-19: The metadata description string in layout.tsx (the
description property of the metadata object) exceeds the 100-character line
limit; split the long description into multiple shorter string literals (for
example by concatenating two strings or using an array.join) so the resulting
code for the description property stays under 100 characters per line while
preserving the exact text, updating the description property in the metadata
object accordingly.

In `@ui-nextjs/components/explore/StateMap.tsx`:
- Around line 54-72: The Geography elements are not keyboard-accessible: update
the Geography component usage (the element with props geography, fill, stroke,
onClick, onMouseEnter/onMouseLeave) to include tabIndex={0}, role="button", and
a descriptive aria-label (e.g., using geo.properties.name and value), add an
onKeyDown handler that triggers onSelectState(fips, geo.properties.name) when
Enter or Space is pressed, and remove outline: 'none' from the default style so
focused elements get a visible focus outline; ensure existing tooltip behavior
(setTooltip) remains on focus/blur or via keyboard activation as appropriate.

---

Nitpick comments:
In `@scripts/ingest_census_acs.py`:
- Around line 74-75: The list comprehension that builds records from headers and
rows uses zip(headers, row) which silently truncates when lengths mismatch;
change the zip call in the return expression to zip(headers, row, strict=True)
so a ValueError is raised on length mismatches, and wrap the comprehension (or
the calling function) in a try/except to catch ValueError, log the offending
headers and row for debugging, and re-raise or handle appropriately; update the
symbols headers, rows and the zip(...) usage in ingest_census_acs.py.
- Around line 104-110: The inline comment about tuple order is misleading;
clarify that for rate metrics the tuple in METRIC_VARS is (denominator,
numerator) but defined as (total, owner-occupied) so the code intentionally uses
vars[1] as numerator and vars[0] as denominator when calling compute_rate;
update the comment above the tuple handling in the block that calls fetch_acs
and compute_rate (referencing vars, METRIC_VARS, fetch_acs, compute_rate, rows,
fips) to explicitly state the tuple element meaning and that compute_rate
receives (numerator, denominator) as compute_rate(vars[1], vars[0]) to avoid
confusion.
- Line 64: Replace the explicit list concatenation when building get_str with
list unpacking for clarity: instead of creating a new list via ["NAME"] + vars
inside the ",".join call, construct the list using unpacking (e.g., ["NAME",
*vars]) before joining; update the expression that assigns get_str to use this
unpacked list form to achieve the same output more idiomatically.

In `@src/d4bl/agents/crew.py`:
- Around line 65-74: TASK_ORDER is currently a mutable class-level list; change
it to an immutable tuple and add an explicit Final type hint to prevent runtime
mutation and signal intent. Replace the list literal assigned to TASK_ORDER with
a tuple literal (use parentheses) and annotate it with typing.Final (e.g., from
typing import Final) so the symbol TASK_ORDER is typed as a Final[tuple[str,
...]] (or Final[Tuple[str, ...]] depending on imports). Ensure imports are
updated to include Final (and Tuple if needed).

In `@src/d4bl/app/api.py`:
- Around line 549-553: The current filter uses
PolicyBill.topic_tags.cast(String).contains(topic) which can produce
partial/false matches; replace it with PostgreSQL JSON containment to match
exact array elements by using PolicyBill.topic_tags.contains([topic]) (ensure
topic_tags is JSONB or cast to postgresql.JSONB) and import JSONB from
sqlalchemy.dialects.postgresql as needed; keep a fallback to the substring
approach only if JSONB is not available or portability is required.

In `@src/d4bl/infra/database.py`:
- Line 9: The long import statement importing JSON, Text, Column, String,
DateTime, Float, Integer, Date, Index, UniqueConstraint exceeds the
100-character limit; break this single line into multiple shorter import lines
(for example grouping related symbols or one-per-line) so the line length is
under 100 characters while keeping the same imported names (JSON, Text, Column,
String, DateTime, Float, Integer, Date, Index, UniqueConstraint).

In `@tests/test_crew_task_ordering.py`:
- Around line 36-39: Remove the redundant in-test imports and path mutation by
deleting the repeated sys.path.insert(0, 'src') and the duplicate from
d4bl.agents.crew import D4Bl in the test block; rely on the module-scope imports
already present and use the existing D4Bl symbol to compute all_agents =
list(D4Bl.AGENT_TASK_MAP.keys()) without re-importing or mutating sys.path in
the test body.

In `@tests/test_explore_models.py`:
- Around line 61-72: The test test_topic_tags_defaults_to_empty_list is misnamed
because the assertion allows PolicyBill.topic_tags to be None or a list; rename
the test to reflect nullable behavior (e.g.,
test_topic_tags_is_nullable_or_list) or change the assertion to enforce a single
expectation; update either the test function name or the assertion around
PolicyBill(topic_tags) so the name and behavior match (reference:
test_topic_tags_defaults_to_empty_list, PolicyBill, topic_tags).

In `@ui-nextjs/components/explore/PolicyTable.tsx`:
- Around line 51-73: The topic buttons (including the "All" button) are toggle
controls but do not expose their pressed/selected state to assistive tech;
update the buttons in PolicyTable (the button rendering that uses activeTopic,
setActiveTopic, and ALL_TOPICS) to include an explicit ARIA state such as
aria-pressed={activeTopic === topic} for each topic button and
aria-pressed={activeTopic === null} for the "All" button (or
aria-current="true"/"false" if you prefer current semantics) so screen readers
can announce which toggle is selected; ensure the attribute value changes when
onClick updates activeTopic.
- Around line 83-85: The mapped rows in PolicyTable use the array index
(key={i}) which can break React reconciliation when filtering; replace the
unstable index key with a stable unique identifier from each bill (e.g.,
bill.id, bill.slug, bill.billId or a deterministic composite like
`${bill.year}-${bill.number}`) in the map over filtered so the <div key=...>
uses that stable property instead of i; ensure the chosen property is unique and
present on the bill objects returned to avoid fallback to indexing.

In `@ui-nextjs/components/explore/RacialGapChart.tsx`:
- Around line 91-93: The Cell elements in the data.map inside RacialGapChart use
the array index as the key; replace the index key with a stable, deterministic
identifier from each entry (e.g., entry.id, entry.label, or a composed key like
`${entry.label}-${entry.fill}`), or add a unique id to the data items before
mapping, and set that property as the key on <Cell> instead of using the index.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9fe5cae and 8cc2d37.

⛔ Files ignored due to path filters (1)
  • ui-nextjs/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (33)
  • .coderabbit.yaml
  • docker-compose.observability.yml
  • docs/plans/2026-02-19-explore-data-design.md
  • docs/plans/2026-02-19-explore-data-page.md
  • scripts/add_evaluation_results_table.py
  • scripts/add_job_id_to_evaluations.py
  • scripts/add_research_data_column.py
  • scripts/add_trace_id_to_research_jobs.py
  • scripts/archive_and_wipe_db.py
  • scripts/ingest_census_acs.py
  • scripts/ingest_openstates.py
  • scripts/init_db.py
  • scripts/test_db_connection.py
  • src/d4bl/agents/crew.py
  • src/d4bl/app/__init__.py
  • src/d4bl/app/api.py
  • src/d4bl/app/schemas.py
  • src/d4bl/crew.py
  • src/d4bl/evals/__init__.py
  • src/d4bl/evals/runner.py
  • src/d4bl/infra/database.py
  • src/d4bl/services/langfuse/client.py
  • tests/test_crew_task_ordering.py
  • tests/test_explore_api.py
  • tests/test_explore_models.py
  • tests/test_explore_schemas.py
  • ui-nextjs/app/explore/page.tsx
  • ui-nextjs/app/layout.tsx
  • ui-nextjs/components/explore/MetricFilterPanel.tsx
  • ui-nextjs/components/explore/PolicyTable.tsx
  • ui-nextjs/components/explore/RacialGapChart.tsx
  • ui-nextjs/components/explore/StateMap.tsx
  • ui-nextjs/package.json

Comment thread .coderabbit.yaml Outdated
Comment thread docs/plans/2026-02-19-explore-data-design.md Outdated
Comment thread scripts/ingest_openstates.py
Comment thread src/d4bl/app/schemas.py
Comment thread src/d4bl/evals/runner.py
Comment thread tests/test_explore_schemas.py
Comment thread ui-nextjs/app/explore/page.tsx Outdated
Comment thread ui-nextjs/app/explore/page.tsx Outdated
Comment thread ui-nextjs/app/layout.tsx Outdated
Comment thread ui-nextjs/components/explore/StateMap.tsx
- Fix .coderabbit.yaml nesting (auto_incremental_review under auto_review)
- Add AbortController to explore page fetches to prevent stale responses
- Replace catch(e: any) with catch(e: unknown) and proper type narrowing
- Add keyboard accessibility to StateMap (tabIndex, role, onKeyDown)
- Add docstrings to IndicatorItem, PolicyBillItem, StateSummaryItem
- Map introduced_date and last_action_date in OpenStates ingestion upsert
- Isolate metric-required test to omit only the metric field
- Add language tag to fenced code block in design doc
- Split long metadata description line in layout.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
ui-nextjs/app/layout.tsx (1)

42-58: Well-structured navigation with good accessibility baseline.

The nav uses semantic HTML (<nav> element), proper Link components for client-side routing, and Tailwind CSS styling as per guidelines.

Optional enhancement: Consider adding visual indication for the active route using usePathname() from next/navigation. This improves UX by showing users which page they're currently viewing.

♻️ Optional: Add active link state
// At the top of the file, add:
"use client";
import { usePathname } from "next/navigation";

// Inside RootLayout, before the return:
const pathname = usePathname();

// Then update Link classNames to include active state:
<Link
  href="/"
  className={`text-sm transition-colors ${
    pathname === "/" ? "text-[`#00ff32`]" : "text-gray-300 hover:text-[`#00ff32`]"
  }`}
>
  Research
</Link>

Note: This would convert the layout to a Client Component. If you prefer keeping it as a Server Component, extract the nav into a separate <NavBar /> client component instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui-nextjs/app/layout.tsx` around lines 42 - 58, Add an active-route visual
state to the navigation by using usePathname() from next/navigation: convert the
nav into a client component (add "use client") or extract it into a new NavBar
client component, import usePathname, call usePathname() inside the component
(e.g., in RootLayout or NavBar) and update the Link className logic for
"Research" and "Explore Data" so they render the active style (e.g.,
text-[`#00ff32`]) when pathname matches the link href and the default gray/hover
style otherwise.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@ui-nextjs/app/layout.tsx`:
- Around line 42-58: Add an active-route visual state to the navigation by using
usePathname() from next/navigation: convert the nav into a client component (add
"use client") or extract it into a new NavBar client component, import
usePathname, call usePathname() inside the component (e.g., in RootLayout or
NavBar) and update the Link className logic for "Research" and "Explore Data" so
they render the active style (e.g., text-[`#00ff32`]) when pathname matches the
link href and the default gray/hover style otherwise.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc2d37 and af50577.

📒 Files selected for processing (8)
  • .coderabbit.yaml
  • docs/plans/2026-02-19-explore-data-design.md
  • scripts/ingest_openstates.py
  • src/d4bl/app/schemas.py
  • tests/test_explore_schemas.py
  • ui-nextjs/app/explore/page.tsx
  • ui-nextjs/app/layout.tsx
  • ui-nextjs/components/explore/StateMap.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • ui-nextjs/app/explore/page.tsx
  • .coderabbit.yaml

@William-Hill

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant