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:
101
tools/connector_reposcoping.py
Normal file
101
tools/connector_reposcoping.py
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""repo-scoping fact ingestion connector (ATLAS-WP-0003-T02).
|
||||
|
||||
Consume repo-scoping observed facts/evidence as connector input and emit candidate
|
||||
configuration surfaces, adding only config-kind/layer classification on top
|
||||
(ecosystem-boundaries §2.4 option a). Read-only: zero writes to repo-scoping or the
|
||||
scanned repo.
|
||||
|
||||
Source resolution (first available):
|
||||
1. --facts <file.json> : a repo-scoping facts export (list of fact objects)
|
||||
2. REPO_SCOPING_URL env : GET {url}/repos/{slug}/facts
|
||||
Degrades gracefully (emits nothing) when no source is available.
|
||||
|
||||
Usage:
|
||||
python3 tools/connector_reposcoping.py <repo-slug> [--facts facts.json]
|
||||
make connect-reposcoping REPO=state-hub
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from connector_base import run_connector
|
||||
|
||||
CONFIG_HINTS = ("config", "env", "settings", "values", ".yaml", ".yml", ".toml", ".ini")
|
||||
|
||||
|
||||
def _load_facts(slug: str, facts_file: str | None) -> list[dict]:
|
||||
if facts_file:
|
||||
p = Path(facts_file)
|
||||
if p.exists():
|
||||
data = json.loads(p.read_text())
|
||||
return data if isinstance(data, list) else data.get("facts", [])
|
||||
print(f"repo-scoping: facts file not found: {facts_file}", file=sys.stderr)
|
||||
return []
|
||||
url = os.environ.get("REPO_SCOPING_URL")
|
||||
if url:
|
||||
try:
|
||||
import urllib.request
|
||||
with urllib.request.urlopen(f"{url}/repos/{slug}/facts", timeout=5) as r:
|
||||
data = json.loads(r.read())
|
||||
return data if isinstance(data, list) else data.get("facts", [])
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"repo-scoping: API unavailable ({exc})", file=sys.stderr)
|
||||
return []
|
||||
print("repo-scoping: no --facts file and REPO_SCOPING_URL unset; nothing to ingest")
|
||||
return []
|
||||
|
||||
|
||||
def _is_config_fact(fact: dict) -> bool:
|
||||
blob = (str(fact.get("path", "")) + " " + str(fact.get("kind", "")) + " "
|
||||
+ str(fact.get("summary", ""))).lower()
|
||||
return any(h in blob for h in CONFIG_HINTS)
|
||||
|
||||
|
||||
def facts_to_candidates(slug: str, facts: list[dict]) -> list[tuple[dict, str]]:
|
||||
out: list[tuple[dict, str]] = []
|
||||
for fact in facts:
|
||||
if not _is_config_fact(fact):
|
||||
continue
|
||||
rel = str(fact.get("path", "")).strip("/")
|
||||
if not rel:
|
||||
continue
|
||||
stem = rel.replace("/", "-").replace(".", "-").replace("_", "-").lower()
|
||||
sid = f"surface.infotech.{slug}.{stem}"
|
||||
entry = {
|
||||
"id": sid,
|
||||
"name": f"{slug}: {rel}",
|
||||
"kind": "app-config",
|
||||
"summary": fact.get("summary") or f"Config surface from repo-scoping fact at {rel}.",
|
||||
"owner": slug,
|
||||
"scope": {"allowed_layers": ["company", "environment", "installation"],
|
||||
"default_layer": "company"},
|
||||
"mutability": "deploy-time",
|
||||
"security_class": "operational",
|
||||
"sources": [{"repo": slug, "path": rel, "role": "company-baseline"}],
|
||||
"evidence": {"discovery_method": "connector:repo-scoping",
|
||||
"change_log_ref": str(fact.get("id", ""))},
|
||||
}
|
||||
out.append((entry, f"Ingested from repo-scoping fact `{fact.get('id','?')}` "
|
||||
f"({rel}). Classify kind/scope and promote or reject.\n"))
|
||||
return out
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
if not argv:
|
||||
print(__doc__)
|
||||
return 2
|
||||
slug = argv[0]
|
||||
facts_file = None
|
||||
if "--facts" in argv:
|
||||
i = argv.index("--facts")
|
||||
facts_file = argv[i + 1] if i + 1 < len(argv) else None
|
||||
return run_connector("repo-scoping", facts_to_candidates(slug, _load_facts(slug, facts_file)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user