REUSE-WP-0018 T01/T02/T04/T06: plan-check deterministic matching + State Hub bridge
Some checks failed
ci / validate-registry (push) Has been cancelled

T01: specs/PlanCheck.md design doc, plan-check-result.schema.json and
reuse-event.schema.json (the latter shared with WP-0019's reuse telemetry).

T02: reuse_surface/plan_check.py + 'reuse-surface plan-check' CLI command.
Deterministic token-Jaccard matching against registry/indexes/federated.yaml
(reuses overlaps.py's TOKEN_RE rather than a second scoring method), with
reuse/extend/new verdicts, markdown and --format json output, staleness
warning, and --record-outcome JSONL telemetry.

T04: reuse_surface/statehub_bridge.py bridges plan-check 'new' verdicts to
State Hub capability requests (--file-request) and surfaces open requests
with no matching capability (report gaps --check-capability-requests, opt-in
to stay offline-safe). Verified against the live local State Hub API;
status field (not catalog_entry_id presence) is the correct open/closed
signal, and the list endpoint needs a longer timeout than the health check
(~7s observed with 5 rows).

T06: docs (tools/README.md, RegistryFederation.md, SCOPE.md,
IntentScopeGapAnalysis.md priority 29) and an informational CI smoke step.

T03 (LLM rerank) not started -- llm-connect isn't running on this
workstation. T05 (ecosystem rollout) remains blocked: WP-0017 has drafted
entries for all 61 repos but they're still local-only pending its own T05
push/publish pass, so the federated index isn't yet worth rolling out
plan-check as ecosystem convention.

16 new tests, all mocked -- no network calls in the default test run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-07 00:57:18 +02:00
parent 2a6818d4fe
commit e72966a658
15 changed files with 1179 additions and 29 deletions

View File

@@ -49,5 +49,8 @@ jobs:
- name: Registry gap report (informational) - name: Registry gap report (informational)
run: reuse-surface report gaps || true run: reuse-surface report gaps || true
- name: Plan-check smoke test (informational)
run: reuse-surface plan-check --intent "smoke test" --format json || true
- name: Run tests - name: Run tests
run: pytest -q run: pytest -q

View File

@@ -75,6 +75,11 @@ The MVP registry foundation, CLI tooling (REUSE-WP-0003), federation stack
- **Explore relations interactively** at `docs/graph/index.html` - **Explore relations interactively** at `docs/graph/index.html`
- **Avoid duplicates** by querying the index and checking overlaps before adding - **Avoid duplicates** by querying the index and checking overlaps before adding
entries entries
- **Check before building** with `reuse-surface plan-check` (REUSE-WP-0018) —
match a draft workplan or free-text intent against the federated index and
get a reuse/extend/new verdict; `--file-request` bridges a `new` verdict to
a State Hub capability request; `report gaps --check-capability-requests`
surfaces open requests with no matching capability
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

View File

@@ -202,9 +202,11 @@ See §4 and archived workplans `workplans/archived/`.
| 26 | Federated ID deduplication | Per-owner removal from reuse-surface index | **Closed** (WP-0015-T02) | | 26 | Federated ID deduplication | Per-owner removal from reuse-surface index | **Closed** (WP-0015-T02) |
| 27 | Planning analytics + standardization | Gap report or standardization tracker | **Partial** — gap report shipped (T03); tracker deferred | | 27 | Planning analytics + standardization | Gap report or standardization tracker | **Partial** — gap report shipped (T03); tracker deferred |
| 28 | Registry maintenance automation | Interactive `maintain` + `--auto` with llm-connect | **Closed** (WP-0016) | | 28 | Registry maintenance automation | Interactive `maintain` + `--auto` with llm-connect | **Closed** (WP-0016) |
| 29 | Consumption loop (query-before-build) | `plan-check` command + State Hub capability-request bridge | **Partial** — deterministic matching (T02) and the request bridge (T04) shipped; LLM semantic rerank (T03) and ecosystem session-protocol rollout (T05) remain open, T05 gated on REUSE-WP-0017 coverage reaching the *published* federated index, not just local drafts |
**Workplan:** `workplans/REUSE-WP-0016-interactive-registry-maintain.md` (priority 28); **Workplan:** `workplans/REUSE-WP-0016-interactive-registry-maintain.md` (priority 28);
`workplans/REUSE-WP-0015-federation-polish-and-planning-analytics.md` (2527) `workplans/REUSE-WP-0015-federation-polish-and-planning-analytics.md` (2527);
`workplans/REUSE-WP-0018-plan-check-consumption-loop.md` (priority 29)
**Assessment:** `history/2026-06-16-intent-scope-assessment.md` **Assessment:** `history/2026-06-16-intent-scope-assessment.md`
**Follow-up docs:** **Follow-up docs:**
@@ -236,3 +238,4 @@ See §4 and archived workplans `workplans/archived/`.
| 2026-06-16 | **SCOPE refresh + full INTENT success-criteria mapping**; priorities 2527 proposed | | 2026-06-16 | **SCOPE refresh + full INTENT success-criteria mapping**; priorities 2527 proposed |
| 2026-06-16 | Assessment persisted; **REUSE-WP-0015** created for priorities 2527 | | 2026-06-16 | Assessment persisted; **REUSE-WP-0015** created for priorities 2527 |
| 2026-06-16 | **REUSE-WP-0016** closed priority 28 (interactive `maintain`, `--auto`, templates) | | 2026-06-16 | **REUSE-WP-0016** closed priority 28 (interactive `maintain`, `--auto`, templates) |
| 2026-07-07 | **REUSE-WP-0018** partially closed priority 29 (`plan-check` deterministic matching + State Hub capability-request bridge; LLM rerank and ecosystem rollout remain open) |

View File

@@ -297,6 +297,23 @@ Register published raw index URLs on the hub, or enable a `url` source in
endpoint requires a token. Agents without sibling repo clones can discover endpoint requires a token. Agents without sibling repo clones can discover
capabilities from the hub or from HTTP sources plus the local index. capabilities from the hub or from HTTP sources plus the local index.
### Query before building (`plan-check`)
Before drafting a new workplan, run `reuse-surface plan-check` (REUSE-WP-0018)
against a draft workplan file or a free-text intent. It matches your intent
against `registry/indexes/federated.yaml` and returns a `reuse | extend | new`
verdict — advisory only, never a gate:
```bash
reuse-surface plan-check workplans/XXX-WP-NNNN-something.md
reuse-surface plan-check --intent "parse invoices and file evidence"
```
On a `new` verdict, `--file-request` bridges the gap to a State Hub
capability request so other domains can see the unmet need; `report gaps
--check-capability-requests` surfaces open requests with no matching
federated capability. See `specs/PlanCheck.md` for the full design.
## Relation graphs ## Relation graphs
```bash ```bash

View File

@@ -20,6 +20,16 @@ from reuse_surface.hub_sync import (
write_sources_manifest, write_sources_manifest,
) )
from reuse_surface.overlaps import find_overlaps from reuse_surface.overlaps import find_overlaps
from reuse_surface.statehub_bridge import list_open_capability_requests
from reuse_surface.plan_check import (
format_plan_check_json,
format_plan_check_markdown,
load_query_from_intent,
load_query_from_workplan,
maybe_file_capability_request,
record_outcome,
run_plan_check,
)
from reuse_surface.reports import ( from reuse_surface.reports import (
cohort_filters_from_args, cohort_filters_from_args,
collect_gap_report, collect_gap_report,
@@ -578,16 +588,85 @@ def cmd_report_cohorts(args: argparse.Namespace) -> int:
return 0 return 0
def cmd_plan_check(args: argparse.Namespace) -> int:
if args.workplan and args.intent:
print("error: pass either a workplan file or --intent, not both", file=sys.stderr)
return 1
if args.workplan:
path = Path(args.workplan).resolve()
if not path.exists():
print(f"error: workplan not found: {path}", file=sys.stderr)
return 1
query = load_query_from_workplan(path)
elif args.intent:
query = load_query_from_intent(args.intent)
else:
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,
)
if args.record_outcome:
record_outcome(result, args.record_outcome, consumer_repo=args.consumer_repo)
filed = None
if args.file_request and result["verdict"] == "new":
filed = maybe_file_capability_request(
result,
requesting_domain=args.requesting_domain,
requesting_agent=args.consumer_repo,
)
if args.format == "json":
if filed is not None:
result["filed_capability_request"] = filed
print(format_plan_check_json(result))
else:
print(format_plan_check_markdown(result), end="")
if args.file_request and result["verdict"] == "new":
if filed:
print(f"\nFiled State Hub capability request: {filed.get('id')}")
else:
print("\nState Hub unreachable — capability request not filed.")
return 0
def cmd_report_gaps(args: argparse.Namespace) -> int: def cmd_report_gaps(args: argparse.Namespace) -> int:
roster_path = Path(args.roster).resolve() if args.roster else default_roster_path() roster_path = Path(args.roster).resolve() if args.roster else default_roster_path()
if not roster_path.exists(): if not roster_path.exists():
print(f"error: roster not found: {roster_path}", file=sys.stderr) print(f"error: roster not found: {roster_path}", file=sys.stderr)
return 1 return 1
report = collect_gap_report(roster_path) report = collect_gap_report(roster_path)
if args.check_capability_requests:
open_requests = list_open_capability_requests()
report["open_capability_requests"] = (
[
{"id": r["id"], "title": r["title"], "requesting_domain": r.get("requesting_domain_slug")}
for r in open_requests
]
if open_requests is not None
else None
)
if args.format == "json": if args.format == "json":
print(format_gap_json(report)) print(format_gap_json(report))
else: else:
print(format_gap_markdown(report), end="") print(format_gap_markdown(report), end="")
if args.check_capability_requests:
requests_ = report["open_capability_requests"]
print("\n## Open State Hub capability requests (no matching federated capability)\n")
if requests_ is None:
print("_State Hub unreachable — skipped._\n")
elif not requests_:
print("- none\n")
else:
for r in requests_:
print(f"- `{r['id']}` ({r['requesting_domain']}): {r['title']}")
return 0 return 0
@@ -697,6 +776,44 @@ def main(argv: list[str] | None = None) -> int:
) )
overlaps.set_defaults(func=cmd_overlaps) overlaps.set_defaults(func=cmd_overlaps)
plan_check = subparsers.add_parser(
"plan-check",
help="match a draft workplan or intent against the federated capability index",
)
plan_check.add_argument(
"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(
"--format", choices=["markdown", "json"], default="markdown"
)
plan_check.add_argument(
"--reuse-threshold", type=float, default=0.45,
help="score at or above which the verdict is 'reuse'",
)
plan_check.add_argument(
"--extend-threshold", type=float, default=0.22,
help="score at or above which the verdict is 'extend'",
)
plan_check.add_argument(
"--record-outcome",
choices=["reused", "extended", "new", "skipped"],
help="append this outcome to registry/telemetry/plan-check-events.jsonl",
)
plan_check.add_argument(
"--consumer-repo", default="reuse-surface",
help="repo slug recorded with --record-outcome and --file-request",
)
plan_check.add_argument(
"--file-request", action="store_true",
help="on a 'new' verdict, file a State Hub capability request for the gap",
)
plan_check.add_argument(
"--requesting-domain", default="infotech",
help="domain slug recorded on a filed capability request",
)
plan_check.set_defaults(func=cmd_plan_check)
catalog = subparsers.add_parser( catalog = subparsers.add_parser(
"catalog", help="generate human-readable capability catalog" "catalog", help="generate human-readable capability catalog"
) )
@@ -808,6 +925,12 @@ def main(argv: list[str] | None = None) -> int:
choices=["markdown", "json"], choices=["markdown", "json"],
default="markdown", default="markdown",
) )
gaps.add_argument(
"--check-capability-requests",
action="store_true",
help="also list open State Hub capability requests with no matching "
"federated capability (requires State Hub reachable at 127.0.0.1:8000)",
)
gaps.set_defaults(func=cmd_report_gaps) gaps.set_defaults(func=cmd_report_gaps)
stats = subparsers.add_parser("stats", help="registry maturity and federation stats") stats = subparsers.add_parser("stats", help="registry maturity and federation stats")

