from pathlib import Path
import base64
import hashlib
import html
import json
import subprocess
import sys
import xml.etree.ElementTree as ET

ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT / 'modules/product/product-group-ai-native/ipd-ltc-itr'
ART = OUT / 'images'
EVIDENCE = Path(__file__).resolve().parent
SKILL = ROOT / 'standards-stack/agent-skills/skills/fireworks-tech-graph'
PAKCO = Path('/Users/jack/code/099-github/pakco-html')
ART.mkdir(exist_ok=True)

# Each handoff is sender, receiver, short diagram label, work split,
# artifact, receiving condition, and authoritative record.
scenarios = [
    dict(id='01-ltc', lane='LTC', title='新需求怎样变成报价、交付和回款',
         trigger='A客户提出多个食堂分开对账、总部统一汇总，希望先拿到方案与报价。',
         outcome='客户拿到经批准的范围与报价；成交后，交付和回款各有证据。',
         owner='销售负责人对商业结果负责；项目负责人对履约负责；财务确认到账。',
         actors=['销售', '产品', '研发／交付', '商务', '客户', '财务'],
         uc='UC-FLOW-001／006', source='framework.md 第3、6章',
         rule='本例假设现有标准能力可以满足。若有能力缺口，转场景02或03，客户需求仍保留。',
         exception='缺价或范围不明 → 商务退回产品／销售补齐；未到账 → 财务反馈销售继续跟进。',
         steps=[
             (0,1,'提交客户需求','销售核实客户场景、原话与期望；产品识别目标和验收。','客户需求单＋来源','产品能说明客户要解决什么、怎样验收。','售前采集＋客户需求编号'),
             (1,2,'评估现有能力','产品核对已发布能力与授权；研发评估接口，交付评估现场条件和实施。','能力匹配＋技术／实施评估','支持范围、限制、依赖和预计投入均明确。','需求决定＋评估证据'),
             (2,3,'交成本与边界','研发提供非标工作量，交付提供实施投入；商务核对收费与合同权益。','工时／成本＋配置边界','未知项不按0元；开发与实施成本不重复。','报价评估＋有效价表'),
             (3,0,'交批准报价包','商务批准指定版本；销售核对配置、报价、方案一致后与客户沟通。','配置＋报价＋方案快照','版本一致，审批、有效期和排除项可回查。','原报价工具＋审批记录'),
             (0,4,'确认合同范围','销售与客户对齐范围、交期、验收和付款；商务处理合同条款。','合同范围基线＋付款节点','客户期望、内部预计、批准交期分开记录。','合同／变更受限原件'),
             (4,2,'确认现场验收','项目负责人接收合同基线；交付实施培训，研发处理依赖；客户确认实际场景并回传验收结果。','实际部署＋培训＋验收记录','实际版本与功能验收齐全，遗留有负责人。','项目总表＋部署／验收记录'),
             (2,5,'交验收与节点','交付提供验收依据，销售跟进收款；财务核对合同节点、发票与到账。','验收凭据＋应收节点','按合同分期核对；发票不当到账。','财务到账真源＋项目概览'),
             (5,0,'反馈到账差额','财务确认到账和差额；销售继续跟进未收项，项目负责人承接交付遗留。','到账结果＋未收事项','商业结项与持续服务分别有去向。','回款节点＋售后交接'),
         ]),
    dict(id='02-ipd', lane='IPD', title='共性增强怎样进入标准产品',
         trigger='确认标准产品尚不支持总部汇总，A和其他客户的场景具有复用价值。',
         outcome='一个共性产品需求形成标准版本；每个来源客户各自得到答复和交付安排。',
         owner='产品负责人决定共性、优先级和发布范围；技术与测试分别对实现和质量负责。',
         actors=['销售／交付', '产品', '研发', '测试', '交付', '商务／推广'],
         uc='UC-FLOW-002', source='framework.md 第2、4章',
         rule='多个客户关联一个共性需求。客户合同承诺独立保留，不能被产品排期覆盖。',
         exception='评审整改未关 → 产品不排开发；测试未通过 → 研发修复；合同早于排期 → 管理者裁决。',
         steps=[
             (0,1,'汇入共性需求','销售提供采购与使用诉求，交付归并客户使用反馈与共性问题；产品查重并分析复用价值。','关联客户需求＋证据','每个来源可追溯，同一目标不重复立项。','原需求池＋客户需求关联'),
             (1,2,'交评审需求基线','产品明确本期／不做范围和UC；研发评估成本、兼容及依赖；管理者处理重大资源冲突。','评审通过的UC＋版本范围','整改关闭，规则、验收、责任和排期可执行。','云效详细UC＋评审基线'),
             (2,3,'交提测与构建','研发按目标仓库规则开发、review、自测；测试按同一UC验证主流程和异常。','构建／MR＋自测＋提测包','可准确复现，测试范围、迁移和已知限制明确。','云效／Codeup＋测试资产'),
             (3,1,'交验证准出报告','测试确认功能、兼容与回归；产品按业务目标验收；技术负责人签准出。','测试报告＋业务验收','阻断问题关闭，未经验证的场景不扩大承诺。','质量报告＋发布准出'),
             (1,4,'交正式发布包','产品明确标准范围；研发提供构建、迁移与回退；交付确认可实施。','发布说明＋准确构建＋部署包','可部署、可验证、可回退；客户适用清单齐全。','正式版本＋客户升级清单'),
             (1,5,'更新可售材料','产品更新能力与限制；商务确认权益和价表影响；推广仅使用批准的卖点与案例。','目录／价格影响＋手册／卖点','商业版本与实际能力一致，不把试点当全量可售。','产品目录＋批准资料版本'),
             (4,1,'回传客户验证','各项目交付按现场条件部署和验收，向产品反馈实际版本、问题及差异。','客户实例验证＋遗留','A验证只证明A；其他客户按场景04逐一评估。','实例／租户部署与验收'),
             (1,0,'反馈结果与去向','产品向销售和交付回告需求结果；销售维护客户承诺，交付更新客户计划并回链原问题。','需求去向＋客户答复','发布与客户交付分别记录，原问题由客户处理流程继续跟进。','原客户需求／问题回链'),
         ]),
    dict(id='03-custom', lane='LTC＋研发', title='专属定制怎样先确认范围再开发',
         trigger='A还要求接入自己的专用财务接口，这部分不属于标准总部汇总能力。',
         outcome='批准的专属范围得到交付；共性部分与客户专属权利、维护责任分开。',
         owner='项目负责人默认由交付承担，统筹履约；授权变更审批人批准范围；商务批准价格。',
         actors=['客户／销售', '交付负责人', '产品', '研发／测试', '商务', '交付执行'],
         uc='UC-FLOW-004', source='framework.md 第3、6章',
         rule='先核原合同。合同内的履约不能变成额外收费；新增范围才进入变更评估。',
         exception='客户未批准 → 保持原基线；定制影响标准能力 → 补兼容与回归；不直接复制A专有内容给B。',
         steps=[
             (0,1,'提出专属接口','销售保留客户来源；项目负责人核查当前合同与实施基线。','新增接口诉求＋原范围','能够判断是履约、澄清还是新增。','客户需求＋合同范围'),
             (1,2,'核对标准边界','项目负责人说明承诺与限制；产品拆清标准汇总能力和客户专属接口。','标准／专属范围分解','共性部分与客户特殊条件不混写。','需求分流决定'),
             (2,3,'交定制评估范围','产品给业务规则与验收；研发确认接口与维护成本，测试设计兼容验证。','UC＋接口约束＋验收','技术依赖、数据权限和不支持项明确。','云效UC＋接口契约'),
             (3,4,'交工期成本风险','研发提供开发估时；交付提供实施投入；商务核价、授权和长期维护条款。','技术／实施评估＋风险','报价与交期有依据，权利边界需客户确认。','报价评估记录'),
             (4,0,'交变更报价','商务批准指定版本；销售向客户说明新增范围、费用、周期及验收。','变更报价＋排除项','与原合同差异清楚，不口头扩大承诺。','变更与报价快照'),
             (0,1,'确认变更基线','客户授权人确认变更；项目负责人接收范围、资源和日期。','客户确认＋批准变更','权限与签署依据清楚；原基线保留历史。','批准变更版本'),
             (1,3,'交批准开发范围','项目负责人下发可执行切片；研发按范围实现，测试验证标准与专属两部分。','关联任务＋范围与验收','任务可回查批准依据，未批准内容不混入。','云效任务＋评审代码'),
             (3,5,'交定制发布包','研发提供准确构建与限制；测试给验证报告；交付确认现场部署与回退。','定制包＋测试＋操作说明','专属配置与标准包可区分，维护责任明确。','定制构建＋部署记录'),
             (5,0,'完成现场验收','交付按批准范围联调；客户确认接口与业务结果；销售按合同跟进回款。','客户验收＋遗留承接','实际版本、接口场景和结果可回查。','项目验收＋回款节点'),
         ]),
    dict(id='04-upgrade', lane='IPD→LTC', title='A上线后，B怎样决定并完成升级',
         trigger='总部汇总能力已形成正式版本并在A上线；B也使用相关产品。',
         outcome='B验证通过，或明确记录待授权、需适配、暂缓、不适用或失败回退。',
         owner='产品负责人统筹覆盖；B项目／受权上线负责人对B现场实施负责。',
         actors=['产品', '商务／销售', '研发', '测试', 'B交付／运维', 'B客户'],
         uc='UC-FLOW-003', source='framework.md 第7、8章',
         rule='同一发布逐实例／租户评估；“全部有决定”与“全部已同步”分开。B版本未知不能从名单删掉。',
         exception='待授权或增购 → 回LTC；需适配 → 先测试；客户暂缓 → 留复核日期；失败 → 回退并保留实际状态。',
         steps=[
             (0,4,'发布影响清单','产品列能力、支持版本和适用场景；B交付核实实际实例、租户、配置与现用版本。','发布范围＋B当前实例','客户多实例分别列，SaaS共享实例逐租户核权益。','版本发布×客户实例关系'),
             (4,1,'核对客户权益','B交付提出升级对象与服务内容；商务核合同授权，销售确认商业与客户安排。','权益与服务核查','包含、增购、待核有条款依据，不默认免费或收费。','合同权益＋客户需求'),
             (1,2,'交权益范围结论','商务说明可用范围；研发核B旧版、硬件、数据库、接口与A定制差异。','权益结论＋适配边界','A专有配置或数据不复制；未知版本先补证。','升级评估与技术方案'),
             (2,3,'交兼容迁移方案','研发给目标构建、迁移与回退；测试覆盖B实际组合及原有业务。','兼容／迁移／回退方案','测试对象与B一致，不仅验证A环境。','测试计划＋目标构建'),
             (3,4,'交兼容验证结论','测试给可升级、需适配或风险结论；交付据此安排现场条件。','兼容与回归报告','阻断项关闭，备份恢复与验证方法可执行。','升级准出与现场计划'),
             (4,5,'申请升级窗口','B交付说明影响、停机、备份和回退；B客户IT及授权人确认窗口与配合。','升级计划＋授权申请','窗口、责任人、客户依赖和退回条件明确。','客户批准记录'),
             (5,4,'批准升级计划','客户批准计划后交付／运维执行；测试协助业务回验，客户确认场景。','前后实际版本＋功能结果','部署成功之外，B业务场景验证和客户确认齐全。','B部署／验收证据'),
             (4,0,'回传实际结果','B交付记录已验证同步、暂缓、例外或失败回退；产品检查覆盖遗漏。','升级决定＋理由＋复核点','已同步有证据；暂缓有日期；失败不标完成。','跨客户同步关联记录'),
         ]),
    dict(id='05-itr', lane='ITR→IPD', title='投产问题怎样恢复、修复并防止复发',
         trigger='A使用汇总功能时发现报表数据不符合约定，业务受到影响。',
         outcome='客户问题经验证关闭；共性缺陷与其他客户影响有处理去向。',
         owner='售后负责人一直负责客户问题闭环；转研发或产品后仍不丢原工单。',
         actors=['客户', '售后', '研发／供方', '测试', '产品', '交付／运维'],
         uc='UC-FLOW-005', source='framework.md 第5章',
         rule='在保／过保与处理状态分开；恢复服务不等于解决根因；新增增强单列需求，不拖住故障。',
         exception='未验证／客户未确认 → 不关闭；复发 → 重开；涉及数据修正须具体授权，历史处理文字不构成授权。',
         steps=[
             (0,1,'报障并说明影响','客户提供发生场景；售后记录影响、实例、时间、服务约定与结果责任人。','工单＋影响与来源','问题可定位，保修权益和处理状态分别记录。','原问题编号＋售后台账'),
             (1,5,'安排恢复与缓解','售后统筹优先级；交付／运维按批准权限恢复或缓解，记录仍未解决的风险。','恢复任务＋权限边界','恢复措施可回退；合同SLA和下一更新时间明确。','工单恢复记录'),
             (5,2,'交现场定位证据','交付回报售后恢复情况，并把现场版本、复现与日志索引交研发／供方定位。','恢复证据＋复现／版本','只用受限必要证据；不把临时恢复标已关闭。','工单＋研发关联缺陷'),
             (1,4,'关联共性缺陷','售后保留原工单，产品确认是否为共性缺陷及其他受支持客户的影响。','共性判断＋受影响范围','共性问题进入IPD，现场恢复继续推进。','IPD缺陷／产品需求回链'),
             (2,3,'交修复与构建','研发／供方提出根因与修复；测试验证复现、回归及兼容，产品确认约定行为。','修复版本＋根因说明','旧错误不再复现，原业务没有新的阻断。','修复MR／构建＋测试'),
             (3,5,'交回归验证结果','测试说明通过范围和限制；交付准备客户实例的更新、验证与回退。','回归报告＋现场计划','本地通过不代替现场；升级仍需客户窗口和授权。','测试报告＋升级计划'),
             (5,0,'完成现场验证','交付执行已授权措施；客户按原场景检查数据与业务结果，测试协助。','现场验证＋实际版本','客户场景通过，数据核对有依据，遗留明确。','现场回验记录'),
             (0,1,'确认解决结果','客户确认处理结果；售后检查关闭条件、剩余任务与复发风险。','客户确认＋关闭依据','未回复保留待确认；重复问题可重新打开。','工单处理状态与关闭证据'),
             (1,4,'提交复盘与知识','售后总结根因、服务投入与反复问题；产品评估标准改进和其他客户处理。','复盘＋脱敏FAQ＋改进项','经验有适用版本，改进有责任与验收，不复制私有数据。','知识资产＋客户覆盖清单'),
             (4,1,'反馈改进与去向','产品反馈修复、暂缓或标准化决定；售后回告相关客户并保留跟进。','评审决定＋相关客户去向','原工单关闭与长期改进分别记录，不丢待办。','工单／需求／发布关联'),
         ]),
]


