#!/usr/bin/env python3
"""Portable independent checks of the C1585–C1634 models and journal.

Uses only the Python standard library, reads source snapshots directly, and
does not import the research writer. Each reported group is an arithmetic or
integrity unit; counts do not represent independent historical witnesses.
"""
import argparse
from datetime import datetime
from fractions import Fraction
import hashlib
import json
import math
from pathlib import Path


def digest_json(value):
    raw = json.dumps(value, ensure_ascii=False, sort_keys=True,
                     separators=(",", ":")).encode()
    return hashlib.sha256(raw).hexdigest()


def inverse(n):
    if not isinstance(n, int) or n <= 0:
        raise ValueError("Only positive original duration legs are admitted")
    place = 1
    while n % 10 == 0:
        n //= 10
        place *= 10
    out = 0
    while n:
        n, digit = divmod(n, 10)
        out = out * 10 + digit
    return out * place


def rounded5(n):
    quotient, remainder = divmod(n, 5)
    return 5 * (quotient + (remainder >= 3))


def matrix_rank(rows):
    a = [list(map(Fraction, row)) for row in rows]
    pivot_row = 0
    for col in range(len(a[0])):
        choices = [r for r in range(pivot_row, len(a)) if a[r][col]]
        if not choices:
            continue
        r = choices[0]
        a[r], a[pivot_row] = a[pivot_row], a[r]
        div = a[pivot_row][col]
        a[pivot_row] = [n/div for n in a[pivot_row]]
        for other in range(len(a)):
            if other != pivot_row:
                factor = a[other][col]
                a[other] = [x-factor*y for x,y in zip(a[other],a[pivot_row])]
        pivot_row += 1
        if pivot_row == len(a):
            break
    return pivot_row


