mirror of
https://github.com/BillyOutlast/CLI-Anything-WEB.git
synced 2026-10-12 02:24:59 +00:00
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
109 lines
3.9 KiB
Python
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)
|