$ 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    "zh": {82        "title": "Human Codex 个人画像", "date": "访谈日期", "mode": "访谈模式",83        "frame": "这是一个非临床的自我反思工具。它描绘的是你所承受的压力,而不是对未来的预测;它不做诊断、不提供治疗,也不能替代专业帮助。下面的每一项解读都是一个假设,并注明了依据、置信度和局限,你始终有权质疑和反驳。",84        "crisis": "致专业人员的提示",85        "headline": "概要", "thesis": "核心论点",86        "axes": "四个维度轴", "strength": "优势", "growth": "成长", "shadow": "阴影", "reflection": "反思",87        "snapshot": "运作概览", "strengths": "优势", "growth_edges": "成长空间",88        "patterns": "模式卡片", "shadow_panel": "阴影与潜意识", "conditions": "运作条件",89        "resourced": "资源充足时", "depleted": "精力耗竭时", "flips": "转变的时机",90        "self_model": "你对自己的认识有多准确", "clear": "你看得清楚的", "overrate": "你可能高估的", "blind": "盲点",91        "forecast": "发展轨迹与压力", "when": "何时", "tends": "倾向于", "horizon": "时间范围", "shift": "什么会改变它",92        "actions": "可执行的建议", "why": "原因", "first": "优先去做,因为", "unlocks": "随后带来",93        "evidence": "依据信号", "map_edges": "这张地图的边界", "codex_map": "Codex 地图",94        "node": "维度", "layer": "层级", "strength_col": "强度", "position": "位置", "durability": "持久性", "confidence": "置信度",95        "edges": "活跃关系", "metrics": "衍生指标", "radar": "雷达评分",96        "epistemics": "认知可靠性", "overall_confidence": "整体置信度", "data_quality": "数据质量",97        "highest": "置信度最高的发现", "lowest": "置信度最低的发现", "limits": "局限",98        "cultural": "文化偏差提示", "follow_up": "建议的后续步骤", "meta": "元信息",99        "depth": "分析深度", "confidence_score": "置信度评分", "signals_used": "使用的信号",100        "raises": "提升", "lowers": "降低",101    },102}103104105class AssembleError(ValueError):106    """Raised when an essential pass output cannot be parsed."""107108109# ---------------------------------------------------------------------------110# Parsing helpers111# ---------------------------------------------------------------------------112113def _strip_fences(text: str) -> str:114    s = (text or "").strip()115    m = re.match(r"^```[a-zA-Z]*\s*(.*?)\s*```$", s, re.S)116    if m:117        return m.group(1).strip()118    m = re.search(r"```(?:json)?\s*(.*?)\s*```", s, re.S)119    if m and m.group(1).lstrip()[:1] in "{[":120        return m.group(1).strip()121    return s122123124def parse_pass_output(text: str, expect: str, unwrap_key: str):125    """Parse an LLM output into JSON.126127    expect: 'object' | 'array'. Returns (value, note); value is None on failure.128    Tolerates code fences, surrounding prose, and a wrapper object such as129    {"analysis": {...}} or {"per_item": [...]}.130    """131    s = _strip_fences(text)132    if not s:133        return None, "empty output"134    open_ch, close_ch = ("{", "}") if expect == "object" else ("[", "]")135    starts = [i for i in (s.find("{"), s.find("[")) if i != -1]136    if not starts:137        return None, "no JSON found"138    start = min(starts)139    end = max(s.rfind("}"), s.rfind("]"))140    if end < start:141        return None, "no JSON found"142    try:143        value = json.loads(s[start:end + 1])144    except json.JSONDecodeError as e:145        i, j = s.find(open_ch), s.rfind(close_ch)146        if i != -1 and j > i:147            try:148                value = json.loads(s[i:j + 1])149            except json.JSONDecodeError as e2:150                return None, f"invalid or truncated JSON ({e2.msg} at char {e2.pos} of {len(s)})"151        else:152            return None, f"invalid or truncated JSON ({e.msg} at char {e.pos} of {len(s)})"153    if isinstance(value, dict) and unwrap_key in value and len(value) <= 2:154        value = value[unwrap_key]155    if expect == "array" and isinstance(value, dict) and unwrap_key in value:156        value = value[unwrap_key]157    if expect == "array" and not isinstance(value, list):158        return None, f"expected a JSON array, got {type(value).__name__}"159    if expect == "object" and not isinstance(value, dict):160        return None, f"expected a JSON object, got {type(value).__name__}"161    return value, ""162163164def parse_settings(text: str) -> dict:165    out = {}166    for line in (text or "").splitlines():167        if ":" in line and not line.startswith("="):168            k, v = line.split(":", 1)169            k = k.strip().upper().replace(" ", "_")170            if k:171                out[k] = v.strip()172    return out173174175# ---------------------------------------------------------------------------176# Markdown rendering177# ---------------------------------------------------------------------------178179def _g(d, *path, default=None):180    cur = d181    for p in path:182        if isinstance(cur, dict) and p in cur:183            cur = cur[p]184        else:185            return default186    return cur187188189def _bullets(items, fmt=None) -> str:190    lines = []191    for it in items or []:192        if fmt:193            lines.append(f"- {fmt(it)}")194        elif isinstance(it, str):195            lines.append(f"- {it}")196        else:197            lines.append(f"- {json.dumps(it, ensure_ascii=False)}")198    return "\n".join(lines) if lines else "-"199200201def crisis_note(analysis: dict):202    for item in _g(analysis, "epistemics", "limitations", default=[]) or []:203        if isinstance(item, str) and item.strip().upper().startswith("CRISIS FLAG"):204            return item205    return None206207208def render_markdown(profile: dict, settings: dict) -> str:209    lang = (settings.get("REPORT_LANGUAGE") or "en").lower()[:2]210    H = HEADINGS.get(lang, HEADINGS["en"])211    pres = profile.get("presentation") or {}212    ana = profile.get("analysis") or {}213    subject = settings.get("SUBJECT_LABEL") or ""214    when = settings.get("INTERVIEW_DATE") or date.today().isoformat()215    mode = settings.get("INTERVIEW_MODE") or ""216217    out = []218    out.append(f"# {H['title']}" + (f" — {subject}" if subject else ""))219    out.append(f"*{H['date']}: {when}*" + (f" · *{H['mode']}: {mode}*" if mode else ""))220    out.append("")221    out.append(f"> {H['frame']}")222    out.append("")223224    crisis = crisis_note(ana)225    if crisis:226        out.append(f"## ⚠ {H['crisis']}")227        out.append(crisis)228        out.append("")229230    head = pres.get("headline") or {}231    archetype = head.get("archetype") or ""232    out.append(f"## {H['headline']}" + (f": {archetype}" if archetype else ""))233    if head.get("organizing_thesis_plain"):234        out.append(f"**{H['thesis']}.** {head['organizing_thesis_plain']}")235        out.append("")236    if head.get("summary"):237        out.append(head["summary"])238        out.append("")239240    axes = pres.get("axis_map") or {}241    out.append(f"## {H['axes']}")242    for key in ("strength", "growth", "shadow", "reflection"):243        ax = axes.get(key) or {}244        out.append(f"### {H[key]} — {ax.get('score', 50)}/100")245        if ax.get("summary"):246            out.append(ax["summary"])247        out.append(_bullets(ax.get("details")))248        out.append("")249250    snap = pres.get("operating_snapshot") or []251    if snap:252        out.append(f"## {H['snapshot']}")253        out.append(_bullets(snap, lambda s: f"**{s.get('label', '')}: {s.get('state', '')}.** {s.get('description', '')}"))254        out.append("")255256    out.append(f"## {H['strengths']}")257    out.append(_bullets(pres.get("strengths")))258    out.append("")259    out.append(f"## {H['growth_edges']}")260    out.append(_bullets(pres.get("growth_edges")))261    out.append("")262263    cards = pres.get("pattern_cards") or []264    if cards:265        out.append(f"## {H['patterns']}")266        for c in cards:267            out.append(f"### {c.get('title', '')} ({c.get('pattern_type', '')})")268            if c.get("summary"):269                out.append(c["summary"])270            if c.get("impact"):271                out.append(f"*{c['impact']}*")272            out.append("")273274    sh = pres.get("shadow_panel") or {}275    out.append(f"## {H['shadow_panel']}")276    if sh.get("summary"):277        out.append(sh["summary"])278    out.append(_bullets(sh.get("items")))279    if sh.get("framing_note"):280        out.append(f"\n*{sh['framing_note']}*")281    out.append("")282283    conds = pres.get("operating_conditions") or []284    if conds:285        out.append(f"## {H['conditions']}")286        for c in conds:287            out.append(f"### {c.get('dimension', '')}")288            out.append(f"- **{H['resourced']}:** {c.get('when_resourced', '')}")289            out.append(f"- **{H['depleted']}:** {c.get('when_depleted', '')}")290            out.append(f"- **{H['flips']}:** {c.get('flips_when', '')}")291            out.append("")292293    sm = pres.get("self_model_panel") or {}294    out.append(f"## {H['self_model']}")295    if sm.get("summary"):296        out.append(sm["summary"])297    out.append(f"\n**{H['clear']}**\n{_bullets(sm.get('you_see_clearly'))}")298    out.append(f"\n**{H['overrate']}**\n{_bullets(sm.get('you_may_overrate'))}")299    out.append(f"\n**{H['blind']}**\n{_bullets(sm.get('blind_spots'))}")300    out.append("")301302    fc = pres.get("forecast_panel") or {}303    out.append(f"## {H['forecast']}")304    if fc.get("trajectory"):305        out.append(fc["trajectory"])306        out.append("")307    for p in fc.get("pressures") or []:308        out.append(f"### {p.get('name', '')}")309        out.append(f"- **{H['when']}:** {p.get('when', '')}")310        out.append(f"- **{H['tends']}:** {p.get('tends_toward', '')}")311        out.append(f"- **{H['horizon']}:** {p.get('horizon', '')}")312        out.append(f"- **{H['shift']}:** {p.get('what_would_shift', '')}")313        out.append("")314    if fc.get("note"):315        out.append(f"*{fc['note']}*")316        out.append("")317318    acts = pres.get("actionable_intelligence") or []319    if acts:320        out.append(f"## {H['actions']}")321        for a in acts:322            out.append(f"### {a.get('step', '')}. {a.get('focus', '')}")323            out.append(f"- **{H['why']}:** {a.get('why', '')}")324            out.append(f"- **{H['first']}:** {a.get('do_first_because', '')}")325            out.append(f"- **{H['unlocks']}:** {a.get('then_unlocks', '')}")326            out.append("")327328    ev = pres.get("evidence_signals") or []329    if ev:330        out.append(f"## {H['evidence']}")331        out.append(_bullets(ev, lambda e: f"**{e.get('label', '')}** — “{e.get('quote', '')}” — {e.get('insight', '')}"))332        out.append("")333334    if pres.get("map_edges_note"):335        out.append(f"## {H['map_edges']}")336        out.append(pres["map_edges_note"])337        out.append("")338339    cm = pres.get("codex_map_3d") or {}340    nodes = cm.get("nodes") or []341    if nodes:342        out.append(f"## {H['codex_map']}")343        if cm.get("narrative_thread"):344            out.append(cm["narrative_thread"])345            out.append("")346        out.append(f"| # | {H['node']} | {H['layer']} | {H['strength_col']} | {H['position']} | {H['durability']} | {H['confidence']} |")347        out.append("|---|---|---|---|---|---|---|")348        for n in nodes:349            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', '')} |")350        out.append("")351        for n in nodes:352            if n.get("click_note"):353                out.append(f"- **{n.get('node_name', '')}** — {n['click_note']}")354        out.append("")355        edges = cm.get("edges") or []356        if edges:357            out.append(f"### {H['edges']}")358            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', '')}"))359            out.append("")360361    dm = pres.get("derived_metrics") or {}362    rs = pres.get("radar_scores") or {}363    if dm or rs:364        out.append(f"## {H['metrics']}")365        for k, v in dm.items():366            out.append(f"- {k}: {v}")367        if rs:368            out.append(f"\n**{H['radar']}**")369            for k, v in rs.items():370                out.append(f"- {k}: {v}")371        out.append("")372373    ep = ana.get("epistemics") or {}374    if ep:375        out.append(f"## {H['epistemics']}")376        out.append(f"- **{H['overall_confidence']}:** {ep.get('overall_confidence', '')}")377        out.append(f"- **{H['data_quality']}:** {ep.get('data_quality', '')}")378        out.append(f"\n**{H['highest']}**\n{_bullets(ep.get('highest_confidence_findings'))}")379        out.append(f"\n**{H['lowest']}**\n{_bullets(ep.get('lowest_confidence_findings'))}")380        out.append(f"\n**{H['limits']}**\n{_bullets((ep.get('limitations') or []) + (ep.get('edges_of_knowledge') or []))}")381        if ep.get("cultural_bias_flags"):382            out.append(f"\n**{H['cultural']}**\n{_bullets(ep.get('cultural_bias_flags'))}")383        if ep.get("follow_up_recommended"):384            out.append(f"\n**{H['follow_up']}**\n{_bullets(ep.get('follow_up_recommended'))}")385        out.append("")386387    meta = pres.get("meta") or {}388    if meta:389        out.append(f"## {H['meta']}")390        out.append(f"- {H['confidence_score']}: {meta.get('confidence_score', '')}")391        out.append(f"- {H['depth']}: {meta.get('analysis_depth', '')}")392        out.append(f"- {H['signals_used']}: {', '.join(meta.get('language_signals_used') or [])}")393        out.append("")394395    return "\n".join(out).strip() + "\n"396397398# ---------------------------------------------------------------------------399# Core400# ---------------------------------------------------------------------------401402def process(inputs: dict) -> dict:403    """Assemble the profile. Raises AssembleError when analysis/presentation are unusable."""404    notes = []405    per_item, n1 = parse_pass_output(inputs.get("per_item") or "", "array", "per_item")406    if per_item is None:407        notes.append(f"WARNING per_item (Pass 1): {n1}; using []")408        per_item = []409    analysis, n3 = parse_pass_output(inputs.get("analysis") or "", "object", "analysis")410    presentation, n4 = parse_pass_output(inputs.get("presentation") or "", "object", "presentation")411    if analysis is None:412        raise AssembleError(f"ERROR analysis (Pass 3) could not be parsed: {n3}. Re-run 'Pass 3 · Map & Score Dimensions'.")413    if presentation is None:414        raise AssembleError(f"ERROR presentation (Pass 4) could not be parsed: {n4}. Re-run 'Pass 4 · Compose Profile'.")415416    for k in TOP_KEYS_ANALYSIS:417        if k not in analysis:418            notes.append(f"WARNING analysis missing key '{k}'")419    for k in TOP_KEYS_PRESENTATION:420        if k not in presentation:421            notes.append(f"WARNING presentation missing key '{k}'")422    dims = analysis.get("dimensions") or []423    edges = analysis.get("edges_active") or []424    configs = analysis.get("configurations") or []425    if len(dims) < 3:426        notes.append(f"WARNING analysis.dimensions has {len(dims)} entries (minimum 3)")427    if len(edges) < 2:428        notes.append("WARNING analysis.edges_active has fewer than 2 entries")429    if len(configs) < 1:430        notes.append("WARNING analysis.configurations is empty")431    if len(per_item) == 0:432        notes.append("WARNING per_item is empty")433434    settings = parse_settings(inputs.get("settings") or "")435    profile = {436        "schema_version": "2.0",437        "per_item": per_item,438        "analysis": analysis,439        "presentation": presentation,440    }441    report = render_markdown(profile, settings)442    headline = _g(presentation, "headline", "archetype", default="") or ""443    validation = "OK" if not notes else "\n".join(notes)444    validation += f"\nitems={len(per_item)} dimensions={len(dims)} edges={len(edges)} configurations={len(configs)}"445    return {446        "profile_json": json.dumps(profile, ensure_ascii=False, indent=2),447        "report_markdown": report,448        "validation_notes": validation,449        "headline": headline,450    }451452453def main() -> None:454    try:455        envelope = json.loads(sys.stdin.read() or "{}")456        inputs = envelope.get("inputs", {}) if isinstance(envelope, dict) else {}457        result = process(inputs)458        print(f"[human-codex-assemble] {result['validation_notes'].splitlines()[-1]}", file=sys.stderr)459        json.dump(result, sys.stdout, ensure_ascii=False)460    except Exception as e:  # noqa: BLE001461        print(json.dumps({"error": str(e), "errorType": type(e).__name__}), file=sys.stderr)462        sys.exit(1)463464465if __name__ == "__main__":466    main()467

$ git log --oneline

v1.1.0
HEAD
2026-10-08