def verify(root, require_50=False):
    model_dir = root / "model"
    models = {p.stem:json.loads(p.read_text()) for p in model_dir.glob("*.json")}
    groups, covered, qualitative = [], set(), set()

    def eq(actual, expected, label):
        if actual != expected:
            raise AssertionError(f"{label}: {actual!r} != {expected!r}")

    def fields(name, expected, label=None):
        value = models[name]
        for key, wanted in expected.items():
            eq(value[key], wanted, name+"."+key)
        covered.add(name)
        groups.append({"group":label or name,"status":"PASS","models":[name]})

    def add_group(label, names=()):
        covered.update(names)
        groups.append({"group":label,"status":"PASS","models":list(names)})

    # Integrity checks bind the finite calculations to their actual sources.
    for filename, binding in models["source_bindings"].items():
        data = (root/"evidence"/filename).read_bytes()
        eq(hashlib.sha256(data).hexdigest(),binding["sha256"],filename+" source digest")
    add_group("Frozen evidence source bindings",["source_bindings"])
    journal = json.loads((root/"journal.json").read_text())
    parent = json.loads((root/"evidence/C1584_parent_record.json").read_text())
    parent_unsigned = {k:v for k,v in parent.items() if k!="sha256"}
    eq(digest_json(parent_unsigned),parent["sha256"],"C1584 parent content hash")
    previous = parent["sha256"]
    for offset, record in enumerate(journal):
        eq(record["step"],1585+offset,"sequential step")
        eq(record["predecessor_sha256"],previous,"predecessor link")
        eq(digest_json({k:v for k,v in record.items() if k!="sha256"}),
           record["sha256"],"record content hash")
        if not all(record.get(k) for k in ["title","question","finding","reassessment","sources"]):
            raise AssertionError("A journal action lacks question, result, sources, or reassessment")
        if datetime.fromisoformat(record["opened_utc"]) > datetime.fromisoformat(record["closed_utc"]):
            raise AssertionError("Action closed before opening")
        previous = record["sha256"]
    if require_50:
        eq(len(journal),50,"required complete cycle")
        eq((root/"pending.json").exists(),False,"no unfinished action")
    if not 20 <= len(journal) <= 50:
        raise AssertionError("Expected 20–50 sequential completed actions")
    add_group("Sequential journal, reassessments, and predecessor hashes")
    scope = models["scope"]
    fields("scope",{"MSS":["MT","LXX","SP"],"anchors_BC":[1406,6],
                    "Rounded_endpoint":"AD 2166",
                    "deferred":["second decimal reversal","deeper mirror study","Gear transport to Creation"]})

    # Read named actual lives independently from frozen inherited source rows.
    inherited = json.loads((root/"evidence/postflood_rows_inherited.json").read_text())
    file18 = (root/"evidence/File18.md").read_text()
    names = ["Arphaxad","2nd Cainan","Shelah","Eber","Peleg","Reu","Serug","Nahor","Terah"]
    variants = {"MT":"MT_native_OFF","LXX":"LXX_native_ON","SP":"SP_native_OFF_official145"}
    actual, rounded, shem_lives = {}, {}, {}
    for mss, key in variants.items():
        rows = inherited[key]
        for row in rows:
            eq(row["b"]+row["r"],row["L"],mss+" actual begetting plus remainder")
            if row["source"]["raw"] not in file18:
                raise AssertionError("Inherited actual row absent from frozen File18")
        row_map = {r["name"]:r["L"] for r in rows}
        actual[mss] = [row_map.get(n,0) for n in names]
        rounded[mss] = [rounded5(n) for n in actual[mss]]
        shem_lives[mss] = row_map["Shem"]
    eq(set(shem_lives.values()),{600},"common source Shem life")
    # Read the complete rounded Abraham–Moses suffix from File51a, including
    # its boundary at2431, rather than deducing1025 by an endpoint residual.
    source51 = (root/"evidence/File51a.md").read_text().splitlines()
    cum_rows = {}
    for line in source51:
        cells = [x.strip() for x in line.strip().strip("|").split("|")]
        if len(cells)==8 and cells[0].isdigit() and 20<=int(cells[0])<=26:
            if cells[3].isdigit() and cells[6].isdigit():
                cum_rows[cells[1]] = {"life":int(cells[3]),"date":int(cells[6])}
    downstream_names = ["Abraham","Isaac","Jacob","Levi","Kohath","Amram","Moses"]
    eq(set(cum_rows),set(downstream_names),"complete downstream source rows")
    suffix = sum(cum_rows[n]["life"] for n in downstream_names)
    eq(suffix,cum_rows["Abraham"]["date"]-1406,"source suffix versus boundary")
    eq(suffix,1025,"source suffix")
    expected_rows = {m:{"names":names,"actual":actual[m],"rounded":rounded[m]} for m in variants}
    expected_rows.update(totals={m:sum(rounded[m]) for m in variants},shared_downstream=suffix)
    fields("postflood_rounded_rows",expected_rows,"Actual source rows, row rounding, and full1025 suffix")

    actual_diff = [a-b for a,b in zip(actual["LXX"],actual["MT"])]
    rounded_diff = [a-b for a,b in zip(rounded["LXX"],rounded["MT"])]
    cainan = rounded_diff[names.index("2nd Cainan")]
    fields("LXX_910_source",{"names":names,"actual_differences":actual_diff,
        "rounded_differences":rounded_diff,"actual_total":sum(actual_diff),
        "rounded_total":sum(rounded_diff),"Cainan":cainan,
        "other_rounded_rows":sum(rounded_diff)-cainan,
        "rounding_change":sum(rounded_diff)-sum(actual_diff)})
    eq((cainan,sum(rounded_diff)-cainan),(460,450),"Cainan plus other rows")
    sp_diff = [a-b for a,b in zip(rounded["SP"],rounded["MT"])]
    fields("SP_120_source",{"names":names,"differences":sp_diff,"sum":sum(sp_diff),
        "Eber":sp_diff[names.index("Eber")],"Terah":sp_diff[names.index("Terah")]})

    nodes = json.loads((root/"evidence/cumulative_source_nodes_inherited.json").read_text())
    prefix = json.loads((root/"evidence/prefix_source_rows_inherited.json").read_text())
    for m in variants:
        eq(prefix["rounded"][m],[rounded5(n) for n in prefix["actual"][m]],m+" prefix rounding")
        eq(prefix["sums"][m],sum(prefix["rounded"][m]),m+" prefix sum")
        eq(nodes[m]["S"]-nodes[m]["F"],600,m+" source Shem edge")
        eq(nodes[m]["N"]-nodes[m]["S"],950,m+" source Noah edge")
    differences = {n:{"LXX_minus_MT":nodes["LXX"][n]-nodes["MT"][n],
                      "SP_minus_MT":nodes["SP"][n]-nodes["MT"][n]} for n in ["F","S","N"]}
    lamech_gap = prefix["rounded"]["LXX"][-1]-prefix["rounded"]["MT"][-1]
    creation_gap = nodes["LXX"]["C"]-nodes["MT"]["C"]
    fields("source_translation_extent",{"Flood_Shem_Noah":differences,
        "LXX_Lamech_difference":lamech_gap,"LXX_creation_difference":creation_gap,
        "source_Creation_decomposition":[cainan,sum(rounded_diff)-cainan+lamech_gap],
        "source_Flood_decomposition":[cainan,sum(rounded_diff)-cainan]})
    eq(creation_gap,sum(rounded_diff)+lamech_gap,"Creation versus Flood displacement")

    original = {m:sum(rounded[m])+suffix for m in variants}
    reversed_legs = {m:inverse(n) for m,n in original.items()}
    for m in variants:
        eq(original[m],nodes[m]["F"]-1406,m+" reconstructed lower duration")
    fields("lower_spine_map",{"original":original,"reversed":reversed_legs,
        "source_gaps_LXX_MT_SP":[original["LXX"]-original["MT"],original["MT"]-original["SP"]],
        "inverse_gaps":[reversed_legs["LXX"]-reversed_legs["MT"],reversed_legs["MT"]-reversed_legs["SP"]]})
    decimal_rows = []
    for m,n in original.items():
        h = n//1000; t=(n//100)%10; u=(n//10)%10
        eq(inverse(n)-n,990*(u-h),m+" three-digit-core place-value rule")
        decimal_rows.append({"MSS":m,"core":[h,t,u],"inverse_minus_original":inverse(n)-n})
    fields("lower_decimal_mechanism",{"rows":decimal_rows,
        "MT_SP_inverse_gap_components":[original["MT"]-original["SP"],original["SP"]-reversed_legs["SP"]]})
    stage = inverse(1406-6)-(1406-6)
    held = {m:1406+n for m,n in reversed_legs.items()}
    completed = {m:n+stage for m,n in held.items()}
    eq(stage,2700,"common inverse-anchor stage difference")
    eq(completed["SP"],nodes["MT"]["S"],"SP completed Flood and MT ordinary Shem junction")
    fields("SP_MT_Shem_condition",{"held_Floods":held,"completed_Floods":completed,
                                    "ordinary_MT_Shem":nodes["MT"]["S"]})
    cuts = {m:{"cut_at_Flood":inverse(original[m])+inverse(600),
               "whole_to_Shem":inverse(original[m]+600)} for m in ["MT","LXX"]}
    cut_gap = cuts["LXX"]["cut_at_Flood"]-cuts["MT"]["cut_at_Flood"]
    merged_gap = cuts["LXX"]["whole_to_Shem"]-cuts["MT"]["whole_to_Shem"]
    fields("translation_cut_domain",{"rows":cuts,"separate_Flood_cut_gap":cut_gap,
        "merged_Shem_gap":merged_gap,"excess":merged_gap-cut_gap})
    eq([cut_gap,merged_gap],[910,1900],"cut-dependent translation widths")
    paths = {m:[1406,held[m],held[m]+inverse(600),held[m]+inverse(600)+inverse(950)] for m in ["MT","LXX"]}
    upper = {m:sum(prefix["rounded"][m]) for m in ["MT","LXX"]}
    upper_inverse = {m:inverse(n) for m,n in upper.items()}
    head_gap = paths["LXX"][-1]+upper_inverse["LXX"]-paths["MT"][-1]-upper_inverse["MT"]
    fields("inverse_prefix_extent",{"paths":paths,
        "node_differences":[b-a for a,b in zip(paths["MT"],paths["LXX"])],
        "upper_original_legs":upper,"upper_inverse_legs":upper_inverse,
        "fully_cut_head_difference":head_gap})

    comparison = {m:{"Flood_BC":f,"to_restoration":f-536,"units70":(f-536)//70,
                    "remainder70":(f-536)%70,"remainder490":(f-536)%490} for m,f in completed.items()}
    fields("three_cumulative_restoration",comparison)
    compensation = stage+1406-536
    fields("restoration_compensation",{"lower_leg_indices":{m:n//70 for m,n in reversed_legs.items()},
        "stage_shift":stage,"Conquest_to_restoration":1406-536,
        "common_addition":compensation,"common_units70":compensation//70,
        "residues70":[stage%70,(1406-536)%70]})
    eq(compensation,51*70,"common restoration compensation")
    fields("restoration_grid_edges",{"ordered_indices":["LXX113","MT100","SP70","restoration0"],
        "adjacent_units70":[(completed["LXX"]-completed["MT"])//70,
                            (completed["MT"]-completed["SP"])//70,(completed["SP"]-536)//70],
        "LXX_SP_gap":completed["LXX"]-completed["SP"],
        "source_LXX_SP_gap":nodes["LXX"]["F"]-nodes["SP"]["F"],
        "SP_inverse_contraction":original["SP"]-reversed_legs["SP"]})
    eq(completed["LXX"]-completed["SP"],7*430,"restoration outer gap")
    regular_lxx_held = 1406+inverse(3236-1406)
    fields("LXX_530_target_transport",{"completed_regular":[regular_lxx_held+stage,6],
        "completed_cumulative":[completed["LXX"],536],"coordinate_difference":536-6,
        "common_span":regular_lxx_held+stage-6,"held_regular":[regular_lxx_held,6],
        "held_cumulative":[held["LXX"],536],"held_common_span":regular_lxx_held-6})
    eq(completed["LXX"]-(regular_lxx_held+stage),536-6,"both endpoint transport530")

    packets = {"MT":[completed["MT"],nodes["MT"]["S"],nodes["MT"]["F"],536],
               "LXX":[completed["LXX"],nodes["LXX"]["S"],nodes["LXX"]["F"],1446]}
    for m,p in packets.items():
        expected={"nodes_BC":p,"edges":[a-b for a,b in zip(p,p[1:])]}
        if m=="MT": expected["whole"]=p[0]-p[-1]
        else: expected["translation_from_MT_BC"]=1446-536
        fields(m+"_7000_packet",expected)
    offsets=[b-a for a,b in zip(packets["MT"],packets["LXX"])]
    eq(offsets,[910]*4,"all four named-node translations")
    fields("LXX_MT_7000_translation",{"MT":[completed["MT"],536],"LXX":[completed["LXX"],1446],
        "both_endpoints_translation_BC":offsets[0],"common_span":completed["MT"]-536})
    R = nodes["MT"]["F"]-536
    eq(R,nodes["LXX"]["F"]-1446,"appointed targets cancel source translation")
    relative=[stage+R,600+R,R,0]
    fields("packet_generator",{"stage_D":stage,"Shem_L":600,"held_Flood_to_target_R":R,
        "relative_nodes":relative,"MT_target":536,"LXX_target":1446})
    for m,p in packets.items(): eq([n-p[-1] for n in p],relative,m+" complete packet generator")
    matrix=models["packet_interval_dependencies"]["matrix_D_L_R"]
    wanted_matrix=[[1,-1,0],[1,0,0],[1,0,1],[0,1,0],[0,1,1],[0,0,1]]
    eq(matrix,wanted_matrix,"pairwise coefficient matrix")
    spans=[sum(a*b for a,b in zip(row,[stage,600,R])) for row in matrix]
    fields("packet_interval_dependencies",{"spans":spans,"rank":matrix_rank(matrix)})
    eq(matrix_rank([matrix[i] for i in [1,3,5]]),3,"explicit nonsingular three-row minor")
    eq(spans,[a-b for i,a in enumerate(relative) for b in relative[i+1:]],"all six pairwise widths")
    primary_heads={m:completed[m]+inverse(nodes[m]["C"]-nodes[m]["F"]) for m in ["MT","LXX"]}
    upper_gap=inverse(nodes["LXX"]["C"]-nodes["LXX"]["F"])-inverse(nodes["MT"]["C"]-nodes["MT"]["F"])
    fields("packet_head_boundary",{"primary_heads":primary_heads,"lower_leg_difference":offsets[0],
        "upper_inverse_leg_difference":upper_gap,"head_difference":primary_heads["LXX"]-primary_heads["MT"],
        "uniform_910_prediction":primary_heads["MT"]+offsets[0],
        "LXX_residual":primary_heads["LXX"]-primary_heads["MT"]-offsets[0]})
    eq(primary_heads["LXX"]-primary_heads["MT"],-1090,"primary head boundary")

    # C1605–C1614: the new MT Flood refines the same three-MSS path;
    # exact Rounded transport does not overwrite the separately civil AD65.
    if "three_MSS_12600_path" in models:
        lxx_regular_sum=inverse(3236-1406)+inverse(5486-3236)
        lxx_regular_head=6+inverse(1406-6)+lxx_regular_sum
        chain=[lxx_regular_head,completed["MT"],completed["SP"],536]
        chain_edges=[a-b for a,b in zip(chain,chain[1:])]
        fields("12600_MT_Flood_cut",{"ordered_BC":[chain[0],chain[1],chain[-1]],
            "edges":[chain_edges[0],sum(chain_edges[1:])],"sum":chain[0]-chain[-1],
            "coefficients700":[chain_edges[0]//700,sum(chain_edges[1:])//700]})
        fields("three_MSS_12600_path",{"nodes_BC":chain,"edges":chain_edges,
            "units700":[n//700 for n in chain_edges]})
        fields("12600_coarsenings",{"finest":chain_edges,
            "Shem_SP_cut":[sum(chain_edges[:2]),chain_edges[-1]],
            "MT_Flood_cut":[chain_edges[0],sum(chain_edges[1:])],"whole":sum(chain_edges)})
        common_gcd=math.gcd(*chain_edges)
        fields("selected_700_grid",{"gcd_edges":common_gcd,
            "relative_indices":[(n-chain[-1])//common_gcd for n in chain],
            "ordered_edge_indices":[n//common_gcd for n in chain_edges],
            "LXX_cumulative_Flood_remainder700":(completed["LXX"]-536)%700})
        fields("12600_source_edges",{"LXX_Regular_inverse_sum":lxx_regular_sum,
            "MT_Cumulative_inverse_lower":reversed_legs["MT"],
            "SP_Cumulative_inverse_lower":reversed_legs["SP"],
            "first_edge":lxx_regular_sum-reversed_legs["MT"],
            "middle_edge":reversed_legs["MT"]-reversed_legs["SP"],
            "last_edge":reversed_legs["SP"]+compensation,
            "shared_completed_base":6+inverse(1406-6)})
        original_q=[1-n for n in chain]
        translated_q=[q+stage for q in original_q]
        labels=[{"era":"BC" if q<0 else "Rounded AD","year":1-q if q<0 else q+1} for q in translated_q]
        fields("three_MSS_translated_path",{"original_q":original_q,"translated_q":translated_q,
            "labels":labels,"edges":[b-a for a,b in zip(translated_q,translated_q[1:])],
            "whole":translated_q[-1]-translated_q[0],"shift":stage})
        rounded_ad2166_width=held["MT"]+2166-2
        civil_ad65_width=held["MT"]+65-1
        fields("4836_junction",{"node_BC":held["MT"],
            "Rounded_AD2166_width":rounded_ad2166_width,"civil_AD65_width":civil_ad65_width,
            "difference_of_declared_widths":rounded_ad2166_width-civil_ad65_width})
        sp_regular_prefix=[inverse(1406-6),inverse(3106-1406)]
        sp_regular_flood=6+sum(sp_regular_prefix)
        fields("SP_Jubilee_prefix",{"reversed_prefix":sp_regular_prefix,"sum":sum(sp_regular_prefix),
            "units_original_1400":sum(sp_regular_prefix)//(1406-6),
            "to_Conquest":sp_regular_flood-1406,"to_AD65_civil":sp_regular_flood+65-1,
            "Conquest_to_AD65":1406+65-1})
        # Twelve pre-appointed Flood nodes, three fixed targets: no search.
        comparison_source=(root/"evidence/Comparative_Study.md").read_text().splitlines()
        flood_source_line=next(line for line in comparison_source if line.startswith("| Flood / declared Arphaxad comparison seam |"))
        source_floods=[int(x.strip()) for x in flood_source_line.strip().strip("|").split("|")[1:]]
        eq(source_floods,[2456,4836,3236,5746,3106,4716],"six frozen source Floods")
        families=["MT Regular","MT Cumulative","LXX Regular","LXX Cumulative","SP Regular","SP Cumulative"]
        table=[]
        for family,source_flood in zip(families,source_floods):
            for state,shift in [("held",0),("completed",stage)]:
                flood=1406+inverse(source_flood-1406)+shift
                spans={"6 BC":flood-6,"1406 BC":flood-1406,"AD65 civil":flood+65-1}
                table.append({"family":family,"stage":state,"Flood_BC":flood,"spans":spans,
                    "remainders490":{k:v%490 for k,v in spans.items()},
                    "remainders70":{k:v%70 for k,v in spans.items()}})
        hits=[r["family"] for r in table if r["stage"]=="completed" and
              r["remainders490"]["1406 BC"]==r["remainders490"]["AD65 civil"]==0]
        fields("fixed_Flood_target_table",{"nodes":table,"completed_Jubilee_pair_hits":hits,
            "node_count":len(table),"comparison_count":sum(len(r["spans"]) for r in table)})
        target_sep=1406+65-1
        eq(target_sep%490,0,"Conquest and AD65 common Jubilee residue class")
        eq((1406-6)%70,0,"Nativity and Conquest common70 residue class")
        eq((6+65-1)%70,0,"Nativity and AD65 common70 residue class")
        for row in table:
            eq(row["remainders490"]["1406 BC"],row["remainders490"]["AD65 civil"],"dependent490 tests")
            eq(len(set(row["remainders70"].values())),1,"dependent70 tests")
        fields("Jubilee_target_dependence",{"target_separation":target_sep,"Jubilees":target_sep//490,
            "completed_remainders70":{r["family"]:r["remainders70"]["6 BC"] for r in table if r["stage"]=="completed"}})

    if "three_4900_placements" in models:
        jubilee_widths=[sp_regular_flood-held["MT"],held["MT"]-1406,1406+65-1]
        fields("Jubilee_partition_dependencies",{"widths":jubilee_widths,
            "units490":[n//490 for n in jubilee_widths],
            "source_constraints":{"SP_to_Conquest":(sp_regular_flood-1406)//490,
                "MT_Flood_to_Conquest":reversed_legs["MT"]//490,"Conquest_to_AD65":target_sep//490}})
        lxx_cut=[sp_regular_flood,held["LXX"],held["MT"],1406,65]
        lxx_cut_widths=[lxx_cut[i]-lxx_cut[i+1] for i in range(3)]+[lxx_cut[-2]+lxx_cut[-1]-1]
        eq(sum(lxx_cut_widths),sp_regular_flood+65-1,"LXX-refined civil total")
        fields("LXX_Jubilee_refinement",{"BC_nodes_then_civil_AD65":lxx_cut,
            "widths":lxx_cut_widths,"units70":[n//70 for n in lxx_cut_widths],
            "total_units70":sum(lxx_cut_widths)//70,
            "group_first_two_units70":sum(lxx_cut_widths[:2])//70})
        before=[1-nodes["MT"]["S"],1-536]
        after=[1-held["MT"],65]
        translation=nodes["MT"]["S"]-held["MT"]
        eq([b-a for a,b in zip(before,after)],[translation]*2,"whole civil600 interval transport")
        fields("Shem_600_restoration_Jubilee_transport",{"civil_coordinates_before":before,
            "civil_coordinates_after":after,"translation":translation,
            "common_width":before[1]-before[0]})
        coordinates=[[1-nodes["LXX"]["S"],1-1446],before,after]
        successive=[b[0]-a[0] for a,b in zip(coordinates,coordinates[1:])]
        eq(successive,[b[1]-a[1] for a,b in zip(coordinates,coordinates[1:])],"both endpoints translate")
        eq([b-a for a,b in coordinates],[4900]*3,"three4900 civil placements")
        fields("three_4900_placements",{"civil_coordinates":coordinates,
            "successive_forward_translations":successive,
            "direct_first_to_third":coordinates[-1][0]-coordinates[0][0],"shared_width":4900})
        separate_mt_shem=held["MT"]+inverse(600)
        whole_mt_shem=1406+inverse(original["MT"]+600)
        eq(separate_mt_shem,completed["SP"],"5436 separate-cut incidence")
        fields("5436_incidence_roles",{"coordinate_BC":separate_mt_shem,
            "different_partition_Shem_BC":whole_mt_shem})

    if "source_to_comparison_kernel" in models:
        def comparison_cells(label):
            line=next(line for line in comparison_source if line.startswith("| "+label+" |"))
            return [x.strip().replace("**","") for x in line.strip().strip("|").split("|")[1:]]
        creation_cells=comparison_cells("Lower rounded Creation member")
        fall_cells=comparison_cells("Fall coordinate adopted in this report")
        source_heads=[int(fall_cells[i] if family.startswith("SP") else creation_cells[i])
                      for i,family in enumerate(families)]
        kernel_inputs={family:[f,h] for family,f,h in zip(families,source_floods,source_heads)}
        reconstructed={}
        for family,(f,h) in kernel_inputs.items():
            legs=[f-1406,h-f]
            inverse_legs=[inverse(n) for n in legs]
            held_nodes=[1406,1406+inverse_legs[0],1406+sum(inverse_legs)]
            completed_nodes=[6,6+inverse(1406-6)]
            for n in inverse_legs:
                completed_nodes.append(completed_nodes[-1]+n)
            eq([b-a for a,b in zip(held_nodes,completed_nodes[1:])],[stage]*3,family+" stage relation")
            reconstructed[family]={"source_Flood":f,"source_head":h,"legs":legs,
                "inverse_legs":inverse_legs,"held_nodes":held_nodes,"completed_nodes":completed_nodes}
        fields("source_to_comparison_kernel",{"inputs":kernel_inputs,"spines":reconstructed,
            "targets":{"Conquest_BC":1406,"Nativity_BC":6,"Exodus_BC":1446,
                "restoration_BC":536,"NT_civil_AD":65,"Abram_birth_BC":2166},"new_operator_count":0})
        mode_rows={}
        for m in variants:
            regular=reconstructed[m+" Regular"]
            cumulative=reconstructed[m+" Cumulative"]
            lower_gap=cumulative["inverse_legs"][0]-regular["inverse_legs"][0]
            upper_gap=cumulative["inverse_legs"][1]-regular["inverse_legs"][1]
            head_difference=cumulative["completed_nodes"][-1]-regular["completed_nodes"][-1]
            eq(lower_gap+upper_gap,head_difference,m+" additive head decomposition")
            mode_rows[m]={"lower_Flood_difference":lower_gap,"upper_difference":upper_gap,
                          "head_difference":head_difference}
        fields("lower_upper_mode_synthesis",{"rows":mode_rows})

    # The following objects select and explain verified findings. Their
    # statements are checked against the numerical witnesses above, but prose
    # classifications are not presented as separately proved arithmetic.
    if "dependency_ledger" in models:
        ledger=models["dependency_ledger"]
        eq(len(ledger),8,"eight dependency families")
        eq(len({r["family"] for r in ledger}),len(ledger),"unique dependency families")
        for row in ledger:
            eq(set(row),{"family","source_condition","consequence"},"dependency ledger structure")
            if not all(isinstance(v,str) and v.strip() for v in row.values()):
                raise AssertionError("Empty dependency explanation")
        qualitative.add("dependency_ledger")
        add_group("Dependency ledger classification and completeness",["dependency_ledger"])
    if "scope_ledger" in models:
        ledger=models["scope_ledger"]
        eq([r["result"] for r in ledger["tests"]],["fails"]*4+["different measure"],"scope verdicts")
        eq(cut_gap!=merged_gap,True,"partition boundary witness")
        eq(primary_heads["LXX"]-primary_heads["MT"]!=910,True,"head boundary witness")
        eq((completed["LXX"]-536)%700!=0,True,"700-grid boundary witness")
        eq(hits,["SP Regular"],"completed Jubilee scope witness")
        eq((4836+2166-1)-(4836+2166-2),1,"separate width conventions")
        eq(ledger["deferred"],["deeper mirrors","second decimal inversion"],"retained deferrals")
        qualitative.add("scope_ledger")
        add_group("Scope ledger classifications tied to computed witnesses",["scope_ledger"])
    if "reader_selection" in models:
        selection=models["reader_selection"]
        eq(len(selection["main_additions"]),6,"six main synthesis selections")
        eq(len(selection["support_only"]),3,"three supporting selections")
        if not selection.get("reason") or not all(selection["main_additions"]+selection["support_only"]):
            raise AssertionError("Incomplete reader selection")
        qualitative.add("reader_selection")
        add_group("Reader selection qualitative coverage",["reader_selection"])

    if "Covenant_common_refinement" in models:
        covenant=2081-215
        covenant_nodes=[lxx_regular_head,completed["MT"],completed["SP"],covenant,536]
        covenant_edges=[a-b for a,b in zip(covenant_nodes,covenant_nodes[1:])]
        fields("Covenant_common_refinement",{"nodes_BC":covenant_nodes,
            "edges":covenant_edges,"units70":[n//70 for n in covenant_edges],
            "whole":sum(covenant_edges),"Covenant_radius":lxx_regular_head-covenant,
            "7000_suffix":sum(covenant_edges[1:]),"4900_suffix":sum(covenant_edges[2:])})
        eq(lxx_regular_head-covenant,23*490,"Covenant radius in common refinement")

    if "rounded_source_parameter_bridge" in models:
        # Recover this fitted dictionary from the already source-verified row
        # differences. It compresses the chosen data; it is not an independent
        # historical explanation of how the numbers originated.
        lxx_by_name=dict(zip(names,rounded_diff))
        sp_by_name=dict(zip(names,sp_diff))
        u=lxx_by_name["Peleg"]
        t=-sp_by_name["Terah"]
        a=lxx_by_name["Arphaxad"]
        inserted_life=rounded["LXX"][names.index("2nd Cainan")]
        fitted_lxx={"Arphaxad":a,"2nd Cainan":inserted_life,"Shelah":a,
            "Eber":u-t,"Peleg":u,"Reu":u,"Serug":u,"Nahor":t,"Terah":0}
        fitted_sp={n:(-t if n in ["Eber","Terah"] else 0) for n in names}
        eq(lxx_by_name,fitted_lxx,"fitted rounded LXX row dictionary")
        eq(sp_by_name,fitted_sp,"fitted SP row dictionary")
        other=4*u+2*a
        eq(other,sum(v for n,v in lxx_by_name.items() if n!="2nd Cainan"),"Eber/Nahor t cancellation")
        source_lower_from_parameters={"MT":original["MT"],
            "LXX":original["MT"]+inserted_life+other,
            "SP":original["MT"]-2*t}
        eq(source_lower_from_parameters,original,"source lower legs from fitted row dictionary")
        fields("rounded_source_parameter_bridge",{"century_u":u,
            "paired_rounded_increment_a":a,"Terah_Eber_parameter_t":t,
            "Cainan_life":inserted_life,"LXX_other_difference":other,
            "LXX_native_difference":inserted_life+other,"SP_difference":-2*t,
            "lower_legs":source_lower_from_parameters},
            "Source-conditioned fitted row-parameter dictionary")

    # Journal results must equal the actual persisted model, not just pass the
    # arithmetic independently. These first20 actions each create one model.
    step_models = ["scope","postflood_rounded_rows","LXX_910_source","SP_120_source",
        "source_translation_extent","lower_spine_map","lower_decimal_mechanism",
        "SP_MT_Shem_condition","translation_cut_domain","inverse_prefix_extent",
        "three_cumulative_restoration","restoration_compensation","restoration_grid_edges",
        "LXX_MT_7000_translation","LXX_530_target_transport","MT_7000_packet","LXX_7000_packet",
        "packet_generator","packet_interval_dependencies","packet_head_boundary",
        "12600_MT_Flood_cut","three_MSS_12600_path","12600_coarsenings","selected_700_grid",
        "12600_source_edges","three_MSS_translated_path","4836_junction","SP_Jubilee_prefix",
        "fixed_Flood_target_table","Jubilee_target_dependence",
        "Jubilee_partition_dependencies","LXX_Jubilee_refinement","Shem_600_restoration_Jubilee_transport",
        "three_4900_placements","5436_incidence_roles","source_to_comparison_kernel",
        "lower_upper_mode_synthesis","dependency_ledger","scope_ledger","reader_selection",
        "Covenant_common_refinement"]
    for record,name in zip(journal,step_models):
        eq(record["results"],models[name],"journal model binding C"+str(record["step"]))
    if "rounded_source_parameter_bridge" in models:
        record=next(r for r in journal if r["step"]==1633)
        eq(record["results"],models["rounded_source_parameter_bridge"],"journal model binding C1633")
    binding_label="C1585–C"+str(1584+min(len(journal),len(step_models)))
    if "rounded_source_parameter_bridge" in models:
        binding_label+=" and C1633"
    add_group(binding_label+" journal result-to-model bindings")

    uncovered=sorted(set(models)-covered)
    result={"status":"PASS" if not uncovered else "PARTIAL",
            "journal_steps_verified":len(journal),"first_step":journal[0]["step"],
            "last_step":journal[-1]["step"],"last_record_sha256":journal[-1]["sha256"],
            "group_count":len(groups),"groups":groups,"covered_models":sorted(covered),
            "qualitative_models":sorted(qualitative),
            "uncovered_models":uncovered,
            "count_note":"Groups are computational or integrity checks, not independent historical witnesses."}
    if require_50 and uncovered:
        raise AssertionError("Uncovered model files: "+", ".join(uncovered))
    return result


if __name__=="__main__":
    parser=argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--root",type=Path,default=Path(__file__).resolve().parents[1])
    parser.add_argument("--require-50",action="store_true")
    parser.add_argument("--output",type=Path)
    args=parser.parse_args()
    result=verify(args.root,args.require_50)
    if args.output:
        args.output.write_text(json.dumps(result,indent=2,ensure_ascii=False)+"\n")
    print(json.dumps({k:v for k,v in result.items() if k!="groups"},indent=2))
