Преглед изворни кода

feat(board): align v3 output matrix and report presentation

pengzhikang пре 1 час
родитељ
комит
48c2134d2a
33 измењених фајлова са 3350 додато и 29 уклоњено
  1. 1215 0
      deploy/board-v3-20261112.json
  2. 73 0
      docs/board-v3-release-20261112/README.md
  3. 21 0
      docs/board-v3-release-20261112/baseline-summary.json
  4. 113 0
      docs/board-v3-release-20261112/browser-check.json
  5. BIN
      docs/board-v3-release-20261112/fixture-statistics-390.png
  6. 671 0
      docs/board-v3-release-20261112/local-simulation.json
  7. 168 0
      docs/board-v3-release-20261112/modules-local.json
  8. 15 0
      docs/board-v3-release-20261112/static-check.txt
  9. 28 0
      docs/board-v3-release-20261112/tests-deployment.txt
  10. 242 0
      docs/board-v3-release-20261112/tests-frontend.txt
  11. 28 0
      docs/board-v3-release-20261112/tests-staged-checkout.txt
  12. 138 0
      docs/board-v3-release-20261112/tests-workshop.txt
  13. BIN
      docs/board-v3-release-20261112/workshop-1440.png
  14. 19 0
      scripts/deploy-board-v3-20261112.sh
  15. 4 2
      scripts/preflight-mime-mjs.mjs
  16. 46 0
      scripts/prepare-board-v3-release.mjs
  17. 1 1
      scripts/publish-board-local.mjs
  18. 29 0
      scripts/publish-board-v3-20261112.mjs
  19. 102 0
      scripts/verify-board-v3-browser.mjs
  20. 2 2
      src/components/kf-opportunity-card.js
  21. 4 3
      src/components/kf-score-panel.js
  22. 36 0
      src/core/course-assessment.js
  23. 33 0
      src/core/report-statistics.js
  24. 3 1
      src/core/workshop-outputs.js
  25. 22 0
      src/pages/report-styles.js
  26. 14 9
      src/pages/view-pool.js
  27. 26 1
      src/pages/view-report.js
  28. 58 10
      src/pages/view-workshop-outputs.js
  29. 3 0
      test/board-ui-deploy.test.mjs
  30. 54 0
      test/board-v3-deploy.test.mjs
  31. 73 0
      test/report-statistics.test.mjs
  32. 44 0
      test/workshop-report-parity.test.js
  33. 65 0
      test/workshop-v3-outputs.test.js

+ 1215 - 0
deploy/board-v3-20261112.json