def svg_start(width, height, title, description):
    return [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" data-diagram-type="sequence" data-quality-profile="showcase" role="img" aria-labelledby="title desc">',
            f'<title id="title">{html.escape(title)}</title>', f'<desc id="desc">{html.escape(description)}</desc>',
            '<style>text{font-family:"PingFang SC","Microsoft YaHei",Helvetica,Arial,sans-serif}</style>',
            '<defs><marker id="arrow" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto"><polygon points="0 0,10 3.5,0 7" fill="#2563eb"/></marker></defs>',
            f'<rect width="{width}" height="{height}" fill="#ffffff" data-graph-role="background"/>']


def text(lines, x, y, value, size=18, color='#111827', anchor='start', role=None, owner=''):
    extra = f' data-graph-role="{role}"' if role else ''
    if owner:
        extra += f' data-owner="{owner}"'
    lines.append(f'<text x="{x}" y="{y}" font-size="{size}" fill="{color}" text-anchor="{anchor}"{extra}>{html.escape(value)}</text>')


def box(lines, x, y, width, height, node, fill='#eff6ff'):
    lines.append(f'<rect id="{node}" data-node-id="{node}" data-graph-role="node" x="{x}" y="{y}" width="{width}" height="{height}" rx="8" fill="{fill}" stroke="#d1d5db" stroke-width="1.5"/>')


