Skip to content

Commit 4d4cc08

Browse files
Pigbibicodex
andcommitted
fix: publish unavailable watcher snapshots safely
Co-Authored-By: Codex <noreply@openai.com>
1 parent fca113c commit 4d4cc08

3 files changed

Lines changed: 96 additions & 7 deletions

File tree

.github/workflows/strategy_optimization_watcher.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ jobs:
272272
| tee data/output/strategy_optimization_watcher/research-diagnosis.json
273273
274274
- name: Extract bounded research task source snapshot
275-
if: steps.fetch-metrics.outputs.downloaded == 'true'
275+
if: success()
276276
working-directory: bridge
277277
run: |
278278
set -euo pipefail
@@ -289,7 +289,7 @@ jobs:
289289
PY
290290
291291
- name: Publish research task index to the unified console
292-
if: steps.fetch-metrics.outputs.downloaded == 'true' && github.event_name == 'schedule'
292+
if: success() && github.event_name == 'schedule'
293293
env:
294294
RESEARCH_TASK_SYNC_URL: ${{ vars.QSL_RESEARCH_TASK_SYNC_URL }}
295295
RESEARCH_TASK_SYNC_TOKEN: ${{ secrets.QSL_RESEARCH_TASK_SYNC_TOKEN }}

scripts/run_strategy_optimization_watcher.py

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -304,13 +304,24 @@ def run_research_input_terminal_watcher(
304304
comment_issue: Callable[[str, str, str], str] = comment_github_issue,
305305
list_issues: Callable[[str], dict[str, str]] = list_open_issue_urls,
306306
) -> dict[str, Any]:
307-
"""Surface a trusted deferred P1 record as an issue-only finding."""
307+
"""Surface a trusted deferred P1 record as an issue-only finding.
308+
309+
An accepted P1 terminal record is not a failure. It simply means that
310+
the producer has not yet emitted the two comparable P3 observations the
311+
watcher needs. Keep that state visible to the unified console without
312+
opening a misleading issue or failing the scheduled watcher.
313+
"""
308314
candidate = terminal.get("candidate") if isinstance(terminal.get("candidate"), dict) else {}
315+
status = str(terminal.get("status") or "").strip().upper()
316+
reason_code = str(terminal.get("reason_code") or "").strip()
317+
if status != "DEFERRED" or not reason_code:
318+
reason = "p1_terminal_accepted" if status == "ACCEPTED" else "p1_terminal_contract_unavailable"
319+
return no_comparable_metrics_result(reason=reason, dry_run=dry_run)
309320
finding = build_research_input_unavailable_finding(
310321
repo=source_repo,
311322
profile=profile,
312-
status=str(terminal.get("status") or ""),
313-
reason_code=str(terminal.get("reason_code") or ""),
323+
status=status,
324+
reason_code=reason_code,
314325
candidate_id=str(candidate.get("candidate_id") or ""),
315326
date_cutoff=str(terminal.get("date_cutoff") or ""),
316327
source=source,
@@ -331,6 +342,32 @@ def run_research_input_terminal_watcher(
331342
return result
332343

333344

345+
def no_comparable_metrics_result(
346+
*, reason: str = "comparable_metrics_unavailable", dry_run: bool = True
347+
) -> dict[str, Any]:
348+
"""Return a successful, source-owned unavailable queue snapshot.
349+
350+
This is deliberately not an exception: optimization requires two trusted
351+
comparable P3 observations. Until they exist, the console must show an
352+
unavailable source rather than silently retaining stale tasks or marking
353+
an accepted P1 acquisition as a watcher failure.
354+
"""
355+
snapshot = research_task_source_snapshot(
356+
[],
357+
context_available=False,
358+
computed_at=datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
359+
)
360+
snapshot["errors"] = sorted(set(snapshot["errors"] + [reason]))
361+
return {
362+
"status": "ok",
363+
"dry_run": dry_run,
364+
"findings": 0,
365+
"issues": [],
366+
"errors": 0,
367+
"research_task_source_snapshot": snapshot,
368+
}
369+
370+
334371
def main() -> int:
335372
try:
336373
input_path = resolve_input_path(
@@ -342,7 +379,8 @@ def main() -> int:
342379
print(json.dumps({"status": "error", "error": str(exc)}, sort_keys=True))
343380
return 2
344381
if input_path is None:
345-
print(json.dumps({"status": "skipped", "reason": "strategy metrics input not configured"}, sort_keys=True))
382+
result = no_comparable_metrics_result(reason="metrics_input_not_configured")
383+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
346384
return 0
347385
terminal_path_text = os.environ.get("STRATEGY_WATCH_TERMINAL_STATUS_PATH", "").strip()
348386
terminal_path = None
@@ -357,7 +395,8 @@ def main() -> int:
357395
return 2
358396
if not input_path.exists():
359397
if terminal_path is None or not terminal_path.is_file():
360-
print(json.dumps({"status": "skipped", "reason": "strategy metrics input not found — this is expected when the source repository has not yet published metrics"}, sort_keys=True))
398+
result = no_comparable_metrics_result()
399+
print(json.dumps(result, ensure_ascii=False, sort_keys=True))
361400
return 0
362401
try:
363402
terminal = load_payload(terminal_path)

tests/test_run_strategy_optimization_watcher.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import subprocess
77
import tempfile
88
import unittest
9+
from contextlib import redirect_stdout
10+
from io import StringIO
911
from unittest.mock import patch
1012

1113
from scripts.run_strategy_optimization_watcher import (
@@ -103,6 +105,54 @@ def test_deferred_terminal_creates_issue_only_finding(self) -> None:
103105
self.assertEqual(result["findings"], 1)
104106
self.assertEqual(len(created), 1)
105107
self.assertEqual(result["issues"][0]["task"]["trigger"]["kind"], "strategy_research_input_unavailable")
108+
109+
def test_accepted_terminal_is_visible_but_not_a_watcher_failure(self) -> None:
110+
result = run_research_input_terminal_watcher(
111+
{
112+
"status": "ACCEPTED",
113+
"reason_code": "",
114+
"candidate": {"candidate_id": "soxl_soxx_core_only_p2_v3"},
115+
},
116+
source_repo="QuantStrategyLab/UsEquitySnapshotPipelines",
117+
profile="soxl_soxx_trend_income",
118+
dry_run=False,
119+
)
120+
121+
snapshot = result["research_task_source_snapshot"]
122+
self.assertEqual(result["status"], "ok")
123+
self.assertFalse(result["dry_run"])
124+
self.assertEqual(result["errors"], 0)
125+
self.assertEqual(result["findings"], 0)
126+
self.assertEqual(snapshot["data_status"], "unavailable")
127+
self.assertEqual(snapshot["tasks"], [])
128+
self.assertIn("p1_terminal_accepted", snapshot["errors"])
129+
self.assertIn("research_task_context_unavailable", snapshot["errors"])
130+
131+
def test_main_publishes_unavailable_snapshot_when_metrics_do_not_exist(self) -> None:
132+
with tempfile.TemporaryDirectory() as directory:
133+
original = dict(os.environ)
134+
output = StringIO()
135+
try:
136+
os.environ.update(
137+
{
138+
"STRATEGY_WATCH_SOURCE_ROOT": directory,
139+
"STRATEGY_WATCH_METRICS_PATH": "data/output/not-yet-published.json",
140+
"STRATEGY_WATCH_SOURCE_REPO": "QuantStrategyLab/UsEquitySnapshotPipelines",
141+
"STRATEGY_WATCH_DRY_RUN": "true",
142+
}
143+
)
144+
with redirect_stdout(output):
145+
self.assertEqual(main(), 0)
146+
finally:
147+
os.environ.clear()
148+
os.environ.update(original)
149+
150+
result = json.loads(output.getvalue())
151+
snapshot = result["research_task_source_snapshot"]
152+
self.assertEqual(result["status"], "ok")
153+
self.assertEqual(snapshot["data_status"], "unavailable")
154+
self.assertIn("comparable_metrics_unavailable", snapshot["errors"])
155+
106156
def test_monitoring_dispatch_does_not_repeat_existing_issue(self) -> None:
107157
finding = build_strategy_monitoring_finding(
108158
domain="crypto",

0 commit comments

Comments
 (0)