#!/usr/bin/env python3 """Build a rollback-only creation plan for rich approved KB proposals. This is the bridge between old Leo freeform approvals and the strict apply worker. It is intentionally non-mutating: the output is a deterministic row contract plus rollback rehearsal SQL, not an apply command. The planner assigns stable UUIDs to proposed claim/source rows, preserves claim bodies and caveats as source-backed evidence, emits only edges supported by the live ``edge_type`` enum, and reports every fragment that still needs a reviewed mapping. Production apply remains a separate reviewed step after clone/staging rehearsal. """ from __future__ import annotations import argparse import hashlib import json import sys import uuid from pathlib import Path from typing import Any LIVE_EDGE_TYPES = { "supports", "challenges", "requires", "relates", "contradicts", "supersedes", "derives_from", "cites", "causes", "constrains", "accelerates", } LIVE_EVIDENCE_ROLES = {"grounds", "illustrates", "contradicts"} LIVE_CLAIM_TYPES = {"empirical", "structural", "normative", "meta", "concept"} LIVE_SOURCE_TYPES = {"paper", "article", "transcript", "observation", "dataset", "post", "dm", "other"} CLAIM_TYPE_ALIASES = { "governance": "meta", "process": "meta", } SOURCE_TYPE_ALIASES = { "book": "other", "interview_article": "article", "official_site": "article", "official_site_foreword": "article", "podcast_show_notes": "article", "podcast_summary": "article", "podcast_transcript": "transcript", "url": "article", "document": "other", "note": "other", "proposal_body": "other", } DEFAULT_EVIDENCE_ROLE = "grounds" BODY_EVIDENCE_ROLE = "illustrates" BODY_SOURCE_TYPE = "proposal_body" def _payload_list(payload: dict[str, Any], key: str) -> list[Any]: value = payload.get(key) return value if isinstance(value, list) else [] def _payload_dict(payload: dict[str, Any], key: str) -> dict[str, Any]: value = payload.get(key) return value if isinstance(value, dict) else {} def _claim_candidate_items(payload: dict[str, Any]) -> list[dict[str, Any]]: items: list[dict[str, Any]] = [] seen: set[str] = set() for key in ("claim_candidates", "proposed_claims"): for candidate in _payload_list(payload, key): if not isinstance(candidate, dict): continue claim_key = str(candidate.get("claim_key") or "").strip() dedupe_key = claim_key or json.dumps(candidate, sort_keys=True, ensure_ascii=False) if dedupe_key in seen: continue seen.add(dedupe_key) items.append(candidate) return items def _stable_uuid(proposal_id: str, kind: str, key: str) -> str: return str(uuid.uuid5(uuid.NAMESPACE_URL, f"teleo:kb-rich-contract:{proposal_id}:{kind}:{key}")) def _stable_hash(value: Any) -> str: payload = json.dumps(value, sort_keys=True, ensure_ascii=False) return hashlib.sha256(payload.encode("utf-8")).hexdigest() def _is_uuid(value: Any) -> bool: try: uuid.UUID(str(value)) except (TypeError, ValueError): return False return True def _sql_literal(value: Any) -> str: if value is None: return "null" if isinstance(value, bool): return "true" if value else "false" if isinstance(value, (int, float)): return repr(value) return "'" + str(value).replace("'", "''") + "'" def _sql_text_array(values: list[Any]) -> str: if not values: return "'{}'::text[]" return "array[" + ", ".join(_sql_literal(str(v)) for v in values) + "]::text[]" def _compact_text(value: str, max_chars: int = 8000) -> str: return value if len(value) <= max_chars else value[: max_chars - 3] + "..." def _source_excerpt(source: dict[str, Any]) -> str: excerpts = source.get("key_excerpts") if isinstance(excerpts, list): parts = [item.get("text", "") if isinstance(item, dict) else str(item) for item in excerpts] excerpt = "\n\n".join(part for part in parts if part).strip() else: excerpt = str(source.get("key_excerpt") or source.get("use") or excerpts or "").strip() metadata = { key: source.get(key) for key in ("title", "source_key", "source_quality", "usage_limits", "claim_id") if source.get(key) is not None } if metadata: return _compact_text(json.dumps({"metadata": metadata, "excerpt": excerpt}, ensure_ascii=False, sort_keys=True)) return _compact_text(excerpt) def _source_type(source: dict[str, Any]) -> str: if source.get("source_type"): return str(source["source_type"]) if source.get("url"): return "url" if source.get("local_text") or source.get("local_snapshot") or source.get("storage_path"): return "document" return "note" def _normalize_source_type(raw_type: Any) -> tuple[str, dict[str, Any] | None]: raw_text = str(raw_type or "other").strip() or "other" if raw_text in LIVE_SOURCE_TYPES: return raw_text, None mapped = SOURCE_TYPE_ALIASES.get(raw_text, "other") return mapped, {"from": raw_text, "to": mapped, "reason": "proposal source_type collapsed to live sources.source_type"} def _claim_text(candidate: dict[str, Any]) -> str: headline = str(candidate.get("headline") or "").strip() body = str(candidate.get("body") or "").strip() text = str(candidate.get("text") or "").strip() return headline or body or text def _normalize_claim_type(raw_type: Any) -> tuple[str, dict[str, Any] | None]: raw_text = str(raw_type or "structural").strip() or "structural" if raw_text in LIVE_CLAIM_TYPES: return raw_text, None parts = [part.strip() for part in raw_text.replace(":", "/").split("/") if part.strip()] for part in parts: if part in LIVE_CLAIM_TYPES: return part, {"from": raw_text, "to": part, "reason": "proposal subtype collapsed to live claims.type"} if part in CLAIM_TYPE_ALIASES: mapped = CLAIM_TYPE_ALIASES[part] return mapped, {"from": raw_text, "to": mapped, "reason": "proposal governance/process subtype mapped to meta"} return "meta", {"from": raw_text, "to": "meta", "reason": "unsupported proposal type requires review; meta used for rehearsal"} def _claim_body_excerpt(proposal: dict[str, Any], candidate: dict[str, Any]) -> str: body_packet = { "proposal_id": proposal.get("id"), "source_ref": proposal.get("source_ref") or (proposal.get("payload") or {}).get("source_ref"), "reviewed_by_handle": proposal.get("reviewed_by_handle"), "review_note": proposal.get("review_note"), "claim_key": candidate.get("claim_key"), "headline": candidate.get("headline"), "body": candidate.get("body"), "text": candidate.get("text"), "candidate_review_note": candidate.get("review_note"), "evidence_tier": candidate.get("evidence_tier"), "needs_research": candidate.get("needs_research"), "evidence": candidate.get("evidence"), "edges": candidate.get("edges"), } return _compact_text(json.dumps(body_packet, ensure_ascii=False, sort_keys=True)) def _candidate_claim_rows(proposal: dict[str, Any], payload: dict[str, Any]) -> tuple[list[dict[str, Any]], dict[str, str]]: rows: list[dict[str, Any]] = [] key_to_id: dict[str, str] = {} proposal_id = str(proposal.get("id") or "") created_by = proposal.get("proposed_by_agent_id") for candidate in _claim_candidate_items(payload): claim_key = str(candidate.get("claim_key") or "").strip() if not claim_key: continue claim_id = _stable_uuid(proposal_id, "claim", claim_key) raw_type = candidate.get("type") or candidate.get("node_kind") or "structural" claim_type, type_normalization = _normalize_claim_type(raw_type) key_to_id[claim_key] = claim_id rows.append( { "id": claim_id, "claim_key": claim_key, "type": claim_type, "original_type": raw_type, "type_normalization": type_normalization, "text": _claim_text(candidate), "status": "open", "confidence": candidate.get("confidence"), "tags": candidate.get("tags") or [], "created_by": created_by, } ) return rows, key_to_id def _source_rows( proposal: dict[str, Any], payload: dict[str, Any], claim_rows: list[dict[str, Any]] ) -> tuple[list[dict[str, Any]], dict[str, str], dict[str, str]]: proposal_id = str(proposal.get("id") or "") created_by = proposal.get("proposed_by_agent_id") rows: list[dict[str, Any]] = [] source_key_to_id: dict[str, str] = {} body_source_by_claim_key: dict[str, str] = {} for source in _payload_list(payload, "source_candidates"): if not isinstance(source, dict): continue source_key = str(source.get("source_key") or "").strip() if not source_key: continue source_id = _stable_uuid(proposal_id, "source", source_key) source_key_to_id[source_key] = source_id excerpt = _source_excerpt(source) raw_source_type = _source_type(source) source_type, source_type_normalization = _normalize_source_type(raw_source_type) row = { "id": source_id, "source_key": source_key, "source_type": source_type, "original_source_type": raw_source_type, "source_type_normalization": source_type_normalization, "url": source.get("url"), "storage_path": source.get("local_text") or source.get("local_snapshot") or source.get("storage_path"), "excerpt": excerpt, "hash": _stable_hash({"proposal_id": proposal_id, "source_key": source_key, "excerpt": excerpt}), "created_by": created_by, } rows.append(row) claim_candidates = {str(candidate.get("claim_key") or ""): candidate for candidate in _claim_candidate_items(payload)} for claim in claim_rows: claim_key = claim["claim_key"] candidate = claim_candidates.get(claim_key) if not candidate: continue source_key = f"{claim_key}__proposal_body" source_id = _stable_uuid(proposal_id, "claim-body-source", claim_key) body_source_by_claim_key[claim_key] = source_id excerpt = _claim_body_excerpt(proposal, candidate) source_type, source_type_normalization = _normalize_source_type(BODY_SOURCE_TYPE) rows.append( { "id": source_id, "source_key": source_key, "source_type": source_type, "original_source_type": BODY_SOURCE_TYPE, "source_type_normalization": source_type_normalization, "url": None, "storage_path": None, "excerpt": excerpt, "hash": _stable_hash({"proposal_id": proposal_id, "source_key": source_key, "excerpt": excerpt}), "created_by": created_by, } ) return rows, source_key_to_id, body_source_by_claim_key def _resolve_claim_ref(ref: Any, claim_key_to_id: dict[str, str]) -> str | None: if not ref: return None ref_text = str(ref) return claim_key_to_id.get(ref_text) or ref_text def _mapping_section(mappings: dict[str, Any] | None, key: str) -> dict[str, Any]: if not mappings: return {} value = mappings.get(key) return value if isinstance(value, dict) else {} def _mapping_value(raw: Any, *, original: Any, default_reason: str) -> tuple[Any, dict[str, Any] | None]: if raw is None: return original, None if isinstance(raw, dict): mapped = raw.get("to") or raw.get("claim_id") or raw.get("target") or raw.get("edge_type") if mapped is None: return original, None return mapped, {"from": original, "to": mapped, "reason": raw.get("reason") or default_reason} return raw, {"from": original, "to": raw, "reason": default_reason} def _map_lookup( mappings: dict[str, Any] | None, section: str, original: Any, *, default_reason: str ) -> tuple[Any, dict[str, Any] | None]: if original is None: return original, None raw = _mapping_section(mappings, section).get(str(original)) return _mapping_value(raw, original=original, default_reason=default_reason) def _edge_fragments(payload: dict[str, Any]) -> list[tuple[dict[str, Any], str | None]]: fragments: list[tuple[dict[str, Any], str | None]] = [] for edge in _payload_list(payload, "proposed_supersession_edges"): if isinstance(edge, dict): fragments.append((edge, edge.get("from") or edge.get("from_claim_key"))) for candidate in _claim_candidate_items(payload): default_from = candidate.get("claim_key") for edge in _payload_list(candidate, "edges"): if isinstance(edge, dict): fragments.append((edge, default_from)) return fragments def _plan_edges( payload: dict[str, Any], claim_key_to_id: dict[str, str], mappings: dict[str, Any] | None = None, ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: planned: list[dict[str, Any]] = [] blocked: list[dict[str, Any]] = [] seen: set[tuple[str, str, str]] = set() for edge, default_from_key in _edge_fragments(payload): original_edge_type = edge.get("edge_type") edge_type, edge_type_mapping = _map_lookup( mappings, "edge_type_aliases", original_edge_type, default_reason="reviewed mapping overlay converted proposal edge_type to live edge_type", ) from_ref = edge.get("from_claim") or edge.get("from_claim_id") or edge.get("from") or default_from_key target = ( edge.get("to_claim") or edge.get("to_claim_id") or edge.get("target_claim_id") or edge.get("to_existing_claim") or edge.get("target") ) mapped_target, target_mapping = _map_lookup( mappings, "target_claim_mappings", target, default_reason="reviewed mapping overlay converted external/context target to canonical claim", ) from_claim = _resolve_claim_ref(from_ref, claim_key_to_id) to_claim = _resolve_claim_ref(mapped_target, claim_key_to_id) reasons = [] if edge_type not in LIVE_EDGE_TYPES: reasons.append(f"unsupported edge_type {edge_type!r}") if not from_claim: reasons.append("missing from_claim") if not to_claim: reasons.append("missing to_claim") if from_claim and not _is_uuid(from_claim): reasons.append("from_claim does not resolve to a UUID") if to_claim and not _is_uuid(to_claim): reasons.append("to_claim does not resolve to a UUID") if ( target and str(target) not in claim_key_to_id and not _is_uuid(target) and not edge.get("target_claim_id") and not edge.get("to_claim_id") and target_mapping is None ): reasons.append("target is external/context text and needs a reviewed canonical mapping") if from_claim and to_claim and from_claim == to_claim: reasons.append("self-edge refused") if reasons: blocked.append( { "kind": "edge", "fragment": edge, "default_from_key": default_from_key, "edge_type_mapping": edge_type_mapping, "target_mapping": target_mapping, "resolved": {"from_claim": from_claim, "to_claim": to_claim, "edge_type": edge_type}, "reasons": reasons, } ) continue key = (from_claim, to_claim, str(edge_type)) if key in seen: continue seen.add(key) planned.append( { "from_claim": from_claim, "to_claim": to_claim, "edge_type": edge_type, "weight": edge.get("weight"), "edge_type_original": original_edge_type, "edge_type_mapping": edge_type_mapping, "target_original": target, "target_mapping": target_mapping, "source_fragment": edge, } ) return planned, blocked def _plan_evidence( payload: dict[str, Any], claim_key_to_id: dict[str, str], source_key_to_id: dict[str, str], body_source_by_claim_key: dict[str, str], ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: planned: list[dict[str, Any]] = [] blocked: list[dict[str, Any]] = [] seen: set[tuple[str, str, str]] = set() def add_evidence(claim_id: str, source_id: str, role: str, fragment: dict[str, Any]) -> None: key = (claim_id, source_id, role) if key not in seen: seen.add(key) planned.append({"claim_id": claim_id, "source_id": source_id, "role": role, "weight": None, "source_fragment": fragment}) for candidate in _claim_candidate_items(payload): claim_key = str(candidate.get("claim_key") or "") claim_id = claim_key_to_id.get(claim_key) if not claim_id: continue body_source_id = body_source_by_claim_key.get(claim_key) if body_source_id: add_evidence(claim_id, body_source_id, BODY_EVIDENCE_ROLE, {"source_key": f"{claim_key}__proposal_body"}) for evidence in _payload_list(candidate, "evidence"): if isinstance(evidence, dict): source_key = evidence.get("source_key") source_id = evidence.get("source_id") or source_key_to_id.get(str(source_key or "")) role = evidence.get("role") or DEFAULT_EVIDENCE_ROLE fragment = evidence else: source_key = str(evidence) source_id = source_key_to_id.get(source_key) role = DEFAULT_EVIDENCE_ROLE fragment = {"source_key": source_key} reasons = [] if role not in LIVE_EVIDENCE_ROLES: reasons.append(f"unsupported evidence_role {role!r}") if not source_id: reasons.append("missing source_id; source_key not present in source_candidates") if reasons: blocked.append({"kind": "evidence", "claim_key": claim_key, "fragment": fragment, "reasons": reasons}) continue add_evidence(claim_id, source_id, role, fragment) return planned, blocked def _sanitize_framework_powers(powers: Any) -> tuple[Any, list[dict[str, Any]]]: if not isinstance(powers, list): return powers, [] sanitized: list[Any] = [] deferred: list[dict[str, Any]] = [] for power in powers: if not isinstance(power, dict): sanitized.append(power) continue power_copy = dict(power) examples = power_copy.get("examples") if isinstance(examples, list): kept_examples: list[Any] = [] for example in examples: if "teleo" in str(example).lower(): deferred.append( { "power": power_copy.get("name"), "reason": "Teleo application examples are deferred from the framework canonical row.", } ) else: kept_examples.append(example) power_copy["examples"] = kept_examples sanitized.append(power_copy) return sanitized, deferred def _sanitize_conceptual_neighborhood(neighborhood: Any) -> tuple[Any, int]: if not isinstance(neighborhood, dict): return neighborhood, 0 clean = dict(neighborhood) candidates = clean.get("additional_moat_neighborhood_candidates") removed = 0 if isinstance(candidates, list): kept: list[Any] = [] for candidate in candidates: if isinstance(candidate, dict) and str(candidate.get("edge_type") or "").lower() == "defer": removed += 1 continue kept.append(candidate) clean["additional_moat_neighborhood_candidates"] = kept return clean, removed def _reasoning_tool_rows(proposal: dict[str, Any], payload: dict[str, Any]) -> list[dict[str, Any]]: tool = _payload_dict(payload, "proposed_reasoning_tool_node") name = str(tool.get("name") or "").strip() if not name: return [] proposal_id = str(proposal.get("id") or "") sanitized_powers, deferred_examples = _sanitize_framework_powers(payload.get("seven_powers")) sanitized_neighborhood, deferred_neighborhood_count = _sanitize_conceptual_neighborhood( payload.get("conceptual_neighborhood") ) description_packet = { "summary": tool.get("summary"), "core_test": tool.get("core_test"), "use_when": tool.get("use_when"), "do_not_use_when": tool.get("do_not_use_when"), "position_in_leo_strategy_stack": tool.get("position_in_leo_strategy_stack"), "review_gate": tool.get("review_gate"), "seven_powers": sanitized_powers, "power_lifecycle_map": payload.get("power_lifecycle_map"), "conceptual_neighborhood": sanitized_neighborhood, "approval_change_log": payload.get("approval_change_log"), "deferred_application_examples_removed": len(deferred_examples), "deferred_neighborhood_candidates_removed": deferred_neighborhood_count, "deferred_boundary": ( "Teleo-specific application/self-positioning examples are not canonicalized in this reasoning_tool row; " "stage them separately if reopened and approved." ), "source_ref": proposal.get("source_ref") or payload.get("source_ref"), "review_note": proposal.get("review_note"), } category = str(tool.get("category") or "strategy_framework").strip() or "strategy_framework" return [ { "id": _stable_uuid(proposal_id, "reasoning-tool", name.lower()), "agent_id": proposal.get("proposed_by_agent_id"), "name": name, "description": _compact_text(json.dumps(description_packet, ensure_ascii=False, sort_keys=True), 20000), "category": category, "source_fragment": tool, } ] def _reasoning_tool_insert_sql(row: dict[str, Any]) -> str: return f"""insert into public.reasoning_tools (id, agent_id, name, description, category) select {_sql_literal(row['id'])}::uuid, {_sql_literal(row['agent_id'])}::uuid, {_sql_literal(row['name'])}, {_sql_literal(row['description'])}, {_sql_literal(row['category'])} where not exists (select 1 from public.reasoning_tools where id = {_sql_literal(row['id'])}::uuid);""" def _claim_insert_sql(row: dict[str, Any]) -> str: return f"""insert into public.claims (id, type, text, status, confidence, tags, created_by) select {_sql_literal(row['id'])}::uuid, {_sql_literal(row['type'])}, {_sql_literal(row['text'])}, {_sql_literal(row['status'])}, {_sql_literal(row['confidence'])}, {_sql_text_array(row['tags'])}, {_sql_literal(row['created_by'])}::uuid where not exists (select 1 from public.claims where id = {_sql_literal(row['id'])}::uuid);""" def _source_insert_sql(row: dict[str, Any]) -> str: return f"""insert into public.sources (id, source_type, url, storage_path, excerpt, hash, created_by) select {_sql_literal(row['id'])}::uuid, {_sql_literal(row['source_type'])}, {_sql_literal(row['url'])}, {_sql_literal(row['storage_path'])}, {_sql_literal(row['excerpt'])}, {_sql_literal(row['hash'])}, {_sql_literal(row['created_by'])}::uuid where not exists (select 1 from public.sources where id = {_sql_literal(row['id'])}::uuid);""" def _edge_insert_sql(row: dict[str, Any]) -> str: return f"""insert into public.claim_edges (from_claim, to_claim, edge_type, weight) select {_sql_literal(row['from_claim'])}::uuid, {_sql_literal(row['to_claim'])}::uuid, {_sql_literal(row['edge_type'])}::edge_type, {_sql_literal(row['weight'])} where not exists ( select 1 from public.claim_edges where from_claim = {_sql_literal(row['from_claim'])}::uuid and to_claim = {_sql_literal(row['to_claim'])}::uuid and edge_type = {_sql_literal(row['edge_type'])}::edge_type);""" def _evidence_insert_sql(row: dict[str, Any]) -> str: return f"""insert into public.claim_evidence (claim_id, source_id, role, weight) select {_sql_literal(row['claim_id'])}::uuid, {_sql_literal(row['source_id'])}::uuid, {_sql_literal(row['role'])}::evidence_role, {_sql_literal(row['weight'])} where not exists ( select 1 from public.claim_evidence where claim_id = {_sql_literal(row['claim_id'])}::uuid and source_id = {_sql_literal(row['source_id'])}::uuid and role = {_sql_literal(row['role'])}::evidence_role);""" def build_rollback_sql(plan: dict[str, Any]) -> str: statements: list[str] = [ "begin;", "set local statement_timeout = '30s';", "-- rollback-only rehearsal: do not replace ROLLBACK with COMMIT without separate review", ] statements.extend(_reasoning_tool_insert_sql(row) for row in plan["planned_rows"]["reasoning_tools"]) statements.extend(_claim_insert_sql(row) for row in plan["planned_rows"]["claims"]) statements.extend(_source_insert_sql(row) for row in plan["planned_rows"]["sources"]) statements.extend(_evidence_insert_sql(row) for row in plan["planned_rows"]["claim_evidence"]) statements.extend(_edge_insert_sql(row) for row in plan["planned_rows"]["claim_edges"]) statements.append("rollback;") return "\n\n".join(statements) + "\n" def build_creation_plan(proposal: dict[str, Any], mappings: dict[str, Any] | None = None) -> dict[str, Any]: payload = proposal.get("payload") if isinstance(proposal.get("payload"), dict) else {} proposal_id = str(proposal.get("id") or "") claim_rows, claim_key_to_id = _candidate_claim_rows(proposal, payload) source_rows, source_key_to_id, body_source_by_claim_key = _source_rows(proposal, payload, claim_rows) edge_rows, blocked_edges = _plan_edges(payload, claim_key_to_id, mappings) evidence_rows, blocked_evidence = _plan_evidence(payload, claim_key_to_id, source_key_to_id, body_source_by_claim_key) reasoning_tool_rows = _reasoning_tool_rows(proposal, payload) old_claim = _payload_dict(payload, "old_claim") concept_map = _payload_dict(payload, "proposed_concept_map_dependency") unresolved_design_decisions: list[dict[str, Any]] = [] if old_claim.get("id") and len(claim_rows) > 1: unresolved_design_decisions.append( { "decision": "many_successor_supersession", "reason": "public.claims.superseded_by stores one UUID, but this proposal splits one claim into multiple successors.", "safe_default": "Do not update public.claims.superseded_by in this plan; use reviewed edges or an aggregate successor node.", } ) if concept_map: unresolved_design_decisions.append( { "decision": "concept_map_storage", "reason": "The proposal includes a concept-map node but the live canonical claim/source contract has no first-class concept-map table in this apply lane.", "safe_default": "Keep concept-map content as source-backed context until a reviewed schema lane exists.", } ) plan = { "proposal_id": proposal_id, "status": proposal.get("status"), "proposal_type": proposal.get("proposal_type"), "reviewed_by_handle": proposal.get("reviewed_by_handle"), "read_only": True, "runtime_change": False, "direct_production_apply_ready": False, "rehearsal_scope": "rollback_only_sql", "mapping_overlay_applied": bool(mappings), "mapping_overlay": mappings or {}, "generated_ids": { "reasoning_tools": {row["name"]: row["id"] for row in reasoning_tool_rows}, "claims": claim_key_to_id, "sources": source_key_to_id, "claim_body_sources": body_source_by_claim_key, }, "planned_rows": { "reasoning_tools": reasoning_tool_rows, "claims": claim_rows, "sources": source_rows, "claim_evidence": evidence_rows, "claim_edges": edge_rows, }, "blocked_fragments": blocked_edges + blocked_evidence, "unresolved_design_decisions": unresolved_design_decisions, "next_action": ( "Run rollback SQL in a clone/staging DB, review blocked edge/context mappings, then create strict child " "proposals or a separately approved claim/source creation apply tool. Do not apply this directly to production." ), } plan["rollback_rehearsal_sql"] = build_rollback_sql(plan) return plan def load_from_input(path: str) -> list[dict[str, Any]]: data = json.loads(Path(path).read_text(encoding="utf-8")) if isinstance(data, list): return data if isinstance(data, dict) and isinstance(data.get("proposals"), list): return data["proposals"] if isinstance(data, dict): return [data] raise SystemExit(f"unsupported proposal JSON shape in {path}") def parse_args(argv: list[str]) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--input-json", required=True, help="proposal JSON or list of proposal JSON objects") parser.add_argument("--mapping-json", help="optional reviewed mapping overlay for edge types and external targets") parser.add_argument("--sql-out", help="optional path for concatenated rollback rehearsal SQL") return parser.parse_args(argv) def main(argv: list[str] | None = None) -> int: args = parse_args(sys.argv[1:] if argv is None else argv) mappings = json.loads(Path(args.mapping_json).read_text(encoding="utf-8")) if args.mapping_json else None plans = [build_creation_plan(proposal, mappings) for proposal in load_from_input(args.input_json)] if args.sql_out: Path(args.sql_out).write_text("\n\n".join(plan["rollback_rehearsal_sql"] for plan in plans), encoding="utf-8") print(json.dumps({"creation_plans": plans}, indent=2, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())