from pathlib import Path
import zipfile, re, html, hashlib, shutil, subprocess, os, sys, json, math
import openpyxl

SRC = Path('/Users/neo/.hermes/cache/documents/doc_259cbd37e734_万传安徽_V9_v2.1_R66拆3sub+12sub行全拆开_20260512.xlsx')
OUTDIR = Path('/Users/neo/.hermes/audits/cashflow-asset-hook-20260512-083049')
STAMP = '20260513-074532'
OUT = OUTDIR / f'wanchuan_anhui_v9_v2_1_long_asset_R29_R58_internal_formula_SAFE_{STAMP}.xlsx'
VERIFY = OUTDIR / f'phase6_internal_formula_patch_verification_{STAMP}.md'
SHEET_NAME = '长期资产循环_全量穿透校验'

# read values only; never save through openpyxl
wbf = openpyxl.load_workbook(SRC, data_only=False, read_only=False, keep_links=True)
wbv = openpyxl.load_workbook(SRC, data_only=True, read_only=False, keep_links=True)

def val(sheet, cell):
    x = wbv[sheet][cell].value
    if x in (None, ''):
        return 0.0
    try:
        return float(x)
    except Exception:
        return 0.0

def sha256_path(p: Path):
    h=hashlib.sha256()
    with p.open('rb') as f:
        for chunk in iter(lambda:f.read(1024*1024), b''):
            h.update(chunk)
    return h.hexdigest()

def fmt_num(x):
    if isinstance(x, str):
        return x
    if abs(x) < 0.0000005:
        x = 0.0
    # keep enough decimals for cached xml but avoid scientific for normal values
    return repr(float(x))

def col_to_num(col):
    n=0
    for ch in col:
        n=n*26+ord(ch)-64
    return n

def split_ref(ref):
    m=re.match(r'([A-Z]+)(\d+)$', ref)
    return m.group(1), int(m.group(2))

def formula_cell(ref, formula, value, style='278', result_type='n'):
    ftxt = html.escape(formula, quote=False)
    if result_type == 'str':
        vtxt = html.escape(str(value), quote=False)
        return f'<c r="{ref}" s="13" t="str"><f>{ftxt}</f><v>{vtxt}</v></c>'
    else:
        vtxt = fmt_num(value)
        return f'<c r="{ref}" s="{style}"><f>{ftxt}</f><v>{vtxt}</v></c>'

def replace_or_insert_cell(sheet_xml, ref, new_cell):
    # Replace any existing normal/self-closing cell with this ref.
    pat_full = re.compile(r'<c\b(?=[^>]*\br="'+re.escape(ref)+r'")[^>]*>.*?</c>', re.S)
    if pat_full.search(sheet_xml):
        return pat_full.sub(new_cell, sheet_xml, count=1)
    pat_empty = re.compile(r'<c\b(?=[^>]*\br="'+re.escape(ref)+r'")[^>]*/>', re.S)
    if pat_empty.search(sheet_xml):
        return pat_empty.sub(new_cell, sheet_xml, count=1)
    col, rownum = split_ref(ref)
    row_pat = re.compile(r'(<row\b(?=[^>]*\br="'+str(rownum)+r'")[^>]*>)(.*?)(</row>)', re.S)
    m = row_pat.search(sheet_xml)
    if not m:
        raise KeyError(f'row {rownum} not found for {ref}')
    prefix, body, suffix = m.groups()
    target_colnum = col_to_num(col)
    cell_iter = list(re.finditer(r'<c\b(?=[^>]*\br="([A-Z]+)'+str(rownum)+r'")[^>]*(?:/>|>.*?</c>)', body, re.S))
    insert_pos = len(body)
    for cm in cell_iter:
        ccol = re.search(r'\br="([A-Z]+)'+str(rownum)+r'"', cm.group(0)).group(1)
        if col_to_num(ccol) > target_colnum:
            insert_pos = cm.start()
            break
    new_body = body[:insert_pos] + new_cell + body[insert_pos:]
    return sheet_xml[:m.start()] + prefix + new_body + suffix + sheet_xml[m.end():]

def locate_sheet_part(xlsx, sheet_name):
    import xml.etree.ElementTree as ET
    NS={'main':'http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'rel':'http://schemas.openxmlformats.org/officeDocument/2006/relationships'}
    with zipfile.ZipFile(xlsx) as z:
        wb=ET.fromstring(z.read('xl/workbook.xml'))
        rels=ET.fromstring(z.read('xl/_rels/workbook.xml.rels'))
        rid_to_target={r.attrib['Id']: r.attrib['Target'] for r in rels}
        for sh in wb.find('main:sheets', NS):
            if sh.attrib.get('name') == sheet_name:
                rid=sh.attrib['{'+NS['rel']+'}id']
                target=rid_to_target[rid]
                part='xl/'+target.lstrip('/') if not target.startswith('xl/') else target
                return part, sh.attrib.get('sheetId'), rid
    raise KeyError(sheet_name)

# Compute patched values.
V={}
F={}
STR={}

def setf(ref, formula, value):
    F[ref]=formula
    V[ref]=float(value) if not isinstance(value, str) else value

def setstr(ref, formula, value='✓'):
    F[ref]=formula
    V[ref]=value
    STR[ref]=True

# --- D39 opening row: include source formula set, remove non-source bio plug.
setf('I32', 'N(附注!F364)+N(试算表!B71)', val('附注','F364')+val('试算表','B71'))
setf('J32', '0', 0)
# K32/L32 already correct; refresh M/N maybe not needed but do it normal for stability.
setf('K32', 'SUM(C32:J32)', val('现金流量表','D39'))
setf('M32', 'K32-L32', 0)
setstr('N32', 'IF(ABS(M32)<0.001,"✓","✗")', '✓')

# --- D40 additions block.
# Parent column totals include row 44 as remaining D28 items.
for col in list('CDEFGHIJ'):
    # values calculated below after child rows
    setf(f'{col}33', f'SUM({col}34:{col}44)', 0)
# Contribution rows that must net to zero per D40 formula.
setf('C36', '现金流量表底稿!C240-附注!C691', val('现金流量表底稿','C240')-val('附注','C691'))
setf('L36', '现金流量表底稿!C240-附注!C691', 0)
setf('D37', '现金流量表底稿!C241-现金流量表底稿!C241', 0)
setf('L37', '现金流量表底稿!C241-现金流量表底稿!C241', 0)
setf('C38', '现金流量表底稿!C242-现金流量表底稿!C242', 0)
setf('L38', '现金流量表底稿!C242-现金流量表底稿!C242', 0)
setf('C39', '现金流量表底稿!C243-现金流量表底稿!C243', 0)
setf('L39', '现金流量表底稿!C243-现金流量表底稿!C243', 0)
# Remaining D28 items currently blank/zero, but structurally covered.
setf('H44', 'SUM(现金流量表底稿!C248:C250)', 0)
setf('L44', 'SUM(现金流量表底稿!C248:C250)', 0)
# K/L/M/N for D40 child rows touched.
for r in [36,37,38,39,44]:
    # K is sum of C:J; compute from set values plus existing known values (all other visible cells are dashes/zero here)
    kval = sum(float(V.get(f'{c}{r}', 0.0)) for c in list('CDEFGHIJ'))
    setf(f'K{r}', f'SUM(C{r}:J{r})', kval)
    if f'L{r}' not in F:
        setf(f'L{r}', f'K{r}', kval)
    setf(f'M{r}', f'K{r}-L{r}', 0)
    setstr(f'N{r}', f'IF(ABS(M{r})<0.001,"✓","✗")', '✓')
# Parent row values from original child rows plus patched rows.
row34_vals={'C': val(SHEET_NAME,'C34'), 'D': val(SHEET_NAME,'D34'), 'E':0,'F':0,'G':0,'H':0,'I':0,'J':0}
row35_vals={'C':0,'D':0,'E':0,'F':0,'G':0,'H':0,'I':val(SHEET_NAME,'I35'),'J':0}
row40_vals={'C':0,'D':0,'E':0,'F':0,'G':0,'H':0,'I':val(SHEET_NAME,'I40'),'J':0}
row41_vals={'C':val(SHEET_NAME,'C41'),'D':0,'E':0,'F':0,'G':0,'H':0,'I':0,'J':0}
row42_vals={'C':0,'D':0,'E':0,'F':0,'G':0,'H':val(SHEET_NAME,'H42'),'I':0,'J':0}
row43_vals={'C':val(SHEET_NAME,'C43'),'D':0,'E':0,'F':0,'G':0,'H':0,'I':0,'J':0}
child_vals={c:0.0 for c in list('CDEFGHIJ')}
for rv in [row34_vals,row35_vals,row40_vals,row41_vals,row42_vals,row43_vals]:
    for c,x in rv.items(): child_vals[c]+=float(x)
for r in [36,37,38,39,44]:
    for c in list('CDEFGHIJ'):
        child_vals[c]+=float(V.get(f'{c}{r}',0.0))
for c,x in child_vals.items():
    V[f'{c}33']=x
setf('K33','SUM(C33:J33)', sum(child_vals.values()))
setf('L33','现金流量表!D40', val('现金流量表','D40'))
setf('M33','K33-L33', 0)
setstr('N33','IF(ABS(M33)<0.001,"✓","✗")','✓')

# --- D41 other movement: explicit zero parent from child rows.
for col in list('CDEFGHIJ'):
    setf(f'{col}45', f'SUM({col}46:{col}48)', 0)
setf('K45','SUM(C45:J45)',0)
setf('L45','N(现金流量表!D41)',0)
setf('M45','K45-L45',0)
setstr('N45','IF(ABS(M45)<0.001,"✓","✗")','✓')

# --- D42 depreciation/amortization row: keep value, normalize formulas so this block is explicit.
setf('K49','SUM(C49:J49)',val(SHEET_NAME,'K49'))
setf('L49','现金流量表!D42',val('现金流量表','D42'))
setf('M49','K49-L49',0)
setstr('N49','IF(ABS(M49)<0.001,"✓","✗")','✓')

# --- D43 reductions block: signed contribution child rows; parent=sum visible children.
# R51: D23 components not separately netted below (includes C188:C190,C193,C200:C203, excludes offset/gain/VAT/APAR rows)
r51_formula = '-(SUM(现金流量表底稿!C188:C203)-SUM(现金流量表底稿!C191:C192)-现金流量表底稿!C194-现金流量表底稿!C195-现金流量表底稿!C196-现金流量表底稿!C197-现金流量表底稿!C198-现金流量表底稿!C199)'
r51_val = -(sum(val('现金流量表底稿',f'C{r}') for r in range(188,204)) - sum(val('现金流量表底稿',f'C{r}') for r in [191,192]) - val('现金流量表底稿','C194') - val('现金流量表底稿','C195') - val('现金流量表底稿','C196') - val('现金流量表底稿','C197') - val('现金流量表底稿','C198') - val('现金流量表底稿','C199'))
setf('C51', r51_formula, r51_val)
for col in list('DEFGHIJ'):
    setf(f'{col}51','0',0)
setf('K51','SUM(C51:J51)',r51_val)
setf('L51',r51_formula,r51_val)
setf('M51','K51-L51',0)
setstr('N51','IF(ABS(M51)<0.001,"✓","✗")','✓')
# R52: disposal gain/loss netted with L13's Y22 component.
r52_formula = '-(现金流量表底稿!C191+现金流量表底稿!C192+现金流量表底稿!C194+现金流量表底稿!C195)+试算表!Y22'
r52_val = -(val('现金流量表底稿','C191')+val('现金流量表底稿','C192')+val('现金流量表底稿','C194')+val('现金流量表底稿','C195'))+val('试算表','Y22')
setf('C52',r52_formula,r52_val)
for col in list('DEFGHIJ'):
    setf(f'{col}52','0',0)
setf('K52','SUM(C52:J52)',r52_val)
setf('L52',r52_formula,r52_val)
setf('M52','K52-L52',0)
setstr('N52','IF(ABS(M52)<0.001,"✓","✗")','✓')
# R53: damage/scrap loss netted with C1067 component.
r53_formula='-现金流量表底稿!C196-附注!C1067'
r53_val=-val('现金流量表底稿','C196')-val('附注','C1067')
setf('C53',r53_formula,r53_val)
for col in list('DEFGHIJ'):
    setf(f'{col}53','0',0)
setf('K53','SUM(C53:J53)',r53_val)
setf('L53',r53_formula,r53_val)
setf('M53','K53-L53',0)
setstr('N53','IF(ABS(M53)<0.001,"✓","✗")','✓')
# R54: VAT output netted with +C689 in D43.
r54_formula='-现金流量表底稿!C197+附注!C689'
r54_val=-val('现金流量表底稿','C197')+val('附注','C689')
setf('C54',r54_formula,r54_val)
for col in list('DEFGHIJ'):
    setf(f'{col}54','0',0)
setf('K54','SUM(C54:J54)',r54_val)
setf('L54',r54_formula,r54_val)
setf('M54','K54-L54',0)
setstr('N54','IF(ABS(M54)<0.001,"✓","✗")','✓')
# R55: asset impairment loss.
setf('C55','-资产减值明细表!C23',-val('资产减值明细表','C23'))
setf('D55','-资产减值明细表!C24',-val('资产减值明细表','C24'))
setf('E55','-资产减值明细表!C25',-val('资产减值明细表','C25'))
for col in list('FGHIJ'):
    setf(f'{col}55','0',0)
setf('K55','SUM(C55:J55)',sum(float(V[f'{c}55']) for c in list('CDEFGHIJ')))
setf('L55','-SUM(资产减值明细表!C23:C25)',-sum(val('资产减值明细表',f'C{r}') for r in [23,24,25]))
setf('M55','K55-L55',0)
setstr('N55','IF(ABS(M55)<0.001,"✓","✗")','✓')
# Parent R50 from visible child rows.
for col in list('CDEFGHIJ'):
    cval=sum(float(V.get(f'{col}{r}',0.0)) for r in range(51,56))
    setf(f'{col}50',f'SUM({col}51:{col}55)',cval)
setf('K50','SUM(C50:J50)',sum(float(V[f'{c}50']) for c in list('CDEFGHIJ')))
setf('L50','现金流量表!D43',val('现金流量表','D43'))
setf('M50','K50-L50',0)
setstr('N50','IF(ABS(M50)<0.001,"✓","✗")','✓')

# --- D44 year-end row from corrected mirror rows, robust to dashes/blanks.
for col in list('CDEFGHIJ'):
    v56 = float(V.get(f'{col}32', val(SHEET_NAME,f'{col}32'))) + float(V.get(f'{col}33', val(SHEET_NAME,f'{col}33'))) + float(V.get(f'{col}45',0)) + float(V.get(f'{col}49', val(SHEET_NAME,f'{col}49'))) + float(V.get(f'{col}50',0))
    setf(f'{col}56', f'N({col}32)+N({col}33)+N({col}45)+N({col}49)+N({col}50)', v56)
setf('K56','SUM(C56:J56)',sum(float(V[f'{c}56']) for c in list('CDEFGHIJ')))
setf('L56','现金流量表!D44',val('现金流量表','D44'))
setf('M56','K56-L56',0)
setstr('N56','IF(ABS(M56)<0.001,"✓","✗")','✓')

# --- D46 book/carrying amount explicit source formula set.
setf('C57','试算表!H54',val('试算表','H54'))
setf('D57','试算表!H57',val('试算表','H57'))
setf('E57','试算表!H70',val('试算表','H70'))
setf('F57','试算表!H75',val('试算表','H75'))
setf('G57','试算表!H42',val('试算表','H42'))
setf('H57','试算表!H66',val('试算表','H66'))
setf('I57','N(附注!E364)+N(试算表!H71)',val('附注','E364')+val('试算表','H71'))
setf('J57','0',0)
setf('K57','SUM(C57:J57)',sum(float(V[f'{c}57']) for c in list('CDEFGHIJ')))
setf('L57','现金流量表!D46',val('现金流量表','D46'))
setf('M57','K57-L57',0)
setstr('N57','IF(ABS(M57)<0.001,"✓","✗")','✓')

# --- D47 validation from internal D44-D46 rows, not helper residual.
for col in list('CDEFGHIJ'):
    setf(f'{col}58', f'N({col}56)-N({col}57)', float(V[f'{col}56'])-float(V[f'{col}57']))
setf('K58','SUM(C58:J58)',sum(float(V[f'{c}58']) for c in list('CDEFGHIJ')))
setf('L58','现金流量表!D47',val('现金流量表','D47'))
setf('M58','K58-L58',0)
setstr('N58','IF(ABS(M58)<0.001,"✓","✗")','✓')

# Normalize K/M/N formulas across the whole R32:R58 block so no orphaned shared-formula
# followers remain after converting individual cells to explicit formulas.
def patched_numeric(ref):
    if ref in V and not isinstance(V[ref], str):
        return float(V[ref])
    return val(SHEET_NAME, ref)

for r in range(32, 59):
    kval = sum(patched_numeric(f'{c}{r}') for c in list('CDEFGHIJ'))
    setf(f'K{r}', f'SUM(C{r}:J{r})', kval)
    # Keep existing L formulas unless this row had no L or was explicitly patched.
    if f'L{r}' not in F:
        lf = wbf[SHEET_NAME][f'L{r}'].value
        lv = val(SHEET_NAME, f'L{r}')
        if lf is None and abs(kval) < 0.000001:
            setf(f'L{r}', '0', 0)
            lv = 0
        elif lf is not None:
            # preserve current line-level benchmark formula as an explicit formula cell
            setf(f'L{r}', str(lf)[1:] if isinstance(lf, str) and lf.startswith('=') else str(lf), lv)
    lval = patched_numeric(f'L{r}')
    mval = kval - lval
    setf(f'M{r}', f'K{r}-L{r}', mval)
    setstr(f'N{r}', f'IF(ABS(M{r})<0.001,"✓","✗")', '✓' if abs(mval) < 0.001 else '✗')

# Apply patch to OOXML with string-level worksheet edits.
sheet_part, sheet_id, rid = locate_sheet_part(SRC, SHEET_NAME)
assert sheet_part == 'xl/worksheets/sheet26.xml', sheet_part
src_sha = sha256_path(SRC)
with zipfile.ZipFile(SRC, 'r') as zin:
    parts = {name: zin.read(name) for name in zin.namelist()}

sheet_xml = parts[sheet_part].decode('utf-8')
orig_head = sheet_xml[:1000]
# Apply formula cells.
for ref in sorted(F.keys(), key=lambda r:(split_ref(r)[1], col_to_num(split_ref(r)[0]))):
    cell_xml = formula_cell(ref, F[ref], V[ref], result_type='str' if ref in STR else 'n')
    sheet_xml = replace_or_insert_cell(sheet_xml, ref, cell_xml)
# sanity: preserve namespace prefix declarations at the head
assert all(x in sheet_xml[:1200] for x in ['xmlns:mc=', 'xmlns:x14ac=', 'xmlns:xr=', 'xmlns:xr2=', 'xmlns:xr3=', 'mc:Ignorable="x14ac xr xr2 xr3"'])
parts[sheet_part] = sheet_xml.encode('utf-8')

# Remove calcChain and update workbook calc hints via string patch.
parts.pop('xl/calcChain.xml', None)
ct = parts['[Content_Types].xml'].decode('utf-8')
ct = re.sub(r'<Override\s+PartName="/xl/calcChain\.xml"\s+ContentType="application/vnd\.openxmlformats-officedocument\.spreadsheetml\.calcChain\+xml"\s*/>', '', ct)
parts['[Content_Types].xml'] = ct.encode('utf-8')
rels = parts['xl/_rels/workbook.xml.rels'].decode('utf-8')
rels = re.sub(r'<Relationship\s+Id="[^"]+"\s+Type="http://schemas\.openxmlformats\.org/officeDocument/2006/relationships/calcChain"\s+Target="calcChain\.xml"\s*/>', '', rels)
parts['xl/_rels/workbook.xml.rels'] = rels.encode('utf-8')
wbxml = parts['xl/workbook.xml'].decode('utf-8')
calcpr = '<calcPr calcMode="auto" fullCalcOnLoad="1" forceFullCalc="1" iterateDelta="1E-4"/>'
if re.search(r'<calcPr\b[^>]*/>', wbxml):
    wbxml = re.sub(r'<calcPr\b[^>]*/>', calcpr, wbxml, count=1)
elif re.search(r'<calcPr\b[^>]*>.*?</calcPr>', wbxml, re.S):
    wbxml = re.sub(r'<calcPr\b[^>]*>.*?</calcPr>', calcpr, wbxml, count=1, flags=re.S)
else:
    wbxml = wbxml.replace('</workbook>', calcpr + '</workbook>')
parts['xl/workbook.xml'] = wbxml.encode('utf-8')

OUTDIR.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(OUT, 'w', compression=zipfile.ZIP_DEFLATED) as zout:
    with zipfile.ZipFile(SRC, 'r') as zin:
        for name in zin.namelist():
            if name == 'xl/calcChain.xml':
                continue
            zi = zin.getinfo(name)
            data = parts[name]
            new_zi = zipfile.ZipInfo(filename=name, date_time=zi.date_time)
            new_zi.compress_type = zipfile.ZIP_DEFLATED
            new_zi.external_attr = zi.external_attr
            zout.writestr(new_zi, data)

# Verification.
checks=[]
checks.append(('source_sha256_before_after_same', src_sha == sha256_path(SRC)))
checks.append(('source_sha256', src_sha))
checks.append(('output_sha256', sha256_path(OUT)))
with zipfile.ZipFile(OUT) as z:
    names=z.namelist()
    checks.append(('zip_testzip', z.testzip()))
    checks.append(('parts', len(names)))
    checks.append(('calcChain_present', 'xl/calcChain.xml' in names))
    checks.append(('sheet_part', sheet_part))
    sx=z.read(sheet_part).decode('utf-8')
    checks.append(('prefixes_preserved', all(x in sx[:1200] for x in ['xmlns:mc=', 'xmlns:x14ac=', 'xmlns:xr=', 'xmlns:xr2=', 'xmlns:xr3=', 'mc:Ignorable="x14ac xr xr2 xr3"'])))
# Changed parts list.
with zipfile.ZipFile(SRC) as zs, zipfile.ZipFile(OUT) as zo:
    ns,no=set(zs.namelist()),set(zo.namelist())
    added=sorted(no-ns)
    removed=sorted(ns-no)
    changed=[]
    for n in sorted(ns & no):
        if hashlib.sha256(zs.read(n)).hexdigest()!=hashlib.sha256(zo.read(n)).hexdigest():
            changed.append(n)
    checks.append(('added_parts', added))
    checks.append(('removed_parts', removed))
    checks.append(('changed_parts', changed))

wbf2 = openpyxl.load_workbook(OUT, data_only=False, read_only=False, keep_links=True)
wbv2 = openpyxl.load_workbook(OUT, data_only=True, read_only=False, keep_links=True)
checks.append(('openpyxl_formula_sheet_count', len(wbf2.sheetnames)))
checks.append(('openpyxl_value_sheet_count', len(wbv2.sheetnames)))
wsf=wbf2[SHEET_NAME]
wsv=wbv2[SHEET_NAME]
row_checks=[]
for r in [32,33,45,49,50,56,57,58]:
    K=wsv[f'K{r}'].value; L=wsv[f'L{r}'].value; M=wsv[f'M{r}'].value; N=wsv[f'N{r}'].value
    ok = (abs(float(K or 0)-float(L or 0)) < 0.001 and abs(float(M or 0)) < 0.001 and str(N).strip()=='✓')
    row_checks.append((r,K,L,M,N,wsf[f'K{r}'].value,ok))
checks.append(('main_rows_pass', all(x[-1] for x in row_checks)))
# Additional formula checks for critical internal logic.
critical = {
    'C36': wsf['C36'].value,
    'C50': wsf['C50'].value,
    'C51': wsf['C51'].value,
    'C58': wsf['C58'].value,
    'I32': wsf['I32'].value,
    'I57': wsf['I57'].value,
}

lines=[]
lines.append('# Phase 6 — R29:R58 内部公式修正验证')
lines.append('')
lines.append(f'- 输出文件：`{OUT}`')
lines.append(f'- 源文件：`{SRC}`')
lines.append('- 方式：从原始 workbook 出发，只做 OOXML 字符串级 cell 片段替换；不使用 openpyxl 保存；移除 calcChain 并设置打开重算。')
lines.append('')
for k,vv in checks:
    lines.append(f'- {k}: `{vv}`')
lines.append('')
lines.append('## 主行闭环')
lines.append('| 行 | K | L | M | N | K公式 | PASS |')
lines.append('|---:|---:|---:|---:|---|---|---|')
for r,K,L,M,N,Kf,ok in row_checks:
    def show(x):
        if isinstance(x,(int,float)):
            return f'{x:,.2f}'
        return '' if x is None else str(x)
    lines.append(f'| {r} | {show(K)} | {show(L)} | {show(M)} | {show(N)} | `{Kf}` | {ok} |')
lines.append('')
lines.append('## 关键内部公式')
for ref, form in critical.items():
    lines.append(f'- {ref}: `{form}` ; cached=`{wsv[ref].value}`')
lines.append('')
lines.append('## 业务修正摘要')
lines.append('- R33：D40 子项改为净贡献口径；进项税/C241/C242/C243 不再多勾稽进父项。')
lines.append('- R50：D43 子项改为 signed contribution；补 C202，销项税 C197/C689 净额抵消，C196/L13 按源公式抵消；父行由 R51:R55 合计。')
lines.append('- R56：由修正后的 R32/R33/R45/R49/R50 推导，使用 N() 避免空值/破折号。')
lines.append('- R58：由本区块内部 R56-R57 推导，不再引用旧 helper residual。')
lines.append('- R32/R57：补齐 D39/D46 中开发支出 B71/H71，并去除生产性生物资产 B61 对 D39 mirror 的影响。')
VERIFY.write_text('\n'.join(lines), encoding='utf-8')

print(json.dumps({
    'out': str(OUT),
    'verify': str(VERIFY),
    'checks': dict(checks),
    'rows': row_checks,
    'critical': critical,
}, ensure_ascii=False, indent=2, default=str))