@@ -0,0 +1,1215 @@
+{
+  "version": "ksf.board-local.publish.v1",
+  "release": "ksf-board-v3-20261112-r2",
+  "origin": "https://kangshifu.fmode.cn/",
+  "baselineReadAt": "2026-11-12T07:20:32.302Z",
+  "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+  "files": [
+    {
+      "path": "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "sha256": "e3ffab45e9ed2d4b9e94532c4abf0ac670eab9b65993dd135e43c0939e835d0a",
+      "priorNormalizedSha256": [
+        "e3ffab45e9ed2d4b9e94532c4abf0ac670eab9b65993dd135e43c0939e835d0a"
+      ],
+      "priorSha256": "75732c4b7e53b4d3a8b3fc191da62cadb4e429a6af7d5895670125d17cde0559",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "sha256": "76e9815ec67d16684bff1444224bf62532d6318dbd8f25e7c1e9546158b1335f",
+      "priorNormalizedSha256": [
+        "76e9815ec67d16684bff1444224bf62532d6318dbd8f25e7c1e9546158b1335f"
+      ],
+      "priorSha256": "285f46c14f3e5b46ad72445adeffca1ec4d486073fc77912264d2fe00f3a1cf8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "sha256": "b878e7f95dec8a9b6e9b217faca6b6a11bad82ebc44c0caa77419ed83edd81f2",
+      "priorNormalizedSha256": [
+        "b878e7f95dec8a9b6e9b217faca6b6a11bad82ebc44c0caa77419ed83edd81f2"
+      ],
+      "priorSha256": "37c95a414752a6be74d2b7e2790c59c2ebb8315a5e5c00b5d30668ca89475c6b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "sha256": "b80a7e0d76893fc144d716031288944a44b77209c8e83f0cf80bbdaf60d4c0bb",
+      "priorNormalizedSha256": [
+        "b80a7e0d76893fc144d716031288944a44b77209c8e83f0cf80bbdaf60d4c0bb"
+      ],
+      "priorSha256": "3a84c7ca03d5da9adc55fa534d4497875e4147b4276dad0fe1019bb45cdbe911",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "sha256": "6c0296f91a07e1bc856787ed019b4ef593c6dbc0d4cb2b2733b89cf12e085f09",
+      "priorNormalizedSha256": [
+        "6c0296f91a07e1bc856787ed019b4ef593c6dbc0d4cb2b2733b89cf12e085f09"
+      ],
+      "priorSha256": "785d10c17acf092c4051381613a099512f8ac3f749527c1cc967a8fb91d8c0f5",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "sha256": "d38529a40a45d85efd03bff58256e92f9e77b3c3b5b3d70dea273b856b27dd79",
+      "priorNormalizedSha256": [
+        "d38529a40a45d85efd03bff58256e92f9e77b3c3b5b3d70dea273b856b27dd79"
+      ],
+      "priorSha256": "3329e8962f43edac6da98f5dc6d2c172e1cc97b8c7ee0b416428295825a052d2",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/index.js",
+      "sha256": "b1993a57ee9b162bc5af6b2f4bc44623c0bd3496be5119c83d5325b04093b65c",
+      "priorNormalizedSha256": [
+        "b1993a57ee9b162bc5af6b2f4bc44623c0bd3496be5119c83d5325b04093b65c"
+      ],
+      "priorSha256": "4781d9420f7087194a75ce450655e765f1a7be2c941d42fc004ff0f8ed35652f",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+      "sha256": "7200354296d2b605247ac0cd41927e55fd741f5dd29beb1387b92373f88df617",
+      "priorNormalizedSha256": [],
+      "priorSha256": null,
+      "allowMissing": true,
+      "changed": true
+    },
+    {
+      "path": "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "sha256": "4c6e05ca3f6e65d57d55f743de39307181f8427626e355a5d2698905c17dd8c4",
+      "priorNormalizedSha256": [
+        "4c6e05ca3f6e65d57d55f743de39307181f8427626e355a5d2698905c17dd8c4"
+      ],
+      "priorSha256": "4c6e05ca3f6e65d57d55f743de39307181f8427626e355a5d2698905c17dd8c4",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/app.config.js",
+      "sha256": "6383371210f0402ff6deb325dd4caf28881017dd9bbe7b79313c7996484e3c5c",
+      "priorNormalizedSha256": [
+        "6383371210f0402ff6deb325dd4caf28881017dd9bbe7b79313c7996484e3c5c"
+      ],
+      "priorSha256": "6383371210f0402ff6deb325dd4caf28881017dd9bbe7b79313c7996484e3c5c",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/app.js",
+      "sha256": "578670a3df6ce7b1690812754243af4b7ef5ed5a467e1c0bf3f3da65eff6d1cf",
+      "priorNormalizedSha256": [
+        "578670a3df6ce7b1690812754243af4b7ef5ed5a467e1c0bf3f3da65eff6d1cf"
+      ],
+      "priorSha256": "60c93012411fdbfc8f1846deeed5367cc07912a4199423d35487f6953903a7d0",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/assets/inter-latin.woff2",
+      "sha256": "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62",
+      "priorNormalizedSha256": [
+        "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62"
+      ],
+      "priorSha256": "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/base.js",
+      "sha256": "a4bcd43fb5506a9245bdc6b1d371aa742e2b71627cfc1119ecb97c5aceb7349d",
+      "priorNormalizedSha256": [
+        "a4bcd43fb5506a9245bdc6b1d371aa742e2b71627cfc1119ecb97c5aceb7349d"
+      ],
+      "priorSha256": "c231ccae925c67b9e59a1f922debc5d6823a8885b15a14f00489dc067b76dee9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/cjk.js",
+      "sha256": "a0686e11325493394016823e813e9c48acd2c9e6735714c6f063dcf1ba05cd0f",
+      "priorNormalizedSha256": [
+        "a0686e11325493394016823e813e9c48acd2c9e6735714c6f063dcf1ba05cd0f"
+      ],
+      "priorSha256": "3516120d9835270cc11c330d4e08d7f6fa9eaff05ae07d240d35d964fa4ffaa0",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/icons.js",
+      "sha256": "0d23722cf2cf494f5666bf56835094c1b94a58f0ca00888495dc6834f7bb9c3c",
+      "priorNormalizedSha256": [
+        "0d23722cf2cf494f5666bf56835094c1b94a58f0ca00888495dc6834f7bb9c3c"
+      ],
+      "priorSha256": "938ebc684cf044eb78921ab696a85cfaec32d7ec67166f578c889d918e9245b4",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/index.js",
+      "sha256": "8af3381a0fa90d08bfc9883dbb6bc6246cfee1e3b68d2fc2377dedbac6bb9ae0",
+      "priorNormalizedSha256": [
+        "8af3381a0fa90d08bfc9883dbb6bc6246cfee1e3b68d2fc2377dedbac6bb9ae0"
+      ],
+      "priorSha256": "1d422169fa605e2fee48529b44cf5591351eca1349ee430ff0458403c1dc4bb1",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-badge.js",
+      "sha256": "e4241502f789951bfc8bb9703f84ce5e00bf24b05e17266a17caca45614b48d6",
+      "priorNormalizedSha256": [
+        "e4241502f789951bfc8bb9703f84ce5e00bf24b05e17266a17caca45614b48d6"
+      ],
+      "priorSha256": "e4241502f789951bfc8bb9703f84ce5e00bf24b05e17266a17caca45614b48d6",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-drawer.js",
+      "sha256": "8278e89ae2aae7820c74b6419e8b89e046e9aae15d520a4a90949933eefbe7d0",
+      "priorNormalizedSha256": [
+        "8278e89ae2aae7820c74b6419e8b89e046e9aae15d520a4a90949933eefbe7d0"
+      ],
+      "priorSha256": "8278e89ae2aae7820c74b6419e8b89e046e9aae15d520a4a90949933eefbe7d0",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-empty.js",
+      "sha256": "1160e036696da5779c687ba5668090cc198720d91634e819bbbace9964be8bd8",
+      "priorNormalizedSha256": [
+        "1160e036696da5779c687ba5668090cc198720d91634e819bbbace9964be8bd8"
+      ],
+      "priorSha256": "056c981dbeab6f716fe73b36deb533a8e9a5245103b167084ecca6df825dc94f",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-evidence-chain.js",
+      "sha256": "bf0e05f2b7fb3efef15e04858fcf70c80ce85f65d1798719fdee9c2778006a91",
+      "priorNormalizedSha256": [
+        "bf0e05f2b7fb3efef15e04858fcf70c80ce85f65d1798719fdee9c2778006a91"
+      ],
+      "priorSha256": "bf0e05f2b7fb3efef15e04858fcf70c80ce85f65d1798719fdee9c2778006a91",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-glass-card.js",
+      "sha256": "f81dd2050674627281f43522cb120175fb91f0008269203fb7cf4254bce40e9a",
+      "priorNormalizedSha256": [
+        "f81dd2050674627281f43522cb120175fb91f0008269203fb7cf4254bce40e9a"
+      ],
+      "priorSha256": "f81dd2050674627281f43522cb120175fb91f0008269203fb7cf4254bce40e9a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-member-picker.js",
+      "sha256": "edfd3155acc80bbd1e8f5357f5e256da668213de16039b064df38e1e9925cb4a",
+      "priorNormalizedSha256": [
+        "edfd3155acc80bbd1e8f5357f5e256da668213de16039b064df38e1e9925cb4a"
+      ],
+      "priorSha256": "edfd3155acc80bbd1e8f5357f5e256da668213de16039b064df38e1e9925cb4a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-opportunity-card.js",
+      "sha256": "00317cf017018d76c5f312aae62b11af237dfbf6c20e83e0d02748ead7958245",
+      "priorNormalizedSha256": [
+        "2acf969ca65c87a9dc9766a2facb50c2e6a6effced474961b35d781930082f4b"
+      ],
+      "priorSha256": "2acf969ca65c87a9dc9766a2facb50c2e6a6effced474961b35d781930082f4b",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/components/kf-pipeline.js",
+      "sha256": "257c02bfec8aee4d538f1e56f017ae133115bd76af6dc95c06de890ddf86ada6",
+      "priorNormalizedSha256": [
+        "257c02bfec8aee4d538f1e56f017ae133115bd76af6dc95c06de890ddf86ada6"
+      ],
+      "priorSha256": "b870d83516b59c9614f5b392381bfa128d59bbdb041e48c3b6022a3129e3f750",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-score-panel.js",
+      "sha256": "3b6f19091728069365c6d199537bb59dcc88f88300bde0c5a289e35e375472eb",
+      "priorNormalizedSha256": [
+        "4441830101350381e23e669941ab8e70b43e2e7902e9c5d755db9ac212c5862e"
+      ],
+      "priorSha256": "a951e2e8a94245d6880e4af78786efc1df6275e54fe736f8ed78258e9dc2dbe4",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/components/kf-source-detail.js",
+      "sha256": "192b61dff91d5170c3661ef6a507500606b3e649bd16c748a1179f26083770f7",
+      "priorNormalizedSha256": [
+        "192b61dff91d5170c3661ef6a507500606b3e649bd16c748a1179f26083770f7"
+      ],
+      "priorSha256": "9c61966edaf5f7df6d593a5d0724d0c24c2aab8e487b8d128692f77d218a4d00",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-stat.js",
+      "sha256": "b6577c0136b422771a448d518db9837f08a8b60d5ebfeddbfd7f297eb5b6e071",
+      "priorNormalizedSha256": [
+        "b6577c0136b422771a448d518db9837f08a8b60d5ebfeddbfd7f297eb5b6e071"
+      ],
+      "priorSha256": "89d26ca79a2b734df9c5a59619fd82f353788a727902ade187c282429b0728a9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-table.js",
+      "sha256": "a023d34e465c419b450439e9616e26867ab54da0b97d27c226514e75d9908212",
+      "priorNormalizedSha256": [
+        "a023d34e465c419b450439e9616e26867ab54da0b97d27c226514e75d9908212"
+      ],
+      "priorSha256": "5e2172700e7989501d64ad7e831b549ca558b682fe578a798f5a07b3a350fbbe",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-toast.js",
+      "sha256": "388e021bfafb2491378f6872142f3f009292b74f6743121d590f2882bed0327d",
+      "priorNormalizedSha256": [
+        "388e021bfafb2491378f6872142f3f009292b74f6743121d590f2882bed0327d"
+      ],
+      "priorSha256": "0c1cdd6b81ddcb51706637b97d85f329bbfb75aa6442d170344bca3736a627a2",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/components/kf-viz.js",
+      "sha256": "f1f9de9b31f43659964ea7b69e1c7f210120b5ca936dc3416e739c709266cbed",
+      "priorNormalizedSha256": [
+        "f1f9de9b31f43659964ea7b69e1c7f210120b5ca936dc3416e739c709266cbed"
+      ],
+      "priorSha256": "8c563ffc4eafe9c198d707bee5595e19edb32ffe595745c9e01d904b57b299b2",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/brand-shop-filters.js",
+      "sha256": "002bab6a57c403bdf80dc1b7c1170f2ae581f078f90da7a5e8411c094bc8cce1",
+      "priorNormalizedSha256": [
+        "002bab6a57c403bdf80dc1b7c1170f2ae581f078f90da7a5e8411c094bc8cce1"
+      ],
+      "priorSha256": "002bab6a57c403bdf80dc1b7c1170f2ae581f078f90da7a5e8411c094bc8cce1",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/course-assessment.js",
+      "sha256": "468e7844e098c80e42e479f31dee9a3ae6c0fc869cc56e7ec00fe9cbd5939145",
+      "priorNormalizedSha256": [],
+      "priorSha256": null,
+      "allowMissing": true,
+      "changed": true
+    },
+    {
+      "path": "src/core/development-references.js",
+      "sha256": "c8c4e1e58b8128a7009593606515682b4f49de264a550df94d350ecf797295ed",
+      "priorNormalizedSha256": [
+        "c8c4e1e58b8128a7009593606515682b4f49de264a550df94d350ecf797295ed"
+      ],
+      "priorSha256": "c8c4e1e58b8128a7009593606515682b4f49de264a550df94d350ecf797295ed",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/evidence/chain.js",
+      "sha256": "91060709ebb32ff0969c24fb797d5f6ad7be4824d311078517effb3acbe2ee29",
+      "priorNormalizedSha256": [
+        "91060709ebb32ff0969c24fb797d5f6ad7be4824d311078517effb3acbe2ee29"
+      ],
+      "priorSha256": "3798974a75fa078cc2c6cef30651bb250917b35fa79625f90d3e89df401a9fb3",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/index.js",
+      "sha256": "01d145691d1d87d5b7e2464f7c8585319619a23843ac665c29ec3ac505174da7",
+      "priorNormalizedSha256": [
+        "01d145691d1d87d5b7e2464f7c8585319619a23843ac665c29ec3ac505174da7"
+      ],
+      "priorSha256": "86c14da3cec2b34e006bb607cb3540912b2f5e1511c51aa9ad90c45adcc80cb9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/industry-brief.js",
+      "sha256": "62ba01751ce032db8da1192d9e481b03449895bd33246c02e59546c30afdb95c",
+      "priorNormalizedSha256": [
+        "62ba01751ce032db8da1192d9e481b03449895bd33246c02e59546c30afdb95c"
+      ],
+      "priorSha256": "350b17cb03874dd93803e49309e492cd7ca216c26adf41c6a1024fbf6c04ecc9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/industry-materials.js",
+      "sha256": "063fb18e442ee703c10618fbb2917eca46ab0ea70bf21edb80675935f153d65c",
+      "priorNormalizedSha256": [
+        "063fb18e442ee703c10618fbb2917eca46ab0ea70bf21edb80675935f153d65c"
+      ],
+      "priorSha256": "d41d64ec5fee22ca0fc15d15e9bea7536aa28f51afd2fb73d8e16675939c08a8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/industry-navigation.js",
+      "sha256": "0d10003bebec2f02c3272f3fbd50c7d43818075a741f9d499b37970b47234d3b",
+      "priorNormalizedSha256": [
+        "0d10003bebec2f02c3272f3fbd50c7d43818075a741f9d499b37970b47234d3b"
+      ],
+      "priorSha256": "0d10003bebec2f02c3272f3fbd50c7d43818075a741f9d499b37970b47234d3b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/industry-planner.js",
+      "sha256": "6b4a426e112f2671ef90801482b729f5dc0a4b4589d0515c239d3ab8f7dc47d7",
+      "priorNormalizedSha256": [
+        "6b4a426e112f2671ef90801482b729f5dc0a4b4589d0515c239d3ab8f7dc47d7"
+      ],
+      "priorSha256": "6b4a426e112f2671ef90801482b729f5dc0a4b4589d0515c239d3ab8f7dc47d7",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/industry-research-directions.js",
+      "sha256": "be7409f6b14cb0d35987eb1fab103949c4f0add52cd30f7c19e6ece0555cc730",
+      "priorNormalizedSha256": [
+        "be7409f6b14cb0d35987eb1fab103949c4f0add52cd30f7c19e6ece0555cc730"
+      ],
+      "priorSha256": "be7409f6b14cb0d35987eb1fab103949c4f0add52cd30f7c19e6ece0555cc730",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/industry-workshop.js",
+      "sha256": "c648236a658d4c1dae2137d9677ec79cb254a6cddee2bbeff04818f29184c65d",
+      "priorNormalizedSha256": [
+        "c648236a658d4c1dae2137d9677ec79cb254a6cddee2bbeff04818f29184c65d"
+      ],
+      "priorSha256": "c648236a658d4c1dae2137d9677ec79cb254a6cddee2bbeff04818f29184c65d",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/kfs/build.js",
+      "sha256": "b7c3cacc4555cbd09c4fed95bc145ee2f99b2d6294f0a4e9e87999ea83d45fae",
+      "priorNormalizedSha256": [
+        "b7c3cacc4555cbd09c4fed95bc145ee2f99b2d6294f0a4e9e87999ea83d45fae"
+      ],
+      "priorSha256": "8e12cfc68c46cef21367dfc80ba4a8492f1377fb2d7a29a990d81a147ef2e4a8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/kfs/employee.js",
+      "sha256": "113fa23092992186eec9104a6c0af4bdb7b323579e43a11421ea4d6193d38b5d",
+      "priorNormalizedSha256": [
+        "113fa23092992186eec9104a6c0af4bdb7b323579e43a11421ea4d6193d38b5d"
+      ],
+      "priorSha256": "113fa23092992186eec9104a6c0af4bdb7b323579e43a11421ea4d6193d38b5d",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/kfs/ledger.js",
+      "sha256": "0ab77bc4db0ad98d184285e7662857d03b90aaa6708f2b9025e16597dc2fecd7",
+      "priorNormalizedSha256": [
+        "0ab77bc4db0ad98d184285e7662857d03b90aaa6708f2b9025e16597dc2fecd7"
+      ],
+      "priorSha256": "7242962b039f7ee307566b8dd90d0e3666f82bb7480b643f24d676db84446134",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/kfs/stats.js",
+      "sha256": "0ad541772cbacbf59302fcbfb8fb20cf04facb98ea3399bad765915e7a1f9112",
+      "priorNormalizedSha256": [
+        "0ad541772cbacbf59302fcbfb8fb20cf04facb98ea3399bad765915e7a1f9112"
+      ],
+      "priorSha256": "6e967eb9c4e711b13f28a3826b6b56f21d85a41f08dd291e56dca9f4a41d2e9c",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/kfs/util.js",
+      "sha256": "c0e77275f414bd2198dec161cf9c4d16dd27dfd99a587b4f7cfb5e66f61dec45",
+      "priorNormalizedSha256": [
+        "c0e77275f414bd2198dec161cf9c4d16dd27dfd99a587b4f7cfb5e66f61dec45"
+      ],
+      "priorSha256": "7a5a682c0a93e1c78e1b24f3aeb5150265450b35baad332ccdb7345ba655c2ee",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/library-selection.js",
+      "sha256": "d62cdb5a48fdbecb3e7d9854e7b6f3744d42e26c7c650249d6bd764dfebd5004",
+      "priorNormalizedSha256": [
+        "d62cdb5a48fdbecb3e7d9854e7b6f3744d42e26c7c650249d6bd764dfebd5004"
+      ],
+      "priorSha256": "c7076d0ab26166b19643b8eb5b0a97f94e1883d43acfb3364805f128667dfef3",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/library.js",
+      "sha256": "94e0d117dec688d5dcbe836f0d0077409e8cd5fe68ae60177444a795b1642513",
+      "priorNormalizedSha256": [
+        "94e0d117dec688d5dcbe836f0d0077409e8cd5fe68ae60177444a795b1642513"
+      ],
+      "priorSha256": "db40234341423cbb13fe6d4773e3cd281246c195c7a807bf9ef66a0de640b9bc",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/library/reports.js",
+      "sha256": "a600a838958d625688d24e0360339323d553c2a07adbc3b5c70f0eb81f36ece9",
+      "priorNormalizedSha256": [
+        "a600a838958d625688d24e0360339323d553c2a07adbc3b5c70f0eb81f36ece9"
+      ],
+      "priorSha256": "a600a838958d625688d24e0360339323d553c2a07adbc3b5c70f0eb81f36ece9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/library/types.js",
+      "sha256": "523b9ed851aea33349e624a4dc608cc26e813f15b674b762555497ad3cdc7d48",
+      "priorNormalizedSha256": [
+        "523b9ed851aea33349e624a4dc608cc26e813f15b674b762555497ad3cdc7d48"
+      ],
+      "priorSha256": "60a2470d29a0efbad02270f695740348ccee14fe03e297038d205f5521b84de6",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/member-groups.js",
+      "sha256": "6a82f26ea84e4b9b0a46aea330b10a87a1927e06532482ba8ab6d25b654d5372",
+      "priorNormalizedSha256": [
+        "6a82f26ea84e4b9b0a46aea330b10a87a1927e06532482ba8ab6d25b654d5372"
+      ],
+      "priorSha256": "6a82f26ea84e4b9b0a46aea330b10a87a1927e06532482ba8ab6d25b654d5372",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/opportunity/opportunity.js",
+      "sha256": "3dc79b829d8ebf36186d42a8429fa33f10f5debb8eed5e910626cd614e32ce2e",
+      "priorNormalizedSha256": [
+        "3dc79b829d8ebf36186d42a8429fa33f10f5debb8eed5e910626cd614e32ce2e"
+      ],
+      "priorSha256": "14c198eefc24883d75dd61a05cc2245c44da2f8cb8acaefade8995d7878616a8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/opportunity/report.js",
+      "sha256": "b3747ec12dda154c80ba69f52856a97099ec6e2ba169f4d20ed3e46bbab30c8b",
+      "priorNormalizedSha256": [
+        "b3747ec12dda154c80ba69f52856a97099ec6e2ba169f4d20ed3e46bbab30c8b"
+      ],
+      "priorSha256": "b3747ec12dda154c80ba69f52856a97099ec6e2ba169f4d20ed3e46bbab30c8b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/opportunity/score.js",
+      "sha256": "6b396005cce87ce42f15c85519401a7c9ae25c5b1e3972958fbd9138c2e7bff4",
+      "priorNormalizedSha256": [
+        "6b396005cce87ce42f15c85519401a7c9ae25c5b1e3972958fbd9138c2e7bff4"
+      ],
+      "priorSha256": "d10eaed160d1c7476b2424b3079b1f4a19e2ceb23be861b934b18b14df6df444",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/report-attachments.js",
+      "sha256": "ee0295e15f6ae773efd1bec774b4e30364027e33c364e187af7f24626ca845b1",
+      "priorNormalizedSha256": [
+        "ee0295e15f6ae773efd1bec774b4e30364027e33c364e187af7f24626ca845b1"
+      ],
+      "priorSha256": "ee0295e15f6ae773efd1bec774b4e30364027e33c364e187af7f24626ca845b1",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/report-delivery.js",
+      "sha256": "8ef6bb453175403e297aa7513915a9ca0b8bf032c7fee809409e0ed2ded1c719",
+      "priorNormalizedSha256": [
+        "8ef6bb453175403e297aa7513915a9ca0b8bf032c7fee809409e0ed2ded1c719"
+      ],
+      "priorSha256": "8ef6bb453175403e297aa7513915a9ca0b8bf032c7fee809409e0ed2ded1c719",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/report-evidence.js",
+      "sha256": "42c90ec91d8625ec7022c5d996d363bb79aeae3cd126ff8c57bbf4e15a79ab03",
+      "priorNormalizedSha256": [
+        "42c90ec91d8625ec7022c5d996d363bb79aeae3cd126ff8c57bbf4e15a79ab03"
+      ],
+      "priorSha256": "42c90ec91d8625ec7022c5d996d363bb79aeae3cd126ff8c57bbf4e15a79ab03",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/report-statistics.js",
+      "sha256": "89417702c1ad1f4aee64eb4f5d894a9fa8e75450ea0da201cb2773ff6936bea3",
+      "priorNormalizedSha256": [
+        "89417702c1ad1f4aee64eb4f5d894a9fa8e75450ea0da201cb2773ff6936bea3"
+      ],
+      "priorSha256": "89417702c1ad1f4aee64eb4f5d894a9fa8e75450ea0da201cb2773ff6936bea3",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/report.js",
+      "sha256": "2c383b3138daea0449467d695d11d1b57e6bbf100138a42d2c22ca75b5ebeac0",
+      "priorNormalizedSha256": [
+        "2c383b3138daea0449467d695d11d1b57e6bbf100138a42d2c22ca75b5ebeac0"
+      ],
+      "priorSha256": "a611fdf8319ef7b236dc96135a7e15c546725e3b936f13f674bd4d0b81a7d10a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/taxonomy/taxonomy.js",
+      "sha256": "7b00ef1858becad5a7db6c0e67080c3f599770bc09d73f8f934973b4f4dad847",
+      "priorNormalizedSha256": [
+        "7b00ef1858becad5a7db6c0e67080c3f599770bc09d73f8f934973b4f4dad847"
+      ],
+      "priorSha256": "ba01984956b28d4c2d7f6e863720342455e843192e8a4cba9bf38800adff27da",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/core/workshop-outputs.js",
+      "sha256": "60cfb6bbdcf4771a30ff0bd0ded735b7d0f690855eed64a316dcc6081be6268a",
+      "priorNormalizedSha256": [
+        "2ce98321c4a1e5bdc274a2d026907680d4987bd9b1dd4304551b481162ed560c"
+      ],
+      "priorSha256": "2ce98321c4a1e5bdc274a2d026907680d4987bd9b1dd4304551b481162ed560c",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/core/workshop-roster.js",
+      "sha256": "d072119c02f9908a24f5c9420141eb90da2bb8807bd21a53e448a1b4df3cbf1a",
+      "priorNormalizedSha256": [
+        "d072119c02f9908a24f5c9420141eb90da2bb8807bd21a53e448a1b4df3cbf1a"
+      ],
+      "priorSha256": "d072119c02f9908a24f5c9420141eb90da2bb8807bd21a53e448a1b4df3cbf1a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/data/development-references.json",
+      "sha256": "315598f057ef53698e1ca38adecae17b5fa79adafc0331c587b506e31801141a",
+      "priorNormalizedSha256": [
+        "315598f057ef53698e1ca38adecae17b5fa79adafc0331c587b506e31801141a"
+      ],
+      "priorSha256": "315598f057ef53698e1ca38adecae17b5fa79adafc0331c587b506e31801141a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/auth-service.js",
+      "sha256": "f7892cb6225dfcb37bcf21beda9e158a6420c410299236ff421650b240a5717e",
+      "priorNormalizedSha256": [
+        "f7892cb6225dfcb37bcf21beda9e158a6420c410299236ff421650b240a5717e"
+      ],
+      "priorSha256": "f7892cb6225dfcb37bcf21beda9e158a6420c410299236ff421650b240a5717e",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/board-service.js",
+      "sha256": "377c79fad4a036cf6a2baa9e1e819652f7384c3a9d110501cf43f4a2da3b988a",
+      "priorNormalizedSha256": [
+        "377c79fad4a036cf6a2baa9e1e819652f7384c3a9d110501cf43f4a2da3b988a"
+      ],
+      "priorSha256": "08ec2876418c6170e78818dfb95cb46fd17cd7d63794ef792f17ba42f05b62f8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/development-service.js",
+      "sha256": "ca1ae91389dc33934a85856681212b1e55a1af692076a95a4d59f2e424ea33d1",
+      "priorNormalizedSha256": [
+        "ca1ae91389dc33934a85856681212b1e55a1af692076a95a4d59f2e424ea33d1"
+      ],
+      "priorSha256": "ca1ae91389dc33934a85856681212b1e55a1af692076a95a4d59f2e424ea33d1",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/industry-service.js",
+      "sha256": "3f84a579f3b1eacdea34453a15b8548b627cc35fd0c8c39e8be9fda71f1330f4",
+      "priorNormalizedSha256": [
+        "3f84a579f3b1eacdea34453a15b8548b627cc35fd0c8c39e8be9fda71f1330f4"
+      ],
+      "priorSha256": "3f84a579f3b1eacdea34453a15b8548b627cc35fd0c8c39e8be9fda71f1330f4",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/library-images.js",
+      "sha256": "48a3fd58bde0f881eb1efbeaec4afd62903b05e15de38a2d6516185cc32a2d5c",
+      "priorNormalizedSha256": [
+        "48a3fd58bde0f881eb1efbeaec4afd62903b05e15de38a2d6516185cc32a2d5c"
+      ],
+      "priorSha256": "48a3fd58bde0f881eb1efbeaec4afd62903b05e15de38a2d6516185cc32a2d5c",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/library-pages.js",
+      "sha256": "fc3c3e494f162b0a60f429d2ccf3a1119e8898caad0745ce323fb5a86ecd9a0b",
+      "priorNormalizedSha256": [
+        "fc3c3e494f162b0a60f429d2ccf3a1119e8898caad0745ce323fb5a86ecd9a0b"
+      ],
+      "priorSha256": "fc3c3e494f162b0a60f429d2ccf3a1119e8898caad0745ce323fb5a86ecd9a0b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/library-service.js",
+      "sha256": "7b404a347664cc9c50a473b53f0c2bef675437b120b8b76f2a0e8e50eff5c695",
+      "priorNormalizedSha256": [
+        "7b404a347664cc9c50a473b53f0c2bef675437b120b8b76f2a0e8e50eff5c695"
+      ],
+      "priorSha256": "7b404a347664cc9c50a473b53f0c2bef675437b120b8b76f2a0e8e50eff5c695",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/member-groups.js",
+      "sha256": "f9e62e05ba51aea92d289bb6127fbae8fcc76541b8898d7c6517702ee1035d1a",
+      "priorNormalizedSha256": [
+        "f9e62e05ba51aea92d289bb6127fbae8fcc76541b8898d7c6517702ee1035d1a"
+      ],
+      "priorSha256": "02a9c39d9f3d90ad47df06619f31181d1661956c9f46e0d46b8f9b4b3693df28",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/platform-auth.js",
+      "sha256": "a84ef01a1dca0dc8f7003a41dbc9569f59a6969392c0ecb58508e7f6ce16c18d",
+      "priorNormalizedSha256": [
+        "a84ef01a1dca0dc8f7003a41dbc9569f59a6969392c0ecb58508e7f6ce16c18d"
+      ],
+      "priorSha256": "a84ef01a1dca0dc8f7003a41dbc9569f59a6969392c0ecb58508e7f6ce16c18d",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/services/workshop-data.js",
+      "sha256": "cae7e9065a37d395429d5f1bd254b2201ad4b0218c6222349029455dc96a7c65",
+      "priorNormalizedSha256": [
+        "cae7e9065a37d395429d5f1bd254b2201ad4b0218c6222349029455dc96a7c65"
+      ],
+      "priorSha256": "c0abee4bc2f331569c6d9d7ab37f9941bfe2d3dd409bdea115778137be16af9e",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/styles/filter-controls.js",
+      "sha256": "d56fea6e56ca06df415d107ad7c5e253a7e650f115377c821c9927bae41ea633",
+      "priorNormalizedSha256": [
+        "d56fea6e56ca06df415d107ad7c5e253a7e650f115377c821c9927bae41ea633"
+      ],
+      "priorSha256": "d56fea6e56ca06df415d107ad7c5e253a7e650f115377c821c9927bae41ea633",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/styles/tokens.js",
+      "sha256": "692243307e126c3367ec9f3a9af44f651d710dd1c961a0a8bdb0ff8c3ace4a52",
+      "priorNormalizedSha256": [
+        "692243307e126c3367ec9f3a9af44f651d710dd1c961a0a8bdb0ff8c3ace4a52"
+      ],
+      "priorSha256": "692243307e126c3367ec9f3a9af44f651d710dd1c961a0a8bdb0ff8c3ace4a52",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/utils/card-view.js",
+      "sha256": "7bcda8fe488b793c78fce5611bcd1e500554668ab762254aaef7c2016b1c4f4f",
+      "priorNormalizedSha256": [
+        "7bcda8fe488b793c78fce5611bcd1e500554668ab762254aaef7c2016b1c4f4f"
+      ],
+      "priorSha256": "beec0ad6768dc44d0a73b091ca59241d5e6042b7598310df6ef69a34478f2a1a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/utils/cjk-deep.js",
+      "sha256": "9d973b2142e06a1c3c3320f6105d0efe6d07f8efc6c3ca96cbe1978957f302b9",
+      "priorNormalizedSha256": [
+        "9d973b2142e06a1c3c3320f6105d0efe6d07f8efc6c3ca96cbe1978957f302b9"
+      ],
+      "priorSha256": "8a545389997937eb1df39928e9c59d8d967716e862e1c79b60756d6f4a9c9029",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/utils/http.js",
+      "sha256": "abbfdf63f13d11c86a771a2a13cb5ecafb33f3f5d50a433dfaafd48259390d77",
+      "priorNormalizedSha256": [
+        "abbfdf63f13d11c86a771a2a13cb5ecafb33f3f5d50a433dfaafd48259390d77"
+      ],
+      "priorSha256": "abbfdf63f13d11c86a771a2a13cb5ecafb33f3f5d50a433dfaafd48259390d77",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/utils/router.js",
+      "sha256": "5f4d85e7beea3f7238e6836c8efdec18f8d42af124e58d8917b6d318a8b95934",
+      "priorNormalizedSha256": [
+        "5f4d85e7beea3f7238e6836c8efdec18f8d42af124e58d8917b6d318a8b95934"
+      ],
+      "priorSha256": "7bf8881481da79c7847c95b44960e2620d8ba1b2de39fb1ba771c80ffc33f30f",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/utils/storage.js",
+      "sha256": "b614463cdeed015327a7851eac6bd43bf95db14c16fe13f094e8af5cec545026",
+      "priorNormalizedSha256": [
+        "b614463cdeed015327a7851eac6bd43bf95db14c16fe13f094e8af5cec545026"
+      ],
+      "priorSha256": "4212824a06c37bfc87b2340a487185779c1ae679f344ee491973ea0c59685b2a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/utils/type.js",
+      "sha256": "5bc6a7bf164b38961ada06ca9c4c1b334b4679d9e227fadee0b80b8308bae165",
+      "priorNormalizedSha256": [
+        "5bc6a7bf164b38961ada06ca9c4c1b334b4679d9e227fadee0b80b8308bae165"
+      ],
+      "priorSha256": "1577a56abf65a0768e1e08ca3d1b4cb7545effb2527296cea8d4a7a0992c05d7",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/card-toolbar.js",
+      "sha256": "0e77d4540f38339fb6c82d0ff6184de257bfb0a1e4e61506c1c91b045e3577db",
+      "priorNormalizedSha256": [
+        "0e77d4540f38339fb6c82d0ff6184de257bfb0a1e4e61506c1c91b045e3577db"
+      ],
+      "priorSha256": "0e77d4540f38339fb6c82d0ff6184de257bfb0a1e4e61506c1c91b045e3577db",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/filter-sheet.js",
+      "sha256": "4f62212bd8646096e80be9d00da7d098f1937a1176be6ccb37b721bbd9c5e556",
+      "priorNormalizedSha256": [
+        "4f62212bd8646096e80be9d00da7d098f1937a1176be6ccb37b721bbd9c5e556"
+      ],
+      "priorSha256": "b4f280579f58e8e907c78d04956dabe0453ba228b34fe705955143377e80523b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-dashboard-view.js",
+      "sha256": "cabcd32387a66f0c05cf9a1c27f3ae402757a842942dc66ef49d251c4c9c197b",
+      "priorNormalizedSha256": [
+        "cabcd32387a66f0c05cf9a1c27f3ae402757a842942dc66ef49d251c4c9c197b"
+      ],
+      "priorSha256": "cabcd32387a66f0c05cf9a1c27f3ae402757a842942dc66ef49d251c4c9c197b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-decision-view.js",
+      "sha256": "7dd15c3aa5e5401f33090ae95e4a02debe2575c67eb64bee4252e72241e8fc6b",
+      "priorNormalizedSha256": [
+        "7dd15c3aa5e5401f33090ae95e4a02debe2575c67eb64bee4252e72241e8fc6b"
+      ],
+      "priorSha256": "e09c3299739a2153959644dd0a03bcad48097b17639037d23339cc7142bfa826",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-detail-view.js",
+      "sha256": "0cc23045442f8e40ee2732c1553086e0543715487e050158e9585e10a5cf6309",
+      "priorNormalizedSha256": [
+        "0cc23045442f8e40ee2732c1553086e0543715487e050158e9585e10a5cf6309"
+      ],
+      "priorSha256": "0cc23045442f8e40ee2732c1553086e0543715487e050158e9585e10a5cf6309",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-development-view.js",
+      "sha256": "8c99dd038109ebf0e63a82b7cb0ae0afdb4894822668a34c6dddabef51c35e85",
+      "priorNormalizedSha256": [
+        "8c99dd038109ebf0e63a82b7cb0ae0afdb4894822668a34c6dddabef51c35e85"
+      ],
+      "priorSha256": "8c99dd038109ebf0e63a82b7cb0ae0afdb4894822668a34c6dddabef51c35e85",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-innovation-view.js",
+      "sha256": "d120407bf4370a73155644ef06a61d337e301dc1bb356387feff6207cb896ebe",
+      "priorNormalizedSha256": [
+        "d120407bf4370a73155644ef06a61d337e301dc1bb356387feff6207cb896ebe"
+      ],
+      "priorSha256": "827c228607c7eb3641872f00346db71e6b1d84788b163ab00c9c0d1e34b642a2",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-pipeline-view.js",
+      "sha256": "000f5ab6cb392ffc663c1696fa45c5835ea4e5261991f9fd0ebd4cf918dc0a50",
+      "priorNormalizedSha256": [
+        "000f5ab6cb392ffc663c1696fa45c5835ea4e5261991f9fd0ebd4cf918dc0a50"
+      ],
+      "priorSha256": "1571abd1063b53c7490887fcbdb5f781719b0a1f009a4d5f0dc6ce4fe6768426",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-planner-view.js",
+      "sha256": "c2adf992ef5d92d58063c61e4ba55155f17ec74c10351dedbcd325fe7ac3fc77",
+      "priorNormalizedSha256": [
+        "c2adf992ef5d92d58063c61e4ba55155f17ec74c10351dedbcd325fe7ac3fc77"
+      ],
+      "priorSha256": "c2adf992ef5d92d58063c61e4ba55155f17ec74c10351dedbcd325fe7ac3fc77",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-policy-view.js",
+      "sha256": "911367be02770172f88b5cfba9c6cbe5258fd77d29465a5d873e5a253cb26f1a",
+      "priorNormalizedSha256": [
+        "911367be02770172f88b5cfba9c6cbe5258fd77d29465a5d873e5a253cb26f1a"
+      ],
+      "priorSha256": "7808cf9eaba3521db8203cad07850455cb0059581e3bfa8ebe07dc962d3fcb5b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-ranking-view.js",
+      "sha256": "364fa8a767669607580669387e8eb9c54e6d31080e15d4d29cee577209240212",
+      "priorNormalizedSha256": [
+        "364fa8a767669607580669387e8eb9c54e6d31080e15d4d29cee577209240212"
+      ],
+      "priorSha256": "364fa8a767669607580669387e8eb9c54e6d31080e15d4d29cee577209240212",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-research-direction-view.js",
+      "sha256": "dfd6ffaada580392a0b6a45b1b13535b348b5747569e594bfb8418e8c0233973",
+      "priorNormalizedSha256": [
+        "dfd6ffaada580392a0b6a45b1b13535b348b5747569e594bfb8418e8c0233973"
+      ],
+      "priorSha256": "388cf96fedbb98b3a3555481ef7f278f51e934e4eabc32b63c1940edf26b1929",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-research-views.js",
+      "sha256": "a40180e68186737ba7a09b4e4449b243a5c88c0d77448ee580e5987800d81415",
+      "priorNormalizedSha256": [
+        "a40180e68186737ba7a09b4e4449b243a5c88c0d77448ee580e5987800d81415"
+      ],
+      "priorSha256": "dd9bc98367311a8804e4c0a7714ba5da4912d0ce6d8ee06d275f735fcd0f10e8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-scope-filters.js",
+      "sha256": "075a15c52f917daefc2bb5eab3eab1d1db7eefcabe5c97294353dd268edfa67c",
+      "priorNormalizedSha256": [
+        "075a15c52f917daefc2bb5eab3eab1d1db7eefcabe5c97294353dd268edfa67c"
+      ],
+      "priorSha256": "4e6b6df9c5bac4a44af94e2d9224735cf0bf08ac5ec46c8c48c187158b4744a2",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/industry-workshop-view.js",
+      "sha256": "f9f4416a438eb4b5b7a0680f4663802b9c26ee885a9c0bda08c712360124673d",
+      "priorNormalizedSha256": [
+        "f9f4416a438eb4b5b7a0680f4663802b9c26ee885a9c0bda08c712360124673d"
+      ],
+      "priorSha256": "51afdfef8eaed975647f0d5934a994a0396d77d374fd08c6180f4a465a56849a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/library-product-detail.js",
+      "sha256": "6f2f7e571a818384602e0b8c0ab0a41d8defa4831c6b7a32d730ebc1c686c532",
+      "priorNormalizedSha256": [
+        "6f2f7e571a818384602e0b8c0ab0a41d8defa4831c6b7a32d730ebc1c686c532"
+      ],
+      "priorSha256": "1f9d6ae038a1f836f6492825779bf37f16eca6a3461746f29b262538878b72da",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/library-product-image.js",
+      "sha256": "735abb1e00b267b03d50336632f6fcec5ed742cf26331d8f2a00dbb6d7036da9",
+      "priorNormalizedSha256": [
+        "735abb1e00b267b03d50336632f6fcec5ed742cf26331d8f2a00dbb6d7036da9"
+      ],
+      "priorSha256": "735abb1e00b267b03d50336632f6fcec5ed742cf26331d8f2a00dbb6d7036da9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/library-styles.js",
+      "sha256": "ab8bffb73b87ac93d20ccbd57408710ca4c06b524f0ce736e9d891abaf05055a",
+      "priorNormalizedSha256": [
+        "ab8bffb73b87ac93d20ccbd57408710ca4c06b524f0ce736e9d891abaf05055a"
+      ],
+      "priorSha256": "3f6195ff1a990eb99d132092dadef53ad10f24738b0504c878a58e38348391c1",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/mobile-research-styles.js",
+      "sha256": "30b9fd469e20d863ca4f76ae791c9803fcd49918764a180f47ec8089536014b1",
+      "priorNormalizedSha256": [
+        "30b9fd469e20d863ca4f76ae791c9803fcd49918764a180f47ec8089536014b1"
+      ],
+      "priorSha256": "d2843c53aae75bd8c032effd7d7b5b159ffd76018b0723ba909bdf672b9d3491",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/policy-reviewed-evidence.js",
+      "sha256": "f56b2b26cb1565656250517d803156b193312d5ef7ef5bfac277bd675e7a8d42",
+      "priorNormalizedSha256": [
+        "f56b2b26cb1565656250517d803156b193312d5ef7ef5bfac277bd675e7a8d42"
+      ],
+      "priorSha256": "f56b2b26cb1565656250517d803156b193312d5ef7ef5bfac277bd675e7a8d42",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/report-card.js",
+      "sha256": "1dfad2b17919da378a75a5345e513bfa0c3fcc5124493327bfa091e219311106",
+      "priorNormalizedSha256": [
+        "1dfad2b17919da378a75a5345e513bfa0c3fcc5124493327bfa091e219311106"
+      ],
+      "priorSha256": "9571a84ff4fe045802774ac9616e9a21cdec1eaee87f97b74fc56d919230263d",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/report-pager.js",
+      "sha256": "a0353d0ce2b8ba7a926d728e933947bd34f06c77dee63329d948803865d41223",
+      "priorNormalizedSha256": [
+        "a0353d0ce2b8ba7a926d728e933947bd34f06c77dee63329d948803865d41223"
+      ],
+      "priorSha256": "b3933fbbd70364622d73a452118736fdeaa32d935227697e623b3a0e057b2fe9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/report-styles.js",
+      "sha256": "69d27be000d8605806d2db3db70e8810e8e7bfab1c3e3a544c2b0faa18b28dfb",
+      "priorNormalizedSha256": [
+        "1dd7c76f670afc9d9cb900c5e234125d5a76fd34e29638f496b5900f9e57a630"
+      ],
+      "priorSha256": "1dd7c76f670afc9d9cb900c5e234125d5a76fd34e29638f496b5900f9e57a630",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/pages/shared.js",
+      "sha256": "92f0deb2f4128bfc0cecaa68ee5f81c7818e5218e97127d1cc1770845ec7db55",
+      "priorNormalizedSha256": [
+        "92f0deb2f4128bfc0cecaa68ee5f81c7818e5218e97127d1cc1770845ec7db55"
+      ],
+      "priorSha256": "f84976ed078f8e4a45bb132d42eef73ac6bbef419fa19152037d6111f4e12afb",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-industry.js",
+      "sha256": "0810bebb95692a9a6b483f1e537083d767e4db4ff252eaec21659077a40c62c7",
+      "priorNormalizedSha256": [
+        "0810bebb95692a9a6b483f1e537083d767e4db4ff252eaec21659077a40c62c7"
+      ],
+      "priorSha256": "a68d5479d99a201fcfb6ff6bb8cc3629aaafee61ce71f428629df657f05cd85b",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-ledger.js",
+      "sha256": "e88ce94f0deb4f96b36e53963423185c84da2793ef785ba8ec3e335566582185",
+      "priorNormalizedSha256": [
+        "e88ce94f0deb4f96b36e53963423185c84da2793ef785ba8ec3e335566582185"
+      ],
+      "priorSha256": "e88ce94f0deb4f96b36e53963423185c84da2793ef785ba8ec3e335566582185",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-library.js",
+      "sha256": "7bd197cf5d73eaf6b3e5cbab7eb7a708db00c44ae7ad190171f1cec3154828e9",
+      "priorNormalizedSha256": [
+        "7bd197cf5d73eaf6b3e5cbab7eb7a708db00c44ae7ad190171f1cec3154828e9"
+      ],
+      "priorSha256": "121dfc1ea5553040f90f08874fa2b5ed1043df2e2b778d4fc3131425a68137a8",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-login.js",
+      "sha256": "0c4002486cc62d7f2d1efe786262d974dfbcc46d4583fca8e41e9d82d2a742c7",
+      "priorNormalizedSha256": [
+        "0c4002486cc62d7f2d1efe786262d974dfbcc46d4583fca8e41e9d82d2a742c7"
+      ],
+      "priorSha256": "0c4002486cc62d7f2d1efe786262d974dfbcc46d4583fca8e41e9d82d2a742c7",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-members.js",
+      "sha256": "fe2ce5224ffbbdda3e7d0b971327f6354800a3a4831e86797bb02357591f4137",
+      "priorNormalizedSha256": [
+        "fe2ce5224ffbbdda3e7d0b971327f6354800a3a4831e86797bb02357591f4137"
+      ],
+      "priorSha256": "8e35771af61d45958333bd1d487f25ad41c8e40b2b4346a57ea4b7b39e407736",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-pool.js",
+      "sha256": "f5325e685782218eae740a09bb4b55420a5ebb7d1d7c71c95aa6251687729809",
+      "priorNormalizedSha256": [
+        "db98e2fdfc150571471d1653d37ef296257343b942d5e653ed4234d685225e7d"
+      ],
+      "priorSha256": "db98e2fdfc150571471d1653d37ef296257343b942d5e653ed4234d685225e7d",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/pages/view-report-type.js",
+      "sha256": "368f25bbe9669528fd287dbdd7ce6e0a6149bbe328e76d17dde182a697f13d0a",
+      "priorNormalizedSha256": [
+        "368f25bbe9669528fd287dbdd7ce6e0a6149bbe328e76d17dde182a697f13d0a"
+      ],
+      "priorSha256": "368f25bbe9669528fd287dbdd7ce6e0a6149bbe328e76d17dde182a697f13d0a",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-report.js",
+      "sha256": "47a7591e469007faa12d6b0fbff1ccec664fd622765ad7d8ac295eb5c0dc131f",
+      "priorNormalizedSha256": [
+        "1a06f5ce023b1c55ccc571e8297fe9bebc780a2901beab8cfe6d68683e42a07a"
+      ],
+      "priorSha256": "3c01f1e40a56bbb507e806056734dc13ad88e8e05017fa0cd4c29b71d6ee6fa3",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/pages/view-reports.js",
+      "sha256": "8cf9926c483bb7518093ef3033e10e0ffc4f83b3a81a06d781ae3893d441a971",
+      "priorNormalizedSha256": [
+        "8cf9926c483bb7518093ef3033e10e0ffc4f83b3a81a06d781ae3893d441a971"
+      ],
+      "priorSha256": "8cf9926c483bb7518093ef3033e10e0ffc4f83b3a81a06d781ae3893d441a971",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-stages.js",
+      "sha256": "61a3312cccb2c7bc8cfc7a13ec6028cb7d06d88a7fc973714e25519a1c44f009",
+      "priorNormalizedSha256": [
+        "61a3312cccb2c7bc8cfc7a13ec6028cb7d06d88a7fc973714e25519a1c44f009"
+      ],
+      "priorSha256": "61a3312cccb2c7bc8cfc7a13ec6028cb7d06d88a7fc973714e25519a1c44f009",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-workbench.js",
+      "sha256": "03343cae4466a46b334b55120c6be4042553f2c230a67e63146ad798868f87d9",
+      "priorNormalizedSha256": [
+        "03343cae4466a46b334b55120c6be4042553f2c230a67e63146ad798868f87d9"
+      ],
+      "priorSha256": "03343cae4466a46b334b55120c6be4042553f2c230a67e63146ad798868f87d9",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/pages/view-workshop-outputs.js",
+      "sha256": "d1b786682d5008e61715e21546428d34cb4a28cd0af8f853d93522f8247f4b93",
+      "priorNormalizedSha256": [
+        "e8030de394d5b94bf73a34533d8a4c5a58d3a7fe5a55a7bf81967b15e35b4da8"
+      ],
+      "priorSha256": "6653b15db49d56ba937ea0665ee0fb28ee37f1cade05417b0343ed05aa0d0dfc",
+      "allowMissing": false,
+      "changed": true
+    },
+    {
+      "path": "src/layout/detail-styles.js",
+      "sha256": "d6bf229a225aa5c715d0645f6d680c6f69abbd53063023528a9d3da8c7fa461b",
+      "priorNormalizedSha256": [
+        "d6bf229a225aa5c715d0645f6d680c6f69abbd53063023528a9d3da8c7fa461b"
+      ],
+      "priorSha256": "fc394d5aca8b57a920b7d40b16225a4039436deeb413c2434a42a79740f4b717",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/layout/kf-nav.js",
+      "sha256": "04c1ffacbe0a247880abc2f814cbb276ab4f3b62edf097a4a93786ce122fbb99",
+      "priorNormalizedSha256": [
+        "04c1ffacbe0a247880abc2f814cbb276ab4f3b62edf097a4a93786ce122fbb99"
+      ],
+      "priorSha256": "f97f9cce3dadc8a3733f8976230ac872a26fb7225b3cda54b105ea69c8d26aa4",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/layout/kf-topbar.js",
+      "sha256": "a80c5d0a07f58980d587cd6bd17875d8e0560af3f9f8a5d1e9d5f653b87e94c0",
+      "priorNormalizedSha256": [
+        "a80c5d0a07f58980d587cd6bd17875d8e0560af3f9f8a5d1e9d5f653b87e94c0"
+      ],
+      "priorSha256": "a80c5d0a07f58980d587cd6bd17875d8e0560af3f9f8a5d1e9d5f653b87e94c0",
+      "allowMissing": false,
+      "changed": false
+    },
+    {
+      "path": "src/layout/kf-app.js",
+      "sha256": "e01c4f4d5702910831f2388eea0c6ecd42585dbfd85cd52080ce0e76b1d5864d",
+      "priorNormalizedSha256": [
+        "e01c4f4d5702910831f2388eea0c6ecd42585dbfd85cd52080ce0e76b1d5864d"
+      ],
+      "priorSha256": "e01c4f4d5702910831f2388eea0c6ecd42585dbfd85cd52080ce0e76b1d5864d",
+      "allowMissing": false,
+      "changed": false
+    }
+  ]
+}

+ 73 - 0
docs/board-v3-release-20261112/README.md

