Add Core Hub production stabilization scheduled checks
Introduce the core-hub context resolver stabilization_check query, State Hub progress summaries, activity-definition projections, and unit tests for the CORE-WP-0007 post-cutover window.
This commit is contained in:
parent
c253139ee3
commit
151512a8fa
5 changed files with 383 additions and 1 deletions
|
|
@ -1,4 +1,5 @@
|
|||
from activity_core.context_resolvers import ( # noqa: F401
|
||||
core_hub,
|
||||
event_payload,
|
||||
kaizen,
|
||||
ops_inventory,
|
||||
|
|
|
|||
193
src/activity_core/context_resolvers/core_hub.py
Normal file
193
src/activity_core/context_resolvers/core_hub.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
"""Core Hub production stabilization context adapter.
|
||||
|
||||
Registered as source type ``core-hub``.
|
||||
|
||||
Supported queries:
|
||||
- stabilization_check: public production surface gates for the post-cutover
|
||||
window before CORE-WP-0007-T02 operator sign-off.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from activity_core.context_resolvers.base import CONTEXT_RESOLVER_REGISTRY, ContextResolver
|
||||
from activity_core.context_resolvers.state_hub import _parse_iso_datetime, _utc_now
|
||||
|
||||
_DEFAULT_BASE_URL = "https://hub.coulomb.social"
|
||||
_DEFAULT_TIMEOUT_SECONDS = 15.0
|
||||
_PUBLIC_CHECKS: tuple[tuple[str, int], ...] = (
|
||||
("/healthz", 200),
|
||||
("/readyz", 200),
|
||||
("/api/v2/widget-types", 200),
|
||||
("/api/v2/hubs", 401),
|
||||
)
|
||||
|
||||
|
||||
class CoreHubContextResolver(ContextResolver):
|
||||
"""Resolve lightweight Core Hub stabilization checks over public HTTP."""
|
||||
|
||||
def resolve(self, query: str, event: Any, params: dict[str, Any]) -> dict[str, Any]:
|
||||
if query == "stabilization_check":
|
||||
return _core_hub_stabilization_check(params)
|
||||
return {}
|
||||
|
||||
|
||||
def _bounded_int(
|
||||
value: Any,
|
||||
*,
|
||||
default: int,
|
||||
minimum: int,
|
||||
maximum: int,
|
||||
) -> int:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
return max(minimum, min(maximum, parsed))
|
||||
|
||||
|
||||
def _probe_status(
|
||||
client: httpx.Client,
|
||||
path: str,
|
||||
expected_status: int,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
response = client.get(path)
|
||||
except Exception as exc: # pragma: no cover - network dependent
|
||||
return {
|
||||
"pass": False,
|
||||
"path": path,
|
||||
"expected_status": expected_status,
|
||||
"error": str(exc),
|
||||
}
|
||||
return {
|
||||
"pass": response.status_code == expected_status,
|
||||
"path": path,
|
||||
"expected_status": expected_status,
|
||||
"status_code": response.status_code,
|
||||
}
|
||||
|
||||
|
||||
def _widget_type_count(client: httpx.Client, minimum: int) -> dict[str, Any]:
|
||||
try:
|
||||
response = client.get("/api/v2/widget-types")
|
||||
except Exception as exc: # pragma: no cover - network dependent
|
||||
return {
|
||||
"pass": False,
|
||||
"minimum": minimum,
|
||||
"count": None,
|
||||
"error": str(exc),
|
||||
}
|
||||
if response.status_code != 200:
|
||||
return {
|
||||
"pass": False,
|
||||
"minimum": minimum,
|
||||
"count": None,
|
||||
"status_code": response.status_code,
|
||||
}
|
||||
try:
|
||||
payload = response.json()
|
||||
except ValueError as exc:
|
||||
return {
|
||||
"pass": False,
|
||||
"minimum": minimum,
|
||||
"count": None,
|
||||
"error": f"invalid JSON: {exc}",
|
||||
}
|
||||
if isinstance(payload, list):
|
||||
count = len(payload)
|
||||
elif isinstance(payload, dict) and isinstance(payload.get("data"), list):
|
||||
count = len(payload["data"])
|
||||
else:
|
||||
count = 0
|
||||
return {
|
||||
"pass": count >= minimum,
|
||||
"minimum": minimum,
|
||||
"count": count,
|
||||
}
|
||||
|
||||
|
||||
def _core_hub_stabilization_check(params: dict[str, Any]) -> dict[str, Any]:
|
||||
closeout = bool(params.get("closeout"))
|
||||
now = _utc_now()
|
||||
window_start = _parse_iso_datetime(
|
||||
params.get("window_start", "2026-07-03T00:00:00+00:00")
|
||||
)
|
||||
window_end = _parse_iso_datetime(
|
||||
params.get("window_end", "2026-07-10T17:35:00+00:00")
|
||||
)
|
||||
within_window = bool(
|
||||
window_start and window_end and window_start <= now <= window_end
|
||||
)
|
||||
|
||||
if not within_window and not closeout:
|
||||
return {
|
||||
"kind": "core_hub_stabilization",
|
||||
"skipped": True,
|
||||
"reason": "outside_stabilization_window",
|
||||
"closeout": closeout,
|
||||
"overall_pass": True,
|
||||
"operator_signoff_needed": False,
|
||||
"base_url": str(params.get("base_url") or os.environ.get("CORE_HUB_BASE_URL") or _DEFAULT_BASE_URL),
|
||||
"window": {
|
||||
"start": window_start.isoformat() if window_start else None,
|
||||
"end": window_end.isoformat() if window_end else None,
|
||||
"within": within_window,
|
||||
},
|
||||
"checks": {},
|
||||
}
|
||||
|
||||
base_url = str(
|
||||
params.get("base_url")
|
||||
or os.environ.get("CORE_HUB_BASE_URL")
|
||||
or _DEFAULT_BASE_URL
|
||||
).rstrip("/")
|
||||
timeout_seconds = float(
|
||||
params.get("timeout_seconds", _DEFAULT_TIMEOUT_SECONDS)
|
||||
)
|
||||
min_widget_types = _bounded_int(
|
||||
params.get("min_widget_types", 26),
|
||||
default=26,
|
||||
minimum=1,
|
||||
maximum=200,
|
||||
)
|
||||
|
||||
checks: dict[str, Any] = {}
|
||||
with httpx.Client(
|
||||
base_url=base_url,
|
||||
timeout=timeout_seconds,
|
||||
headers={"Accept": "application/json", "User-Agent": "activity-core/core-hub-stabilization/0.1"},
|
||||
follow_redirects=True,
|
||||
) as client:
|
||||
for path, expected_status in _PUBLIC_CHECKS:
|
||||
key = path.strip("/").replace("/", "_").replace("-", "_") or "root"
|
||||
checks[key] = _probe_status(client, path, expected_status)
|
||||
checks["widget_types_count"] = _widget_type_count(client, min_widget_types)
|
||||
|
||||
overall_pass = all(section.get("pass") for section in checks.values())
|
||||
result = {
|
||||
"kind": "core_hub_stabilization",
|
||||
"skipped": False,
|
||||
"closeout": closeout,
|
||||
"overall_pass": overall_pass,
|
||||
"operator_signoff_needed": closeout and overall_pass,
|
||||
"base_url": base_url,
|
||||
"window": {
|
||||
"start": window_start.isoformat() if window_start else None,
|
||||
"end": window_end.isoformat() if window_end else None,
|
||||
"within": within_window,
|
||||
},
|
||||
"checks": checks,
|
||||
"source": str(params.get("source") or "activity-core"),
|
||||
}
|
||||
if closeout and not overall_pass:
|
||||
raise RuntimeError(f"core hub stabilization closeout failed: {result}")
|
||||
return result
|
||||
|
||||
|
||||
CONTEXT_RESOLVER_REGISTRY["core-hub"] = CoreHubContextResolver
|
||||
|
|
@ -123,7 +123,11 @@ def _post_state_hub_progress(
|
|||
"context_key": context_key,
|
||||
}
|
||||
|
||||
if probe_result.get("checks") is not None:
|
||||
if probe_result.get("kind") == "core_hub_stabilization":
|
||||
compact = probe_result
|
||||
summary = _core_hub_stabilization_summary_text(probe_result)
|
||||
source_type = "core-hub"
|
||||
elif probe_result.get("checks") is not None:
|
||||
compact = probe_result
|
||||
summary = _phase5_summary_text(probe_result)
|
||||
source_type = "state-hub"
|
||||
|
|
@ -503,6 +507,19 @@ def _compact_access_path(access_path: dict[str, Any]) -> dict[str, Any]:
|
|||
}
|
||||
|
||||
|
||||
def _core_hub_stabilization_summary_text(result: dict[str, Any]) -> str:
|
||||
checks = result.get("checks") or {}
|
||||
widget_types = checks.get("widget_types_count") or {}
|
||||
status = "pass" if result.get("overall_pass") else "fail"
|
||||
mode = "closeout" if result.get("closeout") else "daily"
|
||||
return (
|
||||
f"Core Hub stabilization {mode}: {status}; "
|
||||
f"base={result.get('base_url', '?')}; "
|
||||
f"widget_types={widget_types.get('count', '?')}/"
|
||||
f"{widget_types.get('minimum', '?')}"
|
||||
)
|
||||
|
||||
|
||||
def _phase5_summary_text(result: dict[str, Any]) -> str:
|
||||
checks = result.get("checks") or {}
|
||||
totals = checks.get("totals") or {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue