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.
This commit is contained in:
77
assessments/CX_AI_Diagnostic/diaglib/config.py
Normal file
77
assessments/CX_AI_Diagnostic/diaglib/config.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user