@@ -0,0 +1,73 @@
+# 看板 v3 流程与报告展示发布
+
+发行:`ksf-board-v3-20261112-r2`。本档案记录已完成的本地验证与只读现网基线;正式部署、外网回读和线上浏览器验收须以站点机回执为准。
+
+## 本次行为
+
+小组成果主表按“组员 + 13 个课程环节”展示,顺序为 D1-01~D1-04、D1-N、D2-01~D2-08。报告入口保留具体报告分类,报告中心继续保留 11 类。历史环节不明的报告保留在“环节待定位”,不按标题猜测归属。
+
+报告正文支持统计卡、统计口径和限制、原话证据、课程三维评估与未知项;HTML 报告与看板正文可切换。机会池默认最新上传排序,历史等级和百分制多人点评与课程三维排序分别说明。
+
+## 发布范围
+
+`deploy/board-v3-20261112.json` 列出完整浏览器依赖 121 件。2026-11-12 只读现网核对:112 件与本地一致,本轮 9 件需更新,其中 2 件新增:
+
+- `skills/ksf-workshop-runtime/scripts/course-deliverables.mjs`(新增,网站浏览器依赖)
+- `src/core/course-assessment.js`(新增)
+- `src/components/kf-opportunity-card.js`
+- `src/components/kf-score-panel.js`
+- `src/core/workshop-outputs.js`
+- `src/pages/report-styles.js`
+- `src/pages/view-pool.js`
+- `src/pages/view-report.js`
+- `src/pages/view-workshop-outputs.js`
+
+发布器逐件核对 121 件;实际变更数由站点机现场 plan 决定。只接受清单已知基线或本次目标内容,未知差异即停。网站目录内的 `course-deliverables.mjs` 是页面模块,不是向数字人容器安装 Skill。本次不执行数据库/函数/成员/报告清理,不升级数字人 Skill、VOC 或手册,不发布课前指南。
+
+首页不进入载荷,必须始终保持 SHA-256:
+
+`dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3`
+
+## 已完成验证
+
+| 验证 | 结果 |
+|---|---|
+| 工作坊测试 | 126/126 |
+| 前端测试 | 234/234 |
+| 发布器、旧发布器回归、MIME 和统计检查 | 20/20 |
+| 暂存版本完整性及独立检出 | 121 件载荷哈希一致,独立检出发布检查 20/20 |
+| 静态检查 | CHECK OK;Windows 路径下 core 层匹配数为 0,不将此项当作该层已覆盖 |
+| 本地模块链 | 119 个 JavaScript 模块,失败 0 |
+| 现网公开文件副本演练 | plan 9 → apply 9 → noop 0,首页不变 |
+| 浏览器真实公开数据 + 本地源码,1440/390/320 | 27 位成员无重复;主表 13 列;真实报告 JLF4v5MpyR 可从成果入口打开;11 类报告入口;机会池默认最新;页面横向溢出 0;未捕获异常 0 |
+| 浏览器内存夹具,1440/390/320 | HTML 切换 1→0→1;统计 4/10=40.0%;未知分母不计算占比;课程未知项不补低分 |
+
+上述浏览器验证并未部署源码到生产。真实公开报告 JLF4v5MpyR 在核对时附件为 0、统计卡为 0;HTML/统计新内容的真实保存和展示闭环仍待真实报告验收。浏览器内存夹具不入库、不计为真实成果。
+
+旧六文件发行的两项测试曾因当前源码已前进而报 PAYLOAD_HASH_MISMATCH;仅修改测试夹具,使其用当前源码构造隔离测试哈希,未改旧发布清单。复跑全部发布相关检查 20/20。
+
+证据:`baseline-summary.json`、`local-simulation.json`、`browser-check.json`、`modules-local.json`、`tests-*.txt`、`static-check.txt`。截图 `workshop-1440.png` 为真实公开数据配合本地新源码;`fixture-statistics-390.png` 为浏览器内存夹具,非真实成果。
+
+## 站点机执行
+
+从源码库 `https://git.fmode.cn/18307996893/kangshifu-board.git` 新检出目录,固定到交付提交,确认工作区干净。在该目录运行:
+
+```bash
+bash scripts/deploy-board-v3-20261112.sh /opt/kangshifu-web
+```
+
+要求 Node.js 20+、既有站点写入权限及读取 `/etc/nginx/mime.types` 的权限。脚本不要求数据库凭据,不运行后端部署器,不重启任何服务。
+
+脚本按顺序执行:MIME 只读预检 → 发布安全测试 → 现场 plan → 备份及原子替换 → noop → 外网 121 件 SHA/MIME 回读、首页保护及公开接口只读核对 → 整站模块链。结果目录为 `/opt/ksf-board-v3-20261112-results-*`。新建目录按 umask 022,静态文件为 0644;现有内容全部备份后才开始替换。
+
+任何步骤报错应保留原始回执、停止并核查。不要重新生成清单、增加未知基线或使用旧全量发布脚本强行继续。新增 `.mjs` 必须返回 JavaScript MIME,HTTP 200 的首页兜底不算成功。
+
+## 上线验收与回退
+
+正式域名三档视口 1440/390/320 检查 `/workshop`、真实报告、`/reports`、`/pool`。13 列表格在容器内可横向滑动,页面本身无横向溢出;27 位成员身份保持一致;报告入口与正文对应。以真实成果验收报告,内存夹具结果单独列出,不提交评分、决策或测试报告。
+
+有 Playwright 环境时可复用 `node scripts/verify-board-v3-browser.mjs --production`。`KSF_PLAYWRIGHT_MODULE` 指定 Playwright 的 `index.mjs` 绝对路径,`KSF_BROWSER_EXECUTABLE` 指定浏览器路径,`KSF_BROWSER_OUTPUT` 指定证据目录。该脚本阻断业务写请求;生产只读检查与内存夹具检查分别记录。
+
+发布失败时,发布器自动尝试恢复本轮已替换文件并删除本轮新增文件。备份路径和逐件原始 SHA、目标 SHA 见 apply 回执及备份 `journal.json`。成功发布后若需主动回退,先核实首页不变且每个目标当前 SHA 仍等于 journal 的 afterSha256,再恢复已备份原文件;beforeSha256=null 的条目仅在 afterSha256 仍匹配时删除。发生后续并发更新时停止,不覆盖他人改动。
+
+运维回传:固定提交、结果目录、plan/apply/noop、公网及模块链回执、备份路径和三档截图。分别写明“静态部署”“线上浏览器展示”“真实 HTML/统计成果闭环”的完成状态。

+ 21 - 0
docs/board-v3-release-20261112/baseline-summary.json

@@ -0,0 +1,21 @@
+{
+  "release": "ksf-board-v3-20261112-r2",
+  "productionWrites": 0,
+  "files": 121,
+  "changed": [
+    "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+    "src/components/kf-opportunity-card.js",
+    "src/components/kf-score-panel.js",
+    "src/core/course-assessment.js",
+    "src/core/workshop-outputs.js",
+    "src/pages/report-styles.js",
+    "src/pages/view-pool.js",
+    "src/pages/view-report.js",
+    "src/pages/view-workshop-outputs.js"
+  ],
+  "added": [
+    "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+    "src/core/course-assessment.js"
+  ],
+  "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3"
+}

+ 113 - 0
docs/board-v3-release-20261112/browser-check.json

@@ -0,0 +1,113 @@
+{
+  "release": "ksf-board-v3-20261112-r2",
+  "mode": "local-source-with-public-production-reads",
+  "productionWrites": 0,
+  "deployedByThisCheck": false,
+  "checks": [
+    {
+      "page": "workshop",
+      "width": 1440,
+      "columns": 13,
+      "members": 27,
+      "overflow": 0,
+      "actualPublicData": true
+    },
+    {
+      "page": "real-report",
+      "width": 1440,
+      "artifactId": "JLF4v5MpyR",
+      "attachments": 0,
+      "statisticsCards": 0,
+      "chapters": 7,
+      "overflow": 0,
+      "actualPublicData": true
+    },
+    {
+      "page": "reports-and-pool",
+      "width": 1440,
+      "reportCategories": 11,
+      "defaultSort": "newest",
+      "actualPublicData": true
+    },
+    {
+      "page": "workshop",
+      "width": 390,
+      "columns": 13,
+      "members": 27,
+      "overflow": 0,
+      "actualPublicData": true
+    },
+    {
+      "page": "real-report",
+      "width": 390,
+      "artifactId": "JLF4v5MpyR",
+      "attachments": 0,
+      "statisticsCards": 0,
+      "chapters": 7,
+      "overflow": 0,
+      "actualPublicData": true
+    },
+    {
+      "page": "reports-and-pool",
+      "width": 390,
+      "reportCategories": 11,
+      "defaultSort": "newest",
+      "actualPublicData": true
+    },
+    {
+      "page": "workshop",
+      "width": 320,
+      "columns": 13,
+      "members": 27,
+      "overflow": 0,
+      "actualPublicData": true
+    },
+    {
+      "page": "real-report",
+      "width": 320,
+      "artifactId": "JLF4v5MpyR",
+      "attachments": 0,
+      "statisticsCards": 0,
+      "chapters": 7,
+      "overflow": 0,
+      "actualPublicData": true
+    },
+    {
+      "page": "reports-and-pool",
+      "width": 320,
+      "reportCategories": 11,
+      "defaultSort": "newest",
+      "actualPublicData": true
+    },
+    {
+      "page": "in-memory-fixture",
+      "width": 1440,
+      "statisticsCards": 2,
+      "courseAssessmentCards": 1,
+      "htmlToggle": "1→0→1",
+      "actualPublicData": false,
+      "saved": false
+    },
+    {
+      "page": "in-memory-fixture",
+      "width": 390,
+      "statisticsCards": 2,
+      "courseAssessmentCards": 1,
+      "htmlToggle": "1→0→1",
+      "actualPublicData": false,
+      "saved": false
+    },
+    {
+      "page": "in-memory-fixture",
+      "width": 320,
+      "statisticsCards": 2,
+      "courseAssessmentCards": 1,
+      "htmlToggle": "1→0→1",
+      "actualPublicData": false,
+      "saved": false
+    }
+  ],
+  "errors": [],
+  "blockedWrites": [],
+  "ok": true
+}

BIN
docs/board-v3-release-20261112/fixture-statistics-390.png


+ 671 - 0
docs/board-v3-release-20261112/local-simulation.json

@@ -0,0 +1,671 @@
+{
+  "ok": true,
+  "scope": "local-copy-of-public-production-files",
+  "productionWrites": 0,
+  "plan": {
+    "ok": true,
+    "operation": "read-only-plan",
+    "readOnly": true,
+    "targetDir": "C:\\Users\\JSHY\\AppData\\Local\\Temp\\ksf-v3-simulation-Io1c9H\\site",
+    "release": "ksf-board-v3-20261112-r2",
+    "filesChecked": 121,
+    "changes": [
+      "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+      "src/components/kf-opportunity-card.js",
+      "src/components/kf-score-panel.js",
+      "src/core/course-assessment.js",
+      "src/core/workshop-outputs.js",
+      "src/pages/report-styles.js",
+      "src/pages/view-pool.js",
+      "src/pages/view-report.js",
+      "src/pages/view-workshop-outputs.js"
+    ],
+    "unchanged": [
+      "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "lib/vendor/lit/v3.3.3/index.js",
+      "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "src/app.config.js",
+      "src/app.js",
+      "src/assets/inter-latin.woff2",
+      "src/components/base.js",
+      "src/components/cjk.js",
+      "src/components/icons.js",
+      "src/components/index.js",
+      "src/components/kf-badge.js",
+      "src/components/kf-drawer.js",
+      "src/components/kf-empty.js",
+      "src/components/kf-evidence-chain.js",
+      "src/components/kf-glass-card.js",
+      "src/components/kf-member-picker.js",
+      "src/components/kf-pipeline.js",
+      "src/components/kf-source-detail.js",
+      "src/components/kf-stat.js",
+      "src/components/kf-table.js",
+      "src/components/kf-toast.js",
+      "src/components/kf-viz.js",
+      "src/core/brand-shop-filters.js",
+      "src/core/development-references.js",
+      "src/core/evidence/chain.js",
+      "src/core/index.js",
+      "src/core/industry-brief.js",
+      "src/core/industry-materials.js",
+      "src/core/industry-navigation.js",
+      "src/core/industry-planner.js",
+      "src/core/industry-research-directions.js",
+      "src/core/industry-workshop.js",
+      "src/core/kfs/build.js",
+      "src/core/kfs/employee.js",
+      "src/core/kfs/ledger.js",
+      "src/core/kfs/stats.js",
+      "src/core/kfs/util.js",
+      "src/core/library-selection.js",
+      "src/core/library.js",
+      "src/core/library/reports.js",
+      "src/core/library/types.js",
+      "src/core/member-groups.js",
+      "src/core/opportunity/opportunity.js",
+      "src/core/opportunity/report.js",
+      "src/core/opportunity/score.js",
+      "src/core/report-attachments.js",
+      "src/core/report-delivery.js",
+      "src/core/report-evidence.js",
+      "src/core/report-statistics.js",
+      "src/core/report.js",
+      "src/core/taxonomy/taxonomy.js",
+      "src/core/workshop-roster.js",
+      "src/data/development-references.json",
+      "src/services/auth-service.js",
+      "src/services/board-service.js",
+      "src/services/development-service.js",
+      "src/services/industry-service.js",
+      "src/services/library-images.js",
+      "src/services/library-pages.js",
+      "src/services/library-service.js",
+      "src/services/member-groups.js",
+      "src/services/platform-auth.js",
+      "src/services/workshop-data.js",
+      "src/styles/filter-controls.js",
+      "src/styles/tokens.js",
+      "src/utils/card-view.js",
+      "src/utils/cjk-deep.js",
+      "src/utils/http.js",
+      "src/utils/router.js",
+      "src/utils/storage.js",
+      "src/utils/type.js",
+      "src/pages/card-toolbar.js",
+      "src/pages/filter-sheet.js",
+      "src/pages/industry-dashboard-view.js",
+      "src/pages/industry-decision-view.js",
+      "src/pages/industry-detail-view.js",
+      "src/pages/industry-development-view.js",
+      "src/pages/industry-innovation-view.js",
+      "src/pages/industry-pipeline-view.js",
+      "src/pages/industry-planner-view.js",
+      "src/pages/industry-policy-view.js",
+      "src/pages/industry-ranking-view.js",
+      "src/pages/industry-research-direction-view.js",
+      "src/pages/industry-research-views.js",
+      "src/pages/industry-scope-filters.js",
+      "src/pages/industry-workshop-view.js",
+      "src/pages/library-product-detail.js",
+      "src/pages/library-product-image.js",
+      "src/pages/library-styles.js",
+      "src/pages/mobile-research-styles.js",
+      "src/pages/policy-reviewed-evidence.js",
+      "src/pages/report-card.js",
+      "src/pages/report-pager.js",
+      "src/pages/shared.js",
+      "src/pages/view-industry.js",
+      "src/pages/view-ledger.js",
+      "src/pages/view-library.js",
+      "src/pages/view-login.js",
+      "src/pages/view-members.js",
+      "src/pages/view-report-type.js",
+      "src/pages/view-reports.js",
+      "src/pages/view-stages.js",
+      "src/pages/view-workbench.js",
+      "src/layout/detail-styles.js",
+      "src/layout/kf-nav.js",
+      "src/layout/kf-topbar.js",
+      "src/layout/kf-app.js"
+    ],
+    "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+    "homepageModified": false,
+    "networkRequests": 0,
+    "cdnCacheRefreshed": false,
+    "publicSiteVerified": false,
+    "modeRepairs": [
+      "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "lib/vendor/lit/v3.3.3/index.js",
+      "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "src/app.config.js",
+      "src/app.js",
+      "src/assets/inter-latin.woff2",
+      "src/components/base.js",
+      "src/components/cjk.js",
+      "src/components/icons.js",
+      "src/components/index.js",
+      "src/components/kf-badge.js",
+      "src/components/kf-drawer.js",
+      "src/components/kf-empty.js",
+      "src/components/kf-evidence-chain.js",
+      "src/components/kf-glass-card.js",
+      "src/components/kf-member-picker.js",
+      "src/components/kf-pipeline.js",
+      "src/components/kf-source-detail.js",
+      "src/components/kf-stat.js",
+      "src/components/kf-table.js",
+      "src/components/kf-toast.js",
+      "src/components/kf-viz.js",
+      "src/core/brand-shop-filters.js",
+      "src/core/development-references.js",
+      "src/core/evidence/chain.js",
+      "src/core/index.js",
+      "src/core/industry-brief.js",
+      "src/core/industry-materials.js",
+      "src/core/industry-navigation.js",
+      "src/core/industry-planner.js",
+      "src/core/industry-research-directions.js",
+      "src/core/industry-workshop.js",
+      "src/core/kfs/build.js",
+      "src/core/kfs/employee.js",
+      "src/core/kfs/ledger.js",
+      "src/core/kfs/stats.js",
+      "src/core/kfs/util.js",
+      "src/core/library-selection.js",
+      "src/core/library.js",
+      "src/core/library/reports.js",
+      "src/core/library/types.js",
+      "src/core/member-groups.js",
+      "src/core/opportunity/opportunity.js",
+      "src/core/opportunity/report.js",
+      "src/core/opportunity/score.js",
+      "src/core/report-attachments.js",
+      "src/core/report-delivery.js",
+      "src/core/report-evidence.js",
+      "src/core/report-statistics.js",
+      "src/core/report.js",
+      "src/core/taxonomy/taxonomy.js",
+      "src/core/workshop-roster.js",
+      "src/data/development-references.json",
+      "src/services/auth-service.js",
+      "src/services/board-service.js",
+      "src/services/development-service.js",
+      "src/services/industry-service.js",
+      "src/services/library-images.js",
+      "src/services/library-pages.js",
+      "src/services/library-service.js",
+      "src/services/member-groups.js",
+      "src/services/platform-auth.js",
+      "src/services/workshop-data.js",
+      "src/styles/filter-controls.js",
+      "src/styles/tokens.js",
+      "src/utils/card-view.js",
+      "src/utils/cjk-deep.js",
+      "src/utils/http.js",
+      "src/utils/router.js",
+      "src/utils/storage.js",
+      "src/utils/type.js",
+      "src/pages/card-toolbar.js",
+      "src/pages/filter-sheet.js",
+      "src/pages/industry-dashboard-view.js",
+      "src/pages/industry-decision-view.js",
+      "src/pages/industry-detail-view.js",
+      "src/pages/industry-development-view.js",
+      "src/pages/industry-innovation-view.js",
+      "src/pages/industry-pipeline-view.js",
+      "src/pages/industry-planner-view.js",
+      "src/pages/industry-policy-view.js",
+      "src/pages/industry-ranking-view.js",
+      "src/pages/industry-research-direction-view.js",
+      "src/pages/industry-research-views.js",
+      "src/pages/industry-scope-filters.js",
+      "src/pages/industry-workshop-view.js",
+      "src/pages/library-product-detail.js",
+      "src/pages/library-product-image.js",
+      "src/pages/library-styles.js",
+      "src/pages/mobile-research-styles.js",
+      "src/pages/policy-reviewed-evidence.js",
+      "src/pages/report-card.js",
+      "src/pages/report-pager.js",
+      "src/pages/shared.js",
+      "src/pages/view-industry.js",
+      "src/pages/view-ledger.js",
+      "src/pages/view-library.js",
+      "src/pages/view-login.js",
+      "src/pages/view-members.js",
+      "src/pages/view-report-type.js",
+      "src/pages/view-reports.js",
+      "src/pages/view-stages.js",
+      "src/pages/view-workbench.js",
+      "src/layout/detail-styles.js",
+      "src/layout/kf-nav.js",
+      "src/layout/kf-topbar.js",
+      "src/layout/kf-app.js"
+    ],
+    "backup": null,
+    "filesystemVerified": false,
+    "businessRecordWrites": 0,
+    "backendFunctionsModified": false,
+    "serviceRestarted": false
+  },
+  "apply": {
+    "ok": true,
+    "operation": "filesystem-update",
+    "readOnly": false,
+    "targetDir": "C:\\Users\\JSHY\\AppData\\Local\\Temp\\ksf-v3-simulation-Io1c9H\\site",
+    "release": "ksf-board-v3-20261112-r2",
+    "filesChecked": 121,
+    "changes": [
+      "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+      "src/components/kf-opportunity-card.js",
+      "src/components/kf-score-panel.js",
+      "src/core/course-assessment.js",
+      "src/core/workshop-outputs.js",
+      "src/pages/report-styles.js",
+      "src/pages/view-pool.js",
+      "src/pages/view-report.js",
+      "src/pages/view-workshop-outputs.js"
+    ],
+    "unchanged": [
+      "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "lib/vendor/lit/v3.3.3/index.js",
+      "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "src/app.config.js",
+      "src/app.js",
+      "src/assets/inter-latin.woff2",
+      "src/components/base.js",
+      "src/components/cjk.js",
+      "src/components/icons.js",
+      "src/components/index.js",
+      "src/components/kf-badge.js",
+      "src/components/kf-drawer.js",
+      "src/components/kf-empty.js",
+      "src/components/kf-evidence-chain.js",
+      "src/components/kf-glass-card.js",
+      "src/components/kf-member-picker.js",
+      "src/components/kf-pipeline.js",
+      "src/components/kf-source-detail.js",
+      "src/components/kf-stat.js",
+      "src/components/kf-table.js",
+      "src/components/kf-toast.js",
+      "src/components/kf-viz.js",
+      "src/core/brand-shop-filters.js",
+      "src/core/development-references.js",
+      "src/core/evidence/chain.js",
+      "src/core/index.js",
+      "src/core/industry-brief.js",
+      "src/core/industry-materials.js",
+      "src/core/industry-navigation.js",
+      "src/core/industry-planner.js",
+      "src/core/industry-research-directions.js",
+      "src/core/industry-workshop.js",
+      "src/core/kfs/build.js",
+      "src/core/kfs/employee.js",
+      "src/core/kfs/ledger.js",
+      "src/core/kfs/stats.js",
+      "src/core/kfs/util.js",
+      "src/core/library-selection.js",
+      "src/core/library.js",
+      "src/core/library/reports.js",
+      "src/core/library/types.js",
+      "src/core/member-groups.js",
+      "src/core/opportunity/opportunity.js",
+      "src/core/opportunity/report.js",
+      "src/core/opportunity/score.js",
+      "src/core/report-attachments.js",
+      "src/core/report-delivery.js",
+      "src/core/report-evidence.js",
+      "src/core/report-statistics.js",
+      "src/core/report.js",
+      "src/core/taxonomy/taxonomy.js",
+      "src/core/workshop-roster.js",
+      "src/data/development-references.json",
+      "src/services/auth-service.js",
+      "src/services/board-service.js",
+      "src/services/development-service.js",
+      "src/services/industry-service.js",
+      "src/services/library-images.js",
+      "src/services/library-pages.js",
+      "src/services/library-service.js",
+      "src/services/member-groups.js",
+      "src/services/platform-auth.js",
+      "src/services/workshop-data.js",
+      "src/styles/filter-controls.js",
+      "src/styles/tokens.js",
+      "src/utils/card-view.js",
+      "src/utils/cjk-deep.js",
+      "src/utils/http.js",
+      "src/utils/router.js",
+      "src/utils/storage.js",
+      "src/utils/type.js",
+      "src/pages/card-toolbar.js",
+      "src/pages/filter-sheet.js",
+      "src/pages/industry-dashboard-view.js",
+      "src/pages/industry-decision-view.js",
+      "src/pages/industry-detail-view.js",
+      "src/pages/industry-development-view.js",
+      "src/pages/industry-innovation-view.js",
+      "src/pages/industry-pipeline-view.js",
+      "src/pages/industry-planner-view.js",
+      "src/pages/industry-policy-view.js",
+      "src/pages/industry-ranking-view.js",
+      "src/pages/industry-research-direction-view.js",
+      "src/pages/industry-research-views.js",
+      "src/pages/industry-scope-filters.js",
+      "src/pages/industry-workshop-view.js",
+      "src/pages/library-product-detail.js",
+      "src/pages/library-product-image.js",
+      "src/pages/library-styles.js",
+      "src/pages/mobile-research-styles.js",
+      "src/pages/policy-reviewed-evidence.js",
+      "src/pages/report-card.js",
+      "src/pages/report-pager.js",
+      "src/pages/shared.js",
+      "src/pages/view-industry.js",
+      "src/pages/view-ledger.js",
+      "src/pages/view-library.js",
+      "src/pages/view-login.js",
+      "src/pages/view-members.js",
+      "src/pages/view-report-type.js",
+      "src/pages/view-reports.js",
+      "src/pages/view-stages.js",
+      "src/pages/view-workbench.js",
+      "src/layout/detail-styles.js",
+      "src/layout/kf-nav.js",
+      "src/layout/kf-topbar.js",
+      "src/layout/kf-app.js"
+    ],
+    "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+    "homepageModified": false,
+    "networkRequests": 0,
+    "cdnCacheRefreshed": false,
+    "publicSiteVerified": false,
+    "backup": "C:\\Users\\JSHY\\AppData\\Local\\Temp\\ksf-v3-simulation-Io1c9H\\ksf-industry-ui-backup-0icnq5",
+    "filesystemVerified": true,
+    "publicVerificationRequired": true,
+    "businessRecordWrites": 0,
+    "backendFunctionsModified": false,
+    "serviceRestarted": false
+  },
+  "noop": {
+    "ok": true,
+    "operation": "read-only-plan",
+    "readOnly": true,
+    "targetDir": "C:\\Users\\JSHY\\AppData\\Local\\Temp\\ksf-v3-simulation-Io1c9H\\site",
+    "release": "ksf-board-v3-20261112-r2",
+    "filesChecked": 121,
+    "changes": [],
+    "unchanged": [
+      "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "lib/vendor/lit/v3.3.3/index.js",
+      "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+      "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "src/app.config.js",
+      "src/app.js",
+      "src/assets/inter-latin.woff2",
+      "src/components/base.js",
+      "src/components/cjk.js",
+      "src/components/icons.js",
+      "src/components/index.js",
+      "src/components/kf-badge.js",
+      "src/components/kf-drawer.js",
+      "src/components/kf-empty.js",
+      "src/components/kf-evidence-chain.js",
+      "src/components/kf-glass-card.js",
+      "src/components/kf-member-picker.js",
+      "src/components/kf-opportunity-card.js",
+      "src/components/kf-pipeline.js",
+      "src/components/kf-score-panel.js",
+      "src/components/kf-source-detail.js",
+      "src/components/kf-stat.js",
+      "src/components/kf-table.js",
+      "src/components/kf-toast.js",
+      "src/components/kf-viz.js",
+      "src/core/brand-shop-filters.js",
+      "src/core/course-assessment.js",
+      "src/core/development-references.js",
+      "src/core/evidence/chain.js",
+      "src/core/index.js",
+      "src/core/industry-brief.js",
+      "src/core/industry-materials.js",
+      "src/core/industry-navigation.js",
+      "src/core/industry-planner.js",
+      "src/core/industry-research-directions.js",
+      "src/core/industry-workshop.js",
+      "src/core/kfs/build.js",
+      "src/core/kfs/employee.js",
+      "src/core/kfs/ledger.js",
+      "src/core/kfs/stats.js",
+      "src/core/kfs/util.js",
+      "src/core/library-selection.js",
+      "src/core/library.js",
+      "src/core/library/reports.js",
+      "src/core/library/types.js",
+      "src/core/member-groups.js",
+      "src/core/opportunity/opportunity.js",
+      "src/core/opportunity/report.js",
+      "src/core/opportunity/score.js",
+      "src/core/report-attachments.js",
+      "src/core/report-delivery.js",
+      "src/core/report-evidence.js",
+      "src/core/report-statistics.js",
+      "src/core/report.js",
+      "src/core/taxonomy/taxonomy.js",
+      "src/core/workshop-outputs.js",
+      "src/core/workshop-roster.js",
+      "src/data/development-references.json",
+      "src/services/auth-service.js",
+      "src/services/board-service.js",
+      "src/services/development-service.js",
+      "src/services/industry-service.js",
+      "src/services/library-images.js",
+      "src/services/library-pages.js",
+      "src/services/library-service.js",
+      "src/services/member-groups.js",
+      "src/services/platform-auth.js",
+      "src/services/workshop-data.js",
+      "src/styles/filter-controls.js",
+      "src/styles/tokens.js",
+      "src/utils/card-view.js",
+      "src/utils/cjk-deep.js",
+      "src/utils/http.js",
+      "src/utils/router.js",
+      "src/utils/storage.js",
+      "src/utils/type.js",
+      "src/pages/card-toolbar.js",
+      "src/pages/filter-sheet.js",
+      "src/pages/industry-dashboard-view.js",
+      "src/pages/industry-decision-view.js",
+      "src/pages/industry-detail-view.js",
+      "src/pages/industry-development-view.js",
+      "src/pages/industry-innovation-view.js",
+      "src/pages/industry-pipeline-view.js",
+      "src/pages/industry-planner-view.js",
+      "src/pages/industry-policy-view.js",
+      "src/pages/industry-ranking-view.js",
+      "src/pages/industry-research-direction-view.js",
+      "src/pages/industry-research-views.js",
+      "src/pages/industry-scope-filters.js",
+      "src/pages/industry-workshop-view.js",
+      "src/pages/library-product-detail.js",
+      "src/pages/library-product-image.js",
+      "src/pages/library-styles.js",
+      "src/pages/mobile-research-styles.js",
+      "src/pages/policy-reviewed-evidence.js",
+      "src/pages/report-card.js",
+      "src/pages/report-pager.js",
+      "src/pages/report-styles.js",
+      "src/pages/shared.js",
+      "src/pages/view-industry.js",
+      "src/pages/view-ledger.js",
+      "src/pages/view-library.js",
+      "src/pages/view-login.js",
+      "src/pages/view-members.js",
+      "src/pages/view-pool.js",
+      "src/pages/view-report-type.js",
+      "src/pages/view-report.js",
+      "src/pages/view-reports.js",
+      "src/pages/view-stages.js",
+      "src/pages/view-workbench.js",
+      "src/pages/view-workshop-outputs.js",
+      "src/layout/detail-styles.js",
+      "src/layout/kf-nav.js",
+      "src/layout/kf-topbar.js",
+      "src/layout/kf-app.js"
+    ],
+    "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+    "homepageModified": false,
+    "networkRequests": 0,
+    "cdnCacheRefreshed": false,
+    "publicSiteVerified": false,
+    "modeRepairs": [
+      "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "lib/vendor/lit/v3.3.3/index.js",
+      "skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+      "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "src/app.config.js",
+      "src/app.js",
+      "src/assets/inter-latin.woff2",
+      "src/components/base.js",
+      "src/components/cjk.js",
+      "src/components/icons.js",
+      "src/components/index.js",
+      "src/components/kf-badge.js",
+      "src/components/kf-drawer.js",
+      "src/components/kf-empty.js",
+      "src/components/kf-evidence-chain.js",
+      "src/components/kf-glass-card.js",
+      "src/components/kf-member-picker.js",
+      "src/components/kf-opportunity-card.js",
+      "src/components/kf-pipeline.js",
+      "src/components/kf-score-panel.js",
+      "src/components/kf-source-detail.js",
+      "src/components/kf-stat.js",
+      "src/components/kf-table.js",
+      "src/components/kf-toast.js",
+      "src/components/kf-viz.js",
+      "src/core/brand-shop-filters.js",
+      "src/core/course-assessment.js",
+      "src/core/development-references.js",
+      "src/core/evidence/chain.js",
+      "src/core/index.js",
+      "src/core/industry-brief.js",
+      "src/core/industry-materials.js",
+      "src/core/industry-navigation.js",
+      "src/core/industry-planner.js",
+      "src/core/industry-research-directions.js",
+      "src/core/industry-workshop.js",
+      "src/core/kfs/build.js",
+      "src/core/kfs/employee.js",
+      "src/core/kfs/ledger.js",
+      "src/core/kfs/stats.js",
+      "src/core/kfs/util.js",
+      "src/core/library-selection.js",
+      "src/core/library.js",
+      "src/core/library/reports.js",
+      "src/core/library/types.js",
+      "src/core/member-groups.js",
+      "src/core/opportunity/opportunity.js",
+      "src/core/opportunity/report.js",
+      "src/core/opportunity/score.js",
+      "src/core/report-attachments.js",
+      "src/core/report-delivery.js",
+      "src/core/report-evidence.js",
+      "src/core/report-statistics.js",
+      "src/core/report.js",
+      "src/core/taxonomy/taxonomy.js",
+      "src/core/workshop-outputs.js",
+      "src/core/workshop-roster.js",
+      "src/data/development-references.json",
+      "src/services/auth-service.js",
+      "src/services/board-service.js",
+      "src/services/development-service.js",
+      "src/services/industry-service.js",
+      "src/services/library-images.js",
+      "src/services/library-pages.js",
+      "src/services/library-service.js",
+      "src/services/member-groups.js",
+      "src/services/platform-auth.js",
+      "src/services/workshop-data.js",
+      "src/styles/filter-controls.js",
+      "src/styles/tokens.js",
+      "src/utils/card-view.js",
+      "src/utils/cjk-deep.js",
+      "src/utils/http.js",
+      "src/utils/router.js",
+      "src/utils/storage.js",
+      "src/utils/type.js",
+      "src/pages/card-toolbar.js",
+      "src/pages/filter-sheet.js",
+      "src/pages/industry-dashboard-view.js",
+      "src/pages/industry-decision-view.js",
+      "src/pages/industry-detail-view.js",
+      "src/pages/industry-development-view.js",
+      "src/pages/industry-innovation-view.js",
+      "src/pages/industry-pipeline-view.js",
+      "src/pages/industry-planner-view.js",
+      "src/pages/industry-policy-view.js",
+      "src/pages/industry-ranking-view.js",
+      "src/pages/industry-research-direction-view.js",
+      "src/pages/industry-research-views.js",
+      "src/pages/industry-scope-filters.js",
+      "src/pages/industry-workshop-view.js",
+      "src/pages/library-product-detail.js",
+      "src/pages/library-product-image.js",
+      "src/pages/library-styles.js",
+      "src/pages/mobile-research-styles.js",
+      "src/pages/policy-reviewed-evidence.js",
+      "src/pages/report-card.js",
+      "src/pages/report-pager.js",
+      "src/pages/report-styles.js",
+      "src/pages/shared.js",
+      "src/pages/view-industry.js",
+      "src/pages/view-ledger.js",
+      "src/pages/view-library.js",
+      "src/pages/view-login.js",
+      "src/pages/view-members.js",
+      "src/pages/view-pool.js",
+      "src/pages/view-report-type.js",
+      "src/pages/view-report.js",
+      "src/pages/view-reports.js",
+      "src/pages/view-stages.js",
+      "src/pages/view-workbench.js",
+      "src/pages/view-workshop-outputs.js",
+      "src/layout/detail-styles.js",
+      "src/layout/kf-nav.js",
+      "src/layout/kf-topbar.js",
+      "src/layout/kf-app.js"
+    ],
+    "backup": null,
+    "filesystemVerified": false,
+    "businessRecordWrites": 0,
+    "backendFunctionsModified": false,
+    "serviceRestarted": false
+  }
+}

