Просмотр исходного кода

fix: gate board deployments on JavaScript MIME support

pengzhikang 3 часов назад
Родитель
Сommit
e05985effd

+ 6 - 2
docs/board-local-release-20261111/DEPLOY.md

@@ -21,7 +21,9 @@ export KSF_DEPLOY_AUTH_FILE=/服务器现有私有应用配置的实际路径
 bash scripts/deploy-board-local.sh /opt/kangshifu-web
 bash scripts/deploy-board-local.sh /opt/kangshifu-web
 ```
 ```
 
 
-此命令按序执行部署器测试、分组接口plan、前端dry、分组接口apply、前端apply、两端no-op、外网117文件SHA和模块依赖检查。回执保存到/opt/ksf-board-local-20261111-results-*。备份路径在frontend-apply.json和member-apply.json中。
+此命令先执行.mjs MIME只读预检,再按序执行部署器测试、分组接口plan、前端dry、分组接口apply、前端apply、两端no-op、外网117文件SHA、JavaScript响应类型和模块依赖检查。回执保存到/opt/ksf-board-local-20261111-results-*。备份路径在frontend-apply.json和member-apply.json中。
+
+MIME预检位于所有发布写入之前:检查/etc/nginx/mime.types中的JavaScript映射,并核对目标目录内已部署.mjs文件的公网响应类型与内容。新文件尚未部署时明确标记延后公网核验,发布后的全文件和模块检查继续负责验证。缺少映射、错误响应类型或响应内容不符均停止。预检不自动修改或重载nginx;仅在实际静态服务.mjs且缺配置的站点,由运维备份配置、补充映射、nginx -t通过后reload。
 
 
 不运行旧scripts/deploy.sh prod:它面向历史OBS子路径,当前站点是根路径。不要覆盖首页,不修改nginx或数据库配置,不重启研究服务,不重装数字人Skill。
 不运行旧scripts/deploy.sh prod:它面向历史OBS子路径,当前站点是根路径。不要覆盖首页,不修改nginx或数据库配置,不重启研究服务,不重装数字人Skill。
 
 
@@ -40,4 +42,6 @@ bash scripts/deploy-board-local.sh /opt/kangshifu-web
 
 
 ## 本地验证
 ## 本地验证
 
 
-本机215项前端测试、82项服务端测试、4项部署测试通过;干净检出后的正式前端203项测试通过(本机额外包含已忽略的旧演示测试)。静态检查和115个运行模块依赖通过。报告入口28项浏览器检查通过;此前登录和成员名册等核验材料已随各自任务档案提交。生产部署和生产浏览器验收等待运维回传。
+本机215项前端测试、82项服务端测试、4项部署测试通过;干净检出后的正式前端203项测试通过(本机额外包含已忽略的旧演示测试)。静态检查和115个运行模块依赖通过。报告入口28项浏览器检查通过;此前登录和成员名册等核验材料已随各自任务档案提交。
+
+运维现已回传固定提交11ba2290部署及桌面/390/320浏览器通过,用户已确认截图。生产曾因nginx未映射.mjs而白屏,运维备份后补充映射并reload,幂等复验通过。独立公开回读117文件及5项接口检查通过。生产结果与后续MIME预检固化分别记录于production-status.json;已上线的本版不必为归档再部署。原运维补丁仍为服务器原件,本仓库预检依据相同故障独立实现和测试,不声称两者逐字相同。

+ 17 - 0
docs/board-local-release-20261111/mime-preflight-tests.txt

@@ -0,0 +1,17 @@
+✔ 完整前端发布预检只读,apply可重复且不替换首页 (1148.7812ms)
+✔ 未知现场拒绝,写入失败恢复,新静态文件保持0644 (881.1958ms)
+✔ 成员接口未知版本在写入前停止 (0.6324ms)
+✔ 成员接口已发布版本只读和apply均不重复写入 (0.3047ms)
+✔ 有效配置与真实模块响应均通过,预检保持只读 (34.1998ms)
+✔ 旧配置缺少mjs映射时,在请求前停止,注释不能冒充映射 (80.5207ms)
+✔ 配置正确但响应为octet-stream时拒绝,复现白屏原因 (6.1173ms)
+✔ SPA返回HTML或内容不符不能因200和JavaScript响应头而通过 (5.6561ms)
+✔ 首次发布尚不存在的模块明确留待发布后验证 (6.2961ms)
+ℹ tests 9
+ℹ suites 0
+ℹ pass 9
+ℹ fail 0
+ℹ cancelled 0
+ℹ skipped 0
+ℹ todo 0
+ℹ duration_ms 2133.1961

+ 164 - 0
docs/board-local-release-20261111/production-modules-verification.json

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

+ 1103 - 0
docs/board-local-release-20261111/production-public-verification.json

