$ cat node-template.py

H

Human Codex Assemble

// Deterministic final stage of the Human Codex interview pipeline (no LLM). Parses the three pass outputs (Pass 1 per_item array, Pass 3 analysis object, Pass 4 presentation object), tolerating code fences and surrounding prose, assembles the single schema v2.0 profile JSON {schema_version, per_item, analysis, presentation}, renders a readable second-person Markdown report from the presentation layer, and reports validation notes (OK, or WARNING/ERROR lines naming the pass to re-run). Fails when analysis or presentation is not valid JSON.

Process
Document
#human-codex#personality#profile#assemble#json#markdown#report#deterministic
template.py
1"""2Human Codex Assemble34Deterministic, LLM-free final stage of the Human Codex four-pass interview5pipeline:6  - parses the three LLM outputs (Pass 1 per_item array, Pass 3 analysis7    object, Pass 4 presentation object), tolerating code fences and8    surrounding prose,9  - assembles the single schema v2.0 JSON object10    {schema_version, per_item, analysis, presentation},11  - renders a readable Markdown report from the presentation layer12    (plus the epistemics block of analysis),13  - reports validation notes (missing keys, unparseable passes) so the14    driving agent can re-run a pass instead of shipping a broken file.1516Hard failure (exit 1) only when analysis or presentation cannot be parsed:17those two are the substance of the deliverable. An unparseable per_item18array degrades to [] with a warning.19"""20from __future__ import annotations2122import json23import re24import sys25from datetime import date2627TOP_KEYS_ANALYSIS = [28    "context", "dimensions", "edges_active", "configurations", "coherence",29    "self_model", "operating_model", "forecast", "leverage_path", "epistemics",30]31TOP_KEYS_PRESENTATION = [32    "headline", "codex_map_3d", "axis_map", "operating_snapshot",33    "operating_conditions", "self_model_panel", "pattern_cards", "shadow_panel",34    "strengths", "growth_edges", "forecast_panel", "actionable_intelligence",35    "evidence_signals", "map_edges_note", "derived_metrics", "radar_scores", "meta",36]3738HEADINGS = {39    "en": {40        "title": "Human Codex Profile", "date": "Interview date", "mode": "Interview mode",41        "frame": "This is a non-clinical self-reflection instrument. It is a profile of pressures, not a prediction, and it does not diagnose, treat, or replace professional care. Every reading below is a hypothesis with stated evidence, confidence, and limits, and you retain the right to question and contest it.",42        "crisis": "Practitioner notice",43        "headline": "Headline", "thesis": "Organizing thesis",44        "axes": "The four axes", "strength": "Strength", "growth": "Growth", "shadow": "Shadow", "reflection": "Reflection",45        "snapshot": "Operating snapshot", "strengths": "Strengths", "growth_edges": "Growth edges",46        "patterns": "Pattern cards", "shadow_panel": "Shadow and subconscious", "conditions": "Operating conditions",47        "resourced": "When resourced", "depleted": "When depleted", "flips": "Flips when",48        "self_model": "How accurately you see yourself", "clear": "You see clearly", "overrate": "You may overrate", "blind": "Blind spots",49        "forecast": "Trajectory and pressures", "when": "When", "tends": "Tends toward", "horizon": "Horizon", "shift": "What would shift it",50        "actions": "Actionable intelligence", "why": "Why", "first": "Do first because", "unlocks": "Then unlocks",51        "evidence": "Evidence signals", "map_edges": "Where this map runs out", "codex_map": "Codex map",52        "node": "Dimension", "layer": "Layer", "strength_col": "Strength", "position": "Position", "durability": "Durability", "confidence": "Confidence",53        "edges": "Active relations", "metrics": "Derived metrics", "radar": "Radar scores",54        "epistemics": "Epistemics", "overall_confidence": "Overall confidence", "data_quality": "Data quality",55        "highest": "Highest-confidence findings", "lowest": "Lowest-confidence findings", "limits": "Limitations",56        "cultural": "Cultural bias flags", "follow_up": "Recommended follow-up", "meta": "Meta",57        "depth": "Analysis depth", "confidence_score": "Confidence score", "signals_used": "Signals used",58        "raises": "raises", "lowers": "lowers",59    },60    "it": {61        "title": "Profilo Human Codex", "date": "Data dell'intervista", "mode": "Modalità di intervista",62        "frame": "Questo è uno strumento non clinico di auto-riflessione. È un profilo di pressioni, non una previsione, e non diagnostica, non cura e non sostituisce un percorso professionale. Ogni lettura qui sotto è un'ipotesi con evidenze, confidenza e limiti dichiarati, e mantieni il diritto di metterla in discussione e contestarla.",63        "crisis": "Avviso per il professionista",64        "headline": "In sintesi", "thesis": "Tesi organizzante",65        "axes": "I quattro assi", "strength": "Forza", "growth": "Crescita", "shadow": "Ombra", "reflection": "Riflessione",66        "snapshot": "Istantanea operativa", "strengths": "Punti di forza", "growth_edges": "Margini di crescita",67        "patterns": "Schede dei pattern", "shadow_panel": "Ombra e inconscio", "conditions": "Condizioni operative",68        "resourced": "Quando hai risorse", "depleted": "Quando sei in riserva", "flips": "Cambia quando",69        "self_model": "Quanto ti vedi con precisione", "clear": "Vedi con chiarezza", "overrate": "Potresti sovrastimare", "blind": "Punti ciechi",70        "forecast": "Traiettoria e pressioni", "when": "Quando", "tends": "Tende verso", "horizon": "Orizzonte", "shift": "Cosa la cambierebbe",71        "actions": "Intelligenza attuabile", "why": "Perché", "first": "Prima di tutto perché", "unlocks": "Poi sblocca",72        "evidence": "Segnali di evidenza", "map_edges": "Dove finisce questa mappa", "codex_map": "Mappa del Codex",73        "node": "Dimensione", "layer": "Livello", "strength_col": "Forza", "position": "Posizione", "durability": "Durabilità", "confidence": "Confidenza",74        "edges": "Relazioni attive", "metrics": "Metriche derivate", "radar": "Punteggi radar",75        "epistemics": "Epistemica", "overall_confidence": "Confidenza complessiva", "data_quality": "Qualità dei dati",76        "highest": "Risultati a confidenza più alta", "lowest": "Risultati a confidenza più bassa", "limits": "Limiti",77        "cultural": "Segnalazioni di bias culturale", "follow_up": "Approfondimenti consigliati", "meta": "Meta",78        "depth": "Profondità dell'analisi", "confidence_score": "Punteggio di confidenza", "signals_used": "Segnali usati",79        "raises": "alza", "lowers": "abbassa",80    },81}828384class AssembleError(ValueError):85    """Raised when an essential pass output cannot be parsed."""868788# ---------------------------------------------------------------------------89# Parsing helpers90# ---------------------------------------------------------------------------9192def _strip_fences(text: str) -> str:93    s = (text or "").strip()94    m = re.match(r"^```[a-zA-Z]*\s*(.*?)\s*```$", s, re.S)95    if m:96        return m.group(1).strip()97    m = re.search(r"```(?:json)?\s*(.*?)\s*```", s, re.S)98    if m and m.group(1).lstrip()[:1] in "{[":99        return m.group(1).strip()100    return s101102103def parse_pass_output(text: str, expect: str, unwrap_key: str):104    """Parse an LLM output into JSON.105106    expect: 'object' | 'array'. Returns (value, note); value is None on failure.107    Tolerates code fences, surrounding prose, and a wrapper object such as108    {"analysis": {...}} or {"per_item": [...]}.109    """110    s = _strip_fences(text)111    if not s:112        return None, "empty output"113    open_ch, close_ch = ("{", "}") if expect == "object" else ("[", "]")114    starts = [i for i in (s.find("{"), s.find("[")) if i != -1]115    if not starts:116        return None, "no JSON found"117    start = min(starts)118    end = max(s.rfind("}"), s.rfind("]"))119    if end < start:120        return None, "no JSON found"121    try:122        value = json.loads(s[start:end + 1])123    except json.JSONDecodeError as e:124        i, j = s.find(open_ch), s.rfind(close_ch)125        if i != -1 and j > i:126            try:127                value = json.loads(s[i:j + 1])128            except json.JSONDecodeError as e2:129                return None, f"invalid or truncated JSON ({e2.msg} at char {e2.pos} of {len(s)})"130        else:131            return None, f"invalid or truncated JSON ({e.msg} at char {e.pos} of {len(s)})"132    if isinstance(value, dict) and unwrap_key in value and len(value) <= 2:133        value = value[unwrap_key]134    if expect == "array" and isinstance(value, dict) and unwrap_key in value:135        value = value[unwrap_key]136    if expect == "array" and not isinstance(value, list):137        return None, f"expected a JSON array, got {type(value).__name__}"138    if expect == "object" and not isinstance(value, dict):139        return None, f"expected a JSON object, got {type(value).__name__}"140    return value, ""141142143def parse_settings(text: str) -> dict:144    out = {}145    for line in (text or "").splitlines():146        if ":" in line and not line.startswith("="):147            k, v = line.split(":", 1)148            k = k.strip().upper().replace(" ", "_")149            if k:150                out[k] = v.strip()151    return out152153154# ---------------------------------------------------------------------------155# Markdown rendering156# ---------------------------------------------------------------------------157158def _g(d, *path, default=None):159    cur = d160    for p in path:161        if isinstance(cur, dict) and p in cur:162            cur = cur[p]163        else:164            return default165    return cur166167168def _bullets(items, fmt=None) -> str:169    lines = []170    for it in items or []:171        if fmt:172            lines.append(f"- {fmt(it)}")173        elif isinstance(it, str):174            lines.append(f"- {it}")175        else:176            lines.append(f"- {json.dumps(it, ensure_ascii=False)}")177    return "\n".join(lines) if lines else "-"178179180def crisis_note(analysis: dict):181    for item in _g(analysis, "epistemics", "limitations", default=[]) or []:182        if isinstance(item, str) and item.strip().upper().startswith("CRISIS FLAG"):183            return item184    return None185186187def render_markdown(profile: dict, settings: dict) -> str:188    lang = (settings.get("REPORT_LANGUAGE") or "en").lower()[:2]189    H = HEADINGS.get(lang, HEADINGS["en"])190    pres = profile.get("presentation") or {}191    ana = profile.get("analysis") or {}192    subject = settings.get("SUBJECT_LABEL") or ""193    when = settings.get("INTERVIEW_DATE") or date.today().isoformat()194    mode = settings.get("INTERVIEW_MODE") or ""195196    out = []197    out.append(f"# {H['title']}" + (f" — {subject}" if subject else ""))198    out.append(f"*{H['date']}: {when}*" + (f" · *{H['mode']}: {mode}*" if mode else ""))199    out.append("")200    out.append(f"> {H['frame']}")201    out.append("")202203    crisis = crisis_note(ana)204    if crisis:205        out.append(f"## ⚠ {H['crisis']}")206        out.append(crisis)207        out.append("")208209    head = pres.get("headline") or {}210    archetype = head.get("archetype") or ""211    out.append(f"## {H['headline']}" + (f": {archetype}" if archetype else ""))212    if head.get("organizing_thesis_plain"):213        out.append(f"**{H['thesis']}.** {head['organizing_thesis_plain']}")214        out.append("")215    if head.get("summary"):216        out.append(head["summary"])217        out.append("")218219    axes = pres.get("axis_map") or {}220    out.append(f"## {H['axes']}")221    for key in ("strength", "growth", "shadow", "reflection"):222        ax = axes.get(key) or {}223        out.append(f"### {H[key]} — {ax.get('score', 50)}/100")224        if ax.get("summary"):225            out.append(ax["summary"])226        out.append(_bullets(ax.get("details")))227        out.append("")228229    snap = pres.get("operating_snapshot") or []230    if snap:231        out.append(f"## {H['snapshot']}")232        out.append(_bullets(snap, lambda s: f"**{s.get('label', '')}: {s.get('state', '')}.** {s.get('description', '')}"))233        out.append("")234235    out.append(f"## {H['strengths']}")236    out.append(_bullets(pres.get("strengths")))237    out.append("")238    out.append(f"## {H['growth_edges']}")239    out.append(_bullets(pres.get("growth_edges")))240    out.append("")241242    cards = pres.get("pattern_cards") or []243    if cards:244        out.append(f"## {H['patterns']}")245        for c in cards:246            out.append(f"### {c.get('title', '')} ({c.get('pattern_type', '')})")247            if c.get("summary"):248                out.append(c["summary"])249            if c.get("impact"):250                out.append(f"*{c['impact']}*")251            out.append("")252253    sh = pres.get("shadow_panel") or {}254    out.append(f"## {H['shadow_panel']}")255    if sh.get("summary"):256        out.append(sh["summary"])257    out.append(_bullets(sh.get("items")))258    if sh.get("framing_note"):259        out.append(f"\n*{sh['framing_note']}*")260    out.append("")261262    conds = pres.get("operating_conditions") or []263    if conds:264        out.append(f"## {H['conditions']}")265        for c in conds:266            out.append(f"### {c.get('dimension', '')}")267            out.append(f"- **{H['resourced']}:** {c.get('when_resourced', '')}")268            out.append(f"- **{H['depleted']}:** {c.get('when_depleted', '')}")269            out.append(f"- **{H['flips']}:** {c.get('flips_when', '')}")270            out.append("")271272    sm = pres.get("self_model_panel") or {}273    out.append(f"## {H['self_model']}")274    if sm.get("summary"):275        out.append(sm["summary"])276    out.append(f"\n**{H['clear']}**\n{_bullets(sm.get('you_see_clearly'))}")277    out.append(f"\n**{H['overrate']}**\n{_bullets(sm.get('you_may_overrate'))}")278    out.append(f"\n**{H['blind']}**\n{_bullets(sm.get('blind_spots'))}")279    out.append("")280281    fc = pres.get("forecast_panel") or {}282    out.append(f"## {H['forecast']}")283    if fc.get("trajectory"):284        out.append(fc["trajectory"])285        out.append("")286    for p in fc.get("pressures") or []:287        out.append(f"### {p.get('name', '')}")288        out.append(f"- **{H['when']}:** {p.get('when', '')}")289        out.append(f"- **{H['tends']}:** {p.get('tends_toward', '')}")290        out.append(f"- **{H['horizon']}:** {p.get('horizon', '')}")291        out.append(f"- **{H['shift']}:** {p.get('what_would_shift', '')}")292        out.append("")293    if fc.get("note"):294        out.append(f"*{fc['note']}*")295        out.append("")296297    acts = pres.get("actionable_intelligence") or []298    if acts:299        out.append(f"## {H['actions']}")300        for a in acts:301            out.append(f"### {a.get('step', '')}. {a.get('focus', '')}")302            out.append(f"- **{H['why']}:** {a.get('why', '')}")303            out.append(f"- **{H['first']}:** {a.get('do_first_because', '')}")304            out.append(f"- **{H['unlocks']}:** {a.get('then_unlocks', '')}")305            out.append("")306307    ev = pres.get("evidence_signals") or []308    if ev:309        out.append(f"## {H['evidence']}")310        out.append(_bullets(ev, lambda e: f"**{e.get('label', '')}** — “{e.get('quote', '')}” — {e.get('insight', '')}"))311        out.append("")312313    if pres.get("map_edges_note"):314        out.append(f"## {H['map_edges']}")315        out.append(pres["map_edges_note"])316        out.append("")317318    cm = pres.get("codex_map_3d") or {}319    nodes = cm.get("nodes") or []320    if nodes:321        out.append(f"## {H['codex_map']}")322        if cm.get("narrative_thread"):323            out.append(cm["narrative_thread"])324            out.append("")325        out.append(f"| # | {H['node']} | {H['layer']} | {H['strength_col']} | {H['position']} | {H['durability']} | {H['confidence']} |")326        out.append("|---|---|---|---|---|---|---|")327        for n in nodes:328            out.append(f"| {n.get('node_id', '')} | {n.get('node_name', '')} | {n.get('layer', '')} | {n.get('strength', '')} | {n.get('spectrum_position', '')} | {n.get('durability', '')} | {n.get('confidence', '')} |")329        out.append("")330        for n in nodes:331            if n.get("click_note"):332                out.append(f"- **{n.get('node_name', '')}** — {n['click_note']}")333        out.append("")334        edges = cm.get("edges") or []335        if edges:336            out.append(f"### {H['edges']}")337            out.append(_bullets(edges, lambda e: f"{e.get('source_id', '')} → {e.get('target_id', '')} ({H.get(e.get('sign', ''), e.get('sign', ''))}, w{e.get('weight', '')}, {e.get('interaction_type', '')}): {e.get('rationale', '')}"))338            out.append("")339340    dm = pres.get("derived_metrics") or {}341    rs = pres.get("radar_scores") or {}342    if dm or rs:343        out.append(f"## {H['metrics']}")344        for k, v in dm.items():345            out.append(f"- {k}: {v}")346        if rs:347            out.append(f"\n**{H['radar']}**")348            for k, v in rs.items():349                out.append(f"- {k}: {v}")350        out.append("")351352    ep = ana.get("epistemics") or {}353    if ep:354        out.append(f"## {H['epistemics']}")355        out.append(f"- **{H['overall_confidence']}:** {ep.get('overall_confidence', '')}")356        out.append(f"- **{H['data_quality']}:** {ep.get('data_quality', '')}")357        out.append(f"\n**{H['highest']}**\n{_bullets(ep.get('highest_confidence_findings'))}")358        out.append(f"\n**{H['lowest']}**\n{_bullets(ep.get('lowest_confidence_findings'))}")359        out.append(f"\n**{H['limits']}**\n{_bullets((ep.get('limitations') or []) + (ep.get('edges_of_knowledge') or []))}")360        if ep.get("cultural_bias_flags"):361            out.append(f"\n**{H['cultural']}**\n{_bullets(ep.get('cultural_bias_flags'))}")362        if ep.get("follow_up_recommended"):363            out.append(f"\n**{H['follow_up']}**\n{_bullets(ep.get('follow_up_recommended'))}")364        out.append("")365366    meta = pres.get("meta") or {}367    if meta:368        out.append(f"## {H['meta']}")369        out.append(f"- {H['confidence_score']}: {meta.get('confidence_score', '')}")370        out.append(f"- {H['depth']}: {meta.get('analysis_depth', '')}")371        out.append(f"- {H['signals_used']}: {', '.join(meta.get('language_signals_used') or [])}")372        out.append("")373374    return "\n".join(out).strip() + "\n"375376377# ---------------------------------------------------------------------------378# Core379# ---------------------------------------------------------------------------380381def process(inputs: dict) -> dict:382    """Assemble the profile. Raises AssembleError when analysis/presentation are unusable."""383    notes = []384    per_item, n1 = parse_pass_output(inputs.get("per_item") or "", "array", "per_item")385    if per_item is None:386        notes.append(f"WARNING per_item (Pass 1): {n1}; using []")387        per_item = []388    analysis, n3 = parse_pass_output(inputs.get("analysis") or "", "object", "analysis")389    presentation, n4 = parse_pass_output(inputs.get("presentation") or "", "object", "presentation")390    if analysis is None:391        raise AssembleError(f"ERROR analysis (Pass 3) could not be parsed: {n3}. Re-run 'Pass 3 · Map & Score Dimensions'.")392    if presentation is None:393        raise AssembleError(f"ERROR presentation (Pass 4) could not be parsed: {n4}. Re-run 'Pass 4 · Compose Profile'.")394395    for k in TOP_KEYS_ANALYSIS:396        if k not in analysis:397            notes.append(f"WARNING analysis missing key '{k}'")398    for k in TOP_KEYS_PRESENTATION:399        if k not in presentation:400            notes.append(f"WARNING presentation missing key '{k}'")401    dims = analysis.get("dimensions") or []402    edges = analysis.get("edges_active") or []403    configs = analysis.get("configurations") or []404    if len(dims) < 3:405        notes.append(f"WARNING analysis.dimensions has {len(dims)} entries (minimum 3)")406    if len(edges) < 2:407        notes.append("WARNING analysis.edges_active has fewer than 2 entries")408    if len(configs) < 1:409        notes.append("WARNING analysis.configurations is empty")410    if len(per_item) == 0:411        notes.append("WARNING per_item is empty")412413    settings = parse_settings(inputs.get("settings") or "")414    profile = {415        "schema_version": "2.0",416        "per_item": per_item,417        "analysis": analysis,418        "presentation": presentation,419    }420    report = render_markdown(profile, settings)421    headline = _g(presentation, "headline", "archetype", default="") or ""422    validation = "OK" if not notes else "\n".join(notes)423    validation += f"\nitems={len(per_item)} dimensions={len(dims)} edges={len(edges)} configurations={len(configs)}"424    return {425        "profile_json": json.dumps(profile, ensure_ascii=False, indent=2),426        "report_markdown": report,427        "validation_notes": validation,428        "headline": headline,429    }430431432def main() -> None:433    try:434        envelope = json.loads(sys.stdin.read() or "{}")435        inputs = envelope.get("inputs", {}) if isinstance(envelope, dict) else {}436        result = process(inputs)437        print(f"[human-codex-assemble] {result['validation_notes'].splitlines()[-1]}", file=sys.stderr)438        json.dump(result, sys.stdout, ensure_ascii=False)439    except Exception as e:  # noqa: BLE001440        print(json.dumps({"error": str(e), "errorType": type(e).__name__}), file=sys.stderr)441        sys.exit(1)442443444if __name__ == "__main__":445    main()446

$ git log --oneline

v1.0.0
HEAD
2026-09-04

Initial release: deterministic assembler for the Human Codex interview pipeline