Redhound Roadmap — History & Decision Log¶
This is the record, not the plan. The forward-looking roadmap lives in product_roadmap.md and is deliberately kept short. This file holds what that document used to carry inline: what shipped, and why decisions were made the way they were.
Two parts, both newest-first: 1. Shipped log — what landed, when, which PR. 2. Review notes — the dated decision trail, preserved verbatim. These are evidence of what was believed and when, which is what makes episodes like the 2026-07-12 stale-basis correction legible in hindsight. Never rewrite them. Append new notes at the top.
Reading the notes: they are frozen at their date. Sequencing numbers inside them (
#3,#7, …) refer to the queue as it stood at the time and are not pointers to today's queue — the forward queue is renumbered whenever a shipped item leaves it. Trust the roadmap's queue, never a number quoted here. Likewise, a note's "verified basis" was verified then; code moves.Why this split (2026-07-17): the roadmap had reached 655 lines, ~58% of which was history, and the plan had become a minority tenant in its own file. Warnings that still govern live work were moved onto the roadmap items they govern; everything below is the archive.
Shipped log (newest first)¶
- ~~Daily-brief thin v0 — the front door~~ — done (2026-07-17, PR #477). Spec
docs/specs/2026-07-16-daily-brief-thin-v0-design.md. This is where T1/T2/slice A finally reach a user — all three shipped events with no subscriber and deliberately deferred surfaces, so "your NVDA thesis was invalidated at $478" had been computable since 2026-07-13 and reached nobody.daily_briefsfrozen snapshot (migration 045) + pre-market Temporal schedule (11:00 UTC) GET /api/v1/brief+ the Home band; three absorbed widgets deleted (+2233/−488). Gate note: merged onmake ci-checkgreen alone — CI billing outage (#475) meant required checks physically could not run. Watch the first live 11:00 UTC fire: if "theses moved" reads 0 while deltas exist, the window/offset relationship is wrong — it fails silently, with no error.- ~~IA Phase B — Catalyst folded into Opportunities, Sessions demoted, single nav~~ — done
(2026-07-16, PR #476). Spec
docs/specs/2026-07-16-ia-phase-b-design.md. 11→9 nav destinations;opportunities.sourcefacet (migration 044); catalyst fan-out as a second activity onCatalystScanWorkflow. Accepted trade-off: market-wide catalyst browse is gone — the feed is strictly per-user (holders ∪ watchers). Merged via admin override (CI billing outage, #475). - ~~Strategy Builder repoint → risk/execution rules + merged into Proving Ground~~ — done (2026-07-15, PR #474). Spec
docs/specs/2026-07-14-strategy-risk-rules-design.md. The entireStrategyConfigreached no code path andpaper_portfolio_serviceevaluated every signal against all tracks with module constants — tracks traded identically, so/fund/comparecompared tracks identical by construction. Shipped: analyst-weight canvas deleted (29 components, ~3.6k lines); four user-owned rules (min_signal_confidence/stop_loss_pct/take_profit_pct/max_hold_days) wired end-to-end throughstrategy_config_snapshot→_resolve_rules→ sizing/levels, with user pct bounding ATR (stop = max(entry−2ATR, entry×(1−slp)),target = min(entry+3ATR, entry×(1+tpp))) so the knobs bite on ATR signals too;/strategies→/fundredirect (nav 12→11); rule edits propagate into wired tracks atomically with an append-onlyrules_history(migration 043) surfaced as a "Rules changed" marker. Two tracks with different rules now produce different trades — the load-bearing behavior that was absent. Fixed in passing: strategy-triggered analyses were silently restricted to 5 of 9 analysts (strategy_config_to_session_config, now deleted). Deferred:list_strategies_asyncstill writesis_defaulton the read path;is_default/is_scanner/alerts_enabledcolumn drops need their own migration. - ~~Clean sweep + T1/T2 end-to-end data verification~~ — done (2026-07-14). Triggered a clean universe sweep: 506 matches → 4 dispatched → 4/4 COMPLETED, zero failures; confidences varied 0.55–0.87 (not clamped) — the SEV-0 + signal-quality fixes hold on fresh data. Verified the whole T1+T2 loop on the live worker stack: refresh dispatched system-scoped → frame attached to the SYSTEM thesis → delta snapshot written → discovery fanned a
thesis_flipinto the interested user's feed; the I1 (22:00 offset, created in a real Temporal) and I3 (SELL row survives the long-only sweep, 0 transitions) fixes confirmed live. Regenerated outcome rows (50/50, 0 failures). HOLD signals correctly SKIP thesis creation (by design, not a bug). This was the gate on the learning loop + analog finder below; it also surfaced the leaderboard-hardcode fixed next. - ~~Analyst leaderboard scores all analysts + adaptive edge weights~~ — done (2026-07-14, PR #473). The leaderboard hardcoded the legacy four (
technical/fundamental/sentiment/news) in three places while the pipeline runs 9 and a session's breakdown carries 8 — sosector/insider/earnings_revisions/valuation_comparableswere structurally invisible and could never earn an adaptive weight (surfaced by the clean-sweep verification). Now derived from the session breakdown (4→8 on live data). Also made edge-signal weights adaptive on their own carve-budget (damped ±0.03, clamp [0.02, 0.15], total ≤0.30). Live result:insidera 1.00 directional hit rate, previously pinned at a fixed 0.05 → 0.08;valuation_comparablesat 0.10 → demoted 0.06 → 0.03. De-risks the learning loop — it would otherwise have trained on half the roster and ignored the best analyst. - ~~T1 — Living thesis object (spine)~~ — done (2026-07-13, PR #471). Thesis model + migration 041 +
apply_frame(worker-written, frames immutable) + read-only API + tier-1 lifecycle sweep + Kafka thesis events (topics still unconsumed — the brief reads the DB snapshot instead; see #9). Surfaces deferred then, delivered 2026-07-17 — the daily-brief band is where this reaches the user. - ~~T2 — Overnight flip detection (spine)~~ — done (2026-07-13, PR #472). System-scoped overnight re-run (
ThesisRefreshWorkflow, offset 22:00 UTC) + delta classifier + last-delta snapshot (migration 042) + discovery fan-out as per-userthesis_flip/thesis_shiftopportunities in the existing feed. No new UI. Together with T1 this is exit-intelligence v1 on the portfolio. - ~~"Why now" + invalidation on opportunities~~ — found already shipped (2026-07-12): read-time enrichment, no migration.
- ~~Opportunity lifecycle enforcement (spine T1 slice A)~~ — done (2026-07-12, PR #469). The vocabulary, close-below semantics, and single-writer rule T1 promotes are now settled in code.
- ~~SEV-0: all-HOLD + failing analyses~~ — root-caused and fixed (2026-07-12): Kafka-bootstrap misconfig + poisoned producer singleton (
488976869). Not logic; not a lifecycle regression. - ~~Signal-quality audit (4 defects)~~ — fixed (2026-07-12, PR #470): sentiment double-scaling, confidence clamp, startup reaper, adaptive-weights stub; sweep path proven end-to-end.
- ~~Pipeline-health alerting~~ — shipped in the same PR #470:
PipelineAnalysesFailing+PipelineAllHoldover newredhound_analyses_total/redhound_analysis_decisions_totalmetrics. The SEV-0 cleanup is fully closed. - ~~Strategy Builder
debate.*config~~ — hidden, read-only (2026-07-11). - ~~Adjusted-close fix + outcome-backfill script~~ — code done (2026-07-11). No prod to run it against; regenerate dev data from a clean sweep instead.
Review notes (verbatim, newest first)¶
Frozen at their date. See the reading note above before trusting any number inside them.
The front door landed — IA Phase B + daily-brief thin v0 (2026-07-16/17):
IA Phase B (PR #476). Catalyst folded into Opportunities as an indexed
sourcefacet (opportunities.source, migration 044;/catalyst→/opportunities?source=catalyst), the Sessions/Analyses archive demoted out of nav, and three drifted nav definitions consolidated intofrontend/src/config/nav.tsx— 11→9 destinations. Catalyst fan-out runs as a second activity onCatalystScanWorkflow, reusing the thesis-discovery idempotency/never-raise discipline. Accepted trade-off: market-wide catalyst browse is gone — the feed is strictly per-user (holders ∪ watchers). Still open from Phase B's original direction: merging Signals with Fund/Proving Ground into one track-record surface was evaluated and deferred, not done — it remains a live thread on the P2 canonical-discovery row, not a shipped outcome.Daily-brief thin v0 (PR #477). The roadmap's #1 forward item since 2026-07-11 is shipped, and with it the platform has a front door for the first time. Its real significance is closing a debt the spine had been accumulating: T1, T2, and slice A each shipped a Kafka topic with no subscriber and a deliberately deferred surface, so "your NVDA thesis was invalidated at $478" had been computable since 2026-07-13 and reached nobody.
daily_briefsfrozen snapshot (migration 045) + a pre-market Temporal schedule (11:00 UTC, trading days only) +GET /api/v1/brief+ theOvernightBriefband on Home; four DB-sourced sections and one VIX regime per run. Home lost three widgets it absorbs — net +2233/−488, a simplification.Load-bearing and non-obvious — do not "tidy" this. The 11:00 UTC offset works because the brief's window reaches back past
thesis-refresh's 22:00 UTC write — not because it fires after it (it fires 11h before). EST slack is only ~1h (21:00 close vs 22:00 refresh). Retune either offset and "theses moved" silently reads 0 forever, with no error. The first live fire is the thing to watch; it is carried as an open watch item above the forward queue.Two gaps knowingly shipped (user-approved), both inherited by the full brief (#9). (a) No intraday live personal state: the band is an overnight snapshot windowed
[prev close, ~11:00 UTC], so the trading session itself appears in no brief, and from ~07:00 ET Home carries no live personal state butPortfolioSnapshot. (b) Fired alerts have no destination — thealerts_firedrow is deliberately non-clickable because no/alertsroute exists; the nullablehrefmeans giving them one is a payload change, no migration.Sourcing decision, recorded because it will look like a shortcut later. v0 reads the denormalized DB columns the spine events already write rather than subscribing to the
opportunity.lifecycle/thesis topics. A once-daily pre-market batch reaches the identical user outcome; true event-consumption + push is what buys real-time delivery, so it lands with the full version + notification layer (#9), not v0.Gate note. Both #476 and #477 merged on
make ci-checkgreen alone — the CI billing outage (#475) meant required checks physically could not run. #476 used an admin override.Numbering note (2026-07-17). The forward queue was renumbered when the shipped front-door item left slot #1 (the learning loop now heads it). Numbers in the dated notes above are preserved as written — they were correct at the time and are history, not pointers; trust the queue itself.
Strategy repoint shipped; CI found dead (2026-07-15):
Repoint shipped (PR #474). The 2026-07-14 decision above landed end-to-end. Four user-owned rules (
min_signal_confidence,stop_loss_pct,take_profit_pct,max_hold_days) now flowstrategy_config_snapshot→_resolve_rules→ sizing/levels inpaper_portfolio_service, so two tracks with different rules produce different trades — the load-bearing behavior the whole item existed to create, and the thing that makes/fund/comparemean anything. The analyst-weight canvas is deleted (29 components, ~3.6k lines),/strategiesredirects to/fund, and rule edits propagate into wired tracks atomically alongside an append-onlyrules_history(migration 043).Two findings worth carrying forward. (1) ATR precedence was a spec gap, not a code bug — the spec wired only the non-ATR branch, and every test used
atr=None, so on real signals (where ATR is almost always present) the stop/target knobs would have been silently inert. Resolved by having the user's pct bound ATR rather than replace it. The lesson is that the tests agreed with the spec and both were wrong about production; a whole-branch review caught what task-level review didn't. (2) A live regression was found in passing:strategy_config_to_session_configrestricted strategy-triggered analyses to 5 of 9 analysts. It was deleted. Neither finding was on the plan.CI is dead, and an earlier diagnosis here was wrong. GitHub Actions has been failing on every run since ~2026-07-14 — an account-billing failure, not a workflow defect: jobs are never started, so
Detect changesfails in 3s and every real check skips. This has been true across merged PRs #472/#473/#474 andpush dev, meaning recent merges carry no CI signal whatsoever;474 was gated on a local
make ci-check(lint+type+security, 4858 passed, 84.90% coverage, all 13¶modules ≥80%) plus a live-Postgres migration round-trip instead. An initial theory that the
actions/checkout@v7.0.0pin was invalid was incorrect — v7.0.0 is the current release; the pin is fine. Recorded as a P0 ops row in the release-engineering table. Until billing is fixed, green checkmarks on this repo are meaningless andmake ci-checkis the only gate.Deferred, deliberately.
list_strategies_asyncstill seeds/promotesis_defaulton the read path (spec said stop writing; it interacts with the frontend's auto-prompt, so it needs its own pass), and theis_default/is_scanner/alerts_enabledcolumn drops want a separate migration — the model/schema still carry them. Neither blocks anything.Clean-sweep verified, leaderboard fixed, Strategy Builder repointed (2026-07-14):
Clean sweep done. The gate that headed the queue is cleared: a full local sweep dispatched 4 of 506 matches and all 4 reached COMPLETED, exercising T1+T2 together on real data; outcome rows were regenerated from the clean run (the SEV-0 window wrote a gap, so there was nothing to quarantine). The learning loop (#3) and analog finder are no longer gated on data hygiene.
Leaderboard + edge weights (PR #473). The clean-sweep verification surfaced that the leaderboard hardcoded the legacy four analysts in three places while a session's breakdown carries eight —
sector/insider/earnings_revisions/valuation_comparableswere structurally invisible and could never earn an adaptive weight. Now derived from the breakdown (4→8 live); edge-signal weights made adaptive on their own carve-budget (damped ±0.03, clamp [0.02, 0.15], total ≤0.30). Live result:insiderat a 1.00 directional hit rate (0.05→0.08),valuation_comparablesdemoted (0.10→0.06→0.03). This de-risks the learning loop, which would otherwise have trained on half the roster.Strategy Builder repoint (spec
docs/specs/2026-07-14-strategy-risk-rules-design.md). Discovery during the above: the entireStrategyConfig(analyst weights, aggregator, debate, entry/exit) reaches no code path;is_default/is_scanner/alerts_enabledare write-only; andpaper_portfolio_serviceevaluates every signal against all tracks with module constants — so tracks trade identically and/fund/comparecompares tracks identical by construction. Decision: delete the analyst-weight canvas (the platform owns weighting now — #473), expose the four rules a user genuinely owns (min-confidence, stop-loss, take-profit, max-hold) by wiring them through the existingpaper_portfolios.strategy_config_snapshot(already read formin_signal_confidence), and merge the page into Proving Ground — landing IA Phase B's "extra noun" intent and resolving the 2026-07-11 Strategy Builder pairing rule (the dead controls it warned about are being deleted, not shipped). Makes/fund/comparemeaningful for the first time. Implementation plan next.SEV-0 resolved, roadmap reordered, EDGAR bound to T3 (2026-07-13):
SEV-0 closed. Root cause was infrastructure, not pipeline logic and not a regression from the lifecycle work: a Kafka-bootstrap misconfig (
488976869) meant the job-worker leakedlocalhost:29092from.env, so no system-user sweep session had ever completed; a poisoned producer singleton then hung every later publish. "All HOLD" was a symptom of sweeps being dead, not of bad math. A companion audit found and fixed four signal-quality defects (PR #470: sentiment double-scaling,signals.confidenceclamped to 1.0, a startup reaper that failed live sessions, an adaptive-weights passthrough stub), and pipeline-health alerting shipped in the same PR (PipelineAnalysesFailing,PipelineAllHold). The SEV section is marked RESOLVED with nothing open.No prod yet. The prod-shaped follow-ups were retired: the SEV-0 wrote a data gap, not poison (nothing completed → nothing to quarantine), and the dev-only audit rows are regenerated from a clean local sweep rather than backfilled. Consequently P0 compliance is reframed from a background track to the gate to having prod, with Stripe pulled alongside T1 for lead time.
Structure. P1 was split into P1a — the living-thesis spine (the two shipped slices + T1→T4 as one contiguous milestone table, so the spine reads as a unit) and P1b — edge & moat (non-spine). The T2 "what changed" delta moved up from P2 into P1a as the spine milestone it is. "Recommended sequencing" was rewritten with a Done band, an In-progress band (T1 — spec'd, planned, and partially coded), and a clean forward order.
EDGAR ↔ T3 binding. Scope correction: the EDGAR item is not greenfield —
InsiderAnalystalready feeds insider conviction into confluence, but off FMP's lagged, repackaged data. The genuinely new part is the ingestion layer. Since EDGAR's real edge is latency — which a nightly confluence signal under-realizes — the poller is deliberately built once and consumed twice: as a confluence feature (#7), and as T3's first trigger source (#8). The poller therefore emits filing events from day one, before any consumer exists, mirroring how slice A shipped theopportunity.lifecycletopic ahead of its first subscriber. They stay two sequenced items, not one merged task: the confluence upgrade has no dependency and can ship immediately, whereas T3 is gated behind the learning loop and needs its own design pass.Process note. An earlier version of this reorder was lost because it sat as uncommitted working-tree edits while another session moved HEAD. Commit roadmap changes immediately — unstaged doc edits on a shared branch do not survive.
T1 slice A shipped + SEV-0 opened (2026-07-12, same day): opportunity lifecycle enforcement landed end-to-end (commits
ee2449a09…eba400249on dev), including the three plan-review fixes: scanner freezessetup_expiryat creation (new rows aren't immortal), the sweep gates onis_market_open()(close-below is a real close, not an intraday print), and the card renders only the server's verdict. The spine's T1 groundwork — lifecycle enum in contracts, close-below semantics, job-worker as single writer — is now settled in code. Note the vocabulary islive/…notactive/…; T1 inherits the code's naming. The Kafkaopportunity.lifecycletopic has no consumer yet — the daily-brief thin v0 is its intended first subscriber, which strengthens the case for pulling the brief forward. Same day, a SEV-0 was opened (section above P0): every signal reportedly HOLD, all analyses failing — unverified symptom, triage checklist in the item. Until it's fixed, treat outcome rows written in the breakage window as suspect (they feed the leaderboard, analog finder, and learning loop), and sequence nothing ahead of it.Stale-basis correction (2026-07-12): the "why now + invalidation" P1 item was already shipped when picked up — the 2026-07-07 basis ("
Opportunitycarries no actionability fields;ActionPlanExtractoroutput never reaches the feed") no longer held. Reality:opportunity_enrichment_service.pycomputes a three-layerOpportunityEnrichment(deterministic metrics tier → catalyst overlay → ActionPlan override) at read time (no schema migration), attached to every list/get response and rendered byOpportunityCard.tsx, with integration coverage. The item is struck through with the corrected basis preserved. What genuinely remained is enforcement —setup_expirywas decorative (no list-route filter; expired setups ranked normally, label client-computed) and nothing transitioned an opportunity when price crossedinvalidation_level(action_plan_evalnever propagated status to the Opportunity row) — now captured as a new P1 item, opportunity lifecycle enforcement (spine T1 slice A), sequenced at #2a, whose status vocabulary is deliberately the thesis lifecycle vocabulary so T1 promotes rather than replaces it. Also recorded: both P0.5 defect items landed 2026-07-11 (debate controls hidden; adjusted-close fix with entry-price rebasing; backfill script written, prod run still pending — redeploy worker images first). The episode is the roadmap's own warning made flesh: re-verify before starting an item — code moves.Living-thesis spine + workflow review (2026-07-11): adopted the session→thesis reframe as the roadmap's organizing spine (§ "The organizing spine" above). Sessions stay immutable frames; a per-symbol living thesis links them with status/delta/resolution, re-evaluated on a cost-tiered cadence (free arithmetic every scan → cheap deterministic overnight → full swarm only on material change), scoped to held + watched symbols, with the frames-vs-thesis grading policy to be settled before v1. Existing items were re-tagged as spine milestones: T1 = new living-thesis-v0 item (P1), T2 = "what changed" delta + feed-level flip twin (P2), T3 = trigger center's thesis watchers (P1), T4 = exit intelligence's trim/exit/add engine (P1) — one data model + one tiered loop instead of five disconnected widgets, with portfolio/watchlist/brief/stock-page/alerts as views of it. Also folded in from the same review: daily-brief thin v0 pulled forward as the platform's front door (highest-leverage workflow change; there is currently no daily loop); IA Phase B (fold Catalyst into Opportunities as a source/filter; evaluate merging Signals + Proving Ground into one track-record surface; demote the Sessions/Analyses archive behind stock-page thesis timelines); Strategy Builder pairing rule (quick-validation replay ships alongside the builder or the builder is de-emphasized in beta nav — dead controls plus weeks-to-feedback is a workflow dead end in a flagship); and a cap on the beginner-onramp polish track (tooltips/renames treat symptoms of the wide IA + missing front door; the brief teaches the product by using it). Sequencing updated: thesis v0 (2b) and overnight flip detection (2c) now precede the full exit-intelligence engine, which lands as T4 after triggers (#7).
Discovery-leverage review (2026-07-10): two framings added, no new items. (1) The historical analog finder is also a discovery mode — rank/badge sweep candidates by similarity to past winners, not only analysis-time base rates. (2) The "what changed" delta has a feed-level twin: a consensus-flip detector over stored overnight sessions as a zero-new-data discovery signal into the opportunities feed. Housekeeping done same day: deleted the vestigial
/market→/screenerredirect page; removed stalebackend/legacyexcludes frompyproject.toml(dir no longer exists); CLAUDE.md corrected (dropped deletedmonitor/page, droppedbackend/legacynote, markedk8s/as an untested scale path — production deploys via single-host docker-compose). Verified intentional, left alone:docs/mkdocs/site/is committed on purpose so Cloudflare Pages deploys without a build step (.gitignorecomment).Product-direction review (2026-07-08): added the "Who this is for" compass (semi-pro swing/position traders — 5d/20d/60d horizons, overnight batch, 30 s heartbeat; day trading out of scope by construction). Three P1 additions: position-health / exit intelligence (verified: risk overlay computes stops only at decision time, nothing re-evaluates holdings), historical analog finder (verified: pgvector + outcome data exist, no similarity retrieval anywhere), and earnings-call transcript intelligence (verified: no transcript code in
backend/data/; FMP serves transcripts on the existing key). One P0.5 defect: outcome tracking reads rawclose(job_runners.py:136) while the vendor exposesadj_close(_fmp_market.py:96). Two P2 additions: portfolio-anchored position sizing (risk overlay outputs abstract fractions only) and canonical-discovery IA consolidation (five surfaces answer "where do I find ideas?"). Daily brief gained a thin-v0 exception. Verified non-gap along the way: the catalyst streams survived the yfinance removal — unusual options flow (get_options_chain) and short interest (shortPercentOfFloat) route throughfetch_via_data_layer; the 2026-03-04 trading-OS design's data-source table is stale, not the code.Corrections folded in from the 2026-07-08 signal-pipeline integrity pass (branch
fix/signal-pipeline-integrity-bugs): three latent bugs corrupted the "adaptive/forward-tested" claims. (1) The analyst leaderboard queriedresult->'analyst_scores'/result->'signal', keys sessions never write — the real shape isresult->'unified_signal'->'breakdown'— soWeightServicealways saw zero rows and the adaptive-weight loop was inert. (2) Outcome tracking clamped its price index, finalizingresult_5d/20d/60dfrom insufficient history, and the pending-outcome filter dropped signals after the first (premature) write. (3) The risk overlay defaulted every Starter-plan decision to HOLD because it read only the (empty) Research-Manager plan, never the deterministic recommendation. All fixed test-first; the leaderboard delta was also changed from signed win/loss score averages to a directional hit-rate so a correctly-bearish analyst is rewarded. Follow-up: the historical outcome rows written before the fix are still corrupt — see the backfill item in P0.5. Also corrected:TradeJournalEntry.linked_session_idalready exists (the P2 journal item overstated the gap).Beta-scope pass (2026-07-07, same review): four small additions accepted into beta scope (PWA installability, expanded alert types, journal→signal schema link, "what changed" delta); two consciously deferred to P3 as post-beta power-user features (personal API keys / exports / outbound webhooks, custom screen-condition builder). Verified along the way: screener/scanner already support custom/watchlist/portfolio universes, a command palette exists, and watchlist symbols auto-notify on signals — none of these are gaps.
Corrections folded in from the 2026-07-07 gap review: the "Why now + invalidation" item was described in the P1 purposes but missing from the P1 table — restored (its verified basis:
Opportunitymodel carries no actionability fields;ActionPlanExtractoroutput never reaches the feed). The Signal Intelligence dashboard frontend now exists (components/signals/) — the P2 item was downgraded to verify-and-close. New items added: Strategy Builderdebate.*config is unwired (P0.5 defect); backtest removal (f42da1e85) left no fast strategy-validation path (quick-validation replay, P2); first-run onboarding is undefined while signup is closed (P2).Corrections folded in from the 2026-07-03 verification pass: self-switch is a gated beta flag (not an open leak); the universe is 98 symbols (not ~600); scheduling is single-source Temporal (APScheduler is dead code, not a duplicate-run risk); a
webhooks.pyroute already exists (bounce handling may be partial). Treat any older doc claiming otherwise as stale.
Archived: resolved incident & defect records¶
Moved out of the roadmap 2026-07-17 when the plan/history split happened. These sections were closed — nothing in them is open work — but they carry root-cause detail (exact commits, why the obvious fix was wrong) that the shipped-log summaries above compress away. Preserved verbatim.
SEV-0 — Core pipeline (RESOLVED 2026-07-12)¶
Opened 2026-07-12 (operator: every signal HOLD, analyses failing) → root-caused and fixed the same day. The cause was not pipeline logic and not a regression from the T1-slice-A lifecycle work — it was infrastructure config plus a poisoned producer singleton. A companion end-to-end audit then found four signal-quality defects (the pipeline ran, but produced degraded numbers); all four are fixed, and pipeline-health alerting shipped in the same PR (#470). The whole SEV-0 cleanup is closed — nothing open.
No production environment exists yet, so the usual "quarantine poisoned rows / redeploy prod" follow-ups do not apply: the SEV-0 wrote a data gap (no sweep session ever completed → nothing to quarantine), and the audit-defect rows are throwaway dev data — regenerate clean from a local sweep, don't write a backfill.
| Item | Type | Effort | Impact | Verified basis / status |
|---|---|---|---|---|
| ~~SEV-0: all-HOLD + failing analyses~~ — FIXED 2026-07-12 | Defect | — | Existential | Two infra defects, not logic: (1) docker-compose.yml job-worker leaked .env's localhost:29092 because it lacked REDHOUND_KAFKA_BOOTSTRAP=kafka:9092 → every sweep/scanner dispatch failed at Kafka publish; no system-user sweep session had ever completed (fix: 488976869). (2) kafka_producer.py assigned the module global before start() and only rolled back on TimeoutError → a KafkaConnectionError left a half-started producer poisoning the global, hanging every later send_and_wait (orphaned RUNNING sessions). Fixed: publish to the global only after a successful start, behind an asyncio.Lock. "All HOLD" was the symptom — with sweeps dead, only rare manual runs ran, and those legitimately computed HOLD. |
| ~~Signal-quality audit (4 defects)~~ — FIXED 2026-07-12 (PR #470; sweep path proven end-to-end) | Defect | — | High | (1) Sentiment double-scaling — the worker stored −100..+100 while SignalAggregator expected −1..+1 → sentiment pegged to ±100 on every run (~21% weight; the dominant distortion). (2) signals.confidence always 1.0 — a 0–100 value clamped into a 0–1 contract; _extract_signal also read the wrong key and silently used the pre-debate fallback. (3) Startup session reaper marked all RUNNING/PENDING sessions FAILED on every API start — an in-process-era relic that reaped live worker sessions (extracted to session_reaper.py; explains the "analyses fail across the board" perception). (4) Adaptive weights never applied — run_accuracy_injector was a passthrough stub, so the leaderboard/WeightService was dead in production (now wired). Aggregator math itself verified correct to the cent on two real sessions. |
| ~~Pipeline-health alerting~~ — DONE 2026-07-12 (PR #470) | Foundation | S | High | docker/prometheus/rules/alerts.yml → redhound-pipeline group: PipelineAnalysesFailing (rate-based; >50% of analyses failing over 30m — the "analyses fail across the board" symptom) and PipelineAllHold (decision mix collapsed to HOLD over 6h, with a high threshold + minimum volume so a genuinely quiet market can't trip it). Backed by new metrics redhound_analyses_total{outcome} + redhound_analysis_decisions_total{decision}. Both SEV-0-class breakages were previously caught by a human squinting at row counts; this is the guard that makes the next one page someone. |
P0.5 — Quick wins (defect-level, all shipped 2026-07-11)¶
Small items that erode trust in flagship surfaces. Cheaper to fix than to explain.
| Item | Type | Effort | Impact | Verified basis / dependency |
|---|---|---|---|---|
~~Wire (or hide) Strategy Builder debate.* config~~ — DONE 2026-07-11. Hidden, not wired. |
Defect | S | High (trust) | The DebateStage controls were not merely dead — their skip hints ("Skips when conf ≥ 80% / ≥ 60%") described gating the pipeline never had. Stage is now read-only and states the real behavior (confidence sets round count at 75%; Starter plan skips). DebateConfig is retained in api/models/strategy.py (marked UNWIRED) so already-persisted strategies still validate. Wire later only if strategy-level debate config earns its keep. |
| ~~Backfill corrupt signal-outcome rows~~ — DONE 2026-07-11. | Defect | S | High | scripts/backfill_signal_outcomes.py (--dry-run, --lookback-days). Recomputes rather than nulls: recomputing with the fixed _fetch_prices_for_signal is ground truth for both poisons (clamping and corporate actions), where a time-based null-out only addresses clamping. Rows that cannot be re-derived (delisted, vendor gap) are nulled only if provably unsound, so the fixed nightly job re-evaluates them. Added SignalService.clear_signal_outcome_async (dropping the outcome key is what unlocks a row — the pending filter only re-evaluates while result_60d IS NULL). No prod yet → nothing to run; regenerate dev outcome data from a clean local sweep instead. |
| ~~Outcome tracking must use adjusted closes~~ — DONE 2026-07-11. | Defect | S | High | Fixed in _fetch_prices_for_signal. Note the naive fix (swap close→adj_close) is still wrong: signal.price is the raw entry price, so a 2:1 split still reads as ~-50%. Both sides of the return ratio must share one basis — the entry price is now rebased by the signal-date bar's adj_close/close factor (1.0, i.e. a no-op, absent a corporate action). Expiry-drift used raw close too; also fixed. |
Archived: shipped spine milestone records (P1a)¶
The four shipped rows from the P1a table, moved here 2026-07-17. The roadmap keeps only the constraints these settled (see P1a → "Constraints the shipped spine settled"); the full implementation records — spec/plan paths, migrations, service names — are preserved verbatim here.
| Milestone | Item | Effort | Status / Verified basis |
|---|---|---|---|
| slice | ~~"Why now" + invalidation on opportunities~~ | S–M | SHIPPED (found 2026-07-12). opportunity_enrichment_service.py computes OpportunityEnrichment (why_now/entry/invalidation/target/expiry/…) in three layers — deterministic metrics tier → catalyst overlay → ActionPlan override — at read time (no migration), attached to every list/get, rendered by OpportunityCard.tsx, integration-covered. The death-condition fields T1 needs already exist here. |
| slice A | ~~Opportunity lifecycle enforcement (T1 slice A)~~ | S | DONE 2026-07-12 (PR #469; plan docs/plans/2026-07-12-opportunity-lifecycle-enforcement.md). LifecycleState vocab in backend/contracts/models/lifecycle.py (dependency-free — the enum T1 promotes); lifecycle_state+setup_expiry on Opportunity (migration 040; scanner freezes expiry at creation so new rows aren't immortal); close-below transition rules (opportunity_lifecycle_service.py, precedence plan-verdict > price > expiry, terminal-is-terminal); daily sweep on ActionPlanEvalWorkflow gated on is_market_open(); OpportunityLifecycleEvent → Kafka opportunity.lifecycle (no consumer yet — the daily brief is the intended first subscriber); feed hides corpses (include_dead=true retains them for attribution). Settled for T1: invalidation = close-below at eval time; single writer = job-worker (user-intent status stays orthogonal, API-owned). |
| T1 | ~~Living thesis object + invalidation watching~~ — per-symbol thesis linking sessions as immutable frames; status (live/invalidated/expired/played-out/flipped); tier-1 watching of invalidation/expiry/regime every scan cycle, scoped to held + watched symbols | S–M | DONE 2026-07-13 (PR #471). Spec docs/specs/2026-07-12-t1-living-thesis-design.md. Thesis model + migration 041 + repo + ThesisService.apply_frame (OPEN/ATTACH/FLIP/SKIP, worker-written only) + read-only API + tier-1 lifecycle sweep on ActionPlanEvalWorkflow; ThesisUpdatedEvent/ThesisLifecycleEvent → Kafka (no consumer yet — the daily brief is the intended first subscriber). Frames are immutable; sessions attach via sessions.thesis_id. Surfaces deliberately deferred — no UI yet; "Your NVDA thesis was invalidated at $478 this morning" needs the daily brief (#2) to actually reach the user. |
| T2 | ~~Overnight flip detection~~ — deterministic analysts re-run overnight; consensus-flip + confidence-jump (≥15 pts) deltas vs. the previous frame, surfaced as a feed-level discovery signal | S–M | DONE 2026-07-13 (PR #472). Spec docs/specs/2026-07-13-t2-overnight-flip-detection-design.md. Built as the thesis's delta field, not a standalone widget. Half A: ThesisRefreshWorkflow (24h, offset 22:00 UTC) re-runs cheap deterministic analysis per held∪watched symbol, system-scoped (one analysis/symbol, debate_enabled=False). Half B: pure classify_thesis_delta (flip/confidence_jump/minor/none, 15-pt threshold) → denormalized last-delta snapshot on the theses row (migration 042) → discovery sweep fans significant deltas out as per-user thesis_flip/thesis_shift opportunities in the existing feed. No new UI (matches T1's deferred-surface pattern); flips ride the opportunities feed. Discovery rows expire after 7d. Together with T1 this is exit-intelligence v1 viewed on the portfolio. |