Files
palladium/assessments/CX_AI_Diagnostic/diaglib/config.py
Robert Helewka a967f73d09 feat: add master notebook library scaffolding and review tooling
Add CLAUDE.md defining the Palladium master notebook conventions and
Red Panda Approval criteria, plus a review-notebook slash command for
LLM-driven notebook review.

Expand .gitignore to block client/engagement documents and generated
exports, keeping masters client-clean while allowing text/image sources.

Normalize slider widget numeric values from floats to integers in
notebook JSON.
2026-07-31 16:16:07 +00:00

78 lines
2.9 KiB
Python

"""Load and merge ``configs/*.yaml`` into a validated DiagnosticConfig.
``base.yaml`` holds the industry-independent competency model; every other
YAML in the directory is an industry overlay declaring ``extends: base``
plus its value drivers and unlock costs. Merging is shallow and explicit:
the overlay contributes industry identity, drivers, and costs; the base
contributes everything else. Overlays may not redefine the competency
model — one instrument, many industries.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml
from .models import DiagnosticConfig
#: Overlay keys an industry file may set. Anything else (competencies,
#: capping_heuristic, …) belongs in base.yaml and is rejected loudly.
_OVERLAY_KEYS = {"extends", "industry", "display_name", "value_drivers", "unlock_costs"}
def configs_dir(start: Path | None = None) -> Path:
"""The study's ``configs/`` directory, found from ``start`` (or CWD).
Walks up so it works from the study root, ``notebooks/``, or ``tests/``.
"""
here = (start or Path.cwd()).resolve()
for candidate in (here, *here.parents):
d = candidate / "configs"
if (d / "base.yaml").exists():
return d
raise FileNotFoundError("configs/base.yaml not found above " + str(here))
def list_industries(directory: Path | None = None) -> list[str]:
"""Industry config names (file stems), base excluded, sorted."""
d = directory or configs_dir()
return sorted(p.stem for p in d.glob("*.yaml") if p.stem != "base")
def _read_yaml(path: Path) -> dict[str, Any]:
with open(path, encoding="utf-8") as fh:
data = yaml.safe_load(fh)
if not isinstance(data, dict):
raise ValueError(f"{path.name}: expected a mapping at top level")
return data
def load_config(industry: str, directory: Path | None = None) -> DiagnosticConfig:
"""Load ``base.yaml`` + the named industry overlay, validated."""
d = directory or configs_dir()
base = _read_yaml(d / "base.yaml")
overlay = _read_yaml(d / f"{industry}.yaml")
if overlay.get("extends") != "base":
raise ValueError(f"{industry}.yaml must declare 'extends: base'")
stray = set(overlay) - _OVERLAY_KEYS
if stray:
raise ValueError(
f"{industry}.yaml sets base-only keys {sorted(stray)}"
"the competency model lives in base.yaml")
merged: dict[str, Any] = {
"version": base["version"],
"dimensions": base["dimensions"],
"competencies": base["competencies"],
"capping_heuristic": base["capping_heuristic"],
"foundational_competencies": base["foundational_competencies"],
"industry": overlay["industry"],
"display_name": overlay.get("display_name", overlay["industry"]),
"value_drivers": overlay.get("value_drivers") or [],
"unlock_costs": overlay.get("unlock_costs") or {},
}
return DiagnosticConfig.model_validate(merged)