@@ -0,0 +1,1103 @@
+{
+  "ok": true,
+  "checkedAt": "2026-11-11T13:57:29.871Z",
+  "modules": [
+    {
+      "path": "lib/vendor/lit/v3.3.3/directives/ref.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6c0296f91a07e1bc856787ed019b4ef593c6dbc0d4cb2b2733b89cf12e085f09",
+      "normalizedSha256": "6c0296f91a07e1bc856787ed019b4ef593c6dbc0d4cb2b2733b89cf12e085f09",
+      "matches": true
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/directives/keyed.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b80a7e0d76893fc144d716031288944a44b77209c8e83f0cf80bbdaf60d4c0bb",
+      "normalizedSha256": "b80a7e0d76893fc144d716031288944a44b77209c8e83f0cf80bbdaf60d4c0bb",
+      "matches": true
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/directives/unsafe-html.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d38529a40a45d85efd03bff58256e92f9e77b3c3b5b3d70dea273b856b27dd79",
+      "normalizedSha256": "d38529a40a45d85efd03bff58256e92f9e77b3c3b5b3d70dea273b856b27dd79",
+      "matches": true
+    },
+    {
+      "path": "lib/vendor/lit-html/v3.3.3/lit-html.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b878e7f95dec8a9b6e9b217faca6b6a11bad82ebc44c0caa77419ed83edd81f2",
+      "normalizedSha256": "b878e7f95dec8a9b6e9b217faca6b6a11bad82ebc44c0caa77419ed83edd81f2",
+      "matches": true
+    },
+    {
+      "path": "lib/vendor/@lit/reactive-element/v2.1.2/reactive-element.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "76e9815ec67d16684bff1444224bf62532d6318dbd8f25e7c1e9546158b1335f",
+      "normalizedSha256": "76e9815ec67d16684bff1444224bf62532d6318dbd8f25e7c1e9546158b1335f",
+      "matches": true
+    },
+    {
+      "path": "lib/vendor/@lit-labs/signals/v0.3.0/index.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "e3ffab45e9ed2d4b9e94532c4abf0ac670eab9b65993dd135e43c0939e835d0a",
+      "normalizedSha256": "e3ffab45e9ed2d4b9e94532c4abf0ac670eab9b65993dd135e43c0939e835d0a",
+      "matches": true
+    },
+    {
+      "path": "skills/ksf-workshop-runtime/scripts/report-contract.mjs",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "4c6e05ca3f6e65d57d55f743de39307181f8427626e355a5d2698905c17dd8c4",
+      "normalizedSha256": "4c6e05ca3f6e65d57d55f743de39307181f8427626e355a5d2698905c17dd8c4",
+      "matches": true
+    },
+    {
+      "path": "src/app.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "578670a3df6ce7b1690812754243af4b7ef5ed5a467e1c0bf3f3da65eff6d1cf",
+      "normalizedSha256": "578670a3df6ce7b1690812754243af4b7ef5ed5a467e1c0bf3f3da65eff6d1cf",
+      "matches": true
+    },
+    {
+      "path": "src/components/base.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "a4bcd43fb5506a9245bdc6b1d371aa742e2b71627cfc1119ecb97c5aceb7349d",
+      "normalizedSha256": "a4bcd43fb5506a9245bdc6b1d371aa742e2b71627cfc1119ecb97c5aceb7349d",
+      "matches": true
+    },
+    {
+      "path": "src/components/cjk.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "a0686e11325493394016823e813e9c48acd2c9e6735714c6f063dcf1ba05cd0f",
+      "normalizedSha256": "a0686e11325493394016823e813e9c48acd2c9e6735714c6f063dcf1ba05cd0f",
+      "matches": true
+    },
+    {
+      "path": "src/components/index.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "8af3381a0fa90d08bfc9883dbb6bc6246cfee1e3b68d2fc2377dedbac6bb9ae0",
+      "normalizedSha256": "8af3381a0fa90d08bfc9883dbb6bc6246cfee1e3b68d2fc2377dedbac6bb9ae0",
+      "matches": true
+    },
+    {
+      "path": "src/components/icons.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "938ebc684cf044eb78921ab696a85cfaec32d7ec67166f578c889d918e9245b4",
+      "normalizedSha256": "0d23722cf2cf494f5666bf56835094c1b94a58f0ca00888495dc6834f7bb9c3c",
+      "matches": true
+    },
+    {
+      "path": "src/assets/inter-latin.woff2",
+      "status": 200,
+      "contentType": "font/woff2",
+      "mimeMatches": true,
+      "sha256": "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62",
+      "normalizedSha256": "3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-badge.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "e3ceae48f866067de873233d094e50bfc7707b7b6bb48cba63006293f1a910ae",
+      "normalizedSha256": "e3ceae48f866067de873233d094e50bfc7707b7b6bb48cba63006293f1a910ae",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-drawer.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "8278e89ae2aae7820c74b6419e8b89e046e9aae15d520a4a90949933eefbe7d0",
+      "normalizedSha256": "8278e89ae2aae7820c74b6419e8b89e046e9aae15d520a4a90949933eefbe7d0",
+      "matches": true
+    },
+    {
+      "path": "src/app.config.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6383371210f0402ff6deb325dd4caf28881017dd9bbe7b79313c7996484e3c5c",
+      "normalizedSha256": "6383371210f0402ff6deb325dd4caf28881017dd9bbe7b79313c7996484e3c5c",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-empty.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "056c981dbeab6f716fe73b36deb533a8e9a5245103b167084ecca6df825dc94f",
+      "normalizedSha256": "1160e036696da5779c687ba5668090cc198720d91634e819bbbace9964be8bd8",
+      "matches": true
+    },
+    {
+      "path": "lib/vendor/lit/v3.3.3/index.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b1993a57ee9b162bc5af6b2f4bc44623c0bd3496be5119c83d5325b04093b65c",
+      "normalizedSha256": "b1993a57ee9b162bc5af6b2f4bc44623c0bd3496be5119c83d5325b04093b65c",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-pipeline.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "257c02bfec8aee4d538f1e56f017ae133115bd76af6dc95c06de890ddf86ada6",
+      "normalizedSha256": "257c02bfec8aee4d538f1e56f017ae133115bd76af6dc95c06de890ddf86ada6",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-glass-card.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "f81dd2050674627281f43522cb120175fb91f0008269203fb7cf4254bce40e9a",
+      "normalizedSha256": "f81dd2050674627281f43522cb120175fb91f0008269203fb7cf4254bce40e9a",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-evidence-chain.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "bf0e05f2b7fb3efef15e04858fcf70c80ce85f65d1798719fdee9c2778006a91",
+      "normalizedSha256": "bf0e05f2b7fb3efef15e04858fcf70c80ce85f65d1798719fdee9c2778006a91",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-opportunity-card.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "29894343e14a8c946b1553cc2cb2f966bb8007e5808ad486d0e8236ed2f72271",
+      "normalizedSha256": "29894343e14a8c946b1553cc2cb2f966bb8007e5808ad486d0e8236ed2f72271",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-source-detail.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "9c61966edaf5f7df6d593a5d0724d0c24c2aab8e487b8d128692f77d218a4d00",
+      "normalizedSha256": "192b61dff91d5170c3661ef6a507500606b3e649bd16c748a1179f26083770f7",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-score-panel.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "4441830101350381e23e669941ab8e70b43e2e7902e9c5d755db9ac212c5862e",
+      "normalizedSha256": "4441830101350381e23e669941ab8e70b43e2e7902e9c5d755db9ac212c5862e",
+      "matches": true
+    },
+    {
+      "path": "src/core/brand-shop-filters.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "002bab6a57c403bdf80dc1b7c1170f2ae581f078f90da7a5e8411c094bc8cce1",
+      "normalizedSha256": "002bab6a57c403bdf80dc1b7c1170f2ae581f078f90da7a5e8411c094bc8cce1",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-viz.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "8c563ffc4eafe9c198d707bee5595e19edb32ffe595745c9e01d904b57b299b2",
+      "normalizedSha256": "f1f9de9b31f43659964ea7b69e1c7f210120b5ca936dc3416e739c709266cbed",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-stat.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b6577c0136b422771a448d518db9837f08a8b60d5ebfeddbfd7f297eb5b6e071",
+      "normalizedSha256": "b6577c0136b422771a448d518db9837f08a8b60d5ebfeddbfd7f297eb5b6e071",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-toast.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "388e021bfafb2491378f6872142f3f009292b74f6743121d590f2882bed0327d",
+      "normalizedSha256": "388e021bfafb2491378f6872142f3f009292b74f6743121d590f2882bed0327d",
+      "matches": true
+    },
+    {
+      "path": "src/components/kf-table.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "a023d34e465c419b450439e9616e26867ab54da0b97d27c226514e75d9908212",
+      "normalizedSha256": "a023d34e465c419b450439e9616e26867ab54da0b97d27c226514e75d9908212",
+      "matches": true
+    },
+    {
+      "path": "src/core/development-references.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "c8c4e1e58b8128a7009593606515682b4f49de264a550df94d350ecf797295ed",
+      "normalizedSha256": "c8c4e1e58b8128a7009593606515682b4f49de264a550df94d350ecf797295ed",
+      "matches": true
+    },
+    {
+      "path": "src/core/evidence/chain.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "3798974a75fa078cc2c6cef30651bb250917b35fa79625f90d3e89df401a9fb3",
+      "normalizedSha256": "91060709ebb32ff0969c24fb797d5f6ad7be4824d311078517effb3acbe2ee29",
+      "matches": true
+    },
+    {
+      "path": "src/core/industry-navigation.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0d10003bebec2f02c3272f3fbd50c7d43818075a741f9d499b37970b47234d3b",
+      "normalizedSha256": "0d10003bebec2f02c3272f3fbd50c7d43818075a741f9d499b37970b47234d3b",
+      "matches": true
+    },
+    {
+      "path": "src/core/industry-brief.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "62ba01751ce032db8da1192d9e481b03449895bd33246c02e59546c30afdb95c",
+      "normalizedSha256": "62ba01751ce032db8da1192d9e481b03449895bd33246c02e59546c30afdb95c",
+      "matches": true
+    },
+    {
+      "path": "src/core/industry-materials.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d41d64ec5fee22ca0fc15d15e9bea7536aa28f51afd2fb73d8e16675939c08a8",
+      "normalizedSha256": "063fb18e442ee703c10618fbb2917eca46ab0ea70bf21edb80675935f153d65c",
+      "matches": true
+    },
+    {
+      "path": "src/core/index.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "01d145691d1d87d5b7e2464f7c8585319619a23843ac665c29ec3ac505174da7",
+      "normalizedSha256": "01d145691d1d87d5b7e2464f7c8585319619a23843ac665c29ec3ac505174da7",
+      "matches": true
+    },
+    {
+      "path": "src/core/industry-planner.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6b4a426e112f2671ef90801482b729f5dc0a4b4589d0515c239d3ab8f7dc47d7",
+      "normalizedSha256": "6b4a426e112f2671ef90801482b729f5dc0a4b4589d0515c239d3ab8f7dc47d7",
+      "matches": true
+    },
+    {
+      "path": "src/core/industry-research-directions.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "be7409f6b14cb0d35987eb1fab103949c4f0add52cd30f7c19e6ece0555cc730",
+      "normalizedSha256": "be7409f6b14cb0d35987eb1fab103949c4f0add52cd30f7c19e6ece0555cc730",
+      "matches": true
+    },
+    {
+      "path": "src/core/industry-workshop.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "c648236a658d4c1dae2137d9677ec79cb254a6cddee2bbeff04818f29184c65d",
+      "normalizedSha256": "c648236a658d4c1dae2137d9677ec79cb254a6cddee2bbeff04818f29184c65d",
+      "matches": true
+    },
+    {
+      "path": "src/core/kfs/ledger.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0ab77bc4db0ad98d184285e7662857d03b90aaa6708f2b9025e16597dc2fecd7",
+      "normalizedSha256": "0ab77bc4db0ad98d184285e7662857d03b90aaa6708f2b9025e16597dc2fecd7",
+      "matches": true
+    },
+    {
+      "path": "src/core/kfs/employee.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "113fa23092992186eec9104a6c0af4bdb7b323579e43a11421ea4d6193d38b5d",
+      "normalizedSha256": "113fa23092992186eec9104a6c0af4bdb7b323579e43a11421ea4d6193d38b5d",
+      "matches": true
+    },
+    {
+      "path": "src/core/kfs/stats.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0ad541772cbacbf59302fcbfb8fb20cf04facb98ea3399bad765915e7a1f9112",
+      "normalizedSha256": "0ad541772cbacbf59302fcbfb8fb20cf04facb98ea3399bad765915e7a1f9112",
+      "matches": true
+    },
+    {
+      "path": "src/core/kfs/build.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b7c3cacc4555cbd09c4fed95bc145ee2f99b2d6294f0a4e9e87999ea83d45fae",
+      "normalizedSha256": "b7c3cacc4555cbd09c4fed95bc145ee2f99b2d6294f0a4e9e87999ea83d45fae",
+      "matches": true
+    },
+    {
+      "path": "src/core/kfs/util.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "c0e77275f414bd2198dec161cf9c4d16dd27dfd99a587b4f7cfb5e66f61dec45",
+      "normalizedSha256": "c0e77275f414bd2198dec161cf9c4d16dd27dfd99a587b4f7cfb5e66f61dec45",
+      "matches": true
+    },
+    {
+      "path": "src/core/member-groups.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6a82f26ea84e4b9b0a46aea330b10a87a1927e06532482ba8ab6d25b654d5372",
+      "normalizedSha256": "6a82f26ea84e4b9b0a46aea330b10a87a1927e06532482ba8ab6d25b654d5372",
+      "matches": true
+    },
+    {
+      "path": "src/core/library-selection.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d62cdb5a48fdbecb3e7d9854e7b6f3744d42e26c7c650249d6bd764dfebd5004",
+      "normalizedSha256": "d62cdb5a48fdbecb3e7d9854e7b6f3744d42e26c7c650249d6bd764dfebd5004",
+      "matches": true
+    },
+    {
+      "path": "src/core/library/types.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "523b9ed851aea33349e624a4dc608cc26e813f15b674b762555497ad3cdc7d48",
+      "normalizedSha256": "523b9ed851aea33349e624a4dc608cc26e813f15b674b762555497ad3cdc7d48",
+      "matches": true
+    },
+    {
+      "path": "src/core/library.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "94e0d117dec688d5dcbe836f0d0077409e8cd5fe68ae60177444a795b1642513",
+      "normalizedSha256": "94e0d117dec688d5dcbe836f0d0077409e8cd5fe68ae60177444a795b1642513",
+      "matches": true
+    },
+    {
+      "path": "src/core/library/reports.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "a600a838958d625688d24e0360339323d553c2a07adbc3b5c70f0eb81f36ece9",
+      "normalizedSha256": "a600a838958d625688d24e0360339323d553c2a07adbc3b5c70f0eb81f36ece9",
+      "matches": true
+    },
+    {
+      "path": "src/core/report-delivery.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "8ef6bb453175403e297aa7513915a9ca0b8bf032c7fee809409e0ed2ded1c719",
+      "normalizedSha256": "8ef6bb453175403e297aa7513915a9ca0b8bf032c7fee809409e0ed2ded1c719",
+      "matches": true
+    },
+    {
+      "path": "src/core/opportunity/opportunity.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "14c198eefc24883d75dd61a05cc2245c44da2f8cb8acaefade8995d7878616a8",
+      "normalizedSha256": "3dc79b829d8ebf36186d42a8429fa33f10f5debb8eed5e910626cd614e32ce2e",
+      "matches": true
+    },
+    {
+      "path": "src/core/opportunity/report.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b3747ec12dda154c80ba69f52856a97099ec6e2ba169f4d20ed3e46bbab30c8b",
+      "normalizedSha256": "b3747ec12dda154c80ba69f52856a97099ec6e2ba169f4d20ed3e46bbab30c8b",
+      "matches": true
+    },
+    {
+      "path": "src/core/opportunity/score.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6b396005cce87ce42f15c85519401a7c9ae25c5b1e3972958fbd9138c2e7bff4",
+      "normalizedSha256": "6b396005cce87ce42f15c85519401a7c9ae25c5b1e3972958fbd9138c2e7bff4",
+      "matches": true
+    },
+    {
+      "path": "src/core/report-attachments.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "ee0295e15f6ae773efd1bec774b4e30364027e33c364e187af7f24626ca845b1",
+      "normalizedSha256": "ee0295e15f6ae773efd1bec774b4e30364027e33c364e187af7f24626ca845b1",
+      "matches": true
+    },
+    {
+      "path": "src/core/report-evidence.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "42c90ec91d8625ec7022c5d996d363bb79aeae3cd126ff8c57bbf4e15a79ab03",
+      "normalizedSha256": "42c90ec91d8625ec7022c5d996d363bb79aeae3cd126ff8c57bbf4e15a79ab03",
+      "matches": true
+    },
+    {
+      "path": "src/core/taxonomy/taxonomy.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "7b00ef1858becad5a7db6c0e67080c3f599770bc09d73f8f934973b4f4dad847",
+      "normalizedSha256": "7b00ef1858becad5a7db6c0e67080c3f599770bc09d73f8f934973b4f4dad847",
+      "matches": true
+    },
+    {
+      "path": "src/core/workshop-outputs.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "2ce98321c4a1e5bdc274a2d026907680d4987bd9b1dd4304551b481162ed560c",
+      "normalizedSha256": "2ce98321c4a1e5bdc274a2d026907680d4987bd9b1dd4304551b481162ed560c",
+      "matches": true
+    },
+    {
+      "path": "src/core/report.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "2c383b3138daea0449467d695d11d1b57e6bbf100138a42d2c22ca75b5ebeac0",
+      "normalizedSha256": "2c383b3138daea0449467d695d11d1b57e6bbf100138a42d2c22ca75b5ebeac0",
+      "matches": true
+    },
+    {
+      "path": "src/core/workshop-roster.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d072119c02f9908a24f5c9420141eb90da2bb8807bd21a53e448a1b4df3cbf1a",
+      "normalizedSha256": "d072119c02f9908a24f5c9420141eb90da2bb8807bd21a53e448a1b4df3cbf1a",
+      "matches": true
+    },
+    {
+      "path": "src/data/development-references.json",
+      "status": 200,
+      "contentType": "application/json",
+      "mimeMatches": true,
+      "sha256": "315598f057ef53698e1ca38adecae17b5fa79adafc0331c587b506e31801141a",
+      "normalizedSha256": "315598f057ef53698e1ca38adecae17b5fa79adafc0331c587b506e31801141a",
+      "matches": true
+    },
+    {
+      "path": "src/layout/detail-styles.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "fc394d5aca8b57a920b7d40b16225a4039436deeb413c2434a42a79740f4b717",
+      "normalizedSha256": "d6bf229a225aa5c715d0645f6d680c6f69abbd53063023528a9d3da8c7fa461b",
+      "matches": true
+    },
+    {
+      "path": "src/services/development-service.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "ca1ae91389dc33934a85856681212b1e55a1af692076a95a4d59f2e424ea33d1",
+      "normalizedSha256": "ca1ae91389dc33934a85856681212b1e55a1af692076a95a4d59f2e424ea33d1",
+      "matches": true
+    },
+    {
+      "path": "src/layout/kf-topbar.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "c77c5375cbc0f5ebcf3a7d59854752d744da6e5fc5b2fc6b5c70ed7e651302d6",
+      "normalizedSha256": "c77c5375cbc0f5ebcf3a7d59854752d744da6e5fc5b2fc6b5c70ed7e651302d6",
+      "matches": true
+    },
+    {
+      "path": "src/services/auth-service.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "f7892cb6225dfcb37bcf21beda9e158a6420c410299236ff421650b240a5717e",
+      "normalizedSha256": "f7892cb6225dfcb37bcf21beda9e158a6420c410299236ff421650b240a5717e",
+      "matches": true
+    },
+    {
+      "path": "src/services/board-service.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "377c79fad4a036cf6a2baa9e1e819652f7384c3a9d110501cf43f4a2da3b988a",
+      "normalizedSha256": "377c79fad4a036cf6a2baa9e1e819652f7384c3a9d110501cf43f4a2da3b988a",
+      "matches": true
+    },
+    {
+      "path": "src/services/industry-service.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "3f84a579f3b1eacdea34453a15b8548b627cc35fd0c8c39e8be9fda71f1330f4",
+      "normalizedSha256": "3f84a579f3b1eacdea34453a15b8548b627cc35fd0c8c39e8be9fda71f1330f4",
+      "matches": true
+    },
+    {
+      "path": "src/services/library-images.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "48a3fd58bde0f881eb1efbeaec4afd62903b05e15de38a2d6516185cc32a2d5c",
+      "normalizedSha256": "48a3fd58bde0f881eb1efbeaec4afd62903b05e15de38a2d6516185cc32a2d5c",
+      "matches": true
+    },
+    {
+      "path": "src/services/library-service.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "7b404a347664cc9c50a473b53f0c2bef675437b120b8b76f2a0e8e50eff5c695",
+      "normalizedSha256": "7b404a347664cc9c50a473b53f0c2bef675437b120b8b76f2a0e8e50eff5c695",
+      "matches": true
+    },
+    {
+      "path": "src/services/member-groups.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "02a9c39d9f3d90ad47df06619f31181d1661956c9f46e0d46b8f9b4b3693df28",
+      "normalizedSha256": "f9e62e05ba51aea92d289bb6127fbae8fcc76541b8898d7c6517702ee1035d1a",
+      "matches": true
+    },
+    {
+      "path": "src/services/library-pages.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "fc3c3e494f162b0a60f429d2ccf3a1119e8898caad0745ce323fb5a86ecd9a0b",
+      "normalizedSha256": "fc3c3e494f162b0a60f429d2ccf3a1119e8898caad0745ce323fb5a86ecd9a0b",
+      "matches": true
+    },
+    {
+      "path": "src/services/platform-auth.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "a84ef01a1dca0dc8f7003a41dbc9569f59a6969392c0ecb58508e7f6ce16c18d",
+      "normalizedSha256": "a84ef01a1dca0dc8f7003a41dbc9569f59a6969392c0ecb58508e7f6ce16c18d",
+      "matches": true
+    },
+    {
+      "path": "src/services/workshop-data.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "cae7e9065a37d395429d5f1bd254b2201ad4b0218c6222349029455dc96a7c65",
+      "normalizedSha256": "cae7e9065a37d395429d5f1bd254b2201ad4b0218c6222349029455dc96a7c65",
+      "matches": true
+    },
+    {
+      "path": "src/styles/filter-controls.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d56fea6e56ca06df415d107ad7c5e253a7e650f115377c821c9927bae41ea633",
+      "normalizedSha256": "d56fea6e56ca06df415d107ad7c5e253a7e650f115377c821c9927bae41ea633",
+      "matches": true
+    },
+    {
+      "path": "src/utils/card-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "beec0ad6768dc44d0a73b091ca59241d5e6042b7598310df6ef69a34478f2a1a",
+      "normalizedSha256": "7bcda8fe488b793c78fce5611bcd1e500554668ab762254aaef7c2016b1c4f4f",
+      "matches": true
+    },
+    {
+      "path": "src/utils/http.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "abbfdf63f13d11c86a771a2a13cb5ecafb33f3f5d50a433dfaafd48259390d77",
+      "normalizedSha256": "abbfdf63f13d11c86a771a2a13cb5ecafb33f3f5d50a433dfaafd48259390d77",
+      "matches": true
+    },
+    {
+      "path": "src/utils/cjk-deep.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "9d973b2142e06a1c3c3320f6105d0efe6d07f8efc6c3ca96cbe1978957f302b9",
+      "normalizedSha256": "9d973b2142e06a1c3c3320f6105d0efe6d07f8efc6c3ca96cbe1978957f302b9",
+      "matches": true
+    },
+    {
+      "path": "src/utils/router.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "5f4d85e7beea3f7238e6836c8efdec18f8d42af124e58d8917b6d318a8b95934",
+      "normalizedSha256": "5f4d85e7beea3f7238e6836c8efdec18f8d42af124e58d8917b6d318a8b95934",
+      "matches": true
+    },
+    {
+      "path": "src/styles/tokens.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "692243307e126c3367ec9f3a9af44f651d710dd1c961a0a8bdb0ff8c3ace4a52",
+      "normalizedSha256": "692243307e126c3367ec9f3a9af44f651d710dd1c961a0a8bdb0ff8c3ace4a52",
+      "matches": true
+    },
+    {
+      "path": "src/utils/storage.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b614463cdeed015327a7851eac6bd43bf95db14c16fe13f094e8af5cec545026",
+      "normalizedSha256": "b614463cdeed015327a7851eac6bd43bf95db14c16fe13f094e8af5cec545026",
+      "matches": true
+    },
+    {
+      "path": "src/utils/type.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "5bc6a7bf164b38961ada06ca9c4c1b334b4679d9e227fadee0b80b8308bae165",
+      "normalizedSha256": "5bc6a7bf164b38961ada06ca9c4c1b334b4679d9e227fadee0b80b8308bae165",
+      "matches": true
+    },
+    {
+      "path": "src/pages/card-toolbar.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0e77d4540f38339fb6c82d0ff6184de257bfb0a1e4e61506c1c91b045e3577db",
+      "normalizedSha256": "0e77d4540f38339fb6c82d0ff6184de257bfb0a1e4e61506c1c91b045e3577db",
+      "matches": true
+    },
+    {
+      "path": "src/pages/filter-sheet.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "4f62212bd8646096e80be9d00da7d098f1937a1176be6ccb37b721bbd9c5e556",
+      "normalizedSha256": "4f62212bd8646096e80be9d00da7d098f1937a1176be6ccb37b721bbd9c5e556",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-dashboard-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "cabcd32387a66f0c05cf9a1c27f3ae402757a842942dc66ef49d251c4c9c197b",
+      "normalizedSha256": "cabcd32387a66f0c05cf9a1c27f3ae402757a842942dc66ef49d251c4c9c197b",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-detail-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0cc23045442f8e40ee2732c1553086e0543715487e050158e9585e10a5cf6309",
+      "normalizedSha256": "0cc23045442f8e40ee2732c1553086e0543715487e050158e9585e10a5cf6309",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-decision-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "7dd15c3aa5e5401f33090ae95e4a02debe2575c67eb64bee4252e72241e8fc6b",
+      "normalizedSha256": "7dd15c3aa5e5401f33090ae95e4a02debe2575c67eb64bee4252e72241e8fc6b",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-pipeline-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "1571abd1063b53c7490887fcbdb5f781719b0a1f009a4d5f0dc6ce4fe6768426",
+      "normalizedSha256": "000f5ab6cb392ffc663c1696fa45c5835ea4e5261991f9fd0ebd4cf918dc0a50",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-innovation-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d120407bf4370a73155644ef06a61d337e301dc1bb356387feff6207cb896ebe",
+      "normalizedSha256": "d120407bf4370a73155644ef06a61d337e301dc1bb356387feff6207cb896ebe",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-development-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "8c99dd038109ebf0e63a82b7cb0ae0afdb4894822668a34c6dddabef51c35e85",
+      "normalizedSha256": "8c99dd038109ebf0e63a82b7cb0ae0afdb4894822668a34c6dddabef51c35e85",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-planner-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "c2adf992ef5d92d58063c61e4ba55155f17ec74c10351dedbcd325fe7ac3fc77",
+      "normalizedSha256": "c2adf992ef5d92d58063c61e4ba55155f17ec74c10351dedbcd325fe7ac3fc77",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-policy-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "911367be02770172f88b5cfba9c6cbe5258fd77d29465a5d873e5a253cb26f1a",
+      "normalizedSha256": "911367be02770172f88b5cfba9c6cbe5258fd77d29465a5d873e5a253cb26f1a",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-ranking-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "364fa8a767669607580669387e8eb9c54e6d31080e15d4d29cee577209240212",
+      "normalizedSha256": "364fa8a767669607580669387e8eb9c54e6d31080e15d4d29cee577209240212",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-research-direction-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "dfd6ffaada580392a0b6a45b1b13535b348b5747569e594bfb8418e8c0233973",
+      "normalizedSha256": "dfd6ffaada580392a0b6a45b1b13535b348b5747569e594bfb8418e8c0233973",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-scope-filters.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "075a15c52f917daefc2bb5eab3eab1d1db7eefcabe5c97294353dd268edfa67c",
+      "normalizedSha256": "075a15c52f917daefc2bb5eab3eab1d1db7eefcabe5c97294353dd268edfa67c",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-workshop-view.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "f9f4416a438eb4b5b7a0680f4663802b9c26ee885a9c0bda08c712360124673d",
+      "normalizedSha256": "f9f4416a438eb4b5b7a0680f4663802b9c26ee885a9c0bda08c712360124673d",
+      "matches": true
+    },
+    {
+      "path": "src/pages/industry-research-views.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "a40180e68186737ba7a09b4e4449b243a5c88c0d77448ee580e5987800d81415",
+      "normalizedSha256": "a40180e68186737ba7a09b4e4449b243a5c88c0d77448ee580e5987800d81415",
+      "matches": true
+    },
+    {
+      "path": "src/pages/library-product-detail.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6f2f7e571a818384602e0b8c0ab0a41d8defa4831c6b7a32d730ebc1c686c532",
+      "normalizedSha256": "6f2f7e571a818384602e0b8c0ab0a41d8defa4831c6b7a32d730ebc1c686c532",
+      "matches": true
+    },
+    {
+      "path": "src/pages/library-product-image.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "735abb1e00b267b03d50336632f6fcec5ed742cf26331d8f2a00dbb6d7036da9",
+      "normalizedSha256": "735abb1e00b267b03d50336632f6fcec5ed742cf26331d8f2a00dbb6d7036da9",
+      "matches": true
+    },
+    {
+      "path": "src/pages/mobile-research-styles.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "30b9fd469e20d863ca4f76ae791c9803fcd49918764a180f47ec8089536014b1",
+      "normalizedSha256": "30b9fd469e20d863ca4f76ae791c9803fcd49918764a180f47ec8089536014b1",
+      "matches": true
+    },
+    {
+      "path": "src/pages/policy-reviewed-evidence.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "f56b2b26cb1565656250517d803156b193312d5ef7ef5bfac277bd675e7a8d42",
+      "normalizedSha256": "f56b2b26cb1565656250517d803156b193312d5ef7ef5bfac277bd675e7a8d42",
+      "matches": true
+    },
+    {
+      "path": "src/pages/library-styles.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "ab8bffb73b87ac93d20ccbd57408710ca4c06b524f0ce736e9d891abaf05055a",
+      "normalizedSha256": "ab8bffb73b87ac93d20ccbd57408710ca4c06b524f0ce736e9d891abaf05055a",
+      "matches": true
+    },
+    {
+      "path": "src/pages/report-card.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "1dfad2b17919da378a75a5345e513bfa0c3fcc5124493327bfa091e219311106",
+      "normalizedSha256": "1dfad2b17919da378a75a5345e513bfa0c3fcc5124493327bfa091e219311106",
+      "matches": true
+    },
+    {
+      "path": "src/pages/report-pager.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "b3933fbbd70364622d73a452118736fdeaa32d935227697e623b3a0e057b2fe9",
+      "normalizedSha256": "a0353d0ce2b8ba7a926d728e933947bd34f06c77dee63329d948803865d41223",
+      "matches": true
+    },
+    {
+      "path": "src/pages/shared.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "92f0deb2f4128bfc0cecaa68ee5f81c7818e5218e97127d1cc1770845ec7db55",
+      "normalizedSha256": "92f0deb2f4128bfc0cecaa68ee5f81c7818e5218e97127d1cc1770845ec7db55",
+      "matches": true
+    },
+    {
+      "path": "src/pages/report-styles.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "d4c4b5422e23c294de85421d9eb4822365f56e542f77f1548574a7485f5cc839",
+      "normalizedSha256": "d4c4b5422e23c294de85421d9eb4822365f56e542f77f1548574a7485f5cc839",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-ledger.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "e88ce94f0deb4f96b36e53963423185c84da2793ef785ba8ec3e335566582185",
+      "normalizedSha256": "e88ce94f0deb4f96b36e53963423185c84da2793ef785ba8ec3e335566582185",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-login.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0c4002486cc62d7f2d1efe786262d974dfbcc46d4583fca8e41e9d82d2a742c7",
+      "normalizedSha256": "0c4002486cc62d7f2d1efe786262d974dfbcc46d4583fca8e41e9d82d2a742c7",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-industry.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "0810bebb95692a9a6b483f1e537083d767e4db4ff252eaec21659077a40c62c7",
+      "normalizedSha256": "0810bebb95692a9a6b483f1e537083d767e4db4ff252eaec21659077a40c62c7",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-library.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "7bd197cf5d73eaf6b3e5cbab7eb7a708db00c44ae7ad190171f1cec3154828e9",
+      "normalizedSha256": "7bd197cf5d73eaf6b3e5cbab7eb7a708db00c44ae7ad190171f1cec3154828e9",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-stages.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "61a3312cccb2c7bc8cfc7a13ec6028cb7d06d88a7fc973714e25519a1c44f009",
+      "normalizedSha256": "61a3312cccb2c7bc8cfc7a13ec6028cb7d06d88a7fc973714e25519a1c44f009",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-report-type.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "368f25bbe9669528fd287dbdd7ce6e0a6149bbe328e76d17dde182a697f13d0a",
+      "normalizedSha256": "368f25bbe9669528fd287dbdd7ce6e0a6149bbe328e76d17dde182a697f13d0a",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-members.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "fe2ce5224ffbbdda3e7d0b971327f6354800a3a4831e86797bb02357591f4137",
+      "normalizedSha256": "fe2ce5224ffbbdda3e7d0b971327f6354800a3a4831e86797bb02357591f4137",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-reports.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "8cf9926c483bb7518093ef3033e10e0ffc4f83b3a81a06d781ae3893d441a971",
+      "normalizedSha256": "8cf9926c483bb7518093ef3033e10e0ffc4f83b3a81a06d781ae3893d441a971",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-pool.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "db98e2fdfc150571471d1653d37ef296257343b942d5e653ed4234d685225e7d",
+      "normalizedSha256": "db98e2fdfc150571471d1653d37ef296257343b942d5e653ed4234d685225e7d",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-report.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "7c39fde325d39f4526c71fa80a96c9d0780064b1a78d11085d251a0080fe0bb7",
+      "normalizedSha256": "7c39fde325d39f4526c71fa80a96c9d0780064b1a78d11085d251a0080fe0bb7",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-workbench.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "67ad236087e058925239e25c7434f81efb1056325a70c6d102bdbfea421355d5",
+      "normalizedSha256": "67ad236087e058925239e25c7434f81efb1056325a70c6d102bdbfea421355d5",
+      "matches": true
+    },
+    {
+      "path": "src/pages/view-workshop-outputs.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "e8030de394d5b94bf73a34533d8a4c5a58d3a7fe5a55a7bf81967b15e35b4da8",
+      "normalizedSha256": "e8030de394d5b94bf73a34533d8a4c5a58d3a7fe5a55a7bf81967b15e35b4da8",
+      "matches": true
+    },
+    {
+      "path": "src/layout/kf-app.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "6b681d70e54e949229f9fd3c37668175fd2117b724a10756f08f392772e2c17d",
+      "normalizedSha256": "6b681d70e54e949229f9fd3c37668175fd2117b724a10756f08f392772e2c17d",
+      "matches": true
+    },
+    {
+      "path": "src/layout/kf-nav.js",
+      "status": 200,
+      "contentType": "application/javascript; charset=utf-8",
+      "mimeMatches": true,
+      "sha256": "04c1ffacbe0a247880abc2f814cbb276ab4f3b62edf097a4a93786ce122fbb99",
+      "normalizedSha256": "04c1ffacbe0a247880abc2f814cbb276ab4f3b62edf097a4a93786ce122fbb99",
+      "matches": true
+    }
+  ],
+  "checks": [
+    {
+      "name": "homepage-preserved",
+      "ok": true
+    },
+    {
+      "name": "ksfGetPublicBoard",
+      "ok": true
+    },
+    {
+      "name": "ksfGetPublicMemberGroups",
+      "ok": true
+    },
+    {
+      "name": "roster-available-to-groups",
+      "ok": true
+    },
+    {
+      "name": "research-library-readable",
+      "ok": true
+    }
+  ],
+  "businessWrites": 0,
+  "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+  "memberCount": 40,
+  "groupRevision": 9,
+  "generation": "5567321af2f4f9813ef01927d39f47b515e1733c25fcec97dbf5f87dd16f5e56",
+  "counts": {
+    "products": 4580,
+    "reviews": 6622,
+    "social_posts": 3831,
+    "policy_documents": 28,
+    "policy_clauses": 37,
+    "prices": 4173,
+    "specs": 5636,
+    "product_observations": 9473,
+    "source_records": 20626,
+    "source_links": 32785,
+    "assets": 7580,
+    "collection_requests": 2575,
+    "quality_issues": 11185,
+    "ranking_signals": 55,
+    "coverage_observations": 1384
+  }
+}

