"""New preparation only: exact fibres of fixed source rounding observations.

No root journal writes, date search, target tally, or decimal reversal.
"""
from pathlib import Path
from fractions import Fraction
from itertools import product
import hashlib
import json
import re

BASE = Path(__file__).resolve().parents[1]
ROOT = BASE.parent
OUT = BASE / 'prep'

def sha(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()

def read(path):
    return json.loads(path.read_text())

def write(name, obj):
    path = OUT / name
    path.write_text(json.dumps(obj, indent=2, ensure_ascii=False) + '\n')
    return path

def Q(x):
    return 5 * ((x + 2) // 5)

def rho(x):
    return x - Q(x)

def lift(rounded, residual, shift):
    carry = Q(residual + shift)
    return rounded + carry, residual + shift - carry

def rank(rows):
    a = [[Fraction(x) for x in row] for row in rows]
    r = 0
    for c in range(len(a[0])):
        pivot = next((i for i in range(r, len(a)) if a[i][c]), None)
        if pivot is None:
            continue
        a[r], a[pivot] = a[pivot], a[r]
        factor = a[r][c]
        a[r] = [x / factor for x in a[r]]
        for i in range(len(a)):
            if i != r:
                factor = a[i][c]
                a[i] = [x - factor*y for x, y in zip(a[i], a[r])]
        r += 1
        if r == len(a):
            break
    return r

question_path = OUT / 'round_joint_fibres.questions.json'
assert question_path.exists(), 'Question manifest must exist before computation.'
proposal = read(OUT / 'operator_extension_proposals.json')
row_path = BASE / 'model/complete_source_row_packet.json'
rows = read(row_path)
assert rows == proposal['literal_source_row_packet']
cap_path = BASE / 'inherited/model/sp_cap_complete_ledger.json'
cap = read(cap_path)
file18 = Path(proposal['source_bindings']['File18']['path'])
lines18 = file18.read_text().splitlines()
source_bindings = {}
for name in ('File18', 'File51a', 'File52c_latest', 'Strategy'):
    entry = proposal['source_bindings'][name]
    assert sha(Path(entry['path'])) == entry['sha256']
    source_bindings[name] = entry
source_bindings['complete_source_row_packet'] = {'path': str(row_path), 'sha256': sha(row_path)}
source_bindings['inherited_cap_ledger'] = {'path': str(cap_path), 'sha256': sha(cap_path)}
source_bindings['question_manifest'] = {'path': str(question_path), 'sha256': sha(question_path)}
source_bindings['operator_proposals'] = {'path': str(OUT/'operator_extension_proposals.json'), 'sha256': sha(OUT/'operator_extension_proposals.json')}

ranges = {'MT': [(1223, 1253)], 'SP': [(1444, 1456), (1598, 1608)], 'LXX': [(1694, 1725)]}
def locator(family, name, b, L):
    found = []
    for start, end in ranges[family]:
        for line_number in range(start, end + 1):
            text = lines18[line_number - 1]
            cells = [v.strip() for v in text.split('|')[1:-1]]
            if len(cells) != 5:
                continue
            source_name = cells[0]
            normalized = 'Cainan2' if source_name == '2nd Cainan' else source_name.split(' / ')[0].split(' — ')[0]
            if normalized != name:
                continue
            get_integer = lambda text: int(re.search(r'\d+', text).group())
            if get_integer(cells[3]) == L and get_integer(cells[4]) == b:
                found.append({'line': line_number, 'literal': text})
    assert len(found) == 1, (family, name, found)
    return {'source': 'File18', **found[0]}

observations, truth, excluded = [], {}, []
for family, family_rows in rows.items():
    for row in family_rows:
        key = family + ':' + row['name']
        loc = locator(family, row['name'], row['b_ledger'], row['L_ledger'])
        if family == 'SP' and row['L_counting'] is not None:
            excluded.append({'key': key, 'row': row, 'source_locator': loc, 'reason': 'Inclusive ledger count; not an ordinary biography remainder.'})
            continue
        b, L, r = row['b_ledger'], row['L_ledger'], row['r_calculated']
        assert r == L-b and b >= 0 and r >= 0
        observations.append({'key': key, 'family': family, 'name': row['name'], 'L_exact': L, 'B': Q(b), 'R': Q(r), 'source_locator': {'source':loc['source'],'line':loc['line']}})
        truth[key] = {'b': b, 'r': r, 'r_status': row['r_status'], 'source_locator': loc}
assert len(observations) == 55 and len(excluded) == 3
inputs = {
    'status': 'Frozen preparation inputs; separately specified observations are not claimed independently attested.',
    'source_bindings': source_bindings,
    'observation_domain': 'Nonnegative integer ordinary biographies L=b+r; unique nearest-five Q(x)=5 floor((x+2)/5).',
    'observations_only': observations,
    'source_truth_for_later_validation_only': truth,
    'excluded_SP_inclusive_rows': excluded,
    'source_status': 'All packet remainders are calculated from selected L and b. Keeping Q(r) in an inverse problem adds an observation mathematically, not independent historical evidence.',
    'rounding_policy_locator': {'source': 'File51a', 'lines': [177, 184, 441, 448], 'sections': ['Introduction', '3.4']},
    'LXX_Lamech_control': {'source': 'File18', 'lines': [1743, 1757], 'section': '4A.1', 'status': 'Restored182, attested753, calculated571; no identified joint182/753 manuscript witness.'},
    'source_qualification': 'File51a3.4 singular Methuselah wording is already qualified by inherited C750/C753: Reu also differs, and main LXX Lamech adds another class. No canonical edit is made.',
    'cap_packet': cap,
    'prior_evidence': ['C748 ordered-field inversion', 'C752–C753 forward defect law and complete source support', 'C760 multiples-of-five equivariance', 'C770 residual recovery', 'C968 exact cap fibres', 'C974 strict-support response', 'C981 cap/round comparison support', 'C1177 complete labelled row freeze'],
    'guards': ['No second decimal inversion', 'No endpoint search or target tally', 'No new chronology variants', 'No root journal edits'],
}
input_path = write('round_joint_fibres.inputs.json', inputs)

# The inverse function receives observations only. Source truth is compared afterward.
def fibre(observation):
    L, B, R = [observation[k] for k in ('L_exact', 'B', 'R')]
    low = max(0, B-2, L-R-2)
    high = min(L, B+2, L-R+2)
    pairs = [[b, L-b] for b in range(low, high+1)]
    return {'s': L-B-R, 'b_interval': [low, high], 'candidates': pairs, 'candidate_count': len(pairs), 'singleton': len(pairs)==1}

fibre_rows = []
for obs in observations:
    result = fibre(obs)
    L, B, R = [obs[k] for k in ('L_exact', 'B', 'R')]
    all_rounded = [[B+u, R+v] for u, v in product(range(-2,3), repeat=2)
                   if B+u >= 0 and R+v >= 0 and Q(B+R+u+v)==Q(L)]
    hidden = truth[obs['key']]
    source_pair = [hidden['b'], hidden['r']]
    assert source_pair in result['candidates']
    assert result['candidate_count'] == 5-abs(result['s'])
    assert all(Q(b)==B and Q(r)==R and b+r==L for b,r in result['candidates'])
    fibre_rows.append({**obs, **result, 'hidden_source_pair_for_comparison': source_pair,
                       'source_pair_retained': True, 'rounded_total': Q(L),
                       'parts_minus_total_defect': B+R-Q(L),
                       'three_rounded_candidates_without_exact_L': all_rounded,
                       'Q_L_adds_information_after_exact_L': False})

lift_rows = []
lookup = {family: {r['name']: r for r in family_rows} for family, family_rows in rows.items()}
for target in ('LXX', 'SP'):
    for name, before in lookup['MT'].items():
        after = lookup[target][name]
        if target == 'SP' and after['L_counting'] is not None:
            continue
        streams = {}
        for label, field in [('b','b_ledger'),('r','r_calculated'),('L','L_ledger')]:
            x, z = before[field], after[field]
            d = z-x
            predicted = lift(Q(x), rho(x), d)
            assert predicted == (Q(z),rho(z))
            streams[label] = {'source': x, 'target': z, 'shift': d, 'source_rounded': Q(x),
                              'source_residual':rho(x),'rounded_shift':predicted[0]-Q(x),
                              'target_rounded':predicted[0],'target_residual':predicted[1]}
        db = after['b_ledger']-before['b_ledger']
        dL = after['L_ledger']-before['L_ledger']
        start = (Q(before['r_calculated']),rho(before['r_calculated']))
        minus_b_then_L = lift(*lift(*start,-db),dL)
        L_then_minus_b = lift(*lift(*start,dL),-db)
        assert minus_b_then_L == L_then_minus_b == lift(*start,dL-db)
        lift_rows.append({'source_family':'MT','target_family':target,'name':name,'streams':streams,
                          'source_endpoint_status':'Existing named source comparison; the two scalar contributions to delta_r=delta_L−delta_b are algebraic, not extra intermediate chronology routes.',
                          'two_order_remainder_lifts_agree':True})
assert len(lift_rows)==35

cap_inputs = cap['forward_inputs']
capacities = cap['generated']['capacities']
cap_rows = []
for name, L, c, source_y in zip(cap_inputs['names'],cap_inputs['baseline_lives'],capacities,cap['generated']['lives']):
    y=min(L,c);t=Q(y);z=L-y;ry=rho(y);rL=rho(L)
    assert y==source_y and L==t+ry+z
    assert Q(L)-Q(y)==z+ry-rL
    assert Q(min(L,c))==min(Q(L),Q(c))
    if t<Q(c):
        f={'type':'bounded integer cell','lower':max(0,t-2),'upper':t+2}
    elif t==Q(c):
        f={'type':'saturation ray','lower':max(0,t-2),'upper':None}
    else:
        raise AssertionError('Impossible source rounded cap output')
    cap_rows.append({'name':name,'baseline_L':L,'capacity':c,'capped_y':y,'rounded_capped_T':t,
                     'strict_source_branch':'active' if L>c else 'inactive' if L<c else 'tie',
                     'cap_loss_z':z,'capped_residual_rho_y':ry,'baseline_residual_rho_L':rL,
                     'rounded_loss':Q(L)-t,'rounded_minus_exact_loss':Q(L)-t-z,
                     'co_rounded_min':min(Q(L),Q(c)),'mixed_exact_capacity_min':min(Q(L),c),
                     'co_rounded_minus_mixed':t-min(Q(L),c),'composite_global_fibre':f,
                     'missing_on_known_branch':'positive cap excess z only; rho_y fixed by c' if L>c else 'finite rho_y only; z=0' if L<c else 'none once tie L=c is supplied'})

def suffix(v):
    return [sum(v[i:]) for i in range(len(v))]

loss=[x['cap_loss_z'] for x in cap_rows]
rounded_loss=[x['rounded_loss'] for x in cap_rows]
delta=[x['rounded_minus_exact_loss'] for x in cap_rows]
U=[[int(j>=i) for j in range(9)] for i in range(9)]
active=[int(x['strict_source_branch']=='active') for x in cap_rows]
birth_cap_matrix=[row+[0]*9 for row in U]
birth_cap_matrix += [[active[i]*v for v in U[i]]+[int(j==i)*(1-active[i]) for j in range(9)] for i in range(9)]
assert rank(birth_cap_matrix)==15

answers = [
 ('RJ01','The ordinary domain contains MT19, LXX20 and SP16. The excluded three SP rows retain their inclusive tags and Lamech53/52 distinction.'),
 ('RJ02','L,B,R are separate mathematical observations in the inverse call. All are generated from explored source inputs here; calculated r and Q(r) do not become independent textual premises.'),
 ('RJ03','Fibre: b ranges through [max(0,B−2,L−R−2), min(L,B+2,L−R+2)]∩Z, r=L−b. Interior residuals u,v∈[−2,2] satisfy u+v=s=L−B−R; size5−|s| for |s|≤4.'),
 ('RJ04','All55 hidden source pairs belong to the complete reconstructed fibres. Every candidate is an algebraic alternative only. Singleton rows are listed explicitly with all unresolved rows alongside them.'),
 ('RJ05','Q(L) is a function of exact L and adds nothing after L is fixed. Without exact L, the three rounded values constrain residual sum by rounding, giving the inherited19-or3 class fibres; exact L selects one diagonal.'),
 ('RJ06','A nonsingleton row needs a selector. One independently supplied exact b or residual u suffices. More generally M=αb+βr identifies b=(M−βL)/(α−β) iff α≠β; α=β repeats total L. No claim of global minimal bit encoding is made.'),
 ('RJ07','MT/LXX Methuselah and MT/LXX/SP Reu are singleton conditional fibres. Main LXX Lamech remains(181,572) or(182,571); native Cainan has five pairs. Existing source182 or130 resolves those roles, not the rounded packet alone.'),
 ('RJ08','Q(x+d) descends to a function of Q(x) for every integer x iff d is divisible by5. For other residue shifts, the five-element cell straddles two translated rounding cells; the hidden residual changes the answer.'),
 ('RJ09','Lift(y,rho)=(y+Q(rho+d),rho+d−Q(rho+d)). Transport through x=y+rho proves exact composition and the carry cocycle; keeping the residual is a lossless encoding, not compression.'),
 ('RJ10','All35 named ordinary MT→LXX/SP comparisons, across all three streams, satisfy the same lift. The two scalar contribution orders in delta_r=delta_L−delta_b agree algebraically; no extra intermediate chronology route is claimed.'),
 ('RJ11','Lamech original remainder595 has residual0; lifespan777 has residual2. The same−24 shift gives rounded changes−25 and−20 respectively. This is a controlled residual effect, not two different source displacements.'),
 ('RJ12','For T a multiple5, composite fibre is [max(0,T−2),T+2] if T<Q(c), [max(0,T−2),∞) if T=Q(c), and empty if T>Q(c). Saturation observation alone may include inputs below c.'),
 ('RJ13','Every monotone scalar Q preserves min: Q(min(L,c))=min(Q(L),Q(c)). Keeping c exact is a different mixed representation; the source Jared and Lamech rows then differ by−2 and+2.'),
 ('RJ14','The complete cap-plus-round source field has six bounded rounding cells and three saturation rays. Exact births retain capacities but do not recover any hidden residual or ancestral cap excess by themselves.'),
 ('RJ15','With y=min(L,c), z=L−y, rho_y=y−Q(y): L=Q(y)+rho_y+z and Q(L)−Q(y)=z+rho_y−rho_L. The complete exact reduction488 becomes rounded reduction485; cumulative responses are inherited consequences.'),
 ('RJ16','Complementarity y≤c,z≥0,z(c−y)=0 removes a false double count. On known active support y=c and rho_y=c−Q(c), so only z is missing; inactive support has z=0 and only rho_y missing. A declared tie determines L=c.'),
 ('RJ17','Exact births plus exact capped lives have local rank15 on18 variables and three missing ancestral excesses. Replacing capped lives by rounded ones additionally loses finite residual choices on the six inactive rows. A separately supplied total excess adds one linear constraint; the inherited calculated488 is not an independent observation.'),
 ('RJ18','New contribution: constructive observation fibres and branchwise composite information loss. Existing defect tables, source values, loss totals and full-residual inversion are reused. No historical generation follows from recovering values from derived observations.'),
]
result = {
 'status':'PASS — new bounded preparation, no numbered root actions',
 'question_manifest_sha256':sha(question_path),'input_packet_sha256':sha(input_path),
 'sequential_results':[{'id':i,'result':v,'reassessment':read(question_path)['questions'][n+1]['question'] if n+1<len(answers) else 'Adopt only the source-costed claims useful to the Strategy.'} for n,(i,v) in enumerate(answers)],
 'joint_fibres_all55':fibre_rows,
 'singleton_rows':[x['key'] for x in fibre_rows if x['singleton']],
 'all35_source_shift_lifts':lift_rows,
 'all_nine_cap_round_rows':cap_rows,
 'cap_cumulative_responses':{'exact_losses':loss,'rounded_losses':rounded_loss,'difference':delta,'exact_suffixes':suffix(loss),'rounded_suffixes':suffix(rounded_loss),'difference_suffixes':suffix(delta)},
 'exact_birth_cap_observation_map':{'U':U,'active_mask':active,'Jacobian':birth_cap_matrix,'rank':rank(birth_cap_matrix),'kernel_dimension':3,'domain':'Fixed Noah and inclusive k, known strict active support; differential statement accompanies exact rowwise global fibres.'},
 'minimum_measurement_qualification':{'no_observation_claim':'The six finite residual selectors and three unbounded excess values are not recovered merely by assigning names to them.','cap_total_status':'488 is calculated from the existing baseline and cannot independently regenerate that same baseline.','complementarity':'z(c−y)=0; y≤c,z≥0. Branch support itself must be retained or inferred from additional data.'},
 'checks':{'55_complete_fibres':len(fibre_rows)==55,'every_source_truth_retained':all(x['source_pair_retained'] for x in fibre_rows),'all35_source_shift_lifts':len(lift_rows)==35,'all9_cap_rows':len(cap_rows)==9,'exact_loss_total488':sum(loss)==488,'rounded_loss_total485':sum(rounded_loss)==485,'rank15':rank(birth_cap_matrix)==15},
 'scope':'Fibre sizes and named singleton lists describe exact observation information, not a probability or target-frequency claim.'
}
write('round_joint_fibres.results.json',result)
print(json.dumps({'status':result['status'],'checks':result['checks'],'singletons':result['singleton_rows'],'cap_suffixes':result['cap_cumulative_responses']},indent=2))
