Skip to content

fix(news): guard the panel signals projection#120

Merged
FlyM1ss merged 3 commits into
mainfrom
fix/panel-signals-drift-guard
Jul 15, 2026
Merged

fix(news): guard the panel signals projection#120
FlyM1ss merged 3 commits into
mainfrom
fix/panel-signals-drift-guard

Conversation

@FlyM1ss

@FlyM1ss FlyM1ss commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Closes #119.

The panel's signals dict reached the browser raw from the wire — the one panel path with no projection, and so the one path _alarm_if_all_dropped was blind to, since it only ever sees what a projection dropped. Nothing server-side read sentiment_score; the browser did, where Number(undefined).toFixed(2) renders the string "NaN". A rename would have painted NVDA NaN into every chip with no drop, no exception, no badge, no log. The feed keeps building (different keys), so the existing alarm could not fire.

Project signals to the keys the panel renders, drop what it can't, run the existing alarm over it. Independent of the feed projection: a story with no sentiment read is still a story, so coupling them would let one break take out both.

The alarm is appended after the feed block, not folded in above. The items alarm assigns drifted outright rather than OR-ing it, so seeding it earlier is clobbered whenever the items feed loads — i.e. always, in prod. The guard would read correctly and fire never. test_panel_signals_drift_survives_a_healthy_items_feed pins that; the other drift tests leave _http_get_items unpatched and cannot see it.

Behavior change: an entry missing a rendering key is dropped rather than rendered broken, and an all-dropped signals list escalates to degraded. Verified against the producer that this cannot fire on a legitimate payload: _PANEL_SIGNAL_KEYS is a subset of the vendored v2 schema's per-entry required list, the producer builds all four unconditionally, and _PUBLIC_STRIP is top-level-only.

Second commit is self-review: mutation testing showed 3 of the 4 keys in the new guard were themselves unpinned (dropping sentiment/url/source left the suite green). Also the obvious non-dict test is vacuous — a str substring-matches, so it passes with the guard deleted; parametrized on None/42 instead.

Stacked on #117 (same files). Suite 984 passed / 4 skipped.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pbs5wnQZhTi9CQ3pb14vDo

FlyM1ss and others added 2 commits July 15, 2026 18:37
`get_latest_panel_payload` returned the wire's `signals` dict raw. That made
it the one panel path with no projection, and so the one path
`_alarm_if_all_dropped` was blind to — it can only see what a projection
dropped. Nothing read `sentiment_score` before the browser did, where
`Number(undefined).toFixed(2)` renders the string "NaN": a rename would have
painted "NVDA NaN" into every chip with no drop, no exception, no badge and
no log. The score -> sentiment_score rename passed within inches of this.

Project `signals` to the keys the panel actually renders (sentiment,
sentiment_score, url, source), drop what it can't, and run the existing
alarm over it. Independent of the feed projection: a story with no sentiment
read is still a story, so coupling them would let one break take out both.

The alarm is appended AFTER the feed block, not folded in above: the items
alarm assigns `drifted` outright rather than OR-ing it, so seeding it earlier
is clobbered whenever the items feed loads — i.e. always, in prod. The guard
would read correctly and fire never. test_panel_signals_drift_survives_a_
healthy_items_feed pins exactly that; the other drift tests leave
`_http_get_items` unpatched and cannot see it.

Behavior change: an entry missing a rendering key is now dropped rather than
rendered broken, and an all-dropped signals list escalates to `degraded`.

Suite 978 passed / 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbs5wnQZhTi9CQ3pb14vDo
Self-review with mutation testing: dropping `sentiment`, `url` or `source`
from _PANEL_SIGNAL_KEYS left the whole suite green — only `sentiment_score`
was actually pinned. A guard against renamed fields whose own key set is
unpinned against renaming is the same bug one level up. One failing case per
key now.