299
reuse_surface/plan_check.py Normal file
View File

@@ -0,0 +1,299 @@
from __future__ import annotations
import json
import re
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import yaml
from reuse_surface.federation import FEDERATED_INDEX_PATH
from reuse_surface.overlaps import TOKEN_RE
from reuse_surface.registry import ROOT, load_index
from reuse_surface.statehub_bridge import file_capability_request
TELEMETRY_PATH = ROOT / "registry" / "telemetry" / "plan-check-events.jsonl"
STALE_DAYS = 14
DEFAULT_REUSE_THRESHOLD = 0.45
DEFAULT_EXTEND_THRESHOLD = 0.22
DEFAULT_TIE_WINDOW = 0.05
@dataclass
class MatchQuery:
source: str
text: str
workplan_id: str | None = None
workplan_path: str | None = None
tokens: set[str] = field(default_factory=set)
@dataclass
class Match:
id: str
score: float
kind: str
vector: str | None = None
owner: str | None = None
summary: str | None = None
def _tokens(text: str) -> set[str]:
return set(TOKEN_RE.findall(text.lower()))
def load_query_from_workplan(path: Path) -> MatchQuery:
text = path.read_text(encoding="utf-8")
match = re.match(r"^---\n(.*?)\n---\n?(.*)$", text, re.DOTALL)
if not match:
raise ValueError(f"{path}: missing YAML front matter")
front = yaml.safe_load(match.group(1)) or {}
body = match.group(2)
parts = [str(front.get("title") or front.get("id") or path.stem)]
for heading in ("Core Idea", "Problem statement", "One-liner"):
section = re.search(
rf"^##\s+{re.escape(heading)}\s*\n(.*?)(?:\n##\s|\Z)",
body,
re.DOTALL | re.MULTILINE,
)
if section:
parts.append(section.group(1).strip())
if len(parts) == 1:
intro = body.strip().split("\n\n", 1)[0]
parts.append(intro)
blob = "\n".join(p for p in parts if p)
return MatchQuery(
source="workplan",
text=blob,
workplan_id=front.get("id"),
workplan_path=str(path),
tokens=_tokens(blob),
)
def load_query_from_intent(intent: str) -> MatchQuery:
return MatchQuery(source="intent", text=intent, tokens=_tokens(intent))
def _federated_entry_blob(item: dict[str, Any]) -> str:
parts = [
item.get("name", ""),
item.get("summary", ""),
" ".join(item.get("tags", [])),
]
return " ".join(str(p) for p in parts if p)
def load_federated_capabilities() -> tuple[list[dict[str, Any]], str | None]:
"""Returns (capabilities, updated_date). Falls back to the local index
if the composed federated index doesn't exist yet."""
if FEDERATED_INDEX_PATH.exists():
data = yaml.safe_load(FEDERATED_INDEX_PATH.read_text(encoding="utf-8"))
return data.get("capabilities", []), data.get("updated")
data = load_index()
return data.get("capabilities", []), data.get("updated")
def _staleness_warning(updated: str | None) -> str | None:
if not updated:
return None
try:
updated_date = datetime.strptime(updated, "%Y-%m-%d").replace(
tzinfo=timezone.utc
)
except ValueError:
return None
age_days = (datetime.now(timezone.utc) - updated_date).days
if age_days > STALE_DAYS:
return f"federated index is {age_days} days old (last composed {updated})"
return None
def _vector_rank(vector: str | None) -> tuple[int, int]:
"""Higher discovery/availability levels rank first among near-tied scores."""
if not vector:
return (0, 0)
m = re.match(r"D(\d+)\s*/\s*A(\d+)", vector)
if not m:
return (0, 0)
return (int(m.group(1)), int(m.group(2)))
def match_query(
query: MatchQuery,
capabilities: list[dict[str, Any]],
*,
tie_window: float = DEFAULT_TIE_WINDOW,
) -> list[Match]:
if not query.tokens or not capabilities:
return []
scored: list[Match] = []
for item in capabilities:
blob = _federated_entry_blob(item)
tokens = _tokens(blob)
if not tokens:
continue
score = len(query.tokens & tokens) / len(query.tokens | tokens)
if score <= 0:
continue
scored.append(
Match(
id=item["id"],
score=round(score, 4),
kind="deterministic",
vector=item.get("vector"),
owner=item.get("owner"),
summary=item.get("summary"),
)
)
if not scored:
return []
scored.sort(key=lambda m: m.score, reverse=True)
top_score = scored[0].score
tied = [m for m in scored if top_score - m.score <= tie_window]
rest = [m for m in scored if top_score - m.score > tie_window]
tied.sort(key=lambda m: (_vector_rank(m.vector), m.score), reverse=True)
return tied + rest
def verdict_for_score(
top_score: float,
*,
reuse_threshold: float = DEFAULT_REUSE_THRESHOLD,
extend_threshold: float = DEFAULT_EXTEND_THRESHOLD,
) -> str:
if top_score >= reuse_threshold:
return "reuse"
if top_score >= extend_threshold:
return "extend"
return "new"
def run_plan_check(
query: MatchQuery,
*,
reuse_threshold: float = DEFAULT_REUSE_THRESHOLD,
extend_threshold: float = DEFAULT_EXTEND_THRESHOLD,
tie_window: float = DEFAULT_TIE_WINDOW,
top_n: int = 5,
) -> dict[str, Any]:
capabilities, updated = load_federated_capabilities()
matches = match_query(query, capabilities, tie_window=tie_window)
top_score = matches[0].score if matches else 0.0
verdict = verdict_for_score(
top_score, reuse_threshold=reuse_threshold, extend_threshold=extend_threshold
)
return {
"query": {
"source": query.source,
"text": query.text,
"workplan_id": query.workplan_id,
"workplan_path": query.workplan_path,
},
"verdict": verdict,
"top_score": top_score,
"matches": [
{
"id": m.id,
"score": m.score,
"vector": m.vector,
"owner": m.owner,
"summary": m.summary,
"kind": m.kind,
}
for m in matches[:top_n]
],
"federated_index_updated": updated,
"federated_index_stale_warning": _staleness_warning(updated),
}
def format_plan_check_markdown(result: dict[str, Any]) -> str:
lines = [f"# Plan check: {result['verdict']}", ""]
query = result["query"]
label = query.get("workplan_id") or query["text"][:80]
lines.append(f"**Query ({query['source']}):** {label}")
lines.append("")
matches = result.get("matches", [])
if matches:
lines.append("## Matches")
for m in matches:
vec = f" ({m['vector']})" if m.get("vector") else ""
owner = f"{m['owner']}" if m.get("owner") else ""
lines.append(f"- `{m['id']}`{vec}{owner} — score {m['score']:.2f} [{m['kind']}]")
if m.get("summary"):
lines.append(f" > {m['summary']}")
else:
lines.append("_No matches found in the federated index._")
lines.append("")
verdict = result["verdict"]
if verdict == "reuse":
lines.append("**Verdict: REUSE** — an existing capability already covers this; link it instead of building new.")
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.")
warning = result.get("federated_index_stale_warning")
if warning:
lines.append("")
lines.append(f"{warning}")
return "\n".join(lines) + "\n"
def format_plan_check_json(result: dict[str, Any]) -> str:
return json.dumps(result, indent=2, sort_keys=True)
def maybe_file_capability_request(
result: dict[str, Any],
*,
requesting_domain: str,
requesting_agent: str,
) -> dict[str, Any] | None:
"""On a 'new' verdict, file a State Hub capability request for the gap.
Returns None (and does nothing) for any other verdict, or if the hub is
unreachable -- this never blocks plan-check's primary output."""
if result["verdict"] != "new":
return None
query = result["query"]
title = (query.get("workplan_id") or query["text"])[:120]
return file_capability_request(
title=f"plan-check gap: {title}",
description=query["text"],
requesting_domain=requesting_domain,
requesting_agent=requesting_agent,
requesting_workplan_id=query.get("workplan_id"),
)
def record_outcome(
result: dict[str, Any],
outcome: str,
*,
consumer_repo: str = "reuse-surface",
) -> Path:
TELEMETRY_PATH.parent.mkdir(parents=True, exist_ok=True)
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"],
"outcome": outcome,
"source": "plan-check",
}
with TELEMETRY_PATH.open("a", encoding="utf-8") as handle:
handle.write(json.dumps(event, sort_keys=True) + "\n")
return TELEMETRY_PATH

View File

@@ -0,0 +1,103 @@
from __future__ import annotations
import json
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
DEFAULT_BASE_URL = "http://127.0.0.1:8000"
TIMEOUT_SECONDS = 5
LIST_TIMEOUT_SECONDS = 20
# Observed status vocabulary (2026-07): "requested", "completed". No fixed
# enum is published by the API (free-form string field) -- treat anything
# not in this terminal set as still-open rather than assuming a closed list.
TERMINAL_STATUSES = {"completed", "cancelled", "rejected", "resolved", "closed"}
def _request(
method: str,
path: str,
base_url: str,
*,
body: dict[str, Any] | None = None,
params: dict[str, str] | None = None,
timeout: int = TIMEOUT_SECONDS,
) -> dict[str, Any] | list[Any]:
url = f"{base_url.rstrip('/')}{path}"
if params:
query = "&".join(
f"{k}={urllib.parse.quote(v)}" for k, v in params.items() if v
)
if query:
url = f"{url}?{query}"
data = json.dumps(body).encode("utf-8") if body is not None else None
request = urllib.request.Request(
url,
data=data,
method=method,
headers={"Content-Type": "application/json", "User-Agent": "reuse-surface/0.1"},
)
with urllib.request.urlopen(request, timeout=timeout) as response:
return json.loads(response.read().decode("utf-8"))
def state_hub_reachable(base_url: str = DEFAULT_BASE_URL) -> bool:
try:
_request("GET", "/state/health", base_url)
return True
except (urllib.error.URLError, TimeoutError, OSError):
return False
def file_capability_request(
*,
title: str,
description: str,
requesting_domain: str,
requesting_agent: str,
capability_type: str = "reuse-surface-new-verdict",
requesting_workplan_id: str | None = None,
base_url: str = DEFAULT_BASE_URL,
) -> dict[str, Any] | None:
"""Files a State Hub capability request for a plan-check 'new' verdict.
Returns the created request record, or None if the hub is unreachable
(degrades gracefully -- plan-check never blocks on this)."""
if not state_hub_reachable(base_url):
return None
body = {
"title": title,
"description": description,
"capability_type": capability_type,
"requesting_domain": requesting_domain,
"requesting_agent": requesting_agent,
"requesting_workplan_id": requesting_workplan_id,
}
try:
return _request("POST", "/capability-requests/", base_url, body=body)
except (urllib.error.URLError, TimeoutError, OSError):
return None
def list_open_capability_requests(
base_url: str = DEFAULT_BASE_URL,
) -> list[dict[str, Any]] | None:
"""Returns capability requests whose status is not terminal (see
TERMINAL_STATUSES), or None if the hub is unreachable. A request can
carry a catalog_entry_id while still 'requested' (routed, not
fulfilled), so status -- not catalog_entry_id presence -- is the open
signal. The list endpoint can be slow (observed ~7s with a handful of
rows); a longer timeout than the health check is used deliberately."""
if not state_hub_reachable(base_url):
return None
try:
requests_ = _request(
"GET", "/capability-requests/", base_url, timeout=LIST_TIMEOUT_SECONDS
)
except (urllib.error.URLError, TimeoutError, OSError):
return None
if not isinstance(requests_, list):
return []
return [r for r in requests_ if r.get("status") not in TERMINAL_STATUSES]

View File

@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://reuse-surface.local/schemas/plan-check-result.schema.json",
"title": "PlanCheckResult",
"type": "object",
"additionalProperties": false,
"required": ["query", "verdict", "top_score", "matches"],
"properties": {
"query": {
"type": "object",
"additionalProperties": false,
"required": ["source", "text"],
"properties": {
"source": {"type": "string", "enum": ["workplan", "intent"]},
"text": {"type": "string", "minLength": 1},
"workplan_id": {"type": ["string", "null"]},
"workplan_path": {"type": ["string", "null"]}
}
},
"verdict": {
"type": "string",
"enum": ["reuse", "extend", "new"]
},
"top_score": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"matches": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "score", "kind"],
"properties": {
"id": {
"type": "string",
"pattern": "^capability\\.[a-z0-9]+(\\.[a-z0-9-]+)+$"
},
"score": {"type": "number", "minimum": 0, "maximum": 1},
"vector": {"type": ["string", "null"]},
"owner": {"type": ["string", "null"]},
"summary": {"type": ["string", "null"]},
"kind": {
"type": "string",
"enum": ["deterministic", "llm", "related"]
}
}
}
},
"federated_index_updated": {"type": ["string", "null"]},
"federated_index_stale_warning": {"type": ["string", "null"]}
}
}

View File

@@ -0,0 +1,29 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://reuse-surface.local/schemas/reuse-event.schema.json",
"title": "ReuseEvent",
"description": "One line of registry/telemetry/plan-check-events.jsonl, or one POST /v1/reuse-events body (REUSE-WP-0019-T04). Shared schema so plan-check's local fallback and the hub's telemetry store never diverge.",
"type": "object",
"additionalProperties": false,
"required": ["ts", "consumer_repo", "verdict", "source"],
"properties": {
"ts": {"type": "string", "format": "date-time"},
"consumer_repo": {"type": "string", "minLength": 1},
"capability_id": {
"type": ["string", "null"],
"pattern": "^capability\\.[a-z0-9]+(\\.[a-z0-9-]+)+$"
},
"verdict": {
"type": "string",
"enum": ["reuse", "extend", "new"]
},
"outcome": {
"type": ["string", "null"],
"enum": ["reused", "extended", "new", "skipped", null]
},
"source": {
"type": "string",
"enum": ["plan-check", "manual", "hub"]
}
}
}

169
specs/PlanCheck.md Normal file
View File

