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

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-085849'
OUT = OUTDIR / f'wanchuan_anhui_v9_v2_1_long_asset_R29_R58_asset_class_atomic_SAFE_{STAMP}.xlsx'
VERIFY = OUTDIR / f'phase9_asset_class_atomic_patch_verification_{STAMP}.md'
SHEET_NAME = '长期资产循环_全量穿透校验'

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):
    try:
        x = wbv[sheet][cell].value
    except Exception:
        return 0.0
    if x in (None, ''):
        return 0.0
    try:
        return float(x)
    except Exception:
        return 0.0

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

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 fmt_num(x):
    if isinstance(x, str): return x
    if x is None: x=0
    if abs(float(x)) < 0.0000005: x=0.0
    return repr(float(x))

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

def inline_str_cell(ref, text, style):
    return f'<c r="{ref}" s="{style}" t="inlineStr"><is><t>{html.escape(text, quote=False)}</t></is></c>'

def get_style(sheet_xml, ref, default='13'):
    m=re.search(r'<c\b(?=[^>]*\br="'+re.escape(ref)+r'")([^>]*)', sheet_xml)
    if not m: return default
    sm=re.search(r'\bs="([^"]+)"', m.group(1))
    return sm.group(1) if sm else default

def replace_or_insert_cell(sheet_xml, ref, new_cell):
    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,row=split_ref(ref); target=col_to_num(col)
    row_pat=re.compile(r'(<row\b(?=[^>]*\br="'+str(row)+r'")[^>]*>)(.*?)(</row>)', re.S)
    m=row_pat.search(sheet_xml)
    if not m: raise KeyError(f'row {row} not found')
    prefix,body,suffix=m.groups()
    insert_pos=len(body)
    for cm in re.finditer(r'<c\b(?=[^>]*\br="([A-Z]+)'+str(row)+r'")[^>]*(?:/>|>.*?</c>)', body, re.S):
        ccol=re.search(r'\br="([A-Z]+)'+str(row)+r'"', cm.group(0)).group(1)
        if col_to_num(ccol)>target:
            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={'m':'http://schemas.openxmlformats.org/spreadsheetml/2006/main','r':'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('m:sheets', NS):
            if sh.attrib.get('name')==sheet_name:
                rid=sh.attrib['{'+NS['r']+'}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)

F={}; V={}; STR={}; TEXT={}

def setf(ref, formula, value=0.0):
    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

def settext(ref, text):
    TEXT[ref]=text

# Optional label fixes: no new template, just make existing spare/other column explicit.
settext('J31','J 其他/抵消')
settext('B51','   D23净值/其他非抵消项')
settext('B52','   处置损益/Y22抵消')
settext('B53','   报废毁损/应收预收抵消')
settext('B54','   销项税抵消')

cols=list('CDEFGHIJ')

# R32 D39 opening: asset category level, no B61.
setf('C32','试算表!B54',val('试算表','B54'))
setf('D32','试算表!B57',val('试算表','B57'))
setf('E32','试算表!B70',val('试算表','B70'))
setf('F32','试算表!B75',val('试算表','B75'))
setf('G32','试算表!B42',val('试算表','B42'))
setf('H32','试算表!B66',val('试算表','B66'))
setf('I32','试算表!B71',val('试算表','B71'))
setf('J32','N(附注!F364)',val('附注','F364'))

# R34-R44 D40 additions. Same-asset formulas are acceptable; cross-offset terms are split into separate cells.
for c in cols:
    for r in range(34,45):
        setf(f'{c}{r}','0',0)
setf('C34','固定资产!H7',val('固定资产','H7'))
setf('D34','在建工程情况表!D24-在建工程情况表!Q24',val('在建工程情况表','D24')-val('在建工程情况表','Q24'))
setf('E34','无形资产变动表!I7',val('无形资产变动表','I7'))
setf('F34','长期待摊费用变动明细表!F17',val('长期待摊费用变动明细表','F17'))
setf('G34','投资性房地产!D8',val('投资性房地产','D8'))
# development expense C1205-G1205 split into two visible cells
setf('I35','附注!C1205',val('附注','C1205'))
setf('J35','-附注!G1205',-val('附注','G1205'))
# VAT enters D28 then is subtracted by D40
setf('C36','附注!C691',val('附注','C691'))
setf('J36','-附注!C691',-val('附注','C691'))
# C241 = -E24 enters D28; D40 subtracts C241, so split as +C241 and -C241 without bottom-helper net formula
setf('D37','-在建工程情况表!E24',-val('在建工程情况表','E24'))
setf('J37','在建工程情况表!E24',val('在建工程情况表','E24'))
# C242 = E1224-F1224; subtract C242
setf('C38','N(附注!E1224)',val('附注','E1224'))
setf('D38','-N(附注!F1224)',-val('附注','F1224'))
setf('E38','-N(附注!E1224)',-val('附注','E1224'))
setf('F38','N(附注!F1224)',val('附注','F1224'))
# C243 = C1239-B1239; subtract C243
setf('C39','N(附注!C1239)',val('附注','C1239'))
setf('D39','-N(附注!B1239)',-val('附注','B1239'))
setf('E39','-N(附注!C1239)',-val('附注','C1239'))
setf('F39','N(附注!B1239)',val('附注','B1239'))
# C244/C245/C246/C247 included in D40; split period-difference formulas
setf('I40','N(附注!E364)',val('附注','E364'))
setf('J40','-N(附注!F364)',-val('附注','F364'))
setf('C41','N(附注!D756)',val('附注','D756'))
setf('D41','-N(附注!C756)',-val('附注','C756'))
setf('H42','-N(长期应付款明细!G23)',-val('长期应付款明细','G23'))
setf('I42','N(长期应付款明细!H23)',val('长期应付款明细','H23'))
setf('C43','-附注!H1244',-val('附注','H1244'))
setf('D43','附注!I1244',val('附注','I1244'))
setf('H44','N(现金流量表底稿!C248)',val('现金流量表底稿','C248'))
setf('I44','N(现金流量表底稿!C249)',val('现金流量表底稿','C249'))
setf('J44','N(现金流量表底稿!C250)',val('现金流量表底稿','C250'))

# R45-R48 D41 other movement: preserve asset-level rows, explicit zeros and parent.
for c in cols:
    setf(f'{c}45',f'SUM({c}46:{c}48)',0)
for c in cols:
    setf(f'{c}48','0',0)
# R46/R47 existing asset-level formulas
setf('C46','固定资产!H53',val('固定资产','H53'))
setf('E46','无形资产变动表!I46',val('无形资产变动表','I46'))
setf('G46','投资性房地产!D50',val('投资性房地产','D50'))
setf('H46',"'使用权资产 '!G9-'使用权资产 '!G23-'使用权资产 '!G36",val('使用权资产 ','G9')-val('使用权资产 ','G23')-val('使用权资产 ','G36'))
setf('C47','-固定资产!H57',-val('固定资产','H57'))
setf('E47','-无形资产变动表!I50',-val('无形资产变动表','I50'))
setf('G47','-投资性房地产!D54',-val('投资性房地产','D54'))

# R49 D42 depreciation/amortization at asset category level.
for c in cols: setf(f'{c}49','0',0)
setf('C49','-固定资产!H30',-val('固定资产','H30'))
setf('E49','-无形资产变动表!I27',-val('无形资产变动表','I27'))
setf('F49','-长期待摊费用变动明细表!L17',-val('长期待摊费用变动明细表','L17'))
setf('G49','-投资性房地产!D29',-val('投资性房地产','D29'))
setf('H49',"-'使用权资产 '!G21",-val('使用权资产 ','G21'))

# R51-R55 D43 reductions: no one-cell giant formulas; split by asset class / offset term.
for r in range(51,56):
    for c in cols: setf(f'{c}{r}','0',0)
# R51: D23 non-offset net-value/other terms. Use asset schedule level where available; manual/blank bottom rows are N(bottom cell).
setf('C51','-(固定资产!H15-固定资产!H37-固定资产!H54)',-(val('固定资产','H15')-val('固定资产','H37')-val('固定资产','H54')))
setf('D51','-N(现金流量表底稿!C201)',-val('现金流量表底稿','C201'))
setf('E51','-(无形资产变动表!I15-无形资产变动表!I33-无形资产变动表!I47)',-(val('无形资产变动表','I15')-val('无形资产变动表','I33')-val('无形资产变动表','I47')))
setf('F51','-N(现金流量表底稿!C193)',-val('现金流量表底稿','C193'))
setf('G51','-(投资性房地产!D15-投资性房地产!D35-投资性房地产!D51)',-(val('投资性房地产','D15')-val('投资性房地产','D35')-val('投资性房地产','D51')))
setf('H51',"-('使用权资产 '!G12-'使用权资产 '!G26)",-(val('使用权资产 ','G12')-val('使用权资产 ','G26')))
setf('I51','-附注!C1051',-val('附注','C1051'))
setf('J51','-N(现金流量表底稿!C203)',-val('现金流量表底稿','C203'))
# R52: disposal P/L terms and Y22 offset, plus C198/C199 cancellation split.
setf('C52','-N(附注!C1029)',-val('附注','C1029'))
setf('D52','-N(附注!C1031)',-val('附注','C1031'))
setf('E52','-N(附注!C1030)',-val('附注','C1030'))
setf('G52','-N(附注!C1022)',-val('附注','C1022'))
setf('J52','试算表!Y22',val('试算表','Y22'))
# R53: scrap/damage and C198/C199 zero-net cancellation; split into individual source cells.
setf('C53','附注!C1067',val('附注','C1067'))
setf('D53','-附注!C1067',-val('附注','C1067'))
setf('E53','-N(现金流量表底稿!C198)',-val('现金流量表底稿','C198'))
setf('F53','N(现金流量表底稿!C198)',val('现金流量表底稿','C198'))
setf('G53','-N(现金流量表底稿!C199)',-val('现金流量表底稿','C199'))
setf('H53','N(现金流量表底稿!C199)',val('现金流量表底稿','C199'))
# R54: VAT output-tax cancellation.
setf('C54','-附注!C689',-val('附注','C689'))
setf('D54','附注!C689',val('附注','C689'))
# R55: impairment.
setf('C55','-N(资产减值明细表!C23)',-val('资产减值明细表','C23'))
setf('D55','-N(资产减值明细表!C24)',-val('资产减值明细表','C24'))
setf('E55','-N(资产减值明细表!C25)',-val('资产减值明细表','C25'))

# R57 D46 closing book value: asset category level, split H71/E364.
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','试算表!H71',val('试算表','H71'))
setf('J57','N(附注!E364)',val('附注','E364'))

# Calculate K/L/M/N, parent rows and derived rows.
def num(ref):
    if ref in V and not isinstance(V[ref], str): return float(V[ref])
    col,row=split_ref(ref)
    return val(SHEET_NAME, ref)

# child-row K/L/M/N. For child rows, L mirrors K unless it is a main benchmark row.
for r in list(range(32,45))+list(range(46,56))+[57]:
    if r in [33,45,50,56,58]: continue
    kval=sum(num(f'{c}{r}') for c in cols)
    setf(f'K{r}',f'SUM(C{r}:J{r})',kval)
    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 rows C:J
for c in cols:
    setf(f'{c}33',f'SUM({c}34:{c}44)',sum(num(f'{c}{r}') for r in range(34,45)))
    setf(f'{c}45',f'SUM({c}46:{c}48)',sum(num(f'{c}{r}') for r in range(46,49)))
    setf(f'{c}50',f'SUM({c}51:{c}55)',sum(num(f'{c}{r}') for r in range(51,56)))

# Main row K/L/M/N benchmarks.
main_L={32:('现金流量表!D39',val('现金流量表','D39')),33:('现金流量表!D40',val('现金流量表','D40')),45:('N(现金流量表!D41)',val('现金流量表','D41')),49:('现金流量表!D42',val('现金流量表','D42')),50:('现金流量表!D43',val('现金流量表','D43')),57:('现金流量表!D46',val('现金流量表','D46'))}
for r,(lf,lv) in main_L.items():
    kval=sum(num(f'{c}{r}') for c in cols)
    setf(f'K{r}',f'SUM(C{r}:J{r})',kval)
    setf(f'L{r}',lf,lv)
    setf(f'M{r}',f'K{r}-L{r}',kval-lv)
    setstr(f'N{r}',f'IF(ABS(M{r})<0.001,"✓","✗")','✓' if abs(kval-lv)<0.001 else '✗')

# Derived R56 and R58 by asset category columns.
for c in cols:
    setf(f'{c}56',f'N({c}32)+N({c}33)+N({c}45)+N({c}49)+N({c}50)',num(f'{c}32')+num(f'{c}33')+num(f'{c}45')+num(f'{c}49')+num(f'{c}50'))
setf('K56','SUM(C56:J56)',sum(num(f'{c}56') for c in cols))
setf('L56','现金流量表!D44',val('现金流量表','D44'))
setf('M56','K56-L56',num('K56')-val('现金流量表','D44'))
setstr('N56','IF(ABS(M56)<0.001,"✓","✗")','✓' if abs(num('K56')-val('现金流量表','D44'))<0.001 else '✗')
for c in cols:
    setf(f'{c}58',f'N({c}56)-N({c}57)',num(f'{c}56')-num(f'{c}57'))
setf('K58','SUM(C58:J58)',sum(num(f'{c}58') for c in cols))
setf('L58','现金流量表!D47',val('现金流量表','D47'))
setf('M58','K58-L58',num('K58')-val('现金流量表','D47'))
setstr('N58','IF(ABS(M58)<0.001,"✓","✗")','✓' if abs(num('K58')-val('现金流量表','D47'))<0.001 else '✗')

# Apply OOXML patch.
sheet_part,sheet_id,rid=locate_sheet_part(SRC,SHEET_NAME)
assert sheet_part=='xl/worksheets/sheet26.xml'
src_sha=sha256_path(SRC)
with zipfile.ZipFile(SRC,'r') as zin:
    parts={n:zin.read(n) for n in zin.namelist()}
sheet_xml=parts[sheet_part].decode('utf-8')
# text labels first
for ref,text in TEXT.items():
    style=get_style(sheet_xml,ref,'13')
    sheet_xml=replace_or_insert_cell(sheet_xml,ref,inline_str_cell(ref,text,style))
# formulas
for ref in sorted(F.keys(), key=lambda r:(split_ref(r)[1], col_to_num(split_ref(r)[0]))):
    sheet_xml=replace_or_insert_cell(sheet_xml,ref,formula_cell(ref,F[ref],V[ref],result_type='str' if ref in STR else 'n'))
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')
# calcChain removal + full recalc hints
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, zipfile.ZipFile(SRC,'r') as zin:
    for n in zin.namelist():
        if n=='xl/calcChain.xml': continue
        zi=zin.getinfo(n)
        new=zipfile.ZipInfo(filename=n,date_time=zi.date_time)
        new.compress_type=zipfile.ZIP_DEFLATED
        new.external_attr=zi.external_attr
        zout.writestr(new,parts[n])

# Verify.
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(); sx=z.read(sheet_part).decode('utf-8')
    checks.append(('zip_testzip',z.testzip()))
    checks.append(('calcChain_present','xl/calcChain.xml' in names))
    checks.append(('parts',len(names)))
    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"'])))
    shared=[]
    for m in re.finditer(r'<c\b(?=[^>]* t="shared")(?=[^>]* r="([A-Z]+)(\d+)")[^>]*>.*?</c>|<c\b(?=[^>]* t="shared")(?=[^>]* r="([A-Z]+)(\d+)")[^>]*/>', sx, re.S):
        rm=re.search(r'r="([A-Z]+)(\d+)"',m.group(0))
        if rm and 29<=int(rm.group(2))<=58: shared.append(rm.group(1)+rm.group(2))
    checks.append(('shared_formula_cells_R29_R58',shared))
with zipfile.ZipFile(SRC) as zs, zipfile.ZipFile(OUT) as zo:
    ns,no=set(zs.namelist()),set(zo.namelist())
    changed=[n for n in sorted(ns&no) if hashlib.sha256(zs.read(n)).hexdigest()!=hashlib.sha256(zo.read(n)).hexdigest()]
    checks.append(('removed_parts',sorted(ns-no)))
    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)
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(('openpyxl_formula_sheet_count',len(wbf2.sheetnames)))
checks.append(('openpyxl_value_sheet_count',len(wbv2.sheetnames)))
checks.append(('main_rows_pass',all(x[-1] for x in row_checks)))
# Formula atom check: no big bottom SUM formulas in C:J except allowed simple N(manual bottom blanks); C51 should no longer be giant.
critical={ref:wsf[ref].value for ref in ['C36','J36','C38','D38','E38','F38','C51','D51','I51','C52','J52','C53','D53','C54','D54','I57','J57']}
# LibreOffice smoke test if available.
lo_result='SKIPPED'
if shutil.which('soffice'):
    tmp=Path('/tmp/lo_asset_class_atomic_smoke')
    if tmp.exists(): shutil.rmtree(tmp)
    tmp.mkdir(parents=True,exist_ok=True)
    cp=subprocess.run(['soffice','--headless','--convert-to','xlsx','--outdir',str(tmp),str(OUT)],stdout=subprocess.PIPE,stderr=subprocess.STDOUT,text=True,timeout=180)
    if cp.returncode==0:
        rt=tmp/OUT.name
        try:
            with zipfile.ZipFile(rt) as z: ztest=z.testzip()
            wbvrt=openpyxl.load_workbook(rt,data_only=True,read_only=False,keep_links=True)
            wsrt=wbvrt[SHEET_NAME]
            okrt=[]
            for r in [32,33,45,49,50,56,57,58]:
                okrt.append((r,wsrt[f'K{r}'].value,wsrt[f'L{r}'].value,wsrt[f'M{r}'].value,wsrt[f'N{r}'].value))
            lo_result=f'PASS zip_testzip={ztest} rows={okrt}'
        except Exception as e:
            lo_result=f'FAIL reopen {e}'
    else:
        lo_result=f'FAIL {cp.stdout[:500]}'
checks.append(('libreoffice_smoke',lo_result))

lines=[]
lines.append('# Phase 9 — R29:R58 资产类别层级原子化修补验证')
lines.append('')
lines.append(f'- 输出文件：`{OUT}`')
lines.append(f'- 源文件：`{SRC}`')
lines.append('- 修补口径：不新增模板/新sheet；在 R29:R58 现有格子内，把跨业务/抵消/底稿大公式拆成资产类别层级的一格一项。固定资产等资产类别内部不再拆到 B:G。')
lines.append('')
for k,v in checks:
    lines.append(f'- {k}: `{v}`')
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,f in critical.items():
    lines.append(f'- {ref}: `{f}` ; cached=`{wsv[ref].value}`')
lines.append('')
lines.append('## 本版相对上一版的实质改进')
lines.append('- R36/R37/R38/R39 不再用一个净额大公式，而是把“进入 D28 的项”和“D40 扣除项”拆到不同格子。')
lines.append('- R51:R54 不再用 `SUM(C188:C203)-...` 的大公式；按资产类别/抵消项拆在 C:J 现有格子里。')
lines.append('- R57 将开发支出 H71 与附注 E364 拆到 I/J。')
lines.append('- 没有新增模板或新 sheet；仅复用 R29:R58 现有格子。')
VERIFY.write_text('\n'.join(lines),encoding='utf-8')

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