The isinstance branch was likewise untested, and the obvious test for it is
vacuous: a `str` entry substring-matches (`"sentiment" not in "not-a-dict"`
is just True), so it drops via the missing-key branch and passes with the
guard deleted. Parametrized on None/42, which actually reach the TypeError.

Also widen _alarm_if_all_dropped's `projected` hint to Union[list, dict] —
the new call site passes a dict; only len()/truthiness are used, so this was
cosmetic, but the hint said otherwise.

Suite 984 passed / 4 skipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbs5wnQZhTi9CQ3pb14vDo
@vercel

vercel Bot commented Jul 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentic-trading-lab Ready Ready Preview, Comment Jul 15, 2026 2:53pm

@FlyM1ss

FlyM1ss commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

Note on CI: ci.yml triggers pull_request on main only, so the backend suite does not run on this PR while it is stacked on chore/news-signals-v2-cutover — the green checks above are Vercel only. It retargets to main automatically when #117 merges, and CI fires then.

Run locally against this branch in the meantime: 984 passed, 4 skipped (full dashboard/backend/tests/).

@FlyM1ss
FlyM1ss changed the base branch from chore/news-signals-v2-cutover to main July 15, 2026 13:20
The items alarm assigned `drifted` while the other two OR-ed it, so the
panel-signals alarm had to be appended last and a comment kept it there.
That is the guard's own failure mode one level up: read correctly, fire
never, held off by a comment and a line number.

All three now OR, so each projection sits with its own alarm and order
carries no meaning. Behaviour is identical — `drifted` starts False, so
the old bare assign and the OR agree.

OR alarm-first (`_alarm(...) or drifted`), never `drifted or _alarm(...)`:
`or` short-circuits, so the reversed form stops calling later alarms once
anything has drifted and swallows the ERROR they exist to emit. It reads
identically and passed all 48 tests, so
test_every_concurrent_drift_gets_its_own_alarm pins it.

Also: state the projection-independence invariant once, at
_PANEL_SIGNAL_KEYS where someone would edit it, and drop the incident
retelling _alarm_if_all_dropped already carries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@FlyM1ss

FlyM1ss commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

/simplify pass (4 review angles) — one substantive fix, pushed as f054180.

Accumulation made uniform. The items alarm assigned drifted while the other two OR-ed it, which is why the panel-signals alarm had to be appended last with a comment holding it in position. All three now OR, so each projection sits with its own alarm and order carries no meaning. Behaviour identical (drifted starts False).

Worth knowing: the obvious tidy-up — drifted or _alarm(...) — is a silent regression. or short-circuits, so once anything has drifted the later alarms are never called and never log. It reads fine and passed all 48 tests, so it's now pinned by test_every_concurrent_drift_gets_its_own_alarm. Alarm-first (_alarm(...) or drifted) is load-bearing, not stylistic.

Also: independence invariant stated once (at _PANEL_SIGNAL_KEYS), and dropped the incident retelling _alarm_if_all_dropped already carries.

Reviewed and rejected: generalizing the three projection+alarm pairs into one mechanism (three different trigger conditions and raw types — it'd move complexity, not remove it); deriving _PANEL_SIGNAL_KEYS from signals-v2.schema.json (test-only fixture; prod importing from tests/ is worse).

Follow-ups, not in this PR:

  • _project_panel_signals keeps the full raw entry (projected[sym] = s), so ~1–3KB of unread headline/guid/n_articles still ships to the browser. Safe to trim (no consumer reads them) but it changes the payload shape.
  • docs/integrations/finsearch-news-items.md still describes the alarm as covering two projections; it's three now, and the traceability table has no row for the new guard.

Suite 998 passed / 2 skipped.

@FlyM1ss
FlyM1ss merged commit 972b1c4 into main Jul 15, 2026
5 checks passed
@FlyM1ss
FlyM1ss deleted the fix/panel-signals-drift-guard branch July 16, 2026 03:22
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.

Panel signals passthrough has no drift guard

1 participant