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.
78 lines
2.9 KiB
Python
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)
|