generated from coulomb/repo-seed
feat(connectors): complete ATLAS-WP-0003 — discovery connectors (Phase 2)
Some checks failed
validate-registry / validate (push) Has been cancelled
Some checks failed
validate-registry / validate (push) Has been cancelled
T01 connector_base + docs/discovery-connectors.md (read-only/stateless, candidate->PR->promote; `candidate` added to schema status enum; candidates/ gitignored, excluded from gate). T02 connector_reposcoping (repo-scoping facts -> candidates; graceful degrade). T03 connector_gitconfig (deterministic scan; real .env -> secret-ref, no values; verified 4 real candidates from ~/state-hub). T04 connector_featurecontrol (feature-flag surfaces linking to feature-control keys, no eval logic; FR-12). T05 registry_health (unowned + stale detection). Make targets: connect-gitconfig/reposcoping/featurecontrol, registry-health. WP-0003 finished (5/5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
91
tools/connector_base.py
Normal file
91
tools/connector_base.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Shared base for read-only discovery connectors (ATLAS-WP-0003).
|
||||
|
||||
A connector scans a source and emits *candidate* surface entries for human/agent
|
||||
PR review. Connectors are stateless and read-only: they NEVER write a source
|
||||
system, NEVER auto-merge, and NEVER read or store configuration values or secret
|
||||
values (PRD FR-8; docs/discovery-connectors.md).
|
||||
|
||||
Candidates are written to registry/surfaces/candidates/<id>.md with
|
||||
`status: candidate` and provenance in `evidence`. A candidate is never written if a
|
||||
promoted entry with the same id already exists (the registry is the source of
|
||||
truth; connectors propose, they do not overwrite).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as _dt
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
from jsonschema import Draft202012Validator
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(f"setup error: missing dependency ({exc}). pip install pyyaml jsonschema")
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCHEMA_PATH = ROOT / "schemas" / "surface-entry.schema.json"
|
||||
SURFACES_DIR = ROOT / "registry" / "surfaces"
|
||||
CANDIDATES_DIR = SURFACES_DIR / "candidates"
|
||||
|
||||
_VALIDATOR = Draft202012Validator(json.loads(SCHEMA_PATH.read_text()))
|
||||
TODAY = _dt.date.today().isoformat()
|
||||
|
||||
|
||||
def promoted_ids() -> set[str]:
|
||||
"""Ids of already-promoted (non-candidate) surface entries."""
|
||||
return {p.stem for p in SURFACES_DIR.glob("*.md")}
|
||||
|
||||
|
||||
def validate_entry(entry: dict) -> list[str]:
|
||||
return [f"{'/'.join(str(p) for p in e.path) or '(root)'}: {e.message}"
|
||||
for e in _VALIDATOR.iter_errors(entry)]
|
||||
|
||||
|
||||
def emit_candidate(entry: dict, *, connector: str, body: str = "") -> tuple[str, Path | None]:
|
||||
"""Validate and write one candidate. Returns (status_message, path|None).
|
||||
|
||||
status_message is one of: 'written', 'skipped (promoted)', 'invalid: ...'.
|
||||
"""
|
||||
entry = dict(entry)
|
||||
entry["status"] = "candidate"
|
||||
ev = dict(entry.get("evidence", {}) or {})
|
||||
ev.setdefault("discovery_method", f"connector:{connector}")
|
||||
ev.setdefault("last_seen", TODAY)
|
||||
entry["evidence"] = ev
|
||||
|
||||
sid = entry.get("id", "<no-id>")
|
||||
if sid in promoted_ids():
|
||||
return (f"skipped (promoted): {sid}", None)
|
||||
|
||||
errs = validate_entry(entry)
|
||||
if errs:
|
||||
return (f"invalid: {sid}: {errs[0]}", None)
|
||||
|
||||
CANDIDATES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
fm = yaml.safe_dump(entry, sort_keys=False).strip()
|
||||
text = f"---\n{fm}\n---\n\n# {entry.get('name', sid)} (candidate)\n\n"
|
||||
text += body or (
|
||||
f"Discovered by `{connector}`. Review, refine, and promote to "
|
||||
f"`registry/surfaces/{sid}.md` + `surfaces.yaml`, or reject.\n"
|
||||
)
|
||||
path = CANDIDATES_DIR / f"{sid}.md"
|
||||
path.write_text(text)
|
||||
return (f"written: {sid}", path)
|
||||
|
||||
|
||||
def run_connector(name: str, candidates: list[tuple[dict, str]]) -> int:
|
||||
"""Emit a batch; print a summary. candidates = list of (entry, body)."""
|
||||
if not candidates:
|
||||
print(f"{name}: no candidates discovered (source empty or unavailable)")
|
||||
return 0
|
||||
written = skipped = invalid = 0
|
||||
for entry, body in candidates:
|
||||
msg, _ = emit_candidate(entry, connector=name, body=body)
|
||||
print(f" {msg}")
|
||||
written += msg.startswith("written")
|
||||
skipped += msg.startswith("skipped")
|
||||
invalid += msg.startswith("invalid")
|
||||
print(f"{name}: {written} written, {skipped} skipped, {invalid} invalid "
|
||||
f"-> registry/surfaces/candidates/")
|
||||
return 1 if invalid else 0
|
||||
Reference in New Issue
Block a user