def edge(lines, key, points, source, target):
    path = 'M ' + ' L '.join(f'{x},{y}' for x, y in points)
    lines.append(f'<path id="{key}" data-edge-id="{key}" data-graph-role="edge" data-source="{source}" data-target="{target}" d="{path}" fill="none" stroke="#2563eb" stroke-width="2" marker-end="url(#arrow)"/>')


def sequence(scene):
    height = 300 + 84 * len(scene['steps'])
    lines = svg_start(1440, height, scene['title'], '从上到下是交接顺序；每列是参与岗位。示例演示，不代表已执行。')
    text(lines,40,48,scene['lane']+' · '+scene['title'],30)
    text(lines,40,83,'从上到下读顺序；箭头表示把工作和产物交给谁。主责与协同细节见文档同号步骤。',17,'#6b7280')
    centers = [210 + 220 * i for i in range(6)]
    for i, (actor, x) in enumerate(zip(scene['actors'], centers)):
        box(lines,x-85,120,170,62,f'role-{i}')
        text(lines,x,158,actor,20,anchor='middle')
        lines.append(f'<line x1="{x}" y1="188" x2="{x}" y2="{height-125}" stroke="#d1d5db" stroke-dasharray="5 7" data-graph-role="decoration"/>')
    for i,(sender,receiver,label,split,artifact,condition,record) in enumerate(scene['steps']):
        y = 250 + i * 84
        text(lines,40,y+5,f'{i+1:02}',22,'#2563eb')
        key=f'e{i+1}'
        x1,x2=centers[sender],centers[receiver]
        edge(lines,key,[(x1,y),(x2,y)],f'role-{sender}',f'role-{receiver}')
        text(lines,(x1+x2)/2,y-14,label,19,anchor='middle',role='label',owner=key)
        text(lines,(x1+x2)/2,y+26,f'{scene["actors"][sender]} → {scene["actors"][receiver]}',14,'#6b7280',anchor='middle',role='label',owner=key)
    text(lines,40,height-57,'例外处理：'+scene['exception'],15,'#6b7280',role='reserved')
    text(lines,40,height-25,'蓝色实线＝岗位交接；灰色虚线＝岗位时间线。A／B均为演示客户，图中没有实际执行状态。',14,'#6b7280',role='reserved')
    lines.append('</svg>')
    return '\n'.join(lines)


def overview():
    lines=svg_start(1240,820,'三条流程怎样接成一个闭环','LTC发现需求，IPD建设共性能力，ITR解决投产问题并回流改进或增购。')
    text(lines,40,50,'先记住三个结果：卖得清楚、做成标准、用得稳定',30)
    text(lines,40,85,'两套既有体系继续承载记录；三条流程规定岗位如何推进和交接。',18,'#6b7280')
    box(lines,80,180,320,160,'ltc','#eff6ff');box(lines,840,180,320,160,'ipd','#f0fdf4');box(lines,460,560,320,160,'itr','#fff7ed')
    for x,y,title,sub in [(240,220,'LTC｜线索到回款','销售、商务、交付、财务'),(1000,220,'IPD｜产品开发','产品、研发、测试、推广'),(620,600,'ITR｜问题到解决','售后、研发、交付、客户')]:
        text(lines,x,y,title,26,anchor='middle');text(lines,x,y+36,sub,18,'#6b7280',anchor='middle')
    text(lines,240,305,'范围、交付、验收、到账',18,anchor='middle');text(lines,1000,305,'共性能力、版本、客户覆盖',18,anchor='middle');text(lines,620,685,'恢复、验证、确认、改进',18,anchor='middle')
    edge(lines,'e1',[(400,230),(840,230)],'ltc','ipd');text(lines,620,212,'客户共性需求',19,anchor='middle',role='label',owner='e1')
    edge(lines,'e2',[(840,295),(400,295)],'ipd','ltc');text(lines,620,277,'发布能力供销售交付',19,anchor='middle',role='label',owner='e2')
    edge(lines,'e3',[(240,340),(240,640),(460,640)],'ltc','itr');text(lines,330,622,'投产使用问题',18,anchor='middle',role='label',owner='e3')
    edge(lines,'e4',[(780,640),(1000,640),(1000,340)],'itr','ipd');text(lines,1020,480,'共性修复／改进',18,role='label',owner='e4')
    edge(lines,'e5',[(460,680),(120,680),(120,340)],'itr','ltc');text(lines,295,709,'新增付费范围回LTC',17,anchor='middle',role='label',owner='e5')
    text(lines,40,790,'读图提示：蓝色箭头表示业务交接；产品发布、客户实际部署、客户验收分别保留记录。',17,'#6b7280',role='reserved')
    lines.append('</svg>');return '\n'.join(lines)


files={'00-overview':overview()}
files.update({s['id']:sequence(s) for s in scenarios})
geometry=[]
for name,svg in files.items():
    p=ART/(name+'.svg');p.write_text(svg)
    ET.parse(p)
    result=subprocess.run([sys.executable,str(SKILL/'scripts/fireworks.py'),'check',str(p),'--check','xml','--check','markers','--check','collisions','--check','geometry','--check','composition'],capture_output=True,text=True)
    audit=json.loads(result.stdout)
    geometry.append({'file':p.name,**audit})
    if not audit['ok']:
        (EVIDENCE/'v11-diagram-validation.json').write_text(json.dumps(geometry,ensure_ascii=False,indent=2))
        raise SystemExit(json.dumps(audit,ensure_ascii=False))
    subprocess.run(['/opt/homebrew/bin/rsvg-convert','-w','2200',str(p),'-o',str(ART/(name+'.png'))],check=True)
(EVIDENCE/'v11-diagram-validation.json').write_text(json.dumps(geometry,ensure_ascii=False,indent=2)+'\n')
(OUT/'scenarios.json').write_text(json.dumps(scenarios,ensure_ascii=False,indent=2)+'\n')

md=['# 从需求到客户使用：IPD／LTC／ITR 渐进讲解',
    '', 'V1.2 岗位修订与工具承载版｜2026-10-09｜基于已读取的两套原体系和六项制度。',
    '', '本文按“先看全局，再看场景，接着看每一步协同，最后查规则”展开。五个场景是同一个业务背景的不同路径，A／B均为演示客户，不是实际订单、报价或升级记录。',
    '', '## 第一层：先知道三条流程分别交出什么',
    '', 'LTC把客户诉求变成明确的范围、方案报价、合同交付、验收与回款。IPD把共性缺口变成可复用产品能力和正式版本。ITR把投产问题从受理推进到恢复、解决、验证、客户确认与改进。',
    '', '客户诉求从LTC进入；已有能力可以配置交付，共性缺口进入IPD，专属定制受LTC变更控制。产品发布后，原客户和其他客户分别判断升级与验收；投产问题由ITR持续负责。',
    '', '![三流程总体关系](images/00-overview.png)',
    '', '## 第二层：用一个业务背景，认出五条常见路径',
    '', 'A提出“多食堂分别对账、总部汇总”。先判断产品是否已支持；若已支持，走标准报价与交付；若缺少通用能力，走IPD；若另需专有财务接口，走定制变更。标准版本发布后，再检查B是否适用并升级。投产发现约定能力异常，则进入ITR。',
    '', '角色用岗位名称表示，同一人可以兼岗；每次交接仍要明确发起人、接收人和结果责任。项目负责人默认由交付承担；IPD客户协同接口为销售与交付，不设售后参与岗位，产品、研发、测试继续履行专业职责。图中合并岗位用于便读，交付负责人承担统筹，交付执行承担现场动作，两者可由同一人兼任。正式人员、收费和上线窗口继续按现授权确认。',
    '', '## 第三层：跟着图看每个岗位怎样接力',
    '', '图片从上向下阅读：每列是岗位，每条蓝线是一项交接。图下同号步骤解释工作分工、交付物、接收标准与记录位置。每个接收人都应确认已收到且材料可用；否则退回补齐，原牵头人继续跟进。']
for n,s in enumerate(scenarios,1):
    md += ['',f'### 场景{n}：{s["title"]}', '',f'**什么情况下进入：** {s["trigger"]}', '',f'**完成后得到：** {s["outcome"]}', '',f'**谁对结果负责：** {s["owner"]}', '',f'![{s["title"]}](images/{s["id"]}.png)', '',f'[可编辑SVG](images/{s["id"]}.svg) · 对应 {s["uc"]} · 规则依据：{s["source"]}', '',s['rule'], '', '| 步骤 | 谁交给谁 | 各岗位如何配合 | 交付物 | 接收通过条件 | 结果放哪里 |', '| --- | --- | --- | --- | --- | --- |']
    for i,(a,b,label,split,artifact,condition,record) in enumerate(s['steps'],1):
        md.append(f'| {i:02} {label} | {s["actors"][a]} → {s["actors"][b]} | {split} | {artifact} | {condition} | {record} |')
    md += ['', '**退回或异常怎么协同：** '+s['exception']]
md += ['', '## 第四层：用什么系统约束这些交接',
       '', '员工仍从企业微信进入，后台可以另选。企业微信可做统一入口、轻量工单和协同看板；是否足以承载完整三流程，要验证缺材料或未审批时能否阻止流转，以及需求、报价、版本、客户实例与工单能否关联。',
       '', '建议保留云效研发与Odoo报价分工，优先用已试装NocoBase验证流程主账；希望主要靠配置完成时，比较简道云。专业ITR需要时再评估Frappe Helpdesk。没有负责人不交接、未批准不正式报价、B未回验不标同步、工单未验证不关闭，是选型验收条件。',
       '', '[系统分工、企微可行性、软件比较与十项约束规则](tool-selection.md)。当前只是选型建议，未购买、配置或上线。',
       '', '## 第五层：回到已有体系，把交接结果留下来',
       '', '研发侧沿用需求池、UC与云效执行、Codeup评审、测试、正式版本及资料目录；项目侧沿用售前采集、报价快照、合同范围、项目总表、部署验收、售后台账和回款节点。每个交接先用原有稳定编号和链接串起来，不另抄一套当前状态。',
       '', '交接至少留下：同一业务编号、来源与已批准范围、交付物版本、发起人与接收人、接收结论、下一动作与时间、验收证据。接收者回复“已收到”还不够，必须能据此开始下一步；缺项要说明由谁补、何时复核。',
       '', '原“售后状态＝在保／过保”只表示保修权益；工单处理进度另外记录。产品发布、A上线、B适用性判断、B实际升级和客户验收是不同结果，分别有证据。',
       '', '## 第六层：需要时再查完整规则与执行模板',
       '', '- [完整运行规范与RACI](framework.md)：范围、报价、版本、权限、异常和岗位责任。',
       '- [两套原体系融合映射](integration-map.md)：六制度去向、原字段语义与待处理冲突。',
       '- [执行表与空白模板](IPD-LTC-ITR执行表.xlsx)：客户需求、跨客户同步、ITR登记。',
       '- [六个Use Case及验收设计](use-cases.md)：主流程、分支、异常与恢复。',
       '- [V1.0原评审页](revisions/v1.0/review.html)：保留上一版表达供回看。',
       '', '本次改版调整讲解和协同示例，没有改变收费、审批或生产操作授权，也未配置在线流程。下一步仍是确认真实负责人、权益与试点，用一个需求、一个版本、两个客户和一个工单验证协同。']
(OUT/'guide.md').write_text('\n'.join(md)+'\n')


def esc(value):
    return html.escape(value,quote=True)


def inline_image(name,alt):
    png=base64.b64encode((ART/(name+'.png')).read_bytes()).decode()
    return f'<button class="image-button" type="button" data-image="{name}" aria-label="放大查看：{esc(alt)}"><img src="data:image/png;base64,{png}" alt="{esc(alt)}" loading="lazy"/><span>点击放大 · 可下载PNG或SVG</span></button>'


scene_html=[]
for n,s in enumerate(scenarios,1):
    cards=[]
    for i,(a,b,label,split,artifact,condition,record) in enumerate(s['steps']):
        cards.append(f'<article class="handoff" data-step="{i}" {"hidden" if i else ""}><p class="eyebrow">第{i+1:02}步／共{len(s["steps"])}步</p><h4>{esc(label)}</h4><p class="people">{esc(s["actors"][a])} <span>→</span> {esc(s["actors"][b])}</p><p>{esc(split)}</p><dl><dt>交什么</dt><dd>{esc(artifact)}</dd><dt>怎样才算接住</dt><dd>{esc(condition)}</dd><dt>结果放哪里</dt><dd>{esc(record)}</dd></dl></article>')
    scene_html.append(f'''<details class="scenario" id="scene-{n}" data-scene="{n}"><summary><span class="number">0{n}</span><span class="summary-text"><span class="tag">{esc(s['lane'])}</span><strong>{esc(s['title'])}</strong><span class="summary-outcome">{esc(s['outcome'])}</span></span><span class="expand-hint">展开</span></summary><div class="scene-body"><p><b>从这件事开始：</b>{esc(s['trigger'])}</p><p><b>结果责任：</b>{esc(s['owner'])}</p><div class="rule">{esc(s['rule'])}</div><h3>先看岗位之间怎样交接</h3>{inline_image(s['id'],s['title'])}<p class="downloads"><a href="images/{s['id']}.png" download>下载PNG</a> · <a href="images/{s['id']}.svg" download>下载可编辑SVG</a></p><h3>再按步骤读，弄清楚每个人做什么</h3><div class="step-view">{''.join(cards)}<div class="step-controls"><button type="button" data-prev disabled>上一步</button><p class="step-count" aria-live="polite">1／{len(cards)}</p><button type="button" data-next>下一步</button><button type="button" data-all aria-pressed="false">展开全部步骤</button></div></div><p class="exception"><b>退回或异常：</b>{esc(s['exception'])}</p><p class="reference">规则依据：{esc(s['source'])} · {esc(s['uc'])}</p></div></details>''')