+ 52 - 0
docs/board-local-release-20261111/production-status.json

@@ -0,0 +1,52 @@
+{
+  "schema": "ksf.board-deployment-status.v1",
+  "deployedCommit": "11ba2290d02126c635f5673f651646a92593e279",
+  "productionDeployed": true,
+  "operationsBrowserAcceptancePassed": true,
+  "userScreenshotAcceptancePassed": true,
+  "acceptanceSource": "operations report and user screenshot confirmation",
+  "siteDirectory": "/opt/kangshifu-web",
+  "releaseDirectory": "/opt/ksf-board-local-20261111-r1",
+  "reportedReceiptDirectories": [
+    "/opt/ksf-board-local-20261111-results-7JCuJHh6",
+    "/opt/ksf-board-local-20261111-results-CTLRkm3E",
+    "/opt/ksf-board-local-20261111-results-5V34Anjd"
+  ],
+  "reportedFrontendBackup": "/opt/ksf-industry-ui-backup-wMPRHX",
+  "reportedMemberBackupFile": "member-functions-before-003dbfd9-e3d3-42fd-8d21-0eb7a92e5e7d.private.json",
+  "homepageSha256": "dfeb6a6d0b63e8e1190f9308bf6619a5ca523a5a2f91af5f7ca5ad5fc9642db3",
+  "databaseConfigurationChanged": false,
+  "incident": {
+    "cause": "nginx served report-contract.mjs as application/octet-stream, preventing the application module graph from loading",
+    "operationsFix": "Add mjs to the application/javascript mapping in /etc/nginx/mime.types; nginx -t and reload",
+    "reportedConfigBackup": "/etc/nginx/mime.types.bak-20261111T133816Z",
+    "reportedOriginalPatch": "/opt/ksf-board-mjs-preflight-20261111.patch",
+    "originalPatchDownloadedOrByteVerified": false,
+    "repositoryPreflight": "Independently implemented from the reported failure; not a byte-identical copy of the operations patch"
+  },
+  "otherHosts": {
+    "R730": "No current static .mjs site reported; leave nginx unchanged",
+    "server.fmode.cn": "No current static .mjs site reported; leave nginx unchanged"
+  },
+  "independentPublicVerification": {
+    "filesMatched": 117,
+    "readChecksPassed": 5,
+    "registeredAndHistoricalMembers": 40,
+    "mjsContentType": "application/javascript; charset=utf-8",
+    "evidence": "production-public-verification.json",
+    "checkedAt": "2026-11-11T13:57:29.871Z",
+    "moduleGraphCount": 115,
+    "moduleGraphFailures": 0
+  },
+  "productionRedeploymentRequiredForThisArchive": false,
+  "repositoryPreflightValidation": {
+    "testsPassed": 9,
+    "testsFailed": 0,
+    "testSource": "frozen deployed revision plus MIME preflight scripts",
+    "negativeCliExitCodeVerified": 1,
+    "missingMappingRejected": true,
+    "octetStreamRejected": true,
+    "spaFallbackRejected": true,
+    "serverConfigModifiedByCodex": false
+  }
+}

+ 2 - 1
scripts/deploy-board-local.sh

@@ -9,7 +9,8 @@ cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.."
 umask 022
 umask 022
 node -e 'if(Number(process.versions.node.split(".")[0])<20)process.exit(1)'
 node -e 'if(Number(process.versions.node.split(".")[0])<20)process.exit(1)'
 results_dir="$(mktemp -d /opt/ksf-board-local-20261111-results-XXXXXXXX)"
 results_dir="$(mktemp -d /opt/ksf-board-local-20261111-results-XXXXXXXX)"
-node --test test/board-local-deploy.test.mjs | tee "$results_dir/deploy-tests.txt"
+node scripts/preflight-mime-mjs.mjs --origin https://kangshifu.fmode.cn --target-dir "$1" | tee "$results_dir/mime-preflight.json"
+node --test test/board-local-deploy.test.mjs test/mjs-mime-preflight.test.mjs | tee "$results_dir/deploy-tests.txt"
 node scripts/deploy-board-member-roster.mjs plan | tee "$results_dir/member-plan.json"
 node scripts/deploy-board-member-roster.mjs plan | tee "$results_dir/member-plan.json"
 node scripts/publish-board-local.mjs --target-dir "$1" | tee "$results_dir/frontend-plan.json"
 node scripts/publish-board-local.mjs --target-dir "$1" | tee "$results_dir/frontend-plan.json"
 node scripts/deploy-board-member-roster.mjs apply | tee "$results_dir/member-apply.json"
 node scripts/deploy-board-member-roster.mjs apply | tee "$results_dir/member-apply.json"

