from __future__ import annotations from pathlib import Path import yaml from core.runner import run_skill_script_raw # Must be updated manually when catalog/*.yaml files change. _CATALOG_DATE = "2026-06-13" def assemble_spec_yaml( session_dir: Path, slug: str, route: dict, bind_results: dict, indicator_defs: dict, ) -> Path: """Assemble analysis_spec_.yaml from bind loop results. Returns the path to the written YAML file. Never reads catalog/*.yaml directly — definitions come from indicator_defs (state). """ in_scope_sectors = route.get("sectors", []) primary_sector = in_scope_sectors[0] if in_scope_sectors else "unknown" indicators: dict = {} for ind_id, bind in bind_results.items(): ind_def = indicator_defs.get(ind_id, {}) cluster = ind_def.get("cluster", primary_sector) entry: dict = { "label": ind_def.get("label", ind_id), "dimension": f"sector::{cluster}", "definition": ind_def.get("definition", ""), "measurable": bind["measurable"], "reasons": bind["reasons"], "variables": bind["variables"], # "Indicator name in the analysis" column. Code disposes: the result id is the # catalog slug, not whatever the bind call put in result_ids (it leaks dataset # variable names). The slug is the stable id the computed result carries. "result_ids": [ind_id], } indicators[ind_id] = entry spec = { "dataset": slug, "unit_of_analysis": "key_informant", "n_total": 0, "disaggregation": { "by": "analyst-specified", "groups": {}, "source_variables": [], "trigger": "analyst-specified", }, "step1_framework_route": { "sector": primary_sector, "pillar_2d": None, "subpillar_2d": None, "cross_cutting": None, }, "step2_catalog_version": _CATALOG_DATE, "indicators": indicators, "uncovered_modules": [], "gates": [], } out_path = session_dir / f"analysis_spec_{slug}.yaml" out_path.write_text( yaml.dump(spec, allow_unicode=True, sort_keys=False, default_flow_style=False) ) return out_path def render_spec_md(spec_yaml_path: Path, session_dir: Path) -> Path: """Render analysis_spec_.yaml to analysis_spec_.md. Calls render_spec.py with no -o flag so it writes markdown to stdout. Captures stdout and writes it to the session dir. Returns the path to the written .md file. """ md_content = run_skill_script_raw("render_spec.py", [str(spec_yaml_path)]) md_path = session_dir / spec_yaml_path.with_suffix(".md").name md_path.write_text(md_content) return md_path