Files
Claude 61caad16cb feat: Phases 3-5 — generation v2, fleet ops, api-spec IR, MCP serve fleet-wide
Phase 3 (Generation v2):
- scaffold-cli.py migrated to Jinja2 (${} delimiters preserve existing
  templates; StrictUndefined; TEMPLATE_VERSION 2.0.0; .manifest.json
  provenance emission)
- auth.py.tpl unified (google-sso via {% if %} block; auth_google_sso.py.tpl
  deleted); anchor-based method injection replaced by template conditionals
- New scaffolds: command_group.py.tpl (--resource flag), test_e2e.py.tpl
  (canonical subprocess fixtures), README.md.tpl + SKILL.md.tpl (replace
  dead Handlebars .template files, now actually rendered)
- 21-profile golden test matrix (protocol x http_client x auth) — compile-
  clean, no placeholder leaks, profile-correct deps/files
- Skills consolidation: skills/shared/CONVENTIONS.md (single definition of
  every implementation rule) + RECOVERY.md (per-gate failure decision
  trees); boilerplate skill 829 -> 106 lines; HARNESS.md now navigational;
  reviewer agents de-overlapped; gap-analyzer mandatory first step of
  /refine; quality checklist tiered (38 Tier-1 critical checks) with
  --tier1-only/--strict in validate-checklist.py

Phase 4 (Fleet ops):
- cli-web-devkit canary: registry-declared read-only live-site smoke
  commands (12 no-auth CLIs) + daily canary.yml workflow that auto-files
  site-breakage issues
- contract.yml workflow: fleet contract suite + vendored-file drift on PR
- release-please switched to manifest/monorepo mode (root + cli-web-core +
  devkit packages, per-package changelogs); publish.yml with PyPI trusted
  publishing (setup guide in docs/PUBLISHING.md)
- cli-web-devkit docs: README fleet table + install block generated from
  registry.json between markers, --check freshness gate in CI (caught the
  install list missing linkedin)

Phase 5 (Product leaps):
- MCP SERVE FLEET-WIDE: cli_web_core/mcp_server.py adapter (vendored to all
  19 CLIs, synced by resync) exposes every Click command as a typed MCP
  tool over stdio; mcp-serve wired into all 19 entry points;
  contract-tested handshake x19
- api-spec.json IR: evidence-required endpoint schema + cli-web-devkit
  spec validate; wired into methodology Phase 2 (structural enforcement of
  never-invent-endpoints)
- cli-web-devkit gaps: deterministic captured-vs-implemented-vs-exposed
  diff (AST-based), wired into gap-analyzer skill Step 0
- Capture quality scoring (0-100) in validate-capture report

Fleet contract fixes found by the new gates:
- gh-trending emitted bare JSON arrays instead of the success envelope
  (fixed + e2e updated; stale version assertion fixed)
- notebooklm/stitch/codewiki/chatgpt lacked --version (fixed in Phase 2
  commit); booking registry commands corrected

Verified: 514 plugin/devkit/core tests, 95 fleet contract tests (incl. 19
MCP handshakes), 19/19 CLI unit suites, gh-trending e2e live, ruff+format
clean (512 files), mypy strict clean, verify-plugin 28/28, drift 39/39 ok,
registry + README freshness gates green.

https://claude.ai/code/session_017wCNXuLy6UpGJ1jcPdsD5i
2026-06-09 22:54:20 +00:00

109 lines
3.9 KiB
Python

"""``api-spec.json`` — the typed intermediate representation per CLI.
Phase 2 of the pipeline produces this machine-readable spec from captured
traffic (instead of prose alone). Everything downstream derives from it:
client method stubs, command skeletons, REPL help, the gap analyzer, and
the traffic-fidelity review (which becomes a deterministic spec-vs-traffic
diff with provenance links into ``raw-traffic.json``).
Format: JSON (stdlib-parseable everywhere — devkit has no dependencies).
Schema (version 1)::
{
"spec_version": 1,
"app": "hackernews",
"protocol": "rest",
"auth": {"type": "cookie", "required_cookies": ["user"]},
"endpoints": [
{
"id": "search_stories",
"method": "GET",
"url": "https://hn.algolia.com/api/v1/search",
"params": {"query": {"type": "str", "required": true}},
"evidence": "raw-traffic.json#42"
}
],
"commands": [
{"name": "search stories", "endpoint": "search_stories"}
]
}
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
SPEC_VERSION = 1
SPEC_FILENAME = "api-spec.json"
_VALID_PROTOCOLS = {"rest", "graphql", "html-scraping", "batchexecute", "browser"}
_VALID_METHODS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"}
_VALID_AUTH = {"none", "cookie", "api-key", "google_sso", "browser"}
def validate_spec(raw: dict[str, Any]) -> list[str]:
"""Return a list of problems (empty == valid)."""
problems: list[str] = []
if raw.get("spec_version") != SPEC_VERSION:
problems.append(f"spec_version must be {SPEC_VERSION}, got {raw.get('spec_version')!r}")
if not raw.get("app"):
problems.append("missing 'app'")
if raw.get("protocol") not in _VALID_PROTOCOLS:
problems.append(f"protocol must be one of {sorted(_VALID_PROTOCOLS)}")
auth = raw.get("auth")
if not isinstance(auth, dict) or auth.get("type") not in _VALID_AUTH:
problems.append(f"auth.type must be one of {sorted(_VALID_AUTH)}")
endpoint_ids: set[str] = set()
endpoints = raw.get("endpoints")
if not isinstance(endpoints, list) or not endpoints:
problems.append("endpoints must be a non-empty list")
endpoints = []
for i, ep in enumerate(endpoints):
where = f"endpoints[{i}]"
if not isinstance(ep, dict):
problems.append(f"{where}: not an object")
continue
ep_id = ep.get("id")
if not ep_id:
problems.append(f"{where}: missing id")
elif ep_id in endpoint_ids:
problems.append(f"{where}: duplicate id {ep_id!r}")
else:
endpoint_ids.add(ep_id)
if ep.get("method") not in _VALID_METHODS:
problems.append(f"{where}: invalid method {ep.get('method')!r}")
if not ep.get("url"):
problems.append(f"{where}: missing url")
if not ep.get("evidence"):
problems.append(
f"{where}: missing evidence — every endpoint must cite its captured "
"traffic entry (raw-traffic.json#<index>); never invent endpoints"
)
for i, cmd in enumerate(raw.get("commands", [])):
where = f"commands[{i}]"
if not isinstance(cmd, dict) or not cmd.get("name"):
problems.append(f"{where}: missing name")
continue
endpoint_ref = cmd.get("endpoint")
if endpoint_ref and endpoint_ref not in endpoint_ids:
problems.append(f"{where}: references unknown endpoint {endpoint_ref!r}")
return problems
def validate_file(path: Path) -> list[str]:
try:
raw = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
return [f"{path}: unreadable: {exc}"]
if not isinstance(raw, dict):
return [f"{path}: spec must be a JSON object"]
return validate_spec(raw)