#!/usr/bin/env python3
"""Generate to-be delta SVGs (module/flow/sequence) per fireworks-tech-graph rules.
Delta colors: green=#d4edda (added) / amber=#fff3cd (modified) / red=#ffe0e0 (removed).
"""
import os

ARD = '#155724'  # green stroke
AMB = '#856404'  # amber stroke
RED = '#c62828'  # red stroke

def gen_module_delta():
    W, H = 1280, 760
    NW, NH = 180, 56
    CLW = 220
    L = []
    emit = L.append
    emit('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" font-family="Segoe UI, Helvetica, Arial, sans-serif">' % (W, H))
    emit('  <defs>')
    emit('    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#475569"/></marker>')
    emit('    <marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="%s"/></marker>' % ARD)
    emit('    <style>')
    emit('      .title { font-size:18px; font-weight:700; fill:#7f1d1d; }')
    emit('      .panel { font-size:13px; font-weight:700; }')
    emit('      .lbl { font-size:13px; font-weight:600; fill:#0f172a; text-anchor:middle; }')
    emit('      .sub { font-size:10.5px; fill:#475569; text-anchor:middle; }')
    emit('      .tag { font-size:10px; font-weight:700; }')
    emit('      .elab { font-size:10.5px; fill:#475569; }')
    emit('      .elabG { font-size:10.5px; fill:%s; }' % ARD)
    emit('      .elabA { font-size:10.5px; fill:%s; }' % AMB)
    emit('      .elabR { font-size:10.5px; fill:%s; }' % RED)
    emit('      .legend { font-size:11px; fill:#475569; }')
    emit('    </style>')
    emit('  </defs>')
    emit('  <rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
    emit('  <text x="24" y="32" class="title">to-be &#183; module delta &#183; ++ added / ~ modified / -- removed</text>')

    TK_X, TK_Y, TK_W, TK_H = 40, 70, 580, 600
    emit('  <rect x="%d" y="%d" width="%d" height="%d" fill="#f0f9ff" stroke="#1976d2" stroke-width="1.4" stroke-dasharray="6 4" rx="8"/>' % (TK_X, TK_Y, TK_W, TK_H))
    emit('  <text x="%d" y="%d" class="panel" fill="#1976d2">harness-rd-toolkit (source)</text>' % (TK_X+12, TK_Y+22))
    CL_X, CL_Y, CL_W, CL_H = 700, 70, 540, 600
    emit('  <rect x="%d" y="%d" width="%d" height="%d" fill="#fffaf0" stroke="#f57c00" stroke-width="1.4" stroke-dasharray="6 4" rx="8"/>' % (CL_X, CL_Y, CL_W, CL_H))
    emit('  <text x="%d" y="%d" class="panel" fill="#f57c00">客户端项目 (targetDir)</text>' % (CL_X+12, CL_Y+22))

    # Nodes
    cli_x, cli_y = 80, 120       # ~
    ini_x, ini_y = 340, 120      # unchanged
    upd_x, upd_y = 340, 220      # ~
    ver_x, ver_y = 80, 220       # ~
    newcheck_x, newcheck_y = 80, 320   # ++ (added)
    newstate_x, newstate_y = 340, 320  # ++ (added)
    protected_x, protected_y = 80, 430  # ++ (added, internal helper)
    guided_x, guided_y = 340, 430  # ++ (added, internal helper)

    claude_x, claude_y = 760, 120
    set_x, set_y = 760, 210
    ctx_x, ctx_y = 760, 300
    cfg_x, cfg_y = 760, 390
    stamp_x, stamp_y = 760, 480
    stamp_w, stamp_h = 220, 80

    force_x, force_y = 760, 580
    force_w, force_h = 220, 60

    def R(x,y,w,h): return (x+w, y+h//2)
    def LE(x,y,w,h): return (x, y+h//2)
    def T(x,y,w,h): return (x+w//2, y)
    def B(x,y,w,h): return (x+w//2, y+h)

    # ===== EDGES (all green for added connections) =====
    # cli --update-check--> newcheck
    sx,sy = B(cli_x,cli_y,NW,NH); ex,ey = T(newcheck_x,newcheck_y,NW,NH)
    emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (sx,sy,ex,ey,ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ update-check</text>' % (sx+8, (sy+ey)//2))

    # cli --guided update--> upd
    sx,sy = R(cli_x,cli_y,NW,NH); ex,ey = LE(upd_x,upd_y,NW,NH)
    mx = (sx+ex)//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arr)"/>' % (sx,sy,mx,sy,mx,ey,ex,ey,'#856404'))
    emit('  <text x="%d" y="%d" class="elabA">~ guided update</text>' % (mx+8, (sy+ey)//2))

    # cli --version--> ver
    sx,sy = B(cli_x,cli_y+20,NW,NH); ex,ey = T(ver_x,ver_y,NW,NH) if False else (ver_x+NW//2, ver_y)
    # actually cli is above ver
    emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arr)"/>' % (cli_x+NW//2, cli_y+NH, ver_x+NW//2, ver_y, '#856404'))
    emit('  <text x="%d" y="%d" class="elabA">~ version 对照</text>' % (cli_x+NW+8, (cli_y+NH+ver_y)//2))

    # newcheck -> newstate (added edge)
    sx,sy = B(newcheck_x,newcheck_y,NW,NH); ex,ey = T(newstate_x,newstate_y,NW,NH)
    # route diagonally
    emit('  <path d="M %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (sx,sy,(sx+ex)//2,sy+30,(sx+ex)//2,sy+30,ARD))
    # simpler: newcheck reads newstate
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (newcheck_x+NW, newcheck_y+NH//2, (newcheck_x+NW+newstate_x)//2, newcheck_y+NH//2, (newcheck_x+NW+newstate_x)//2, newstate_y+NH//2, newstate_x, newstate_y+NH//2, ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ 读 state</text>' % ((newcheck_x+NW+newstate_x)//2-30, newcheck_y+NH//2-6))

    # upd -> newstate
    sx,sy = B(upd_x,upd_y,NW,NH); ex,ey = T(newstate_x,newstate_y,NW,NH)
    emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (sx,sy,ex,ey,ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ 读/写 state</text>' % (sx+8, (sy+ey)//2))

    # upd -> protected (++)
    sx,sy = B(upd_x,upd_y+20,NW,NH); ex,ey = T(protected_x,protected_y,NW,NH) if False else (upd_x+NW//2, protected_y)
    # upd above protected (different column); protected is below newcheck, upd is at column 1
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (upd_x, upd_y+NH, upd_x, (upd_y+NH+protected_y)//2, protected_x+NW, (upd_y+NH+protected_y)//2, protected_x+NW, protected_y+NH//2, ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ uses</text>' % ((upd_x+protected_x+NW)//2-30, (upd_y+NH+protected_y)//2-6))

    # upd -> guided (++)
    sx,sy = B(upd_x,upd_y+40,NW,NH); ex,ey = T(guided_x,guided_y,NW,NH)
    emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (sx,sy,ex,ey,ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ entrypoint</text>' % (sx+8, (sy+ey)//2))

    # upd -> client (modified sync, amber)
    sx,sy = R(upd_x,upd_y,NW,NH); ex,ey = LE(cfg_x,cfg_y,CLW,NH)
    mx = (sx+ex)//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arr)"/>' % (sx,sy,mx,sy,mx,ey,ex,ey,'#856404'))
    emit('  <text x="%d" y="%d" class="elabA">~ 受清单约束同步</text>' % (mx+8, sy-6))

    # upd -> settings (unchanged union merge)
    sx,sy = R(upd_x,upd_y+15,NW,NH); ex,ey = LE(set_x,set_y,CLW,NH)
    mx = (sx+ex)//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="#475569" stroke-width="1.4" fill="none" marker-end="url(#arr)" stroke-dasharray="4 3"/>' % (sx,sy,mx,sy,mx,ey,ex,ey))
    emit('  <text x="%d" y="%d" class="elab">union merge (不变)</text>' % (mx+8, sy+18))

    # upd -> CLAUDE (red dashed -- force 无效)
    sx,sy = R(upd_x,upd_y-15,NW,NH); ex,ey = LE(claude_x,claude_y,CLW,NH)
    mx = (sx+ex)//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arr)" stroke-dasharray="5 3"/>' % (sx,sy,mx,sy,mx,ey,ex,ey,RED))
    emit('  <text x="%d" y="%d" class="elabR">++ 受保护: 逐项确认</text>' % (mx+8, ey-6))

    # upd -> ctx (++)
    sx,sy = R(upd_x,upd_y+30,NW,NH); ex,ey = LE(ctx_x,ctx_y,CLW,NH)
    mx = (sx+ex)//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (sx,sy,mx,sy,mx,ey,ex,ey,ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ 受保护逐项</text>' % (mx+8, ey+18))

    # newstate -> stamp (++)
    sx,sy = R(newstate_x,newstate_y,NW,NH); ex,ey = LE(stamp_x,stamp_y,stamp_w,stamp_h)
    mx = (sx+ex)//2
    emit('  <path d="M %d %d L %d %d L %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (sx,sy,mx,sy,mx,ey,ex,ey,ARD))
    emit('  <text x="%d" y="%d" class="elabG">++ 读写</text>' % (mx+8, (sy+ey)//2))

    # ===== NODES =====
    def node(x,y,w,h,fill,stroke,label,sub=None,lbl_fill='#0f172a',sub_fill='#475569',sw=1.6,dash=None,tag=None):
        d = ' stroke-dasharray="%s"' % dash if dash else ''
        emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="%s" stroke="%s" stroke-width="%s"%s/>' % (x,y,w,h,fill,stroke,sw,d))
        if tag:
            emit('  <text x="%d" y="%d" class="tag" fill="%s">%s</text>' % (x+6, y-4, stroke, tag))
        cy = y + h//2
        if sub:
            emit('  <text x="%d" y="%d" class="lbl" fill="%s">%s</text>' % (x+w//2, cy-4, lbl_fill, label))
            emit('  <text x="%d" y="%d" class="sub" fill="%s">%s</text>' % (x+w//2, cy+12, sub_fill, sub))
        else:
            emit('  <text x="%d" y="%d" class="lbl" fill="%s">%s</text>' % (x+w//2, cy+5, lbl_fill, label))

    # toolkit nodes
    node(cli_x, cli_y, NW, NH, '#fff3cd', AMB, 'cli.ts', '+ update-check 子命令', tag='~')
    node(ini_x, ini_y, NW, NH, '#ffffff', '#3b82f6', 'init.ts', '不变', sw=1.3)
    node(upd_x, upd_y, NW, NH, '#fff3cd', AMB, 'update.ts', '+ ProtectedRegistry + guided', tag='~')
    node(ver_x, ver_y, NW, NH, '#fff3cd', AMB, 'version.ts', '+ compareVersions / resolveRemote', tag='~')
    node(newcheck_x, newcheck_y, NW, NH, '#d4edda', ARD, 'update-check.ts', 'runUpdateCheck (纯读)', tag='++')
    node(newstate_x, newstate_y, NW, NH, '#d4edda', ARD, 'client-state.ts', '读写 .harness-toolkit-state/', tag='++')
    node(protected_x, protected_y, NW, NH, '#d4edda', ARD, 'ProtectedRegistry', 'CLAUDE/settings/CONTEXT', tag='++', sw=1.6)
    node(guided_x, guided_y, NW, NH, '#d4edda', ARD, 'guidedUpdateFlow()', '双重确认入口', tag='++')

    # client nodes
    node(claude_x, claude_y, CLW, NH, '#ffffff', '#f57c00', 'CLAUDE.md', '(++, 受保护, --force 无效)')
    node(set_x, set_y, CLW, NH, '#ffffff', '#f57c00', '.claude/settings.json', '(字段 union, 不变)')
    node(ctx_x, ctx_y, CLW, NH, '#ffffff', '#f57c00', 'CONTEXT.md', '(++, 受保护逐项)')
    node(cfg_x, cfg_y, CLW, NH, '#ffffff', '#f57c00', 'pipeline*.yaml / gates / skills', '(逐项 y/N)')
    node(stamp_x, stamp_y, stamp_w, stamp_h, '#d4edda', ARD, '.harness-toolkit-state/', 'installed-version / deletions.txt / last-sync-at', tag='++')
    # force constraint callout
    emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="8" fill="#fff3cd" stroke="%s" stroke-width="1.6"/>' % (force_x, force_y, force_w, force_h, AMB))
    emit('  <text x="%d" y="%d" class="lbl" fill="%s">--force 行为变更</text>' % (force_x+force_w//2, force_y+18, AMB))
    emit('  <text x="%d" y="%d" class="sub" fill="%s">-- 受保护文件无效 (RejectForce)</text>' % (force_x+force_w//2, force_y+34, AMB))
    emit('  <text x="%d" y="%d" class="sub" fill="%s">++ 非受保护文件 --force 仍可</text>' % (force_x+force_w//2, force_y+50, '#155724'))

    # Legend
    LG_Y = H - 56
    emit('  <rect x="40" y="%d" width="1200" height="44" fill="#f8fafc" stroke="#cbd5e1" rx="6"/>' % LG_Y)
    emit('  <text x="56" y="%d" class="legend" font-weight="700">Legend:</text>' % (LG_Y+26))
    items = [('#d4edda', ARD, '++ added'), ('#fff3cd', AMB, '~ modified'), ('#ffe0e0', RED, '-- removed (虚线)'), ('#ffffff', '#3b82f6', 'unchanged')]
    lx = 130
    for fill, stroke, txt in items:
        dash = ' stroke-dasharray="3 2"' if 'removed' in txt else ''
        emit('  <rect x="%d" y="%d" width="18" height="14" rx="3" fill="%s" stroke="%s" stroke-width="1.4"%s/>' % (lx, LG_Y+14, fill, stroke, dash))
        emit('  <text x="%d" y="%d" class="legend">%s</text>' % (lx+24, LG_Y+26, txt))
        lx += 180

    emit('</svg>')
    p = 'docs/4plus1/delta/client-update-flow/to-be/rendered/module-delta.svg'
    with open(p, 'w', encoding='utf-8') as f:
        f.write('\n'.join(L))
    print('module-delta written lines=%d size=%d' % (len(L), os.path.getsize(p)))


def gen_flow_delta():
    W, H = 1280, 1080
    L = []
    emit = L.append
    emit('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" font-family="Segoe UI, Helvetica, Arial, sans-serif">' % (W, H))
    emit('  <defs>')
    emit('    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#475569"/></marker>')
    emit('    <marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="%s"/></marker>' % ARD)
    emit('    <marker id="arrA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="%s"/></marker>' % AMB)
    emit('    <style>')
    emit('      .title { font-size:18px; font-weight:700; fill:#7f1d1d; }')
    emit('      .cap { font-size:13px; font-weight:700; }')
    emit('      .added { fill:#d4edda; stroke:%s; stroke-width:1.6; }' % ARD)
    emit('      .modified { fill:#fff3cd; stroke:%s; stroke-width:1.5; }' % AMB)
    emit('      .plain { fill:#ffffff; stroke:#3b82f6; stroke-width:1.3; }')
    emit('      .lbl { font-size:12px; font-weight:600; fill:#0f172a; text-anchor:middle; }')
    emit('      .sub { font-size:10px; fill:#475569; text-anchor:middle; }')
    emit('      .elab { font-size:10.5px; fill:#475569; }')
    emit('      .elabG { font-size:10.5px; fill:%s; font-weight:600; }' % ARD)
    emit('      .elabA { font-size:10.5px; fill:%s; }' % AMB)
    emit('      .legend { font-size:11px; fill:#475569; }')
    emit('    </style>')
    emit('  </defs>')
    emit('  <rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
    emit('  <text x="24" y="32" class="title">to-be &#183; flow delta &#183; ++ added / ~ modified</text>')

    # notify lane (++)
    emit('  <text x="40" y="76" class="cap" fill="%s">提示链路 (++)</text>' % ARD)
    notify_nodes = [
        (40, 100, 220, 50, '++ update-check (SessionStart hook)'),
        (300, 100, 220, 50, '++ compareVersions(state)'),
        (560, 100, 220, 50, '++ 注入 session 提示'),
        (820, 100, 220, 50, '++ 用户看到提示'),
    ]
    emit('  <rect x="20" y="86" width="1240" height="80" fill="#ecfdf5" stroke="%s" stroke-width="1" rx="6"/>' % ARD)
    for i, (x,y,w,h,lbl) in enumerate(notify_nodes):
        emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="6" class="added"/>' % (x,y,w,h))
        emit('  <text x="%d" y="%d" class="lbl">%s</text>' % (x+w//2, y+h//2+5, lbl))
        if i < len(notify_nodes)-1:
            nx = notify_nodes[i+1]
            emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#arrG)"/>' % (x+w, y+h//2, nx[0], nx[1]+nx[3]//2, ARD))

    # guided update flow (~)
    emit('  <text x="40" y="200" class="cap" fill="%s">引导式 update (~)</text>' % AMB)
    emit('  <rect x="20" y="210" width="1240" height="380" fill="#fefce8" stroke="%s" stroke-width="1" rx="6"/>' % AMB)

    guided_nodes = [
        (40, 240, 220, 50, 'plain', 'harness-toolkit update', ''),
        (300, 240, 220, 50, 'added', '++ precheck + 读 state', '),
        (560, 240, 240, 50, 'added', '++ 引导总览 (++/~/-- 清单)', ''),
        (840, 240, 220, 50, 'added', '++ "确认进入同步?"', '需求: 客户点击确认'),
        (1080, 240, 140, 50, 'added', '用户 yes', ''),
        (40, 320, 220, 60, 'modified', '~ syncOneFile', '+ProtectedRegistry +deletions'),
        (300, 320, 220, 60, 'added', '++ dest 缠失? E deletions', '命中 -> skipped-user-deleted'),
        (560, 320, 220, 60, 'plain', 'dest -- src?', ''),
        (840, 320, 220, 60, 'added', '++ Protected?', 'CLAUDE/settings/CONTEXT'),
        (40, 410, 220, 60, 'modified', '~ settings.*: 字段 union', '不删项 (不变)'),
        (300, 410, 220, 60, 'added', '++ CLAUDE/CONTEXT 逐项', '--force 无效 (RejectForce)'),
        (560, 410, 220, 60, 'added', '++ 其他文件逐项', '--force 可自动 yes'),
        (840, 410, 220, 60, 'added', '++ 应用之前再问', '"按以上决策应用?"'),
        (40, 510, 220, 50, 'added', '++ 用户否认 -> 0 改动', ''),
        (300, 510, 220, 50, 'added', '++ 用户否认 -> 0 改动', ''),
        (560, 510, 220, 50, 'added', '++ 写文件 + 写 state', 'installed-version = toolkitVer'),
        (840, 510, 220, 50, 'modified', '~ formatUpdateReport', '+ state diff'),
    ]

    # Edges (drawn first)
    edges = [
        # (x1,y1, x2,y2, color, marker, dash=None, elab=None)
        (260,265, 300,265, '#475569', 'arr', None, None),
        (520,265, 560,265, ARD, 'arrG', None, '++ precheck'),
        (800,265, 840,265, ARD, 'arrG', None, None),
        (1060,265, 1080,265, ARD, 'arrG', None, None),
        (150,290, 150,320, '#475569', 'arr', None, None),
        (260,350, 300,350, '#475569', 'arr', None, None),
        (520,350, 560,350, '#475569', 'arr', None, None),
        (780,350, 840,350, '#475569', 'arr', None, None),
        (150,380, 70,410, '#475569', 'arr', '4 3', None),  # to settings path (routing simplified)
        (410,380, 410,410, ARD, 'arrG', None, None),
        (620,380, 410,410, ARD, 'arrG', None, None),
        (150,470, 150,510, ARD, 'arrG', None, None),
        (410,470, 670,510, ARD, 'arrG', None, None),
        (950,470, 950,510, ARD, 'arrG', None, None),
    ]
    for x1,y1,x2,y2,c,mk,dash,elab in edges:
        d = ' stroke-dasharray="%s"' % dash if dash else ''
        emit('  <path d="M %d %d L %d %d" stroke="%s" stroke-width="1.5" fill="none" marker-end="url(#%s)"%s/>' % (x1,y1,x2,y2,c,mk,d))
        if elab:
            emit('  <text x="%d" y="%d" class="elabG">%s</text>' % ((x1+x2)//2-20, (y1+y2)//2-6, elab))

    # Then draw nodes
    for (x,y,w,h,cls,lbl,sub) in guided_nodes:
        emit('  <rect x="%d" y="%d" width="%d" height="%d" rx="8" class="%s"/>' % (x,y,w,h,cls))
        if sub:
            emit('  <text x="%d" y="%d" class="lbl">%s</text>' % (x+w//2, y+h//2-4, lbl))
            emit('  <text x="%d" y="%d" class="sub">%s</text>' % (x+w//2, y+h//2+12, sub))
        else:
            emit('  <text x="%d" y="%d" class="lbl">%s</text>' % (x+w//2, y+h//2+5, lbl))

    # Legend
    LG_Y = H - 56
    emit('  <rect x="40" y="%d" width="1200" height="44" fill="#f8fafc" stroke="#cbd5e1" rx="6"/>' % LG_Y)
    emit('  <text x="56" y="%d" class="legend" font-weight="700">Legend:</text>' % (LG_Y+26))
    items = [('#d4edda', ARD, '++ added'), ('#fff3cd', AMB, '~ modified'), ('#ffffff', '#3b82f6', 'unchanged')]
    lx = 130
    for fill, stroke, txt in items:
        emit('  <rect x="%d" y="%d" width="18" height="14" rx="3" fill="%s" stroke="%s" stroke-width="1.4"/>' % (lx, LG_Y+14, fill, stroke))
        emit('  <text x="%d" y="%d" class="legend">%s</text>' % (lx+24, LG_Y+26, txt))
        lx += 180

    emit('</svg>')
    p = 'docs/4plus1/delta/client-update-flow/to-be/rendered/flow-delta.svg'
    with open(p, 'w', encoding='utf-8') as f:
        f.write('\n'.join(L))
    print('flow-delta written lines=%d size=%d' % (len(L), os.path.getsize(p)))


def gen_sequence_delta():
    lanes = [
        ('用户/Claude', 80),
        ('++ update-check', 240),
        ('++ client-state', 390),
        ('~ update.ts', 540),
        ('init.ts', 690),
        ('++ version.ts', 840),
        ('客户端 FS', 990),
    ]
    messages = [
        ('++ runUpdateCheck (SessionStart hook)', 0, 1, False, True),
        ('++ readState', 1, 2, False, True),
        ('读 installed-version', 2, 6, False, True),
        ('stamp', 6, 2, True, True),
        ('++ compareVersions -> hasUpdate', 2, 5, True, True),
        ('++ 注入提示到 session', 1, 0, False, True),
        ('harness-toolkit update (用户看到提示后)', 0, 3, False, False),
        ('++ readState + deletions', 3, 2, False, True),
        ('全量 hash 扫描', 3, 6, False, False),
        ('文件清单 + hash', 6, 3, True, False),
        ('++ 引导总览: N 更新 / M 冲突 / K 受保护 / L 删除戳记', 3, 0, False, True),
        ('++ "确认进入同步?" (Q3 双重确认之一)', 3, 0, False, True),
        ('++ yes', 0, 3, False, True),
        ('loop 每个待同步文件:', -1, -1, False, False),  # marker line
        ('++ dest 缺失 E deletions -> skipped-user-deleted', 3, 6, False, True),
        ('++ settings.* -> 字段 union merge', 3, 4, False, True),
        ('写回 settings.* (不删项)', 4, 6, False, True),
        ('++ 受保护(CLAUDE/CONTEXT): 逐项 y/N, 拒绝则不改 (硬约束)', 3, 0, False, True),
        ('++ --force 命中受保护 -> RejectForce, 不覆盖', 3, 0, False, True),
        ('++ 非受保护: 逐项 y/N (--force 可自动 yes)', 3, 0, False, True),
        ('++ "按以上决策应用?" (Q3 双重确认之二)', 3, 0, False, True),
        ('++ yes', 0, 3, False, True),
        ('++ 写"认可"项 + 字段合并 + state', 3, 6, False, True),
        ('++ 写 installed-version = toolkitVer', 3, 2, False, True),
        ('~ formatUpdateReport (+ state diff)', 3, 0, False, False),
    ]
    SKIP = -1
    W = 1100
    H = 100 + len(messages) * 50 + 130
    L = []
    emit = L.append
    emit('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 %d %d" font-family="Segoe UI, Helvetica, Arial, sans-serif">' % (W, H))
    emit('  <defs>')
    emit('    <marker id="arr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#475569"/></marker>')
    emit('    <marker id="arrG" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="%s"/></marker>' % ARD)
    emit('    <marker id="arrA" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="10" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="%s"/></marker>' % AMB)
    emit('    <style>')
    emit('      .title { font-size:18px; font-weight:700; fill:#7f1d1d; }')
    emit('      .lane { font-size:11.5px; font-weight:700; text-anchor:middle; }')
    emit('      .msg { font-size:10px; fill:#1f2937; }')
    emit('      .msgG { font-size:10px; fill:%s; font-weight:600; }' % ARD)
    emit('      .msgA { font-size:10px; fill:%s; }' % AMB)
    emit('      .note { font-size:10.5px; fill:#4338ca; font-style:italic; }')
    emit('      .legend { font-size:11px; fill:#475569; }')
    emit('    </style>')
    emit('  </defs>')
    emit('  <rect x="0" y="0" width="%d" height="%d" fill="#ffffff"/>' % (W, H))
    emit('  <text x="24" y="32" class="title">to-be &#183; sequence delta &#183; ++ added / ~ modified step</text>')

    lane_top = 70
    lane_bot = H - 90
    lane_fills = {0:'#f1f5f9', 1:'#d4edda', 2:'#d4edda', 3:'#fff3cd', 4:'#f1f5f9', 5:'#d4edda', 6:'#fff3e0'}
    lane_strokes = {0:'#cbd5e1', 1:ARD, 2:ARD, 3:AMB, 4:'#cbd5e1', 5:ARD, 6:'#f57c00'}
    for name, x in lanes:
        idx = lanes.index((name,x))
        emit('  <rect x="%d" y="%d" width="120" height="24" rx="5" fill="%s" stroke="%s"/>' % (x-60, lane_top-30, lane_fills[idx], lane_strokes[idx]))
        emit('  <text x="%d" y="%d" class="lane">%s</text>' % (x, lane_top-13, name))
        emit('  <line x1="%d" y1="%d" x2="%d" y2="%d" stroke="#94a3b8" stroke-width="1.2" stroke-dasharray="4 3"/>' % (x, lane_top, x, lane_bot))

    y = lane_top + 30
    for msg in messages:
        if len(msg) == 5:
            text, src, dst, dashed, added = msg
        else:
            text, src, dst, dashed = msg; added=False
        if src == SKIP and dst == SKIP:
            emit('  <text x="40" y="%d" class="note">%s</text>' % (y, text))
            y += 30
            continue
        x1 = lanes[src][1]; x2 = lanes[dst][1]
        if added:
            color = ARD; mk = 'arrG'; cls = 'msgG'
        else:
            color = '#475569'; mk = 'arr'; cls = 'msg'
        dash_attr = ' stroke-dasharray="4 3"' if dashed else ''
        emit('  <line x1="%d" y1="%d" x2="%d" y2="%d" stroke="%s" stroke-width="1.5" marker-end="url(#%s)"%s/>' % (x1, y, x2, y, color, mk, dash_attr))
        midx = (x1+x2)//2
        emit('  <text x="%d" y="%d" class="%s" text-anchor="middle">%s</text>' % (midx, y-6, cls, text))
        y += 50

    emit('  <text x="40" y="%d" class="msgG" font-weight="700">++ 全程有版本提示; ++ 双重确认; ++ 受保护清单; ++ 删除戳记守护</text>' % (H-50))
    emit('  <text x="40" y="%d" class="msgG" font-weight="700">-- 默认路径不再 exit 1; -- --force 对受保护文件无效 (RejectForce)</text>' % (H-32))

    emit('</svg>')
    p = 'docs/4plus1/delta/client-update-flow/to-be/rendered/sequence-delta.svg'
    with open(p, 'w', encoding='utf-8') as f:
        f.write('\n'.join(L))
    print('sequence-delta written lines=%d size=%d' % (len(L), os.path.getsize(p)))


if __name__ == '__main__':
    gen_module_delta()
    gen_flow_delta()
    gen_sequence_delta()