+ 55 - 0
scripts/preflight-mime-mjs.mjs

@@ -0,0 +1,55 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import crypto from 'node:crypto';
+import {fileURLToPath} from 'node:url';
+
+const sha=bytes=>crypto.createHash('sha256').update(bytes.toString('utf8').replaceAll('\r\n','\n')).digest('hex');
+const jsType=value=>/^(?:application|text)\/javascript$/i.test(String(value).split(';')[0].trim());
+const remedy='将 mjs 加入 nginx 的 JavaScript MIME 映射,确认配置后执行 nginx -t && nginx -s reload,再重新预检。';
+
+/** Read-only gate: a correct file hash cannot compensate for an unusable module Content-Type. */
+export async function preflightMjsMime({origin,targetDir,mimeFile='/etc/nginx/mime.types',files,fetchImpl=fetch}){
+ const result={ok:false,readOnly:true,checkedAt:new Date().toISOString(),checks:[],modules:[],businessWrites:0};
+ const check=(name,ok,extra={})=>result.checks.push({name,ok,...extra});
+ try{
+  const base=new URL(origin);
+  if(!['http:','https:'].includes(base.protocol)||base.username||base.password||base.pathname!=='/'||base.search||base.hash)throw Error('INVALID_ORIGIN');
+  if(!path.isAbsolute(targetDir)||await fs.realpath(targetDir)!==path.resolve(targetDir))throw Error('INVALID_TARGET_DIRECTORY');
+  result.origin=base.origin;result.targetDir=targetDir;
+  const text=(await fs.readFile(mimeFile,'utf8')).replace(/#[^\r\n]*/g,'');
+  const mappings=text.split(';').map(s=>s.replace(/[{}]/g,' ').trim().split(/\s+/)).filter(words=>words.includes('mjs'));
+  check('nginx-mjs-javascript-mapping',mappings.length===1&&mappings[0].some(jsType),{mimeFile});
+  if(!result.checks[0].ok)throw Error('MJS_JAVASCRIPT_MAPPING_MISSING');
+  const modules=files.filter(file=>file.path.endsWith('.mjs'));
+  for(const file of modules){
+   if(file.path.startsWith('/')||file.path.includes('\\')||file.path.split('/').some(p=>p==='..'||!p))throw Error('INVALID_MODULE_PATH');
+   const local=path.resolve(targetDir,file.path);
+   let bytes;
+   try{bytes=await fs.readFile(local);}catch(e){if(e.code==='ENOENT'){result.modules.push({path:file.path,checked:false,reason:'not-yet-deployed'});continue;}throw e;}
+   const url=new URL(file.path,base);url.searchParams.set('mjsPreflight',crypto.randomUUID());
+   const response=await fetchImpl(url,{headers:{'cache-control':'no-cache'},redirect:'error',signal:AbortSignal.timeout(30000)});
+   const contentType=response.headers.get('content-type')||'',body=Buffer.from(await response.arrayBuffer());
+   const row={path:file.path,checked:true,status:response.status,contentType,bytesMatchTarget:sha(bytes)===sha(body),ok:response.status===200&&jsType(contentType)&&sha(bytes)===sha(body)};
+   result.modules.push(row);check('public-mjs:'+file.path,row.ok);
+  }
+  result.publicModulesChecked=result.modules.filter(m=>m.checked).length;
+  result.publicVerificationDeferred=result.modules.some(m=>!m.checked);
+  result.ok=result.checks.every(c=>c.ok);
+  if(!result.ok)result.errorCode='PUBLIC_MJS_NOT_LOADABLE';
+ }catch(e){result.errorCode=/^[A-Z_]+$/.test(e.message)?e.message:'MJS_PREFLIGHT_UNAVAILABLE';}
+ if(!result.ok)result.remedy=remedy;
+ return result;
+}
+
+if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
+ 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]];
+   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 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;}
+}

