Explorar el Código

feat: integrate isolated five-view demo and showcase pages

Complete sample filters, local actions, guided product cases and six introduction pages. Preserve deployed public opportunity/report repairs and add a verified static deployment package with rollback.
pengzhikang hace 1 día
padre
commit
46aa789571
Se han modificado 51 ficheros con 8582 adiciones y 1000 borrados
  1. 1 0
      .gitignore
  2. 221 0
      deploy/five-views-files-20261103.json
  3. 221 0
      deploy/five-views-production-baseline-20261103.json
  4. 13 0
      docs/showcase/api.md
  5. 47 0
      docs/showcase/deploy.md
  6. 9 0
      docs/showcase/design.md
  7. 21 0
      docs/showcase/prd.md
  8. 13 0
      docs/showcase/schema.md
  9. 9 0
      docs/showcase/test.md
  10. 26 0
      docs/showcase/validation-result.json
  11. 38 0
      scripts/build-five-views-package.mjs
  12. 6 5
      scripts/check.mjs
  13. 100 0
      scripts/generate-demo-extension.py
  14. 169 0
      scripts/publish-five-views.mjs
  15. 298 0
      scripts/verify-showcase-browser.mjs
  16. BIN
      src/assets/showcase/classroom.png
  17. BIN
      src/assets/showcase/collaboration.png
  18. 0 0
      src/assets/showcase/demo-extension.json
  19. BIN
      src/assets/showcase/leadership.png
  20. 6035 0
      src/assets/showcase/preview-seed.json
  21. BIN
      src/assets/showcase/research.png
  22. BIN
      src/assets/showcase/validation.png
  23. 3 7
      src/components/kf-opportunity-card.js
  24. 1 0
      src/core/evidence/chain.js
  25. 15 1
      src/core/report.js
  26. 142 0
      src/core/sample.js
  27. 121 0
      src/core/showcase.js
  28. 53 83
      src/layout/kf-app.js
  29. 6 2
      src/layout/kf-nav.js
  30. 54 0
      src/layout/sample-shell.js
  31. 89 113
      src/pages/sample-base.js
  32. 82 0
      src/pages/sample-detail.js
  33. 86 0
      src/pages/showcase-content.js
  34. 4 113
      src/pages/view-class.js
  35. 4 127
      src/pages/view-cockpit.js
  36. 4 119
      src/pages/view-coop.js
  37. 4 101
      src/pages/view-intel.js
  38. 4 3
      src/pages/view-pool.js
  39. 4 102
      src/pages/view-review.js
  40. 264 0
      src/pages/view-showcase.js
  41. 25 16
      src/services/board-service.js
  42. 59 204
      src/services/sample-data.js
  43. 20 0
      src/services/showcase-data.js
  44. 15 1
      src/services/workshop-data.js
  45. 2 0
      src/utils/http.js
  46. 7 3
      src/utils/router.js
  47. 72 0
      test/five-views-publish.test.js
  48. 24 0
      test/report.test.js
  49. 87 0
      test/sample-flow.test.js
  50. 85 0
      test/showcase-sample.test.js
  51. 19 0
      test/workshop-data.test.js

+ 1 - 0
.gitignore

@@ -1,5 +1,6 @@
 # 依赖
 node_modules/
+work/
 package-lock.json
 
 # 运行日志(Claude Code 每轮 dispatch 的输出)

+ 221 - 0
deploy/five-views-files-20261103.json

@@ -0,0 +1,221 @@
+{
+  "version": "ksf.five-views.publish.v1",
+  "release": "ksf.five-views.20261103.v1",
+  "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+  "files": [
+    {
+      "path": "src/layout/kf-app.js",
+      "bytes": 31003,
+      "sha256": "c61882cb4f0763cb85dea64638ca2c41d8dd295deb7d85ec9eb60613d7bd201a",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "ac936167c2820fc2cd8a122cd3686ad741bfde2b7628f59bd1e6de157df77272"
+      ]
+    },
+    {
+      "path": "src/layout/kf-nav.js",
+      "bytes": 10350,
+      "sha256": "699f2d51b1b79f6c6bc979b05a72b2426ccc956ce72f793ad2b0944000cd2abc",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "6e3fb092b13320e799cebbacdc87878ca0c2e56b58114ca561512bbfcef1a5fa"
+      ]
+    },
+    {
+      "path": "src/layout/sample-shell.js",
+      "bytes": 7669,
+      "sha256": "25bb95548a3c4850a18efc524c1035c735e77d5717e3ea29a14f208ebb76bcb1",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/utils/router.js",
+      "bytes": 8109,
+      "sha256": "b0cc1e555e55f137f5d3bd0772a2fa027b39419aa3fb0bb72a5be9b2d51b5a51",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "32bb5ce86da922e14f0aa4f34dd8fbaef60a13588e7d564f6476c58fc5355cad"
+      ]
+    },
+    {
+      "path": "src/utils/http.js",
+      "bytes": 2897,
+      "sha256": "627444b3614136bfb11999cb73e37a4e95f4bd950711c6128c59ef005c848504",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "adb3a42a54fbb60667216e551f7cc2aa6a272f7324ca9522bf14d586eb40d8ce"
+      ]
+    },
+    {
+      "path": "src/services/workshop-data.js",
+      "bytes": 1994,
+      "sha256": "cae7e9065a37d395429d5f1bd254b2201ad4b0218c6222349029455dc96a7c65",
+      "allowMissing": false,
+      "priorNormalizedSha256": [
+        "8b97ff1f55f7f7df0c886009478b0403bbd456089190f8f9bb2595c3f4b5dc46"
+      ]
+    },
+    {
+      "path": "src/services/sample-data.js",
+      "bytes": 3157,
+      "sha256": "032b3844f1f86e4d2ea72e5ab6ffd0e68f015d0cfb4a71acde16454dbad50454",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/services/showcase-data.js",
+      "bytes": 740,
+      "sha256": "9480902d681019e8ef8bb292f61e56c0b9b85cd014d98547d49629fc15d9a8ee",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/core/sample.js",
+      "bytes": 10615,
+      "sha256": "a38aff36f42512d863416a2499939be2ef876c70d0c7f314a8dbd614c5e139d4",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/core/showcase.js",
+      "bytes": 6436,
+      "sha256": "5b9d322789bf63d1f1e40cd27b1438b940cd7596a90386970efd1d69e82cc3b4",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/sample-base.js",
+      "bytes": 21505,
+      "sha256": "3ee78ee16d738dc3fd49e3d87dbe2106f8daffbe2e3e1d6835a4a3be58b98ad8",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/sample-detail.js",
+      "bytes": 14247,
+      "sha256": "a50ec08dcdffe11fb943f249b89aa25020a6b0e9e0a09f8b403bd13409dfca0d",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/view-cockpit.js",
+      "bytes": 193,
+      "sha256": "582ef3bf59399b7d9a28dddbc564ffd469569c541815a161f1eeec41efdba8e9",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/view-intel.js",
+      "bytes": 187,
+      "sha256": "e8430ffb54d11226451569665988fb7af41b5b9b49d87f19686bf221807e7ab8",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/view-coop.js",
+      "bytes": 184,
+      "sha256": "22bb653db3bb00211ebb179ad686194cf285fde42a2adcb099a893594b736b78",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/view-review.js",
+      "bytes": 190,
+      "sha256": "df413a9eeb2dd0ad9a91205df0b0c00d9df7261c6f22f9454596b8cb652979f3",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/view-class.js",
+      "bytes": 187,
+      "sha256": "c4d36a9801c3fa34f5a1dcf39882b7a5559f4cd018905b21d45598c44612ec34",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/view-showcase.js",
+      "bytes": 39928,
+      "sha256": "3149116a31bed76ff10a0fc5eb6f0813c4a833d5bbc26398b552e1bbfd033b84",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/pages/showcase-content.js",
+      "bytes": 8775,
+      "sha256": "e200aae7477d889fbd389073432a11b9c5dc443c39ba5dc29a29307df653cadf",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/leadership.png",
+      "bytes": 625274,
+      "sha256": "39f878e869c6339f912bac0a819a00a49d4000ee5d1994cfe3339ae323a1aece",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/research.png",
+      "bytes": 674665,
+      "sha256": "0c00b10c94b4b145a86665c230bea5a3160cc55490e777270bfba258b153cfc1",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/collaboration.png",
+      "bytes": 600604,
+      "sha256": "468951506acae8745c16c2d2762522643b7a60235af16f0f7ecc57e768c76834",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/validation.png",
+      "bytes": 589103,
+      "sha256": "20234f98e934d8e25bd767b5e0344f0723ea9911e0d93a85f77a4e89b92fcfcd",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/classroom.png",
+      "bytes": 476854,
+      "sha256": "86aadd604cd8c73fe89cc6d87b326707c1e13e7012a2fb878034bbc87c0a6aa3",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/preview-seed.json",
+      "bytes": 185185,
+      "sha256": "def81ca0592cb8eeae7c77a0522c53a4d283257ee2dd3434196c4cf72a013ca7",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    },
+    {
+      "path": "src/assets/showcase/demo-extension.json",
+      "bytes": 12134800,
+      "sha256": "1806c29f2832831f5621f206b188223e9c8fed9d21797ca06aa267d214770252",
+      "allowMissing": true,
+      "priorNormalizedSha256": []
+    }
+  ],
+  "preserved": [
+    {
+      "path": "src/services/board-service.js",
+      "normalizedSha256": "7d7f9e87a0b210e5083b388f492de0968f20bf3193fe6a3f9400005def10ead2"
+    },
+    {
+      "path": "src/components/kf-opportunity-card.js",
+      "normalizedSha256": "feacdb73b680c3292e6742ed17bf2a199d52ae8d161376df27d0dd86ed6a1975"
+    },
+    {
+      "path": "src/pages/view-pool.js",
+      "normalizedSha256": "bf3d3b1d8c6f388499f868188c135fe4d370ac8012c31ee2e5586892874611af"
+    },
+    {
+      "path": "src/core/report.js",
+      "normalizedSha256": "e253e94724acc02a406eb1e73e03341def57948a1fa65cf9c15fcae2b66b9d88"
+    },
+    {
+      "path": "src/core/evidence/chain.js",
+      "normalizedSha256": "91060709ebb32ff0969c24fb797d5f6ad7be4824d311078517effb3acbe2ee29"
+    }
+  ]
+}

+ 221 - 0
deploy/five-views-production-baseline-20261103.json

@@ -0,0 +1,221 @@
+{
+  "at": "2026-11-03T10:45:09.668Z",
+  "origin": "https://kangshifu.fmode.cn",
+  "readOnly": true,
+  "records": [
+    {
+      "path": "index.html",
+      "bytes": 1943,
+      "rawSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+      "normalizedSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+      "contentType": "text/html; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/layout/kf-app.js",
+      "bytes": 29565,
+      "rawSha256": "db72bd558cf6b40d741203e5a0d00bde77774fb29bc7e32bd6c1a49a9c8e3f3b",
+      "normalizedSha256": "ac936167c2820fc2cd8a122cd3686ad741bfde2b7628f59bd1e6de157df77272",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/utils/router.js",
+      "bytes": 7819,
+      "rawSha256": "fd8450a38320017becc0d421fca48079f68fda9201d5603bd85b437a32c26f48",
+      "normalizedSha256": "32bb5ce86da922e14f0aa4f34dd8fbaef60a13588e7d564f6476c58fc5355cad",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/layout/kf-nav.js",
+      "bytes": 9736,
+      "rawSha256": "6e3fb092b13320e799cebbacdc87878ca0c2e56b58114ca561512bbfcef1a5fa",
+      "normalizedSha256": "6e3fb092b13320e799cebbacdc87878ca0c2e56b58114ca561512bbfcef1a5fa",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/utils/http.js",
+      "bytes": 2771,
+      "rawSha256": "adb3a42a54fbb60667216e551f7cc2aa6a272f7324ca9522bf14d586eb40d8ce",
+      "normalizedSha256": "adb3a42a54fbb60667216e551f7cc2aa6a272f7324ca9522bf14d586eb40d8ce",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/services/workshop-data.js",
+      "bytes": 1987,
+      "rawSha256": "8b97ff1f55f7f7df0c886009478b0403bbd456089190f8f9bb2595c3f4b5dc46",
+      "normalizedSha256": "8b97ff1f55f7f7df0c886009478b0403bbd456089190f8f9bb2595c3f4b5dc46",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/services/board-service.js",
+      "bytes": 6206,
+      "rawSha256": "2c1ecd438be53a5fee56e7255d7a06213dbe6ca2e0a0e827543608e62990146b",
+      "normalizedSha256": "7d7f9e87a0b210e5083b388f492de0968f20bf3193fe6a3f9400005def10ead2",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/components/kf-opportunity-card.js",
+      "bytes": 14677,
+      "rawSha256": "faa9e0850f7a4cee47deb2ead3ec546d18a2ee3cbedfd21861fc4e58dcea1cfe",
+      "normalizedSha256": "feacdb73b680c3292e6742ed17bf2a199d52ae8d161376df27d0dd86ed6a1975",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/pages/view-pool.js",
+      "bytes": 18158,
+      "rawSha256": "ed3c4b5f0d3ccf50e3d23bfefb9f3bb50852c2620a5798bc0c3a7099819f6c1c",
+      "normalizedSha256": "bf3d3b1d8c6f388499f868188c135fe4d370ac8012c31ee2e5586892874611af",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/core/report.js",
+      "bytes": 5028,
+      "rawSha256": "206def0bcde21a0dbe9fb8e937f02fd035afaa268d022c2515e2249075615655",
+      "normalizedSha256": "e253e94724acc02a406eb1e73e03341def57948a1fa65cf9c15fcae2b66b9d88",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/core/evidence/chain.js",
+      "bytes": 4639,
+      "rawSha256": "3798974a75fa078cc2c6cef30651bb250917b35fa79625f90d3e89df401a9fb3",
+      "normalizedSha256": "91060709ebb32ff0969c24fb797d5f6ad7be4824d311078517effb3acbe2ee29",
+      "contentType": "application/javascript; charset=utf-8",
+      "present": true
+    },
+    {
+      "path": "src/layout/sample-shell.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/services/sample-data.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/services/showcase-data.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/core/sample.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/core/showcase.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/sample-base.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/sample-detail.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/view-cockpit.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/view-intel.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/view-coop.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/view-review.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/view-class.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/view-showcase.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/pages/showcase-content.js",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/leadership.png",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/research.png",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/collaboration.png",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/validation.png",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/classroom.png",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/preview-seed.json",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    },
+    {
+      "path": "src/assets/showcase/demo-extension.json",
+      "status": 200,
+      "present": false,
+      "htmlFallback": true
+    }
+  ]
+}

+ 13 - 0
docs/showcase/api.md

@@ -0,0 +1,13 @@
+# 静态读取与本机操作
+
+`loadShowcase()`读取同包334条基础JSON,校验后派生介绍页计数与故事。
+
+`createSampleService()`读取基础与扩展两个静态JSON,合并只读索引。加载失败显示错误和重试,不回退线上接口。
+
+`filterSample(model, filters)`统一派生五端范围;`sampleDetail(model,type,id)`只返回本模拟空间记录,缺失时显示本机错误。
+
+`sampleService.mutate(action,input,revision)`支持status、score、comment。验证署名、ID、分数和revision后写入独立localStorage;失败不显示成功,不进行网络重试。`reset()`仅重置本机演示键。
+
+演示外壳不引用boardService;kf-app在介绍页与五端暂停生产轮询,忽略全局生产写事件。返回原路由恢复原有读取;演示ID即使从旧路由误发也被拦截。
+
+生产公开读取及状态/评分函数保留原合同,本任务不新增云函数,不改Schema、ACL、Skill或业务记录。

+ 47 - 0
docs/showcase/deploy.md

@@ -0,0 +1,47 @@
+# 运维001:五端演示与介绍页部署
+
+本包尚未部署。只更新26个静态文件,不执行业务函数、不改数据库、首页、nginx、Schema、ACL、Skill或wrapper,不需要OBS凭据、服务密钥或重启。
+
+## 包与目录
+
+发布版本:`ksf.five-views.20261103.v1`。脚本需要Node.js 20或以上,无npm安装步骤。
+
+将zip解压到站点以外的新目录,例如`/opt/ksf-five-views-20261103/`。不要解压到站点内。进入解包根目录,执行`sha256sum -c FILES.sha256`,必须全部通过。
+
+此前维护人核实的站点为`/opt/kangshifu-web`,执行前再次从生效的nginx配置确认真实root。若不同,向脚本传入实际绝对路径,不猜目录。
+
+## 先计划,再执行
+
+```sh
+node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web
+node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web --apply
+```
+
+第一条默认只读,核对`filesChecked=26`、changes清单与manifest一致。第二条会在站点外创建独立备份目录,先备份全部旧文件,再替换。留存两次完整stdout/stderr/exitcode。任何失败即停,不覆盖未知版本、不改manifest绕过检查。
+
+脚本要求首页SHA256为`dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3`;保留既有公开机会卡、评分、报告与证据修复的五个文件指纹。既有共享文件接受已核验的LF归一化旧指纹;包内新文件及执行后文件使用原始字节SHA256精确核对。
+
+生产`/data/`返回403,所以浏览器JSON部署于`src/assets/showcase/`,不得改回`/data/`或把资料灌进生产表。
+
+## 外部HTTPS与浏览器验收
+
+1. 按manifest对26个文件做HTTPS GET并计算原始SHA256;JSON应为JSON、JS应为JS,不能把SPA的200 HTML兜底当作文件成功。首页哈希保持不变。
+2. 打开并刷新`/showcase`与其五个介绍页:`/showcase/leadership`、`/showcase/research`、`/showcase/collaboration`、`/showcase/validation`、`/showcase/classroom`。五张图与五端入口均可用。
+3. 打开`/cockpit`、`/intel`、`/coop`、`/review`、`/class`。A端12项目/36人/24卡/44报告/3受阻;B端12,000声音/720商品/360引文;课堂18单作业。基线334条,扩展16,446条另计。
+4. 用模拟卡验证本机状态和评分、同名重评,打开来源、报告、四条价格,刷新保留,重置后只清本机演示修改。用浏览器网络日志确认新路由没有生产业务请求。
+5. 检查P01/P03/P10动线、小组/成员筛选、分页、手机布局和旧看板入口。旧报告错误链接应自动恢复;原公开卡详情应正常。对生产状态/评分只检查入口与合同,未经另外指示不为验收改真实记录。
+
+本机已完成69项Node测试、13项静态检查及95项Edge验收;详见`docs/showcase/validation-result.json`。本机回归中的旧公开操作由浏览器拦截并本地回放,生产请求实际发送数为0。本机验证不等于生产验收:部署回执和生产浏览器验收后才能记录`browserVerified=true`。
+
+## 回退
+
+仅使用本次stdout返回的backup绝对目录。先只读核对,确认后执行:
+
+```sh
+node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web --rollback /opt/ksf-five-views-backup-实际编号
+node scripts/publish-five-views.mjs --target-dir /opt/kangshifu-web --rollback /opt/ksf-five-views-backup-实际编号 --apply
+```
+
+回退工具先校验全部当前文件仍为本次发布版本和备份哈希,然后恢复旧文件、删除本包新增文件。发现他人修改即停,不覆盖并发更新。备份和journal保留。
+
+课堂链原有S5/completed、三份报告及13项未决问题无需重跑。模拟状态、课堂完成、员工确认、正式发布、企业审批及真实验证分别处理。

+ 9 - 0
docs/showcase/design.md

@@ -0,0 +1,9 @@
+# 页面与链接
+
+复用现有Lit、设计令牌、渲染期中英混排与History路由。六个介绍页采用自然滚动的图文长页,配五端入口、图片放大、逐步案例与数据口径表;五个操作页使用一致的筛选、分页与本机详情。
+
+五张3840×2160图片为用户提供的HTML/CDP设计预览,明确标明来源,不冒充skill-image结果。真实交互页面链接独立提供。页面随视口自然滚动,表格允许内部横向滚动;不为凑满固定画布拉高空卡。
+
+原有看板导航新增模拟入口,手机保留五个原主Tab,另加模拟介绍快捷入口。链接均保留部署前缀,支持根路径或子目录部署与刷新。
+
+介绍及演示外壳在生产数据判断之前渲染,生产服务不可用时仍可进入静态演示。署名不作为身份认证,所有新增资料明确标模拟。

+ 21 - 0
docs/showcase/prd.md

@@ -0,0 +1,21 @@
+# 五端演示与六个介绍长页
+
+本分支接收王刚001的2429478,在远端d14d02f基线之上整合五端交互与六个介绍页。原始导入分支独立保留;现有公开机会卡、报告阅读和旧错误链接恢复修复全部保留。
+
+| 入口 | 作用 |
+|---|---|
+| /showcase | 总介绍、五端入口、课程、三条产品案例 |
+| /showcase/leadership | A端创新驾驶舱介绍 |
+| /showcase/research | B端情报与洞察介绍 |
+| /showcase/collaboration | C端产品共创介绍 |
+| /showcase/validation | D端验证与评审介绍 |
+| /showcase/classroom | E端课堂与成果介绍 |
+| /cockpit /intel /coop /review /class | 五端可交互演示 |
+
+五端使用334条原始模拟基线,另附16,446条独立扩展,不连接生产业务接口。支持小组、成员、项目、阶段与状态筛选,声音搜索,分页和本机详情。机会卡状态、评分和评论只存本机独立命名空间;同一署名重评替换旧分,跨窗口过期版本被拒绝。
+
+P01展示创意到课堂完成;P03展示目标场景与制备条件冲突;P10展示证据不足时保留其他解释。所有故事步骤可进入对应记录;未产生的后续阶段保持为空。三个半天和夜间研究任务贯通,18单作业指向44份报告。
+
+公开浏览无需登录。演示署名为自填信息,不代表员工身份认证。新入口可从既有看板导航进入。
+
+本任务交付源码与运维部署包;生产部署由运维001执行。模拟资料可用于方案演示,界面持续标明模拟,不冒充真实研究或销量。

+ 13 - 0
docs/showcase/schema.md

@@ -0,0 +1,13 @@
+# 数据合同
+
+`data/preview-seed.json`原件SHA256:`def81ca0592cb8eeae7c77a0522c53a4d283257ee2dd3434196c4cf72a013ca7`。
+
+12个集合实际共334条:6组、36人、72创意、6来源、6引文、24机会卡、12项目、8方案、44报告、72问题、30验证任务、18作业。阶段S3/S4/S5=2/6/4;进行中/受阻/课堂完成=7/3/2。统计不采信声明字段。来源按直接sourceIds与evidenceIds间接来源并集去重。
+
+`src/assets/showcase/demo-extension.json`版本`ksf.synthetic-extension.20261103.v2`,另计16,446条:12,000条虚构声音、720条商品观察、2,880条价格、354条新增引文、180项研究任务、312条协作记录。引文含基础6条,共360条;每条引文可回查同源原文。合计16,780条,不能称为基线增至16,780。
+
+全部资料属于同一demoId、dataMode=sample、isSynthetic=true。服务拒绝混入live记录、悬空/跨组引用或不在同源rawText的引文。不读取历史ext-live-20261103资料。
+
+本机修改键`kf-innovation-demo::20261103::v2`只包含演示状态、评分、评论和revision;与生产身份存储分离。重置只移除此键,恢复334条基线与扩展的原始状态,不清其他localStorage。
+
+方案四项未知规格继续为null,formulaItems/productionSteps为空,课堂完成不清空未决问题。没有生成配方、真实研究结论或正式发布。

+ 9 - 0
docs/showcase/test.md

@@ -0,0 +1,9 @@
+# 验证范围
+
+Node:现有模型、报告与公开接口合同回归;基础334条计数、同源引文与来源并集;扩展16,446条、12,000条不同原文、价格和引用完整性;筛选联动;状态/评分/评论持久化、跨窗口冲突和写入失败处理。
+
+真实Edge:六个介绍页、五个交互页与刷新;子路径部署;Shadow DOM导航;三条案例步骤;图片放大和Escape;筛选、搜索空态、分页、商品四条价格、本机详情、机会卡状态/评分、同名重评、评论、来源回查、重置与未知记录。检查手机横向溢出,记录控制台及全部网络。
+
+浏览器验收拦截所有外部请求。演示与介绍操作不产生外部业务请求;返回旧路由的公开读取使用本机回放,生产请求发送数为0。本机验证不等于生产浏览器验收。
+
+部署工具默认只读计划;以临时站点验证发布、重复发布、回退、未知版本拒绝、并发变更和失败后恢复。生产实际发布与外网验证留给运维001。

+ 26 - 0
docs/showcase/validation-result.json

@@ -0,0 +1,26 @@
+{
+  "version": "ksf.five-views.validation.20261103.v1",
+  "nodeTests": {
+    "total": 69,
+    "passed": 69,
+    "failed": 0
+  },
+  "staticChecks": {
+    "total": 13,
+    "passed": 13
+  },
+  "browser": {
+    "engine": "Microsoft Edge headless",
+    "checks": 95,
+    "passed": 95,
+    "errors": [],
+    "productionRequestsSent": 0,
+    "externalTrafficIntercepted": true,
+    "productionBrowserVerified": false
+  },
+  "scope": {
+    "baseline": 334,
+    "extension": 16446,
+    "deployed": false
+  }
+}

+ 38 - 0
scripts/build-five-views-package.mjs

@@ -0,0 +1,38 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { fileURLToPath } from 'node:url';
+import { FILES, RELEASE } from './publish-five-views.mjs';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');
+const baseline = JSON.parse(fs.readFileSync(path.join(root, 'deploy/five-views-production-baseline-20261103.json')));
+const prior = new Map(baseline.records.map((r) => [r.path, r]));
+const manifest = {
+  version: 'ksf.five-views.publish.v1', release: RELEASE,
+  homepageSha256: prior.get('index.html').rawSha256,
+  files: FILES.map((name) => ({ path: name, bytes: fs.statSync(path.join(root, name)).size,
+    sha256: sha(fs.readFileSync(path.join(root, name))), allowMissing: !prior.get(name)?.present,
+    priorNormalizedSha256: prior.get(name)?.present ? [prior.get(name).normalizedSha256] : [] })),
+  preserved: ['src/services/board-service.js', 'src/components/kf-opportunity-card.js', 'src/pages/view-pool.js', 'src/core/report.js', 'src/core/evidence/chain.js']
+    .map((name) => {
+      const normalized = sha(Buffer.from(fs.readFileSync(path.join(root, name), 'utf8').replaceAll('\r\n', '\n')));
+      if (normalized !== prior.get(name)?.normalizedSha256) throw new Error('Published repair would change: ' + name);
+      return { path: name, normalizedSha256: normalized };
+    }),
+};
+fs.writeFileSync(path.join(root, 'deploy/five-views-files-20261103.json'), JSON.stringify(manifest, null, 2));
+if (!process.argv.includes('--manifest-only')) {
+  const index = process.argv.indexOf('--out');
+  if (index < 0 || !process.argv[index + 1]) throw new Error('Use --out ABSOLUTE_NEW_DIRECTORY or --manifest-only');
+  const out = process.argv[index + 1];
+  if (!path.isAbsolute(out) || fs.existsSync(out) || out === root) throw new Error('Output must be a new absolute directory');
+  fs.mkdirSync(out, { recursive: true });
+  const copy = (source, target = source) => { const dest = path.join(out, target); fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.copyFileSync(path.join(root, source), dest); };
+  for (const file of FILES) copy(file, 'payload/' + file);
+  for (const file of ['scripts/publish-five-views.mjs', 'deploy/five-views-files-20261103.json', 'deploy/five-views-production-baseline-20261103.json',
+    'docs/showcase/prd.md', 'docs/showcase/schema.md', 'docs/showcase/api.md', 'docs/showcase/design.md', 'docs/showcase/test.md', 'docs/showcase/deploy.md', 'docs/showcase/validation-result.json']) copy(file);
+  const all = fs.readdirSync(out, { recursive: true }).filter((p) => fs.statSync(path.join(out, p)).isFile()).sort();
+  fs.writeFileSync(path.join(out, 'FILES.sha256'), all.map((p) => sha(fs.readFileSync(path.join(out, p))) + '  ' + p.replaceAll('\\', '/')).join('\n') + '\n');
+  console.log(JSON.stringify({ ok: true, release: RELEASE, out, payloadFiles: FILES.length, files: all.length + 1, deployed: false }));
+} else console.log(JSON.stringify({ ok: true, payloadFiles: FILES.length, manifestOnly: true }));

+ 6 - 5
scripts/check.mjs

@@ -42,7 +42,8 @@ function walk(dir, out = [], filter = () => true) {
 /** 去注释(保留字符串:判断 import / innerHTML 这类字面量) */
 const stripComments = (s) => s.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:'"`])\/\/[^\n]*/g, '$1');
 
-const srcFiles = walk(join(ROOT, 'src'), [], (p) => p.endsWith('.js') && !p.includes('/dev/'));
+const slash = (p) => p.replaceAll('\\', '/');
+const srcFiles = walk(join(ROOT, 'src'), [], (p) => p.endsWith('.js') && !slash(p).includes('/dev/'));
 const rel = (p) => relative(ROOT, p);
 const read = (p) => readFileSync(p, 'utf8');
 
@@ -72,7 +73,7 @@ const htmlBad = srcFiles.filter((f) => /innerHTML/.test(stripComments(read(f))))
 check(4, '无 innerHTML 拼接', htmlBad.length === 0, htmlBad.map((f) => rel(f)).join('\n'));
 
 // 5. core 不碰 DOM / fetch(去注释、去 import 路径里的词)
-const coreFiles = srcFiles.filter((f) => f.includes(`${join(ROOT, 'src', 'core')}/`));
+const coreFiles = srcFiles.filter((f) => slash(f).startsWith(slash(join(ROOT, 'src', 'core')) + '/'));
 const DOM_GLOBALS = /\b(window|document|localStorage|sessionStorage|navigator|HTMLElement|CustomEvent|fetch)\b/;
 const coreHits = [];
 for (const f of coreFiles) {
@@ -86,9 +87,9 @@ for (const f of coreFiles) {
 check(5, `core 层不碰 DOM/fetch(${coreFiles.length} 个文件)`, coreHits.length === 0, coreHits.join('\n'));
 
 // 6. 视图 / 组件不直接 fetch(网络只在 services → utils/http.js)
-const viewFiles = srcFiles.filter((f) => /\/(pages|layout|components)\//.test(f));
+const viewFiles = srcFiles.filter((f) => /\/(pages|layout|components)\//.test(slash(f)));
 const fetchHits = viewFiles.filter((f) => /\bfetch\s*\(/.test(stripComments(read(f))));
-const svcFetch = srcFiles.filter((f) => f.includes(`${join(ROOT, 'src', 'services')}/`) && /fetch\s*\(/.test(stripComments(read(f))));
+const svcFetch = srcFiles.filter((f) => slash(f).startsWith(slash(join(ROOT, 'src', 'services')) + '/') && /fetch\s*\(/.test(stripComments(read(f))));
 check(6, '视图/组件不直接 fetch(且 services 也不直连 fetch)', fetchHits.length === 0 && svcFetch.length === 0,
   [...fetchHits, ...svcFetch].map((f) => rel(f)).join('\n'));
 
@@ -180,7 +181,7 @@ for (const f of srcFiles) {
     defs.push([f, m[1]]);
     if (!m[1].includes('-')) defineBad.push(`${rel(f)}: ${m[1]}`);
     if (!/customElements\.get\(/.test(body) && !/if\s*\(!customElements\.get/.test(body) && !f.includes('components/base.js')) defineBad.push(`${rel(f)}: define(${m[1]}) 未判重`);
-    if (f.includes('/components/') && !/extends\s+LitElement/.test(body) && !f.endsWith('index.js') && !f.endsWith('base.js')) litBad.push(`${rel(f)}: ${m[1]} 不是 LitElement`);
+    if (slash(f).includes('/components/') && !/extends\s+LitElement/.test(body) && !f.endsWith('index.js') && !f.endsWith('base.js')) litBad.push(`${rel(f)}: ${m[1]} 不是 LitElement`);
   }
 }
 check('E', `自定义元素标签带连字符、注册幂等、wc 用 Lit 实现(${defs.length} 个)`, defineBad.length === 0 && litBad.length === 0,

+ 100 - 0
scripts/generate-demo-extension.py

@@ -0,0 +1,100 @@
+"""Deterministic fictional industry data; no real users, scraped quotes or production IO."""
+import hashlib
+import json
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+seed = json.loads((ROOT / 'data/preview-seed.json').read_text(encoding='utf-8'))
+DEMO = seed['groups'][0]['demoId']
+COMMON = dict(demoId=DEMO, dataMode='sample', isSynthetic=True, provenanceType='synthetic_demo')
+ext = dict(version='ksf.synthetic-extension.20261103.v2', demoId=DEMO, isSynthetic=True,
+           note='全部为规则生成的虚构观察;不是实际访谈、商品报价、销量或营养检测。',
+           voices=[], observations=[], prices=[], citations=[], researchTasks=[], collaborationRecords=[])
+contexts = [
+    ['工位午餐', '加班后在茶水间', '通勤前的早餐', '午休只剩十五分钟', '下午临时加餐', '晚归后独自吃饭'],
+    ['宿舍晚自习后', '考试周的午饭', '同寝分享夜宵', '周末留校晚饭', '图书馆回宿舍', '月末控制餐费'],
+    ['有炉具的露营', '只有保温水壶的徒步', '长途高铁上', '服务区停留', '雨天帐篷里', '出差住酒店'],
+    ['运动后便餐', '午间轻食', '晚间控制总能量', '轮班后的早餐', '通勤时补一餐', '周末在家吃面'],
+    ['两人晚餐', '独自加班后回家', '孩子睡后的夜宵', '家庭周末午饭', '冰箱剩菜搭配', '临时替代外卖'],
+    ['第一次尝红酸汤', '连续第二次吃地域风味', '朋友推荐的椒麻面', '周末尝新', '节前囤几种口味', '与室友分吃新口味'],
+]
+purposes = ['这顿当正餐', '只是两餐之间垫一下', '想和同伴分着吃', '主要想喝热汤', '只是嘴馋试味道', '晚饭之后又有点饿']
+tools = ['手边只有热水,没有锅', '可以用电煮锅,愿意多等三分钟', '微波炉要排队,暂时不考虑', '需要倒水的做法不方便', '有锅但不想洗太多餐具', '有小炉子但用水有限']
+tastes = ['喜欢葱香但不想味道留在房间', '想要番茄酸味,不喜欢甜腻', '希望辣油单独放,可以自己决定', '更在意面条有嚼劲', '喜欢浓酱,清淡不是我的首选', '汤鲜就行,不追求地名听起来正宗']
+behaviors = ['我平时就加鸡蛋,不代表这份面不够', '这次吃一份刚好,不打算再加主食', '如果是加餐,我可能只吃半份', '愿意配蔬菜是为了换口感', '偶尔会再买饭团,要先区分是不是没吃正餐', '包装里有没有碗,比多一包酱更影响我选择']
+budgets = ['每份五元以内才会常买', '七元上下可以接受,要把运费算进去', '愿意多花两元换方便的制备', '组合装先算清楚每顿多少钱', '促销会囤,平时按需要买', '价格不是第一位,吃得方便更重要']
+frequencies = ['每周大约一次', '一个月两三次', '只在赶时间时', '最近想换口味时', '出差或值班才', '和朋友一起时']
+channels = ['模拟社媒分享', '模拟电商评价', '模拟追问记录', '模拟使用日记']
+for group_index, group in enumerate(seed['groups']):
+    group_projects = [p for p in seed['projects'] if p['groupId'] == group['id']]
+    for i in range(2000):
+        # A coprime permutation spreads all dimensions rather than repeating six stock quotes.
+        code = (i * 7919 + group_index * 137) % (6 ** 7)
+        digits = []
+        for _ in range(7):
+            digits.append(code % 6); code //= 6
+        a, b, c, d, e, f, g = digits
+        raw = f'{contexts[group_index][a]},{purposes[b]}。{tools[c]};{tastes[d]}。{behaviors[e]}。{budgets[f]},我{frequencies[g]}会考虑方便面。'
+        project = group_projects[i % 2]
+        ext['voices'].append(dict(COMMON, id=f'SIM-VOICE-{group_index+1}-{i+1:04d}', groupId=group['id'], projectId=project['id'],
+            title=f'{contexts[group_index][a]}|{purposes[b]}', rawText=raw, channel=channels[i % 4],
+            authorLabel=f'模拟观察{group_index+1}-{i+1:04d}', verificationState='synthetic_not_real_interview',
+            mealPurpose=purposes[b], preparation=tools[c], budget=budgets[f], behavior=behaviors[e],
+            contentState=['相关分享', '相关分享', '广告待排除', '内容不全', '相关分享'][i % 5]))
+
+packaging = ['袋装', '碗装', '杯装', '双份组合', '家庭六连包']
+flavors = ['番茄鸡蛋', '葱油酱香', '菌菇清汤', '轻辣芝士', '红酸汤', '川香椒麻']
+for i in range(720):
+    p = seed['projects'][i % 12]
+    pack_count = [1, 1, 2, 6, None][i % 5]
+    weight = [85, 95, 110, 125, None][(i // 5) % 5]
+    obs = dict(COMMON, id=f'SIM-OBS-{i+1:04d}', groupId=p['groupId'], projectId=p['id'],
+        title=f'模拟系列{(i//30)+1:02d} · {flavors[(i//5)%6]} · {packaging[i%5]}',
+        productName=f'模拟系列{(i//30)+1:02d}', flavor=flavors[(i//5)%6], packaging=packaging[i%5],
+        netWeightG=weight, packCount=pack_count, preparation=['热水复水候选', '需煮制候选', '条件待核'][i % 3],
+        nutritionClaim=None, actualSku=None, note='虚构商品观察,未关联真实在售商品;营养与工艺未知。')
+    ext['observations'].append(obs)
+    for j, day in enumerate(['2026-10-12', '2026-10-19', '2026-10-26', '2026-11-02']):
+        count = pack_count
+        paid = round((4.5 + (i % 17) * .37 + j * .25) * (count or 1), 2)
+        comparable = bool(count and weight and j != 2)
+        ext['prices'].append(dict(COMMON, id=f'SIM-PRICE-{i+1:04d}-{j+1}', observationId=obs['id'], projectId=p['id'], groupId=p['groupId'],
+            date=day, channel=['模拟电商自营', '模拟商超', '模拟促销组合', '模拟便利店'][j], currency='CNY',
+            pricePaid=paid, includesShipping=j == 0, packCount=count, netWeightG=weight,
+            unitPricePer100G=round(paid / (count * weight) * 100, 2) if comparable else None,
+            comparisonState='可按演示规格比较' if comparable else '口径待核,不参与单位价比较'))
+
+for i in range(354):
+    source = ext['voices'][(i * 31) % len(ext['voices'])]
+    quote = source['rawText'].split(';')[0] + ';'
+    ext['citations'].append(dict(COMMON, id=f'SIM-EVIDENCE-{i+1:03d}', groupId=source['groupId'], projectId=source['projectId'],
+        sourceId=source['id'], quote=quote, locator=dict(start=0, end=len(quote)), reviewState='demo_only',
+        stance=['support', 'contradict', 'unresolved'][i % 3], claim='用于演示不同场景与其他解释,并非已验证用户需求。'))
+
+for i in range(180):
+    p = seed['projects'][i % 12]
+    state = ['done', 'in_progress', 'pending', 'failed', 'reused'][i % 5]
+    ext['researchTasks'].append(dict(COMMON, id=f'SIM-RESEARCH-{i+1:03d}', groupId=p['groupId'], projectId=p['id'],
+        ownerMemberId=p['memberIds'][(i//12) % 6], scope=['核对面体制备说明', '区分正餐与加餐', '检查商品组合规格', '核对相反材料', '追问搭配原因'][i % 5],
+        state=state, requiredMaterial=['包装制备原文', '带情境的追问记录', '规格与价格截图'][i % 3],
+        failureReason='演示来源不可用,保留失败并待补其他来源' if state == 'failed' else None,
+        note='模拟任务回放;没有执行真实采集。'))
+
+for i in range(312):
+    p = seed['projects'][i % 12]
+    owner = seed['members'][(i * 7) % 36]
+    body = ['这条声音是否可能来自加餐?建议先问清楚当餐目的。', '已在模拟资料中补上制备条件,原方案版本保留。',
+            '支持继续研究,但不能把搭配习惯直接当成份量不足。', '价格缺少包数,请先核规格,暂不参与单位价比较。',
+            '工艺条件与目标场景冲突,建议保留暂停理由并分配补证任务。', '课堂方案可展示,实际成本和营养参数仍待核实。'][i % 6]
+    ext['collaborationRecords'].append(dict(COMMON, id=f'SIM-COLLAB-{i+1:03d}', groupId=p['groupId'], projectId=p['id'],
+        ownerMemberId=owner['id'], user=owner['displayName'], kind=['质疑', '答复', '补证', '版本说明', '取舍', '复核'][i % 6], body=body,
+        at=f'2026-11-0{1+i%3}T{9+(i%8):02d}:{i%60:02d}:00+08:00', note='模拟协作记录'))
+
+counts = {key: len(ext[key]) for key in ['voices', 'observations', 'prices', 'citations', 'researchTasks', 'collaborationRecords']}
+assert len({v['rawText'] for v in ext['voices']}) == 12000
+assert sum(counts.values()) == 16446
+ext['counts'] = counts
+target = ROOT / 'src/assets/showcase/demo-extension.json'
+target.write_text(json.dumps(ext, ensure_ascii=False, separators=(',', ':')) + '\n', encoding='utf-8')
+print(json.dumps(dict(file=str(target), bytes=target.stat().st_size, counts=counts, uniqueVoices=12000,
+                     totalEvidence=len(seed['evidence'])+counts['citations'], sha256=hashlib.sha256(target.read_bytes()).hexdigest()), ensure_ascii=False))

+ 169 - 0
scripts/publish-five-views.mjs

@@ -0,0 +1,169 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { fileURLToPath } from 'node:url';
+
+export const RELEASE = 'ksf.five-views.20261103.v1';
+export const FILES = Object.freeze([
+  'src/layout/kf-app.js', 'src/layout/kf-nav.js', 'src/layout/sample-shell.js',
+  'src/utils/router.js', 'src/utils/http.js', 'src/services/workshop-data.js',
+  'src/services/sample-data.js', 'src/services/showcase-data.js',
+  'src/core/sample.js', 'src/core/showcase.js',
+  'src/pages/sample-base.js', 'src/pages/sample-detail.js',
+  'src/pages/view-cockpit.js', 'src/pages/view-intel.js', 'src/pages/view-coop.js',
+  'src/pages/view-review.js', 'src/pages/view-class.js',
+  'src/pages/view-showcase.js', 'src/pages/showcase-content.js',
+  'src/assets/showcase/leadership.png', 'src/assets/showcase/research.png',
+  'src/assets/showcase/collaboration.png', 'src/assets/showcase/validation.png',
+  'src/assets/showcase/classroom.png', 'src/assets/showcase/preview-seed.json', 'src/assets/showcase/demo-extension.json',
+]);
+const PRESERVED = ['src/services/board-service.js', 'src/components/kf-opportunity-card.js',
+  'src/pages/view-pool.js', 'src/core/report.js', 'src/core/evidence/chain.js'];
+const sha = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
+const normalized = (bytes) => sha(Buffer.from(bytes.toString('utf8').replaceAll('\r\n', '\n')));
+const fail = (code, file) => { throw Object.assign(new Error(code), { code, file }); };
+const plain = (file) => { const s = fs.lstatSync(file); if (!s.isFile() || s.isSymbolicLink()) fail('NOT_PLAIN_FILE', file); return s; };
+const hashPattern = /^[a-f0-9]{64}$/;
+function safe(root, relative) {
+  if (typeof relative !== 'string' || relative.split('/').some((s) => !s || s === '.' || s === '..' || s.includes('\\') || s.includes(':'))) fail('BAD_RELATIVE_PATH');
+  let current = root;
+  for (const part of relative.split('/')) {
+    current = path.join(current, part);
+    if (fs.existsSync(current) && fs.lstatSync(current).isSymbolicLink()) fail('SYMLINK_PATH', relative);
+  }
+  return current;
+}
+function unchanged(file, expected) {
+  if (expected === null ? fs.existsSync(file) : !fs.existsSync(file) || sha(fs.readFileSync(file)) !== expected) fail('CONCURRENT_CHANGE', file);
+}
+function atomicWrite(file, bytes, mode) {
+  const temp = file + '.ksf-' + crypto.randomUUID(); let fd;
+  try {
+    fd = fs.openSync(temp, 'wx', mode); fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
+    fs.renameSync(temp, file);
+  } finally { if (fd !== undefined) fs.closeSync(fd); if (fs.existsSync(temp)) fs.unlinkSync(temp); }
+}
+function targetRoot(targetDir) {
+  if (!path.isAbsolute(targetDir)) fail('ABSOLUTE_TARGET_REQUIRED');
+  const root = path.resolve(targetDir);
+  if (root === path.parse(root).root || fs.realpathSync(root) !== root) fail('UNSAFE_TARGET');
+  return root;
+}
+function checkUntouched(root, manifest) {
+  const homepage = safe(root, 'index.html'); plain(homepage);
+  if (sha(fs.readFileSync(homepage)) !== manifest.homepageSha256) fail('HOMEPAGE_MISMATCH');
+  for (const def of manifest.preserved) {
+    const file = safe(root, def.path); plain(file);
+    if (normalized(fs.readFileSync(file)) !== def.normalizedSha256) fail('PUBLISHED_REPAIR_MISMATCH', def.path);
+  }
+}
+function readManifest(file) {
+  const m = JSON.parse(fs.readFileSync(file, 'utf8'));
+  if (m.version !== 'ksf.five-views.publish.v1' || m.release !== RELEASE || !hashPattern.test(m.homepageSha256)) fail('BAD_MANIFEST');
+  const pathsEqual = (actual, expected) => JSON.stringify([...actual].sort()) === JSON.stringify([...expected].sort());
+  if (!Array.isArray(m.files) || !pathsEqual(m.files.map((f) => f.path), FILES) || !Array.isArray(m.preserved) || !pathsEqual(m.preserved.map((f) => f.path), PRESERVED)) fail('UNEXPECTED_FILE_LIST');
+  for (const f of m.files) if (!hashPattern.test(f.sha256) || !Array.isArray(f.priorNormalizedSha256) || f.priorNormalizedSha256.some((h) => !hashPattern.test(h)) || typeof f.allowMissing !== 'boolean') fail('BAD_MANIFEST');
+  for (const f of m.preserved) if (!hashPattern.test(f.normalizedSha256)) fail('BAD_MANIFEST');
+  return m;
+}
+
+/** Static filesystem changes only. Does not contact an API or infer a site directory. */
+export function publish({ sourceDir, targetDir, manifestPath, apply = false }, hooks = {}) {
+  if (!path.isAbsolute(sourceDir)) fail('ABSOLUTE_SOURCE_REQUIRED');
+  const source = fs.realpathSync(sourceDir), root = targetRoot(targetDir);
+  if (source === root || source.startsWith(root + path.sep) || root.startsWith(source + path.sep)) fail('SOURCE_TARGET_OVERLAP');
+  const manifest = readManifest(manifestPath);
+  checkUntouched(root, manifest);
+  const entries = manifest.files.map((def) => {
+    const input = safe(source, 'payload/' + def.path), target = safe(root, def.path); plain(input);
+    const bytes = fs.readFileSync(input);
+    if (sha(bytes) !== def.sha256) fail('PAYLOAD_HASH_MISMATCH', def.path);
+    const stat = fs.existsSync(target) ? plain(target) : null;
+    const before = stat ? fs.readFileSync(target) : null;
+    const same = before !== null && sha(before) === def.sha256;
+    if (before !== null && !same && !def.priorNormalizedSha256.includes(normalized(before))) fail('UNKNOWN_TARGET_CONTENT', def.path);
+    if (!stat && !def.allowMissing) fail('BASELINE_FILE_MISSING', def.path);
+    return { path: def.path, target, bytes, before, beforeSha256: before === null ? null : sha(before), mode: stat ? stat.mode & 0o777 : 0o644, same };
+  });
+  const changes = entries.filter((e) => !e.same);
+  const result = { ok: true, release: RELEASE, operation: apply ? 'filesystem-update' : 'read-only-plan', readOnly: !apply,
+    filesChecked: entries.length, changes: changes.map((e) => e.path), unchanged: entries.filter((e) => e.same).map((e) => e.path),
+    preservedRepairs: PRESERVED, homepageModified: false, networkRequests: 0, businessWrites: 0, browserVerified: false };
+  if (!apply || !changes.length) return { ...result, backup: null };
+  const lock = path.join(path.dirname(root), '.' + path.basename(root) + '.ksf-static.lock');
+  const fd = fs.openSync(lock, 'wx', 0o600); let backup;
+  const done = [], createdDirs = [];
+  try {
+    backup = fs.mkdtempSync(path.join(path.dirname(root), 'ksf-five-views-backup-'));
+    const journal = { release: RELEASE, targetDir: root, at: new Date().toISOString(), status: 'prepared', homepageSha256: manifest.homepageSha256,
+      preserved: manifest.preserved, files: changes.map((e) => ({ path: e.path, beforeSha256: e.beforeSha256, afterSha256: sha(e.bytes), mode: e.mode })) };
+    const saveJournal = () => fs.writeFileSync(path.join(backup, 'journal.json'), JSON.stringify(journal, null, 2), { mode: 0o600 });
+    for (const e of changes) if (e.before !== null) {
+      const file = safe(backup, e.path); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, e.before, { flag: 'wx', mode: 0o600 });
+    }
+    saveJournal();
+    for (const e of changes) {
+      hooks.beforeWrite?.(e.path);
+      checkUntouched(root, manifest); safe(root, e.path); unchanged(e.target, e.beforeSha256);
+      const absent = []; let parent = path.dirname(e.target);
+      while (!fs.existsSync(parent)) { absent.push(parent); parent = path.dirname(parent); }
+      for (const dir of absent.reverse()) { fs.mkdirSync(dir); createdDirs.push(dir); }
+      atomicWrite(e.target, e.bytes, e.mode); done.push(e);
+    }
+    for (const e of entries) { plain(e.target); if (sha(fs.readFileSync(e.target)) !== sha(e.bytes)) fail('TARGET_VERIFY_FAILED', e.path); }
+    checkUntouched(root, manifest); journal.status = 'verified'; saveJournal();
+    return { ...result, backup, filesystemVerified: true, publicVerificationRequired: true };
+  } catch (error) {
+    let rollbackComplete = true;
+    for (const e of done.reverse()) try {
+      safe(root, e.path); unchanged(e.target, sha(e.bytes));
+      if (e.before === null) fs.unlinkSync(e.target); else atomicWrite(e.target, e.before, e.mode);
+    } catch { rollbackComplete = false; }
+    for (const dir of createdDirs.reverse()) try { fs.rmdirSync(dir); } catch {}
+    if (backup) { const file = path.join(backup, 'journal.json'); if (fs.existsSync(file)) { const journal = JSON.parse(fs.readFileSync(file)); fs.writeFileSync(file, JSON.stringify({ ...journal, status: 'stopped', rollbackComplete }, null, 2)); } }
+    throw Object.assign(error, { backup, rollbackComplete });
+  } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
+}
+
+/** Explicit rollback checks every target and backup before restoring the first file. */
+export function rollback({ targetDir, backupDir, apply = false }) {
+  const root = targetRoot(targetDir);
+  if (!path.isAbsolute(backupDir)) fail('ABSOLUTE_BACKUP_REQUIRED');
+  const backup = fs.realpathSync(backupDir);
+  if (backup === root || backup.startsWith(root + path.sep) || root.startsWith(backup + path.sep)) fail('BACKUP_TARGET_OVERLAP');
+  const journalFile = safe(backup, 'journal.json'); plain(journalFile);
+  const j = JSON.parse(fs.readFileSync(journalFile, 'utf8'));
+  if (j.targetDir !== root || j.release !== RELEASE || j.status !== 'verified' || !Array.isArray(j.files) || j.files.some((e) => !FILES.includes(e.path))) fail('BAD_BACKUP_JOURNAL');
+  checkUntouched(root, j);
+  const entries = j.files.map((e) => {
+    const target = safe(root, e.path); plain(target); unchanged(target, e.afterSha256);
+    let bytes = null;
+    if (e.beforeSha256 !== null) { const file = safe(backup, e.path); plain(file); bytes = fs.readFileSync(file); if (sha(bytes) !== e.beforeSha256) fail('BACKUP_HASH_MISMATCH', e.path); }
+    return { ...e, target, bytes };
+  });
+  const result = { ok: true, operation: apply ? 'filesystem-rollback' : 'read-only-rollback-plan', readOnly: !apply, filesChecked: entries.length, businessWrites: 0, networkRequests: 0, homepageModified: false };
+  if (!apply) return result;
+  const lock = path.join(path.dirname(root), '.' + path.basename(root) + '.ksf-static.lock');
+  const fd = fs.openSync(lock, 'wx', 0o600);
+  try {
+    for (const e of entries.reverse()) { checkUntouched(root, j); safe(root, e.path); unchanged(e.target, e.afterSha256); if (e.bytes === null) fs.unlinkSync(e.target); else atomicWrite(e.target, e.bytes, e.mode); }
+    for (const e of entries) unchanged(e.target, e.beforeSha256);
+    j.status = 'rolled-back'; fs.writeFileSync(journalFile, JSON.stringify(j, null, 2));
+    return { ...result, filesystemVerified: true };
+  } finally { fs.closeSync(fd); fs.unlinkSync(lock); }
+}
+
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+  try {
+    const args = process.argv.slice(2); let targetDir, backupDir, apply = false;
+    for (let i = 0; i < args.length; i++) {
+      if (args[i] === '--apply' && !apply) apply = true;
+      else if (args[i] === '--target-dir' && !targetDir) targetDir = args[++i];
+      else if (args[i] === '--rollback' && !backupDir) backupDir = args[++i];
+      else fail('USAGE');
+    }
+    if (!targetDir) fail('USAGE');
+    const sourceDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+    console.log(JSON.stringify(backupDir ? rollback({ targetDir, backupDir, apply }) : publish({ sourceDir, targetDir, manifestPath: path.join(sourceDir, 'deploy/five-views-files-20261103.json'), apply }), null, 2));
+  } catch (error) { console.error(JSON.stringify({ ok: false, code: error.code || 'PUBLISH_STOPPED', file: error.file, backup: error.backup, rollbackComplete: error.rollbackComplete, message: '已停止。保留回执,核对目录或版本差异,不放宽校验。' }, null, 2)); process.exitCode = 1; }
+}

+ 298 - 0
scripts/verify-showcase-browser.mjs

@@ -0,0 +1,298 @@
+/** Local Edge acceptance. Non-local browser requests are intercepted, never sent. */
+import assert from 'node:assert/strict';
+import { createServer } from 'node:http';
+import { spawn } from 'node:child_process';
+import { readFile, stat, mkdir, writeFile } from 'node:fs/promises';
+import { existsSync, readFileSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { indexHtml } from './config.mjs';
+
+const root = fileURLToPath(new URL('../', import.meta.url));
+const run = path.join(root, 'work', `showcase-browser-${Date.now()}`);
+await mkdir(run, { recursive: true });
+const checks = [], errors = [], requests = [], forbidden = [], observations = [];
+const mime = { '.html': 'text/html', '.js': 'text/javascript', '.mjs': 'text/javascript', '.json': 'application/json', '.png': 'image/png', '.svg': 'image/svg+xml', '.woff2': 'font/woff2' };
+const mount = '/preview-test/';
+let seedUnavailable = false;
+const server = createServer(async (req, res) => {
+  try {
+    const url = new URL(req.url, 'http://localhost');
+    const sub = url.pathname.startsWith(mount);
+    const rel = decodeURIComponent(sub ? url.pathname.slice(mount.length) : url.pathname.slice(1));
+    if (/^(?:api|parse)\//.test(rel)) { res.writeHead(451); res.end('No business proxy'); return; }
+    if (seedUnavailable && rel === 'src/assets/showcase/preview-seed.json') { res.writeHead(503); res.end('sample unavailable'); return; }
+    const file = path.resolve(root, rel || 'index.html');
+    if (!file.startsWith(path.resolve(root) + path.sep)) { res.writeHead(403); res.end(); return; }
+    let body;
+    let ext = path.extname(file);
+    if (ext === '.html' || !ext) { body = indexHtml(sub ? mount : '/', { dev: false }); ext = '.html'; }
+    else { assert.ok((await stat(file)).isFile()); body = await readFile(file); }
+    res.writeHead(200, { 'content-type': mime[ext] || 'application/octet-stream', 'cache-control': 'no-store' });
+    res.end(body);
+  } catch { res.writeHead(404); res.end('missing resource'); }
+});
+await new Promise((done) => server.listen(0, '127.0.0.1', done));
+const origin = `http://127.0.0.1:${server.address().port}`;
+const profile = path.join(run, 'profile');
+await mkdir(profile);
+const executable = process.env.CHROME_PATH || 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe';
+assert.ok(existsSync(executable), 'Edge/Chromium executable required');
+const browser = spawn(executable, ['--headless=new', '--disable-gpu', '--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--remote-debugging-port=0', `--user-data-dir=${profile}`, 'about:blank'], { windowsHide: true, stdio: 'ignore' });
+const pause = (ms) => new Promise((done) => setTimeout(done, ms));
+let socket;
+const check = (name, passed, detail) => { checks.push({ name, passed: Boolean(passed), ...(detail ? { detail } : {}) }); assert.ok(passed, name); };
+try {
+  let port;
+  for (let i = 0; i < 100; i++) {
+    try { port = Number(readFileSync(path.join(profile, 'DevToolsActivePort'), 'utf8').split('\n')[0]); } catch {}
+    if (port) break;
+    await pause(100);
+  }
+  assert.ok(port);
+  const tabs = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json();
+  socket = new WebSocket(tabs.find((x) => x.type === 'page').webSocketDebuggerUrl);
+  await new Promise((done, reject) => { socket.addEventListener('open', done, { once: true }); socket.addEventListener('error', reject, { once: true }); });
+  let seq = 0;
+  const pending = new Map();
+  const cdp = (method, params = {}) => new Promise((resolve, reject) => {
+    const id = ++seq;
+    const timer = setTimeout(() => { pending.delete(id); reject(new Error(`${method} timeout`)); }, 15000);
+    pending.set(id, { resolve, reject, timer });
+    socket.send(JSON.stringify({ id, method, params }));
+  });
+  const evaluate = async (expression) => {
+    const value = await cdp('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
+    if (value.exceptionDetails) throw new Error(value.exceptionDetails.exception?.description || value.exceptionDetails.text);
+    return value.result.value;
+  };
+  const fixture = { result: { ok: true, data: { project: [], artifact: [], insight: [], source: [], session: [], opportunity: [], asOf: '2026-11-03T00:00:00.000Z', nextCursor: null } } };
+  fixture.result.data.artifact.push({ objectId: 'fixture-report', title: '原有报告回归', stageCode: 'S4', report: { title: '原有报告回归', chapters: [{ id: 'scope', title: '核对原文', sections: [{ kind: 'para', text: '公开报告阅读回归正文。' }] }], evidence: {} } });
+  fixture.result.data.opportunity.push({ objectId: 'fixture-opportunity', title: '原有机会卡回归', status: 'candidate', linkedArtifacts: ['fixture-report'], scores: [], updatedAt: '2026-11-03T00:00:00.000Z' });
+  socket.addEventListener('message', ({ data }) => {
+    const m = JSON.parse(data);
+    if (m.id && pending.has(m.id)) {
+      const p = pending.get(m.id); clearTimeout(p.timer); pending.delete(m.id);
+      if (m.error) p.reject(new Error(m.error.message)); else p.resolve(m.result);
+    }
+    if (m.method === 'Runtime.exceptionThrown') errors.push(m.params.exceptionDetails.exception?.description || m.params.exceptionDetails.text);
+    if (m.method === 'Network.requestWillBeSent') requests.push({ url: m.params.request.url, method: m.params.request.method, body: m.params.request.postData || '' });
+    if (m.method === 'Fetch.requestPaused') {
+      const { requestId, request } = m.params;
+      if (request.url.startsWith(origin + '/')) cdp('Fetch.continueRequest', { requestId }).catch((e) => errors.push(e.message));
+      else {
+        forbidden.push({ url: request.url, method: request.method, body: request.postData || '' });
+        const isPublicRead = request.postData?.includes('ksfGetPublicBoard');
+        const isPreflight = request.method === 'OPTIONS';
+        const action = request.postData ? JSON.parse(request.postData) : {};
+        const isPublicAction = ['/ksfSetPublicOpportunityStatus', '/ksfRatePublicOpportunity'].includes(action.path);
+        if (isPublicAction) {
+          const opp = fixture.result.data.opportunity[0], params = action.params;
+          opp.updatedAt = new Date().toISOString();
+          if (action.path.includes('SetPublic')) opp.status = params.status;
+          else opp.scores = [{ user: params.by, score: params.score, at: opp.updatedAt }];
+        }
+        cdp('Fetch.fulfillRequest', { requestId, responseCode: isPreflight ? 204 : isPublicRead || isPublicAction ? 200 : 451,
+          responseHeaders: [{ name: 'Content-Type', value: 'application/json' }, { name: 'Access-Control-Allow-Origin', value: '*' }, { name: 'Access-Control-Allow-Headers', value: 'content-type,x-parse-application-id' }, { name: 'Access-Control-Allow-Methods', value: 'GET,POST,OPTIONS' }],
+          body: Buffer.from(JSON.stringify(isPublicRead ? fixture : isPublicAction ? { result: { ok: true, data: { opportunity: fixture.result.data.opportunity[0] } } } : { error: 'No external traffic in acceptance' })).toString('base64'),
+        }).catch((e) => errors.push(e.message));
+      }
+    }
+  });
+  await cdp('Page.enable'); await cdp('Runtime.enable'); await cdp('Network.enable');
+  await cdp('Fetch.enable', { patterns: [{ urlPattern: '*' }] });
+  await cdp('Page.addScriptToEvaluateOnNewDocument', { source: `const originalInterval=window.setInterval;window.setInterval=(fn,ms,...args)=>originalInterval(fn,ms===30000?100:ms,...args);` });
+  const host = `document.querySelector('kf-app')?.shadowRoot?.querySelector('view-showcase')`;
+  const shadow = `${host}?.shadowRoot`;
+  const ready = async () => {
+    for (let i = 0; i < 100; i++) { if (await evaluate(`Boolean(${host}?._model && !${host}?._busy)`)) return; await pause(100); }
+    throw new Error('showcase did not load');
+  };
+  const go = async (route) => { await cdp('Page.navigate', { url: origin + route }); await ready(); await pause(150); };
+  const shot = async (name, full = false) => {
+    const params = { format: 'png', captureBeyondViewport: full };
+    if (full) {
+      const metrics = await cdp('Page.getLayoutMetrics');
+      params.clip = { x: 0, y: 0, width: metrics.cssContentSize.width, height: metrics.cssContentSize.height, scale: 1 };
+    }
+    const result = await cdp('Page.captureScreenshot', params);
+    await writeFile(path.join(run, name + '.png'), Buffer.from(result.data, 'base64'));
+  };
+  await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
+  for (const key of ['overview', 'leadership', 'research', 'collaboration', 'validation', 'classroom']) {
+    const route = key === 'overview' ? '/showcase' : '/showcase/' + key;
+    await go(route);
+    const info = await evaluate(`(()=>{const el=${host},root=el.shadowRoot;return {system:el.system,total:el._model.total,title:root.querySelector('h1')?.textContent,counts:[...root.querySelectorAll('.count-grid dd')].map(x=>Number(x.textContent)),width:document.documentElement.scrollWidth,viewport:innerWidth,images:[...root.images||root.querySelectorAll('img')].map(x=>({loaded:x.complete&&x.naturalWidth>0,src:x.src})),pending:root.querySelector('.pending')?.textContent};})()`);
+    check(`${key}: correct route, real totals and loaded image`, info.system === key && info.total === 334 && info.counts.length === 12 && info.images.every((x) => x.loaded));
+    check(`${key}: no page horizontal overflow`, info.width <= info.viewport + 1);
+    observations.push(info);
+    await shot(`desktop-${key}`, key === 'overview' || key === 'classroom');
+    await cdp('Page.reload', { ignoreCache: true }); await ready();
+    check(`${key}: survives direct refresh`, await evaluate(`${host}.system===${JSON.stringify(key)}`));
+  }
+  await go('/showcase');
+  await evaluate(`${shadow}.querySelector('a[href$="/showcase/research"]').click()`); await pause(200);
+  check('Shadow DOM links navigate to the correct page', await evaluate(`location.pathname==='/showcase/research'&&${host}.system==='research'`));
+  await evaluate(`${shadow}.querySelector('.image-button').click()`); await pause(100);
+  check('preview opens in a real modal dialog', await evaluate(`${shadow}.querySelector('dialog').open`));
+  await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
+  await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }); await pause(100);
+  check('Escape closes preview', await evaluate(`!${shadow}.querySelector('dialog').open`));
+
+  await go('/showcase?case=P01');
+  for (let i = 0; i < 7; i++) {
+    const observation = await evaluate(`(()=>{const el=${host};return {step:el._step,text:el.shadowRoot.querySelector('.story-body').textContent};})()`);
+    check(`P01 step ${i + 1} displays its real record`, observation.step === i && observation.text.trim().length > 10);
+    if (i < 6) await evaluate(`${shadow}.querySelector('.story-controls .primary').click()`);
+    await pause(60);
+  }
+  check('P01 retains six questions after classroom completion', await evaluate(`${shadow}.querySelectorAll('.question-list li').length===6`));
+  await go('/showcase/validation?case=P03');
+  await evaluate(`${shadow}.querySelectorAll('.story-steps button')[1].click()`); await pause(80);
+  check('P03 shows preparation conflict', await evaluate(`${shadow}.querySelector('.story-body').textContent.includes('无灶具')`));
+  await evaluate(`${shadow}.querySelectorAll('.story-steps button')[2].click()`); await pause(80);
+  check('P03 questions retain owners', await evaluate(`${shadow}.querySelectorAll('.question-list li').length===6&&${shadow}.querySelector('.question-list').textContent.includes('演示学员')`));
+  await go('/showcase/validation?case=P10');
+  await evaluate(`${shadow}.querySelectorAll('.story-steps button')[3].click()`); await pause(80);
+  check('P10 does not fabricate missing validation tasks', await evaluate(`${shadow}.querySelector('.empty-inline').textContent.includes('尚无验证任务')`));
+
+  for (const key of ['overview', 'leadership', 'research', 'collaboration', 'validation', 'classroom']) {
+    await cdp('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
+    await go(key === 'overview' ? '/showcase' : '/showcase/' + key);
+    check(`${key}: mobile has no horizontal page overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
+    if (key === 'overview' || key === 'research') await shot('mobile-' + key, true);
+  }
+  await go(mount + 'showcase/collaboration?case=P01');
+  check('nested deployment preserves route and image URLs', await evaluate(`${host}.system==='collaboration'&&${shadow}.querySelector('img').src.includes('/preview-test/src/assets/showcase/')`));
+  await evaluate(`${shadow}.querySelector('a[href$="/showcase/validation"]').click()`); await pause(150);
+  check('nested navigation preserves base path', await evaluate(`location.pathname==='/preview-test/showcase/validation'`));
+  const before = forbidden.length;
+  await evaluate(`(()=>{const app=document.querySelector('kf-app');for(const type of ['refresh','status-change','score-submit','artifact-open'])app.dispatchEvent(new CustomEvent(type,{detail:{id:'DEMO-O01',status:'approved',by:'模拟'}}));})()`);
+  await pause(500);
+  check('sample shell ignores global live business events', forbidden.length === before);
+  check('all introduction interactions made zero external business requests', forbidden.length === 0);
+
+  const sampleHost = `document.querySelector('kf-app').shadowRoot.querySelector('sample-shell')`;
+  const viewHost = `${sampleHost}.shadowRoot.querySelector('main').firstElementChild`;
+  const viewRoot = `${viewHost}.shadowRoot`;
+  const detailHost = `${sampleHost}.shadowRoot.querySelector('sample-detail')`;
+  const detailRoot = `${detailHost}.shadowRoot`;
+  const sampleReady = async () => {
+    for (let i = 0; i < 160; i++) { if (await evaluate(`(()=>{const shell=document.querySelector('kf-app')?.shadowRoot?.querySelector('sample-shell');return Boolean(shell?._model && shell.shadowRoot?.querySelector('main')?.firstElementChild?.shadowRoot?.querySelector('h1'));})()`)) { await pause(100); return; } await pause(100); }
+    throw new Error('sample did not load');
+  };
+  const sampleGo = async (route) => { await cdp('Page.navigate', { url: origin + route }); await sampleReady(); };
+  const clickRecord = async (id) => { await evaluate(`${viewRoot}.querySelector('button[data-record="${id}"]').click()`); await pause(150); };
+  const submit = async (action, fields) => {
+    await evaluate(`(()=>{const form=${detailRoot}.querySelector('[data-action="${action}"]').closest('form');for(const [key,value] of Object.entries(${JSON.stringify(fields)}))form.elements.namedItem(key).value=value;form.requestSubmit();})()`);
+    await pause(200);
+  };
+  await cdp('Emulation.setDeviceMetricsOverride', { width: 1440, height: 1000, deviceScaleFactor: 1, mobile: false });
+  for (const name of ['cockpit', 'intel', 'coop', 'review', 'class']) {
+    await sampleGo('/' + name);
+    check(`${name}: independent sample shell and actual extension`, await evaluate(`${sampleHost}._model.baselineEntityCount===334&&${sampleHost}._model.extension.voices.length===12000`));
+    check(`${name}: no page horizontal overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
+    await shot('demo-' + name);
+    await cdp('Page.reload', { ignoreCache: true }); await sampleReady();
+    check(`${name}: refresh preserves route`, await evaluate(`${sampleHost}.view===${JSON.stringify(name)}`));
+  }
+  await sampleGo('/cockpit?group=DEMO-G2');
+  check('group filter scopes both project and report counts', await evaluate(`${viewHost}.filtered.projects.length===2&&${viewHost}.filtered.reports.every(r=>r.groupId==='DEMO-G2')`));
+  await evaluate(`${viewRoot}.querySelector('[data-filter="group"]').value='';${viewRoot}.querySelector('[data-filter="group"]').dispatchEvent(new Event('change',{bubbles:true}));`); await pause(100);
+  await evaluate(`${viewRoot}.querySelector('[data-filter="member"]').value='DEMO-M31';${viewRoot}.querySelector('[data-filter="member"]').dispatchEvent(new Event('change',{bubbles:true}));`); await pause(150);
+  check('member selection filters owned and collaborating projects', await evaluate(`${viewHost}.filtered.projects.length<12&&${viewHost}.filtered.projects.some(p=>p.id==='DEMO-P10')`));
+  await sampleGo('/intel');
+  await evaluate(`${viewRoot}.querySelector('[data-filter="search"]').value='完全无此关键词XYZ';${viewRoot}.querySelector('[data-filter="search"]').dispatchEvent(new Event('change',{bubbles:true}));`); await pause(150);
+  check('search has a real zero-result state', await evaluate(`${viewHost}.filtered.voices.length===0&&${viewRoot}.textContent.includes('没有符合条件')`));
+  await cdp('Page.reload', { ignoreCache: true }); await sampleReady();
+  check('search is preserved in URL after refresh', await evaluate(`${viewHost}.filters.search==='完全无此关键词XYZ'`));
+  await sampleGo('/intel');
+  await evaluate(`${viewRoot}.querySelector('[data-page="voicePage-next"]').click()`); await pause(150);
+  check('pagination changes the visible voice records', await evaluate(`location.search.includes('voicePage=2')&&${viewRoot}.querySelector('[data-record^="SIM-VOICE"]').dataset.record==='SIM-VOICE-1-0011'`));
+  await clickRecord('SIM-OBS-0001');
+  check('product detail shows four distinct price snapshots', await evaluate(`${detailRoot}.querySelectorAll('tbody tr').length===4`));
+  await cdp('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 });
+  await cdp('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Escape', code: 'Escape', windowsVirtualKeyCode: 27 }); await pause(150);
+  check('Escape closes record detail and removes deep-link query', await evaluate(`!${detailRoot}.querySelector('dialog').open&&!location.search.includes('record=')`));
+
+  await sampleGo('/coop');
+  await evaluate(`localStorage.setItem('unrelated-demo-sentinel','preserve')`);
+  await clickRecord('DEMO-O01');
+  check('opportunity opens local dialog', await evaluate(`${detailRoot}.querySelector('dialog').open&&${detailHost}.recordId==='DEMO-O01'`));
+  await submit('status', { user: '模拟甲', status: 'candidate' });
+  check('status action mutates only sample state', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].status==='candidate'`));
+  await submit('score', { user: '模拟甲', score: '80', comment: '先核制备条件' });
+  await submit('score', { user: '模拟乙', score: '60' });
+  await submit('score', { user: '模拟甲', score: '100' });
+  check('two voters and same-name replacement produce correct average', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].stats.avg===80&&${sampleHost}._model.opportunitiesById['DEMO-O01'].stats.n===2`));
+  await cdp('Page.reload', { ignoreCache: true }); await sampleReady();
+  check('status, scores and open record survive refresh', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].status==='candidate'&&${sampleHost}._model.opportunitiesById['DEMO-O01'].stats.avg===80&&${detailRoot}.querySelector('dialog').open`));
+  await evaluate(`${detailRoot}.querySelector('[data-record="DEMO-P01"]').click()`); await pause(150);
+  await submit('comment', { user: '模拟甲', body: '请补复水与沥水条件。' });
+  check('project comment is retained with its project', await evaluate(`${sampleHost}._model.comments['DEMO-P01'].some(c=>c.body==='请补复水与沥水条件。')`));
+  await evaluate(`${detailRoot}.querySelector('[data-record="DEMO-R-P01-S3"]').click()`); await pause(150);
+  check('report detail opens actual stage record and pending questions', await evaluate(`${detailHost}.recordId==='DEMO-R-P01-S3'&&${detailRoot}.textContent.includes('依据与适用边界')&&${detailRoot}.querySelectorAll('[data-record^="DEMO-Q01"]').length===6`));
+  await evaluate(`${detailRoot}.querySelector('[data-record="DEMO-S01"]').click()`); await pause(150);
+  check('report source opens original sample text', await evaluate(`${detailRoot}.querySelector('.raw').textContent.includes('中午只有热水')`));
+  await sampleGo('/coop?detail=source&record=UNKNOWN');
+  check('unknown sample detail shows local error with no production lookup', await evaluate(`${detailRoot}.textContent.includes('不会向线上接口补查')`));
+  await sampleGo('/review?stage=S3');
+  check('S3 does not fabricate later validation tasks', await evaluate(`${viewHost}.filtered.validationTasks.length===0&&${viewHost}.filtered.questions.length===12`));
+  await sampleGo('/class'); await clickRecord('DEMO-A-DEMO-G1-2');
+  check('class assignment resolves its own report references', await evaluate(`${detailRoot}.querySelectorAll('[data-record^="DEMO-R-"]').length===4`));
+  for (const [key, count] of [['P01', 7], ['P03', 4], ['P10', 4]]) {
+    await sampleGo(`/review?case=${key}&step=${count - 1}`);
+    check(`${key}: clickable guided path resolves to local project records`, await evaluate(`${viewRoot}.querySelectorAll('.steps button').length===${count}&&${viewRoot}.querySelectorAll('.story-records [data-record]').length>0`));
+  }
+  await sampleGo('/coop');
+  await evaluate(`${sampleHost}.shadowRoot.querySelector('.sample-strip button').click()`); await pause(200);
+  check('reset restores statuses and removes local votes while preserving unrelated storage', await evaluate(`${sampleHost}._model.opportunitiesById['DEMO-O01'].status==='approved'&&${sampleHost}._model.opportunitiesById['DEMO-O01'].stats===null&&localStorage.getItem('unrelated-demo-sentinel')==='preserve'`));
+  for (const name of ['cockpit', 'intel', 'coop', 'review', 'class']) {
+    await cdp('Emulation.setDeviceMetricsOverride', { width: 390, height: 844, deviceScaleFactor: 1, mobile: true });
+    await sampleGo('/' + name);
+    check(`${name}: mobile layout permits table scrolling without page overflow`, await evaluate('document.documentElement.scrollWidth<=innerWidth+1'));
+    if (name === 'coop') await shot('demo-mobile-coop');
+  }
+  await sampleGo(mount + 'coop?detail=opportunity&record=DEMO-O01');
+  check('nested demo detail loads without production routing', await evaluate(`${detailHost}.recordId==='DEMO-O01'&&${detailRoot}.querySelector('dialog').open`));
+  await evaluate(`${detailHost}.close()`); await pause(100);
+  await evaluate(`(()=>{const app=document.querySelector('kf-app');for(const type of ['refresh','status-change','score-submit'])app.dispatchEvent(new CustomEvent(type,{detail:{id:'DEMO-O01',status:'approved'}}));})()`); await pause(350);
+  check('five views and local operations made zero external business requests', forbidden.length === 0);
+
+  // Existing live route regression uses a local interception fixture, never production.
+  await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate('/'))`); await pause(600);
+  check('existing live route resumes loading', forbidden.some((r) => r.body.includes('ksfGetPublicBoard')));
+  check('existing desktop and mobile navigation expose base-safe demo entry', await evaluate(`(()=>{const root=document.querySelector('kf-app').shadowRoot.querySelector('kf-nav').shadowRoot;return root.querySelector('a[data-path="/showcase"]').href.includes('/preview-test/showcase')&&root.querySelector('.showcase-entry').href.includes('/preview-test/showcase');})()`));
+  const legacyNavigate = async (route) => { await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate(${JSON.stringify(route)}))`); await pause(450); };
+  await legacyNavigate('/pool/fixture-report');
+  check('legacy wrong report link recovers to report route', await evaluate(`location.pathname==='/preview-test/report/fixture-report'&&Boolean(document.querySelector('kf-app').shadowRoot.querySelector('view-report'))`));
+  await legacyNavigate('/pool/fixture-opportunity');
+  const legacyDrawer = `document.querySelector('kf-app').shadowRoot.querySelector('kf-drawer').shadowRoot`;
+  check('existing public opportunity drawer resolves its record', await evaluate(`${legacyDrawer}.textContent.includes('决策与状态')&&!${legacyDrawer}.textContent.includes('未找到这张机会卡')`));
+  await evaluate(`${legacyDrawer}.querySelectorAll('.st-row button')[1].click()`); await pause(250);
+  check('existing state button retains public function contract', forbidden.some((r) => r.body.includes('ksfSetPublicOpportunityStatus') && r.body.includes('fixture-opportunity')));
+  await evaluate(`document.querySelector('kf-app').dispatchEvent(new CustomEvent('score-submit',{detail:{user:'回归访客',score:85,comment:'本机拦截回放'}}))`); await pause(250);
+  check('existing rating retains public function contract', forbidden.some((r) => r.body.includes('ksfRatePublicOpportunity') && r.body.includes('fixture-opportunity')));
+  await evaluate(`import(${JSON.stringify(origin + mount + 'src/utils/router.js')}).then(m=>m.navigate('/showcase'))`); await ready(); await pause(200);
+  const afterLive = forbidden.length;
+  await pause(500);
+  check('returning to introduction stops accelerated live polling', forbidden.length === afterLive);
+
+  seedUnavailable = true;
+  await cdp('Page.navigate', { url: origin + '/showcase' });
+  for (let i = 0; i < 60; i++) { if (await evaluate(`Boolean(${host}?._error)`)) break; await pause(100); }
+  check('failed sample fetch shows error instead of live fallback', await evaluate(`${host}._error.includes('503')`));
+  seedUnavailable = false;
+  await evaluate(`${shadow}.querySelector('.loading button').click()`); await ready();
+  check('retry recovers the local sample', await evaluate(`${host}._model.total===334`));
+  check('no uncaught browser exceptions', errors.length === 0, errors);
+  await writeFile(path.join(run, 'acceptance.json'), JSON.stringify({ ok: true, checks, observations, requests, interceptedExternalRequests: forbidden, errors, productionRequestsSent: 0, deployed: false }, null, 2));
+  console.log(JSON.stringify({ ok: true, checks: checks.length, output: run, productionRequestsSent: 0 }));
+} catch (error) {
+  await writeFile(path.join(run, 'acceptance.json'), JSON.stringify({ ok: false, error: error.stack, checks, observations, requests, interceptedExternalRequests: forbidden, errors, deployed: false }, null, 2));
+  console.error(error);
+  console.error(`Evidence: ${run}`);
+  process.exitCode = 1;
+} finally {
+  socket?.close(); browser.kill(); server.closeAllConnections(); server.close();
+}

BIN
src/assets/showcase/classroom.png


BIN
src/assets/showcase/collaboration.png


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
src/assets/showcase/demo-extension.json


BIN
src/assets/showcase/leadership.png


+ 6035 - 0
src/assets/showcase/preview-seed.json

@@ -0,0 +1,6035 @@
+{
+  "version": "ksf.innovation-preview-seed.v1",
+  "description": "仅为关联种子;大规模扩展目标另列,不是完整12000条用户声音。",
+  "scaleTargets": {
+    "members": 36,
+    "groups": 6,
+    "ideas": 72,
+    "researchTasks": 180,
+    "syntheticVoices": 12000,
+    "productObservations": 720,
+    "priceSnapshots": 2880,
+    "evidence": 360,
+    "opportunities": 24,
+    "projects": 12,
+    "validationTasks": 30,
+    "conceptVersions": 8,
+    "reports": 44,
+    "questions": 72,
+    "collaborationRecords": 312
+  },
+  "groups": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-G1",
+      "name": "G1 上班族工作餐与加班餐",
+      "scene": "上班族工作餐与加班餐",
+      "memberIds": [
+        "DEMO-M01",
+        "DEMO-M02",
+        "DEMO-M03",
+        "DEMO-M04",
+        "DEMO-M05",
+        "DEMO-M06"
+      ],
+      "leaderMemberId": "DEMO-M01"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-G2",
+      "name": "G2 学生宿舍与夜宵",
+      "scene": "学生宿舍与夜宵",
+      "memberIds": [
+        "DEMO-M07",
+        "DEMO-M08",
+        "DEMO-M09",
+        "DEMO-M10",
+        "DEMO-M11",
+        "DEMO-M12"
+      ],
+      "leaderMemberId": "DEMO-M07"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-G3",
+      "name": "G3 户外露营与出行",
+      "scene": "户外露营与出行",
+      "memberIds": [
+        "DEMO-M13",
+        "DEMO-M14",
+        "DEMO-M15",
+        "DEMO-M16",
+        "DEMO-M17",
+        "DEMO-M18"
+      ],
+      "leaderMemberId": "DEMO-M13"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-G4",
+      "name": "G4 健康控卡人群",
+      "scene": "健康控卡人群",
+      "memberIds": [
+        "DEMO-M19",
+        "DEMO-M20",
+        "DEMO-M21",
+        "DEMO-M22",
+        "DEMO-M23",
+        "DEMO-M24"
+      ],
+      "leaderMemberId": "DEMO-M19"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-G5",
+      "name": "G5 一人食与家庭囤货",
+      "scene": "一人食与家庭囤货",
+      "memberIds": [
+        "DEMO-M25",
+        "DEMO-M26",
+        "DEMO-M27",
+        "DEMO-M28",
+        "DEMO-M29",
+        "DEMO-M30"
+      ],
+      "leaderMemberId": "DEMO-M25"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-G6",
+      "name": "G6 地域风味与高端尝鲜",
+      "scene": "地域风味与高端尝鲜",
+      "memberIds": [
+        "DEMO-M31",
+        "DEMO-M32",
+        "DEMO-M33",
+        "DEMO-M34",
+        "DEMO-M35",
+        "DEMO-M36"
+      ],
+      "leaderMemberId": "DEMO-M31"
+    }
+  ],
+  "members": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M01",
+      "displayName": "演示学员01",
+      "role": "产品经理",
+      "groupId": "DEMO-G1",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M02",
+      "displayName": "演示学员02",
+      "role": "用户研究",
+      "groupId": "DEMO-G1",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M03",
+      "displayName": "演示学员03",
+      "role": "风味研发",
+      "groupId": "DEMO-G1",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M04",
+      "displayName": "演示学员04",
+      "role": "面体工艺",
+      "groupId": "DEMO-G1",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M05",
+      "displayName": "演示学员05",
+      "role": "供应链",
+      "groupId": "DEMO-G1",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M06",
+      "displayName": "演示学员06",
+      "role": "包装与渠道",
+      "groupId": "DEMO-G1",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M07",
+      "displayName": "演示学员07",
+      "role": "产品经理",
+      "groupId": "DEMO-G2",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M08",
+      "displayName": "演示学员08",
+      "role": "用户研究",
+      "groupId": "DEMO-G2",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M09",
+      "displayName": "演示学员09",
+      "role": "风味研发",
+      "groupId": "DEMO-G2",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M10",
+      "displayName": "演示学员10",
+      "role": "面体工艺",
+      "groupId": "DEMO-G2",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M11",
+      "displayName": "演示学员11",
+      "role": "供应链",
+      "groupId": "DEMO-G2",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M12",
+      "displayName": "演示学员12",
+      "role": "包装与渠道",
+      "groupId": "DEMO-G2",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M13",
+      "displayName": "演示学员13",
+      "role": "产品经理",
+      "groupId": "DEMO-G3",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M14",
+      "displayName": "演示学员14",
+      "role": "用户研究",
+      "groupId": "DEMO-G3",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M15",
+      "displayName": "演示学员15",
+      "role": "风味研发",
+      "groupId": "DEMO-G3",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M16",
+      "displayName": "演示学员16",
+      "role": "面体工艺",
+      "groupId": "DEMO-G3",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M17",
+      "displayName": "演示学员17",
+      "role": "供应链",
+      "groupId": "DEMO-G3",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M18",
+      "displayName": "演示学员18",
+      "role": "包装与渠道",
+      "groupId": "DEMO-G3",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M19",
+      "displayName": "演示学员19",
+      "role": "产品经理",
+      "groupId": "DEMO-G4",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M20",
+      "displayName": "演示学员20",
+      "role": "用户研究",
+      "groupId": "DEMO-G4",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M21",
+      "displayName": "演示学员21",
+      "role": "风味研发",
+      "groupId": "DEMO-G4",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M22",
+      "displayName": "演示学员22",
+      "role": "面体工艺",
+      "groupId": "DEMO-G4",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M23",
+      "displayName": "演示学员23",
+      "role": "供应链",
+      "groupId": "DEMO-G4",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M24",
+      "displayName": "演示学员24",
+      "role": "包装与渠道",
+      "groupId": "DEMO-G4",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M25",
+      "displayName": "演示学员25",
+      "role": "产品经理",
+      "groupId": "DEMO-G5",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M26",
+      "displayName": "演示学员26",
+      "role": "用户研究",
+      "groupId": "DEMO-G5",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M27",
+      "displayName": "演示学员27",
+      "role": "风味研发",
+      "groupId": "DEMO-G5",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M28",
+      "displayName": "演示学员28",
+      "role": "面体工艺",
+      "groupId": "DEMO-G5",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M29",
+      "displayName": "演示学员29",
+      "role": "供应链",
+      "groupId": "DEMO-G5",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M30",
+      "displayName": "演示学员30",
+      "role": "包装与渠道",
+      "groupId": "DEMO-G5",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M31",
+      "displayName": "演示学员31",
+      "role": "产品经理",
+      "groupId": "DEMO-G6",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M32",
+      "displayName": "演示学员32",
+      "role": "用户研究",
+      "groupId": "DEMO-G6",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M33",
+      "displayName": "演示学员33",
+      "role": "风味研发",
+      "groupId": "DEMO-G6",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M34",
+      "displayName": "演示学员34",
+      "role": "面体工艺",
+      "groupId": "DEMO-G6",
+      "aiFamiliarity": "新手",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M35",
+      "displayName": "演示学员35",
+      "role": "供应链",
+      "groupId": "DEMO-G6",
+      "aiFamiliarity": "日常使用",
+      "phone": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-M36",
+      "displayName": "演示学员36",
+      "role": "包装与渠道",
+      "groupId": "DEMO-G6",
+      "aiFamiliarity": "熟练",
+      "phone": null
+    }
+  ],
+  "ideas": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-01",
+      "groupId": "DEMO-G1",
+      "title": "工位轻香葱油拌面 · 主方向",
+      "ownerMemberId": "DEMO-M01",
+      "intendedProjectId": "DEMO-P01",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-02",
+      "groupId": "DEMO-G1",
+      "title": "晚归番茄鸡蛋汤面 · 相邻风味",
+      "ownerMemberId": "DEMO-M02",
+      "intendedProjectId": "DEMO-P02",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-03",
+      "groupId": "DEMO-G1",
+      "title": "工位轻香葱油拌面 · 小份选择",
+      "ownerMemberId": "DEMO-M03",
+      "intendedProjectId": "DEMO-P01",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-04",
+      "groupId": "DEMO-G1",
+      "title": "晚归番茄鸡蛋汤面 · 辣度可调",
+      "ownerMemberId": "DEMO-M04",
+      "intendedProjectId": "DEMO-P02",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-05",
+      "groupId": "DEMO-G1",
+      "title": "工位轻香葱油拌面 · 包装提示",
+      "ownerMemberId": "DEMO-M05",
+      "intendedProjectId": "DEMO-P01",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-06",
+      "groupId": "DEMO-G1",
+      "title": "晚归番茄鸡蛋汤面 · 便携制备",
+      "ownerMemberId": "DEMO-M06",
+      "intendedProjectId": "DEMO-P02",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-07",
+      "groupId": "DEMO-G1",
+      "title": "工位轻香葱油拌面 · 分酱设计",
+      "ownerMemberId": "DEMO-M01",
+      "intendedProjectId": "DEMO-P01",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-08",
+      "groupId": "DEMO-G1",
+      "title": "晚归番茄鸡蛋汤面 · 冷热条件",
+      "ownerMemberId": "DEMO-M02",
+      "intendedProjectId": "DEMO-P02",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-09",
+      "groupId": "DEMO-G1",
+      "title": "工位轻香葱油拌面 · 口感迭代",
+      "ownerMemberId": "DEMO-M03",
+      "intendedProjectId": "DEMO-P01",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-10",
+      "groupId": "DEMO-G1",
+      "title": "晚归番茄鸡蛋汤面 · 囤货组合",
+      "ownerMemberId": "DEMO-M04",
+      "intendedProjectId": "DEMO-P02",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-11",
+      "groupId": "DEMO-G1",
+      "title": "工位轻香葱油拌面 · 反向场景",
+      "ownerMemberId": "DEMO-M05",
+      "intendedProjectId": "DEMO-P01",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I1-12",
+      "groupId": "DEMO-G1",
+      "title": "晚归番茄鸡蛋汤面 · 待验证备选",
+      "ownerMemberId": "DEMO-M06",
+      "intendedProjectId": "DEMO-P02",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-01",
+      "groupId": "DEMO-G2",
+      "title": "宿舍轻辣番茄拌面 · 主方向",
+      "ownerMemberId": "DEMO-M07",
+      "intendedProjectId": "DEMO-P03",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-02",
+      "groupId": "DEMO-G2",
+      "title": "夜宵芝士轻辣拌面 · 相邻风味",
+      "ownerMemberId": "DEMO-M08",
+      "intendedProjectId": "DEMO-P04",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-03",
+      "groupId": "DEMO-G2",
+      "title": "宿舍轻辣番茄拌面 · 小份选择",
+      "ownerMemberId": "DEMO-M09",
+      "intendedProjectId": "DEMO-P03",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-04",
+      "groupId": "DEMO-G2",
+      "title": "夜宵芝士轻辣拌面 · 辣度可调",
+      "ownerMemberId": "DEMO-M10",
+      "intendedProjectId": "DEMO-P04",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-05",
+      "groupId": "DEMO-G2",
+      "title": "宿舍轻辣番茄拌面 · 包装提示",
+      "ownerMemberId": "DEMO-M11",
+      "intendedProjectId": "DEMO-P03",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-06",
+      "groupId": "DEMO-G2",
+      "title": "夜宵芝士轻辣拌面 · 便携制备",
+      "ownerMemberId": "DEMO-M12",
+      "intendedProjectId": "DEMO-P04",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-07",
+      "groupId": "DEMO-G2",
+      "title": "宿舍轻辣番茄拌面 · 分酱设计",
+      "ownerMemberId": "DEMO-M07",
+      "intendedProjectId": "DEMO-P03",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-08",
+      "groupId": "DEMO-G2",
+      "title": "夜宵芝士轻辣拌面 · 冷热条件",
+      "ownerMemberId": "DEMO-M08",
+      "intendedProjectId": "DEMO-P04",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-09",
+      "groupId": "DEMO-G2",
+      "title": "宿舍轻辣番茄拌面 · 口感迭代",
+      "ownerMemberId": "DEMO-M09",
+      "intendedProjectId": "DEMO-P03",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-10",
+      "groupId": "DEMO-G2",
+      "title": "夜宵芝士轻辣拌面 · 囤货组合",
+      "ownerMemberId": "DEMO-M10",
+      "intendedProjectId": "DEMO-P04",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-11",
+      "groupId": "DEMO-G2",
+      "title": "宿舍轻辣番茄拌面 · 反向场景",
+      "ownerMemberId": "DEMO-M11",
+      "intendedProjectId": "DEMO-P03",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I2-12",
+      "groupId": "DEMO-G2",
+      "title": "夜宵芝士轻辣拌面 · 待验证备选",
+      "ownerMemberId": "DEMO-M12",
+      "intendedProjectId": "DEMO-P04",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-01",
+      "groupId": "DEMO-G3",
+      "title": "露营菌菇热汤面 · 主方向",
+      "ownerMemberId": "DEMO-M13",
+      "intendedProjectId": "DEMO-P05",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-02",
+      "groupId": "DEMO-G3",
+      "title": "高铁低气味葱香拌面 · 相邻风味",
+      "ownerMemberId": "DEMO-M14",
+      "intendedProjectId": "DEMO-P06",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-03",
+      "groupId": "DEMO-G3",
+      "title": "露营菌菇热汤面 · 小份选择",
+      "ownerMemberId": "DEMO-M15",
+      "intendedProjectId": "DEMO-P05",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-04",
+      "groupId": "DEMO-G3",
+      "title": "高铁低气味葱香拌面 · 辣度可调",
+      "ownerMemberId": "DEMO-M16",
+      "intendedProjectId": "DEMO-P06",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-05",
+      "groupId": "DEMO-G3",
+      "title": "露营菌菇热汤面 · 包装提示",
+      "ownerMemberId": "DEMO-M17",
+      "intendedProjectId": "DEMO-P05",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-06",
+      "groupId": "DEMO-G3",
+      "title": "高铁低气味葱香拌面 · 便携制备",
+      "ownerMemberId": "DEMO-M18",
+      "intendedProjectId": "DEMO-P06",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-07",
+      "groupId": "DEMO-G3",
+      "title": "露营菌菇热汤面 · 分酱设计",
+      "ownerMemberId": "DEMO-M13",
+      "intendedProjectId": "DEMO-P05",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-08",
+      "groupId": "DEMO-G3",
+      "title": "高铁低气味葱香拌面 · 冷热条件",
+      "ownerMemberId": "DEMO-M14",
+      "intendedProjectId": "DEMO-P06",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-09",
+      "groupId": "DEMO-G3",
+      "title": "露营菌菇热汤面 · 口感迭代",
+      "ownerMemberId": "DEMO-M15",
+      "intendedProjectId": "DEMO-P05",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-10",
+      "groupId": "DEMO-G3",
+      "title": "高铁低气味葱香拌面 · 囤货组合",
+      "ownerMemberId": "DEMO-M16",
+      "intendedProjectId": "DEMO-P06",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-11",
+      "groupId": "DEMO-G3",
+      "title": "露营菌菇热汤面 · 反向场景",
+      "ownerMemberId": "DEMO-M17",
+      "intendedProjectId": "DEMO-P05",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I3-12",
+      "groupId": "DEMO-G3",
+      "title": "高铁低气味葱香拌面 · 待验证备选",
+      "ownerMemberId": "DEMO-M18",
+      "intendedProjectId": "DEMO-P06",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-01",
+      "groupId": "DEMO-G4",
+      "title": "高纤荞麦拌面 · 主方向",
+      "ownerMemberId": "DEMO-M19",
+      "intendedProjectId": "DEMO-P07",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-02",
+      "groupId": "DEMO-G4",
+      "title": "高蛋白番茄拌面 · 相邻风味",
+      "ownerMemberId": "DEMO-M20",
+      "intendedProjectId": "DEMO-P08",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-03",
+      "groupId": "DEMO-G4",
+      "title": "高纤荞麦拌面 · 小份选择",
+      "ownerMemberId": "DEMO-M21",
+      "intendedProjectId": "DEMO-P07",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-04",
+      "groupId": "DEMO-G4",
+      "title": "高蛋白番茄拌面 · 辣度可调",
+      "ownerMemberId": "DEMO-M22",
+      "intendedProjectId": "DEMO-P08",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-05",
+      "groupId": "DEMO-G4",
+      "title": "高纤荞麦拌面 · 包装提示",
+      "ownerMemberId": "DEMO-M23",
+      "intendedProjectId": "DEMO-P07",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-06",
+      "groupId": "DEMO-G4",
+      "title": "高蛋白番茄拌面 · 便携制备",
+      "ownerMemberId": "DEMO-M24",
+      "intendedProjectId": "DEMO-P08",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-07",
+      "groupId": "DEMO-G4",
+      "title": "高纤荞麦拌面 · 分酱设计",
+      "ownerMemberId": "DEMO-M19",
+      "intendedProjectId": "DEMO-P07",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-08",
+      "groupId": "DEMO-G4",
+      "title": "高蛋白番茄拌面 · 冷热条件",
+      "ownerMemberId": "DEMO-M20",
+      "intendedProjectId": "DEMO-P08",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-09",
+      "groupId": "DEMO-G4",
+      "title": "高纤荞麦拌面 · 口感迭代",
+      "ownerMemberId": "DEMO-M21",
+      "intendedProjectId": "DEMO-P07",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-10",
+      "groupId": "DEMO-G4",
+      "title": "高蛋白番茄拌面 · 囤货组合",
+      "ownerMemberId": "DEMO-M22",
+      "intendedProjectId": "DEMO-P08",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-11",
+      "groupId": "DEMO-G4",
+      "title": "高纤荞麦拌面 · 反向场景",
+      "ownerMemberId": "DEMO-M23",
+      "intendedProjectId": "DEMO-P07",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I4-12",
+      "groupId": "DEMO-G4",
+      "title": "高蛋白番茄拌面 · 待验证备选",
+      "ownerMemberId": "DEMO-M24",
+      "intendedProjectId": "DEMO-P08",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-01",
+      "groupId": "DEMO-G5",
+      "title": "家庭双人小份组合 · 主方向",
+      "ownerMemberId": "DEMO-M25",
+      "intendedProjectId": "DEMO-P09",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-02",
+      "groupId": "DEMO-G5",
+      "title": "一人食酱香大满足 · 相邻风味",
+      "ownerMemberId": "DEMO-M26",
+      "intendedProjectId": "DEMO-P10",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-03",
+      "groupId": "DEMO-G5",
+      "title": "家庭双人小份组合 · 小份选择",
+      "ownerMemberId": "DEMO-M27",
+      "intendedProjectId": "DEMO-P09",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-04",
+      "groupId": "DEMO-G5",
+      "title": "一人食酱香大满足 · 辣度可调",
+      "ownerMemberId": "DEMO-M28",
+      "intendedProjectId": "DEMO-P10",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-05",
+      "groupId": "DEMO-G5",
+      "title": "家庭双人小份组合 · 包装提示",
+      "ownerMemberId": "DEMO-M29",
+      "intendedProjectId": "DEMO-P09",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-06",
+      "groupId": "DEMO-G5",
+      "title": "一人食酱香大满足 · 便携制备",
+      "ownerMemberId": "DEMO-M30",
+      "intendedProjectId": "DEMO-P10",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-07",
+      "groupId": "DEMO-G5",
+      "title": "家庭双人小份组合 · 分酱设计",
+      "ownerMemberId": "DEMO-M25",
+      "intendedProjectId": "DEMO-P09",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-08",
+      "groupId": "DEMO-G5",
+      "title": "一人食酱香大满足 · 冷热条件",
+      "ownerMemberId": "DEMO-M26",
+      "intendedProjectId": "DEMO-P10",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-09",
+      "groupId": "DEMO-G5",
+      "title": "家庭双人小份组合 · 口感迭代",
+      "ownerMemberId": "DEMO-M27",
+      "intendedProjectId": "DEMO-P09",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-10",
+      "groupId": "DEMO-G5",
+      "title": "一人食酱香大满足 · 囤货组合",
+      "ownerMemberId": "DEMO-M28",
+      "intendedProjectId": "DEMO-P10",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-11",
+      "groupId": "DEMO-G5",
+      "title": "家庭双人小份组合 · 反向场景",
+      "ownerMemberId": "DEMO-M29",
+      "intendedProjectId": "DEMO-P09",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I5-12",
+      "groupId": "DEMO-G5",
+      "title": "一人食酱香大满足 · 待验证备选",
+      "ownerMemberId": "DEMO-M30",
+      "intendedProjectId": "DEMO-P10",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-01",
+      "groupId": "DEMO-G6",
+      "title": "贵州红酸汤风味面 · 主方向",
+      "ownerMemberId": "DEMO-M31",
+      "intendedProjectId": "DEMO-P11",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-02",
+      "groupId": "DEMO-G6",
+      "title": "川香椒麻宽面 · 相邻风味",
+      "ownerMemberId": "DEMO-M32",
+      "intendedProjectId": "DEMO-P12",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-03",
+      "groupId": "DEMO-G6",
+      "title": "贵州红酸汤风味面 · 小份选择",
+      "ownerMemberId": "DEMO-M33",
+      "intendedProjectId": "DEMO-P11",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-04",
+      "groupId": "DEMO-G6",
+      "title": "川香椒麻宽面 · 辣度可调",
+      "ownerMemberId": "DEMO-M34",
+      "intendedProjectId": "DEMO-P12",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-05",
+      "groupId": "DEMO-G6",
+      "title": "贵州红酸汤风味面 · 包装提示",
+      "ownerMemberId": "DEMO-M35",
+      "intendedProjectId": "DEMO-P11",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-06",
+      "groupId": "DEMO-G6",
+      "title": "川香椒麻宽面 · 便携制备",
+      "ownerMemberId": "DEMO-M36",
+      "intendedProjectId": "DEMO-P12",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-07",
+      "groupId": "DEMO-G6",
+      "title": "贵州红酸汤风味面 · 分酱设计",
+      "ownerMemberId": "DEMO-M31",
+      "intendedProjectId": "DEMO-P11",
+      "basis": "模拟创意,待验证",
+      "state": "proposed"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-08",
+      "groupId": "DEMO-G6",
+      "title": "川香椒麻宽面 · 冷热条件",
+      "ownerMemberId": "DEMO-M32",
+      "intendedProjectId": "DEMO-P12",
+      "basis": "模拟创意,待验证",
+      "state": "researching"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-09",
+      "groupId": "DEMO-G6",
+      "title": "贵州红酸汤风味面 · 口感迭代",
+      "ownerMemberId": "DEMO-M33",
+      "intendedProjectId": "DEMO-P11",
+      "basis": "模拟创意,待验证",
+      "state": "selected"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-10",
+      "groupId": "DEMO-G6",
+      "title": "川香椒麻宽面 · 囤货组合",
+      "ownerMemberId": "DEMO-M34",
+      "intendedProjectId": "DEMO-P12",
+      "basis": "模拟创意,待验证",
+      "state": "merged"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-11",
+      "groupId": "DEMO-G6",
+      "title": "贵州红酸汤风味面 · 反向场景",
+      "ownerMemberId": "DEMO-M35",
+      "intendedProjectId": "DEMO-P11",
+      "basis": "模拟创意,待验证",
+      "state": "parked"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-I6-12",
+      "groupId": "DEMO-G6",
+      "title": "川香椒麻宽面 · 待验证备选",
+      "ownerMemberId": "DEMO-M36",
+      "intendedProjectId": "DEMO-P12",
+      "basis": "模拟创意,待验证",
+      "state": "rejected"
+    }
+  ],
+  "sources": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-S01",
+      "groupId": "DEMO-G1",
+      "title": "模拟用户声音:上班族工作餐与加班餐",
+      "rawText": "中午只有热水,排队等微波炉太久。我会先看要不要煮,酱味太冲就留到下班再吃。",
+      "sourceUrl": null,
+      "platform": "模拟渠道",
+      "authorLabel": "模拟受访者01",
+      "verificationState": "synthetic_not_real_interview"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-S02",
+      "groupId": "DEMO-G2",
+      "title": "模拟用户声音:学生宿舍与夜宵",
+      "rawText": "想吃一点辣,但别辣到喝好多水。宿舍不能用锅,写清楚能不能泡比送几个料包更重要。",
+      "sourceUrl": null,
+      "platform": "模拟渠道",
+      "authorLabel": "模拟受访者02",
+      "verificationState": "synthetic_not_real_interview"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-S03",
+      "groupId": "DEMO-G3",
+      "title": "模拟用户声音:户外露营与出行",
+      "rawText": "露营时面是好带,可用水和收垃圾更麻烦。有炉子和没炉子,我选的东西完全不一样。",
+      "sourceUrl": null,
+      "platform": "模拟渠道",
+      "authorLabel": "模拟受访者03",
+      "verificationState": "synthetic_not_real_interview"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-S04",
+      "groupId": "DEMO-G4",
+      "title": "模拟用户声音:健康控卡人群",
+      "rawText": "我会看每份热量和配料,但不想只吃荞麦味。加鸡蛋是我平时的习惯,不一定是面太少。",
+      "sourceUrl": null,
+      "platform": "模拟渠道",
+      "authorLabel": "模拟受访者04",
+      "verificationState": "synthetic_not_real_interview"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-S05",
+      "groupId": "DEMO-G5",
+      "title": "模拟用户声音:一人食与家庭囤货",
+      "rawText": "两个人口味不一样,分开酱包会方便。整包便宜不代表每份便宜,我会看一包到底算几顿。",
+      "sourceUrl": null,
+      "platform": "模拟渠道",
+      "authorLabel": "模拟受访者05",
+      "verificationState": "synthetic_not_real_interview"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-S06",
+      "groupId": "DEMO-G6",
+      "title": "模拟用户声音:地域风味与高端尝鲜",
+      "rawText": "红酸汤第一次觉得新鲜,吃第二次更在意酸味和面条搭不搭,不是名字越地方就越想买。",
+      "sourceUrl": null,
+      "platform": "模拟渠道",
+      "authorLabel": "模拟受访者06",
+      "verificationState": "synthetic_not_real_interview"
+    }
+  ],
+  "evidence": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-E01",
+      "sourceId": "DEMO-S01",
+      "quote": "中午只有热水,排队等微波炉太久。我会先看要不要煮,酱味太冲就留到下班再吃。",
+      "locator": {
+        "start": 0,
+        "end": 37
+      },
+      "reviewState": "demo_only",
+      "claim": "供演示用的情境线索,不能推断人群普遍需求"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-E02",
+      "sourceId": "DEMO-S02",
+      "quote": "想吃一点辣,但别辣到喝好多水。宿舍不能用锅,写清楚能不能泡比送几个料包更重要。",
+      "locator": {
+        "start": 0,
+        "end": 39
+      },
+      "reviewState": "demo_only",
+      "claim": "供演示用的情境线索,不能推断人群普遍需求"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-E03",
+      "sourceId": "DEMO-S03",
+      "quote": "露营时面是好带,可用水和收垃圾更麻烦。有炉子和没炉子,我选的东西完全不一样。",
+      "locator": {
+        "start": 0,
+        "end": 38
+      },
+      "reviewState": "demo_only",
+      "claim": "供演示用的情境线索,不能推断人群普遍需求"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-E04",
+      "sourceId": "DEMO-S04",
+      "quote": "我会看每份热量和配料,但不想只吃荞麦味。加鸡蛋是我平时的习惯,不一定是面太少。",
+      "locator": {
+        "start": 0,
+        "end": 39
+      },
+      "reviewState": "demo_only",
+      "claim": "供演示用的情境线索,不能推断人群普遍需求"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-E05",
+      "sourceId": "DEMO-S05",
+      "quote": "两个人口味不一样,分开酱包会方便。整包便宜不代表每份便宜,我会看一包到底算几顿。",
+      "locator": {
+        "start": 0,
+        "end": 40
+      },
+      "reviewState": "demo_only",
+      "claim": "供演示用的情境线索,不能推断人群普遍需求"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-E06",
+      "sourceId": "DEMO-S06",
+      "quote": "红酸汤第一次觉得新鲜,吃第二次更在意酸味和面条搭不搭,不是名字越地方就越想买。",
+      "locator": {
+        "start": 0,
+        "end": 39
+      },
+      "reviewState": "demo_only",
+      "claim": "供演示用的情境线索,不能推断人群普遍需求"
+    }
+  ],
+  "opportunities": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O01",
+      "title": "工位轻香葱油拌面",
+      "groupId": "DEMO-G1",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P01",
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "usageScenario": "工位午餐,只有热水",
+      "missingConditions": "低气味与酱香可能冲突,制备和沥水条件待核",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O02",
+      "title": "晚归番茄鸡蛋汤面",
+      "groupId": "DEMO-G1",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P02",
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "usageScenario": "晚归加班餐,希望热乎但不油腻",
+      "missingConditions": "泡发口感与加料习惯混杂",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O03",
+      "title": "宿舍轻辣番茄拌面",
+      "groupId": "DEMO-G2",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P03",
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "usageScenario": "无灶具宿舍,预算敏感",
+      "missingConditions": "无灶具目标与面体需煮条件冲突",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O04",
+      "title": "夜宵芝士轻辣拌面",
+      "groupId": "DEMO-G2",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P04",
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "usageScenario": "同寝分享夜宵,希望辣度可调",
+      "missingConditions": "芝士浓郁与轻负担诉求冲突",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O05",
+      "title": "露营菌菇热汤面",
+      "groupId": "DEMO-G3",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P05",
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "usageScenario": "有限炊具的户外露营",
+      "missingConditions": "热源、用水及垃圾回收条件不统一",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O06",
+      "title": "高铁低气味葱香拌面",
+      "groupId": "DEMO-G3",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P06",
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "usageScenario": "出差高铁,只有开水",
+      "missingConditions": "弃水、气味与携带便利相互牵制",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O07",
+      "title": "高纤荞麦拌面",
+      "groupId": "DEMO-G4",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P07",
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "usageScenario": "午间控卡,仍希望保留面食口感",
+      "missingConditions": "营养、口感、配方和成本均需证据",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O08",
+      "title": "高蛋白番茄拌面",
+      "groupId": "DEMO-G4",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P08",
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "usageScenario": "运动后便餐,关注配料及饱腹体验",
+      "missingConditions": "蛋白来源与检测口径未知",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O09",
+      "title": "家庭双人小份组合",
+      "groupId": "DEMO-G5",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P09",
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "usageScenario": "两人晚餐,偏好分装、可自行搭菜",
+      "missingConditions": "总价与单份价容易混淆,真实用餐量待核",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O10",
+      "title": "一人食酱香大满足",
+      "groupId": "DEMO-G5",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P10",
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "usageScenario": "一人晚餐,想减少临时补主食",
+      "missingConditions": "正餐、加餐和习惯搭配尚未区分",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O11",
+      "title": "贵州红酸汤风味面",
+      "groupId": "DEMO-G6",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P11",
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "usageScenario": "地域尝鲜,偏好酸鲜但怕踩雷",
+      "missingConditions": "风味分歧与正宗表达依据待核",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O12",
+      "title": "川香椒麻宽面",
+      "groupId": "DEMO-G6",
+      "claimKind": "hypothesis",
+      "status": "approved",
+      "linkedProjectId": "DEMO-P12",
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "usageScenario": "嗜辣尝鲜,希望可控椒麻层次",
+      "missingConditions": "宽面复水、麻辣分离和酱料挂壁待核",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O13",
+      "title": "工位清爽酸汤面",
+      "groupId": "DEMO-G1",
+      "claimKind": "hypothesis",
+      "status": "candidate",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "missingConditions": "待补人群与使用条件",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O14",
+      "title": "办公室重蒜香拌面",
+      "groupId": "DEMO-G1",
+      "claimKind": "hypothesis",
+      "status": "rejected",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "missingConditions": "强气味与目标使用情境存在冲突",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O15",
+      "title": "夜宵小份菌菇面",
+      "groupId": "DEMO-G2",
+      "claimKind": "hypothesis",
+      "status": "candidate",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "missingConditions": "待补人群与使用条件",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O16",
+      "title": "宿舍现煮宽面",
+      "groupId": "DEMO-G2",
+      "claimKind": "hypothesis",
+      "status": "rejected",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "missingConditions": "无灶具条件与制备路线不符",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O17",
+      "title": "轻量干拌露营面",
+      "groupId": "DEMO-G3",
+      "claimKind": "hypothesis",
+      "status": "candidate",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "missingConditions": "待补人群与使用条件",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O18",
+      "title": "高铁现煮麻辣火锅面",
+      "groupId": "DEMO-G3",
+      "claimKind": "hypothesis",
+      "status": "rejected",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "missingConditions": "热源与乘车情境条件不符",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O19",
+      "title": "番茄全麦汤面",
+      "groupId": "DEMO-G4",
+      "claimKind": "hypothesis",
+      "status": "candidate",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "missingConditions": "待补人群与使用条件",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O20",
+      "title": "无依据零负担健康面",
+      "groupId": "DEMO-G4",
+      "claimKind": "hypothesis",
+      "status": "rejected",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "missingConditions": "健康表达没有配方与检测依据",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O21",
+      "title": "分酱家庭囤货装",
+      "groupId": "DEMO-G5",
+      "claimKind": "hypothesis",
+      "status": "candidate",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "missingConditions": "待补人群与使用条件",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O22",
+      "title": "超大单包装一人份",
+      "groupId": "DEMO-G5",
+      "claimKind": "hypothesis",
+      "status": "rejected",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "missingConditions": "开封保存与目标使用量不清",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O23",
+      "title": "云南菌菇风味面",
+      "groupId": "DEMO-G6",
+      "claimKind": "hypothesis",
+      "status": "candidate",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "missingConditions": "待补人群与使用条件",
+      "version": 1
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-O24",
+      "title": "没有依据的正宗产地承诺",
+      "groupId": "DEMO-G6",
+      "claimKind": "hypothesis",
+      "status": "rejected",
+      "linkedProjectId": null,
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "missingConditions": "产地与风味表达依据缺失",
+      "version": 1
+    }
+  ],
+  "projects": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P01",
+      "title": "工位轻香葱油拌面",
+      "groupId": "DEMO-G1",
+      "stageCode": "S5",
+      "progressState": "completed",
+      "ownerMemberId": "DEMO-M01",
+      "memberIds": [
+        "DEMO-M01",
+        "DEMO-M02",
+        "DEMO-M03",
+        "DEMO-M04",
+        "DEMO-M05",
+        "DEMO-M06"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M04"
+      ],
+      "opportunityId": "DEMO-O01",
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "usageScenario": "工位午餐,只有热水",
+      "targetPrice": {
+        "amount": 6.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "低气味与酱香可能冲突,制备和沥水条件待核",
+      "conceptIds": [
+        "DEMO-C01-V1",
+        "DEMO-C01-V2"
+      ],
+      "currentConceptId": "DEMO-C01-V2",
+      "questionIds": [
+        "DEMO-Q01-1",
+        "DEMO-Q01-2",
+        "DEMO-Q01-3",
+        "DEMO-Q01-4",
+        "DEMO-Q01-5",
+        "DEMO-Q01-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P02",
+      "title": "晚归番茄鸡蛋汤面",
+      "groupId": "DEMO-G1",
+      "stageCode": "S4",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M02",
+      "memberIds": [
+        "DEMO-M01",
+        "DEMO-M02",
+        "DEMO-M03",
+        "DEMO-M04",
+        "DEMO-M05",
+        "DEMO-M06"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M07"
+      ],
+      "opportunityId": "DEMO-O02",
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "usageScenario": "晚归加班餐,希望热乎但不油腻",
+      "targetPrice": {
+        "amount": 8.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "泡发口感与加料习惯混杂",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q02-1",
+        "DEMO-Q02-2",
+        "DEMO-Q02-3",
+        "DEMO-Q02-4",
+        "DEMO-Q02-5",
+        "DEMO-Q02-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P03",
+      "title": "宿舍轻辣番茄拌面",
+      "groupId": "DEMO-G2",
+      "stageCode": "S4",
+      "progressState": "blocked",
+      "ownerMemberId": "DEMO-M07",
+      "memberIds": [
+        "DEMO-M07",
+        "DEMO-M08",
+        "DEMO-M09",
+        "DEMO-M10",
+        "DEMO-M11",
+        "DEMO-M12"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M10"
+      ],
+      "opportunityId": "DEMO-O03",
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "usageScenario": "无灶具宿舍,预算敏感",
+      "targetPrice": {
+        "amount": 4.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "无灶具目标与面体需煮条件冲突",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q03-1",
+        "DEMO-Q03-2",
+        "DEMO-Q03-3",
+        "DEMO-Q03-4",
+        "DEMO-Q03-5",
+        "DEMO-Q03-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P04",
+      "title": "夜宵芝士轻辣拌面",
+      "groupId": "DEMO-G2",
+      "stageCode": "S5",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M08",
+      "memberIds": [
+        "DEMO-M07",
+        "DEMO-M08",
+        "DEMO-M09",
+        "DEMO-M10",
+        "DEMO-M11",
+        "DEMO-M12"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M13"
+      ],
+      "opportunityId": "DEMO-O04",
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "usageScenario": "同寝分享夜宵,希望辣度可调",
+      "targetPrice": {
+        "amount": 5.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "芝士浓郁与轻负担诉求冲突",
+      "conceptIds": [
+        "DEMO-C04-V1",
+        "DEMO-C04-V2"
+      ],
+      "currentConceptId": "DEMO-C04-V2",
+      "questionIds": [
+        "DEMO-Q04-1",
+        "DEMO-Q04-2",
+        "DEMO-Q04-3",
+        "DEMO-Q04-4",
+        "DEMO-Q04-5",
+        "DEMO-Q04-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P05",
+      "title": "露营菌菇热汤面",
+      "groupId": "DEMO-G3",
+      "stageCode": "S4",
+      "progressState": "blocked",
+      "ownerMemberId": "DEMO-M13",
+      "memberIds": [
+        "DEMO-M13",
+        "DEMO-M14",
+        "DEMO-M15",
+        "DEMO-M16",
+        "DEMO-M17",
+        "DEMO-M18"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M16"
+      ],
+      "opportunityId": "DEMO-O05",
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "usageScenario": "有限炊具的户外露营",
+      "targetPrice": {
+        "amount": 9.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "热源、用水及垃圾回收条件不统一",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q05-1",
+        "DEMO-Q05-2",
+        "DEMO-Q05-3",
+        "DEMO-Q05-4",
+        "DEMO-Q05-5",
+        "DEMO-Q05-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P06",
+      "title": "高铁低气味葱香拌面",
+      "groupId": "DEMO-G3",
+      "stageCode": "S3",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M14",
+      "memberIds": [
+        "DEMO-M13",
+        "DEMO-M14",
+        "DEMO-M15",
+        "DEMO-M16",
+        "DEMO-M17",
+        "DEMO-M18"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M19"
+      ],
+      "opportunityId": "DEMO-O06",
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "usageScenario": "出差高铁,只有开水",
+      "targetPrice": {
+        "amount": 7.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "弃水、气味与携带便利相互牵制",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q06-1",
+        "DEMO-Q06-2",
+        "DEMO-Q06-3",
+        "DEMO-Q06-4",
+        "DEMO-Q06-5",
+        "DEMO-Q06-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P07",
+      "title": "高纤荞麦拌面",
+      "groupId": "DEMO-G4",
+      "stageCode": "S4",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M19",
+      "memberIds": [
+        "DEMO-M19",
+        "DEMO-M20",
+        "DEMO-M21",
+        "DEMO-M22",
+        "DEMO-M23",
+        "DEMO-M24"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M22"
+      ],
+      "opportunityId": "DEMO-O07",
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "usageScenario": "午间控卡,仍希望保留面食口感",
+      "targetPrice": {
+        "amount": 8.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "营养、口感、配方和成本均需证据",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q07-1",
+        "DEMO-Q07-2",
+        "DEMO-Q07-3",
+        "DEMO-Q07-4",
+        "DEMO-Q07-5",
+        "DEMO-Q07-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P08",
+      "title": "高蛋白番茄拌面",
+      "groupId": "DEMO-G4",
+      "stageCode": "S4",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M20",
+      "memberIds": [
+        "DEMO-M19",
+        "DEMO-M20",
+        "DEMO-M21",
+        "DEMO-M22",
+        "DEMO-M23",
+        "DEMO-M24"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M25"
+      ],
+      "opportunityId": "DEMO-O08",
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "usageScenario": "运动后便餐,关注配料及饱腹体验",
+      "targetPrice": {
+        "amount": 9.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "蛋白来源与检测口径未知",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q08-1",
+        "DEMO-Q08-2",
+        "DEMO-Q08-3",
+        "DEMO-Q08-4",
+        "DEMO-Q08-5",
+        "DEMO-Q08-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P09",
+      "title": "家庭双人小份组合",
+      "groupId": "DEMO-G5",
+      "stageCode": "S5",
+      "progressState": "completed",
+      "ownerMemberId": "DEMO-M25",
+      "memberIds": [
+        "DEMO-M25",
+        "DEMO-M26",
+        "DEMO-M27",
+        "DEMO-M28",
+        "DEMO-M29",
+        "DEMO-M30"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M28"
+      ],
+      "opportunityId": "DEMO-O09",
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "usageScenario": "两人晚餐,偏好分装、可自行搭菜",
+      "targetPrice": {
+        "amount": 6.45,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": 12.9,
+      "knownRisk": "总价与单份价容易混淆,真实用餐量待核",
+      "conceptIds": [
+        "DEMO-C09-V1",
+        "DEMO-C09-V2"
+      ],
+      "currentConceptId": "DEMO-C09-V2",
+      "questionIds": [
+        "DEMO-Q09-1",
+        "DEMO-Q09-2",
+        "DEMO-Q09-3",
+        "DEMO-Q09-4",
+        "DEMO-Q09-5",
+        "DEMO-Q09-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P10",
+      "title": "一人食酱香大满足",
+      "groupId": "DEMO-G5",
+      "stageCode": "S3",
+      "progressState": "blocked",
+      "ownerMemberId": "DEMO-M26",
+      "memberIds": [
+        "DEMO-M25",
+        "DEMO-M26",
+        "DEMO-M27",
+        "DEMO-M28",
+        "DEMO-M29",
+        "DEMO-M30"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M31"
+      ],
+      "opportunityId": "DEMO-O10",
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "usageScenario": "一人晚餐,想减少临时补主食",
+      "targetPrice": {
+        "amount": 8.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "正餐、加餐和习惯搭配尚未区分",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q10-1",
+        "DEMO-Q10-2",
+        "DEMO-Q10-3",
+        "DEMO-Q10-4",
+        "DEMO-Q10-5",
+        "DEMO-Q10-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P11",
+      "title": "贵州红酸汤风味面",
+      "groupId": "DEMO-G6",
+      "stageCode": "S5",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M31",
+      "memberIds": [
+        "DEMO-M31",
+        "DEMO-M32",
+        "DEMO-M33",
+        "DEMO-M34",
+        "DEMO-M35",
+        "DEMO-M36"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M34"
+      ],
+      "opportunityId": "DEMO-O11",
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "usageScenario": "地域尝鲜,偏好酸鲜但怕踩雷",
+      "targetPrice": {
+        "amount": 9.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "风味分歧与正宗表达依据待核",
+      "conceptIds": [
+        "DEMO-C11-V1",
+        "DEMO-C11-V2"
+      ],
+      "currentConceptId": "DEMO-C11-V2",
+      "questionIds": [
+        "DEMO-Q11-1",
+        "DEMO-Q11-2",
+        "DEMO-Q11-3",
+        "DEMO-Q11-4",
+        "DEMO-Q11-5",
+        "DEMO-Q11-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-P12",
+      "title": "川香椒麻宽面",
+      "groupId": "DEMO-G6",
+      "stageCode": "S4",
+      "progressState": "in_progress",
+      "ownerMemberId": "DEMO-M32",
+      "memberIds": [
+        "DEMO-M31",
+        "DEMO-M32",
+        "DEMO-M33",
+        "DEMO-M34",
+        "DEMO-M35",
+        "DEMO-M36"
+      ],
+      "collaboratorMemberIds": [
+        "DEMO-M01"
+      ],
+      "opportunityId": "DEMO-O12",
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "usageScenario": "嗜辣尝鲜,希望可控椒麻层次",
+      "targetPrice": {
+        "amount": 8.9,
+        "currency": "CNY",
+        "basis": "per_serving",
+        "kind": "demo_target_assumption"
+      },
+      "packPrice": null,
+      "knownRisk": "宽面复水、麻辣分离和酱料挂壁待核",
+      "conceptIds": [],
+      "currentConceptId": null,
+      "questionIds": [
+        "DEMO-Q12-1",
+        "DEMO-Q12-2",
+        "DEMO-Q12-3",
+        "DEMO-Q12-4",
+        "DEMO-Q12-5",
+        "DEMO-Q12-6"
+      ],
+      "enterpriseApproval": false,
+      "formalReportPublished": false
+    }
+  ],
+  "conceptVersions": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C01-V1",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "version": 1,
+      "previousVersionId": null,
+      "name": "工位轻香葱油拌面",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q01-1",
+        "DEMO-Q01-2",
+        "DEMO-Q01-3",
+        "DEMO-Q01-4",
+        "DEMO-Q01-5",
+        "DEMO-Q01-6"
+      ],
+      "changeNote": "课堂设计初稿"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C01-V2",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "version": 2,
+      "previousVersionId": "DEMO-C01-V1",
+      "name": "工位轻香葱油拌面",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q01-1",
+        "DEMO-Q01-2",
+        "DEMO-Q01-3",
+        "DEMO-Q01-4",
+        "DEMO-Q01-5",
+        "DEMO-Q01-6"
+      ],
+      "changeNote": "补充适用条件与反证"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C04-V1",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "version": 1,
+      "previousVersionId": null,
+      "name": "夜宵芝士轻辣拌面",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q04-1",
+        "DEMO-Q04-2",
+        "DEMO-Q04-3",
+        "DEMO-Q04-4",
+        "DEMO-Q04-5",
+        "DEMO-Q04-6"
+      ],
+      "changeNote": "课堂设计初稿"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C04-V2",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "version": 2,
+      "previousVersionId": "DEMO-C04-V1",
+      "name": "夜宵芝士轻辣拌面",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q04-1",
+        "DEMO-Q04-2",
+        "DEMO-Q04-3",
+        "DEMO-Q04-4",
+        "DEMO-Q04-5",
+        "DEMO-Q04-6"
+      ],
+      "changeNote": "补充适用条件与反证"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C09-V1",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "version": 1,
+      "previousVersionId": null,
+      "name": "家庭双人小份组合",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q09-1",
+        "DEMO-Q09-2",
+        "DEMO-Q09-3",
+        "DEMO-Q09-4",
+        "DEMO-Q09-5",
+        "DEMO-Q09-6"
+      ],
+      "changeNote": "课堂设计初稿"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C09-V2",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "version": 2,
+      "previousVersionId": "DEMO-C09-V1",
+      "name": "家庭双人小份组合",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q09-1",
+        "DEMO-Q09-2",
+        "DEMO-Q09-3",
+        "DEMO-Q09-4",
+        "DEMO-Q09-5",
+        "DEMO-Q09-6"
+      ],
+      "changeNote": "补充适用条件与反证"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C11-V1",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "version": 1,
+      "previousVersionId": null,
+      "name": "贵州红酸汤风味面",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q11-1",
+        "DEMO-Q11-2",
+        "DEMO-Q11-3",
+        "DEMO-Q11-4",
+        "DEMO-Q11-5",
+        "DEMO-Q11-6"
+      ],
+      "changeNote": "课堂设计初稿"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-C11-V2",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "version": 2,
+      "previousVersionId": "DEMO-C11-V1",
+      "name": "贵州红酸汤风味面",
+      "specification": {
+        "currentSku": null,
+        "netWeightG": null,
+        "noodleWeightG": null,
+        "packCount": null,
+        "packaging": "课堂候选,待核"
+      },
+      "formulaItems": [],
+      "productionSteps": [],
+      "questionIds": [
+        "DEMO-Q11-1",
+        "DEMO-Q11-2",
+        "DEMO-Q11-3",
+        "DEMO-Q11-4",
+        "DEMO-Q11-5",
+        "DEMO-Q11-6"
+      ],
+      "changeNote": "补充适用条件与反证"
+    }
+  ],
+  "reports": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P01-S3",
+      "title": "工位轻香葱油拌面|S3课堂报告",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "questionIds": [
+        "DEMO-Q01-1",
+        "DEMO-Q01-2",
+        "DEMO-Q01-3",
+        "DEMO-Q01-4",
+        "DEMO-Q01-5",
+        "DEMO-Q01-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:工位午餐,只有热水;限制:低气味与酱香可能冲突,制备和沥水条件待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P01-S4",
+      "title": "工位轻香葱油拌面|S4课堂报告",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "questionIds": [
+        "DEMO-Q01-1",
+        "DEMO-Q01-2",
+        "DEMO-Q01-3",
+        "DEMO-Q01-4",
+        "DEMO-Q01-5",
+        "DEMO-Q01-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:工位午餐,只有热水;限制:低气味与酱香可能冲突,制备和沥水条件待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P01-S5",
+      "title": "工位轻香葱油拌面|S5课堂报告",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "stageCode": "S5",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "questionIds": [
+        "DEMO-Q01-1",
+        "DEMO-Q01-2",
+        "DEMO-Q01-3",
+        "DEMO-Q01-4",
+        "DEMO-Q01-5",
+        "DEMO-Q01-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:工位午餐,只有热水;限制:低气味与酱香可能冲突,制备和沥水条件待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P02-S3",
+      "title": "晚归番茄鸡蛋汤面|S3课堂报告",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "questionIds": [
+        "DEMO-Q02-1",
+        "DEMO-Q02-2",
+        "DEMO-Q02-3",
+        "DEMO-Q02-4",
+        "DEMO-Q02-5",
+        "DEMO-Q02-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:晚归加班餐,希望热乎但不油腻;限制:泡发口感与加料习惯混杂。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P02-S4",
+      "title": "晚归番茄鸡蛋汤面|S4课堂报告",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S01"
+      ],
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "questionIds": [
+        "DEMO-Q02-1",
+        "DEMO-Q02-2",
+        "DEMO-Q02-3",
+        "DEMO-Q02-4",
+        "DEMO-Q02-5",
+        "DEMO-Q02-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:晚归加班餐,希望热乎但不油腻;限制:泡发口感与加料习惯混杂。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P03-S3",
+      "title": "宿舍轻辣番茄拌面|S3课堂报告",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "questionIds": [
+        "DEMO-Q03-1",
+        "DEMO-Q03-2",
+        "DEMO-Q03-3",
+        "DEMO-Q03-4",
+        "DEMO-Q03-5",
+        "DEMO-Q03-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:无灶具宿舍,预算敏感;限制:无灶具目标与面体需煮条件冲突。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P03-S4",
+      "title": "宿舍轻辣番茄拌面|S4课堂报告",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "questionIds": [
+        "DEMO-Q03-1",
+        "DEMO-Q03-2",
+        "DEMO-Q03-3",
+        "DEMO-Q03-4",
+        "DEMO-Q03-5",
+        "DEMO-Q03-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:无灶具宿舍,预算敏感;限制:无灶具目标与面体需煮条件冲突。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P04-S3",
+      "title": "夜宵芝士轻辣拌面|S3课堂报告",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "questionIds": [
+        "DEMO-Q04-1",
+        "DEMO-Q04-2",
+        "DEMO-Q04-3",
+        "DEMO-Q04-4",
+        "DEMO-Q04-5",
+        "DEMO-Q04-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:同寝分享夜宵,希望辣度可调;限制:芝士浓郁与轻负担诉求冲突。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P04-S4",
+      "title": "夜宵芝士轻辣拌面|S4课堂报告",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "questionIds": [
+        "DEMO-Q04-1",
+        "DEMO-Q04-2",
+        "DEMO-Q04-3",
+        "DEMO-Q04-4",
+        "DEMO-Q04-5",
+        "DEMO-Q04-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:同寝分享夜宵,希望辣度可调;限制:芝士浓郁与轻负担诉求冲突。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P04-S5",
+      "title": "夜宵芝士轻辣拌面|S5课堂报告",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "stageCode": "S5",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S02"
+      ],
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "questionIds": [
+        "DEMO-Q04-1",
+        "DEMO-Q04-2",
+        "DEMO-Q04-3",
+        "DEMO-Q04-4",
+        "DEMO-Q04-5",
+        "DEMO-Q04-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:同寝分享夜宵,希望辣度可调;限制:芝士浓郁与轻负担诉求冲突。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P05-S3",
+      "title": "露营菌菇热汤面|S3课堂报告",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "questionIds": [
+        "DEMO-Q05-1",
+        "DEMO-Q05-2",
+        "DEMO-Q05-3",
+        "DEMO-Q05-4",
+        "DEMO-Q05-5",
+        "DEMO-Q05-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:有限炊具的户外露营;限制:热源、用水及垃圾回收条件不统一。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P05-S4",
+      "title": "露营菌菇热汤面|S4课堂报告",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "questionIds": [
+        "DEMO-Q05-1",
+        "DEMO-Q05-2",
+        "DEMO-Q05-3",
+        "DEMO-Q05-4",
+        "DEMO-Q05-5",
+        "DEMO-Q05-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:有限炊具的户外露营;限制:热源、用水及垃圾回收条件不统一。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P06-S3",
+      "title": "高铁低气味葱香拌面|S3课堂报告",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S03"
+      ],
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "questionIds": [
+        "DEMO-Q06-1",
+        "DEMO-Q06-2",
+        "DEMO-Q06-3",
+        "DEMO-Q06-4",
+        "DEMO-Q06-5",
+        "DEMO-Q06-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:出差高铁,只有开水;限制:弃水、气味与携带便利相互牵制。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P07-S3",
+      "title": "高纤荞麦拌面|S3课堂报告",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "questionIds": [
+        "DEMO-Q07-1",
+        "DEMO-Q07-2",
+        "DEMO-Q07-3",
+        "DEMO-Q07-4",
+        "DEMO-Q07-5",
+        "DEMO-Q07-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:午间控卡,仍希望保留面食口感;限制:营养、口感、配方和成本均需证据。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P07-S4",
+      "title": "高纤荞麦拌面|S4课堂报告",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "questionIds": [
+        "DEMO-Q07-1",
+        "DEMO-Q07-2",
+        "DEMO-Q07-3",
+        "DEMO-Q07-4",
+        "DEMO-Q07-5",
+        "DEMO-Q07-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:午间控卡,仍希望保留面食口感;限制:营养、口感、配方和成本均需证据。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P08-S3",
+      "title": "高蛋白番茄拌面|S3课堂报告",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "questionIds": [
+        "DEMO-Q08-1",
+        "DEMO-Q08-2",
+        "DEMO-Q08-3",
+        "DEMO-Q08-4",
+        "DEMO-Q08-5",
+        "DEMO-Q08-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:运动后便餐,关注配料及饱腹体验;限制:蛋白来源与检测口径未知。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P08-S4",
+      "title": "高蛋白番茄拌面|S4课堂报告",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S04"
+      ],
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "questionIds": [
+        "DEMO-Q08-1",
+        "DEMO-Q08-2",
+        "DEMO-Q08-3",
+        "DEMO-Q08-4",
+        "DEMO-Q08-5",
+        "DEMO-Q08-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:运动后便餐,关注配料及饱腹体验;限制:蛋白来源与检测口径未知。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P09-S3",
+      "title": "家庭双人小份组合|S3课堂报告",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "questionIds": [
+        "DEMO-Q09-1",
+        "DEMO-Q09-2",
+        "DEMO-Q09-3",
+        "DEMO-Q09-4",
+        "DEMO-Q09-5",
+        "DEMO-Q09-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:两人晚餐,偏好分装、可自行搭菜;限制:总价与单份价容易混淆,真实用餐量待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P09-S4",
+      "title": "家庭双人小份组合|S4课堂报告",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "questionIds": [
+        "DEMO-Q09-1",
+        "DEMO-Q09-2",
+        "DEMO-Q09-3",
+        "DEMO-Q09-4",
+        "DEMO-Q09-5",
+        "DEMO-Q09-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:两人晚餐,偏好分装、可自行搭菜;限制:总价与单份价容易混淆,真实用餐量待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P09-S5",
+      "title": "家庭双人小份组合|S5课堂报告",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "stageCode": "S5",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "questionIds": [
+        "DEMO-Q09-1",
+        "DEMO-Q09-2",
+        "DEMO-Q09-3",
+        "DEMO-Q09-4",
+        "DEMO-Q09-5",
+        "DEMO-Q09-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:两人晚餐,偏好分装、可自行搭菜;限制:总价与单份价容易混淆,真实用餐量待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P10-S3",
+      "title": "一人食酱香大满足|S3课堂报告",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S05"
+      ],
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "questionIds": [
+        "DEMO-Q10-1",
+        "DEMO-Q10-2",
+        "DEMO-Q10-3",
+        "DEMO-Q10-4",
+        "DEMO-Q10-5",
+        "DEMO-Q10-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:一人晚餐,想减少临时补主食;限制:正餐、加餐和习惯搭配尚未区分。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P11-S3",
+      "title": "贵州红酸汤风味面|S3课堂报告",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "questionIds": [
+        "DEMO-Q11-1",
+        "DEMO-Q11-2",
+        "DEMO-Q11-3",
+        "DEMO-Q11-4",
+        "DEMO-Q11-5",
+        "DEMO-Q11-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:地域尝鲜,偏好酸鲜但怕踩雷;限制:风味分歧与正宗表达依据待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P11-S4",
+      "title": "贵州红酸汤风味面|S4课堂报告",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "questionIds": [
+        "DEMO-Q11-1",
+        "DEMO-Q11-2",
+        "DEMO-Q11-3",
+        "DEMO-Q11-4",
+        "DEMO-Q11-5",
+        "DEMO-Q11-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:地域尝鲜,偏好酸鲜但怕踩雷;限制:风味分歧与正宗表达依据待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P11-S5",
+      "title": "贵州红酸汤风味面|S5课堂报告",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "stageCode": "S5",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "questionIds": [
+        "DEMO-Q11-1",
+        "DEMO-Q11-2",
+        "DEMO-Q11-3",
+        "DEMO-Q11-4",
+        "DEMO-Q11-5",
+        "DEMO-Q11-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:地域尝鲜,偏好酸鲜但怕踩雷;限制:风味分歧与正宗表达依据待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P12-S3",
+      "title": "川香椒麻宽面|S3课堂报告",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "stageCode": "S3",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "questionIds": [
+        "DEMO-Q12-1",
+        "DEMO-Q12-2",
+        "DEMO-Q12-3",
+        "DEMO-Q12-4",
+        "DEMO-Q12-5",
+        "DEMO-Q12-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:嗜辣尝鲜,希望可控椒麻层次;限制:宽面复水、麻辣分离和酱料挂壁待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-P12-S4",
+      "title": "川香椒麻宽面|S4课堂报告",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "stageCode": "S4",
+      "version": 1,
+      "sourceIds": [
+        "DEMO-S06"
+      ],
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "questionIds": [
+        "DEMO-Q12-1",
+        "DEMO-Q12-2",
+        "DEMO-Q12-3",
+        "DEMO-Q12-4",
+        "DEMO-Q12-5",
+        "DEMO-Q12-6"
+      ],
+      "bodySummary": "模拟课堂记录。使用情境:嗜辣尝鲜,希望可控椒麻层次;限制:宽面复水、麻辣分离和酱料挂壁待核。未开展真实实物或市场验证。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G1-S1",
+      "title": "G1 上班族工作餐与加班餐|S1小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G1",
+      "stageCode": "S1",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G1-S2",
+      "title": "G1 上班族工作餐与加班餐|S2小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G1",
+      "stageCode": "S2",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G1-S3",
+      "title": "G1 上班族工作餐与加班餐|S3小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G1",
+      "stageCode": "S3",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G2-S1",
+      "title": "G2 学生宿舍与夜宵|S1小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G2",
+      "stageCode": "S1",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G2-S2",
+      "title": "G2 学生宿舍与夜宵|S2小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G2",
+      "stageCode": "S2",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G2-S3",
+      "title": "G2 学生宿舍与夜宵|S3小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G2",
+      "stageCode": "S3",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G3-S1",
+      "title": "G3 户外露营与出行|S1小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G3",
+      "stageCode": "S1",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G3-S2",
+      "title": "G3 户外露营与出行|S2小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G3",
+      "stageCode": "S2",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G3-S3",
+      "title": "G3 户外露营与出行|S3小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G3",
+      "stageCode": "S3",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G4-S1",
+      "title": "G4 健康控卡人群|S1小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G4",
+      "stageCode": "S1",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G4-S2",
+      "title": "G4 健康控卡人群|S2小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G4",
+      "stageCode": "S2",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G4-S3",
+      "title": "G4 健康控卡人群|S3小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G4",
+      "stageCode": "S3",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G5-S1",
+      "title": "G5 一人食与家庭囤货|S1小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G5",
+      "stageCode": "S1",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G5-S2",
+      "title": "G5 一人食与家庭囤货|S2小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G5",
+      "stageCode": "S2",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G5-S3",
+      "title": "G5 一人食与家庭囤货|S3小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G5",
+      "stageCode": "S3",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G6-S1",
+      "title": "G6 地域风味与高端尝鲜|S1小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G6",
+      "stageCode": "S1",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G6-S2",
+      "title": "G6 地域风味与高端尝鲜|S2小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G6",
+      "stageCode": "S2",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-R-DEMO-G6-S3",
+      "title": "G6 地域风味与高端尝鲜|S3小组作业",
+      "projectId": null,
+      "groupId": "DEMO-G6",
+      "stageCode": "S3",
+      "version": 1,
+      "bodySummary": "模拟小组作业,保留原始依据与待补问题。"
+    }
+  ],
+  "questions": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q01-1",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M01",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q01-2",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M02",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q01-3",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M03",
+      "state": "closed_in_demo",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "resolution": "仅为模拟关闭,真实研究未发生"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q01-4",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "question": "该方向的关键工艺限制如何核实:低气味与酱香可能冲突,制备和沥水条件待核?",
+      "ownerMemberId": "DEMO-M04",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q01-5",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M05",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q01-6",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M06",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q02-1",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M01",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q02-2",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M02",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q02-3",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M03",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E01"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q02-4",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "question": "该方向的关键工艺限制如何核实:泡发口感与加料习惯混杂?",
+      "ownerMemberId": "DEMO-M04",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q02-5",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M05",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q02-6",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M06",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q03-1",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M07",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q03-2",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M08",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q03-3",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M09",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q03-4",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "question": "该方向的关键工艺限制如何核实:无灶具目标与面体需煮条件冲突?",
+      "ownerMemberId": "DEMO-M10",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q03-5",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M11",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q03-6",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M12",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q04-1",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M07",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q04-2",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M08",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q04-3",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M09",
+      "state": "closed_in_demo",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E02"
+      ],
+      "resolution": "仅为模拟关闭,真实研究未发生"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q04-4",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "question": "该方向的关键工艺限制如何核实:芝士浓郁与轻负担诉求冲突?",
+      "ownerMemberId": "DEMO-M10",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q04-5",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M11",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q04-6",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M12",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q05-1",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M13",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q05-2",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M14",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q05-3",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M15",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q05-4",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "question": "该方向的关键工艺限制如何核实:热源、用水及垃圾回收条件不统一?",
+      "ownerMemberId": "DEMO-M16",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q05-5",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M17",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q05-6",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M18",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q06-1",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M13",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q06-2",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M14",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q06-3",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M15",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E03"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q06-4",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "question": "该方向的关键工艺限制如何核实:弃水、气味与携带便利相互牵制?",
+      "ownerMemberId": "DEMO-M16",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q06-5",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M17",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q06-6",
+      "projectId": "DEMO-P06",
+      "groupId": "DEMO-G3",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M18",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q07-1",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M19",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q07-2",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M20",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q07-3",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M21",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q07-4",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "question": "该方向的关键工艺限制如何核实:营养、口感、配方和成本均需证据?",
+      "ownerMemberId": "DEMO-M22",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q07-5",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M23",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q07-6",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M24",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q08-1",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M19",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q08-2",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M20",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q08-3",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M21",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E04"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q08-4",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "question": "该方向的关键工艺限制如何核实:蛋白来源与检测口径未知?",
+      "ownerMemberId": "DEMO-M22",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q08-5",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M23",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q08-6",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M24",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q09-1",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M25",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q09-2",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M26",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q09-3",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M27",
+      "state": "closed_in_demo",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "resolution": "仅为模拟关闭,真实研究未发生"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q09-4",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "question": "该方向的关键工艺限制如何核实:总价与单份价容易混淆,真实用餐量待核?",
+      "ownerMemberId": "DEMO-M28",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q09-5",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M29",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q09-6",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M30",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q10-1",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M25",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q10-2",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M26",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q10-3",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M27",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E05"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q10-4",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "question": "该方向的关键工艺限制如何核实:正餐、加餐和习惯搭配尚未区分?",
+      "ownerMemberId": "DEMO-M28",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q10-5",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M29",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q10-6",
+      "projectId": "DEMO-P10",
+      "groupId": "DEMO-G5",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M30",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q11-1",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M31",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q11-2",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M32",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q11-3",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M33",
+      "state": "closed_in_demo",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "resolution": "仅为模拟关闭,真实研究未发生"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q11-4",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "question": "该方向的关键工艺限制如何核实:风味分歧与正宗表达依据待核?",
+      "ownerMemberId": "DEMO-M34",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q11-5",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M35",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q11-6",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M36",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q12-1",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "question": "拟研究的具体商品版本、规格与制备条件是什么?",
+      "ownerMemberId": "DEMO-M31",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "具体包装与版本资料",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q12-2",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "question": "用餐目的是正餐、加餐、夜宵还是分享?",
+      "ownerMemberId": "DEMO-M32",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "使用情境记录",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q12-3",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "question": "支持本假设与相反情况的原始材料分别在哪里?",
+      "ownerMemberId": "DEMO-M33",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "可回溯原文及反证",
+      "evidenceIds": [
+        "DEMO-E06"
+      ],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q12-4",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "question": "该方向的关键工艺限制如何核实:宽面复水、麻辣分离和酱料挂壁待核?",
+      "ownerMemberId": "DEMO-M34",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "工艺条件说明",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q12-5",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "question": "目标价格、包材、工艺与成本假设是否可同时成立?",
+      "ownerMemberId": "DEMO-M35",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "同口径价格与成本依据",
+      "evidenceIds": [],
+      "resolution": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-Q12-6",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "question": "下一步研究怎样记录行为、减少诱导并预先定义判断条件?",
+      "ownerMemberId": "DEMO-M36",
+      "state": "awaiting_evidence",
+      "requiredMaterial": "研究方案",
+      "evidenceIds": [],
+      "resolution": null
+    }
+  ],
+  "validationTasks": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V01-1",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V01-2",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V01-3",
+      "projectId": "DEMO-P01",
+      "groupId": "DEMO-G1",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V02-1",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V02-2",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V02-3",
+      "projectId": "DEMO-P02",
+      "groupId": "DEMO-G1",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V03-1",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "scope": "证据与反证核对",
+      "state": "awaiting_material",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V03-2",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "scope": "制备和可行性桌面评估",
+      "state": "awaiting_material",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V03-3",
+      "projectId": "DEMO-P03",
+      "groupId": "DEMO-G2",
+      "scope": "后续验证方案",
+      "state": "awaiting_material",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V04-1",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V04-2",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V04-3",
+      "projectId": "DEMO-P04",
+      "groupId": "DEMO-G2",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V05-1",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "scope": "证据与反证核对",
+      "state": "awaiting_material",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V05-2",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "scope": "制备和可行性桌面评估",
+      "state": "awaiting_material",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V05-3",
+      "projectId": "DEMO-P05",
+      "groupId": "DEMO-G3",
+      "scope": "后续验证方案",
+      "state": "awaiting_material",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V07-1",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V07-2",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V07-3",
+      "projectId": "DEMO-P07",
+      "groupId": "DEMO-G4",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V08-1",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V08-2",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V08-3",
+      "projectId": "DEMO-P08",
+      "groupId": "DEMO-G4",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V09-1",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V09-2",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V09-3",
+      "projectId": "DEMO-P09",
+      "groupId": "DEMO-G5",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V11-1",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V11-2",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V11-3",
+      "projectId": "DEMO-P11",
+      "groupId": "DEMO-G6",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V12-1",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "scope": "证据与反证核对",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V12-2",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "scope": "制备和可行性桌面评估",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-V12-3",
+      "projectId": "DEMO-P12",
+      "groupId": "DEMO-G6",
+      "scope": "后续验证方案",
+      "state": "in_progress",
+      "method": "桌面模拟",
+      "recommendation": "补齐依据后复核",
+      "physicalTestPerformed": false,
+      "enterpriseApproval": false,
+      "dimensions": {
+        "用户依据": null,
+        "制备条件": null,
+        "成本": null
+      },
+      "totalScore": null
+    }
+  ],
+  "assignments": [
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G1-1",
+      "groupId": "DEMO-G1",
+      "halfDay": 1,
+      "deliverableTypes": [
+        "S1"
+      ],
+      "reportIds": [
+        "DEMO-R-DEMO-G1-S1"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G1-2",
+      "groupId": "DEMO-G1",
+      "halfDay": 2,
+      "deliverableTypes": [
+        "S2",
+        "S3"
+      ],
+      "reportIds": [
+        "DEMO-R-P01-S3",
+        "DEMO-R-P02-S3",
+        "DEMO-R-DEMO-G1-S2",
+        "DEMO-R-DEMO-G1-S3"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G1-3",
+      "groupId": "DEMO-G1",
+      "halfDay": 3,
+      "deliverableTypes": [
+        "S4",
+        "S5"
+      ],
+      "reportIds": [
+        "DEMO-R-P01-S4",
+        "DEMO-R-P01-S5",
+        "DEMO-R-P02-S4"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G2-1",
+      "groupId": "DEMO-G2",
+      "halfDay": 1,
+      "deliverableTypes": [
+        "S1"
+      ],
+      "reportIds": [
+        "DEMO-R-DEMO-G2-S1"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G2-2",
+      "groupId": "DEMO-G2",
+      "halfDay": 2,
+      "deliverableTypes": [
+        "S2",
+        "S3"
+      ],
+      "reportIds": [
+        "DEMO-R-P03-S3",
+        "DEMO-R-P04-S3",
+        "DEMO-R-DEMO-G2-S2",
+        "DEMO-R-DEMO-G2-S3"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G2-3",
+      "groupId": "DEMO-G2",
+      "halfDay": 3,
+      "deliverableTypes": [
+        "S4",
+        "S5"
+      ],
+      "reportIds": [
+        "DEMO-R-P03-S4",
+        "DEMO-R-P04-S4",
+        "DEMO-R-P04-S5"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G3-1",
+      "groupId": "DEMO-G3",
+      "halfDay": 1,
+      "deliverableTypes": [
+        "S1"
+      ],
+      "reportIds": [
+        "DEMO-R-DEMO-G3-S1"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G3-2",
+      "groupId": "DEMO-G3",
+      "halfDay": 2,
+      "deliverableTypes": [
+        "S2",
+        "S3"
+      ],
+      "reportIds": [
+        "DEMO-R-P05-S3",
+        "DEMO-R-P06-S3",
+        "DEMO-R-DEMO-G3-S2",
+        "DEMO-R-DEMO-G3-S3"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G3-3",
+      "groupId": "DEMO-G3",
+      "halfDay": 3,
+      "deliverableTypes": [
+        "S4",
+        "S5"
+      ],
+      "reportIds": [
+        "DEMO-R-P05-S4"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G4-1",
+      "groupId": "DEMO-G4",
+      "halfDay": 1,
+      "deliverableTypes": [
+        "S1"
+      ],
+      "reportIds": [
+        "DEMO-R-DEMO-G4-S1"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G4-2",
+      "groupId": "DEMO-G4",
+      "halfDay": 2,
+      "deliverableTypes": [
+        "S2",
+        "S3"
+      ],
+      "reportIds": [
+        "DEMO-R-P07-S3",
+        "DEMO-R-P08-S3",
+        "DEMO-R-DEMO-G4-S2",
+        "DEMO-R-DEMO-G4-S3"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G4-3",
+      "groupId": "DEMO-G4",
+      "halfDay": 3,
+      "deliverableTypes": [
+        "S4",
+        "S5"
+      ],
+      "reportIds": [
+        "DEMO-R-P07-S4",
+        "DEMO-R-P08-S4"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G5-1",
+      "groupId": "DEMO-G5",
+      "halfDay": 1,
+      "deliverableTypes": [
+        "S1"
+      ],
+      "reportIds": [
+        "DEMO-R-DEMO-G5-S1"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G5-2",
+      "groupId": "DEMO-G5",
+      "halfDay": 2,
+      "deliverableTypes": [
+        "S2",
+        "S3"
+      ],
+      "reportIds": [
+        "DEMO-R-P09-S3",
+        "DEMO-R-P10-S3",
+        "DEMO-R-DEMO-G5-S2",
+        "DEMO-R-DEMO-G5-S3"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G5-3",
+      "groupId": "DEMO-G5",
+      "halfDay": 3,
+      "deliverableTypes": [
+        "S4",
+        "S5"
+      ],
+      "reportIds": [
+        "DEMO-R-P09-S4",
+        "DEMO-R-P09-S5"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G6-1",
+      "groupId": "DEMO-G6",
+      "halfDay": 1,
+      "deliverableTypes": [
+        "S1"
+      ],
+      "reportIds": [
+        "DEMO-R-DEMO-G6-S1"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G6-2",
+      "groupId": "DEMO-G6",
+      "halfDay": 2,
+      "deliverableTypes": [
+        "S2",
+        "S3"
+      ],
+      "reportIds": [
+        "DEMO-R-P11-S3",
+        "DEMO-R-P12-S3",
+        "DEMO-R-DEMO-G6-S2",
+        "DEMO-R-DEMO-G6-S3"
+      ],
+      "state": "review_required"
+    },
+    {
+      "demoId": "KSF-INNOVATION-DEMO-20261103",
+      "dataMode": "sample",
+      "isSynthetic": true,
+      "provenanceType": "synthetic_demo",
+      "id": "DEMO-A-DEMO-G6-3",
+      "groupId": "DEMO-G6",
+      "halfDay": 3,
+      "deliverableTypes": [
+        "S4",
+        "S5"
+      ],
+      "reportIds": [
+        "DEMO-R-P11-S4",
+        "DEMO-R-P11-S5",
+        "DEMO-R-P12-S4"
+      ],
+      "state": "review_required"
+    }
+  ],
+  "actualSeedCounts": {
+    "groups": 6,
+    "members": 36,
+    "ideas": 72,
+    "sources": 6,
+    "evidence": 6,
+    "opportunities": 24,
+    "projects": 12,
+    "conceptVersions": 8,
+    "reports": 44,
+    "questions": 72,
+    "validationTasks": 30,
+    "assignments": 18
+  },
+  "projectStageCounts": {
+    "S5": 4,
+    "S4": 6,
+    "S3": 2
+  },
+  "projectStateCounts": {
+    "completed": 2,
+    "in_progress": 7,
+    "blocked": 3
+  }
+}

BIN
src/assets/showcase/research.png


BIN
src/assets/showcase/validation.png


+ 3 - 7
src/components/kf-opportunity-card.js

@@ -19,6 +19,7 @@ import './kf-badge.js';
 import './kf-glass-card.js';
 import { fmtDate } from '../core/index.js';
 import { t } from './cjk.js';
+import { href } from '../utils/router.js';
 
 const METHOD_LABEL = { session: '会话上传', skill: t('Skill 上传'), manual: '手工录入' };
 
@@ -125,11 +126,6 @@ export class KfOpportunityCard extends LitElement {
     }
   `;
 
-  /** 内嵌链接:action 选「报告」/「台账」,由 page 通过 link-href 事件问 router 要完整 URL */
-  #linkHref(kind, id) {
-    return `${this.base}/${encodeURIComponent(id)}`;
-  }
-
   #open = (e) => {
     e?.stopPropagation();
     if (!this.opp) return;
@@ -158,7 +154,7 @@ export class KfOpportunityCard extends LitElement {
     const s = o.stats || { avg: null, n: 0 };
     const u = o.upload || {};
     const dec = o.decision;
-    const reportHref = o.reportArtifact ? `${this.base || ''}/report/${encodeURIComponent(o.reportArtifact.id)}` : '';
+    const reportHref = o.reportArtifact ? href(`/report/${encodeURIComponent(o.reportArtifact.id)}`) : '';
     return html`
       <kf-glass-card lift fill-to surface="2" tone=${o.status === 'rejected' ? 'danger' : o.status === 'approved' ? 'ok' : ''} @card-activate=${this.#open}>
         <div class="wrap">
@@ -215,7 +211,7 @@ export class KfOpportunityCard extends LitElement {
             </div>
             <span class="sp"></span>
             ${o.reportArtifact
-              ? html`<a class="report" href=${this.#linkHref('report', o.reportArtifact.id)} data-link data-report=${o.reportArtifact.id}
+              ? html`<a class="report" href=${reportHref} data-link data-report=${o.reportArtifact.id}
                   @click=${(e) => e.stopPropagation()} title="打开关联的完整报告">完整报告 ↗</a>`
               : ''}
             <button class="kf-btn ghost" @click=${this.#open}>详情 →</button>

+ 1 - 0
src/core/evidence/chain.js

@@ -23,6 +23,7 @@ export function reportEvidence(db, artifactId, evId) {
       ref: String(q.ref || ''),
       author: String(q.author || ''),
       likes: q.likes == null ? null : Number(q.likes),
+      sourceId: q.sourceId || ev.sourceId || null,
       artId: q.artId || null,
       art: q.artId ? db.artifactById[q.artId] || null : null,
     })),

+ 15 - 1
src/core/report.js

@@ -73,7 +73,7 @@ export function reportOverview(db, artifactId) {
   };
 }
 
-/** 结论块:从 findings 小节里取(同一证据锚点只取第一次出现),并附上它的证据厚度 */
+/** 结论与资料引用:保留 findings 顺序,并补齐没有 findings 小节的证据锚点。 */
 export function findingsOf(db, artifactId) {
   const a = db.artifactById[artifactId];
   if (!a?.report) return [];
@@ -96,6 +96,20 @@ export function findingsOf(db, artifactId) {
       });
     }
   }
+  // Table/paragraph reports also carry source-linked quotations. Keep them reachable.
+  for (const [evId, e] of Object.entries(a.report.evidence || {})) {
+    if (seen.has(evId)) continue;
+    seen.add(evId);
+    out.push({
+      id: evId,
+      evId,
+      conclusion: String(e.claim || '资料引用'),
+      attitude: '',
+      ownerCount: e.ownerCount ?? null,
+      quoteCount: (Array.isArray(e.quotes) ? e.quotes : []).length,
+      derivedCount: (Array.isArray(e.derivedFrom) ? e.derivedFrom : []).filter(Boolean).length,
+    });
+  }
   return out;
 }
 

+ 142 - 0
src/core/sample.js

@@ -0,0 +1,142 @@
+import { snapshot, sourceUnion } from './showcase.js';
+
+export const SAMPLE_NAMESPACE = 'kf-innovation-demo::20261103::v2';
+export const DEMO_ID = 'KSF-INNOVATION-DEMO-20261103';
+export const LABELS = Object.freeze({
+  in_progress: '进行中', blocked: '受阻', completed: '课堂完成', approved: '选入项目',
+  candidate: '候选', rejected: '否决', awaiting_evidence: '待补证据', review_required: '待点评',
+  pending: '待启动', done: '已完成', open: '待处理', synthetic_not_real_interview: '模拟资料',
+  demo_only: '仅供演示', support: '模拟支持材料', contradict: '模拟相反材料', unresolved: '尚待判断',
+});
+
+export function validateExtension(extension, base) {
+  if (!extension || extension.demoId !== DEMO_ID || extension.isSynthetic !== true) throw new Error('扩展数据空间不匹配');
+  const keys = ['voices', 'observations', 'prices', 'citations', 'researchTasks', 'collaborationRecords'];
+  const indexes = Object.fromEntries(keys.map((key) => {
+    if (!Array.isArray(extension[key])) throw new Error(`扩展集合缺失:${key}`);
+    return [key, new Map(extension[key].map((row) => [row.id, row]))];
+  }));
+  const ids = new Set(Object.values(base.indexes).flatMap((index) => [...index.keys()]));
+  for (const key of keys) for (const row of extension[key]) {
+    if (!row.id || ids.has(row.id)) throw new Error(`扩展记录标识重复:${row.id}`);
+    ids.add(row.id);
+    if (row.demoId !== DEMO_ID || !row.isSynthetic || row.dataMode !== 'sample') throw new Error(`非模拟扩展记录:${row.id}`);
+    for (const [field, collection] of [['projectId', 'projects'], ['groupId', 'groups'], ['ownerMemberId', 'members']]) {
+      if (row[field] && !base.indexes[collection].has(row[field])) throw new Error(`${row.id} 引用缺失:${field}`);
+    }
+    if (row.projectId && row.groupId !== base.indexes.projects.get(row.projectId).groupId) throw new Error(`${row.id} 跨组引用错误`);
+    if (row.observationId && !indexes.observations.has(row.observationId)) throw new Error(`${row.id} 商品观察引用缺失`);
+    if (row.sourceId) {
+      const source = indexes.voices.get(row.sourceId) || base.indexes.sources.get(row.sourceId);
+      if (!source || !row.quote || !source.rawText.includes(row.quote)) throw new Error(`${row.id} 引文无法逐字核对`);
+    }
+  }
+  return Object.fromEntries(keys.map((key) => [key, extension[key].length]));
+}
+
+export function scoreStats(scores = []) {
+  const byUser = new Map();
+  for (const item of scores) {
+    if (typeof item.user === 'string' && item.user.trim() && Number.isFinite(item.score) && item.score >= 0 && item.score <= 100) byUser.set(item.user.trim(), item);
+  }
+  const rows = [...byUser.values()];
+  return rows.length ? { n: rows.length, avg: Math.round(rows.reduce((n, s) => n + s.score, 0) / rows.length * 10) / 10, rows } : null;
+}
+
+export function buildSampleModel(seed, mutations = {}, extension) {
+  const base = snapshot(seed);
+  if (seed.groups[0]?.demoId !== DEMO_ID) throw new Error('基础模拟空间不匹配');
+  const extensionCounts = extension ? validateExtension(extension, base) : {};
+  const opportunities = seed.opportunities.map((row) => {
+    const patch = mutations.opportunities?.[row.id] || {};
+    return { ...row, status: ['candidate', 'approved', 'rejected'].includes(patch.status) ? patch.status : row.status,
+      projectId: row.linkedProjectId || null, ownerName: base.indexes.members.get(base.indexes.projects.get(row.linkedProjectId)?.ownerMemberId)?.displayName || '',
+      scores: scoreStats(patch.scores)?.rows || [], stats: scoreStats(patch.scores), sources: sourceUnion([row], base.indexes),
+    };
+  });
+  const members = seed.members.map((row) => ({ ...row, name: row.displayName }));
+  const projects = seed.projects.map((row) => ({ ...row, ownerName: base.indexes.members.get(row.ownerMemberId)?.displayName || '' }));
+  const model = { ...base, ...seed, counts: base.counts, baselineEntityCount: base.total, projects, members, opportunities,
+    extension: extension || { voices: [], observations: [], prices: [], citations: [], researchTasks: [], collaborationRecords: [] }, extensionCounts,
+    comments: mutations.comments || {}, revision: mutations.revision || 0,
+  };
+  for (const [key, rows] of Object.entries(model)) {
+    if (Array.isArray(rows)) model[`${key}ById`] = Object.fromEntries(rows.map((row) => [row.id, row]));
+  }
+  model.sourceById = { ...model.sourcesById, ...Object.fromEntries(model.extension.voices.map((row) => [row.id, row])) };
+  model.evidenceById = { ...model.evidenceById, ...Object.fromEntries(model.extension.citations.map((row) => [row.id, row])) };
+  model.recordById = Object.fromEntries([...Object.values(base.indexes).flatMap((index) => [...index.values()]), ...Object.values(model.extension).filter(Array.isArray).flat()].map((row) => [row.id, row]));
+  return model;
+}
+
+export function filterSample(model, filters = {}) {
+  const { group = '', member = '', project = '', stage = '', status = '', sourceType = '', search = '' } = filters;
+  const scoped = Boolean(group || member || project || stage);
+  const projects = model.projects.filter((p) => (!group || p.groupId === group) && (!project || p.id === project) && (!stage || p.stageCode === stage) &&
+    (!member || p.ownerMemberId === member || p.memberIds.includes(member) || p.collaboratorMemberIds.includes(member)));
+  const projectIds = new Set(projects.map((p) => p.id));
+  const groupIds = new Set(projects.map((p) => p.groupId));
+  const groupMatch = (row) => !group || row.groupId === group;
+  const related = (row) => groupMatch(row) && (!scoped || (row.projectId ? projectIds.has(row.projectId) : groupIds.has(row.groupId)));
+  const opportunities = model.opportunities.filter((o) => groupMatch(o) && (!status || o.status === status) &&
+    (!scoped || (o.linkedProjectId ? projectIds.has(o.linkedProjectId) : !member && !project && !stage)));
+  const reports = model.reports.filter((r) => groupMatch(r) && (!scoped || (r.projectId ? projectIds.has(r.projectId) : !project && !member && !stage)));
+  const members = model.members.filter((m) => (!group || m.groupId === group) && (!member || m.id === member) &&
+    (!project && !stage || projects.some((p) => [...p.memberIds, ...p.collaboratorMemberIds].includes(m.id))));
+  const textMatches = (row) => !search.trim() || [row.title, row.rawText, row.quote, row.productName, row.scope, row.body, row.question].filter(Boolean).join(' ').toLowerCase().includes(search.trim().toLowerCase());
+  const sources = model.sources.filter((s) => groupMatch(s) && (!scoped || groupIds.has(s.groupId)));
+  return {
+    projects, members, opportunities, reports, sources,
+    voices: model.extension.voices.filter((row) => related(row) && (!sourceType || row.channel === sourceType) && textMatches(row)),
+    observations: model.extension.observations.filter((row) => related(row) && textMatches(row)),
+    evidence: model.evidence.filter((e) => sources.some((s) => s.id === e.sourceId)),
+    questions: model.questions.filter(related), validationTasks: model.validationTasks.filter(related),
+    conceptVersions: model.conceptVersions.filter(related),
+    assignments: model.assignments.filter((a) => groupMatch(a) && (!project && !member && !stage || groupIds.has(a.groupId))),
+    groups: model.groups.filter((g) => !group || g.id === group),
+    researchTasks: model.extension.researchTasks.filter(related), collaborationRecords: model.extension.collaborationRecords.filter(related),
+  };
+}
+
+export function sampleDetail(model, type, id) {
+  const collections = { project: 'projectsById', opportunity: 'opportunitiesById', source: 'sourceById', evidence: 'evidenceById',
+    report: 'reportsById', concept: 'conceptVersionsById', question: 'questionsById', task: 'validationTasksById', assignment: 'assignmentsById',
+    observation: 'recordById', collaboration: 'recordById', research: 'recordById' };
+  const row = model[collections[type]]?.[id];
+  if (!row) return null;
+  const projectId = row.projectId || row.linkedProjectId || (type === 'project' ? row.id : null);
+  return { type, row, project: model.projectsById[projectId] || null,
+    sources: type === 'source' ? [row] : sourceUnion([row], model.indexes),
+    evidence: (row.evidenceIds || []).map((key) => model.evidenceById[key]).filter(Boolean),
+    reports: projectId ? model.reports.filter((r) => r.projectId === projectId) : [], questions: projectId ? model.questions.filter((q) => q.projectId === projectId) : [],
+    concepts: projectId ? model.conceptVersions.filter((c) => c.projectId === projectId) : [], tasks: projectId ? model.validationTasks.filter((v) => v.projectId === projectId) : [],
+    comments: [...model.extension.collaborationRecords.filter((c) => c.projectId === projectId), ...(model.comments[projectId || id] || [])],
+  };
+}
+
+export function changeSample(model, mutations, action, input, at) {
+  const next = structuredClone(mutations);
+  if (action === 'comment') {
+    if (!model.projectsById[input.id]) throw new Error('模拟项目不存在');
+    const user = String(input.user || '').trim(), body = String(input.body || '').trim();
+    if (!user || !body || user.length > 80 || body.length > 2000) throw new Error('请填写署名和2000字以内的评论');
+    next.comments ||= {}; next.comments[input.id] ||= [];
+    next.comments[input.id].push({ id: `LOCAL-${next.comments[input.id].length + 1}`, projectId: input.id, user, body, at, isSynthetic: true });
+    return next;
+  }
+  if (!model.opportunitiesById[input.id]) throw new Error('模拟机会卡不存在');
+  next.opportunities ||= {};
+  const record = next.opportunities[input.id] ||= {};
+  if (action === 'status') {
+    if (!['candidate', 'approved', 'rejected'].includes(input.status)) throw new Error('不支持的模拟状态');
+    const user = String(input.user || '').trim();
+    if (!user || user.length > 80) throw new Error('请填写演示署名');
+    record.status = input.status; record.at = at; record.by = user;
+  } else if (action === 'score') {
+    const user = String(input.user || '').trim();
+    if (input.score === '' || input.score == null || !user || user.length > 80 || !Number.isFinite(Number(input.score)) || Number(input.score) < 0 || Number(input.score) > 100) throw new Error('请填写署名与0—100之间的评分');
+    record.scores = (record.scores || []).filter((s) => s.user !== user);
+    record.scores.push({ user, score: Number(input.score), comment: String(input.comment || '').slice(0, 2000), at });
+  } else throw new Error('不支持的模拟操作');
+  return next;
+}

+ 121 - 0
src/core/showcase.js

@@ -0,0 +1,121 @@
+/** Read-only sample projections. No DOM, transport, clock or live fallback. */
+export const COLLECTIONS = Object.freeze({
+  groups: '小组', members: '成员', ideas: '创意', sources: '来源', evidence: '引文',
+  opportunities: '机会卡', projects: '项目', conceptVersions: '方案版本',
+  reports: '报告', questions: '未决问题', validationTasks: '验证任务', assignments: '课堂作业',
+});
+export const STAGE_LABEL = Object.freeze({ S1: '市场分析', S2: '需求痛点', S3: '机会识别', S4: '桌面评估', S5: '产品概念' });
+export const STATE_LABEL = Object.freeze({
+  in_progress: '进行中', blocked: '受阻', completed: '课堂完成', approved: '选入项目',
+  candidate: '候选', rejected: '否决', review_required: '待点评', awaiting_evidence: '待补证据',
+});
+
+const tally = (rows, field) => rows.reduce((counts, row) => {
+  const key = row[field];
+  counts[key] = (counts[key] || 0) + 1;
+  return counts;
+}, {});
+
+export function validateSample(seed) {
+  const errors = [];
+  if (!seed || typeof seed !== 'object') return ['模拟快照不是对象'];
+  const indexes = {};
+  const all = [];
+  for (const key of Object.keys(COLLECTIONS)) {
+    if (!Array.isArray(seed[key])) errors.push(`缺少集合:${key}`);
+    const rows = Array.isArray(seed[key]) ? seed[key] : [];
+    indexes[key] = new Map();
+    for (const row of rows) {
+      if (!row || typeof row !== 'object') { errors.push(`${key} 含无效对象`); continue; }
+      if (!row.id || indexes[key].has(row.id)) errors.push(`${key} 标识缺失或重复:${row.id}`);
+      indexes[key].set(row.id, row);
+      all.push(row);
+    }
+  }
+  const demoIds = new Set(all.map((r) => r.demoId));
+  if (demoIds.size !== 1 || !all[0]?.demoId) errors.push('模拟快照必须属于同一演示空间');
+  const ids = new Set();
+  for (const row of all) {
+    if (ids.has(row.id)) errors.push(`跨集合标识重复:${row.id}`);
+    ids.add(row.id);
+    if (row.dataMode !== 'sample' || row.isSynthetic !== true) errors.push(`${row.id} 不是模拟对象`);
+    const fields = {
+      groupId: 'groups', ownerMemberId: 'members', leaderMemberId: 'members',
+      intendedProjectId: 'projects', linkedProjectId: 'projects', projectId: 'projects',
+      sourceId: 'sources', opportunityId: 'opportunities', currentConceptId: 'conceptVersions',
+      previousVersionId: 'conceptVersions',
+    };
+    const arrays = {
+      memberIds: 'members', collaboratorMemberIds: 'members', sourceIds: 'sources',
+      evidenceIds: 'evidence', conceptIds: 'conceptVersions', questionIds: 'questions', reportIds: 'reports',
+    };
+    for (const [field, collection] of Object.entries(fields)) {
+      if (row[field] != null && !indexes[collection].has(row[field])) errors.push(`${row.id}.${field} 缺少引用:${row[field]}`);
+    }
+    for (const [field, collection] of Object.entries(arrays)) {
+      if (row[field] == null) continue;
+      if (!Array.isArray(row[field])) { errors.push(`${row.id}.${field} 必须为数组`); continue; }
+      for (const id of row[field]) if (!indexes[collection].has(id)) errors.push(`${row.id}.${field} 缺少引用:${id}`);
+    }
+    if (row.projectId) {
+      const project = indexes.projects.get(row.projectId);
+      if (project && row.groupId && row.groupId !== project.groupId) errors.push(`${row.id} 与项目小组不一致`);
+    }
+  }
+  for (const item of indexes.evidence.values()) {
+    const source = indexes.sources.get(item.sourceId);
+    if (!item.quote || !source?.rawText?.includes(item.quote)) errors.push(`${item.id} 引文不在同源原文中`);
+  }
+  for (const project of indexes.projects.values()) {
+    const opportunity = indexes.opportunities.get(project.opportunityId);
+    if (opportunity && opportunity.linkedProjectId !== project.id) errors.push(`${project.id} 机会卡互链不一致`);
+    for (const id of project.conceptIds || []) {
+      if (indexes.conceptVersions.get(id)?.projectId !== project.id) errors.push(`${project.id} 方案引用串项目`);
+    }
+  }
+  return errors;
+}
+
+export function snapshot(seed) {
+  const errors = validateSample(seed);
+  if (errors.length) throw new Error(`模拟数据校验失败:${errors.slice(0, 5).join(';')}`);
+  const counts = Object.fromEntries(Object.keys(COLLECTIONS).map((key) => [key, seed[key].length]));
+  const indexes = Object.fromEntries(Object.keys(COLLECTIONS).map((key) => [key, new Map(seed[key].map((row) => [row.id, row]))]));
+  return {
+    seed, counts, indexes, total: Object.values(counts).reduce((a, b) => a + b, 0),
+    stages: tally(seed.projects, 'stageCode'), states: tally(seed.projects, 'progressState'),
+    opportunityStates: tally(seed.opportunities, 'status'), reportStages: tally(seed.reports, 'stageCode'),
+  };
+}
+
+/** Count both direct source references and sources reached through evidence. */
+export function sourceUnion(rows, indexes) {
+  const ids = new Set();
+  for (const row of rows) {
+    for (const id of row.sourceIds || []) if (indexes.sources.has(id)) ids.add(id);
+    for (const id of row.evidenceIds || []) {
+      const sourceId = indexes.evidence.get(id)?.sourceId;
+      if (sourceId && indexes.sources.has(sourceId)) ids.add(sourceId);
+    }
+  }
+  return [...ids].map((id) => indexes.sources.get(id));
+}
+
+export function projectStory(model, projectId) {
+  const project = model.indexes.projects.get(projectId);
+  if (!project) return null;
+  const { seed, indexes } = model;
+  const opportunities = seed.opportunities.filter((row) => row.linkedProjectId === projectId || row.id === project.opportunityId);
+  const reports = seed.reports.filter((row) => row.projectId === projectId);
+  const evidenceIds = new Set([project, ...opportunities, ...reports].flatMap((row) => row.evidenceIds || []));
+  return {
+    project, opportunities, reports,
+    group: indexes.groups.get(project.groupId), owner: indexes.members.get(project.ownerMemberId),
+    ideas: seed.ideas.filter((row) => row.intendedProjectId === projectId),
+    sources: sourceUnion([project, ...opportunities, ...reports], indexes),
+    evidence: [...evidenceIds].map((id) => indexes.evidence.get(id)).filter(Boolean),
+    concepts: seed.conceptVersions.filter((row) => row.projectId === projectId).sort((a, b) => a.version - b.version),
+    questions: seed.questions.filter((row) => row.projectId === projectId),
+    tasks: seed.validationTasks.filter((row) => row.projectId === projectId),
+  };
+}

+ 53 - 83
src/layout/kf-app.js

@@ -19,12 +19,6 @@ import '../components/index.js';
 import './kf-nav.js';
 import './kf-topbar.js';
 import '../pages/view-workbench.js';
-import '../pages/view-cockpit.js';
-import '../pages/view-intel.js';
-import '../pages/view-coop.js';
-import '../pages/view-review.js';
-import '../pages/view-class.js';
-import { getCurrentModel as getCurrentSampleModel } from '../services/sample-data.js';
 import '../pages/view-pool.js';
 import '../pages/view-stages.js';
 import '../pages/view-reports.js';
@@ -32,13 +26,12 @@ import '../pages/view-report-type.js';
 import '../pages/view-report.js';
 import '../pages/view-ledger.js';
 import '../pages/view-members.js';
+import '../pages/view-showcase.js';
+import './sample-shell.js';
+
+const SAMPLE_ROUTES = new Set(['cockpit', 'intel', 'coop', 'review', 'class']);
 
 const TITLES = {
-  cockpit: ['创新驾驶舱', '整场创新现在怎么样 · 谁卡住了'],
-  intel: ['情报与洞察', '我们掌握了什么证据 · 覆盖到哪些场景'],
-  coop: ['产品共创工作台', '机会怎么变成项目 · 谁在推进 · 做到哪了'],
-  review: ['验证与评审', '结论站得住吗 · 缺什么证据'],
-  class: ['课堂运营与成果', '这两天跑得怎么样 · 各阶段交出什么'],
   workbench: ['工作台', ''],
   pool: ['机会卡池', ''],
   mode: ['机会卡', ''],
@@ -63,7 +56,6 @@ export class KfApp extends SignalWatcher(LitElement) {
     this._route = { name: 'workbench', params: {}, path: '/' };
     this._query = {};
     this._drawer = null; // { type: 'opp'|'evidence'|'artifact'|'insight', id, evId?, reportId? }
-    this._sampleModel = null; // 模拟页的模型(A—E 五端),抽屉优先用它查卡
   }
 
   connectedCallback() {
@@ -72,7 +64,9 @@ export class KfApp extends SignalWatcher(LitElement) {
     this.#unsub = subscribeRoute(({ name, params, path, query }) => {
       this._route = { name, params, path };
       this._query = query;
-      this.#syncDrawerFromRoute(name, params, query);
+      if (name === 'showcase' || SAMPLE_ROUTES.has(name)) this._drawer = null;
+      else this.#syncDrawerFromRoute(name, params, query);
+      this.#syncLivePolling();
       // 换页回到顶部;只改查询串(筛选 / 当前章 / 抽屉)不跳(由最近一次触发者自行滚动)
       const raw = (path || '/').split('?')[0];
       const key = raw.replace(/^\/pool\/[^/]+$/, '/pool/:id').replace(/^\/reports\/[^/]+$/, '/reports/:type').replace(/^\/report\/[^/]+$/, '/report/:id');
@@ -88,10 +82,6 @@ export class KfApp extends SignalWatcher(LitElement) {
       'opportunity-open', 'report-open', 'ledger-jump', 'me-change', 'refresh', 'nav-close']) {
       this.addEventListener(t, this.#boundAction);
     }
-    this.#poll = setInterval(() => {
-      if (document.visibilityState === 'visible') boardService.load({ force: true }).catch(() => {});
-    }, CONFIG.refreshMs);
-    boardService.load().catch(() => {});
     // 入场错峰(任务书 2.6):进入视口的 .kf-enter 才播放;已在首屏的立即播放
     this.#io = new IntersectionObserver((entries) => {
       for (const e of entries) if (e.isIntersecting) { e.target.removeAttribute('data-enter'); this.#io.unobserve(e.target); }
@@ -139,6 +129,14 @@ export class KfApp extends SignalWatcher(LitElement) {
 
   /** 新渲染出来的 .kf-enter:屏外的先挂 pending,滚到时再播 */
   updated() {
+    if (this._route.name === 'showcase' || SAMPLE_ROUTES.has(this._route.name)) return;
+    // 旧卡片把报告链接写成 /pool/:artifactId;兼容已有书签,避免误报机会卡被删除。
+    const id = this._route.params?.id;
+    const db = this.#svc.db.get();
+    if (this._route.name === 'mode' && db?.artifactById[id] && !db.opportunityById[id]) {
+      navigate(`/report/${encodeURIComponent(id)}`, { replace: true });
+      return;
+    }
     this.#renderDrawerBody();
     this.#armEntrance();
   }
@@ -169,6 +167,7 @@ export class KfApp extends SignalWatcher(LitElement) {
       this.removeEventListener(t, this.#boundAction);
     }
     clearInterval(this.#poll);
+    this.#liveActive = false;
     this.#io?.disconnect();
   }
 
@@ -176,6 +175,27 @@ export class KfApp extends SignalWatcher(LitElement) {
   #poll = null;
   #prevKey = null;
   #boundAction = null;
+  #liveActive = false;
+
+  /** Introduction pages are static/sample-only; live polling resumes on existing routes. */
+  #syncLivePolling() {
+    if (this._route.name === 'showcase' || SAMPLE_ROUTES.has(this._route.name)) {
+      clearInterval(this.#poll);
+      this.#poll = null;
+      this.#liveActive = false;
+      return;
+    }
+    if (this.#liveActive) return;
+    this.#liveActive = true;
+    boardService.load().catch(() => {});
+    if (CONFIG.refreshMs > 0) {
+      this.#poll = setInterval(() => {
+        if (this._route.name !== 'showcase' && !SAMPLE_ROUTES.has(this._route.name) && document.visibilityState === 'visible') {
+          boardService.load({ force: true }).catch(() => {});
+        }
+      }, CONFIG.refreshMs);
+    }
+  }
 
   #syncDrawerFromRoute(name, params, query) {
     if (name === 'mode') this._drawer = { type: 'opp', id: params.id };
@@ -193,6 +213,8 @@ export class KfApp extends SignalWatcher(LitElement) {
    * 箭头函数字段会在 super() 之前初始化,event 选项读字段时可能拿到 undefined,交互事件会静默丢失。
    */
   async #onAction(e) {
+    if (this._route.name === 'showcase' || SAMPLE_ROUTES.has(this._route.name)) return;
+    if (/^(DEMO-|SIM-)/.test(String(e.detail?.id || ''))) return;
     const t = e.target;
     const detail = e.detail || {};
     switch (e.type) {
@@ -203,9 +225,6 @@ export class KfApp extends SignalWatcher(LitElement) {
       case 'opportunity-open':
         this._drawer = { type: 'opp', id: detail.id };
         break;
-      case 'sample-model':
-        this._sampleModel = detail.model;
-        break;
       case 'evidence-open':
         this._drawer = { type: 'evidence', reportId: detail.reportId, evId: detail.evId };
         break;
@@ -219,11 +238,7 @@ export class KfApp extends SignalWatcher(LitElement) {
         navigate(`/report/${encodeURIComponent(detail.id)}`);
         break;
       case 'status-change': {
-        const me = this.#svc.me.get();
-        if (!me) {
-          this.#svc.notify('warn', '先在右上角选「我是谁」,改状态要记录操作人');
-          break;
-        }
+        const me = this.#svc.me.get() || '匿名访客';
         try {
           await this.#svc.setOpportunityStatus(detail.id, detail.status, { by: me, reason: detail.reason || '' });
           if (this._drawer?.type === 'opp') this._drawer = { ...this._drawer };
@@ -234,7 +249,7 @@ export class KfApp extends SignalWatcher(LitElement) {
       }
       case 'score-submit': {
         try {
-          await this.#svc.rateOpportunity(this._currentOppId(), detail);
+          await this.#svc.rateOpportunity(this.#currentOppId(), detail);
           if (this._drawer?.type === 'opp') this._drawer = { ...this._drawer };
         } catch (err) {
           this.#svc.notify('err', `评分失败:${err.message}`);
@@ -264,61 +279,13 @@ export class KfApp extends SignalWatcher(LitElement) {
   }
 
   /** 抽屉内容:机会卡 / 证据链 / 成果链 / 归纳链 */
-  /**
-   * 模拟卡片的抽屉内容。
-   * 与线上抽屉同构(标题 / 一句话 / 阶段 / 决策 / 评分),但:
-   *   - 数据来自 sample-data,不查线上库;
-   *   - 明确标注「模拟」,写操作交给 sampleWriter(只落本地命名空间)。
-   */
-  #sampleDrawer(opp) {
-    const S = { candidate: '候选', approved: '已立项', rejected: '已否决', merged: '已合并', hold: '暂缓' };
-    return {
-      heading: opp.title,
-      sub: t([opp.stageCode, S[opp.status] || opp.status, opp.usageScenario].filter(Boolean).join(' · ')),
-      body: html`
-        <div class="d-sec">
-          ${opp.oneLiner ? html`<div class="one">${opp.oneLiner}</div>` : ''}
-          <div class="chips">
-            <kf-badge kind="sample" label="模拟数据"></kf-badge>
-            ${opp.stageCode ? html`<kf-badge kind="stage" .value=${opp.stageCode} label=${opp.stageCode}></kf-badge>` : ''}
-          </div>
-        </div>
-        <div class="d-sec">
-          <h4>决策与状态</h4>
-          <div class="st-row" data-no-activate>
-            <span class="kf-btn primary">${S[opp.status] || opp.status}</span>
-          </div>
-          <p class="hint">模拟页的状态只作展示,不写回线上库。</p>
-        </div>
-        ${opp.stats ? html`
-          <div class="d-sec">
-            <h4>评分</h4>
-            <div>平均 <b>${opp.stats.avg}</b> 分 · ${opp.stats.n} 人评(最低 ${opp.stats.min} / 最高 ${opp.stats.max})</div>
-          </div>` : html`
-          <div class="d-sec"><h4>评分</h4><p class="hint">这张卡还没有评分,字段为空,未补 0。</p></div>`}
-        <div class="d-sec">
-          <h4>场景与受众</h4>
-          <div>使用场景:${opp.usageScenario || '待补场景(该字段尚未填写)'}</div>
-          ${opp.targetAudience ? html`<div>目标人群:${opp.targetAudience}</div>` : ''}
-          ${opp.claimKind === 'hypothesis' ? html`<div class="hint">本条为假设,尚未经过验证。</div>` : ''}
-        </div>`,
-    };
-  }
-
   #drawerContent() {
     const db = this.#svc.db.get();
     const d = this._drawer;
-    if (!d) return null;
-    // 模拟页:卡片来自 sample-data(DEMO-* 不在线上库里),抽屉也要能查到
-    const sm = getCurrentSampleModel();
-    const smOpp = sm?.opportunityById?.[d.id];
-    if (d.type === 'opp' && smOpp && !db?.opportunityById?.[d.id]) {
-      return this.#sampleDrawer(smOpp);
-    }
-    if (!db) return null;
+    if (!db || !d) return null;
     if (d.type === 'opp') {
       const opp = db.opportunityById[d.id];
-      if (!opp) return { heading: '机会卡不存在', body: html`<kf-empty kind="error" title="难道这张卡已被删除?" hint=${`库里找不到 ${d.id}`}></kf-empty>` };
+      if (!opp) return { heading: '未找到机会卡', body: html`<kf-empty kind="error" title="未找到这张机会卡" hint=${`请刷新或返回机会卡池核对编号 ${d.id}`}></kf-empty>` };
       return {
         heading: opp.title,
         sub: t(`${opp.priority ? `${opp.priority} 级 · ` : ''}${opp.category} · ${opp.stage} · ${opp.statusLabel}`),
@@ -396,7 +363,9 @@ export class KfApp extends SignalWatcher(LitElement) {
                 ${q.author ? html`<span>· ${q.author}</span>` : ''}
                 ${q.art
                   ? html`<button class="lnk" @click=${() => this.#onAction({ type: 'artifact-open', detail: { id: q.art.id } })}>${t(`· 原文来自「${q.art.shortTitle}」(${q.art.ownerName})→`)}</button>`
-                  : html`<span>· 平台原文,未落入知识库</span>`}
+                  : q.sourceId
+                    ? html`<span>· 已关联原始资料</span>`
+                    : html`<span>· 未关联来源记录</span>`}
               </div>
             </li>`)}</ul>` : html`<div class="kf-muted">这条结论没有原文片段。</div>`}
           </div>
@@ -448,11 +417,6 @@ export class KfApp extends SignalWatcher(LitElement) {
     if (!db) return this.#loading();
     switch (r.name) {
       case 'workbench': return html`<view-workbench .db=${db} .me=${this.#svc.me.get()}></view-workbench>`;
-      case 'cockpit': return html`<view-cockpit></view-cockpit>`;
-      case 'intel': return html`<view-intel></view-intel>`;
-      case 'coop': return html`<view-coop></view-coop>`;
-      case 'review': return html`<view-review></view-review>`;
-      case 'class': return html`<view-class></view-class>`;
       case 'pool': return html`<view-pool .db=${db} .me=${this.#svc.me.get()} .routeQuery=${q}></view-pool>`;
       case 'mode': return html`<view-pool .db=${db} .me=${this.#svc.me.get()} .routeQuery=${q} .focusId=${r.params.id}></view-pool>`;
       case 'stages': return html`<view-stages .db=${db} .routeQuery=${q} .me=${this.#svc.me.get()}></view-stages>`;
@@ -481,6 +445,12 @@ export class KfApp extends SignalWatcher(LitElement) {
   }
 
   render() {
+    if (SAMPLE_ROUTES.has(this._route.name)) {
+      return html`<sample-shell .view=${this._route.name} .filters=${this._query}></sample-shell>`;
+    }
+    if (this._route.name === 'showcase') {
+      return html`<view-showcase .system=${this._route.params.system || 'overview'} .routeQuery=${this._query}></view-showcase>`;
+    }
     const [title, sub] = TITLES[this._route.name] || TITLES.notfound;
     const db = this.#svc.db.get();
     const drawer = this.#drawerContent();
@@ -572,7 +542,7 @@ export class KfApp extends SignalWatcher(LitElement) {
     @media (max-width: 767px) {
       .shell { grid-template-columns: minmax(0, 1fr); }
       .nav { position: fixed; left: 0; right: 0; bottom: 0; top: auto; height: auto; z-index: 30; }
-      .view { padding: 8px 16px calc(76px + env(safe-area-inset-bottom)); overflow-x: clip; }
+      .view { padding: 8px 16px calc(110px + env(safe-area-inset-bottom)); overflow-x: clip; }
       /* 手机上所有文字落在玻璃上都要看得清:原 14px 正文升到 15,行高 1.55 */
       .view { font-size: 14px; line-height: 1.55; }
       .one { font-size: 14px; line-height: 1.55; }

+ 6 - 2
src/layout/kf-nav.js

@@ -11,6 +11,7 @@ import { LitElement, html, css } from 'lit';
 import { useTokens, define } from '../components/base.js';
 import { icon } from '../components/icons.js';
 import { t } from '../components/cjk.js';
+import { href } from '../utils/router.js';
 
 const ITEMS = [
   { path: '/', label: '工作台', en: 'Workbench', icon: 'home' },
@@ -19,6 +20,7 @@ const ITEMS = [
   { path: '/report', label: '分析报告', en: 'Reports', icon: 'doc', short: '报告' },
   { path: '/ledger', label: '证据台账', en: 'Ledger', icon: 'ledger', mobileHidden: true },
   { path: '/members', label: '成员', en: 'Members', icon: 'users', badge: 'stuck' },
+  { path: '/showcase', label: '五端模拟演示', en: 'Innovation demo', icon: 'bars', mobileHidden: true },
 ];
 
 export const NAV_ITEMS = ITEMS;
@@ -107,6 +109,7 @@ export class KfNav extends LitElement {
         border-radius: 0 0 2px 2px; background: var(--kf-accent); }
       /* 移动 Tab 角标:不绝对定位出头(v10 实测悬在栏外 20px 处,形成一条悬空的小色块) */
       .tabbar .badge { position: static; margin: -4px 0 0 20px; min-width: 16px; height: 16px; padding: 0 4px; font-size: 12px; border-radius: var(--r-sm); }
+      .tabbar a.showcase-entry { grid-column: 1 / -1; height: 28px; font-size: 11px; color: var(--kf-accent-ink); border-top: 1px solid var(--kf-line-soft); margin-top: 4px; }
     }
     @media (min-width: 768px) { .tabbar { display: none; } }
   `;
@@ -131,7 +134,7 @@ export class KfNav extends LitElement {
           <div><div class="name">产品创新看板</div><div class="sub">${t('研发中心 · AI 实战营')}</div></div>
         </div>
         <nav>
-          ${ITEMS.map((it) => html`<a href=${it.path} data-link data-path=${it.path} aria-current=${curAria(it)}>
+          ${ITEMS.map((it) => html`<a href=${href(it.path)} data-link data-path=${it.path} aria-current=${curAria(it)}>
               ${icon(it.icon, 18)}
               <span class="lbl">${it.label}<span class="en">${it.en}</span></span>
               ${badge(it, '')}
@@ -143,11 +146,12 @@ export class KfNav extends LitElement {
         </div>
       </aside>
       <nav class="tabbar" aria-label="主导航(手机)" data-tabbar>
-        ${ITEMS.filter((it) => !it.mobileHidden).map((it) => html`<a href=${it.path} data-link data-path=${it.path} aria-current=${curAria(it)}
+        ${ITEMS.filter((it) => !it.mobileHidden).map((it) => html`<a href=${href(it.path)} data-link data-path=${it.path} aria-current=${curAria(it)}
             aria-label=${it.label}>
             <span class="ico">${icon(it.icon, 22, 'ln')}${icon(`${it.icon}Solid`, 22, 'sl')}${badge(it, '')}</span>
             <span class="short">${it.short || it.label}</span>
           </a>`)}
+        <a class="showcase-entry" href=${href('/showcase')} data-link>五端模拟演示与方案介绍 ↗</a>
       </nav>
     `;
   }

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 54 - 0
src/layout/sample-shell.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 89 - 113
src/pages/sample-base.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 82 - 0
src/pages/sample-detail.js


+ 86 - 0
src/pages/showcase-content.js

@@ -0,0 +1,86 @@
+/** Editorial copy for the six introduction pages; business numbers come from the seed. */
+export const SYSTEMS = [
+  {
+    key: 'leadership', letter: 'A', name: '创新驾驶舱', question: '现在怎么样,哪里需要支持?',
+    title: '把全员创意,变成看得清的产品组合。',
+    intro: '先看团队正在探索哪些人群和场景、哪些方向进入评估、哪些缺口需要协调,再展开具体产品的判断依据。',
+    audience: '业务负责人 · 产品负责人 · 教练', demoPath: '/cockpit',
+    preview: '项目组合、阶段分布、小组进度与受阻支持清单的设计预览',
+    features: [
+      ['看组合', '沿着产品方向看进度', '从创意到机会卡,再到有负责人和阶段成果的项目。数量来自同一快照,完成项与进行中分开。'],
+      ['看卡点', '支持清单能落到人', '把制备条件、证据缺口和跨组协作放到台面上;从卡点进入产品,再追到具体问题和所需材料。'],
+      ['看取舍', '留住判断的理由', '继续、调整、暂停都保留依据。评分缺少输入时显示待补,不用一个红绿灯代替解释。'],
+    ],
+    handoff: ['从共创工作台接收项目进度', '从验证评审接收待支持事项', '把资源安排送回小组与负责人'],
+    defaultCase: 'P03', related: ['collaboration', 'validation'],
+  },
+  {
+    key: 'research', letter: 'B', name: '情报与用户洞察', question: '我们掌握了什么依据?',
+    title: '让每张机会卡,都能回到原始依据。',
+    intro: '将用户声音、商品观察、工艺资料和团队经验放在一处。先核原话与使用条件,再看相反解释,资料可以被不同产品复用。',
+    audience: '研究人员 · 产品经理 · 资料采集者', demoPath: '/intel',
+    preview: '来源列表、逐字引文、场景覆盖与洞察归纳的设计预览',
+    features: [
+      ['留原件', '来源、原话与用途一起保存', '记录平台、定位与使用条件。摘要用于阅读,逐字原文用于复核,两者可以同时回查。'],
+      ['辨条件', '同一个词,可能指向不同需求', '“辣”可能是偏好,“加蛋”可能是习惯。人群标签和场景词典是整理工具,不能直接成为研究结论。'],
+      ['找缺口', '资料不足也进入任务队列', '把采集成功、失败、复用与待补区分记录;不同规格与组合装价格先统一比较口径。'],
+    ],
+    handoff: ['接收小组提出的研究问题', '把来源与引文关联到机会卡', '将支持材料与相反材料送入评审'],
+    defaultCase: 'P10', related: ['collaboration', 'validation'],
+  },
+  {
+    key: 'collaboration', letter: 'C', name: '产品共创工作台', question: '机会怎样变成项目,谁来接力?',
+    title: '多人同时做产品,每一步都有接力。',
+    intro: '围绕同一产品持续协作:成员知道自己负责什么,证据与方案版本跟着产品走,旧版本保留,下一位接手者能看见变化和原因。',
+    audience: '小组成员 · 项目负责人 · 研发协作者', demoPath: '/coop',
+    preview: '机会卡、项目分工、协作记录与方案版本的设计预览',
+    features: [
+      ['选方向', '从创意走到可讨论的机会卡', '写清目标人群、使用场景、候选方向和证据缺口。候选、选入项目与否决各自有明确状态。'],
+      ['做接力', '一个产品,串起多人工作', '同组成员共同推进,也允许跨组协作;资料可以复用,负责人和任务归属保持清楚。'],
+      ['留版本', '比较为什么改,不只比较字数', '串联项目基线、桌面评估和方案修订。AI辅助整理与草稿需要复核,不代表企业已经批准。'],
+    ],
+    handoff: ['从情报中心引用原始依据', '将方案与未决问题交给评审', '把阶段成果交到课堂成果墙'],
+    defaultCase: 'P01', related: ['research', 'validation', 'classroom'],
+  },
+  {
+    key: 'validation', letter: 'D', name: '验证与评审', question: '结论靠不靠得住,先补什么?',
+    title: '把有吸引力的想法,变成有条件的判断。',
+    intro: '同时看支持材料、相反材料和未知条件。制备、风味、规格、成本与工艺缺项摆在明处,下一步补什么、谁负责,都能说清。',
+    audience: '评审专家 · 研发人员 · 产品负责人', demoPath: '/review',
+    preview: '评审冲突、未决问题、验证范围与后续任务的设计预览',
+    features: [
+      ['看依据', '把证据与另一种解释并排', '原话能说明什么、不能说明什么,分别写清。证据不足时保留问题,不将未知直接归为否定。'],
+      ['看约束', '把使用条件带进设计', '无灶具与需煮面体、低气味与酱香强度,都是需要核实的条件。方案有吸引力也需要可行路径。'],
+      ['做判断', '决定与补证任务一起留下', '继续、调整、暂停附带理由、负责人和复核条件。未知规格保持空值,不编配方、成本或总分。'],
+    ],
+    handoff: ['接收共创方案和研究材料', '把补证任务送回小组', '把资源缺口与课堂反馈送往驾驶舱和教练'],
+    defaultCase: 'P03', related: ['research', 'collaboration', 'leadership'],
+  },
+  {
+    key: 'classroom', letter: 'E', name: '课堂运营与成果', question: '三个半天怎样跑,交了什么?',
+    title: '三个半天,每组都有清楚的产出。',
+    intro: '课表、作业、提交、点评与路演放在一条时间线上。教练按卡点安排支持,团队能回看每个产品从原始资料走到阶段成果的过程。',
+    audience: '课程运营 · 教练 · 学员 · 管理者', demoPath: '/class',
+    preview: '三个半天节奏、小组作业矩阵与报告成果墙的设计预览',
+    features: [
+      ['管节奏', '夜间采集承接第一半天', '准备研究问题和采集任务,第二半天验收资料并形成机会卡;夜间执行不是额外的第四个半天。'],
+      ['看作业', '交付物可以追到产品', '按小组、阶段和产品查看报告、点评与未决问题。文件数量不是研究质量的替代指标。'],
+      ['促落地', '路演结束仍有后续行动', '课堂完成保留待验证问题,并接续49天行动安排;完成课堂流程不等于产品验证或量产通过。'],
+    ],
+    handoff: ['从工作台接收阶段成果', '将教练反馈送往共创和评审', '向驾驶舱汇总各组进度与下一步'],
+    defaultCase: 'P01', related: ['collaboration', 'validation', 'leadership'],
+  },
+];
+
+export const COURSE = [
+  { tag: '第一半天', title: '建立共同的问题与研究方法', body: '市场与竞品图谱、假设清单、验证纪律。把创意拆成可分工的资料采集与研究任务。', output: '交付:市场分析、假设清单、夜间采集计划' },
+  { tag: '夜间任务', title: '带着问题搜集资料', body: '收集原文、商品规格与使用条件,保留来源和失败记录。夜间是执行任务,第二半天集中复核。', output: '带回:可追溯来源、引文与待补材料' },
+  { tag: '第二半天', title: '从声音走到机会与选题', body: '验收夜间资料、归纳用户声音、形成机会卡,选定产品项目并保存项目基线。', output: '交付:洞察、机会卡、项目S3基线' },
+  { tag: '第三半天', title: '评估、修订与产品路演', body: '完成S4桌面评估,处理冲突与缺项,形成S5概念和卖点,说明下一步验证条件。', output: '交付:评估、概念方案、路演与49天行动计划' },
+];
+
+export const CASES = [
+  { key: 'P01', id: 'DEMO-P01', title: '完整推进', summary: '从工位午餐的条件出发,沿着同一产品看机会、评估、方案版本与保留问题。', steps: [['ideas', '创意'], ['evidence', '原话与条件'], ['opportunities', '机会卡'], ['project', '项目'], ['reports', '阶段报告'], ['concepts', '方案版本'], ['questions', '完成后待验证']] },
+  { key: 'P03', id: 'DEMO-P03', title: '制备条件受阻', summary: '宿舍无灶具与面体需煮发生冲突,先明确约束,再落到所需材料与负责人。', steps: [['evidence', '使用条件'], ['project', '设计冲突'], ['questions', '补证与负责人'], ['tasks', '验证任务']] },
+  { key: 'P10', id: 'DEMO-P10', title: '证据不足,继续追问', summary: '加蛋加菜可能来自习惯,也可能与用餐目的有关;保留不同解释,继续补问。', steps: [['evidence', '已知原话'], ['project', '其他解释'], ['questions', '需要追问'], ['tasks', '下一步验证']] },
+];

+ 4 - 113
src/pages/view-class.js

@@ -1,114 +1,5 @@
-/**
- * pages/view-class.js —— E 端:课堂运营与成果(模拟演示)。
- * 回答:两天课堂跑得怎么样?各阶段交出什么?谁还没交?
- *
- * 口径说明(重要):
- *   种子里没有「成员提交记录」字段,所以不做「谁交/谁没交」的个人判断。
- *   真实可算的只有 reports(44 份,带 groupId + stageCode)与 assignments(18 条半场作业)。
- *   本页一律按组 × 阶段 统计,不编造个人进度。
- */
-import { css, html } from 'lit';
-import { section, VIEW_CSS } from './shared.js';
-import { SampleViewBase, SAMPLE_BADGE_CSS, FILTER_CSS } from './sample-base.js';
+import { SampleViewBase } from './sample-base.js';
+import { define } from '../components/base.js';
 
-const STAGES = [['S1','品类市场分析'],['S2','用户需求与痛点'],['S3','机会点识别'],['S4','机会验证评估'],['S5','场景与卖点设计']];
-const ASSIGN_STATE = { review_required: '待评审', done: '已完成', pending: '待启动' };
-
-export class ViewClass extends SampleViewBase {
-  static styles = [VIEW_CSS, SAMPLE_BADGE_CSS, FILTER_CSS, css`
-    .kpis { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--kf-space-in); }
-    .kpi { padding: 14px 16px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .kpi .v { font-size: 26px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .kpi .v .u { margin-left: 3px; font-size: 12px; font-weight: 400; color: var(--kf-ink-3); }
-    .kpi .l { margin-top: 4px; font-size: 13px; color: var(--kf-ink-2); }
-    .kpi .d { margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); }
-
-    .grs2 { display: flex; flex-direction: column; gap: 9px; }
-    .gr2 { display: grid; grid-template-columns: 132px minmax(0,1fr) 92px; gap: 12px; align-items: center; font-size: 13px; }
-    .gr2 .gn2 { color: var(--kf-ink-2); font-size: 12.5px; line-height: 1.35; }
-    .gr2 .gb2 { height: 9px; border-radius: 4px; background: var(--kf-surface-btn); overflow: hidden; }
-    .gr2 .gb2 i { display: block; height: 100%; background: var(--kf-accent); opacity: .8; }
-    .gr2 .gv2 { text-align: right; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; font-size: 12px; }
-
-    .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
-    .tbl th { text-align: left; font-weight: 600; font-size: 12px; color: var(--kf-ink-3); padding: 0 10px 8px; border-bottom: 1px solid var(--kf-line); }
-    .tbl td { padding: 8px 10px; border-bottom: 1px solid var(--kf-line); color: var(--kf-ink-2); }
-    .tbl tr:last-child td { border-bottom: 0; }
-    .tbl .tt { color: var(--kf-ink); font-weight: 500; }
-    .tag { display: inline-block; font-size: 11.5px; line-height: 17px; padding: 0 6px; border-radius: var(--r-sm); border: 1px solid var(--kf-line); color: var(--kf-ink-2); white-space: nowrap; }
-    .foot { margin-top: 8px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.6; }
-    @media (max-width: 1000px) { .kpis { grid-template-columns: repeat(2, minmax(0,1fr)); } .gr2 { grid-template-columns: 100px minmax(0,1fr) 78px; } }
-  `];
-
-  render() {
-    if (this.error) return html`<kf-empty title="模拟数据读取失败" hint=${this.error}></kf-empty>`;
-    const m = this.model;
-    if (!m) return html`<kf-empty title="正在读取模拟数据" hint="加载 334 条种子…"></kf-empty>`;
-
-    const G = m.groups.filter((g) => !this.fg || g.id === this.fg);
-    const RBG = m.reportsByGroup || {};
-    const RBS = m.reportsByStage || {};
-    const A = (m.assignments || []).filter((a) => !this.fg || a.groupId === this.fg);
-
-    const totalReports = Object.values(RBS).reduce((n, x) => n + x, 0);
-    const maxStage = Math.max(1, ...STAGES.map(([c]) => RBS[c] || 0));
-    const maxGrp = Math.max(1, ...G.map((g) => (RBG[g.name]?.total || RBG[g.id]?.total || 0)));
-
-    const doneAssign = A.filter((a) => a.state === 'done').length;
-
-    return html`
-      ${this.renderHead('课堂运营与成果', '这两天跑得怎么样 · 各阶段交出什么')}
-      ${this.renderBadge()}
-      ${this.renderFilter({ show: ['group'], count: `当前 ${G.length} 组 · ${totalReports} 份报告` })}
-
-      ${section('概览', '课堂规模')}
-      <div class="kpis">
-        <div class="kpi"><div class="v">${m.members.length}<span class="u">人</span></div><div class="l">学员</div><div class="d">${m.groups.length} 组</div></div>
-        <div class="kpi"><div class="v">${totalReports}<span class="u">份</span></div><div class="l">阶段报告</div><div class="d">五阶段累计</div></div>
-        <div class="kpi"><div class="v">${A.length}<span class="u">次</span></div><div class="l">半场作业</div><div class="d">${doneAssign} 已通过</div></div>
-        <div class="kpi"><div class="v">${(m.conceptVersions || []).length}<span class="u">版</span></div><div class="l">方案版本</div><div class="d">课堂产出</div></div>
-      </div>
-
-      ${section('五阶段交付', '每阶段报告份数')}
-      <div class="grs2">
-        ${STAGES.map(([code, label]) => html`
-          <div class="gr2">
-            <span class="gn2">${code} ${label}</span>
-            <span class="gb2"><i style="width:${((RBS[code] || 0) / maxStage) * 100}%"></i></span>
-            <span class="gv2">${RBS[code] || 0} 份</span>
-          </div>`)}
-      </div>
-      <div class="foot">分母为单阶段最多 ${maxStage} 份。S3 最多是因为组级 S3 报告与项目级 S3 基线同时计入。</div>
-
-      ${section('各组产出', '每组交出多少份报告')}
-      <div class="grs2">
-        ${G.map((g, i) => {
-          const rec = RBG[g.name] || RBG[g.id] || Object.entries(RBG).find(([k]) => k === g.name || k === g.id)?.[1] || { total: 0, stages: {} };
-          const stg = Object.keys(rec.stages || {}).sort().join(' ');
-          return html`
-            <div class="gr2">
-              <span class="gn2">${g.name}</span>
-              <span class="gb2"><i style="width:${((rec.total || 0) / maxGrp) * 100}%"></i></span>
-              <span class="gv2">${rec.total || 0} 份 · ${stg || '—'}</span>
-            </div>`;
-        })}
-      </div>
-      <div class="foot">右侧列出该组已产出的阶段编号。没有的分阶段就不列,不补。</div>
-
-      ${section('半场作业', `共 ${A.length} 次`)}
-      <table class="tbl">
-        <thead><tr><th>组</th><th>半场</th><th>交付类型</th><th>状态</th></tr></thead>
-        <tbody>
-          ${A.slice(0, 8).map((a) => html`<tr>
-            <td class="tt">${a.groupId}</td>
-            <td>D${a.halfDay}</td>
-            <td>${(a.deliverableTypes || []).join('、') || '—'}</td>
-            <td><span class="tag">${ASSIGN_STATE[a.state] || a.state || '—'}</span></td>
-          </tr>`)}
-        </tbody>
-      </table>
-      <div class="foot">显示 ${Math.min(8, A.length)} / ${A.length} 次。</div>`;
-  }
-}
-
-if (!customElements.get('view-class')) customElements.define('view-class', ViewClass);
+export class ViewClass extends SampleViewBase {}
+define('view-class', ViewClass);

+ 4 - 127
src/pages/view-cockpit.js

@@ -1,128 +1,5 @@
-/**
- * pages/view-cockpit.js —— A 端:创新驾驶舱(模拟演示)。
- * 回答:整场创新现在怎么样?谁卡住了?
- * 数据:sample-data.js(334 条种子),不读线上。
- */
-import { css, html } from 'lit';
-import { section, VIEW_CSS, link } from './shared.js';
-import { SampleViewBase, SAMPLE_BADGE_CSS, FILTER_CSS } from './sample-base.js';
-import { href } from '../utils/router.js';
+import { SampleViewBase } from './sample-base.js';
+import { define } from '../components/base.js';
 
-const STATE_LABEL = { in_progress: '进行中', blocked: '受阻', completed: '已完成' };
-
-export class ViewCockpit extends SampleViewBase {
-  static styles = [VIEW_CSS, SAMPLE_BADGE_CSS, FILTER_CSS, css`
-    .kpis { display: grid; grid-template-columns: repeat(5, minmax(0,1fr)); gap: var(--kf-space-in); }
-    .kpi { padding: 14px 16px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .kpi .v { font-size: 26px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .kpi .v .u { margin-left: 3px; font-size: 12px; font-weight: 400; color: var(--kf-ink-3); }
-    .kpi .l { margin-top: 4px; font-size: 13px; color: var(--kf-ink-2); }
-    .kpi .d { margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); }
-
-    .dist { display: flex; gap: 12px; }
-    .ds { flex: 1; padding: 12px 14px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .ds .dn { font-size: 12px; color: var(--kf-ink-3); }
-    .ds .dv { font-size: 22px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; margin-top: 2px; }
-    .ds .dd { font-size: 12px; color: var(--kf-ink-3); margin-top: 2px; }
-
-    .stages { display: grid; grid-template-columns: repeat(5, minmax(0,1fr)); gap: 10px; }
-    .sg { padding: 12px 13px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .sg .sc { font-size: 12px; color: var(--kf-ink-3); }
-    .sg .st { font-size: 13px; font-weight: 600; color: var(--kf-ink); margin-top: 2px; line-height: 1.35; }
-    .sg .sv { margin-top: 7px; font-size: 19px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .sg .sv .u2 { font-size: 11.5px; color: var(--kf-ink-3); font-weight: 400; margin-left: 2px; }
-    .bar2 { height: 5px; border-radius: 3px; background: var(--kf-surface-btn); margin-top: 7px; overflow: hidden; }
-    .bar2 i { display: block; height: 100%; background: var(--kf-accent); opacity: .78; }
-
-    .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
-    .tbl th { text-align: left; font-weight: 600; font-size: 12px; color: var(--kf-ink-3); padding: 0 10px 8px; border-bottom: 1px solid var(--kf-line); }
-    .tbl td { padding: 8px 10px; border-bottom: 1px solid var(--kf-line); color: var(--kf-ink-2); }
-    .tbl tr:last-child td { border-bottom: 0; }
-    .tbl .tt { color: var(--kf-ink); font-weight: 500; }
-    .tag { display: inline-block; font-size: 11.5px; line-height: 17px; padding: 0 6px; border-radius: var(--r-sm); border: 1px solid var(--kf-line); color: var(--kf-ink-2); white-space: nowrap; }
-    .tag.s-blocked { color: var(--kf-s); border-color: rgba(200,60,60,.3); }
-    .tag.s-completed { color: var(--kf-ok-text); border-color: rgba(40,140,90,.3); }
-    .foot { margin-top: 8px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.6; }
-    @media (max-width: 1000px) { .kpis, .stages { grid-template-columns: repeat(2, minmax(0,1fr)); } .dist { flex-direction: column; } }
-  `];
-
-  render() {
-    if (this.error) return html`<kf-empty title="模拟数据读取失败" hint=${this.error}></kf-empty>`;
-    const m = this.model;
-    if (!m) return html`<kf-empty title="正在读取模拟数据" hint="加载 334 条种子…"></kf-empty>`;
-
-    const f = this.filtered;
-    const P = f.projects;
-    const M = f.members;
-    const O = f.opportunities;
-
-    const inProg = P.filter((p) => p.progressState === 'in_progress').length;
-    const blocked = P.filter((p) => p.progressState === 'blocked').length;
-    const done = P.filter((p) => p.progressState === 'completed').length;
-
-    // 五阶段:按项目阶段计数
-    const stageRows = [['S1','品类市场分析'],['S2','用户需求与痛点'],['S3','机会点识别'],['S4','机会验证评估'],['S5','场景与卖点设计']]
-      .map(([code, label]) => ({ code, label, n: P.filter((p) => p.stageCode === code).length }));
-    const maxN = Math.max(1, ...stageRows.map((s) => s.n));
-
-    const stuckP = P.filter((p) => p.progressState === 'blocked');
-    const projRows = P.slice(0, 8).map((p) => html`
-      <tr>
-        <td class="tt">${p.title}</td>
-        <td>${p.stageCode || '—'}</td>
-        <td><span class="tag ${p.progressState === 'blocked' ? 's-blocked' : p.progressState === 'completed' ? 's-completed' : ''}">${STATE_LABEL[p.progressState] || '进行中'}</span></td>
-        <td>${p.ownerName || '—'}</td>
-      </tr>`);
-
-    const filterOn = this.fg || this.fm || this.fp;
-
-    return html`
-      ${this.renderHead('创新驾驶舱', '整场创新现在怎么样 · 谁卡住了')}
-      ${this.renderBadge()}
-      ${this.renderFilter({ count: filterOn ? `筛选后 ${P.length} / ${m.projects.length} 个项目` : `全部 ${m.projects.length} 个项目` })}
-
-      ${section('概览', '项目与成员')}
-      <div class="kpis">
-        <div class="kpi"><div class="v">${P.length}<span class="u">个</span></div><div class="l">项目</div><div class="d">基线 ${m.projects.length} 个</div></div>
-        <div class="kpi"><div class="v">${M.length}<span class="u">人</span></div><div class="l">成员</div><div class="d">${m.groups.length} 组</div></div>
-        <div class="kpi"><div class="v">${O.length}<span class="u">张</span></div><div class="l">机会卡</div><div class="d">入选 ${O.filter((o) => o.status === 'approved').length}</div></div>
-        <div class="kpi"><div class="v">${m.reports.length}<span class="u">份</span></div><div class="l">报告</div><div class="d">五阶段累计</div></div>
-        <div class="kpi"><div class="v">${stuckP.length}<span class="u">个</span></div><div class="l">受阻项目</div><div class="d">需现场裁决</div></div>
-      </div>
-
-      ${section('项目状态', '完成 / 进行中 / 受阻')}
-      <div class="dist">
-        <div class="ds"><div class="dn">进行中</div><div class="dv">${inProg} 个</div><div class="dd">正常推进</div></div>
-        <div class="ds"><div class="dn">受阻</div><div class="dv">${blocked} 个</div><div class="dd">需要裁决</div></div>
-        <div class="ds"><div class="dn">已完成</div><div class="dv">${done} 个</div><div class="dd">五阶段收口</div></div>
-      </div>
-
-      ${section('五阶段分布', '项目当前所处阶段')}
-      <div class="stages">
-        ${stageRows.map((s) => html`
-          <div class="sg">
-            <div class="sc">${s.code}</div>
-            <div class="st">${s.label}</div>
-            <div class="sv">${s.n}<span class="u2">个</span></div>
-            <div class="bar2"><i style="width:${(s.n / maxN) * 100}%"></i></div>
-          </div>`)}
-      </div>
-      <div class="foot">S5 ${m.projectStageCounts.S5 || 0} · S4 ${m.projectStageCounts.S4 || 0} · S3 ${m.projectStageCounts.S3 || 0}(种子阶段分布)。</div>
-
-      ${stuckP.length ? section('谁卡住了', `${stuckP.length} 个项目受阻`) : ''}
-      ${stuckP.length ? html`
-        <div class="foot" style="font-size:13px;line-height:1.9;color:var(--kf-ink-2)">
-          ${stuckP.map((p) => html`<div>· <b style="color:var(--kf-ink)">${p.title}</b>(${p.groupId || '未分组'},${p.stage.label && p.stage.label !== p.stageCode ? p.stageCode + ' ' + p.stage.label : p.stageCode})${p.knownRisk ? '卡在:' + p.knownRisk : '受阻原因待补'}。</div>`)}
-        </div>
-        <div class="foot">受阻原因取项目 knownRisk 原字段,未补写。</div>` : ''}
-
-      ${section('项目明细', `共 ${P.length} 个`)}
-      <table class="tbl">
-        <thead><tr><th>项目</th><th>阶段</th><th>状态</th><th>负责人</th></tr></thead>
-        <tbody>${projRows.length ? projRows : html`<tr><td colspan="4" style="color:var(--kf-ink-3)">当前筛选下没有项目。</td></tr>`}</tbody>
-      </table>
-      <div class="foot">显示 ${Math.min(8, P.length)} / ${P.length} 个项目。</div>`;
-  }
-}
-
-if (!customElements.get('view-cockpit')) customElements.define('view-cockpit', ViewCockpit);
+export class ViewCockpit extends SampleViewBase {}
+define('view-cockpit', ViewCockpit);

+ 4 - 119
src/pages/view-coop.js

@@ -1,120 +1,5 @@
-/**
- * pages/view-coop.js —— C 端:产品共创工作台(模拟演示)。
- * 回答:机会怎么变成项目?谁在推进?做到哪了?
- * 卡片点击派发 opportunity-open,由 kf-app 打开详情抽屉(与线上同一套抽屉)。
- */
-import { css, html } from 'lit';
-import { section, VIEW_CSS } from './shared.js';
-import { SampleViewBase, SAMPLE_BADGE_CSS, FILTER_CSS } from './sample-base.js';
+import { SampleViewBase } from './sample-base.js';
+import { define } from '../components/base.js';
 
-const STATUS_LABEL = { candidate: '候选', approved: '已立项', rejected: '已否决', merged: '已合并', hold: '暂缓' };
-
-export class ViewCoop extends SampleViewBase {
-  static styles = [VIEW_CSS, SAMPLE_BADGE_CSS, FILTER_CSS, css`
-    .kpis { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--kf-space-in); }
-    .kpi { padding: 14px 16px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .kpi .v { font-size: 26px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .kpi .v .u { margin-left: 3px; font-size: 12px; font-weight: 400; color: var(--kf-ink-3); }
-    .kpi .l { margin-top: 4px; font-size: 13px; color: var(--kf-ink-2); }
-    .kpi .d { margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); }
-
-    .funnel { display: flex; gap: 12px; }
-    .fs { flex: 1; padding: 12px 14px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .fs .fn { font-size: 12px; color: var(--kf-ink-3); }
-    .fs .fv { font-size: 22px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; margin-top: 2px; }
-    .fs .fd { font-size: 12px; color: var(--kf-ink-3); margin-top: 2px; }
-
-    .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(232px, 1fr)); gap: 12px; }
-    .cd { padding: 12px 14px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); cursor: pointer; transition: border-color .15s; }
-    .cd:hover, .cd:focus-visible { border-color: var(--kf-accent); outline: none; }
-    .cd .ct { font-size: 13.5px; font-weight: 600; color: var(--kf-ink); line-height: 1.42; }
-    .cd .cm { margin-top: 5px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.5; }
-    .cd .cf { margin-top: 8px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
-    .tag { display: inline-block; font-size: 11.5px; line-height: 17px; padding: 0 6px; border-radius: var(--r-sm); border: 1px solid var(--kf-line); color: var(--kf-ink-2); }
-    .tag.st-approved { color: var(--kf-ok-text); border-color: rgba(40,140,90,.3); }
-    .tag.st-rejected { color: var(--kf-s); border-color: rgba(200,60,60,.3); }
-    .sc { font-size: 11.5px; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; }
-
-    .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
-    .tbl th { text-align: left; font-weight: 600; font-size: 12px; color: var(--kf-ink-3); padding: 0 10px 8px; border-bottom: 1px solid var(--kf-line); }
-    .tbl td { padding: 8px 10px; border-bottom: 1px solid var(--kf-line); color: var(--kf-ink-2); }
-    .tbl tr:last-child td { border-bottom: 0; }
-    .tbl .tt { color: var(--kf-ink); font-weight: 500; }
-    .foot { margin-top: 8px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.6; }
-    @media (max-width: 1000px) { .kpis { grid-template-columns: repeat(2, minmax(0,1fr)); } .funnel { flex-direction: column; } }
-  `];
-
-  #open(id) {
-    this.dispatchEvent(new CustomEvent('opportunity-open', { detail: { id, scope: 'sample' }, bubbles: true, composed: true }));
-  }
-
-  render() {
-    if (this.error) return html`<kf-empty title="模拟数据读取失败" hint=${this.error}></kf-empty>`;
-    const m = this.model;
-    if (!m) return html`<kf-empty title="正在读取模拟数据" hint="加载 334 条种子…"></kf-empty>`;
-
-    const f = this.filtered;
-    const O = f.opportunities;
-    const P = f.projects;
-
-    const cnt = (s) => O.filter((o) => o.status === s).length;
-    const cand = cnt('candidate'), appr = cnt('approved'), rej = cnt('rejected') + cnt('merged');
-
-    // 三态各取代表,避免少数派被截断掉
-    const pick = (s, n) => O.filter((o) => o.status === s).slice(0, n);
-    const shown = [...pick('approved', 3), ...pick('candidate', 3), ...pick('rejected', 2)];
-    const list = shown.length ? shown : O.slice(0, 8);
-
-    const projRows = P.slice(0, 8).map((p) => html`
-      <tr>
-        <td class="tt">${p.title}</td>
-        <td>${p.stageCode || '—'}</td>
-        <td>${p.ownerName || '—'}</td>
-        <td>${p.usageScenario || '待补'}</td>
-      </tr>`);
-
-    return html`
-      ${this.renderHead('产品共创工作台', '机会怎么变成项目 · 谁在推进 · 做到哪了')}
-      ${this.renderBadge()}
-      ${this.renderFilter({ count: `筛选后 ${O.length} 张卡 / ${P.length} 个项目` })}
-
-      ${section('概览', '机会与项目')}
-      <div class="kpis">
-        <div class="kpi"><div class="v">${O.length}<span class="u">张</span></div><div class="l">机会卡</div><div class="d">全员池子</div></div>
-        <div class="kpi"><div class="v">${P.length}<span class="u">个</span></div><div class="l">项目</div><div class="d">已立项推进</div></div>
-        <div class="kpi"><div class="v">${(m.conceptVersions || []).length}<span class="u">版</span></div><div class="l">方案版本</div><div class="d">概念迭代</div></div>
-        <div class="kpi"><div class="v">${m.members.length}<span class="u">人</span></div><div class="l">参与成员</div><div class="d">${m.groups.length} 组</div></div>
-      </div>
-
-      ${section('选题结果', '候选 / 已立项 / 已否决 三种结果')}
-      <div class="funnel">
-        <div class="fs"><div class="fn">候选(待决策)</div><div class="fv">${cand} 张</div><div class="fd">尚未立项</div></div>
-        <div class="fs"><div class="fn">已立项</div><div class="fv">${appr} 张</div><div class="fd">已转项目</div></div>
-        <div class="fs"><div class="fn">已否决 / 合并</div><div class="fv">${rej} 张</div><div class="fd">含合并去重</div></div>
-      </div>
-
-      ${section('机会卡', '点卡片打开详情与决策')}
-      <div class="cards">
-        ${list.map((o) => html`
-          <div class="cd" @click=${() => this.#open(o.id)} @keydown=${(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.#open(o.id); } }} role="button" tabindex="0">
-            <div class="ct">${o.title}</div>
-            <div class="cm">${o.oneLiner || '暂无一句话说明'}</div>
-            <div class="cf">
-              <span class="tag st-${o.status}">${STATUS_LABEL[o.status] || o.status}</span>
-              ${o.stats ? html`<span class="sc">${o.stats.avg} 分 · ${o.stats.n} 人评</span>` : html`<span class="sc">暂无评分</span>`}
-              <span class="sc">${o.usageScenario || '待补场景'}</span>
-            </div>
-          </div>`)}
-      </div>
-      <div class="foot">显示 ${list.length} / ${O.length} 张(已立项 3 · 候选 3 · 已否决 2,三种结果各取样)。点击卡片打开详情抽屉,抽屉内的状态与评分操作只作用于模拟数据,不会写入线上。</div>
-
-      ${section('项目推进', `共 ${P.length} 个`)}
-      <table class="tbl">
-        <thead><tr><th>项目</th><th>阶段</th><th>负责人</th><th>使用场景</th></tr></thead>
-        <tbody>${projRows.length ? projRows : html`<tr><td colspan="4" style="color:var(--kf-ink-3)">当前筛选下没有项目。</td></tr>`}</tbody>
-      </table>
-      <div class="foot">显示 ${Math.min(8, P.length)} / ${P.length} 个项目。</div>`;
-  }
-}
-
-if (!customElements.get('view-coop')) customElements.define('view-coop', ViewCoop);
+export class ViewCoop extends SampleViewBase {}
+define('view-coop', ViewCoop);

+ 4 - 101
src/pages/view-intel.js

@@ -1,102 +1,5 @@
-/**
- * pages/view-intel.js —— B 端:情报与洞察(模拟演示)。
- * 回答:我们手里到底掌握了什么证据?来源 → 引文 → 机会卡覆盖。
- */
-import { css, html } from 'lit';
-import { section, VIEW_CSS } from './shared.js';
-import { SampleViewBase, SAMPLE_BADGE_CSS, FILTER_CSS } from './sample-base.js';
+import { SampleViewBase } from './sample-base.js';
+import { define } from '../components/base.js';
 
-const VERIFY_LABEL = { verified: '已核实', demo_only: '模拟数据', pending: '待核实', unverified: '未核实' };
-
-export class ViewIntel extends SampleViewBase {
-  static styles = [VIEW_CSS, SAMPLE_BADGE_CSS, FILTER_CSS, css`
-    .kpis { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--kf-space-in); }
-    .kpi { padding: 14px 16px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .kpi .v { font-size: 26px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .kpi .v .u { margin-left: 3px; font-size: 12px; font-weight: 400; color: var(--kf-ink-3); }
-    .kpi .l { margin-top: 4px; font-size: 13px; color: var(--kf-ink-2); }
-    .kpi .d { margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); }
-
-    .scenes { display: flex; flex-direction: column; gap: 8px; }
-    .scrow { display: grid; grid-template-columns: 132px minmax(0,1fr) 78px; gap: 12px; align-items: center; font-size: 13px; }
-    .scrow .scn { color: var(--kf-ink-2); font-size: 12.5px; line-height: 1.35; }
-    .scrow .scb { height: 9px; border-radius: 4px; background: var(--kf-surface-btn); overflow: hidden; }
-    .scrow .scb i { display: block; height: 100%; background: var(--kf-accent); opacity: .8; }
-    .scrow .scv { text-align: right; color: var(--kf-ink-3); font-variant-numeric: tabular-nums; font-size: 12px; }
-
-    .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
-    .tbl th { text-align: left; font-weight: 600; font-size: 12px; color: var(--kf-ink-3); padding: 0 10px 8px; border-bottom: 1px solid var(--kf-line); }
-    .tbl td { padding: 8px 10px; border-bottom: 1px solid var(--kf-line); color: var(--kf-ink-2); }
-    .tbl tr:last-child td { border-bottom: 0; }
-    .tbl .tt { color: var(--kf-ink); font-weight: 500; }
-    .tag { display: inline-block; font-size: 11.5px; line-height: 17px; padding: 0 6px; border-radius: var(--r-sm); border: 1px solid var(--kf-line); color: var(--kf-ink-2); white-space: nowrap; }
-    .tag.demo { color: #9a6b00; border-color: rgba(200,150,40,.44); }
-    .foot { margin-top: 8px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.6; }
-    @media (max-width: 1000px) { .kpis { grid-template-columns: repeat(2, minmax(0,1fr)); } .scrow { grid-template-columns: 100px minmax(0,1fr) 66px; } }
-  `];
-
-  render() {
-    if (this.error) return html`<kf-empty title="模拟数据读取失败" hint=${this.error}></kf-empty>`;
-    const m = this.model;
-    if (!m) return html`<kf-empty title="正在读取模拟数据" hint="加载 334 条种子…"></kf-empty>`;
-
-    const O = m.opportunities;
-    const SRC = m.sources;
-    const EV = m.evidence || [];
-    const ideaN = (m.ideas || []).length;
-
-    // 场景分布:分母为全部机会卡(统一公共比例尺)
-    const sceneMap = new Map();
-    for (const o of O) {
-      const k = o.usageScenario || '待补场景';
-      sceneMap.set(k, (sceneMap.get(k) || 0) + 1);
-    }
-    const scenes = [...sceneMap.entries()].sort((a, b) => b[1] - a[1]);
-    const missingScene = O.filter((o) => !o.usageScenario).length;
-
-    const evidenceSum = O.reduce((n, o) => n + (o.quoteCount || 0), 0);
-    const withScene = O.length - missingScene;
-
-    const srcRows = SRC.slice(0, 10).map((s) => html`
-      <tr>
-        <td class="tt">${s.title}</td>
-        <td>${s.platform || '未标注'}</td>
-        <td>${s.authorLabel || '—'}</td>
-        <td><span class="tag ${s.verificationState === 'demo_only' ? 'demo' : ''}">${VERIFY_LABEL[s.verificationState] || '—'}</span></td>
-      </tr>`);
-
-    return html`
-      ${this.renderHead('情报与洞察', '我们掌握了什么证据 · 覆盖到哪些场景')}
-      ${this.renderBadge()}
-
-      ${section('概览', '证据规模')}
-      <div class="kpis">
-        <div class="kpi"><div class="v">${ideaN}<span class="u">条</span></div><div class="l">模拟创意</div><div class="d">已形成 ${O.length} 张机会卡</div></div>
-        <div class="kpi"><div class="v">${SRC.length}<span class="u">条</span></div><div class="l">来源</div><div class="d">全部为模拟数据</div></div>
-        <div class="kpi"><div class="v">${EV.length}<span class="u">条</span></div><div class="l">证据条目</div><div class="d">可追溯到来源</div></div>
-        <div class="kpi"><div class="v">${evidenceSum}<span class="u">条</span></div><div class="l">机会卡引用</div><div class="d">${O.filter((o) => o.quoteCount > 0).length} 张有引用</div></div>
-      </div>
-
-      ${section('场景覆盖', `分母为全部 ${O.length} 张机会卡`)}
-      <div class="scenes">
-        ${scenes.map(([name, n]) => html`
-          <div class="scrow">
-            <span class="scn">${name}</span>
-            <span class="scb"><i style="width:${(n / O.length) * 100}%"></i></span>
-            <span class="scv">${n} 张</span>
-          </div>`)}
-      </div>
-      <div class="foot">
-        有场景 ${withScene} 张 · 待补场景 ${missingScene} 张(这 ${missingScene} 张卡的使用场景尚未填写,如实标为待补,未计入其他场景)。
-      </div>
-
-      ${section('来源', `共 ${SRC.length} 条`)}
-      <table class="tbl">
-        <thead><tr><th>来源</th><th>平台</th><th>作者</th><th>核实状态</th></tr></thead>
-        <tbody>${srcRows.length ? srcRows : html`<tr><td colspan="4" style="color:var(--kf-ink-3)">暂无来源。</td></tr>`}</tbody>
-      </table>
-      <div class="foot">全部 ${SRC.length} 条来源均为合成内容,并非真实访谈记录。显示 ${Math.min(10, SRC.length)} 条。</div>`;
-  }
-}
-
-if (!customElements.get('view-intel')) customElements.define('view-intel', ViewIntel);
+export class ViewIntel extends SampleViewBase {}
+define('view-intel', ViewIntel);

+ 4 - 3
src/pages/view-pool.js

@@ -182,8 +182,9 @@ export class ViewPool extends LitElement {
     this.#apply(c);
   }
 
-  #changeStatus(e, id) {
-    this.dispatchEvent(new CustomEvent('status-change', { detail: { id, status: e.detail.status }, bubbles: true, composed: true }));
+  #changeStatus(e) {
+    e.stopPropagation();
+    this.dispatchEvent(new CustomEvent('status-change', { detail: e.detail, bubbles: true, composed: true }));
   }
 
   render() {
@@ -245,7 +246,7 @@ export class ViewPool extends LitElement {
 
       ${filtered.length ? html`<div class="cards kf-enter" data-list-ids=${filtered.map((o) => o.id).join(',')}>
         ${filtered.map((o) => html`<kf-opportunity-card key=${o.id} class=${o.id === this.focusId ? 'is-focus' : ''} .opp=${o} base="/pool"
-          @card-open=${(e) => this.dispatchEvent(new CustomEvent('card-open', { detail: e.detail, bubbles: true, composed: true }))}
+          @card-open=${(e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent('card-open', { detail: e.detail, bubbles: true, composed: true })); }}
           @status-change=${this.#changeStatus}></kf-opportunity-card>`)}
       </div>` : html`<kf-glass-card surface="2"><kf-empty title="没有符合条件的卡" hint="试试点上面的「清除全部条件」,或换一个排序看看。" action-label="清除全部条件" @empty-action=${() => navigate('/pool', { replace: true })}></kf-empty></kf-glass-card>`}
       ${/* 筛选栏已收起页脚(排序行末尾:本页条数 + 回到顶部),卡片流之后不再重复 */ ''}

+ 4 - 102
src/pages/view-review.js

@@ -1,103 +1,5 @@
-/**
- * pages/view-review.js —— D 端:验证与评审(模拟演示)。
- * 回答:这些结论站得住吗?缺什么证据?
- * 口径:证据不足写「证据不足以归因」,不写成否定结论;评分空着写「待补」,不补 0。
- */
-import { css, html } from 'lit';
-import { section, VIEW_CSS } from './shared.js';
-import { SampleViewBase, SAMPLE_BADGE_CSS, FILTER_CSS } from './sample-base.js';
+import { SampleViewBase } from './sample-base.js';
+import { define } from '../components/base.js';
 
-const LV_LABEL = { high: '强', medium: '中', low: '弱' };
-const TASK_STATE = { review_required: '待评审', done: '已完成', pending: '待启动' };
-const VERDICT = { approve: '立项', reject: '否决', hold: '暂缓', merge: '合并' };
-
-export class ViewReview extends SampleViewBase {
-  static styles = [VIEW_CSS, SAMPLE_BADGE_CSS, FILTER_CSS, css`
-    .kpis { display: grid; grid-template-columns: repeat(4, minmax(0,1fr)); gap: var(--kf-space-in); }
-    .kpi { padding: 14px 16px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .kpi .v { font-size: 26px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .kpi .v .u { margin-left: 3px; font-size: 12px; font-weight: 400; color: var(--kf-ink-3); }
-    .kpi .v .sm { font-size: 13px; font-weight: 400; color: var(--kf-ink-3); }
-    .kpi .l { margin-top: 4px; font-size: 13px; color: var(--kf-ink-2); }
-    .kpi .d { margin-top: 2px; font-size: 12px; color: var(--kf-ink-3); }
-
-    .lv { display: flex; gap: 12px; }
-    .lvc { flex: 1; padding: 12px 14px; border-radius: var(--r-md); border: 1px solid var(--kf-line); background: var(--kf-surface); }
-    .lvc .lh { display: flex; align-items: baseline; gap: 8px; }
-    .lvc .ln { font-size: 13px; font-weight: 600; color: var(--kf-ink); }
-    .lvc .lv2 { font-size: 20px; font-weight: 600; color: var(--kf-ink); font-variant-numeric: tabular-nums; }
-    .lvc .ld { margin-top: 4px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.55; }
-
-    .tbl { width: 100%; border-collapse: collapse; font-size: 13px; }
-    .tbl th { text-align: left; font-weight: 600; font-size: 12px; color: var(--kf-ink-3); padding: 0 10px 8px; border-bottom: 1px solid var(--kf-line); }
-    .tbl td { padding: 8px 10px; border-bottom: 1px solid var(--kf-line); color: var(--kf-ink-2); vertical-align: top; }
-    .tbl tr:last-child td { border-bottom: 0; }
-    .tbl .tt { color: var(--kf-ink); font-weight: 500; }
-    .tag { display: inline-block; font-size: 11.5px; line-height: 17px; padding: 0 6px; border-radius: var(--r-sm); border: 1px solid var(--kf-line); color: var(--kf-ink-2); white-space: nowrap; }
-    .foot { margin-top: 8px; font-size: 12px; color: var(--kf-ink-3); line-height: 1.6; }
-    @media (max-width: 1000px) { .kpis { grid-template-columns: repeat(2, minmax(0,1fr)); } .lv { flex-direction: column; } }
-  `];
-
-  render() {
-    if (this.error) return html`<kf-empty title="模拟数据读取失败" hint=${this.error}></kf-empty>`;
-    const m = this.model;
-    if (!m) return html`<kf-empty title="正在读取模拟数据" hint="加载 334 条种子…"></kf-empty>`;
-
-    const f = this.filtered;
-    const O = f.opportunities;
-    const Q = m.questions.filter((q) => !this.fp || q.projectId === this.fp);
-    const V = m.validationTasks.filter((v) => !this.fp || v.projectId === this.fp);
-
-    const lvH = O.filter((o) => o.evidenceLevel === 'high' || o.evidenceLevel === 'medium').length;
-    const lvMid = O.filter((o) => o.evidenceLevel === 'medium').length;
-    const lvLow = O.filter((o) => o.evidenceLevel === 'low').length;
-    const lvNone = O.filter((o) => !o.evidenceLevel).length;
-    const scored = O.filter((o) => o.stats && o.stats.n > 0).length;
-
-    const openQ = Q.filter((q) => q.status === 'open').length;
-
-    const taskRows = V.slice(0, 8).map((v) => html`
-      <tr>
-        <td class="tt">${v.title || '(未命名任务)'}</td>
-        <td>${v.projectId || '—'}</td>
-        <td><span class="tag">${TASK_STATE[v.state] || v.state || '—'}</span></td>
-      </tr>`);
-
-    return html`
-      ${this.renderHead('验证与评审', '结论站得住吗 · 缺什么证据')}
-      ${this.renderBadge()}
-      ${this.renderFilter({ count: `筛选后 ${O.length} 张卡 · ${V.length} 项验证任务` })}
-
-      ${section('概览', '验证与评审规模')}
-      <div class="kpis">
-        <div class="kpi"><div class="v">${V.length}<span class="u">项</span></div><div class="l">验证任务</div><div class="d">S4/S5 阶段</div></div>
-        <div class="kpi"><div class="v">${scored}<span class="sm"> / ${O.length}</span></div><div class="l">已有评分</div><div class="d">${O.length - scored} 张待补</div></div>
-        <div class="kpi"><div class="v">${openQ}<span class="u">条</span></div><div class="l">未决问题</div><div class="d">共 ${Q.length} 条</div></div>
-        <div class="kpi"><div class="v">${m.reports.length}<span class="u">份</span></div><div class="l">阶段报告</div><div class="d">评审依据</div></div>
-      </div>
-
-      ${section('证据等级', '强 / 中 / 弱 三级')}
-      <div class="lv">
-        <div class="lvc"><div class="lh"><span class="ln">强</span><span class="lv2">${lvH - lvMid}</span></div><div class="ld">有原文出处,来源可独立核对。</div></div>
-        <div class="lvc"><div class="lh"><span class="ln">中</span><span class="lv2">${lvMid}</span></div><div class="ld">有原文出处,但来源单一或无法交叉验证。</div></div>
-        <div class="lvc"><div class="lh"><span class="ln">弱</span><span class="lv2">${lvLow}</span></div><div class="ld">仅二手转述或综合印象,无原话可查。</div></div>
-      </div>
-      <div class="foot">未评级 ${lvNone} 张。等级按来源可核对性判定,不代表结论正确性;证据弱不等于结论错,只说明还不足以支撑决策。</div>
-
-      ${section('验证任务', `共 ${V.length} 项`)}
-      <table class="tbl">
-        <thead><tr><th>任务</th><th>所属项目</th><th>状态</th></tr></thead>
-        <tbody>${taskRows.length ? taskRows : html`<tr><td colspan="3" style="color:var(--kf-ink-3)">当前筛选下没有验证任务。</td></tr>`}</tbody>
-      </table>
-      <div class="foot">显示 ${Math.min(8, V.length)} / ${V.length} 项。</div>
-
-      ${section('证据缺口', '这是最该优先补的三件事')}
-      <div class="foot" style="font-size:13px;line-height:1.9;color:var(--kf-ink-2)">
-        1. <b style="color:var(--kf-ink)">${openQ} 条</b>未决问题还没有答案,卡在证据不足,不是卡在判断。<br>
-        2. <b style="color:var(--kf-ink)">${O.length - scored} 张</b>机会卡尚无评分,无法横向比较优先级。<br>
-        3. 所有机会点目前均为假设:现有证据只能支撑「值得试」,不能支撑「一定成」。
-      </div>`;
-  }
-}
-
-if (!customElements.get('view-review')) customElements.define('view-review', ViewReview);
+export class ViewReview extends SampleViewBase {}
+define('view-review', ViewReview);

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 264 - 0
src/pages/view-showcase.js


+ 25 - 16
src/services/board-service.js

@@ -12,8 +12,7 @@
  */
 import { signal } from '@lit-labs/signals';
 import { CONFIG } from '../app.config.js';
-import { createHttp } from '../utils/http.js';
-import { loadPublicBoard } from './workshop-data.js';
+import { loadPublicBoard, publicBoardAction } from './workshop-data.js';
 import { createStorage } from '../utils/storage.js';
 import {
   buildModel, forMe as coreForMe, STATUS_DECISION, statusPatch, scorePatch, scoreStats, toTime, oppStatusOf,
@@ -28,7 +27,7 @@ export class BoardService {
   /** 当前「我是谁」——多人共用一台电脑时,每人可选自己的名字(只存本机) */
   me = signal('');
 
-  #http = createHttp({ base: CONFIG.parseBase, appId: CONFIG.appId });
+  #mutating = new Set();
   #store = createStorage('kf-board');
   #inflight = null;
 
@@ -85,21 +84,17 @@ export class BoardService {
    * actor = 当前操作人(页面传入,必填)。
    */
   async setOpportunityStatus(id, status, { by, reason = '' } = {}) {
+    if (this.#mutating.has(id)) return { changed: false };
     const db = this.db.get();
     const opp = db?.opportunityById[id];
     if (!opp) throw new Error('机会卡不存在');
     const r = statusPatch(opp, status, { by, reason, at: new Date().toISOString() });
     if (!r.changed) return { changed: false };
     const type = STATUS_DECISION[status];
-    const body = {
-      status,
-      decision: r.entry,
-      decisionLog: { __op: 'AddUnique', objects: [r.entry] },
-    };
-    await this.#http.put(`/classes/${CONFIG.classes.opportunity}/${encodeURIComponent(id)}`, body);
+    const saved = await this.#writeOpportunity('ksfSetPublicOpportunityStatus', opp, { status, by, reason });
     // 局部合并:不阻塞用户,后台再整表刷新拿到服务器最新形态
     this.#patchOpportunity(id, (o) => ({ ...o, status, statusLabel: oppStatusOf(status).label,
-      decision: r.entry, decisionLog: [...o.decisionLog, r.entry] }));
+      decision: saved.decision, decisionLog: saved.decisionLog || [], updatedAt: saved.updatedAt }));
     this.notify('ok', `已把「${opp.title}」改为「${r.entry.label}」`);
     this.load({ force: true }).catch(() => {});
     return { changed: true, type, entry: r.entry };
@@ -107,19 +102,33 @@ export class BoardService {
 
   /** 追加评分:同一人重评覆盖自己上一次 */
   async rateOpportunity(id, { user, score, comment = '' }) {
+    if (this.#mutating.has(id)) return null;
     const db = this.db.get();
     const opp = db?.opportunityById[id];
     if (!opp) throw new Error('机会卡不存在');
-    const { scores } = scorePatch(opp, { user, score, comment, at: new Date().toISOString() });
-    await this.#http.put(`/classes/${CONFIG.classes.opportunity}/${encodeURIComponent(id)}`, {
-      scores: scores.map((s) => ({ user: s.user, score: s.score, comment: s.comment, at: s.at })),
-    });
-    const stats = scoreStats(scores, toTime);
-    this.#patchOpportunity(id, (o) => ({ ...o, scores: stats.scores, stats }));
+    scorePatch(opp, { user, score, comment, at: new Date().toISOString() });
+    const saved = await this.#writeOpportunity('ksfRatePublicOpportunity', opp, { by: user, score: Number(score), comment });
+    const stats = scoreStats(saved.scores, toTime);
+    this.#patchOpportunity(id, (o) => ({ ...o, scores: stats.scores, stats, updatedAt: saved.updatedAt }));
     this.notify('ok', `已记录 ${user} 对「${opp.title}」的评分 ${score}`);
     return stats;
   }
 
+  async #writeOpportunity(name, opp, params) {
+    this.#mutating.add(opp.id);
+    try {
+      const requestId = 'public-board:' + crypto.randomUUID();
+      const result = await publicBoardAction({ url: CONFIG.functionsUrl, appId: CONFIG.appId, name,
+        params: { ...params, opportunityId: opp.id, expectedUpdatedAt: opp.updatedAt, requestId } });
+      return result.opportunity;
+    } catch (error) {
+      if (error.code === 'VERSION_CONFLICT') this.load({ force: true }).catch(() => {});
+      throw error;
+    } finally {
+      this.#mutating.delete(opp.id);
+    }
+  }
+
   #patchOpportunity(id, fn) {
     const db = this.db.get();
     if (!db) return;

+ 59 - 204
src/services/sample-data.js

@@ -1,211 +1,66 @@
-/**
- * services/sample-data.js —— 五端模拟视图的独立数据服务(仅演示)。
- *
- * 隔离原则(重要):
- *   1. 数据源是 preview-seed.json(334 条基线),不读线上、不读生产库。
- *   2. 一切写入(改状态 / 评分 / 评论)只落在内存 + 独立 localStorage 命名空间
- *      `kf-sample::`,绝不调用 /classes/* 或 /api/functions。
- *   3. 与 board-service 暴露同名方法,页面可以复用同一套交互组件,
- *      但底层不是生产写路径 —— 这是为了「抽屉能点,但不会动线上数据」。
- *   4. 提供 resetSample() 一键还原,演示前可清场。
- */
-import { createHttp } from '../utils/http.js';
+import { request } from '../utils/http.js';
+import { buildSampleModel, changeSample, SAMPLE_NAMESPACE } from '../core/sample.js';
 
-// 种子是静态文件,但同样走 services 的统一网络出口(视图层不 fetch、services 不直连 fetch)
-const seedHttp = createHttp({ base: '', appId: 'kf-sample' });
-
-const SEED_URL = new URL('../../data/preview-seed.json', import.meta.url).href;
-const LS_KEY = 'kf-sample::mutations::v1';
 export const SAMPLE_DATASET_VERSION = 'ksf.innovation-preview-seed.v1';
 export const SAMPLE_BASELINE_COUNT = 334;
-
-let seedCache = null;
-let seeded = null;
-/** 模块级共享引用:视图加载后写这里,抽屉直接读,避免事件时序问题 */
-let currentModel = null;
-export function setCurrentModel(m) { currentModel = m; }
-export function getCurrentModel() { return currentModel; }
-
-/** 读种子(浏览器端缓存;失败抛错,不兜底造数据) */
-export async function loadSeed() {
-  if (seedCache) return seedCache;
-  seedCache = await seedHttp.get(SEED_URL);
-  return seedCache;
-}
-
-/** 本地改动(状态 / 评分 / 评论),命名空间隔离,可清空 */
-function readMutations() {
-  try { return JSON.parse(localStorage.getItem(LS_KEY) || '{}'); } catch { return {}; }
-}
-function writeMutations(m) {
-  localStorage.setItem(LS_KEY, JSON.stringify(m));
-}
-export function resetSample() {
-  localStorage.removeItem(LS_KEY);
-}
-export function hasSampleMutations() {
-  return Object.keys(readMutations()).length > 0;
-}
-
-/**
- * 把种子适配成界面模型(五端统一用这一份)。
- * 只做字段改名与派生,不改数量、不引入种子外的对象。
- */
-export function adaptSeed(seed) {
-  if (seeded) return seeded;
-  const mut = readMutations();
-
-  const groups = (seed.groups || []).map((g) => ({
-    id: g.id, name: g.name || g.id,
-    memberIds: g.memberIds || [], projectIds: g.projectIds || [],
-  }));
-
-  // 成员:种子里没有 stuck / stagesSubmitted 字段 —— 不造。
-  // 「交付了哪些阶段」由 reports 按 groupId + stageCode 实算(见下方 reportsByGroup)。
-  const members = (seed.members || []).map((m) => ({
-    id: m.id, name: m.displayName || m.name || m.id,
-    role: m.role || '', groupId: m.groupId || '',
-    aiFamiliarity: m.aiFamiliarity || '',
-  }));
-
-  // 机会卡:状态/评分以本地改动为优先(仅演示)
-  const opportunities = (seed.opportunities || []).map((o) => {
-    const patch = mut.opportunities?.[o.id] || {};
-    const scores = patch.scores || o.scores || [];
-    const n = scores.length;
-    const avg = n ? Math.round(scores.reduce((s, x) => s + (Number(x.score) || 0), 0) / n * 10) / 10 : null;
-    return {
-      id: o.id,
-      title: o.title || '(未命名)',
-      oneLiner: o.oneLiner || o.demandSummary || '',
-      demandSummary: o.demandSummary || '',
-      status: patch.status || o.status || 'candidate',
-      statusRaw: patch.status || o.status || 'candidate',
-      stageCode: o.stageCode || '',
-      category: o.category || '',
-      usageScenario: o.usageScenario || '',
-      targetAudience: o.targetAudience || '',
-      claimKind: o.claimKind || 'hypothesis',
-      priority: o.priority || '',
-      evidenceIds: o.evidenceIds || [],
-      quoteCount: o.quoteCount != null ? o.quoteCount : (o.evidenceIds || []).length,
-      citeCount: o.citeCount || 0,
-      projectId: o.projectId || '',
-      groupId: o.groupId || '',
-      ownerName: o.ownerName || '',
-      scores,
-      stats: n ? { n, avg, min: Math.min(...scores.map((s) => Number(s.score))), max: Math.max(...scores.map((s) => Number(s.score))), latest: scores[n - 1] } : null,
-      decision: o.decision || null,
-      decisionLog: o.decisionLog || [],
-    };
-  });
-
-  // 项目:阶段 / 状态用种子原值
-  const projects = (seed.projects || []).map((p) => ({
-    id: p.id,
-    title: p.title || p.name || '(未命名项目)',
-    groupId: p.groupId || '',
-    ownerMemberId: p.ownerMemberId || '',
-    ownerName: (seed.members || []).find((x) => x.id === p.ownerMemberId)?.displayName || '',
-    targetPrice: p.targetPrice || null,
-    enterpriseApproval: !!p.enterpriseApproval,
-    formalReportPublished: !!p.formalReportPublished,
-    stageCode: p.stageCode || '',
-    stage: { code: p.stageCode || '', label: p.stageLabel || p.stageCode || '' },
-    progressState: p.progressState || p.state || '',
-    stateLabel: p.stateLabel || '',
-    usageScenario: p.usageScenario || '',
-    knownRisk: p.knownRisk || '',
-    questionIds: p.questionIds || [],
-    artifactIds: p.artifactIds || [],
-    opportunityId: p.opportunityId || '',
-  }));
-
-  const sources = (seed.sources || []).map((s) => ({
-    id: s.id, title: s.title || '(未命名来源)',
-    platform: s.platform || '', authorLabel: s.authorLabel || '',
-    verificationState: s.verificationState || '', kind: s.kind || '',
-  }));
-
-  const evidence = seed.evidence || [];
-  const reports = (seed.reports || []).map((r) => ({
-    id: r.id, title: r.title || '', stageCode: r.stageCode || '',
-    projectId: r.projectId || '', groupId: r.groupId || '',
-    dataMode: r.dataMode || 'demo_only', version: r.version || 1,
-  }));
-
-  const questions = (seed.questions || []).map((q) => ({
-    id: q.id, text: q.text || q.title || '', projectId: q.projectId || '', status: q.status || 'open',
-  }));
-
-  const validationTasks = (seed.validationTasks || []).map((v) => ({
-    id: v.id, projectId: v.projectId || '', state: v.state || '', title: v.title || '',
-  }));
-
-  const actual = seed.actualSeedCounts || {};
-  const baselineCount = ['groups','members','ideas','sources','evidence','opportunities','projects','conceptVersions','reports','questions','validationTasks','assignments']
-    .reduce((n, k) => n + (Array.isArray(seed[k]) ? seed[k].length : 0), 0);
-
-  // 按组×阶段实算交付情况(种子无 stagesSubmitted,这是唯一的真实来源)
-  const reportsByGroup = {};
-  const reportsByStage = {};
-  for (const r of reports) {
-    const g = r.groupId || '未分组';
-    reportsByGroup[g] = reportsByGroup[g] || { total: 0, stages: {} };
-    reportsByGroup[g].total += 1;
-    reportsByGroup[g].stages[r.stageCode] = (reportsByGroup[g].stages[r.stageCode] || 0) + 1;
-    reportsByStage[r.stageCode] = (reportsByStage[r.stageCode] || 0) + 1;
+export const SAMPLE_STORAGE_KEY = SAMPLE_NAMESPACE;
+
+export function createSampleService({ transport = request, storage, now = () => new Date().toISOString() } = {}) {
+  let seed, extension, model, inflight;
+  const listeners = new Set();
+  let memory = { revision: 0 };
+  const store = () => storage === undefined ? globalThis.localStorage : storage;
+  function read() {
+    try {
+      const target = store();
+      if (!target) return memory;
+      const raw = target.getItem(SAMPLE_STORAGE_KEY);
+      if (!raw) return { revision: 0 };
+      const parsed = JSON.parse(raw);
+      if (!parsed || !Number.isSafeInteger(parsed.revision)) throw new Error('invalid revision');
+      return parsed;
+    } catch { return memory; }
   }
-
-  // 半场作业状态(assignments 真实字段)
-  const assignments = (seed.assignments || []).map((a) => ({
-    id: a.id, groupId: a.groupId || '', halfDay: a.halfDay || 1,
-    state: a.state || '', reportIds: a.reportIds || [],
-    deliverableTypes: a.deliverableTypes || [],
-  }));
-
-  seeded = {
-    datasetVersion: seed.version || SAMPLE_DATASET_VERSION,
-    reportsByGroup, reportsByStage, assignmentsReal: assignments,
-    baselineEntityCount: baselineCount,
-    groups, members, opportunities, projects, sources, evidence, reports, questions, validationTasks,
-    ideas: seed.ideas || [], conceptVersions: seed.conceptVersions || [], assignments: assignments,
-    projectStageCounts: seed.projectStageCounts || {}, projectStateCounts: seed.projectStateCounts || {},
-    actualSeedCounts: actual,
-    opportunityById: Object.fromEntries(opportunities.map((o) => [o.id, o])),
+  function notify() { for (const fn of listeners) fn(model); }
+  const service = {
+    get model() { return model; },
+    subscribe(fn) { listeners.add(fn); if (model) fn(model); return () => listeners.delete(fn); },
+    async load() {
+      if (model) return model;
+      if (inflight) return inflight;
+      inflight = (async () => {
+        const [baseResponse, extResponse] = await Promise.all([
+          transport(new URL('../assets/showcase/preview-seed.json', import.meta.url)),
+          transport(new URL('../assets/showcase/demo-extension.json', import.meta.url)),
+        ]);
+        if (!baseResponse.ok || !extResponse.ok) throw new Error('模拟资料读取失败,请重试');
+        seed = await baseResponse.json(); extension = await extResponse.json();
+        model = buildSampleModel(seed, read(), extension); notify(); return model;
+      })();
+      try { return await inflight; } finally { inflight = null; }
+    },
+    mutate(action, input, expectedRevision = model?.revision || 0) {
+      if (!model) throw new Error('模拟资料尚未加载');
+      const current = read();
+      if (expectedRevision !== current.revision) {
+        model = { ...buildSampleModel(seed, current, extension), revision: current.revision };
+        notify(); throw new Error('其他演示窗口已更新,请核对后重新操作');
+      }
+      const next = { ...changeSample(model, current, action, input, now()), revision: current.revision + 1 };
+      const target = store();
+      if (target) target.setItem(SAMPLE_STORAGE_KEY, JSON.stringify(next));
+      memory = next; model = { ...buildSampleModel(seed, next, extension), revision: next.revision };
+      notify(); return model;
+    },
+    reset() {
+      if (store()) store().removeItem(SAMPLE_STORAGE_KEY);
+      memory = { revision: 0 }; model = { ...buildSampleModel(seed, memory, extension), revision: 0 };
+      notify(); return model;
+    },
   };
-  return seeded;
-}
-
-/** 便捷:加载 + 适配 */
-export async function loadSampleModel() {
-  const seed = await loadSeed();
-  return adaptSeed(seed);
+  return service;
 }
 
-/**
- * 模拟写操作(与 board-service 同名同形,但只写本地命名空间)。
- * 调用方:详情抽屉在模拟模式下注入本对象,替代生产 boardService。
- */
-export const sampleWriter = {
-  mode: 'sample',
-  setOpportunityStatus(id, status, { by = '演示', reason = '' } = {}) {
-    const m = readMutations();
-    m.opportunities = m.opportunities || {};
-    m.opportunities[id] = { ...(m.opportunities[id] || {}), status, by, reason, at: new Date().toISOString() };
-    writeMutations(m);
-    seeded = null;
-    return { changed: true, scope: 'sample' };
-  },
-  rateOpportunity(id, { user = '演示', score, comment = '' }) {
-    const m = readMutations();
-    m.opportunities = m.opportunities || {};
-    const rec = m.opportunities[id] || {};
-    const scores = (rec.scores || []).filter((s) => s.user !== user);
-    scores.push({ user, score: Number(score), comment, at: new Date().toISOString() });
-    m.opportunities[id] = { ...rec, scores };
-    writeMutations(m);
-    seeded = null;
-    return { scope: 'sample', n: scores.length };
-  },
-};
+export const sampleService = createSampleService();
+export const loadSampleModel = () => sampleService.load();
+export const getCurrentModel = () => sampleService.model;

+ 20 - 0
src/services/showcase-data.js

@@ -0,0 +1,20 @@
+import { snapshot } from '../core/showcase.js';
+import { request } from '../utils/http.js';
+
+const seedUrl = new URL('../assets/showcase/preview-seed.json', import.meta.url);
+let ready = null;
+let inflight = null;
+
+/** Static sample data only. No production service imports or fallback. */
+export async function loadShowcase({ transport = request } = {}) {
+  if (ready) return ready;
+  if (inflight) return inflight;
+  inflight = (async () => {
+    const response = await transport(seedUrl);
+    if (!response.ok) throw new Error(`模拟快照读取失败(${response.status})`);
+    ready = snapshot(await response.json());
+    return ready;
+  })();
+  try { return await inflight; }
+  finally { inflight = null; }
+}

+ 15 - 1
src/services/workshop-data.js

@@ -1,5 +1,19 @@
+import { request } from '../utils/http.js';
+
+/** One audited action, no automatic retries after an uncertain write. */
+export async function publicBoardAction({ url, appId, name, params, fetchImpl = request }) {
+  const response = await fetchImpl(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId },
+    body: JSON.stringify({ path: '/' + name, params }), cache: 'no-store', signal: AbortSignal.timeout(30000) });
+  const result = (await response.json())?.result;
+  if (!response.ok || result?.ok !== true || !result.data) {
+    const error = result?.errors?.[0];
+    throw Object.assign(new Error(error?.message || '操作结果未确认,请刷新核对后再操作'), { code: error?.code || 'ACTION_UNCERTAIN', requestId: params.requestId });
+  }
+  return result.data;
+}
+
 /** Public reads require no account. Service credentials are never sent by this module. */
-export async function loadPublicBoard({url='/api/functions',appId,fetchImpl=(...args)=>fetch(...args)}) {
+export async function loadPublicBoard({url='/api/functions',appId,fetchImpl=request}) {
   const names=['project','artifact','insight','source','session','opportunity'];
   const raw=Object.fromEntries(names.map(n=>[n,[]]));raw.truncated=[];
   let cursor=null;

+ 2 - 0
src/utils/http.js

@@ -2,6 +2,8 @@
  * utils/http.js —— 唯一的网络出口(只有 services 调它;视图层不直接 fetch)。
  * 只带公开的 App ID;任何凭据都不进前端。
  */
+export const request = (...args) => fetch(...args);
+
 export class HttpError extends Error {
   constructor(status, body, url) {
     super(`HTTP ${status}${body ? ` · ${body}` : ''}`);

+ 7 - 3
src/utils/router.js

@@ -19,12 +19,14 @@
  *   /members                成员
  */
 export const ROUTES = [
-  { name: 'workbench', path: '/' },
   { name: 'cockpit', path: '/cockpit' },
   { name: 'intel', path: '/intel' },
   { name: 'coop', path: '/coop' },
   { name: 'review', path: '/review' },
   { name: 'class', path: '/class' },
+  { name: 'showcase', path: '/showcase' },
+  { name: 'showcase', path: '/showcase/:system' },
+  { name: 'workbench', path: '/' },
   { name: 'pool', path: '/pool' },
   { name: 'mode', path: '/pool/:id' },
   { name: 'stages', path: '/stages' },
@@ -167,11 +169,13 @@ export function setQuery(patch, { replace = true, force = false } = {}) {
 /** 内部链接点击的统一拦截(真实 <a> 保留「新标签打开」能力) */
 export function onLinkClick(e) {
   if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
-  const a = e.target instanceof Element ? e.target.closest('a[data-link]') : null;
+  // document 捕获阶段的 target 会被 Shadow DOM 重定向为组件宿主。
+  const a = e.composedPath?.().find((node) => node instanceof HTMLAnchorElement && node.matches('a[data-link]'))
+    || (e.target instanceof Element ? e.target.closest('a[data-link]') : null);
   if (!a) return;
   const url = new URL(a.href, location.href);
   if (url.origin !== location.origin) return;
   e.preventDefault();
   e.stopPropagation(); // 卡片内嵌链接:不触发外层卡片跳转
-  navigate(url.pathname + url.search + url.hash);
+  navigate(stripBase(url.pathname) + url.search + url.hash);
 }

+ 72 - 0
test/five-views-publish.test.js

@@ -0,0 +1,72 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import { publish, rollback, FILES, RELEASE } from '../scripts/publish-five-views.mjs';
+
+const sha = (b) => crypto.createHash('sha256').update(b).digest('hex');
+function setup(t) {
+  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-demo-publish-'));
+  t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
+  const sourceDir = path.join(dir, 'release'), targetDir = path.join(dir, 'site');
+  const put = (file, text) => { fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, text); };
+  const protectedFiles = ['src/services/board-service.js', 'src/components/kf-opportunity-card.js', 'src/pages/view-pool.js', 'src/core/report.js', 'src/core/evidence/chain.js'];
+  put(path.join(targetDir, 'index.html'), 'homepage');
+  for (const f of protectedFiles) put(path.join(targetDir, f), 'published-repair');
+  const files = FILES.map((f, i) => {
+    put(path.join(sourceDir, 'payload', f), 'new-' + i);
+    if (i < 4) put(path.join(targetDir, f), 'old-' + i);
+    return { path: f, sha256: sha('new-' + i), allowMissing: i >= 4, priorNormalizedSha256: i < 4 ? [sha('old-' + i)] : [] };
+  });
+  const manifestPath = path.join(sourceDir, 'manifest.json');
+  fs.writeFileSync(manifestPath, JSON.stringify({ version: 'ksf.five-views.publish.v1', release: RELEASE, homepageSha256: sha('homepage'), files,
+    preserved: protectedFiles.map((f) => ({ path: f, normalizedSha256: sha('published-repair') })) }));
+  return { sourceDir, targetDir, manifestPath, put };
+}
+test('release defaults to a read-only plan, applies once and can restore all prior files', (t) => {
+  const s = setup(t);
+  assert.equal(publish(s).changes.length, FILES.length);
+  assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'old-0');
+  const result = publish({ ...s, apply: true });
+  assert.equal(result.filesystemVerified, true);
+  assert.equal(publish({ ...s, apply: true }).changes.length, 0);
+  const args = { targetDir: s.targetDir, backupDir: result.backup };
+  assert.equal(rollback(args).readOnly, true);
+  assert.equal(rollback({ ...args, apply: true }).filesystemVerified, true);
+  assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'old-0');
+  assert.equal(fs.existsSync(path.join(s.targetDir, FILES[5])), false);
+  assert.equal(fs.readFileSync(path.join(s.targetDir, 'index.html'), 'utf8'), 'homepage');
+});
+test('unknown versions, homepage changes, protected repair changes and overlapping directories stop before writes', (t) => {
+  const s = setup(t);
+  s.put(path.join(s.targetDir, FILES[0]), 'third-party');
+  assert.throws(() => publish({ ...s, apply: true }), { code: 'UNKNOWN_TARGET_CONTENT' });
+  s.put(path.join(s.targetDir, FILES[0]), 'old-0');
+  s.put(path.join(s.targetDir, 'index.html'), 'other');
+  assert.throws(() => publish(s), { code: 'HOMEPAGE_MISMATCH' });
+  s.put(path.join(s.targetDir, 'index.html'), 'homepage');
+  s.put(path.join(s.targetDir, 'src/services/board-service.js'), 'other');
+  assert.throws(() => publish(s), { code: 'PUBLISHED_REPAIR_MISMATCH' });
+  assert.throws(() => publish({ ...s, sourceDir: s.targetDir }), { code: 'SOURCE_TARGET_OVERLAP' });
+});
+test('an interrupted publication rolls back completed files without overwriting concurrent edits', (t) => {
+  const s = setup(t); let caught;
+  try { publish({ ...s, apply: true }, { beforeWrite(name) { if (name === FILES[2]) { s.put(path.join(s.targetDir, name), 'concurrent'); } } }); }
+  catch (error) { caught = error; }
+  assert.equal(caught.code, 'CONCURRENT_CHANGE'); assert.equal(caught.rollbackComplete, true);
+  assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'old-0');
+  assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[2]), 'utf8'), 'concurrent');
+  assert.equal(JSON.parse(fs.readFileSync(path.join(caught.backup, 'journal.json'))).status, 'stopped');
+});
+test('rollback refuses a modified target or corrupted backup before restoring any file', (t) => {
+  const s = setup(t), result = publish({ ...s, apply: true });
+  const args = { targetDir: s.targetDir, backupDir: result.backup, apply: true };
+  s.put(path.join(s.targetDir, FILES[1]), 'someone-else');
+  assert.throws(() => rollback(args), { code: 'CONCURRENT_CHANGE' });
+  assert.equal(fs.readFileSync(path.join(s.targetDir, FILES[0]), 'utf8'), 'new-0');
+  s.put(path.join(s.targetDir, FILES[1]), 'new-1');
+  s.put(path.join(result.backup, FILES[0]), 'broken-backup');
+  assert.throws(() => rollback(args), { code: 'BACKUP_HASH_MISMATCH' });
+});

+ 24 - 0
test/report.test.js

@@ -53,6 +53,30 @@ test('结论块去重(同一证据锚点只出现一次)', () => {
   assert.equal(f[0].ownerCount, 3);
 });
 
+test('表格和段落小结的全部逐字引文都有证据入口,来源关系保留', () => {
+  const report = {
+    chapters: [{ title: '资料小结', sections: [{ kind: 'para', text: '资料范围' }, { kind: 'table', data: { cols: ['产品'], rows: [['拌面']] } }] }],
+    evidence: {
+      competitor: { sourceId: 'source-competitor', claim: '官网口味说明', quotes: [{ text: '官网原文' }] },
+      own: { sourceId: 'source-own', claim: '2022年品牌通告', quotes: [{ text: '品牌升级' }, { text: '辣味选择' }, { text: '滤水口' }] },
+    },
+  };
+  const model = buildModel({ artifact: [{ objectId: 'summary', report }], source: [] });
+  const entries = findingsOf(model, 'summary');
+  assert.deepEqual(entries.map(e => [e.evId, e.quoteCount]), [['competitor', 1], ['own', 3]]);
+  assert.ok(entries.every(e => e.ownerCount === null), '品牌文案不填消费者人数');
+  const own = reportEvidence(model, 'summary', 'own');
+  assert.ok(own.quotes.every(q => q.sourceId === 'source-own' && q.art === null));
+  assert.equal(reportOverview(model, 'summary').quoteCount, 4);
+  report.chapters[0].sections.push({ kind: 'findings', data: [{ conclusion: '原有判断', evidence: 'own' }] });
+  const mixed = findingsOf(model, 'summary');
+  assert.deepEqual(mixed.map(e => e.evId), ['own', 'competitor']);
+  assert.equal(mixed[0].conclusion, '原有判断');
+  assert.equal(mixed[1].conclusion, '官网口味说明');
+  const fallback = reportEvidence(buildModel({ artifact: [{ objectId: 'empty', report: { evidence: { x: { quotes: [{ text: '无来源' }] } } } }] }), 'empty', 'x');
+  assert.equal(fallback.quotes[0].sourceId, null);
+});
+
 test('章节归一:缺 id 用序号补齐', () => {
   const c = chapters({ chapters: [{ title: 'x' }, { id: 'y', title: 'z' }] });
   assert.equal(c[0].id, 'ch1');

+ 87 - 0
test/sample-flow.test.js

@@ -0,0 +1,87 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { buildSampleModel, filterSample, sampleDetail, validateExtension } from '../src/core/sample.js';
+import { snapshot } from '../src/core/showcase.js';
+import { createSampleService, SAMPLE_STORAGE_KEY } from '../src/services/sample-data.js';
+
+const seed = JSON.parse(readFileSync(new URL('../data/preview-seed.json', import.meta.url), 'utf8'));
+const extension = JSON.parse(readFileSync(new URL('../src/assets/showcase/demo-extension.json', import.meta.url), 'utf8'));
+const model = buildSampleModel(seed, {}, extension);
+const storage = () => { const values = new Map(); return { getItem: (k) => values.get(k) ?? null, setItem: (k, v) => values.set(k, v), removeItem: (k) => values.delete(k), values }; };
+const transport = async (url) => ({ ok: true, json: async () => String(url).includes('preview-seed') ? structuredClone(seed) : structuredClone(extension) });
+
+test('334 baseline records remain separate from 16446 rich synthetic extension records', () => {
+  assert.equal(model.baselineEntityCount, 334);
+  assert.equal(Object.values(model.extensionCounts).reduce((n, x) => n + x, 0), 16446);
+  assert.equal(new Set(extension.voices.map((v) => v.rawText)).size, 12000);
+  assert.equal(model.evidence.length + extension.citations.length, 360);
+  assert.equal(extension.prices.length, extension.observations.length * 4);
+  assert.ok(extension.prices.some((p) => p.unitPricePer100G == null));
+  assert.ok(extension.researchTasks.some((v) => v.state === 'failed' && v.failureReason));
+  for (const p of extension.prices.filter((p) => p.unitPricePer100G != null)) assert.ok(Math.abs(p.unitPricePer100G - p.pricePaid / p.packCount / p.netWeightG * 100) <= .0051);
+});
+
+test('extension validation rejects live data and unrelated references', () => {
+  const ext = structuredClone(extension);
+  ext.voices[0].dataMode = 'live';
+  assert.throws(() => validateExtension(ext, snapshot(seed)), /非模拟/);
+  ext.voices[0].dataMode = 'sample'; ext.citations[0].quote = '不在原文里';
+  assert.throws(() => validateExtension(ext, snapshot(seed)), /逐字/);
+});
+
+test('member, group and project filters agree across projects, reports, sources and cards', () => {
+  const member = 'DEMO-M31';
+  const f = filterSample(model, { member });
+  assert.ok(f.projects.some((p) => p.id === 'DEMO-P10'), 'cross-group collaborator participates');
+  const ids = new Set(f.projects.map((p) => p.id));
+  assert.ok(f.reports.every((r) => ids.has(r.projectId)));
+  assert.ok(f.opportunities.every((o) => ids.has(o.linkedProjectId)));
+  const g = filterSample(model, { group: 'DEMO-G2' });
+  assert.equal(g.projects.length, 2); assert.equal(g.opportunities.length, 4); assert.equal(g.voices.length, 2000);
+  assert.ok(g.reports.every((r) => r.groupId === 'DEMO-G2'));
+  assert.equal(filterSample(model, { group: 'DEMO-G1', project: 'DEMO-P03' }).projects.length, 0);
+  assert.equal(filterSample(model, { search: '完全无此关键词XYZ' }).voices.length, 0);
+});
+
+test('details preserve report, question and concept fields, with no live fallback', () => {
+  const p = sampleDetail(model, 'project', 'DEMO-P01');
+  assert.equal(p.questions.length, 6); assert.equal(p.concepts.length, 2); assert.equal(p.reports.length, 3);
+  assert.ok(p.questions.every((q) => q.question && q.requiredMaterial && q.ownerMemberId));
+  assert.equal(sampleDetail(model, 'project', 'DEMO-P10').concepts.length, 0);
+  assert.equal(sampleDetail(model, 'source', 'UNKNOWN'), null);
+  assert.ok(sampleDetail(model, 'source', extension.voices[0].id).row.rawText);
+});
+
+test('state, score replacement, comments and refresh remain in a separate namespace', async () => {
+  const store = storage(); store.setItem('kf-board:me', 'unchanged');
+  const service = createSampleService({ storage: store, transport, now: () => '2026-11-03T08:00:00Z' });
+  await service.load();
+  service.mutate('status', { id: 'DEMO-O01', status: 'candidate', user: '模拟甲' });
+  service.mutate('score', { id: 'DEMO-O01', user: '甲', score: 80 });
+  service.mutate('score', { id: 'DEMO-O01', user: '乙', score: 60 });
+  service.mutate('score', { id: 'DEMO-O01', user: '甲', score: 100 });
+  assert.equal(service.model.opportunitiesById['DEMO-O01'].stats.avg, 80);
+  assert.equal(service.model.opportunitiesById['DEMO-O01'].stats.n, 2);
+  service.mutate('comment', { id: 'DEMO-P01', user: '甲', body: '先核复水条件' });
+  const refreshed = createSampleService({ storage: store, transport }); await refreshed.load();
+  assert.equal(refreshed.model.opportunitiesById['DEMO-O01'].status, 'candidate');
+  assert.equal(refreshed.model.comments['DEMO-P01'][0].body, '先核复水条件');
+  service.reset();
+  assert.equal(service.model.opportunitiesById['DEMO-O01'].status, 'approved');
+  assert.equal(store.getItem('kf-board:me'), 'unchanged'); assert.equal(store.getItem(SAMPLE_STORAGE_KEY), null);
+});
+
+test('stale revisions, invalid scores and failed storage writes never report success', async () => {
+  const store = storage();
+  const a = createSampleService({ storage: store, transport }), b = createSampleService({ storage: store, transport });
+  await Promise.all([a.load(), b.load()]);
+  a.mutate('score', { id: 'DEMO-O01', user: '甲', score: 80 });
+  assert.throws(() => b.mutate('score', { id: 'DEMO-O01', user: '乙', score: 20 }, 0), /其他演示窗口/);
+  assert.equal(b.model.revision, 1);
+  assert.throws(() => a.mutate('score', { id: 'DEMO-O01', user: '甲', score: '' }), /0—100/);
+  assert.throws(() => a.mutate('status', { id: 'QE9hjtCj8E', status: 'approved', user: '甲' }), /模拟机会卡不存在/);
+  const failed = createSampleService({ storage: { ...store, setItem() { throw new Error('quota full'); } }, transport }); await failed.load();
+  assert.throws(() => failed.mutate('score', { id: 'DEMO-O01', user: '甲', score: 30 }), /quota full/);
+  assert.equal(failed.model.opportunitiesById['DEMO-O01'].stats.avg, 80);
+});

+ 85 - 0
test/showcase-sample.test.js

@@ -0,0 +1,85 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { snapshot, projectStory, sourceUnion, validateSample } from '../src/core/showcase.js';
+
+const seed = () => JSON.parse(readFileSync(new URL('../data/preview-seed.json', import.meta.url), 'utf8'));
+
+test('sample metrics use actual records, even when declared totals are stale', () => {
+  const data = seed();
+  data.actualSeedCounts.projects = 999;
+  const model = snapshot(data);
+  assert.equal(model.total, 334);
+  assert.equal(model.counts.projects, 12);
+  assert.deepEqual(model.stages, { S5: 4, S4: 6, S3: 2 });
+  assert.deepEqual(model.states, { completed: 2, in_progress: 7, blocked: 3 });
+  assert.equal(model.opportunityStates.approved, 12);
+});
+
+test('a live object or a second demo namespace is rejected', () => {
+  const data = seed();
+  data.projects[0].dataMode = 'live';
+  data.members[0].demoId = 'OTHER';
+  assert.ok(validateSample(data).some((x) => x.includes('不是模拟对象')));
+  assert.throws(() => snapshot(data), /同一演示空间/);
+});
+
+test('dangling references, duplicate records and mismatched quotes are rejected', () => {
+  const data = seed();
+  data.projects[0].sourceIds.push('MISSING');
+  data.members.push({ ...data.members[0] });
+  data.evidence[0].quote = '这不是原文';
+  const errors = validateSample(data);
+  assert.ok(errors.some((x) => x.includes('MISSING')));
+  assert.ok(errors.some((x) => x.includes('重复')));
+  assert.ok(errors.some((x) => x.includes('引文不在同源原文中')));
+});
+
+test('a malformed collection returns validation errors', () => {
+  const data = seed();
+  data.evidence = {};
+  assert.ok(validateSample(data).some((x) => x.includes('缺少集合:evidence')));
+});
+
+test('source union counts indirect references and deduplicates direct ones', () => {
+  const model = snapshot(seed());
+  const source = model.seed.sources[0];
+  const evidence = model.seed.evidence[0];
+  assert.deepEqual(sourceUnion([
+    { sourceIds: [source.id] }, { evidenceIds: [evidence.id] },
+    { sourceIds: [source.id], evidenceIds: [evidence.id] },
+  ], model.indexes).map((x) => x.id), [source.id]);
+});
+
+test('stories preserve project scope and empty later stages', () => {
+  const model = snapshot(seed());
+  const complete = projectStory(model, 'DEMO-P01');
+  assert.deepEqual(complete.reports.map((r) => r.stageCode), ['S3', 'S4', 'S5']);
+  assert.deepEqual(complete.concepts.map((c) => c.version), [1, 2]);
+  assert.equal(complete.questions.length, 6);
+  for (const id of ['DEMO-P01', 'DEMO-P03', 'DEMO-P10']) {
+    const story = projectStory(model, id);
+    for (const row of [...story.reports, ...story.questions, ...story.tasks, ...story.concepts]) assert.equal(row.projectId, id);
+    for (const row of story.ideas) assert.equal(row.intendedProjectId, id);
+  }
+  const insufficient = projectStory(model, 'DEMO-P10');
+  assert.equal(insufficient.project.stageCode, 'S3');
+  assert.equal(insufficient.concepts.length, 0);
+  assert.equal(insufficient.tasks.length, 0);
+  assert.equal(projectStory(model, 'MISSING'), null);
+});
+
+test('static service retries failures, shares inflight reads and never falls back', async () => {
+  const { loadShowcase } = await import('../src/services/showcase-data.js?retry-test');
+  const calls = [];
+  const failed = (url) => { calls.push(url.href); return Promise.resolve({ ok: false, status: 503 }); };
+  await assert.rejects(loadShowcase({ transport: failed }), /503/);
+  let resolve;
+  const transport = (url) => { calls.push(url.href); return new Promise((done) => { resolve = done; }); };
+  const first = loadShowcase({ transport });
+  const second = loadShowcase({ transport });
+  resolve({ ok: true, json: async () => seed() });
+  assert.strictEqual(await first, await second);
+  assert.equal(calls.length, 2);
+  assert.ok(calls.every((url) => url.endsWith('/src/assets/showcase/preview-seed.json')));
+});

+ 19 - 0
test/workshop-data.test.js

@@ -0,0 +1,19 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {loadPublicBoard,publicBoardAction} from '../src/services/workshop-data.js';
+import {buildModel} from '../src/core/kfs/build.js';
+const blank=()=>Object.fromEntries(['project','artifact','insight','source','session','opportunity'].map(k=>[k,[]]));
+test('公开操作只发一次,不带身份密钥;业务失败与未知写结果均不重试',async()=>{
+ const base={url:'/api/functions',appId:'public-id',name:'ksfSetPublicOpportunityStatus',params:{requestId:'same-request'}};
+ let calls=0;
+ const fetchImpl=async(_url,options)=>{calls++;assert.deepEqual(Object.keys(options.headers).sort(),['Content-Type','X-Parse-Application-Id']);return{ok:true,json:async()=>({result:{ok:false,errors:[{code:'VERSION_CONFLICT',message:'请刷新'}]}})};};
+ await assert.rejects(()=>publicBoardAction({...base,fetchImpl}),{code:'VERSION_CONFLICT',requestId:'same-request'});assert.equal(calls,1);
+ calls=0;await assert.rejects(()=>publicBoardAction({...base,fetchImpl:async()=>{calls++;throw Error('connection closed');}}),/connection closed/);assert.equal(calls,1);
+});
+test('公开查询无需身份凭据,分页合并,不用 REST count',async()=>{const requests=[];const raw=await loadPublicBoard({appId:'public-id',fetchImpl:async(url,options)=>{
+ requests.push({url,options});const params=JSON.parse(options.body).params;
+ return{ok:true,json:async()=>({result:{ok:true,data:{...blank(),artifact:[{objectId:params.cursor?'a2':'a1'}],nextCursor:params.cursor?null:'next',asOf:'now'}}})};
+ }});assert.deepEqual(raw.artifact.map(x=>x.objectId),['a1','a2']);assert.equal(requests.length,2);assert.ok(requests.every(x=>!Object.keys(x.options.headers).some(k=>/token|master|authorization/i.test(k))));});
+test('新版阶段/项目 Pointer 与 Agent 归属进入既有模型,显式卡住可见',()=>{const raw={...blank(),project:[{objectId:'p',title:'项目',stageCode:'S4',ownerName:'彭智康',uploadedByKey:'participant:one',progressState:'blocked'}],artifact:[{objectId:'a',title:'报告',stageCode:'S3',projectRef:{objectId:'p'},ownerName:'彭智康',uploadedByKey:'participant:one',agentName:'王刚001',agentId:'name:王刚001',uploadMethod:'session'}],session:[{objectId:'s',studentName:'彭智康',stageCode:'S4',groupId:'G1',meta:{participantKey:'participant:one'}}]};const db=buildModel(raw);assert.equal(db.artifacts[0].stageObj.code,'S3');assert.equal(db.artifacts[0].projectId,'p');assert.equal(db.artifacts[0].upload.agentName,'王刚001');assert.equal(db.projects[0].stage.code,'S4');assert.deepEqual(db.projects[0].artifactIds,['a']);assert.equal(db.memberByName['彭智康'].stuck,true);});
+test('同名学员不同稳定键不会合并为一个人',()=>{const raw={...blank(),session:[{objectId:'s1',studentName:'同名',groupId:'G1',meta:{participantKey:'participant:one'}},{objectId:'s2',studentName:'同名',groupId:'G2',meta:{participantKey:'participant:two'}}]};const db=buildModel(raw);assert.equal(db.members.length,2);assert.ok(db.memberByName['同名(G1)']);assert.ok(db.memberByName['同名(G2)']);});
+test('同组同名学员仍分开,并将项目归给正确参与人',()=>{const raw={...blank(),session:[{objectId:'s1',studentName:'同名',groupId:'G1',meta:{participantKey:'participant:00000001'}},{objectId:'s2',studentName:'同名',groupId:'G1',meta:{participantKey:'participant:00000002'}}],project:[{objectId:'p',ownerName:'同名',uploadedByKey:'participant:00000002'}]};const db=buildModel(raw);assert.equal(db.members.length,2);assert.deepEqual(db.memberByName['同名(G1 · 00000002)'].projectIds,['p']);assert.equal(db.memberByName['同名(G1 · 00000001)'].projectIds.length,0);});

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio