prepare-S4-classroom-next.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. """Prepare linked classroom workflow templates after public-source acceptance.
  2. No business API calls. IDs returned by future writes stay null until bound in
  3. the current actor's real-message execution. Never modify previous versions.
  4. """
  5. from copy import deepcopy
  6. import hashlib
  7. import json
  8. from pathlib import Path
  9. import zipfile
  10. ROOT = Path(__file__).resolve().parents[1]
  11. DOCS = Path('F:/文档/工作坊当前资料')
  12. OUT = ROOT / 'work/S4-classroom-next-20261103'
  13. ZIP = DOCS / 'ksf-S4-classroom-next-20261103.zip'
  14. assert not OUT.exists() and not ZIP.exists()
  15. OUT.mkdir()
  16. readback = json.loads((ROOT / 'work/S4-public-readback-20261103.json').read_text(encoding='utf-8'))
  17. assert readback['ok'] and readback['systemStage'] == 'S3'
  18. assert readback['projects'] == readback['opportunities'] == readback['s4Artifacts'] == 0
  19. existing = json.loads((ROOT / 'work/first-live-S3-v2-template.json').read_text(encoding='utf-8'))
  20. records = json.loads((ROOT / 'work/S4-public-record-inspection-20261103.json').read_text(encoding='utf-8'))['records']
  21. def put(name, data):
  22. with (OUT / name).open('xb') as stream:
  23. stream.write(data.encode('utf-8') if isinstance(data, str) else data)
  24. def put_json(name, data):
  25. put(name, json.dumps(data, ensure_ascii=False, indent=2) + '\n')
  26. def para(title, text):
  27. return {'kind': 'para', 'title': title, 'text': text}
  28. def table(title, cols, rows):
  29. return {'kind': 'table', 'title': title, 'data': {'cols': cols, 'rows': rows}}
  30. base = 'KSF-真实资料-20261102-01'
  31. source_ids = ['m1OQx9aynA', 'QdeMcyTTjX', 'PjORcvaRHn', '5S_Y4IgT5q']
  32. evidence = {key: deepcopy(existing['report']['evidence'][key]) for key in ['consumer-cq-03', 'consumer-cq-04']}
  33. evidence['public-p1'] = {
  34. 'sourceId': 'PjORcvaRHn', 'claim': '只证明苏宁公开图片页参数块显示净含量585克、袋装,不证明实物或当前在售。',
  35. 'quotes': [{'text': '净含量\n585克\n包装\n袋装', 'ref': 'https://m.suning.com/bigimages/12447641634.html;2026-11-03北京时间采集;evidenceId=xHGV_mRWcG;页面对象P1,不合并SKU'}]
  36. }
  37. evidence['public-p2'] = {
  38. 'sourceId': '5S_Y4IgT5q', 'claim': '只记录2023年作者对自己煮制的叙述,不替代包装说明,不作为本轮试验结果。',
  39. 'quotes': [{'text': '包装上写的煮面6-7分钟,实测写少了,煮了10分钟面才软。', 'ref': 'https://post.smzdm.com/p/aeqxm74k/;页面发表时间2023-12-18 21:20:15,时区未知;evidenceId=CeAEIwymTa'}]
  40. }
  41. pending = [
  42. '当前拟研究实物及版本、批次、获取渠道与日期未知;网页对象P1不等于已取得样品。',
  43. '旧cq-03/cq-04、P1和历史P2之间的精确SKU对应关系未知,不合并为同款同批次。',
  44. '份量与用餐目的、既有搭配习惯、提前或吃后补主食动机仍需实际观察;没有本轮试吃结果。',
  45. '具体人群、使用目的及场景分布、需求强度、支付意愿与市场普遍程度未知。',
  46. '燃魂具体常规/加量版本及当前可取得情况未知,不把宽品类公告当燃魂版本证明。',
  47. '如果开展人员研究,目标、对象、资源、样本与记录/比较方法及预先判断规则待定。',
  48. '配方、原料、产线、工艺、成本及相应合规资料未取得,不评定量产可行性。',
  49. '煮制告知的购买路径与用户理解未测试;香辣留水方向另需具体商品,不复用燃魂条件。'
  50. ]
  51. scope = '公开资料支持的课堂流程测试。候选问题尚未证明;不声称员工已正式确认、企业已立项或实物试验已完成。'
  52. portion = '对明确的一份,在当次实际用餐目的下是否需要额外补主食?“正餐”是待记录的目的之一,不预设人人都把它当一顿饭。'
  53. quotes_rows = [[entry['claim'], entry['quotes'][0]['text'], entry['sourceId']] for entry in evidence.values()]
  54. label_note = 'P1单袋正面图的人工目视标示为“净含量:面饼+配料117克 面饼:90克”和“煮面6-7分钟”;这是证据xHGV_mRWcG的locator.imageReadings及images/suning-05.jpg所示,不是网页rawText中的引文或本轮称量。标题117g×5与参数585克算术一致,不确认当前实物。P2历史背面图单列,不与P1拼成同SKU。'
  55. method = '公开资料一致性与证据缺口审查;引用4份已入库来源各1段主引文。包装照片读取与网页文字分开;没有招募、试吃、称量、问卷、A/B测试或统计估计。'
  56. source_note = 'cq-03/cq-04为2023年两个显示账号的历史自述;P1为2026-11-03北京时间观察到的苏宁公开高清图片页,P2为同日采集的2023年帖子。购买、真实身份、当前库存和SKU对应未核;完整原件见42号、数据库核对见45号。'
  57. def make_report(task_suffix, title, stage, artifact_type, chapters, project=False, executive=None):
  58. summary = executive or [scope, portion, '新资料已入库并独立回读,但当前市场需求、实物与量产结论仍未知。']
  59. rendered = [title, *summary]
  60. for chapter in chapters:
  61. rendered.append(chapter['title'])
  62. for section in chapter['sections']:
  63. rendered.append(section['title'])
  64. if section['kind'] == 'para':
  65. rendered.append(section['text'])
  66. else:
  67. rendered.extend(' | '.join(str(value) for value in row) for row in section['data']['rows'])
  68. raw = '\n\n'.join(rendered)
  69. result = {
  70. 'sessionId': None, 'taskKey': base + ':' + task_suffix, 'version': 1,
  71. 'stageCode': stage, 'title': title, 'artifactType': artifact_type,
  72. 'category': '拌面', 'rawText': raw, 'sourceIds': source_ids[:],
  73. 'pendingQuestions': pending[:],
  74. 'report': {'subtitle': '历史个案与公开页面的桌面评估,实物和市场验证待补',
  75. 'executive': summary, 'method': method, 'sources': source_note,
  76. 'chapters': chapters, 'evidence': deepcopy(evidence)}
  77. }
  78. if project:
  79. result['projectId'] = None
  80. return result
  81. candidate = make_report('S3-份量候选', '燃魂份量与用餐目的适配|课堂待验证候选', 'S3', 'opportunity_card', [
  82. {'id': 'candidate', 'title': '拟选课堂方向', 'stage': 'S3', 'sections': [para('问题', portion), para('证据程度', 'cq-03/cq-04形成历史重复线索,不证明普遍需求。P1/P2补对象标示与制备旁证,尚不证明实际份量适配。'), para('上下文', 'cq-03同帖还有价格正面描述及青豆不满,并添加挂面、菠菜、煎蛋;cq-04肯定味道且另加蛋肠。搭配习惯和补偿动机可能并存。'), table('四段原文与来源', ['支持范围', '逐字引文', 'sourceId'], quotes_rows)]},
  83. {'id': 'limits', 'title': '边界与待补', 'stage': 'S3', 'sections': [para('网页与包装', label_note), para('本轮选择含义', '供本次软件流程测试选择的课堂候选;执行本包前未宣称员工已经选定。煮制告知与香辣留水继续留在原S3第2版,不因本次取舍被否决。'), table('待补问题', ['序号', '待核实问题'], [[str(i + 1), q] for i, q in enumerate(pending)])]}
  84. ])
  85. candidate['opportunity'] = {
  86. 'key': base + ':portion-fit', 'title': '燃魂份量与用餐目的适配|待验证',
  87. 'demandSummary': '检验一份与实际用餐目的是否适配;历史个案仅作线索,是否需要补主食待验证,不预定加量方案。',
  88. 'targetAudience': '具体人群未验证;后续按实际用餐目的与研究目标界定。',
  89. 'usageScenario': '正餐、加餐等用途待记录,不预设统一情境。',
  90. 'fusionDefinition': '拌面×具体人群待定×用餐目的待定;公开资料支持的候选问题,不是已确认机会。'
  91. }
  92. baseline = make_report('S3-项目基线', '燃魂份量适配|项目S3证据基线', 'S3', 'opportunity_card', [
  93. {'id': 'baseline', 'title': '项目范围与证据基线', 'stage': 'S3', 'sections': [para('问题', portion), para('范围', scope), para('对象', label_note), table('已有主引文', ['支持范围', '逐字引文', 'sourceId'], quotes_rows)]},
  94. {'id': 'next', 'title': '进入桌面评估的依据与缺口', 'stage': 'S3', 'sections': [para('转阶段条件', '这份基线保存到本次真实返回的projectId下后,可按合同进入S4公开资料桌面评估。进入S4仅表示开始评估,不是已经验证需求或完成试吃。'), table('待补问题', ['序号', '待核实问题'], [[str(i + 1), q] for i, q in enumerate(pending)])]}
  95. ], project=True)
  96. assessment_rows = [
  97. ['规格展示一致性', 'P1标题117g×5、参数585克;单袋图含料117克、面饼90克', '页面标示算术一致;重量类型分开', '不证明现货、实发版本、实物重量或旧帖同SKU'],
  98. ['份量问题线索', 'cq-03/cq-04两篇2023年自述', '仅支持保留份量与用途适配问题', '没有本轮食用观察,不算市场比例或需求强度'],
  99. ['煮制说明存在性', 'P1图片6—7分钟;P2历史包装背面步骤', '两来源可找到煮制信息,分别保留时点', '不证明购买路径已讲清,不认定全系列当前说明'],
  100. ['制备表现', 'P2作者说自己煮10分钟才软', '保留历史个体叙述', '没有控制水量/火力/偏好,不判包装错误'],
  101. ['工艺成本合规', '企业资料未取得', '无法评定,保持未知', '不填写数值评分、可量产或低成本结论']
  102. ]
  103. s4 = make_report('S4', '燃魂份量适配|公开资料桌面评估 第1版', 'S4', 'assessment_report', [
  104. {'id': 'scope', 'title': '评估范围与已知对象', 'stage': 'S4', 'sections': [para('本轮实际工作', '已完成公开资料采集、原件核对及入库独立验收。本报告整合这些已有结果,不新增消费者试验数据。'), para('问题', portion), para('网页及图片依据', label_note)]},
  105. {'id': 'evidence', 'title': '证据与核对结果', 'stage': 'S4', 'sections': [table('主引文', ['支持范围', '逐字引文', 'sourceId'], quotes_rows), table('桌面评估', ['评估项', '依据', '结果', '不能判定'], assessment_rows)]},
  106. {'id': 'validation', 'title': '假设处理与后续验证条件', 'stage': 'S4', 'sections': [para('份量方向', '保留待验证,不据重量认定吃饱程度。提前加主食与吃后补食都记录;习惯与补偿可并存,原话与研究者解释分开。未实施的方案不产出结果。'), para('其他方向', '煮制告知收窄为真实购买路径的可见性与理解,尚无受试者测试;香辣留水是另一具体商品问题,不复用燃魂参数。'), para('继续、调整或停止', '若未来在明确对象与情境下取得额外补主食行为及份量原因原话,可继续研究并考察其他解释;动机混合或条件不足则调整问题,证据不足不等于证明无效。样本、方法和门槛待目标及资源确定后预先写明。')]},
  107. {'id': 'pending', 'title': '待核实问题与阶段结论', 'stage': 'S4', 'sections': [table('待补问题', ['序号', '待核实问题'], [[str(i + 1), q] for i, q in enumerate(pending)]), para('结论', '公开资料桌面评估已形成;当前需求、实物试验与量产验证未完成。课堂项目保持S4 in_progress,不标completed,不推进S5,不据此正式发布。')]}
  108. ], project=True, executive=[scope, '本报告为公开资料一致性和证据缺口评估,含已完成核对;不是空白试验计划,也不是实物试验报告。', '4份已入库来源、4段主引文;图示读取另有原图定位。当前SKU与历史帖子不合并。', '桌面评估可继续;当前人群、场景、需求强度、成本及量产结论仍未知。'])
  109. tasks = {
  110. '01-candidate-save.template.json': candidate,
  111. '02-project-create.template.json': {'sessionId': None, 'opportunityId': None, 'projectKey': base + ':portion-fit-project', 'title': '燃魂份量适配|公开资料课堂测试'},
  112. '03-project-S3-save.template.json': baseline,
  113. '04-project-S4-progress.template.json': {'sessionId': None, 'projectId': None, 'toStage': 'S4', 'state': 'in_progress', 'note': '本项目已保存S3证据基线,现进入公开资料桌面评估;实物与需求验证待补,不代表试吃完成或企业批准。'},
  114. '05-project-S4-save.template.json': s4
  115. }
  116. # Static preflight verifies references, no unknown IDs, visible pending table and
  117. # exact quotes against accepted sources; it does not claim runtime/server save.
  118. old_sources = {entry['sourceId']: entry['quotes'][0]['text'] for entry in existing['report']['evidence'].values()}
  119. new_sources = {r['objectId']: r['record']['rawText'] for r in records if r['className'] == 'KfSource'}
  120. for name, task in tasks.items():
  121. assert task['sessionId'] is None
  122. if 'projectId' in task:
  123. assert task['projectId'] is None
  124. if 'report' in task:
  125. assert task['sourceIds'] == source_ids and len(task['report']['evidence']) == 4
  126. assert task['pendingQuestions'] == pending and len(pending) == 8
  127. for entry in task['report']['evidence'].values():
  128. assert entry['sourceId'] in source_ids
  129. for quote in entry['quotes']:
  130. assert quote['text'] in (new_sources.get(entry['sourceId']) or old_sources[entry['sourceId']])
  131. assert quote['text'] in task['rawText']
  132. visible = [s for ch in task['report']['chapters'] for s in ch['sections'] if s.get('title') == '待补问题']
  133. assert len(visible) == 1
  134. assert visible[0]['data']['rows'] == [[str(i + 1), q] for i, q in enumerate(pending)]
  135. assert 'opportunity' not in task or task is candidate
  136. assert all(s['kind'] in ('para', 'table') for ch in task['report']['chapters'] for s in ch['sections'])
  137. put_json(name, task)
  138. assert all(0 < len(value) <= 200 for value in candidate['opportunity'].values())
  139. put_json('preflight.json', {'ok': True, 'businessApiCalls': 0, 'businessWrites': 0, 'saved': False,
  140. 'browserVerified': False, 'localStructureOnly': True, 'templates': list(tasks),
  141. 'inputBinding': 'sessionId null; projectId/opportunityId null where required',
  142. 'reportCount': 3, 'sourcesPerReport': 4, 'quotesPerReport': 4, 'pendingPerReport': 8})
  143. handoff = '''运维001,请核验本包并准备后续课堂流程的固定模板。当前仍为S3,本包未执行任何写入。候选沿用S3已列“燃魂份量与用餐目的适配”,仅拟作本次软件测试方向,不宣称员工已经正式选定;用户转交本单用于准备,不由运维代作业务选择或写入。
  144. 先读45号独立验收。两份新资料已导入且Codex独立核查通过:P1 PjORcvaRHn/xHGV_mRWcG,P2 5S_Y4IgT5q/CeAEIwymTa,禁止重导。旧cq-03 m1OQx9aynA/_9mUml0wmh、cq-04 QdeMcyTTjX/2bFGP7axMo沿用。共4来源、4主引文,候选/项目基线/S4报告均保留8条缺口,不含评分或实物结果。
  145. 将5份template原样落地/opt/data/ksf-executor-20261103/S4-classroom-next-01/,root:root0644且hermes实读,核清单与完整哈希。模板sessionId均为null,涉及opportunityId/projectId处也为null,不能直接执行,不在本轮生成可执行输入。
  146. 按现网合同核workshop-save/project-create/project-progress及S4报告,复核同一真实会话btrFqkz0yw、当前王刚身份及S1/S2/S3归属。报告仅允许绑定规定来源,4引文逐字存在;旧两条可沿用45号前后的已核记录,不必为重复写稿再采集。发现与当前合同不同,先报差异,不改Skill/Function/schema或放宽校验。
  147. 历史检查范围:本次候选系列KSF-真实资料-20261102-01:S3-份量候选 v1;机会系列KSF-真实资料-20261102-01:portion-fit;项目键KSF-真实资料-20261102-01:portion-fit-project;项目S3系列KSF-真实资料-20261102-01:S3-项目基线 v1;S4系列KSF-真实资料-20261102-01:S4 v1。检查47号全部requestId及目标文件、在途。已有对象或回执先按编号回报,不删除、不换键。跨日不新建会话。
  148. 留存目标/opt/data/ksf-workshop-private/S4-classroom-next-01/仅在不存在时新建,hermes0700,先确认为空。保留既有S4-public-import/及所有S1/S2/S3/S4-live-00回执。
  149. 关键顺序:候选S3报告带opportunity创建卡→读取卡→project-create并读取项目→保存带新projectId的S3基线并读取→project-progress进入S4→读取项目→保存项目下S4桌面评估并读取→最终project-status。现有S3第2版没有projectRef,不能代替项目S3基线;不补写旧记录。
  150. 本轮只核包、落地、合同、归属、来源、历史及在途。不执行save/project-create/project-progress;不重导、不建产品/分类、不采购或试吃、不更新S5或正式发布。回报5模板字节/完整哈希及各预检结果,完成后按47号交王刚001在真实消息执行。
  151. '''
  152. execution = '''王刚001,本单仅在用户选择“燃魂份量与用餐目的适配”用于本次课堂流程测试并转交执行、且46号预检通过后使用。用户尚未选择、身份不符、已有本任务回执或在途时先停核,不擅自称员工已选定。它创建课堂候选卡与项目,不是企业立项;只做公开资料桌面评估,不声称实物/市场验证。
  153. 目录:模板/opt/data/ksf-executor-20261103/S4-classroom-next-01/;输入与回执/opt/data/ksf-workshop-private/S4-classroom-next-01/。本轮不重新import任何来源。每一步成功并核对后才做下一步,不把命令批量无条件连跑。
  154. 一、身份与固定模板
  155. 无参数调用ksfregistercurrent_employee核实本条完整agentProfileKey与会话btrFqkz0yw及旧S3归属一致。实读5份模板核FILES.sha256、47号全文及所有目标文件不存在;不从截断键或口头姓名拼身份。保留当前会话,不执行start。
  156. 模板以外的固定内容不得改写。读入内存后仅绑定sessionId=btrFqkz0yw;涉及opportunityId/projectId必须用本次已验证返回值。逐字段差异严格等于以下各步列出的字段;使用O_EXCL新建0600输入,目录0700。已存在即停查,禁止覆盖或删除。每次stdout/stderr/退出码、身份及两层回执独占0600留存,禁止输出凭据。
  157. 执行入口统一为node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs;所有业务写命令带--agent-profile <本条完整agentProfileKey>,读命令可不带。
  158. 二、依次执行(每条最多一次)
  159. 01. 从01-candidate-save.template.json仅填sessionId,新建01-candidate-save.json;workshop-save,requestId=KSF-真实资料-20261102-01:S3-份量候选:save:v1。应返回新artifactId/sourceId/opportunityId,projectId=null。这是新系列v1,不修改S3第2版,不冒报S3第3版。
  160. 02. 取新artifactId,独占生成{"className":"KfArtifact","objectId":"<返回值>"}到01-candidate.read.json;workshop-read,requestId=KSF-真实资料-20261102-01:S3-份量候选:read:v1。
  161. 03. 取新opportunityId,独占生成KfOpportunity精确输入01-opportunity.read.json;workshop-read,requestId=KSF-真实资料-20261102-01:portion-fit:read:v1。核claimKind=hypothesis、status=candidate、opportunitySeriesKey=KSF-真实资料-20261102-01:portion-fit、version1、session归属、linkedArtifacts包含步骤01新报告、全部opportunity字段等于模板,未连项目。
  162. 04. 02-project-create.template.json仅填sessionId与步骤03已核opportunityId,新建02-project-create.json;project-create,requestId=KSF-真实资料-20261102-01:portion-fit-project:create:v1。应返回新projectId,stageCode=S3、enterpriseApproval=false;保留eventId。仅创建课堂项目。
  163. 05. 独占新建02-project-status.json={"sessionId":"btrFqkz0yw","projectId":"<步骤04返回值>"};project-status,requestId=KSF-真实资料-20261102-01:portion-fit-project:read-created:v1。核项目归属、live、S3、opportunityId为步骤03,暂无项目成果或方案。项目创建会把课堂机会卡status改为approved、linkedProject填新项目;approved只作课堂状态,不说企业审批通过。
  164. 06. 03-project-S3-save.template.json仅填sessionId与已核projectId,新建03-project-S3-save.json;workshop-save,requestId=KSF-真实资料-20261102-01:S3-项目基线:save:v1。应返回新S3 artifactId/sourceId,projectId同本次项目、opportunityId=null。
  165. 07. 独占新建03-project-S3.read.json按新artifactId精读;workshop-read,requestId=KSF-真实资料-20261102-01:S3-项目基线:read:v1。核projectRef/projectId确实指本项目,这是转阶段的必要前置,不用旧S3报告代替。
  166. 08. 04-project-S4-progress.template.json仅填sessionId与projectId,新建04-project-S4-progress.json;project-progress,requestId=KSF-真实资料-20261102-01:portion-fit-project:to-S4:v1。应返回stageCode=S4、progressState=in_progress、enterpriseApproval=false,保留eventId。
  167. 09. 使用已保存的02-project-status.json只读;project-status,requestId=KSF-真实资料-20261102-01:portion-fit-project:read-S4:v1。核阶段S4/in_progress,项目S3基线仍在。此时不声称S4报告已保存。
  168. 10. 05-project-S4-save.template.json仅填sessionId与projectId,新建05-project-S4-save.json;workshop-save,requestId=KSF-真实资料-20261102-01:S4:save:v1。应返回S4 artifactId/sourceId,同项目,opportunityId=null,version1。
  169. 11. 独占新建05-project-S4.read.json按返回artifactId精读;workshop-read,requestId=KSF-真实资料-20261102-01:S4:read:v1。
  170. 12. 使用02-project-status.json;project-status,requestId=KSF-真实资料-20261102-01:portion-fit-project:read-final:v1。核项目S4/in_progress、所属S3基线与S4报告都有、无新品方案、不推进S5或completed。
  171. 三、命令格式(由当前步已独占新建的文件和上列requestId填入,不改变业务键)
  172. 写:node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs <workshop-save或project-create或project-progress> --agent-profile <本条完整agentProfileKey> --input /opt/data/ksf-workshop-private/S4-classroom-next-01/<上述本步输入名> --request-id <上述本步requestId>
  173. 读:node /opt/data/skills/ksf-workshop-runtime/scripts/ksf-secure.mjs <workshop-read或project-status> --input /opt/data/ksf-workshop-private/S4-classroom-next-01/<上述本步读取输入名> --request-id <上述本步requestId>
  174. 四、各报告保存及回读必核
  175. 外层ok、entryResult.ok、EXIT=0;保存取sessionId/artifactId/sourceId/opportunityId/projectId。回读逐项核stageCode/version1/reportSeriesKey/artifactType/live/dashboardState=workshop/归属session、projectRef(后两报告)、tags含拌面与课堂过程成果、rawText、sourceIds恰4个、report.chapters/evidence/method/sources/subtitle与模板逐项一致,8条pendingQuestions及可见表8行逐字对应。4主引文逐字、sourceId对应正确。report.executive仅允许服务在前方加课堂声明;report.title/generatedAt为服务新增。previousVersionRef可null,不声称Pointer版本链。
  176. 保存响应boardVisible为课堂可见;不要求回读出现此字段。dashboardPublishedAt=null、employeeConfirmed=false、formalReportPublished=false为本轮预期。没有打开生产浏览器时browserVerified=false,后续由Codex验收。不把报告保存成功说成实物试验、已验证普遍需求或量产已可行。
  177. 任一步失败、超时、不确定立即停止,保留原requestId及已取得全部编号/回执;不自动重试、换键、改输入、删除或回滚记录。错误不等于零写入,只读失败不因此重跑任何save或create。交Codex按现存编号独立核对。
  178. 最终回报新候选artifactId/sourceId/opportunityId、新projectId/eventId、项目S3基线artifactId/sourceId、S4 artifactId/sourceId;每步两层调用编号、退出码、核对结果与留存路径。新建3报告+其3正文源+1课堂机会卡+1项目属预期,不改既有S3两版或S1/S2,不建产品/分类,不重导资料、不正式发布。
  179. '''
  180. put('46-发给运维001-S4课堂项目与评估准备.txt', handoff)
  181. put('47-发给王刚001-S4课堂项目与桌面评估.txt', execution)
  182. put('45-S4公开资料入库独立验收结果.md', (DOCS / '45-S4公开资料入库独立验收结果.md').read_bytes())
  183. put('README.txt', 'S4课堂后续固定模板包。未执行写入。先45,再46准备,再由用户选择课堂测试方向后按47在真实消息执行。\n全部绑定ID字段为null,禁止直接提交模板。机会卡/项目为课堂状态,不是企业审批。没有虚构试吃、评分或成本结果。\n顺序包含项目S3成果→转S4,避免现有无projectRef的旧S3无法满足推进前置。旧报告保持原样。\n')
  184. manifest = []
  185. for file in sorted(OUT.iterdir()):
  186. if file.is_file():
  187. manifest.append(hashlib.sha256(file.read_bytes()).hexdigest() + ' ' + file.name)
  188. put('FILES.sha256', '\n'.join(manifest) + '\n')
  189. with zipfile.ZipFile(ZIP, 'x', compression=zipfile.ZIP_DEFLATED, compresslevel=7) as bundle:
  190. for file in sorted(OUT.iterdir()):
  191. bundle.write(file, file.name)
  192. with zipfile.ZipFile(ZIP) as bundle:
  193. assert bundle.testzip() is None
  194. for line in manifest:
  195. digest, name = line.split(' ', 1)
  196. assert hashlib.sha256(bundle.read(name)).hexdigest() == digest
  197. for name in ['46-发给运维001-S4课堂项目与评估准备.txt', '47-发给王刚001-S4课堂项目与桌面评估.txt']:
  198. with (DOCS / name).open('xb') as stream:
  199. stream.write((OUT / name).read_bytes())
  200. receipt = {'zip': str(ZIP), 'bytes': ZIP.stat().st_size, 'sha256': hashlib.sha256(ZIP.read_bytes()).hexdigest(),
  201. 'entries': len(manifest) + 1, 'manifestVerified': len(manifest), 'businessApiCalls': 0, 'businessWrites': 0,
  202. 'saved': False, 'employeeDirectionSelectionConfirmed': False,
  203. 'templates': {name: {'bytes': (OUT / name).stat().st_size, 'sha256': hashlib.sha256((OUT / name).read_bytes()).hexdigest()} for name in tasks}}
  204. with (ROOT / 'work/S4-classroom-next-package-receipt-20261103.json').open('x', encoding='utf-8') as stream:
  205. json.dump(receipt, stream, ensure_ascii=False, indent=2)
  206. print(json.dumps(receipt, ensure_ascii=True, indent=2))