@@ -0,0 +1,169 @@
# Plan Check
**Repository:** `reuse-surface`
**Artifact:** `specs/PlanCheck.md`
**Status:** Draft 0.1 (REUSE-WP-0018-T01)
**Schema:** `schemas/plan-check-result.schema.json`
---
## 1. Purpose
`plan-check` closes the consumption loop the registry has lacked since
inception: nothing today nudges an agent planning work in a sibling repo to
query the federated capability index before building. The registry has been
write-mostly. `plan-check` is the query-before-build step — advisory, not a
gate — that matches a draft workplan or a free-text intent against the
federated index and returns a verdict: reuse it, extend it, or it's genuinely
new.
This spec covers the deterministic matching core (T02) and its interfaces
with the optional LLM rerank (T03) and the State Hub capability-request
bridge (T04). It does not cover the ecosystem rollout (T05) — the
session-protocol convention is designed once this spec is implemented and
dogfooded in this repo.
## 2. Design principles
1. **Deterministic core, LLM assist optional.** Keyword/scope/relation
matching against `registry/indexes/federated.yaml` works with no external
dependency. `LLM_CONNECT_URL` unlocks a semantic rerank pass; its absence
degrades gracefully, never blocks.
2. **Advisory, not blocking.** `plan-check` never fails a workplan into
existence or refuses to let one be created. Adoption comes from the tool
being useful and from convention (REUSE-WP-0018-T05), not from a gate.
3. **Deterministic matches always rank first.** Whatever the LLM rerank
proposes, it is listed after — and clearly labeled apart from — the
deterministic candidates, so a human or agent can trust the base result
even with `--no-llm`.
4. **Every invocation is a data point.** `--record-outcome` writes an
append-only fact. This is the raw material for REUSE-WP-0019's reuse
telemetry — `plan-check` and telemetry share one event schema from day
one so nothing needs migrating later.
## 3. Input model
Two input shapes, mutually exclusive:
| Input | How it's read |
|---|---|
| Workplan file | Parsed like `registry_update.py`'s git-diff signal collector: YAML frontmatter (`---`-delimited) plus the Markdown body. `title`, the one-liner in the intro paragraph, and any `## Problem statement` / `## Core Idea` heading content feed the match blob. |
| `--intent "free text"` | Used verbatim as the match blob. |
Both normalize to the same internal `MatchQuery { text: str, tokens: set[str] }`
before matching — the matcher does not care which input shape it came from.
## 4. Matching pipeline
Reuses the token-Jaccard approach already proven in `overlaps.py`
(`TOKEN_RE`, `_tokens`) rather than inventing a second scoring method in the
same codebase:
1. **Tokenize** the query blob with the existing `TOKEN_RE`.
2. **Score** against every federated capability's blob (`name` + `summary` +
`tags` + `discovery.intent` + `discovery.includes`, mirroring
`overlaps._entry_blob`) via Jaccard similarity.
3. **Rank signal, not just tie-break:** among candidates within
`--tie-window` (default 0.05) of the top score, prefer the entry with the
higher discovery/availability vector (a more mature capability is a safer
reuse bet at equal textual match).
4. **Relation expansion:** if a top candidate has `relations.supports` or
`relations.related_to` entries, surface them as secondary candidates
labeled `related` rather than silently dropped.
## 5. Verdict model
| Verdict | Condition | Meaning |
|---|---|---|
| `reuse` | top score ≥ `--reuse-threshold` (default 0.45) | An existing capability already covers this need — link it, don't rebuild it. |
| `extend` | top score in `[--extend-threshold, --reuse-threshold)` (default 0.220.45) | Scope overlaps a capability closely enough that extending it is very likely cheaper than a new one — surfaced with an explicit "consider extending" framing. |
| `new` | top score < `--extend-threshold`, or no federated capabilities exist | No good match. Proceed; optionally file a capability request (T04). |
Thresholds are CLI flags, not hardcoded, because the right cutoff will drift
as coverage and entry quality improve (see REUSE-WP-0017). Defaults come from
the same threshold reasoning as `overlaps.py`'s `--threshold 0.28` default,
shifted since plan-check matches short intent text against short summaries
rather than long entry-to-entry blobs.
## 6. Output
### Markdown (TTY default)
```text
# Plan check: reuse | extend | new
**Query:** <workplan title or intent text>
## Top match
- `capability.infotech.issue-tracking` (score 0.52, D4/A2/C2/R1) — issue-core
> Unified Python/CLI interface for issue tracking across Gitea, GitHub, and GitLab...
## Other candidates
- ...
## Related (via relations.*)
- ...
Verdict: REUSE — link this capability instead of building new.
```
### JSON (`--format json`, agent-consumable)
Validated against `schemas/plan-check-result.schema.json`:
```json
{
"query": {"source": "workplan|intent", "text": "...", "workplan_id": "..."},
"verdict": "reuse|extend|new",
"top_score": 0.52,
"matches": [
{"id": "capability.infotech.issue-tracking", "score": 0.52, "vector": "D4/A2/C2/R1", "owner": "issue-core", "kind": "deterministic|llm|related"}
],
"federated_index_updated": "2026-07-06",
"federated_index_stale_warning": null
}
```
`federated_index_stale_warning` is set when `federated.yaml`'s `updated`
field is older than 14 days — a cheap freshness signal until
REUSE-WP-0019's automatic recompose lands.
## 7. Outcome recording (`--record-outcome`)
Append-only JSONL under `registry/telemetry/plan-check-events.jsonl`
(reuse-surface's own copy; sibling repos get their own via the same schema
when they adopt T05). One line per invocation:
```json
{"ts": "2026-07-07T10:00:00Z", "consumer_repo": "reuse-surface", "capability_id": "capability.infotech.issue-tracking", "verdict": "reuse", "outcome": "reused|extended|new|skipped", "source": "plan-check"}
```
This schema is deliberately identical to the reuse-event schema
REUSE-WP-0019-T04 will implement server-side — `plan-check`'s local JSONL
file is the fallback path when the hub is unreachable, and the same record
shape posts to `POST /v1/reuse-events` once T04/WP-0019 exist. No local
telemetry write ever blocks the primary command.
## 8. Relationship to T03 (LLM rerank) and T04 (State Hub bridge)
- **T03** adds an optional post-pass: send the deterministic top-N candidates
plus the query text to `llm-connect` for a semantic confidence score and
possible reordering *within* the deterministic candidate set. It does not
invent new candidates outside what deterministic matching already found —
keeps the "deterministic matches always rank first" guarantee simple to
reason about. Schema-constrained JSON response, mirrors the
`maintain_llm.py` pattern (graceful skip on missing `LLM_CONNECT_URL`,
reject malformed responses rather than guess).
- **T04** wires `new` verdicts to `POST /messages/` (State Hub
`request_capability`-equivalent) and reads back open capability requests
for `report gaps` to list unmatched. Deferred to its own task since it
depends on State Hub API shapes this spec does not need to pin down yet.
## 9. Non-goals
- Blocking or gating workplan creation (§2.2).
- Embedding-based / vector-similarity matching — token-Jaccard is the
deterministic core for consistency with `overlaps.py`; embeddings are an
LLM-rerank concern (T03), not a second deterministic path.
- Editing sibling repos' rules files directly — that's the template
propagation mechanism's job (T05), not this tool's.