+ 168 - 0
docs/board-v3-release-20261112/modules-local.json

@@ -0,0 +1,168 @@
+{
+  "base": "http://127.0.0.1:4181/",
+  "moduleCount": 119,
+  "modules": [
+    "http://127.0.0.1:4181/src/app.js",
+    "http://127.0.0.1:4181/src/components/base.js",
+    "http://127.0.0.1:4181/src/app.config.js",
+    "http://127.0.0.1:4181/src/styles/tokens.js",
+    "http://127.0.0.1:4181/src/layout/kf-app.js",
+    "http://127.0.0.1:4181/src/styles/filter-controls.js",
+    "http://127.0.0.1:4181/lib/vendor/lit/v3.3.3/index.js",
+    "http://127.0.0.1:4181/lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+    "http://127.0.0.1:4181/src/components/cjk.js",
+    "http://127.0.0.1:4181/src/services/board-service.js",
+    "http://127.0.0.1:4181/src/services/auth-service.js",
+    "http://127.0.0.1:4181/src/core/opportunity/report.js",
+    "http://127.0.0.1:4181/src/utils/router.js",
+    "http://127.0.0.1:4181/src/layout/detail-styles.js",
+    "http://127.0.0.1:4181/src/core/industry-navigation.js",
+    "http://127.0.0.1:4181/src/core/index.js",
+    "http://127.0.0.1:4181/src/components/index.js",
+    "http://127.0.0.1:4181/src/components/kf-source-detail.js",
+    "http://127.0.0.1:4181/src/layout/kf-nav.js",
+    "http://127.0.0.1:4181/src/layout/kf-topbar.js",
+    "http://127.0.0.1:4181/src/pages/view-workbench.js",
+    "http://127.0.0.1:4181/src/pages/view-pool.js",
+    "http://127.0.0.1:4181/src/pages/view-stages.js",
+    "http://127.0.0.1:4181/src/pages/view-reports.js",
+    "http://127.0.0.1:4181/src/pages/view-report-type.js",
+    "http://127.0.0.1:4181/src/pages/view-report.js",
+    "http://127.0.0.1:4181/src/pages/view-ledger.js",
+    "http://127.0.0.1:4181/src/pages/view-members.js",
+    "http://127.0.0.1:4181/src/pages/view-workshop-outputs.js",
+    "http://127.0.0.1:4181/src/pages/view-library.js",
+    "http://127.0.0.1:4181/src/pages/view-industry.js",
+    "http://127.0.0.1:4181/src/pages/view-login.js",
+    "http://127.0.0.1:4181/lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+    "http://127.0.0.1:4181/lib/vendor/lit-html/v3.3.3/lit-html.js",
+    "http://127.0.0.1:4181/src/utils/type.js",
+    "http://127.0.0.1:4181/src/utils/cjk-deep.js",
+    "http://127.0.0.1:4181/src/services/workshop-data.js",
+    "http://127.0.0.1:4181/src/utils/storage.js",
+    "http://127.0.0.1:4181/src/core/workshop-roster.js",
+    "http://127.0.0.1:4181/src/services/platform-auth.js",
+    "http://127.0.0.1:4181/src/core/report-delivery.js",
+    "http://127.0.0.1:4181/src/core/taxonomy/taxonomy.js",
+    "http://127.0.0.1:4181/src/core/opportunity/opportunity.js",
+    "http://127.0.0.1:4181/src/core/opportunity/score.js",
+    "http://127.0.0.1:4181/src/core/evidence/chain.js",
+    "http://127.0.0.1:4181/src/core/report.js",
+    "http://127.0.0.1:4181/src/core/library/types.js",
+    "http://127.0.0.1:4181/src/core/library/reports.js",
+    "http://127.0.0.1:4181/src/core/kfs/util.js",
+    "http://127.0.0.1:4181/src/core/kfs/ledger.js",
+    "http://127.0.0.1:4181/src/core/kfs/build.js",
+    "http://127.0.0.1:4181/src/core/kfs/stats.js",
+    "http://127.0.0.1:4181/src/components/kf-glass-card.js",
+    "http://127.0.0.1:4181/src/components/kf-stat.js",
+    "http://127.0.0.1:4181/src/components/kf-badge.js",
+    "http://127.0.0.1:4181/src/components/kf-empty.js",
+    "http://127.0.0.1:4181/src/components/kf-table.js",
+    "http://127.0.0.1:4181/src/components/kf-drawer.js",
+    "http://127.0.0.1:4181/src/components/kf-opportunity-card.js",
+    "http://127.0.0.1:4181/src/components/kf-score-panel.js",
+    "http://127.0.0.1:4181/src/components/kf-evidence-chain.js",
+    "http://127.0.0.1:4181/src/components/kf-pipeline.js",
+    "http://127.0.0.1:4181/src/components/kf-toast.js",
+    "http://127.0.0.1:4181/src/components/kf-viz.js",
+    "http://127.0.0.1:4181/src/components/icons.js",
+    "http://127.0.0.1:4181/src/core/library.js",
+    "http://127.0.0.1:4181/src/pages/shared.js",
+    "http://127.0.0.1:4181/src/pages/report-card.js",
+    "http://127.0.0.1:4181/src/components/kf-member-picker.js",
+    "http://127.0.0.1:4181/lib/vendor/lit/v3.3.3/directives/ref.js",
+    "http://127.0.0.1:4181/src/pages/filter-sheet.js",
+    "http://127.0.0.1:4181/src/utils/card-view.js",
+    "http://127.0.0.1:4181/src/pages/report-pager.js",
+    "http://127.0.0.1:4181/src/pages/card-toolbar.js",
+    "http://127.0.0.1:4181/lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+    "http://127.0.0.1:4181/lib/vendor/lit/v3.3.3/directives/keyed.js",
+    "http://127.0.0.1:4181/src/core/report-attachments.js",
+    "http://127.0.0.1:4181/src/core/report-evidence.js",
+    "http://127.0.0.1:4181/src/core/report-statistics.js",
+    "http://127.0.0.1:4181/src/core/course-assessment.js",
+    "http://127.0.0.1:4181/src/pages/report-styles.js",
+    "http://127.0.0.1:4181/src/core/member-groups.js",
+    "http://127.0.0.1:4181/src/services/member-groups.js",
+    "http://127.0.0.1:4181/src/core/kfs/employee.js",
+    "http://127.0.0.1:4181/src/core/workshop-outputs.js",
+    "http://127.0.0.1:4181/skills/ksf-workshop-runtime/scripts/course-deliverables.mjs",
+    "http://127.0.0.1:4181/src/pages/library-styles.js",
+    "http://127.0.0.1:4181/src/core/brand-shop-filters.js",
+    "http://127.0.0.1:4181/src/services/library-service.js",
+    "http://127.0.0.1:4181/src/core/library-selection.js",
+    "http://127.0.0.1:4181/src/pages/library-product-image.js",
+    "http://127.0.0.1:4181/src/pages/library-product-detail.js",
+    "http://127.0.0.1:4181/src/services/industry-service.js",
+    "http://127.0.0.1:4181/src/pages/industry-research-views.js",
+    "http://127.0.0.1:4181/src/pages/industry-policy-view.js",
+    "http://127.0.0.1:4181/src/pages/industry-pipeline-view.js",
+    "http://127.0.0.1:4181/src/pages/industry-ranking-view.js",
+    "http://127.0.0.1:4181/src/pages/industry-detail-view.js",
+    "http://127.0.0.1:4181/src/pages/industry-innovation-view.js",
+    "http://127.0.0.1:4181/src/core/industry-planner.js",
+    "http://127.0.0.1:4181/src/pages/industry-planner-view.js",
+    "http://127.0.0.1:4181/src/core/industry-workshop.js",
+    "http://127.0.0.1:4181/src/pages/industry-scope-filters.js",
+    "http://127.0.0.1:4181/src/pages/industry-workshop-view.js",
+    "http://127.0.0.1:4181/src/pages/industry-decision-view.js",
+    "http://127.0.0.1:4181/src/pages/mobile-research-styles.js",
+    "http://127.0.0.1:4181/src/core/industry-materials.js",
+    "http://127.0.0.1:4181/src/services/library-images.js",
+    "http://127.0.0.1:4181/src/services/development-service.js",
+    "http://127.0.0.1:4181/src/pages/industry-development-view.js",
+    "http://127.0.0.1:4181/src/utils/http.js",
+    "http://127.0.0.1:4181/skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+    "http://127.0.0.1:4181/src/services/library-pages.js",
+    "http://127.0.0.1:4181/src/pages/policy-reviewed-evidence.js",
+    "http://127.0.0.1:4181/src/core/industry-brief.js",
+    "http://127.0.0.1:4181/src/core/industry-research-directions.js",
+    "http://127.0.0.1:4181/src/pages/industry-research-direction-view.js",
+    "http://127.0.0.1:4181/src/pages/industry-dashboard-view.js",
+    "http://127.0.0.1:4181/src/core/development-references.js"
+  ],
+  "failures": [],
+  "results": [
+    {
+      "name": "index.html 200(http://127.0.0.1:4181/)",
+      "ok": true,
+      "detail": "status=200"
+    },
+    {
+      "name": "<base href> 存在",
+      "ok": true,
+      "detail": "= /"
+    },
+    {
+      "name": "importmap 内联且可解析",
+      "ok": true,
+      "detail": "14 条映射"
+    },
+    {
+      "name": "入口 <script type=\"module\"> 存在",
+      "ok": true,
+      "detail": "/src/app.js"
+    },
+    {
+      "name": "模块图共 119 个请求全部 200 + text/javascript",
+      "ok": true,
+      "detail": ""
+    },
+    {
+      "name": "没有无法解析的裸标识符",
+      "ok": true,
+      "detail": ""
+    },
+    {
+      "name": "lit 单实例(模块图里 2 个 vendor 包,每包一个版本)",
+      "ok": true,
+      "detail": ""
+    },
+    {
+      "name": "importmap 每条目标可达",
+      "ok": true,
+      "detail": ""
+    }
+  ]
+}

+ 15 - 0
docs/board-v3-release-20261112/static-check.txt

@@ -0,0 +1,15 @@
+✓ [1] 无装饰器语法
+✓ [2] 相对 import 全带 .js/.mjs 且目标存在
+✓ [3] 无 window.KF_* 全局挂载
+✓ [4] 无 innerHTML 拼接
+✓ [5] core 层不碰 DOM/fetch(0 个文件)
+✓ [6] 视图/组件不直接 fetch(且 services 也不直连 fetch)
+✓ [7] base href / importmap / 部署前缀 三处同源(env=local)
+✓ [8] 无 @lit-labs/router
+✓ [9] lit 单实例(importmap 每包一个版本,源码不绕过 importmap)
+✓ [C1] 前端无凭据(masterKey / 密钥值)
+✓ [S] 全部 .js 通过 node --check
+✓ [I] 裸标识符都在 importmap 里
+✓ [E] 自定义元素标签带连字符、注册幂等、wc 用 Lit 实现(16 个)
+
+CHECK OK

+ 28 - 0
docs/board-v3-release-20261112/tests-deployment.txt

@@ -0,0 +1,28 @@
+✔ 完整前端发布预检只读,apply可重复且不替换首页 (1382.0377ms)
+✔ 未知现场拒绝,写入失败恢复,新静态文件保持0644 (944.3861ms)
+✔ 成员接口未知版本在写入前停止 (0.6276ms)
+✔ 成员接口已发布版本只读和apply均不重复写入 (0.3304ms)
+✔ six-file UI release applies and is idempotent while preserving an independent report update (128.7566ms)
+✔ UI release refuses an unexpected file list and an unknown baseline before writing (26.2764ms)
+✔ UI release rolls back both replaced files and its newly added component on a write failure (70.687ms)
+✔ v3发布检查完整浏览器依赖,原子更新后重复执行无变化,首页不变 (1812.596ms)
+✔ v3发布拒绝未知现场、额外文件和首页变化,写入失败回退已改文件及新增模块 (1261.8681ms)
+✔ 有效配置与真实模块响应均通过,预检保持只读 (40.7908ms)
+✔ 旧配置缺少mjs映射时,在请求前停止,注释不能冒充映射 (90.3583ms)
+✔ 配置正确但响应为octet-stream时拒绝,复现白屏原因 (6.3801ms)
+✔ SPA返回HTML或内容不符不能因200和JavaScript响应头而通过 (6.6471ms)
+✔ 首次发布尚不存在的模块明确留待发布后验证 (7.4127ms)
+✔ 统计卡从同一n/N计算占比,保留范围、限制、产品含义和实际证据键 (3.5198ms)
+✔ 未核分母或无有效材料不编造百分比,已确认零提及保留0% (0.8884ms)
+✔ 非法数值、口径缺失、错列和不存在的证据键在保存前拒绝 (1.7947ms)
+✔ 多标签主题可重叠,不要求各主题相加等于100%,原始数量不被修改 (0.8752ms)
+✔ 看板排版薄空格不改变统计识别,浏览器与HTML的统计校验保持同步 (0.9832ms)
+✔ 统计表完整保留在默认HTML附件与同版report,生成器不从精选引文数量推断分子 (4.7066ms)
+ℹ tests 20
+ℹ suites 0
+ℹ pass 20
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 3199.1426

+ 242 - 0
docs/board-v3-release-20261112/tests-frontend.txt

@@ -0,0 +1,242 @@
+✔ 综合店铺多品牌切换保留店铺,店铺变化保留已有的兼容品牌 (2.2716ms)
+✔ 切换不兼容品牌或店铺时清除冲突条件,切平台清店铺 (0.345ms)
+✔ 清除一个条件不清另一个;当前范围空结果不篡改已有选择 (0.294ms)
+✔ 联动列表仅保留有数据的选项,未知品牌可选,失效旧选择明确不可用 (0.2759ms)
+✔ 用户分面忽略自身单选条件,显示每位用户真实数量 (3.5101ms)
+✔ 用户分面受阶段和品类交集约束,其他分面保留用户条件 (0.6048ms)
+✔ 报告分页覆盖全部记录且无重复;首尾页与空清单保持有效 (2.7365ms)
+✔ 损坏或非整数 URL 页码不会漏记录、返回空页或 NaN (0.6667ms)
+✔ 翻页与刷新保留证据、阶段、排序和列表视图 (0.3903ms)
+✔ 研发资料来源、品类关系和五维记录可回溯,统计保留候选分母 (3.3227ms)
+✔ 来源丢失、重复ID或关系断开拒绝展示;错误计数不能变成零 (8.0701ms)
+✔ 方向假设保留原评与完整定位,不把待核配方称为已验证结果 (1.1068ms)
+✔ 驳回资料不再作为可用研发参考,原料角色和标准版次分开 (5.3617ms)
+✔ 研发读取请求不带凭据,网络和格式错误直接报告 (2.0729ms)
+✔ industry API requires server-side authorization outside local preview (125.7474ms)
+✔ unconfigured library is unavailable, not an empty successful dataset (26.6866ms)
+✔ industry API rejects unbounded input and non-read transport methods before starting Python (23.692ms)
+✔ 方向资料只包含选中的方向及数据库定位信息 (3.1745ms)
+✔ copy scope preserves explicitly selected directions across focus without adding unselected directions (5.3249ms)
+✔ 方向资料保留完整ID和真实来源,关联记录不继承其他商品的来源 (0.9892ms)
+✔ visual figures use their own true denominators and never turn missing data into zero (0.3583ms)
+✔ old paid collection entrypoints stop before loading credentials or a collector (484.6664ms)
+✔ single choices replace each other; tentative excludes concrete multi choices in both directions (3.0115ms)
+✔ every group must be explicit, invalid saved choices and multiple radio values are not complete (1.4381ms)
+✔ prompt uses exact source IDs and real counts, separates assumptions and rejects incomplete or mismatched data (5.854ms)
+✔ exploratory themes retain real examples without borrowing unrelated hypothesis evidence; tentative keeps broad scope explicit (0.7519ms)
+✔ 未命中方向保留假设与缺口,不借用好评或标题作为痛点 (2.9019ms)
+✔ 场景与动机使用各自现有去重数,不合并或推定交集 (1.1234ms)
+✔ production service requires one explicit data binding and a server-side token (3.0523ms)
+✔ standalone service has no static-file, collection, admin or generic proxy route (141.1642ms)
+✔ 结果分页默认商品,保留旧版原评与市场链接的实际目的地 (1.5675ms)
+✔ 研究多选在组内取或、清除时移除限定,并重置商品与原评页码 (2.7234ms)
+✔ 分析复制保留版本、选项、完整引用,重复证据不反复添加 (2.0576ms)
+✔ 复制保留关键词、原文筛选和重复关联编号,不把其他关联的来源冒充主来源 (0.8986ms)
+✔ 复制决策包含业务假设、验证条件、优势与不在当前页的依据和竞品编号 (1.8123ms)
+✔ 单维多选:任一命中(OR) (4.2336ms)
+✔ 跨维组合:取交集(AND) (0.5129ms)
+✔ 六维全部叠加 (0.3359ms)
+✔ 搜索词跨字段 (0.4025ms)
+✔ URL 状态:条件 → 查询串 → 条件 往返不丢 (0.5219ms)
+✔ 分面计数受其他维度约束、不受本维约束 (0.3163ms)
+✔ toggle 与取值收集 (20.6788ms)
+✔ 店铺必须绑定平台,跨平台/移除平台清店铺和游标,不因店名更改品牌 (4.6275ms)
+✔ 2–4件对比按完整复合编号去重、拒绝第5件,URL刷新可恢复 (2.5766ms)
+✔ 原评价证据固定商品与评价编号,跳详情关闭弹窗但保留列表筛选和所选项 (0.4677ms)
+✔ 价格严格按币种和口径拆行,0元和缺失不混淆,不补算单位价格 (0.7528ms)
+✔ 固定同一代际并限制两路并发,局部失败保留成功列且不填0 (29.0287ms)
+✔ 代际错误/响应代际不一致使整组失效,再读取从新summary开始;不同编号响应不能串列 (1.7537ms)
+✔ 批量图片只匹配完整商品ID,读取时不发送服务凭据 (3.4659ms)
+✔ 图片重复或损坏不串图,图片服务失败保留商品资料 (1.2961ms)
+✔ 新版读取器直接按页读取一次,不走游标兼容路径 (2.589ms)
+✔ 旧接口跨远页按100条跳读,并缓存精确范围/版本下的游标 (4.4923ms)
+✔ 旧接口空范围正确显示0页,读取失败与代际变更不转成空页 (2.4728ms)
+✔ 多选按资料类型和完整ID去重,政策编号不要求平台前缀 (3.9832ms)
+✔ 跨页追加保留每条选取条件,达到上限不悄悄丢掉已有选择 (4.7326ms)
+✔ 刷新恢复选择,代际变化清空,损坏的浏览器保存不当成有效资料 (4.3381ms)
+✔ 复制仅包含资料名称与数据库定位键,保留完整ID和版本,省去提示词 (1.1197ms)
+✔ 行业监测转入精确商品及版本,清理旧对比参数后保留浏览范围 (1.4752ms)
+✔ 主题引文打开固定商品下的精确评价,不继承其他商品或列表条件 (4.1268ms)
+✔ 资料条件同时保留,未知字段和日期不被关键词替代 (2.3499ms)
+✔ 列表每页24条,页码可直接查询,切换筛选或排序回第一页 (1.9832ms)
+✔ 详情深链接保留查询,标签使用真实字典 (0.554ms)
+✔ 商品评论和价格只排序,不缩小结果范围;URL保留排序且不影响商品内评论 (0.5144ms)
+✔ 阅读信息按资料类型选择业务事实,内部规则、编号和指纹不进入展示 (0.6639ms)
+✔ 阅读价格使用元且不暴露分单位和技术字段,单位价格只在核对后展示 (0.4767ms)
+✔ 商品是浏览入口,详情内查询固定商品编号并隔离列表条件和分页 (0.4576ms)
+✔ 接口失败不作为空数据,浏览器不发送服务凭据 (1.8981ms)
+✔ 生产接口默认拒绝匿名及写操作,凭据不进入返回 (1.0435ms)
+✔ 同一员工跨会话、旧键与昵称仍归同人,同名其他身份不合并 (25.5856ms)
+✔ 姓名补充不能改变旧数据的分组编号,前端与服务端成员索引逐项一致 (1.3491ms)
+✔ 分组名独立于成员身份,重命名不改归属,删除只移回未分组 (8.4352ms)
+✔ 重复、空白、保留名、长名及未知成员不会产生有效分组变更 (1.8779ms)
+✔ 分组服务发送版本与稳定编号,网络异常不自动重试 (0.6978ms)
+✔ 27 位已核验账号无需会话即可展示,不伪造成果、进度或活跃时间 (1.4994ms)
+✔ 名册按显式稳定编号关联成果,同名旧记录不自动归入注册账号 (1.3069ms)
+✔ 模型:成果 / 成员 / 机会卡计数与归属 (3.5257ms)
+✔ 视角一「给我的」:交了几份、哪个阶段没交 (0.8178ms)
+✔ 视角二「池子」:总数 / 状态分布 / 最新 / 最高分 (0.542ms)
+✔ 卡住:只列开工了、没交、超时的人名 (0.3014ms)
+✔ 视角三「现场看板」:五阶段 × 人 (0.6804ms)
+✔ 五阶段板块:产出数 + 最新 + 谁产的 (0.5078ms)
+✔ 证据链:结论 → 具体成果名和人名(不存在的如实标出) (0.5718ms)
+✔ 证据链:成果 → 引用它的结论 / 归纳 / 机会卡 (0.7504ms)
+✔ 机会报告只按显式关系找正文,优先结构化报告,再用已保存原文 (1.9379ms)
+✔ 共用报告必须明确评分对象,错误绑定不能误落到另一张机会卡 (1.3112ms)
+✔ 机会卡修复发布默认只读,六个文件备份后更新,重复执行不写且首页不变 (183.0165ms)
+✔ 未知站点版本、损坏文件、首页不匹配和越界清单在写前停止 (187.7978ms)
+✔ 写入失败按备份回退六个文件 (98.5015ms)
+✔ 并发修改不会被失败回退覆盖 (68.5715ms)
+✔ 多人评分:平均、同一人取最后一次、非法分丢弃 (10.0083ms)
+✔ 没有评分时 avg 为 null(页面显示「暂无评分」而不是 0) (0.4417ms)
+✔ Parse Date 对象格式的 at 也能参与时间排序 (1.243ms)
+✔ 五维加权综合分 (0.3968ms)
+✔ 上传四要素:缺哪个就标哪个,不拿 createdAt 冒充 uploadedAt (0.6327ms)
+✔ 排序:优先级与评分切换后顺序真实变化 (1.6929ms)
+✔ 改状态:产出 patch 与一条决策;同状态不改 (1.1891ms)
+✔ 追加评分:同一人重评替换旧分 (0.7961ms)
+✔ 平台登录用POST密码,随后核会话和企业;公开状态仅保留必要身份字段 (6.2075ms)
+✔ 账号密码错误不重试,不把平台原始错误内容交给界面 (2.0352ms)
+✔ 登录返回企业正确也必须以users/me为准,跨企业拒绝并关闭新会话 (1.9777ms)
+✔ 网络、限流、非JSON响应提示可恢复;不会泄露原始响应 (6.7435ms)
+✔ 刷新从服务端重新核验,不信任浏览器伪造的用户或姓名 (0.8526ms)
+✔ 登录只持久化会话,不持久化密码/平台档案;退出先清本机再关闭服务端会话 (0.7683ms)
+✔ 失效会话清除身份,临时网络故障保留可重试凭据但不冒称已登录 (0.6978ms)
+✔ 浏览器禁用存储仍可登录,公开状态不暴露会话 (0.7352ms)
+✔ 退出后才返回的登录请求不恢复身份,并撤销刚创建的会话 (0.6719ms)
+✔ 并发登录仅保留最后一次明确提交的账号;迟到响应不覆盖 (0.6118ms)
+✔ 退出中断刷新核验;恢复请求迟到也不能再次变为登录 (0.4021ms)
+✔ 报告引文发布默认只读,三个文件备份后更新,重复执行不写且首页不变 (143.8187ms)
+✔ 未知站点版本、损坏文件、首页不匹配和越界清单在写前停止 (125.3427ms)
+✔ 写入失败按备份回退三个文件 (70.0887ms)
+✔ 并发修改不会被失败回退覆盖 (50.5629ms)
+✔ excerpts retain exact saved text and resolve original sources without inventing dates or authors (1.5632ms)
+✔ quote-specific source overrides entry source, missing sources stay visibly unresolved (0.3772ms)
+✔ source links reject executable protocols and embedded credentials (0.4285ms)
+✔ missing or malformed quotes never turn a summary into an original quotation (0.9824ms)
+✔ 数据管线:首尾推算过滤率 (2.7416ms)
+✔ 报告概览:章数 / 结论 / 引用 / 原文 / 跨阶段 (3.401ms)
+✔ 结论块去重(同一证据锚点只出现一次) (0.7254ms)
+✔ 表格和段落小结的全部逐字引文都有证据入口,来源关系保留 (1.4675ms)
+✔ 章节归一:缺 id 用序号补齐 (0.2961ms)
+✔ 证据锚点 → 台账行 id(含不存在的成果也如实返回) (0.7254ms)
+✔ 归纳 → 来自的报告 (0.9151ms)
+✔ 时间工具:Parse 日期 / ISO / 毫秒都能读,空值不炸 (3.157ms)
+✔ 决策归一:未知类型保留 label,缺 label 给「决策」 (0.3843ms)
+✔ 卡片摘要不含内部大对象 (1.5556ms)
+✔ 现场看板:没有任何会话时不崩 (0.9197ms)
+✔ 详情的来源和证据链接保留筛选,替换上一种详情,并兼容旧链接 (6.1454ms)
+✔ 机会卡深链接保留筛选、排序和关闭返回地址;报告携带回卡片地址 (1.8186ms)
+✔ 来自其他页面的机会卡返回原页,不将来源页参数误用为卡池筛选 (0.6655ms)
+✔ 直接阅读机会报告保留卡池筛选与明确的评分对象,刷新可重建关联 (0.504ms)
+✔ 推入历史保存离开页的位置,换卡 replace 保留返回凭据且不多压一层 (1.4394ms)
+✔ 订阅分别识别站内导航与浏览器返回,返回读取当前实际路径和位置 (2.3477ms)
+✔ 章节和浮层查询串更新不丢返回地址和历史状态 (1.2283ms)
+✔ 非法或站外返回地址落到应用内兜底,损坏编码匹配为不存在 (2.4952ms)
+✔ 根部署与子路径导航均保留查询和章节,不重复压入相同 URL (0.8569ms)
+✔ 报告返回链接使用浏览器历史;新标签与下载链接保留浏览器行为 (2.2151ms)
+✔ 历史达到浏览器上限、来源条目被裁剪时,关闭仍回指定页 (0.7334ms)
+✔ 334 baseline records remain separate from 16446 rich synthetic extension records (11.1204ms)
+✔ extension validation rejects live data and unrelated references (101.1821ms)
+✔ member, group and project filters agree across projects, reports, sources and cards (31.3441ms)
+✔ details preserve report, question and concept fields, with no live fallback (0.5871ms)
+✔ state, score replacement, comments and refresh remain in a separate namespace (501.6065ms)
+✔ stale revisions, invalid scores and failed storage writes never report success (396.4168ms)
+✔ sample metrics use actual records, even when declared totals are stale (15.3016ms)
+✔ a live object or a second demo namespace is rejected (13.5438ms)
+✔ dangling references, duplicate records and mismatched quotes are rejected (8.9496ms)
+✔ a malformed collection returns validation errors (5.7208ms)
+✔ source union counts indirect references and deduplicates direct ones (5.95ms)
+✔ stories preserve project scope and empty later stages (4.9972ms)
+✔ static service retries failures, shares inflight reads and never falls back (8.6736ms)
+✔ 报告中心:只计个人成果报告,机会卡对象与品类资料不混入报告数 (7.2786ms)
+✔ 中心清单保留未归类成果,可打开正文,不被五个类型过滤掉 (2.1286ms)
+✔ 类型页:阶段二次筛选 + 只看有证据 (0.6352ms)
+✔ 卡片关键指标按列对齐:每张卡 4 个指标,与类型页列名一致 (0.6158ms)
+✔ 阶段产出:按类型分组 + 机会卡单独成组 + 产出人 (1.652ms)
+✔ 痛点归并:同一条痛点(同一报告结论)多人整理只算一份;没标严重度就是 null (1.6136ms)
+✔ 报告可扫:章节摘要只抽原文,不生成 (0.9931ms)
+✔ 竞品矩阵:按对位主题合并成一行 (0.7862ms)
+✔ 空态说明书:每个类型都写清将来会出现什么(不是「暂无数据」) (0.3109ms)
+✔ 阶段归一:三种写法落到同一阶段 (1.8415ms)
+✔ 四层归属:历史 tier 与「被引用升级」都有明确规则 (0.465ms)
+✔ 归纳没有出身不许进沉淀层 (1.7025ms)
+✔ evidenceLevel=verified 但没有原文 → 降级为「有佐证」 (0.4097ms)
+✔ 机会卡证据强度与上传方式 (4.1729ms)
+✔ 首次绑定命令只传当前档案和自报称呼账号,不收密码或冒充平台确认 (7.3082ms)
+✔ 档案不匹配、冒用其他账号或通道字段写入前拒绝 (0.5854ms)
+✔ 新增受控命令沿用加密凭据入口,要求Agent及请求编号 (2.4288ms)
+✔ 报告回读移除新增私有绑定快照和平台编号,只保留公开memberKey (1.1339ms)
+✔ 真实workshop-contract路径保留公开账号绑定契约,不把正常脱敏当成服务端未部署 (2.5703ms)
+✔ 会话创建回执保留安全绑定摘要,私有归属快照即使伪装成公开契约仍不透出 (0.5578ms)
+✔ 公开操作只发一次,不带身份密钥;业务失败与未知写结果均不重试 (3.995ms)
+✔ 公开查询无需身份凭据,分页合并,不用 REST count (1.1902ms)
+✔ 新版阶段/项目 Pointer 与 Agent 归属进入既有模型,显式卡住可见 (4.4883ms)
+✔ 同名学员不同稳定键不会合并为一个人 (19.2542ms)
+✔ 同组同名学员仍分开,并将项目归给正确参与人 (0.98ms)
+✔ Skill 调用固定业务函数,不接受任意命令;请求沿用指定稳定键 (2.705ms)
+✔ 结构化产品/原料导入解析真实依赖并核对逐项回执 (1.7954ms)
+✔ 网络结果不明确时不自动重试;API 业务错误阻止成功 (0.8908ms)
+✔ 查重可独立只读调用,无查重合同导入在任何写入前停 (1.8243ms)
+✔ 导入中途回读失败,保留来源、已成功对象及当前任务编号 (1.1084ms)
+✔ 加密入口新命令仍逐次解密并核对新脚本指纹,保留旧入口 (8.4526ms)
+✔ 新 Agent 自助登记生成通用稳定 UUID、同资料重用,密钥字段拒绝 (190.6145ms)
+✔ 真实 Agent ID 生成可校验的账号摘要;稳定 ID 冲突拒绝 (72.5622ms)
+✔ 可信来源必须有稳定 ID,篡改 UUID 档案的来源不能冒充运行配置 (73.2796ms)
+✔ 任意数量同角色数字人可共存,旧档案迁入不覆盖原件,CLI仅输出摘要 (159.2082ms)
+✔ 新员工先登记;稳定账号自动复用,无账号同名需人工确认 (206.6744ms)
+✔ workshop-start 必须引用登记的员工和 Agent 档案,并注入字段快照 (61.918ms)
+✔ 旧服务不支持身份版本时会话写入前停止 (51.4409ms)
+✔ 只读回读脱敏账号与会话键,公开producer不携带账号值 (51.6307ms)
+✔ aibot 无企业ID或姓名也能按发送者自动登记,单聊换群不改变员工键 (72.991ms)
+✔ 同一群里不同发送者分开;跨机器人标识不擅自合并 (203.704ms)
+✔ aibot 缺发送者/机器人/群会话ID或机器人不匹配在登记前停止 (48.4753ms)
+✔ aibot 旧文件 CLI 已关闭;旧合同与错机器人阻止会话 (115.3194ms)
+✔ 通用员工 JSON 不能替代通道工具;内部登记复用规范键且拒绝错实例与运维角色 (143.9837ms)
+✔ 归一化保留真实任务系列、版本、课题和模式,非法版本保持未知 (4.1967ms)
+✔ 只按有正文的明确章节或任务定位;标题和成果类型不会把运行记录当提案 (1.4934ms)
+✔ 明确标为方法材料的配置卡不因课表环节被计作核心市场报告 (4.5775ms)
+✔ 默认区分真实资料、样例、人工和未知,不根据标题猜模式 (20.0154ms)
+✔ 共享稳定身份决定分组;同名学员各自归属,失效组不按原快照猜 (1.5687ms)
+✔ 同一稳定课题、作者、系列选择最新版本;同版本冲突及未知版本不隐藏 (1.7626ms)
+✔ 缺系列或稳定身份时保留每条记录,不按相同标题合并 (1.0227ms)
+✔ 课题、小组、阶段、报告分类和搜索可共同筛选,分类统计落在个人报告上 (1.0987ms)
+✔ 成果目录按小组、成员、报告展开,并保留当前筛选下没有报告的成员 (1.8578ms)
+✔ 分类按钮筛选的是组员自己的报告,分类缺失的报告仍归入未归类 (1.1634ms)
+✔ 成员姓名搜索保留零报告成员,报告搜索只显示命中的作者和小组 (1.0136ms)
+✔ 评估、运行记录与概念提案分开,同时都可从总清单打开 (5.6171ms)
+✔ 默认预检不创建文件;实际更新九文件,首页保持,重复运行不写入 (229.2319ms)
+✔ 未知目标内容、首页不符、坏包及超出白名单均在写入之前拒绝 (126.3975ms)
+✔ 中途写入失败恢复已改文件,只清理本次新建文件,保留完整备份 (82.5813ms)
+✔ 已发布文件被其他人并发改动时,回滚保留外部版本并报告未完全恢复 (56.9318ms)
+✔ Skill课表13环节的每种报告均经保存入口、精确回读、公开分页、正式分类和全文模型保留正文 (99.8573ms)
+✔ 35位组员共用同一智能体,5组各人的同系列报告和版本不混合,报告分类与组员归属对应 (262.1112ms)
+✔ 七类核心交付物可承载在正式报告类型中,但章节导航与报告分类是两个维度 (6.1352ms)
+✔ 七类新报告经实际Skill保存、精确回读、公开读取校验,个人七项提交状态与分类一致 (29.6697ms)
+✔ 默认HTML生成后走实际保存、编号回读、公开投影和文件字节回读;员工确认保持待处理 (46.3148ms)
+✔ 全部相关资料去向、正反证统计与代表原话随HTML及正文保存、回读和公开展示,分子不由引文卡数代替 (11.1135ms)
+✔ 课程三维与未知项以现有报告表保存,公开数据及HTML字节回读一致,无新增数据库字段 (12.1667ms)
+✔ HTML与图片随报告原子保存、文件字节回读、公开预览关联;直接匿名取私有文件被拒 (10.3404ms)
+✔ 附件内容损坏、伪造回执、跨身份取文件、空交付物均拒绝,失败不遗留报告或文件 (5.667ms)
+✔ 仅有boardVisible回执不算验证完成;回读不符保留编号且不重试保存,旧服务在写前停止 (2.5316ms)
+✔ 个人交付核对按课题、资料模式、最新版本隔离;草稿不计已交,缺正文、旧成果待定位均明示 (7.6022ms)
+✔ 报告中心仅计成果报告;旧原文可读、无正文不伪造,独立机会及资料分开统计 (1.1426ms)
+✔ 已有上传回执可关联报告;跨身份回执及超限附件在写入前拒绝 (4.8095ms)
+✔ 课程三维分别为1–3,未知保持未知,资格不是自动前三或总分 (2.8048ms)
+✔ 一维1分或未知时不能按样卡进入前三,越界分值和缺依据不通过 (1.2204ms)
+✔ 独立HTML从同一评分表展示三维、未知及说明,不生成百分制总分 (3.149ms)
+✔ 课程非法排序在任何远程调用前停止保存 (1.5806ms)
+✔ final操作手册采用v3全文,13段提示词原样,页内锚点不被远程base改写 (8.2187ms)
+✔ 13 个环节覆盖完整课程,11 个阅读分类和七类内容索引不能代替课程清单 (27.6185ms)
+✔ 同一环节多报告或附件不增加环节份数,配置卡、任务卡、验收与指标各有位置 (1.1624ms)
+✔ 旧系列接受冒号或斜线,标题猜测、非法交付标记和互相矛盾的环节不计数 (0.9556ms)
+✔ 仅原文、草稿、空章节、未保存分别显示,有正文不被称为质量通过或员工确认 (3.6946ms)
+✔ 最新版缺正文不能借用历史版补齐,作者、课题与模式的报告不能互相补足 (1.0804ms)
+ℹ tests 234
+ℹ suites 0
+ℹ pass 234
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 2278.1188

+ 28 - 0
docs/board-v3-release-20261112/tests-staged-checkout.txt

@@ -0,0 +1,28 @@
+✔ 完整前端发布预检只读,apply可重复且不替换首页 (1403.8364ms)
+✔ 未知现场拒绝,写入失败恢复,新静态文件保持0644 (1002.5274ms)
+✔ 成员接口未知版本在写入前停止 (0.6001ms)
+✔ 成员接口已发布版本只读和apply均不重复写入 (0.3597ms)
+✔ six-file UI release applies and is idempotent while preserving an independent report update (141.0765ms)
+✔ UI release refuses an unexpected file list and an unknown baseline before writing (27.3921ms)
+✔ UI release rolls back both replaced files and its newly added component on a write failure (76.2153ms)
+✔ v3发布检查完整浏览器依赖,原子更新后重复执行无变化,首页不变 (1883.4829ms)
+✔ v3发布拒绝未知现场、额外文件和首页变化,写入失败回退已改文件及新增模块 (1397.6574ms)
+✔ 有效配置与真实模块响应均通过,预检保持只读 (48.5091ms)
+✔ 旧配置缺少mjs映射时,在请求前停止,注释不能冒充映射 (100.81ms)
+✔ 配置正确但响应为octet-stream时拒绝,复现白屏原因 (7.6432ms)
+✔ SPA返回HTML或内容不符不能因200和JavaScript响应头而通过 (7.618ms)
+✔ 首次发布尚不存在的模块明确留待发布后验证 (7.6586ms)
+✔ 统计卡从同一n/N计算占比,保留范围、限制、产品含义和实际证据键 (2.7119ms)
+✔ 未核分母或无有效材料不编造百分比,已确认零提及保留0% (1.0127ms)
+✔ 非法数值、口径缺失、错列和不存在的证据键在保存前拒绝 (1.7109ms)
+✔ 多标签主题可重叠,不要求各主题相加等于100%,原始数量不被修改 (0.6196ms)
+✔ 看板排版薄空格不改变统计识别,浏览器与HTML的统计校验保持同步 (1.4891ms)
+✔ 统计表完整保留在默认HTML附件与同版report,生成器不从精选引文数量推断分子 (4.8682ms)
+ℹ tests 20
+ℹ suites 0
+ℹ pass 20
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 3424.4293

+ 138 - 0
docs/board-v3-release-20261112/tests-workshop.txt

@@ -0,0 +1,138 @@
+
+> kangshifu-innovation-board@5.0.0 test:workshop
+> node --test server/test/application.test.mjs server/test/workshop.test.mjs server/test/board-actions.test.mjs server/test/identity-deploy.test.mjs server/test/dedup-deploy.test.mjs test/workshop*.test.js
+
+✔ 公开合同不授权写表,公布实际动态入口和受限能力 (14.7524ms)
+✔ 没有身份不能写入 (2.2567ms)
+✔ 服务凭据以 hash 验证,并核验过期时间和作用范围 (2.8496ms)
+✔ 真实三态:未知采集时间和未判断噪声保持未知 (4.9235ms)
+✔ 同请求重放回执一致且不增加记录;不同内容同 requestId 拒绝 (3.1799ms)
+✔ 跨批重导报告 unchanged,成功计数来自实际写入 (3.054ms)
+✔ 来源跨requestId复用,URL、模式和owner分开,原件字节变化保留 (30.9253ms)
+✔ 分类按版本和代码保护唯一身份:相同定义复用,不同定义返回可处理冲突 (5.5711ms)
+✔ 跨批换业务键精确重复复用原始记录,并保留已有人工噪声状态 (3.1913ms)
+✔ 不同批次来源下同平台对象同载荷复用,内容变化保留新快照 (5.8503ms)
+✔ 相同短评不同上游对象不合并,缺ID不同索引不合并 (8.3065ms)
+✔ 同源产品同字段复用,不同规格和价格时间不被名称合并 (4.9694ms)
+✔ 只读查重不写记录,依赖待解析不宣称命中,提交仍独立复核 (3.5091ms)
+✔ 来源不跨服务身份泄漏或合并,查重扫描未完成明确失败 (1.4408ms)
+✔ 字段脏数据不静默删除;rawPayload 内同名字段原样保留 (1.545ms)
+✔ 业务键相同但原始载荷变化拒绝,不清空历史判断 (2.9215ms)
+✔ 错误 Pointer 和不存在的来源会拒绝 (0.8261ms)
+✔ 正文指纹保留空白、标点与负面评价;互动数不影响指纹 (0.8432ms)
+✔ 回执使用 data.job 和逐条当前快照,分页游标不能跨任务 (1.8055ms)
+✔ 人工噪声判断服务身份不能冒充,false 也必须真实保存 (3.3054ms)
+✔ 过期版本和正文指纹不符不能更新噪声 (2.0467ms)
+✔ 原始查询必须有研究口径;样例默认不进入正式样本 (1.763ms)
+✔ 不能查询账号密码、凭据、内部事件表 (1.0355ms)
+✔ 伪造报告确认字段不能创建可信确认事件 (1.2791ms)
+✔ 真实员工会话上下文才能建立事件,确认回执绑定 hash 与版本 (2.8033ms)
+✔ 未配置立项门槛不伪报成功;知识库同步明确无需执行 (1.616ms)
+✔ 文件未真实完成传输不能登记成来源 (1.3548ms)
+✔ 本地夹具报告确认后入库、子类型保存、独立发布及卡片回读 (9.5886ms)
+✔ 修改已确认报告的正文、类型或数据模式被拒绝 (3.0133ms)
+✔ 报告看板在分页之前筛选 published,草稿不会挤空首页 (0.7215ms)
+✔ 匿名三状态持久化,保留互链与旧决策,不产生项目或企业批准 (14.5885ms)
+✔ 重复请求复用回执,改内容复用请求键、过期版本均不写入 (2.7766ms)
+✔ 私有/归档/未知记录、越权字段与坏状态被拒绝,回执不泄露私有字段 (2.5598ms)
+✔ 匿名评分校验与同署名覆盖,保留其他评分 (3.7389ms)
+✔ 查重部署默认只读,定向五函数更新、结构不变且二次不写 (50.3509ms)
+✔ 未知版本/撞名在写前停;中途失败恢复已更新代码 (26.2894ms)
+✔ 并发更新不覆盖他人改动,其他文件按备份回退 (33.2875ms)
+✔ 身份发布默认只读;仅更新固定两函数,复验结构权限且重放不写 (39.9589ms)
+✔ 未知线上版本、损坏包在写前停止;中途失败恢复已更新函数 (22.4965ms)
+✔ 失败回滚不覆盖其他维护者并发改动 (21.9526ms)
+✔ 真实S1小结绑定两份官网来源,逐字引文支持公开课堂展示,不制造机会项目或正式确认 (39.9494ms)
+✔ 真实官网归档资料入库:9个口味和逐字证据,复用品类,原文来源精确回读,不创建课堂报告 (140.1022ms)
+✔ 新批次直接引用已有分类,保留产品证据依赖,不再次提交分类或修改旧范围说明 (53.304ms)
+✔ 真实批次来源/引文损坏写入前停止,拒绝把样例键或秘密塞入上下文 (45.4521ms)
+✔ 专用查重执行器跨批次复用同一来源原料,不创建工作坊会话;保留两批审计 (56.303ms)
+✔ 查重第二次导入响应丢失时停住,保留首批编号和在途记录,不重试 (50.9989ms)
+✔ 查重预检编号不一致时禁止第二批写入;上下文拒绝密钥和旧任务键 (44.9565ms)
+✔ 记录员工与王刚001,重放不新建会话,不建立假员工账号 (1.2108ms)
+✔ aibot 无姓名无企业ID自动登记后保存归属,机器人作用域不会进入公开看板 (86.4884ms)
+✔ 账号归属保存至私有元数据并继承到报告项目,公开看板不泄露;登记不等于认证 (7.02ms)
+✔ 同名员工不同渠道身份不串会话,改参数重用 requestId 被拒 (1.2991ms)
+✔ 新数字人和同名新员工先登记,再经 Skill 保存正确归属并公开脱敏 (177.9541ms)
+✔ 课堂报告/机会保留 sample,保存有归属且原子重放不重复 (3.5683ms)
+✔ 真实项目、阶段门槛、方案版本与完整事件可回读 (8.5916ms)
+✔ 其他服务/学员/小组不能写入别人的项目 (2.1446ms)
+✔ 公开分页无需登录,含课堂样例但不泄露私密会话/普通原文/未发布正式稿 (3.0461ms)
+✔ 完整验收任务从已登记aibot员工走S1-S5、检索选卡、资料依赖、卡住和新品方案,公开回读一致 (96.1248ms)
+✔ 首次绑定命令只传当前档案和自报称呼账号,不收密码或冒充平台确认 (4.7934ms)
+✔ 档案不匹配、冒用其他账号或通道字段写入前拒绝 (0.8272ms)
+✔ 新增受控命令沿用加密凭据入口,要求Agent及请求编号 (3.7888ms)
+✔ 报告回读移除新增私有绑定快照和平台编号,只保留公开memberKey (2.2807ms)
+✔ 真实workshop-contract路径保留公开账号绑定契约,不把正常脱敏当成服务端未部署 (1.6511ms)
+✔ 会话创建回执保留安全绑定摘要,私有归属快照即使伪装成公开契约仍不透出 (0.6212ms)
+✔ 公开操作只发一次,不带身份密钥;业务失败与未知写结果均不重试 (3.8969ms)
+✔ 公开查询无需身份凭据,分页合并,不用 REST count (1.2097ms)
+✔ 新版阶段/项目 Pointer 与 Agent 归属进入既有模型,显式卡住可见 (4.0209ms)
+✔ 同名学员不同稳定键不会合并为一个人 (22.0417ms)
+✔ 同组同名学员仍分开,并将项目归给正确参与人 (0.8131ms)
+✔ Skill 调用固定业务函数,不接受任意命令;请求沿用指定稳定键 (4.128ms)
+✔ 结构化产品/原料导入解析真实依赖并核对逐项回执 (4.1298ms)
+✔ 网络结果不明确时不自动重试;API 业务错误阻止成功 (0.9745ms)
+✔ 查重可独立只读调用,无查重合同导入在任何写入前停 (3.1099ms)
+✔ 导入中途回读失败,保留来源、已成功对象及当前任务编号 (1.91ms)
+✔ 加密入口新命令仍逐次解密并核对新脚本指纹,保留旧入口 (11.8362ms)
+✔ 新 Agent 自助登记生成通用稳定 UUID、同资料重用,密钥字段拒绝 (196.2021ms)
+✔ 真实 Agent ID 生成可校验的账号摘要;稳定 ID 冲突拒绝 (83.2193ms)
+✔ 可信来源必须有稳定 ID,篡改 UUID 档案的来源不能冒充运行配置 (85.6524ms)
+✔ 任意数量同角色数字人可共存,旧档案迁入不覆盖原件,CLI仅输出摘要 (200.8847ms)
+✔ 新员工先登记;稳定账号自动复用,无账号同名需人工确认 (228.6345ms)
+✔ workshop-start 必须引用登记的员工和 Agent 档案,并注入字段快照 (65.7759ms)
+✔ 旧服务不支持身份版本时会话写入前停止 (62.3534ms)
+✔ 只读回读脱敏账号与会话键,公开producer不携带账号值 (56.34ms)
+✔ aibot 无企业ID或姓名也能按发送者自动登记,单聊换群不改变员工键 (68.9109ms)
+✔ 同一群里不同发送者分开;跨机器人标识不擅自合并 (198.386ms)
+✔ aibot 缺发送者/机器人/群会话ID或机器人不匹配在登记前停止 (46.0885ms)
+✔ aibot 旧文件 CLI 已关闭;旧合同与错机器人阻止会话 (124.17ms)
+✔ 通用员工 JSON 不能替代通道工具;内部登记复用规范键且拒绝错实例与运维角色 (166.2908ms)
+✔ 归一化保留真实任务系列、版本、课题和模式,非法版本保持未知 (4.3054ms)
+✔ 只按有正文的明确章节或任务定位;标题和成果类型不会把运行记录当提案 (1.6093ms)
+✔ 明确标为方法材料的配置卡不因课表环节被计作核心市场报告 (4.6931ms)
+✔ 默认区分真实资料、样例、人工和未知,不根据标题猜模式 (21.5304ms)
+✔ 共享稳定身份决定分组;同名学员各自归属,失效组不按原快照猜 (1.2937ms)
+✔ 同一稳定课题、作者、系列选择最新版本;同版本冲突及未知版本不隐藏 (1.6932ms)
+✔ 缺系列或稳定身份时保留每条记录,不按相同标题合并 (0.9445ms)
+✔ 课题、小组、阶段、报告分类和搜索可共同筛选,分类统计落在个人报告上 (0.9075ms)
+✔ 成果目录按小组、成员、报告展开,并保留当前筛选下没有报告的成员 (2.8023ms)
+✔ 分类按钮筛选的是组员自己的报告,分类缺失的报告仍归入未归类 (1.0853ms)
+✔ 成员姓名搜索保留零报告成员,报告搜索只显示命中的作者和小组 (0.9706ms)
+✔ 评估、运行记录与概念提案分开,同时都可从总清单打开 (5.552ms)
+✔ 默认预检不创建文件;实际更新九文件,首页保持,重复运行不写入 (230.2585ms)
+✔ 未知目标内容、首页不符、坏包及超出白名单均在写入之前拒绝 (147.0395ms)
+✔ 中途写入失败恢复已改文件,只清理本次新建文件,保留完整备份 (89.9682ms)
+✔ 已发布文件被其他人并发改动时,回滚保留外部版本并报告未完全恢复 (70.7792ms)
+✔ Skill课表13环节的每种报告均经保存入口、精确回读、公开分页、正式分类和全文模型保留正文 (88.0196ms)
+✔ 35位组员共用同一智能体,5组各人的同系列报告和版本不混合,报告分类与组员归属对应 (316.1471ms)
+✔ 七类核心交付物可承载在正式报告类型中,但章节导航与报告分类是两个维度 (8.4197ms)
+✔ 七类新报告经实际Skill保存、精确回读、公开读取校验,个人七项提交状态与分类一致 (36.1432ms)
+✔ 默认HTML生成后走实际保存、编号回读、公开投影和文件字节回读;员工确认保持待处理 (53.551ms)
+✔ 全部相关资料去向、正反证统计与代表原话随HTML及正文保存、回读和公开展示,分子不由引文卡数代替 (12.5202ms)
+✔ 课程三维与未知项以现有报告表保存,公开数据及HTML字节回读一致,无新增数据库字段 (10.5811ms)
+✔ HTML与图片随报告原子保存、文件字节回读、公开预览关联;直接匿名取私有文件被拒 (6.6959ms)
+✔ 附件内容损坏、伪造回执、跨身份取文件、空交付物均拒绝,失败不遗留报告或文件 (4.9441ms)
+✔ 仅有boardVisible回执不算验证完成;回读不符保留编号且不重试保存,旧服务在写前停止 (2.5211ms)
+✔ 个人交付核对按课题、资料模式、最新版本隔离;草稿不计已交,缺正文、旧成果待定位均明示 (8.0942ms)
+✔ 报告中心仅计成果报告;旧原文可读、无正文不伪造,独立机会及资料分开统计 (1.3661ms)
+✔ 已有上传回执可关联报告;跨身份回执及超限附件在写入前拒绝 (5.0284ms)
+✔ 课程三维分别为1–3,未知保持未知,资格不是自动前三或总分 (5.5497ms)
+✔ 一维1分或未知时不能按样卡进入前三,越界分值和缺依据不通过 (1.1449ms)
+✔ 独立HTML从同一评分表展示三维、未知及说明,不生成百分制总分 (2.858ms)
+✔ 课程非法排序在任何远程调用前停止保存 (1.2215ms)
+✔ final操作手册采用v3全文,13段提示词原样,页内锚点不被远程base改写 (8.164ms)
+✔ 13 个环节覆盖完整课程,11 个阅读分类和七类内容索引不能代替课程清单 (24.0391ms)
+✔ 同一环节多报告或附件不增加环节份数,配置卡、任务卡、验收与指标各有位置 (1.1251ms)
+✔ 旧系列接受冒号或斜线,标题猜测、非法交付标记和互相矛盾的环节不计数 (0.8292ms)
+✔ 仅原文、草稿、空章节、未保存分别显示,有正文不被称为质量通过或员工确认 (1.9642ms)
+✔ 最新版缺正文不能借用历史版补齐,作者、课题与模式的报告不能互相补足 (1.1595ms)
+ℹ tests 126
+ℹ suites 0
+ℹ pass 126
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 1856.5855

BIN
docs/board-v3-release-20261112/workshop-1440.png


+ 19 - 0
scripts/deploy-board-v3-20261112.sh

@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+if [[ $# -ne 1 || "$1" != /opt/kangshifu-web ]]; then
+  echo 'Usage: bash scripts/deploy-board-v3-20261112.sh /opt/kangshifu-web' >&2
+  exit 2
+fi
+cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
+umask 022
+node -e 'if(Number(process.versions.node.split(".")[0])<20)process.exit(1)'
+results_dir="$(mktemp -d /opt/ksf-board-v3-20261112-results-XXXXXXXX)"
+git rev-parse HEAD | tee "$results_dir/commit.txt"
+node scripts/preflight-mime-mjs.mjs --origin https://kangshifu.fmode.cn --target-dir "$1" --manifest deploy/board-v3-20261112.json | tee "$results_dir/mime-preflight.json"
+node --test test/board-local-deploy.test.mjs test/board-v3-deploy.test.mjs test/mjs-mime-preflight.test.mjs | tee "$results_dir/deploy-tests.txt"
+node scripts/publish-board-v3-20261112.mjs --target-dir "$1" | tee "$results_dir/frontend-plan.json"
+node scripts/publish-board-v3-20261112.mjs --target-dir "$1" --apply | tee "$results_dir/frontend-apply.json"
+node scripts/publish-board-v3-20261112.mjs --target-dir "$1" | tee "$results_dir/frontend-noop.json"
+node scripts/verify-board-local-public.mjs --manifest deploy/board-v3-20261112.json | tee "$results_dir/public-verification.json"
+node scripts/verify-modules.mjs https://kangshifu.fmode.cn/ --json | tee "$results_dir/module-verification.json"
+echo "Static deployment complete. Browser acceptance remains required. Receipts: $results_dir"

+ 4 - 2
scripts/preflight-mime-mjs.mjs

@@ -45,11 +45,13 @@ if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.ur
  try{
   const options={origin:'https://kangshifu.fmode.cn'};const args=process.argv.slice(2);
   for(let i=0;i<args.length;i++){
-   const key={'--origin':'origin','--target-dir':'targetDir','--mime-file':'mimeFile'}[args[i]];
+   const key={'--origin':'origin','--target-dir':'targetDir','--mime-file':'mimeFile','--manifest':'manifest'}[args[i]];
    if(!key||!args[i+1]||args[i+1].startsWith('--'))throw Error('USAGE');options[key]=args[++i];
   }
   if(!options.targetDir)throw Error('USAGE');
-  const manifest=JSON.parse(await fs.readFile(new URL('../deploy/board-local-20261111.json',import.meta.url)));
+  const manifestPath=options.manifest||'deploy/board-local-20261111.json';
+  if(!/^deploy\/[a-zA-Z0-9_.-]+\.json$/.test(manifestPath))throw Error('USAGE');
+  const manifest=JSON.parse(await fs.readFile(new URL('../'+manifestPath,import.meta.url)));
   const result=await preflightMjsMime({...options,files:manifest.files});console.log(JSON.stringify(result,null,2));if(!result.ok)process.exitCode=1;
  }catch{console.error(JSON.stringify({ok:false,readOnly:true,errorCode:'MJS_PREFLIGHT_STOPPED',remedy}));process.exitCode=1;}
 }

+ 46 - 0
scripts/prepare-board-v3-release.mjs

@@ -0,0 +1,46 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import crypto from 'node:crypto';
+
+const release = 'ksf-board-v3-20261112-r2';
+const origin = 'https://kangshifu.fmode.cn/';
+const output = 'work/board-v3-release-20261112';
+const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
+const normal = bytes => bytes.includes(0) ? bytes : Buffer.from(bytes.toString('utf8').replaceAll('\r\n', '\n'));
+const graph = JSON.parse((await fs.readFile('work/board-deploy-20261112-modules-local.json', 'utf8')).replace(/^\uFEFF/, ''));
+if (graph.failures.length) throw Error('LOCAL_MODULE_GRAPH_FAILED');
+const paths = [...new Set([...graph.modules.map(url => new URL(url).pathname.slice(1)),
+  'src/assets/inter-latin.woff2', 'src/data/development-references.json'])];
+const allowed = /^(?:src\/(?!dev\/)[a-zA-Z0-9_./-]+\.(?:js|woff2|json)|lib\/vendor\/[a-zA-Z0-9_@./-]+\.js|skills\/ksf-workshop-runtime\/scripts\/(?:report-contract|course-deliverables)\.mjs)$/;
+if (paths.some(p => !allowed.test(p) || p.split('/').includes('..'))) throw Error('UNEXPECTED_BROWSER_DEPENDENCY');
+async function read(relative) {
+  const url = new URL(relative, origin); url.searchParams.set('boardReleaseBaseline', release);
+  const response = await fetch(url, { redirect: 'error', headers: { 'cache-control': 'no-cache' }, signal: AbortSignal.timeout(45000) });
+  if (![200, 404].includes(response.status)) throw Error('BASELINE_HTTP:' + relative + ':' + response.status);
+  return { status: response.status, type: response.headers.get('content-type') || '', bytes: Buffer.from(await response.arrayBuffer()) };
+}
+const home = await read('index.html');
+if (home.status !== 200 || sha(home.bytes) !== 'dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3') throw Error('HOMEPAGE_BASELINE_CHANGED');
+await fs.mkdir(output + '/production-before', { recursive: true });
+await fs.writeFile(output + '/production-index.html', home.bytes);
+const files = []; let next = 0;
+await Promise.all(Array.from({ length: 4 }, async () => { while (next < paths.length) {
+  const relative = paths[next++], bytes = normal(await fs.readFile(relative)), prior = await read(relative);
+  const absent = prior.status === 404 || prior.type.includes('html') && prior.bytes.equals(home.bytes);
+  if (!absent && /\.m?js$/.test(relative) && !/^(?:application|text)\/javascript\b/.test(prior.type)) throw Error('BASELINE_MIME:' + relative);
+  if (!absent) {
+    const target = output + '/production-before/' + relative;
+    await fs.mkdir(path.dirname(target), { recursive: true }); await fs.writeFile(target, prior.bytes);
+  }
+  files.push({ path: relative, sha256: sha(bytes), priorNormalizedSha256: absent ? [] : [sha(normal(prior.bytes))],
+    priorSha256: absent ? null : sha(prior.bytes), allowMissing: absent, changed: absent || sha(normal(prior.bytes)) !== sha(bytes) });
+} }));
+const rank = p => p === 'src/layout/kf-app.js' ? 4 : p.startsWith('src/layout/') ? 3 : p.startsWith('src/pages/') ? 2 : 1;
+files.sort((a, b) => rank(a.path) - rank(b.path) || a.path.localeCompare(b.path));
+const manifest = { version: 'ksf.board-local.publish.v1', release, origin, baselineReadAt: new Date().toISOString(), homepageSha256: sha(home.bytes), files };
+await fs.writeFile('deploy/board-v3-20261112.json', JSON.stringify(manifest, null, 2) + '\n');
+await fs.writeFile(output + '/baseline-summary.json', JSON.stringify({ release, productionWrites: 0, files: files.length,
+  changed: files.filter(file => file.changed).map(file => file.path), added: files.filter(file => file.allowMissing).map(file => file.path),
+  homepageSha256: manifest.homepageSha256 }, null, 2) + '\n');
+console.log(JSON.stringify({ release, files: files.length, changed: files.filter(file => file.changed).map(file => file.path),
+  added: files.filter(file => file.allowMissing).map(file => file.path), productionWrites: 0 }, null, 2));

+ 1 - 1
scripts/publish-board-local.mjs

@@ -39,7 +39,7 @@ export function publishFiles({sourceDir,manifestPath,targetDir,apply=false,relea
  const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));
  if(manifest.version!=='ksf.board-local.publish.v1'||manifest.release!==releaseDefinition.release||!/^[a-f0-9]{64}$/.test(manifest.homepageSha256))fail('BAD_MANIFEST');
  const expectedFiles=releaseDefinition.files.map(f=>f.path);
- if(!Array.isArray(manifest.files)||JSON.stringify(manifest.files.map(f=>f.path).sort())!==JSON.stringify([...expectedFiles].sort())||new Set(expectedFiles).size!==expectedFiles.length||expectedFiles.some(p=>!(/^(?:src\/(?!dev\/)[a-zA-Z0-9_./-]+\.(?:js|woff2|json)|lib\/vendor\/[a-zA-Z0-9_@./-]+\.js|skills\/ksf-workshop-runtime\/scripts\/report-contract\.mjs)$/.test(p))))fail('UNEXPECTED_FILE_LIST');
+ if(!Array.isArray(manifest.files)||JSON.stringify(manifest.files.map(f=>f.path).sort())!==JSON.stringify([...expectedFiles].sort())||new Set(expectedFiles).size!==expectedFiles.length||expectedFiles.some(p=>p.split('/').includes('..')||!(/^(?:src\/(?!dev\/)[a-zA-Z0-9_./-]+\.(?:js|woff2|json)|lib\/vendor\/[a-zA-Z0-9_@./-]+\.js|skills\/ksf-workshop-runtime\/scripts\/(?:report-contract|course-deliverables)\.mjs)$/.test(p))))fail('UNEXPECTED_FILE_LIST');
  const index=safePath(targetDir,'index.html');plain(index);const indexHash=sha(fs.readFileSync(index));
  if(indexHash!==manifest.homepageSha256)fail('HOMEPAGE_MISMATCH','index.html');
  const byName=new Map(manifest.files.map(f=>[f.path,f]));

+ 29 - 0
scripts/publish-board-v3-20261112.mjs

@@ -0,0 +1,29 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { publishFiles } from './publish-board-local.mjs';
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+export const MANIFEST_PATH = path.join(root, 'deploy/board-v3-20261112.json');
+const definition = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
+export function publishBoardV3({ targetDir, sourceDir = root, manifestPath = MANIFEST_PATH, apply = false }, dependencies = {}) {
+  if (definition.release !== 'ksf-board-v3-20261112-r2') throw Error('UNEXPECTED_RELEASE');
+  const result = publishFiles({ sourceDir, targetDir, manifestPath, apply, releaseDefinition: definition }, dependencies);
+  return { ...result, businessRecordWrites: 0, backendFunctionsModified: false, serviceRestarted: false };
+}
+if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+  try {
+    const args = process.argv.slice(2); let targetDir, apply = false;
+    for (let i = 0; i < args.length; i++) {
+      if (args[i] === '--target-dir' && !targetDir && args[i + 1] && !args[i + 1].startsWith('--')) targetDir = args[++i];
+      else if (args[i] === '--apply' && !apply) apply = true;
+      else throw Error('USAGE');
+    }
+    if (targetDir !== '/opt/kangshifu-web') throw Error('TARGET_REQUIRED');
+    console.log(JSON.stringify(publishBoardV3({ targetDir, apply }), null, 2));
+  } catch (error) {
+    console.error(JSON.stringify({ ok: false, error: { code: error.code || error.message, file: error.file },
+      ...(error.backup ? { backup: error.backup, rollbackComplete: error.rollbackComplete } : {}), businessRecordWrites: 0 }, null, 2));
+    process.exitCode = 1;
+  }
+}

+ 102 - 0
scripts/verify-board-v3-browser.mjs

@@ -0,0 +1,102 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import assert from 'node:assert/strict';
+import { pathToFileURL } from 'node:url';
+
+const production = process.argv.includes('--production');
+const output = path.resolve(process.env.KSF_BROWSER_OUTPUT || 'work/board-v3-release-20261112/browser');
+await fs.mkdir(output, { recursive: true });
+const manifest = JSON.parse(await fs.readFile('deploy/board-v3-20261112.json'));
+const files = new Set(manifest.files.map(file => file.path));
+const modulePath = process.env.KSF_PLAYWRIGHT_MODULE || path.resolve('work/workshop-manual-tools/node_modules/playwright/index.mjs');
+const { chromium } = await import(pathToFileURL(modulePath).href);
+const browser = await chromium.launch({ headless: true, ...(process.env.KSF_BROWSER_EXECUTABLE
+  ? { executablePath: process.env.KSF_BROWSER_EXECUTABLE }
+  : process.platform === 'win32' ? { executablePath: 'C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe' } : {}) });
+const origin = 'https://kangshifu.fmode.cn', checks = [], errors = [], blockedWrites = [];
+const receipt = { release: manifest.release, mode: production ? 'production-read-only' : 'local-source-with-public-production-reads',
+  productionWrites: 0, deployedByThisCheck: false, checks, errors, blockedWrites };
+try {
+  const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } });
+  await context.route('**/*', async route => {
+    const request = route.request(), url = new URL(request.url());
+    if (!production && url.origin === origin && files.has(url.pathname.slice(1))) return route.fulfill({
+      contentType: /\.m?js$/.test(url.pathname) ? 'application/javascript' : url.pathname.endsWith('.json') ? 'application/json' : 'font/woff2',
+      body: await fs.readFile(url.pathname.slice(1)) });
+    if (url.searchParams.get('fileId') === 'browser-only-file') return route.fulfill({ contentType: 'text/html',
+      body: '<!doctype html><meta charset="utf-8"><h1>浏览器夹具:HTML排版展示</h1><p>仅在浏览器内存中验证,未保存为真实成果。</p>',
+      headers: { 'content-security-policy': "default-src 'none'; sandbox" } });
+    if (!['GET', 'HEAD'].includes(request.method())) {
+      const body = request.postDataJSON();
+      if (url.origin !== origin || url.pathname !== '/api/functions'
+        || !['/ksfGetPublicBoard', '/ksfGetPublicMemberGroups'].includes(body?.path)) {
+        blockedWrites.push(url.origin + url.pathname); return route.abort();
+      }
+    }
+    return route.continue();
+  });
+  const page = await context.newPage(); page.on('pageerror', error => errors.push(error.message));
+  let reportPath;
+  for (const width of [1440, 390, 320]) {
+    await page.setViewportSize({ width, height: 1000 });
+    await page.goto(origin + '/workshop'); await page.locator('[data-output-member]').first().waitFor();
+    const columns = await page.locator('thead').first().locator('[data-course-column]').count(); assert.equal(columns, 13);
+    const memberIds = await page.locator('[data-output-member]').evaluateAll(rows => rows.map(row => row.dataset.outputMember));
+    assert.equal(new Set(memberIds).size, memberIds.length);
+    const overflow = await page.evaluate(() => Math.max(0, document.documentElement.scrollWidth - innerWidth)); assert.equal(overflow, 0);
+    reportPath ||= await page.locator('[data-workshop-artifact]').first().getAttribute('href');
+    await page.screenshot({ path: path.join(output, 'workshop-' + width + '.png') });
+    checks.push({ page: 'workshop', width, columns, members: memberIds.length, overflow, actualPublicData: true });
+    await page.goto(new URL(reportPath, origin).href); await page.locator('view-report h1').first().waitFor();
+    const report = await page.locator('view-report').evaluate(view => ({ artifactId: view.artifactId,
+      attachments: view.db.artifactById[view.artifactId]?.report?.attachments?.length || 0,
+      statisticsCards: view.renderRoot.querySelectorAll('.statistics-card').length,
+      chapters: view.renderRoot.querySelectorAll('[data-chapter]').length }));
+    assert.ok(report.chapters > 0); const reportOverflow = await page.evaluate(() => Math.max(0, document.documentElement.scrollWidth - innerWidth));
+    assert.equal(reportOverflow, 0); await page.screenshot({ path: path.join(output, 'real-report-' + width + '.png') });
+    checks.push({ page: 'real-report', width, ...report, overflow: reportOverflow, actualPublicData: true });
+    await page.goto(origin + '/reports'); await page.locator('[data-type-card]').first().waitFor();
+    assert.equal(await page.locator('[data-type-card]').count(), 11);
+    await page.goto(origin + '/pool'); await page.locator('view-pool .course-note').waitFor();
+    assert.equal(await page.evaluate(() => Math.max(0, document.documentElement.scrollWidth - innerWidth)), 0);
+    assert.equal(await page.locator('view-pool button[aria-pressed="true"]').filter({ hasText: '最新' }).count(), 2);
+    checks.push({ page: 'reports-and-pool', width, reportCategories: 11, defaultSort: 'newest', actualPublicData: true });
+  }
+  // Separate in-memory fixtures exercise cards even when public reports have not saved these sections yet.
+  for (const width of [1440, 390, 320]) {
+    await page.setViewportSize({ width, height: 1000 }); await page.goto(new URL(reportPath, origin).href);
+    await page.locator('view-report h1').first().waitFor();
+    await page.locator('view-report').evaluate(async view => {
+      const db = view.db, original = db.artifactById[view.artifactId];
+      const report = { title: '浏览器内存夹具:统计和课程排序', executive: ['仅用于排版验证,未写入业务数据。'],
+        chapters: [{ id: 'browser-verification', title: '浏览器夹具', sections: [
+          { id: 'statistics-summary', kind: 'table', data: { cols: ['维度/主题', '提及数 n', '有效样本 N', '统计口径与范围', '数据解释与限制', '产品含义', '证据'], rows: [
+            ['密封便利', 4, 10, '夹具样本10条,不代表真实调研', '多主题可重叠', '用于验证密封结构卡片', ''],
+            ['待核材料', null, null, '夹具分母尚未核实', '不计算比例', '先补原件再判断', '']] } },
+          { id: 'course-priority', kind: 'table', data: { cols: ['机会', '证据强度', '可做性', '商业价值', '依据一句话', '排序'], rows: [
+            ['浏览器夹具候选', 3, 2, null, '商业价值尚未核实,仅验证未知项展示', '待补资料']] } },
+        ] }], evidence: {}, attachments: [{ id: 'browser-only-file', name: '浏览器夹具.html', contentType: 'text/html', sha256: 'a'.repeat(64),
+          url: location.origin + '/api/functions?path=%2Fksf%2Ffile&artifactId=' + encodeURIComponent(view.artifactId) + '&fileId=browser-only-file' }] };
+      const artifact = { ...original, report };
+      view.db = { ...db, artifacts: db.artifacts.map(row => row.id === artifact.id ? artifact : row), artifactById: { ...db.artifactById, [artifact.id]: artifact } };
+      view._readingFormat = 'html'; await view.updateComplete;
+    });
+    await page.locator('[data-html-report] iframe').waitFor();
+    await page.frameLocator('[data-html-report] iframe').locator('h1').waitFor();
+    await page.getByRole('button', { name: '看板正文与证据', exact: true }).click();
+    assert.equal(await page.locator('[data-html-report] iframe').count(), 0);
+    assert.equal(await page.locator('.statistics-card').count(), 2); assert.equal(await page.locator('.course-assessment').count(), 1);
+    assert.ok((await page.locator('.statistics-card').first().textContent()).includes('40.0%'));
+    assert.ok((await page.locator('.statistics-card').nth(1).textContent()).includes('不计算占比'));
+    assert.ok((await page.locator('.course-assessment').textContent()).includes('未知项待补'));
+    assert.equal(await page.evaluate(() => Math.max(0, document.documentElement.scrollWidth - innerWidth)), 0);
+    await page.locator('[data-statistics-summary]').scrollIntoViewIfNeeded();
+    await page.screenshot({ path: path.join(output, 'fixture-statistics-' + width + '.png') });
+    await page.getByRole('button', { name: 'HTML 排版', exact: true }).click();
+    await page.locator('[data-html-report] iframe').waitFor();
+    checks.push({ page: 'in-memory-fixture', width, statisticsCards: 2, courseAssessmentCards: 1, htmlToggle: '1→0→1', actualPublicData: false, saved: false });
+  }
+  assert.deepEqual(blockedWrites, []); assert.deepEqual(errors, []); receipt.ok = true;
+} catch (error) { receipt.ok = false; receipt.failure = error.stack; process.exitCode = 1; }
+finally { await browser.close(); await fs.writeFile(path.join(output, 'browser-check.json'), JSON.stringify(receipt, null, 2) + '\n'); }
+console.log(JSON.stringify(receipt, null, 2));

+ 2 - 2
src/components/kf-opportunity-card.js

