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:
85
tools/connector_featurecontrol.py
Normal file
85
tools/connector_featurecontrol.py
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""feature-control flag connector (ATLAS-WP-0003-T04).
|
||||
|
||||
Inventory feature-control keys and emit `feature-flag` surfaces that LINK to the
|
||||
authoritative feature-control key (`sources[].role: feature-control-key`) and
|
||||
contain no evaluation logic (PRD FR-12 delegation boundary). Read-only.
|
||||
|
||||
Source resolution (first available):
|
||||
1. --keys <file> : newline- or yaml-list of feature keys
|
||||
2. ~/feature-control/registry/indexes/feature-keys.yaml (if present)
|
||||
Degrades gracefully when feature-control has no key registry yet (planning phase).
|
||||
|
||||
Usage:
|
||||
python3 tools/connector_featurecontrol.py [--keys keys.yaml]
|
||||
make connect-featurecontrol
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit(f"setup error: missing PyYAML ({exc})")
|
||||
|
||||
from connector_base import run_connector
|
||||
|
||||
FC_KEYS = Path.home() / "feature-control" / "registry" / "indexes" / "feature-keys.yaml"
|
||||
|
||||
|
||||
def _load_keys(keys_file: str | None) -> list[str]:
|
||||
src = Path(keys_file) if keys_file else FC_KEYS
|
||||
if not src.exists():
|
||||
print(f"feature-control: no key registry at {src} (planning phase — none yet)")
|
||||
return []
|
||||
raw = src.read_text()
|
||||
try:
|
||||
data = yaml.safe_load(raw)
|
||||
except yaml.YAMLError:
|
||||
data = None
|
||||
if isinstance(data, dict):
|
||||
keys = data.get("keys") or data.get("feature_keys") or []
|
||||
elif isinstance(data, list):
|
||||
keys = data
|
||||
else:
|
||||
keys = [ln.strip() for ln in raw.splitlines() if ln.strip() and not ln.startswith("#")]
|
||||
return [str(k) for k in keys]
|
||||
|
||||
|
||||
def keys_to_candidates(keys: list[str]) -> list[tuple[dict, str]]:
|
||||
out: list[tuple[dict, str]] = []
|
||||
for key in keys:
|
||||
slug = key.replace(".", "-").replace("_", "-").lower()
|
||||
sid = f"surface.infotech.feature-control.{slug}"
|
||||
entry = {
|
||||
"id": sid,
|
||||
"name": f"feature flag: {key}",
|
||||
"kind": "feature-flag",
|
||||
"summary": f"Runtime feature availability controlled by feature-control key `{key}`.",
|
||||
"owner": "feature-control",
|
||||
"scope": {"allowed_layers": ["company", "environment", "tenant", "user"],
|
||||
"default_layer": "company"},
|
||||
"mutability": "hot-reloadable",
|
||||
"security_class": "operational",
|
||||
"sources": [{"repo": "feature-control", "endpoint": f"openfeature:{key}",
|
||||
"role": "feature-control-key"}],
|
||||
"relations": {"related_to": []},
|
||||
}
|
||||
out.append((entry, f"Links to feature-control key `{key}`. config-atlas maps "
|
||||
f"the flag; feature-control owns evaluation. Promote or reject.\n"))
|
||||
return out
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
keys_file = None
|
||||
if "--keys" in argv:
|
||||
i = argv.index("--keys")
|
||||
keys_file = argv[i + 1] if i + 1 < len(argv) else None
|
||||
return run_connector("feature-control", keys_to_candidates(_load_keys(keys_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