Răsfoiți Sursa

update fmode image set skill to v0.2.0

Yi Jiarui 1 lună în urmă
părinte
comite
c85159420a
100 a modificat fișierele cu 9014 adăugiri și 346 ștergeri
  1. 2 2
      claude-code/fmode-image-set/.claude-plugin/plugin.json
  2. 1 0
      claude-code/fmode-image-set/.gitignore
  3. 9 0
      claude-code/fmode-image-set/.npmignore
  4. 86 59
      claude-code/fmode-image-set/README.md
  5. 1 1
      claude-code/fmode-image-set/bin/fmode-image-set.js
  6. 47 43
      claude-code/fmode-image-set/docs/capability-map.md
  7. 6 22
      claude-code/fmode-image-set/docs/customer-quickstart.md
  8. 48 44
      claude-code/fmode-image-set/docs/deliverables/image-set-result.md
  9. 57 41
      claude-code/fmode-image-set/docs/live-manual-acceptance-checklist.md
  10. 23 0
      claude-code/fmode-image-set/mcp/src/core/run-paths.mjs
  11. 4 0
      claude-code/fmode-image-set/mcp/src/core/run-paths.test.mjs
  12. 63 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/adaptive-overlay-compositor-v2.test.mjs
  13. 158 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/adaptive-overlay-compositor.mjs
  14. 49 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/benchmark-harness.mjs
  15. 164 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/commercial-copy-compiler.mjs
  16. 121 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/commercial-pipeline.test.mjs
  17. 126 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/control-plane.test.mjs
  18. 36 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/delivery-profile-compiler.mjs
  19. 108 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/design-ir-compiler.mjs
  20. 62 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/design-recipe-registry.mjs
  21. 42 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/design-token-compiler.mjs
  22. 106 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/designed-asset-runner.mjs
  23. 49 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/designed-asset-runner.test.mjs
  24. 22 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/font-resolver.mjs
  25. 84 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/layout-compositor.mjs
  26. 13 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/page-spec-compiler.mjs
  27. 37 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/pango-text-metrics.mjs
  28. 13 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/pango-text-metrics.test.mjs
  29. 36 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/product-evidence-gate.mjs
  30. 40 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/product-evidence-gate.test.mjs
  31. 122 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/product-layer-builder.mjs
  32. 128 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/prompt-contract-compiler.mjs
  33. 109 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/quality-repair-compiler.mjs
  34. 29 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/recipe-svg-renderer.mjs
  35. 129 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/seven-page-design-ir-compiler.mjs
  36. 37 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/seven-page-design-ir-compiler.test.mjs
  37. 20 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/text-layout-v2.test.mjs
  38. 55 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/text-layout.mjs
  39. 35 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/view-coverage-compiler.mjs
  40. 51 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/view-strategy-router.mjs
  41. 20 0
      claude-code/fmode-image-set/mcp/src/features/designed-asset/view-strategy-router.test.mjs
  42. 49 0
      claude-code/fmode-image-set/mcp/src/features/image-set/claude-authored-v3-pipeline.integration.test.mjs
  43. 870 24
      claude-code/fmode-image-set/mcp/src/features/image-set/image-set-runner.mjs
  44. 387 3
      claude-code/fmode-image-set/mcp/src/features/image-set/image-set-runner.test.mjs
  45. 95 0
      claude-code/fmode-image-set/mcp/src/features/image-set/platform-design-profile-registry.mjs
  46. 28 0
      claude-code/fmode-image-set/mcp/src/features/image-set/platform-design-profile-registry.test.mjs
  47. 16 2
      claude-code/fmode-image-set/mcp/src/features/image-set/product-vision-client.mjs
  48. 138 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-design-bridge.mjs
  49. 118 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-design-bridge.test.mjs
  50. 127 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-outcome-compiler.mjs
  51. 83 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-outcome-compiler.test.mjs
  52. 667 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-plan-compiler.mjs
  53. 249 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-plan-compiler.test.mjs
  54. 76 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-result-verifier.mjs
  55. 22 0
      claude-code/fmode-image-set/mcp/src/features/image-set/production-result-verifier.test.mjs
  56. 153 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-asset-plan-compiler.mjs
  57. 61 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-asset-plan-compiler.test.mjs
  58. 126 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-contract-schemas.mjs
  59. 14 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-contract-schemas.test.mjs
  60. 91 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-copy-plan.mjs
  61. 25 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-copy-plan.test.mjs
  62. 73 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-creative-planner.mjs
  63. 57 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-creative-planner.test.mjs
  64. 104 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-design-postprocessor.mjs
  65. 46 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-design-postprocessor.test.mjs
  66. 104 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-layout-region-detector.mjs
  67. 36 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-layout-region-detector.test.mjs
  68. 148 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-page-template-registry.mjs
  69. 36 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-page-template-registry.test.mjs
  70. 179 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-pipeline.integration.test.mjs
  71. 153 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-set-blueprint.mjs
  72. 56 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-set-blueprint.test.mjs
  73. 284 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-v3-contracts.mjs
  74. 54 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-v3-contracts.test.mjs
  75. 43 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-v3-fixtures.mjs
  76. 83 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-visual-observer.mjs
  77. 26 0
      claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-visual-observer.test.mjs
  78. 253 0
      claude-code/fmode-image-set/mcp/src/features/image-set/scene-affordance-policy.mjs
  79. 79 0
      claude-code/fmode-image-set/mcp/src/features/image-set/scene-affordance-policy.test.mjs
  80. 61 0
      claude-code/fmode-image-set/mcp/src/features/image-set/set-diversity-policy.mjs
  81. 148 0
      claude-code/fmode-image-set/mcp/src/features/image-set/set-quality-verifier.mjs
  82. 45 0
      claude-code/fmode-image-set/mcp/src/features/image-set/set-quality-verifier.test.mjs
  83. 42 0
      claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-annotated-renderer.mjs
  84. 19 0
      claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-annotated-renderer.test.mjs
  85. 28 0
      claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-copy-plan.mjs
  86. 12 0
      claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-copy-plan.test.mjs
  87. 105 12
      claude-code/fmode-image-set/mcp/src/features/image-set/validate-plan.mjs
  88. 89 2
      claude-code/fmode-image-set/mcp/src/features/image-set/validate-plan.test.mjs
  89. 74 0
      claude-code/fmode-image-set/mcp/src/features/image-set/view-strategy-compatibility.mjs
  90. 23 0
      claude-code/fmode-image-set/mcp/src/features/image-set/view-strategy-compatibility.test.mjs
  91. 9 5
      claude-code/fmode-image-set/mcp/src/providers/claude-attachment-client.mjs
  92. 53 1
      claude-code/fmode-image-set/mcp/src/providers/claude-attachment-client.test.mjs
  93. 35 3
      claude-code/fmode-image-set/mcp/src/providers/commerce-vision-provider.mjs
  94. 41 0
      claude-code/fmode-image-set/mcp/src/providers/commerce-vision-provider.test.mjs
  95. 101 60
      claude-code/fmode-image-set/mcp/src/providers/jimeng-v4-client.mjs
  96. 85 0
      claude-code/fmode-image-set/mcp/src/providers/jimeng-v4-client.test.mjs
  97. 43 6
      claude-code/fmode-image-set/mcp/src/server.mjs
  98. 211 16
      claude-code/fmode-image-set/mcp/src/tools/image-set-run.mjs
  99. 230 0
      claude-code/fmode-image-set/mcp/src/tools/image-set-run.test.mjs
  100. 3 0
      claude-code/fmode-image-set/mcp/test-fixtures/task-0016-coat-failure/README.md

+ 2 - 2
claude-code/fmode-image-set/.claude-plugin/plugin.json

@@ -1,7 +1,7 @@
 {
   "name": "fmode-image-set",
-  "description": "Product-truth-first ecommerce image-set planning and generation for Claude Code, with visual auditing, seven-slot prompt planning, Jimeng execution, recovery, cost control, and delivery integrity checks.",
-  "version": "0.1.0",
+  "description": "Reference-first product three-view, setting-board, and seven-image ecommerce workflow with experimental typography output for Claude Code.",
+  "version": "0.2.0",
   "author": {
     "name": "Fmode"
   }

+ 1 - 0
claude-code/fmode-image-set/.gitignore

@@ -5,4 +5,5 @@ outputs/
 .env
 .env.local
 .tmp/
+**/.idea/
 *.tgz

+ 9 - 0
claude-code/fmode-image-set/.npmignore

@@ -0,0 +1,9 @@
+node_modules/
+生成结果/
+memory/
+outputs/
+.tmp/
+**/.idea/**
+.env
+.env.local
+*.tgz

+ 86 - 59
claude-code/fmode-image-set/README.md

@@ -1,99 +1,126 @@
 # Fmode Image Set
 
-面向 Claude Code 的电商商品套图 Skill。用户提供一张商品图和一句需求,系统完成商品事实审计、七图规划、逐图 Prompt 校验,并在用户明确授权后调用即梦生成
+面向VSCode Claude Code的Claude主导、平台特化电商七图生产线。用户提供一张商品图和一句自然需求;Qwen只提供视觉观察,Claude按Skill框架编写动态商品设定板和七份自包含单图合同,MCP确定性执行,并保留一条Sharp/Pango实验性后置文字链路
 
-## 能力
+## 默认流程
 
-- 识别商品可见事实,分离推断与未知项;
-- 锁定商品外观、Logo、包装文字和不可新增结构;
-- 规划 1 张主图、2 张场景图、2 张卖点图、2 张购买决策图;
-- 校验卖点证据、角色配额、构图多样性和 Prompt 完整性;
-- 串行调用即梦并下载成品;
-- 对生成商品进行身份复核;
-- 支持同一运行恢复、费用上限和交付完整性审计。
+```text
+start:原图 → Qwen描述性视觉观察 → Claude动态设定板方案
+generate_references:原图 → 固定三视图 + 动态无字视觉设定板 + 中文标注设定板
+Qwen联合观察实际参考图 → Claude平台特化七页方案和PromptModules
+generate_set:01白底 + 02/03卖点 + 04/05场景 + 06/07购买决策
+七份自包含Prompt → 七张无新增文字视觉底图
+实际区域识别 + CopyPlan → 七张实验性后置文字版本
+```
+
+七图生图阶段只引用三视图和无字视觉设定板,不输入原图。Qwen观察不是创意规划或商品审核;区域识别只用于排版避让;默认流程不启用旧式内容质量门禁或自动返修。
+
+## 当前质量状态
+
+- V3流程、结构校验、哈希绑定、费用记录、恢复和后置文字渲染已具备自动测试;
+- 用户真实验收结论是:当前“无字底图 + Sharp/Pango后置文字”的视觉效果很差,未达到商业设计或竞品套图质量;
+- 后置文字文件只作为实验性兼容输出和后续改造基线,不得称为最终设计成品、商业可用成品或质量已通过;
+- 交付时必须同时保留无字视觉底图,并把底图质量与实验性后置文字质量分别说明。
 
-## 目录
+## 核心能力
+
+- 当前VSCode Claude唯一附件协议级优先;
+- 修正为水平摄影机方位的45度观察三视图;
+- 图片模型不生成新增中文和参数;
+- Qwen视觉辅助、Claude创意主导和同run哈希复用;
+- 白底、卖点、场景、购买决策定位模板族;
+- 淘宝、京东、抖音电商、拼多多和通用平台设计Profile;
+- 七份完整、自包含的独立单图Prompt;
+- 商品、人脸、人体、Logo、包装原字、关键结构和安全区几何识别;
+- Sharp/Pango字形度量、中文换行、字号搜索和语义完整性检查(工程实现;视觉质量未通过真实验收);
+- 安全区冲突时使用信息面板或扩展画布,不裁切商品硬叠字;
+- 串行图片提交、HMAC状态、保守恢复和未知提交不重提。
+
+## 目录职责
 
 ```text
-.claude-plugin/          Claude Code 插件元数据
-bin/                     安装与命令行入口
-docs/                    能力地图、快速开始、验收和交付物合同
-mcp/src/core/            路径、状态签名和结果信封
-mcp/src/features/        套图编排、视觉理解和计划校验
-mcp/src/providers/       Claude 附件、视觉模型、七牛、即梦适配
-mcp/src/tools/           MCP/CLI 薄入口
-memory-templates/        默认套图偏好模板
-scripts/                 smoke 与 acceptance
-skills/fmode-image-set/  Skill 入口、references 和 Agent 元数据
+.claude-plugin/                 # Claude Code插件元数据
+bin/                            # 全局/工作区安装CLI与复制边界
+docs/                           # 用户入口、能力地图、交付合同和手工验收说明
+mcp/src/core/                   # 配置、状态、费用、恢复、Provider通用基础设施
+mcp/src/providers/              # Qwen、即梦、七牛等外部服务适配器
+mcp/src/tools/                  # MCP工具参数转换、消息和入口编排
+mcp/src/features/image-set/     # V2/V3套图规划、参考资产、生成、排版和恢复主链
+mcp/src/features/designed-asset/# 三页设计资产兼容链
+mcp/test-fixtures/              # 可重复的失败/恢复测试夹具
+memory-templates/               # 安装到工作区的长期记忆模板
+scripts/                        # 测试、安装、MCP、打包和验收脚本
+skills/fmode-image-set/         # Skill入口、代理元数据和按需加载参考合同
 ```
 
-## 安装
+`node_modules/`、`生成结果/`、`.idea/`、环境文件和临时目录不是交付包内容;测试与失败夹具属于发布包,用于安装后回归和问题复现。
 
-当前目录检查:
+## 安装与验证
 
 ```powershell
 npm install
+npm test
 npm run install:check
 npm run smoke:package
 npm run mcp:smoke
-```
-
-安装到当前 Claude Code 工作区:
-
-```powershell
+npm run acceptance
 node bin/fmode-image-set.js workspace --smoke
 ```
 
-安装后会写入:
-
-```text
-.mcp.json
-.claude/skills/fmode-image-set/   完整交付包,同时包含根级 Skill 发现入口
-```
+安装目标:`.claude/skills/fmode-image-set/`。
 
 ## 使用
 
-在 Claude Code 中上传商品图并说:
+上传一张商品图,然后说:
 
 ```text
-给这款商品做一套高级、适合电商详情页的七图
+给这款商品做一套适合淘宝详情页的中文七图,直接生成并交付,过程中不用再问我
 ```
 
-只规划、不生图:
-
-```text
-先帮我规划这套图,不要实际生图。
+普通套图由Claude在同一用户请求内连续调用三个内部阶段;用户无需参与阶段切换:
+
+```yaml
+action: start
+production:
+  instruction: 用户当前需求
+  platform: taobao
+  market: CN
+  targetLanguage: zh-CN
+  aspectRatio: "1:1"
+workspaceRoot: 当前项目绝对路径
+cwd: 当前项目绝对路径
 ```
 
-## MCP 工具
+随后Claude根据返回观察编写`referenceAuthoring`调用`generate_references`,再根据参考图观察编写`setPlan`调用`generate_set`。完整字段见`skills/fmode-image-set/SKILL.md`。
 
-- `fmode_image_set_run`
-  - `start`:创建运行并完成商品视觉理解;
-  - `execute`:在计划通过且用户授权后生图;
-  - `resume`:恢复同一个运行;
-  - `status`:只读查看状态。
-- `fmode_image_set_validate`
-  - 校验 `prompt-plan.json`,不调用生图模型。
+用户如同时提供商品名称、卖点、规格或官方文案,可分别传入 `productName`、`sellingPoints`、`specifications` 和 `officialCopy`;未提供时不得虚构事实参数。
 
 ## 运行产物
 
-默认写入项目目录:
-
 ```text
 生成结果/电商七图/<run_id>/
 ├── 商品附件/
-├── 成品图/
-├── vision-result.json
+├── 参考资产/
+├── 成品图/                         # 七张无新增文字视觉底图
+├── 平台设计成品V2/                 # 历史目录名:七张实验性后置文字版本与manifest
+├── reference-first-request.json
+├── reference-asset-plan.json
+├── reference-asset-result.json
+├── set-blueprint.json
 ├── prompt-plan.json
-├── reference-assets.json
-└── jimeng-result.json
+├── jimeng-result.json
+├── copy-plan.json
+├── layout-regions.json
+├── layout-report.json
+└── reference-first-result.json
 ```
 
-## 安全边界
+## 费用和边界
 
-- 未经用户明确授权不执行付费生图
-- 默认费用上限为 1.54 元
+- 当前图片Provider单图 `0.22` 元,2+7共九张预计 `1.98` 元
+- 原图/参考图视觉观察和区域识别模型调用单独记录usage,价格取决于本机配置
 - `submission_unknown` 不自动重提;
-- 商品身份复核失败的图片不作为合格交付;
-- 不复制其他运行的图片补齐当前运行;
-- Token 不进入计划、报告或日志。
+- 已完成阶段在同run恢复时不重复付费调用;
+- 平台Profile是项目设计策略,不是平台官方规范或发布合规保证;
+- 技术完成不代表商品真实性、内容质量或人工审美批准;
+- 当前后置文字视觉质量已知不合格,不得仅凭自动测试或文件生成成功对外宣称成品质量。

+ 1 - 1
claude-code/fmode-image-set/bin/fmode-image-set.js

@@ -13,7 +13,7 @@ const invokedFromSource = invocationRelativeToSource === ""
 const WORKSPACE_ROOT = invokedFromSource ? path.dirname(SOURCE_ROOT) : INVOCATION_ROOT;
 const DEFAULT_TARGET = path.join(os.homedir(), ".claude", "plugins", "fmode-image-set");
 const WORKSPACE_TARGET = path.join(WORKSPACE_ROOT, ".claude", "skills", "fmode-image-set");
-const EXCLUDED = new Set(["node_modules", "生成结果", "memory", "outputs", ".tmp", ".git", ".env", ".env.local"]);
+const EXCLUDED = new Set(["node_modules", "生成结果", "memory", "outputs", ".tmp", ".git", ".idea", ".env", ".env.local"]);
 EXCLUDED.add(".claude");
 EXCLUDED.add(".codex");
 

+ 47 - 43
claude-code/fmode-image-set/docs/capability-map.md

@@ -2,46 +2,50 @@
 
 ## 一句话定位
 
-把一张真实商品图和一句业务需求,转成有证据、有分工、可校验、可恢复的电商七图。
-
-## 用户任务到入口映射
-
-| 用户任务 | Skill | MCP 工具 | 动作 | 输出 |
-| --- | --- | --- | --- | --- |
-| 理解商品并开始规划 | `fmode-image-set` | `fmode_image_set_run` | `start` | 运行目录、视觉理解、商品身份锁 |
-| 校验七图计划 | `fmode-image-set` | `fmode_image_set_validate` | — | 校验结果与修改项 |
-| 实际生成七图 | `fmode-image-set` | `fmode_image_set_run` | `execute` | 本地图片、状态与费用 |
-| 恢复中断任务 | `fmode-image-set` | `fmode_image_set_run` | `resume` | 同运行增量结果 |
-| 查看运行状态 | `fmode-image-set` | `fmode_image_set_run` | `status` | 账本与交付完整性 |
-
-## 能力边界
-
-- 当前固定七张,不是任意数量套图引擎。
-- 当前下游执行器为即梦 4.0。
-- 没有确定性文字排版器,重要文字和数值不交给生图模型重绘。
-- 商品图可证明可见事实,不能证明功效、成分、销量和认证。
-- 竞品图只能提供设计语法,不能作为商品资产。
-
-## 文件索引
-
-| 文件 | 用途 |
-| --- | --- |
-| `skills/fmode-image-set/SKILL.md` | 触发规则、证据边界、执行纪律和用户输出 |
-| `skills/fmode-image-set/references/*` | 商品理解、七图规划、Prompt 编译、Schema、即梦执行 |
-| `mcp/src/server.mjs` | MCP 工具注册与 Zod Schema |
-| `mcp/src/tools/*` | CLI/MCP 薄入口 |
-| `mcp/src/features/image-set/*` | 套图编排、视觉理解与计划校验 |
-| `mcp/src/providers/*` | 附件、视觉、七牛、即梦 |
-| `mcp/src/core/*` | 路径门禁、状态签名和统一结果 |
-| `docs/deliverables/image-set-result.md` | 套图交付合同 |
-| `docs/live-manual-acceptance-checklist.md` | 真实接口人工验收 |
-| `scripts/*` | 包级、MCP 和安装验收 |
-
-## 发布检查点
-
-- `package.json`、插件清单、包清单和 MCP Server 版本一致;
-- `npm run smoke:package` 通过;
-- `npm run mcp:smoke` 通过;
-- `npm run acceptance` 通过;
-- 真实生图发布前完成最小人工验收;
-- 不把真实 Token、用户附件和运行结果打入 npm 包。
+把一张商品图和一句自然需求,转化为商品感知、平台特化的七张无字视觉底图,并生成仅供评审的实验性后置文字版本。
+
+## 用户任务到入口
+
+| 用户任务 | MCP工具 | 动作 | 输出 |
+|---|---|---|---|
+| 捕获并观察原图 | `fmode_image_set_run` | `start` | OriginalVisualObservation,供Claude编写动态设定板 |
+| 生成参考资产 | `fmode_image_set_run` | `generate_references` | 固定三视图、动态无字/中文标注设定板、ReferenceVisualObservation |
+| 生成七图 | `fmode_image_set_run` | `generate_set` | Claude七页PromptModules对应的七张无字视觉底图、实验性后置文字版本、费用和布局报告 |
+| 恢复中断 | `fmode_image_set_run` | `resume` | 同run未完成阶段,不重提已完成付费图片/规划 |
+| 查看状态 | `fmode_image_set_run` | `status` | 当前阶段、文件、图片费用和辅助模型usage |
+| 诊断历史0.6计划 | `fmode_image_set_validate` | — | 旧受治理计划诊断结果 |
+
+## 生产链
+
+```text
+Qwen原图观察 → Claude动态设定板规格/Prompt
+原图 → 固定三视图/动态视觉设定板 → Qwen联合观察
+Claude平台Profile具体决定/七页PromptModules → 1白底 + 2卖点 + 2场景 + 2购买决策
+七份单图合同 → 七张视觉底图
+实际区域 + CopyPlan → SVG/Sharp实验性后置文字版本
+```
+
+## 关键文件
+
+| 文件 | 职责 |
+|---|---|
+| `reference-first-visual-observer.mjs` | Qwen描述性视觉输入和同run复用 |
+| `reference-first-v3-contracts.mjs` | Claude动态设定板/七页合同、哈希和确定性Prompt拼装 |
+| `reference-first-page-template-registry.mjs` | 白底、卖点、场景和购买决策模板族 |
+| `platform-design-profile-registry.mjs` | 平台策略解析和Prompt内容 |
+| `reference-asset-plan-compiler.mjs` | 参考资产及七份自包含单图合同 |
+| `setting-board-copy-plan.mjs` | 设定板Claude中文与来源 |
+| `setting-board-annotated-renderer.mjs` | Sharp/Pango中文标注设定板 |
+| `reference-first-layout-region-detector.mjs` | 排版几何识别 |
+| `adaptive-overlay-compositor.mjs` | 实验性中文排版、碰撞避让和扩展面板兜底 |
+| `reference-first-design-postprocessor.mjs` | 后置文字版本、manifest和布局报告;不代表视觉质量通过 |
+
+## 边界
+
+- 七图不输入用户原图;
+- 不启用旧商品事实、身份和套图质量门禁;
+- Qwen视觉观察不是创意规划或审核;几何识别不是发布门禁;
+- 平台Profile是项目设计策略,不冒充官方规则;
+- 商品事实文案只来自用户或用户提供的官方材料;
+- 当前后置文字视觉效果已被用户判定为很差,只能作为兼容输出和改造基线;自动排版检查通过不等于商业设计质量通过;
+- 费用、HMAC、串行提交和保守恢复继续作为承重墙。

+ 6 - 22
claude-code/fmode-image-set/docs/customer-quickstart.md

@@ -1,31 +1,15 @@
 # 客户快速开始
 
-## 你可以这样开始
-
-上传一张清晰商品图,然后说:
+上传一张商品图,然后说:
 
 ```text
-给这款商品做一套高级、适合电商详情页的七图
+给这款商品做一套适合淘宝详情页的中文七图,直接生成并交付,过程中不用再问我
 ```
 
-只想先看方案:
-
-```text
-先规划七张图分别讲什么,不要实际生图。
-```
-
-## 你会看到什么
-
-- 一张白底主图;
-- 两张不同任务的场景图;
-- 两张有证据的卖点图;
-- 两张解决购买疑问的决策图;
-- 商品外观和整套视觉风格保持一致。
-
-## 什么时候会产生费用
+系统会自动完成Qwen视觉辅助、固定三视图、Claude商品自适应无字视觉设定板、Sharp/Pango中文标注设定板、Claude平台特化七页PromptModules、七张无新增文字底图、排版区域识别和实验性后置文字渲染。七图结构为一张白底、两张卖点、两张场景和两张购买决策图。
 
-只有你明确要求“开始生图”后才会调用图片模型。默认七张各生成一次,费用上限为 1.54 元;需要额外重试时会先说明
+图片生成共九张,按当前图片Provider预计 `1.98` 元(参考阶段0.44元、七图阶段1.54元)。视觉观察和区域识别的模型usage另行记录,具体费用取决于本机配置。三个内部阶段由Claude连续执行,不要求用户写方案或再次确认。
 
-## 图片不够清楚怎么办
+用户无需编写长Prompt。若有官方商品名称、卖点、规格或文案,可在自然需求中一并提供;未提供时系统不会虚构参数补满页面。
 
-系统会指出缺少的最短补拍清单,不会为了填满七张而虚构不可见结构、规格或功效
+本流程不执行商品内容质量门禁,最终仍需用户人工判断商品一致性、材质和审美质量。当前已知“无字底图 + Sharp/Pango后置文字”的视觉效果很差,后置文字文件仅供技术评审和后续改造,不是商业可用成品。平台Profile属于项目设计策略,不代表平台官方审核保证。

+ 48 - 44
claude-code/fmode-image-set/docs/deliverables/image-set-result.md

@@ -1,44 +1,48 @@
-# 商品套图交付合同
-
-## 适用场景
-
-用户提供真实商品图,希望获得一套用于电商主图、详情页或商品传播的七张图片。
-
-## 输入字段
-
-- 商品参考图;
-- 一句话业务需求;
-- 平台/市场,可选;
-- 商品官方事实,可选;
-- VOC、竞品、关键词和趋势,可选;
-- 是否授权实际生图;
-- 费用上限。
-
-## 输出结构
-
-1. 运行编号与状态;
-2. 七张图片的业务角色;
-3. 01–07 本地成品;
-4. 成品文件夹;
-5. 完成、失败、未知、拒绝和可恢复图位;
-6. 已发生费用范围;
-7. 下一步动作。
-
-内部同时保留:
-
-- `vision-result.json`
-- `prompt-plan.json`
-- `reference-assets.json`
-- `jimeng-result.json`
-
-## 验收标准
-
-- 恰好 1 张主图、2 张场景图、2 张卖点图、2 张购买决策图;
-- 商品真实性严重错误为 0;
-- 两张卖点图使用不同证据;
-- 七张图不是同一构图换背景;
-- 商品锁、可见事实和视觉审计保持一致;
-- 重要 Logo、包装文字、规格和数量没有漂移;
-- 交付文件全部来自本次运行账本;
-- 费用不超过用户授权上限;
-- 部分完成时不伪称七张全部成功。
+# Claude主导动态平台特化套图交付合同V3
+
+## 输入
+
+- 当前商品图一张;
+- 一句自然套图需求;
+- 平台、市场、语言和比例(可选);
+- 用户提供的商品名称、卖点、规格和官方文案(可选)。
+
+## 用户交付
+
+1. 运行编号和输出目录;
+2. 16:9无新增文字产品三视图;
+3. 16:9动态产品视觉设定板(内部无字参考版 + 用户中文标注版);
+4. 01白底、02/03卖点、04/05场景、06/07购买决策共七张无字视觉底图;
+5. 如后处理成功,七张实验性后置文字版本;这些文件不代表商业设计质量通过;
+6. 所有图片绝对路径;
+7. 图片费用范围、辅助模型调用次数和usage;
+8. 中断、未知提交和可恢复阶段;
+9. 当前未执行商品内容质量门禁以及后置文字质量未通过真实验收的边界说明。
+
+## 内部可审计产物
+
+- `reference-first-request.json`;
+- `original-visual-observation.json` / `claude-reference-authoring.json`;
+- `reference-asset-plan.json` / `reference-asset-result.json`;
+- `reference-visual-observation.json` / `claude-authored-set-plan.json`;
+- `setting-board-copy-plan.json` / `setting-board-layout-report.json`;
+- `prompt-plan.json` / `jimeng-result.json`;
+- `copy-plan.json`;
+- `layout-regions.json`;
+- `layout-report.json`;
+- `platform-designed-image-set-manifest.json`;
+- `reference-first-result.json`。
+
+## 技术验收
+
+- 两张参考资产直接由原图生成;
+- 七图只引用两张生成资产,不输入原图;
+- Claude把平台Profile转成逐页具体决定并实际进入Prompt和排版;
+- 七个Prompt自包含且只生成当前单图;
+- 六张非白底图来自三类定位模板族和六个不同子配方;
+- 新增文字只由确定性后处理生成且具有来源;该断言只覆盖工程可追溯性,不覆盖视觉质量;
+- 实际安全区与底图哈希绑定;
+- 无安全区时使用面板或扩展画布;
+- 已完成图片、视觉观察和区域识别在同run恢复时不重复调用;
+- 技术完成不冒充商品真实性、平台合规或人工审美批准;
+- 当前后置文字版本的真实视觉质量结论为不合格,不得按最终设计成品交付。

+ 57 - 41
claude-code/fmode-image-set/docs/live-manual-acceptance-checklist.md

@@ -1,41 +1,57 @@
-# Fmode Image Set 真实执行验收清单
-
-## 验收前准备
-
-- 使用无敏感信息的测试商品;
-- 确认视觉模型、七牛和即梦凭据已配置;
-- 确认服务端余额;
-- 使用新的测试运行,不修改历史状态文件;
-- 默认费用上限保持 1.54 元。
-
-## 最小验收
-
-1. 使用一张清晰商品图执行 `start`。
-2. 检查 `vision-result.json`:
-   - 可见事实与图片一致;
-   - 推断与未知项已分离;
-   - `identity_manifest` 没有虚构结构。
-3. 编写七图计划并执行校验。
-4. 确认校验不通过时没有调用即梦。
-5. 用户明确授权后执行 `execute`。
-6. 检查每张图完成下载后才提交下一张。
-7. 检查生成后商品身份复核。
-8. 检查成品目录与账本一致。
-9. 检查最终费用范围。
-
-## 失败场景
-
-- 连接预检失败:应标记未提交,并允许同运行恢复;
-- `submission_unknown`:不得自动重提;
-- 单图明确失败:隔离后继续后续图位;
-- `quality_rejected`:不交付、不自动补图;
-- 状态被手工修改:恢复应被签名校验拒绝;
-- 从其他运行复制图片:交付审计应阻断。
-
-## 通过标准
-
-- 商品真实性严重错误为 0;
-- 计划校验与身份复核均生效;
-- 状态和费用可追踪;
-- 同一用户请求不会创建第二个运行;
-- 所有失败都有明确状态和下一步。
+# Fmode Image Set V3真实验收清单
+
+> 当前基线结论:用户已判定“无字底图 + Sharp/Pango后置文字”的视觉效果很差。以下清单分别记录工程链路和真实视觉质量;即使工程项通过,也不得将当前后置文字版本标记为商业质量通过。
+
+## 准备
+
+- 在VSCode Claude Code中使用新的无敏感商品附件;
+- 确认七牛、Jimeng和视觉观察/区域Provider配置;
+- 图片预计生成九张、费用 `1.98` 元;辅助模型usage另行记录;
+- 真实付费验收由用户执行,不修改历史run。
+
+## 完整流程
+
+1. 上传一张商品图,用一句自然需求要求直接生成七图。
+2. Claude应在同一用户请求中连续调用`start → generate_references → generate_set`,聊天附件场景不传`image`,阶段间不再询问用户。
+3. 检查三视图第二列是水平45度观察,商品没有倾倒。
+4. 检查`setting-board-visual.png`没有模型新增文字,`setting-board-annotated.png`含可识别中文;内容与当前商品匹配而非通用模板,并单独记录其视觉质量。
+5. 检查`claude-reference-authoring.json`和`claude-authored-set-plan.json`均标记`authored_by=claude`并绑定前阶段哈希。
+6. 检查 `prompt-plan.json` 七个Prompt都声明只生成当前一张单图,并且只引用两张生成参考资产。
+7. 检查视觉底图为01白底、02/03卖点、04/05场景、06/07购买决策,且没有模型新增营销文字。
+8. 检查 `layout-regions.json` 与七张底图URL哈希绑定,只输出几何区域。
+9. 对实验性后置文字版本检查中文完整、无乱码、无语义截断,不遮挡商品、人脸、Logo和包装原字;这些工程项不能替代版式、层级、风格融合和商业审美验收。
+10. 无安全区页面应使用信息面板或扩展画布,不直接覆盖商品。
+11. 检查 `layout-report.json` 记录字体、字号、换行、选区、碰撞和兜底路径。
+12. 检查图片费用、辅助模型调用次数和usage分别记录。
+
+## 中断恢复
+
+- 参考图失败:七图不启动,同run resume;
+- Claude阶段合同结构失败:修复同一阶段输入;等待Claude输入的resume不得产生外部调用;
+- 七图技术失败:已完成图位不重提,`submission_unknown`不盲目重提;
+- 区域或排版失败:保留九张已付费图片,同run resume只继续后处理;
+- 完整run再次resume:Jimeng提交增量0,视觉观察和区域模型调用增量0。
+
+## 多商品和平台对照
+
+- 至少测试可穿戴、包装商品和硬质设备三类商品;
+- 同一商品至少比较淘宝和另一个平台;
+- 检查不同平台在构图、主体比例、视觉节奏、人物/场景权重和文字布局上具有可解释差异;
+- 检查六张非白底图不是同一模板换背景。
+
+## 工程链路通过标准
+
+- 观察→Claude动态设定板→2→观察→Claude七页设计→7→区域→排版阶段完整;
+- 七图无原图;
+- 1+2+2+2定位正确,同定位两页任务和视觉证明不同;
+- 材质、颜色和关键识别元素人工检查可接受;
+- 后置文字无乱码、无语义截断且无关键区域碰撞;
+- 恢复不产生重复图片或辅助模型调用;
+- 费用和边界说明真实。
+
+## 视觉质量通过标准
+
+- 字体、字号、层级、字距、行距、对齐、色彩、材质、信息卡和画面风格形成统一设计系统;
+- 文案与商品视觉建立自然关系,而不是像通用UI面板覆盖在底图上;
+- 至少与目标竞品样本进行盲评或并排评审,并达到预先约定阈值;
+- 当前版本未达到以上标准,真实验收结果应记录为**不通过**。

+ 23 - 0
claude-code/fmode-image-set/mcp/src/core/run-paths.mjs

@@ -95,11 +95,33 @@ export function pathsForRun(runId) {
     run_id: runId,
     run_directory: runDirectory,
     attachments_directory: join(runDirectory, "商品附件"),
+    reference_assets_directory: join(runDirectory, "参考资产"),
     images_directory: join(runDirectory, "成品图"),
+    reference_asset_plan: join(runDirectory, "reference-asset-plan.json"),
+    reference_asset_result: join(runDirectory, "reference-asset-result.json"),
+    reference_capture: join(runDirectory, "reference-capture.json"),
+    original_visual_observation: join(runDirectory, "original-visual-observation.json"),
+    claude_reference_authoring: join(runDirectory, "claude-reference-authoring.json"),
+    reference_visual_observation: join(runDirectory, "reference-visual-observation.json"),
+    setting_board_copy_plan: join(runDirectory, "setting-board-copy-plan.json"),
+    setting_board_layout_report: join(runDirectory, "setting-board-layout-report.json"),
+    setting_board_visual: join(runDirectory, "setting-board-visual.png"),
+    setting_board_annotated: join(runDirectory, "setting-board-annotated.png"),
+    claude_authored_set_plan: join(runDirectory, "claude-authored-set-plan.json"),
+    v3_stage_state: join(runDirectory, "claude-authored-v3-state.json"),
+    reference_first_request: join(runDirectory, "reference-first-request.json"),
+    reference_first_result: join(runDirectory, "reference-first-result.json"),
+    set_blueprint: join(runDirectory, "set-blueprint.json"),
+    copy_plan: join(runDirectory, "copy-plan.json"),
+    layout_regions: join(runDirectory, "layout-regions.json"),
+    layout_report: join(runDirectory, "layout-report.json"),
     vision_result: join(runDirectory, "vision-result.json"),
     prompt_plan: join(runDirectory, "prompt-plan.json"),
+    preflight_record: join(runDirectory, "preflight.json"),
+    execution_authorization: join(runDirectory, "execution-authorization.json"),
     asset_map: join(runDirectory, "reference-assets.json"),
     jimeng_result: join(runDirectory, "jimeng-result.json"),
+    production_outcome: join(runDirectory, "production-outcome.json"),
   };
 }
 
@@ -115,6 +137,7 @@ async function allocateRun() {
     try {
       await mkdir(paths.run_directory, { recursive: false });
       await mkdir(paths.attachments_directory);
+      await mkdir(paths.reference_assets_directory);
       await mkdir(paths.images_directory);
       return paths;
     } catch (error) {

+ 4 - 0
claude-code/fmode-image-set/mcp/src/core/run-paths.test.mjs

@@ -21,6 +21,10 @@ test("run IDs and paths always resolve below the project run root", () => {
   assert.equal(paths.runs_root, join(paths.project_root, "生成结果", "电商七图"));
   assert.equal(paths.attachments_directory, join(runsRoot, runId, "商品附件"));
   assert.equal(paths.images_directory, join(runsRoot, runId, "成品图"));
+  assert.equal(paths.original_visual_observation, join(runsRoot, runId, "original-visual-observation.json"));
+  assert.equal(paths.claude_reference_authoring, join(runsRoot, runId, "claude-reference-authoring.json"));
+  assert.equal(paths.reference_visual_observation, join(runsRoot, runId, "reference-visual-observation.json"));
+  assert.equal(paths.claude_authored_set_plan, join(runsRoot, runId, "claude-authored-set-plan.json"));
 });
 
 test("run IDs use Asia/Shanghai calendar dates across the UTC midnight boundary", () => {

+ 63 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/adaptive-overlay-compositor-v2.test.mjs

@@ -0,0 +1,63 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import sharp from "sharp";
+import { compositeAdaptiveOverlay, selectAdaptiveLayout } from "./adaptive-overlay-compositor.mjs";
+
+const copy = { eyebrow: "测试商品", title: "完整中文标题不能被截断", subtitle: "这一行用于验证中文自动换行与完整语义" };
+const source = () => sharp({ create: { width: 600, height: 400, channels: 3, background: "#D8D2C8" } }).png().toBuffer();
+const regions = {
+  product_region: { status: "verified", boxes: [{ x: 0.48, y: 0.12, width: 0.46, height: 0.72 }] },
+  face_regions: { status: "unverifiable", boxes: [] }, body_and_hand_regions: { status: "unverifiable", boxes: [] },
+  critical_product_regions: { status: "verified", boxes: [{ x: 0.55, y: 0.2, width: 0.2, height: 0.2 }] },
+  logo_and_original_text_regions: { status: "verified", boxes: [{ x: 0.62, y: 0.25, width: 0.1, height: 0.06 }] },
+  recommended_text_safe_regions: { status: "verified", boxes: [{ x: 0.03, y: 0.08, width: 0.38, height: 0.38 }] },
+};
+
+test("adaptive compositor renders measured complete Chinese copy inside a verified safe region", async () => {
+  const result = await compositeAdaptiveOverlay({
+    source: await source(), page: { role: "selling_point", template_id: "selling_form", copy, content_regions: regions },
+    size: { width: 600, height: 600 }, allowPanelFallback: true,
+  });
+  assert.equal(result.decision.mode, "overlay");
+  assert.equal(result.decision.typography.title.lines.join(""), copy.title);
+  assert.equal(result.decision.typography.subtitle.lines.join(""), copy.subtitle);
+  assert.equal(result.decision.typography.title.measurement, "sharp_pango_glyph_metrics");
+  const metadata = await sharp(result.buffer).metadata();
+  assert.deepEqual([metadata.width, metadata.height], [600, 600]);
+});
+
+test("adaptive compositor deterministically extends a panel when no safe region exists", async () => {
+  const decision = selectAdaptiveLayout({ page: { role: "purchase_decision" }, regions: null, allowPanelFallback: true });
+  assert.equal(decision.mode, "extended_panel");
+  const result = await compositeAdaptiveOverlay({
+    source: await source(), page: { role: "purchase_decision", template_id: "decision_value_summary", copy, content_regions: null },
+    size: 600, allowPanelFallback: true,
+  });
+  assert.equal(result.decision.mode, "extended_panel");
+  assert.ok(result.decision.panel_height > 0);
+  assert.equal(result.decision.typography.title.lines.join(""), copy.title);
+});
+
+test("white baseline page remains clean and uncropped", async () => {
+  const result = await compositeAdaptiveOverlay({ source: await source(), page: { overlay_required: false, copy: null }, size: 600, allowPanelFallback: true });
+  assert.equal(result.decision.reason, "page_has_no_copy");
+  const metadata = await sharp(result.buffer).metadata();
+  assert.deepEqual([metadata.width, metadata.height], [600, 600]);
+});
+
+test("platform typography strategy changes safe-region preference", () => {
+  const multiple = {
+    ...regions,
+    product_region: { status: "verified", boxes: [{ x: 0.4, y: 0.4, width: 0.2, height: 0.2 }] },
+    critical_product_regions: { status: "unverifiable", boxes: [] },
+    logo_and_original_text_regions: { status: "unverifiable", boxes: [] },
+    recommended_text_safe_regions: { status: "verified", boxes: [
+      { x: 0.02, y: 0.04, width: 0.3, height: 0.25 },
+      { x: 0.68, y: 0.04, width: 0.3, height: 0.25 },
+    ] },
+  };
+  const jd = selectAdaptiveLayout({ page: { role: "selling_point", slot_id: "02", platform_profile: "jd" }, regions: multiple });
+  const douyin = selectAdaptiveLayout({ page: { role: "selling_point", slot_id: "02", platform_profile: "douyin_ecommerce" }, regions: multiple });
+  assert.ok(jd.box.x < 0.5);
+  assert.ok(douyin.box.x > 0.5);
+});

+ 158 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/adaptive-overlay-compositor.mjs

@@ -0,0 +1,158 @@
+import sharp from "sharp";
+import { fitTextWithPango } from "./pango-text-metrics.mjs";
+
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const xml = (value) => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
+
+function intersects(left, right, padding = 0.012) {
+  return left.x < right.x + right.width + padding
+    && left.x + left.width + padding > right.x
+    && left.y < right.y + right.height + padding
+    && left.y + left.height + padding > right.y;
+}
+
+function verifiedBoxes(region) { return region?.status === "verified" && Array.isArray(region.boxes) ? region.boxes : []; }
+
+function targetForPage(page = {}) {
+  const role = text(page.role, text(page.id));
+  const platform = text(page.platform_profile, "general_ecommerce");
+  if (platform === "jd") return role === "purchase_decision" ? [0.5, 0.88] : [0.12, 0.2];
+  if (platform === "douyin_ecommerce") return Number(page.slot_id || page.id) % 2 === 0 ? [0.84, 0.22] : [0.12, 0.72];
+  if (platform === "pinduoduo") return [0.5, 0.86];
+  if (role === "selling_point") return [0.12, 0.18];
+  if (role === "usage_scene") return [0.84, 0.48];
+  if (role === "purchase_decision") return [0.5, 0.86];
+  const legacy = {
+    hero_corner_title: [0.08, 0.12], detail_bottom_caption: [0.5, 0.88], scene_vertical_side: [0.08, 0.5],
+    feature_top_right: [0.88, 0.12], scale_bottom_left: [0.12, 0.86], action_motion_caption: [0.84, 0.52], decision_footer_grid: [0.5, 0.9],
+  };
+  return legacy[page.layout_recipe] || [0.08, 0.12];
+}
+
+export function selectAdaptiveLayout({ page = {}, regions = null, allowPanelFallback = false } = {}) {
+  const panel = (reason) => allowPanelFallback ? { mode: "extended_panel", reason, box: null } : { mode: "no_overlay", reason, box: null };
+  if (!regions || regions.product_region?.status !== "verified" || regions.recommended_text_safe_regions?.status !== "verified") {
+    return panel("content_regions_unverifiable");
+  }
+  const exclusion = [
+    ...verifiedBoxes(regions.product_region), ...verifiedBoxes(regions.face_regions), ...verifiedBoxes(regions.body_and_hand_regions),
+    ...verifiedBoxes(regions.critical_product_regions), ...verifiedBoxes(regions.logo_and_original_text_regions),
+  ];
+  const target = targetForPage(page);
+  const candidates = verifiedBoxes(regions.recommended_text_safe_regions)
+    .filter((box) => box.width >= 0.23 && box.height >= 0.12)
+    .filter((candidate) => exclusion.every((forbidden) => !intersects(candidate, forbidden)))
+    .sort((left, right) => {
+      const score = (box) => Math.hypot(box.x + box.width / 2 - target[0], box.y + box.height / 2 - target[1]) - box.width * box.height * 0.12;
+      return score(left) - score(right);
+    });
+  const box = candidates[0];
+  if (!box) return panel("all_safe_regions_conflict");
+  return { mode: "overlay", reason: "verified_safe_region", box, recipe: page.layout_recipe || page.template_id };
+}
+
+function tspanLines(lines, x, firstY, lineHeight, attributes = "") {
+  return lines.map((line, index) => `<tspan x="${x}" y="${firstY + index * lineHeight}" ${attributes}>${xml(line)}</tspan>`).join("");
+}
+
+async function copyLayout(copy, boxPixels) {
+  const inset = Math.max(18, Math.round(boxPixels.width * 0.07));
+  const maxWidth = Math.max(80, boxPixels.width - inset * 2);
+  const eyebrowValue = text(copy?.eyebrow);
+  const titleValue = text(copy?.title, "商品亮点");
+  const subtitleValue = text(copy?.subtitle);
+  const eyebrow = eyebrowValue ? await fitTextWithPango(eyebrowValue, { maxWidth, maxHeight: 28, fontSize: 16, minFontSize: 12, maxLines: 1 }) : null;
+  const titleHeight = Math.max(42, Math.round(boxPixels.height * 0.43));
+  const title = await fitTextWithPango(titleValue, { maxWidth, maxHeight: titleHeight, fontSize: 32, minFontSize: 17, maxLines: 2, font: eyebrow?.font });
+  const used = (eyebrow ? eyebrow.lineHeight + 8 : 0) + title.lines.length * title.lineHeight;
+  const remaining = boxPixels.height - inset * 2 - used - 8;
+  if (subtitleValue && remaining < 28) throw new Error(`DESIGNED_ASSET_TEXT_OVERFLOW:${subtitleValue}`);
+  const subtitle = subtitleValue
+    ? await fitTextWithPango(subtitleValue, { maxWidth, maxHeight: remaining, fontSize: 18, minFontSize: 13, maxLines: 2, font: title.font })
+    : null;
+  return { inset, maxWidth, eyebrow, title, subtitle, font: title.font };
+}
+
+function styleForPage(page = {}) {
+  const role = text(page.role, text(page.id));
+  const platform = text(page.platform_profile, "general_ecommerce");
+  if (platform === "jd") return { opacity: 0.92, radius: 2, accent: role === "purchase_decision" ? "grid" : "line" };
+  if (platform === "douyin_ecommerce") return { opacity: 0.82, radius: 34, accent: role === "usage_scene" ? "vertical" : "line" };
+  if (platform === "pinduoduo") return { opacity: 0.94, radius: 10, accent: "grid" };
+  if (role === "usage_scene") return { opacity: 0.74, radius: 4, accent: "vertical" };
+  if (role === "purchase_decision") return { opacity: 0.9, radius: 0, accent: "grid" };
+  if (role === "selling_point") return { opacity: 0.86, radius: 22, accent: "line" };
+  return { opacity: 0.84, radius: 16, accent: "line" };
+}
+
+async function overlaySvg({ page, box, tokens, width, height }) {
+  const pixels = { x: Math.round(box.x * width), y: Math.round(box.y * height), width: Math.round(box.width * width), height: Math.round(box.height * height) };
+  const layout = await copyLayout(page.copy, pixels);
+  const primary = text(tokens?.primary, "#222222");
+  const accent = text(tokens?.accent, "#C58A3A");
+  const light = text(tokens?.lightText, "#FFFFFF");
+  const style = styleForPage(page);
+  const tx = pixels.x + layout.inset;
+  let cursor = pixels.y + layout.inset;
+  const eyebrowMarkup = layout.eyebrow ? tspanLines(layout.eyebrow.lines, tx, cursor + layout.eyebrow.fontSize, layout.eyebrow.lineHeight) : "";
+  if (layout.eyebrow) cursor += layout.eyebrow.lineHeight + 8;
+  const titleMarkup = tspanLines(layout.title.lines, tx, cursor + layout.title.fontSize, layout.title.lineHeight, 'font-weight="700"');
+  cursor += layout.title.lines.length * layout.title.lineHeight + 8;
+  const subtitleMarkup = layout.subtitle ? tspanLines(layout.subtitle.lines, tx, cursor + layout.subtitle.fontSize, layout.subtitle.lineHeight) : "";
+  const accentMarkup = style.accent === "grid"
+    ? `<path d="M ${pixels.x + 16} ${pixels.y + 12} H ${pixels.x + pixels.width - 16} M ${pixels.x + pixels.width - 48} ${pixels.y + 6} V ${pixels.y + pixels.height - 6}" stroke="${accent}" stroke-width="3"/>`
+    : style.accent === "vertical"
+      ? `<rect x="${pixels.x}" y="${pixels.y}" width="7" height="${pixels.height}" fill="${accent}"/>`
+      : `<rect x="${pixels.x + 16}" y="${pixels.y + 14}" width="5" height="${Math.max(28, pixels.height - 28)}" rx="2" fill="${accent}"/>`;
+  const family = xml(layout.font.family.split(",")[0]);
+  return {
+    buffer: Buffer.from(`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
+      <style>.cn{font-family:"${family}","Microsoft YaHei","Noto Sans CJK SC",sans-serif}</style>
+      <rect x="${pixels.x}" y="${pixels.y}" width="${pixels.width}" height="${pixels.height}" rx="${style.radius}" fill="${primary}" opacity="${style.opacity}"/>
+      ${accentMarkup}
+      <text class="cn" font-size="${layout.eyebrow?.fontSize || 14}" letter-spacing="1.5" fill="${light}" opacity="0.78">${eyebrowMarkup}</text>
+      <text class="cn" font-size="${layout.title.fontSize}" fill="${light}">${titleMarkup}</text>
+      <text class="cn" font-size="${layout.subtitle?.fontSize || 14}" fill="${light}" opacity="0.9">${subtitleMarkup}</text>
+    </svg>`),
+    layout,
+    pixels,
+  };
+}
+
+async function extendedPanelComposite({ source, page, tokens, width, height }) {
+  const densityFactor = page.platform_profile === "jd" ? 0.36 : page.platform_profile === "douyin_ecommerce" ? 0.31 : page.platform_profile === "pinduoduo" ? 0.36 : 0.34;
+  const panelHeight = Math.min(height - 120, Math.max(230, Math.round(height * densityFactor)));
+  const imageHeight = height - panelHeight;
+  const background = text(tokens?.background, "#F4F1EC");
+  const image = await sharp(source).resize({ width, height: imageHeight, fit: "contain", background }).png().toBuffer();
+  const canvas = await sharp({ create: { width, height, channels: 4, background } })
+    .composite([{ input: image, left: 0, top: 0 }]).png().toBuffer();
+  const box = { x: 0.055, y: imageHeight / height + 0.025, width: 0.89, height: (panelHeight / height) - 0.05 };
+  const overlay = await overlaySvg({ page, box, tokens, width, height });
+  const buffer = await sharp(canvas).composite([{ input: overlay.buffer }]).png().toBuffer();
+  return { buffer, layout: overlay.layout, box, panel_height: panelHeight };
+}
+
+export async function compositeAdaptiveOverlay({ source, page, tokens = {}, size = 1200, allowPanelFallback = false } = {}) {
+  if (!source) throw new Error("ADAPTIVE_OVERLAY_SOURCE_REQUIRED");
+  const dimensions = typeof size === "number" ? { width: size, height: size } : { width: Number(size?.width), height: Number(size?.height) };
+  if (!(dimensions.width > 0 && dimensions.height > 0)) throw new Error("ADAPTIVE_OVERLAY_SIZE_INVALID");
+  const image = await sharp(source).resize({ ...dimensions, fit: "contain", background: text(tokens?.background, "#F4F1EC") }).png().toBuffer();
+  if (page?.overlay_required === false || !page?.copy) return { buffer: image, decision: { mode: "no_overlay", reason: "page_has_no_copy", box: null } };
+  let decision = selectAdaptiveLayout({ page, regions: page.content_regions, allowPanelFallback });
+  if (decision.mode === "no_overlay") return { buffer: image, decision };
+  if (decision.mode === "extended_panel") {
+    const panel = await extendedPanelComposite({ source, page, tokens, ...dimensions });
+    return { buffer: panel.buffer, decision: { ...decision, box: panel.box, panel_height: panel.panel_height, typography: panel.layout } };
+  }
+  try {
+    const overlay = await overlaySvg({ page, box: decision.box, tokens, ...dimensions });
+    const buffer = await sharp(image).composite([{ input: overlay.buffer }]).png().toBuffer();
+    return { buffer, decision: { ...decision, typography: overlay.layout } };
+  } catch (error) {
+    if (!allowPanelFallback || !String(error?.message).includes("TEXT_OVERFLOW")) throw error;
+    const panel = await extendedPanelComposite({ source, page, tokens, ...dimensions });
+    decision = { mode: "extended_panel", reason: "verified_region_text_overflow", box: panel.box, panel_height: panel.panel_height, typography: panel.layout };
+    return { buffer: panel.buffer, decision };
+  }
+}

+ 49 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/benchmark-harness.mjs

@@ -0,0 +1,49 @@
+const clamp = (value) => Math.max(0, Math.min(100, Math.round(value)));
+const ratio = (passed, total) => total === 0 ? 0 : (passed / total) * 100;
+
+export function compileBenchmarkResult({
+  ir,
+  promptBundle,
+  viewCoverage,
+  qualityReport,
+  deliveryProfile,
+  realGenerationValidated = false,
+  humanApproved = false,
+} = {}) {
+  const pageChecks = qualityReport.pages.flatMap((page) => page.checks);
+  const evidenceChecks = pageChecks.filter((check) => check.code === "evidence_coverage");
+  const fileChecks = pageChecks.filter((check) => check.code === "file_and_dimensions" || check.code === "text_and_layout_acceptance");
+  const viewChecks = pageChecks.filter((check) => check.code === "view_validity");
+  const setChecks = qualityReport.set_checks;
+  const blocking = qualityReport.status === "blocked" || viewCoverage.status === "blocked";
+  let releaseStatus = "review_required";
+  if (blocking) releaseStatus = "development_only";
+  else if (realGenerationValidated && humanApproved) releaseStatus = "publishable";
+
+  const metrics = {
+    truth_score: clamp(ratio(pageChecks.filter((check) => check.status === "pass").length, pageChecks.length)),
+    evidence_coverage: clamp(ratio(evidenceChecks.filter((check) => check.status === "pass").length, evidenceChecks.length)),
+    prompt_completeness: clamp(ratio(promptBundle.contracts.filter((contract) => contract.prompt_hash && contract.acceptance_checks?.length).length, ir.page_plans.length)),
+    layout_safety: clamp(ratio(fileChecks.filter((check) => check.status === "pass").length, fileChecks.length)),
+    view_validity: clamp(ratio(viewChecks.filter((check) => check.status === "pass").length, viewChecks.length)),
+    set_diversity: clamp(ratio(setChecks.filter((check) => ["unique_sales_tasks", "recipe_diversity"].includes(check.code) && check.status === "pass").length, 2)),
+    platform_readiness: deliveryProfile.rule_confidence === "creative_profile_only" ? 70 : 100,
+  };
+  return {
+    schema_version: "1.0",
+    status: "complete",
+    metrics,
+    release_status: releaseStatus,
+    release_evidence: {
+      blocking_issues_absent: !blocking,
+      real_generation_validated: Boolean(realGenerationValidated),
+      human_approved: Boolean(humanApproved),
+      platform_compliance_asserted: deliveryProfile.compliance_claim !== "not_asserted",
+    },
+    limitations: [
+      "Deterministic SVG composition is not real photography or provider-generation validation.",
+      "Creative platform profiles do not assert current marketplace compliance.",
+      "Publishable requires both real generated-image validation and explicit human approval.",
+    ],
+  };
+}

+ 164 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/commercial-copy-compiler.mjs

@@ -0,0 +1,164 @@
+const INTERNAL_TERMS = /参考图|参考像素|事实边界|不可见|额外参考|不推断|审计|仿射|隐藏结构|identity|reference/i;
+const BRAND_CLAIMS = /品牌识别|记住品牌|品牌视觉|品牌呈现/i;
+const PACKAGING_CLAIMS = /包装|包装正面|包装规格/i;
+
+const clean = (value) => String(value || "")
+  .replace(/([^)]*(推断|非事实)[^)]*)/g, "")
+  .replace(/\s+/g, " ")
+  .trim();
+
+const asText = (item) => clean(typeof item === "string" ? item : item?.text || item?.value);
+const factsOf = (product) => product.verified_visible_facts || product.visible_facts || [];
+const specsOf = (product) => product.verified_specs || product.visible_specs || {};
+
+function ngrams(value, size = 2) {
+  const normalized = clean(value).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
+  const chars = Array.from(normalized);
+  if (chars.length <= size) return new Set(chars.length ? [chars.join("")] : []);
+  return new Set(chars.slice(0, -(size - 1)).map((_, index) => chars.slice(index, index + size).join("")));
+}
+
+function evidenceScore(left, right) {
+  const a = ngrams(left);
+  const b = ngrams(right);
+  if (!a.size || !b.size) return 0;
+  let shared = 0;
+  for (const token of a) if (b.has(token)) shared += 1;
+  return (2 * shared) / (a.size + b.size);
+}
+
+export function classifyProductForm(product = {}) {
+  const facts = factsOf(product).join(" ");
+  const specs = Object.keys(specsOf(product)).join(" ");
+  const haystack = `${product.category || ""} ${product?.product_name?.value || product.product_name || ""} ${facts} ${specs}`;
+  if (/包装|包装袋|盒装|瓶装|罐装|袋装|净含量|咖啡|茶叶|食品|饮料|零食|护肤|化妆品/.test(haystack)) return "packaged_goods";
+  if (/服装|服饰|箱包|背包|鞋|织物|面料/.test(haystack)) return "soft_goods";
+  if (/键盘|鼠标|耳机|音箱|手机|电脑|数码|电器|工具|家具|设备/.test(haystack)) return "durable_goods";
+  return "generic_product";
+}
+
+export function detectVerifiedBrand(product = {}) {
+  const visibleText = (product.visible_text || []).map(asText).filter(Boolean);
+  const name = clean(product?.product_name?.value || product.product_name);
+  if (!name || !visibleText.length) return { status: "unavailable", value: "", evidence_refs: [] };
+  const normalizedName = name.toLowerCase();
+  const matchIndex = visibleText.findIndex((text) => {
+    const normalizedText = text.toLowerCase();
+    return normalizedName.includes(normalizedText) || normalizedText.includes(normalizedName)
+      || normalizedName.split(/\s+/).filter((part) => part.length >= 3).some((part) => normalizedText.includes(part));
+  });
+  return matchIndex >= 0
+    ? { status: "verified", value: name, evidence_refs: [`visible_text:${matchIndex + 1}`] }
+    : { status: "unavailable", value: "", evidence_refs: [] };
+}
+
+export function resolveEvidenceRefs(text, product = {}, suppliedRefs = []) {
+  const facts = factsOf(product);
+  if (!facts.length) return suppliedRefs.filter((ref) => /^visible_fact:\d+$/.test(ref));
+  const semanticGroups = [
+    /铜色|拉丝|旋钮/,
+    /无印字|键帽/,
+    /紧凑|布局/,
+    /黑色|哑光/,
+    /深绿|米白|分区/,
+    /徽标|山|树|波浪/,
+    /净含量|容量/,
+  ];
+  const semanticRefs = semanticGroups.flatMap((matcher) => {
+    if (!matcher.test(text)) return [];
+    const candidates = facts
+      .map((fact, index) => ({ ref: `visible_fact:${index + 1}`, fact, score: evidenceScore(text, fact) }))
+      .filter((item) => matcher.test(item.fact))
+      .sort((a, b) => b.score - a.score);
+    return candidates[0] ? [candidates[0].ref] : [];
+  });
+  const ranked = facts
+    .map((fact, index) => ({ ref: `visible_fact:${index + 1}`, score: evidenceScore(text, fact) }))
+    .sort((a, b) => b.score - a.score);
+  const best = ranked[0]?.score || 0;
+  const matched = [...new Set([
+    ...ranked.filter((item) => item.score >= 0.1 && item.score >= best * 0.58).map((item) => item.ref),
+    ...semanticRefs,
+  ])].slice(0, 4);
+  if (matched.length) return matched;
+  return suppliedRefs.filter((ref) => {
+    const match = /^visible_fact:(\d+)$/.exec(ref);
+    return match && Number(match[1]) <= facts.length && evidenceScore(text, facts[Number(match[1]) - 1]) > 0;
+  });
+}
+
+function semanticTitle(value) {
+  const text = clean(value);
+  if (/深绿/.test(text) && /米白/.test(text)) return "深绿 × 米白";
+  if (/徽标/.test(text) && /山|树|波浪/.test(text)) return "金色山林徽标";
+  if (/铜色/.test(text) && /旋钮/.test(text)) return "铜色拉丝旋钮";
+  if (/无(?:字符)?印字/.test(text) && /键帽/.test(text)) return "全黑无印字键帽";
+  if (/紧凑/.test(text) && /布局/.test(text)) return "紧凑布局";
+  const completeClause = text.split(/[。;;::]|作为|共同|构成|呈现|形成|突出|强调/)[0].trim();
+  return completeClause || "可见设计细节";
+}
+
+function pointFromText(text, evidenceRefs = []) {
+  const value = clean(text);
+  if (/深绿/.test(value) && /米白/.test(value)) return { title: "深绿 × 米白", description: "深绿与米白垂直分区,形成清晰视觉层次", evidenceRefs };
+  if (/徽标/.test(value) && /山|树|波浪/.test(value)) return { title: "金色山林徽标", description: "金色徽标中的山、树与波浪图形清晰可见", evidenceRefs };
+  return { title: semanticTitle(value), description: value, evidenceRefs };
+}
+
+function displayName(product) {
+  const name = clean(product?.product_name?.value || product.product_name || product.category || "精选商品");
+  return name.replace(/([^)]+)/g, "").trim() || "精选商品";
+}
+
+function semanticSpecTitle(label, value) {
+  const normalized = clean(value);
+  return Array.from(normalized).length > 14 || /[((]/.test(normalized) ? clean(label) : normalized;
+}
+
+export function compileCommercialCopy(product = {}, context = {}) {
+  const facts = factsOf(product);
+  const specs = Object.entries(specsOf(product));
+  const productForm = context.productForm || classifyProductForm(product);
+  const brand = context.brand || detectVerifiedBrand(product);
+  const auditedPoints = product.verified_selling_points || product.visible_selling_points || [];
+  const points = auditedPoints.slice(0, 2).map((item) => {
+    const value = clean(item.value || item.text);
+    return pointFromText(value, resolveEvidenceRefs(value, product, item.basis_refs || []));
+  });
+  if (specs.length && points.length < 3) {
+    const [label, value] = specs[0];
+    points.push({ title: semanticSpecTitle(label, value), description: `${clean(label)}:${clean(value)}`, evidenceRefs: ["visible_specs"] });
+  }
+  let factCursor = 0;
+  while (points.length < 3 && factCursor < facts.length) {
+    const fact = clean(facts[factCursor]);
+    const ref = `visible_fact:${factCursor + 1}`;
+    factCursor += 1;
+    if (points.some((point) => point.evidenceRefs.includes(ref))) continue;
+    points.push(pointFromText(fact, [ref]));
+  }
+  if (!points.length && facts.length) points.push(pointFromText(facts[0], ["visible_fact:1"]));
+
+  const name = displayName(product);
+  const result = {
+    productLabel: name,
+    brand: brand.status === "verified" ? brand.value : name,
+    brandVerified: brand.status === "verified",
+    heroTitle: name,
+    heroSubtitle: `${points[0].title},细节清晰`,
+    heroBadge: specs[0] ? `${clean(specs[0][0])} · ${clean(specs[0][1])}` : points[1].title,
+    sellingTitle: productForm === "packaged_goods" ? "包装细节,清晰呈现" : "设计细节,清晰呈现",
+    points,
+    sceneTitleLines: productForm === "packaged_goods" ? ["融入日常场景", "保持视觉秩序"] : ["融入日常空间", "自成视觉焦点"],
+    sceneSubtitle: productForm === "packaged_goods" ? "清晰外观与有序配色,融入日常使用场景" : "简洁外观融入空间,商品细节清晰可见",
+  };
+  const visibleCopy = JSON.stringify(result);
+  if (INTERNAL_TERMS.test(visibleCopy)) throw new Error("DESIGNED_ASSET_INTERNAL_COPY_REJECTED");
+  if (brand.status !== "verified" && BRAND_CLAIMS.test(visibleCopy)) throw new Error("DESIGNED_ASSET_UNVERIFIED_BRAND_COPY_REJECTED");
+  if (productForm !== "packaged_goods" && PACKAGING_CLAIMS.test(visibleCopy)) throw new Error("DESIGNED_ASSET_NON_PACKAGE_COPY_REJECTED");
+  return result;
+}
+
+export function containsInternalCopy(value) {
+  return INTERNAL_TERMS.test(String(value || ""));
+}

+ 121 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/commercial-pipeline.test.mjs

@@ -0,0 +1,121 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import sharp from "sharp";
+import { compileCommercialCopy, containsInternalCopy } from "./commercial-copy-compiler.mjs";
+import { compileDesignTokens } from "./design-token-compiler.mjs";
+import { compileDesignIr, validateDesignIr } from "./design-ir-compiler.mjs";
+import { fitText } from "./text-layout.mjs";
+import { buildProductLayer } from "./product-layer-builder.mjs";
+import { listDesignRecipes, selectDesignRecipes, validateRecipeSet } from "./design-recipe-registry.mjs";
+
+const coffee = {
+  category: "食品饮料-咖啡",
+  product_name: { value: "MORI COFFEE" },
+  visible_text: [{ text: "MORI COFFEE" }, { text: "250 g" }],
+  verified_visible_facts: [
+    "包装正面可见 MORI COFFEE 文字",
+    "包装采用深绿与米白双色竖向分区",
+    "金色圆形徽标含山、树与波浪图形",
+  ],
+  visible_selling_points: [
+    { value: "双色竖向分区设计:深绿与米白构成明确视觉分割", basis_refs: ["visible_fact:2"] },
+    { value: "金色圆形徽标位于品牌名上方,图形含山、树、波浪元素", basis_refs: ["visible_fact:3"] },
+  ],
+  visible_specs: { 净含量: "250 g" },
+};
+
+test("commercial copy uses audited evidence and excludes internal review language", () => {
+  const copy = compileCommercialCopy(coffee);
+  assert.equal(copy.heroTitle, "MORI COFFEE");
+  assert.equal(copy.points[0].title, "深绿 × 米白");
+  assert.equal(copy.points[2].title, "250 g");
+  assert.equal(copy.points[0].evidenceRefs[0], "visible_fact:2");
+  assert.equal(copy.points[1].evidenceRefs[0], "visible_fact:3");
+  assert.equal(copy.brandVerified, true);
+  assert.equal(containsInternalCopy(JSON.stringify(copy)), false);
+});
+
+const keyboard = {
+  category: "键盘",
+  product_name: { value: "黑色紧凑型键盘(带铜色旋钮)" },
+  visible_text: [],
+  verified_visible_facts: [
+    "键盘主体为黑色,表面呈哑光质感",
+    "所有键帽无字符印字",
+    "布局紧凑,无数字小键盘区",
+    "右上角可见铜色圆形拉丝旋钮",
+  ],
+  visible_selling_points: [
+    { value: "右上角铜色拉丝旋钮作为唯一异色元素,构成强视觉焦点", basis_refs: ["visible_fact:1"] },
+    { value: "全黑无印字键帽与极简紧凑布局共同呈现统一哑光视觉语言", basis_refs: ["visible_fact:2"] },
+  ],
+  visible_specs: { 颜色方案: "主体黑色 + 铜色旋钮" },
+};
+
+test("durable goods never inherit packaging or unverified brand language", () => {
+  const copy = compileCommercialCopy(keyboard);
+  assert.equal(copy.brandVerified, false);
+  assert.equal(copy.heroTitle, "黑色紧凑型键盘");
+  assert.equal(copy.points[0].title, "铜色拉丝旋钮");
+  assert.equal(copy.points[0].evidenceRefs[0], "visible_fact:4");
+  assert.match(copy.points[1].title, /无印字键帽/);
+  assert.ok(copy.points[1].evidenceRefs.includes("visible_fact:1"));
+  assert.ok(copy.points[1].evidenceRefs.includes("visible_fact:3"));
+  assert.doesNotMatch(JSON.stringify(copy), /包装|品牌识别|记住品牌|品牌视觉/);
+  assert.doesNotMatch(JSON.stringify(copy), /旋钮作[,。]/);
+});
+
+test("Design IR is complete, deterministic and evidence-valid across product forms", async () => {
+  const layer = await sharp({ create: { width: 200, height: 120, channels: 4, background: { r: 35, g: 35, b: 40, alpha: 1 } } }).png().toBuffer();
+  const productLayer = { buffer: layer };
+  const first = await compileDesignIr(keyboard, productLayer);
+  const second = await compileDesignIr(keyboard, productLayer);
+  assert.deepEqual(first, second);
+  assert.equal(first.product_profile.product_form, "durable_goods");
+  assert.equal(first.product_profile.brand.status, "unavailable");
+  assert.equal(first.schema_version, "1.1");
+  assert.equal(first.layout_plan.recipe_set.style, "technical_precision");
+  assert.equal(first.layout_plan.recipe_set.scene, "technical_workspace");
+  assert.equal(first.page_plans.length, 3);
+  assert.equal(validateDesignIr(first), first);
+});
+
+test("recipe selection composes form, aspect ratio and information density", () => {
+  const packaged = selectDesignRecipes({ productForm: "packaged_goods", width: 420, height: 760, pointCount: 3 });
+  const durable = selectDesignRecipes({ productForm: "durable_goods", width: 900, height: 360, pointCount: 2 });
+  const soft = selectDesignRecipes({ productForm: "soft_goods", width: 480, height: 700, pointCount: 3 });
+  assert.deepEqual([packaged.style, packaged.hero, packaged.scene], ["natural_editorial", "portrait_editorial", "warm_countertop"]);
+  assert.deepEqual([durable.style, durable.hero, durable.scene], ["technical_precision", "landscape_split", "technical_workspace"]);
+  assert.equal(durable.detail, "gallery_evidence");
+  assert.deepEqual([soft.style, soft.hero, soft.scene], ["tactile_editorial", "soft_pedestal", "textile_studio"]);
+  assert.equal(packaged.selection_basis.orientation, "portrait");
+  assert.equal(durable.selection_basis.orientation, "landscape");
+  assert.equal(packaged.selection_basis.information_density, "rich");
+  assert.throws(() => validateRecipeSet({ style: "missing", hero: packaged.hero, detail: packaged.detail, scene: packaged.scene }), /DESIGN_RECIPE_NOT_FOUND/);
+  assert.ok(listDesignRecipes().scene.includes("gallery_podium"));
+});
+
+test("text layout wraps complete copy and fails closed when constraints cannot fit", () => {
+  const fitted = fitText("全黑无印字键帽与极简紧凑布局共同呈现统一哑光视觉语言", { maxWidth: 380, fontSize: 20, minFontSize: 16, maxLines: 2 });
+  assert.ok(fitted.lines.length <= 2);
+  assert.equal(fitted.lines.join(""), "全黑无印字键帽与极简紧凑布局共同呈现统一哑光视觉语言");
+  assert.throws(() => fitText("无法放入狭小区域的完整长文案", { maxWidth: 20, fontSize: 20, minFontSize: 20, maxLines: 1 }), /DESIGNED_ASSET_TEXT_OVERFLOW/);
+});
+
+test("light opaque product interiors remain solid on dark designed scenes", async () => {
+  const reference = await sharp({ create: { width: 320, height: 320, channels: 3, background: "#dadada" } })
+    .composite([{ input: Buffer.from('<svg width="320" height="320"><rect x="65" y="45" width="190" height="235" rx="18" fill="#eee9de" stroke="#33503a" stroke-width="18"/><rect x="65" y="45" width="70" height="235" fill="#33503a"/></svg>') }])
+    .png().toBuffer();
+  const layer = await buildProductLayer(reference);
+  const { data, info } = await sharp(layer.buffer).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
+  const centerAlpha = data[(Math.floor(info.height / 2) * info.width + Math.floor(info.width * 0.68)) * 4 + 3];
+  assert.ok(centerAlpha >= 245, `expected opaque light interior, got alpha=${centerAlpha}`);
+});
+
+test("design tokens follow the dominant product colour", async () => {
+  const layer = await sharp({ create: { width: 200, height: 260, channels: 4, background: { r: 30, g: 65, b: 45, alpha: 1 } } }).png().toBuffer();
+  const tokens = await compileDesignTokens(layer);
+  assert.match(tokens.primary, /^#[0-9A-F]{6}$/);
+  assert.notEqual(tokens.primary, "#263126");
+  assert.notEqual(tokens.primary, tokens.surface);
+});

+ 126 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/control-plane.test.mjs

@@ -0,0 +1,126 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import sharp from "sharp";
+import { compileDesignIr } from "./design-ir-compiler.mjs";
+import { compilePromptContracts, validatePromptContract, promptContractLimits } from "./prompt-contract-compiler.mjs";
+import { compileViewCoverage } from "./view-coverage-compiler.mjs";
+import { compileDeliveryProfile } from "./delivery-profile-compiler.mjs";
+import { compileQualityReport, compileRepairPlan } from "./quality-repair-compiler.mjs";
+import { compileBenchmarkResult } from "./benchmark-harness.mjs";
+
+const products = {
+  keyboard: {
+    category: "键盘",
+    product_name: { value: "黑色紧凑型键盘" },
+    verified_visible_facts: ["键盘主体为黑色", "右上角可见铜色旋钮"],
+    visible_selling_points: [{ value: "右上角铜色旋钮", basis_refs: ["visible_fact:2"] }],
+  },
+  coffee: {
+    category: "食品饮料-咖啡",
+    product_name: { value: "MORI COFFEE" },
+    visible_text: [{ text: "MORI COFFEE" }],
+    verified_visible_facts: ["包装正面可见 MORI COFFEE 文字", "包装采用深绿与米白分区"],
+    visible_selling_points: [{ value: "深绿与米白分区", basis_refs: ["visible_fact:2"] }],
+  },
+  backpack: {
+    category: "背包",
+    product_name: { value: "轻量双肩包" },
+    verified_visible_facts: ["主体为米白色织物", "正面可见黑色矩形标记"],
+    visible_selling_points: [{ value: "米白织物与黑色标记", basis_refs: ["visible_fact:1", "visible_fact:2"] }],
+    visible_specs: { 商品类型: "双肩包" },
+  },
+};
+
+async function makeIr(product = products.keyboard) {
+  const buffer = await sharp({ create: { width: 640, height: 420, channels: 4, background: { r: 35, g: 37, b: 40, alpha: 1 } } }).png().toBuffer();
+  return compileDesignIr(product, { buffer });
+}
+
+function syntheticFiles(ir) {
+  return ir.page_plans.map((page) => ({ slot: page.id, file: import.meta.filename, width: 1200, height: 1200, sha256: "a".repeat(64) }));
+}
+
+test("provider contracts are complete, deterministic, evidence-resolved and product-specific", async () => {
+  const [keyboardIr, coffeeIr, backpackIr] = await Promise.all(Object.values(products).map(makeIr));
+  const keyboard = compilePromptContracts(keyboardIr);
+  const repeated = compilePromptContracts(keyboardIr);
+  const coffee = compilePromptContracts(coffeeIr);
+  const backpack = compilePromptContracts(backpackIr);
+  assert.equal(keyboard.contracts.length, 3);
+  assert.equal(keyboard.bundle_hash, repeated.bundle_hash);
+  assert.notEqual(keyboard.bundle_hash, coffee.bundle_hash);
+  assert.notEqual(coffee.bundle_hash, backpack.bundle_hash);
+  for (const contract of keyboard.contracts) {
+    assert.ok(contract.provider_prompt && contract.negative_prompt && contract.localized_copy);
+    assert.ok(contract.prompt_source.evidence_refs.every((ref) => ref in contract.evidence_map));
+  }
+});
+
+test("prompt contract fails closed for hidden-view, view-conflict, secrets and excess length", async () => {
+  const contract = structuredClone(compilePromptContracts(await makeIr()).contracts[0]);
+  contract.provider_prompt = "展示背面,并使用本地确定性排版,不生成营销文字";
+  assert.throws(() => validatePromptContract(contract), /PROMPT_CONTRACT_HIDDEN_VIEW_REQUESTED/);
+  contract.provider_prompt = "只保留参考图视角,无新增营销文字,本地确定性排版";
+  contract.prompt_source.allowed_views = ["back"];
+  contract.prompt_source.forbidden_views = ["back"];
+  assert.throws(() => validatePromptContract(contract), /PROMPT_CONTRACT_VIEW_CONFLICT/);
+  contract.prompt_source.forbidden_views = [];
+  contract.provider_prompt = "Authorization: Bearer secret-value;无新增营销文字;本地确定性排版";
+  assert.throws(() => validatePromptContract(contract), /PROMPT_CONTRACT_SECRET_OR_PATH_REJECTED/);
+  contract.provider_prompt = `${"甲".repeat(promptContractLimits.maxProviderPromptLength + 1)}本地确定性排版`;
+  assert.throws(() => validatePromptContract(contract), /PROVIDER_PROMPT_LENGTH_EXCEEDED/);
+  contract.provider_prompt = "只保留参考图视角,无新增营销文字,本地确定性排版";
+  contract.localized_copy = { title: "甲".repeat(promptContractLimits.maxLocalizedCopyLength + 1) };
+  assert.throws(() => validatePromptContract(contract), /LOCALIZED_COPY_LENGTH_EXCEEDED/);
+});
+
+test("view capability routing never fabricates an unsupported unseen view", async () => {
+  const ir = await makeIr();
+  const blocked = compileViewCoverage(ir, { requestedViews: { scene: "back" } });
+  assert.equal(blocked.status, "blocked");
+  assert.equal(blocked.pages.find((page) => page.page_id === "scene").decision, "requires_additional_reference");
+  const controlled = compileViewCoverage(ir, {
+    requestedViews: { scene: "back" },
+    providerCapabilities: { reference_image_generation: true, controlled_view_generation: true, multi_reference: false, image_edit: false, mask_edit: false },
+  });
+  assert.equal(controlled.pages.find((page) => page.page_id === "scene").decision, "provider_reference_generation");
+  assert.equal(controlled.pages.find((page) => page.page_id === "scene").requires_identity_review, true);
+});
+
+test("delivery profile validates platform and BCP-47 language", async () => {
+  const ir = await makeIr();
+  assert.equal(compileDeliveryProfile(ir).release_gate, "review_required");
+  assert.throws(() => compileDeliveryProfile(ir, { platform: "unknown_channel" }), /DELIVERY_PROFILE_UNKNOWN_PLATFORM/);
+  assert.throws(() => compileDeliveryProfile(ir, { target_language: "中文" }), /DELIVERY_PROFILE_LANGUAGE_INVALID/);
+});
+
+test("quality repair is non-paid and release cannot publish without both approvals", async () => {
+  const ir = await makeIr();
+  const promptBundle = compilePromptContracts(ir);
+  const viewCoverage = compileViewCoverage(ir);
+  const deliveryProfile = compileDeliveryProfile(ir);
+  const qualityReport = compileQualityReport({ ir, promptBundle, viewCoverage, files: syntheticFiles(ir) });
+  const repairPlan = compileRepairPlan(qualityReport);
+  assert.equal(qualityReport.status, "review_required");
+  assert.equal(repairPlan.paid_action_authorized, false);
+  assert.ok(repairPlan.actions.every((action) => action.paid_action_authorized === false));
+  const defaultBenchmark = compileBenchmarkResult({ ir, promptBundle, viewCoverage, qualityReport, deliveryProfile });
+  assert.equal(defaultBenchmark.release_status, "review_required");
+  const halfApproved = compileBenchmarkResult({ ir, promptBundle, viewCoverage, qualityReport, deliveryProfile, realGenerationValidated: true });
+  assert.equal(halfApproved.release_status, "review_required");
+  const publishable = compileBenchmarkResult({ ir, promptBundle, viewCoverage, qualityReport, deliveryProfile, realGenerationValidated: true, humanApproved: true });
+  assert.equal(publishable.release_status, "publishable");
+});
+
+test("blocking quality issues force development-only release and reference repair", async () => {
+  const ir = await makeIr();
+  const promptBundle = compilePromptContracts(ir);
+  const viewCoverage = compileViewCoverage(ir, { requestedViews: { scene: "back" } });
+  const deliveryProfile = compileDeliveryProfile(ir);
+  const qualityReport = compileQualityReport({ ir, promptBundle, viewCoverage, files: syntheticFiles(ir) });
+  const repairPlan = compileRepairPlan(qualityReport);
+  const benchmark = compileBenchmarkResult({ ir, promptBundle, viewCoverage, qualityReport, deliveryProfile, realGenerationValidated: true, humanApproved: true });
+  assert.equal(qualityReport.status, "blocked");
+  assert.equal(benchmark.release_status, "development_only");
+  assert.ok(repairPlan.actions.some((action) => action.action === "requires_reference"));
+});

+ 36 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/delivery-profile-compiler.mjs

@@ -0,0 +1,36 @@
+const profiles = {
+  general_ecommerce: { creative_grammar: "balanced_commercial", information_density: "medium", rule_confidence: "creative_profile_only" },
+  cn_marketplace: { creative_grammar: "evidence_forward", information_density: "high", rule_confidence: "creative_profile_only" },
+  taobao: { creative_grammar: "evidence_forward", information_density: "high", rule_confidence: "creative_profile_only" },
+  global_marketplace: { creative_grammar: "product_clarity_first", information_density: "medium", rule_confidence: "creative_profile_only" },
+  social_media: { creative_grammar: "emotion_and_visual_hook", information_density: "low", rule_confidence: "creative_profile_only" },
+};
+
+const bcp47 = /^[a-z]{2,3}(?:-[A-Z][a-z]{3})?(?:-[A-Z]{2}|-\d{3})?(?:-[a-zA-Z0-9]{5,8})*$/;
+
+export function compileDeliveryProfile(ir, overrides = {}) {
+  const platform = overrides.platform || ir.business_context.platform || "general_ecommerce";
+  const config = profiles[platform];
+  if (!config) throw new Error(`DELIVERY_PROFILE_UNKNOWN_PLATFORM:${platform}`);
+  const targetLanguage = overrides.target_language || ir.business_context.target_language || "zh-CN";
+  if (!bcp47.test(targetLanguage)) throw new Error(`DELIVERY_PROFILE_LANGUAGE_INVALID:${targetLanguage}`);
+  return {
+    schema_version: "1.0",
+    platform,
+    market: overrides.market || ir.business_context.market || "unspecified",
+    target_language: targetLanguage,
+    client_language: overrides.client_language || "zh-CN",
+    provider_prompt_language: "zh-CN",
+    aspect_ratio: ir.layout_plan.canvas.ratio,
+    text_strategy: "text_free_visual_plus_deterministic_overlay",
+    information_density: overrides.information_density || config.information_density,
+    creative_grammar: config.creative_grammar,
+    rule_confidence: config.rule_confidence,
+    compliance_claim: "not_asserted",
+    release_gate: "review_required",
+  };
+}
+
+export function listDeliveryProfiles() {
+  return Object.keys(profiles);
+}

+ 108 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/design-ir-compiler.mjs

@@ -0,0 +1,108 @@
+import { compileCommercialCopy, classifyProductForm, detectVerifiedBrand } from "./commercial-copy-compiler.mjs";
+import { compileDesignTokens } from "./design-token-compiler.mjs";
+import { selectDesignRecipes, validateRecipeSet } from "./design-recipe-registry.mjs";
+
+const requiredSections = ["product_profile", "business_context", "creative_direction", "page_plans", "copy_plan", "scene_plan", "layout_plan", "render_strategy", "acceptance_checks"];
+
+function evidenceMap(product = {}) {
+  const facts = product.verified_visible_facts || product.visible_facts || [];
+  const visibleText = product.visible_text || [];
+  return {
+    ...Object.fromEntries(facts.map((value, index) => [`visible_fact:${index + 1}`, String(value)])),
+    ...Object.fromEntries(visibleText.map((value, index) => [`visible_text:${index + 1}`, String(value?.text || value?.value || value)])),
+    visible_specs: product.verified_specs || product.visible_specs || {},
+  };
+}
+
+export function validateDesignIr(ir) {
+  for (const section of requiredSections) if (!(section in (ir || {}))) throw new Error(`DESIGN_IR_SECTION_REQUIRED:${section}`);
+  if (ir.schema_version !== "1.1") throw new Error("DESIGN_IR_VERSION_UNSUPPORTED");
+  if (!Array.isArray(ir.page_plans) || ir.page_plans.length !== 3) throw new Error("DESIGN_IR_THREE_PAGE_PLAN_REQUIRED");
+  if (!Array.isArray(ir.copy_plan?.points) || !ir.copy_plan.points.length) throw new Error("DESIGN_IR_EVIDENCE_POINT_REQUIRED");
+  const evidence = ir.evidence_map || {};
+  for (const point of ir.copy_plan?.points || []) {
+    if (!point.evidenceRefs?.length) throw new Error("DESIGN_IR_POINT_EVIDENCE_REQUIRED");
+    for (const ref of point.evidenceRefs || []) if (!(ref in evidence)) throw new Error(`DESIGN_IR_EVIDENCE_NOT_FOUND:${ref}`);
+  }
+  const copy = JSON.stringify(ir.copy_plan || {});
+  if (ir.product_profile?.brand?.status !== "verified" && /品牌识别|记住品牌|品牌视觉|品牌呈现/i.test(copy)) throw new Error("DESIGN_IR_UNVERIFIED_BRAND_CLAIM");
+  if (ir.product_profile?.product_form !== "packaged_goods" && /包装|包装正面|包装规格/i.test(copy)) throw new Error("DESIGN_IR_NON_PACKAGE_CLAIM");
+  validateRecipeSet(ir.layout_plan?.recipe_set);
+  return ir;
+}
+
+export async function compileDesignIr(product = {}, productLayer, context = {}) {
+  const productForm = classifyProductForm(product);
+  const brand = detectVerifiedBrand(product);
+  const tokens = await compileDesignTokens(productLayer.buffer);
+  const copy = compileCommercialCopy(product, { productForm, brand });
+  const recipeSelection = selectDesignRecipes({
+    productForm,
+    width: productLayer.width,
+    height: productLayer.height,
+    pointCount: copy.points.length,
+  });
+  const recipeSet = { style: recipeSelection.style, hero: recipeSelection.hero, detail: recipeSelection.detail, scene: recipeSelection.scene };
+  const evidence = evidenceMap(product);
+  const pointRefs = copy.points.flatMap((point) => point.evidenceRefs);
+  const ir = {
+    schema_version: "1.1",
+    product_profile: {
+      category: String(product.category || "通用商品"),
+      product_label: copy.productLabel,
+      product_form: productForm,
+      brand,
+      visible_fact_refs: Object.keys(evidence).filter((key) => key.startsWith("visible_fact:")),
+      visible_specs: evidence.visible_specs,
+    },
+    business_context: {
+      platform: context.platform || "general_ecommerce",
+      market: context.market || "unspecified",
+      target_language: context.targetLanguage || "zh-CN",
+      output_level: "designed_asset",
+    },
+    creative_direction: {
+      color_strategy: "product_pixel_derived",
+      visual_system: productForm === "packaged_goods" ? "packaging_editorial" : "product_functional_editorial",
+      style_recipe: recipeSelection.style,
+      recipe_selection_basis: recipeSelection.selection_basis,
+      tokens,
+    },
+    page_plans: [
+      { id: "hero", sales_task: "快速识别商品与核心视觉特征", evidence_refs: pointRefs.slice(0, 1), layout_recipe: recipeSelection.hero, render_strategy: "derived_affine" },
+      { id: "selling_point", sales_task: "解释可见设计细节与规格", evidence_refs: pointRefs, layout_recipe: recipeSelection.detail, render_strategy: "reference_exact" },
+      { id: "scene", sales_task: "建立不改变商品结构的使用情境", evidence_refs: pointRefs.slice(0, 1), layout_recipe: recipeSelection.scene, render_strategy: "derived_affine" },
+    ],
+    copy_plan: copy,
+    scene_plan: {
+      scene_family: recipeSelection.scene,
+      recipe_id: recipeSelection.scene,
+      product_relation: "product remains unobstructed on a stable surface",
+      forbidden_reveals: ["back", "interior", "unseen_side"],
+    },
+    layout_plan: {
+      canvas: { width: 1200, height: 1200, ratio: "1:1" },
+      safe_area: { left: 55, top: 55, right: 55, bottom: 55 },
+      recipe_set: recipeSet,
+      recipe_selection_basis: recipeSelection.selection_basis,
+      page_recipes: [recipeSelection.hero, recipeSelection.detail, recipeSelection.scene],
+      text_rules: { language: "zh-CN", wrapping: "semantic_lines", overflow: "fail_closed", minimum_font_size: 15 },
+    },
+    render_strategy: {
+      product_pixels: "reference_preserved",
+      hero: "derived_affine",
+      selling_point: "reference_exact",
+      scene: "derived_affine",
+      paid_image_generation: false,
+    },
+    acceptance_checks: [
+      "all_copy_has_valid_evidence_or_is_explicit_scene_language",
+      "no_unverified_brand_claim",
+      "no_packaging_claim_for_non_package_product",
+      "all_text_inside_safe_area",
+      "product_identity_pixels_preserved",
+    ],
+    evidence_map: evidence,
+  };
+  return validateDesignIr(ir);
+}

+ 62 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/design-recipe-registry.mjs

@@ -0,0 +1,62 @@
+const recipes = {
+  style: {
+    natural_editorial: { id: "natural_editorial", family: "organic", contrast: "soft", geometry: "arches" },
+    technical_precision: { id: "technical_precision", family: "technical", contrast: "high", geometry: "grid" },
+    tactile_editorial: { id: "tactile_editorial", family: "tactile", contrast: "soft", geometry: "curves" },
+    gallery_minimal: { id: "gallery_minimal", family: "gallery", contrast: "balanced", geometry: "blocks" },
+  },
+  hero: {
+    portrait_editorial: { id: "portrait_editorial", productWidth: 570, productLeft: 620, minTop: 365, rotate: -0.35, shear: -0.01 },
+    landscape_split: { id: "landscape_split", productWidth: 620, productLeft: 560, minTop: 470, rotate: -0.8, shear: -0.018 },
+    soft_pedestal: { id: "soft_pedestal", productWidth: 500, productLeft: 665, minTop: 395, rotate: -0.2, shear: 0 },
+    gallery_offset: { id: "gallery_offset", productWidth: 560, productLeft: 620, minTop: 420, rotate: -0.4, shear: -0.008 },
+  },
+  detail: {
+    portrait_evidence: { id: "portrait_evidence", productWidth: 455, productLeft: 110, centerY: 675 },
+    landscape_evidence: { id: "landscape_evidence", productWidth: 515, productLeft: 80, centerY: 655 },
+    soft_evidence: { id: "soft_evidence", productWidth: 440, productLeft: 120, centerY: 680 },
+    gallery_evidence: { id: "gallery_evidence", productWidth: 480, productLeft: 100, centerY: 670 },
+  },
+  scene: {
+    warm_countertop: { id: "warm_countertop", productWidth: 520, productLeft: 665, centerY: 735, rotate: 0.2, sceneFamily: "countertop" },
+    technical_workspace: { id: "technical_workspace", productWidth: 540, productLeft: 630, centerY: 735, rotate: 0.15, sceneFamily: "workspace" },
+    textile_studio: { id: "textile_studio", productWidth: 465, productLeft: 700, centerY: 720, rotate: -0.1, sceneFamily: "studio" },
+    gallery_podium: { id: "gallery_podium", productWidth: 500, productLeft: 670, centerY: 730, rotate: 0, sceneFamily: "gallery" },
+  },
+};
+
+export function getDesignRecipe(kind, id) {
+  const recipe = recipes[kind]?.[id];
+  if (!recipe) throw new Error(`DESIGN_RECIPE_NOT_FOUND:${kind}:${id}`);
+  return recipe;
+}
+
+export function validateRecipeSet(recipeSet) {
+  for (const kind of ["style", "hero", "detail", "scene"]) getDesignRecipe(kind, recipeSet?.[kind]);
+  return recipeSet;
+}
+
+export function selectDesignRecipes({ productForm, width, height, pointCount = 0 } = {}) {
+  const aspectRatio = width > 0 && height > 0 ? width / height : 1;
+  const orientation = aspectRatio >= 1.35 ? "landscape" : aspectRatio <= 0.8 ? "portrait" : "balanced";
+  const informationDensity = pointCount >= 3 ? "rich" : "focused";
+  let recipeSet;
+  if (productForm === "packaged_goods") {
+    recipeSet = { style: "natural_editorial", hero: orientation === "landscape" ? "landscape_split" : "portrait_editorial", detail: informationDensity === "focused" ? "gallery_evidence" : orientation === "landscape" ? "landscape_evidence" : "portrait_evidence", scene: "warm_countertop" };
+  } else if (productForm === "durable_goods") {
+    recipeSet = { style: "technical_precision", hero: orientation === "portrait" ? "gallery_offset" : "landscape_split", detail: informationDensity === "focused" ? "gallery_evidence" : orientation === "portrait" ? "gallery_evidence" : "landscape_evidence", scene: "technical_workspace" };
+  } else if (productForm === "soft_goods") {
+    recipeSet = { style: "tactile_editorial", hero: "soft_pedestal", detail: informationDensity === "focused" ? "gallery_evidence" : "soft_evidence", scene: "textile_studio" };
+  } else {
+    recipeSet = { style: "gallery_minimal", hero: orientation === "landscape" ? "landscape_split" : "gallery_offset", detail: "gallery_evidence", scene: "gallery_podium" };
+  }
+  validateRecipeSet(recipeSet);
+  return {
+    ...recipeSet,
+    selection_basis: { product_form: productForm, aspect_ratio: Number(aspectRatio.toFixed(3)), orientation, information_density: informationDensity },
+  };
+}
+
+export function listDesignRecipes() {
+  return Object.fromEntries(Object.entries(recipes).map(([kind, values]) => [kind, Object.keys(values)]));
+}

+ 42 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/design-token-compiler.mjs

@@ -0,0 +1,42 @@
+import sharp from "sharp";
+
+const hex = (rgb) => `#${rgb.map((value) => Math.max(0, Math.min(255, Math.round(value))).toString(16).padStart(2, "0")).join("").toUpperCase()}`;
+const mix = (a, b, ratio) => a.map((value, index) => value * (1 - ratio) + b[index] * ratio);
+
+export async function compileDesignTokens(productBuffer) {
+  const { data, info } = await sharp(productBuffer).resize({ width: 160 }).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
+  const buckets = new Map();
+  for (let index = 0; index < data.length; index += info.channels) {
+    if (data[index + 3] < 180) continue;
+    const rgb = [data[index], data[index + 1], data[index + 2]];
+    const max = Math.max(...rgb); const min = Math.min(...rgb);
+    if (max > 242 && max - min < 12) continue;
+    const key = rgb.map((value) => Math.floor(value / 32) * 32).join(",");
+    buckets.set(key, (buckets.get(key) || 0) + 1);
+  }
+  const ranked = [...buckets.entries()].map(([key, count]) => {
+    const rgb = key.split(",").map(Number).map((value) => value + 16);
+    const saturation = Math.max(...rgb) - Math.min(...rgb);
+    const luminance = rgb[0] * 0.2126 + rgb[1] * 0.7152 + rgb[2] * 0.0722;
+    return { rgb, count, saturation, luminance, score: count * (1 + saturation / 55) * (luminance < 210 ? 1.25 : 0.5) };
+  }).sort((a, b) => b.score - a.score);
+  const primary = ranked.find((item) => item.saturation >= 18 && item.luminance < 190)?.rgb
+    || ranked[0]?.rgb
+    || [38, 49, 38];
+  const accentCandidate = ranked.find((item) => (
+    item.saturation > 24
+    && item.rgb[0] > 120
+    && item.rgb[0] >= item.rgb[1] * 0.95
+    && item.rgb[1] > item.rgb[2] * 1.12
+  ));
+  const accent = accentCandidate?.rgb || mix(primary, [218, 160, 72], 0.58);
+  return {
+    primary: hex(primary),
+    primarySoft: hex(mix(primary, [255, 255, 255], 0.72)),
+    primaryDark: hex(mix(primary, [15, 20, 16], 0.42)),
+    surface: hex(mix(primary, [250, 247, 239], 0.88)),
+    accent: hex(accent),
+    ink: "#1D2922",
+    lightText: "#FAF7EF",
+  };
+}

+ 106 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/designed-asset-runner.mjs

@@ -0,0 +1,106 @@
+import { createHash } from "node:crypto";
+import { mkdir, writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
+import sharp from "sharp";
+import { buildProductLayer } from "./product-layer-builder.mjs";
+import { composeDesignedPages } from "./layout-compositor.mjs";
+import { compilePageSpec } from "./page-spec-compiler.mjs";
+import { routeViewStrategy } from "./view-strategy-router.mjs";
+import { compilePromptContracts } from "./prompt-contract-compiler.mjs";
+import { compileViewCoverage } from "./view-coverage-compiler.mjs";
+import { compileDeliveryProfile } from "./delivery-profile-compiler.mjs";
+import { compileQualityReport, compileRepairPlan } from "./quality-repair-compiler.mjs";
+import { compileBenchmarkResult } from "./benchmark-harness.mjs";
+import { assertDesignedAssetProductEvidence } from "./product-evidence-gate.mjs";
+
+const names = ["01-主视觉设计稿.png", "02-卖点设计稿.png", "03-场景设计稿.png"];
+const digest = (buffer) => createHash("sha256").update(buffer).digest("hex");
+
+export async function runDesignedAsset({ reference, outputDirectory, productUnderstanding = {}, externalCalls = {}, viewOptions = {}, deliveryOverrides = {}, releaseEvidence = {} } = {}) {
+  if (!outputDirectory) throw new Error("DESIGNED_ASSET_OUTPUT_DIRECTORY_REQUIRED");
+  assertDesignedAssetProductEvidence(productUnderstanding);
+  const directory = resolve(outputDirectory);
+  await mkdir(directory, { recursive: true });
+  const productLayer = await buildProductLayer(reference);
+  await writeFile(join(directory, "product-layer.png"), productLayer.buffer);
+  const compiledPageSpec = await compilePageSpec(productUnderstanding, productLayer);
+  const { design_ir: designIr, ...pageSpec } = compiledPageSpec;
+  const promptBundle = compilePromptContracts(designIr);
+  const viewCoverage = compileViewCoverage(designIr, viewOptions);
+  const deliveryProfile = compileDeliveryProfile(designIr, deliveryOverrides);
+  const pages = await composeDesignedPages(productLayer, pageSpec);
+  const files = [];
+  for (let index = 0; index < pages.length; index += 1) {
+    const file = join(directory, names[index]);
+    await writeFile(file, pages[index].buffer);
+    const meta = await sharp(pages[index].buffer).metadata();
+    files.push({ file, slot: pages[index].slot, strategy: pages[index].strategy, width: meta.width, height: meta.height, sha256: digest(pages[index].buffer) });
+  }
+  const qualityReport = compileQualityReport({ ir: designIr, promptBundle, viewCoverage, files });
+  const repairPlan = compileRepairPlan(qualityReport);
+  const benchmarkResult = compileBenchmarkResult({
+    ir: designIr,
+    promptBundle,
+    viewCoverage,
+    qualityReport,
+    deliveryProfile,
+    realGenerationValidated: releaseEvidence.real_generation_validated,
+    humanApproved: releaseEvidence.human_approved,
+  });
+  const controlPayloads = {
+    design_ir: ["design-ir.json", designIr],
+    prompt_contracts: ["prompt-contracts.json", promptBundle],
+    view_coverage: ["view-coverage.json", viewCoverage],
+    quality_report: ["quality-report.json", qualityReport],
+    repair_plan: ["repair-plan.json", repairPlan],
+    delivery_profile: ["delivery-profile.json", deliveryProfile],
+    benchmark_result: ["benchmark-result.json", benchmarkResult],
+  };
+  const controlFiles = {};
+  for (const [key, [filename, value]] of Object.entries(controlPayloads)) {
+    const file = join(directory, filename);
+    await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
+    controlFiles[key] = file;
+  }
+  const promptDirectory = join(directory, "provider-prompts");
+  await mkdir(promptDirectory, { recursive: true });
+  for (let index = 0; index < promptBundle.contracts.length; index += 1) {
+    const contract = promptBundle.contracts[index];
+    const file = join(promptDirectory, `${String(index + 1).padStart(2, "0")}-${contract.page_id.replaceAll("_", "-")}.txt`);
+    await writeFile(file, `${contract.provider_prompt}\n\n负面约束:${contract.negative_prompt}\n`, "utf8");
+  }
+  const manifest = {
+    schema_version: "0.3",
+    status: "complete",
+    output_level: "designed_asset",
+    paid_external_calls: 0,
+    external_calls: {
+      vision_understanding: Number(externalCalls.vision_understanding || 0),
+      fact_audit: Number(externalCalls.fact_audit || 0),
+      asset_upload: Number(externalCalls.asset_upload || 0),
+      paid_image_generation: 0,
+    },
+    product_layer: { ...productLayer, buffer: undefined },
+    design_ir: designIr,
+    page_spec: pageSpec,
+    control_plane: {
+      prompt_contract_status: promptBundle.status,
+      view_coverage_status: viewCoverage.status,
+      quality_status: qualityReport.status,
+      repair_status: repairPlan.status,
+      delivery_profile: deliveryProfile.platform,
+      release_status: benchmarkResult.release_status,
+    },
+    control_files: controlFiles,
+    view_strategies: {
+      hero: routeViewStrategy({ purpose: "creative" }),
+      selling_point: routeViewStrategy({ purpose: "selling_point" }),
+      scene: routeViewStrategy({ purpose: "scene" }),
+    },
+    files,
+    limitations: ["Affine views preserve visible pixels but do not prove a real new camera angle.", "Back, interior and unseen surfaces require additional references."],
+  };
+  const manifestFile = join(directory, "designed-asset-manifest.json");
+  await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
+  return { ...manifest, manifest_file: manifestFile };
+}

+ 49 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/designed-asset-runner.test.mjs

@@ -0,0 +1,49 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { access, mkdtemp, readFile, rm } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import sharp from "sharp";
+import { runDesignedAsset } from "./designed-asset-runner.mjs";
+
+test("runner creates three deterministic designed pages without a paid call", async () => {
+  const root = await mkdtemp(join(tmpdir(), "fmode-designed-"));
+  try {
+    const reference = join(root, "reference.png");
+    await sharp({ create: { width: 500, height: 600, channels: 3, background: "#f4f2ed" } })
+      .composite([{ input: Buffer.from('<svg width="500" height="600"><rect x="110" y="80" width="280" height="440" rx="55" fill="#68794e"/><rect x="205" y="410" width="90" height="55" fill="#151515"/></svg>') }])
+      .png().toFile(reference);
+    const result = await runDesignedAsset({
+      reference,
+      outputDirectory: join(root, "out"),
+      productUnderstanding: {
+        category: "通用商品",
+        product_name: { value: "TEST PRODUCT" },
+        visible_selling_points: [
+          { value: "绿色主体轮廓", basis_refs: ["visible_fact:1"] },
+          { value: "正面黑色标记", basis_refs: ["visible_fact:2"] },
+        ],
+        verified_visible_facts: ["商品具有绿色主体轮廓", "商品正面可见黑色标记"],
+      },
+      externalCalls: { vision_understanding: 1, fact_audit: 1, asset_upload: 1 },
+    });
+    assert.equal(result.status, "complete");
+    assert.equal(result.paid_external_calls, 0);
+    assert.equal(result.files.length, 3);
+    assert.ok(result.files.every((file) => file.width === 1200 && file.height === 1200));
+    assert.equal(result.page_spec.copy.heroTitle, "TEST PRODUCT");
+    assert.equal(result.design_ir.schema_version, "1.1");
+    assert.ok(result.design_ir.layout_plan.recipe_set.style);
+    assert.equal(result.external_calls.vision_understanding, 1);
+    assert.equal(result.external_calls.paid_image_generation, 0);
+    assert.ok(result.product_layer.edgeCleanup);
+    assert.equal(result.control_plane.release_status, "review_required");
+    assert.equal(result.control_plane.quality_status, "review_required");
+    assert.deepEqual(Object.keys(result.control_files).sort(), ["benchmark_result", "delivery_profile", "design_ir", "prompt_contracts", "quality_report", "repair_plan", "view_coverage"]);
+    await Promise.all(Object.values(result.control_files).map((file) => access(file)));
+    const benchmark = JSON.parse(await readFile(result.control_files.benchmark_result, "utf8"));
+    assert.equal(benchmark.release_status, "review_required");
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});

+ 22 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/font-resolver.mjs

@@ -0,0 +1,22 @@
+import { access } from "node:fs/promises";
+import { constants } from "node:fs";
+import { join } from "node:path";
+
+async function readable(file) {
+  if (!file) return false;
+  try { await access(file, constants.R_OK); return true; } catch { return false; }
+}
+
+export async function resolveChineseFont() {
+  const candidates = [
+    process.env.FMODE_IMAGE_SET_FONT_FILE,
+    process.platform === "win32" ? join(process.env.WINDIR || "C:\\Windows", "Fonts", "msyh.ttc") : "",
+    process.platform === "win32" ? join(process.env.WINDIR || "C:\\Windows", "Fonts", "msyhbd.ttc") : "",
+    "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
+    "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
+  ].filter(Boolean);
+  for (const fontfile of candidates) {
+    if (await readable(fontfile)) return { family: "Microsoft YaHei,Noto Sans CJK SC,sans-serif", fontfile, source: fontfile === process.env.FMODE_IMAGE_SET_FONT_FILE ? "environment" : "system" };
+  }
+  return { family: "sans-serif", fontfile: null, source: "fontconfig_fallback" };
+}

+ 84 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/layout-compositor.mjs

@@ -0,0 +1,84 @@
+import sharp from "sharp";
+import { fitText } from "./text-layout.mjs";
+import { getDesignRecipe, validateRecipeSet } from "./design-recipe-registry.mjs";
+import { renderDetailBackdrop, renderHeroBackdrop, renderSceneBackdrop } from "./recipe-svg-renderer.mjs";
+
+const SIZE = 1200;
+const xml = (value) => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
+const svg = (body) => Buffer.from(`<svg width="${SIZE}" height="${SIZE}" xmlns="http://www.w3.org/2000/svg"><style>.cn{font-family:"Microsoft YaHei","Noto Sans CJK SC",Arial,sans-serif}.en{font-family:Arial,sans-serif;letter-spacing:3px}</style>${body}</svg>`);
+
+function textSvg(value, { x, y, maxWidth, fontSize, minFontSize = 15, maxLines = 2, className = "cn", fill = "#111", weight = 400, anchor = "start" }) {
+  const layout = fitText(value, { maxWidth, fontSize, minFontSize, maxLines });
+  const spans = layout.lines.map((line, index) => `<tspan x="${x}" dy="${index === 0 ? 0 : layout.lineHeight}">${xml(line)}</tspan>`).join("");
+  return { ...layout, svg: `<text class="${className}" x="${x}" y="${y}" font-size="${layout.fontSize}" font-weight="${weight}" text-anchor="${anchor}" fill="${fill}">${spans}</text>` };
+}
+
+async function productVariant(input, { width, rotate = 0, shear = 0 } = {}) {
+  let pipeline = sharp(input).resize({ width, kernel: sharp.kernel.lanczos3 });
+  if (shear) pipeline = pipeline.affine([[1, shear], [0, 1]], { interpolator: sharp.interpolators.bicubic, background: { r: 0, g: 0, b: 0, alpha: 0 } });
+  if (rotate) pipeline = pipeline.rotate(rotate, { background: { r: 0, g: 0, b: 0, alpha: 0 } });
+  return pipeline.png().toBuffer();
+}
+
+async function compose(background, product, left, top, shadow = null) {
+  const layers = [];
+  if (shadow) layers.push({ input: shadow, left: 0, top: 0 });
+  layers.push({ input: product, left, top });
+  return sharp(background).composite(layers).png().toBuffer();
+}
+
+const shadowSvg = (cx, cy, rx, opacity = 0.2) => svg(`<defs><filter id="b"><feGaussianBlur stdDeviation="17"/></filter></defs><ellipse cx="${cx}" cy="${cy}" rx="${rx}" ry="35" fill="#111A14" opacity="${opacity}" filter="url(#b)"/>`);
+
+function pointCards(points, tokens) {
+  return points.slice(0, 3).map((point, index) => {
+    const y = 260 + index * 220;
+    const title = textSvg(point.title, { x: 754, y: y + 53, maxWidth: 345, fontSize: 29, minFontSize: 21, maxLines: 2, fill: tokens.primaryDark, weight: 800 });
+    const descriptionY = y + 104 + Math.max(0, title.lines.length - 1) * title.lineHeight;
+    const description = textSvg(point.description, { x: 720, y: descriptionY, maxWidth: 380, fontSize: 20, minFontSize: 16, maxLines: 2, fill: "#667068" });
+    return `<rect x="680" y="${y}" width="455" height="194" rx="25" fill="#FFFFFF"/><circle cx="720" cy="${y + 43}" r="17" fill="${tokens.accent}"/><text class="en" x="720" y="${y + 49}" text-anchor="middle" font-size="12" font-weight="700" fill="${tokens.primaryDark}">${String(index + 1).padStart(2, "0")}</text>${title.svg}${description.svg}`;
+  }).join("");
+}
+
+export async function composeDesignedPages(productLayer, pageSpec) {
+  const { copy, tokens } = pageSpec;
+  const recipeSet = validateRecipeSet(pageSpec.layout_plan?.recipe_set);
+  const heroRecipe = getDesignRecipe("hero", recipeSet.hero);
+  const detailRecipe = getDesignRecipe("detail", recipeSet.detail);
+  const sceneRecipe = getDesignRecipe("scene", recipeSet.scene);
+  const heroTitle = textSvg(copy.heroTitle, { x: 70, y: 255, maxWidth: 1020, fontSize: 67, minFontSize: 42, maxLines: 2, fill: tokens.primaryDark, weight: 800 });
+  const heroRuleY = 305 + (heroTitle.lines.length - 1) * heroTitle.lineHeight;
+  const heroSubtitle = textSvg(copy.heroSubtitle, { x: 70, y: heroRuleY + 72, maxWidth: 630, fontSize: 25, minFontSize: 19, maxLines: 2, fill: tokens.primary });
+  const heroBadge = textSvg(copy.heroBadge, { x: 250, y: heroRuleY + 201, maxWidth: 320, fontSize: 18, minFontSize: 14, maxLines: 1, fill: tokens.lightText, anchor: "middle" });
+  const heroHeader = textSvg(`${copy.productLabel} / 01`, { x: 70, y: 78, maxWidth: 760, fontSize: 17, minFontSize: 12, maxLines: 1, className: "en", fill: tokens.primary, weight: 700 });
+  const heroFooter = textSvg("PRODUCT VISUAL / DESIGN EDITION", { x: 70, y: 1110, maxWidth: 520, fontSize: 13, minFontSize: 11, maxLines: 1, className: "en", fill: tokens.primary });
+
+  const heroProduct = await productVariant(productLayer.buffer, { width: heroRecipe.productWidth, rotate: heroRecipe.rotate, shear: heroRecipe.shear });
+  const heroMeta = await sharp(heroProduct).metadata();
+  const heroTop = Math.max(heroRecipe.minTop, Math.round((SIZE - heroMeta.height) / 2 + 120));
+  const heroShadowX = heroRecipe.productLeft + Math.round(heroMeta.width / 2);
+  const hero = await compose(svg(`${renderHeroBackdrop(recipeSet.style, tokens)}${heroHeader.svg}${heroTitle.svg}<rect x="70" y="${heroRuleY}" width="100" height="8" rx="4" fill="${tokens.accent}"/>${heroSubtitle.svg}<rect x="70" y="${heroRuleY + 162}" width="360" height="58" rx="29" fill="${tokens.primary}"/>${heroBadge.svg}${heroFooter.svg}`), heroProduct, heroRecipe.productLeft, heroTop, shadowSvg(heroShadowX, Math.min(1030, heroTop + heroMeta.height - 12), Math.round(heroMeta.width * 0.38), 0.18));
+
+  const detailTitle = textSvg(copy.sellingTitle, { x: 64, y: 132, maxWidth: 1040, fontSize: 50, minFontSize: 38, maxLines: 1, fill: tokens.lightText, weight: 800 });
+  const detailProduct = await productVariant(productLayer.buffer, { width: detailRecipe.productWidth });
+  const detailMeta = await sharp(detailProduct).metadata();
+  const detailTop = Math.round(detailRecipe.centerY - detailMeta.height / 2);
+  const detailShadowX = detailRecipe.productLeft + Math.round(detailMeta.width / 2);
+  const detail = await compose(svg(`${renderDetailBackdrop(recipeSet.style, tokens)}<text class="en" x="64" y="62" font-size="16" fill="${tokens.accent}">DESIGN DETAILS / 02</text>${detailTitle.svg}${pointCards(copy.points, tokens)}`), detailProduct, detailRecipe.productLeft, detailTop, shadowSvg(detailShadowX, Math.min(1000, detailTop + detailMeta.height - 5), Math.round(detailMeta.width * 0.4), 0.16));
+
+  const sceneProduct = await productVariant(productLayer.buffer, { width: sceneRecipe.productWidth, rotate: sceneRecipe.rotate });
+  const sceneMeta = await sharp(sceneProduct).metadata();
+  const sceneTop = Math.max(350, Math.round(sceneRecipe.centerY - sceneMeta.height / 2));
+  const [sceneLine1, sceneLine2] = copy.sceneTitleLines;
+  const sceneTitle1 = textSvg(sceneLine1, { x: 65, y: 260, maxWidth: 420, fontSize: 60, minFontSize: 44, maxLines: 1, fill: tokens.lightText, weight: 800 });
+  const sceneTitle2 = textSvg(sceneLine2, { x: 65, y: 340, maxWidth: 420, fontSize: 60, minFontSize: 44, maxLines: 1, fill: tokens.lightText, weight: 800 });
+  const sceneSubtitle = textSvg(copy.sceneSubtitle, { x: 65, y: 458, maxWidth: 500, fontSize: 22, minFontSize: 17, maxLines: 2, fill: tokens.primarySoft });
+  const sceneFooter = textSvg(`${copy.productLabel} / DAILY SETTING`, { x: 65, y: 1110, maxWidth: 750, fontSize: 13, minFontSize: 10, maxLines: 1, className: "en", fill: tokens.primaryDark });
+  const sceneShadowX = sceneRecipe.productLeft + Math.round(sceneMeta.width / 2);
+  const scene = await compose(svg(`${renderSceneBackdrop(recipeSet.scene, tokens)}<text class="en" x="65" y="80" font-size="16" fill="${tokens.accent}">LIFESTYLE SET / 03</text>${sceneTitle1.svg}${sceneTitle2.svg}<rect x="65" y="386" width="95" height="8" rx="4" fill="${tokens.accent}"/>${sceneSubtitle.svg}${sceneFooter.svg}`), sceneProduct, sceneRecipe.productLeft, sceneTop, shadowSvg(sceneShadowX, Math.min(1035, sceneTop + sceneMeta.height - 8), Math.round(sceneMeta.width * 0.4), 0.25));
+
+  return [
+    { slot: "hero", strategy: "derived_affine", buffer: hero },
+    { slot: "selling_point", strategy: "reference_exact", buffer: detail },
+    { slot: "scene", strategy: "derived_affine", buffer: scene },
+  ];
+}

+ 13 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/page-spec-compiler.mjs

@@ -0,0 +1,13 @@
+import { compileDesignIr } from "./design-ir-compiler.mjs";
+
+export async function compilePageSpec(productUnderstanding, productLayer) {
+  const designIr = await compileDesignIr(productUnderstanding, productLayer);
+  return {
+    schema_version: "0.3",
+    copy: designIr.copy_plan,
+    tokens: designIr.creative_direction.tokens,
+    layout_plan: designIr.layout_plan,
+    scene_plan: designIr.scene_plan,
+    design_ir: designIr,
+  };
+}

+ 37 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/pango-text-metrics.mjs

@@ -0,0 +1,37 @@
+import sharp from "sharp";
+import { fitChineseText } from "./text-layout.mjs";
+import { resolveChineseFont } from "./font-resolver.mjs";
+
+const escapeMarkup = (value) => String(value).replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
+
+async function glyphWidth(character, fontSize, font) {
+  if (/\s/.test(character)) return fontSize * 0.34;
+  const input = { text: { text: escapeMarkup(character), font: `${font.family} ${fontSize}`, dpi: 72, rgba: true } };
+  if (font.fontfile) input.text.fontfile = font.fontfile;
+  const { info } = await sharp(input).png().toBuffer({ resolveWithObject: true });
+  return Math.max(1, info.width);
+}
+
+export async function measuredGlyphWidths(value, fontSize, font = null) {
+  const resolved = font || await resolveChineseFont();
+  const characters = [...new Set(Array.from(String(value || "")))];
+  const entries = await Promise.all(characters.map(async (character) => [character, await glyphWidth(character, fontSize, resolved)]));
+  return { font: resolved, widths: new Map(entries) };
+}
+
+export async function fitTextWithPango(value, constraints = {}) {
+  const font = constraints.font || await resolveChineseFont();
+  const initial = Number(constraints.fontSize);
+  const minimum = Number(constraints.minFontSize ?? 14);
+  for (let size = initial; size >= minimum; size -= 1) {
+    const metrics = await measuredGlyphWidths(value, size, font);
+    const measure = (candidate) => Array.from(String(candidate)).reduce((sum, character) => sum + (metrics.widths.get(character) || size), 0);
+    try {
+      const result = fitChineseText(value, { ...constraints, fontSize: size, minFontSize: size, measure });
+      return { ...result, font: metrics.font, measurement: "sharp_pango_glyph_metrics" };
+    } catch (error) {
+      if (!String(error?.message).includes("TEXT_OVERFLOW")) throw error;
+    }
+  }
+  throw new Error(`DESIGNED_ASSET_TEXT_OVERFLOW:${String(value)}`);
+}

+ 13 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/pango-text-metrics.test.mjs

@@ -0,0 +1,13 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { fitTextWithPango, measuredGlyphWidths } from "./pango-text-metrics.mjs";
+
+test("Sharp/Pango supplies actual Chinese glyph metrics and preserves full copy", async () => {
+  const metrics = await measuredGlyphWidths("真实ABC", 24);
+  assert.ok(metrics.widths.get("真") > 0);
+  assert.ok(metrics.widths.get("A") > 0);
+  const result = await fitTextWithPango("完整中文标题,不能截断。", { maxWidth: 180, maxHeight: 100, fontSize: 28, minFontSize: 14, maxLines: 3 });
+  assert.equal(result.lines.join(""), "完整中文标题,不能截断。");
+  assert.equal(result.measurement, "sharp_pango_glyph_metrics");
+  assert.ok(result.font.source);
+});

+ 36 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/product-evidence-gate.mjs

@@ -0,0 +1,36 @@
+const nonProductSignals = [
+  /(?:不是|并非|非)商品(?:实物|实体|图像|图片|渲染图)?/i,
+  /无(?:任何|可见)?商品(?:实体|图像|图片|内容)/i,
+  /不(?:包含|存在|展示).*商品(?:实体|图像|图片|内容)/i,
+  /不得视为商品视觉证据/i,
+  /图像附件.*(?:未加载|不可视|无法验证)/i,
+  /(?:VS\s*Code|编辑器|开发调试|软件开发).*截图/i,
+  /界面截图.*(?:非商品|无商品)/i,
+];
+
+export class DesignedAssetProductEvidenceError extends Error {
+  constructor(message = "当前参考图未显示可识别商品,请重新附加一张清晰商品图片。") {
+    super(message);
+    this.name = "DesignedAssetProductEvidenceError";
+    this.code = "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE";
+  }
+}
+
+function evidenceText(product = {}) {
+  return JSON.stringify({
+    product_name: product.product_name,
+    verified_visible_facts: product.verified_visible_facts,
+    product_invariants: product.product_invariants,
+    unsupported_claims: product.unsupported_claims,
+    unknown_features: product.unknown_features,
+    identity_manifest: product.identity_manifest,
+  });
+}
+
+export function assertDesignedAssetProductEvidence(product = {}) {
+  const text = evidenceText(product);
+  if (nonProductSignals.some((pattern) => pattern.test(text))) {
+    throw new DesignedAssetProductEvidenceError();
+  }
+  return product;
+}

+ 40 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/product-evidence-gate.test.mjs

@@ -0,0 +1,40 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { access, mkdtemp, rm } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { assertDesignedAssetProductEvidence } from "./product-evidence-gate.mjs";
+import { runDesignedAsset } from "./designed-asset-runner.mjs";
+
+const screenshotAudit = {
+  category: "中性品类",
+  product_name: { value: "无可见商品名称或品牌" },
+  verified_visible_facts: ["界面为 VS Code 编辑器截图"],
+  product_invariants: ["此为软件开发调试界面截图,非商品实物或渲染图", "图像附件未加载成功,不得视为商品视觉证据"],
+  identity_manifest: { must_preserve: ["所有产品表述均属上下文占位符,无对应可见实体"] },
+};
+
+test("explicit non-product audit fails before product layering", async () => {
+  assert.throws(() => assertDesignedAssetProductEvidence(screenshotAudit), (error) => error.code === "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE");
+  const root = await mkdtemp(join(tmpdir(), "fmode-non-product-"));
+  const output = join(root, "designed-assets");
+  try {
+    await assert.rejects(
+      runDesignedAsset({ reference: join(root, "missing.png"), outputDirectory: output, productUnderstanding: screenshotAudit }),
+      (error) => error.code === "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE",
+    );
+    await assert.rejects(access(join(output, "product-layer.png")));
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});
+
+test("normal visible product evidence passes the gate", () => {
+  const product = {
+    category: "键盘",
+    product_name: { value: "黑色紧凑型键盘" },
+    verified_visible_facts: ["键盘主体为黑色", "右上角可见铜色旋钮"],
+    identity_manifest: { must_preserve: ["黑色主体", "铜色旋钮"] },
+  };
+  assert.equal(assertDesignedAssetProductEvidence(product), product);
+});

+ 122 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/product-layer-builder.mjs

@@ -0,0 +1,122 @@
+import { createHash } from "node:crypto";
+import sharp from "sharp";
+
+const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
+
+async function loadReference(reference) {
+  if (/^https?:\/\//i.test(String(reference))) {
+    const response = await fetch(reference);
+    if (!response.ok) throw new Error(`DESIGNED_ASSET_REFERENCE_FETCH_FAILED: ${response.status}`);
+    return Buffer.from(await response.arrayBuffer());
+  }
+  return reference;
+}
+
+export async function buildProductLayer(reference) {
+  if (!reference) throw new Error("DESIGNED_ASSET_REFERENCE_REQUIRED");
+  const input = await loadReference(reference);
+  const { data, info } = await sharp(input).removeAlpha().raw().toBuffer({ resolveWithObject: true });
+  const points = [[3, 3], [info.width - 4, 3], [3, info.height - 4], [info.width - 4, info.height - 4]];
+  const background = [0, 1, 2].map((channel) => Math.round(points.reduce(
+    (sum, [x, y]) => sum + data[(y * info.width + x) * 3 + channel], 0,
+  ) / points.length));
+  const backgroundLuminance = background[0] * 0.2126 + background[1] * 0.7152 + background[2] * 0.0722;
+  const rgba = Buffer.alloc(info.width * info.height * 4);
+  let strongMinX = info.width; let strongMinY = info.height; let strongMaxX = -1; let strongMaxY = -1;
+
+  for (let y = 0; y < info.height; y += 1) {
+    for (let x = 0; x < info.width; x += 1) {
+      const src = (y * info.width + x) * 3;
+      const [r, g, b] = [data[src], data[src + 1], data[src + 2]];
+      const distance = Math.hypot(r - background[0], g - background[1], b - background[2]);
+      const chroma = Math.max(r, g, b) - Math.min(r, g, b);
+      const luminance = r * 0.2126 + g * 0.7152 + b * 0.0722;
+      const strong = (chroma > 11 && distance > 28)
+        || luminance > backgroundLuminance + 24
+        || (luminance < 72 && distance > 95);
+      if (strong) {
+        strongMinX = Math.min(strongMinX, x); strongMinY = Math.min(strongMinY, y);
+        strongMaxX = Math.max(strongMaxX, x); strongMaxY = Math.max(strongMaxY, y);
+      }
+    }
+  }
+  if (strongMaxX < strongMinX || strongMaxY < strongMinY) throw new Error("DESIGNED_ASSET_PRODUCT_NOT_DETECTED");
+  const strongPadding = 10;
+  const allowed = {
+    left: Math.max(0, strongMinX - strongPadding),
+    top: Math.max(0, strongMinY - strongPadding),
+    right: Math.min(info.width - 1, strongMaxX + strongPadding),
+    bottom: Math.min(info.height - 1, strongMaxY + strongPadding),
+  };
+  const alphaMap = Buffer.alloc(info.width * info.height);
+
+  for (let y = 0; y < info.height; y += 1) {
+    for (let x = 0; x < info.width; x += 1) {
+      const src = (y * info.width + x) * 3;
+      const [r, g, b] = [data[src], data[src + 1], data[src + 2]];
+      const distance = Math.hypot(r - background[0], g - background[1], b - background[2]);
+      const chroma = Math.max(r, g, b) - Math.min(r, g, b);
+      const luminance = r * 0.2126 + g * 0.7152 + b * 0.0722;
+      const inside = x >= allowed.left && x <= allowed.right && y >= allowed.top && y <= allowed.bottom;
+      let alpha = inside ? Math.round(clamp(Math.max((distance - 38) * 5.4, (chroma - 5) * 11, (132 - luminance) * 3.5), 0, 255)) : 0;
+      if (alpha < 34) alpha = 0;
+      else if (alpha < 220) alpha = Math.round(clamp((alpha - 24) * 1.16, 0, 255));
+      alphaMap[y * info.width + x] = alpha;
+    }
+  }
+
+  // Opaque products with light surfaces need a solid interior mask. Preserve the
+  // anti-aliased outer edge, but fill between independently detected left/right
+  // foreground edges so a dark scene cannot bleed through pale packaging.
+  for (let y = allowed.top; y <= allowed.bottom; y += 1) {
+    let first = -1; let last = -1; let strongCount = 0;
+    for (let x = allowed.left; x <= allowed.right; x += 1) {
+      if (alphaMap[y * info.width + x] >= 180) {
+        if (first < 0) first = x;
+        last = x;
+        strongCount += 1;
+      }
+    }
+    const span = last - first + 1;
+    if (first >= 0 && span >= 18 && strongCount >= Math.max(4, Math.round(span * 0.035))) {
+      for (let x = first + 1; x < last; x += 1) alphaMap[y * info.width + x] = Math.max(alphaMap[y * info.width + x], 250);
+    }
+  }
+
+  let minX = info.width; let minY = info.height; let maxX = -1; let maxY = -1; let visiblePixels = 0;
+  for (let y = 0; y < info.height; y += 1) {
+    for (let x = 0; x < info.width; x += 1) {
+      const src = (y * info.width + x) * 3;
+      const dst = (y * info.width + x) * 4;
+      const [r, g, b] = [data[src], data[src + 1], data[src + 2]];
+      const alpha = alphaMap[y * info.width + x];
+      const opacity = Math.max(alpha / 255, 0.16);
+      const corrected = [r, g, b].map((value, channel) => clamp(
+        (value - background[channel] * (1 - opacity)) / opacity,
+        0,
+        255,
+      ));
+      const cleanupStrength = alpha > 220 ? 0.18 : alpha > 100 ? 0.28 : 0.08;
+      rgba[dst] = r * (1 - cleanupStrength) + corrected[0] * cleanupStrength;
+      rgba[dst + 1] = g * (1 - cleanupStrength) + corrected[1] * cleanupStrength;
+      rgba[dst + 2] = b * (1 - cleanupStrength) + corrected[2] * cleanupStrength;
+      rgba[dst + 3] = alpha;
+      if (alpha > 22) {
+        visiblePixels += 1; minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y);
+      }
+    }
+  }
+  if (maxX < minX || maxY < minY) throw new Error("DESIGNED_ASSET_PRODUCT_NOT_DETECTED");
+  const padding = 8;
+  const left = Math.max(0, minX - padding); const top = Math.max(0, minY - padding);
+  const width = Math.min(info.width - left, maxX - minX + 1 + padding * 2);
+  const height = Math.min(info.height - top, maxY - minY + 1 + padding * 2);
+  const buffer = await sharp(rgba, { raw: { width: info.width, height: info.height, channels: 4 } })
+    .extract({ left, top, width, height }).png().toBuffer();
+  return {
+    buffer, width, height, visiblePixels, backgroundEstimate: background,
+    sha256: createHash("sha256").update(buffer).digest("hex"),
+    extractionMethod: "neutral-background-distance-mask-v3",
+    edgeCleanup: "strong-foreground-bounds-interior-solidification-and-background-decontamination",
+  };
+}

+ 128 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/prompt-contract-compiler.mjs

@@ -0,0 +1,128 @@
+import { createHash } from "node:crypto";
+
+const PROMPT_VERSION = "1.0";
+const MAX_PROVIDER_PROMPT_LENGTH = 3000;
+const MAX_LOCALIZED_COPY_LENGTH = 1200;
+const hiddenViewTerms = /背面|内部|底部|不可见侧面|打开内部|拆解结构/;
+const secretOrPath = /api[_-]?key|authorization|bearer\s+|secret|token\s*[=:]|[a-z]:\\|\/users\//i;
+
+const stableDigest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
+const unique = (values) => [...new Set(values.filter(Boolean))];
+
+function pageCopy(copy, pageId) {
+  if (pageId === "hero") return { title: copy.heroTitle, subtitle: copy.heroSubtitle, badge: copy.heroBadge };
+  if (pageId === "selling_point") return { title: copy.sellingTitle, points: copy.points.map(({ title, description }) => ({ title, description })) };
+  return { title_lines: copy.sceneTitleLines, subtitle: copy.sceneSubtitle };
+}
+
+function allowedViews(page) {
+  if (page.render_strategy === "reference_exact") return ["reference_visible_surface"];
+  if (page.render_strategy === "derived_affine") return ["reference_visible_surface", "layout_only_affine"];
+  return ["reference_visible_surface"];
+}
+
+function compileOne(ir, page) {
+  const evidence = Object.fromEntries((page.evidence_refs || []).map((ref) => [ref, ir.evidence_map[ref]]).filter(([, value]) => value !== undefined));
+  const source = {
+    sales_task: page.sales_task,
+    product_identity: {
+      label: ir.product_profile.product_label,
+      form: ir.product_profile.product_form,
+      brand: ir.product_profile.brand,
+      evidence_refs: page.evidence_refs,
+    },
+    evidence_refs: page.evidence_refs,
+    allowed_views: allowedViews(page),
+    forbidden_views: ir.scene_plan.forbidden_reveals,
+    scene_relationship: page.id === "scene" ? ir.scene_plan.product_relation : "商品保持完整无遮挡,装饰元素位于商品之后",
+    composition: {
+      recipe_id: page.layout_recipe,
+      canvas: ir.layout_plan.canvas,
+      safe_area: ir.layout_plan.safe_area,
+      product_priority: "primary",
+    },
+    material_and_light: {
+      style_recipe: ir.creative_direction.style_recipe,
+      preserve_visible_material_response: true,
+      light_language: page.id === "scene" ? "coherent_environmental_light" : "controlled_commercial_light",
+      shadow_language: "physically_attached_soft_shadow",
+    },
+    set_visual_dna: {
+      style_recipe: ir.creative_direction.style_recipe,
+      color_tokens: ir.creative_direction.tokens,
+      product_identity_locked: true,
+    },
+    intentional_variation: page.id === "hero" ? "快速识别与主体优先" : page.id === "selling_point" ? "证据卡片与原视角细节" : "环境代入且商品无遮挡",
+    text_strategy: "generate_text_free_visual_base_then_apply_localized_copy_deterministically",
+    negative_constraints: unique([
+      "禁止改变商品结构、数量、Logo和可见包装文字",
+      "禁止生成背面、内部、底部或参考素材未覆盖的表面",
+      "禁止添加不存在的配件、接口、人物交互或功能演示",
+      "禁止让模型绘制关键营销文案、水印或随机字符",
+      "禁止悬浮、穿插、错误接触阴影和不一致透视",
+    ]),
+  };
+  const providerPrompt = [
+    `页面任务:${source.sales_task}。`,
+    `商品身份:严格保持“${source.product_identity.label}”的参考图可见结构、配色、标记和数量。`,
+    `视角:只展示${source.allowed_views.join("、")};不得展示参考素材未覆盖的背面、内部、底部或新结构。`,
+    `场景关系:${source.scene_relationship}。`,
+    `构图:使用${source.composition.recipe_id}设计语法,商品为第一视觉主体,保持安全区和清晰轮廓。`,
+    `材质与光影:保留可见材质响应,使用${source.material_and_light.light_language}与${source.material_and_light.shadow_language}。`,
+    `整套视觉DNA:${source.set_visual_dna.style_recipe},使用商品像素提取色板,跨页保持身份、色彩和光向协调。`,
+    `本页变化:${source.intentional_variation}。`,
+    "输出无新增营销文字的商业视觉底图,重要文案由本地确定性排版完成。",
+  ].join("");
+  const contract = {
+    prompt_version: PROMPT_VERSION,
+    page_id: page.id,
+    provider: "jimeng_v4_compatible",
+    provider_prompt_language: "zh-CN",
+    target_copy_language: ir.business_context.target_language,
+    prompt_source: source,
+    provider_prompt: providerPrompt,
+    negative_prompt: source.negative_constraints.join(";"),
+    localized_copy: pageCopy(ir.copy_plan, page.id),
+    evidence_map: evidence,
+    acceptance_checks: unique([
+      ...ir.acceptance_checks,
+      `page_sales_task:${page.sales_task}`,
+      `allowed_views:${source.allowed_views.join(",")}`,
+      "no_model_generated_marketing_copy",
+      "composition_matches_recipe",
+    ]),
+  };
+  contract.prompt_hash = stableDigest(contract);
+  return validatePromptContract(contract);
+}
+
+export function validatePromptContract(contract) {
+  for (const key of ["prompt_source", "provider_prompt", "negative_prompt", "localized_copy", "evidence_map", "acceptance_checks"]) {
+    if (contract?.[key] === undefined) throw new Error(`PROMPT_CONTRACT_FIELD_REQUIRED:${key}`);
+  }
+  if (contract.prompt_version !== PROMPT_VERSION) throw new Error("PROMPT_CONTRACT_VERSION_UNSUPPORTED");
+  if (contract.provider_prompt.length > MAX_PROVIDER_PROMPT_LENGTH) throw new Error("PROVIDER_PROMPT_LENGTH_EXCEEDED");
+  if (JSON.stringify(contract.localized_copy).length > MAX_LOCALIZED_COPY_LENGTH) throw new Error("LOCALIZED_COPY_LENGTH_EXCEEDED");
+  if (secretOrPath.test(JSON.stringify(contract))) throw new Error("PROMPT_CONTRACT_SECRET_OR_PATH_REJECTED");
+  const refs = contract.prompt_source.evidence_refs || [];
+  for (const ref of refs) if (!(ref in contract.evidence_map)) throw new Error(`PROMPT_CONTRACT_EVIDENCE_NOT_FOUND:${ref}`);
+  const allowed = contract.prompt_source.allowed_views || [];
+  const forbidden = contract.prompt_source.forbidden_views || [];
+  if (allowed.some((view) => forbidden.includes(view))) throw new Error("PROMPT_CONTRACT_VIEW_CONFLICT");
+  if (hiddenViewTerms.test(contract.provider_prompt) && /展示(?:背面|内部|底部)|打开内部/.test(contract.provider_prompt)) throw new Error("PROMPT_CONTRACT_HIDDEN_VIEW_REQUESTED");
+  if (!/无新增营销文字|本地确定性排版/.test(contract.provider_prompt)) throw new Error("PROMPT_CONTRACT_TEXT_STRATEGY_REQUIRED");
+  return contract;
+}
+
+export function compilePromptContracts(ir) {
+  const contracts = ir.page_plans.map((page) => compileOne(ir, page));
+  return {
+    schema_version: "1.0",
+    prompt_version: PROMPT_VERSION,
+    status: "complete",
+    contracts,
+    bundle_hash: stableDigest(contracts.map((contract) => contract.prompt_hash)),
+  };
+}
+
+export const promptContractLimits = { maxProviderPromptLength: MAX_PROVIDER_PROMPT_LENGTH, maxLocalizedCopyLength: MAX_LOCALIZED_COPY_LENGTH };

+ 109 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/quality-repair-compiler.mjs

@@ -0,0 +1,109 @@
+import { existsSync } from "node:fs";
+
+const PAGE_IDS = ["hero", "selling_point", "scene"];
+const REPAIR_ACTIONS = new Set([
+  "copy_recompile",
+  "layout_recompose",
+  "scene_regenerate",
+  "product_reject",
+  "requires_reference",
+  "manual_review",
+]);
+
+const pass = (code, details = {}) => ({ code, status: "pass", ...details });
+const fail = (code, severity, details = {}) => ({ code, status: "fail", severity, ...details });
+
+function pageReport(pageId, ir, promptBundle, viewCoverage, files) {
+  const pagePlan = ir.page_plans.find((page) => page.id === pageId);
+  const contract = promptBundle.contracts.find((item) => item.page_id === pageId);
+  const view = viewCoverage.pages.find((item) => item.page_id === pageId);
+  const file = files.find((item) => item.slot === pageId);
+  const unresolvedEvidence = (pagePlan?.evidence_refs || []).filter((ref) => !(ref in (contract?.evidence_map || {})));
+  const checks = [
+    contract?.prompt_hash ? pass("prompt_contract_valid", { prompt_hash: contract.prompt_hash }) : fail("prompt_contract_missing", "blocking"),
+    unresolvedEvidence.length === 0 && (pagePlan?.evidence_refs || []).length > 0
+      ? pass("evidence_coverage", { evidence_refs: pagePlan.evidence_refs })
+      : fail("evidence_coverage", "blocking", { unresolved_evidence: unresolvedEvidence }),
+    view && !view.blocking
+      ? pass("view_validity", { decision: view.decision, requested_view: view.requested_view })
+      : fail("view_validity", "blocking", { decision: view?.decision || "missing", requested_view: view?.requested_view }),
+    file && existsSync(file.file) && file.width === ir.layout_plan.canvas.width && file.height === ir.layout_plan.canvas.height
+      ? pass("file_and_dimensions", { width: file.width, height: file.height, sha256: file.sha256 })
+      : fail("file_and_dimensions", "blocking", { expected: ir.layout_plan.canvas, actual: file ? { width: file.width, height: file.height } : null }),
+    ir.acceptance_checks.includes("all_text_inside_safe_area")
+      ? pass("text_and_layout_acceptance", { verification: "deterministic_compositor_completed" })
+      : fail("text_and_layout_acceptance", "major"),
+    fail("product_identity_human_review", "review", { reason: "local pixel preservation is not a substitute for human release approval" }),
+  ];
+  const blocking = checks.some((check) => check.status === "fail" && ["blocking", "major"].includes(check.severity));
+  return {
+    page_id: pageId,
+    status: blocking ? "blocked" : "review_required",
+    checks,
+    blocking_issues: checks.filter((check) => check.status === "fail" && ["blocking", "major"].includes(check.severity)).map((check) => check.code),
+    review_issues: checks.filter((check) => check.status === "fail" && !["blocking", "major"].includes(check.severity)).map((check) => check.code),
+  };
+}
+
+export function compileQualityReport({ ir, promptBundle, viewCoverage, files }) {
+  const pages = PAGE_IDS.map((pageId) => pageReport(pageId, ir, promptBundle, viewCoverage, files));
+  const salesTasks = ir.page_plans.map((page) => page.sales_task);
+  const recipes = ir.page_plans.map((page) => page.layout_recipe);
+  const copyLanguage = ir.layout_plan.text_rules.language;
+  const setChecks = [
+    new Set(salesTasks).size === PAGE_IDS.length ? pass("unique_sales_tasks") : fail("unique_sales_tasks", "blocking"),
+    new Set(recipes).size === PAGE_IDS.length ? pass("recipe_diversity", { recipes }) : fail("recipe_diversity", "major", { recipes }),
+    copyLanguage === ir.business_context.target_language
+      ? pass("language_consistency", { language: copyLanguage })
+      : fail("language_consistency", "blocking", { copy_language: copyLanguage, target_language: ir.business_context.target_language }),
+    viewCoverage.missing_views.length === 0
+      ? pass("view_coverage_complete")
+      : fail("view_coverage_complete", "blocking", { missing_views: viewCoverage.missing_views }),
+  ];
+  const blockingIssueCount = pages.reduce((sum, page) => sum + page.blocking_issues.length, 0)
+    + setChecks.filter((check) => check.status === "fail" && ["blocking", "major"].includes(check.severity)).length;
+  return {
+    schema_version: "1.0",
+    status: blockingIssueCount > 0 ? "blocked" : "review_required",
+    release_ceiling: "review_required",
+    pages,
+    set_checks: setChecks,
+    summary: {
+      page_count: pages.length,
+      blocking_issue_count: blockingIssueCount,
+      review_issue_count: pages.reduce((sum, page) => sum + page.review_issues.length, 0),
+    },
+  };
+}
+
+function actionForIssue(code, pageId) {
+  if (code.includes("evidence") || code.includes("prompt")) return "copy_recompile";
+  if (code.includes("view")) return "requires_reference";
+  if (code.includes("file") || code.includes("layout") || code.includes("language")) return "layout_recompose";
+  if (code.includes("identity")) return "manual_review";
+  return pageId === "scene" ? "scene_regenerate" : "manual_review";
+}
+
+export function compileRepairPlan(qualityReport) {
+  const actions = [];
+  for (const page of qualityReport.pages) {
+    for (const issue of [...page.blocking_issues, ...page.review_issues]) {
+      const action = actionForIssue(issue, page.page_id);
+      if (!REPAIR_ACTIONS.has(action)) throw new Error(`REPAIR_ACTION_UNSUPPORTED:${action}`);
+      actions.push({ page_id: page.page_id, issue, action, paid_action_authorized: false });
+    }
+  }
+  for (const check of qualityReport.set_checks.filter((item) => item.status === "fail")) {
+    const action = actionForIssue(check.code, null);
+    actions.push({ page_id: null, issue: check.code, action, paid_action_authorized: false });
+  }
+  return {
+    schema_version: "1.0",
+    status: qualityReport.status === "blocked" ? "repair_required" : "manual_review_required",
+    paid_action_authorized: false,
+    automatic_external_calls: false,
+    actions,
+  };
+}
+
+export const allowedRepairActions = [...REPAIR_ACTIONS];

+ 29 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/recipe-svg-renderer.mjs

@@ -0,0 +1,29 @@
+import { getDesignRecipe } from "./design-recipe-registry.mjs";
+
+export function renderHeroBackdrop(styleId, tokens) {
+  getDesignRecipe("style", styleId);
+  const base = `<defs><linearGradient id="heroBg" x1="0" y1="0" x2="1" y2="1"><stop stop-color="${tokens.surface}"/><stop offset="1" stop-color="${tokens.primarySoft}"/></linearGradient></defs><rect width="1200" height="1200" fill="url(#heroBg)"/>`;
+  if (styleId === "technical_precision") return `${base}<path d="M520 120H1140M520 220H1140M520 320H1140M620 70V1020M760 70V1020M900 70V1020M1040 70V1020" stroke="${tokens.primary}" stroke-width="2" opacity=".09"/><circle cx="920" cy="640" r="390" fill="${tokens.accent}" opacity=".12"/><rect x="535" y="455" width="610" height="420" rx="30" fill="${tokens.lightText}" opacity=".08"/>`;
+  if (styleId === "natural_editorial") return `${base}<path d="M550 1050V520C550 280 720 120 940 120s390 160 390 400v530Z" fill="${tokens.accent}" opacity=".11"/><circle cx="925" cy="620" r="330" fill="${tokens.lightText}" opacity=".18"/><path d="M535 935C720 850 1000 850 1190 930" fill="none" stroke="${tokens.primary}" stroke-width="3" opacity=".12"/>`;
+  if (styleId === "tactile_editorial") return `${base}<path d="M480 260C690 90 1040 130 1195 380V1100H505C650 880 555 620 480 260Z" fill="${tokens.accent}" opacity=".13"/><path d="M620 170C800 310 920 240 1120 130M570 980C780 820 980 900 1190 760" fill="none" stroke="${tokens.primary}" stroke-width="26" stroke-linecap="round" opacity=".08"/>`;
+  return `${base}<rect x="545" y="130" width="560" height="780" rx="44" fill="${tokens.accent}" opacity=".1"/><circle cx="1070" cy="250" r="170" fill="${tokens.primary}" opacity=".08"/>`;
+}
+
+export function renderDetailBackdrop(styleId, tokens) {
+  getDesignRecipe("style", styleId);
+  const base = `<rect width="1200" height="1200" fill="${tokens.surface}"/><rect width="1200" height="185" fill="${tokens.primaryDark}"/>`;
+  if (styleId === "technical_precision") return `${base}<rect x="55" y="240" width="570" height="900" rx="18" fill="${tokens.primarySoft}"/><path d="M80 280H600M80 380H600M80 480H600M180 250V1110M340 250V1110M500 250V1110" stroke="${tokens.primary}" stroke-width="2" opacity=".08"/><circle cx="340" cy="670" r="255" fill="${tokens.accent}" opacity=".1"/>`;
+  if (styleId === "natural_editorial") return `${base}<path d="M55 1140V430C55 310 150 240 270 240h130c120 0 225 70 225 190v710Z" fill="${tokens.primarySoft}"/><circle cx="340" cy="660" r="250" fill="${tokens.accent}" opacity=".1"/>`;
+  if (styleId === "tactile_editorial") return `${base}<rect x="55" y="240" width="570" height="900" rx="70" fill="${tokens.primarySoft}"/><path d="M70 900C220 740 420 820 610 620" fill="none" stroke="${tokens.accent}" stroke-width="70" stroke-linecap="round" opacity=".1"/>`;
+  return `${base}<rect x="55" y="240" width="570" height="900" rx="34" fill="${tokens.primarySoft}"/><rect x="95" y="285" width="490" height="810" rx="24" fill="${tokens.lightText}" opacity=".1"/>`;
+}
+
+export function renderSceneBackdrop(sceneId, tokens) {
+  getDesignRecipe("scene", sceneId);
+  const defs = `<defs><linearGradient id="sceneWall" x1="0" y1="0" x2="1" y2="1"><stop stop-color="${tokens.primaryDark}"/><stop offset="1" stop-color="${tokens.primary}"/></linearGradient><linearGradient id="sceneFloor" x1="0" y1="0" x2="1" y2="0"><stop stop-color="${tokens.primarySoft}"/><stop offset="1" stop-color="${tokens.accent}"/></linearGradient></defs>`;
+  const room = `${defs}<rect width="1200" height="790" fill="url(#sceneWall)"/><rect y="790" width="1200" height="410" fill="url(#sceneFloor)"/>`;
+  if (sceneId === "technical_workspace") return `${room}<path d="M500 130H1120M500 230H1120M500 330H1120M600 70V650M760 70V650M920 70V650M1080 70V650" stroke="${tokens.lightText}" stroke-width="2" opacity=".08"/><rect x="520" y="105" width="300" height="230" rx="12" fill="${tokens.lightText}" opacity=".13"/><rect x="655" y="335" width="30" height="110" fill="${tokens.lightText}" opacity=".13"/><rect x="575" y="445" width="190" height="16" rx="8" fill="${tokens.lightText}" opacity=".13"/><path d="M120 790V610Q120 535 190 535h95V790M205 535V470" fill="none" stroke="${tokens.primarySoft}" stroke-width="24" stroke-linecap="round" opacity=".18"/><circle cx="205" cy="442" r="42" fill="${tokens.primarySoft}" opacity=".18"/>`;
+  if (sceneId === "warm_countertop") return `${room}<path d="M520 790V310C520 165 635 70 790 70s270 95 270 240v480Z" fill="${tokens.lightText}" opacity=".1"/><path d="M570 520H1010" stroke="${tokens.lightText}" stroke-width="15" opacity=".13"/><circle cx="670" cy="440" r="55" fill="${tokens.accent}" opacity=".18"/><path d="M85 790C90 660 135 610 175 790M125 710C70 650 70 585 90 550M145 680C210 630 220 570 205 525" fill="none" stroke="${tokens.primarySoft}" stroke-width="24" stroke-linecap="round" opacity=".2"/><ellipse cx="390" cy="885" rx="80" ry="18" fill="${tokens.primaryDark}" opacity=".22"/><path d="M335 840H455L440 930Q390 950 350 930Z" fill="${tokens.lightText}" opacity=".78"/><path d="M455 858Q505 852 495 900Q480 925 445 908" fill="none" stroke="${tokens.lightText}" stroke-width="15" opacity=".78"/>`;
+  if (sceneId === "textile_studio") return `${room}<path d="M520 70C610 170 575 340 650 455S820 650 770 790" fill="none" stroke="${tokens.lightText}" stroke-width="85" opacity=".08"/><path d="M580 70V790M660 70V790M740 70V790" stroke="${tokens.lightText}" stroke-width="5" opacity=".1"/><path d="M110 790V410Q110 315 205 315h170Q470 315 470 410v380" fill="none" stroke="${tokens.primarySoft}" stroke-width="22" opacity=".18"/><path d="M290 315V230M230 280Q290 220 350 280" fill="none" stroke="${tokens.primarySoft}" stroke-width="18" stroke-linecap="round" opacity=".18"/><ellipse cx="420" cy="870" rx="145" ry="38" fill="${tokens.primaryDark}" opacity=".18"/>`;
+  return `${room}<rect x="540" y="100" width="430" height="560" rx="220" fill="${tokens.lightText}" opacity=".1"/><ellipse cx="390" cy="880" rx="160" ry="38" fill="${tokens.primaryDark}" opacity=".2"/><path d="M250 880H530L500 790H280Z" fill="${tokens.lightText}" opacity=".14"/>`;
+}

+ 129 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/seven-page-design-ir-compiler.mjs

@@ -0,0 +1,129 @@
+import { compileCommercialCopy, classifyProductForm, detectVerifiedBrand } from "./commercial-copy-compiler.mjs";
+import { compileDesignTokens } from "./design-token-compiler.mjs";
+
+const asArray = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+
+const pageRoles = [
+  { id: "hero", title: "一眼看清整体", subtitle: "完整呈现商品轮廓与核心配色", recipe: "hero_corner_title", salesTask: "快速识别商品" },
+  { id: "material_detail", title: "质感清晰可见", subtitle: "光影呈现参考可见的表面层次", recipe: "detail_bottom_caption", salesTask: "理解可见材质与配色" },
+  { id: "lifestyle_scene", title: "自然融入日常", subtitle: "在可信空间中看见搭配关系", recipe: "scene_vertical_side", salesTask: "建立可信环境关系" },
+  { id: "feature_detail", title: "细节经得起近看", subtitle: "聚焦参考图已经证明的设计", recipe: "feature_top_right", salesTask: "理解可见结构细节" },
+  { id: "scale_context", title: "比例更直观", subtitle: "借助环境参照理解视觉尺度", recipe: "scale_bottom_left", salesTask: "完成比例关系判断" },
+  { id: "action_scene", title: "动态之间更有型", subtitle: "真实动作呈现自然使用状态", recipe: "action_motion_caption", salesTask: "理解动态使用情境" },
+  { id: "decision_summary", title: "从整体看到细节", subtitle: "重要信息集中呈现,选择更从容", recipe: "decision_footer_grid", salesTask: "汇总购买判断" },
+];
+
+const forbiddenPlanningTerms = /关系型画面|支撑展示|购买决策任务|证据驱动|身份锚点|参考可见表面|规划页|scene relation|support display/gi;
+
+export function sanitizeConsumerCopy(value, fallback = "") {
+  const cleaned = text(value, fallback)
+    .replace(forbiddenPlanningTerms, "")
+    .replace(/[,,]{2,}/g, ",")
+    .replace(/[。\.]{2,}/g, "。")
+    .replace(/,\s*。/g, "。")
+    .replace(/\s+/g, " ")
+    .trim()
+    .replace(/^[,。;\s]+|[,;\s]+$/g, "");
+  return cleaned || fallback;
+}
+
+function evidenceMap(product = {}) {
+  const facts = asArray(product.verified_visible_facts || product.visible_facts).map(String);
+  return Object.fromEntries(facts.map((value, index) => [`visible_fact:${index + 1}`, value]));
+}
+
+export function validateSevenPageDesignIr(ir) {
+  if (ir?.schema_version !== "2.0") throw new Error("SEVEN_PAGE_DESIGN_IR_VERSION_UNSUPPORTED");
+  if (!Array.isArray(ir.page_plans) || ir.page_plans.length !== 7) throw new Error("SEVEN_PAGE_DESIGN_IR_SEVEN_PAGES_REQUIRED");
+  if (new Set(ir.page_plans.map((page) => page.id)).size !== 7) throw new Error("SEVEN_PAGE_DESIGN_IR_PAGE_ROLE_DUPLICATED");
+  if (new Set(ir.page_plans.map((page) => page.layout_recipe)).size !== 7) throw new Error("SEVEN_PAGE_DESIGN_IR_LAYOUT_RECIPE_DUPLICATED");
+  const copy = JSON.stringify(ir.page_plans.map((page) => page.copy));
+  if (forbiddenPlanningTerms.test(copy)) throw new Error("SEVEN_PAGE_DESIGN_IR_INTERNAL_COPY_FORBIDDEN");
+  forbiddenPlanningTerms.lastIndex = 0;
+  if (/[,,]{2,}/.test(copy)) throw new Error("SEVEN_PAGE_DESIGN_IR_PUNCTUATION_INVALID");
+  if (!text(ir.business_context?.platform) || !text(ir.business_context?.market) || !text(ir.business_context?.target_language)) {
+    throw new Error("SEVEN_PAGE_DESIGN_IR_BUSINESS_CONTEXT_REQUIRED");
+  }
+  return ir;
+}
+
+export async function compileSevenPageDesignIr({
+  product = {},
+  productLayer,
+  promptPlan = {},
+  setQualityReview = {},
+} = {}) {
+  if (!productLayer?.buffer) throw new Error("SEVEN_PAGE_DESIGN_IR_PRODUCT_LAYER_REQUIRED");
+  if (setQualityReview?.status !== "quality_passed") throw new Error("SEVEN_PAGE_DESIGN_IR_QUALITY_GATE_REQUIRED");
+  const productForm = classifyProductForm(product);
+  const brand = detectVerifiedBrand(product);
+  const tokens = await compileDesignTokens(productLayer.buffer);
+  const commercial = compileCommercialCopy(product, { productForm, brand });
+  const evidence = evidenceMap(product);
+  const facts = Object.values(evidence);
+  const productLabel = sanitizeConsumerCopy(commercial.productLabel, text(product.product_name?.value, "商品"));
+  const context = promptPlan.context || {};
+  const pages = pageRoles.map((role, index) => {
+    const planned = asArray(promptPlan.images)[index] || {};
+    const fact = facts[index % Math.max(1, facts.length)] || role.subtitle;
+    const observed = asArray(setQualityReview.pages)[index]?.page_compliance || null;
+    return {
+      id: role.id,
+      slot_id: planned.id || String(index + 1).padStart(2, "0"),
+      sales_task: role.salesTask,
+      evidence_refs: asArray(planned.evidence_refs),
+      layout_recipe: role.recipe,
+      copy: {
+        eyebrow: index === 0 ? text(context.platform, "电商精选").toUpperCase() : `${String(index + 1).padStart(2, "0")} / 07`,
+        title: index === 0 ? productLabel : role.title,
+        subtitle: sanitizeConsumerCopy(index === 0 ? role.subtitle : fact, role.subtitle).slice(0, 34),
+      },
+      content_regions: observed ? {
+        product_region: observed.product_region,
+        face_regions: observed.face_regions,
+        critical_product_regions: observed.critical_product_regions,
+        recommended_text_safe_regions: observed.recommended_text_safe_regions,
+      } : null,
+      render_strategy: "deterministic_overlay_on_verified_visual_base",
+    };
+  });
+  const ir = {
+    schema_version: "2.0",
+    product_profile: {
+      category: text(product.category, "通用商品"),
+      product_label: productLabel,
+      product_form: productForm,
+      brand,
+      visible_fact_refs: Object.keys(evidence),
+    },
+    business_context: {
+      platform: text(context.platform, "general_ecommerce"),
+      market: text(context.market, "unspecified"),
+      target_language: text(context.target_language, "zh-CN"),
+      output_level: "seven_page_designed_production",
+    },
+    creative_direction: { visual_system: "seven_role_adaptive_editorial", tokens },
+    page_plans: pages,
+    copy_plan: { product_label: productLabel, pages: pages.map((page) => page.copy) },
+    scene_plan: { source: "verified_visual_base", forbidden_reveals: ["back", "interior", "bottom", "unseen_side"] },
+    layout_plan: {
+      canvas: { width: 1200, height: 1200, ratio: text(context.aspect_ratio, "1:1") },
+      safe_area: { left: 44, top: 44, right: 44, bottom: 44 },
+      page_recipes: pages.map((page) => page.layout_recipe),
+      collision_policy: "product_face_and_critical_regions_are_exclusion_zones",
+      unverifiable_policy: "no_overlay",
+    },
+    render_strategy: { product_pixels: "visual_base_preserved", paid_image_generation: false, overlay: "adaptive_deterministic" },
+    acceptance_checks: [
+      "seven_unique_page_roles_and_recipes",
+      "consumer_facing_copy_only",
+      "platform_market_and_language_propagated",
+      "overlay_does_not_intersect_exclusion_regions",
+      "unverifiable_regions_use_no_overlay",
+    ],
+    evidence_map: evidence,
+  };
+  return validateSevenPageDesignIr(ir);
+}
+

+ 37 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/seven-page-design-ir-compiler.test.mjs

@@ -0,0 +1,37 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import sharp from "sharp";
+import { selectAdaptiveLayout } from "./adaptive-overlay-compositor.mjs";
+import { compileSevenPageDesignIr, sanitizeConsumerCopy } from "./seven-page-design-ir-compiler.mjs";
+
+const regions = {
+  product_region: { status: "verified", boxes: [{ x: 0.42, y: 0.18, width: 0.5, height: 0.68 }] },
+  face_regions: { status: "unverifiable", boxes: [] },
+  critical_product_regions: { status: "verified", boxes: [{ x: 0.5, y: 0.25, width: 0.3, height: 0.35 }] },
+  recommended_text_safe_regions: { status: "verified", boxes: [{ x: 0.03, y: 0.05, width: 0.34, height: 0.22 }] },
+};
+
+test("seven-page Design IR propagates Taobao context and has seven distinct recipes", async () => {
+  const buffer = await sharp({ create: { width: 200, height: 200, channels: 3, background: "#9A704D" } }).png().toBuffer();
+  const result = await compileSevenPageDesignIr({
+    product: { category: "服装", product_name: { value: "长款外套" }, verified_visible_facts: ["驼色长款廓形", "同面料腰带于正面系结,,细节清晰"] },
+    productLayer: { buffer, width: 200, height: 200 },
+    promptPlan: { context: { platform: "taobao", market: "cn", target_language: "zh-CN", aspect_ratio: "1:1" }, images: Array.from({ length: 7 }, (_, index) => ({ id: String(index + 1).padStart(2, "0"), evidence_refs: ["visible_fact:1"] })) },
+    setQualityReview: { status: "quality_passed", pages: Array.from({ length: 7 }, () => ({ page_compliance: regions })) },
+  });
+  assert.equal(result.schema_version, "2.0");
+  assert.equal(result.business_context.platform, "taobao");
+  assert.equal(result.business_context.market, "cn");
+  assert.equal(result.page_plans.length, 7);
+  assert.equal(new Set(result.page_plans.map((page) => page.layout_recipe)).size, 7);
+  assert.doesNotMatch(JSON.stringify(result.copy_plan), /,,|关系型画面|支撑展示/);
+});
+
+test("adaptive overlay avoids exclusions and fails closed when regions are unverifiable", () => {
+  const safe = selectAdaptiveLayout({ page: { layout_recipe: "hero_corner_title" }, regions });
+  assert.equal(safe.mode, "overlay");
+  const blocked = selectAdaptiveLayout({ page: {}, regions: { ...regions, recommended_text_safe_regions: { status: "unverifiable", boxes: [] } } });
+  assert.equal(blocked.mode, "no_overlay");
+  assert.equal(sanitizeConsumerCopy("关系型画面,,细节清晰"), "细节清晰");
+});
+

+ 20 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/text-layout-v2.test.mjs

@@ -0,0 +1,20 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { fitChineseText, wrapChineseText } from "./text-layout.mjs";
+
+test("Chinese wrapping keeps closing punctuation off a new line", () => {
+  const lines = wrapChineseText("真实材质,清晰可见。", { maxWidth: 60, fontSize: 20 });
+  assert.ok(lines.every((line) => !/^[,。!?;:、)》】」』]/.test(line)));
+  assert.equal(lines.join(""), "真实材质,清晰可见。");
+});
+
+test("Chinese fitting preserves full semantics and shrinks instead of truncating", () => {
+  const value = "完整商品信息必须全部保留";
+  const result = fitChineseText(value, { maxWidth: 150, maxHeight: 100, fontSize: 30, minFontSize: 14, maxLines: 3 });
+  assert.equal(result.lines.join(""), value);
+  assert.ok(result.fontSize <= 30);
+});
+
+test("Chinese fitting fails when complete copy cannot fit", () => {
+  assert.throws(() => fitChineseText("非常长的文案".repeat(20), { maxWidth: 80, maxHeight: 30, fontSize: 24, minFontSize: 20, maxLines: 1 }), /TEXT_OVERFLOW/);
+});

+ 55 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/text-layout.mjs

@@ -0,0 +1,55 @@
+const widthFactor = (character) => /[\u0000-\u00ff]/.test(character) ? 0.58 : 1;
+const lineWidth = (value, fontSize) => Array.from(String(value)).reduce((sum, character) => sum + widthFactor(character) * fontSize, 0);
+
+function wrapAtSize(value, maxWidth, fontSize) {
+  const lines = [];
+  let current = "";
+  for (const character of Array.from(String(value || ""))) {
+    const candidate = current + character;
+    if (current && lineWidth(candidate, fontSize) > maxWidth) {
+      lines.push(current.trimEnd());
+      current = character.trimStart();
+    } else current = candidate;
+  }
+  if (current) lines.push(current.trimEnd());
+  return lines;
+}
+
+export function fitText(value, { maxWidth, fontSize, minFontSize = 15, maxLines = 2 } = {}) {
+  if (!maxWidth || !fontSize || !maxLines) throw new Error("TEXT_LAYOUT_INVALID_CONSTRAINTS");
+  for (let size = fontSize; size >= minFontSize; size -= 1) {
+    const lines = wrapAtSize(value, maxWidth, size);
+    if (lines.length <= maxLines) return { fontSize: size, lines, lineHeight: Math.round(size * 1.32), fits: true };
+  }
+  throw new Error(`DESIGNED_ASSET_TEXT_OVERFLOW:${String(value)}`);
+}
+
+const forbiddenLineStart = new Set(Array.from(",。!?;:、)》】」』”’%‰"));
+const forbiddenLineEnd = new Set(Array.from("(《【「『“‘"));
+
+export function wrapChineseText(value, { maxWidth, fontSize, measure = (textValue, size) => lineWidth(textValue, size) } = {}) {
+  const lines = [];
+  let current = "";
+  for (const character of Array.from(String(value || ""))) {
+    if (character === "\n") { if (current) lines.push(current); current = ""; continue; }
+    const candidate = current + character;
+    if (current && measure(candidate, fontSize) > maxWidth && !forbiddenLineStart.has(character)) {
+      let carry = "";
+      if (forbiddenLineEnd.has(current.at(-1))) { carry = current.at(-1); current = current.slice(0, -1); }
+      if (current) lines.push(current.trimEnd());
+      current = `${carry}${character}`.trimStart();
+    } else current = candidate;
+  }
+  if (current) lines.push(current.trimEnd());
+  return lines;
+}
+
+export function fitChineseText(value, { maxWidth, maxHeight, fontSize, minFontSize = 14, maxLines = 3, measure } = {}) {
+  if (!maxWidth || !maxHeight || !fontSize) throw new Error("TEXT_LAYOUT_INVALID_CONSTRAINTS");
+  for (let size = fontSize; size >= minFontSize; size -= 1) {
+    const lines = wrapChineseText(value, { maxWidth, fontSize: size, measure });
+    const lineHeight = Math.round(size * 1.34);
+    if (lines.length <= maxLines && lines.length * lineHeight <= maxHeight) return { fontSize: size, lines, lineHeight, fits: true };
+  }
+  throw new Error(`DESIGNED_ASSET_TEXT_OVERFLOW:${String(value)}`);
+}

+ 35 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/view-coverage-compiler.mjs

@@ -0,0 +1,35 @@
+const supportedViews = new Set(["reference_visible_surface", "front", "left", "right", "back", "top", "bottom", "interior", "detail"]);
+
+export function compileViewCoverage(ir, {
+  referenceViews = ["reference_visible_surface"],
+  requestedViews = {},
+  providerCapabilities = { reference_image_generation: true, controlled_view_generation: false, multi_reference: false, image_edit: false, mask_edit: false },
+} = {}) {
+  const references = [...new Set(referenceViews)];
+  for (const view of references) if (!supportedViews.has(view)) throw new Error(`VIEW_COVERAGE_UNKNOWN_REFERENCE:${view}`);
+  const pages = ir.page_plans.map((page) => {
+    const requested = requestedViews[page.id] || "reference_visible_surface";
+    if (!supportedViews.has(requested)) throw new Error(`VIEW_COVERAGE_UNKNOWN_REQUEST:${requested}`);
+    let decision;
+    if (references.includes(requested)) decision = page.render_strategy === "reference_exact" ? "reference_exact" : "derived_affine";
+    else if (requested === "detail" && references.includes("reference_visible_surface")) decision = "reference_crop";
+    else if (providerCapabilities.controlled_view_generation && providerCapabilities.reference_image_generation) decision = "provider_reference_generation";
+    else decision = "requires_additional_reference";
+    return {
+      page_id: page.id,
+      requested_view: requested,
+      decision,
+      evidence_refs: references.includes(requested) ? [requested] : [],
+      requires_identity_review: decision === "provider_reference_generation",
+      blocking: decision === "requires_additional_reference",
+    };
+  });
+  return {
+    schema_version: "1.0",
+    status: pages.some((page) => page.blocking) ? "blocked" : "complete",
+    reference_views: references,
+    missing_views: [...new Set(pages.filter((page) => page.blocking).map((page) => page.requested_view))],
+    provider_capabilities: providerCapabilities,
+    pages,
+  };
+}

+ 51 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/view-strategy-router.mjs

@@ -0,0 +1,51 @@
+const FACTUAL_PURPOSES = new Set(["fact", "detail", "selling_point", "identity"]);
+const HIDDEN_VIEWS = new Set(["back", "interior", "bottom", "exact_dimensions"]);
+
+export function routeViewStrategy({
+  purpose = "creative",
+  requestedView = "auto",
+  referenceCount = 1,
+  allowInference = false,
+} = {}) {
+  const normalizedPurpose = String(purpose).toLowerCase();
+  const normalizedView = String(requestedView).toLowerCase();
+
+  if (HIDDEN_VIEWS.has(normalizedView) && referenceCount < 2) {
+    return {
+      strategy: "requires_additional_reference",
+      viewConfidence: "none",
+      allowedReveal: [],
+      forbiddenReveal: ["back", "interior", "bottom", "exact_dimensions"],
+      requiresIdentityReview: true,
+    };
+  }
+
+  if (FACTUAL_PURPOSES.has(normalizedPurpose)) {
+    return {
+      strategy: normalizedView === "detail" ? "reference_crop" : "reference_exact",
+      viewConfidence: "high",
+      allowedReveal: ["reference_visible_surface"],
+      forbiddenReveal: ["back", "interior", "unseen_side"],
+      requiresIdentityReview: false,
+    };
+  }
+
+  if (allowInference && normalizedView === "three_quarter") {
+    return {
+      strategy: "inferred_three_quarter",
+      viewConfidence: "medium",
+      allowedReveal: ["front", "partial_side"],
+      forbiddenReveal: ["back", "interior"],
+      requiresIdentityReview: true,
+    };
+  }
+
+  return {
+    strategy: "derived_affine",
+    viewConfidence: "high",
+    allowedReveal: ["reference_visible_surface"],
+    forbiddenReveal: ["back", "interior", "unseen_side"],
+    requiresIdentityReview: false,
+  };
+}
+

+ 20 - 0
claude-code/fmode-image-set/mcp/src/features/designed-asset/view-strategy-router.test.mjs

@@ -0,0 +1,20 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { routeViewStrategy } from "./view-strategy-router.mjs";
+
+test("factual pages preserve the reference view", () => {
+  assert.equal(routeViewStrategy({ purpose: "selling_point" }).strategy, "reference_exact");
+});
+
+test("single-reference hidden views require another reference", () => {
+  const result = routeViewStrategy({ requestedView: "back", referenceCount: 1 });
+  assert.equal(result.strategy, "requires_additional_reference");
+  assert.equal(result.requiresIdentityReview, true);
+});
+
+test("three-quarter inference is explicit and review-gated", () => {
+  const result = routeViewStrategy({ requestedView: "three_quarter", allowInference: true });
+  assert.equal(result.strategy, "inferred_three_quarter");
+  assert.equal(result.requiresIdentityReview, true);
+});
+

+ 49 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/claude-authored-v3-pipeline.integration.test.mjs

@@ -0,0 +1,49 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import sharp from "sharp";
+import { createRun } from "../../core/run-paths.mjs";
+import { contractSha256 } from "./reference-first-v3-contracts.mjs";
+import { boardSpec, setPlan } from "./reference-first-v3-fixtures.mjs";
+import { generateClaudeAuthoredReferences, generateClaudeAuthoredSet, startClaudeAuthoredV3 } from "./image-set-runner.mjs";
+
+const visualProduct = { category: "鞋", form: "自然直立", colors: ["黑色"], material_and_light_cues: ["哑光鞋面"], identity_cues: ["白色鞋底", "黑色鞋带"], natural_support_and_use: ["地面穿着"] };
+const png = () => sharp({ create: { width: 320, height: 180, channels: 3, background: "#ddd" } }).png().toBuffer();
+
+test("V3 stages keep Qwen descriptive, Claude prompts dynamic, and set references limited to 2 generated assets", async () => {
+  const paths = await createRun();
+  try {
+    const original = join(paths.attachments_directory, "original.png"); await writeFile(original, await png());
+    const originalObservation = { schema_version: "1.0", purpose: "claude_visual_input_not_creative_plan", run_id: paths.run_id, input_asset_sha256: "originalhash", product: visualProduct };
+    const start = await startClaudeAuthoredV3({ paths, originalReference: original, productionBrief: { instruction: "淘宝七图", platform: "taobao", aspectRatio: "1:1" }, observeOriginal: async ({ outputFile }) => { await writeFile(outputFile, JSON.stringify(originalObservation)); return { observation: originalObservation, model_calls: 1 }; } });
+    assert.equal(start.status, "ready_for_reference_authoring"); assert.equal(start.cost.confirmed_image_cost_cny, 0);
+
+    const spec = boardSpec(paths.run_id, start.original_visual_observation_sha256);
+    const dynamicPrompt = `生成一张16:9横版当前鞋类商品设定图。${"根据当前黑色鞋面、白色鞋底、地面自然支撑关系设计主视角、多角度、材质细节、穿着和玄关收纳场景;保持真实商业摄影与无新增文字。".repeat(12)}`;
+    const authoring = { schema_version: "1.0", authored_by: "claude", run_id: paths.run_id, source_observation_sha256: start.original_visual_observation_sha256, setting_board_spec: spec, setting_board_visual_prompt: dynamicPrompt };
+    const refExecute = async (_script, args) => {
+      const files = [join(paths.reference_assets_directory, "three.png"), join(paths.reference_assets_directory, "board.png")];
+      await Promise.all(files.map(async (file) => writeFile(file, await png())));
+      const summary = { status: "complete", local_images: files, remote_images: ["https://img.test/three.png", "https://img.test/board.png"], cost_range_cny: { min: 0.44, max: 0.44 } };
+      const output = args[args.indexOf("--output") + 1]; await writeFile(output, JSON.stringify({ status: "complete", results: files.map((file, index) => ({ status: "complete", local_images: [file], images: [summary.remote_images[index]], cost_cny: 0.22 })) })); return JSON.stringify(summary);
+    };
+    const referenceObservation = { schema_version: "1.0", purpose: "claude_reference_visual_input_not_creative_plan", run_id: paths.run_id, input_assets_sha256: ["a", "b"], assets: [{ asset_role: "three_view", product: visualProduct }, { asset_role: "setting_board_visual", product: visualProduct }] };
+    const refs = await generateClaudeAuthoredReferences({ paths, referenceAuthoring: authoring, executeNode: refExecute, observeReferences: async ({ outputFile }) => { await writeFile(outputFile, JSON.stringify(referenceObservation)); return { observation: referenceObservation, model_calls: 1 }; } });
+    assert.equal(refs.status, "ready_for_set_authoring");
+    const refPlan = JSON.parse(await readFile(paths.reference_asset_plan, "utf8")); assert.equal(refPlan.images[1].generation_prompt, dynamicPrompt); assert.equal(refPlan.reference_authoring.authored_by, "claude");
+
+    const plan = setPlan(paths.run_id, refs.reference_visual_observation_sha256);
+    const setExecute = async (_script, args) => {
+      const files = await Promise.all(Array.from({ length: 7 }, async (_, index) => { const file = join(paths.images_directory, `${index + 1}.png`); await writeFile(file, await png()); return file; }));
+      const summary = { status: "complete", local_images: files, remote_images: files.map((_, index) => `https://img.test/${index + 1}.png`), cost_range_cny: { min: 1.54, max: 1.54 } };
+      const output = args[args.indexOf("--output") + 1]; await writeFile(output, JSON.stringify(summary)); return JSON.stringify(summary);
+    };
+    const layoutPages = Array.from({ length: 7 }, (_, index) => ({ slot_id: String(index + 1).padStart(2, "0"), product_region: { status: "unverifiable", boxes: [] }, recommended_text_safe_regions: { status: "unverifiable", boxes: [] } }));
+    const designedFiles = Array.from({ length: 7 }, (_, index) => ({ file: join(paths.run_directory, "designed", `${index + 1}.png`) }));
+    const completed = await generateClaudeAuthoredSet({ paths, setPlan: plan, executeNode: setExecute, detectLayout: async () => ({ regions: { pages: layoutPages }, model_calls: 1 }), postprocessDesign: async () => { await mkdir(join(paths.run_directory, "designed"), { recursive: true }); await Promise.all(designedFiles.map(async (item) => writeFile(item.file, await png()))); return { files: designedFiles, manifest_file: join(paths.run_directory, "designed", "manifest.json") }; } });
+    assert.equal(completed.status, "complete"); assert.equal(completed.original_reference_used_for_set, false);
+    const promptPlan = JSON.parse(await readFile(paths.prompt_plan, "utf8")); assert.ok(promptPlan.images.every((image) => image.reference_images.length === 2)); assert.ok(promptPlan.images.every((image) => !image.reference_images.includes(original)));
+    assert.equal(contractSha256(referenceObservation), refs.reference_visual_observation_sha256);
+  } finally { await rm(paths.run_directory, { recursive: true, force: true }); }
+});

+ 870 - 24
claude-code/fmode-image-set/mcp/src/features/image-set/image-set-runner.mjs

@@ -1,13 +1,33 @@
 import { spawn } from "node:child_process";
-import { readFile, readdir } from "node:fs/promises";
-import { dirname, join, resolve } from "node:path";
+import { copyFile, readFile, readdir, writeFile } from "node:fs/promises";
+import { basename, dirname, join, resolve } from "node:path";
 import { fileURLToPath } from "node:url";
 import { inspectLatestClaudeInvocation } from "../../providers/claude-attachment-client.mjs";
 import { createRun, pathsForRun } from "../../core/run-paths.mjs";
+import { runDesignedAsset } from "../designed-asset/designed-asset-runner.mjs";
+import { compileProductionPlan } from "./production-plan-compiler.mjs";
+import {
+  compileReferenceAssetPlan,
+  compileReferenceFirstSetPlan,
+} from "./reference-asset-plan-compiler.mjs";
+import { planReferenceFirstCreative } from "./reference-first-creative-planner.mjs";
+import { writeReferenceFirstCopyPlan } from "./reference-first-copy-plan.mjs";
+import { detectReferenceFirstLayoutRegions } from "./reference-first-layout-region-detector.mjs";
+import { runReferenceFirstDesignPostprocessor } from "./reference-first-design-postprocessor.mjs";
+import { compileProductionOutcome } from "./production-outcome-compiler.mjs";
+import { runProductionDesignBridge } from "./production-design-bridge.mjs";
+import { verifyProductionSetQuality } from "./set-quality-verifier.mjs";
+import { sha256 } from "../../core/run-state-integrity.mjs";
+import { observeGeneratedReferences, observeOriginalVisual } from "./reference-first-visual-observer.mjs";
+import { assembleClaudeAuthoredPromptPlan, contractSha256, validateClaudeAuthoredSetPlan, validateClaudeReferenceAuthoring, V3_WORKFLOW } from "./reference-first-v3-contracts.mjs";
+import { writeSettingBoardCopyPlan } from "./setting-board-copy-plan.mjs";
+import { renderAnnotatedSettingBoard } from "./setting-board-annotated-renderer.mjs";
 
 const scriptsDirectory = dirname(fileURLToPath(import.meta.url));
+const packageRoot = resolve(scriptsDirectory, "..", "..", "..", "..");
 const productVisionFile = resolve(scriptsDirectory, "product-vision-client.mjs");
 const jimengClientFile = resolve(scriptsDirectory, "..", "..", "providers", "jimeng-v4-client.mjs");
+const validatePlanFile = resolve(scriptsDirectory, "validate-plan.mjs");
 const args = process.argv.slice(2);
 const command = args[0];
 const option = (name, fallback = "") => {
@@ -15,6 +35,214 @@ const option = (name, fallback = "") => {
   return index >= 0 ? args[index + 1] : fallback;
 };
 
+function inferredWorkspaceRoot() {
+  const packageParent = dirname(packageRoot);
+  const container = dirname(packageParent);
+  if (
+    basename(packageParent).toLowerCase() === "skills"
+    && new Set([".claude", ".codex"]).has(basename(container).toLowerCase())
+  ) {
+    return dirname(container);
+  }
+  return packageParent;
+}
+
+export function resolveAttachmentCwd({ explicitCwd = "", env = process.env } = {}) {
+  return resolve(
+    explicitCwd
+    || env.CLAUDE_PROJECT_DIR
+    || env.FMODE_IMAGE_SET_PROJECT_ROOT
+    || inferredWorkspaceRoot(),
+  );
+}
+
+export async function resolveStartInvocation({
+  explicitImage = "",
+  preferCurrentAttachment = false,
+  cwd,
+  claudeConfigDirectory = "",
+  sessionId = "",
+  inspectInvocation = inspectLatestClaudeInvocation,
+} = {}) {
+  if (explicitImage && !preferCurrentAttachment) return null;
+  let invocation;
+  try {
+    invocation = await inspectInvocation({ cwd, claudeConfigDirectory, sessionId });
+  } catch (error) {
+    if (explicitImage && new Set(["CLAUDE_ATTACHMENT_NOT_FOUND", "CLAUDE_SESSION_NOT_FOUND", "CLAUDE_ATTACHMENT_EXPIRED"]).has(error?.code)) return null;
+    throw error;
+  }
+  if (invocation.image_count === 0 && explicitImage) return null;
+  if (invocation.image_count !== 1) {
+    throw new Error(`CURRENT_REQUEST_IMAGE_COUNT_INVALID: expected one image, found ${invocation.image_count}`);
+  }
+  return { ...invocation, explicit_image_ignored: Boolean(explicitImage) };
+}
+
+export function buildInputProvenance({ invocation = null, explicitImage = "", vision = null } = {}) {
+  return invocation
+    ? {
+      mode: "current_vscode_claude_attachment",
+      prompt_id: invocation.prompt_id,
+      session_id: invocation.session_id,
+      image_count: invocation.image_count,
+      invocation_mode: invocation.invocation_mode,
+      explicit_image_ignored: Boolean(invocation.explicit_image_ignored),
+      sha256: vision?.reference?.attachment?.sha256 || null,
+    }
+    : {
+      mode: "explicit_user_path_or_url",
+      image_count: explicitImage ? 1 : 0,
+      explicit_image_ignored: false,
+      sha256: null,
+    };
+}
+
+export function buildVisionArguments({ invocation = null, explicitImage = "", outputFile, attachmentCwd, claudeConfigDirectory = "", sessionId = "" } = {}) {
+  if (invocation) {
+    return [
+      "--from-claude-attachment",
+      "--output", outputFile,
+      "--attachment-cwd", attachmentCwd,
+      ...(claudeConfigDirectory ? ["--claude-config-dir", claudeConfigDirectory] : []),
+      ...(sessionId ? ["--session-id", sessionId] : []),
+    ];
+  }
+  if (!explicitImage) throw new Error("START_IMAGE_SOURCE_REQUIRED");
+  return ["--image", explicitImage, "--output", outputFile];
+}
+
+export function buildStartResult({
+  vision,
+  outputLevel,
+  paths,
+  designedAsset = null,
+  inputProvenance = null,
+  productionPlan = null,
+  planValidation = null,
+  preflightRecord = null,
+  executionAuthorization = null,
+}) {
+  if (vision?.status !== "complete") {
+    return {
+      status: "blocked",
+      output_level: outputLevel,
+      paths,
+      vision,
+      ...(inputProvenance ? { input_provenance: inputProvenance } : {}),
+      ...(designedAsset ? { designed_asset: designedAsset } : {}),
+      next_action: "stop_and_report_error",
+    };
+  }
+  if (outputLevel === "designed_asset") {
+    const complete = designedAsset?.status === "complete";
+    return {
+      status: complete ? "complete" : "blocked",
+      output_level: outputLevel,
+      paths,
+      vision,
+      ...(inputProvenance ? { input_provenance: inputProvenance } : {}),
+      ...(designedAsset ? { designed_asset: designedAsset } : {}),
+      next_action: complete
+        ? "stop_designed_asset_complete"
+        : designedAsset?.next_action || "stop_and_report_designed_asset_error",
+    };
+  }
+  if (productionPlan || planValidation) {
+    const valid = planValidation?.valid === true && productionPlan?.images?.length === 7;
+    const paidGenerationAuthorized = valid && executionAuthorization?.authorized === true;
+    return {
+      status: valid ? "waiting_for_confirmation" : "blocked",
+      output_level: outputLevel,
+      paths,
+      vision,
+      ...(inputProvenance ? { input_provenance: inputProvenance } : {}),
+      production: {
+        mode: "seven_image_vertical_slice",
+        plan_file: paths.prompt_plan,
+        schema_version: productionPlan?.schema_version || "0.6",
+        page_count: productionPlan?.images?.length || 0,
+        validation: planValidation || { valid: false, errors: ["production plan was not compiled"] },
+        estimated_cost_cny: 1.54,
+        max_cost_required_cny: 1.54,
+        paid_generation_authorized: paidGenerationAuthorized,
+        authorization_source: paidGenerationAuthorized ? executionAuthorization.source : null,
+        authorization_max_cost_cny: paidGenerationAuthorized ? executionAuthorization.max_cost_cny : null,
+        preflight_plan_sha256: preflightRecord?.plan_sha256 || null,
+        paid_external_calls: 0,
+      },
+      files: valid ? [paths.prompt_plan] : [],
+      next_action: valid
+        ? paidGenerationAuthorized
+          ? "execute_same_run_under_recorded_authorization"
+          : "review_plan_and_explicitly_authorize_execute"
+        : "stop_and_report_plan_validation_error",
+    };
+  }
+  return {
+    status: "ready_for_plan",
+    output_level: outputLevel,
+    paths,
+    vision,
+    ...(inputProvenance ? { input_provenance: inputProvenance } : {}),
+    next_action: "write_prompt_plan_then_execute_same_run",
+  };
+}
+
+export function normalizeExecutionAuthorization(productionBrief = {}, planSha256 = "") {
+  const input = productionBrief?.authorization;
+  if (!input || input.mode !== "preauthorized") return null;
+  if (input.source !== "current_user_prompt") {
+    throw new Error("EXECUTION_AUTHORIZATION_SOURCE_INVALID: preauthorization must come from the current user prompt");
+  }
+  const maxCost = Number(input.maxCost ?? 1.54);
+  if (!Number.isFinite(maxCost) || maxCost < 1.54) {
+    throw new Error("EXECUTION_AUTHORIZATION_COST_LIMIT_INSUFFICIENT: expected at least 1.54");
+  }
+  return {
+    schema_version: "0.1",
+    authorized: true,
+    mode: "preauthorized",
+    source: input.source,
+    max_cost_cny: maxCost,
+    plan_sha256: planSha256,
+  };
+}
+
+async function readOptionalJson(filename) {
+  try {
+    return JSON.parse(await readFile(filename, "utf8"));
+  } catch (error) {
+    if (error?.code === "ENOENT") return null;
+    throw error;
+  }
+}
+
+export async function assertPreflightPlanUnchanged(paths) {
+  const preflight = await readOptionalJson(paths.preflight_record);
+  if (!preflight) return null;
+  const currentPlanText = await readFile(paths.prompt_plan, "utf8");
+  if (preflight.run_id !== paths.run_id || preflight.plan_sha256 !== sha256(currentPlanText)) {
+    throw new Error("PROMPT_PLAN_CHANGED_AFTER_PREFLIGHT: compile and validate a new run instead of editing prompt-plan.json");
+  }
+  return preflight;
+}
+
+export function isExecutorBlocked(summary) {
+  return new Set(["blocked", "error"]).has(summary?.status);
+}
+
+export function buildDesignedAssetFailure(error) {
+  const code = error?.code || (/^[A-Z][A-Z0-9_]+/.exec(error?.message || "")?.[0]) || "DESIGNED_ASSET_COMPOSITION_FAILED";
+  return {
+    status: "blocked",
+    error: { code, message: error?.message || String(error) },
+    next_action: code === "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE"
+      ? "attach_one_clear_product_image_and_start_again"
+      : "keep_visual_base_and_review_reference_background",
+  };
+}
+
 function runNode(scriptFile, childArgs) {
   return new Promise((resolvePromise, reject) => {
     const child = spawn(process.execPath, [scriptFile, ...childArgs], {
@@ -23,12 +251,16 @@ function runNode(scriptFile, childArgs) {
       stdio: ["ignore", "pipe", "pipe"],
     });
     let stdout = "";
+    let stderr = "";
     child.stdout.on("data", (chunk) => { stdout += chunk; });
-    child.stderr.on("data", (chunk) => { process.stderr.write(chunk); });
+    child.stderr.on("data", (chunk) => { stderr += chunk; process.stderr.write(chunk); });
     child.on("error", reject);
     child.on("close", (code) => {
       if (code !== 0) {
-        reject(new Error(`${scriptFile} exited with code ${code}`));
+        const error = new Error(`${scriptFile} exited with code ${code}`);
+        error.stdout = stdout;
+        error.stderr = stderr;
+        reject(error);
         return;
       }
       resolvePromise(stdout);
@@ -36,6 +268,394 @@ function runNode(scriptFile, childArgs) {
   });
 }
 
+function addCostRanges(...summaries) {
+  return summaries.reduce((total, summary) => ({
+    min: Number((total.min + Number(summary?.cost_range_cny?.min || summary?.confirmed_minimum_cost_cny || 0)).toFixed(2)),
+    max: Number((total.max + Number(summary?.cost_range_cny?.max
+      || (Number(summary?.confirmed_minimum_cost_cny || 0) + Number(summary?.potential_additional_cost_cny || 0)))).toFixed(2)),
+  }), { min: 0, max: 0 });
+}
+
+function auxiliaryModelUsage(...stages) {
+  const calls = stages.reduce((sum, stage) => sum + Number(stage?.model_calls || 0), 0);
+  const unitCost = Number(process.env.COMMERCE_VISION_COST_PER_CALL_CNY);
+  return {
+    calls,
+    usage: stages.map((stage) => stage?.usage || null),
+    cost_cny: Number.isFinite(unitCost) && unitCost >= 0 ? Number((calls * unitCost).toFixed(4)) : null,
+    pricing_status: Number.isFinite(unitCost) && unitCost >= 0 ? "configured_estimate" : "usage_recorded_price_not_configured",
+  };
+}
+
+async function writeReferenceFirstResult(paths, result) {
+  await writeFile(paths.reference_first_result, `${JSON.stringify(result, null, 2)}\n`, "utf8");
+  return result;
+}
+
+async function writeV3State(paths, state) {
+  const record = { schema_version: "1.0", workflow: V3_WORKFLOW, run_id: paths.run_id, ...state, updated_at: new Date().toISOString() };
+  await writeFile(paths.v3_stage_state, `${JSON.stringify(record, null, 2)}\n`, "utf8");
+  return record;
+}
+
+function generatedInputs(summary, localImages) {
+  const remote = Array.isArray(summary?.remote_images) ? summary.remote_images.filter(Boolean) : [];
+  return remote.length === localImages.length ? remote : localImages;
+}
+
+function providerLocalImages(record) {
+  const direct = Array.isArray(record?.local_images) ? record.local_images.filter(Boolean) : [];
+  if (direct.length) return direct;
+  return Array.isArray(record?.results) ? record.results.filter((item) => item.status === "complete").flatMap((item) => item.local_images || []).filter(Boolean) : [];
+}
+
+function providerRemoteImages(record) {
+  const direct = Array.isArray(record?.remote_images) ? record.remote_images.filter(Boolean) : [];
+  if (direct.length) return direct;
+  return Array.isArray(record?.results) ? record.results.filter((item) => item.status === "complete").flatMap((item) => item.images || []).filter(Boolean) : [];
+}
+
+export async function startClaudeAuthoredV3({ paths, originalReference, productionBrief = {}, referenceCapture = null, inputProvenance = null, observeOriginal = observeOriginalVisual } = {}) {
+  if (!paths?.run_id || !originalReference) throw new Error("CLAUDE_AUTHORED_V3_START_INPUT_REQUIRED");
+  const observationStage = await observeOriginal({ runId: paths.run_id, source: originalReference, outputFile: paths.original_visual_observation });
+  const observationSha = contractSha256(observationStage.observation);
+  const request = {
+    schema_version: "1.0", workflow: V3_WORKFLOW, run_id: paths.run_id, original_reference: originalReference,
+    production_brief: productionBrief, expected_paid_images: 9, estimated_cost_cny: 1.98,
+    direct_generation_authorization: { authorized: true, source: "current_user_prompt", max_cost_cny: 1.98 },
+  };
+  await writeFile(paths.reference_first_request, `${JSON.stringify(request, null, 2)}\n`, "utf8");
+  if (referenceCapture) await writeFile(paths.reference_capture, `${JSON.stringify(referenceCapture, null, 2)}\n`, "utf8");
+  await writeV3State(paths, { stage: "ready_for_reference_authoring", original_observation_sha256: observationSha, paid_image_calls: 0, auxiliary_model_calls: observationStage.model_calls });
+  const result = {
+    status: "ready_for_reference_authoring", output_level: "visual_base", workflow: V3_WORKFLOW, paths,
+    original_visual_observation: observationStage.observation, original_visual_observation_sha256: observationSha,
+    reference_capture: referenceCapture, input_provenance: inputProvenance,
+    authorization: request.direct_generation_authorization,
+    cost: { confirmed_image_cost_cny: 0, next_stage_max_image_cost_cny: 0.44, total_authorized_max_cny: 1.98 },
+    files: [paths.original_visual_observation], next_action: "claude_author_reference_assets_then_generate_references",
+  };
+  await writeReferenceFirstResult(paths, result);
+  return result;
+}
+
+export async function generateClaudeAuthoredReferences({ paths, referenceAuthoring, executeNode = runNode, observeReferences = observeGeneratedReferences, writeBoardCopy = writeSettingBoardCopyPlan, renderBoard = renderAnnotatedSettingBoard } = {}) {
+  const request = await readOptionalJson(paths?.reference_first_request);
+  const originalObservation = await readOptionalJson(paths?.original_visual_observation);
+  if (request?.workflow !== V3_WORKFLOW || !originalObservation) throw new Error("CLAUDE_AUTHORED_V3_START_REQUIRED");
+  const observationSha = contractSha256(originalObservation);
+  const authored = validateClaudeReferenceAuthoring(referenceAuthoring, { runId: paths.run_id, observationSha256: observationSha });
+  await writeFile(paths.claude_reference_authoring, `${JSON.stringify(authored, null, 2)}\n`, "utf8");
+  const authoringSha = contractSha256(authored);
+  const referencePlan = compileReferenceAssetPlan({ runId: paths.run_id, originalReference: request.original_reference, settingBoardPrompt: authored.setting_board_visual_prompt, authoredBy: "claude", sourceAuthoringSha256: authoringSha });
+  await writeFile(paths.reference_asset_plan, `${JSON.stringify(referencePlan, null, 2)}\n`, "utf8");
+  const referenceStdout = await executeNode(jimengClientFile, [paths.reference_asset_plan, "--execute", "--asset-map", paths.asset_map, "--max-cost", "0.44", "--output", paths.reference_asset_result]);
+  const referenceSummary = JSON.parse(referenceStdout);
+  const localImages = Array.isArray(referenceSummary.local_images) ? referenceSummary.local_images.filter(Boolean) : [];
+  if (referenceSummary.status !== "complete" || localImages.length !== 2) {
+    const costRange = addCostRanges(referenceSummary);
+    await writeV3State(paths, { stage: "reference_generation_partial", authoring_sha256: authoringSha, paid_image_calls: localImages.length, cost_range_cny: costRange });
+    return writeReferenceFirstResult(paths, { status: "partial", workflow: V3_WORKFLOW, output_level: "visual_base", paths, reference_assets: referenceSummary, files: localImages, local_images: localImages, cost: { stage_cost_range_cny: costRange, next_stage_max_image_cost_cny: Number(((2 - localImages.length) * 0.22).toFixed(2)) }, next_action: "resume_same_run_after_reference_generation_technical_failure" });
+  }
+  await copyFile(localImages[1], paths.setting_board_visual);
+  const copyPlan = await writeBoardCopy(authored.setting_board_spec, paths.setting_board_copy_plan);
+  const boardReport = await renderBoard({ sourceVisual: paths.setting_board_visual, copyPlan, outputFile: paths.setting_board_annotated, reportFile: paths.setting_board_layout_report });
+  const inputs = generatedInputs(referenceSummary, localImages);
+  const referenceObservationStage = await observeReferences({ runId: paths.run_id, sources: inputs, outputFile: paths.reference_visual_observation });
+  const referenceObservationSha = contractSha256(referenceObservationStage.observation);
+  await writeV3State(paths, { stage: "ready_for_set_authoring", authoring_sha256: authoringSha, reference_observation_sha256: referenceObservationSha, paid_image_calls: 2, confirmed_image_cost_cny: 0.44, auxiliary_model_calls: referenceObservationStage.model_calls });
+  const result = {
+    status: "ready_for_set_authoring", workflow: V3_WORKFLOW, output_level: "visual_base", paths,
+    reference_assets: { ...referenceSummary, local_images: localImages, generation_inputs: inputs },
+    reference_visual_observation: referenceObservationStage.observation, reference_visual_observation_sha256: referenceObservationSha,
+    setting_board: { visual_file: paths.setting_board_visual, annotated_file: paths.setting_board_annotated, copy_plan_file: paths.setting_board_copy_plan, layout_report_file: paths.setting_board_layout_report, report: boardReport },
+    cost: { confirmed_image_cost_cny: 0.44, next_stage_max_image_cost_cny: 1.54, total_authorized_max_cny: 1.98 },
+    files: [localImages[0], paths.setting_board_annotated, paths.reference_visual_observation], local_images: [localImages[0], paths.setting_board_annotated],
+    original_reference_used_for_set: false, next_action: "claude_author_seven_page_plan_then_generate_set",
+  };
+  await writeReferenceFirstResult(paths, result); return result;
+}
+
+function v3CopyPlan(plan) {
+  return { schema_version: "1.0", run_id: plan.run_id, inferred_product_facts_forbidden: true, pages: plan.pages.map((page) => ({ slot_id: page.id, role: page.role, overlay_required: page.overlay_required, copy: page.copy, source_refs: page.source_refs })) };
+}
+
+function v3Blueprint(plan) {
+  return { schema_version: "3.0", run_id: plan.run_id, platform_strategy: { resolved_profile: plan.platform.resolved_profile }, pages: plan.pages.map((page) => ({ id: page.id, role: page.role, template_id: page.template_id })) };
+}
+
+export async function generateClaudeAuthoredSet({ paths, setPlan, executeNode = runNode, detectLayout = detectReferenceFirstLayoutRegions, postprocessDesign = runReferenceFirstDesignPostprocessor } = {}) {
+  const request = await readOptionalJson(paths?.reference_first_request); const referenceObservation = await readOptionalJson(paths?.reference_visual_observation); const referenceResult = await readOptionalJson(paths?.reference_asset_result);
+  if (request?.workflow !== V3_WORKFLOW || !referenceObservation || !referenceResult) throw new Error("CLAUDE_AUTHORED_V3_REFERENCES_REQUIRED");
+  const referenceObservationSha = contractSha256(referenceObservation);
+  const authored = validateClaudeAuthoredSetPlan(setPlan, { runId: paths.run_id, referenceObservationSha256: referenceObservationSha });
+  await writeFile(paths.claude_authored_set_plan, `${JSON.stringify(authored, null, 2)}\n`, "utf8");
+  const referenceLocal = providerLocalImages(referenceResult);
+  const referenceRemote = providerRemoteImages(referenceResult);
+  const referenceInputs = referenceRemote.length === 2 ? referenceRemote : referenceLocal;
+  if (referenceInputs.length !== 2) throw new Error("CLAUDE_AUTHORED_V3_TWO_REFERENCE_ASSETS_REQUIRED");
+  const promptPlan = assembleClaudeAuthoredPromptPlan({ plan: authored, generatedReferences: referenceInputs, aspectRatio: request.production_brief?.aspectRatio || "1:1" });
+  await writeFile(paths.prompt_plan, `${JSON.stringify(promptPlan, null, 2)}\n`, "utf8");
+  const stdout = await executeNode(jimengClientFile, [paths.prompt_plan, "--execute", "--asset-map", paths.asset_map, "--max-cost", "1.54", "--output", paths.jimeng_result]);
+  const setSummary = JSON.parse(stdout); const setImages = Array.isArray(setSummary.local_images) ? setSummary.local_images.filter(Boolean) : [];
+  const costRange = addCostRanges({ cost_range_cny: { min: 0.44, max: 0.44 } }, setSummary);
+  if (setSummary.status !== "complete" || setImages.length !== 7) {
+    await writeV3State(paths, { stage: "set_generation_partial", set_plan_sha256: contractSha256(authored), paid_image_calls: 2 + setImages.length, cost_range_cny: costRange });
+    return writeReferenceFirstResult(paths, { status: "partial", workflow: V3_WORKFLOW, output_level: "visual_base", paths, set_generation: setSummary, visual_base_images: setImages, files: [referenceLocal[0], paths.setting_board_annotated, ...setImages].filter(Boolean), local_images: setImages, cost: { total_cost_range_cny: costRange, next_stage_max_image_cost_cny: Number(((7 - setImages.length) * 0.22).toFixed(2)) }, original_reference_used_for_set: false, next_action: "resume_same_run_after_set_generation_technical_failure" });
+  }
+  const copyPlan = v3CopyPlan(authored); await writeFile(paths.copy_plan, `${JSON.stringify(copyPlan, null, 2)}\n`, "utf8");
+  const remoteSet = generatedInputs(setSummary, setImages); let layoutDetection; let designedSet;
+  try {
+    layoutDetection = await detectLayout({ runId: paths.run_id, imageUrls: remoteSet, outputFile: paths.layout_regions });
+    designedSet = await postprocessDesign({ runDirectory: paths.run_directory, outputDirectory: join(paths.run_directory, "平台设计成品V3"), baseImages: setImages, blueprint: v3Blueprint(authored), copyPlan, layoutRegions: layoutDetection.regions, aspectRatio: request.production_brief?.aspectRatio || "1:1", layoutReportFile: paths.layout_report });
+  } catch (error) {
+    await writeV3State(paths, { stage: "design_postprocess_partial", set_plan_sha256: contractSha256(authored), paid_image_calls: 9, confirmed_image_cost_cny: 1.98, technical_error: error?.message || String(error) });
+    return writeReferenceFirstResult(paths, { status: "partial", workflow: V3_WORKFLOW, output_level: "visual_base", paths, set_generation: setSummary, visual_base_images: setImages, layout_geometry: { status: "blocked", error: error?.message || String(error) }, files: [referenceLocal[0], paths.setting_board_annotated, ...setImages].filter(Boolean), local_images: setImages, cost: { confirmed_image_cost_cny: 1.98, next_stage_max_image_cost_cny: 0 }, original_reference_used_for_set: false, next_action: "resume_design_postprocess_without_regenerating_paid_images" });
+  }
+  const designedImages = designedSet.files.map((item) => item.file);
+  await writeV3State(paths, { stage: "complete", set_plan_sha256: contractSha256(authored), paid_image_calls: 9, confirmed_image_cost_cny: 1.98, auxiliary_model_calls: layoutDetection.model_calls });
+  const result = {
+    status: "complete", workflow: V3_WORKFLOW, output_level: "platform_designed_image_set_v3", paths,
+    reference_assets: { ...referenceResult, local_images: referenceLocal }, setting_board: { visual_file: paths.setting_board_visual, annotated_file: paths.setting_board_annotated },
+    claude_authored_set_plan_file: paths.claude_authored_set_plan, prompt_plan_file: paths.prompt_plan,
+    set_generation: { ...setSummary, local_images: setImages }, visual_base_images: setImages,
+    layout_geometry: { status: "complete", regions_file: paths.layout_regions, model_calls: layoutDetection.model_calls, reused: layoutDetection.reused, usage: layoutDetection.usage }, designed_set: designedSet,
+    local_images: [referenceLocal[0], paths.setting_board_annotated, ...designedImages], files: [referenceLocal[0], paths.setting_board_annotated, ...designedImages],
+    summary: { completed: 9, expected_total: 9, reference_assets_completed: 2, set_images_completed: 7, designed_images_completed: 7, cost_range_cny: costRange },
+    cost: { confirmed_image_cost_cny: 1.98, next_stage_max_image_cost_cny: 0 }, content_review_enabled: false, quality_gate_enabled: false, structural_contract_validation_enabled: true, original_reference_used_for_set: false,
+    next_action: "deliver_three_view_annotated_setting_board_and_platform_designed_seven_images",
+  };
+  await writeReferenceFirstResult(paths, result); return result;
+}
+
+export async function runReferenceFirstPipeline({
+  paths,
+  originalReference,
+  productionBrief = {},
+  executeNode = runNode,
+  planCreative = planReferenceFirstCreative,
+  writeCopyPlan = writeReferenceFirstCopyPlan,
+  detectLayout = detectReferenceFirstLayoutRegions,
+  postprocessDesign = runReferenceFirstDesignPostprocessor,
+} = {}) {
+  if (!paths?.run_id || !originalReference) throw new Error("REFERENCE_FIRST_PIPELINE_INPUT_REQUIRED");
+  const requestRecord = {
+    schema_version: "0.1",
+    workflow: "reference_first_platform_v2",
+    run_id: paths.run_id,
+    original_reference: originalReference,
+    production_brief: productionBrief,
+    expected_paid_images: 9,
+    estimated_cost_cny: 1.98,
+  };
+  await writeFile(paths.reference_first_request, `${JSON.stringify(requestRecord, null, 2)}\n`, "utf8");
+
+  const referencePlan = compileReferenceAssetPlan({
+    runId: paths.run_id,
+    originalReference,
+  });
+  await writeFile(paths.reference_asset_plan, `${JSON.stringify(referencePlan, null, 2)}\n`, "utf8");
+  const referenceStdout = await executeNode(jimengClientFile, [
+    paths.reference_asset_plan,
+    "--execute",
+    "--asset-map", paths.asset_map,
+    "--max-cost", "0.44",
+    "--output", paths.reference_asset_result,
+  ]);
+  const referenceSummary = JSON.parse(referenceStdout);
+  const generatedReferences = Array.isArray(referenceSummary.local_images)
+    ? referenceSummary.local_images.filter(Boolean)
+    : [];
+  if (referenceSummary.status !== "complete" || generatedReferences.length !== 2) {
+    const costRange = addCostRanges(referenceSummary);
+    return writeReferenceFirstResult(paths, {
+      status: "partial",
+      output_level: "visual_base",
+      workflow: "reference_first_platform_v2",
+      paths,
+      reference_assets: referenceSummary,
+      local_images: generatedReferences,
+      files: generatedReferences,
+      summary: {
+        completed: generatedReferences.length,
+        failed: Math.max(0, 2 - generatedReferences.length),
+        expected_total: 9,
+        cost_range_cny: costRange,
+      },
+      content_review_enabled: false,
+      next_action: "resume_same_run_after_reference_asset_technical_failure",
+    });
+  }
+
+  const generatedReferenceInputs = Array.isArray(referenceSummary.remote_images)
+    && referenceSummary.remote_images.filter(Boolean).length === 2
+    ? referenceSummary.remote_images.filter(Boolean)
+    : generatedReferences;
+  let creativePlanning;
+  try {
+    creativePlanning = await planCreative({
+      runId: paths.run_id,
+      generatedReferences: generatedReferenceInputs,
+      brief: productionBrief,
+      outputFile: paths.set_blueprint,
+    });
+  } catch (error) {
+    const costRange = addCostRanges(referenceSummary);
+    return writeReferenceFirstResult(paths, {
+      status: "partial",
+      output_level: "visual_base",
+      workflow: "reference_first_platform_v2",
+      paths,
+      reference_assets: referenceSummary,
+      creative_planning: { status: "blocked", error: error?.message || String(error), model_calls: Number(error?.model_calls || 0), usage: error?.usage || null },
+      local_images: generatedReferences,
+      files: generatedReferences,
+      summary: {
+        completed: generatedReferences.length,
+        failed: 0,
+        expected_total: 9,
+        reference_assets_completed: generatedReferences.length,
+        set_images_completed: 0,
+        cost_range_cny: costRange,
+      },
+      content_review_enabled: false,
+      validation_enabled: false,
+      original_reference_used_for_set: false,
+      next_action: "configure_creative_planner_then_resume_same_run_without_regenerating_reference_assets",
+    });
+  }
+  const setPlan = compileReferenceFirstSetPlan({
+    runId: paths.run_id,
+    generatedReferences: generatedReferenceInputs,
+    brief: productionBrief,
+    blueprint: creativePlanning.blueprint,
+  });
+  await writeFile(paths.prompt_plan, `${JSON.stringify(setPlan, null, 2)}\n`, "utf8");
+  const setStdout = await executeNode(jimengClientFile, [
+    paths.prompt_plan,
+    "--execute",
+    "--asset-map", paths.asset_map,
+    "--max-cost", "1.54",
+    "--output", paths.jimeng_result,
+  ]);
+  const setSummary = JSON.parse(setStdout);
+  const setImages = Array.isArray(setSummary.local_images) ? setSummary.local_images.filter(Boolean) : [];
+  const generatedSetComplete = setSummary.status === "complete" && setImages.length === 7;
+  const costRange = addCostRanges(referenceSummary, setSummary);
+  if (!generatedSetComplete) {
+    const localImages = [...generatedReferences, ...setImages];
+    return writeReferenceFirstResult(paths, {
+      status: "partial", output_level: "visual_base", workflow: "reference_first_platform_v2", paths,
+      reference_assets: { ...referenceSummary, plan_file: paths.reference_asset_plan, result_file: paths.reference_asset_result, local_images: generatedReferences },
+      creative_planning: { status: "complete", blueprint_file: paths.set_blueprint, reused: creativePlanning.reused, model_calls: creativePlanning.model_calls, usage: creativePlanning.usage },
+      set_generation: { ...setSummary, plan_file: paths.prompt_plan, result_file: paths.jimeng_result, local_images: setImages },
+      visual_base_images: setImages, local_images: localImages, files: localImages,
+      summary: { completed: localImages.length, failed: Math.max(0, 9 - localImages.length), expected_total: 9, reference_assets_completed: generatedReferences.length, set_images_completed: setImages.length, cost_range_cny: costRange },
+      estimated_cost_cny: 1.98, confirmed_minimum_cost_cny: costRange.min,
+      potential_additional_cost_cny: Number((costRange.max - costRange.min).toFixed(2)),
+      content_review_enabled: false, validation_enabled: false, original_reference_used_for_set: false,
+      next_action: "resume_same_run_after_generation_technical_failure",
+    });
+  }
+
+  const copyPlan = await writeCopyPlan({ brief: productionBrief, blueprint: creativePlanning.blueprint, outputFile: paths.copy_plan });
+  const remoteSetImages = Array.isArray(setSummary.remote_images) && setSummary.remote_images.filter(Boolean).length === 7
+    ? setSummary.remote_images.filter(Boolean)
+    : Array.isArray(setSummary.results)
+      ? setSummary.results.flatMap((item) => Array.isArray(item.images) ? item.images.slice(0, 1) : []).filter(Boolean)
+      : [];
+  let layoutDetection;
+  let designedSet;
+  try {
+    layoutDetection = await detectLayout({
+      runId: paths.run_id, imageUrls: remoteSetImages, outputFile: paths.layout_regions,
+    });
+    designedSet = await postprocessDesign({
+      runDirectory: paths.run_directory,
+      outputDirectory: join(paths.run_directory, "平台设计成品V2"),
+      baseImages: setImages,
+      blueprint: creativePlanning.blueprint,
+      copyPlan,
+      layoutRegions: layoutDetection.regions,
+      aspectRatio: productionBrief.aspectRatio || "1:1",
+      layoutReportFile: paths.layout_report,
+    });
+  } catch (error) {
+    const localImages = [...generatedReferences, ...setImages];
+    return writeReferenceFirstResult(paths, {
+      status: "partial", output_level: "visual_base", workflow: "reference_first_platform_v2", paths,
+      reference_assets: { ...referenceSummary, plan_file: paths.reference_asset_plan, result_file: paths.reference_asset_result, local_images: generatedReferences },
+      creative_planning: { status: "complete", blueprint_file: paths.set_blueprint, reused: creativePlanning.reused, model_calls: creativePlanning.model_calls, usage: creativePlanning.usage },
+      set_generation: { ...setSummary, plan_file: paths.prompt_plan, result_file: paths.jimeng_result, local_images: setImages },
+      copy_plan_file: paths.copy_plan,
+      layout_geometry: { status: "blocked", error: error?.message || String(error), regions_file: paths.layout_regions, model_calls: Number(error?.model_calls || 0), usage: error?.usage || null },
+      design_status: "blocked", visual_base_images: setImages, local_images: localImages, files: localImages,
+      summary: { completed: 9, failed: 0, expected_total: 9, reference_assets_completed: 2, set_images_completed: 7, designed_images_completed: 0, cost_range_cny: costRange },
+      estimated_cost_cny: 1.98, confirmed_minimum_cost_cny: costRange.min,
+      potential_additional_cost_cny: Number((costRange.max - costRange.min).toFixed(2)),
+      content_review_enabled: false, validation_enabled: false, original_reference_used_for_set: false,
+      next_action: "configure_layout_geometry_then_resume_same_run_without_regenerating_paid_images",
+    });
+  }
+  const designedImages = designedSet.files.map((item) => item.file);
+  const localImages = [...generatedReferences, ...designedImages];
+  const complete = designedImages.length === 7;
+  return writeReferenceFirstResult(paths, {
+    status: complete ? "complete" : "partial",
+    output_level: "visual_base",
+    workflow: "reference_first_platform_v2",
+    paths,
+    reference_assets: {
+      ...referenceSummary,
+      plan_file: paths.reference_asset_plan,
+      result_file: paths.reference_asset_result,
+      local_images: generatedReferences,
+    },
+    creative_planning: {
+      status: "complete",
+      blueprint_file: paths.set_blueprint,
+      reused: creativePlanning.reused,
+      model_calls: creativePlanning.model_calls,
+      usage: creativePlanning.usage,
+    },
+    set_generation: {
+      ...setSummary,
+      plan_file: paths.prompt_plan,
+      result_file: paths.jimeng_result,
+      local_images: setImages,
+    },
+    visual_base_images: setImages,
+    copy_plan_file: paths.copy_plan,
+    layout_geometry: {
+      status: "complete", regions_file: paths.layout_regions,
+      reused: layoutDetection.reused, model_calls: layoutDetection.model_calls, usage: layoutDetection.usage,
+    },
+    auxiliary_model_usage: auxiliaryModelUsage(creativePlanning, layoutDetection),
+    designed_set: designedSet,
+    designed_production_files: designedSet.files,
+    layout_report_file: paths.layout_report,
+    local_images: localImages,
+    files: localImages,
+    summary: {
+      completed: localImages.length,
+      failed: Math.max(0, 9 - localImages.length),
+      expected_total: 9,
+      reference_assets_completed: generatedReferences.length,
+      set_images_completed: setImages.length,
+      designed_images_completed: designedImages.length,
+      cost_range_cny: costRange,
+    },
+    estimated_cost_cny: 1.98,
+    confirmed_minimum_cost_cny: costRange.min,
+    potential_additional_cost_cny: Number((costRange.max - costRange.min).toFixed(2)),
+    content_review_enabled: false,
+    validation_enabled: false,
+    original_reference_used_for_set: false,
+    layout_geometry_enabled: true,
+    next_action: complete ? "deliver_reference_assets_and_platform_designed_seven_images" : "resume_same_run_after_technical_failure",
+  });
+}
+
 export async function auditDelivery(paths, result) {
   const tracked = new Set(
     (result.results ?? [])
@@ -57,49 +677,262 @@ export async function auditDelivery(paths, result) {
   };
 }
 
-async function withDeliveryAudit(paths, summary) {
+export async function withDeliveryAudit(paths, summary) {
   const state = JSON.parse(await readFile(paths.jimeng_result, "utf8"));
   const deliveryIntegrity = await auditDelivery(paths, state);
+  const executionAuthorization = await readOptionalJson(paths.execution_authorization);
+  const promptPlan = await readOptionalJson(paths.prompt_plan);
+  const setQualityReview = await verifyProductionSetQuality({ runState: state, promptPlan: promptPlan || {} });
+  const baseOutcome = compileProductionOutcome({
+    runState: state,
+    deliveryIntegrity,
+    executionAuthorization,
+    setQualityReview,
+  });
+  let designedProduction = {
+    status: "blocked",
+    reason: baseOutcome.status !== "complete"
+      ? "base_result_incomplete"
+      : baseOutcome.quality_status !== "quality_passed"
+        ? "set_quality_gate_not_passed"
+        : "design_bridge_not_run",
+    quality_status: baseOutcome.quality_status,
+    required_base_pages: 7,
+    paid_external_calls: 0,
+    files: [],
+  };
+  if (baseOutcome.status === "complete"
+    && baseOutcome.quality_status === "quality_passed"
+    && deliveryIntegrity.status === "verified") {
+    try {
+      const vision = await readOptionalJson(paths.vision_result);
+      designedProduction = await runProductionDesignBridge({
+        runState: state,
+        runDirectory: paths.run_directory,
+        outputDirectory: join(paths.run_directory, "designed-production"),
+        reference: vision?.reference?.source || vision?.reference?.public_url,
+        productUnderstanding: vision?.product_understanding || {},
+        promptPlan: promptPlan || {},
+        setQualityReview,
+      });
+    } catch (error) {
+      designedProduction = {
+        status: "blocked",
+        reason: "design_bridge_failed",
+        error: { code: error?.code || "PRODUCTION_DESIGN_BRIDGE_FAILED", message: error?.message || String(error) },
+        required_base_pages: 7,
+        paid_external_calls: 0,
+        files: [],
+      };
+    }
+  }
+  const productionOutcome = { ...baseOutcome, designed_production: designedProduction };
+  await writeFile(paths.production_outcome, `${JSON.stringify(productionOutcome, null, 2)}\n`, "utf8");
   if (deliveryIntegrity.status === "verified") {
-    return { ...summary, delivery_integrity: deliveryIntegrity };
+    return {
+      ...summary,
+      delivery_integrity: deliveryIntegrity,
+      production_outcome: productionOutcome,
+      production_outcome_file: paths.production_outcome,
+      designed_production: designedProduction,
+      designed_production_files: designedProduction.files || [],
+    };
   }
   return {
     ...summary,
     status: "blocked",
     delivery_integrity: deliveryIntegrity,
+    production_outcome: productionOutcome,
+    production_outcome_file: paths.production_outcome,
+    designed_production: designedProduction,
+    designed_production_files: designedProduction.files || [],
     next_action: "stop_and_report_delivery_integrity_error",
   };
 }
 
 async function runCli() {
 if (command === "start") {
+  const outputLevel = option("--output-level", "visual_base");
+  if (!new Set(["visual_base", "designed_asset"]).has(outputLevel)) {
+    throw new Error("OUTPUT_LEVEL_INVALID: expected visual_base or designed_asset");
+  }
   const explicitImage = option("--image");
-  const invocation = await inspectLatestClaudeInvocation({
-    cwd: option("--cwd", process.cwd()),
-    claudeConfigDirectory: option("--claude-config-dir", process.env.CLAUDE_CONFIG_DIR),
-    sessionId: option("--session-id", process.env.CLAUDE_CODE_SESSION_ID || ""),
+  const preferCurrentAttachment = args.includes("--prefer-current-attachment");
+  const attachmentCwd = resolveAttachmentCwd({ explicitCwd: option("--cwd") });
+  const claudeConfigDirectory = option("--claude-config-dir", process.env.CLAUDE_CONFIG_DIR);
+  const sessionId = option("--session-id", process.env.CLAUDE_CODE_SESSION_ID || "");
+  const invocation = await resolveStartInvocation({
+    explicitImage,
+    preferCurrentAttachment,
+    cwd: attachmentCwd,
+    claudeConfigDirectory,
+    sessionId,
   });
-  if (!explicitImage && invocation.image_count !== 1) {
-    throw new Error(`CURRENT_REQUEST_IMAGE_COUNT_INVALID: expected one image, found ${invocation.image_count}`);
+  const paths = await createRun(invocation
+    ? { requestKey: `${invocation.session_id}:${invocation.prompt_id}` }
+    : {});
+  const productionBriefJson = option("--production-brief-json");
+  const visionArgs = buildVisionArguments({
+    invocation,
+    explicitImage,
+    outputFile: paths.vision_result,
+    attachmentCwd,
+    claudeConfigDirectory,
+    sessionId,
+  });
+  if (outputLevel === "visual_base" && productionBriefJson) {
+    const captureStdout = await runNode(productVisionFile, [...visionArgs, "--dry-run"]);
+    const referenceCapture = JSON.parse(captureStdout);
+    const inputProvenance = buildInputProvenance({ invocation, explicitImage, vision: referenceCapture });
+    const originalReference = referenceCapture?.reference?.source || referenceCapture?.reference?.public_url;
+    if (referenceCapture?.status !== "dry-run" || !originalReference) {
+      console.log(JSON.stringify({
+        status: "blocked",
+        output_level: outputLevel,
+        workflow: "reference_first_platform_v2",
+        paths,
+        reference_capture: referenceCapture,
+        input_provenance: inputProvenance,
+        error: { code: "REFERENCE_CAPTURE_FAILED", message: "当前商品图片未能进入参考资产优先流程" },
+        next_action: "attach_one_product_image_and_start_again",
+      }, null, 2));
+      return;
+    }
+    const productionBrief = JSON.parse(productionBriefJson);
+    if (productionBrief.orchestrationMode !== "legacy_internal_planner") {
+      const result = await startClaudeAuthoredV3({ paths, originalReference, productionBrief, referenceCapture, inputProvenance });
+      console.log(JSON.stringify(result, null, 2));
+      return;
+    }
+    const pipelineResult = await runReferenceFirstPipeline({ paths, originalReference, productionBrief });
+    const result = {
+      ...pipelineResult,
+      reference_capture: referenceCapture,
+      input_provenance: inputProvenance,
+    };
+    await writeReferenceFirstResult(paths, result);
+    console.log(JSON.stringify(result, null, 2));
+    return;
   }
-  const paths = await createRun({ requestKey: `${invocation.session_id}:${invocation.prompt_id}` });
-  const visionStdout = await runNode(productVisionFile, explicitImage
-    ? ["--image", explicitImage, "--output", paths.vision_result]
-    : ["--from-claude-attachment", "--output", paths.vision_result]);
+  const visionStdout = await runNode(productVisionFile, visionArgs);
   const vision = JSON.parse(visionStdout);
-  console.log(JSON.stringify({
-    status: vision.status === "complete" ? "ready_for_plan" : "blocked",
-    paths,
+  const inputProvenance = buildInputProvenance({ invocation, explicitImage, vision });
+  let designedAsset = null;
+  let productionPlan = null;
+  let planValidation = null;
+  let preflightRecord = null;
+  let executionAuthorization = null;
+  if (outputLevel === "designed_asset" && vision.status === "complete") {
+    try {
+      designedAsset = await runDesignedAsset({
+        reference: vision?.reference?.source || explicitImage,
+        outputDirectory: join(paths.run_directory, "designed-assets"),
+        productUnderstanding: vision?.product_understanding || {},
+        externalCalls: vision?.external_calls || {},
+      });
+    } catch (error) {
+      designedAsset = buildDesignedAssetFailure(error);
+    }
+  }
+  if (outputLevel === "visual_base" && productionBriefJson && vision.status === "complete") {
+    try {
+      const productionBrief = JSON.parse(productionBriefJson);
+      productionPlan = compileProductionPlan({
+        runId: paths.run_id,
+        referenceUrl: vision?.reference?.public_url,
+        productUnderstanding: vision?.product_understanding,
+        brief: productionBrief,
+      });
+      await writeFile(paths.prompt_plan, `${JSON.stringify(productionPlan, null, 2)}\n`, "utf8");
+      const validationStdout = await runNode(validatePlanFile, [paths.prompt_plan]);
+      planValidation = JSON.parse(validationStdout);
+      if (planValidation?.valid === true && productionPlan?.images?.length === 7) {
+        const planText = await readFile(paths.prompt_plan, "utf8");
+        preflightRecord = {
+          schema_version: "0.1",
+          run_id: paths.run_id,
+          plan_sha256: sha256(planText),
+          validation: { valid: true, image_count: 7 },
+          paid_external_calls: 0,
+          created_at: new Date().toISOString(),
+        };
+        await writeFile(paths.preflight_record, `${JSON.stringify(preflightRecord, null, 2)}\n`, "utf8");
+        executionAuthorization = normalizeExecutionAuthorization(productionBrief, preflightRecord.plan_sha256);
+        if (executionAuthorization) {
+          executionAuthorization = {
+            ...executionAuthorization,
+            run_id: paths.run_id,
+            recorded_at: new Date().toISOString(),
+          };
+          await writeFile(paths.execution_authorization, `${JSON.stringify(executionAuthorization, null, 2)}\n`, "utf8");
+        }
+      }
+    } catch (error) {
+      let detail = null;
+      try { detail = JSON.parse(String(error?.stderr || "")); } catch { detail = null; }
+      planValidation = {
+        valid: false,
+        errors: detail?.errors || [error?.message || "PRODUCTION_PLAN_COMPILATION_FAILED"],
+      };
+    }
+  }
+  console.log(JSON.stringify(buildStartResult({
     vision,
-    next_action: vision.status === "complete"
-      ? "write_prompt_plan_then_execute_same_run"
-      : "stop_and_report_error",
-  }, null, 2));
+    outputLevel,
+    paths,
+    designedAsset,
+    inputProvenance,
+    productionPlan,
+    planValidation,
+    preflightRecord,
+    executionAuthorization,
+  }), null, 2));
+} else if (command === "generate_references") {
+  const runId = option("--run-id"); if (!runId) throw new Error("RUN_ID_REQUIRED");
+  const authoringJson = option("--reference-authoring-json"); if (!authoringJson) throw new Error("REFERENCE_AUTHORING_JSON_REQUIRED");
+  console.log(JSON.stringify(await generateClaudeAuthoredReferences({ paths: pathsForRun(runId), referenceAuthoring: JSON.parse(authoringJson) }), null, 2));
+} else if (command === "generate_set") {
+  const runId = option("--run-id"); if (!runId) throw new Error("RUN_ID_REQUIRED");
+  const setPlanJson = option("--set-plan-json"); if (!setPlanJson) throw new Error("SET_PLAN_JSON_REQUIRED");
+  console.log(JSON.stringify(await generateClaudeAuthoredSet({ paths: pathsForRun(runId), setPlan: JSON.parse(setPlanJson) }), null, 2));
 } else if (command === "execute" || command === "resume") {
   const runId = option("--run-id");
   if (!runId) throw new Error("RUN_ID_REQUIRED");
   const paths = pathsForRun(runId);
+  const prototypeRequest = await readOptionalJson(paths.reference_first_request);
+  if (command === "resume" && prototypeRequest?.workflow === V3_WORKFLOW) {
+    const stage = await readOptionalJson(paths.v3_stage_state);
+    if (stage?.stage === "reference_generation_partial") {
+      console.log(JSON.stringify(await generateClaudeAuthoredReferences({ paths, referenceAuthoring: await readOptionalJson(paths.claude_reference_authoring) }), null, 2)); return;
+    }
+    if (new Set(["set_generation_partial", "design_postprocess_partial"]).has(stage?.stage)) {
+      console.log(JSON.stringify(await generateClaudeAuthoredSet({ paths, setPlan: await readOptionalJson(paths.claude_authored_set_plan) }), null, 2)); return;
+    }
+    const current = await readOptionalJson(paths.reference_first_result);
+    console.log(JSON.stringify({ ...current, resumed: false, reason: "waiting_for_claude_authored_next_stage_input", next_action: current?.next_action || "provide_next_stage_contract" }, null, 2)); return;
+  }
+  if (command === "resume" && new Set(["reference_first_prototype", "reference_first_platform_v2"]).has(prototypeRequest?.workflow)) {
+    const previousResult = await readOptionalJson(paths.reference_first_result);
+    const resumed = await runReferenceFirstPipeline({
+      paths,
+      originalReference: prototypeRequest.original_reference,
+      productionBrief: prototypeRequest.production_brief || {},
+    });
+    const result = {
+      ...resumed,
+      ...(previousResult?.reference_capture ? { reference_capture: previousResult.reference_capture } : {}),
+      ...(previousResult?.input_provenance ? { input_provenance: previousResult.input_provenance } : {}),
+    };
+    await writeReferenceFirstResult(paths, result);
+    console.log(JSON.stringify(result, null, 2));
+    return;
+  }
   const maxCost = option("--max-cost", "1.54");
+  await assertPreflightPlanUnchanged(paths);
+  const executionAuthorization = await readOptionalJson(paths.execution_authorization);
+  if (executionAuthorization?.authorized === true && Number(maxCost) > Number(executionAuthorization.max_cost_cny)) {
+    throw new Error("EXECUTION_AUTHORIZATION_LIMIT_EXCEEDED");
+  }
   const stdout = await runNode(jimengClientFile, [
     paths.prompt_plan,
     "--execute",
@@ -109,15 +942,28 @@ if (command === "start") {
     "--output", paths.jimeng_result,
   ]);
   const summary = JSON.parse(stdout);
+  if (isExecutorBlocked(summary)) {
+    console.log(JSON.stringify(summary, null, 2));
+    return;
+  }
   console.log(JSON.stringify(await withDeliveryAudit(paths, summary), null, 2));
 } else if (command === "status") {
   const runId = option("--run-id");
   if (!runId) throw new Error("RUN_ID_REQUIRED");
   const paths = pathsForRun(runId);
+  const prototypeResult = await readOptionalJson(paths.reference_first_result);
+  if (prototypeResult?.workflow === V3_WORKFLOW) {
+    const stage = await readOptionalJson(paths.v3_stage_state);
+    console.log(JSON.stringify({ ...prototypeResult, stage_state: stage }, null, 2)); return;
+  }
+  if (new Set(["reference_first_prototype", "reference_first_platform_v2"]).has(prototypeResult?.workflow)) {
+    console.log(JSON.stringify(prototypeResult, null, 2));
+    return;
+  }
   const result = JSON.parse(await readFile(paths.jimeng_result, "utf8"));
   console.log(JSON.stringify(await withDeliveryAudit(paths, { paths, result }), null, 2));
 } else {
-  throw new Error("Usage: node image-set-runner.mjs start [--image <path-or-url>] | execute --run-id <run_id> [--max-cost 1.54] | resume --run-id <run_id> | status --run-id <run_id>");
+  throw new Error("Usage: node image-set-runner.mjs start [--image <path-or-url>] | generate_references --run-id <run_id> --reference-authoring-json <json> | generate_set --run-id <run_id> --set-plan-json <json> | resume --run-id <run_id> | status --run-id <run_id>");
 }
 }
 

+ 387 - 3
claude-code/fmode-image-set/mcp/src/features/image-set/image-set-runner.test.mjs

@@ -1,9 +1,124 @@
 import assert from "node:assert/strict";
-import { rm, writeFile } from "node:fs/promises";
-import { join } from "node:path";
+import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
+import { join, resolve } from "node:path";
 import test from "node:test";
-import { auditDelivery } from "./image-set-runner.mjs";
+import sharp from "sharp";
+import {
+  auditDelivery,
+  buildDesignedAssetFailure,
+  buildInputProvenance,
+  buildStartResult,
+  buildVisionArguments,
+  assertPreflightPlanUnchanged,
+  isExecutorBlocked,
+  normalizeExecutionAuthorization,
+  runReferenceFirstPipeline,
+  resolveAttachmentCwd,
+  resolveStartInvocation,
+  withDeliveryAudit,
+} from "./image-set-runner.mjs";
 import { createRun } from "../../core/run-paths.mjs";
+import { compileSetBlueprint } from "./reference-first-set-blueprint.mjs";
+import { compileReferenceFirstCopyPlan } from "./reference-first-copy-plan.mjs";
+
+test("reference-first pipeline executes two assets then seven images without original set reference", async () => {
+  const paths = await createRun();
+  const original = join(paths.attachments_directory, "product.png");
+  const threeView = join(paths.reference_assets_directory, "产品三视图.png");
+  const settingBoard = join(paths.reference_assets_directory, "产品设定图.png");
+  const setImages = Array.from({ length: 7 }, (_, index) => join(paths.images_directory, `${String(index + 1).padStart(2, "0")}.png`));
+  const calls = [];
+  try {
+    await writeFile(original, "product");
+    const result = await runReferenceFirstPipeline({
+      paths,
+      originalReference: original,
+      productionBrief: { instruction: "生成淘宝中文七图", platform: "taobao" },
+      executeNode: async (script, args) => {
+        calls.push({ script, args });
+        if (args[0] === paths.reference_asset_plan) {
+          await Promise.all([writeFile(threeView, "three-view"), writeFile(settingBoard, "setting-board")]);
+          return JSON.stringify({ status: "complete", completed: 2, local_images: [threeView, settingBoard], cost_range_cny: { min: 0.44, max: 0.44 } });
+        }
+        await Promise.all(setImages.map((file) => writeFile(file, "set-image")));
+        return JSON.stringify({ status: "complete", completed: 7, local_images: setImages, cost_range_cny: { min: 1.54, max: 1.54 } });
+      },
+      planCreative: async ({ runId, generatedReferences, brief }) => {
+        const ids = ["white_complete", "selling_form", "selling_material", "scene_core_environment", "scene_human_interaction", "decision_scale_fit", "decision_value_summary"];
+        const creativePlan = {
+          product_creative_brief: {
+            category: "测试商品", form: "完整", colors: ["原色"], material_cues: ["原材质"], identity_cues: ["轮廓"],
+            visible_selling_point_candidates: ["结构", "纹理"], natural_support_and_use: ["自然承载"], human_interactions: ["自然使用"], commercial_direction: "商业摄影",
+          },
+          visual_bible: { commercial_style: "商业摄影", palette: ["原色"], lighting: "柔光", prop_language: "克制" },
+          pages: ids.map((template_id, index) => ({
+            template_id, page_job: `任务${index}`, scene: `场景${index}`, product_pose: `姿态${index}`, camera: `机位${index}`,
+            human_relation: `人物${index}`, composition: `构图${index}`, text_safe_zone: `留白${index}`, difference_from_peer: `差异${index}`,
+          })),
+        };
+        return { blueprint: compileSetBlueprint({ runId, generatedReferences, brief, creativePlan }), reused: false, model_calls: 1, usage: null };
+      },
+      writeCopyPlan: async ({ brief, blueprint }) => compileReferenceFirstCopyPlan({ brief, blueprint }),
+      detectLayout: async () => ({ regions: { schema_version: "1.0", pages: Array.from({ length: 7 }, (_, index) => ({ slot_id: String(index + 1).padStart(2, "0") })) }, reused: false, model_calls: 1, usage: null }),
+      postprocessDesign: async ({ outputDirectory }) => {
+        await mkdir(outputDirectory, { recursive: true });
+        const files = await Promise.all(setImages.map(async (_, index) => {
+          const file = join(outputDirectory, `final-${index + 1}.png`);
+          await writeFile(file, "final-image");
+          return { file, slot_id: String(index + 1).padStart(2, "0") };
+        }));
+        return { status: "complete", files, manifest_file: join(outputDirectory, "manifest.json") };
+      },
+    });
+    assert.equal(result.status, "complete");
+    assert.equal(result.summary.completed, 9);
+    assert.deepEqual(result.summary.cost_range_cny, { min: 1.98, max: 1.98 });
+    assert.equal(result.content_review_enabled, false);
+    assert.equal(result.validation_enabled, false);
+    assert.equal(result.original_reference_used_for_set, false);
+    assert.equal(calls.length, 2);
+    const referencePlan = JSON.parse(await readFile(paths.reference_asset_plan, "utf8"));
+    const setPlan = JSON.parse(await readFile(paths.prompt_plan, "utf8"));
+    assert.ok(referencePlan.images.every((image) => image.reference_images[0] === original));
+    assert.ok(setPlan.images.every((image) => JSON.stringify(image.reference_images) === JSON.stringify([threeView, settingBoard])));
+    assert.doesNotMatch(JSON.stringify(setPlan), new RegExp(original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"));
+  } finally {
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});
+
+test("reference-first pipeline stops before the seven-image stage when a reference asset is incomplete", async () => {
+  const paths = await createRun();
+  const original = join(paths.attachments_directory, "product.png");
+  const threeView = join(paths.reference_assets_directory, "three-view.png");
+  let calls = 0;
+  try {
+    await writeFile(original, "product");
+    const result = await runReferenceFirstPipeline({
+      paths,
+      originalReference: original,
+      productionBrief: { instruction: "生成淘宝中文七图", platform: "taobao" },
+      executeNode: async () => {
+        calls += 1;
+        await writeFile(threeView, "three-view");
+        return JSON.stringify({
+          status: "partial",
+          completed: 1,
+          local_images: [threeView],
+          cost_range_cny: { min: 0.22, max: 0.44 },
+        });
+      },
+    });
+    assert.equal(result.status, "partial");
+    assert.equal(result.summary.completed, 1);
+    assert.equal(result.summary.expected_total, 9);
+    assert.equal(result.next_action, "resume_same_run_after_reference_asset_technical_failure");
+    assert.equal(calls, 1);
+    await assert.rejects(readFile(paths.prompt_plan, "utf8"), /ENOENT/);
+  } finally {
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});
 
 test("delivery audit rejects files copied from another run", async () => {
   const paths = await createRun();
@@ -22,3 +137,272 @@ test("delivery audit rejects files copied from another run", async () => {
     await rm(paths.run_directory, { recursive: true, force: true });
   }
 });
+
+test("delivery audit writes a derived production outcome without changing executor state", async () => {
+  const paths = await createRun();
+  try {
+    const reference = join(paths.attachments_directory, "reference.png");
+    await writeFile(reference, await sharp({ create: { width: 640, height: 640, channels: 3, background: "#F5F2EC" } })
+      .composite([{ input: Buffer.from('<svg width="640" height="640" xmlns="http://www.w3.org/2000/svg"><rect x="150" y="170" width="340" height="310" rx="45" fill="#285B47"/></svg>') }])
+      .png().toBuffer());
+    const results = [];
+    const relations = ["isolated", "detail", "environmental", "detail", "human_proximity", "human_handheld", "stable_display"];
+    const axes = ["front", "macro", "three_quarter_left", "side", "three_quarter_right", "low", "top"];
+    const framings = ["full_product", "detail", "product_in_context", "full_product", "product_in_context", "human_action", "multi_panel"];
+    for (let index = 0; index < 7; index += 1) {
+      const slot = String(index + 1).padStart(2, "0");
+      const file = join(paths.images_directory, `${slot}.png`);
+      await writeFile(file, await sharp({ create: { width: 720, height: 720, channels: 3, background: index % 2 ? "#DDE7E0" : "#EEE4D6" } })
+        .composite([{ input: Buffer.from(`<svg width="720" height="720" xmlns="http://www.w3.org/2000/svg"><rect x="175" y="210" width="370" height="320" rx="45" fill="#285B47"/><text x="360" y="405" text-anchor="middle" font-size="72" fill="#FFF">${slot}</text></svg>`) }])
+        .png().toBuffer());
+      results.push({
+        slot_id: slot,
+        status: "complete",
+        local_images: [file],
+        quality_review: { accepted: true, same_product: true, violations: [], page_compliance: {
+          actual_camera_axis: axes[index], actual_framing_mode: framings[index], product_occupancy_pct: 45,
+          product_human_relation: relations[index], scene_family: `scene-${index}`, composition_family: `composition-${index}`,
+          page_job_compliant: true, visible_evidence: [], forbidden_reveals: [], violations: [],
+          product_region: { status: "verified", boxes: [{ x: 0.43, y: 0.18, width: 0.48, height: 0.67 }] },
+          face_regions: "unverifiable",
+          critical_product_regions: { status: "verified", boxes: [{ x: 0.5, y: 0.26, width: 0.28, height: 0.32 }] },
+          recommended_text_safe_regions: { status: "verified", boxes: [{ x: 0.03, y: 0.05, width: 0.34, height: 0.2 }] },
+        } },
+        cost_cny: 0.22,
+      });
+    }
+    const state = { run_id: paths.run_id, status: "complete", results };
+    await writeFile(paths.jimeng_result, JSON.stringify(state));
+    await writeFile(paths.vision_result, JSON.stringify({
+      reference: { source: reference },
+      product_understanding: {
+        category: "咖啡饮品",
+        product_name: { value: "袋装咖啡" },
+        visible_facts: ["深绿色袋装外形", "正面未见可识别文字"],
+        verified_visible_facts: ["深绿色袋装外形", "正面未见可识别文字"],
+      },
+    }));
+    await writeFile(paths.prompt_plan, JSON.stringify({
+      context: { platform: "taobao", market: "cn", target_language: "zh-CN", aspect_ratio: "1:1" },
+      images: Array.from({ length: 7 }, (_, index) => ({
+        id: String(index + 1).padStart(2, "0"), category: index === 0 ? "hero" : "scene",
+        decision_subject: `第${index + 1}页购买判断`, evidence_refs: [],
+        expected_page_compliance: { product_human_relation: relations[index] },
+      })),
+    }));
+    const original = await readFile(paths.jimeng_result, "utf8");
+    const result = await withDeliveryAudit(paths, { status: "complete", local_images: results.flatMap((item) => item.local_images) });
+    const after = await readFile(paths.jimeng_result, "utf8");
+    assert.equal(after, original);
+    assert.equal(result.production_outcome.status, "complete");
+    assert.equal(result.production_outcome.summary.completed, 7);
+    assert.equal(result.production_outcome.quality_status, "quality_passed");
+    assert.equal(result.designed_production.status, "complete", JSON.stringify(result.designed_production));
+    assert.equal(result.designed_production.files.length, 7);
+    assert.equal(result.designed_production.paid_external_calls, 0);
+    assert.equal(result.production_outcome_file, paths.production_outcome);
+  } finally {
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});
+
+test("explicit images bypass Claude attachment history", async () => {
+  let inspected = false;
+  const invocation = await resolveStartInvocation({
+    explicitImage: "D:\\product.png",
+    inspectInvocation: async () => { inspected = true; },
+  });
+  assert.equal(invocation, null);
+  assert.equal(inspected, false);
+});
+
+test("MCP preference makes one current VSCode Claude attachment override an explicit image", async () => {
+  let inspected = false;
+  const invocation = await resolveStartInvocation({
+    explicitImage: "D:\\wrong-history.png",
+    preferCurrentAttachment: true,
+    inspectInvocation: async () => {
+      inspected = true;
+      return { image_count: 1, prompt_id: "prompt-current", session_id: "session-current", invocation_mode: "current_user_prompt" };
+    },
+  });
+  assert.equal(inspected, true);
+  assert.equal(invocation.prompt_id, "prompt-current");
+  assert.equal(invocation.explicit_image_ignored, true);
+});
+
+test("MCP preference falls back to an explicit path only when the current request has no attachment", async () => {
+  const invocation = await resolveStartInvocation({
+    explicitImage: "D:\\user-path.png",
+    preferCurrentAttachment: true,
+    inspectInvocation: async () => ({ image_count: 0 }),
+  });
+  assert.equal(invocation, null);
+});
+
+test("MCP preference never lets an explicit path bypass multiple current attachments", async () => {
+  await assert.rejects(
+    resolveStartInvocation({
+      explicitImage: "D:\\one.png",
+      preferCurrentAttachment: true,
+      inspectInvocation: async () => ({ image_count: 2 }),
+    }),
+    /CURRENT_REQUEST_IMAGE_COUNT_INVALID/,
+  );
+});
+
+test("input provenance records current attachment precedence without exposing the ignored path", () => {
+  const provenance = buildInputProvenance({
+    invocation: { prompt_id: "p1", session_id: "s1", image_count: 1, invocation_mode: "current_user_prompt", explicit_image_ignored: true },
+    explicitImage: "D:\\secret\\wrong.png",
+    vision: { reference: { attachment: { sha256: "abc123" } } },
+  });
+  assert.equal(provenance.mode, "current_vscode_claude_attachment");
+  assert.equal(provenance.explicit_image_ignored, true);
+  assert.equal(provenance.sha256, "abc123");
+  assert.doesNotMatch(JSON.stringify(provenance), /wrong\.png|secret/);
+});
+
+test("vision arguments use the attachment bridge when an explicit path was overridden", () => {
+  const args = buildVisionArguments({
+    invocation: { prompt_id: "current" },
+    explicitImage: "D:\\wrong-history.png",
+    outputFile: "D:\\run\\vision-result.json",
+    attachmentCwd: "D:\\workspace",
+    sessionId: "session-current",
+  });
+  assert.ok(args.includes("--from-claude-attachment"));
+  assert.equal(args.includes("--image"), false);
+  assert.equal(args.includes("D:\\wrong-history.png"), false);
+});
+
+test("attachment lookup prefers the Claude project directory", () => {
+  assert.equal(
+    resolveAttachmentCwd({
+      env: {
+        CLAUDE_PROJECT_DIR: "D:\\Angular\\套图设计",
+        FMODE_IMAGE_SET_PROJECT_ROOT: "D:\\wrong",
+      },
+    }),
+    resolve("D:\\Angular\\套图设计"),
+  );
+});
+
+test("implicit attachments retain the unique-current-image gate", async () => {
+  const invocation = await resolveStartInvocation({
+    cwd: "D:\\Angular\\套图设计",
+    inspectInvocation: async (options) => ({
+      image_count: 1,
+      session_id: "session",
+      prompt_id: options.cwd,
+    }),
+  });
+  assert.equal(invocation.prompt_id, "D:\\Angular\\套图设计");
+});
+
+test("designed asset start completes and stops while visual base keeps planning", () => {
+  const vision = { status: "complete" };
+  const paths = { run_id: "run-test" };
+  const designed = buildStartResult({
+    vision,
+    paths,
+    outputLevel: "designed_asset",
+    designedAsset: { status: "complete" },
+  });
+  assert.equal(designed.status, "complete");
+  assert.equal(designed.next_action, "stop_designed_asset_complete");
+
+  const visualBase = buildStartResult({ vision, paths, outputLevel: "visual_base" });
+  assert.equal(visualBase.status, "ready_for_plan");
+  assert.equal(visualBase.next_action, "write_prompt_plan_then_execute_same_run");
+});
+
+test("validated production start waits for explicit paid confirmation", () => {
+  const result = buildStartResult({
+    vision: { status: "complete", external_calls: { paid_image_generation: 0 } },
+    paths: { run_id: "run-production", prompt_plan: "D:\\run\\prompt-plan.json" },
+    outputLevel: "visual_base",
+    productionPlan: { schema_version: "0.6", images: Array.from({ length: 7 }) },
+    planValidation: { valid: true, images: 7 },
+  });
+  assert.equal(result.status, "waiting_for_confirmation");
+  assert.equal(result.next_action, "review_plan_and_explicitly_authorize_execute");
+  assert.equal(result.production.estimated_cost_cny, 1.54);
+  assert.equal(result.production.paid_generation_authorized, false);
+  assert.equal(result.production.plan_file, "D:\\run\\prompt-plan.json");
+});
+
+test("preauthorized production start records an executable same-run action", () => {
+  const result = buildStartResult({
+    vision: { status: "complete" },
+    paths: { run_id: "run-production", prompt_plan: "D:\\run\\prompt-plan.json" },
+    outputLevel: "visual_base",
+    productionPlan: { schema_version: "0.6", images: Array.from({ length: 7 }) },
+    planValidation: { valid: true, images: 7 },
+    preflightRecord: { plan_sha256: "abc" },
+    executionAuthorization: {
+      authorized: true,
+      source: "current_user_prompt",
+      max_cost_cny: 1.54,
+    },
+  });
+  assert.equal(result.status, "waiting_for_confirmation");
+  assert.equal(result.production.paid_generation_authorized, true);
+  assert.equal(result.next_action, "execute_same_run_under_recorded_authorization");
+});
+
+test("preflight authorization requires the default seven-image cost cap", () => {
+  assert.throws(
+    () => normalizeExecutionAuthorization({ authorization: { mode: "preauthorized", source: "current_user_prompt", maxCost: 1.53 } }, "abc"),
+    /EXECUTION_AUTHORIZATION_COST_LIMIT_INSUFFICIENT/,
+  );
+  const authorization = normalizeExecutionAuthorization({ authorization: { mode: "preauthorized", source: "current_user_prompt", maxCost: 1.54 } }, "abc");
+  assert.equal(authorization.authorized, true);
+  assert.equal(authorization.plan_sha256, "abc");
+  assert.throws(
+    () => normalizeExecutionAuthorization({ authorization: { mode: "preauthorized", maxCost: 1.54 } }, "abc"),
+    /EXECUTION_AUTHORIZATION_SOURCE_INVALID/,
+  );
+});
+
+test("blocked executor summaries are returned before delivery audit", () => {
+  assert.equal(isExecutorBlocked({ status: "blocked", jimeng_called: false }), true);
+  assert.equal(isExecutorBlocked({ status: "complete" }), false);
+});
+
+test("manual plan edits after preflight are rejected before execution", async () => {
+  const paths = await createRun();
+  try {
+    await writeFile(paths.prompt_plan, "{\"status\":\"ready\"}");
+    const preflight = { run_id: paths.run_id, plan_sha256: "different" };
+    await writeFile(paths.preflight_record, JSON.stringify(preflight));
+    await assert.rejects(
+      assertPreflightPlanUnchanged(paths),
+      /PROMPT_PLAN_CHANGED_AFTER_PREFLIGHT/,
+    );
+  } finally {
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});
+
+test("invalid production plan fails closed before paid generation", () => {
+  const result = buildStartResult({
+    vision: { status: "complete" },
+    paths: { run_id: "run-production" },
+    outputLevel: "visual_base",
+    productionPlan: { schema_version: "0.6", images: [] },
+    planValidation: { valid: false, errors: ["ready plan must contain exactly seven images"] },
+  });
+  assert.equal(result.status, "blocked");
+  assert.equal(result.next_action, "stop_and_report_plan_validation_error");
+  assert.equal(result.production.paid_generation_authorized, false);
+});
+
+test("designed asset product evidence errors preserve their code and request a new attachment", () => {
+  const error = new Error("当前参考图未显示可识别商品");
+  error.code = "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE";
+  const result = buildDesignedAssetFailure(error);
+  assert.equal(result.status, "blocked");
+  assert.equal(result.error.code, "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE");
+  assert.equal(result.next_action, "attach_one_clear_product_image_and_start_again");
+});

+ 95 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/platform-design-profile-registry.mjs

@@ -0,0 +1,95 @@
+const freeze = (value) => Object.freeze(value);
+
+const profiles = {
+  taobao: {
+    id: "taobao", version: "1.0.0", label: "淘宝",
+    rhythm: "由清晰商品识别进入细节说服,再进入生活方式场景,最后完成选择收束",
+    subject_scale: "白底图主体约占画面68%;卖点图55%~72%;场景图35%~58%;决策图48%~68%",
+    composition: "干净但不空泛,允许局部非对称构图,通过模块化留白承接后置中文信息",
+    scene_human_weight: "生活方式与真实使用关系并重,人物只为商品使用状态服务",
+    color_lighting: "色彩准确、商业摄影光线柔和,场景色与商品主色协调",
+    copy_density: "中等信息密度,标题、短副标题和单一辅助信息形成三级以内层级",
+    safe_zone_bias: "优先侧边或角落模块化留白,避免七页统一顶部条幅",
+    role_weights: { selling_point: 1, usage_scene: 1, purchase_decision: 1 },
+  },
+  jd: {
+    id: "jd", version: "1.0.0", label: "京东",
+    rhythm: "先建立产品完整性与专业感,再用结构和细节证明,场景用于解释使用,决策页保持理性收束",
+    subject_scale: "商品整体占比偏高;卖点和决策页优先完整轮廓与清晰结构",
+    composition: "秩序明确、几何稳定、背景克制,留白与信息区边界清晰",
+    scene_human_weight: "产品和功能关系优先,人物互动适量且动作明确",
+    color_lighting: "中性准确的色温、较清晰的轮廓光和结构层次,减少过度氛围化滤镜",
+    copy_density: "中高但结构化的信息密度,适合短标题、证据短句和参数占位区",
+    safe_zone_bias: "优先规则矩形信息区、侧栏或底栏",
+    role_weights: { selling_point: 1.25, usage_scene: 0.8, purchase_decision: 1.15 },
+  },
+  douyin_ecommerce: {
+    id: "douyin_ecommerce", version: "1.0.0", label: "抖音电商",
+    rhythm: "快速建立视觉钩子,用明显镜头变化和真实动作维持节奏,再以简洁决策画面收束",
+    subject_scale: "主体占比变化更大胆,但商品始终清晰且不可被人物或道具抢占",
+    composition: "动势更强、允许近距离透视与前景层次,各页镜头变化明显",
+    scene_human_weight: "真实人物动作和即时使用结果权重较高,避免静态陈列连续重复",
+    color_lighting: "对比更鲜明、光影更有节奏,但不得改变商品真实颜色和材质",
+    copy_density: "低到中等信息密度,优先短标题和单一视觉结论",
+    safe_zone_bias: "预留大字短句区域,优先上侧或左右单一区域,不做复杂表格",
+    role_weights: { selling_point: 0.9, usage_scene: 1.35, purchase_decision: 0.9 },
+  },
+  pinduoduo: {
+    id: "pinduoduo", version: "1.0.0", label: "拼多多",
+    rhythm: "商品识别直接、卖点结论明确、场景易理解、决策信息集中",
+    subject_scale: "商品占比高,减少无关环境面积,场景中也保持产品主导",
+    composition: "视觉关系直接,层级数量受控,避免过度艺术化造成卖点不清",
+    scene_human_weight: "人物和道具用于直观解释尺寸或使用,不承担独立叙事",
+    color_lighting: "清晰明亮、对比明确,商品颜色准确,背景不过度复杂",
+    copy_density: "中等偏高但文案短促,优先一页一个结论",
+    safe_zone_bias: "优先高对比信息卡片或底栏,保证在复杂背景上仍可读",
+    role_weights: { selling_point: 1.15, usage_scene: 0.85, purchase_decision: 1.25 },
+  },
+  general_ecommerce: {
+    id: "general_ecommerce", version: "1.0.0", label: "通用电商",
+    rhythm: "商品识别、卖点证明、真实使用和购买判断保持均衡",
+    subject_scale: "主体占比随页面任务变化,白底和卖点偏高,场景适度降低",
+    composition: "商业摄影构图清晰,每页只有一个视觉中心,并预留可验证文字区域",
+    scene_human_weight: "按商品自然使用方式决定人物关系",
+    color_lighting: "准确中性的商品色彩与协调一致的商业光线",
+    copy_density: "中等信息密度",
+    safe_zone_bias: "按实际低细节区域选择侧边、角落或独立信息面板",
+    role_weights: { selling_point: 1, usage_scene: 1, purchase_decision: 1 },
+  },
+};
+
+const aliases = new Map([
+  ["taobao", "taobao"], ["淘宝", "taobao"], ["tmall", "taobao"], ["天猫", "taobao"],
+  ["jd", "jd"], ["jingdong", "jd"], ["京东", "jd"],
+  ["douyin", "douyin_ecommerce"], ["douyin_ecommerce", "douyin_ecommerce"], ["抖音", "douyin_ecommerce"], ["抖音电商", "douyin_ecommerce"],
+  ["pinduoduo", "pinduoduo"], ["pdd", "pinduoduo"], ["拼多多", "pinduoduo"],
+  ["general_ecommerce", "general_ecommerce"], ["general", "general_ecommerce"], ["通用电商", "general_ecommerce"],
+]);
+
+for (const [key, value] of Object.entries(profiles)) profiles[key] = freeze({ ...value, role_weights: freeze({ ...value.role_weights }) });
+
+export const PLATFORM_DESIGN_PROFILES = freeze({ ...profiles });
+
+export function resolvePlatformDesignProfile(requestedPlatform = "general_ecommerce") {
+  const requested = String(requestedPlatform || "general_ecommerce").trim();
+  const id = aliases.get(requested.toLowerCase()) || aliases.get(requested) || "general_ecommerce";
+  return {
+    requested_platform: requested,
+    resolved_profile: id,
+    fallback_used: id === "general_ecommerce" && !new Set(["general", "general_ecommerce", "通用电商"]).has(requested.toLowerCase()),
+    profile: PLATFORM_DESIGN_PROFILES[id],
+  };
+}
+
+export function platformProfilePrompt(profile) {
+  if (!profile?.id) throw new Error("PLATFORM_DESIGN_PROFILE_REQUIRED");
+  return [
+    `目标平台设计策略:${profile.label}(${profile.id})。`,
+    `整套节奏:${profile.rhythm}。`,
+    `商品主体比例:${profile.subject_scale}。`,
+    `构图策略:${profile.composition}。`,
+    `场景与人物:${profile.scene_human_weight}。`,
+    `色彩与光线:${profile.color_lighting}。`,
+    `后置文字密度:${profile.copy_density};留白策略:${profile.safe_zone_bias}。`,
+  ].join("\n");
+}

+ 28 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/platform-design-profile-registry.test.mjs

@@ -0,0 +1,28 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { PLATFORM_DESIGN_PROFILES, platformProfilePrompt, resolvePlatformDesignProfile } from "./platform-design-profile-registry.mjs";
+
+test("platform profiles resolve aliases and preserve an explicit fallback", () => {
+  assert.equal(resolvePlatformDesignProfile("淘宝").resolved_profile, "taobao");
+  assert.equal(resolvePlatformDesignProfile("JD").resolved_profile, "jd");
+  assert.equal(resolvePlatformDesignProfile("抖音电商").resolved_profile, "douyin_ecommerce");
+  assert.equal(resolvePlatformDesignProfile("pdd").resolved_profile, "pinduoduo");
+  const fallback = resolvePlatformDesignProfile("unknown-marketplace");
+  assert.equal(fallback.resolved_profile, "general_ecommerce");
+  assert.equal(fallback.fallback_used, true);
+  assert.equal(fallback.requested_platform, "unknown-marketplace");
+});
+
+test("platform profiles change actual design instructions rather than only the label", () => {
+  const prompts = Object.values(PLATFORM_DESIGN_PROFILES).map(platformProfilePrompt);
+  assert.equal(new Set(prompts).size, 5);
+  for (const prompt of prompts) {
+    assert.match(prompt, /整套节奏/);
+    assert.match(prompt, /商品主体比例/);
+    assert.match(prompt, /构图策略/);
+    assert.match(prompt, /后置文字密度/);
+  }
+  assert.notEqual(PLATFORM_DESIGN_PROFILES.taobao.subject_scale, PLATFORM_DESIGN_PROFILES.jd.subject_scale);
+  assert.notEqual(PLATFORM_DESIGN_PROFILES.jd.scene_human_weight, PLATFORM_DESIGN_PROFILES.douyin_ecommerce.scene_human_weight);
+  assert.notEqual(PLATFORM_DESIGN_PROFILES.douyin_ecommerce.safe_zone_bias, PLATFORM_DESIGN_PROFILES.pinduoduo.safe_zone_bias);
+});

+ 16 - 2
claude-code/fmode-image-set/mcp/src/features/image-set/product-vision-client.mjs

@@ -319,19 +319,27 @@ if (attachment) {
     prompt_id: attachment.prompt_id,
     session_id: attachment.session_id,
     captured_at: attachment.captured_at,
+    invocation_mode: attachment.invocation_mode,
   };
 }
-const config = await loadVisionConfig();
 if (args.includes("--dry-run")) {
   await writeResult({
     schema_version: "0.3",
     status: "dry-run",
     vision_called: false,
+    fact_audit_called: false,
+    external_calls: {
+      vision_understanding: 0,
+      fact_audit: 0,
+      asset_upload: reference.asset ? 1 : 0,
+      paid_image_generation: 0,
+    },
     reference,
-    provider: { name: config.provider, model: config.model },
+    provider: { name: "not_called", model: null },
   });
   throw new GracefulStop();
 }
+const config = await loadVisionConfig();
 if (!config.apiKey) await fail("VISION_PROVIDER_NOT_CONFIGURED", "Qwen vision API key is not configured on this machine");
 if (!reference.public_url) await fail("REFERENCE_UPLOAD_FAILED", "The product image could not be converted to a public URL");
 
@@ -376,6 +384,12 @@ try {
     status: "complete",
     vision_called: true,
     fact_audit_called: true,
+    external_calls: {
+      vision_understanding: 1,
+      fact_audit: 1,
+      asset_upload: reference.asset ? 1 : 0,
+      paid_image_generation: 0,
+    },
     reference,
     provider: { name: config.provider, model: config.model },
     product_understanding: productUnderstanding,

+ 138 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-design-bridge.mjs

@@ -0,0 +1,138 @@
+import { createHash } from "node:crypto";
+import { mkdir, writeFile } from "node:fs/promises";
+import { isAbsolute, relative, resolve, join } from "node:path";
+import sharp from "sharp";
+import { buildProductLayer } from "../designed-asset/product-layer-builder.mjs";
+import { compileDeliveryProfile } from "../designed-asset/delivery-profile-compiler.mjs";
+import { compileSevenPageDesignIr } from "../designed-asset/seven-page-design-ir-compiler.mjs";
+import { compositeAdaptiveOverlay } from "../designed-asset/adaptive-overlay-compositor.mjs";
+
+const SIZE = 1200;
+const digest = (buffer) => createHash("sha256").update(buffer).digest("hex");
+const asArray = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
+
+const names = [
+  "01-主视觉成品.png", "02-卖点一成品.png", "03-场景成品.png", "04-卖点二成品.png",
+  "05-尺度判断成品.png", "06-互动情境成品.png", "07-购买决策成品.png",
+];
+
+function assertBaseImageInsideRun(file, runDirectory) {
+  const absolute = resolve(file);
+  const root = resolve(runDirectory);
+  const pathFromRoot = relative(root, absolute);
+  if (!pathFromRoot || pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) {
+    throw new Error("PRODUCTION_DESIGN_BASE_IMAGE_OUTSIDE_RUN");
+  }
+  return absolute;
+}
+
+function completeBasePages(runState = {}) {
+  const pages = asArray(runState.results);
+  if (pages.length !== 7) return { complete: false, reason: "base_image_count_not_seven", pages };
+  const selected = pages.map((entry) => {
+    const accepted = entry.status === "complete" && entry.quality_review?.accepted === true;
+    return { ...entry, accepted, file: asArray(entry.local_images)[0] };
+  });
+  if (selected.some((entry) => !entry.accepted || !entry.file)) {
+    return { complete: false, reason: "base_images_incomplete_or_identity_rejected", pages: selected };
+  }
+  return { complete: true, pages: selected };
+}
+
+export function bridgeStatusForRun(runState = {}, setQualityReview = null) {
+  const result = completeBasePages(runState);
+  return result.complete && setQualityReview?.status === "quality_passed" ? "ready" : "blocked";
+}
+
+export async function runProductionDesignBridge({
+  runState = {},
+  runDirectory,
+  outputDirectory,
+  reference,
+  productUnderstanding = {},
+  promptPlan = {},
+  setQualityReview = null,
+} = {}) {
+  if (!runDirectory || !outputDirectory) throw new Error("PRODUCTION_DESIGN_OUTPUT_DIRECTORY_REQUIRED");
+  const base = completeBasePages(runState);
+  if (!base.complete) {
+    return {
+      schema_version: "0.1",
+      status: "blocked",
+      reason: base.reason,
+      required_base_pages: 7,
+      paid_external_calls: 0,
+      files: [],
+    };
+  }
+  if (setQualityReview?.status !== "quality_passed") {
+    return {
+      schema_version: "0.2",
+      status: "blocked",
+      reason: "set_quality_gate_not_passed",
+      quality_status: setQualityReview?.status || "quality_unverified",
+      required_base_pages: 7,
+      paid_external_calls: 0,
+      files: [],
+    };
+  }
+  if (!reference) throw new Error("PRODUCTION_DESIGN_REFERENCE_REQUIRED");
+  const productLayer = await buildProductLayer(reference);
+  const designIr = await compileSevenPageDesignIr({
+    product: productUnderstanding,
+    productLayer,
+    promptPlan,
+    setQualityReview,
+  });
+  const deliveryProfile = compileDeliveryProfile(designIr);
+  const directory = resolve(outputDirectory);
+  await mkdir(directory, { recursive: true });
+  const images = asArray(promptPlan.images);
+  const files = [];
+  for (let index = 0; index < base.pages.length; index += 1) {
+    const entry = base.pages[index];
+    const source = assertBaseImageInsideRun(entry.file, runDirectory);
+    const page = designIr.page_plans[index];
+    const composed = await compositeAdaptiveOverlay({
+      source,
+      page,
+      tokens: designIr.creative_direction.tokens,
+      size: SIZE,
+    });
+    const output = composed.buffer;
+    const file = join(directory, names[index]);
+    await writeFile(file, output);
+    const metadata = await sharp(output).metadata();
+    files.push({
+      file, slot_id: entry.slot_id || String(index + 1).padStart(2, "0"),
+      page_role: images[index]?.category || ["hero", "selling_point", "scene", "selling_point", "decision_support", "scene", "decision_support"][index],
+      source_base_image: source,
+      copy: page.copy,
+      layout_recipe: page.layout_recipe,
+      overlay_decision: composed.decision,
+      width: metadata.width, height: metadata.height, sha256: digest(output),
+      design_ir_schema_version: designIr.schema_version,
+      paid_external_calls: 0,
+    });
+  }
+  const manifest = {
+    schema_version: "0.2",
+    status: "complete",
+    output_level: "visual_base_design_bridge",
+    source_output_level: "visual_base",
+    base_page_count: base.pages.length,
+    design_ir_schema_version: designIr.schema_version,
+    design_ir: designIr,
+    delivery_profile: deliveryProfile,
+    files,
+    paid_external_calls: 0,
+    generation_status: "generation_complete",
+    quality_status: "quality_passed",
+    design_status: "design_complete",
+    release_status: "review_required",
+    limitations: ["桥接仅在完整且身份审核通过的七张 visual_base 上运行", "底图像素保留,设计叠加不等同于新的摄影机位或真人实拍"],
+  };
+  const manifestFile = join(directory, "designed-production-manifest.json");
+  await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
+  return { ...manifest, manifest_file: manifestFile };
+}

+ 118 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-design-bridge.test.mjs

@@ -0,0 +1,118 @@
+import assert from "node:assert/strict";
+import { mkdir, rm, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import test from "node:test";
+import sharp from "sharp";
+import { createRun } from "../../core/run-paths.mjs";
+import { bridgeStatusForRun, runProductionDesignBridge } from "./production-design-bridge.mjs";
+
+async function productReference(file) {
+  const buffer = await sharp({
+    create: { width: 720, height: 720, channels: 3, background: "#F6F4EF" },
+  }).composite([{
+    input: Buffer.from('<svg width="720" height="720" xmlns="http://www.w3.org/2000/svg"><rect x="170" y="190" width="380" height="350" rx="50" fill="#27624A"/><circle cx="360" cy="365" r="72" fill="#D9A441"/></svg>'),
+  }]).png().toBuffer();
+  await writeFile(file, buffer);
+}
+
+async function baseImage(file, index) {
+  const buffer = await sharp({ create: { width: 900, height: 900, channels: 3, background: index % 2 ? "#D9E3DC" : "#EDE4D4" } })
+    .composite([{ input: Buffer.from(`<svg width="900" height="900" xmlns="http://www.w3.org/2000/svg"><rect x="230" y="260" width="440" height="380" rx="55" fill="#27624A"/><text x="450" y="490" text-anchor="middle" font-size="88" fill="#FFF">${index}</text></svg>`) }])
+    .png().toBuffer();
+  await writeFile(file, buffer);
+}
+
+function understanding() {
+  return {
+    category: "咖啡饮品",
+    product_name: { value: "袋装咖啡" },
+    visible_facts: ["正面可见深绿色袋装外形", "中央可见圆形金色图案"],
+    verified_visible_facts: ["正面可见深绿色袋装外形", "中央可见圆形金色图案"],
+    visible_text: [],
+    visible_specs: {},
+  };
+}
+
+function passedSetQuality() {
+  const relations = ["isolated", "detail", "stable_display", "detail", "human_proximity", "human_handheld", "stable_display"];
+  return {
+    status: "quality_passed", accepted: true,
+    pages: relations.map((relation, index) => ({
+      status: "quality_passed",
+      page_compliance: {
+        actual_camera_axis: ["front", "macro", "three_quarter_left", "front", "three_quarter_right", "low", "top"][index],
+        actual_framing_mode: ["full_product", "detail", "product_in_context", "full_product", "product_in_context", "human_action", "multi_panel"][index],
+        product_human_relation: relation,
+        scene_family: `scene-${index + 1}`, composition_family: `composition-${index + 1}`, page_job_compliant: true,
+        visible_evidence: [], forbidden_reveals: [], violations: [],
+        product_region: { status: "verified", boxes: [{ x: 0.42, y: 0.18, width: 0.5, height: 0.68 }] },
+        face_regions: { status: "unverifiable", boxes: [] },
+        critical_product_regions: { status: "verified", boxes: [{ x: 0.5, y: 0.25, width: 0.28, height: 0.35 }] },
+        recommended_text_safe_regions: { status: "verified", boxes: [{ x: 0.03, y: 0.05, width: 0.34, height: 0.22 }] },
+      },
+    })),
+  };
+}
+
+test("complete seven-page visual_base deterministically bridges into seven Design IR deliverables", async () => {
+  const paths = await createRun();
+  try {
+    const reference = join(paths.attachments_directory, "reference.png");
+    await productReference(reference);
+    const results = [];
+    for (let index = 1; index <= 7; index += 1) {
+      const slot = String(index).padStart(2, "0");
+      const file = join(paths.images_directory, `${slot}.png`);
+      await baseImage(file, index);
+      results.push({ slot_id: slot, status: "complete", local_images: [file], quality_review: { accepted: true, same_product: true } });
+    }
+    const runState = { run_id: paths.run_id, results };
+    const setQualityReview = passedSetQuality();
+    const outputDirectory = join(paths.run_directory, "designed-production");
+    const result = await runProductionDesignBridge({
+      runState,
+      runDirectory: paths.run_directory,
+      outputDirectory,
+      reference,
+      productUnderstanding: understanding(),
+      promptPlan: { context: { platform: "taobao", market: "cn", target_language: "zh-CN", aspect_ratio: "1:1" }, images: Array.from({ length: 7 }, (_, index) => ({ id: String(index + 1).padStart(2, "0"), category: index === 0 ? "hero" : "scene", decision_subject: `第${index + 1}页购买判断` })) },
+      setQualityReview,
+    });
+    assert.equal(bridgeStatusForRun(runState, setQualityReview), "ready");
+    assert.equal(result.status, "complete");
+    assert.equal(result.files.length, 7);
+    assert.equal(result.paid_external_calls, 0);
+    assert.equal(result.design_ir.schema_version, "2.0");
+    assert.equal(result.design_ir.page_plans.length, 7);
+    assert.equal(new Set(result.design_ir.page_plans.map((page) => page.layout_recipe)).size, 7);
+    assert.equal(result.delivery_profile.platform, "taobao");
+    assert.equal(result.delivery_profile.market, "cn");
+    assert.ok(result.files.every((file) => file.width === 1200 && file.height === 1200));
+    assert.ok(result.files.every((file) => file.file.startsWith(outputDirectory)));
+  } finally {
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});
+
+test("partial or identity-rejected visual_base blocks the design bridge without output", async () => {
+  const paths = await createRun();
+  try {
+    await mkdir(join(paths.run_directory, "designed-production"), { recursive: true });
+    const runState = {
+      results: Array.from({ length: 7 }, (_, index) => ({
+        slot_id: String(index + 1).padStart(2, "0"),
+        status: index === 6 ? "quality_rejected" : "complete",
+        local_images: [join(paths.images_directory, `${index + 1}.png`)],
+        quality_review: { accepted: index !== 6 },
+      })),
+    };
+    const result = await runProductionDesignBridge({ runState, runDirectory: paths.run_directory, outputDirectory: join(paths.run_directory, "designed-production") });
+    assert.equal(bridgeStatusForRun(runState), "blocked");
+    assert.equal(result.status, "blocked");
+    assert.equal(result.reason, "base_images_incomplete_or_identity_rejected");
+    assert.deepEqual(result.files, []);
+    assert.equal(result.paid_external_calls, 0);
+  } finally {
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});

+ 127 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-outcome-compiler.mjs

@@ -0,0 +1,127 @@
+const asArray = (value) => Array.isArray(value) ? value : [];
+
+function repairAction(entry) {
+  if (entry.status === "submission_unknown") return "manual_submission_resolution_do_not_resubmit";
+  if (entry.status === "quality_rejected") return "manual_review_then_explicit_paid_regeneration";
+  if (entry.status === "complete") return "";
+  return "resume_same_run";
+}
+
+export function compileProductionOutcome({
+  runState = {},
+  deliveryIntegrity = null,
+  executionAuthorization = null,
+  setQualityReview = null,
+} = {}) {
+  const results = asArray(runState.results);
+  const pages = results.map((entry, index) => ({
+    slot_id: entry.slot_id,
+    status: entry.status,
+    local_images: asArray(entry.local_images),
+    identity_review: entry.quality_review
+      ? {
+        accepted: entry.quality_review.accepted === true,
+        same_product: entry.quality_review.same_product === true
+          || entry.quality_review.reviews?.[0]?.same_product === true,
+        violations: asArray(entry.quality_review.violations).length
+          ? asArray(entry.quality_review.violations)
+          : asArray(entry.quality_review.reviews?.[0]?.critical_violations),
+      }
+      : { accepted: false, same_product: false, violations: ["identity review unavailable"] },
+    page_compliance: setQualityReview?.pages?.[index]?.page_compliance
+      || entry.quality_review?.page_compliance
+      || entry.quality_review?.reviews?.[0]?.page_compliance
+      || null,
+    page_quality_status: setQualityReview?.pages?.[index]?.status || "quality_unverified",
+    cost_cny: Number(entry.cost_cny || 0),
+  }));
+  const completed = pages.filter((page) => page.status === "complete").length;
+  const identityAccepted = pages.filter((page) => page.status === "complete" && page.identity_review.accepted).length;
+  const unknown = pages.filter((page) => page.status === "submission_unknown").map((page) => page.slot_id);
+  const rejected = pages.filter((page) => page.status === "quality_rejected").map((page) => page.slot_id);
+  const resumable = pages
+    .filter((page) => !new Set(["complete", "submission_unknown", "quality_rejected"]).has(page.status))
+    .map((page) => page.slot_id);
+  const deliveryVerified = deliveryIntegrity?.status === "verified";
+  const complete = completed === 7 && identityAccepted === 7 && deliveryVerified;
+  const status = deliveryIntegrity && !deliveryVerified ? "blocked" : complete ? "complete" : "partial";
+  const qualityStatus = !complete
+    ? "quality_not_ready"
+    : setQualityReview?.status || "quality_unverified";
+  const releaseStatus = !complete
+    ? "development_only"
+    : qualityStatus === "quality_passed"
+      ? "review_required"
+      : qualityStatus === "quality_blocked"
+        ? "quality_blocked"
+        : "quality_review_required";
+  const actions = results.flatMap((entry) => {
+    const action = repairAction(entry);
+    return action ? [{
+      slot_id: entry.slot_id,
+      issue: entry.status,
+      action,
+      paid_action_authorized: false,
+    }] : [];
+  });
+
+  let nextAction = "inspect_partial_result";
+  if (complete && qualityStatus === "quality_passed") nextAction = "human_review_then_deliver";
+  else if (complete && qualityStatus === "quality_blocked") nextAction = "inspect_quality_failures_no_automatic_regeneration";
+  else if (complete) nextAction = "complete_output_level_quality_review_before_design";
+  else if (!deliveryVerified) nextAction = "stop_and_resolve_delivery_integrity";
+  else if (unknown.length) nextAction = "resolve_unknown_submission_without_resubmitting";
+  else if (resumable.length) nextAction = "resume_same_run";
+  else if (rejected.length) nextAction = "manual_review_then_request_explicit_regeneration_authorization";
+
+  return {
+    schema_version: "1.0",
+    run_id: runState.run_id || "",
+    status,
+    generation_status: complete ? "generation_complete" : status === "partial" ? "generation_partial" : "generation_blocked",
+    quality_status: qualityStatus,
+    pages,
+    summary: {
+      expected: 7,
+      completed,
+      identity_accepted: identityAccepted,
+      unknown_slots: unknown,
+      rejected_slots: rejected,
+      resumable_slots: resumable,
+      delivery_integrity: deliveryIntegrity?.status || "unavailable",
+      confirmed_cost_cny: Number(results.reduce((total, entry) => total + Number(entry.cost_cny || 0), 0).toFixed(2)),
+    },
+    quality_scope: {
+      product_identity_reviewed: true,
+      full_aesthetic_reviewed: false,
+      set_visual_consistency_reviewed: Boolean(setQualityReview),
+      output_page_compliance_reviewed: setQualityReview?.pages?.length === 7,
+      set_diversity_reviewed: Boolean(setQualityReview),
+      human_approved: false,
+    },
+    set_quality_review: setQualityReview,
+    release_status: releaseStatus,
+    repair_plan: {
+      paid_action_authorized: false,
+      automatic_external_calls: false,
+      actions,
+    },
+    authorization: executionAuthorization
+      ? {
+        recorded: true,
+        paid_generation_authorized: executionAuthorization.authorized === true,
+        source: executionAuthorization.source || null,
+        max_cost_cny: Number(executionAuthorization.max_cost_cny || 0),
+        plan_sha256: executionAuthorization.plan_sha256 || null,
+      }
+      : {
+        recorded: false,
+        paid_generation_authorized: null,
+        source: null,
+        max_cost_cny: null,
+        plan_sha256: null,
+      },
+    delivery_integrity: deliveryIntegrity,
+    next_action: nextAction,
+  };
+}

+ 83 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-outcome-compiler.test.mjs

@@ -0,0 +1,83 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { compileProductionOutcome } from "./production-outcome-compiler.mjs";
+
+test("complete seven-page state produces a delivery-ready review outcome", () => {
+  const results = Array.from({ length: 7 }, (_, index) => ({
+    slot_id: String(index + 1).padStart(2, "0"),
+    status: "complete",
+    local_images: [`D:\\run\\${index + 1}.png`],
+    quality_review: { accepted: true, same_product: true, violations: [] },
+    cost_cny: 0.22,
+  }));
+  const outcome = compileProductionOutcome({
+    runState: { run_id: "run-complete", status: "complete", results },
+    deliveryIntegrity: { status: "verified", untracked_files: [], missing_files: [] },
+    setQualityReview: { status: "quality_passed", accepted: true, pages: Array.from({ length: 7 }, () => ({ status: "quality_passed", page_compliance: {} })) },
+  });
+  assert.equal(outcome.status, "complete");
+  assert.equal(outcome.summary.completed, 7);
+  assert.equal(outcome.summary.identity_accepted, 7);
+  assert.equal(outcome.release_status, "review_required");
+  assert.equal(outcome.generation_status, "generation_complete");
+  assert.equal(outcome.quality_status, "quality_passed");
+  assert.equal(outcome.repair_plan.actions.length, 0);
+  assert.equal(outcome.next_action, "human_review_then_deliver");
+});
+
+test("complete legacy state without output-level evidence remains quality unverified", () => {
+  const results = Array.from({ length: 7 }, (_, index) => ({
+    slot_id: String(index + 1).padStart(2, "0"), status: "complete",
+    local_images: [`D:\\run\\${index + 1}.png`], quality_review: { accepted: true, same_product: true },
+  }));
+  const outcome = compileProductionOutcome({ runState: { results }, deliveryIntegrity: { status: "verified" } });
+  assert.equal(outcome.status, "complete");
+  assert.equal(outcome.quality_status, "quality_unverified");
+  assert.equal(outcome.release_status, "quality_review_required");
+});
+
+test("unknown, rejected and resumable slots get distinct non-paid decisions", () => {
+  const outcome = compileProductionOutcome({
+    runState: {
+      run_id: "run-partial",
+      status: "partial",
+      results: [
+        { slot_id: "01", status: "complete", local_images: ["D:\\run\\1.png"], quality_review: { accepted: true } },
+        { slot_id: "02", status: "submission_unknown", local_images: [] },
+        { slot_id: "03", status: "quality_rejected", local_images: ["D:\\run\\3.png"], quality_review: { accepted: false } },
+        { slot_id: "04", status: "poll_timeout", local_images: [] },
+      ],
+    },
+    deliveryIntegrity: { status: "verified", untracked_files: [], missing_files: [] },
+  });
+  assert.equal(outcome.status, "partial");
+  assert.deepEqual(outcome.repair_plan.actions.map((item) => item.action), [
+    "manual_submission_resolution_do_not_resubmit",
+    "manual_review_then_explicit_paid_regeneration",
+    "resume_same_run",
+  ]);
+  assert.ok(outcome.repair_plan.actions.every((item) => item.paid_action_authorized === false));
+  assert.equal(outcome.release_status, "development_only");
+});
+
+test("outcome reads identity acceptance from the provider review list", () => {
+  const outcome = compileProductionOutcome({
+    runState: {
+      run_id: "run-nested-review",
+      status: "partial",
+      results: [{
+        slot_id: "01",
+        status: "complete",
+        local_images: ["D:\\run\\01.png"],
+        quality_review: {
+          accepted: true,
+          reviews: [{ same_product: true, critical_violations: [] }],
+        },
+        cost_cny: 0.22,
+      }],
+    },
+    deliveryIntegrity: { status: "verified", untracked_files: [], missing_files: [] },
+  });
+  assert.equal(outcome.pages[0].identity_review.accepted, true);
+  assert.equal(outcome.pages[0].identity_review.same_product, true);
+});

+ 667 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-plan-compiler.mjs

@@ -0,0 +1,667 @@
+import { compileProductLock } from "../../providers/commerce-vision-provider.mjs";
+import {
+  compileSceneAffordancePolicy,
+  interactionProfileFromPolicy,
+  sceneCandidatesForPolicy,
+  sceneGuidanceForPolicy,
+} from "./scene-affordance-policy.mjs";
+import { enforceViewStrategyCompatibility } from "./view-strategy-compatibility.mjs";
+import { compileSetDiversityPolicy, contentSignatureForPage } from "./set-diversity-policy.mjs";
+
+const asArray = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const unique = (values) => [...new Set(asArray(values).map((value) => text(value)).filter(Boolean))];
+const concise = (value, maximum) => {
+  const characters = [...text(value)];
+  return characters.length <= maximum ? characters.join("") : `${characters.slice(0, maximum - 1).join("")}…`;
+};
+
+function dimensionsForAspectRatio(value) {
+  const ratio = text(value, "1:1");
+  const match = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(ratio);
+  if (!match) throw new Error("PRODUCTION_PLAN_ASPECT_RATIO_INVALID");
+  const horizontal = Number(match[1]);
+  const vertical = Number(match[2]);
+  if (!(horizontal > 0 && vertical > 0) || horizontal / vertical > 4 || vertical / horizontal > 4) {
+    throw new Error("PRODUCTION_PLAN_ASPECT_RATIO_INVALID");
+  }
+  if (horizontal === vertical) return { width: 2048, height: 2048 };
+  const align8 = (number) => Math.max(512, Math.round(number / 8) * 8);
+  return horizontal > vertical
+    ? { width: 2048, height: align8(2048 * vertical / horizontal) }
+    : { width: align8(2048 * horizontal / vertical), height: 2048 };
+}
+
+function normalizeIdentityManifest(source = {}, visibleFacts = []) {
+  const fallbackPreserve = [
+    ...asArray(source.must_preserve),
+    ...visibleFacts.slice(0, 5),
+    "参考图中的完整商品外形",
+    "参考图中的商品数量",
+    "参考图中的主体颜色关系",
+  ];
+  const sourcePreserve = unique(source.must_preserve);
+  const sourceNotAdd = unique(source.must_not_add);
+  const sourceUnknown = unique(source.unknown_do_not_assert);
+  const identity = {
+    // The audited identity manifest is an execution boundary. Do not append
+    // generic constraints here: even harmless additions make the plan differ
+    // from vision-result.json and must correctly block execution.
+    must_preserve: sourcePreserve.length ? sourcePreserve : unique(fallbackPreserve).slice(0, 7),
+    must_not_add: sourceNotAdd,
+    unknown_do_not_assert: sourceUnknown,
+  };
+  identity.product_lock = text(source.product_lock) || compileProductLock(identity);
+  return identity;
+}
+
+function normalizeSellingPoints(source, visibleFacts) {
+  const validFactRefs = new Set(visibleFacts.map((_, index) => `visible_fact:${index + 1}`));
+  const supplied = asArray(source.visible_selling_points ?? source.verified_selling_points)
+    .map((point, index) => ({
+      id: text(point?.id, `visible-sp-${index + 1}`),
+      value: text(point?.value),
+      basis_refs: asArray(point?.basis_refs).filter((reference) => validFactRefs.has(reference)),
+      confidence: new Set(["high", "medium", "low", "unknown"]).has(point?.confidence) ? point.confidence : "medium",
+    }))
+    .filter((point) => point.value && point.basis_refs.length);
+
+  const result = [];
+  for (const point of supplied) {
+    if (!result.some((candidate) => candidate.id === point.id)) result.push(point);
+  }
+  for (let index = 0; result.length < 2; index += 1) {
+    const factIndex = Math.min(index, Math.max(0, visibleFacts.length - 1));
+    const fact = visibleFacts[factIndex] || "参考图中的完整商品外形清晰可见";
+    result.push({
+      id: `visible-sp-${result.length + 1}`,
+      value: result.length === 0 ? `清晰呈现${fact}` : `从可见细节理解${fact}`,
+      basis_refs: [`visible_fact:${factIndex + 1}`],
+      confidence: "high",
+    });
+  }
+  return result.slice(0, Math.max(2, result.length));
+}
+
+function normalizeUnderstanding(source = {}) {
+  const visibleFacts = unique([
+    ...asArray(source.visible_facts),
+    ...asArray(source.verified_visible_facts),
+  ]);
+  if (visibleFacts.length === 0) visibleFacts.push("参考图中的完整商品外形清晰可见");
+  const identityManifest = normalizeIdentityManifest(source.identity_manifest, visibleFacts);
+  return {
+    category: text(source.category, "通用消费商品"),
+    product_name: {
+      value: text(source.product_name?.value, text(source.category, "商品")),
+      basis: text(source.product_name?.basis, "依据参考图中可见外形和结构识别"),
+      confidence: new Set(["high", "medium", "low", "unknown"]).has(source.product_name?.confidence)
+        ? source.product_name.confidence
+        : "medium",
+    },
+    visible_facts: visibleFacts,
+    verified_visible_facts: unique(asArray(source.verified_visible_facts)).length
+      ? unique(asArray(source.verified_visible_facts))
+      : [...visibleFacts],
+    inferred_attributes: unique(asArray(source.inferred_attributes)),
+    unknown_features: unique([
+      ...asArray(source.unknown_features),
+      ...asArray(source.uncertain_features),
+    ]),
+    identity_manifest: identityManifest,
+    provided_facts: unique(asArray(source.provided_facts)),
+    candidate_selling_points: normalizeSellingPoints(source, visibleFacts),
+    visible_specs: source.visible_specs && typeof source.visible_specs === "object" && !Array.isArray(source.visible_specs)
+      ? source.visible_specs
+      : {},
+    creative_positioning: unique(asArray(source.creative_positioning)),
+    suggested_audiences: unique(asArray(source.suggested_audiences)),
+    suggested_scenes: unique(asArray(source.suggested_scenes)),
+    unsupported_claims: unique(asArray(source.unsupported_claims)),
+    unknowns: unique([
+      ...asArray(source.unknowns),
+      ...asArray(source.unknown_features),
+      ...asArray(source.uncertain_features),
+    ]),
+  };
+}
+
+const basePageConfigs = [
+  {
+    category: "hero", render: "isolated_product", complexity: "simple", evidence: "fact:1",
+    decision: "快速识别完整商品外观、数量与核心配色",
+    scene: "纯净无缝背景中只呈现参考商品,保留完整轮廓、自然接触阴影和充足安全边距。",
+    composition: "商品作为唯一视觉主体居中略偏下,完整外形不被裁切,低信息密度建立清晰开场。",
+  },
+  {
+    category: "selling_point", render: "material_demonstration", complexity: "medium", evidence: "selling:1",
+    decision: "理解第一项有证据的可见材质或配色特征",
+    scene: "使用克制的单向掠射光沿参考图可见表面移动,通过真实明暗过渡呈现第一项已审计卖点;画面保留完整商品作为身份锚点,不改变商品颜色、结构、标识或数量,也不加入性能结论。",
+    composition: "以一处参考可见表面的细节裁切作为重点,完整商品退居连续空间后景,形成细节与整体的明确阅读关系。",
+  },
+  {
+    category: "scene", render: "environmental_scene", complexity: "complex", evidence: "scene:1",
+    decision: "建立商品与可信日常环境的空间关系",
+    scene: "在具有前景承托物、中景商品和远景环境层次的可信日常空间中放置商品。商品接触面、投影方向、环境反射和透视保持一致;道具只用于解释氛围与空间尺度,不遮挡身份特征,不暗示未经证实的功能、性能或适用承诺。",
+    composition: "采用宽松环境构图,以前中后景建立纵深,商品位于视觉交汇位置并保留一侧呼吸空间,避免与主视觉重复。",
+  },
+  {
+    category: "selling_point", render: "feature_demonstration", complexity: "medium", evidence: "selling:2",
+    decision: "理解第二项有证据的可见结构或细节特征",
+    scene: "在同一连续摄影空间中突出第二项已审计卖点对应的参考可见结构,通过边缘光、局部层次和清晰焦平面建立重点;禁止补画参考图未覆盖的内部、背面、开合结构、配件或功能说明。",
+    composition: "商品保持参考可见方向,以非破坏性缩放和位置关系呈现结构重点,避免机械重复和无证据标注。",
+  },
+  {
+    category: "decision_support", render: "scale_context", complexity: "medium", evidence: "fact:2",
+    decision: "通过可见外形和环境参照完成比例关系判断",
+    scene: "把完整商品置于尺寸中性的真实承托环境,用前后遮挡、接触阴影和空间留白表达相对视觉尺度;不提供具体测量值,不添加未知配件,不改变参考商品的外形、颜色、标识和数量。",
+    composition: "采用上方空间较充足的情境构图,商品与中性参照物保持清楚间距,以轮廓和负空间帮助判断而不声称真实尺寸。",
+  },
+  {
+    category: "scene", render: "human_interaction", complexity: "complex", evidence: "audience:1",
+    decision: "理解人物、商品与环境之间的使用情境",
+    scene: "人物仅作为情境和尺度参照,从画面边缘自然进入并与商品保持克制接触。手部、身体、承托面和商品之间的遮挡与受力真实;商品仍是视觉焦点,不遮住身份特征,不展示未知操作或用途。",
+    composition: "使用人物中景与商品主体形成对角阅读路径,环境保持简洁,动作区和商品识别区彼此分离。",
+  },
+  {
+    category: "decision_support", render: "usage_sequence", complexity: "medium", evidence: "fact:3",
+    decision: "汇总参考可见细节并完成整套购买判断",
+    scene: "在唯一一张多格构图中只使用参考图支持的完整外观与局部裁切,统一比例、光色和背景关系;各格依次展示完整轮廓、可见细节和环境关系,不生成未知背面、内部、底部、配件或测量文字。",
+    composition: "建立三个间距明确的横向阅读区域,第一格保持完整商品,后两格仅裁切参考可见表面,形成从整体到细节的收束。",
+  },
+];
+
+const baseShotSpecs = [
+  ["centered hero", "full product", "reference eye level", "front", "full_product", "upright", 75, "pure seamless", "even studio", "single accurate product", "low"],
+  ["material detail field", "detail and whole", "reference detail crop", "macro", "detail", "detail_crop", 48, "material field", "directional material", "material relation", "medium"],
+  ["wide environment depth", "wide environment", "reference-preserving oblique placement", "three_quarter_left", "product_in_context", "upright", 28, "lived-in room", "window ambient", "environment placement", "medium"],
+  ["single-space feature relation", "medium product", "reference-preserving side placement", "side", "full_product", "arranged_sequence", 58, "feature stage", "edge definition", "feature relation", "medium"],
+  ["scale context", "context medium", "reference-preserving top placement", "top", "product_in_context", "laid_flat", 38, "neutral context", "clear contextual", "relative scale relation", "medium"],
+  ["human context frame", "human medium-wide", "reference-preserving action frame", "front", "human_action", "held", 22, "human environment", "motivated environmental", "human context", "medium"],
+  ["visible evidence sequence", "multi-view crop sequence", "reference surface sequence", "low", "multi_panel", "multi_view", 62, "neutral sequence", "flat comparative", "reference crop sequence", "high"],
+];
+
+function interactionProfileFor(understanding, brief) {
+  const policy = compileSceneAffordancePolicy({ understanding, brief });
+  return {
+    ...interactionProfileFromPolicy(policy, brief),
+    scene_affordance_policy: policy,
+  };
+}
+
+function pageGoalsFor(interactionProfile) {
+  const configs = basePageConfigs.map((config) => ({ ...config }));
+  configs[2] = { ...configs[2], route_kind: "environmental", scene_slot: 1 };
+  configs[4] = { ...configs[4], route_kind: "scale_context", scene_slot: 3 };
+  configs[5] = { ...configs[5], route_kind: "interaction", scene_slot: 2 };
+  if (interactionProfile.kind === "wearable") {
+    const footwear = interactionProfile.wearable_subtype === "footwear";
+    configs[4] = {
+      ...configs[4],
+      route_kind: "footwear_scale",
+      scene_slot: 3,
+      ...(footwear ? {
+        scene: "把完整双鞋置于整洁玄关的平整地面或低矮换鞋凳旁,用接触阴影、鞋架边缘和留白表达真实尺度;不提供具体测量值,不添加未知配件,不改变参考商品的外形、颜色、标识和数量。",
+        composition: "采用接近地面的宽松三分之四构图,商品与换鞋动线保持清楚间距,环境只提供尺度和购买准备的生活线索。",
+      } : {
+        scene: "把完整商品置于与穿戴动作相邻的中性环境,以人物身体局部、接触关系和留白表达相对尺度;不提供具体测量值,不添加未知配件,不改变参考商品的外形、颜色、标识和数量。",
+        composition: "采用与穿戴部位齐平的中近景构图,商品和身体局部保持清楚间距,人物只提供尺度与搭配线索。",
+      }),
+    };
+    configs[6] = {
+      ...configs[6],
+      render: "fit_context",
+      complexity: "complex",
+      evidence: "fact:3",
+      decision: footwear ? "通过静态上脚近景完成鞋型、穿着关系与搭配判断" : "通过静态穿戴近景完成穿戴关系与搭配判断",
+      scene: footwear
+        ? "真人坐在低矮换鞋凳或入口区域,双脚自然落地,参考商品真实穿在脚上;画面只呈现参考图支持的可见表面,不新增鞋底、内部、后跟或配件结构。"
+        : "真人在安静的穿戴准备区域保持自然姿态,商品真实穿戴在正确身体部位;画面只呈现参考图支持的可见表面,不新增背面、内部或配件结构。",
+      composition: footwear
+        ? "以小腿以下和双脚为主体的静态近景构图,脚部受力和接触阴影自然,人物服装只作为克制的搭配线索。"
+        : "以穿戴部位为主体的静态近景构图,身体受力和接触关系自然,人物服装只作为克制的搭配线索。",
+      route_kind: "wearable_fit",
+      scene_slot: 2,
+    };
+  }
+  return configs.map((config, index) => ({ config, shot: [...baseShotSpecs[index]] }));
+}
+
+function shotSpecFromArray(shot) {
+  return {
+    composition_family: shot[0],
+    shot_scale: shot[1],
+    camera_angle: shot[2],
+    camera_axis: shot[3],
+    framing_mode: shot[4],
+    product_pose: shot[5],
+    product_occupancy_pct: shot[6],
+    background_family: shot[7],
+    lighting_family: shot[8],
+    interaction_mode: shot[9],
+    information_density: shot[10],
+  };
+}
+
+function sceneRouteFor({ config, interactionProfile, sceneHint, explicitSurfaceOverride = false }) {
+  if (!config.route_kind) return null;
+  if (config.route_kind === "environmental") {
+    const guidance = sceneGuidanceForPolicy(interactionProfile.scene_affordance_policy, sceneHint);
+    if (interactionProfile.support_mode === "body_worn") {
+      return {
+        id: "body_worn_stable_display",
+        decision: "在不依赖真人穿戴的前提下理解商品与搭配环境的关系",
+        scene: `在“${concise(sceneHint, 32)}”中让商品以参考可见正面独立陈列在稳定的穿戴准备区,人物不得穿着、手持或遮挡商品;承托、接触、投影和透视真实,不展示参考图未覆盖的背面、内部或新结构。`,
+        composition: "使用完整商品前中景和克制环境纵深构图,商品保持第一视觉焦点,环境仅提供搭配与空间线索。",
+        shot_overrides: { interaction_mode: "stable_display", camera_axis: "three_quarter_left", framing_mode: "product_in_context", product_pose: "upright" },
+        view_decision: "derived_affine",
+        requires_identity_review: false,
+      };
+    }
+    return {
+      id: guidance.route_id,
+      decision: guidance.decision,
+      scene: guidance.scene,
+      composition: guidance.composition || config.composition,
+      shot_overrides: guidance.shot_overrides || {},
+      view_decision: "derived_affine",
+      requires_identity_review: false,
+    };
+  }
+
+  if (config.route_kind === "scale_context") {
+    const guidance = sceneGuidanceForPolicy(interactionProfile.scene_affordance_policy, sceneHint);
+    return {
+      id: `${guidance.route_id}_scale_context`,
+      decision: "通过符合商品承托方式的环境参照完成相对比例判断",
+      scene: `${guidance.scene} 通过商品与承托面、邻近参照物、接触阴影和空间留白表达相对视觉尺度;不提供具体测量值,不添加未知配件。`,
+      composition: "采用上方空间较充足的情境构图,让完整商品、真实承托面和中性参照物保持清楚间距,以轮廓与负空间帮助判断而不声称真实尺寸。",
+      shot_overrides: {
+        ...(guidance.shot_overrides || {}),
+        framing_mode: "product_in_context",
+        interaction_mode: "relative scale relation",
+      },
+      view_decision: "derived_affine",
+      requires_identity_review: false,
+    };
+  }
+
+  if (config.route_kind === "footwear_scale" && interactionProfile.wearable_subtype === "footwear") {
+    return {
+      id: "footwear_scale_context",
+      decision: "通过鞋架、地面与动线关系完成相对尺度判断",
+      scene: `在“${concise(sceneHint, 32)}”中以完整双鞋和合理的接触、投影与参照关系表达相对视觉尺度;商品不放在与鞋类无关的高位桌面,不提供具体测量值,不添加未知配件。`,
+      composition: "采用与地面或承托关系齐平的三分之四构图,双鞋与参照物保持清楚间距,环境只提供尺度和购买准备的生活线索。",
+      shot_overrides: {
+        composition_family: "footwear scale path",
+        shot_scale: "context medium",
+        camera_angle: "reference-preserving three-quarter scale view",
+        camera_axis: "three_quarter_right",
+        framing_mode: "product_in_context",
+        product_pose: "arranged_sequence",
+        product_occupancy_pct: 38,
+        background_family: "footwear scale context",
+        lighting_family: "clear contextual",
+        interaction_mode: "relative scale relation",
+      },
+      view_decision: "derived_affine",
+      requires_identity_review: false,
+    };
+  }
+
+  if (config.route_kind === "wearable_fit" && interactionProfile.kind === "wearable") {
+    const footwear = interactionProfile.wearable_subtype === "footwear";
+    const fittingHint = /玄关|鞋架|换鞋凳|室内|entryway|bench|shoe rack/i.test(sceneHint)
+      ? `坐在“${concise(sceneHint, 32)}”的换鞋区域`
+      : `在“${concise(sceneHint, 32)}”的平整地面上自然站立`;
+    return {
+      id: footwear ? "human_wearing_fit_footwear" : "human_wearing_fit",
+      decision: footwear ? "通过静态真人上脚近景完成鞋型与搭配判断" : "通过静态真人穿戴近景完成穿戴关系判断",
+      scene: footwear
+        ? `真人${fittingHint},双脚自然落地,参考商品真实穿在脚上;鞋面和参考可见侧面朝向镜头,裤脚、脚部受力、接触阴影和空间透视自然。不得把鞋拿在手中,不展示参考图未覆盖的鞋底、内部或后跟新结构。`
+        : `真人在“${concise(sceneHint, 32)}”中静态穿戴商品,姿态、受力和遮挡符合真实关系;参考可见表面朝向镜头,商品识别区保持清晰,不展示参考图未覆盖的背面、内部或新结构。`,
+      composition: footwear
+        ? "使用小腿以下的静态近景构图,双脚和地面接触关系清楚,服装边缘只作为搭配线索,商品保持第一视觉焦点。"
+        : "使用穿戴部位的静态近景构图,人物和环境退居次要层级,避免肢体遮住商品关键识别特征。",
+      camera_style: footwear
+        ? "采用与鞋面齐平的低机位静态商业摄影,焦平面锁定参考可见鞋面,保持自然景深;新摄影机位必须接受商品身份复核。"
+        : "采用与穿戴部位齐平的静态商业摄影,焦平面锁定商品,保持自然景深;新摄影机位必须接受商品身份复核。",
+      negative: "错误穿戴关系、肢体畸形、手托商品冒充使用",
+      shot_overrides: {
+        composition_family: footwear ? "static on-body fit frame" : "static on-body usage frame",
+        shot_scale: footwear ? "lower-body close-medium" : "body-level close-medium",
+        camera_angle: footwear ? "low front fit view" : "body-level front fit view",
+        camera_axis: "three_quarter_left",
+        framing_mode: "human_action",
+        product_pose: "in_use",
+        product_occupancy_pct: footwear ? 52 : 45,
+        background_family: "quiet fitting area",
+        lighting_family: "soft motivated fitting light",
+        interaction_mode: "human_wearing",
+      },
+      view_decision: "provider_reference_generation",
+      requires_identity_review: true,
+    };
+  }
+
+  if (config.route_kind === "interaction" && interactionProfile.kind === "wearable") {
+    const footwear = interactionProfile.wearable_subtype === "footwear";
+    return {
+      id: footwear ? "human_wearing_footwear" : "human_wearing",
+      decision: footwear ? "通过真人自然上脚理解商品的穿着情境" : "通过真人自然穿戴理解商品的使用情境",
+      scene: footwear
+        ? `真人在“${concise(sceneHint, 32)}”中自然行走或站立,画面完整包含小腿以下、双脚与地面接触关系;参考商品真实穿在脚上,鞋面和参考可见侧面朝向镜头,脚部受力、裤脚遮挡、接触阴影和步态自然。不得把鞋拿在手中,不展示参考图未覆盖的鞋底、内部或后跟新结构。`
+        : `真人在“${concise(sceneHint, 32)}”中把商品自然穿戴在正确身体部位,姿态、受力和遮挡符合真实使用关系;参考可见表面朝向镜头,商品识别区与人物动作区分离。不得手托商品冒充穿戴,不展示参考图未覆盖的背面、内部或新结构。`,
+      composition: footwear
+        ? "使用人物膝部以下的中广景动作构图,商品所在脚位于视觉交汇点,另一只脚和环境只用于解释步态与尺度;商品保持清晰焦点并留出运动方向空间。"
+        : "使用人物中景或全身情境构图,让正确穿戴部位与商品形成第一视觉落点;人物身份线索和环境退居次要层级,避免肢体遮住商品关键识别特征。",
+      camera_style: footwear
+        ? "采用接近地面的三分之四动作机位和中等快门凝固自然步态,焦平面锁定参考可见鞋面;新摄影机位必须接受商品身份复核。"
+        : "采用与穿戴部位齐平的三分之四人物机位,焦平面锁定商品,人物与环境保持自然景深;新摄影机位必须接受商品身份复核。",
+      negative: "错误穿戴关系、肢体畸形、手托商品冒充使用",
+      shot_overrides: {
+        composition_family: footwear ? "on-body action path" : "on-body usage frame",
+        shot_scale: footwear ? "lower-body medium-wide" : "human medium-wide",
+        camera_angle: footwear ? "low three-quarter action view" : "body-level three-quarter in-use view",
+        camera_axis: footwear ? "low" : "three_quarter_right",
+        framing_mode: "human_action",
+        product_pose: "in_use",
+        product_occupancy_pct: footwear ? 30 : 26,
+        background_family: "usage environment",
+        lighting_family: "motivated action light",
+        interaction_mode: "human_wearing",
+      },
+      view_decision: "provider_reference_generation",
+      requires_identity_review: true,
+    };
+  }
+
+  if (interactionProfile.kind === "handheld") {
+    return {
+      id: "human_handheld",
+      decision: "通过真人自然手持理解商品的使用情境",
+      scene: `真人在“${concise(sceneHint, 32)}”的中景自然手持或托持商品,前景手部、商品主体和背景环境形成清楚空间层次;手指、掌心、商品重心和受力方向真实,商品参考可见表面朝向镜头且不被手部遮住关键识别特征,不演示未经证实的功能。`,
+      composition: "使用人物中景与商品主体形成对角阅读路径,手部动作区和商品识别区彼此分离,环境只提供尺度与情绪线索。",
+      shot_overrides: {
+        camera_axis: "three_quarter_right",
+        framing_mode: "human_action",
+        product_pose: "held",
+        interaction_mode: "human_handheld",
+      },
+      view_decision: "provider_reference_generation",
+      requires_identity_review: true,
+    };
+  }
+
+  return {
+    id: "human_proximity",
+    decision: "通过人物邻近关系理解商品的环境尺度",
+    scene: `人物位于“${concise(sceneHint, 32)}”的中后景作为尺度与生活线索,不拿取、不穿戴且不操作商品;商品位于前景承托面并保持完整可见,人物动线、投影和空间透视与商品一致。`,
+    composition: "使用商品前景和人物中后景形成纵深,商品为第一视觉落点,人物只建立尺度关系,避免把未知用途表演成事实。",
+    shot_overrides: {
+      composition_family: "foreground product human depth",
+      shot_scale: "environment medium-wide",
+      camera_angle: "reference-preserving three-quarter depth",
+      camera_axis: "three_quarter_right",
+      framing_mode: "product_in_context",
+      product_pose: "upright",
+      product_occupancy_pct: 34,
+      background_family: "human scale environment",
+      lighting_family: "motivated environmental",
+      interaction_mode: "human_proximity",
+    },
+    view_decision: "derived_affine",
+    requires_identity_review: false,
+  };
+}
+
+function evidenceFor(config, understanding, index, sceneCandidate = null) {
+  if (config.evidence.startsWith("selling:")) {
+    const point = understanding.candidate_selling_points[Number(config.evidence.split(":")[1]) - 1];
+    return { refs: [point.id], sellingPointId: point.id, evidenceText: point.value };
+  }
+  if (config.evidence.startsWith("scene:")) {
+    if (sceneCandidate?.text) {
+      return {
+        refs: [sceneCandidate.evidenceRef],
+        sellingPointId: "",
+        evidenceText: sceneCandidate.text,
+      };
+    }
+    const position = Math.min(Number(config.evidence.split(":")[1]), understanding.suggested_scenes.length) || 1;
+    return understanding.suggested_scenes.length
+      ? { refs: [`suggested_scene:${position}`], sellingPointId: "", evidenceText: understanding.suggested_scenes[position - 1] }
+      : { refs: ["creative_positioning:1"], sellingPointId: "", evidenceText: understanding.creative_positioning[0] || "可信日常环境" };
+  }
+  if (config.evidence.startsWith("audience:")) {
+    return understanding.suggested_audiences.length
+      ? { refs: ["suggested_audience:1"], sellingPointId: "", evidenceText: understanding.suggested_audiences[0] }
+      : { refs: ["visible_fact:1"], sellingPointId: "", evidenceText: understanding.visible_facts[0] };
+  }
+  const requested = Number(config.evidence.split(":")[1]) || index + 1;
+  const position = Math.min(requested, understanding.visible_facts.length);
+  return { refs: [`visible_fact:${position}`], sellingPointId: "", evidenceText: understanding.visible_facts[position - 1] };
+}
+
+function viewStrategyFor(config, evidenceRefs, route) {
+  const detail = config.render === "material_demonstration" || config.render === "feature_demonstration" || config.render === "usage_sequence";
+  return {
+    requested_view: detail ? "detail" : "reference_visible_surface",
+    decision: route?.view_decision ?? (detail ? "reference_crop" : config.category === "hero" ? "reference_exact" : "derived_affine"),
+    evidence_refs: evidenceRefs,
+    allowed_reveal: ["reference_visible_surface"],
+    forbidden_reveal: ["back", "interior", "bottom", "unseen_side"],
+    requires_identity_review: route?.requires_identity_review === true,
+    route_reason: route?.id ?? "default_reference_preserving_route",
+  };
+}
+
+function plannedRelationMode({ config, route, index }) {
+  if (index === 0) return "isolated";
+  if (config.category === "selling_point") return "detail";
+  if (route?.id === "human_proximity") return "human_proximity";
+  if (route?.id?.includes("stable_display")) return "stable_display";
+  if (route?.id?.startsWith("human_wearing") && config.route_kind === "wearable_fit") return "human_wearing_static";
+  if (route?.id?.startsWith("human_wearing")) return "human_wearing_action";
+  if (route?.id === "human_handheld") return "human_handheld";
+  if (route?.id?.endsWith("_context") && route?.id !== "installed_or_environmental_context") return "stable_display";
+  if (config.category === "scene") return "environmental";
+  return "stable_display";
+}
+
+function promptModules({ config, index, identity, productLabel, evidenceText, sceneHint, route, decision }) {
+  const contextualScene = route?.scene ?? (config.category === "scene" && sceneHint
+    ? `${config.scene} 场景方向采用“${concise(sceneHint, 32)}”,但它只控制环境氛围,不作为商品事实或功能声明。`
+    : config.scene);
+  const evidenceSummary = config.category === "selling_point"
+    ? concise(evidenceText, 56)
+    : "对应的已审计可见事实";
+  return {
+    product_lock: identity.product_lock,
+    image_job: `第${index + 1}张图只承担一个购买决策任务:${decision}。商品为${concise(productLabel, 32)};本页依据“${evidenceSummary}”组织画面,不扩写为未经证实的性能。`,
+    scene: contextualScene,
+    composition: `${route?.composition ?? config.composition} 本页与其他图位保持不同的主体占比、空间深度和阅读顺序。`,
+    lighting_material: `第${index + 1}张使用与场景动机一致的真实光源面积、方向、软硬度、色温、接触阴影和材质响应,保持参考商品颜色与表面层次。`,
+    camera_style: route?.camera_style ?? (config.complexity === "complex"
+      ? `使用独立的商业摄影焦段、焦平面和景深过渡,但保持参考图支持的可见表面,不生成隐藏视角。`
+      : `使用独立商业摄影机位和非破坏性裁切,保持参考可见方向与商品几何。`),
+    text_layout: "",
+    negative_constraints: `禁止商品身份漂移、结构变形、数量变化、无关物体、重复商品、虚构卖点、水印、额外文字、额外品牌标识、未知配件、隐藏背面、内部或底部${route?.negative ? `、${route.negative}` : ""}。`,
+  };
+}
+
+const compilePrompt = (modules) => Object.values(modules).filter(Boolean).join(" ");
+
+export function compileProductionPlan({
+  runId,
+  referenceUrl,
+  productUnderstanding,
+  brief = {},
+} = {}) {
+  if (!text(runId)) throw new Error("PRODUCTION_PLAN_RUN_ID_REQUIRED");
+  if (!text(referenceUrl)) throw new Error("PRODUCTION_PLAN_REFERENCE_REQUIRED");
+  const understanding = normalizeUnderstanding(productUnderstanding);
+  if (understanding.creative_positioning.length === 0) understanding.creative_positioning.push("克制、清晰的商业视觉");
+  const productLabel = understanding.product_name.value;
+  const referenceImages = [referenceUrl];
+  const aspectRatio = text(brief.aspectRatio, "1:1");
+  const dimensions = dimensionsForAspectRatio(aspectRatio);
+  const interactionProfile = interactionProfileFor(understanding, brief);
+  const diversityPolicy = compileSetDiversityPolicy({
+    ...interactionProfile.scene_affordance_policy,
+    wearable_subtype: interactionProfile.wearable_subtype,
+  });
+  const sceneStrategy = sceneCandidatesForPolicy({
+    understanding,
+    policy: interactionProfile.scene_affordance_policy,
+  });
+  const pageGoals = pageGoalsFor(interactionProfile);
+
+  const images = pageGoals.map(({ config, shot: baseShot }, index) => {
+    const sceneCandidate = config.scene_slot
+      ? sceneStrategy.candidates[(config.scene_slot - 1) % sceneStrategy.candidates.length]
+      : null;
+    const sceneHint = sceneCandidate?.text || "可信日常环境";
+    const evidence = evidenceFor(config, understanding, index, sceneCandidate);
+    const route = sceneRouteFor({
+      config,
+      interactionProfile,
+      sceneHint,
+      explicitSurfaceOverride: sceneStrategy.explicit_surface_override,
+    });
+    const decision = route?.decision ?? config.decision;
+    const modules = promptModules({
+      config,
+      index,
+      identity: understanding.identity_manifest,
+      productLabel,
+      evidenceText: evidence.evidenceText,
+      sceneHint,
+      route,
+      decision,
+    });
+    const rawShot = {
+      ...shotSpecFromArray(baseShot),
+      ...route?.shot_overrides,
+    };
+    const rawViewStrategy = viewStrategyFor(config, evidence.refs, route);
+    const compatible = enforceViewStrategyCompatibility({ shotSpec: rawShot, viewStrategy: rawViewStrategy });
+    const shot = compatible.shot_spec;
+    const relationMode = plannedRelationMode({ config, route, index });
+    const page = {
+      id: String(index + 1).padStart(2, "0"),
+      role: config.category,
+      business_category: config.category,
+      decision_subject: decision,
+      evidence_refs: evidence.refs,
+      selling_point_id: evidence.sellingPointId,
+      purpose: decision,
+      voc_refs: [],
+      set_design: {
+        render_family: config.render,
+        scene_complexity: config.complexity,
+        unique_visual_hook: `第${index + 1}张以${route?.id ?? config.render}完成“${decision}”,与其他页面不可互换。`,
+        contrast_with_previous: index === 0 ? "以低信息密度完整商品开场" : `相较第${index}张改变空间、景别、主体占比和信息关系。`,
+        relation_mode: relationMode,
+      },
+      shot_spec: {
+        narrative_job: `job-${index + 1}-${config.category}`,
+        composition_family: shot.composition_family,
+        shot_scale: shot.shot_scale,
+        camera_angle: shot.camera_angle,
+        camera_axis: shot.camera_axis,
+        framing_mode: shot.framing_mode,
+        product_pose: shot.product_pose,
+        product_placement: `distinct placement ${index + 1}`,
+        product_occupancy_pct: shot.product_occupancy_pct,
+        depth_structure: `distinct depth structure ${index + 1}`,
+        background_family: shot.background_family,
+        lighting_family: shot.lighting_family,
+        interaction_mode: shot.interaction_mode,
+        information_density: shot.information_density,
+      },
+      view_strategy: compatible.view_strategy,
+      view_strategy_compatibility: compatible.compatibility,
+      expected_page_compliance: {
+        product_human_relation: relationMode,
+        allowed_product_human_relations: [relationMode],
+        expected_camera_axis: shot.camera_axis,
+        expected_framing_mode: shot.framing_mode,
+        page_job: decision,
+        required_evidence_refs: evidence.refs,
+        forbidden_reveals: compatible.view_strategy.forbidden_reveal,
+      },
+      prompt_modules: modules,
+      generation_prompt: compilePrompt(modules),
+      negative_prompt: "变形,水印,额外文字,虚构配件",
+      overlay_copy: [],
+      reference_images: referenceImages,
+      model_parameters: {
+        adapter: "jimeng_img_v4",
+        width: dimensions.width,
+        height: dimensions.height,
+        scale: 0.6,
+        force_single: true,
+      },
+      acceptance_checks: [
+        "商品身份、数量、颜色和可见结构保持一致",
+        "只揭示参考图支持的表面和细节",
+        "本页唯一购买决策任务可辨认",
+        "没有未经证实的商品声明或额外文字",
+        ...(route?.requires_identity_review ? ["新摄影机位必须通过逐图商品身份复核"] : []),
+        ...(route?.id?.startsWith("human_wearing") ? ["人物穿戴关系自然且商品识别区无遮挡"] : []),
+      ],
+    };
+    page.set_design.content_signature = contentSignatureForPage({ page, relationMode, index });
+    return page;
+  });
+
+  return {
+    schema_version: "0.6",
+    run_id: runId,
+    status: "ready",
+    user_output_mode: "images_only",
+    request: {
+      instruction: text(brief.instruction, "根据当前商品参考图规划一套通用电商七图。"),
+      reference_images: referenceImages,
+    },
+    context: {
+      platform: text(brief.platform, "general_ecommerce"),
+      market: text(brief.market, "unspecified"),
+      target_language: text(brief.targetLanguage, "zh-CN"),
+      aspect_ratio: aspectRatio,
+      voc: { status: "unavailable", findings: [] },
+      interaction_profile: interactionProfile,
+      scene_policy: {
+        kind: interactionProfile.scene_affordance_policy.support_mode,
+        explicit_surface_override: sceneStrategy.explicit_surface_override,
+        rejected_hints: sceneStrategy.rejected_hints,
+      },
+      scene_affordance_policy: interactionProfile.scene_affordance_policy,
+      set_diversity_policy: diversityPolicy,
+    },
+    product_understanding: understanding,
+    global_consistency_lock: {
+      product_invariants: understanding.identity_manifest.must_preserve,
+      visual_system: unique([
+        ...understanding.creative_positioning.slice(0, 2),
+        "统一商品身份、核心色彩、真实材质响应与光影逻辑",
+      ]),
+      prohibited_changes: understanding.identity_manifest.must_not_add,
+    },
+    set_blueprint: {
+      business_allocation: { hero: 1, scene: 2, selling_point: 2, decision_support: 2 },
+      visual_thesis: `围绕${productLabel}建立从识别、理解、代入到判断的七页证据驱动视觉叙事。`,
+      narrative_arc: ["识别商品", "理解可见价值", "代入可信环境", "完成购买判断"],
+      shared_visual_dna: ["商品身份锁", "参考商品核心色彩", "一致的真实材质响应", "克制商业摄影语言"],
+      intentional_variations: ["销售任务", "主体占比", "空间深度", "景别与信息关系", "人物和环境参与度"],
+      diversity_policy: diversityPolicy,
+    },
+    images,
+    production_control: {
+      workflow: "seven_image_vertical_slice",
+      paid_generation_authorized: false,
+      next_action: "validate_then_wait_for_confirmation",
+    },
+  };
+}

+ 249 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-plan-compiler.test.mjs

@@ -0,0 +1,249 @@
+import assert from "node:assert/strict";
+import { execFile } from "node:child_process";
+import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { promisify } from "node:util";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+import { compileProductionPlan } from "./production-plan-compiler.mjs";
+
+const execFileAsync = promisify(execFile);
+const validatorFile = fileURLToPath(new URL("./validate-plan.mjs", import.meta.url));
+
+const understanding = {
+  category: "鞋履",
+  product_name: { value: "低帮系带帆布鞋", basis: "可见低帮系带结构", confidence: "high" },
+  visible_facts: [
+    "鞋面主体为米白色织物",
+    "鞋带为深蓝色扁平织带",
+    "鞋头覆盖深蓝色橡胶包头",
+    "中底侧边有连续深蓝色细线",
+  ],
+  verified_visible_facts: [
+    "鞋面主体为米白色织物",
+    "鞋带为深蓝色扁平织带",
+    "鞋头覆盖深蓝色橡胶包头",
+    "中底侧边有连续深蓝色细线",
+  ],
+  visible_selling_points: [
+    { id: "visible-sp-1", value: "米白鞋身与深蓝包头形成清晰对比", basis_refs: ["visible_fact:1", "visible_fact:3"], confidence: "high" },
+    { id: "visible-sp-2", value: "深蓝鞋带与中底侧线形成统一细节", basis_refs: ["visible_fact:2", "visible_fact:4"], confidence: "high" },
+  ],
+  visible_specs: { 鞋型: "低帮系带款" },
+  creative_positioning: ["简约日常风格"],
+  suggested_audiences: ["偏好简洁搭配的人群"],
+  suggested_scenes: ["整洁室内休闲空间", "城市日常出行环境"],
+  unsupported_claims: ["防水", "天然橡胶"],
+  inferred_attributes: [],
+  unknown_features: ["鞋垫属性", "后跟结构"],
+  identity_manifest: {
+    must_preserve: ["米白织物鞋面", "深蓝橡胶包头", "深蓝扁平鞋带", "深蓝中底侧线"],
+    must_not_add: ["品牌标识", "型号文字", "额外配件"],
+    unknown_do_not_assert: ["鞋垫属性", "后跟结构"],
+  },
+};
+
+test("production compiler creates a validated seven-page plan from audited product truth", async () => {
+  const plan = compileProductionPlan({
+    runId: "run-20260731220000-test001",
+    referenceUrl: "https://images.example.test/shoe.jpg",
+    productUnderstanding: understanding,
+    brief: {
+      instruction: "为这款商品生成淘宝中文七图",
+      platform: "taobao",
+      market: "CN",
+      targetLanguage: "zh-CN",
+      aspectRatio: "1:1",
+    },
+  });
+
+  assert.equal(plan.schema_version, "0.6");
+  assert.equal(plan.images.length, 7);
+  assert.deepEqual(
+    plan.images.map((image) => image.business_category),
+    ["hero", "selling_point", "scene", "selling_point", "decision_support", "scene", "decision_support"],
+  );
+  assert.equal(plan.context.platform, "taobao");
+  assert.equal(plan.context.market, "CN");
+  assert.equal(plan.context.target_language, "zh-CN");
+  assert.equal(plan.context.aspect_ratio, "1:1");
+  assert.deepEqual(plan.product_understanding.identity_manifest.must_not_add, understanding.identity_manifest.must_not_add);
+  assert.deepEqual(plan.product_understanding.identity_manifest.unknown_do_not_assert, understanding.identity_manifest.unknown_do_not_assert);
+  assert.equal(plan.context.interaction_profile.kind, "wearable");
+  assert.equal(plan.context.interaction_profile.wearable_subtype, "footwear");
+  assert.equal(plan.images[5].shot_spec.interaction_mode, "human_wearing");
+  assert.equal(plan.images[5].shot_spec.product_pose, "in_use");
+  assert.equal(plan.images[5].shot_spec.framing_mode, "human_action");
+  assert.equal(plan.images[5].view_strategy.decision, "provider_reference_generation");
+  assert.equal(plan.images[5].view_strategy.requires_identity_review, true);
+  assert.match(plan.images[5].prompt_modules.scene, /穿在脚上/);
+  assert.equal(plan.images[6].set_design.render_family, "fit_context");
+  assert.equal(plan.images[6].shot_spec.interaction_mode, "human_wearing");
+  assert.equal(plan.images[6].shot_spec.product_pose, "in_use");
+  assert.match(plan.images[2].prompt_modules.scene, /地面|换鞋区域/);
+  assert.match(plan.images[4].prompt_modules.scene, /鞋架|玄关|地面|换鞋凳/);
+  assert.equal(plan.context.scene_policy.explicit_surface_override, false);
+  assert.equal(plan.context.scene_affordance_policy.schema_version, "0.2");
+  assert.ok(plan.context.scene_affordance_policy.allowed_relation_modes.includes("stable_display"));
+  assert.equal(plan.context.set_diversity_policy.maximum_human_wearing_pages, 3);
+  assert.ok(plan.images.every((image) => image.set_design.content_signature));
+  assert.ok(plan.images.every((image) => image.view_strategy_compatibility.compatible));
+  assert.ok(!plan.images.some((image) => image.shot_spec.camera_axis === "side" && image.view_strategy.decision === "reference_crop"));
+  assert.deepEqual(plan.context.scene_policy.rejected_hints, []);
+  assert.equal(new Set(plan.images.map((image) => image.prompt_modules.scene)).size, 7);
+  assert.ok(plan.images.every((image) => image.prompt_modules.product_lock === plan.product_understanding.identity_manifest.product_lock));
+  assert.ok(plan.images.every((image) => image.view_strategy?.requested_view === "reference_visible_surface" || image.view_strategy?.requested_view === "detail"));
+  assert.ok(plan.images.every((image) => !["back", "interior", "bottom"].includes(image.view_strategy?.requested_view)));
+
+  const directory = await mkdtemp(join(tmpdir(), "production-plan-"));
+  try {
+    const filename = join(directory, "prompt-plan.json");
+    await writeFile(filename, JSON.stringify(plan));
+    const { stdout } = await execFileAsync(process.execPath, [validatorFile, filename]);
+    assert.equal(JSON.parse(stdout).valid, true);
+  } finally {
+    await rm(directory, { recursive: true, force: true });
+  }
+});
+
+test("body-worn product reserves the set for display, proximity, action and static fit relations", () => {
+  const garment = {
+    ...understanding,
+    category: "服装",
+    product_name: { value: "驼色长款外套", basis: "可见长款外套", confidence: "high" },
+    visible_facts: ["驼色长款廓形", "宽幅翻领", "同面料腰带正面系结", "右侧斜插袋可见"],
+    verified_visible_facts: ["驼色长款廓形", "宽幅翻领", "同面料腰带正面系结", "右侧斜插袋可见"],
+    identity_manifest: {
+      must_preserve: ["驼色长款廓形", "宽幅翻领", "同面料腰带"],
+      must_not_add: ["纽扣", "额外品牌"], unknown_do_not_assert: ["背面结构", "材质成分"],
+    },
+  };
+  const plan = compileProductionPlan({
+    runId: "run-garment-diversity", referenceUrl: "https://images.example.test/coat.jpg",
+    productUnderstanding: garment,
+    brief: { instruction: "生成淘宝中文七图", platform: "taobao", market: "cn", targetLanguage: "zh-CN" },
+  });
+  assert.deepEqual(
+    plan.images.map((image) => image.set_design.relation_mode),
+    ["isolated", "detail", "stable_display", "detail", "human_proximity", "human_wearing_action", "human_wearing_static"],
+  );
+  assert.equal(plan.images.filter((image) => image.set_design.relation_mode.startsWith("human_wearing")).length, 2);
+  assert.equal(new Set(plan.images.map((image) => image.set_design.content_signature)).size, 7);
+});
+
+test("production compiler derives safe selling-point fallbacks without inventing claims", () => {
+  const sparse = structuredClone(understanding);
+  sparse.visible_selling_points = [];
+  sparse.visible_facts = ["完整商品外形清晰可见", "主体颜色和表面层次可见"];
+  sparse.verified_visible_facts = [...sparse.visible_facts];
+  const plan = compileProductionPlan({
+    runId: "run-20260731220000-test002",
+    referenceUrl: "https://images.example.test/generic.jpg",
+    productUnderstanding: sparse,
+    brief: {},
+  });
+  assert.equal(plan.product_understanding.candidate_selling_points.length, 2);
+  assert.ok(plan.product_understanding.candidate_selling_points.every((point) => point.basis_refs[0].startsWith("visible_fact:")));
+  assert.ok(plan.images.every((image) => !image.generation_prompt.includes("防水")));
+  assert.ok(plan.images.every((image) => !image.generation_prompt.includes("天然橡胶")));
+});
+
+test("production compiler applies the requested aspect ratio to provider dimensions", () => {
+  const portrait = compileProductionPlan({
+    runId: "run-20260731220000-test003",
+    referenceUrl: "https://images.example.test/portrait.jpg",
+    productUnderstanding: understanding,
+    brief: { aspectRatio: "3:4" },
+  });
+  assert.ok(portrait.images.every((image) => image.model_parameters.width === 1536));
+  assert.ok(portrait.images.every((image) => image.model_parameters.height === 2048));
+  assert.throws(() => compileProductionPlan({
+    runId: "run-20260731220000-test004",
+    referenceUrl: "https://images.example.test/invalid.jpg",
+    productUnderstanding: understanding,
+    brief: { aspectRatio: "freeform" },
+  }), /PRODUCTION_PLAN_ASPECT_RATIO_INVALID/);
+});
+
+test("production compiler keeps a non-wearable product out of the wearing route", () => {
+  const cup = structuredClone(understanding);
+  cup.category = "陶瓷杯";
+  cup.product_name = { value: "哑光陶瓷杯", basis: "可见杯体和杯柄", confidence: "high" };
+  cup.visible_facts = ["白色陶瓷杯体", "侧面可见弧形杯柄"];
+  cup.verified_visible_facts = [...cup.visible_facts];
+  cup.suggested_scenes = ["整洁厨房台面", "早餐桌面环境"];
+  const plan = compileProductionPlan({
+    runId: "run-20260731220000-test005",
+    referenceUrl: "https://images.example.test/cup.jpg",
+    productUnderstanding: cup,
+    brief: { instruction: "生成日常使用场景七图" },
+  });
+  assert.notEqual(plan.context.interaction_profile.kind, "wearable");
+  assert.notEqual(plan.images[5].shot_spec.interaction_mode, "human_wearing");
+  assert.equal(plan.images[5].shot_spec.interaction_mode, "human_handheld");
+  assert.equal(plan.images[5].shot_spec.framing_mode, "human_action");
+});
+
+test("coffee pouch plan is self-validating across scale context, handheld depth and camera rhythm", async () => {
+  const coffee = {
+    ...structuredClone(understanding),
+    category: "咖啡包装食品",
+    product_name: { value: "MORI COFFEE 250g 咖啡豆袋装", basis: "正面包装文字与袋型可见", confidence: "high" },
+    visible_facts: ["奶油白与绿色拼色袋装包装", "正面可见 MORI COFFEE 标识", "包装正面可见 250g 规格", "袋口和直立袋型可见"],
+    verified_visible_facts: ["奶油白与绿色拼色袋装包装", "正面可见 MORI COFFEE 标识", "包装正面可见 250g 规格", "袋口和直立袋型可见"],
+    suggested_scenes: ["整洁咖啡操作台", "自然光早餐桌", "生活方式类电商详情页首图"],
+    visible_selling_points: [
+      { id: "visible-sp-1", value: "奶油白与绿色形成清楚包装层次", basis_refs: ["visible_fact:1"], confidence: "high" },
+      { id: "visible-sp-2", value: "正面品牌与规格信息清晰可见", basis_refs: ["visible_fact:2", "visible_fact:3"], confidence: "high" },
+    ],
+    identity_manifest: {
+      must_preserve: ["奶油白与绿色拼色", "MORI COFFEE 正面标识", "250g 正面规格", "直立袋型"],
+      must_not_add: ["额外品牌", "虚构产地", "冲泡器具配件"],
+      unknown_do_not_assert: ["背面文字", "咖啡豆产地", "烘焙度"],
+    },
+  };
+  const plan = compileProductionPlan({
+    runId: "run-coffee-regression",
+    referenceUrl: "https://images.example.test/coffee-pouch.jpg",
+    productUnderstanding: coffee,
+    brief: { instruction: "给这款商品做一套适合淘宝详情页的中文七图,直接生成并交付", platform: "taobao", market: "CN", targetLanguage: "zh-CN" },
+  });
+
+  assert.equal(plan.context.scene_affordance_policy.support_mode, "tabletop_supported");
+  assert.match(plan.images[4].prompt_modules.scene, /桌面|台面|托盘|餐桌|厨房/);
+  assert.doesNotMatch(plan.images[4].prompt_modules.scene, /详情页|首图/);
+  assert.deepEqual(plan.context.scene_policy.rejected_hints, ["生活方式类电商详情页首图"]);
+  assert.match(plan.images[5].prompt_modules.scene, /前景.*中景|中景.*前景/);
+  assert.match(plan.images[5].prompt_modules.scene, /手持|托持/);
+  const axisCounts = Map.groupBy
+    ? [...Map.groupBy(plan.images, (image) => image.shot_spec.camera_axis)].map(([axis, images]) => [axis, images.length])
+    : Object.entries(plan.images.reduce((counts, image) => ({ ...counts, [image.shot_spec.camera_axis]: (counts[image.shot_spec.camera_axis] || 0) + 1 }), {}));
+  assert.ok(axisCounts.every(([, count]) => count <= 2));
+
+  const directory = await mkdtemp(join(tmpdir(), "coffee-production-plan-"));
+  try {
+    const filename = join(directory, "prompt-plan.json");
+    await writeFile(filename, JSON.stringify(plan));
+    const { stdout } = await execFileAsync(process.execPath, [validatorFile, filename]);
+    assert.equal(JSON.parse(stdout).valid, true);
+  } finally {
+    await rm(directory, { recursive: true, force: true });
+  }
+});
+
+test("production compiler filters implausible footwear surfaces and keeps dynamic context routes", () => {
+  const shoe = structuredClone(understanding);
+  shoe.suggested_scenes = ["办公室书桌中央", "图书馆桌面", "玄关换鞋区域"];
+  const plan = compileProductionPlan({
+    runId: "run-20260731220000-test006",
+    referenceUrl: "https://images.example.test/shoe-context.jpg",
+    productUnderstanding: shoe,
+    brief: { instruction: "生成一套淘宝鞋类七图,包含真人上脚图" },
+  });
+  assert.deepEqual(plan.context.scene_policy.rejected_hints, ["办公室书桌中央", "图书馆桌面"]);
+  assert.match(plan.images[2].prompt_modules.scene, /地面|换鞋/);
+  assert.doesNotMatch(plan.images[2].prompt_modules.scene.split("不得")[0], /书桌|桌面|办公桌/);
+  assert.equal(plan.images[6].set_design.render_family, "fit_context");
+  assert.equal(plan.images[6].view_strategy.requires_identity_review, true);
+});

+ 76 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-result-verifier.mjs

@@ -0,0 +1,76 @@
+const asArray = (value) => Array.isArray(value) ? value : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const allowedAxes = new Set(["front", "three_quarter_left", "three_quarter_right", "side", "top", "low", "macro", "unknown"]);
+const allowedFraming = new Set(["full_product", "product_in_context", "detail", "multi_panel", "human_action", "unknown"]);
+
+function normalizedBox(value) {
+  const box = value || {};
+  const result = {
+    x: Number(box.x), y: Number(box.y), width: Number(box.width), height: Number(box.height),
+  };
+  if (Object.values(result).some((item) => !Number.isFinite(item) || item < 0 || item > 1)) return null;
+  if (result.width <= 0 || result.height <= 0 || result.x + result.width > 1.001 || result.y + result.height > 1.001) return null;
+  return result;
+}
+
+export function normalizeRegionEvidence(value) {
+  if (value === "unverifiable" || value?.status === "unverifiable") return { status: "unverifiable", boxes: [] };
+  const candidates = Array.isArray(value) ? value : asArray(value?.boxes ?? (value ? [value] : []));
+  const boxes = candidates.map(normalizedBox).filter(Boolean);
+  return boxes.length ? { status: "verified", boxes } : { status: "unverifiable", boxes: [] };
+}
+
+export function normalizePageCompliance(value = {}) {
+  const axis = text(value.actual_camera_axis, text(value.actual_view?.camera_axis, "unknown"));
+  const framing = text(value.actual_framing_mode, text(value.actual_view?.framing_mode, "unknown"));
+  const occupancy = Number(value.product_occupancy_pct ?? value.actual_view?.product_occupancy_pct);
+  return {
+    schema_version: "1.0",
+    actual_camera_axis: allowedAxes.has(axis) ? axis : "unknown",
+    actual_framing_mode: allowedFraming.has(framing) ? framing : "unknown",
+    product_occupancy_pct: Number.isFinite(occupancy) ? Math.max(0, Math.min(100, occupancy)) : null,
+    product_human_relation: text(value.product_human_relation, "unknown"),
+    scene_family: text(value.scene_family, "unknown"),
+    composition_family: text(value.composition_family, "unknown"),
+    page_job_compliant: value.page_job_compliant === true,
+    visible_evidence: asArray(value.visible_evidence).map(String).filter(Boolean),
+    forbidden_reveals: asArray(value.forbidden_reveals).map(String).filter(Boolean),
+    violations: asArray(value.violations).map((item) => typeof item === "string"
+      ? { code: "PAGE_COMPLIANCE_VIOLATION", detail: item }
+      : { code: text(item?.code, "PAGE_COMPLIANCE_VIOLATION"), detail: text(item?.detail, "unspecified violation") }),
+    product_region: normalizeRegionEvidence(value.product_region),
+    face_regions: normalizeRegionEvidence(value.face_regions),
+    critical_product_regions: normalizeRegionEvidence(value.critical_product_regions),
+    recommended_text_safe_regions: normalizeRegionEvidence(value.recommended_text_safe_regions),
+    semantic_duplicate_group: text(value.semantic_duplicate_group),
+    notes: text(value.notes),
+  };
+}
+
+export function verifyPageCompliance({ pagePlan = {}, observed = null } = {}) {
+  if (!observed) return { status: "quality_unverified", accepted: false, violations: [{ code: "PAGE_COMPLIANCE_UNAVAILABLE", detail: "No output-level page compliance review" }], observed: null };
+  const actual = normalizePageCompliance(observed);
+  const expected = pagePlan.expected_page_compliance || {};
+  const violations = [...actual.violations];
+  const unknownCore = actual.actual_camera_axis === "unknown"
+    || actual.actual_framing_mode === "unknown"
+    || actual.product_human_relation === "unknown";
+  const allowedRelations = asArray(expected.allowed_product_human_relations).length
+    ? asArray(expected.allowed_product_human_relations)
+    : expected.product_human_relation ? [expected.product_human_relation] : [];
+  if (!unknownCore && allowedRelations.length && !allowedRelations.includes(actual.product_human_relation)) {
+    violations.push({
+      code: "PRODUCT_HUMAN_RELATION_MISMATCH",
+      detail: `Expected ${allowedRelations.join(" or ")}, observed ${actual.product_human_relation}`,
+    });
+  }
+  if (!unknownCore && !actual.page_job_compliant) violations.push({ code: "PAGE_JOB_NOT_SATISFIED", detail: "Generated output does not satisfy its assigned sales task" });
+  if (actual.forbidden_reveals.length) violations.push({ code: "FORBIDDEN_SURFACE_REVEALED", detail: actual.forbidden_reveals.join(", ") });
+  return {
+    status: violations.length ? "quality_blocked" : unknownCore ? "quality_unverified" : "quality_passed",
+    accepted: violations.length === 0 && !unknownCore,
+    violations,
+    expected,
+    observed: actual,
+  };
+}

+ 22 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/production-result-verifier.test.mjs

@@ -0,0 +1,22 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { normalizeRegionEvidence, verifyPageCompliance } from "./production-result-verifier.mjs";
+
+test("page verifier blocks an actual wearer when the page requires human proximity", () => {
+  const result = verifyPageCompliance({
+    pagePlan: { expected_page_compliance: { product_human_relation: "human_proximity" } },
+    observed: {
+      actual_camera_axis: "front", actual_framing_mode: "human_action",
+      product_human_relation: "human_wearing_static", page_job_compliant: false,
+      violations: [], forbidden_reveals: [],
+    },
+  });
+  assert.equal(result.status, "quality_blocked");
+  assert.ok(result.violations.some((item) => item.code === "PRODUCT_HUMAN_RELATION_MISMATCH"));
+});
+
+test("invalid or absent normalized regions fail closed as unverifiable", () => {
+  assert.deepEqual(normalizeRegionEvidence({ x: 0.8, y: 0.8, width: 0.4, height: 0.4 }), { status: "unverifiable", boxes: [] });
+  assert.deepEqual(normalizeRegionEvidence("unverifiable"), { status: "unverifiable", boxes: [] });
+});
+

+ 153 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-asset-plan-compiler.mjs

@@ -0,0 +1,153 @@
+import { pageTemplate, pageTemplatePrompt } from "./reference-first-page-template-registry.mjs";
+import { platformProfilePrompt } from "./platform-design-profile-registry.mjs";
+
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+
+export const THREE_VIEW_PROMPT = `一张16:9的产品多视图设定图,白色纯背景。画面采用3列网格布局:
+编号1(第一列):产品正面全景视图。产品居中放置,正面朝向镜头,完整展示产品正面外观、包装设计、logo与品牌标识。光线均匀柔和,无强烈反光。
+编号2(第二列):产品45度观察全景视图。产品保持与第一列完全相同的自然稳定承载姿态,支撑面、垂直方向和摆放状态不变。仅改变摄影机观察方位:摄影机在水平面内绕产品中心移动45度,形成正面与侧面的三分之四观察视角。摄影机滚转角roll为0,地平线保持水平。禁止产品倾倒、侧翻、斜放、悬浮或绕水平轴旋转。清晰呈现产品厚度、材质质感与立体结构,光线略侧以突出体积感。
+编号3(第三列):产品侧面/背面视图。展示产品侧面轮廓或背面标签信息,完整呈现产品整体形态。光线均匀,与其他视图保持一致的色温与亮度。
+所有视图中,产品为同一物体,包装颜色、材质反光、形态比例完全一致。产品不悬浮、不倾斜、不做特效处理。正面均匀照明,无硬阴影,高分辨率,商业产品摄影风格,4K。画面中不生成新增标题、标签、尺寸、参数、说明文字或水印;产品自身logo、品牌标识与包装原有文字正常保留。参考图用于确定产品外观和色彩基准。`;
+
+export const PRODUCT_SETTING_BOARD_PROMPT = `生成一张16:9横版无新增文字的产品视觉设定板,作品集级产品设计板,商业产品摄影质感,干净高级的视觉排版,白色微纹理背景,细灰色分割线。参考图用于确定产品外观、颜色、材质和品牌识别基准。
+
+整体构图(横版16:9):
+
+左侧区域(约30%宽度):
+放置一张产品主视角特写,采用商业摄影布光和浅景深。下方保留干净视觉留白和无文字信息卡框架,只用色块、细线和间距表达层级,不填入标题、标签、参数或说明。
+
+中间区域(约30%宽度):
+展示同一产品的正面全景、45度观察视角、侧面轮廓、俯视/底部视图。四图统一外观、材质与色彩。背景允许浅灰辅助线表达比例关系,但不标注尺寸数字和文字。
+
+右侧区域(约40%宽度,上下两段):
+上半部用无文字视觉卡片展示包装层次与细节特写,包括外盒、内托、产品本体、材质纹理、logo和可见接口;下半部展示产品在4个典型且适合该商品的场景中的画面。产品始终是视觉重心,光影色调一致。
+
+视觉风格:
+商业产品摄影质感,高级提案排版,干净白底,细线分隔,灰黑色调,简约现代。材质真实,纹理清晰,色彩准确,光影专业。
+画面要求:16:9横版,高清,信息丰富但不杂乱。画面中不生成任何新增中文、英文、字母、数字、尺寸、标题、标签、参数、说明文字或水印;产品自身原有logo、品牌标识和包装原字正常保留。不要完整人物,不要卡通、二次元或插画风,要真实商业产品摄影质感;手持特写允许出现局部手部。`;
+
+function dimensionsForAspectRatio(value) {
+  const match = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(text(value, "1:1"));
+  if (!match) throw new Error("REFERENCE_FIRST_ASPECT_RATIO_INVALID");
+  const horizontal = Number(match[1]);
+  const vertical = Number(match[2]);
+  if (!(horizontal > 0 && vertical > 0)) throw new Error("REFERENCE_FIRST_ASPECT_RATIO_INVALID");
+  if (horizontal === vertical) return { width: 2048, height: 2048 };
+  const align8 = (number) => Math.max(512, Math.round(number / 8) * 8);
+  return horizontal > vertical
+    ? { width: 2048, height: align8(2048 * vertical / horizontal) }
+    : { width: align8(2048 * horizontal / vertical), height: 2048 };
+}
+
+function imageEntry({ id, prompt, negativePrompt = "", references, width, height }) {
+  return {
+    id,
+    generation_prompt: prompt,
+    negative_prompt: negativePrompt,
+    reference_images: [...references],
+    model_parameters: { adapter: "jimeng_img_v4", width, height, scale: 0.6, force_single: true },
+  };
+}
+
+export function compileReferenceAssetPlan({ runId, originalReference, settingBoardPrompt = "", authoredBy = "pipeline", sourceAuthoringSha256 = "" } = {}) {
+  if (!text(runId)) throw new Error("REFERENCE_ASSET_RUN_ID_REQUIRED");
+  if (!text(originalReference)) throw new Error("REFERENCE_ASSET_ORIGINAL_REFERENCE_REQUIRED");
+  const references = [originalReference];
+  return {
+    schema_version: "0.2",
+    plan_type: "reference_assets",
+    status: "ready",
+    run_id: runId,
+    request: { reference_images: [] },
+    images: [
+      imageEntry({ id: "three-view", prompt: THREE_VIEW_PROMPT, references, width: 2048, height: 1152 }),
+      imageEntry({ id: "setting-board", prompt: text(settingBoardPrompt, PRODUCT_SETTING_BOARD_PROMPT), references, width: 2048, height: 1152 }),
+    ],
+    reference_authoring: { authored_by: authoredBy, source_authoring_sha256: sourceAuthoringSha256 || null, setting_board_prompt_mode: settingBoardPrompt ? "claude_dynamic" : "legacy_fixed" },
+  };
+}
+
+function visualBiblePrompt(visualBible = {}) {
+  return [
+    `商业风格:${text(visualBible.commercial_style, "真实商业产品摄影")}。`,
+    `色板:${Array.isArray(visualBible.palette) ? visualBible.palette.join("、") : "以商品主色为基准"}。`,
+    `光线:${text(visualBible.lighting, "专业、统一且不改变商品材质")}。`,
+    `道具语言:${text(visualBible.prop_language, "克制且只解释商品关系")}。`,
+    `背景材料:${Array.isArray(visualBible.background_materials) && visualBible.background_materials.length ? visualBible.background_materials.join("、") : "真实、克制并与商品协调"}。`,
+  ].join("\n");
+}
+
+function compilePagePrompt({ page, blueprint, brief, instruction }) {
+  const template = pageTemplate(page.template_id);
+  const productBrief = blueprint.product_creative_brief;
+  return `任务声明:你只负责生成整套七图中的第${page.id}张单图。只输出一张${text(brief.aspectRatio, "1:1")}图片,不生成拼图、九宫格、信息板、套版或其他页面。
+
+用户需求:${instruction}
+
+两张参考图的职责:
+- 第一张产品三视图是商品基准。产品形态、比例、颜色、材质、表面纹理、光泽、Logo、品牌标识和包装原有文字以三视图为准。
+- 第二张产品视觉设定板只用于场景、氛围、构图、道具语言和使用关系参考,不用它改写产品本体材质。
+- 商品关键识别元素:${productBrief.identity_cues.join(";")}。
+- 多样性通过页面配方、机位、空间、人物关系、构图和光影实现,不得通过改变商品颜色或材质实现。
+
+${platformProfilePrompt(blueprint.platform_strategy.profile_snapshot)}
+
+整套共同视觉圣经:
+${visualBiblePrompt(blueprint.visual_bible)}
+
+${pageTemplatePrompt(template)}
+
+本页动态执行参数:
+- 选择该配方的理由:${page.selection_reason}。
+- 本页具体任务:${page.page_job}。
+- 物理场景:${page.scene}。
+- 商品位置、姿态和支撑关系:${page.product_pose}。
+- 摄影机:${page.camera}。
+- 人物关系:${page.human_relation}。
+- 道具:${page.props.join("、") || "不使用无关道具"}。
+- 构图与前中后景:${page.composition}。
+- 后置文字预期区域:${page.text_safe_zone}。这里只形成低细节留白,画面内不要生成文字。
+- 与同定位另一页的差异:${page.difference_from_peer}。
+
+输出要求:真实商业产品摄影,高分辨率,商品是唯一视觉重心。画面中不生成任何新增标题、标签、字母、数字、参数、说明文字或水印;产品自身真实Logo、品牌标识和包装原有文字保持不变。`;
+}
+
+export function compileReferenceFirstSetPlan({ runId, generatedReferences, brief = {}, blueprint = null } = {}) {
+  if (!text(runId)) throw new Error("REFERENCE_FIRST_SET_RUN_ID_REQUIRED");
+  const references = Array.isArray(generatedReferences) ? generatedReferences.map((item) => text(item)).filter(Boolean) : [];
+  if (references.length !== 2) throw new Error("REFERENCE_FIRST_SET_REQUIRES_TWO_GENERATED_REFERENCES");
+  if (!blueprint || blueprint.run_id !== runId || !Array.isArray(blueprint.pages) || blueprint.pages.length !== 7) {
+    throw new Error("REFERENCE_FIRST_SET_BLUEPRINT_REQUIRED");
+  }
+  const instruction = text(brief.instruction, "生成一套适合电商详情页的七图");
+  const dimensions = dimensionsForAspectRatio(brief.aspectRatio);
+  return {
+    schema_version: "0.2",
+    plan_type: "reference_first_set",
+    status: "ready",
+    run_id: runId,
+    request: {
+      instruction,
+      platform: text(brief.platform, "general_ecommerce"),
+      market: text(brief.market, "unspecified"),
+      target_language: text(brief.targetLanguage, "zh-CN"),
+      aspect_ratio: text(brief.aspectRatio, "1:1"),
+      reference_images: [],
+    },
+    set_blueprint: {
+      schema_version: blueprint.schema_version,
+      platform_strategy: blueprint.platform_strategy,
+      product_creative_brief: blueprint.product_creative_brief,
+      visual_bible: blueprint.visual_bible,
+      difference_matrix: blueprint.difference_matrix,
+    },
+    images: blueprint.pages.map((page) => imageEntry({
+      id: page.id,
+      prompt: compilePagePrompt({ page, blueprint, brief, instruction }),
+      negativePrompt: "拼图、九宫格、信息板、其他页面、新增文字、乱码、标签、参数、水印、商品倾倒、悬浮、商品颜色变化、商品材质变化、Logo或包装原字被改写",
+      references,
+      width: dimensions.width,
+      height: dimensions.height,
+    })),
+  };
+}

+ 61 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-asset-plan-compiler.test.mjs

@@ -0,0 +1,61 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { compileReferenceAssetPlan, compileReferenceFirstSetPlan, PRODUCT_SETTING_BOARD_PROMPT, THREE_VIEW_PROMPT } from "./reference-asset-plan-compiler.mjs";
+import { compileSetBlueprint } from "./reference-first-set-blueprint.mjs";
+
+function creativePlan() {
+  const ids = ["white_complete", "selling_form", "selling_material", "scene_core_environment", "scene_human_interaction", "decision_scale_fit", "decision_value_summary"];
+  return {
+    product_creative_brief: {
+      category: "商品", form: "立体商品", colors: ["准确原色"], material_cues: ["哑光"], identity_cues: ["轮廓", "Logo"],
+      visible_selling_point_candidates: ["结构", "纹理"], natural_support_and_use: ["自然承载"], human_interactions: ["自然使用"],
+      commercial_direction: "高级商业摄影", unsupported_copy_facts: ["精确尺寸"],
+    },
+    visual_bible: { commercial_style: "高级商业摄影", palette: ["原色", "中性色"], lighting: "柔和统一", prop_language: "克制" },
+    pages: ids.map((template_id, index) => ({
+      template_id, selection_reason: `理由${index}`, page_job: `任务${index}`, scene: `具体场景${index}`,
+      product_pose: `稳定姿态${index}`, camera: `机位${index}`, human_relation: `人物关系${index}`, props: [`道具${index}`],
+      composition: `构图${index}`, text_safe_zone: `留白${index}`, difference_from_peer: `差异${index}`,
+    })),
+  };
+}
+
+test("reference assets are two direct text-free 16:9 generations from the original image", () => {
+  const original = "D:\\run\\product.png";
+  const plan = compileReferenceAssetPlan({ runId: "run-20260804120000-aabbcc", originalReference: original });
+  assert.equal(plan.plan_type, "reference_assets");
+  assert.deepEqual(plan.images.map((image) => image.id), ["three-view", "setting-board"]);
+  assert.ok(plan.images.every((image) => image.model_parameters.width === 2048 && image.model_parameters.height === 1152));
+  assert.ok(plan.images.every((image) => JSON.stringify(image.reference_images) === JSON.stringify([original])));
+  assert.match(THREE_VIEW_PROMPT, /摄影机在水平面内绕产品中心移动45度/);
+  assert.match(THREE_VIEW_PROMPT, /roll为0/);
+  assert.match(THREE_VIEW_PROMPT, /禁止产品倾倒、侧翻、斜放、悬浮/);
+  assert.doesNotMatch(PRODUCT_SETTING_BOARD_PROMPT, /标题写|标注产品尺寸|所有中文标题/);
+  assert.match(PRODUCT_SETTING_BOARD_PROMPT, /不生成任何新增中文、英文、字母、数字/);
+});
+
+test("seven-image set uses specialized 1+2+2+2 contracts and only the two generated references", () => {
+  const runId = "run-20260804120000-aabbcc";
+  const generated = ["D:\\run\\three-view.png", "D:\\run\\setting-board.png"];
+  const brief = { instruction: "生成淘宝中文七图", platform: "taobao", aspectRatio: "1:1" };
+  const blueprint = compileSetBlueprint({ runId, generatedReferences: generated, brief, creativePlan: creativePlan() });
+  const plan = compileReferenceFirstSetPlan({ runId, generatedReferences: generated, brief, blueprint });
+  assert.equal(plan.plan_type, "reference_first_set");
+  assert.equal(plan.images.length, 7);
+  assert.deepEqual(blueprint.pages.map((page) => page.role), ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"]);
+  assert.ok(plan.images.every((image) => JSON.stringify(image.reference_images) === JSON.stringify(generated)));
+  assert.ok(plan.images.every((image) => !image.reference_images.includes("D:\\run\\product.png")));
+  assert.ok(plan.images.every((image) => /只负责生成整套七图中的第\d{2}张单图/.test(image.generation_prompt)));
+  assert.ok(plan.images.every((image) => image.generation_prompt.includes("目标平台设计策略:淘宝")));
+  assert.ok(plan.images.every((image) => image.generation_prompt.includes("产品三视图是商品基准")));
+  assert.ok(plan.images.every((image) => image.generation_prompt.includes("产品视觉设定板只用于场景")));
+  assert.ok(plan.images.every((image) => image.generation_prompt.includes("不生成任何新增标题")));
+  assert.ok(plan.images.every((image) => !/生成同一产品的电商套图|自由选择|自由安排|自由发挥/.test(image.generation_prompt)));
+  assert.equal(new Set(blueprint.pages.map((page) => page.template_id)).size, 7);
+});
+
+test("set compilation refuses to recreate the old generic template without a dynamic blueprint", () => {
+  assert.throws(() => compileReferenceFirstSetPlan({
+    runId: "run-20260804120000-aabbcc", generatedReferences: ["three", "board"], brief: {},
+  }), /REFERENCE_FIRST_SET_BLUEPRINT_REQUIRED/);
+});

+ 126 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-contract-schemas.mjs

@@ -0,0 +1,126 @@
+const object = (required, properties) => ({ type: "object", additionalProperties: true, required, properties });
+const string = { type: "string", minLength: 1 };
+const strings = { type: "array", items: string };
+const box = object(["x", "y", "width", "height"], Object.fromEntries(["x", "y", "width", "height"].map((key) => [key, { type: "number", minimum: 0, maximum: 1 }])));
+const region = object(["status", "boxes"], { status: { enum: ["verified", "unverifiable"] }, confidence: { type: ["number", "null"], minimum: 0, maximum: 1 }, boxes: { type: "array", items: box } });
+
+export const PRODUCT_CREATIVE_BRIEF_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/product-creative-brief/1.0",
+  ...object(["category", "form", "colors", "material_cues", "identity_cues", "visible_selling_point_candidates", "natural_support_and_use"], {
+    category: string, form: string, colors: strings, material_cues: strings, identity_cues: strings,
+    visible_selling_point_candidates: { type: "array", minItems: 2, items: string }, natural_support_and_use: { type: "array", minItems: 1, items: string },
+    human_interactions: strings, commercial_direction: string, creative_audience_hypothesis: { type: "string" }, unsupported_copy_facts: strings,
+  }),
+};
+
+export const SET_BLUEPRINT_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/set-blueprint/2.0",
+  ...object(["schema_version", "run_id", "platform_strategy", "product_creative_brief", "visual_bible", "pages"], {
+    schema_version: { const: "2.0" }, run_id: string, platform_strategy: object(["resolved_profile"], { resolved_profile: string }),
+    product_creative_brief: PRODUCT_CREATIVE_BRIEF_JSON_SCHEMA,
+    visual_bible: object(["commercial_style", "palette", "lighting", "material_source", "setting_board_role"], { commercial_style: string, palette: strings, lighting: string, material_source: string, setting_board_role: string }),
+    pages: { type: "array", minItems: 7, maxItems: 7, items: object(["id", "role", "template_id", "page_job", "scene", "product_pose", "camera", "composition", "text_safe_zone", "difference_from_peer"], {
+      id: string, role: string, template_id: string, page_job: string, scene: string, product_pose: string, camera: string,
+      human_relation: { type: "string" }, props: strings, composition: string, text_safe_zone: string, difference_from_peer: string,
+    }) },
+  }),
+};
+
+export const COPY_PLAN_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/copy-plan/1.0",
+  ...object(["schema_version", "pages", "inferred_product_facts_forbidden"], {
+    schema_version: { const: "1.0" }, inferred_product_facts_forbidden: { const: true },
+    pages: { type: "array", minItems: 7, maxItems: 7, items: object(["slot_id", "overlay_required", "source_refs"], { slot_id: string, overlay_required: { type: "boolean" }, source_refs: strings }) },
+  }),
+};
+
+export const LAYOUT_REGIONS_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/layout-regions/1.0",
+  ...object(["schema_version", "purpose", "pages"], {
+    schema_version: { const: "1.0" }, purpose: { const: "layout_geometry_not_content_review" },
+    pages: { type: "array", minItems: 7, maxItems: 7, items: object(["slot_id", "product_region", "recommended_text_safe_regions"], { slot_id: string, product_region: region, recommended_text_safe_regions: region }) },
+  }),
+};
+
+export const LAYOUT_REPORT_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/layout-report/1.0",
+  ...object(["schema_version", "status", "pages"], {
+    schema_version: { const: "1.0" }, status: { const: "complete" },
+    pages: { type: "array", minItems: 7, maxItems: 7, items: object(["slot_id", "selected_mode", "reason"], { slot_id: string, selected_mode: string, reason: string }) },
+  }),
+};
+
+const sourcedText = object(["text", "source_type"], {
+  text: string,
+  source_type: { enum: ["user_fact", "visible_observation", "creative_assumption"] },
+  source_ref: { type: "string" },
+});
+
+const visualProduct = object(["category", "form", "colors", "material_and_light_cues", "identity_cues", "natural_support_and_use"], {
+  category: string, product_name_or_visible_text: { type: "string" }, form: string, colors: strings,
+  material_and_light_cues: strings, identity_cues: { type: "array", minItems: 2, items: string },
+  visible_details: strings, visible_text: strings, natural_support_and_use: strings,
+  possible_human_interactions: strings, scene_affordance_hypotheses: strings,
+  uncertainties: strings, generation_risks: strings,
+});
+
+export const ORIGINAL_VISUAL_OBSERVATION_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/original-visual-observation/1.0",
+  ...object(["schema_version", "purpose", "run_id", "input_asset_sha256", "product"], {
+    schema_version: { const: "1.0" }, purpose: { const: "claude_visual_input_not_creative_plan" },
+    run_id: string, input_asset_sha256: string, product: visualProduct,
+  }),
+};
+
+export const REFERENCE_VISUAL_OBSERVATION_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/reference-visual-observation/1.0",
+  ...object(["schema_version", "purpose", "run_id", "input_assets_sha256", "assets"], {
+    schema_version: { const: "1.0" }, purpose: { const: "claude_reference_visual_input_not_creative_plan" },
+    run_id: string, input_assets_sha256: { type: "array", minItems: 2, maxItems: 2, items: string },
+    assets: { type: "array", minItems: 2, maxItems: 2, items: object(["asset_role", "product"], {
+      asset_role: { enum: ["three_view", "setting_board_visual"] }, product: visualProduct,
+      actual_composition_and_scenes: strings, observed_deviations: strings,
+    }) },
+  }),
+};
+
+export const PRODUCT_SETTING_BOARD_SPEC_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/product-setting-board-spec/1.0",
+  ...object(["schema_version", "authored_by", "run_id", "source_observation_sha256", "identity", "hero", "information_table", "product_story", "multi_views", "packaging_flow", "detail_breakdowns", "scenes", "visual_style"], {
+    schema_version: { const: "1.0" }, authored_by: { const: "claude" }, run_id: string,
+    source_observation_sha256: string,
+    identity: object(["main_title", "sub_title", "brand_series", "category"], { main_title: sourcedText, sub_title: sourcedText, brand_series: sourcedText, category: sourcedText }),
+    hero: object(["view", "focus_area", "appearance_cues", "lighting", "depth_of_field"], { view: string, focus_area: string, appearance_cues: strings, lighting: string, depth_of_field: string }),
+    information_table: { type: "array", minItems: 4, items: object(["label", "content"], { label: string, content: sourcedText }) },
+    product_story: sourcedText,
+    multi_views: { type: "array", minItems: 4, maxItems: 4, items: object(["intent", "label", "description"], { intent: string, label: string, description: string }) },
+    packaging_flow: { type: "array", minItems: 3, items: object(["level", "label", "content"], { level: string, label: string, content: sourcedText }) },
+    detail_breakdowns: { type: "array", minItems: 4, maxItems: 4, items: object(["intent", "label", "content"], { intent: string, label: string, content: sourcedText }) },
+    scenes: { type: "array", minItems: 4, maxItems: 4, items: object(["intent", "label", "content"], { intent: string, label: string, content: sourcedText }) },
+    visual_style: object(["commercial_direction", "palette", "materials", "lighting", "atmosphere", "typography_style"], { commercial_direction: string, palette: strings, materials: strings, lighting: string, atmosphere: string, typography_style: string }),
+  }),
+};
+
+export const CLAUDE_AUTHORED_SET_PLAN_JSON_SCHEMA = {
+  $id: "https://fmode.local/schemas/claude-authored-set-plan/3.0",
+  ...object(["schema_version", "authored_by", "run_id", "source_reference_observation_sha256", "platform", "pages"], {
+    schema_version: { const: "3.0" }, authored_by: { const: "claude" }, run_id: string,
+    source_reference_observation_sha256: string, platform: object(["requested_platform"], { requested_platform: string }),
+    pages: { type: "array", minItems: 7, maxItems: 7, items: object(["id", "role", "template_id", "page_job", "selection_reason", "platform_decisions", "prompt_modules", "difference_from_peer"], {
+      id: string, role: string, template_id: string, page_job: string, selection_reason: string,
+      platform_decisions: { type: "object" }, prompt_modules: { type: "object" }, difference_from_peer: string,
+    }) },
+  }),
+};
+
+export const REFERENCE_FIRST_CONTRACT_SCHEMAS = Object.freeze({
+  product_creative_brief: PRODUCT_CREATIVE_BRIEF_JSON_SCHEMA,
+  set_blueprint: SET_BLUEPRINT_JSON_SCHEMA,
+  copy_plan: COPY_PLAN_JSON_SCHEMA,
+  layout_regions: LAYOUT_REGIONS_JSON_SCHEMA,
+  layout_report: LAYOUT_REPORT_JSON_SCHEMA,
+  original_visual_observation: ORIGINAL_VISUAL_OBSERVATION_JSON_SCHEMA,
+  reference_visual_observation: REFERENCE_VISUAL_OBSERVATION_JSON_SCHEMA,
+  product_setting_board_spec: PRODUCT_SETTING_BOARD_SPEC_JSON_SCHEMA,
+  claude_authored_set_plan: CLAUDE_AUTHORED_SET_PLAN_JSON_SCHEMA,
+});

+ 14 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-contract-schemas.test.mjs

@@ -0,0 +1,14 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { REFERENCE_FIRST_CONTRACT_SCHEMAS } from "./reference-first-contract-schemas.mjs";
+
+test("all reference-first V2/V3 contracts have unique versioned JSON Schema identifiers", () => {
+  const schemas = Object.values(REFERENCE_FIRST_CONTRACT_SCHEMAS);
+  assert.equal(new Set(schemas.map((schema) => schema.$id)).size, schemas.length);
+  assert.ok(schemas.every((schema) => schema.type === "object" && Array.isArray(schema.required)));
+  assert.equal(REFERENCE_FIRST_CONTRACT_SCHEMAS.set_blueprint.properties.pages.minItems, 7);
+  assert.equal(REFERENCE_FIRST_CONTRACT_SCHEMAS.layout_regions.properties.purpose.const, "layout_geometry_not_content_review");
+  assert.equal(REFERENCE_FIRST_CONTRACT_SCHEMAS.original_visual_observation.properties.purpose.const, "claude_visual_input_not_creative_plan");
+  assert.equal(REFERENCE_FIRST_CONTRACT_SCHEMAS.product_setting_board_spec.properties.authored_by.const, "claude");
+  assert.equal(REFERENCE_FIRST_CONTRACT_SCHEMAS.claude_authored_set_plan.properties.pages.minItems, 7);
+});

+ 91 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-copy-plan.mjs

@@ -0,0 +1,91 @@
+import { writeFile } from "node:fs/promises";
+
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const asArray = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
+
+function normalizeSellingPoint(value, index) {
+  if (typeof value === "string") return { title: value.trim(), detail: "", source: `production.sellingPoints[${index}]` };
+  if (value && typeof value === "object") return {
+    title: text(value.title, text(value.name)), detail: text(value.detail, text(value.description)),
+    source: text(value.source, `production.sellingPoints[${index}]`),
+  };
+  return null;
+}
+
+function officialFacts(brief = {}) {
+  const sellingPoints = asArray(brief.sellingPoints).map(normalizeSellingPoint).filter((item) => item?.title);
+  const specifications = brief.specifications && typeof brief.specifications === "object"
+    ? Object.entries(brief.specifications).filter(([, value]) => text(String(value))).map(([key, value]) => ({ key, value: String(value), source: `production.specifications.${key}` }))
+    : [];
+  return { sellingPoints, specifications, officialCopy: asArray(brief.officialCopy).map(String).filter(Boolean) };
+}
+
+const creativeFallbacks = {
+  selling_point: [
+    { title: "轮廓清晰可见", subtitle: "从整体看见设计层次" },
+    { title: "细节值得近看", subtitle: "让质感回到真实光影" },
+  ],
+  usage_scene: [
+    { title: "自然融入日常", subtitle: "在真实关系中看见使用状态" },
+    { title: "使用,自然发生", subtitle: "动作与商品保持清晰联系" },
+  ],
+  purchase_decision: [
+    { title: "比例更直观", subtitle: "借助真实关系理解视觉尺度" },
+    { title: "从整体到选择", subtitle: "重要视觉信息清晰收束" },
+  ],
+};
+
+export function validateReferenceFirstCopyPlan(plan) {
+  if (plan?.schema_version !== "1.0") throw new Error("REFERENCE_FIRST_COPY_PLAN_VERSION_UNSUPPORTED");
+  if (!Array.isArray(plan.pages) || plan.pages.length !== 7) throw new Error("REFERENCE_FIRST_COPY_PLAN_SEVEN_PAGES_REQUIRED");
+  for (const page of plan.pages) {
+    if (page.slot_id === "01") continue;
+    if (!text(page.copy?.title)) throw new Error(`REFERENCE_FIRST_COPY_TITLE_REQUIRED:${page.slot_id}`);
+    if (page.copy.title.length > 24 || text(page.copy.subtitle).length > 48) throw new Error(`REFERENCE_FIRST_COPY_EXCESS_LENGTH:${page.slot_id}`);
+    if (!Array.isArray(page.source_refs) || page.source_refs.length === 0) throw new Error(`REFERENCE_FIRST_COPY_SOURCE_REQUIRED:${page.slot_id}`);
+  }
+  return plan;
+}
+
+export function compileReferenceFirstCopyPlan({ brief = {}, blueprint } = {}) {
+  if (!blueprint?.pages || blueprint.pages.length !== 7) throw new Error("REFERENCE_FIRST_COPY_BLUEPRINT_REQUIRED");
+  const facts = officialFacts(brief);
+  const productName = text(brief.productName);
+  const roleIndexes = { selling_point: 0, usage_scene: 0, purchase_decision: 0 };
+  const pages = blueprint.pages.map((page) => {
+    if (page.id === "01") return { slot_id: page.id, role: page.role, copy: null, source_refs: [], overlay_required: false };
+    const roleIndex = roleIndexes[page.role]++;
+    const explicit = page.role === "selling_point" ? facts.sellingPoints[roleIndex] : null;
+    const fallback = creativeFallbacks[page.role][roleIndex];
+    const title = explicit?.title || fallback.title;
+    const subtitle = explicit?.detail || fallback.subtitle;
+    return {
+      slot_id: page.id,
+      role: page.role,
+      template_id: page.template_id,
+      copy: {
+        eyebrow: productName || (page.role === "selling_point" ? "产品亮点" : page.role === "usage_scene" ? "使用场景" : "购买参考"),
+        title,
+        subtitle,
+      },
+      source_refs: explicit ? [explicit.source] : ["project.non_fact_creative_copy"],
+      overlay_required: true,
+    };
+  });
+  return validateReferenceFirstCopyPlan({
+    schema_version: "1.0",
+    platform_profile: blueprint.platform_strategy?.resolved_profile || "general_ecommerce",
+    platform_copy_density: blueprint.platform_strategy?.profile_snapshot?.copy_density || "中等信息密度",
+    target_language: text(brief.targetLanguage, "zh-CN"),
+    product_name: productName || null,
+    facts: { selling_points: facts.sellingPoints, specifications: facts.specifications, official_copy: facts.officialCopy },
+    inferred_product_facts_forbidden: true,
+    pages,
+  });
+}
+
+export async function writeReferenceFirstCopyPlan({ brief, blueprint, outputFile } = {}) {
+  const plan = compileReferenceFirstCopyPlan({ brief, blueprint });
+  if (outputFile) await writeFile(outputFile, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
+  return plan;
+}

+ 25 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-copy-plan.test.mjs

@@ -0,0 +1,25 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { compileReferenceFirstCopyPlan } from "./reference-first-copy-plan.mjs";
+
+const pages = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"]
+  .map((role, index) => ({ id: String(index + 1).padStart(2, "0"), role, template_id: `template-${index}` }));
+
+test("copy plan keeps the white page clean and uses explicit selling points with source refs", () => {
+  const plan = compileReferenceFirstCopyPlan({
+    brief: { productName: "测试商品", sellingPoints: [{ title: "用户卖点一", detail: "用户证据一" }, "用户卖点二"] },
+    blueprint: { pages },
+  });
+  assert.equal(plan.pages[0].overlay_required, false);
+  assert.equal(plan.pages[0].copy, null);
+  assert.equal(plan.pages[1].copy.title, "用户卖点一");
+  assert.equal(plan.pages[2].copy.title, "用户卖点二");
+  assert.match(plan.pages[1].source_refs[0], /production\.sellingPoints/);
+});
+
+test("copy plan fills absent user facts only with explicitly non-factual creative copy", () => {
+  const plan = compileReferenceFirstCopyPlan({ brief: {}, blueprint: { pages } });
+  assert.ok(plan.pages.slice(1).every((page) => page.source_refs[0] === "project.non_fact_creative_copy"));
+  assert.doesNotMatch(JSON.stringify(plan.pages), /厘米|功效|成分|容量|性能/);
+  assert.equal(plan.inferred_product_facts_forbidden, true);
+});

+ 73 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-creative-planner.mjs

@@ -0,0 +1,73 @@
+import { createHash } from "node:crypto";
+import { readFile, writeFile } from "node:fs/promises";
+import { compileSetBlueprint, creativePlannerInstruction } from "./reference-first-set-blueprint.mjs";
+import { resolvePlatformDesignProfile } from "./platform-design-profile-registry.mjs";
+import { loadVisionConfig, requestVisionJson } from "../../providers/commerce-vision-provider.mjs";
+
+const sha256 = (value) => createHash("sha256").update(String(value)).digest("hex");
+
+async function readOptionalJson(file) {
+  if (!file) return null;
+  try { return JSON.parse((await readFile(file, "utf8")).replace(/^\uFEFF/, "")); }
+  catch (error) { if (error?.code === "ENOENT") return null; throw error; }
+}
+
+function planningDigest(runId, references, brief) {
+  return sha256(JSON.stringify({ run_id: runId, references, brief }));
+}
+
+export async function planReferenceFirstCreative({
+  runId,
+  generatedReferences,
+  brief = {},
+  outputFile = "",
+  config = null,
+  requestJson = requestVisionJson,
+  loadConfig = loadVisionConfig,
+} = {}) {
+  const references = Array.isArray(generatedReferences) ? generatedReferences.filter(Boolean) : [];
+  if (!runId || references.length !== 2) throw new Error("REFERENCE_FIRST_CREATIVE_PLANNER_INPUT_REQUIRED");
+  const inputDigest = planningDigest(runId, references, brief);
+  const existing = await readOptionalJson(outputFile);
+  if (existing?.planning?.input_sha256 === inputDigest && existing?.schema_version === "2.0") {
+    return { blueprint: existing, reused: true, model_calls: 0, usage: existing.planning.usage || null };
+  }
+  const effectiveConfig = config || await loadConfig();
+  if (!effectiveConfig?.apiKey) throw new Error("CREATIVE_PLANNER_NOT_CONFIGURED");
+  const platformResolution = resolvePlatformDesignProfile(brief.platform);
+  const baseInstruction = creativePlannerInstruction({ brief, platformResolution });
+  let lastError;
+  let usage = null;
+  for (let semanticAttempt = 1; semanticAttempt <= 2; semanticAttempt += 1) {
+    try {
+      const response = await requestJson({
+        config: effectiveConfig,
+        imageUrls: references,
+        instruction: semanticAttempt === 1
+          ? baseInstruction
+          : `${baseInstruction}\n\n上一次输出未通过结构校验:${lastError?.message || "未知结构错误"}。请重新输出完整JSON并严格修复该问题。`,
+        temperature: 0.35,
+      });
+      usage = response.usage || usage;
+      const blueprint = compileSetBlueprint({ runId, generatedReferences: references, brief, creativePlan: response.value });
+      blueprint.planning = {
+        purpose: "creative_planning_not_content_review",
+        provider: effectiveConfig.provider,
+        model: effectiveConfig.model,
+        input_sha256: inputDigest,
+        semantic_attempts: semanticAttempt,
+        usage,
+        generated_at: new Date().toISOString(),
+      };
+      if (outputFile) await writeFile(outputFile, `${JSON.stringify(blueprint, null, 2)}\n`, "utf8");
+      return { blueprint, reused: false, model_calls: semanticAttempt, usage };
+    } catch (error) {
+      lastError = error;
+      if (String(error?.message || "").includes("VISION_PROVIDER_NOT_CONFIGURED")) break;
+    }
+  }
+  const error = new Error(`CREATIVE_PLANNING_FAILED:${lastError?.message || "unknown error"}`);
+  error.model_calls = 2;
+  error.usage = usage;
+  throw error;
+}

+ 57 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-creative-planner.test.mjs

@@ -0,0 +1,57 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { mkdtemp, rm } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { planReferenceFirstCreative } from "./reference-first-creative-planner.mjs";
+
+function responseValue() {
+  const ids = ["white_complete", "selling_operation", "selling_material", "scene_lifestyle", "scene_human_interaction", "decision_usage_threshold", "decision_value_summary"];
+  return {
+    product_creative_brief: {
+      category: "小型商品", form: "立体", colors: ["黑色"], material_cues: ["哑光"], identity_cues: ["正面标志"],
+      visible_selling_point_candidates: ["操作区", "表面纹理"], natural_support_and_use: ["桌面自然承载"], human_interactions: ["手部操作"],
+      commercial_direction: "现代商业摄影", unsupported_copy_facts: ["尺寸"],
+    },
+    visual_bible: { commercial_style: "现代", palette: ["黑", "灰"], lighting: "柔光", prop_language: "克制" },
+    pages: ids.map((template_id, index) => ({
+      template_id, selection_reason: `选择${index}`, page_job: `任务${index}`, scene: `场景${index}`, product_pose: `姿态${index}`,
+      camera: `机位${index}`, human_relation: `人物关系${index}`, props: [`道具${index}`], composition: `构图${index}`,
+      text_safe_zone: `留白${index}`, difference_from_peer: `差异${index}`,
+    })),
+  };
+}
+
+test("creative planner semantically repairs invalid JSON and persists a reusable blueprint", async () => {
+  const directory = await mkdtemp(join(tmpdir(), "creative-plan-"));
+  const outputFile = join(directory, "set-blueprint.json");
+  let calls = 0;
+  try {
+    const first = await planReferenceFirstCreative({
+      runId: "run-20260804120000-aabbcc", generatedReferences: ["https://test/three.png", "https://test/board.png"],
+      brief: { platform: "taobao", instruction: "生成七图" }, outputFile,
+      config: { apiKey: "test", provider: "mock", model: "mock-vl" },
+      requestJson: async () => {
+        calls += 1;
+        return { value: calls === 1 ? { pages: [] } : responseValue(), usage: { total_tokens: 100 } };
+      },
+    });
+    assert.equal(first.model_calls, 2);
+    assert.equal(first.blueprint.planning.purpose, "creative_planning_not_content_review");
+    const reused = await planReferenceFirstCreative({
+      runId: "run-20260804120000-aabbcc", generatedReferences: ["https://test/three.png", "https://test/board.png"],
+      brief: { platform: "taobao", instruction: "生成七图" }, outputFile,
+      config: { apiKey: "test" }, requestJson: async () => { throw new Error("must not be called"); },
+    });
+    assert.equal(reused.reused, true);
+    assert.equal(reused.model_calls, 0);
+    assert.equal(calls, 2);
+  } finally { await rm(directory, { recursive: true, force: true }); }
+});
+
+test("creative planner requires configuration instead of silently falling back to a static template", async () => {
+  await assert.rejects(() => planReferenceFirstCreative({
+    runId: "run-20260804120000-aabbcc", generatedReferences: ["a", "b"], brief: {},
+    loadConfig: async () => ({ apiKey: "" }),
+  }), /CREATIVE_PLANNER_NOT_CONFIGURED/);
+});

+ 104 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-design-postprocessor.mjs

@@ -0,0 +1,104 @@
+import { createHash } from "node:crypto";
+import { mkdir, readFile, writeFile } from "node:fs/promises";
+import { isAbsolute, join, relative, resolve } from "node:path";
+import sharp from "sharp";
+import { compositeAdaptiveOverlay } from "../designed-asset/adaptive-overlay-compositor.mjs";
+
+const sha256 = (buffer) => createHash("sha256").update(buffer).digest("hex");
+
+const tokenProfiles = {
+  taobao: { primary: "#24211F", accent: "#C58A3A", lightText: "#FFFFFF", background: "#F4F1EC" },
+  jd: { primary: "#202733", accent: "#D83A3A", lightText: "#FFFFFF", background: "#F5F6F8" },
+  douyin_ecommerce: { primary: "#18181B", accent: "#25F4EE", lightText: "#FFFFFF", background: "#F5F5F5" },
+  pinduoduo: { primary: "#3A1F1F", accent: "#E02E24", lightText: "#FFFFFF", background: "#FFF7F5" },
+  general_ecommerce: { primary: "#222222", accent: "#C58A3A", lightText: "#FFFFFF", background: "#F4F1EC" },
+};
+
+const outputNames = [
+  "01-白底商品基准图.png", "02-卖点图一.png", "03-卖点图二.png", "04-场景图一.png",
+  "05-场景图二.png", "06-购买决策图一.png", "07-购买决策图二.png",
+];
+
+function insideRun(file, runDirectory) {
+  const absolute = resolve(file);
+  const pathFromRoot = relative(resolve(runDirectory), absolute);
+  if (!pathFromRoot || pathFromRoot.startsWith("..") || isAbsolute(pathFromRoot)) throw new Error("REFERENCE_FIRST_DESIGN_SOURCE_OUTSIDE_RUN");
+  return absolute;
+}
+
+function dimensions(aspectRatio = "1:1", longSide = 1200) {
+  const match = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(String(aspectRatio));
+  if (!match) throw new Error("REFERENCE_FIRST_DESIGN_ASPECT_RATIO_INVALID");
+  const horizontal = Number(match[1]); const vertical = Number(match[2]);
+  return horizontal >= vertical
+    ? { width: longSide, height: Math.round(longSide * vertical / horizontal) }
+    : { width: Math.round(longSide * horizontal / vertical), height: longSide };
+}
+
+export async function runReferenceFirstDesignPostprocessor({
+  runDirectory, outputDirectory, baseImages, blueprint, copyPlan, layoutRegions,
+  aspectRatio = "1:1", layoutReportFile = "",
+} = {}) {
+  if (!runDirectory || !outputDirectory) throw new Error("REFERENCE_FIRST_DESIGN_PATH_REQUIRED");
+  if (!Array.isArray(baseImages) || baseImages.length !== 7) throw new Error("REFERENCE_FIRST_DESIGN_SEVEN_BASE_IMAGES_REQUIRED");
+  if (!blueprint?.pages || blueprint.pages.length !== 7 || !copyPlan?.pages || copyPlan.pages.length !== 7 || !layoutRegions?.pages || layoutRegions.pages.length !== 7) {
+    throw new Error("REFERENCE_FIRST_DESIGN_INPUT_CONTRACT_REQUIRED");
+  }
+  const platform = blueprint.platform_strategy.resolved_profile;
+  const tokens = tokenProfiles[platform] || tokenProfiles.general_ecommerce;
+  const size = dimensions(aspectRatio);
+  const directory = resolve(outputDirectory);
+  await mkdir(directory, { recursive: true });
+  const files = [];
+  const reportPages = [];
+  for (let index = 0; index < 7; index += 1) {
+    const source = insideRun(baseImages[index], runDirectory);
+    const blueprintPage = blueprint.pages[index];
+    const copyPage = copyPlan.pages[index];
+    const regionPage = layoutRegions.pages[index];
+    if (blueprintPage.id !== copyPage.slot_id || blueprintPage.id !== regionPage.slot_id) throw new Error(`REFERENCE_FIRST_DESIGN_SLOT_MISMATCH:${blueprintPage.id}`);
+    const page = {
+      id: blueprintPage.id, role: blueprintPage.role, template_id: blueprintPage.template_id,
+      slot_id: blueprintPage.id, platform_profile: platform,
+      layout_recipe: blueprintPage.template_id, copy: copyPage.copy, overlay_required: copyPage.overlay_required,
+      content_regions: regionPage,
+    };
+    const composed = await compositeAdaptiveOverlay({ source, page, tokens, size, allowPanelFallback: true });
+    const file = join(directory, outputNames[index]);
+    await writeFile(file, composed.buffer);
+    const metadata = await sharp(composed.buffer).metadata();
+    files.push({
+      file, slot_id: blueprintPage.id, role: blueprintPage.role, template_id: blueprintPage.template_id,
+      source_base_image: source, source_sha256: sha256(await readFile(source)), output_sha256: sha256(composed.buffer),
+      width: metadata.width, height: metadata.height, copy: copyPage.copy, copy_source_refs: copyPage.source_refs,
+      overlay_decision: composed.decision,
+    });
+    reportPages.push({
+      slot_id: blueprintPage.id, role: blueprintPage.role, template_id: blueprintPage.template_id,
+      selected_mode: composed.decision.mode, reason: composed.decision.reason, selected_box: composed.decision.box,
+      panel_height: composed.decision.panel_height || null,
+      typography: composed.decision.typography ? {
+        font: composed.decision.typography.font,
+        title: composed.decision.typography.title,
+        subtitle: composed.decision.typography.subtitle,
+      } : null,
+      collision_policy: "product_face_body_hand_logo_original_text_and_critical_regions_excluded",
+    });
+  }
+  const report = {
+    schema_version: "1.0", status: "complete", platform_profile: platform,
+    output_size: size, pages: reportPages, generated_at: new Date().toISOString(),
+  };
+  if (layoutReportFile) await writeFile(layoutReportFile, `${JSON.stringify(report, null, 2)}\n`, "utf8");
+  const manifest = {
+    schema_version: "1.0", status: "complete", output_level: "platform_designed_image_set_v2",
+    platform_profile: platform, blueprint_schema_version: blueprint.schema_version,
+    copy_plan_schema_version: copyPlan.schema_version, layout_regions_schema_version: layoutRegions.schema_version,
+    layout_report: layoutReportFile || null, files,
+    content_quality_gate_enabled: false,
+    limitations: ["区域识别仅用于排版,不代表商品真实性或发布质量审核"],
+  };
+  const manifestFile = join(directory, "platform-designed-image-set-manifest.json");
+  await writeFile(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
+  return { ...manifest, manifest_file: manifestFile, layout_report_data: report };
+}

+ 46 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-design-postprocessor.test.mjs

@@ -0,0 +1,46 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import sharp from "sharp";
+import { runReferenceFirstDesignPostprocessor } from "./reference-first-design-postprocessor.mjs";
+
+test("postprocessor produces seven platform-designed images with safe-region and panel fallbacks", async () => {
+  const runDirectory = await mkdtemp(join(tmpdir(), "design-post-"));
+  const outputDirectory = join(runDirectory, "设计成品V2");
+  const baseImages = [];
+  try {
+    for (let index = 0; index < 7; index += 1) {
+      const file = join(runDirectory, `${index + 1}.png`);
+      await writeFile(file, await sharp({ create: { width: 500, height: 500, channels: 3, background: "#DAD4CC" } }).png().toBuffer());
+      baseImages.push(file);
+    }
+    const roles = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"];
+    const blueprint = {
+      schema_version: "2.0", platform_strategy: { resolved_profile: "taobao" },
+      pages: roles.map((role, index) => ({ id: String(index + 1).padStart(2, "0"), role, template_id: `template-${index}` })),
+    };
+    const copyPlan = { schema_version: "1.0", pages: roles.map((role, index) => ({
+      slot_id: String(index + 1).padStart(2, "0"), role, overlay_required: index !== 0,
+      copy: index === 0 ? null : { eyebrow: "测试", title: `完整标题${index}`, subtitle: "完整中文副标题" }, source_refs: ["test"],
+    })) };
+    const layoutRegions = { schema_version: "1.0", pages: roles.map((_, index) => ({
+      slot_id: String(index + 1).padStart(2, "0"),
+      product_region: { status: "verified", boxes: [{ x: 0.45, y: 0.15, width: 0.5, height: 0.7 }] },
+      face_regions: { status: "unverifiable", boxes: [] }, body_and_hand_regions: { status: "unverifiable", boxes: [] },
+      critical_product_regions: { status: "verified", boxes: [{ x: 0.5, y: 0.25, width: 0.2, height: 0.2 }] },
+      logo_and_original_text_regions: { status: "unverifiable", boxes: [] },
+      recommended_text_safe_regions: index === 4 ? { status: "unverifiable", boxes: [] } : { status: "verified", boxes: [{ x: 0.03, y: 0.06, width: 0.35, height: 0.35 }] },
+    })) };
+    const result = await runReferenceFirstDesignPostprocessor({
+      runDirectory, outputDirectory, baseImages, blueprint, copyPlan, layoutRegions,
+      aspectRatio: "1:1", layoutReportFile: join(runDirectory, "layout-report.json"),
+    });
+    assert.equal(result.files.length, 7);
+    assert.equal(result.files[0].overlay_decision.reason, "page_has_no_copy");
+    assert.equal(result.files[4].overlay_decision.mode, "extended_panel");
+    assert.ok(result.files.slice(1).every((file) => file.copy.title && file.copy_source_refs.length));
+    assert.equal(result.layout_report_data.pages.length, 7);
+  } finally { await rm(runDirectory, { recursive: true, force: true }); }
+});

+ 104 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-layout-region-detector.mjs

@@ -0,0 +1,104 @@
+import { createHash } from "node:crypto";
+import { readFile, writeFile } from "node:fs/promises";
+import { loadVisionConfig, requestVisionJson } from "../../providers/commerce-vision-provider.mjs";
+
+const sha256 = (value) => createHash("sha256").update(String(value)).digest("hex");
+
+async function readOptional(file) {
+  if (!file) return null;
+  try { return JSON.parse((await readFile(file, "utf8")).replace(/^\uFEFF/, "")); }
+  catch (error) { if (error?.code === "ENOENT") return null; throw error; }
+}
+
+function validBox(value) {
+  const box = { x: Number(value?.x), y: Number(value?.y), width: Number(value?.width), height: Number(value?.height) };
+  if (![box.x, box.y, box.width, box.height].every(Number.isFinite)
+    || box.x < 0 || box.y < 0 || box.width <= 0 || box.height <= 0
+    || box.x + box.width > 1.001 || box.y + box.height > 1.001) {
+    throw new Error(`LAYOUT_REGION_BOX_INVALID:${JSON.stringify(value)}`);
+  }
+  return box;
+}
+
+function region(value, { required = false } = {}) {
+  const boxes = Array.isArray(value?.boxes) ? value.boxes.map(validBox) : [];
+  const status = value?.status === "verified" && boxes.length ? "verified" : "unverifiable";
+  if (required && status !== "verified") throw new Error("LAYOUT_REGION_REQUIRED_BOX_MISSING");
+  return { status, boxes, confidence: Number.isFinite(Number(value?.confidence)) ? Number(value.confidence) : null };
+}
+
+export function normalizeLayoutRegions(value, imageUrls) {
+  const rawPages = Array.isArray(value?.pages) ? value.pages : [];
+  if (rawPages.length !== 7) throw new Error("LAYOUT_REGIONS_SEVEN_PAGES_REQUIRED");
+  const pages = rawPages.map((page, index) => {
+    const slotId = String(index + 1).padStart(2, "0");
+    if (String(page.slot_id || slotId).padStart(2, "0") !== slotId) throw new Error(`LAYOUT_REGION_SLOT_ORDER_INVALID:${slotId}`);
+    return {
+      slot_id: slotId,
+      image_url: imageUrls[index],
+      image_sha256: sha256(imageUrls[index]),
+      product_region: region(page.product_region, { required: true }),
+      face_regions: region(page.face_regions),
+      body_and_hand_regions: region(page.body_and_hand_regions),
+      critical_product_regions: region(page.critical_product_regions),
+      logo_and_original_text_regions: region(page.logo_and_original_text_regions),
+      recommended_text_safe_regions: region(page.recommended_text_safe_regions),
+      background: {
+        brightness: String(page.background?.brightness || "mixed"),
+        contrast: String(page.background?.contrast || "mixed"),
+        texture_density: String(page.background?.texture_density || "mixed"),
+      },
+    };
+  });
+  return { schema_version: "1.0", purpose: "layout_geometry_not_content_review", pages };
+}
+
+export function layoutRegionInstruction() {
+  return `你只负责分析七张电商视觉底图的几何排版区域,不审核商品真假、图片美学或是否可发布。七张图片按01到07顺序输入。只输出JSON,不要Markdown。
+输出结构:{"pages":[七个对象]}。每个对象必须包含slot_id,以及以下区域对象:product_region、face_regions、body_and_hand_regions、critical_product_regions、logo_and_original_text_regions、recommended_text_safe_regions。每个区域对象结构为{"status":"verified或unverifiable","confidence":0到1,"boxes":[{"x":0到1,"y":0到1,"width":0到1,"height":0到1}]}。坐标相对整张图归一化,框不得越界。
+product_region必须框住完整商品。face_regions框人脸,body_and_hand_regions框与商品相关的人体和手,critical_product_regions框商品关键轮廓/结构,logo_and_original_text_regions框商品自身Logo和包装原字。recommended_text_safe_regions只能提供低细节、无遮挡且不会压住上述区域的矩形;没有可靠安全区时标记unverifiable并返回空boxes。每页还要包含background:brightness、contrast、texture_density。不要输出任何商品事实判断。`;
+}
+
+export async function detectReferenceFirstLayoutRegions({
+  runId,
+  imageUrls,
+  outputFile = "",
+  config = null,
+  requestJson = requestVisionJson,
+  loadConfig = loadVisionConfig,
+} = {}) {
+  const urls = Array.isArray(imageUrls) ? imageUrls.filter(Boolean) : [];
+  if (!runId || urls.length !== 7) throw new Error("LAYOUT_REGION_DETECTOR_SEVEN_IMAGES_REQUIRED");
+  const inputDigest = sha256(JSON.stringify({ run_id: runId, image_urls: urls }));
+  const existing = await readOptional(outputFile);
+  if (existing?.detection?.input_sha256 === inputDigest && existing?.schema_version === "1.0") {
+    return { regions: existing, reused: true, model_calls: 0, usage: existing.detection.usage || null };
+  }
+  const effectiveConfig = config || await loadConfig();
+  if (!effectiveConfig?.apiKey) throw new Error("LAYOUT_REGION_DETECTOR_NOT_CONFIGURED");
+  let lastError;
+  let usage = null;
+  for (let semanticAttempt = 1; semanticAttempt <= 2; semanticAttempt += 1) {
+    try {
+      const response = await requestJson({
+        config: effectiveConfig, imageUrls: urls,
+        instruction: semanticAttempt === 1 ? layoutRegionInstruction() : `${layoutRegionInstruction()}\n上一次结构错误:${lastError?.message}。请完整修复并重新输出。`,
+        temperature: 0.05,
+      });
+      usage = response.usage || usage;
+      const regions = normalizeLayoutRegions(response.value, urls);
+      regions.run_id = runId;
+      regions.detection = {
+        provider: effectiveConfig.provider, model: effectiveConfig.model,
+        input_sha256: inputDigest, semantic_attempts: semanticAttempt, usage,
+        detected_at: new Date().toISOString(),
+      };
+      if (outputFile) await writeFile(outputFile, `${JSON.stringify(regions, null, 2)}\n`, "utf8");
+      return { regions, reused: false, model_calls: semanticAttempt, usage };
+    } catch (error) { lastError = error; }
+  }
+  const error = new Error(`LAYOUT_REGION_DETECTION_FAILED:${lastError?.message || "unknown error"}`);
+  error.model_calls = 2;
+  error.usage = usage;
+  throw error;
+}

+ 36 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-layout-region-detector.test.mjs

@@ -0,0 +1,36 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { detectReferenceFirstLayoutRegions, normalizeLayoutRegions } from "./reference-first-layout-region-detector.mjs";
+
+const urls = Array.from({ length: 7 }, (_, index) => `https://test/${index + 1}.png`);
+const validValue = () => ({ pages: urls.map((_, index) => ({
+  slot_id: String(index + 1).padStart(2, "0"),
+  product_region: { status: "verified", confidence: 0.9, boxes: [{ x: 0.4, y: 0.2, width: 0.45, height: 0.65 }] },
+  face_regions: { status: "unverifiable", boxes: [] }, body_and_hand_regions: { status: "unverifiable", boxes: [] },
+  critical_product_regions: { status: "verified", boxes: [{ x: 0.45, y: 0.25, width: 0.3, height: 0.3 }] },
+  logo_and_original_text_regions: { status: "verified", boxes: [{ x: 0.5, y: 0.35, width: 0.12, height: 0.08 }] },
+  recommended_text_safe_regions: { status: "verified", confidence: 0.8, boxes: [{ x: 0.04, y: 0.08, width: 0.28, height: 0.3 }] },
+  background: { brightness: "light", contrast: "low", texture_density: "low" },
+})) });
+
+test("layout detector is geometry-only and reuses a valid result", async () => {
+  let calls = 0;
+  const result = await detectReferenceFirstLayoutRegions({
+    runId: "run-20260804120000-aabbcc", imageUrls: urls,
+    config: { apiKey: "x", provider: "mock", model: "mock" },
+    requestJson: async () => { calls += 1; return { value: validValue(), usage: { total_tokens: 10 } }; },
+  });
+  assert.equal(result.regions.purpose, "layout_geometry_not_content_review");
+  assert.equal(result.regions.pages.length, 7);
+  assert.equal(result.regions.pages[0].recommended_text_safe_regions.status, "verified");
+  assert.equal(calls, 1);
+});
+
+test("layout coordinates reject overflow and missing product boxes", () => {
+  const overflow = validValue();
+  overflow.pages[0].product_region.boxes[0] = { x: 0.9, y: 0.2, width: 0.2, height: 0.2 };
+  assert.throws(() => normalizeLayoutRegions(overflow, urls), /LAYOUT_REGION_BOX_INVALID/);
+  const missing = validValue();
+  missing.pages[0].product_region = { status: "unverifiable", boxes: [] };
+  assert.throws(() => normalizeLayoutRegions(missing, urls), /LAYOUT_REGION_REQUIRED_BOX_MISSING/);
+});

+ 148 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-page-template-registry.mjs

@@ -0,0 +1,148 @@
+const freeze = (value) => Object.freeze(value);
+
+function recipe(id, family, pageGoal, visualEvidence, cameraLanguage, relation, composition, safeZone) {
+  return freeze({ id, family, page_goal: pageGoal, visual_evidence: visualEvidence, camera_language: cameraLanguage, relation, composition, safe_zone: safeZone });
+}
+
+export const REFERENCE_FIRST_PAGE_TEMPLATES = freeze({
+  white_complete: recipe(
+    "white_complete", "white_background", "建立商品基准识别",
+    "完整商品外形、准确颜色、Logo与包装原有识别元素清晰可见",
+    "接近平视的正面或轻微三分之四观察,全景,透视克制",
+    "商品自然稳定地落在支撑面,无遮挡、无人物、无道具",
+    "纯白背景,商品居中,主体约占画面合理范围,四周保留均衡呼吸空间",
+    "不规划营销文字;只保留商品自身原有文字"
+  ),
+  selling_form: recipe(
+    "selling_form", "selling_point", "证明完整形态、轮廓或结构优势",
+    "用一张画面同时看清商品整体和最具识别力的可见结构",
+    "三分之四全景或中近景,结构方向清晰,避免与白底正面机位重复",
+    "商品保持自然承载,辅助道具只解释结构或功能关系",
+    "主体偏一侧,另一侧形成低细节信息区;前中后景服务结构层次",
+    "在不遮挡轮廓、Logo和关键结构的一侧预留中等标题区"
+  ),
+  selling_material: recipe(
+    "selling_material", "selling_point", "证明材质、工艺、表面或关键局部",
+    "整体识别锚点与一个真实可见局部同时出现,局部纹理不改变原材质",
+    "微距或近景,侧向柔光呈现纹理和光泽;保留足够整体轮廓用于身份识别",
+    "商品不变形,不用特效伪造纹理;布光变化不能表现为另一种材质",
+    "局部细节为主视觉中心,整体商品作为次级锚点,背景低干扰",
+    "在纹理主视觉相反方向预留短标题和一句证据短句区域"
+  ),
+  selling_operation: recipe(
+    "selling_operation", "selling_point", "证明关键操作、接口或功能关系",
+    "可见操作区域、接口、开合或使用触点与完整商品关系明确",
+    "中近景,观察轴正对操作关系,避免为了戏剧性遮挡关键部位",
+    "允许局部手部示范,但手不能覆盖关键结构和品牌识别",
+    "动作路径清晰,一页只说明一个操作结论",
+    "在动作路径之外预留结构化信息区"
+  ),
+  selling_brand: recipe(
+    "selling_brand", "selling_point", "强化包装视觉与品牌识别",
+    "商品自身真实 Logo、品牌标识、配色和包装原字保持可读且一致",
+    "正面或轻微侧向中近景,光线均匀,避免反光破坏原字",
+    "不新增假包装、赠品或包装清单",
+    "品牌识别区域清晰,商品仍是唯一主角",
+    "远离包装原字预留后置短标题区域"
+  ),
+  scene_core_environment: recipe(
+    "scene_core_environment", "usage_scene", "展示商品最自然的核心使用空间",
+    "商品、承载面和环境关系符合参考设定所表达的使用方式",
+    "环境中景或中近景,空间层次完整,机位与卖点页明显不同",
+    "商品稳定处于自然位置,道具数量受控且与使用直接相关",
+    "建立明确前中后景,商品位于视觉焦点,环境不抢主体",
+    "在低细节墙面、天空、地面或侧边空间预留中等信息区"
+  ),
+  scene_human_interaction: recipe(
+    "scene_human_interaction", "usage_scene", "展示人物与商品的真实使用关系",
+    "人物动作、接触部位、商品姿态和使用结果在单帧内可理解",
+    "动作中景或局部身体近景,快门感自然,避免摆拍僵硬",
+    "人物只服务商品使用,手部或身体不能不合理穿透、覆盖或扭曲商品",
+    "动作线与商品形成视觉关系,人物脸部不是画面唯一中心",
+    "避开人脸、手部和商品互动区域预留短标题区"
+  ),
+  scene_lifestyle: recipe(
+    "scene_lifestyle", "usage_scene", "建立与商品匹配的日常生活方式",
+    "环境材质、道具和光线支持商品调性,但不引入无关叙事",
+    "较宽环境镜头,允许浅景深,商品仍清晰可识别",
+    "商品处于正在被使用或随时可使用的自然状态",
+    "氛围层次丰富但只有一个商品视觉中心",
+    "利用自然负空间预留低到中等密度文案区"
+  ),
+  scene_mobile_outdoor: recipe(
+    "scene_mobile_outdoor", "usage_scene", "解释商品在移动或户外状态中的使用",
+    "携带、穿戴、手持或环境承载关系完整可信",
+    "动态三分之四或跟随视角,地平线水平,商品不因动势而变形",
+    "人物动作符合重力、人体和商品使用逻辑",
+    "环境动势与商品方向协调,商品颜色和材质不受滤镜改变",
+    "在运动方向前方或背景低细节区预留短文案"
+  ),
+  scene_installed: recipe(
+    "scene_installed", "usage_scene", "展示安装、摆放或环境融合关系",
+    "商品与墙面、桌面、地面、设备或空间的连接方式清楚",
+    "平视或轻俯视中景,空间基准线稳定",
+    "安装/摆放遵循参考设定,不虚构无法支持的内部结构",
+    "商品与环境边界清晰,参照物用于解释位置和尺度",
+    "在连接关系之外预留结构化说明区"
+  ),
+  decision_scale_fit: recipe(
+    "decision_scale_fit", "purchase_decision", "帮助判断尺度、体积、穿戴或空间适配",
+    "通过真实人物、手部、常见参照物或空间比例表达相对尺度,不生成具体数字",
+    "接近平视的中景或全景,透视克制,参照关系可比较",
+    "参照物不能遮挡商品关键轮廓,人物姿态自然",
+    "商品和参照物形成明确比例组,避免装饰性杂物",
+    "预留适合后置尺度说明的规则信息区"
+  ),
+  decision_usage_threshold: recipe(
+    "decision_usage_threshold", "purchase_decision", "回答操作门槛或典型使用障碍",
+    "一个清晰的使用前后关系、操作触点或空间门槛视觉结论",
+    "正对关系的中近景,不使用复杂多格拼图",
+    "动作、商品和环境形成单一因果关系,不虚构性能结果",
+    "主次层级清晰,避免一页同时解释多个问题",
+    "预留标题加两行以内说明区域"
+  ),
+  decision_context_match: recipe(
+    "decision_context_match", "purchase_decision", "帮助判断商品与目标环境或使用方式是否匹配",
+    "同时呈现完整商品和一个关键环境关系,避免无依据兼容性声明",
+    "稳定三分之四中景,环境基准清晰",
+    "商品自然处于目标环境,关系来自创意简报的可支持线索",
+    "商品与目标空间形成清楚边界和视觉对比",
+    "在环境负空间预留结构化判断信息区"
+  ),
+  decision_value_summary: recipe(
+    "decision_value_summary", "purchase_decision", "以完整商品和核心价值完成详情页收束",
+    "完整商品、核心识别元素和前页视觉语言在一张单图中形成统一结论",
+    "稳定完整视角,避免复制白底图;通过场景光线或构图完成收束",
+    "不虚构附件、赠品、包装清单或参数",
+    "主体层级明确、画面完整、结尾感强,保留一个清晰信息区",
+    "预留适合后置主标题与简短行动提示的区域"
+  ),
+});
+
+export const DEFAULT_PAGE_TEMPLATE_IDS = freeze([
+  "white_complete", "selling_form", "selling_material", "scene_core_environment",
+  "scene_human_interaction", "decision_scale_fit", "decision_value_summary",
+]);
+
+export function pageTemplate(id) {
+  const value = REFERENCE_FIRST_PAGE_TEMPLATES[String(id || "")];
+  if (!value) throw new Error(`REFERENCE_FIRST_PAGE_TEMPLATE_UNKNOWN:${String(id)}`);
+  return value;
+}
+
+export function templatesForFamily(family) {
+  return Object.values(REFERENCE_FIRST_PAGE_TEMPLATES).filter((value) => value.family === family);
+}
+
+export function pageTemplatePrompt(template) {
+  if (!template?.id) throw new Error("REFERENCE_FIRST_PAGE_TEMPLATE_REQUIRED");
+  return [
+    `页面定位:${template.family};页面配方:${template.id}。`,
+    `页面任务:${template.page_goal}。`,
+    `必须形成的视觉证据:${template.visual_evidence}。`,
+    `摄影语言:${template.camera_language}。`,
+    `商品、人物与环境关系:${template.relation}。`,
+    `构图要求:${template.composition}。`,
+    `后置文字预留:${template.safe_zone}。该区域只是生成构图目标,不是已经验证的叠字安全区。`,
+  ].join("\n");
+}

+ 36 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-page-template-registry.test.mjs

@@ -0,0 +1,36 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import {
+  DEFAULT_PAGE_TEMPLATE_IDS,
+  pageTemplate,
+  pageTemplatePrompt,
+  REFERENCE_FIRST_PAGE_TEMPLATES,
+  templatesForFamily,
+} from "./reference-first-page-template-registry.mjs";
+
+test("page template registry provides one white page and specialized recipe families", () => {
+  assert.equal(templatesForFamily("white_background").length, 1);
+  assert.ok(templatesForFamily("selling_point").length >= 4);
+  assert.ok(templatesForFamily("usage_scene").length >= 4);
+  assert.ok(templatesForFamily("purchase_decision").length >= 4);
+  assert.deepEqual(DEFAULT_PAGE_TEMPLATE_IDS.map((id) => pageTemplate(id).family), [
+    "white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision",
+  ]);
+  assert.equal(new Set(DEFAULT_PAGE_TEMPLATE_IDS).size, 7);
+});
+
+test("every specialized recipe compiles a complete positioning contract", () => {
+  for (const template of Object.values(REFERENCE_FIRST_PAGE_TEMPLATES)) {
+    const prompt = pageTemplatePrompt(template);
+    assert.match(prompt, /页面任务/);
+    assert.match(prompt, /视觉证据/);
+    assert.match(prompt, /摄影语言/);
+    assert.match(prompt, /商品、人物与环境关系/);
+    assert.match(prompt, /构图要求/);
+    assert.match(prompt, /不是已经验证的叠字安全区/);
+  }
+});
+
+test("unknown recipes fail explicitly instead of falling back to the generic template", () => {
+  assert.throws(() => pageTemplate("shoe-desk-special"), /REFERENCE_FIRST_PAGE_TEMPLATE_UNKNOWN/);
+});

+ 179 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-pipeline.integration.test.mjs

@@ -0,0 +1,179 @@
+import assert from "node:assert/strict";
+import { execFile } from "node:child_process";
+import { createServer } from "node:http";
+import { readFile, rm } from "node:fs/promises";
+import { promisify } from "node:util";
+import { fileURLToPath } from "node:url";
+import test from "node:test";
+import sharp from "sharp";
+
+const execFileAsync = promisify(execFile);
+const runnerFile = fileURLToPath(new URL("./image-set-runner.mjs", import.meta.url));
+
+test("one start directly generates two reference assets then seven images", async () => {
+  const submissions = [];
+  let visionCalls = 0;
+  const imageBuffer = await sharp({ create: { width: 512, height: 512, channels: 3, background: "#D8D2C8" } }).png().toBuffer();
+  const server = createServer(async (request, response) => {
+    const base = `http://127.0.0.1:${server.address().port}`;
+    if (request.method === "POST" && request.url === "/submit") {
+      let body = "";
+      for await (const chunk of request) body += chunk;
+      const payload = JSON.parse(body);
+      const slot = payload.client_request_id.split(":").pop();
+      submissions.push({ slot, image_urls: payload.image_urls, prompt: payload.prompt });
+      response.writeHead(200, { "Content-Type": "application/json" });
+      response.end(JSON.stringify({ code: 200, data: { workId: `work-${slot}` } }));
+      return;
+    }
+    if (request.method === "GET" && request.url?.startsWith("/result/work-")) {
+      const slot = request.url.split("work-")[1];
+      response.writeHead(200, { "Content-Type": "application/json" });
+      response.end(JSON.stringify({ images: [`${base}/image/${slot}.png`] }));
+      return;
+    }
+    if (request.method === "GET" && request.url?.startsWith("/image/")) {
+      response.writeHead(200, { "Content-Type": "image/png" });
+      response.end(imageBuffer);
+      return;
+    }
+    if (request.method === "POST" && request.url === "/vision/chat/completions") {
+      visionCalls += 1;
+      let body = "";
+      for await (const chunk of request) body += chunk;
+      const payload = JSON.parse(body);
+      const imageUrls = payload.messages[0].content.filter((item) => item.type === "image_url").map((item) => item.image_url.url);
+      if (imageUrls.length === 7) {
+        const pages = imageUrls.map((_, index) => ({
+          slot_id: String(index + 1).padStart(2, "0"),
+          product_region: { status: "verified", confidence: 0.9, boxes: [{ x: 0.45, y: 0.15, width: 0.5, height: 0.7 }] },
+          face_regions: { status: "unverifiable", boxes: [] }, body_and_hand_regions: { status: "unverifiable", boxes: [] },
+          critical_product_regions: { status: "verified", boxes: [{ x: 0.52, y: 0.25, width: 0.22, height: 0.22 }] },
+          logo_and_original_text_regions: { status: "verified", boxes: [{ x: 0.58, y: 0.32, width: 0.12, height: 0.07 }] },
+          recommended_text_safe_regions: { status: "verified", confidence: 0.85, boxes: [{ x: 0.03, y: 0.06, width: 0.35, height: 0.35 }] },
+          background: { brightness: "light", contrast: "low", texture_density: "low" },
+        }));
+        response.writeHead(200, { "Content-Type": "application/json" });
+        response.end(JSON.stringify({ choices: [{ message: { content: JSON.stringify({ pages }) } }], usage: { total_tokens: 100 } }));
+        return;
+      }
+      assert.deepEqual(imageUrls, [`${base}/image/three-view.png`, `${base}/image/setting-board.png`]);
+      const ids = ["white_complete", "selling_form", "selling_material", "scene_core_environment", "scene_human_interaction", "decision_scale_fit", "decision_value_summary"];
+      const value = {
+        product_creative_brief: {
+          category: "测试商品", form: "完整商品", colors: ["准确原色"], material_cues: ["参考可见纹理"], identity_cues: ["轮廓", "Logo"],
+          visible_selling_point_candidates: ["完整结构", "表面纹理"], natural_support_and_use: ["自然承载"], human_interactions: ["自然使用"],
+          commercial_direction: "淘宝商业摄影", unsupported_copy_facts: ["精确尺寸"],
+        },
+        visual_bible: { commercial_style: "淘宝商业摄影", palette: ["原色", "中性色"], lighting: "柔和统一", prop_language: "克制" },
+        pages: ids.map((template_id, index) => ({
+          template_id, selection_reason: `理由${index}`, page_job: `页面任务${index}`, scene: `具体场景${index}`,
+          product_pose: `稳定姿态${index}`, camera: `独立机位${index}`, human_relation: `人物关系${index}`, props: [`道具${index}`],
+          composition: `构图${index}`, text_safe_zone: `留白${index}`, difference_from_peer: `差异${index}`,
+        })),
+      };
+      response.writeHead(200, { "Content-Type": "application/json" });
+      response.end(JSON.stringify({ choices: [{ message: { content: JSON.stringify(value) } }], usage: { total_tokens: 200 } }));
+      return;
+    }
+    response.writeHead(404);
+    response.end();
+  });
+
+  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
+  const base = `http://127.0.0.1:${server.address().port}`;
+  let runDirectory = "";
+  try {
+    const original = "https://images.example.test/original-product.png";
+    const { stdout } = await execFileAsync(process.execPath, [
+      runnerFile,
+      "start",
+      "--image", original,
+      "--production-brief-json", JSON.stringify({
+        instruction: "生成淘宝中文七图",
+        platform: "taobao",
+        market: "CN",
+        targetLanguage: "zh-CN",
+        aspectRatio: "1:1",
+        orchestrationMode: "legacy_internal_planner",
+      }),
+      "--output-level", "visual_base",
+    ], {
+      env: {
+        ...process.env,
+        JIMENG_TOKEN: "one-click-test-token",
+        JIMENG_SUBMIT_URL: `${base}/submit`,
+        JIMENG_QUERY_URL: `${base}/query`,
+        JIMENG_RESULT_BASE_URL: `${base}/result`,
+        JIMENG_PREFLIGHT_URL: "",
+        COMMERCE_VISION_API_KEY: "creative-test-key",
+        COMMERCE_VISION_BASE_URL: `${base}/vision`,
+        COMMERCE_VISION_MODEL: "mock-vl",
+      },
+      maxBuffer: 1024 * 1024 * 10,
+    });
+    const result = JSON.parse(stdout);
+    runDirectory = result.paths.run_directory;
+    assert.equal(result.status, "complete");
+    assert.equal(result.workflow, "reference_first_platform_v2");
+    assert.equal(result.summary.completed, 9);
+    assert.equal(result.original_reference_used_for_set, false);
+    assert.equal(result.content_review_enabled, false);
+    assert.equal(result.validation_enabled, false);
+    assert.equal(result.reference_capture.vision_called, false);
+    assert.deepEqual(submissions.map((item) => item.slot), [
+      "three-view", "setting-board", "01", "02", "03", "04", "05", "06", "07",
+    ]);
+    assert.ok(submissions.slice(0, 2).every((item) => JSON.stringify(item.image_urls) === JSON.stringify([original])));
+    const expectedGeneratedReferences = [`${base}/image/three-view.png`, `${base}/image/setting-board.png`];
+    assert.ok(submissions.slice(2).every((item) => JSON.stringify(item.image_urls) === JSON.stringify(expectedGeneratedReferences)));
+    assert.ok(submissions.slice(2).every((item) => !item.image_urls.includes(original)));
+    assert.equal(visionCalls, 2);
+    assert.equal(result.creative_planning.status, "complete");
+    assert.equal(result.creative_planning.model_calls, 1);
+    assert.equal(result.layout_geometry.status, "complete");
+    assert.equal(result.layout_geometry.model_calls, 1);
+    assert.equal(result.designed_set.files.length, 7);
+    assert.equal(result.summary.designed_images_completed, 7);
+
+    const setPlan = JSON.parse(await readFile(result.paths.prompt_plan, "utf8"));
+    assert.ok(setPlan.images.every((image) => JSON.stringify(image.reference_images) === JSON.stringify(expectedGeneratedReferences)));
+    assert.doesNotMatch(JSON.stringify(setPlan), /original-product\.png/);
+    assert.deepEqual(setPlan.set_blueprint.platform_strategy.resolved_profile, "taobao");
+    assert.deepEqual(setPlan.set_blueprint.difference_matrix.map((page) => page.template_id), [
+      "selling_form", "selling_material", "scene_core_environment", "scene_human_interaction", "decision_scale_fit", "decision_value_summary",
+    ]);
+    assert.ok(setPlan.images.every((image) => /只负责生成整套七图中的第\d{2}张单图/.test(image.generation_prompt)));
+    assert.ok(setPlan.images.every((image) => image.generation_prompt.includes("目标平台设计策略:淘宝")));
+
+    const submissionsBeforeResume = submissions.length;
+    const { stdout: resumeStdout } = await execFileAsync(process.execPath, [
+      runnerFile,
+      "resume",
+      "--run-id", result.paths.run_id,
+    ], {
+      env: {
+        ...process.env,
+        JIMENG_TOKEN: "one-click-test-token",
+        JIMENG_SUBMIT_URL: `${base}/submit`,
+        JIMENG_QUERY_URL: `${base}/query`,
+        JIMENG_RESULT_BASE_URL: `${base}/result`,
+        JIMENG_PREFLIGHT_URL: "",
+        COMMERCE_VISION_API_KEY: "creative-test-key",
+        COMMERCE_VISION_BASE_URL: `${base}/vision`,
+        COMMERCE_VISION_MODEL: "mock-vl",
+      },
+      maxBuffer: 1024 * 1024 * 10,
+    });
+    const resumed = JSON.parse(resumeStdout);
+    assert.equal(resumed.status, "complete");
+    assert.equal(resumed.summary.completed, 9);
+    assert.equal(submissions.length, submissionsBeforeResume);
+    assert.equal(visionCalls, 2);
+    assert.equal(resumed.creative_planning.reused, true);
+    assert.equal(resumed.layout_geometry.reused, true);
+  } finally {
+    await new Promise((resolve) => server.close(resolve));
+    if (runDirectory) await rm(runDirectory, { recursive: true, force: true });
+  }
+});

+ 153 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-set-blueprint.mjs

@@ -0,0 +1,153 @@
+import { createHash } from "node:crypto";
+import { DEFAULT_PAGE_TEMPLATE_IDS, pageTemplate } from "./reference-first-page-template-registry.mjs";
+import { resolvePlatformDesignProfile } from "./platform-design-profile-registry.mjs";
+
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const array = (value) => Array.isArray(value) ? value.map((item) => text(item)).filter(Boolean) : [];
+const digest = (value) => createHash("sha256").update(String(value)).digest("hex");
+
+function normalizeCreativeBrief(value = {}) {
+  const result = {
+    category: text(value.category, "未明确类别商品"),
+    form: text(value.form, "以三视图可见完整形态为准"),
+    colors: array(value.colors),
+    material_cues: array(value.material_cues),
+    identity_cues: array(value.identity_cues),
+    visible_selling_point_candidates: array(value.visible_selling_point_candidates),
+    natural_support_and_use: array(value.natural_support_and_use),
+    human_interactions: array(value.human_interactions),
+    commercial_direction: text(value.commercial_direction, "真实商业产品摄影"),
+    creative_audience_hypothesis: text(value.creative_audience_hypothesis),
+    unsupported_copy_facts: array(value.unsupported_copy_facts),
+  };
+  if (result.colors.length === 0) result.colors.push("颜色严格以三视图为准");
+  if (result.material_cues.length === 0) result.material_cues.push("材质和光泽严格以三视图为准");
+  if (result.identity_cues.length === 0) result.identity_cues.push("商品轮廓、比例、Logo和包装原有识别元素");
+  if (result.visible_selling_point_candidates.length < 2) throw new Error("CREATIVE_BRIEF_TWO_VISIBLE_SELLING_POINTS_REQUIRED");
+  if (result.natural_support_and_use.length === 0) throw new Error("CREATIVE_BRIEF_NATURAL_USE_REQUIRED");
+  return result;
+}
+
+function normalizePage(page = {}, index) {
+  const id = String(index + 1).padStart(2, "0");
+  const templateId = text(page.template_id, DEFAULT_PAGE_TEMPLATE_IDS[index]);
+  const template = pageTemplate(templateId);
+  return {
+    id,
+    role: template.family,
+    template_id: template.id,
+    selection_reason: text(page.selection_reason, `该配方用于完成${template.page_goal}`),
+    page_job: text(page.page_job, template.page_goal),
+    scene: text(page.scene, index === 0 ? "纯白无缝背景" : "依据视觉设定板选择与商品自然使用关系一致的具体物理空间"),
+    product_pose: text(page.product_pose, index === 0 ? "完整商品自然稳定居中,无遮挡" : template.relation),
+    camera: text(page.camera, template.camera_language),
+    human_relation: text(page.human_relation, template.family === "usage_scene" ? "根据商品自然使用方式建立明确人物关系" : "人物仅在有助于本页任务时出现"),
+    props: array(page.props),
+    composition: text(page.composition, template.composition),
+    text_safe_zone: text(page.text_safe_zone, template.safe_zone),
+    difference_from_peer: text(page.difference_from_peer, index === 0 ? "白底基准页无同定位页面" : "与同定位另一页采用不同任务、镜头、关系和构图"),
+  };
+}
+
+export function validateSetBlueprint(blueprint) {
+  if (blueprint?.schema_version !== "2.0") throw new Error("SET_BLUEPRINT_VERSION_UNSUPPORTED");
+  if (!Array.isArray(blueprint.pages) || blueprint.pages.length !== 7) throw new Error("SET_BLUEPRINT_SEVEN_PAGES_REQUIRED");
+  const expected = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"];
+  if (blueprint.pages.some((page, index) => page.id !== String(index + 1).padStart(2, "0") || page.role !== expected[index])) {
+    throw new Error("SET_BLUEPRINT_ROLE_ALLOCATION_INVALID");
+  }
+  if (new Set(blueprint.pages.map((page) => page.template_id)).size !== 7) throw new Error("SET_BLUEPRINT_TEMPLATE_REPEATED");
+  for (const page of blueprint.pages) {
+    for (const key of ["page_job", "scene", "product_pose", "camera", "composition", "text_safe_zone", "difference_from_peer"]) {
+      if (!text(page[key])) throw new Error(`SET_BLUEPRINT_PAGE_FIELD_REQUIRED:${page.id}:${key}`);
+    }
+  }
+  for (const pair of [[1, 2], [3, 4], [5, 6]]) {
+    const left = blueprint.pages[pair[0]];
+    const right = blueprint.pages[pair[1]];
+    const differing = ["template_id", "page_job", "camera", "human_relation", "composition", "text_safe_zone"]
+      .filter((key) => JSON.stringify(left[key]) !== JSON.stringify(right[key])).length;
+    if (differing < 4) throw new Error(`SET_BLUEPRINT_PEER_DIFFERENCE_INSUFFICIENT:${left.role}`);
+  }
+  if (!blueprint.platform_strategy?.resolved_profile) throw new Error("SET_BLUEPRINT_PLATFORM_PROFILE_REQUIRED");
+  return blueprint;
+}
+
+export function compileSetBlueprint({ runId, generatedReferences, brief = {}, creativePlan = {} } = {}) {
+  if (!text(runId)) throw new Error("SET_BLUEPRINT_RUN_ID_REQUIRED");
+  const references = array(generatedReferences);
+  if (references.length !== 2) throw new Error("SET_BLUEPRINT_TWO_REFERENCES_REQUIRED");
+  const platform = resolvePlatformDesignProfile(brief.platform);
+  const creativeBrief = normalizeCreativeBrief(creativePlan.product_creative_brief || creativePlan.product || {});
+  const rawPages = Array.isArray(creativePlan.pages) ? creativePlan.pages : [];
+  if (rawPages.length !== 7) throw new Error("CREATIVE_PLAN_SEVEN_PAGES_REQUIRED");
+  const pages = rawPages.map(normalizePage);
+  const profile = platform.profile;
+  const blueprint = {
+    schema_version: "2.0",
+    run_id: runId,
+    input_asset_sha256: references.map(digest),
+    instruction: text(brief.instruction, "生成适合电商详情页的中文七图"),
+    platform_strategy: {
+      requested_platform: platform.requested_platform,
+      resolved_profile: platform.resolved_profile,
+      fallback_used: platform.fallback_used,
+      profile_version: profile.version,
+      profile_snapshot: profile,
+    },
+    product_creative_brief: creativeBrief,
+    visual_bible: {
+      commercial_style: text(creativePlan.visual_bible?.commercial_style, creativeBrief.commercial_direction),
+      palette: array(creativePlan.visual_bible?.palette).length ? array(creativePlan.visual_bible.palette) : creativeBrief.colors,
+      lighting: text(creativePlan.visual_bible?.lighting, profile.color_lighting),
+      prop_language: text(creativePlan.visual_bible?.prop_language, "道具克制且只解释商品关系"),
+      background_materials: array(creativePlan.visual_bible?.background_materials),
+      material_source: "商品形态、颜色、材质、纹理与光泽以产品三视图为唯一商品基准",
+      setting_board_role: "产品视觉设定板只用于场景、氛围、构图与使用关系参考",
+    },
+    pages,
+    difference_matrix: pages.slice(1).map((page) => ({ page_id: page.id, role: page.role, template_id: page.template_id, peer_difference: page.difference_from_peer })),
+    copy_policy: {
+      generation: "image_model_adds_no_new_text",
+      verified_sources: ["current_user_prompt", "user_supplied_official_material"],
+      inferred_product_facts_forbidden: true,
+    },
+  };
+  return validateSetBlueprint(blueprint);
+}
+
+export function creativePlannerInstruction({ brief = {}, platformResolution = resolvePlatformDesignProfile(brief.platform) } = {}) {
+  return `你是电商套图创意规划器,不是商品审核器。请根据输入的第一张产品三视图和第二张产品视觉设定板,为本次需求生成结构化设计方案。
+
+用户需求:${text(brief.instruction, "生成适合电商详情页的中文七图")}
+目标平台:${platformResolution.profile.label}
+平台设计方向:${platformResolution.profile.rhythm};${platformResolution.profile.composition};${platformResolution.profile.scene_human_weight}。
+
+只输出JSON对象,不要Markdown。结构必须为:
+{
+  "product_creative_brief": {
+    "category":"",
+    "form":"",
+    "colors":[""],
+    "material_cues":[""],
+    "identity_cues":[""],
+    "visible_selling_point_candidates":["至少两个仅根据可见画面提出的卖点候选"],
+    "natural_support_and_use":[""],
+    "human_interactions":[""],
+    "commercial_direction":"",
+    "creative_audience_hypothesis":"",
+    "unsupported_copy_facts":["不能从两张图证明的尺寸、性能、成分、功效等"]
+  },
+  "visual_bible": {"commercial_style":"","palette":[""],"lighting":"","prop_language":"","background_materials":[""]},
+  "pages":[七个页面对象]
+}
+
+七个页面按顺序必须使用以下定位与可选配方:
+01 white_background:white_complete
+02-03 selling_point:从 selling_form、selling_material、selling_operation、selling_brand 中选择两个不同配方
+04-05 usage_scene:从 scene_core_environment、scene_human_interaction、scene_lifestyle、scene_mobile_outdoor、scene_installed 中选择两个不同配方
+06-07 purchase_decision:从 decision_scale_fit、decision_usage_threshold、decision_context_match、decision_value_summary 中选择两个不同配方
+
+每个页面对象必须包含:template_id、selection_reason、page_job、scene、product_pose、camera、human_relation、props数组、composition、text_safe_zone、difference_from_peer。
+具体场景和动作必须适合当前商品;不要硬套不合理桌面、户外、穿戴或手持关系。六张非白底图不能只是同一模板换背景。同定位两页必须在任务、镜头、主体比例、人物/空间关系、视觉证据和文字区域中形成明确差异。图片后续独立生成,因此每页描述必须自足。不要把推断尺寸、材质、功效、成分、性能或包装清单写成商品事实。`;
+}

+ 56 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-set-blueprint.test.mjs

@@ -0,0 +1,56 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { compileSetBlueprint, creativePlannerInstruction, validateSetBlueprint } from "./reference-first-set-blueprint.mjs";
+
+function plan() {
+  const ids = ["white_complete", "selling_form", "selling_material", "scene_core_environment", "scene_human_interaction", "decision_scale_fit", "decision_value_summary"];
+  return {
+    product_creative_brief: {
+      category: "测试商品", form: "完整立体商品", colors: ["深色"], material_cues: ["哑光表面"], identity_cues: ["正面标识"],
+      visible_selling_point_candidates: ["完整轮廓", "表面纹理"], natural_support_and_use: ["自然稳定承载"],
+      human_interactions: ["自然手持"], commercial_direction: "现代商业产品摄影", unsupported_copy_facts: ["精确尺寸"],
+    },
+    visual_bible: { commercial_style: "现代克制", palette: ["深灰", "米白"], lighting: "柔和侧光", prop_language: "少量自然道具" },
+    pages: ids.map((template_id, index) => ({
+      template_id, selection_reason: `理由${index}`, page_job: `页面任务${index}`,
+      scene: index === 0 ? "纯白无缝背景" : `具体物理场景${index}`,
+      product_pose: `稳定商品姿态${index}`, camera: `独立摄影机位${index}`,
+      human_relation: `独立人物关系${index}`, props: [`道具${index}`],
+      composition: `独立构图${index}`, text_safe_zone: `独立留白${index}`,
+      difference_from_peer: `与同定位页面在任务、镜头、关系和构图上不同${index}`,
+    })),
+  };
+}
+
+test("dynamic blueprint combines product planning, specialized recipes and platform design", () => {
+  const blueprint = compileSetBlueprint({
+    runId: "run-20260804120000-aabbcc",
+    generatedReferences: ["https://example.test/three.png", "https://example.test/board.png"],
+    brief: { instruction: "生成淘宝七图", platform: "taobao" }, creativePlan: plan(),
+  });
+  assert.deepEqual(blueprint.pages.map((page) => page.role), [
+    "white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision",
+  ]);
+  assert.equal(new Set(blueprint.pages.map((page) => page.template_id)).size, 7);
+  assert.equal(blueprint.platform_strategy.resolved_profile, "taobao");
+  assert.match(blueprint.visual_bible.material_source, /三视图/);
+  assert.match(blueprint.visual_bible.setting_board_role, /场景/);
+});
+
+test("blueprint rejects repeated recipes and insufficient peer differentiation", () => {
+  const duplicate = compileSetBlueprint({ runId: "run-20260804120000-aabbcc", generatedReferences: ["a", "b"], brief: { platform: "jd" }, creativePlan: plan() });
+  duplicate.pages[2].template_id = duplicate.pages[1].template_id;
+  assert.throws(() => validateSetBlueprint(duplicate), /SET_BLUEPRINT_TEMPLATE_REPEATED/);
+  const same = compileSetBlueprint({ runId: "run-20260804120000-aabbcc", generatedReferences: ["a", "b"], brief: { platform: "jd" }, creativePlan: plan() });
+  for (const key of ["page_job", "camera", "human_relation", "composition", "text_safe_zone"]) same.pages[2][key] = same.pages[1][key];
+  assert.throws(() => validateSetBlueprint(same), /SET_BLUEPRINT_PEER_DIFFERENCE_INSUFFICIENT/);
+});
+
+test("creative planner instruction requires product-aware seven-page JSON rather than free directions", () => {
+  const instruction = creativePlannerInstruction({ brief: { instruction: "生成抖音七图", platform: "douyin" } });
+  assert.match(instruction, /visible_selling_point_candidates/);
+  assert.match(instruction, /selling_form/);
+  assert.match(instruction, /scene_human_interaction/);
+  assert.match(instruction, /decision_scale_fit/);
+  assert.match(instruction, /不能只是同一模板换背景/);
+});

+ 284 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-v3-contracts.mjs

@@ -0,0 +1,284 @@
+import { createHash } from "node:crypto";
+import { pageTemplate } from "./reference-first-page-template-registry.mjs";
+import { resolvePlatformDesignProfile } from "./platform-design-profile-registry.mjs";
+
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const list = (value) => Array.isArray(value) ? value.map((item) => text(item)).filter(Boolean) : [];
+const sha256 = (value) => createHash("sha256").update(String(value)).digest("hex");
+
+export const V3_WORKFLOW = "claude_authored_platform_v3";
+export const SOURCE_TYPES = Object.freeze(["user_fact", "visible_observation", "creative_assumption"]);
+export const PROMPT_MODULE_ORDER = Object.freeze([
+  "task_declaration", "reference_roles", "product_identity", "platform_design", "visual_bible",
+  "page_job", "scene_relation", "camera_composition", "lighting_material",
+  "text_safe_zone_intent", "peer_difference",
+]);
+
+function requireText(value, code) {
+  const result = text(value);
+  if (!result) throw new Error(code);
+  return result;
+}
+
+function requireList(value, code, minimum = 1) {
+  const result = list(value);
+  if (result.length < minimum) throw new Error(code);
+  return result;
+}
+
+function sourceType(value, code) {
+  if (!SOURCE_TYPES.includes(value)) throw new Error(code);
+  return value;
+}
+
+function sourcedText(value, code) {
+  if (!value || typeof value !== "object") throw new Error(code);
+  return {
+    text: requireText(value.text ?? value.value, `${code}:TEXT_REQUIRED`),
+    source_type: sourceType(value.source_type, `${code}:SOURCE_TYPE_INVALID`),
+    source_ref: text(value.source_ref, value.source_type === "creative_assumption" ? "claude.creative_assumption" : ""),
+  };
+}
+
+function normalizeVisualProduct(value = {}) {
+  return {
+    category: requireText(value.category, "VISUAL_OBSERVATION_CATEGORY_REQUIRED"),
+    product_name_or_visible_text: text(value.product_name_or_visible_text),
+    form: requireText(value.form, "VISUAL_OBSERVATION_FORM_REQUIRED"),
+    colors: requireList(value.colors, "VISUAL_OBSERVATION_COLORS_REQUIRED"),
+    material_and_light_cues: requireList(value.material_and_light_cues, "VISUAL_OBSERVATION_MATERIAL_REQUIRED"),
+    identity_cues: requireList(value.identity_cues, "VISUAL_OBSERVATION_IDENTITY_REQUIRED", 2),
+    visible_details: list(value.visible_details),
+    visible_text: list(value.visible_text),
+    natural_support_and_use: requireList(value.natural_support_and_use, "VISUAL_OBSERVATION_USE_REQUIRED"),
+    possible_human_interactions: list(value.possible_human_interactions),
+    scene_affordance_hypotheses: list(value.scene_affordance_hypotheses),
+    uncertainties: list(value.uncertainties),
+    generation_risks: list(value.generation_risks),
+  };
+}
+
+export function validateOriginalVisualObservation(value) {
+  if (value?.schema_version !== "1.0" || value?.purpose !== "claude_visual_input_not_creative_plan") {
+    throw new Error("ORIGINAL_VISUAL_OBSERVATION_CONTRACT_INVALID");
+  }
+  return {
+    ...value,
+    run_id: requireText(value.run_id, "ORIGINAL_VISUAL_OBSERVATION_RUN_ID_REQUIRED"),
+    input_asset_sha256: requireText(value.input_asset_sha256, "ORIGINAL_VISUAL_OBSERVATION_HASH_REQUIRED"),
+    product: normalizeVisualProduct(value.product),
+  };
+}
+
+export function validateReferenceVisualObservation(value) {
+  if (value?.schema_version !== "1.0" || value?.purpose !== "claude_reference_visual_input_not_creative_plan") {
+    throw new Error("REFERENCE_VISUAL_OBSERVATION_CONTRACT_INVALID");
+  }
+  if (!Array.isArray(value.assets) || value.assets.length !== 2) throw new Error("REFERENCE_VISUAL_OBSERVATION_TWO_ASSETS_REQUIRED");
+  return {
+    ...value,
+    run_id: requireText(value.run_id, "REFERENCE_VISUAL_OBSERVATION_RUN_ID_REQUIRED"),
+    input_assets_sha256: requireList(value.input_assets_sha256, "REFERENCE_VISUAL_OBSERVATION_HASHES_REQUIRED", 2),
+    assets: value.assets.map((asset, index) => ({
+      asset_role: index === 0 ? "three_view" : "setting_board_visual",
+      product: normalizeVisualProduct(asset?.product || asset),
+      actual_composition_and_scenes: list(asset?.actual_composition_and_scenes),
+      observed_deviations: list(asset?.observed_deviations),
+    })),
+  };
+}
+
+function normalizeBoardItem(value, code, extra = {}) {
+  if (!value || typeof value !== "object") throw new Error(code);
+  return { ...extra, ...value, label: requireText(value.label, `${code}:LABEL_REQUIRED`), content: sourcedText(value.content ?? value, `${code}:CONTENT`) };
+}
+
+export function validateProductSettingBoardSpec(value, { runId = "" } = {}) {
+  if (value?.schema_version !== "1.0" || value?.authored_by !== "claude") throw new Error("SETTING_BOARD_SPEC_CONTRACT_INVALID");
+  if (runId && value.run_id !== runId) throw new Error("SETTING_BOARD_SPEC_RUN_ID_MISMATCH");
+  const identity = value.identity || {};
+  const hero = value.hero || {};
+  const style = value.visual_style || {};
+  const informationTable = Array.isArray(value.information_table) ? value.information_table : [];
+  const multiViews = Array.isArray(value.multi_views) ? value.multi_views : [];
+  const packaging = Array.isArray(value.packaging_flow) ? value.packaging_flow : [];
+  const details = Array.isArray(value.detail_breakdowns) ? value.detail_breakdowns : [];
+  const scenes = Array.isArray(value.scenes) ? value.scenes : [];
+  if (informationTable.length < 4) throw new Error("SETTING_BOARD_INFORMATION_TABLE_FOUR_REQUIRED");
+  if (multiViews.length !== 4) throw new Error("SETTING_BOARD_FOUR_VIEWS_REQUIRED");
+  if (packaging.length < 3) throw new Error("SETTING_BOARD_PACKAGING_FLOW_REQUIRED");
+  if (details.length !== 4) throw new Error("SETTING_BOARD_FOUR_DETAILS_REQUIRED");
+  if (scenes.length !== 4) throw new Error("SETTING_BOARD_FOUR_SCENES_REQUIRED");
+  return {
+    ...value,
+    run_id: requireText(value.run_id, "SETTING_BOARD_RUN_ID_REQUIRED"),
+    source_observation_sha256: requireText(value.source_observation_sha256, "SETTING_BOARD_OBSERVATION_HASH_REQUIRED"),
+    identity: {
+      main_title: sourcedText(identity.main_title, "SETTING_BOARD_MAIN_TITLE"),
+      sub_title: sourcedText(identity.sub_title, "SETTING_BOARD_SUB_TITLE"),
+      brand_series: sourcedText(identity.brand_series, "SETTING_BOARD_BRAND_SERIES"),
+      category: sourcedText(identity.category, "SETTING_BOARD_CATEGORY"),
+    },
+    hero: {
+      view: requireText(hero.view, "SETTING_BOARD_HERO_VIEW_REQUIRED"),
+      focus_area: requireText(hero.focus_area, "SETTING_BOARD_HERO_FOCUS_REQUIRED"),
+      appearance_cues: requireList(hero.appearance_cues, "SETTING_BOARD_HERO_CUES_REQUIRED"),
+      lighting: requireText(hero.lighting, "SETTING_BOARD_HERO_LIGHT_REQUIRED"),
+      depth_of_field: requireText(hero.depth_of_field, "SETTING_BOARD_HERO_DOF_REQUIRED"),
+    },
+    information_table: informationTable.map((item, index) => normalizeBoardItem(item, `SETTING_BOARD_INFO_${index + 1}`)),
+    product_story: sourcedText(value.product_story, "SETTING_BOARD_PRODUCT_STORY"),
+    multi_views: multiViews.map((item, index) => ({
+      intent: requireText(item.intent, `SETTING_BOARD_VIEW_${index + 1}_INTENT_REQUIRED`),
+      label: requireText(item.label, `SETTING_BOARD_VIEW_${index + 1}_LABEL_REQUIRED`),
+      description: requireText(item.description, `SETTING_BOARD_VIEW_${index + 1}_DESCRIPTION_REQUIRED`),
+    })),
+    measurements: Array.isArray(value.measurements) ? value.measurements.map((item, index) => normalizeBoardItem(item, `SETTING_BOARD_MEASUREMENT_${index + 1}`)) : [],
+    packaging_flow: packaging.map((item, index) => ({ level: requireText(item.level, `SETTING_BOARD_PACKAGE_${index + 1}_LEVEL_REQUIRED`), ...normalizeBoardItem(item, `SETTING_BOARD_PACKAGE_${index + 1}`) })),
+    detail_breakdowns: details.map((item, index) => ({ intent: requireText(item.intent, `SETTING_BOARD_DETAIL_${index + 1}_INTENT_REQUIRED`), ...normalizeBoardItem(item, `SETTING_BOARD_DETAIL_${index + 1}`) })),
+    scenes: scenes.map((item, index) => ({ intent: requireText(item.intent, `SETTING_BOARD_SCENE_${index + 1}_INTENT_REQUIRED`), ...normalizeBoardItem(item, `SETTING_BOARD_SCENE_${index + 1}`) })),
+    visual_style: {
+      commercial_direction: requireText(style.commercial_direction, "SETTING_BOARD_STYLE_DIRECTION_REQUIRED"),
+      palette: requireList(style.palette, "SETTING_BOARD_STYLE_PALETTE_REQUIRED"),
+      materials: requireList(style.materials, "SETTING_BOARD_STYLE_MATERIALS_REQUIRED"),
+      lighting: requireText(style.lighting, "SETTING_BOARD_STYLE_LIGHTING_REQUIRED"),
+      atmosphere: requireText(style.atmosphere, "SETTING_BOARD_STYLE_ATMOSPHERE_REQUIRED"),
+      typography_style: requireText(style.typography_style, "SETTING_BOARD_STYLE_TYPOGRAPHY_REQUIRED"),
+    },
+  };
+}
+
+export function validateClaudeReferenceAuthoring(value, { runId, observationSha256 } = {}) {
+  if (value?.schema_version !== "1.0" || value?.authored_by !== "claude") throw new Error("CLAUDE_REFERENCE_AUTHORING_CONTRACT_INVALID");
+  if (value.run_id !== runId) throw new Error("CLAUDE_REFERENCE_AUTHORING_RUN_ID_MISMATCH");
+  if (value.source_observation_sha256 !== observationSha256) throw new Error("CLAUDE_REFERENCE_AUTHORING_OBSERVATION_HASH_MISMATCH");
+  const prompt = requireText(value.setting_board_visual_prompt, "CLAUDE_SETTING_BOARD_PROMPT_REQUIRED");
+  if (!/16\s*:\s*9/.test(prompt)) throw new Error("CLAUDE_SETTING_BOARD_PROMPT_ASPECT_REQUIRED");
+  if ([...prompt].length < 500) throw new Error("CLAUDE_SETTING_BOARD_PROMPT_TOO_GENERIC");
+  return { ...value, setting_board_spec: validateProductSettingBoardSpec(value.setting_board_spec, { runId }), setting_board_visual_prompt: prompt };
+}
+
+function normalizePromptModules(value, pageId) {
+  if (!value || typeof value !== "object") throw new Error(`CLAUDE_PROMPT_MODULES_REQUIRED:${pageId}`);
+  const result = Object.fromEntries(PROMPT_MODULE_ORDER.map((key) => [key, requireText(value[key], `CLAUDE_PROMPT_MODULE_REQUIRED:${pageId}:${key}`)]));
+  result.negative_constraints = requireText(value.negative_constraints, `CLAUDE_PROMPT_MODULE_REQUIRED:${pageId}:negative_constraints`);
+  if (!new RegExp(`第?${pageId}.*(?:单图|一张)`).test(result.task_declaration) || !/(?:只生成|只负责|只输出)/.test(result.task_declaration)) {
+    throw new Error(`CLAUDE_PROMPT_SINGLE_PAGE_DECLARATION_REQUIRED:${pageId}`);
+  }
+  const joined = Object.values(result).join("\n");
+  if (/自由选择|自由安排|自由发挥|生成(?:一套|整套).*套图/.test(joined)) throw new Error(`CLAUDE_PROMPT_UNCERTAIN_EXPRESSION_REJECTED:${pageId}`);
+  return result;
+}
+
+function normalizePageCopy(value, pageId) {
+  if (pageId === "01") return { overlay_required: false, copy: null, source_refs: [] };
+  if (!value || value.overlay_required !== true || !value.copy) throw new Error(`CLAUDE_PAGE_COPY_REQUIRED:${pageId}`);
+  const copy = {
+    eyebrow: text(value.copy.eyebrow),
+    title: requireText(value.copy.title, `CLAUDE_PAGE_COPY_TITLE_REQUIRED:${pageId}`),
+    subtitle: text(value.copy.subtitle),
+  };
+  const sourceRefs = requireList(value.source_refs, `CLAUDE_PAGE_COPY_SOURCE_REQUIRED:${pageId}`);
+  return { overlay_required: true, copy, source_refs: sourceRefs };
+}
+
+export function validateClaudeAuthoredSetPlan(value, { runId = "", referenceObservationSha256 = "" } = {}) {
+  if (value?.schema_version !== "3.0" || value?.authored_by !== "claude") throw new Error("CLAUDE_SET_PLAN_CONTRACT_INVALID");
+  if (runId && value.run_id !== runId) throw new Error("CLAUDE_SET_PLAN_RUN_ID_MISMATCH");
+  if (referenceObservationSha256 && value.source_reference_observation_sha256 !== referenceObservationSha256) throw new Error("CLAUDE_SET_PLAN_REFERENCE_HASH_MISMATCH");
+  if (!Array.isArray(value.pages) || value.pages.length !== 7) throw new Error("CLAUDE_SET_PLAN_SEVEN_PAGES_REQUIRED");
+  const expectedRoles = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"];
+  const platform = resolvePlatformDesignProfile(value.platform?.requested_platform || value.platform?.resolved_profile);
+  const pages = value.pages.map((page, index) => {
+    const id = String(index + 1).padStart(2, "0");
+    if (page.id !== id || page.role !== expectedRoles[index]) throw new Error(`CLAUDE_SET_PLAN_ROLE_INVALID:${id}`);
+    const template = pageTemplate(page.template_id);
+    if (template.family !== page.role) throw new Error(`CLAUDE_SET_PLAN_TEMPLATE_FAMILY_INVALID:${id}`);
+    const decisions = page.platform_decisions || {};
+    const normalized = {
+      ...page,
+      id,
+      page_job: requireText(page.page_job, `CLAUDE_SET_PLAN_PAGE_JOB_REQUIRED:${id}`),
+      selection_reason: requireText(page.selection_reason, `CLAUDE_SET_PLAN_SELECTION_REASON_REQUIRED:${id}`),
+      platform_decisions: {
+        subject_scale: requireText(decisions.subject_scale, `CLAUDE_PLATFORM_DECISION_REQUIRED:${id}:subject_scale`),
+        composition: requireText(decisions.composition, `CLAUDE_PLATFORM_DECISION_REQUIRED:${id}:composition`),
+        scene_human_weight: requireText(decisions.scene_human_weight, `CLAUDE_PLATFORM_DECISION_REQUIRED:${id}:scene_human_weight`),
+        color_lighting: requireText(decisions.color_lighting, `CLAUDE_PLATFORM_DECISION_REQUIRED:${id}:color_lighting`),
+        copy_density: requireText(decisions.copy_density, `CLAUDE_PLATFORM_DECISION_REQUIRED:${id}:copy_density`),
+        safe_zone_bias: requireText(decisions.safe_zone_bias, `CLAUDE_PLATFORM_DECISION_REQUIRED:${id}:safe_zone_bias`),
+      },
+      prompt_modules: normalizePromptModules(page.prompt_modules, id),
+      difference_from_peer: requireText(page.difference_from_peer, `CLAUDE_SET_PLAN_PEER_DIFFERENCE_REQUIRED:${id}`),
+      ...normalizePageCopy(page, id),
+    };
+    return normalized;
+  });
+  if (new Set(pages.map((page) => page.template_id)).size !== 7) throw new Error("CLAUDE_SET_PLAN_TEMPLATE_REPEATED");
+  for (const [leftIndex, rightIndex] of [[1, 2], [3, 4], [5, 6]]) {
+    const left = pages[leftIndex]; const right = pages[rightIndex];
+    const fields = ["template_id", "page_job", "difference_from_peer"];
+    const moduleFields = ["scene_relation", "camera_composition", "text_safe_zone_intent"];
+    const differences = fields.filter((key) => left[key] !== right[key]).length
+      + moduleFields.filter((key) => left.prompt_modules[key] !== right.prompt_modules[key]).length;
+    if (differences < 4) throw new Error(`CLAUDE_SET_PLAN_PEER_DIFFERENCE_INSUFFICIENT:${left.role}`);
+  }
+  return {
+    ...value,
+    run_id: requireText(value.run_id, "CLAUDE_SET_PLAN_RUN_ID_REQUIRED"),
+    source_reference_observation_sha256: requireText(value.source_reference_observation_sha256, "CLAUDE_SET_PLAN_REFERENCE_HASH_REQUIRED"),
+    platform: {
+      requested_platform: platform.requested_platform,
+      resolved_profile: platform.resolved_profile,
+      fallback_used: platform.fallback_used,
+      profile_version: platform.profile.version,
+      role_weight_decisions: text(value.platform?.role_weight_decisions, "固定七页配额下,权重仅用于Claude解释页面表现强度,不改变1+2+2+2数量"),
+    },
+    product_creative_brief: value.product_creative_brief || {},
+    visual_bible: value.visual_bible || {},
+    pages,
+  };
+}
+
+function dimensionsForAspectRatio(value = "1:1") {
+  const match = /^(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)$/.exec(text(value, "1:1"));
+  if (!match) throw new Error("CLAUDE_SET_PLAN_ASPECT_RATIO_INVALID");
+  const horizontal = Number(match[1]); const vertical = Number(match[2]);
+  const align8 = (number) => Math.max(512, Math.round(number / 8) * 8);
+  if (horizontal === vertical) return { width: 2048, height: 2048 };
+  return horizontal > vertical ? { width: 2048, height: align8(2048 * vertical / horizontal) } : { width: align8(2048 * horizontal / vertical), height: 2048 };
+}
+
+export function assembleClaudeAuthoredPromptPlan({ plan, generatedReferences, aspectRatio = "1:1" } = {}) {
+  const references = list(generatedReferences);
+  if (references.length !== 2) throw new Error("CLAUDE_PROMPT_PLAN_TWO_REFERENCES_REQUIRED");
+  const validated = validateClaudeAuthoredSetPlan(plan);
+  const size = dimensionsForAspectRatio(aspectRatio);
+  return {
+    schema_version: "0.3",
+    plan_type: "claude_authored_reference_first_set",
+    workflow: V3_WORKFLOW,
+    status: "ready",
+    run_id: validated.run_id,
+    authored_by: "claude",
+    source_plan_sha256: sha256(JSON.stringify(validated)),
+    request: { reference_images: [], aspect_ratio: aspectRatio, platform: validated.platform.resolved_profile },
+    images: validated.pages.map((page) => ({
+      id: page.id,
+      role: page.role,
+      template_id: page.template_id,
+      generation_prompt: PROMPT_MODULE_ORDER.map((key) => page.prompt_modules[key]).join("\n\n"),
+      negative_prompt: page.prompt_modules.negative_constraints,
+      reference_images: [...references],
+      model_parameters: { adapter: "jimeng_img_v4", width: size.width, height: size.height, scale: 0.6, force_single: true },
+      copy: page.copy,
+      source_refs: page.source_refs,
+      platform_decisions: page.platform_decisions,
+    })),
+  };
+}
+
+export function contractSha256(value) {
+  return sha256(JSON.stringify(value));
+}
+

+ 54 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-v3-contracts.test.mjs

@@ -0,0 +1,54 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { assembleClaudeAuthoredPromptPlan, contractSha256, validateClaudeAuthoredSetPlan, validateProductSettingBoardSpec, V3_WORKFLOW } from "./reference-first-v3-contracts.mjs";
+import { boardSpec, setPlan } from "./reference-first-v3-fixtures.mjs";
+
+test("product setting board contract keeps reusable structure and requires Claude-authored dynamic content", () => {
+  const spec = validateProductSettingBoardSpec(boardSpec());
+  assert.equal(spec.authored_by, "claude");
+  assert.equal(spec.multi_views.length, 4);
+  assert.equal(spec.detail_breakdowns.length, 4);
+  assert.equal(spec.scenes.length, 4);
+  assert.throws(() => validateProductSettingBoardSpec({ ...boardSpec(), scenes: [] }), /SETTING_BOARD_FOUR_SCENES_REQUIRED/);
+});
+
+test("Claude-authored set plan owns all creative prompt modules and Node only assembles them", () => {
+  const source = setPlan();
+  const validated = validateClaudeAuthoredSetPlan(source, { runId: source.run_id, referenceObservationSha256: "refhash" });
+  const promptPlan = assembleClaudeAuthoredPromptPlan({ plan: validated, generatedReferences: ["three", "board"], aspectRatio: "1:1" });
+  assert.equal(promptPlan.workflow, V3_WORKFLOW);
+  assert.equal(promptPlan.images.length, 7);
+  assert.ok(promptPlan.images.every((image) => image.reference_images.join(",") === "three,board"));
+  assert.ok(promptPlan.images[4].generation_prompt.includes(source.pages[4].prompt_modules.scene_relation));
+  assert.ok(!promptPlan.images[4].generation_prompt.includes(source.pages[4].prompt_modules.negative_constraints));
+  assert.equal(promptPlan.images[4].negative_prompt, source.pages[4].prompt_modules.negative_constraints);
+});
+
+test("V3 plan rejects Qwen-like or generic uncertain authorship", () => {
+  assert.throws(() => validateClaudeAuthoredSetPlan({ ...setPlan(), authored_by: "qwen" }), /CLAUDE_SET_PLAN_CONTRACT_INVALID/);
+  const invalid = setPlan();
+  invalid.pages[4].prompt_modules.scene_relation = "场景自由发挥";
+  assert.throws(() => validateClaudeAuthoredSetPlan(invalid), /UNCERTAIN_EXPRESSION_REJECTED/);
+});
+
+test("contract hashes are deterministic for cross-stage binding", () => {
+  assert.equal(contractSha256(boardSpec()), contractSha256(boardSpec()));
+  assert.notEqual(contractSha256(boardSpec()), contractSha256({ ...boardSpec(), product_story: { text: "不同故事", source_type: "creative_assumption", source_ref: "claude.creative_assumption" } }));
+});
+
+test("one semantic setting-board framework adapts across six product families without code branches", () => {
+  const products = [
+    ["精品咖啡豆", "包装正面", "居家手冲"], ["香氛洗衣液", "瓶身标签与泵头", "洗衣房取用"],
+    ["运动鞋", "鞋面轮廓与鞋底", "真人行走"], ["机械键盘", "键位与机身边框", "桌面输入"],
+    ["壁挂灯具", "灯体与安装连接", "墙面安装"], ["通勤背包", "软体轮廓与肩带", "真人背负"],
+  ];
+  const hashes = products.map(([title, focus, scene], index) => {
+    const spec = boardSpec(`run-20260804210${String(index).padStart(3, "0")}-aabbcc`, `obs${index}`);
+    spec.identity.main_title = { text: title, source_type: "visible_observation", source_ref: "original_visual_observation.product" };
+    spec.hero.focus_area = focus;
+    spec.scenes[0].content = { text: scene, source_type: "creative_assumption", source_ref: "claude.creative_assumption" };
+    const result = validateProductSettingBoardSpec(spec); assert.equal(result.identity.main_title.text, title); assert.equal(result.scenes.length, 4);
+    return contractSha256(result);
+  });
+  assert.equal(new Set(hashes).size, products.length);
+});

+ 43 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-v3-fixtures.mjs

@@ -0,0 +1,43 @@
+const sourced = (text, source_type = "creative_assumption") => ({ text, source_type, source_ref: source_type === "creative_assumption" ? "claude.creative_assumption" : "original_observation.product" });
+
+export function boardSpec(runId = "run-20260804210000-aabbcc", observationSha = "abc") {
+  return {
+    schema_version: "1.0", authored_by: "claude", run_id: runId, source_observation_sha256: observationSha,
+    identity: { main_title: sourced("商品名称", "visible_observation"), sub_title: sourced("产品设定图"), brand_series: sourced("品牌系列"), category: sourced("商品品类", "visible_observation") },
+    hero: { view: "正面主视角", focus_area: "商品关键识别面", appearance_cues: ["准确轮廓", "真实原色"], lighting: "柔和侧光", depth_of_field: "浅景深" },
+    information_table: ["品牌 / 系列", "产品品类", "规格", "核心材质"].map((label) => ({ label, content: sourced(`${label}内容`) })),
+    product_story: sourced("围绕当前商品形成的创意产品故事。"),
+    multi_views: ["identity", "volume_and_depth", "profile", "access_or_interaction"].map((intent, index) => ({ intent, label: `视图${index + 1}`, description: `当前商品视图描述${index + 1}` })),
+    measurements: [{ label: "相对高度", content: sourced("仅标注项目,不虚构数字") }],
+    packaging_flow: ["outer", "protection", "product"].map((level, index) => ({ level, label: `包装层级${index + 1}`, content: sourced(`包装描述${index + 1}`) })),
+    detail_breakdowns: ["surface_material", "closure_or_operation", "seal_or_interface", "content_or_critical_component"].map((intent, index) => ({ intent, label: `细节${index + 1}`, content: sourced(`细节描述${index + 1}`) })),
+    scenes: ["core_use", "lifestyle_environment", "storage_or_carry", "direct_interaction"].map((intent, index) => ({ intent, label: `场景${index + 1}`, content: sourced(`场景描述${index + 1}`) })),
+    visual_style: { commercial_direction: "商业产品摄影", palette: ["商品原色", "中性色"], materials: ["真实商品材质"], lighting: "柔和专业", atmosphere: "干净高级", typography_style: "现代中文无衬线" },
+  };
+}
+
+export function setPlan(runId = "run-20260804210000-aabbcc", observationSha = "refhash") {
+  const ids = ["white_complete", "selling_form", "selling_material", "scene_core_environment", "scene_human_interaction", "decision_scale_fit", "decision_value_summary"];
+  const roles = ["white_background", "selling_point", "selling_point", "usage_scene", "usage_scene", "purchase_decision", "purchase_decision"];
+  return {
+    schema_version: "3.0", authored_by: "claude", run_id: runId, source_reference_observation_sha256: observationSha,
+    platform: { requested_platform: "taobao", role_weight_decisions: "淘宝保持均衡七页配额,场景页强调生活关系" },
+    product_creative_brief: { category: "当前商品" }, visual_bible: { commercial_style: "统一商业摄影" },
+    pages: ids.map((template_id, index) => {
+      const id = String(index + 1).padStart(2, "0");
+      return {
+        id, role: roles[index], template_id, page_job: `Claude编写的页面任务${id}`, selection_reason: `当前商品选择配方的理由${id}`,
+        difference_from_peer: index === 0 ? "白底基准无同定位页" : `与同定位页面在任务、机位、关系和留白方面形成差异${id}`,
+        platform_decisions: { subject_scale: `占比${id}`, composition: `构图${id}`, scene_human_weight: `人物关系${id}`, color_lighting: `光线${id}`, copy_density: `密度${id}`, safe_zone_bias: `安全区${id}` },
+        prompt_modules: {
+          task_declaration: `只负责生成七图中的第${id}张单图,只输出一张图片。`, reference_roles: `第${id}页两张参考图职责明确。`,
+          product_identity: `第${id}页严格保持当前商品身份。`, platform_design: `第${id}页淘宝具体设计决定。`, visual_bible: `第${id}页遵循共同视觉圣经。`,
+          page_job: `第${id}页唯一任务。`, scene_relation: `第${id}页具体物理空间、商品和人物关系。`, camera_composition: `第${id}页独立机位、景别和前中后景构图。`,
+          lighting_material: `第${id}页光线与真实材质响应。`, text_safe_zone_intent: `第${id}页形成低细节留白但不生成文字。`, peer_difference: `第${id}页与同定位页明确不同。`,
+          negative_constraints: "拼图、其他页面、新增乱码文字、水印、商品材质变化",
+        },
+        ...(index === 0 ? { overlay_required: false, copy: null, source_refs: [] } : { overlay_required: true, copy: { eyebrow: "栏目", title: `标题${id}`, subtitle: `副标题${id}` }, source_refs: ["claude.creative_assumption"] }),
+      };
+    }),
+  };
+}

+ 83 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-visual-observer.mjs

@@ -0,0 +1,83 @@
+import { createHash } from "node:crypto";
+import { readFile, writeFile } from "node:fs/promises";
+import { isAbsolute } from "node:path";
+import { loadVisionConfig, requestVisionJson } from "../../providers/commerce-vision-provider.mjs";
+import { uploadLocalImage } from "../../providers/qiniu-upload-client.mjs";
+import { validateOriginalVisualObservation, validateReferenceVisualObservation } from "./reference-first-v3-contracts.mjs";
+
+const sha256 = (value) => createHash("sha256").update(value).digest("hex");
+const isHttp = (value) => /^https?:\/\//i.test(String(value || ""));
+
+async function readOptionalJson(file) {
+  if (!file) return null;
+  try { return JSON.parse((await readFile(file, "utf8")).replace(/^\uFEFF/, "")); }
+  catch (error) { if (error?.code === "ENOENT") return null; throw error; }
+}
+
+async function prepareVisualSource(source, uploadImage) {
+  if (isHttp(source)) return { public_url: source, sha256: sha256(String(source)), source_kind: "remote_url" };
+  if (!isAbsolute(String(source || ""))) throw new Error("VISUAL_OBSERVER_SOURCE_REQUIRED");
+  const bytes = await readFile(source);
+  const uploaded = await uploadImage(source);
+  return { public_url: uploaded.public_url || uploaded.url, sha256: sha256(bytes), source_kind: "local_file" };
+}
+
+const PRODUCT_SCHEMA = `{
+  "category":"可见商品品类",
+  "product_name_or_visible_text":"可见名称或包装原字;没有则为空",
+  "form":"形态、比例、支撑和摆放状态",
+  "colors":["可见颜色"],
+  "material_and_light_cues":["只描述可见材质与反光线索"],
+  "identity_cues":["至少两个稳定识别特征"],
+  "visible_details":["可见细节"],
+  "visible_text":["仅抄录确定可见的原字"],
+  "natural_support_and_use":["根据结构可自然承载或使用的方式"],
+  "possible_human_interactions":["结构允许的人物交互"],
+  "scene_affordance_hypotheses":["供Claude参考的可能场景,不做页面规划"],
+  "uncertainties":["看不清或不确定之处"],
+  "generation_risks":["生成时容易发生的身份、材质或姿态错误"]
+}`;
+
+export const ORIGINAL_OBSERVER_INSTRUCTION = `你是Claude的视觉输入适配器,不是创意策划器。只观察当前唯一一张商品图,帮助不能直接可靠识图的Claude理解画面。
+禁止选择页面模板、规划七图、编写生图提示词、决定平台设计、虚构参数或把推测写成事实。场景只作为结构可供性假设,并明确它不是页面方案。
+返回严格JSON,顶层只能是 {"product": PRODUCT},PRODUCT结构如下:
+${PRODUCT_SCHEMA}`;
+
+export const REFERENCE_OBSERVER_INSTRUCTION = `你是Claude的视觉输入适配器,不是创意策划器。按输入顺序观察两张已生成参考资产:第一张是产品三视图,第二张是无新增文字的产品视觉设定板。
+分别描述实际生成出来的商品身份、构图、场景和偏差。禁止选择七页模板、规划页面、编写生图提示词、决定平台策略或审美评分。
+返回严格JSON:{"assets":[{"product":PRODUCT,"actual_composition_and_scenes":[],"observed_deviations":[]},{"product":PRODUCT,"actual_composition_and_scenes":[],"observed_deviations":[]}]}
+PRODUCT结构如下:
+${PRODUCT_SCHEMA}`;
+
+export async function observeOriginalVisual({ runId, source, outputFile = "", config = null, requestJson = requestVisionJson, loadConfig = loadVisionConfig, uploadImage = uploadLocalImage } = {}) {
+  if (!runId) throw new Error("ORIGINAL_VISUAL_OBSERVER_RUN_ID_REQUIRED");
+  const prepared = await prepareVisualSource(source, uploadImage);
+  const existing = await readOptionalJson(outputFile);
+  if (existing?.input_asset_sha256 === prepared.sha256) return { observation: validateOriginalVisualObservation(existing), reused: true, model_calls: 0 };
+  const effectiveConfig = config || await loadConfig();
+  const response = await requestJson({ config: effectiveConfig, imageUrls: [prepared.public_url], instruction: ORIGINAL_OBSERVER_INSTRUCTION, temperature: 0 });
+  const observation = validateOriginalVisualObservation({
+    schema_version: "1.0", purpose: "claude_visual_input_not_creative_plan", run_id: runId,
+    input_asset_sha256: prepared.sha256, input_public_url: prepared.public_url, product: response.value?.product,
+    observation: { provider: effectiveConfig.provider, model: effectiveConfig.model, usage: response.usage || null, generated_at: new Date().toISOString() },
+  });
+  if (outputFile) await writeFile(outputFile, `${JSON.stringify(observation, null, 2)}\n`, "utf8");
+  return { observation, reused: false, model_calls: 1, usage: response.usage || null };
+}
+
+export async function observeGeneratedReferences({ runId, sources, outputFile = "", config = null, requestJson = requestVisionJson, loadConfig = loadVisionConfig, uploadImage = uploadLocalImage } = {}) {
+  if (!runId || !Array.isArray(sources) || sources.length !== 2) throw new Error("REFERENCE_VISUAL_OBSERVER_TWO_SOURCES_REQUIRED");
+  const prepared = await Promise.all(sources.map((source) => prepareVisualSource(source, uploadImage)));
+  const hashes = prepared.map((item) => item.sha256);
+  const existing = await readOptionalJson(outputFile);
+  if (JSON.stringify(existing?.input_assets_sha256) === JSON.stringify(hashes)) return { observation: validateReferenceVisualObservation(existing), reused: true, model_calls: 0 };
+  const effectiveConfig = config || await loadConfig();
+  const response = await requestJson({ config: effectiveConfig, imageUrls: prepared.map((item) => item.public_url), instruction: REFERENCE_OBSERVER_INSTRUCTION, temperature: 0 });
+  const observation = validateReferenceVisualObservation({
+    schema_version: "1.0", purpose: "claude_reference_visual_input_not_creative_plan", run_id: runId,
+    input_assets_sha256: hashes, input_public_urls: prepared.map((item) => item.public_url), assets: response.value?.assets,
+    observation: { provider: effectiveConfig.provider, model: effectiveConfig.model, usage: response.usage || null, generated_at: new Date().toISOString() },
+  });
+  if (outputFile) await writeFile(outputFile, `${JSON.stringify(observation, null, 2)}\n`, "utf8");
+  return { observation, reused: false, model_calls: 1, usage: response.usage || null };
+}

+ 26 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/reference-first-visual-observer.test.mjs

@@ -0,0 +1,26 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import { observeGeneratedReferences, observeOriginalVisual, ORIGINAL_OBSERVER_INSTRUCTION } from "./reference-first-visual-observer.mjs";
+
+const product = { category: "鞋", form: "自然直立", colors: ["黑色"], material_and_light_cues: ["哑光鞋面"], identity_cues: ["白色鞋底", "黑色鞋带"], natural_support_and_use: ["地面穿着"] };
+
+test("original observer makes one descriptive Qwen call and reuses by source hash", async () => {
+  const directory = await mkdtemp(join(tmpdir(), "fmode-observer-"));
+  try {
+    const source = join(directory, "source.png"); const output = join(directory, "observation.json");
+    await writeFile(source, "image"); let calls = 0;
+    const options = { runId: "run-20260804210000-aabbcc", source, outputFile: output, config: { provider: "qwen", model: "qwen3-vl-plus", apiKey: "x" }, uploadImage: async () => ({ public_url: "https://image.test/source.png" }), requestJson: async ({ instruction }) => { calls += 1; assert.match(instruction, /不是创意策划器/); return { value: { product }, usage: { total_tokens: 10 } }; } };
+    assert.equal((await observeOriginalVisual(options)).model_calls, 1);
+    assert.equal((await observeOriginalVisual(options)).model_calls, 0);
+    assert.equal(calls, 1); assert.match(ORIGINAL_OBSERVER_INSTRUCTION, /禁止选择页面模板/);
+  } finally { await rm(directory, { recursive: true, force: true }); }
+});
+
+test("reference observer observes both assets in one model call", async () => {
+  let calls = 0;
+  const result = await observeGeneratedReferences({ runId: "run-20260804210000-aabbcc", sources: ["https://image.test/three.png", "https://image.test/board.png"], config: { provider: "qwen", model: "qwen3-vl-plus", apiKey: "x" }, requestJson: async ({ imageUrls }) => { calls += 1; assert.equal(imageUrls.length, 2); return { value: { assets: [{ product }, { product, actual_composition_and_scenes: ["四场景"] }] } }; } });
+  assert.equal(result.model_calls, 1); assert.equal(calls, 1); assert.equal(result.observation.assets[1].asset_role, "setting_board_visual");
+});

+ 253 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/scene-affordance-policy.mjs

@@ -0,0 +1,253 @@
+const asArray = (value) => Array.isArray(value) ? value.filter(Boolean) : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+const unique = (values) => [...new Set(asArray(values).map((value) => text(value)).filter(Boolean))];
+
+const wearablePattern = /鞋|靴|拖鞋|凉鞋|服装|衣|裤|裙|帽|眼镜|手表|腕表|戒指|项链|耳环|手套|围巾|袜|背包|双肩包|挎包|jacket|shirt|dress|shoe|boot|bag|watch|glasses/i;
+const footwearPattern = /鞋|靴|拖鞋|凉鞋|袜|shoe|boot|sandal|slipper/i;
+const handheldPattern = /手机|相机|杯|瓶|工具|餐具|玩具|包装|罐|盒|phone|camera|cup|bottle|tool|toy|package|can|box/i;
+const tabletopPattern = /咖啡|饮品|食品|零食|餐具|化妆|护肤|香水|包装|咖啡豆|茶|台灯|coffee|beverage|food|snack|cosmetic|skincare|perfume|tea|desk lamp/i;
+const interactiveTabletopPattern = /咖啡|饮品|食品|零食|餐具|化妆|护肤|香水|咖啡豆|茶|coffee|beverage|food|snack|cosmetic|skincare|perfume|tea/i;
+const floorPattern = /家具|椅|沙发|行李箱|落地灯|地毯|收纳|柜|桌|furniture|chair|sofa|luggage|floor lamp|rug|cabinet|shelf/i;
+const explicitSurfaceRequestPattern = /(?:鞋|靴|拖鞋|凉鞋|咖啡|杯|瓶|手机|背包|商品|product|shoe|coffee|phone|bag).{0,14}(?:放|摆|置于|陈列|place|put|display).{0,14}(?:书桌|办公桌|餐桌|咖啡桌|茶几|桌面|工作台|desk|desktop|dining table|coffee table|tabletop|workbench)/i;
+const nonSpatialPresentationPattern = /电商.{0,8}(?:详情页|首图|主图)|详情页.{0,8}(?:首图|主图|页面)|白底(?:图|主图)|海报(?:排版|页面)|(?:ecommerce|detail page|listing).{0,8}(?:hero|layout|page)/i;
+
+const policyForMode = {
+  floor_supported: {
+    allowed_surfaces: ["地面", "鞋架", "换鞋凳旁", "低矮承托面", "floor", "shelf", "bench"],
+    forbidden_surfaces: ["书桌", "办公桌", "餐桌", "咖啡桌", "茶几", "桌面", "高位桌面", "desk", "desktop", "dining table", "coffee table", "tabletop"],
+    relation_terms: ["地面", "鞋架", "玄关", "换鞋凳", "步道", "道路", "承托", "floor", "shelf", "entryway", "bench", "ground"],
+    human_actions: ["穿戴", "行走", "搬运", "邻近参照", "wear", "walk", "carry", "proximity"],
+    relation_required: true,
+  },
+  tabletop_supported: {
+    allowed_surfaces: ["餐桌", "厨房台面", "托盘", "咖啡桌", "countertop", "dining table", "tray", "counter"],
+    forbidden_surfaces: ["地面中央", "浴室地面", "鞋架", "道路中央", "floor", "shoe rack", "road center"],
+    relation_terms: ["桌面", "台面", "托盘", "餐桌", "厨房", "countertop", "table", "tray", "kitchen"],
+    human_actions: ["手持", "倒入", "取用", "冲泡", "held", "pour", "use", "brew"],
+    relation_required: true,
+  },
+  handheld: {
+    allowed_surfaces: ["手持", "桌面", "台面", "收纳区", "hand", "table", "countertop", "storage"],
+    forbidden_surfaces: ["漂浮", "穿戴在错误部位", "floating", "wrong body part"],
+    relation_terms: ["手持", "手指", "掌心", "桌面", "台面", "hand", "finger", "palm", "table", "counter"],
+    human_actions: ["手持", "操作", "取用", "held", "operate", "use"],
+    relation_required: true,
+  },
+  body_worn: {
+    allowed_surfaces: ["正确身体部位", "地面", "换衣区", "fitting area", "correct body part", "floor"],
+    forbidden_surfaces: ["穿戴在错误部位", "漂浮", "wrong body part", "floating"],
+    relation_terms: ["穿", "戴", "佩戴", "身体", "姿态", "受力", "worn", "wear", "body", "pose", "fit"],
+    human_actions: ["穿戴", "试穿", "行走", "worn", "wear", "fit", "walk"],
+    relation_required: true,
+  },
+  installed_or_environmental: {
+    allowed_surfaces: ["稳定承托面", "墙面", "地面", "stable surface", "wall", "floor"],
+    forbidden_surfaces: ["漂浮", "穿戴在错误部位", "floating", "wrong body part"],
+    relation_terms: ["承托", "墙面", "地面", "安装", "稳定", "surface", "wall", "floor", "stable", "installed"],
+    human_actions: ["邻近参照", "观察", "proximity", "observe"],
+    relation_required: false,
+  },
+};
+
+function relationPolicyFor({ supportMode, interactionKind, wearableSubtype }) {
+  if (interactionKind === "wearable") {
+    return {
+      allowed_relation_modes: ["isolated", "detail", "stable_display", "human_proximity", "human_wearing_static", "human_wearing_action"],
+      set_budget: { maximum_human_wearing_pages: 3, minimum_non_wearing_pages: 4, maximum_human_handheld_pages: 0 },
+      relation_basis: wearableSubtype === "footwear" ? "footwear_floor_and_on_body_relations" : "garment_display_and_on_body_relations",
+    };
+  }
+  if (supportMode === "tabletop_supported") return {
+    allowed_relation_modes: ["isolated", "detail", "stable_display", "environmental", "human_proximity", "human_handheld"],
+    set_budget: { maximum_human_wearing_pages: 0, minimum_non_wearing_pages: 7, maximum_human_handheld_pages: 2 },
+    relation_basis: "tabletop_display_and_takeup_relations",
+  };
+  if (supportMode === "handheld") return {
+    allowed_relation_modes: ["isolated", "detail", "stable_display", "environmental", "human_proximity", "human_handheld"],
+    set_budget: { maximum_human_wearing_pages: 0, minimum_non_wearing_pages: 7, maximum_human_handheld_pages: 2 },
+    relation_basis: "stable_support_and_handheld_relations",
+  };
+  return {
+    allowed_relation_modes: ["isolated", "detail", "stable_display", "environmental", "human_proximity"],
+    set_budget: { maximum_human_wearing_pages: 0, minimum_non_wearing_pages: 7, maximum_human_handheld_pages: 0 },
+    relation_basis: "stable_environmental_relations",
+  };
+}
+
+function productTextFor(understanding = {}) {
+  return [
+    understanding.category,
+    understanding.product_name?.value,
+    ...asArray(understanding.visible_facts),
+  ].filter(Boolean).join(" ");
+}
+
+export function compileSceneAffordancePolicy({ understanding = {}, brief = {} } = {}) {
+  const productText = productTextFor(understanding);
+  const interactionKind = wearablePattern.test(productText)
+    ? "wearable"
+    : handheldPattern.test(productText) || interactiveTabletopPattern.test(productText)
+      ? "handheld"
+      : "environmental";
+  const wearableSubtype = interactionKind === "wearable"
+    ? footwearPattern.test(productText) ? "footwear" : "body_worn"
+    : "none";
+  const supportMode = wearableSubtype === "footwear"
+    ? "floor_supported"
+    : interactionKind === "wearable"
+      ? "body_worn"
+      : tabletopPattern.test(productText)
+        ? "tabletop_supported"
+        : handheldPattern.test(productText)
+          ? "handheld"
+          : floorPattern.test(productText)
+            ? "floor_supported"
+            : "installed_or_environmental";
+  const modePolicy = policyForMode[supportMode];
+  const relationPolicy = relationPolicyFor({ supportMode, interactionKind, wearableSubtype });
+  const explicitSurfaceOverride = explicitSurfaceRequestPattern.test(text(brief.instruction));
+  return {
+    schema_version: "0.2",
+    interaction_kind: interactionKind,
+    wearable_subtype: wearableSubtype,
+    support_mode: supportMode,
+    allowed_surfaces: [...modePolicy.allowed_surfaces],
+    forbidden_surfaces: [...modePolicy.forbidden_surfaces],
+    forbidden_surface_terms: [...modePolicy.forbidden_surfaces],
+    relation_terms: [...modePolicy.relation_terms],
+    human_actions: [...modePolicy.human_actions],
+    relation_required: modePolicy.relation_required,
+    allowed_relation_modes: [...relationPolicy.allowed_relation_modes],
+    set_budget: { ...relationPolicy.set_budget },
+    relation_basis: relationPolicy.relation_basis,
+    explicit_surface_override: explicitSurfaceOverride,
+    explicit_surface_override_source: explicitSurfaceOverride ? "user_instruction" : "none",
+    basis: "category_product_name_visible_facts_with_conservative_fallback",
+  };
+}
+
+export function sceneContainsForbiddenSurface(policy = {}, value = "") {
+  const candidate = text(value).toLowerCase();
+  return asArray(policy.forbidden_surface_terms ?? policy.forbidden_surfaces)
+    .some((term) => candidate.includes(text(term).toLowerCase()));
+}
+
+export function sceneHasPlausibleRelation(policy = {}, value = "") {
+  const candidate = text(value).toLowerCase();
+  return unique([
+    ...asArray(policy.relation_terms),
+    ...asArray(policy.human_actions),
+  ]).some((term) => candidate.includes(text(term).toLowerCase()));
+}
+
+export function sceneGuidanceForPolicy(policy = {}, sceneHint = "") {
+  const hint = text(sceneHint, "可信日常空间");
+  const explicit = policy.explicit_surface_override === true && sceneContainsForbiddenSurface(policy, hint);
+  if (explicit) {
+    return {
+      route_id: `${policy.support_mode || "environmental"}_explicit_user_override`,
+      decision: "按用户明确要求建立商品与异常场景的可解释关系",
+      scene: `按照用户明确指定的“${hint}”陈列商品,承托、接触、重心、投影、透视和空间尺度必须真实;只执行该场景指令,不扩写为商品功能或适用性结论。`,
+      composition: "让商品、指定承载面和必要尺度参照形成清晰关系,商品识别区不被道具或人物遮挡。",
+      shot_overrides: {},
+    };
+  }
+  const guidance = {
+    floor_supported: {
+      decision: "建立商品与地面、低位承托和真实动线的关系",
+      scene: `在“${hint}”中把商品自然置于干净平整的地面、低位承托面或相符动线,接触、重心、投影、透视和尺度保持真实;不得转移到与商品关系不成立的桌面。`,
+      composition: "采用接近承载面的三分之四环境构图,商品位于前中景交汇点,低位参照只解释尺度和动线。",
+      shot_overrides: { camera_angle: "low three-quarter placement", product_pose: "arranged_sequence", interaction_mode: "environment placement" },
+    },
+    tabletop_supported: {
+      decision: "建立商品与桌面、台面或托盘的可信使用关系",
+      scene: `在“${hint}”中把商品稳定置于与品类相符的桌面、台面或托盘,接触阴影、重心、人物取用动线和空间透视保持真实;不得无理由放在地面或鞋架。`,
+      composition: "以承载台面形成清晰水平基准,商品保持第一视觉焦点,手部或道具仅解释取用关系。",
+      shot_overrides: { product_pose: "upright", interaction_mode: "tabletop relation" },
+    },
+    handheld: {
+      decision: "建立商品与手部取用或稳定收纳面的可信关系",
+      scene: `在“${hint}”中让商品由手部自然托持,或稳定放在可取用的桌面、台面或收纳区;手指、掌心、重心、接触阴影和透视关系真实,不得悬浮或错误穿戴。`,
+      composition: "分离商品识别区与动作区,手部或收纳面只解释受力和取用动线,不遮挡关键特征。",
+      shot_overrides: { interaction_mode: "handheld or stable support" },
+    },
+    body_worn: {
+      decision: "建立商品与正确穿戴部位或穿戴准备区的可信关系",
+      scene: `在“${hint}”中把商品穿戴在正确身体部位,或放在紧邻穿戴动作的稳定准备区;受力、遮挡、接触阴影和姿态真实,不得悬浮或穿戴在错误部位。`,
+      composition: "让正确穿戴部位、商品与必要环境形成第一阅读路径,人物和道具不遮挡商品身份特征。",
+      shot_overrides: { interaction_mode: "wearing relation" },
+    },
+    installed_or_environmental: {
+      decision: "建立商品与稳定安装面或环境尺度的可信关系",
+      scene: `在“${hint}”中让商品由稳定表面、墙面或地面正确承载或安装,接触、投影、透视和环境尺度一致;人物只作邻近参照,不擅自操作未知功能。`,
+      composition: "以稳定承载或安装关系为构图基准,商品保持完整可见,人物和环境退居次要层级。",
+      shot_overrides: { interaction_mode: "stable environmental relation" },
+    },
+  }[policy.support_mode] ?? null;
+  const selected = guidance || {
+    decision: "建立商品与可信环境的稳定关系",
+    scene: `在“${hint}”中让商品保持稳定承载和完整可见,不演示未经证实的操作或功能。`,
+    composition: "商品保持第一视觉焦点,环境仅解释尺度、承载和动线。",
+    shot_overrides: {},
+  };
+  return { route_id: `${policy.support_mode || "environmental"}_context`, ...selected };
+}
+
+export function sceneCandidatesForPolicy({ understanding = {}, policy = {} } = {}) {
+  const sourceScenes = asArray(understanding.suggested_scenes)
+    .map((value, index) => ({ text: text(value), evidenceRef: `suggested_scene:${index + 1}` }))
+    .filter((item) => item.text);
+  const spatialScenes = sourceScenes.filter((item) => !nonSpatialPresentationPattern.test(item.text));
+  const filtered = policy.explicit_surface_override
+    ? spatialScenes
+    : spatialScenes.filter((item) => !sceneContainsForbiddenSurface(policy, item.text));
+  const fallbackByMode = {
+    floor_supported: [
+      { text: "城市步道或平整地面的自然动线", evidenceRef: "creative_positioning:1" },
+      { text: "稳定承托面旁保持动线清晰的生活空间", evidenceRef: "creative_positioning:1" },
+      { text: "整洁玄关或低矮承托区域", evidenceRef: "creative_positioning:1" },
+    ],
+    tabletop_supported: [
+      { text: "整洁厨房台面与餐桌之间的准备区域", evidenceRef: "creative_positioning:1" },
+      { text: "托盘、杯具和原料关系清楚的生活桌面", evidenceRef: "creative_positioning:1" },
+      { text: "自然光下的早餐或冲泡操作区域", evidenceRef: "creative_positioning:1" },
+    ],
+    handheld: [
+      { text: "手部动作清楚的桌边或收纳区域", evidenceRef: "creative_positioning:1" },
+      { text: "符合取用动线的生活空间", evidenceRef: "creative_positioning:1" },
+      { text: "中性背景下的自然手持场景", evidenceRef: "creative_positioning:1" },
+    ],
+    body_worn: [
+      { text: "安静的穿戴准备区域", evidenceRef: "creative_positioning:1" },
+      { text: "符合行走或站立动作的生活空间", evidenceRef: "creative_positioning:1" },
+      { text: "克制的搭配和试穿环境", evidenceRef: "creative_positioning:1" },
+    ],
+    installed_or_environmental: [
+      { text: "稳定承托且动线清晰的日常空间", evidenceRef: "creative_positioning:1" },
+      { text: "与商品尺度相符的中性环境", evidenceRef: "creative_positioning:1" },
+      { text: "自然光和空间透视一致的生活区域", evidenceRef: "creative_positioning:1" },
+    ],
+  };
+  const candidates = [...filtered];
+  for (const fallback of fallbackByMode[policy.support_mode] ?? fallbackByMode.installed_or_environmental) {
+    if (candidates.length >= 3) break;
+    if (!candidates.some((candidate) => candidate.text === fallback.text)) candidates.push(fallback);
+  }
+  return {
+    candidates: candidates.slice(0, 3),
+    explicit_surface_override: policy.explicit_surface_override === true,
+    rejected_hints: policy.explicit_surface_override
+      ? sourceScenes.filter((item) => nonSpatialPresentationPattern.test(item.text)).map((item) => item.text)
+      : sourceScenes.filter((item) => nonSpatialPresentationPattern.test(item.text) || sceneContainsForbiddenSurface(policy, item.text)).map((item) => item.text),
+  };
+}
+
+export function interactionProfileFromPolicy(policy = {}, brief = {}) {
+  return {
+    kind: policy.interaction_kind || "environmental",
+    wearable_subtype: policy.wearable_subtype || "none",
+    explicit_usage_request: /上脚|试穿|穿着|穿戴|佩戴|上身|真人.{0,8}(穿|戴|用)|使用中|行走|步行|通勤|wear|walk|use/i.test(text(brief.instruction)),
+    basis: policy.basis || "scene_affordance_policy",
+    support_mode: policy.support_mode || "installed_or_environmental",
+  };
+}

+ 79 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/scene-affordance-policy.test.mjs

@@ -0,0 +1,79 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import {
+  compileSceneAffordancePolicy,
+  sceneCandidatesForPolicy,
+  sceneContainsForbiddenSurface,
+  sceneHasPlausibleRelation,
+} from "./scene-affordance-policy.mjs";
+
+function understanding(category, name, scenes = []) {
+  return {
+    category,
+    product_name: { value: name },
+    visible_facts: [name],
+    suggested_scenes: scenes,
+  };
+}
+
+test("affordance policy covers the five requested product families", () => {
+  const cases = [
+    ["鞋履", "低帮帆布鞋", "floor_supported", "wearable"],
+    ["咖啡饮品", "挂耳咖啡", "tabletop_supported", "handheld"],
+    ["箱包", "通勤背包", "body_worn", "wearable"],
+    ["手机", "智能手机", "handheld", "handheld"],
+    ["照明", "台灯", "tabletop_supported", "environmental"],
+  ];
+  for (const [category, name, supportMode, interactionKind] of cases) {
+    const policy = compileSceneAffordancePolicy({ understanding: understanding(category, name) });
+    assert.equal(policy.support_mode, supportMode, `${name} support mode`);
+    assert.equal(policy.interaction_kind, interactionKind, `${name} interaction kind`);
+    assert.ok(policy.allowed_surfaces.length > 0);
+    assert.ok(policy.relation_terms.length > 0);
+  }
+});
+
+test("policy filters a forbidden suggested scene and records its provenance", () => {
+  const product = understanding("鞋履", "低帮帆布鞋", ["办公室书桌中央", "玄关地面"]);
+  const policy = compileSceneAffordancePolicy({ understanding: product, brief: { instruction: "生成七图" } });
+  const strategy = sceneCandidatesForPolicy({ understanding: product, policy });
+  assert.deepEqual(strategy.rejected_hints, ["办公室书桌中央"]);
+  assert.equal(strategy.explicit_surface_override, false);
+  assert.ok(strategy.candidates.some((item) => item.text === "玄关地面"));
+  assert.equal(sceneContainsForbiddenSurface(policy, "鞋放在办公室书桌中央"), true);
+  assert.equal(sceneHasPlausibleRelation(policy, "鞋放在玄关地面"), true);
+});
+
+test("explicit user instruction is the only scene override source", () => {
+  const product = understanding("鞋履", "低帮帆布鞋", ["办公室书桌中央"]);
+  const policy = compileSceneAffordancePolicy({
+    understanding: product,
+    brief: { instruction: "明确要求把鞋放在办公室书桌中央" },
+  });
+  const strategy = sceneCandidatesForPolicy({ understanding: product, policy });
+  assert.equal(policy.explicit_surface_override, true);
+  assert.equal(policy.explicit_surface_override_source, "user_instruction");
+  assert.deepEqual(strategy.rejected_hints, []);
+  assert.equal(strategy.candidates[0].text, "办公室书桌中央");
+});
+
+test("unknown product falls back to conservative environmental relations", () => {
+  const policy = compileSceneAffordancePolicy({ understanding: understanding("未知商品", "未分类物品") });
+  assert.equal(policy.support_mode, "installed_or_environmental");
+  assert.equal(policy.relation_required, false);
+  assert.deepEqual(policy.human_actions, ["邻近参照", "观察", "proximity", "observe"]);
+});
+
+test("scene candidates reject page-layout suggestions and replace them with physical spaces", () => {
+  const product = understanding("食品饮料-咖啡", "MORI COFFEE", [
+    "精品咖啡馆吧台陈列",
+    "家居厨房收纳架展示",
+    "生活方式类电商详情页首图",
+  ]);
+  const policy = compileSceneAffordancePolicy({ understanding: product });
+  const strategy = sceneCandidatesForPolicy({ understanding: product, policy });
+  assert.deepEqual(strategy.rejected_hints, ["生活方式类电商详情页首图"]);
+  assert.equal(strategy.candidates.length, 3);
+  assert.ok(strategy.candidates.every((item) => !/详情页|首图/.test(item.text)));
+  assert.match(strategy.candidates[2].text, /台面|餐桌|托盘|冲泡/);
+});

+ 61 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/set-diversity-policy.mjs

@@ -0,0 +1,61 @@
+const asArray = (value) => Array.isArray(value) ? value : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+
+const wearingRelations = new Set(["human_wearing", "human_wearing_static", "human_wearing_action"]);
+
+export function compileSetDiversityPolicy(scenePolicy = {}) {
+  const supportMode = text(scenePolicy.support_mode, "installed_or_environmental");
+  const wearable = supportMode === "body_worn" || supportMode === "floor_supported" && scenePolicy.wearable_subtype === "footwear";
+  return {
+    schema_version: "1.0",
+    minimum_distinct_content_signatures: 5,
+    maximum_same_axis_framing: 2,
+    maximum_human_wearing_pages: Number(scenePolicy.set_budget?.maximum_human_wearing_pages ?? (wearable ? 3 : 0)),
+    required_relation_families: wearable
+      ? ["isolated", "detail", "stable_display", "human_proximity", "human_wearing"]
+      : ["isolated", "detail", "environmental", "human_context"],
+    allowed_relation_modes: asArray(scenePolicy.allowed_relation_modes).length
+      ? [...scenePolicy.allowed_relation_modes]
+      : wearable
+      ? ["isolated", "detail", "stable_display", "human_proximity", "human_wearing_static", "human_wearing_action"]
+      : ["isolated", "detail", "stable_display", "environmental", "human_proximity", "human_handheld"],
+    automatic_paid_regeneration: false,
+  };
+}
+
+export function relationFamily(value = "") {
+  const relation = text(value, "unknown");
+  if (wearingRelations.has(relation)) return "human_wearing";
+  if (relation.includes("detail") || relation.includes("material") || relation.includes("feature")) return "detail";
+  if (relation.includes("isolated") || relation.includes("single accurate")) return "isolated";
+  if (relation.includes("proximity")) return "human_proximity";
+  if (relation.includes("handheld")) return "human_context";
+  if (relation.includes("environment") || relation.includes("tabletop") || relation.includes("support") || relation.includes("display")) return "stable_display";
+  return relation;
+}
+
+export function contentSignatureForPage({ page = {}, relationMode = "", index = 0 } = {}) {
+  const shot = page.shot_spec || {};
+  const evidence = asArray(page.evidence_refs).join("+") || `page:${index + 1}`;
+  return [
+    text(page.business_category, text(page.role, "page")),
+    text(page.set_design?.render_family, "render"),
+    text(shot.camera_axis, "axis"),
+    text(shot.framing_mode, "framing"),
+    text(relationMode || shot.interaction_mode, "relation"),
+    evidence,
+  ].join("|");
+}
+
+export function summarizePlannedDiversity(images = [], policy = {}) {
+  const signatures = asArray(images).map((page) => text(page.set_design?.content_signature));
+  const axisFraming = asArray(images).map((page) => `${text(page.shot_spec?.camera_axis)}|${text(page.shot_spec?.framing_mode)}`);
+  const wearing = asArray(images).filter((page) => relationFamily(page.expected_page_compliance?.product_human_relation) === "human_wearing").length;
+  const counts = (values) => Object.fromEntries([...new Set(values)].map((value) => [value, values.filter((candidate) => candidate === value).length]));
+  return {
+    distinct_content_signatures: new Set(signatures.filter(Boolean)).size,
+    axis_framing_counts: counts(axisFraming),
+    human_wearing_pages: wearing,
+    policy: policy.schema_version ? policy : compileSetDiversityPolicy(policy),
+  };
+}

+ 148 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/set-quality-verifier.mjs

@@ -0,0 +1,148 @@
+import sharp from "sharp";
+import { relationFamily } from "./set-diversity-policy.mjs";
+import { verifyPageCompliance } from "./production-result-verifier.mjs";
+
+const asArray = (value) => Array.isArray(value) ? value : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+
+function hamming(left = "", right = "") {
+  if (!left || left.length !== right.length) return Number.POSITIVE_INFINITY;
+  let distance = 0;
+  for (let index = 0; index < left.length; index += 1) {
+    const value = Number.parseInt(left[index], 16) ^ Number.parseInt(right[index], 16);
+    distance += value.toString(2).split("1").length - 1;
+  }
+  return distance;
+}
+
+async function differenceHash(file, regionEvidence) {
+  if (!file) return null;
+  let pipeline = sharp(file, { failOn: "warning" });
+  const metadata = await pipeline.metadata();
+  const region = regionEvidence?.status === "verified" ? regionEvidence.boxes?.[0] : null;
+  if (region && metadata.width && metadata.height) {
+    const left = Math.max(0, Math.floor(region.x * metadata.width));
+    const top = Math.max(0, Math.floor(region.y * metadata.height));
+    const width = Math.max(1, Math.min(metadata.width - left, Math.floor(region.width * metadata.width)));
+    const height = Math.max(1, Math.min(metadata.height - top, Math.floor(region.height * metadata.height)));
+    pipeline = pipeline.extract({ left, top, width, height });
+  }
+  const { data } = await pipeline.greyscale().resize(9, 8, { fit: "fill" }).raw().toBuffer({ resolveWithObject: true });
+  let bits = "";
+  for (let row = 0; row < 8; row += 1) {
+    for (let column = 0; column < 8; column += 1) bits += data[row * 9 + column] > data[row * 9 + column + 1] ? "1" : "0";
+  }
+  return BigInt(`0b${bits}`).toString(16).padStart(16, "0");
+}
+
+function groups(values, minimum = 2) {
+  const map = new Map();
+  values.forEach(({ key, slot_id }) => {
+    if (!key) return;
+    map.set(key, [...(map.get(key) || []), slot_id]);
+  });
+  return [...map.entries()].filter(([, slots]) => slots.length >= minimum).map(([key, slots]) => ({ key, slots }));
+}
+
+function observedCompliance(entry) {
+  return entry.page_compliance
+    || entry.quality_review?.page_compliance
+    || entry.quality_review?.reviews?.[0]?.page_compliance
+    || null;
+}
+
+export async function verifyProductionSetQuality({ runState = {}, promptPlan = {}, policy = null } = {}) {
+  const results = asArray(runState.results);
+  const plans = asArray(promptPlan.images);
+  const activePolicy = policy || promptPlan.set_blueprint?.diversity_policy || {
+    minimum_distinct_content_signatures: 5,
+    maximum_same_axis_framing: 2,
+    maximum_human_wearing_pages: 3,
+  };
+  const pages = [];
+  for (let index = 0; index < results.length; index += 1) {
+    const entry = results[index];
+    const verification = verifyPageCompliance({ pagePlan: plans[index] || {}, observed: observedCompliance(entry) });
+    const observed = verification.observed;
+    let perceptualHash = null;
+    try { perceptualHash = await differenceHash(asArray(entry.local_images)[0], observed?.product_region); } catch { perceptualHash = null; }
+    const actualSignature = observed ? [
+      observed.actual_camera_axis,
+      observed.actual_framing_mode,
+      observed.product_human_relation,
+      observed.scene_family,
+      observed.composition_family,
+    ].join("|") : "";
+    pages.push({
+      slot_id: entry.slot_id || String(index + 1).padStart(2, "0"),
+      status: verification.status,
+      accepted: verification.accepted,
+      violations: verification.violations,
+      actual_signature: actualSignature,
+      axis_framing_signature: observed ? `${observed.actual_camera_axis}|${observed.actual_framing_mode}` : "",
+      relation_family: observed ? relationFamily(observed.product_human_relation) : "unknown",
+      semantic_duplicate_group: observed?.semantic_duplicate_group || "",
+      perceptual_hash: perceptualHash,
+      page_compliance: observed,
+    });
+  }
+
+  const signatureGroups = groups(pages.map((page) => ({ key: page.actual_signature, slot_id: page.slot_id })));
+  const semanticGroups = groups(pages.map((page) => ({ key: page.semantic_duplicate_group, slot_id: page.slot_id })));
+  const axisFramingGroups = groups(pages.map((page) => ({ key: page.axis_framing_signature, slot_id: page.slot_id })), Number(activePolicy.maximum_same_axis_framing || 2) + 1);
+  const hashPairs = [];
+  for (let left = 0; left < pages.length; left += 1) {
+    for (let right = left + 1; right < pages.length; right += 1) {
+      const distance = hamming(pages[left].perceptual_hash, pages[right].perceptual_hash);
+      if (distance <= 5) hashPairs.push({ slots: [pages[left].slot_id, pages[right].slot_id], distance });
+    }
+  }
+  const distinct = new Set(pages.map((page) => page.actual_signature).filter(Boolean)).size;
+  const wearingPages = pages.filter((page) => page.relation_family === "human_wearing").map((page) => page.slot_id);
+  const violations = [];
+  const unverifiedReasons = [];
+  for (const page of pages) {
+    if (page.status === "quality_blocked") for (const violation of page.violations) violations.push({ ...violation, slots: [page.slot_id] });
+    if (page.status === "quality_unverified") for (const violation of page.violations) unverifiedReasons.push({ ...violation, slots: [page.slot_id] });
+  }
+  const hasUnverified = pages.some((page) => page.status === "quality_unverified") || pages.length !== 7;
+  if (!hasUnverified && distinct < Number(activePolicy.minimum_distinct_content_signatures || 5)) {
+    violations.push({ code: "INSUFFICIENT_CONTENT_DIVERSITY", detail: `Only ${distinct} distinct actual content signatures`, slots: pages.map((page) => page.slot_id) });
+  }
+  if (!hasUnverified && wearingPages.length > Number(activePolicy.maximum_human_wearing_pages ?? 3)) {
+    violations.push({ code: "HUMAN_WEARING_BUDGET_EXCEEDED", detail: `${wearingPages.length} wearer pages exceed the set budget`, slots: wearingPages });
+  }
+  const duplicateMap = new Map();
+  for (const group of [...semanticGroups, ...signatureGroups]) duplicateMap.set(group.slots.join("|"), group);
+  const duplicateClusters = [...duplicateMap.values()];
+  const imageDuplicatePairs = hashPairs.filter(({ slots }) => {
+    const [left, right] = slots.map((slot) => pages.find((page) => page.slot_id === slot));
+    return left?.axis_framing_signature === right?.axis_framing_signature && left?.relation_family === right?.relation_family;
+  });
+  for (const group of duplicateClusters) violations.push({ code: "SEMANTIC_DUPLICATE_CLUSTER", detail: `Duplicate content group ${group.key}`, slots: group.slots });
+  for (const pair of imageDuplicatePairs) violations.push({ code: "IMAGE_NEAR_DUPLICATE", detail: `Normalized product-region dHash distance=${pair.distance}`, slots: pair.slots });
+  if (!hasUnverified) for (const group of axisFramingGroups) violations.push({ code: "AXIS_FRAMING_BUDGET_EXCEEDED", detail: `${group.key} appears ${group.slots.length} times`, slots: group.slots });
+
+  const blocking = violations.length > 0;
+  const status = blocking ? "quality_blocked" : hasUnverified ? "quality_unverified" : "quality_passed";
+  return {
+    schema_version: "1.0",
+    status,
+    accepted: status === "quality_passed",
+    page_count: pages.length,
+    distinct_content_signatures: distinct,
+    human_wearing_pages: wearingPages,
+    duplicate_clusters: [
+      ...duplicateClusters.map((group) => ({ ...group, source: "structured_semantics" })),
+      ...imageDuplicatePairs.map((pair) => ({ key: `dhash:${pair.distance}`, slots: pair.slots, source: "normalized_product_region_dhash" })),
+    ],
+    perceptual_similarity_pairs: hashPairs,
+    axis_framing_overages: axisFramingGroups,
+    violations,
+    unverified_reasons: unverifiedReasons,
+    pages,
+    policy: activePolicy,
+    automatic_external_calls: false,
+    paid_regeneration_authorized: false,
+  };
+}

+ 45 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/set-quality-verifier.test.mjs

@@ -0,0 +1,45 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+import { verifyProductionSetQuality } from "./set-quality-verifier.mjs";
+
+const fixture = fileURLToPath(new URL("../../../test-fixtures/task-0016-coat-failure/", import.meta.url));
+
+test("portable real failure fixture detects duplicate clusters and page 05 relation violation", async () => {
+  const labels = JSON.parse(await readFile(join(fixture, "labels.json"), "utf8"));
+  const plan = JSON.parse(await readFile(join(fixture, "plan.json"), "utf8"));
+  const results = labels.pages.map((page) => ({
+    slot_id: page.slot_id, status: "complete",
+    local_images: [join(fixture, "images", `${page.slot_id}.jpg`)],
+    quality_review: { accepted: true, page_compliance: page },
+  }));
+  const review = await verifyProductionSetQuality({ runState: { results }, promptPlan: plan });
+  assert.equal(review.status, "quality_blocked");
+  assert.ok(review.duplicate_clusters.some((group) => group.slots.join(",") === "01,04"));
+  assert.ok(review.duplicate_clusters.some((group) => group.slots.join(",") === "03,05,07"));
+  const page05 = review.pages.find((page) => page.slot_id === "05");
+  assert.ok(page05.violations.some((item) => item.code === "PRODUCT_HUMAN_RELATION_MISMATCH"));
+});
+
+test("seven semantically distinct compliant pages pass without external calls", async () => {
+  const relations = ["isolated", "detail", "environmental", "detail", "human_proximity", "human_wearing_action", "human_wearing_static"];
+  const plan = {
+    set_blueprint: { diversity_policy: { minimum_distinct_content_signatures: 5, maximum_same_axis_framing: 2, maximum_human_wearing_pages: 3 } },
+    images: relations.map((relation) => ({ expected_page_compliance: { product_human_relation: relation } })),
+  };
+  const results = relations.map((relation, index) => ({
+    slot_id: String(index + 1).padStart(2, "0"), status: "complete", local_images: [],
+    quality_review: { accepted: true, page_compliance: {
+      actual_camera_axis: ["front", "macro", "three_quarter_left", "side", "three_quarter_right", "low", "front"][index],
+      actual_framing_mode: ["full_product", "detail", "product_in_context", "full_product", "product_in_context", "human_action", "human_action"][index],
+      product_human_relation: relation, scene_family: `scene-${index}`, composition_family: `composition-${index}`,
+      page_job_compliant: true, violations: [], forbidden_reveals: [],
+    } },
+  }));
+  const review = await verifyProductionSetQuality({ runState: { results }, promptPlan: plan });
+  assert.equal(review.status, "quality_passed", JSON.stringify(review.violations));
+  assert.equal(review.automatic_external_calls, false);
+});
+

+ 42 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-annotated-renderer.mjs

@@ -0,0 +1,42 @@
+import { createHash } from "node:crypto";
+import { readFile, writeFile } from "node:fs/promises";
+import sharp from "sharp";
+import { resolveChineseFont } from "../designed-asset/font-resolver.mjs";
+import { fitTextWithPango } from "../designed-asset/pango-text-metrics.mjs";
+
+const sha256 = (value) => createHash("sha256").update(value).digest("hex");
+const esc = (value) => String(value || "").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
+
+async function textBuffer(value, { width, height, size = 24, color = "#202020", bold = false } = {}, font) {
+  const input = { text: { text: `<span foreground="${color}"${bold ? ' weight="bold"' : ""}>${esc(value)}</span>`, font: `${font.family} ${size}`, width, height, rgba: true, align: "left", spacing: Math.round(size * 0.28) } };
+  if (font.fontfile) input.text.fontfile = font.fontfile;
+  return sharp(input).png().toBuffer();
+}
+
+function compact(items, maximum = 4) {
+  return items.slice(0, maximum).map((item) => `${item.label}:${item.text || item.content || ""}`).join("\n");
+}
+
+async function fittedText(value, constraints, font) {
+  const fit = await fitTextWithPango(value, { maxWidth: constraints.width, maxHeight: constraints.height, fontSize: constraints.size, minFontSize: 12, maxLines: constraints.maxLines || 14, font });
+  return { buffer: await textBuffer(fit.lines.join("\n"), { ...constraints, size: fit.fontSize }, font), fit };
+}
+
+export async function renderAnnotatedSettingBoard({ sourceVisual, copyPlan, outputFile, reportFile = "" } = {}) {
+  if (!sourceVisual || !outputFile || copyPlan?.status !== "ready") throw new Error("SETTING_BOARD_ANNOTATED_INPUT_REQUIRED");
+  const source = await sharp(sourceVisual).resize(2048, 1152, { fit: "cover" }).png().toBuffer();
+  const font = await resolveChineseFont();
+  const panels = Buffer.from(`<svg width="2048" height="1152" xmlns="http://www.w3.org/2000/svg"><rect x="0" y="0" width="2048" height="150" fill="rgba(255,255,255,.94)"/><rect x="0" y="770" width="2048" height="382" fill="rgba(255,255,255,.96)"/><line x1="620" y1="790" x2="620" y2="1125" stroke="#d7d7d7"/><line x1="1320" y1="790" x2="1320" y2="1125" stroke="#d7d7d7"/></svg>`);
+  const title = await textBuffer(`${copyPlan.title.text}\n${copyPlan.subtitle.text}`, { width: 940, height: 125, size: 42, bold: true }, font);
+  const info = await fittedText(`产品信息\n${compact(copyPlan.information_table)}\n\n产品故事\n${copyPlan.product_story.text}`, { width: 555, height: 325, size: 20, maxLines: 14 }, font);
+  const middle = await fittedText(`多角度视图\n${compact(copyPlan.sections.multi_views)}\n\n包装与细节拆解\n${compact(copyPlan.sections.details, 4)}`, { width: 625, height: 325, size: 18, maxLines: 16 }, font);
+  const right = await fittedText(`包装层次\n${compact(copyPlan.sections.packaging, copyPlan.sections.packaging.length)}\n\n场景展示\n${compact(copyPlan.sections.scenes, 4)}`, { width: 650, height: 325, size: 18, maxLines: 16 }, font);
+  const output = await sharp(source).composite([
+    { input: panels, left: 0, top: 0 }, { input: title, left: 58, top: 18 },
+    { input: info.buffer, left: 42, top: 802 }, { input: middle.buffer, left: 650, top: 802 }, { input: right.buffer, left: 1350, top: 802 },
+  ]).png().toBuffer();
+  await writeFile(outputFile, output);
+  const report = { schema_version: "1.0", status: "complete", source_visual: sourceVisual, source_visual_sha256: sha256(await readFile(sourceVisual)), output_sha256: sha256(output), output_size: { width: 2048, height: 1152 }, text_renderer: "sharp_pango", font, typography: { information_and_story: info.fit, views_and_details: middle.fit, packaging_and_scenes: right.fit }, zones: ["header", "information_and_story", "views_and_details", "packaging_and_scenes"], rendered_copy_counts: { information: copyPlan.information_table.length, views: copyPlan.sections.multi_views.length, packaging: copyPlan.sections.packaging.length, details: copyPlan.sections.details.length, scenes: copyPlan.sections.scenes.length }, generated_at: new Date().toISOString() };
+  if (reportFile) await writeFile(reportFile, `${JSON.stringify(report, null, 2)}\n`, "utf8");
+  return report;
+}

+ 19 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-annotated-renderer.test.mjs

@@ -0,0 +1,19 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { mkdtemp, rm } from "node:fs/promises";
+import { join } from "node:path";
+import { tmpdir } from "node:os";
+import sharp from "sharp";
+import { boardSpec } from "./reference-first-v3-fixtures.mjs";
+import { compileSettingBoardCopyPlan } from "./setting-board-copy-plan.mjs";
+import { renderAnnotatedSettingBoard } from "./setting-board-annotated-renderer.mjs";
+
+test("annotated setting board renders deterministic Chinese copy with Sharp/Pango", async () => {
+  const directory = await mkdtemp(join(tmpdir(), "fmode-board-"));
+  try {
+    const source = join(directory, "visual.png"); const output = join(directory, "annotated.png");
+    await sharp({ create: { width: 2048, height: 1152, channels: 3, background: "#ececec" } }).png().toFile(source);
+    const report = await renderAnnotatedSettingBoard({ sourceVisual: source, copyPlan: compileSettingBoardCopyPlan(boardSpec()), outputFile: output });
+    const metadata = await sharp(output).metadata(); assert.equal(report.status, "complete"); assert.equal(metadata.width, 2048); assert.equal(metadata.height, 1152); assert.equal(report.rendered_copy_counts.details, 4); assert.equal(report.rendered_copy_counts.scenes, 4);
+  } finally { await rm(directory, { recursive: true, force: true }); }
+});

+ 28 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-copy-plan.mjs

@@ -0,0 +1,28 @@
+import { writeFile } from "node:fs/promises";
+import { validateProductSettingBoardSpec } from "./reference-first-v3-contracts.mjs";
+
+const toCopy = (value) => ({ text: value.text, source_type: value.source_type, source_ref: value.source_ref || "" });
+
+export function compileSettingBoardCopyPlan(spec) {
+  const board = validateProductSettingBoardSpec(spec);
+  return {
+    schema_version: "1.0", status: "ready", run_id: board.run_id, authored_by: "claude",
+    title: toCopy(board.identity.main_title), subtitle: toCopy(board.identity.sub_title),
+    identity: { brand_series: toCopy(board.identity.brand_series), category: toCopy(board.identity.category) },
+    information_table: board.information_table.map((item) => ({ label: item.label, ...toCopy(item.content) })),
+    product_story: toCopy(board.product_story),
+    sections: {
+      multi_views: board.multi_views.map((item) => ({ label: item.label, text: item.description })),
+      packaging: board.packaging_flow.map((item) => ({ label: item.label, ...toCopy(item.content) })),
+      details: board.detail_breakdowns.map((item) => ({ label: item.label, ...toCopy(item.content) })),
+      scenes: board.scenes.map((item) => ({ label: item.label, ...toCopy(item.content) })),
+    },
+    forbidden_copy_source_types: [],
+  };
+}
+
+export async function writeSettingBoardCopyPlan(spec, outputFile) {
+  const plan = compileSettingBoardCopyPlan(spec);
+  await writeFile(outputFile, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
+  return plan;
+}

+ 12 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/setting-board-copy-plan.test.mjs

@@ -0,0 +1,12 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { boardSpec } from "./reference-first-v3-fixtures.mjs";
+import { compileSettingBoardCopyPlan } from "./setting-board-copy-plan.mjs";
+
+test("setting board copy plan preserves Claude text and provenance", () => {
+  const source = boardSpec(); const plan = compileSettingBoardCopyPlan(source);
+  assert.equal(plan.title.text, source.identity.main_title.text);
+  assert.equal(plan.information_table.length, 4);
+  assert.equal(plan.sections.scenes.length, 4);
+  assert.equal(plan.product_story.source_type, "creative_assumption");
+});

+ 105 - 12
claude-code/fmode-image-set/mcp/src/features/image-set/validate-plan.mjs

@@ -1,4 +1,7 @@
 import { readFile } from "node:fs/promises";
+import { sceneContainsForbiddenSurface, sceneHasPlausibleRelation } from "./scene-affordance-policy.mjs";
+import { assessViewStrategyCompatibility } from "./view-strategy-compatibility.mjs";
+import { summarizePlannedDiversity } from "./set-diversity-policy.mjs";
 
 const filename = process.argv[2];
 if (!filename) throw new Error("Usage: node validate-plan.mjs <prompt-plan.json>");
@@ -21,6 +24,10 @@ if (!Array.isArray(plan.request?.reference_images)) errors.push("request.referen
 if (!isText(plan.context?.platform)) errors.push("context.platform is required");
 if (!new Set(["available", "unavailable"]).has(plan.context?.voc?.status)) errors.push("context.voc.status is invalid");
 if (!Array.isArray(plan.context?.voc?.findings)) errors.push("context.voc.findings must be an array");
+if (plan.context?.scene_affordance_policy?.explicit_surface_override === true
+  && plan.context.scene_affordance_policy.explicit_surface_override_source !== "user_instruction") {
+  errors.push("context.scene_affordance_policy explicit override must come from user_instruction");
+}
 
 const understanding = plan.product_understanding ?? {};
 if (!isText(understanding.category)) errors.push("product_understanding.category is required");
@@ -121,6 +128,7 @@ const renderFamiliesByCategory = {
   decision_support: new Set([
     "multi_angle",
     "usage_sequence",
+    "fit_context",
     "package_contents",
     "scale_context",
     "comparison_sequence",
@@ -139,9 +147,9 @@ const moduleNames = [
   "negative_constraints",
 ];
 const complexityRules = {
-  simple: { promptMin: 100, sceneMin: 12 },
-  medium: { promptMin: 220, sceneMin: 60 },
-  complex: { promptMin: 360, sceneMin: 160 },
+  simple: {},
+  medium: {},
+  complex: { requiredSceneSignals: 2 },
 };
 const highRiskClaimTerms = [
   "防水", "抗菌", "临床", "认证", "销量第一", "最畅销", "耐磨", "耐用", "续航",
@@ -169,6 +177,8 @@ const decisions = [];
 const shotSpecs = [];
 const setDesigns = [];
 const creativeBodies = [];
+const sceneBodies = [];
+const roleSignatures = [];
 const categoryValues = [];
 const sellingImages = [];
 const categoryRenderFamilies = new Map();
@@ -233,6 +243,20 @@ for (const [index, image] of asArray(plan.images).entries()) {
   }
   if (!isText(setDesign.unique_visual_hook)) errors.push(`images[${index}].set_design.unique_visual_hook is required`);
   if (!isText(setDesign.contrast_with_previous)) errors.push(`images[${index}].set_design.contrast_with_previous is required`);
+  if (plan.context?.set_diversity_policy) {
+    if (!isText(setDesign.content_signature)) errors.push(`images[${index}].set_design.content_signature is required`);
+    if (!isText(setDesign.relation_mode)) errors.push(`images[${index}].set_design.relation_mode is required`);
+    if (isText(setDesign.relation_mode)
+      && Array.isArray(plan.context.scene_affordance_policy?.allowed_relation_modes)
+      && !plan.context.scene_affordance_policy.allowed_relation_modes.includes(setDesign.relation_mode)) {
+      errors.push(`images[${index}].set_design.relation_mode is not allowed by scene_affordance_policy`);
+    }
+    if (!isText(image.expected_page_compliance?.product_human_relation)) {
+      errors.push(`images[${index}].expected_page_compliance.product_human_relation is required`);
+    }
+    const compatibility = assessViewStrategyCompatibility({ shotSpec: image.shot_spec, viewStrategy: image.view_strategy });
+    for (const violation of compatibility.violations) errors.push(`images[${index}].view_strategy ${violation.code}: ${violation.detail}`);
+  }
 
   if (image.business_category === "selling_point") {
     sellingImages.push(image);
@@ -273,6 +297,30 @@ for (const [index, image] of asArray(plan.images).entries()) {
   for (const moduleName of ["product_lock", "image_job", "scene", "composition", "lighting_material", "negative_constraints"]) {
     if (!isText(promptModules[moduleName])) errors.push(`images[${index}].prompt_modules.${moduleName} is required`);
   }
+  const interactionMode = image.shot_spec?.interaction_mode;
+  const requestedView = image.view_strategy?.requested_view;
+  if (["back", "interior", "bottom", "unseen_side"].includes(requestedView)
+    && image.view_strategy?.decision !== "requires_additional_reference") {
+    errors.push(`images[${index}].view_strategy hidden view requires additional reference`);
+  }
+  if (interactionMode === "human_wearing") {
+    if (image.business_category !== "scene" && image.set_design?.render_family !== "fit_context") {
+      errors.push(`images[${index}] human_wearing is only allowed for scene or fit_context images`);
+    }
+    if (image.shot_spec?.framing_mode !== "human_action") errors.push(`images[${index}] human_wearing requires framing_mode=human_action`);
+    if (image.shot_spec?.product_pose !== "in_use") errors.push(`images[${index}] human_wearing requires product_pose=in_use`);
+    if (image.view_strategy?.requires_identity_review !== true) errors.push(`images[${index}] human_wearing requires identity review`);
+    if (!/(穿|戴|佩戴|上脚|使用中|worn|wear)/i.test(promptModules.scene ?? "")) {
+      errors.push(`images[${index}] human_wearing scene must describe the product being worn or used`);
+    }
+  }
+  if (interactionMode === "human_handheld") {
+    if (image.shot_spec?.framing_mode !== "human_action") errors.push(`images[${index}] human_handheld requires framing_mode=human_action`);
+    if (image.shot_spec?.product_pose !== "held") errors.push(`images[${index}] human_handheld requires product_pose=held`);
+  }
+  if (interactionMode === "human_proximity" && image.shot_spec?.framing_mode !== "product_in_context") {
+    errors.push(`images[${index}] human_proximity requires framing_mode=product_in_context`);
+  }
   const productLockLength = [...(promptModules.product_lock ?? "")].length;
   if (productLockLength < 20 || productLockLength > 160) {
     errors.push(`images[${index}].prompt_modules.product_lock must be 20..160 characters (${productLockLength})`);
@@ -317,16 +365,16 @@ for (const [index, image] of asArray(plan.images).entries()) {
     errors.push(`images[${index}] uses a panel/grid layout without framing_mode=multi_panel`);
   }
   const complexityRule = complexityRules[setDesign.scene_complexity];
-  const sceneLength = [...(promptModules.scene ?? "")].length;
-  if (complexityRule && sceneLength < complexityRule.sceneMin) {
-    errors.push(`images[${index}].prompt_modules.scene is too short for ${setDesign.scene_complexity} complexity (${sceneLength} < ${complexityRule.sceneMin})`);
-  }
   if (setDesign.scene_complexity === "complex") {
     if (!isText(promptModules.camera_style)) {
       errors.push(`images[${index}].prompt_modules.camera_style is required for complex scenes`);
     }
-    if (sceneLength <= productLockLength * 1.25) {
-      errors.push(`images[${index}] complex scene detail must be longer than the product lock`);
+    const sceneSignals = [
+      /前景|中景|远景|空间|动线|透视|景深|环境|动作|姿态|步态|景别|foreground|midground|background|perspective|depth|environment|action|pose|walk/i,
+      /承托|接触|投影|受力|遮挡|地面|人物|穿|戴|手指|floor|contact|shadow|worn|person|hand/i,
+    ].filter((pattern) => pattern.test(promptModules.scene)).length;
+    if (sceneSignals < (complexityRule.requiredSceneSignals || 0)) {
+      errors.push(`images[${index}] complex scene lacks spatial or interaction detail`);
     }
   }
   if (isText(promptModules.text_layout)) {
@@ -334,9 +382,6 @@ for (const [index, image] of asArray(plan.images).entries()) {
   }
   if (!isText(image.generation_prompt)) errors.push(`images[${index}].generation_prompt is required`);
   const promptLength = [...(image.generation_prompt ?? "")].length;
-  if (complexityRule && promptLength < complexityRule.promptMin) {
-    errors.push(`images[${index}].generation_prompt is too short for ${setDesign.scene_complexity} complexity (${promptLength} < ${complexityRule.promptMin})`);
-  }
   if (promptLength > 800) errors.push(`images[${index}].generation_prompt exceeds 800 characters (${promptLength})`);
   const normalizedPrompt = (image.generation_prompt ?? "").replace(/\s+/g, "");
   for (const moduleName of moduleNames) {
@@ -383,12 +428,40 @@ for (const [index, image] of asArray(plan.images).entries()) {
     promptModules.scene,
     promptModules.composition,
   ].filter(Boolean).join(" ").replace(/\s+/g, "").toLowerCase());
+  sceneBodies.push(promptModules.scene);
+  roleSignatures.push([
+    setDesign.render_family,
+    image.shot_spec?.framing_mode,
+    image.shot_spec?.interaction_mode,
+  ].join("|"));
+
+  const sceneClauses = promptModules.scene
+    .split(/[。!?;.!?;]+/)
+    .map((clause) => clause.trim())
+    .filter((clause) => clause.length >= 12);
+  if (new Set(sceneClauses).size !== sceneClauses.length) {
+    errors.push(`images[${index}].prompt_modules.scene contains repeated clauses`);
+  }
 
   const claimText = [
     promptModules.image_job,
     promptModules.scene,
     ...asArray(image.overlay_copy),
   ].filter(Boolean).join(" ");
+  const positiveClaimText = claimText.split(/不得|禁止|不展示|不添加|不放在|不提供|do not|avoid/i)[0];
+  const affordancePolicy = plan.context?.scene_affordance_policy ?? {};
+  if (affordancePolicy.support_mode
+    && !affordancePolicy.explicit_surface_override
+    && sceneContainsForbiddenSurface(affordancePolicy, positiveClaimText)) {
+    errors.push(`images[${index}] scene violates scene_affordance_policy forbidden surface for ${affordancePolicy.support_mode}`);
+  }
+  if (affordancePolicy.relation_required === true
+    && ["scene", "decision_support"].includes(image.business_category)
+    && ["product_in_context", "human_action"].includes(image.shot_spec?.framing_mode)
+    && !sceneHasPlausibleRelation(affordancePolicy, positiveClaimText)) {
+    errors.push(`images[${index}] scene lacks a plausible ${affordancePolicy.support_mode} support or interaction relation`);
+  }
+
   for (const claim of asArray(understanding.unsupported_claims).filter(isText)) {
     if (claimText.includes(claim)) errors.push(`images[${index}] uses unsupported claim: ${claim}`);
   }
@@ -414,6 +487,13 @@ if (plan.status === "ready") {
     }
   }
   if (new Set(decisions).size !== decisions.length) errors.push("decision_subject must be unique across all seven images");
+  if (new Set(roleSignatures).size !== roleSignatures.length) {
+    errors.push("render_family + framing_mode + interaction_mode role signatures must be unique across all seven images");
+  }
+  const normalizedScenes = sceneBodies.map((value) => value.replace(/\s+/g, "").toLowerCase());
+  if (new Set(normalizedScenes).size !== normalizedScenes.length) {
+    errors.push("prompt_modules.scene must be unique across all seven images");
+  }
   if (sellingImages.length === 2 && sellingImages[0].selling_point_id === sellingImages[1].selling_point_id) {
     errors.push("selling_point images must use different selling_point_id values");
   }
@@ -427,6 +507,19 @@ if (plan.status === "ready") {
     errors.push(`static product render families may appear at most twice across the set (${staticProductCount})`);
   }
 
+  if (plan.context?.set_diversity_policy) {
+    const diversity = summarizePlannedDiversity(plan.images, plan.context.set_diversity_policy);
+    if (diversity.distinct_content_signatures < diversity.policy.minimum_distinct_content_signatures) {
+      errors.push(`set diversity requires at least ${diversity.policy.minimum_distinct_content_signatures} distinct content signatures`);
+    }
+    if (diversity.human_wearing_pages > diversity.policy.maximum_human_wearing_pages) {
+      errors.push(`human wearing pages exceed set budget (${diversity.human_wearing_pages})`);
+    }
+    for (const [signature, count] of Object.entries(diversity.axis_framing_counts)) {
+      if (count > diversity.policy.maximum_same_axis_framing) errors.push(`axis/framing signature repeats too often: ${signature} (${count})`);
+    }
+  }
+
   const diversityRules = [
     ["narrative_job", 7, 1],
     ["composition_family", 5, 2],

+ 89 - 2
claude-code/fmode-image-set/mcp/src/features/image-set/validate-plan.test.mjs

@@ -7,6 +7,7 @@ import { promisify } from "node:util";
 import test from "node:test";
 import { fileURLToPath } from "node:url";
 import { buildValidPlan, compilePrompt } from "../../../../scripts/test-plan-fixture.mjs";
+import { compileSceneAffordancePolicy } from "./scene-affordance-policy.mjs";
 
 const execFileAsync = promisify(execFile);
 const validatorFile = fileURLToPath(new URL("./validate-plan.mjs", import.meta.url));
@@ -103,7 +104,7 @@ test("validator rejects fake information layouts without a compositor", async ()
   });
 });
 
-test("validator rejects a complex scene compressed to a short template", async () => {
+test("validator rejects a complex scene that lacks spatial or interaction detail", async () => {
   const plan = buildValidPlan();
   const image = plan.images[2];
   image.prompt_modules.scene = "A product on a table.";
@@ -111,7 +112,7 @@ test("validator rejects a complex scene compressed to a short template", async (
   await withPlan(plan, "short-complex-scene.json", async (filename) => {
     await assert.rejects(validate(filename), (error) => {
       const result = JSON.parse(error.stderr);
-      assert.ok(result.errors.some((item) => item.includes("scene is too short for complex complexity")));
+      assert.ok(result.errors.some((item) => item.includes("complex scene lacks spatial or interaction detail")));
       return true;
     });
   });
@@ -159,6 +160,43 @@ test("validator rejects a prompt that rewrites the audited product lock", async
   });
 });
 
+test("validator rejects an internally inconsistent human-wearing route", async () => {
+  const plan = buildValidPlan();
+  const image = plan.images[5];
+  image.shot_spec.interaction_mode = "human_wearing";
+  image.shot_spec.framing_mode = "human_action";
+  image.shot_spec.product_pose = "held";
+  image.view_strategy = {
+    requested_view: "reference_visible_surface",
+    decision: "provider_reference_generation",
+    requires_identity_review: true,
+  };
+  await withPlan(plan, "invalid-human-wearing.json", async (filename) => {
+    await assert.rejects(validate(filename), (error) => {
+      const result = JSON.parse(error.stderr);
+      assert.ok(result.errors.some((item) => item.includes("human_wearing requires product_pose=in_use")));
+      assert.ok(result.errors.some((item) => item.includes("scene must describe the product being worn")));
+      return true;
+    });
+  });
+});
+
+test("validator keeps hidden product views behind the additional-reference gate", async () => {
+  const plan = buildValidPlan();
+  plan.images[2].view_strategy = {
+    requested_view: "back",
+    decision: "derived_affine",
+    requires_identity_review: true,
+  };
+  await withPlan(plan, "hidden-view-without-reference.json", async (filename) => {
+    await assert.rejects(validate(filename), (error) => {
+      const result = JSON.parse(error.stderr);
+      assert.ok(result.errors.some((item) => item.includes("hidden view requires additional reference")));
+      return true;
+    });
+  });
+});
+
 test("validator rejects invented closure details that contradict the identity manifest", async () => {
   const plan = buildValidPlan();
   plan.images[1].prompt_modules.scene += " 画面清楚展示单排扣闭合结构。";
@@ -185,3 +223,52 @@ test("validator allows only one detail frame and one multi-panel frame", async (
     });
   });
 });
+
+test("validator rejects an implausible footwear tabletop context", async () => {
+  const plan = buildValidPlan("canvas shoes");
+  plan.context.interaction_profile = { kind: "wearable", wearable_subtype: "footwear" };
+  plan.context.scene_policy = { kind: "footwear_floor_or_wearing", explicit_surface_override: false, rejected_hints: [] };
+  plan.context.scene_affordance_policy = compileSceneAffordancePolicy({
+    understanding: {
+      category: "鞋履",
+      product_name: { value: "帆布鞋" },
+      visible_facts: ["帆布鞋"],
+    },
+  });
+  plan.images[2].prompt_modules.scene = "完整双鞋放在办公桌中央,桌面道具建立生活氛围。";
+  plan.images[2].generation_prompt = compilePrompt(plan.images[2].prompt_modules);
+  await withPlan(plan, "footwear-tabletop.json", async (filename) => {
+    await assert.rejects(validate(filename), (error) => {
+      const result = JSON.parse(error.stderr);
+      assert.ok(result.errors.some((item) => item.includes("scene_affordance_policy forbidden surface")));
+      return true;
+    });
+  });
+});
+
+test("validator rejects repeated scene clauses instead of accepting character padding", async () => {
+  const plan = buildValidPlan();
+  plan.images[2].prompt_modules.scene = "前景承托面保持真实透视,商品位于中景。前景承托面保持真实透视,商品位于中景。";
+  plan.images[2].generation_prompt = compilePrompt(plan.images[2].prompt_modules);
+  await withPlan(plan, "repeated-scene-clause.json", async (filename) => {
+    await assert.rejects(validate(filename), (error) => {
+      const result = JSON.parse(error.stderr);
+      assert.ok(result.errors.some((item) => item.includes("scene contains repeated clauses")));
+      return true;
+    });
+  });
+});
+
+test("validator rejects duplicate set-level role signatures", async () => {
+  const plan = buildValidPlan();
+  plan.images[5].set_design.render_family = plan.images[2].set_design.render_family;
+  plan.images[5].shot_spec.framing_mode = plan.images[2].shot_spec.framing_mode;
+  plan.images[5].shot_spec.interaction_mode = plan.images[2].shot_spec.interaction_mode;
+  await withPlan(plan, "duplicate-role-signature.json", async (filename) => {
+    await assert.rejects(validate(filename), (error) => {
+      const result = JSON.parse(error.stderr);
+      assert.ok(result.errors.some((item) => item.includes("role signatures must be unique")));
+      return true;
+    });
+  });
+});

+ 74 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/view-strategy-compatibility.mjs

@@ -0,0 +1,74 @@
+const asArray = (value) => Array.isArray(value) ? value : [];
+const text = (value, fallback = "") => typeof value === "string" && value.trim() ? value.trim() : fallback;
+
+export const VIEW_STRATEGY_COMPATIBILITY = Object.freeze({
+  reference_exact: {
+    camera_axes: ["front"],
+    framing_modes: ["full_product", "product_in_context"],
+    fallback_axis: "front",
+  },
+  reference_crop: {
+    camera_axes: ["front", "macro"],
+    framing_modes: ["detail", "multi_panel", "full_product"],
+    fallback_axis: "macro",
+  },
+  derived_affine: {
+    camera_axes: ["front", "three_quarter_left", "three_quarter_right", "top", "low"],
+    framing_modes: ["full_product", "product_in_context", "detail", "multi_panel"],
+    fallback_axis: "three_quarter_right",
+  },
+  provider_reference_generation: {
+    camera_axes: ["front", "three_quarter_left", "three_quarter_right", "low", "macro"],
+    framing_modes: ["full_product", "product_in_context", "detail", "human_action"],
+    fallback_axis: "three_quarter_right",
+  },
+});
+
+export function assessViewStrategyCompatibility({ shotSpec = {}, viewStrategy = {} } = {}) {
+  const decision = text(viewStrategy.decision);
+  const rule = VIEW_STRATEGY_COMPATIBILITY[decision];
+  const cameraAxis = text(shotSpec.camera_axis);
+  const framingMode = text(shotSpec.framing_mode);
+  const violations = [];
+  if (!rule) violations.push({ code: "VIEW_STRATEGY_UNKNOWN", detail: `Unknown decision: ${decision || "empty"}` });
+  if (rule && !rule.camera_axes.includes(cameraAxis)) {
+    violations.push({
+      code: "CAMERA_AXIS_STRATEGY_INCOMPATIBLE",
+      detail: `${decision} cannot truthfully produce camera_axis=${cameraAxis}`,
+    });
+  }
+  if (rule && !rule.framing_modes.includes(framingMode)) {
+    violations.push({
+      code: "FRAMING_STRATEGY_INCOMPATIBLE",
+      detail: `${decision} cannot truthfully produce framing_mode=${framingMode}`,
+    });
+  }
+  if (decision === "reference_crop" && asArray(viewStrategy.allowed_reveal).some((value) => value !== "reference_visible_surface")) {
+    violations.push({ code: "REFERENCE_CROP_REVEAL_INCOMPATIBLE", detail: "reference_crop may only reveal the reference-visible surface" });
+  }
+  return { compatible: violations.length === 0, decision, camera_axis: cameraAxis, framing_mode: framingMode, violations };
+}
+
+export function enforceViewStrategyCompatibility({ shotSpec = {}, viewStrategy = {} } = {}) {
+  const assessment = assessViewStrategyCompatibility({ shotSpec, viewStrategy });
+  if (assessment.compatible) return { shot_spec: shotSpec, view_strategy: viewStrategy, compatibility: assessment };
+  const rule = VIEW_STRATEGY_COMPATIBILITY[assessment.decision];
+  if (!rule) return { shot_spec: shotSpec, view_strategy: viewStrategy, compatibility: assessment };
+  const framingMode = rule.framing_modes.includes(shotSpec.framing_mode)
+    ? shotSpec.framing_mode
+    : assessment.decision === "reference_crop" ? "detail" : rule.framing_modes[0];
+  const cameraAxis = rule.camera_axes.includes(shotSpec.camera_axis)
+    ? shotSpec.camera_axis
+    : assessment.decision === "reference_crop" && framingMode === "full_product" ? "front" : rule.fallback_axis;
+  const normalizedShot = { ...shotSpec, camera_axis: cameraAxis, framing_mode: framingMode };
+  return {
+    shot_spec: normalizedShot,
+    view_strategy: viewStrategy,
+    compatibility: {
+      ...assessViewStrategyCompatibility({ shotSpec: normalizedShot, viewStrategy }),
+      normalized: true,
+      original_violations: assessment.violations,
+    },
+  };
+}
+

+ 23 - 0
claude-code/fmode-image-set/mcp/src/features/image-set/view-strategy-compatibility.test.mjs

@@ -0,0 +1,23 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { assessViewStrategyCompatibility, enforceViewStrategyCompatibility } from "./view-strategy-compatibility.mjs";
+
+test("reference crop rejects a claimed side camera axis", () => {
+  const result = assessViewStrategyCompatibility({
+    shotSpec: { camera_axis: "side", framing_mode: "full_product" },
+    viewStrategy: { decision: "reference_crop", allowed_reveal: ["reference_visible_surface"] },
+  });
+  assert.equal(result.compatible, false);
+  assert.ok(result.violations.some((item) => item.code === "CAMERA_AXIS_STRATEGY_INCOMPATIBLE"));
+});
+
+test("compatibility normalization never upgrades a crop into an unsupported hidden view", () => {
+  const result = enforceViewStrategyCompatibility({
+    shotSpec: { camera_axis: "side", framing_mode: "full_product" },
+    viewStrategy: { decision: "reference_crop", allowed_reveal: ["reference_visible_surface"] },
+  });
+  assert.equal(result.compatibility.compatible, true);
+  assert.equal(result.shot_spec.camera_axis, "front");
+  assert.equal(result.view_strategy.decision, "reference_crop");
+});
+

+ 9 - 5
claude-code/fmode-image-set/mcp/src/providers/claude-attachment-client.mjs

@@ -83,7 +83,7 @@ function contentParts(content) {
   return [];
 }
 
-function latestSkillInvocation(records, skillName, expectedCwd) {
+function latestCurrentInvocation(records, skillName, expectedCwd) {
   const groups = new Map();
   records.forEach((record, index) => {
     if (record.type !== "user" || record.message?.role !== "user" || !record.promptId) return;
@@ -113,9 +113,11 @@ function latestSkillInvocation(records, skillName, expectedCwd) {
   ];
   return [...groups.values()]
     .filter((group) => group.cwd && normalizePath(group.cwd) === expected)
-    .filter((group) => {
+    .map((group) => {
       const text = group.texts.join("\n");
-      return text.includes(commandMarker) || skillPathMarkers.some((marker) => text.toLowerCase().includes(marker.toLowerCase()));
+      const skillMarked = text.includes(commandMarker)
+        || skillPathMarkers.some((marker) => text.toLowerCase().includes(marker.toLowerCase()));
+      return { ...group, invocationMode: skillMarked ? "skill_invocation" : "current_user_prompt" };
     })
     .sort((a, b) => {
       const timeDifference = Date.parse(b.timestamp || 0) - Date.parse(a.timestamp || 0);
@@ -169,7 +171,7 @@ export async function inspectLatestClaudeInvocation({
     } catch {
       continue;
     }
-    const invocation = latestSkillInvocation(parsed.records, skillName, cwd);
+    const invocation = latestCurrentInvocation(parsed.records, skillName, cwd);
     if (!invocation) continue;
     candidates.push({
       file,
@@ -179,7 +181,7 @@ export async function inspectLatestClaudeInvocation({
     });
   }
   if (candidates.length === 0) {
-    throw new AttachmentResolutionError("CLAUDE_ATTACHMENT_NOT_FOUND", "No recent Claude Code skill invocation was found for the current project");
+    throw new AttachmentResolutionError("CLAUDE_ATTACHMENT_NOT_FOUND", "No recent Claude Code user request was found for the current project");
   }
   candidates.sort((a, b) => {
     const timeDifference = Date.parse(b.timestamp || 0) - Date.parse(a.timestamp || 0);
@@ -195,6 +197,7 @@ export async function inspectLatestClaudeInvocation({
     session_id: current.sessionId,
     captured_at: current.timestamp,
     image_count: current.images.length,
+    invocation_mode: current.invocationMode,
     current,
   };
 }
@@ -240,6 +243,7 @@ export async function extractLatestClaudeAttachment({
     prompt_id: current.promptId,
     session_id: current.sessionId,
     captured_at: current.timestamp,
+    invocation_mode: invocation.invocation_mode,
   };
 }
 

+ 53 - 1
claude-code/fmode-image-set/mcp/src/providers/claude-attachment-client.test.mjs

@@ -71,12 +71,57 @@ test("extracts the unique image from the latest matching Claude skill invocation
     assert.equal(result.status, "complete");
     assert.equal(result.prompt_id, "prompt-current");
     assert.equal(result.media_type, "image/jpeg");
+    assert.equal(result.invocation_mode, "skill_invocation");
     assert.deepEqual(await readFile(result.file_path), jpeg);
   } finally {
     await rm(directory, { recursive: true, force: true });
   }
 });
 
+test("extracts the unique image from the latest user request before the skill is loaded", async () => {
+  const directory = await mkdtemp(join(tmpdir(), "commerce-direct-attachment-test-"));
+  try {
+    const configDirectory = join(directory, ".claude");
+    const timestamp = new Date().toISOString();
+    await writeSession(configDirectory, "77777777-7777-7777-7777-777777777777", [userRecord({
+      promptId: "prompt-direct",
+      timestamp,
+      content: [
+        { type: "image", source: { type: "base64", media_type: "image/jpeg", data: jpeg.toString("base64") } },
+        { type: "text", text: "请使用 fmode-image-set,只执行 start" },
+      ],
+    })]);
+    const result = await extractLatestClaudeAttachment({
+      cwd,
+      outputDirectory: join(directory, "run"),
+      claudeConfigDirectory: configDirectory,
+      now: Date.parse(timestamp) + 1000,
+    });
+    assert.equal(result.prompt_id, "prompt-direct");
+    assert.equal(result.invocation_mode, "current_user_prompt");
+  } finally {
+    await rm(directory, { recursive: true, force: true });
+  }
+});
+
+test("a newer user request without an image never reuses an older skill image", async () => {
+  const directory = await mkdtemp(join(tmpdir(), "commerce-direct-stale-test-"));
+  try {
+    const configDirectory = join(directory, ".claude");
+    const old = invocationRecords("prompt-old", "2026-07-31T07:00:00.000Z");
+    const current = userRecord({ promptId: "prompt-new", timestamp: "2026-07-31T07:01:00.000Z", content: "继续处理" });
+    await writeSession(configDirectory, "88888888-8888-8888-8888-888888888888", [...old, current]);
+    await assert.rejects(extractLatestClaudeAttachment({
+      cwd,
+      outputDirectory: join(directory, "run"),
+      claudeConfigDirectory: configDirectory,
+      now: Date.parse("2026-07-31T07:01:01.000Z"),
+    }), (error) => error.code === "CLAUDE_ATTACHMENT_NOT_FOUND");
+  } finally {
+    await rm(directory, { recursive: true, force: true });
+  }
+});
+
 test("does not reuse an older attachment when the latest invocation has no image", async () => {
   const directory = await mkdtemp(join(tmpdir(), "commerce-attachment-stale-test-"));
   try {
@@ -102,7 +147,7 @@ test("fails closed when the current invocation contains multiple images", async
   const directory = await mkdtemp(join(tmpdir(), "commerce-attachment-ambiguous-test-"));
   try {
     const configDirectory = join(directory, ".claude");
-    const timestamp = "2026-07-23T06:31:00.000Z";
+    const timestamp = new Date().toISOString();
     await writeSession(configDirectory, "33333333-3333-3333-3333-333333333333", invocationRecords("prompt-current", timestamp, [jpeg, jpeg]));
     await assert.rejects(
       extractLatestClaudeAttachment({
@@ -142,6 +187,13 @@ test("product vision client accepts the Claude attachment bridge without a manua
     assert.equal(result.vision_called, false);
     assert.equal(result.reference.kind, "local");
     assert.equal(result.reference.attachment.prompt_id, "prompt-current");
+    assert.equal(result.reference.attachment.invocation_mode, "skill_invocation");
+    assert.deepEqual(result.external_calls, {
+      vision_understanding: 0,
+      fact_audit: 0,
+      asset_upload: 0,
+      paid_image_generation: 0,
+    });
     assert.ok(result.reference.source.startsWith(paths.attachments_directory));
     assert.deepEqual(await readFile(result.reference.source), jpeg);
   } finally {

+ 35 - 3
claude-code/fmode-image-set/mcp/src/providers/commerce-vision-provider.mjs

@@ -1,6 +1,7 @@
 import { readFile } from "node:fs/promises";
 import { homedir } from "node:os";
 import { resolve } from "node:path";
+import { normalizePageCompliance } from "../features/image-set/production-result-verifier.mjs";
 
 const sleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
 
@@ -106,20 +107,50 @@ export async function reviewGeneratedProduct({
   referenceUrl,
   generatedUrl,
   identityManifest,
+  pagePlan = null,
   config = null,
 }) {
   const effectiveConfig = config ?? await loadVisionConfig();
-  const instruction = `你是商品身份一致性审核器。第一张图是原始商品参考图,第二张图是生成图。只检查商品是否仍为同一款,不评价摄影美感。商品身份约束:
+  const pageContract = pagePlan ? {
+    decision_subject: pagePlan.decision_subject,
+    shot_spec: pagePlan.shot_spec,
+    expected_page_compliance: pagePlan.expected_page_compliance,
+    view_strategy: pagePlan.view_strategy,
+    evidence_refs: pagePlan.evidence_refs,
+  } : null;
+  const complianceInstruction = pageContract ? `\n同时审查第二张图的实际画面是否完成本页任务。页面合同:
+${JSON.stringify(pageContract)}
+不要根据合同复述计划值,必须根据第二张图报告实际值。所有区域坐标使用左上角为原点的 0–1 归一化 x/y/width/height;无法可靠定位时返回 {"status":"unverifiable","boxes":[]}。人物存在但未穿戴商品时必须报告 human_proximity;人物实际穿戴商品时必须报告 human_wearing_static 或 human_wearing_action。` : "";
+  const pageSchema = pageContract ? `,
+  "page_compliance": {
+    "actual_camera_axis":"front|three_quarter_left|three_quarter_right|side|top|low|macro|unknown",
+    "actual_framing_mode":"full_product|product_in_context|detail|multi_panel|human_action|unknown",
+    "product_occupancy_pct": 0,
+    "product_human_relation":"isolated|detail|stable_display|environmental|human_proximity|human_handheld|human_wearing_static|human_wearing_action|unknown",
+    "scene_family":"简短实际场景类别",
+    "composition_family":"简短实际构图类别",
+    "page_job_compliant":true,
+    "visible_evidence":["实际可见证据"],
+    "forbidden_reveals":["back|interior|bottom|unseen_side"],
+    "violations":[{"code":"SCENE_MISMATCH|RELATION_MISMATCH|JOB_NOT_SATISFIED|FORBIDDEN_REVEAL|OTHER","detail":"具体问题"}],
+    "product_region":{"status":"verified|unverifiable","boxes":[{"x":0,"y":0,"width":0,"height":0}]},
+    "face_regions":{"status":"verified|unverifiable","boxes":[]},
+    "critical_product_regions":{"status":"verified|unverifiable","boxes":[]},
+    "recommended_text_safe_regions":{"status":"verified|unverifiable","boxes":[]},
+    "notes":"简短说明"
+  }` : "";
+  const instruction = `你是商品身份与页面结果审核器。第一张图是原始商品参考图,第二张图是生成图。商品身份判断不评价摄影美感。商品身份约束:
 ${JSON.stringify(identityManifest)}
+${complianceInstruction}
 返回严格 JSON:
 {
   "same_product": true,
   "confidence": "high|medium|low",
   "preserved_features": ["已保持的关键特征"],
   "critical_violations": [{"code":"ADDED_STRUCTURE|REMOVED_STRUCTURE|SHAPE_CHANGED|COLOR_CHANGED|MATERIAL_APPEARANCE_CHANGED|TEXT_CHANGED|MULTI_PRODUCT_INCONSISTENT","detail":"具体问题"}],
-  "notes": "简短说明"
+  "notes": "简短说明"${pageSchema}
 }
-只要新增或删除结构、颜色或轮廓明显变化、多格图中的商品互不一致,就必须 same_product=false。`;
+只要新增或删除结构、颜色或轮廓明显变化、多格图中的商品互不一致,就必须 same_product=false。页面任务不符合不应改变 same_product,而应写入 page_compliance。`;
   const { value, usage } = await requestVisionJson({
     config: effectiveConfig,
     imageUrls: [referenceUrl, generatedUrl],
@@ -142,6 +173,7 @@ ${JSON.stringify(identityManifest)}
       : [],
     critical_violations: violations,
     notes: String(value.notes ?? "").trim(),
+    ...(pageContract ? { page_compliance: normalizePageCompliance(value.page_compliance || {}) } : {}),
     usage,
   };
 }

+ 41 - 0
claude-code/fmode-image-set/mcp/src/providers/commerce-vision-provider.test.mjs

@@ -57,3 +57,44 @@ test("generated-product review rejects structural drift", async () => {
     await new Promise((resolve) => server.close(resolve));
   }
 });
+
+test("one generated-product review call also returns normalized page compliance", async () => {
+  let calls = 0;
+  const server = createServer(async (request, response) => {
+    calls += 1;
+    let body = "";
+    for await (const chunk of request) body += chunk;
+    const parsed = JSON.parse(body);
+    const instruction = parsed.messages[0].content.find((item) => item.type === "text").text;
+    assert.match(instruction, /页面合同/);
+    assert.match(instruction, /0–1 归一化/);
+    response.writeHead(200, { "Content-Type": "application/json" });
+    response.end(JSON.stringify({ choices: [{ message: { content: JSON.stringify({
+      same_product: true, confidence: "high", preserved_features: ["驼色"], critical_violations: [], notes: "ok",
+      page_compliance: {
+        actual_camera_axis: "front", actual_framing_mode: "human_action", product_occupancy_pct: 64,
+        product_human_relation: "human_wearing_static", scene_family: "street", composition_family: "front_model",
+        page_job_compliant: true, visible_evidence: ["腰带"], forbidden_reveals: [], violations: [],
+        product_region: { status: "verified", boxes: [{ x: 0.3, y: 0.2, width: 0.4, height: 0.65 }] },
+        face_regions: { status: "verified", boxes: [{ x: 0.43, y: 0.03, width: 0.14, height: 0.13 }] },
+        critical_product_regions: "unverifiable", recommended_text_safe_regions: "unverifiable"
+      },
+    }) } }] }));
+  });
+  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
+  try {
+    const result = await reviewGeneratedProduct({
+      referenceUrl: "https://images.example.test/reference.png", generatedUrl: "https://images.example.test/generated.png",
+      identityManifest: { must_preserve: ["驼色"], must_not_add: [], unknown_do_not_assert: [] },
+      pagePlan: { decision_subject: "静态穿戴", shot_spec: { camera_axis: "front" }, expected_page_compliance: { product_human_relation: "human_wearing_static" } },
+      config: { apiKey: "test-key", model: "test-vl", baseUrl: `http://127.0.0.1:${server.address().port}/v1` },
+    });
+    assert.equal(calls, 1);
+    assert.equal(result.accepted, true);
+    assert.equal(result.page_compliance.product_human_relation, "human_wearing_static");
+    assert.equal(result.page_compliance.product_region.status, "verified");
+    assert.equal(result.page_compliance.recommended_text_safe_regions.status, "unverifiable");
+  } finally {
+    await new Promise((resolve) => server.close(resolve));
+  }
+});

+ 101 - 60
claude-code/fmode-image-set/mcp/src/providers/jimeng-v4-client.mjs

@@ -12,7 +12,7 @@ import { loadVisionConfig, reviewGeneratedProduct } from "./commerce-vision-prov
 const args = process.argv.slice(2);
 const planFileArg = args[0];
 if (!planFileArg || planFileArg.startsWith("--")) {
-  throw new Error("Usage: node jimeng-v4-client.mjs <prompt-plan.json> --dry-run|--prepare-assets|--execute|--download-only [--vision-result vision-result.json] [--max-cost 1.54] [--asset-map assets.json] [--output result.json] [--download-timeout-seconds 180]");
+  throw new Error("Usage: node jimeng-v4-client.mjs <plan.json> --dry-run|--prepare-assets|--execute|--download-only [--vision-result vision-result.json] [--max-cost amount] [--asset-map assets.json] [--output result.json] [--download-timeout-seconds 180]");
 }
 
 const flag = (name) => args.includes(name);
@@ -38,37 +38,47 @@ if (requestedModes.length === 0) console.warn("No mode supplied; defaulting to d
 const planFile = resolve(planFileArg);
 const planText = await readFile(planFile, "utf8");
 const plan = JSON.parse(planText);
-if (plan.status !== "ready" || !Array.isArray(plan.images) || plan.images.length !== 7) {
-  throw new Error("Prompt plan must be ready and contain seven images");
+const prototypePlanTypes = new Set(["reference_assets", "reference_first_set", "claude_authored_reference_first_set"]);
+const isPrototypePlan = prototypePlanTypes.has(plan.plan_type);
+const isReferenceAssetPlan = plan.plan_type === "reference_assets";
+const requiredImageCount = isReferenceAssetPlan ? 2 : 7;
+if (plan.status !== "ready" || !Array.isArray(plan.images) || plan.images.length !== requiredImageCount) {
+  throw new Error(`Plan must be ready and contain ${requiredImageCount} images`);
 }
 const canonicalPaths = pathsForRun(plan.run_id);
-assertRunFile(planFile, canonicalPaths.run_directory, "prompt-plan.json");
+assertRunFile(
+  planFile,
+  canonicalPaths.run_directory,
+  isReferenceAssetPlan ? "reference-asset-plan.json" : "prompt-plan.json",
+);
 const execFileAsync = promisify(execFile);
 const validatorFile = fileURLToPath(new URL("../features/image-set/validate-plan.mjs", import.meta.url));
-try {
-  await execFileAsync(process.execPath, [validatorFile, planFile]);
-} catch (error) {
-  let validation = null;
+if (!isPrototypePlan) {
   try {
-    validation = JSON.parse(error?.stderr || "");
-  } catch {
-    validation = null;
+    await execFileAsync(process.execPath, [validatorFile, planFile]);
+  } catch (error) {
+    let validation = null;
+    try {
+      validation = JSON.parse(error?.stderr || "");
+    } catch {
+      validation = null;
+    }
+    console.log(JSON.stringify({
+      status: "blocked",
+      jimeng_called: false,
+      error: {
+        code: "PLAN_VALIDATION_FAILED",
+        message: validation?.errors?.join("; ") || error?.message || "Prompt plan validation failed",
+      },
+    }, null, 2));
+    process.exit(0);
   }
-  console.log(JSON.stringify({
-    status: "blocked",
-    jimeng_called: false,
-    error: {
-      code: "PLAN_VALIDATION_FAILED",
-      message: validation?.errors?.join("; ") || error?.message || "Prompt plan validation failed",
-    },
-  }, null, 2));
-  process.exit(0);
 }
 
 let visionResult = null;
 let visionResultFile = null;
 let visionResultText = "";
-if (mode === "execute") {
+if (mode === "execute" && !isPrototypePlan) {
   const visionResultArg = option("--vision-result", "");
   if (!visionResultArg) {
     console.log(JSON.stringify({
@@ -128,16 +138,19 @@ const assetMapFile = assertRunFile(
   "reference-assets.json",
 );
 const resultOutputFile = assertRunFile(
-  resolve(option("--output", canonicalPaths.jimeng_result)),
+  resolve(option("--output", isReferenceAssetPlan ? canonicalPaths.reference_asset_result : canonicalPaths.jimeng_result)),
   canonicalPaths.run_directory,
-  "jimeng-result.json",
+  isReferenceAssetPlan ? "reference-asset-result.json" : "jimeng-result.json",
 );
-const downloadDir = resolve(option("--download-dir", canonicalPaths.images_directory));
+const expectedDownloadDirectory = isReferenceAssetPlan
+  ? canonicalPaths.reference_assets_directory
+  : canonicalPaths.images_directory;
+const downloadDir = resolve(option("--download-dir", expectedDownloadDirectory));
 const samePath = (left, right) => process.platform === "win32"
   ? resolve(left).toLowerCase() === resolve(right).toLowerCase()
   : resolve(left) === resolve(right);
-if (!samePath(downloadDir, canonicalPaths.images_directory)) {
-  throw new Error(`RUN_PATH_INVALID: images must be downloaded to ${canonicalPaths.images_directory}`);
+if (!samePath(downloadDir, expectedDownloadDirectory)) {
+  throw new Error(`RUN_PATH_INVALID: images must be downloaded to ${expectedDownloadDirectory}`);
 }
 const downloadTimeoutSeconds = Math.max(10, Number(option("--download-timeout-seconds", "180")) || 180);
 const forwardPath = (value) => resolve(value).replace(/\\/g, "/");
@@ -172,6 +185,8 @@ const localRoleNames = {
   decision_support: "购买决策图",
 };
 function localImageStem(slotId) {
+  if (slotId === "three-view") return "产品三视图";
+  if (slotId === "setting-board") return "产品设定图";
   const image = plan.images.find((item) => item.id === slotId);
   return `${slotId}-${localRoleNames[image?.business_category] ?? "商品图"}`;
 }
@@ -315,7 +330,10 @@ function buildTasks() {
     const pendingLocal = references.some((reference) => reference.kind === "local" && !urlBySource.has(reference.source));
     const negative = image.negative_prompt?.trim();
     const prompt = negative ? `${image.generation_prompt}\n禁止:${negative}` : image.generation_prompt;
-    if ([...prompt].length > 800) throw new Error(`Slot ${image.id} final prompt exceeds 800 characters`);
+    const maximumPromptLength = isPrototypePlan ? 1600 : 800;
+    if ([...prompt].length > maximumPromptLength) {
+      throw new Error(`Slot ${image.id} final prompt exceeds ${maximumPromptLength} characters`);
+    }
     return {
       slot_id: image.id,
       endpoint: "https://server.fmode.cn/api/volcengine/jimeng/getImgV4",
@@ -354,7 +372,7 @@ if (mode === "execute") {
 await prepareLocalAssets();
 const tasks = buildTasks();
 if (tasks.some((task) => task.payload.image_urls.length === 0)) {
-  throw new Error("All seven slots require at least one product reference image; local uploads must succeed before Jimeng is called");
+  throw new Error("All slots require at least one product reference image; local uploads must succeed before Jimeng is called");
 }
 
 if (mode === "prepare-assets") {
@@ -392,8 +410,9 @@ if (mode === "execute") {
         ? `${new URL(submitUrl).origin}/parse/classes/ImagineWork/connection-preflight`
         : ""),
   );
-  const qualityConfig = await loadVisionConfig();
-  if (!qualityConfig.apiKey) {
+  const contentReviewEnabled = !isPrototypePlan;
+  const qualityConfig = contentReviewEnabled ? await loadVisionConfig() : null;
+  if (contentReviewEnabled && !qualityConfig.apiKey) {
     console.log(JSON.stringify({
       status: "blocked",
       jimeng_called: false,
@@ -461,7 +480,9 @@ if (mode === "execute") {
       verifyState(existing, stateSecret);
       if (existing.run_id !== plan.run_id) throw new Error("RUN_STATE_PLAN_MISMATCH: run_id changed");
       if (existing.plan_sha256 !== sha256(planText)) throw new Error("RUN_STATE_PLAN_MISMATCH: prompt-plan.json changed after execution started");
-      if (existing.vision_sha256 !== sha256(visionResultText)) throw new Error("RUN_STATE_VISION_MISMATCH: vision-result.json changed after execution started");
+      if (contentReviewEnabled && existing.vision_sha256 !== sha256(visionResultText)) {
+        throw new Error("RUN_STATE_VISION_MISMATCH: vision-result.json changed after execution started");
+      }
     } catch (error) {
       if (error?.code !== "ENOENT") throw new Error(`Cannot read run state: ${error.message}`);
     }
@@ -475,9 +496,11 @@ if (mode === "execute") {
       status: "running",
       estimated_cost_cny: estimatedCost,
       asset_uploads: assetUploads,
+      plan_type: plan.plan_type || "legacy_seven_image_set",
+      content_review_enabled: contentReviewEnabled,
       vision_result: visionResultFile,
       plan_sha256: sha256(planText),
-      vision_sha256: sha256(visionResultText),
+      vision_sha256: contentReviewEnabled ? sha256(visionResultText) : null,
       started_at: existing?.started_at || new Date().toISOString(),
       updated_at: new Date().toISOString(),
       issues: Array.isArray(existing?.issues) ? existing.issues : [],
@@ -503,7 +526,18 @@ if (mode === "execute") {
     await writeFile(outputFile, `${JSON.stringify(signed, null, 2)}\n`, "utf8");
   }
   async function materializeCompletedImages(entry) {
-    if (!Array.isArray(entry.images) || entry.images.length === 0 || entry.local_images?.length === entry.images.length) return;
+    if (!Array.isArray(entry.images) || entry.images.length === 0) return;
+    const existingFiles = Array.isArray(entry.local_images) && entry.local_images.length === entry.images.length
+      ? await Promise.all(entry.local_images.map(async (file) => {
+        try {
+          const info = await stat(file);
+          return info.isFile() && info.size > 0;
+        } catch {
+          return false;
+        }
+      }))
+      : [];
+    if (existingFiles.length === entry.images.length && existingFiles.every(Boolean)) return;
     entry.local_images = await retrySafe(() => downloadImagesForSlot(entry.slot_id, entry.images));
     delete entry.download_error;
   }
@@ -607,33 +641,37 @@ if (mode === "execute") {
             await recordIssue(entry, "download_failed", error);
             return false;
           }
-          entry.status = "quality_review";
-          await saveRunState();
-          try {
-            const reviews = [];
-            for (const generatedUrl of entry.images) {
-              reviews.push(await retrySafe(() => reviewGeneratedProduct({
-                referenceUrl: visionResult.reference.public_url,
-                generatedUrl,
-                identityManifest: visionResult.product_understanding.identity_manifest,
-                config: qualityConfig,
-              })));
-            }
-            entry.quality_review = {
-              accepted: reviews.every((review) => review.accepted),
-              reviews,
-              reviewed_at: new Date().toISOString(),
-            };
-            if (!entry.quality_review.accepted) {
-              const violations = reviews.flatMap((review) => review.critical_violations ?? []);
-              await recordIssue(entry, "quality_rejected", new Error(
-                violations.map((item) => `${item.code}: ${item.detail}`).join("; ") || "Generated product no longer matches the reference",
-              ));
+          if (contentReviewEnabled) {
+            entry.status = "quality_review";
+            await saveRunState();
+            try {
+              const reviews = [];
+              for (const generatedUrl of entry.images) {
+                reviews.push(await retrySafe(() => reviewGeneratedProduct({
+                  referenceUrl: visionResult.reference.public_url,
+                  generatedUrl,
+                  identityManifest: visionResult.product_understanding.identity_manifest,
+                  pagePlan: plan.images.find((image) => image.id === entry.slot_id) || null,
+                  config: qualityConfig,
+                })));
+              }
+              entry.quality_review = {
+                accepted: reviews.every((review) => review.accepted),
+                reviews,
+                page_compliance: reviews[0]?.page_compliance || null,
+                reviewed_at: new Date().toISOString(),
+              };
+              if (!entry.quality_review.accepted) {
+                const violations = reviews.flatMap((review) => review.critical_violations ?? []);
+                await recordIssue(entry, "quality_rejected", new Error(
+                  violations.map((item) => `${item.code}: ${item.detail}`).join("; ") || "Generated product no longer matches the reference",
+                ));
+                return false;
+              }
+            } catch (error) {
+              await recordIssue(entry, "quality_review_failed", error);
               return false;
             }
-          } catch (error) {
-            await recordIssue(entry, "quality_review_failed", error);
-            return false;
           }
           entry.status = "complete";
           await saveRunState();
@@ -671,7 +709,7 @@ if (mode === "execute") {
   // Unknown submissions are never resubmitted because the remote service has no idempotency key.
   for (const task of tasks) {
     const entry = entryBySlot.get(task.slot_id);
-    if (entry.status === "complete" && entry.quality_review?.accepted) {
+    if (entry.status === "complete" && (!contentReviewEnabled || entry.quality_review?.accepted)) {
       try {
         await materializeCompletedImages(entry);
         await saveRunState();
@@ -719,7 +757,7 @@ if (mode === "execute") {
     min: confirmedMinimumCost,
     max: Number((confirmedMinimumCost + potentialAdditionalCost).toFixed(2)),
   };
-  runState.status = completed === 7 ? "complete" : "partial";
+  runState.status = completed === requiredImageCount ? "complete" : "partial";
   runState.summary = {
     completed,
     failed,
@@ -745,6 +783,9 @@ if (mode === "execute") {
   console.log(JSON.stringify({
     output: outputFile,
     ...deliveryMetadata(completedItems),
+    remote_images: runState.results
+      .filter((item) => item.status === "complete")
+      .flatMap((item) => item.images ?? []),
     status: runState.status,
     completed,
     pending,

+ 85 - 0
claude-code/fmode-image-set/mcp/src/providers/jimeng-v4-client.test.mjs

@@ -8,6 +8,7 @@ import { fileURLToPath } from "node:url";
 import { buildValidPlan } from "../../../scripts/test-plan-fixture.mjs";
 import { createRun } from "../core/run-paths.mjs";
 import { verifyState } from "../core/run-state-integrity.mjs";
+import { compileReferenceAssetPlan } from "../features/image-set/reference-asset-plan-compiler.mjs";
 
 const execFileAsync = promisify(execFile);
 const executorFile = fileURLToPath(new URL("./jimeng-v4-client.mjs", import.meta.url));
@@ -18,6 +19,90 @@ function slotFromPayload(payload) {
     ?? "unknown";
 }
 
+test("prototype reference assets execute without plan validation or vision review", async () => {
+  let visionCalls = 0;
+  let submissions = 0;
+  let imageDownloads = 0;
+  const server = createServer(async (request, response) => {
+    const base = `http://127.0.0.1:${server.address().port}`;
+    if (request.method === "POST" && request.url === "/submit") {
+      submissions += 1;
+      let body = "";
+      for await (const chunk of request) body += chunk;
+      const slot = slotFromPayload(JSON.parse(body));
+      response.writeHead(200, { "Content-Type": "application/json" });
+      response.end(JSON.stringify({ code: 200, data: { workId: `work-${slot}` } }));
+      return;
+    }
+    if (request.method === "GET" && request.url?.startsWith("/result/work-")) {
+      const slot = request.url.split("work-")[1];
+      response.writeHead(200, { "Content-Type": "application/json" });
+      response.end(JSON.stringify({ images: [`${base}/image/${slot}.png`] }));
+      return;
+    }
+    if (request.method === "GET" && request.url?.startsWith("/image/")) {
+      imageDownloads += 1;
+      response.writeHead(200, { "Content-Type": "image/png" });
+      response.end(Buffer.from("prototype-image"));
+      return;
+    }
+    if (request.url?.includes("vision")) visionCalls += 1;
+    response.writeHead(404);
+    response.end();
+  });
+  await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
+  const paths = await createRun();
+  const secret = "prototype-test-token";
+  try {
+    const plan = compileReferenceAssetPlan({
+      runId: paths.run_id,
+      originalReference: "https://images.example.test/product.png",
+    });
+    await writeFile(paths.reference_asset_plan, `${JSON.stringify(plan, null, 2)}\n`);
+    const base = `http://127.0.0.1:${server.address().port}`;
+    const run = () => execFileAsync(process.execPath, [
+      executorFile,
+      paths.reference_asset_plan,
+      "--execute",
+      "--asset-map", paths.asset_map,
+      "--output", paths.reference_asset_result,
+      "--max-cost", "0.44",
+      "--poll-seconds", "0.05",
+      "--timeout-seconds", "1",
+    ], {
+      env: {
+        ...process.env,
+        JIMENG_TOKEN: secret,
+        JIMENG_SUBMIT_URL: `${base}/submit`,
+        JIMENG_QUERY_URL: `${base}/query`,
+        JIMENG_RESULT_BASE_URL: `${base}/result`,
+        JIMENG_PREFLIGHT_URL: "",
+        COMMERCE_VISION_API_KEY: "",
+      },
+    });
+    const { stdout } = await run();
+    const summary = JSON.parse(stdout);
+    assert.equal(summary.status, "complete");
+    assert.equal(summary.completed, 2);
+    assert.equal(summary.download_dir, paths.reference_assets_directory);
+    assert.equal(summary.confirmed_minimum_cost_cny, 0.44);
+    assert.equal(visionCalls, 0);
+    const state = JSON.parse(await readFile(paths.reference_asset_result, "utf8"));
+    assert.equal(verifyState(state, secret), true);
+    assert.equal(state.content_review_enabled, false);
+    assert.ok(state.results.every((entry) => entry.status === "complete" && !entry.quality_review));
+
+    await rm(state.results[0].local_images[0], { force: true });
+    const resumed = JSON.parse((await run()).stdout);
+    assert.equal(resumed.status, "complete");
+    assert.equal(submissions, 2, "resume must not resubmit completed reference assets");
+    assert.equal(imageDownloads, 3, "resume must restore the one missing local file from its recorded remote URL");
+  } finally {
+    await new Promise((resolve) => server.close(resolve));
+    await rm(paths.run_directory, { recursive: true, force: true });
+  }
+});
+
 test("a definite slot failure is isolated and later slots still complete", async () => {
   const submissions = new Map();
   let preflightCalls = 0;

+ 43 - 6
claude-code/fmode-image-set/mcp/src/server.mjs

@@ -7,7 +7,7 @@ import { z } from "zod";
 import { runImageSet } from "./tools/image-set-run.mjs";
 import { validateImageSetPlan } from "./tools/image-set-validate.mjs";
 
-const VERSION = "0.1.0";
+const VERSION = "0.2.0";
 
 function asToolResult(result = {}) {
   return {
@@ -42,14 +42,51 @@ export function createServer() {
     {
       title: "Run Fmode Image Set",
       description: [
-        "Create, execute, resume, or inspect a product-truth-first ecommerce image-set run.",
-        "Use start for visual understanding and run allocation.",
-        "Use execute only after a valid prompt-plan exists and the user explicitly authorizes paid generation.",
+        "Create, resume, or inspect a reference-first platform-aware ecommerce image-set run.",
+        "For a normal request use three internal stages without asking the user again: start observes the original image for Claude; generate_references accepts Claude-authored dynamic setting-board content and creates a fixed three-view plus visual/annotated setting boards; generate_set accepts Claude-authored per-page PromptModules and generates 1 white-background, 2 selling-point, 2 usage-scene and 2 purchase-decision images using only the two generated references.",
+        "Image generation costs are staged: start 0 CNY, generate_references max 0.44 CNY, generate_set max 1.54 CNY. The current user's direct-generation request is recorded once and carried across stages. Qwen is visual observation assistance only; Claude owns creative authoring. Content review and quality gates remain disabled; structural contract checks remain enabled. Sharp/Pango post-text files are experimental outputs whose current visual quality is not production-ready.",
+        "When the current user message contains an image attachment, call start first and omit image; never discover or substitute a workspace image with Glob, Read, or shell search.",
+        "Set image only when the user explicitly typed that exact local path or URL; a unique current VSCode Claude attachment still takes protocol-level precedence.",
         "Use resume only for the same runId; use status for read-only inspection.",
       ].join(" "),
       inputSchema: {
-        action: z.enum(["start", "execute", "resume", "status"]),
-        image: z.string().optional(),
+        action: z.enum(["start", "generate_references", "generate_set", "execute", "resume", "status"]),
+        outputLevel: z.enum(["visual_base", "designed_asset"]).optional(),
+        production: z.object({
+          instruction: z.string().optional(),
+          platform: z.string().optional(),
+          market: z.string().optional(),
+          targetLanguage: z.string().optional(),
+          aspectRatio: z.string().optional(),
+          productName: z.string().optional(),
+          sellingPoints: z.array(z.union([z.string(), z.object({
+            title: z.string().optional(),
+            name: z.string().optional(),
+            detail: z.string().optional(),
+            description: z.string().optional(),
+            source: z.string().optional(),
+          })])).optional(),
+          specifications: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
+          officialCopy: z.array(z.string()).optional(),
+          orchestrationMode: z.enum(["claude_authored_v3", "legacy_internal_planner"]).optional(),
+          authorization: z.object({
+            mode: z.enum(["preauthorized"]).optional(),
+            maxCost: z.union([z.number().nonnegative(), z.string()]).optional(),
+            source: z.enum(["current_user_prompt"]).optional(),
+          }).optional(),
+        }).optional().describe("Reference-first production brief. Product copy is optional; supplied facts are source-tracked for an experimental post-generation typography output."),
+        referenceAuthoring: z.object({
+          schema_version: z.literal("1.0"), authored_by: z.literal("claude"), run_id: z.string(),
+          source_observation_sha256: z.string(), setting_board_spec: z.object({}).passthrough(),
+          setting_board_visual_prompt: z.string().min(500),
+        }).passthrough().optional().describe("Claude-authored dynamic ProductSettingBoardSpec and product-specific 16:9 visual prompt returned after start."),
+        setPlan: z.object({
+          schema_version: z.literal("3.0"), authored_by: z.literal("claude"), run_id: z.string(),
+          source_reference_observation_sha256: z.string(), platform: z.object({}).passthrough(),
+          product_creative_brief: z.object({}).passthrough(), visual_bible: z.object({}).passthrough(),
+          pages: z.array(z.object({}).passthrough()).length(7),
+        }).passthrough().optional().describe("ClaudeAuthoredSetPlan with seven specialized self-contained PromptModules returned after generate_references."),
+        image: z.string().optional().describe("Only an exact path or URL explicitly typed by the user. Omit for chat attachments; never populate from workspace search."),
         runId: z.string().optional(),
         maxCost: z.union([z.number().nonnegative(), z.string()]).optional(),
         workspaceRoot: z.string().optional(),

+ 211 - 16
claude-code/fmode-image-set/mcp/src/tools/image-set-run.mjs

@@ -1,14 +1,27 @@
 #!/usr/bin/env node
 import { execFile } from "node:child_process";
-import { dirname, resolve } from "node:path";
+import { basename, dirname, resolve } from "node:path";
 import { fileURLToPath } from "node:url";
 import { promisify } from "node:util";
 import { errorResult, okResult } from "../core/result-envelope.mjs";
 
 const execFileAsync = promisify(execFile);
 const toolsDirectory = dirname(fileURLToPath(import.meta.url));
+const packageRoot = resolve(toolsDirectory, "..", "..", "..");
 const runnerFile = resolve(toolsDirectory, "..", "features", "image-set", "image-set-runner.mjs");
 
+export function inferProjectRoot(packageDirectory = packageRoot) {
+  const packageParent = dirname(resolve(packageDirectory));
+  const container = dirname(packageParent);
+  if (
+    basename(packageParent).toLowerCase() === "skills"
+    && new Set([".claude", ".codex"]).has(basename(container).toLowerCase())
+  ) {
+    return dirname(container);
+  }
+  return packageParent;
+}
+
 function parseJson(value) {
   const text = String(value || "").trim();
   if (!text) return null;
@@ -21,19 +34,109 @@ function parseJson(value) {
   }
 }
 
-function collectFiles(result) {
+export function collectFiles(result) {
   const direct = Array.isArray(result?.files) ? result.files : [];
+  const directImages = Array.isArray(result?.local_images) ? result.local_images : [];
   const fromResults = Array.isArray(result?.results)
     ? result.results.flatMap((item) => item?.local_images || [])
     : [];
   const nested = Array.isArray(result?.result?.results)
     ? result.result.results.flatMap((item) => item?.local_images || [])
     : [];
-  return [...new Set([...direct, ...fromResults, ...nested].filter(Boolean))];
+  const designed = Array.isArray(result?.designed_asset?.files)
+    ? result.designed_asset.files.map((item) => typeof item === "string" ? item : item?.file)
+    : [];
+  const manifest = result?.designed_asset?.manifest_file ? [result.designed_asset.manifest_file] : [];
+  const controlFiles = result?.designed_asset?.control_files && typeof result.designed_asset.control_files === "object"
+    ? Object.values(result.designed_asset.control_files)
+    : [];
+  const productionOutcome = result?.production_outcome_file ? [result.production_outcome_file] : [];
+  const designedProduction = Array.isArray(result?.designed_production_files)
+    ? result.designed_production_files.map((item) => typeof item === "string" ? item : item?.file)
+    : [];
+  const designedProductionManifest = result?.designed_production?.manifest_file
+    ? [result.designed_production.manifest_file]
+    : [];
+  const referenceFirstDesigned = Array.isArray(result?.designed_set?.files)
+    ? result.designed_set.files.map((item) => typeof item === "string" ? item : item?.file)
+    : [];
+  const referenceFirstManifest = result?.designed_set?.manifest_file ? [result.designed_set.manifest_file] : [];
+  const referenceFirstControlFiles = [
+    result?.creative_planning?.blueprint_file,
+    result?.copy_plan_file,
+    result?.layout_geometry?.regions_file,
+    result?.layout_report_file,
+    result?.paths?.original_visual_observation,
+    result?.paths?.reference_visual_observation,
+    result?.paths?.claude_reference_authoring,
+    result?.paths?.claude_authored_set_plan,
+    result?.setting_board?.visual_file,
+    result?.setting_board?.annotated_file,
+    result?.setting_board?.copy_plan_file,
+    result?.setting_board?.layout_report_file,
+  ];
+  return [...new Set([
+    ...direct,
+    ...directImages,
+    ...fromResults,
+    ...nested,
+    ...designed,
+    ...manifest,
+    ...controlFiles,
+    ...productionOutcome,
+    ...designedProduction,
+    ...designedProductionManifest,
+    ...referenceFirstDesigned,
+    ...referenceFirstManifest,
+    ...referenceFirstControlFiles,
+  ].filter(Boolean))];
 }
 
-function buildAssistantMessage(action, result) {
+const referenceFirstWorkflows = new Set(["reference_first_prototype", "reference_first_platform_v2", "claude_authored_platform_v3"]);
+
+export function buildAssistantMessage(action, result) {
   const runId = result?.paths?.run_id || result?.result?.run_id || result?.run_id || "";
+  if (result?.workflow === "claude_authored_platform_v3" && result?.status === "ready_for_reference_authoring") {
+    return [`商品原图已由视觉辅助模型转成可供Claude使用的观察合同。`, runId ? `运行编号:${runId}` : "", `本阶段未调用即梦,图片费用为0元。下一步由Claude编写动态商品设定图规格和长提示词,再调用generate_references。`].filter(Boolean).join("\n");
+  }
+  if (result?.workflow === "claude_authored_platform_v3" && result?.status === "ready_for_set_authoring") {
+    return [`固定三视图、动态无字设定板和Sharp/Pango实验性中文标注版已生成。`, runId ? `运行编号:${runId}` : "", `已确认图片费用0.44元。下一步由Claude结合两张参考资产的实际观察结果编写七页特化计划,再调用generate_set。`].filter(Boolean).join("\n");
+  }
+  if (action === "start" && referenceFirstWorkflows.has(result?.workflow) && result?.status === "complete") {
+    return [
+      result.workflow === "reference_first_platform_v2"
+        ? "产品三视图、无字视觉设定板、七张无字视觉底图和实验性后置文字版本已通过一次 start 生成。"
+        : "产品三视图、产品设定图和七张电商套图已通过一次 start 直接生成。",
+      runId ? `运行编号:${runId}` : "",
+      result?.paths?.run_directory ? `成品目录:${result.paths.run_directory}` : "",
+      result?.summary?.cost_range_cny
+        ? `实际费用范围:${result.summary.cost_range_cny.min}–${result.summary.cost_range_cny.max} 元`
+        : "",
+      result.workflow === "reference_first_platform_v2"
+        ? "已执行商品感知创意规划和排版几何识别,但未执行商品真实性或内容质量门禁;几何识别只用于SVG/Sharp文字避让,后置文字视觉质量未通过真实验收。"
+        : "本原型未执行商品真实性审查、计划校验或套图质量门禁,图片按生成结果直接交付。",
+    ].filter(Boolean).join("\n");
+  }
+  if (referenceFirstWorkflows.has(result?.workflow) && result?.status === "partial") {
+    return [
+      "参考资产优先套图流程仅技术性部分完成。",
+      runId ? `运行编号:${runId}` : "",
+      result?.summary ? `完成 ${result.summary.completed || 0}/${result.summary.expected_total || 9} 张。` : "",
+      result?.summary?.cost_range_cny
+        ? `实际费用范围:${result.summary.cost_range_cny.min}–${result.summary.cost_range_cny.max} 元`
+        : "",
+      result?.layout_geometry?.status === "blocked"
+        ? `七张视觉底图已保留;排版几何或设计后处理未完成,可对同一runId使用resume继续且不会重提已完成图片。${result.layout_geometry.error ? ` 原因:${result.layout_geometry.error}` : ""}`
+        : "未进行内容审查或自动返修;可对同一 runId 使用 resume 继续技术失败图位。",
+    ].filter(Boolean).join("\n");
+  }
+  if (action === "start" && result?.output_level === "designed_asset" && result?.status === "complete") {
+    return [
+      "三页设计成品已完成,本次 start 到此停止,未进入七图规划或付费生图。",
+      runId ? `运行编号:${runId}` : "",
+      result?.designed_asset?.manifest_file ? `设计清单:${result.designed_asset.manifest_file}` : "",
+    ].filter(Boolean).join("\n");
+  }
   if (action === "start" && result?.status === "ready_for_plan") {
     return [
       "商品视觉理解已完成,可以编写七图计划。",
@@ -42,8 +145,62 @@ function buildAssistantMessage(action, result) {
       "下一步:依据视觉结果生成 prompt-plan.json,并调用 fmode_image_set_validate。",
     ].filter(Boolean).join("\n");
   }
+  if (action === "start" && result?.status === "waiting_for_confirmation") {
+    const preauthorized = result?.production?.paid_generation_authorized === true;
+    return [
+      preauthorized
+        ? "商品理解、七图计划和确定性校验已完成;当前请求已记录前置付费授权,将继续对同一 runId 执行。"
+        : "商品理解、七图计划和确定性校验已完成;付费生图尚未授权,本次必须停在确认门禁。",
+      runId ? `运行编号:${runId}` : "",
+      result?.paths?.prompt_plan ? `七图计划:${result.paths.prompt_plan}` : "",
+      result?.production?.page_count ? `计划页数:${result.production.page_count}` : "",
+      result?.production?.estimated_cost_cny !== undefined ? `预计即梦费用:${result.production.estimated_cost_cny} 元` : "",
+      preauthorized
+        ? "下一步:调用同一 runId 的 action=execute,不再重复询问。"
+        : "下一步:先审阅七页任务和 Prompt;只有用户明确授权费用后,才能对同一 runId 调用 action=execute。",
+    ].filter(Boolean).join("\n");
+  }
+  if (action === "start" && result?.output_level === "visual_base" && result?.status === "blocked") {
+    const validationErrors = Array.isArray(result?.production?.validation?.errors)
+      ? result.production.validation.errors.filter(Boolean)
+      : [];
+    const error = result?.production?.error || result?.vision?.error || result?.error || {};
+    return [
+      "七图生产计划未通过确定性校验,本次已在付费生图前阻断;未调用即梦,新增费用为 0 元。",
+      runId ? `运行编号:${runId}` : "",
+      result?.paths?.prompt_plan ? `七图计划:${result.paths.prompt_plan}` : "",
+      error.code ? `错误码:${error.code}` : "",
+      error.message ? `原因:${error.message}` : "",
+      ...validationErrors.map((item, index) => `校验错误 ${index + 1}:${item}`),
+      "下一步:根据以上错误修正同一运行的计划;不要盲目重试 start,也不要在缺少 runId 时调用 status。",
+    ].filter(Boolean).join("\n");
+  }
+  if (action === "start" && result?.output_level === "designed_asset" && result?.status === "blocked") {
+    const error = result?.designed_asset?.error || result?.vision?.error || result?.error || {};
+    return [
+      "设计成品阶段已阻断;没有生成三页成品、manifest 或控制面文件,不得报告为 complete。",
+      runId ? `运行编号:${runId}` : "",
+      error.code ? `错误码:${error.code}` : "",
+      error.message ? `原因:${error.message}` : "",
+      result?.next_action ? `下一步:${result.next_action}` : "",
+    ].filter(Boolean).join("\n");
+  }
+  if (result?.production_outcome?.generation_status === "generation_complete"
+    && result?.production_outcome?.quality_status === "quality_blocked") {
+    return [
+      `七张视觉底图已生成${runId ? `,运行编号:${runId}` : ""},但套图质量门禁未通过。`,
+      "未生成或交付七页设计成品;请检查页级违约和重复簇。任何付费重生成都需要新的明确授权。",
+    ].join("\n");
+  }
+  if (result?.production_outcome?.generation_status === "generation_complete"
+    && ["quality_unverified", "quality_review_required"].includes(result?.production_outcome?.quality_status)) {
+    return `七张视觉底图已生成${runId ? `,运行编号:${runId}` : ""},但输出级质量证据不足;七页设计桥已阻断,不能报告为质量完成。`;
+  }
   if (result?.status === "complete") {
-    return `商品套图已完成${runId ? `,运行编号:${runId}` : ""}。`;
+    if (result?.workflow === "claude_authored_platform_v3") return `Claude主导的三视图、动态设定板和七张无字视觉底图已生成${runId ? `,运行编号:${runId}` : ""};实验性后置文字版本也已生成,但其视觉质量当前未通过真实验收,不得作为商业成品。图片费用1.98元,未启用内容审查或质量门禁。`;
+    return result?.designed_production?.status === "complete"
+      ? `七张视觉底图通过输出级质量门禁,七页确定性设计成品已完成${runId ? `,运行编号:${runId}` : ""};发布前仍需人工审美复核。`
+      : `七张视觉底图已生成并通过质量门禁${runId ? `,运行编号:${runId}` : ""},但设计成品未完成。`;
   }
   if (result?.status === "partial") {
     return [
@@ -58,28 +215,63 @@ function buildAssistantMessage(action, result) {
   return result?.error?.message || "套图运行被阻断,请按结构化结果中的下一步处理。";
 }
 
+export function productionStatusFor(result = {}) {
+  if (referenceFirstWorkflows.has(result?.workflow)) {
+    if (result.workflow === "claude_authored_platform_v3") return result.status === "complete" ? "claude_authored_v3_complete" : result.status;
+    if (result.workflow === "reference_first_platform_v2") return result.status === "complete" ? "platform_designed_complete" : "platform_designed_partial";
+    return result.status === "complete" ? "prototype_complete" : "prototype_partial";
+  }
+  return result?.production_outcome?.release_status
+    || result?.production_outcome?.status
+    || (result?.production?.validation?.valid === true
+      ? result?.production?.paid_generation_authorized === true
+        ? "authorized_ready_to_execute"
+        : "waiting_for_confirmation"
+      : result?.production ? "blocked" : "not_requested");
+}
+
+export function buildRunnerArguments(input, attachmentCwd) {
+  const action = String(input.action || "start");
+  const runnerArgs = [runnerFile, action];
+  if (input.image) runnerArgs.push("--image", String(input.image));
+  if (action === "start") runnerArgs.push("--prefer-current-attachment");
+  if (input.runId) runnerArgs.push("--run-id", String(input.runId));
+  if (input.maxCost !== undefined) runnerArgs.push("--max-cost", String(input.maxCost));
+  if (action === "generate_references" && input.referenceAuthoring) runnerArgs.push("--reference-authoring-json", JSON.stringify(input.referenceAuthoring));
+  if (action === "generate_set" && input.setPlan) runnerArgs.push("--set-plan-json", JSON.stringify(input.setPlan));
+  runnerArgs.push("--cwd", attachmentCwd);
+  if (input.claudeConfigDir) runnerArgs.push("--claude-config-dir", String(input.claudeConfigDir));
+      if (input.sessionId) runnerArgs.push("--session-id", String(input.sessionId));
+  if (action === "start" && input.production && typeof input.production === "object") {
+    runnerArgs.push("--production-brief-json", JSON.stringify(input.production));
+  }
+  runnerArgs.push("--output-level", String(input.outputLevel || "visual_base"));
+  return runnerArgs;
+}
+
 export async function runImageSet(input = {}) {
   const action = String(input.action || "start");
-  if (!new Set(["start", "execute", "resume", "status"]).has(action)) {
-    return errorResult("needs_input", "action 必须是 start、execute、resume 或 status。");
+  if (!new Set(["start", "generate_references", "generate_set", "execute", "resume", "status"]).has(action)) {
+    return errorResult("needs_input", "action 必须是 start、generate_references、generate_set、execute、resume 或 status。");
   }
 
-  const args = [runnerFile, action];
-  if (input.image) args.push("--image", String(input.image));
-  if (input.runId) args.push("--run-id", String(input.runId));
-  if (input.maxCost !== undefined) args.push("--max-cost", String(input.maxCost));
-  if (input.cwd) args.push("--cwd", String(input.cwd));
-  if (input.claudeConfigDir) args.push("--claude-config-dir", String(input.claudeConfigDir));
-  if (input.sessionId) args.push("--session-id", String(input.sessionId));
+  const inferredProjectRoot = resolve(
+    input.workspaceRoot
+    || process.env.CLAUDE_PROJECT_DIR
+    || process.env.FMODE_IMAGE_SET_PROJECT_ROOT
+    || inferProjectRoot(),
+  );
+  const attachmentCwd = resolve(input.cwd || inferredProjectRoot);
+  const args = buildRunnerArguments(input, attachmentCwd);
 
   const env = {
     ...process.env,
-    ...(input.workspaceRoot ? { FMODE_IMAGE_SET_PROJECT_ROOT: resolve(String(input.workspaceRoot)) } : {}),
+    FMODE_IMAGE_SET_PROJECT_ROOT: inferredProjectRoot,
   };
 
   try {
     const { stdout } = await execFileAsync(process.execPath, args, {
-      cwd: input.workspaceRoot ? resolve(String(input.workspaceRoot)) : process.cwd(),
+      cwd: inferredProjectRoot,
       env,
       maxBuffer: 1024 * 1024 * 20,
     });
@@ -91,6 +283,9 @@ export async function runImageSet(input = {}) {
         action,
         runId: result?.paths?.run_id || result?.result?.run_id || result?.run_id || input.runId || "",
         runtimeStatus: result?.status || "unknown",
+        outputLevel: result?.output_level || input.outputLevel || "visual_base",
+        designedAssetStatus: result?.designed_asset?.status || "not_requested",
+        productionStatus: productionStatusFor(result),
       },
       data: result,
       files,

+ 230 - 0
claude-code/fmode-image-set/mcp/src/tools/image-set-run.test.mjs

@@ -0,0 +1,230 @@
+import test from "node:test";
+import assert from "node:assert/strict";
+import { buildAssistantMessage, buildRunnerArguments, collectFiles, inferProjectRoot, productionStatusFor } from "./image-set-run.mjs";
+
+test("MCP file envelope includes designed asset pages and manifest", () => {
+  const files = collectFiles({
+    designed_asset: {
+      files: [{ file: "page-1.png" }, { file: "page-2.png" }],
+      manifest_file: "designed-asset-manifest.json",
+      control_files: { prompt_contracts: "prompt-contracts.json", quality_report: "quality-report.json" },
+    },
+  });
+  assert.deepEqual(files, ["page-1.png", "page-2.png", "designed-asset-manifest.json", "prompt-contracts.json", "quality-report.json"]);
+});
+
+test("MCP file envelope includes generated images and production outcome", () => {
+  const files = collectFiles({
+    local_images: ["D:\\run\\01.png", "D:\\run\\02.png"],
+    production_outcome_file: "D:\\run\\production-outcome.json",
+  });
+  assert.deepEqual(files, ["D:\\run\\01.png", "D:\\run\\02.png", "D:\\run\\production-outcome.json"]);
+});
+
+test("MCP file envelope includes all seven bridged designed pages and their manifest", () => {
+  const designed = Array.from({ length: 7 }, (_, index) => ({ file: `D:\\run\\designed-production\\0${index + 1}.png` }));
+  const files = collectFiles({
+    designed_production_files: designed,
+    designed_production: { manifest_file: "D:\\run\\designed-production\\manifest.json" },
+  });
+  assert.equal(files.length, 8);
+  assert.equal(files[0], "D:\\run\\designed-production\\01.png");
+  assert.equal(files.at(-1), "D:\\run\\designed-production\\manifest.json");
+});
+
+test("MCP summary exposes the derived production release status after execution", () => {
+  assert.equal(productionStatusFor({ workflow: "reference_first_prototype", status: "complete" }), "prototype_complete");
+  assert.equal(productionStatusFor({ workflow: "reference_first_platform_v2", status: "complete" }), "platform_designed_complete");
+  assert.equal(productionStatusFor({ production: { validation: { valid: true } } }), "waiting_for_confirmation");
+  assert.equal(productionStatusFor({ production_outcome: { status: "partial", release_status: "development_only" } }), "development_only");
+  assert.equal(productionStatusFor({ production_outcome: { status: "complete", release_status: "review_required" } }), "review_required");
+  assert.equal(productionStatusFor({ production_outcome: { status: "complete", release_status: "quality_blocked" } }), "quality_blocked");
+});
+
+test("platform-aware reference-first completion reports creative planning, geometry and experimental typography boundary", () => {
+  const message = buildAssistantMessage("start", {
+    workflow: "reference_first_platform_v2", status: "complete",
+    paths: { run_id: "run-platform-v2", run_directory: "D:\\run" },
+    summary: { completed: 9, expected_total: 9, cost_range_cny: { min: 1.98, max: 1.98 } },
+  });
+  assert.match(message, /无字视觉设定板/);
+  assert.match(message, /七张无字视觉底图和实验性后置文字版本/);
+  assert.match(message, /商品感知创意规划/);
+  assert.match(message, /排版几何识别/);
+  assert.match(message, /SVG\/Sharp/);
+  assert.match(message, /未执行商品真实性或内容质量门禁/);
+  assert.match(message, /后置文字视觉质量未通过真实验收/);
+});
+
+test("reference-first completion reports all assets, direct delivery and no content gate", () => {
+  const message = buildAssistantMessage("start", {
+    workflow: "reference_first_prototype",
+    status: "complete",
+    paths: { run_id: "run-reference-first", run_directory: "D:\\run" },
+    summary: { completed: 9, expected_total: 9, cost_range_cny: { min: 1.98, max: 1.98 } },
+  });
+  assert.match(message, /产品三视图/);
+  assert.match(message, /产品设定图/);
+  assert.match(message, /七张电商套图/);
+  assert.match(message, /1\.98/);
+  assert.match(message, /未执行商品真实性审查/);
+  assert.doesNotMatch(message, /质量门禁未通过|等待确认/);
+});
+
+test("completion wording never calls a quality-blocked generated set complete", () => {
+  const message = buildAssistantMessage("execute", {
+    status: "complete", run_id: "run-quality-blocked",
+    production_outcome: { generation_status: "generation_complete", quality_status: "quality_blocked" },
+  });
+  assert.match(message, /底图已生成/);
+  assert.match(message, /质量门禁未通过/);
+  assert.match(message, /新的明确授权/);
+  assert.doesNotMatch(message, /设计成品已完成/);
+});
+
+test("workspace installs infer the project above .claude", () => {
+  assert.equal(
+    inferProjectRoot("D:\\Angular\\套图设计\\.claude\\skills\\fmode-image-set"),
+    "D:\\Angular\\套图设计",
+  );
+});
+
+test("designed asset completion message explicitly stops after start", () => {
+  const message = buildAssistantMessage("start", {
+    status: "complete",
+    output_level: "designed_asset",
+    paths: { run_id: "run-test" },
+    designed_asset: { manifest_file: "manifest.json" },
+  });
+  assert.match(message, /本次 start 到此停止/);
+  assert.doesNotMatch(message, /七图计划/);
+});
+
+test("designed asset blocking message exposes the real error and forbids a complete claim", () => {
+  const message = buildAssistantMessage("start", {
+    status: "blocked",
+    output_level: "designed_asset",
+    paths: { run_id: "run-blocked" },
+    designed_asset: {
+      status: "blocked",
+      error: { code: "DESIGNED_ASSET_PRODUCT_NOT_VISIBLE", message: "当前参考图未显示可识别商品" },
+    },
+    next_action: "attach_one_clear_product_image_and_start_again",
+  });
+  assert.match(message, /DESIGNED_ASSET_PRODUCT_NOT_VISIBLE/);
+  assert.match(message, /没有生成三页成品/);
+  assert.match(message, /不得报告为 complete/);
+});
+
+test("MCP start always enables current VSCode Claude attachment precedence", () => {
+  const args = buildRunnerArguments({ action: "start", image: "D:\\wrong-history.png", outputLevel: "designed_asset" }, "D:\\workspace");
+  assert.ok(args.includes("--prefer-current-attachment"));
+  assert.ok(args.includes("--image"));
+  const statusArgs = buildRunnerArguments({ action: "status", runId: "run-test" }, "D:\\workspace");
+  assert.equal(statusArgs.includes("--prefer-current-attachment"), false);
+});
+
+test("MCP production context is passed as one structured non-paid brief", () => {
+  const args = buildRunnerArguments({
+    action: "start",
+    production: {
+      instruction: "生成淘宝中文七图",
+      platform: "taobao",
+      market: "CN",
+      targetLanguage: "zh-CN",
+      aspectRatio: "1:1",
+    },
+  }, "D:\\workspace");
+  const index = args.indexOf("--production-brief-json");
+  assert.ok(index > 0);
+  assert.deepEqual(JSON.parse(args[index + 1]), {
+    instruction: "生成淘宝中文七图",
+    platform: "taobao",
+    market: "CN",
+    targetLanguage: "zh-CN",
+    aspectRatio: "1:1",
+  });
+});
+
+test("production start message reports seven-page preflight and paid stop", () => {
+  const message = buildAssistantMessage("start", {
+    status: "waiting_for_confirmation",
+    output_level: "visual_base",
+    paths: { run_id: "run-production", prompt_plan: "D:\\run\\prompt-plan.json" },
+    production: { page_count: 7, estimated_cost_cny: 1.54, paid_generation_authorized: false },
+  });
+  assert.match(message, /七图计划/);
+  assert.match(message, /1\.54/);
+  assert.match(message, /尚未授权/);
+  assert.match(message, /明确授权/);
+});
+
+test("blocked production start exposes run id, validator errors and zero paid generation", () => {
+  const message = buildAssistantMessage("start", {
+    status: "blocked",
+    output_level: "visual_base",
+    paths: { run_id: "run-plan-blocked", prompt_plan: "D:\\run\\prompt-plan.json" },
+    production: {
+      validation: {
+        valid: false,
+        errors: [
+          "images[4] scene lacks a plausible tabletop_supported support or interaction relation",
+          "shot_spec.camera_axis repeats too often: front (3)",
+        ],
+      },
+    },
+  });
+  assert.match(message, /run-plan-blocked/);
+  assert.match(message, /images\[4\]/);
+  assert.match(message, /front \(3\)/);
+  assert.match(message, /未调用即梦/);
+  assert.match(message, /0 元/);
+  assert.match(message, /不要盲目重试 start/);
+  assert.match(message, /缺少 runId/);
+  assert.doesNotMatch(message, /套图运行被阻断,请按结构化结果/);
+});
+
+test("preauthorized production start message continues without a second question", () => {
+  const message = buildAssistantMessage("start", {
+    status: "waiting_for_confirmation",
+    output_level: "visual_base",
+    paths: { run_id: "run-preauthorized", prompt_plan: "D:\\run\\prompt-plan.json" },
+    production: { page_count: 7, estimated_cost_cny: 1.54, paid_generation_authorized: true },
+  });
+  assert.match(message, /前置付费授权/);
+  assert.match(message, /不再重复询问/);
+  assert.doesNotMatch(message, /尚未授权/);
+});
+
+test("MCP production brief can carry explicit current-prompt authorization", () => {
+  const args = buildRunnerArguments({
+    action: "start",
+    production: {
+      instruction: "直接生成并交付淘宝中文七图",
+      authorization: { mode: "preauthorized", source: "current_user_prompt", maxCost: 1.54 },
+    },
+  }, "D:\\workspace");
+  const index = args.indexOf("--production-brief-json");
+  assert.deepEqual(JSON.parse(args[index + 1]).authorization, {
+    mode: "preauthorized",
+    source: "current_user_prompt",
+    maxCost: 1.54,
+  });
+});
+
+test("V3 runner passes Claude-authored contracts as structured stage inputs", () => {
+  const refArgs = buildRunnerArguments({ action: "generate_references", runId: "run-test", referenceAuthoring: { authored_by: "claude", value: "dynamic" } }, "D:\\workspace");
+  assert.deepEqual(JSON.parse(refArgs[refArgs.indexOf("--reference-authoring-json") + 1]), { authored_by: "claude", value: "dynamic" });
+  const setArgs = buildRunnerArguments({ action: "generate_set", runId: "run-test", setPlan: { authored_by: "claude", pages: [] } }, "D:\\workspace");
+  assert.deepEqual(JSON.parse(setArgs[setArgs.indexOf("--set-plan-json") + 1]), { authored_by: "claude", pages: [] });
+});
+
+test("V3 stage messages make Claude ownership and staged costs explicit", () => {
+  const start = buildAssistantMessage("start", { workflow: "claude_authored_platform_v3", status: "ready_for_reference_authoring", paths: { run_id: "run-v3" } });
+  assert.match(start, /Claude/); assert.match(start, /0元/); assert.match(start, /generate_references/);
+  const refs = buildAssistantMessage("generate_references", { workflow: "claude_authored_platform_v3", status: "ready_for_set_authoring", paths: { run_id: "run-v3" } });
+  assert.match(refs, /实验性中文标注版/); assert.match(refs, /0\.44元/); assert.match(refs, /generate_set/);
+  const complete = buildAssistantMessage("generate_set", { workflow: "claude_authored_platform_v3", status: "complete", paths: { run_id: "run-v3" } });
+  assert.match(complete, /七张无字视觉底图已生成/); assert.match(complete, /后置文字版本/); assert.match(complete, /不得作为商业成品/);
+  assert.equal(productionStatusFor({ workflow: "claude_authored_platform_v3", status: "complete" }), "claude_authored_v3_complete");
+});

+ 3 - 0
claude-code/fmode-image-set/mcp/test-fixtures/task-0016-coat-failure/README.md

@@ -0,0 +1,3 @@
+# TASK-0016 coat failure fixture
+
+Sanitized 320px derivatives of run-20260804002111-a2838e. Tests only use this directory. Human semantic labels in labels.json are frozen review evidence, not historical model output.

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff