"""Render a frozen, reviewable activity-proxy report. No human-hours inference."""
from pathlib import Path
import json, html, hashlib, collections
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Font, PatternFill, Alignment

ROOT = Path(__file__).resolve().parent
REPO = ROOT.parents[1]
rows = json.loads((ROOT / 'source-tasks.json').read_text())
groups = json.loads((ROOT / 'group-map.json').read_text())
history = json.loads((ROOT / 'history-evidence.json').read_text())
by_id = {r['task_id']: r for r in rows}
META = {
'S': ('产品标准化与客户方案适配', 'SKU、配置、报价和客户定开边界持续梳理；学校、医院、社区等场景仍需逐项对照。', 'SKU体系、医院适配、校园与社区方案'),
'N': ('营养健康功能与体验落地', '把营养建议、食动日历、体重目标和拍照记录做成可执行、可验收的产品流程。', '食动日历、安庆菜谱评价、饮食拍照'),
'A': ('AI协作工具与知识复用', '持续搭建和调整 Skill、PRD、工具链、知识入口与协作方式，形成团队可复用能力。', 'Ruflo、Use Case、知识精简、跨部门模板'),
'O': ('环境部署与版本一致性', '本地、云端、客户分支、SQL和运行配置需要反复核对，部署与迁移仍依赖项目化处理。', '哈尔滨部署、食安部署、科训迁移'),
'M': ('项目协同与进度可视化', '反复汇集进度、责任人、周月报和下一步动作，补齐分散且缺失的项目管理信息。', '交付驾驶舱、周月报、企微任务催办'),
'P': ('消费结算与补贴账务规则', '补单、消费限制、余额链、退款状态和核销规则需按场景调整，并保持原资金链路一致。', '国信补单、账户流水、补贴与退款'),
'I': ('第三方接口与设备数据贯通', '设备协议、身份关联、菜品映射和历史兼容需要逐项联调；单个主题常涉及多个系统。', '上禾、天厨、一卡通、InBody'),
'F': ('现场故障与运行稳定性', '漏票、卡顿、扫码误判、断连和外部服务超时持续占用排查、修复与复测精力。', '金斯瑞小票、江西206、410扫码、智盘超时'),
'U': ('培训、手册与交付物料', '演示账号、培训和操作手册的制作与维护。', '产品大学、出入库秤资料、进销存手册'),
'D': ('基础数据与业务页面', '分类、统计口径、演示数据和订单页面的完善。', '菜品分类、活跃人次、订单详情'),
'T': ('独立回归与上线审查', '未归入具体业务主题的独立测试和发布核查。', '国信回归、滨州上线审查'),
'C': ('安全整改与等保资料', '漏洞清理、网安整改和等保资料整理。', '国信网安、新吴区残留清理'),
}
observed_days = collections.defaultdict(set)
for event in history:
    observed_days[event['task_id']].add(event['date'])
for group in groups:
    assert set(group['days']) == set().union(*(observed_days[t] for t in group['task_ids']))
flat = [t for g in groups for t in g['task_ids']]
assert len(flat) == len(set(flat)) == len(rows) == 200
assert set(flat) == set(by_id)
stats = []
for cat, (name, desc, examples) in META.items():
 gs = [g for g in groups if g['category'] == cat]
 stats.append(dict(category=cat,name=name,desc=desc,examples=examples,groups=len(gs),tasks=sum(len(g['task_ids']) for g in gs),days=sum(len(g['days']) for g in gs)))
stats.sort(key=lambda s: (-s['days'], -s['groups'], s['category']))
for n,s in enumerate(stats,1): s['rank']=n
assert sum(s['tasks'] for s in stats)==200
assert sum(s['groups'] for s in stats)==125
assert sum(s['days'] for s in stats)==185
assert [s['category'] for s in stats[:8]] == list('SNAOMPIF')
missing = [g for g in groups if not g['days']]
summary = '当前协作记录中的持续投入，首先集中在产品标准化与客户方案适配，其次是营养健康功能落地和 AI 协作能力建设。环境部署、项目协调、交易规则、系统集成与现场故障也构成了持续负担。它提示我们：下一阶段值得优先减少的，是同一方案、接口、环境和交付信息在不同项目中的重复确认；但哪些环节真正消耗最多人时，还需要统一工时记录验证。'
method = [
'范围：当前 zhctprompt 团队共享仓库。时间窗：2026-08-20 至 2026-09-18，共30个自然日。用户未指定时间窗，本报告取最近30天作为“当前”快照，不是全历史排名。',
'基线：67c33a686c364735c9c3f555f70e650752b72c85；全库718条唯一任务索引，按 updated_at 日期筛出200条。最新更新不在窗口内的任务及未登记任务不计入；因此不代表团队全部工作。',
'归并：逐条核对200个任务标题与证据入口，合并为125个问题/工作主题，归入12类；重点主题抽读原始说明。分类与合并属于分析判断，可在明细中逐项调整。',
'去重：同一业务目标的方案、父任务、开发子任务、诊断与复测合为一组。主题以独立业务结果为单位，不以文件数计数；一个主题只计入一个主类，跨类内容不重复计数。',
'主指标“问题记录日”：取该主题关联任务索引文件在窗口内的 Git 提交日期并集；同一主题同一天多次提交或多个子任务只计1日，再按类求和。使用作者日期，不统计文件行数、关键词次数或提交次数。',
'排序：问题记录日降序；同值按问题组数降序，仅用于展示。第4/5名同为20日、第6/7名同为17日，真实投入不分高低。',
'总计185个问题记录日。这是多个问题跨日期的记录量，可以超过30；绝不是185个人天。补录、集中归档和文档维护会影响该指标，不能把记录日期当作实际连续工作日期。',
'124/125主题有可匹配Git记录；“本地模型运行能力”缺匹配记录，主指标计0但保留在主题数和任务数中，不补造时间。',
'对话采集登记仅能证明姜阳本机覆盖；没有全员统一人工工时。少量Ruflo任务周期或Codex回合耗时，不能直接和人工工时相加，也不能由单项目外推全团队。',
'类别是工作问题的聚合，不表示所有投入都是返工或浪费；AI方法建设、营养产品研发可能形成长期资产。安全类留痕少也不代表风险低或不重要。',
'证据等级：任务索引与Git存在性为A；说明与复盘为B；问题合并、成因与管理建议为D。未做个人绩效排名。',
]
md = '# 当前协作空间持续投入的八类问题\n\n> **这是持续处理记录的代理排序，不是全员工时榜。** 当前没有足够证据回答精确的“大家花时间最多”。\n\n'
md += '统计日期：2026-09-18。范围为 zhctprompt 当前共享任务索引，最近30天。\n\n'
md += '| 顺序 | 问题类别 | 问题记录日 | 去重主题 | 关联任务 |\n|---:|---|---:|---:|---:|\n'
for s in stats[:8]: md += f"| {s['rank']} | {s['name']} | {s['days']} | {s['groups']} | {s['tasks']} |\n"
md += '\n第4/5名及第6/7名主指标并列，顺序仅由主题数打破；不能认定真实耗时有差别。\n\n## 可以直接用于汇报的结论\n\n'+summary+'\n\n'
md += '## 八类问题的具体表现\n\n'
for s in stats[:8]: md += f"{s['rank']}. **{s['name']}**：{s['desc']} 代表主题：{s['examples']}。\n\n"
md += '## 从“记录频度”到“耗时”的校验\n\n如果仅按去重主题数量排序，顺序为：产品标准化（18，与AI同量、以记录日破同值）、AI协作（18）、环境部署（16）、项目管理（14）、现场稳定性（13）、交易规则（12）、系统集成（8）、营养功能（7，与培训资料同量、以记录日破同值）。前八类别一致，但位置变化较大，因此当前证据不足以给出稳定的真实工时名次。\n\n'
md += '营养功能类7个主题累计24个记录日，体现少量主题的持续打磨。系统集成只有8个主题，但哈尔滨复盘里仅天厨接入一条主线就有16.77小时Codex活跃耗时；该项目515个去重回合共38.48小时，明确不包含线下会议、等待、现场人员操作和完整人工投入。此例说明：不能用任务少推断耗时少，也不能用该项目时长替换全团队统计。\n\n'
md += '## 方法与边界\n\n'+'\n\n'.join(f'{i+1}. {v}' for i,v in enumerate(method))+'\n\n'
md += '## 其余四类（保留以证明没有强行只分八类）\n\n'
for s in stats[8:]: md += f"- {s['name']}：{s['days']}个记录日 / {s['groups']}个主题 / {s['tasks']}项任务。\n"
md += '\n## 来源与交付物\n\n- [任务索引](../../control/task-index/tasks.tsv)\n- [采集范围登记](../team-learning/codex-conversation-archive/collectors.csv)\n- [哈尔滨时间复盘](../../work_haerbin_shanzhishe/retrospectives/2026-09-16-project-retrospective.md)\n- [结论网页](index.html)\n- [逐任务来源快照](source-tasks.json)\n- [逐主题分类与合并表](group-map.json)\n- [提交证据](history-evidence.json)\n- Excel与PNG仅存本地 artifacts/；本业务分析不公开发布。\n\n## 下一步采集建议\n\n如要升级为真实工时榜，每位负责人按统一问题ID记录实际投入分钟、工作类型、参与人、开始/结束和验收结果；客户/厂家等待时间单列。持续记录至少一个完整工作周期后，按人员实际投入求和并另列AI活跃耗时。建议尚未执行，不假称工时系统已经建立。\n'
(ROOT/'summary.md').write_text(md)
(ROOT/'stats.json').write_text(json.dumps(stats,ensure_ascii=False,indent=2))
# Self-contained webpage derived from pakco-html webpage scaffold and KPI grid.
e=html.escape
cards=''.join(f'<article class="card"><span class="rank">{s["rank"]:02d}</span><div><h2>{e(s["name"])}</h2><p class="metric"><b>{s["days"]}</b> 问题记录日 · {s["groups"]} 个主题</p><p>{e(s["desc"])}</p></div></article>' for s in stats[:8])
details=''.join(f'<details><summary>{s["rank"]:02d} · {e(s["name"])} <span>{s["tasks"]}项任务</span></summary><ul>'+''.join(f'<li><strong>{e(g["name"])}</strong> · {len(g["days"])}个记录日<br><small>{e("、".join(g["task_ids"]))}</small></li>' for g in groups if g['category']==s['category'])+'</ul></details>' for s in stats)
page='''<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>协作空间持续投入的八类问题</title><style>
:root{--bg:#f3f0e8;--surface:#fffdfa;--text-1:#17251f;--text-2:#63716a;--accent:#c88428;--border:#d8d1c4;--radius:16px;--font-sans:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif}*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text-1);font-family:var(--font-sans);line-height:1.65}.page{max-width:1500px;margin:auto;padding:36px 48px}.hero{display:grid;grid-template-columns:165px 1fr;gap:16px}.kicker{font-size:14px;white-space:nowrap;color:var(--accent);font-weight:700;letter-spacing:2px;margin:7px 0}.h1{font-size:clamp(28px,3.4vw,49px);line-height:1.3;margin:0 0 12px;letter-spacing:-1px}.lede{margin:0;color:var(--text-2);font-size:16px}.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:28px}.card{display:flex;align-items:flex-start;gap:16px;padding:23px 19px;border:1px solid var(--border);background:var(--surface);border-radius:var(--radius)}.rank{font-size:40px;line-height:1.2;color:var(--accent);font-family:Georgia,serif;font-weight:bold}.card h2{font-size:18px;line-height:1.4;margin:0 0 10px}.card p{font-size:14px;color:var(--text-2);margin:0}.card p.metric{margin-bottom:10px;font-size:13px}.metric b{font-size:19px;color:var(--text-1)}.footnote{color:var(--text-2);font-size:13px;margin:16px 0}.conclusion{border-top:1px solid var(--border);padding-top:19px;margin-top:19px}.conclusion p{font-size:20px;line-height:1.7;margin:0}.conclusion strong{color:var(--text-1)}.lower{overflow-wrap:anywhere;max-width:1050px;margin:42px auto 0}.lower h2{font-size:23px}.lower p,.lower li{color:var(--text-2);font-size:15px}.lower li{margin:12px 0}.warning{border-left:3px solid var(--accent);padding:12px 18px;background:var(--surface)}details{border-bottom:1px solid var(--border);padding:16px 0}summary{cursor:pointer;font-weight:600}summary span{float:right;color:var(--text-2);font-weight:400}small{overflow-wrap:anywhere}a{color:var(--accent)}footer{border-top:1px solid var(--border);margin-top:38px;padding-top:18px;color:var(--text-2);font-size:12px}button{padding:9px 15px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text-1);cursor:pointer}@media(max-width:1100px){.grid{grid-template-columns:repeat(2,1fr)}.hero{grid-template-columns:1fr}}@media(max-width:600px){.page{padding:24px 18px}.grid{grid-template-columns:1fr;gap:12px;margin-top:22px}.hero{gap:5px}.card{padding:20px}.rank{font-size:35px}.conclusion p{font-size:17px}.lede{font-size:14px}.lower{margin-top:28px}summary span{float:none;display:block}}@media print{.page{padding:20px}.lower,button{display:none}.grid{grid-template-columns:repeat(4,1fr)}.card{break-inside:avoid}.rank{font-size:30px}.card h2{font-size:15px}}
</style></head><body class="webpage"><main class="page"><section id="overview"><header class="hero"><p class="kicker">02 / 协作投入地图</p><div><h1 class="h1">当前持续投入的八类问题</h1><p class="lede">2026.08.20—09.18 · zhctprompt 团队共享记录<br>200 项近期任务 → 125 个去重主题；以下按问题记录日排序，<strong>不代表全员工时、问题严重度或返工浪费。</strong></p></div></header><div class="grid">CARDS</div><p class="footnote">口径：同一主题同一天更新任务索引，只计 1 个“问题记录日”。第 4/5 名、第 6/7 名主指标并列，以主题数确定展示顺序。数据缺全员工时，无法据此确认真实耗时名次。</p><section class="conclusion"><p><strong>持续投入最集中的是产品标准化、营养功能落地和 AI 协作能力建设。</strong><br>可优先减少跨项目对方案、接口、环境和交付信息的重复确认；实际节省多少时间，需要工时数据验证。</p></section></section><section class="lower"><h2>这份排序回答了什么</h2><p class="warning">它回答“哪些类别持续留下了最多处理记录”。它尚不能准确回答“全体成员在哪类问题上花了最多小时”。记录日既可能反映持续工作，也可能反映补录和归档习惯。</p><p>若按去重主题数排序，营养功能从第 2 位变为第 8 位，现场稳定性从第 8 位变为第 5 位。前八类保持一致，但名次对口径敏感。</p><p>哈尔滨项目复盘记录了 38.48 小时 Codex 活跃投入，其中天厨接入为 16.77 小时。这只说明少数集成主题可能很重，不能外推全团队人工工时。</p><h2>统计方法与覆盖边界</h2><ol>METHOD</ol><h2>展开查看全部 12 类与主题</h2>DETAILS<footer>统计基线 67c33a686 · 2026-09-18 · 供内部分析，待工时验证<br><a href="summary.md">阅读完整结论与来源</a> · <button onclick="window.print()">打印结论页</button><p>模板结构派生自 pakco-html Webpage + KPI Grid（MIT，lewis / pakco77）；采用用户示例的米白、橙色序号与八卡片排版。</p></footer></section></main></body></html>'''
page=page.replace('CARDS',cards).replace('METHOD',''.join('<li>'+e(m)+'</li>' for m in method)).replace('DETAILS',details)
(ROOT/'index.html').write_text(page)
art=ROOT/'artifacts';art.mkdir(exist_ok=True)
wb=Workbook();ws=wb.active;ws.title='排名与口径';ws.append(['顺序','问题类别','问题记录日（非人天）','去重主题数','关联任务数','典型问题'])
for s in stats:ws.append([s['rank'],s['name'],s['days'],s['groups'],s['tasks'],s['desc']])
ws.append([]);ws.append(['结论',summary]);ws.append([])
for i,m in enumerate(method,1):ws.append([f'口径{i}',m])
w=wb.create_sheet('125个去重主题');w.append(['主题ID','分类','主题名称','记录日数（非工时）','可追溯日期','任务数量','关联任务ID'])
for g in groups:w.append([g['group_id'],META[g['category']][0],g['name'],len(g['days']),', '.join(g['days']),len(g['task_ids']),'、'.join(g['task_ids'])])
w=wb.create_sheet('200项来源');w.append(['任务ID','标题','更新时间','主题ID','分类','证据路径'])
for r in rows:
 g=next(g for g in groups if r['task_id'] in g['task_ids']);w.append([r['task_id'],r['title'],r['updated_at'],g['group_id'],META[g['category']][0],r['evidence_path']])
w=wb.create_sheet('Git提交证据');w.append(['任务ID','日期（作者日期）','Commit','任务索引路径'])
for r in history:w.append([r['task_id'],r['date'],r['commit'],r['path']])
for ws in wb:
 ws.freeze_panes='A2';ws.auto_filter.ref=ws.dimensions
 for row in ws:
  for c in row:c.font=Font(name='微软雅黑',size=12);c.alignment=Alignment(vertical='top',wrap_text=True)
 for c in ws[1]:c.fill=PatternFill('solid',fgColor='24372D');c.font=Font(name='微软雅黑',size=12,bold=True,color='FFFFFF')
 for col in ws.columns:ws.column_dimensions[col[0].column_letter].width=24 if col[0].column<3 else 32
 ws.column_dimensions['B'].width=48
 ws.row_dimensions[1].height=38
 for n in range(2,ws.max_row+1):ws.row_dimensions[n].height=55
wb.save(art/'协作问题排名与证据.xlsx')
check=load_workbook(art/'协作问题排名与证据.xlsx',data_only=True)
assert check['125个去重主题'].max_row==126
assert check['200项来源'].max_row==201
assert sum(check['排名与口径'].cell(i,3).value for i in range(2,14))==185
(ROOT/'artifact-manifest.json').write_text(json.dumps({'local_only':[{'path':str(f.relative_to(ROOT)),'sha256':hashlib.sha256(f.read_bytes()).hexdigest()} for f in art.glob('*.xlsx')],'publication':'not_published_business_analysis','template':'pakco-html@c8ce8b6dc3de1b87926a6b6a672733f6a3feef36/templates/webpage.html','source_head':'67c33a686c364735c9c3f555f70e650752b72c85'},ensure_ascii=False,indent=2))
print(json.dumps({'tasks':len(rows),'groups':len(groups),'record_days':185,'missing_groups':len(missing),'top8':[(s['name'],s['days']) for s in stats[:8]]},ensure_ascii=False))
