#!/usr/bin/env python3 """Regenerate src/ratatoskr/web/static/persona_render_canon.json from the vendored Worldtree d2 render canons (docs/vendor/worldtree-persona-canon/). The web persona pane renders the CANONICAL affect->NL (mood word + relationship directive) BYTE-EXACT to what Worldtree injects into the agent's context. That render needs the relation canon parsed into per-band phrase maps; this script reparses the vendored raw canons into the flat form the browser JS consumes. Uses Worldtree's OWN loader (core.persona.stance_render.load_canon) as the authoritative parser, so the flat form can never drift from Worldtree's parsing semantics. Requires Worldtree's venv (pydantic etc.). Run when scripts/canonical_drift.py flags a canon bump: PYTHONPATH=~/development/Worldtree ~/development/Worldtree/.venv/bin/python \ scripts/build_persona_canon.py """ import json from pathlib import Path from core.persona.stance_render import load_canon # Worldtree (authoritative parser) ROOT = Path(__file__).resolve().parent.parent VENDOR = ROOT / "docs" / "vendor" / "worldtree-persona-canon" OUT = ROOT / "src" / "ratatoskr" / "web" / "static" / "persona_render_canon.json" canon = load_canon(str(VENDOR / "d2-render-canon-v1.json")) mood = json.loads((VENDOR / "d2-mood-render-canon-v1.json").read_text()) out = { "_source": "vendored from Worldtree core/persona/canon/{d2-mood-render-canon-v1,d2-render-canon-v1}.json", "_generated_by": "scripts/build_persona_canon.py (regen on canonical_drift flag)", "_render_path": "deterministic, no LLM; mirrors Worldtree describe_pad + render_d2_canonical byte-exact", "mood_grid": mood["describe_pad"]["valence_arousal_grid"], # Context-injection reconstruction (affect-egress-consumer-reference §2b/2c): the # hidden mood DIRECTIVE. occ_directives = per-OCC-type behavioral string + tier; # pad_band_fallback = the P×A-quadrant default when no emotion is salient. The # salience gate (emotion_salience) + full_only tiers drive which one fires — but # affect.emit is type-only (no intensity), so the consumer shows BOTH candidates. "mood_directive": { "salience": mood["thresholds"]["emotion_salience"], "pad_band_cutoff": mood["thresholds"]["pad_band_cutoff"], "full_only": mood["emotion_tiers"]["full_only"], "occ_directives": { t: {"directive": e["directive"], "tier": e["tier"]} for t, e in mood["derive_directive"]["occ_directives"].items() }, "pad_band_fallback": mood["pad_band_fallback"], }, "relation": { "trust_cuts": [list(c) for c in canon.trust_cuts], "warmth_cuts": [list(c) for c in canon.warmth_cuts], "agency_cuts": [list(c) for c in canon.agency_cuts], "warmth_phrase": canon.warmth_phrase, "warmth_beh": canon.warmth_beh, "agency_phrase": canon.agency_phrase, "agency_beh": canon.agency_beh, "history": canon.history, "prefix": "Use this graded relationship state: toward target, warmth is ", "tbeh": {"low_trust": "verify important claims before relying on them", "cold_warmth": "protect boundaries while staying useful", "default": "work from ordinary good faith"}, "cold_warmth_bands": ["distant", "cold", "hostile"], "high_conf_floor": 0.55, }, } OUT.write_text(json.dumps(out, indent=1) + "\n") print(f"wrote {OUT.relative_to(ROOT)}")