Phase 3 AI hub LLM seam: live analyst/critic + NVDA smoke - #4
Conversation
…LI). Land EvidenceCard/CriticReview contracts, gate projection, numeric allowlist, citation ingest, brain Typer commands, and offline Properties so Fable can one-shot the LLM analyst/critic path on the demo_eligible pack without redesigning packet law. Co-authored-by: Cursor <cursoragent@cursor.com>
- agents/llm_client.py: LiveLLMClient — litellm.Router (Gemini Flash →
Groq → Ollama) + instructor JSON mode; sole litellm import site (C4);
DEFAULT_MAX_TOKENS=8192 (Gemini 3.x thinking shares the completion
budget), reasoning_effort=low, fail-fast after ROUTER_MAX_FAILURES.
Fixture client stays the CI default.
- agents/analyst.py / critic.py: evidence-bound prompts — ScorePacket
numbers + evidence_refs for the analyst, four-key gate whitelist for
the critic; quotable numbers pre-rendered at display precision;
confidence may only be capped/lowered.
- agents/runner.py: happy path wired to real prompts for any client;
ids/caps/provenance stamped server-side, never model-authored; one
corrective retry feeding the validator error back (live only).
- cli_desk.py: analyze-symbol happy path via FactorEngine over the
configured universe (--price-source, --spec-id); blocked E1 path
unchanged (MISSING/CONTRADICTORY → zero LLM calls).
- cards/validators.py: integral float tokens ("ranks 3.") may match the
int bucket — live-run regression.
- read_api.py: DuckDB UUID column coerced to str in get_quality_report.
- scripts/live_ai_card_smoke.py: env-gated NVDA smoke — live card +
CriticReview under data/cards/, planted false Sharpe fails closed,
pass/fail stdout only; never part of default pytest.
- tests: fixture happy-path/fail-closed/fail-fast coverage + Property 23
(497 passed offline).
- .env.example: gemini-2.0-flash retired 2026-06-01 → gemini-3.5-flash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wire critique-spec through FactorEngine, default tiingo + vault mirror in smoke, and sync Phase 3 docs so the green branch is PR-ready. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
📝 WalkthroughWalkthroughThis PR adds the Phase 3 AI brain hub: structured evidence cards and critic reviews, constrained LLM routing, deterministic citation ingestion, desk CLI workflows, paper-journal integration, live smoke validation, and offline/security test coverage. ChangesPhase 3 AI brain hub
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant DeskCLI
participant AgentRunner
participant LLMClient
participant Validators
participant CardStorage
DeskCLI->>AgentRunner: assemble symbol bundle
AgentRunner->>LLMClient: request structured EvidenceCard
LLMClient->>Validators: return model output
Validators->>CardStorage: write validated card
DeskCLI->>AgentRunner: request CriticReview
AgentRunner->>LLMClient: request structured review
LLMClient->>Validators: return review output
Validators->>CardStorage: write validated review
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/research_data/paper/store.py (1)
40-44: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winAdd a migration for
paper_theses.init_schema()only creates the table, so existing databases will keep the old schema whilepropose_thesis()now writessource_card_id. Reads already handle the legacy row shape, but inserts will fail until the column is added.src/research_data/paper/store.py:40🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/research_data/paper/store.py` around lines 40 - 44, Add a schema migration for the existing paper_theses table and invoke it from init_schema(), ensuring source_card_id is added when absent while preserving compatibility with databases where the column already exists. Keep propose_thesis() inserts and existing legacy-read handling unchanged.
🧹 Nitpick comments (5)
src/research_data/agents/runner.py (1)
98-102: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReaching into
FixtureLLMClient._canned(private attribute) from another module.Consider exposing a small public accessor (e.g.
client.has_canned(EvidenceCard)) onFixtureLLMClientinstead of readingclient._canneddirectly, to keep the fixture/live client boundary encapsulated.Also applies to: 177-181
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/research_data/agents/runner.py` around lines 98 - 102, Replace direct access to FixtureLLMClient._canned in the runner validation checks with a small public FixtureLLMClient accessor such as has_canned(EvidenceCard). Add the accessor on FixtureLLMClient and use it in both affected checks, preserving the existing RunnerError behavior.src/research_data/brain/store.py (1)
490-513: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoffPositional dual-shape row parsing is fragile to future schema drift.
_row_to_specbranches onlen(row) >= 17to decide which positional mapping to use. Any further column addition will require a third branch and careful index bookkeeping; a name-based row-to-model mapper (e.g. via DuckDB's cursor.descriptionor a dict-cursor) would be more robust against future schema changes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/research_data/brain/store.py` around lines 490 - 513, Replace the positional length check in _row_to_spec with name-based column mapping using the query cursor’s column metadata or an equivalent dict-row representation. Map StrategySpec fields by column name so added or reordered schema columns do not require new positional branches, while preserving compatibility with existing pre-D2 and post-D2 rows.src/research_data/brain/citations.py (1)
88-125: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicated upsert-by-id pattern between
cite_from_vaultandcite_from_journal.Both functions repeat "compute id → try
get_citation→ exceptBrainStoreError→ build+addCitation". Could be consolidated into a shared_upsert_citation(store, citation_id, build_fn)helper to keep the check-then-act sequence single-sourced as more citation sources are added.Also applies to: 132-161
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/research_data/brain/citations.py` around lines 88 - 125, The citation insertion flow is duplicated between cite_from_vault and cite_from_journal. Add a shared _upsert_citation helper that accepts the store, citation ID, and a citation-building callable, performs the existing get_citation/BrainStoreError check, and adds the new citation when absent; update both cite_from_vault and cite_from_journal to use it while preserving their existing status messages and citation construction.src/research_data/cards/models.py (1)
67-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider typing
gate_summarymore strictly.
gate_summary: dict[str, Any]is a loose passthrough. The solution design (line 37-39) specifies exactly four whitelist keys:oos_net_sharpe,mc_p5_return,wf_pct_positive,deflated_sharpe_probability. Adict[str, float]or a small Pydantic sub-model would provide compile-time safety and prevent accidental injection of non-whitelisted gate values into the critic review. This is deferrable since validators.py enforces the numeric allowlist downstream, but it would tighten the contract at the schema level.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/research_data/cards/models.py` around lines 67 - 97, Tighten CriticReview.gate_summary from dict[str, Any] to a typed structure containing only oos_net_sharpe, mc_p5_return, wf_pct_positive, and deflated_sharpe_probability, using an appropriate Pydantic sub-model or constrained mapping. Preserve the existing default-empty behavior while preventing non-whitelisted or non-numeric gate values at schema validation time.tests/test_property_ai_hub_cards.py (1)
135-138: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueRedundant local re-import of
run_analyze_symbol.Already imported at module level (line 16); the local import here just shadows it unnecessarily.
♻️ Proposed cleanup
import tempfile - from research_data.agents.runner import run_analyze_symbol from research_data.cards.writer import format_card_float🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_property_ai_hub_cards.py` around lines 135 - 138, Remove the redundant local import of run_analyze_symbol near the tempfile import, and rely on the existing module-level import while leaving the function’s usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@AGENTS.md`:
- Around line 45-47: Update the `Done` bullet in AGENTS.md to distinguish
implementing `scripts/live_ai_card_smoke.py` from completing its validation:
describe the NVDA smoke script as implemented or available, without implying the
optional live smoke or CI checks have passed. Keep the surrounding Phase 3 seam
status and pending validation context unchanged.
In `@src/research_data/agents/analyst.py`:
- Around line 63-83: The build_analyst_user_prompt function currently exposes
only evidence reference keys, not their metadata. Serialize bundle.evidence_refs
into an EVIDENCE_REFS section alongside ALLOWED_EVIDENCE_REF_KEYS, including
each reference’s table, source, and as-of metadata, so the model can associate
claims with the correct evidence.
In `@src/research_data/agents/llm_client.py`:
- Around line 27-30: Update the LiteLLM dependency minimum in pyproject.toml to
the first release that supports the gemini/gemini-3.5-flash alias used by
DEFAULT_GEMINI_MODEL, replacing the current >=1.60 floor while preserving the
existing dependency configuration.
In `@src/research_data/agents/runner.py`:
- Around line 81-94: Remove the unnecessary LLM client creation and
invocation_count checks from both blocked-quality branches in the runner flow.
Keep each deterministic card constructor, evidence validation, persistence, and
return behavior unchanged so MISSING or CONTRADICTORY quality statuses produce
the documented INSUFFICIENT_DATA card without requiring provider configuration
or making LLM calls.
In `@src/research_data/brain/store.py`:
- Around line 202-206: Update the hook validation in the proposal flow around
resolve_hook so BrainLoopError is converted to BrainStoreError before returning
to propose_cmd. Preserve the CLI’s existing error boundary and
typer.Exit(code=1) behavior for invalid --hook-ref values.
In `@src/research_data/cards/validators.py`:
- Around line 71-78: Remove the exported confidence_values parameter and its
no-op validation loop from the relevant validator function, updating its
signature and any public API exposure in __all__. Preserve confidence
enforcement through validate_confidence_cap and the existing main numeric token
validation.
In `@src/research_data/cli_desk.py`:
- Around line 371-386: Update analyze_symbol_cmd around the spec lookup and
build_happy_path_bundle flow to catch BrainStoreError alongside RunnerError and
StopIteration, echo its message to stderr, and raise typer.Exit(code=1) from the
error. Ensure invalid spec_id failures from store.get_spec or latest_gate_batch
receive the same clean handling as other analysis errors, matching the existing
critique_spec_cmd behavior.
In `@src/research_data/paper/engine.py`:
- Around line 263-269: The _journal method must make journal persistence and the
_on_lesson_journaled callback recoverable as one operation. Use an available
transaction spanning add_journal_entry and the callback; if cross-store
transactions are unavailable, persist a durable outbox/retry record before
returning and process it idempotently so callback failures can be retried
without duplicating provenance or leaving partial replay completion.
In `@tests/test_ai_hub_security.py`:
- Around line 84-94: Update the `banned` regular expression in the security test
so the `params.get(...)` alternative also matches the `cost-bps` key variant,
preserving the existing case-insensitive detection and other banned keys.
In `@tests/test_citations_and_projection.py`:
- Around line 79-81: Remove the tautological “or True” assertion in the citation
test and replace it with a meaningful check that validates c1.citation_id
against the expected vault_citation_id result for the exact extracted claims
section. Keep the existing same-id-twice stability check, using the relevant
citation identifiers from the test.
---
Outside diff comments:
In `@src/research_data/paper/store.py`:
- Around line 40-44: Add a schema migration for the existing paper_theses table
and invoke it from init_schema(), ensuring source_card_id is added when absent
while preserving compatibility with databases where the column already exists.
Keep propose_thesis() inserts and existing legacy-read handling unchanged.
---
Nitpick comments:
In `@src/research_data/agents/runner.py`:
- Around line 98-102: Replace direct access to FixtureLLMClient._canned in the
runner validation checks with a small public FixtureLLMClient accessor such as
has_canned(EvidenceCard). Add the accessor on FixtureLLMClient and use it in
both affected checks, preserving the existing RunnerError behavior.
In `@src/research_data/brain/citations.py`:
- Around line 88-125: The citation insertion flow is duplicated between
cite_from_vault and cite_from_journal. Add a shared _upsert_citation helper that
accepts the store, citation ID, and a citation-building callable, performs the
existing get_citation/BrainStoreError check, and adds the new citation when
absent; update both cite_from_vault and cite_from_journal to use it while
preserving their existing status messages and citation construction.
In `@src/research_data/brain/store.py`:
- Around line 490-513: Replace the positional length check in _row_to_spec with
name-based column mapping using the query cursor’s column metadata or an
equivalent dict-row representation. Map StrategySpec fields by column name so
added or reordered schema columns do not require new positional branches, while
preserving compatibility with existing pre-D2 and post-D2 rows.
In `@src/research_data/cards/models.py`:
- Around line 67-97: Tighten CriticReview.gate_summary from dict[str, Any] to a
typed structure containing only oos_net_sharpe, mc_p5_return, wf_pct_positive,
and deflated_sharpe_probability, using an appropriate Pydantic sub-model or
constrained mapping. Preserve the existing default-empty behavior while
preventing non-whitelisted or non-numeric gate values at schema validation time.
In `@tests/test_property_ai_hub_cards.py`:
- Around line 135-138: Remove the redundant local import of run_analyze_symbol
near the tempfile import, and rely on the existing module-level import while
leaving the function’s usage unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2fbb621c-48ad-4751-87e6-d023b91c81c1
📒 Files selected for processing (40)
.env.exampleAGENTS.mdDocs/FABLE5_PHASE3_AI_BRAIN_PROMPT.mdDocs/NORTH_STAR_DESK.mdDocs/PHASE3_AI_BRAIN_PROBLEM_STATEMENT.mdDocs/PHASE3_AI_BRAIN_RUNBOOK.mdDocs/PHASE3_AI_BRAIN_SOLUTION_DESIGN.mdDocs/SESSION_RECAP_AI_BRAIN_HUB_2026-07-12.mdDocs/YEAR_AHEAD_BASE.mdDocs/fable5_run_memory.mdpyproject.tomlscripts/live_ai_card_smoke.pyscripts/run_quality_momentum_study.pysrc/research_data/agents/__init__.pysrc/research_data/agents/analyst.pysrc/research_data/agents/assemble.pysrc/research_data/agents/critic.pysrc/research_data/agents/llm_client.pysrc/research_data/agents/runner.pysrc/research_data/brain/citations.pysrc/research_data/brain/models.pysrc/research_data/brain/store.pysrc/research_data/cards/__init__.pysrc/research_data/cards/allowlist.pysrc/research_data/cards/gate_projection.pysrc/research_data/cards/models.pysrc/research_data/cards/store.pysrc/research_data/cards/validators.pysrc/research_data/cards/writer.pysrc/research_data/cli.pysrc/research_data/cli_desk.pysrc/research_data/paper/engine.pysrc/research_data/paper/models.pysrc/research_data/paper/store.pysrc/research_data/read_api.pytests/test_ai_hub_llm_seam.pytests/test_ai_hub_security.pytests/test_citations_and_projection.pytests/test_property_ai_hub_cards.pytests/test_security_scope.py
| - **Done:** year-ahead base; Phase 2a/2b (`demo_eligible` on tiingo); Cursor Phase 3 AI hub prereqs + questionnaire locks; **Fable Phase 3 LLM seam** (2026-07-12, branch `feat/phase3-llm-seam`): `LiveLLMClient` (litellm.Router Gemini→Groq→Ollama + instructor, sole site `agents/llm_client.py`), evidence-bound analyst/critic prompts, CLI happy path, `scripts/live_ai_card_smoke.py` NVDA smoke; Cursor polish (`critique-spec` FactorEngine path, smoke vault mirror, tiingo CLI default). | ||
| - **Next:** human opens/merges the Phase 3 seam PR; V1.1 candidates (StrategySpec proposer, DuckDB `evidence_cards` build #2) stay parked per non-goals. No Phase 3b required for V1 locks. | ||
| - Vault SoT: `Session Findings — AI Brain Hub (2026-07-12)`; full Q&A: `Session Recap — AI Brain Hub Questionnaire (2026-07-12)`. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Distinguish landed implementation from completed validation.
The Done bullet lists the NVDA smoke as part of completed work, while the PR status says CI validation and the optional live smoke are still pending. Wording this as “smoke script implemented” would avoid implying that the live safety check has already passed.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@AGENTS.md` around lines 45 - 47, Update the `Done` bullet in AGENTS.md to
distinguish implementing `scripts/live_ai_card_smoke.py` from completing its
validation: describe the NVDA smoke script as implemented or available, without
implying the optional live smoke or CI checks have passed. Keep the surrounding
Phase 3 seam status and pending validation context unchanged.
| def build_analyst_user_prompt(bundle: AnalystInputBundle) -> str: | ||
| """Assemble the analyst's entire evidence view — nothing else reaches it.""" | ||
| allowlist = build_allowlist_from_score_packet(bundle.score_packet) | ||
| packet_json = score_packet_to_analyst_dict(bundle.score_packet) | ||
| ref_keys = sorted(bundle.evidence_ref_keys) | ||
| cap = bundle.score_packet.data_quality.max_confidence | ||
| return ( | ||
| f"Symbol: {bundle.symbol}\n" | ||
| f"as_of (copy into the as_of field): {bundle.as_of.isoformat()}\n" | ||
| f"data_quality_status (copy exactly): {bundle.score_packet.data_quality.status.value}\n" | ||
| f"max_confidence cap (copy exactly; confidence must not exceed it): " | ||
| f"{format_card_float(cap, confidence=True)}\n\n" | ||
| "SCORE_PACKET (the only source of facts; statuses of 'insufficient_data' " | ||
| "mean that factor is unknown):\n" | ||
| f"{json.dumps(packet_json, indent=2)}\n\n" | ||
| "QUOTABLE_NUMBERS (the only numbers you may write in prose):\n" | ||
| f"{render_quotable_numbers(allowlist)}\n\n" | ||
| "ALLOWED_EVIDENCE_REF_KEYS (the only legal ref_key values):\n" | ||
| f"{json.dumps(ref_keys)}\n\n" | ||
| "Write the EvidenceCard now. Remember: no digits in prose except " | ||
| "QUOTABLE_NUMBERS copied verbatim; never write ids or dates in prose." |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Provide the evidence references, not just their keys.
bundle.evidence_refs never reaches the model, so it can attach any allowed key to any claim without seeing its table, source, or as-of metadata. Serialize an EVIDENCE_REFS section alongside the key allowlist.
Proposed fix
f"{json.dumps(packet_json, indent=2)}\n\n"
+ "EVIDENCE_REFS (provenance available for claims):\n"
+ f"{json.dumps(bundle.evidence_refs, indent=2)}\n\n"
"QUOTABLE_NUMBERS (the only numbers you may write in prose):\n"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def build_analyst_user_prompt(bundle: AnalystInputBundle) -> str: | |
| """Assemble the analyst's entire evidence view — nothing else reaches it.""" | |
| allowlist = build_allowlist_from_score_packet(bundle.score_packet) | |
| packet_json = score_packet_to_analyst_dict(bundle.score_packet) | |
| ref_keys = sorted(bundle.evidence_ref_keys) | |
| cap = bundle.score_packet.data_quality.max_confidence | |
| return ( | |
| f"Symbol: {bundle.symbol}\n" | |
| f"as_of (copy into the as_of field): {bundle.as_of.isoformat()}\n" | |
| f"data_quality_status (copy exactly): {bundle.score_packet.data_quality.status.value}\n" | |
| f"max_confidence cap (copy exactly; confidence must not exceed it): " | |
| f"{format_card_float(cap, confidence=True)}\n\n" | |
| "SCORE_PACKET (the only source of facts; statuses of 'insufficient_data' " | |
| "mean that factor is unknown):\n" | |
| f"{json.dumps(packet_json, indent=2)}\n\n" | |
| "QUOTABLE_NUMBERS (the only numbers you may write in prose):\n" | |
| f"{render_quotable_numbers(allowlist)}\n\n" | |
| "ALLOWED_EVIDENCE_REF_KEYS (the only legal ref_key values):\n" | |
| f"{json.dumps(ref_keys)}\n\n" | |
| "Write the EvidenceCard now. Remember: no digits in prose except " | |
| "QUOTABLE_NUMBERS copied verbatim; never write ids or dates in prose." | |
| f"{json.dumps(packet_json, indent=2)}\n\n" | |
| "EVIDENCE_REFS (provenance available for claims):\n" | |
| f"{json.dumps(bundle.evidence_refs, indent=2)}\n\n" | |
| "QUOTABLE_NUMBERS (the only numbers you may write in prose):\n" |
🧰 Tools
🪛 ast-grep (0.44.1)
[info] 76-76: use jsonify instead of json.dumps for JSON output
Context: json.dumps(packet_json, indent=2)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
[info] 80-80: use jsonify instead of json.dumps for JSON output
Context: json.dumps(ref_keys)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/agents/analyst.py` around lines 63 - 83, The
build_analyst_user_prompt function currently exposes only evidence reference
keys, not their metadata. Serialize bundle.evidence_refs into an EVIDENCE_REFS
section alongside ALLOWED_EVIDENCE_REF_KEYS, including each reference’s table,
source, and as-of metadata, so the model can associate claims with the correct
evidence.
| #: Confirmed current Gemini Flash litellm alias (gemini-2.0-flash retired 2026-06-01). | ||
| DEFAULT_GEMINI_MODEL = "gemini/gemini-3.5-flash" | ||
| DEFAULT_GROQ_MODEL = "groq/llama-3.3-70b-versatile" | ||
| DEFAULT_OLLAMA_MODEL = "ollama/llama3.1" |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check declared litellm/instructor pins
fd pyproject.toml --exec grep -nE "litellm|instructor" {}Repository: gupta-builds/TradingView
Length of output: 214
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== pyproject dependency lines ==\n'
sed -n '1,80p' pyproject.toml | nl -ba | sed -n '1,80p'
printf '\n== llm_client.py relevant lines ==\n'
sed -n '1,220p' src/research_data/agents/llm_client.py | nl -ba | sed -n '1,220p'Repository: gupta-builds/TradingView
Length of output: 236
🌐 Web query:
LiteLLM Gemini 3.5 Flash support release notes v1.85.1 reasoning_effort gemini-3.5-flash
💡 Result:
LiteLLM introduced support for Gemini 3.5 Flash in version v1.85.1 [1][2]. Regarding the reasoning_effort parameter, LiteLLM automatically maps the OpenAI-standard reasoning_effort parameter to the Gemini-specific thinking_level parameter for Gemini 3 and newer models (including Gemini 3.5 Flash) [3][4][5][6]. This abstraction allows users to leverage reasoning capabilities in LiteLLM using a consistent interface across different providers [3][4]. Key details regarding this release: - Release v1.85.1 included day-0 support for gemini-3.5-flash on both Google AI Studio (gemini provider) and Vertex AI (vertex_ai provider) [1]. - The release also addressed specific reliability issues for Gemini 3.5+, such as omitting the function_call ID on Vertex AI tool turns to prevent HTTP 400 errors [1][7]. - Pricing and capability metadata (such as vision, audio, and function calling support) were added for gemini-3.5-flash as part of the model registry updates [1][8][9].
Citations:
- 1: https://docs.litellm.ai/release_notes/v1.85.1/v1-85-1
- 2: https://github.com/BerriAI/litellm/releases/tag/v1.85.1
- 3: https://docs.litellm.ai/docs/providers/gemini
- 4: https://docs.litellm.ai/docs/providers/vertex
- 5: https://docs.litellm.ai/blog/gemini_3
- 6: https://docs.litellm.ai/blog/gemini_3_1_pro
- 7: BerriAI/litellm@v1.85.0...v1.85.1
- 8: Day 0 support : Gemini 3.5 Flash BerriAI/litellm#28268
- 9: feat(models): add gemini-3.5-flash pricing and metadata BerriAI/litellm#28299
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== litellm references ==\n'
rg -n --hidden --glob '!**/.git/**' '\blitellm\b' .
printf '\n== instructor references ==\n'
rg -n --hidden --glob '!**/.git/**' '\binstructor\b' .Repository: gupta-builds/TradingView
Length of output: 8631
Raise the LiteLLM minimum for the Gemini 3.5 default pyproject.toml still allows litellm>=1.60, which is older than the first LiteLLM release that added gemini/gemini-3.5-flash support. Bump the floor to a compatible LiteLLM release so the live Gemini default doesn’t break on older installs.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/agents/llm_client.py` around lines 27 - 30, Update the
LiteLLM dependency minimum in pyproject.toml to the first release that supports
the gemini/gemini-3.5-flash alias used by DEFAULT_GEMINI_MODEL, replacing the
current >=1.60 floor while preserving the existing dependency configuration.
| if quality_blocks_llm(bundle.score_packet.data_quality.status): | ||
| client = llm_client or get_llm_client() | ||
| before = getattr(client, "invocation_count", 0) | ||
| card = _insufficient_data_card(bundle) | ||
| # Assert path did not call LLM | ||
| after = getattr(client, "invocation_count", 0) | ||
| if after != before: | ||
| raise RunnerError("LLM was invoked on blocked quality status") | ||
| allowlist = build_allowlist_from_score_packet(bundle.score_packet) | ||
| validate_evidence_card(card, allowlist, bundle.evidence_ref_keys) | ||
| write_evidence_card(card, cards_dir) | ||
| if vault_mirror_path is not None: | ||
| write_vault_mirror(card, vault_mirror_path) | ||
| return card |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Fail-closed path unnecessarily requires a working LLM client.
Both blocked-quality branches build client = llm_client or get_llm_client() and check invocation_count before/after — but the deterministic card constructors never receive or call client, so the check can never fail; it's dead code. Worse, in live mode with no provider keys configured, get_llm_client() itself raises LLMClientError (see llm_client.py lines 100-104), so a MISSING/CONTRADICTORY-quality request would crash on an unrelated provider-config error instead of returning the documented deterministic INSUFFICIENT_DATA card ("zero LLM calls").
🐛 Proposed fix
if quality_blocks_llm(bundle.score_packet.data_quality.status):
- client = llm_client or get_llm_client()
- before = getattr(client, "invocation_count", 0)
card = _insufficient_data_card(bundle)
- # Assert path did not call LLM
- after = getattr(client, "invocation_count", 0)
- if after != before:
- raise RunnerError("LLM was invoked on blocked quality status")
allowlist = build_allowlist_from_score_packet(bundle.score_packet)- client = llm_client or get_llm_client()
if quality_blocks_llm(bundle.score_packet.data_quality.status):
- before = getattr(client, "invocation_count", 0)
review = CriticReview(
card_id=card.card_id if card else None,
spec_id=bundle.spec_id,
suggestion="insufficient_data",
confidence_delta=0.0,
rationale="Input quality blocks analysis; no demotion math applied.",
rejected=False,
)
- after = getattr(client, "invocation_count", 0)
- if after != before:
- raise RunnerError("LLM was invoked on blocked quality status")
write_critic_review(review, cards_dir)
return review
+ client = llm_client or get_llm_client()
if isinstance(client, FixtureLLMClient) and CriticReview not in client._canned:Also applies to: 160-176
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/agents/runner.py` around lines 81 - 94, Remove the
unnecessary LLM client creation and invocation_count checks from both
blocked-quality branches in the runner flow. Keep each deterministic card
constructor, evidence validation, persistence, and return behavior unchanged so
MISSING or CONTRADICTORY quality statuses produce the documented
INSUFFICIENT_DATA card without requiring provider configuration or making LLM
calls.
| if spec.hook_ref: | ||
| # Fail-closed early (D2): resolve at propose-time, not only approve. | ||
| from research_data.brain.loop import resolve_hook | ||
|
|
||
| resolve_hook(spec.hook_ref) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate the relevant files and inspect the surrounding code.
git ls-files 'src/research_data/brain/store.py' 'src/research_data/brain/loop.py' 'src/research_data/brain/cli_desk.py' 'Docs/PHASE3_AI_BRAIN_RUNBOOK.md' | sed -n '1,20p'
printf '\n## store.py outline\n'
ast-grep outline src/research_data/brain/store.py --view expanded || true
printf '\n## loop.py outline\n'
ast-grep outline src/research_data/brain/loop.py --view expanded || true
printf '\n## cli_desk.py outline\n'
ast-grep outline src/research_data/brain/cli_desk.py --view expanded || true
printf '\n## relevant excerpts from store.py\n'
sed -n '1,260p' src/research_data/brain/store.py | cat -n | sed -n '1,260p'
printf '\n## relevant excerpts from loop.py\n'
sed -n '1,260p' src/research_data/brain/loop.py | cat -n | sed -n '1,260p'
printf '\n## relevant excerpts from cli_desk.py\n'
sed -n '1,260p' src/research_data/brain/cli_desk.py | cat -n | sed -n '1,260p'
printf '\n## runbook mentions of rebuild/migration/schema\n'
rg -n "rebuild|migrat|schema|desk policy|CREATE TABLE IF NOT EXISTS|ALTER TABLE" Docs/PHASE3_AI_BRAIN_RUNBOOK.md src/research_data/brain -S || trueRepository: gupta-builds/TradingView
Length of output: 19814
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## brain-related CLI files\n'
fd -a '.*desk.*\.py|.*cli.*\.py' src/research_data/brain
printf '\n## propose_cmd / BrainStoreError / BrainLoopError references\n'
rg -n "def propose_cmd|BrainStoreError|BrainLoopError|resolve_hook|typer.Exit|except \(BrainStoreError|except .*JSONDecodeError" src/research_data/brain -S
printf '\n## runbook excerpts around schema/rebuild/migration\n'
sed -n '1,260p' Docs/PHASE3_AI_BRAIN_RUNBOOK.md | cat -n | sed -n '1,260p'
printf '\n## any migration helpers or schema upgrade paths\n'
rg -n "ALTER TABLE|migration|migrate|upgrade schema|rebuild per desk policy|init_schema\(" src/research_data/brain Docs -SRepository: gupta-builds/TradingView
Length of output: 5624
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## callers of propose_spec\n'
rg -n "propose_spec\(" src -S
printf '\n## BrainLoopError handling across src\n'
rg -n "except .*BrainLoopError|BrainLoopError\)" src -S
printf '\n## spec schema read/write shape comments\n'
sed -n '470,525p' src/research_data/brain/store.py | cat -n
printf '\n## any schema rebuild/drop logic in brain code\n'
rg -n "DROP TABLE|rebuild schema|rebuild per desk policy|CREATE TABLE IF NOT EXISTS brain_strategy_specs|ALTER TABLE brain_strategy_specs" src Docs -SRepository: gupta-builds/TradingView
Length of output: 391
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '## cli_desk.py excerpt around propose_cmd\n'
sed -n '200,260p' src/research_data/cli_desk.py | cat -n
printf '\n## brain store read-side schema compatibility\n'
sed -n '485,520p' src/research_data/brain/store.py | cat -n
printf '\n## any catch of BrainLoopError outside propose_cmd\n'
rg -n "BrainLoopError|BrainStoreError|JSONDecodeError|typer.Exit" src/research_data/cli_desk.py src/research_data -SRepository: gupta-builds/TradingView
Length of output: 10150
resolve_hook needs to stay inside the CLI error boundary. src/research_data/brain/loop.py::resolve_hook() raises BrainLoopError, but src/research_data/cli_desk.py::propose_cmd() only catches BrainStoreError and JSONDecodeError, so a bad --hook-ref now escapes as an uncaught exception instead of the normal typer.Exit(code=1) path. Wrap this as BrainStoreError here or catch BrainLoopError in the CLI.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/brain/store.py` around lines 202 - 206, Update the hook
validation in the proposal flow around resolve_hook so BrainLoopError is
converted to BrainStoreError before returning to propose_cmd. Preserve the CLI’s
existing error boundary and typer.Exit(code=1) behavior for invalid --hook-ref
values.
| for conf in confidence_values: | ||
| if not allowlist.allows_float(conf, confidence=True): | ||
| # Confidence must be ≤ max and present; max is always on allowlist | ||
| if allowlist.allows_float(conf, confidence=True) is False: | ||
| # Allow any confidence that rounds within [0, max] if max is listed | ||
| pass | ||
| # Confidence field itself is checked via validate_confidence_cap; | ||
| # free-text confidence mentions must match allowlist floats. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
confidence_values loop is a dead no-op — validation never fires.
The confidence_values parameter is exported via __all__ in the package public API, but the loop body never raises. Line 72 checks not allows_float(...) and line 74 re-checks the same condition (allows_float(...) is False), then pass. No error is raised regardless of whether the confidence value is in the allowlist or not. No current caller passes confidence_values, so this is latent — but any future caller relying on this parameter gets zero validation.
Confidence is already enforced by validate_confidence_cap, and free-text confidence mentions are already caught by the main numeric token loop. Remove the parameter and dead loop to keep the API honest.
🧹 Proposed fix: remove dead `confidence_values` parameter and loop
def validate_numeric_allowlist(
texts: Iterable[str],
allowlist: NumericAllowlist,
- *,
- confidence_values: Iterable[float] = (),
) -> None:
"""Every numeric token in texts must appear in the allowlist buckets."""
for text in texts:
for raw, value in extract_numeric_tokens(text):
if isinstance(value, int):
# Also allow if it matches a rounded float display (e.g. "1")
if allowlist.allows_int(value):
continue
if allowlist.allows_float(float(value)):
continue
raise CardValidationError(
f"integer token {raw!r} not in numeric allowlist"
)
if allowlist.allows_float(value):
continue
if allowlist.allows_float(value, confidence=True):
continue
# Symmetric with the int branch: "ranks 3." lexes as float 3.0 but
# denotes the allowlisted integer 3 followed by a full stop.
if value.is_integer() and allowlist.allows_int(int(value)):
continue
raise CardValidationError(
f"float token {raw!r} not in numeric allowlist"
)
- for conf in confidence_values:
- if not allowlist.allows_float(conf, confidence=True):
- # Confidence must be ≤ max and present; max is always on allowlist
- if allowlist.allows_float(conf, confidence=True) is False:
- # Allow any confidence that rounds within [0, max] if max is listed
- pass
- # Confidence field itself is checked via validate_confidence_cap;
- # free-text confidence mentions must match allowlist floats.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| for conf in confidence_values: | |
| if not allowlist.allows_float(conf, confidence=True): | |
| # Confidence must be ≤ max and present; max is always on allowlist | |
| if allowlist.allows_float(conf, confidence=True) is False: | |
| # Allow any confidence that rounds within [0, max] if max is listed | |
| pass | |
| # Confidence field itself is checked via validate_confidence_cap; | |
| # free-text confidence mentions must match allowlist floats. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/cards/validators.py` around lines 71 - 78, Remove the
exported confidence_values parameter and its no-op validation loop from the
relevant validator function, updating its signature and any public API exposure
in __all__. Preserve confidence enforcement through validate_confidence_cap and
the existing main numeric token validation.
| if spec_id is not None: | ||
| store = _brain(conn) | ||
| store.get_spec(spec_id) | ||
| gate_runs = latest_gate_batch(store, spec_id) | ||
| try: | ||
| bundle = build_happy_path_bundle( | ||
| conn, | ||
| symbol=symbol, | ||
| as_of=as_of_date, | ||
| price_source=price_source, | ||
| spec_id=spec_id, | ||
| gate_runs=gate_runs, | ||
| ) | ||
| except (RunnerError, StopIteration) as e: | ||
| typer.echo(str(e) or "no ScorePacket produced", err=True) | ||
| raise typer.Exit(code=1) from e |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Invalid --spec-id raises an uncaught BrainStoreError in analyze-symbol.
store.get_spec(spec_id) (and latest_gate_batch) can raise BrainStoreError, but this block only catches (RunnerError, StopIteration). Unlike critique_spec_cmd, which wraps its body in except BrainStoreError (Line 482), analyze_symbol_cmd has no such handler, so a bad spec_id produces a traceback instead of a clean Exit(code=1) with the error message.
🛡️ Proposed guard
gate_runs = None
if spec_id is not None:
store = _brain(conn)
- store.get_spec(spec_id)
- gate_runs = latest_gate_batch(store, spec_id)
+ try:
+ store.get_spec(spec_id)
+ gate_runs = latest_gate_batch(store, spec_id)
+ except BrainStoreError as e:
+ typer.echo(str(e), err=True)
+ raise typer.Exit(code=1) from e📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if spec_id is not None: | |
| store = _brain(conn) | |
| store.get_spec(spec_id) | |
| gate_runs = latest_gate_batch(store, spec_id) | |
| try: | |
| bundle = build_happy_path_bundle( | |
| conn, | |
| symbol=symbol, | |
| as_of=as_of_date, | |
| price_source=price_source, | |
| spec_id=spec_id, | |
| gate_runs=gate_runs, | |
| ) | |
| except (RunnerError, StopIteration) as e: | |
| typer.echo(str(e) or "no ScorePacket produced", err=True) | |
| raise typer.Exit(code=1) from e | |
| if spec_id is not None: | |
| store = _brain(conn) | |
| try: | |
| store.get_spec(spec_id) | |
| gate_runs = latest_gate_batch(store, spec_id) | |
| except BrainStoreError as e: | |
| typer.echo(str(e), err=True) | |
| raise typer.Exit(code=1) from e | |
| try: | |
| bundle = build_happy_path_bundle( | |
| conn, | |
| symbol=symbol, | |
| as_of=as_of_date, | |
| price_source=price_source, | |
| spec_id=spec_id, | |
| gate_runs=gate_runs, | |
| ) | |
| except (RunnerError, StopIteration) as e: | |
| typer.echo(str(e) or "no ScorePacket produced", err=True) | |
| raise typer.Exit(code=1) from e |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/cli_desk.py` around lines 371 - 386, Update
analyze_symbol_cmd around the spec lookup and build_happy_path_bundle flow to
catch BrainStoreError alongside RunnerError and StopIteration, echo its message
to stderr, and raise typer.Exit(code=1) from the error. Ensure invalid spec_id
failures from store.get_spec or latest_gate_batch receive the same clean
handling as other analysis errors, matching the existing critique_spec_cmd
behavior.
| def _journal(self, entry: JournalEntry) -> JournalEntry: | ||
| self._store.add_journal_entry(entry) | ||
| if ( | ||
| self._on_lesson_journaled is not None | ||
| and entry.entry_type in {"lesson", "exit"} | ||
| ): | ||
| self._on_lesson_journaled(entry) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Make journal-to-citation persistence recoverable or atomic.
If the callback raises after add_journal_entry, the exit journal entry is durable but its citation and replay completion are not. A rerun can then fail or produce partial provenance. Wrap this cross-store operation in a transaction where possible, or write a durable outbox/retry record before returning.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/research_data/paper/engine.py` around lines 263 - 269, The _journal
method must make journal persistence and the _on_lesson_journaled callback
recoverable as one operation. Use an available transaction spanning
add_journal_entry and the callback; if cross-store transactions are unavailable,
persist a durable outbox/retry record before returning and process it
idempotently so callback failures can be retried without duplicating provenance
or leaving partial replay completion.
| banned = re.compile( | ||
| r"""params\s*\[\s*['\"]?(universe|symbols|cost_bps|cost-bps)['\"]?\s*\]""" | ||
| r"""|params\.get\(\s*['\"]?(universe|symbols|cost_bps)['\"]?""", | ||
| re.I, | ||
| ) | ||
| offenders = [] | ||
| for path in strategies.rglob("*.py"): | ||
| text = path.read_text(encoding="utf-8") | ||
| if banned.search(text): | ||
| offenders.append(path.name) | ||
| assert offenders == [] |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
.get() banned-pattern misses the cost-bps variant.
The bracket-access alternative includes cost-bps, but the params.get(...) alternative only lists cost_bps. A hook written as params.get("cost-bps") would slip past this D3 guard undetected.
🛡️ Proposed fix
banned = re.compile(
r"""params\s*\[\s*['\"]?(universe|symbols|cost_bps|cost-bps)['\"]?\s*\]"""
- r"""|params\.get\(\s*['\"]?(universe|symbols|cost_bps)['\"]?""",
+ r"""|params\.get\(\s*['\"]?(universe|symbols|cost_bps|cost-bps)['\"]?""",
re.I,
)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| banned = re.compile( | |
| r"""params\s*\[\s*['\"]?(universe|symbols|cost_bps|cost-bps)['\"]?\s*\]""" | |
| r"""|params\.get\(\s*['\"]?(universe|symbols|cost_bps)['\"]?""", | |
| re.I, | |
| ) | |
| offenders = [] | |
| for path in strategies.rglob("*.py"): | |
| text = path.read_text(encoding="utf-8") | |
| if banned.search(text): | |
| offenders.append(path.name) | |
| assert offenders == [] | |
| banned = re.compile( | |
| r"""params\s*\[\s*['\"]?(universe|symbols|cost_bps|cost-bps)['\"]?\s*\]""" | |
| r"""|params\.get\(\s*['\"]?(universe|symbols|cost_bps|cost-bps)['\"]?""", | |
| re.I, | |
| ) | |
| offenders = [] | |
| for path in strategies.rglob("*.py"): | |
| text = path.read_text(encoding="utf-8") | |
| if banned.search(text): | |
| offenders.append(path.name) | |
| assert offenders == [] |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_ai_hub_security.py` around lines 84 - 94, Update the `banned`
regular expression in the security test so the `params.get(...)` alternative
also matches the `cost-bps` key variant, preserving the existing
case-insensitive detection and other banned keys.
| section = "## Claims\n\n- Claim one persists." | ||
| assert c1.citation_id == vault_citation_id("Research/note.md", section.strip()) or True | ||
| # Stable id depends on exact claims_section extraction — just assert same id twice. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Tautological assertion always passes.
assert X or True never fails regardless of X, so this line verifies nothing about the citation id computation — it's dead weight masquerading as a check on vault_citation_id's exact hash input.
🐛 Proposed fix
- section = "## Claims\n\n- Claim one persists."
- assert c1.citation_id == vault_citation_id("Research/note.md", section.strip()) or True
- # Stable id depends on exact claims_section extraction — just assert same id twice.
+ # Idempotency across re-ingestion is already verified above (same citation_id
+ # returned for identical vault content).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| section = "## Claims\n\n- Claim one persists." | |
| assert c1.citation_id == vault_citation_id("Research/note.md", section.strip()) or True | |
| # Stable id depends on exact claims_section extraction — just assert same id twice. | |
| # Idempotency across re-ingestion is already verified above (same citation_id | |
| # returned for identical vault content). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_citations_and_projection.py` around lines 79 - 81, Remove the
tautological “or True” assertion in the citation test and replace it with a
meaningful check that validates c1.citation_id against the expected
vault_citation_id result for the exact extracted claims section. Keep the
existing same-id-twice stability check, using the relevant citation identifiers
from the test.
Summary
LiveLLMClient(litellm.Router Gemini→Groq→Ollama + instructor), evidence-bound analyst/critic prompts, FactorEngine happy-path CLI, env-gated NVDA live smoke with planted-Sharpe fail-closed.critique-specvia FactorEngine, default--price-source tiingo, smoke vault mirror, docs/status sync.cards/, fixture agents, desk CLI, Properties) if not yet on remotemain.Test plan
pytest -qoffline (497 passed locally in fixture mode)RESEARCH_DATA_LLM=live python scripts/live_ai_card_smoke.py --db data/market.duckdb --symbol NVDAMerge policy: merge on CI green; CodeRabbit optional / do not block.
Summary by CodeRabbit
New Features
Documentation
Tests