generated from coulomb/repo-seed
REUSE-WP-0018 T01/T02/T04/T06: plan-check deterministic matching + State Hub bridge
Some checks failed
ci / validate-registry (push) Has been cancelled
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:
@@ -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
|
||||||
5
SCOPE.md
5
SCOPE.md
@@ -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
|
||||||
|
|||||||
@@ -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` (25–27)
|
`workplans/REUSE-WP-0015-federation-polish-and-planning-analytics.md` (25–27);
|
||||||
|
`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:**
|
||||||
@@ -235,4 +237,5 @@ See §4 and archived workplans `workplans/archived/`.
|
|||||||
| 2026-06-16 | WP-0014 closed priority 18; 60 workstation repos |
|
| 2026-06-16 | WP-0014 closed priority 18; 60 workstation repos |
|
||||||
| 2026-06-16 | **SCOPE refresh + full INTENT success-criteria mapping**; priorities 25–27 proposed |
|
| 2026-06-16 | **SCOPE refresh + full INTENT success-criteria mapping**; priorities 25–27 proposed |
|
||||||
| 2026-06-16 | Assessment persisted; **REUSE-WP-0015** created for priorities 25–27 |
|
| 2026-06-16 | Assessment persisted; **REUSE-WP-0015** created for priorities 25–27 |
|
||||||
| 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) |
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
299
reuse_surface/plan_check.py
Normal 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
|
||||||
103
reuse_surface/statehub_bridge.py
Normal file
103
reuse_surface/statehub_bridge.py
Normal 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]
|
||||||
54
schemas/plan-check-result.schema.json
Normal file
54
schemas/plan-check-result.schema.json
Normal 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"]}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
schemas/reuse-event.schema.json
Normal file
29
schemas/reuse-event.schema.json
Normal 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
169
specs/PlanCheck.md
Normal 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.22–0.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
198
tests/test_plan_check.py
Normal 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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
66
tests/test_statehub_bridge.py
Normal file
66
tests/test_statehub_bridge.py
Normal 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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user