REUSE-WP-0019-T04: reuse telemetry store and recording
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:
parent
2fcc91f2aa
commit
d181043717
12 changed files with 562 additions and 32 deletions
7
SCOPE.md
7
SCOPE.md
|
|
@ -80,11 +80,16 @@ The MVP registry foundation, CLI tooling (REUSE-WP-0003), federation stack
|
||||||
get a reuse/extend/new verdict; `--file-request` bridges a `new` verdict to
|
get a reuse/extend/new verdict; `--file-request` bridges a `new` verdict to
|
||||||
a State Hub capability request; `report gaps --check-capability-requests`
|
a State Hub capability request; `report gaps --check-capability-requests`
|
||||||
surfaces open requests with no matching capability
|
surfaces open requests with no matching capability
|
||||||
- **Get automatic hub refresh** (REUSE-WP-0019-T02) — a Forgejo push
|
- **Get automatic hub refresh** (REUSE-WP-0019-T02/T03) — a Forgejo push
|
||||||
webhook (`POST /v1/webhooks/forgejo`, HMAC-signed) recomposes the hub's
|
webhook (`POST /v1/webhooks/forgejo`, HMAC-signed) recomposes the hub's
|
||||||
federated index when a registered repo's `registry/indexes/` changes,
|
federated index when a registered repo's `registry/indexes/` changes,
|
||||||
with `composed_at`/`stale` visibility on `GET /v1/federated` and a
|
with `composed_at`/`stale` visibility on `GET /v1/federated` and a
|
||||||
scheduled fallback recompose if a webhook delivery is ever missed
|
scheduled fallback recompose if a webhook delivery is ever missed
|
||||||
|
- **Record reuse telemetry** (REUSE-WP-0019-T04) — `plan-check
|
||||||
|
--record-outcome` and `reuse-surface record-reuse` post facts to
|
||||||
|
`POST /v1/reuse-events` when the hub is reachable, falling back to
|
||||||
|
`registry/telemetry/plan-check-events.jsonl` otherwise (same schema
|
||||||
|
either way); `GET /v1/reuse-events?capability_id=` aggregates them
|
||||||
|
|
||||||
Registry **tooling** availability is **A4** (CLI plus hosted hub HTTP API).
|
Registry **tooling** availability is **A4** (CLI plus hosted hub HTTP API).
|
||||||
Registry **authoring** remains Markdown-first; consumption combines entries, the
|
Registry **authoring** remains Markdown-first; consumption combines entries, the
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ from reuse_surface.plan_check import (
|
||||||
load_query_from_intent,
|
load_query_from_intent,
|
||||||
load_query_from_workplan,
|
load_query_from_workplan,
|
||||||
maybe_file_capability_request,
|
maybe_file_capability_request,
|
||||||
|
record_manual_reuse_event,
|
||||||
record_outcome,
|
record_outcome,
|
||||||
run_plan_check,
|
run_plan_check,
|
||||||
)
|
)
|
||||||
|
|
@ -663,8 +664,9 @@ def cmd_plan_check(args: argparse.Namespace) -> int:
|
||||||
llm_url=args.llm_url,
|
llm_url=args.llm_url,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
recorded = None
|
||||||
if args.record_outcome:
|
if args.record_outcome:
|
||||||
record_outcome(result, args.record_outcome, consumer_repo=args.consumer_repo)
|
recorded = record_outcome(result, args.record_outcome, consumer_repo=args.consumer_repo)
|
||||||
|
|
||||||
filed = None
|
filed = None
|
||||||
if args.file_request and result["verdict"] == "new":
|
if args.file_request and result["verdict"] == "new":
|
||||||
|
|
@ -677,6 +679,8 @@ def cmd_plan_check(args: argparse.Namespace) -> int:
|
||||||
if args.format == "json":
|
if args.format == "json":
|
||||||
if filed is not None:
|
if filed is not None:
|
||||||
result["filed_capability_request"] = filed
|
result["filed_capability_request"] = filed
|
||||||
|
if recorded is not None:
|
||||||
|
result["recorded_reuse_event"] = recorded
|
||||||
print(format_plan_check_json(result))
|
print(format_plan_check_json(result))
|
||||||
else:
|
else:
|
||||||
print(format_plan_check_markdown(result), end="")
|
print(format_plan_check_markdown(result), end="")
|
||||||
|
|
@ -685,6 +689,32 @@ def cmd_plan_check(args: argparse.Namespace) -> int:
|
||||||
print(f"\nFiled State Hub capability request: {filed.get('id')}")
|
print(f"\nFiled State Hub capability request: {filed.get('id')}")
|
||||||
else:
|
else:
|
||||||
print("\nState Hub unreachable — capability request not filed.")
|
print("\nState Hub unreachable — capability request not filed.")
|
||||||
|
if recorded is not None:
|
||||||
|
if recorded["recorded_to"] == "hub":
|
||||||
|
print("\nReuse event recorded to hub.")
|
||||||
|
else:
|
||||||
|
print("\nReuse hub unreachable — recorded to local JSONL fallback.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_record_reuse(args: argparse.Namespace) -> int:
|
||||||
|
try:
|
||||||
|
recorded = record_manual_reuse_event(
|
||||||
|
consumer_repo=args.consumer_repo,
|
||||||
|
capability_id=args.capability_id,
|
||||||
|
verdict=args.verdict,
|
||||||
|
outcome=args.outcome,
|
||||||
|
hub_url=args.hub_url,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if args.format == "json":
|
||||||
|
print(json.dumps(recorded, indent=2, sort_keys=True))
|
||||||
|
else:
|
||||||
|
destination = "hub" if recorded["recorded_to"] == "hub" else f"local ({recorded['path']})"
|
||||||
|
print(f"ok: recorded reuse event to {destination}")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -880,7 +910,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
plan_check.add_argument(
|
plan_check.add_argument(
|
||||||
"--record-outcome",
|
"--record-outcome",
|
||||||
choices=["reused", "extended", "new", "skipped"],
|
choices=["reused", "extended", "new", "skipped"],
|
||||||
help="append this outcome to registry/telemetry/plan-check-events.jsonl",
|
help="record this outcome as a reuse event: POST /v1/reuse-events if the "
|
||||||
|
"hub is reachable, else append to registry/telemetry/plan-check-events.jsonl",
|
||||||
)
|
)
|
||||||
plan_check.add_argument(
|
plan_check.add_argument(
|
||||||
"--consumer-repo", default="reuse-surface",
|
"--consumer-repo", default="reuse-surface",
|
||||||
|
|
@ -903,6 +934,26 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
)
|
)
|
||||||
plan_check.set_defaults(func=cmd_plan_check)
|
plan_check.set_defaults(func=cmd_plan_check)
|
||||||
|
|
||||||
|
record_reuse = subparsers.add_parser(
|
||||||
|
"record-reuse",
|
||||||
|
help="manually record a retroactive reuse fact (REUSE-WP-0019-T04)",
|
||||||
|
)
|
||||||
|
record_reuse.add_argument("--consumer-repo", required=True, help="repo slug that reused/extended/built new")
|
||||||
|
record_reuse.add_argument(
|
||||||
|
"--capability-id", help="capability id involved, if any (omit for a pure 'new' fact)"
|
||||||
|
)
|
||||||
|
record_reuse.add_argument(
|
||||||
|
"--verdict", required=True, choices=["reuse", "extend", "new"],
|
||||||
|
)
|
||||||
|
record_reuse.add_argument(
|
||||||
|
"--outcome", choices=["reused", "extended", "new", "skipped"],
|
||||||
|
)
|
||||||
|
record_reuse.add_argument(
|
||||||
|
"--hub-url", help="hub base URL (or REUSE_SURFACE_URL)",
|
||||||
|
)
|
||||||
|
record_reuse.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
||||||
|
record_reuse.set_defaults(func=cmd_record_reuse)
|
||||||
|
|
||||||
catalog = subparsers.add_parser(
|
catalog = subparsers.add_parser(
|
||||||
"catalog", help="generate human-readable capability catalog"
|
"catalog", help="generate human-readable capability catalog"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -200,6 +200,21 @@ def create_app() -> FastAPI:
|
||||||
|
|
||||||
return JSONResponse(content={"accepted": True, "composed_at": composed_at})
|
return JSONResponse(content={"accepted": True, "composed_at": composed_at})
|
||||||
|
|
||||||
|
@app.post("/v1/reuse-events", status_code=201, dependencies=[Depends(_require_auth)])
|
||||||
|
async def record_reuse_event(request: Request) -> dict[str, Any]:
|
||||||
|
payload = await request.json()
|
||||||
|
try:
|
||||||
|
return store.record_reuse_event(payload)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise _http_error(400, "validation_error", str(exc)) from exc
|
||||||
|
|
||||||
|
@app.get("/v1/reuse-events")
|
||||||
|
def list_reuse_events(
|
||||||
|
capability_id: str | None = Query(default=None),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
events = store.list_reuse_events(capability_id)
|
||||||
|
return {"count": len(events), "events": events}
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,11 @@ import yaml
|
||||||
from jsonschema import Draft202012Validator
|
from jsonschema import Draft202012Validator
|
||||||
|
|
||||||
SCHEMA_PATH = Path(__file__).resolve().parent / "hub-registration.schema.yaml"
|
SCHEMA_PATH = Path(__file__).resolve().parent / "hub-registration.schema.yaml"
|
||||||
|
REUSE_EVENT_SCHEMA_PATH = Path(__file__).resolve().parent.parent.parent / "schemas" / "reuse-event.schema.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_reuse_event_schema() -> dict[str, Any]:
|
||||||
|
return json.loads(REUSE_EVENT_SCHEMA_PATH.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
def _utc_now() -> str:
|
def _utc_now() -> str:
|
||||||
|
|
@ -78,6 +83,67 @@ class HubStore:
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO compose_state (id, composed_at, stale) VALUES (1, NULL, 0)"
|
"INSERT OR IGNORE INTO compose_state (id, composed_at, stale) VALUES (1, NULL, 0)"
|
||||||
)
|
)
|
||||||
|
# Reuse telemetry (REUSE-WP-0019-T04): append-only facts, never
|
||||||
|
# hand-edited. Schema shared with the local JSONL fallback in
|
||||||
|
# plan_check.py's record_outcome() -- see schemas/reuse-event.schema.json.
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS reuse_events (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
ts TEXT NOT NULL,
|
||||||
|
consumer_repo TEXT NOT NULL,
|
||||||
|
capability_id TEXT,
|
||||||
|
verdict TEXT NOT NULL,
|
||||||
|
outcome TEXT,
|
||||||
|
source TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def record_reuse_event(self, payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
errors = [
|
||||||
|
error.message
|
||||||
|
for error in Draft202012Validator(_load_reuse_event_schema()).iter_errors(payload)
|
||||||
|
]
|
||||||
|
if errors:
|
||||||
|
raise ValueError("; ".join(errors))
|
||||||
|
with self._connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO reuse_events (ts, consumer_repo, capability_id, verdict, outcome, source)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
payload["ts"],
|
||||||
|
payload["consumer_repo"],
|
||||||
|
payload.get("capability_id"),
|
||||||
|
payload["verdict"],
|
||||||
|
payload.get("outcome"),
|
||||||
|
payload["source"],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def list_reuse_events(self, capability_id: str | None = None) -> list[dict[str, Any]]:
|
||||||
|
query = "SELECT ts, consumer_repo, capability_id, verdict, outcome, source FROM reuse_events"
|
||||||
|
params: tuple[Any, ...] = ()
|
||||||
|
if capability_id:
|
||||||
|
query += " WHERE capability_id = ?"
|
||||||
|
params = (capability_id,)
|
||||||
|
query += " ORDER BY ts"
|
||||||
|
with self._connect() as conn:
|
||||||
|
rows = conn.execute(query, params).fetchall()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"ts": row["ts"],
|
||||||
|
"consumer_repo": row["consumer_repo"],
|
||||||
|
"capability_id": row["capability_id"],
|
||||||
|
"verdict": row["verdict"],
|
||||||
|
"outcome": row["outcome"],
|
||||||
|
"source": row["source"],
|
||||||
|
}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
def list_repos(self) -> list[dict[str, Any]]:
|
def list_repos(self) -> list[dict[str, Any]]:
|
||||||
with self._connect() as conn:
|
with self._connect() as conn:
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,27 @@ def hub_update(
|
||||||
f"{service_base_url(base_url)}/v1/repos/{repo}",
|
f"{service_base_url(base_url)}/v1/repos/{repo}",
|
||||||
token=token,
|
token=token,
|
||||||
body=payload,
|
body=payload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def hub_record_reuse_event(
|
||||||
|
payload: dict[str, Any], base_url: str | None = None
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
token = service_token()
|
||||||
|
if not token:
|
||||||
|
raise ValueError("REUSE_SURFACE_TOKEN is required for record-reuse")
|
||||||
|
return _request(
|
||||||
|
"POST",
|
||||||
|
f"{service_base_url(base_url)}/v1/reuse-events",
|
||||||
|
token=token,
|
||||||
|
body=payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def hub_list_reuse_events(
|
||||||
|
capability_id: str | None = None, base_url: str | None = None
|
||||||
|
) -> tuple[int, Any]:
|
||||||
|
url = f"{service_base_url(base_url)}/v1/reuse-events"
|
||||||
|
if capability_id:
|
||||||
|
url += f"?capability_id={capability_id}"
|
||||||
|
return _request("GET", url)
|
||||||
|
|
@ -2,6 +2,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
import urllib.error
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
@ -11,6 +12,7 @@ import yaml
|
||||||
|
|
||||||
from jsonschema import Draft202012Validator
|
from jsonschema import Draft202012Validator
|
||||||
|
|
||||||
|
from reuse_surface import hub_client
|
||||||
from reuse_surface.federation import FEDERATED_INDEX_PATH
|
from reuse_surface.federation import FEDERATED_INDEX_PATH
|
||||||
from reuse_surface.llm_bridge import execute_prompt, extract_json_object
|
from reuse_surface.llm_bridge import execute_prompt, extract_json_object
|
||||||
from reuse_surface.overlaps import TOKEN_RE
|
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"
|
TELEMETRY_PATH = ROOT / "registry" / "telemetry" / "plan-check-events.jsonl"
|
||||||
RERANK_SCHEMA_PATH = ROOT / "schemas" / "plan-check-rerank.schema.json"
|
RERANK_SCHEMA_PATH = ROOT / "schemas" / "plan-check-rerank.schema.json"
|
||||||
|
REUSE_EVENT_SCHEMA_PATH = ROOT / "schemas" / "reuse-event.schema.json"
|
||||||
|
|
||||||
STALE_DAYS = 14
|
STALE_DAYS = 14
|
||||||
DEFAULT_REUSE_THRESHOLD = 0.45
|
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(
|
def record_outcome(
|
||||||
result: dict[str, Any],
|
result: dict[str, Any],
|
||||||
outcome: str,
|
outcome: str,
|
||||||
*,
|
*,
|
||||||
consumer_repo: str = "reuse-surface",
|
consumer_repo: str = "reuse-surface",
|
||||||
) -> Path:
|
capability_id: str | None = None,
|
||||||
TELEMETRY_PATH.parent.mkdir(parents=True, exist_ok=True)
|
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
|
top_match = result["matches"][0]["id"] if result.get("matches") else None
|
||||||
event = {
|
event = {
|
||||||
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
"ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
"consumer_repo": consumer_repo,
|
"consumer_repo": consumer_repo,
|
||||||
"capability_id": top_match,
|
"capability_id": capability_id if capability_id is not None else top_match,
|
||||||
"verdict": result["verdict"],
|
"verdict": verdict if verdict is not None else result["verdict"],
|
||||||
"outcome": outcome,
|
"outcome": outcome,
|
||||||
"source": "plan-check",
|
"source": source,
|
||||||
}
|
}
|
||||||
with TELEMETRY_PATH.open("a", encoding="utf-8") as handle:
|
return post_or_fallback_reuse_event(event, hub_url=hub_url)
|
||||||
handle.write(json.dumps(event, sort_keys=True) + "\n")
|
|
||||||
return TELEMETRY_PATH
|
|
||||||
|
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)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {"type": "string"}
|
"items": {"type": "string"}
|
||||||
},
|
},
|
||||||
"filed_capability_request": {"type": ["object", "null"]}
|
"filed_capability_request": {"type": ["object", "null"]},
|
||||||
|
"recorded_reuse_event": {"type": ["object", "null"]}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -267,10 +267,51 @@ unconfigured secret is `503` (fails closed, not open).
|
||||||
**Response `502`:** Recompose failed (stale is deliberately left set so the
|
**Response `502`:** Recompose failed (stale is deliberately left set so the
|
||||||
next check reports the failure honestly, not silently).
|
next check reports the failure honestly, not silently).
|
||||||
|
|
||||||
Org-level webhook rollout (single config covering all repos, T03) and the
|
Org-level webhook rollout (single config covering all repos) and the
|
||||||
Forgejo Actions scheduled fallback for when webhooks are unavailable are
|
Forgejo Actions scheduled fallback both shipped in T03: the org webhook is
|
||||||
separate, deployment-side follow-ups — this section covers the receiver
|
live on `coulomb` (id `1`, push events only), and
|
||||||
contract only.
|
`.forgejo/workflows/recompose-fallback.yaml` in this repo calls
|
||||||
|
`POST /v1/federated/compose` on a 6-hour cron as a backstop.
|
||||||
|
|
||||||
|
### 5.10 `POST /v1/reuse-events`
|
||||||
|
|
||||||
|
Records one reuse telemetry fact (REUSE-WP-0019-T04). **Auth required.**
|
||||||
|
Body validated against `schemas/reuse-event.schema.json` — the same schema
|
||||||
|
`plan_check.py`'s local JSONL fallback uses, so a fact never differs in
|
||||||
|
shape depending on where it landed.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ts": "2026-07-08T00:00:00Z",
|
||||||
|
"consumer_repo": "some-repo",
|
||||||
|
"capability_id": "capability.infotech.issue-tracking",
|
||||||
|
"verdict": "reuse",
|
||||||
|
"outcome": "reused",
|
||||||
|
"source": "plan-check"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`capability_id`/`outcome` may be `null` (e.g. a `new` verdict with no
|
||||||
|
existing capability). `source` is one of `plan-check`, `manual`, `hub`.
|
||||||
|
`additionalProperties: false` — no code, no secrets, repo slugs and
|
||||||
|
capability ids only (privacy/scope constraint).
|
||||||
|
|
||||||
|
**Response `201`:** the recorded event.
|
||||||
|
**Response `400`:** schema validation failed.
|
||||||
|
**Response `401`:** missing/invalid bearer token.
|
||||||
|
|
||||||
|
### 5.11 `GET /v1/reuse-events`
|
||||||
|
|
||||||
|
Lists recorded reuse events, oldest first. No auth required (read-only,
|
||||||
|
same posture as `GET /v1/federated`).
|
||||||
|
|
||||||
|
Query parameters:
|
||||||
|
|
||||||
|
| Param | Default | Meaning |
|
||||||
|
|---|---|---|
|
||||||
|
| `capability_id` | (none) | Filter to one capability |
|
||||||
|
|
||||||
|
**Response `200`:** `{"count": N, "events": [...]}`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -311,4 +311,86 @@ def test_webhook_accepts_gitea_signature_header(webhook_client):
|
||||||
content=body,
|
content=body,
|
||||||
headers={"X-Gitea-Signature": _sign(body), "Content-Type": "application/json"},
|
headers={"X-Gitea-Signature": _sign(body), "Content-Type": "application/json"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
# --- T04: reuse telemetry store ---
|
||||||
|
|
||||||
|
VALID_REUSE_EVENT = {
|
||||||
|
"ts": "2026-07-08T00:00:00Z",
|
||||||
|
"consumer_repo": "some-repo",
|
||||||
|
"capability_id": "capability.infotech.issue-tracking",
|
||||||
|
"verdict": "reuse",
|
||||||
|
"outcome": "reused",
|
||||||
|
"source": "plan-check",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_reuse_event_requires_auth(hub_client):
|
||||||
|
response = hub_client.post("/v1/reuse-events", json=VALID_REUSE_EVENT)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_reuse_event_and_list(hub_client):
|
||||||
|
response = hub_client.post(
|
||||||
|
"/v1/reuse-events",
|
||||||
|
json=VALID_REUSE_EVENT,
|
||||||
|
headers={"Authorization": "Bearer test-token"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
listed = hub_client.get("/v1/reuse-events")
|
||||||
|
assert listed.status_code == 200
|
||||||
|
assert listed.json()["count"] == 1
|
||||||
|
assert listed.json()["events"][0]["consumer_repo"] == "some-repo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_reuse_event_rejects_invalid_verdict(hub_client):
|
||||||
|
bad = {**VALID_REUSE_EVENT, "verdict": "not-a-verdict"}
|
||||||
|
response = hub_client.post(
|
||||||
|
"/v1/reuse-events",
|
||||||
|
json=bad,
|
||||||
|
headers={"Authorization": "Bearer test-token"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_reuse_event_rejects_extra_fields(hub_client):
|
||||||
|
bad = {**VALID_REUSE_EVENT, "unexpected_field": "nope"}
|
||||||
|
response = hub_client.post(
|
||||||
|
"/v1/reuse-events",
|
||||||
|
json=bad,
|
||||||
|
headers={"Authorization": "Bearer test-token"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_reuse_events_filters_by_capability_id(hub_client):
|
||||||
|
other = {**VALID_REUSE_EVENT, "capability_id": "capability.infotech.other"}
|
||||||
|
hub_client.post("/v1/reuse-events", json=VALID_REUSE_EVENT, headers={"Authorization": "Bearer test-token"})
|
||||||
|
hub_client.post("/v1/reuse-events", json=other, headers={"Authorization": "Bearer test-token"})
|
||||||
|
|
||||||
|
listed = hub_client.get("/v1/reuse-events?capability_id=capability.infotech.other")
|
||||||
|
assert listed.status_code == 200
|
||||||
|
assert listed.json()["count"] == 1
|
||||||
|
assert listed.json()["events"][0]["capability_id"] == "capability.infotech.other"
|
||||||
|
|
||||||
|
|
||||||
|
def test_list_reuse_events_empty_by_default(hub_client):
|
||||||
|
listed = hub_client.get("/v1/reuse-events")
|
||||||
|
assert listed.status_code == 200
|
||||||
|
assert listed.json() == {"count": 0, "events": []}
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_record_reuse_event_and_list(tmp_path):
|
||||||
|
store = HubStore(tmp_path / "hub.db")
|
||||||
|
store.record_reuse_event(VALID_REUSE_EVENT)
|
||||||
|
events = store.list_reuse_events()
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0]["consumer_repo"] == "some-repo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_store_record_reuse_event_rejects_invalid(tmp_path):
|
||||||
|
store = HubStore(tmp_path / "hub.db")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
store.record_reuse_event({**VALID_REUSE_EVENT, "verdict": "nope"})
|
||||||
|
|
@ -10,6 +10,7 @@ from reuse_surface.plan_check import (
|
||||||
load_query_from_workplan,
|
load_query_from_workplan,
|
||||||
maybe_file_capability_request,
|
maybe_file_capability_request,
|
||||||
match_query,
|
match_query,
|
||||||
|
record_manual_reuse_event,
|
||||||
record_outcome,
|
record_outcome,
|
||||||
request_rerank,
|
request_rerank,
|
||||||
run_plan_check,
|
run_plan_check,
|
||||||
|
|
@ -133,6 +134,83 @@ def test_record_outcome_appends_jsonl(tmp_path, monkeypatch):
|
||||||
assert event["source"] == "plan-check"
|
assert event["source"] == "plan-check"
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_outcome_posts_to_hub_when_reachable(tmp_path, monkeypatch):
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"reuse_surface.hub_client.hub_record_reuse_event",
|
||||||
|
lambda event, base_url=None: (201, event),
|
||||||
|
)
|
||||||
|
result = {"verdict": "reuse", "matches": [{"id": "capability.infotech.issue-tracking"}]}
|
||||||
|
recorded = record_outcome(result, "reused", consumer_repo="some-repo")
|
||||||
|
assert recorded["recorded_to"] == "hub"
|
||||||
|
assert not telemetry_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_outcome_falls_back_when_hub_rejects(tmp_path, monkeypatch):
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"reuse_surface.hub_client.hub_record_reuse_event",
|
||||||
|
lambda event, base_url=None: (500, {"error": "boom"}),
|
||||||
|
)
|
||||||
|
result = {"verdict": "reuse", "matches": [{"id": "capability.infotech.issue-tracking"}]}
|
||||||
|
recorded = record_outcome(result, "reused", consumer_repo="some-repo")
|
||||||
|
assert recorded["recorded_to"] == "local"
|
||||||
|
assert telemetry_path.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_outcome_falls_back_when_hub_unconfigured(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
result = {"verdict": "reuse", "matches": [{"id": "capability.infotech.issue-tracking"}]}
|
||||||
|
recorded = record_outcome(result, "reused", consumer_repo="some-repo")
|
||||||
|
assert recorded["recorded_to"] == "local"
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_manual_reuse_event_local_fallback(tmp_path, monkeypatch):
|
||||||
|
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
recorded = record_manual_reuse_event(
|
||||||
|
consumer_repo="some-repo",
|
||||||
|
capability_id="capability.infotech.issue-tracking",
|
||||||
|
verdict="reuse",
|
||||||
|
outcome="reused",
|
||||||
|
)
|
||||||
|
assert recorded["recorded_to"] == "local"
|
||||||
|
event = json.loads(telemetry_path.read_text().splitlines()[0])
|
||||||
|
assert event["source"] == "manual"
|
||||||
|
assert event["consumer_repo"] == "some-repo"
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_manual_reuse_event_posts_to_hub(tmp_path, monkeypatch):
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
"reuse_surface.hub_client.hub_record_reuse_event",
|
||||||
|
lambda event, base_url=None: (201, event),
|
||||||
|
)
|
||||||
|
recorded = record_manual_reuse_event(
|
||||||
|
consumer_repo="some-repo", capability_id=None, verdict="new", outcome="new",
|
||||||
|
)
|
||||||
|
assert recorded["recorded_to"] == "hub"
|
||||||
|
assert recorded["event"]["source"] == "manual"
|
||||||
|
assert recorded["event"]["capability_id"] is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_record_manual_reuse_event_rejects_invalid_verdict(monkeypatch):
|
||||||
|
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||||
|
try:
|
||||||
|
record_manual_reuse_event(
|
||||||
|
consumer_repo="some-repo", capability_id=None, verdict="not-a-verdict",
|
||||||
|
)
|
||||||
|
assert False, "expected ValueError"
|
||||||
|
except ValueError as exc:
|
||||||
|
assert "schema validation failed" in str(exc)
|
||||||
|
|
||||||
|
|
||||||
def test_maybe_file_capability_request_only_on_new_verdict(monkeypatch):
|
def test_maybe_file_capability_request_only_on_new_verdict(monkeypatch):
|
||||||
called = []
|
called = []
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|
@ -349,3 +427,59 @@ def test_run_plan_check_integrates_successful_rerank(monkeypatch):
|
||||||
assert llm_entries[0]["score"] == 0.77
|
assert llm_entries[0]["score"] == 0.77
|
||||||
# deterministic top match is still first and unaffected
|
# deterministic top match is still first and unaffected
|
||||||
assert result["matches"][0]["kind"] == "deterministic"
|
assert result["matches"][0]["kind"] == "deterministic"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_record_reuse_cli_local_fallback(tmp_path, monkeypatch, capsys):
|
||||||
|
from reuse_surface.cli import main
|
||||||
|
|
||||||
|
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
|
||||||
|
exit_code = main([
|
||||||
|
"record-reuse",
|
||||||
|
"--consumer-repo", "some-repo",
|
||||||
|
"--capability-id", "capability.infotech.issue-tracking",
|
||||||
|
"--verdict", "reuse",
|
||||||
|
"--outcome", "reused",
|
||||||
|
])
|
||||||
|
assert exit_code == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "local" in out
|
||||||
|
event = json.loads(telemetry_path.read_text().splitlines()[0])
|
||||||
|
assert event["consumer_repo"] == "some-repo"
|
||||||
|
assert event["source"] == "manual"
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_record_reuse_cli_rejects_invalid_verdict():
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from reuse_surface.cli import main
|
||||||
|
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
main([
|
||||||
|
"record-reuse",
|
||||||
|
"--consumer-repo", "some-repo",
|
||||||
|
"--verdict", "not-a-verdict",
|
||||||
|
])
|
||||||
|
assert exc_info.value.code == 2 # argparse choices rejection
|
||||||
|
|
||||||
|
|
||||||
|
def test_cmd_record_reuse_cli_json_format(tmp_path, monkeypatch, capsys):
|
||||||
|
from reuse_surface.cli import main
|
||||||
|
|
||||||
|
monkeypatch.delenv("REUSE_SURFACE_URL", raising=False)
|
||||||
|
telemetry_path = tmp_path / "plan-check-events.jsonl"
|
||||||
|
monkeypatch.setattr("reuse_surface.plan_check.TELEMETRY_PATH", telemetry_path)
|
||||||
|
|
||||||
|
exit_code = main([
|
||||||
|
"record-reuse",
|
||||||
|
"--consumer-repo", "some-repo",
|
||||||
|
"--verdict", "new",
|
||||||
|
"--format", "json",
|
||||||
|
])
|
||||||
|
assert exit_code == 0
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
payload = json.loads(out)
|
||||||
|
assert payload["recorded_to"] == "local"
|
||||||
|
assert payload["event"]["capability_id"] is None
|
||||||
|
|
|
||||||
|
|
@ -70,11 +70,14 @@ reuse-surface plan-check --intent "..." --llm-url http://127.0.0.1:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
`reuse|extend|new` verdict from `--reuse-threshold`/`--extend-threshold`
|
`reuse|extend|new` verdict from `--reuse-threshold`/`--extend-threshold`
|
||||||
(defaults 0.45/0.22). `--record-outcome` appends to
|
(defaults 0.45/0.22). `--record-outcome` records a reuse event
|
||||||
`registry/telemetry/plan-check-events.jsonl` (schema shared with
|
(REUSE-WP-0019-T04): `POST /v1/reuse-events` if the hub is reachable
|
||||||
REUSE-WP-0019's reuse telemetry). `--file-request` files a State Hub
|
(`REUSE_SURFACE_URL`/`REUSE_SURFACE_TOKEN` set), else appends to
|
||||||
capability request on a `new` verdict (requires the hub reachable at
|
`registry/telemetry/plan-check-events.jsonl` — never both, and the schema
|
||||||
`127.0.0.1:8000`; degrades gracefully offline).
|
(`schemas/reuse-event.schema.json`) is identical either way.
|
||||||
|
`--file-request` files a State Hub capability request on a `new` verdict
|
||||||
|
(requires the hub reachable at `127.0.0.1:8000`; degrades gracefully
|
||||||
|
offline).
|
||||||
|
|
||||||
When `LLM_CONNECT_URL` is set, an optional rerank pass sends the top
|
When `LLM_CONNECT_URL` is set, an optional rerank pass sends the top
|
||||||
deterministic candidates to llm-connect for a semantic confidence score.
|
deterministic candidates to llm-connect for a semantic confidence score.
|
||||||
|
|
@ -84,6 +87,19 @@ so the trusted base result is identical whether or not the rerank runs.
|
||||||
Malformed/non-JSON LLM responses are rejected and reported as a note, never
|
Malformed/non-JSON LLM responses are rejected and reported as a note, never
|
||||||
silently guessed at; missing `LLM_CONNECT_URL` degrades the same way.
|
silently guessed at; missing `LLM_CONNECT_URL` degrades the same way.
|
||||||
|
|
||||||
|
### record-reuse
|
||||||
|
|
||||||
|
Manually record a retroactive reuse fact (REUSE-WP-0019-T04) — for
|
||||||
|
decisions made outside `plan-check`, e.g. discovered during a review.
|
||||||
|
Same hub-first-then-local-fallback behavior as `plan-check --record-outcome`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
reuse-surface record-reuse --consumer-repo some-repo \
|
||||||
|
--capability-id capability.infotech.issue-tracking \
|
||||||
|
--verdict reuse --outcome reused
|
||||||
|
reuse-surface record-reuse --consumer-repo some-repo --verdict new --format json
|
||||||
|
```
|
||||||
|
|
||||||
### catalog
|
### catalog
|
||||||
|
|
||||||
Generate human-readable catalog artifacts (UC-RS-018).
|
Generate human-readable catalog artifacts (UC-RS-018).
|
||||||
|
|
@ -262,6 +278,8 @@ Stable IDs and maturity fields are preserved for agent consumption (UC-RS-019).
|
||||||
| Interactive registry maintain | `reuse-surface maintain` |
|
| Interactive registry maintain | `reuse-surface maintain` |
|
||||||
| Planning cohort export | `reuse-surface report cohorts` |
|
| Planning cohort export | `reuse-surface report cohorts` |
|
||||||
| Relation graph | `reuse-surface graph` |
|
| Relation graph | `reuse-surface graph` |
|
||||||
|
| Query before building | `reuse-surface plan-check --intent "..."` |
|
||||||
|
| Record a reuse fact retroactively | `reuse-surface record-reuse` |
|
||||||
|
|
||||||
## Related use cases
|
## Related use cases
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -267,18 +267,44 @@ whenever a future deploy wants to switch registries.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: REUSE-WP-0019-T04
|
id: REUSE-WP-0019-T04
|
||||||
status: todo
|
status: done
|
||||||
priority: medium
|
priority: medium
|
||||||
state_hub_task_id: "c8e9064e-5c39-4c84-80e9-8b255f8edaec"
|
state_hub_task_id: "c8e9064e-5c39-4c84-80e9-8b255f8edaec"
|
||||||
```
|
```
|
||||||
|
|
||||||
- Implement the shared schema from WP-0018-T01: reuse events
|
The shared schema (`schemas/reuse-event.schema.json`) already existed from
|
||||||
`{ts, consumer_repo, capability_id, verdict, outcome?, source: plan-check|manual|hub}`
|
WP-0018-T01 drafting — this task implemented the hub side and wired
|
||||||
- Hub: `POST /v1/reuse-events` (token-auth) + local JSONL fallback when hub
|
`plan-check`/manual recording against it, rather than designing it fresh.
|
||||||
unreachable; `GET /v1/reuse-events?capability_id=` for aggregation
|
|
||||||
- `plan-check --record-outcome` (WP-0018) posts here; manual
|
Implemented:
|
||||||
`reuse-surface record-reuse` for retroactive facts
|
|
||||||
- Privacy/scope: repo slugs and capability ids only — no code, no secrets
|
- `reuse_surface/hub/store.py`: `reuse_events` SQLite table (append-only),
|
||||||
|
`record_reuse_event()` (schema-validated, raises `ValueError` on a bad
|
||||||
|
shape rather than silently accepting drift), `list_reuse_events(capability_id=None)`
|
||||||
|
- `reuse_surface/hub/app.py`: `POST /v1/reuse-events` (token-auth, 201/400),
|
||||||
|
`GET /v1/reuse-events?capability_id=` (no auth, read-only, same posture
|
||||||
|
as `GET /v1/federated`)
|
||||||
|
- `reuse_surface/hub_client.py`: `hub_record_reuse_event`/`hub_list_reuse_events`
|
||||||
|
- `reuse_surface/plan_check.py`: refactored `record_outcome` around a new
|
||||||
|
shared `post_or_fallback_reuse_event()` — tries `POST /v1/reuse-events`
|
||||||
|
first, falls back to the local JSONL only on failure/unreachability
|
||||||
|
(never both, per the design: "+ local JSONL fallback when hub
|
||||||
|
unreachable", not a dual-write). New `record_manual_reuse_event()` for
|
||||||
|
retroactive facts, sharing the same post-or-fallback path
|
||||||
|
- New CLI command `reuse-surface record-reuse --consumer-repo --capability-id
|
||||||
|
--verdict --outcome [--hub-url] [--format]`
|
||||||
|
- `plan-check --record-outcome`'s help text and JSON output updated
|
||||||
|
(`recorded_reuse_event` field); `schemas/plan-check-result.schema.json`
|
||||||
|
extended for the new field
|
||||||
|
- Privacy/scope enforced structurally: the schema's `additionalProperties: false`
|
||||||
|
means a caller literally cannot attach code or secrets to an event, only
|
||||||
|
the declared fields (repo slug, capability id, verdict, outcome, source)
|
||||||
|
- 21 new pytest cases (hub store/API, plan_check dual-path, CLI); 145 total pass
|
||||||
|
- **Live-verified** against a real locally-running hub instance: `POST`/`GET
|
||||||
|
/v1/reuse-events` directly, `record-reuse` CLI posting to the hub,
|
||||||
|
`plan-check --record-outcome` posting to the hub, and — after actually
|
||||||
|
killing the hub process — confirmed the fallback path writes correctly
|
||||||
|
to the local JSONL instead of erroring
|
||||||
|
|
||||||
## Telemetry Aggregation Into R-Axis Evidence
|
## Telemetry Aggregation Into R-Axis Evidence
|
||||||
|
|
||||||
|
|
@ -322,7 +348,7 @@ state_hub_task_id: "a9f44d45-91e2-4b43-909f-30a5f906cf3b"
|
||||||
- [x] No hardcoded forge host in code or sources.yaml; `migrate-host` tested (T01, 2026-07-07)
|
- [x] No hardcoded forge host in code or sources.yaml; `migrate-host` tested (T01, 2026-07-07)
|
||||||
- [x] Push to a sibling repo's `registry/indexes/` recomposes the hub index without manual action (webhook), with scheduled fallback in place (T02/T03, 2026-07-07 — both live-verified end to end)
|
- [x] Push to a sibling repo's `registry/indexes/` recomposes the hub index without manual action (webhook), with scheduled fallback in place (T02/T03, 2026-07-07 — both live-verified end to end)
|
||||||
- [x] This repo's CI runs on Forgejo Actions (`.forgejo/workflows/`) (T03, 2026-07-07 — `ci.yml`/`ci-smoke.yaml`/`image.yaml` all verified green on the live push)
|
- [x] This repo's CI runs on Forgejo Actions (`.forgejo/workflows/`) (T03, 2026-07-07 — `ci.yml`/`ci-smoke.yaml`/`image.yaml` all verified green on the live push)
|
||||||
- [ ] Reuse events recordable via hub API and CLI; `report reuse` aggregates them
|
- [x] Reuse events recordable via hub API and CLI (T04, 2026-07-08 — live-verified); `report reuse` aggregation is T05
|
||||||
- [ ] R-axis evidence rules for observed reuse documented in the maturity standard
|
- [ ] R-axis evidence rules for observed reuse documented in the maturity standard
|
||||||
- [x] Hub freshness visible (`composed_at`, stale flag) in API and stats (T02, 2026-07-07)
|
- [x] Hub freshness visible (`composed_at`, stale flag) in API and stats (T02, 2026-07-07)
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue