@runweave/server-linux-x64 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/manifest.json +231 -226
  2. package/native.tar.gz +0 -0
  3. package/package.json +1 -1
  4. package/payload/app-server/build-info.json +4 -4
  5. package/payload/app-server/index.cjs +46 -25
  6. package/payload/app-server/index.cjs.map +1 -1
  7. package/payload/backend/activity-sqlite-worker.cjs +33 -21
  8. package/payload/backend/build-info.json +4 -4
  9. package/payload/backend/dev-resources/control.cjs +158 -59
  10. package/payload/backend/index.cjs +3512 -2764
  11. package/payload/backend/scheduled-tasks-sqlite-worker.cjs +12 -0
  12. package/payload/frontend/dist/assets/{_baseUniq-V9OGBCCp.js → _baseUniq-pNlq_DCQ.js} +1 -1
  13. package/payload/frontend/dist/assets/{access-Bw9yaM2u.js → access-BJmFgyT9.js} +1 -1
  14. package/payload/frontend/dist/assets/{advanced-BspSUvyf.js → advanced-B1_D3p4w.js} +1 -1
  15. package/payload/frontend/dist/assets/{appearance-BHYR2QUj.js → appearance-CSQVEVTh.js} +1 -1
  16. package/payload/frontend/dist/assets/{arc-C4bVuMy6.js → arc-DE1dO7p3.js} +1 -1
  17. package/payload/frontend/dist/assets/{architectureDiagram-Q4EWVU46-B_EOBe6n.js → architectureDiagram-Q4EWVU46-QKJomhdt.js} +1 -1
  18. package/payload/frontend/dist/assets/{blockDiagram-DXYQGD6D-aAkbmNO9.js → blockDiagram-DXYQGD6D-CV58X8VO.js} +1 -1
  19. package/payload/frontend/dist/assets/{c4Diagram-AHTNJAMY-DKeUolGU.js → c4Diagram-AHTNJAMY-DPu-RnDw.js} +1 -1
  20. package/payload/frontend/dist/assets/channel-DWLRGtq3.js +1 -0
  21. package/payload/frontend/dist/assets/{chunk-4BX2VUAB-cB5sH0Mw.js → chunk-4BX2VUAB-COwHdW0x.js} +1 -1
  22. package/payload/frontend/dist/assets/{chunk-4TB4RGXK-CxVRfINM.js → chunk-4TB4RGXK-StHcdOx9.js} +1 -1
  23. package/payload/frontend/dist/assets/{chunk-55IACEB6-BFRLEEBY.js → chunk-55IACEB6-D7PMYZ35.js} +1 -1
  24. package/payload/frontend/dist/assets/{chunk-EDXVE4YY-DA5BuoT3.js → chunk-EDXVE4YY-D84apqdf.js} +1 -1
  25. package/payload/frontend/dist/assets/{chunk-FMBD7UC4-DIvA96Y1.js → chunk-FMBD7UC4-CJmwcVvj.js} +1 -1
  26. package/payload/frontend/dist/assets/{chunk-OYMX7WX6-8Zl-vtJp.js → chunk-OYMX7WX6-DQhh6xQp.js} +1 -1
  27. package/payload/frontend/dist/assets/{chunk-QZHKN3VN-DnW4X6Wx.js → chunk-QZHKN3VN-C6X5cUuc.js} +1 -1
  28. package/payload/frontend/dist/assets/{chunk-YZCP3GAM-CH4YkXv-.js → chunk-YZCP3GAM-CryVThNG.js} +1 -1
  29. package/payload/frontend/dist/assets/classDiagram-6PBFFD2Q-B2WrDPFz.js +1 -0
  30. package/payload/frontend/dist/assets/classDiagram-v2-HSJHXN6E-B2WrDPFz.js +1 -0
  31. package/payload/frontend/dist/assets/clone-BJQLpgjC.js +1 -0
  32. package/payload/frontend/dist/assets/{connection-CINMNAPm.js → connection-425RE3U4.js} +1 -1
  33. package/payload/frontend/dist/assets/{connections-DsHGQRR2.js → connections-Dx05bPzL.js} +1 -1
  34. package/payload/frontend/dist/assets/{cose-bilkent-S5V4N54A-Dm1QOjDr.js → cose-bilkent-S5V4N54A-DqICEhiJ.js} +1 -1
  35. package/payload/frontend/dist/assets/{dagre-KV5264BT-BEbPPDoW.js → dagre-KV5264BT-SyIBcIz3.js} +1 -1
  36. package/payload/frontend/dist/assets/{desktop-1jdcRQ7b.js → desktop-DBzea7Bt.js} +1 -1
  37. package/payload/frontend/dist/assets/{diagram-5BDNPKRD-C-DmBIhV.js → diagram-5BDNPKRD-xS_FVHQR.js} +1 -1
  38. package/payload/frontend/dist/assets/{diagram-G4DWMVQ6-Ddv6Kw2_.js → diagram-G4DWMVQ6-CLuycLkG.js} +1 -1
  39. package/payload/frontend/dist/assets/{diagram-MMDJMWI5-Xae79J8R.js → diagram-MMDJMWI5-BTNd9OzE.js} +1 -1
  40. package/payload/frontend/dist/assets/{diagram-TYMM5635-CavrGRG2.js → diagram-TYMM5635-DyxLhyoy.js} +1 -1
  41. package/payload/frontend/dist/assets/{erDiagram-SMLLAGMA-BRRD8F-e.js → erDiagram-SMLLAGMA-C2a9uz8_.js} +1 -1
  42. package/payload/frontend/dist/assets/{flowDiagram-DWJPFMVM-C-ewssi0.js → flowDiagram-DWJPFMVM-DndbO3ER.js} +1 -1
  43. package/payload/frontend/dist/assets/{ganttDiagram-T4ZO3ILL-BcZnXV6L.js → ganttDiagram-T4ZO3ILL-BSV1btDD.js} +1 -1
  44. package/payload/frontend/dist/assets/{gitGraphDiagram-UUTBAWPF-ZcfWVMGG.js → gitGraphDiagram-UUTBAWPF-DnYc3Xm2.js} +1 -1
  45. package/payload/frontend/dist/assets/{graph-DRJG1bKF.js → graph-iOswNacw.js} +1 -1
  46. package/payload/frontend/dist/assets/{image-GQYH_1h4.js → image-BaXi5ATy.js} +1 -1
  47. package/payload/frontend/dist/assets/index-BRwhFF-d.js +1258 -0
  48. package/payload/frontend/dist/assets/{infoDiagram-42DDH7IO-BWsoBaYn.js → infoDiagram-42DDH7IO-BoWdmtBx.js} +1 -1
  49. package/payload/frontend/dist/assets/{ishikawaDiagram-UXIWVN3A-C3ayGr4G.js → ishikawaDiagram-UXIWVN3A-B8KC_SgA.js} +1 -1
  50. package/payload/frontend/dist/assets/{journeyDiagram-VCZTEJTY-DB1DxZ1v.js → journeyDiagram-VCZTEJTY-CN_MxT78.js} +1 -1
  51. package/payload/frontend/dist/assets/{kanban-definition-6JOO6SKY-CtZWN0CK.js → kanban-definition-6JOO6SKY-j17QaQ4r.js} +1 -1
  52. package/payload/frontend/dist/assets/{layout-KX5XwX_6.js → layout-CLR2o63S.js} +1 -1
  53. package/payload/frontend/dist/assets/{linear-DMZrhooi.js → linear-CL0d3XrR.js} +1 -1
  54. package/payload/frontend/dist/assets/main-BO-hQK7n.css +33 -0
  55. package/payload/frontend/dist/assets/{main-LH8ULGFT.js → main-DWkxAGaw.js} +142 -142
  56. package/payload/frontend/dist/assets/{min-CWc1cxEQ.js → min-2aYi0G6U.js} +1 -1
  57. package/payload/frontend/dist/assets/{mindmap-definition-QFDTVHPH-tj_8hDDH.js → mindmap-definition-QFDTVHPH-CxVQlE5J.js} +1 -1
  58. package/payload/frontend/dist/assets/{monitoring-BaUd-PhH.js → monitoring-GwSez8fI.js} +1 -1
  59. package/payload/frontend/dist/assets/{notifications-CTv1WVBh.js → notifications-Bx24ROH2.js} +1 -1
  60. package/payload/frontend/dist/assets/{pieDiagram-DEJITSTG-Cwc4Gfp4.js → pieDiagram-DEJITSTG-CpRHmWOC.js} +1 -1
  61. package/payload/frontend/dist/assets/{quadrantDiagram-34T5L4WZ-Clq2RqDJ.js → quadrantDiagram-34T5L4WZ-CZG_QGX1.js} +1 -1
  62. package/payload/frontend/dist/assets/{record-content-DQ0F1zbW.js → record-content-Di3pmXSa.js} +1 -1
  63. package/payload/frontend/dist/assets/{requirementDiagram-MS252O5E-gaTHj3JP.js → requirementDiagram-MS252O5E-C1PCFhMe.js} +1 -1
  64. package/payload/frontend/dist/assets/{sankeyDiagram-XADWPNL6-CyBolt3D.js → sankeyDiagram-XADWPNL6-RxkobNrx.js} +1 -1
  65. package/payload/frontend/dist/assets/{sequenceDiagram-FGHM5R23-CO4UgRHw.js → sequenceDiagram-FGHM5R23-Bem61Ef-.js} +1 -1
  66. package/payload/frontend/dist/assets/{stateDiagram-FHFEXIEX-ygBwN1Az.js → stateDiagram-FHFEXIEX-DaHV-La3.js} +1 -1
  67. package/payload/frontend/dist/assets/stateDiagram-v2-QKLJ7IA2-VVMK0Gty.js +1 -0
  68. package/payload/frontend/dist/assets/{status-BOcUUbE9.js → status-DytAqsKi.js} +1 -1
  69. package/payload/frontend/dist/assets/suiji-ClmlHdOg.js +1 -0
  70. package/payload/frontend/dist/assets/{svg-BfkbqvDT.js → svg-BNFCoAlV.js} +1 -1
  71. package/payload/frontend/dist/assets/{tabs-88TVAJwX.js → tabs-CNVBHmw-.js} +1 -1
  72. package/payload/frontend/dist/assets/{timeline-definition-GMOUNBTQ-8Iy3eg4l.js → timeline-definition-GMOUNBTQ-xVCcgT05.js} +1 -1
  73. package/payload/frontend/dist/assets/{vennDiagram-DHZGUBPP-DLcttV2t.js → vennDiagram-DHZGUBPP-D4YvcLlt.js} +1 -1
  74. package/payload/frontend/dist/assets/{wardley-RL74JXVD-DUvr0OYP.js → wardley-RL74JXVD-WtdSbuj2.js} +1 -1
  75. package/payload/frontend/dist/assets/{wardleyDiagram-NUSXRM2D-Dm-sdRY6.js → wardleyDiagram-NUSXRM2D-Br4RBb8X.js} +1 -1
  76. package/payload/frontend/dist/assets/{xychartDiagram-5P7HB3ND-DR_MOYh1.js → xychartDiagram-5P7HB3ND-Dx8RJOfi.js} +1 -1
  77. package/payload/frontend/dist/build-info.json +4 -4
  78. package/payload/frontend/dist/companion.html +1 -1
  79. package/payload/frontend/dist/index.html +3 -3
  80. package/payload/plugins/toolkit/.codex-plugin/plugin.json +1 -1
  81. package/payload/plugins/toolkit/README.md +3 -3
  82. package/payload/plugins/toolkit/skills/acceptance/SKILL.md +4 -2
  83. package/payload/plugins/toolkit/skills/acceptance/references/delivery.md +11 -1
  84. package/payload/plugins/toolkit/skills/acceptance/references/gallery-contract.md +62 -39
  85. package/payload/plugins/toolkit/skills/acceptance/references/gallery-copy.md +8 -0
  86. package/payload/plugins/toolkit/skills/acceptance/scripts/delivery_check.py +151 -0
  87. package/payload/plugins/toolkit/skills/acceptance/scripts/gallery.py +67 -11
  88. package/payload/plugins/toolkit/skills/acceptance/scripts/preview.py +21 -2
  89. package/payload/plugins/toolkit/skills/acceptance/templates/reader.css +1 -14
  90. package/payload/plugins/toolkit/skills/acceptance/templates/reader.html +23 -4
  91. package/payload/plugins/toolkit/skills/acceptance/templates/reader.js +76 -3
  92. package/payload/plugins/toolkit/skills/acceptance/templates/report.css +80 -0
  93. package/payload/plugins/toolkit/skills/architecture-flow-prototyping/SKILL.md +2 -2
  94. package/payload/plugins/toolkit/skills/playwright-cli/SKILL.md +19 -6
  95. package/payload/plugins/toolkit/skills/review-only/SKILL.md +29 -4
  96. package/payload/plugins/toolkit/skills/review-only/references/delivery-verification.md +38 -0
  97. package/payload/plugins/toolkit/skills/using-rw/SKILL.md +37 -1
  98. package/payload/plugins/toolkit/skills/visual-plan-review/SKILL.md +45 -0
  99. package/payload/plugins/toolkit/skills/visual-plan-review/agents/openai.yaml +3 -0
  100. package/payload/plugins/toolkit/skills/visual-plan-review/references/review-checklist.md +13 -0
  101. package/payload/plugins/toolkit/skills/visual-plan-review/references/visual-plan-template.html +8 -0
  102. package/payload/plugins/toolkit-release.json +1 -1
  103. package/payload/pty-host/index.cjs +217 -41
  104. package/payload/server/index.cjs +40 -20
  105. package/payload/frontend/dist/assets/channel-CBjt7igT.js +0 -1
  106. package/payload/frontend/dist/assets/classDiagram-6PBFFD2Q-HmoWChAG.js +0 -1
  107. package/payload/frontend/dist/assets/classDiagram-v2-HSJHXN6E-HmoWChAG.js +0 -1
  108. package/payload/frontend/dist/assets/clone-CiD5LYOe.js +0 -1
  109. package/payload/frontend/dist/assets/index-UR7g5PLZ.js +0 -448
  110. package/payload/frontend/dist/assets/main-D4_uoZQa.css +0 -33
  111. package/payload/frontend/dist/assets/stateDiagram-v2-QKLJ7IA2-DTtHtWkN.js +0 -1
  112. package/payload/frontend/dist/assets/suiji-DPDKlWVi.js +0 -1
  113. package/payload/plugins/toolkit/skills/prototype-first-development/SKILL.md +0 -134
  114. package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/README.md +0 -88
  115. package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/app.js +0 -145
  116. package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/index.html +0 -229
  117. package/payload/plugins/toolkit/skills/prototype-first-development/references/html-prototype-template/mock-state.json +0 -60
  118. package/payload/plugins/toolkit/skills/prototype-first-development/references/implementation-plan-handoff-template.md +0 -61
  119. package/payload/plugins/toolkit/skills/prototype-first-development/references/prototype-workflow-checklist.md +0 -64
@@ -1,20 +1,38 @@
1
- # 图文材料数据合同 v1
1
+ # 图文材料与验收报告数据合同
2
2
 
3
- ## 输入
3
+ ## 输入与兼容
4
4
 
5
5
  UTF-8 JSON,拒绝重复键、未知字段、NaN 和未知版本。文件路径相对 `--input-root`;`--input` 默认 `task.json`。根目录必须已存在。ID 为 1–80 位字母、数字、`-`、`_`,首位为字母或数字。
6
6
 
7
+ 新报告使用 `schemaVersion: 2`。旧版 `schemaVersion: 1` 输入继续支持,不修改历史产物,也不根据旧页的 observed 自动推导通过结论。v1 不接受 report 或 item.summary;升级时必须按实际记录补齐报告,不能仅改版本号。
8
+
7
9
  | 对象 | 字段 |
8
10
  | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
9
- | root | `schemaVersion: 1`、`task`、`assets: []`、`pages: []` |
11
+ | root | `schemaVersion: 1/2`、`task`、`assets: []`、`pages: []`;v2 必须有 `report` |
10
12
  | task | `id`、`title`、`scope`、`materialKind: current/historical`、`limitations: string[]` |
11
13
  | asset | 唯一 `id`、`kind: image/text`、`path`、`label`、`origin` |
12
14
  | origin | `surface`、`capturedAt`、`sourceRef`,每项为非空字符串或 `null` |
13
15
  | page | 唯一 `id`、`type`、`title`、`description`、`evidenceState`、`items`、`sourceIds: string[]`;可选 `lookAt`、`limitations: string[]` |
14
- | item | `assetId`、`label`;图片项必需 `alt`,可选 `crop: {x,y,width,height}`;文字项不接受 alt/crop |
16
+ | item | `assetId`、`label`;图片项必需 `alt`,可选 `crop: {x,y,width,height}`;v2 文字项可选 `summary`,不接受 alt/crop |
17
+ | report | `summary`、非空数组 `checks` |
18
+ | check | 唯一 `id`、`title`、`expected`、`actual`、`execution`、`outcome`、`pageIds: string[]` |
15
19
 
16
20
  文字必填项须为非空字符串。时间按实际采集记录填写,建议带时区 ISO 8601;不知道则 `null`。版本、设备、对象身份等条件写在 scope、description 或关联文字记录中,`sourceRef` 给其原始位置或记录标识。各素材允许不同来源,但不能因此宣称同一事件。
17
21
 
22
+ ## 验收项与首屏总览(v2)
23
+
24
+ - `report.summary`:作者写的一段总体判断及关键保留条件;只总结本次真实记录,不由生成器计算“整体通过”。task.limitations 直接可见,不折叠关键边界。
25
+ - 每项 check 对应一个可判断的用户目标。`expected` 写标准,`actual` 写操作后观察或未执行/阻塞原因。不要把“桌面 UI 检查”和“真机扫码成功”合为一项再统一标通过。
26
+ - `execution` 为 `real`(实际执行)、`mock`(确实在 Mock/模拟环境执行)、`not-run`(没有执行)。没有证据不能凭空改称 Mock;运行端及浏览器模拟/真机的具体边界仍需写明。
27
+ - `outcome` 为 `pass`、`fail`、`blocked`、`not-run`,是报告作者记录的**自检结果**,不代表用户验收确认。实际执行后观察到不符合标准可记 fail;无法得出结果时记 blocked 并说明原因。
28
+ - `pageIds` 只引用本报告已有证据页且不重复,可为空。pass/fail 必须关联至少一页 observed;pass 不可引用 insufficient/not-run 的页作为支持。not-run 结果只能配 not-run 执行;未执行不能标 pass/fail。
29
+ - 失败、阻塞、未执行项在总览中优先且默认展开;通过项显示标题及执行方式,按需展开预期、实际和证据链接。是否有图不影响缺口的可见性。
30
+ - v2 允许 `pages: []`、`assets: []`,用于只有阻塞或未执行项的报告,不要求制造截图或无意义文字文件。v1 仍至少需要一页。
31
+
32
+ 校验只检查结构、引用和显式状态是否自洽,无法判定截图真假、目标是否遗漏或作者结论是否正确。报告必须覆盖本次约定的目标,包括没有执行的部分;不能仅列成功项。
33
+
34
+ ## 证据页与素材
35
+
18
36
  | type | items 数量 | 素材 kind |
19
37
  | -------- | ---------- | --------- |
20
38
  | single | 1 | image |
@@ -22,63 +40,68 @@ UTF-8 JSON,拒绝重复键、未知字段、NaN 和未知版本。文件路径
22
40
  | sequence | 2–4 | image |
23
41
  | text | 1 | text |
24
42
 
25
- `pages` 至少一项;数组顺序为阅读顺序。页面 ID 用于 `#page=<id>`,修改描述时保留 ID。
43
+ 页面数组顺序为证据阅读顺序。页面 ID 用于 `#page=<id>`,修改描述时保留 ID。
44
+
45
+ `evidenceState` 为 `observed`(有观察材料)、`insufficient`(证据不足)、`not-run`(未执行),不含业务通过/失败判定。后两项的具体缺口写在正文或 limitations 中。observed 同样可以记录失败,不能根据页数计算通过率。
26
46
 
27
- `evidenceState` 为 `observed`(有观察材料)、`insufficient`(证据不足)、`not-run`(未执行),不含业务通过/失败判定。后两项的具体缺口应写在正文或 limitations 中;未执行又没有截图时用真实文字说明页,不造图。
47
+ v2 文字项的 `summary` 是面向用户的简短摘要,按正常正文排版;原始文字不被替换或截断,点击“打开原文”即可核对。不填 summary 时仍显示完整等宽原文。来源区只列出处和打开入口,不重复铺开全文。所有文本均为纯文本,不解析 Markdown/HTML。
28
48
 
29
- `sourceIds` 只引用 text 素材,可为空。PNG/JPEG 图片最大 32 MiB、宽高各最多 32768px;文字 TXT/MD/JSON 及输入 JSON 最大 2 MiB。JSON 素材也要求合法 JSON。PNG 校验结构和 CRC;JPEG 检查容器与常用 SOF 尺寸,完整解码由浏览器验证。若图片打不开,不能只依据 validate 成功交付。
49
+ `sourceIds` 只引用 text 素材,可为空。PNG/JPEG 图片最大 32 MiB、宽高各最多 32768px;文字 TXT/MD/JSON 及输入 JSON 最大 2 MiB。JSON 素材也要求合法 JSON。PNG 校验结构和 CRC;JPEG 检查容器与常用 SOF 尺寸,完整解码由浏览器验证。素材上限不等于 Runweave 交付上限,见下文预检。
30
50
 
31
51
  `crop` 为原 PNG 整数像素坐标,x/y 非负、width/height 为正且完整位于原图内。JPEG 仅允许完整展示,避免 EXIF 方向与坐标歧义。原图不被重采样或写回。
32
52
 
33
53
  ## 最小可执行输入
34
54
 
35
- 在同一输入目录准备真实的 `observation.txt`,再保存如下 `task.json`;把示例内容改成当前任务事实后执行。
55
+ 以下示例明确表示没有执行;保存为 `task.json` 即可生成无素材的范围说明报告。使用时将目标、标准和原因改为本次事实。补充真实证据之后才能改结果,不要复制示例冒充完成验收。
36
56
 
37
57
  ```json
38
58
  {
39
- "schemaVersion": 1,
59
+ "schemaVersion": 2,
40
60
  "task": {
41
61
  "id": "current-task",
42
- "title": "当前任务的核对结果",
43
- "scope": "说明本次核对范围与环境",
62
+ "title": "当前任务验收报告",
63
+ "scope": "填写本次版本、环境与目标范围",
44
64
  "materialKind": "current",
45
- "limitations": []
65
+ "limitations": ["尚未连接目标设备,不能确认真机行为。"]
46
66
  },
47
- "assets": [
48
- {
49
- "id": "observation",
50
- "kind": "text",
51
- "path": "observation.txt",
52
- "label": "实际观察记录",
53
- "origin": { "surface": "CLI", "capturedAt": null, "sourceRef": null }
54
- }
55
- ],
56
- "pages": [
57
- {
58
- "id": "result",
59
- "type": "text",
60
- "title": "本次实际观察到了什么",
61
- "description": "交代操作条件、实际动作与观察,不超过原始记录的证据范围。",
62
- "evidenceState": "observed",
63
- "items": [{ "assetId": "observation", "label": "结果原文" }],
64
- "sourceIds": []
65
- }
66
- ]
67
+ "report": {
68
+ "summary": "真机路径尚未执行,本报告不作通过结论。",
69
+ "checks": [
70
+ {
71
+ "id": "phone-flow",
72
+ "title": "手机完成目标操作",
73
+ "expected": "在目标真机完成操作并核对保存后的状态。",
74
+ "actual": "未连接目标设备,没有执行此路径。",
75
+ "execution": "not-run",
76
+ "outcome": "blocked",
77
+ "pageIds": []
78
+ }
79
+ ]
80
+ },
81
+ "assets": [],
82
+ "pages": []
67
83
  }
68
84
  ```
69
85
 
70
- ## 原始素材查看
71
-
72
- 正文图片、标题旁的原图/原文和来源链接都在阅读器内部打开,不依赖弹窗或页面跳转。图片可切换实际尺寸并滚动;关闭后保持原来的页码和焦点。原图与原文文件仍单独保留,复制成果目录后可直接使用。Backend 的 HTML 预览沙箱与真机阅读仍须通过实际入口核对,不能仅凭本地打开推断。
86
+ 有真实记录时按上方 asset/page 合同补充素材。文字项示例为 `{"assetId":"observation","label":"执行记录","summary":"填写该记录支持的实际观察与限制。"}`;check.pageIds 引用对应页的 id,而不是素材 id。
73
87
 
74
- ## 输出与错误
88
+ ## 交付预检、输出与错误
75
89
 
76
- `validate` 返回 JSON `{valid, pages, assets}`,不写文件。`build` 返回 `{output, index, pages}`。成功退出 0;输入、文件或运行错误退出 1,命令参数错误退出 2,stderr 给出字段或文件原因。`serve` 前台长驻,第一行返回 `{url,pid,directory,stop}`;Ctrl-C 正常停止。
90
+ - `validate` 返回 `{valid, pages, assets}`,只验证输入,不写文件;不证明可通过 Backend 交付。
91
+ - `build` 默认 `--target runweave`,生成临时成果后按 Runweave 的 HTML 1 MiB、单资源 5 MiB 上限检查完整清单。不可交付则退出 1 并指出文件,最终目录不落盘;不读取凭据或申请票据。
92
+ - 明确需要离线大文件时使用 `build --target portable`。仍保留原素材并返回 `deliveryCheck`,其中 ready:false 表示不能通过当前 Runweave 入口,不能当作已交付成功。
93
+ - `build` 返回 `{output, index, pages, target, deliveryCheck}`。`deliveryCheck` 含 `target`、`ready`、`filesChecked`、`issues`;本地通过不证明资源解码、浏览器加载或手机可达。
94
+ - 已生成报告可运行 `gallery.py preflight --directory <成果目录>`,输出本地检查 JSON,ready 为 false 时退出 1。详细入口与票据流程见 [预览与检查](delivery.md)。
95
+ - 成功退出 0;输入、文件或运行错误退出 1,命令参数错误退出 2,stderr 给出原因。`serve` 前台长驻,第一行返回 `{url,pid,directory,stop}`;Ctrl-C 正常停止。
77
96
 
78
- 输出文件:`index.html`、`reader.css`、`viewer.css`、`reader.js`、`task.json`、`manifest.json` 与 `assets/`。正文及关联文字内嵌进页面,不依赖 fetch 或外网;原始文字与图片也单独保留。task.json 保持输入合同,path 改为成果内相对路径。manifest 保存 schemaVersion、templateVersion、原始输入摘要及复制素材 SHA-256。
97
+ 输出文件:`index.html`、`reader.css`、`report.css`、`viewer.css`、`reader.js`、`task.json`、`manifest.json` 与 `assets/`。文字内嵌进页面,不依赖 fetch 或外网;原始文字与图片也单独保留。task.json 保持输入合同,path 改为成果内相对路径。manifest 保存 schemaVersion、templateVersion、原始输入摘要及复制素材 SHA-256。
79
98
 
80
99
  生成失败不留下最终目录,已有输出拒绝覆盖。生成器用同目标的临时锁串行化生成;进程被强杀可能留下 `.gallery-lock` 和 `.gallery-*`,核对没有相同目标构建在运行后,才清理这些本次临时文件或换新输出目录。不要删除旧成果绕过覆盖检查。
81
100
 
82
- 未知版本拒绝;不自动迁移。每份报告带模板副本,后续升级不改变旧报告。生成器只处理显式列出的素材,不自动爬取来源、执行命令或读整个历史。
101
+ 每份报告带模板副本,后续升级不改变旧报告。生成器只处理显式列出的素材,不自动爬取来源、执行命令或读整个历史。
102
+
103
+ ## 原始素材查看与本地服务
104
+
105
+ 正文图片、标题旁的原图/原文和来源链接都在阅读器内部打开,不依赖弹窗或页面跳转。图片可切换实际尺寸并滚动;关闭后保持原来的页码和焦点。原图与原文文件仍单独保留,复制成果目录后可直接使用。Backend 的 HTML 预览沙箱与真机阅读仍须通过实际入口核对,不能仅凭本地打开推断。
83
106
 
84
107
  `serve` 只绑定 `127.0.0.1`,只允许清单中的文件,不开放目录列表和写接口;根目录越界及符号链接逃逸均拒绝。素材路径不接受绝对路径、`..`、反斜线、冒号或远程 URL。页面文本转义显示,不支持任务传入 HTML。输入输出根目录应由当前用户控制,不将此本机工具部署为多用户公共服务。
@@ -21,6 +21,14 @@
21
21
 
22
22
  对照前核对目标、运行端、版本、尺寸与条件;不同条件必须说明。有结果图但没有前图时退回单图,不能生成假前图。需要真实运动或闪烁证据的任务不以截图模板替代原采集方式。
23
23
 
24
+ ## 验收总览与正文
25
+
26
+ 新报告先填写 report.summary:本次验证到什么程度、哪些结论受限。逐项列用户目标、预期、实际与对应证据,未执行和阻塞也必须列出。通过结论只适用于明确执行的范围;桌面 UI、浏览器模拟和真机路径分别判断。真实/Mock 是执行条件,不是证据缺失的代称。
27
+
28
+ 总览优先呈现失败、阻塞、未执行;不以“是否有截图”决定重要性。代码变更清单和技术实现按需作为辅助材料,不占据用户验收主线。不要把 Agent 自检记录写成用户已确认。
29
+
30
+ 文字页可用 item.summary 写面向用户的简短摘要;原始日志保留在素材与原文查看器中,不在摘要、正文、来源区反复粘贴全文。不根据文件后缀猜测总结,不改写或截断原始证据。
31
+
24
32
  ## 文案如何写
25
33
 
26
34
  标题说明本页要核对的结果;建议 12–24 个中文字。正文以 1–2 句交代“条件、动作、实际观察”,建议 40–100 字。软长度目标不覆盖准确性要求。
@@ -0,0 +1,151 @@
1
+ """Local checks for generated galleries; no credentials, network or HTML crawling."""
2
+ import json
3
+ from pathlib import Path
4
+ import re
5
+ import stat
6
+
7
+ # Keep the delivery limits aligned with backend/src/terminal/preview/html-preview.ts.
8
+ MAX_HTML_BYTES = 1024 * 1024
9
+ MAX_RESOURCE_BYTES = 5 * 1024 * 1024
10
+ MAX_MANIFEST_BYTES = 2 * 1024 * 1024
11
+ _TEMPLATE_VERSIONS = ('1.0.0', '1.1.0', '1.2.0')
12
+ _SHA256 = re.compile(r'[0-9a-fA-F]{64}')
13
+
14
+
15
+ def check_gallery(directory: Path) -> dict:
16
+ """Check only fixed files and explicit manifest assets, counting unique paths.
17
+
18
+ ready means these local files meet delivery limits, not that a browser loaded
19
+ their contents or a phone can reach the Backend. Empty asset lists are valid.
20
+ """
21
+ result = {'target': 'runweave-html', 'ready': False, 'filesChecked': 0, 'issues': []}
22
+
23
+ def issue(path, reason, **details):
24
+ result['issues'].append({'path': path, 'reason': reason, **details})
25
+
26
+ try:
27
+ root = directory.resolve(strict=True)
28
+ if not root.is_dir():
29
+ raise ValueError('Gallery root must be a directory')
30
+ except (OSError, RuntimeError, ValueError) as error:
31
+ issue(str(directory), str(error))
32
+ return result
33
+
34
+ checked = {}
35
+
36
+ def inspect(relative, limit):
37
+ if relative in checked:
38
+ return checked[relative]
39
+ checked[relative] = None
40
+ result['filesChecked'] += 1
41
+ if (not relative or '\\' in relative or '\x00' in relative or
42
+ any(part in ('', '.', '..') for part in relative.split('/'))):
43
+ issue(relative, 'Invalid relative file path; directory escapes are not allowed')
44
+ return None
45
+ try:
46
+ resolved = (root / relative).resolve(strict=True)
47
+ if not resolved.is_relative_to(root):
48
+ issue(relative, 'File or symlink escapes the gallery directory')
49
+ return None
50
+ info = resolved.stat()
51
+ if not stat.S_ISREG(info.st_mode):
52
+ issue(relative, 'Expected a regular file')
53
+ return None
54
+ if info.st_size > limit:
55
+ issue(relative, 'File exceeds local delivery size limit',
56
+ bytes=info.st_size, limitBytes=limit)
57
+ return None
58
+ except (OSError, RuntimeError, ValueError) as error:
59
+ issue(relative, 'Missing or inaccessible file: ' + str(error))
60
+ return None
61
+ checked[relative] = resolved
62
+ return resolved
63
+
64
+ for name in ('index.html', 'reader.css', 'reader.js', 'task.json', 'manifest.json'):
65
+ limit = (MAX_HTML_BYTES if name == 'index.html' else
66
+ MAX_MANIFEST_BYTES if name == 'manifest.json' else MAX_RESOURCE_BYTES)
67
+ inspect(name, limit)
68
+ manifest_path = checked['manifest.json']
69
+ if manifest_path is None:
70
+ return result
71
+
72
+ def unique_object(pairs):
73
+ obj = {}
74
+ for key, value in pairs:
75
+ if key in obj:
76
+ raise ValueError('Duplicate JSON key: ' + key)
77
+ obj[key] = value
78
+ return obj
79
+
80
+ def reject_constant(value):
81
+ raise ValueError('Invalid JSON constant: ' + value)
82
+
83
+ try:
84
+ # Bound the read as well as the stat, even if the file changed meanwhile.
85
+ with manifest_path.open('rb') as stream:
86
+ data = stream.read(MAX_MANIFEST_BYTES + 1)
87
+ if len(data) > MAX_MANIFEST_BYTES:
88
+ issue('manifest.json', 'Manifest exceeds local parsing limit',
89
+ bytes=len(data), limitBytes=MAX_MANIFEST_BYTES)
90
+ return result
91
+ manifest = json.loads(data.decode('utf-8'), object_pairs_hook=unique_object,
92
+ parse_constant=reject_constant)
93
+ except (OSError, ValueError, RecursionError) as error:
94
+ issue('manifest.json', 'Invalid or unreadable manifest JSON: ' + str(error))
95
+ return result
96
+ if not isinstance(manifest, dict):
97
+ issue('manifest.json', 'Manifest must be an object')
98
+ return result
99
+ if type(manifest.get('schemaVersion')) is not int or manifest['schemaVersion'] != 1:
100
+ issue('manifest.json', 'Expected manifest schemaVersion 1')
101
+ version = manifest.get('templateVersion')
102
+ if version not in _TEMPLATE_VERSIONS:
103
+ issue('manifest.json', 'Unsupported or missing templateVersion')
104
+ if version in ('1.1.0', '1.2.0'):
105
+ inspect('viewer.css', MAX_RESOURCE_BYTES)
106
+ if version == '1.2.0':
107
+ inspect('report.css', MAX_RESOURCE_BYTES)
108
+ if not isinstance(manifest.get('inputSha256'), str) or not _SHA256.fullmatch(manifest['inputSha256']):
109
+ issue('manifest.json', 'inputSha256 must be a SHA-256 hex string')
110
+ assets = manifest.get('assets')
111
+ if not isinstance(assets, list):
112
+ issue('manifest.json', 'assets must be an array (it may be empty)')
113
+ return result
114
+ ids, paths = set(), set()
115
+ for index, asset in enumerate(assets):
116
+ location = f'manifest.json assets[{index}]'
117
+ if not isinstance(asset, dict):
118
+ issue('manifest.json', location + ' must be an object')
119
+ continue
120
+ asset_id = asset.get('id')
121
+ if not isinstance(asset_id, str) or not asset_id.strip():
122
+ issue('manifest.json', location + '.id must be a nonempty string')
123
+ elif asset_id in ids:
124
+ issue('manifest.json', location + '.id is duplicated')
125
+ else:
126
+ ids.add(asset_id)
127
+ if not isinstance(asset.get('sha256'), str) or not _SHA256.fullmatch(asset['sha256']):
128
+ issue('manifest.json', location + '.sha256 must be a SHA-256 hex string')
129
+ relative = asset.get('path')
130
+ if not isinstance(relative, str) or not relative:
131
+ issue('manifest.json', location + '.path must be a nonempty relative path')
132
+ continue
133
+ if relative in paths:
134
+ issue(relative, 'Duplicate manifest asset path')
135
+ paths.add(relative)
136
+ inspect(relative, MAX_RESOURCE_BYTES)
137
+ result['ready'] = not result['issues']
138
+ return result
139
+
140
+
141
+ def require_ready(result: dict) -> None:
142
+ """Stop delivery on failed local checks and identify the affected files."""
143
+ if result.get('ready') is True:
144
+ return
145
+ details = []
146
+ for item in result.get('issues', []):
147
+ description = f'{item["path"]}: {item["reason"]}'
148
+ if 'bytes' in item and 'limitBytes' in item:
149
+ description += f' ({item["bytes"]} bytes; limit {item["limitBytes"]} bytes)'
150
+ details.append(description)
151
+ raise ValueError('Runweave HTML local preflight failed: ' + ('; '.join(details) or 'not ready'))
@@ -16,7 +16,10 @@ import zlib
16
16
  from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
17
17
  from urllib.parse import unquote, urlsplit
18
18
 
19
- TEMPLATE_VERSION = '1.1.0'
19
+ sys.dont_write_bytecode = True
20
+ from delivery_check import check_gallery, require_ready
21
+
22
+ TEMPLATE_VERSION = '1.2.0'
20
23
  TEMPLATES = Path(__file__).resolve().parent.parent / 'templates'
21
24
  ID = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$')
22
25
  MAX_FILE = 32 * 1024 * 1024
@@ -136,8 +139,10 @@ def parse_json(data):
136
139
  def load_task(root, input_name):
137
140
  raw = read_file(local_file(root, input_name), MAX_TEXT)
138
141
  task = parse_json(raw)
139
- fields(task, 'root', ['schemaVersion', 'task', 'assets', 'pages'])
140
- require(type(task['schemaVersion']) is int and task['schemaVersion'] == 1, 'schemaVersion', '仅支持版本 1')
142
+ require(isinstance(task, dict), 'root', '需要对象')
143
+ version = task.get('schemaVersion')
144
+ require(type(version) is int and version in (1, 2), 'schemaVersion', '仅支持版本 1/2')
145
+ fields(task, 'root', ['schemaVersion', 'task', 'assets', 'pages'] + (['report'] if version == 2 else []))
141
146
  meta = task['task']
142
147
  fields(meta, 'task', ['id', 'title', 'scope', 'materialKind', 'limitations'])
143
148
  identifier(meta['id'], 'task.id')
@@ -170,7 +175,8 @@ def load_task(root, input_name):
170
175
  parse_json(data)
171
176
  assets[asset['id']] = asset
172
177
  payloads[asset['id']] = (data, suffix)
173
- require(isinstance(task['pages'], list) and len(task['pages']) > 0, 'pages', '至少需要一页')
178
+ require(isinstance(task['pages'], list), 'pages', '需要数组')
179
+ require(version == 2 or len(task['pages']) > 0, 'pages', '版本 1 至少需要一页')
174
180
  seen = set()
175
181
  for i, page in enumerate(task['pages']):
176
182
  where = f'pages[{i}]'
@@ -190,7 +196,7 @@ def load_task(root, input_name):
190
196
  require(low <= len(items) <= high, where, f'当前页型需要 {low}–{high} 个素材项')
191
197
  for j, item in enumerate(items):
192
198
  iw = where + f'.items[{j}]'
193
- fields(item, iw, ['assetId', 'label'], ['alt', 'crop'])
199
+ fields(item, iw, ['assetId', 'label'], ['alt', 'crop'] + (['summary'] if version == 2 else []))
194
200
  string(item['assetId'], iw + '.assetId')
195
201
  string(item['label'], iw + '.label')
196
202
  require(item['assetId'] in assets, iw, '素材 ID 不存在')
@@ -201,6 +207,9 @@ def load_task(root, input_name):
201
207
  string(item.get('alt'), iw + '.alt')
202
208
  else:
203
209
  require('crop' not in item and 'alt' not in item, iw, '文字项不接受 crop/alt')
210
+ if 'summary' in item:
211
+ require(kind == 'text', iw, 'summary 仅用于文字素材摘要')
212
+ string(item['summary'], iw + '.summary')
204
213
  if 'crop' in item:
205
214
  crop = item['crop']
206
215
  fields(crop, iw + '.crop', ['x', 'y', 'width', 'height'])
@@ -213,14 +222,47 @@ def load_task(root, input_name):
213
222
  strings(page['sourceIds'], where + '.sourceIds')
214
223
  for source_id in page['sourceIds']:
215
224
  require(source_id in assets and assets[source_id]['kind'] == 'text', where, 'sourceIds 只能引用存在的文字素材')
225
+ if version == 2:
226
+ validate_report(task['report'], task['pages'])
216
227
  return task, payloads, image_sizes, hashlib.sha256(raw).hexdigest()
217
228
 
218
229
 
230
+ def validate_report(report, pages):
231
+ fields(report, 'report', ['summary', 'checks'])
232
+ string(report['summary'], 'report.summary')
233
+ require(isinstance(report['checks'], list) and report['checks'], 'report.checks', '至少需要一项验收目标')
234
+ by_id = {page['id']: page for page in pages}
235
+ seen = set()
236
+ for i, check in enumerate(report['checks']):
237
+ where = f'report.checks[{i}]'
238
+ fields(check, where, ['id', 'title', 'expected', 'actual', 'execution', 'outcome', 'pageIds'])
239
+ identifier(check['id'], where + '.id')
240
+ require(check['id'] not in seen, where, '重复验收项 ID')
241
+ seen.add(check['id'])
242
+ for key in ('title', 'expected', 'actual'):
243
+ string(check[key], where + '.' + key)
244
+ execution, outcome = check['execution'], check['outcome']
245
+ require(execution in ('real', 'mock', 'not-run'), where, 'execution 需要 real/mock/not-run')
246
+ require(outcome in ('pass', 'fail', 'blocked', 'not-run'), where, 'outcome 需要 pass/fail/blocked/not-run')
247
+ strings(check['pageIds'], where + '.pageIds')
248
+ require(len(set(check['pageIds'])) == len(check['pageIds']), where, '重复证据页 ID')
249
+ require(all(id_ in by_id for id_ in check['pageIds']), where, '证据页 ID 不存在')
250
+ if outcome == 'not-run':
251
+ require(execution == 'not-run', where, '未执行结果不能声明已实际执行或 Mock 执行')
252
+ if execution == 'not-run':
253
+ require(outcome in ('not-run', 'blocked'), where, '未执行不能声明通过或失败')
254
+ if outcome in ('pass', 'fail'):
255
+ evidence = [by_id[id_]['evidenceState'] for id_ in check['pageIds']]
256
+ require('observed' in evidence, where, '通过或失败需要关联实际观察的证据页')
257
+ if outcome == 'pass':
258
+ require(all(state == 'observed' for state in evidence), where, '证据不足或未执行的页不能支持通过结论')
259
+
260
+
219
261
  def dump(path, value):
220
262
  path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
221
263
 
222
264
 
223
- def build(loaded, output):
265
+ def build(loaded, output, target='runweave'):
224
266
  task, payloads, sizes, input_hash = loaded
225
267
  output = output.absolute()
226
268
  output.parent.mkdir(parents=True, exist_ok=True)
@@ -257,10 +299,13 @@ def build(loaded, output):
257
299
  before, after = template.split('@@DATA@@')
258
300
  before = before.replace('@@TITLE@@', html.escape(task['task']['title']))
259
301
  (stage / 'index.html').write_text(before + embedded + after, encoding='utf-8')
260
- for name in ('reader.css', 'reader.js', 'viewer.css'):
302
+ for name in ('reader.css', 'reader.js', 'viewer.css', 'report.css'):
261
303
  shutil.copyfile(TEMPLATES / name, stage / name)
262
304
  dump(stage / 'task.json', snapshot)
263
305
  dump(stage / 'manifest.json', manifest)
306
+ delivery = check_gallery(stage)
307
+ if target == 'runweave':
308
+ require_ready(delivery)
264
309
  require(not output.exists() and not output.is_symlink(), 'output', '目标已存在')
265
310
  stage.rename(output)
266
311
  stage = None
@@ -268,7 +313,8 @@ def build(loaded, output):
268
313
  if stage is not None:
269
314
  shutil.rmtree(stage)
270
315
  lock.unlink()
271
- return {'output': str(output), 'index': str(output / 'index.html'), 'pages': len(task['pages'])}
316
+ return {'output': str(output), 'index': str(output / 'index.html'), 'pages': len(task['pages']),
317
+ 'target': target, 'deliveryCheck': delivery}
272
318
 
273
319
 
274
320
  def serve(directory, port):
@@ -276,8 +322,10 @@ def serve(directory, port):
276
322
  manifest = parse_json(read_file(root / 'manifest.json', MAX_TEXT))
277
323
  require(manifest.get('schemaVersion') == 1 and isinstance(manifest.get('assets'), list), 'directory', '不是生成的成果目录')
278
324
  allowed = {'index.html', 'reader.css', 'reader.js', 'task.json', 'manifest.json'}
279
- if manifest.get('templateVersion') == '1.1.0':
325
+ if manifest.get('templateVersion') in ('1.1.0', TEMPLATE_VERSION):
280
326
  allowed.add('viewer.css')
327
+ if manifest.get('templateVersion') == TEMPLATE_VERSION:
328
+ allowed.add('report.css')
281
329
  allowed.update(asset['path'] for asset in manifest['assets'])
282
330
  for relative in allowed:
283
331
  local_file(root, relative)
@@ -324,7 +372,7 @@ def serve(directory, port):
324
372
 
325
373
 
326
374
  def main():
327
- parser = argparse.ArgumentParser(description='本地图文验收阅读器:validate / build / serve')
375
+ parser = argparse.ArgumentParser(description='本地图文验收阅读器:validate / build / preflight / serve')
328
376
  sub = parser.add_subparsers(dest='command', required=True)
329
377
  for name in ('validate', 'build'):
330
378
  command = sub.add_parser(name)
@@ -332,6 +380,10 @@ def main():
332
380
  command.add_argument('--input', default='task.json')
333
381
  if name == 'build':
334
382
  command.add_argument('--output', type=Path, required=True)
383
+ command.add_argument('--target', choices=('runweave', 'portable'), default='runweave',
384
+ help='默认按 Runweave 预览限额预检;portable 只生成离线成果并报告限制')
385
+ command = sub.add_parser('preflight')
386
+ command.add_argument('--directory', type=Path, required=True)
335
387
  command = sub.add_parser('serve')
336
388
  command.add_argument('--directory', type=Path, required=True)
337
389
  command.add_argument('--port', type=int, default=0)
@@ -341,8 +393,12 @@ def main():
341
393
  require(0 <= args.port <= 65535, 'port', '端口范围 0–65535')
342
394
  serve(args.directory, args.port)
343
395
  return
396
+ if args.command == 'preflight':
397
+ result = check_gallery(args.directory)
398
+ print(json.dumps(result, ensure_ascii=False))
399
+ sys.exit(0 if result['ready'] else 1)
344
400
  loaded = load_task(args.input_root.resolve(strict=True), args.input)
345
- result = build(loaded, args.output) if args.command == 'build' else {'valid': True, 'pages': len(loaded[0]['pages']), 'assets': len(loaded[0]['assets'])}
401
+ result = build(loaded, args.output, args.target) if args.command == 'build' else {'valid': True, 'pages': len(loaded[0]['pages']), 'assets': len(loaded[0]['assets'])}
346
402
  print(json.dumps(result, ensure_ascii=False))
347
403
  except (Invalid, OSError, ValueError, KeyError, TypeError) as error:
348
404
  print(f'gallery: {error}', file=sys.stderr)
@@ -9,6 +9,7 @@ import sys
9
9
  import urllib.parse
10
10
  sys.dont_write_bytecode = True
11
11
  from common import base_url, http, write_json
12
+ from delivery_check import MAX_HTML_BYTES, check_gallery, require_ready
12
13
 
13
14
 
14
15
  def configure_parser(parser, default_kind='auto'):
@@ -25,7 +26,8 @@ def configure_parser(parser, default_kind='auto'):
25
26
 
26
27
 
27
28
  def preview(args):
28
- file = args.file.resolve(strict=True)
29
+ requested = args.file.absolute()
30
+ file = requested.resolve(strict=True)
29
31
  if not file.is_file():
30
32
  raise ValueError('Preview input must be a regular file')
31
33
  kind = args.kind
@@ -38,6 +40,23 @@ def preview(args):
38
40
  raise FileExistsError('Use a new output path for each preview ticket')
39
41
  if not args.output.parent.is_dir():
40
42
  raise ValueError('Preview output parent must already exist')
43
+ local_check = None
44
+ if kind == 'html':
45
+ manifest = requested.parent / 'manifest.json'
46
+ if requested.name == 'index.html' and (manifest.exists() or manifest.is_symlink()):
47
+ local_check = check_gallery(requested.parent)
48
+ local_check['scope'] = 'gallery-fixed-files-and-manifest-assets'
49
+ else:
50
+ size = file.stat().st_size
51
+ issues = []
52
+ if size > MAX_HTML_BYTES:
53
+ issues.append({'path': str(requested), 'reason': 'HTML entry exceeds preview size limit',
54
+ 'bytes': size, 'limitBytes': MAX_HTML_BYTES})
55
+ local_check = {'target': 'runweave-html', 'ready': not issues, 'filesChecked': 1,
56
+ 'issues': issues, 'scope': 'html-entry-only',
57
+ 'resources': 'not-checked; arbitrary HTML resources are not inspected'}
58
+ # Fail locally before accessing credentials or contacting either Backend URL.
59
+ require_ready(local_check)
41
60
  if args.token_env:
42
61
  if not args.backend:
43
62
  raise ValueError('--token-env requires --backend')
@@ -89,7 +108,7 @@ def preview(args):
89
108
  status, _, content = http(url)
90
109
  if status != 200 or content != file.read_bytes():
91
110
  raise ValueError('Anonymous HTML GET did not match local bytes')
92
- receipt.update(htmlStatus=200, htmlMatchesLocal=True,
111
+ receipt.update(htmlStatus=200, htmlMatchesLocal=True, localPreflight=local_check,
93
112
  resources='not-verified; check styles/scripts/images in a real browser')
94
113
  write_json(args.output, receipt, private=True)
95
114
  print(json.dumps({k: v for k, v in receipt.items() if k != 'url'}, ensure_ascii=False))
@@ -128,14 +128,6 @@ ul {
128
128
  margin-top: 8px;
129
129
  max-width: 960px;
130
130
  }
131
- .masthead details {
132
- font-size: 13px;
133
- color: var(--muted);
134
- margin-top: 10px;
135
- }
136
- .masthead details ul {
137
- margin: 6px 0;
138
- }
139
131
  .workspace {
140
132
  max-width: 1400px;
141
133
  margin: auto;
@@ -361,11 +353,6 @@ figcaption a {
361
353
  .source-item p {
362
354
  margin-top: 4px;
363
355
  }
364
- .source-item pre {
365
- margin-top: 8px;
366
- max-height: 32vh;
367
- font-size: 12px;
368
- }
369
356
  .navigation {
370
357
  position: fixed;
371
358
  bottom: 0;
@@ -516,7 +503,7 @@ figcaption a {
516
503
  }
517
504
  .text-evidence {
518
505
  padding: 12px;
519
- font-size: 12px;
506
+ font-size: 14px;
520
507
  }
521
508
  .notice {
522
509
  bottom: 82px;
@@ -7,6 +7,7 @@
7
7
  <link rel="icon" href="data:," />
8
8
  <title>@@TITLE@@</title>
9
9
  <link rel="stylesheet" href="reader.css" />
10
+ <link rel="stylesheet" href="report.css" />
10
11
  <link rel="stylesheet" href="viewer.css" />
11
12
  <script id="gallery-data" type="application/json">
12
13
  @@DATA@@
@@ -25,13 +26,31 @@
25
26
  <button id="expand" type="button" aria-pressed="false">展开全部</button>
26
27
  </div>
27
28
  <p id="task-scope"></p>
28
- <details id="task-limits" hidden>
29
- <summary>本次材料的范围</summary>
30
- <ul></ul>
31
- </details>
32
29
  </header>
30
+ <section
31
+ id="report-overview"
32
+ class="report-overview"
33
+ aria-labelledby="report-heading"
34
+ >
35
+ <h2 id="report-heading" tabindex="-1">验收总览</h2>
36
+ <p id="report-summary" class="description"></p>
37
+ <section
38
+ id="task-limits"
39
+ class="limits"
40
+ aria-label="本次范围与限制"
41
+ hidden
42
+ >
43
+ <h3>本次范围与限制</h3>
44
+ <ul></ul>
45
+ </section>
46
+ <p class="report-note">
47
+ 以下为报告作者记录的自检结果,不代表用户已确认验收。有观察材料不等于通过。
48
+ </p>
49
+ <ul id="report-checks" class="report-checks"></ul>
50
+ </section>
33
51
  <div class="workspace">
34
52
  <aside class="directory">
53
+ <button id="back-overview" type="button">返回验收总览</button>
35
54
  <details id="directory-details" open>
36
55
  <summary>阅读目录</summary>
37
56
  <nav aria-label="页面目录" id="directory"></nav>