$ 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