+ 3 - 1
scripts/verify-board-local-public.mjs

@@ -8,7 +8,9 @@ let next=0;
 try{
 try{
  await Promise.all(Array.from({length:6},async()=>{while(next<manifest.files.length){const file=manifest.files[next++],url=new URL(file.path,base);url.searchParams.set('boardRelease','20261111-'+Date.now());
  await Promise.all(Array.from({length:6},async()=>{while(next<manifest.files.length){const file=manifest.files[next++],url=new URL(file.path,base);url.searchParams.set('boardRelease','20261111-'+Date.now());
   const r=await fetch(url,{headers:{'cache-control':'no-cache'},signal:AbortSignal.timeout(45000)}),bytes=Buffer.from(await r.arrayBuffer());
   const r=await fetch(url,{headers:{'cache-control':'no-cache'},signal:AbortSignal.timeout(45000)}),bytes=Buffer.from(await r.arrayBuffer());
-  receipt.modules.push({path:file.path,status:r.status,sha256:sha(bytes),normalizedSha256:sha(normal(bytes)),matches:r.ok&&sha(normal(bytes))===file.sha256});
+  const contentType=r.headers.get('content-type')||'',javascript=/\.m?js$/.test(file.path);
+  const mimeMatches=!javascript||/^(?:application|text)\/javascript(?:\s*;|$)/i.test(contentType);
+  receipt.modules.push({path:file.path,status:r.status,contentType,mimeMatches,sha256:sha(bytes),normalizedSha256:sha(normal(bytes)),matches:r.ok&&mimeMatches&&sha(normal(bytes))===file.sha256});
  }}));
  }}));
  const home=await fetch(base+'index.html',{headers:{'cache-control':'no-cache'}});receipt.homepageSha256=sha(Buffer.from(await home.arrayBuffer()));
  const home=await fetch(base+'index.html',{headers:{'cache-control':'no-cache'}});receipt.homepageSha256=sha(Buffer.from(await home.arrayBuffer()));
  receipt.checks.push({name:'homepage-preserved',ok:receipt.homepageSha256===manifest.homepageSha256});
  receipt.checks.push({name:'homepage-preserved',ok:receipt.homepageSha256===manifest.homepageSha256});

+ 34 - 0
test/mjs-mime-preflight.test.mjs

@@ -0,0 +1,34 @@
+import fs from 'node:fs/promises';
+import path from 'node:path';
+import os from 'node:os';
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {spawnSync} from 'node:child_process';
+import {preflightMjsMime} from '../scripts/preflight-mime-mjs.mjs';
+
+async function fixture(t,{mapping='types { application/javascript js mjs; }',type='application/javascript; charset=utf-8',body='export const ready=true;'}={}){
+ const root=await fs.mkdtemp(path.join(os.tmpdir(),'ksf-mjs-preflight-'));t.after(()=>fs.rm(root,{recursive:true,force:true}));
+ const targetDir=await fs.realpath(root),mimeFile=path.join(root,'mime.types');await fs.writeFile(mimeFile,mapping);await fs.writeFile(path.join(root,'module.mjs'),'export const ready=true;');
+ return{origin:'https://example.test',targetDir,mimeFile,files:[{path:'module.mjs'}],fetchImpl:async()=>new Response(body,{headers:{'content-type':type}})};
+}
+test('有效配置与真实模块响应均通过,预检保持只读',async t=>{
+ const options=await fixture(t),before=await fs.readdir(options.targetDir),r=await preflightMjsMime(options);
+ assert.equal(r.ok,true);assert.equal(r.readOnly,true);assert.equal(r.publicModulesChecked,1);assert.deepEqual(await fs.readdir(options.targetDir),before);
+});
+test('旧配置缺少mjs映射时,在请求前停止,注释不能冒充映射',async t=>{
+ const options=await fixture(t,{mapping:'types { application/javascript js; }\n# application/javascript mjs;'});let requests=0;
+ const r=await preflightMjsMime({...options,fetchImpl:async()=>{requests++;throw Error('should not fetch');}});
+ assert.equal(r.ok,false);assert.equal(r.errorCode,'MJS_JAVASCRIPT_MAPPING_MISSING');assert.equal(requests,0);assert.ok(r.remedy.includes('nginx -t'));
+ const cli=spawnSync(process.execPath,['scripts/preflight-mime-mjs.mjs','--target-dir',options.targetDir,'--mime-file',options.mimeFile],{encoding:'utf8'});
+ assert.equal(cli.status,1);assert.equal(JSON.parse(cli.stdout).ok,false);
+});
+test('配置正确但响应为octet-stream时拒绝,复现白屏原因',async t=>{
+ const r=await preflightMjsMime(await fixture(t,{type:'application/octet-stream'}));assert.equal(r.ok,false);assert.equal(r.errorCode,'PUBLIC_MJS_NOT_LOADABLE');
+});
+test('SPA返回HTML或内容不符不能因200和JavaScript响应头而通过',async t=>{
+ const r=await preflightMjsMime(await fixture(t,{body:'<!doctype html><title>fallback</title>'}));assert.equal(r.ok,false);assert.equal(r.modules[0].bytesMatchTarget,false);
+});
+test('首次发布尚不存在的模块明确留待发布后验证',async t=>{
+ const options=await fixture(t);await fs.unlink(path.join(options.targetDir,'module.mjs'));
+ const r=await preflightMjsMime(options);assert.equal(r.ok,true);assert.equal(r.publicModulesChecked,0);assert.equal(r.publicVerificationDeferred,true);assert.equal(r.modules[0].reason,'not-yet-deployed');
+});