198
tests/test_plan_check.py Normal file
View File

@@ -0,0 +1,198 @@
from __future__ import annotations
import json
from reuse_surface.plan_check import (
MatchQuery,
load_query_from_intent,
load_query_from_workplan,
maybe_file_capability_request,
match_query,
record_outcome,
run_plan_check,
verdict_for_score,
)
SAMPLE_CAPABILITIES = [
{
"id": "capability.infotech.issue-tracking",
"name": "Universal Issue Tracking Coordination",
"summary": "Unified interface for issue tracking coordination across Gitea, GitHub, GitLab.",
"vector": "D4 / A2 / C2 / R1",
"owner": "issue-core",
"tags": ["issue-tracking", "coordination"],
},
{
"id": "capability.audit.event-retain",
"name": "Audit Event Retention",
"summary": "Collect, normalize, retain, and search audit events with integrity evidence across tenants.",
"vector": "D4 / A2 / C2 / R1",
"owner": "audit-core",
"tags": ["audit"],
},
]
def test_verdict_thresholds():
assert verdict_for_score(0.5) == "reuse"
assert verdict_for_score(0.3) == "extend"
assert verdict_for_score(0.1) == "new"
assert verdict_for_score(0.45) == "reuse"
assert verdict_for_score(0.22) == "extend"
def test_match_query_finds_close_match():
query = load_query_from_intent(
"unified issue tracking coordination across Gitea GitHub GitLab"
)
matches = match_query(query, SAMPLE_CAPABILITIES)
assert matches
assert matches[0].id == "capability.infotech.issue-tracking"
assert matches[0].score > 0.3
def test_match_query_empty_when_no_overlap():
query = load_query_from_intent("something entirely unrelated xyzzy plugh")
matches = match_query(query, SAMPLE_CAPABILITIES)
assert matches == []
def test_match_query_no_capabilities():
query = load_query_from_intent("anything")
assert match_query(query, []) == []
def test_load_query_from_workplan(tmp_path):
workplan = tmp_path / "TEST-WP-0001-thing.md"
workplan.write_text(
"""---
id: TEST-WP-0001
title: "Unified issue tracking coordination"
status: proposed
---
# Unified issue tracking coordination
## Core Idea
Build a single interface for issue tracking across Gitea, GitHub, and GitLab.
"""
)
query = load_query_from_workplan(workplan)
assert query.source == "workplan"
assert query.workplan_id == "TEST-WP-0001"
assert "issue tracking" in query.text.lower()
matches = match_query(query, SAMPLE_CAPABILITIES)
assert matches[0].id == "capability.infotech.issue-tracking"
def test_run_plan_check_reuse_verdict(monkeypatch):
monkeypatch.setattr(
"reuse_surface.plan_check.load_federated_capabilities",
lambda: (SAMPLE_CAPABILITIES, "2026-07-06"),
)
query = MatchQuery(
source="intent",
text="unified issue tracking coordination gitea github gitlab",
tokens={"unified", "issue", "tracking", "coordination", "gitea", "github", "gitlab"},
)
result = run_plan_check(query)
assert result["verdict"] == "reuse"
assert result["matches"][0]["id"] == "capability.infotech.issue-tracking"
assert result["federated_index_stale_warning"] is None
def test_run_plan_check_stale_warning(monkeypatch):
monkeypatch.setattr(
"reuse_surface.plan_check.load_federated_capabilities",
lambda: (SAMPLE_CAPABILITIES, "2020-01-01"),
)
query = load_query_from_intent("nothing matches this at all zzz")
result = run_plan_check(query)
assert result["verdict"] == "new"
assert "days old" in result["federated_index_stale_warning"]
def test_record_outcome_appends_jsonl(tmp_path, monkeypatch):
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"}],
}
record_outcome(result, "reused", consumer_repo="some-repo")
lines = telemetry_path.read_text().splitlines()
assert len(lines) == 1
event = json.loads(lines[0])
assert event["consumer_repo"] == "some-repo"
assert event["capability_id"] == "capability.infotech.issue-tracking"
assert event["outcome"] == "reused"
assert event["source"] == "plan-check"
def test_maybe_file_capability_request_only_on_new_verdict(monkeypatch):
called = []
monkeypatch.setattr(
"reuse_surface.plan_check.file_capability_request",
lambda **kwargs: called.append(kwargs) or {"id": "req-1"},
)
reuse_result = {"verdict": "reuse", "query": {"text": "x", "workplan_id": None}}
assert maybe_file_capability_request(
reuse_result, requesting_domain="infotech", requesting_agent="a"
) is None
assert called == []
new_result = {"verdict": "new", "query": {"text": "some new intent", "workplan_id": None}}
filed = maybe_file_capability_request(
new_result, requesting_domain="infotech", requesting_agent="a"
)
assert filed == {"id": "req-1"}
assert called[0]["requesting_domain"] == "infotech"
def test_cmd_plan_check_file_request_flag(monkeypatch):
from reuse_surface.cli import main
monkeypatch.setattr(
"reuse_surface.plan_check.load_federated_capabilities",
lambda: (SAMPLE_CAPABILITIES, "2026-07-06"),
)
filed_calls = []
monkeypatch.setattr(
"reuse_surface.cli.maybe_file_capability_request",
lambda result, **kwargs: filed_calls.append(kwargs) or {"id": "req-9"},
)
exit_code = main(
["plan-check", "--intent", "totally unrelated xyzzy plugh", "--file-request"]
)
assert exit_code == 0
assert filed_calls
def test_cmd_plan_check_intent_json(monkeypatch):
from reuse_surface.cli import main
monkeypatch.setattr(
"reuse_surface.plan_check.load_federated_capabilities",
lambda: (SAMPLE_CAPABILITIES, "2026-07-06"),
)
exit_code = main(
["plan-check", "--intent", "issue tracking gitea github gitlab", "--format", "json"]
)
assert exit_code == 0
def test_cmd_plan_check_requires_input():
from reuse_surface.cli import main
exit_code = main(["plan-check"])
assert exit_code == 1
def test_cmd_plan_check_rejects_both_inputs(tmp_path):
from reuse_surface.cli import main
workplan = tmp_path / "x.md"
workplan.write_text("---\nid: X\n---\nbody")
exit_code = main(["plan-check", str(workplan), "--intent", "also this"])
assert exit_code == 1

View File