style='''
:root{--bg:#f5f7fa;--surface:#fff;--ink:#193b57;--muted:#526477;--line:#dce4eb;--accent:#237f77;--soft:#e9f4f1;--radius:16px;--warn:#8a5a12;--warn-bg:#fff5e4;--font:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}
*{box-sizing:border-box}body.webpage{margin:0;background:var(--bg);color:var(--ink);font:16px/1.8 var(--font)}.page{max-width:1080px;margin:auto;padding:44px 28px 64px}section{margin:0 0 36px}h1{font-size:clamp(32px,4.5vw,50px);line-height:1.25;margin:12px 0 18px;letter-spacing:-.03em}h2{font-size:26px;line-height:1.4;margin:0 0 14px}h3{font-size:20px;margin:28px 0 12px}h4{font-size:22px;line-height:1.4;margin:6px 0 14px}p{margin:8px 0 14px}.kicker,.eyebrow{font-size:13px;color:var(--accent);font-weight:600;letter-spacing:.08em}.lede{font-size:18px;max-width:48em;color:var(--muted)}.meta,.note,.reference{font-size:14px;color:var(--muted)}a{color:var(--accent);text-underline-offset:3px}.reading-order,.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}.reading-order{margin:22px 0}.reading-order a,.card{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:18px;text-decoration:none}.reading-order b,.reading-order span{display:block}.reading-order span,.card p{font-size:14px;color:var(--muted)}.card h3{margin:0 0 10px}.banner,.rule{padding:17px 20px;background:var(--soft);border-left:3px solid var(--accent);border-radius:10px}.image-button{display:block;width:100%;padding:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--surface);cursor:zoom-in}.image-button img{display:block;width:100%;height:auto}.image-button span{display:block;padding:8px;font:13px var(--font);color:var(--muted);background:var(--bg)}.scenario{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);margin:16px 0;scroll-margin-top:18px}.scenario>summary{cursor:pointer;display:flex;gap:16px;align-items:center;padding:20px;list-style:none}.scenario>summary::-webkit-details-marker{display:none}.number{font-size:26px;color:var(--accent);font-weight:600;min-width:40px}.summary-text{flex:1;min-width:0}.summary-text strong,.summary-outcome{display:block}.summary-text strong{font-size:20px;line-height:1.5;margin:4px 0}.summary-outcome{font-size:14px;color:var(--muted)}.tag{display:inline-block;font-size:12px;color:var(--accent);background:var(--soft);padding:2px 9px;border-radius:6px}.expand-hint{font-size:13px;color:var(--muted)}details[open]>.summary-text{border-bottom:1px solid var(--line)}.scene-body{border-top:1px solid var(--line);padding:24px}.downloads{font-size:14px;margin:10px 0 20px}.handoff{padding:22px;border:1px solid var(--line);background:var(--bg);border-radius:12px;margin:14px 0}.handoff[hidden]{display:none}.people{font-size:18px;font-weight:600;color:var(--accent)}.people span{padding:0 12px}dl{display:grid;grid-template-columns:120px 1fr;gap:8px 18px;margin:18px 0 0;font-size:15px}dt{font-weight:600}dd{margin:0;color:var(--muted)}.step-controls{display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:16px}.step-controls button,.dialog-toolbar button{border:1px solid var(--line);background:var(--surface);color:var(--ink);padding:9px 14px;border-radius:9px;font:15px var(--font);cursor:pointer}.step-controls button:disabled{opacity:.45;cursor:default}.step-count{margin:0 6px;font-size:14px;color:var(--muted)}.exception{padding:14px 17px;background:var(--warn-bg);color:var(--warn);font-size:14px;border-radius:10px;margin-top:22px}.references{border:1px solid var(--line);padding:20px;border-radius:var(--radius);background:var(--surface)}.references summary{cursor:pointer;font-weight:600}.references li{padding:4px 0}.footer{color:var(--muted);font-size:13px;margin-top:32px}button:focus-visible,a:focus-visible,summary:focus-visible{outline:3px solid var(--accent);outline-offset:3px}dialog{width:min(1300px,95vw);max-width:95vw;max-height:94vh;padding:18px;border:1px solid var(--line);border-radius:var(--radius);color:var(--ink);background:var(--surface)}dialog::backdrop{background:#10263bbf}.dialog-toolbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;justify-content:space-between;margin-bottom:12px}.dialog-toolbar h2{font-size:18px;margin:0;flex:1;min-width:120px}.image-scroll{max-height:75vh;overflow:auto;background:white;border:1px solid var(--line)}.image-scroll img{display:block;width:100%;max-width:none;height:auto}.diagram-links{font-size:14px}.diagram-links a{margin-right:12px}@media(max-width:700px){.page{padding:26px 18px 44px}.cards,.reading-order{grid-template-columns:1fr}.reading-order{gap:8px}.reading-order a{padding:12px 16px}h2{font-size:23px}.scenario>summary{padding:16px;gap:9px}.summary-text strong{font-size:18px}.summary-outcome{font-size:13px}.expand-hint{display:none}.scene-body{padding:16px}.handoff{padding:17px}dl{grid-template-columns:1fr;gap:2px}dd{margin-bottom:10px}.number{font-size:22px;min-width:30px}.people{font-size:16px}.people span{padding:0 5px}.step-controls button{padding:8px 11px}.banner,.rule{padding:14px}.dialog-toolbar button{padding:7px 10px}}@media print{.scenario{break-before:page}.scenario .scene-body{display:block}.handoff[hidden]{display:block}.step-controls,.image-button span,.reading-order{display:none}dialog{display:none}}
'''
body=f'''<div class="page"><section class="hero"><p class="kicker">康比特数字业务 · 图解协同</p><h1>从一个客户需求开始，<br>看清大家怎样一起完成</h1><p class="lede">先理解三条流程，再选一个熟悉的场景，最后沿着交接一步步看：谁先做、交给谁、交什么、怎样才算接住。</p><p class="meta">V1.1 图解协同版 · 2026年10月9日 · 原体系已核源</p><nav class="reading-order" aria-label="阅读层次"><a href="#overview"><b>01 先看全局</b><span>三条流程分别交出什么结果</span></a><a href="#scenarios"><b>02 选一个场景</b><span>五个常见业务流程，按需展开</span></a><a href="#references"><b>03 再查规则</b><span>原体系、岗位责任和执行模板</span></a></nav></section>
<section id="overview"><p class="eyebrow">第一层 · 先明白分工</p><h2>卖得清楚，做成标准，用得稳定</h2><div class="cards"><article class="card"><h3>LTC：兑现客户承诺</h3><p>销售收需求，商务管报价合同，交付完成现场，财务确认到账。</p></article><article class="card"><h3>IPD：建设共性能力</h3><p>产品决定范围与优先级，研发实现，测试验证，发布后供各客户使用。</p></article><article class="card"><h3>ITR：解决使用问题</h3><p>售后牵头受理与闭环，交付、研发和供方处理，客户验证结果。</p></article></div>{inline_image('00-overview','三条流程总体关系')}<p class="note">产品发布、客户实际部署、客户验收是三个结果；微信登录与Dify承担支撑作用。</p></section>
<section id="scenarios"><p class="eyebrow">第二层 · 从熟悉的场景进入</p><h2>A客户想要“多食堂对账、总部汇总”</h2><p>现有能力能满足，就走报价与交付；通用能力有缺口，就进入产品开发；客户还要专有接口，就走定制变更。发布后看B如何升级，投产后看问题如何闭环。</p><div class="banner">下面是同一业务背景的五条演示路径。展开一个场景，先看岗位交接图，再逐步阅读协同细节。</div>{''.join(scene_html)}</section>
<section id="references"><p class="eyebrow">第三层 · 需要时再查完整规则</p><h2>交接有记录，后面的人才能继续</h2><p>每次交接留下同一业务编号、批准范围、交付物版本、发起人和接收人、接收结论、下一动作与时间。材料不足就退回补齐，原牵头人继续跟进。</p><div class="cards"><article class="card"><h3>研发侧继续用原体系</h3><p>需求池 → UC与云效 → 研发评审与测试 → 正式版本 → 能力及资料目录。</p></article><article class="card"><h3>项目侧继续用原体系</h3><p>售前采集 → 报价合同 → 项目交付 → 部署验收 → 售后台账与回款。</p></article><article class="card"><h3>A与B分别留结果</h3><p>A上线后，B仍需核适用、权益、兼容和窗口；升级或暂缓都留依据。</p></article></div><details class="references"><summary>展开原体系、岗位分工和执行文件</summary><ul><li><a href="guide.md">渐进讲解文档：五场景完整步骤</a></li><li><a href="framework.md">完整运行规范与14项岗位责任</a></li><li><a href="integration-map.md">六项原制度与两套体系融合</a></li><li><a href="IPD-LTC-ITR执行表.xlsx">执行表与三个空白登记模板</a></li><li><a href="use-cases.md">六个Use Case与验收设计</a></li><li><a href="revisions/v1.0/review.html">回看V1.0原评审页</a></li></ul></details><p class="note">本次更新讲解方式和协同示例。岗位用角色表示；具体人员、收费、权限与试点由相应负责人确认。未修改在线流程或实施客户升级。</p></section><footer class="footer">图文同源，共6张协同图片，含5个流程、43个交接步骤。A／B均为演示客户。主图片内嵌，可离线阅读；PNG与SVG保留下载入口。</footer></div>
<dialog id="image-dialog" aria-labelledby="image-title"><div class="dialog-toolbar"><h2 id="image-title">流程图</h2><button type="button" id="zoom-out">缩小</button><button type="button" id="zoom-in">放大</button><button type="button" id="close-image">关闭</button></div><div class="image-scroll"><img id="large-image" alt=""/></div><p class="diagram-links"><a id="download-png" download>下载PNG</a><a id="download-svg" download>下载SVG</a></p></dialog>'''
script='''<script>
document.querySelectorAll('.scenario').forEach(scene=>{let current=0;const cards=[...scene.querySelectorAll('.handoff')],prev=scene.querySelector('[data-prev]'),next=scene.querySelector('[data-next]'),all=scene.querySelector('[data-all]'),count=scene.querySelector('.step-count');let showAll=false;function draw(){cards.forEach((c,i)=>c.hidden=!showAll&&i!==current);prev.disabled=showAll||current===0;next.disabled=showAll||current===cards.length-1;count.textContent=showAll?'全部'+cards.length+'步':(current+1)+'／'+cards.length;all.setAttribute('aria-pressed',String(showAll));all.textContent=showAll?'恢复逐步阅读':'展开全部步骤';}prev.addEventListener('click',()=>{current=Math.max(0,current-1);draw()});next.addEventListener('click',()=>{current=Math.min(cards.length-1,current+1);draw()});all.addEventListener('click',()=>{showAll=!showAll;draw()});scene.addEventListener('toggle',()=>scene.querySelector('.expand-hint').textContent=scene.open?'收起':'展开');draw();});
const dialog=document.getElementById('image-dialog'),large=document.getElementById('large-image');let zoom=100,trigger;
document.querySelectorAll('[data-image]').forEach(button=>button.addEventListener('click',()=>{trigger=button;zoom=100;large.style.width='100%';const source=button.querySelector('img');large.src=source.src;large.alt=source.alt;document.getElementById('image-title').textContent=source.alt;document.getElementById('download-png').href='images/'+button.dataset.image+'.png';document.getElementById('download-svg').href='images/'+button.dataset.image+'.svg';dialog.showModal();document.getElementById('close-image').focus();}));
document.getElementById('zoom-in').addEventListener('click',()=>{zoom=Math.min(400,zoom+50);large.style.width=zoom+'%'});document.getElementById('zoom-out').addEventListener('click',()=>{zoom=Math.max(100,zoom-50);large.style.width=zoom+'%'});document.getElementById('close-image').addEventListener('click',()=>dialog.close());dialog.addEventListener('close',()=>trigger&&trigger.focus());
</script>'''
systems_html='''<section id="systems"><p class="eyebrow">第三层 · 用系统约束协同</p><h2>入口仍是企业微信，关键条件由流程系统把关</h2><div class="banner">企微可以做统一入口和轻量工单。完整三流程要进一步验证：缺负责人、缺审批、缺客户回验时，系统能否阻止继续。</div><div class="cards"><article class="card"><h3>员工从企业微信进入</h3><p>工作台入口、待办、消息与原表看板；登录、业务权限和消息回读分别验证。</p></article><article class="card"><h3>保留云效与Odoo</h3><p>云效／Codeup仍管研发执行；现有Odoo管配置报价与快照，不重复抄研发进度。</p></article><article class="card"><h3>补一个流程主账</h3><p>优先验证已有NocoBase底座，关联需求、范围、工单、客户实例与升级决定；不是已上线。</p></article></div><h3>先验四件事，再谈软件是否合适</h3><ul><li>评审没过，不能转开发。</li><li>报价改范围，必须新版本重新批准。</li><li>A已上线，B没回验，不能标B已同步。</li><li>工单只写修好了，没有现场与客户确认，不能关闭。</li></ul><details class="references"><summary>如果企微工单不足，其他软件怎么选</summary><p><b>NocoBase：</b>结合已有本地底座优先做主账试点，业务模型和规则需要建设；独立审批插件的版本与授权另核。</p><p><b>简道云：</b>商业低代码路线，适合希望主要靠配置完成；有节点条件与限时处理，仍须验证API、权限和复杂关联。</p><p><b>Frappe Helpdesk：</b>适合专业ITR服务台；企微、产品版本及报价关联需要接入，不承担完整三流程。</p><p><b>Odoo扩展：</b>若统一商业主账再评估Helpdesk，当前报价已上线不代表工单模块已安装或授权。</p><p><a href="tool-selection.md">完整选型建议、官方依据与十项约束</a></p></details><p class="note">岗位修订：IPD客户接口为销售和交付；项目负责人默认由交付承担。后台可另选，企业微信入口已确认。尚未购买、配置或部署新系统。</p></section>'''
body=body.replace('<section id="references">',systems_html+'<section id="references">').replace('V1.1 图解协同版','V1.2 岗位修订与工具承载版').replace('<a href="#references"><b>03 再查规则</b>','<a href="#systems"><b>03 工具与规则</b>').replace('第三层 · 需要时再查完整规则','第四层 · 需要时再查完整规则')
template=(PAKCO/'templates/webpage.html').read_text()
head=template.split('<head>',1)[0]+'<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>IPD／LTC／ITR · 渐进讲解与岗位协同</title><style>'+style+'</style></head>'
(OUT/'review.html').write_text(head+'<body class="webpage">'+body+script+'</body></html>')
assert sum(len(s['steps']) for s in scenarios)==43
report={'version':'1.2','scenario_count':len(scenarios),'handoff_steps':43,'diagrams':len(files),'image_format':['SVG','PNG'],'visual_review':'pending','files':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in ART.iterdir() if p.is_file()},'html_bytes':(OUT/'review.html').stat().st_size}
(EVIDENCE/'v11-artifact-validation.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n')
print(json.dumps({k:v for k,v in report.items() if k!='files'},ensure_ascii=False))
