You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(api): add /api/news/items/ raw news-items endpoint (#359)
* test(api): add TDD suite for /api/news/items/ (Phase B, ATL integration)
17 cases pinning the raw news-items response contract: newest-by-mtime
selection, limit clamping [1,200], conditional GET/ETag variance, poison-pill
vs drop-story validation semantics, and security parity (control/bidi strip,
NEWS_DATA marker, field caps). Written red against the not-yet-existing
news_items view/RAW_ITEMS_DIR setting.
* feat(api): add /api/news/items/ raw news-items endpoint
Phase B of the ATL news-signals integration plan: serves the newest
items-*.jsonl batch, ungated (no subject/roundup/LLM gating — that stays
news_signals-only). Validation/sanitization (_validate_items/_clean_text) is
a byte-faithful port of Heartbeat/news_signals.py's validation_gate/
clean_text, since that script is deliberately isolated and not importable
from Django. Same decorator stack, fail-closed 404 contract, and per-request
memoization pattern as news_signals. Also extends test_api_auth.py with
dedicated 401/dev-mode/valid-bearer coverage for the new route.
Suite: 738 passed, 1 skipped (pre-existing).
* chore(deploy): mount heartbeat digests dir for /api/news/items/
Sibling of the existing signals :ro mount + env: without this the endpoint
404s in prod (RAW_ITEMS_DIR unset -> unconfigured -> fail-closed).
* fix(api): fail-closed on corrupt tickers + no re-stat in conditional-GET + regex port fidelity
- Guard the tickers comprehension in _validate_items against non-string
elements (AttributeError -> 500) and non-list values (char-iteration ->
silently wrong tickers), dropping only the bad entries per the module's
never-500 contract.
- Thread the newest batch's mtime through the _load_items memo instead of
re-stat()ing in _items_etag/_items_last_modified, closing a TOCTOU window
where a pruned batch could 500 instead of 404.
- Restore double-backslash regex escapes in _CONTROL_RE/_LINEBREAK_RE to
match Heartbeat/news_signals.py verbatim (ASCII-only .pattern).
* feat(api): items endpoint speaks news-story v1 (headline/url + schema_version)
* fix(news): harden the items gate and pin the port to its source
Review findings on #359:
- clean_text/validation_gate: non-str required fields drop the story
(same stance as the numeric parse) instead of raising; clean_text is
now total. Fixes a live crash in news_signals.py, where a corrupt
"tickers":[123] raised AttributeError past process_batch's
ValueError-only except and aborted the whole sweep.
- Both copies (Heartbeat + the Django port) fixed together and pinned by
Heartbeat/tests/test_port_parity.py, which AST-extracts the ported
region and compares behaviour without importing Django. Corpus is
mutation-proven: it goes red on the drift it claims to catch.
- _MAX_ITEMS_FILE_MB no longer hardcoded — settings.RAW_ITEMS_MAX_FILE_MB
reads SIGNALS_MAX_FILE_MB, so one operator knob moves both readers.
- signals_views.py added to heartbeat-tests.yml pull_request paths only:
the push trigger also gates the deploy job (paths scope the run, not
the job), so listing it there would redeploy the droplet on a
Django-only merge.
- Coverage for the ?limit Last-Modified suppression, the recency upper
bound, and the blank-line skip — each mutation-verified.
VERSION -> 2026-07-14.1 (deployed behaviour changed); fixture regenerated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(api): document GET /api/news/items/
- api_reference: endpoints-table row + a News Items section (limit
clamp, no as_of, fail-closed 404, ETag/Last-Modified variance). Notes
that its `score` is the editorial score, not the [-1,1] sentiment
score of the same name under /api/signals/news/.
- project_structure: signals_views.py hosts both news endpoints now.
- pipeline design spec: SIGNALS_MAX_FILE_MB has a second reader.
Sphinx build clean (0 warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: Docs/superpowers/specs/2026-07-06-news-to-signals-pipeline-design.md
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -197,7 +197,7 @@ A machine-readable JSON Schema ships at `Heartbeat/schemas/signals-v1.schema.jso
197
197
198
198
## 5. Configuration (the v1 tuning surface)
199
199
200
-
Env vars read by `news_signals.py` (module constants as defaults). This is the surface future user-facing tuning builds on:
200
+
Env vars read by `news_signals.py` (module constants as defaults). This is the surface future user-facing tuning builds on. All are pipeline-only except `SIGNALS_MAX_FILE_MB`, which the Django API reads too (see its row):
201
201
202
202
| Var | Default | Meaning |
203
203
|-----|---------|---------|
@@ -209,7 +209,7 @@ Env vars read by `news_signals.py` (module constants as defaults). This is the s
209
209
|`SIGNALS_THRESHOLD`|`0.20`| ± threshold for bullish/bearish label (40/60 band, empirically backed; was 0.15) |
210
210
|`SIGNALS_DAMP_CAP`|`0.7`| Max \|score\| when under-corroborated |
211
211
|`SIGNALS_DAMP_MIN_ARTICLES`|`2`| Corroboration needed for \|score\| > damp cap |
|`SIGNALS_MAX_FILE_MB`|`10`| Reject oversized items files. **Two readers as of 2026-07-14:**`news_signals.py` (via `load_config`) and the Django API (via `settings.RAW_ITEMS_MAX_FILE_MB`, which `GET /api/news/items/` enforces when validating a batch). Deliberately one operator knob — raising it for the pipeline without raising it for the API would 404 a batch the pipeline happily accepted. Set it in `.env.production` alongside the heartbeat's own env file; the two defaults are pinned together by `Heartbeat/tests/test_port_parity.py`.|
213
213
|`SIGNALS_STALENESS_ALERT_H`|`20`| Canary threshold (§6-C). Tuned, not arbitrary: the daily canary check runs 2 h after the daily beat, so a single fully-missed day leaves the newest artifact ~25.5 h old at the *next* day's check — a 30 h threshold would not cross that (it silently absorbs one entire missed day, only firing after a second consecutive miss); 20 h does. |
0 commit comments