from __future__ import annotations import argparse import hashlib import json from pathlib import Path from typing import Any ROOT = Path(__file__).resolve().parents[1] DEFAULT_SOURCE = ROOT / "Temp" / "strategy_decision_result_v3.json" DEFAULT_REPORT = ROOT / "Temp" / "operational_report.json" DEFAULT_OUT = ROOT / "Temp" / "truthful_decision_ledger_v2.json" def _load_json(path: Path) -> dict[str, Any]: if not path.exists(): return {} try: payload = json.loads(path.read_text(encoding="utf-8")) except Exception: return {} return payload if isinstance(payload, dict) else {} def _as_rows(value: Any) -> list[dict[str, Any]]: if isinstance(value, list): return [row for row in value if isinstance(row, dict)] return [] def _canonical(obj: Any) -> str: return json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def _sha256_text(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() def _row_hash(row: dict[str, Any]) -> str: payload = dict(row) payload.pop("output_hash", None) return _sha256_text(_canonical(payload)) def _source_fields(row: dict[str, Any]) -> list[str]: keys = [ "account", "ticker", "name", "current_holding_quantity", "average_cost_krw", "current_price_krw", "order_type", "mode", "limit_price_krw", "quantity", "stop_price_krw", "take_profit_price_krw", "validation_status", "rationale_code", "spsv2_verdict", "blocked_by_gate", "lock_applied", ] return [key for key in keys if row.get(key) not in (None, "", [])] def _reason_codes(row: dict[str, Any]) -> list[str]: reasons: list[str] = [] for key in ("rationale_code", "blocked_by_gate", "lock_applied", "export_gate"): value = row.get(key) if isinstance(value, str) and value.strip(): reasons.extend([part.strip() for part in value.split("|") if part.strip()]) if not reasons: reasons.append("NO_REASON_CODE") return reasons def _gate_stack(row: dict[str, Any], export_gate: str) -> list[str]: stack = [f"EXPORT_GATE={export_gate}"] if row.get("validation_status"): stack.append(f"VALIDATION={row.get('validation_status')}") if row.get("spsv2_verdict"): stack.append(f"SPSV2={row.get('spsv2_verdict')}") if row.get("blocked_by_gate"): stack.append(f"BLOCKED_BY={row.get('blocked_by_gate')}") if row.get("lock_applied"): stack.append(f"LOCK={row.get('lock_applied')}") return stack def main() -> int: ap = argparse.ArgumentParser(description="Build truthful decision ledger v2.") ap.add_argument("--source", default=str(DEFAULT_SOURCE)) ap.add_argument("--report", default=str(DEFAULT_REPORT)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() source_path = Path(args.source) report_path = Path(args.report) out_path = Path(args.out) if not source_path.is_absolute(): source_path = ROOT / source_path if not report_path.is_absolute(): report_path = ROOT / report_path if not out_path.is_absolute(): out_path = ROOT / out_path source = _load_json(source_path) report = _load_json(report_path) orders = _as_rows(source.get("order_blueprint")) shadow = _as_rows(source.get("shadow_ledger")) export_gate = str((source.get("global_gates") or {}).get("export_gate") or "UNKNOWN") route = source.get("route") if isinstance(source.get("route"), dict) else {} route_id = str(route.get("route_id") or "UNKNOWN_ROUTE") input_hash = _sha256_text(_canonical(source)) report_hash = _sha256_text(_canonical(report)) if report else None ledger_rows: list[dict[str, Any]] = [] for idx, row in enumerate(orders): decision_id = f"{route_id}:{idx:03d}:{str(row.get('ticker') or 'NA')}" base_row = { "decision_id": decision_id, "proposal_id": f"{str(row.get('ticker') or 'NA')}:{idx:03d}", "ticker": row.get("ticker"), "action": row.get("order_type"), "state": row.get("validation_status"), "formula_id": "TRUTHFUL_DECISION_LEDGER_V2", "input_hash": input_hash, "source_snapshot_hash": input_hash, "price_basis": "HARNESS_ONLY", "qty_basis": "ORDER_BLUEPRINT", "source_fields": _source_fields(row), "reason_codes": _reason_codes(row), "gate_stack": _gate_stack(row, export_gate), "export_gate": export_gate, "renderer_section": "concise_hts_input_sheet" if str(row.get("validation_status") or "").upper() == "PASS" else "reference_price_ledger", "llm_numeric_generated_flag": False, "outcome_binding_id": f"{route_id}:{str(row.get('ticker') or 'NA')}:{str(row.get('validation_status') or 'UNKNOWN')}", "record_type": "order_blueprint", } if report_hash is not None: base_row["report_hash"] = report_hash base_row["output_hash"] = _row_hash(base_row) ledger_rows.append(base_row) for idx, row in enumerate(shadow): decision_id = f"{route_id}:shadow:{idx:03d}:{str(row.get('ticker') or 'NA')}" base_row = { "decision_id": decision_id, "proposal_id": f"SHADOW:{str(row.get('ticker') or 'NA')}:{idx:03d}", "ticker": row.get("ticker"), "action": row.get("order_type"), "state": row.get("validation_status"), "formula_id": "TRUTHFUL_DECISION_LEDGER_V2", "input_hash": input_hash, "source_snapshot_hash": input_hash, "price_basis": "HARNESS_ONLY", "qty_basis": "ORDER_BLUEPRINT", "source_fields": _source_fields(row), "reason_codes": _reason_codes(row), "gate_stack": _gate_stack(row, export_gate), "export_gate": export_gate, "renderer_section": "reference_price_ledger", "llm_numeric_generated_flag": False, "outcome_binding_id": f"{route_id}:{str(row.get('ticker') or 'NA')}:{str(row.get('validation_status') or 'UNKNOWN')}", "record_type": "shadow_ledger", } if report_hash is not None: base_row["report_hash"] = report_hash base_row["output_hash"] = _row_hash(base_row) ledger_rows.append(base_row) result = { "formula_id": "TRUTHFUL_DECISION_LEDGER_V2", "schema_version": "truthful-decision-ledger-v2", "as_of": source.get("as_of") or source.get("analysis_date") or "", "input_hash": input_hash, "report_hash": report_hash, "route_id": route_id, "price_basis": "HARNESS_ONLY", "qty_basis": "ORDER_BLUEPRINT", "llm_numeric_generated_flag": False, "ledger_rows": ledger_rows, "ledger_count": len(ledger_rows), "targets": { "llm_numeric_generated_flag": False, "source_fields_required": True, "output_hash_required": True, "report_hash_required": bool(report_hash is not None), }, } out_path.parent.mkdir(parents=True, exist_ok=True) out_path.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())