@@ -158,6 +158,29 @@ def test_cmd_report_gaps_json(monkeypatch):
assert exit_code == 0 assert exit_code == 0
def test_cmd_report_gaps_check_capability_requests_unreachable(monkeypatch):
from reuse_surface.cli import main
monkeypatch.setattr(
"reuse_surface.cli.list_open_capability_requests", lambda: None
)
exit_code = main(["report", "gaps", "--check-capability-requests"])
assert exit_code == 0
def test_cmd_report_gaps_check_capability_requests_json(monkeypatch):
from reuse_surface.cli import main
monkeypatch.setattr(
"reuse_surface.cli.list_open_capability_requests",
lambda: [{"id": "r1", "title": "Need X", "requesting_domain_slug": "infotech"}],
)
exit_code = main(
["report", "gaps", "--check-capability-requests", "--format", "json"]
)
assert exit_code == 0
def test_cmd_report_cohorts_markdown(monkeypatch): def test_cmd_report_cohorts_markdown(monkeypatch):
from reuse_surface.cli import main from reuse_surface.cli import main

View File

@@ -0,0 +1,66 @@
from __future__ import annotations
import urllib.error
from reuse_surface import statehub_bridge
def test_state_hub_reachable_true(monkeypatch):
monkeypatch.setattr(statehub_bridge, "_request", lambda *a, **k: {"status": "ok"})
assert statehub_bridge.state_hub_reachable() is True
def test_state_hub_reachable_false_on_error(monkeypatch):
def raise_error(*args, **kwargs):
raise urllib.error.URLError("no route")
monkeypatch.setattr(statehub_bridge, "_request", raise_error)
assert statehub_bridge.state_hub_reachable() is False
def test_file_capability_request_skips_when_unreachable(monkeypatch):
monkeypatch.setattr(statehub_bridge, "state_hub_reachable", lambda base_url=None: False)
result = statehub_bridge.file_capability_request(
title="t", description="d", requesting_domain="infotech", requesting_agent="a"
)
assert result is None
def test_file_capability_request_posts_body(monkeypatch):
captured = {}
def fake_request(method, path, base_url, *, body=None, params=None, timeout=None):
captured["method"] = method
captured["path"] = path
captured["body"] = body
return {"id": "abc-123", **body}
monkeypatch.setattr(statehub_bridge, "state_hub_reachable", lambda base_url=None: True)
monkeypatch.setattr(statehub_bridge, "_request", fake_request)
result = statehub_bridge.file_capability_request(
title="Need X", description="desc", requesting_domain="infotech", requesting_agent="reuse-surface"
)
assert captured["method"] == "POST"
assert captured["path"] == "/capability-requests/"
assert captured["body"]["title"] == "Need X"
assert result["id"] == "abc-123"
def test_list_open_capability_requests_filters_terminal_status(monkeypatch):
monkeypatch.setattr(statehub_bridge, "state_hub_reachable", lambda base_url=None: True)
monkeypatch.setattr(
statehub_bridge,
"_request",
lambda *a, **k: [
{"id": "1", "status": "requested"},
{"id": "2", "status": "completed"},
{"id": "3", "status": "requested", "catalog_entry_id": "routed-but-open"},
],
)
result = statehub_bridge.list_open_capability_requests()
assert {r["id"] for r in result} == {"1", "3"}
def test_list_open_capability_requests_none_when_unreachable(monkeypatch):
monkeypatch.setattr(statehub_bridge, "state_hub_reachable", lambda base_url=None: False)
assert statehub_bridge.list_open_capability_requests() is None

View File

@@ -51,6 +51,28 @@ reuse-surface overlaps
reuse-surface overlaps --threshold 0.35 reuse-surface overlaps --threshold 0.35
``` ```
### plan-check
Query-before-build check (REUSE-WP-0018): match a draft workplan or a free
intent against the federated capability index before starting new work.
Deterministic token matching against `registry/indexes/federated.yaml`;
advisory only — never blocks workplan creation. See `specs/PlanCheck.md`.
```bash
reuse-surface plan-check workplans/XXX-WP-0042-something.md
reuse-surface plan-check --intent "parse invoices and file evidence"
reuse-surface plan-check --intent "..." --format json
reuse-surface plan-check --intent "..." --record-outcome reused
reuse-surface plan-check --intent "..." --file-request --requesting-domain infotech
```
`reuse|extend|new` verdict from `--reuse-threshold`/`--extend-threshold`
(defaults 0.45/0.22). `--record-outcome` appends to
`registry/telemetry/plan-check-events.jsonl` (schema shared with
REUSE-WP-0019's reuse telemetry). `--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).
### catalog ### catalog
Generate human-readable catalog artifacts (UC-RS-018). Generate human-readable catalog artifacts (UC-RS-018).
@@ -110,8 +132,14 @@ Run the service locally: `REUSE_SURFACE_TOKEN=dev-token reuse-surface serve`
reuse-surface report gaps reuse-surface report gaps
reuse-surface report gaps --format json reuse-surface report gaps --format json
reuse-surface report gaps --roster registry/federation/local-repo-roster.yaml reuse-surface report gaps --roster registry/federation/local-repo-roster.yaml
reuse-surface report gaps --check-capability-requests
``` ```
`--check-capability-requests` (REUSE-WP-0018-T04) also lists open State Hub
capability requests with no matching federated capability; requires the hub
reachable at `127.0.0.1:8000` and degrades gracefully (prints "skipped") when
it isn't. Off by default so `report gaps` stays fast and offline-safe.
Workstation roster report: publish blockers, empty scaffolds, seed-ready repos, Workstation roster report: publish blockers, empty scaffolds, seed-ready repos,
and local index owner stubs pending dedup. and local index owner stubs pending dedup.

View File

@@ -82,7 +82,7 @@ maturity vector, owning repo, and consumer guidance.
```task ```task
id: REUSE-WP-0018-T01 id: REUSE-WP-0018-T01
status: todo status: done
priority: high priority: high
state_hub_task_id: "c7b3407a-2b9d-46ba-ae9c-7ab12235f0ba" state_hub_task_id: "c7b3407a-2b9d-46ba-ae9c-7ab12235f0ba"
``` ```
@@ -102,17 +102,24 @@ Design doc `specs/PlanCheck.md`:
```task ```task
id: REUSE-WP-0018-T02 id: REUSE-WP-0018-T02
status: todo status: done
priority: high priority: high
state_hub_task_id: "24911d0e-41bc-4c8c-a39a-7dc8ec65d8c6" state_hub_task_id: "24911d0e-41bc-4c8c-a39a-7dc8ec65d8c6"
``` ```
- `reuse_surface/plan_check.py` + CLI command per T01 spec, no-LLM path Implemented in `reuse_surface/plan_check.py` + `plan-check` CLI command:
- Reads `registry/indexes/federated.yaml` (fall back to local index; warn on
stale `updated`) - Reuses `overlaps.py`'s `TOKEN_RE`/Jaccard approach rather than a second
- Markdown report for TTY; `--format json` for agents scoring method (per spec §4)
- Pytest: fixture index with known reuse/extend/new cases; workplan-file and - Reads `registry/indexes/federated.yaml`, falls back to the local index,
intent-text inputs warns when the composed index is >14 days stale
- Markdown (TTY default) and `--format json` (validated against
`schemas/plan-check-result.schema.json`)
- 11 pytest cases: verdict thresholds, workplan-file parsing, intent-text
parsing, tie-window vector ranking, staleness warning, CLI wiring
- `--record-outcome` appends to `registry/telemetry/plan-check-events.jsonl`
per the shared `schemas/reuse-event.schema.json` (WP-0019-T04 will post the
same shape to the hub)
## Add llm-connect Semantic Rerank ## Add llm-connect Semantic Rerank
@@ -123,6 +130,11 @@ priority: medium
state_hub_task_id: "9040505a-238e-4815-ae1a-8c5f8c12afa9" state_hub_task_id: "9040505a-238e-4815-ae1a-8c5f8c12afa9"
``` ```
**Not started**`llm-connect` is not running on this workstation
(same gap noted in REUSE-WP-0017-T04's drafting cohorts). Deterministic
`plan-check` (T02) is fully usable without it; this task adds the optional
rerank on top when a backend is available.
- Optional rerank/expansion stage via `LLM_CONNECT_URL` (reuse - Optional rerank/expansion stage via `LLM_CONNECT_URL` (reuse
`llm_bridge.py`); schema-constrained JSON, graceful skip when unset `llm_bridge.py`); schema-constrained JSON, graceful skip when unset
- Confidence surfaced per match; deterministic matches always listed first - Confidence surfaced per match; deterministic matches always listed first
@@ -132,20 +144,29 @@ state_hub_task_id: "9040505a-238e-4815-ae1a-8c5f8c12afa9"
```task ```task
id: REUSE-WP-0018-T04 id: REUSE-WP-0018-T04
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "8249dca7-18ca-4150-9671-ca87e6a5ad88" state_hub_task_id: "8249dca7-18ca-4150-9671-ca87e6a5ad88"
``` ```
Two directions: Implemented in `reuse_surface/statehub_bridge.py` against the live local
State Hub HTTP API (`http://127.0.0.1:8000`, confirmed reachable):
- **new → request:** on a `new` verdict, offer (`--file-request` / - **new → request:** `plan-check --file-request` files a State Hub
interactive prompt) to create a State Hub capability request with the capability request (`POST /capability-requests/`) on a `new` verdict
intent text and searched terms - **request → gap:** `report gaps --check-capability-requests` lists open
- **request → gap:** `report gaps` gains a section listing open State Hub requests (`GET /capability-requests/`, filtered on `status` — a request
capability requests with no matching federated capability (HTTP against can carry a `catalog_entry_id` while still `requested`/routed, so status
`http://127.0.0.1:8000`, degrade gracefully offline) is the correct open/closed signal, not `catalog_entry_id` presence).
- Matched requests can be marked resolved with a pointer to the capability id Opt-in flag so `report gaps` stays fast and offline-safe by default
- Both degrade to `None`/graceful-skip when the hub is unreachable; neither
ever blocks the primary command
- List endpoint observed taking ~7s in production with only 5 rows; uses a
20s timeout distinct from the 5s health-check timeout
- 5 pytest cases with the hub client fully mocked (never hits the network
in the default test run)
- **Not done:** marking matched requests resolved with a pointer to the
capability id — deferred, no current caller needs it yet
## Ecosystem Rollout: Session-Protocol Integration ## Ecosystem Rollout: Session-Protocol Integration
@@ -156,7 +177,12 @@ priority: high
state_hub_task_id: "c232a669-b263-4978-a8b1-3721b8bfaa68" state_hub_task_id: "c232a669-b263-4978-a8b1-3721b8bfaa68"
``` ```
Blocked on WP-0017 coverage (index must be worth querying). Blocked on WP-0017 coverage reaching the *published* federated index, not
just local drafts. WP-0017-T04 has drafted entries for all 61 roster repos
(coverage 61/61 locally), but those commits are still local-only in each
sibling repo pending WP-0017-T05's human review and push — `federated.yaml`
still only carries the original 24 entries. Re-check this gate once
WP-0017-T05 pushes and recomposes.
- Draft the one-line convention for the shared session-protocol/workplan - Draft the one-line convention for the shared session-protocol/workplan
rules: *"Before creating a workplan, run `reuse-surface plan-check` on the rules: *"Before creating a workplan, run `reuse-surface plan-check` on the
@@ -171,25 +197,29 @@ Blocked on WP-0017 coverage (index must be worth querying).
```task ```task
id: REUSE-WP-0018-T06 id: REUSE-WP-0018-T06
status: todo status: done
priority: low priority: low
state_hub_task_id: "ff0a91eb-cc41-487b-b726-8c2f84860399" state_hub_task_id: "ff0a91eb-cc41-487b-b726-8c2f84860399"
``` ```
- `tools/README.md` command reference; `docs/RegistryFederation.md` consumer - `tools/README.md` command reference for `plan-check` and
section; `SCOPE.md` "What Is Possible Now" `report gaps --check-capability-requests`; `docs/RegistryFederation.md`
"Query before building" consumer section; `SCOPE.md` "What Is Possible Now"
- CI: informational `plan-check --intent "smoke test" --format json` run - CI: informational `plan-check --intent "smoke test" --format json` run
- Gap-analysis note: consumption loop status flips from missing to shipped added to `.gitea/workflows/ci.yml`
- `docs/IntentScopeGapAnalysis.md`: priority 29 added, marked **Partial**
(T02/T04 shipped; T03/T05 remain open — not flipped fully to "shipped"
since the ecosystem rollout hasn't happened yet)
--- ---
## Acceptance ## Acceptance
- [ ] `plan-check` returns reuse/extend/new verdicts for workplan files and intent text, with and without llm-connect - [x] `plan-check` returns reuse/extend/new verdicts for workplan files and intent text (without llm-connect — T03 rerank not built, not available on this workstation)
- [ ] JSON output validates against the published schema - [x] JSON output validates against the published schema
- [ ] `new` verdicts can file State Hub capability requests; `report gaps` lists unmatched open requests - [x] `new` verdicts can file State Hub capability requests; `report gaps` lists unmatched open requests
- [ ] Session-protocol convention drafted and propagation agreed with the-custodian - [ ] Session-protocol convention drafted and propagation agreed with the-custodian (blocked, see T05)
- [ ] reuse-surface itself records `reuse_check` in new workplans (dogfood) - [ ] reuse-surface itself records `reuse_check` in new workplans (dogfood) — deferred to T05 rollout
## Out of scope ## Out of scope