From 3b909338cb8cd88ca8b436d55426706e7afa0036 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 27 Jun 2026 00:05:01 +0200 Subject: [PATCH] =?UTF-8?q?feat(explain):=20implement=20ATLAS-WP-0004=20T0?= =?UTF-8?q?1+T02=20=E2=80=94=20effective-config=20resolver=20+=20config=20?= =?UTF-8?q?explain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Activate WP-0003 and WP-0004. Add tools/effective_config.py (deterministic, order-independent override-path resolver — path only, never a value) and tools/config_explain.py + `make explain` to render the layer path, winning layer, validator, owner, consumers, and secret references for any surface.*. Verified on all 4 seeded surfaces; order-independent; no values/secrets leak. Co-Authored-By: Claude Opus 4.8 --- .claude/rules/stack-and-commands.md | 3 + Makefile | 9 +- tools/config_explain.py | 65 +++++++ tools/effective_config.py | 161 ++++++++++++++++++ .../ATLAS-WP-0003-discovery-connectors.md | 4 +- workplans/ATLAS-WP-0004-explain-and-graph.md | 21 ++- 6 files changed, 255 insertions(+), 8 deletions(-) create mode 100644 tools/config_explain.py create mode 100644 tools/effective_config.py diff --git a/.claude/rules/stack-and-commands.md b/.claude/rules/stack-and-commands.md index 49498cd..66501b9 100644 --- a/.claude/rules/stack-and-commands.md +++ b/.claude/rules/stack-and-commands.md @@ -20,6 +20,9 @@ python3 tools/validate_registry.py # schema + index consistency only git diff --check # whitespace / conflict markers reuse-surface validate --root . # capability federation (from reuse-surface checkout) +# Explain a surface's effective-config override path (no values shown) +make explain SURFACE=surface.infotech.state-hub.api-config + # After workplan or registry edits — from ~/state-hub make fix-consistency REPO=config-atlas ``` diff --git a/Makefile b/Makefile index 472a109..71a8350 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,12 @@ # config-atlas — registry validation gate (ATLAS-WP-0002-T06) -# Markdown-first repo: no build/run, only validation. +# Markdown-first repo: no build/run, only validation and the explain tool. -.PHONY: validate validate-schema validate-reuse validate-whitespace +.PHONY: validate validate-schema validate-reuse validate-whitespace explain + +# Effective-config override path for a surface (ATLAS-WP-0004). +# make explain SURFACE=surface.infotech.state-hub.api-config +explain: + @python3 tools/config_explain.py $(SURFACE) # Full gate run by agents and CI. validate: validate-schema validate-whitespace validate-reuse diff --git a/tools/config_explain.py b/tools/config_explain.py new file mode 100644 index 0000000..528cbe9 --- /dev/null +++ b/tools/config_explain.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""`config explain ` (ATLAS-WP-0004-T02). + +Render the effective-config override PATH for a configuration surface as a +human/agent-readable view. Shows which layer wins, what it overrode, the +validating schema, owner, and consumers -- never a resolved or secret value. + +Usage: + python3 tools/config_explain.py surface.infotech.state-hub.api-config + make explain SURFACE=surface.infotech.state-hub.api-config +""" +from __future__ import annotations + +import sys + +from effective_config import OverridePath, find_entry, resolve_path + + +def render(op: OverridePath) -> str: + lines: list[str] = [] + lines.append(f"config explain {op.surface_id}") + lines.append("") + lines.append(f" {op.name}") + lines.append(f" kind: {op.kind} owner: {op.owner} mutability: {op.mutability} class: {op.security_class}") + if op.allowed_layers: + lines.append(f" allowed layers: {', '.join(op.allowed_layers)} default: {op.default_layer}") + lines.append("") + lines.append(" effective layer path (most-specific wins):") + if not op.contributions: + lines.append(" (no sources declared)") + for c in op.contributions: + layer = c.layer if c.layer else f"[{c.role}]" + marker = " <== winning" if c.winning else "" + over = f" (overrides {c.overrides})" if c.overrides else "" + lines.append(f" {layer:<16} {c.ref}{over}{marker}") + lines.append("") + if op.validator: + lines.append(f" validated by: {op.validator}") + if op.consumers: + lines.append(f" consumed by: {', '.join(op.consumers)}") + if op.secret_deps: + lines.append(f" depends on secret (ref): {', '.join(op.secret_deps)}") + if op.related: + lines.append(f" related: {', '.join(op.related)}") + for note in op.notes: + lines.append(f" · {note}") + return "\n".join(lines) + + +def main(argv: list[str]) -> int: + if len(argv) != 1: + print(__doc__) + return 2 + surface_id = argv[0] + try: + entry = find_entry(surface_id) + except FileNotFoundError as exc: + print(f"error: {exc}", file=sys.stderr) + return 1 + print(render(resolve_path(entry))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/effective_config.py b/tools/effective_config.py new file mode 100644 index 0000000..52ac37b --- /dev/null +++ b/tools/effective_config.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +"""Effective-config path resolver (ATLAS-WP-0004-T01). + +Given a configuration-surface entry, compute the *override path*: the ordered +contributing layers, which layer wins, what each overrode, the validating schema, +and the owner. This renders the PATH only -- never a resolved or live value, and +never a secret value (config-atlas stores the map, not the value; PRD FR-5). + +The resolver is deterministic and order-independent: the result depends only on +each source's layer position in the canonical L0-L9 ordering, not on the order +sources happen to appear in the entry. +""" +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from pathlib import Path + +try: + import yaml +except ImportError as exc: # pragma: no cover + raise SystemExit(f"setup error: missing PyYAML ({exc}). pip install pyyaml") + +# Canonical L0-L9 ordering (must match schemas/surface-entry.schema.json $defs/layer). +LAYER_ORDER = [ + "product-default", "company", "platform", "environment", "region", + "installation", "tenant", "group", "user", "agent", "emergency", +] +LAYER_INDEX = {name: i for i, name in enumerate(LAYER_ORDER)} + +ROOT = Path(__file__).resolve().parent.parent +SURFACES_DIR = ROOT / "registry" / "surfaces" +FRONTMATTER = re.compile(r"^---\n(.*?)\n---\n", re.S) + +# Roles that contribute no orderable layer (linked authority, not an overlay). +NON_LAYER_ROLES = {"feature-control-key"} + + +@dataclass +class Contribution: + """One source's contribution to the override path.""" + role: str + layer: str | None # canonical layer, or None for non-layer roles + rank: int # position in LAYER_ORDER, or -1 + ref: str # repo:path / endpoint reference (never a value) + overrides: str | None = None # the lower layer this one overrides, if any + winning: bool = False + + +@dataclass +class OverridePath: + surface_id: str + name: str + kind: str + owner: str + mutability: str + security_class: str + default_layer: str | None + allowed_layers: list[str] + validator: str | None + contributions: list[Contribution] = field(default_factory=list) + consumers: list[str] = field(default_factory=list) + secret_deps: list[str] = field(default_factory=list) + related: list[str] = field(default_factory=list) + notes: list[str] = field(default_factory=list) + + @property + def winner(self) -> Contribution | None: + return next((c for c in self.contributions if c.winning), None) + + +def _layer_for_role(role: str) -> str | None: + """Map a source role (e.g. 'environment-overlay') to a canonical layer. + + Strategy: longest canonical layer name that the role starts with or contains. + Non-layer roles (feature-control-key) return None. + """ + if role in NON_LAYER_ROLES: + return None + for layer in sorted(LAYER_ORDER, key=len, reverse=True): + if role == layer or role.startswith(layer + "-") or layer in role.split("-"): + return layer + return None + + +def load_entry(path: Path) -> dict: + m = FRONTMATTER.match(path.read_text()) + if not m: + raise ValueError(f"{path}: missing YAML frontmatter") + data = yaml.safe_load(m.group(1)) + if not isinstance(data, dict): + raise ValueError(f"{path}: frontmatter is not a mapping") + return data + + +def find_entry(surface_id: str) -> dict: + path = SURFACES_DIR / f"{surface_id}.md" + if not path.exists(): + raise FileNotFoundError(f"no surface entry for id '{surface_id}' at {path}") + return load_entry(path) + + +def resolve_path(entry: dict) -> OverridePath: + """Compute the override path for a surface entry. Pure / deterministic.""" + scope = entry.get("scope", {}) or {} + schema = entry.get("schema", {}) or {} + relations = entry.get("relations", {}) or {} + + op = OverridePath( + surface_id=entry["id"], + name=entry.get("name", entry["id"]), + kind=entry.get("kind", "?"), + owner=entry.get("owner", "?"), + mutability=entry.get("mutability", "?"), + security_class=entry.get("security_class", "?"), + default_layer=scope.get("default_layer"), + allowed_layers=list(scope.get("allowed_layers", [])), + validator=schema.get("validator"), + consumers=list(relations.get("consumed_by", [])), + secret_deps=list(relations.get("depends_on_secret", [])), + related=list(relations.get("related_to", [])), + ) + + contribs: list[Contribution] = [] + for src in entry.get("sources", []) or []: + role = src.get("role", "?") + ref = src.get("repo", "") + (":" if src.get("repo") and src.get("path") else "") + (src.get("path") or src.get("endpoint") or "") + layer = _layer_for_role(role) + rank = LAYER_INDEX.get(layer, -1) if layer else -1 + contribs.append(Contribution(role=role, layer=layer, rank=rank, ref=ref or role)) + + # Deterministic, order-independent: sort by layer rank, then ref for ties. + layered = sorted([c for c in contribs if c.rank >= 0], key=lambda c: (c.rank, c.ref)) + non_layered = [c for c in contribs if c.rank < 0] + + # Each higher layer overrides the previous lower one; the last (most specific) wins. + for i, c in enumerate(layered): + if i > 0: + c.overrides = layered[i - 1].layer + if layered: + layered[-1].winning = True + + op.contributions = layered + non_layered + + if non_layered: + op.notes.append( + "linked authority (no overlay ordering): " + + ", ".join(f"{c.role} -> {c.ref}" for c in non_layered) + ) + if op.security_class == "secret-ref" or op.secret_deps: + op.notes.append("secret values are referenced only, never resolved here") + op.notes.append("no values shown — config-atlas maps the surface, not the value") + return op + + +if __name__ == "__main__": # pragma: no cover - smoke check + import sys + sid = sys.argv[1] if len(sys.argv) > 1 else "surface.infotech.state-hub.api-config" + p = resolve_path(find_entry(sid)) + w = p.winner + print(p.surface_id, "->", "winning:", w.layer if w else "(none)") diff --git a/workplans/ATLAS-WP-0003-discovery-connectors.md b/workplans/ATLAS-WP-0003-discovery-connectors.md index 929dbb3..db3aed1 100644 --- a/workplans/ATLAS-WP-0003-discovery-connectors.md +++ b/workplans/ATLAS-WP-0003-discovery-connectors.md @@ -4,11 +4,11 @@ type: workplan title: "Discovery connectors" domain: infotech repo: config-atlas -status: ready +status: active owner: codex topic_slug: custodian created: "2026-06-26" -updated: "2026-06-26" +updated: "2026-06-27" state_hub_workstream_id: "e4400d9c-021a-4e44-8e9b-719f94a9561a" --- diff --git a/workplans/ATLAS-WP-0004-explain-and-graph.md b/workplans/ATLAS-WP-0004-explain-and-graph.md index df7dd6f..de63384 100644 --- a/workplans/ATLAS-WP-0004-explain-and-graph.md +++ b/workplans/ATLAS-WP-0004-explain-and-graph.md @@ -4,11 +4,11 @@ type: workplan title: "Effective-config explain and graph" domain: infotech repo: config-atlas -status: ready +status: active owner: codex topic_slug: custodian created: "2026-06-26" -updated: "2026-06-26" +updated: "2026-06-27" state_hub_workstream_id: "fbfdbf2b-ca6b-450e-a654-a61c5939f068" --- @@ -44,11 +44,18 @@ T04 (blast-radius). T01 and T03 may start in parallel. ```task id: ATLAS-WP-0004-T01 -status: todo +status: done priority: high state_hub_task_id: "cee293aa-b407-4b97-a462-b67d7aa0f170" ``` +Result 2026-06-27: Added `tools/effective_config.py` — a pure, deterministic +resolver that orders a surface's `sources[]` by canonical L0-L9 layer rank and +emits the override path (contributing layers, winning layer, what each overrode, +validator, owner). Renders the PATH only, never a value; non-layer roles +(feature-control-key) are listed as linked authority. Verified order-independent +(8x shuffle -> identical path). + Implement a static resolver that, from a surface's `sources[]` (with layer `role`), the L0–L9 ordering, and the explicit merge rules, produces the **override path**: ordered contributing layers, the winning layer, what each overrode, the validating @@ -63,11 +70,17 @@ the `config explain` shape in [`wiki/ConfigLayering.md`](../wiki/ConfigLayering. ```task id: ATLAS-WP-0004-T02 -status: todo +status: done priority: high state_hub_task_id: "5d66a8c1-74bd-4c6f-9ea6-839e884ad103" ``` +Result 2026-06-27: Added `tools/config_explain.py` and `make explain +SURFACE=...`. Renders the resolver output (layer path, overrides, winning layer, +validator, owner, consumers, secret refs) for any `surface.*` id. Verified on all +4 seeded surfaces; no values or secret values appear in output. Documented in +`.claude/rules/stack-and-commands.md`. + Add a `tools/` command (e.g. `config_explain.py`) that takes a `surface.*` id and renders the resolver output as the human/agent-readable `config explain` view. Reuse the existing `tools/` + Makefile pattern (`make explain SURFACE=...`).