REUSE-WP-0019-T04: reuse telemetry store and recording
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 10s
ci / validate-registry (push) Has been cancelled
Build and Publish Container Image / build-and-push (push) Successful in 1m16s

Implements the hub side of the shared reuse-event schema (already drafted
in WP-0018-T01, schemas/reuse-event.schema.json): a SQLite reuse_events
table, POST /v1/reuse-events (token-auth), GET /v1/reuse-events?capability_id=
(read-only).

reuse_surface/plan_check.py: refactored record_outcome around a new
shared post_or_fallback_reuse_event() helper -- tries the hub first, falls
back to the local JSONL only on failure/unreachability, never both. New
record_manual_reuse_event() backs a new CLI command, reuse-surface
record-reuse, for retroactive facts recorded outside plan-check.

Privacy/scope (repo slugs and capability ids only, no code, no secrets) is
enforced structurally via the schema's additionalProperties: false, not
just by convention.

21 new pytest cases, 145 total pass. Live-verified against a real running
hub instance: POST/GET /v1/reuse-events directly, record-reuse and
plan-check --record-outcome both posting successfully to the hub, and --
after actually killing the hub process -- confirmed the fallback path
writes correctly to the local JSONL instead of erroring.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-07 22:32:19 +02:00
parent 2fcc91f2aa
commit d181043717
12 changed files with 562 additions and 32 deletions

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import json
import re
import urllib.error
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
@ -11,6 +12,7 @@ import yaml
from jsonschema import Draft202012Validator
from reuse_surface import hub_client
from reuse_surface.federation import FEDERATED_INDEX_PATH
from reuse_surface.llm_bridge import execute_prompt, extract_json_object
from reuse_surface.overlaps import TOKEN_RE
@ -19,6 +21,7 @@ from reuse_surface.statehub_bridge import file_capability_request
TELEMETRY_PATH = ROOT / "registry" / "telemetry" / "plan-check-events.jsonl"
RERANK_SCHEMA_PATH = ROOT / "schemas" / "plan-check-rerank.schema.json"
REUSE_EVENT_SCHEMA_PATH = ROOT / "schemas" / "reuse-event.schema.json"
STALE_DAYS = 14
DEFAULT_REUSE_THRESHOLD = 0.45
@ -371,22 +374,87 @@ def maybe_file_capability_request(
)
def load_reuse_event_schema() -> dict[str, Any]:
return json.loads(REUSE_EVENT_SCHEMA_PATH.read_text(encoding="utf-8"))
def validate_reuse_event(event: dict[str, Any]) -> None:
validator = Draft202012Validator(load_reuse_event_schema())
errors = sorted(validator.iter_errors(event), key=lambda err: list(err.path))
if errors:
messages = "; ".join(error.message for error in errors[:3])
raise ValueError(f"reuse event schema validation failed: {messages}")
def append_local_reuse_event(event: dict[str, Any]) -> Path:
TELEMETRY_PATH.parent.mkdir(parents=True, exist_ok=True)
with TELEMETRY_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
return TELEMETRY_PATH
def post_or_fallback_reuse_event(
event: dict[str, Any], *, hub_url: str | None = None
) -> dict[str, Any]:
"""Records one reuse event: the shared schema (schemas/reuse-event.schema.json)
is the same whether the fact ends up in the hub's reuse_events table or the
local JSONL fallback. Tries POST /v1/reuse-events first; on any
misconfiguration or unreachability, falls back to the local file rather
than losing the fact -- never both, per the design ("+ local JSONL
fallback when hub unreachable", not a dual-write)."""
validate_reuse_event(event)
try:
status, _payload = hub_client.hub_record_reuse_event(event, hub_url)
except (ValueError, urllib.error.URLError, TimeoutError, OSError):
status = None
if status == 201:
return {"event": event, "recorded_to": "hub", "path": None}
path = append_local_reuse_event(event)
return {"event": event, "recorded_to": "local", "path": str(path)}
def record_outcome(
result: dict[str, Any],
outcome: str,
*,
consumer_repo: str = "reuse-surface",
) -> Path:
TELEMETRY_PATH.parent.mkdir(parents=True, exist_ok=True)
capability_id: str | None = None,
verdict: str | None = None,
source: str = "plan-check",
hub_url: str | None = None,
) -> dict[str, Any]:
top_match = result["matches"][0]["id"] if result.get("matches") else None
event = {
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"consumer_repo": consumer_repo,
"capability_id": top_match,
"verdict": result["verdict"],
"capability_id": capability_id if capability_id is not None else top_match,
"verdict": verdict if verdict is not None else result["verdict"],
"outcome": outcome,
"source": "plan-check",
"source": source,
}
with TELEMETRY_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
return TELEMETRY_PATH
return post_or_fallback_reuse_event(event, hub_url=hub_url)
def record_manual_reuse_event(
*,
consumer_repo: str,
capability_id: str | None,
verdict: str,
outcome: str | None = None,
hub_url: str | None = None,
) -> dict[str, Any]:
"""Retroactive manual fact recording (`reuse-surface record-reuse`) --
for reuse decisions made outside plan-check, e.g. discovered after the
fact during a review."""
event = {
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"consumer_repo": consumer_repo,
"capability_id": capability_id,
"verdict": verdict,
"outcome": outcome,
"source": "manual",
}
return post_or_fallback_reuse_event(event, hub_url=hub_url)