@@ -114,7 +114,7 @@ export class KfOpportunityCard extends LitElement {
         <div class="wrap">
           <div class="top">
             <div class="tags">
-              ${o.priority ? html`<kf-badge kind="priority" .value=${o.priority}></kf-badge>` : ''}
+              ${o.priority ? html`<span title="历史优先级,不代替课程三维评分">历史 <kf-badge kind="priority" .value=${o.priority}></kf-badge></span>` : ''}
               ${o.category ? html`<kf-badge kind="plain" .value=${o.category}></kf-badge>` : ''}
               ${o.stageObj ? html`<kf-badge kind="stage" .value=${o.stageObj.code} label=${`${o.stageObj.code} ${o.stageObj.name}`}></kf-badge>` : ''}
             </div>
@@ -129,7 +129,7 @@ export class KfOpportunityCard extends LitElement {
           <div class="nums">
             <div class="n score">
               <div class="v ${s.avg == null ? 'none' : ''}">${s.avg == null ? '暂无评分' : s.avg}<small>${s.avg == null ? '' : '分'}</small></div>
-              <div class="l">${s.n ? t(`${s.n} 人评分`) : t('待第 1 人')}</div>
+              <div class="l">${s.n ? t(`${s.n} 人点评`) : t('待第 1 人点评')}</div>
             </div>
             <div class="n">
               <div class="v">${o.evidenceLabel}<small>证据</small></div>

+ 4 - 3
src/components/kf-score-panel.js

@@ -128,16 +128,17 @@ export class KfScorePanel extends LitElement {
 
       <form @submit=${this.#submit} data-no-activate aria-busy=${this.busy ? 'true' : 'false'}>
         <div class="row">
-          <strong>我来评分</strong>
+          <strong>我来点评</strong>
           <span class="hint">${this.user ? html`评分人:<b>${this.user}</b>` : '未选择「我是谁」'}</span>
         </div>
         <div class="row">
           <input type="number" min="0" max="100" placeholder="0–100" .value=${this._score} ?disabled=${this.busy}
-            @input=${(e) => { this._score = e.target.value; }} aria-label="分数" />
+            @input=${(e) => { this._score = e.target.value; }} aria-label="点评分数(0–100)" />
           <input type="text" placeholder="一句评语(可选)" .value=${this._comment} ?disabled=${this.busy}
             @input=${(e) => { this._comment = e.target.value; }} aria-label="评语" />
-          <button class="kf-btn primary" type="submit" ?disabled=${this.busy}>${this.busy ? '提交中…' : '提交评分'}</button>
+          <button class="kf-btn primary" type="submit" ?disabled=${this.busy}>${this.busy ? '提交中…' : '提交点评'}</button>
         </div>
+        <p class="hint">百分制多人点评,不代替课程的三维1–3分排序。</p>
         ${this._err ? html`<div class="err" role="alert">${this._err}</div>` : ''}
         ${s.latest ? html`<div class="hint">${t(`最近一次:${s.latest.user} ${s.latest.score} 分 · ${fmtDate(s.latest.at, true)}`)}</div>` : ''}
       </form>

+ 36 - 0
src/core/course-assessment.js

@@ -0,0 +1,36 @@
+// Generated by scripts/build-workshop-skill.mjs; maintain the Skill course-assessment.mjs source.
+/** v3 course scores live in an existing report table, separately from 0–100 peer reviews. */
+export const COURSE_ASSESSMENT_ID = 'course-priority';
+export const COURSE_ASSESSMENT_COLUMNS = Object.freeze(['机会', '证据强度', '可做性', '商业价值', '依据一句话', '排序']);
+const invalid = () => { throw Object.assign(Error('COURSE_ASSESSMENT_INVALID'), { code: 'COURSE_ASSESSMENT_INVALID' }); };
+const key = value => typeof value === 'string' ? value.replace(/\s/gu, '') : '';
+const text = value => typeof value === 'string' && Boolean(value.trim());
+const score = value => {
+  if (value === null || value === '' || ['待确认', '未知', '未评分', '—'].includes(value)) return null;
+  if (typeof value === 'string' && /^[123]$/.test(value.trim())) value = Number(value.trim());
+  if (!Number.isInteger(value) || value < 1 || value > 3) invalid();
+  return value;
+};
+
+export function courseAssessmentRows(section) {
+  if (section?.id !== COURSE_ASSESSMENT_ID) return null;
+  if (section.kind !== 'table' || !Array.isArray(section.data?.cols)
+    || section.data.cols.length !== COURSE_ASSESSMENT_COLUMNS.length
+    || section.data.cols.some((column, i) => key(column) !== key(COURSE_ASSESSMENT_COLUMNS[i]))
+    || !Array.isArray(section.data.rows) || !section.data.rows.length) invalid();
+  return section.data.rows.map(row => {
+    if (!Array.isArray(row) || row.length !== 6) invalid();
+    const [candidate, evidence, feasibility, business, basis, order] = row;
+    if (!text(candidate) || !text(basis) || typeof order !== 'string') invalid();
+    const scores = [evidence, feasibility, business].map(score);
+    const unknown = scores.some(value => value === null);
+    const low = scores.includes(1);
+    if ((unknown || low) && /^(?:第\s*)?[123一二三]\s*(?:名|位)?$/.test(order.trim())) invalid();
+    return { candidate, scores, basis, order, unknown, low,
+      status: low ? '不进入前三' : unknown ? '未知项待补' : '可参与排序' };
+  });
+}
+
+export function validateCourseAssessment(report) {
+  for (const chapter of report?.chapters || []) for (const section of chapter.sections || []) courseAssessmentRows(section);
+}

+ 33 - 0
src/core/report-statistics.js

@@ -0,0 +1,33 @@
+// Generated by scripts/build-workshop-skill.mjs; maintain the Skill report-statistics.mjs source.
+/** A semantic table within the existing report schema; no new section.kind or API fields. */
+export const STATISTICS_SECTION_ID = 'statistics-summary';
+export const STATISTICS_COLUMNS = Object.freeze(['维度/主题', '提及数 n', '有效样本 N', '统计口径与范围', '数据解释与限制', '产品含义', '证据']);
+const invalid = () => { throw Object.assign(new Error('REPORT_STATISTICS_INVALID'), { code: 'REPORT_STATISTICS_INVALID' }); };
+const hasText = value => typeof value === 'string' && Boolean(value.trim());
+const count = value => value === null || Number.isSafeInteger(value) && value >= 0;
+// The board's CJK typography pass may replace ASCII spaces with thin spaces.
+const columnKey = value => typeof value === 'string' ? value.replace(/\s/gu, '') : null;
+
+export function statisticsRows(section) {
+  if (section?.id !== STATISTICS_SECTION_ID) return null;
+  if (section.kind !== 'table' || !Array.isArray(section.data?.cols) || section.data.cols.length !== STATISTICS_COLUMNS.length
+    || section.data.cols.some((column, index) => columnKey(column) !== columnKey(STATISTICS_COLUMNS[index]))
+    || !Array.isArray(section.data?.rows) || !section.data.rows.length) invalid();
+  return section.data.rows.map(row => {
+    if (!Array.isArray(row) || row.length !== 7) invalid();
+    const [label, numerator, denominator, scope, interpretation, implication, evidenceId] = row;
+    if (![label, scope, interpretation, implication].every(hasText) || typeof evidenceId !== 'string'
+      || !count(numerator) || !count(denominator)
+      || numerator !== null && denominator !== null && numerator > denominator) invalid();
+    const ratio = numerator !== null && denominator !== null && denominator > 0 ? numerator / denominator : null;
+    return { label, numerator, denominator, scope, interpretation, implication, evidenceId,
+      ratio, percent: ratio === null ? null : (ratio * 100).toFixed(1) + '%' };
+  });
+}
+
+export function validateReportStatistics(report) {
+  for (const chapter of report?.chapters || []) for (const section of chapter.sections || []) {
+    const rows = statisticsRows(section);
+    if (rows?.some(row => row.evidenceId && !Object.hasOwn(report.evidence || {}, row.evidenceId))) invalid();
+  }
+}

+ 3 - 1
src/core/workshop-outputs.js

@@ -1,6 +1,7 @@
 /** Classroom output navigation. Explicit task/chapter references only; no title guessing. */
 import { REPORT_TYPES } from './library/types.js';
 import { CORE_DELIVERABLES, chapterHasContent, reportBodyStatus, reportCategoryOf, validateReportDelivery } from './report-delivery.js';
+import { courseOutputLedger, courseModuleOf } from '../../skills/ksf-workshop-runtime/scripts/course-deliverables.mjs';
 
 export const WORKSHOP_OUTPUTS = CORE_DELIVERABLES;
 export const WORKSHOP_REPORT_CATEGORIES = Object.freeze([
@@ -98,7 +99,8 @@ export function workshopReview(db, groupState, query = {}) {
       return modes.map(mode => {
         const reports = topicRows.filter(row => row.mode === mode);
         const unlocatedCount = reports.filter(row => !row.refs.length && row.reportType !== 'method').length;
-        return { workshopKey, mode, unlocatedCount, outputs: WORKSHOP_OUTPUTS.map(output => {
+        return { workshopKey, mode, unlocatedCount, courseUnlocatedCount: reports.filter(row => !courseModuleOf(row.artifact)).length,
+          courseOutputs: courseOutputLedger(reports, hasContent), outputs: WORKSHOP_OUTPUTS.map(output => {
           const located = reports.filter(row => row.refs.some(ref => ref.key === output.key));
           const submitted = located.filter(row => row.artifact.report?.delivery?.contentStatus !== 'draft');
           const incomplete = reports.filter(row => Array.isArray(row.artifact.report?.chapters) && row.artifact.report.chapters.some(ch => ch?.id === output.anchor));

+ 22 - 0
src/pages/report-styles.js

@@ -181,6 +181,19 @@ export const REPORT_CSS = css`
   .chain i { font-style:normal; font-size:11px; color:var(--kf-ink-3); }
 
   [hidden] { display:none!important; }
+  .statistics-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:18px; }
+  .statistics-card { min-width:0; padding:24px; border:1px solid var(--kf-line); border-top:3px solid var(--kf-accent-ink); border-radius:12px; background:var(--kf-surface); }
+  .statistics-card h5 { margin:0 0 16px; font-size:16px; line-height:1.7; overflow-wrap:anywhere; }
+  .statistics-number { display:flex; flex-wrap:wrap; gap:8px; align-items:baseline; font-variant-numeric:tabular-nums; }
+  .statistics-number strong { font-size:30px; line-height:1.3; color:var(--kf-accent-ink); }
+  .statistics-number span { color:var(--kf-ink-3); font-size:14px; }
+  .statistics-scope { color:var(--kf-ink-3); font-size:12px; line-height:1.9; margin-top:14px; white-space:pre-line; overflow-wrap:anywhere; }
+  .statistics-bar { height:5px; background:var(--kf-line); border-radius:6px; margin:16px 0; overflow:hidden; }
+  .statistics-bar i { display:block; height:100%; background:var(--kf-accent-ink); }
+  .statistics-interpretation,.statistics-implication { margin:16px 0 0; font-size:14px; line-height:1.95; white-space:pre-line; overflow-wrap:anywhere; }
+  .statistics-implication { border-top:1px solid var(--kf-line); padding-top:14px; color:var(--kf-ink-2); }
+  .statistics-table summary { font-size:13px; line-height:1.8; color:var(--kf-accent-ink); cursor:pointer; padding:12px 0; }
+  @media(max-width:767px) { .statistics-grid { grid-template-columns:1fr; gap:14px; } .statistics-card { padding:18px; } }
   .html-reading { overflow:hidden; }
   .html-eyebrow { color:var(--kf-accent-ink); font-size:11px; letter-spacing:.12em; margin-bottom:5px; }
   .html-toolbar { display:flex; align-items:center; flex-wrap:wrap; gap:14px; justify-content:space-between; margin:20px 0 14px; font-size:13px; }
@@ -233,6 +246,15 @@ export const REPORT_CSS = css`
     .decision-history summary { min-height:44px; display:flex; align-items:center; }
   }
   @media(max-width:360px) { .cover { --kf-card-pad:16px; } article.ch { padding:16px; } .cover h1 { font-size:21px; } .finding { padding:12px; } }
+  .course-assessments { display:grid; gap:18px; }
+  .course-assessment { min-width:0; padding:22px; border:1px solid var(--kf-line); border-radius:12px; background:var(--kf-surface-btn); }
+  .course-assessment h5 { margin:0; font-size:16px; }
+  .course-status { font-size:12px; color:var(--kf-accent-ink); margin:8px 0 14px; }
+  .course-dimensions { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:8px; margin-bottom:16px; }
+  .course-dimensions > div { min-width:0; padding:10px; border:1px solid var(--kf-line); border-radius:8px; background:var(--kf-surface); }
+  .course-dimensions span { display:block; color:var(--kf-ink-3); font-size:12px; }
+  .course-dimensions strong { font-size:18px; overflow-wrap:anywhere; }
+  @media(max-width:480px) { .course-assessment { padding:14px; } .course-dimensions { gap:4px; } .course-dimensions > div { padding:8px 4px; } }
   @media print {
     .reading-nav,.toc,.rail { display:none; }
     .layout { display:block; }

+ 14 - 9
src/pages/view-pool.js

@@ -9,8 +9,8 @@
  */
 import { LitElement, css, html } from 'lit';
 import { useTokens } from '../components/base.js';
-import { criteriaOf, queryOf, applyFilter, facetCounts, collectValues, toggleFacet, SORTS, sortOpportunities } from '../core/index.js';
-import { navigate } from '../utils/router.js';
+import { criteriaOf, queryOf, applyFilter, facetCounts, collectValues, toggleFacet, SORTS as LEGACY_SORTS, sortOpportunities } from '../core/index.js';
+import { navigate, href } from '../utils/router.js';
 import { ref } from 'lit/directives/ref.js';
 import { t, head, section, VIEW_CSS } from './shared.js';
 import { CARD_CSS } from './report-card.js';
@@ -19,10 +19,13 @@ import './filter-sheet.js';
 
 /* 手机筛选弹层:机会卡池的五维;「状态」是单选维度(同一时间只能是候选 / 已立项 / 已否决之一) */
 const SINGLE_DIM = 'status';
+const DEFAULT_SORT = 'newest';
+const SORTS = LEGACY_SORTS.map(sort => ({ ...sort, label: ({ priority: '历史等级 S→A→B', score: '多人点评 高→低',
+  evidence: '卡片证据等级', debate: '点评分歧' })[sort.key] || sort.label }));
 
 const DIMS = [
   { key: 'status', label: '状态', values: () => [{ value: 'candidate', label: '候选' }, { value: 'approved', label: '已立项' }, { value: 'rejected', label: '已否决' }] },
-  { key: 'priority', label: '优先级', values: () => ['S', 'A', 'B'].map((v) => ({ value: v, label: t(`${v} 级`) })) },
+  { key: 'priority', label: '历史等级', values: () => ['S', 'A', 'B'].map((v) => ({ value: v, label: t(`${v} 级`) })) },
   { key: 'stage', label: '阶段', values: () => ['S1', 'S2', 'S3', 'S4', 'S5'].map((v) => ({ value: v, label: v })) },
   { key: 'category', label: '品类', values: (rows) => collectValues(rows, 'category') },
   { key: 'member', label: '上传人', values: (rows) => collectValues(rows, 'member') },
@@ -55,6 +58,7 @@ export class ViewPool extends LitElement {
      没有 .rc/.cfoot 规则;见 docs/v13-report.md 三),所以下面还留着一份「排序行内的就地样式」,
      两者同时命中时由就地样式决定 —— 见 docs 里「A 的验证结果」。 */
   static styles = [VIEW_CSS, CARD_CSS, css`
+    .course-note { font-size:13px; line-height:1.8; color:var(--kf-ink-3); overflow-wrap:anywhere; margin:0 0 18px; }
     /* 筛选栏(v12 P1):一个容器,内部分组「状态 | 优先级 | 阶段 | 品类 | 上传人 | 排序」。
        分组之间 1px 竖线(::before),组内 chip 8px 间隔;最后一行「排序 + 搜索」与上面的 chip 行之间 1px 横线。
        容器不是玻璃卡嵌套:它就是筛选区本身,sticky 吸在顶栏下。 */
@@ -190,13 +194,14 @@ export class ViewPool extends LitElement {
     const c = this.#criteria;
     const cnt = DIMS.reduce((n, d) => n + (c[d.key] || []).length, 0);
     const rows = db.opportunities;
-    const filtered = sortOpportunities(applyFilter(rows, c), c.sort || 'priority');
+    const filtered = sortOpportunities(applyFilter(rows, c), c.sort || DEFAULT_SORT);
     const active = DIMS.some((d) => (c[d.key] || []).length) || c.q;
     return html`
-      ${head('机会卡池', '全员机会卡汇总入库,带评分与优先级 —— 这是研发中心的机会储备。',
+      ${head('机会卡池', '个人候选机会与关联报告;课程排序沿 D2-05 三维评分及未知项表。',
         html`<span class="count"><b>${filtered.length}</b> / ${t(`${rows.length} 张`)}</span>
           ${active ? html`<button class="reset" @click=${() => navigate('/pool', { replace: true })}>清除全部条件</button>` : ''}`)}
 
+      <p class="course-note">课程使用证据强度、可做性、商业价值各1–3分;任一维1分不进前三,未知单列。<a data-link href=${href('/reports/assessment?stage=S4')}>查看评估与排序报告</a>。卡片百分制多人点评与历史等级分别保留。</p>
       <div class="pool-body">
       <!-- 手机工具条:与类型页同一套(筛选按钮带已选数 / 结果数 / 排序收起 / 搜索),筛选面板 ⤢ 按钮锚定到这里 -->
       <div class="pbar tb kf-glass is-panel" data-toolbar-m ${ref((el) => { this._pbar = el; })}>
@@ -205,8 +210,8 @@ export class ViewPool extends LitElement {
         </button>
         <input type="search" placeholder="搜索标题 / 一句话 / 上传人" .value=${c.q || ''} @input=${this.#search} aria-label="搜索机会卡" />
         <details class="pbar-more">
-          <summary aria-label="排序">${icon('sort', 16)}<span>${SORTS.find((s) => s.key === (c.sort || 'priority')).label}</span></summary>
-          <div class="pbar-sorts">${SORTS.map((s) => html`<button aria-pressed=${(c.sort || 'priority') === s.key ? 'true' : 'false'} @click=${() => { this.#setSort(s.key); this.renderRoot.querySelector('.pbar-more')?.removeAttribute('open'); }}>${s.label}</button>`)}</div>
+          <summary aria-label="排序">${icon('sort', 16)}<span>${SORTS.find((s) => s.key === (c.sort || DEFAULT_SORT)).label}</span></summary>
+          <div class="pbar-sorts">${SORTS.map((s) => html`<button aria-pressed=${(c.sort || DEFAULT_SORT) === s.key ? 'true' : 'false'} @click=${() => { this.#setSort(s.key); this.renderRoot.querySelector('.pbar-more')?.removeAttribute('open'); }}>${s.label}</button>`)}</div>
         </details>
       </div>
       ${this.#mobileSheet(c, rows, filtered.length)}
@@ -230,7 +235,7 @@ export class ViewPool extends LitElement {
         <div class="frow toolbar">
           <span class="lbl">排序</span>
           <div class="seg" role="group" aria-label="排序方式">
-            ${SORTS.map((s) => html`<button aria-pressed=${(c.sort || 'priority') === s.key ? 'true' : 'false'} @click=${() => this.#setSort(s.key)}>${s.label}</button>`)}
+            ${SORTS.map((s) => html`<button aria-pressed=${(c.sort || DEFAULT_SORT) === s.key ? 'true' : 'false'} @click=${() => this.#setSort(s.key)}>${s.label}</button>`)}
           </div>
           <input type="search" placeholder="搜索卡片标题 / 一句话 / 上传人" .value=${c.q || ''} @input=${this.#search} aria-label="搜索机会卡" />
           ${/* 页脚收尾:与报告中心同一条「本页多少张」+「回到顶部」(样式与行为逐字对齐 cardFooter) */ ''}
@@ -239,7 +244,7 @@ export class ViewPool extends LitElement {
         </div>
       </div>
 
-      ${section('池子', active ? '符合条件的卡片' : '全部卡片', html`<span>${SORTS.find((s) => s.key === (c.sort || 'priority')).label}</span>`)}
+      ${section('池子', active ? '符合条件的卡片' : '全部卡片', html`<span>${SORTS.find((s) => s.key === (c.sort || DEFAULT_SORT)).label}</span>`)}
 
       ${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"

+ 26 - 1
src/pages/view-report.js

@@ -17,6 +17,8 @@ import { reportOverview, reportExecutive, findingsOf, fmtDate, chapterDigests, r
 import { reportCategoryOf, reportBodyStatus } from '../core/report-delivery.js';
 import { reportAttachments } from '../core/report-attachments.js';
 import { evidenceExcerpts } from '../core/report-evidence.js';
+import { statisticsRows } from '../core/report-statistics.js';
+import { courseAssessmentRows } from '../core/course-assessment.js';
 import { opportunitiesForReport, reportOpportunity } from '../core/opportunity/report.js';
 import '../components/kf-score-panel.js';
 import '../components/kf-evidence-chain.js';
@@ -219,7 +221,7 @@ export class ViewReport extends LitElement {
     const selected = this.#selectedOpportunity();
     if (!linked.length && !selected && !this.opportunityId) return '';
     return html`<kf-glass-card surface="1" class="r rating-card" id="opportunity-rating" data-opportunity-rating><section class="r-in">
-      <h2 class="rating-title">多人评分</h2>
+        <h2 class="rating-title">多人点评(0–100分)</h2>
       ${linked.length > 1 || (this.artifactId && this.opportunityId && !selected) ? html`
         <label class="rating-label" for="rating-opportunity">选择评分与决策的机会</label>
         <select id="rating-opportunity" aria-label="评分与决策机会" @change=${e => {
@@ -533,6 +535,29 @@ export class ViewReport extends LitElement {
           <thead><tr>${(s.data?.cols || []).map((c) => html`<th>${c}</th>`)}</tr></thead>
           <tbody>${(s.data?.rows || []).map((row) => html`<tr>${row.map((cell, i) => html`<td class=${i === 0 ? 'k' : ''}>${this.#cell(cell)}</td>`)}</tr>`)}</tbody></table></div>`;
       case 'table':
+        if (s.id === 'course-priority') {
+          let rows;
+          try { rows = courseAssessmentRows(s); } catch { /* Keep malformed historical tables readable as saved. */ }
+          if (rows) return html`${title}<div class="course-assessments" data-course-assessment>${rows.map(row => html`<article class="course-assessment">
+            <h5>${row.candidate}</h5><p class="course-status">${row.status}</p>
+            <div class="course-dimensions">${row.scores.map((value, i) => html`<div><span>${['证据强度', '可做性', '商业价值'][i]}</span><strong>${value === null ? '待确认' : value + ' / 3'}</strong></div>`)}</div>
+            <p>${row.basis}</p><p class="evidence-note">排序说明:${row.order || '未给排序'}</p>
+          </article>`)}</div><p class="evidence-note">三维各1–3分;任一维1分不进入前三,未知不填低分。商业价值需本组复核;不自动求总分或补足前三。</p>
+          <details><summary>查看原始排序表</summary><div class="tbl-wrap"><table class="rt"><thead><tr>${s.data.cols.map(c => html`<th>${c}</th>`)}</tr></thead><tbody>${s.data.rows.map(row => html`<tr>${row.map(cell => html`<td>${this.#cell(cell)}</td>`)}</tr>`)}</tbody></table></div></details>`;
+        }
+        if (s.id === 'statistics-summary') {
+          let rows;
+          try { rows = statisticsRows(s); } catch { /* Historical malformed tables remain readable below. */ }
+          if (rows) return html`${title}<div class="statistics-grid" data-statistics-summary>${rows.map(row => html`<article class="statistics-card">
+            <h5>${row.label}</h5><div class="statistics-number"><strong>${row.numerator ?? '待统计'}</strong><span> / ${row.denominator ?? '未核'} ${row.percent !== null ? ' · ' + row.percent : ''}</span></div>
+            <p class="statistics-scope">${row.scope}</p>
+            ${row.ratio !== null ? html`<div class="statistics-bar" role="img" aria-label=${'本样本内占比 ' + row.percent}><i style=${'width:' + row.ratio * 100 + '%'}></i></div>` : html`<p class="evidence-note">${row.denominator === 0 ? '无有效样本,不计算占比' : '统计未完整核实,不计算占比'}</p>`}
+            <p class="statistics-interpretation">${row.interpretation}</p><p class="statistics-implication"><b>产品含义</b> ${row.implication}</p>
+            ${this.#anchor(row.evidenceId, '查看代表原话与出处')}
+          </article>`)}</div><p class="evidence-note">条形仅表示所列有效样本内的提及占比,不是证据强度评分;多主题可重叠,适用范围见各卡口径。</p>
+          <details class="statistics-table"><summary>查看统计汇总表与口径</summary><div class="tbl-wrap"><table class="rt"><thead><tr>${s.data.cols.map(c => html`<th>${c}</th>`)}</tr></thead>
+            <tbody>${s.data.rows.map(row => html`<tr>${row.map(cell => html`<td>${this.#cell(cell)}</td>`)}</tr>`)}</tbody></table></div></details>`;
+        }
         return html`${title}<div class="tbl-wrap"><table class="rt">
           <thead><tr>${(s.data?.cols || []).map((c) => html`<th>${c}</th>`)}</tr></thead>
           <tbody>${(s.data?.rows || []).map((row) => html`<tr>${row.map((cell, i) => html`<td class=${i === 0 ? 'k' : ''}>${this.#cell(cell)}</td>`)}</tr>`)}</tbody></table></div>`;

+ 58 - 10
src/pages/view-workshop-outputs.js

@@ -1,12 +1,14 @@
 import { LitElement, html, css } from 'lit';
 import { useTokens } from '../components/base.js';
 import { WORKSHOP_REPORT_CATEGORIES, OUTPUT_GROUP_NONE, workshopReview } from '../core/workshop-outputs.js';
+import { COURSE_DELIVERABLES, courseModuleOf } from '../../skills/ksf-workshop-runtime/scripts/course-deliverables.mjs';
 import { memberGroupService } from '../services/member-groups.js';
 import { href, setQuery, currentUrl, reportUrl } from '../utils/router.js';
 import { head, VIEW_CSS } from './shared.js';
 import { icon } from '../components/icons.js';
 
 const MODE_LABELS = { sample: '样例' };
+const REPORT_CATEGORY_NAMES = Object.fromEntries(WORKSHOP_REPORT_CATEGORIES.map(category => [category.key, category.name]));
 
 export class ViewWorkshopOutputs extends LitElement {
   static properties = { db: { type: Object }, routeQuery: { type: Object }, _groups: { state: true }, _loading: { state: true }, _error: { state: true } };
@@ -27,19 +29,47 @@ export class ViewWorkshopOutputs extends LitElement {
   _outputLink(row) {
     return reportUrl(row.id, currentUrl());
   }
-  _reportCell(member, category) {
-    const reports = member.reports.filter(row => (row.reportType || 'unclassified') === category.key);
-    return html`<td data-category=${category.key}>
+  _reportCell(member, output) {
+    const reports = member.reports.filter(row => courseModuleOf(row.artifact) === output.moduleId);
+    const multipleTopics = new Set(member.reports.map(row => row.artifact.workshopKey).filter(Boolean)).size > 1;
+    return html`<td data-course-column=${output.moduleId}>
       ${reports.length ? html`<div class="cell-links">${reports.map((row, index) => html`
         <a class="report-link" data-link data-workshop-artifact=${row.id}
           href=${href(this._outputLink(row))} title=${row.artifact.title}
-          aria-label=${`${member.name} · ${category.name} · ${row.artifact.title}`}>
+          aria-label=${`${member.name} · ${output.moduleId} · ${output.name} · ${row.artifact.title}`}>
           <span class="link-label"><span>${reports.length === 1 ? '查看报告' : `查看报告 ${index + 1}`}</span>${icon('right', 10)}</span>
+          <small class="report-category">${REPORT_CATEGORY_NAMES[row.reportType] || '未归类'}</small>
+          ${multipleTopics ? html`<small class="report-topic">${row.artifact.workshopKey || '课题未标明'}</small>` : ''}
           ${MODE_LABELS[row.mode] ? html`<small class="mode">${MODE_LABELS[row.mode]}</small>` : ''}
         </a>`)}
-      </div>` : html`<span class="empty-cell" aria-label="暂无报告">—</span>`}
+      </div>` : html`<span class="empty-cell">未保存</span>`}
     </td>`;
   }
+  _unlocatedReports(member) {
+    const reports = member.reports.filter(row => !courseModuleOf(row.artifact));
+    if (!reports.length) return '';
+    return html`<section class="unlocated-reports" data-unlocated-member=${member.id}>
+      <h4>${member.name} · 环节待定位(${reports.length} 份)</h4>
+      <div class="cell-links">${reports.map(row => html`<a class="report-link" data-link data-workshop-artifact=${row.id}
+        href=${href(this._outputLink(row))}>${row.artifact.title}<small>${REPORT_CATEGORY_NAMES[row.reportType] || '未归类'}</small></a>`)}</div>
+    </section>`;
+  }
+  _courseLedger(member) {
+    const labels = { 'has-body': '已有正文', draft: '草稿', 'text-only': '仅原文', 'missing-body': '缺正文', missing: '未保存' };
+    return html`<details class="course-ledger" data-course-member=${member.id}>
+      <summary>${member.name} · 13 份环节产出</summary>
+      ${member.topics.length ? member.topics.map(topic => html`<section class="course-topic">
+        <p class="course-note">课题 ${topic.workshopKey || '未标明'} · ${topic.mode === 'sample' ? '样例' : topic.mode === 'manual' ? '人工资料' : topic.mode === 'live' ? '真实资料' : '模式未标明'} · ${topic.courseOutputs.filter(output => output.state === 'has-body').length}/13 环节已有正文</p>
+        <p class="course-note">保存状态用于定位成果;内容完整性、HTML、证据及员工确认仍需分别核验。</p>
+        ${topic.courseUnlocatedCount ? html`<p class="course-note">${topic.courseUnlocatedCount} 份报告的环节待定位,可从本组的“环节待定位”区域打开。</p>` : ''}
+        <ol>${topic.courseOutputs.map(output => html`<li data-course-module=${output.moduleId}>
+          <div><b>${output.moduleId}</b><span>${output.name}</span><em>${labels[output.state]}</em></div>
+          <small>${output.checks.join(' · ')}</small>
+          ${output.reports.map(row => html`<a class="report-link" data-link href=${href(this._outputLink(row))}>查看报告 · ${row.artifact.title}</a>`)}
+        </li>`)}</ol>
+      </section>`) : html`<p class="course-note">尚无课题成果;保存后按环节显示 13 份产出物的位置。</p>`}
+    </details>`;
+  }
   render() {
     if (!this.db || (this._loading && !this._groups)) return html`<kf-empty kind="loading" title="小组成果"></kf-empty>`;
     // Only group selection affects this page. Legacy filter URLs cannot hide a member's reports.
@@ -48,7 +78,6 @@ export class ViewWorkshopOutputs extends LitElement {
     const allGroups = view.hierarchy.filter(group => group.id !== OUTPUT_GROUP_NONE || group.members.length);
     const selected = String(this.routeQuery?.group || '');
     const groups = allGroups.filter(group => !selected || group.id === selected);
-    const categories = WORKSHOP_REPORT_CATEGORIES.filter(category => category.key !== 'unclassified' || view.unclassified);
     const countReports = group => group.members.reduce((sum, member) => sum + member.reportCount, 0);
     return html`
       ${head('小组成果')}
@@ -62,6 +91,7 @@ export class ViewWorkshopOutputs extends LitElement {
           <span>${view.groupStateAvailable ? group.name : '归属待确认'}</span><b>${countReports(group)}</b>
         </button>`)}
       </nav>
+      <p class="table-hint">按 13 个环节查看组员成果,可左右滑动查看全部产出。</p>
       ${this._error ? html`<p class="error" role="alert">${this._error},小组归属暂未确认。<button type="button" @click=${() => this._readGroups()}>重试</button></p>` : ''}
       <div class="group-tables">${groups.map((group, groupIndex) => html`
         <section class="group-block" data-output-group=${group.id} aria-labelledby=${`output-group-${groupIndex}`}>
@@ -72,14 +102,16 @@ export class ViewWorkshopOutputs extends LitElement {
           ${group.members.length ? html`
             <div class="table-scroll" tabindex="0" role="region" aria-label=${`${group.name}报告表格`}>
               <table aria-label=${`${group.name}组员报告`}>
-                <colgroup><col class="member-col">${categories.map(() => html`<col class="report-col">`)}</colgroup>
-                <thead><tr><th scope="col" class="member-cell">组员</th>${categories.map(category => html`<th scope="col" data-category=${category.key}>${category.name}</th>`)}</tr></thead>
+                <colgroup><col class="member-col">${COURSE_DELIVERABLES.map(() => html`<col class="report-col">`)}</colgroup>
+                <thead><tr><th scope="col" class="member-cell">组员</th>${COURSE_DELIVERABLES.map(output => html`<th scope="col" data-course-column=${output.moduleId} title=${output.name}><b class="module-code">${output.moduleId}</b><span>${output.name}</span></th>`)}</tr></thead>
                 <tbody>${group.members.map(member => html`<tr data-output-member=${member.id}>
                   <th scope="row" class="member-cell"><span class="member-name">${member.name}</span><span class="member-count">${member.reportCount} 份报告</span></th>
-                  ${categories.map(category => this._reportCell(member, category))}
+                  ${COURSE_DELIVERABLES.map(output => this._reportCell(member, output))}
                 </tr>`)}</tbody>
               </table>
             </div>` : html`<div class="empty-group">暂无组员</div>`}
+          ${group.members.map(member => this._unlocatedReports(member))}
+          ${group.members.map(member => this._courseLedger(member))}
         </section>`)}
       </div>
       ${!groups.length ? html`<kf-empty title=${selected ? '未找到该小组' : '暂无小组成果'}
@@ -96,17 +128,32 @@ export class ViewWorkshopOutputs extends LitElement {
       padding: 8px 12px; border: 1px solid var(--kf-line); border-radius: 8px; background: var(--kf-surface); color: var(--kf-ink-2); text-align: left; }
     .categories button span { overflow-wrap: anywhere; }
     .group-tables { display: grid; gap: 24px; min-width: 0; }
+    .table-hint { margin: -10px 0 18px; color: var(--kf-ink-3); font-size: 12px; line-height: 1.7; }
+    .course-ledger { border-top: 1px solid var(--kf-line); padding: 14px 18px; }
+    .course-ledger summary { cursor: pointer; color: var(--kf-ink); font-weight: 600; }
+    .course-note { color: var(--kf-ink-3); font-size: 12px; overflow-wrap: anywhere; line-height: 1.7; }
+    .course-ledger ol { list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); gap: 10px; }
+    .course-ledger li { min-width: 0; padding: 12px; border: 1px solid var(--kf-line); border-radius: 8px; }
+    .course-ledger li > div { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; line-height: 1.7; }
+    .course-ledger li span { flex: 1; min-width: 120px; color: var(--kf-ink); }
+    .course-ledger li em { font-style: normal; font-size: 12px; color: var(--kf-ink-3); }
+    .course-ledger li small { display: block; color: var(--kf-ink-3); line-height: 1.8; margin: 6px 0; }
     .group-block { min-width: 0; overflow: hidden; background: var(--kf-surface); border: 1px solid var(--kf-line); border-radius: 12px; }
+    .unlocated-reports { padding: 14px 18px; border-top: 1px solid var(--kf-line); }
+    .unlocated-reports h4 { margin: 0 0 10px; color: var(--kf-ink-2); font-size: 13px; }
+    .unlocated-reports .cell-links { justify-content: flex-start; }
     .group-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 16px 18px; }
     .group-heading h3 { margin: 0; color: var(--kf-ink); font-size: 16px; font-weight: 600; line-height: 1.5; overflow-wrap: anywhere; }
     .group-heading > span { flex: none; color: var(--kf-ink-3); font-size: 12px; font-variant-numeric: tabular-nums; }
     .sep { margin: 0 8px; }
     .table-scroll { overflow: auto; max-width: 100%; scrollbar-width: thin; scrollbar-color: var(--kf-line-strong) transparent; }
-    table { width: 100%; min-width: 1380px; border-spacing: 0; table-layout: fixed; font-size: 13px; color: var(--kf-ink-2); }
+    table { width: 100%; min-width: 2428px; border-spacing: 0; table-layout: fixed; font-size: 13px; color: var(--kf-ink-2); }
     .member-col { width: 140px; }
+    .report-col { width: 176px; }
     th, td { padding: 12px 10px; text-align: center; vertical-align: middle; border-top: 1px solid var(--kf-line); border-right: 1px solid var(--kf-line-soft); }
     tr > :last-child { border-right: 0; }
     thead th { background: #F6F7FB; font-size: 12px; font-weight: 500; line-height: 1.6; height: 58px; overflow-wrap: anywhere; }
+    .module-code { display: block; margin-bottom: 5px; color: var(--kf-ink); font-size: 12px; }
     tbody th, tbody td { background: var(--kf-surface); }
     tbody tr:hover > * { background: #F9FAFD; }
     .member-cell { position: sticky; left: 0; z-index: 1; text-align: left; padding-inline: 18px; box-shadow: 1px 0 0 var(--kf-line); }
@@ -119,6 +166,7 @@ export class ViewWorkshopOutputs extends LitElement {
     .link-label { display: inline-flex; align-items: center; gap: 2px; white-space: nowrap; }
     .report-link svg { flex: none; }
     .mode { font-size: 10px; line-height: 1.4; color: var(--kf-ink-3); }
+    .report-category, .report-topic { font-size: 10px; overflow-wrap: anywhere; }
     .empty-cell { color: #B6BDCB; font-size: 14px; }
     .empty-group { padding: 24px 18px; border-top: 1px solid var(--kf-line); color: var(--kf-ink-3); text-align: center; font-size: 13px; }
     .error { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 0 0 16px; color: var(--kf-bad-text); font-size: 13px; }

+ 3 - 0
test/board-ui-deploy.test.mjs

@@ -15,6 +15,9 @@ function fixture() {
   const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH));
   manifest.homepageSha256 = sha('fixture homepage');
   for (const file of manifest.files) {
+    // Exercise publisher behavior with current source, independently of the archived release hashes.
+    const source = fs.readFileSync(file.path);
+    file.sha256 = sha(Buffer.from(source.toString('utf8').replaceAll('\r\n', '\n')));
     if (file.allowMissing) continue;
     const target = path.join(targetDir, file.path), bytes = Buffer.from('fixture prior: ' + file.path);
     fs.mkdirSync(path.dirname(target), { recursive: true });

+ 54 - 0
test/board-v3-deploy.test.mjs

@@ -0,0 +1,54 @@
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { publishBoardV3, MANIFEST_PATH } from '../scripts/publish-board-v3-20261112.mjs';
+
+const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
+function fixture() {
+  const base = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-board-v3-test-'));
+  const targetDir = path.join(base, 'site'); fs.mkdirSync(targetDir);
+  const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH)); manifest.homepageSha256 = sha('homepage');
+  fs.writeFileSync(path.join(targetDir, 'index.html'), 'homepage');
+  for (const file of manifest.files) {
+    if (file.allowMissing) continue;
+    const target = path.join(targetDir, file.path), bytes = Buffer.from('prior ' + file.path);
+    fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, bytes);
+    file.priorNormalizedSha256 = [sha(bytes)];
+  }
+  const manifestPath = path.join(base, 'manifest.json'); fs.writeFileSync(manifestPath, JSON.stringify(manifest));
+  return { base, targetDir, manifestPath, manifest };
+}
+test('v3发布检查完整浏览器依赖,原子更新后重复执行无变化,首页不变', () => {
+  const f = fixture(); try {
+    const plan = publishBoardV3(f); assert.equal(plan.readOnly, true); assert.equal(plan.filesChecked, f.manifest.files.length);
+    assert.equal(plan.changes.length, f.manifest.files.length);
+    assert.equal(fs.existsSync(path.join(f.targetDir, 'skills/ksf-workshop-runtime/scripts/course-deliverables.mjs')), false);
+    const result = publishBoardV3({ ...f, apply: true }); assert.equal(result.filesystemVerified, true);
+    assert.equal(result.homepageModified, false); assert.equal(result.businessRecordWrites, 0);
+    assert.deepEqual(publishBoardV3(f).changes, []); assert.equal(fs.readFileSync(path.join(f.targetDir, 'index.html'), 'utf8'), 'homepage');
+  } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
+});
+test('v3发布拒绝未知现场、额外文件和首页变化,写入失败回退已改文件及新增模块', () => {
+  const f = fixture(); try {
+    const originalManifest = fs.readFileSync(f.manifestPath), prior = new Map();
+    for (const file of f.manifest.files) if (!file.allowMissing) prior.set(file.path, fs.readFileSync(path.join(f.targetDir, file.path)));
+    assert.throws(() => publishBoardV3({ ...f, apply: true }, { beforeWrite(relative) {
+      if (relative === 'src/pages/view-report.js') throw Error('simulated write failure');
+    } }), error => error.rollbackComplete === true);
+    for (const file of f.manifest.files) {
+      const target = path.join(f.targetDir, file.path);
+      if (file.allowMissing) assert.equal(fs.existsSync(target), false); else assert.deepEqual(fs.readFileSync(target), prior.get(file.path));
+    }
+    f.manifest.files.push({ path: 'server/library-api.mjs' }); fs.writeFileSync(f.manifestPath, JSON.stringify(f.manifest));
+    assert.throws(() => publishBoardV3({ ...f, apply: true }), { code: 'UNEXPECTED_FILE_LIST' });
+    fs.writeFileSync(f.manifestPath, originalManifest);
+    const unknown = f.manifest.files.find(file => !file.allowMissing).path;
+    fs.writeFileSync(path.join(f.targetDir, unknown), 'concurrent content');
+    assert.throws(() => publishBoardV3({ ...f, apply: true }), { code: 'UNKNOWN_TARGET_CONTENT' });
+    fs.writeFileSync(path.join(f.targetDir, unknown), prior.get(unknown)); fs.writeFileSync(path.join(f.targetDir, 'index.html'), 'other homepage');
+    assert.throws(() => publishBoardV3({ ...f, apply: true }), { code: 'HOMEPAGE_MISMATCH' });
+  } finally { fs.rmSync(f.base, { recursive: true, force: true }); }
+});

+ 73 - 0
test/report-statistics.test.mjs

@@ -0,0 +1,73 @@
+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 { STATISTICS_COLUMNS, statisticsRows, validateReportStatistics } from '../skills/ksf-workshop-runtime/scripts/report-statistics.mjs';
+import { renderReportHtml } from '../skills/ksf-workshop-runtime/scripts/render-report-html.mjs';
+import { prepareHtmlReport, saveAndVerifyReport } from '../skills/ksf-workshop-runtime/scripts/save-report.mjs';
+import { statisticsRows as boardStatisticsRows } from '../src/core/report-statistics.js';
+const section = () => ({ id: 'statistics-summary', kind: 'table', data: { cols: [...STATISTICS_COLUMNS], rows: [
+  ['支持A的记录', 4, 10, '合成测试|逐条判定10条有效记录;同主题去重;非真实资料', '部分支持,不等于结论成立;反证仍需比较', '先核对照样品,不自动改变产品', 'e'],
+] } });
+const task = () => ({ sessionId: 'test-person', taskKey: 'statistics-test', version: 1, artifactType: 'voc_report', title: '隔离合成统计测试',
+  report: { executive: ['合成测试,不是研究结果'], delivery: { schema: 'ksf.workshop-delivery.v1', moduleId: 'D2-02', kind: 'research', contentStatus: 'draft', outputs: [] },
+    chapters: [{ title: '全体相关材料统计', sections: [section()] }], evidence: { e: { claim: '测试支持材料', quotes: [{ text: '合成原话,只用于测试', ref: 'https://example.org/test' }] } } } });
+
+test('统计卡从同一n/N计算占比,保留范围、限制、产品含义和实际证据键', () => {
+  const [row] = statisticsRows(section());
+  assert.equal(row.percent, '40.0%');assert.equal(row.ratio, 0.4);
+  const html = renderReportHtml(task());
+  for (const value of ['40.0%', 'width:40%', '逐条判定10条有效记录', '部分支持,不等于结论成立', '先核对照样品', '合成原话,只用于测试']) assert.ok(html.includes(value), value);
+  assert.match(html, /href="#evidence-1"/);assert.match(html, /查看统计汇总表与口径/);
+});
+
+test('未核分母或无有效材料不编造百分比,已确认零提及保留0%', () => {
+  for (const [n, N] of [[null, null], [4, null], [null, 10], [0, 0]]) {
+    const t = task();t.report.chapters[0].sections[0].data.rows[0].splice(1, 2, n, N);
+    assert.equal(statisticsRows(t.report.chapters[0].sections[0])[0].percent, null);
+    assert.doesNotMatch(renderReportHtml(t), /role="img" aria-label="本样本内占比|NaN%|Infinity%/);
+  }
+  const s = section();s.data.rows[0][1] = 0;assert.equal(statisticsRows(s)[0].percent, '0.0%');
+});
+
+test('非法数值、口径缺失、错列和不存在的证据键在保存前拒绝', async () => {
+  for (const [n, N] of [[11, 10], [-1, 10], [4.5, 10], ['4', 10], [1, 0], [1, -1]]) {
+    const s = section();s.data.rows[0].splice(1, 2, n, N);assert.throws(() => statisticsRows(s), { code: 'REPORT_STATISTICS_INVALID' });
+  }
+  const s = section();s.data.rows[0][3] = '';assert.throws(() => statisticsRows(s), { code: 'REPORT_STATISTICS_INVALID' });
+  const t = task();t.report.evidence = {};let called = false;
+  assert.throws(() => renderReportHtml(t), { code: 'REPORT_STATISTICS_INVALID' });
+  await assert.rejects(saveAndVerifyReport(t, { requestId: 'test', invoke: async () => { called = true; } }), { code: 'REPORT_STATISTICS_INVALID' });
+  assert.equal(called, false);
+  const wrong = section();wrong.data.cols.reverse();assert.throws(() => statisticsRows(wrong), { code: 'REPORT_STATISTICS_INVALID' });
+  assert.equal(statisticsRows({ kind: 'table', data: { rows: [] } }), null, '普通表不受此语义模板限制');
+});
+
+test('多标签主题可重叠,不要求各主题相加等于100%,原始数量不被修改', () => {
+  const s = section();s.data.rows[0][1] = 8;s.data.rows.push(['主题B', 7, 10, '同10条合成测试记录;多标签重叠', '不能相加为总体比例', '比较场景差异', '']);
+  const original = structuredClone(s), rows = statisticsRows(s);
+  assert.deepEqual(rows.map(r => r.percent), ['80.0%', '70.0%']);assert.deepEqual(s, original);
+});
+
+test('看板排版薄空格不改变统计识别,浏览器与HTML的统计校验保持同步', () => {
+  const s = section();s.data.cols = s.data.cols.map(value => value.replaceAll(' ', '\u2009'));
+  const original = structuredClone(s);
+  assert.equal(statisticsRows(s)[0].percent, '40.0%');
+  assert.deepEqual(boardStatisticsRows(s), statisticsRows(s));
+  assert.deepEqual(s, original, '仅对列名匹配消除排版空白,不改写数据');
+  s.data.rows[0][1] = 11;
+  for (const parse of [statisticsRows, boardStatisticsRows]) assert.throws(() => parse(s), { code: 'REPORT_STATISTICS_INVALID' });
+  assert.equal(fs.readFileSync(new URL('../src/core/report-statistics.js', import.meta.url), 'utf8'),
+    '// Generated by scripts/build-workshop-skill.mjs; maintain the Skill report-statistics.mjs source.\n' + fs.readFileSync(new URL('../skills/ksf-workshop-runtime/scripts/report-statistics.mjs', import.meta.url), 'utf8'));
+});
+
+test('统计表完整保留在默认HTML附件与同版report,生成器不从精选引文数量推断分子', t => {
+  const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-statistics-test-'));
+  t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+  const original = task(), prepared = prepareHtmlReport(original, { directory, dataMode: 'sample' });
+  assert.deepEqual(prepared.task.report.chapters, original.report.chapters);
+  const html = fs.readFileSync(prepared.htmlPath, 'utf8');assert.match(html, /40.0%/);
+  assert.equal(original.report.evidence.e.quotes.length, 1);assert.equal(statisticsRows(original.report.chapters[0].sections[0])[0].numerator, 4);
+  validateReportStatistics(original.report);
+});

+ 44 - 0
test/workshop-report-parity.test.js

@@ -16,6 +16,8 @@ import { createFileAdapter } from '../server/src/files.mjs';
 import { CORE_DELIVERABLES, DELIVERY_KINDS, DELIVERY_SCHEMA, reportCategoryOf } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs';
 import { saveAndVerifyReport, prepareHtmlReport } from '../skills/ksf-workshop-runtime/scripts/save-report.mjs';
 import { reportAttachments } from '../src/core/report-attachments.js';
+import { STATISTICS_COLUMNS, statisticsRows } from '../skills/ksf-workshop-runtime/scripts/report-statistics.mjs';
+import { COURSE_ASSESSMENT_COLUMNS, courseAssessmentRows } from '../skills/ksf-workshop-runtime/scripts/course-assessment.mjs';
 
 // Isolated storage: these tests never connect to the production service.
 const data = JSON.parse(fs.readFileSync(new URL('../server/references/runtime-data.json', import.meta.url)));
@@ -193,6 +195,48 @@ test('默认HTML生成后走实际保存、编号回读、公开投影和文件
   assert.equal((await f.store.list('KfArtifact')).length, CORE_DELIVERABLES.length);
 });
 
+test('全部相关资料去向、正反证统计与代表原话随HTML及正文保存、回读和公开展示,分子不由引文卡数代替', async t => {
+  const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-statistics-parity-'));
+  t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+  const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId, 'social');
+  const corpus = [...Array(10)].map((_, i) => ({ id: 'test-' + i, disposition: 'included', stance: i < 4 ? 'support' : i < 6 ? 'oppose' : 'unknown', text: '合成原话' + i }));
+  corpus.push({ id: 'test-10', disposition: 'excluded-irrelevant' }, { id: 'test-11', disposition: 'excluded-duplicate' }, { id: 'test-12', disposition: 'pending-body' });
+  const source = await f.store.create('KfSource', { title: '隔离合成测试资料', rawText: JSON.stringify(corpus), dataMode: 'sample', meta: { ownerId: actor.id } });
+  task.sourceIds = [source.objectId];task.report.evidence = { e: { sourceId: source.objectId, claim: '合成测试,原话不等于全量分子', quotes: [{ text: '合成原话0', ref: 'https://example.org/test' }] } };
+  const included = corpus.filter(r => r.disposition === 'included');
+  const summary = { id: 'statistics-summary', kind: 'table', data: { cols: [...STATISTICS_COLUMNS], rows: ['support', 'oppose', 'unknown'].map(stance => [stance,
+    included.filter(r => r.stance === stance).length, included.length, '合成测试|13条候选:10纳入、2排除、1待核;非真实消费者资料', '全体已判定记录统计;相反材料也保留', '仅测试数据流,不形成实际研发建议', 'e']) } };
+  task.report.chapters[0].sections.push({ kind: 'table', title: '全部相关资料处理去向', data: { cols: ['记录', '去向', '判定'], rows: corpus.map(r => [r.id, r.disposition, r.stance || '未判定']) } }, summary);
+  task.rawText = JSON.stringify({ corpus, summary, explanation: '隔离测试;精选一条引文不代表只分析了一条资料' });
+  const { task: prepared, htmlPath } = prepareHtmlReport(task, { directory, dataMode: 'sample' });
+  const saved = await saveAndVerifyReport(prepared, verifier(f, { requestId: 'statistics-saved' }));
+  const db = buildModel(await f.publicData()), actual = db.artifactById[saved.artifactId];
+  assert.deepEqual(actual.report.chapters, task.report.chapters);
+  assert.deepEqual(statisticsRows(actual.report.chapters[0].sections.at(-1)).map(row => row.percent), ['40.0%', '20.0%', '40.0%']);
+  const file = await f.files.readFile({ query: Object.fromEntries(new URL(saved.htmlFiles[0].url).searchParams) }, null);
+  assert.ok(file.bytes.equals(fs.readFileSync(htmlPath)));assert.match(file.bytes.toString(), /2排除、1待核/);
+  assert.equal(actual.report.evidence.e.quotes.length, 1);assert.equal(saved.filesVerified, 1);
+  assert.equal(statisticsRows(summary)[0].numerator, 4);
+});
+
+test('课程三维与未知项以现有报告表保存,公开数据及HTML字节回读一致,无新增数据库字段', async t => {
+  const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ksf-course-parity-'));
+  t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
+  const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId, 'validation');
+  task.taskKey = 'parity-topic:D2-05:priority-list';
+  task.report.chapters[0].sections.push({ kind: 'table', id: 'course-priority', data: {
+    cols: [...COURSE_ASSESSMENT_COLUMNS], rows: [['测试A', 2, 3, 2, '隔离测试依据,商业价值需本组复核', '第1'],
+      ['测试B', 2, null, 2, '测试工艺未知,另表列出补证方式', '先补证']],
+  } });
+  const prepared = prepareHtmlReport(task, { directory, dataMode: 'sample' });
+  const saved = await saveAndVerifyReport(prepared.task, verifier(f, { requestId: 'course-score-saved' }));
+  const actual = buildModel(await f.publicData()).artifactById[saved.artifactId];
+  assert.deepEqual(courseAssessmentRows(actual.report.chapters[0].sections.at(-1)).map(row => row.scores), [[2, 3, 2], [2, null, 2]]);
+  const file = await f.files.readFile({ query: Object.fromEntries(new URL(saved.htmlFiles[0].url).searchParams) }, null);
+  assert.ok(file.bytes.equals(fs.readFileSync(prepared.htmlPath)));assert.match(file.bytes.toString(), /data-course-assessment/);
+  assert.equal(saved.publicDataVerified, true);assert.equal(saved.employeeConfirmation, 'pending');
+});
+
 test('HTML与图片随报告原子保存、文件字节回读、公开预览关联;直接匿名取私有文件被拒', async () => {
   const f = fixture(), { sessionId } = await f.start(0), task = deliveryTask(sessionId, 'proposal');
   task.report.attachments = [inlineFile('最终提案.html', 'text/html', Buffer.from('<!doctype html><meta charset="utf-8"><h1>隔离测试提案</h1>')),

+ 65 - 0
test/workshop-v3-outputs.test.js

@@ -0,0 +1,65 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { COURSE_DELIVERABLES, courseModuleOf } from '../skills/ksf-workshop-runtime/scripts/course-deliverables.mjs';
+import { DELIVERY_SCHEMA } from '../skills/ksf-workshop-runtime/scripts/report-contract.mjs';
+import { workshopReview } from '../src/core/workshop-outputs.js';
+import { REPORT_TYPES } from '../src/core/library/types.js';
+
+const chapter = text => ({ id: 'analysis', title: '实际正文', sections: [{ kind: 'para', text }] });
+const row = (moduleId, overrides = {}) => ({ id: moduleId, type: 'assessment_report', memberId: 'employee-a',
+  workshopKey: 'topic-a', dataMode: 'live', version: 1, reportSeriesKey: `topic-a:${moduleId}:report`,
+  report: { chapters: [chapter('该环节已保存的实质内容')] }, ...overrides });
+const view = artifacts => workshopReview({ artifacts, members: [{ id: 'employee-a', name: '甲' }, { id: 'employee-b', name: '乙' }] },
+  { groups: [], assignments: {} }, { mode: 'all' });
+const topics = result => result.hierarchy.flatMap(group => group.members.flatMap(member => member.topics));
+
+test('13 个环节覆盖完整课程,11 个阅读分类和七类内容索引不能代替课程清单', () => {
+  assert.deepEqual(COURSE_DELIVERABLES.map(output => output.moduleId), [
+    'D1-01', 'D1-02', 'D1-03', 'D1-04', 'D1-N', 'D2-01', 'D2-02', 'D2-03', 'D2-04', 'D2-05', 'D2-06', 'D2-07', 'D2-08',
+  ]);
+  assert.equal(REPORT_TYPES.length, 11);
+  const topic = topics(view(COURSE_DELIVERABLES.map(output => row(output.moduleId))))[0];
+  assert.equal(topic.courseOutputs.length, 13);
+  assert.equal(topic.courseOutputs.filter(output => output.state === 'has-body').length, 13);
+  assert.equal(topic.outputs.length, 7, '旧内容索引保留兼容,不影响13项环节导航');
+});
+
+test('同一环节多报告或附件不增加环节份数,配置卡、任务卡、验收与指标各有位置', () => {
+  const artifacts = ['D1-02', 'D1-04', 'D2-01', 'D2-03', 'D2-04'].map(moduleId => row(moduleId));
+  artifacts.push(row('D2-04', { id: 'card-two', reportSeriesKey: 'topic-a:D2-04:card-two',
+    report: { chapters: [chapter('第二张候选卡')], attachments: [{ name: 'report.html' }] } }));
+  const ledger = topics(view(artifacts))[0].courseOutputs;
+  assert.equal(ledger.filter(output => output.state === 'has-body').length, 5);
+  assert.equal(ledger.find(output => output.moduleId === 'D2-04').reports.length, 2);
+});
+
+test('旧系列接受冒号或斜线,标题猜测、非法交付标记和互相矛盾的环节不计数', () => {
+  assert.equal(courseModuleOf(row('D1-01', { reportSeriesKey: 'topic-a/D1-01' })), 'D1-01');
+  assert.equal(courseModuleOf(row('D1-01', { reportSeriesKey: '', title: 'D1-01 品类全景图谱' })), '');
+  const delivery = { schema: DELIVERY_SCHEMA, moduleId: 'D1-02', kind: 'method', outputs: [], contentStatus: 'ready' };
+  assert.equal(courseModuleOf(row('D1-01', { report: { delivery, chapters: [chapter('冲突内容')] } })), '');
+  assert.equal(courseModuleOf(row('D1-02', { report: { delivery: { ...delivery, schema: 'invalid' }, chapters: [chapter('正文')] } })), '');
+  const topic = topics(view([row('D1-01', { reportSeriesKey: '', title: 'D1-01 品类全景图谱' })]))[0];
+  assert.equal(topic.courseUnlocatedCount, 1);
+  assert.equal(topic.courseOutputs.every(output => output.state === 'missing'), true);
+});
+
+test('仅原文、草稿、空章节、未保存分别显示,有正文不被称为质量通过或员工确认', () => {
+  const artifacts = [row('D1-01', { report: null, rawText: '只有旧原文' }),
+    row('D1-02', { report: { delivery: { schema: DELIVERY_SCHEMA, moduleId: 'D1-02', kind: 'method', contentStatus: 'draft', outputs: [] }, chapters: [chapter('配置草稿')] } }),
+    row('D1-03', { report: { chapters: [chapter('')] } })];
+  const ledger = topics(view(artifacts))[0].courseOutputs;
+  assert.deepEqual(ledger.slice(0, 4).map(output => output.state), ['text-only', 'draft', 'missing-body', 'missing']);
+  assert.equal(ledger.some(output => output.state === 'completed' || output.state === 'accepted'), false);
+});
+
+test('最新版缺正文不能借用历史版补齐,作者、课题与模式的报告不能互相补足', () => {
+  const artifacts = [row('D1-01'), row('D1-01', { id: 'v2', version: 2, report: { chapters: [chapter('')] } }),
+    row('D1-02', { memberId: 'employee-b' }), row('D1-03', { workshopKey: 'topic-b' }), row('D1-04', { dataMode: 'sample' })];
+  const result = view(artifacts);
+  const employee = result.hierarchy.flatMap(group => group.members).find(member => member.id === 'employee-a');
+  const current = employee.topics.find(topic => topic.workshopKey === 'topic-a' && topic.mode === 'live');
+  assert.deepEqual(current.courseOutputs.slice(0, 4).map(output => output.state), ['missing-body', 'missing', 'missing', 'missing']);
+  assert.equal(current.courseOutputs[0].reports.length, 1);
+  assert.equal(current.courseOutputs[0].reports[0].id, 'v2');
+});