Use explicit fresh hosted capability snapshots for plan checks
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
parent
5501b8bb56
commit
2621cf2752
6 changed files with 353 additions and 11 deletions
|
|
@ -22,6 +22,7 @@ from reuse_surface.hub_sync import (
|
|||
)
|
||||
from reuse_surface.overlaps import find_overlaps
|
||||
from reuse_surface.statehub_bridge import list_open_capability_requests
|
||||
from reuse_surface.plan_snapshot import SnapshotUnavailable
|
||||
from reuse_surface.plan_check import (
|
||||
format_plan_check_json,
|
||||
format_plan_check_markdown,
|
||||
|
|
@ -661,13 +662,19 @@ def cmd_plan_check(args: argparse.Namespace) -> int:
|
|||
print("error: provide a workplan file or --intent", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
result = run_plan_check(
|
||||
query,
|
||||
reuse_threshold=args.reuse_threshold,
|
||||
extend_threshold=args.extend_threshold,
|
||||
use_llm=not args.no_llm,
|
||||
llm_url=args.llm_url,
|
||||
)
|
||||
try:
|
||||
result = run_plan_check(
|
||||
query,
|
||||
reuse_threshold=args.reuse_threshold,
|
||||
extend_threshold=args.extend_threshold,
|
||||
use_llm=not args.no_llm,
|
||||
llm_url=args.llm_url,
|
||||
hub_url=args.hub_url,
|
||||
max_index_age_hours=args.max_index_age_hours,
|
||||
)
|
||||
except SnapshotUnavailable as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
recorded = None
|
||||
if args.record_outcome:
|
||||
|
|
@ -941,6 +948,13 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"workplan", nargs="?", help="path to a workplan file (frontmatter + body)"
|
||||
)
|
||||
plan_check.add_argument("--intent", help="free-text intent instead of a workplan file")
|
||||
plan_check.add_argument(
|
||||
"--hub-url", help="read the hosted /v1/federated index; fail on unavailable, stale or warned composition",
|
||||
)
|
||||
plan_check.add_argument(
|
||||
"--max-index-age-hours", type=float, default=24,
|
||||
help="maximum hosted composition age in hours (default: 24); requires --hub-url",
|
||||
)
|
||||
plan_check.add_argument(
|
||||
"--format", choices=["markdown", "json"], default="markdown"
|
||||
)
|
||||
|
|
@ -1210,4 +1224,4 @@ def main(argv: list[str] | None = None) -> int:
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
raise SystemExit(main())
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ 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
|
||||
from reuse_surface.plan_snapshot import load_hosted_snapshot
|
||||
from reuse_surface.registry import ROOT, load_index
|
||||
from reuse_surface.statehub_bridge import file_capability_request
|
||||
|
||||
|
|
@ -260,8 +261,16 @@ def run_plan_check(
|
|||
top_n: int = 5,
|
||||
use_llm: bool = True,
|
||||
llm_url: str | None = None,
|
||||
hub_url: str | None = None,
|
||||
max_index_age_hours: float = 24,
|
||||
) -> dict[str, Any]:
|
||||
capabilities, updated = load_federated_capabilities()
|
||||
if hub_url is not None:
|
||||
capabilities, updated, provenance = load_hosted_snapshot(
|
||||
hub_url, max_age_hours=max_index_age_hours
|
||||
)
|
||||
else:
|
||||
capabilities, updated = load_federated_capabilities()
|
||||
provenance = {"kind": "local", "capability_count": len(capabilities)}
|
||||
matches = match_query(query, capabilities, tie_window=tie_window)
|
||||
top_score = matches[0].score if matches else 0.0
|
||||
verdict = verdict_for_score(
|
||||
|
|
@ -302,6 +311,7 @@ def run_plan_check(
|
|||
],
|
||||
"federated_index_updated": updated,
|
||||
"federated_index_stale_warning": _staleness_warning(updated),
|
||||
"index_source": provenance,
|
||||
}
|
||||
if notes:
|
||||
result["notes"] = notes
|
||||
|
|
@ -334,7 +344,13 @@ def format_plan_check_markdown(result: dict[str, Any]) -> str:
|
|||
elif verdict == "extend":
|
||||
lines.append("**Verdict: EXTEND** — scope overlaps closely enough that extending the top match is likely cheaper than a new capability.")
|
||||
else:
|
||||
lines.append("**Verdict: NEW** — no close match in the federated index; proceed.")
|
||||
lines.append("**Verdict: NEW** — no close lexical match in this index; review candidates and owner contracts before building.")
|
||||
|
||||
source = result.get("index_source", {})
|
||||
if source.get("kind") == "hosted":
|
||||
lines.extend(["", f"Source: {source['url']} ({source['capability_count']} capabilities)",
|
||||
f"Composed: {source['composed_at']}; fetched: {source['fetched_at']}",
|
||||
f"Snapshot SHA-256: `{source['sha256']}`"])
|
||||
|
||||
warning = result.get("federated_index_stale_warning")
|
||||
if warning:
|
||||
|
|
|
|||
109
reuse_surface/plan_snapshot.py
Normal file
109
reuse_surface/plan_snapshot.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
"""Read-only hosted discovery with explicit, bounded source acceptance."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import math
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
from typing import Any
|
||||
|
||||
MAX_SNAPSHOT_BYTES = 5 * 1024 * 1024
|
||||
|
||||
|
||||
class SnapshotUnavailable(ValueError):
|
||||
"""The selected source cannot support a planning recommendation."""
|
||||
|
||||
|
||||
def load_hosted_snapshot(
|
||||
base_url: str, *, max_age_hours: float = 24
|
||||
) -> tuple[list[dict[str, Any]], str, dict[str, Any]]:
|
||||
"""Fetch the public index; never send intent, tokens or mutation requests.
|
||||
|
||||
Acceptance concerns the hub's composition and warning signals. It does not
|
||||
certify that every upstream repository has published its latest capability.
|
||||
Failure must not become a local fallback or a 'new capability' verdict.
|
||||
"""
|
||||
url = base_url.rstrip("/") + "/v1/federated"
|
||||
try:
|
||||
parsed = urllib.parse.urlsplit(base_url)
|
||||
valid_url = (parsed.scheme in {"http", "https"} and parsed.hostname
|
||||
and not parsed.username and not parsed.password
|
||||
and not parsed.query and not parsed.fragment)
|
||||
except ValueError:
|
||||
valid_url = False
|
||||
if not valid_url:
|
||||
raise SnapshotUnavailable("hub URL must be HTTP(S), without credentials, query or fragment")
|
||||
if not math.isfinite(max_age_hours) or max_age_hours <= 0:
|
||||
raise SnapshotUnavailable("maximum index age must be a positive finite number of hours")
|
||||
|
||||
request = urllib.request.Request(
|
||||
url, headers={"Accept": "application/json", "User-Agent": "reuse-surface/0.1"}
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if response.status != 200:
|
||||
raise SnapshotUnavailable(f"hosted index returned HTTP {response.status}")
|
||||
if response.headers.get("X-Federation-Warnings"):
|
||||
raise SnapshotUnavailable("hosted index reports federation warnings; inspect its sources")
|
||||
raw = response.read(MAX_SNAPSHOT_BYTES + 1)
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise SnapshotUnavailable(f"hosted index returned HTTP {exc.code}") from exc
|
||||
except (urllib.error.URLError, OSError, ValueError) as exc:
|
||||
if isinstance(exc, SnapshotUnavailable):
|
||||
raise
|
||||
raise SnapshotUnavailable("hosted index could not be fetched; no local fallback used") from exc
|
||||
if len(raw) > MAX_SNAPSHOT_BYTES:
|
||||
raise SnapshotUnavailable("hosted index exceeds the 5 MiB response limit")
|
||||
try:
|
||||
payload = json.loads(raw)
|
||||
except (ValueError, UnicodeError) as exc:
|
||||
raise SnapshotUnavailable("hosted index is not valid JSON") from exc
|
||||
if not isinstance(payload, dict) or payload.get("stale") is not False:
|
||||
raise SnapshotUnavailable("hosted index is stale or its freshness signal is missing")
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
try:
|
||||
composed = datetime.fromisoformat(payload["composed_at"])
|
||||
updated = payload["updated"]
|
||||
date.fromisoformat(updated)
|
||||
if composed.tzinfo is None:
|
||||
raise ValueError("timezone missing")
|
||||
except (KeyError, TypeError, ValueError) as exc:
|
||||
raise SnapshotUnavailable("hosted index has invalid composition metadata") from exc
|
||||
age = (now - composed).total_seconds()
|
||||
if age < -timedelta(minutes=5).total_seconds() or age > max_age_hours * 3600:
|
||||
raise SnapshotUnavailable("hosted index composition is outside the accepted age window")
|
||||
|
||||
capabilities = payload.get("capabilities")
|
||||
if not isinstance(capabilities, list) or not capabilities:
|
||||
raise SnapshotUnavailable("hosted index has no usable capability collection")
|
||||
ids: set[str] = set()
|
||||
for item in capabilities:
|
||||
if (not isinstance(item, dict)
|
||||
or not isinstance(item.get("id"), str) or not item["id"].strip()
|
||||
or item["id"] in ids
|
||||
or not isinstance(item.get("name"), str)
|
||||
or not isinstance(item.get("summary"), str)
|
||||
or not isinstance(item.get("tags", []), list)
|
||||
or any(not isinstance(tag, str) for tag in item.get("tags", []))
|
||||
or any(item.get(key) is not None and not isinstance(item[key], str)
|
||||
for key in ("owner", "vector"))):
|
||||
raise SnapshotUnavailable("hosted index contains malformed or duplicate capabilities")
|
||||
ids.add(item["id"])
|
||||
|
||||
provenance = {
|
||||
"kind": "hosted",
|
||||
"url": url,
|
||||
"fetched_at": now.isoformat(),
|
||||
"composed_at": payload["composed_at"],
|
||||
"capability_count": len(capabilities),
|
||||
"sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"digest_format": "response-bytes",
|
||||
"max_age_hours": max_age_hours,
|
||||
"stale": False,
|
||||
"federation_warnings": False,
|
||||
}
|
||||
return capabilities, updated, provenance
|
||||
Loading…
Add table
Add a link
Reference in a new issue