← Files MightShapeARCHIVED FILE

scripts/score_build_gate.py

7.96 KB · Sep 30, 2026 · 23:14 UTC

↓ Download file

#!/usr/bin/env python3
"""Advisory, reversibility-aware Build Gate scoring."""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

from dc_core import DesignCouncilError, json_output, load_json, now_utc


def _score_problem(state: dict[str, Any]) -> int:
    score = 0
    challenge = state.get("challenge", {})
    if challenge.get("current_problem_frame"):
        score += 5
    active_povs = [pov for pov in state.get("povs", []) if pov.get("status") == "active"]
    if active_povs:
        best = max((pov.get("heuristic_score") or 0 for pov in active_povs), default=0)
        score += 3 if best >= 21 else 2
    if challenge.get("desired_outcome"):
        score += 2
    return min(10, score)


def assess_build_gate(state: dict[str, Any]) -> dict[str, Any]:
    if not isinstance(state, dict):
        raise DesignCouncilError("project state must be an object")
    classification = state.get("classification", {})
    consequence = classification.get("consequence_of_error", "UNKNOWN")
    reversibility = classification.get("reversibility", "UNKNOWN")
    evidence = state.get("evidence", [])
    human = [item for item in evidence if item.get("provenance") in {"HUMAN_INTERVIEW", "OBSERVED_HUMAN_BEHAVIOR"}]
    authoritative = [item for item in evidence if item.get("provenance") == "AUTHORITATIVE_RESEARCH"]
    synthetic = [item for item in evidence if str(item.get("provenance", "")).startswith("SYNTHETIC_")]
    active_povs = [item for item in state.get("povs", []) if item.get("status") == "active"]
    territories = {item.get("territory") for item in state.get("ideas", []) if item.get("status", "active") != "retired"}
    open_high = [item for item in state.get("assumptions", []) if item.get("status") == "OPEN_HIGH_RISK"]
    testing = [item for item in state.get("assumptions", []) if item.get("status") == "TESTING"]
    completed_experiments = [item for item in state.get("experiments", []) if item.get("hypothesis_status") in {"supported", "weakened", "falsified", "inconclusive"}]
    prototypes = state.get("prototypes", [])
    reality_checks = state.get("reality_checks", [])
    contradictions = [item for item in reality_checks if item.get("outcome") in {"contradicted", "transformed"}]

    problem = _score_problem(state)
    human_grounding = min(10, len({item.get("participant_id") for item in human if item.get("participant_id")}) * 2 + sum(min(3, int(item.get("evidence_strength", 0))) for item in human))
    evidence_quality = min(10, sum(min(2, int(item.get("evidence_strength", 0))) for item in evidence) + min(2, len(authoritative)))
    pov_quality = min(10, round(max((item.get("heuristic_score") or 0 for item in active_povs), default=0) / 3))
    solution_diversity = min(10, len(territories) * 2)
    assumption_learning = min(10, len(completed_experiments) * 3 + len([item for item in state.get("assumptions", []) if item.get("status") in {"RESOLVED", "FALSIFIED"}]) * 2)
    prototype_learning = min(10, len(prototypes) + len(completed_experiments) * 3)
    heuristics = {
        "problem_clarity": problem,
        "human_grounding": human_grounding,
        "evidence_quality": evidence_quality,
        "pov_quality": pov_quality,
        "solution_diversity": solution_diversity,
        "assumption_learning": assumption_learning,
        "prototype_test_learning": prototype_learning,
        "open_high_risk_assumptions": len(open_high),
        "synthetic_human_contradictions": len(contradictions),
        "reversibility": reversibility,
        "consequence_of_error": consequence,
    }
    reasons: list[str] = []
    high_stakes = consequence == "HIGH" or reversibility == "DIFFICULT"
    easy_learning_build = consequence == "LOW" and reversibility == "EASY"
    frame_absent = not state.get("challenge", {}).get("current_problem_frame") and not active_povs
    pivotal_untested = bool(open_high) and not completed_experiments

    if frame_absent and not easy_learning_build:
        status = "REFRAME_FIRST"
        reasons.append("No current problem frame or active POV is recorded.")
    elif problem < 4 and high_stakes:
        status = "REFRAME_FIRST"
        reasons.append("The problem frame is too fragile for a consequential or difficult-to-reverse build.")
    elif pivotal_untested or (high_stakes and human_grounding == 0):
        status = "TEST_FIRST"
        if pivotal_untested:
            reasons.append("At least one open high-risk assumption has no completed experiment.")
        if high_stakes and human_grounding == 0:
            reasons.append("High-consequence or difficult-to-reverse work has no direct human grounding.")
    elif problem >= 7 and (not open_high) and (prototype_learning >= 3 or easy_learning_build) and (human_grounding > 0 or not high_stakes):
        status = "READY"
        reasons.append("The frame and pivotal learning are proportionate to the build's reversibility and consequence.")
    else:
        status = "READY_WITH_KNOWN_RISK" if easy_learning_build or (problem >= 5 and not high_stakes) else "TEST_FIRST"
        reasons.append("Proceed only with the visible uncertainties and a reversible implementation boundary." if status == "READY_WITH_KNOWN_RISK" else "One focused experiment can reduce a material uncertainty before production investment.")

    if synthetic and not human:
        reasons.append("Synthetic signals are present without human evidence; this is Evidence Debt, not proof.")
    if contradictions:
        reasons.append("Synthetic-to-human contradiction or transformation requires the current frame to reflect the Reality Check.")
    if len(territories) < 3 and state.get("ideas"):
        reasons.append("The active solution set covers fewer than three conceptual territories.")
    if testing:
        reasons.append("Some assumptions are still under test; preserve their status rather than implying resolution.")
    return {
        "status": status,
        "assessed_at": now_utc(),
        "heuristics": heuristics,
        "reasons": reasons,
        "unresolved_assumptions": [item.get("id") for item in open_high],
        "risk_formula": "uncertainty × cost_of_being_wrong × irreversibility",
        "advisory": True,
        "override_available": True,
        "note": "A user may say 'build it anyway'; record Design/Evidence Debt and proceed reversibly.",
    }


def apply_gate_assessment(project_root: str | Path, assessment: dict[str, Any]) -> dict[str, Any]:
    from project_state import commit_project, load_project

    required = {"status", "assessed_at", "heuristics", "reasons", "unresolved_assumptions"}
    if not required <= assessment.keys():
        raise DesignCouncilError(f"assessment missing: {', '.join(sorted(required - assessment.keys()))}")
    if assessment["status"] not in {"READY", "READY_WITH_KNOWN_RISK", "TEST_FIRST", "REFRAME_FIRST"}:
        raise DesignCouncilError("invalid Build Gate status")
    state = load_project(project_root)
    existing_override = state.get("build_gate", {}).get("override", {"active": False, "recorded_at": None, "note": None})
    state["build_gate"] = {
        "status": assessment["status"],
        "assessed_at": assessment["assessed_at"],
        "heuristics": assessment["heuristics"],
        "reasons": assessment["reasons"],
        "unresolved_assumptions": assessment["unresolved_assumptions"],
        "override": existing_override,
    }
    return commit_project(project_root, state, "BUILD_GATE_ASSESSED", {"status": assessment["status"]})


def main() -> int:
    parser = argparse.ArgumentParser(description="Assess the advisory MightShape Build Gate")
    parser.add_argument("input", nargs="?", help="Project state JSON file; stdin when omitted")
    args = parser.parse_args()
    try:
        state = load_json(args.input) if args.input else json.load(sys.stdin)
        json_output(assess_build_gate(state))
    except (DesignCouncilError, json.JSONDecodeError) as exc:
        print(f"MightShape error: {exc}", file=sys.stderr)
        return 2
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 2ea9d90b4499f3eb78541ba9ca41d9390d0a18bad3968a1b1d40227a49d3dd0c