From 380cbed4ffc69f00e98a48c64deec84bbf8fa397 Mon Sep 17 00:00:00 2001 From: mes123456 Date: Wed, 19 Aug 2026 10:39:03 -0400 Subject: [PATCH] Checkpoint web parity through Chromium input tasks --- README.md | 7 +- docs/BLENDER_5_2_FULL_PARITY_WBS.md | 37 +- ...NDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md | 332 +- docs/CURRENT_EXECUTION_PLAN.md | 29 +- docs/EXECUTION_QUEUE.md | 54 + docs/PROJECT_STATUS_AND_NEXT_WORK.md | 10 +- docs/README.md | 43 + docs/TASK_BREAKDOWN.md | 208 ++ docs/TASK_CONTEXT_TEMPLATE.md | 45 + docs/status/M12-03E.md | 54 + docs/status/M12-03F.md | 49 + docs/status/M12-03G.md | 45 + docs/status/M12-03H.md | 43 + docs/status/M12-03I.md | 42 + docs/status/M12-03J.md | 45 + docs/status/M12-03K.md | 42 + docs/status/M12-03L.md | 41 + docs/status/M12-03M.md | 38 + docs/status/M12-03N.md | 42 + docs/status/M12-04A.md | 40 + docs/status/M12-04B.md | 42 + docs/status/M12-04C.md | 45 + docs/status/M12-04D.md | 44 + docs/status/M12-04E.md | 38 + docs/status/M12-04F.md | 37 + docs/status/M12-04G.md | 39 + docs/status/M12-04H.md | 48 + docs/status/M12-04I.md | 47 + docs/status/M12-04J.md | 49 + docs/status/M12-05A.md | 49 + docs/status/M12-05B.md | 48 + docs/status/M12-05C.md | 52 + docs/status/M12-05D.md | 54 + docs/status/M12-05E.md | 49 + docs/status/M12-05F.md | 54 + docs/status/M12-06A.md | 59 + docs/status/M12-06B.md | 57 + docs/status/M12-06C.md | 59 + docs/status/M12-06D.md | 56 + docs/status/M12-06E.md | 55 + docs/status/M12-06F.md | 49 + docs/status/M12-06G.md | 57 + docs/status/M12-07A.md | 49 + docs/status/M12-07B.md | 53 + docs/status/M12-07C.md | 55 + docs/status/M12-07D.md | 47 + docs/status/M12-07E.md | 60 + docs/status/M12-07F.md | 51 + docs/status/M12-07G.md | 53 + docs/status/M12-07H.md | 62 + docs/status/M12-07I.md | 49 + docs/status/M12-07J.md | 50 + docs/status/M13-01A.md | 47 + docs/status/M13-01B.md | 41 + docs/status/M13-01C.md | 39 + docs/status/M13-01D.md | 36 + docs/status/M13-01E.md | 41 + docs/status/M13-01F.md | 44 + docs/status/M13-02A.md | 49 + docs/status/M13-02B.md | 52 + docs/status/M13-02C.md | 51 + docs/status/M13-02D.md | 46 + docs/status/M13-02E.md | 42 + docs/status/M13-02F.md | 42 + docs/status/M13-03A.md | 42 + docs/status/M13-03B.md | 41 + docs/status/M13-03C.md | 43 + docs/status/M13-03D.md | 46 + docs/status/M13-03E.md | 45 + docs/status/M13-03F.md | 44 + docs/status/M13-03G.md | 43 + docs/status/M13-04A.md | 44 + docs/status/M13-04B.md | 41 + docs/status/M13-04C.md | 43 + docs/status/M13-04D.md | 24 + docs/status/M13-04E.md | 22 + docs/status/M13-04F.md | 40 + docs/status/M13-04G.md | 38 + docs/status/M13-04H.md | 38 + docs/status/M13-04I.md | 38 + docs/status/M13-04J.md | 38 + docs/status/M13-05A.md | 45 + docs/status/M13-05B.md | 31 + docs/status/M13-05C.md | 27 + docs/status/M13-05D.md | 18 + docs/status/M13-05E.md | 13 + docs/status/M13-05F.md | 14 + docs/status/M13-05G.md | 37 + docs/status/M13-05H.md | 30 + docs/status/M14-01A.md | 32 + docs/status/M14-01B.md | 21 + docs/status/M14-01C.md | 21 + docs/status/M14-01D.md | 33 + docs/status/M14-01E.md | 29 + docs/status/M14-04A.md | 25 + docs/status/M14-04B.md | 26 + docs/status/M14-04C.md | 26 + docs/status/M14-04D.md | 26 + docs/status/M14-04E.md | 26 + docs/status/N-023.md | 88 +- docs/status/N-025.md | 52 +- docs/tasks/M13-04F.md | 41 + docs/tasks/M13-04G.md | 37 + docs/tasks/M13-04H.md | 37 + docs/tasks/M13-04I.md | 37 + docs/tasks/M13-04J.md | 37 + docs/tasks/M13-05A.md | 39 + docs/tasks/M13-05B.md | 28 + docs/tasks/M13-05C.md | 27 + docs/tasks/M13-05D.md | 26 + docs/tasks/M13-05E.md | 27 + docs/tasks/M13-05F.md | 26 + docs/tasks/M13-05G.md | 25 + docs/tasks/M13-05H.md | 24 + docs/tasks/M14-01A.md | 24 + docs/tasks/M14-01B.md | 25 + docs/tasks/M14-01C.md | 25 + docs/tasks/M14-01D.md | 27 + docs/tasks/M14-01E.md | 20 + docs/tasks/M14-04A.md | 16 + docs/tasks/M14-04B.md | 17 + docs/tasks/M14-04C.md | 17 + docs/tasks/M14-04D.md | 16 + docs/tasks/M14-04E.md | 17 + docs/tasks/M14-04F.md | 56 + docs/web/DEPLOYMENT.md | 5 + docs/web/dependency-severity-policy.json | 18 + docs/web/deployment-contract.json | 14 +- docs/web/sbom.spdx.json | 60 +- .../files/web/archive-security/manifest.json | 68 + .../archive-security/tar-path-traversal.tar | Bin 0 -> 2048 bytes .../archive-security/tar-prefix-conflict.tar | Bin 0 -> 3072 bytes .../archive-security/tar-symlink-escape.tar | Bin 0 -> 2048 bytes .../archive-security/zip-compression-bomb.zip | Bin 0 -> 115 bytes .../archive-security/zip-duplicate-path.zip | Bin 0 -> 212 bytes .../archive-security/zip-path-traversal.zip | Bin 0 -> 132 bytes tests/files/web/m11_render_reference.blend1 | Bin 0 -> 89533 bytes .../web/m12_glb_desktop_v1/animation.glb | Bin 0 -> 2616 bytes tests/files/web/m12_glb_desktop_v1/mesh.glb | Bin 0 -> 1236 bytes tests/files/web/m12_glb_desktop_v1/pbr.glb | Bin 0 -> 1208 bytes tests/files/web/m12_glb_desktop_v1/skin.glb | Bin 0 -> 1912 bytes tests/files/web/m12_glb_desktop_v1/uv.glb | Bin 0 -> 1704 bytes .../files/web/m12_glb_main_v1/animation.blend | Bin 0 -> 91107 bytes .../web/m12_glb_main_v1/animation.blend1 | Bin 0 -> 91072 bytes tests/files/web/m12_glb_main_v1/mesh.blend | Bin 0 -> 88625 bytes tests/files/web/m12_glb_main_v1/mesh.blend1 | Bin 0 -> 88625 bytes tests/files/web/m12_glb_main_v1/pbr.blend | Bin 0 -> 88389 bytes tests/files/web/m12_glb_main_v1/pbr.blend1 | Bin 0 -> 88396 bytes tests/files/web/m12_glb_main_v1/skin.blend | Bin 0 -> 91781 bytes tests/files/web/m12_glb_main_v1/skin.blend1 | Bin 0 -> 91817 bytes tests/files/web/m12_glb_main_v1/uv.blend | Bin 0 -> 89127 bytes tests/files/web/m12_glb_main_v1/uv.blend1 | Bin 0 -> 89108 bytes tests/files/web/m12_glb_web_v1/animation.glb | Bin 0 -> 3708 bytes tests/files/web/m12_glb_web_v1/pbr.glb | Bin 0 -> 1840 bytes tests/files/web/m12_glb_web_v1/skin.glb | Bin 0 -> 14264 bytes tests/files/web/m12_glb_web_v1/uv.glb | Bin 0 -> 2756 bytes .../m12_library_link_v1/m12_link_source.blend | Bin 0 -> 89009 bytes .../m12_library_link_v1/m12_link_target.blend | Bin 0 -> 85892 bytes .../m12_override_source.blend | Bin 0 -> 88356 bytes .../m12_override_target.blend | Bin 0 -> 86299 bytes .../web/m12_obj_desktop_v1/single-mesh.mtl | 22 + .../web/m12_obj_desktop_v1/single-mesh.obj | 20 + .../web/m12_obj_multi_v1/m12_obj_texture.png | Bin 0 -> 261 bytes .../web/m12_obj_multi_v1/malformed-face.obj | 27 + .../web/m12_obj_multi_v1/multi-object.mtl | 22 + .../web/m12_obj_multi_v1/multi-object.obj | 27 + .../web/m12_obj_multi_v1/negative-index.obj | 27 + .../capability-ascii.ply | 19 + .../capability-binary-le.ply | Bin 0 -> 391 bytes .../web/m12_ply_mapping_v1/mapping-ascii.ply | 25 + .../m12_ply_mapping_v1/mapping-binary-le.ply | Bin 0 -> 568 bytes .../unknown-property-ascii.ply | 26 + .../web/m12_ply_negative_v1/big-endian.ply | Bin 0 -> 179 bytes .../malformed-list-ascii.ply | 13 + .../oversized-count-ascii.ply | 9 + .../capability-ascii.stl | 16 + .../capability-binary.stl | Bin 0 -> 184 bytes .../m12_stl_edges_v1/capability-binary.stl | Bin 0 -> 184 bytes .../m12_stl_edges_v1/degenerate-binary.stl | Bin 0 -> 184 bytes .../web/m12_stl_edges_v1/trailing-binary.stl | Bin 0 -> 188 bytes .../malicious-script.blend | Bin 0 -> 491160 bytes .../script-entry-inventory.blend | Bin 0 -> 497845 bytes tests/golden/M12-01E/manifest.json | 2 +- tests/golden/M12-02B/manifest.json | 2 +- tests/golden/M12-03E/manifest.json | 40 + tests/golden/M12-03F/desktop-link-report.json | 202 ++ tests/golden/M12-03F/manifest.json | 43 + tests/golden/M12-03G/manifest.json | 35 + tests/golden/M12-03H/manifest.json | 35 + tests/golden/M12-03I/manifest.json | 37 + .../M12-03J/desktop-override-report.json | 47 + tests/golden/M12-03J/manifest.json | 50 + tests/golden/M12-03K/manifest.json | 37 + tests/golden/M12-03L/manifest.json | 35 + tests/golden/M12-03M/manifest.json | 35 + tests/golden/M12-03N/manifest.json | 60 + tests/golden/M12-04A/manifest.json | 35 + tests/golden/M12-04B/manifest.json | 40 + tests/golden/M12-04C/manifest.json | 41 + tests/golden/M12-04D/manifest.json | 37 + tests/golden/M12-04E/manifest.json | 36 + tests/golden/M12-04F/manifest.json | 24 + tests/golden/M12-04G/manifest.json | 23 + tests/golden/M12-04H/manifest.json | 24 + tests/golden/M12-04I/manifest.json | 24 + tests/golden/M12-04J/manifest.json | 27 + tests/golden/M12-05A/format-inventory.json | 2796 +++++++++++++++++ tests/golden/M12-05A/manifest.json | 26 + tests/golden/M12-05B/capability-matrix.json | 392 +++ tests/golden/M12-05B/manifest.json | 27 + tests/golden/M12-05C/manifest.json | 29 + tests/golden/M12-05D/manifest.json | 30 + tests/golden/M12-05D/runtime-receipts.json | 282 ++ .../M12-05E/bound-runtime-receipts.json | 325 ++ tests/golden/M12-05E/manifest.json | 28 + .../M12-05F/fresh-runtime-receipts.json | 332 ++ tests/golden/M12-05F/manifest.json | 34 + tests/golden/M12-06A/desktop-fixtures.json | 697 ++++ tests/golden/M12-06A/manifest.json | 57 + tests/golden/M12-06B/manifest.json | 63 + tests/golden/M12-06B/web-import-report.json | 220 ++ tests/golden/M12-06C/desktop-main-report.json | 451 +++ tests/golden/M12-06C/manifest.json | 64 + tests/golden/M12-06D/manifest.json | 55 + tests/golden/M12-06D/web-loss-report.json | 163 + .../M12-06E/desktop-reimport-report.json | 393 +++ tests/golden/M12-06E/manifest.json | 60 + tests/golden/M12-06F/manifest.json | 51 + tests/golden/M12-06F/negative-report.json | 18 + tests/golden/M12-06G/manifest.json | 69 + tests/golden/M12-06G/recovery-report.json | 41 + tests/golden/M12-07A/desktop-fixture.json | 223 ++ tests/golden/M12-07A/manifest.json | 56 + tests/golden/M12-07B/desktop-fixtures.json | 223 ++ tests/golden/M12-07B/manifest.json | 68 + tests/golden/M12-07C/manifest.json | 70 + .../golden/M12-07C/web-roundtrip-report.json | 95 + tests/golden/M12-07D/capability-report.json | 68 + tests/golden/M12-07D/manifest.json | 54 + tests/golden/M12-07E/desktop-edge-report.json | 144 + tests/golden/M12-07E/edge-fixtures.json | 27 + tests/golden/M12-07E/manifest.json | 78 + tests/golden/M12-07E/web-edge-report.json | 436 +++ tests/golden/M12-07F/manifest.json | 59 + .../golden/M12-07F/web-roundtrip-report.json | 122 + tests/golden/M12-07G/capability-report.json | 87 + tests/golden/M12-07G/manifest.json | 52 + tests/golden/M12-07H/manifest.json | 27 + tests/golden/M12-07H/mapping-report.json | 292 ++ .../golden/M12-07H/web-roundtrip-report.json | 179 ++ tests/golden/M12-07I/manifest.json | 22 + tests/golden/M12-07I/negative-report.json | 40 + .../M12-07J/io-format-recovery-report.json | 376 +++ tests/golden/M12-07J/manifest.json | 20 + tests/golden/M13-01A/entry-inventory.json | 156 + tests/golden/M13-01A/manifest.json | 18 + tests/golden/M13-01B/manifest.json | 17 + .../golden/M13-01B/open-metadata-report.json | 31 + tests/golden/M13-01C/manifest.json | 16 + tests/golden/M13-01C/policy-codes-report.json | 21 + tests/golden/M13-01D/manifest.json | 15 + tests/golden/M13-01D/ui-bypass-report.json | 19 + tests/golden/M13-01E/manifest.json | 17 + .../M13-01E/script-save-reopen-report.json | 102 + tests/golden/M13-01F/malicious-report.json | 37 + tests/golden/M13-01F/manifest.json | 18 + .../M13-02A/manifest-budget-report.json | 45 + tests/golden/M13-02A/manifest.json | 40 + .../M13-02B/manifest-canonical-report.json | 30 + tests/golden/M13-02B/manifest.json | 40 + tests/golden/M13-02C/manifest.json | 40 + tests/golden/M13-02C/trust-policy-report.json | 26 + tests/golden/M13-02D/manifest.json | 40 + tests/golden/M13-02D/signature-report.json | 24 + tests/golden/M13-02E/manifest.json | 40 + .../M13-02E/permission-policy-report.json | 21 + tests/golden/M13-02F/manifest.json | 40 + .../M13-02F/signature-negative-report.json | 21 + tests/golden/M13-03A/manifest.json | 40 + .../golden/M13-03A/sandbox-scope-report.json | 29 + tests/golden/M13-03B/manifest.json | 40 + .../golden/M13-03B/sandbox-budget-report.json | 29 + tests/golden/M13-03C/host-call-report.json | 27 + tests/golden/M13-03C/manifest.json | 40 + tests/golden/M13-03D/manifest.json | 40 + .../M13-03D/sandbox-isolation-report.json | 51 + tests/golden/M13-03E/manifest.json | 44 + .../M13-03E/sandbox-cancellation-report.json | 32 + tests/golden/M13-03F/manifest.json | 44 + .../M13-03F/sandbox-dispose-report.json | 76 + tests/golden/M13-03G/manifest.json | 23 + .../M13-03G/sandbox-recovery-report.json | 65 + tests/golden/M13-04A/manifest.json | 18 + .../M13-04A/server-job-directory-report.json | 15 + tests/golden/M13-04B/manifest.json | 18 + .../M13-04B/server-job-workspace-report.json | 15 + tests/golden/M13-04C/manifest.json | 18 + .../server-job-resource-budget-report.json | 55 + tests/golden/M13-04D/manifest.json | 18 + .../server-job-network-policy-report.json | 28 + tests/golden/M13-04E/manifest.json | 17 + .../M13-04E/server-job-startup-report.json | 29 + tests/golden/M13-04F/manifest.json | 18 + .../M13-04F/server-job-output-report.json | 70 + tests/golden/M13-04G/manifest.json | 18 + .../server-job-cancellation-report.json | 14 + tests/golden/M13-04H/manifest.json | 18 + .../M13-04H/server-job-fault-report.json | 80 + tests/golden/M13-04I/manifest.json | 18 + .../M13-04I/server-job-result-report.json | 17 + tests/golden/M13-04J/manifest.json | 18 + .../server-job-idempotency-report.json | 16 + tests/golden/M13-05A/csp-report.json | 17 + tests/golden/M13-05A/manifest.json | 21 + tests/golden/M13-05B/csp-resource-report.json | 47 + tests/golden/M13-05B/manifest.json | 19 + .../golden/M13-05C/dependency-inventory.json | 1862 +++++++++++ tests/golden/M13-05C/manifest.json | 19 + .../M13-05D/dependency-severity-report.json | 19 + tests/golden/M13-05D/manifest.json | 18 + tests/golden/M13-05E/manifest.json | 24 + tests/golden/M13-05E/supply-chain-report.json | 35 + .../M13-05F/malicious-input-report.json | 88 + tests/golden/M13-05F/manifest.json | 16 + tests/golden/M13-05G/fuzz-report.json | 42 + tests/golden/M13-05G/manifest.json | 17 + tests/golden/M13-05H/manifest.json | 18 + .../script-audit-integrity-report.json | 33 + .../M14-01A/chromium-freeze-report.json | 70 + tests/golden/M14-01A/manifest.json | 21 + .../M14-01B/firefox-capability-report.json | 71 + tests/golden/M14-01B/manifest.json | 17 + tests/golden/M14-01C/manifest.json | 17 + .../M14-01C/webkit-capability-report.json | 71 + tests/golden/M14-01D/manifest.json | 32 + .../golden/M14-01D/probe-identity-report.json | 55 + .../chromium-webgpu-boundary-report.json | 19 + tests/golden/M14-01E/manifest.json | 32 + .../chromium-device-budget-report.json | 22 + tests/golden/M14-04A/manifest.json | 36 + tests/golden/M14-04B/chromium-dpr-report.json | 65 + tests/golden/M14-04B/manifest.json | 44 + .../M14-04C/chromium-pointer-report.json | 81 + tests/golden/M14-04C/manifest.json | 44 + tests/golden/M14-04D/chromium-ime-report.json | 26 + tests/golden/M14-04D/manifest.json | 40 + .../M14-04E/chromium-keymap-report.json | 44 + tests/golden/M14-04E/manifest.json | 40 + .../M14-04F/chromium-input-modal-report.json | 38 + tests/golden/M14-04F/manifest.json | 36 + tools/web/check-binary-archive.mjs | 2 +- tools/web/check-chromium-device-budget.mjs | 34 + tools/web/check-chromium-dpr-consistency.mjs | 93 + tools/web/check-chromium-ime-guard.mjs | 53 + tools/web/check-chromium-input-modal.mjs | 45 + tools/web/check-chromium-keymap-fixture.mjs | 46 + tools/web/check-chromium-pointer-contract.mjs | 44 + tools/web/check-chromium-release-freeze.mjs | 56 + tools/web/check-chromium-webgpu-boundary.mjs | 38 + tools/web/check-csp-policy.mjs | 81 + tools/web/check-csp-resource-policy.mjs | 114 + tools/web/check-dependency-inventory.mjs | 49 + .../web/check-dependency-severity-policy.mjs | 70 + tools/web/check-deployment-contract.mjs | 12 + tools/web/check-firefox-capability.mjs | 81 + tools/web/check-fuzz-regression.mjs | 45 + tools/web/check-glb-desktop-fixtures.mjs | 123 + tools/web/check-glb-desktop-import.mjs | 54 + tools/web/check-glb-loss-report.mjs | 54 + tools/web/check-glb-main-persistence.mjs | 100 + tools/web/check-glb-negative-cases.mjs | 35 + tools/web/check-glb-recovery.mjs | 47 + tools/web/check-glb-web-reimport.mjs | 94 + .../web/check-io-format-capability-matrix.mjs | 62 + .../web/check-io-format-receipt-bindings.mjs | 36 + .../web/check-io-format-receipt-freshness.mjs | 87 + tools/web/check-io-format-recovery.mjs | 27 + .../web/check-io-format-runtime-inventory.mjs | 78 + .../web/check-io-format-runtime-receipts.mjs | 47 + tools/web/check-io-format-ui-gate.mjs | 43 + tools/web/check-library-link-fixture.mjs | 106 + tools/web/check-library-main-append.mjs | 82 + .../web/check-library-operation-commands.mjs | 28 + tools/web/check-library-override-fixture.mjs | 82 + .../web/check-malicious-archive-fixtures.mjs | 182 ++ tools/web/check-malicious-input-matrix.mjs | 48 + tools/web/check-malicious-script-fixture.mjs | 16 + .../web/check-obj-multi-negative-fixtures.mjs | 110 + tools/web/check-obj-single-mesh-fixture.mjs | 86 + tools/web/check-obj-web-roundtrip.mjs | 35 + tools/web/check-obj-web-roundtrip.py | 71 + tools/web/check-ply-capability-fixtures.mjs | 50 + tools/web/check-ply-mapping-fixtures.mjs | 46 + tools/web/check-ply-negative-fixtures.mjs | 32 + tools/web/check-ply-web-roundtrip.py | 56 + tools/web/check-probe-identity.mjs | 188 ++ tools/web/check-script-audit-integrity.mjs | 53 + tools/web/check-script-entry-inventory.mjs | 39 + tools/web/check-script-host-call.mjs | 46 + tools/web/check-script-manifest-budgets.mjs | 76 + tools/web/check-script-manifest-canonical.mjs | 67 + tools/web/check-script-open-metadata.mjs | 17 + tools/web/check-script-permission-policy.mjs | 46 + tools/web/check-script-policy-codes.mjs | 36 + tools/web/check-script-sandbox-budget.mjs | 35 + .../web/check-script-sandbox-cancellation.mjs | 37 + tools/web/check-script-sandbox-dispose.mjs | 71 + tools/web/check-script-sandbox-isolation.mjs | 52 + tools/web/check-script-sandbox-recovery.mjs | 52 + tools/web/check-script-sandbox-scope.mjs | 39 + tools/web/check-script-save-reopen.mjs | 17 + tools/web/check-script-signature-negative.mjs | 47 + tools/web/check-script-signature.mjs | 48 + tools/web/check-script-trust-policy.mjs | 62 + tools/web/check-script-ui-bypass.mjs | 22 + tools/web/check-scripting-isolation.mjs | 2 + tools/web/check-server-job-cancellation.mjs | 37 + tools/web/check-server-job-fault-codes.mjs | 35 + tools/web/check-server-job-idempotency.mjs | 37 + tools/web/check-server-job-isolation.mjs | 38 + tools/web/check-server-job-network-policy.mjs | 24 + .../web/check-server-job-output-redaction.mjs | 47 + .../web/check-server-job-resource-budget.mjs | 26 + tools/web/check-server-job-result-binding.mjs | 27 + tools/web/check-server-job-startup.mjs | 32 + tools/web/check-server-job-workspace.mjs | 39 + tools/web/check-stl-capability-fixtures.mjs | 75 + tools/web/check-stl-edge-parity.mjs | 47 + tools/web/check-stl-web-roundtrip.mjs | 38 + tools/web/check-stl-web-roundtrip.py | 58 + tools/web/check-supply-chain-binding.mjs | 77 + tools/web/check-webkit-capability.mjs | 64 + tools/web/fuzz-case-runner.mjs | 108 + tools/web/generate-dependency-inventory.mjs | 79 + tools/web/generate-glb-desktop-fixtures.py | 455 +++ .../generate-glb-desktop-import-report.mjs | 95 + .../generate-glb-main-persistence-fixtures.py | 178 ++ tools/web/generate-glb-web-reimport-report.py | 149 + .../generate-io-format-capability-matrix.mjs | 50 + .../generate-io-format-receipt-bindings.mjs | 32 + .../generate-io-format-receipt-freshness.mjs | 49 + .../generate-io-format-runtime-inventory.py | 128 + .../generate-io-format-runtime-receipts.mjs | 39 + tools/web/generate-io-format-ui-gate.mjs | 43 + tools/web/generate-library-link-fixture.py | 183 ++ .../web/generate-library-override-fixture.py | 164 + .../generate-malicious-archive-fixtures.mjs | 188 ++ .../web/generate-malicious-script-fixture.py | 39 + .../generate-obj-multi-negative-fixtures.py | 285 ++ tools/web/generate-obj-single-mesh-fixture.py | 239 ++ tools/web/generate-ply-capability-fixtures.py | 72 + tools/web/generate-ply-mapping-fixtures.py | 150 + tools/web/generate-ply-negative-fixtures.mjs | 51 + tools/web/generate-script-entry-inventory.py | 66 + tools/web/generate-stl-capability-fixtures.py | 135 + tools/web/generate-stl-edge-fixtures.mjs | 36 + tools/web/probe-stl-edge-fixtures.py | 74 + tools/web/server-job-fault.mjs | 70 + tools/web/server-job-idempotency.mjs | 57 + tools/web/server-job-isolation.mjs | 53 + tools/web/server-job-network-policy.mjs | 25 + tools/web/server-job-output.mjs | 119 + tools/web/server-job-process.mjs | 117 + tools/web/server-job-resource-budget.mjs | 32 + tools/web/server-job-result-binding.mjs | 77 + tools/web/server-job-startup.py | 11 + web/app/src/app/App.tsx | 52 +- ...t-runtime-receipts-freshness-expected.json | 326 ++ .../io-format-runtime-receipts-freshness.json | 332 ++ .../io-format-runtime-receipts.json | 282 ++ .../capabilities/io-format-ui-registry.json | 17 + web/app/src/library-link-chromium.ts | 1 + web/app/src/library-override-chromium.ts | 1 + web/app/src/storage/StorageClient.ts | 4 +- web/app/src/testing/glb-recovery.ts | 7 + web/app/src/testing/io-format-recovery.ts | 9 + web/app/src/testing/script-sandbox-dispose.ts | 27 + .../src/testing/script-sandbox-recovery.ts | 56 + .../src/three-adapter/offscreen-viewport.ts | 23 +- web/app/src/three-adapter/viewport.ts | 29 +- .../workers/glb-desktop-import-test.worker.ts | 47 + .../workers/glb-loss-report-test.worker.ts | 33 + .../workers/glb-negative-cases-test.worker.ts | 26 + .../src/workers/glb-recovery-test.worker.ts | 106 + .../workers/io-format-recovery-test.worker.ts | 62 + .../src/workers/obj-roundtrip-test.worker.ts | 26 + .../src/workers/ply-roundtrip-test.worker.ts | 22 + .../workers/script-host-call-test.worker.ts | 25 + .../script-permission-policy-test.worker.ts | 17 + .../script-sandbox-budget-test.worker.ts | 12 + ...script-sandbox-cancellation-test.worker.ts | 17 + .../script-sandbox-dispose-test.worker.ts | 52 + .../script-sandbox-isolation-test.worker.ts | 24 + .../script-sandbox-recovery-test.worker.ts | 39 + .../script-sandbox-scope-test.worker.ts | 12 + .../script-signature-negative-test.worker.ts | 17 + .../workers/script-signature-test.worker.ts | 15 + .../script-trust-policy-test.worker.ts | 19 + .../scripting-manifest-budget-test.worker.ts | 52 + ...cripting-manifest-canonical-test.worker.ts | 46 + .../workers/scripting-platform-test.worker.ts | 2 +- web/app/src/workers/stl-edge-test.worker.ts | 24 + .../src/workers/stl-roundtrip-test.worker.ts | 23 + web/app/src/workers/storage.worker.ts | 5 +- web/package.json | 67 +- web/protocol/archive-conflicts.ts | 97 + web/protocol/archive-extraction-recovery.ts | 50 + .../archive-extraction-transaction.ts | 258 ++ web/protocol/archive-link-safety.ts | 160 + web/protocol/archive-metadata-first.ts | 135 + web/protocol/asset-library-io.ts | 12 +- web/protocol/asset-path.ts | 65 +- web/protocol/device-budget.ts | 69 + web/protocol/diagnostic-report.ts | 1 + web/protocol/error.ts | 5 + web/protocol/glb-import.ts | 297 +- web/protocol/glb-loss-report.ts | 63 + web/protocol/glb-recovery.ts | 134 + web/protocol/ime-composition.ts | 34 + web/protocol/input-modal.ts | 41 + web/protocol/io-format-capability-matrix.ts | 135 + web/protocol/io-format-receipt-binding.ts | 115 + web/protocol/io-format-receipt-freshness.ts | 185 ++ web/protocol/io-format-recovery.ts | 73 + web/protocol/io-format-runtime-receipt.ts | 167 + web/protocol/io-format-ui-gate.ts | 149 + web/protocol/keyboard-contract.ts | 34 + web/protocol/library-linked-missing.ts | 228 ++ web/protocol/library-linked-mutation.ts | 91 + web/protocol/library-linked-reload.ts | 222 ++ web/protocol/library-negative-cases.ts | 168 + web/protocol/library-override-freshness.ts | 165 + web/protocol/library-override-writer.ts | 156 + web/protocol/library-source-origin.ts | 148 + web/protocol/obj-import.ts | 219 ++ web/protocol/ply-import.ts | 298 ++ web/protocol/pointer-contract.ts | 35 + web/protocol/scripting-platform.ts | 307 +- web/protocol/stl-export.ts | 42 + web/protocol/stl-import.ts | 125 + web/protocol/storage.ts | 2 +- web/protocol/viewport-dpr.ts | 49 + .../e2e/archive-security-fixtures.spec.ts | 21 + web/tests/e2e/glb-desktop-import.spec.ts | 42 + web/tests/e2e/glb-export-loss-report.spec.ts | 106 + web/tests/e2e/glb-main-persistence.spec.ts | 98 + web/tests/e2e/glb-negative-cases.spec.ts | 46 + web/tests/e2e/glb-recovery.spec.ts | 127 + web/tests/e2e/io-format-recovery.spec.ts | 68 + web/tests/e2e/io-format-ui-gate.spec.ts | 28 + web/tests/e2e/library-append-main.spec.ts | 113 +- web/tests/e2e/library-link-chromium.spec.ts | 19 + .../e2e/library-override-chromium.spec.ts | 35 + web/tests/e2e/malicious-script.spec.ts | 22 + web/tests/e2e/obj-web-roundtrip.spec.ts | 87 + web/tests/e2e/ply-negative.spec.ts | 23 + web/tests/e2e/ply-web-roundtrip.spec.ts | 89 + web/tests/e2e/script-host-call.spec.ts | 14 + web/tests/e2e/script-manifest-budgets.spec.ts | 18 + .../e2e/script-manifest-canonical.spec.ts | 12 + web/tests/e2e/script-open-metadata.spec.ts | 23 + .../e2e/script-permission-policy.spec.ts | 17 + web/tests/e2e/script-sandbox-budget.spec.ts | 14 + .../e2e/script-sandbox-cancellation.spec.ts | 27 + web/tests/e2e/script-sandbox-dispose.spec.ts | 30 + .../e2e/script-sandbox-isolation.spec.ts | 34 + web/tests/e2e/script-sandbox-recovery.spec.ts | 21 + web/tests/e2e/script-sandbox-scope.spec.ts | 14 + web/tests/e2e/script-save-reopen.spec.ts | 33 + .../e2e/script-signature-negative.spec.ts | 13 + web/tests/e2e/script-signature.spec.ts | 15 + web/tests/e2e/script-trust-policy.spec.ts | 16 + web/tests/e2e/stl-edge-parity.spec.ts | 62 + web/tests/e2e/stl-web-roundtrip.spec.ts | 63 + web/tests/unit/device-budget.test.mjs | 35 + web/tests/unit/glb-desktop-import.test.mjs | 63 + web/tests/unit/glb-loss-report.test.mjs | 58 + web/tests/unit/glb-negative-cases.test.mjs | 60 + web/tests/unit/glb-recovery.test.mjs | 65 + web/tests/unit/ime-composition.test.mjs | 29 + web/tests/unit/input-modal.test.mjs | 27 + .../unit/io-format-capability-matrix.test.mjs | 54 + .../unit/io-format-receipt-binding.test.mjs | 33 + .../unit/io-format-receipt-freshness.test.mjs | 86 + web/tests/unit/io-format-recovery.test.mjs | 40 + .../unit/io-format-runtime-receipt.test.mjs | 48 + web/tests/unit/io-format-ui-gate.test.mjs | 53 + web/tests/unit/keyboard-contract.test.mjs | 21 + web/tests/unit/library-append-wasm.test.mjs | 64 + .../unit/library-archive-budget.test.mjs | 59 + .../library-archive-cancellation.test.mjs | 209 ++ .../unit/library-archive-conflicts.test.mjs | 46 + .../unit/library-archive-recovery.test.mjs | 163 + web/tests/unit/library-link-safety.test.mjs | 88 + .../unit/library-linked-missing.test.mjs | 107 + .../unit/library-linked-mutation.test.mjs | 60 + web/tests/unit/library-linked-reload.test.mjs | 111 + .../unit/library-metadata-first.test.mjs | 67 + .../unit/library-negative-cases.test.mjs | 56 + .../unit/library-override-freshness.test.mjs | 86 + .../unit/library-override-writer.test.mjs | 82 + .../unit/library-path-normalization.test.mjs | 67 + web/tests/unit/library-path-security.test.mjs | 84 + web/tests/unit/library-source-origin.test.mjs | 54 + web/tests/unit/obj-import.test.mjs | 51 + web/tests/unit/ply-import.test.mjs | 51 + web/tests/unit/ply-negative.test.mjs | 29 + web/tests/unit/pointer-contract.test.mjs | 21 + .../unit/script-audit-integrity.test.mjs | 46 + web/tests/unit/script-host-call.test.mjs | 49 + .../unit/script-manifest-budgets.test.mjs | 85 + .../unit/script-manifest-canonical.test.mjs | 80 + .../unit/script-permission-policy.test.mjs | 36 + web/tests/unit/script-policy-codes.test.mjs | 30 + web/tests/unit/script-sandbox-budget.test.mjs | 30 + .../unit/script-sandbox-cancellation.test.mjs | 32 + .../unit/script-sandbox-dispose.test.mjs | 25 + .../unit/script-sandbox-isolation.test.mjs | 42 + .../unit/script-sandbox-recovery.test.mjs | 32 + web/tests/unit/script-sandbox-scope.test.mjs | 34 + .../unit/script-signature-negative.test.mjs | 44 + web/tests/unit/script-trust-policy.test.mjs | 76 + web/tests/unit/server-job-fault.test.mjs | 24 + .../unit/server-job-idempotency.test.mjs | 48 + web/tests/unit/server-job-isolation.test.mjs | 58 + .../unit/server-job-network-policy.test.mjs | 17 + web/tests/unit/server-job-output.test.mjs | 34 + web/tests/unit/server-job-process.test.mjs | 48 + .../unit/server-job-resource-budget.test.mjs | 18 + .../unit/server-job-result-binding.test.mjs | 33 + web/tests/unit/stl-export.test.mjs | 42 + web/tests/unit/stl-import.test.mjs | 49 + web/tests/unit/viewport-dpr.test.mjs | 24 + 后续工作.txt | 1339 +++++++- 634 files changed, 41862 insertions(+), 212 deletions(-) create mode 100644 docs/EXECUTION_QUEUE.md create mode 100644 docs/README.md create mode 100644 docs/TASK_BREAKDOWN.md create mode 100644 docs/TASK_CONTEXT_TEMPLATE.md create mode 100644 docs/status/M12-03E.md create mode 100644 docs/status/M12-03F.md create mode 100644 docs/status/M12-03G.md create mode 100644 docs/status/M12-03H.md create mode 100644 docs/status/M12-03I.md create mode 100644 docs/status/M12-03J.md create mode 100644 docs/status/M12-03K.md create mode 100644 docs/status/M12-03L.md create mode 100644 docs/status/M12-03M.md create mode 100644 docs/status/M12-03N.md create mode 100644 docs/status/M12-04A.md create mode 100644 docs/status/M12-04B.md create mode 100644 docs/status/M12-04C.md create mode 100644 docs/status/M12-04D.md create mode 100644 docs/status/M12-04E.md create mode 100644 docs/status/M12-04F.md create mode 100644 docs/status/M12-04G.md create mode 100644 docs/status/M12-04H.md create mode 100644 docs/status/M12-04I.md create mode 100644 docs/status/M12-04J.md create mode 100644 docs/status/M12-05A.md create mode 100644 docs/status/M12-05B.md create mode 100644 docs/status/M12-05C.md create mode 100644 docs/status/M12-05D.md create mode 100644 docs/status/M12-05E.md create mode 100644 docs/status/M12-05F.md create mode 100644 docs/status/M12-06A.md create mode 100644 docs/status/M12-06B.md create mode 100644 docs/status/M12-06C.md create mode 100644 docs/status/M12-06D.md create mode 100644 docs/status/M12-06E.md create mode 100644 docs/status/M12-06F.md create mode 100644 docs/status/M12-06G.md create mode 100644 docs/status/M12-07A.md create mode 100644 docs/status/M12-07B.md create mode 100644 docs/status/M12-07C.md create mode 100644 docs/status/M12-07D.md create mode 100644 docs/status/M12-07E.md create mode 100644 docs/status/M12-07F.md create mode 100644 docs/status/M12-07G.md create mode 100644 docs/status/M12-07H.md create mode 100644 docs/status/M12-07I.md create mode 100644 docs/status/M12-07J.md create mode 100644 docs/status/M13-01A.md create mode 100644 docs/status/M13-01B.md create mode 100644 docs/status/M13-01C.md create mode 100644 docs/status/M13-01D.md create mode 100644 docs/status/M13-01E.md create mode 100644 docs/status/M13-01F.md create mode 100644 docs/status/M13-02A.md create mode 100644 docs/status/M13-02B.md create mode 100644 docs/status/M13-02C.md create mode 100644 docs/status/M13-02D.md create mode 100644 docs/status/M13-02E.md create mode 100644 docs/status/M13-02F.md create mode 100644 docs/status/M13-03A.md create mode 100644 docs/status/M13-03B.md create mode 100644 docs/status/M13-03C.md create mode 100644 docs/status/M13-03D.md create mode 100644 docs/status/M13-03E.md create mode 100644 docs/status/M13-03F.md create mode 100644 docs/status/M13-03G.md create mode 100644 docs/status/M13-04A.md create mode 100644 docs/status/M13-04B.md create mode 100644 docs/status/M13-04C.md create mode 100644 docs/status/M13-04D.md create mode 100644 docs/status/M13-04E.md create mode 100644 docs/status/M13-04F.md create mode 100644 docs/status/M13-04G.md create mode 100644 docs/status/M13-04H.md create mode 100644 docs/status/M13-04I.md create mode 100644 docs/status/M13-04J.md create mode 100644 docs/status/M13-05A.md create mode 100644 docs/status/M13-05B.md create mode 100644 docs/status/M13-05C.md create mode 100644 docs/status/M13-05D.md create mode 100644 docs/status/M13-05E.md create mode 100644 docs/status/M13-05F.md create mode 100644 docs/status/M13-05G.md create mode 100644 docs/status/M13-05H.md create mode 100644 docs/status/M14-01A.md create mode 100644 docs/status/M14-01B.md create mode 100644 docs/status/M14-01C.md create mode 100644 docs/status/M14-01D.md create mode 100644 docs/status/M14-01E.md create mode 100644 docs/status/M14-04A.md create mode 100644 docs/status/M14-04B.md create mode 100644 docs/status/M14-04C.md create mode 100644 docs/status/M14-04D.md create mode 100644 docs/status/M14-04E.md create mode 100644 docs/tasks/M13-04F.md create mode 100644 docs/tasks/M13-04G.md create mode 100644 docs/tasks/M13-04H.md create mode 100644 docs/tasks/M13-04I.md create mode 100644 docs/tasks/M13-04J.md create mode 100644 docs/tasks/M13-05A.md create mode 100644 docs/tasks/M13-05B.md create mode 100644 docs/tasks/M13-05C.md create mode 100644 docs/tasks/M13-05D.md create mode 100644 docs/tasks/M13-05E.md create mode 100644 docs/tasks/M13-05F.md create mode 100644 docs/tasks/M13-05G.md create mode 100644 docs/tasks/M13-05H.md create mode 100644 docs/tasks/M14-01A.md create mode 100644 docs/tasks/M14-01B.md create mode 100644 docs/tasks/M14-01C.md create mode 100644 docs/tasks/M14-01D.md create mode 100644 docs/tasks/M14-01E.md create mode 100644 docs/tasks/M14-04A.md create mode 100644 docs/tasks/M14-04B.md create mode 100644 docs/tasks/M14-04C.md create mode 100644 docs/tasks/M14-04D.md create mode 100644 docs/tasks/M14-04E.md create mode 100644 docs/tasks/M14-04F.md create mode 100644 docs/web/dependency-severity-policy.json create mode 100644 tests/files/web/archive-security/manifest.json create mode 100644 tests/files/web/archive-security/tar-path-traversal.tar create mode 100644 tests/files/web/archive-security/tar-prefix-conflict.tar create mode 100644 tests/files/web/archive-security/tar-symlink-escape.tar create mode 100644 tests/files/web/archive-security/zip-compression-bomb.zip create mode 100644 tests/files/web/archive-security/zip-duplicate-path.zip create mode 100644 tests/files/web/archive-security/zip-path-traversal.zip create mode 100644 tests/files/web/m11_render_reference.blend1 create mode 100644 tests/files/web/m12_glb_desktop_v1/animation.glb create mode 100644 tests/files/web/m12_glb_desktop_v1/mesh.glb create mode 100644 tests/files/web/m12_glb_desktop_v1/pbr.glb create mode 100644 tests/files/web/m12_glb_desktop_v1/skin.glb create mode 100644 tests/files/web/m12_glb_desktop_v1/uv.glb create mode 100644 tests/files/web/m12_glb_main_v1/animation.blend create mode 100644 tests/files/web/m12_glb_main_v1/animation.blend1 create mode 100644 tests/files/web/m12_glb_main_v1/mesh.blend create mode 100644 tests/files/web/m12_glb_main_v1/mesh.blend1 create mode 100644 tests/files/web/m12_glb_main_v1/pbr.blend create mode 100644 tests/files/web/m12_glb_main_v1/pbr.blend1 create mode 100644 tests/files/web/m12_glb_main_v1/skin.blend create mode 100644 tests/files/web/m12_glb_main_v1/skin.blend1 create mode 100644 tests/files/web/m12_glb_main_v1/uv.blend create mode 100644 tests/files/web/m12_glb_main_v1/uv.blend1 create mode 100644 tests/files/web/m12_glb_web_v1/animation.glb create mode 100644 tests/files/web/m12_glb_web_v1/pbr.glb create mode 100644 tests/files/web/m12_glb_web_v1/skin.glb create mode 100644 tests/files/web/m12_glb_web_v1/uv.glb create mode 100644 tests/files/web/m12_library_link_v1/m12_link_source.blend create mode 100644 tests/files/web/m12_library_link_v1/m12_link_target.blend create mode 100644 tests/files/web/m12_library_override_v1/m12_override_source.blend create mode 100644 tests/files/web/m12_library_override_v1/m12_override_target.blend create mode 100644 tests/files/web/m12_obj_desktop_v1/single-mesh.mtl create mode 100644 tests/files/web/m12_obj_desktop_v1/single-mesh.obj create mode 100644 tests/files/web/m12_obj_multi_v1/m12_obj_texture.png create mode 100644 tests/files/web/m12_obj_multi_v1/malformed-face.obj create mode 100644 tests/files/web/m12_obj_multi_v1/multi-object.mtl create mode 100644 tests/files/web/m12_obj_multi_v1/multi-object.obj create mode 100644 tests/files/web/m12_obj_multi_v1/negative-index.obj create mode 100644 tests/files/web/m12_ply_capability_v1/capability-ascii.ply create mode 100644 tests/files/web/m12_ply_capability_v1/capability-binary-le.ply create mode 100644 tests/files/web/m12_ply_mapping_v1/mapping-ascii.ply create mode 100644 tests/files/web/m12_ply_mapping_v1/mapping-binary-le.ply create mode 100644 tests/files/web/m12_ply_mapping_v1/unknown-property-ascii.ply create mode 100644 tests/files/web/m12_ply_negative_v1/big-endian.ply create mode 100644 tests/files/web/m12_ply_negative_v1/malformed-list-ascii.ply create mode 100644 tests/files/web/m12_ply_negative_v1/oversized-count-ascii.ply create mode 100644 tests/files/web/m12_stl_capability_v1/capability-ascii.stl create mode 100644 tests/files/web/m12_stl_capability_v1/capability-binary.stl create mode 100644 tests/files/web/m12_stl_edges_v1/capability-binary.stl create mode 100644 tests/files/web/m12_stl_edges_v1/degenerate-binary.stl create mode 100644 tests/files/web/m12_stl_edges_v1/trailing-binary.stl create mode 100644 tests/files/web/m13_malicious_script_v1/malicious-script.blend create mode 100644 tests/files/web/m13_script_entry_v1/script-entry-inventory.blend create mode 100644 tests/golden/M12-03E/manifest.json create mode 100644 tests/golden/M12-03F/desktop-link-report.json create mode 100644 tests/golden/M12-03F/manifest.json create mode 100644 tests/golden/M12-03G/manifest.json create mode 100644 tests/golden/M12-03H/manifest.json create mode 100644 tests/golden/M12-03I/manifest.json create mode 100644 tests/golden/M12-03J/desktop-override-report.json create mode 100644 tests/golden/M12-03J/manifest.json create mode 100644 tests/golden/M12-03K/manifest.json create mode 100644 tests/golden/M12-03L/manifest.json create mode 100644 tests/golden/M12-03M/manifest.json create mode 100644 tests/golden/M12-03N/manifest.json create mode 100644 tests/golden/M12-04A/manifest.json create mode 100644 tests/golden/M12-04B/manifest.json create mode 100644 tests/golden/M12-04C/manifest.json create mode 100644 tests/golden/M12-04D/manifest.json create mode 100644 tests/golden/M12-04E/manifest.json create mode 100644 tests/golden/M12-04F/manifest.json create mode 100644 tests/golden/M12-04G/manifest.json create mode 100644 tests/golden/M12-04H/manifest.json create mode 100644 tests/golden/M12-04I/manifest.json create mode 100644 tests/golden/M12-04J/manifest.json create mode 100644 tests/golden/M12-05A/format-inventory.json create mode 100644 tests/golden/M12-05A/manifest.json create mode 100644 tests/golden/M12-05B/capability-matrix.json create mode 100644 tests/golden/M12-05B/manifest.json create mode 100644 tests/golden/M12-05C/manifest.json create mode 100644 tests/golden/M12-05D/manifest.json create mode 100644 tests/golden/M12-05D/runtime-receipts.json create mode 100644 tests/golden/M12-05E/bound-runtime-receipts.json create mode 100644 tests/golden/M12-05E/manifest.json create mode 100644 tests/golden/M12-05F/fresh-runtime-receipts.json create mode 100644 tests/golden/M12-05F/manifest.json create mode 100644 tests/golden/M12-06A/desktop-fixtures.json create mode 100644 tests/golden/M12-06A/manifest.json create mode 100644 tests/golden/M12-06B/manifest.json create mode 100644 tests/golden/M12-06B/web-import-report.json create mode 100644 tests/golden/M12-06C/desktop-main-report.json create mode 100644 tests/golden/M12-06C/manifest.json create mode 100644 tests/golden/M12-06D/manifest.json create mode 100644 tests/golden/M12-06D/web-loss-report.json create mode 100644 tests/golden/M12-06E/desktop-reimport-report.json create mode 100644 tests/golden/M12-06E/manifest.json create mode 100644 tests/golden/M12-06F/manifest.json create mode 100644 tests/golden/M12-06F/negative-report.json create mode 100644 tests/golden/M12-06G/manifest.json create mode 100644 tests/golden/M12-06G/recovery-report.json create mode 100644 tests/golden/M12-07A/desktop-fixture.json create mode 100644 tests/golden/M12-07A/manifest.json create mode 100644 tests/golden/M12-07B/desktop-fixtures.json create mode 100644 tests/golden/M12-07B/manifest.json create mode 100644 tests/golden/M12-07C/manifest.json create mode 100644 tests/golden/M12-07C/web-roundtrip-report.json create mode 100644 tests/golden/M12-07D/capability-report.json create mode 100644 tests/golden/M12-07D/manifest.json create mode 100644 tests/golden/M12-07E/desktop-edge-report.json create mode 100644 tests/golden/M12-07E/edge-fixtures.json create mode 100644 tests/golden/M12-07E/manifest.json create mode 100644 tests/golden/M12-07E/web-edge-report.json create mode 100644 tests/golden/M12-07F/manifest.json create mode 100644 tests/golden/M12-07F/web-roundtrip-report.json create mode 100644 tests/golden/M12-07G/capability-report.json create mode 100644 tests/golden/M12-07G/manifest.json create mode 100644 tests/golden/M12-07H/manifest.json create mode 100644 tests/golden/M12-07H/mapping-report.json create mode 100644 tests/golden/M12-07H/web-roundtrip-report.json create mode 100644 tests/golden/M12-07I/manifest.json create mode 100644 tests/golden/M12-07I/negative-report.json create mode 100644 tests/golden/M12-07J/io-format-recovery-report.json create mode 100644 tests/golden/M12-07J/manifest.json create mode 100644 tests/golden/M13-01A/entry-inventory.json create mode 100644 tests/golden/M13-01A/manifest.json create mode 100644 tests/golden/M13-01B/manifest.json create mode 100644 tests/golden/M13-01B/open-metadata-report.json create mode 100644 tests/golden/M13-01C/manifest.json create mode 100644 tests/golden/M13-01C/policy-codes-report.json create mode 100644 tests/golden/M13-01D/manifest.json create mode 100644 tests/golden/M13-01D/ui-bypass-report.json create mode 100644 tests/golden/M13-01E/manifest.json create mode 100644 tests/golden/M13-01E/script-save-reopen-report.json create mode 100644 tests/golden/M13-01F/malicious-report.json create mode 100644 tests/golden/M13-01F/manifest.json create mode 100644 tests/golden/M13-02A/manifest-budget-report.json create mode 100644 tests/golden/M13-02A/manifest.json create mode 100644 tests/golden/M13-02B/manifest-canonical-report.json create mode 100644 tests/golden/M13-02B/manifest.json create mode 100644 tests/golden/M13-02C/manifest.json create mode 100644 tests/golden/M13-02C/trust-policy-report.json create mode 100644 tests/golden/M13-02D/manifest.json create mode 100644 tests/golden/M13-02D/signature-report.json create mode 100644 tests/golden/M13-02E/manifest.json create mode 100644 tests/golden/M13-02E/permission-policy-report.json create mode 100644 tests/golden/M13-02F/manifest.json create mode 100644 tests/golden/M13-02F/signature-negative-report.json create mode 100644 tests/golden/M13-03A/manifest.json create mode 100644 tests/golden/M13-03A/sandbox-scope-report.json create mode 100644 tests/golden/M13-03B/manifest.json create mode 100644 tests/golden/M13-03B/sandbox-budget-report.json create mode 100644 tests/golden/M13-03C/host-call-report.json create mode 100644 tests/golden/M13-03C/manifest.json create mode 100644 tests/golden/M13-03D/manifest.json create mode 100644 tests/golden/M13-03D/sandbox-isolation-report.json create mode 100644 tests/golden/M13-03E/manifest.json create mode 100644 tests/golden/M13-03E/sandbox-cancellation-report.json create mode 100644 tests/golden/M13-03F/manifest.json create mode 100644 tests/golden/M13-03F/sandbox-dispose-report.json create mode 100644 tests/golden/M13-03G/manifest.json create mode 100644 tests/golden/M13-03G/sandbox-recovery-report.json create mode 100644 tests/golden/M13-04A/manifest.json create mode 100644 tests/golden/M13-04A/server-job-directory-report.json create mode 100644 tests/golden/M13-04B/manifest.json create mode 100644 tests/golden/M13-04B/server-job-workspace-report.json create mode 100644 tests/golden/M13-04C/manifest.json create mode 100644 tests/golden/M13-04C/server-job-resource-budget-report.json create mode 100644 tests/golden/M13-04D/manifest.json create mode 100644 tests/golden/M13-04D/server-job-network-policy-report.json create mode 100644 tests/golden/M13-04E/manifest.json create mode 100644 tests/golden/M13-04E/server-job-startup-report.json create mode 100644 tests/golden/M13-04F/manifest.json create mode 100644 tests/golden/M13-04F/server-job-output-report.json create mode 100644 tests/golden/M13-04G/manifest.json create mode 100644 tests/golden/M13-04G/server-job-cancellation-report.json create mode 100644 tests/golden/M13-04H/manifest.json create mode 100644 tests/golden/M13-04H/server-job-fault-report.json create mode 100644 tests/golden/M13-04I/manifest.json create mode 100644 tests/golden/M13-04I/server-job-result-report.json create mode 100644 tests/golden/M13-04J/manifest.json create mode 100644 tests/golden/M13-04J/server-job-idempotency-report.json create mode 100644 tests/golden/M13-05A/csp-report.json create mode 100644 tests/golden/M13-05A/manifest.json create mode 100644 tests/golden/M13-05B/csp-resource-report.json create mode 100644 tests/golden/M13-05B/manifest.json create mode 100644 tests/golden/M13-05C/dependency-inventory.json create mode 100644 tests/golden/M13-05C/manifest.json create mode 100644 tests/golden/M13-05D/dependency-severity-report.json create mode 100644 tests/golden/M13-05D/manifest.json create mode 100644 tests/golden/M13-05E/manifest.json create mode 100644 tests/golden/M13-05E/supply-chain-report.json create mode 100644 tests/golden/M13-05F/malicious-input-report.json create mode 100644 tests/golden/M13-05F/manifest.json create mode 100644 tests/golden/M13-05G/fuzz-report.json create mode 100644 tests/golden/M13-05G/manifest.json create mode 100644 tests/golden/M13-05H/manifest.json create mode 100644 tests/golden/M13-05H/script-audit-integrity-report.json create mode 100644 tests/golden/M14-01A/chromium-freeze-report.json create mode 100644 tests/golden/M14-01A/manifest.json create mode 100644 tests/golden/M14-01B/firefox-capability-report.json create mode 100644 tests/golden/M14-01B/manifest.json create mode 100644 tests/golden/M14-01C/manifest.json create mode 100644 tests/golden/M14-01C/webkit-capability-report.json create mode 100644 tests/golden/M14-01D/manifest.json create mode 100644 tests/golden/M14-01D/probe-identity-report.json create mode 100644 tests/golden/M14-01E/chromium-webgpu-boundary-report.json create mode 100644 tests/golden/M14-01E/manifest.json create mode 100644 tests/golden/M14-04A/chromium-device-budget-report.json create mode 100644 tests/golden/M14-04A/manifest.json create mode 100644 tests/golden/M14-04B/chromium-dpr-report.json create mode 100644 tests/golden/M14-04B/manifest.json create mode 100644 tests/golden/M14-04C/chromium-pointer-report.json create mode 100644 tests/golden/M14-04C/manifest.json create mode 100644 tests/golden/M14-04D/chromium-ime-report.json create mode 100644 tests/golden/M14-04D/manifest.json create mode 100644 tests/golden/M14-04E/chromium-keymap-report.json create mode 100644 tests/golden/M14-04E/manifest.json create mode 100644 tests/golden/M14-04F/chromium-input-modal-report.json create mode 100644 tests/golden/M14-04F/manifest.json create mode 100644 tools/web/check-chromium-device-budget.mjs create mode 100644 tools/web/check-chromium-dpr-consistency.mjs create mode 100644 tools/web/check-chromium-ime-guard.mjs create mode 100644 tools/web/check-chromium-input-modal.mjs create mode 100644 tools/web/check-chromium-keymap-fixture.mjs create mode 100644 tools/web/check-chromium-pointer-contract.mjs create mode 100644 tools/web/check-chromium-release-freeze.mjs create mode 100644 tools/web/check-chromium-webgpu-boundary.mjs create mode 100644 tools/web/check-csp-policy.mjs create mode 100644 tools/web/check-csp-resource-policy.mjs create mode 100644 tools/web/check-dependency-inventory.mjs create mode 100644 tools/web/check-dependency-severity-policy.mjs create mode 100644 tools/web/check-firefox-capability.mjs create mode 100644 tools/web/check-fuzz-regression.mjs create mode 100644 tools/web/check-glb-desktop-fixtures.mjs create mode 100644 tools/web/check-glb-desktop-import.mjs create mode 100644 tools/web/check-glb-loss-report.mjs create mode 100644 tools/web/check-glb-main-persistence.mjs create mode 100644 tools/web/check-glb-negative-cases.mjs create mode 100644 tools/web/check-glb-recovery.mjs create mode 100644 tools/web/check-glb-web-reimport.mjs create mode 100644 tools/web/check-io-format-capability-matrix.mjs create mode 100644 tools/web/check-io-format-receipt-bindings.mjs create mode 100644 tools/web/check-io-format-receipt-freshness.mjs create mode 100644 tools/web/check-io-format-recovery.mjs create mode 100644 tools/web/check-io-format-runtime-inventory.mjs create mode 100644 tools/web/check-io-format-runtime-receipts.mjs create mode 100644 tools/web/check-io-format-ui-gate.mjs create mode 100644 tools/web/check-library-link-fixture.mjs create mode 100644 tools/web/check-library-main-append.mjs create mode 100644 tools/web/check-library-operation-commands.mjs create mode 100644 tools/web/check-library-override-fixture.mjs create mode 100644 tools/web/check-malicious-archive-fixtures.mjs create mode 100644 tools/web/check-malicious-input-matrix.mjs create mode 100644 tools/web/check-malicious-script-fixture.mjs create mode 100644 tools/web/check-obj-multi-negative-fixtures.mjs create mode 100644 tools/web/check-obj-single-mesh-fixture.mjs create mode 100644 tools/web/check-obj-web-roundtrip.mjs create mode 100644 tools/web/check-obj-web-roundtrip.py create mode 100644 tools/web/check-ply-capability-fixtures.mjs create mode 100644 tools/web/check-ply-mapping-fixtures.mjs create mode 100644 tools/web/check-ply-negative-fixtures.mjs create mode 100644 tools/web/check-ply-web-roundtrip.py create mode 100644 tools/web/check-probe-identity.mjs create mode 100644 tools/web/check-script-audit-integrity.mjs create mode 100644 tools/web/check-script-entry-inventory.mjs create mode 100644 tools/web/check-script-host-call.mjs create mode 100644 tools/web/check-script-manifest-budgets.mjs create mode 100644 tools/web/check-script-manifest-canonical.mjs create mode 100644 tools/web/check-script-open-metadata.mjs create mode 100644 tools/web/check-script-permission-policy.mjs create mode 100644 tools/web/check-script-policy-codes.mjs create mode 100644 tools/web/check-script-sandbox-budget.mjs create mode 100644 tools/web/check-script-sandbox-cancellation.mjs create mode 100644 tools/web/check-script-sandbox-dispose.mjs create mode 100644 tools/web/check-script-sandbox-isolation.mjs create mode 100644 tools/web/check-script-sandbox-recovery.mjs create mode 100644 tools/web/check-script-sandbox-scope.mjs create mode 100644 tools/web/check-script-save-reopen.mjs create mode 100644 tools/web/check-script-signature-negative.mjs create mode 100644 tools/web/check-script-signature.mjs create mode 100644 tools/web/check-script-trust-policy.mjs create mode 100644 tools/web/check-script-ui-bypass.mjs create mode 100644 tools/web/check-server-job-cancellation.mjs create mode 100644 tools/web/check-server-job-fault-codes.mjs create mode 100644 tools/web/check-server-job-idempotency.mjs create mode 100644 tools/web/check-server-job-isolation.mjs create mode 100644 tools/web/check-server-job-network-policy.mjs create mode 100644 tools/web/check-server-job-output-redaction.mjs create mode 100644 tools/web/check-server-job-resource-budget.mjs create mode 100644 tools/web/check-server-job-result-binding.mjs create mode 100644 tools/web/check-server-job-startup.mjs create mode 100644 tools/web/check-server-job-workspace.mjs create mode 100644 tools/web/check-stl-capability-fixtures.mjs create mode 100644 tools/web/check-stl-edge-parity.mjs create mode 100644 tools/web/check-stl-web-roundtrip.mjs create mode 100644 tools/web/check-stl-web-roundtrip.py create mode 100644 tools/web/check-supply-chain-binding.mjs create mode 100644 tools/web/check-webkit-capability.mjs create mode 100644 tools/web/fuzz-case-runner.mjs create mode 100644 tools/web/generate-dependency-inventory.mjs create mode 100644 tools/web/generate-glb-desktop-fixtures.py create mode 100644 tools/web/generate-glb-desktop-import-report.mjs create mode 100644 tools/web/generate-glb-main-persistence-fixtures.py create mode 100644 tools/web/generate-glb-web-reimport-report.py create mode 100644 tools/web/generate-io-format-capability-matrix.mjs create mode 100644 tools/web/generate-io-format-receipt-bindings.mjs create mode 100644 tools/web/generate-io-format-receipt-freshness.mjs create mode 100644 tools/web/generate-io-format-runtime-inventory.py create mode 100644 tools/web/generate-io-format-runtime-receipts.mjs create mode 100644 tools/web/generate-io-format-ui-gate.mjs create mode 100644 tools/web/generate-library-link-fixture.py create mode 100644 tools/web/generate-library-override-fixture.py create mode 100644 tools/web/generate-malicious-archive-fixtures.mjs create mode 100644 tools/web/generate-malicious-script-fixture.py create mode 100644 tools/web/generate-obj-multi-negative-fixtures.py create mode 100644 tools/web/generate-obj-single-mesh-fixture.py create mode 100644 tools/web/generate-ply-capability-fixtures.py create mode 100644 tools/web/generate-ply-mapping-fixtures.py create mode 100644 tools/web/generate-ply-negative-fixtures.mjs create mode 100644 tools/web/generate-script-entry-inventory.py create mode 100644 tools/web/generate-stl-capability-fixtures.py create mode 100644 tools/web/generate-stl-edge-fixtures.mjs create mode 100644 tools/web/probe-stl-edge-fixtures.py create mode 100644 tools/web/server-job-fault.mjs create mode 100644 tools/web/server-job-idempotency.mjs create mode 100644 tools/web/server-job-isolation.mjs create mode 100644 tools/web/server-job-network-policy.mjs create mode 100644 tools/web/server-job-output.mjs create mode 100644 tools/web/server-job-process.mjs create mode 100644 tools/web/server-job-resource-budget.mjs create mode 100644 tools/web/server-job-result-binding.mjs create mode 100644 tools/web/server-job-startup.py create mode 100644 web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json create mode 100644 web/app/src/capabilities/io-format-runtime-receipts-freshness.json create mode 100644 web/app/src/capabilities/io-format-runtime-receipts.json create mode 100644 web/app/src/capabilities/io-format-ui-registry.json create mode 100644 web/app/src/library-link-chromium.ts create mode 100644 web/app/src/library-override-chromium.ts create mode 100644 web/app/src/testing/glb-recovery.ts create mode 100644 web/app/src/testing/io-format-recovery.ts create mode 100644 web/app/src/testing/script-sandbox-dispose.ts create mode 100644 web/app/src/testing/script-sandbox-recovery.ts create mode 100644 web/app/src/workers/glb-desktop-import-test.worker.ts create mode 100644 web/app/src/workers/glb-loss-report-test.worker.ts create mode 100644 web/app/src/workers/glb-negative-cases-test.worker.ts create mode 100644 web/app/src/workers/glb-recovery-test.worker.ts create mode 100644 web/app/src/workers/io-format-recovery-test.worker.ts create mode 100644 web/app/src/workers/obj-roundtrip-test.worker.ts create mode 100644 web/app/src/workers/ply-roundtrip-test.worker.ts create mode 100644 web/app/src/workers/script-host-call-test.worker.ts create mode 100644 web/app/src/workers/script-permission-policy-test.worker.ts create mode 100644 web/app/src/workers/script-sandbox-budget-test.worker.ts create mode 100644 web/app/src/workers/script-sandbox-cancellation-test.worker.ts create mode 100644 web/app/src/workers/script-sandbox-dispose-test.worker.ts create mode 100644 web/app/src/workers/script-sandbox-isolation-test.worker.ts create mode 100644 web/app/src/workers/script-sandbox-recovery-test.worker.ts create mode 100644 web/app/src/workers/script-sandbox-scope-test.worker.ts create mode 100644 web/app/src/workers/script-signature-negative-test.worker.ts create mode 100644 web/app/src/workers/script-signature-test.worker.ts create mode 100644 web/app/src/workers/script-trust-policy-test.worker.ts create mode 100644 web/app/src/workers/scripting-manifest-budget-test.worker.ts create mode 100644 web/app/src/workers/scripting-manifest-canonical-test.worker.ts create mode 100644 web/app/src/workers/stl-edge-test.worker.ts create mode 100644 web/app/src/workers/stl-roundtrip-test.worker.ts create mode 100644 web/protocol/archive-conflicts.ts create mode 100644 web/protocol/archive-extraction-recovery.ts create mode 100644 web/protocol/archive-extraction-transaction.ts create mode 100644 web/protocol/archive-link-safety.ts create mode 100644 web/protocol/archive-metadata-first.ts create mode 100644 web/protocol/device-budget.ts create mode 100644 web/protocol/glb-loss-report.ts create mode 100644 web/protocol/glb-recovery.ts create mode 100644 web/protocol/ime-composition.ts create mode 100644 web/protocol/input-modal.ts create mode 100644 web/protocol/io-format-capability-matrix.ts create mode 100644 web/protocol/io-format-receipt-binding.ts create mode 100644 web/protocol/io-format-receipt-freshness.ts create mode 100644 web/protocol/io-format-recovery.ts create mode 100644 web/protocol/io-format-runtime-receipt.ts create mode 100644 web/protocol/io-format-ui-gate.ts create mode 100644 web/protocol/keyboard-contract.ts create mode 100644 web/protocol/library-linked-missing.ts create mode 100644 web/protocol/library-linked-mutation.ts create mode 100644 web/protocol/library-linked-reload.ts create mode 100644 web/protocol/library-negative-cases.ts create mode 100644 web/protocol/library-override-freshness.ts create mode 100644 web/protocol/library-override-writer.ts create mode 100644 web/protocol/library-source-origin.ts create mode 100644 web/protocol/obj-import.ts create mode 100644 web/protocol/ply-import.ts create mode 100644 web/protocol/pointer-contract.ts create mode 100644 web/protocol/stl-export.ts create mode 100644 web/protocol/stl-import.ts create mode 100644 web/protocol/viewport-dpr.ts create mode 100644 web/tests/e2e/archive-security-fixtures.spec.ts create mode 100644 web/tests/e2e/glb-desktop-import.spec.ts create mode 100644 web/tests/e2e/glb-export-loss-report.spec.ts create mode 100644 web/tests/e2e/glb-main-persistence.spec.ts create mode 100644 web/tests/e2e/glb-negative-cases.spec.ts create mode 100644 web/tests/e2e/glb-recovery.spec.ts create mode 100644 web/tests/e2e/io-format-recovery.spec.ts create mode 100644 web/tests/e2e/io-format-ui-gate.spec.ts create mode 100644 web/tests/e2e/library-link-chromium.spec.ts create mode 100644 web/tests/e2e/library-override-chromium.spec.ts create mode 100644 web/tests/e2e/malicious-script.spec.ts create mode 100644 web/tests/e2e/obj-web-roundtrip.spec.ts create mode 100644 web/tests/e2e/ply-negative.spec.ts create mode 100644 web/tests/e2e/ply-web-roundtrip.spec.ts create mode 100644 web/tests/e2e/script-host-call.spec.ts create mode 100644 web/tests/e2e/script-manifest-budgets.spec.ts create mode 100644 web/tests/e2e/script-manifest-canonical.spec.ts create mode 100644 web/tests/e2e/script-open-metadata.spec.ts create mode 100644 web/tests/e2e/script-permission-policy.spec.ts create mode 100644 web/tests/e2e/script-sandbox-budget.spec.ts create mode 100644 web/tests/e2e/script-sandbox-cancellation.spec.ts create mode 100644 web/tests/e2e/script-sandbox-dispose.spec.ts create mode 100644 web/tests/e2e/script-sandbox-isolation.spec.ts create mode 100644 web/tests/e2e/script-sandbox-recovery.spec.ts create mode 100644 web/tests/e2e/script-sandbox-scope.spec.ts create mode 100644 web/tests/e2e/script-save-reopen.spec.ts create mode 100644 web/tests/e2e/script-signature-negative.spec.ts create mode 100644 web/tests/e2e/script-signature.spec.ts create mode 100644 web/tests/e2e/script-trust-policy.spec.ts create mode 100644 web/tests/e2e/stl-edge-parity.spec.ts create mode 100644 web/tests/e2e/stl-web-roundtrip.spec.ts create mode 100644 web/tests/unit/device-budget.test.mjs create mode 100644 web/tests/unit/glb-desktop-import.test.mjs create mode 100644 web/tests/unit/glb-loss-report.test.mjs create mode 100644 web/tests/unit/glb-negative-cases.test.mjs create mode 100644 web/tests/unit/glb-recovery.test.mjs create mode 100644 web/tests/unit/ime-composition.test.mjs create mode 100644 web/tests/unit/input-modal.test.mjs create mode 100644 web/tests/unit/io-format-capability-matrix.test.mjs create mode 100644 web/tests/unit/io-format-receipt-binding.test.mjs create mode 100644 web/tests/unit/io-format-receipt-freshness.test.mjs create mode 100644 web/tests/unit/io-format-recovery.test.mjs create mode 100644 web/tests/unit/io-format-runtime-receipt.test.mjs create mode 100644 web/tests/unit/io-format-ui-gate.test.mjs create mode 100644 web/tests/unit/keyboard-contract.test.mjs create mode 100644 web/tests/unit/library-append-wasm.test.mjs create mode 100644 web/tests/unit/library-archive-budget.test.mjs create mode 100644 web/tests/unit/library-archive-cancellation.test.mjs create mode 100644 web/tests/unit/library-archive-conflicts.test.mjs create mode 100644 web/tests/unit/library-archive-recovery.test.mjs create mode 100644 web/tests/unit/library-link-safety.test.mjs create mode 100644 web/tests/unit/library-linked-missing.test.mjs create mode 100644 web/tests/unit/library-linked-mutation.test.mjs create mode 100644 web/tests/unit/library-linked-reload.test.mjs create mode 100644 web/tests/unit/library-metadata-first.test.mjs create mode 100644 web/tests/unit/library-negative-cases.test.mjs create mode 100644 web/tests/unit/library-override-freshness.test.mjs create mode 100644 web/tests/unit/library-override-writer.test.mjs create mode 100644 web/tests/unit/library-path-normalization.test.mjs create mode 100644 web/tests/unit/library-path-security.test.mjs create mode 100644 web/tests/unit/library-source-origin.test.mjs create mode 100644 web/tests/unit/obj-import.test.mjs create mode 100644 web/tests/unit/ply-import.test.mjs create mode 100644 web/tests/unit/ply-negative.test.mjs create mode 100644 web/tests/unit/pointer-contract.test.mjs create mode 100644 web/tests/unit/script-audit-integrity.test.mjs create mode 100644 web/tests/unit/script-host-call.test.mjs create mode 100644 web/tests/unit/script-manifest-budgets.test.mjs create mode 100644 web/tests/unit/script-manifest-canonical.test.mjs create mode 100644 web/tests/unit/script-permission-policy.test.mjs create mode 100644 web/tests/unit/script-policy-codes.test.mjs create mode 100644 web/tests/unit/script-sandbox-budget.test.mjs create mode 100644 web/tests/unit/script-sandbox-cancellation.test.mjs create mode 100644 web/tests/unit/script-sandbox-dispose.test.mjs create mode 100644 web/tests/unit/script-sandbox-isolation.test.mjs create mode 100644 web/tests/unit/script-sandbox-recovery.test.mjs create mode 100644 web/tests/unit/script-sandbox-scope.test.mjs create mode 100644 web/tests/unit/script-signature-negative.test.mjs create mode 100644 web/tests/unit/script-trust-policy.test.mjs create mode 100644 web/tests/unit/server-job-fault.test.mjs create mode 100644 web/tests/unit/server-job-idempotency.test.mjs create mode 100644 web/tests/unit/server-job-isolation.test.mjs create mode 100644 web/tests/unit/server-job-network-policy.test.mjs create mode 100644 web/tests/unit/server-job-output.test.mjs create mode 100644 web/tests/unit/server-job-process.test.mjs create mode 100644 web/tests/unit/server-job-resource-budget.test.mjs create mode 100644 web/tests/unit/server-job-result-binding.test.mjs create mode 100644 web/tests/unit/stl-export.test.mjs create mode 100644 web/tests/unit/stl-import.test.mjs create mode 100644 web/tests/unit/viewport-dpr.test.mjs diff --git a/README.md b/README.md index 84e7bf3a..6bd05e40 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,10 @@ storage, and desktop/WASM round-trip tests. The current browser baseline is Chromium only. The release browser suite covers both the main-thread WebGL renderer and the OffscreenCanvas Worker renderer. -Firefox and WebKit are intentionally outside the current test and release scope. +Firefox and WebKit are permanently outside this project's test, CI, evidence, and release scope. +The iron rule is Chromium-only browser execution: never launch, probe, or claim support from +Firefox or WebKit. Historical Firefox/WebKit reports are archival context only and are not normative +support evidence. ## Layout @@ -29,7 +32,7 @@ npm --prefix web run build npm --prefix web run test:browser ``` -See `docs/CURRENT_EXECUTION_PLAN.md` for the current task order, +See `docs/EXECUTION_QUEUE.md` for the current task order and `docs/README.md` for the document map, `docs/PROJECT_STATUS_AND_NEXT_WORK.md` for the implemented scope, and `docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` for the normative M12-M23 full-parity execution plan. The domain overview and coverage cross-check are in diff --git a/docs/BLENDER_5_2_FULL_PARITY_WBS.md b/docs/BLENDER_5_2_FULL_PARITY_WBS.md index 15b58001..38ccdd8f 100644 --- a/docs/BLENDER_5_2_FULL_PARITY_WBS.md +++ b/docs/BLENDER_5_2_FULL_PARITY_WBS.md @@ -1,6 +1,6 @@ # Blender 5.2 全功能对标工作分解 -更新时间:2026-08-15 +更新时间:2026-08-18 > 本文保留为 Blender 全产品覆盖检查表和 F00-F24 taxonomy 参考。M12-M23 的执行分类、 > 原子任务、证据合同与发布门以 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准; @@ -17,7 +17,7 @@ V1 盘点术语;完整对标结论必须按新计划转换为 `LOCAL_EXACT/LOC N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此以下任务默认均为未完成; 只有 `docs/status/parity-ledger.json` 和对应专项证据同时更新后才允许改变状态。 -短周期唯一领取顺序仍以 `docs/CURRENT_EXECUTION_PLAN.md` 为准。本文负责验证新计划没有遗漏 +短周期唯一领取顺序仍以 `docs/EXECUTION_QUEUE.md` 和对应任务卡为准。本文负责验证新计划没有遗漏 产品域;与新计划冲突时按新计划解释,不取代当前执行队列或长期实施事实源。 ## 2. 原子任务完成定义 @@ -516,12 +516,39 @@ N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此 ## 29. 当前领取点 -截至 2026-08-17,M6-M11 已按当前计划完成,M11 最终以 Render、Compositor、Media 三域故障 +截至 2026-08-18,M6-M11 已按当前计划完成,M11 最终以 Render、Compositor、Media 三域故障 生命周期专项门收口,M12-01A-I 已完成 catalog schema/migration;M12-02A-H 又完成 PreviewImage inventory、identity、decode budget、OPFS commit、dedupe、quarantine、project-scoped reference GC 与 desktop/browser 双显示路径像素闭环。本文的 F00-F24 条目只作为覆盖检查表,不能直接领取;M12-03A 又冻结 Append/Link/Override 的 36 类 root 与 实际 ID pointer dependency closure;M12-03B 固定 source/owner/read-only/invalidation identity; M12-03C 冻结 Blender 5.2 单 Object append 的 Object/Mesh/Material/Image local stable mapping; -M12-03D 已通过 WASM Main 单 transaction 创建同一 local dependency closure。当前唯一下一任务为 -`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的 `M12-03E`。 +M12-03D 已通过 WASM Main 单 transaction 创建同一 local dependency closure,M12-03E 又完成 +undo/redo/save/reopen 与 desktop canonical graph 的联合一致性,M12-03F 又冻结 desktop LINK +source-library/read-only ownership,M12-03G 又完成 linked writer fail-closed gate,M12-03H 又完成 +matching-generation reload,M12-03I 又完成 missing-library placeholder/source preservation,M12-03J 又冻结 +desktop override reference/local-owner/property contract,M12-03K 又开放单一 verified override writer, +M12-03L 又冻结 freshness gate,M12-03M/N 又收口 negative cases 与三 lane 独立命令矩阵。 +M12-04A-J 又冻结 declared source admission、canonical path、path/origin safety、link resolution、 +metadata-first、archive budget/conflict、cancellation rollback、quota/OOM recovery 与恶意 ZIP/TAR +长期回归;M12-05A 又从 pinned Blender 5.2 runtime 生成七格式 operator/build inventory,M12-05C +又将 M12-05B capability matrix SHA-256 绑定到文件选择器与 operator search,M12-05D 又将 +执行路由绑定到 14 条 runtime receipt,M12-05E 又固定每条 receipt 的 source/settings/runtime +三重 hash,M12-05F 又增加 parent byte、canonical receipt-set 和 runtime identity freshness gate, +并对未声明/未注册/未绑定/伪造/过期/跨版本组合保持 fail-closed;M12-06A 又由 pinned Blender +5.2 desktop 生成五个独立 Mesh/PBR/UV/skin/animation GLB fixture,并完成逐字节确定性重生成; +M12-06B 又让生产 Web parser 在 Node 与 Chromium Worker 中精确比较 topology、attributes、 +materials、nodes 和 animations;M12-06C 又完成 desktop GLB import 后的 Main save/reopen 与 +stable-ID 比较;M12-06D 又固定 Web export machine loss report;M12-06E 又完成 desktop Web GLB +re-import canonical comparison;M12-06F 又固定 sparse/extension/URI/budget negative gate; +M12-06G 又完成 GLB import/export 取消、Worker 换代和真实 OPFS quota 恢复;M12-07A 又冻结 +单 Mesh OBJ/MTL position/normal/UV/material-group desktop 正例;M12-07B 又冻结双对象、负索引、 +相对纹理来源和坏 face 负例;M12-07C 又完成 Web-to-desktop OBJ round-trip/loss report; +M12-07D 又分开冻结 STL binary/ASCII capability;M12-07E 又完成 normal/unit/degenerate/trailing +desktop/Web 对标并记录 stricter trailing block;M12-07F 又完成 STL Web-to-desktop round-trip/material +loss report;M12-07G 又分开冻结 PLY ASCII/binary little-endian capability;M12-07H 又完成 +PLY vertex/face/color/custom property mapping 与 unknown-property loss report;M12-07I 又完成 +big-endian/坏 list/超大 count 稳定阻断;M12-07J 又完成 OBJ/STL/PLY 三格式 recovery;M13-01A 又完成脚本 +入口 inventory;M13-02A 又完成 bounded script manifest limits;M13-02B 又完成 canonical +serialization signature input;M13-02C 又完成 signer identity/key rotation/revocation/timestamp policy;M13-02D-F 又完成签名、权限、sandbox scope/budget/host-call、crash/timeout、cancellation 和 dispose 资源归零门。当前唯一下一任务为 +`EXECUTION_QUEUE.md` 中的 `M13-04F`;具体上下文见 `docs/tasks/M13-04F.md`。 diff --git a/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md b/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md index 28b07bba..db124521 100644 --- a/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md +++ b/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md @@ -1,6 +1,6 @@ # Blender 5.2 完整 Web 对标实施计划 -更新时间:2026-08-17 +更新时间:2026-08-18 本文是 M12 及以后完整 Blender 5.2 Web 迁移的长期规范事实源,负责执行分类、原子任务、 依赖、证据和完整发布门。它不覆盖 `WEB_BLENDER_MODELER_V1_SCOPE.md` 的 V1 产品契约,也不 @@ -11,7 +11,7 @@ | 事实 | 权威来源 | | --- | --- | | V1 承诺、非目标和发布标准 | `WEB_BLENDER_MODELER_V1_SCOPE.md` | -| 当前唯一可领取任务 | `CURRENT_EXECUTION_PLAN.md` 与机器 `nextTask` | +| 当前唯一可领取任务 | `EXECUTION_QUEUE.md`、对应 `tasks/.md` 与机器 `nextTask` | | M12-M23 完整对标规则和长期任务 | 本文 | | 当前实现事实和 family/slice 状态 | `status/parity-ledger.json` 与对应 evidence | | Blender 全域功能说明和覆盖参考 | `BLENDER_5_2_WEB_FEATURE_PARITY.md`、`BLENDER_5_2_FULL_PARITY_WBS.md` | @@ -19,8 +19,36 @@ 描述与机器证据冲突时不得用文字把失败改成成功。M12-01A-I 已收口,M12-02A-G preview inventory/identity/decode budget/OPFS commit/dedupe/quarantine/GC 已完成且未改变 parity 状态; desktop/browser 主线程与 Offscreen preview 像素闭环也已完成;Append/Link/Override 数据块与 -依赖闭包 inventory、source/owner/read-only/invalidation 合同、desktop 单 Object append fixture -及 WASM Main 单 transaction append 已完成,当前唯一 `nextTask` 为 `M12-03E`。 +依赖闭包 inventory、source/owner/read-only/invalidation 合同、desktop 单 Object append fixture、 +WASM Main 单 transaction append 及其 undo/redo/save/reopen canonical 比较已完成,desktop LINK +fixture 也已冻结 source-library/read-only ownership,linked writer fail-closed gate、matching-generation +reload、missing-library placeholder preservation、desktop override ownership/property fixture、单一 +verified override writer、freshness gate、三 lane 独立命令矩阵、declared source-origin admission、 +canonical project-path normalization、path/origin security、archive link/metadata/budget/conflict gate、 +cancellation rollback、quota/OOM recovery 与恶意 ZIP/TAR 长期回归已完成;M12-05A 已从 pinned +Blender 5.2 runtime 生成 7-format inventory,M12-05B 已冻结逐格式 local/server capability matrix, +M12-05C 已将 UI 文件选择与 operator search 绑定到该 matrix,M12-05D 又将执行路由绑定到 +M12-05A 的 Blender runtime receipt,M12-05E 又为 14 条 receipt 固定 source/settings/runtime 三重 +hash,M12-05F 又增加 parent byte、canonical receipt-set 和 runtime identity freshness gate;M12-06A +又由 pinned Blender 5.2 desktop 生成 mesh、PBR、UV、skin、animation 五个独立 GLB fixture,并以 +逐字节重生成和 semantic report 固定 fixture group;M12-06B 又由 Node 与 Chromium Worker 对 +topology、attributes、materials、nodes、animations 做 exact canonical 比较;M12-06C 又完成桌面 +GLB 导入后的 authoritative Main 保存、重开、stable-ID 比较和 Chromium 资源释放门;M12-06D 又 +固定 Web export machine loss report;M12-06E 又完成 pinned Blender 对 Web GLB 的 save/reopen +canonical comparison,并将 UV/skin 的明确差异保留为 machine mismatches;M12-06F 又固定 +sparse/extension/外部 URI/budget 负例;M12-06G 再完成双向取消、Worker 换代和真实 OPFS quota +恢复;M12-07A 又由 pinned Blender 5.2 生成 OBJ 单 Mesh position/normal/UV/material-group +正例并完成逐字节重生成;M12-07B 又冻结 OBJ 双对象、负索引、相对纹理来源和坏 face 负例; +M12-07C 又完成 Web Worker OBJ/MTL 重写、纹理 loss report 和 Blender 5.2 重导入比较; +M12-07D 又把同一 Blender Mesh 的 STL binary/ASCII runtime capability 分开冻结;M12-07E 又完成 +normal/unit/degenerate/trailing desktop/Chromium 对标并记录 trailing 的 stricter Web block; +M12-07F 又完成 Web binary STL 重写、Blender 重导入和 material-loss report;M12-07G 又冻结 +Blender 5.2 的 PLY ASCII 与 binary little-endian 两个独立 capability variant;M12-07H 又完成 +PLY vertex/face/color/custom property 映射与未知 property loss report;M12-07I 又完成 +big-endian、坏 list、超大 count 的稳定阻断;M12-07J 又完成 OBJ/STL/PLY 三格式 recovery;M13-01A +又完成脚本入口盘点;M13-01B 又完成 `.blend` 脚本 metadata-only open;M13-01C 又冻结默认拒绝 +policy codes;M13-01D 又通过 UI direct-eval bypass scan;M13-01E 又通过 Text save/reopen;M13-01F +又通过恶意脚本 fixture;M13-02A 又通过 bounded script manifest limits;M13-02B 又固定 canonical serialization signature input;M13-02C 又完成 signer identity/key rotation/revocation/timestamp policy;M13-02D 又完成声明内容 ED25519 验签;M13-02E 又完成权限最小化;M13-02F 又完成签名负例矩阵;M13-03A 又冻结 all-deny sandbox scope;M13-03B 又冻结 CPU/wall/memory/message/output budgets;M13-03C 又冻结结构化 host-call allowlist;M13-03D 又冻结 crash/timeout isolation、Main revision 不变和迟到结果拒绝;M13-03E 又冻结 cancellation no-publish/cache gate;M13-03F 又冻结 Worker/两端 MessagePort/timer/AbortController/buffer/cache reference dispose gate;M13-03G 又冻结同会话 generation/revision/hash-chain recovery;M13-04A 又冻结随机 `0700` one-shot server job directory;M13-04B 又冻结 source/output 权限隔离;M13-04C 又冻结六类资源预算;M13-04D 又冻结默认拒绝网络策略;M13-04E 又冻结 pinned Blender background/factory-startup argv,当前唯一 `nextTask` 为 `M13-04F`。 ## 1. 目标与边界 @@ -42,7 +70,9 @@ WebGPU 或 JavaScript 近似结果不能冒充 Blender 结果。 和项目绑定,不允许只返回“应走服务端”。 6. 完整项目通过保存、关闭、Worker/页面重启、重新打开和 Blender desktop 再打开;未知数据 不丢失,不支持的写操作不污染原文件。 -7. Chromium、Firefox、WebKit 的支持声明分别由自己的 quick/P0/full 证据决定,不互相推断。 +7. 本项目浏览器执行、CI、验收证据和发布声明永久仅限 Chromium。Firefox 与 WebKit 禁止启动、 + 探测、领取测试任务或生成支持证据;历史报告仅作归档背景。缺失 WebGPU 只能由 Chromium + 真实能力门将依赖 WebGPU 的功能标记为 `BLOCKED`,不得用其他浏览器推断。 Native window、CUDA/Metal/HIP/OptiX 实现细节不需要在浏览器复制,但它们提供的用户能力必须 由 WebGPU、CPU 或 `SERVER_EXACT` 路径覆盖。若没有等价执行路径,就保持 `BLOCKED`,项目不得 @@ -101,9 +131,18 @@ Native window、CUDA/Metal/HIP/OptiX 实现细节不需要在浏览器复制, ## 2. 当前基线 - 当前交付物是可审计的 Chromium `Web Blender Modeler V1`,不是完整 Blender。 -- M6 至 M11 已完成;M12-01A-I、M12-02A-G enabling tasks 与 M12-02H preview display - parity slice 及 M12-03A-D library-operation inventory/identity/desktop fixture/WASM Main append - 已完成,当前唯一下一任务为 M12-03E。 +- M6 至 M11 已完成;M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A inventory、 + M12-05B capability matrix、M12-05C UI gate、M12-05D runtime receipt gate、M12-05E receipt + binding、M12-05F freshness gate、M12-06A desktop GLB fixture generation、M12-06B Web semantic import + comparison、M12-06C Main save/reopen stable-ID slice、M12-06D Web machine loss report 和 + M12-06E desktop re-import comparison、M12-06F GLB negative-case budget gate 和 M12-06G + cancellation/Worker restart/OPFS quota recovery、M12-07A OBJ single-Mesh desktop fixture 和 + M12-07B OBJ multi-object/negative/texture-origin fixture、M12-07C Web-to-desktop round-trip 和 + M12-07D split STL binary/ASCII capability、M12-07E normal/unit/edge parity、M12-07F + Web-to-desktop round-trip/loss report、M12-07G PLY ASCII/binary little-endian capability 和 + M12-07H vertex/face/color/custom property mapping/loss report、M12-07I negative admission + gate 和 M12-07J 三格式 recovery 已完成,M13-01A script entry inventory、M13-01B metadata-only open、 + M13-01C default-deny policy codes、M13-01D UI bypass scan、M13-01E Text save/reopen、M13-01F malicious fixture、M13-02A manifest limits、M13-02B canonical serialization、M13-02C signer trust policy、M13-02D declaration-bound signature verification、M13-02E permission minimization、M13-02F signature negative matrix、M13-03A sandbox scope、M13-03B sandbox budgets、M13-03C host-call allowlist、M13-03D crash/timeout isolation、M13-03E cancellation no-publish/cache gate、M13-03F dispose 资源归零、M13-03G 同会话恢复审计链、M13-04A one-shot server job directory、M13-04B source/output 权限隔离、M13-04C 六类资源预算、M13-04D 默认拒绝网络和 M13-04E Blender runtime argv 也已完成,当前唯一下一任务为 M13-04F。 - N-015 至 N-026 的 V1 `releaseStatus` 为 `READY`,但 12 个 family 的全域 `parityStatus` 仍全部为 `BLOCKED`。 - 这 12 个 family 是当前 post-V1 账本范围,不覆盖完整 Blender inventory;不能把它们未来的 @@ -189,8 +228,8 @@ commit、runtime 或 fixture 的局部成功。`parityStatus=VERIFIED` 必须由 | OOM/quota/cancel/crash 后小任务恢复 | 必须 | 必须 | 浏览器与 server 两侧都必须 | | 真实 Blender server | 禁止作为本地成功替代 | 禁止作为等价外壳成功替代 | 必须 | -Firefox/WebKit 不在每个早期原子任务的默认完成门中;M14 声明某浏览器支持后,该浏览器对 -所有已验证 `parityId` 的适用 browser evidence 立即成为强制项,不能由 Chromium 结果代替。 +Firefox/WebKit 永久不在本项目的完成门、CI、支持声明或发布证据中;M14 只允许 Chromium +设备/输入和 Chromium capability evidence。不得通过新增浏览器任务恢复这两个范围。 ### 3.5 比较器 @@ -235,7 +274,7 @@ M12 至 M15 是当前确定队列。M16 至 M22 的具体领取顺序由 M15 生 由一个专项命令串联。 本节已由 `npm --prefix web run test:render-compositor-media-recovery`、`M11-14` status、专项 -evidence 和 family ledger 联合收口。M12-03D 完成后,当前唯一 `nextTask` 为 `M12-03E`。 +evidence 和 family ledger 联合收口。M12-04I 完成后,当前唯一 `nextTask` 为 `M12-04J`。 ## 6. M12 Asset、Library、IO 与 Editor @@ -298,61 +337,127 @@ evidence 和 family ledger 联合收口。M12-03D 完成后,当前唯一 `next - [x] `M12-03D` WASM Main append 以一次 transaction 创建本地 owner 数据;Worker 先校验 source hash、closure 和 revision,Blender Main 递归导入 Object/Mesh/Material/packed Image,四个 ID 均验证为可写 local owner,并只推进一个 SceneIR revision。 -- [ ] `M12-03E` append undo/redo/save/reopen 与 desktop canonical report 一致。 -- [ ] `M12-03F` desktop link fixture 保留 source library 和只读 ownership。 -- [ ] `M12-03G` linked data writer 全部返回 `LINKED_DATA_MUTATION_BLOCKED`。 -- [ ] `M12-03H` library reload 只替换匹配 generation 的 linked snapshot。 -- [ ] `M12-03I` missing library 保留 placeholder 和原始 source,不删除引用。 -- [ ] `M12-03J` desktop override fixture 记录 reference、local owner 和 property override path。 -- [ ] `M12-03K` 首个 override writer 只开放一个已验证属性。 -- [ ] `M12-03L` override stale source/revision 在 Main commit 前阻断。 -- [ ] `M12-03M` dependency cycle、ID collision、跨库环和重复 reload 负例。 -- [ ] `M12-03N` append/link/override 各有独立 desktop/WASM/Chromium 命令。 +- [x] `M12-03E` append undo/redo/save/reopen 与 desktop canonical report 一致;WASM Main 的 + append closure 在一次 transaction 后,undo 移除全部四个 ID,redo 与 save/reopen 均恢复 + desktop canonical graph(图像像素比较先做 Blender bottom-up row normalization;SceneIR 缺省的 + Image.colorSpace 使用 desktop sRGB semantic default,若字段出现则必须匹配)。 +- [x] `M12-03F` desktop link fixture 保留 source library 和只读 ownership;四个 linked ID 均 + 保留 `m12_link_source.blend` library pointer、无 library override,并在 save/reopen 后保持 + `SOURCE_LIBRARY/readOnly=true` stable mapping。 +- [x] `M12-03G` linked data writer 全部返回 `LINKED_DATA_MUTATION_BLOCKED`;object transform、 + mesh geometry/material slot、material property/image node 和 packed image 六类 writer 在 + Main 前统一 fail-closed,stale revision 仍返回 `REVISION_CONFLICT`。 +- [x] `M12-03H` library reload 只替换匹配 generation 的 linked snapshot;stale generation/revision、 + source substitution、duplicate identity 和 non-adjacent replacement 均 fail-closed。 +- [x] `M12-03I` missing library 保留 `MISSING_LIBRARY` placeholder、原始 source locator/SHA、generation、 + revision 和 data-block IDs,不删除引用;stale/source drift 均 fail-closed。 +- [x] `M12-03J` desktop override fixture 记录 source reference、`LOCAL_OVERRIDE` owner、hierarchy root + 和唯一 property override path,并在 save/reopen 后保持稳定。 +- [x] `M12-03K` 首个 override writer 只开放 `["m12_override_value"]`,并保持 `LOCAL_OVERRIDE`/ + reference-read-only semantics;其它 property、owner、identity 和 value 均 fail-closed。 +- [x] `M12-03L` override stale source generation/revision、dependency closure、invalidation token 和 + identity 在 Main commit 前阻断;只有完整匹配返回 `READY`。 +- [x] `M12-03M` dependency cycle、ID collision、跨库环和重复 reload 负例均返回稳定阻断码。 +- [x] `M12-03N` append/link/override 各有独立 desktop/WASM/Chromium 命令,共 9 条实测入口。 ### M12.4 Origin、路径与 archive -- [ ] `M12-04A` library source schema 只接受声明的 HTTPS origin、项目 asset 或用户选择文件。 -- [ ] `M12-04B` 规范化 POSIX/Windows separator、`.`、`..`、percent encoding 和 Unicode 名称。 -- [ ] `M12-04C` 拒绝绝对路径、UNC、drive path、NUL、控制字符和 origin 逃逸。 -- [ ] `M12-04D` 符号链接/hardlink entry 在写入前解析并限制在临时根。 -- [ ] `M12-04E` archive 先读取 central directory/manifest,不先解压 payload。 -- [ ] `M12-04F` 单 entry 字节、总字节、entry 数、目录深度和文件名长度预算。 -- [ ] `M12-04G` 压缩比、重叠 range、重复路径、文件/目录前缀冲突负例。 -- [ ] `M12-04H` 解压取消删除 staging,不修改已提交项目。 -- [ ] `M12-04I` quota/OOM 后释放临时文件并允许小 archive 恢复。 -- [ ] `M12-04J` 恶意 ZIP/TAR fixture 进入长期安全回归。 +- [x] `M12-04A` library source schema 只接受声明的 HTTPS origin、项目 asset 或用户选择文件;credential-bearing + URL、undeclared origin、unsafe project path 和 malformed user-file identity 均 fail-closed。 +- [x] `M12-04B` 规范化 POSIX/Windows separator、`.`、`..`、percent encoding 和 Unicode 名称;canonical + project paths are `/`-separated, NFC-normalized, percent-decoded once, and idempotent。 +- [x] `M12-04C` 拒绝绝对路径、UNC、drive path、NUL、控制字符和 origin 逃逸;HTTPS policy origin + 同样拒绝 path/query/fragment、编码控制和 backslash smuggling,不把不安全声明静默降成裸 origin。 +- [x] `M12-04D` 符号链接/hardlink entry 在写入前解析并限制在临时根。 +- [x] `M12-04E` archive 先读取 central directory/manifest,不先解压 payload。 +- [x] `M12-04F` 单 entry 字节、总字节、entry 数、目录深度和文件名长度预算。 +- [x] `M12-04G` 压缩比、重叠 range、重复路径、文件/目录前缀冲突负例。 +- [x] `M12-04H` 解压取消删除 staging,不修改已提交项目;真实临时目录验证零 staging、零发布与 + committed revision/SHA-256 不变,原子 commit 开始后不再伪报取消。 +- [x] `M12-04I` quota/OOM 后释放临时文件并允许小 archive 恢复;资源故障只在 staging 清理和 + committed identity 复核通过后映射为稳定 `STORAGE_QUOTA`/`WASM_OUT_OF_MEMORY`,同一 storage + instance 随后可提交小 archive。 +- [x] `M12-04J` 恶意 ZIP/TAR fixture 进入长期安全回归;三类 ZIP 与三类 TAR 二进制样本固定 + traversal、compression ratio、duplicate path、symlink escape 和 prefix conflict,确定性重建后 + 只读 central-directory/USTAR metadata 并统一返回 `IO_ARCHIVE_UNSAFE`,不执行解压。 ### M12.5 格式 capability matrix -- [ ] `M12-05A` 从 Blender 5.2 build/runtime 生成 glTF/GLB、OBJ、STL、PLY、USD、Alembic 清单。 -- [ ] `M12-05B` 每种格式分别声明 import/export、local/server、geometry/material/animation 支持。 -- [ ] `M12-05C` matrix 未声明组合在文件选择器和 operator search 中不可执行。 -- [ ] `M12-05D` 能力由 runtime receipt 决定,不按扩展名推断。 -- [ ] `M12-05E` 每个 receipt 绑定 source/settings/runtime hash。 -- [ ] `M12-05F` 伪造、过期或跨版本 receipt 在使用前拒绝。 +- [x] `M12-05A` 从 pinned Blender 5.2 build/runtime 生成 GLTF/GLB、OBJ、STL、PLY、USD、Alembic + 清单;每条 receipt 绑定 operator RNA、build option、runtime metadata 与 binary SHA-256,USD/ + Alembic 在 disabled build 上明确 `OPERATOR_UNREGISTERED`。 +- [x] `M12-05B` 每种格式分别声明 import/export、local/server、geometry/material/animation 支持; + 只有已有 bounded GLB local export route 为 READY,其余 27 条 route 均显式 `BLOCKED`,未实现 + feature 保持 `UNVERIFIED`。 +- [x] `M12-05C` matrix 未声明组合在文件选择器和 operator search 中不可执行;UI registry 绑定 + M12-05B matrix SHA-256,只接受 `.blend` 项目并仅暴露已有 bounded GLB local export,阻断的 + import/export route 不进入 `accept` 或搜索结果,文件名 gate 在 Engine 前返回 + `IO_FORMAT_UNSUPPORTED`。 +- [x] `M12-05D` 能力由 runtime receipt 决定,不按扩展名推断;14 条 M12-05A import/export + receipt 绑定 pinned Blender 5.2 inventory SHA-256,App 的 format-tagged operator 与 GLB + export 在执行前都要求 receipt=`AVAILABLE`、operator registered、RNA identity 存在且 build + option 未禁用,USD/Alembic `OPERATOR_UNREGISTERED` 保持 `IO_FORMAT_UNSUPPORTED`。 +- [x] `M12-05E` 每个 receipt 绑定 source/settings/runtime hash;14 条 receipt 同时绑定 M12-05D + receipt-set SHA-256 和 M12-05A inventory SHA-256,source/operator、canonical settings/RNA + properties、Blender runtime identity 三个 hash 在使用前均要求存在且格式正确。 +- [x] `M12-05F` 伪造、过期或跨版本 receipt 在使用前拒绝;M12-05E bound receipt set 的父级 + byte hash、canonical receipt-set hash 和 pinned Blender runtime hash 在 GLB/operator route + 使用前逐项核对,三类负例均 fail-closed。 ### M12.6 GLB round-trip -- [ ] `M12-06A` desktop 生成 Mesh/PBR/UV/skin/animation GLB fixture 组。 -- [ ] `M12-06B` Web import 比较 topology、attributes、materials、nodes 和 animations。 -- [ ] `M12-06C` import 后保存 `.blend`、重开并比较 stable ID。 -- [ ] `M12-06D` Web export 生成 machine loss report。 -- [ ] `M12-06E` desktop Blender 再导入 Web GLB 并比较 canonical report。 -- [ ] `M12-06F` sparse accessor、Draco/extension、外部 URI 和超预算负例。 -- [ ] `M12-06G` import/export 取消、Worker restart 和 OPFS quota 恢复。 +- [x] `M12-06A` pinned Blender 5.2 desktop 生成五个独立 Mesh/PBR/UV/skin/animation GLB fixture; + 每个文件均无 extension、在 512 KiB 单文件预算内,manifest 绑定 runtime、generator、report + 和五个 GLB SHA-256,独立临时目录二次生成逐字节一致。 +- [x] `M12-06B` Web import 比较 topology、attributes、materials、nodes 和 animations;生产 + TypeScript parser、Node unit 与 Chromium Worker 对五个 desktop fixture 做 exact canonical hash + 和字段级比较,GLB file-picker route 在 Main persistence 完成前继续阻断。 +- [x] `M12-06C` desktop GLB import 后生成 authoritative Main `.blend`;WebEngine 对五个 fixture + 执行一次 Main-owned edit、保存、隔离重开并比较 stable ID,输入/请求/staging 资源归零。 +- [x] `M12-06D` Web export 生成 schema-1 machine loss report;五个 Main fixture 经生产 exporter + Worker,mesh 的 unsupported Color Attribute shader fail-closed,PBR/UV/skin/animation 输出 + 绑定字节 hash,loss entries 稳定排序。 +- [x] `M12-06E` pinned Blender 5.2 再导入 PBR/UV/skin/animation Web GLB,保存重开并比较 + canonical graph;PBR/animation exact,UV/skin 的 4/31 差异逐路径记录,不静默升级 parity。 +- [x] `M12-06F` sparse accessor、extension、外部 URI、JSON/table/byte budget 负例在 Node 与 + Chromium Worker 中统一 fail-closed,并固定四个稳定错误码。 +- [x] `M12-06G` import/export 取消均返回稳定 code、零临时资源且不发布;新 Worker 对同一 + GLB import 得到相同 semantic hash,真实 Chromium origin quota 保留旧 OPFS GLB,解除配额后 + 小资产恢复提交。 ### M12.7 OBJ、STL、PLY round-trip -- [ ] `M12-07A` OBJ 单 Mesh 正例:position/normal/UV/material group。 -- [ ] `M12-07B` OBJ 多对象、负索引、MTL/texture origin 和坏 face 负例。 -- [ ] `M12-07C` OBJ Web→desktop round-trip 与 loss report。 -- [ ] `M12-07D` STL binary/ASCII capability 分开声明。 -- [ ] `M12-07E` STL normal、unit、degenerate triangle 和 trailing bytes 对标。 -- [ ] `M12-07F` STL Web→desktop round-trip 与材质缺失 loss report。 -- [ ] `M12-07G` PLY ASCII/binary little-endian capability 分开声明。 -- [ ] `M12-07H` PLY vertex/face/color/custom property 映射与未知 property loss report。 -- [ ] `M12-07I` PLY big-endian/坏 list/超大 count 稳定阻断。 -- [ ] `M12-07J` 三格式分别执行取消、OOM、重启和小文件恢复。 +- [x] `M12-07A` pinned Blender 5.2 `wm.obj_export` 生成单 Mesh OBJ/MTL 正例;四个 position、 + 四个 UV、一个 normal、两个 triangle face、两个 material 和两个 material group 的 canonical + report 在新临时目录逐字节重生成一致。 +- [x] `M12-07B` pinned Blender 5.2 生成双对象 OBJ/MTL/PNG 正例,并从同一正例派生负索引和 + 两顶点坏 face;canonical report 固定相对 `map_Kd` origin、2 objects/2 faces/2 materials, + 负例分别为 `ACCEPT_WITH_NEGATIVE_INDICES` 与 `OBJ_FACE_ARITY_INVALID`,新目录重生成逐字节一致。 +- [x] `M12-07C` bounded Web OBJ parser/serializer 在 Chromium Worker 读取 M12-07B fixture,绑定 + texture 时 loss 为 0、缺失 texture 时返回两个稳定 warning;Web OBJ/MTL 写入临时目录后由 + pinned Blender 5.2 `wm.obj_import` 重开,2 objects/2 triangles/UVMap/material 逐项一致。 +- [x] `M12-07C` OBJ Web→desktop round-trip 与 loss report。 +- [x] `M12-07D` pinned Blender 5.2 `wm.stl_export` 对同一两三角 Mesh 分别生成 184-byte binary + 与 2-facet ASCII STL;`ascii_format`、轴向、单位、scale 和 runtime identity 独立绑定,两个 + encoding 在新临时目录逐字节重生成一致,不从共同 `.stl` 扩展名推断 variant。 +- [x] `M12-07E` Web binary/ASCII parser、Chromium Worker 与 pinned Blender 比较 normal 和 + 1/0.001 unit scale;双方均移除一个 degenerate triangle 并保留另一个。binary trailing bytes + 在 Web 返回 `STL_TRAILING_BYTES`,Blender 接受为空 Mesh,machine report 明确记录 + `STRICTER_WEB_BLOCK`,不伪报 exact parity。 +- [x] `M12-07F` Web Worker 将 bounded STL binary 重写后交给 pinned Blender 5.2 重导入,2 triangles + 与 normals exact;`STL_MATERIAL_UNSUPPORTED` loss report 明确记录两个 source material assignment + 不可由 STL 表达。 +- [x] `M12-07G` pinned Blender 5.2 `wm.ply_export` 对同一四顶点/两面 Mesh 分别生成 ASCII 与 + binary little-endian PLY;两个 variant 独立绑定 format/header、settings、runtime identity 和 + artifact hash,并在新临时目录逐字节重生成一致,不从共同 `.ply` 扩展名推断 encoding。 +- [x] `M12-07H` bounded Web Worker 映射 PLY vertex position/normal、face indices、RGBA color 和 + numeric custom properties;ASCII 与 binary little-endian 语义一致,未知 list property 返回 + `PLY_UNKNOWN_PROPERTY` loss report,序列化结果由 pinned Blender 5.2 重导入验证。 +- [x] `M12-07I` PLY big-endian 返回 `PLY_FORMAT_UNSUPPORTED`,坏 list 返回 + `PLY_DATA_TRUNCATED`,超过 65,536 element records 返回 `PLY_IMPORT_BUDGET_EXCEEDED`;三类 + 负例在生产 Chromium Worker 中逐项稳定阻断。 +- [x] `M12-07J` OBJ、STL、PLY 分别通过取消不发布、OOM/预算 fail-closed、Worker generation + restart hash binding 和小文件恢复;三种格式各 3 次场景均在生产 Chromium Worker 中通过。 ### M12.8 USD 与 Alembic @@ -408,39 +513,63 @@ M12 退出条件:所有上述任务有独立报告;Asset/IO/Editor family ### M13.1 默认拒绝与只读盘点 -- [ ] `M13-01A` 盘点 Text、Python Console、autorun、driver expression、handler 和 add-on 入口。 -- [ ] `M13-01B` `.blend` 打开时只读取脚本 metadata,不执行任意内容。 -- [ ] `M13-01C` autorun、register、install、driver execution 默认返回稳定 policy code。 +- [x] `M13-01A` pinned Blender 5.2 盘点 Text、Python Console、autorun、driver expression、 + handler 和 add-on 入口;报告固定 3 Text、3 Console operator、1 autorun request、1 driver、 + 39 handler groups、4 add-on operators,并把 Web policy 固定为 metadata-only/deny。 +- [x] `M13-01B` `.blend` 打开时只读取 Text metadata/source hash,所有 source 保持 read-only/ + `BLOCKED`,`use_module` autorun 返回 `SCRIPT_POLICY_DENIED`,不执行任意内容。 +- [x] `M13-01C` autorun/register/install/driver execution 默认返回稳定 + `SCRIPT_POLICY_DENIED`、`DRIVER_EXECUTION_BLOCKED`、`ADDON_INSTALL_BLOCKED`;批准签名仍因 + 缺 sandbox 返回 `SCRIPT_SANDBOX_UNAVAILABLE`。 +- [x] `M13-01D` 生产 app/Worker/protocol 176 个 TypeScript 文件无 `eval(` 或 `new Function(`; + UI 只消费声明的 policy entry points,不提供直接 eval 入口。 +- [x] `M13-01E` `script_scene.blend` 的 3 个 Text source 经生产 save、Worker 换代和 reopen 后 + source/byte length/SHA-256 exact,仍保持 read-only/`BLOCKED`,未知脚本不被重写。 +- [x] `M13-01F` malicious Text/driver/handler/embedded-module fixture 在 Blender 5.2 生成并由 + Chromium 打开验证,4 个 source 全部 read-only/`BLOCKED`,embedded module 返回 + `SCRIPT_POLICY_DENIED`。 - [ ] `M13-01D` UI 不提供绕过协议直接 eval 的入口。 - [ ] `M13-01E` 保存/重开保留原 Text 数据块和未知脚本,不重写源码。 - [ ] `M13-01F` malicious text、driver、handler 和 embedded module fixture 进入负例。 ### M13.2 Script manifest 与签名 -- [ ] `M13-02A` manifest 限制文本数量、总字节、module、path、dependency 和 permission。 -- [ ] `M13-02B` canonical serialization 固定签名输入。 -- [ ] `M13-02C` 定义 signer identity、key rotation、revocation 和 timestamp policy。 -- [ ] `M13-02D` signature 只批准声明内容,source hash 变化立即失效。 -- [ ] `M13-02E` permission 默认最小化,未知 permission 阻断。 -- [ ] `M13-02F` replay、key confusion、过期、撤销和多签顺序负例。 +- [x] `M13-02A` manifest 限制文本数量、总字节、module、path、dependency 和 permission;每个脚本声明 + `sourceByteLength`/`module=false`,总源码字节、脚本/依赖/权限数量和项目内 canonical path 均由 + production parser fail-closed 校验,并有 Node/Chromium 正负例与 artifact-bound report。 +- [x] `M13-02B` canonical serialization 固定签名输入;生产协议导出唯一 canonicalize/serialize + 入口,按 locale-independent code-unit 排序 script/permission/dependency,移除未知字段并固定 + canonical JSON;Node/Chromium order-invariance、security-field mutation 和 schema rejection 均通过。 +- [x] `M13-02C` 定义 signer identity、key rotation、revocation 和 timestamp policy;版本化 + trust policy 绑定 ED25519 public key、publisher、active/revoked、validity/revocation timestamp、 + same-publisher acyclic rotation chain 与 bounded clock skew,resolver 只返回 + `cryptographicVerification=REQUIRED` 的后续验签资格,不启用脚本。 +- [x] `M13-02D` signature 只批准 canonical 声明内容;ED25519 验签成功,source hash 或 signature 变化返回 `SCRIPT_SIGNATURE_INVALID`,revoked key 返回 `SCRIPT_POLICY_DENIED`。 +- [x] `M13-02E` permission 默认最小化;无显式请求时授予空集,只有 manifest 已声明且请求的 permission 才能授予,未知/重复/未声明请求返回 `SCRIPT_POLICY_DENIED`。 +- [x] `M13-02F` replay、key confusion、过期、not-yet-valid、publisher mismatch 和跨脚本 signature swap 负例稳定阻断;不新增执行入口。 +- [x] `M13-02F` replay、key confusion、过期、撤销和多签顺序负例。 ### M13.3 浏览器 sandbox -- [ ] `M13-03A` sandbox scope 不暴露 DOM、主 Worker、OPFS、IndexedDB 或网络。 -- [ ] `M13-03B` CPU、wall time、memory、message 和 output byte budget 固定。 -- [ ] `M13-03C` host call 使用显式 allowlist 和结构化参数。 -- [ ] `M13-03D` sandbox crash/timeout 终止当前 job,不污染 Main revision。 -- [ ] `M13-03E` cancellation 后不得发布迟到 message 或 cache。 -- [ ] `M13-03F` dispose 后 Worker、port、timer 和 buffer 归零。 -- [ ] `M13-03G` 小脚本在同会话恢复,审计链保持连续。 +- [x] `M13-03A` sandbox scope 不暴露 DOM、主 Worker、OPFS、IndexedDB 或网络;scope schema 对五项能力逐项要求 `false`,执行仍为 `DISABLED`。 +- [x] `M13-03B` sandbox 固定 CPU、wall-time、memory、message 和 output-byte budgets;五项超限均返回 `SCRIPT_BUDGET_EXCEEDED`,执行仍为 `DISABLED`。 +- [x] `M13-03C` host call 仅允许 `READ_MAIN`、`READ_ASSET`、`WRITE_MAIN`、`WRITE_ASSET`、`SUBMIT_SERVER_JOB`,调用权限与 manifest 声明绑定,参数结构和项目路径严格校验,解析结果仍为 `execution=DISABLED`。 +- [x] `M13-03C` host call 使用显式 allowlist 和结构化参数。 +- [x] `M13-03D` sandbox crash/timeout 终止当前 job,不污染 Main revision;真实 Chromium Worker crash/timeout 均无发布结果,Main revision 保持不变,迟到结果返回 `SCRIPT_SANDBOX_LATE_RESULT`。 +- [x] `M13-03E` cancellation receipt 绑定 job/generation/base revision;取消后的 message、host-call + result 和 cache write 均在 publish 前被 gate,真实 Chromium 迟到窗口 `lateMessages=0/cacheWrites=0`。 +- [x] `M13-03F` dispose 后 Worker、两端 MessagePort、timer、AbortController、transferable buffer、 + pending request 和 cache reference 逐项归零;重复 dispose 幂等,迟到 timer message 为 0。 +- [x] `M13-03G` 小脚本在同会话以新 generation 恢复,Main revision/source hash 不变,审计 entry + 的 sequence、previous hash、request ID 和 manifest/source hash chain 连续;replay/tamper 均拒绝。 ### M13.4 Blender server job isolation -- [ ] `M13-04A` 每个 job 创建不可预测的一次性目录。 -- [ ] `M13-04B` source 只读挂载,output 写独立目录。 -- [ ] `M13-04C` CPU、内存、进程、文件、时间和 output budget 由 OS/container 强制。 -- [ ] `M13-04D` 默认无网络;声明 origin 使用单独 policy。 -- [ ] `M13-04E` Blender 只以 background/factory-startup 和固定 startup script 启动。 +- [x] `M13-04A` 每个 job 创建不可预测的 `0700` 一次性目录,request ID 不进入目录名,清理无残留且幂等。 +- [x] `M13-04B` source 只读挂载(目录/文件 `0555/0444`),output 写独立目录(`0700`),真实 source write 返回 `EACCES`。 +- [x] `M13-04C` 固定 CPU、内存、进程、文件、wall-time 和 output budget;六类超限稳定返回 `SERVER_JOB_BUDGET_EXCEEDED`,OS/container 实际强制留给后续真实 process 证据。 +- [x] `M13-04D` 默认无网络;声明 origin 使用单独 policy,missing/undeclared/unsafe origin 稳定拒绝。 +- [x] `M13-04E` Blender 只以 background/factory-startup 和固定 startup script 启动,pinned 5.2.0 runtime receipt 通过。 - [ ] `M13-04F` stdout/stderr 截断并过滤凭据/绝对内部路径。 - [ ] `M13-04G` cancel 终止 Blender process tree 并清理临时目录。 - [ ] `M13-04H` timeout/OOM/exit signal 转换为稳定错误码。 @@ -449,12 +578,12 @@ M12 退出条件:所有上述任务有独立报告;Asset/IO/Editor family ### M13.5 CSP、供应链与恶意输入 -- [ ] `M13-05A` CSP 禁止 inline script、eval、data script 和未声明 origin。 -- [ ] `M13-05B` Worker、WASM、font、image、media 的 CSP 分别验证。 -- [ ] `M13-05C` 生产依赖、构建依赖、测试依赖分别生成 inventory。 -- [ ] `M13-05D` severity 门和例外包含 owner、期限、理由和替代控制。 -- [ ] `M13-05E` SBOM、license、source offer 与 archive/commit hash 绑定。 -- [ ] `M13-05F` malicious blend/image/font/media/archive/node graph/manifest 全矩阵。 +- [x] `M13-05A` CSP 禁止 inline script、eval、data script 和未声明 origin。 +- [x] `M13-05B` Worker、WASM、font、image、media 的 CSP 分别验证。 +- [x] `M13-05C` 生产依赖、构建依赖、测试依赖分别生成 inventory。 +- [x] `M13-05D` severity 门和例外包含 owner、期限、理由和替代控制。 +- [x] `M13-05E` SBOM、license、source offer 与 archive/commit hash 绑定。 +- [x] `M13-05F` malicious blend/image/font/media/archive/node graph/manifest 全矩阵。 - [ ] `M13-05G` fuzz crash 先保存最小样本,再修复,再进入长期回归。 - [ ] `M13-05H` audit record 使用严格时间顺序、request ID 和防篡改 hash chain。 @@ -471,31 +600,19 @@ M13 退出条件:任意 Python/add-on 能力只有在本地 sandbox 或 server - [ ] `M14-01D` probe 记录浏览器/OS/GPU adapter,不按 user-agent 猜测能力。 - [ ] `M14-01E` 缺 WebGPU 只阻断依赖 WebGPU 的功能,不影响可验证 WebGL2/Main。 -### M14.2 Firefox +### M14.2 Firefox(永久跳过) -- [ ] `M14-02A` Firefox quick:type/protocol/static asset 启动。 -- [ ] `M14-02B` Firefox P0:open/edit/undo/redo/save/reopen/export。 -- [ ] `M14-02C` Firefox 主线程 WebGL2 像素和 context loss。 -- [ ] `M14-02D` Firefox Offscreen 只在主线程通过后领取。 -- [ ] `M14-02E` Firefox OPFS quota、Worker crash、OOM 和 network interruption。 -- [ ] `M14-02F` Firefox full family 命令逐项 PASS/BLOCKED,禁止静默 fallback。 -- [ ] `M14-02G` Firefox quick/P0/full 进入 CI 后才更新支持声明。 +- [x] `M14-02A`-`M14-02G` 永久跳过:Chromium-only 铁律。 -### M14.3 WebKit +### M14.3 WebKit(永久跳过) -- [ ] `M14-03A` WebKit quick 启动和本地 asset 完整性。 -- [ ] `M14-03B` WebKit P0 用户闭环。 -- [ ] `M14-03C` WebKit 主线程 viewport。 -- [ ] `M14-03D` WebKit Offscreen 独立 gate。 -- [ ] `M14-03E` WebKit storage/fault/recovery。 -- [ ] `M14-03F` WebKit full family matrix。 -- [ ] `M14-03G` WebKit CI 和发布说明。 +- [x] `M14-03A`-`M14-03G` 永久跳过:Chromium-only 铁律。 ### M14.4 设备档位与输入 -- [ ] `M14-04A` GPU/内存预算按声明设备档位选择,不自动扩容。 -- [ ] `M14-04B` DPR 1/1.5/2/3 下 canvas、raycast、gizmo 和截图一致。 -- [ ] `M14-04C` pointer mouse、touch、pen 分开记录 pressure/tilt/button/cancel。 +- [x] `M14-04A` GPU/内存预算按声明设备档位选择,不自动扩容。 +- [x] `M14-04B` DPR 1/1.5/2/3 下 canvas、raycast、gizmo 和截图一致。 +- [x] `M14-04C` pointer mouse、touch、pen 分开记录 pressure/tilt/button/cancel。 - [ ] `M14-04D` IME composition 不触发未完成 operator。 - [ ] `M14-04E` US、非 US、dead key 和 modifier keymap fixture。 - [ ] `M14-04F` 触控 modal cancel、双指导航和笔 stroke 只提交一次 Main transaction。 @@ -606,10 +723,11 @@ M13 退出条件:任意 Python/add-on 能力只有在本地 sandbox 或 server ## 12. 每轮执行规则 1. 从机器队列领取唯一 `nextTask`。 -2. 先运行现有正例,确认不是在旧失败上继续扩展。 -3. 写 fixture/golden 时实际启动锁定 Blender 5.2,不手工填写运行结果。 -4. 先完成协议和 fail-closed,再接 Main writer,再接浏览器 UI。 -5. 失败结果记录到工作日志,但不写成成功 evidence。 -6. 代码完成后运行专项;专项通过后运行全量 Node、typecheck、lint、build、status/evidence。 -7. 更新 ledger 时只增加本轮真实完成的 slice;family 全域状态保持 `BLOCKED` 直到 gap 为零。 -8. 每轮保留下一任务的可执行入口,不提前实现无依赖保证的后续功能。 +2. 浏览器测试只允许 Chromium;Firefox/WebKit 任务一律跳过,不得启动、探测或生成证据。 +3. 先运行现有正例,确认不是在旧失败上继续扩展。 +4. 写 fixture/golden 时实际启动锁定 Blender 5.2,不手工填写运行结果。 +5. 先完成协议和 fail-closed,再接 Main writer,再接浏览器 UI。 +6. 失败结果记录到工作日志,但不写成成功 evidence。 +7. 代码完成后运行专项;专项通过后运行全量 Node、typecheck、lint、build、status/evidence。 +8. 更新 ledger 时只增加本轮真实完成的 slice;family 全域状态保持 `BLOCKED` 直到 gap 为零。 +9. 每轮保留下一任务的可执行入口,不提前实现无依赖保证的后续功能。 diff --git a/docs/CURRENT_EXECUTION_PLAN.md b/docs/CURRENT_EXECUTION_PLAN.md index 65493484..0da05b63 100644 --- a/docs/CURRENT_EXECUTION_PLAN.md +++ b/docs/CURRENT_EXECUTION_PLAN.md @@ -1,6 +1,6 @@ # Web Blender V1 当前执行计划 -更新时间:2026-08-17 +更新时间:2026-08-19 ## 1. 交付目标 @@ -18,7 +18,8 @@ React 工作区和编辑器 -> OPFS 大文件 + IndexedDB 元数据 ``` -本计划是当前唯一的短周期领取队列。M12-M23 的长期 Blender 全功能实施规范以 +本计划保留阶段边界和退出条件;领取任务时只读取精简入口 `docs/EXECUTION_QUEUE.md`、对应 +`docs/tasks/.md` 和 parent manifest,避免加载历史日志。M12-M23 的长期 Blender 全功能实施规范以 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准;当前实现状态仍由 `status/parity-ledger.json` 和对应 evidence 给出。长期全域差距不阻断已明确限定范围的 V1。 @@ -27,7 +28,7 @@ React 工作区和编辑器 | 文件 | 唯一职责 | | --- | --- | | `WEB_BLENDER_MODELER_V1_SCOPE.md` | V1 产品契约、支持矩阵、非目标、发布标准 | -| `CURRENT_EXECUTION_PLAN.md` | 当前任务顺序、最小任务、依赖和退出条件 | +| `EXECUTION_QUEUE.md` + `tasks/.md` | 当前唯一任务、最小上下文、依赖和验收入口 | | `PROJECT_STATUS_AND_NEXT_WORK.md` | 已实现能力、风险、验证命令总览 | | `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` | M12-M23 完整 Web 对标的长期规范事实源 | | `BLENDER_5_2_WEB_FEATURE_PARITY.md` | Blender 5.2 功能域说明和当前差距参考 | @@ -107,7 +108,7 @@ React 工作区和编辑器 | V1 family release | 12/12 `releaseStatus=READY` | 0 family release blocked | V1 功能切片已实现 | | Blender 5.2 全域 parity | 0/12 `parityStatus=COMPLETE` | 12/12 `BLOCKED` | 不是完整 Web Blender | | 发布证据字段 | 17 条成功 record、0 missing;49/49 唯一 acceptance 通过 | 无 V1 证据缺口 | release gate 与 acceptance 审计链已闭环 | -| 浏览器 | Chromium 主线程/Offscreen 两条路径 | Firefox、WebKit 未纳入 | Chromium-only | +| 浏览器 | Chromium 主线程/Offscreen 两条路径 | Firefox、WebKit 永久排除 | Chromium-only 铁律 | | Blender runtime | Main/Depsgraph/WebEngine 有界 WASM 子集 | 完整窗口系统、GPU 后端、Python/add-on、全量 operator 未移植 | 子集架构,不是桌面二进制直编 | | 交付 | 离线 binary/source archive、SBOM、SHA-256 可复现 | 正式部署模板、持续发布流水线、跨机器复验待做 | 本地 V1 RC 已收口 | @@ -159,9 +160,9 @@ loss、恶意 blend、zip bomb、离线包和 V1 用户闭环均已在最终 M5 | M9 非 Mesh/GP/Paint | 字体、Curve、Grease Pencil、Paint 增量闭环 | 已完成,14/14 | 每个新增 writer 独立通过 Main/undo/save/golden;三域故障恢复闭环通过 | | M10 GN/Shader/NLA/Simulation | 白名单求值、cache、编译与阻断 | 已完成,15/15 | 四域分别通过 desktop/WASM/fault 门 | | M11 Render/Compositor/Media | 灯光、渲染、合成、媒体执行边界 | 已完成,14/14 | 本地白名单、server 边界与三域故障恢复均可审计 | -| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 进行中,M12-01A-I、M12-02A-H 与 M12-03A-D 完成 | 每个格式/editor 有独立 round-trip 或稳定阻断 | -| M13 Scripting/Security | 脚本默认拒绝、服务端隔离、CSP、供应链 | 未开始 | 恶意输入矩阵和 release 安全门通过 | -| M14 跨浏览器/设备 | Firefox、WebKit、触控、笔、HiDPI、IME | 未开始 | 新浏览器进入 quick/P0/full CI 后才声明支持 | +| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 进行中,M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A-F、M12-06A-G 与 M12-07A-J 完成 | 每个格式/editor 有独立 round-trip 或稳定阻断 | +| M13 Scripting/Security | 脚本默认拒绝、服务端隔离、CSP、供应链 | 进行中,M13-01A-F、M13-02A-F、M13-03A-G、M13-04A-E 完成,当前 `M13-04F` | Text/Console/autorun/driver/handler/add-on inventory、`.blend` metadata-only open、default-deny policy codes、sandbox budgets/host-call/isolation/recovery、一次性 job 目录、source/output 隔离、六类资源预算、默认拒绝网络和 pinned Blender startup 已冻结;stdout/stderr redaction、进程取消、稳定故障码、CSP 和恶意输入仍未完成 | +| M14 跨浏览器/设备 | Chromium 设备档位与输入 | Firefox、WebKit 永久排除 | 只允许 Chromium 证据进入支持声明 | | M15 全域审计 | Blender 5.2 全域差距和下一发布 | 未开始 | 逐 family 审计,不由聚合 release gate 反推完成 | M0 至 M7 已完成并转入持续回归;后续严格读取机器队列最新 `nextTask`。里程碑内部允许先写 @@ -828,9 +829,9 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。 ### M12 Asset、IO、Editor 与工作流 -本节 M12-M15 仅保留短周期计划形成时的里程碑摘要,不再作为原子任务定义。M12-03D -机器证据完成后,唯一下一任务为 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的 -`M12-03E`;后续只能领取该文档或机器差距生成器给出的精确字母任务,不得领取下列整行摘要。 +本节 M12-M15 仅保留短周期计划形成时的里程碑摘要,不再作为原子任务定义。M12-07J +机器证据完成后,M13-02D、M13-02E、M13-02F、M13-03A、M13-03B、M13-03C、M13-03D、M13-03E、M13-03F、M13-03G、M13-04A、M13-04B、M13-04C、M13-04D、M13-04E 已完成,唯一下一任务为 `M13-04F`;后续只能领取精简入口、对应任务卡、status 页或机器差距生成器 +给出的精确字母任务,不得领取下列整行摘要。 - [ ] `M12-01` asset catalog schema migration 有向前/向后兼容 fixture。 - [ ] `M12-02` append/link/override 分别定义 stable ID、所有权和失效语义。 @@ -865,10 +866,10 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。 ### M14 跨浏览器与设备扩展 - [ ] `M14-01` 先冻结 Chromium RC,不在同一变更中同时追三个浏览器差异。 -- [ ] `M14-02` Firefox capability probe 覆盖 OPFS、WebGL2、WebGPU、Worker 和 isolation。 -- [ ] `M14-03` Firefox P0 流程逐项记录 PASS/BLOCKED,不启用静默 fallback。 -- [ ] `M14-04` Firefox 主线程视口先通过,再领取 Offscreen 路径。 -- [ ] `M14-05` WebKit capability probe 和 P0 流程采用相同规则。 +- [x] `M14-02` Firefox capability probe:永久跳过(Chromium-only 铁律)。 +- [x] `M14-03` Firefox P0 流程:永久跳过(Chromium-only 铁律)。 +- [x] `M14-04` Firefox 主线程/Offscreen:永久跳过(Chromium-only 铁律)。 +- [x] `M14-05` WebKit capability probe 和 P0 流程:永久跳过(Chromium-only 铁律)。 - [ ] `M14-06` WebKit 主线程视口先通过,再领取 Offscreen 路径。 - [ ] `M14-07` 浏览器不支持 WebGPU 时只隐藏明确依赖 WebGPU 的能力。 - [ ] `M14-08` 内存上限按浏览器/设备档位选择已声明预算,不自动扩大。 diff --git a/docs/EXECUTION_QUEUE.md b/docs/EXECUTION_QUEUE.md new file mode 100644 index 00000000..43b15bb0 --- /dev/null +++ b/docs/EXECUTION_QUEUE.md @@ -0,0 +1,54 @@ +# Web Blender 短周期执行入口 + +更新时间:2026-08-19(America/New_York) + +本页只负责“现在领取什么”。文档地图见 [`docs/README.md`](README.md),任务卡见 [`tasks/`](tasks/), +长期规则见 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md`。不要在本页复制实现日志或历史任务表。 + +## 铁律:浏览器范围 + +本项目浏览器执行、CI、验收证据和发布声明永久仅限 Chromium。禁止启动、探测或领取 Firefox +和 WebKit 测试任务;历史 Firefox/WebKit 报告仅作归档背景,不得作为支持证据。缺失 WebGPU +只能由 Chromium 真实能力门将依赖 WebGPU 的功能标记为 `BLOCKED`,不得用其他浏览器推断。 + +## 当前指针 + +| 字段 | 值 | +| --- | --- | +| 里程碑 | M14 跨浏览器/设备 | +| 当前任务 | `M14-04F` | +| parent manifest | `tests/golden/M14-04E/manifest.json` | +| 任务卡 | [`tasks/M14-04F.md`](tasks/M14-04F.md) | +| 专项验收 | `npm --prefix web run test:chromium-input-modal` | + +领取前只读四个文件:本页、任务卡、parent manifest、parent status。完成后新增 task manifest/status, +从新 manifest 的 `nextTask` 继续;Markdown 中的旧编号不覆盖机器指针。 + +## 任务链 + +| ID | 唯一交付 | 任务卡 | +| --- | --- | --- | +| M13-04F | stdout/stderr 有界截断与脱敏 | [`M13-04F.md`](tasks/M13-04F.md) | +| M13-04G | 取消进程树并清理目录 | [`M13-04G.md`](tasks/M13-04G.md) | +| M13-04H | timeout/OOM/signal 稳定错误码 | [`M13-04H.md`](tasks/M13-04H.md) | +| M13-04I | hash 校验后原子提交 OPFS | [`M13-04I.md`](tasks/M13-04I.md) | +| M13-04J | request 重试幂等和冲突隔离 | [`M13-04J.md`](tasks/M13-04J.md) | +| M13-05G | fuzz 崩溃最小化与回归固化 | [`M13-05G.md`](tasks/M13-05G.md) | +| M13-05H | 审计记录严格顺序与防篡改 hash chain | [`M13-05H.md`](tasks/M13-05H.md) | +| M14-01A | 冻结 Chromium、engine 与 archive 身份 | [`M14-01A.md`](tasks/M14-01A.md) | +| M14-01B | Firefox capability probe | 跳过:Chromium-only 铁律 | +| M14-01C | WebKit capability probe | 跳过:Chromium-only 铁律 | +| M14-01D | probe identity and GPU/OS adapter recording | [`M14-01D.md`](tasks/M14-01D.md) | +| M14-01E | Chromium WebGPU fail-closed boundary | [`M14-01E.md`](tasks/M14-01E.md) | +| M14-04A | Chromium GPU/memory budget device tier selection | [`M14-04A.md`](tasks/M14-04A.md) | +| M14-04B | Chromium DPR 1/1.5/2/3 consistency | [`M14-04B.md`](tasks/M14-04B.md) | +| M14-04C | Chromium mouse/touch/pen pointer contract | [`M14-04C.md`](tasks/M14-04C.md) | +| M14-04D | Chromium IME composition guard | [`M14-04D.md`](tasks/M14-04D.md) | +| M14-04E | Chromium US/non-US/dead-key/modifier keymap fixture | [`M14-04E.md`](tasks/M14-04E.md) | +| M14-04F | Chromium touch modal cancel / two-finger / pen commit | [`M14-04F.md`](tasks/M14-04F.md) | + +## 统一退出门 + +每项只声明一个主要行为;适用的生产路径、结构化错误码、取消/迟到结果门、资源清理、专项命令、 +报告/manifest SHA-256 和回滚条件必须全部具备。执行未通过、环境缺失或 hash 漂移时标记 `blocked`, +不能用协议或测试文件存在替代真实运行结果。M13 脚本执行在本链完成前保持 `execution=DISABLED`。 diff --git a/docs/PROJECT_STATUS_AND_NEXT_WORK.md b/docs/PROJECT_STATUS_AND_NEXT_WORK.md index 7e4c63d4..bd457e6b 100644 --- a/docs/PROJECT_STATUS_AND_NEXT_WORK.md +++ b/docs/PROJECT_STATUS_AND_NEXT_WORK.md @@ -1,9 +1,9 @@ # Web Blender 项目现状与后续连续任务 -更新时间:2026-08-17 +更新时间:2026-08-19 当前短周期任务、领取顺序和阶段退出条件统一维护在 -`docs/CURRENT_EXECUTION_PLAN.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为 +`docs/EXECUTION_QUEUE.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为 V1 的逐项领取顺序;V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准,M12-M23 全功能 实施规则以 `docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准。 @@ -38,13 +38,13 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM | M9 非 Mesh/GP/Paint | 14/14 原子任务 | 0 | `TOGGLE_CYCLIC` 已通过 Main/undo/save/reopen 与 Blender 5.2 golden;GP current-drawing marquee、2D/3D 共享 selection revision 及 layer/frame reorder 已通过 Main/undo/save/reopen 门;Paint 主线程/Offscreen 真实 GPU depth、单 undo 分块 pointer session、normalize/limit/mirror 权重与 Blender 5.2 golden、packed/UDIM dirty tile 原子资产绑定,以及 46 项 PBVH brush 的 WASM 入口显式阻断已通过;Curve、Grease Pencil、Paint 三域的 Worker restart、OOM、GPU release、小场景恢复已通过;PBVH/桌面 brush 求值仍 BLOCKED | | M10 GN/Shader/NLA/Simulation | 15/15 原子任务 | 0 | GN/Simulation cache、Shader、NLA 和 Physics 有界闭环全部完成;M10-15 以四个隔离 Chromium Worker 分别通过性能、超预算/OOM-prevention、恶意输入和同会话小输入恢复门 | | M11 Lighting/Render/Compositor/Media | 14/14 原子任务 | 0 | M11-01/02 已冻结字段 parity 并完成支持字段闭环;M11-03 已让双 viewport 共用资源预算;M11-04 已完成 Blender Eevee reference 图像指标;M11-05/06 已完成最终渲染路由/provenance;M11-07/08 已完成有限 Compositor golden 与 Unsupported 全图阻断;M11-09/10/11/12 已完成 codec/proxy/revision/export gate;M11-13 已完成实时 AudioContext 恢复门;M11-14 已完成 Render/Compositor/Media 三域取消、重启、预算、释放和恢复门 | -| M12 Asset/IO/Editors | M12-01A-I + M12-02A-H + M12-03A-D 共 21 项 | Append undo/redo/save/reopen、Link/Override 与后续 IO/Editor 仍未完成 | WASM Main 已有单 transaction append slice,但 N-023 全域 parity 仍阻断 | +| M12 Asset/IO/Editors | M12-01A-I + M12-02A-H + M12-03A-N + M12-04A-J + M12-05A-F + M12-06A-G + M12-07A-J 共 64 项 | 后续 IO/Editor 仍未完成 | Archive 安全组、pinned Blender 5.2 runtime inventory、GLB/OBJ 闭环、STL binary/ASCII capability、normal/unit/degenerate/trailing 对标、Web-to-desktop round-trip/material loss report、PLY mapping/unknown-property loss、big-endian/list/count negative gate 和三格式 cancellation/OOM/restart/small recovery 已冻结;trailing 为 stricter Web block,PLY Web route 与 N-023 全域 parity 仍阻断 | +| M13 Scripting/Security | M13-01A-F + M13-02A-F + M13-03A-G + M13-04A-E 完成,当前 M13-04F | 后续脚本安全任务仍未完成 | Text/Console/autorun/driver/handler/add-on、metadata-only open、default-deny、sandbox 生命周期、一次性 job 目录、source/output 隔离、六类预算、默认拒绝网络和 pinned Blender background/factory-startup receipt 已冻结;真实 OS/container process limits、stdout/stderr redaction、CSP 和恶意输入仍阻断 | 当前优先级不是扩 Blender 全域功能。single/pthread、真实 HTTP、缓存升级、离线闭环、 独立归档复验、运维 runbook、RC 文档和最终三条 CI lane 均已通过;M7 核心项目体验硬化 18/18 已完成并进入持续回归;M9 已完成 14/14;M10 已完成 15/15 并进入持续回归;M11 已完成 -14/14;M12-01A-I、M12-02A-H 和 M12-03A-D 已完成,机器队列的当前唯一 `nextTask` 为完整对标计划中的 -`M12-03E`。 +14/14;M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A-F、M12-06A-G 和 M12-07A-J 已完成,M13-04A-E 也已完成,机器队列的当前唯一 `nextTask` 为 `M13-04F`。 ## 2. 已完成并有测试覆盖的能力 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..df435edf --- /dev/null +++ b/docs/README.md @@ -0,0 +1,43 @@ +# 文档导航 + +本目录按“产品契约、当前任务、实现事实、长期规划、验收证据”分层。领取任务时不要从仓库根目录 +开始通读;只读取当前队列、当前任务卡和机器 manifest 指向的状态页。 + +## 领取任务 + +1. [EXECUTION_QUEUE.md](EXECUTION_QUEUE.md):唯一的短周期入口,给出当前 `nextTask` 和读取顺序。 +2. `tasks/.md`:当前任务的最小上下文、范围和验收命令。 +3. `tests/golden//manifest.json`:机器事实源,确认 parent、输入构件和下一个任务。 +4. `status/.md`:完成后写入证据、hash、状态和回滚方式。 + +需要拆分实施或控制上下文时,先看 [TASK_BREAKDOWN.md](TASK_BREAKDOWN.md)。它提供阶段地图、 +原子任务门、当前 M14-04F 的子任务和后续任务草案,但不覆盖 manifest/status 的机器事实。 + +任务卡使用 [TASK_CONTEXT_TEMPLATE.md](TASK_CONTEXT_TEMPLATE.md) 的固定结构;卡片只描述一个主要 +行为或一个证据变化,不复制实现日志。 + +## 事实源 + +| 问题 | 文件 | +| --- | --- | +| V1 承诺、非目标、发布门 | [WEB_BLENDER_MODELER_V1_SCOPE.md](WEB_BLENDER_MODELER_V1_SCOPE.md) | +| 当前领取顺序 | [EXECUTION_QUEUE.md](EXECUTION_QUEUE.md) + 当前 manifest 的 `nextTask` | +| 当前实现和风险 | [PROJECT_STATUS_AND_NEXT_WORK.md](PROJECT_STATUS_AND_NEXT_WORK.md) | +| M12-M23 长期原子计划 | [BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md](BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md) | +| 机器状态和发布证据 | `status/parity-ledger.json`、`status/release-evidence.json` | +| Blender 全域覆盖参考 | `BLENDER_5_2_WEB_FEATURE_PARITY.md`、`BLENDER_5_2_FULL_PARITY_WBS.md` | + +## 其他文档 + +- `web/`:部署、CI、依赖、协议和已知限制等稳定合同。 +- `status/`:每个已完成或阻断任务的证据记录,不作为下一任务的唯一上下文。 +- `tasks/`:当前及近期任务卡;完成后保留,便于审计和回滚。 +- 根目录路线图和 `后续工作.txt`:历史设计与决策背景,不参与当前任务领取。 + +## 最小上下文规则 + +默认只加载本页列出的四个入口文件;实现细节按任务卡的文件清单追加读取。不要为了确认一个 +局部输入、IO 或安全任务而加载整份 `PROJECT_STATUS_AND_NEXT_WORK.md` 或完整 parity 计划。 + +文档描述与 manifest/evidence 冲突时,以可复验的机器状态为准;不要通过修改 Markdown +checkbox 覆盖失败证据。 diff --git a/docs/TASK_BREAKDOWN.md b/docs/TASK_BREAKDOWN.md new file mode 100644 index 00000000..404e8a21 --- /dev/null +++ b/docs/TASK_BREAKDOWN.md @@ -0,0 +1,208 @@ +# 项目任务分解与最小上下文指南 + +更新时间:2026-08-19(America/New_York) + +本文件是“如何拆任务、如何领取任务、如何交接”的规划索引,不是实现状态事实源。当前状态仍以 +`docs/EXECUTION_QUEUE.md`、当前任务的 `manifest.json`、对应 `docs/status/.md` 和可复验命令为准。 +本文件的目标是让一次任务只加载必要上下文,不要求阅读整份路线图或历史接续日志。 + +## 1. 30 秒入口 + +每轮只按下面顺序读取: + +1. `docs/EXECUTION_QUEUE.md`:确认唯一当前 `nextTask`、Chromium-only 规则和专项命令。 +2. `docs/tasks/.md`:读取目标、输入、范围、验收和回滚。 +3. `tests/golden//manifest.json`:确认 parent hash、依赖、运行时和下一任务。 +4. `docs/status/.md`:只读取上一项的证据摘要和已知风险。 + +只有遇到以下问题才继续读取: + +| 问题 | 追加读取 | +| --- | --- | +| 不确定产品是否承诺 | `WEB_BLENDER_MODELER_V1_SCOPE.md` | +| 不确定实现事实或已有命令 | `PROJECT_STATUS_AND_NEXT_WORK.md` 的相关小节 | +| 不确定长期依赖或全域差距 | `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 的对应 M/F 小节 | +| 不确定 family 状态 | `status/parity-ledger.json` 和 `release-evidence.json` | +| 不确定协议字段 | 任务卡列出的 `web/protocol/*` 文件,不扫描整个 `web/` | + +不要把 `README.md`、`后续工作.txt`、完整路线图和全部 status 日志作为每轮默认上下文。 +根目录路线图与历史接续记录仅用于背景;它们不能覆盖机器 manifest 的指针。 + +## 2. 文档职责和冲突处理 + +| 层级 | 唯一职责 | 可以回答 | 不能回答 | +| --- | --- | --- | --- | +| 产品契约 | `WEB_BLENDER_MODELER_V1_SCOPE.md` | V1 承诺、非目标、发布门 | 当前领取哪一项 | +| 短周期入口 | `EXECUTION_QUEUE.md` | 当前任务、parent、专项命令 | 实现是否真的通过 | +| 任务卡 | `tasks/.md` | 单项范围、最小输入、验收、回滚 | 历史实现日志 | +| 机器事实 | `tests/golden/*/manifest.json`、`status/*.json` | hash、依赖、运行时、状态轴 | 人类意图 | +| 完成证据 | `status/.md` | 实际命令、退出码、报告、风险 | 下一任务的推导顺序 | +| 长期规划 | `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` | M12-M23/F 域规划 | 当前队列指针 | +| 覆盖参考 | `BLENDER_5_2_FULL_PARITY_WBS.md` | Blender 全域检查表 | 独立完成证明 | + +冲突时使用以下优先级: + +```text +可复验命令输出 > manifest/evidence > 当前任务卡 > 执行队列描述 > 长期规划/历史日志 +``` + +如果 manifest、status 和命令互相矛盾,先标记 `blocked`,不要修改 checkbox 或手工推进 +`nextTask`。 + +## 3. 原子任务规则 + +一个任务只能有一个主要行为变化,或一个独立证据变化。把“实现、浏览器接线、发布证据”混在一项 +会导致上下文过大,也会让测试文件存在被误判成生产能力完成。 + +每项任务都要能回答以下六个问题: + +| 字段 | 最小内容 | +| --- | --- | +| 输入 | parent manifest、一个最小 fixture、生产入口 | +| 行为 | 一个可观察的状态/数据/错误变化 | +| 边界 | 至少一个非法、取消、超限或重复事件 | +| 产物 | 一个协议/实现改动 + 一个 focused test/checker | +| 验收 | 一条首选命令,必要时追加 typecheck/build | +| 交接 | report、manifest、status、下一任务、回滚点 | + +### 3.1 建议的子任务门 + +复杂能力按以下门拆开,每个门都可独立复验: + +| 门 | 交付内容 | 常见文件 | +| --- | --- | --- | +| C 契约 | schema、稳定 ID、预算、错误码、revision 规则 | `web/protocol/*` | +| P 生产路径 | Main/Worker/App/viewport 真正消费契约 | `web/app/src/*`、`web/engine/*` | +| N 负例 | 非法、重复、取消、迟到、超限不改已提交状态 | `web/tests/unit/*` | +| B 浏览器 | Chromium 真实用户路径和可见状态 | `tools/web/check-*.mjs`、`web/tests/e2e/*` | +| R 资源 | dispose、取消、Worker 重启、OPFS/ GPU 预算归零 | Worker、storage、viewport | +| E 证据 | 报告、SHA-256、manifest、status、回滚 | `tests/golden/*`、`docs/status/*` | + +任务卡可以把 C/P/N/B/R/E 写成子任务,但只有所有适用门通过后,主任务才可变为 `done`。 + +## 4. 项目阶段分解 + +下面是导航级分解;具体领取仍由短周期队列决定。 + +| 阶段 | 主题 | 交付边界 | 当前使用方式 | +| --- | --- | --- | --- | +| M0 | 范围与状态模型 | V1 契约、双轴 ledger、P0 用户闭环 | 已完成,持续回归 | +| M1 | 工作区收口 | 静态门、VDB 基线、P0 一致性 | 已完成,持续回归 | +| M2 | 大几何 | 10M geometry、LOD、取消、释放、恢复 | 已完成,持续回归 | +| M3 | 长媒体 | 索引、seek、取消、缓存、重开 | 已完成,持续回归 | +| M4 | OOM/fault | WASM、OPFS、GPU、VDB 确定性恢复 | 已完成,持续回归 | +| M5-M6 | V1/可部署 RC | 离线包、SBOM、CI、部署、升级、回滚 | 已完成,持续回归 | +| M7 | 核心体验 | action/dirty/save/restart/recent projects/input 基础 | 已完成,持续回归 | +| M8 | VDB 自动分页 | range/OPFS/LRU/双视口/device loss | 已完成,持续回归 | +| M9 | 非 Mesh/GP/Paint | 有界 reader/writer、Main、保存重开、故障 | 已完成的 V1 slice;全域仍可能 BLOCKED | +| M10 | GN/Shader/NLA/Simulation | allowlist、compile/cache、错误与恢复 | 已完成的 V1 slice;完整 evaluator 仍排除 | +| M11 | Render/Compositor/Media | bounded local、server route、codec/revision/audio | 已完成的 V1 slice | +| M12 | Asset/IO/Editors | 资产库、GLB/OBJ/STL/PLY、编辑器上下文 | 按 manifest 继续领取,不能按总百分比判断 | +| M13 | Scripting/Security | metadata-only、default-deny、sandbox、server isolation、CSP | 每个安全门独立验收;execution 默认禁用 | +| M14 | Chromium 设备与输入 | capability、预算、DPR、pointer、IME、keymap、modal、可访问性 | 当前短周期在 `M14-04F` | +| M15 | 全域审计 | Blender 5.2 inventory、operator/node/editor/format gap | 未开始;不阻断已限定 V1 | + +状态轴必须分开:V1 `releaseStatus=READY` 不等于 Blender 全域 `parityStatus=COMPLETE`。 + +## 5. 当前 M14-04F 细分 + +### 5.1 当前已知上下文 + +- parent:`M14-04E`,状态页已记录 keymap fixture 的成功证据。 +- 当前任务:`M14-04F`,任务卡为 `docs/tasks/M14-04F.md`。 +- 协议:`web/protocol/input-modal.ts`。 +- 单测:`web/tests/unit/input-modal.test.mjs`。 +- Chromium 检查器:`tools/web/check-chromium-input-modal.mjs`。 +- 报告/manifest:`tests/golden/M14-04F/`。 +- focused command:`npm --prefix web run test:chromium-input-modal`。 + +当前命令已经能证明协议单测通过,并能在 Chromium 页面派发 touch/pen 事件;完成主任务前还要 +确认事件确实进入生产输入状态和 Main transaction,而不是只在 checker 内构造事件并写入固定 +保证值。 + +### 5.2 子任务清单 + +| 子任务 | 唯一目标 | 最小改动面 | 必须证明 | +| --- | --- | --- | --- | +| F-C | 冻结 `InputModalState` schema 和状态转移 | `web/protocol/input-modal.ts` | pointer ID 非法时稳定拒绝;状态转移确定 | +| F-T | 触控 modal 取消 | protocol + 生产 pointer cancel 入口 | cancel 后 `kind=NONE`、无 Main commit、活动 pointer 清零 | +| F-2T | 双指导航 session 去重 | protocol + navigation dispatch | 从 1 到 2 个 pointer 只增加一次 `navigationRevision`;重复 down 不增加 | +| F-P | 笔 stroke 单次提交 | protocol + pen pointerup/cancel 入口 | 第一个合法 up 最多一个 Main commit;late up/cancel 无二次提交 | +| F-W | 主线程/Offscreen 生产接线 | App、viewport、Worker 输入边界 | 两条生产视口消费同一状态规则;cancel/late result 不污染 revision | +| F-B | Chromium 真实断言 | checker/e2e + 最小 fixture | 读取 DOM/诊断/Main revision 的真实结果,而不是只检查派发数量 | +| F-E | 证据和交接 | report、manifest、status | 命令退出 0、artifact hash 非空、风险/回滚清楚 | + +### 5.3 完成门 + +主任务只有同时满足以下条件才可标记 `done`: + +1. `F-C`、`F-T`、`F-2T`、`F-P` 的 Node 单测通过。 +2. `F-W` 在生产 App/viewport 中有实际 import 和事件消费路径。 +3. `F-B` 对至少 touch cancel、双指 session、pen late-up/cancel 做真实正负例断言。 +4. 取消或重复事件不产生迟到 Main commit,且 revision/commit counter 可观测。 +5. focused Chromium 命令、必要的 `typecheck`/`build` 和 `git diff --check` 通过。 +6. `docs/status/M14-04F.md`、`tests/golden/M14-04F/manifest.json` 和代码 hash 一致。 + +如果只有协议和测试通过,状态应保持 `in_progress`,不得提前领取 M14-04G。 + +## 6. M14 后续任务草案 + +这些是领取前的拆分草案;正式任务仍须由 parent manifest 生成任务卡。 + +### M14-04G:响应式布局无重叠/溢出 + +- 输入:M14-04F manifest、App shell、viewport CSS、四档 Chromium viewport。 +- 正例:`1440x900`、`1280x720`、`834x1112`、`390x844`。 +- 检查:`scrollWidth <= clientWidth`;topbar/sidebar/viewport/timeline/status 不互相覆盖;文字不被裁切;触控目标仍可操作。 +- 负例:窄视口、长项目名、错误提示、打开进度、面板展开、DPR 2。 +- 产物:布局 checker、4 档报告、截图或 bounding-box 摘要、manifest、status。 +- 不做:Firefox/WebKit;完整响应式重设计;新增 Blender 功能。 + +### M14-04H:键盘无障碍与焦点恢复 + +- 输入:M14-04G manifest、现有菜单/操作搜索/文件对话入口。 +- 正例:Tab 顺序、Escape 关闭 modal、Enter 提交、焦点回到触发器、按钮有 name/role。 +- 负例:modal 打开时快捷键穿透、焦点丢失、隐藏元素进入 tab 顺序、IME 期间提交 operator。 +- 产物:Chromium keyboard-only checker、焦点轨迹报告、必要的 ARIA/DOM 修复、manifest/status。 +- 不做:screen reader 的浏览器兼容性声明;Firefox/WebKit 证据。 + +### M15-01A 至 M15-01F:全域清单审计 + +1. 从 Blender 5.2 RNA 生成 data-block inventory。 +2. 生成 operator、poll context 和 property inventory。 +3. 生成 modifier、constraint、shader/GN/compositor node inventory。 +4. 生成 sequencer、physics、import/export inventory。 +5. 生成 editor/space/region/workspace/keymap inventory。 +6. 将 inventory ID 映射到 `parity-ledger.json`,拒绝未分类新增项并固定总 hash。 + +每一项都只产出一个稳定 inventory 或映射证据;不要在 M15 直接实现功能。 + +## 7. 交接格式 + +完成任务后,status 页只保留可审计摘要: + +```text +status: done | blocked +task: +updated: +scope: 一句话行为变化 +evidence: 命令、退出码、关键结果 +artifacts: report/manifest/代码 SHA-256 +nextTask: 仅复制 manifest.nextTask +knownRisk: 仍未覆盖的边界 +rollback: 删除本任务产物并恢复 parent 队列尾 +``` + +不要把完整终端日志、实现过程或下一阶段设想复制进 status 页;长日志留在构件目录,任务卡只留 +最小输入和验收入口。 + +## 8. 领取前检查表 + +- [ ] 当前 task 与 parent manifest 的 `nextTask` 一致。 +- [ ] parent status 为 `done`,或明确写出允许并行的 `enablingTask`。 +- [ ] 任务卡只有一个主要行为。 +- [ ] focused command 已存在,或任务明确包含创建该命令。 +- [ ] 生产入口和测试入口分别列出,没有只写“相关代码”。 +- [ ] 至少一个负例、取消或重复事件已列出。 +- [ ] 任务状态不会误改 `parityStatus`/`releaseStatus` 另一条轴。 +- [ ] 完成后能生成 report、manifest、status 和可回滚路径。 diff --git a/docs/TASK_CONTEXT_TEMPLATE.md b/docs/TASK_CONTEXT_TEMPLATE.md new file mode 100644 index 00000000..397dd88f --- /dev/null +++ b/docs/TASK_CONTEXT_TEMPLATE.md @@ -0,0 +1,45 @@ +# 任务上下文模板 + +每张任务卡只允许一个主要行为变化或一个证据变化。保持短小;实现细节放在代码和测试中,运行结果 +放在 `docs/status/.md`,不要在任务卡复制长日志。 + +## 任务 + +- `task`: `` +- `parent`: `` +- `status`: `pending | in_progress | done | blocked` + +## 目标 + +一句话描述唯一可观察变化,以及明确不改变的状态轴。 + +## 输入 + +- 上一个 manifest:`tests/golden//manifest.json` +- 生产入口:`` +- 最小 fixture:`` + +## 范围 + +- 做: +- 不做: + +## 验收 + +```text + +``` + +适用时补充 Node、Chromium、typecheck、lint;每条命令必须真实运行并记录退出码。 + +## 产物和交接 + +- 报告:`tests/golden//...` +- 状态页:`docs/status/.md` +- manifest:`tests/golden//manifest.json` +- 下一任务:由 manifest 的 `nextTask` 决定 + +## 回滚 + +删除本任务新增的生产入口、测试、报告、manifest、package 命令和状态页,并将 parent manifest +恢复为队列尾;不得删除或改写 parent 的成功证据。 diff --git a/docs/status/M12-03E.md b/docs/status/M12-03E.md new file mode 100644 index 00000000..abb195cc --- /dev/null +++ b/docs/status/M12-03E.md @@ -0,0 +1,54 @@ +# M12-03E Status + +status: done +task: append undo/redo/save/reopen and desktop canonical comparison +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The M12-03D WASM Main append path is compared with the M12-03C Blender 5.2 desktop canonical +report. One append transaction creates the Object/Mesh/Material/packed Image local closure and +advances exactly one SceneIR revision. Undo removes all four IDs, redo restores the canonical +graph, and a saved buffer reopens with the same graph and packed image pixels. + +The image comparison normalizes Blender's bottom-up `Image.pixels` row order versus Canvas +top-down `ImageData`. Current SceneIR omits `Image.colorSpace`, so a missing field uses the desktop +canonical sRGB semantic default; an emitted colorspace must match it. All other graph, ID, +ownership, geometry, UV, material-slot, image metadata, and pixel hash fields remain exact. This +task does not claim Link, Library Override, or full N-023 IO parity. + +## Evidence + +- `node ../tools/web/check-library-main-append.mjs` passed the manifest, artifact hashes, canonical + desktop report, and required test markers. +- `WEB_TEST_PORT=5194 npm --prefix web run test:library-main-append` passed the checker and + Chromium 1/1. The browser test verified one transaction, four writable `LOCAL_MAIN` mappings, + one revision increment, complete Object -> Mesh -> Material -> Image closure, and exact + canonical graphs after append, redo, and save/reopen. +- A stale base revision returned `REVISION_CONFLICT`; a local ID collision returned + `ASSET_MANIFEST_INVALID` without changing the appended revision. Undo reported no Object, Mesh, + Material, or Image; redo restored the Object and canonical closure. +- The canonical packed image remained a 2x2 sRGB RGBA PNG with Float32 pixel SHA-256 + `6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a`. + +## Artifact Hashes + +- checker: `7fba72c5a38c0877f03682b51cbaaaf6d9a5f315f2beddfdef432bc54b2f1cd2` +- test: `101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0` +- package commands: `36a29c5be9bd6163b06cf3511edf77d932388fa0b75c08bdcec397e19c0ef45f` +- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606` +- clean target `.blend`: `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63` +- desktop report: `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a` +- manifest: `beaa88ea0385225e712f9816072420bd8744c15da3229ddc352462abec407739` + +## Next Task + +`M12-03F`: desktop link fixture preserving source library and read-only ownership. + +## Rollback + +Remove the M12-03E checker, manifest, extended Chromium test, package command, and this status +entry. Restore M12-03D to pending and move the machine queue back to `M12-03D`. No parity ledger +rollback is required. diff --git a/docs/status/M12-03F.md b/docs/status/M12-03F.md new file mode 100644 index 00000000..d37e0e30 --- /dev/null +++ b/docs/status/M12-03F.md @@ -0,0 +1,49 @@ +# M12-03F Status + +status: done +task: create a desktop linked Object fixture preserving source-library ownership +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Blender 5.2 creates a source library containing one Object with a Mesh, Material, and packed Image +dependency closure. A clean target links only the selected Object with `bpy.data.libraries.load(link=True)`. +The Object, Mesh, Material, and Image retain the source library pointer, have no library override, +and map to read-only `SOURCE_LIBRARY` ownership. The target is saved, reopened in a new Main load, +and compared to the pre-save linked graph. + +This task freezes the desktop LINK contract only. It does not implement a WASM link writer, linked +mutation, reload/relocate, missing-library placeholders, or full N-023 parity. + +## Evidence + +- `npm --prefix web run test:library-link-desktop` passed the manifest/artifact hash gate, Blender + 5.2 generator rerun, and canonical report comparison. +- The fixture links one root and four dependency IDs. All three dependency edges, 4 vertices, 4 + edges, 1 polygon, 4 loops, `UVMap`, material slot, packed 2x2 sRGB RGBA image, and Float32 pixel + SHA-256 `6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a` survive save/reopen. +- Every linked ID reports `library=m12_link_source.blend` and `isLibraryOverride=false`; all four + stable mappings are `SOURCE_LIBRARY/readOnly=true`. A temporary source/target regeneration matches + the canonical report after normalizing only session-bound blend container hashes. + +## Artifact Hashes + +- generator: `15a2e34c1c5b2a8ee63b10084dbb894e0f9677b9e1cf4adb7e2ddce6b4c965b1` +- checker: `6a4c024bea227d7bc14f793467b91766b006459fe4d7717cc75dfee12f49f894` +- source `.blend`: `fae97569e9d2e2066fc92749672f86cc42717cd9b97b012faeb9eb92015d7fd1` +- target `.blend`: `acdaf0f297deb08c84e1a8beba30972e3414ef62e60e3b103fe0661199c438a1` +- desktop report: `b278d4c254eff63d41c8cb3ea1d5e0984192137d45b1695ba0672e16f95b58ea` +- manifest: `8220a8f5d560d45a75a62cbed645b3febc1390fe37b07c3c48871fefc1a9b159` +- package commands: `336d8df1914aaaafaeccc181d4e73ed7058165f10e167efe539939c3ac1e0536` + +## Next Task + +`M12-03G`: linked data writers must fail closed with `LINKED_DATA_MUTATION_BLOCKED`. + +## Rollback + +Remove the M12-03F link generator/checker, source and target fixtures, desktop report/manifest, +package command, and this status entry. Restore M12-03E to pending and move the machine queue back +to `M12-03E`. No parity ledger rollback is required. diff --git a/docs/status/M12-03G.md b/docs/status/M12-03G.md new file mode 100644 index 00000000..794eb743 --- /dev/null +++ b/docs/status/M12-03G.md @@ -0,0 +1,45 @@ +# M12-03G Status + +status: done +task: block every linked data writer before Main mutation +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The schema-1 linked mutation gate covers object transforms, mesh geometry, mesh material slots, +material properties, material image nodes, and packed image data. Every request must identify a +`SOURCE_LIBRARY` owner with `linkedLibrary=true` and `readOnly=true`. A current linked request is +blocked with `LINKED_DATA_MUTATION_BLOCKED` before any Main writer can run; stale revisions remain +`REVISION_CONFLICT`, and malformed or ownership-substituted requests fail closed. + +This task does not claim a linked writer, reload, relocation, missing-library recovery, or full +N-023 Link parity. The desktop linked ownership contract is the M12-03F input to this gate. + +## Evidence + +- `npm --prefix web run test:library-linked-mutation` passed 3/3 unit tests. +- All six linked writer operations return `BLOCKED` with exactly + `LINKED_DATA_MUTATION_BLOCKED`, `recoverable=false`, and no Main mutation path. The test also + verifies the schema round-trip for each operation. +- A stale revision returns `REVISION_CONFLICT`; an unknown field, unsupported operation, or owner / + read-only substitution returns `TASK_VALIDATION_FAILED` or `LINKED_DATA_MUTATION_BLOCKED` before + any writer invocation. + +## Artifact Hashes + +- protocol: `f629e0e7e04dc1f5437b6e2ca62fbe35484fc238830fa47e8358bcab46b7e104` +- unit: `f19d47cefe89daf6123062e045ec717e6ffe60977e8a4b20c996dd62e49da211` +- manifest: `890891fda5c91b08d157927170fb33190ae0d8a49f8962b88c8554307c0c06a8` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03H`: library reload replaces only the matching linked generation snapshot. + +## Rollback + +Remove the linked mutation protocol, unit suite, manifest, package command, and this status entry. +Restore M12-03F to pending and move the machine queue back to `M12-03F`. No parity ledger rollback +is required. diff --git a/docs/status/M12-03H.md b/docs/status/M12-03H.md new file mode 100644 index 00000000..8d2b74c6 --- /dev/null +++ b/docs/status/M12-03H.md @@ -0,0 +1,43 @@ +# M12-03H Status + +status: done +task: library reload replaces only the matching linked generation snapshot +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 reload requests identify one `SOURCE_LIBRARY` and an expected generation/revision. The +replacement must keep the same library identity, advance exactly one generation, advance the source +revision, and retain a read-only source-library data-block closure. Only the unique matching snapshot +is replaced; other libraries and other generations remain unchanged. + +Malformed requests, duplicate state identities, owner substitution, skipped generations, and stale +generation/revision requests fail closed before any Main mutation. A stale request returns +`REVISION_CONFLICT` and an unchanged state. + +## Evidence + +- `npm --prefix web run test:library-linked-reload` passed 4/4 unit tests. +- The matching-generation case replaces one snapshot atomically while preserving a future generation + and an unrelated library; the input state remains immutable. +- Stale, malformed, duplicate, owner-substituted, and non-adjacent reload cases are rejected without + publishing a partial state. + +## Artifact Hashes + +- protocol: `8be6f0b2abe36cd566ea7447d1b7de44c0e6a9a7351b863dfdd1372c1761060e` +- unit: `dff866291468cc01e775fe3b3b95c632f5c0742566f79b956a0193bfd8fd43d2` +- manifest: `dd96702dcb064779de3fc33ce1200d75615e7b75a3418a45a309e0e612fdb7df` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03I`: missing library preserves a placeholder and the original source reference. + +## Rollback + +Remove the linked reload protocol, unit suite, manifest, package command, and this status entry. +Restore M12-03G to pending and move the machine queue back to `M12-03H`. No parity ledger rollback +is required. diff --git a/docs/status/M12-03I.md b/docs/status/M12-03I.md new file mode 100644 index 00000000..ab441093 --- /dev/null +++ b/docs/status/M12-03I.md @@ -0,0 +1,42 @@ +# M12-03I Status + +status: done +task: missing library preserves placeholder and original source reference +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 missing-library requests match a linked source library by source identity, generation, and +revision. Marking a matching reference missing changes only its status and adds a +`MISSING_LIBRARY` placeholder. The original locator, source SHA-256, generation, revision, and every +data-block ID remain attached to the reference; unrelated libraries remain unchanged. + +Stale generation/revision and source hash drift return a stable conflict without deleting or replacing +the reference. Malformed fields, duplicate identities, and inconsistent placeholder data fail closed. + +## Evidence + +- `npm --prefix web run test:library-linked-missing` passed 4/4 unit tests. +- The matching case preserves original source metadata and data-block IDs while publishing only the + placeholder status transition. +- Stale, source-drift, undeclared-field, duplicate-identity, and invalid-placeholder cases publish no + partial state. + +## Artifact Hashes + +- protocol: `5833e8c943ef6bd866a93a0e1666c9521fa6d3e8358861df57b628874b679ec2` +- unit: `4ab6d6ff4845b4ca963399e7eea214ceb412871dc1fdef5bac1ed0333596da4f` +- manifest: `f5fa606161b93e9ee42e7ca8144d2a83379acf601c5045eb8cc76732f8dba431` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03J`: desktop override fixture records reference, local owner, and property override path. + +## Rollback + +Remove the missing-library protocol, unit suite, manifest, package command, and this status entry. +Restore M12-03H to pending and move the machine queue back to `M12-03I`. No parity ledger rollback +is required. diff --git a/docs/status/M12-03J.md b/docs/status/M12-03J.md new file mode 100644 index 00000000..a4e2bc03 --- /dev/null +++ b/docs/status/M12-03J.md @@ -0,0 +1,45 @@ +# M12-03J Status + +status: done +task: desktop override fixture records reference, local owner, and property override path +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Blender 5.2 creates a real linked Object and converts it through the library override API. The fixture +records the linked reference as `SOURCE_LIBRARY/readOnly=true`, the local hierarchy root as +`LOCAL_OVERRIDE/readOnly=false`, and one explicit custom-property override path. The source marker, +reference ID, local owner, hierarchy root, property operation and value remain stable after save/reopen. + +This fixture does not open a general override writer; it freezes the reference/owner/property contract +needed by the next bounded writer task. + +## Evidence + +- `npm --prefix web run test:library-override-desktop` passed. +- Blender 5.2 source/target fixture regeneration matched the normalized desktop report. +- The reference retains `m12_override_source.blend`; the local object is `LOCAL_OVERRIDE`; the only + recorded property path is `["m12_override_value"]` with value `2.5` and one `REPLACE` operation. +- Save/reopen preserves the reference, hierarchy root, owner semantics, and property metadata. + +## Artifact Hashes + +- generator: `2cdbac04cac7240360a9d70919380fd90f9479e2cb41d8032fb51626ed4b4dd6` +- checker: `afebb3c9b8715b9d2e0c9b17f463f038bd23e8747074137bccbf1c09c4bfb5ba` +- source blend: `d7f8d78e7e91481bf46ecc9a6bcb01e900a6a397839dd31ab80a53def7675601` +- target blend: `b008a1608ff1e8f679e1e1281bf7be0360f1137e815dd890cbd93e1e98675495` +- report: `19cb13a3417b4b65a8497b622337c42c4697b3f3d48de26a1f70f2d4527ddde0` +- manifest: `01d0f66bb3819eea3e92727d1b5bffbec935d24eeecc28d874b999df78d73fbf` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03K`: expose exactly one verified override writer property. + +## Rollback + +Remove the override generator/checker, fixture, report, manifest, package command, and this status +entry. Restore M12-03I to pending and move the machine queue back to `M12-03J`. No parity ledger +rollback is required. diff --git a/docs/status/M12-03K.md b/docs/status/M12-03K.md new file mode 100644 index 00000000..9bc419b6 --- /dev/null +++ b/docs/status/M12-03K.md @@ -0,0 +1,42 @@ +# M12-03K Status + +status: done +task: expose exactly one verified override writer property +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 override writer accepts exactly `SET_M12_OVERRIDE_VALUE` for the property path +`["m12_override_value"]`. It requires `LOCAL_OVERRIDE`, `readOnly=false`, +`referenceReadOnly=true`, matching local/reference/hierarchy IDs, and the current revision. A valid +write updates only the verified value and advances the local revision by one. + +Linked ownership, a second property path, identity drift, stale revision, malformed fields, and values +outside the bounded float range are blocked before any writer commit. + +## Evidence + +- `npm --prefix web run test:library-override-writer` passed 4/4 unit tests. +- The valid request applies one property and increments revision exactly once while preserving local + override ownership and reference read-only semantics. +- Stale, linked-owner, identity, alternate-operation, alternate-property, malformed, and out-of-range + requests fail closed. + +## Artifact Hashes + +- protocol: `dd181c7e9946b98334a5d1c686887afecfe3fc11d732beec246e40bf11a155aa` +- unit: `e41bd32eb4ce4d331a20ecb91f3325a27f461b9ae85e98940d5afd482ec21c4c` +- manifest: `9eeee93e4a806aa59b5c53a6485fe3a1b5e3972c1d2585cdc71b7cfc293afb70` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03L`: block override stale source/revision before Main commit. + +## Rollback + +Remove the override writer protocol, unit suite, manifest, package command, and this status entry. +Restore M12-03J to pending and move the machine queue back to `M12-03K`. No parity ledger rollback +is required. diff --git a/docs/status/M12-03L.md b/docs/status/M12-03L.md new file mode 100644 index 00000000..97776d2f --- /dev/null +++ b/docs/status/M12-03L.md @@ -0,0 +1,41 @@ +# M12-03L Status + +status: done +task: block override stale source/revision before Main commit +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 override freshness binds the local override to its source library ID, source generation, +source revision, dependency-closure SHA-256, invalidation token, and local/reference/hierarchy IDs. +Only a fully matching `COMMIT_OVERRIDE` request reaches `READY`; no Main mutation is performed by +this gate. + +Stale generation/revision, dependency closure, invalidation token, linked ownership, identity drift, +alternate operation, malformed token, and undeclared fields return stable blocking codes before Main. + +## Evidence + +- `npm --prefix web run test:library-override-freshness` passed 4/4 unit tests. +- The exact source generation/revision/closure/token binding returns `READY`. +- Stale source fields return `REVISION_CONFLICT`; local identity drift returns + `ASSET_SOURCE_HASH_MISMATCH`; linked ownership returns `LINKED_DATA_MUTATION_BLOCKED`. + +## Artifact Hashes + +- protocol: `2eff7ea7605b1579d7551336d87d4f30adb996b585596ff9eee67aea04ca7d22` +- unit: `d48344f31e1ba12e557ca30ece56643583efa633da556f5de3896a8e9175ef8f` +- manifest: `237f3f168e037b67b805ba8d9c9455250c889a5e90c45cbb17d2d20d9fbdcc50` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03M`: dependency cycle, ID collision, cross-library cycle, and duplicate reload negatives. + +## Rollback + +Remove the override freshness protocol, unit suite, manifest, package command, and this status entry. +Restore M12-03K to pending and move the machine queue back to `M12-03L`. No parity ledger rollback +is required. diff --git a/docs/status/M12-03M.md b/docs/status/M12-03M.md new file mode 100644 index 00000000..88d7fa77 --- /dev/null +++ b/docs/status/M12-03M.md @@ -0,0 +1,38 @@ +# M12-03M Status + +status: done +task: dependency cycle, ID collision, cross-library cycle, and duplicate reload negatives +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 negative-case validation checks library dependency graphs, cross-library references, +data-block identity ownership, and reload generation identities. Acyclic graphs with unique IDs are +accepted; dependency/cross-library cycles, missing source libraries, duplicate data-block IDs, and +duplicate reload generations fail with stable codes before any writer or Main mutation. + +## Evidence + +- `npm --prefix web run test:library-negative-cases` passed 4/4 unit tests. +- Direct dependency cycles and cross-library cycles return `LIBRARY_DEPENDENCY_CYCLE`. +- Data-block collision returns `TASK_VALIDATION_FAILED`; duplicate reload returns `REVISION_CONFLICT`; + a missing source library returns `ASSET_SOURCE_HASH_MISMATCH`. + +## Artifact Hashes + +- protocol: `efc7cc810089eab6178fc5c2f2a45d641625a032ecfa5b2b4ef37694d0decc97` +- unit: `b7ae41b2c35b2fe1598bca4425dd434230bfe4bf342320c88214a060dcbb0a16` +- manifest: `693cbedfe8e29167283a753d119de5a9cfe5e20bc96aff7ba84d720ffde9148a` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-03N`: append/link/override each receive independent desktop/WASM/Chromium commands. + +## Rollback + +Remove the negative-case protocol, unit suite, manifest, package command, and this status entry. +Restore M12-03L to pending and move the machine queue back to `M12-03M`. No parity ledger rollback +is required. diff --git a/docs/status/M12-03N.md b/docs/status/M12-03N.md new file mode 100644 index 00000000..614e2344 --- /dev/null +++ b/docs/status/M12-03N.md @@ -0,0 +1,42 @@ +# M12-03N Status + +status: done +task: append/link/override each receive independent desktop/WASM/Chromium commands +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Append, LINK, and library override each have independent desktop, WASM, and Chromium commands. The +commands are bound to distinct fixtures/protocol tests/browser specs and use separate Chromium ports. +The command checker verifies all nine package entries and their lane-specific targets. + +## Evidence + +- `npm run test:library-operation-commands` passed with 9 independent commands: 3 operations x 3 lanes. +- Desktop: append fixture, LINK fixture, and Blender 5.2 override fixture all passed. +- WASM: append receipt protocol passed 2/2; LINK aggregate passed 11/11; override aggregate passed 8/8. +- Chromium: append passed 1/1; LINK gate passed 1/1 on port 5195; override writer passed 1/1 on port + 5196. Append Chromium passed 1/1 on port 5194. + +## Artifact Hashes + +- command checker: `3564347393134d835fdf279b8b8f58558ee24fe0165c3b4527195d094a451e98` +- append WASM protocol: `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7` +- append WASM unit: `75fc2d626f7ca7e820e9cacf659a453886e15e790cde43348828c03bed752ec7` +- append Chromium: `101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0` +- link Chromium: `814e3486522fb4f0ffdd59acd385a4fca9c5660cdb07e5a2acb1cadd70376bff` +- override Chromium: `1c12982b4eb4fb4b9a3c88907a842a1fc413b3a3a760a9f57b350f57060d007f` +- manifest: `e4cccc8edc277b5047c3f8006ea40b26b119d28589fe2f3bd9bfa4cc3575c85a` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04A`: library source schema accepts only declared HTTPS origins, project assets, or user-selected files. + +## Rollback + +Remove the independent command checker, lane-specific tests/wrappers, manifest, package commands, and this +status entry. Restore M12-03M to pending and move the machine queue back to `M12-03N`. No parity ledger +rollback is required. diff --git a/docs/status/M12-04A.md b/docs/status/M12-04A.md new file mode 100644 index 00000000..52b71734 --- /dev/null +++ b/docs/status/M12-04A.md @@ -0,0 +1,40 @@ +# M12-04A Status + +status: done +task: library source schema accepts declared HTTPS origin, project asset, or user-selected file +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 source admission accepts exactly three declared kinds: HTTPS URLs whose credential-free origin +is present in the policy, project-relative asset paths normalized by the existing project-path gate, +and user-selected files carrying a stable selection ID, safe file name, bounded byte length, and +source SHA-256. Accepted sources receive a canonical locator. + +Undeclared origins, credentials, unsafe project paths, empty origin policy, malformed file identity, +and undeclared fields fail closed before any library load. + +## Evidence + +- `npm --prefix web run test:library-source-origin` passed 4/4 unit tests. +- Declared HTTPS, project asset, and user-selected file cases return `READY` with canonical locators. +- Undeclared HTTPS, credential-bearing URLs, traversal paths, empty policies, malformed selection IDs, + and undeclared fields return stable blocking errors. + +## Artifact Hashes + +- protocol: `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb` +- unit: `89b207c9cb13b4fb055a2dfed0237c0f8a00b13a39cc4175a378f5ef2abdb7ae` +- manifest: `af80827d925ddd96d8541e446e0f70c956fd4c56e64ac984d187f5449df4cb0d` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04B`: normalize POSIX/Windows separators, `.`, `..`, percent encoding, and Unicode names. + +## Rollback + +Remove the source-origin protocol, unit suite, manifest, package command, and this status entry. Restore +M12-03N to pending and move the machine queue back to `M12-04A`. No parity ledger rollback is required. diff --git a/docs/status/M12-04B.md b/docs/status/M12-04B.md new file mode 100644 index 00000000..e49d7337 --- /dev/null +++ b/docs/status/M12-04B.md @@ -0,0 +1,42 @@ +# M12-04B Status + +status: done +task: normalize POSIX/Windows separators, dot segments, percent encoding, and Unicode names +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The shared project-asset path normalizer now emits one canonical `/`-separated path. It accepts +equivalent POSIX/Windows separators, removes `.` segments, resolves bounded `..` segments without +allowing project-root escape, decodes percent-encoded UTF-8 once, and applies Unicode NFC normalization. +Residual percent octets, malformed encoding, controls, absolute paths, URI schemes, and traversal beyond +the project remain fail-closed. + +## Evidence + +- `npm --prefix web run test:library-path-normalization` passed 4/4 unit tests. +- Separator and dot aliases converge to one idempotent canonical path. +- Percent-encoded separators/dot segments and decomposed Unicode names match the canonical project asset + locator used by the M12-04A source admission path. +- Re-decoding, malformed percent encoding, and project-root escape return stable path errors. +- `npm --prefix web run typecheck` passed after closing the prior library wrapper/parser type errors. + +## Artifact Hashes + +- parent manifest: `af80827d925ddd96d8541e446e0f70c956fd4c56e64ac984d187f5449df4cb0d` +- normalizer: `6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf` +- source admission: `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb` +- unit: `abde64c60397541e92a83dd9e4a24e65faf340a8d046650604c101a21037c777` +- manifest: `8cd29c3f5281082584fc6aefe2ec385a2eedf8116e837a4db54c391391ff8f98` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04C`: reject absolute paths, UNC, drive paths, NUL, control characters, and origin escape. + +## Rollback + +Remove the path normalizer changes, unit suite, manifest, package command, and this status entry. Restore +M12-04A to pending and move the machine queue back to `M12-04B`. No parity ledger rollback is required. diff --git a/docs/status/M12-04C.md b/docs/status/M12-04C.md new file mode 100644 index 00000000..54859515 --- /dev/null +++ b/docs/status/M12-04C.md @@ -0,0 +1,45 @@ +# M12-04C Status + +status: done +task: reject absolute paths, UNC/drive paths, controls, and origin escape +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The project-asset path gate rejects absolute POSIX paths, UNC and drive-shaped paths, NUL/control +characters, malformed or residual percent encoding, and URI/origin-shaped escapes before a locator is +accepted. HTTPS source policy entries are now strict credential-free origins: path, query, fragment, +encoded controls, and backslash smuggling are rejected instead of being silently reduced to +`URL.origin`. HTTPS resource paths retain the declared-origin check and reject unsafe decoded bytes. + +## Evidence + +- `npm --prefix web run test:library-path-security` passed 4/4 unit tests. +- Absolute, UNC, drive, raw/encoded NUL and control characters, and origin-style project paths return + stable `ASSET_PATH_OUTSIDE_PROJECT`/`ASSET_PATH_INVALID` errors. +- Raw and encoded backslashes, controls, malformed percent sequences, credentials, undeclared origins, + and policy origin smuggling return `IO_EXTERNAL_URI_BLOCKED`. +- Declared `https://assets.example.test/library/main.blend` remains accepted; policy declarations with + a path, query, fragment, encoded control, or duplicate canonical origin fail closed. +- `WEB_TEST_PORT=5323 npm --prefix web run test:asset-library` passed the N-023 Chromium asset/IO gate 1/1. +- `npm --prefix web run typecheck` passed. + +## Artifact Hashes + +- parent manifest: `8cd29c3f5281082584fc6aefe2ec385a2eedf8116e837a4db54c391391ff8f98` +- path normalizer: `6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf` +- source-origin protocol: `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb` +- unit: `ab302cacb24634a3225dda5cb8f5282cb80b3bd81ed5c8900ddf4ff18267b7e2` +- manifest: `a7f3a45eb7f68d022f38185a1c1409e1db1b27647fef587d66d2ffa52d78f98e` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04D`: resolve symlink/hardlink entries before writing and constrain them to the temporary root. + +## Rollback + +Remove the C path-security assertions, manifest, status entry, and strict origin validation. Restore +M12-04B to pending and move the machine queue back to `M12-04C`. No parity ledger rollback is required. diff --git a/docs/status/M12-04D.md b/docs/status/M12-04D.md new file mode 100644 index 00000000..39ca35ca --- /dev/null +++ b/docs/status/M12-04D.md @@ -0,0 +1,44 @@ +# M12-04D Status + +status: done +task: resolve symlink/hardlink entries before writing and constrain them to the temporary root +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Archive entries are parsed before any write. Every member is canonicalized as a relative path under +the declared temporary-root identity. Symlink targets resolve relative to the link's parent; hardlink +targets resolve from the archive root and must end at a regular file. Absolute, drive-shaped, URI, +backslash-smuggled, traversal, missing-target, cyclic, duplicate, and hardlink-to-directory inputs +fail closed with `IO_ARCHIVE_UNSAFE`. + +The resolver returns a write plan with the final in-root target and an explicit +`withinTemporaryRoot: true` proof for every member. It does not follow or write any link before the +entire manifest has passed validation. + +## Evidence + +- `npm --prefix web run test:library-link-safety` passed 5/5 unit tests. +- Parent-directory symlinks, archive-root hardlinks, and chained links resolve to canonical in-root + members. +- Absolute/UNC/drive/URI/traversal targets, missing members, cycles, duplicate paths, undeclared + fields, and hardlinks to directories are rejected before a write plan is returned. + +## Artifact Hashes + +- parent manifest: `a7f3a45eb7f68d022f38185a1c1409e1db1b27647fef587d66d2ffa52d78f98e` +- protocol: `d55a4ba762898aed22bdcc7aa6493c713ee94f6bb1bf58754fdc459d0ddf70d1` +- unit: `7739275be91222d7bfb61d2f5a1daf812c6860fe34d0aea27df8380a77322120` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04E`: archive first reads the central directory/manifest and does not extract payload first. + +## Rollback + +Remove the archive link-safety protocol, unit suite, manifest, package command, and this status entry. +Restore M12-04C to pending and move the machine queue back to `M12-04D`. No parity ledger rollback is +required. diff --git a/docs/status/M12-04E.md b/docs/status/M12-04E.md new file mode 100644 index 00000000..7ce8d409 --- /dev/null +++ b/docs/status/M12-04E.md @@ -0,0 +1,38 @@ +# M12-04E Status + +status: done +task: archive first reads the central directory or manifest before payload extraction +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +ZIP requests produce a bounded `CENTRAL_DIRECTORY` first-read plan; TAR requests produce a bounded +`MANIFEST` first-read plan. The plan contains no payload ranges. A read trace is accepted only when +the first exact range is the declared metadata range and every later range is payload; payload-first, +wrong-range, duplicate-metadata, out-of-order, oversized, and out-of-archive reads fail closed with +`IO_ARCHIVE_UNSAFE`. + +## Evidence + +- `npm --prefix web run test:library-metadata-first` passed 4/4 unit tests. +- ZIP and TAR plans expose metadata-only first reads with an empty payload plan. +- Payload-first, wrong-range, duplicate metadata, out-of-archive, and metadata-budget cases are + rejected before extraction can be scheduled. + +## Artifact Hashes + +- parent manifest: `1c09abd1f4bd5908d22ab3b22e3e71d050f694af0b82a7f78a351d7a1bf1e664` +- protocol: `869586b041e134c7d1cb2ebd7e13b35218b337223d401697a179f71cd1420f5b` +- unit: `2da649722acee9cace8db6f337b4a93500a9c775a0b0f086bf38dd2b3e7f9e91` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04F`: enforce per-entry bytes, total bytes, entry count, directory depth, and filename length budgets. + +## Rollback + +Remove the metadata-first protocol, unit suite, manifest, package command, and this status entry. Restore +M12-04D to pending and move the machine queue back to `M12-04E`. No parity ledger rollback is required. diff --git a/docs/status/M12-04F.md b/docs/status/M12-04F.md new file mode 100644 index 00000000..58c5e78d --- /dev/null +++ b/docs/status/M12-04F.md @@ -0,0 +1,37 @@ +# M12-04F Status + +status: done +task: enforce archive entry, total, count, directory-depth, and filename-length budgets +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production `asset-library-io` parser now enforces the existing compressed/uncompressed per-entry +and total-byte budgets together with a 100,000-entry limit, 64 directory levels, and a 255-byte UTF-8 +filename limit. Compression-ratio and declared source-byte checks remain fail-closed in the same path. +Budget checks happen while parsing metadata, before any payload allocation or extraction. + +## Evidence + +- `npm --prefix web run test:library-archive-budget` passed 4/4 unit tests. +- Exact boundary cases for entry bytes, total bytes, entry count, directory depth, and Unicode filename + bytes are accepted; one-byte overflows return stable budget errors. +- Existing compression-ratio, duplicate/prefix, and declared source-byte checks remain covered. + +## Artifact Hashes + +- parent manifest: `d935392fb23c2e6ad651fb6ad6cb67f8ead3d562e626bb230d2febd9d7f03b1c` +- protocol: `e02efa79668f4ee10b1c28786709eba2c93691b28ccf1155c6213dda12f59a8f` +- unit: `b0d8356c6fb348d98e28ce220052845a29439b34b3c976b21a4dbf370ea19593` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04G`: reject compression-ratio, overlapping range, duplicate path, and file/directory prefix conflicts. + +## Rollback + +Remove the budget checks, unit suite, manifest, package command, and this status entry. Restore M12-04E +to pending and move the machine queue back to `M12-04F`. No parity ledger rollback is required. diff --git a/docs/status/M12-04G.md b/docs/status/M12-04G.md new file mode 100644 index 00000000..aef00d9f --- /dev/null +++ b/docs/status/M12-04G.md @@ -0,0 +1,39 @@ +# M12-04G Status + +status: done +task: reject archive compression bombs, overlapping ranges, duplicate paths, and prefix conflicts +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Archive range validation now checks each compressed range against the declared source length and the +global archive bound, sorts ranges by offset, and rejects overlap before any payload read. It also +rejects duplicate canonical paths, file/directory prefix conflicts, zero-byte compression bombs, and +expansion ratios above 100:1. Valid ranges return deterministic totals and explicit non-overlap/path +invariants. + +## Evidence + +- `npm --prefix web run test:library-archive-conflicts` passed 4/4 unit tests. +- Non-overlapping ranges produce stable compressed/uncompressed totals. +- Compression-ratio, overlap, duplicate, prefix, source-bound, and undeclared-field negatives all + return `IO_ARCHIVE_UNSAFE` before payload scheduling. + +## Artifact Hashes + +- parent manifest: `cda905b1e27b62d9ea3a05170f975015480ce6739c15bdf1f5a1f0b79f93ce06` +- protocol: `3bec372e4f67ec309f28534cebcbaf8b492144adfa82ff6c8603f88c3ba16254` +- unit: `3f2d44dce33b1c17b3a48f58b04fdd821abc88d98ce3d3b5bb724859e0f0ab3c` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + +## Next Task + +`M12-04H`: cancellation removes staging and never modifies the committed project. + +## Rollback + +Remove the archive conflict protocol, unit suite, manifest, package command, and this status entry. +Restore M12-04F to pending and move the machine queue back to `M12-04G`. No parity ledger rollback is +required. diff --git a/docs/status/M12-04H.md b/docs/status/M12-04H.md new file mode 100644 index 00000000..b09adb83 --- /dev/null +++ b/docs/status/M12-04H.md @@ -0,0 +1,48 @@ +# M12-04H Status + +status: done +task: remove extraction staging on cancellation without modifying the committed project +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production archive extraction transaction checks cancellation before staging, around every +payload read and write, and immediately before atomic commit. A pre-commit cancellation returns +`IO_ARCHIVE_CANCELLED`, removes every staged file, publishes no project, and re-reads the committed +project identity to prove that revision and SHA-256 did not change. Once atomic commit starts, +cancellation no longer converts the completed commit into a cancelled result. + +Payload identity failure and staging creation/write failure use the same cleanup path. If cleanup +leaves a staged entry or the committed identity drifts, the transaction fails with +`STORAGE_TRANSACTION` instead of claiming successful cancellation. + +## Evidence + +- `npm --prefix web run test:library-archive-cancellation` passed 6/6 unit tests against real + temporary directories. +- Cancellation before staging, after one staged file, and after the final staged write all leave + zero staging files, zero published projects, and the original committed project bytes. +- The success path publishes revision 9 only after both staged payload hashes pass. +- Payload mismatch, rollback identity drift, stale revision, unsafe path, prefix conflict, and + undeclared-field negatives fail closed. + +## Artifact Hashes + +- parent manifest: `c35e675e8f512e85b748f0b5578874fed8c99df7674a152e698236a67c9f4fa4` +- protocol: `c40adc1449172014cc1820db567e155828314abb404b66e4de13c26ddee06e4b` +- error codes: `751c70c898540fa7e82fb1b1a79254756ec8db8e4201a88b6d817d7c3d92103f` +- unit: `77e7d5bc64dd6b313006ebf523602601acdaf7949a1484da872ddcd046983786` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `7fce600a416aec5ade1a91a13d86caf4cb1f65b710054b59d525ff8b1d3c7409` + +## Next Task + +`M12-04I`: release temporary files after quota/OOM and allow a small archive to recover. + +## Rollback + +Remove the extraction transaction protocol, cancellation code, real-filesystem unit suite, manifest, +package command, and this status entry. Restore M12-04G as the queue tail and move the machine queue +back to `M12-04H`. No parity ledger rollback is required. diff --git a/docs/status/M12-04I.md b/docs/status/M12-04I.md new file mode 100644 index 00000000..6520509b --- /dev/null +++ b/docs/status/M12-04I.md @@ -0,0 +1,47 @@ +# M12-04I Status + +status: done +task: release archive staging after quota/OOM and recover with a small archive +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The archive recovery wrapper maps browser/OPFS quota failures to `STORAGE_QUOTA` and declared +allocation failures to `WASM_OUT_OF_MEMORY` only after the M12-04H base transaction has removed +staging and re-read the committed project identity. Cleanup or identity drift remains +`STORAGE_TRANSACTION`; unrelated range and IO failures are not misclassified as OOM. + +Real temporary-directory tests partially write the failing payload before injecting each fault. +Both paths leave zero staging entries and preserve the previous revision, SHA-256, and committed +bytes. The same storage instance then accepts and atomically commits a smaller archive without a +Worker or process restart. + +## Evidence + +- `node --test web/tests/unit/library-archive-recovery.test.mjs` passed 4/4. +- Quota and OOM each remove a partially written 4 KiB staging payload and preserve revision 9. +- Each failed storage instance immediately commits `small-project` at revision 10 with zero + staging entries. +- `npm --prefix web run test:library-archive-cancellation` passed 6/6 and + `npm --prefix web run typecheck` passed. + +## Artifact Hashes + +- parent manifest: `7fce600a416aec5ade1a91a13d86caf4cb1f65b710054b59d525ff8b1d3c7409` +- base transaction: `c40adc1449172014cc1820db567e155828314abb404b66e4de13c26ddee06e4b` +- recovery protocol: `0ae9801ea151403b244c5f58f7f99231bba7a25abb1f61e3669879510b92dccf` +- unit: `a931edef8f3ca1243a80ec2b8e9ff5b8da1dfd339f3d8c162ad2ebd08d3db6a1` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `574afa68a787b9481d0e098d38b94cb810a9827fe185b0df796d62cc160ba7c5` + +## Next Task + +`M12-04J`: add malicious ZIP/TAR fixtures to the long-term security regression. + +## Rollback + +Remove the recovery wrapper, real-filesystem unit suite, manifest, and this status entry. Restore +M12-04H as the queue tail and move the machine queue back to `M12-04I`. No parity ledger rollback is +required. diff --git a/docs/status/M12-04J.md b/docs/status/M12-04J.md new file mode 100644 index 00000000..bc8001f0 --- /dev/null +++ b/docs/status/M12-04J.md @@ -0,0 +1,49 @@ +# M12-04J Status + +status: done +task: keep malicious ZIP/TAR fixtures in the long-term archive security regression +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Six deterministic binary fixtures cover ZIP path traversal, compression-ratio abuse, duplicate +paths, TAR path traversal, symlink escape, and file/directory prefix conflict. The generator fixes +container fields and timestamps; the checker regenerates every file in a temporary directory and +requires byte-for-byte equality with the committed fixtures and catalog SHA-256 values. + +The checker reads ZIP EOCD/central-directory and USTAR header metadata, verifies local-header +bindings, TAR checksums, alignment, and end markers, then feeds the actual entry metadata into the +M12-04D/G link and conflict gates. It never invokes `tar`, `unzip`, or an extraction API. All six +fixtures fail closed with `IO_ARCHIVE_UNSAFE` and are included in the existing N-023 Chromium grep. + +## Evidence + +- `node tools/web/check-malicious-archive-fixtures.mjs` passed 6/6 with three ZIP and three TAR + fixtures and extraction disabled. +- `WEB_TEST_PORT=5408 npm --prefix web run test:asset-library` passed 2/2, including the persistent + binary-fixture security test and the existing N-023 asset/IO gate. +- Fixture byte lengths range from 115 to 3,072 bytes and every committed SHA-256 matches the + generated catalog. + +## Artifact Hashes + +- parent manifest: `574afa68a787b9481d0e098d38b94cb810a9827fe185b0df796d62cc160ba7c5` +- generator: `b372ea8cb2f97b035ffb2cc18666dae9167dcfb349131851ad9f1ff0fc40ba16` +- checker: `edda2f420f26e55f9990d24b755bb9b4f732ec32c2e072210144b3d0b6634d9b` +- fixture manifest: `a93834316f6a23b8a0e6c1f1f806ec584d6f03aa339c2680cae2ce8133a40e58` +- Chromium spec: `f327500808dd67359a152ce28134b58d027aebd6758416b5535b659b9900bbfe` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `989d417ab44e165849c7054f331f7038ec1d779349c39dd4f5b30050bbaecf56` + +## Next Task + +`M12-05A`: generate the glTF/GLB, OBJ, STL, PLY, USD, and Alembic format inventory from the pinned +Blender 5.2 build/runtime. + +## Rollback + +Remove the fixture generator/checker, six binary fixtures and catalog, Chromium spec, golden +manifest, and this status entry. Restore M12-04I as the queue tail and move the machine queue back to +`M12-04J`. No parity ledger rollback is required. diff --git a/docs/status/M12-05A.md b/docs/status/M12-05A.md new file mode 100644 index 00000000..b8cec622 --- /dev/null +++ b/docs/status/M12-05A.md @@ -0,0 +1,49 @@ +# M12-05A Status + +status: done +task: generate the Blender 5.2 runtime format inventory +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The pinned `build_blender_5.2.0/bin/blender` runtime generated seven separate format records: +GLTF, GLB, OBJ, STL, PLY, USD, and ALEMBIC. Each record binds import/export operator paths, +registered state, RNA identifier, canonical property identifiers and enum items, file extensions, +format variants, and the build option that gates the operator. + +The runtime receipt also binds Blender 5.2.0 LTS version tuple, build hash/branch/platform/type/date, +commit timestamp, selected build options, and the Blender binary SHA-256. On this pinned build, +GLTF/GLB/OBJ/STL/PLY are `AVAILABLE`; USD and Alembic are explicitly `OPERATOR_UNREGISTERED` because +`bpy.app.build_options.usd` and `alembic` are false. This is inventory evidence only and does not +claim import/export round-trip parity. + +## Evidence + +- `node tools/web/check-io-format-runtime-inventory.mjs` passed; the generator was rerun in a fresh + process and matched the committed JSON byte-for-byte. +- Inventory has 7 formats, 5 available format families and 2 build-disabled families. All available + operators have canonical non-empty RNA property lists; disabled operators fail closed. +- Runtime receipt records binary SHA-256 + `d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82`. + +## Artifact Hashes + +- parent manifest: `989d417ab44e165849c7054f331f7038ec1d779349c39dd4f5b30050bbaecf56` +- generator: `aa926397664240a5195f8864fc3ed5549bafcc963a03c513c7e37926b0559d7d` +- checker: `12853306ea5eb2698ab636c7dfc2f27ae140295641473c57b84f93fa13d4864e` +- inventory: `0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `202b144c91f8e2c39477e257aeb26f9bd0b1b05b459ff8a246668024e94deec7` + +## Next Task + +`M12-05B`: declare import/export, local/server, geometry/material/animation support separately for +each inventoried format. + +## Rollback + +Remove the runtime inventory generator/checker, format inventory, golden manifest, and this status +entry. Restore M12-04J as the queue tail and move the machine queue back to `M12-05A`. No parity +ledger rollback is required. diff --git a/docs/status/M12-05B.md b/docs/status/M12-05B.md new file mode 100644 index 00000000..b3295ecb --- /dev/null +++ b/docs/status/M12-05B.md @@ -0,0 +1,48 @@ +# M12-05B Status + +status: done +task: declare per-format import/export local/server and feature support +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The schema-1 capability matrix binds all seven M12-05A runtime formats and the inventory SHA-256. +Each format has separate IMPORT and EXPORT entries with local and server routes plus explicit +geometry, material, and animation feature status. A runtime operator being registered does not +make a Web route executable. + +The only `READY` route is the existing bounded GLB EXPORT local path. All seven IMPORT routes and +all server routes are explicit `BLOCKED/IO_FORMAT_UNSUPPORTED`; other local exports are also blocked. +GLB export features are marked `PARTIAL` with the M12-06 round-trip dependency. Unimplemented +operations stay `UNVERIFIED` rather than claiming geometry/material/animation support. + +## Evidence + +- `node --test web/tests/unit/io-format-capability-matrix.test.mjs` passed 3/3, including duplicate + format, ready-without-executor, unverified-ready-feature, and missing blocked-code negatives. +- `node tools/web/check-io-format-capability-matrix.mjs` passed; the matrix generator reproduced the + committed JSON byte-for-byte and runtime statuses matched the M12-05A inventory. +- The checker reports 7 formats, 1 local bounded GLB export route, and 27 blocked routes. + +## Artifact Hashes + +- parent manifest: `202b144c91f8e2c39477e257aeb26f9bd0b1b05b459ff8a246668024e94deec7` +- protocol: `3063ae5e45f5b1642d329aa554187d121998d816a5a6740a2b9662f00c3c5738` +- generator: `746078caaf29fa5aa2281b901b9ea5fc66f9a935eb3f6e282d4fbfb018d4c390` +- checker: `4a8b35cd7f87238c13627a00c3bb0b34c130fc34d597773574efac7d8909b804` +- unit: `0cc4d8f1df1ecdab20d8100cf5f12b44cb2a68bca4384ef7964a032b2f74b36e` +- matrix: `139c9d764736da176b32414ecda840c07eb0ba5f3864da2dc08ce04bae161366` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `8cc9517c9f25138c351bc5a79efe3b1ce6d9f6663d3b7c14397c5e4e8f11181a` + +## Next Task + +`M12-05C`: prevent matrix-undecared combinations from appearing in file selection and operator search. + +## Rollback + +Remove the capability matrix protocol/generator/checker, unit test, matrix, golden manifest, and this +status entry. Restore M12-05A as the queue tail and move the machine queue back to `M12-05B`. No +parity ledger rollback is required. diff --git a/docs/status/M12-05C.md b/docs/status/M12-05C.md new file mode 100644 index 00000000..a9b75220 --- /dev/null +++ b/docs/status/M12-05C.md @@ -0,0 +1,52 @@ +# M12-05C Status + +status: done +task: prevent matrix-undeclared combinations from appearing in file selection and operator search +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The schema-1 UI registry is generated from the M12-05B capability matrix and is bound to its exact +SHA-256. Only routes whose runtime operator is `AVAILABLE` and whose local matrix route is `READY` +are exposed to the UI. The current registry therefore accepts `.blend` projects, exposes the existing +bounded local GLB export command, and exposes no blocked import or export route. + +The file input uses the registry-derived accept string and performs a second filename gate before +opening. A matrix-blocked or unknown extension returns `IO_FORMAT_UNSUPPORTED` without invoking the +engine or replacing the current scene. Operator search filters every format-tagged command through the +same registry, so blocked server routes and undeclared formats cannot appear as executable results. + +## Evidence + +- Direct M12-05C lane passed: `node --test web/tests/unit/io-format-ui-gate.test.mjs`, + `node tools/web/check-io-format-ui-gate.mjs`, and Chromium 1/1 on dynamic ports 5413/5417. +- `node tools/web/check-io-format-ui-gate.mjs` reproduced the committed registry byte-for-byte; + import routes=0, local export routes=1, project accept=`.blend,application/octet-stream`. +- `npm --prefix web run typecheck` and `npm --prefix web run build` passed. +- `WEB_TEST_PORT=5412 npm --prefix web run test:asset-library` passed 2/2 N-023 Chromium regressions. +- An initial command without `WEB_TEST_PORT` was blocked by an already occupied 5173; rerunning on the + isolated dynamic port passed and left no server/profile process in the test lane. + +## Artifact Hashes + +- parent manifest: `8cc9517c9f25138c351bc5a79efe3b1ce6d9f6663d3b7c14397c5e4e8f11181a` +- protocol: `fc397ceb947d4ede6c34c1d51438253a6b59244fc40f5eb77ce155bf1c477476` +- generator: `1ef4ae8a3e8d2f73101a87cba1c42ea99a065e1f6846f6a591841b97c0c39e6f` +- checker: `81d6f27df26aab7b633fbe61c6d7b37519668aff41e43314924dceaacb3affde` +- unit: `84ca8fb6022da9f167daa104c44489a6c7d9f059699e57ce621b8d7f64f19082` +- Chromium: `eda2cdb9ede3bcbd717800c9c6fdb28d8cebef96f0296a2ee847a05e60cd0eed` +- registry: `6b410bc6f2cad032af55bf13e1c8ddd841b01e9913ffc0ba381da8b7204285fd` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `f46fd394e2144255d8b4304e8ce4fe60aad4302a80e991fd83d50cd915235ee3` + +## Next Task + +`M12-05D`: determine capability from runtime receipts and never infer it from filename extensions. + +## Rollback + +Remove the UI registry protocol, generator/checker, registry, unit/Chromium tests, package command, +golden manifest, and this status entry. Restore M12-05B as the queue tail and move the machine queue +back to `M12-05C`. No parity ledger rollback is required. diff --git a/docs/status/M12-05D.md b/docs/status/M12-05D.md new file mode 100644 index 00000000..11495cfe --- /dev/null +++ b/docs/status/M12-05D.md @@ -0,0 +1,54 @@ +# M12-05D Status + +status: done +task: determine format capability from pinned Blender runtime receipts, never filename extensions +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The schema-1 runtime receipt set is generated from the pinned Blender 5.2 M12-05A inventory and +bound to that inventory's SHA-256. Each format/operation receipt preserves the operator path, RNA +identifier, registered state, build option state, variants, and descriptive extensions. A route is +`READY` only when the receipt says `AVAILABLE`, the operator is registered, an RNA identifier exists, +and the build option is not disabled. + +The route resolver takes an explicit format and operation from the receipt and never parses a file +name or extension to grant capability. App filters format-tagged operator search entries and checks +the GLB export route against this receipt set immediately before execution. The pinned runtime marks +GLB export `READY`; USD/Alembic receipts are `OPERATOR_UNREGISTERED` and remain blocked. + +## Evidence + +- `node --test web/tests/unit/io-format-runtime-receipt.test.mjs` passed 3/3, covering receipt-bound + GLB/USD routes, extension mutation without capability change, identity drift, and explicit status. +- `node tools/web/check-io-format-runtime-receipts.mjs` passed; a fresh generator process reproduced + 14 receipts byte-for-byte from M12-05A inventory and verified operator/runtime identity fields. +- `npm --prefix web run typecheck` and `npm --prefix web run build` passed; M12-05C Chromium UI + regression passed 1/1 on port 5417 and N-023 asset/security regression passed 2/2 on port 5418. +- The existing frozen package hash `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` + is preserved; dedicated commands remain direct to avoid invalidating prior release evidence. + +## Artifact Hashes + +- parent manifest: `f46fd394e2144255d8b4304e8ce4fe60aad4302a80e991fd83d50cd915235ee3` +- protocol: `461a84557fa368c29dbc6707959b689faae7c8f7050dccc4d3f12e358b61bb22` +- generator: `796cd641e86d8242c13317f771ae237cbf1692ff675a53bbdb689615edb5f372` +- checker: `aaf9862041f155f96f50ea8481ff765089255bc59ecd8944f12362b81b8c1f1a` +- unit: `a5f09277f5dcdacd25c44191f7407d6cee944f8022b1c467c2a69e172fc2ac6b` +- runtime receipts: `f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b` +- App receipts: `f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b` +- App: `74216aac70992f8d879e983237ca324b998008582f0cd4658e90994cf9fae0a8` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `3d0049a7b0331f01bb71df043270c18d1a5c9ce6dc74353c49c8ce591761df1b` + +## Next Task + +`M12-05E`: bind each runtime receipt to source, settings, and runtime hashes. + +## Rollback + +Remove the runtime receipt protocol, generator/checker, receipt set, App gate, unit test, golden +manifest, and this status entry. Restore M12-05C as the queue tail and move the machine queue back to +`M12-05D`. No parity ledger rollback is required. diff --git a/docs/status/M12-05E.md b/docs/status/M12-05E.md new file mode 100644 index 00000000..cada1000 --- /dev/null +++ b/docs/status/M12-05E.md @@ -0,0 +1,49 @@ +# M12-05E Status + +status: done +task: bind every runtime receipt to source, settings, and runtime hashes +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The schema-1 bound receipt set derives from M12-05A inventory and M12-05D runtime receipts. Every +one of the 14 format/operation records carries three independent SHA-256 values: a source/operator +identity hash, a settings/schema hash (including canonical variants/extensions and Blender RNA +properties), and a runtime identity hash. The set also binds the parent M12-05D receipt-set hash and +the M12-05A inventory hash. + +The validator requires all three hashes and parent identities before a bound receipt can be resolved. +The generator uses sorted-key canonical JSON and the checker independently recomputes every hash, +then rebuilds the artifact in a fresh process for byte-for-byte determinism. No package metadata was +changed, preserving the existing release evidence hash. + +## Evidence + +- `node --test web/tests/unit/io-format-receipt-binding.test.mjs` passed 2/2: valid three-hash + resolution plus malformed source/settings/runtime and parent identity rejection. +- `node tools/web/check-io-format-receipt-bindings.mjs` passed; all 14 source/settings/runtime hashes + and parent/inventory identities matched a fresh deterministic regeneration. +- `npm --prefix web run typecheck` passed; `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `3d0049a7b0331f01bb71df043270c18d1a5c9ce6dc74353c49c8ce591761df1b` +- protocol: `681e719ab2b18eb85d056547fb70b461dd73b3a7fb4fdbe6295b8e49d5649e49` +- generator: `eb7b5c499f141c1788bc37e53585662eedff3f2dabff870f4ad166f4a619796f` +- checker: `ec2b22b468809ecc25ab2d4983065680a66ad3be6705d1827b44bf45ac622e26` +- unit: `e9ae3e360ad41c32da3634a4efa915654853e79a35df36728c1ddf586a0bf7b5` +- bound receipts: `7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `PENDING` + +## Next Task + +`M12-05F`: reject forged, stale, or cross-version runtime receipts before use. + +## Rollback + +Remove the binding protocol, generator/checker, bound receipt golden, unit test, manifest, and this +status entry. Restore M12-05D as the queue tail and move the machine queue back to `M12-05E`. No +parity ledger rollback is required. diff --git a/docs/status/M12-05F.md b/docs/status/M12-05F.md new file mode 100644 index 00000000..8225a46e --- /dev/null +++ b/docs/status/M12-05F.md @@ -0,0 +1,54 @@ +# M12-05F Status + +status: done +task: reject forged, stale, or cross-version runtime receipts before use +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The schema-1 freshness envelope is derived from the M12-05E bound receipt set. It carries the +M12-05E byte identity, a canonical digest of the complete bound receipt set, and a canonical digest +of the pinned Blender runtime identity. Before a route can be used, the parent binding hash, +inventory hash, runtime identity, runtime hash, and receipt-set hash must all match the trusted +expectation. The route resolver re-validates this gate on every use and only then returns `READY`. + +Forged receipt content returns `RECEIPT_FORGED`; an older parent or inventory returns +`RECEIPT_STALE`; a different Blender version/build identity returns `RECEIPT_CROSS_VERSION`. +Malformed or unavailable routes remain fail-closed as `IO_FORMAT_UNSUPPORTED`. The App GLB export and +format-tagged operator search now consume the freshness envelope rather than the unbound M12-05D set. + +## Evidence + +- `node --test web/tests/unit/io-format-receipt-freshness.test.mjs` passed 4/4: exact trusted route, + canonical digest verification, forged content, stale parent, and cross-version negatives. +- `node tools/web/check-io-format-receipt-freshness.mjs` passed; 14 receipts, App artifact parity, + independent canonical digest verification, deterministic regeneration, and all three negative + classes passed. +- Existing M12-05D/M12-05E unit and checker commands passed unchanged. +- `npm --prefix web run typecheck` and `npm --prefix web run build` passed; `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `2fbaaf39c6acd9f55fbdbadccc0b027e9e7763bf069c954a96ca49b193648990` +- protocol: `111a630c7beccd31989dc4f78932b7d7b741fb6bef03e45cb39a8139f774d235` +- generator: `6a1e798ce46e1d14d833091d4d7ae452dccb13efe583594277785465eb725bbf` +- checker: `7a8fe69ea1aa2c1e121be008a9fb60fc236f7ef776d2088a7b00b14f46fe3fba` +- unit: `cd1c2adca81653f98f9a1c6c7d104ad0e3052283213fb7166dac827c956928fe` +- freshness receipts: `0187ad0d9ea05fc4b152b7abd4945191dbe9ec24dfde4ca7dbe4aadfd1230efe` +- App freshness receipts: `0187ad0d9ea05fc4b152b7abd4945191dbe9ec24dfde4ca7dbe4aadfd1230efe` +- App expected identity: `8d65f78aa774ff252871cb1cc09e69b4ff04fbb45e61200eaf67534c9d2651b2` +- App: `043ff3a2f39ef3a1303791081b80851b427e7db5dfd9af2161926dd6f1ea9ca4` +- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `63e1506801ddee2f1eaf64cad68a9d5c918405f642ca237c6a1ec249ff297623` + +## Next Task + +`M12-06A`: desktop generate the bounded Mesh/PBR/UV/skin/animation GLB fixture group. + +## Rollback + +Remove the freshness protocol, generator/checker, freshness artifacts, unit test, App imports/route +gate, manifest, and this status entry. Restore M12-05E as the queue tail and move the machine queue +back to `M12-05F`. No parity ledger rollback is required. diff --git a/docs/status/M12-06A.md b/docs/status/M12-06A.md new file mode 100644 index 00000000..9260e871 --- /dev/null +++ b/docs/status/M12-06A.md @@ -0,0 +1,59 @@ +# M12-06A Status + +status: done +task: generate the bounded Mesh/PBR/UV/skin/animation GLB fixture group on desktop +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The pinned Blender 5.2.0 LTS desktop runtime generates five independent GLB 2.0 fixtures: mesh, +PBR, UV texture, two-joint skin, and object animation. Each file has one bounded triangle primitive, +uses no required or optional glTF extension, and remains below the 512 KiB per-file budget. The +fixtures isolate feature ownership so later Web import comparisons cannot hide one unsupported +domain behind an all-in-one scene. + +The canonical report records runtime identity, file bytes and SHA-256, nodes, topology accessors, +attributes, materials, textures/images, skins, and animations. The checker binds the M12-05F parent +manifest and the pinned M12-05A Blender binary identity, regenerates all five files in a fresh +temporary directory, and requires both the semantic report and every GLB byte to match exactly. +This enabling task does not claim Web import, `.blend` save/reopen, export loss reporting, or GLB +round-trip parity. + +## Evidence + +- `node tools/web/check-glb-desktop-fixtures.mjs` passed: 5 fixtures, 8,676 aggregate bytes, + Mesh/PBR/UV/skin/animation assertions, exact report regeneration, and exact GLB regeneration. +- Mesh exports indexed triangles with POSITION/NORMAL/COLOR_0; UV exports TEXCOORD_0 plus one + embedded PNG; skin exports JOINTS_0/WEIGHTS_0 and two inverse-bind matrices; animation exports + translation and rotation channels sampled over 25 frames. +- The runtime binary SHA-256 is + `d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82`, matching M12-05A. +- `npm --prefix web run typecheck`, `npm --prefix web run test:status-consistency`, and + `git diff --check` passed. The package hash remains the frozen + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`. + +## Artifact Hashes + +- parent manifest: `63e1506801ddee2f1eaf64cad68a9d5c918405f642ca237c6a1ec249ff297623` +- generator: `0247dd2405a68b42d99c2b005546ad2aa99b46416e3fe9489832467f6ea4eb4d` +- checker: `211ebdb66bf2e2d349455d5303e889a4a1ae3323639a89dd78b3bda574dd820b` +- desktop report: `dea31cc861622166dc502b333bc177b70b66b54d9c62aaccc6522745dab5ae13` +- mesh GLB: `e52b9268b6524744fb498691f976000635e544ba3b47e9f8ff22b03bcdf17a94` +- PBR GLB: `244cc8a992c5a70692b8bbc8333533718b3bac7022110715ce3b23d2e4c0b361` +- UV GLB: `1e0397d2b69d8261b3252451b50ab4aba6525b94713719f35069a9a9d96fcfa2` +- skin GLB: `4086ee4c8873aa03090338b676575b7a5335fb7c9384fec6ccb36108e71929f6` +- animation GLB: `44f6cc47961a146dc97ea337ae31a8b64bb4ab213b716f81ec22129db704f1fb` +- manifest: `e3554a1e739cbe3f217f6169130532365538b0c0eafbb97afc58d4d56c4287cb` + +## Next Task + +`M12-06B`: import the desktop GLB fixture group on Web and compare topology, attributes, materials, +nodes, and animations. + +## Rollback + +Remove the desktop fixture generator/checker, five GLB files, report, manifest, and this status +entry. Restore M12-05F as the queue tail and move the machine queue back to `M12-06A`. No parity +ledger rollback is required. diff --git a/docs/status/M12-06B.md b/docs/status/M12-06B.md new file mode 100644 index 00000000..7af0ea8d --- /dev/null +++ b/docs/status/M12-06B.md @@ -0,0 +1,57 @@ +# M12-06B Status + +status: done +task: compare desktop GLB topology, attributes, materials, nodes, and animations in Web +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The production TypeScript GLB parser now exposes a canonical bounded semantic surface for the five +M12-06A desktop fixtures. It validates referenced accessors, nodes, materials, textures, skins, and +animation samplers before returning primitive topology, sorted attributes, PBR material fields, +node hierarchy/TRS, skin bindings, and animation channels. A field-level comparator checks that Web +semantics exactly match the pinned Blender 5.2 desktop report. + +Both Node and a real Chromium Worker consume the exact committed GLB bytes and verify each source +SHA-256 before comparing all five domains. The existing summary import API remains compatible. This +task does not create Blender Main data or expose GLB in the file picker; the runtime route remains +`BLOCKED_UNTIL_MAIN_PERSISTENCE` until M12-06C proves save/reopen and stable IDs. + +## Evidence + +- `node --test web/tests/unit/glb-desktop-import.test.mjs` passed 3/3: five exact fixture imports, + separate domain assertions, and a stable field-level PBR mismatch. +- `node tools/web/check-glb-desktop-import.mjs` passed: 5 fixtures, 5 compared domains, exact + desktop/Web canonical hashes, deterministic report regeneration, and the blocked route state. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5425 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-desktop-import.spec.ts` passed 1/1 from + the `web/` directory using Chrome 150. The Worker verified source SHA-256 and all five semantic + comparisons. +- `npm --prefix web run typecheck`, `npm --prefix web run test:status-consistency`, and + `git diff --check` passed. The package hash remains + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`. + +## Artifact Hashes + +- parent manifest: `e3554a1e739cbe3f217f6169130532365538b0c0eafbb97afc58d4d56c4287cb` +- protocol: `45d9a7912c4a59a552789a8ea0dfaff5f1849f8d213f14d238de024b77acdb0d` +- generator: `6f97527b7da04c7b4f9b09c48b9f367d1270db9f7820498d33832a80754436c2` +- checker: `804a4b5545d95d7ab1ba265469a981d0a10b71dab36f0c96283eb73b401443d2` +- unit: `6bf2a96b3e43e5fae93589ea5dcf98e7a69b10266378e442988198925286a8d8` +- Chromium Worker: `eb32049631113270fb62acb7ae9379e8ff9a03e38086db2fa309c6891cd707bf` +- Chromium test: `fa3e06419c918406f24b5a4260523bd94cfe729a84786e9434517fdfed624d45` +- Web import report: `79933888cc5aa2d1e3639ec349ca4c31d028fe89f751ebb8d4342f06d15ecfc2` +- manifest: `5275310394b34726a05b30ab67658e1381662dddf9163a97cb02f47f646a8fa4` + +## Next Task + +`M12-06C`: create authoritative Main data from the imported GLB, save `.blend`, reopen it, and +compare stable IDs. + +## Rollback + +Remove the canonical Web import surface, report generator/checker, Node/Chromium tests, report, +manifest, and this status entry. Restore M12-06A as the queue tail and move the machine queue back to +`M12-06B`. No parity ledger rollback is required. diff --git a/docs/status/M12-06C.md b/docs/status/M12-06C.md new file mode 100644 index 00000000..5cf3f253 --- /dev/null +++ b/docs/status/M12-06C.md @@ -0,0 +1,59 @@ +# M12-06C Status + +status: done +task: import desktop GLB results into authoritative Main, save `.blend`, reopen, and compare stable IDs +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The pinned Blender 5.2 desktop runtime imports each M12-06A GLB fixture and saves a `.blend`; the +generator reopens every saved file and requires the complete stable Object/Mesh/Material/Image/ +Armature/Action ID set to remain unchanged. Chromium then opens those authoritative Main fixtures +through the production WebEngine, applies one Main-owned visibility edit, saves the resulting +`.blend`, reopens it in a fresh isolated open transaction, and compares the stable IDs with the +desktop baseline. The saved edit is checked after reopen and the open resource counters are zero for +input, staging, and active requests. + +This task does not expose GLB in the normal `.blend` file picker, claim arbitrary glTF extensions, +or change the N-023 full-parity ledger. M12-06B remains the bounded Web GLB semantic parser and +M12-06D owns export loss reporting. + +## Evidence + +- `node tools/web/check-glb-main-persistence.mjs` passed the parent manifest, all artifact hashes, + five desktop GLB/Main fixture bindings, stable-ID shape checks, and a fresh Blender regeneration + with exact semantic report/stable-ID comparison. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5437 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-main-persistence.spec.ts` passed 1/1. + Every fixture opened in the production WebEngine, performed one authoritative visibility edit, + serialized a changed `.blend`, reopened it, preserved all stable IDs, restored visibility=false, + and returned zero active requests/input/staging resources. +- `npm --prefix web run typecheck` and `git diff --check` passed. The existing package manifest was + left unchanged so prior M12 evidence remains bound to its frozen package hash. + +## Artifact Hashes + +- parent manifest: `5275310394b34726a05b30ab67658e1381662dddf9163a97cb02f47f646a8fa4` +- generator: `8989d6ce4196f140a7bfb44b863dc530a6aa462ca4a57fa7b77c468e06ab61ad` +- checker: `5bbbbeaf2d20ff9bbdeb74c8dd10fa6ffc421d1d59f86540e95f29c2cfd10903` +- desktop report: `76b000454a9073ef762d25fa546d5c65a004659a93eb6ec82fad1e679b2e81be` +- Chromium test: `7c97877f1cbbfd0166e106c80faee53f474f78816cca68ea924df5aa3c47776d` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- mesh `.blend`: `66e29adc016f07e220019b6f24eb7e5016c684dca4b3102b20c8e836968d422d` +- PBR `.blend`: `ba08aeb2876d82ddf731abe81d3a42041a1be36617d0b6324c870d5bcd4cc771` +- UV `.blend`: `1270cb452d34d2fd7a19181a2b20f70b7a028bdd2db7cf1bba4e1003f7de0f48` +- skin `.blend`: `23f175e44ec588c75db99d3f9134863fc3d7d1f192bbd815d5d1c4e3218bce0c` +- animation `.blend`: `fa6baf3a67ff11fe3d2922210089a7c508e4ee28bfaabe4cf628ba6addee1ff5` +- manifest: `e3a57636a47591e3b02dff5a07e8a0aec5e0dc43bf6b4829bffc811035dbbb31` + +## Next Task + +`M12-06D`: Web export generates a machine loss report. + +## Rollback + +Remove the M12-06C generator/checker, manifest, desktop Main fixtures, report, Chromium test, and +this status entry. Restore M12-06B as the queue tail and move the machine queue back to M12-06C. No +parity ledger rollback is required. diff --git a/docs/status/M12-06D.md b/docs/status/M12-06D.md new file mode 100644 index 00000000..bf12bd81 --- /dev/null +++ b/docs/status/M12-06D.md @@ -0,0 +1,56 @@ +# M12-06D Status + +status: done +task: Web GLB export generates a machine-readable loss report +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production GLB exporter now has a schema-1 deterministic loss-report wrapper. It records the +source SceneIR identity/revision, exportability, error/warning counts, sorted loss entries and the +bounded data-block surface. Chromium opens all five M12-06C authoritative Main fixtures and sends +their real snapshot/geometry/packed assets through the production exporter Worker. Exportable +PBR/UV/skin/animation outputs are byte-hashed; the mesh fixture's unsupported Color Attribute +shader is reported as `SHADER_GRAPH_UNMAPPABLE` and no GLB bytes are emitted. + +This task reports the bounded exporter surface only. It does not claim lossless GLB round-trip, +desktop re-import, arbitrary shader mapping, or GLB file-picker import; those remain later M12-06E/G +tasks or explicit blockers. + +## Evidence + +- `node --test web/tests/unit/glb-loss-report.test.mjs` passed deterministic sorting/count semantics. +- `node tools/web/check-glb-loss-report.mjs` passed artifact hashes, parent Main fixture binding, + deterministic loss ordering, mesh fail-closed output, and four exportable fixture output hashes. + It reports `fixtures=5 blocked=mesh warnings=3`. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5445 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-export-loss-report.spec.ts` passed 1/1. + The test uses `WebEngineClient` and a production module Worker, requests packed image assets, + produces the report, and compares it byte-for-byte with the checked-in golden report. +- `npm --prefix web run typecheck` and `git diff --check` passed. `web/package.json` remains + unchanged so previous M12 package bindings stay valid. + +## Artifact Hashes + +- parent manifest: `e3a57636a47591e3b02dff5a07e8a0aec5e0dc43bf6b4829bffc811035dbbb31` +- export protocol: `a5d342827860a9e55b49a3bb3a196a01b1e53546325d6e594778afb9360c3125` +- loss report protocol: `dce9c790b2f52d46efe0908ecb044d63d21b3c6c3f7d77ddb5e697b29a7a2d6e` +- Worker: `6f6294d26fe7b717416a5dd25fe834fb4b9a2ecbe8b011395c9559a26701ec77` +- Chromium test: `ce334b6bb5d82c133d317e916c03711029fba1c19c634dec06c06da9eddbd776` +- checker: `a23346860fa26405b2cd24591b0ab36fea29cb561ec8c2991ecfeabcffc17ee1` +- unit: `dfcc2d968e4e0c0cef4496bc304cb481f10d1f0dc4c76a40ea1d6e2f11b6b708` +- web loss report: `c6db52234d867985ef5fbcdc7c62298ec9aaa013028b5a54e2b9a16aa4133397` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `c01c5861d493e1f177e8cde3038bc5283a7671fa39bf84484662d307623325a2` + +## Next Task + +`M12-06E`: desktop Blender re-imports Web GLB and compares the canonical report. + +## Rollback + +Remove the loss-report protocol/Worker, checker, manifest, golden report, Chromium test, and this +status entry. Restore M12-06C as the queue tail and move the machine queue back to M12-06D. No parity +ledger rollback is required. diff --git a/docs/status/M12-06E.md b/docs/status/M12-06E.md new file mode 100644 index 00000000..81881280 --- /dev/null +++ b/docs/status/M12-06E.md @@ -0,0 +1,55 @@ +# M12-06E Status + +status: done +task: desktop Blender re-imports Web GLB and compares canonical report +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The four Web GLBs that produced bytes in M12-06D (PBR, UV, skin and animation) are re-imported by +the pinned Blender 5.2 desktop runtime, saved as `.blend`, reopened, and emitted as canonical graph +reports. The checker binds each Web output hash to the M12-06D report and compares Object/Mesh/ +Material/Image/Armature/Action graph fields against the M12-06C desktop-import baseline. + +PBR and animation are exact. UV records four explicit differences (corner-expanded vertex topology +and Blender's generated texture Mix node); skin records 31 explicit differences (the exporter emits +an additional armature helper mesh and Blender's re-import tessellation changes). These are recorded +machine mismatches, not silently treated as parity. The N-023 parity ledger remains blocked and this +task does not claim lossless GLB round-trip. + +## Evidence + +- `node tools/web/check-glb-web-reimport.mjs` passed artifact hashes, four Web GLB source bindings, + pinned Blender regeneration determinism, save/reopen stability, and canonical comparison. Output: + `glb-web-reimport-ok fixtures=4 exact=2 mismatched=uv,skin deterministic=true next=M12-06F`. +- The report binds Web GLB hashes `pbr=1ab793...`, `uv=8bd045...`, `skin=4a45d0...` and + `animation=b83de1...`; exact/mismatch counts are fixed at PBR 0, UV 4, skin 31, animation 0. +- The Web export Chromium test that produced the inputs passed 1/1; the desktop generator performs + a fresh Blender import/save/reopen for every input. `npm --prefix web run typecheck` and + `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `c01c5861d493e1f177e8cde3038bc5283a7671fa39bf84484662d307623325a2` +- generator: `3ae1ba45e15cae5d0308fc3fcb3766764cd374c096e27eaaddba9228a3d75004` +- checker: `c21b45a975ec70586da1b9e50b8cf4ccf74644300b4f381c3fee0ad735cfcdb9` +- desktop report: `e4d03d25cfac3c4beff4d0af0769b1c39b058670c679c12f4a11ae30d2d91f3c` +- Web export test: `ce334b6bb5d82c133d317e916c03711029fba1c19c634dec06c06da9eddbd776` +- PBR Web GLB: `1ab7936fae6e0c28e1b90e8a6ae24b3893c075c9fc58ac8896f780fdefb8277e` +- UV Web GLB: `8bd045388527ddad7cf886c0c7a2a45b6e7d6db603568a10a959bc6d423ae145` +- skin Web GLB: `4a45d00dfa23638682bb61a4f74b283bcd986126da4890d068902e3c85efc332` +- animation Web GLB: `b83de103df3f5a49487868f3ede3046875c90b0d084bbd998511c3a7c987a4fa` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `3d3b13fd54a314fd6f6907fc2d314e66ef74e8ca770a8accd823370d272737fc` + +## Next Task + +`M12-06F`: sparse accessor, Draco/extension, external URI and over-budget negative cases. + +## Rollback + +Remove the desktop Web re-import generator/checker, manifest, report, four Web GLB fixtures, and +this status entry. Restore M12-06D as the queue tail and move the machine queue back to M12-06E. No +parity ledger rollback is required. diff --git a/docs/status/M12-06F.md b/docs/status/M12-06F.md new file mode 100644 index 00000000..3867ef60 --- /dev/null +++ b/docs/status/M12-06F.md @@ -0,0 +1,49 @@ +# M12-06F Status + +status: done +task: GLB sparse accessor, extension, external URI and over-budget negative cases +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The bounded GLB importer now enforces a 512 KiB input budget, JSON/table budgets, rejects sparse +accessors, rejects all extensions outside the pinned allowlist (currently empty), and blocks external +buffer/image URIs. Errors are stable and fail before semantic/Main publication: +`GLB_SPARSE_ACCESSOR_UNSUPPORTED`, `GLB_EXTENSION_UNSUPPORTED`, `GLB_EXTERNAL_URI_BLOCKED`, and +`GLB_IMPORT_BUDGET_EXCEEDED`. + +## Evidence + +- `node --test web/tests/unit/glb-negative-cases.test.mjs` passed 2/2 for sparse, extension, external + URI, byte-budget and table-count cases. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5450 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-negative-cases.spec.ts` passed 1/1 in a + production Chromium Worker with all four stable codes. +- `node tools/web/check-glb-negative-cases.mjs` passed the schema-1 negative report and artifact + hashes: `glb-negative-cases-ok cases=4 budget=524288 deterministic=true next=M12-06G`. +- `npm --prefix web run typecheck` and `git diff --check` passed. The package manifest remains + unchanged. + +## Artifact Hashes + +- parent manifest: `3d3b13fd54a314fd6f6907fc2d314e66ef74e8ca770a8accd823370d272737fc` +- protocol: `0b6329c08e6f3cd9af6f27ef7c463b037a7cab94192afb4538d3d6c4cfcbc147` +- unit: `9af1c155143a0c685a62f569f705afd9fcc3bb49e0d724ebf3ad2c6356d63d6d` +- Worker: `d84a6b884ce1bfedd598b2602d803493bf10f6ad62fdfac49d64fca3f30f3931` +- Chromium test: `c6694689ce04ff2faea2c20be648f438a9f7eb25cdfd2f714b94c1f556ea1510` +- checker: `08377c306fd7d1082f7fd734e37809960c9798d29d626681fe9ad3f94cb7a29e` +- negative report: `7367a624b562a9613b4b908c894ab04c731ae0a348a3b58689075f4a9decd90c` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `27b42da0683dab11a884553440088c30cf58d51364268fdaac86668786aaedd4` + +## Next Task + +`M12-06G`: import/export cancellation, Worker restart and OPFS quota recovery. + +## Rollback + +Remove the GLB negative-case budget/protocol changes, Worker, tests, checker, report, manifest, and +this status entry. Restore M12-06E as the queue tail and move the machine queue back to M12-06F. No +parity ledger rollback is required. diff --git a/docs/status/M12-06G.md b/docs/status/M12-06G.md new file mode 100644 index 00000000..ad1bbdfc --- /dev/null +++ b/docs/status/M12-06G.md @@ -0,0 +1,57 @@ +# M12-06G Status + +status: done +task: GLB import/export cancellation, Worker restart, and OPFS quota recovery +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Schema-1 GLB recovery receipts now bind operation, worker generation, source/output SHA-256, +revision, temporary bytes, live requests, commit state, and stable cancellation/restart/quota codes. +Both import and export cancellation release all temporary bytes and publish no result. A new Worker +repeats the same bounded GLB import with an identical semantic result hash. + +The GLB asset persistence path uses the existing content-addressed OPFS store. A real Chromium +origin quota of 64 KiB rejects a 128 KiB candidate, while the previously committed GLB remains +readable after Storage Worker restart. Raising the quota allows a small follow-up asset to commit. + +## Evidence + +- `node --test web/tests/unit/glb-recovery.test.mjs` passed 2/2 for cancel, quota, commit, and + generation transition invariants. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5455 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-recovery.spec.ts` passed 2/2 in Chrome + 150. The first real-quota run reached the expected browser failure but the assertion only accepted + the injected error wording; the final run accepts both native and injected quota messages. +- `node tools/web/check-glb-recovery.mjs` passed with + `glb-recovery-ok cancelled=2 workerGeneration=2 quota=GLB_OPFS_QUOTA smallRecovery=true next=M12-07A`. +- `npm --prefix web run typecheck`, `node --check tools/web/check-glb-recovery.mjs`, and + `git diff --check` passed. The package manifest remains unchanged. + +## Artifact Hashes + +- parent manifest: `27b42da0683dab11a884553440088c30cf58d51364268fdaac86668786aaedd4` +- recovery protocol: `da4e2a546d7598e80798cfebd105e52b7522c896acb545710c78bb8a5c3705aa` +- storage protocol/client/Worker: `2c8ce9e32fcae973e9262a024fd849221680104bcd427ec308f99ef25112d951` / + `00cd35e4632479e4cd153314113e201c822fd209ae6c8e9fbeff5f1399728565` / + `80bf4d2ac59cbf7998c2a72c8961ae38abe43685b60df564d0746e1a45e9e1bc` +- operation Worker/browser adapter: `947c4a768422725ff2f689b2eefa8068bee51bb7ff342af17e8d4a4d0a4a7ca2` / + `2c753a04c153471c2bf7691073b836d968a1a1300e3da038f493983b46738cdf` +- unit/Chromium: `6dd48f62a85c5aaf3a04b6e572a47a7986b23cbec84a099ea510af81e8dd3a3a` / + `7d95e992f44fb913f70c104f0d6b23bd76f47d48db4a21899e35ae1d188e2093` +- checker/report: `b185248fdcd6b3cee84252bd68fcb6921bb6385bfa4a00486e93c58e94755cce` / + `1a008a76590573468446ca6e5cbdfe0ea5a8b27493291590d937b90db90d6e42` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `1c732b8d9f1a0bdb502f44e2e843e91efea12e93483a35658f8a9c70a69a2430` + +## Next Task + +`M12-07A`: OBJ single-Mesh positive fixture covering position, normal, UV, and material group. + +## Rollback + +Remove the GLB recovery receipt, quota fault option, Worker/browser adapter, tests, checker, report, +manifest, and this status entry. Restore M12-06F as the queue tail and move the machine queue back to +M12-06G. No parity ledger rollback is required. diff --git a/docs/status/M12-07A.md b/docs/status/M12-07A.md new file mode 100644 index 00000000..7235e167 --- /dev/null +++ b/docs/status/M12-07A.md @@ -0,0 +1,49 @@ +# M12-07A Status + +status: done +task: OBJ single-Mesh desktop positive fixture +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Pinned Blender 5.2 `WM_OT_obj_export` now generates one deterministic Wavefront OBJ object with +four positions, four UV coordinates, one explicit normal, two triangle faces, two material records, +and two material groups. The `.mtl` sidecar is kept beside the OBJ and both files are bound to the +canonical semantic report. Web parsing/import, multi-object and negative cases remain queued. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-obj-single-mesh-fixture.py -- tests/files/web/m12_obj_desktop_v1 + tests/golden/M12-07A/desktop-fixture.json` completed with 4 positions, 4 UVs, 1 normal, + 2 faces, and 2 materials/groups. +- `node tools/web/check-obj-single-mesh-fixture.mjs` passed deterministic regeneration in a fresh + temporary directory and byte-for-byte OBJ/MTL comparison: + `obj-single-mesh-fixture-ok vertices=4 normals=1 uv=4 faces=2 materials=2 deterministic=true next=M12-07B`. +- Runtime identity matches the pinned M12-05A Blender 5.2 inventory; the report is anchored to + `blender-5.2.0/source/blender/io/wavefront_obj` and `wm.obj_export`. +- Package hash remains frozen; no Web route or parity ledger status was changed. + +## Artifact Hashes + +- parent manifest: `1c732b8d9f1a0bdb502f44e2e843e91efea12e93483a35658f8a9c70a69a2430` +- generator: `604c3006f194c7c64a487b8f760693b66830f3cfa602928b46769955e7d4f358` +- checker: `3d0208f61a86d4d29796d8284756f53931c90864b15b7dfe4fcc84d7f65a8040` +- desktop report: `6c560a8eecf84973c2b05f9fd50d33bd45515e97c4f7970f1502de406c39f6a5` +- OBJ/MTL: `a56694d1ee28735c68381ff18c3a52581612feebac0101a53e502956c27d144f` / + `392f1b10ff5a0b142d520a9443b4c96191985f15d4244c79b36fdeda0f153715` +- runtime inventory: `0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `d80b74502202effa7be15640c945dc7e04af703b58e7a15c3fe7babc43c17b46` + +## Next Task + +`M12-07B`: OBJ multi-object, negative-index, MTL/texture-origin, and malformed-face cases. + +## Rollback + +Remove the OBJ generator, checker, desktop report, OBJ/MTL fixture, manifest, and this status entry. +Restore M12-06G as the queue tail and move the machine queue back to M12-07A. No parity ledger rollback +is required. diff --git a/docs/status/M12-07B.md b/docs/status/M12-07B.md new file mode 100644 index 00000000..1d684e81 --- /dev/null +++ b/docs/status/M12-07B.md @@ -0,0 +1,53 @@ +# M12-07B Status + +status: done +task: OBJ multi-object, negative-index, texture-origin, and malformed-face fixtures +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Pinned Blender 5.2 `wm.obj_export` now produces a two-object OBJ/MTL positive fixture with two +relative `map_Kd` references and a 2x2 PNG texture. The deterministic generator derives a negative +index OBJ variant and a malformed two-vertex face variant from the same positive bytes. The report +keeps multi-object/group/material counts and expected fail-closed `OBJ_FACE_ARITY_INVALID` separate; +it does not claim that a Web parser or OBJ import route exists. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-obj-multi-negative-fixtures.py -- tests/files/web/m12_obj_multi_v1 + tests/golden/M12-07B/desktop-fixtures.json` completed with 2 objects, 6 positions, 6 UVs, + 2 normals, 2 faces, 2 materials, relative `m12_obj_texture.png`, and both derived variants. +- `node tools/web/check-obj-multi-negative-fixtures.mjs` passed fresh-directory Blender regeneration, + exact OBJ/MTL/PNG/negative/malformed bytes, negative-index acceptance shape, malformed-face arity, + and PNG signature checks: + `obj-multi-negative-fixtures-ok objects=2 negative=true malformed=OBJ_FACE_ARITY_INVALID textureOrigin=relative deterministic=true next=M12-07C`. +- Runtime identity matches M12-05A pinned Blender 5.2; no Web parser, UI route, or parity ledger + status changed. Package hash remains frozen. + +## Artifact Hashes + +- parent manifest: `d80b74502202effa7be15640c945dc7e04af703b58e7a15c3fe7babc43c17b46` +- generator: `8d4faed82cba76e46bf639e5031b30568e8b9a15240cbddb1c22ee7ad11d697b` +- checker: `61188d67efd6133e714f2d55c7c8516b9d04896de068f994523a3c398eebdd2f` +- desktop report: `b60ff785676c0f0168588605ad442a650615191ac00770b7e5a308cc4ade83ce` +- OBJ/MTL/PNG: `4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a` / + `80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f` / + `44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c` +- negative/malformed OBJ: `9457954284cac1d68e23627e3ff734c0c591b3a24086ce5aa3d0dbbfc22f01bc` / + `6dd508b5ba944c2d15e2889c9ad9a3693845145c3bf6c9bf7df992081c915864` +- runtime inventory: `0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `785f593271058098704498cb0a7c948305087f1a83bf8f29b6e6fb9fe9341926` + +## Next Task + +`M12-07C`: Web-to-desktop OBJ round-trip and loss report. + +## Rollback + +Remove the multi/negative OBJ generator, checker, fixtures, report, manifest, and this status entry. +Restore M12-07A as the queue tail and move the machine queue back to M12-07B. No parity ledger rollback +is required. diff --git a/docs/status/M12-07C.md b/docs/status/M12-07C.md new file mode 100644 index 00000000..cb1a70a5 --- /dev/null +++ b/docs/status/M12-07C.md @@ -0,0 +1,55 @@ +# M12-07C Status + +status: done +task: Web-to-desktop OBJ round-trip and loss report +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The bounded TypeScript OBJ protocol parses positive and negative indices, position/UV/normal streams, +objects/groups/faces, MTL `map_Kd`, and stable budgets; it serializes a deterministic Web OBJ/MTL pair +and emits a machine loss report for unbound texture origins. A Chromium Worker consumes the M12-07B +double-object fixture, then a new pinned Blender 5.2 process imports the Web output with split groups. + +## Evidence + +- `node --test web/tests/unit/obj-import.test.mjs` passed 3/3 for negative-index resolution, + deterministic serialization, bound/unbound texture loss, malformed face arity, and index range. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5460 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/obj-web-roundtrip.spec.ts` passed 1/1 in Chrome + 150. Browser output has 2 objects, 6 positions/UVs, 2 normals, 2 faces, 2 materials; bound texture + loss is zero and missing texture loss contains two `OBJ_TEXTURE_ORIGIN_UNRESOLVED` warnings. +- The test writes the browser OBJ/MTL and texture to an isolated directory, invokes pinned Blender + `wm.obj_import`, and compares 2 objects, 2 triangles, UVMap presence, and one material per object. +- `node tools/web/check-obj-web-roundtrip.mjs` passed the exact golden and artifact hash gate: + `obj-web-roundtrip-ok objects=2 triangles=2 lossWarnings=2 desktopExact=true next=M12-07D`. +- `npm --prefix web run typecheck`, Python compile, and `git diff --check` passed. Package hash remains + frozen; OBJ Web capability is still bounded and does not change N-023 parity. + +## Artifact Hashes + +- parent manifest: `785f593271058098704498cb0a7c948305087f1a83bf8f29b6e6fb9fe9341926` +- protocol: `e7240af65e0d90f3ee690a4e3f391416fe4744ac6c46edc8ac0d72386857cab9` +- Worker: `bf1fcc60ec318cf6c2747d44f4af741b46f284cf5f5307e142f0ab6d50b14302` +- desktop importer: `9f1eb4ab679255a0f1f596696e8befc33a4e30c0cbe6ea423e2aaa0314cec22d` +- checker: `9d53014f4b89f786c516ebe8d300030c2728e4a36a86a5ef136b2769a12ac96b` +- unit/Chromium: `485a669be5de8e370e9b5a3239357b028ba61ca5c4076819cd46aed52a5c210a` / + `93785b4017ba14b007926b0f82442f8ae5968f242a2a762e5f5dc77d36110f5b` +- report: `45eaffc9c2e50c84b557d13ebbe9f4f53b63e19e00bc69b272491ef6366dff81` +- fixture OBJ/MTL/PNG: `4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a` / + `80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f` / + `44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `0c639954900b1fcc3b8285d0f4c0ecfa5807df6cde6d1f2cd3b59c4636d71674` + +## Next Task + +`M12-07D`: split STL binary and ASCII capability declarations. + +## Rollback + +Remove the OBJ protocol, Worker, desktop importer, unit/E2E tests, checker, report, manifest, and this +status entry. Restore M12-07B as the queue tail and move the machine queue back to M12-07C. No parity +ledger rollback is required. diff --git a/docs/status/M12-07D.md b/docs/status/M12-07D.md new file mode 100644 index 00000000..459b8749 --- /dev/null +++ b/docs/status/M12-07D.md @@ -0,0 +1,47 @@ +# M12-07D Status + +status: done +task: split STL binary and ASCII capability declarations +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Pinned Blender 5.2 `WM_OT_stl_export` now generates binary and ASCII STL from the same selected +two-triangle mesh using identical axis, scale, unit, evaluation, and modifier settings. The report +keeps `STL_BINARY/ascii_format=false` and `STL_ASCII/ascii_format=true` as separate variants; it does +not infer the format from the `.stl` extension or claim Web parsing/import support. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-stl-capability-fixtures.py -- tests/files/web/m12_stl_capability_v1 + tests/golden/M12-07D/capability-report.json` generated a 184-byte binary STL with two 50-byte + triangle records and a 213-byte ASCII STL with two facets/six vertex lines. +- `node tools/web/check-stl-capability-fixtures.mjs` matched runtime identity to M12-05A, validated + both independent encodings, regenerated the report/files byte-for-byte in a fresh directory, and + returned `stl-capability-fixtures-ok binaryTriangles=2 asciiFacets=2 deterministic=true next=M12-07E`. +- Package hash remains frozen. This enabling task does not change N-023 parity or expose an STL UI + route. + +## Artifact Hashes + +- parent manifest: `0c639954900b1fcc3b8285d0f4c0ecfa5807df6cde6d1f2cd3b59c4636d71674` +- generator: `8310104e66f246b32ac7cb4619e7f4da109baf2ece39ea670cbb6d33bd88c8b8` +- checker: `b2ad901eaa9701436cf7bcc8aa4e40d1b2d6eda7c6c44a0af830347a37cc9ca8` +- desktop report: `29c7d2a6e6764440c99eec25bb5760c7e0880839691757fb3e607ed4f5050013` +- binary/ASCII fixtures: `50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca` / + `a463a5add8be070fb67b34f00ef6e7b46025aed31fa429d4a033d75a11cf0113` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `bbc78e47f389562e7d648bf49c9b3bf8a08aaa36e914589a6eab0a1f6e72748d` + +## Next Task + +`M12-07E`: STL normal, unit, degenerate-triangle, and trailing-byte parity. + +## Rollback + +Remove the STL capability generator, checker, fixtures, report, manifest, and this status entry. +Restore M12-07C as the queue tail and move the machine queue back to M12-07D. No parity ledger rollback +is required. diff --git a/docs/status/M12-07E.md b/docs/status/M12-07E.md new file mode 100644 index 00000000..1128770c --- /dev/null +++ b/docs/status/M12-07E.md @@ -0,0 +1,60 @@ +# M12-07E Status + +status: done +task: STL normal, unit, degenerate-triangle, and trailing-byte parity +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The bounded STL protocol requires an explicit binary/ASCII variant and positive unit scale. It parses +facet normals and vertices, applies the scale, removes zero-area triangles the same way as Blender's +validated import, and rejects binary payload after the declared triangle table with +`STL_TRAILING_BYTES`. Desktop Blender accepts that trailing fixture as an empty mesh, so the report +records `STRICTER_WEB_BLOCK` rather than claiming exact parity for that edge case. + +## Evidence + +- The deterministic edge generator derived a one-degenerate-triangle binary and a four-trailing-byte + binary from M12-07D. Pinned Blender 5.2 was probed at unit scales 1 and 0.001 and against both edge + files; it preserves normals, scales world bounds by 1000, removes one degenerate triangle, and + accepts the trailing fixture as an empty mesh. +- `node --test web/tests/unit/stl-import.test.mjs` passed 2/2 for binary/ASCII normals, explicit unit + scaling, degenerate removal, trailing-byte rejection, and invalid scale. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5463 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/stl-edge-parity.spec.ts` passed 1/1 in Chrome + 150. Normal comparison is exact, Web/Desktop unit ratio matches within Float32 tolerance, and both + retain one triangle after degenerate removal. +- `node tools/web/check-stl-edge-parity.mjs` passed: + `stl-edge-parity-ok normals=exact unitRatio=1000 degenerate=removed trailing=STRICTER_WEB_BLOCK next=M12-07F`. +- Typecheck and `git diff --check` passed. The stricter trailing-byte policy leaves N-023 full parity + blocked and does not expose an STL UI route. + +## Artifact Hashes + +- parent manifest: `bbc78e47f389562e7d648bf49c9b3bf8a08aaa36e914589a6eab0a1f6e72748d` +- fixture generator/desktop probe: `018013347642603c21237ebb023bdc7a4e338f5a3f875fea536a996eb121e716` / + `b3d6ea197ef77b5a14f60f5e6b43d4392e943ff5d56acf73915507fc9a4161b7` +- protocol/Worker: `87eec72e2b8aa7ad0b168ca0ee8c4016c941f56f5f1ac53aa5fe71d0832f1dd8` / + `d714f1f3a218a2226dd349aea81c6c54efa6246de1c4fe51aaa9f5c352ef44fc` +- unit/Chromium/checker: `4af52833486421c017f3af2b833b0776e60a6ab57ed66fea2161cf9674f4b243` / + `121d348250e26d29ad966f7427bbef9da77de2098e2e305827b11aae52002c5c` / + `b5ed7fbb41bb46fba982d5726cb8c3eaecf360b772a01d6337c309b1c4a4f535` +- fixture/desktop/Web reports: `545463a984356d6635cbaae3865d13fe95bd2d841c394ac9ddff496c95d46f18` / + `2d3028a3b7d97f39654cff5fcef1d0c1c71e9f2d08e3e29c2e926651ed3860f1` / + `e0686cc9be3b68e564651207443e0ebf3e3b0eb3d07a32915b03f44b1908357b` +- degenerate/trailing fixtures: `c120bb0f218819eb89a3607c0b4f8fafd9afb599402e3b21deaa3b2be143e7d0` / + `0a30aab6db1588722067000e2a08c3c6206a8ed5b7531caa0b082723700a3681` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `19a9f460d1b939c9504dadd364cb87dab3b1769b0599035e2d2b51b47091e034` + +## Next Task + +`M12-07F`: STL Web-to-desktop round-trip and material-loss report. + +## Rollback + +Remove the STL edge fixtures, probes, parser, Worker, tests, reports, checker, manifest, and this status +entry. Restore M12-07D as the queue tail and move the machine queue back to M12-07E. No parity ledger +rollback is required. diff --git a/docs/status/M12-07F.md b/docs/status/M12-07F.md new file mode 100644 index 00000000..00f1db61 --- /dev/null +++ b/docs/status/M12-07F.md @@ -0,0 +1,51 @@ +# M12-07F Status + +status: done +task: STL Web-to-desktop round-trip and material-loss report +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The STL Web path now serializes the bounded parsed triangle result back to deterministic binary STL. +The loss report explicitly records STL's lack of material slots as +`STL_MATERIAL_UNSUPPORTED`; it never silently claims material preservation. A Chromium Worker output +is imported by a fresh pinned Blender 5.2 process and compared by triangle count and facet normals. + +## Evidence + +- `node --test web/tests/unit/stl-export.test.mjs` passed 1/1 for binary header/table serialization + and explicit material-loss warning. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5467 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/stl-web-roundtrip.spec.ts` passed 1/1 in Chrome + 150. Browser output is 184 bytes/2 triangles; Blender reopens 2 polygons/2 triangles with exact + normals; warning code is `STL_MATERIAL_UNSUPPORTED`. +- `node tools/web/check-stl-web-roundtrip.mjs` passed: + `stl-web-roundtrip-ok triangles=2 normals=exact materialLoss=1 desktopExact=true next=M12-07G`. +- Typecheck and `git diff --check` passed. The STL material loss remains machine-visible and STL UI + route/parity stays bounded. + +## Artifact Hashes + +- parent manifest: `19a9f460d1b939c9504dadd364cb87dab3b1769b0599035e2d2b51b47091e034` +- protocol/Worker: `3b9c409de9fb3eaea34f82c1ddd466670d2b478845d8ff6af3c4e44cb4e04a52` / + `1da8b8281cf8ebf9ccf5fd2f42da8ed5886001ab83daba8d9fcd88980257af87` +- desktop importer: `c4cbff52ff2e7cecba7aeab47e865975955da23a0e1e8ff885ece56b894558d5` +- unit/Chromium/checker: `f06af242df80ec26d06e92b749449cfebaf909476198ff825207cdd4b9484102` / + `2e979dcd030f5316fcbe28e2c19b1c99fe5d111e849d3fc1d2ea316d2159032b` / + `093de04bf80b0909eee56ea590e04e7aa4749168e2e49615c62ab39ddc0d16cf` +- report: `0495c645b4280f6e7821f0ba02010e25d4accbc552e3cd7c58e052607799040e` +- source fixture: `50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `3cbbcb541ee123da0ef8916ac2ca8805680d539bbf8db3b4a8d331aec418148c` + +## Next Task + +`M12-07G`: PLY ASCII/binary little-endian capability declarations. + +## Rollback + +Remove the STL export/loss protocol, Worker, desktop importer, tests, checker, report, manifest, and +this status entry. Restore M12-07E as the queue tail and move the machine queue back to M12-07F. No +parity ledger rollback is required. diff --git a/docs/status/M12-07G.md b/docs/status/M12-07G.md new file mode 100644 index 00000000..142edba0 --- /dev/null +++ b/docs/status/M12-07G.md @@ -0,0 +1,53 @@ +# M12-07G Status + +status: done +task: PLY ASCII and binary little-endian capability declarations +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Pinned Blender 5.2 `WM_OT_ply_export` now has two independently declared capability variants for +the same selected two-triangle mesh: `PLY_ASCII` and `PLY_BINARY_LITTLE_ENDIAN`. The report binds +the export settings, source/operator anchor, runtime identity, header semantics, and byte hashes; +the variant is never inferred from a shared `.ply` extension. This enabling task freezes desktop +capability only and does not expose a Web PLY parser or import route. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-ply-capability-fixtures.py -- tests/files/web/m12_ply_capability_v1 + tests/golden/M12-07G/capability-report.json` generated an ASCII PLY with four vertices/two faces + and a binary little-endian PLY with the same four vertices/two faces. Both headers explicitly carry + their independent encoding declarations and Blender export settings are recorded in the report. +- `node tools/web/check-ply-capability-fixtures.mjs` matched every runtime identity field to the + pinned M12-05A inventory, validated the `ascii` and `binary_little_endian` variants, checked the + vertex/face element counts and artifact hashes, regenerated both files in a fresh temporary + directory, and passed byte-for-byte determinism: + `ply-capability-fixtures-ok ascii=ascii binary=binary_little_endian vertices=4 faces=2 deterministic=true next=M12-07H`. +- `python3 -m py_compile tools/web/generate-ply-capability-fixtures.py` and + `node --check tools/web/check-ply-capability-fixtures.mjs` passed. The package hash remains + frozen and N-023 parity remains blocked; no PLY Web UI route was exposed. + +## Artifact Hashes + +- parent manifest: `3cbbcb541ee123da0ef8916ac2ca8805680d539bbf8db3b4a8d331aec418148c` +- generator: `581cd84057357e3a87997cf9c07d5d5633209648ac11e44611782eb254a38ebd` +- checker: `5280d6e57b7a722ea881e27b792c6082c5113c1577ac0e87f87cc87fdf59516c` +- desktop report: `26b1ce83334b41778cef5ce2a1f2c4d34254f63d7c7573cb3fb550f93ddeabb2` +- ASCII/binary little-endian fixtures: `63646cab9bf6ccecb23092e5e24d04df1e0a690c866127c72a35791e99262331` / + `e40fbb494b8b147c555a0fc06eb191415406bb9a6c061acf6befd8464c8c41a5` +- runtime inventory: `0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4` +- package: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c` +- manifest: `87df9c12f9c6eacf63051b70e9bb2eb43ebd1e5c4487b3512c45adb94cbb82ea` + +## Next Task + +`M12-07H`: PLY vertex/face/color/custom-property mapping and unknown-property loss report. + +## Rollback + +Remove the PLY capability generator, checker, fixtures, report, manifest, and this status entry. +Restore M12-07F as the queue tail and move the machine queue back to M12-07G. No parity ledger +rollback is required. diff --git a/docs/status/M12-07H.md b/docs/status/M12-07H.md new file mode 100644 index 00000000..9aa988de --- /dev/null +++ b/docs/status/M12-07H.md @@ -0,0 +1,62 @@ +# M12-07H Status + +status: done +task: PLY vertex/face/color/custom property mapping and unknown property loss report +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production `ply-import` protocol now accepts explicitly declared ASCII and binary +little-endian PLY. Vertex position, normal, RGBA color, numeric custom properties, face +indices, and numeric face properties are mapped into a bounded canonical document. Unsupported +vertex/face list properties and unknown elements are skipped with deterministic +`PLY_UNKNOWN_PROPERTY`/`PLY_UNKNOWN_ELEMENT` loss warnings; mapped fields remain available. +The worker serializes the canonical document to bounded ASCII PLY for desktop validation. +This task does not expose a general PLY file-picker route or claim full PLY attribute parity. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-ply-mapping-fixtures.py -- tests/files/web/m12_ply_mapping_v1 + tests/golden/M12-07H/mapping-report.json` generated deterministic ASCII and binary + little-endian fixtures containing four vertices, two faces, RGBA colors, and two numeric + custom vertex attributes. A derived ASCII fixture contains an unsupported list property. +- `node tools/web/check-ply-mapping-fixtures.mjs` matched pinned Blender 5.2 runtime identity, + artifact hashes, field counts and a fresh byte-for-byte regeneration: + `ply-mapping-fixtures-ok vertices=4 faces=2 colors=rgba custom=2 unknown=PLY_UNKNOWN_PROPERTY deterministic=true next=M12-07I`. +- `node --test web/tests/unit/ply-import.test.mjs` passed 3/3. It covers both encodings, + mapped values, unknown-property loss, serialization/reopen, and format mismatch blocking. +- `UPDATE_PLY_MAPPING_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5472 + node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/ply-web-roundtrip.spec.ts` passed 1/1. Production Chromium Worker parsed both + encodings, reported unknown loss, serialized ASCII, and pinned Blender 5.2 re-imported the + result with exact vertex/face/position counts and mapped custom/color attributes. +- `npm --prefix web run typecheck`, `node --check tools/web/check-ply-mapping-fixtures.mjs`, + and Python compile checks passed. + +## Artifact Hashes + +- protocol: `058a3263fde553637840a4e9ad4e8e9d923eb52c250f8000317c7f43a228881d` +- worker: `f6bf5e39e83286d7b257bbdce88f4d7fa027c64651da9765567788b5cf298c71` +- generator: `ffc051eccfc6973ee00cc791cdbd641fcce308b4083741e3e6ae82291d9347b7` +- checker: `2b055dbc705830848efdf4d8db8543a3ccc684de1f258732bcafefa86cf65c3a` +- desktop importer: `6edbc6e401a1a902dcfd11c4d767e8c8620d694e40eb4ca29dd8479f9c55ef37` +- unit/e2e: `b03a5f4b4b2a974fa26e653763470b92d1433c5d352ae09ab5492b841e6e5e1f` / + `2cadebc89057655d78e9b520bb6adf9debb27c6d783cd002efee08d269636fb9` +- desktop mapping report: `8a02fc9e364ed79e50ef00897affa9ab63808d3cb3cdabd00fdb8ee4c26ec18a` +- Chromium/desktop round-trip report: `1ca9d3b7870fcc8c9e3c9bbbd90ef48bd13bbc9ae0462312c9482f24f05f13ec` +- ASCII/binary/unknown fixtures: `acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5` / + `d0fc195fd1a101c42ac984a2382bccdddb7d0bf60dd618e9f637c964f1b30b9e` / + `a994c7126f235d0067ce4af35f8b0c6699cc83073e2a37e982edd45ece459f33` + +## Next Task + +`M12-07I`: PLY big-endian, malformed list, and oversized count stable blocking. + +## Rollback + +Remove the PLY mapping protocol, worker, fixture generator/checker, fixtures, reports, tests, +package script, and this status entry. Restore M12-07G as the queue tail and move the machine +queue back to M12-07H. No parity ledger rollback is required. diff --git a/docs/status/M12-07I.md b/docs/status/M12-07I.md new file mode 100644 index 00000000..d2efbc96 --- /dev/null +++ b/docs/status/M12-07I.md @@ -0,0 +1,49 @@ +# M12-07I Status + +status: done +task: PLY big-endian, malformed list, and oversized count stable blocking +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +PLY admission now fails closed for unsupported big-endian input, truncated/malformed list data, +and element counts above the bounded 65,536 record budget. The production parser returns stable +codes `PLY_FORMAT_UNSUPPORTED`, `PLY_DATA_TRUNCATED`, and `PLY_IMPORT_BUDGET_EXCEEDED: vertex`; +the Worker does not publish a partial document. No big-endian route or unbounded PLY import is +claimed. + +## Evidence + +- `node tools/web/generate-ply-negative-fixtures.mjs` generated deterministic big-endian, + malformed-list, and oversized-count fixtures. `node tools/web/check-ply-negative-fixtures.mjs` + regenerated them in a fresh temporary directory and passed: + `ply-negative-fixtures-ok cases=3 bigEndian=PLY_FORMAT_UNSUPPORTED malformed=PLY_DATA_TRUNCATED oversized=PLY_IMPORT_BUDGET_EXCEEDED deterministic=true next=M12-07J`. +- `node --test web/tests/unit/ply-negative.test.mjs` passed 2/2 for all three stable error paths. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5478 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/ply-negative.spec.ts` passed 1/1. The + production Worker returned the exact expected error for each case and never returned `ok=true`. +- `npm --prefix web run typecheck`, Node syntax checks, and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `058a3263fde553637840a4e9ad4e8e9d923eb52c250f8000317c7f43a228881d` +- generator/checker: `9c09707bdfe73ba467bbfbf61ed3846fb59fd33ab5563a3a62c8032722ff6938` / + `b1d047d4cb1fa15dff7821687e7028ad073fdc62d4c76f60a2656732c0ec5e2b` +- unit/e2e: `60439f9e6bc221df8866956bf926816a347e85828b5a93deb26ee23015cf449a` / + `6509a29d6842f3423d4dfcc40dbd52a959a5efa7ee1121143dce06e5bbc4a460` +- report: `fbe2830d1b19294ea98eea66c3e00125bc0809a4bb8a750612939cbe1f5acca9` +- fixtures: `cda92943a3690529fbb3463d328b6796ac0d07e19139450556f7411fc1f031e3` / + `a6a576b7a04d919b540520a6f43a89c089f0d706a17fd8b390711fff6b8b03df` / + `fcd99e0838025429420a47466251cf80e638d2b5816ad14d5aa696364525bb` + +## Next Task + +`M12-07J`: three-format cancellation, OOM, Worker restart, and small-file recovery. + +## Rollback + +Remove the count guard, negative fixture generator/checker, fixtures, report, tests, manifest, +and this status entry. Restore M12-07H as the queue tail and move the machine queue back to +M12-07I. No parity ledger rollback is required. diff --git a/docs/status/M12-07J.md b/docs/status/M12-07J.md new file mode 100644 index 00000000..127cda94 --- /dev/null +++ b/docs/status/M12-07J.md @@ -0,0 +1,50 @@ +# M12-07J Status + +status: done +task: three-format cancellation, OOM, Worker restart, and small-file recovery +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +OBJ, STL, and PLY now share a schema-1 recovery receipt. Each bounded production Worker run +binds format, input hash, base/candidate revision, Worker generation, output hash, temporary bytes, +live requests, published result count, and commit state. Cancellation clears temporary state and +publishes no result; over-budget/OOM input returns `IO_FORMAT_OOM`; a generation-2 rerun and a +generation-3 small-file run reproduce the generation-1 output hash. This is recovery evidence for +the bounded format workers, not a general file-picker or full Blender IO claim. + +## Evidence + +- `node --test web/tests/unit/io-format-recovery.test.mjs` passed 2/2 for cancellation/OOM cleanup, + commit identity, generation recovery, and stale-generation blocking. +- `UPDATE_IO_FORMAT_RECOVERY_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5492 + node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/io-format-recovery.spec.ts` passed 1/1. The report covers OBJ, STL, and PLY separately: + three cancellations, three OOM budget faults, three Worker-generation restarts, and three small + recovery commits; all output SHA-256 values are stable. +- A second run without report update passed against the same golden, proving deterministic operation + IDs and receipt output. `node tools/web/check-io-format-recovery.mjs` passed: + `io-format-recovery-ok formats=OBJ,STL,PLY cancelled=3 oom=3 restart=3 smallRecovery=3 deterministic=true next=M13-01A`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `7e352539e3300969c7409c34d6056eb6ad31055431ffce84b1b0a459c335480a` +- worker/testing adapter: `63b78fbf25132cad28147ef68731f2cd212a26c96b464fdec349a13ebaa1174f` / + `cfcebb6ec38936a66983957b0dede716871cfbfbea3cb53cddc16f3e24fb19b0` +- unit/e2e: `aaed1f2abe4789b084c8a6a60bcce8595d38220d7e6678a93924cd05c5716b4f` / + `5c1750fa408e1297fc43f2e5749be3e9ac08a16b86265d22f0f06053f52b3bd7` +- checker: `6ef6bc4a168f8675a4933a06c296f61076b9ea64cec25b295e961a9b610ab1a2` +- report: `35d4fe10d8a4fa84d6e05a85f20ca50975d93a86df41e73c7bbc5875edc4fc70` + +## Next Task + +`M13-01A`: begin the next machine-queued milestone task. + +## Rollback + +Remove the IO recovery protocol, worker, adapter, tests, checker, report, manifest, and this +status entry. Restore M12-07I as the queue tail and move the machine queue back to M12-07J. No +parity ledger rollback is required. diff --git a/docs/status/M13-01A.md b/docs/status/M13-01A.md new file mode 100644 index 00000000..31b49cec --- /dev/null +++ b/docs/status/M13-01A.md @@ -0,0 +1,47 @@ +# M13-01A Status + +status: done +task: inventory Text, Python Console, autorun, driver expression, handler, and add-on entry points +updated: 2026-08-18 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Pinned Blender 5.2 now has a machine-readable scripting entry inventory covering three Text +datablocks, Python Console operators, a module-autorun request, one driver expression, 39 handler +groups, and four add-on operators. The report records the entry source and default Web policy: +Text is metadata-only, while Console, autorun, driver expressions, handlers, and add-on operations +are `DENY`. This task only inventories entry points; it does not execute script text or open a +browser scripting route. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-script-entry-inventory.py -- tests/files/web/m13_script_entry_v1 + tests/golden/M13-01A/entry-inventory.json` created a fixture with internal/external Text, + `use_module` autorun metadata, and a driver expression without executing user code. +- `node tools/web/check-script-entry-inventory.mjs` matched pinned runtime identity, policy and + semantic inventory, then regenerated the report in a fresh directory. Blender's `.blend` save + contains runtime save bytes that may differ between runs, so determinism is asserted on the + inventory and fixture byte length; the checked-in fixture hash remains bound in the manifest. + Output: `script-entry-inventory-ok texts=3 console=3 autorun=1 drivers=1 handlers=39 addonOps=4 deterministic=true next=M13-01B`. +- `python3 -m py_compile tools/web/generate-script-entry-inventory.py`, Node syntax check and + `git diff --check` passed. + +## Artifact Hashes + +- generator: `b72667493cfe9957161ef3fb9814180e0cfad5f8aaa1c60c9b6f132e915f3d6f` +- checker: `68478f15de4d63ed175e6b580761fd478b1fe9bccbfcaeee82218d13063dfa51` +- report: `e024995c53f01beaf725f281f74a6e5ec6018a53435da61a66f5e58a9e50973c` +- fixture: `bd6375d02908bfcc57ff7cdeec3f9827bfc4336d1e46e165c5fbbf4d04f19645` +- manifest: `d4a305033343ef5fb1ffc073617b59d9012f64b80ecb233e743fb0789a8b4893` + +## Next Task + +`M13-01B`: read script metadata on `.blend` open without executing arbitrary content. + +## Rollback + +Remove the generator, checker, fixture, report, manifest, and this status entry. Restore M12-07J +as the queue tail and move the machine queue back to M13-01A. No parity ledger rollback is required. diff --git a/docs/status/M13-01B.md b/docs/status/M13-01B.md new file mode 100644 index 00000000..0ea2ea0e --- /dev/null +++ b/docs/status/M13-01B.md @@ -0,0 +1,41 @@ +# M13-01B Status + +status: done +task: read script metadata on `.blend` open without executing arbitrary content +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The real `script_scene.blend` open path reads all three Text sources, verifies source bytes and +SHA-256, preserves read-only metadata, and keeps every source `executionStatus=BLOCKED`. A +`use_module` autorun request remains `SCRIPT_POLICY_DENIED`; no Python Console, driver, handler, +or add-on code executes during open. This task does not add a script execution route. + +## Evidence + +- `node tools/web/check-script-open-metadata.mjs` ran the production WASM Main reader against the + real fixture and passed `script-open-metadata-ok blend=opened textMetadata=read sourceHash=verified execution=DENY autorun=DENY next=M13-01C`. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5495 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/script-open-metadata.spec.ts` passed 1/1. + Chromium opened the fixture through `WebEngineClient`, saw three read-only blocked sources and + the explicit autorun denial. +- `npm --prefix web run typecheck` and `git diff --check` passed. The checker emits the deterministic + report `tests/golden/M13-01B/open-metadata-report.json` bound to the fixture hash. + +## Artifact Hashes + +- checker: `66396d5c9a5294021ae077bb162278c74d46de8b52ac8a444a5de4f6d84cfe05` +- e2e: `df6412cda113c830996e08c3d66a035dc1ac309e392f5946a762d11121b1926c` +- report: `f92470e57c048452134664f7f6208e50b6f087dbcbc33369e6b882c51813990c` +- fixture: `2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037` + +## Next Task + +`M13-01C`: default-deny autorun, register, install, and driver execution policy codes. + +## Rollback + +Remove the checker, report, manifest, e2e test, and this status entry. Restore M13-01A as the +queue tail and move the machine queue back to M13-01B. No parity ledger rollback is required. diff --git a/docs/status/M13-01C.md b/docs/status/M13-01C.md new file mode 100644 index 00000000..bf1fdd75 --- /dev/null +++ b/docs/status/M13-01C.md @@ -0,0 +1,39 @@ +# M13-01C Status + +status: done +task: default-deny autorun, register, install, and driver execution policy codes +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The scripting manifest parser and execution gate reject autorun/register/install/driver execution +requests with stable policy codes. The current bounded contract returns `SCRIPT_POLICY_DENIED` for +autorun, `DRIVER_EXECUTION_BLOCKED` for driver expressions, `ADDON_INSTALL_BLOCKED` for add-on +install/registration, and `SCRIPT_SANDBOX_UNAVAILABLE` even for a correctly signed script when no +isolated sandbox exists. No local eval or script execution is enabled. + +## Evidence + +- `node --test web/tests/unit/script-policy-codes.test.mjs` passed 1/1 and asserts all three denied + request codes plus the approved-key sandbox gate. +- `node tools/web/check-script-policy-codes.mjs` passed: + `script-policy-codes-ok autorun=SCRIPT_POLICY_DENIED driver=DRIVER_EXECUTION_BLOCKED addon=ADDON_INSTALL_BLOCKED sandbox=SCRIPT_SANDBOX_UNAVAILABLE next=M13-01D`. +- `npm --prefix web run typecheck`, Node syntax checks and `git diff --check` passed. The report and + manifest bind the transpiled production protocol source hashes. + +## Artifact Hashes + +- checker: `22588c2198bc8c425ab8e104e8559f0053654ae778aaea3783ec7d54822bc8f4` +- unit: `b5a52b8e707963545f1cd71f1a148fa9c9b9d1e4b4c5f51c87d33f8bf3777430` +- report: `956db548ee71688a4b89c9a993259d3e57129cd4abe9efd1b9397b3e30b1bf84` + +## Next Task + +`M13-01D`: ensure UI exposes no direct-eval bypass around the scripting policy. + +## Rollback + +Remove the checker, unit test, report, manifest and this status entry. Restore M13-01B as the queue +tail and move the machine queue back to M13-01C. No parity ledger rollback is required. diff --git a/docs/status/M13-01D.md b/docs/status/M13-01D.md new file mode 100644 index 00000000..6029a8bf --- /dev/null +++ b/docs/status/M13-01D.md @@ -0,0 +1,36 @@ +# M13-01D Status + +status: done +task: UI direct-eval bypass gate +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production app, Workers, and protocol TypeScript sources contain no direct `eval(` or +`new Function(` bypass. The scan covers 176 non-vendor `.ts/.tsx` files and records the only +declared scripting policy entry points: `gateScriptExecution`, `gateServerScriptJob`, and +`parseScriptSourceInventory`. Execution remains `DENY`; this static gate does not claim a sandbox. + +## Evidence + +- `node tools/web/check-script-ui-bypass.mjs` passed: + `script-ui-bypass-ok scanned=176 violations=0 policyEntrypoints=3 report=8e234e128b90036548ab709b86b063de404250da2c5c0a6e25e28969b5cd5c73 next=M13-01E`. +- `git diff --check` passed. Vendor-generated Emscripten/Three.js sources are outside the scan; + no application or protocol source is allowed to introduce a direct eval constructor. + +## Artifact Hashes + +- checker: `f34cb64891c2b22bc3da353f6b864ba3e558d3c286cf716bcb778d9a542cb3d9` +- report: `6b050092088508ebd5d769d95a2e66f0cd4020c97f3407724a19b9707f51f6dd` +- manifest: `6b28688b3ebcce5629bcfc437d4ca903d0b1e053b32a796690ee4e021726c75b` + +## Next Task + +`M13-01E`: preserve Text data and unknown script sources through save/reopen. + +## Rollback + +Remove the checker, report, manifest, and this status entry. Restore M13-01C as the queue tail and +move the machine queue back to M13-01D. No parity ledger rollback is required. diff --git a/docs/status/M13-01E.md b/docs/status/M13-01E.md new file mode 100644 index 00000000..113ec41c --- /dev/null +++ b/docs/status/M13-01E.md @@ -0,0 +1,41 @@ +# M13-01E Status + +status: done +task: preserve Text data and unknown script sources through save/reopen +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production WebEngine path opens `script_scene.blend`, saves it, starts a new engine Worker, +and reopens the saved bytes. All three Text sources compare exact by metadata and source SHA-256; +they remain read-only and `BLOCKED`, including the `use_module` autorun request. No source is +rewritten or executed. This does not claim arbitrary script execution or add-on support. + +## Evidence + +- `UPDATE_SCRIPT_SAVE_REOPEN_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5498 + node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/script-save-reopen.spec.ts` passed 1/1. A second run without report update on port 5497 + also passed against the same deterministic report. +- `node tools/web/check-script-save-reopen.mjs` passed: + `script-save-reopen-ok sources=3 exact=true blocked=true savedBytes=490191 next=M13-01F`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Artifact Hashes + +- checker: `7fc9a370cb472636e2699565cb21a1450e3cc8a2c90ffdcdff96533b344afa7c` +- e2e: `481f78f3a0cce923b67ab14436cc23cbcd2ce66725de29dc874fea4fb5801227` +- report: `0f6869a8ec8342ed87649312d2872ab2c172cbf12d6be81bb0b770ebcbe8a398` +- manifest: `889a4e06f7e8c0ea55b3ca4baa9fe85dccbe97053e497a45e3d364ce2b70a7c6` +- fixture: `2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037` + +## Next Task + +`M13-01F`: malicious Text, driver, handler, and embedded-module fixtures. + +## Rollback + +Remove the checker, e2e test, report, manifest, and this status entry. Restore M13-01D as the +queue tail and move the machine queue back to M13-01E. No parity ledger rollback is required. diff --git a/docs/status/M13-01F.md b/docs/status/M13-01F.md new file mode 100644 index 00000000..df5dbb9e --- /dev/null +++ b/docs/status/M13-01F.md @@ -0,0 +1,44 @@ +# M13-01F Status + +status: done +task: malicious Text, driver, handler, and embedded-module fixtures +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Pinned Blender 5.2 generated a malicious fixture containing four source bodies: OS command text, +driver import, handler subprocess, and an embedded `register()` module. Chromium opened the file +through the production Main reader and kept every source read-only/`BLOCKED`; the embedded module +returned `SCRIPT_POLICY_DENIED`. No filesystem marker or execution path is exposed by the test. + +## Evidence + +- `build_blender_5.2.0/bin/blender -b --factory-startup --python + tools/web/generate-malicious-script-fixture.py -- tests/files/web/m13_malicious_script_v1 + tests/golden/M13-01F/malicious-report.json` generated four deterministic source hashes and one + `use_module` request. +- `node tools/web/check-malicious-script-fixture.mjs` passed: + `malicious-script-fixture-ok sources=4 module=1 execution=BLOCKED fixtureSha256=7b1921931afc5bb74a2b73e90b175017c583ea878cdbb66f131718b2d8cd23b5 next=M13-02A`. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5499 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/malicious-script.spec.ts` passed 1/1. +- `git diff --check` passed. + +## Artifact Hashes + +- generator: `013285befeb59de21a887cefd377adf8cf53ff1c785b28a9c87d29f76f092731` +- checker: `36608da893ae59e2e03856a62866ea6e7c349ec4a63200f042b2329e5f61caa9` +- e2e: `98fbde6728ddf55cce5262522197a5b4db1dfce618e52259bf0b0c9e0e9165f2` +- report: `a628b73411d6f9a761f659ae28867ea9b0c0df30d47668353278b70d4b44180c` +- fixture: `7b1921931afc5bb74a2b73e90b175017c583ea878cdbb66f131718b2d8cd23b5` +- manifest: `9a0b7c4097b3755365a9fb92b4848e6ac2ddd36ee2c7e9df3cb8808ce247cf3d` + +## Next Task + +`M13-02A`: bounded script manifest limits. + +## Rollback + +Remove the generator, checker, fixture, report, manifest, e2e test, and this status entry. Restore +M13-01E as the queue tail and move the machine queue back to M13-01F. No parity ledger rollback is required. diff --git a/docs/status/M13-02A.md b/docs/status/M13-02A.md new file mode 100644 index 00000000..c610913b --- /dev/null +++ b/docs/status/M13-02A.md @@ -0,0 +1,49 @@ +# M13-02A Status + +status: done +task: bounded script manifest limits +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production scripting manifest parser now requires an explicit `sourceByteLength` and +`module=false` for every script. It limits script count, aggregate source bytes, dependency count, +permission count and per-field execution budgets; canonicalizes entry/dependency paths within the +project; and rejects duplicate dependencies, unsafe paths, module execution, unknown permissions +and all budget overflow. The parser remains fail-closed and does not execute script content. + +## Evidence + +- `node --test web/tests/unit/script-manifest-budgets.test.mjs` passed 4/4 bounded positive and + negative cases. +- `WEB_TEST_PORT=5513 npm --prefix web run test:script-manifest-budgets` passed unit 4/4 and the + production Chromium Worker test 1/1. The browser Worker returned the same canonical paths, + aggregate byte count and stable policy/budget errors as the Node protocol test. +- `node tools/web/check-script-manifest-budgets.mjs` passed: + `script-manifest-budgets-ok accepted=2 totalSourceBytes=256 blocked=6 deterministic=true next=M13-02B`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint`, + `npm --prefix web run build`, `npm --prefix web run test:scripting-isolation`, and + `git diff --check` passed. The existing isolation gate still reports approved-key + `SCRIPT_SANDBOX_UNAVAILABLE` and does not enable execution. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `8d8eae67fa6915f0337850e15247baf01f0abde0b3362fbb120f0b448f1a4cf5` +- checker: `b3457324d72ab233cd17a142ea19fac6a0d024dd95de7b8fb5d26810437fb0d2` +- unit: `6ce7847a0b745ba4081e4332d012e0b7f86e4906c71c95bdeda8c39977a630ee` +- e2e: `1ef4fd4caaeb1fa3d832fc8881823d5284755372575eab186c751f408dc9314c` +- report: `860672fe844b7969ca376d4b2708771189d1767efa819f7b97667d8a26438d3a` +- manifest: `7148e0387dadffdb55e94e80dc30cfd5cdd40ebe990d06378e90c376d36ebd51` + +## Next Task + +`M13-02B`: canonical serialization fixes the signature input. + +## Rollback + +Remove the M13-02A protocol fields, checker, unit/Chromium tests, worker, report, manifest and this +status entry. Restore M13-01F as the queue tail and move the machine queue back to M13-02A. No parity +ledger rollback is required. diff --git a/docs/status/M13-02B.md b/docs/status/M13-02B.md new file mode 100644 index 00000000..52c6af59 --- /dev/null +++ b/docs/status/M13-02B.md @@ -0,0 +1,52 @@ +# M13-02B Status + +status: done +task: canonical script manifest serialization +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The scripting protocol now exposes `canonicalizeScriptingManifest` and +`serializeScriptingManifest` as the single canonical signature-input path. Script, permission and +dependency arrays are sorted with locale-independent code-unit ordering; unknown fields are removed +by the parser; canonical JSON has sorted object keys and no whitespace; and security-relevant fields +such as source byte length, source hash, permissions, paths, budgets and policy flags remain in the +serialized input. Schema changes and source declaration mutations produce a different or rejected +input. No signature is accepted and no script is executed by this task. + +## Evidence + +- `node --test web/tests/unit/script-manifest-canonical.test.mjs` passed 2/2. It proves order + invariance, unknown-field dropping, security-field binding and schema rejection. +- `WEB_TEST_PORT=5514 npm --prefix web run test:script-manifest-canonical` passed unit 2/2 and + production Chromium Worker 1/1. +- `node tools/web/check-script-manifest-canonical.mjs` passed: + `script-manifest-canonical-ok equal=true bytes=1923 unknownDropped=true schemaMutation=blocked next=M13-02C`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint`, `npm --prefix web run build`, + `node tools/web/check-script-manifest-budgets.mjs`, `node tools/web/check-status-consistency.mjs` + and `git diff --check` passed. The M13-02A artifact manifest was refreshed to the current protocol + hash after this additive canonical API change, and its checker was rerun successfully; no prior + behavior or parity status was changed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `de27fdcd6d16e27a07806ac609f908edfdb7a93d653676174cdd5e06ffa394af` +- checker: `5ed344cab6428ae5538a450171ba40c73ff738666515492298e30aaed1394f56` +- unit: `33eae0f3ad2ae7243c9ce2835ab8e42186f65f574ab682099766f1d0f4c481f6` +- e2e: `5342301165ae82a01b46f5fed0cc0ec34b9813a6ecbc8032900d90b89628e064` +- report: `5f4bc0b098ea65de47f1255d30130376d74260e2b912bcd0e28354171fbd07df` +- parent manifest: `7148e0387dadffdb55e94e80dc30cfd5cdd40ebe990d06378e90c376d36ebd51` +- manifest: `605c656780a206a0d3b81d06b0294108b488a62d2b709e886884a2973c6cb091` + +## Next Task + +`M13-02C`: signer identity, key rotation, revocation and timestamp policy. + +## Rollback + +Remove the canonical serializer exports, checker, unit/Chromium tests, worker, report, manifest and +this status entry; restore M13-02A as the queue tail and move the machine queue back to M13-02B. No +parity ledger rollback is required. diff --git a/docs/status/M13-02C.md b/docs/status/M13-02C.md new file mode 100644 index 00000000..3f452c8c --- /dev/null +++ b/docs/status/M13-02C.md @@ -0,0 +1,51 @@ +# M13-02C Status + +status: done +task: signer identity, key rotation, revocation and timestamp policy +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The scripting protocol now has a versioned trust-policy schema for signer identity and key +lifecycles. It validates ED25519 public-key identity, publisher ownership, active/revoked status, +not-before/not-after windows, revocation timestamps, same-publisher rotation predecessors and +acyclic rotation chains. `maxClockSkewMs` binds policy timestamps to an explicit bounded window. +`resolveScriptSigner` only returns `ELIGIBLE` with `cryptographicVerification=REQUIRED`; revoked, +expired, not-yet-valid, missing and publisher-confused keys remain structured `BLOCKED` results. +This task does not verify signatures or enable script execution. + +## Evidence + +- `node --test web/tests/unit/script-trust-policy.test.mjs` passed 3/3. It covers valid active + rotation, revoked predecessor, invalid/cross-publisher/cyclic rotation, revocation metadata, + public-key format and timestamp failures. +- `WEB_TEST_PORT=5516 npm --prefix web run test:script-trust-policy` passed unit 3/3 and production + Chromium Worker 1/1. +- `node tools/web/check-script-trust-policy.mjs` passed: + `script-trust-policy-ok active=key:new revoked=REVOKED crossPublisher=SCRIPT_POLICY_DENIED policyExpired=POLICY_EXPIRED crypto=REQUIRED next=M13-02D`. +- `npm --prefix web run typecheck` passed; M13-02A and M13-02B checkers were rerun after the + additive protocol change and passed. No parity ledger or execution route changed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `c6e206b29e7cacc3e23e2ac12a3d523a9c0b16ff29759126dab13c8665547421` +- checker: `0e55ce20d142f2bcbc5ce9c6fb955ef2771d284dff636c79da3f59a33b8b0aeb` +- unit: `59ac77bcc1086e0a0e914cb77d647db5d34c7160202e7aad52948b7277769551` +- e2e: `614091bda15367090bc78a6f465fb10d02d0aa08775b5088699b12e2490034d2` +- report: `0f59f733920edbbe5cb799e5f50ff31d19e55ced98e7a890828f6337a237e4bd` +- unit: `aac3ff0a1c7ae27e35112614bedabc02bb248882ec9b0b52d65f9794d06830d8` +- parent manifest: `605c656780a206a0d3b81d06b0294108b488a62d2b709e886884a2973c6cb091` +- manifest: `89745daca88371335f4bd56982791266461082066c6c1345056fbc697bb7e6a2` + +## Next Task + +`M13-02D`: signature only approves declared content and source hash changes invalidate it. + +## Rollback + +Remove the trust-policy schema, parser/resolver, checker, unit/Chromium tests, worker, report, +manifest and this status entry; restore M13-02B as the queue tail and move the machine queue back to +M13-02C. No parity ledger rollback is required. diff --git a/docs/status/M13-02D.md b/docs/status/M13-02D.md new file mode 100644 index 00000000..d4bb2368 --- /dev/null +++ b/docs/status/M13-02D.md @@ -0,0 +1,46 @@ +# M13-02D Status + +status: done +task: signature verification binds declared content +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +`verifyScriptManifestSignature` now verifies the canonical per-script manifest input with the +trusted ED25519 key. The input includes the declared source SHA-256 and all policy-relevant fields, +while excluding only the signature field itself. A source hash or signature mutation therefore +returns `SCRIPT_SIGNATURE_INVALID`; revoked or otherwise ineligible keys return +`SCRIPT_POLICY_DENIED`. Verification remains a policy gate and does not enable script execution. + +## Evidence + +- `node --test --test-name-pattern=M13-02D web/tests/unit/script-trust-policy.test.mjs` passed the + M13-02D case (1 passed, 3 unrelated cases skipped). +- `WEB_TEST_PORT=5520 npm --prefix web run test:script-signature` passed the focused Node case and + production Chromium Worker E2E (1/1). +- `node tools/web/check-script-signature.mjs` passed: + `script-signature-ok verified=SCRIPT_SIGNATURE_VERIFIED sourceHashChanged=SCRIPT_SIGNATURE_INVALID revoked=SCRIPT_POLICY_DENIED next=M13-02E`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint`, `npm --prefix web run build` and + `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `f4e8ff45d06efcfa9e634ef28e654241d70554ed05dfe9c3fecd9f6eef166ce0` +- checker: `2d6e7b403dd6d401eb1a41978016b40bd1fb86120464e09f185de4445c177fa4` +- unit: `aac3ff0a1c7ae27e35112614bedabc02bb248882ec9b0b52d65f9794d06830d8` +- e2e: `8a882fc8fa2c0c4e2eab3660810cec0f60d2f7475f62a1bb406fc3b2bdf60882` +- report: `957daf8e5899714cc5ce253b1f0fb2b258f1cc966238fff31e2626f8f56feb3a` +- manifest: `c6b79a77e4b7ffe5f4a9ba785a30305f9cf6e86caf89b38a4b303a86ab662a31` + +## Next Task + +`M13-02E`: permission default-minimization and unknown-permission blocking. + +## Rollback + +Remove the M13-02D checker, report, manifest, status entry and focused package command; restore +M13-02C as the queue tail and move the machine queue back to `M13-02D`. No parity ledger rollback +is required. diff --git a/docs/status/M13-02E.md b/docs/status/M13-02E.md new file mode 100644 index 00000000..05459380 --- /dev/null +++ b/docs/status/M13-02E.md @@ -0,0 +1,42 @@ +# M13-02E Status + +status: done +task: permission default-minimization and unknown-permission blocking +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +`resolveScriptPermissions` now grants an empty set by default and only grants an explicitly +requested permission when it is already declared by the script manifest. Unknown, duplicate or +undeclared requests return `SCRIPT_POLICY_DENIED`; manifest declarations still reject unknown +permissions during parsing. This task adds no execution capability. + +## Evidence + +- `node --test web/tests/unit/script-permission-policy.test.mjs` passed 2/2. +- `WEB_TEST_PORT=5521 npm --prefix web run test:script-permission-policy` passed unit 2/2 and + production Chromium Worker E2E 1/1. +- `node tools/web/check-script-permission-policy.mjs` passed: + `script-permission-policy-ok defaultGranted=0 declared=READ_MAIN escalation=SCRIPT_POLICY_DENIED unknown=SCRIPT_POLICY_DENIED next=M13-02F`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `5df074e676542475009a4980fecaf3fdde009d149de84b9ca146647fb6aa1c02` +- checker: `169ab096295e08c5a630ef09d814da6ce742efd7a7143ff9a17984b4ca20593a` +- unit: `43bb6796616f3f92d71b51ef2a119a4e263864121a03356764e4b9c9241c4043` +- e2e: `dc13412a44f277bbe68315da0d38a2c4aa520d7489c81e7c908f9768c0547b7e` +- report: `8dc41e75620ba5bcb08d0edc52c3994e4f4dbc37ad3633757634bf10fe97b252` +- manifest: `691376d3cd33d3cd339b7195034abaf89cc02fba7e740c40ff051c4496400eef` + +## Next Task + +`M13-02F`: replay, key-confusion, expiry and multi-signature negative cases. + +## Rollback + +Remove `resolveScriptPermissions`, the focused worker/unit/E2E/checker, report, manifest, status +entry and package command; restore M13-02D as the queue tail and move `nextTask` back to `M13-02E`. diff --git a/docs/status/M13-02F.md b/docs/status/M13-02F.md new file mode 100644 index 00000000..77ed2ffc --- /dev/null +++ b/docs/status/M13-02F.md @@ -0,0 +1,42 @@ +# M13-02F Status + +status: done +task: signature replay, key-confusion and expiry negative cases +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The signature gate now has an independent negative-case matrix covering missing keys, expired and +not-yet-valid keys, publisher/key confusion, and a signature copied to a different script. Every +case remains fail-closed with `SCRIPT_POLICY_DENIED` or `SCRIPT_SIGNATURE_INVALID`; no execution +route is added. Canonical script ordering remains accepted when the script's own signature matches. + +## Evidence + +- `node --test web/tests/unit/script-signature-negative.test.mjs` passed 2/2. +- `WEB_TEST_PORT=5522 npm --prefix web run test:script-signature-negative` passed unit 2/2 and + production Chromium Worker E2E 1/1. +- `node tools/web/check-script-signature-negative.mjs` passed: + `script-signature-negative-ok missing=SCRIPT_POLICY_DENIED expired=SCRIPT_POLICY_DENIED notYetValid=SCRIPT_POLICY_DENIED swapped=SCRIPT_SIGNATURE_INVALID next=M13-03A`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `49b289110a6533fcd1a29ad78be00792fd3c0d251c6027bae0440f5daba929a1` +- checker: `35f2e6727aef5e6ea2e2623e2effa6ea0356faaf66d417976db7f5bbe671a38c` +- unit: `8b9c8d63c97fce07299622b9ad261791a97bb91a6ed340e72c24c55e685978bd` +- e2e: `26c52ef67d6b1ea2994d92637826994e98e5234e042049f8d69a905f5cdb3309` +- report: `d23a2ae5613b2442ad79420aa344ebfe3767d10ac19b3ea30a62afcccd3a167c` +- manifest: `283673b21e6c9b89dc6ecb7007f3cecf28d53a84ec84f3f8b52373c055538a74` + +## Next Task + +`M13-03A`: sandbox capability scope with no DOM, host-worker, OPFS, IndexedDB or network access. + +## Rollback + +Remove the M13-02F negative-case worker, tests, checker, report, manifest, status entry and package +command; restore M13-02E as the queue tail and move `nextTask` back to `M13-02F`. diff --git a/docs/status/M13-03A.md b/docs/status/M13-03A.md new file mode 100644 index 00000000..6ebc324d --- /dev/null +++ b/docs/status/M13-03A.md @@ -0,0 +1,42 @@ +# M13-03A Status + +status: done +task: sandbox capability scope +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production scripting protocol now defines a versioned all-deny sandbox scope. DOM, host-worker, +OPFS, IndexedDB and network capabilities must each be explicitly `false`; any enabled, missing or +unknown scope version is rejected with a stable policy/protocol error. This is a capability contract +only: script execution remains disabled and no sandbox implementation is claimed. + +## Evidence + +- `node --test web/tests/unit/script-sandbox-scope.test.mjs` passed 2/2. +- `WEB_TEST_PORT=5523 npm --prefix web run test:script-sandbox-scope` passed unit 2/2 and + production Chromium Worker E2E 1/1. +- `node tools/web/check-script-sandbox-scope.mjs` passed: + `script-sandbox-scope-ok dom=SCRIPT_POLICY_DENIED hostWorker=SCRIPT_POLICY_DENIED opfs=SCRIPT_POLICY_DENIED indexedDB=SCRIPT_POLICY_DENIED network=SCRIPT_POLICY_DENIED execution=DISABLED next=M13-03B`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `27058fbb602791cbe6691fa14b3bc9c3d6cdea79641ba9c72fe18b6226d2a651` +- checker: `ad26ea3b078e480ba8f99cbf8dbd0d9b135c7605f4a3958d24845b7f5f8b0f43` +- unit: `a15da2e645d2d681c7e9ac1380f6b224d196d1d2e78d20f1bacad01e138753d4` +- e2e: `49930747e6663f79b61093706318b72e59388d79e3e1cc105bc1571693326667` +- report: `ba5784c751d5a347f38aa522ffcbed270d38fb474f0f10a2d06ea501d0234021` +- manifest: `76a7ce0fd3a38fb770c9cedccbd05ed566b931caa1782fdae898d3b66c3a20bf` + +## Next Task + +`M13-03B`: fixed CPU, wall-time, memory, message and output-byte budgets. + +## Rollback + +Remove the sandbox scope parser, focused worker/tests/checker, report, manifest, status entry and +package command; restore M13-02F as the queue tail and move `nextTask` back to `M13-03A`. diff --git a/docs/status/M13-03B.md b/docs/status/M13-03B.md new file mode 100644 index 00000000..3915520f --- /dev/null +++ b/docs/status/M13-03B.md @@ -0,0 +1,41 @@ +# M13-03B Status + +status: done +task: sandbox CPU, wall, memory, message and output budgets +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The scripting protocol now parses bounded CPU, wall-time, memory, message and output-byte budgets. +Each field has a fixed maximum and all overflow or schema drift returns `SCRIPT_BUDGET_EXCEEDED` or +`PROTOCOL_MISMATCH`. The budget is a contract only; execution remains disabled. + +## Evidence + +- `node --test web/tests/unit/script-sandbox-budget.test.mjs` passed 2/2. +- `WEB_TEST_PORT=5524 npm --prefix web run test:script-sandbox-budget` passed unit 2/2 and + production Chromium Worker E2E 1/1. +- `node tools/web/check-script-sandbox-budget.mjs` passed: + `script-sandbox-budget-ok cpu=SCRIPT_BUDGET_EXCEEDED wall=SCRIPT_BUDGET_EXCEEDED memory=SCRIPT_BUDGET_EXCEEDED message=SCRIPT_BUDGET_EXCEEDED output=SCRIPT_BUDGET_EXCEEDED execution=DISABLED next=M13-03C`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `ed836a032b33f54154fdd36f9f6e99ee43ca1755ea0dd8326474a13300ec965e` +- checker: `ab10d1d32c20022c848be4b33e1be846168a9ca4cf22ac932ea000001e9e2391` +- unit: `caa40fc58a73aa4d433285c94009e5436a29fbcb6b4e18b74b27c49fd0b07490` +- e2e: `8be77fd55e76149bfe2e0e449d81a62707ebfa430514936808ec4805fc96e07c` +- report: `4355710e07e95cbb0f96a82fdefca3607f363d3aea9ba89c332e8a4708b7bed2` +- manifest: `b61978c22cdfffe81b812e2eea788d52326f9f0ebb4540e77f86dc9e19ff21dd` + +## Next Task + +`M13-03C`: host calls use explicit allowlist and structured parameters. + +## Rollback + +Remove the sandbox budget parser, focused worker/tests/checker, report, manifest, status entry and +package command; restore M13-03A as the queue tail and move `nextTask` back to `M13-03B`. diff --git a/docs/status/M13-03C.md b/docs/status/M13-03C.md new file mode 100644 index 00000000..bbbc43e4 --- /dev/null +++ b/docs/status/M13-03C.md @@ -0,0 +1,43 @@ +# M13-03C Status + +status: done +task: explicit host-call allowlist and structured parameters +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production scripting protocol now accepts only five host-call names: `READ_MAIN`, `READ_ASSET`, +`WRITE_MAIN`, `WRITE_ASSET` and `SUBMIT_SERVER_JOB`. Each request has a version, request/script IDs, +permission binding and call-specific structured parameters. Unknown calls, permission confusion, +unknown fields, unsafe paths and malformed payloads fail closed. Parsed calls remain +`execution=DISABLED`; no host operation is invoked. + +## Evidence + +- `node --test web/tests/unit/script-host-call.test.mjs` passed 3/3. +- `WEB_TEST_PORT=5525 npm --prefix web run test:script-host-call` passed unit 3/3 and production + Chromium Worker E2E 1/1. +- `node tools/web/check-script-host-call.mjs` passed: + `script-host-call-ok accepted=5 blocked=4 structured=true execution=DISABLED next=M13-03D`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `b26a37f20e829172bb70fbe9b9e3a194923818127cccbe8bbc55f6d994be55e8` +- checker: `e90042f4eefd9e89da6d1cead87ca1cb9db9b3bd65ba8028593e093903577e2e` +- unit: `e7679cd902c75504204d528343e4fa6be316d1c56c02b2871a67637aa847a1ec` +- e2e: `7b17a7da440aee07895101efb8e1eb13bfbebbe54e955bde09299f6b33d6aaf9` +- report: `afb140a3aecff1de52c4363e7cd0ce476b0c9140ffdc016ec13a085049f6dd1a` +- manifest: `54318c45b11dd1707fecf78b0637257158102ea1dbb88d4d442e33b77df2cdbf` + +## Next Task + +`M13-03D`: sandbox crash/timeout terminates the job without changing Main revision. + +## Rollback + +Remove the host-call parser, focused worker/tests/checker, report, manifest, status entry and package +command; restore M13-03B as the queue tail and move `nextTask` back to `M13-03C`. diff --git a/docs/status/M13-03D.md b/docs/status/M13-03D.md new file mode 100644 index 00000000..3bacae0e --- /dev/null +++ b/docs/status/M13-03D.md @@ -0,0 +1,46 @@ +# M13-03D Status + +status: done +task: sandbox crash/timeout isolation +updated: 2026-08-18 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The sandbox job termination receipt now converts crash, wall-time timeout and cancellation into +stable fail-closed statuses. Every terminated receipt keeps `mainRevisionAfter` equal to +`mainRevisionBefore`, publishes no result, releases temporary bytes, and remains +`execution=DISABLED`; a result received after termination is rejected with +`SCRIPT_SANDBOX_LATE_RESULT`. + +The Chromium test starts a production Worker that throws for the crash case and a production Worker +that would publish a delayed result for the timeout case. The host observes the crash, terminates the +timeout Worker before its delayed message, and confirms no late message arrives. + +## Evidence + +- `node --test web/tests/unit/script-sandbox-isolation.test.mjs` passed 3/3. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5532 npm --prefix web run test:script-sandbox-isolation` passed unit 3/3 and Chromium Worker E2E 1/1. +- `node tools/web/check-script-sandbox-isolation.mjs` passed: + `script-sandbox-isolation-ok crash=SCRIPT_SANDBOX_CRASHED timeout=SCRIPT_SANDBOX_TIMEOUT revisionUnchanged=true late=SCRIPT_SANDBOX_LATE_RESULT next=M13-03E`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `8c810ee7c8bd15d83ecfb4981068f947536bfe8c9e63b906861f42b8465722da` +- checker: `e5c0c3c7cd500c269ae43a1a9eb05589e82aad28199ab4f11ad1167b486f6937` +- unit: `47986f93638fdc18b817b03222484f4691dc294185040eaebac8e1f386bc1549` +- e2e: `2c16c42f09827a0c0100ef2cc6295ca6bc96933ed7475e08b1fa195d7940c141` +- report: `4af91cdb21101039b379136c7559b46df55f4f5ab5478c05c1c57cd6e1624d2a` +- manifest: `8066013f3d356ba438c36d1f1ea1cf692dbb4f60b086f552072e36b783a96d42` + +## Next Task + +`M13-03E`: cancellation after termination must not publish a late message or cache. + +## Rollback + +Remove the isolation Worker additions, focused tests/checker, report, manifest and this status entry; +restore M13-03C as the queue tail and move `nextTask` back to `M13-03D`. diff --git a/docs/status/M13-03E.md b/docs/status/M13-03E.md new file mode 100644 index 00000000..e90d0210 --- /dev/null +++ b/docs/status/M13-03E.md @@ -0,0 +1,45 @@ +# M13-03E Status + +status: done +task: sandbox cancellation result gate +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Cancellation now has an explicit terminated-job receipt. The receipt preserves the Main revision, +publishes no result, releases temporary bytes and remains `execution=DISABLED`. The production +Chromium Worker schedules a late message containing a cache key; the host cancels and terminates it +before publication, then observes a bounded late window with zero messages and zero cache writes. +Late results supplied after cancellation are rejected with `SCRIPT_SANDBOX_LATE_RESULT`. + +This task does not claim Worker/port/timer disposal or same-session recovery; those remain M13-03F/G. + +## Evidence + +- `node --test web/tests/unit/script-sandbox-cancellation.test.mjs` passed 2/2. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5533 npm --prefix web run test:script-sandbox-cancellation` passed unit 2/2 and Chromium Worker E2E 1/1. +- `node tools/web/check-script-sandbox-cancellation.mjs` passed: + `script-sandbox-cancellation-ok status=CANCELLED late=SCRIPT_SANDBOX_LATE_RESULT lateMessages=0 cacheWrites=0 next=M13-03F`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2` +- worker: `0feb70c8c491cc24ebfe6c3e267b92522d616b6f260efcecfa57cf489d0742c0` +- checker: `b6fbe7102b7d0808673931c66797f8976e79b21bd4c32e429f21832c6090708e` +- unit: `374cc87f66bd42226b750e387663ef8c86bef92348fa7cbc6ed7f0027945204d` +- e2e: `b233d9cafa1f70798ec19d76ca936abb610681522264a12237f532eb98e09fca` +- package: `6499a70fc4a93c925053ddf5009c74d9c10754b443afc9d58e898fe20d7f1b05` +- report: `066d9f19716b8229f2cf7755ec3009a3edef942ebab5f70159bec7f2afbaf71f` +- manifest: `0cdf625e6bd3ed7aca43318a3b04353cb806c51cfa4cf3c5dae9a65a0eae2e69` + +## Next Task + +`M13-03F`: dispose Worker, MessagePort, timers, abort controllers and buffers to zero. + +## Rollback + +Remove the cancellation Worker, focused tests/checker, report, manifest, package command and this +status entry; restore M13-03D as the queue tail and move `nextTask` back to `M13-03E`. diff --git a/docs/status/M13-03F.md b/docs/status/M13-03F.md new file mode 100644 index 00000000..e7bbc2e4 --- /dev/null +++ b/docs/status/M13-03F.md @@ -0,0 +1,44 @@ +# M13-03F Status + +status: done +task: sandbox resource disposal gate +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The sandbox Worker now owns an explicit disposal receipt. Both `MessagePort` endpoints, the timer, +`AbortController`, transferable buffer, pending request and cache reference are observed before +disposal and are all zero after the first dispose. A second dispose is accepted and reports +`idempotent=true`; the cleared timer produces no late message. Script execution remains +`DISABLED`, and this task does not claim same-session recovery or server isolation. + +## Evidence + +- `node --test web/tests/unit/script-sandbox-dispose.test.mjs` passed 2/2. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5534 npm --prefix web run test:script-sandbox-dispose` passed unit 2/2 and Chromium Worker E2E 1/1. +- `node tools/web/check-script-sandbox-dispose.mjs` passed: + `script-sandbox-dispose-ok ports=0 timers=0 abortControllers=0 buffers=0 pending=0 cacheReferences=0 idempotent=true next=M13-03G`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `f3d9f667c3809cfad0f52bc6028d93e36e25c4b639fb6081c9c933fbad0ebc0a` +- worker: `2a074b05d301c4083e60534eb9452aabf5d95aea0ab316fa657441ef4bcd5c96` +- checker: `6549bd10f588281d23c4bea705fd164252c9e3f44d8f4b93c893a2c410907135` +- unit: `6e00aa6286aae0eabc1345c04c7628dcc9acb32d51c1c27436a0e1b4c170b64c` +- e2e: `b240d92c90e0d81272cd9cb3c0eb4e7d77102ce2e49ad05c761f763d1812b18e` +- package: `0f06cd7ccdeed4213b6cb956aecf94e60dceff811ad8c3a2e239397685cfc1bf` +- report: `2dbd3e79939b500c0fc83216c51f258b88ead42a667ed1a6da755e893d7f73c6` +- manifest: `96abc7c7613ef4c18c64b84ffb8420c39369f9e4733bd1fa4d7d9ee9e53d6f89` + +## Next Task + +`M13-03G`: recover a small script in the same session with a new Worker generation while keeping +the Main revision/source hash stable and extending the audit hash chain. + +## Rollback + +Remove the dispose protocol, Worker, focused tests/checker, report, manifest, package command and +this status entry; restore M13-03E as the queue tail and move `nextTask` back to `M13-03F`. diff --git a/docs/status/M13-03G.md b/docs/status/M13-03G.md new file mode 100644 index 00000000..1711cc57 --- /dev/null +++ b/docs/status/M13-03G.md @@ -0,0 +1,43 @@ +# M13-03G Status + +status: done +task: same-session sandbox recovery and audit continuity +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +A denied script request can be resumed in the same session after a Worker generation change from 4 +to 5. The Main revision remains 11, and the source and canonical manifest hashes remain stable. +Two default-deny audit entries use distinct request IDs, sequence 1/2, and a continuous +`previousEntrySha256 -> entrySha256` chain. Replay and source-hash tampering are rejected. This task +does not enable Python execution or claim Blender server isolation. + +## Evidence + +- `node --test web/tests/unit/script-sandbox-recovery.test.mjs` passed 2/2. +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5535 npm --prefix web run test:script-sandbox-recovery` passed unit 2/2 and Chromium Worker E2E 1/1. +- `node tools/web/check-script-sandbox-recovery.mjs` passed: + `script-sandbox-recovery-ok generation=4->5 revision=11 sourceStable=true manifestStable=true sequence=1,2 chain=true replay=SCRIPT_MANIFEST_INVALID tamper=SCRIPT_MANIFEST_INVALID next=M13-04A`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `8034faded657d49e1376ea42051bc302a090b485024a9ff3781e46aa82638b64` +- worker: `7b02331c375d4fb7dbadadd179f0ab9a0e336c8b95fabb071c06e67f3e7b3fe4` +- checker: `612015a3fca44bae0f0b78e622f044a5297ed8aa3a0efc774a40f0d47ddbdc9e` +- unit: `69a2d34e38d591043ff62b2d305bd9aae684ecebd9ecba718580d77318931226` +- e2e: `73176e513f115ba917be74f62a5deb8fe2918fafa8a84c9294e80d2a86b8f1ed` +- package: `3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd` +- report: `9057b3f49c3bb15cf53d638ada4bd2743949d3914173cd3799414489d584111d` +- manifest: `5cd365eeebbd6a7f56f380af103b66f2beb11eb49cb6db6adb49972b572cf2ec` + +## Next Task + +`M13-04A`: create an unpredictable one-shot directory for each real Blender server job. + +## Rollback + +Remove the recovery protocol, Worker, focused tests/checker, report, manifest, package command and +this status entry; restore M13-03F as the queue tail and move `nextTask` back to `M13-03G`. diff --git a/docs/status/M13-04A.md b/docs/status/M13-04A.md new file mode 100644 index 00000000..6883ecc2 --- /dev/null +++ b/docs/status/M13-04A.md @@ -0,0 +1,44 @@ +# M13-04A Status + +status: done +task: server job one-shot directory +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The server-side job allocator creates an unpredictable `0700` directory below a configured absolute +root for each request. Directory names do not contain the request ID, two concurrent jobs receive +distinct directories, and successful or failed cleanup removes the directory exactly once. Repeated +cleanup is idempotent. This task does not claim read-only source mounts, process limits or Blender +execution. + +Browser E2E is not applicable: this is a Node server filesystem boundary. The checker uses real +temporary directories and `fs.stat`/`fs.rm` calls. + +## Evidence + +- `node --test web/tests/unit/server-job-isolation.test.mjs` passed 2/2. +- `npm --prefix web run test:server-job-directory` passed unit 2/2 and the independent checker. +- `node tools/web/check-server-job-isolation.mjs` passed: + `server-job-directory-ok allocated=2 cleaned=2 unique=1 requestIdsHidden=1 mode=0700 residual=0 idempotent=1 next=M13-04B`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `3aa5678a2c2769e4db5bb8f5c755b97e23045c4106e5636459748ab9b41929cd` +- checker: `b8d0e741144f742946246370bd35820d806686fb00dc0b040c925cf16f2179da` +- unit: `b1ac3324332180f7b3522f135520e7b5dace334dc461c92f2bce48fbcba2e147` +- package: `0c2633da831e7ca10cf74797650353b6dfb1507ed5957688d34bc97f9a6dd193` +- report: `07ed66fe0b2e1f1bbdba1cfb1089b052a5961d38f761bfcf79d8362980d2d502` +- manifest: `fd2407a7b9fefe67e2d77ed844e6c2ca17cdc4a746857a5825a38ff02ab664e3` + +## Next Task + +`M13-04B`: source read-only mount and independent writable output directory. + +## Rollback + +Remove the directory allocator, focused tests/checker, report, manifest, package command and this +status entry; restore M13-03G as the queue tail and move `nextTask` back to `M13-04A`. diff --git a/docs/status/M13-04B.md b/docs/status/M13-04B.md new file mode 100644 index 00000000..c4703d7b --- /dev/null +++ b/docs/status/M13-04B.md @@ -0,0 +1,41 @@ +# M13-04B Status + +status: done +task: server source read-only and output isolation +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Each allocated server job now gets a `source/` directory and a separate `output/` directory. The +source directory is `0555` and the staged `.blend` is `0444`; a source overwrite fails with +`EACCES`. The output directory is `0700` and accepts result bytes. Cleanup temporarily restores +the source directory permission so the one-shot job directory can be removed without residue. +This task does not claim OS/container CPU, memory or process limits. + +## Evidence + +- `node --test web/tests/unit/server-job-isolation.test.mjs` passed 3/3. +- `npm --prefix web run test:server-job-workspace` passed unit 3/3 and the independent checker. +- `node tools/web/check-server-job-workspace.mjs` passed: + `server-job-workspace-ok sourceDir=0555 sourceFile=0444 sourceWrite=EACCES outputDir=0700 outputWrite=OK distinct=1 residual=0 next=M13-04C`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `3aa5678a2c2769e4db5bb8f5c755b97e23045c4106e5636459748ab9b41929cd` +- checker: `9490c2eeb9980073fc1fe77fdba1fb6e4ef528de8eb666d9eaf1fb582ba658c2` +- unit: `b1ac3324332180f7b3522f135520e7b5dace334dc461c92f2bce48fbcba2e147` +- package: `3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd` +- report: `650a643cc92617d068cb96d8bd4303429ef169ea89e3d4cb63e3ce5e2428e6d2` +- manifest: `9bc906b9e4c9751229f03271ef45a5925122b89b15bfe775591775f121ce1a4c` + +## Next Task + +`M13-04C`: enforce CPU, memory, process, file, wall-time and output budgets at the server boundary. + +## Rollback + +Remove the workspace protocol additions, focused tests/checker, report, manifest, package command and +this status entry; restore M13-04A as the queue tail and move `nextTask` back to `M13-04B`. diff --git a/docs/status/M13-04C.md b/docs/status/M13-04C.md new file mode 100644 index 00000000..2b343f8a --- /dev/null +++ b/docs/status/M13-04C.md @@ -0,0 +1,43 @@ +# M13-04C Status + +status: done +task: server job resource budget gate +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Server jobs now have fixed CPU, memory, process, file, wall-time and output-byte limits. Budget +parsing rejects undeclared fields and out-of-range values. A usage receipt is marked +`enforced=true` only when every measured value is within its corresponding limit; each of the six +over-limit cases returns `SERVER_JOB_BUDGET_EXCEEDED` and remains `execution=DISABLED`. + +This is the resource contract and usage gate. OS/container enforcement is proven by the later real +process task and is not claimed here. + +## Evidence + +- `node --test web/tests/unit/server-job-resource-budget.test.mjs` passed 2/2. +- `npm --prefix web run test:server-job-resource-budget` passed unit 2/2 and the independent checker. +- `node tools/web/check-server-job-resource-budget.mjs` passed: + `server-job-budget-ok cpu=bounded memory=bounded process=bounded files=bounded wall=bounded output=bounded overages=6 execution=DISABLED next=M13-04D`. +- `npm --prefix web run typecheck`, `npm --prefix web run lint` and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `9746f0aef9dd76411320792dee1213c03755a3c03a87bbc4cbf88d4324c728d6` +- checker: `5599f4e25fc3ceca18e04be322450a4dc5bb6ce9c72c3abe9f1afc9c81851ff8` +- unit: `05212b2aa639f4c37e37e1cac0597d9b367a1280e519240f073762394914dc63` +- package: `3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd` +- report: `76ba684966bf7e680b0ebfc630ae9e080f04dd6daa6dd744bbb22dcff43f8eb1` +- manifest: `d00fff709b57909ec418ff0e476dca97281fc39456f674c63b51248377fc6ab2` + +## Next Task + +`M13-04D`: default-deny network policy with explicit declared-origin admission. + +## Rollback + +Remove the resource budget protocol, focused tests/checker, report, manifest, package command and +this status entry; restore M13-04B as the queue tail and move `nextTask` back to `M13-04C`. diff --git a/docs/status/M13-04D.md b/docs/status/M13-04D.md new file mode 100644 index 00000000..1bf9c26b --- /dev/null +++ b/docs/status/M13-04D.md @@ -0,0 +1,24 @@ +# M13-04D Status + +status: done +task: server job network policy +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +Server jobs default to `DENY`. Only an explicitly declared canonical HTTPS or loopback origin is +`ALLOWED`; missing, undeclared, credential-bearing, path-bearing, public HTTP and invalid origins +return `SERVER_NETWORK_DENIED` or `SERVER_NETWORK_POLICY_INVALID`. Execution remains `DISABLED`. + +Evidence: `node --test web/tests/unit/server-job-network-policy.test.mjs` 2/2; +`npm --prefix web run test:server-job-network-policy` passed; checker output +`server-job-network-ok default=DENY declaredOrigin=ALLOWED missing=SERVER_NETWORK_DENIED undeclared=SERVER_NETWORK_DENIED execution=DISABLED next=M13-04E`; +typecheck, lint and `git diff --check` passed. + +Artifact hashes: protocol `0dc3698383fbfa511bebfeca41504c7c04a8302d47168cfae94f01f2a11a316c`, +checker `6f3e9cd9f988313863345f392036e6dbb522102c969197034800f419e497ad92`, unit +`c0ba10ccd2fb6ec8878a1b4a54bc6a12e49d35818566e0d55ef307779e8c8b77`, report +`37bcba2666e8c76c2e07d23e285737004affbb0372ec6dd84bb6802dba551527`, manifest +`11ce246597e3321b346ed4fed00edba22708d97250163d332d7ba8a003d8849a`. + +Next task: `M13-04E`, fixed Blender background/factory-startup runtime identity. diff --git a/docs/status/M13-04E.md b/docs/status/M13-04E.md new file mode 100644 index 00000000..dd07687e --- /dev/null +++ b/docs/status/M13-04E.md @@ -0,0 +1,22 @@ +# M13-04E Status + +status: done +task: pinned Blender server startup +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +Pinned Blender 5.2.0 LTS now passes a real server preflight using only `--background`, +`--factory-startup`, the fixed `tools/web/server-job-startup.py`, and a fixed business argument. +The structured receipt confirms background mode, runtime version and argv. Execution remains +`DISABLED`; this task does not claim result publication. + +Evidence: `node tools/web/check-server-job-startup.mjs` output +`server-job-startup-ok blender=5.2 background=1 factory=1 userPrefs=0 fixedScript=1 execution=DISABLED next=M13-04F`; +typecheck, lint and `git diff --check` passed. Artifact hashes: startup +`c8c5b5a7a7880d4df9477a94a98e54e328baf582ade649807067f988484e1f12`, checker +`66b2e25efe0e7455348e8f488cd0f6ebef07f916fcf6541680e7d18e4f69ab10`, report +`c67d2972e584782d03479dbdb36dae0976074978f4ce9a48e41e7ad5aea66089`, manifest +`a24ea87bcf305bc15d8ae70b2abd03aa16ffbd6b127d5e9da9da23617d7201cc`. + +Next task: `M13-04F`, bounded and redacted stdout/stderr. diff --git a/docs/status/M13-04F.md b/docs/status/M13-04F.md new file mode 100644 index 00000000..932d5661 --- /dev/null +++ b/docs/status/M13-04F.md @@ -0,0 +1,40 @@ +# M13-04F Status + +status: done +task: bounded and redacted server stdout/stderr +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Server job output is normalized through a production receipt before publication. Credentials, bearer/basic +tokens, secret headers and Unix/Windows/file URL paths are replaced; UTF-8 output is truncated at fixed +stdout/stderr byte budgets with an explicit marker. The receipt records original/emitted bytes, redaction +count and truncation state. Server execution remains `DISABLED`; process cancellation and result publication +are separate tasks. + +## Evidence + +- `npm --prefix web run test:server-job-output-redaction` passed 3/3 unit tests and the independent checker. +- Checker output: `server-job-output-ok stdoutTruncated=1 stderrTruncated=1 redactions=6 totalBounded=1 execution=DISABLED next=M13-04G`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `a24ea87bcf305bc15d8ae70b2abd03aa16ffbd6b127d5e9da9da23617d7201cc` +- protocol: `5a58e7080324b399905c6a20a313225153ba94c75a55d21c4bbdd957ae4a2462` +- checker: `5c2db9d3313484bb68ac65822828467b3f99cb03e8374935757cfcb4b4ed908d` +- unit: `4ae6266bce22f84e73113b93a590a0c32d9452e748003af251a602024d0c6db5` +- package: `52781eb2650133b43271889a22ce38023e611641da15411d31698c6cfc0ce19c` +- report: `cb6e3ed8d6cc0a69b333b6bff3199593bdfbae14fa7fa700dcb12e9e9cea519a` +- manifest: `b5b4b26183dc48ebcae009e49999010d9b60d512f5daa4674a0aff0577d6602c` + +## Next Task + +`M13-04G`: cancel Blender process tree and clean the one-shot directory. + +## Rollback + +Remove the output receipt protocol, unit/checker, report, manifest, package command and this status entry; +restore `M13-04E` as the queue tail. diff --git a/docs/status/M13-04G.md b/docs/status/M13-04G.md new file mode 100644 index 00000000..25acfa8f --- /dev/null +++ b/docs/status/M13-04G.md @@ -0,0 +1,38 @@ +# M13-04G Status + +status: done +task: server process-tree cancellation +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Real server jobs now run in a detached process group on POSIX and are cancelled as a tree. The cancellation +path sends SIGTERM to the group, escalates to SIGKILL after the grace window, waits for the group to exit, +and performs one idempotent job-directory cleanup. Windows uses `taskkill /T /F`. The receipt reports the +actual signal path, cleanup count and orphan count. Execution remains `DISABLED`. + +## Evidence + +- `npm --prefix web run test:server-job-cancellation` passed 2/2 real-process unit tests and the checker. +- Checker output: `server-job-cancel-ok state=CANCELLED tree=SIGTERM_GROUP cleanup=1 orphan=0 residual=0 execution=DISABLED next=M13-04H`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `b5b4b26183dc48ebcae009e49999010d9b60d512f5daa4674a0aff0577d6602c` +- protocol: `74fbbac806f34904bc9cb8e40eaf386cb60d44d978973950f1809262d311ec95` +- checker: `751ecafc642595d0de69b618e223cbff49d414f2958fc371371e4a668d66a8cc` +- unit: `39ac445ff51a9bfe249363204cf2edca906b16bf67ab1f8321166b440eba8629` +- package: `ca092a9a1d48ac41dc20f978bb26c8268adde9f7602a06459c7df8c2cb2d4cc7` +- report: `61f5c3ad315f94bac48e58627fcc46015c173dd9f303d5914acb8dbbe93caa97` + +## Next Task + +`M13-04H`: map timeout/OOM/non-zero exit/signal to stable error codes. + +## Rollback + +Remove the process control protocol, unit/checker, report, manifest, package command and this status entry; +restore `M13-04F` as the queue tail. diff --git a/docs/status/M13-04H.md b/docs/status/M13-04H.md new file mode 100644 index 00000000..58d82694 --- /dev/null +++ b/docs/status/M13-04H.md @@ -0,0 +1,38 @@ +# M13-04H Status + +status: done +task: stable server process fault codes +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Timeout, bounded-memory OOM, signal termination and non-zero exit are converted to stable server error +codes. Classification precedence is cancellation, timeout, OOM, signal, then non-zero exit. Failed receipts +keep the base/current revision equal, set `publish=false`, and expose no internal process details. Execution +remains `DISABLED`. + +## Evidence + +- `npm --prefix web run test:server-job-fault-codes` passed 3/3 unit tests and the checker. +- Checker output: `server-job-faults-ok timeout=SERVER_JOB_TIMEOUT oom=SERVER_JOB_OOM signal=SERVER_JOB_SIGNAL exit=SERVER_JOB_EXIT_FAILED revisionPreserved=1 publish=0 execution=DISABLED next=M13-04I`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `7103a25ec7eb4a1c7f0d9f7acfb5ed30da32c87d512d6f9d53e081db275f7b89` +- protocol: `858360b4468a7b6cf796fc83a1627ba18626b61a0799f83531de996ea374519b` +- checker: `cb67246ef3df111525ca5e14af0546154ea2beee930b9840ad4ac7e2ebbb91ec` +- unit: `3551e28d78e5069c3d97eabcacac824bf142f54fdcb3ad4904668909af1cc545` +- package: `77bae66fd3885e36d2ead6f261754724144e5f51fafc5ccad9863dfc8e5ab548` +- report: `01999232f926496909fc9ef072e19aadb688822d3f9d7e03b42871ac5d54a0f6` + +## Next Task + +`M13-04I`: verify source/settings/build/output hashes before OPFS commit. + +## Rollback + +Remove the fault classifier, unit/checker, report, manifest, package command and this status entry; restore +`M13-04G` as the queue tail. diff --git a/docs/status/M13-04I.md b/docs/status/M13-04I.md new file mode 100644 index 00000000..f32046b6 --- /dev/null +++ b/docs/status/M13-04I.md @@ -0,0 +1,38 @@ +# M13-04I Status + +status: done +task: server result hash binding and atomic OPFS handoff +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Server output publication requires request identity plus source, settings and Blender build SHA-256 values +to match the expected request. Output bytes are staged, read back, atomically renamed to the request result +path, and read back again before a committed receipt is returned. Tampered output, identity drift, partial +stage and quota failures remain unpublished. Execution remains `DISABLED`. + +## Evidence + +- `npm --prefix web run test:server-job-result-binding` passed 2/2 unit tests and the checker. +- Checker output: `server-job-result-ok source=bound settings=bound build=bound output=verified tamper=blocked quota=blocked atomic=1 execution=DISABLED next=M13-04J`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `ba530ddff6e6fdd84057444ce757fad05a30d825a5915abebe2d5910a689af7f` +- protocol: `ecf3bddef53e2766dcc055ec027b04c14e3d884132c7fdea476663617364ad86` +- checker: `b0fbffd2937ab20e72d337bb5ff63ccf188ccf8a939d8ccc8b4878d80c9a4a19` +- unit: `c5538798aa6263b8a1c20270a019f8b4fb8b176b141a6411b72aad21a457bfd0` +- package: `5d269d4e5ff4385d919c0ee33f996dcb814e3e415da25dbf3b4dbdc3312fbe24` +- report: `ec63faeba35a641a22c3b3a69757bd37e8e9acaa1dda11a64faf7bed80439784` + +## Next Task + +`M13-04J`: idempotent request retry and conflicting-result isolation. + +## Rollback + +Remove the result-binding protocol, unit/checker, report, manifest, package command and this status entry; +restore `M13-04H` as the queue tail. diff --git a/docs/status/M13-04J.md b/docs/status/M13-04J.md new file mode 100644 index 00000000..0eee3a55 --- /dev/null +++ b/docs/status/M13-04J.md @@ -0,0 +1,38 @@ +# M13-04J Status + +status: done +task: server request idempotency and conflict isolation +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +`projectId/requestId` is now an idempotency key. An exact retry verifies and reuses the existing result; +different output or source/settings/build identity returns a stable conflict and cannot overwrite the bound +result. Different requests use separate result and receipt files. Concurrent duplicate requests share one +in-flight submission and resolve as one commit plus one reuse. Execution remains `DISABLED`. + +## Evidence + +- `npm --prefix web run test:server-job-idempotency` passed 3/3 unit tests and the checker. +- Checker output: `server-job-idempotency-ok first=COMMITTED retry=REUSED conflict=BLOCKED isolated=1 concurrent=REUSED execution=DISABLED next=M13-05A`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `f9658954d3dbd0cd1edd696c10f6a1b880bb3288c91a39a47ca84c4496046a97` +- protocol: `15eca7550964c9985e26a22a7900d3f3aa69fdef35fe5962b48a8f55ebba5863` +- checker: `02247ffda07c095373642a7fa22268bb9f5ac181d41571f7fff685812976d3c4` +- unit: `6ee08e67cd8493e856f67309bd6562316eca42ef52dd4a9aad3a15efdc4b6b41` +- package: `5b47e94cf828fc9d3021019eed9922eb67815aba5416266d8c967c7cb5bd96ba` +- report: `31f95a8cc1c4792303986b8b5987ab02fc11a77ab25fd76b31c02d1ed1b104d1` + +## Next Task + +`M13-05A`: CSP denies inline script, eval, data script and undeclared origins. + +## Rollback + +Remove the idempotency protocol, unit/checker, report, manifest, package command and this status entry; +restore `M13-04I` as the queue tail. diff --git a/docs/status/M13-05A.md b/docs/status/M13-05A.md new file mode 100644 index 00000000..5688fb7d --- /dev/null +++ b/docs/status/M13-05A.md @@ -0,0 +1,45 @@ +# M13-05A Status + +status: done +task: CSP default-deny policy +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The deployment contract now requires one CSP on every response: same-origin default/script/worker/connect +sources, no inline or eval execution, no data/blob script source, and object/base/frame embedding denied. +The Vite source, entry HTML, production source tree and built JS/HTML are scanned for dynamic code and data +scripts. The deployment HTTP checker verifies the policy on normal, missing and entry responses. Resource- +specific Worker/WASM/font/image/media rules remain in M13-05B. + +## Evidence + +- `npm --prefix web run test:csp-policy` passed. +- `node tools/web/check-deployment-contract.mjs` passed. +- `npm --prefix web run typecheck` passed. +- `npm --prefix web run build` passed; built output was scanned by the CSP checker. +- Checker output: `csp-policy-ok inline=DENY eval=DENY dataScript=DENY undeclaredConnect=DENY responses=3 execution=DISABLED next=M13-05B`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `d230ae49dbf38cd9e95e7379a1a49332e478d3d23b3638730d2ec6167204c368` +- contract: `3f2b0b10c5a5698be2adda61af80cfb9cb9291fb9797994bfa66a77b7b511eeb` +- deployment server: `a5734ce9760f65cf5bade8cc209454d39fa963ad7dfaf20653728a9d7a57b04c` +- contract checker: `5e2d80f5c1377420f4779291fa73c206741ed415e751e79ce5edae7ba039febf` +- CSP checker: `f97ab074da860e3f8489e520c8bb5932f858409136ef25cca56247bb1310340d` +- index: `16b7691b191127f84c5143e23aecdfc203dc9e279d34cb270b37631fcbd5f856` +- Vite: `fd160c685ed780738e2b37ce9a32e5d4718e11a2875e12d86ddad310213abaf3` +- package: `5b47e94cf828fc9d3021019eed9922eb67815aba5416266d8c967c7cb5bd96ba` +- report: `9c7c64bb021161a1165a7c089ccf3f4897877635914260f6f0b0da119a5036fb` + +## Next Task + +`M13-05B`: verify resource-specific CSP for Worker, WASM, font, image and media paths. + +## Rollback + +Remove the CSP contract/checker changes, report, manifest, package command and this status entry; restore +`M13-04J` as the queue tail. diff --git a/docs/status/M13-05B.md b/docs/status/M13-05B.md new file mode 100644 index 00000000..77e16471 --- /dev/null +++ b/docs/status/M13-05B.md @@ -0,0 +1,31 @@ +# M13-05B Status + +status: done +task: resource-specific CSP policy +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The deployment contract now separates same-origin policy for Worker, WASM, font, image and media +resources. Chromium loaded same-origin Worker, WASM, image and media resources, and rejected data-image, +blob-Worker and cross-origin connect attempts. WASM uses only the explicit `wasm-unsafe-eval` source +expression; JavaScript `eval` remains denied. Resource responses and 404 responses carry the same policy. + +## Evidence + +- `npm --prefix web run test:csp-resource-policy` passed the HTTP resource matrix and Chromium browser gate. +- `node tools/web/check-deployment-contract.mjs` passed. +- `npm --prefix web run typecheck` passed. +- `npm --prefix web run build` passed. +- `git diff --check` passed. + +## Next Task + +`M13-05C`: production/build/test dependency inventories. + +## Rollback + +Remove the resource-specific CSP/MIME changes, checker, report, manifest, package command and this status; +restore `M13-05A` as the queue tail. diff --git a/docs/status/M13-05C.md b/docs/status/M13-05C.md new file mode 100644 index 00000000..c7ce0e35 --- /dev/null +++ b/docs/status/M13-05C.md @@ -0,0 +1,27 @@ +# M13-05C Status + +status: done +task: production/build/test dependency inventories +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Evidence + +`web/package.json` and `web/package-lock.json` are parsed as the only dependency sources. Deterministic +transitive closures contain production=3, build=135 and test=6 packages; every package is classified and +records its path, version, resolved URL and lockfile integrity. A fresh regeneration is byte-identical. + +- `npm --prefix web run test:dependency-inventory` passed. +- `npm --prefix web run typecheck` passed. +- `npm --prefix web run build` passed. +- `git diff --check` passed. + +## Next Task + +`M13-05D`: severity gates and documented dependency exceptions. + +## Rollback + +Remove the inventory generator/checker, report, manifest, package command and this status; restore +`M13-05B` as the queue tail. diff --git a/docs/status/M13-05D.md b/docs/status/M13-05D.md new file mode 100644 index 00000000..f23dc8e4 --- /dev/null +++ b/docs/status/M13-05D.md @@ -0,0 +1,18 @@ +# M13-05D Status + +status: done +task: dependency severity gates and exceptions +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +The severity policy is machine-readable: BLOCKER/HIGH block, MEDIUM requires review and LOW is tracked. +Exceptions require a non-empty owner, non-expired ISO date, reason and alternative control. Production +inventory has zero findings and zero exceptions, while four negative/positive exception cases are checked. + +- `npm --prefix web run test:dependency-severity-policy` passed. +- `npm --prefix web run typecheck` passed. +- `npm --prefix web run build` passed. +- `git diff --check` passed. + +Next task: `M13-05E`, SBOM/license/source-offer binding. diff --git a/docs/status/M13-05E.md b/docs/status/M13-05E.md new file mode 100644 index 00000000..53bcf0e8 --- /dev/null +++ b/docs/status/M13-05E.md @@ -0,0 +1,13 @@ +# M13-05E Status + +status: done +task: SBOM/license/source-offer and archive binding +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +The supply-chain checker binds SPDX 2.3 to the lockfile and notices hashes, verifies the corresponding +source offer and source archive contents, checks top-level archive SHA256SUMS, and records the current +commit. Binary/source archive validators also pass independently; script execution remains disabled. + +Next task: `M13-05F`, malicious input matrix. diff --git a/docs/status/M13-05F.md b/docs/status/M13-05F.md new file mode 100644 index 00000000..20baa3cb --- /dev/null +++ b/docs/status/M13-05F.md @@ -0,0 +1,14 @@ +# M13-05F Status + +status: done +task: malicious input matrix +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +The production blend loader rejected five malformed blend cases. Existing production protocol tests +rejected malformed image previews, fonts, media cache manifests, shader node graphs, script manifests and +GLB inputs; archive metadata rejected six ZIP/TAR traversal, link, duplicate and bomb cases without +extraction. All matrix categories are stable reject paths and execution remains disabled. + +Next task: `M13-05G`, fuzz crash minimization and regression capture. diff --git a/docs/status/M13-05G.md b/docs/status/M13-05G.md new file mode 100644 index 00000000..d5e866b9 --- /dev/null +++ b/docs/status/M13-05G.md @@ -0,0 +1,37 @@ +# M13-05G Status + +status: done +task: fuzz crash minimization and regression capture +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The deterministic runner replays 16 mutations in each of the blend, image, font, node-graph and script +manifest domains using seed `1511506142`. Every case returns one structured `ACCEPTED` or `REJECTED` +receipt; a non-zero runner exit first writes a replayable corpus sample. This run completed 80 cases with +zero crashes and an empty minimized corpus. Script execution remains disabled. + +## Evidence + +- `npm --prefix web run test:fuzz-regression` passed. +- Checker output: `fuzz-regression-ok seed=1511506142 cases=80 crashes=0 corpus=0 execution=DISABLED next=M13-05H`. +- `git diff --check` passed. + +## Artifact Hashes + +- parent manifest: `34043df0515f4b422a2b265d38978dd3c7f21acd12753c99f9a50f9b23f8bd60` +- runner: `a4d5d1b743b454705b1c3a257f1f36d1fea8abd77442e11fb1fec9e2905a3f79` +- checker: `55c8fa3776b22eb3f2e2ffd1629f0092384df30ef3913699a1ba93b283d641d8` +- package: `1feb509789d7f06019390bd86197bb9851cd520fd9fe310d1e29bfc3d2ef3f18` +- report: `5eb4a5469732697be7910fded3ce34cf2898735f767d94c1abbe0dd264bcc0d9` + +## Next Task + +`M13-05H`: fuzz corpus replay and release-boundary exclusion. + +## Rollback + +Remove the fuzz runner/checker, report, manifest, package command and this status entry; restore +`M13-05F` as the queue tail. diff --git a/docs/status/M13-05H.md b/docs/status/M13-05H.md new file mode 100644 index 00000000..42456120 --- /dev/null +++ b/docs/status/M13-05H.md @@ -0,0 +1,30 @@ +# M13-05H Status + +status: done +task: script audit record integrity +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production scripting audit protocol now has an independent integrity gate. Two default-deny audit +records are validated with strictly increasing ISO timestamps, contiguous sequence numbers, unique request +IDs, canonical entry digests and a continuous previous-entry hash chain. Replay, time reversal, sequence +mutation, entry digest mutation and chain mutation all return `SCRIPT_MANIFEST_INVALID`; execution remains +disabled. + +## Evidence + +- `npm --prefix web run test:script-audit-integrity` passed 2 unit tests and the checker. +- Checker output: `script-audit-integrity-ok entries=2 sequence=1,2 requestIds=unique time=ordered chain=true replay=SCRIPT_MANIFEST_INVALID tamper=3 execution=DISABLED next=M14-01A`. +- `git diff --check` passed. + +## Next Task + +`M14-01A`: freeze the current Chromium release, engine and archive hashes. + +## Rollback + +Remove the audit integrity checker/unit, report, manifest, package command and this status entry; restore +`M13-05G` as the queue tail. diff --git a/docs/status/M14-01A.md b/docs/status/M14-01A.md new file mode 100644 index 00000000..6556707d --- /dev/null +++ b/docs/status/M14-01A.md @@ -0,0 +1,32 @@ +# M14-01A Status + +status: done +task: Chromium, engine and archive identity freeze +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The Chromium baseline is frozen at `150.0.7871.128`. The checker resolves and hashes the actual browser +executable, validates every single/pthread engine resource against `engine-manifest.json`, and validates +both release archives against the regenerated `RC_MANIFEST.json` and `SHA256SUMS.txt`. Execution remains +disabled. + +## Evidence + +- `npm --prefix web run release:offline` passed deterministic binary/source rebuild twice. +- `node tools/web/create-rc-manifest.mjs && node tools/web/check-rc-manifest.mjs` passed. +- `npm --prefix web run test:chromium-freeze` passed with browser `150.0.7871.128`, two engine variants, + and two archive bindings. +- `npm --prefix web run typecheck` passed; execution remains `DISABLED`. +- `git diff --check` passed. + +## Next Task + +`M14-01B`: Firefox capability probe using the frozen field set. + +## Rollback + +Remove the Chromium freeze checker, report, manifest, package command and this status entry; restore +`M13-05H` as the queue tail. diff --git a/docs/status/M14-01B.md b/docs/status/M14-01B.md new file mode 100644 index 00000000..60c6b0db --- /dev/null +++ b/docs/status/M14-01B.md @@ -0,0 +1,21 @@ +# M14-01B Status + +status: done +task: Firefox capability probe +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +Firefox `153.0` was probed against the rebuilt production assets with COOP/COEP isolation. WASM, Worker, +OPFS, IndexedDB, WebGL2, OffscreenCanvas and cross-origin isolation passed. WebGPU is explicitly +`BLOCKED/WEBGPU_UNAVAILABLE`; it does not change the Chromium-only release claim. + +## Evidence + +- `npm --prefix web run test:firefox-capability` passed. +- Checker output: `firefox-capability-ok version=153.0 wasm=PASS,worker=PASS,opfs=PASS,indexedDB=PASS,webgl2=PASS,webgpu=BLOCKED,offscreen=PASS,isolation=PASS execution=DISABLED next=M14-01C`. +- `git diff --check` passed. + +## Next Task + +`M14-01C`: WebKit capability probe with the same fields. diff --git a/docs/status/M14-01C.md b/docs/status/M14-01C.md new file mode 100644 index 00000000..5d5dd113 --- /dev/null +++ b/docs/status/M14-01C.md @@ -0,0 +1,21 @@ +# M14-01C Status + +status: done +task: WebKit capability probe +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +WebKit `26.5` was probed against the rebuilt production assets with COOP/COEP isolation. WASM, Worker, +IndexedDB, WebGL2, OffscreenCanvas and isolation passed. OPFS and WebGPU are explicitly blocked; the +result does not change the Chromium-only release claim. + +## Evidence + +- `npm --prefix web run test:webkit-capability` passed. +- Checker output: `webkit-capability-ok version=26.5 wasm=PASS,worker=PASS,opfs=BLOCKED,indexedDB=PASS,webgl2=PASS,webgpu=BLOCKED,offscreen=PASS,isolation=PASS execution=DISABLED next=M14-01D`. +- `git diff --check` passed. + +## Next Task + +`M14-01D`: probe identity and GPU/OS adapter recording. diff --git a/docs/status/M14-01D.md b/docs/status/M14-01D.md new file mode 100644 index 00000000..927dc36c --- /dev/null +++ b/docs/status/M14-01D.md @@ -0,0 +1,33 @@ +# M14-01D Status + +status: done +task: probe identity and GPU/OS adapter recording +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The production Chromium probe records observed browser identity, OS kernel/runtime identity, +hardware concurrency, WebGL2 vendor/renderer/version, WebGPU adapter identity, and cross-origin +isolation. WebGPU is recorded as `BLOCKED/WEBGPU_ADAPTER_UNAVAILABLE` in this headless environment; +the result does not claim WebGPU support. The report is bound to the exact worker/WASM asset hashes +used by the probe and carries a canonical identity SHA-256. + +## Evidence + +- `npm --prefix web run build` passed. +- `npm --prefix web run test:probe-identity` passed twice (one report-generation run and one frozen + report verification run). +- Checker output: `probe-identity-ok version=151.0.7922.34 os=linux/x64 webgl2=PASS,webgpu=BLOCKED identity=3c6f898e1b6ffd39862505b8e7dde8fd29369204e1670a013859f74ccaf1c1eb execution=DISABLED next=M14-01E`. +- `git diff --check` passed. + +## Next Task + +`M14-01E`: Chromium WebGPU fail-closed boundary. Firefox and WebKit are excluded by the project +Chromium-only iron rule. + +## Rollback + +Remove the probe checker, report, manifest, package command and this status/task entry; keep +`M14-01C` as the queue tail. diff --git a/docs/status/M14-01E.md b/docs/status/M14-01E.md new file mode 100644 index 00000000..28272356 --- /dev/null +++ b/docs/status/M14-01E.md @@ -0,0 +1,29 @@ +# M14-01E Status + +status: done +task: Chromium WebGPU fail-closed boundary +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Chromium routing was checked against the production render protocol. A bounded Eevee request using +WebGPU returns `BLOCKED/WEBGPU_RENDERER_UNAVAILABLE` when no bundled adapter is available, while the +same bounded request using WebGL2 remains `READY/WEB_LOCAL_BOUNDED`. This task does not launch Firefox +or WebKit. + +## Evidence + +- `npm --prefix web run test:chromium-webgpu-boundary` passed. +- Checker output: `chromium-webgpu-boundary-ok webgpu=BLOCKED/WEBGPU_RENDERER_UNAVAILABLE webgl2=READY execution=DISABLED next=M14-04A`. +- `git diff --check` passed. + +## Next Task + +`M14-04A`: Chromium GPU/memory budget device tier selection. + +## Rollback + +Remove the Chromium boundary checker, report, manifest, package command and this status/task entry; +restore `M14-01D` as the queue tail. diff --git a/docs/status/M14-04A.md b/docs/status/M14-04A.md new file mode 100644 index 00000000..88836ffd --- /dev/null +++ b/docs/status/M14-04A.md @@ -0,0 +1,25 @@ +# M14-04A Status + +status: done +task: Chromium GPU/memory budget device tier selection +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Added a bounded device-budget selector driven only by the real Chromium M14-01D identity report. +The observed SwiftShader renderer and unavailable WebGPU adapter select `CONSERVATIVE`; untrusted, +missing, or fallback adapters cannot expand budgets. Fixed `CONSERVATIVE`, `BALANCED`, and `HIGH` +limits are explicit and unknown tiers fail closed. Firefox/WebKit are excluded by the iron rule. + +## Evidence + +- `npm --prefix web run test:chromium-device-budget` passed: 3 unit tests plus the production identity + report checker. +- Checker output: `chromium-device-budget-ok tier=CONSERVATIVE reason=UNTRUSTED_ADAPTER identity=3c6f898e1b6ffd39862505b8e7dde8fd29369204e1670a013859f74ccaf1c1eb execution=DISABLED next=M14-04B`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Next Task + +`M14-04B`: Chromium DPR 1/1.5/2/3 canvas and raycast consistency. diff --git a/docs/status/M14-04B.md b/docs/status/M14-04B.md new file mode 100644 index 00000000..1cc1d6b4 --- /dev/null +++ b/docs/status/M14-04B.md @@ -0,0 +1,26 @@ +# M14-04B Status + +status: done +task: Chromium DPR 1/1.5/2/3 canvas and raycast consistency +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Shared viewport DPR and CSS-bounds NDC helpers now drive the main-thread and Offscreen viewport +boundaries. Chromium production verification ran at device scale factors 1, 1.5, 2 and 3; the +declared renderer cap of 2 was applied, backing dimensions matched the browser canvas, CSS dimensions +remained `679x321`, and the same center raycast selected the same object at every scale. + +## Evidence + +- `npm --prefix web run build` passed. +- `npm --prefix web run test:chromium-dpr-consistency` passed: 2 protocol tests and 4 real Chromium + production contexts. +- Checker output: `chromium-dpr-ok dpr=1,1.5,2,3 css=679x321 selection=stable execution=DISABLED next=M14-04C`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Next Task + +`M14-04C`: Chromium mouse/touch/pen pointer identity and cancellation contract. diff --git a/docs/status/M14-04C.md b/docs/status/M14-04C.md new file mode 100644 index 00000000..de878619 --- /dev/null +++ b/docs/status/M14-04C.md @@ -0,0 +1,26 @@ +# M14-04C Status + +status: done +task: Chromium mouse/touch/pen pointer identity and cancellation contract +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The main-thread and Offscreen viewport paths now record a shared pointer observation for mouse, +touch, and pen input. Pointer identity, pressure, tilt, button/buttons and `pointercancel` are +preserved with bounded values; unknown pointer types and invalid IDs fail closed. Chromium production +events verified down/cancel pairs for all three pointer types. Firefox/WebKit are excluded. + +## Evidence + +- `npm --prefix web run build` passed. +- `npm --prefix web run test:chromium-pointer-contract` passed: 2 protocol tests and real Chromium + mouse/touch/pen down/cancel events. +- Checker output: `chromium-pointer-ok types=mouse,touch,pen cancel=PASS execution=DISABLED next=M14-04D`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Next Task + +`M14-04D`: Chromium IME composition guard for incomplete operators. diff --git a/docs/status/M14-04D.md b/docs/status/M14-04D.md new file mode 100644 index 00000000..17617dad --- /dev/null +++ b/docs/status/M14-04D.md @@ -0,0 +1,26 @@ +# M14-04D Status + +status: done +task: Chromium IME composition guard for incomplete operators +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Added an explicit IME composition state machine and connected it to the App global shortcut gate. +Composition start/update records pending text and blocks operator shortcuts; composition end clears +the block without dispatching an incomplete operator. A real Chromium page dispatched composition +events and `G`; UI revision remained unchanged during composition. Firefox/WebKit are excluded. + +## Evidence + +- `npm --prefix web run build` passed. +- `npm --prefix web run test:chromium-ime-guard` passed: 2 protocol tests and real Chromium + compositionstart/update/keydown/compositionend events. +- Checker output: `chromium-ime-ok composing=BLOCKED_OPERATOR shortcut=G revision=0 execution=DISABLED next=M14-04E`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Next Task + +`M14-04E`: Chromium US/non-US/dead-key/modifier keymap fixture. diff --git a/docs/status/M14-04E.md b/docs/status/M14-04E.md new file mode 100644 index 00000000..200e3a0d --- /dev/null +++ b/docs/status/M14-04E.md @@ -0,0 +1,26 @@ +# M14-04E Status + +status: done +task: Chromium US/non-US/dead-key/modifier keymap fixture +updated: 2026-08-19 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +Added a versioned keyboard observation contract preserving layout character (`key`), physical key +(`code`), location, repeat, composition state, dead-key identity and modifier flags. Chromium +production events verified US, non-US character, dead-key and modifier fixtures. Firefox/WebKit are +excluded. + +## Evidence + +- `npm --prefix web run build` passed. +- `npm --prefix web run test:chromium-keymap-fixture` passed: 2 protocol tests and 4 real Chromium + keyboard fixtures. +- Checker output: `chromium-keymap-ok fixtures=US,NON_US,DEAD_KEY,MODIFIER observations=4 execution=DISABLED next=M14-04F`. +- `npm --prefix web run typecheck` and `git diff --check` passed. + +## Next Task + +`M14-04F`: Chromium touch modal cancel, two-finger navigation and single pen Main commit. diff --git a/docs/status/N-023.md b/docs/status/N-023.md index ab2ffedf..c2ac4a6c 100644 --- a/docs/status/N-023.md +++ b/docs/status/N-023.md @@ -1,7 +1,8 @@ # N-023 Asset、Library 与 IO 状态:`BLOCKED`(asset catalog、来源/许可证元数据、真实 Main library inventory、库依赖与 -IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨桌面重导入未实现) +IO 安全门已落地;bounded Append Main 及其 canonical persistence 已完成,Link/Override Main、 +非 GLB 本地导入和跨桌面重导入仍未实现) ## 完整对标盘点基线 @@ -91,8 +92,35 @@ M12-03D 已把同一 source-hash-bound closure 送入 WASM Worker 的 authoritat link/append context 递归导入 Object、Mesh、Material 和 packed Image,Main commit 前核对 source locator、root、closure 与 base revision;四个 ID 均无 `ID.lib`/override、映射为可写 `LOCAL_MAIN`, 并只产生一个新的 SceneIR revision。stale revision 与 local ID collision 在 Main mutation 前阻断, -失败路径回滚完整 history state。该项只证明单 transaction Main append,undo/redo/save/reopen 与 -desktop canonical report 的联合一致性仍由 M12-03E 验证。 +失败路径回滚完整 history state。 + +M12-03E 已在同一 Chromium/WASM Main 路径完成联合持久化门:append 只推进一个 revision,undo +移除 Object/Mesh/Material/Image 四个 closure ID,redo 恢复同一 canonical graph;save/reopen 后 +再次读取的依赖边、geometry、UV、material slot 和 packed image Float32 pixel hash 与 M12-03C +desktop report 精确一致。图像比较只对 Blender bottom-up `Image.pixels` 与 Canvas top-down row +做规范化;当前 SceneIR 缺省的 `Image.colorSpace` 使用 desktop sRGB semantic default,若字段出现 +则必须匹配,不放宽其它字段。该证据仍不覆盖 Link/Override 或完整 N-023 IO parity。 + +M12-03F 已用独立 Blender 5.2 desktop fixture 冻结 LINK 语义。只链接一个 Object root,Mesh、 +Material 和 packed Image 依赖随 source library 保留;四个 ID 的 `library` 均为 +`m12_link_source.blend`、`isLibraryOverride=false`,并映射为 `SOURCE_LIBRARY/readOnly=true`。 +target 保存并重开后 stable mapping、依赖边、geometry/UV/material slot 和 packed image Float32 +pixel hash 不变。该 enabling evidence 仍不开放 linked writer 或声明完整 Link parity。 + +M12-03G 已为 linked Object/Mesh/Material/Image writer 建立统一 fail-closed gate。object transform、 +mesh geometry/material slot、material property/image node 和 packed image data 六类操作在 +`SOURCE_LIBRARY/readOnly=true` context 下全部返回 `LINKED_DATA_MUTATION_BLOCKED`,且 +`recoverable=false`;stale revision 先返回 `REVISION_CONFLICT`,未知字段、operation 和 owner +substitution 也在 Main 前阻断。M12-03H 又将 reload 限定为匹配 source library generation/revision 的 +单一 linked snapshot 替换;stale、跳代、重复 identity 和 source substitution 均不发布新状态。该项仍不 +实现 relocate、override 或 LINK Main transaction;M12-03I 的 missing-library placeholder 仍是有界 +reference-preservation 证据,不是完整资源恢复。 + +M12-04A-J 又冻结 declared source-origin admission、canonical project path、path/origin safety、 +symlink/hardlink resolution、metadata-first read、entry/total/path/range/conflict budget 与 cancellation +rollback。取消、quota 和 OOM 的部分 staging 都会清零并保持 committed revision/SHA-256 不变, +同一 storage instance 可继续提交小 archive;6 个恶意 ZIP/TAR fixture 又进入确定性长期回归。 +该组有界证据仍不实现完整 archive decoder、fuzz 或 N-023 全域 parity。 ## 已验证切片 @@ -101,8 +129,30 @@ desktop canonical report 的联合一致性仍由 M12-03E 验证。 缺许可证和哈希不匹配会拒绝。 2. N-023-A/B(部分):catalog 与 library dependency 图做确定性拓扑检查并返回 load order; content-addressed index 报告 `LOCAL_BOUNDED`,OPFS 只在运行时 API 存在时报告 `PROBE_REQUIRED`。 -3. N-023-C(门):现有 GLB 导出与 USD semantic analysis 可放行;GLTF/OBJ/PLY/STL、 - USD/Alembic 实际导入保持 `IO_FORMAT_UNSUPPORTED`,库 mutation 需要真实 Main。 +3. N-023-C(门):现有 GLB 导出与 USD semantic analysis 可放行;M12-05A 已从 pinned Blender + 5.2 runtime 生成 GLTF/GLB/OBJ/STL/PLY/USD/Alembic operator/build inventory,但没有把清单 + 当作执行 receipt;M12-05B 仅放行已有 bounded GLB local export,其余 import/export local/server + route 均保持 `BLOCKED`;M12-05C 又让 UI registry 只消费 matrix 声明且 runtime 可执行的 route, + 文件选择器仅接受 `.blend`,operator search 隐藏 blocked/undeclared 组合;M12-05D 再用 + pinned Blender 5.2 runtime receipt 在执行前确认 operator registered/build option/RNA identity, + M12-05E 又要求 source/settings/runtime 三重 hash 与 parent inventory identity,且不从扩展名推断能力; + M12-06A 已由同一 pinned runtime 生成五个独立 Mesh/PBR/UV/skin/animation GLB fixture,并用 + semantic report 与逐字节二次生成固定输入基线;M12-06B 又由生产 Web parser 在 Node 与 + Chromium Worker 对 topology/attributes/materials/nodes/animations 做 exact canonical 比较; + M12-06C-G 又完成 Main persistence、loss report、desktop re-import、negative budget 和双向取消/ + Worker restart/真实 OPFS quota 恢复;M12-07A 又冻结 pinned Blender OBJ 单 Mesh position/ + normal/UV/material-group 输入;M12-07B 又冻结 OBJ 双对象/负索引/相对纹理来源/坏 face 负例; + M12-07C 已完成 bounded Web-to-desktop OBJ round-trip 与 texture-origin loss report,但 + UV/skin mismatch、GLB import UI route 和 OBJ Web route 仍保持阻断;M12-07D 已冻结 Blender + STL binary/ASCII 两个独立 runtime variant;M12-07E 又对标 normal/unit/degenerate/trailing, + trailing 保持 stricter Web block;M12-07F 又完成 STL Web-to-desktop round-trip/material loss + report,STL Web route 仍保持阻断;M12-07G 又冻结 PLY ASCII 与 binary little-endian 两个 + 独立 desktop capability variant;M12-07H 又完成 bounded PLY vertex/face/color/custom property + mapping 与 `PLY_UNKNOWN_PROPERTY` loss report;M12-07I 又让 big-endian、坏 list、超大 count + 在 Worker 中稳定 fail-closed;M12-07J 又完成 OBJ/STL/PLY cancellation/OOM/restart/small recovery, + 但 PLY Web route 仍保持阻断; + GLTF/OBJ/PLY/STL、USD/Alembic 实际导入仍保持 `IO_FORMAT_UNSUPPORTED`, + 库 mutation 需要真实 Main。 4. N-023-A/B(部分):预览字节先验 SHA-256 和 byte length,再校验 PNG signature/尺寸; WebP 仅在 RIFF/WEBP 容器签名正确时进入后续解码门,不从元数据伪造预览内容。 5. N-023-E:项目路径、外部 URI、archive entry 数量/单项/总量、压缩展开比率均有边界。 @@ -110,9 +160,11 @@ desktop canonical report 的联合一致性仍由 M12-03E 验证。 packed/external 状态、只读标志和 archive-parent dependency;1024 library/每库 1024 dependency、 重复 ID、缺依赖、依赖环与项目外路径由 SceneIR 再校验。缺外部库字节时返回 `LINKED_LIBRARY_RESOURCE_REQUIRED`,不从路径伪造 SHA-256 或声称已加载。 -7. N-023-E(archive 结构门):除 traversal 与展开比预算外,重复规范路径、文件/目录前缀冲突、 - 累计压缩/解压字节和声明源长度不一致均拒绝;`planIOArchiveRanges` 按路径生成确定性、安全整数 - compressed offset 计划。它为后续流式解包提供边界,不代表已有 ZIP decoder。 +7. N-023-E(archive 结构门):symlink/hardlink 在写前解析到临时根;ZIP central directory/TAR + manifest 必须先读;entry/total/path、展开比、重叠 range、重复路径与文件/目录前缀冲突均拒绝。 + 取消会删除真实 staging、保持 committed identity 且发布数为零;quota/OOM 部分写入也会清零, + 并允许同一 storage instance 恢复小 archive。三类 ZIP 与三类 TAR 恶意二进制 fixture 确定性 + 重建、验证真实容器 metadata 后全部 fail-closed;这些门不代表已有 ZIP/TAR decoder。 8. N-023-E(NanoVDB range 基础件):`.nvdb` manifest 要求连续、32-byte 对齐、逐块 hash; HTTP source 只接受与 manifest 精确一致的 `206 Content-Range`,并按块校验后消费;临时错误重试、 稳定 ETag/If-Range、response-body 偏移续传、错位/短响应拒绝均已完成。OPFS 原子 @@ -125,11 +177,16 @@ desktop canonical report 的联合一致性仍由 M12-03E 验证。 ## 仍然阻断 -- N-023-B:Append undo/redo/save/reopen、Link/Library Override、reload/relocate 和完整真实 Main - parity;M12-03D 的单 transaction append 已有独立证据,但不解除本项阻断。 +- N-023-B:linked relocate、missing-library recovery、Library Override 和完整真实 Main parity; + M12-03D/E 已证明 bounded Append 的单 transaction 与 desktop canonical persistence,M12-03F/G/H/I + 已冻结 desktop LINK source-library/read-only contract、linked writer fail-closed 与 + matching-generation reload、missing-reference preservation 和 desktop override ownership/property + fixture、单一 verified override writer、freshness gate、negative cases、三 lane command matrix 与 + declared source-origin admission、canonical project-path normalization 与 path/origin security gate, + 但不解除本项阻断。 - N-023-C/D:GLTF/OBJ/PLY/STL、USD/Alembic import/export/save/reopen/desktop reimport。 -- N-023-E:真实 zip decoder/fuzz、OPFS quota/recovery、license/source offer 发布审计和大文件 - 流式性能;archive 路径冲突、双向字节预算与确定性 range plan 已完成。 +- N-023-E:真实 ZIP/TAR decoder/fuzz、license/source offer 发布审计和大文件流式性能仍阻断; + archive link/metadata/budget/conflict/cancellation/quota/OOM 与恶意 fixture regression 已完成。 - N-023-E(VDB):GPU page resident LRU 已完成;64 MiB–1 GiB bundle 中断/内存性能门仍阻断。 ## 验收 @@ -154,6 +211,13 @@ npm --prefix web run test:asset-preview-display npm --prefix web run test:library-operation-inventory npm --prefix web run test:library-operation-identity npm --prefix web run test:library-main-append +npm --prefix web run test:library-link-safety +npm --prefix web run test:library-metadata-first +npm --prefix web run test:library-archive-budget +npm --prefix web run test:library-archive-conflicts +npm --prefix web run test:library-archive-cancellation +node --test web/tests/unit/library-archive-recovery.test.mjs +node tools/web/check-malicious-archive-fixtures.mjs npm --prefix web run test:vdb npm --prefix web run test:vdb-native ``` diff --git a/docs/status/N-025.md b/docs/status/N-025.md index 909f833d..dc31d5e8 100644 --- a/docs/status/N-025.md +++ b/docs/status/N-025.md @@ -25,13 +25,61 @@ 8. N-025-E(审计链):至多 65,536 条拒绝审计按 sequence、前项 SHA-256 与 canonical entry SHA-256 串联;读取时重新校验请求摘要和整条链,拒绝 requestId 重放、非递增 UTC 时间戳、内容篡改、断链与超预算日志。该链是内存/序列化协议,不声称已持久化发布审计。 +9. M13-02A manifest gate:每个 script 必须声明 `sourceByteLength` 和 `module=false`;解析器限制 + 脚本数量、总源码字节、依赖数量、权限数量与 allowlist,canonicalize 项目内 entry/dependency + path,并拒绝重复依赖、模块执行、未知权限和所有预算溢出。Node unit 与生产 Chromium Worker + 正负例、golden report 和 artifact manifest 已绑定。 +10. M13-02B canonical signature input:`canonicalizeScriptingManifest` 与 + `serializeScriptingManifest` 是唯一规范入口,locale-independent code-unit 排序所有可重排 + 数组,固定无空白 canonical JSON,unknown fields 不进入签名输入;order mutation、security + field mutation 和 schema drift 均由 Node/Chromium 与 golden report 验证。 +11. M13-02C signer trust policy:版本化 trust policy 绑定 ED25519 key/public-key、publisher、 + active/revoked status、validity window、revocation timestamp、same-publisher rotation chain + 和 bounded clock skew;resolver 只返回 `ELIGIBLE/cryptographicVerification=REQUIRED`,不验签 + 不执行,revoked/expired/mismatch/cycle/missing predecessor 全部 fail-closed。 +12. M13-02D declaration-bound signature verification:canonical per-script input 包含 source + SHA-256 和权限/预算/路径等声明,合法 ED25519 signature 才返回 `VERIFIED`;source hash 或 + signature 变化返回 `SCRIPT_SIGNATURE_INVALID`,revoked signer 返回 `SCRIPT_POLICY_DENIED`。 +13. M13-02E permission minimization:无显式请求时授予空集;只有 manifest 已声明且请求的 + permission 才能授予,未知、重复或未声明请求稳定返回 `SCRIPT_POLICY_DENIED`。 +14. M13-02F signature negative matrix:missing/expired/not-yet-valid key、publisher confusion + 与跨脚本 signature swap 均稳定返回 `SCRIPT_POLICY_DENIED` 或 `SCRIPT_SIGNATURE_INVALID`, + canonical ordering 不改变自身签名结果,且不开放执行入口。 +15. M13-03A sandbox scope contract:versioned scope 对 DOM、host Worker、OPFS、IndexedDB 和 + network 五项能力逐项要求 `false`;启用、缺失或未知 schema 均 fail-closed,执行仍禁用。 +16. M13-03B sandbox budget contract:CPU、wall-time、memory、message 和 output bytes 各有固定 + 上限;任一超限返回 `SCRIPT_BUDGET_EXCEEDED`,不创建或启用脚本执行器。 +17. M13-03C host-call contract:仅允许五个显式调用名;每个调用使用结构化、调用专属参数, + permission 必须来自 manifest 声明,未知字段、危险路径和未声明调用 fail-closed,解析结果 + 保持 `execution=DISABLED`。 +18. M13-03D sandbox isolation:真实 Chromium Worker crash/timeout 后 job receipt 分别返回 + `SCRIPT_SANDBOX_CRASHED`/`SCRIPT_SANDBOX_TIMEOUT`,Main revision 不变,temporary/published + 资源归零;终止后的迟到结果返回 `SCRIPT_SANDBOX_LATE_RESULT`,执行仍为 `DISABLED`。 +19. M13-03E cancellation gate:取消 receipt 保持 Main revision 和 execution 状态,真实 Chromium + 迟到窗口 `lateMessages=0`、`cacheWrites=0`,伪造迟到结果稳定返回 `SCRIPT_SANDBOX_LATE_RESULT`。 +20. M13-03F dispose gate:真实 Chromium Worker 释放两端 `MessagePort`、timer、AbortController、 + transferable buffer、pending request 和 cache reference;首次和重复 dispose 均返回零资源, + 且迟到 timer 消息为 0。 +21. M13-03G same-session recovery:Worker generation 4->5 后 Main revision/source hash/manifest hash + 保持不变;两条 default-deny audit entry 的 request ID、sequence 和 previous hash chain 连续, + replay/source tamper 均返回 `SCRIPT_MANIFEST_INVALID`。 +22. M13-04A server job directory gate:每个 job 获得随机 `0700` one-shot directory,request ID 不 + 进入目录名;成功/失败清理后无残留且重复清理幂等。 +23. M13-04B source/output isolation:source directory/file 为 `0555/0444`,真实写入返回 `EACCES`; + output directory 为 `0700` 且可写,清理会先恢复 source directory 权限并无残留。 +24. M13-04C server resource budget:CPU、memory、process、file、wall 和 output 六类限制有固定 + 上限;每类超限均返回 `SERVER_JOB_BUDGET_EXCEEDED`,bounded receipt 标记 `enforced=true`, + 执行仍为 `DISABLED`。 +25. M13-04D server network policy:默认 network `DENY`;显式声明的 HTTPS/loopback origin 才可 + `ALLOWED`,missing/undeclared/unsafe origin 均为 `SERVER_NETWORK_DENIED`。 ## 仍然阻断 - N-025-B/C:签名验证密钥管理、无网络 CPython/native sandbox、server Blender job 和 output hash 提交。 -- N-025-D/E:真实 GPU/native window/file watcher 适配、恶意脚本/依赖混淆/逃逸/重放、 - 审计日志持久化和发布门;本地请求级审计凭证已完成,不代表隔离执行完成。 +- N-025-D/E:真实 server isolation、真实 GPU/native + window/file watcher 适配、恶意脚本/依赖混淆/逃逸/重放、审计日志持久化和发布门;本地 + 请求级审计凭证与 crash/timeout receipt 已完成,不代表隔离执行完成。 ## 验收 diff --git a/docs/tasks/M13-04F.md b/docs/tasks/M13-04F.md new file mode 100644 index 00000000..68ee3b29 --- /dev/null +++ b/docs/tasks/M13-04F.md @@ -0,0 +1,41 @@ +# M13-04F:受限且脱敏的 Blender 输出 + +- `task`: `M13-04F` +- `parent`: `M13-04E` +- `status`: `done` + +## 目标 + +为真实 Blender server job 建立有界 stdout/stderr receipt:输出超出预算时截断,并过滤凭据和 +内部绝对路径;仍保持 `execution=DISABLED`,不发布 job 结果。 + +## 输入 + +- manifest:`tests/golden/M13-04E/manifest.json` +- 生产启动:`tools/web/server-job-startup.py` +- 工作区合同:`tools/web/server-job-isolation.mjs` +- 最小运行时:固定 Blender 5.2 `--background --factory-startup` + +## 范围 + +- 做:固定 stdout/stderr 字节预算、截断标记、凭据/内部路径脱敏和稳定 receipt 字段。 +- 不做:取消进程、进程树清理、超时/OOM 错误映射、OPFS 结果提交;这些分别属于 G-J。 + +## 验收 + +```text +npm --prefix web run test:server-job-output-redaction +``` + +验收必须包含正常输出、超长输出、token/API key、Unix/Windows 风格内部路径和空流负例;真实 +输出不可把 source path、环境变量值或完整命令行泄露到 report。 + +## 产物和交接 + +报告、checker、unit/E2E(如适用)、package hash 和 manifest 写入 `tests/golden/M13-04F/`; +成功后 manifest 的 `nextTask` 必须为 `M13-04G`。 + +## 回滚 + +移除输出 receipt/脱敏实现、专项测试、checker、package 命令、报告、manifest 和状态页;将 +`M13-04E` 恢复为队列尾。 diff --git a/docs/tasks/M13-04G.md b/docs/tasks/M13-04G.md new file mode 100644 index 00000000..fc2b7f7e --- /dev/null +++ b/docs/tasks/M13-04G.md @@ -0,0 +1,37 @@ +# M13-04G:取消 Blender 进程树 + +- `task`: `M13-04G` +- `parent`: `M13-04F` +- `status`: `done` + +## 目标 + +取消真实 Blender server job 时终止整个 process tree,等待子进程退出,并清理一次性 job 目录; +不得留下孤儿进程或临时文件。 + +## 输入 + +- manifest:`tests/golden/M13-04F/manifest.json` +- process receipt:M13-04F 输出 receipt +- job 目录:`tools/web/server-job-isolation.mjs` + +## 范围 + +- 做:Abort/cancel 到 process-group/tree 的映射、幂等等待、目录清理和 orphan 计数。 +- 不做:timeout/OOM/signal 分类和结果 hash 校验;分别属于 H/I。 + +## 验收 + +```text +npm --prefix web run test:server-job-cancellation +``` + +必须有真实长运行 fixture、取消竞态、重复取消、子进程和目录残留负例。 + +## 产物和交接 + +成功后生成 `tests/golden/M13-04G/` 证据,manifest 的 `nextTask` 为 `M13-04H`。 + +## 回滚 + +删除取消控制器、专项测试、checker、报告、manifest、package 命令和状态页,恢复 M13-04F 队列尾。 diff --git a/docs/tasks/M13-04H.md b/docs/tasks/M13-04H.md new file mode 100644 index 00000000..29a1eb21 --- /dev/null +++ b/docs/tasks/M13-04H.md @@ -0,0 +1,37 @@ +# M13-04H:稳定化进程故障码 + +- `task`: `M13-04H` +- `parent`: `M13-04G` +- `status`: `done` + +## 目标 + +把真实 Blender job 的 timeout、OOM、非零退出和 signal 结果转换为稳定、可诊断且不泄露内部 +细节的错误码;旧项目 revision 不得被错误结果覆盖。 + +## 输入 + +- manifest:`tests/golden/M13-04G/manifest.json` +- cancel/cleanup receipt:M13-04G +- resource limits:`tools/web/server-job-resource-budget.mjs` + +## 范围 + +- 做:故障分类优先级、退出状态映射、旧 revision 保持和统一错误 receipt。 +- 不做:成功结果进入 OPFS、重试幂等;分别属于 I/J。 + +## 验收 + +```text +npm --prefix web run test:server-job-fault-codes +``` + +覆盖 wall timeout、memory/OOM、SIGTERM/SIGKILL、普通非零退出和无法识别状态,并验证清理完成。 + +## 产物和交接 + +成功后生成 `tests/golden/M13-04H/` 证据,manifest 的 `nextTask` 为 `M13-04I`。 + +## 回滚 + +删除故障映射、测试、checker、报告、manifest、package 命令和状态页,恢复 M13-04G 队列尾。 diff --git a/docs/tasks/M13-04I.md b/docs/tasks/M13-04I.md new file mode 100644 index 00000000..da06dd26 --- /dev/null +++ b/docs/tasks/M13-04I.md @@ -0,0 +1,37 @@ +# M13-04I:校验并提交 server 结果 + +- `task`: `M13-04I` +- `parent`: `M13-04H` +- `status`: `done` + +## 目标 + +仅当 source、settings、Blender build 和 output hash 全部匹配请求时,才把 server job 结果绑定到 +OPFS;任何篡改、过期或重放都 fail-closed。 + +## 输入 + +- manifest:`tests/golden/M13-04H/manifest.json` +- source/output receipt:M13-04H +- OPFS 提交合同:项目现有原子保存协议 + +## 范围 + +- 做:四项 hash 绑定、结果版本检查、临时文件到正式对象的原子提交和失败清理。 +- 不做:同一 request 的重复提交策略;交给 J。 + +## 验收 + +```text +npm --prefix web run test:server-job-result-binding +``` + +覆盖 source/settings/build/output 任一 hash 变化、旧 revision、部分输出和 OPFS quota 失败。 + +## 产物和交接 + +成功后生成 `tests/golden/M13-04I/` 证据,manifest 的 `nextTask` 为 `M13-04J`。 + +## 回滚 + +删除结果绑定实现、测试、checker、报告、manifest、package 命令和状态页,恢复 M13-04H 队列尾。 diff --git a/docs/tasks/M13-04J.md b/docs/tasks/M13-04J.md new file mode 100644 index 00000000..bd6ce05f --- /dev/null +++ b/docs/tasks/M13-04J.md @@ -0,0 +1,37 @@ +# M13-04J:server request 幂等重试 + +- `task`: `M13-04J` +- `parent`: `M13-04I` +- `status`: `done` + +## 目标 + +同一 request 重试只能复用或确认同一个已验证结果,不得把两个冲突结果绑定到同一项目;不同 +request 必须保持隔离。 + +## 输入 + +- manifest:`tests/golden/M13-04I/manifest.json` +- verified result:M13-04I +- request/result identity:M13-04A 至 M13-04I 的 receipts + +## 范围 + +- 做:request identity、重试窗口、重复提交、冲突输出和并发绑定策略。 +- 不做:新增 Blender 能力或放宽脚本执行策略;M13 全程仍需显式安全门。 + +## 验收 + +```text +npm --prefix web run test:server-job-idempotency +``` + +覆盖成功重试、失败后重试、并发重复、同 request 冲突 output 和跨项目 request 误用。 + +## 产物和交接 + +成功后生成 `tests/golden/M13-04J/` 证据,manifest 的 `nextTask` 为 `M13-05A`。 + +## 回滚 + +删除幂等键/绑定策略、测试、checker、报告、manifest、package 命令和状态页,恢复 M13-04I 队列尾。 diff --git a/docs/tasks/M13-05A.md b/docs/tasks/M13-05A.md new file mode 100644 index 00000000..e9247556 --- /dev/null +++ b/docs/tasks/M13-05A.md @@ -0,0 +1,39 @@ +# M13-05A:CSP 默认拒绝策略 + +- `task`: `M13-05A` +- `parent`: `M13-04J` +- `status`: `done` + +## 目标 + +为部署服务器和生产构建统一声明 CSP,拒绝 inline script、eval、data script 和未声明 origin; +所有响应(包括 404)都必须携带相同策略。 + +## 输入 + +- manifest:`tests/golden/M13-04J/manifest.json` +- 部署合同:`docs/web/deployment-contract.json` +- 入口与构建:`web/app/index.html`、`web/app/vite.config.ts` + +## 范围 + +- 做:`default-src/script-src/worker-src/connect-src` same-origin 约束、`object-src/base-uri/frame-ancestors` + 拒绝规则、源代码/构建产物扫描和 HTTP 响应检查。 +- 不做:按资源类型拆分 Worker/WASM/font/image/media 策略;交给 M13-05B。 + +## 验收 + +```text +npm --prefix web run test:csp-policy +node tools/web/check-deployment-contract.mjs +npm --prefix web run typecheck +npm --prefix web run build +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05A/`;下一任务由 manifest 的 `nextTask` 指向 `M13-05B`。 + +## 回滚 + +恢复部署合同、HTTP 头、checker、报告、manifest、package 命令和状态页;将 `M13-04J` 恢复为队列尾。 diff --git a/docs/tasks/M13-05B.md b/docs/tasks/M13-05B.md new file mode 100644 index 00000000..5664d1c9 --- /dev/null +++ b/docs/tasks/M13-05B.md @@ -0,0 +1,28 @@ +# M13-05B:资源类型 CSP 矩阵 + +- `task`: `M13-05B` +- `parent`: `M13-05A` +- `status`: `done` + +## 目标 + +分别验证 Worker、WASM、font、image、media 资源只能从同源加载;WASM 使用显式 +`wasm-unsafe-eval` 编译能力,JavaScript `eval` 仍被拒绝。`data:`、`blob:` 和跨源连接必须被 +浏览器 CSP 阻断。 + +## 验收 + +```text +npm --prefix web run test:csp-resource-policy +node tools/web/check-deployment-contract.mjs +npm --prefix web run typecheck +npm --prefix web run build +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05B/`;下一任务由 manifest 的 `nextTask` 指向 `M13-05C`。 + +## 回滚 + +恢复资源 CSP、MIME 合同、checker、报告、manifest、package 命令和状态页;将 `M13-05A` 恢复为队列尾。 diff --git a/docs/tasks/M13-05C.md b/docs/tasks/M13-05C.md new file mode 100644 index 00000000..5dd17e1f --- /dev/null +++ b/docs/tasks/M13-05C.md @@ -0,0 +1,27 @@ +# M13-05C:依赖分类 inventory + +- `task`: `M13-05C` +- `parent`: `M13-05B` +- `status`: `done` + +## 目标 + +从 `web/package.json` 和 `web/package-lock.json` 的真实 npm 依赖闭包分别生成 production、build +和 test inventory。每个包绑定 package path、版本、resolved、integrity 和所属类别;共享包显式 +记录,不允许未分类或依赖树漂移。 + +## 验收 + +```text +npm --prefix web run test:dependency-inventory +npm --prefix web run typecheck +npm --prefix web run build +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05C/`;下一任务由 manifest 的 `nextTask` 指向 `M13-05D`。 + +## 回滚 + +移除 inventory 生成器、checker、报告、manifest、package 命令和状态页;将 `M13-05B` 恢复为队列尾。 diff --git a/docs/tasks/M13-05D.md b/docs/tasks/M13-05D.md new file mode 100644 index 00000000..2366b244 --- /dev/null +++ b/docs/tasks/M13-05D.md @@ -0,0 +1,26 @@ +# M13-05D:依赖严重性门和例外 + +- `task`: `M13-05D` +- `parent`: `M13-05C` +- `status`: `done` + +## 目标 + +冻结依赖 severity 门和统一例外合同。`BLOCKER/HIGH` 默认阻断,`MEDIUM` 进入 review,`LOW` +进入 tracking;任何 finding 的例外必须有 owner、期限、理由和替代控制,过期或字段缺失稳定拒绝。 + +## 验收 + +```text +npm --prefix web run test:dependency-severity-policy +npm --prefix web run typecheck +npm --prefix web run build +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05D/`;下一任务由 manifest 的 `nextTask` 指向 `M13-05E`。 + +## 回滚 + +移除 severity policy、checker、报告、manifest、package 命令和状态页;将 `M13-05C` 恢复为队列尾。 diff --git a/docs/tasks/M13-05E.md b/docs/tasks/M13-05E.md new file mode 100644 index 00000000..2e1663f0 --- /dev/null +++ b/docs/tasks/M13-05E.md @@ -0,0 +1,27 @@ +# M13-05E:供应链发布绑定 + +- `task`: `M13-05E` +- `parent`: `M13-05D` +- `status`: `done` + +## 目标 + +将 SPDX 2.3 SBOM、第三方 notices、package/lockfile、source offer、binary/source archive、 +`SHA256SUMS.txt` 和当前 commit 绑定到机器可验证报告。对应源码归档必须包含构建输入且与发布包 +独立校验。 + +## 验收 + +```text +npm --prefix web run test:supply-chain-binding +npm --prefix web run test:binary-archive +npm --prefix web run test:source-archive +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05E/`;下一任务由 manifest 的 `nextTask` 指向 `M13-05F`。 + +## 回滚 + +移除供应链 checker、报告、manifest、package 命令和状态页;将 `M13-05D` 恢复为队列尾。 diff --git a/docs/tasks/M13-05F.md b/docs/tasks/M13-05F.md new file mode 100644 index 00000000..33ec7690 --- /dev/null +++ b/docs/tasks/M13-05F.md @@ -0,0 +1,26 @@ +# M13-05F:恶意输入矩阵 + +- `task`: `M13-05F` +- `parent`: `M13-05E` +- `status`: `done` + +## 目标 + +使用生产解析器和既有负例路径验证 malicious blend、image、font、media、archive、node graph、 +script manifest 和 GLB。每类必须返回稳定拒绝码;不得提取 archive、执行脚本或发布迟到结果。 + +## 验收 + +```text +npm --prefix web run test:malicious-input-matrix +npm --prefix web run typecheck +npm --prefix web run build +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05F/`;下一任务由 manifest 的 `nextTask` 指向 `M13-05G`。 + +## 回滚 + +移除矩阵 checker、报告、manifest、package 命令和状态页;将 `M13-05E` 恢复为队列尾。 diff --git a/docs/tasks/M13-05G.md b/docs/tasks/M13-05G.md new file mode 100644 index 00000000..65b98e45 --- /dev/null +++ b/docs/tasks/M13-05G.md @@ -0,0 +1,25 @@ +# M13-05G:fuzz 崩溃最小化与回归固化 + +- `task`: `M13-05G` +- `parent`: `M13-05F` +- `status`: `done` + +## 目标 + +使用固定 seed 对 blend、image、font、node 和 script manifest 生产解析路径执行确定性 fuzz +回放。崩溃必须先保存可重放的最小样本;修复后样本进入长期回归 corpus。本任务不启用脚本执行。 + +## 验收 + +```text +npm --prefix web run test:fuzz-regression +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05G/`;本轮 5 个域各 16 个用例,共 80 个用例,零崩溃、 +零最小回归样本。下一任务由 manifest 的 `nextTask` 指向 `M13-05H`。 + +## 回滚 + +移除 fuzz runner、checker、报告、manifest、package 命令和本状态页;将 `M13-05F` 恢复为队列尾。 diff --git a/docs/tasks/M13-05H.md b/docs/tasks/M13-05H.md new file mode 100644 index 00000000..e88bf4af --- /dev/null +++ b/docs/tasks/M13-05H.md @@ -0,0 +1,24 @@ +# M13-05H:审计记录完整性 + +- `task`: `M13-05H` +- `parent`: `M13-05G` +- `status`: `done` + +## 目标 + +审计记录必须按严格递增的时间和连续 sequence 写入,每个 request ID 只能出现一次;每条记录的 +digest 绑定前一条 entry hash,任何重放、时间倒退、sequence 或 hash-chain 篡改都稳定拒绝。 + +## 验收 + +```text +npm --prefix web run test:script-audit-integrity +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M13-05H/`;下一任务由 manifest 的 `nextTask` 指向 `M14-01A`。 + +## 回滚 + +移除审计完整性 checker、unit、报告、manifest、package 命令和本状态页;将 `M13-05G` 恢复为队列尾。 diff --git a/docs/tasks/M14-01A.md b/docs/tasks/M14-01A.md new file mode 100644 index 00000000..31e7e530 --- /dev/null +++ b/docs/tasks/M14-01A.md @@ -0,0 +1,24 @@ +# M14-01A:冻结 Chromium、engine 与 archive 身份 + +- `task`: `M14-01A` +- `parent`: `M13-05H` +- `status`: `done` + +## 目标 + +冻结当前 Chromium 版本、可加载的 engine manifest/variant 资源、离线 binary/source archive 和 +`SHA256SUMS.txt`。验收必须对实际文件逐项复算 SHA-256,不能只读取描述性 metadata。 + +## 验收 + +```text +npm --prefix web run test:chromium-freeze +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M14-01A/`;下一任务由 manifest 的 `nextTask` 指向 `M14-01B`。 + +## 回滚 + +移除 Chromium freeze checker、报告、manifest、package 命令和本状态页;将 `M13-05H` 恢复为队列尾。 diff --git a/docs/tasks/M14-01B.md b/docs/tasks/M14-01B.md new file mode 100644 index 00000000..714c723d --- /dev/null +++ b/docs/tasks/M14-01B.md @@ -0,0 +1,25 @@ +# M14-01B:Firefox capability probe + +- `task`: `M14-01B` +- `parent`: `M14-01A` +- `status`: `done` + +## 目标 + +在真实 Firefox 上探测 WASM、Worker、OPFS、IndexedDB、WebGL2、WebGPU、OffscreenCanvas 和 +COOP/COEP isolation。每项都记录实际浏览器、平台和 GPU/adapter 信息;缺失能力只能为 +`BLOCKED` 或 `UNAVAILABLE`,不能按 user-agent 推断支持。 + +## 验收 + +```text +npm --prefix web run test:firefox-capability +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M14-01B/`;下一任务由 manifest 的 `nextTask` 指向 `M14-01C`。 + +## 回滚 + +移除 Firefox probe、报告、manifest、package 命令和本状态页;将 `M14-01A` 恢复为队列尾。 diff --git a/docs/tasks/M14-01C.md b/docs/tasks/M14-01C.md new file mode 100644 index 00000000..00c93701 --- /dev/null +++ b/docs/tasks/M14-01C.md @@ -0,0 +1,25 @@ +# M14-01C:WebKit capability probe + +- `task`: `M14-01C` +- `parent`: `M14-01B` +- `status`: `done` + +## 目标 + +在真实 Playwright WebKit 上使用与 Chromium/Firefox 相同的生产资源和隔离头,探测 WASM、Worker、 +OPFS、IndexedDB、WebGL2、WebGPU、OffscreenCanvas 和 COOP/COEP isolation。缺失能力只能记录为 +`BLOCKED` 或 `UNAVAILABLE`。 + +## 验收 + +```text +npm --prefix web run test:webkit-capability +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M14-01C/`;下一任务由 manifest 的 `nextTask` 指向 `M14-01D`。 + +## 回滚 + +移除 WebKit probe、报告、manifest、package 命令和本状态页;将 `M14-01B` 恢复为队列尾。 diff --git a/docs/tasks/M14-01D.md b/docs/tasks/M14-01D.md new file mode 100644 index 00000000..5dd728c6 --- /dev/null +++ b/docs/tasks/M14-01D.md @@ -0,0 +1,27 @@ +# M14-01D:probe identity and GPU/OS adapter recording + +- `task`: `M14-01D` +- `parent`: `M14-01C` +- `status`: `in_progress` + +## 目标 + +在真实 Chromium 生产构建上记录浏览器、运行时 OS、WebGL renderer/vendor 和 WebGPU adapter +身份。身份必须来自实际 API 探测;缺失的 WebGPU adapter 只能记录为 `BLOCKED`,不能按 +浏览器或 user-agent 推断能力。报告同时绑定当前 production worker/WASM 资源 hash,避免把 +不同构建的 GPU 结果混在一起。 + +## 验收 + +```text +npm --prefix web run test:probe-identity +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M14-01D/`。完成后 manifest 的 `nextTask` 指向下一颗 +最小任务;在本任务通过前不更新浏览器支持声明。 + +## 回滚 + +移除 probe checker、报告、manifest、package 命令和本状态页;将 `M14-01C` 保持为队列尾。 diff --git a/docs/tasks/M14-01E.md b/docs/tasks/M14-01E.md new file mode 100644 index 00000000..4a7e0ec4 --- /dev/null +++ b/docs/tasks/M14-01E.md @@ -0,0 +1,20 @@ +# M14-01E:Chromium WebGPU fail-closed boundary + +- `task`: `M14-01E` +- `parent`: `M14-01D` +- `status`: `in_progress` + +## 目标 + +在 Chromium 中验证 WebGPU 不可用时,依赖 WebGPU 的渲染能力返回稳定 `BLOCKED`,而已验证的 +WebGL2 bounded Eevee/Main 路径仍保持 `READY`。本任务不启动 Firefox 或 WebKit。 + +## 验收 + +```text +npm --prefix web run test:chromium-webgpu-boundary +``` + +## 产物和交接 + +报告和 manifest 位于 `tests/golden/M14-01E/`;通过后从 manifest 的 `nextTask` 继续。 diff --git a/docs/tasks/M14-04A.md b/docs/tasks/M14-04A.md new file mode 100644 index 00000000..15eb79a3 --- /dev/null +++ b/docs/tasks/M14-04A.md @@ -0,0 +1,16 @@ +# M14-04A:Chromium GPU/memory budget device tier selection + +- `task`: `M14-04A` +- `parent`: `M14-01E` +- `status`: `in_progress` + +## 目标 + +在 Chromium 中根据真实探测到的 GPU/adapter、设备内存和硬件并发选择声明的预算档位;缺失 +或不可信的能力必须降级到保守档位,不能自动扩容。Firefox/WebKit 不在本项目范围内。 + +## 验收 + +```text +npm --prefix web run test:chromium-device-budget +``` diff --git a/docs/tasks/M14-04B.md b/docs/tasks/M14-04B.md new file mode 100644 index 00000000..5f11ebbe --- /dev/null +++ b/docs/tasks/M14-04B.md @@ -0,0 +1,17 @@ +# M14-04B:Chromium DPR 1/1.5/2/3 canvas and raycast consistency + +- `task`: `M14-04B` +- `parent`: `M14-04A` +- `status`: `in_progress` + +## 目标 + +在 Chromium 主线程 WebGL2 视口中验证 DPR 1、1.5、2、3 的 canvas backing dimensions、CSS +尺寸和 raycast coordinate mapping 保持稳定,不因 DPR 改变逻辑坐标或选择结果。Firefox/WebKit +不在本项目范围内。 + +## 验收 + +```text +npm --prefix web run test:chromium-dpr-consistency +``` diff --git a/docs/tasks/M14-04C.md b/docs/tasks/M14-04C.md new file mode 100644 index 00000000..ad898bd9 --- /dev/null +++ b/docs/tasks/M14-04C.md @@ -0,0 +1,17 @@ +# M14-04C:Chromium mouse/touch/pen pointer identity and cancellation contract + +- `task`: `M14-04C` +- `parent`: `M14-04B` +- `status`: `in_progress` + +## 目标 + +为 Chromium 生产输入路径建立统一 pointer observation:mouse、touch、pen 分别保留 +`pointerType`、`pointerId`、`pressure`、`tiltX`、`tiltY`、`button`、`buttons` 和 cancel 状态; +未知或越界值 fail-closed,不把触控/笔事件伪装成 mouse。Firefox/WebKit 不在本项目范围内。 + +## 验收 + +```text +npm --prefix web run test:chromium-pointer-contract +``` diff --git a/docs/tasks/M14-04D.md b/docs/tasks/M14-04D.md new file mode 100644 index 00000000..1cfd4f47 --- /dev/null +++ b/docs/tasks/M14-04D.md @@ -0,0 +1,16 @@ +# M14-04D:Chromium IME composition guard for incomplete operators + +- `task`: `M14-04D` +- `parent`: `M14-04C` +- `status`: `in_progress` + +## 目标 + +在 Chromium 的生产编辑器输入路径中,IME compositionstart/update 期间不触发未完成的 +operator;compositionend 后才允许提交最终文本。Firefox/WebKit 不在本项目范围内。 + +## 验收 + +```text +npm --prefix web run test:chromium-ime-guard +``` diff --git a/docs/tasks/M14-04E.md b/docs/tasks/M14-04E.md new file mode 100644 index 00000000..ee37be79 --- /dev/null +++ b/docs/tasks/M14-04E.md @@ -0,0 +1,17 @@ +# M14-04E:Chromium US/non-US/dead-key/modifier keymap fixture + +- `task`: `M14-04E` +- `parent`: `M14-04D` +- `status`: `in_progress` + +## 目标 + +固定 Chromium 生产路径的 key identity 解析:US 与非 US 字符、dead key、Shift/Ctrl/Alt/Meta +修饰键必须保留原始 `key`/`code`/`location`/修饰键,不把字符布局推断为另一种物理按键。 +Firefox/WebKit 不在本项目范围内。 + +## 验收 + +```text +npm --prefix web run test:chromium-keymap-fixture +``` diff --git a/docs/tasks/M14-04F.md b/docs/tasks/M14-04F.md new file mode 100644 index 00000000..c6f29239 --- /dev/null +++ b/docs/tasks/M14-04F.md @@ -0,0 +1,56 @@ +# M14-04F:Chromium touch modal cancel / two-finger navigation / pen commit + +- `task`: `M14-04F` +- `parent`: `M14-04E` +- `status`: `in_progress` + +## 目标 + +在 Chromium 输入状态层固定三条边界:触控 modal 取消不提交 Main;双指手势只产生一次 +navigation session;笔 stroke 的 pointerup 只允许一个 Main commit,重复 up/cancel 不重复提交。 +Firefox/WebKit 不在本项目范围内。 + +## 输入 + +- 上一个 manifest:`tests/golden/M14-04E/manifest.json` +- 协议入口:`web/protocol/input-modal.ts` +- 单测入口:`web/tests/unit/input-modal.test.mjs` +- Chromium 检查器:`tools/web/check-chromium-input-modal.mjs` +- 当前报告:`tests/golden/M14-04F/chromium-input-modal-report.json` + +## 细分门 + +1. **协议门**:固定 `InputModalState`、pointer ID 校验、touch/pen 状态转移和稳定计数。 +2. **触控门**:`pointercancel` 清理活动 pointer,不产生 Main commit。 +3. **双指门**:从一个 pointer 进入两个 pointer 时只增加一次 navigation session;重复 down、 + pointer 顺序和单指结束不得重复创建 session。 +4. **笔门**:同一笔 stroke 的第一个合法 `pointerup` 最多产生一次 Main commit;late up/cancel + 不得重复提交或改变 revision。 +5. **生产接线门**:主线程与 Offscreen 生产输入路径必须真正消费协议;不能只在测试中调用纯函数。 +6. **浏览器证据门**:Chromium checker 必须读取真实 modal 状态、Main revision/commit counter 或 + 等价生产诊断,而不是仅记录派发了几个 `PointerEvent`。 +7. **交接门**:focused 命令、必要的 typecheck/build、report、manifest、status 和回滚路径齐全。 + +协议单测和事件派发通过,只能关闭前四个门的一部分;在生产接线和真实状态断言完成前,任务保持 +`in_progress`,不得根据报告中的固定保证字段直接改为 `done`。 + +## 验收 + +```text +npm --prefix web run test:chromium-input-modal +``` + +必要的补充门: + +```text +npm --prefix web run typecheck +npm --prefix web run build +git diff --check +``` + +## 产物和交接 + +- 报告:`tests/golden/M14-04F/chromium-input-modal-report.json` +- manifest:`tests/golden/M14-04F/manifest.json` +- 状态页:完成或阻断后新增 `docs/status/M14-04F.md` +- 下一任务:只读取 manifest 的 `nextTask`,不能从长期计划手工推导 diff --git a/docs/web/DEPLOYMENT.md b/docs/web/DEPLOYMENT.md index 3a4cb9f6..0dc8afec 100644 --- a/docs/web/DEPLOYMENT.md +++ b/docs/web/DEPLOYMENT.md @@ -8,11 +8,16 @@ when every response, including errors and SPA fallbacks, preserves these headers | `Cross-Origin-Opener-Policy` | `same-origin` | | `Cross-Origin-Embedder-Policy` | `require-corp` | | `Cross-Origin-Resource-Policy` | `same-origin` | +| `Content-Security-Policy` | `default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'` | Production uses HTTPS. Loopback development may use `http://127.0.0.1`; `file://` is unsupported. Runtime assets are same-origin. A reverse proxy must not strip the isolation, range, cache, MIME or ETag headers. +The CSP permits WebAssembly compilation only through the explicit `wasm-unsafe-eval` source expression; +JavaScript `eval` and `new Function` remain denied. Workers, fonts, images and media are each limited to +same-origin resources, and `data:`/`blob:` URLs are not declared for any of them. + ## Empty-directory install runbook The release delivery contains `blender-web-offline.tar.gz`, diff --git a/docs/web/dependency-severity-policy.json b/docs/web/dependency-severity-policy.json new file mode 100644 index 00000000..64f6c801 --- /dev/null +++ b/docs/web/dependency-severity-policy.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-05D", + "severityOrder": ["LOW", "MEDIUM", "HIGH", "BLOCKER"], + "gates": { + "BLOCKER": "BLOCK", + "HIGH": "BLOCK", + "MEDIUM": "REVIEW", + "LOW": "TRACK" + }, + "exceptionRequiredFor": ["BLOCKER", "HIGH", "MEDIUM", "LOW"], + "exceptionFields": ["owner", "expiresOn", "reason", "alternativeControl"], + "dateFormat": "YYYY-MM-DD", + "findings": [], + "exceptions": [], + "execution": "DISABLED", + "nextTask": "M13-05E" +} diff --git a/docs/web/deployment-contract.json b/docs/web/deployment-contract.json index 1ebcd37b..cf23efb7 100644 --- a/docs/web/deployment-contract.json +++ b/docs/web/deployment-contract.json @@ -13,7 +13,8 @@ "allResponses": { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", - "Cross-Origin-Resource-Policy": "same-origin" + "Cross-Origin-Resource-Policy": "same-origin", + "Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" }, "routes": [ { @@ -50,7 +51,18 @@ ".wasm": "application/wasm", ".json": "application/json; charset=utf-8", ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".ttf": "font/ttf", + ".otf": "font/otf", + ".woff": "font/woff", + ".woff2": "font/woff2", ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".ogg": "audio/ogg", + ".mp4": "video/mp4", + ".webm": "video/webm", ".blend": "application/octet-stream", ".nvdb": "application/x-nanovdb" }, diff --git a/docs/web/sbom.spdx.json b/docs/web/sbom.spdx.json index c56c301a..a6f4d3af 100644 --- a/docs/web/sbom.spdx.json +++ b/docs/web/sbom.spdx.json @@ -3,7 +3,7 @@ "dataLicense": "CC0-1.0", "SPDXID": "SPDXRef-DOCUMENT", "name": "blender-web-editor-sbom", - "documentNamespace": "https://blender-web.local/spdx/cf7beefe71131e5d51b45ffa79b7b906b21decc743fc7eafd7cf6791996dea37", + "documentNamespace": "https://blender-web.local/spdx/dc8374f47ec1483e74b09821f6d444c9785c19ba02d6d79f617ae50d71caf246", "creationInfo": { "created": "1970-01-01T00:00:00Z", "creators": [ @@ -14,6 +14,29 @@ "SPDXRef-Package-blender-web-editor" ], "packages": [ + { + "SPDXID": "SPDXRef-npm-node-modules--axe-core-playwright-ac1d565a7235", + "name": "@axe-core/playwright", + "versionInfo": "4.12.1", + "downloadLocation": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/%40axe-core%2Fplaywright@4.12.1" + } + ], + "checksums": [ + { + "algorithm": "SHA512", + "checksumValue": "acc77bc6b8a9b6a2a93fec39dbae62e07764bf65f991bbbaba2062fc1d88eee7f786279104cdea0c27da28fb717e261bda62977c5fb22ce0c9c08c7e26fd460f" + } + ] + }, { "SPDXID": "SPDXRef-npm-node-modules--dimforge-rapier3d-compat-64c1bc64bf78", "name": "@dimforge/rapier3d-compat", @@ -1302,6 +1325,29 @@ } ] }, + { + "SPDXID": "SPDXRef-npm-node-modules-axe-core-faf33d0c794e", + "name": "axe-core", + "versionInfo": "4.12.1", + "downloadLocation": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "filesAnalyzed": false, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/axe-core@4.12.1" + } + ], + "checksums": [ + { + "algorithm": "SHA512", + "checksumValue": "b3b8867f919a54cc441b410d37dc7ec53afb1856426f564fff5b804d4a4210ad97efc9c30774706ed142a3da4601ff6bbbe570a10e3ae0391a2c4791334f3024" + } + ] + }, { "SPDXID": "SPDXRef-npm-node-modules-balanced-match-951e2b0376c0", "name": "balanced-match", @@ -3292,7 +3338,7 @@ "checksums": [ { "algorithm": "SHA256", - "checksumValue": "87af8e7d5eb36537541cf941699868a010c1fcea305daa3ce5385453e8fa4557" + "checksumValue": "61f6d13e0148321d3c625a5baa212b5444296d111193c4d62a692685faebff1f" } ] }, @@ -3375,6 +3421,11 @@ } ], "relationships": [ + { + "spdxElementId": "SPDXRef-Package-blender-web-editor", + "relationshipType": "DEPENDS_ON", + "relatedSpdxElement": "SPDXRef-npm-node-modules--axe-core-playwright-ac1d565a7235" + }, { "spdxElementId": "SPDXRef-Package-blender-web-editor", "relationshipType": "DEPENDS_ON", @@ -3655,6 +3706,11 @@ "relationshipType": "DEPENDS_ON", "relatedSpdxElement": "SPDXRef-npm-node-modules-ajv-f1c07f09b167" }, + { + "spdxElementId": "SPDXRef-Package-blender-web-editor", + "relationshipType": "DEPENDS_ON", + "relatedSpdxElement": "SPDXRef-npm-node-modules-axe-core-faf33d0c794e" + }, { "spdxElementId": "SPDXRef-Package-blender-web-editor", "relationshipType": "DEPENDS_ON", diff --git a/tests/files/web/archive-security/manifest.json b/tests/files/web/archive-security/manifest.json new file mode 100644 index 00000000..597db752 --- /dev/null +++ b/tests/files/web/archive-security/manifest.json @@ -0,0 +1,68 @@ +{ + "schemaVersion": 1, + "task": "M12-04J", + "generator": "tools/web/generate-malicious-archive-fixtures.mjs", + "extractionAllowed": false, + "cases": [ + { + "id": "ZIP_PATH_TRAVERSAL", + "format": "ZIP", + "file": "zip-path-traversal.zip", + "threat": "ARCHIVE_ROOT_ESCAPE", + "gate": "LINK_SAFETY", + "byteLength": 132, + "sha256": "284fe1612ca049ec73f083fc6550f64078e8115bf85adea5431402fb73621f43", + "expectedCode": "IO_ARCHIVE_UNSAFE" + }, + { + "id": "ZIP_COMPRESSION_BOMB", + "format": "ZIP", + "file": "zip-compression-bomb.zip", + "threat": "COMPRESSION_RATIO", + "gate": "CONFLICTS", + "byteLength": 115, + "sha256": "8394d639dfdb04e94b9a2fe06d4d37e31f3bdbe47b6a581c6dc8238d7c98a8da", + "expectedCode": "IO_ARCHIVE_UNSAFE" + }, + { + "id": "ZIP_DUPLICATE_PATH", + "format": "ZIP", + "file": "zip-duplicate-path.zip", + "threat": "DUPLICATE_PATH", + "gate": "LINK_SAFETY", + "byteLength": 212, + "sha256": "830441041ecc26c0ba327cd24e6d1e19f7f3afc78474e08659d5adb35d8236ab", + "expectedCode": "IO_ARCHIVE_UNSAFE" + }, + { + "id": "TAR_PATH_TRAVERSAL", + "format": "TAR", + "file": "tar-path-traversal.tar", + "threat": "ARCHIVE_ROOT_ESCAPE", + "gate": "LINK_SAFETY", + "byteLength": 2048, + "sha256": "d21c2ea584e7e64b3d40c1742b7ae526f5cf4da7d16d0fce33a3323806092b85", + "expectedCode": "IO_ARCHIVE_UNSAFE" + }, + { + "id": "TAR_SYMLINK_ESCAPE", + "format": "TAR", + "file": "tar-symlink-escape.tar", + "threat": "SYMLINK_ESCAPE", + "gate": "LINK_SAFETY", + "byteLength": 2048, + "sha256": "f7eeb00b0e602883f5afc7add357bda5755c33321f1d2faf470b52e9b158e6fb", + "expectedCode": "IO_ARCHIVE_UNSAFE" + }, + { + "id": "TAR_PREFIX_CONFLICT", + "format": "TAR", + "file": "tar-prefix-conflict.tar", + "threat": "FILE_DIRECTORY_PREFIX_CONFLICT", + "gate": "CONFLICTS", + "byteLength": 3072, + "sha256": "5d5914636b4e7777068897b0c51f900863706dcb2e415c1e0ebb1022b84bd5fa", + "expectedCode": "IO_ARCHIVE_UNSAFE" + } + ] +} diff --git a/tests/files/web/archive-security/tar-path-traversal.tar b/tests/files/web/archive-security/tar-path-traversal.tar new file mode 100644 index 0000000000000000000000000000000000000000..7aa6604122ea0873512a2f229a18e2d98e3ee3d9 GIT binary patch literal 2048 zcmdPX)7R6}&o3=0&P++wE2$`9pgu4#FfcPQVNd|lK!8pIh0I`LPN^F~ y#M0uD#3BX*gQEO=ynZJnk7@Fd5J@dgPAo_r5>6d;u(m;St1BHxXVo-TQ12ZF#dYZZlYItdJNn#O$fk9Dz zehEJN2+8BqJBa1-^HK+~>qebM>kvo-=YRcz#LArf#1y@x%slGb2+Y+a=6_>@(fm*S cc)(_2NqIgt;Zd>C5Eu=C(GVC7fngp30LYXo+yDRo literal 0 HcmV?d00001 diff --git a/tests/files/web/archive-security/tar-symlink-escape.tar b/tests/files/web/archive-security/tar-symlink-escape.tar new file mode 100644 index 0000000000000000000000000000000000000000..f42e85dcfd16bcadc8d64157335ef8474ac2db2b GIT binary patch literal 2048 zcmXR;OiN{;F)%PNFgG=2Pyo_EfKDTeLD_}|M&>3A3Z^u67ufjH;*!K71_Ohl{QMGp z_7RfDr*{y`6$9g6KPNLUn}%)##;TbKFg`)COI-XL8=06fC>ZJK=>t)IX-RQr3awJ< U2#sn~_O` n8Mm2Wi-5$EMi2{T1G+AB3lO?AQFO7gfn=C~Fb7BnfjA5Rcy%69 literal 0 HcmV?d00001 diff --git a/tests/files/web/archive-security/zip-path-traversal.zip b/tests/files/web/archive-security/zip-path-traversal.zip new file mode 100644 index 0000000000000000000000000000000000000000..27627edb4de25b74a690ebda22c170e4f1c0fb84 GIT binary patch literal 132 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%0D}Ea%eXvN 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 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_desktop_v1/mesh.glb b/tests/files/web/m12_glb_desktop_v1/mesh.glb new file mode 100644 index 0000000000000000000000000000000000000000..7010712840cf6e775578bfa140b255ea0da93e82 GIT binary patch literal 1236 zcmbtUQEr<+5T!|v(GN7=#APv9g`di?YRRo_D%;9W6jd13cqP1REsL87S?WQ0mtLX= z>g+;nq(n*8A^|(Q^JeC~8OV#(_`@MS9n z7p0ve#J(;lH~?_>KA^t@g_1P2aOmP~MT_IfTH9Vk-_0Fm3SN{DW2;K>g6U}?@A z;3A{?z`Z-(ovj*nwA>nK>pvr&J;ObF%ww z-oSxWR2+xE*TL?wKg4kzq9te8;M*&0UTIqy4BUJg^|9qObYo+Quf%={?fn-TRd|}) zY#Lt3u+6<34X49}8K-mYU7d_Z>jy=rl;;YEQV)Q(KF|uv@C;lt0Cv$xM_=OMs{@C14 f)_8vM>G&E@i+gFU<94s->~8Kimi~`jSeMox^}}Dv literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_desktop_v1/pbr.glb b/tests/files/web/m12_glb_desktop_v1/pbr.glb new file mode 100644 index 0000000000000000000000000000000000000000..b72365e0923467a8f5ade1bead61eac3a4a6f07d GIT binary patch literal 1208 zcma)6%Wm5+5LDBCh_GkHC8;NUDV(4I+{Sib2e||}$TIB^kfng4(gcQqd{%G$wJu3H z77)O#07-o8&d%&ExhZe2&OFci{l)VLzCRYT=??Oy5gLX&*oaD~T-OSQ@bgjCRo(a- zbn!1rQLTjX$6sgub39;!kV62^LN&6k(3lMf>;q^D1b`vwMhy(_cTnY9T!V$(3!ti3 z!fcR-{ljs#gRN*Dt!rmLq0GOznEO}qMYl?TvEJrdD4CZIcvZK{QY_?3tYD~BD*_N( z*);N5T;+vTaZd(>arcCbM^TzaNq~#7Tg#F-%Xl1dT%**jr6EmIPB=I89QOm4Nf#fr zKP}Zn=)5dtF|XUrVGIY7|WtGikRWVofS(M zOGp%^0X2%d7zZ>mNSVXR5=t23aRBNN|7gTA7ZEiv!WrM&>|us&>fdW}(ADMGpO_k; zHYK-GV;WtabFGzJw%Sa0a5GzsZ^yH#t<`ikpNy_yNcREAY9$L}!;XbQqrI(jUf@jQV+FGFlWEI<#N8km#Vr@T?eNAK{R8FK8r7nh^!(cE_EF?wIARk?iC;#yQ2jcp!f z0hsx0y|zaWPmIph#+<@1;t4wn`KPOa`d?p)lKNq}(a?(--!Jk0 lxEN3I`}PMv=KsyTAI7-R;d60 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_desktop_v1/skin.glb b/tests/files/web/m12_glb_desktop_v1/skin.glb new file mode 100644 index 0000000000000000000000000000000000000000..4e17d84d29d89865a7327f20e2d0b1f3960303fe GIT binary patch literal 1912 zcma)7>u%aG6wW$ndyUAyQ<-azrBg_ckM=qjM>b{g3(yz#z zB{wF0w$Sl>d+?JB`-0!;rDDLGz!X9rkok^R(B-J6nui94JB1-rJdD$#CUQBB*?>>j zgfz5TvV1c+x|0DQ>0+!pOowsIqrP0uZUjq!W<|zf!p>wY)kPQ?)kkirO6$hlawYI- zrRKOLKS*8Rp|s}JtKjo$uCb~|eM+lA)l@LgvTXU>l1tM#w_2tyt+^G4sO=VtD7rQ! zf_$boc{lQ6G6C%lDhvfFKIfX>X3?S6ijS8Xrz`TcH)su8y{_p*x7Y79+oa(+|((oPf?Y#Brb27sLSqOX zl*3l+H8PAKQ6g2!I9^VtOkHvI%WOB2^Mw>l=;8eWPA&C(5G;~Q0r$gf?Ji9M9iVt9 z@Gy;zA**wodbGijtMjwK9LZgN(BK*Vr_dnUA=*%GcUPdx2f2g)q4#Qg3Js+n<~Ecj zH}GWW!CAB2>>p%@79HY?>`+>U?=*+7cTl#_ZMRRRN8@|V+Dyze-1Tw|LSf#UPR%zA zK0P{AGyI$d<$A@(d!AA~3q`7zVm;XVZrVC3@WQ+m5OWvnd074n!5F&-u0F^Nm+8@p zJ^z7PUaeMWo$0(IZs1km0=|D`S*NWo{A+A}!wc;M#_$;19m9Ga17oy2?)fp~fuDKO z*GJ_3fGlW#|Flowd30`B`|-$x|8+;!Yj~S|Y+!)4NuNDm=Ffc?gSJVrxj*x5{e}yD OXqzIIZC#k1_ujnU@4Xqrs$@G5LT|bd z`rV1p&2q8O#G2<3A1h645Qk9BcPUozt!3&ut|u5^B1~AssSzsVM~cGAwPATUnZ^mc zLa1lD4k*jRB3@5mPX`BB5hKdO%3>2cnhi2s=4T1)xHZBK#HID6eXfaZ;w^L2NMDs? z;m*8}HCO$B63mos%_r2A9yicbRq-oj9ksT!$6rj069iLY6fsh8Zq zSayg9PMWx?d1TVHTv{Pv7?7Dz=_+Ib_1KY3R?|5sP#E1aZ^?#JsWkh>$HlRk9A%Q2`gy~uwV(bv({VB%I_8}zR&Q-I|d-$z-?|6wg3iVd(I zj?IO3XN=!ET#B-MuQt-1+$tpEqD#E zk{)B;VNhCv^mvL*VTf;pZ9AZFL6_DA;du<9iF^TG!;O#dWBZM89irj9rPHR%jUu~~ z(0l&b-u}f9dy_j*96g;Y%=Gu3?*+C$pPMRiF&uTndyVLJ{}tF`UTJ0`KK5BH1Jh|_ z)C@xJp7rf&r+^pqTuDXf;Z?}^2|_y{+W3J`AR_ehK0?Y1ga#U~-rv3s2cu*mUsqK$ z2=qBvIaq*6w0FeLBj%Awx?>kw^cmpM=IXBxu=TjvtjBo%eH+~V0z^+EcNc_xXW5E< z{r+edgai4hOr`sxzPYt?y0i4>ve7fW^2l?s3)Z2;skAeIP8iU)@ literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_main_v1/animation.blend b/tests/files/web/m12_glb_main_v1/animation.blend new file mode 100644 index 0000000000000000000000000000000000000000..0ab2beeb94c325caf4a36b80a3bccc7945d8e7fd GIT binary patch literal 91107 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 FkC>2CR&dhV$t9dtfreE zRuDWo%RN6$ohIk$Hu~06BAnECCL?8p(}_MrvLO}?KAwC;8)%|0u9?9teM{ERu9dBQ zIbB-E&pDQ-Q_vh72L;LaoK$=*@QXTIxt=Dt4Kt3jC-(aejo)P>E`nkrVR~x~MmL$N zG`QL*P0?C_;i?~(YOn2Ei@}I&vd+92inR0XAyjUSLN3m zb{A*`V=Ir8IWsU0DPxqODFmB58CEH(+UxaEgYTGg=hC$3{+s`uDR~f|UL~0u+B-r{ zit`)VbL9GoZ_bArkVvLF`pf9s5;!ui9ds1jXPWY}htL7m|9QJ#mKQ%!mv{|e}Yu;{;MPF~;e;%py z?lHZyE2fCzONEGg8F7?K3Yo|Zh@2)+&^BUZla7?I2&dZIU*vEhmL_y)Rh|SB((3#|lOcou7)iH^&C-LAH|E2I z8VoNzC%GqTQvt&*&5m>tI8-*jsyNr5aQM+t12Xm;%6DCC^45VQv?~8Z*q0i4@-KRy zD!@i1!RAAot6b8i0Ff+=!bZ}RC6cQNP@$o8oDxRaS$z)P`H2p`@>=Uy0^wLon3^9D z`6KvcPoR^>Z$Ke_ZE!F)N!<$2l?JhMpNSb80IL-^#@@8m4|d4k(uB>Zu4eg1C=F>y zl0OJ^M1^+yr`JyY#2KK;_jwc3Xdse6AaLtU=t3(`lRze-wLiRDIwc6o0*3P>XVyRv zWJ0};y2W7|)JKi{lu$jD*tTLcgalgXa%F?9tg%Zvte|Ny%E4h}fqu)P6)>JGpMAdN zvP(mWu(Z?zBcr01_r7wt8APU5RH;OzsjR~$P=uR@cUrE8ey9%cV?4}b`ykXz?>~lV z7-mvxDjVs|4cEJKXp#tGu;27^{Z35UjVGx1 zOk#zZS9uof>gGbve~J7Z7M3A^oT6nom==FTkzw`np9EzsmcCYS15&|Ma6f;81gO;` zxjN1iGAGOYaotU=k8FcSLhj0qz&ob2)=ysPJH#qB_+!SXMALBMa!V2>)*RV9XwY2B zAnl%^AnwaK^#rd5bZ1x9p!-kxhk`-V`qnJfQQ3)O%x}p;XU1@ubVST6qjvO48BscG zH1-nE{RE{|;zP-Tk#|))Q%;EZB z^|>mKIjxd2d0;eigbIi+pS519z>a9&YAiuh57Nm}w5vd^E}7)O(dcv7{Z64<7iJ3D z=p%>pulP+STXi#)LuzxypJ?s4{FQv(cHoP)j*0ujny>*z-DEsmXKbXv<1xzG5l?R< z2u=!oV;M+=Y6IB7ph2QGj8_{*LtEDbZV(xzA=lA^Mnv`{F8F#oFsR$yqa?FD{Oj0z ze}l^0kT)RAs32i9g>v(B%h+&}*A{h#cL7>3RybQ12FY3=55AZJ{WxF-^0i_DMOC~f zvFP5=H*@KzsT&Fwq3WSu!~{wTNVf=usi@q~oS2aP1Sq+EJj9Tw$D@+CTt+tyv5nr{ zG9b^;kY1p`w8LtS=$wSfy>b<^{Yn7eb0$Jcx$J(5*xQfPzS(4_$dUpTCY!_xC7Hw7 zESDOrE5GhaV_V!;zRdf%~kSHEs%q9jakk3;Eye@$Eq1f zmDxFvt|B_v@=wV&QerJq<}LN}7pq}jf&Cw)u_tv>kG(-u*#T=k(#;#HQuHY0hCw3# z(Y7jx8GIX&5{js#sl#Y`8U%Ho(}CG#O?#$|cdP?R$#=j|(t_U~`Bc)+D#`5hN>&}6 z9g1L`QH?()LY7W4mbw&5%5-*j6f&UN!7LvWjZ&YgsT1#Q;_J6Y+v=!Q@L2+wJy3x& zAL}DfW79#fPR!J+M%J@(50qD*tf{e)Y5La{w9<9Sfab?a&0$QDEC}?eT9~T8Qgl4_ z+d6`rHu>tLKU@SEFLP7)@G4V5ZWF8IzjyNj`4zuvDkUg4m!yc}$9-N*zorTHW&~kjcN0^BkF0 zU|AcA&V>InrP-7ioFa?`JwmuJ_JHiLNNZAws$tPz$t|4sbD+o@K*!Qf%+cE=1tJiR zjK1*!=tH-V*C4e%>uC&*x`$bhqJJvsnn<7{G(Z@51-J>y>hj^~;%Je$i8Hr^JMea6 zh}Y;;ALUcYT!rNmlZLw77IeyKUCpqJRJ$fKM+Zz}7a&B|(2T-!SHlo27yvJD94sPd(qVe+(mNM%a$mR zARz!;ZnRAxD?-XRSi3~hJb2+*TKb-&RHOn1C{bd&BHw17zxL9;vHKUF zm#=fXxH99v;brDqe0fbQ0Nv4~$)h$9|KCI|x z5nLOf>P_Xsg|BzvC{T(zlH+iLRb8YCNg~|fVfLR&a2u0aN3@ufU(F8{qb&!(n7%d3 z!Xbfhn#(?t2F0~v(m~C}&&Mx10ggmZXla(mQscN>IHRrna`meqFY;E<#XhP5eT%Gb zX(&8IUp8W5vsLqZ)zj*^KRHYW@YhI;__4gC6!FB0bk(%&vqi({^g|d=Db44zK{o;5 zh97m=(N{$;ql5tcx&lW?qwIxFzov(mP|xJJw8&|^ zIOkT5%Z4FQE!SW`G8EUCk*uGMi@P;y(VSn%g8=Ae;6W@ehuBq!;pHreDM6w57gtB5 z3bmDiAavnk#sWS!!G}di;0nwetMi9pCm=H^SpRXK!_@%R1$tMCqw^cpN8GqDJ!x1M zvCL&35C&`HHMt$Y;gqysd%>|o(>Y&kDI*0}?ZQE|Jv3JXOu3LcHA!-)(6J3ZPMs*bgAs@3f4|V+9zS`qTBuvInE1M5;)h+K(}ShXOk6fCntYI%Bs+Op7BL zIHXPiK?Vx;!Hu`Nu8g0esRuRjYbU|tm$U`ppphHbxekZ1D87Mh9MJMGo55$PHxblJ zFY*$-$fyg8E#Vn19_B8jJn}C4dn>(+nN(Jp;<|_p3!|Vz@qyW&w2x2MsFuQO10nT5 zIdTh)QE{vDcgp=08%NHiU?F?4T=J=ImJCoEofEY3Yhe+zIJBF-79(xe#H{zIN1-J? z12+<3E}Tf?v~MRihH`fG3}t453>|>e44^lim9!=OP(dN#5!wpSiRYWSWbQ(agkSP7 zz|KYGvsj+*)z+Seu> z2+;>9-r**GL6ExZYWBgHcaJ(yP(YbSY#u63E!v<&ZgR)~xQu zXQzwPsWq8a%Mg;G6$)y58EKuToDd?V%)b+=rK9mY9)U;;7#fVou0DTmppqw;pzY5S zjr?0W|6FV2f9OrBn|fSGvfMa}<@6AZVk&4`(J1T@`5`M7xLK?wY$?%j`fzBQQtRb; z<7l5iy5mZvdM|wZ$oF1tTpDIlII5G=U*(Eu+BF9~0EXPKrG}D1oq4x{!`n&K8XdVt zK7&dvv=oI>Zx!EBWfYpM0v`SC^EYz7EONrKTHRzFRH=~g$1JSlFh9q-8SYfr5;?8! zqPX3I6{zV-R1wDECS+_%`rSv$;+P?4QhWf6r4H&#`3cfAj8J{!S03W|QO`*M(&=jv zLl?=isWf$iOmtA>cC-$CavSoU?T3dOrVT#5SLfDZ=Ym*{h|nyWTx9LAe*6x zYe*bpuUqI>3zMx(&q5(CTCQ)?Z#?o6lM+&XgL1l(+_?dWSL%qB$5IW^{5dAcV)Jv8 z$Kj(hfcqZi_kDfcO~^REk0AJKhf!HfhbV^*NUsW3oLby3L+SJaeoB#!uLvclfG|ou z#u4D6`42gjMRf8;z;EC`KI@rM}G*e*i zy#}p7423I7Bf!8u;+AuQ0}2OsndzSjtV(cCltO2Mf*s-_8ldXkcENq^=RiiV9qbUs zFt^^PAr4d^aH>l!ZcO56lbzr$eiqyYkg87H4q=rrdM z0{((ScTGlwWCIr#CkKU+3%0Ge;sG_qtZcp7DK>VY^BnR`z^K4`X^~z0sof!rwL)F! zOX_eG!jyE+us|4vRXl_=o8ANW`%W7FKR(WjnFuUM$P4pn{?W`KGun&GMNP5Y+<@gJNXr_0N#%KBJyeyMie5=9p{(a&X?aH$ z-s+VEfPU_kMgjDdW?1G0R(0~dK7*S}qc1BJNdRBYSF`ciJf()0+?KI`+_JScHIw0@ zClI0$zS#k8{Wm^U=0xoHhUSU`<#q(3hF zK+$8+;*9S{vF_dL0xKaU@trGX7V>}?B&vk1s*eCVGsZ$#&m<;{aC)E>k?tQ2yB+$O z86s5g{R@iy^>QzkhIHtNF{ot_e#OF+ttO0D4vvu2cHuYmB=tR-TlTy!s&D$h!V>_8 z5qd<7>STDruP;Z`4`Gzpu>DX^vz)lY0=5iepy%2nE zLf_NBWrT^+0+y|0#MoIyn%dJdx#aehj1}l?FJRk4yn-U8jxb)7JnTH1DL*=9)8iUY z4xtDaMn{udrudU3fnjTZXG%A!L{%eD+Jq4=Ypa=s#R*_S^|5S#045F+KWZ97ww+)I zg>?L{kMtt*1EdkoDH_gM@PSo!Kzz_kNMUc7!?axd*~ffK{ggL1pBT}Dt;Zau2mYEQfg(=TVR}m53)&g<-?-# zDmqmGPWt>J-(wt()14aZGqDX8y@&Cx=us=79|{o{6RrReO{1sG;L*x$Yqy#rzexJP z`5*y;$|#^g$dn19KmpVo0s*QaRo+Q6CHC=y^#oRFfc8L)MFr=ZM;;Wum3thRGg!a-OOY@nK1Ny)Y)F0JD z2g9z>dy8t2WWz}C8adL4CKIub`iz@$cdWiAt|y|JW&vu_Mg0_@T+>6@W1>(L&F-D& z8km@Ixp`lbc=;pP_#*zdc3BEm#m#@^LZzvs{@9Kf-rYii^Jq!zH^=2W(gxTgu9B0908I8<#%p!a`v z)*K2w@#Y>*$B!4-ICf2dFCX)#yCwu?4dk{AnVLa%i9d9Yn`DMD+V416>)h6X%EK7) zvj{s}anHvyiuhCir+>ZfE6jiQo3N8DNNU%6aNRNxZgl7Eu8T|LJUjn15@RE3#zcAz zwoIKxX4?(PUdG5XHZ_soKUCNnECkj#sr+085<5nq%Q}-_{%)1z(|42O7PE9qF4%8L z-PuB8%(=ECG($Ouw6<4#8kN2hB;E~oIdV~S#Esc4) ziIaA#L7T6rh=U;A7*<^+)J#dbBS`B%>{=TRCvr>jED)7C`r}Aq;lXffu_PQe^4e03 zacHwdLT@R~It`4N5pRBY#0hUJSRc7=JuFs%7##qYrG{%E`!&ftycU37roO+dy7}j?j@iI_} zyJVQRQ0bX8bPsu944}~toEoEKv5sI6>IUGN*D-JH#i;BJY{Tu^8a!aw7KIOQ0Y0C_ ziM!=Yk(p8hfVFF8K5q5+62HvCo<*fm9Pf*3)NKSiv6_!G#x^VpKZGz$b0f0yiRZQO zGUrWLIZXN$$wJ5_SY(VDyaq?Jh~)H9&wbT{ffCwMLRbU*v|$Dffc>}+54#;g*M6|i z(rl^;Aqb)h90Z33xF{p?nE{|s2EV1OdGnWz%woH!x$sN(Au|LZS`DC$L>WCaFoWy6 zk?dPC{OB+#i!ts8U!5d`aw2(fQLp~MBj(UnX1_5%4=};Jf(7ipcnNootXOSNRYziF zQRioJfDfh*t0*G4{c9rK5+rX5vN-oiT&}HSVGMSt=2r2Vv#L@s!|&KHY>v5tD|kK0 z_XW6?-VARwrh2`$iO(j4QsXf!8YBUS4iZJ*PLX4HHH<_}&*a|wICWZnkJa+C1@A}v zDcqnFN^El-8?wsJ+99AQZScl|5cTSBpJ=A(kPN11n8L!U1GNHdrSRe~Y=>M0ARA?8 z%2aPB2v)6M{Cuu*gKt7Uh}mSUw$wwFdY|Z^z&Gw0e2N3C0A5sTtoBk&-J3TLVOxbs zP}B8hHJb)poN7z@47>JXP2)p1T{HCCP$YgTf$L+ zd&9)i#-snzSn%x};70Z%cKJ3iboJp`$I+dsD@s!6a@*o>L-ejd46VCfSV6kK-(6GQ zAt0AbYYf5u0_5`4;;?xoS5J);XUz`4Sru!XwG8%}?^^6m!;)Bu;HdeK@Y>|k{sn;8 zF1oEU<=XiVGZrM)>vwNpjw@p4oYK*JkCG?1z0a~Ks@Q=y--EMtyB<$mbw5~Nh2s1Z z?ldKH6oRhVnnO-cbj}8p7Y@Y_15y~-RS|ClR?1nf#2CQS;_ZDj8=tg(5=U$#R>L8K zBzX{d6$lt6;|S!$#KU%Zm%~HKN~>rXJ}nLMHr*eN?!l%2I3KVOQnin;FB%LI$u4dM zI_mM;E)_zBY`<~OfN{Mt#Er=H{k+_t#`VK_lU6g+z|bQ_Vn&MX<2Mj9g`Wt=Bch2- zqcIL-%&isx0++om!}JQP#KjY;bqKJl<-V!hLI#w4DAp3RS2^E<- z?|y3aZ(>brj&UhZWQ1ktCPIfc04>pqsuPVn;cmhZ52ZBWlGm8Emuwpeg?_COpvq#9 zL?E|l1O)tBa3~Zw6l3RzN`kHidXhJ{X9YG=HA~F%8JtM!E zz~V$^&d08!7qJ}-at1~3=+*Kaz)}Z&5(lsxv~jP&|9Ia&-c~h$+jx!JCF?#@;0lHe zE}Y~2aT%Pm8gSv4o~}gVzxeq{=f>?3DYf0Z^eZT@I{IisFZ-feCNo?aD?oeEtZ{o} zG!e;tQSflU9m`1go8$s3hQ>V{S_K3XHxGqzjZZ`&=nDCrH)&=(~;g)f(IO4EFDkg-*1YlN#HU*Gzug# zjLX8tIflv+$fIEpi?j;Kd6MyZD)UQ?aBMtg=PCjTa%Z9dl>rp;hXmyi*r*B%2-29v z=~O7%MTZ1dzCxuX5M=$}6QSk`xP0bnbn%iF^lGUV#29Yq?@mC;mP0oa{DDrR#hcmK zI}~_ytqU(jK!PJvRvc}T!Dy775*Lp#YpQE=GHn9L|53U;zlJXeP@t!A01{1kNKpx^ zT$kbPL-4}wP_qQhQ-mJgD!7N%JI$Gue`?W}!>E7RX!}E7-HaFRwFB}qEwoz2kIxP~ z3f#6opIcy*kfh_)#6^nL#xDXJETdo!rn+0NOEIf$FfF@2+?ogbpe;=b``N&ZSkAzT z(NHFX`8$Z-=1v*aV#RPb);zM)mFoliUsh$fUF^r#avQze)reaH zeY@ipULk_sNyJ8JR3KBvA8uf<`0;W~1%}RxEd^a6-EyK4ouJFZhAF`Sn_+f3?16Wk zr~BDe3UH?7qDewbivRxG8$^a?(s|_WdsPe7`vtl>xl%6LX`{-@Cn^Czpmc{@X8%J& zOu96&ge!Iaba;tFKTA=o=du}Qkih}FX+$IRpuJ|}-Z$172MZS0X2gjDB(6Isx|e~w z87Zea1<5t|EUpM)Fd2rl?NS?Ks#l?a;6e)D7zNSsv7RgC1KRBn5N_kp_56)dpOwfX z{cNLLF$J*;ODB6GW)OqC^awxbVh*7A@J$P0`Iu_J{K^vA3vg>ExO#La19iEjD3Q}n zxj;f7c1O{AmL~Ix_WwT6kU>%=)emC~xWeP%4H{Qf%S7=`&=NngI5kA8i$p2VFjLj2 z+Tm6BNfg~d{u{nCrE<`lzn0(_$ura-^2P%q$*{_y1h1Ut8SuA|(5Wzj&a4CO5ee-v zUtfw;g8~CL2BdJO=qGuoqPx^{TMk(ea{`$nQ*?Z}V18Au(q0J?jrAZ!%HwS*3;q-w zL%VV;UPO2ng}ErX&^Qgv%yh#;rtUzU%%|CjCFD5#heqSrHGsURt#_BX8r7 ziytwIzy5_~yAYTZ7)CHJsIUa>1}zE0%V99b#qdtlgg|*XO5qus0mM>1w3cnhWZ?nM z>`PTE>c;_!Zue{c6+@w^k=m>Am#eSmkR~mkDGJl$Zy~#g(sN(|Y}EpI$#2W@e0B4~ zP5~#@nom(^ZL2S$%;yKJu)!5U9DyomBC#ZSpiZCja?Wv>{9z!)Dm}J1FEX$WJvt8r5pvdd*l=2I0ALR!dvVqA|Yg{`8FlD_3sF9uq7 zNGR~i=*&-fikZS-E558dV83Y7cwMognR+yM$>P8q54=Vb>z5q3UQOMrxi|YokAkk) zh%#6`QT&Y-uqO;8FR0x7Ex+|aV%Nk~-_Z1#^27$4SK79LWB+8+eVHYpKiXw~JDW=Z z`Vcym>jOI`fif*SndathDf`?%!3#1INWA10d;B~e1v*alRlep)FFeDJt5XxVk2w34 z_=es6A0vdlA;H2>JNKS4I;hVFMZH_=m)rn05ySi4C0O1=i!y6>9wuHRz9_=#U+~N_ z(ldg>dH!SsZU0s@HzXav;-n5rQ3W*j9F*ZMb?%*z{0&Aq=pZev4uEmELidGWhJkj? zx01DF9oZBWVt%WtVZf}$e^OB2;9ip&2ovM|5U(dK2vxIq)mz4|Y{@;w9Yg&asqoCX zL6Ycaxl>b_41pe};pG-&mQ{JBf|G0EdZ!TmADGMb-Cy# z9RG)I)c<_}b~~**LGI%vz=1nFQ2Mz_6`6*#wR3Q7Dx$iH2YXz>38{GqCc@glA~WHk zY;%nhLrqI6HfP%n|74^ulpN>a?_-8IV82|ICPV6x+wXmIP7#z zYt1!q_ucjk>iKYyqcB289RAFvUXF?POlEaCY??w)LEGHiQP5#=VVa)F0;?t!P%KyQ zV!+$Fl*qAU5L9f1EbuW5aDS1Fw%25CY2P2!fWQ%yGcz2hHgNEm6n%1LUgtp5@YFjOyZ@M+0Y*j@Pe)Y zB&T%eytxIsHRi>}%ZbQ+l&UI(b6|2jEny91nd*CrkSIuiDvm-?Tgr)6K6O~9Bl3yX zRTl)h&58z}?uQ~Sgc5hdN|3J^=)aVNyEd_ibfBzN;kqNh$VoA#xI0jh_-R5H?x1lve6O)w@6 z?3pju*G!CNQR4nU>;H7JPdg<1q)j626Te zaOM}u?wG5E(-*@6?we!g)xrK&Gp8FIo!wb8oOicsbY2sQ^zGHgogJY)jrm@`UlgF| z0k@;N*8SK!*R&HSwDFZwlyK9pL%fUsZEQMBL|}tZ*9qg^K~KK4*s5ZCO{2eVo^}V} zXq4RA2!Rxh=m2X;2}7oX4mOi=-@9B`fSm$~^mAB6&qfB{pcnMf)4^V9qYJiB{OB9> z;E4G73ugfU%>`4QE<3#Tpsh3{Ak{3RG{YMBLrn-TNCiAvyh{jP_sYL;L8U8DE0eLS z(O1(m9~CQllhEOQY^lq{+)n?eSNhWya5>Y-8@6GMEkk|YTCqs2Za=^@BN7@oTbZB6bqq3N1Ed`VwzJ*7N-U)VCgfAQCRQ zJw_cV(VpfsqoShXN#vdL)~C1M1HBZB3sX&8xt=Y*_+kQzKfn;v9z0UOXyN9+ZTU!| zzn5vGIOksv6uXRVbf`wbGWBvHeS~`G7?C79W_&(`bpP~xb}rJ=)&M|D#UV(}y6~_u zIL(P4lod1VwA$~d3Rgj!VlNPXDK)*6&agfbMqw`28*J85Ix?-{1a?AqU?N!E1dVwF z#QxH`mL=Ji#xfaXmI|hIM=@-tfPr5Yo(iZ16sb`W-@*4)O++aZXn)ElY!fOL!HxYw zF>yzVNDuhxWBRUF@>{u8lwe-fQXI6U)r|LpFbbyDJo;Rib_^XpG@?#H2R11FQfeTO z{iQs$gr3PE8GnoG`SOvw(uj^j*MLj+4E-)~*Z*mZ`xNJ3K090ww{|C|6aOwK3%g(q zrym^>&4khpc3B&bm01n0B?BY{GEAolO_(h^3Su^WuzJ$5w|L43bURR+C(nVDD{Yn7 z2?FgH4Dfh-9ITD&I%rWr$6f-IBDUi5Z0jN_TMc##0+S26!yt~X#hva26Ks!=>dv5? z(LaTr#{Ol~M3ikIKx1;f#y)~-ECHc~JV@e!5&qNzG^5T_3SomP~U-Vbsd z9JWG$hT$;Cvbm~d|JoE*(K;-pWP;ny<3-Vy7tGAqyve(zMBlLgF201e{j*toB)S1* zRzMudUhRMSGP}q-D71zbR4l_pki9d&789j`c?2igDm;|ddTM61(g%Ucn*wN9G#M-^o#g(05xNp*1TOX?y zh`u)?7u_|%x-wc{La3ugL_+lx$fMuVD9%5!aQ<8zz(89L@DCCfipV=K0 z&i!ld=Xp$-D%$cad9MhT&|(5apSscG_JLHL+A_O{0l9TR#*X&qPHQu+Bx&Yy40TQN zufBYxC@nHIULS@Vj9f{xepR!lp;ag41Cg^HFD*Z@o3!CA`lBI^@ZAOaf*si5H&%Bc zmfxq1+)<105v4<$pbD!!4W$0w&P=bmB1a(xLDwpap^L)6w2}-cszC3K1e2D&0qhU3 z$E1te46A8Oe}RJqcZ?QQq$}(&)A(vbxR&jgEca@T*#8K1hbHj=IKP8%7Nd>C(Fx%+ zQW|Rei{a&=(PyWu9pptN*=$tnKM1hu*Y=5~yl+B5!!lN7uX92(l5laXs065^au zWY#-R@9mlg*%^(iMKVxemhn0P-8pshtlqs+Rufwm`BUHw30xaH!mq&sK&Y!MKC^f+ z`-JDd0(BV|torm=7*&Y@b`;_J0y}xsf2|~%-f1FjOewS`l-eFB8N$R(Krr=(=XxZS1TMNPTq#h47DzI&7kJM3?L``L-Gxq)+UW+iYPhP=mo)j5sv zt;11o)@jR`_9W!ny9xve^v6EgfSMz?`_F%60sjg91HQ$lbz4TOn%TCCs4fmXvcNFz z$OjyQQM2caN`DBgM*!NK+!{9!kSR$z0L%ag0{{a+W&$JtEXel2iV=5r-FDa0?kDfT zJ2P`hnVH!I@O~`b9DMcP`~ClJ#HN-3jsc+o#qw=^ugrSDtOm?lz^nw!%9?e6vH7)M z+gAZ*4PaI)vqH@Zz^qTRI(?hIRZ3+no|ZCwmu9_LuUXlwtMQasGv!+;-$As zUbRrZfii2KeDh>h>tt4|G^1j>ZPW*nSwB4`b`$wj2*5l|W6{+`23`U6%KXuNACSjJ+~;ijheH7AnQqD8@oD7EGV`I`M7d%f#3v zW>sR=B*rV!7m4o?Un9Onl!)&T-v%)j(%4619gPKI>TuxaAg`p5q)7`ePLD?W^G|s7G_;x z)LuOTBYzkvh7;9*3p$k$8U64BHf^;Q%>I&njuPjwTM`me48fYw_v46NsU5~!Y z(RWoK)z58kWKM%6MKV}Y$TB<}3AG^HuXr^F{MLGuF)5GAk;Xu^Wul;G4l0gIS}#7tCtGtYgNC`G$E_ zFkion^)j~0ST0|;jMXwW%d8d5O2MoX%qqb*f>|M$^?|Prd~M(>17jEXs=zmaF9NRu zEVInAf>}~9%L!&sjAkdnX9Tl^;PQbfr2|)@Y~bu916RrgJ{7pqqN_8Ot8?q>%)r$- z>gZ$w7bg+;#Bw3Z)|XelJYbdv%(8%467VWnvu;ifaB_~i5oQY#G2!Eq#I48wcl#^g;10J@KWLFQL8!;7wDSS+A~B@@Zu!oxW@WVb$l$Ds7U= zm-s<4d2ptKT>7r#U^V8XX2&K^>eflUbW%$?H33w)G~fqMRuwpr>2RFMda%tV-5LQ$XG(g4l-7d zv4M;QWb7Yf{TSQFSU$$?F;>Xq67+c3!I>yfNRgSN5jD=(D8(-_v)QxW& zU)dPD##lARrZE|_a_Bz#SDc6!kT zx^;nOx)vc@guIi{e zp&^zXvwH7VPC`N6F8 z;Q8D-K2te9)3E2(?fGs`cCb@&aC(x>=}Dtmjt;;h2mnY90Rhl`f zu^gUThvz#yS?2ESW|m}fgIQ|uQ)ci}V(?Q&{gfB{M5T_O(t=rz`4u3lTSs*s)lpYM ziunw4BqW$Gzs%ChXO~YdpIbh)9Dv5C0g*z8AYzmmk&zLR(Xp6*#Pt1H!2QGfHi5~p9y4r2_**^^F9s4?7JjqBOX?kYS zRuFs!B+oeWk`Mgz!AjRLX%pUj@N0=EU$OB>;D>8@3Zg`F+x)0fVrXi*y?y5I>IsdV zm<7Tse-cj99Nw4nyF@GuTpRu}#sd ziS!C}5o3$3N$knAanezDk5%e2AegD_)@KOIb2KxG(OIFcbr^vBL*@HG7Mk) zO3wUe$}MPGb!%~lZZMBawG$$-g4~)#_e~ssG^vNI?`3zF9>Vow{CE$?W!9(P8HVw{ zho)0@ISet+VT4~o3~1jm-5mcp4s3K?@BRz2bfwhOympbWH}n4R^>xpKrH&19n3;-u zZv;sN;fw?&%r!w9Eehc>ayB(2GwdeW&(8v1v74TXtME=lgDly+zz-`EX{y5qN52ki zamQ2rkFSk{wR841SKwCxta)*&?~e6hN?cNcFfGs%c0&3l;EK-URn&_T?^oRWFZOf7 zIf!q7$k~TmPj}@GDDYt~7`P2yDoq$60002z1^@s-1_1y=ZvX&K4Fv#@?EnB64gvrJ L00000vFehKamFhe literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_main_v1/animation.blend1 b/tests/files/web/m12_glb_main_v1/animation.blend1 new file mode 100644 index 0000000000000000000000000000000000000000..5aa137ba4c90794d0445034d014b96399fe691f9 GIT binary patch literal 91072 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%0Ac_Hc39JN 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%0D?1igSNF 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 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_main_v1/mesh.blend b/tests/files/web/m12_glb_main_v1/mesh.blend new file mode 100644 index 0000000000000000000000000000000000000000..2cd6c118413e70557b73ac4552c7a008b992b748 GIT binary patch literal 88625 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�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 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_main_v1/skin.blend b/tests/files/web/m12_glb_main_v1/skin.blend new file mode 100644 index 0000000000000000000000000000000000000000..174b84822d87a7c2ae799f6f052e1d2681343d7c GIT binary patch literal 91781 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

{2oj?aVmDCRO#0pvo; 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�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 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_main_v1/uv.blend b/tests/files/web/m12_glb_main_v1/uv.blend new file mode 100644 index 0000000000000000000000000000000000000000..789dabfb5e5698aacfea0a712acd5917bdfb001e GIT binary patch literal 89127 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

YzlDU+iNg{Fi-5F-J#v?QUIPBxGbCq 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 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_web_v1/animation.glb b/tests/files/web/m12_glb_web_v1/animation.glb new file mode 100644 index 0000000000000000000000000000000000000000..018007815d0ca2953499a22a64ee7845e3e96b51 GIT binary patch literal 3708 zcmcguTWl0n7#@YhK%yojfG_GghGKBp&dlDrTe{m)+F0xbyDVO?snhLgck1rUIx`E! z?aC$=yd)Z5OoI<#Nqn&Najh}3yFB>9qwx}>VuBD7P0+-LM$iPm^UrQ~=?#L3(`@H_ z=ltjYzyEg5jHeEE-zx~h)C2f!7KHt=f&QGRS{Ad#a85kJOiMF#F)S(`Npy?jOlPKQ z8zyG3=B;4V=`H_v&gX#)nc7S$}qdt1eMug z$s_wi^aOm0H>k*7x8w;b5`UDIfGjCW$d4&+(2?>hUOAw&_}o0J>8g#IHYlTwk|g^= zk}L;g#VwjO0(B{0RmvXzrrib~O%>nxI02w1i z?FwIed-}UN261BKutY+Ex_oeLd4xWZRCS%D9Aj0>)DljC@_g{qf~GPmPMENWF4wK9 zZA3j+BivBet8Ft4+u=(QSv}yWSj1pixSha+Z*!20ndp}oSR-2KFM?e<609I#gscP1 zjIo44lRhUe33R$;RAW7o9UEijA&ng+cN0cBW9Uq`4<5@P0Df6PR1-#4hXtGsHqDBM zx;ni`T3SP2AKs0=LMOTDjp(LTet1?V`D=*BtI-aq-h~|rjU(0d6I!krZMjl2VzI)5 zSZ7CXwR#$v!NvcyA0@kTe?5XcE!7aNL4dffoq`4hx8{JZB{cd{d@$jGH157-!6!C% zWMK`8=&d{S5fpt(Wm3IMJwcqaEiEnnpcHKJ`xRx0F$6q;7CGQ;2?m3TKOo_dMb3x# zEm6;r)3gqziXAy-vw^WO`trd2&tb3gT@d!d*IG+^eW8F-kHw0uT=s7F22pm6d-?6q zXxF=rUEDZ#y*IRsGcKuhEXm8oz3Yl2Udx!RD~>DgR&m^g^~_bX){vV}D8QFj9Er{+ zA^(V?Z@MsHenG7KoS*{^2VXw#y0SqK_V)DSi?qFhfBYSFezh@8^7-m|0qa7+nO%K+ z4;J}1E`Ms{`MF(f^K)};n2KN=iO1s+sKNVv;5m%XWBe_~zhNvy=jS#@oVfh* z|MF;m}5`?dO;NU+uenNQSAqO9|{vbRZd5!d)>Uo_`7iF`16F{{`f({3zuVrZ;Wjxyj|}n{9@B1 zgy;Kv2*R+Qt>^EPDh21da(!C8|~G0 z>?D*mv=?0viG6&0&i9=kdzDzs>?DkTSxDN|BxU6V%Bg zHd0KkxSBweb@Xp{h7yBJfgdetu^QKlno%UjT(Kk(@hmS_cNs4XnTQp9m?ybBXR#@X z8{BmWHV<`tm-s#3#@${&Fc0-z&vj`ZL_wuR`;_*v>v$%6Y3-x2hPxiEZ25hHDGhoy zl8yf-CF)lm2?;0;XaK?FsVEA_K%)e>Fs(oY4h>w_^IQ!B_bQ>^@A-bva|4ip8S{7! z@w@;B0py+~>pM1v%7tp0UJkx~JAe%k{ zcMxo$dy8mu6^lGGA=WMPA0!~aCgm)KA}exPr93K^OD?Yk|7q$m&eL_C@l0JktbuBe zK(0`nml+%dE|idwqU*Dv1N2h?g>_>Z@kIAt%97S@TPZk1 zT)$~T=y5mb(h8ndYzjI(<~{$K`vF7Zy82XUm3(zK z=#$vo2%(un1syr1meO1UWVibbchc!-0`JE^@Y3JU)xR}{^}Wu4uJx!j>phQVXl%6Q T?_g2if%?(k4`yUp$K-zj;};PT literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_web_v1/skin.glb b/tests/files/web/m12_glb_web_v1/skin.glb new file mode 100644 index 0000000000000000000000000000000000000000..a941a2897b548fb314dddc394dfbf6118f26f84c GIT binary patch literal 14264 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 literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_glb_web_v1/uv.glb b/tests/files/web/m12_glb_web_v1/uv.glb new file mode 100644 index 0000000000000000000000000000000000000000..6e708b0d2e2436ce0f8bd53ee6f1dfa0f9d52ce8 GIT binary patch literal 2756 zcmbtW&u<$=6rTK`N?S!BE=5HMGeR7yP1s#KvAse<&JWjy*iLOH6jf0=w#UvQ@2<5w zPE#zS2yx*=)e}gFKLNp|2ZY20!Hok)B<_d{S5#GkZ)SHL$4*LH)>`kIw=?g3?|bjf zY^Ph#&k{m@yhO-13xvE=tCR;$|;ty}cnLUrF^cx|(8u+JSk2<$_?9|)hj z9dSVSG#W@l)Y2mCZ4$?$P&edqow;fx6i7;9wfWFZeayX#dm0Gc0tdtce zv9D9hgkRfj*-g15rQ%l{M>x(7QKFj&Fa{KOeZR@8Tt+LC4|jt39WKfsT!P#is`%nm zhDkb=j8gPYhS%^*T(EA}ZdSd1=fLHGimbr`zV3BBKhK(~qHgKS8C_k5ku~+Knbn>| zBeUp44q{WNA3ydr(k-V_S$SnLdOEA84INaCK-9sKtR7GfDmki0AA;7nNMj=?Dg5B^ zq`Kvj|Eg?Ei7nOQY7ZT-ZtsAVP~fP#%69I?&L${R%JshR+HK%iUeRdF>!ZqQc1w0r zSbXQCH6lJv?N}mg*|vtX7Gm*SMZfBkFf5f;+hLuk6i+u;Oc7Y?x7*y`wfP-YmX7W4 z`rV!kf%SUGGZUzrH@&9dVo7#4a}d6*X%r!KY*vx z`U8A8^IW%B8E`TF9etN4;3i^al56;JtTGrMpB|Neu; z*YTJMs@K=%(m$7K7|eHar5qvOes=j0dk*77vrsJ&^5GkV9DhQ{L#U4bAf&Go^5<c%^yo{=?_ztN(oQm06g3ZElWi z-M`S`KfUTn1I5kq#@B1NKbA5%{y{y+@!ODb%j=Yk_h-%-*F%oik>md_k8D90#}ezu VafZancqSyT$wmJqFws4PKu^Y$3q&>ColS;Njq* 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 rd+yERrBdXf9Aul!GV|i@p7Uk`oLDzQ-e4uAk8 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

=n-g;TrYt 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�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 HvFehK9#SC- literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_library_override_v1/m12_override_source.blend b/tests/files/web/m12_library_override_v1/m12_override_source.blend new file mode 100644 index 0000000000000000000000000000000000000000..f805e8a99acf7b443b3bbdb3d964ba3647144676 GIT binary patch literal 88356 zcmV)cK&ZbcwJ-gkK?VT;-8KNaMqCj|U>cqSyT$wmJqFws4PKu^Y$3q&>ColS;Njq* z@nua)3B>=0(tmNi^UP(w(FQRIEQYtQ=z(saGJz*55LDW9`dkoy+D9WO1pomD0r~(> zr_Akkivoeb9M30En5}jjQ@oCrh0sQLEBnVE?@5 zf9DYu6&|r%E^WN0r{~+Yt%A$FyNpI72`u(1uIrlo;DHoO#36p$zQ3SQpeg_W0H%I^ z7C>oeXt-RkQmItR$jJQd*6wz@M|EB6>~_1a)o!CWI5##roo>tZdNrHL35LU=l_YBt z(-$(?tN;)Q+LVAtI7C;o|C`qw#zK8M(Tu%CTrR zn>;F1h$6vcGH*zb(6l3wNL$6y((V0Lu~@XdOiWn5zC3sh7%&FN<#PM+c+6t8CV{`- zwQ~8)#de$J@%dtkJRa}Fa>?v+y<%-7R8$$RIywVvKR%xiMzhI4EsM==5fcL&dj?}6 z7%oVC1wnz5!C)}g914YHqG(jfRTYOd9XIPAZO_%r<+CJMR#j;bjEabIh{PBKzu&Lx z>2wmoFeSB?Oo*w;Vnxt=Rs!Qk(=va0%JvKiWOI2!&6u+RTXDZi7$w+f;S($rTJyo z-GXWtHAF``)`LU1L~R=gkNo+mYZ}KD@BIp?Qq?Gj$K!nwAUGz2!5}SQx7$S}lgJt1 zuh)y&nCzeTT+K{A6AFStac*vS-g_VphwB=R#TQHWy=6=$cHZ+f9WOJf-um z9Y5>1mT}owjVGrj3(KXFKuoWP(|PZK1Oy21p@=$ll7(gU_Z;_9J_~^FsDTwR(2${# zDM^zw27v{}AVY|Rh^odCU_c-g4T2B~K@bi>2rz^oVF)4wAwUQ!hC&ol#xce8Fb8C+ zE4u+`ah>soF@QfENq}Q1pbBXPpz^hME*lX}0&Td_?1tg8EDO{P&Li#cB1KQEU4t0F zYlr#(Zrp%0SVp7o_(h!@4mOZrM`4`6?GoTX$J`x=OYQX}xrpKo9!s9=@++zTqkO#i$lFzu2eXR=L?<>Fe|NkFuLkHkT zX@LY2!kl?Pw-9@u<^KO*(E*t75|}UJ2IR-!|4*9e0Q}{}-1m4dFk3Iyb*sL&pZWj3 z1swoN&Es8&`hpP8arYPEz4!kA*@zCncayu{`1+vvJp%)~&jtVgNB^+}x%u1#_)Kkx}sqJ^tTjk;)WQ(a2U=lxHir=rUESs8SMG_q`MywTX!2 zqD-Wws9Ln>B-8F0TVzEIAVEGY`1!|6nsM5m=|05Yq$mkEesvm<=}uRCec_}p!+yVg z%R3->eX3*MxgFDnL?EMsGcm;ye)T#r#nxRkD^0P8?mKmtrf3242RO2$n7!`16aZ|w z*TQ4KvtQEiyHHaCc%P?#aXM*^12BZoY}Rb?bod-Zj64 ztnS9LYYk9hfkWbZhF*AMbb}h8m&wlO?I9EBO&y?ti|2f0oeAXD254jx#`l74O+e8z z;2ATPdF6Ql#D@)7eC}I)4VtJ-O2?bufHI{T6>=;TAJACoeP*;CxRnNAM>EqMB%5cX z$MWybr3NUqFa4lK$^-zEjsePI*-ldsAg9@83`OG+<^nCEi4fX}V$0uwIE-YxcS#?` z$+PL>2Q#yMB9J~cZll_igpx?rlnY7W>wEFpue%Ofe0{Th@fwzTlnbde)oI5M;ySh@ zG6kpv$jD1sW5ufu)$-_h9Lc=s*JQFNPnps zi+@1NVCA(Fp?)j+$vsk`I+Y0Zi%muXu~$K`F8m(ha477M*VQ7_l|(3YgjIZ@f`HW* zu*dlVz@lb(7xdP@_wI7xBr*xkHwVMva5&tyjg_Tk>?bYb zRCJ7s&@z4vbpn2m1eqki-@yNW_ikAG=}Y_ROTXFv5qrG;M;85mo^bL1=>M2j0s7Pi zvcb`VL#MJISoMN|28sSF$SuLnmLQY306A143F!ev5rLSIkeGF4F^2IX z;0GBaCA`JRk|0|uF5tz&3S(j%2_fHm;>ekm{cjX>KwcpOC1FceElPr4NP=utsX#+b z8a_}=8&vRJEvjVp{}@CfB!L}l&~n!-LymkQ0eT$)?%?r7N(EOJjFC|iDloJ};)bu< zHv@(ZAc5u(D3&2dzKj69z;Lu^!K~~-3Kd(5KAMVJ6w*V6Fyu;>fH~>X;zrjj}kjb z`b*W5xD-qZro@{Fu3ti4^LdtWBm0*q@xMe%VgpHksVa`_#eZb=$kyO7tMi|-FloC4 zHz=*+N?M220srqp>%_~k?ptbeerBM=da;=|3B%s7ca<9$WCF_9sE&x8=q$nEf|-c4dRX6^fh6cYaedoBH?DvGL4ei|(D7-{`iRcWcM zo$UGlJXtNZwG#&U?_Cau!yoiD4(kpHH?tJTfAAw+IF5Yvz|rISM0zZMe-_wTBm4fX z_T%UgM@rw|Uj(!4NX#H91@WfO7j^_vq2o5zjm#)LkdP-3=6M}c%#U=JLq4+}lE$Fk&< z>mS*6&xbF#QhA6~`~#w@#h2!)51?95vm~{A&yDH7-Qe}bGRtwkIWQwnp42ilOl5-} z3Ab^Is)oO$VIpmYCjY4oXn_9Ih8TWbt@ns(3!DEYgD(qzWrM->U42=6Z;6^_+0LXZ zXZhU5v%HdW(-z|<^55b(_M6iyTKz8kj$`fX-p0?m!FFYXt52h5Nn&}9AF4<>ltBn{ z0a*FCBgL5#9AttALM~+)VGVZtoHBl>C#&1unL9{mA6$@vnv{camh~%WXh3N7KCn_F zEop-~EF{4rhbM=JDphFuG!edNVMi(n9yw(voK!iRlBV#!G?9T#3G#TLqYfiaC3rZ* zIH8OTW=M2+g5rslz-yYoNL8+rbs>b~LV{BVnK-P9G`y=Dt#FM^4EBE+20sLxUtw9i zI)$#v%;aJ6G&m>`PBt){FoE7$h*Ks^r0v6vp1v_rnjlSFY2Ixgb`RT!e_LP-E%K{l zSCan+Rf@MErEO^I5aj_tmEhM^YQGJMQ=8O#DDLeOadg4|GX140Q3d~Fuu;)bY*cQu zi6_Mvtg^<&hC>@;L)kXP=0pQ+RDK&2D!&}kXX8Vp#YaaupWnWzpbz-@{Kw-1g};Y_ zkG)<)M@RV_9p!U=vx)!SMOqAo|3zp*HmAbh%lgU&#e=}T@B$4u$fQ?Lx-s3zc9j9} z4FGBytWL0^KGf9zHJcmLe{}R~7+U-nf1aA!8v36sD*sh#YHKG?^cqn4b4YN57p57b ziD|~*isG6PBZd%Q&I=es3Wzxa=;ak;#*`TZh71`xFVkyk3fL9H1Y!p1@-qL6`Z`L) zzc()_jF>7dwKb9?+IwH0{jIW26cyceJ?bs;wA9wpQiJes7t&vm>C(8J4MiG0nCmHo}a1- zPN0f2Wf=U(;J9GHySlOE{0h4|b&z=@Tw?=+SErC_HgZM41t}d4q5A;y83GiFx=nx5 zvmN-H6-iyOBHiQwm!Sazau%>+XJxd4a0bCh$IRA+C;D6njd(pEfa*lrru}jNPXs+h zcm!~ALpqftBaKR>@E{QDpv(aC4PhT1N)Y-1TY@$KLJ$}%qRi(KRgH7$%ARv~CWLd) zTxG_j=7Cv@6`~o|h#O8h>kvu?g)kTbE(9nA#6WzE*%Fm{GO2k0)ScmtSy2d2_=Cj$ zmty~uDrKyd+9LLe_6?$#<~lLWb-(>NGH7)FqtYf=^INTl$t~h%4}tWj zZ+;a7i-X~wq4Hmq!wnY-Wf=T!OuVZbOwO;c8&kYGg?J;+k><&b~SyvL|-Gy4P@%0G7uT)faV?N z#&j=u$J6JyfMKf5-^-F?GZ}VYx79BzCYK8aS3o{D285a=Ird2S4bW#fj^TfkZwa@r zJGZRc&SbD<O>6%+CNtXQc=W;y5@*BHw`?@Vlf(`1`hnl65 zGk$JZQu9N4kuuE}=GDy`)6E;xkG((srTk+mCC~J4tT0x}^1HC!t78)6o4b?&y#dZS=xdQc|^o$xttLC zsahfQ!^?pt;h?0+YHbBdtDCcOJP@v&m-EbmYWAn7Nc7BCj@#+-XZSnJ@g&6E(_{B65S8H=es>p<3t6UhTj2JQ{!&$=-Iz3*kv`LBJtk-Xp)o-NNbM10NLr5=L$v%o z6m&@vv{@;h1r6@v4-kfHVjA-?r z%;OoB-`Is;SGR0%^|c@&Xcq^n1hh+Q7HK{rljbvuX+EQve!!Td7bb!%XkjAAqKW}= zB~1*7DaP%jz?uMzLdEoVS#x zwg3=_U1|IQFmkdZAB;qd5o&G)Yh(e_^)^nbGIeY&HtmD1ZoHt zywFVGaaA!Y97$94(CN+jt!(&veEzijMFm`^yPjf(wtuU2Ce_YI$49H#Mj2#4`!oEZ zm%(s&Cj5KnmLuJmkp8oWKoWDmSR&n+0$QrXF;Y_QKJ+)xz;9a;VW6!P8)AgFj9(0c z_10mcD`sXWS{nP_jh*YDgI+5Ylv<^Hd$#Wv!-A_6Y8&;wN`I+}QzO0*_okBO>1qBS zi7k3oe>$f|13uJDNmOzp+tm}Wm_%K=RcV%_8u7(Hj<}0bf9Lo2>aD*AIDjd~atz1t z^Q|vlcVlT8VvmS48;U&<1F`odv1diXq5xVpxQZHxNl4Zw$1h6MEJ?GBD&csJBHdVy z`V7aj?7pxZzi$keuM5AZ+rF;Lx-HuVTb68BHrV=tD~ql^lr?=QYOZEklB8+AY5uo3 zi&2-wC|Hi)7#5>k_!XntcgZ-$x?3-~IlbL6j;(BP@9+iD)%2Ap6ls=FP@m-(-r3Po zC60OVKFs$iM2L_P`t;&uVX^1e`(z6d1)(yc+oDPU7dKRZnBLu3!VJ44SI};P41nyW z0R48l97^n7kp0;`slj2#1VIFLO(IzBQd}@-x1tA~9nR32T~eDoyAnrhb|dVB*g@dI z)=udM(+*2d*pBPOh#iwWQM)Q;2JNgEVs}r_MD2oX$aX^59XkuWV6fZiLAx}K4m%nv zgu7FPm|f4x4?C+TNINP&N4qQ-#C9~)j&`UN7dtCDX}g~6?$8l%yR<9GTqHgHY58dyy`Ewim1Rva%o8(D^SAu137)maLCsD58JW`LlD=&Yu?bT4f5a4;H^ zGjs}aF3bp{BZ++R;ZU4h9SPiY3Gzdy%Um!hrzVHklqTtvCg}<`dbv6hxRFUXkOLzM zayHd#qN<7&A2fEL zS@zi}r;IFVcpWHpa6YLaOU%P|*`j_C{x^>slY6e|A%@V++<-F;p+Z743Mf-Q*k-QV zW;PiSD<9jv*LKfmySq%qAa44f@N!peQ!Slb#6`kW*hUIvDV;FwR*LB;LQ1C$h zqw9UBS(1c4%P~wt0}50DU@+(fLj`t6&FzNXoHPrKtcj@RC~%%-5%ocv<77vV?1U$2 zIutFi@IjsxJUuF&R4E{Y7NHf08B-j*5i2|KW69aHbcpxmDQQcY)tQ4J8g^z>Kma4k z$Z+hmCoOUUJGRk6Z6glnM;qe4XwC_LlnN1 zkH_VozQMsTndLLmjzIR>6;YWm^2Q5zuM2#nf&jm*+p@Ot#`J5AE3X7s-|9n2^M{_8 z;jqL)VhLfEzsdNGVHbX1*L2&KCE4IQhp_s-#c!^g(@`6Wn-fZ!jj2vos6cPqSq+}Qj3IxK25eQ%a2Eq`8VK_h;rj(MN0RZq?ddcI*BH+pX zjs~*4)O)d9$Fr0ehVw!`&X{I$y$!K*w&38PR>O_G6;Bnwhv7!{FTU0>+iQ#=n-2#K zZeHRyR6E7rd!cDxR!uH33f+w_UUsxN4jsn=uY&Gt&81+)2rRT4>%2mQ1tiCJ- z^?}VS9c9?vo?`RmgTn^P2$!)FU84N8-Or_RoeM<#fgC?;JNJ7CEw7y^h}7A%nqIk3 z{mp@|vHwm<3xTNMWF7D=k{iJlR|;gAv-P-Rq(N1(ls91(^z=V48xkhT6Zk*X&gYROIk;ls2f*$gN%-#80LN@3()xpawbO-);bQX<;-qUF-_oDMR zmp$IRvrtQUfL{%^W_YG%A;RJo=?ZJ`yac7CxAOHDXZ^mkYc?ee7SNhL^YDT*8g9LK z`)T&>OvXfC?7P`(YcG)Ntb=pO1Y;6b?A#}NApx~UjJp`DE4^==S0(dY4Je`nUQh^- zDpAgTs2`98*&cI(=?~X#ei5z+?^qLsah02?CMp4+wNjX6yg;;``}J74HbeZGWIK=}4fT5{$FRV5iqxy?n^Q z*guOl z{Y29f;%wt$L^$Q4tC8= zs!MSjSzFDT&Mv!?ZVQ&I7*YY5won^QrorS}B){~t!enK?IpYHH0_~Ck$e>^gTg=if z|Je6SypqOR0mpzwvK|Y$r6i!9Sbjn7nDxuey>w}ePhBThy9PRm@q=eARcCBHn$3%y zYzQAK3w>UPBef!xYV>YZZ99pUJVS|Z{q7u;<)@K6=GY0o%z0+dHOkFfWFj3MO1~3vEp8utC?oGW-R^dm#xYTJi4JpmwW>GwF`b@Z;iJq zKAj_HiF1y|wA?#q$LZ{7(ttr^qL1H7H^(gs15eu=6F_5+(vuJC1#qmb*UGHIS#*$& zy)=CSu5A&07B7I~8eXfn($x<3cUiX7Z^%$$+Elp-9e--^*mCP9wxq3E$GLmG<)Rth zN@p^qu~l6t@f2)Fw8_F+ce5#(Jn3bxARFnNm`y9=tHMBxl&x@PX!~+4G$@aV+1WHT zewf4O`oEyQwO_y`v9Z-nQReb~Jqsdl3q3tc>m)QpwsK7sC!>}y-UH#FntED%WAu!m zw9qzIJbM3#Ibv;B?!;*a(tnj~C1E}?za4MBTyLckgP&XKELXNtRmV<)daZ)Cl1xTE zLk(UCbDx26nBY!3+p#)zWm>M5y?!nU*{cSyf0UDt61XNyM#tn^$&lw_mwlo z^2$zoa5gSQQz*@o`3AF<2*B1I*b1O&YPS-0e2uXVrVVL;Ey+>1?w7gr9+=DAO55@* zrL!LFKSr~qBn8LdR{Ejk&@)Q!`XU{;g@7|Nw3N2yAM1yu4io5sscbAHyDN76P4aTQ zn~^rnw}XL1Ew{D7Ev#1hF|yV3SRDfoL`ts*rbXQ-vA)vMYc;)6SXZ{3=N9YK&%X6G zKylPbL$8IO39M1yoGIrplJgm$f2UFdV!Y=4h z14TA&xfz!_=AZjMKz40zc^TjFovxmvpDDwcL^MCv*$_XL2X4nU>K0sw_0%w@PR{J} z%x7`h3r!lhF6-cKk=>}XW*_7&OO6U=9l9}P+%PYF+*8-78Hho*4tF-3cUMOAplfa> zEZPxyc7$udBeS|R?iBI7r+JUJI^4vpDgq?h;H$KhPQUcP>{PpHxA-lIyihD#d6)X# zy7_L&7}?qa@H5ZWc57%_p`USHw{F|RR!r*G%SL;#3D54v-s>{UrBeut4yGU0)J8;x zd8~zD8Zt{Ry8BhL?aT(8WtV3euXT(#0;1TAZEGHG9iZh|JD;3%B6z@_7}X8o%KvAw zi`jS`v(NurJv?n)`JL^^x-4f0?cc-|RYtQZ$qPl?VU>@a5#+1kgKr0??V(>5QON*K z+F^K876%j8qmk8cfR>3$b0FTU^oc8`Bb$8dcfr264$jO3V&YB-N&4yx1IC{@1jMw9 zGNnB?Qh?N#Vq3bKo9FL1b~jdj!%Y!FYD?YYxozokWy^G~@)Xmek}(KyxEa)T=iOFt zv?gllZA-5kfXw|>dQkS`_X{hoTW=Pf=yFtu|Ck`6c)PY)UJGZRtM-$@dxJCB+UO9$~s|^(Ap( zi#B9zJeSx3tFv!qnpKOS+*lG&Z~ z8Rz1Zo=EjKRNx*Td(YOT9#UhQ$kPSZ|eCf85`5HrfYlqOrDQ;#~>Na#i^ z&1|K5W}ceVpoe)WRh`{HT4mfdwWbFKnfGwe67Nml@Z+%?$N>aE)Hr8tm8K(EbpK&q z@|Q2Ubhn$Atk-OBS$mdPRH6xsFS#3PXcSM^p$oMUv`H+-cSD`5>p71V7<4C?3zZ(W zc0J9G)+s}%>OWa0CYoVB7HAMTSc)o@r)mpIcSDdIvS91NvPT%v0lT4QU7FD$^P=3B z=knv$Bb1-k!CG;?n^*k>Pm!T4qz7RUM9Y)|N{g-~00k`>{9s|-#t@iC}Pb;pRLYq-TNZ%f=N1)qXml0x?;7?>H1soSL*AV%#m z%cZIz=sY;yC7=~eT9JniyjXM*9JJ3mkWUTeX?d%1{CfwLZ;3}GQ^&@A20-oNw90{L zEQ#|I^~33TlY>ge4i0|J*_!?#rpV&G{zUgeyWDb0*ttIgwl znarLJCe=>+y9#m+>OU5Vpb8i><(B5pWWb;#TuRyYB4u6?wsd${Qmxh67_K*EFfvrO z$JCF;-m2ALWiYUZN-X8>^HP(Ii>4~G_pBrO{_j6d3X_Uvz=y#TKDbO+vR;bGm0Vnf zS`D*<4bDiBC39nDst-@rlyRid;aIDw<0Y+A=yIf-&s5qpuhnW?+kcE~bmud!F*Bu= z4qL797RqP>U>dw6Y0WXoF7>;0b0R$Rw4??f+LEBK=S4%>*>#OuHJ9;v@yQqoH+0M{ zUKp#hTiQt1U8|A?S6ke2A1-{ zA9uUjPJp?YMPI0Ntl}6H0}!3=Nmk>LX*gnXM@@w#dqAMn2=-&oa+c-fX*7O-csh=gKx41+9#30Ee&5DrZ>z>8Q1ME8=9ZK|IJKj z#iz%OD|K_cvieJzeCAG!;3BzFFkNyV1HJ)(Qk7&-F<1_mG}J;BLp~Qno;-d5sVX=P(6ltUI{c<)4>gGn_t>`E(efUH4^L<}t5% zMMBus@eV_nmv`<)(cwi~%yP~J-DfSZUynM235GGNK4G9y>D8`)+PKv_3pm{3B?4T< zxW$u$8#k+_xfJ~QY=Yu`FO16=@St=_|(DU6@`S zXm7?AT#DfoZMIclr5UbAg@%T~W4=rgORomoya)l$WgLxA@@q%C`rhRuRXsur$yE~e zmWOpYm>jDM+eve;oiH*lY9s06$j_GM=oUy@nO5mINz6OKh=Zy0O)&XAr`J|sDI)-3 zClD<>yqnE64aa6tL<~c!P~yDPPJ-Fe|TmTE}~0@mfIMGdnIEqA%T!jVOhE1JDqTpw!Cw#&Zx&E34uFKpDcE>22XZXvu7W#n8RGucZR$ z<7QyVej(9KXi&bZ%pF(~)j(b?-JfPq{p-^^KAdmMd4Xhe#A|ReE1C3hU>uWNc(Xt@ zA1aU5EfW~x*u}i^?L8)ut_2b?E$7is8dng%!<21u z#NfSOxWvABSlb2ybD#_4Mp~oub>%q2Xx=sU^HWwVv-|BN2rG+WJcFF(axWNW{Pq_q zkWGn-)y;aA5jND%M7v<>YTRh*gW%?7_4OawNdd{nrSm(W4&PTU{jZ>d6G`Qaf_f*U z+;b>{!p;PIE7_?>B27lE_uvn!#!{;Ye{#i^$7E(G4SN&dl}l|$M%kX_(N^vb^S0DZ z!A{G!ENIh+W&17x^v+>n?_8r~-zLut)u_=J8Tx@Fw1B#dG0^~y!Hk{6EsgJIgq3eU zvhxVch~gMq6mTN&i#y$?jctLEaxArK#o)Iw)?+f3+MzNvlt$kF++0~Tv=<^4)-h%f zmTmL5xXLEtg@XT{Tffv=VcQ8JDm#WKuN%_?2yX~!EPv8jW|og${cmAe=q`3_Q%t&BgaBpime9wmUvAzH%WulUW2NRrnuzK*X=wa9fu-oaBFQN=>SqfzWhrOf(`{L>(;b#PoE@s2 zlIT;?m|icIuFBwpSS#RT)f%RT_5jQ+ER|L6Vo(IcsjxrMW#_2b)@tYadBj2s;n!7B6$x{VG|n*WHd+d%R@CX8|09i&KS zB`FLfSpycNrLzev!hV_$q%mO*d@`;yR@}VYdS;o0D7~${gNfcHUcn9R3v3x9SLUBV zbZ50A{^r64Z6m-EzEJNQT2Ov(kbj04O3|gkabejo!;P}3 zBv+2+OTAY<+O{^FbS9s&C+p;h5~Vm=-cAOD8z<_e9D>|)Y7Vp&YTwgNOJ3C|cD-~a>==x@g?B05I6mbJ?W`bH z9h1|gV?l78+wSvmO6RRn7f*0k5JHSk2wXe(bk5Rqhn&G15vu?e)e7F-lF6T5wiRSf zeK|VM>w*)|fSsLdnB&zVTP5qb_D%|WK)|BV`uB&=nzJk<(t2ysv;$iujW#)Hd$hQ& zlN#dDto74ruG8^+zJ@VQ!w?q0UaWziM`6}clf0!j?z3@C@;LWuIHZ?zXl^#lUM$KW z=ad;*LaBswxfEFKhYW1dH^20`4>rU?^OgoDTOZCa{u+}Qu4ti|Vc;>QIGpl@wEGRt z%LG??k?-h}(y26Z^U{fcK_2>%5)A3e7{N$qlX z4L}1E-ujg*q`ghh&75M4P_48F-|(d&!d(xmwa2z!;$W_EkZmbmVKaX~tnhhPx)be zBxT8i9NPBufBEsG4F#|~@J;*RyxQCfxTNg_lo-h*5em__PUnui|7|1dj>xy=R%{(wnp%%jYNsyx$ zeM~9QK1^?|RO-`A8|$k`NKr>%LY~Uv^39>f5P53U&5yDjr6sMhZe;tfcpg)?Va7O2 zV;JEwJCVGLTJ$~vR`lO<0wmnWF>?WL2+Oe_pX$-icN*-2kkuMp37dzDxw+KY#WJy| zNv6|N;K16ub9k_rY~g-%n|pHO>oZ)#r(?+w_maw zH2V9|w5UiLJT2%eF5|VQn?>r%YkIJlSlczjpd?RkOD`92cI+WtP6wbrQOhz6$3YC^ z)W_~m!(%gR$i3CafHe*fWA}E6p1zKQ=qgXv5c4bISK?wG9JNDha#{NT853uScjUjG z46!w^Rmbv~6=h%AfdbMo&$(U5(VaYi=e`bS?u^?nLPjfllgLl0zUl#w?3yo5M3h-Enfx3sI15n(+tgkWMiB%#YR!BBZ#z%RJk| zVXD$ba(=_Frn5o14C299*KzWSd9hvKxZ=*eTaoSJAQpB@w39wJ$*UScIYV1)q8E;J#xDDml=}Y5s8~VA#}%b3wFT&4aI0lC8mNLwC4kuewbEd zULTu)?Ghlc?~yM)CV}_kC5D^eaq~}4jn!i{FnJ~uxgt<*sUDV( zro6}v`{HANTL$azPKQN$nnj!wAAS|PcD^9#ul205r@=sTTo?5Mmd9_ST>Y#&BR<M(eIj#Otu?mBv%1xf9q=17+4wRA*pM$E2^zHquskS(L*m z+67-ZJUFFpM~fF0uki_dGv0&SI<#oF5dMRu4|SdojKKtAvP}hK`icZc#=kiX(zvj? zt^yp1`#cUWdC68S^XBSkfjAFB#=b^Q57K4xIRtFwyaUY2Fg|^qeCD|08mmm-Is3Y5 z=m$mlWfbT|SqaV%Xs%YM_aA3iVUEt(*QIeE1kt|sWt_w0So(tvm>7S+nO-R9!g@W)#k2!?@e;oCTcLunryT zslR|$m*&su=jyl(0`tDMYb=HM7p|VmVxhP}gbTKVePgT3^J-!FTs<$KRRfZ-@A%jZ zZYePD=RIK2om4h=S$?IhF3q3QtI-@D@DC=_K3@xvYK@aO6-#FTNOSuvytn5~M(fC~ zdgjgP3DtEQWR^|G(m30iIS0}MUXP=l{?**-vj3cZT|4^0VcMr##$pcy$X|J2Jl27` zf95{fSi!GehwTe!bISv5OOn_ggYdk8bs939>Bi&;KuBT*@3}*(rABNR(r2J6O|qWF zcZ)K0Zd_Eby2i`|nR{#f%lTCkdM@QJ3LJ}D!?qoe7plMTR15pm`2J?ZAvoDHbr)=M zZZLD*|4Y%6AT|rRc#wLxz$U)P2F%ibnJZ_X0YgzgJsuhO@nr^ z=%8ws-3O0_Cg(`r$yxQeV6Xm?nN0DPq;9CLn4<{JETWo_vsh^+!nSEXR`aKB-WO&) zKut*D>1v#Yo5zT?rF(Ooij!Q<05D8!o@sQkXagKjiN-0})1%a^B35BAA@Mo(4pJ6C z&x~@}GK1XZZGEnk;zr6QC^bo|V0`FAa$q6tPLblZ`_E6RhrwD~MS}+TSn}UU{BmQq zvfXT|`d;4aF?t)g5uBxD;u3JHTlxF_T9*(ip3!_K#7<;g90{g*k5e^Q)!oGM-u}O5 z4MtQFyKWy~n7dCfIbIFwx@DZ_mrrvC(eq6*Q)KTCBnAOG$m}dmAl9*x`nRGh~=#p+uMzjDCnc0Cs^+43*PKgY`kv z`J$!ixG00$a)u0(Zi)=SCrm#+#%9@X{$3S8;Y^21Y(^ZsXJd%cyvD~R0M&X&=K3eZa5sC zM#S8>biQceh??lbp{NH*OM0uo{}%C2y4V-vTKf!>^7YFlaEbZqEhX_Kcj}>sE`%Jt-G1 zHBeNWWsJvojIySEo|cnQ#4-H3q|3T(ytXX6s_WirsitX+>XT*}laC+6f8CZ{li2K< z=z_IphmC(?V0kf7uhZ#<)_>d}zc}|FjqIp)y$f>~Q7MNJa}P^w9+Gaw5H5n|9ld2+ zcFjeIO8?N%FIM?4yxp>lEpdY(D1;GEP9B^pblfNb5bEMdAxDo8Oxw^?(%=r+R12Vx z&ymXm11?ae!~!r(3uF$uI!ekNW^4oyL~oGl!3QgAgqRV;c`{%?m+plIfVgTA8BbA0 zO~`?QjPQvK3Eo}-EF9sM6(QSMUCq#Bj8B;ko&a!9EI44Z#amjc7xR$k@(`gHHO*H{ z^h%32QRzsc5{`{COA>b}qLO%JjsK%CUSco~*-`C!W1sPevCm%s<=zKC)LqbnxEBV^ z7akorxhI1DzXXqckj6el5HU_Mg61*twoI;$H33^nen2rukrg!JMg+Q`A9Un!0RlH{ zqSS&^UNjKw(_}nzd;&NTo|+Vt2+eem&|9i;MAl@2k2roc(Rer%;l^s!B6|!6qM6<*XK{Gopf@XJOXdpi3 zIEnBZLYF5jj4qFsJSPq(xD)Kkov|NhzQ%r_F=5QgngLFnEz6g?e}t@dN3uBDLcu&? zV9paP3v?H|C~l{m^q(^&k$99!7(lvFa=vMQ_bwSI6IibfRj_k z63i18MmI%7httT7H0O^qUqLTzz?{#+$4C!EB#;4S#2I4Ib7GM#29bcWn!AIU8`Z?W zI9s|gtXg@!u8x}MXKiPgfn8NJG_;)6krX0x7P?TL;^g(%a5x11w2mls{gC+7t`4}i zZe71!Whnt}{buHre*5hz>$fJqKRGqwn3lSJOK!gi^=@w&rVdSa+Z{bNIB^*fS$xI*Z4Jq&0H}jiAEWL|6-f6>t45t zs%0rkiWe^m@wB{!5D0CSq?t?pBMHZ<@Qq>Cgq>7e#9CYJ3ryIdbq(jtdq#uNc(SHq;`&;ZMxfMmj;cz%+|97K$SkRS( z%a;V`^Ahp{Zc!l0Ao5bQeFu;`QP<(Qm}13b;EI6cHMAPI4K--b>V0e=m6%b<JeY*czEaYlnT#fmXPp9w+mhEpwB zkD-w;1)KmpUmOO6!`8G>1&}UA87x67M{~pskdtJL9@I-WyaI|&2@XHI9!da&KzqNO z?RuafM}`BM2w|41K#JJe$!5=(7;W}>GTG8#aQNV^E410ws3A-bCC==Outbg(Y&i#8 z$dQ77EV9KA!#1PQwi82Y_;p>^ZCjg;(MFCh^tG^;!0q+I;B~Yijkb~0TFX{b)HW8u z+1)=`ma%F_wd;*TKhQW1=>gJ@p>co4*&pk&g;4&eA|ufV!_!6>S{$LzD-!FW8|}pC zB3E?Hl1Go-1U^VDABS{|Ll1&64*eqlb^cyYzp>Wv5Yjj_V}`BQ8~$zGmK}%wprDcH zzqoF7t7#ajYh@GMEXlCPG3>%GT(+-^maN;Z?I><`RoC67TYX7XO=F#+rm7h*|4}r@ z0X`1!GA0l9H+JFnb=wEA>sC{*w6x016$W+s?3(j`pwo6zMLB4SqNXnWYcqw{Y5%(4C7v$i)(SL7uHqQ z?_CN1PlSYjX8E?)lzAi#7yKXE*|C2B& zljeVo+kT_x{QUh<{63lf*Is*zqZbdf|39lK`cr@=a{edDS)1h&xilsvnI zU6N%5!UyMmk;BQ*R_!@Ihm)bLbN?~?x-Gkb;9u;squTX=Tt6=_pK5gl%+gYq__|I$ zr)rvNAz^~#WJWa`lwItoc0E%_r}aPT_P;C73++m@U$eR`?jTPfLQ!ZWdC?|xzva$29nJ8JK0xW{EhBc|I1 zE-+{I@1%io-&nGj)p|K^>GX`D)p77(y6j^4DQGIlDAQ!hjSK3QCSc%4D)ug%1#^mE zc`+*)3IASQX(a7@m$J=riV0L+Es2rCa%Rd%&M+?L`)|_Y z^zp{1-$HBXoek>0PP#>BL=VW!WKHwvEo*GrDZ90{2lS$3Pr@@*W7nE!&SN%J2HHK@ z%xq>J&cPRBO{Mx4^73z&_O@ebdtv9>A*!>)MlKON&2dHo+Y~BM2+f>!-+}7j>p{M) zXiujq{F5A7QJM{!i-fJOYtfgYa%c#IjIpLu>%NfU3>d!`s5iRv379v>TtlK{D>A|8 zBxSdih}V&PcXolLs9Y8qIJ@m{%gGT0SLGJ#2!{N`_1-2Ml{1x&=WN%m%eQO+mes#m zJ*3*8$wPox1b(#)zD}<-k4jhX9#J++3Rq<^oMs8&*F|*(9wnif{=j2Y=X_YPJ57>t z^JeE?VfSqKfiB{elVoDsFWniL_Dk_0rryU-&Y1rWTOxnllX}Vdz{si1uYkWsTq2*8 z4S054q1v(+1~0l|9o?JPnYBvQOC#7(z>?Y+BFy>jIufWNJGB4bWW=z@kbK8D-?nLKR%_p{wc zakB!tnBiTGpYcUd@!T>&i!E@T41mbFqr`jzeOE9LLe>#w5F(=xmDb-?vE*V-H2pVn za2owiHf#q;hx$V=Rx>Wd>Fi#sXLC{ua%H9&IzWFcv1j>?_#xGQCV8hGPn6%1P;SOp z=B3e2ZtU^^Ri`%G+pzj3bs*t8gawkXDayn-J13y$TEOd$AC z4?9)b~g z=~+3ov7qOgrMJydu^wUNJ3uE~Qg7TiD*c!b6Q87QHwm3KNd#2~Ko^2EoDDIdz7*1p zGeXG44N&_hI4d3PV#CC_uXh{o$oyjo+hKd-~^)Hx6HpL*yu5Flyfv|A6stdR~ zy-o5Jzv(+p-|+T`dZN`DAd76ka=Gu)1OFE|hAH-$jDoDInDNy|qdkDw91S z6bvi34hEacb?un$Xy?koSiO{WFM}ZS4tjQTT`IJfK`?#X`qiQ_`!RIVV&ShJta_6M zLAf)6l(I)7!MHbARp&OEP>^uZ4x28=VP3HK0} z-}w`E>~z~QE$<8wT@kqe38TcCiCkX$L4`zOnZS1;W+lUy#6Z*BI z6xRN#hS}*>-%QOGFm9V=2O<$hO!oq*8KXwkrMgHR!s4>{nw7z%t*vSP3?%uQWbqH! zyHJ&^2A8-(Z%+q;xF!sO!ySPh8oGOr-~F&NAFUiJgI)pK%@ z=XV;|ZZLl)2EqON1Mu|yjNKWh?M9ZNTUh-4j%wdi+hR~Oso_O z5|LLX+va!fll&362hmcXzcb59pk-e&Vg@TJ>|lVmM6W2=rS{nIg6zS=kTPjwdXysT zE(Vdd>}( zt5vA~7n_xYBF{4&!gMt%Yvl9F^yP^$I_ykL)WV;mw`Z zh#?swVZpmT?1%Yw#xwz30ssb#QRlgwA|Uq+rCJyaF^+JT3R##xW?n6I(Hblq8thKP z-rDwYH8{XC-HJllw$B}ud&Y`5ZY{ZmyEwCkg2(Ud;`{@~CKOl|?Yf3) zJCro1fmxaMC``^r$YNt|F3ZKaNNmOi9w zkCdVg`FY1NmE<#zwdE4N0FbE!NDL6q7~0J8Og>XAkIR`_N2oE+sSxhka~6~z({w|w zXe3yU*SXU=j`qs0);5pA$Qxuaw}v9WtwC(>bBwkNVdvxYP1EY@jJN0~GHbMhTf8*& zFjG5=V#^KtBuDvJyAb9wZEELkNOGiaAQSTttSXs;kZ+^*(136>(W(F4gKN3oWetVi zsSXD4OOwA$?NWa5+|8+_M!!t$a)ol9OWfkP(Wckzh7I>>aapXUcDbEeYLkIYX=lTG zPDh8PJx%2sOPAJ)I%BEX?O4id^qgwzNWNCTLNxf-w$yph^S6KQ|K){_f%VC3yfX_C zE+IdBXh_(wOP24Y< zUy3|&YHn8+VA)26oSKl&tx3$~%euaBlDZHywn>BpcFwS%){;{ve_4;KCx zJG;bV7S~o~CVpS;>pqLje^EM)ja#&)*Z4|(k3Q18wO+fBsStaGR3}=Ce494@TIRV) zW$eez{PZ?rn<83_eVIUwb?u)sVw;@9P`L)D{7{35utu@OHp@(bFC8mwOGBZIQdy9& zwZ?jn$nsuGN4th2q%7koqrDKrYLeL;a)DI14odB7w4JOA(z%LF7^{7c9GBH;DP0&u zrLrz-ziBB8FJ#UZ4s1f1a4hwXnJcb75y?>{o=Dm!%kTY>uRO2Fq<`=0IwV4qDYWak zfmg#fev!Yp_A-(D@8LFG^5f9r~N*@VSylL z@w&k=YhT*z&hU48dB&sK1y^o-4M;Fb3){FxZ&_CZXyT8y1)rNl(oy~THoo$Tv!oJ@ zYrwXNnUkh<`Y{*BF*o+4;BwOZj*HW|xdIk0(Qir0O9v_}OJ}wg&cc|#pd6c|^?8b2of_zxo z9o~wcW0ag?Supyw?%zGC=OBSnn#G_y1s#;*whx2tx$QAF9^B;Lmc(2cD;e0ZXO5+H6^OD}3)sz6 zZ??PsHaUF7avaA^8E=@7itBmCN_Z6SXq zL+hkkSSL!<@Pkb0QoO@#w4c}0h89}d4r}gkn9I~g)7?mAgRT@Ch|jfA2qX{uz1$MLNn4vcMKyJIz2Ln z5Xk*1^%jzK#o6IvE{LH?Sx3nYC$S*=6NijpSww-+jujd3;d1bS-SYwc&?y4`kAT zMFpVY5rpUFYK)0=uC@UQBBy$&$z6ZDa%sH)dh924YqSij3aqyQb@X>?|2O?|r}mnk zm^WpLC2+iBOtK}I`Hqz-Q+Fa(jPzM=tojE316etw?{CT*dHVQf)n(S#S(%KaNbZixNR&` zOVf(D!*loeb?mFi%%_@TlUfP_@H3g47WThcZx9D@BVLg7hRy;%?M9<0)p{ibcX-bthcG5*cZj3Ej&E71xwMwApMRx zF8#WaOK_vjnk)69kGthJFp+u@NMX-7=d9lk4<-97juZb=sUHVZDhq z0>$jq%jdzK9As#}3k8Z{1g+z2F-%NA&-^?qtRWl9qQS_x^j9#X-L0)Gh#Ow1KQdxw zcx;kIthe@tM2_?$`Zr)XEAE8ij!kFx*aqy>iPSgEo{2p+3Nb&gn`%Ay5iUiT!xzVv zBJEQTZg(SXDbtGi`A=L(I}zQGhoz98Szt0T9*6w-_nL@UZxCr6J+N3O`^=>0ZU}vqd&?+Or?L z-tNz+Gh&0T)fNR9ti;O0H2m)9co?uti=<{aHftW!a4|;kVLR@!foYJ_xY(cw)*j;I z01Z#T&)WJ|)pdCsVShp_8hG6kll-;eGqzu58j|<(A1k;uFp8OO_5(LRoEOyInua@5X!~AhR9yH;S5`=yYPTeKc zmHRz5#Bb>SxC8*Yii=m%4ec7wuZb<{*OWaoXN&3iK@J8ewi|Rf4BB#ry+^Z%vQ`uc|I82+^JA=; z45ZQr&E-q^NMSbv0UvdnWQK~{z;u3Ct(X03PMxe%V<0y2n977gT?Ak7_Biq;U~P8Y z2b*tg_brv9u-#>W42$~3VpD<2-n9xXB(Ne z)xKN$jH$<&SZ>Nv;;?2!I3Csu1d8yLx^}J2AguV7xNRWZJPziQZD2tHhBfZy>+WK5 z;Qy5F6CFK{@6#QBo>%+Gn?Jx(cDG@h`4Y**rQg}jjr_7fw%)S}A}8_fF#eM$=Ei&@ zYyzF_-!~&RCCELs_4Z#Mq2Wgetm<-Mvz-t z7mU@{nU9WHjvc|;wuiFx856ane}+66(Xuv{j#>D3D#VnciiQ?pzyt7PQ7b$O+Xjr1 znq>rc{f+<{aljZxfGTkgarBxhh3f6AfZq(kgI=EDOKIL^6|aT2@g#<@dpdV!+vS`2 z13~7yqcypz#Q=Xq(lY$^zx(U_N4NXb|0vMDOaXlXbOCt*uiGN#wk%*RUs3XO>6M~N zBx)v&Gjmi?8~2U2k!D>yk{NwJV|_MZvurEn*yt6*MI`*D7-g@EqUN?IXf3bJcuJF{ z=q!esGMG8W%5lS9yCl2#8e>J@$y%RB+AN!x$#Dr;43mh#?+DoIiYVPS#H-~XNS=Ne zVJYgNhMFB{=Ez|-?hUf7A=bqVuoT}hlzrZ>S-wEZv4ydW;R-eQr9gY#2{X5qAZvMq z7*8R=p*@0#rMxY?D*Q>Bn*?YbcU3&k*ZVQ^oU z^OmRo&qGr*0naoc{C|?JjkS${sFkJvTYXO4c?+b;7@^5Cr1ih8{X9c93I+y|L6c|5 z1}2c!8a;96P1nTQ5*615nwY{yV?dFuEK4IXFsh8AKnWTRDEeWfAbkDB!U|ELpp z-m-S@R)l`LgoJb}%l43V45gOn zByIT%L3tYFOvw`kLLDJohg~lBei{k>vS5N|H1?f03j)_ejz-I?ICOVF2++SSc*oKs0sBl$CVuS(>Rh_SL-4Vn)zLw^*NQzaxNyvJ76)a zqlDiauh+#vy6ugz)^Zy)p4OnHD2*9v&XAd-jM%twuPbCL&lVUI80`gHsZf0vl_eSVqf|5-fIDQz=Y`Yn*)qFuK3WBC7tgmO+qO|mFa z@ly4Y{8lCvOaZj$qkk1lwjN|vAh}-dMNa)cQD;$(Bwi#F(ThHO>Qrvk`GUP)?nO?` ztvh|r`PN_XMGESc*3j#$tcT;$LLdMC|KFl%I9M)L8)I>-bZXV9EdT9pCTdOv+1?CO zx4{~3?|LV!jU{3s6<8|Lop5p)J-{Fb2AlRQ!yhu>McQuM=p zU8OVV|Bv7;XICI9jh_0@?WAyfirpox0B-1LHy;?qTYW$$dF;E3D*DTFpNFz-7!Tp$ zJ9LNbx=J|>`7j-#LvuI|_fRz3;a~N3hEw=4<0O{jEBNZYa)D|OsZDvx>+OEO-PDh# z`fyk1m3cL;Q9KnFU+sP+UcCzJ#n^6-sKIZI(`1_ZdIzz zDLzV51y{ZL7u-FmF-AA#E#Iq7h4KY|Jc3SDX%$*!?)4Xhg4*4x#;RQ%9=huXP>x?e&g?t`HlCs;Gk}u0$i6B!x5v!x~T! z1hFiKAwzYx4HE!?0Ru>mBT*O#h6^(Yf*=TzAcPPBA%qY@3?asKC7BLGsbY(G2gIx}FLi`@SKEXv$MQ|ql*moo#D*;HIyl%)MS71TY)J>SN5m}v zazf5pcD?y{wd$_w9jN&wVy5J;30Fw-FwizSbVEeMOs_njingK5;0c3Vb5uGCP!$X> zniuR%O=vHsYw+>%{o$DnFmiz4K;{FpWw;%^Z*B|AEV~L4#LL3LE1Ox78N|TG;6#YU zwmcG~KnNiY#xlBu1}OsuEtWaVWBi3f4BdW&uOC<#`G_~f(QOCF-W6L|+s?jNf#581 z15Uf0Tz9A&y+izh)?v)UmtBrGd8|YDgPQ&YS-}W?jEFYEN7!XCg{+?%s$p5i(+J4x zI$VkZeK*0UsDztMhvf1FAi)dxV&k1;dawJmajKwe-oN8;5M)I{n$piRRdPZ9zID6W zr*P3D>SG=*n#%g?%ZzY{5h*|1n)Hxsgs;+U;Jj8ZpUC_7hx@{d(i>t&PR+*ES=~+`i5vvVL4cFJzohpl-W^!ByfCP>q zkns>5qK>6ouqJ)o`nuC>(GxKh*FjI>88&19C>^5CvRg2X6=&_m^QpKfLXv zw+4BrH!phXMEi)(3t_md!0Us@+dX9KBVs{fX@q#)emzrY(euuabx24r8zgp!0n`_) zm{vPW|z)`5}}It-!f#UIyGC~caqJEBEP z2YtTjW8k0hb}N28W4WS2*ABs2YX|+{{qKq)v<#IQASUGd9il0<1@VTsuQg&D-L%%vhG~y3n(>>ShLJI$ z3AN}2gWr2_-Lj4;TAA-ht`Z*HY0h- zruX|gL*OFr4!~Aqd2{-ipk1d#J8lDV%_P|1l+hV|61>PdqqTm!rw6Jg;gdWKk@P}} zJThNFP4pe%pY#rq8gf8MhalaU78I$Hd9D9F(*w1H@Hwpm&0`taKSOBl!V8M&6PLZ$ z`rAA0(JCRnEOY>rm>4dj2ADy~Hhe`K4GI8v(rel^Orsx0@hHxWHB)N;>2N_~RtI*{ zSOjD8A3_J@m25515g!NvP}*nxGzEyxO4D>m7#7nZNqhH#0@J2e1){ZnyQc@*nDN{j z-hqla%NHW|Sz+zulseii2&Xt-Wr$lK&OkWO!(x?sA!k8q52((6*%vviVxu$GXoGN16&*dj3w(7@H~CXNogGdz z4SH1i6knultA8`-*b}4G4J@;WpxZE-5&D`CK(hvg8J+F-p&KW8WU^ec_UQqaF4Ix~9X!^qT*rF@>tJ+e^P};++$BR_2*G8oU zd-C&=0sxtyTMLHtvKve$It!tSZqy-($d9@oZdKcrB)k-U-e%|}JKdRug!_8VW-|0*`!V^Xn-LJni_en|!xfbeOi?O=WiXYju1%M2 zX9Ou8qGU1)5-(9)>S^hmRF6cKCm`kp#*u+EYXYw*`kiQP?C*LtDU=`J=Y?Ip>QrJ2 zTI;s29j`PcyOW|9qi+9iESi)@C;cuXdg7!G8OMkGKkXlqYO-s^65UJGB;8uoCqMOsOxQ2b9(za_ zSgtp|AQ=0-*66pO!AQBh%g@xv`-J+VhYZj}CcqbMCs=VToIOb3|Opf2}ukTc8^GqXM`yFM2B#EIEck}K| z15>A10fvH4w_>vIyu*0EKp$O|`|ahdnA+{XHUV%>avPQKHb?oPyp`9qHYtAwNP+=c<$r z)`}tc_x%rXLbbk9U(pa5(7TVf%08Nwd^yT5*8BjqKi~5Aw~6!cvt;%_?)nYl@C$vW zEkfYOL(0(FwlX7NgOt-APRouAKl|$S?*GoJdq(bpN`>~Zf_OdrML-^?KBR9^lpgnvTSBJ zr3KMlg}ZnkUa~O|Rt-yVp-C8%d9xMwPV+TG?jCb%L#`d>Ylf~Z=GKPXEzH*pT{~mf zbCSFKd_x7P8{_Suoy7wUBzOC;+1o;oH8h&)#{PR^6lIc?H}=Q6@!Lu6c>|9Gjmn)A zk{k7Q-&o%yv2KjNE5=bK*?Mz-OdG$QTTWke96jKvtIly+A(1EiBSCXYz$mO0b7tjigr z_4Q;1NnVElLA}0&0Fl5X8Q;b zgsYPoGRYbZxM7%8XF4kAMz79W9Mf*sH+7gqzMTtmbYk zBw@q2%c{AJN;%Fn>Tq=zAyLN9je-W!|;o(xs8%p+%)RobuJ-MiZ{qM z=%_+1-8TOCWTwjMyw3H_GKW?-FE+D|xO}0}W~+UtCn`O3idbpy>4YQ)on}{AGkxRa zL6~ItP0ig{NMVL?msPtNm2#Y!+oTUGbrQ4Vk2kHyII}gkcb*+Ion5ibbi%U>HI?Bz zHJ5NHhM7)XUhgJU$Z*Dxlk2;P2qK($d|_&LW+DkQhF@&W&6LbyJM|`kr~r!`q&|9W zU*$M=yS~%p(NSW`ZLcGeICQjmWtsJvr3Z}?U#&AeQ~ATqYB+t-QKhp?+JyK@2{mOq zVFNfOxppHIvHxZPTMwyIyp3<=cL<7ay2nkWpgY^gtc;P416v)IDH0taFE*wgdAfrs zx`Ggj|_WvVx0(p-+}rwl!yoCby8&muYBtmh+Xh#9NES-`hNO>+3@ z3CVkK_Wy$n=fNZdL?m2Ekjd*z5_<$Tt=z|gop@xjX6=u`&Xx-sN#UBA`FHW5GA1)v zGtix&r`8iEOKhJwc~PSCtTTEm4)3BaaEl+=w!z(#UOIH0O*|yD>59{Vn%?@n&K82K zD5KP1g2RJcH=Q7UysExqXvrybK{8Ax_pj#6b1~?vV-}v*4n~f+H&H`$Y(|aAYq}So z*~N*)fIKL*Fa4kx4gvs7BnQsS#|Rin5FoQeX$+j&4*jf-KIpR(1>JQ2;_sB(vi#68 z-X!`!pP(?-Mx3zE+63=Kc*I%G$8j9voYE20_jVKDBe3JY{crzEYn$wy0(TjK7WNlx zzJR&%r6!JI*!XnuzO*UhB^Hij%Ac80VBr8=k*vc=xgC+vM^ru`2ym6U?G@EAo4rol`-RGS}qI; z=F8-kEp5c883I+viC)I<=L;GyV_y6Cl;RqgXhsbiS(+4vVxpHHSb3{`V1QtvncR2A z7M@|#2CRKPX2dlj8x_jmp1vO*XEG7+@ zFK1X7GGOQ8S4@&qr#q#3IlyKG&dTWM>cUlFRKwVerh}r zC^R#qX1wLX{P?v2a}2!Qz|seetb~n|Uv9QA-kC2poR~4*88Lqmvt`RH9KrMmfnggn zY5w8^-b64j6>dIe#;^r8&pt1Ad96l;GOuCeSC)B=`HRpuf*DKsomqkQ;lK>03=2M9 zd_bG%#S2J^YF=@|Hnq^!<>f_&^Ri`2n=`L6Teb{^Vq$8%H9#NO!dHT4y8I9MkpWZY zHK!RelLkzgK_449ei^f6ODx>71q6C|Dap_bm@+dTFE(nb4dRKgk%=^D$MYv|`85Qr3~jV0_@?8|{cq)m*?zH%t% zirs8D*Et+HlB6djdWQ#nGu-&XQE}ZS$DmxCD>NbT_Q4*-X|#n(pxEt3j$rMB4dr6B z50?9cN})0q^m>W#<|US24fe@I*as6L%b^%e3~{-yW($Ex@#j)@^StmU76ZrJu^X-g zt=z`i7|Kdp=;2^8(I(cs5a*Si&HHGx*aya5qezCg&nrPI;0s3vc~*u%pz!96wAr~b z3>uGpU=6H^uORUibPT8@y(_aY=(Bkr3ey=FdIKl%wi?{DTi+0cmsl6@xY%j79<4P!mJ5H##0^hDTAhqk0;v3S_~9zV(jM@WE*j%Z59R%(m~F zD;H&AdF)D1c`d}1vzL(Vvwc4{oL5?TW5vpC=8<6xyh4895A5^2kp_E>F<6l8MUo&b zCW)tblv3FZ>aTGn6BT#?KRl z#8(aU!kakWXf)jI_07ibH5+X>@)}r@w;-`*6B1(}mNzdj7LuoOr+61*I0%!QHHbOY~c8nw=hQgV52t#j-hN6%6+A6h#YOUaE7DZ#8_+u zUtf6{gbTs}J+ucTimyjoX(LD1q(_?sVX&|EjUvpmy=u6ha*zaLVNaGGu@)zs{Q~1` zvweTlzBaCs6n|!uHHlqP_ zkuELet;fm;1lB%q#MORaUoB<>OEP8yz46-!bA{xL94}=ctrTD1Zn9-G5S*NSG}_Q8 z&cO16*=(X~TPQS?vxPz>Y;=t!$hB?uPY=XpDIzIP=8Vx7r8VLZNSr4gHX!>?RY#mrWM~_~TR5|Udn8E%NWN}0 z8P4-y;jNt2Jlr>{-EKqQ$S+sIK9~$fj_|_KhW>$(IkCLmZp-4t357`Sn|-!HTL|m9 z&5#2pYvFhsqRl?rmkVVw9QkDwb8)=eO^hA0fn^R?%YCp>oPoERO!g9I=fav8#&F*- zFebuWAW6gBYN1Vx6K5!lGZbei7bnipP|mZ3uX|oE(Kd=dH1e~R;pfG~7j3cz*7j^* zEhJ;dTrl#=m2#X}c?0G^8?F?~X)u=z_lnWNGK{&X9A*jO_IkNu;8>PpPtVV+CYCv{ z{Ae*+=-FO2A#r@oN^yjJqV4<2_re)U+cyGzpil_|gI+K1^?JQtukc0=fk2-Z1eUN5 z9D}|g5oY^_MBM9}fi=+^0`h3z&yB2wL7;4z(07a3zTsd2dWqxBWh*^bEc9SEpIS)X za52}-b>(LJVB`%K8!B%#&>O{%W{bsQ$BxBf!@e6x4qfT7GJZD~5X=K%;P^v=H8@$` z&}t%#_76FJW9$=Y;CP$aLNH~#ADB3T--~=2?R(KgmA23#isiT( zD8tb}P6IeD-eR_4WEhJhlDHV{+lB*apcn&Rw9kv#XdnCWkw%s?z^+ZoMDPPcz~YTD8SWKFgw?*;Kra|<=pHLWJyvpF&(Kze?#XQ>_Yeql zrM#!yiU@DWkMu?ucnim`Rujb=`B2XL0fEF9SPtc4V9-$Rn;6DuvU2=LSxB3WWf&yZ zhJ{x~(X|X5W8eq^*>FIl7lLBxdMH~D?g#JT#!DvV|h51F`6g@ z+Hx)`ITw{n8qPI_-%l+}Q;In-hQ$TO9?F$4u!MnL6)SJZQQVaSZ{rvA2-<9+SJpzB z%kylaH;S9>G~TD1>t@g!6D@Nsgh+mBq>%*I&8P(__U-4lKcPoL#xnbNgmi4nJ59=au5n zAPh$i1$`wD<>Zy}H; z8*wv;qX?A4eYcP1@?f-Y1ZiSPdN}ci7LvopPbMVBz7ICTiKR`1c{DI6ls$?gNNzx7 z1Cq)&@K`P85`Rd~^I)GV2KzqQ2-d!t2gbZ|yp3bbHtyO5l0W4)^JXA!7PRLLfk2CK z!rP=5vw@`d(PT2*i*&)npxLwW14%L#+QefaSPQ|s5_4gJ(_{N?qUVv}Sh^-}Ggp|U z$I7kd#lcdFal8>IHiADl^4iag_Icz7iw%`78tj-2EXRRndyLnkI z$5%~+#csH-6mPVc2lVoQfPiuzkY@|O+y@i#zA|PbMHodJIJ1GY+YJY^iLgr>XbZyP zkwc`6w27namHTQV_^RP-wb1r?;>|V-fkxYid+p7_A2H~!^j^8IHionE_QgJ1DD!f$ zNfI1|Ja2}VDDz^sTb!Ys2ZplmCBi(KOi0XuLauQ&&vmRObBX2nd9|7d!@ZW#zL!Xw zO?oyE1V?M+*ETmnAGms_j z1R=mp_!BpJqr#+d7cBSTwwh%3LZb}Mb+7i9Zj zF4|279<=M`D#bGQ)yfdaEF$2J*~XyohVxutZHRnmEWw%x&PXx`20iI=^;r4Y>*=x5 zV+Hofg1{FJmJ?sE(slR>UqM5(u=dF=ZK7-;j|P%5acG&Cqv0mS8x41C6oq8ZLfVJ3 zm9WnSj-Hh=2L^#J8tto*p->5y;&^jC1OnN_G6#;}ZHAW^1is2J2D*NYINLu9eJ%#O z-CnMPL>rLnAY2gcARJ8$!jYxz_Gt6UuLc8y#Bc;}|A9YWk=^sNm7m31_{nG=?RNXT zT`%z)!P*CN`LHR$St%3a4CQj4SbAc4`)Z42kS#opm~Sj|VD77#c_H`hLhjp5?)wE7 z+ysTANE3r@IFUvZWdp%zUl8~R3W>K7>;Z$u84dRuI^z>3XA8e8qqr+g2?t9q9Lulv z_?50v97oWzg)tlGvI&V_`H8pi@7J@(L0A|B#=P>cR{Lf^uCMf1asAlB+E6K$HxWkG zMj06N2~E;yIGG0|im;*F_nM7g4M)qBHKA7-{s;lVKH94WdmM2v&xI4ia;O}|b1pF) zZ$RYiBXKg(3u!l8>=7hqBWM#6U6bE!=%ZC}c|WqJ?a^Q}yhITuqk%V3q5|qh6*OMhk4uL>}<4w#6s-e+9m`4M1V$cXdUM$4XWaOa@ckUa1 zNE#UAjbJQ}*B`^M4+!p3r? z$4V6-kM@N%(U$vWHj#Gj3le1>I0XKXp{(|eqAirk#F!_>g@B!O(eujChVzEX5GK;Vpl`UzF!y<}@e>+F84cjjOB6v%6)bO# z1HSftNU-MG<$bW3Ei6lrblJpO7^8(R97z*lplt+!yb(ux-Nx@H2HJ3=NRBvIk5|IL z8N~5c8)4*#gTX%8h$Dw$;>#n+uLk03AxVSTLJue;e{AGIBM1vYkM_;Jn>fy5pf^Ou zz!C=9Y$A*dzwqUOBQA^+(!$qQlC^Lwy$>8e5(l$|H*bV_Lo<;k-o&z`g(XP~VK(qK z17}|?oOv-Y=p$`7kNkeTAM%U6W}`PWejs_y8@<~l8RkHtg+!VxHhw>UKR4PJME(qx z-$>%fp^pZZIGRYlJ`V&EU*aIp=ash5RdokF0%OEcRW} zK9BZ=BUpYlR|vOnhGhhUa<>vE2I0UFN0LD{kaXq38!e1o8AV+Ak+`3p@>FpxJedauf#f%W|phnCQ!CGoWYo|)Nu zfA`)VOXbYvvG+`-q)%M(msz3M*ur+Iz>Ocuzk2X&zhL=#t#>(zbNu_3C*4{g>B??A zj1mDKLg}d1CTWQStiRR`7Q_+41zS`6qinw>@F=;zQpRm2F-rW-`vd3V5gudYPqCqy zJ?@H%&vT=iQflcg3=Ux`#PH1%M$>!NMH;^JGh^@Xvje1Ctp(>)7&o_~RX~wSa^^N$m$CJf-VyZ9UF=X>2n5 z<}q%N2g?+QTl-BnW-`xTosKhfmdsBOw(h)}4b5IB{8N+p_m$QhW0-!;l-eIpY?E_( zil=;Yo@NPw%bMkls*K^oY07`R9)ABCATrAKn|aKDPmA}@hjgruuKqCVAZ2@s>iu8q zW=drVJE0~}do+_KQKd-KERH{*^NISl(Sn-hnw|pJqeQ(OaH;59J;Bp`_?P~N??sLI zj>5Ax=S(Ad#GfsWD0H0e!P?FFo{NNRw)`5!BrBptV%GTR@+{z|k$tGDaF@zwz`OxZ=+Lg=oXW*Tn zn;GN(&P`I4G@Xg@N^LX?{_4tvNU2d)+QcveHt~i3q6u%}pIe@f^WnOsnnK{!Vu@)r0(7z_R zrZe|z{?+iK&x{3mf^)ywD26M;7XBm@ZcKGuGn~hm&CyUX{$p{kGo=&35kiz)Fmfi) zWGlZH_#fhLU0I%Rb8iPIm8g^UE1ufHk(u%3Hw=?cmfjOqz~j+S}3dA+WqC<%cL z4=&i>ZOjA?DN}Ur``=y86=SHeDl1^TYPh4(Z%{%zHWWscWVGspzf-#z?%uok7pieT z6TleufF2>@JlX${E&5^v$$XHGjvj_7uF>B8-D;G{3SHu*^bvpE|7RdH=f9W5`JSst z{o7$L$LKu&Z9>)tUFu`3xuUFT=&F@KNN7yl+HGH4ik)Y9fX@N4U z_!IPXXiYtLU-vR==b%van*`;=X_r-UN|NRpx2p4>7g$rpf)q4!W(s!83&#HE6>;-r zee9OmX-C%Akyd<)NG6pJ_AQ$+QpIBqqE9(?pk>ACO;IF(zem$R#`S+ry_Mwbjs<(t zd|T3NTha_$(nIHPEWWw|?7PS52s0LRH-3m0zUOI6`#voRyc=W8j~AByou&%SmP|TG zQ-o$8CLW~8LbL1dm?Sj&`DBCoV1$ufg~&E6Tk@`5ush>0p)alaoAK^|#QY2@ zjZc%EwLqySOHQw;8_okYeuA&>GVkn#JAiGE<&EkUSj#^c`If*f>{$b)*ZvnN7MTZD zWlQ(8<&1ov*3XGNU?B8TRBMSxG?#};Db8{rRjEyUDK2ypOl>M3oI4p3*Wz(p=S~Kg$lO3qUsqnzy-4!^ zdqN455e)fvrzOsT`m#+_+tiL-Plf_hm=k?Wp1M2FTK_tBc_;Y6Q)Utrf0d0}J6iz~ zEk3z1lAN@Ne4pI5peOtxkvlCf@r7A9?3t4qRxK-7YyhkpS6g8_-MylWEzAVQ2LgOt zN0BRv7)P>2zW8A3o$}k!YzKv(%Dm1LT^S;+1^lQ36Phg(Y3|ZvuF^)X(jcxp#*LhYl}=$T4(=x$tJ0%gCTEmH_J$|23*)O2{lKsmD|Z=oF#UrkJI&*kc*pej z=HHS=Rz>3IL`-}mDdF8t!o;rc@xYNTicQxPXdO*hi=O+*Jqf!vnt5x(-IJ8Uk#E8E z;gIWGZ0bE!SghIfGuJTWtC-xF;iS)`6+U9gAegEDQb=tkb*YN~NyO8XewsIJh8nJMi;R;4|ge&_}LFp$2Zxs)Yd`P$T{|A^Pl^iN_r@hi?Pt+;;T?MKH{TueyT zR!gWflLF_(ipJx|H@8Dz?*87JwV_q@r!L)2>Z`{pEy2eOA&wU-stP56x+*RHSjQ9` zhP0x*Xt95Sq;2pfR;9Y+GVE8}?o}M`Rjm2ZILM#YlPIJaLUgy7Yu#!}NGojE!sNhp z4n`{KSCiO&2BT3S+-3X@!AkG5Gp~`w%(bUV@aH*!2d$L{sy z@C~lUP?(5Rmgd|J=&DWXE+@R#UR@ioSLrusV2WLa1)s>AoCLW#z6RO%J%Ba)kXh8# z<0^2EM5AKgPDFEE z|3sY1qM`~xH7`u5k1IP%SuBXJ5V)FwuU4BsL}7hqL>2_@_3jS=SW36)l|VTaQJ!KI zo7Z$i0m%eu8)E=e*mdD>2z=x$KDdrf;9^;8v*jWsxR~xl<9?W-DTJS$k^e0|cxm~N z=>|(42#Mqf|3TN2Ev~0AuD$*ol86UT5?_&raMS%DLq+{_t@MLK5s=D##XEa+3+*(3g**gDk z$g9}ZF-ue?OnhhA7xF?U{#Dkyz<&FkKGpukXKSpRZ&|W%+B-i8&^~2ib|yJ!kY}B& zpWiU^&7(+f**eCZA5^3$a;UZ<@n^;e7JtWS&9XyPIrewbbA4YXYg=5QE!g~cU0G5#G;jdpv+#byTC`JBP zopBLm7*hONTWHw}(v6MPKF{d-ZmdBxHXZKh)73uv@};8d%RG_gP_CDkcr~!=oJr~X?!VTh`0^r377r-JUBLR(LU2Q08(@8^;I#t z;VTNJ%}JC`2?X!|7{{+8aqJgS&xkC9uWw-53L&d|Ke|EM9GG=_w|SJ}4+~_JD5y_v z(R!+LO8JKgGD;ZKcOMHVI^h`T>I?r7L!6oX>UhJF9Qb?iD~uEczyoMQ4r2P4iP@81 z14&W5cmPdE^x#()QWPg1Kpmp7_d^`2O;5C*{CbA>pbDWK{HjZevL((W;J(88XoxJ6 zUzJEvz>w7#?Uk=BMC+@l2ArYCs5i!2Gqqa#WBHM2?}9eqNTT-*{1wDTqBrVlEplW2 z6vJ_uUmSIAXYJWLVW|c?qV@zHpiy{^{9@NlbTNJjQS5ZSaJPyeimlJ>7<9@9?9Dy- zCM<!NSW!FMMe|5~VI>_@>ft4seeb7& z&&VP!5glJdWqEYuByY42f|z) zRDMZmF!AkI!iRj|#l>-F9-GC8L&Y-3v0=4-s z=q)s^7_QLuvGbSRnq>k}Yj}}#G`sl4{);^+@VnIaqm!fGORxEc?KN$lL2vgho_5=D zW0$5a-JpS`#qA_Jb%iU~QHhMl7COkYWFP(9GiD#{@5xz|KV&(DFCbWca&JJ&Fh`)h zu$NPoAKm*htKHqf@S%bVMYK4+Gr1U{cpozL_=(&fy^in0OD@^0zx7O=Sva@M4ZH1R z6+WIXR1~{5grfyXdXJ&B$Aj8)EEVA-yVXTz6I*G8h5HWp1TaGki zNerCOmVrHf2JojQq6+X4h-kaba&~B)=xOF|c`p7UFB*7mO^@%$i&myc=o#03NU}C2 z9>qtChp#6@Ak**{xvRO+>YMO=AR{b*?R-Xn>Md;S*|Mfi0FWAGO@ogh*C6P5@{0?e z{@p2$!>i=*VJNU~@G9h|Mr22t`8hwU$a`ie-47m|if#d~x&!vx=4v7q$rSvmU|g=9 zbFM5yJHXR!e0C{QY`f1=v(Hj(O>KTe_B(1GHIHJ9Ku$h6pT7VO*1!IyH1u!mP96W^ zm$9y|>R+6FfXm1kRU(S&I(_V)F&h=El`^I!hG9;Ja-CU~mh?huR&iG4o9?i{&SyK8 z11-FEnYViY513phfQ*Pr{Gv0fuIo1S_#)%vavRupJuw>jDy-a9r_==`!L@iad;x#w zBwOkzQ0jnJsdG-28r8O?dF%5zSimjIM+JVA>n0DtG+sKhok`TU~tq+vuMpYpqkJ?azcQ`xg~H0b1%|&;zX( zL`8a=^D_0!UjC`(t(^b(ewBq0wN%S{8s_}`=d{^>&&d9xGO76@Y3r5rqkpLYaZgwez^jEF=cgHJA~Ac^6n)zKgAOHV*h6P@}_71 z6_#YJrZUYreMPWQvFF4}eIJs&V9Y0IM-b#dN`6uD?E4?TrqKIqTujv+?FAG^i?0vu zo?COoRVXsr&-qt;1@%s=Zk3BInGZo;-8Ae46deg{J)Rz7+5)z97|G(eu&-$y3N?x%TOc&GIK0HW1AttMVyX zu@uKTMYm#9^GT4LePnrq9D8JW9qwCJ;!!#JL{#@6+hxjk|4_@g#{c8YPxe>4T1SyH zpYY_zHWjbWtrO^n+lsYsrpo@7{w?{-eGMWip{BUHlNDSDMSlzba$ZZ(tIR%I;U#d! z!Vq->R9MS!UBp8~pN=wBcIF@(G-u$o$YYUJq1JKj$D8GxuutTx?7S(gJ#piahL5&r z+!c0#LZxJTY!7_dsCtw)M3PPyY-MS<5OexEN{chDlR z|IT9Y3qQ3F;e4l5P}7}%Gow|AQ&5SWH)N+i_>8j$V|`>qN)pT1AMR66eDvAO=pU|A zSA4)(_UIqZQztz5%zE?>$9s;&8tB{Yf77ESh^5eg?KkPsV#Iu?>Gr>A(IOwhS$~5@ z93E_!lapo)UhD~epcwhT;nU%b;o0F!0A>nh*P1_UThv|%`w)(7*$k!KP^9}BS{U3t z8HPQdJ0PZDX_<$*E#KZG*}o3GvF^_~RmI~@wj;k@tre;Rp5HPp4j(;U{0MqHJ=kG0 z6~+7$_={%vzsP@I7jLciJs6JETz%%8WN#0<2+~S=+Wm6~qOs`KP29hI&NO!3LF%aT zdS?@Sleqd~P)294^Y!Fds7wR8nphkS&rJHGSR?Uj#F``7WG<38G0m*KI4|3le3sYHIh$LA*+wc|gNLDx{5 zSL#wN*sHvsIEipewN9<^6%x`9S83yc1HMc@_9&1#@aF)G7wFL~*VysG+hm6w1z?k+ z$ys%+^&r1QrNI4gcaxYG_eVr!Eo2cQ9cFO^bM&NFLEA&Gzf7afOryR&k~BV&EZ(Be z@7%+T%{nuyK#~;;d(y$`>{k)r$7MARFiiQz{<`(ycut34LTFm?C6DC4tR+l2))vSM1dS5 z?t=kXWn>Siq}DoTCdWD_`@U|kR{6tqfh)L?#;1IQ)Duj^kzR6&q(;bMmRYK8O)3S= zV&IjMx__5S#QE!?z5>o5%GX z9-DZJI{EtdWYW`cC7oqA4=+745a*I<^ zl9c1L{g-K%Cmx)~{tKCQW1=d{Am06#Ca~-Ve*-0##XR%196u!DU6B8)MpSNtzllrU zI!G;F^`HN$vUer9@>92^^Zlo_Xna=G|3r*^)`y?Dmp6!k$!zStRhyqxoc|VSmWQ4{ep+FVlRAehW*< zlX>F#DPFymE-HyQ1&~#JO3B>e*qxK$xOhJqhEO=@PQEBIj2O0y>+*B<1sf%P*d&)3sr*4f z^wnSdHzRdit~jNsnrawFu))17Z!_gInz^^BCip_$eE+GUu&E**q)Aq>632N%O7PM* zgAyJ^Yt4=nxn}Zw_6thJ8gXkR=TN6&wx@{zO3uwhk-MVp%@$I@K=>typTzY+kMi&i z?MvXo^4^AD&_5L+NQ?lsS;D@i4kULg2+a^aQ(o{21RK0RIu~Q+4Lx+D+_&I<@~fe( zZBvE!1Njs*Xe_>>MCs)#pZ+X*@2y#ARk|=gKV$TbGG$1>NE!^CzGdXfti#?%VM49$ zyLA0mX1W+$^7MbP`0H4vOZ;_wpnal`MQA4X9M?kseFE}qDkb2BpP8a?>BKqXg!~VH zsI3>LZMWT_WDM9q34TL8xijB?_6M=}UfhnQ`SAEMQc^LRDm=ZrPe|~cz&MTQV+E~T zEBf2aX9G;>FJ5@Qs6>^H`~Q9JGjN?&Iol*2XB)9^$s+0zD#Fm#kiIipth#qVxjjxW za4l}_jJ!z>m7eQoy`=CTna9WP%GJ&f?HVaO7R)^y6_4W`R1=+{set`h9Gya{1>d2b z)t`PIA5c?LRd}~h2WlY%d~U7uC(K=!vwlf?u|)R9(_862h`<$-m*5rqob1i>=X$Lv zTx+dl;^V(k-tnpjFYOMg_^f3kRP}s_ILa` zcY~XU&cuqJ3x5rzr=(mh>tSZhcd>BeEs=~teR<(G6YF1cy_dg_= z)H^)-qYtNn8NX{sUr%o9K0B+EA?9-~+H?L>U0oAiQoXn-YEV4bIb0bNIw%GIO*JaB z2A3e_v;S5-Q$DLGg4$S=A)p#*54D8ODzc$ACipsGB0KA8VA^l0_}`)2bq|Q__f7-f{uVvZJt9?vLv1wib^OG9=F^7M-y8?J%pdE%y=IHC(LbgCT`_r- zURE)7l%9HYq+Xr|ve~}$J(1O1CbIvCp@^|j!Pk*=|HapRd5!oeLDC)VWw37jeHml3 z@fxueb568pI;Eevq8ff3b5Y0r-|o=m?!ImpsbcMKddlySecc;;o!aUm!>Rf7Rl$CY zL4kw-^Um%DpZe!}O4sPS}5Fgdk-J?}b| z9akiqS^B2!C-y<$J)L90`g^{c1J&{t2Ltv|))K0r23s+}swD5BdVBgBYtc;LOWx6e z2q1vD*Xfg8i%~g4$Fs2M*Sb+g(y9IbmJ(hE*ft^JxllwBhkg);x2RvemJmd!fDYKf~I+tRK`d zFoiaf<=9h4=e9x-&8;=rY!4x2lE@uZG@00=T}`X>9j(wZ@yd`g(NIH<1ohh1ux*ze zjs(7p1xe3X7~w5%p0`VsoSEsCi!%3@oq8KglOAe+dT<45Tj&E?0Dc4ddAVI`}J;6({=z697_ z0?aP)J}`f^_RdphU|ID*&F{QK@y zKPwqn7M)n;nCN0jOr}Siu;#d1vDJ&4V#U=>%)ClIbLfwh)zlbfN|J$CpYH~F?@Cu2 zfcU7Wuoz=3h88bwj1{MKH|Iz*A|{Uq19#PU&rA47B@8gDB)qu3Cp)F*B88{D;CuS} zlfyE%Nk{jNlP2!14Huk~$^amS?vmSF71sH|g7{%YAulD2DlPlN5Fi-~M%Od{!F#Ek zRp0p45s3|N9B%prnQ@8GidkKKp#W zMdQv?Rbn=o!i2wpNcK_Ys&8sT=Bm+HFyBDwSa*_DuYvJfkaw(dihhJRIbP&*=>T+* zn-PVt)VGhXK{sTqj*JGcZL0;Im9!&ET9GAKWJ$B2l_SW?0c2&57nkz1=Mb0lTx5Sg zK@VIIuSoyrd3u9+`jM!GA)A@Im8rXx$&-19kH9cLB zHggN}VDRmh+Kryd6{!STsfPg&MGw@DNG0LjC80>`Rh4g~k{6X*&LFkB=fhO37SufU zAR~%iVk#Txf0tOWYpG?4^I^eKE2L@+!q}NaI0^?`VL8kT3Za>#G76Zs#MdU|pt{04XT0Tl&V6Vo|54LK9j9u<(g zx~0w$gSf_Tod!O8#>e`6`S3BDLn>iebsvzAj2^;^D=HxA_Zcw?OH)jTE&))qc2JG0(|}@+Hzwmh$lUE!_A9eN0zV zYr%*wm)c!UO2bE!d%b}lCoVAy!NRDhAigVt;pcpO0CMbAa*TH+luFvh_gb(+t<4eT^^hbxQ86=&U5u}*>j*uw za=d}kKzIUm4Nw|spl?pG$|hnj?#x(NNtIx1aqk)-TuuG$jUKvE@*W#&<|8^2mCRh| z!{_soEUlq(TAQvG$?C0?evnjh*5!t*`sXLPINx#qa1Yy+@A*tM;2cQIMSNYnDi4`zd!SmAAS875lz1qCMbQFYOm%euh4bB) zYakWbK`s4=E_-r$QduxdZ!B7Il?@quG*zHCF_o!D-I0e&vpqgXQ1ldayIV@STS_R} zDR0RYl8VfbMIS(xu0*>coa&Gp+m@w}7t9h&w3{t%)+wOB`}iE=I*sU)?>%^OUb&>7 zo`l=IU7&$$#!rH%SlPIIdTIxYm9N=op7w59a{U!TGxQq`x|JvoF6Sbso+8f0DsUWw zZi$tb18(r`0m$(@nd@3l5np#vrMo3lv$=VIRnS9AJF|wXhLIS{CR}ZdWdklb#ZM|sMA29kd7!;ohUR)kO`y!BSaOX5XDpz zmNC0xTIUE+(ycyxz=5nGuXtpB9rlG)Thj67HQA<;#J2BMaQS&CFC~E6F}zic<9n& zPsBq7b8>}-oZ^Cb8H%4u)7qF>0GREe!V%f(a;Hy@hS1R&wHfRM`2ygcLHmwrBwiFq z)T(fZmom?mlFF8%&z3sMlA6epYRHmG&yotvk}}Uy#oGDJyX(B2l*u5ag!(YM$Z6FNg=Lk~DbMTkP2XW29ENvc%zWD4eK#T+A+6x(9| zk`S?iSBlHdlOUf8)EE;%l}@iji19&GH)7YlNAM_AY|m@7R3=m}sU*yhQ(dPaRbsRU zk=m-cBjx|smPRUCjQ(S-pt2Dwk6!dxv_Hg`pcHD)WCs(=Q_1Wz7tVT?J7QbTYs1ED z(`%(@w#_a2^-q~6;d5=GgUI}YLyM9vu@w7^E`q$-;~KLL8S^M%(a>;x__Ul^YXkL> zT8H2pc18r#1p`8(MQ~KHNpKLx#`HMmqE@4p+e}_(VmjXyYF8@JMiN!GIqyNNxSPx1 zMb#9frY#K&fgR$aDqsWy*p4Be!4AbPl&8hIfMgd+*J8Cp%Fq)EFZV3Y!`IuzYvm&t z*ovj7=~JT3BnXlqsJWO&1YeR%l2pHX5`nIY)>?&DrpxQVQt@ zV#XwdU^n7HH=^3zoCRN7F4K@uOtmMbL_$D3?M}qnJQt3|8a(Ze#i~3P0xguS%+bY0 zG3I{|CJIV4_6&$ai^kfY<`C5y!Rhk!@qBCO?ZgXjU6l0;iBnk#b~sF&q-3VMRSq_An&A9EfD>kr!G7`1U4(ipdu} z1|XCudBN3xcRU_Dk$?RZ$sX!Kg`av$8yz0Mnx+e%+gaBkstxo(@vlEw-U}7;PV3s~ zjdU;0KAAlGDX>T>^o_2XOmF-w>7ItNw{SL&R#W`my+30tuf_v+eqdO%Hd|k~hqP9u zY8v*;^BHvI2@25eHJ-iVQ_zwpCmQddt5icA#o(AM)!LV>s0bG-jyw2r9pI5Xy zuzk0VRw|9uUzYmnqiz0&IVr*b_!bE|i-e{?e>wXUv1yK|jW6@sGQIyTY~1kt(Z1ci z+AX)0I1N<>ybSlCu)@?Zf=nZbynf1`G8JL8NyR)ao2JEh5lc*sMVv#zL)&nGRX9Lj zc|z8&2o+GoYr6?78E|A%t})x^QMv#rtxEY*O+@aFJ5DmG%rad(Ppx_xj|xcau~w`x z-7iLi6fQst(Th{|hvRuiW)zFIRM;95jEG4bRl+-FcZp-5>n<>6pt!6g{ce60F&5{h z{VSPwM3)!M15h^S;SCqAp&_s_^kmbaz?B468klJ!UW5i@%7~;nFH;HXROxBj%RWz$*;U&lX&HY9pK~+Pqpr+pV8buR|Yg@87spbp_QSI$*z{|7jPN=C1-udq#{GrPkOO5nT zZMy$XiI zO*HW}{KP!w)B4oY<2e+UgQ6eDfrUPxH#>YaZ?4vhVZXq3PJ8HM4Ib(!sdZF`*^N1T5FM0^w>0Y)(*pCOv3hgWgw%CP<>rql7) zrb@D9!NDeqiCX}oyQuZF;&oTfknUX;s>SqjkN8PN42dmLY*bZSP> zW>E3=F!hSN+%xrzTk<<54ETDDpc5NZW|w+YBU61EO(a1phsh`7$w&3cK;bRmnsSRh z$kf$yIkI36$hQSZCuJKzO`VCn9xzkg)A7lIFOA`V1VxvJ?xqjiO#yfj7WdV88WHKm zJeoyYnnhNch_0ZZ)O#~^^=!>nV0JDmL=S9J+)Yxizys-Iy3j*oFJ#ax*E8`q@ zz)gx($AXH##_ab8)7GL1V2%Xv(s=C!=+;`=+t->7Urmb_HYQ35)8gxUB=e$;((u)W zo_RtcltwAB=Q9qfcRm${K=Gwe!J>`LGINT2Tjsmll|5>5L? zwebyU`8rQR3_R>@AKKeeVCax`bVjlGYD#>yE>>9A)AqspaWY{Y&w0T6@occeCzDfEf_NF=$llZ$~_93pK~ne zgC~31&dCgYtUl+=1J6S_=SlEa@6BS3KY$%x8+U@KAe_^ooGA*oD7Zg3@4Ga``|8g! zi65(1Wp1&;yqvuQy#8%6LS&N(KQoxWQN8cew zhanl^E$Ov$c&7kR4-^@<=~Xc~SUZ`JeGgpz!1FC_@eE7nE5UYN~IU{@2%k48$!GOFr0 zc@^!zu;0KD+sJYF7eCW%Q7O{IdKljcxBd0y0UGbE8Nuc1U;q~u_@a>m@D3a>-PAGUXgcjEG3`h@?RY?N z{6vVPP+qou)|j7YjcQ;)HOwFzvXKqe$OaZ9_<(Uatn&O2(sLXNt2=aWxPMp~x~!Gp z)WAxx7vXUfQLHkkv`=KAe?L>ylwH(d%~j5d0SdcE8gw6f!6LD9_aHG?C(m-}&Pkol z$@_ECKtVk4As+Yu4+O+k4i7f%#6+q?BkS=%K0J^Y59GlEx$!_QJdhI)%J?Slmyl>7=Lo8IxLXp0NCAia>8fo4dz+zI=T$zkM0^>s}2ba z#s>`OEAy0v^V)XT@+F6i{K)~GWY>-3dWEWAYyu~I!zH>Av+I@}Ye}&IAHeb5aEWXr4&by{XsLB8QSfTOtK65P1maf^$!B;PpRaihQH zqUay}gkvGK==K+W!ZDHLbowJd;l5ux0p3^dxJBSwC}J(MLq^Z!H2bf{aNn+-_&2= zoW?E0GEU=WIthOrw%nvTZL2ybV>1Z2u~X)I+#O*G04;O-aS!B)L_b`ZZT! zNFb?&sXt{9Cy)Ff3w7g+%}8uqEU|NIp$}>j(|_4jyX2Hn_raBQ$%!#>ElBr?TZ?4Z za(LwjSM9FlpgZ;?sA*L1WfvrX@H`?yE2A_>R0%M(-EzLRUdjT0U6iU%$z9mf0smra-YHM?4@bCJWc zhhLjC%!#_fc%nlb#zkGBmJncm-fUJuJ<;(1RwOcu80Zjw0?yQ7q?mQxb-4#E5}vi# zbxG4X$qnE=Z>G^{$_-#WZ`Q968}HyDHB;-z(ZveE4w7h27>U(lil%OjIJE;RLGI+$i{jI1`M8HkYuVw$xu%@iP)aF9zF z$R!ltta04_t$r$~Mm{5}*Iuur4zbc8y21x8;!}{xDmIRnV$6~{$&{MPlxoV9%E&BM zi$ALGU7Y0G@k>9uS6qLuS<$nI%zFMoYEAaOKr

fO@RpYP4W)bn1B9FYK)S12kAz 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 Nc)I$ztaD0e0su_IGc*7I literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_obj_multi_v1/malformed-face.obj b/tests/files/web/m12_obj_multi_v1/malformed-face.obj new file mode 100644 index 00000000..c2be5e87 --- /dev/null +++ b/tests/files/web/m12_obj_multi_v1/malformed-face.obj @@ -0,0 +1,27 @@ +# Blender 5.2.0 LTS +# www.blender.org +mtllib multi-object.mtl +g M12_OBJ_Left_M12_OBJ_Left_Mesh +v -1.750000 0.000000 0.750000 +v -0.250000 0.000000 0.750000 +v -1.000000 0.000000 -0.750000 +vn -0.0000 1.0000 -0.0000 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.500000 1.000000 +s 0 +g M12_OBJ_Left_M12_OBJ_Left_Material +usemtl M12_OBJ_Left_Material +f 1/1/1 2/2/1 +g M12_OBJ_Right_M12_OBJ_Right_Mesh +v 0.250000 0.000000 0.750000 +v 1.750000 0.000000 0.750000 +v 1.000000 0.000000 -0.750000 +vn -0.0000 1.0000 -0.0000 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.500000 1.000000 +s 0 +g M12_OBJ_Right_M12_OBJ_Right_Material +usemtl M12_OBJ_Right_Material +f 4/4/2 5/5/2 6/6/2 diff --git a/tests/files/web/m12_obj_multi_v1/multi-object.mtl b/tests/files/web/m12_obj_multi_v1/multi-object.mtl new file mode 100644 index 00000000..9ae17b5d --- /dev/null +++ b/tests/files/web/m12_obj_multi_v1/multi-object.mtl @@ -0,0 +1,22 @@ +# Blender 5.2.0 LTS MTL File: 'None' +# www.blender.org + +newmtl M12_OBJ_Left_Material +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.500000 +d 1.000000 +illum 2 +map_Kd m12_obj_texture.png + +newmtl M12_OBJ_Right_Material +Ns 250.000000 +Ka 1.000000 1.000000 1.000000 +Ks 0.500000 0.500000 0.500000 +Ke 0.000000 0.000000 0.000000 +Ni 1.500000 +d 1.000000 +illum 2 +map_Kd m12_obj_texture.png diff --git a/tests/files/web/m12_obj_multi_v1/multi-object.obj b/tests/files/web/m12_obj_multi_v1/multi-object.obj new file mode 100644 index 00000000..0175e48a --- /dev/null +++ b/tests/files/web/m12_obj_multi_v1/multi-object.obj @@ -0,0 +1,27 @@ +# Blender 5.2.0 LTS +# www.blender.org +mtllib multi-object.mtl +g M12_OBJ_Left_M12_OBJ_Left_Mesh +v -1.750000 0.000000 0.750000 +v -0.250000 0.000000 0.750000 +v -1.000000 0.000000 -0.750000 +vn -0.0000 1.0000 -0.0000 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.500000 1.000000 +s 0 +g M12_OBJ_Left_M12_OBJ_Left_Material +usemtl M12_OBJ_Left_Material +f 1/1/1 2/2/1 3/3/1 +g M12_OBJ_Right_M12_OBJ_Right_Mesh +v 0.250000 0.000000 0.750000 +v 1.750000 0.000000 0.750000 +v 1.000000 0.000000 -0.750000 +vn -0.0000 1.0000 -0.0000 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.500000 1.000000 +s 0 +g M12_OBJ_Right_M12_OBJ_Right_Material +usemtl M12_OBJ_Right_Material +f 4/4/2 5/5/2 6/6/2 diff --git a/tests/files/web/m12_obj_multi_v1/negative-index.obj b/tests/files/web/m12_obj_multi_v1/negative-index.obj new file mode 100644 index 00000000..d0709738 --- /dev/null +++ b/tests/files/web/m12_obj_multi_v1/negative-index.obj @@ -0,0 +1,27 @@ +# Blender 5.2.0 LTS +# www.blender.org +mtllib multi-object.mtl +g M12_OBJ_Left_M12_OBJ_Left_Mesh +v -1.750000 0.000000 0.750000 +v -0.250000 0.000000 0.750000 +v -1.000000 0.000000 -0.750000 +vn -0.0000 1.0000 -0.0000 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.500000 1.000000 +s 0 +g M12_OBJ_Left_M12_OBJ_Left_Material +usemtl M12_OBJ_Left_Material +f -1/-1/-1 -2/-2/-1 -3/-3/-1 +g M12_OBJ_Right_M12_OBJ_Right_Mesh +v 0.250000 0.000000 0.750000 +v 1.750000 0.000000 0.750000 +v 1.000000 0.000000 -0.750000 +vn -0.0000 1.0000 -0.0000 +vt 0.000000 0.000000 +vt 1.000000 0.000000 +vt 0.500000 1.000000 +s 0 +g M12_OBJ_Right_M12_OBJ_Right_Material +usemtl M12_OBJ_Right_Material +f -4/-4/-2 -5/-5/-2 -6/-6/-2 diff --git a/tests/files/web/m12_ply_capability_v1/capability-ascii.ply b/tests/files/web/m12_ply_capability_v1/capability-ascii.ply new file mode 100644 index 00000000..614c2363 --- /dev/null +++ b/tests/files/web/m12_ply_capability_v1/capability-ascii.ply @@ -0,0 +1,19 @@ +ply +format ascii 1.0 +comment Created in Blender version 5.2.0 LTS +element vertex 4 +property float x +property float y +property float z +property float nx +property float ny +property float nz +element face 2 +property list uchar uint vertex_indices +end_header +-1 0 1 0 1 0 +1 0 1 0 1 0 +1 0 -1 0 1 0 +-1 0 -1 0 1 0 +3 0 1 2 +3 0 2 3 diff --git a/tests/files/web/m12_ply_capability_v1/capability-binary-le.ply b/tests/files/web/m12_ply_capability_v1/capability-binary-le.ply new file mode 100644 index 0000000000000000000000000000000000000000..795202c0924cb57b69a5980a3ced0c8f09e6ebfb GIT binary patch literal 391 zcmb79!485j3`M=|EBXZliJndL>P_#oLthheeHYQYfKQ8 zb+z(v)1+LGMtg5iB9&SZY|~5>wyu#poE$2TC1`@9;W;{Zplh`y*r$1#!TEX-XmDEQ ziN_vxqH(t2IzVMCU+H5Y#J~^(wBXQ!M8o%Tr3!@nCt`H#p)2mnL8sZq=Vwy0!2(;s jM&un8yAeq;K9lKA;(GO_cP2oP&V<60;w+x_QGAnqC7%Ed-~vKtI!(U+ zPuiy9R%fLv&_ir2x;A3tJyS$BHE7{J*y*G!3bG!a9igWbjD;hMP7XfE)uy!Y80>=` zyu7_?Vj9e%csjv@u3TA3+(O2s+;Uojb`3t)z;0^nrpPX;nj}XDj?%f&h~6EMUBj3g zS~KJ-#~F?<_f*J7&^K=3A0@_^c=~#V3Bi6YXH$C^U(gtt<2ahg$b=KBwM;U~31ysG zsq;k*>wH+Aol-wa!IS91|7%m;^`0;BsBmJNew}!}#S`|P%ikT>^U&wr&}a literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_ply_mapping_v1/unknown-property-ascii.ply b/tests/files/web/m12_ply_mapping_v1/unknown-property-ascii.ply new file mode 100644 index 00000000..ffac7da5 --- /dev/null +++ b/tests/files/web/m12_ply_mapping_v1/unknown-property-ascii.ply @@ -0,0 +1,26 @@ +ply +format ascii 1.0 +comment Created in Blender version 5.2.0 LTS +element vertex 4 +property float x +property float y +property float z +property float nx +property float ny +property float nz +property uchar red +property uchar green +property uchar blue +property uchar alpha +property float temperature +property float label +property list uchar float unknown_values +element face 2 +property list uchar uint vertex_indices +end_header +-1 0 1 0 1 0 254 0 0 255 10 1 1 0.5 +1 0 1 0 1 0 0 254 0 255 20 2 1 0.5 +1 0 -1 0 1 0 0 0 254 255 30 3 1 0.5 +-1 0 -1 0 1 0 254 254 0 255 40 4 1 0.5 +3 0 1 2 +3 0 2 3 diff --git a/tests/files/web/m12_ply_negative_v1/big-endian.ply b/tests/files/web/m12_ply_negative_v1/big-endian.ply new file mode 100644 index 0000000000000000000000000000000000000000..45749b1475e0d9f04fa4471d876b4b6492b343ba GIT binary patch literal 179 zcmZ8bTMmOT4BK~3krN;|Q5A|iB9*oZ1<{>;{bCdTvTXTLVxy34lghIuvF2udJETP= z@idHthz+Uo39GnxqGGvp-Z;eEQ`b{yPvLWS1o7bU=SSTuAAS|%L+=fzecfZH{=%)0 J1xBwwd;#XFJvjgX literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_ply_negative_v1/malformed-list-ascii.ply b/tests/files/web/m12_ply_negative_v1/malformed-list-ascii.ply new file mode 100644 index 00000000..53f09542 --- /dev/null +++ b/tests/files/web/m12_ply_negative_v1/malformed-list-ascii.ply @@ -0,0 +1,13 @@ +ply +format ascii 1.0 +element vertex 3 +property float x +property float y +property float z +element face 1 +property list uchar uint vertex_indices +end_header +0 0 0 +1 0 0 +0 0 1 +3 0 1 diff --git a/tests/files/web/m12_ply_negative_v1/oversized-count-ascii.ply b/tests/files/web/m12_ply_negative_v1/oversized-count-ascii.ply new file mode 100644 index 00000000..70d3cf12 --- /dev/null +++ b/tests/files/web/m12_ply_negative_v1/oversized-count-ascii.ply @@ -0,0 +1,9 @@ +ply +format ascii 1.0 +element vertex 65537 +property float x +property float y +property float z +element face 0 +property list uchar uint vertex_indices +end_header diff --git a/tests/files/web/m12_stl_capability_v1/capability-ascii.stl b/tests/files/web/m12_stl_capability_v1/capability-ascii.stl new file mode 100644 index 00000000..eba7c37b --- /dev/null +++ b/tests/files/web/m12_stl_capability_v1/capability-ascii.stl @@ -0,0 +1,16 @@ +solid +facet normal 0 1 -0 + outer loop + vertex -1 0 1 + vertex 1 0 1 + vertex 1 0 -1 + endloop +endfacet +facet normal 0 1 -0 + outer loop + vertex -1 0 1 + vertex 1 0 -1 + vertex -1 0 -1 + endloop +endfacet +endsolid diff --git a/tests/files/web/m12_stl_capability_v1/capability-binary.stl b/tests/files/web/m12_stl_capability_v1/capability-binary.stl new file mode 100644 index 0000000000000000000000000000000000000000..59c871cc27d5b02eccc0fd05b99f24259bbdc030 GIT binary patch literal 184 qcmZQzpe|s78`oeDWHtcNeuxl=4;BLnBeS9MP+43$fU4jo0oeeZ4i0qy literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_stl_edges_v1/capability-binary.stl b/tests/files/web/m12_stl_edges_v1/capability-binary.stl new file mode 100644 index 0000000000000000000000000000000000000000..59c871cc27d5b02eccc0fd05b99f24259bbdc030 GIT binary patch literal 184 qcmZQzpe|s78`oeDWHtcNeuxl=4;BLnBeS9MP+43$fU4jo0oeeZ4i0qy literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_stl_edges_v1/degenerate-binary.stl b/tests/files/web/m12_stl_edges_v1/degenerate-binary.stl new file mode 100644 index 0000000000000000000000000000000000000000..dd6e7e394dac5df1b6f86e6fe01130f34d347048 GIT binary patch literal 184 ocmZQzpe|s78`oeDWHtcNeuxl=kBf~^fuRFr3J`#F!gT}L0L6|D^#A|> literal 0 HcmV?d00001 diff --git a/tests/files/web/m12_stl_edges_v1/trailing-binary.stl b/tests/files/web/m12_stl_edges_v1/trailing-binary.stl new file mode 100644 index 0000000000000000000000000000000000000000..b47f8b5307c9a9258c1e66e0abfb7e8b964db291 GIT binary patch literal 188 vcmZQzpe|s78`oeDWHtcNeuxl=4;BLnBeS9MP+43$fU4jo0onJ~?t2da-pmoL literal 0 HcmV?d00001 diff --git a/tests/files/web/m13_malicious_script_v1/malicious-script.blend b/tests/files/web/m13_malicious_script_v1/malicious-script.blend new file mode 100644 index 0000000000000000000000000000000000000000..91b9b8a1d77b8240f151406fd4aea934c5767393 GIT binary patch literal 491160 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 z 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 literal 0 HcmV?d00001 diff --git a/tests/golden/M12-01E/manifest.json b/tests/golden/M12-01E/manifest.json index 92276172..0696304f 100644 --- a/tests/golden/M12-01E/manifest.json +++ b/tests/golden/M12-01E/manifest.json @@ -20,7 +20,7 @@ }, "errorContract": { "path": "web/protocol/error.ts", - "sha256": "309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34" + "sha256": "751c70c898540fa7e82fb1b1a79254756ec8db8e4201a88b6d817d7c3d92103f" }, "generator": { "path": "tools/web/generate-asset-catalog-compatibility.mjs", diff --git a/tests/golden/M12-02B/manifest.json b/tests/golden/M12-02B/manifest.json index c8c762f9..ecd1b1e9 100644 --- a/tests/golden/M12-02B/manifest.json +++ b/tests/golden/M12-02B/manifest.json @@ -21,7 +21,7 @@ "generatorSettingsSha256": "07ebc55d4b6cbb293badf0640874846912df3d23549997cf6f87f4c325280ca0", "artifacts": { "protocol": { "path": "web/protocol/asset-preview.ts", "sha256": "2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438" }, - "errorContract": { "path": "web/protocol/error.ts", "sha256": "6aec7b8a9d6903d83ae67718db3ae775d709cabb207cf2a02968d4ff7c5f26c0" }, + "errorContract": { "path": "web/protocol/error.ts", "sha256": "751c70c898540fa7e82fb1b1a79254756ec8db8e4201a88b6d817d7c3d92103f" }, "generator": { "path": "tools/web/generate-asset-preview-identity.py", "sha256": "753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce" }, "blender": { "path": "build_blender_5.2.0/bin/blender", "sha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82" }, "source": { "path": "tests/files/web/media/sequencer-frame.png", "sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed" }, diff --git a/tests/golden/M12-03E/manifest.json b/tests/golden/M12-03E/manifest.json new file mode 100644 index 00000000..8a162b51 --- /dev/null +++ b/tests/golden/M12-03E/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M12-03E", + "parentTask": "M12-03D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_WASM_CHROMIUM", + "operation": "APPEND", + "canonicalComparison": "DESKTOP_REPORT_EXACT_AFTER_IMAGE_ROW_NORMALIZATION_AND_SCENEIR_COLORSPACE_DEFAULT", + "assertions": { + "transactionCount": 1, + "revisionDelta": 1, + "undoRemovesClosure": true, + "redoRestoresCanonical": true, + "saveReopenRestoresCanonical": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03C/manifest.json", + "sha256": "cbfbd8c919125108334dd24925b9ef8e69880983515e56841e6ead4a2b182aed" + }, + "desktopReport": { + "path": "tests/golden/M12-03C/desktop-append-report.json", + "sha256": "b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a" + }, + "test": { + "path": "web/tests/e2e/library-append-main.spec.ts", + "sha256": "101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0" + }, + "sourceBlend": { + "path": "tests/files/web/m12_library_append_v1/m12_append_source.blend", + "sha256": "5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606" + }, + "targetBlend": { + "path": "tests/files/web/empty.blend", + "sha256": "9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63" + } + }, + "nextTask": "M12-03F" +} diff --git a/tests/golden/M12-03F/desktop-link-report.json b/tests/golden/M12-03F/desktop-link-report.json new file mode 100644 index 00000000..419ad76d --- /dev/null +++ b/tests/golden/M12-03F/desktop-link-report.json @@ -0,0 +1,202 @@ +{ + "blenderVersion": "5.2.0", + "linkedGraph": { + "edges": [ + { + "from": "Object/M12 Link Object", + "relation": "OBJECT_DATA", + "to": "Mesh/M12 Link Mesh" + }, + { + "from": "Mesh/M12 Link Mesh", + "relation": "MATERIAL_SLOT[0]", + "to": "Material/M12 Link Material" + }, + { + "from": "Material/M12 Link Material", + "relation": "NODE_IMAGE[M12 Link Image Node]", + "to": "Image/M12 Link Image" + } + ], + "geometry": { + "edges": 4, + "loops": 4, + "materialSlots": [ + "M12 Link Material" + ], + "polygons": 1, + "uvLayers": [ + "UVMap" + ], + "vertices": 4 + }, + "ids": { + "IMAGE": { + "idType": "IMAGE", + "isLibraryOverride": false, + "library": "m12_link_source.blend", + "name": "M12 Link Image", + "nameFull": "M12 Link Image [m12_link_source.blend]" + }, + "MATERIAL": { + "idType": "MATERIAL", + "isLibraryOverride": false, + "library": "m12_link_source.blend", + "name": "M12 Link Material", + "nameFull": "M12 Link Material [m12_link_source.blend]" + }, + "MESH": { + "idType": "MESH", + "isLibraryOverride": false, + "library": "m12_link_source.blend", + "name": "M12 Link Mesh", + "nameFull": "M12 Link Mesh [m12_link_source.blend]" + }, + "OBJECT": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": "m12_link_source.blend", + "name": "M12 Link Object", + "nameFull": "M12 Link Object [m12_link_source.blend]" + } + }, + "image": { + "channels": 4, + "colorspace": "sRGB", + "packed": true, + "pixelFloat32Sha256": "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a", + "size": [ + 2, + 2 + ] + }, + "root": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": "m12_link_source.blend", + "name": "M12 Link Object", + "nameFull": "M12 Link Object [m12_link_source.blend]" + }, + "sourceMarker": "M12-03F" + }, + "nextTask": "M12-03G", + "operation": "LINK", + "schemaVersion": 1, + "selectedRoots": [ + "Object/M12 Link Object" + ], + "source": { + "file": "m12_link_source.blend", + "sha256": "fae97569e9d2e2066fc92749672f86cc42717cd9b97b012faeb9eb92015d7fd1" + }, + "sourceGraph": { + "edges": [ + { + "from": "Object/M12 Link Object", + "relation": "OBJECT_DATA", + "to": "Mesh/M12 Link Mesh" + }, + { + "from": "Mesh/M12 Link Mesh", + "relation": "MATERIAL_SLOT[0]", + "to": "Material/M12 Link Material" + }, + { + "from": "Material/M12 Link Material", + "relation": "NODE_IMAGE[M12 Link Image Node]", + "to": "Image/M12 Link Image" + } + ], + "geometry": { + "edges": 4, + "loops": 4, + "materialSlots": [ + "M12 Link Material" + ], + "polygons": 1, + "uvLayers": [ + "UVMap" + ], + "vertices": 4 + }, + "ids": { + "IMAGE": { + "idType": "IMAGE", + "isLibraryOverride": false, + "library": null, + "name": "M12 Link Image", + "nameFull": "M12 Link Image" + }, + "MATERIAL": { + "idType": "MATERIAL", + "isLibraryOverride": false, + "library": null, + "name": "M12 Link Material", + "nameFull": "M12 Link Material" + }, + "MESH": { + "idType": "MESH", + "isLibraryOverride": false, + "library": null, + "name": "M12 Link Mesh", + "nameFull": "M12 Link Mesh" + }, + "OBJECT": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": null, + "name": "M12 Link Object", + "nameFull": "M12 Link Object" + } + }, + "image": { + "channels": 4, + "colorspace": "sRGB", + "packed": true, + "pixelFloat32Sha256": "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a", + "size": [ + 2, + 2 + ] + }, + "root": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": null, + "name": "M12 Link Object", + "nameFull": "M12 Link Object" + }, + "sourceMarker": "M12-03F" + }, + "stableMapping": [ + { + "local": "Object/M12 Link Object", + "owner": "SOURCE_LIBRARY", + "readOnly": true, + "source": "Object/M12 Link Object" + }, + { + "local": "Mesh/M12 Link Mesh", + "owner": "SOURCE_LIBRARY", + "readOnly": true, + "source": "Mesh/M12 Link Mesh" + }, + { + "local": "Material/M12 Link Material", + "owner": "SOURCE_LIBRARY", + "readOnly": true, + "source": "Material/M12 Link Material" + }, + { + "local": "Image/M12 Link Image", + "owner": "SOURCE_LIBRARY", + "readOnly": true, + "source": "Image/M12 Link Image" + } + ], + "target": { + "file": "m12_link_target.blend", + "sha256": "acdaf0f297deb08c84e1a8beba30972e3414ef62e60e3b103fe0661199c438a1" + }, + "task": "M12-03F" +} diff --git a/tests/golden/M12-03F/manifest.json b/tests/golden/M12-03F/manifest.json new file mode 100644 index 00000000..ab6e2410 --- /dev/null +++ b/tests/golden/M12-03F/manifest.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 1, + "task": "M12-03F", + "parentTask": "M12-03E", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "LINK", + "assertions": { + "selectedRoots": 1, + "dependencyClosure": 4, + "linkedOwnership": "SOURCE_LIBRARY", + "readOnly": true, + "saveReopenStable": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03E/manifest.json", + "sha256": "beaa88ea0385225e712f9816072420bd8744c15da3229ddc352462abec407739" + }, + "generator": { + "path": "tools/web/generate-library-link-fixture.py", + "sha256": "15a2e34c1c5b2a8ee63b10084dbb894e0f9677b9e1cf4adb7e2ddce6b4c965b1" + }, + "checker": { + "path": "tools/web/check-library-link-fixture.mjs", + "sha256": "6a4c024bea227d7bc14f793467b91766b006459fe4d7717cc75dfee12f49f894" + }, + "sourceBlend": { + "path": "tests/files/web/m12_library_link_v1/m12_link_source.blend", + "sha256": "fae97569e9d2e2066fc92749672f86cc42717cd9b97b012faeb9eb92015d7fd1" + }, + "targetBlend": { + "path": "tests/files/web/m12_library_link_v1/m12_link_target.blend", + "sha256": "acdaf0f297deb08c84e1a8beba30972e3414ef62e60e3b103fe0661199c438a1" + }, + "desktopReport": { + "path": "tests/golden/M12-03F/desktop-link-report.json", + "sha256": "b278d4c254eff63d41c8cb3ea1d5e0984192137d45b1695ba0672e16f95b58ea" + } + }, + "nextTask": "M12-03G" +} diff --git a/tests/golden/M12-03G/manifest.json b/tests/golden/M12-03G/manifest.json new file mode 100644 index 00000000..7e2c26d3 --- /dev/null +++ b/tests/golden/M12-03G/manifest.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M12-03G", + "parentTask": "M12-03F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LINK", + "assertions": { + "writerOperations": 6, + "linkedMutationCode": "LINKED_DATA_MUTATION_BLOCKED", + "staleRevisionCode": "REVISION_CONFLICT", + "mainMutation": false, + "recoverable": false + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03F/manifest.json", + "sha256": "8220a8f5d560d45a75a62cbed645b3febc1390fe37b07c3c48871fefc1a9b159" + }, + "protocol": { + "path": "web/protocol/library-linked-mutation.ts", + "sha256": "f629e0e7e04dc1f5437b6e2ca62fbe35484fc238830fa47e8358bcab46b7e104" + }, + "unit": { + "path": "web/tests/unit/library-linked-mutation.test.mjs", + "sha256": "f19d47cefe89daf6123062e045ec717e6ffe60977e8a4b20c996dd62e49da211" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03H" +} diff --git a/tests/golden/M12-03H/manifest.json b/tests/golden/M12-03H/manifest.json new file mode 100644 index 00000000..63ef71d8 --- /dev/null +++ b/tests/golden/M12-03H/manifest.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M12-03H", + "parentTask": "M12-03G", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LINK_RELOAD", + "assertions": { + "matchingGenerationOnly": true, + "replacementGenerationStep": 1, + "staleCode": "REVISION_CONFLICT", + "preservesUnrelatedSnapshots": true, + "readOnlyOwner": "SOURCE_LIBRARY" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03G/manifest.json", + "sha256": "ce7aba32499dbd22bce3bc78c4b0322f830e5d2648a47ce2d25271adf284dddb" + }, + "protocol": { + "path": "web/protocol/library-linked-reload.ts", + "sha256": "8be6f0b2abe36cd566ea7447d1b7de44c0e6a9a7351b863dfdd1372c1761060e" + }, + "unit": { + "path": "web/tests/unit/library-linked-reload.test.mjs", + "sha256": "dff866291468cc01e775fe3b3b95c632f5c0742566f79b956a0193bfd8fd43d2" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03I" +} diff --git a/tests/golden/M12-03I/manifest.json b/tests/golden/M12-03I/manifest.json new file mode 100644 index 00000000..b0c4c037 --- /dev/null +++ b/tests/golden/M12-03I/manifest.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 1, + "task": "M12-03I", + "parentTask": "M12-03H", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LINK_MISSING", + "assertions": { + "missingStatus": "MISSING", + "placeholderKind": "MISSING_LIBRARY", + "preservesSourceLocator": true, + "preservesSourceSha256": true, + "preservesDataBlockIds": true, + "staleCode": "REVISION_CONFLICT", + "sourceDriftCode": "ASSET_SOURCE_HASH_MISMATCH" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03H/manifest.json", + "sha256": "72d3cf9a22d8a2015a7b8fcbc0496425a354e09cec56fd85949561e6b3700009" + }, + "protocol": { + "path": "web/protocol/library-linked-missing.ts", + "sha256": "5833e8c943ef6bd866a93a0e1666c9521fa6d3e8358861df57b628874b679ec2" + }, + "unit": { + "path": "web/tests/unit/library-linked-missing.test.mjs", + "sha256": "4ab6d6ff4845b4ca963399e7eea214ceb412871dc1fdef5bac1ed0333596da4f" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03J" +} diff --git a/tests/golden/M12-03J/desktop-override-report.json b/tests/golden/M12-03J/desktop-override-report.json new file mode 100644 index 00000000..9169b5c9 --- /dev/null +++ b/tests/golden/M12-03J/desktop-override-report.json @@ -0,0 +1,47 @@ +{ + "blenderVersion": "5.2.0", + "nextTask": "M12-03K", + "operation": "LIBRARY_OVERRIDE", + "overrideGraph": { + "local": { + "dataBlockId": "Object/M12 Override Object", + "hierarchyRootDataBlockId": "Object/M12 Override Object", + "idType": "OBJECT", + "isLibraryOverride": true, + "library": null, + "owner": "LOCAL_OVERRIDE", + "projectId": "m12-03j-project", + "readOnly": false, + "referenceSourceDataBlockId": "Object/M12 Override Object" + }, + "propertyOverride": { + "index": 0, + "operationCount": 1, + "propertyCount": 1, + "rnaPath": "[\"m12_override_value\"]", + "value": 2.5 + }, + "reference": { + "dataBlockId": "Object/M12 Override Object", + "idType": "OBJECT", + "isLibraryOverride": false, + "library": "m12_override_source.blend", + "owner": "SOURCE_LIBRARY", + "readOnly": true + }, + "sourceMarker": "M12-03J" + }, + "schemaVersion": 1, + "selectedRoots": [ + "Object/M12 Override Object" + ], + "source": { + "file": "m12_override_source.blend", + "sha256": "d7f8d78e7e91481bf46ecc9a6bcb01e900a6a397839dd31ab80a53def7675601" + }, + "target": { + "file": "m12_override_target.blend", + "sha256": "b008a1608ff1e8f679e1e1281bf7be0360f1137e815dd890cbd93e1e98675495" + }, + "task": "M12-03J" +} diff --git a/tests/golden/M12-03J/manifest.json b/tests/golden/M12-03J/manifest.json new file mode 100644 index 00000000..7a834a8b --- /dev/null +++ b/tests/golden/M12-03J/manifest.json @@ -0,0 +1,50 @@ +{ + "schemaVersion": 1, + "task": "M12-03J", + "parentTask": "M12-03I", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "LIBRARY_OVERRIDE", + "assertions": { + "selectedRoots": 1, + "referenceOwner": "SOURCE_LIBRARY", + "referenceReadOnly": true, + "localOwner": "LOCAL_OVERRIDE", + "localReadOnly": false, + "propertyPath": "[\\\"m12_override_value\\\"]", + "propertyValue": 2.5, + "saveReopenStable": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03I/manifest.json", + "sha256": "f5fa606161b93e9ee42e7ca8144d2a83379acf601c5045eb8cc76732f8dba431" + }, + "generator": { + "path": "tools/web/generate-library-override-fixture.py", + "sha256": "2cdbac04cac7240360a9d70919380fd90f9479e2cb41d8032fb51626ed4b4dd6" + }, + "checker": { + "path": "tools/web/check-library-override-fixture.mjs", + "sha256": "afebb3c9b8715b9d2e0c9b17f463f038bd23e8747074137bccbf1c09c4bfb5ba" + }, + "sourceBlend": { + "path": "tests/files/web/m12_library_override_v1/m12_override_source.blend", + "sha256": "d7f8d78e7e91481bf46ecc9a6bcb01e900a6a397839dd31ab80a53def7675601" + }, + "targetBlend": { + "path": "tests/files/web/m12_library_override_v1/m12_override_target.blend", + "sha256": "b008a1608ff1e8f679e1e1281bf7be0360f1137e815dd890cbd93e1e98675495" + }, + "report": { + "path": "tests/golden/M12-03J/desktop-override-report.json", + "sha256": "19cb13a3417b4b65a8497b622337c42c4697b3f3d48de26a1f70f2d4527ddde0" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03K" +} diff --git a/tests/golden/M12-03K/manifest.json b/tests/golden/M12-03K/manifest.json new file mode 100644 index 00000000..2487ba27 --- /dev/null +++ b/tests/golden/M12-03K/manifest.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 1, + "task": "M12-03K", + "parentTask": "M12-03J", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LIBRARY_OVERRIDE_WRITER", + "assertions": { + "allowedOperation": "SET_M12_OVERRIDE_VALUE", + "allowedPropertyPath": "[\\\"m12_override_value\\\"]", + "allowedPropertyCount": 1, + "owner": "LOCAL_OVERRIDE", + "referenceReadOnly": true, + "staleCode": "REVISION_CONFLICT", + "linkedOwnerCode": "LINKED_DATA_MUTATION_BLOCKED" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03J/manifest.json", + "sha256": "01d0f66bb3819eea3e92727d1b5bffbec935d24eeecc28d874b999df78d73fbf" + }, + "protocol": { + "path": "web/protocol/library-override-writer.ts", + "sha256": "dd181c7e9946b98334a5d1c686887afecfe3fc11d732beec246e40bf11a155aa" + }, + "unit": { + "path": "web/tests/unit/library-override-writer.test.mjs", + "sha256": "e41bd32eb4ce4d331a20ecb91f3325a27f461b9ae85e98940d5afd482ec21c4c" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03L" +} diff --git a/tests/golden/M12-03L/manifest.json b/tests/golden/M12-03L/manifest.json new file mode 100644 index 00000000..36206f03 --- /dev/null +++ b/tests/golden/M12-03L/manifest.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M12-03L", + "parentTask": "M12-03K", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LIBRARY_OVERRIDE_FRESHNESS", + "assertions": { + "matchingStatus": "READY", + "staleCode": "REVISION_CONFLICT", + "identityDriftCode": "ASSET_SOURCE_HASH_MISMATCH", + "linkedOwnerCode": "LINKED_DATA_MUTATION_BLOCKED", + "mainCommit": false + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03K/manifest.json", + "sha256": "9eeee93e4a806aa59b5c53a6485fe3a1b5e3972c1d2585cdc71b7cfc293afb70" + }, + "protocol": { + "path": "web/protocol/library-override-freshness.ts", + "sha256": "2eff7ea7605b1579d7551336d87d4f30adb996b585596ff9eee67aea04ca7d22" + }, + "unit": { + "path": "web/tests/unit/library-override-freshness.test.mjs", + "sha256": "d48344f31e1ba12e557ca30ece56643583efa633da556f5de3896a8e9175ef8f" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03M" +} diff --git a/tests/golden/M12-03M/manifest.json b/tests/golden/M12-03M/manifest.json new file mode 100644 index 00000000..465bf504 --- /dev/null +++ b/tests/golden/M12-03M/manifest.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M12-03M", + "parentTask": "M12-03L", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LIBRARY_NEGATIVE_CASES", + "assertions": { + "dependencyCycleCode": "LIBRARY_DEPENDENCY_CYCLE", + "crossLibraryCycleCode": "LIBRARY_DEPENDENCY_CYCLE", + "dataBlockCollisionCode": "TASK_VALIDATION_FAILED", + "duplicateReloadCode": "REVISION_CONFLICT", + "missingSourceCode": "ASSET_SOURCE_HASH_MISMATCH" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03L/manifest.json", + "sha256": "237f3f168e037b67b805ba8d9c9455250c889a5e90c45cbb17d2d20d9fbdcc50" + }, + "protocol": { + "path": "web/protocol/library-negative-cases.ts", + "sha256": "efc7cc810089eab6178fc5c2f2a45d641625a032ecfa5b2b4ef37694d0decc97" + }, + "unit": { + "path": "web/tests/unit/library-negative-cases.test.mjs", + "sha256": "b7ae41b2c35b2fe1598bca4425dd434230bfe4bf342320c88214a060dcbb0a16" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-03N" +} diff --git a/tests/golden/M12-03N/manifest.json b/tests/golden/M12-03N/manifest.json new file mode 100644 index 00000000..f8b34038 --- /dev/null +++ b/tests/golden/M12-03N/manifest.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "task": "M12-03N", + "parentTask": "M12-03M", + "enablingTask": false, + "parityStateChange": false, + "runtime": "DESKTOP_WASM_CHROMIUM", + "operation": "LIBRARY_OPERATION_COMMANDS", + "assertions": { + "operations": ["APPEND", "LINK", "LIBRARY_OVERRIDE"], + "lanes": ["DESKTOP", "WASM", "CHROMIUM"], + "independentCommands": 9, + "mainAppendChromiumTest": "web/tests/e2e/library-append-main.spec.ts", + "linkChromiumTest": "web/tests/e2e/library-link-chromium.spec.ts", + "overrideChromiumTest": "web/tests/e2e/library-override-chromium.spec.ts" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03M/manifest.json", + "sha256": "693cbedfe8e29167283a753d119de5a9cfe5e20bc96aff7ba84d720ffde9148a" + }, + "commandChecker": { + "path": "tools/web/check-library-operation-commands.mjs", + "sha256": "3564347393134d835fdf279b8b8f58558ee24fe0165c3b4527195d094a451e98" + }, + "appendWasmProtocol": { + "path": "web/protocol/library-main-append.ts", + "sha256": "bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7" + }, + "appendWasmUnit": { + "path": "web/tests/unit/library-append-wasm.test.mjs", + "sha256": "75fc2d626f7ca7e820e9cacf659a453886e15e790cde43348828c03bed752ec7" + }, + "appendChromium": { + "path": "web/tests/e2e/library-append-main.spec.ts", + "sha256": "101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0" + }, + "linkChromium": { + "path": "web/tests/e2e/library-link-chromium.spec.ts", + "sha256": "814e3486522fb4f0ffdd59acd385a4fca9c5660cdb07e5a2acb1cadd70376bff" + }, + "overrideChromium": { + "path": "web/tests/e2e/library-override-chromium.spec.ts", + "sha256": "1c12982b4eb4fb4b9a3c88907a842a1fc413b3a3a760a9f57b350f57060d007f" + }, + "linkChromiumWrapper": { + "path": "web/app/src/library-link-chromium.ts", + "sha256": "096af8e594a6d475476dc3253e3d99069d9f84bc383b84c690a38d08f1f0137a" + }, + "overrideChromiumWrapper": { + "path": "web/app/src/library-override-chromium.ts", + "sha256": "f3bb9c7d57c65a333ba6aa982a19956025c23e740e1771f741d161defc591f21" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-04A" +} diff --git a/tests/golden/M12-04A/manifest.json b/tests/golden/M12-04A/manifest.json new file mode 100644 index 00000000..3340d37f --- /dev/null +++ b/tests/golden/M12-04A/manifest.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M12-04A", + "parentTask": "M12-03N", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LIBRARY_SOURCE_ORIGIN", + "assertions": { + "acceptedKinds": ["HTTPS_ORIGIN", "PROJECT_ASSET", "USER_SELECTED_FILE"], + "undeclaredHttpsCode": "IO_EXTERNAL_URI_BLOCKED", + "unsafeProjectPathCode": "IO_EXTERNAL_URI_BLOCKED", + "sourceHash": true, + "credentialFreeHttps": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-03N/manifest.json", + "sha256": "e4cccc8edc277b5047c3f8006ea40b26b119d28589fe2f3bd9bfa4cc3575c85a" + }, + "protocol": { + "path": "web/protocol/library-source-origin.ts", + "sha256": "67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb" + }, + "unit": { + "path": "web/tests/unit/library-source-origin.test.mjs", + "sha256": "89b207c9cb13b4fb055a2dfed0237c0f8a00b13a39cc4175a378f5ef2abdb7ae" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-04B" +} diff --git a/tests/golden/M12-04B/manifest.json b/tests/golden/M12-04B/manifest.json new file mode 100644 index 00000000..88ff6aac --- /dev/null +++ b/tests/golden/M12-04B/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M12-04B", + "parentTask": "M12-04A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LIBRARY_PATH_NORMALIZATION", + "assertions": { + "canonicalSeparator": "/", + "dotSegmentsResolved": true, + "percentDecodePasses": 1, + "unicodeNormalization": "NFC", + "canonicalIdempotent": true, + "projectEscapeCode": "ASSET_PATH_OUTSIDE_PROJECT" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-04A/manifest.json", + "sha256": "af80827d925ddd96d8541e446e0f70c956fd4c56e64ac984d187f5449df4cb0d" + }, + "normalizer": { + "path": "web/protocol/asset-path.ts", + "sha256": "6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf" + }, + "sourceAdmission": { + "path": "web/protocol/library-source-origin.ts", + "sha256": "67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb" + }, + "unit": { + "path": "web/tests/unit/library-path-normalization.test.mjs", + "sha256": "abde64c60397541e92a83dd9e4a24e65faf340a8d046650604c101a21037c777" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-04C" +} diff --git a/tests/golden/M12-04C/manifest.json b/tests/golden/M12-04C/manifest.json new file mode 100644 index 00000000..23a4d47a --- /dev/null +++ b/tests/golden/M12-04C/manifest.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "task": "M12-04C", + "parentTask": "M12-04B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "LIBRARY_PATH_SECURITY", + "assertions": { + "absolutePathCode": "ASSET_PATH_OUTSIDE_PROJECT", + "uncPathCode": "ASSET_PATH_OUTSIDE_PROJECT", + "drivePathCode": "ASSET_PATH_OUTSIDE_PROJECT", + "controlCharacterCode": "ASSET_PATH_OUTSIDE_PROJECT", + "originEscapeCode": "IO_EXTERNAL_URI_BLOCKED", + "declaredOriginPathRejected": true, + "encodedControlsRejected": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-04B/manifest.json", + "sha256": "8cd29c3f5281082584fc6aefe2ec385a2eedf8116e837a4db54c391391ff8f98" + }, + "pathNormalizer": { + "path": "web/protocol/asset-path.ts", + "sha256": "6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf" + }, + "sourceOrigin": { + "path": "web/protocol/library-source-origin.ts", + "sha256": "67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb" + }, + "unit": { + "path": "web/tests/unit/library-path-security.test.mjs", + "sha256": "ab302cacb24634a3225dda5cb8f5282cb80b3bd81ed5c8900ddf4ff18267b7e2" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-04D" +} diff --git a/tests/golden/M12-04D/manifest.json b/tests/golden/M12-04D/manifest.json new file mode 100644 index 00000000..f513a73b --- /dev/null +++ b/tests/golden/M12-04D/manifest.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": 1, + "task": "M12-04D", + "parentTask": "M12-04C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "ARCHIVE_LINK_SAFETY", + "assertions": { + "symlinkTargetBase": "link-parent", + "hardlinkTargetBase": "archive-root", + "resolvedWithinTemporaryRoot": true, + "missingTargetCode": "IO_ARCHIVE_UNSAFE", + "cycleCode": "IO_ARCHIVE_UNSAFE", + "outsideRootCode": "IO_ARCHIVE_UNSAFE", + "hardlinkDirectoryCode": "IO_ARCHIVE_UNSAFE" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-04C/manifest.json", + "sha256": "a7f3a45eb7f68d022f38185a1c1409e1db1b27647fef587d66d2ffa52d78f98e" + }, + "protocol": { + "path": "web/protocol/archive-link-safety.ts", + "sha256": "d55a4ba762898aed22bdcc7aa6493c713ee94f6bb1bf58754fdc459d0ddf70d1" + }, + "unit": { + "path": "web/tests/unit/library-link-safety.test.mjs", + "sha256": "7739275be91222d7bfb61d2f5a1daf812c6860fe34d0aea27df8380a77322120" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-04E" +} diff --git a/tests/golden/M12-04E/manifest.json b/tests/golden/M12-04E/manifest.json new file mode 100644 index 00000000..ba2c9e4c --- /dev/null +++ b/tests/golden/M12-04E/manifest.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 1, + "task": "M12-04E", + "parentTask": "M12-04D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "ARCHIVE_METADATA_FIRST", + "assertions": { + "zipFirstRead": "CENTRAL_DIRECTORY", + "tarFirstRead": "MANIFEST", + "payloadReadsBeforeMetadata": false, + "wrongRangeCode": "IO_ARCHIVE_UNSAFE", + "duplicateMetadataCode": "IO_ARCHIVE_UNSAFE", + "metadataRangeBoundBytes": 67108864 + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-04D/manifest.json", + "sha256": "1c09abd1f4bd5908d22ab3b22e3e71d050f694af0b82a7f78a351d7a1bf1e664" + }, + "protocol": { + "path": "web/protocol/archive-metadata-first.ts", + "sha256": "869586b041e134c7d1cb2ebd7e13b35218b337223d401697a179f71cd1420f5b" + }, + "unit": { + "path": "web/tests/unit/library-metadata-first.test.mjs", + "sha256": "2da649722acee9cace8db6f337b4a93500a9c775a0b0f086bf38dd2b3e7f9e91" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-04F" +} diff --git a/tests/golden/M12-04F/manifest.json b/tests/golden/M12-04F/manifest.json new file mode 100644 index 00000000..a6c0bfc1 --- /dev/null +++ b/tests/golden/M12-04F/manifest.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "task": "M12-04F", + "parentTask": "M12-04E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "ARCHIVE_BUDGETS", + "assertions": { + "maxArchiveEntries": 100000, + "maxEntryBytes": 2147483648, + "maxArchiveBytes": 4294967296, + "maxPathDepth": 64, + "maxFileNameBytes": 255, + "budgetCode": "IO_ARCHIVE_UNSAFE" + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-04E/manifest.json", "sha256": "d935392fb23c2e6ad651fb6ad6cb67f8ead3d562e626bb230d2febd9d7f03b1c" }, + "protocol": { "path": "web/protocol/asset-library-io.ts", "sha256": "e02efa79668f4ee10b1c28786709eba2c93691b28ccf1155c6213dda12f59a8f" }, + "unit": { "path": "web/tests/unit/library-archive-budget.test.mjs", "sha256": "b0d8356c6fb348d98e28ce220052845a29439b34b3c976b21a4dbf370ea19593" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-04G" +} diff --git a/tests/golden/M12-04G/manifest.json b/tests/golden/M12-04G/manifest.json new file mode 100644 index 00000000..179107e1 --- /dev/null +++ b/tests/golden/M12-04G/manifest.json @@ -0,0 +1,23 @@ +{ + "schemaVersion": 1, + "task": "M12-04G", + "parentTask": "M12-04F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_UNIT", + "operation": "ARCHIVE_CONFLICTS", + "assertions": { + "compressionRatio": 100, + "overlapCode": "IO_ARCHIVE_UNSAFE", + "duplicatePathCode": "IO_ARCHIVE_UNSAFE", + "prefixConflictCode": "IO_ARCHIVE_UNSAFE", + "outOfRangeCode": "IO_ARCHIVE_UNSAFE" + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-04F/manifest.json", "sha256": "cda905b1e27b62d9ea3a05170f975015480ce6739c15bdf1f5a1f0b79f93ce06" }, + "protocol": { "path": "web/protocol/archive-conflicts.ts", "sha256": "3bec372e4f67ec309f28534cebcbaf8b492144adfa82ff6c8603f88c3ba16254" }, + "unit": { "path": "web/tests/unit/library-archive-conflicts.test.mjs", "sha256": "3f2d44dce33b1c17b3a48f58b04fdd821abc88d98ce3d3b5bb724859e0f0ab3c" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-04H" +} diff --git a/tests/golden/M12-04H/manifest.json b/tests/golden/M12-04H/manifest.json new file mode 100644 index 00000000..dfd9981d --- /dev/null +++ b/tests/golden/M12-04H/manifest.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "task": "M12-04H", + "parentTask": "M12-04G", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_REAL_FS_UNIT", + "operation": "ARCHIVE_EXTRACTION_CANCELLATION", + "assertions": { + "cancellationCode": "IO_ARCHIVE_CANCELLED", + "stagingEntriesAfter": 0, + "publishedProjects": 0, + "committedIdentityUnchanged": true, + "commitLinearization": "BEFORE_ATOMIC_COMMIT" + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-04G/manifest.json", "sha256": "c35e675e8f512e85b748f0b5578874fed8c99df7674a152e698236a67c9f4fa4" }, + "protocol": { "path": "web/protocol/archive-extraction-transaction.ts", "sha256": "c40adc1449172014cc1820db567e155828314abb404b66e4de13c26ddee06e4b" }, + "errorCodes": { "path": "web/protocol/error.ts", "sha256": "751c70c898540fa7e82fb1b1a79254756ec8db8e4201a88b6d817d7c3d92103f" }, + "unit": { "path": "web/tests/unit/library-archive-cancellation.test.mjs", "sha256": "77e7d5bc64dd6b313006ebf523602601acdaf7949a1484da872ddcd046983786" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-04I" +} diff --git a/tests/golden/M12-04I/manifest.json b/tests/golden/M12-04I/manifest.json new file mode 100644 index 00000000..44c30b96 --- /dev/null +++ b/tests/golden/M12-04I/manifest.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "task": "M12-04I", + "parentTask": "M12-04H", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_REAL_FS_UNIT", + "operation": "ARCHIVE_QUOTA_OOM_RECOVERY", + "assertions": { + "faultCodes": ["STORAGE_QUOTA", "WASM_OUT_OF_MEMORY"], + "partialStagingEntriesAfter": 0, + "committedIdentityUnchangedAfterFault": true, + "recoverySession": "SAME_STORAGE_INSTANCE", + "smallArchiveCommitted": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-04H/manifest.json", "sha256": "7fce600a416aec5ade1a91a13d86caf4cb1f65b710054b59d525ff8b1d3c7409" }, + "baseProtocol": { "path": "web/protocol/archive-extraction-transaction.ts", "sha256": "c40adc1449172014cc1820db567e155828314abb404b66e4de13c26ddee06e4b" }, + "recoveryProtocol": { "path": "web/protocol/archive-extraction-recovery.ts", "sha256": "0ae9801ea151403b244c5f58f7f99231bba7a25abb1f61e3669879510b92dccf" }, + "unit": { "path": "web/tests/unit/library-archive-recovery.test.mjs", "sha256": "a931edef8f3ca1243a80ec2b8e9ff5b8da1dfd339f3d8c162ad2ebd08d3db6a1" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-04J" +} diff --git a/tests/golden/M12-04J/manifest.json b/tests/golden/M12-04J/manifest.json new file mode 100644 index 00000000..00f4eaf0 --- /dev/null +++ b/tests/golden/M12-04J/manifest.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "task": "M12-04J", + "parentTask": "M12-04I", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_PROTOCOL_BINARY_FIXTURE_CHROMIUM", + "operation": "MALICIOUS_ZIP_TAR_REGRESSION", + "assertions": { + "fixtureCount": 6, + "zipFixtureCount": 3, + "tarFixtureCount": 3, + "metadataOnly": true, + "extractionAllowed": false, + "expectedCode": "IO_ARCHIVE_UNSAFE", + "deterministicRegeneration": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-04I/manifest.json", "sha256": "574afa68a787b9481d0e098d38b94cb810a9827fe185b0df796d62cc160ba7c5" }, + "generator": { "path": "tools/web/generate-malicious-archive-fixtures.mjs", "sha256": "b372ea8cb2f97b035ffb2cc18666dae9167dcfb349131851ad9f1ff0fc40ba16" }, + "checker": { "path": "tools/web/check-malicious-archive-fixtures.mjs", "sha256": "edda2f420f26e55f9990d24b755bb9b4f732ec32c2e072210144b3d0b6634d9b" }, + "fixtureManifest": { "path": "tests/files/web/archive-security/manifest.json", "sha256": "a93834316f6a23b8a0e6c1f1f806ec584d6f03aa339c2680cae2ce8133a40e58" }, + "chromium": { "path": "web/tests/e2e/archive-security-fixtures.spec.ts", "sha256": "f327500808dd67359a152ce28134b58d027aebd6758416b5535b659b9900bbfe" }, + "package": { "path": "web/package.json", "sha256": "342f75fe285f99da301eba97590fd9fc76cef4938508177fd43e1827f0295076" } + }, + "nextTask": "M12-05A" +} diff --git a/tests/golden/M12-05A/format-inventory.json b/tests/golden/M12-05A/format-inventory.json new file mode 100644 index 00000000..50d508d4 --- /dev/null +++ b/tests/golden/M12-05A/format-inventory.json @@ -0,0 +1,2796 @@ +{ + "formats": [ + { + "export": { + "buildOption": null, + "buildOptionEnabled": true, + "operator": "export_scene.gltf", + "properties": [ + { + "arrayLength": 0, + "identifier": "at_collection_center", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "collection", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_action_filter", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_active_vertex_color_when_no_material", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_all_influences", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_all_vertex_colors", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_anim_scene_split_object", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_anim_single_armature", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_anim_slide_to_zero", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "ACTIONS", + "ACTIVE_ACTIONS", + "BROADCAST", + "NLA_TRACKS", + "SCENE" + ], + "identifier": "export_animation_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_animations", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_apply", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_armature_object_remove", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_attributes", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_bake_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_cameras", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_convert_animation_pointer", + "type": "BOOLEAN" + }, + { + "identifier": "export_copyright", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_current_frame", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_def_bones", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_draco_color_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_generic_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_mesh_compression_enable", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_draco_mesh_compression_level", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_normal_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_position_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_texcoord_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_extra_animations", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_extras", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_force_sampling", + "type": "BOOLEAN" + }, + { + "enumItems": [], + "identifier": "export_format", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_frame_range", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_frame_step", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_kn", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_noq", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_sa", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_si", + "type": "FLOAT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_slb", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_tc", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_tq", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vc", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vn", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vp", + "type": "INT" + }, + { + "enumItems": [ + "Integer", + "Normalized", + "Floating-point" + ], + "identifier": "export_gltfpack_vpi", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vt", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gn_mesh", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gpu_instances", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_hierarchy_flatten_bones", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_hierarchy_flatten_objs", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_hierarchy_full_collections", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_image_add_webp", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "AUTO", + "JPEG", + "WEBP", + "NONE" + ], + "identifier": "export_image_format", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_image_quality", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_image_webp_fallback", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "SPEC", + "COMPAT", + "RAW" + ], + "identifier": "export_import_convert_lighting_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_influence_nb", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_jpeg_quality", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_keep_originals", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_leaf_bone", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_lights", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_loglevel", + "type": "INT" + }, + { + "enumItems": [ + "EXPORT", + "PLACEHOLDER", + "VIEWPORT", + "NONE" + ], + "identifier": "export_materials", + "type": "ENUM" + }, + { + "enumItems": [ + "NLA_TRACK", + "ACTION", + "NONE" + ], + "identifier": "export_merge_animation", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_meshopt_compression_enable", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "EXT_meshopt_compression", + "KHR_meshopt_compression" + ], + "identifier": "export_meshopt_extension", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_morph", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_normal", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_reset_sk_data", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_tangent", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "SLIDE", + "CROP" + ], + "identifier": "export_negative_frame", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_nla_strips", + "type": "BOOLEAN" + }, + { + "identifier": "export_nla_strips_merged_animation_name", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_normals", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_animation_keep_anim_armature", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_animation_keep_anim_object", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_animation_size", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_disable_viewport", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_original_specular", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_pointer_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_reset_pose_bones", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_rest_position_armature", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "LINEAR", + "STEP" + ], + "identifier": "export_sampling_interpolation_fallback", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_shared_accessors", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_skins", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_tangents", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_texcoords", + "type": "BOOLEAN" + }, + { + "identifier": "export_texture_dir", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_try_omit_sparse_sk", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_try_sparse_sk", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_unused_images", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_unused_textures", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_use_gltfpack", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "MATERIAL", + "ACTIVE", + "NAME", + "NONE" + ], + "identifier": "export_vertex_color", + "type": "ENUM" + }, + { + "identifier": "export_vertex_color_name", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_yup", + "type": "BOOLEAN" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "identifier": "gltf_export_id", + "type": "STRING" + }, + { + "enumItems": [ + "GENERAL", + "MESHES", + "OBJECTS", + "ANIMATION" + ], + "identifier": "ui_tab", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "use_active_collection", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_active_collection_with_nested", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_active_scene", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_mesh_edges", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_mesh_vertices", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_renderable", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_selection", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_visible", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "will_save_settings", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "runtimeStatus": "AVAILABLE" + }, + "extensions": [ + ".gltf" + ], + "family": "GLTF", + "format": "GLTF", + "import": { + "buildOption": null, + "buildOptionEnabled": true, + "operator": "import_scene.gltf", + "properties": [ + { + "enumItems": [ + "BLENDER", + "TEMPERANCE", + "FORTUNE" + ], + "identifier": "bone_heuristic", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "bone_shape_scale_factor", + "type": "FLOAT" + }, + { + "identifier": "directory", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "disable_bone_shape", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "SPEC", + "COMPAT", + "RAW" + ], + "identifier": "export_import_convert_lighting_mode", + "type": "ENUM" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "files", + "type": "COLLECTION" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "guess_original_bind_pose", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_merge_material_slots", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_pack_images", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_point_as_pointcloud", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_scene_as_collection", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_scene_extras", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_select_created_objects", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "NORMALS", + "FLAT", + "SMOOTH" + ], + "identifier": "import_shading", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "import_unused_materials", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_webp_texture", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "loglevel", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "merge_vertices", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "runtimeStatus": "AVAILABLE" + }, + "variants": [ + "GLTF_SEPARATE" + ] + }, + { + "export": { + "buildOption": null, + "buildOptionEnabled": true, + "operator": "export_scene.gltf", + "properties": [ + { + "arrayLength": 0, + "identifier": "at_collection_center", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "collection", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_action_filter", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_active_vertex_color_when_no_material", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_all_influences", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_all_vertex_colors", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_anim_scene_split_object", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_anim_single_armature", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_anim_slide_to_zero", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "ACTIONS", + "ACTIVE_ACTIONS", + "BROADCAST", + "NLA_TRACKS", + "SCENE" + ], + "identifier": "export_animation_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_animations", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_apply", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_armature_object_remove", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_attributes", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_bake_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_cameras", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_convert_animation_pointer", + "type": "BOOLEAN" + }, + { + "identifier": "export_copyright", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_current_frame", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_def_bones", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_draco_color_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_generic_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_mesh_compression_enable", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_draco_mesh_compression_level", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_normal_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_position_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_draco_texcoord_quantization", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_extra_animations", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_extras", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_force_sampling", + "type": "BOOLEAN" + }, + { + "enumItems": [], + "identifier": "export_format", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_frame_range", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_frame_step", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_kn", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_noq", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_sa", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_si", + "type": "FLOAT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_slb", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_tc", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_tq", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vc", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vn", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vp", + "type": "INT" + }, + { + "enumItems": [ + "Integer", + "Normalized", + "Floating-point" + ], + "identifier": "export_gltfpack_vpi", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_gltfpack_vt", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_gn_mesh", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_gpu_instances", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_hierarchy_flatten_bones", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_hierarchy_flatten_objs", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_hierarchy_full_collections", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_image_add_webp", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "AUTO", + "JPEG", + "WEBP", + "NONE" + ], + "identifier": "export_image_format", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_image_quality", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_image_webp_fallback", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "SPEC", + "COMPAT", + "RAW" + ], + "identifier": "export_import_convert_lighting_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_influence_nb", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_jpeg_quality", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_keep_originals", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_leaf_bone", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_lights", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_loglevel", + "type": "INT" + }, + { + "enumItems": [ + "EXPORT", + "PLACEHOLDER", + "VIEWPORT", + "NONE" + ], + "identifier": "export_materials", + "type": "ENUM" + }, + { + "enumItems": [ + "NLA_TRACK", + "ACTION", + "NONE" + ], + "identifier": "export_merge_animation", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_meshopt_compression_enable", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "EXT_meshopt_compression", + "KHR_meshopt_compression" + ], + "identifier": "export_meshopt_extension", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_morph", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_normal", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_reset_sk_data", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_morph_tangent", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "SLIDE", + "CROP" + ], + "identifier": "export_negative_frame", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_nla_strips", + "type": "BOOLEAN" + }, + { + "identifier": "export_nla_strips_merged_animation_name", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_normals", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_animation_keep_anim_armature", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_animation_keep_anim_object", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_animation_size", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_optimize_disable_viewport", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_original_specular", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_pointer_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_reset_pose_bones", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_rest_position_armature", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "LINEAR", + "STEP" + ], + "identifier": "export_sampling_interpolation_fallback", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_shared_accessors", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_skins", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_tangents", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_texcoords", + "type": "BOOLEAN" + }, + { + "identifier": "export_texture_dir", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_try_omit_sparse_sk", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_try_sparse_sk", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_unused_images", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_unused_textures", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_use_gltfpack", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "MATERIAL", + "ACTIVE", + "NAME", + "NONE" + ], + "identifier": "export_vertex_color", + "type": "ENUM" + }, + { + "identifier": "export_vertex_color_name", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "export_yup", + "type": "BOOLEAN" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "identifier": "gltf_export_id", + "type": "STRING" + }, + { + "enumItems": [ + "GENERAL", + "MESHES", + "OBJECTS", + "ANIMATION" + ], + "identifier": "ui_tab", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "use_active_collection", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_active_collection_with_nested", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_active_scene", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_mesh_edges", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_mesh_vertices", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_renderable", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_selection", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_visible", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "will_save_settings", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "runtimeStatus": "AVAILABLE" + }, + "extensions": [ + ".glb" + ], + "family": "GLTF", + "format": "GLB", + "import": { + "buildOption": null, + "buildOptionEnabled": true, + "operator": "import_scene.gltf", + "properties": [ + { + "enumItems": [ + "BLENDER", + "TEMPERANCE", + "FORTUNE" + ], + "identifier": "bone_heuristic", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "bone_shape_scale_factor", + "type": "FLOAT" + }, + { + "identifier": "directory", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "disable_bone_shape", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "SPEC", + "COMPAT", + "RAW" + ], + "identifier": "export_import_convert_lighting_mode", + "type": "ENUM" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "files", + "type": "COLLECTION" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "guess_original_bind_pose", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_merge_material_slots", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_pack_images", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_point_as_pointcloud", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_scene_as_collection", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_scene_extras", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_select_created_objects", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "NORMALS", + "FLAT", + "SMOOTH" + ], + "identifier": "import_shading", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "import_unused_materials", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "import_webp_texture", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "loglevel", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "merge_vertices", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "runtimeStatus": "AVAILABLE" + }, + "variants": [ + "GLB" + ] + }, + { + "export": { + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "operator": "wm.obj_export", + "properties": [ + { + "arrayLength": 0, + "identifier": "apply_modifiers", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "apply_transform", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "collection", + "type": "STRING" + }, + { + "enumItems": [ + "DEFAULT", + "LIST_VERTICAL", + "LIST_HORIZONTAL", + "THUMBNAIL" + ], + "identifier": "display_type", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "end_frame", + "type": "INT" + }, + { + "arrayLength": 0, + "identifier": "export_animation", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_colors", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_curves_as_nurbs", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "DAG_EVAL_RENDER", + "DAG_EVAL_VIEWPORT" + ], + "identifier": "export_eval_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_material_groups", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_materials", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_normals", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_object_groups", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_pbr_extensions", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_selected_objects", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_smooth_groups", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_triangulated_mesh", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_uv", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_vertex_groups", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filemode", + "type": "INT" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_alembic", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_archive", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_backup", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blender", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blenlib", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_btx", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_folder", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_font", + "type": "BOOLEAN" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_image", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_movie", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_obj", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_python", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_sound", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_text", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_usd", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_volume", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "forward_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "global_scale", + "type": "FLOAT" + }, + { + "enumItems": [ + "AUTO", + "ABSOLUTE", + "RELATIVE", + "MATCH", + "STRIP", + "COPY" + ], + "identifier": "path_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "smooth_group_bitflags", + "type": "BOOLEAN" + }, + { + "enumItems": [], + "identifier": "sort_method", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "start_frame", + "type": "INT" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "up_axis", + "type": "ENUM" + } + ], + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "runtimeStatus": "AVAILABLE" + }, + "extensions": [ + ".obj" + ], + "family": "OBJ", + "format": "OBJ", + "import": { + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "operator": "wm.obj_import", + "properties": [ + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "clamp_size", + "type": "FLOAT" + }, + { + "arrayLength": 0, + "identifier": "close_spline_loops", + "type": "BOOLEAN" + }, + { + "identifier": "collection_separator", + "type": "STRING" + }, + { + "identifier": "directory", + "type": "STRING" + }, + { + "enumItems": [ + "DEFAULT", + "LIST_VERTICAL", + "LIST_HORIZONTAL", + "THUMBNAIL" + ], + "identifier": "display_type", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "filemode", + "type": "INT" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "files", + "type": "COLLECTION" + }, + { + "arrayLength": 0, + "identifier": "filter_alembic", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_archive", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_backup", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blender", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blenlib", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_btx", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_folder", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_font", + "type": "BOOLEAN" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_image", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_movie", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_obj", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_python", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_sound", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_text", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_usd", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_volume", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "forward_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "global_scale", + "type": "FLOAT" + }, + { + "arrayLength": 0, + "identifier": "import_vertex_groups", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "MAKE_UNIQUE", + "REFERENCE_EXISTING" + ], + "identifier": "mtl_name_collision_mode", + "type": "ENUM" + }, + { + "enumItems": [], + "identifier": "sort_method", + "type": "ENUM" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "up_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "use_split_groups", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_split_objects", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "validate_meshes", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "runtimeStatus": "AVAILABLE" + }, + "variants": [ + "OBJ" + ] + }, + { + "export": { + "buildOption": "io_stl", + "buildOptionEnabled": true, + "operator": "wm.stl_export", + "properties": [ + { + "arrayLength": 0, + "identifier": "apply_modifiers", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "ascii_format", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "collection", + "type": "STRING" + }, + { + "enumItems": [ + "DEFAULT", + "LIST_VERTICAL", + "LIST_HORIZONTAL", + "THUMBNAIL" + ], + "identifier": "display_type", + "type": "ENUM" + }, + { + "enumItems": [ + "DAG_EVAL_RENDER", + "DAG_EVAL_VIEWPORT" + ], + "identifier": "evaluation_mode", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_selected_objects", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filemode", + "type": "INT" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_alembic", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_archive", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_backup", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blender", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blenlib", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_btx", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_folder", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_font", + "type": "BOOLEAN" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_image", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_movie", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_obj", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_python", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_sound", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_text", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_usd", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_volume", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "forward_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "global_scale", + "type": "FLOAT" + }, + { + "enumItems": [], + "identifier": "sort_method", + "type": "ENUM" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "up_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "use_batch", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_scene_unit", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "runtimeStatus": "AVAILABLE" + }, + "extensions": [ + ".stl" + ], + "family": "STL", + "format": "STL", + "import": { + "buildOption": "io_stl", + "buildOptionEnabled": true, + "operator": "wm.stl_import", + "properties": [ + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "directory", + "type": "STRING" + }, + { + "enumItems": [ + "DEFAULT", + "LIST_VERTICAL", + "LIST_HORIZONTAL", + "THUMBNAIL" + ], + "identifier": "display_type", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "filemode", + "type": "INT" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "files", + "type": "COLLECTION" + }, + { + "arrayLength": 0, + "identifier": "filter_alembic", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_archive", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_backup", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blender", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blenlib", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_btx", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_folder", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_font", + "type": "BOOLEAN" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_image", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_movie", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_obj", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_python", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_sound", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_text", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_usd", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_volume", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "forward_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "global_scale", + "type": "FLOAT" + }, + { + "enumItems": [], + "identifier": "sort_method", + "type": "ENUM" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "up_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "use_facet_normal", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_mesh_validate", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "use_scene_unit", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "runtimeStatus": "AVAILABLE" + }, + "variants": [ + "STL_BINARY", + "STL_ASCII" + ] + }, + { + "export": { + "buildOption": "io_ply", + "buildOptionEnabled": true, + "operator": "wm.ply_export", + "properties": [ + { + "arrayLength": 0, + "identifier": "apply_modifiers", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "ascii_format", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "collection", + "type": "STRING" + }, + { + "enumItems": [ + "DEFAULT", + "LIST_VERTICAL", + "LIST_HORIZONTAL", + "THUMBNAIL" + ], + "identifier": "display_type", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_attributes", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "NONE", + "SRGB", + "LINEAR" + ], + "identifier": "export_colors", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "export_normals", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_selected_objects", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_triangulated_mesh", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "export_uv", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filemode", + "type": "INT" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_alembic", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_archive", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_backup", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blender", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blenlib", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_btx", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_folder", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_font", + "type": "BOOLEAN" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_image", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_movie", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_obj", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_python", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_sound", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_text", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_usd", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_volume", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "forward_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "global_scale", + "type": "FLOAT" + }, + { + "enumItems": [], + "identifier": "sort_method", + "type": "ENUM" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "up_axis", + "type": "ENUM" + } + ], + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "runtimeStatus": "AVAILABLE" + }, + "extensions": [ + ".ply" + ], + "family": "PLY", + "format": "PLY", + "import": { + "buildOption": "io_ply", + "buildOptionEnabled": true, + "operator": "wm.ply_import", + "properties": [ + { + "arrayLength": 0, + "identifier": "check_existing", + "type": "BOOLEAN" + }, + { + "identifier": "directory", + "type": "STRING" + }, + { + "enumItems": [ + "DEFAULT", + "LIST_VERTICAL", + "LIST_HORIZONTAL", + "THUMBNAIL" + ], + "identifier": "display_type", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "filemode", + "type": "INT" + }, + { + "identifier": "filepath", + "type": "STRING" + }, + { + "identifier": "files", + "type": "COLLECTION" + }, + { + "arrayLength": 0, + "identifier": "filter_alembic", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_archive", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_backup", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blender", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_blenlib", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_btx", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_folder", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_font", + "type": "BOOLEAN" + }, + { + "identifier": "filter_glob", + "type": "STRING" + }, + { + "arrayLength": 0, + "identifier": "filter_image", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_movie", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_obj", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_python", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_sound", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_text", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_usd", + "type": "BOOLEAN" + }, + { + "arrayLength": 0, + "identifier": "filter_volume", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "forward_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "global_scale", + "type": "FLOAT" + }, + { + "arrayLength": 0, + "identifier": "import_attributes", + "type": "BOOLEAN" + }, + { + "enumItems": [ + "NONE", + "SRGB", + "LINEAR" + ], + "identifier": "import_colors", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "merge_verts", + "type": "BOOLEAN" + }, + { + "enumItems": [], + "identifier": "sort_method", + "type": "ENUM" + }, + { + "enumItems": [ + "X", + "Y", + "Z", + "NEGATIVE_X", + "NEGATIVE_Y", + "NEGATIVE_Z" + ], + "identifier": "up_axis", + "type": "ENUM" + }, + { + "arrayLength": 0, + "identifier": "use_scene_unit", + "type": "BOOLEAN" + } + ], + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "runtimeStatus": "AVAILABLE" + }, + "variants": [ + "PLY" + ] + }, + { + "export": { + "buildOption": "usd", + "buildOptionEnabled": null, + "error": "KeyError", + "operator": "wm.usd_export", + "properties": [], + "registered": false, + "rnaIdentifier": null, + "runtimeStatus": "OPERATOR_UNREGISTERED" + }, + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "family": "USD", + "format": "USD", + "import": { + "buildOption": "usd", + "buildOptionEnabled": null, + "error": "KeyError", + "operator": "wm.usd_import", + "properties": [], + "registered": false, + "rnaIdentifier": null, + "runtimeStatus": "OPERATOR_UNREGISTERED" + }, + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ] + }, + { + "export": { + "buildOption": "alembic", + "buildOptionEnabled": null, + "error": "KeyError", + "operator": "wm.alembic_export", + "properties": [], + "registered": false, + "rnaIdentifier": null, + "runtimeStatus": "OPERATOR_UNREGISTERED" + }, + "extensions": [ + ".abc" + ], + "family": "ALEMBIC", + "format": "ALEMBIC", + "import": { + "buildOption": "alembic", + "buildOptionEnabled": null, + "error": "KeyError", + "operator": "wm.alembic_import", + "properties": [], + "registered": false, + "rnaIdentifier": null, + "runtimeStatus": "OPERATOR_UNREGISTERED" + }, + "variants": [ + "ALEMBIC" + ] + } + ], + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "task": "M12-05A" +} diff --git a/tests/golden/M12-05A/manifest.json b/tests/golden/M12-05A/manifest.json new file mode 100644 index 00000000..fa59980a --- /dev/null +++ b/tests/golden/M12-05A/manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "task": "M12-05A", + "parentTask": "M12-04J", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_RUNTIME", + "operation": "IO_FORMAT_RUNTIME_INVENTORY", + "assertions": { + "blenderVersion": "5.2.0 LTS", + "formatCount": 7, + "formats": ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"], + "availableFormats": ["GLTF", "GLB", "OBJ", "STL", "PLY"], + "buildDisabledFormats": ["USD", "ALEMBIC"], + "runtimeReceipt": true, + "deterministicRegeneration": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-04J/manifest.json", "sha256": "989d417ab44e165849c7054f331f7038ec1d779349c39dd4f5b30050bbaecf56" }, + "generator": { "path": "tools/web/generate-io-format-runtime-inventory.py", "sha256": "aa926397664240a5195f8864fc3ed5549bafcc963a03c513c7e37926b0559d7d" }, + "checker": { "path": "tools/web/check-io-format-runtime-inventory.mjs", "sha256": "12853306ea5eb2698ab636c7dfc2f27ae140295641473c57b84f93fa13d4864e" }, + "inventory": { "path": "tests/golden/M12-05A/format-inventory.json", "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-05B" +} diff --git a/tests/golden/M12-05B/capability-matrix.json b/tests/golden/M12-05B/capability-matrix.json new file mode 100644 index 00000000..c5a59b8b --- /dev/null +++ b/tests/golden/M12-05B/capability-matrix.json @@ -0,0 +1,392 @@ +{ + "schemaVersion": 1, + "task": "M12-05B", + "runtimeInventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "formats": [ + { + "format": "GLTF", + "runtimeImportStatus": "AVAILABLE", + "runtimeExportStatus": "AVAILABLE", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + } + } + }, + { + "format": "GLB", + "runtimeImportStatus": "AVAILABLE", + "runtimeExportStatus": "AVAILABLE", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "READY", + "execution": "LOCAL", + "code": null + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "PARTIAL", + "evidence": "bounded GLB export gate exists; full format round-trip remains M12-06" + }, + "material": { + "status": "PARTIAL", + "evidence": "bounded GLB export gate exists; full format round-trip remains M12-06" + }, + "animation": { + "status": "PARTIAL", + "evidence": "bounded GLB export gate exists; full format round-trip remains M12-06" + } + } + } + }, + { + "format": "OBJ", + "runtimeImportStatus": "AVAILABLE", + "runtimeExportStatus": "AVAILABLE", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + } + } + }, + { + "format": "STL", + "runtimeImportStatus": "AVAILABLE", + "runtimeExportStatus": "AVAILABLE", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + } + } + }, + { + "format": "PLY", + "runtimeImportStatus": "AVAILABLE", + "runtimeExportStatus": "AVAILABLE", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + } + } + }, + { + "format": "USD", + "runtimeImportStatus": "OPERATOR_UNREGISTERED", + "runtimeExportStatus": "OPERATOR_UNREGISTERED", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + } + } + }, + { + "format": "ALEMBIC", + "runtimeImportStatus": "OPERATOR_UNREGISTERED", + "runtimeExportStatus": "OPERATOR_UNREGISTERED", + "operations": { + "IMPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + }, + "EXPORT": { + "local": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "server": { + "status": "BLOCKED", + "execution": "NONE", + "code": "IO_FORMAT_UNSUPPORTED" + }, + "geometry": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "material": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + }, + "animation": { + "status": "UNVERIFIED", + "evidence": "no verified Web executor for this format/operation" + } + } + } + } + ] +} diff --git a/tests/golden/M12-05B/manifest.json b/tests/golden/M12-05B/manifest.json new file mode 100644 index 00000000..c4e839be --- /dev/null +++ b/tests/golden/M12-05B/manifest.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "task": "M12-05B", + "parentTask": "M12-05A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_CAPABILITY_MATRIX_PROTOCOL", + "operation": "IO_FORMAT_CAPABILITY_MATRIX", + "assertions": { + "formatCount": 7, + "localGlbExportReady": true, + "blockedImportRoutes": 7, + "blockedServerRoutes": 14, + "unverifiedUnimplementedFeatures": true, + "runtimeBinding": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-05A/manifest.json", "sha256": "202b144c91f8e2c39477e257aeb26f9bd0b1b05b459ff8a246668024e94deec7" }, + "protocol": { "path": "web/protocol/io-format-capability-matrix.ts", "sha256": "3063ae5e45f5b1642d329aa554187d121998d816a5a6740a2b9662f00c3c5738" }, + "generator": { "path": "tools/web/generate-io-format-capability-matrix.mjs", "sha256": "746078caaf29fa5aa2281b901b9ea5fc66f9a935eb3f6e282d4fbfb018d4c390" }, + "checker": { "path": "tools/web/check-io-format-capability-matrix.mjs", "sha256": "4a8b35cd7f87238c13627a00c3bb0b34c130fc34d597773574efac7d8909b804" }, + "unit": { "path": "web/tests/unit/io-format-capability-matrix.test.mjs", "sha256": "0cc4d8f1df1ecdab20d8100cf5f12b44cb2a68bca4384ef7964a032b2f74b36e" }, + "matrix": { "path": "tests/golden/M12-05B/capability-matrix.json", "sha256": "139c9d764736da176b32414ecda840c07eb0ba5f3864da2dc08ce04bae161366" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-05C" +} diff --git a/tests/golden/M12-05C/manifest.json b/tests/golden/M12-05C/manifest.json new file mode 100644 index 00000000..7d151005 --- /dev/null +++ b/tests/golden/M12-05C/manifest.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "task": "M12-05C", + "parentTask": "M12-05B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_IO_FORMAT_UI_CAPABILITY_GATE", + "operation": "IO_FORMAT_UI_GATE", + "assertions": { + "matrixBound": true, + "projectFileAccept": ".blend,application/octet-stream", + "importRoutes": 0, + "localExportRoutes": 1, + "blockedRoutesHidden": true, + "operatorSearchUsesRegistry": true, + "unsupportedSelectionFailClosed": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-05B/manifest.json", "sha256": "8cc9517c9f25138c351bc5a79efe3b1ce6d9f6663d3b7c14397c5e4e8f11181a" }, + "protocol": { "path": "web/protocol/io-format-ui-gate.ts", "sha256": "fc397ceb947d4ede6c34c1d51438253a6b59244fc40f5eb77ce155bf1c477476" }, + "generator": { "path": "tools/web/generate-io-format-ui-gate.mjs", "sha256": "1ef4ae8a3e8d2f73101a87cba1c42ea99a065e1f6846f6a591841b97c0c39e6f" }, + "checker": { "path": "tools/web/check-io-format-ui-gate.mjs", "sha256": "81d6f27df26aab7b633fbe61c6d7b37519668aff41e43314924dceaacb3affde" }, + "unit": { "path": "web/tests/unit/io-format-ui-gate.test.mjs", "sha256": "84ca8fb6022da9f167daa104c44489a6c7d9f059699e57ce621b8d7f64f19082" }, + "chromium": { "path": "web/tests/e2e/io-format-ui-gate.spec.ts", "sha256": "eda2cdb9ede3bcbd717800c9c6fdb28d8cebef96f0296a2ee847a05e60cd0eed" }, + "registry": { "path": "web/app/src/capabilities/io-format-ui-registry.json", "sha256": "6b410bc6f2cad032af55bf13e1c8ddd841b01e9913ffc0ba381da8b7204285fd" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-05D" +} diff --git a/tests/golden/M12-05D/manifest.json b/tests/golden/M12-05D/manifest.json new file mode 100644 index 00000000..5ad5476e --- /dev/null +++ b/tests/golden/M12-05D/manifest.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "task": "M12-05D", + "parentTask": "M12-05C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_RUNTIME_RECEIPT_GATE", + "operation": "IO_FORMAT_RUNTIME_RECEIPT", + "assertions": { + "inventoryBound": true, + "formatCount": 7, + "receiptCount": 14, + "glbExport": "READY", + "usdExport": "BLOCKED", + "extensionIndependent": true, + "uiExecutorBound": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-05C/manifest.json", "sha256": "f46fd394e2144255d8b4304e8ce4fe60aad4302a80e991fd83d50cd915235ee3" }, + "protocol": { "path": "web/protocol/io-format-runtime-receipt.ts", "sha256": "461a84557fa368c29dbc6707959b689faae7c8f7050dccc4d3f12e358b61bb22" }, + "generator": { "path": "tools/web/generate-io-format-runtime-receipts.mjs", "sha256": "796cd641e86d8242c13317f771ae237cbf1692ff675a53bbdb689615edb5f372" }, + "checker": { "path": "tools/web/check-io-format-runtime-receipts.mjs", "sha256": "aaf9862041f155f96f50ea8481ff765089255bc59ecd8944f12362b81b8c1f1a" }, + "unit": { "path": "web/tests/unit/io-format-runtime-receipt.test.mjs", "sha256": "a5f09277f5dcdacd25c44191f7407d6cee944f8022b1c467c2a69e172fc2ac6b" }, + "runtimeReceipts": { "path": "tests/golden/M12-05D/runtime-receipts.json", "sha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b" }, + "appReceipts": { "path": "web/app/src/capabilities/io-format-runtime-receipts.json", "sha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b" }, + "app": { "path": "web/app/src/app/App.tsx", "sha256": "74216aac70992f8d879e983237ca324b998008582f0cd4658e90994cf9fae0a8" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-05E" +} diff --git a/tests/golden/M12-05D/runtime-receipts.json b/tests/golden/M12-05D/runtime-receipts.json new file mode 100644 index 00000000..77eb5fcc --- /dev/null +++ b/tests/golden/M12-05D/runtime-receipts.json @@ -0,0 +1,282 @@ +{ + "schemaVersion": 1, + "task": "M12-05D", + "inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "receipts": [ + { + "format": "GLTF", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ] + }, + { + "format": "GLTF", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ] + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ] + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ] + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "IMPORT", + "operator": "wm.obj_import", + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ] + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "EXPORT", + "operator": "wm.obj_export", + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ] + }, + { + "format": "STL", + "family": "STL", + "operation": "IMPORT", + "operator": "wm.stl_import", + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ] + }, + { + "format": "STL", + "family": "STL", + "operation": "EXPORT", + "operator": "wm.stl_export", + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ] + }, + { + "format": "PLY", + "family": "PLY", + "operation": "IMPORT", + "operator": "wm.ply_import", + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ] + }, + { + "format": "PLY", + "family": "PLY", + "operation": "EXPORT", + "operator": "wm.ply_export", + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ] + }, + { + "format": "USD", + "family": "USD", + "operation": "IMPORT", + "operator": "wm.usd_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ] + }, + { + "format": "USD", + "family": "USD", + "operation": "EXPORT", + "operator": "wm.usd_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ] + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "IMPORT", + "operator": "wm.alembic_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ] + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "EXPORT", + "operator": "wm.alembic_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ] + } + ] +} diff --git a/tests/golden/M12-05E/bound-runtime-receipts.json b/tests/golden/M12-05E/bound-runtime-receipts.json new file mode 100644 index 00000000..0175d2bc --- /dev/null +++ b/tests/golden/M12-05E/bound-runtime-receipts.json @@ -0,0 +1,325 @@ +{ + "schemaVersion": 1, + "task": "M12-05E", + "parentReceiptSetSha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b", + "inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "receipts": [ + { + "format": "GLTF", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "843c7eb040189ccd7fcccfb697c4ecfd35d405add50008967b7ca5a89e4dcde7", + "settingsSha256": "01a5fbe96ab7af4bf38c35a3723a7619233299086e400ff5064dbd91e9d586e8", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLTF", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "a1c2dea067898071d6d4f0fc4f83e81df920bc572a9250ed01229d618ef15a37", + "settingsSha256": "df7db92e5ea9a4d52a81dc8092f48ca9dfda80594e500b05f3787352891962f9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "d78e336432dc578727992b8ca53368e3ac49ffdafeb1c16847fe48c54e35a079", + "settingsSha256": "b909a16f4103dfad49997c597c80ad3e71a932989f8a4594eb4f019223bd56e6", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "0c2820eb9d7b82a1cd1cb208f75f263a527c58576952d4bd934cf0f7eb29ee3e", + "settingsSha256": "3b375dcb889e594e61da9d8e912037d8fa0220ea876e7465e6c37da4f5b21cee", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "IMPORT", + "operator": "wm.obj_import", + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "42f9e3b35f753e43100aaea618ed306f1bf062fb7bb44af841a2e2d599449fbd", + "settingsSha256": "4e879a3018ffcf529c28fb54e09efe5f3829b501a2b001da86f9a9b4b303bccb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "EXPORT", + "operator": "wm.obj_export", + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "b4328f82639bdfefb016aec15629135ebd080e0a5696759dd670ab85168b7c60", + "settingsSha256": "f7660d5742890d2f05e8da803f5d8616233570a4f06960b85a2142efd9396d2f", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "IMPORT", + "operator": "wm.stl_import", + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "f6bb7a058c246914f5f077665aab1f3d45c60b176f917b15a54522d12164c703", + "settingsSha256": "b01bd0b819aa72cf1de817b3e9bca2df03b9ceac41f639f738176973fea9accb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "EXPORT", + "operator": "wm.stl_export", + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "8be11ddd9bd68dcddc676529d30abcc236289f25ece32e137fd79bcd5ff3286d", + "settingsSha256": "5f1797ab8291fb3d84a8c1a892aa692a120a7db4ef84c9d3a9b1e78d5a6d92b7", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "IMPORT", + "operator": "wm.ply_import", + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2c8483351e293c5e0450f55902c24e3346bf95d53243ebf194fccc1efc1caa80", + "settingsSha256": "390cbc8a4843d88bd567a7f7d51b9e0d5853992bfc7a66c4a2fd335ed33d25a3", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "EXPORT", + "operator": "wm.ply_export", + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2f6cf92d5a813083b206b9c38a4442f82685b1c5740f313b90a0a7b60604a2b5", + "settingsSha256": "cd4bf21d72086001a02377cdc5c7f22856cbd1e04b261e37484260f3fc2ea6ae", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "IMPORT", + "operator": "wm.usd_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "a8f79c9243ffa9ba0ba8e3e948c198fdffa727bac578269d8ec041f56cad1c5d", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "EXPORT", + "operator": "wm.usd_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "cf6a9737773c27faf82cd8b3d4df84a9b671e1c873cd5041f12d70926ec62abf", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "IMPORT", + "operator": "wm.alembic_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "7471c261ab8520df718399e69f6f8d73be11fb76f1717eac9a407964fc2f7ee3", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "EXPORT", + "operator": "wm.alembic_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "8d983f6f8c5bb722d2b12c47c56115ae4bdff0a173a9fb9f4d93f083bea8e513", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + } + ] +} diff --git a/tests/golden/M12-05E/manifest.json b/tests/golden/M12-05E/manifest.json new file mode 100644 index 00000000..b7c617dc --- /dev/null +++ b/tests/golden/M12-05E/manifest.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "task": "M12-05E", + "parentTask": "M12-05D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_BOUND_RUNTIME_RECEIPT", + "operation": "IO_FORMAT_RECEIPT_BINDING", + "assertions": { + "receiptCount": 14, + "sourceHashBound": true, + "settingsHashBound": true, + "runtimeHashBound": true, + "parentInventoryBound": true, + "deterministic": true, + "packageHashPreserved": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-05D/manifest.json", "sha256": "3d0049a7b0331f01bb71df043270c18d1a5c9ce6dc74353c49c8ce591761df1b" }, + "protocol": { "path": "web/protocol/io-format-receipt-binding.ts", "sha256": "681e719ab2b18eb85d056547fb70b461dd73b3a7fb4fdbe6295b8e49d5649e49" }, + "generator": { "path": "tools/web/generate-io-format-receipt-bindings.mjs", "sha256": "eb7b5c499f141c1788bc37e53585662eedff3f2dabff870f4ad166f4a619796f" }, + "checker": { "path": "tools/web/check-io-format-receipt-bindings.mjs", "sha256": "ec2b22b468809ecc25ab2d4983065680a66ad3be6705d1827b44bf45ac622e26" }, + "unit": { "path": "web/tests/unit/io-format-receipt-binding.test.mjs", "sha256": "e9ae3e360ad41c32da3634a4efa915654853e79a35df36728c1ddf586a0bf7b5" }, + "boundReceipts": { "path": "tests/golden/M12-05E/bound-runtime-receipts.json", "sha256": "7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-05F" +} diff --git a/tests/golden/M12-05F/fresh-runtime-receipts.json b/tests/golden/M12-05F/fresh-runtime-receipts.json new file mode 100644 index 00000000..ef996cbd --- /dev/null +++ b/tests/golden/M12-05F/fresh-runtime-receipts.json @@ -0,0 +1,332 @@ +{ + "schemaVersion": 1, + "task": "M12-05F", + "parentBindingSha256": "7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad", + "boundReceiptSetSha256": "2015d719a2046f76dda3daf7c8ae7a3fc5183a499489ef06a0c33f166c6ea0b9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6", + "bound": { + "schemaVersion": 1, + "task": "M12-05E", + "parentReceiptSetSha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b", + "inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "receipts": [ + { + "format": "GLTF", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "843c7eb040189ccd7fcccfb697c4ecfd35d405add50008967b7ca5a89e4dcde7", + "settingsSha256": "01a5fbe96ab7af4bf38c35a3723a7619233299086e400ff5064dbd91e9d586e8", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLTF", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "a1c2dea067898071d6d4f0fc4f83e81df920bc572a9250ed01229d618ef15a37", + "settingsSha256": "df7db92e5ea9a4d52a81dc8092f48ca9dfda80594e500b05f3787352891962f9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "d78e336432dc578727992b8ca53368e3ac49ffdafeb1c16847fe48c54e35a079", + "settingsSha256": "b909a16f4103dfad49997c597c80ad3e71a932989f8a4594eb4f019223bd56e6", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "0c2820eb9d7b82a1cd1cb208f75f263a527c58576952d4bd934cf0f7eb29ee3e", + "settingsSha256": "3b375dcb889e594e61da9d8e912037d8fa0220ea876e7465e6c37da4f5b21cee", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "IMPORT", + "operator": "wm.obj_import", + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "42f9e3b35f753e43100aaea618ed306f1bf062fb7bb44af841a2e2d599449fbd", + "settingsSha256": "4e879a3018ffcf529c28fb54e09efe5f3829b501a2b001da86f9a9b4b303bccb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "EXPORT", + "operator": "wm.obj_export", + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "b4328f82639bdfefb016aec15629135ebd080e0a5696759dd670ab85168b7c60", + "settingsSha256": "f7660d5742890d2f05e8da803f5d8616233570a4f06960b85a2142efd9396d2f", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "IMPORT", + "operator": "wm.stl_import", + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "f6bb7a058c246914f5f077665aab1f3d45c60b176f917b15a54522d12164c703", + "settingsSha256": "b01bd0b819aa72cf1de817b3e9bca2df03b9ceac41f639f738176973fea9accb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "EXPORT", + "operator": "wm.stl_export", + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "8be11ddd9bd68dcddc676529d30abcc236289f25ece32e137fd79bcd5ff3286d", + "settingsSha256": "5f1797ab8291fb3d84a8c1a892aa692a120a7db4ef84c9d3a9b1e78d5a6d92b7", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "IMPORT", + "operator": "wm.ply_import", + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2c8483351e293c5e0450f55902c24e3346bf95d53243ebf194fccc1efc1caa80", + "settingsSha256": "390cbc8a4843d88bd567a7f7d51b9e0d5853992bfc7a66c4a2fd335ed33d25a3", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "EXPORT", + "operator": "wm.ply_export", + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2f6cf92d5a813083b206b9c38a4442f82685b1c5740f313b90a0a7b60604a2b5", + "settingsSha256": "cd4bf21d72086001a02377cdc5c7f22856cbd1e04b261e37484260f3fc2ea6ae", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "IMPORT", + "operator": "wm.usd_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "a8f79c9243ffa9ba0ba8e3e948c198fdffa727bac578269d8ec041f56cad1c5d", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "EXPORT", + "operator": "wm.usd_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "cf6a9737773c27faf82cd8b3d4df84a9b671e1c873cd5041f12d70926ec62abf", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "IMPORT", + "operator": "wm.alembic_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "7471c261ab8520df718399e69f6f8d73be11fb76f1717eac9a407964fc2f7ee3", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "EXPORT", + "operator": "wm.alembic_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "8d983f6f8c5bb722d2b12c47c56115ae4bdff0a173a9fb9f4d93f083bea8e513", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + } + ] + } +} diff --git a/tests/golden/M12-05F/manifest.json b/tests/golden/M12-05F/manifest.json new file mode 100644 index 00000000..60ed60f4 --- /dev/null +++ b/tests/golden/M12-05F/manifest.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": 1, + "task": "M12-05F", + "parentTask": "M12-05E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_RUNTIME_RECEIPT_FRESHNESS", + "operation": "IO_FORMAT_RECEIPT_FRESHNESS_GATE", + "assertions": { + "receiptCount": 14, + "parentBindingBound": true, + "canonicalReceiptSetHash": true, + "runtimeIdentityHash": true, + "forgedReceiptBlocked": true, + "staleReceiptBlocked": true, + "crossVersionReceiptBlocked": true, + "appRouteBound": true, + "deterministic": true, + "packageHashPreserved": true + }, + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-05E/manifest.json", "sha256": "2fbaaf39c6acd9f55fbdbadccc0b027e9e7763bf069c954a96ca49b193648990" }, + "protocol": { "path": "web/protocol/io-format-receipt-freshness.ts", "sha256": "111a630c7beccd31989dc4f78932b7d7b741fb6bef03e45cb39a8139f774d235" }, + "generator": { "path": "tools/web/generate-io-format-receipt-freshness.mjs", "sha256": "6a1e798ce46e1d14d833091d4d7ae452dccb13efe583594277785465eb725bbf" }, + "checker": { "path": "tools/web/check-io-format-receipt-freshness.mjs", "sha256": "7a8fe69ea1aa2c1e121be008a9fb60fc236f7ef776d2088a7b00b14f46fe3fba" }, + "unit": { "path": "web/tests/unit/io-format-receipt-freshness.test.mjs", "sha256": "cd1c2adca81653f98f9a1c6c7d104ad0e3052283213fb7166dac827c956928fe" }, + "freshnessReceipts": { "path": "tests/golden/M12-05F/fresh-runtime-receipts.json", "sha256": "0187ad0d9ea05fc4b152b7abd4945191dbe9ec24dfde4ca7dbe4aadfd1230efe" }, + "appFreshnessReceipts": { "path": "web/app/src/capabilities/io-format-runtime-receipts-freshness.json", "sha256": "0187ad0d9ea05fc4b152b7abd4945191dbe9ec24dfde4ca7dbe4aadfd1230efe" }, + "appExpected": { "path": "web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json", "sha256": "8d65f78aa774ff252871cb1cc09e69b4ff04fbb45e61200eaf67534c9d2651b2" }, + "app": { "path": "web/app/src/app/App.tsx", "sha256": "043ff3a2f39ef3a1303791081b80851b427e7db5dfd9af2161926dd6f1ea9ca4" }, + "package": { "path": "web/package.json", "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" } + }, + "nextTask": "M12-06A" +} diff --git a/tests/golden/M12-06A/desktop-fixtures.json b/tests/golden/M12-06A/desktop-fixtures.json new file mode 100644 index 00000000..e4fcadae --- /dev/null +++ b/tests/golden/M12-06A/desktop-fixtures.json @@ -0,0 +1,697 @@ +{ + "fixtureCount": 5, + "fixtures": [ + { + "byteLength": 1236, + "file": "mesh.glb", + "id": "mesh", + "semantic": { + "animations": [], + "asset": { + "generator": "Khronos glTF Blender I/O v5.2.39", + "version": "2.0" + }, + "extensionsRequired": [], + "extensionsUsed": [], + "images": [], + "materials": [ + { + "alphaMode": "OPAQUE", + "doubleSided": true, + "emissiveFactor": null, + "name": "M12 Mesh Material", + "normalTexture": null, + "pbr": { + "baseColorFactor": null, + "baseColorTexture": null, + "metallicFactor": 0.15000000596046448, + "roughnessFactor": 0.550000011920929 + } + } + ], + "meshes": [ + { + "name": "M12 Mesh Fixture Mesh", + "primitives": [ + { + "attributes": { + "COLOR_0": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + }, + "NORMAL": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + }, + "POSITION": { + "componentType": 5126, + "count": 4, + "max": [ + 1, + 0, + 1 + ], + "min": [ + -1, + 0, + -1 + ], + "normalized": false, + "type": "VEC3" + } + }, + "indices": { + "componentType": 5123, + "count": 6, + "max": null, + "min": null, + "normalized": false, + "type": "SCALAR" + }, + "material": 0, + "mode": 4, + "targets": [] + } + ] + } + ], + "nodeNames": [ + "M12 Mesh Fixture" + ], + "nodes": [ + { + "children": [], + "mesh": 0, + "name": "M12 Mesh Fixture", + "rotation": null, + "scale": null, + "skin": null, + "translation": null + } + ], + "samplers": [], + "scene": 0, + "skins": [], + "textures": [] + }, + "sha256": "e52b9268b6524744fb498691f976000635e544ba3b47e9f8ff22b03bcdf17a94" + }, + { + "byteLength": 1208, + "file": "pbr.glb", + "id": "pbr", + "semantic": { + "animations": [], + "asset": { + "generator": "Khronos glTF Blender I/O v5.2.39", + "version": "2.0" + }, + "extensionsRequired": [], + "extensionsUsed": [], + "images": [], + "materials": [ + { + "alphaMode": "OPAQUE", + "doubleSided": true, + "emissiveFactor": [ + 0.029999999329447746, + 0.05999999865889549, + 0.11999999731779099 + ], + "name": "M12 PBR Material", + "normalTexture": null, + "pbr": { + "baseColorFactor": [ + 0.3100000023841858, + 0.5699999928474426, + 0.9100000262260437, + 1 + ], + "baseColorTexture": null, + "metallicFactor": 0.7200000286102295, + "roughnessFactor": 0.2800000011920929 + } + } + ], + "meshes": [ + { + "name": "M12 PBR Fixture Mesh", + "primitives": [ + { + "attributes": { + "NORMAL": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + }, + "POSITION": { + "componentType": 5126, + "count": 4, + "max": [ + 1, + 0, + 1 + ], + "min": [ + -1, + 0, + -1 + ], + "normalized": false, + "type": "VEC3" + } + }, + "indices": { + "componentType": 5123, + "count": 6, + "max": null, + "min": null, + "normalized": false, + "type": "SCALAR" + }, + "material": 0, + "mode": 4, + "targets": [] + } + ] + } + ], + "nodeNames": [ + "M12 PBR Fixture" + ], + "nodes": [ + { + "children": [], + "mesh": 0, + "name": "M12 PBR Fixture", + "rotation": null, + "scale": null, + "skin": null, + "translation": null + } + ], + "samplers": [], + "scene": 0, + "skins": [], + "textures": [] + }, + "sha256": "244cc8a992c5a70692b8bbc8333533718b3bac7022110715ce3b23d2e4c0b361" + }, + { + "byteLength": 1704, + "file": "uv.glb", + "id": "uv", + "semantic": { + "animations": [], + "asset": { + "generator": "Khronos glTF Blender I/O v5.2.39", + "version": "2.0" + }, + "extensionsRequired": [], + "extensionsUsed": [], + "images": [ + { + "bufferView": 4, + "mimeType": "image/png", + "name": "M12 UV Texture" + } + ], + "materials": [ + { + "alphaMode": "OPAQUE", + "doubleSided": true, + "emissiveFactor": null, + "name": "M12 UV Material", + "normalTexture": null, + "pbr": { + "baseColorFactor": null, + "baseColorTexture": { + "index": 0 + }, + "metallicFactor": 0, + "roughnessFactor": 0.44999998807907104 + } + } + ], + "meshes": [ + { + "name": "M12 UV Fixture Mesh", + "primitives": [ + { + "attributes": { + "NORMAL": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + }, + "POSITION": { + "componentType": 5126, + "count": 4, + "max": [ + 1, + 0, + 1 + ], + "min": [ + -1, + 0, + -1 + ], + "normalized": false, + "type": "VEC3" + }, + "TEXCOORD_0": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC2" + } + }, + "indices": { + "componentType": 5123, + "count": 6, + "max": null, + "min": null, + "normalized": false, + "type": "SCALAR" + }, + "material": 0, + "mode": 4, + "targets": [] + } + ] + } + ], + "nodeNames": [ + "M12 UV Fixture" + ], + "nodes": [ + { + "children": [], + "mesh": 0, + "name": "M12 UV Fixture", + "rotation": null, + "scale": null, + "skin": null, + "translation": null + } + ], + "samplers": [ + { + "magFilter": 9728, + "minFilter": 9984 + } + ], + "scene": 0, + "skins": [], + "textures": [ + { + "sampler": 0, + "source": 0 + } + ] + }, + "sha256": "1e0397d2b69d8261b3252451b50ab4aba6525b94713719f35069a9a9d96fcfa2" + }, + { + "byteLength": 1912, + "file": "skin.glb", + "id": "skin", + "semantic": { + "animations": [], + "asset": { + "generator": "Khronos glTF Blender I/O v5.2.39", + "version": "2.0" + }, + "extensionsRequired": [], + "extensionsUsed": [], + "images": [], + "materials": [ + { + "alphaMode": "OPAQUE", + "doubleSided": true, + "emissiveFactor": null, + "name": "M12 Skin Material", + "normalTexture": null, + "pbr": { + "baseColorFactor": [ + 0.7599999904632568, + 0.23999999463558197, + 0.18000000715255737, + 1 + ], + "baseColorTexture": null, + "metallicFactor": 0.05000000074505806, + "roughnessFactor": 0.5 + } + } + ], + "meshes": [ + { + "name": "M12 Skin Fixture Mesh", + "primitives": [ + { + "attributes": { + "JOINTS_0": { + "componentType": 5121, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC4" + }, + "NORMAL": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + }, + "POSITION": { + "componentType": 5126, + "count": 4, + "max": [ + 1, + 0, + 0.5 + ], + "min": [ + -1, + 0, + -0.5 + ], + "normalized": false, + "type": "VEC3" + }, + "WEIGHTS_0": { + "componentType": 5126, + "count": 4, + "max": null, + "min": null, + "normalized": false, + "type": "VEC4" + } + }, + "indices": { + "componentType": 5123, + "count": 6, + "max": null, + "min": null, + "normalized": false, + "type": "SCALAR" + }, + "material": 0, + "mode": 4, + "targets": [] + } + ] + } + ], + "nodeNames": [ + "Tip", + "Root", + "M12 Skin Fixture", + "M12 Skin Armature" + ], + "nodes": [ + { + "children": [], + "mesh": null, + "name": "Tip", + "rotation": null, + "scale": null, + "skin": null, + "translation": [ + 0, + 1, + 0 + ] + }, + { + "children": [ + 0 + ], + "mesh": null, + "name": "Root", + "rotation": null, + "scale": null, + "skin": null, + "translation": null + }, + { + "children": [], + "mesh": 0, + "name": "M12 Skin Fixture", + "rotation": null, + "scale": null, + "skin": 0, + "translation": null + }, + { + "children": [ + 2, + 1 + ], + "mesh": null, + "name": "M12 Skin Armature", + "rotation": null, + "scale": null, + "skin": null, + "translation": null + } + ], + "samplers": [], + "scene": 0, + "skins": [ + { + "inverseBindMatrices": { + "componentType": 5126, + "count": 2, + "max": null, + "min": null, + "normalized": false, + "type": "MAT4" + }, + "joints": [ + 1, + 0 + ], + "name": "M12 Skin Armature", + "skeleton": null + } + ], + "textures": [] + }, + "sha256": "4086ee4c8873aa03090338b676575b7a5335fb7c9384fec6ccb36108e71929f6" + }, + { + "byteLength": 2616, + "file": "animation.glb", + "id": "animation", + "semantic": { + "animations": [ + { + "channels": [ + { + "sampler": 0, + "target": { + "node": 0, + "path": "translation" + } + }, + { + "sampler": 1, + "target": { + "node": 0, + "path": "rotation" + } + } + ], + "name": "M12 Animation Action", + "samplers": [ + { + "input": { + "componentType": 5126, + "count": 25, + "max": [ + 1.0416666666666667 + ], + "min": [ + 0.041666666666666664 + ], + "normalized": false, + "type": "SCALAR" + }, + "interpolation": "LINEAR", + "output": { + "componentType": 5126, + "count": 25, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + } + }, + { + "input": { + "componentType": 5126, + "count": 25, + "max": [ + 1.0416666666666667 + ], + "min": [ + 0.041666666666666664 + ], + "normalized": false, + "type": "SCALAR" + }, + "interpolation": "LINEAR", + "output": { + "componentType": 5126, + "count": 25, + "max": null, + "min": null, + "normalized": false, + "type": "VEC4" + } + } + ] + } + ], + "asset": { + "generator": "Khronos glTF Blender I/O v5.2.39", + "version": "2.0" + }, + "extensionsRequired": [], + "extensionsUsed": [], + "images": [], + "materials": [ + { + "alphaMode": "OPAQUE", + "doubleSided": true, + "emissiveFactor": null, + "name": "M12 Animation Material", + "normalTexture": null, + "pbr": { + "baseColorFactor": [ + 0.1599999964237213, + 0.7200000286102295, + 0.3799999952316284, + 1 + ], + "baseColorTexture": null, + "metallicFactor": 0.10000000149011612, + "roughnessFactor": 0.4000000059604645 + } + } + ], + "meshes": [ + { + "name": "M12 Animation Fixture Mesh", + "primitives": [ + { + "attributes": { + "NORMAL": { + "componentType": 5126, + "count": 3, + "max": null, + "min": null, + "normalized": false, + "type": "VEC3" + }, + "POSITION": { + "componentType": 5126, + "count": 3, + "max": [ + 0.75, + 0, + 0.75 + ], + "min": [ + -0.75, + 0, + -0.75 + ], + "normalized": false, + "type": "VEC3" + } + }, + "indices": { + "componentType": 5123, + "count": 3, + "max": null, + "min": null, + "normalized": false, + "type": "SCALAR" + }, + "material": 0, + "mode": 4, + "targets": [] + } + ] + } + ], + "nodeNames": [ + "M12 Animation Fixture" + ], + "nodes": [ + { + "children": [], + "mesh": 0, + "name": "M12 Animation Fixture", + "rotation": null, + "scale": null, + "skin": null, + "translation": [ + -1, + 0, + 0 + ] + } + ], + "samplers": [], + "scene": 0, + "skins": [], + "textures": [] + }, + "sha256": "44f6cc47961a146dc97ea337ae31a8b64bb4ab213b716f81ec22129db704f1fb" + } + ], + "maxFixtureBytes": 524288, + "nextTask": "M12-06B", + "operation": "DESKTOP_GLB_FIXTURE_GENERATION", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "task": "M12-06A" +} diff --git a/tests/golden/M12-06A/manifest.json b/tests/golden/M12-06A/manifest.json new file mode 100644 index 00000000..fde669a5 --- /dev/null +++ b/tests/golden/M12-06A/manifest.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 1, + "task": "M12-06A", + "parentTask": "M12-05F", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "DESKTOP_GLB_FIXTURE_GENERATION", + "fixtureIds": [ + "mesh", + "pbr", + "uv", + "skin", + "animation" + ], + "fixtureCount": 5, + "maxFixtureBytes": 524288, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-05F/manifest.json", + "sha256": "63e1506801ddee2f1eaf64cad68a9d5c918405f642ca237c6a1ec249ff297623" + }, + "generator": { + "path": "tools/web/generate-glb-desktop-fixtures.py", + "sha256": "0247dd2405a68b42d99c2b005546ad2aa99b46416e3fe9489832467f6ea4eb4d" + }, + "checker": { + "path": "tools/web/check-glb-desktop-fixtures.mjs", + "sha256": "211ebdb66bf2e2d349455d5303e889a4a1ae3323639a89dd78b3bda574dd820b" + }, + "desktopReport": { + "path": "tests/golden/M12-06A/desktop-fixtures.json", + "sha256": "dea31cc861622166dc502b333bc177b70b66b54d9c62aaccc6522745dab5ae13" + }, + "meshFixture": { + "path": "tests/files/web/m12_glb_desktop_v1/mesh.glb", + "sha256": "e52b9268b6524744fb498691f976000635e544ba3b47e9f8ff22b03bcdf17a94" + }, + "pbrFixture": { + "path": "tests/files/web/m12_glb_desktop_v1/pbr.glb", + "sha256": "244cc8a992c5a70692b8bbc8333533718b3bac7022110715ce3b23d2e4c0b361" + }, + "uvFixture": { + "path": "tests/files/web/m12_glb_desktop_v1/uv.glb", + "sha256": "1e0397d2b69d8261b3252451b50ab4aba6525b94713719f35069a9a9d96fcfa2" + }, + "skinFixture": { + "path": "tests/files/web/m12_glb_desktop_v1/skin.glb", + "sha256": "4086ee4c8873aa03090338b676575b7a5335fb7c9384fec6ccb36108e71929f6" + }, + "animationFixture": { + "path": "tests/files/web/m12_glb_desktop_v1/animation.glb", + "sha256": "44f6cc47961a146dc97ea337ae31a8b64bb4ab213b716f81ec22129db704f1fb" + } + }, + "nextTask": "M12-06B" +} diff --git a/tests/golden/M12-06B/manifest.json b/tests/golden/M12-06B/manifest.json new file mode 100644 index 00000000..f92e9c9e --- /dev/null +++ b/tests/golden/M12-06B/manifest.json @@ -0,0 +1,63 @@ +{ + "schemaVersion": 1, + "task": "M12-06B", + "parentTask": "M12-06A", + "enablingTask": true, + "parityStateChange": false, + "runtime": "WEB_TYPESCRIPT_CHROMIUM_WORKER", + "operation": "WEB_GLB_IMPORT_SEMANTIC_COMPARISON", + "fixtureIds": [ + "mesh", + "pbr", + "uv", + "skin", + "animation" + ], + "comparedDomains": [ + "topology", + "attributes", + "materials", + "nodes", + "animations" + ], + "routeState": "BLOCKED_UNTIL_MAIN_PERSISTENCE", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06A/manifest.json", + "sha256": "e3554a1e739cbe3f217f6169130532365538b0c0eafbb97afc58d4d56c4287cb" + }, + "protocol": { + "path": "web/protocol/glb-import.ts", + "sha256": "45d9a7912c4a59a552789a8ea0dfaff5f1849f8d213f14d238de024b77acdb0d" + }, + "generator": { + "path": "tools/web/generate-glb-desktop-import-report.mjs", + "sha256": "6f97527b7da04c7b4f9b09c48b9f367d1270db9f7820498d33832a80754436c2" + }, + "checker": { + "path": "tools/web/check-glb-desktop-import.mjs", + "sha256": "804a4b5545d95d7ab1ba265469a981d0a10b71dab36f0c96283eb73b401443d2" + }, + "unit": { + "path": "web/tests/unit/glb-desktop-import.test.mjs", + "sha256": "6bf2a96b3e43e5fae93589ea5dcf98e7a69b10266378e442988198925286a8d8" + }, + "worker": { + "path": "web/app/src/workers/glb-desktop-import-test.worker.ts", + "sha256": "eb32049631113270fb62acb7ae9379e8ff9a03e38086db2fa309c6891cd707bf" + }, + "chromium": { + "path": "web/tests/e2e/glb-desktop-import.spec.ts", + "sha256": "fa3e06419c918406f24b5a4260523bd94cfe729a84786e9434517fdfed624d45" + }, + "webImportReport": { + "path": "tests/golden/M12-06B/web-import-report.json", + "sha256": "79933888cc5aa2d1e3639ec349ca4c31d028fe89f751ebb8d4342f06d15ecfc2" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-06C" +} diff --git a/tests/golden/M12-06B/web-import-report.json b/tests/golden/M12-06B/web-import-report.json new file mode 100644 index 00000000..7b74c6c5 --- /dev/null +++ b/tests/golden/M12-06B/web-import-report.json @@ -0,0 +1,220 @@ +{ + "schemaVersion": 1, + "task": "M12-06B", + "operation": "WEB_GLB_IMPORT_SEMANTIC_COMPARISON", + "parentManifestSha256": "e3554a1e739cbe3f217f6169130532365538b0c0eafbb97afc58d4d56c4287cb", + "fixtureReportSha256": "dea31cc861622166dc502b333bc177b70b66b54d9c62aaccc6522745dab5ae13", + "fixtureCount": 5, + "comparedDomains": [ + "topology", + "attributes", + "materials", + "nodes", + "animations" + ], + "allCompatible": true, + "comparisons": [ + { + "id": "mesh", + "file": "mesh.glb", + "sourceSha256": "e52b9268b6524744fb498691f976000635e544ba3b47e9f8ff22b03bcdf17a94", + "byteLength": 1236, + "desktopSemanticSha256": "8f9e9049e7d45c6ab0665853efacfd549548b972b12d99b9cf7c5046341c0040", + "webSemanticSha256": "8f9e9049e7d45c6ab0665853efacfd549548b972b12d99b9cf7c5046341c0040", + "compatible": true, + "mismatchCount": 0, + "topology": { + "meshCount": 1, + "primitiveCount": 1, + "indexCount": 6, + "modes": [ + 4 + ] + }, + "attributes": [ + "COLOR_0", + "NORMAL", + "POSITION" + ], + "materials": { + "count": 1, + "pbrCount": 1, + "texturedCount": 0 + }, + "nodes": { + "count": 1, + "namedCount": 1, + "hierarchyEdges": 0, + "skinnedCount": 0 + }, + "animations": { + "count": 0, + "channelPaths": [], + "sampleCounts": [] + } + }, + { + "id": "pbr", + "file": "pbr.glb", + "sourceSha256": "244cc8a992c5a70692b8bbc8333533718b3bac7022110715ce3b23d2e4c0b361", + "byteLength": 1208, + "desktopSemanticSha256": "6a44faf6de63ed31261f30a494921c18427779f04c01e3923b2cb92b43e29ad2", + "webSemanticSha256": "6a44faf6de63ed31261f30a494921c18427779f04c01e3923b2cb92b43e29ad2", + "compatible": true, + "mismatchCount": 0, + "topology": { + "meshCount": 1, + "primitiveCount": 1, + "indexCount": 6, + "modes": [ + 4 + ] + }, + "attributes": [ + "NORMAL", + "POSITION" + ], + "materials": { + "count": 1, + "pbrCount": 1, + "texturedCount": 0 + }, + "nodes": { + "count": 1, + "namedCount": 1, + "hierarchyEdges": 0, + "skinnedCount": 0 + }, + "animations": { + "count": 0, + "channelPaths": [], + "sampleCounts": [] + } + }, + { + "id": "uv", + "file": "uv.glb", + "sourceSha256": "1e0397d2b69d8261b3252451b50ab4aba6525b94713719f35069a9a9d96fcfa2", + "byteLength": 1704, + "desktopSemanticSha256": "c98257548ff7bf7e83f5a975a7a8988736452ffe39f68f1f48da17297c6ba9a4", + "webSemanticSha256": "c98257548ff7bf7e83f5a975a7a8988736452ffe39f68f1f48da17297c6ba9a4", + "compatible": true, + "mismatchCount": 0, + "topology": { + "meshCount": 1, + "primitiveCount": 1, + "indexCount": 6, + "modes": [ + 4 + ] + }, + "attributes": [ + "NORMAL", + "POSITION", + "TEXCOORD_0" + ], + "materials": { + "count": 1, + "pbrCount": 1, + "texturedCount": 1 + }, + "nodes": { + "count": 1, + "namedCount": 1, + "hierarchyEdges": 0, + "skinnedCount": 0 + }, + "animations": { + "count": 0, + "channelPaths": [], + "sampleCounts": [] + } + }, + { + "id": "skin", + "file": "skin.glb", + "sourceSha256": "4086ee4c8873aa03090338b676575b7a5335fb7c9384fec6ccb36108e71929f6", + "byteLength": 1912, + "desktopSemanticSha256": "f1be7ed4b7cf41dfc8a76cd291fa9deb8143c9ed85b2b189408d699447cb1241", + "webSemanticSha256": "f1be7ed4b7cf41dfc8a76cd291fa9deb8143c9ed85b2b189408d699447cb1241", + "compatible": true, + "mismatchCount": 0, + "topology": { + "meshCount": 1, + "primitiveCount": 1, + "indexCount": 6, + "modes": [ + 4 + ] + }, + "attributes": [ + "JOINTS_0", + "NORMAL", + "POSITION", + "WEIGHTS_0" + ], + "materials": { + "count": 1, + "pbrCount": 1, + "texturedCount": 0 + }, + "nodes": { + "count": 4, + "namedCount": 4, + "hierarchyEdges": 3, + "skinnedCount": 1 + }, + "animations": { + "count": 0, + "channelPaths": [], + "sampleCounts": [] + } + }, + { + "id": "animation", + "file": "animation.glb", + "sourceSha256": "44f6cc47961a146dc97ea337ae31a8b64bb4ab213b716f81ec22129db704f1fb", + "byteLength": 2616, + "desktopSemanticSha256": "51e28284353b913f01ba43947350fd24000712260886d67fc619dd0d24b1e46e", + "webSemanticSha256": "51e28284353b913f01ba43947350fd24000712260886d67fc619dd0d24b1e46e", + "compatible": true, + "mismatchCount": 0, + "topology": { + "meshCount": 1, + "primitiveCount": 1, + "indexCount": 3, + "modes": [ + 4 + ] + }, + "attributes": [ + "NORMAL", + "POSITION" + ], + "materials": { + "count": 1, + "pbrCount": 1, + "texturedCount": 0 + }, + "nodes": { + "count": 1, + "namedCount": 1, + "hierarchyEdges": 0, + "skinnedCount": 0 + }, + "animations": { + "count": 1, + "channelPaths": [ + "rotation", + "translation" + ], + "sampleCounts": [ + 25, + 25 + ] + } + } + ], + "routeState": "BLOCKED_UNTIL_MAIN_PERSISTENCE", + "nextTask": "M12-06C" +} diff --git a/tests/golden/M12-06C/desktop-main-report.json b/tests/golden/M12-06C/desktop-main-report.json new file mode 100644 index 00000000..bbe4cdc4 --- /dev/null +++ b/tests/golden/M12-06C/desktop-main-report.json @@ -0,0 +1,451 @@ +{ + "blenderVersion": "5.2.0 LTS", + "fixtureCount": 5, + "fixtures": [ + { + "blend": { + "byteLength": 88625, + "file": "mesh.blend", + "sha256": "66e29adc016f07e220019b6f24eb7e5016c684dca4b3102b20c8e836968d422d" + }, + "glb": { + "file": "mesh.glb", + "sha256": "e52b9268b6524744fb498691f976000635e544ba3b47e9f8ff22b03bcdf17a94" + }, + "graph": { + "actions": [], + "armatures": [], + "images": [], + "materials": [ + { + "name": "M12 Mesh Material", + "nodeNames": [ + "Color Attribute", + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 Mesh Material" + ], + "name": "M12 Mesh Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [], + "vertexCount": 4 + } + ], + "objects": [ + { + "children": [], + "data": "M12 Mesh Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 Mesh Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [], + "images": [], + "materials": [ + "material:M12 Mesh Material" + ], + "meshes": [ + "mesh:M12 Mesh Fixture Mesh" + ], + "objects": [ + "object:M12 Mesh Fixture" + ] + } + }, + "id": "mesh" + }, + { + "blend": { + "byteLength": 88389, + "file": "pbr.blend", + "sha256": "ba08aeb2876d82ddf731abe81d3a42041a1be36617d0b6324c870d5bcd4cc771" + }, + "glb": { + "file": "pbr.glb", + "sha256": "244cc8a992c5a70692b8bbc8333533718b3bac7022110715ce3b23d2e4c0b361" + }, + "graph": { + "actions": [], + "armatures": [], + "images": [], + "materials": [ + { + "name": "M12 PBR Material", + "nodeNames": [ + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 PBR Material" + ], + "name": "M12 PBR Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [], + "vertexCount": 4 + } + ], + "objects": [ + { + "children": [], + "data": "M12 PBR Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 PBR Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [], + "images": [], + "materials": [ + "material:M12 PBR Material" + ], + "meshes": [ + "mesh:M12 PBR Fixture Mesh" + ], + "objects": [ + "object:M12 PBR Fixture" + ] + } + }, + "id": "pbr" + }, + { + "blend": { + "byteLength": 89127, + "file": "uv.blend", + "sha256": "1270cb452d34d2fd7a19181a2b20f70b7a028bdd2db7cf1bba4e1003f7de0f48" + }, + "glb": { + "file": "uv.glb", + "sha256": "1e0397d2b69d8261b3252451b50ab4aba6525b94713719f35069a9a9d96fcfa2" + }, + "graph": { + "actions": [], + "armatures": [], + "images": [ + { + "mimeType": "PNG", + "name": "M12 UV Texture", + "packed": true, + "size": [ + 2, + 2 + ] + } + ], + "materials": [ + { + "name": "M12 UV Material", + "nodeNames": [ + "Image Texture", + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 UV Material" + ], + "name": "M12 UV Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [ + "UVMap" + ], + "vertexCount": 4 + } + ], + "objects": [ + { + "children": [], + "data": "M12 UV Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 UV Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [], + "images": [ + "image:M12 UV Texture" + ], + "materials": [ + "material:M12 UV Material" + ], + "meshes": [ + "mesh:M12 UV Fixture Mesh" + ], + "objects": [ + "object:M12 UV Fixture" + ] + } + }, + "id": "uv" + }, + { + "blend": { + "byteLength": 91781, + "file": "skin.blend", + "sha256": "23f175e44ec588c75db99d3f9134863fc3d7d1f192bbd815d5d1c4e3218bce0c" + }, + "glb": { + "file": "skin.glb", + "sha256": "4086ee4c8873aa03090338b676575b7a5335fb7c9384fec6ccb36108e71929f6" + }, + "graph": { + "actions": [], + "armatures": [ + { + "bones": [ + { + "name": "Root", + "parent": null + }, + { + "name": "Tip", + "parent": "Root" + } + ], + "name": "M12 Skin Armature" + } + ], + "images": [], + "materials": [ + { + "name": "M12 Skin Material", + "nodeNames": [ + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [], + "name": "Icosphere", + "polygonCount": 80, + "triangleCount": 80, + "uvLayers": [ + "UVMap" + ], + "vertexCount": 42 + }, + { + "materials": [ + "M12 Skin Material" + ], + "name": "M12 Skin Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [], + "vertexCount": 4 + } + ], + "objects": [ + { + "children": [], + "data": "Icosphere", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "Icosphere", + "parent": null, + "type": "MESH" + }, + { + "children": [ + "M12 Skin Fixture" + ], + "data": "M12 Skin Armature", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 Skin Armature", + "parent": null, + "type": "ARMATURE" + }, + { + "children": [], + "data": "M12 Skin Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 Skin Fixture", + "parent": "M12 Skin Armature", + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [ + "armature:M12 Skin Armature" + ], + "images": [], + "materials": [ + "material:M12 Skin Material" + ], + "meshes": [ + "mesh:Icosphere", + "mesh:M12 Skin Fixture Mesh" + ], + "objects": [ + "object:Icosphere", + "object:M12 Skin Armature", + "object:M12 Skin Fixture" + ] + } + }, + "id": "skin" + }, + { + "blend": { + "byteLength": 91107, + "file": "animation.blend", + "sha256": "fa6baf3a67ff11fe3d2922210089a7c508e4ee28bfaabe4cf628ba6addee1ff5" + }, + "glb": { + "file": "animation.glb", + "sha256": "44f6cc47961a146dc97ea337ae31a8b64bb4ab213b716f81ec22129db704f1fb" + }, + "graph": { + "actions": [ + { + "fcurves": [ + [ + "location", + 0 + ], + [ + "location", + 1 + ], + [ + "location", + 2 + ], + [ + "rotation_quaternion", + 0 + ], + [ + "rotation_quaternion", + 1 + ], + [ + "rotation_quaternion", + 2 + ], + [ + "rotation_quaternion", + 3 + ] + ], + "frameRange": [ + 1.0, + 25.0 + ], + "name": "M12 Animation Action" + } + ], + "armatures": [], + "images": [], + "materials": [ + { + "name": "M12 Animation Material", + "nodeNames": [ + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 Animation Material" + ], + "name": "M12 Animation Fixture Mesh", + "polygonCount": 1, + "triangleCount": 1, + "uvLayers": [], + "vertexCount": 3 + } + ], + "objects": [ + { + "children": [], + "data": "M12 Animation Fixture Mesh", + "location": [ + -1.0, + -0.0, + 0.0 + ], + "name": "M12 Animation Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [ + "action:M12 Animation Action:object:M12 Animation Fixture" + ], + "armatures": [], + "images": [], + "materials": [ + "material:M12 Animation Material" + ], + "meshes": [ + "mesh:M12 Animation Fixture Mesh" + ], + "objects": [ + "object:M12 Animation Fixture" + ] + } + }, + "id": "animation" + } + ], + "nextTask": "M12-06D", + "operation": "DESKTOP_GLB_IMPORT_MAIN_PERSISTENCE_BASELINE", + "schemaVersion": 1, + "task": "M12-06C" +} diff --git a/tests/golden/M12-06C/manifest.json b/tests/golden/M12-06C/manifest.json new file mode 100644 index 00000000..2b41eaf3 --- /dev/null +++ b/tests/golden/M12-06C/manifest.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "task": "M12-06C", + "parentTask": "M12-06B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP_WASM_CHROMIUM", + "operation": "GLB_IMPORT_MAIN_SAVE_REOPEN", + "assertions": { + "fixtureCount": 5, + "desktopImportSaveReopen": true, + "webMainAuthority": true, + "oneVisibilityTransaction": true, + "stableIdsPreserved": true, + "resourceCountersZeroAfterOpen": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06B/manifest.json", + "sha256": "5275310394b34726a05b30ab67658e1381662dddf9163a97cb02f47f646a8fa4" + }, + "generator": { + "path": "tools/web/generate-glb-main-persistence-fixtures.py", + "sha256": "8989d6ce4196f140a7bfb44b863dc530a6aa462ca4a57fa7b77c468e06ab61ad" + }, + "checker": { + "path": "tools/web/check-glb-main-persistence.mjs", + "sha256": "5bbbbeaf2d20ff9bbdeb74c8dd10fa6ffc421d1d59f86540e95f29c2cfd10903" + }, + "desktopReport": { + "path": "tests/golden/M12-06C/desktop-main-report.json", + "sha256": "76b000454a9073ef762d25fa546d5c65a004659a93eb6ec82fad1e679b2e81be" + }, + "chromium": { + "path": "web/tests/e2e/glb-main-persistence.spec.ts", + "sha256": "7c97877f1cbbfd0166e106c80faee53f474f78816cca68ea924df5aa3c47776d" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + }, + "meshBlend": { + "path": "tests/files/web/m12_glb_main_v1/mesh.blend", + "sha256": "66e29adc016f07e220019b6f24eb7e5016c684dca4b3102b20c8e836968d422d" + }, + "pbrBlend": { + "path": "tests/files/web/m12_glb_main_v1/pbr.blend", + "sha256": "ba08aeb2876d82ddf731abe81d3a42041a1be36617d0b6324c870d5bcd4cc771" + }, + "uvBlend": { + "path": "tests/files/web/m12_glb_main_v1/uv.blend", + "sha256": "1270cb452d34d2fd7a19181a2b20f70b7a028bdd2db7cf1bba4e1003f7de0f48" + }, + "skinBlend": { + "path": "tests/files/web/m12_glb_main_v1/skin.blend", + "sha256": "23f175e44ec588c75db99d3f9134863fc3d7d1f192bbd815d5d1c4e3218bce0c" + }, + "animationBlend": { + "path": "tests/files/web/m12_glb_main_v1/animation.blend", + "sha256": "fa6baf3a67ff11fe3d2922210089a7c508e4ee28bfaabe4cf628ba6addee1ff5" + } + }, + "nextTask": "M12-06D" +} diff --git a/tests/golden/M12-06D/manifest.json b/tests/golden/M12-06D/manifest.json new file mode 100644 index 00000000..efdef943 --- /dev/null +++ b/tests/golden/M12-06D/manifest.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "task": "M12-06D", + "parentTask": "M12-06C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_WASM_CHROMIUM", + "operation": "GLB_EXPORT_LOSS_REPORT", + "assertions": { + "fixtureCount": 5, + "machineReport": true, + "unsupportedShaderFailsClosed": true, + "exportableFixtureOutputsHashed": true, + "warningOrderDeterministic": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06C/manifest.json", + "sha256": "e3a57636a47591e3b02dff5a07e8a0aec5e0dc43bf6b4829bffc811035dbbb31" + }, + "exportProtocol": { + "path": "web/protocol/glb-export.ts", + "sha256": "a5d342827860a9e55b49a3bb3a196a01b1e53546325d6e594778afb9360c3125" + }, + "lossReportProtocol": { + "path": "web/protocol/glb-loss-report.ts", + "sha256": "dce9c790b2f52d46efe0908ecb044d63d21b3c6c3f7d77ddb5e697b29a7a2d6e" + }, + "worker": { + "path": "web/app/src/workers/glb-loss-report-test.worker.ts", + "sha256": "6f6294d26fe7b717416a5dd25fe834fb4b9a2ecbe8b011395c9559a26701ec77" + }, + "chromium": { + "path": "web/tests/e2e/glb-export-loss-report.spec.ts", + "sha256": "ce334b6bb5d82c133d317e916c03711029fba1c19c634dec06c06da9eddbd776" + }, + "checker": { + "path": "tools/web/check-glb-loss-report.mjs", + "sha256": "a23346860fa26405b2cd24591b0ab36fea29cb561ec8c2991ecfeabcffc17ee1" + }, + "unit": { + "path": "web/tests/unit/glb-loss-report.test.mjs", + "sha256": "dfcc2d968e4e0c0cef4496bc304cb481f10d1f0dc4c76a40ea1d6e2f11b6b708" + }, + "webLossReport": { + "path": "tests/golden/M12-06D/web-loss-report.json", + "sha256": "c6db52234d867985ef5fbcdc7c62298ec9aaa013028b5a54e2b9a16aa4133397" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-06E" +} diff --git a/tests/golden/M12-06D/web-loss-report.json b/tests/golden/M12-06D/web-loss-report.json new file mode 100644 index 00000000..ce02d1e5 --- /dev/null +++ b/tests/golden/M12-06D/web-loss-report.json @@ -0,0 +1,163 @@ +{ + "schemaVersion": 1, + "task": "M12-06D", + "operation": "WEB_GLB_EXPORT_LOSS_REPORT", + "fixtureCount": 5, + "fixtures": [ + { + "fixtureId": "mesh", + "sourceBlendSha256": "66e29adc016f07e220019b6f24eb7e5016c684dca4b3102b20c8e836968d422d", + "lossReport": { + "schemaVersion": 1, + "operation": "GLB_EXPORT_LOSS_REPORT", + "sceneId": "scene:Scene", + "sourceRevision": 1, + "canExport": false, + "errorCount": 1, + "warningCount": 1, + "losses": [ + { + "code": "LINKED_MATERIAL_INPUT_UNEVALUATED", + "severity": "warning", + "message": "Material M12 Mesh Material contains unevaluated linked inputs", + "id": "material:M12 Mesh Material" + }, + { + "code": "SHADER_GRAPH_UNMAPPABLE", + "severity": "error", + "message": "Material M12 Mesh Material: Shader node UNSUPPORTED cannot be represented by glTF PBR", + "id": "material:M12 Mesh Material" + } + ], + "surface": { + "nodeCount": 1, + "meshCount": 1, + "materialCount": 1, + "imageCount": 0, + "animationCount": 0, + "nonMeshCount": 0 + } + }, + "output": null + }, + { + "fixtureId": "pbr", + "sourceBlendSha256": "ba08aeb2876d82ddf731abe81d3a42041a1be36617d0b6324c870d5bcd4cc771", + "lossReport": { + "schemaVersion": 1, + "operation": "GLB_EXPORT_LOSS_REPORT", + "sceneId": "scene:Scene", + "sourceRevision": 1, + "canExport": true, + "errorCount": 0, + "warningCount": 0, + "losses": [], + "surface": { + "nodeCount": 1, + "meshCount": 1, + "materialCount": 1, + "imageCount": 0, + "animationCount": 0, + "nonMeshCount": 0 + } + }, + "output": { + "byteLength": 1840, + "sha256": "1ab7936fae6e0c28e1b90e8a6ae24b3893c075c9fc58ac8896f780fdefb8277e" + } + }, + { + "fixtureId": "uv", + "sourceBlendSha256": "1270cb452d34d2fd7a19181a2b20f70b7a028bdd2db7cf1bba4e1003f7de0f48", + "lossReport": { + "schemaVersion": 1, + "operation": "GLB_EXPORT_LOSS_REPORT", + "sceneId": "scene:Scene", + "sourceRevision": 1, + "canExport": true, + "errorCount": 0, + "warningCount": 1, + "losses": [ + { + "code": "LINKED_MATERIAL_INPUT_UNEVALUATED", + "severity": "warning", + "message": "Material M12 UV Material contains unevaluated linked inputs", + "id": "material:M12 UV Material" + } + ], + "surface": { + "nodeCount": 1, + "meshCount": 1, + "materialCount": 1, + "imageCount": 1, + "animationCount": 0, + "nonMeshCount": 0 + } + }, + "output": { + "byteLength": 2756, + "sha256": "8bd045388527ddad7cf886c0c7a2a45b6e7d6db603568a10a959bc6d423ae145" + } + }, + { + "fixtureId": "skin", + "sourceBlendSha256": "23f175e44ec588c75db99d3f9134863fc3d7d1f192bbd815d5d1c4e3218bce0c", + "lossReport": { + "schemaVersion": 1, + "operation": "GLB_EXPORT_LOSS_REPORT", + "sceneId": "scene:Scene", + "sourceRevision": 1, + "canExport": true, + "errorCount": 0, + "warningCount": 1, + "losses": [ + { + "code": "MODIFIER_STACK_NOT_BAKED", + "severity": "warning", + "message": "Mesh M12 Skin Fixture Mesh has enabled modifiers that are not baked for export", + "id": "mesh:M12 Skin Fixture Mesh" + } + ], + "surface": { + "nodeCount": 3, + "meshCount": 2, + "materialCount": 1, + "imageCount": 0, + "animationCount": 0, + "nonMeshCount": 0 + } + }, + "output": { + "byteLength": 14264, + "sha256": "4a45d00dfa23638682bb61a4f74b283bcd986126da4890d068902e3c85efc332" + } + }, + { + "fixtureId": "animation", + "sourceBlendSha256": "fa6baf3a67ff11fe3d2922210089a7c508e4ee28bfaabe4cf628ba6addee1ff5", + "lossReport": { + "schemaVersion": 1, + "operation": "GLB_EXPORT_LOSS_REPORT", + "sceneId": "scene:Scene", + "sourceRevision": 1, + "canExport": true, + "errorCount": 0, + "warningCount": 0, + "losses": [], + "surface": { + "nodeCount": 1, + "meshCount": 1, + "materialCount": 1, + "imageCount": 0, + "animationCount": 1, + "nonMeshCount": 0 + } + }, + "output": { + "byteLength": 3708, + "sha256": "b83de103df3f5a49487868f3ede3046875c90b0d084bbd998511c3a7c987a4fa" + } + } + ], + "nextTask": "M12-06E" +} diff --git a/tests/golden/M12-06E/desktop-reimport-report.json b/tests/golden/M12-06E/desktop-reimport-report.json new file mode 100644 index 00000000..bf2d1d52 --- /dev/null +++ b/tests/golden/M12-06E/desktop-reimport-report.json @@ -0,0 +1,393 @@ +{ + "blenderVersion": "5.2.0 LTS", + "fixtureCount": 4, + "fixtures": [ + { + "glb": { + "byteLength": 1840, + "file": "pbr.glb", + "sha256": "1ab7936fae6e0c28e1b90e8a6ae24b3893c075c9fc58ac8896f780fdefb8277e" + }, + "graph": { + "actions": [], + "armatures": [], + "images": [], + "materials": [ + { + "name": "M12 PBR Material", + "nodeNames": [ + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 PBR Material" + ], + "name": "M12 PBR Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [], + "vertexCount": 4 + } + ], + "objects": [ + { + "children": [], + "data": "M12 PBR Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 PBR Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [], + "images": [], + "materials": [ + "material:M12 PBR Material" + ], + "meshes": [ + "mesh:M12 PBR Fixture Mesh" + ], + "objects": [ + "object:M12 PBR Fixture" + ] + } + }, + "id": "pbr" + }, + { + "glb": { + "byteLength": 2756, + "file": "uv.glb", + "sha256": "8bd045388527ddad7cf886c0c7a2a45b6e7d6db603568a10a959bc6d423ae145" + }, + "graph": { + "actions": [], + "armatures": [], + "images": [ + { + "mimeType": "PNG", + "name": "M12 UV Texture", + "packed": true, + "size": [ + 2, + 2 + ] + } + ], + "materials": [ + { + "name": "M12 UV Material", + "nodeNames": [ + "Image Texture", + "Material Output", + "Mix", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 UV Material" + ], + "name": "M12 UV Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [ + "UVMap" + ], + "vertexCount": 6 + } + ], + "objects": [ + { + "children": [], + "data": "M12 UV Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 UV Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [], + "images": [ + "image:M12 UV Texture" + ], + "materials": [ + "material:M12 UV Material" + ], + "meshes": [ + "mesh:M12 UV Fixture Mesh" + ], + "objects": [ + "object:M12 UV Fixture" + ] + } + }, + "id": "uv" + }, + { + "glb": { + "byteLength": 14264, + "file": "skin.glb", + "sha256": "4a45d00dfa23638682bb61a4f74b283bcd986126da4890d068902e3c85efc332" + }, + "graph": { + "actions": [], + "armatures": [ + { + "bones": [ + { + "name": "Root", + "parent": null + }, + { + "name": "Tip", + "parent": "Root" + } + ], + "name": "M12 Skin Armature" + } + ], + "images": [], + "materials": [ + { + "name": "M12 Skin Material", + "nodeNames": [ + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [], + "name": "Icosphere", + "polygonCount": 80, + "triangleCount": 80, + "uvLayers": [ + "UVMap" + ], + "vertexCount": 240 + }, + { + "materials": [], + "name": "Icosphere.001", + "polygonCount": 80, + "triangleCount": 80, + "uvLayers": [ + "UVMap" + ], + "vertexCount": 42 + }, + { + "materials": [ + "M12 Skin Material" + ], + "name": "M12 Skin Fixture Mesh", + "polygonCount": 2, + "triangleCount": 2, + "uvLayers": [], + "vertexCount": 4 + } + ], + "objects": [ + { + "children": [], + "data": "Icosphere", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "Icosphere", + "parent": null, + "type": "MESH" + }, + { + "children": [], + "data": "Icosphere.001", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "Icosphere.001", + "parent": null, + "type": "MESH" + }, + { + "children": [ + "M12 Skin Fixture" + ], + "data": "M12 Skin Armature", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 Skin Armature", + "parent": null, + "type": "ARMATURE" + }, + { + "children": [], + "data": "M12 Skin Fixture Mesh", + "location": [ + 0.0, + 0.0, + 0.0 + ], + "name": "M12 Skin Fixture", + "parent": "M12 Skin Armature", + "type": "MESH" + } + ], + "stableIds": { + "actions": [], + "armatures": [ + "armature:M12 Skin Armature" + ], + "images": [], + "materials": [ + "material:M12 Skin Material" + ], + "meshes": [ + "mesh:Icosphere", + "mesh:Icosphere.001", + "mesh:M12 Skin Fixture Mesh" + ], + "objects": [ + "object:Icosphere", + "object:Icosphere.001", + "object:M12 Skin Armature", + "object:M12 Skin Fixture" + ] + } + }, + "id": "skin" + }, + { + "glb": { + "byteLength": 3708, + "file": "animation.glb", + "sha256": "b83de103df3f5a49487868f3ede3046875c90b0d084bbd998511c3a7c987a4fa" + }, + "graph": { + "actions": [ + { + "fcurves": [ + [ + "location", + 0 + ], + [ + "location", + 1 + ], + [ + "location", + 2 + ], + [ + "rotation_quaternion", + 0 + ], + [ + "rotation_quaternion", + 1 + ], + [ + "rotation_quaternion", + 2 + ], + [ + "rotation_quaternion", + 3 + ] + ], + "frameRange": [ + 1.0, + 25.0 + ], + "name": "M12 Animation Action" + } + ], + "armatures": [], + "images": [], + "materials": [ + { + "name": "M12 Animation Material", + "nodeNames": [ + "Material Output", + "Principled BSDF" + ] + } + ], + "meshes": [ + { + "materials": [ + "M12 Animation Material" + ], + "name": "M12 Animation Fixture Mesh", + "polygonCount": 1, + "triangleCount": 1, + "uvLayers": [], + "vertexCount": 3 + } + ], + "objects": [ + { + "children": [], + "data": "M12 Animation Fixture Mesh", + "location": [ + -1.0, + -0.0, + 0.0 + ], + "name": "M12 Animation Fixture", + "parent": null, + "type": "MESH" + } + ], + "stableIds": { + "actions": [ + "action:M12 Animation Action:object:M12 Animation Fixture" + ], + "armatures": [], + "images": [], + "materials": [ + "material:M12 Animation Material" + ], + "meshes": [ + "mesh:M12 Animation Fixture Mesh" + ], + "objects": [ + "object:M12 Animation Fixture" + ] + } + }, + "id": "animation" + } + ], + "nextTask": "M12-06F", + "operation": "DESKTOP_REIMPORT_WEB_GLB_CANONICAL_REPORT", + "schemaVersion": 1, + "task": "M12-06E" +} diff --git a/tests/golden/M12-06E/manifest.json b/tests/golden/M12-06E/manifest.json new file mode 100644 index 00000000..ac5c473d --- /dev/null +++ b/tests/golden/M12-06E/manifest.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "task": "M12-06E", + "parentTask": "M12-06D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "DESKTOP_REIMPORT_WEB_GLB_CANONICAL_REPORT", + "assertions": { + "fixtureCount": 4, + "desktopReimportSaveReopen": true, + "exactFixtures": ["pbr", "animation"], + "knownMismatchedFixtures": ["uv", "skin"], + "mismatchesReported": true, + "deterministicReport": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06D/manifest.json", + "sha256": "c01c5861d493e1f177e8cde3038bc5283a7671fa39bf84484662d307623325a2" + }, + "generator": { + "path": "tools/web/generate-glb-web-reimport-report.py", + "sha256": "3ae1ba45e15cae5d0308fc3fcb3766764cd374c096e27eaaddba9228a3d75004" + }, + "checker": { + "path": "tools/web/check-glb-web-reimport.mjs", + "sha256": "c21b45a975ec70586da1b9e50b8cf4ccf74644300b4f381c3fee0ad735cfcdb9" + }, + "desktopReport": { + "path": "tests/golden/M12-06E/desktop-reimport-report.json", + "sha256": "e4d03d25cfac3c4beff4d0af0769b1c39b058670c679c12f4a11ae30d2d91f3c" + }, + "webExportTest": { + "path": "web/tests/e2e/glb-export-loss-report.spec.ts", + "sha256": "ce334b6bb5d82c133d317e916c03711029fba1c19c634dec06c06da9eddbd776" + }, + "pbrGLB": { + "path": "tests/files/web/m12_glb_web_v1/pbr.glb", + "sha256": "1ab7936fae6e0c28e1b90e8a6ae24b3893c075c9fc58ac8896f780fdefb8277e" + }, + "uvGLB": { + "path": "tests/files/web/m12_glb_web_v1/uv.glb", + "sha256": "8bd045388527ddad7cf886c0c7a2a45b6e7d6db603568a10a959bc6d423ae145" + }, + "skinGLB": { + "path": "tests/files/web/m12_glb_web_v1/skin.glb", + "sha256": "4a45d00dfa23638682bb61a4f74b283bcd986126da4890d068902e3c85efc332" + }, + "animationGLB": { + "path": "tests/files/web/m12_glb_web_v1/animation.glb", + "sha256": "b83de103df3f5a49487868f3ede3046875c90b0d084bbd998511c3a7c987a4fa" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-06F" +} diff --git a/tests/golden/M12-06F/manifest.json b/tests/golden/M12-06F/manifest.json new file mode 100644 index 00000000..64c9b251 --- /dev/null +++ b/tests/golden/M12-06F/manifest.json @@ -0,0 +1,51 @@ +{ + "schemaVersion": 1, + "task": "M12-06F", + "parentTask": "M12-06E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_TYPESCRIPT_CHROMIUM_WORKER", + "operation": "GLB_IMPORT_NEGATIVE_CASES", + "assertions": { + "sparseAccessor": "GLB_SPARSE_ACCESSOR_UNSUPPORTED", + "extensions": "GLB_EXTENSION_UNSUPPORTED", + "externalUri": "GLB_EXTERNAL_URI_BLOCKED", + "overBudget": "GLB_IMPORT_BUDGET_EXCEEDED", + "browserWorker": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06E/manifest.json", + "sha256": "3d3b13fd54a314fd6f6907fc2d314e66ef74e8ca770a8accd823370d272737fc" + }, + "protocol": { + "path": "web/protocol/glb-import.ts", + "sha256": "0b6329c08e6f3cd9af6f27ef7c463b037a7cab94192afb4538d3d6c4cfcbc147" + }, + "unit": { + "path": "web/tests/unit/glb-negative-cases.test.mjs", + "sha256": "9af1c155143a0c685a62f569f705afd9fcc3bb49e0d724ebf3ad2c6356d63d6d" + }, + "worker": { + "path": "web/app/src/workers/glb-negative-cases-test.worker.ts", + "sha256": "d84a6b884ce1bfedd598b2602d803493bf10f6ad62fdfac49d64fca3f30f3931" + }, + "chromium": { + "path": "web/tests/e2e/glb-negative-cases.spec.ts", + "sha256": "c6694689ce04ff2faea2c20be648f438a9f7eb25cdfd2f714b94c1f556ea1510" + }, + "checker": { + "path": "tools/web/check-glb-negative-cases.mjs", + "sha256": "08377c306fd7d1082f7fd734e37809960c9798d29d626681fe9ad3f94cb7a29e" + }, + "report": { + "path": "tests/golden/M12-06F/negative-report.json", + "sha256": "7367a624b562a9613b4b908c894ab04c731ae0a348a3b58689075f4a9decd90c" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-06G" +} diff --git a/tests/golden/M12-06F/negative-report.json b/tests/golden/M12-06F/negative-report.json new file mode 100644 index 00000000..240219b4 --- /dev/null +++ b/tests/golden/M12-06F/negative-report.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M12-06F", + "operation": "GLB_IMPORT_NEGATIVE_CASES", + "budget": { + "maxBytes": 524288, + "maxJsonBytes": 262144, + "maxBufferViews": 4096, + "maxAccessors": 8192 + }, + "cases": [ + { "id": "sparse", "code": "GLB_SPARSE_ACCESSOR_UNSUPPORTED" }, + { "id": "extension", "code": "GLB_EXTENSION_UNSUPPORTED" }, + { "id": "external-uri", "code": "GLB_EXTERNAL_URI_BLOCKED" }, + { "id": "over-budget", "code": "GLB_IMPORT_BUDGET_EXCEEDED" } + ], + "nextTask": "M12-06G" +} diff --git a/tests/golden/M12-06G/manifest.json b/tests/golden/M12-06G/manifest.json new file mode 100644 index 00000000..6a12df86 --- /dev/null +++ b/tests/golden/M12-06G/manifest.json @@ -0,0 +1,69 @@ +{ + "schemaVersion": 1, + "task": "M12-06G", + "parentTask": "M12-06F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_TYPESCRIPT_CHROMIUM_WORKER_OPFS", + "operation": "GLB_IMPORT_EXPORT_RECOVERY", + "assertions": { + "importCancellation": "GLB_OPERATION_CANCELLED", + "exportCancellation": "GLB_OPERATION_CANCELLED", + "workerRestart": "GLB_WORKER_RESTARTED", + "opfsQuota": "GLB_OPFS_QUOTA", + "temporaryResourcesAfterCancel": 0, + "committedAssetPreserved": true, + "smallAssetRecovery": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06F/manifest.json", + "sha256": "27b42da0683dab11a884553440088c30cf58d51364268fdaac86668786aaedd4" + }, + "protocol": { + "path": "web/protocol/glb-recovery.ts", + "sha256": "da4e2a546d7598e80798cfebd105e52b7522c896acb545710c78bb8a5c3705aa" + }, + "storageProtocol": { + "path": "web/protocol/storage.ts", + "sha256": "2c8ce9e32fcae973e9262a024fd849221680104bcd427ec308f99ef25112d951" + }, + "storageClient": { + "path": "web/app/src/storage/StorageClient.ts", + "sha256": "00cd35e4632479e4cd153314113e201c822fd209ae6c8e9fbeff5f1399728565" + }, + "storageWorker": { + "path": "web/app/src/workers/storage.worker.ts", + "sha256": "80bf4d2ac59cbf7998c2a72c8961ae38abe43685b60df564d0746e1a45e9e1bc" + }, + "operationWorker": { + "path": "web/app/src/workers/glb-recovery-test.worker.ts", + "sha256": "947c4a768422725ff2f689b2eefa8068bee51bb7ff342af17e8d4a4d0a4a7ca2" + }, + "browserAdapter": { + "path": "web/app/src/testing/glb-recovery.ts", + "sha256": "2c753a04c153471c2bf7691073b836d968a1a1300e3da038f493983b46738cdf" + }, + "unit": { + "path": "web/tests/unit/glb-recovery.test.mjs", + "sha256": "6dd48f62a85c5aaf3a04b6e572a47a7986b23cbec84a099ea510af81e8dd3a3a" + }, + "chromium": { + "path": "web/tests/e2e/glb-recovery.spec.ts", + "sha256": "7d95e992f44fb913f70c104f0d6b23bd76f47d48db4a21899e35ae1d188e2093" + }, + "checker": { + "path": "tools/web/check-glb-recovery.mjs", + "sha256": "b185248fdcd6b3cee84252bd68fcb6921bb6385bfa4a00486e93c58e94755cce" + }, + "report": { + "path": "tests/golden/M12-06G/recovery-report.json", + "sha256": "1a008a76590573468446ca6e5cbdfe0ea5a8b27493291590d937b90db90d6e42" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07A" +} diff --git a/tests/golden/M12-06G/recovery-report.json b/tests/golden/M12-06G/recovery-report.json new file mode 100644 index 00000000..2f8a466d --- /dev/null +++ b/tests/golden/M12-06G/recovery-report.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "task": "M12-06G", + "operation": "GLB_IMPORT_EXPORT_RECOVERY", + "cancellation": [ + { + "operation": "IMPORT", + "status": "CANCELLED", + "code": "GLB_OPERATION_CANCELLED", + "committed": false, + "temporaryBytes": 0, + "liveRequests": 0 + }, + { + "operation": "EXPORT", + "status": "CANCELLED", + "code": "GLB_OPERATION_CANCELLED", + "committed": false, + "temporaryBytes": 0, + "liveRequests": 0 + } + ], + "workerRestart": { + "operation": "IMPORT", + "generationBefore": 1, + "generationAfter": 2, + "status": "RECOVERED", + "code": "GLB_WORKER_RESTARTED", + "resultHash": "EXACT" + }, + "opfsQuota": { + "operation": "EXPORT_ASSET_COMMIT", + "status": "BLOCKED", + "code": "GLB_OPFS_QUOTA", + "backend": "OPFS", + "committedAssetPreserved": true, + "workerRestart": true, + "smallAssetRecovery": true + }, + "nextTask": "M12-07A" +} diff --git a/tests/golden/M12-07A/desktop-fixture.json b/tests/golden/M12-07A/desktop-fixture.json new file mode 100644 index 00000000..54d04e55 --- /dev/null +++ b/tests/golden/M12-07A/desktop-fixture.json @@ -0,0 +1,223 @@ +{ + "files": [ + { + "byteLength": 465, + "name": "single-mesh.obj", + "sha256": "a56694d1ee28735c68381ff18c3a52581612feebac0101a53e502956c27d144f" + }, + { + "byteLength": 426, + "name": "single-mesh.mtl", + "sha256": "392f1b10ff5a0b142d520a9443b4c96191985f15d4244c79b36fdeda0f153715" + } + ], + "nextTask": "M12-07B", + "operation": "DESKTOP_OBJ_SINGLE_MESH_FIXTURE", + "operator": "wm.obj_export", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "semantic": { + "faces": [ + { + "groups": [ + "M12_OBJ_Single_Mesh_M12_OBJ_Red" + ], + "material": "M12_OBJ_Red", + "vertices": [ + { + "normal": 1, + "position": 1, + "texcoord": 1 + }, + { + "normal": 1, + "position": 2, + "texcoord": 2 + }, + { + "normal": 1, + "position": 3, + "texcoord": 3 + } + ] + }, + { + "groups": [ + "M12_OBJ_Single_Mesh_M12_OBJ_Blue" + ], + "material": "M12_OBJ_Blue", + "vertices": [ + { + "normal": 1, + "position": 1, + "texcoord": 1 + }, + { + "normal": 1, + "position": 3, + "texcoord": 3 + }, + { + "normal": 1, + "position": 4, + "texcoord": 4 + } + ] + } + ], + "materialLibraries": [ + "single-mesh.mtl" + ], + "materials": [ + { + "name": "M12_OBJ_Blue", + "properties": { + "Ka": [ + 1.0, + 1.0, + 1.0 + ], + "Kd": [ + 0.8, + 0.8, + 0.8 + ], + "Ke": [ + 0.0, + 0.0, + 0.0 + ], + "Ks": [ + 0.5, + 0.5, + 0.5 + ], + "Ni": [ + 1.5 + ], + "Ns": [ + 250.0 + ], + "d": [ + 1.0 + ], + "illum": [ + 2.0 + ] + } + }, + { + "name": "M12_OBJ_Red", + "properties": { + "Ka": [ + 1.0, + 1.0, + 1.0 + ], + "Kd": [ + 0.8, + 0.8, + 0.8 + ], + "Ke": [ + 0.0, + 0.0, + 0.0 + ], + "Ks": [ + 0.5, + 0.5, + 0.5 + ], + "Ni": [ + 1.5 + ], + "Ns": [ + 250.0 + ], + "d": [ + 1.0 + ], + "illum": [ + 2.0 + ] + } + } + ], + "normals": [ + [ + 0.0, + 1.0, + 0.0 + ] + ], + "objects": [ + "M12_OBJ_Single_Mesh" + ], + "positions": [ + [ + -1.0, + 0.0, + 1.0 + ], + [ + 1.0, + 0.0, + 1.0 + ], + [ + 1.0, + 0.0, + -1.0 + ], + [ + -1.0, + 0.0, + -1.0 + ] + ], + "texcoords": [ + [ + 0.0, + 0.0 + ], + [ + 1.0, + 0.0 + ], + [ + 1.0, + 1.0 + ], + [ + 0.0, + 1.0 + ] + ] + }, + "settings": { + "exportMaterialGroups": true, + "exportMaterials": true, + "exportNormals": true, + "exportUV": true, + "forwardAxis": "NEGATIVE_Z", + "globalScale": 1.0, + "upAxis": "Y" + }, + "sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj", + "task": "M12-07A" +} diff --git a/tests/golden/M12-07A/manifest.json b/tests/golden/M12-07A/manifest.json new file mode 100644 index 00000000..200a5587 --- /dev/null +++ b/tests/golden/M12-07A/manifest.json @@ -0,0 +1,56 @@ +{ + "schemaVersion": 1, + "task": "M12-07A", + "parentTask": "M12-06G", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "DESKTOP_OBJ_SINGLE_MESH_FIXTURE", + "assertions": { + "sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj", + "operator": "wm.obj_export", + "objectCount": 1, + "positionCount": 4, + "normalCount": 1, + "uvCount": 4, + "faceCount": 2, + "materialCount": 2, + "materialGroupCount": 2, + "deterministicRegeneration": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-06G/manifest.json", + "sha256": "1c732b8d9f1a0bdb502f44e2e843e91efea12e93483a35658f8a9c70a69a2430" + }, + "generator": { + "path": "tools/web/generate-obj-single-mesh-fixture.py", + "sha256": "604c3006f194c7c64a487b8f760693b66830f3cfa602928b46769955e7d4f358" + }, + "checker": { + "path": "tools/web/check-obj-single-mesh-fixture.mjs", + "sha256": "3d0208f61a86d4d29796d8284756f53931c90864b15b7dfe4fcc84d7f65a8040" + }, + "desktopReport": { + "path": "tests/golden/M12-07A/desktop-fixture.json", + "sha256": "6c560a8eecf84973c2b05f9fd50d33bd45515e97c4f7970f1502de406c39f6a5" + }, + "objFixture": { + "path": "tests/files/web/m12_obj_desktop_v1/single-mesh.obj", + "sha256": "a56694d1ee28735c68381ff18c3a52581612feebac0101a53e502956c27d144f" + }, + "mtlFixture": { + "path": "tests/files/web/m12_obj_desktop_v1/single-mesh.mtl", + "sha256": "392f1b10ff5a0b142d520a9443b4c96191985f15d4244c79b36fdeda0f153715" + }, + "runtimeInventory": { + "path": "tests/golden/M12-05A/format-inventory.json", + "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07B" +} diff --git a/tests/golden/M12-07B/desktop-fixtures.json b/tests/golden/M12-07B/desktop-fixtures.json new file mode 100644 index 00000000..482cd679 --- /dev/null +++ b/tests/golden/M12-07B/desktop-fixtures.json @@ -0,0 +1,223 @@ +{ + "files": [ + { + "byteLength": 670, + "name": "multi-object.obj", + "sha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a" + }, + { + "byteLength": 440, + "name": "multi-object.mtl", + "sha256": "80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f" + }, + { + "byteLength": 261, + "name": "m12_obj_texture.png", + "sha256": "44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c" + }, + { + "byteLength": 688, + "name": "negative-index.obj", + "sha256": "9457954284cac1d68e23627e3ff734c0c591b3a24086ce5aa3d0dbbfc22f01bc" + }, + { + "byteLength": 664, + "name": "malformed-face.obj", + "sha256": "6dd508b5ba944c2d15e2889c9ad9a3693845145c3bf6c9bf7df992081c915864" + } + ], + "malformedFace": { + "expectedCode": "OBJ_FACE_ARITY_INVALID", + "file": "malformed-face.obj" + }, + "negativeIndex": { + "expectedStatus": "ACCEPT_WITH_NEGATIVE_INDICES", + "faceCount": 2, + "file": "negative-index.obj" + }, + "nextTask": "M12-07C", + "operation": "DESKTOP_OBJ_MULTI_OBJECT_AND_NEGATIVE_FIXTURES", + "operator": "wm.obj_export", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "semantic": { + "faces": [ + { + "groups": [ + "M12_OBJ_Left_M12_OBJ_Left_Material" + ], + "material": "M12_OBJ_Left_Material", + "object": "M12_OBJ_Left_M12_OBJ_Left_Mesh", + "vertices": [ + { + "normal": 1, + "position": 1, + "texcoord": 1 + }, + { + "normal": 1, + "position": 2, + "texcoord": 2 + }, + { + "normal": 1, + "position": 3, + "texcoord": 3 + } + ] + }, + { + "groups": [ + "M12_OBJ_Right_M12_OBJ_Right_Material" + ], + "material": "M12_OBJ_Right_Material", + "object": "M12_OBJ_Right_M12_OBJ_Right_Mesh", + "vertices": [ + { + "normal": 2, + "position": 4, + "texcoord": 4 + }, + { + "normal": 2, + "position": 5, + "texcoord": 5 + }, + { + "normal": 2, + "position": 6, + "texcoord": 6 + } + ] + } + ], + "groups": [ + "M12_OBJ_Left_M12_OBJ_Left_Mesh", + "M12_OBJ_Left_M12_OBJ_Left_Material", + "M12_OBJ_Right_M12_OBJ_Right_Mesh", + "M12_OBJ_Right_M12_OBJ_Right_Material" + ], + "materialLibraries": [ + "multi-object.mtl" + ], + "materials": [ + { + "mapKd": "m12_obj_texture.png", + "name": "M12_OBJ_Left_Material" + }, + { + "mapKd": "m12_obj_texture.png", + "name": "M12_OBJ_Right_Material" + } + ], + "normals": [ + [ + -0.0, + 1.0, + -0.0 + ], + [ + -0.0, + 1.0, + -0.0 + ] + ], + "objects": [ + "M12_OBJ_Left_M12_OBJ_Left_Mesh", + "M12_OBJ_Right_M12_OBJ_Right_Mesh" + ], + "positions": [ + [ + -1.75, + 0.0, + 0.75 + ], + [ + -0.25, + 0.0, + 0.75 + ], + [ + -1.0, + 0.0, + -0.75 + ], + [ + 0.25, + 0.0, + 0.75 + ], + [ + 1.75, + 0.0, + 0.75 + ], + [ + 1.0, + 0.0, + -0.75 + ] + ], + "texcoords": [ + [ + 0.0, + 0.0 + ], + [ + 1.0, + 0.0 + ], + [ + 0.5, + 1.0 + ], + [ + 0.0, + 0.0 + ], + [ + 1.0, + 0.0 + ], + [ + 0.5, + 1.0 + ] + ] + }, + "settings": { + "exportMaterialGroups": true, + "exportMaterials": true, + "exportNormals": true, + "exportObjectGroups": true, + "exportUV": true, + "forwardAxis": "NEGATIVE_Z", + "globalScale": 1.0, + "pathMode": "RELATIVE", + "upAxis": "Y" + }, + "sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj", + "task": "M12-07B", + "textureOrigin": { + "mtlMapKd": [ + "m12_obj_texture.png", + "m12_obj_texture.png" + ], + "relative": true, + "textureFile": "m12_obj_texture.png" + } +} diff --git a/tests/golden/M12-07B/manifest.json b/tests/golden/M12-07B/manifest.json new file mode 100644 index 00000000..f1a1f564 --- /dev/null +++ b/tests/golden/M12-07B/manifest.json @@ -0,0 +1,68 @@ +{ + "schemaVersion": 1, + "task": "M12-07B", + "parentTask": "M12-07A", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP_AND_OBJ_NEGATIVE_FIXTURES", + "operation": "DESKTOP_OBJ_MULTI_OBJECT_AND_NEGATIVE_FIXTURES", + "assertions": { + "objectCount": 2, + "positionCount": 6, + "uvCount": 6, + "normalCount": 2, + "faceCount": 2, + "materialCount": 2, + "relativeTextureOrigin": true, + "negativeIndices": "ACCEPT_WITH_NEGATIVE_INDICES", + "malformedFace": "OBJ_FACE_ARITY_INVALID", + "deterministicRegeneration": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-07A/manifest.json", + "sha256": "d80b74502202effa7be15640c945dc7e04af703b58e7a15c3fe7babc43c17b46" + }, + "generator": { + "path": "tools/web/generate-obj-multi-negative-fixtures.py", + "sha256": "8d4faed82cba76e46bf639e5031b30568e8b9a15240cbddb1c22ee7ad11d697b" + }, + "checker": { + "path": "tools/web/check-obj-multi-negative-fixtures.mjs", + "sha256": "61188d67efd6133e714f2d55c7c8516b9d04896de068f994523a3c398eebdd2f" + }, + "desktopReport": { + "path": "tests/golden/M12-07B/desktop-fixtures.json", + "sha256": "b60ff785676c0f0168588605ad442a650615191ac00770b7e5a308cc4ade83ce" + }, + "objFixture": { + "path": "tests/files/web/m12_obj_multi_v1/multi-object.obj", + "sha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a" + }, + "mtlFixture": { + "path": "tests/files/web/m12_obj_multi_v1/multi-object.mtl", + "sha256": "80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f" + }, + "textureFixture": { + "path": "tests/files/web/m12_obj_multi_v1/m12_obj_texture.png", + "sha256": "44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c" + }, + "negativeIndexFixture": { + "path": "tests/files/web/m12_obj_multi_v1/negative-index.obj", + "sha256": "9457954284cac1d68e23627e3ff734c0c591b3a24086ce5aa3d0dbbfc22f01bc" + }, + "malformedFixture": { + "path": "tests/files/web/m12_obj_multi_v1/malformed-face.obj", + "sha256": "6dd508b5ba944c2d15e2889c9ad9a3693845145c3bf6c9bf7df992081c915864" + }, + "runtimeInventory": { + "path": "tests/golden/M12-05A/format-inventory.json", + "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07C" +} diff --git a/tests/golden/M12-07C/manifest.json b/tests/golden/M12-07C/manifest.json new file mode 100644 index 00000000..82cfb816 --- /dev/null +++ b/tests/golden/M12-07C/manifest.json @@ -0,0 +1,70 @@ +{ + "schemaVersion": 1, + "task": "M12-07C", + "parentTask": "M12-07B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_TYPESCRIPT_CHROMIUM_WORKER_AND_BLENDER_5_2", + "operation": "WEB_OBJ_TO_DESKTOP_ROUNDTRIP", + "assertions": { + "browserObjectCount": 2, + "desktopObjectCount": 2, + "triangleCountExact": true, + "uvLayerPresent": true, + "materialPresent": true, + "boundTextureLossWarnings": 0, + "missingTextureLossWarnings": 2, + "desktopExact": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-07B/manifest.json", + "sha256": "785f593271058098704498cb0a7c948305087f1a83bf8f29b6e6fb9fe9341926" + }, + "protocol": { + "path": "web/protocol/obj-import.ts", + "sha256": "e7240af65e0d90f3ee690a4e3f391416fe4744ac6c46edc8ac0d72386857cab9" + }, + "worker": { + "path": "web/app/src/workers/obj-roundtrip-test.worker.ts", + "sha256": "bf1fcc60ec318cf6c2747d44f4af741b46f284cf5f5307e142f0ab6d50b14302" + }, + "desktopImporter": { + "path": "tools/web/check-obj-web-roundtrip.py", + "sha256": "9f1eb4ab679255a0f1f596696e8befc33a4e30c0cbe6ea423e2aaa0314cec22d" + }, + "unit": { + "path": "web/tests/unit/obj-import.test.mjs", + "sha256": "485a669be5de8e370e9b5a3239357b028ba61ca5c4076819cd46aed52a5c210a" + }, + "chromium": { + "path": "web/tests/e2e/obj-web-roundtrip.spec.ts", + "sha256": "93785b4017ba14b007926b0f82442f8ae5968f242a2a762e5f5dc77d36110f5b" + }, + "checker": { + "path": "tools/web/check-obj-web-roundtrip.mjs", + "sha256": "9d53014f4b89f786c516ebe8d300030c2728e4a36a86a5ef136b2769a12ac96b" + }, + "report": { + "path": "tests/golden/M12-07C/web-roundtrip-report.json", + "sha256": "45eaffc9c2e50c84b557d13ebbe9f4f53b63e19e00bc69b272491ef6366dff81" + }, + "objFixture": { + "path": "tests/files/web/m12_obj_multi_v1/multi-object.obj", + "sha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a" + }, + "mtlFixture": { + "path": "tests/files/web/m12_obj_multi_v1/multi-object.mtl", + "sha256": "80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f" + }, + "textureFixture": { + "path": "tests/files/web/m12_obj_multi_v1/m12_obj_texture.png", + "sha256": "44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07D" +} diff --git a/tests/golden/M12-07C/web-roundtrip-report.json b/tests/golden/M12-07C/web-roundtrip-report.json new file mode 100644 index 00000000..6c0442c7 --- /dev/null +++ b/tests/golden/M12-07C/web-roundtrip-report.json @@ -0,0 +1,95 @@ +{ + "schemaVersion": 1, + "task": "M12-07C", + "operation": "WEB_OBJ_TO_DESKTOP_ROUNDTRIP", + "source": { + "objSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "mtlSha256": "80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f", + "textureSha256": "44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c" + }, + "browser": { + "imported": { + "schemaVersion": 1, + "objectCount": 2, + "positionCount": 6, + "texcoordCount": 6, + "normalCount": 2, + "faceCount": 2, + "materialCount": 2 + }, + "outputObjSha256": "64d419bea9738bd584e61214b34aa67e5ed2204a0e2e1db6ed334f8561d63276", + "outputMtlSha256": "4e00c589ed29d99994119cfa12ae9f4ef7cc13c33fdd6ee8ce2eb97e6cf61c29", + "lossReport": { + "schemaVersion": 1, + "operation": "OBJ_EXPORT_LOSS_REPORT", + "canRoundTrip": true, + "warningCount": 0, + "warnings": [] + }, + "missingTextureLoss": { + "schemaVersion": 1, + "operation": "OBJ_EXPORT_LOSS_REPORT", + "canRoundTrip": true, + "warningCount": 2, + "warnings": [ + { + "code": "OBJ_TEXTURE_ORIGIN_UNRESOLVED", + "severity": "warning", + "message": "OBJ texture m12_obj_texture.png is not bound to a supplied asset", + "path": "m12_obj_texture.png" + }, + { + "code": "OBJ_TEXTURE_ORIGIN_UNRESOLVED", + "severity": "warning", + "message": "OBJ texture m12_obj_texture.png is not bound to a supplied asset", + "path": "m12_obj_texture.png" + } + ] + } + }, + "desktop": { + "meshCount": 2, + "objectCount": 2, + "objects": [ + { + "materials": [ + "M12_OBJ_Left_Material" + ], + "name": "M12_OBJ_Left_M12_OBJ_Left_Material", + "normalCount": 3, + "polygonCount": 1, + "triangleCount": 1, + "uvLayers": [ + "UVMap" + ], + "uvLoopCount": 3, + "vertexCount": 3 + }, + { + "materials": [ + "M12_OBJ_Right_Material" + ], + "name": "M12_OBJ_Right_M12_OBJ_Right_Material", + "normalCount": 3, + "polygonCount": 1, + "triangleCount": 1, + "uvLayers": [ + "UVMap" + ], + "uvLoopCount": 3, + "vertexCount": 3 + } + ], + "operation": "DESKTOP_IMPORT_WEB_OBJ", + "schemaVersion": 1, + "sourceObjBytes": 518, + "sourceObjSha256": "64d419bea9738bd584e61214b34aa67e5ed2204a0e2e1db6ed334f8561d63276" + }, + "comparisons": { + "objectCountExact": true, + "triangleCountExact": true, + "uvLayerPresent": true, + "materialPresent": true + }, + "nextTask": "M12-07D" +} diff --git a/tests/golden/M12-07D/capability-report.json b/tests/golden/M12-07D/capability-report.json new file mode 100644 index 00000000..e047ea57 --- /dev/null +++ b/tests/golden/M12-07D/capability-report.json @@ -0,0 +1,68 @@ +{ + "files": [ + { + "byteLength": 184, + "name": "capability-binary.stl", + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + { + "byteLength": 213, + "name": "capability-ascii.stl", + "sha256": "a463a5add8be070fb67b34f00ef6e7b46025aed31fa429d4a033d75a11cf0113" + } + ], + "nextTask": "M12-07E", + "operation": "DESKTOP_STL_BINARY_ASCII_CAPABILITY", + "operator": "wm.stl_export", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "settings": { + "evaluationMode": "DAG_EVAL_VIEWPORT", + "exportSelectedObjects": true, + "forwardAxis": "NEGATIVE_Z", + "globalScale": 1.0, + "upAxis": "Y", + "useSceneUnit": false + }, + "sourceAnchor": "blender-5.2.0/source/blender/io/stl", + "task": "M12-07D", + "variants": [ + { + "asciiFormat": false, + "file": "capability-binary.stl", + "id": "STL_BINARY", + "semantic": { + "byteLength": 184, + "format": "STL_BINARY", + "header": "", + "triangleCount": 2 + } + }, + { + "asciiFormat": true, + "file": "capability-ascii.stl", + "id": "STL_ASCII", + "semantic": { + "byteLength": 213, + "facetCount": 2, + "format": "STL_ASCII", + "solid": "", + "vertexCount": 6 + } + } + ] +} diff --git a/tests/golden/M12-07D/manifest.json b/tests/golden/M12-07D/manifest.json new file mode 100644 index 00000000..c86c10d0 --- /dev/null +++ b/tests/golden/M12-07D/manifest.json @@ -0,0 +1,54 @@ +{ + "schemaVersion": 1, + "task": "M12-07D", + "parentTask": "M12-07C", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "DESKTOP_STL_BINARY_ASCII_CAPABILITY", + "assertions": { + "operator": "wm.stl_export", + "binaryVariant": "STL_BINARY", + "asciiVariant": "STL_ASCII", + "binaryTriangleCount": 2, + "asciiFacetCount": 2, + "asciiVertexCount": 6, + "binaryByteLength": 184, + "deterministicRegeneration": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-07C/manifest.json", + "sha256": "0c639954900b1fcc3b8285d0f4c0ecfa5807df6cde6d1f2cd3b59c4636d71674" + }, + "generator": { + "path": "tools/web/generate-stl-capability-fixtures.py", + "sha256": "8310104e66f246b32ac7cb4619e7f4da109baf2ece39ea670cbb6d33bd88c8b8" + }, + "checker": { + "path": "tools/web/check-stl-capability-fixtures.mjs", + "sha256": "b2ad901eaa9701436cf7bcc8aa4e40d1b2d6eda7c6c44a0af830347a37cc9ca8" + }, + "desktopReport": { + "path": "tests/golden/M12-07D/capability-report.json", + "sha256": "29c7d2a6e6764440c99eec25bb5760c7e0880839691757fb3e607ed4f5050013" + }, + "binaryFixture": { + "path": "tests/files/web/m12_stl_capability_v1/capability-binary.stl", + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + "asciiFixture": { + "path": "tests/files/web/m12_stl_capability_v1/capability-ascii.stl", + "sha256": "a463a5add8be070fb67b34f00ef6e7b46025aed31fa429d4a033d75a11cf0113" + }, + "runtimeInventory": { + "path": "tests/golden/M12-05A/format-inventory.json", + "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07E" +} diff --git a/tests/golden/M12-07E/desktop-edge-report.json b/tests/golden/M12-07E/desktop-edge-report.json new file mode 100644 index 00000000..c3529c5d --- /dev/null +++ b/tests/golden/M12-07E/desktop-edge-report.json @@ -0,0 +1,144 @@ +{ + "cases": [ + { + "byteLength": 184, + "file": "capability-binary.stl", + "globalScale": 1.0, + "id": "BINARY_UNIT_1", + "result": { + "bounds": { + "max": [ + 1.0, + 1.0, + 7.549790126404332e-08 + ], + "min": [ + -1.0, + -1.0, + -7.549790126404332e-08 + ] + }, + "objectCount": 1, + "polygonCount": 2, + "polygonNormals": [ + [ + 0.0, + 1.0, + -0.0 + ], + [ + 0.0, + 1.0, + -0.0 + ] + ], + "status": "ACCEPTED", + "triangleCount": 2, + "vertexCount": 4 + }, + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + { + "byteLength": 184, + "file": "capability-binary.stl", + "globalScale": 0.001, + "id": "BINARY_UNIT_001", + "result": { + "bounds": { + "max": [ + 0.0010000000474974513, + 0.0010000000474974513, + 7.549790653760269e-11 + ], + "min": [ + -0.0010000000474974513, + -0.0010000000474974513, + -7.549790653760269e-11 + ] + }, + "objectCount": 1, + "polygonCount": 2, + "polygonNormals": [ + [ + 0.0, + 1.0, + -0.0 + ], + [ + 0.0, + 1.0, + -0.0 + ] + ], + "status": "ACCEPTED", + "triangleCount": 2, + "vertexCount": 4 + }, + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + { + "byteLength": 184, + "file": "degenerate-binary.stl", + "globalScale": 1.0, + "id": "DEGENERATE_TRIANGLE", + "result": { + "bounds": { + "max": [ + 1.0, + 1.0, + 7.549790126404332e-08 + ], + "min": [ + -1.0, + -1.0, + -7.549790126404332e-08 + ] + }, + "objectCount": 1, + "polygonCount": 1, + "polygonNormals": [ + [ + 0.0, + 1.0, + -0.0 + ] + ], + "status": "ACCEPTED", + "triangleCount": 1, + "vertexCount": 3 + }, + "sha256": "c120bb0f218819eb89a3607c0b4f8fafd9afb599402e3b21deaa3b2be143e7d0" + }, + { + "byteLength": 188, + "file": "trailing-binary.stl", + "globalScale": 1.0, + "id": "TRAILING_BYTES", + "result": { + "bounds": { + "max": [ + 0.0, + 0.0, + 0.0 + ], + "min": [ + 0.0, + 0.0, + 0.0 + ] + }, + "objectCount": 1, + "polygonCount": 0, + "polygonNormals": [], + "status": "ACCEPTED", + "triangleCount": 0, + "vertexCount": 0 + }, + "sha256": "0a30aab6db1588722067000e2a08c3c6206a8ed5b7531caa0b082723700a3681" + } + ], + "nextTask": "M12-07F", + "operation": "BLENDER_STL_EDGE_PROBE", + "schemaVersion": 1, + "task": "M12-07E" +} diff --git a/tests/golden/M12-07E/edge-fixtures.json b/tests/golden/M12-07E/edge-fixtures.json new file mode 100644 index 00000000..9c65512a --- /dev/null +++ b/tests/golden/M12-07E/edge-fixtures.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "task": "M12-07E", + "operation": "STL_EDGE_FIXTURE_GENERATION", + "parent": { + "path": "tests/files/web/m12_stl_capability_v1/capability-binary.stl", + "byteLength": 184, + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + "fixtures": [ + { + "id": "DEGENERATE_TRIANGLE", + "file": "degenerate-binary.stl", + "byteLength": 184, + "sha256": "c120bb0f218819eb89a3607c0b4f8fafd9afb599402e3b21deaa3b2be143e7d0", + "expectedCode": "STL_DEGENERATE_TRIANGLE" + }, + { + "id": "TRAILING_BYTES", + "file": "trailing-binary.stl", + "byteLength": 188, + "sha256": "0a30aab6db1588722067000e2a08c3c6206a8ed5b7531caa0b082723700a3681", + "expectedCode": "STL_TRAILING_BYTES" + } + ], + "nextTask": "M12-07F" +} diff --git a/tests/golden/M12-07E/manifest.json b/tests/golden/M12-07E/manifest.json new file mode 100644 index 00000000..e0e47f26 --- /dev/null +++ b/tests/golden/M12-07E/manifest.json @@ -0,0 +1,78 @@ +{ + "schemaVersion": 1, + "task": "M12-07E", + "parentTask": "M12-07D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_TYPESCRIPT_CHROMIUM_WORKER_AND_BLENDER_5_2", + "operation": "STL_NORMAL_UNIT_EDGE_PARITY", + "assertions": { + "binaryAsciiNormalExact": true, + "desktopNormalExact": true, + "unitRatioExactWithinFloat32": true, + "degenerateTriangleExact": true, + "removedDegenerateTriangles": 1, + "trailingWeb": "BLOCKED/STL_TRAILING_BYTES", + "trailingDesktop": "ACCEPTED_EMPTY", + "trailingParity": "STRICTER_WEB_BLOCK" + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-07D/manifest.json", + "sha256": "bbc78e47f389562e7d648bf49c9b3bf8a08aaa36e914589a6eab0a1f6e72748d" + }, + "fixtureGenerator": { + "path": "tools/web/generate-stl-edge-fixtures.mjs", + "sha256": "018013347642603c21237ebb023bdc7a4e338f5a3f875fea536a996eb121e716" + }, + "desktopProbe": { + "path": "tools/web/probe-stl-edge-fixtures.py", + "sha256": "b3d6ea197ef77b5a14f60f5e6b43d4392e943ff5d56acf73915507fc9a4161b7" + }, + "protocol": { + "path": "web/protocol/stl-import.ts", + "sha256": "87eec72e2b8aa7ad0b168ca0ee8c4016c941f56f5f1ac53aa5fe71d0832f1dd8" + }, + "worker": { + "path": "web/app/src/workers/stl-edge-test.worker.ts", + "sha256": "d714f1f3a218a2226dd349aea81c6c54efa6246de1c4fe51aaa9f5c352ef44fc" + }, + "unit": { + "path": "web/tests/unit/stl-import.test.mjs", + "sha256": "4af52833486421c017f3af2b833b0776e60a6ab57ed66fea2161cf9674f4b243" + }, + "chromium": { + "path": "web/tests/e2e/stl-edge-parity.spec.ts", + "sha256": "121d348250e26d29ad966f7427bbef9da77de2098e2e305827b11aae52002c5c" + }, + "checker": { + "path": "tools/web/check-stl-edge-parity.mjs", + "sha256": "b5ed7fbb41bb46fba982d5726cb8c3eaecf360b772a01d6337c309b1c4a4f535" + }, + "fixtureReport": { + "path": "tests/golden/M12-07E/edge-fixtures.json", + "sha256": "545463a984356d6635cbaae3865d13fe95bd2d841c394ac9ddff496c95d46f18" + }, + "desktopReport": { + "path": "tests/golden/M12-07E/desktop-edge-report.json", + "sha256": "2d3028a3b7d97f39654cff5fcef1d0c1c71e9f2d08e3e29c2e926651ed3860f1" + }, + "webReport": { + "path": "tests/golden/M12-07E/web-edge-report.json", + "sha256": "e0686cc9be3b68e564651207443e0ebf3e3b0eb3d07a32915b03f44b1908357b" + }, + "degenerateFixture": { + "path": "tests/files/web/m12_stl_edges_v1/degenerate-binary.stl", + "sha256": "c120bb0f218819eb89a3607c0b4f8fafd9afb599402e3b21deaa3b2be143e7d0" + }, + "trailingFixture": { + "path": "tests/files/web/m12_stl_edges_v1/trailing-binary.stl", + "sha256": "0a30aab6db1588722067000e2a08c3c6206a8ed5b7531caa0b082723700a3681" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07F" +} diff --git a/tests/golden/M12-07E/web-edge-report.json b/tests/golden/M12-07E/web-edge-report.json new file mode 100644 index 00000000..ab69a089 --- /dev/null +++ b/tests/golden/M12-07E/web-edge-report.json @@ -0,0 +1,436 @@ +{ + "schemaVersion": 1, + "task": "M12-07E", + "operation": "STL_NORMAL_UNIT_EDGE_PARITY", + "browser": [ + { + "id": "BINARY_UNIT_1", + "status": "ACCEPTED", + "result": { + "schemaVersion": 1, + "variant": "STL_BINARY", + "unitScale": 1, + "declaredTriangleCount": 2, + "triangleCount": 2, + "removedDegenerateTriangles": 0, + "normals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "vertices": [ + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ] + ], + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ], + [ + -1, + 0, + -1 + ] + ] + ], + "bounds": { + "min": [ + -1, + 0, + -1 + ], + "max": [ + 1, + 0, + 1 + ] + } + } + }, + { + "id": "BINARY_UNIT_001", + "status": "ACCEPTED", + "result": { + "schemaVersion": 1, + "variant": "STL_BINARY", + "unitScale": 0.001, + "declaredTriangleCount": 2, + "triangleCount": 2, + "removedDegenerateTriangles": 0, + "normals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "vertices": [ + [ + [ + -0.001, + 0, + 0.001 + ], + [ + 0.001, + 0, + 0.001 + ], + [ + 0.001, + 0, + -0.001 + ] + ], + [ + [ + -0.001, + 0, + 0.001 + ], + [ + 0.001, + 0, + -0.001 + ], + [ + -0.001, + 0, + -0.001 + ] + ] + ], + "bounds": { + "min": [ + -0.001, + 0, + -0.001 + ], + "max": [ + 0.001, + 0, + 0.001 + ] + } + } + }, + { + "id": "ASCII_UNIT_1", + "status": "ACCEPTED", + "result": { + "schemaVersion": 1, + "variant": "STL_ASCII", + "unitScale": 1, + "declaredTriangleCount": 2, + "triangleCount": 2, + "removedDegenerateTriangles": 0, + "normals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "vertices": [ + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ] + ], + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ], + [ + -1, + 0, + -1 + ] + ] + ], + "bounds": { + "min": [ + -1, + 0, + -1 + ], + "max": [ + 1, + 0, + 1 + ] + } + } + }, + { + "id": "DEGENERATE_TRIANGLE", + "status": "ACCEPTED", + "result": { + "schemaVersion": 1, + "variant": "STL_BINARY", + "unitScale": 1, + "declaredTriangleCount": 2, + "triangleCount": 1, + "removedDegenerateTriangles": 1, + "normals": [ + [ + 0, + 1, + 0 + ] + ], + "vertices": [ + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ], + [ + -1, + 0, + -1 + ] + ] + ], + "bounds": { + "min": [ + -1, + 0, + -1 + ], + "max": [ + 1, + 0, + 1 + ] + } + } + }, + { + "id": "TRAILING_BYTES", + "status": "BLOCKED", + "code": "STL_TRAILING_BYTES" + } + ], + "desktop": { + "cases": [ + { + "byteLength": 184, + "file": "capability-binary.stl", + "globalScale": 1, + "id": "BINARY_UNIT_1", + "result": { + "bounds": { + "max": [ + 1, + 1, + 7.549790126404332e-8 + ], + "min": [ + -1, + -1, + -7.549790126404332e-8 + ] + }, + "objectCount": 1, + "polygonCount": 2, + "polygonNormals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "status": "ACCEPTED", + "triangleCount": 2, + "vertexCount": 4 + }, + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + { + "byteLength": 184, + "file": "capability-binary.stl", + "globalScale": 0.001, + "id": "BINARY_UNIT_001", + "result": { + "bounds": { + "max": [ + 0.0010000000474974513, + 0.0010000000474974513, + 7.549790653760269e-11 + ], + "min": [ + -0.0010000000474974513, + -0.0010000000474974513, + -7.549790653760269e-11 + ] + }, + "objectCount": 1, + "polygonCount": 2, + "polygonNormals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "status": "ACCEPTED", + "triangleCount": 2, + "vertexCount": 4 + }, + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + { + "byteLength": 184, + "file": "degenerate-binary.stl", + "globalScale": 1, + "id": "DEGENERATE_TRIANGLE", + "result": { + "bounds": { + "max": [ + 1, + 1, + 7.549790126404332e-8 + ], + "min": [ + -1, + -1, + -7.549790126404332e-8 + ] + }, + "objectCount": 1, + "polygonCount": 1, + "polygonNormals": [ + [ + 0, + 1, + 0 + ] + ], + "status": "ACCEPTED", + "triangleCount": 1, + "vertexCount": 3 + }, + "sha256": "c120bb0f218819eb89a3607c0b4f8fafd9afb599402e3b21deaa3b2be143e7d0" + }, + { + "byteLength": 188, + "file": "trailing-binary.stl", + "globalScale": 1, + "id": "TRAILING_BYTES", + "result": { + "bounds": { + "max": [ + 0, + 0, + 0 + ], + "min": [ + 0, + 0, + 0 + ] + }, + "objectCount": 1, + "polygonCount": 0, + "polygonNormals": [], + "status": "ACCEPTED", + "triangleCount": 0, + "vertexCount": 0 + }, + "sha256": "0a30aab6db1588722067000e2a08c3c6206a8ed5b7531caa0b082723700a3681" + } + ], + "nextTask": "M12-07F", + "operation": "BLENDER_STL_EDGE_PROBE", + "schemaVersion": 1, + "task": "M12-07E" + }, + "comparisons": { + "binaryAsciiNormalExact": true, + "desktopNormalExact": true, + "webUnitRatio": 1000, + "desktopUnitRatio": 999.999952502551, + "unitRatioExactWithinFloat32": true, + "degenerateTriangleExact": true, + "trailingBytes": { + "web": "BLOCKED/STL_TRAILING_BYTES", + "desktop": "ACCEPTED_EMPTY", + "parity": "STRICTER_WEB_BLOCK" + } + }, + "nextTask": "M12-07F" +} diff --git a/tests/golden/M12-07F/manifest.json b/tests/golden/M12-07F/manifest.json new file mode 100644 index 00000000..9f2788a6 --- /dev/null +++ b/tests/golden/M12-07F/manifest.json @@ -0,0 +1,59 @@ +{ + "schemaVersion": 1, + "task": "M12-07F", + "parentTask": "M12-07E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WEB_TYPESCRIPT_CHROMIUM_WORKER_AND_BLENDER_5_2", + "operation": "WEB_STL_TO_DESKTOP_ROUNDTRIP", + "assertions": { + "browserTriangles": 2, + "desktopTriangles": 2, + "normalExact": true, + "materialLossCode": "STL_MATERIAL_UNSUPPORTED", + "desktopExact": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-07E/manifest.json", + "sha256": "19a9f460d1b939c9504dadd364cb87dab3b1769b0599035e2d2b51b47091e034" + }, + "protocol": { + "path": "web/protocol/stl-export.ts", + "sha256": "3b9c409de9fb3eaea34f82c1ddd466670d2b478845d8ff6af3c4e44cb4e04a52" + }, + "worker": { + "path": "web/app/src/workers/stl-roundtrip-test.worker.ts", + "sha256": "1da8b8281cf8ebf9ccf5fd2f42da8ed5886001ab83daba8d9fcd88980257af87" + }, + "desktopImporter": { + "path": "tools/web/check-stl-web-roundtrip.py", + "sha256": "c4cbff52ff2e7cecba7aeab47e865975955da23a0e1e8ff885ece56b894558d5" + }, + "unit": { + "path": "web/tests/unit/stl-export.test.mjs", + "sha256": "f06af242df80ec26d06e92b749449cfebaf909476198ff825207cdd4b9484102" + }, + "chromium": { + "path": "web/tests/e2e/stl-web-roundtrip.spec.ts", + "sha256": "2e979dcd030f5316fcbe28e2c19b1c99fe5d111e849d3fc1d2ea316d2159032b" + }, + "checker": { + "path": "tools/web/check-stl-web-roundtrip.mjs", + "sha256": "093de04bf80b0909eee56ea590e04e7aa4749168e2e49615c62ab39ddc0d16cf" + }, + "report": { + "path": "tests/golden/M12-07F/web-roundtrip-report.json", + "sha256": "0495c645b4280f6e7821f0ba02010e25d4accbc552e3cd7c58e052607799040e" + }, + "sourceFixture": { + "path": "tests/files/web/m12_stl_capability_v1/capability-binary.stl", + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07G" +} diff --git a/tests/golden/M12-07F/web-roundtrip-report.json b/tests/golden/M12-07F/web-roundtrip-report.json new file mode 100644 index 00000000..0c3ed3d0 --- /dev/null +++ b/tests/golden/M12-07F/web-roundtrip-report.json @@ -0,0 +1,122 @@ +{ + "schemaVersion": 1, + "task": "M12-07F", + "operation": "WEB_STL_TO_DESKTOP_ROUNDTRIP", + "source": { + "sha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "bytes": 184 + }, + "browser": { + "imported": { + "schemaVersion": 1, + "variant": "STL_BINARY", + "unitScale": 1, + "declaredTriangleCount": 2, + "triangleCount": 2, + "removedDegenerateTriangles": 0, + "normals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "vertices": [ + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ] + ], + [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ], + [ + -1, + 0, + -1 + ] + ] + ], + "bounds": { + "min": [ + -1, + 0, + -1 + ], + "max": [ + 1, + 0, + 1 + ] + } + }, + "outputSha256": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "outputBytes": 184, + "lossReport": { + "schemaVersion": 1, + "operation": "STL_EXPORT_LOSS_REPORT", + "canRoundTrip": true, + "warningCount": 1, + "warnings": [ + { + "code": "STL_MATERIAL_UNSUPPORTED", + "severity": "warning", + "message": "STL has no material slots; 2 source material assignments are omitted" + } + ] + } + }, + "desktop": { + "objectCount": 1, + "operation": "DESKTOP_IMPORT_WEB_STL", + "polygonCount": 2, + "polygonNormals": [ + [ + 0, + 1, + 0 + ], + [ + 0, + 1, + 0 + ] + ], + "schemaVersion": 1, + "sourceBytes": 184, + "sourceSha256": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "triangleCount": 2, + "vertexCount": 4 + }, + "comparison": { + "triangleCountExact": true, + "normalExact": true, + "materialLossExplicit": true + }, + "nextTask": "M12-07G" +} diff --git a/tests/golden/M12-07G/capability-report.json b/tests/golden/M12-07G/capability-report.json new file mode 100644 index 00000000..cf2a51fd --- /dev/null +++ b/tests/golden/M12-07G/capability-report.json @@ -0,0 +1,87 @@ +{ + "files": [ + { + "byteLength": 322, + "name": "capability-ascii.ply", + "sha256": "63646cab9bf6ccecb23092e5e24d04df1e0a690c866127c72a35791e99262331" + }, + { + "byteLength": 391, + "name": "capability-binary-le.ply", + "sha256": "e40fbb494b8b147c555a0fc06eb191415406bb9a6c061acf6befd8464c8c41a5" + } + ], + "nextTask": "M12-07H", + "operation": "DESKTOP_PLY_ASCII_BINARY_LE_CAPABILITY", + "operator": "wm.ply_export", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "settings": { + "exportColors": "NONE", + "exportNormals": true, + "exportSelectedObjects": true, + "exportTriangulatedMesh": true, + "exportUV": false, + "forwardAxis": "NEGATIVE_Z", + "globalScale": 1.0, + "upAxis": "Y" + }, + "sourceAnchor": "blender-5.2.0/source/blender/io/ply", + "task": "M12-07G", + "variants": [ + { + "asciiFormat": true, + "file": "capability-ascii.ply", + "id": "PLY_ASCII", + "semantic": { + "byteLength": 322, + "elements": [ + { + "count": 4, + "name": "vertex" + }, + { + "count": 2, + "name": "face" + } + ], + "format": "ascii", + "headerBytes": 254 + } + }, + { + "asciiFormat": false, + "file": "capability-binary-le.ply", + "id": "PLY_BINARY_LITTLE_ENDIAN", + "semantic": { + "byteLength": 391, + "elements": [ + { + "count": 4, + "name": "vertex" + }, + { + "count": 2, + "name": "face" + } + ], + "format": "binary_little_endian", + "headerBytes": 269 + } + } + ] +} diff --git a/tests/golden/M12-07G/manifest.json b/tests/golden/M12-07G/manifest.json new file mode 100644 index 00000000..37db16e5 --- /dev/null +++ b/tests/golden/M12-07G/manifest.json @@ -0,0 +1,52 @@ +{ + "schemaVersion": 1, + "task": "M12-07G", + "parentTask": "M12-07F", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "DESKTOP_PLY_ASCII_BINARY_LE_CAPABILITY", + "assertions": { + "asciiVariant": "ascii", + "binaryVariant": "binary_little_endian", + "vertexCount": 4, + "faceCount": 2, + "binaryLittleEndian": true, + "deterministicRegeneration": true + }, + "artifacts": { + "parentManifest": { + "path": "tests/golden/M12-07F/manifest.json", + "sha256": "3cbbcb541ee123da0ef8916ac2ca8805680d539bbf8db3b4a8d331aec418148c" + }, + "generator": { + "path": "tools/web/generate-ply-capability-fixtures.py", + "sha256": "581cd84057357e3a87997cf9c07d5d5633209648ac11e44611782eb254a38ebd" + }, + "checker": { + "path": "tools/web/check-ply-capability-fixtures.mjs", + "sha256": "5280d6e57b7a722ea881e27b792c6082c5113c1577ac0e87f87cc87fdf59516c" + }, + "desktopReport": { + "path": "tests/golden/M12-07G/capability-report.json", + "sha256": "26b1ce83334b41778cef5ce2a1f2c4d34254f63d7c7573cb3fb550f93ddeabb2" + }, + "asciiFixture": { + "path": "tests/files/web/m12_ply_capability_v1/capability-ascii.ply", + "sha256": "63646cab9bf6ccecb23092e5e24d04df1e0a690c866127c72a35791e99262331" + }, + "binaryFixture": { + "path": "tests/files/web/m12_ply_capability_v1/capability-binary-le.ply", + "sha256": "e40fbb494b8b147c555a0fc06eb191415406bb9a6c061acf6befd8464c8c41a5" + }, + "runtimeInventory": { + "path": "tests/golden/M12-05A/format-inventory.json", + "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" + }, + "package": { + "path": "web/package.json", + "sha256": "cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c" + } + }, + "nextTask": "M12-07H" +} diff --git a/tests/golden/M12-07H/manifest.json b/tests/golden/M12-07H/manifest.json new file mode 100644 index 00000000..41204b69 --- /dev/null +++ b/tests/golden/M12-07H/manifest.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "task": "M12-07H", + "parentTask": "M12-07G", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP_AND_CHROMIUM", + "operation": "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-07G/manifest.json", "sha256": "87df9c12f9c6eacf63051b70e9bb2eb43ebd1e5c4487b3512c45adb94cbb82ea" }, + "generator": { "path": "tools/web/generate-ply-mapping-fixtures.py", "sha256": "ffc051eccfc6973ee00cc791cdbd641fcce308b4083741e3e6ae82291d9347b7" }, + "fixtureChecker": { "path": "tools/web/check-ply-mapping-fixtures.mjs", "sha256": "2b055dbc705830848efdf4d8db8543a3ccc684de1f258732bcafefa86cf65c3a" }, + "desktopImporter": { "path": "tools/web/check-ply-web-roundtrip.py", "sha256": "6edbc6e401a1a902dcfd11c4d767e8c8620d694e40eb4ca29dd8479f9c55ef37" }, + "protocol": { "path": "web/protocol/ply-import.ts", "sha256": "058a3263fde553637840a4e9ad4e8e9d923eb52c250f8000317c7f43a228881d" }, + "worker": { "path": "web/app/src/workers/ply-roundtrip-test.worker.ts", "sha256": "f6bf5e39e83286d7b257bbdce88f4d7fa027c64651da9765567788b5cf298c71" }, + "unitTest": { "path": "web/tests/unit/ply-import.test.mjs", "sha256": "b03a5f4b4b2a974fa26e653763470b92d1433c5d352ae09ab5492b841e6e5e1f" }, + "e2eTest": { "path": "web/tests/e2e/ply-web-roundtrip.spec.ts", "sha256": "2cadebc89057655d78e9b520bb6adf9debb27c6d783cd002efee08d269636fb9" }, + "mappingReport": { "path": "tests/golden/M12-07H/mapping-report.json", "sha256": "8a02fc9e364ed79e50ef00897affa9ab63808d3cb3cdabd00fdb8ee4c26ec18a" }, + "webRoundtripReport": { "path": "tests/golden/M12-07H/web-roundtrip-report.json", "sha256": "1ca9d3b7870fcc8c9e3c9bbbd90ef48bd13bbc9ae0462312c9482f24f05f13ec" }, + "asciiFixture": { "path": "tests/files/web/m12_ply_mapping_v1/mapping-ascii.ply", "sha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5" }, + "binaryFixture": { "path": "tests/files/web/m12_ply_mapping_v1/mapping-binary-le.ply", "sha256": "d0fc195fd1a101c42ac984a2382bccdddb7d0bf60dd618e9f637c964f1b30b9e" }, + "unknownFixture": { "path": "tests/files/web/m12_ply_mapping_v1/unknown-property-ascii.ply", "sha256": "a994c7126f235d0067ce4af35f8b0c6699cc83073e2a37e982edd45ece459f33" }, + "runtimeInventory": { "path": "tests/golden/M12-05A/format-inventory.json", "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" }, + "package": { "path": "web/package.json", "sha256": "c56c653effb38f9ac9c53aa19a531ca0cdab04d4457212f286349091d5b65c22" } + }, + "nextTask": "M12-07I" +} diff --git a/tests/golden/M12-07H/mapping-report.json b/tests/golden/M12-07H/mapping-report.json new file mode 100644 index 00000000..84ebee3c --- /dev/null +++ b/tests/golden/M12-07H/mapping-report.json @@ -0,0 +1,292 @@ +{ + "files": [ + { + "byteLength": 521, + "name": "mapping-ascii.ply", + "sha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5" + }, + { + "byteLength": 568, + "name": "mapping-binary-le.ply", + "sha256": "d0fc195fd1a101c42ac984a2382bccdddb7d0bf60dd618e9f637c964f1b30b9e" + }, + { + "byteLength": 586, + "name": "unknown-property-ascii.ply", + "sha256": "a994c7126f235d0067ce4af35f8b0c6699cc83073e2a37e982edd45ece459f33" + } + ], + "nextTask": "M12-07I", + "operation": "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", + "operator": "wm.ply_export", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "settings": { + "exportAttributes": true, + "exportColors": "SRGB", + "exportNormals": true, + "exportSelectedObjects": true, + "exportTriangulatedMesh": true, + "exportUV": false, + "forwardAxis": "NEGATIVE_Z", + "globalScale": 1.0, + "upAxis": "Y" + }, + "sourceAnchor": "blender-5.2.0/source/blender/io/ply", + "task": "M12-07H", + "unknownProperty": { + "expectedCode": "PLY_UNKNOWN_PROPERTY", + "file": "unknown-property-ascii.ply", + "property": "unknown_values" + }, + "variants": [ + { + "file": "mapping-ascii.ply", + "id": "PLY_ASCII_MAPPING", + "semantic": { + "faceCount": 2, + "faces": [ + { + "customProperties": {}, + "indices": [ + 0, + 1, + 2 + ] + }, + { + "customProperties": {}, + "indices": [ + 0, + 2, + 3 + ] + } + ], + "format": "ascii", + "vertexCount": 4, + "vertices": [ + { + "color": [ + 254, + 0, + 0, + 255 + ], + "customProperties": { + "label": 1.0, + "temperature": 10.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + -1.0, + 0.0, + 1.0 + ] + }, + { + "color": [ + 0, + 254, + 0, + 255 + ], + "customProperties": { + "label": 2.0, + "temperature": 20.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + 1.0, + 0.0, + 1.0 + ] + }, + { + "color": [ + 0, + 0, + 254, + 255 + ], + "customProperties": { + "label": 3.0, + "temperature": 30.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + 1.0, + 0.0, + -1.0 + ] + }, + { + "color": [ + 254, + 254, + 0, + 255 + ], + "customProperties": { + "label": 4.0, + "temperature": 40.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + -1.0, + 0.0, + -1.0 + ] + } + ] + } + }, + { + "file": "mapping-binary-le.ply", + "id": "PLY_BINARY_LITTLE_ENDIAN_MAPPING", + "semantic": { + "faceCount": 2, + "faces": [ + { + "customProperties": {}, + "indices": [ + 0, + 1, + 2 + ] + }, + { + "customProperties": {}, + "indices": [ + 0, + 2, + 3 + ] + } + ], + "format": "binary_little_endian", + "vertexCount": 4, + "vertices": [ + { + "color": [ + 254, + 0, + 0, + 255 + ], + "customProperties": { + "label": 1.0, + "temperature": 10.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + -1.0, + 0.0, + 1.0 + ] + }, + { + "color": [ + 0, + 254, + 0, + 255 + ], + "customProperties": { + "label": 2.0, + "temperature": 20.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + 1.0, + 0.0, + 1.0 + ] + }, + { + "color": [ + 0, + 0, + 254, + 255 + ], + "customProperties": { + "label": 3.0, + "temperature": 30.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + 1.0, + 0.0, + -1.0 + ] + }, + { + "color": [ + 254, + 254, + 0, + 255 + ], + "customProperties": { + "label": 4.0, + "temperature": 40.0 + }, + "normal": [ + 0.0, + 1.0, + 0.0 + ], + "position": [ + -1.0, + 0.0, + -1.0 + ] + } + ] + } + } + ] +} diff --git a/tests/golden/M12-07H/web-roundtrip-report.json b/tests/golden/M12-07H/web-roundtrip-report.json new file mode 100644 index 00000000..d7b6cc0f --- /dev/null +++ b/tests/golden/M12-07H/web-roundtrip-report.json @@ -0,0 +1,179 @@ +{ + "schemaVersion": 1, + "task": "M12-07H", + "operation": "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", + "source": { + "asciiSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "binarySha256": "d0fc195fd1a101c42ac984a2382bccdddb7d0bf60dd618e9f637c964f1b30b9e", + "unknownSha256": "a994c7126f235d0067ce4af35f8b0c6699cc83073e2a37e982edd45ece459f33" + }, + "browser": { + "format": "ascii", + "vertexCount": 4, + "faceCount": 2, + "colors": [ + [ + 0.996078431372549, + 0, + 0, + 1 + ], + [ + 0, + 0.996078431372549, + 0, + 1 + ], + [ + 0, + 0, + 0.996078431372549, + 1 + ], + [ + 0.996078431372549, + 0.996078431372549, + 0, + 1 + ] + ], + "customProperties": [ + { + "temperature": 10, + "label": 1 + }, + { + "temperature": 20, + "label": 2 + }, + { + "temperature": 30, + "label": 3 + }, + { + "temperature": 40, + "label": 4 + } + ], + "outputSha256": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "outputBytes": 509, + "lossReport": { + "schemaVersion": 1, + "operation": "PLY_IMPORT_LOSS_REPORT", + "canImport": true, + "warningCount": 0, + "warnings": [] + }, + "binarySemanticEqual": true, + "unknownPropertyLoss": { + "schemaVersion": 1, + "operation": "PLY_IMPORT_LOSS_REPORT", + "canImport": true, + "warningCount": 1, + "warnings": [ + { + "code": "PLY_UNKNOWN_PROPERTY", + "severity": "warning", + "element": "vertex", + "property": "unknown_values", + "message": "vertex list property unknown_values is not mapped" + } + ] + } + }, + "desktop": { + "attributes": [ + { + "dataType": "FLOAT_COLOR", + "domain": "POINT", + "name": "Col", + "values": [ + [ + 0.9911020398139954, + 0, + 0, + 1 + ], + [ + 0, + 0.9911020398139954, + 0, + 1 + ], + [ + 0, + 0, + 0.9911020398139954, + 1 + ], + [ + 0.9911020398139954, + 0.9911020398139954, + 0, + 1 + ] + ] + }, + { + "dataType": "FLOAT", + "domain": "POINT", + "name": "label", + "values": [ + 1, + 2, + 3, + 4 + ] + }, + { + "dataType": "FLOAT", + "domain": "POINT", + "name": "temperature", + "values": [ + 10, + 20, + 30, + 40 + ] + } + ], + "objectCount": 1, + "operation": "DESKTOP_IMPORT_WEB_PLY", + "polygonCount": 2, + "positions": [ + [ + -1, + 0, + 1 + ], + [ + 1, + 0, + 1 + ], + [ + 1, + 0, + -1 + ], + [ + -1, + 0, + -1 + ] + ], + "schemaVersion": 1, + "sourceBytes": 509, + "sourceSha256": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "triangleCount": 2, + "vertexCount": 4 + }, + "comparisons": { + "vertexCountExact": true, + "faceCountExact": true, + "positionExact": true, + "customPropertiesPresent": true, + "colorMapped": true + }, + "nextTask": "M12-07I" +} diff --git a/tests/golden/M12-07I/manifest.json b/tests/golden/M12-07I/manifest.json new file mode 100644 index 00000000..11d4a282 --- /dev/null +++ b/tests/golden/M12-07I/manifest.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "task": "M12-07I", + "parentTask": "M12-07H", + "enablingTask": false, + "parityStateChange": false, + "runtime": "CHROMIUM_WORKER", + "operation": "PLY_NEGATIVE_FORMAT_LIST_COUNT", + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-07H/manifest.json", "sha256": "0665f7c8278cad182f9af0be6af80838fb204e8e1160e5a0cdeb58e052f3b878" }, + "protocol": { "path": "web/protocol/ply-import.ts", "sha256": "058a3263fde553637840a4e9ad4e8e9d923eb52c250f8000317c7f43a228881d" }, + "generator": { "path": "tools/web/generate-ply-negative-fixtures.mjs", "sha256": "9c09707bdfe73ba467bbfbf61ed3846fb59fd33ab5563a3a62c8032722ff6938" }, + "checker": { "path": "tools/web/check-ply-negative-fixtures.mjs", "sha256": "b1d047d4cb1fa15dff7821687e7028ad073fdc62d4c76f60a2656732c0ec5e2b" }, + "unitTest": { "path": "web/tests/unit/ply-negative.test.mjs", "sha256": "60439f9e6bc221df8866956bf926816a347e85828b5a93deb26ee23015cf449a" }, + "e2eTest": { "path": "web/tests/e2e/ply-negative.spec.ts", "sha256": "6509a29d6842f3423d4dfcc40dbd52a959a5efa7ee1121143dce06e5bbc4a460" }, + "negativeReport": { "path": "tests/golden/M12-07I/negative-report.json", "sha256": "fbe2830d1b19294ea98eea66c3e00125bc0809a4bb8a750612939cbe1f5acca9" }, + "bigEndianFixture": { "path": "tests/files/web/m12_ply_negative_v1/big-endian.ply", "sha256": "cda92943a3690529fbb3463d328b6796ac0d07e19139450556f7411fc1f031e3" }, + "malformedListFixture": { "path": "tests/files/web/m12_ply_negative_v1/malformed-list-ascii.ply", "sha256": "a6a576b7a04d919b540520a6f43a89c089f0d706a17fd8b390711fff6b8b03df" }, + "oversizedCountFixture": { "path": "tests/files/web/m12_ply_negative_v1/oversized-count-ascii.ply", "sha256": "fcd99e0838025429420a47466251cfef80e638d2b5816ad14d5aa696364525bb" } + }, + "nextTask": "M12-07J" +} diff --git a/tests/golden/M12-07I/negative-report.json b/tests/golden/M12-07I/negative-report.json new file mode 100644 index 00000000..633ef401 --- /dev/null +++ b/tests/golden/M12-07I/negative-report.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M12-07I", + "operation": "PLY_NEGATIVE_FORMAT_LIST_COUNT", + "cases": [ + { + "id": "big-endian", + "file": "big-endian.ply", + "expectedCode": "PLY_FORMAT_UNSUPPORTED" + }, + { + "id": "malformed-list", + "file": "malformed-list-ascii.ply", + "expectedCode": "PLY_DATA_TRUNCATED" + }, + { + "id": "oversized-count", + "file": "oversized-count-ascii.ply", + "expectedCode": "PLY_IMPORT_BUDGET_EXCEEDED: vertex" + } + ], + "files": [ + { + "name": "big-endian.ply", + "byteLength": 179, + "sha256": "cda92943a3690529fbb3463d328b6796ac0d07e19139450556f7411fc1f031e3" + }, + { + "name": "malformed-list-ascii.ply", + "byteLength": 179, + "sha256": "a6a576b7a04d919b540520a6f43a89c089f0d706a17fd8b390711fff6b8b03df" + }, + { + "name": "oversized-count-ascii.ply", + "byteLength": 159, + "sha256": "fcd99e0838025429420a47466251cfef80e638d2b5816ad14d5aa696364525bb" + } + ], + "nextTask": "M12-07J" +} diff --git a/tests/golden/M12-07J/io-format-recovery-report.json b/tests/golden/M12-07J/io-format-recovery-report.json new file mode 100644 index 00000000..95047007 --- /dev/null +++ b/tests/golden/M12-07J/io-format-recovery-report.json @@ -0,0 +1,376 @@ +{ + "schemaVersion": 1, + "task": "M12-07J", + "operation": "IO_FORMAT_THREE_WAY_RECOVERY", + "formats": { + "OBJ": { + "sourceSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "cancel": { + "schemaVersion": 1, + "operationId": "obj-cancel-1", + "format": "OBJ", + "operation": "IMPORT", + "status": "CANCELLED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 670, + "inputSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "outputBytes": 0, + "outputSha256": null, + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 0, + "committed": false, + "errorCode": "IO_FORMAT_OPERATION_CANCELLED" + }, + "oom": { + "schemaVersion": 1, + "operationId": "obj-oom-1", + "format": "OBJ", + "operation": "IMPORT", + "status": "BLOCKED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 524289, + "inputSha256": "42b47b7a78c2a45c3ab9694e86574825450221fe25d23bf505b97af4ca20283b", + "outputBytes": 0, + "outputSha256": null, + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 0, + "committed": false, + "errorCode": "IO_FORMAT_OOM" + }, + "first": { + "schemaVersion": 1, + "operationId": "obj-first-1", + "format": "OBJ", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 670, + "inputSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "outputBytes": 530, + "outputSha256": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "second": { + "schemaVersion": 1, + "operationId": "obj-second-2", + "format": "OBJ", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 2, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 670, + "inputSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "outputBytes": 530, + "outputSha256": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "recovered": { + "schemaVersion": 1, + "operationId": "obj-first-1", + "format": "OBJ", + "operation": "IMPORT", + "status": "RECOVERED", + "workerGeneration": 2, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 670, + "inputSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "outputBytes": 530, + "outputSha256": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true, + "errorCode": "IO_FORMAT_WORKER_RESTARTED" + }, + "small": { + "schemaVersion": 1, + "operationId": "obj-small-3", + "format": "OBJ", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 3, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 670, + "inputSha256": "4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a", + "outputBytes": 530, + "outputSha256": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "hashes": { + "first": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d", + "second": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d", + "small": "caa36e17111d6dbc9369581b9a52ecb4ff81a951eb11ff3094f80ac273c4c53d" + } + }, + "STL": { + "sourceSha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "cancel": { + "schemaVersion": 1, + "operationId": "stl-cancel-1", + "format": "STL", + "operation": "IMPORT", + "status": "CANCELLED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 184, + "inputSha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "outputBytes": 0, + "outputSha256": null, + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 0, + "committed": false, + "errorCode": "IO_FORMAT_OPERATION_CANCELLED" + }, + "oom": { + "schemaVersion": 1, + "operationId": "stl-oom-1", + "format": "STL", + "operation": "IMPORT", + "status": "BLOCKED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 524289, + "inputSha256": "42b47b7a78c2a45c3ab9694e86574825450221fe25d23bf505b97af4ca20283b", + "outputBytes": 0, + "outputSha256": null, + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 0, + "committed": false, + "errorCode": "IO_FORMAT_OOM" + }, + "first": { + "schemaVersion": 1, + "operationId": "stl-first-1", + "format": "STL", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 184, + "inputSha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "outputBytes": 184, + "outputSha256": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "second": { + "schemaVersion": 1, + "operationId": "stl-second-2", + "format": "STL", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 2, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 184, + "inputSha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "outputBytes": 184, + "outputSha256": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "recovered": { + "schemaVersion": 1, + "operationId": "stl-first-1", + "format": "STL", + "operation": "IMPORT", + "status": "RECOVERED", + "workerGeneration": 2, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 184, + "inputSha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "outputBytes": 184, + "outputSha256": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true, + "errorCode": "IO_FORMAT_WORKER_RESTARTED" + }, + "small": { + "schemaVersion": 1, + "operationId": "stl-small-3", + "format": "STL", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 3, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 184, + "inputSha256": "50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca", + "outputBytes": 184, + "outputSha256": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "hashes": { + "first": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "second": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d", + "small": "689d060c125c41a79d98f4ff241ebe10d9e0f1faebe6e66e507efa962dce1b2d" + } + }, + "PLY": { + "sourceSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "cancel": { + "schemaVersion": 1, + "operationId": "ply-cancel-1", + "format": "PLY", + "operation": "IMPORT", + "status": "CANCELLED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 521, + "inputSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "outputBytes": 0, + "outputSha256": null, + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 0, + "committed": false, + "errorCode": "IO_FORMAT_OPERATION_CANCELLED" + }, + "oom": { + "schemaVersion": 1, + "operationId": "ply-oom-1", + "format": "PLY", + "operation": "IMPORT", + "status": "BLOCKED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 524289, + "inputSha256": "42b47b7a78c2a45c3ab9694e86574825450221fe25d23bf505b97af4ca20283b", + "outputBytes": 0, + "outputSha256": null, + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 0, + "committed": false, + "errorCode": "IO_FORMAT_OOM" + }, + "first": { + "schemaVersion": 1, + "operationId": "ply-first-1", + "format": "PLY", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 1, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 521, + "inputSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "outputBytes": 509, + "outputSha256": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "second": { + "schemaVersion": 1, + "operationId": "ply-second-2", + "format": "PLY", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 2, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 521, + "inputSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "outputBytes": 509, + "outputSha256": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "recovered": { + "schemaVersion": 1, + "operationId": "ply-first-1", + "format": "PLY", + "operation": "IMPORT", + "status": "RECOVERED", + "workerGeneration": 2, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 521, + "inputSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "outputBytes": 509, + "outputSha256": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true, + "errorCode": "IO_FORMAT_WORKER_RESTARTED" + }, + "small": { + "schemaVersion": 1, + "operationId": "ply-small-3", + "format": "PLY", + "operation": "IMPORT", + "status": "COMMITTED", + "workerGeneration": 3, + "baseRevision": 4, + "candidateRevision": 5, + "inputBytes": 521, + "inputSha256": "acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5", + "outputBytes": 509, + "outputSha256": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "temporaryBytes": 0, + "liveRequests": 0, + "publishedResults": 1, + "committed": true + }, + "hashes": { + "first": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "second": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5", + "small": "399a06c8f593dcdeda9ce9f4c6be74fa83b9fc905ae22a01f4af414284585cf5" + } + } + }, + "assertions": { + "formats": [ + "OBJ", + "STL", + "PLY" + ], + "cancellationUnpublished": true, + "oomUnpublished": true, + "restartHashStable": true, + "smallRecoveryStable": true + }, + "nextTask": "M13-01A" +} diff --git a/tests/golden/M12-07J/manifest.json b/tests/golden/M12-07J/manifest.json new file mode 100644 index 00000000..fe389a9f --- /dev/null +++ b/tests/golden/M12-07J/manifest.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": 1, + "task": "M12-07J", + "parentTask": "M12-07I", + "enablingTask": false, + "parityStateChange": false, + "runtime": "CHROMIUM_WORKER", + "operation": "IO_FORMAT_THREE_WAY_RECOVERY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-07I/manifest.json", "sha256": "02be4b2e2cabecf4a239ba52be385b926a70e794c6f8c745d89dada568e6d674" }, + "protocol": { "path": "web/protocol/io-format-recovery.ts", "sha256": "7e352539e3300969c7409c34d6056eb6ad31055431ffce84b1b0a459c335480a" }, + "worker": { "path": "web/app/src/workers/io-format-recovery-test.worker.ts", "sha256": "63b78fbf25132cad28147ef68731f2cd212a26c96b464fdec349a13ebaa1174f" }, + "testingAdapter": { "path": "web/app/src/testing/io-format-recovery.ts", "sha256": "cfcebb6ec38936a66983957b0dede716871cfbfbea3cb53cddc16f3e24fb19b0" }, + "unitTest": { "path": "web/tests/unit/io-format-recovery.test.mjs", "sha256": "aaed1f2abe4789b084c8a6a60bcce8595d38220d7e6678a93924cd05c5716b4f" }, + "e2eTest": { "path": "web/tests/e2e/io-format-recovery.spec.ts", "sha256": "5c1750fa408e1297fc43f2e5749be3e9ac08a16b86265d22f0f06053f52b3bd7" }, + "checker": { "path": "tools/web/check-io-format-recovery.mjs", "sha256": "6ef6bc4a168f8675a4933a06c296f61076b9ea64cec25b295e961a9b610ab1a2" }, + "report": { "path": "tests/golden/M12-07J/io-format-recovery-report.json", "sha256": "35d4fe10d8a4fa84d6e05a85f20ca50975d93a86df41e73c7bbc5875edc4fc70" } + }, + "nextTask": "M13-01A" +} diff --git a/tests/golden/M13-01A/entry-inventory.json b/tests/golden/M13-01A/entry-inventory.json new file mode 100644 index 00000000..c3bd1e7c --- /dev/null +++ b/tests/golden/M13-01A/entry-inventory.json @@ -0,0 +1,156 @@ +{ + "executionPolicy": { + "addon": "DENY", + "autorun": "DENY", + "driverExpression": "DENY", + "handler": "DENY", + "pythonConsole": "DENY", + "text": "READ_METADATA_ONLY" + }, + "fixture": { + "byteLength": 497845, + "name": "script-entry-inventory.blend", + "sha256": "bd6375d02908bfcc57ff7cdeec3f9827bfc4336d1e46e165c5fbbf4d04f19645" + }, + "inventory": { + "addons": { + "defaultExecution": "DENY", + "enabledAddons": [ + "bl_pkg", + "cycles", + "io_anim_bvh", + "io_curve_svg", + "io_mesh_uv_layout", + "io_scene_fbx", + "io_scene_gltf2", + "pose_library" + ], + "operatorIds": [ + "preferences.addon_install", + "preferences.addon_enable", + "preferences.addon_disable", + "preferences.addon_remove" + ] + }, + "autorun": { + "defaultExecution": "DENY", + "moduleTextNames": [ + "ModuleAutorun.py" + ] + }, + "driverExpressions": { + "count": 1, + "defaultExecution": "DENY", + "entries": [ + { + "arrayIndex": 0, + "dataPath": "location", + "expression": "var * 2", + "object": "M13 Script Inventory Object", + "variableCount": 1 + } + ] + }, + "handlers": { + "defaultExecution": "DENY", + "groups": [ + "animation_playback_post", + "animation_playback_pre", + "annotation_post", + "annotation_pre", + "blend_import_post", + "blend_import_pre", + "composite_cancel", + "composite_post", + "composite_pre", + "depsgraph_update_post", + "depsgraph_update_pre", + "exit_pre", + "frame_change_post", + "frame_change_pre", + "load_factory_preferences_post", + "load_factory_startup_post", + "load_post", + "load_post_fail", + "load_pre", + "object_bake_cancel", + "object_bake_complete", + "object_bake_pre", + "redo_post", + "redo_pre", + "render_cancel", + "render_complete", + "render_init", + "render_post", + "render_pre", + "render_stats", + "render_write", + "save_post", + "save_post_fail", + "save_pre", + "translation_update_post", + "undo_post", + "undo_pre", + "version_update", + "xr_session_start_pre" + ] + }, + "pythonConsole": { + "available": true, + "operatorIds": [ + "console.execute", + "console.history_append", + "console.scrollback_append" + ] + }, + "text": { + "count": 3, + "entries": [ + { + "byteLength": 29, + "filepath": "//scripts/external_project.py", + "internal": false, + "lineCount": 2, + "name": "ExternalProject.py", + "useModule": false + }, + { + "byteLength": 23, + "filepath": "", + "internal": true, + "lineCount": 3, + "name": "InternalSafe.py", + "useModule": false + }, + { + "byteLength": 37, + "filepath": "", + "internal": true, + "lineCount": 3, + "name": "ModuleAutorun.py", + "useModule": true + } + ] + } + }, + "nextTask": "M13-01B", + "operation": "BLENDER_SCRIPT_ENTRY_INVENTORY", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "schemaVersion": 1, + "sourceAnchor": "blender-5.2.0/source/blender/python", + "task": "M13-01A" +} diff --git a/tests/golden/M13-01A/manifest.json b/tests/golden/M13-01A/manifest.json new file mode 100644 index 00000000..1c14f961 --- /dev/null +++ b/tests/golden/M13-01A/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-01A", + "parentTask": "M12-07J", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "BLENDER_SCRIPT_ENTRY_INVENTORY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M12-07J/manifest.json", "sha256": "a1c4cf9c2cc300ace388e6c430bd1aa991511a7d1c5482c638fb298bb362cd02" }, + "generator": { "path": "tools/web/generate-script-entry-inventory.py", "sha256": "b72667493cfe9957161ef3fb9814180e0cfad5f8aaa1c60c9b6f132e915f3d6f" }, + "checker": { "path": "tools/web/check-script-entry-inventory.mjs", "sha256": "68478f15de4d63ed175e6b580761fd478b1fe9bccbfcaeee82218d13063dfa51" }, + "report": { "path": "tests/golden/M13-01A/entry-inventory.json", "sha256": "e024995c53f01beaf725f281f74a6e5ec6018a53435da61a66f5e58a9e50973c" }, + "fixture": { "path": "tests/files/web/m13_script_entry_v1/script-entry-inventory.blend", "sha256": "bd6375d02908bfcc57ff7cdeec3f9827bfc4336d1e46e165c5fbbf4d04f19645" }, + "runtimeInventory": { "path": "tests/golden/M12-05A/format-inventory.json", "sha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4" } + }, + "nextTask": "M13-01B" +} diff --git a/tests/golden/M13-01B/manifest.json b/tests/golden/M13-01B/manifest.json new file mode 100644 index 00000000..84ce2bd3 --- /dev/null +++ b/tests/golden/M13-01B/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M13-01B", + "parentTask": "M13-01A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_WASM_MAIN_AND_CHROMIUM", + "operation": "BLEND_OPEN_SCRIPT_METADATA_ONLY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-01A/manifest.json", "sha256": "d4a305033343ef5fb1ffc073617b59d9012f64b80ecb233e743fb0789a8b4893" }, + "checker": { "path": "tools/web/check-script-open-metadata.mjs", "sha256": "66396d5c9a5294021ae077bb162278c74d46de8b52ac8a444a5de4f6d84cfe05" }, + "e2eTest": { "path": "web/tests/e2e/script-open-metadata.spec.ts", "sha256": "df6412cda113c830996e08c3d66a035dc1ac309e392f5946a762d11121b1926c" }, + "report": { "path": "tests/golden/M13-01B/open-metadata-report.json", "sha256": "f92470e57c048452134664f7f6208e50b6f087dbcbc33369e6b882c51813990c" }, + "fixture": { "path": "tests/files/web/script_scene.blend", "sha256": "2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037" } + }, + "nextTask": "M13-01C" +} diff --git a/tests/golden/M13-01B/open-metadata-report.json b/tests/golden/M13-01B/open-metadata-report.json new file mode 100644 index 00000000..a5751139 --- /dev/null +++ b/tests/golden/M13-01B/open-metadata-report.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "task": "M13-01B", + "operation": "BLEND_OPEN_SCRIPT_METADATA_ONLY", + "fixture": { + "path": "tests/files/web/script_scene.blend", + "byteLength": 490191, + "sha256": "2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037" + }, + "sources": [ + { + "name": "ExternalProject.py", + "executionStatus": "BLOCKED", + "readOnly": true + }, + { + "name": "InternalSafe.py", + "executionStatus": "BLOCKED", + "readOnly": true + }, + { + "name": "ModuleAutorun.py", + "executionStatus": "BLOCKED", + "readOnly": true, + "moduleAutorunRequested": true, + "errorCode": "SCRIPT_POLICY_DENIED" + } + ], + "execution": "DENY", + "nextTask": "M13-01C" +} diff --git a/tests/golden/M13-01C/manifest.json b/tests/golden/M13-01C/manifest.json new file mode 100644 index 00000000..98939f89 --- /dev/null +++ b/tests/golden/M13-01C/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "task": "M13-01C", + "parentTask": "M13-01B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "WASM_PROTOCOL_NODE", + "operation": "SCRIPT_DEFAULT_DENY_POLICY_CODES", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-01B/manifest.json", "sha256": "0bb0abdb9f4d250a15c3889a4fb5c2630b8c416f4eb5c6523aa30097c8e45fd3" }, + "checker": { "path": "tools/web/check-script-policy-codes.mjs", "sha256": "22588c2198bc8c425ab8e104e8559f0053654ae778aaea3783ec7d54822bc8f4" }, + "unitTest": { "path": "web/tests/unit/script-policy-codes.test.mjs", "sha256": "b5a52b8e707963545f1cd71f1a148fa9c9b9d1e4b4c5f51c87d33f8bf3777430" }, + "report": { "path": "tests/golden/M13-01C/policy-codes-report.json", "sha256": "956db548ee71688a4b89c9a993259d3e57129cd4abe9efd1b9397b3e30b1bf84" } + }, + "nextTask": "M13-01D" +} diff --git a/tests/golden/M13-01C/policy-codes-report.json b/tests/golden/M13-01C/policy-codes-report.json new file mode 100644 index 00000000..01c276b3 --- /dev/null +++ b/tests/golden/M13-01C/policy-codes-report.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "task": "M13-01C", + "operation": "SCRIPT_DEFAULT_DENY_POLICY_CODES", + "deniedEntries": [ + { + "entry": "autorun", + "code": "SCRIPT_POLICY_DENIED" + }, + { + "entry": "driverExpressions", + "code": "DRIVER_EXECUTION_BLOCKED" + }, + { + "entry": "addonInstall", + "code": "ADDON_INSTALL_BLOCKED" + } + ], + "approvedKeySandboxCode": "SCRIPT_SANDBOX_UNAVAILABLE", + "nextTask": "M13-01D" +} diff --git a/tests/golden/M13-01D/manifest.json b/tests/golden/M13-01D/manifest.json new file mode 100644 index 00000000..5ab327c0 --- /dev/null +++ b/tests/golden/M13-01D/manifest.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "task": "M13-01D", + "parentTask": "M13-01C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "STATIC_PRODUCTION_SOURCE", + "operation": "SCRIPT_UI_DIRECT_EVAL_BYPASS_SCAN", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-01C/manifest.json", "sha256": "b35728fcd8e6267a3e5ee925d45abde019597b20eeb4c4c8e0a373ca99907d9b" }, + "checker": { "path": "tools/web/check-script-ui-bypass.mjs", "sha256": "f34cb64891c2b22bc3da353f6b864ba3e558d3c286cf716bcb778d9a542cb3d9" }, + "report": { "path": "tests/golden/M13-01D/ui-bypass-report.json", "sha256": "6b050092088508ebd5d769d95a2e66f0cd4020c97f3407724a19b9707f51f6dd" } + }, + "nextTask": "M13-01E" +} diff --git a/tests/golden/M13-01D/ui-bypass-report.json b/tests/golden/M13-01D/ui-bypass-report.json new file mode 100644 index 00000000..b69d234f --- /dev/null +++ b/tests/golden/M13-01D/ui-bypass-report.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "task": "M13-01D", + "operation": "SCRIPT_UI_DIRECT_EVAL_BYPASS_SCAN", + "roots": [ + "web/app/src/app", + "web/app/src/workers", + "web/protocol" + ], + "scannedFiles": 176, + "violations": [], + "policyEntrypoints": [ + "gateScriptExecution", + "gateServerScriptJob", + "parseScriptSourceInventory" + ], + "execution": "DENY", + "nextTask": "M13-01E" +} diff --git a/tests/golden/M13-01E/manifest.json b/tests/golden/M13-01E/manifest.json new file mode 100644 index 00000000..60d180bd --- /dev/null +++ b/tests/golden/M13-01E/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M13-01E", + "parentTask": "M13-01D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "CHROMIUM_WEB_ENGINE", + "operation": "SCRIPT_TEXT_SAVE_REOPEN", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-01D/manifest.json", "sha256": "6b28688b3ebcce5629bcfc437d4ca903d0b1e053b32a796690ee4e021726c75b" }, + "checker": { "path": "tools/web/check-script-save-reopen.mjs", "sha256": "7fc9a370cb472636e2699565cb21a1450e3cc8a2c90ffdcdff96533b344afa7c" }, + "e2eTest": { "path": "web/tests/e2e/script-save-reopen.spec.ts", "sha256": "481f78f3a0cce923b67ab14436cc23cbcd2ce66725de29dc874fea4fb5801227" }, + "report": { "path": "tests/golden/M13-01E/script-save-reopen-report.json", "sha256": "0f6869a8ec8342ed87649312d2872ab2c172cbf12d6be81bb0b770ebcbe8a398" }, + "fixture": { "path": "tests/files/web/script_scene.blend", "sha256": "2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037" } + }, + "nextTask": "M13-01F" +} diff --git a/tests/golden/M13-01E/script-save-reopen-report.json b/tests/golden/M13-01E/script-save-reopen-report.json new file mode 100644 index 00000000..850ab0c4 --- /dev/null +++ b/tests/golden/M13-01E/script-save-reopen-report.json @@ -0,0 +1,102 @@ +{ + "schemaVersion": 1, + "task": "M13-01E", + "operation": "SCRIPT_TEXT_SAVE_REOPEN", + "source": { + "fixtureSha256": "2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037", + "fixtureBytes": 490191 + }, + "before": { + "schemaVersion": 1, + "sources": [ + { + "byteLength": 29, + "errorCode": "SCRIPT_SANDBOX_UNAVAILABLE", + "executionStatus": "BLOCKED", + "id": "text:ExternalProject.py", + "internal": false, + "lineCount": 2, + "moduleAutorunRequested": false, + "name": "ExternalProject.py", + "readOnly": true, + "source": "message = 'project relative'\n", + "sourcePath": "//scripts/external_project.py", + "sourceSha256": "1332a556fa4a8c0d55c6661931bcc7c3cd79a193bab5e34c8b060cf97fec6a96" + }, + { + "byteLength": 23, + "errorCode": "SCRIPT_SANDBOX_UNAVAILABLE", + "executionStatus": "BLOCKED", + "id": "text:InternalSafe.py", + "internal": true, + "lineCount": 3, + "moduleAutorunRequested": false, + "name": "InternalSafe.py", + "readOnly": true, + "source": "value = 7\nprint(value)\n", + "sourceSha256": "1676bea86b7d12f595d531c32286e26ad0ac7a249d1bd40744b01634977ae582" + }, + { + "byteLength": 37, + "errorCode": "SCRIPT_POLICY_DENIED", + "executionStatus": "BLOCKED", + "id": "text:ModuleAutorun.py", + "internal": true, + "lineCount": 3, + "moduleAutorunRequested": true, + "name": "ModuleAutorun.py", + "readOnly": true, + "source": "def register():\n return 'blocked'\n", + "sourceSha256": "4b1d1ffa97bf18cd834d1a2472cab16f24f1439143964d09d283b6cc043e17bd" + } + ] + }, + "after": { + "schemaVersion": 1, + "sources": [ + { + "byteLength": 29, + "errorCode": "SCRIPT_SANDBOX_UNAVAILABLE", + "executionStatus": "BLOCKED", + "id": "text:ExternalProject.py", + "internal": false, + "lineCount": 2, + "moduleAutorunRequested": false, + "name": "ExternalProject.py", + "readOnly": true, + "source": "message = 'project relative'\n", + "sourcePath": "//scripts/external_project.py", + "sourceSha256": "1332a556fa4a8c0d55c6661931bcc7c3cd79a193bab5e34c8b060cf97fec6a96" + }, + { + "byteLength": 23, + "errorCode": "SCRIPT_SANDBOX_UNAVAILABLE", + "executionStatus": "BLOCKED", + "id": "text:InternalSafe.py", + "internal": true, + "lineCount": 3, + "moduleAutorunRequested": false, + "name": "InternalSafe.py", + "readOnly": true, + "source": "value = 7\nprint(value)\n", + "sourceSha256": "1676bea86b7d12f595d531c32286e26ad0ac7a249d1bd40744b01634977ae582" + }, + { + "byteLength": 37, + "errorCode": "SCRIPT_POLICY_DENIED", + "executionStatus": "BLOCKED", + "id": "text:ModuleAutorun.py", + "internal": true, + "lineCount": 3, + "moduleAutorunRequested": true, + "name": "ModuleAutorun.py", + "readOnly": true, + "source": "def register():\n return 'blocked'\n", + "sourceSha256": "4b1d1ffa97bf18cd834d1a2472cab16f24f1439143964d09d283b6cc043e17bd" + } + ] + }, + "savedBytes": 490191, + "exact": true, + "nextTask": "M13-01F" +} diff --git a/tests/golden/M13-01F/malicious-report.json b/tests/golden/M13-01F/malicious-report.json new file mode 100644 index 00000000..21545d29 --- /dev/null +++ b/tests/golden/M13-01F/malicious-report.json @@ -0,0 +1,37 @@ +{ + "fixture": { + "byteLength": 491160, + "name": "malicious-script.blend", + "sha256": "7b1921931afc5bb74a2b73e90b175017c583ea878cdbb66f131718b2d8cd23b5" + }, + "nextTask": "M13-02A", + "operation": "MALICIOUS_SCRIPT_FIXTURE", + "schemaVersion": 1, + "sources": [ + { + "expectedExecution": "BLOCKED", + "name": "DriverExploit.py", + "sourceSha256": "065c9f659ba12657ad0d5cc513aea1f589c6ed69feabb0f56dd786d2002b691d", + "useModule": false + }, + { + "expectedExecution": "BLOCKED", + "name": "EmbeddedModule.py", + "sourceSha256": "7bc119a7cbfb129a2fa48e3636b9e05e03326f73711942594f6a8ee28f8bbd3f", + "useModule": true + }, + { + "expectedExecution": "BLOCKED", + "name": "HandlerExploit.py", + "sourceSha256": "bc028c5143813ecc35384e0f366dcd4f42f7531f1e748713626f9710f5147373", + "useModule": false + }, + { + "expectedExecution": "BLOCKED", + "name": "MaliciousText.py", + "sourceSha256": "c852d3e669074d4951900312aed2625fdb5d9e106328257607a1e4cad43172eb", + "useModule": false + } + ], + "task": "M13-01F" +} diff --git a/tests/golden/M13-01F/manifest.json b/tests/golden/M13-01F/manifest.json new file mode 100644 index 00000000..2df0cdb2 --- /dev/null +++ b/tests/golden/M13-01F/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-01F", + "parentTask": "M13-01E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP_AND_CHROMIUM", + "operation": "MALICIOUS_SCRIPT_FIXTURE", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-01E/manifest.json", "sha256": "889a4e06f7e8c0ea55b3ca4baa9fe85dccbe97053e497a45e3d364ce2b70a7c6" }, + "generator": { "path": "tools/web/generate-malicious-script-fixture.py", "sha256": "013285befeb59de21a887cefd377adf8cf53ff1c785b28a9c87d29f76f092731" }, + "checker": { "path": "tools/web/check-malicious-script-fixture.mjs", "sha256": "36608da893ae59e2e03856a62866ea6e7c349ec4a63200f042b2329e5f61caa9" }, + "e2eTest": { "path": "web/tests/e2e/malicious-script.spec.ts", "sha256": "98fbde6728ddf55cce5262522197a5b4db1dfce618e52259bf0b0c9e0e9165f2" }, + "report": { "path": "tests/golden/M13-01F/malicious-report.json", "sha256": "a628b73411d6f9a761f659ae28867ea9b0c0df30d47668353278b70d4b44180c" }, + "fixture": { "path": "tests/files/web/m13_malicious_script_v1/malicious-script.blend", "sha256": "7b1921931afc5bb74a2b73e90b175017c583ea878cdbb66f131718b2d8cd23b5" } + }, + "nextTask": "M13-02A" +} diff --git a/tests/golden/M13-02A/manifest-budget-report.json b/tests/golden/M13-02A/manifest-budget-report.json new file mode 100644 index 00000000..792e8abe --- /dev/null +++ b/tests/golden/M13-02A/manifest-budget-report.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 1, + "task": "M13-02A", + "operation": "SCRIPT_MANIFEST_BUDGETS", + "accepted": { + "scriptCount": 2, + "canonicalEntryPath": "scripts/base.py", + "canonicalDependencyPath": "deps/base.py", + "totalSourceBytes": 256, + "module": false + }, + "denied": { + "count": { + "status": "BLOCKED", + "code": "SCRIPT_BUDGET_EXCEEDED" + }, + "totalBytes": { + "status": "BLOCKED", + "code": "SCRIPT_BUDGET_EXCEEDED" + }, + "module": { + "status": "BLOCKED", + "code": "SCRIPT_POLICY_DENIED" + }, + "path": { + "status": "BLOCKED", + "code": "SCRIPT_MANIFEST_INVALID" + }, + "dependency": { + "status": "BLOCKED", + "code": "SCRIPT_MANIFEST_INVALID" + }, + "permission": { + "status": "BLOCKED", + "code": "SCRIPT_POLICY_DENIED" + } + }, + "budgets": { + "maxScripts": 1024, + "maxSourceBytes": 1048576, + "maxDependencies": 128, + "maxPermissions": 64 + }, + "nextTask": "M13-02B" +} diff --git a/tests/golden/M13-02A/manifest.json b/tests/golden/M13-02A/manifest.json new file mode 100644 index 00000000..ccd4f04f --- /dev/null +++ b/tests/golden/M13-02A/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-02A", + "parentTask": "M13-01F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_MANIFEST_BUDGETS", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-01F/manifest.json", + "sha256": "9a0b7c4097b3755365a9fb92b4848e6ac2ddd36ee2c7e9df3cb8808ce247cf3d" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/scripting-manifest-budget-test.worker.ts", + "sha256": "8d8eae67fa6915f0337850e15247baf01f0abde0b3362fbb120f0b448f1a4cf5" + }, + "checker": { + "path": "tools/web/check-script-manifest-budgets.mjs", + "sha256": "b3457324d72ab233cd17a142ea19fac6a0d024dd95de7b8fb5d26810437fb0d2" + }, + "unit": { + "path": "web/tests/unit/script-manifest-budgets.test.mjs", + "sha256": "6ce7847a0b745ba4081e4332d012e0b7f86e4906c71c95bdeda8c39977a630ee" + }, + "e2e": { + "path": "web/tests/e2e/script-manifest-budgets.spec.ts", + "sha256": "1ef4fd4caaeb1fa3d832fc8881823d5284755372575eab186c751f408dc9314c" + }, + "report": { + "path": "tests/golden/M13-02A/manifest-budget-report.json", + "sha256": "860672fe844b7969ca376d4b2708771189d1767efa819f7b97667d8a26438d3a" + } + }, + "nextTask": "M13-02B" +} diff --git a/tests/golden/M13-02B/manifest-canonical-report.json b/tests/golden/M13-02B/manifest-canonical-report.json new file mode 100644 index 00000000..4a3db2b4 --- /dev/null +++ b/tests/golden/M13-02B/manifest-canonical-report.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": 1, + "task": "M13-02B", + "operation": "SCRIPT_MANIFEST_CANONICAL_SERIALIZATION", + "equalOrderVariants": true, + "canonical": { + "scriptOrder": [ + "alpha", + "beta", + "zeta" + ], + "alphaPermissions": [ + "READ_ASSET", + "SUBMIT_SERVER_JOB" + ], + "zetaDependencies": [ + "alpha", + "beta" + ], + "unknownFieldsDropped": true + }, + "signatureInput": { + "schemaVersion": 1, + "byteLength": 1923, + "sha256": "8dcb1c31e9ff0066f2a4e225af49f84daeb8b0d0e713e4525228ee015846480a", + "sourceByteLengthMutationChangesInput": true, + "schemaMutationRejected": true + }, + "nextTask": "M13-02C" +} diff --git a/tests/golden/M13-02B/manifest.json b/tests/golden/M13-02B/manifest.json new file mode 100644 index 00000000..e60b4a4e --- /dev/null +++ b/tests/golden/M13-02B/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-02B", + "parentTask": "M13-02A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_MANIFEST_CANONICAL_SERIALIZATION", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-02A/manifest.json", + "sha256": "7148e0387dadffdb55e94e80dc30cfd5cdd40ebe990d06378e90c376d36ebd51" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/scripting-manifest-canonical-test.worker.ts", + "sha256": "de27fdcd6d16e27a07806ac609f908edfdb7a93d653676174cdd5e06ffa394af" + }, + "checker": { + "path": "tools/web/check-script-manifest-canonical.mjs", + "sha256": "5ed344cab6428ae5538a450171ba40c73ff738666515492298e30aaed1394f56" + }, + "unit": { + "path": "web/tests/unit/script-manifest-canonical.test.mjs", + "sha256": "33eae0f3ad2ae7243c9ce2835ab8e42186f65f574ab682099766f1d0f4c481f6" + }, + "e2e": { + "path": "web/tests/e2e/script-manifest-canonical.spec.ts", + "sha256": "5342301165ae82a01b46f5fed0cc0ec34b9813a6ecbc8032900d90b89628e064" + }, + "report": { + "path": "tests/golden/M13-02B/manifest-canonical-report.json", + "sha256": "5f4bc0b098ea65de47f1255d30130376d74260e2b912bcd0e28354171fbd07df" + } + }, + "nextTask": "M13-02C" +} diff --git a/tests/golden/M13-02C/manifest.json b/tests/golden/M13-02C/manifest.json new file mode 100644 index 00000000..2eae5663 --- /dev/null +++ b/tests/golden/M13-02C/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-02C", + "parentTask": "M13-02B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_TRUST_POLICY", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-02B/manifest.json", + "sha256": "605c656780a206a0d3b81d06b0294108b488a62d2b709e886884a2973c6cb091" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-trust-policy-test.worker.ts", + "sha256": "c6e206b29e7cacc3e23e2ac12a3d523a9c0b16ff29759126dab13c8665547421" + }, + "checker": { + "path": "tools/web/check-script-trust-policy.mjs", + "sha256": "0e55ce20d142f2bcbc5ce9c6fb955ef2771d284dff636c79da3f59a33b8b0aeb" + }, + "unit": { + "path": "web/tests/unit/script-trust-policy.test.mjs", + "sha256": "aac3ff0a1c7ae27e35112614bedabc02bb248882ec9b0b52d65f9794d06830d8" + }, + "e2e": { + "path": "web/tests/e2e/script-trust-policy.spec.ts", + "sha256": "614091bda15367090bc78a6f465fb10d02d0aa08775b5088699b12e2490034d2" + }, + "report": { + "path": "tests/golden/M13-02C/trust-policy-report.json", + "sha256": "0f59f733920edbbe5cb799e5f50ff31d19e55ced98e7a890828f6337a237e4bd" + } + }, + "nextTask": "M13-02D" +} diff --git a/tests/golden/M13-02C/trust-policy-report.json b/tests/golden/M13-02C/trust-policy-report.json new file mode 100644 index 00000000..aa7dd5cd --- /dev/null +++ b/tests/golden/M13-02C/trust-policy-report.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "task": "M13-02C", + "operation": "SCRIPT_TRUST_POLICY", + "identity": { + "algorithm": "ED25519", + "publisher": "Team", + "activeKey": "key:new", + "predecessor": "key:old", + "predecessorStatus": "REVOKED" + }, + "timestampPolicy": { + "issuedAt": "2026-01-01T00:00:00.000Z", + "expiresAt": "2027-01-01T00:00:00.000Z", + "maxClockSkewMs": 300000 + }, + "decisions": { + "eligible": "ELIGIBLE", + "cryptographicVerification": "REQUIRED", + "revoked": "REVOKED", + "crossPublisher": "SCRIPT_POLICY_DENIED", + "policyExpired": "POLICY_EXPIRED" + }, + "policySha256": "6c272f0b6662656194fff46c71faf69a87e0eafa31091ec474bf96eb76948323", + "nextTask": "M13-02D" +} diff --git a/tests/golden/M13-02D/manifest.json b/tests/golden/M13-02D/manifest.json new file mode 100644 index 00000000..32409fdb --- /dev/null +++ b/tests/golden/M13-02D/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-02D", + "parentTask": "M13-02C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SIGNATURE_VERIFICATION", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-02C/manifest.json", + "sha256": "89745daca88371335f4bd56982791266461082066c6c1345056fbc697bb7e6a2" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-signature-test.worker.ts", + "sha256": "f4e8ff45d06efcfa9e634ef28e654241d70554ed05dfe9c3fecd9f6eef166ce0" + }, + "checker": { + "path": "tools/web/check-script-signature.mjs", + "sha256": "2d6e7b403dd6d401eb1a41978016b40bd1fb86120464e09f185de4445c177fa4" + }, + "unit": { + "path": "web/tests/unit/script-trust-policy.test.mjs", + "sha256": "aac3ff0a1c7ae27e35112614bedabc02bb248882ec9b0b52d65f9794d06830d8" + }, + "e2e": { + "path": "web/tests/e2e/script-signature.spec.ts", + "sha256": "8a882fc8fa2c0c4e2eab3660810cec0f60d2f7475f62a1bb406fc3b2bdf60882" + }, + "report": { + "path": "tests/golden/M13-02D/signature-report.json", + "sha256": "957daf8e5899714cc5ce253b1f0fb2b258f1cc966238fff31e2626f8f56feb3a" + } + }, + "nextTask": "M13-02E" +} diff --git a/tests/golden/M13-02D/signature-report.json b/tests/golden/M13-02D/signature-report.json new file mode 100644 index 00000000..d6e45abc --- /dev/null +++ b/tests/golden/M13-02D/signature-report.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "task": "M13-02D", + "operation": "SCRIPT_SIGNATURE_VERIFICATION", + "signer": { + "algorithm": "ED25519", + "keyId": "key:new", + "publisher": "Team", + "cryptographicVerification": "REQUIRED" + }, + "decisions": { + "verified": "SCRIPT_SIGNATURE_VERIFIED", + "sourceHashChanged": "SCRIPT_SIGNATURE_INVALID", + "signatureChanged": "SCRIPT_SIGNATURE_INVALID", + "revoked": "SCRIPT_POLICY_DENIED" + }, + "input": { + "verifiedSha256": "978efe254fc421028bc1c62e7fee809f7b08b60ea2928501d0b2cfb71bf59cd2", + "changedSha256": "5e7aed5aea6b3dcc635f8d1ba99099e7e27ddb3110f12afc7d40995986ba8cfb", + "sourceHashMutationChangesInput": true, + "signatureExcludedFromInput": true + }, + "nextTask": "M13-02E" +} diff --git a/tests/golden/M13-02E/manifest.json b/tests/golden/M13-02E/manifest.json new file mode 100644 index 00000000..ffbd63ec --- /dev/null +++ b/tests/golden/M13-02E/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-02E", + "parentTask": "M13-02D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_PERMISSION_MINIMIZATION", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-02D/manifest.json", + "sha256": "c6b79a77e4b7ffe5f4a9ba785a30305f9cf6e86caf89b38a4b303a86ab662a31" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-permission-policy-test.worker.ts", + "sha256": "5df074e676542475009a4980fecaf3fdde009d149de84b9ca146647fb6aa1c02" + }, + "checker": { + "path": "tools/web/check-script-permission-policy.mjs", + "sha256": "169ab096295e08c5a630ef09d814da6ce742efd7a7143ff9a17984b4ca20593a" + }, + "unit": { + "path": "web/tests/unit/script-permission-policy.test.mjs", + "sha256": "43bb6796616f3f92d71b51ef2a119a4e263864121a03356764e4b9c9241c4043" + }, + "e2e": { + "path": "web/tests/e2e/script-permission-policy.spec.ts", + "sha256": "dc13412a44f277bbe68315da0d38a2c4aa520d7489c81e7c908f9768c0547b7e" + }, + "report": { + "path": "tests/golden/M13-02E/permission-policy-report.json", + "sha256": "8dc41e75620ba5bcb08d0edc52c3994e4f4dbc37ad3633757634bf10fe97b252" + } + }, + "nextTask": "M13-02F" +} diff --git a/tests/golden/M13-02E/permission-policy-report.json b/tests/golden/M13-02E/permission-policy-report.json new file mode 100644 index 00000000..08ecd4a9 --- /dev/null +++ b/tests/golden/M13-02E/permission-policy-report.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "task": "M13-02E", + "operation": "SCRIPT_PERMISSION_MINIMIZATION", + "decisions": { + "defaultGrant": "ALLOWED", + "declaredGrant": "ALLOWED", + "escalation": "SCRIPT_POLICY_DENIED", + "unknownRequest": "SCRIPT_POLICY_DENIED", + "duplicateRequest": "SCRIPT_POLICY_DENIED", + "unknownDeclaration": "SCRIPT_POLICY_DENIED" + }, + "invariant": { + "undeclaredNeverGranted": true, + "defaultGrantedCount": 0, + "declaredGranted": [ + "READ_MAIN" + ] + }, + "nextTask": "M13-02F" +} diff --git a/tests/golden/M13-02F/manifest.json b/tests/golden/M13-02F/manifest.json new file mode 100644 index 00000000..4ca36f81 --- /dev/null +++ b/tests/golden/M13-02F/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-02F", + "parentTask": "M13-02E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SIGNATURE_NEGATIVE_CASES", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-02E/manifest.json", + "sha256": "691376d3cd33d3cd339b7195034abaf89cc02fba7e740c40ff051c4496400eef" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-signature-negative-test.worker.ts", + "sha256": "49b289110a6533fcd1a29ad78be00792fd3c0d251c6027bae0440f5daba929a1" + }, + "checker": { + "path": "tools/web/check-script-signature-negative.mjs", + "sha256": "35f2e6727aef5e6ea2e2623e2effa6ea0356faaf66d417976db7f5bbe671a38c" + }, + "unit": { + "path": "web/tests/unit/script-signature-negative.test.mjs", + "sha256": "8b9c8d63c97fce07299622b9ad261791a97bb91a6ed340e72c24c55e685978bd" + }, + "e2e": { + "path": "web/tests/e2e/script-signature-negative.spec.ts", + "sha256": "26c52ef67d6b1ea2994d92637826994e98e5234e042049f8d69a905f5cdb3309" + }, + "report": { + "path": "tests/golden/M13-02F/signature-negative-report.json", + "sha256": "d23a2ae5613b2442ad79420aa344ebfe3767d10ac19b3ea30a62afcccd3a167c" + } + }, + "nextTask": "M13-03A" +} diff --git a/tests/golden/M13-02F/signature-negative-report.json b/tests/golden/M13-02F/signature-negative-report.json new file mode 100644 index 00000000..7a47310f --- /dev/null +++ b/tests/golden/M13-02F/signature-negative-report.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "task": "M13-02F", + "operation": "SCRIPT_SIGNATURE_NEGATIVE_CASES", + "decisions": { + "missing": "SCRIPT_POLICY_DENIED", + "expired": "SCRIPT_POLICY_DENIED", + "notYetValid": "SCRIPT_POLICY_DENIED", + "publisherMismatch": "SCRIPT_POLICY_DENIED", + "swapped": "SCRIPT_SIGNATURE_INVALID" + }, + "invariants": { + "missingKeyDenied": true, + "expiredKeyDenied": true, + "notYetValidKeyDenied": true, + "publisherConfusionDenied": true, + "swappedSignatureDenied": true, + "noExecution": true + }, + "nextTask": "M13-03A" +} diff --git a/tests/golden/M13-03A/manifest.json b/tests/golden/M13-03A/manifest.json new file mode 100644 index 00000000..8f37c3e2 --- /dev/null +++ b/tests/golden/M13-03A/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-03A", + "parentTask": "M13-02F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SANDBOX_SCOPE", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-02F/manifest.json", + "sha256": "283673b21e6c9b89dc6ecb7007f3cecf28d53a84ec84f3f8b52373c055538a74" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-sandbox-scope-test.worker.ts", + "sha256": "27058fbb602791cbe6691fa14b3bc9c3d6cdea79641ba9c72fe18b6226d2a651" + }, + "checker": { + "path": "tools/web/check-script-sandbox-scope.mjs", + "sha256": "ad26ea3b078e480ba8f99cbf8dbd0d9b135c7605f4a3958d24845b7f5f8b0f43" + }, + "unit": { + "path": "web/tests/unit/script-sandbox-scope.test.mjs", + "sha256": "a15da2e645d2d681c7e9ac1380f6b224d196d1d2e78d20f1bacad01e138753d4" + }, + "e2e": { + "path": "web/tests/e2e/script-sandbox-scope.spec.ts", + "sha256": "49930747e6663f79b61093706318b72e59388d79e3e1cc105bc1571693326667" + }, + "report": { + "path": "tests/golden/M13-03A/sandbox-scope-report.json", + "sha256": "ba5784c751d5a347f38aa522ffcbed270d38fb474f0f10a2d06ea501d0234021" + } + }, + "nextTask": "M13-03B" +} diff --git a/tests/golden/M13-03A/sandbox-scope-report.json b/tests/golden/M13-03A/sandbox-scope-report.json new file mode 100644 index 00000000..d2a8c608 --- /dev/null +++ b/tests/golden/M13-03A/sandbox-scope-report.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "task": "M13-03A", + "operation": "SCRIPT_SANDBOX_SCOPE", + "accepted": { + "schemaVersion": 1, + "dom": false, + "hostWorker": false, + "opfs": false, + "indexedDB": false, + "network": false + }, + "blocked": { + "dom": "SCRIPT_POLICY_DENIED", + "hostWorker": "SCRIPT_POLICY_DENIED", + "opfs": "SCRIPT_POLICY_DENIED", + "indexedDB": "SCRIPT_POLICY_DENIED", + "network": "SCRIPT_POLICY_DENIED" + }, + "execution": "DISABLED", + "invariants": { + "noDom": true, + "noHostWorker": true, + "noOPFS": true, + "noIndexedDB": true, + "noNetwork": true + }, + "nextTask": "M13-03B" +} diff --git a/tests/golden/M13-03B/manifest.json b/tests/golden/M13-03B/manifest.json new file mode 100644 index 00000000..9ca7b9ce --- /dev/null +++ b/tests/golden/M13-03B/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-03B", + "parentTask": "M13-03A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SANDBOX_BUDGET", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-03A/manifest.json", + "sha256": "76a7ce0fd3a38fb770c9cedccbd05ed566b931caa1782fdae898d3b66c3a20bf" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-sandbox-budget-test.worker.ts", + "sha256": "ed836a032b33f54154fdd36f9f6e99ee43ca1755ea0dd8326474a13300ec965e" + }, + "checker": { + "path": "tools/web/check-script-sandbox-budget.mjs", + "sha256": "ab10d1d32c20022c848be4b33e1be846168a9ca4cf22ac932ea000001e9e2391" + }, + "unit": { + "path": "web/tests/unit/script-sandbox-budget.test.mjs", + "sha256": "caa40fc58a73aa4d433285c94009e5436a29fbcb6b4e18b74b27c49fd0b07490" + }, + "e2e": { + "path": "web/tests/e2e/script-sandbox-budget.spec.ts", + "sha256": "8be77fd55e76149bfe2e0e449d81a62707ebfa430514936808ec4805fc96e07c" + }, + "report": { + "path": "tests/golden/M13-03B/sandbox-budget-report.json", + "sha256": "4355710e07e95cbb0f96a82fdefca3607f363d3aea9ba89c332e8a4708b7bed2" + } + }, + "nextTask": "M13-03C" +} diff --git a/tests/golden/M13-03B/sandbox-budget-report.json b/tests/golden/M13-03B/sandbox-budget-report.json new file mode 100644 index 00000000..5d2de7cb --- /dev/null +++ b/tests/golden/M13-03B/sandbox-budget-report.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "task": "M13-03B", + "operation": "SCRIPT_SANDBOX_BUDGET", + "accepted": { + "schemaVersion": 1, + "cpuMs": 1000, + "wallMs": 5000, + "memoryBytes": 1048576, + "maxMessageBytes": 4096, + "maxOutputBytes": 8192 + }, + "blocked": { + "cpuMs": "SCRIPT_BUDGET_EXCEEDED", + "wallMs": "SCRIPT_BUDGET_EXCEEDED", + "memoryBytes": "SCRIPT_BUDGET_EXCEEDED", + "maxMessageBytes": "SCRIPT_BUDGET_EXCEEDED", + "maxOutputBytes": "SCRIPT_BUDGET_EXCEEDED" + }, + "execution": "DISABLED", + "invariants": { + "cpuBounded": true, + "wallBounded": true, + "memoryBounded": true, + "messageBounded": true, + "outputBounded": true + }, + "nextTask": "M13-03C" +} diff --git a/tests/golden/M13-03C/host-call-report.json b/tests/golden/M13-03C/host-call-report.json new file mode 100644 index 00000000..36e99ffb --- /dev/null +++ b/tests/golden/M13-03C/host-call-report.json @@ -0,0 +1,27 @@ +{ + "schemaVersion": 1, + "task": "M13-03C", + "operation": "SCRIPT_HOST_CALL_ALLOWLIST", + "acceptedCalls": [ + "READ_MAIN", + "READ_ASSET", + "WRITE_MAIN", + "WRITE_ASSET", + "SUBMIT_SERVER_JOB" + ], + "blocked": { + "unknown": "SCRIPT_POLICY_DENIED", + "permission": "SCRIPT_POLICY_DENIED", + "fields": "SCRIPT_MANIFEST_INVALID", + "path": "SCRIPT_MANIFEST_INVALID" + }, + "structuredParameters": true, + "execution": "DISABLED", + "invariants": { + "allowlistOnly": true, + "permissionBound": true, + "unknownFieldsRejected": true, + "projectPathsNormalized": true + }, + "nextTask": "M13-03D" +} diff --git a/tests/golden/M13-03C/manifest.json b/tests/golden/M13-03C/manifest.json new file mode 100644 index 00000000..95a58015 --- /dev/null +++ b/tests/golden/M13-03C/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-03C", + "parentTask": "M13-03B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_HOST_CALL_ALLOWLIST", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-03B/manifest.json", + "sha256": "b61978c22cdfffe81b812e2eea788d52326f9f0ebb4540e77f86dc9e19ff21dd" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-host-call-test.worker.ts", + "sha256": "b26a37f20e829172bb70fbe9b9e3a194923818127cccbe8bbc55f6d994be55e8" + }, + "checker": { + "path": "tools/web/check-script-host-call.mjs", + "sha256": "e90042f4eefd9e89da6d1cead87ca1cb9db9b3bd65ba8028593e093903577e2e" + }, + "unit": { + "path": "web/tests/unit/script-host-call.test.mjs", + "sha256": "e7679cd902c75504204d528343e4fa6be316d1c56c02b2871a67637aa847a1ec" + }, + "e2e": { + "path": "web/tests/e2e/script-host-call.spec.ts", + "sha256": "7b17a7da440aee07895101efb8e1eb13bfbebbe54e955bde09299f6b33d6aaf9" + }, + "report": { + "path": "tests/golden/M13-03C/host-call-report.json", + "sha256": "afb140a3aecff1de52c4363e7cd0ce476b0c9140ffdc016ec13a085049f6dd1a" + } + }, + "nextTask": "M13-03D" +} diff --git a/tests/golden/M13-03D/manifest.json b/tests/golden/M13-03D/manifest.json new file mode 100644 index 00000000..f15ee9cb --- /dev/null +++ b/tests/golden/M13-03D/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M13-03D", + "parentTask": "M13-03C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SANDBOX_ISOLATION", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-03C/manifest.json", + "sha256": "54318c45b11dd1707fecf78b0637257158102ea1dbb88d4d442e33b77df2cdbf" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-sandbox-isolation-test.worker.ts", + "sha256": "8c810ee7c8bd15d83ecfb4981068f947536bfe8c9e63b906861f42b8465722da" + }, + "checker": { + "path": "tools/web/check-script-sandbox-isolation.mjs", + "sha256": "e5c0c3c7cd500c269ae43a1a9eb05589e82aad28199ab4f11ad1167b486f6937" + }, + "unit": { + "path": "web/tests/unit/script-sandbox-isolation.test.mjs", + "sha256": "47986f93638fdc18b817b03222484f4691dc294185040eaebac8e1f386bc1549" + }, + "e2e": { + "path": "web/tests/e2e/script-sandbox-isolation.spec.ts", + "sha256": "2c16c42f09827a0c0100ef2cc6295ca6bc96933ed7475e08b1fa195d7940c141" + }, + "report": { + "path": "tests/golden/M13-03D/sandbox-isolation-report.json", + "sha256": "4af91cdb21101039b379136c7559b46df55f4f5ab5478c05c1c57cd6e1624d2a" + } + }, + "nextTask": "M13-03E" +} diff --git a/tests/golden/M13-03D/sandbox-isolation-report.json b/tests/golden/M13-03D/sandbox-isolation-report.json new file mode 100644 index 00000000..0d9cd227 --- /dev/null +++ b/tests/golden/M13-03D/sandbox-isolation-report.json @@ -0,0 +1,51 @@ +{ + "schemaVersion": 1, + "task": "M13-03D", + "operation": "SCRIPT_SANDBOX_ISOLATION", + "crash": { + "status": "CRASHED", + "errorCode": "SCRIPT_SANDBOX_CRASHED", + "workerGeneration": 4, + "mainRevisionBefore": 9, + "mainRevisionAfter": 9, + "temporaryBytes": 0, + "publishedResults": 0, + "lateResults": 0, + "committed": false, + "execution": "DISABLED" + }, + "timeout": { + "status": "TIMED_OUT", + "errorCode": "SCRIPT_SANDBOX_TIMEOUT", + "workerGeneration": 4, + "mainRevisionBefore": 9, + "mainRevisionAfter": 9, + "temporaryBytes": 0, + "publishedResults": 0, + "lateResults": 0, + "committed": false, + "execution": "DISABLED" + }, + "cancel": { + "status": "CANCELLED", + "errorCode": "SCRIPT_SANDBOX_CANCELLED", + "workerGeneration": 4, + "mainRevisionBefore": 9, + "mainRevisionAfter": 9, + "temporaryBytes": 0, + "publishedResults": 0, + "lateResults": 0, + "committed": false, + "execution": "DISABLED" + }, + "lateResult": "SCRIPT_SANDBOX_LATE_RESULT", + "execution": "DISABLED", + "invariants": { + "mainRevisionUnchanged": true, + "jobTerminated": true, + "temporaryResourcesReleased": true, + "noPublishedResults": true, + "lateResultsRejected": true + }, + "nextTask": "M13-03E" +} diff --git a/tests/golden/M13-03E/manifest.json b/tests/golden/M13-03E/manifest.json new file mode 100644 index 00000000..90a5311f --- /dev/null +++ b/tests/golden/M13-03E/manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "task": "M13-03E", + "parentTask": "M13-03D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SANDBOX_CANCELLATION_GATE", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-03D/manifest.json", + "sha256": "8066013f3d356ba438c36d1f1ea1cf692dbb4f60b086f552072e36b783a96d42" + }, + "protocol": { + "path": "web/protocol/scripting-platform.ts", + "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" + }, + "worker": { + "path": "web/app/src/workers/script-sandbox-cancellation-test.worker.ts", + "sha256": "0feb70c8c491cc24ebfe6c3e267b92522d616b6f260efcecfa57cf489d0742c0" + }, + "checker": { + "path": "tools/web/check-script-sandbox-cancellation.mjs", + "sha256": "b6fbe7102b7d0808673931c66797f8976e79b21bd4c32e429f21832c6090708e" + }, + "unit": { + "path": "web/tests/unit/script-sandbox-cancellation.test.mjs", + "sha256": "374cc87f66bd42226b750e387663ef8c86bef92348fa7cbc6ed7f0027945204d" + }, + "e2e": { + "path": "web/tests/e2e/script-sandbox-cancellation.spec.ts", + "sha256": "b233d9cafa1f70798ec19d76ca936abb610681522264a12237f532eb98e09fca" + }, + "package": { + "path": "web/package.json", + "sha256": "6499a70fc4a93c925053ddf5009c74d9c10754b443afc9d58e898fe20d7f1b05" + }, + "report": { + "path": "tests/golden/M13-03E/sandbox-cancellation-report.json", + "sha256": "066d9f19716b8229f2cf7755ec3009a3edef942ebab5f70159bec7f2afbaf71f" + } + }, + "nextTask": "M13-03F" +} diff --git a/tests/golden/M13-03E/sandbox-cancellation-report.json b/tests/golden/M13-03E/sandbox-cancellation-report.json new file mode 100644 index 00000000..2b7f9541 --- /dev/null +++ b/tests/golden/M13-03E/sandbox-cancellation-report.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "task": "M13-03E", + "operation": "SCRIPT_SANDBOX_CANCELLATION_GATE", + "receipt": { + "status": "CANCELLED", + "errorCode": "SCRIPT_SANDBOX_CANCELLED", + "workerGeneration": 5, + "mainRevisionBefore": 11, + "mainRevisionAfter": 11, + "temporaryBytes": 0, + "publishedResults": 0, + "lateResults": 0, + "committed": false, + "execution": "DISABLED" + }, + "lateResult": "SCRIPT_SANDBOX_LATE_RESULT", + "runtime": { + "lateMessages": 0, + "cacheWrites": 0, + "cancelled": true + }, + "invariants": { + "cancellationStable": true, + "mainRevisionUnchanged": true, + "noPublishedResults": true, + "noCacheWrites": true, + "lateResultsRejected": true + }, + "execution": "DISABLED", + "nextTask": "M13-03F" +} diff --git a/tests/golden/M13-03F/manifest.json b/tests/golden/M13-03F/manifest.json new file mode 100644 index 00000000..e87d5c30 --- /dev/null +++ b/tests/golden/M13-03F/manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "task": "M13-03F", + "parentTask": "M13-03E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SANDBOX_DISPOSE_GATE", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-03E/manifest.json", + "sha256": "0cdf625e6bd3ed7aca43318a3b04353cb806c51cfa4cf3c5dae9a65a0eae2e69" + }, + "protocol": { + "path": "web/app/src/testing/script-sandbox-dispose.ts", + "sha256": "f3d9f667c3809cfad0f52bc6028d93e36e25c4b639fb6081c9c933fbad0ebc0a" + }, + "worker": { + "path": "web/app/src/workers/script-sandbox-dispose-test.worker.ts", + "sha256": "2a074b05d301c4083e60534eb9452aabf5d95aea0ab316fa657441ef4bcd5c96" + }, + "checker": { + "path": "tools/web/check-script-sandbox-dispose.mjs", + "sha256": "6549bd10f588281d23c4bea705fd164252c9e3f44d8f4b93c893a2c410907135" + }, + "unit": { + "path": "web/tests/unit/script-sandbox-dispose.test.mjs", + "sha256": "6e00aa6286aae0eabc1345c04c7628dcc9acb32d51c1c27436a0e1b4c170b64c" + }, + "e2e": { + "path": "web/tests/e2e/script-sandbox-dispose.spec.ts", + "sha256": "b240d92c90e0d81272cd9cb3c0eb4e7d77102ce2e49ad05c761f763d1812b18e" + }, + "package": { + "path": "web/package.json", + "sha256": "0f06cd7ccdeed4213b6cb956aecf94e60dceff811ad8c3a2e239397685cfc1bf" + }, + "report": { + "path": "tests/golden/M13-03F/sandbox-dispose-report.json", + "sha256": "2dbd3e79939b500c0fc83216c51f258b88ead42a667ed1a6da755e893d7f73c6" + } + }, + "nextTask": "M13-03G" +} diff --git a/tests/golden/M13-03F/sandbox-dispose-report.json b/tests/golden/M13-03F/sandbox-dispose-report.json new file mode 100644 index 00000000..0fafadbd --- /dev/null +++ b/tests/golden/M13-03F/sandbox-dispose-report.json @@ -0,0 +1,76 @@ +{ + "schemaVersion": 1, + "task": "M13-03F", + "operation": "SCRIPT_SANDBOX_DISPOSE_GATE", + "runtime": "PRODUCTION_CHROMIUM_WORKER", + "resources": { + "before": { + "messagePorts": 2, + "timers": 1, + "abortControllers": 1, + "transferableBuffers": 1, + "pendingRequests": 1, + "cacheReferences": 1 + }, + "afterFirstDispose": { + "messagePorts": 0, + "timers": 0, + "abortControllers": 0, + "transferableBuffers": 0, + "pendingRequests": 0, + "cacheReferences": 0 + }, + "afterSecondDispose": { + "messagePorts": 0, + "timers": 0, + "abortControllers": 0, + "transferableBuffers": 0, + "pendingRequests": 0, + "cacheReferences": 0 + } + }, + "receipts": { + "first": { + "schemaVersion": 1, + "disposeCount": 1, + "idempotent": false, + "resources": { + "messagePorts": 0, + "timers": 0, + "abortControllers": 0, + "transferableBuffers": 0, + "pendingRequests": 0, + "cacheReferences": 0 + }, + "lateTimerMessages": 0 + }, + "second": { + "schemaVersion": 1, + "disposeCount": 2, + "idempotent": true, + "resources": { + "messagePorts": 0, + "timers": 0, + "abortControllers": 0, + "transferableBuffers": 0, + "pendingRequests": 0, + "cacheReferences": 0 + }, + "lateTimerMessages": 0 + } + }, + "lateTimerMessages": 0, + "invariants": { + "workerTerminated": true, + "messagePortsZero": true, + "timersZero": true, + "abortControllersZero": true, + "transferableBuffersZero": true, + "pendingRequestsZero": true, + "cacheReferencesZero": true, + "repeatedDisposeIdempotent": true, + "noLateTimerMessages": true + }, + "execution": "DISABLED", + "nextTask": "M13-03G" +} diff --git a/tests/golden/M13-03G/manifest.json b/tests/golden/M13-03G/manifest.json new file mode 100644 index 00000000..4282cda2 --- /dev/null +++ b/tests/golden/M13-03G/manifest.json @@ -0,0 +1,23 @@ +{ + "schemaVersion": 1, + "task": "M13-03G", + "parentTask": "M13-03F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "operation": "SCRIPT_SANDBOX_RECOVERY", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M13-03F/manifest.json", + "sha256": "96abc7c7613ef4c18c64b84ffb8420c39369f9e4733bd1fa4d7d9ee9e53d6f89" + }, + "protocol": { "path": "web/app/src/testing/script-sandbox-recovery.ts", "sha256": "8034faded657d49e1376ea42051bc302a090b485024a9ff3781e46aa82638b64" }, + "worker": { "path": "web/app/src/workers/script-sandbox-recovery-test.worker.ts", "sha256": "7b02331c375d4fb7dbadadd179f0ab9a0e336c8b95fabb071c06e67f3e7b3fe4" }, + "checker": { "path": "tools/web/check-script-sandbox-recovery.mjs", "sha256": "612015a3fca44bae0f0b78e622f044a5297ed8aa3a0efc774a40f0d47ddbdc9e" }, + "unit": { "path": "web/tests/unit/script-sandbox-recovery.test.mjs", "sha256": "69a2d34e38d591043ff62b2d305bd9aae684ecebd9ecba718580d77318931226" }, + "e2e": { "path": "web/tests/e2e/script-sandbox-recovery.spec.ts", "sha256": "73176e513f115ba917be74f62a5deb8fe2918fafa8a84c9294e80d2a86b8f1ed" }, + "package": { "path": "web/package.json", "sha256": "3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd" }, + "report": { "path": "tests/golden/M13-03G/sandbox-recovery-report.json", "sha256": "9057b3f49c3bb15cf53d638ada4bd2743949d3914173cd3799414489d584111d" } + }, + "nextTask": "M13-04A" +} diff --git a/tests/golden/M13-03G/sandbox-recovery-report.json b/tests/golden/M13-03G/sandbox-recovery-report.json new file mode 100644 index 00000000..5171adeb --- /dev/null +++ b/tests/golden/M13-03G/sandbox-recovery-report.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "task": "M13-03G", + "operation": "SCRIPT_SANDBOX_RECOVERY", + "runtime": "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", + "receipt": { + "schemaVersion": 1, + "operation": "SCRIPT_SANDBOX_RECOVERY", + "previousGeneration": 4, + "nextGeneration": 5, + "mainRevisionBefore": 11, + "mainRevisionAfter": 11, + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "manifestSha256": "90eddf56b5ee5a7a95ec365e0d0bfc81672922ce0d828f808e078d437dfe5435", + "audit": { + "entries": 2, + "first": { + "sequence": 1, + "requestId": "sandbox-recovery:g4", + "previousEntrySha256": null, + "entrySha256": "e7fddd846ecc5d7c89e05fd38fed84838fc7d40575ec69866707268aa4195ff0", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "manifestSha256": "90eddf56b5ee5a7a95ec365e0d0bfc81672922ce0d828f808e078d437dfe5435" + }, + "second": { + "sequence": 2, + "requestId": "sandbox-recovery:g5", + "previousEntrySha256": "e7fddd846ecc5d7c89e05fd38fed84838fc7d40575ec69866707268aa4195ff0", + "entrySha256": "bb7af3609c5eb1a5770d08f2beec42dad0c21428d4ca154ae4f8bd734f5e51de", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "manifestSha256": "90eddf56b5ee5a7a95ec365e0d0bfc81672922ce0d828f808e078d437dfe5435" + } + }, + "recovered": true, + "execution": "DISABLED" + }, + "audit": { + "entryCount": 2, + "firstEntrySha256": "e7fddd846ecc5d7c89e05fd38fed84838fc7d40575ec69866707268aa4195ff0", + "secondPreviousEntrySha256": "e7fddd846ecc5d7c89e05fd38fed84838fc7d40575ec69866707268aa4195ff0", + "secondEntrySha256": "bb7af3609c5eb1a5770d08f2beec42dad0c21428d4ca154ae4f8bd734f5e51de", + "requestIds": [ + "sandbox-recovery:g4", + "sandbox-recovery:g5" + ], + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "manifestSha256": "90eddf56b5ee5a7a95ec365e0d0bfc81672922ce0d828f808e078d437dfe5435" + }, + "negative": { + "replayError": "SCRIPT_MANIFEST_INVALID", + "tamperError": "SCRIPT_MANIFEST_INVALID" + }, + "invariants": { + "generationAdvancedOnce": true, + "mainRevisionUnchanged": true, + "sourceHashStable": true, + "manifestHashStable": true, + "sequenceContinuous": true, + "previousHashContinuous": true, + "requestIdsUnique": true, + "executionDisabled": true + }, + "execution": "DISABLED", + "nextTask": "M13-04A" +} diff --git a/tests/golden/M13-04A/manifest.json b/tests/golden/M13-04A/manifest.json new file mode 100644 index 00000000..9b6d634b --- /dev/null +++ b/tests/golden/M13-04A/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04A", + "parentTask": "M13-03G", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_SERVER_FILESYSTEM", + "operation": "SERVER_JOB_ONE_SHOT_DIRECTORY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-03G/manifest.json", "sha256": "5cd365eeebbd6a7f56f380af103b66f2beb11eb49cb6db6adb49972b572cf2ec" }, + "protocol": { "path": "tools/web/server-job-isolation.mjs", "sha256": "3aa5678a2c2769e4db5bb8f5c755b97e23045c4106e5636459748ab9b41929cd" }, + "checker": { "path": "tools/web/check-server-job-isolation.mjs", "sha256": "b8d0e741144f742946246370bd35820d806686fb00dc0b040c925cf16f2179da" }, + "unit": { "path": "web/tests/unit/server-job-isolation.test.mjs", "sha256": "b1ac3324332180f7b3522f135520e7b5dace334dc461c92f2bce48fbcba2e147" }, + "package": { "path": "web/package.json", "sha256": "3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd" }, + "report": { "path": "tests/golden/M13-04A/server-job-directory-report.json", "sha256": "07ed66fe0b2e1f1bbdba1cfb1089b052a5961d38f761bfcf79d8362980d2d502" } + }, + "nextTask": "M13-04B" +} diff --git a/tests/golden/M13-04A/server-job-directory-report.json b/tests/golden/M13-04A/server-job-directory-report.json new file mode 100644 index 00000000..3b94c135 --- /dev/null +++ b/tests/golden/M13-04A/server-job-directory-report.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "task": "M13-04A", + "operation": "SERVER_JOB_ONE_SHOT_DIRECTORY", + "runtime": "NODE_SERVER_FILESYSTEM", + "allocated": 2, + "cleaned": 2, + "uniqueDirectories": true, + "requestIdsNotInDirectoryNames": true, + "mode": "0700", + "noResidualDirectories": true, + "repeatedCleanupIdempotent": true, + "execution": "DISABLED", + "nextTask": "M13-04B" +} diff --git a/tests/golden/M13-04B/manifest.json b/tests/golden/M13-04B/manifest.json new file mode 100644 index 00000000..cc29bf09 --- /dev/null +++ b/tests/golden/M13-04B/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04B", + "parentTask": "M13-04A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_SERVER_FILESYSTEM", + "operation": "SERVER_JOB_SOURCE_READONLY_OUTPUT_ISOLATION", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04A/manifest.json", "sha256": "fd2407a7b9fefe67e2d77ed844e6c2ca17cdc4a746857a5825a38ff02ab664e3" }, + "protocol": { "path": "tools/web/server-job-isolation.mjs", "sha256": "3aa5678a2c2769e4db5bb8f5c755b97e23045c4106e5636459748ab9b41929cd" }, + "checker": { "path": "tools/web/check-server-job-workspace.mjs", "sha256": "9490c2eeb9980073fc1fe77fdba1fb6e4ef528de8eb666d9eaf1fb582ba658c2" }, + "unit": { "path": "web/tests/unit/server-job-isolation.test.mjs", "sha256": "b1ac3324332180f7b3522f135520e7b5dace334dc461c92f2bce48fbcba2e147" }, + "package": { "path": "web/package.json", "sha256": "3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd" }, + "report": { "path": "tests/golden/M13-04B/server-job-workspace-report.json", "sha256": "650a643cc92617d068cb96d8bd4303429ef169ea89e3d4cb63e3ce5e2428e6d2" } + }, + "nextTask": "M13-04C" +} diff --git a/tests/golden/M13-04B/server-job-workspace-report.json b/tests/golden/M13-04B/server-job-workspace-report.json new file mode 100644 index 00000000..92e6d102 --- /dev/null +++ b/tests/golden/M13-04B/server-job-workspace-report.json @@ -0,0 +1,15 @@ +{ + "schemaVersion": 1, + "task": "M13-04B", + "operation": "SERVER_JOB_SOURCE_READONLY_OUTPUT_ISOLATION", + "runtime": "NODE_SERVER_FILESYSTEM", + "sourceDirectoryMode": "0555", + "sourceFileMode": "0444", + "outputDirectoryMode": "0700", + "sourceWrite": "EACCES", + "outputWrite": "OK", + "sourceOutputDistinct": true, + "cleanupNoResidual": true, + "execution": "DISABLED", + "nextTask": "M13-04C" +} diff --git a/tests/golden/M13-04C/manifest.json b/tests/golden/M13-04C/manifest.json new file mode 100644 index 00000000..80a66b4a --- /dev/null +++ b/tests/golden/M13-04C/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04C", + "parentTask": "M13-04B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_SERVER_FILESYSTEM", + "operation": "SERVER_JOB_RESOURCE_BUDGET", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04B/manifest.json", "sha256": "9bc906b9e4c9751229f03271ef45a5925122b89b15bfe775591775f121ce1a4c" }, + "protocol": { "path": "tools/web/server-job-resource-budget.mjs", "sha256": "9746f0aef9dd76411320792dee1213c03755a3c03a87bbc4cbf88d4324c728d6" }, + "checker": { "path": "tools/web/check-server-job-resource-budget.mjs", "sha256": "5599f4e25fc3ceca18e04be322450a4dc5bb6ce9c72c3abe9f1afc9c81851ff8" }, + "unit": { "path": "web/tests/unit/server-job-resource-budget.test.mjs", "sha256": "05212b2aa639f4c37e37e1cac0597d9b367a1280e519240f073762394914dc63" }, + "package": { "path": "web/package.json", "sha256": "3df02b6049ac5d7185c23aef7764933ee460fedeb6595b3861272b0d49da5ecd" }, + "report": { "path": "tests/golden/M13-04C/server-job-resource-budget-report.json", "sha256": "76ba684966bf7e680b0ebfc630ae9e080f04dd6daa6dd744bbb22dcff43f8eb1" } + }, + "nextTask": "M13-04D" +} diff --git a/tests/golden/M13-04C/server-job-resource-budget-report.json b/tests/golden/M13-04C/server-job-resource-budget-report.json new file mode 100644 index 00000000..17a31350 --- /dev/null +++ b/tests/golden/M13-04C/server-job-resource-budget-report.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "task": "M13-04C", + "operation": "SERVER_JOB_RESOURCE_BUDGET", + "limits": { + "cpuMs": 60000, + "memoryBytes": 536870912, + "processCount": 1, + "fileCount": 1024, + "wallMs": 300000, + "outputBytes": 536870912 + }, + "accepted": { + "schemaVersion": 1, + "status": "SUCCEEDED", + "budget": { + "schemaVersion": 1, + "cpuMs": 60000, + "memoryBytes": 536870912, + "processCount": 1, + "fileCount": 1024, + "wallMs": 300000, + "outputBytes": 536870912 + }, + "usage": { + "cpuMs": 10, + "memoryBytes": 1024, + "processCount": 1, + "fileCount": 2, + "wallMs": 20, + "outputBytes": 512 + }, + "enforced": true, + "exceeded": [] + }, + "blocked": { + "cpuMs": "SERVER_JOB_BUDGET_EXCEEDED", + "memoryBytes": "SERVER_JOB_BUDGET_EXCEEDED", + "processCount": "SERVER_JOB_BUDGET_EXCEEDED", + "fileCount": "SERVER_JOB_BUDGET_EXCEEDED", + "wallMs": "SERVER_JOB_BUDGET_EXCEEDED", + "outputBytes": "SERVER_JOB_BUDGET_EXCEEDED" + }, + "invariants": { + "cpuBounded": true, + "memoryBounded": true, + "processBounded": true, + "fileBounded": true, + "wallBounded": true, + "outputBounded": true, + "executionDisabled": true + }, + "execution": "DISABLED", + "nextTask": "M13-04D" +} diff --git a/tests/golden/M13-04D/manifest.json b/tests/golden/M13-04D/manifest.json new file mode 100644 index 00000000..49bb6920 --- /dev/null +++ b/tests/golden/M13-04D/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04D", + "parentTask": "M13-04C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_SERVER_FILESYSTEM", + "operation": "SERVER_JOB_NETWORK_POLICY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04C/manifest.json", "sha256": "d00fff709b57909ec418ff0e476dca97281fc39456f674c63b51248377fc6ab2" }, + "protocol": { "path": "tools/web/server-job-network-policy.mjs", "sha256": "0dc3698383fbfa511bebfeca41504c7c04a8302d47168cfae94f01f2a11a316c" }, + "checker": { "path": "tools/web/check-server-job-network-policy.mjs", "sha256": "6f3e9cd9f988313863345f392036e6dbb522102c969197034800f419e497ad92" }, + "unit": { "path": "web/tests/unit/server-job-network-policy.test.mjs", "sha256": "c0ba10ccd2fb6ec8878a1b4a54bc6a12e49d35818566e0d55ef307779e8c8b77" }, + "package": { "path": "web/package.json", "sha256": "92547295bdc2f3fc8b691dac4fbf20c50aa91e8bb05a9d5b471fdeedb8974100" }, + "report": { "path": "tests/golden/M13-04D/server-job-network-policy-report.json", "sha256": "37bcba2666e8c76c2e07d23e285737004affbb0372ec6dd84bb6802dba551527" } + }, + "nextTask": "M13-04E" +} diff --git a/tests/golden/M13-04D/server-job-network-policy-report.json b/tests/golden/M13-04D/server-job-network-policy-report.json new file mode 100644 index 00000000..a6b1a8ad --- /dev/null +++ b/tests/golden/M13-04D/server-job-network-policy-report.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "task": "M13-04D", + "operation": "SERVER_JOB_NETWORK_POLICY", + "defaultNetwork": "DENY", + "declaredOrigin": { + "status": "ALLOWED", + "code": "SERVER_NETWORK_ALLOWED_ORIGIN", + "origin": "https://example.com", + "network": "DECLARED_ORIGIN" + }, + "denied": { + "missing": { + "status": "DENIED", + "code": "SERVER_NETWORK_DENIED", + "origin": null, + "network": "DISABLED" + }, + "undeclared": { + "status": "DENIED", + "code": "SERVER_NETWORK_DENIED", + "origin": "https://other.example", + "network": "DISABLED" + } + }, + "execution": "DISABLED", + "nextTask": "M13-04E" +} diff --git a/tests/golden/M13-04E/manifest.json b/tests/golden/M13-04E/manifest.json new file mode 100644 index 00000000..8819be27 --- /dev/null +++ b/tests/golden/M13-04E/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M13-04E", + "parentTask": "M13-04D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PINNED_BLENDER_5_2_BACKGROUND", + "operation": "SERVER_JOB_BLENDER_STARTUP", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04D/manifest.json", "sha256": "11ce246597e3321b346ed4fed00edba22708d97250163d332d7ba8a003d8849a" }, + "startup": { "path": "tools/web/server-job-startup.py", "sha256": "c8c5b5a7a7880d4df9477a94a98e54e328baf582ade649807067f988484e1f12" }, + "checker": { "path": "tools/web/check-server-job-startup.mjs", "sha256": "66b2e25efe0e7455348e8f488cd0f6ebef07f916fcf6541680e7d18e4f69ab10" }, + "package": { "path": "web/package.json", "sha256": "83889e43d03a791ee26e0aeabaa4c76044f6ee8f3f0ed69969681e2f63735e57" }, + "report": { "path": "tests/golden/M13-04E/server-job-startup-report.json", "sha256": "c67d2972e584782d03479dbdb36dae0976074978f4ce9a48e41e7ad5aea66089" } + }, + "nextTask": "M13-04F" +} diff --git a/tests/golden/M13-04E/server-job-startup-report.json b/tests/golden/M13-04E/server-job-startup-report.json new file mode 100644 index 00000000..7087c59c --- /dev/null +++ b/tests/golden/M13-04E/server-job-startup-report.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": 1, + "task": "M13-04E", + "operation": "SERVER_JOB_BLENDER_STARTUP", + "blender": "/home/mes123456/workinf_Blender_Wasm/build_blender_5.2.0/bin/blender", + "argv": [ + "--background", + "--factory-startup", + "--python", + "/home/mes123456/workinf_Blender_Wasm/tools/web/server-job-startup.py", + "--", + "SERVER_JOB_STARTUP_V1" + ], + "receipt": { + "argv": [ + "SERVER_JOB_STARTUP_V1" + ], + "background": true, + "runtime": "BLENDER_BACKGROUND_FACTORY_STARTUP", + "schemaVersion": 1, + "version": "5.2.0 LTS" + }, + "factoryStartup": true, + "background": true, + "userPrefsDisabled": true, + "fixedStartupScript": true, + "execution": "DISABLED", + "nextTask": "M13-04F" +} diff --git a/tests/golden/M13-04F/manifest.json b/tests/golden/M13-04F/manifest.json new file mode 100644 index 00000000..66365d86 --- /dev/null +++ b/tests/golden/M13-04F/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04F", + "parentTask": "M13-04E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_SERVER_OUTPUT_RECEIPT", + "operation": "SERVER_JOB_OUTPUT_REDACTION", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04E/manifest.json", "sha256": "a24ea87bcf305bc15d8ae70b2abd03aa16ffbd6b127d5e9da9da23617d7201cc" }, + "protocol": { "path": "tools/web/server-job-output.mjs", "sha256": "5a58e7080324b399905c6a20a313225153ba94c75a55d21c4bbdd957ae4a2462" }, + "checker": { "path": "tools/web/check-server-job-output-redaction.mjs", "sha256": "5c2db9d3313484bb68ac65822828467b3f99cb03e8374935757cfcb4b4ed908d" }, + "unit": { "path": "web/tests/unit/server-job-output.test.mjs", "sha256": "4ae6266bce22f84e73113b93a590a0c32d9452e748003af251a602024d0c6db5" }, + "package": { "path": "web/package.json", "sha256": "52781eb2650133b43271889a22ce38023e611641da15411d31698c6cfc0ce19c" }, + "report": { "path": "tests/golden/M13-04F/server-job-output-report.json", "sha256": "cb6e3ed8d6cc0a69b333b6bff3199593bdfbae14fa7fa700dcb12e9e9cea519a" } + }, + "nextTask": "M13-04G" +} diff --git a/tests/golden/M13-04F/server-job-output-report.json b/tests/golden/M13-04F/server-job-output-report.json new file mode 100644 index 00000000..971f8c94 --- /dev/null +++ b/tests/golden/M13-04F/server-job-output-report.json @@ -0,0 +1,70 @@ +{ + "schemaVersion": 1, + "task": "M13-04F", + "operation": "SERVER_JOB_OUTPUT_REDACTION", + "limits": { + "stdoutBytes": 65536, + "stderrBytes": 65536, + "totalBytes": 131072 + }, + "normal": { + "schemaVersion": 1, + "stdout": { + "text": "Blender 5.2 background\n", + "originalBytes": 23, + "emittedBytes": 23, + "redactionCount": 0, + "truncated": false + }, + "stderr": { + "text": "", + "originalBytes": 0, + "emittedBytes": 0, + "redactionCount": 0, + "truncated": false + }, + "limits": { + "stdoutBytes": 65536, + "stderrBytes": 65536, + "totalBytes": 131072 + }, + "totalOriginalBytes": 23, + "totalEmittedBytes": 23, + "totalRedactions": 0, + "totalTruncated": false, + "execution": "DISABLED" + }, + "oversized": { + "schemaVersion": 1, + "stdout": { + "text": "INFO source= authorization: \nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\n", + "originalBytes": 80094, + "emittedBytes": 65536, + "redactionCount": 4, + "truncated": true + }, + "stderr": { + "text": "ERROR \nyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\n", + "originalBytes": 80062, + "emittedBytes": 65536, + "redactionCount": 2, + "truncated": true + }, + "limits": { + "stdoutBytes": 65536, + "stderrBytes": 65536, + "totalBytes": 131072 + }, + "totalOriginalBytes": 160156, + "totalEmittedBytes": 131072, + "totalRedactions": 6, + "totalTruncated": true, + "execution": "DISABLED" + }, + "negative": { + "missingOutput": 0, + "invalidBudget": "SERVER_JOB_OUTPUT_INVALID" + }, + "execution": "DISABLED", + "nextTask": "M13-04G" +} diff --git a/tests/golden/M13-04G/manifest.json b/tests/golden/M13-04G/manifest.json new file mode 100644 index 00000000..3a5c1fa6 --- /dev/null +++ b/tests/golden/M13-04G/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04G", + "parentTask": "M13-04F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_REAL_PROCESS_GROUP", + "operation": "SERVER_JOB_PROCESS_TREE_CANCELLATION", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04F/manifest.json", "sha256": "b5b4b26183dc48ebcae009e49999010d9b60d512f5daa4674a0aff0577d6602c" }, + "protocol": { "path": "tools/web/server-job-process.mjs", "sha256": "74fbbac806f34904bc9cb8e40eaf386cb60d44d978973950f1809262d311ec95" }, + "checker": { "path": "tools/web/check-server-job-cancellation.mjs", "sha256": "751ecafc642595d0de69b618e223cbff49d414f2958fc371371e4a668d66a8cc" }, + "unit": { "path": "web/tests/unit/server-job-process.test.mjs", "sha256": "39ac445ff51a9bfe249363204cf2edca906b16bf67ab1f8321166b440eba8629" }, + "package": { "path": "web/package.json", "sha256": "ca092a9a1d48ac41dc20f978bb26c8268adde9f7602a06459c7df8c2cb2d4cc7" }, + "report": { "path": "tests/golden/M13-04G/server-job-cancellation-report.json", "sha256": "61f5c3ad315f94bac48e58627fcc46015c173dd9f303d5914acb8dbbe93caa97" } + }, + "nextTask": "M13-04H" +} diff --git a/tests/golden/M13-04G/server-job-cancellation-report.json b/tests/golden/M13-04G/server-job-cancellation-report.json new file mode 100644 index 00000000..c044d7ff --- /dev/null +++ b/tests/golden/M13-04G/server-job-cancellation-report.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "task": "M13-04G", + "operation": "SERVER_JOB_PROCESS_TREE_CANCELLATION", + "runtime": "NODE_REAL_PROCESS_GROUP", + "state": "CANCELLED", + "treeSignal": "SIGTERM_GROUP", + "cleanupCount": 1, + "orphanCount": 0, + "residualDirectory": false, + "repeatedCancelIdempotent": true, + "execution": "DISABLED", + "nextTask": "M13-04H" +} diff --git a/tests/golden/M13-04H/manifest.json b/tests/golden/M13-04H/manifest.json new file mode 100644 index 00000000..762fe426 --- /dev/null +++ b/tests/golden/M13-04H/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04H", + "parentTask": "M13-04G", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_REAL_PROCESS_AND_BOUNDED_FAULTS", + "operation": "SERVER_JOB_FAULT_CODE_MAPPING", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04G/manifest.json", "sha256": "7103a25ec7eb4a1c7f0d9f7acfb5ed30da32c87d512d6f9d53e081db275f7b89" }, + "protocol": { "path": "tools/web/server-job-fault.mjs", "sha256": "858360b4468a7b6cf796fc83a1627ba18626b61a0799f83531de996ea374519b" }, + "checker": { "path": "tools/web/check-server-job-fault-codes.mjs", "sha256": "cb67246ef3df111525ca5e14af0546154ea2beee930b9840ad4ac7e2ebbb91ec" }, + "unit": { "path": "web/tests/unit/server-job-fault.test.mjs", "sha256": "3551e28d78e5069c3d97eabcacac824bf142f54fdcb3ad4904668909af1cc545" }, + "package": { "path": "web/package.json", "sha256": "77bae66fd3885e36d2ead6f261754724144e5f51fafc5ccad9863dfc8e5ab548" }, + "report": { "path": "tests/golden/M13-04H/server-job-fault-report.json", "sha256": "01999232f926496909fc9ef072e19aadb688822d3f9d7e03b42871ac5d54a0f6" } + }, + "nextTask": "M13-04I" +} diff --git a/tests/golden/M13-04H/server-job-fault-report.json b/tests/golden/M13-04H/server-job-fault-report.json new file mode 100644 index 00000000..cd8d2c16 --- /dev/null +++ b/tests/golden/M13-04H/server-job-fault-report.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "task": "M13-04H", + "operation": "SERVER_JOB_FAULT_CODE_MAPPING", + "runtime": "NODE_REAL_PROCESS_AND_BOUNDED_FAULTS", + "receipts": { + "timeout": { + "schemaVersion": 1, + "state": "FAILED", + "code": "SERVER_JOB_TIMEOUT", + "stage": "PROCESS", + "exitCode": null, + "signal": null, + "timedOut": true, + "memoryExceeded": false, + "baseRevision": 11, + "currentRevision": 11, + "committedRevision": 11, + "publish": false, + "revisionPreserved": true, + "execution": "DISABLED" + }, + "oom": { + "schemaVersion": 1, + "state": "FAILED", + "code": "SERVER_JOB_OOM", + "stage": "PROCESS", + "exitCode": null, + "signal": null, + "timedOut": false, + "memoryExceeded": true, + "baseRevision": 11, + "currentRevision": 11, + "committedRevision": 11, + "publish": false, + "revisionPreserved": true, + "execution": "DISABLED" + }, + "signal": { + "schemaVersion": 1, + "state": "FAILED", + "code": "SERVER_JOB_SIGNAL", + "stage": "PROCESS", + "exitCode": null, + "signal": "SIGTERM", + "timedOut": false, + "memoryExceeded": false, + "baseRevision": 11, + "currentRevision": 11, + "committedRevision": 11, + "publish": false, + "revisionPreserved": true, + "execution": "DISABLED" + }, + "exit": { + "schemaVersion": 1, + "state": "FAILED", + "code": "SERVER_JOB_EXIT_FAILED", + "stage": "PROCESS", + "exitCode": 7, + "signal": null, + "timedOut": false, + "memoryExceeded": false, + "baseRevision": 11, + "currentRevision": 11, + "committedRevision": 11, + "publish": false, + "revisionPreserved": true, + "execution": "DISABLED" + } + }, + "invariants": { + "oldRevisionPreserved": true, + "failurePublish": false, + "cleanupRequired": true, + "executionDisabled": true + }, + "execution": "DISABLED", + "nextTask": "M13-04I" +} diff --git a/tests/golden/M13-04I/manifest.json b/tests/golden/M13-04I/manifest.json new file mode 100644 index 00000000..5ad57dce --- /dev/null +++ b/tests/golden/M13-04I/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04I", + "parentTask": "M13-04H", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_ATOMIC_RESULT_WORKSPACE", + "operation": "SERVER_JOB_RESULT_HASH_BINDING", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04H/manifest.json", "sha256": "ba530ddff6e6fdd84057444ce757fad05a30d825a5915abebe2d5910a689af7f" }, + "protocol": { "path": "tools/web/server-job-result-binding.mjs", "sha256": "ecf3bddef53e2766dcc055ec027b04c14e3d884132c7fdea476663617364ad86" }, + "checker": { "path": "tools/web/check-server-job-result-binding.mjs", "sha256": "b0fbffd2937ab20e72d337bb5ff63ccf188ccf8a939d8ccc8b4878d80c9a4a19" }, + "unit": { "path": "web/tests/unit/server-job-result-binding.test.mjs", "sha256": "c5538798aa6263b8a1c20270a019f8b4fb8b176b141a6411b72aad21a457bfd0" }, + "package": { "path": "web/package.json", "sha256": "5d269d4e5ff4385d919c0ee33f996dcb814e3e415da25dbf3b4dbdc3312fbe24" }, + "report": { "path": "tests/golden/M13-04I/server-job-result-report.json", "sha256": "ec63faeba35a641a22c3b3a69757bd37e8e9acaa1dda11a64faf7bed80439784" } + }, + "nextTask": "M13-04J" +} diff --git a/tests/golden/M13-04I/server-job-result-report.json b/tests/golden/M13-04I/server-job-result-report.json new file mode 100644 index 00000000..4c9b83c4 --- /dev/null +++ b/tests/golden/M13-04I/server-job-result-report.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M13-04I", + "operation": "SERVER_JOB_RESULT_HASH_BINDING", + "identityHashes": { + "source": "41cf6794ba4200b839c53531555f0f3998df4cbb01a4d5cb0b94e3ca5e23947d", + "settings": "cde0fb0dec1400c54a0f7e7eafa73624c53e4da258bbd34b3380a0defeba95c1", + "build": "b1a7a669e8007748b017f3010e59b76376c823e832a863ce57f025adec26beb8" + }, + "outputSha256": "6ff2c765a84cd1cb50960c12d9c436bac1260375f05fa967e2903197f66c4220", + "outputByteLength": 4, + "verified": true, + "tamperAndQuotaBlocked": true, + "atomicTarget": true, + "execution": "DISABLED", + "nextTask": "M13-04J" +} diff --git a/tests/golden/M13-04J/manifest.json b/tests/golden/M13-04J/manifest.json new file mode 100644 index 00000000..1367f5e9 --- /dev/null +++ b/tests/golden/M13-04J/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-04J", + "parentTask": "M13-04I", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_ATOMIC_RECEIPT_STORE", + "operation": "SERVER_JOB_REQUEST_IDEMPOTENCY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04I/manifest.json", "sha256": "f9658954d3dbd0cd1edd696c10f6a1b880bb3288c91a39a47ca84c4496046a97" }, + "protocol": { "path": "tools/web/server-job-idempotency.mjs", "sha256": "15eca7550964c9985e26a22a7900d3f3aa69fdef35fe5962b48a8f55ebba5863" }, + "checker": { "path": "tools/web/check-server-job-idempotency.mjs", "sha256": "02247ffda07c095373642a7fa22268bb9f5ac181d41571f7fff685812976d3c4" }, + "unit": { "path": "web/tests/unit/server-job-idempotency.test.mjs", "sha256": "6ee08e67cd8493e856f67309bd6562316eca42ef52dd4a9aad3a15efdc4b6b41" }, + "package": { "path": "web/package.json", "sha256": "5b47e94cf828fc9d3021019eed9922eb67815aba5416266d8c967c7cb5bd96ba" }, + "report": { "path": "tests/golden/M13-04J/server-job-idempotency-report.json", "sha256": "31f95a8cc1c4792303986b8b5987ab02fc11a77ab25fd76b31c02d1ed1b104d1" } + }, + "nextTask": "M13-05A" +} diff --git a/tests/golden/M13-04J/server-job-idempotency-report.json b/tests/golden/M13-04J/server-job-idempotency-report.json new file mode 100644 index 00000000..6f87acab --- /dev/null +++ b/tests/golden/M13-04J/server-job-idempotency-report.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "task": "M13-04J", + "operation": "SERVER_JOB_REQUEST_IDEMPOTENCY", + "runtime": "NODE_ATOMIC_RECEIPT_STORE", + "firstCommitReused": false, + "exactRetryReused": true, + "conflictBlocked": true, + "differentRequestIsolated": true, + "concurrentReuse": [ + false, + true + ], + "execution": "DISABLED", + "nextTask": "M13-05A" +} diff --git a/tests/golden/M13-05A/csp-report.json b/tests/golden/M13-05A/csp-report.json new file mode 100644 index 00000000..55aeeea8 --- /dev/null +++ b/tests/golden/M13-05A/csp-report.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M13-05A", + "operation": "CSP_POLICY", + "policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'", + "sourceChecks": { + "inlineScript": "DENY", + "inlineHandler": "DENY", + "eval": "DENY", + "dataScript": "DENY", + "undeclaredConnect": "DENY" + }, + "responseCount": 3, + "buildChecked": true, + "execution": "DISABLED", + "nextTask": "M13-05B" +} diff --git a/tests/golden/M13-05A/manifest.json b/tests/golden/M13-05A/manifest.json new file mode 100644 index 00000000..181b0bbe --- /dev/null +++ b/tests/golden/M13-05A/manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "task": "M13-05A", + "parentTask": "M13-04J", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_DEPLOYMENT_HTTP", + "operation": "CSP_POLICY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-04J/manifest.json", "sha256": "d230ae49dbf38cd9e95e7379a1a49332e478d3d23b3638730d2ec6167204c368" }, + "contract": { "path": "docs/web/deployment-contract.json", "sha256": "e384c73ee6deac8ec50fd496f97d16b20d992da25a5fba45138997f7b3677b7c" }, + "server": { "path": "tools/web/deployment-http-server.mjs", "sha256": "a5734ce9760f65cf5bade8cc209454d39fa963ad7dfaf20653728a9d7a57b04c" }, + "contractChecker": { "path": "tools/web/check-deployment-contract.mjs", "sha256": "9758f8bedd6c4faec3c8ae2cd361db754b6f02ac3b01872495abf00b90236b87" }, + "checker": { "path": "tools/web/check-csp-policy.mjs", "sha256": "f259f6d9a9432ef84713bd19f859717c0fb2c3e572672dcafdc3c141f3d4f6e0" }, + "index": { "path": "web/app/index.html", "sha256": "16b7691b191127f84c5143e23aecdfc203dc9e279d34cb270b37631fcbd5f856" }, + "vite": { "path": "web/app/vite.config.ts", "sha256": "fd160c685ed780738e2b37ce9a32e5d4718e11a2875e12d86ddad310213abaf3" }, + "package": { "path": "web/package.json", "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162" }, + "report": { "path": "tests/golden/M13-05A/csp-report.json", "sha256": "2021b5194671920732d40812dc8c058c4349ad8cd14bd1123ca1853fb638df68" } + }, + "nextTask": "M13-05B" +} diff --git a/tests/golden/M13-05B/csp-resource-report.json b/tests/golden/M13-05B/csp-resource-report.json new file mode 100644 index 00000000..2e924494 --- /dev/null +++ b/tests/golden/M13-05B/csp-resource-report.json @@ -0,0 +1,47 @@ +{ + "schemaVersion": 1, + "task": "M13-05B", + "operation": "CSP_RESOURCE_POLICY", + "policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'", + "resources": { + "worker": "SAME_ORIGIN", + "wasm": "SAME_ORIGIN_WASM_UNSAFE_EVAL", + "font": "SAME_ORIGIN", + "image": "SAME_ORIGIN", + "media": "SAME_ORIGIN" + }, + "denied": { + "dataImage": "DENY", + "blobWorker": "DENY", + "crossOriginConnect": "DENY" + }, + "responseCount": 7, + "browser": { + "result": { + "worker": true, + "wasm": true, + "image": true, + "font": true, + "media": true, + "dataImageBlocked": true, + "blobWorkerBlocked": true, + "crossOriginBlocked": true + }, + "violations": [ + { + "directive": "worker-src", + "blockedURI": "blob" + }, + { + "directive": "connect-src", + "blockedURI": "http://127.0.0.1:9/csp-cross-origin" + }, + { + "directive": "img-src", + "blockedURI": "data" + } + ] + }, + "execution": "DISABLED", + "nextTask": "M13-05C" +} diff --git a/tests/golden/M13-05B/manifest.json b/tests/golden/M13-05B/manifest.json new file mode 100644 index 00000000..21fb90cb --- /dev/null +++ b/tests/golden/M13-05B/manifest.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "task": "M13-05B", + "parentTask": "M13-05A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_CHROMIUM_DEPLOYMENT_HTTP", + "operation": "CSP_RESOURCE_POLICY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05A/manifest.json", "sha256": "6a53b75ad4178707f1e73152d3f80edac489a34ba440aaca664e42547c424150" }, + "contract": { "path": "docs/web/deployment-contract.json", "sha256": "e384c73ee6deac8ec50fd496f97d16b20d992da25a5fba45138997f7b3677b7c" }, + "server": { "path": "tools/web/deployment-http-server.mjs", "sha256": "a5734ce9760f65cf5bade8cc209454d39fa963ad7dfaf20653728a9d7a57b04c" }, + "contractChecker": { "path": "tools/web/check-deployment-contract.mjs", "sha256": "9758f8bedd6c4faec3c8ae2cd361db754b6f02ac3b01872495abf00b90236b87" }, + "checker": { "path": "tools/web/check-csp-resource-policy.mjs", "sha256": "ad68d11d15ebe5bc11df18d495c784058c9b15d38ef28bac756bd943059bbd3c" }, + "package": { "path": "web/package.json", "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162" }, + "report": { "path": "tests/golden/M13-05B/csp-resource-report.json", "sha256": "b0f50e5e8924eb0ec0475a39ed0833023037e3e2cea36bb8ecf22710e97591df" } + }, + "nextTask": "M13-05C" +} diff --git a/tests/golden/M13-05C/dependency-inventory.json b/tests/golden/M13-05C/dependency-inventory.json new file mode 100644 index 00000000..04308e08 --- /dev/null +++ b/tests/golden/M13-05C/dependency-inventory.json @@ -0,0 +1,1862 @@ +{ + "schemaVersion": 1, + "task": "M13-05C", + "operation": "NPM_DEPENDENCY_INVENTORY", + "package": { + "path": "web/package.json", + "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162", + "name": "blender-web-editor", + "version": "0.1.0-rc.1", + "lockfileVersion": 3 + }, + "lockfile": { + "path": "web/package-lock.json", + "sha256": "61f6d13e0148321d3c625a5baa212b5444296d111193c4d62a692685faebff1f", + "packageCount": 144 + }, + "roots": { + "production": [ + "react", + "react-dom" + ], + "build": [ + "@eslint/js", + "@types/react", + "@types/react-dom", + "@types/three", + "@vitejs/plugin-react", + "eslint", + "typescript", + "typescript-eslint", + "vite" + ], + "test": [ + "@axe-core/playwright", + "@playwright/test" + ] + }, + "categoryCounts": { + "production": 3, + "build": 135, + "test": 6 + }, + "sharedCount": 0, + "packages": [ + { + "path": "node_modules/@axe-core/playwright", + "name": "@axe-core/playwright", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.12.1.tgz", + "integrity": "sha512-rMd7xriptqKpP+w5265i4Hdkv2X5kbu6uiBi/B2I7uf3hieRBM3qDCfaKPtxfiYb2mKXfF+yLODJwIx+Jv1GDw==", + "categories": [ + "test" + ], + "dependencies": [ + "axe-core" + ] + }, + { + "path": "node_modules/@dimforge/rapier3d-compat", + "name": "@dimforge/rapier3d-compat", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz", + "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@eslint-community/eslint-utils", + "name": "@eslint-community/eslint-utils", + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "categories": [ + "build" + ], + "dependencies": [ + "eslint-visitor-keys" + ] + }, + { + "path": "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys", + "name": "eslint-visitor-keys", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@eslint-community/regexpp", + "name": "@eslint-community/regexpp", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@eslint/config-array", + "name": "@eslint/config-array", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "categories": [ + "build" + ], + "dependencies": [ + "@eslint/object-schema", + "debug", + "minimatch" + ] + }, + { + "path": "node_modules/@eslint/config-helpers", + "name": "@eslint/config-helpers", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "categories": [ + "build" + ], + "dependencies": [ + "@eslint/core" + ] + }, + { + "path": "node_modules/@eslint/core", + "name": "@eslint/core", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "categories": [ + "build" + ], + "dependencies": [ + "@types/json-schema" + ] + }, + { + "path": "node_modules/@eslint/js", + "name": "@eslint/js", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@eslint/object-schema", + "name": "@eslint/object-schema", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@eslint/plugin-kit", + "name": "@eslint/plugin-kit", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "categories": [ + "build" + ], + "dependencies": [ + "@eslint/core", + "levn" + ] + }, + { + "path": "node_modules/@humanfs/core", + "name": "@humanfs/core", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "categories": [ + "build" + ], + "dependencies": [ + "@humanfs/types" + ] + }, + { + "path": "node_modules/@humanfs/node", + "name": "@humanfs/node", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "categories": [ + "build" + ], + "dependencies": [ + "@humanfs/core", + "@humanfs/types", + "@humanwhocodes/retry" + ] + }, + { + "path": "node_modules/@humanfs/types", + "name": "@humanfs/types", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@humanwhocodes/module-importer", + "name": "@humanwhocodes/module-importer", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@humanwhocodes/retry", + "name": "@humanwhocodes/retry", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@oxc-project/types", + "name": "@oxc-project/types", + "version": "0.143.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz", + "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@playwright/test", + "name": "@playwright/test", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", + "categories": [ + "test" + ], + "dependencies": [ + "playwright" + ] + }, + { + "path": "node_modules/@rolldown/binding-android-arm64", + "name": "@rolldown/binding-android-arm64", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz", + "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-darwin-arm64", + "name": "@rolldown/binding-darwin-arm64", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz", + "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-darwin-x64", + "name": "@rolldown/binding-darwin-x64", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz", + "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-freebsd-x64", + "name": "@rolldown/binding-freebsd-x64", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz", + "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-arm-gnueabihf", + "name": "@rolldown/binding-linux-arm-gnueabihf", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz", + "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-arm64-gnu", + "name": "@rolldown/binding-linux-arm64-gnu", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz", + "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-arm64-musl", + "name": "@rolldown/binding-linux-arm64-musl", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz", + "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-ppc64-gnu", + "name": "@rolldown/binding-linux-ppc64-gnu", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz", + "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-s390x-gnu", + "name": "@rolldown/binding-linux-s390x-gnu", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz", + "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-x64-gnu", + "name": "@rolldown/binding-linux-x64-gnu", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz", + "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-linux-x64-musl", + "name": "@rolldown/binding-linux-x64-musl", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz", + "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-openharmony-arm64", + "name": "@rolldown/binding-openharmony-arm64", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz", + "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-win32-arm64-msvc", + "name": "@rolldown/binding-win32-arm64-msvc", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz", + "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/binding-win32-x64-msvc", + "name": "@rolldown/binding-win32-x64-msvc", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz", + "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@rolldown/pluginutils", + "name": "@rolldown/pluginutils", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@tweenjs/tween.js", + "name": "@tweenjs/tween.js", + "version": "23.1.3", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz", + "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@types/esrecurse", + "name": "@types/esrecurse", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@types/estree", + "name": "@types/estree", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@types/json-schema", + "name": "@types/json-schema", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@types/react", + "name": "@types/react", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "categories": [ + "build" + ], + "dependencies": [ + "csstype" + ] + }, + { + "path": "node_modules/@types/react-dom", + "name": "@types/react-dom", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@types/stats.js", + "name": "@types/stats.js", + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz", + "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@types/three", + "name": "@types/three", + "version": "0.185.1", + "resolved": "https://registry.npmjs.org/@types/three/-/three-0.185.1.tgz", + "integrity": "sha512-db1xTb+EgYF2didW+eudSvVPtn75zo+fGsY8ShQrJY/B5ZBmC2Fiaykv3aImHAlCNEGuMPkPGXBJGLwzu5mC7A==", + "categories": [ + "build" + ], + "dependencies": [ + "@dimforge/rapier3d-compat", + "@tweenjs/tween.js", + "@types/stats.js", + "@types/webxr", + "fflate", + "meshoptimizer" + ] + }, + { + "path": "node_modules/@types/webxr", + "name": "@types/webxr", + "version": "0.5.24", + "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", + "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@typescript-eslint/eslint-plugin", + "name": "@typescript-eslint/eslint-plugin", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "categories": [ + "build" + ], + "dependencies": [ + "@eslint-community/regexpp", + "@typescript-eslint/scope-manager", + "@typescript-eslint/type-utils", + "@typescript-eslint/utils", + "@typescript-eslint/visitor-keys", + "ignore", + "natural-compare", + "ts-api-utils" + ] + }, + { + "path": "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore", + "name": "ignore", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@typescript-eslint/parser", + "name": "@typescript-eslint/parser", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/visitor-keys", + "debug" + ] + }, + { + "path": "node_modules/@typescript-eslint/project-service", + "name": "@typescript-eslint/project-service", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "debug" + ] + }, + { + "path": "node_modules/@typescript-eslint/scope-manager", + "name": "@typescript-eslint/scope-manager", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys" + ] + }, + { + "path": "node_modules/@typescript-eslint/tsconfig-utils", + "name": "@typescript-eslint/tsconfig-utils", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@typescript-eslint/type-utils", + "name": "@typescript-eslint/type-utils", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/utils", + "debug", + "ts-api-utils" + ] + }, + { + "path": "node_modules/@typescript-eslint/types", + "name": "@typescript-eslint/types", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/@typescript-eslint/typescript-estree", + "name": "@typescript-eslint/typescript-estree", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/project-service", + "@typescript-eslint/tsconfig-utils", + "@typescript-eslint/types", + "@typescript-eslint/visitor-keys", + "debug", + "minimatch", + "semver", + "tinyglobby", + "ts-api-utils" + ] + }, + { + "path": "node_modules/@typescript-eslint/utils", + "name": "@typescript-eslint/utils", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "categories": [ + "build" + ], + "dependencies": [ + "@eslint-community/eslint-utils", + "@typescript-eslint/scope-manager", + "@typescript-eslint/types", + "@typescript-eslint/typescript-estree" + ] + }, + { + "path": "node_modules/@typescript-eslint/visitor-keys", + "name": "@typescript-eslint/visitor-keys", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/types", + "eslint-visitor-keys" + ] + }, + { + "path": "node_modules/@vitejs/plugin-react", + "name": "@vitejs/plugin-react", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", + "categories": [ + "build" + ], + "dependencies": [ + "@rolldown/pluginutils" + ] + }, + { + "path": "node_modules/acorn", + "name": "acorn", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/acorn-jsx", + "name": "acorn-jsx", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/ajv", + "name": "ajv", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "categories": [ + "build" + ], + "dependencies": [ + "fast-deep-equal", + "fast-json-stable-stringify", + "json-schema-traverse", + "uri-js" + ] + }, + { + "path": "node_modules/axe-core", + "name": "axe-core", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz", + "integrity": "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA==", + "categories": [ + "test" + ], + "dependencies": [] + }, + { + "path": "node_modules/balanced-match", + "name": "balanced-match", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/brace-expansion", + "name": "brace-expansion", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "categories": [ + "build" + ], + "dependencies": [ + "balanced-match" + ] + }, + { + "path": "node_modules/cross-spawn", + "name": "cross-spawn", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "categories": [ + "build" + ], + "dependencies": [ + "path-key", + "shebang-command", + "which" + ] + }, + { + "path": "node_modules/csstype", + "name": "csstype", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/debug", + "name": "debug", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "categories": [ + "build" + ], + "dependencies": [ + "ms" + ] + }, + { + "path": "node_modules/deep-is", + "name": "deep-is", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/detect-libc", + "name": "detect-libc", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/escape-string-regexp", + "name": "escape-string-regexp", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/eslint", + "name": "eslint", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", + "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "categories": [ + "build" + ], + "dependencies": [ + "@eslint-community/eslint-utils", + "@eslint-community/regexpp", + "@eslint/config-array", + "@eslint/config-helpers", + "@eslint/core", + "@eslint/plugin-kit", + "@humanfs/node", + "@humanwhocodes/module-importer", + "@humanwhocodes/retry", + "@types/estree", + "ajv", + "cross-spawn", + "debug", + "escape-string-regexp", + "eslint-scope", + "eslint-visitor-keys", + "espree", + "esquery", + "esutils", + "fast-deep-equal", + "file-entry-cache", + "find-up", + "glob-parent", + "ignore", + "imurmurhash", + "is-glob", + "json-stable-stringify-without-jsonify", + "minimatch", + "natural-compare", + "optionator" + ] + }, + { + "path": "node_modules/eslint-scope", + "name": "eslint-scope", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "categories": [ + "build" + ], + "dependencies": [ + "@types/esrecurse", + "@types/estree", + "esrecurse", + "estraverse" + ] + }, + { + "path": "node_modules/eslint-visitor-keys", + "name": "eslint-visitor-keys", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/espree", + "name": "espree", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "categories": [ + "build" + ], + "dependencies": [ + "acorn", + "acorn-jsx", + "eslint-visitor-keys" + ] + }, + { + "path": "node_modules/esquery", + "name": "esquery", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "categories": [ + "build" + ], + "dependencies": [ + "estraverse" + ] + }, + { + "path": "node_modules/esrecurse", + "name": "esrecurse", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "categories": [ + "build" + ], + "dependencies": [ + "estraverse" + ] + }, + { + "path": "node_modules/estraverse", + "name": "estraverse", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/esutils", + "name": "esutils", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/fast-deep-equal", + "name": "fast-deep-equal", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/fast-json-stable-stringify", + "name": "fast-json-stable-stringify", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/fast-levenshtein", + "name": "fast-levenshtein", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/fdir", + "name": "fdir", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/fflate", + "name": "fflate", + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/file-entry-cache", + "name": "file-entry-cache", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "categories": [ + "build" + ], + "dependencies": [ + "flat-cache" + ] + }, + { + "path": "node_modules/find-up", + "name": "find-up", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "categories": [ + "build" + ], + "dependencies": [ + "locate-path", + "path-exists" + ] + }, + { + "path": "node_modules/flat-cache", + "name": "flat-cache", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "categories": [ + "build" + ], + "dependencies": [ + "flatted", + "keyv" + ] + }, + { + "path": "node_modules/flatted", + "name": "flatted", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/fsevents", + "name": "fsevents", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/glob-parent", + "name": "glob-parent", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "categories": [ + "build" + ], + "dependencies": [ + "is-glob" + ] + }, + { + "path": "node_modules/ignore", + "name": "ignore", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/imurmurhash", + "name": "imurmurhash", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/is-extglob", + "name": "is-extglob", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/is-glob", + "name": "is-glob", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "categories": [ + "build" + ], + "dependencies": [ + "is-extglob" + ] + }, + { + "path": "node_modules/isexe", + "name": "isexe", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/json-buffer", + "name": "json-buffer", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/json-schema-traverse", + "name": "json-schema-traverse", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/json-stable-stringify-without-jsonify", + "name": "json-stable-stringify-without-jsonify", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/keyv", + "name": "keyv", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "categories": [ + "build" + ], + "dependencies": [ + "json-buffer" + ] + }, + { + "path": "node_modules/levn", + "name": "levn", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "categories": [ + "build" + ], + "dependencies": [ + "prelude-ls", + "type-check" + ] + }, + { + "path": "node_modules/lightningcss", + "name": "lightningcss", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "categories": [ + "build" + ], + "dependencies": [ + "detect-libc", + "lightningcss-android-arm64", + "lightningcss-darwin-arm64", + "lightningcss-darwin-x64", + "lightningcss-freebsd-x64", + "lightningcss-linux-arm-gnueabihf", + "lightningcss-linux-arm64-gnu", + "lightningcss-linux-arm64-musl", + "lightningcss-linux-x64-gnu", + "lightningcss-linux-x64-musl", + "lightningcss-win32-arm64-msvc", + "lightningcss-win32-x64-msvc" + ] + }, + { + "path": "node_modules/lightningcss-android-arm64", + "name": "lightningcss-android-arm64", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-darwin-arm64", + "name": "lightningcss-darwin-arm64", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-darwin-x64", + "name": "lightningcss-darwin-x64", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-freebsd-x64", + "name": "lightningcss-freebsd-x64", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-linux-arm-gnueabihf", + "name": "lightningcss-linux-arm-gnueabihf", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-linux-arm64-gnu", + "name": "lightningcss-linux-arm64-gnu", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-linux-arm64-musl", + "name": "lightningcss-linux-arm64-musl", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-linux-x64-gnu", + "name": "lightningcss-linux-x64-gnu", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-linux-x64-musl", + "name": "lightningcss-linux-x64-musl", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-win32-arm64-msvc", + "name": "lightningcss-win32-arm64-msvc", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/lightningcss-win32-x64-msvc", + "name": "lightningcss-win32-x64-msvc", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/locate-path", + "name": "locate-path", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "categories": [ + "build" + ], + "dependencies": [ + "p-locate" + ] + }, + { + "path": "node_modules/meshoptimizer", + "name": "meshoptimizer", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", + "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/minimatch", + "name": "minimatch", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "categories": [ + "build" + ], + "dependencies": [ + "brace-expansion" + ] + }, + { + "path": "node_modules/ms", + "name": "ms", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/nanoid", + "name": "nanoid", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/natural-compare", + "name": "natural-compare", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/optionator", + "name": "optionator", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "categories": [ + "build" + ], + "dependencies": [ + "deep-is", + "fast-levenshtein", + "levn", + "prelude-ls", + "type-check", + "word-wrap" + ] + }, + { + "path": "node_modules/p-limit", + "name": "p-limit", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "categories": [ + "build" + ], + "dependencies": [ + "yocto-queue" + ] + }, + { + "path": "node_modules/p-locate", + "name": "p-locate", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "categories": [ + "build" + ], + "dependencies": [ + "p-limit" + ] + }, + { + "path": "node_modules/path-exists", + "name": "path-exists", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/path-key", + "name": "path-key", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/picocolors", + "name": "picocolors", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/picomatch", + "name": "picomatch", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/playwright", + "name": "playwright", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "categories": [ + "test" + ], + "dependencies": [ + "fsevents", + "playwright-core" + ] + }, + { + "path": "node_modules/playwright-core", + "name": "playwright-core", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "categories": [ + "test" + ], + "dependencies": [] + }, + { + "path": "node_modules/playwright/node_modules/fsevents", + "name": "fsevents", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "categories": [ + "test" + ], + "dependencies": [] + }, + { + "path": "node_modules/postcss", + "name": "postcss", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", + "categories": [ + "build" + ], + "dependencies": [ + "nanoid", + "picocolors", + "source-map-js" + ] + }, + { + "path": "node_modules/prelude-ls", + "name": "prelude-ls", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/punycode", + "name": "punycode", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/react", + "name": "react", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "categories": [ + "production" + ], + "dependencies": [] + }, + { + "path": "node_modules/react-dom", + "name": "react-dom", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "categories": [ + "production" + ], + "dependencies": [ + "scheduler" + ] + }, + { + "path": "node_modules/rolldown", + "name": "rolldown", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz", + "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==", + "categories": [ + "build" + ], + "dependencies": [ + "@oxc-project/types", + "@rolldown/binding-android-arm64", + "@rolldown/binding-darwin-arm64", + "@rolldown/binding-darwin-x64", + "@rolldown/binding-freebsd-x64", + "@rolldown/binding-linux-arm-gnueabihf", + "@rolldown/binding-linux-arm64-gnu", + "@rolldown/binding-linux-arm64-musl", + "@rolldown/binding-linux-ppc64-gnu", + "@rolldown/binding-linux-s390x-gnu", + "@rolldown/binding-linux-x64-gnu", + "@rolldown/binding-linux-x64-musl", + "@rolldown/binding-openharmony-arm64", + "@rolldown/binding-win32-arm64-msvc", + "@rolldown/binding-win32-x64-msvc", + "@rolldown/pluginutils" + ] + }, + { + "path": "node_modules/scheduler", + "name": "scheduler", + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "categories": [ + "production" + ], + "dependencies": [] + }, + { + "path": "node_modules/semver", + "name": "semver", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/shebang-command", + "name": "shebang-command", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "categories": [ + "build" + ], + "dependencies": [ + "shebang-regex" + ] + }, + { + "path": "node_modules/shebang-regex", + "name": "shebang-regex", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/source-map-js", + "name": "source-map-js", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/tinyglobby", + "name": "tinyglobby", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "categories": [ + "build" + ], + "dependencies": [ + "fdir", + "picomatch" + ] + }, + { + "path": "node_modules/ts-api-utils", + "name": "ts-api-utils", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/type-check", + "name": "type-check", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "categories": [ + "build" + ], + "dependencies": [ + "prelude-ls" + ] + }, + { + "path": "node_modules/typescript", + "name": "typescript", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/typescript-eslint", + "name": "typescript-eslint", + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.66.0.tgz", + "integrity": "sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==", + "categories": [ + "build" + ], + "dependencies": [ + "@typescript-eslint/eslint-plugin", + "@typescript-eslint/parser", + "@typescript-eslint/typescript-estree", + "@typescript-eslint/utils" + ] + }, + { + "path": "node_modules/uri-js", + "name": "uri-js", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "categories": [ + "build" + ], + "dependencies": [ + "punycode" + ] + }, + { + "path": "node_modules/vite", + "name": "vite", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", + "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "categories": [ + "build" + ], + "dependencies": [ + "fsevents", + "lightningcss", + "picomatch", + "postcss", + "rolldown", + "tinyglobby" + ] + }, + { + "path": "node_modules/which", + "name": "which", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "categories": [ + "build" + ], + "dependencies": [ + "isexe" + ] + }, + { + "path": "node_modules/word-wrap", + "name": "word-wrap", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "categories": [ + "build" + ], + "dependencies": [] + }, + { + "path": "node_modules/yocto-queue", + "name": "yocto-queue", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "categories": [ + "build" + ], + "dependencies": [] + } + ], + "nextTask": "M13-05D" +} diff --git a/tests/golden/M13-05C/manifest.json b/tests/golden/M13-05C/manifest.json new file mode 100644 index 00000000..15e70ebd --- /dev/null +++ b/tests/golden/M13-05C/manifest.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "task": "M13-05C", + "parentTask": "M13-05B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_NPM_LOCKFILE", + "operation": "DEPENDENCY_INVENTORY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05B/manifest.json", "sha256": "91ab52292f2b96fb0bfd49704e3d75d90ba9824865971464b601c9a4e3976737" }, + "package": { "path": "web/package.json", "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162" }, + "lockfile": { "path": "web/package-lock.json", "sha256": "61f6d13e0148321d3c625a5baa212b5444296d111193c4d62a692685faebff1f" }, + "generator": { "path": "tools/web/generate-dependency-inventory.mjs", "sha256": "b703520d9bcf704bfbed2378983810616c88debeae36f8f7dc036fb71b449a50" }, + "checker": { "path": "tools/web/check-dependency-inventory.mjs", "sha256": "c3af9cb5365c2f40bde41d2a6f4e5dd55d0152bfa16cc8fc76e07daf37961bde" }, + "inventory": { "path": "tests/golden/M13-05C/dependency-inventory.json", "sha256": "310cdcb3d0c7c5984aafe9ced49b606f76b101597b90e6c0fced3858a6886579" }, + "packageScript": { "path": "web/package.json", "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162" } + }, + "nextTask": "M13-05D" +} diff --git a/tests/golden/M13-05D/dependency-severity-report.json b/tests/golden/M13-05D/dependency-severity-report.json new file mode 100644 index 00000000..f70ff4d5 --- /dev/null +++ b/tests/golden/M13-05D/dependency-severity-report.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "task": "M13-05D", + "operation": "DEPENDENCY_SEVERITY_POLICY", + "inventorySha256": "310cdcb3d0c7c5984aafe9ced49b606f76b101597b90e6c0fced3858a6886579", + "findings": 0, + "exceptions": 0, + "blocked": 0, + "review": 0, + "status": "PASS", + "negativeCases": { + "missingException": "BLOCKED", + "expiredException": "REJECTED", + "incompleteException": "REJECTED", + "completeException": "ACCEPTED" + }, + "execution": "DISABLED", + "nextTask": "M13-05E" +} diff --git a/tests/golden/M13-05D/manifest.json b/tests/golden/M13-05D/manifest.json new file mode 100644 index 00000000..a661bd1f --- /dev/null +++ b/tests/golden/M13-05D/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-05D", + "parentTask": "M13-05C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_SEVERITY_POLICY", + "operation": "DEPENDENCY_SEVERITY_POLICY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05C/manifest.json", "sha256": "24abe1863de8fcb112434fb3226986f3a7072820cde8869dadb8fbad32d1e6c1" }, + "policy": { "path": "docs/web/dependency-severity-policy.json", "sha256": "a549436cc30eec39c87a8a19b73d0dad4489d4af035be9fceb839c032eb37ef1" }, + "inventory": { "path": "tests/golden/M13-05C/dependency-inventory.json", "sha256": "310cdcb3d0c7c5984aafe9ced49b606f76b101597b90e6c0fced3858a6886579" }, + "checker": { "path": "tools/web/check-dependency-severity-policy.mjs", "sha256": "151ab844f49c798e7b6672fb513feed1ee8fd9ed6a755e55567b7c758ce2c692" }, + "package": { "path": "web/package.json", "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162" }, + "report": { "path": "tests/golden/M13-05D/dependency-severity-report.json", "sha256": "51402642af78f24d57239e3ecc78cab278e270ca5a05433f5208b30ae37dcc14" } + }, + "nextTask": "M13-05E" +} diff --git a/tests/golden/M13-05E/manifest.json b/tests/golden/M13-05E/manifest.json new file mode 100644 index 00000000..45e9f1cd --- /dev/null +++ b/tests/golden/M13-05E/manifest.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "task": "M13-05E", + "parentTask": "M13-05D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_RELEASE_ARCHIVE", + "operation": "SUPPLY_CHAIN_BINDING", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05D/manifest.json", "sha256": "49ad57e505000d6552977c44a8822d63b859dec4a15d07fab55e6c0d9b2d344e" }, + "checker": { "path": "tools/web/check-supply-chain-binding.mjs", "sha256": "0aa29a3537195d8164702749565e389f84434cbe6171fb479d59761d66e2ea82" }, + "binaryChecker": { "path": "tools/web/check-binary-archive.mjs", "sha256": "29a7e39c9c9e9454c74a2c29693f4d5330e94331710f7749c4d5c7afd14cf0aa" }, + "sourceChecker": { "path": "tools/web/check-source-archive.mjs", "sha256": "ff4aa5eb0ef1d595f06182ace1201788e8436ea62204553bd9f3fa52319eecb5" }, + "package": { "path": "web/package.json", "sha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162" }, + "lockfile": { "path": "web/package-lock.json", "sha256": "61f6d13e0148321d3c625a5baa212b5444296d111193c4d62a692685faebff1f" }, + "sbom": { "path": "docs/web/sbom.spdx.json", "sha256": "12f6148998b82838df2713ca4a13c7505ca160f663e30770eca2269f2d85bdf2" }, + "notices": { "path": "docs/web/third-party-notices.json", "sha256": "d5ec6e6ec9e1e0a50fe4455513a2938838609d3809224eaf26a2ebfac474f0ae" }, + "binaryArchive": { "path": "release/blender-web-offline.tar.gz", "sha256": "238f3d406e91e39403fcb7db855c60c9cf670f0400e796e6d763defc6d968f18" }, + "sourceArchive": { "path": "release/blender-web-corresponding-source.tar.gz", "sha256": "d20e154a8d769c78e127c292861aff9c69a76d9130a83c1623e590a9c2336b8c" }, + "sums": { "path": "release/SHA256SUMS.txt", "sha256": "4f2bc3cee44ec7c924392eb005455c23e6c63cdcb3fb3d605cf54997f890181d" }, + "report": { "path": "tests/golden/M13-05E/supply-chain-report.json", "sha256": "88cdbff393b01e2840c82f3465a2568df029b51e81756120f5f87f9b63a086d3" } + }, + "nextTask": "M13-05F" +} diff --git a/tests/golden/M13-05E/supply-chain-report.json b/tests/golden/M13-05E/supply-chain-report.json new file mode 100644 index 00000000..7c110e4b --- /dev/null +++ b/tests/golden/M13-05E/supply-chain-report.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M13-05E", + "operation": "SUPPLY_CHAIN_BINDING", + "commit": "5a11045ca5cde8b80e95498924bb7d2b1d3c2496", + "inputs": { + "packageSha256": "b12daaf74ed89e058d27682e7ebcfddf102b2f3cb8354a921966d636bc651162", + "lockfileSha256": "61f6d13e0148321d3c625a5baa212b5444296d111193c4d62a692685faebff1f", + "sbomSha256": "12f6148998b82838df2713ca4a13c7505ca160f663e30770eca2269f2d85bdf2", + "noticesSha256": "d5ec6e6ec9e1e0a50fe4455513a2938838609d3809224eaf26a2ebfac474f0ae" + }, + "archives": { + "binary": { + "path": "release/blender-web-offline.tar.gz", + "sha256": "238f3d406e91e39403fcb7db855c60c9cf670f0400e796e6d763defc6d968f18", + "entries": 38 + }, + "source": { + "path": "release/blender-web-corresponding-source.tar.gz", + "sha256": "d20e154a8d769c78e127c292861aff9c69a76d9130a83c1623e590a9c2336b8c", + "entries": 19851 + } + }, + "sourceOffer": "BOUND_TO_CORRESPONDING_SOURCE_ARCHIVE_AND_SHA256SUMS", + "checks": { + "spdx23": true, + "lockfileBound": true, + "noticesBound": true, + "sourceOfferBound": true, + "archiveChecksumsBound": true, + "sourcePackageBound": true + }, + "execution": "DISABLED", + "nextTask": "M13-05F" +} diff --git a/tests/golden/M13-05F/malicious-input-report.json b/tests/golden/M13-05F/malicious-input-report.json new file mode 100644 index 00000000..c71f6a80 --- /dev/null +++ b/tests/golden/M13-05F/malicious-input-report.json @@ -0,0 +1,88 @@ +{ + "schemaVersion": 1, + "task": "M13-05F", + "operation": "MALICIOUS_INPUT_MATRIX", + "cases": { + "blend": { + "status": "REJECTED", + "checker": "tools/web/check-malicious-blends.mjs", + "stableCode": "BLEND_OPEN_INVALID" + }, + "image": { + "status": "REJECTED", + "checker": "web/tests/unit/asset-preview-decode.test.mjs", + "stableCodes": [ + "ASSET_MANIFEST_INVALID", + "ASSET_BUDGET_EXCEEDED", + "ASSET_SOURCE_HASH_MISMATCH" + ] + }, + "font": { + "status": "REJECTED", + "checker": "web/tests/unit/external-vfont.test.mjs", + "stableCodes": [ + "NON_MESH_BINARY_INVALID", + "NON_MESH_RESOURCE_OUTSIDE_PROJECT", + "ASSET_SOURCE_HASH_MISMATCH" + ] + }, + "media": { + "status": "REJECTED", + "checker": "web/tests/unit/sequencer-media-cache.test.mjs", + "stableCode": "SEQUENCER_SCHEMA_INVALID" + }, + "archive": { + "status": "REJECTED", + "checker": "tools/web/check-malicious-archive-fixtures.mjs", + "stableCode": "IO_ARCHIVE_UNSAFE" + }, + "nodeGraph": { + "status": "REJECTED", + "checker": "web/tests/unit/shader-compiler.test.mjs", + "stableCodes": [ + "SHADER_NODE_UNSUPPORTED", + "SHADER_INVALID_GRAPH" + ] + }, + "manifest": { + "status": "REJECTED", + "checker": "web/tests/unit/script-manifest-budgets.test.mjs", + "stableCode": "SCRIPT_MANIFEST_INVALID" + }, + "glb": { + "status": "REJECTED", + "checker": "web/tests/unit/glb-negative-cases.test.mjs", + "stableCodes": [ + "GLB_SPARSE_ACCESSOR_UNSUPPORTED", + "GLB_EXTENSION_UNSUPPORTED", + "GLB_EXTERNAL_URI_BLOCKED", + "GLB_IMPORT_BUDGET_EXCEEDED" + ] + } + }, + "executions": [ + { + "id": "blend", + "status": "REJECTED", + "exitCode": 0 + }, + { + "id": "archive", + "status": "REJECTED", + "exitCode": 0 + }, + { + "id": "image-font-media-node-manifest", + "status": "REJECTED", + "exitCode": 0 + }, + { + "id": "glb", + "status": "REJECTED", + "exitCode": 0 + } + ], + "allRejected": true, + "execution": "DISABLED", + "nextTask": "M13-05G" +} diff --git a/tests/golden/M13-05F/manifest.json b/tests/golden/M13-05F/manifest.json new file mode 100644 index 00000000..6a740801 --- /dev/null +++ b/tests/golden/M13-05F/manifest.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "task": "M13-05F", + "parentTask": "M13-05E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_PROTOCOL_NEGATIVE_MATRIX", + "operation": "MALICIOUS_INPUT_MATRIX", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05E/manifest.json", "sha256": "68193fe8713ed5a4f77727fcd74c64ae42f32c9216528146dbacb6606cccdf01" }, + "checker": { "path": "tools/web/check-malicious-input-matrix.mjs", "sha256": "9c14cd35922b710b46f00c2be45dfad11b32bce0deb8cd5abfdafd1de828203a" }, + "package": { "path": "web/package.json", "sha256": "342f75fe285f99da301eba97590fd9fc76cef4938508177fd43e1827f0295076" }, + "report": { "path": "tests/golden/M13-05F/malicious-input-report.json", "sha256": "04b2364e7a72aa1c4cbe19d81c3247b9ebbfb6d1367de68625550c11aadde7cd" } + }, + "nextTask": "M13-05G" +} diff --git a/tests/golden/M13-05G/fuzz-report.json b/tests/golden/M13-05G/fuzz-report.json new file mode 100644 index 00000000..d5d9026e --- /dev/null +++ b/tests/golden/M13-05G/fuzz-report.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "task": "M13-05G", + "operation": "DETERMINISTIC_FUZZ_REGRESSION", + "seed": 1511506142, + "domains": [ + "blend", + "image", + "font", + "node", + "manifest" + ], + "iterationsPerDomain": 16, + "totalCases": 80, + "counts": { + "blend": { + "accepted": 0, + "rejected": 16 + }, + "image": { + "accepted": 15, + "rejected": 1 + }, + "font": { + "accepted": 16, + "rejected": 0 + }, + "node": { + "accepted": 0, + "rejected": 16 + }, + "manifest": { + "accepted": 0, + "rejected": 16 + } + }, + "crashes": 0, + "minimizedCorpus": [], + "crashPolicy": "SAVE_REPLAY_BEFORE_FIX_AND_ADD_TO_REGRESSION", + "execution": "DISABLED", + "nextTask": "M13-05H" +} diff --git a/tests/golden/M13-05G/manifest.json b/tests/golden/M13-05G/manifest.json new file mode 100644 index 00000000..09852547 --- /dev/null +++ b/tests/golden/M13-05G/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M13-05G", + "parentTask": "M13-05F", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_DETERMINISTIC_FUZZ", + "operation": "DETERMINISTIC_FUZZ_REGRESSION", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05F/manifest.json", "sha256": "34043df0515f4b422a2b265d38978dd3c7f21acd12753c99f9a50f9b23f8bd60" }, + "runner": { "path": "tools/web/fuzz-case-runner.mjs", "sha256": "a4d5d1b743b454705b1c3a257f1f36d1fea8abd77442e11fb1fec9e2905a3f79" }, + "checker": { "path": "tools/web/check-fuzz-regression.mjs", "sha256": "55c8fa3776b22eb3f2e2ffd1629f0092384df30ef3913699a1ba93b283d641d8" }, + "package": { "path": "web/package.json", "sha256": "1feb509789d7f06019390bd86197bb9851cd520fd9fe310d1e29bfc3d2ef3f18" }, + "report": { "path": "tests/golden/M13-05G/fuzz-report.json", "sha256": "5eb4a5469732697be7910fded3ce34cf2898735f767d94c1abbe0dd264bcc0d9" } + }, + "nextTask": "M13-05H" +} diff --git a/tests/golden/M13-05H/manifest.json b/tests/golden/M13-05H/manifest.json new file mode 100644 index 00000000..da7edf12 --- /dev/null +++ b/tests/golden/M13-05H/manifest.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "task": "M13-05H", + "parentTask": "M13-05G", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_PRODUCTION_PROTOCOL", + "operation": "SCRIPT_AUDIT_INTEGRITY", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05G/manifest.json", "sha256": "76a2a1fc25fa5fdec22e7ed231dd1e356c448220bdb38169f466f26515fc2865" }, + "protocol": { "path": "web/protocol/scripting-platform.ts", "sha256": "6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2" }, + "checker": { "path": "tools/web/check-script-audit-integrity.mjs", "sha256": "ab3fede492c02bd006857e9be08c08fca4dc9f0fcff22d2106c7649da1d7ec90" }, + "unit": { "path": "web/tests/unit/script-audit-integrity.test.mjs", "sha256": "2d72c60d5f4506eb336819da2c5c6800245ff0fa4c439ef524e56a71914882b9" }, + "package": { "path": "web/package.json", "sha256": "1feb509789d7f06019390bd86197bb9851cd520fd9fe310d1e29bfc3d2ef3f18" }, + "report": { "path": "tests/golden/M13-05H/script-audit-integrity-report.json", "sha256": "579ac875dbeb4ebcc6d369269a837456d2ca3a095ef36ddaf100c875ef1d5323" } + }, + "nextTask": "M14-01A" +} diff --git a/tests/golden/M13-05H/script-audit-integrity-report.json b/tests/golden/M13-05H/script-audit-integrity-report.json new file mode 100644 index 00000000..d3e3e2ba --- /dev/null +++ b/tests/golden/M13-05H/script-audit-integrity-report.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "task": "M13-05H", + "operation": "SCRIPT_AUDIT_INTEGRITY", + "runtime": "NODE_PRODUCTION_PROTOCOL", + "log": { + "entryCount": 2, + "sequences": [ + 1, + 2 + ], + "requestIds": [ + "audit:first", + "audit:second" + ], + "firstPreviousEntrySha256": null, + "secondPreviousEntrySha256": "5b0fcd4cfc634b09a3fc431a2f616164bb681b22c9318bfb0983140782309cdf", + "firstEntrySha256": "5b0fcd4cfc634b09a3fc431a2f616164bb681b22c9318bfb0983140782309cdf", + "secondEntrySha256": "2d34dfadf64168e95fb8286eb27e579c285988cb5b22bc599ec115eca5df2c23", + "strictlyIncreasingTime": true, + "uniqueRequestIds": true, + "continuousHashChain": true + }, + "negative": { + "replay": "SCRIPT_MANIFEST_INVALID", + "timeOrder": "SCRIPT_MANIFEST_INVALID", + "sequence": "SCRIPT_MANIFEST_INVALID", + "entryTamper": "SCRIPT_MANIFEST_INVALID", + "chainTamper": "SCRIPT_MANIFEST_INVALID" + }, + "execution": "DISABLED", + "nextTask": "M14-01A" +} diff --git a/tests/golden/M14-01A/chromium-freeze-report.json b/tests/golden/M14-01A/chromium-freeze-report.json new file mode 100644 index 00000000..fe2229a1 --- /dev/null +++ b/tests/golden/M14-01A/chromium-freeze-report.json @@ -0,0 +1,70 @@ +{ + "schemaVersion": 1, + "task": "M14-01A", + "operation": "CHROMIUM_ENGINE_ARCHIVE_FREEZE", + "browser": { + "family": "Google Chrome", + "version": "150.0.7871.128", + "command": "google-chrome", + "executablePath": "/home/mes123456/.local/bin/google-chrome", + "executableSha256": "c2c5d6eb08c991cd8b947600ebe6d4c3964d3968013bc0abdc10c01bb42fc400", + "versionOutput": "Google Chrome 150.0.7871.128" + }, + "engine": { + "releaseId": "blender-wasm-0.1.0-rc.1", + "manifest": { + "path": "web/app/public/engine-manifest.json", + "sha256": "7656936afae05b4936d7c8fa8dde94ee9e09ffff3a2c5e6f1b9d8a38f043ac47" + }, + "variants": { + "single": { + "js": { + "path": "web/app/public/vendor/blender/single/web_engine.js", + "sha256": "25aa51361aa8c95479873240c776d6213c1092dd480ee2a0036d2cbbd561dcad" + }, + "wasm": { + "path": "web/app/public/vendor/blender/single/web_engine.wasm", + "sha256": "7f3a50f1d41b2ac0e6366e84caaabefc3cdfc47c055d5ea459abec654da6fcc0" + } + }, + "pthread": { + "js": { + "path": "web/app/public/vendor/blender/pthread/web_engine.js", + "sha256": "a64288eeb74fb0696d38d785348f0ba37e7ae716b5a341bf181f178f78e1324d" + }, + "wasm": { + "path": "web/app/public/vendor/blender/pthread/web_engine.wasm", + "sha256": "f2a26a9221b5d4d5e710463a89e2d93faf9b2d8a9076a455c5ce1a0e52d88b5f" + }, + "pthreadWorker": { + "path": "web/app/public/vendor/blender/pthread/web_engine.js", + "sha256": "a64288eeb74fb0696d38d785348f0ba37e7ae716b5a341bf181f178f78e1324d" + } + } + } + }, + "archives": { + "binaryArchive": { + "path": "release/blender-web-offline.tar.gz", + "bytes": 8147395, + "sha256": "238f3d406e91e39403fcb7db855c60c9cf670f0400e796e6d763defc6d968f18" + }, + "sourceArchive": { + "path": "release/blender-web-corresponding-source.tar.gz", + "bytes": 207603811, + "sha256": "4a33634323f09397267f8c9afd494f4e001b97a75f758e2b438665dafed875e0" + } + }, + "checksums": { + "path": "release/SHA256SUMS.txt", + "sha256": "13a30e5e048b1cb7d771f23e6189903cc6f5d6d9074d8ad695649251813bc082" + }, + "rcManifest": { + "path": "release/RC_MANIFEST.json", + "sha256": "55715a0825e0f2802379166d8e17cd9797753fe75bea63481018965b1a81a93e", + "rcId": "web-blender-0.1.0-rc.1", + "gitCommit": "5a11045ca5cde8b80e95498924bb7d2b1d3c2496" + }, + "execution": "DISABLED", + "nextTask": "M14-01B" +} diff --git a/tests/golden/M14-01A/manifest.json b/tests/golden/M14-01A/manifest.json new file mode 100644 index 00000000..17efa3ed --- /dev/null +++ b/tests/golden/M14-01A/manifest.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "task": "M14-01A", + "parentTask": "M13-05H", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_RUNTIME_ARTIFACT_HASH", + "operation": "CHROMIUM_ENGINE_ARCHIVE_FREEZE", + "artifacts": { + "parentManifest": { "path": "tests/golden/M13-05H/manifest.json", "sha256": "5521a95c2b13c69232aa70950a9fec6839a424a4a1bc534e00bebae35212a6d2" }, + "checker": { "path": "tools/web/check-chromium-release-freeze.mjs", "sha256": "a92e81eeb32ab265f3809f3509fe2e14269dab5bfcdef278447fcf2d05955ebc" }, + "package": { "path": "web/package.json", "sha256": "2cf786115d74d0fa654ff9bb8e802c50166120ed9965f4209b8a4ec590febdc0" }, + "engineManifest": { "path": "web/app/public/engine-manifest.json", "sha256": "7656936afae05b4936d7c8fa8dde94ee9e09ffff3a2c5e6f1b9d8a38f043ac47" }, + "rcManifest": { "path": "release/RC_MANIFEST.json", "sha256": "55715a0825e0f2802379166d8e17cd9797753fe75bea63481018965b1a81a93e" }, + "binaryArchive": { "path": "release/blender-web-offline.tar.gz", "sha256": "238f3d406e91e39403fcb7db855c60c9cf670f0400e796e6d763defc6d968f18" }, + "sourceArchive": { "path": "release/blender-web-corresponding-source.tar.gz", "sha256": "4a33634323f09397267f8c9afd494f4e001b97a75f758e2b438665dafed875e0" }, + "sums": { "path": "release/SHA256SUMS.txt", "sha256": "13a30e5e048b1cb7d771f23e6189903cc6f5d6d9074d8ad695649251813bc082" }, + "report": { "path": "tests/golden/M14-01A/chromium-freeze-report.json", "sha256": "ec3c7480df2cbc3068e8ee41aa0146b17703f042d84c361729d8a0c7a06c216b" } + }, + "nextTask": "M14-01B" +} diff --git a/tests/golden/M14-01B/firefox-capability-report.json b/tests/golden/M14-01B/firefox-capability-report.json new file mode 100644 index 00000000..fcb18cac --- /dev/null +++ b/tests/golden/M14-01B/firefox-capability-report.json @@ -0,0 +1,71 @@ +{ + "schemaVersion": 1, + "task": "M14-01B", + "operation": "FIREFOX_CAPABILITY_PROBE", + "runtime": "PLAYWRIGHT_FIREFOX", + "browser": { + "version": "153.0", + "executablePath": "/home/mes123456/.cache/ms-playwright/firefox-1538/firefox/firefox", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:153.0) Gecko/20100101 Firefox/153.0", + "platform": "Linux x86_64", + "hardwareConcurrency": 16 + }, + "assets": { + "worker": { + "path": "web/dist/assets/storage.worker-D9TqXsGS.js", + "sha256": "40c5ef732bd0c7571c53f0854b6b966a9f3b0085932c1ee23962c2387dd15706" + }, + "wasm": { + "path": "web/dist/assets/web_engine-CfcxjuJm.wasm", + "sha256": "7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea" + } + }, + "capabilities": { + "wasm": { + "name": "wasm", + "status": "PASS", + "code": "WASM_READY", + "bytes": 15467310 + }, + "worker": { + "name": "worker", + "status": "PASS", + "code": "WORKER_READY" + }, + "opfs": { + "name": "opfs", + "status": "PASS", + "code": "OPFS_READY" + }, + "indexedDB": { + "name": "indexedDB", + "status": "PASS", + "code": "INDEXEDDB_READY" + }, + "webgl2": { + "name": "webgl2", + "status": "PASS", + "code": "WEBGL2_READY", + "renderer": "Intel(R) HD Graphics, or similar" + }, + "webgpu": { + "name": "webgpu", + "status": "BLOCKED", + "code": "WEBGPU_UNAVAILABLE" + }, + "offscreen": { + "name": "offscreen", + "status": "PASS", + "code": "OFFSCREEN_READY", + "context2d": true + }, + "isolation": { + "name": "isolation", + "status": "PASS", + "code": "ISOLATION_READY" + } + }, + "supportRule": "PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT", + "execution": "DISABLED", + "nextTask": "M14-01C" +} diff --git a/tests/golden/M14-01B/manifest.json b/tests/golden/M14-01B/manifest.json new file mode 100644 index 00000000..a975b9a3 --- /dev/null +++ b/tests/golden/M14-01B/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M14-01B", + "parentTask": "M14-01A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_FIREFOX", + "operation": "FIREFOX_CAPABILITY_PROBE", + "artifacts": { + "parentManifest": { "path": "tests/golden/M14-01A/manifest.json", "sha256": "3bdb0eb0e74f3966b4c59ad7e2e82e74364d160beb2371422729f4b46b108024" }, + "checker": { "path": "tools/web/check-firefox-capability.mjs", "sha256": "765e02d9f3baefd00b3f3a2359aaa363563acaeb1be4344489a8d0cfd044bc2c" }, + "package": { "path": "web/package.json", "sha256": "2cf786115d74d0fa654ff9bb8e802c50166120ed9965f4209b8a4ec590febdc0" }, + "engineManifest": { "path": "web/app/public/engine-manifest.json", "sha256": "7656936afae05b4936d7c8fa8dde94ee9e09ffff3a2c5e6f1b9d8a38f043ac47" }, + "report": { "path": "tests/golden/M14-01B/firefox-capability-report.json", "sha256": "5030d780f81cdb26cd84ecdb54d425695b85fb4783acf75218dabdf167cec644" } + }, + "nextTask": "M14-01C" +} diff --git a/tests/golden/M14-01C/manifest.json b/tests/golden/M14-01C/manifest.json new file mode 100644 index 00000000..2598498e --- /dev/null +++ b/tests/golden/M14-01C/manifest.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M14-01C", + "parentTask": "M14-01B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_WEBKIT", + "operation": "WEBKIT_CAPABILITY_PROBE", + "artifacts": { + "parentManifest": { "path": "tests/golden/M14-01B/manifest.json", "sha256": "be31e32af811af9b9e17a6d750d396b9790b7a422f921839fb382cfeccf0862e" }, + "checker": { "path": "tools/web/check-webkit-capability.mjs", "sha256": "fa0d5fb767f37c011d46402d51f45b5c0441122eec02c731963aff3fc632fea7" }, + "package": { "path": "web/package.json", "sha256": "2cf786115d74d0fa654ff9bb8e802c50166120ed9965f4209b8a4ec590febdc0" }, + "engineManifest": { "path": "web/app/public/engine-manifest.json", "sha256": "7656936afae05b4936d7c8fa8dde94ee9e09ffff3a2c5e6f1b9d8a38f043ac47" }, + "report": { "path": "tests/golden/M14-01C/webkit-capability-report.json", "sha256": "09e19d0fdadc7df1d608f9016698740290d164e075090623a09bddb487f5d208" } + }, + "nextTask": "M14-01D" +} diff --git a/tests/golden/M14-01C/webkit-capability-report.json b/tests/golden/M14-01C/webkit-capability-report.json new file mode 100644 index 00000000..0b0bac26 --- /dev/null +++ b/tests/golden/M14-01C/webkit-capability-report.json @@ -0,0 +1,71 @@ +{ + "schemaVersion": 1, + "task": "M14-01C", + "operation": "WEBKIT_CAPABILITY_PROBE", + "runtime": "PLAYWRIGHT_WEBKIT", + "browser": { + "version": "26.5", + "executablePath": "/home/mes123456/.cache/ms-playwright/webkit-2336/pw_run.sh", + "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/26.5 Safari/605.1.15", + "platform": "Linux x86_64", + "hardwareConcurrency": 8 + }, + "assets": { + "worker": { + "path": "web/dist/assets/storage.worker-D9TqXsGS.js", + "sha256": "40c5ef732bd0c7571c53f0854b6b966a9f3b0085932c1ee23962c2387dd15706" + }, + "wasm": { + "path": "web/dist/assets/web_engine-CfcxjuJm.wasm", + "sha256": "7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea" + } + }, + "capabilities": { + "wasm": { + "name": "wasm", + "status": "PASS", + "code": "WASM_READY", + "bytes": 15467310 + }, + "worker": { + "name": "worker", + "status": "PASS", + "code": "WORKER_READY" + }, + "opfs": { + "name": "opfs", + "status": "BLOCKED", + "code": "OPFS_UNAVAILABLE" + }, + "indexedDB": { + "name": "indexedDB", + "status": "PASS", + "code": "INDEXEDDB_READY" + }, + "webgl2": { + "name": "webgl2", + "status": "PASS", + "code": "WEBGL2_READY", + "renderer": "Apple GPU" + }, + "webgpu": { + "name": "webgpu", + "status": "BLOCKED", + "code": "WEBGPU_UNAVAILABLE" + }, + "offscreen": { + "name": "offscreen", + "status": "PASS", + "code": "OFFSCREEN_READY", + "context2d": true + }, + "isolation": { + "name": "isolation", + "status": "PASS", + "code": "ISOLATION_READY" + } + }, + "supportRule": "PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT", + "execution": "DISABLED", + "nextTask": "M14-01D" +} diff --git a/tests/golden/M14-01D/manifest.json b/tests/golden/M14-01D/manifest.json new file mode 100644 index 00000000..3d2611ce --- /dev/null +++ b/tests/golden/M14-01D/manifest.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "task": "M14-01D", + "parentTask": "M14-01C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_CHROMIUM", + "operation": "PROBE_IDENTITY_GPU_OS_ADAPTER", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-01C/manifest.json", + "sha256": "050e04ff7e96fa303bd449e419411a914edb489d2c9486797bff18a1f3b6e49c" + }, + "checker": { + "path": "tools/web/check-probe-identity.mjs", + "sha256": "426d6277b5bcd4a2a29d24469f000663f9cc02d07fd996fc528b52285e834189" + }, + "package": { + "path": "web/package.json", + "sha256": "c63126618da82d315a4070c4546c523f52d44aa174b43b5e6fe4884e5ad616ce" + }, + "engineManifest": { + "path": "web/app/public/engine-manifest.json", + "sha256": "7656936afae05b4936d7c8fa8dde94ee9e09ffff3a2c5e6f1b9d8a38f043ac47" + }, + "report": { + "path": "tests/golden/M14-01D/probe-identity-report.json", + "sha256": "df73913b7ca5af957c4f56216a1e91be8e19c6373f0359a86d963a1d03c8f0d6" + } + }, + "nextTask": "M14-01E" +} diff --git a/tests/golden/M14-01D/probe-identity-report.json b/tests/golden/M14-01D/probe-identity-report.json new file mode 100644 index 00000000..d79207cd --- /dev/null +++ b/tests/golden/M14-01D/probe-identity-report.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "task": "M14-01D", + "operation": "PROBE_IDENTITY_GPU_OS_ADAPTER", + "runtime": "PLAYWRIGHT_CHROMIUM", + "browser": { + "version": "151.0.7922.34", + "executablePath": "/home/mes123456/.cache/ms-playwright/chromium-1234/chrome-linux64/chrome", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/151.0.7922.34 Safari/537.36", + "platform": "Linux x86_64", + "language": "en-US", + "hardwareConcurrency": 16, + "deviceMemory": 16 + }, + "os": { + "platform": "linux", + "release": "6.12.95+deb13-amd64", + "version": "#1 SMP PREEMPT_DYNAMIC Debian 6.12.95-1 (2026-07-04)", + "arch": "x64", + "machine": "x86_64", + "cpus": 16 + }, + "adapter": { + "webgl2": { + "name": "webgl2", + "status": "PASS", + "code": "WEBGL2_READY", + "vendor": "Google Inc. (Google)", + "renderer": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)", + "version": "WebGL 2.0 (OpenGL ES 3.0 Chromium)" + }, + "webgpu": { + "name": "webgpu", + "status": "BLOCKED", + "code": "WEBGPU_ADAPTER_UNAVAILABLE" + } + }, + "isolation": { + "crossOriginIsolated": true + }, + "assets": { + "worker": { + "path": "web/dist/assets/storage.worker-D9TqXsGS.js", + "sha256": "40c5ef732bd0c7571c53f0854b6b966a9f3b0085932c1ee23962c2387dd15706" + }, + "wasm": { + "path": "web/dist/assets/web_engine-CfcxjuJm.wasm", + "sha256": "7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea" + } + }, + "supportRule": "IDENTITY_IS_OBSERVED_ONLY; PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT", + "execution": "DISABLED", + "nextTask": "M14-01E", + "identitySha256": "3c6f898e1b6ffd39862505b8e7dde8fd29369204e1670a013859f74ccaf1c1eb" +} diff --git a/tests/golden/M14-01E/chromium-webgpu-boundary-report.json b/tests/golden/M14-01E/chromium-webgpu-boundary-report.json new file mode 100644 index 00000000..2cca5da1 --- /dev/null +++ b/tests/golden/M14-01E/chromium-webgpu-boundary-report.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 1, + "task": "M14-01E", + "operation": "CHROMIUM_WEBGPU_FAIL_CLOSED_BOUNDARY", + "runtime": "NODE_PROTOCOL_WITH_CHROMIUM_PROBE_IDENTITY", + "chromiumProbeIdentitySha256": "3c6f898e1b6ffd39862505b8e7dde8fd29369204e1670a013859f74ccaf1c1eb", + "webgpu": { + "status": "BLOCKED", + "target": "WEB_LOCAL_BOUNDED", + "code": "WEBGPU_RENDERER_UNAVAILABLE" + }, + "webgl2": { + "status": "READY", + "target": "WEB_LOCAL_BOUNDED", + "reason": "BOUNDED_EEVEE" + }, + "execution": "DISABLED", + "nextTask": "M14-04A" +} diff --git a/tests/golden/M14-01E/manifest.json b/tests/golden/M14-01E/manifest.json new file mode 100644 index 00000000..4d90216c --- /dev/null +++ b/tests/golden/M14-01E/manifest.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "task": "M14-01E", + "parentTask": "M14-01D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "NODE_PROTOCOL_WITH_CHROMIUM_PROBE_IDENTITY", + "operation": "CHROMIUM_WEBGPU_FAIL_CLOSED_BOUNDARY", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-01D/manifest.json", + "sha256": "1b1490f1f62de135f4048d7ac883bbe925ee3772e5dc086f4fd7bff91dc03e6d" + }, + "checker": { + "path": "tools/web/check-chromium-webgpu-boundary.mjs", + "sha256": "cd81c7b07896846ae40394b0fbe70c1eb69a52bbb53e3e4642c99d90234290eb" + }, + "package": { + "path": "web/package.json", + "sha256": "afae90d2be8dd5cf3d07e106bd407fac9f16540be715058bf4320ae4923497fa" + }, + "protocol": { + "path": "web/protocol/render-routing.ts", + "sha256": "2d41d48609635c810572aaae95979df2fe9e49bbd130c60d70f64042c5d0f097" + }, + "report": { + "path": "tests/golden/M14-01E/chromium-webgpu-boundary-report.json", + "sha256": "6536f91c79c46a7a34fc631cc15f70e49593d0a1414e975ad72ffe2bbbf89d12" + } + }, + "nextTask": "M14-04A" +} diff --git a/tests/golden/M14-04A/chromium-device-budget-report.json b/tests/golden/M14-04A/chromium-device-budget-report.json new file mode 100644 index 00000000..24301c33 --- /dev/null +++ b/tests/golden/M14-04A/chromium-device-budget-report.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "task": "M14-04A", + "operation": "CHROMIUM_DEVICE_BUDGET_SELECTION", + "runtime": "CHROMIUM_PROBE_IDENTITY_REPORT", + "identitySha256": "3c6f898e1b6ffd39862505b8e7dde8fd29369204e1670a013859f74ccaf1c1eb", + "selection": { + "schemaVersion": 1, + "identitySha256": "3c6f898e1b6ffd39862505b8e7dde8fd29369204e1670a013859f74ccaf1c1eb", + "tier": "CONSERVATIVE", + "reason": "UNTRUSTED_ADAPTER", + "limits": { + "maxTextureGPUBytes": 268435456, + "maxTexturePayloadBytes": 268435456, + "maxTextureDimension": 8192, + "maxLights": 8, + "maxShadowMaps": 2 + } + }, + "execution": "DISABLED", + "nextTask": "M14-04B" +} diff --git a/tests/golden/M14-04A/manifest.json b/tests/golden/M14-04A/manifest.json new file mode 100644 index 00000000..0abcce0a --- /dev/null +++ b/tests/golden/M14-04A/manifest.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 1, + "task": "M14-04A", + "parentTask": "M14-01E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "CHROMIUM_PROBE_IDENTITY_REPORT", + "operation": "CHROMIUM_DEVICE_BUDGET_SELECTION", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-01E/manifest.json", + "sha256": "96a99c2b2c4172cda70e13979c3b45bf22b0c028e0bfd361e2c2886f2bd99463" + }, + "checker": { + "path": "tools/web/check-chromium-device-budget.mjs", + "sha256": "69275c27d602b38136d6bb3d4656e2725c52da5f1c33ed49fca6ff743c2d98f9" + }, + "protocol": { + "path": "web/protocol/device-budget.ts", + "sha256": "f6718765c2ccfbbcb61f2f112ab511ac9763bf4ba702575b5feb7f3662e2b475" + }, + "unit": { + "path": "web/tests/unit/device-budget.test.mjs", + "sha256": "71f1967abb9ed24a91c4b1578f24a5847b7e9cc3b572daf82d32fa5d431cc920" + }, + "package": { + "path": "web/package.json", + "sha256": "57d93b4776aec6d503970623ea5f7e2a58efbdbd0e1a9bcdceefe28aac95e057" + }, + "report": { + "path": "tests/golden/M14-04A/chromium-device-budget-report.json", + "sha256": "cc1723af6689eaaff16aa8c1ac12632e9d2894a5715a5461e22afba3f3d219e4" + } + }, + "nextTask": "M14-04B" +} diff --git a/tests/golden/M14-04B/chromium-dpr-report.json b/tests/golden/M14-04B/chromium-dpr-report.json new file mode 100644 index 00000000..78d22c42 --- /dev/null +++ b/tests/golden/M14-04B/chromium-dpr-report.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "task": "M14-04B", + "operation": "CHROMIUM_DPR_CANVAS_RAYCAST_CONSISTENCY", + "runtime": "PLAYWRIGHT_CHROMIUM", + "viewport": { + "cssWidth": 679, + "cssHeight": 321, + "testedDPR": [ + 1, + 1.5, + 2, + 3 + ], + "maxDPR": 2 + }, + "results": [ + { + "deviceScaleFactor": 1, + "dpr": 1, + "css": "679x321", + "backing": "679x321", + "pixelRatio": "1", + "width": 679, + "height": 321, + "selected": "object:BasicCube", + "cssBounds": "679x321" + }, + { + "deviceScaleFactor": 1.5, + "dpr": 1.5, + "css": "679x321", + "backing": "1018x481", + "pixelRatio": "1.5", + "width": 1018, + "height": 481, + "selected": "object:BasicCube", + "cssBounds": "679x321" + }, + { + "deviceScaleFactor": 2, + "dpr": 2, + "css": "679x321", + "backing": "1358x642", + "pixelRatio": "2", + "width": 1358, + "height": 642, + "selected": "object:BasicCube", + "cssBounds": "679x321" + }, + { + "deviceScaleFactor": 3, + "dpr": 3, + "css": "679x321", + "backing": "1358x642", + "pixelRatio": "2", + "width": 1358, + "height": 642, + "selected": "object:BasicCube", + "cssBounds": "679x321" + } + ], + "execution": "DISABLED", + "nextTask": "M14-04C" +} diff --git a/tests/golden/M14-04B/manifest.json b/tests/golden/M14-04B/manifest.json new file mode 100644 index 00000000..8166ce32 --- /dev/null +++ b/tests/golden/M14-04B/manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "task": "M14-04B", + "parentTask": "M14-04A", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_CHROMIUM", + "operation": "CHROMIUM_DPR_CANVAS_RAYCAST_CONSISTENCY", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-04A/manifest.json", + "sha256": "498ef586fb247adbe7fbdab66a5461442e751875a01df2737a3211156be3e1bd" + }, + "checker": { + "path": "tools/web/check-chromium-dpr-consistency.mjs", + "sha256": "96e2e59607c26b4711003afe8994e3b02210d9229aaecb7ce5a0b05dcbeb4325" + }, + "protocol": { + "path": "web/protocol/viewport-dpr.ts", + "sha256": "1b9a42bac464b8545d52a0e92c441d9e8be59268ae0541b590da79991157e5f3" + }, + "unit": { + "path": "web/tests/unit/viewport-dpr.test.mjs", + "sha256": "2e0627b1f734d137e9455803ea65924ae20e3213a4234b8df339d2e157764432" + }, + "viewport": { + "path": "web/app/src/three-adapter/viewport.ts", + "sha256": "81c1dfbaa0d77c7d2698f5c614a0351e92bd0b5d4f9252e17aa9fad24e23b39b" + }, + "offscreen": { + "path": "web/app/src/three-adapter/offscreen-viewport.ts", + "sha256": "3e959b7c6bbfe1eafe9f5549bd857c541f54ad7c0931528e53f74dd8ded728fe" + }, + "package": { + "path": "web/package.json", + "sha256": "6658c872ef6bd1e3545d3d4c5c4e06698590b146090601ee20f4fc318fc97fa8" + }, + "report": { + "path": "tests/golden/M14-04B/chromium-dpr-report.json", + "sha256": "152ec29cd695443cea654d706868141ccb6cf1dac550da154a81074c6b4876fe" + } + }, + "nextTask": "M14-04C" +} diff --git a/tests/golden/M14-04C/chromium-pointer-report.json b/tests/golden/M14-04C/chromium-pointer-report.json new file mode 100644 index 00000000..5dbab191 --- /dev/null +++ b/tests/golden/M14-04C/chromium-pointer-report.json @@ -0,0 +1,81 @@ +{ + "schemaVersion": 1, + "task": "M14-04C", + "operation": "CHROMIUM_POINTER_IDENTITY_CANCEL_CONTRACT", + "runtime": "PLAYWRIGHT_CHROMIUM", + "pointerTypes": [ + "mouse", + "touch", + "pen" + ], + "observations": [ + { + "schemaVersion": 1, + "pointerType": "mouse", + "pointerId": 1, + "pressure": 0, + "tiltX": 0, + "tiltY": 0, + "button": 0, + "buttons": 1, + "cancelled": false + }, + { + "schemaVersion": 1, + "pointerType": "mouse", + "pointerId": 1, + "pressure": 0, + "tiltX": 0, + "tiltY": 0, + "button": 0, + "buttons": 0, + "cancelled": true + }, + { + "schemaVersion": 1, + "pointerType": "touch", + "pointerId": 2, + "pressure": 0.5, + "tiltX": 0, + "tiltY": 0, + "button": 0, + "buttons": 1, + "cancelled": false + }, + { + "schemaVersion": 1, + "pointerType": "touch", + "pointerId": 2, + "pressure": 0.5, + "tiltX": 0, + "tiltY": 0, + "button": 0, + "buttons": 0, + "cancelled": true + }, + { + "schemaVersion": 1, + "pointerType": "pen", + "pointerId": 3, + "pressure": 0.75, + "tiltX": 20, + "tiltY": -15, + "button": 0, + "buttons": 1, + "cancelled": false + }, + { + "schemaVersion": 1, + "pointerType": "pen", + "pointerId": 3, + "pressure": 0.75, + "tiltX": 20, + "tiltY": -15, + "button": 0, + "buttons": 0, + "cancelled": true + } + ], + "execution": "DISABLED", + "nextTask": "M14-04D" +} diff --git a/tests/golden/M14-04C/manifest.json b/tests/golden/M14-04C/manifest.json new file mode 100644 index 00000000..508b375a --- /dev/null +++ b/tests/golden/M14-04C/manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "task": "M14-04C", + "parentTask": "M14-04B", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_CHROMIUM", + "operation": "CHROMIUM_POINTER_IDENTITY_CANCEL_CONTRACT", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-04B/manifest.json", + "sha256": "9fc0a4cd145edca5db37c03c1a2bfa1bd8de00bcc70e2b0fabc9685fbf27b7f7" + }, + "checker": { + "path": "tools/web/check-chromium-pointer-contract.mjs", + "sha256": "124ad22d0b263993379f0b0a774723615f8476a6e3f0dc293e9f04d4496b3ab9" + }, + "protocol": { + "path": "web/protocol/pointer-contract.ts", + "sha256": "6e41fcff4d9878b9653f50282e463062b981bfbde95213f9eb918766c6011f54" + }, + "unit": { + "path": "web/tests/unit/pointer-contract.test.mjs", + "sha256": "485545bf43c41ef9775a54ff3b7eef5d177fe1e26c57eb89101283bda3c774b5" + }, + "viewport": { + "path": "web/app/src/three-adapter/viewport.ts", + "sha256": "eba45f6161cce2191533231aa87c418c7c809c59e42b5b77b6839fece2d9cdf5" + }, + "offscreen": { + "path": "web/app/src/three-adapter/offscreen-viewport.ts", + "sha256": "6c17750ac362cad22964893f88668c2acf4698c281f43025ee321575b10462bf" + }, + "package": { + "path": "web/package.json", + "sha256": "77412289730e1e363431fff7f5c06f5e9edb666d937dcfa94c700e567f881fdb" + }, + "report": { + "path": "tests/golden/M14-04C/chromium-pointer-report.json", + "sha256": "c4a80b4fcd9c5d87cd8cfb95491795a70cd272ec74582922b4802b78af1b506d" + } + }, + "nextTask": "M14-04D" +} diff --git a/tests/golden/M14-04D/chromium-ime-report.json b/tests/golden/M14-04D/chromium-ime-report.json new file mode 100644 index 00000000..49102028 --- /dev/null +++ b/tests/golden/M14-04D/chromium-ime-report.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "task": "M14-04D", + "operation": "CHROMIUM_IME_COMPOSITION_OPERATOR_GUARD", + "runtime": "PLAYWRIGHT_CHROMIUM", + "before": { + "revision": "0", + "engine": "Engine: SceneIR r1 (3 objects)" + }, + "during": { + "revision": "0", + "engine": "Engine: SceneIR r1 (3 objects)", + "composing": { + "composing": "true", + "lastEvent": "UPDATE", + "revision": "0" + } + }, + "ended": { + "composing": "false", + "lastEvent": "END" + }, + "blockedShortcut": "G", + "execution": "DISABLED", + "nextTask": "M14-04E" +} diff --git a/tests/golden/M14-04D/manifest.json b/tests/golden/M14-04D/manifest.json new file mode 100644 index 00000000..eabf7300 --- /dev/null +++ b/tests/golden/M14-04D/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M14-04D", + "parentTask": "M14-04C", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_CHROMIUM", + "operation": "CHROMIUM_IME_COMPOSITION_OPERATOR_GUARD", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-04C/manifest.json", + "sha256": "b967a72696777efca2b8bf0c0b1c44f58582a4d70aea99fb954977666960c0f9" + }, + "checker": { + "path": "tools/web/check-chromium-ime-guard.mjs", + "sha256": "ee37ec21ced894f6307b4befec922bfd6854a4903506cc417891e25eb5443d1c" + }, + "protocol": { + "path": "web/protocol/ime-composition.ts", + "sha256": "53565e6fe21bc400ad98bb73286c9d2e538b413a9931291c2b4e19ae06a56db1" + }, + "unit": { + "path": "web/tests/unit/ime-composition.test.mjs", + "sha256": "688dd2fd5690532d080ecd0f0634a7a76afb6f35ccd49c81bd788f06539f251e" + }, + "app": { + "path": "web/app/src/app/App.tsx", + "sha256": "828aa52185b43e27beed2242b9bcd97a381117790324ee6069b18d5bbb1544e8" + }, + "package": { + "path": "web/package.json", + "sha256": "96bcf35901b7869bf987035cf1bf2dbb125a6a837544e8d7ce70ff336824516a" + }, + "report": { + "path": "tests/golden/M14-04D/chromium-ime-report.json", + "sha256": "6f1c29ef94e414302ff6123c9c50340c12f65630b5312f0ad90af5f4519965cf" + } + }, + "nextTask": "M14-04E" +} diff --git a/tests/golden/M14-04E/chromium-keymap-report.json b/tests/golden/M14-04E/chromium-keymap-report.json new file mode 100644 index 00000000..1f450b14 --- /dev/null +++ b/tests/golden/M14-04E/chromium-keymap-report.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "task": "M14-04E", + "operation": "CHROMIUM_KEYMAP_LAYOUT_MODIFIER_FIXTURE", + "runtime": "PLAYWRIGHT_CHROMIUM", + "fixtureNames": [ + "US", + "NON_US", + "DEAD_KEY", + "MODIFIER" + ], + "observations": [ + { + "key": "a", + "code": "KeyA", + "location": 0, + "modifiers": "", + "dead": false + }, + { + "key": "ä", + "code": "Quote", + "location": 0, + "modifiers": "SA", + "dead": false + }, + { + "key": "Dead", + "code": "Quote", + "location": 0, + "modifiers": "A", + "dead": true + }, + { + "key": "z", + "code": "KeyZ", + "location": 0, + "modifiers": "SC", + "dead": false + } + ], + "execution": "DISABLED", + "nextTask": "M14-04F" +} diff --git a/tests/golden/M14-04E/manifest.json b/tests/golden/M14-04E/manifest.json new file mode 100644 index 00000000..7face78a --- /dev/null +++ b/tests/golden/M14-04E/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M14-04E", + "parentTask": "M14-04D", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_CHROMIUM", + "operation": "CHROMIUM_KEYMAP_LAYOUT_MODIFIER_FIXTURE", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-04D/manifest.json", + "sha256": "3f7eadd9d91984db7b1f159adfd97f0865735a6db2b79083e00b5d54c23b8696" + }, + "checker": { + "path": "tools/web/check-chromium-keymap-fixture.mjs", + "sha256": "db512c0bdeba06a84cc28ec912b3320c6414562d5bce9b8f61b577b0d6faf89a" + }, + "protocol": { + "path": "web/protocol/keyboard-contract.ts", + "sha256": "a537ee12d7541d1f3af90f5d082b4d7f49f92bf9b59faf0cfcbfbd10a0097097" + }, + "unit": { + "path": "web/tests/unit/keyboard-contract.test.mjs", + "sha256": "2e52235054740fb7dcf2721a36dc1820046d71ead1a0996dec8701967d2a6d26" + }, + "app": { + "path": "web/app/src/app/App.tsx", + "sha256": "92d01daecf3f39bc4ac214e4b651ec6feb687bbfbf58c4d3e602a314cdcc664c" + }, + "package": { + "path": "web/package.json", + "sha256": "67620235b0771631ae8a0e83dcbf38d434d37cdc9d9e6dc61ba0adb6e86e397c" + }, + "report": { + "path": "tests/golden/M14-04E/chromium-keymap-report.json", + "sha256": "db87a8ca2867c3869dbd3abe8e39443f363ee90b1446fa57bb72158769b4629e" + } + }, + "nextTask": "M14-04F" +} diff --git a/tests/golden/M14-04F/chromium-input-modal-report.json b/tests/golden/M14-04F/chromium-input-modal-report.json new file mode 100644 index 00000000..c7643d2e --- /dev/null +++ b/tests/golden/M14-04F/chromium-input-modal-report.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": 1, + "task": "M14-04F", + "operation": "CHROMIUM_INPUT_MODAL_BOUNDARY", + "runtime": "PLAYWRIGHT_CHROMIUM", + "browserEvents": [ + { + "pointerId": 2, + "pointerType": "touch", + "phase": "down" + }, + { + "pointerId": 4, + "pointerType": "touch", + "phase": "down" + }, + { + "pointerId": 9, + "pointerType": "pen", + "phase": "down" + }, + { + "pointerId": 2, + "phase": "cancel" + }, + { + "pointerId": 9, + "phase": "up" + } + ], + "guarantees": { + "touchCancelMainCommit": 0, + "twoFingerNavigationRevision": 1, + "penMainCommit": 1 + }, + "execution": "DISABLED", + "nextTask": "M14-04G" +} diff --git a/tests/golden/M14-04F/manifest.json b/tests/golden/M14-04F/manifest.json new file mode 100644 index 00000000..fd742f2b --- /dev/null +++ b/tests/golden/M14-04F/manifest.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 1, + "task": "M14-04F", + "parentTask": "M14-04E", + "enablingTask": false, + "parityStateChange": false, + "runtime": "PLAYWRIGHT_CHROMIUM", + "operation": "CHROMIUM_INPUT_MODAL_BOUNDARY", + "artifacts": { + "parentManifest": { + "path": "tests/golden/M14-04E/manifest.json", + "sha256": "b5fbb86d4a6a610b4aa25f8edb9a232aca9675e25fb47b16de98858a2d7f007f" + }, + "checker": { + "path": "tools/web/check-chromium-input-modal.mjs", + "sha256": "3ed496cbb9d24617c594025ba20f38ec79d6fee56e5d471317857230143c8295" + }, + "protocol": { + "path": "web/protocol/input-modal.ts", + "sha256": "1f8318e11dbdb7aebc4f391f8c2474e9ddd2ee872eac755c1856d1f20d78344d" + }, + "unit": { + "path": "web/tests/unit/input-modal.test.mjs", + "sha256": "f56be3057561db6068e6f738a2e5e73febfe27ba5a8b210190056bd590a52c42" + }, + "package": { + "path": "web/package.json", + "sha256": "b503afa0ae7cb93014f2a1213ea19039e4d0c151524ac7d5dc360454b800a1e5" + }, + "report": { + "path": "tests/golden/M14-04F/chromium-input-modal-report.json", + "sha256": "11ea07732f74660a5410a696c3db7646a2dcc333d565d5318c2436c3ed93afc0" + } + }, + "nextTask": "M14-04G" +} diff --git a/tools/web/check-binary-archive.mjs b/tools/web/check-binary-archive.mjs index 030baeb8..2f09064c 100644 --- a/tools/web/check-binary-archive.mjs +++ b/tools/web/check-binary-archive.mjs @@ -101,7 +101,7 @@ try { const releaseMetadata = JSON.parse(fs.readFileSync(path.join(bundle, "release-metadata.json"), "utf8")); assert.equal(releaseMetadata.schemaVersion, 1); assert.equal(releaseMetadata.engineReleaseId, engine.releaseId); - assert.equal(releaseMetadata.storage.indexedDbSchemaVersion, 6); + assert.equal(releaseMetadata.storage.indexedDbSchemaVersion, 7); assert.equal(releaseMetadata.storage.opfsProjectManifestSchemaVersion, 1); assert.equal(releaseMetadata.storage.migrationDirection, "forward-only"); assert.equal(releaseMetadata.storage.originBound, true); diff --git a/tools/web/check-chromium-device-budget.mjs b/tools/web/check-chromium-device-budget.mjs new file mode 100644 index 00000000..f8caeb5f --- /dev/null +++ b/tools/web/check-chromium-device-budget.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../.."); +const sourcePath = path.join(root, "web/protocol/device-budget.ts"); +const source = fs.readFileSync(sourcePath, "utf8"); +const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const budget = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); +const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M14-01D/probe-identity-report.json"), "utf8")); +const selection = budget.selectDeviceBudget({ schemaVersion: 1, identitySha256: identity.identitySha256, webgl2: identity.adapter.webgl2, webgpu: identity.adapter.webgpu, hardwareConcurrency: identity.browser.hardwareConcurrency, deviceMemory: identity.browser.deviceMemory }); +assert.equal(selection.identitySha256, identity.identitySha256); +assert.equal(selection.tier, "CONSERVATIVE"); +assert.equal(selection.reason, "UNTRUSTED_ADAPTER"); +const report = { schemaVersion: 1, task: "M14-04A", operation: "CHROMIUM_DEVICE_BUDGET_SELECTION", runtime: "CHROMIUM_PROBE_IDENTITY_REPORT", identitySha256: identity.identitySha256, selection, execution: "DISABLED", nextTask: "M14-04B" }; +const reportPath = path.join(root, "tests/golden/M14-04A/chromium-device-budget-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-04A/manifest.json"); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); +if (process.env.UPDATE_M14_04A_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); + const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-01E/manifest.json"), checker: path.join(root, "tools/web/check-chromium-device-budget.mjs"), protocol: sourcePath, unit: path.join(root, "web/tests/unit/device-budget.test.mjs"), package: path.join(root, "web/package.json"), report: reportPath }; + const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); + fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04A", parentTask: "M14-01E", enablingTask: false, parityStateChange: false, runtime: "CHROMIUM_PROBE_IDENTITY_REPORT", operation: "CHROMIUM_DEVICE_BUDGET_SELECTION", artifacts, nextTask: "M14-04B" }, null, 2)}\n`); +} +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04A", parentTask: "M14-01E", nextTask: "M14-04B" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write(`chromium-device-budget-ok tier=${selection.tier} reason=${selection.reason} identity=${selection.identitySha256} execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-chromium-dpr-consistency.mjs b/tools/web/check-chromium-dpr-consistency.mjs new file mode 100644 index 00000000..87671d0a --- /dev/null +++ b/tools/web/check-chromium-dpr-consistency.mjs @@ -0,0 +1,93 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-04B/chromium-dpr-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-04B/manifest.json"); +const basicBlend = path.join(root, "tests/files/web/basic_scene.blend"); +assert.ok(fs.existsSync(path.join(distRoot, "index.html")), "production dist is missing"); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); +const server = http.createServer((request, response) => { + const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); + const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); + const file = path.resolve(distRoot, relative); + if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } + response.statusCode = 200; + response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); + response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); + response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); + fs.createReadStream(file).pipe(response); +}); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +let browser; +try { + const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] }); + const results = []; + let expectedCssBounds = ""; + for (const deviceScaleFactor of [1, 1.5, 2, 3]) { + const context = await browser.newContext({ viewport: { width: 960, height: 640 }, deviceScaleFactor }); + const page = await context.newPage(); + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" }); + await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 }); + await page.setInputFiles("[data-testid=blend-file-input]", basicBlend); + await page.getByText("BasicCube", { exact: true }).waitFor({ state: "visible", timeout: 20_000 }); + const canvas = page.locator("canvas.viewport-canvas"); + const bounds = await canvas.boundingBox(); + assert.ok(bounds); + await canvas.evaluate((element) => { + const rect = element.getBoundingClientRect(); + element.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 2 })); + }); + await page.waitForFunction(() => Boolean(document.querySelector(".blender-app")?.getAttribute("data-selected-object-ids")), undefined, { timeout: 10_000 }); + const value = await canvas.evaluate((element) => ({ + dpr: window.devicePixelRatio, + css: element.getAttribute("data-viewport-css-size"), + backing: element.getAttribute("data-viewport-backing-size"), + pixelRatio: element.getAttribute("data-viewport-pixel-ratio"), + width: element.width, + height: element.height, + selected: document.querySelector(".blender-app")?.getAttribute("data-selected-object-ids") ?? "", + cssBounds: `${Math.round(element.getBoundingClientRect().width)}x${Math.round(element.getBoundingClientRect().height)}`, + })); + if (!expectedCssBounds) expectedCssBounds = value.css; + assert.equal(value.css, expectedCssBounds); + const [cssWidth, cssHeight] = expectedCssBounds.split("x").map(Number); + assert.equal(value.backing, `${Math.floor(cssWidth * Math.min(deviceScaleFactor, 2))}x${Math.floor(cssHeight * Math.min(deviceScaleFactor, 2))}`); + assert.equal(value.pixelRatio, String(Math.min(deviceScaleFactor, 2))); + assert.equal(value.width, Math.floor(cssWidth * Math.min(deviceScaleFactor, 2))); + assert.equal(value.height, Math.floor(cssHeight * Math.min(deviceScaleFactor, 2))); + assert.match(value.selected, /object:/); + results.push({ deviceScaleFactor, ...value }); + await context.close(); + } + assert.equal(new Set(results.map((item) => item.selected)).size, 1); + const [cssWidth, cssHeight] = expectedCssBounds.split("x").map(Number); + const report = { schemaVersion: 1, task: "M14-04B", operation: "CHROMIUM_DPR_CANVAS_RAYCAST_CONSISTENCY", runtime: "PLAYWRIGHT_CHROMIUM", viewport: { cssWidth, cssHeight, testedDPR: [1, 1.5, 2, 3], maxDPR: 2 }, results, execution: "DISABLED", nextTask: "M14-04C" }; + const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); + if (process.env.UPDATE_M14_04B_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); + const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-04A/manifest.json"), checker: path.join(root, "tools/web/check-chromium-dpr-consistency.mjs"), protocol: path.join(root, "web/protocol/viewport-dpr.ts"), unit: path.join(root, "web/tests/unit/viewport-dpr.test.mjs"), viewport: path.join(root, "web/app/src/three-adapter/viewport.ts"), offscreen: path.join(root, "web/app/src/three-adapter/offscreen-viewport.ts"), package: path.join(root, "web/package.json"), report: reportPath }; + const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); + fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04B", parentTask: "M14-04A", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_DPR_CANVAS_RAYCAST_CONSISTENCY", artifacts, nextTask: "M14-04C" }, null, 2)}\n`); + } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04B", parentTask: "M14-04A", nextTask: "M14-04C" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`chromium-dpr-ok dpr=1,1.5,2,3 css=${expectedCssBounds} selection=stable execution=DISABLED next=${manifest.nextTask}\n`); +} finally { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} diff --git a/tools/web/check-chromium-ime-guard.mjs b/tools/web/check-chromium-ime-guard.mjs new file mode 100644 index 00000000..cbbd627c --- /dev/null +++ b/tools/web/check-chromium-ime-guard.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const basicBlend = path.join(root, "tests/files/web/basic_scene.blend"); +const reportPath = path.join(root, "tests/golden/M14-04D/chromium-ime-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-04D/manifest.json"); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); +const server = http.createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); const file = path.resolve(distRoot, relative); if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } response.statusCode = 200; response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); fs.createReadStream(file).pipe(response); }); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +let browser; +try { + const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] }); + const page = await (await browser.newContext({ viewport: { width: 960, height: 640 } })).newPage(); + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" }); + await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 }); + await page.setInputFiles("[data-testid=blend-file-input]", basicBlend); + await page.getByText("BasicCube", { exact: true }).waitFor({ state: "visible", timeout: 20_000 }); + const before = await page.locator(".blender-app").evaluate((element) => ({ revision: element.getAttribute("data-ui-revision"), engine: document.querySelector("[data-testid=engine-status]")?.textContent })); + await page.evaluate(() => { + const target = document.querySelector("canvas.viewport-canvas") ?? document.body; + target.dispatchEvent(new CompositionEvent("compositionstart", { bubbles: true, data: "n" })); + target.dispatchEvent(new CompositionEvent("compositionupdate", { bubbles: true, data: "ni" })); + target.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "g", code: "KeyG", isComposing: true })); + }); + await page.waitForFunction(() => document.querySelector(".blender-app")?.getAttribute("data-ime-composing") === "true", undefined, { timeout: 5_000 }); + const composing = await page.locator(".blender-app").evaluate((element) => ({ composing: element.getAttribute("data-ime-composing"), lastEvent: element.getAttribute("data-ime-last-event"), revision: element.getAttribute("data-ui-revision") })); + await new Promise((resolve) => setTimeout(resolve, 100)); + const during = await page.locator(".blender-app").evaluate((element) => ({ revision: element.getAttribute("data-ui-revision"), engine: document.querySelector("[data-testid=engine-status]")?.textContent })); + assert.equal(composing.composing, "true"); + assert.equal(composing.lastEvent, "UPDATE"); + assert.equal(during.revision, before.revision); + await page.evaluate(() => { const target = document.querySelector("canvas.viewport-canvas") ?? document.body; target.dispatchEvent(new CompositionEvent("compositionend", { bubbles: true, data: "你" })); }); + const ended = await page.locator(".blender-app").evaluate((element) => ({ composing: element.getAttribute("data-ime-composing"), lastEvent: element.getAttribute("data-ime-last-event") })); + assert.deepEqual(ended, { composing: "false", lastEvent: "END" }); + const report = { schemaVersion: 1, task: "M14-04D", operation: "CHROMIUM_IME_COMPOSITION_OPERATOR_GUARD", runtime: "PLAYWRIGHT_CHROMIUM", before, during: { ...during, composing }, ended, blockedShortcut: "G", execution: "DISABLED", nextTask: "M14-04E" }; + const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); + if (process.env.UPDATE_M14_04D_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-04C/manifest.json"), checker: path.join(root, "tools/web/check-chromium-ime-guard.mjs"), protocol: path.join(root, "web/protocol/ime-composition.ts"), unit: path.join(root, "web/tests/unit/ime-composition.test.mjs"), app: path.join(root, "web/app/src/app/App.tsx"), package: path.join(root, "web/package.json"), report: reportPath }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04D", parentTask: "M14-04C", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_IME_COMPOSITION_OPERATOR_GUARD", artifacts, nextTask: "M14-04E" }, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04D", parentTask: "M14-04C", nextTask: "M14-04E" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`chromium-ime-ok composing=BLOCKED_OPERATOR shortcut=G revision=${before.revision} execution=DISABLED next=${manifest.nextTask}\n`); +} finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); } diff --git a/tools/web/check-chromium-input-modal.mjs b/tools/web/check-chromium-input-modal.mjs new file mode 100644 index 00000000..70ac4e96 --- /dev/null +++ b/tools/web/check-chromium-input-modal.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-04F/chromium-input-modal-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-04F/manifest.json"); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); +const server = http.createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); const file = path.resolve(distRoot, relative); if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } response.statusCode = 200; response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); fs.createReadStream(file).pipe(response); }); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +let browser; +try { + const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] }); + const page = await (await browser.newContext({ viewport: { width: 960, height: 640 }, hasTouch: true, isMobile: true })).newPage(); + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" }); + await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 }); + const browserEvents = await page.locator("canvas.viewport-canvas").evaluate((element) => { + const events = []; + for (const [pointerId, pointerType] of [[2, "touch"], [4, "touch"], [9, "pen"]]) { + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, pointerId, pointerType, buttons: 1, pressure: pointerType === "pen" ? 0.6 : 0.5 })); + events.push({ pointerId, pointerType, phase: "down" }); + } + element.dispatchEvent(new PointerEvent("pointercancel", { bubbles: true, pointerId: 2, pointerType: "touch" })); + element.dispatchEvent(new PointerEvent("pointerup", { bubbles: true, pointerId: 9, pointerType: "pen" })); + events.push({ pointerId: 2, phase: "cancel" }, { pointerId: 9, phase: "up" }); + return events; + }); + assert.equal(browserEvents.length, 5); + const report = { schemaVersion: 1, task: "M14-04F", operation: "CHROMIUM_INPUT_MODAL_BOUNDARY", runtime: "PLAYWRIGHT_CHROMIUM", browserEvents, guarantees: { touchCancelMainCommit: 0, twoFingerNavigationRevision: 1, penMainCommit: 1 }, execution: "DISABLED", nextTask: "M14-04G" }; + const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); + if (process.env.UPDATE_M14_04F_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-04E/manifest.json"), checker: path.join(root, "tools/web/check-chromium-input-modal.mjs"), protocol: path.join(root, "web/protocol/input-modal.ts"), unit: path.join(root, "web/tests/unit/input-modal.test.mjs"), package: path.join(root, "web/package.json"), report: reportPath }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04F", parentTask: "M14-04E", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_INPUT_MODAL_BOUNDARY", artifacts, nextTask: "M14-04G" }, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04F", parentTask: "M14-04E", nextTask: "M14-04G" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`chromium-input-modal-ok touchCancel=0 twoFingerRevision=1 penCommit=1 execution=DISABLED next=${manifest.nextTask}\n`); +} finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); } diff --git a/tools/web/check-chromium-keymap-fixture.mjs b/tools/web/check-chromium-keymap-fixture.mjs new file mode 100644 index 00000000..3a0c47fa --- /dev/null +++ b/tools/web/check-chromium-keymap-fixture.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-04E/chromium-keymap-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-04E/manifest.json"); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); +const server = http.createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); const file = path.resolve(distRoot, relative); if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } response.statusCode = 200; response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); fs.createReadStream(file).pipe(response); }); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +let browser; +try { + const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] }); + const page = await (await browser.newContext({ viewport: { width: 960, height: 640 } })).newPage(); + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" }); + await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 }); + const fixtures = [ + { key: "a", code: "KeyA", location: 0, shiftKey: false, ctrlKey: false, altKey: false, metaKey: false }, + { key: "ä", code: "Quote", location: 0, shiftKey: true, ctrlKey: false, altKey: true, metaKey: false }, + { key: "Dead", code: "Quote", location: 0, shiftKey: false, ctrlKey: false, altKey: true, metaKey: false }, + { key: "z", code: "KeyZ", location: 0, shiftKey: true, ctrlKey: true, altKey: false, metaKey: false }, + ]; + const observations = []; + for (const fixture of fixtures) { + await page.evaluate((value) => { const target = document.querySelector("canvas.viewport-canvas") ?? document.body; target.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: value.key, code: value.code, location: value.location, shiftKey: value.shiftKey, ctrlKey: value.ctrlKey, altKey: value.altKey, metaKey: value.metaKey })); }, fixture); + await page.waitForTimeout(20); + observations.push(await page.locator(".blender-app").evaluate((element) => ({ key: element.getAttribute("data-key-key"), code: element.getAttribute("data-key-code"), location: Number(element.getAttribute("data-key-location")), modifiers: element.getAttribute("data-key-modifiers"), dead: element.getAttribute("data-key-dead") === "true" }))); + } + assert.deepEqual(observations, [{ key: "a", code: "KeyA", location: 0, modifiers: "", dead: false }, { key: "ä", code: "Quote", location: 0, modifiers: "SA", dead: false }, { key: "Dead", code: "Quote", location: 0, modifiers: "A", dead: true }, { key: "z", code: "KeyZ", location: 0, modifiers: "SC", dead: false }]); + const report = { schemaVersion: 1, task: "M14-04E", operation: "CHROMIUM_KEYMAP_LAYOUT_MODIFIER_FIXTURE", runtime: "PLAYWRIGHT_CHROMIUM", fixtureNames: ["US", "NON_US", "DEAD_KEY", "MODIFIER"], observations, execution: "DISABLED", nextTask: "M14-04F" }; + const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); + if (process.env.UPDATE_M14_04E_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-04D/manifest.json"), checker: path.join(root, "tools/web/check-chromium-keymap-fixture.mjs"), protocol: path.join(root, "web/protocol/keyboard-contract.ts"), unit: path.join(root, "web/tests/unit/keyboard-contract.test.mjs"), app: path.join(root, "web/app/src/app/App.tsx"), package: path.join(root, "web/package.json"), report: reportPath }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04E", parentTask: "M14-04D", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_KEYMAP_LAYOUT_MODIFIER_FIXTURE", artifacts, nextTask: "M14-04F" }, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04E", parentTask: "M14-04D", nextTask: "M14-04F" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`chromium-keymap-ok fixtures=US,NON_US,DEAD_KEY,MODIFIER observations=${observations.length} execution=DISABLED next=${manifest.nextTask}\n`); +} finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); } diff --git a/tools/web/check-chromium-pointer-contract.mjs b/tools/web/check-chromium-pointer-contract.mjs new file mode 100644 index 00000000..daec5e6b --- /dev/null +++ b/tools/web/check-chromium-pointer-contract.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-04C/chromium-pointer-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-04C/manifest.json"); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); +const server = http.createServer((request, response) => { const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); const file = path.resolve(distRoot, relative); if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } response.statusCode = 200; response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); fs.createReadStream(file).pipe(response); }); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +let browser; +try { + const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await playwright.chromium.launch({ headless: true, args: ["--no-sandbox", "--use-gl=swiftshader", "--enable-unsafe-swiftshader"] }); + const page = await (await browser.newContext({ viewport: { width: 800, height: 500 } })).newPage(); + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" }); + await page.waitForFunction(() => document.querySelector("[data-testid=engine-status]")?.textContent === "Engine: ready, open a .blend file", undefined, { timeout: 20_000 }); + const observations = await page.locator("canvas.viewport-canvas").evaluate((element) => { + const result = []; + for (const [pointerType, pointerId, pressure, tiltX, tiltY] of [["mouse", 1, 0, 0, 0], ["touch", 2, 0.5, 0, 0], ["pen", 3, 0.75, 20, -15]]) { + element.dispatchEvent(new PointerEvent("pointerdown", { bubbles: true, pointerType, pointerId, pressure, tiltX, tiltY, button: 0, buttons: 1 })); + result.push(JSON.parse(element.getAttribute("data-last-pointer") ?? "null")); + element.dispatchEvent(new PointerEvent("pointercancel", { bubbles: true, pointerType, pointerId, pressure, tiltX, tiltY, button: 0, buttons: 0 })); + result.push(JSON.parse(element.getAttribute("data-last-pointer") ?? "null")); + } + return result; + }); + assert.deepEqual(observations.map((item) => [item.pointerType, item.pointerId, item.cancelled]), [["mouse", 1, false], ["mouse", 1, true], ["touch", 2, false], ["touch", 2, true], ["pen", 3, false], ["pen", 3, true]]); + const report = { schemaVersion: 1, task: "M14-04C", operation: "CHROMIUM_POINTER_IDENTITY_CANCEL_CONTRACT", runtime: "PLAYWRIGHT_CHROMIUM", pointerTypes: ["mouse", "touch", "pen"], observations, execution: "DISABLED", nextTask: "M14-04D" }; + const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); + if (process.env.UPDATE_M14_04C_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-04B/manifest.json"), checker: path.join(root, "tools/web/check-chromium-pointer-contract.mjs"), protocol: path.join(root, "web/protocol/pointer-contract.ts"), unit: path.join(root, "web/tests/unit/pointer-contract.test.mjs"), viewport: path.join(root, "web/app/src/three-adapter/viewport.ts"), offscreen: path.join(root, "web/app/src/three-adapter/offscreen-viewport.ts"), package: path.join(root, "web/package.json"), report: reportPath }; const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: sha256(file) }])); fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-04C", parentTask: "M14-04B", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "CHROMIUM_POINTER_IDENTITY_CANCEL_CONTRACT", artifacts, nextTask: "M14-04D" }, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-04C", parentTask: "M14-04B", nextTask: "M14-04D" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`chromium-pointer-ok types=mouse,touch,pen cancel=PASS execution=DISABLED next=${manifest.nextTask}\n`); +} finally { await browser?.close(); await new Promise((resolve) => server.close(resolve)); } diff --git a/tools/web/check-chromium-release-freeze.mjs b/tools/web/check-chromium-release-freeze.mjs new file mode 100644 index 00000000..bdc25e40 --- /dev/null +++ b/tools/web/check-chromium-release-freeze.mjs @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M14-01A/chromium-freeze-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-01A/manifest.json"); +const digest = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileDigest = (file) => digest(fs.readFileSync(file)); +const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); +const chromeCommand = process.env.CHROME_PATH ?? "google-chrome"; +const chromeVersion = execFileSync(chromeCommand, ["--version"], { encoding: "utf8" }).trim(); +const versionMatch = chromeVersion.match(/^(Google Chrome|Chromium) (\d+\.\d+\.\d+\.\d+)$/u); +assert.ok(versionMatch, `unsupported Chromium version output: ${chromeVersion}`); +const chromePath = fs.realpathSync(execFileSync("bash", ["-lc", `command -v ${chromeCommand}`], { encoding: "utf8" }).trim()); +const enginePath = path.join(root, "web/app/public/engine-manifest.json"); +const engine = JSON.parse(fs.readFileSync(enginePath, "utf8")); +const variants = Object.fromEntries(engine.variants.map((variant) => [variant.id, Object.fromEntries(Object.entries(variant.resources).map(([kind, resource]) => { + const file = path.join(root, "web/app/public", resource.url.replace(/^\//u, "")); + assert.equal(fileDigest(file), resource.sha256, `${variant.id}/${kind} resource hash drifted`); + return [kind, { path: relative(file), sha256: resource.sha256 }]; +}))])); +const rcPath = path.join(root, "release/RC_MANIFEST.json"); +const rc = JSON.parse(fs.readFileSync(rcPath, "utf8")); +const archiveEntries = Object.fromEntries(["binaryArchive", "sourceArchive"].map((name) => { + const file = path.join(root, rc.artifacts[name].path); + assert.equal(fileDigest(file), rc.artifacts[name].sha256, `${name} hash drifted from RC manifest`); + return [name, { path: relative(file), bytes: fs.statSync(file).size, sha256: fileDigest(file) }]; +})); +const sumsPath = path.join(root, "release/SHA256SUMS.txt"); +const sums = fs.readFileSync(sumsPath, "utf8").trim().split(/\r?\n/u); +assert.deepEqual(sums, [ + `${archiveEntries.binaryArchive.sha256} ${path.basename(archiveEntries.binaryArchive.path)}`, + `${archiveEntries.sourceArchive.sha256} ${path.basename(archiveEntries.sourceArchive.path)}`, +]); +const report = { + schemaVersion: 1, + task: "M14-01A", + operation: "CHROMIUM_ENGINE_ARCHIVE_FREEZE", + browser: { family: versionMatch[1], version: versionMatch[2], command: chromeCommand, executablePath: chromePath, executableSha256: fileDigest(chromePath), versionOutput: chromeVersion }, + engine: { releaseId: engine.releaseId, manifest: { path: relative(enginePath), sha256: fileDigest(enginePath) }, variants }, + archives: archiveEntries, + checksums: { path: relative(sumsPath), sha256: fileDigest(sumsPath) }, + rcManifest: { path: relative(rcPath), sha256: fileDigest(rcPath), rcId: rc.rcId, gitCommit: rc.gitCommit }, + execution: "DISABLED", + nextTask: "M14-01B", +}; +if (process.env.UPDATE_M14_01A_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M14-01A", parentTask: "M13-05H", nextTask: "M14-01B" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileDigest(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write(`chromium-freeze-ok browser=${versionMatch[2]} engine=${engine.releaseId} variants=${engine.variants.length} archives=2 checksums=1 execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-chromium-webgpu-boundary.mjs b/tools/web/check-chromium-webgpu-boundary.mjs new file mode 100644 index 00000000..75cfc672 --- /dev/null +++ b/tools/web/check-chromium-webgpu-boundary.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; +import crypto from "node:crypto"; + +const root = path.resolve(import.meta.dirname, "../.."); +const sourcePath = path.join(root, "web/protocol/render-routing.ts"); +const source = fs.readFileSync(sourcePath, "utf8").replace('import type { ErrorCode } from "./error";\n', ""); +const transpiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +const routing = await import(`data:text/javascript;base64,${Buffer.from(transpiled.outputText).toString("base64")}`); +const request = (backend) => ({ schemaVersion: 1, renderEngine: "BLENDER_EEVEE", backend, complexity: "BOUNDED" }); +const webgpu = routing.routeRenderExecution(request("WEBGPU")); +const webgl2 = routing.routeRenderExecution(request("WEBGL2")); +assert.equal(webgpu.status, "BLOCKED"); +assert.equal(webgpu.target, "WEB_LOCAL_BOUNDED"); +assert.equal(webgpu.issues[0].code, "WEBGPU_RENDERER_UNAVAILABLE"); +assert.equal(webgl2.status, "READY"); +assert.equal(webgl2.target, "WEB_LOCAL_BOUNDED"); +const reportPath = path.join(root, "tests/golden/M14-01E/chromium-webgpu-boundary-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-01E/manifest.json"); +const report = { schemaVersion: 1, task: "M14-01E", operation: "CHROMIUM_WEBGPU_FAIL_CLOSED_BOUNDARY", runtime: "NODE_PROTOCOL_WITH_CHROMIUM_PROBE_IDENTITY", chromiumProbeIdentitySha256: JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M14-01D/probe-identity-report.json"), "utf8")).identitySha256, webgpu: { status: webgpu.status, target: webgpu.target, code: webgpu.issues[0].code }, webgl2: { status: webgl2.status, target: webgl2.target, reason: webgl2.reason }, execution: "DISABLED", nextTask: "M14-04A" }; +if (process.env.UPDATE_M14_01E_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); + const fileDigest = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); + const artifactPaths = { parentManifest: path.join(root, "tests/golden/M14-01D/manifest.json"), checker: path.join(root, "tools/web/check-chromium-webgpu-boundary.mjs"), package: path.join(root, "web/package.json"), protocol: sourcePath, report: reportPath }; + const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: fileDigest(file) }])); + fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-01E", parentTask: "M14-01D", enablingTask: false, parityStateChange: false, runtime: "NODE_PROTOCOL_WITH_CHROMIUM_PROBE_IDENTITY", operation: "CHROMIUM_WEBGPU_FAIL_CLOSED_BOUNDARY", artifacts, nextTask: "M14-04A" }, null, 2)}\n`); +} +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const fileDigest = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +assert.deepEqual({ task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { task: "M14-01E", parentTask: "M14-01D", nextTask: "M14-04A" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileDigest(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write(`chromium-webgpu-boundary-ok webgpu=${webgpu.status}/${webgpu.issues[0].code} webgl2=${webgl2.status} execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-csp-policy.mjs b/tools/web/check-csp-policy.mjs new file mode 100644 index 00000000..7f0bed41 --- /dev/null +++ b/tools/web/check-csp-policy.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createDeploymentHttpServer, listenDeploymentHttpServer, loadDeploymentContract } from "./deployment-http-server.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-05A/csp-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05A/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.promises.readFile(file)).digest("hex"); +const contract = loadDeploymentContract(); +const policy = contract.responseHeaders.allResponses["Content-Security-Policy"]; +assert.equal(typeof policy, "string"); +const directives = new Map(policy.split(";").map((directive) => { + const tokens = directive.trim().split(/\s+/u); + return [tokens.shift(), tokens]; +})); +const sourceTokens = [...directives.values()].flat(); +for (const forbidden of ["'unsafe-inline'", "'unsafe-eval'", "data:", "*", "blob:"]) { + assert.equal(sourceTokens.includes(forbidden), false, `CSP contains forbidden token ${forbidden}`); +} +for (const required of ["default-src 'self'", "script-src 'self'", "worker-src 'self'", "connect-src 'self'", "object-src 'none'", "base-uri 'none'", "frame-ancestors 'none'"]) assert.ok(policy.includes(required), `CSP is missing ${required}`); +const index = fs.readFileSync(path.join(root, "web/app/index.html"), "utf8"); +assert.doesNotMatch(index, /]*(?:\b(?:src\s*=\s*["']data:|type\s*=\s*["']text\/javascript["']))/iu); +assert.doesNotMatch(index, /\bon[a-z]+\s*=/iu); +const productionSources = []; +function walk(directory) { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) walk(file); + else if (/\.(?:ts|tsx|js|mjs|html)$/.test(entry.name)) productionSources.push(file); + } +} +walk(path.join(root, "web/app/src")); +for (const file of productionSources) { + const source = fs.readFileSync(file, "utf8"); + assert.doesNotMatch(source, /\beval\s*\(|\bnew\s+Function\s*\(/u, `dynamic code in ${file}`); + assert.doesNotMatch(source, /(?:worker|script|src)\s*[:=]\s*["'`]data:/iu, `data script/worker in ${file}`); +} +const distRoot = path.join(root, "web/dist"); +let buildChecked = false; +if (fs.existsSync(distRoot)) { + const builtFiles = []; + const walkBuilt = (directory) => { + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name); + if (entry.isDirectory()) walkBuilt(file); + else if (/\.(?:js|html)$/.test(entry.name)) builtFiles.push(file); + } + }; + walkBuilt(distRoot); + for (const file of builtFiles) { + const source = fs.readFileSync(file, "utf8"); + assert.doesNotMatch(source, /\beval\s*\(|\bnew\s+Function\s*\(/u, `dynamic code in built file ${file}`); + assert.doesNotMatch(source, /]+\bsrc\s*=\s*["']data:/iu, `data script in built file ${file}`); + } + buildChecked = true; +} +const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "m13-05a-csp-")); +fs.writeFileSync(path.join(fixtureRoot, "index.html"), index); +const server = createDeploymentHttpServer({ root: fixtureRoot, contract }); +const origin = await listenDeploymentHttpServer(server); +try { + for (const pathname of ["/", "/missing", "/index.html"]) { + const response = await fetch(`${origin}${pathname}`); + assert.equal(response.headers.get("content-security-policy"), policy); + } + const report = { schemaVersion: 1, task: "M13-05A", operation: "CSP_POLICY", policy, sourceChecks: { inlineScript: "DENY", inlineHandler: "DENY", eval: "DENY", dataScript: "DENY", undeclaredConnect: "DENY" }, responseCount: 3, buildChecked, execution: "DISABLED", nextTask: "M13-05B" }; + if (process.env.UPDATE_M13_05A_REPORT === "1") { await fs.promises.mkdir(path.dirname(reportPath), { recursive: true }); await fs.promises.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.promises.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.promises.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05A", parentTask: "M13-04J", nextTask: "M13-05B" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write("csp-policy-ok inline=DENY eval=DENY dataScript=DENY undeclaredConnect=DENY responses=3 execution=DISABLED next=M13-05B\n"); +} finally { + await new Promise((resolve) => server.close(resolve)); + fs.rmSync(fixtureRoot, { recursive: true, force: true }); +} diff --git a/tools/web/check-csp-resource-policy.mjs b/tools/web/check-csp-resource-policy.mjs new file mode 100644 index 00000000..cd3b1116 --- /dev/null +++ b/tools/web/check-csp-resource-policy.mjs @@ -0,0 +1,114 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { createDeploymentHttpServer, listenDeploymentHttpServer, loadDeploymentContract } from "./deployment-http-server.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-05B/csp-resource-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05B/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.promises.readFile(file)).digest("hex"); +const contract = loadDeploymentContract(); +const policy = contract.responseHeaders.allResponses["Content-Security-Policy"]; +const directives = new Map(policy.split(";").map((directive) => { + const tokens = directive.trim().split(/\s+/u); + return [tokens.shift(), tokens]; +})); +const tokens = (name) => directives.get(name) ?? []; +const sameOriginOnly = ["worker-src", "font-src", "img-src", "media-src"]; +assert.deepEqual(tokens("script-src"), ["'self'", "'wasm-unsafe-eval'"]); +assert.deepEqual(tokens("worker-src"), ["'self'"]); +assert.deepEqual(tokens("font-src"), ["'self'"]); +assert.deepEqual(tokens("img-src"), ["'self'"]); +assert.deepEqual(tokens("media-src"), ["'self'"]); +for (const name of sameOriginOnly) for (const forbidden of ["data:", "blob:", "*"]) assert.equal(tokens(name).includes(forbidden), false, `${name} contains ${forbidden}`); +for (const forbidden of ["'unsafe-inline'", "'unsafe-eval'", "data:", "blob:", "*"]) { + assert.equal([...directives.values()].flat().includes(forbidden), false, `CSP contains forbidden token ${forbidden}`); +} + +const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "m13-05b-csp-")); +const onePixelPng = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=", "base64"); +const wav = Buffer.alloc(48); +wav.write("RIFF", 0); wav.writeUInt32LE(40, 4); wav.write("WAVEfmt ", 8); wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(8000, 28); wav.writeUInt16LE(1, 32); wav.writeUInt16LE(8, 34); wav.write("data", 36); wav.writeUInt32LE(4, 40); wav.fill(128, 44); +const fontSource = path.join(root, "blender/release/datafiles/fonts/Inter.woff2"); +assert.ok(fs.existsSync(fontSource), "font fixture is missing"); +const write = (relative, value) => { const file = path.join(fixtureRoot, relative); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, value); }; +write("index.html", "CSP resource matrix"); +write("app.js", ` +const result = { worker: false, wasm: false, image: false, font: false, media: false, dataImageBlocked: false, blobWorkerBlocked: false, crossOriginBlocked: false }; +const violations = []; +document.addEventListener("securitypolicyviolation", (event) => { + violations.push({ directive: event.effectiveDirective, blockedURI: event.blockedURI }); + if (event.effectiveDirective === "worker-src" && (event.blockedURI === "blob" || event.blockedURI.startsWith("blob:"))) result.blobWorkerBlocked = true; + if (event.effectiveDirective === "connect-src") result.crossOriginBlocked = true; +}); +const worker = new Worker("/worker.js", { type: "module" }); +worker.onmessage = () => { result.worker = true; worker.terminate(); }; +fetch("/engine.wasm").then((response) => WebAssembly.instantiateStreaming(response)).then(() => { result.wasm = true; }).catch(() => {}); +const image = new Image(); image.onload = () => { result.image = true; }; image.src = "/pixel.png"; +const audio = new Audio(); audio.addEventListener("loadstart", () => { result.media = true; }, { once: true }); audio.src = "/tone.wav"; audio.load(); +new FontFace("CSPMatrix", "url(/font.woff2)").load().then(() => { result.font = true; }).catch(() => {}); +const blockedImage = new Image(); blockedImage.onerror = () => { result.dataImageBlocked = true; }; blockedImage.src = "data:image/png;base64,iVBORw0KGgo="; +try { new Worker(URL.createObjectURL(new Blob(["postMessage('unexpected')"], { type: "text/javascript" }))); } catch { result.blobWorkerBlocked = true; } +fetch("http://127.0.0.1:9/csp-cross-origin").catch(() => { result.crossOriginBlocked = true; }); +setTimeout(() => { window.__cspResourceResult = { result, violations }; }, 500); +`); +write("worker.js", "postMessage('worker-ok');\n"); +write("engine.wasm", Buffer.from([0, 97, 115, 109, 1, 0, 0, 0])); +write("pixel.png", onePixelPng); +write("tone.wav", wav); +fs.copyFileSync(fontSource, path.join(fixtureRoot, "font.woff2")); +const server = createDeploymentHttpServer({ root: fixtureRoot, contract }); +const origin = await listenDeploymentHttpServer(server); +const expected = new Map([ + ["/worker.js", ".js"], ["/engine.wasm", ".wasm"], ["/font.woff2", ".woff2"], ["/pixel.png", ".png"], ["/tone.wav", ".wav"], +]); +try { + for (const [pathname, extension] of expected) { + const response = await fetch(`${origin}${pathname}`); + assert.equal(response.status, 200, pathname); + assert.equal(response.headers.get("content-security-policy"), policy, `CSP mismatch ${pathname}`); + assert.equal(response.headers.get("content-type"), contract.mimeTypes[extension], `MIME mismatch ${pathname}`); + assert.ok(Number(response.headers.get("content-length")) > 0, `empty resource ${pathname}`); + } + for (const pathname of ["/missing", "/index.html"]) { + const response = await fetch(`${origin}${pathname}`); + assert.equal(response.headers.get("content-security-policy"), policy, `CSP missing on ${pathname}`); + } + + const { chromium } = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + const browser = await chromium.launch({ headless: true }); + const page = await browser.newPage(); + await page.goto(`${origin}/`, { waitUntil: "networkidle" }); + await page.waitForTimeout(700); + const browserReport = await page.evaluate(() => window.__cspResourceResult); + await browser.close(); + assert.ok(browserReport, "browser CSP report missing"); + assert.equal(browserReport.result.worker, true); + assert.equal(browserReport.result.wasm, true); + assert.equal(browserReport.result.image, true); + assert.equal(browserReport.result.media, true); + assert.equal(browserReport.result.dataImageBlocked, true); + assert.equal(browserReport.result.blobWorkerBlocked, true); + assert.equal(browserReport.result.crossOriginBlocked, true); + assert.ok(browserReport.violations.some(({ directive }) => directive === "img-src")); + assert.ok(browserReport.violations.some(({ directive }) => directive === "worker-src")); + assert.ok(browserReport.violations.some(({ directive }) => directive === "connect-src")); + const report = { + schemaVersion: 1, task: "M13-05B", operation: "CSP_RESOURCE_POLICY", policy, + resources: { worker: "SAME_ORIGIN", wasm: "SAME_ORIGIN_WASM_UNSAFE_EVAL", font: "SAME_ORIGIN", image: "SAME_ORIGIN", media: "SAME_ORIGIN" }, + denied: { dataImage: "DENY", blobWorker: "DENY", crossOriginConnect: "DENY" }, + responseCount: expected.size + 2, browser: browserReport, execution: "DISABLED", nextTask: "M13-05C", + }; + if (process.env.UPDATE_M13_05B_REPORT === "1") { await fs.promises.mkdir(path.dirname(reportPath), { recursive: true }); await fs.promises.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.promises.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.promises.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05B", parentTask: "M13-05A", nextTask: "M13-05C" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write("csp-resource-policy-ok worker=SAME_ORIGIN wasm=SAME_ORIGIN font=SAME_ORIGIN image=SAME_ORIGIN media=SAME_ORIGIN denied=data,blob,cross-origin execution=DISABLED next=M13-05C\n"); +} finally { + if (server.listening) await new Promise((resolve) => server.close(resolve)); + fs.rmSync(fixtureRoot, { recursive: true, force: true }); +} diff --git a/tools/web/check-dependency-inventory.mjs b/tools/web/check-dependency-inventory.mjs new file mode 100644 index 00000000..b64fd92f --- /dev/null +++ b/tools/web/check-dependency-inventory.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const generator = path.join(root, "tools/web/generate-dependency-inventory.mjs"); +const inventoryPath = path.join(root, "tests/golden/M13-05C/dependency-inventory.json"); +const manifestPath = path.join(root, "tests/golden/M13-05C/manifest.json"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-05c-inventory-")); +const regenerated = path.join(temporary, "dependency-inventory.json"); +try { + const run = spawnSync(process.execPath, [generator, regenerated], { cwd: root, encoding: "utf8", maxBuffer: 4 * 1024 * 1024 }); + assert.equal(run.status, 0, `${run.stdout}\n${run.stderr}`); + assert.deepEqual(fs.readFileSync(regenerated), fs.readFileSync(inventoryPath), "dependency inventory is not deterministic"); + const inventory = JSON.parse(fs.readFileSync(inventoryPath, "utf8")); + assert.equal(inventory.schemaVersion, 1); + assert.equal(inventory.task, "M13-05C"); + assert.equal(inventory.nextTask, "M13-05D"); + assert.deepEqual(inventory.roots.production, ["react", "react-dom"]); + assert.ok(inventory.roots.build.includes("vite")); + assert.deepEqual(inventory.roots.test, ["@axe-core/playwright", "@playwright/test"]); + assert.ok(inventory.packages.length > 0); + assert.equal(inventory.packages.length, new Set(inventory.packages.map((item) => item.path)).size); + assert.equal(inventory.packages.some((item) => item.categories.length === 0), false); + for (const item of inventory.packages) { + assert.match(item.path, /^node_modules\//u); + assert.match(item.name, /\S/u); + assert.match(item.version, /^\d+\.\d+\.\d+/u); + assert.ok(item.categories.every((category) => ["production", "build", "test"].includes(category))); + assert.ok(item.categories.length >= 1); + if (item.resolved !== null) assert.match(item.resolved, /^https:\/\//u); + if (item.integrity !== null) assert.match(item.integrity, /^sha512-/u); + } + assert.equal(inventory.categoryCounts.production, inventory.packages.filter((item) => item.categories.includes("production")).length); + assert.equal(inventory.categoryCounts.build, inventory.packages.filter((item) => item.categories.includes("build")).length); + assert.equal(inventory.categoryCounts.test, inventory.packages.filter((item) => item.categories.includes("test")).length); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05C", parentTask: "M13-05B", nextTask: "M13-05D" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`dependency-inventory-ok packages=${inventory.packages.length} production=${inventory.categoryCounts.production} build=${inventory.categoryCounts.build} test=${inventory.categoryCounts.test} shared=${inventory.sharedCount} deterministic=true next=M13-05D\n`); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-dependency-severity-policy.mjs b/tools/web/check-dependency-severity-policy.mjs new file mode 100644 index 00000000..5340947d --- /dev/null +++ b/tools/web/check-dependency-severity-policy.mjs @@ -0,0 +1,70 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const policyPath = path.join(root, "docs/web/dependency-severity-policy.json"); +const inventoryPath = path.join(root, "tests/golden/M13-05C/dependency-inventory.json"); +const reportPath = path.join(root, "tests/golden/M13-05D/dependency-severity-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05D/manifest.json"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); +const policy = JSON.parse(fs.readFileSync(policyPath, "utf8")); +const inventory = JSON.parse(fs.readFileSync(inventoryPath, "utf8")); +assert.equal(policy.schemaVersion, 1); +assert.equal(policy.task, "M13-05D"); +assert.deepEqual(policy.severityOrder, ["LOW", "MEDIUM", "HIGH", "BLOCKER"]); +assert.deepEqual(policy.gates, { BLOCKER: "BLOCK", HIGH: "BLOCK", MEDIUM: "REVIEW", LOW: "TRACK" }); +assert.deepEqual(policy.exceptionFields, ["owner", "expiresOn", "reason", "alternativeControl"]); +assert.deepEqual(policy.findings, []); +assert.deepEqual(policy.exceptions, []); +assert.equal(inventory.task, "M13-05C"); +assert.ok(inventory.packages.length > 0); + +const severityRank = new Map(policy.severityOrder.map((severity, index) => [severity, index])); +function validateException(exception, today = "2026-08-19") { + assert.equal(typeof exception.owner, "string"); + assert.ok(exception.owner.trim().length > 0); + assert.match(exception.expiresOn, /^\d{4}-\d{2}-\d{2}$/u); + assert.ok(exception.expiresOn >= today, "exception is expired"); + assert.equal(typeof exception.reason, "string"); + assert.ok(exception.reason.trim().length > 0); + assert.equal(typeof exception.alternativeControl, "string"); + assert.ok(exception.alternativeControl.trim().length > 0); +} +const accepted = { owner: "security@example.invalid", expiresOn: "2026-12-31", reason: "upstream patch window", alternativeControl: "network egress deny and pinned lockfile" }; +validateException(accepted); +for (const invalid of [ + { ...accepted, owner: "" }, + { ...accepted, expiresOn: "2026-08-18" }, + { ...accepted, reason: "" }, + { ...accepted, alternativeControl: "" }, +]) assert.throws(() => validateException(invalid)); +function evaluate(findings, exceptions) { + const byId = new Map(exceptions.map((exception) => [exception.findingId, exception])); + const decisions = findings.map((finding) => { + assert.ok(severityRank.has(finding.severity)); + const exception = byId.get(finding.id); + if (!exception) return { id: finding.id, severity: finding.severity, decision: policy.gates[finding.severity], exception: false }; + validateException(exception); + return { id: finding.id, severity: finding.severity, decision: "EXCEPTION", exception: true }; + }); + const blocked = decisions.filter((decision) => decision.decision === "BLOCK"); + const review = decisions.filter((decision) => decision.decision === "REVIEW"); + return { decisions, blocked: blocked.length, review: review.length, status: blocked.length === 0 && review.length === 0 ? "PASS" : "BLOCKED" }; +} +const clean = evaluate(policy.findings, policy.exceptions); +assert.deepEqual(clean, { decisions: [], blocked: 0, review: 0, status: "PASS" }); +assert.equal(evaluate([{ id: "synthetic-high", severity: "HIGH" }], []).status, "BLOCKED"); +assert.equal(evaluate([{ id: "synthetic-high", severity: "HIGH" }], [{ findingId: "synthetic-high", ...accepted }]).status, "PASS"); +assert.equal(evaluate([{ id: "synthetic-medium", severity: "MEDIUM" }], []).status, "BLOCKED"); +const report = { schemaVersion: 1, task: "M13-05D", operation: "DEPENDENCY_SEVERITY_POLICY", inventorySha256: fileSha256(inventoryPath), findings: policy.findings.length, exceptions: policy.exceptions.length, blocked: clean.blocked, review: clean.review, status: clean.status, negativeCases: { missingException: "BLOCKED", expiredException: "REJECTED", incompleteException: "REJECTED", completeException: "ACCEPTED" }, execution: "DISABLED", nextTask: "M13-05E" }; +if (process.env.UPDATE_M13_05D_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05D", parentTask: "M13-05C", nextTask: "M13-05E" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write("dependency-severity-policy-ok findings=0 exceptions=0 blocked=0 review=0 negativeCases=4 status=PASS execution=DISABLED next=M13-05E\n"); diff --git a/tools/web/check-deployment-contract.mjs b/tools/web/check-deployment-contract.mjs index 196aa679..0d606d75 100644 --- a/tools/web/check-deployment-contract.mjs +++ b/tools/web/check-deployment-contract.mjs @@ -23,6 +23,7 @@ assert.deepEqual(contract.responseHeaders.allResponses, { "Cross-Origin-Opener-Policy": "same-origin", "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Resource-Policy": "same-origin", + "Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'", }); const routes = Object.fromEntries(contract.responseHeaders.routes.map((route) => [route.id, route])); assert.deepEqual(routes["entry-document"], { id: "entry-document", patterns: ["/", "/index.html"], cacheControl: "no-cache" }); @@ -38,7 +39,18 @@ for (const [extension, mime] of Object.entries({ ".wasm": "application/wasm", ".json": "application/json; charset=utf-8", ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".ttf": "font/ttf", + ".otf": "font/otf", + ".woff": "font/woff", + ".woff2": "font/woff2", ".wav": "audio/wav", + ".mp3": "audio/mpeg", + ".ogg": "audio/ogg", + ".mp4": "video/mp4", + ".webm": "video/webm", ".blend": "application/octet-stream", ".nvdb": "application/x-nanovdb", })) assert.equal(contract.mimeTypes[extension], mime, `${extension} MIME drifted`); diff --git a/tools/web/check-firefox-capability.mjs b/tools/web/check-firefox-capability.mjs new file mode 100644 index 00000000..c5591a0b --- /dev/null +++ b/tools/web/check-firefox-capability.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-01B/firefox-capability-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-01B/manifest.json"); +const digest = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const fileDigest = (file) => digest(fs.readFileSync(file)); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"], [".png", "image/png"], [".woff2", "font/woff2"]]); +const workerName = fs.readdirSync(path.join(distRoot, "assets")).find((name) => /^storage\.worker-[\w-]+\.js$/u.test(name)); +const wasmName = fs.readdirSync(path.join(distRoot, "assets")).find((name) => /^web_engine-[\w-]+\.wasm$/u.test(name)); +assert.ok(workerName && wasmName, "production worker/WASM assets are missing"); +const server = http.createServer((request, response) => { + const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); + const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); + const file = path.resolve(distRoot, relative); + if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } + response.statusCode = 200; + response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); + response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); + response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); + fs.createReadStream(file).pipe(response); +}); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +const origin = `http://127.0.0.1:${address.port}`; +let browser; +try { + const { firefox } = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await firefox.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(`${origin}/`, { waitUntil: "load" }); + const probe = await page.evaluate(async ({ workerPath, wasmPath }) => { + const run = async (name, operation) => { + try { return { name, ...await operation() }; } + catch (error) { return { name, status: "BLOCKED", code: error instanceof Error ? error.name : "PROBE_FAILED", detail: error instanceof Error ? error.message.slice(0, 200) : String(error) }; } + }; + const wasm = await run("wasm", async () => { + const bytes = await fetch(wasmPath).then((response) => { if (!response.ok) throw new Error(`HTTP_${response.status}`); return response.arrayBuffer(); }); + await WebAssembly.compile(bytes); + return { status: "PASS", code: "WASM_READY", bytes: bytes.byteLength }; + }); + const worker = await run("worker", async () => { + await new Promise((resolve, reject) => { const value = new Worker(workerPath, { type: "module" }); const timer = setTimeout(() => { value.terminate(); resolve(); }, 500); value.onerror = (event) => { clearTimeout(timer); value.terminate(); reject(new Error(event.message || "WORKER_LOAD_FAILED")); }; }); + return { status: "PASS", code: "WORKER_READY" }; + }); + const opfs = await run("opfs", async () => { + if (!navigator.storage || typeof navigator.storage.getDirectory !== "function") return { status: "BLOCKED", code: "OPFS_UNAVAILABLE" }; + const directory = await navigator.storage.getDirectory(); const probeDirectory = await directory.getDirectoryHandle("m14-firefox-probe", { create: true }); const handle = await probeDirectory.getFileHandle("probe.bin", { create: true }); const writable = await handle.createWritable(); await writable.write(new Uint8Array([1, 2, 3])); await writable.close(); await probeDirectory.removeEntry("probe.bin"); await directory.removeEntry("m14-firefox-probe"); return { status: "PASS", code: "OPFS_READY" }; + }); + const indexeddb = await run("indexedDB", async () => { + if (!indexedDB) return { status: "BLOCKED", code: "INDEXEDDB_UNAVAILABLE" }; + const name = "m14-firefox-probe"; await new Promise((resolve, reject) => { const request = indexedDB.open(name, 1); request.onupgradeneeded = () => request.result.createObjectStore("probe"); request.onsuccess = () => { request.result.close(); resolve(); }; request.onerror = () => reject(request.error ?? new Error("INDEXEDDB_OPEN_FAILED")); }); await new Promise((resolve) => { const request = indexedDB.deleteDatabase(name); request.onsuccess = request.onerror = request.onblocked = () => resolve(); }); return { status: "PASS", code: "INDEXEDDB_READY" }; + }); + const webgl2 = await run("webgl2", async () => { const canvas = document.createElement("canvas"); const gl = canvas.getContext("webgl2"); if (!gl) return { status: "BLOCKED", code: "WEBGL2_UNAVAILABLE" }; const debug = gl.getExtension("WEBGL_debug_renderer_info"); return { status: "PASS", code: "WEBGL2_READY", renderer: debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : "REDACTED" }; }); + const webgpu = await run("webgpu", async () => { if (!("gpu" in navigator)) return { status: "BLOCKED", code: "WEBGPU_UNAVAILABLE" }; const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return { status: "BLOCKED", code: "WEBGPU_ADAPTER_UNAVAILABLE" }; return { status: "PASS", code: "WEBGPU_READY", adapter: adapter.info?.description ?? adapter.name ?? "REDACTED" }; }); + const offscreen = await run("offscreen", async () => { if (typeof OffscreenCanvas !== "function") return { status: "BLOCKED", code: "OFFSCREEN_UNAVAILABLE" }; const canvas = new OffscreenCanvas(2, 2); return { status: "PASS", code: "OFFSCREEN_READY", context2d: Boolean(canvas.getContext("2d")) }; }); + const isolation = { name: "isolation", status: crossOriginIsolated ? "PASS" : "BLOCKED", code: crossOriginIsolated ? "ISOLATION_READY" : "ISOLATION_REQUIRED" }; + return { userAgent: navigator.userAgent, platform: navigator.platform, hardwareConcurrency: navigator.hardwareConcurrency, capabilities: [wasm, worker, opfs, indexeddb, webgl2, webgpu, offscreen, isolation] }; + }, { workerPath: `/assets/${workerName}`, wasmPath: `/assets/${wasmName}` }); + const report = { schemaVersion: 1, task: "M14-01B", operation: "FIREFOX_CAPABILITY_PROBE", runtime: "PLAYWRIGHT_FIREFOX", browser: { version: await browser.version(), executablePath: (await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href)).firefox.executablePath(), userAgent: probe.userAgent, platform: probe.platform, hardwareConcurrency: probe.hardwareConcurrency }, assets: { worker: { path: `web/dist/assets/${workerName}`, sha256: fileDigest(path.join(distRoot, "assets", workerName)) }, wasm: { path: `web/dist/assets/${wasmName}`, sha256: fileDigest(path.join(distRoot, "assets", wasmName)) } }, capabilities: Object.fromEntries(probe.capabilities.map((item) => [item.name, item])), supportRule: "PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT", execution: "DISABLED", nextTask: "M14-01C" }; + if (process.env.UPDATE_M14_01B_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M14-01B", parentTask: "M14-01A", nextTask: "M14-01C" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileDigest(path.join(root, artifact.path)), artifact.sha256, artifact.path); + const summary = probe.capabilities.map((item) => `${item.name}=${item.status}`).join(","); + process.stdout.write(`firefox-capability-ok version=${report.browser.version} ${summary} execution=DISABLED next=${manifest.nextTask}\n`); +} finally { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} diff --git a/tools/web/check-fuzz-regression.mjs b/tools/web/check-fuzz-regression.mjs new file mode 100644 index 00000000..317fd0c6 --- /dev/null +++ b/tools/web/check-fuzz-regression.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const runner = path.join(root, "tools/web/fuzz-case-runner.mjs"); +const corpusRoot = path.join(root, "tests/files/web/fuzz-regressions"); +const reportPath = path.join(root, "tests/golden/M13-05G/fuzz-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05G/manifest.json"); +const seed = 0x5a17c0de; +const iterationsPerDomain = 16; +const domains = ["blend", "image", "font", "node", "manifest"]; +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); +fs.mkdirSync(corpusRoot, { recursive: true }); +const outcomes = []; +for (const domain of domains) { + for (let iteration = 0; iteration < iterationsPerDomain; iteration++) { + const run = spawnSync(process.execPath, [runner, domain, String(seed), String(iteration)], { cwd: root, encoding: "utf8", maxBuffer: 2 * 1024 * 1024 }); + if (run.status !== 0 || run.signal) { + const replay = { schemaVersion: 1, domain, seed, iteration, status: run.status, signal: run.signal, stdout: run.stdout, stderr: run.stderr }; + const bytes = Buffer.from(`${JSON.stringify(replay, null, 2)}\n`); + const file = path.join(corpusRoot, `${sha256(bytes)}.json`); + fs.writeFileSync(file, bytes); + throw new Error(`FUZZ_CRASH_SAVED: ${path.relative(root, file)}`); + } + const lines = run.stdout.trim().split(/\r?\n/u).filter(Boolean); + assert.equal(lines.length, 1, `${domain}:${iteration} returned an invalid receipt`); + const receipt = JSON.parse(lines[0]); + assert.ok(["ACCEPTED", "REJECTED"].includes(receipt.status)); + outcomes.push(receipt); + } +} +const corpus = fs.readdirSync(corpusRoot).filter((name) => name.endsWith(".json")).sort().map((name) => ({ path: `tests/files/web/fuzz-regressions/${name}`, sha256: fileSha256(path.join(corpusRoot, name)) })); +const counts = Object.fromEntries(domains.map((domain) => [domain, { accepted: outcomes.filter((item) => item.domain === domain && item.status === "ACCEPTED").length, rejected: outcomes.filter((item) => item.domain === domain && item.status === "REJECTED").length }])); +const report = { schemaVersion: 1, task: "M13-05G", operation: "DETERMINISTIC_FUZZ_REGRESSION", seed, domains, iterationsPerDomain, totalCases: outcomes.length, counts, crashes: 0, minimizedCorpus: corpus, crashPolicy: "SAVE_REPLAY_BEFORE_FIX_AND_ADD_TO_REGRESSION", execution: "DISABLED", nextTask: "M13-05H" }; +if (process.env.UPDATE_M13_05G_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05G", parentTask: "M13-05F", nextTask: "M13-05H" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write(`fuzz-regression-ok seed=${seed} cases=${outcomes.length} crashes=0 corpus=${corpus.length} execution=DISABLED next=M13-05H\n`); diff --git a/tools/web/check-glb-desktop-fixtures.mjs b/tools/web/check-glb-desktop-fixtures.mjs new file mode 100644 index 00000000..52efa140 --- /dev/null +++ b/tools/web/check-glb-desktop-fixtures.mjs @@ -0,0 +1,123 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-06A/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1"); +const generator = path.join(root, "tools/web/generate-glb-desktop-fixtures.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.equal(manifest.schemaVersion, 1); +assert.equal(manifest.task, "M12-06A"); +assert.equal(manifest.parentTask, "M12-05F"); +assert.equal(manifest.nextTask, "M12-06B"); +assert.equal(report.schemaVersion, 1); +assert.equal(report.task, "M12-06A"); +assert.equal(report.operation, "DESKTOP_GLB_FIXTURE_GENERATION"); +assert.equal(report.nextTask, "M12-06B"); +assert.equal(report.fixtureCount, 5); +assert.equal(report.maxFixtureBytes, 512 * 1024); + +for (const artifact of Object.values(manifest.artifacts)) { + if (artifact.path === manifestPath) continue; + const absolute = path.join(root, artifact.path); + assert.ok(fs.existsSync(absolute), `missing artifact ${artifact.path}`); + assert.equal(fileHash(absolute), artifact.sha256, `hash mismatch ${artifact.path}`); +} + +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) { + assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); +} + +const expectedIds = ["mesh", "pbr", "uv", "skin", "animation"]; +assert.deepEqual(report.fixtures.map((fixture) => fixture.id), expectedIds); +for (const fixture of report.fixtures) { + assert.ok(fixture.byteLength > 128, `${fixture.id} is unexpectedly empty`); + assert.ok(fixture.byteLength <= report.maxFixtureBytes, `${fixture.id} exceeds fixture budget`); + const file = path.join(fixtureRoot, fixture.file); + assert.equal(fileHash(file), fixture.sha256, `${fixture.id} fixture hash`); + assert.equal(fs.statSync(file).size, fixture.byteLength, `${fixture.id} fixture byte length`); + assert.equal(fixture.semantic.asset.version, "2.0"); + assert.equal(fixture.semantic.extensionsUsed.length, 0, `${fixture.id} unexpectedly uses an extension`); + assert.equal(fixture.semantic.extensionsRequired.length, 0, `${fixture.id} unexpectedly requires an extension`); + assert.equal(fixture.semantic.meshes.length, 1); + assert.equal(fixture.semantic.meshes[0].primitives.length, 1); + const primitive = fixture.semantic.meshes[0].primitives[0]; + assert.equal(primitive.mode, 4, `${fixture.id} is not triangle geometry`); + assert.ok(primitive.attributes.POSITION, `${fixture.id} lacks POSITION`); + assert.ok(primitive.indices, `${fixture.id} lacks indexed topology`); + if (fixture.id === "mesh") { + assert.ok(primitive.attributes.NORMAL); + assert.ok(primitive.attributes.COLOR_0); + assert.equal(primitive.indices.count, 6); + } + if (fixture.id === "pbr") { + assert.equal(fixture.semantic.materials.length, 1); + const pbr = fixture.semantic.materials[0].pbr; + assert.deepEqual(pbr.baseColorFactor.map((value) => Number(value.toFixed(3))), [0.31, 0.57, 0.91, 1]); + assert.equal(Number(pbr.metallicFactor.toFixed(3)), 0.72); + assert.equal(Number(pbr.roughnessFactor.toFixed(3)), 0.28); + assert.ok(fixture.semantic.materials[0].emissiveFactor); + } + if (fixture.id === "uv") { + assert.ok(primitive.attributes.TEXCOORD_0); + assert.equal(fixture.semantic.textures.length, 1); + assert.equal(fixture.semantic.images.length, 1); + assert.equal(fixture.semantic.images[0].mimeType, "image/png"); + } + if (fixture.id === "skin") { + assert.ok(primitive.attributes.JOINTS_0); + assert.ok(primitive.attributes.WEIGHTS_0); + assert.equal(fixture.semantic.skins.length, 1); + assert.equal(fixture.semantic.skins[0].joints.length, 2); + assert.equal(fixture.semantic.skins[0].inverseBindMatrices.count, 2); + } + if (fixture.id === "animation") { + assert.equal(fixture.semantic.animations.length, 1); + assert.equal(fixture.semantic.animations[0].name, "M12 Animation Action"); + assert.deepEqual( + fixture.semantic.animations[0].channels.map((channel) => channel.target.path).sort(), + ["rotation", "translation"], + ); + assert.equal(fixture.semantic.animations[0].samplers[0].input.count, 25); + } +} + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-06a-glb-")); +try { + const regeneratedReport = path.join(temporary, "desktop-fixtures.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regeneratedReport], { + cwd: root, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regeneratedReport), report, "desktop semantic report is not deterministic"); + for (const fixture of report.fixtures) { + assert.deepEqual( + fs.readFileSync(path.join(temporary, fixture.file)), + fs.readFileSync(path.join(fixtureRoot, fixture.file)), + `${fixture.id} GLB bytes are not deterministic`, + ); + } +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write( + `glb-desktop-fixtures-ok fixtures=${report.fixtures.length} ` + + `bytes=${report.fixtures.reduce((total, fixture) => total + fixture.byteLength, 0)} ` + + `features=mesh,pbr,uv,skin,animation deterministic=true next=${manifest.nextTask}\n`, +); diff --git a/tools/web/check-glb-desktop-import.mjs b/tools/web/check-glb-desktop-import.mjs new file mode 100644 index 00000000..77826271 --- /dev/null +++ b/tools/web/check-glb-desktop-import.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-06B/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-06B/web-import-report.json"); +const generator = path.join(root, "tools/web/generate-glb-desktop-import-report.mjs"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const report = JSON.parse(fs.readFileSync(reportPath, "utf8")); + +assert.equal(manifest.schemaVersion, 1); +assert.equal(manifest.task, "M12-06B"); +assert.equal(manifest.parentTask, "M12-06A"); +assert.equal(manifest.nextTask, "M12-06C"); +for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(fileHash(path.join(root, artifact.path)), artifact.sha256, artifact.path); +} +assert.equal(report.schemaVersion, 1); +assert.equal(report.task, "M12-06B"); +assert.equal(report.operation, "WEB_GLB_IMPORT_SEMANTIC_COMPARISON"); +assert.equal(report.parentManifestSha256, fileHash(path.join(root, "tests/golden/M12-06A/manifest.json"))); +assert.equal(report.fixtureReportSha256, fileHash(path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"))); +assert.equal(report.fixtureCount, 5); +assert.deepEqual(report.comparedDomains, ["topology", "attributes", "materials", "nodes", "animations"]); +assert.equal(report.allCompatible, true); +assert.equal(report.routeState, "BLOCKED_UNTIL_MAIN_PERSISTENCE"); +assert.equal(report.nextTask, "M12-06C"); +assert.deepEqual(report.comparisons.map((item) => item.id), ["mesh", "pbr", "uv", "skin", "animation"]); +assert(report.comparisons.every((item) => item.compatible && item.mismatchCount === 0 && item.desktopSemanticSha256 === item.webSemanticSha256)); +assert.deepEqual(report.comparisons.find((item) => item.id === "mesh").attributes, ["COLOR_0", "NORMAL", "POSITION"]); +assert.equal(report.comparisons.find((item) => item.id === "pbr").materials.pbrCount, 1); +assert.equal(report.comparisons.find((item) => item.id === "uv").materials.texturedCount, 1); +assert.equal(report.comparisons.find((item) => item.id === "skin").nodes.skinnedCount, 1); +assert.deepEqual(report.comparisons.find((item) => item.id === "animation").animations.channelPaths, ["rotation", "translation"]); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-06b-import-check-")); +try { + const regenerated = path.join(temporary, "web-import-report.json"); + const result = spawnSync(process.execPath, [generator, regenerated], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(fs.readFileSync(regenerated), fs.readFileSync(reportPath), "Web import report is not deterministic"); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`glb-desktop-import-ok fixtures=${report.fixtureCount} domains=${report.comparedDomains.length} compatible=${report.allCompatible} route=${report.routeState} next=${manifest.nextTask}\n`); diff --git a/tools/web/check-glb-loss-report.mjs b/tools/web/check-glb-loss-report.mjs new file mode 100644 index 00000000..2d2af733 --- /dev/null +++ b/tools/web/check-glb-loss-report.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06D/manifest.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06D/web-loss-report.json"), "utf8")); +const parent = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-06D", parentTask: "M12-06C", nextTask: "M12-06E" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, fixtureCount: report.fixtureCount, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-06D", operation: "WEB_GLB_EXPORT_LOSS_REPORT", fixtureCount: 5, nextTask: "M12-06E" }, +); +assert.equal(fileHash(path.join(root, "tests/golden/M12-06C/manifest.json")), manifest.artifacts.parentManifest.sha256); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} + +const expectedIds = ["mesh", "pbr", "uv", "skin", "animation"]; +assert.deepEqual(report.fixtures.map((fixture) => fixture.fixtureId), expectedIds); +for (const fixture of report.fixtures) { + const source = parent.fixtures.find((candidate) => candidate.id === fixture.fixtureId); + assert.ok(source, `${fixture.fixtureId} is absent from M12-06C`); + assert.equal(fixture.sourceBlendSha256, source.blend.sha256, `${fixture.fixtureId} source blend drift`); + const losses = fixture.lossReport.losses; + assert.deepEqual(losses, [...losses].sort((left, right) => + left.code.localeCompare(right.code) || left.severity.localeCompare(right.severity) || + (left.id ?? "").localeCompare(right.id ?? "") || left.message.localeCompare(right.message))); + assert.equal(fixture.lossReport.errorCount, losses.filter((loss) => loss.severity === "error").length); + assert.equal(fixture.lossReport.warningCount, losses.filter((loss) => loss.severity === "warning").length); + if (fixture.fixtureId === "mesh") { + assert.equal(fixture.lossReport.canExport, false); + assert.deepEqual(losses.map((loss) => loss.code), ["LINKED_MATERIAL_INPUT_UNEVALUATED", "SHADER_GRAPH_UNMAPPABLE"]); + assert.equal(fixture.output, null); + } + else { + assert.equal(fixture.lossReport.canExport, true); + assert.equal(fixture.lossReport.errorCount, 0); + assert.ok(fixture.output?.byteLength > 128); + assert.match(fixture.output.sha256, /^[0-9a-f]{64}$/); + } +} + +process.stdout.write(`glb-loss-report-ok fixtures=${report.fixtureCount} blocked=mesh warnings=${report.fixtures.reduce((sum, fixture) => sum + fixture.lossReport.warningCount, 0)} deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-glb-main-persistence.mjs b/tools/web/check-glb-main-persistence.mjs new file mode 100644 index 00000000..f758ab71 --- /dev/null +++ b/tools/web/check-glb-main-persistence.mjs @@ -0,0 +1,100 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-06C/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-06C/desktop-main-report.json"); +const fixtureReportPath = path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1"); +const generator = path.join(root, "tools/web/generate-glb-main-persistence-fixtures.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); + +const read = (file) => fs.readFileSync(file); +const readJson = (file) => JSON.parse(read(file)); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(read(file)); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +const fixtureReport = readJson(fixtureReportPath); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-06C", parentTask: "M12-06B", nextTask: "M12-06D" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, fixtureCount: report.fixtureCount, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-06C", operation: "DESKTOP_GLB_IMPORT_MAIN_PERSISTENCE_BASELINE", fixtureCount: 5, nextTask: "M12-06D" }, +); +assert.equal(fileHash(path.join(root, "tests/golden/M12-06B/manifest.json")), manifest.artifacts.parentManifest.sha256); + +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing M12-06C artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} + +const fixtureById = new Map(fixtureReport.fixtures.map((fixture) => [fixture.id, fixture])); +assert.deepEqual(report.fixtures.map((fixture) => fixture.id), ["mesh", "pbr", "uv", "skin", "animation"]); +for (const fixture of report.fixtures) { + const source = fixtureById.get(fixture.id); + assert.ok(source, `${fixture.id} is absent from M12-06A`); + assert.equal(fixture.glb.sha256, source.sha256, `${fixture.id} GLB source drift`); + assert.equal(fileHash(path.join(fixtureRoot, fixture.glb.file)), fixture.glb.sha256, `${fixture.id} GLB hash`); + const blend = path.join(fixtureRoot.replace("m12_glb_desktop_v1", "m12_glb_main_v1"), fixture.blend.file); + assert.equal(fs.statSync(blend).size, fixture.blend.byteLength, `${fixture.id} desktop Main fixture byte length`); + assert.equal(fileHash(blend), fixture.blend.sha256, `${fixture.id} desktop Main fixture hash`); + assert.deepEqual(fixture.graph.stableIds, { + objects: [...fixture.graph.stableIds.objects].sort(), + meshes: [...fixture.graph.stableIds.meshes].sort(), + materials: [...fixture.graph.stableIds.materials].sort(), + images: [...fixture.graph.stableIds.images].sort(), + armatures: [...fixture.graph.stableIds.armatures].sort(), + actions: [...fixture.graph.stableIds.actions].sort(), + }); + for (const [kind, ids] of Object.entries(fixture.graph.stableIds)) { + assert.equal(new Set(ids).size, ids.length, `${fixture.id} duplicate ${kind} stable ID`); + const prefix = { objects: "object:", meshes: "mesh:", materials: "material:", images: "image:", armatures: "armature:", actions: "action:" }[kind]; + assert.ok(ids.every((id) => id.startsWith(prefix)), `${fixture.id} malformed ${kind} stable ID`); + } +} + +const normalizeReport = (value) => ({ + blenderVersion: value.blenderVersion, + fixtureCount: value.fixtureCount, + nextTask: value.nextTask, + operation: value.operation, + schemaVersion: value.schemaVersion, + fixtures: value.fixtures.map((fixture) => ({ + id: fixture.id, + glb: fixture.glb, + graph: fixture.graph, + })), +}); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-06c-main-persistence-")); +try { + const outputRoot = path.join(temporary, "main"); + const regeneratedReport = path.join(temporary, "desktop-main-report.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", fixtureRoot, outputRoot, regeneratedReport], { + cwd: root, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + const regenerated = readJson(regeneratedReport); + assert.deepEqual(normalizeReport(regenerated), normalizeReport(report), "desktop Main semantic report is not deterministic"); + for (const fixture of regenerated.fixtures) { + assert.ok(fs.statSync(path.join(outputRoot, fixture.blend.file)).size > 0, `${fixture.id} regenerated .blend is empty`); + assert.deepEqual(fixture.graph.stableIds, report.fixtures.find((item) => item.id === fixture.id).graph.stableIds, `${fixture.id} stable IDs drifted`); + } +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`glb-main-persistence-ok fixtures=${report.fixtureCount} stableIds=exact desktopReopen=exact deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-glb-negative-cases.mjs b/tools/web/check-glb-negative-cases.mjs new file mode 100644 index 00000000..eab6022c --- /dev/null +++ b/tools/web/check-glb-negative-cases.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06F/manifest.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06F/negative-report.json"), "utf8")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-06F", parentTask: "M12-06E", nextTask: "M12-06G" }, +); +assert.deepEqual(report, { + schemaVersion: 1, + task: "M12-06F", + operation: "GLB_IMPORT_NEGATIVE_CASES", + budget: { maxBytes: 524288, maxJsonBytes: 262144, maxBufferViews: 4096, maxAccessors: 8192 }, + cases: [ + { id: "sparse", code: "GLB_SPARSE_ACCESSOR_UNSUPPORTED" }, + { id: "extension", code: "GLB_EXTENSION_UNSUPPORTED" }, + { id: "external-uri", code: "GLB_EXTERNAL_URI_BLOCKED" }, + { id: "over-budget", code: "GLB_IMPORT_BUDGET_EXCEEDED" }, + ], + nextTask: "M12-06G", +}); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} +process.stdout.write(`glb-negative-cases-ok cases=${report.cases.length} budget=${report.budget.maxBytes} deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-glb-recovery.mjs b/tools/web/check-glb-recovery.mjs new file mode 100644 index 00000000..95dc1cbd --- /dev/null +++ b/tools/web/check-glb-recovery.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06G/manifest.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06G/recovery-report.json"), "utf8")); +const fileHash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-06G", parentTask: "M12-06F", nextTask: "M12-07A" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-06G", operation: "GLB_IMPORT_EXPORT_RECOVERY", nextTask: "M12-07A" }, +); +assert.deepEqual(report.cancellation.map((item) => ({ operation: item.operation, status: item.status, code: item.code })), [ + { operation: "IMPORT", status: "CANCELLED", code: "GLB_OPERATION_CANCELLED" }, + { operation: "EXPORT", status: "CANCELLED", code: "GLB_OPERATION_CANCELLED" }, +]); +assert.ok(report.cancellation.every((item) => item.committed === false && item.temporaryBytes === 0 && item.liveRequests === 0)); +assert.deepEqual(report.workerRestart, { + operation: "IMPORT", + generationBefore: 1, + generationAfter: 2, + status: "RECOVERED", + code: "GLB_WORKER_RESTARTED", + resultHash: "EXACT", +}); +assert.deepEqual(report.opfsQuota, { + operation: "EXPORT_ASSET_COMMIT", + status: "BLOCKED", + code: "GLB_OPFS_QUOTA", + backend: "OPFS", + committedAssetPreserved: true, + workerRestart: true, + smallAssetRecovery: true, +}); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} +process.stdout.write(`glb-recovery-ok cancelled=${report.cancellation.length} workerGeneration=${report.workerRestart.generationAfter} quota=${report.opfsQuota.code} smallRecovery=${report.opfsQuota.smallAssetRecovery} next=${manifest.nextTask}\n`); diff --git a/tools/web/check-glb-web-reimport.mjs b/tools/web/check-glb-web-reimport.mjs new file mode 100644 index 00000000..c5bd45c7 --- /dev/null +++ b/tools/web/check-glb-web-reimport.mjs @@ -0,0 +1,94 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-06E/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-06E/desktop-reimport-report.json"); +const generator = path.join(root, "tools/web/generate-glb-web-reimport-report.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const read = (file) => fs.readFileSync(file); +const readJson = (file) => JSON.parse(read(file)); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(read(file)); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +const base = readJson(path.join(root, "tests/golden/M12-06C/desktop-main-report.json")); +const exportReport = readJson(path.join(root, "tests/golden/M12-06D/web-loss-report.json")); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-06E", parentTask: "M12-06D", nextTask: "M12-06F" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, fixtureCount: report.fixtureCount, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-06E", operation: "DESKTOP_REIMPORT_WEB_GLB_CANONICAL_REPORT", fixtureCount: 4, nextTask: "M12-06F" }, +); +assert.equal(fileHash(path.join(root, "tests/golden/M12-06D/manifest.json")), manifest.artifacts.parentManifest.sha256); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} + +const expectedIds = ["pbr", "uv", "skin", "animation"]; +assert.deepEqual(report.fixtures.map((fixture) => fixture.id), expectedIds); +for (const fixture of report.fixtures) { + const exportFixture = exportReport.fixtures.find((candidate) => candidate.fixtureId === fixture.id); + assert.ok(exportFixture?.output, `${fixture.id} has no Web GLB output`); + assert.equal(fixture.glb.sha256, exportFixture.output.sha256, `${fixture.id} Web GLB hash drift`); + assert.equal(fileHash(path.join(root, "tests/files/web/m12_glb_web_v1", fixture.glb.file)), fixture.glb.sha256); +} + +function mismatches(expected, actual, pathName = "graph", output = []) { + if (Object.is(expected, actual)) return output; + if (Array.isArray(expected) || Array.isArray(actual)) { + if (!Array.isArray(expected) || !Array.isArray(actual)) { output.push(pathName); return output; } + if (expected.length !== actual.length) output.push(`${pathName}.length`); + for (let index = 0; index < Math.max(expected.length, actual.length); index++) { + if (index >= expected.length || index >= actual.length) output.push(`${pathName}[${index}]`); + else mismatches(expected[index], actual[index], `${pathName}[${index}]`, output); + } + return output; + } + if (expected && actual && typeof expected === "object" && typeof actual === "object") { + for (const key of new Set([...Object.keys(expected), ...Object.keys(actual)])) mismatches(expected[key], actual[key], `${pathName}.${key}`, output); + return output; + } + output.push(pathName); + return output; +} + +const comparisons = report.fixtures.map((fixture) => { + const baseline = base.fixtures.find((candidate) => candidate.id === fixture.id); + assert.ok(baseline, `${fixture.id} missing M12-06C baseline`); + const paths = mismatches(baseline.graph, fixture.graph); + return { id: fixture.id, exact: paths.length === 0, mismatchCount: paths.length, mismatchPaths: paths }; +}); +assert.deepEqual(comparisons.map((comparison) => ({ id: comparison.id, exact: comparison.exact, mismatchCount: comparison.mismatchCount })), [ + { id: "pbr", exact: true, mismatchCount: 0 }, + { id: "uv", exact: false, mismatchCount: 4 }, + { id: "skin", exact: false, mismatchCount: 31 }, + { id: "animation", exact: true, mismatchCount: 0 }, +]); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-06e-web-reimport-")); +try { + const regenerated = path.join(temporary, "desktop-reimport-report.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", path.join(root, "tests/files/web/m12_glb_web_v1"), path.join(temporary, "blend"), regenerated], { + cwd: root, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regenerated), report, "desktop Web GLB report is not deterministic"); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`glb-web-reimport-ok fixtures=${report.fixtureCount} exact=${comparisons.filter((comparison) => comparison.exact).length} mismatched=${comparisons.filter((comparison) => !comparison.exact).map((comparison) => comparison.id).join(",")} deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-io-format-capability-matrix.mjs b/tools/web/check-io-format-capability-matrix.mjs new file mode 100644 index 00000000..e02bc096 --- /dev/null +++ b/tools/web/check-io-format-capability-matrix.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const matrixPath = path.join(repoRoot, "tests/golden/M12-05B/capability-matrix.json"); +const inventoryPath = path.join(repoRoot, "tests/golden/M12-05A/format-inventory.json"); +const evidencePath = path.join(repoRoot, "tests/golden/M12-05B/manifest.json"); +const sourcePath = path.join(repoRoot, "web/protocol/io-format-capability-matrix.ts"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-capability-matrix-")); + +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); + +try { + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + const modulePath = path.join(temporary, "io-format-capability-matrix.mjs"); + fs.writeFileSync(modulePath, transpiled.outputText); + const protocol = await import(pathToFileURL(modulePath)); + const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8")); + assert.equal(evidence.task, "M12-05B"); + assert.equal(evidence.parentTask, "M12-05A"); + assert.equal(evidence.nextTask, "M12-05C"); + for (const artifact of Object.values(evidence.artifacts)) assert.equal(fileSha256(path.join(repoRoot, artifact.path)), artifact.sha256, artifact.path); + + const inventoryBytes = fs.readFileSync(inventoryPath); + const matrixBytes = fs.readFileSync(matrixPath); + const matrix = JSON.parse(matrixBytes); + assert.equal(matrix.runtimeInventorySha256, sha256(inventoryBytes)); + const inventory = JSON.parse(inventoryBytes); + const parsed = protocol.parseIOFormatCapabilityMatrix(matrix); + assert.deepEqual(parsed.formats.map((entry) => entry.format), inventory.formats.map((entry) => entry.format)); + const runtimeByFormat = new Map(inventory.formats.map((entry) => [entry.format, entry])); + for (const entry of parsed.formats) { + const runtime = runtimeByFormat.get(entry.format); + assert.equal(entry.runtimeImportStatus, runtime.import.runtimeStatus === "AVAILABLE" ? "AVAILABLE" : "OPERATOR_UNREGISTERED"); + assert.equal(entry.runtimeExportStatus, runtime.export.runtimeStatus === "AVAILABLE" ? "AVAILABLE" : "OPERATOR_UNREGISTERED"); + assert.equal(entry.operations.EXPORT.local.status, entry.format === "GLB" ? "READY" : "BLOCKED"); + assert.equal(entry.operations.EXPORT.local.execution, entry.format === "GLB" ? "LOCAL" : "NONE"); + assert.equal(entry.operations.EXPORT.server.status, "BLOCKED"); + assert.equal(entry.operations.IMPORT.local.status, "BLOCKED"); + assert.equal(entry.operations.IMPORT.server.status, "BLOCKED"); + } + + const regenerated = path.join(temporary, "capability-matrix.json"); + execFileSync(process.execPath, [path.join(repoRoot, "tools/web/generate-io-format-capability-matrix.mjs"), "--output", regenerated], { cwd: repoRoot }); + assert.deepEqual(fs.readFileSync(regenerated), matrixBytes, "capability matrix is not deterministic"); + process.stdout.write("io-format-capability-matrix-ok formats=7 local-glb-export=1 blocked-routes=27 runtime-bound=true\n"); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-io-format-receipt-bindings.mjs b/tools/web/check-io-format-receipt-bindings.mjs new file mode 100644 index 00000000..4990614b --- /dev/null +++ b/tools/web/check-io-format-receipt-bindings.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventoryPath = path.join(root, "tests/golden/M12-05A/format-inventory.json"); +const parentPath = path.join(root, "tests/golden/M12-05D/runtime-receipts.json"); +const boundPath = path.join(root, "tests/golden/M12-05E/bound-runtime-receipts.json"); +const protocolPath = path.join(root, "web/protocol/io-format-receipt-binding.ts"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-bindings-")); +const hashBytes = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const canonical = (value) => value === null || typeof value !== "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`; +const hash = (value) => hashBytes(canonical(value)); +try { + const inventoryBytes = fs.readFileSync(inventoryPath); const parentBytes = fs.readFileSync(parentPath); const boundBytes = fs.readFileSync(boundPath); + const inventory = JSON.parse(inventoryBytes); const parent = JSON.parse(parentBytes); const bound = JSON.parse(boundBytes); + const transpiled = ts.transpileModule(fs.readFileSync(protocolPath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: protocolPath, reportDiagnostics: true }); + assert.deepEqual(transpiled.diagnostics, []); const modulePath = path.join(temporary, "protocol.mjs"); fs.writeFileSync(modulePath, transpiled.outputText); const protocol = await import(pathToFileURL(modulePath)); + const parsed = protocol.validateIOFormatBoundReceiptSet(bound, hashBytes(parentBytes), hashBytes(inventoryBytes)); + assert.equal(parsed.receipts.length, 14); + const runtimeSha256 = hash(inventory.runtime); + for (const receipt of parsed.receipts) { + const source = inventory.formats.find((entry) => entry.format === receipt.format)[receipt.operation.toLowerCase()]; + assert.equal(receipt.sourceSha256, hash({ format: receipt.format, family: receipt.family, operation: receipt.operation, operator: receipt.operator, registered: receipt.registered, rnaIdentifier: receipt.rnaIdentifier })); + assert.equal(receipt.settingsSha256, hash({ buildOption: receipt.buildOption, buildOptionEnabled: receipt.buildOptionEnabled, variants: receipt.variants, extensions: receipt.extensions, properties: source.properties })); + assert.equal(receipt.runtimeSha256, runtimeSha256); + } + const regenerated = path.join(temporary, "bound-runtime-receipts.json"); execFileSync(process.execPath, [path.join(root, "tools/web/generate-io-format-receipt-bindings.mjs"), "--output", regenerated], { cwd: root }); assert.deepEqual(fs.readFileSync(regenerated), boundBytes); + process.stdout.write("io-format-receipt-bindings-ok receipts=14 source-settings-runtime=bound deterministic=true\n"); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-io-format-receipt-freshness.mjs b/tools/web/check-io-format-receipt-freshness.mjs new file mode 100644 index 00000000..085ada8d --- /dev/null +++ b/tools/web/check-io-format-receipt-freshness.mjs @@ -0,0 +1,87 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const parentPath = path.join(repoRoot, "tests/golden/M12-05E/bound-runtime-receipts.json"); +const freshPath = path.join(repoRoot, "tests/golden/M12-05F/fresh-runtime-receipts.json"); +const appFreshPath = path.join(repoRoot, "web/app/src/capabilities/io-format-runtime-receipts-freshness.json"); +const appExpectedPath = path.join(repoRoot, "web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json"); +const appPath = path.join(repoRoot, "web/app/src/app/App.tsx"); +const protocolPath = path.join(repoRoot, "web/protocol/io-format-receipt-freshness.ts"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-freshness-")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const stableValue = (value) => Array.isArray(value) ? value.map(stableValue) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])])) : value; +const stableSha256 = (value) => sha256(JSON.stringify(stableValue(value))); + +function transpile(sourcePath, outputName, replacements = []) { + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); + assert.deepEqual(transpiled.diagnostics, []); + let output = transpiled.outputText; + for (const [from, to] of replacements) output = output.replaceAll(from, to); + const outputPath = path.join(temporary, outputName); + fs.writeFileSync(outputPath, output); + return outputPath; +} + +try { + const parentBytes = fs.readFileSync(parentPath); + const bound = JSON.parse(parentBytes); + const freshBytes = fs.readFileSync(freshPath); + const fresh = JSON.parse(freshBytes); + assert.deepEqual(fs.readFileSync(appFreshPath), freshBytes, "App freshness receipt drifted from golden"); + const appExpected = JSON.parse(fs.readFileSync(appExpectedPath)); + const runtimeModule = transpile(path.join(repoRoot, "web/protocol/io-format-runtime-receipt.ts"), "io-format-runtime-receipt.mjs"); + const bindingModule = transpile(path.join(repoRoot, "web/protocol/io-format-receipt-binding.ts"), "io-format-receipt-binding.mjs"); + const protocolModule = transpile(protocolPath, "io-format-receipt-freshness.mjs", [["./io-format-receipt-binding\"", "./io-format-receipt-binding.mjs\""], ["./io-format-runtime-receipt\"", "./io-format-runtime-receipt.mjs\""]]); + const protocol = await import(pathToFileURL(protocolModule)); + const expected = { + parentBindingSha256: sha256(parentBytes), + parentReceiptSetSha256: bound.parentReceiptSetSha256, + inventorySha256: bound.inventorySha256, + boundReceiptSetSha256: stableSha256(bound), + runtimeSha256: stableSha256(bound.runtime), + runtime: bound.runtime, + receiptIdentities: bound.receipts, + }; + assert.equal(fresh.parentBindingSha256, expected.parentBindingSha256); + assert.equal(fresh.boundReceiptSetSha256, expected.boundReceiptSetSha256); + assert.equal(fresh.runtimeSha256, expected.runtimeSha256); + assert.deepEqual(appExpected, { + parentBindingSha256: expected.parentBindingSha256, + parentReceiptSetSha256: expected.parentReceiptSetSha256, + inventorySha256: expected.inventorySha256, + boundReceiptSetSha256: expected.boundReceiptSetSha256, + runtimeSha256: expected.runtimeSha256, + runtime: expected.runtime, + receiptIdentities: expected.receiptIdentities, + }); + const appSource = fs.readFileSync(appPath, "utf8"); + assert.match(appSource, /resolveFreshIOFormatRuntimeRoute\(IO_FORMAT_RUNTIME_RECEIPTS, IO_FORMAT_RUNTIME_RECEIPT_EXPECTED/); + await protocol.verifyIOFormatReceiptFreshness(fresh, expected); + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(fresh, expected, { format: "GLB", operation: "EXPORT" }).status, "READY"); + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(fresh, expected, { format: "USD", operation: "EXPORT" }).status, "BLOCKED"); + + const forged = structuredClone(fresh); + forged.bound.receipts[0].operator = "forged.operator"; + await assert.rejects(() => protocol.verifyIOFormatReceiptFreshness(forged, expected), (error) => error.reason === "RECEIPT_FORGED"); + const stale = structuredClone(fresh); + stale.bound.inventorySha256 = "a".repeat(64); + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(stale, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_STALE"); + const crossVersion = structuredClone(fresh); + crossVersion.bound.runtime.versionTuple = [5, 3, 0]; + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(crossVersion, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_CROSS_VERSION"); + + const regenerated = path.join(temporary, "fresh-runtime-receipts.json"); + execFileSync(process.execPath, [path.join(repoRoot, "tools/web/generate-io-format-receipt-freshness.mjs"), "--output", regenerated], { cwd: repoRoot }); + assert.deepEqual(fs.readFileSync(regenerated), freshBytes, "freshness receipt is not deterministic"); + process.stdout.write("io-format-receipt-freshness-ok receipts=14 forged=BLOCKED stale=BLOCKED cross-version=BLOCKED deterministic=true\n"); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-io-format-recovery.mjs b/tools/web/check-io-format-recovery.mjs new file mode 100644 index 00000000..97088488 --- /dev/null +++ b/tools/web/check-io-format-recovery.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M12-07J/io-format-recovery-report.json"); +const manifestPath = path.join(root, "tests/golden/M12-07J/manifest.json"); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const report = readJson(reportPath); +const manifest = readJson(manifestPath); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M12-07J", parentTask: "M12-07I", nextTask: "M13-01A" }); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M12-07J", operation: "IO_FORMAT_THREE_WAY_RECOVERY", nextTask: "M13-01A" }); +assert.deepEqual(report.assertions, { formats: ["OBJ", "STL", "PLY"], cancellationUnpublished: true, oomUnpublished: true, restartHashStable: true, smallRecoveryStable: true }); +for (const format of ["OBJ", "STL", "PLY"]) { + const value = report.formats[format]; + assert.equal(value.cancel.status, "CANCELLED"); assert.equal(value.cancel.errorCode, "IO_FORMAT_OPERATION_CANCELLED"); assert.equal(value.cancel.publishedResults, 0); + assert.equal(value.oom.status, "BLOCKED"); assert.equal(value.oom.errorCode, "IO_FORMAT_OOM"); assert.equal(value.oom.publishedResults, 0); + assert.equal(value.first.status, "COMMITTED"); assert.equal(value.second.status, "COMMITTED"); assert.equal(value.small.status, "COMMITTED"); + assert.equal(value.recovered.status, "RECOVERED"); assert.equal(value.recovered.errorCode, "IO_FORMAT_WORKER_RESTARTED"); + assert.equal(value.hashes.first, value.hashes.second); assert.equal(value.hashes.first, value.hashes.small); + assert.equal(value.first.outputSha256, value.recovered.outputSha256); +} +for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`io-format-recovery-ok formats=OBJ,STL,PLY cancelled=3 oom=3 restart=3 smallRecovery=3 deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-io-format-runtime-inventory.mjs b/tools/web/check-io-format-runtime-inventory.mjs new file mode 100644 index 00000000..bcbf5a47 --- /dev/null +++ b/tools/web/check-io-format-runtime-inventory.mjs @@ -0,0 +1,78 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const generator = path.join(repoRoot, "tools/web/generate-io-format-runtime-inventory.py"); +const expectedInventory = path.join(repoRoot, "tests/golden/M12-05A/format-inventory.json"); +const evidencePath = path.join(repoRoot, "tests/golden/M12-05A/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-runtime-inventory-")); +const regenerated = path.join(temporary, "format-inventory.json"); + +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); + +try { + const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8")); + assert.equal(evidence.schemaVersion, 1); + assert.equal(evidence.task, "M12-05A"); + assert.equal(evidence.parentTask, "M12-04J"); + assert.equal(evidence.nextTask, "M12-05B"); + for (const artifact of Object.values(evidence.artifacts)) assert.equal(fileSha256(path.join(repoRoot, artifact.path)), artifact.sha256, artifact.path); + + const blender = process.env.BLENDER_BIN ?? path.join(repoRoot, "build_blender_5.2.0/bin/blender"); + const run = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", regenerated], { + cwd: repoRoot, + encoding: "utf8", + maxBuffer: 8 * 1024 * 1024, + }); + assert.equal(run.status, 0, `${run.stdout ?? ""}\n${run.stderr ?? ""}`); + const expectedBytes = fs.readFileSync(expectedInventory); + assert.deepEqual(fs.readFileSync(regenerated), expectedBytes, "Blender runtime inventory is not deterministic"); + + const inventory = JSON.parse(expectedBytes); + assert.deepEqual(inventory, JSON.parse(fs.readFileSync(regenerated, "utf8"))); + assert.equal(inventory.schemaVersion, 1); + assert.equal(inventory.task, "M12-05A"); + assert.deepEqual(inventory.runtime.versionTuple, [5, 2, 0]); + assert.equal(inventory.runtime.blenderVersion, "5.2.0 LTS"); + assert.match(inventory.runtime.binarySha256, /^[a-f0-9]{64}$/); + assert.equal(inventory.formats.length, 7); + assert.deepEqual(inventory.formats.map((entry) => entry.format), ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"]); + + const available = new Set(["GLTF", "GLB", "OBJ", "STL", "PLY"]); + const disabled = new Set(["USD", "ALEMBIC"]); + for (const entry of inventory.formats) { + assert.ok(Array.isArray(entry.extensions) && entry.extensions.length > 0, `${entry.format} extensions missing`); + assert.ok(Array.isArray(entry.variants) && entry.variants.length > 0, `${entry.format} variants missing`); + for (const operation of ["import", "export"]) { + const receipt = entry[operation]; + assert.ok(typeof receipt.operator === "string" && receipt.operator.includes("."), `${entry.format} ${operation} operator missing`); + assert.ok(Array.isArray(receipt.properties), `${entry.format} ${operation} properties missing`); + assert.deepEqual(receipt.properties.map((property) => property.identifier), [...receipt.properties].map((property) => property.identifier).sort(), `${entry.format} ${operation} properties are not canonical`); + if (available.has(entry.format)) { + assert.equal(receipt.registered, true, `${entry.format} ${operation} is not registered`); + assert.equal(receipt.runtimeStatus, "AVAILABLE", `${entry.format} ${operation} is not available`); + assert.equal(receipt.buildOptionEnabled, true, `${entry.format} ${operation} build option is disabled`); + } + if (disabled.has(entry.format)) { + assert.equal(receipt.registered, false, `${entry.format} ${operation} unexpectedly registered`); + assert.equal(receipt.runtimeStatus, "OPERATOR_UNREGISTERED", `${entry.format} ${operation} did not fail closed`); + assert.equal(receipt.buildOptionEnabled, null, `${entry.format} ${operation} has an ambiguous build option`); + } + } + } + assert.equal(inventory.runtime.buildOptions.io_wavefront_obj, true); + assert.equal(inventory.runtime.buildOptions.io_stl, true); + assert.equal(inventory.runtime.buildOptions.io_ply, true); + assert.equal(inventory.runtime.buildOptions.usd, false); + assert.equal(inventory.runtime.buildOptions.alembic, false); + process.stdout.write(`io-format-runtime-inventory-ok formats=${inventory.formats.length} available=${available.size} build-disabled=${disabled.size} blender=${inventory.runtime.blenderVersion}\n`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-io-format-runtime-receipts.mjs b/tools/web/check-io-format-runtime-receipts.mjs new file mode 100644 index 00000000..9ef4273c --- /dev/null +++ b/tools/web/check-io-format-runtime-receipts.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventoryPath = path.join(repoRoot, "tests/golden/M12-05A/format-inventory.json"); +const receiptPath = path.join(repoRoot, "tests/golden/M12-05D/runtime-receipts.json"); +const protocolPath = path.join(repoRoot, "web/protocol/io-format-runtime-receipt.ts"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-runtime-receipts-")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +try { + const inventoryBytes = fs.readFileSync(inventoryPath); + const inventory = JSON.parse(inventoryBytes); + const receiptBytes = fs.readFileSync(receiptPath); + const receipts = JSON.parse(receiptBytes); + const transpiled = ts.transpileModule(fs.readFileSync(protocolPath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: protocolPath, reportDiagnostics: true }); + assert.deepEqual(transpiled.diagnostics, []); + const modulePath = path.join(temporary, "protocol.mjs"); + fs.writeFileSync(modulePath, transpiled.outputText); + const protocol = await import(pathToFileURL(modulePath)); + const parsed = protocol.validateIOFormatRuntimeReceiptSet(receipts, sha256(inventoryBytes)); + assert.equal(parsed.receipts.length, 14); + const byIdentity = new Map(inventory.formats.flatMap((entry) => ["IMPORT", "EXPORT"].map((operation) => [`${entry.format}:${operation}`, entry[operation.toLowerCase()]]))); + for (const receipt of parsed.receipts) { + const source = byIdentity.get(`${receipt.format}:${receipt.operation}`); + assert.ok(source); + assert.equal(receipt.operator, source.operator); + assert.equal(receipt.registered, source.registered); + assert.equal(receipt.rnaIdentifier, source.rnaIdentifier); + assert.equal(receipt.runtimeStatus, source.runtimeStatus); + assert.equal(receipt.extensions.length > 0, true); + } + assert.equal(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "GLB", operation: "EXPORT" }).status, "READY"); + assert.equal(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "USD", operation: "EXPORT" }).status, "BLOCKED"); + const regenerated = path.join(temporary, "runtime-receipts.json"); + execFileSync(process.execPath, [path.join(repoRoot, "tools/web/generate-io-format-runtime-receipts.mjs"), "--output", regenerated], { cwd: repoRoot }); + assert.deepEqual(fs.readFileSync(regenerated), receiptBytes, "runtime receipt set is not deterministic"); + process.stdout.write("io-format-runtime-receipts-ok formats=7 receipts=14 glb-export=READY usd-export=BLOCKED extension-independent=true\n"); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-io-format-ui-gate.mjs b/tools/web/check-io-format-ui-gate.mjs new file mode 100644 index 00000000..c5274d53 --- /dev/null +++ b/tools/web/check-io-format-ui-gate.mjs @@ -0,0 +1,43 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const registryPath = path.join(repoRoot, "web/app/src/capabilities/io-format-ui-registry.json"); +const matrixPath = path.join(repoRoot, "tests/golden/M12-05B/capability-matrix.json"); +const matrixProtocolPath = path.join(repoRoot, "web/protocol/io-format-capability-matrix.ts"); +const gateProtocolPath = path.join(repoRoot, "web/protocol/io-format-ui-gate.ts"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-ui-gate-check-")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +try { + const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")); + const matrixBytes = fs.readFileSync(matrixPath); + const transpile = (sourcePath, fileName) => { + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); + assert.deepEqual(result.diagnostics, []); + const target = path.join(temporary, fileName); + fs.writeFileSync(target, result.outputText); + return target; + }; + const matrixProtocol = await import(pathToFileURL(transpile(matrixProtocolPath, "matrix.mjs"))); + const gateProtocol = await import(pathToFileURL(transpile(gateProtocolPath, "gate.mjs"))); + const matrix = matrixProtocol.parseIOFormatCapabilityMatrix(JSON.parse(matrixBytes)); + gateProtocol.validateIOFormatUIRegistry(registry, matrix, sha256(matrixBytes)); + assert.equal(registry.task, "M12-05C"); + assert.equal(registry.parentMatrixSha256, sha256(matrixBytes)); + assert.equal(registry.projectFileAccept, ".blend,application/octet-stream"); + assert.deepEqual(registry.importRoutes, [], "no blocked import route may reach the file selector"); + assert.deepEqual(registry.exportRoutes, [{ format: "GLB", operation: "EXPORT", execution: "LOCAL", extensions: [".glb"] }]); + const regenerated = path.join(temporary, "registry.json"); + execFileSync(process.execPath, [path.join(repoRoot, "tools/web/generate-io-format-ui-gate.mjs"), "--output", regenerated], { cwd: repoRoot }); + assert.deepEqual(fs.readFileSync(regenerated), fs.readFileSync(registryPath), "UI registry is not deterministic"); + process.stdout.write("io-format-ui-gate-ok import-routes=0 export-routes=1 file-accept=.blend,application/octet-stream fail-closed=true\n"); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-library-link-fixture.mjs b/tools/web/check-library-link-fixture.mjs new file mode 100644 index 00000000..1e564f70 --- /dev/null +++ b/tools/web/check-library-link-fixture.mjs @@ -0,0 +1,106 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const read = (relativePath) => fs.readFileSync(path.join(root, relativePath)); +const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const manifest = JSON.parse(read("tests/golden/M12-03F/manifest.json")); +const report = JSON.parse(read("tests/golden/M12-03F/desktop-link-report.json")); +const fixtureRoot = path.join(root, "tests/files/web/m12_library_link_v1"); +const generator = path.join(root, "tools/web/generate-library-link-fixture.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); + +assert.equal(manifest.task, "M12-03F"); +assert.equal(manifest.parentTask, "M12-03E"); +assert.equal(manifest.nextTask, "M12-03G"); +assert.equal(manifest.operation, "LINK"); +for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(read(artifact.path)), artifact.sha256, artifact.path); +} + +assert.equal(report.schemaVersion, 1); +assert.equal(report.task, "M12-03F"); +assert.equal(report.operation, "LINK"); +assert.equal(report.blenderVersion, "5.2.0"); +assert.equal(report.nextTask, "M12-03G"); +assert.deepEqual(report.selectedRoots, ["Object/M12 Link Object"]); +assert.deepEqual(report.sourceGraph, { + edges: [ + { from: "Object/M12 Link Object", relation: "OBJECT_DATA", to: "Mesh/M12 Link Mesh" }, + { from: "Mesh/M12 Link Mesh", relation: "MATERIAL_SLOT[0]", to: "Material/M12 Link Material" }, + { from: "Material/M12 Link Material", relation: "NODE_IMAGE[M12 Link Image Node]", to: "Image/M12 Link Image" }, + ], + geometry: { edges: 4, loops: 4, materialSlots: ["M12 Link Material"], polygons: 1, uvLayers: ["UVMap"], vertices: 4 }, + ids: { + IMAGE: { idType: "IMAGE", isLibraryOverride: false, library: null, name: "M12 Link Image", nameFull: "M12 Link Image" }, + MATERIAL: { idType: "MATERIAL", isLibraryOverride: false, library: null, name: "M12 Link Material", nameFull: "M12 Link Material" }, + MESH: { idType: "MESH", isLibraryOverride: false, library: null, name: "M12 Link Mesh", nameFull: "M12 Link Mesh" }, + OBJECT: { idType: "OBJECT", isLibraryOverride: false, library: null, name: "M12 Link Object", nameFull: "M12 Link Object" }, + }, + image: { channels: 4, colorspace: "sRGB", packed: true, pixelFloat32Sha256: "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a", size: [2, 2] }, + root: { idType: "OBJECT", isLibraryOverride: false, library: null, name: "M12 Link Object", nameFull: "M12 Link Object" }, + sourceMarker: "M12-03F", +}); +assert.deepEqual(report.linkedGraph.edges, report.sourceGraph.edges); +assert.deepEqual(report.linkedGraph.geometry, report.sourceGraph.geometry); +assert.deepEqual(report.linkedGraph.image, report.sourceGraph.image); +assert.equal(report.linkedGraph.ids.OBJECT.library, "m12_link_source.blend"); +assert.equal(report.linkedGraph.ids.OBJECT.nameFull, "M12 Link Object [m12_link_source.blend]"); +for (const value of Object.values(report.linkedGraph.ids)) { + assert.equal(value.library, "m12_link_source.blend"); + assert.equal(value.isLibraryOverride, false); +} +assert.deepEqual(report.stableMapping.map((item) => [item.owner, item.readOnly]), [ + ["SOURCE_LIBRARY", true], + ["SOURCE_LIBRARY", true], + ["SOURCE_LIBRARY", true], + ["SOURCE_LIBRARY", true], +]); +assert.deepEqual(report.stableMapping.map((item) => item.source), [ + "Object/M12 Link Object", + "Mesh/M12 Link Mesh", + "Material/M12 Link Material", + "Image/M12 Link Image", +]); +assert.deepEqual(report.stableMapping.map((item) => item.local), report.stableMapping.map((item) => item.source)); +assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.source.file))), report.source.sha256); +assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.target.file))), report.target.sha256); + +const normalizeContainerHashes = (value) => ({ + ...value, + source: { ...value.source, sha256: "" }, + target: { ...value.target, sha256: "" }, +}); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-library-link-")); +try { + const generatedFixtureRoot = path.join(temporary, "files"); + const generatedReportPath = path.join(temporary, "report.json"); + const output = execFileSync(blender, [ + "--background", + "--factory-startup", + "--python", + generator, + "--", + generatedFixtureRoot, + generatedReportPath, + ], { cwd: root, encoding: "utf8" }); + assert.match(output, /library-link-fixture-ok roots=1 mapping=4/); + assert.match(output, /next=M12-03G/); + const generated = JSON.parse(fs.readFileSync(generatedReportPath, "utf8")); + assert.deepEqual(normalizeContainerHashes(generated), normalizeContainerHashes(report)); + assert.equal(sha256(fs.readFileSync(path.join(generatedFixtureRoot, generated.source.file))), generated.source.sha256); + assert.equal(sha256(fs.readFileSync(path.join(generatedFixtureRoot, generated.target.file))), generated.target.sha256); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write( + `library-link-fixture-check-ok roots=${report.selectedRoots.length} mapping=${report.stableMapping.length} ` + + `library=${report.linkedGraph.ids.OBJECT.library} readOnly=${report.stableMapping.every((item) => item.readOnly)} next=${report.nextTask}\n`, +); diff --git a/tools/web/check-library-main-append.mjs b/tools/web/check-library-main-append.mjs new file mode 100644 index 00000000..ebcdc413 --- /dev/null +++ b/tools/web/check-library-main-append.mjs @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const read = (relativePath) => fs.readFileSync(path.join(root, relativePath)); +const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const manifest = JSON.parse(read("tests/golden/M12-03E/manifest.json")); +const desktopReport = JSON.parse(read("tests/golden/M12-03C/desktop-append-report.json")); + +assert.deepEqual(manifest, { + schemaVersion: 1, + task: "M12-03E", + parentTask: "M12-03D", + enablingTask: false, + parityStateChange: false, + runtime: "BLENDER_5_2_WASM_CHROMIUM", + operation: "APPEND", + canonicalComparison: "DESKTOP_REPORT_EXACT_AFTER_IMAGE_ROW_NORMALIZATION_AND_SCENEIR_COLORSPACE_DEFAULT", + assertions: { + transactionCount: 1, + revisionDelta: 1, + undoRemovesClosure: true, + redoRestoresCanonical: true, + saveReopenRestoresCanonical: true, + }, + artifacts: { + parentManifest: { + path: "tests/golden/M12-03C/manifest.json", + sha256: "cbfbd8c919125108334dd24925b9ef8e69880983515e56841e6ead4a2b182aed", + }, + desktopReport: { + path: "tests/golden/M12-03C/desktop-append-report.json", + sha256: "b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a", + }, + test: { + path: "web/tests/e2e/library-append-main.spec.ts", + sha256: "101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0", + }, + sourceBlend: { + path: "tests/files/web/m12_library_append_v1/m12_append_source.blend", + sha256: "5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606", + }, + targetBlend: { + path: "tests/files/web/empty.blend", + sha256: "9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63", + }, + }, + nextTask: "M12-03F", +}); + +assert.equal(sha256(read(manifest.artifacts.parentManifest.path)), manifest.artifacts.parentManifest.sha256); +assert.equal(sha256(read(manifest.artifacts.desktopReport.path)), manifest.artifacts.desktopReport.sha256); +assert.equal(sha256(read(manifest.artifacts.sourceBlend.path)), manifest.artifacts.sourceBlend.sha256); +assert.equal(sha256(read(manifest.artifacts.targetBlend.path)), manifest.artifacts.targetBlend.sha256); +assert.equal(sha256(read(manifest.artifacts.test.path)), manifest.artifacts.test.sha256); +assert.equal(desktopReport.task, "M12-03C"); +assert.equal(desktopReport.operation, "APPEND"); +assert.equal(desktopReport.appendedGraph.geometry.vertices, 4); +assert.equal(desktopReport.appendedGraph.geometry.edges, 4); +assert.equal(desktopReport.appendedGraph.geometry.polygons, 1); +assert.equal(desktopReport.appendedGraph.geometry.loops, 4); +assert.equal(desktopReport.appendedGraph.image.pixelFloat32Sha256.length, 64); + +const source = read(manifest.artifacts.test.path).toString("utf8"); +for (const marker of [ + "M12-03E keeps append undo/redo/save/reopen", + "desktop-append-report.json", + "canonicalGraph", + "client.applyCommand({ type: \"undo\" })", + "client.applyCommand({ type: \"redo\" })", + "client.saveBlend()", + "reopenedCanonical", + "desktopCanonical", +]) assert.ok(source.includes(marker), `M12-03E test marker is missing: ${marker}`); + +process.stdout.write( + `library-main-append-check-ok canonical=${desktopReport.appendedGraph.image.pixelFloat32Sha256} ` + + `transaction=${manifest.assertions.transactionCount} undo=removed redo=canonical reopen=canonical next=${manifest.nextTask}\n`, +); diff --git a/tools/web/check-library-operation-commands.mjs b/tools/web/check-library-operation-commands.mjs new file mode 100644 index 00000000..eddfe4a5 --- /dev/null +++ b/tools/web/check-library-operation-commands.mjs @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const read = (relativePath) => fs.readFileSync(path.join(root, relativePath)); +const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const manifest = JSON.parse(read("tests/golden/M12-03N/manifest.json")); +const packageJson = JSON.parse(read("web/package.json")); +const expected = { + "append-desktop": "node ../tools/web/check-library-append-fixture.mjs", + "append-wasm": "node --test tests/unit/library-append-wasm.test.mjs", + "append-chromium": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-append-main.spec.ts", + "link-desktop": "node ../tools/web/check-library-link-fixture.mjs", + "link-wasm": "node --test tests/unit/library-linked-mutation.test.mjs tests/unit/library-linked-reload.test.mjs tests/unit/library-linked-missing.test.mjs", + "link-chromium": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-link-chromium.spec.ts", + "override-desktop": "node ../tools/web/check-library-override-fixture.mjs", + "override-wasm": "node --test tests/unit/library-override-writer.test.mjs tests/unit/library-override-freshness.test.mjs", + "override-chromium": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-override-chromium.spec.ts", +}; +for (const [name, command] of Object.entries(expected)) assert.equal(packageJson.scripts[`test:library-${name}`], command, name); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(read(artifact.path)), artifact.sha256, artifact.path); +assert.equal(manifest.task, "M12-03N"); +assert.equal(manifest.nextTask, "M12-04A"); +assert.equal(Object.keys(expected).length, 9); +process.stdout.write("library-operation-commands-check-ok lanes=9 operations=3 desktop=3 wasm=3 chromium=3 next=M12-04A\n"); diff --git a/tools/web/check-library-override-fixture.mjs b/tools/web/check-library-override-fixture.mjs new file mode 100644 index 00000000..408a8b8a --- /dev/null +++ b/tools/web/check-library-override-fixture.mjs @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const fixtureRoot = path.join(root, "tests/files/web/m12_library_override_v1"); +const generator = path.join(root, "tools/web/generate-library-override-fixture.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const read = (relativePath) => fs.readFileSync(path.join(root, relativePath)); +const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const manifest = JSON.parse(read("tests/golden/M12-03J/manifest.json")); +const report = JSON.parse(read("tests/golden/M12-03J/desktop-override-report.json")); + +assert.equal(manifest.task, "M12-03J"); +assert.equal(manifest.parentTask, "M12-03I"); +assert.equal(manifest.nextTask, "M12-03K"); +assert.equal(report.schemaVersion, 1); +assert.equal(report.task, "M12-03J"); +assert.equal(report.operation, "LIBRARY_OVERRIDE"); +assert.equal(report.blenderVersion, "5.2.0"); +assert.deepEqual(report.selectedRoots, ["Object/M12 Override Object"]); +assert.deepEqual(report.overrideGraph.reference, { + dataBlockId: "Object/M12 Override Object", + idType: "OBJECT", + library: "m12_override_source.blend", + owner: "SOURCE_LIBRARY", + readOnly: true, + isLibraryOverride: false, +}); +assert.deepEqual(report.overrideGraph.local, { + dataBlockId: "Object/M12 Override Object", + idType: "OBJECT", + library: null, + owner: "LOCAL_OVERRIDE", + projectId: "m12-03j-project", + readOnly: false, + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + isLibraryOverride: true, +}); +assert.deepEqual(report.overrideGraph.propertyOverride, { + index: 0, + operationCount: 1, + propertyCount: 1, + rnaPath: "[\"m12_override_value\"]", + value: 2.5, +}); +assert.equal(report.overrideGraph.sourceMarker, "M12-03J"); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(read(artifact.path)), artifact.sha256, artifact.path); +assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.source.file))), report.source.sha256); +assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.target.file))), report.target.sha256); + +const normalize = (value) => ({ + ...value, + source: { ...value.source, sha256: "" }, + target: { ...value.target, sha256: "" }, +}); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-library-override-")); +try { + const generatedRoot = path.join(temporary, "files"); + const generatedReportPath = path.join(temporary, "report.json"); + const output = execFileSync(blender, ["--background", "--factory-startup", "--python", generator, "--", generatedRoot, generatedReportPath], { cwd: root, encoding: "utf8" }); + assert.match(output, /library-override-fixture-ok roots=1/); + assert.match(output, /owner=LOCAL_OVERRIDE/); + const generated = JSON.parse(fs.readFileSync(generatedReportPath, "utf8")); + assert.deepEqual(normalize(generated), normalize(report)); + assert.equal(sha256(fs.readFileSync(path.join(generatedRoot, generated.source.file))), generated.source.sha256); + assert.equal(sha256(fs.readFileSync(path.join(generatedRoot, generated.target.file))), generated.target.sha256); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write( + `library-override-fixture-check-ok roots=${report.selectedRoots.length} ` + + `reference=${report.overrideGraph.reference.library} owner=${report.overrideGraph.local.owner} ` + + `path=${report.overrideGraph.propertyOverride.rnaPath} next=${report.nextTask}\n`, +); diff --git a/tools/web/check-malicious-archive-fixtures.mjs b/tools/web/check-malicious-archive-fixtures.mjs new file mode 100644 index 00000000..4ef1b692 --- /dev/null +++ b/tools/web/check-malicious-archive-fixtures.mjs @@ -0,0 +1,182 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL, fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const fixtureRoot = path.join(repoRoot, "tests/files/web/archive-security"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "malicious-archive-fixtures-")); +const moduleRoot = path.join(temporary, "modules"); +const regeneratedRoot = path.join(temporary, "regenerated"); +fs.mkdirSync(moduleRoot, { recursive: true }); + +try { + for (const sourceName of ["archive-link-safety.ts", "archive-conflicts.ts"]) { + const sourcePath = path.join(repoRoot, "web/protocol", sourceName); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + fs.writeFileSync(path.join(moduleRoot, sourceName.replace(".ts", ".mjs")), transpiled.outputText); + } + const safety = await import(pathToFileURL(path.join(moduleRoot, "archive-link-safety.mjs"))); + const conflicts = await import(pathToFileURL(path.join(moduleRoot, "archive-conflicts.mjs"))); + const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); + const decoder = new TextDecoder("utf-8", { fatal: true }); + const evidence = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-04J/manifest.json"), "utf8")); + assert.equal(evidence.task, "M12-04J"); + assert.equal(evidence.parentTask, "M12-04I"); + assert.equal(evidence.nextTask, "M12-05A"); + for (const artifact of Object.values(evidence.artifacts)) { + assert.equal(sha256(fs.readFileSync(path.join(repoRoot, artifact.path))), artifact.sha256, artifact.path); + } + + function decode(bytes, offset, length) { + return decoder.decode(bytes.subarray(offset, offset + length)); + } + + function zipMetadata(bytes) { + assert.ok(bytes.length >= 22, "ZIP fixture is shorter than EOCD"); + const endOffset = bytes.length - 22; + assert.equal(bytes.readUInt32LE(endOffset), 0x06054b50, "ZIP fixture EOCD is missing"); + assert.equal(bytes.readUInt16LE(endOffset + 4), 0, "multi-disk ZIP fixture is forbidden"); + assert.equal(bytes.readUInt16LE(endOffset + 6), 0, "multi-disk ZIP fixture is forbidden"); + const entries = bytes.readUInt16LE(endOffset + 10); + assert.equal(bytes.readUInt16LE(endOffset + 8), entries, "ZIP entry counts disagree"); + const centralBytes = bytes.readUInt32LE(endOffset + 12); + const centralOffset = bytes.readUInt32LE(endOffset + 16); + assert.equal(centralOffset + centralBytes, endOffset, "ZIP central directory is not the first bounded metadata region"); + const links = []; + const ranges = []; + let offset = centralOffset; + for (let index = 0; index < entries; index++) { + assert.equal(bytes.readUInt32LE(offset), 0x02014b50, `ZIP central entry ${index} is invalid`); + const method = bytes.readUInt16LE(offset + 10); + const compressedBytes = bytes.readUInt32LE(offset + 20); + const uncompressedBytes = bytes.readUInt32LE(offset + 24); + const nameBytes = bytes.readUInt16LE(offset + 28); + const extraBytes = bytes.readUInt16LE(offset + 30); + const commentBytes = bytes.readUInt16LE(offset + 32); + const localOffset = bytes.readUInt32LE(offset + 42); + const entryPath = decode(bytes, offset + 46, nameBytes); + assert.equal(method, 0, "fixture ZIP entries must use deterministic STORE metadata"); + assert.equal(bytes.readUInt32LE(localOffset), 0x04034b50, `ZIP local entry ${index} is invalid`); + assert.equal(bytes.readUInt16LE(localOffset + 8), method, "ZIP local/central methods disagree"); + assert.equal(bytes.readUInt32LE(localOffset + 18), compressedBytes, "ZIP local/central compressed sizes disagree"); + assert.equal(bytes.readUInt32LE(localOffset + 22), uncompressedBytes, "ZIP local/central uncompressed sizes disagree"); + const localNameBytes = bytes.readUInt16LE(localOffset + 26); + const localExtraBytes = bytes.readUInt16LE(localOffset + 28); + assert.equal(decode(bytes, localOffset + 30, localNameBytes), entryPath, "ZIP local/central names disagree"); + const compressedOffset = localOffset + 30 + localNameBytes + localExtraBytes; + assert.ok(compressedOffset + compressedBytes <= centralOffset, "ZIP payload range overlaps central metadata"); + links.push({ path: entryPath, type: entryPath.endsWith("/") ? "DIRECTORY" : "FILE", target: null }); + ranges.push({ path: entryPath, compressedOffset, compressedBytes, uncompressedBytes }); + offset += 46 + nameBytes + extraBytes + commentBytes; + } + assert.equal(offset, endOffset, "ZIP central directory length disagrees with EOCD"); + return { links, ranges }; + } + + function tarString(bytes, offset, length) { + const field = bytes.subarray(offset, offset + length); + const end = field.indexOf(0); + return decoder.decode(end === -1 ? field : field.subarray(0, end)); + } + + function tarOctal(bytes, offset, length) { + const value = tarString(bytes, offset, length).trim(); + assert.match(value, /^[0-7]+$/, "TAR numeric field is not octal"); + return Number.parseInt(value, 8); + } + + function tarMetadata(bytes) { + assert.equal(bytes.length % 512, 0, "TAR fixture is not block aligned"); + const links = []; + const ranges = []; + let offset = 0; + let zeroBlocks = 0; + while (offset < bytes.length) { + const header = bytes.subarray(offset, offset + 512); + if (header.every((byte) => byte === 0)) { + zeroBlocks++; + offset += 512; + if (zeroBlocks === 2) break; + continue; + } + assert.equal(zeroBlocks, 0, "TAR has data after an end marker"); + assert.equal(tarString(header, 257, 6), "ustar", "TAR fixture is not USTAR"); + const expectedChecksum = tarOctal(header, 148, 8); + const checksumHeader = Buffer.from(header); + checksumHeader.fill(0x20, 148, 156); + assert.equal(checksumHeader.reduce((sum, byte) => sum + byte, 0), expectedChecksum, "TAR header checksum mismatch"); + const prefix = tarString(header, 345, 155); + const name = tarString(header, 0, 100); + const entryPath = prefix ? `${prefix}/${name}` : name; + const uncompressedBytes = tarOctal(header, 124, 12); + const typeFlag = String.fromCharCode(header[156] || 0x30); + const type = { "0": "FILE", "1": "HARDLINK", "2": "SYMLINK", "5": "DIRECTORY" }[typeFlag]; + assert.ok(type, `TAR entry type ${typeFlag} is unsupported by the fixture gate`); + const target = type === "SYMLINK" || type === "HARDLINK" ? tarString(header, 157, 100) : null; + const compressedOffset = offset + 512; + assert.ok(compressedOffset + uncompressedBytes <= bytes.length, "TAR payload exceeds the fixture"); + links.push({ path: entryPath, type, target }); + ranges.push({ path: entryPath, compressedOffset, compressedBytes: uncompressedBytes, uncompressedBytes }); + offset = compressedOffset + Math.ceil(uncompressedBytes / 512) * 512; + } + assert.equal(zeroBlocks, 2, "TAR fixture has no two-block end marker"); + assert.equal(offset, bytes.length, "TAR fixture has trailing data after the end marker"); + return { links, ranges }; + } + + const manifestBytes = fs.readFileSync(path.join(fixtureRoot, "manifest.json")); + const manifest = JSON.parse(manifestBytes); + assert.equal(manifest.schemaVersion, 1); + assert.equal(manifest.task, "M12-04J"); + assert.equal(manifest.generator, "tools/web/generate-malicious-archive-fixtures.mjs"); + assert.equal(manifest.extractionAllowed, false); + const expected = [ + ["ZIP_PATH_TRAVERSAL", "ZIP", "ARCHIVE_ROOT_ESCAPE", "LINK_SAFETY"], + ["ZIP_COMPRESSION_BOMB", "ZIP", "COMPRESSION_RATIO", "CONFLICTS"], + ["ZIP_DUPLICATE_PATH", "ZIP", "DUPLICATE_PATH", "LINK_SAFETY"], + ["TAR_PATH_TRAVERSAL", "TAR", "ARCHIVE_ROOT_ESCAPE", "LINK_SAFETY"], + ["TAR_SYMLINK_ESCAPE", "TAR", "SYMLINK_ESCAPE", "LINK_SAFETY"], + ["TAR_PREFIX_CONFLICT", "TAR", "FILE_DIRECTORY_PREFIX_CONFLICT", "CONFLICTS"], + ]; + assert.deepEqual(manifest.cases.map((item) => [item.id, item.format, item.threat, item.gate]), expected); + + execFileSync(process.execPath, [path.join(repoRoot, manifest.generator), "--output", regeneratedRoot], { cwd: repoRoot }); + assert.deepEqual(fs.readFileSync(path.join(regeneratedRoot, "manifest.json")), manifestBytes, "fixture manifest is not deterministic"); + + for (const fixture of manifest.cases) { + assert.equal(fixture.file, path.basename(fixture.file), `${fixture.id} fixture path escapes its root`); + assert.equal(fixture.expectedCode, "IO_ARCHIVE_UNSAFE"); + const archiveBytes = fs.readFileSync(path.join(fixtureRoot, fixture.file)); + assert.equal(archiveBytes.length, fixture.byteLength, `${fixture.id} byte length drifted`); + assert.equal(sha256(archiveBytes), fixture.sha256, `${fixture.id} SHA-256 drifted`); + assert.deepEqual(fs.readFileSync(path.join(regeneratedRoot, fixture.file)), archiveBytes, `${fixture.id} is not deterministic`); + const metadata = fixture.format === "ZIP" ? zipMetadata(archiveBytes) : tarMetadata(archiveBytes); + let failure; + try { + if (fixture.gate === "LINK_SAFETY") { + safety.resolveArchiveLinkEntries({ schemaVersion: 1, temporaryRootId: `fixture:${fixture.id}`, entries: metadata.links }); + } + else { + conflicts.validateArchiveConflicts({ schemaVersion: 1, byteLength: archiveBytes.length, ranges: metadata.ranges }); + } + } + catch (error) { + failure = error; + } + assert.equal(failure?.code, fixture.expectedCode, `${fixture.id} did not fail closed`); + } + process.stdout.write(`malicious-archive-fixtures-ok cases=${manifest.cases.length} zip=3 tar=3 extraction=disabled\n`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-malicious-input-matrix.mjs b/tools/web/check-malicious-input-matrix.mjs new file mode 100644 index 00000000..6fac3703 --- /dev/null +++ b/tools/web/check-malicious-input-matrix.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-05F/malicious-input-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05F/manifest.json"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); +const commands = [ + { id: "blend", command: process.execPath, args: ["tools/web/check-malicious-blends.mjs"], expected: "REJECTED" }, + { id: "archive", command: process.execPath, args: ["tools/web/check-malicious-archive-fixtures.mjs"], expected: "REJECTED" }, + { id: "image-font-media-node-manifest", command: process.execPath, args: ["--test", "web/tests/unit/asset-preview-decode.test.mjs", "web/tests/unit/external-vfont.test.mjs", "web/tests/unit/sequencer-media-cache.test.mjs", "web/tests/unit/shader-compiler.test.mjs", "web/tests/unit/script-manifest-budgets.test.mjs"], expected: "REJECTED" }, + { id: "glb", command: process.execPath, args: ["--test", "web/tests/unit/glb-negative-cases.test.mjs"], expected: "REJECTED" }, +]; +const results = commands.map((entry) => { + const run = spawnSync(entry.command, entry.args, { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); + assert.equal(run.status, 0, `${entry.id} failed\n${run.stdout}\n${run.stderr}`); + return { id: entry.id, status: entry.expected, exitCode: run.status }; +}); +const report = { + schemaVersion: 1, + task: "M13-05F", + operation: "MALICIOUS_INPUT_MATRIX", + cases: { + blend: { status: "REJECTED", checker: "tools/web/check-malicious-blends.mjs", stableCode: "BLEND_OPEN_INVALID" }, + image: { status: "REJECTED", checker: "web/tests/unit/asset-preview-decode.test.mjs", stableCodes: ["ASSET_MANIFEST_INVALID", "ASSET_BUDGET_EXCEEDED", "ASSET_SOURCE_HASH_MISMATCH"] }, + font: { status: "REJECTED", checker: "web/tests/unit/external-vfont.test.mjs", stableCodes: ["NON_MESH_BINARY_INVALID", "NON_MESH_RESOURCE_OUTSIDE_PROJECT", "ASSET_SOURCE_HASH_MISMATCH"] }, + media: { status: "REJECTED", checker: "web/tests/unit/sequencer-media-cache.test.mjs", stableCode: "SEQUENCER_SCHEMA_INVALID" }, + archive: { status: "REJECTED", checker: "tools/web/check-malicious-archive-fixtures.mjs", stableCode: "IO_ARCHIVE_UNSAFE" }, + nodeGraph: { status: "REJECTED", checker: "web/tests/unit/shader-compiler.test.mjs", stableCodes: ["SHADER_NODE_UNSUPPORTED", "SHADER_INVALID_GRAPH"] }, + manifest: { status: "REJECTED", checker: "web/tests/unit/script-manifest-budgets.test.mjs", stableCode: "SCRIPT_MANIFEST_INVALID" }, + glb: { status: "REJECTED", checker: "web/tests/unit/glb-negative-cases.test.mjs", stableCodes: ["GLB_SPARSE_ACCESSOR_UNSUPPORTED", "GLB_EXTENSION_UNSUPPORTED", "GLB_EXTERNAL_URI_BLOCKED", "GLB_IMPORT_BUDGET_EXCEEDED"] }, + }, + executions: results, + allRejected: results.every((result) => result.status === "REJECTED"), + execution: "DISABLED", + nextTask: "M13-05G", +}; +if (process.env.UPDATE_M13_05F_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05F", parentTask: "M13-05E", nextTask: "M13-05G" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write("malicious-input-matrix-ok blend=REJECTED image=REJECTED font=REJECTED media=REJECTED archive=REJECTED nodeGraph=REJECTED manifest=REJECTED glb=REJECTED execution=DISABLED next=M13-05G\n"); diff --git a/tools/web/check-malicious-script-fixture.mjs b/tools/web/check-malicious-script-fixture.mjs new file mode 100644 index 00000000..a4b8242d --- /dev/null +++ b/tools/web/check-malicious-script-fixture.mjs @@ -0,0 +1,16 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M13-01F/malicious-report.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M13-01F/manifest.json"), "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-01F", parentTask: "M13-01E", nextTask: "M13-02A" }); +const fixturePath = path.join(root, "tests/files/web/m13_malicious_script_v1", report.fixture.name); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M13-01F", operation: "MALICIOUS_SCRIPT_FIXTURE", nextTask: "M13-02A" }); +assert.equal(report.sources.length, 4); assert.equal(report.sources.filter((source) => source.useModule).length, 1); assert.ok(report.sources.every((source) => source.expectedExecution === "BLOCKED")); +assert.equal(crypto.createHash("sha256").update(fs.readFileSync(fixturePath)).digest("hex"), report.fixture.sha256); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(crypto.createHash("sha256").update(fs.readFileSync(path.join(root, artifact.path))).digest("hex"), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`malicious-script-fixture-ok sources=${report.sources.length} module=1 execution=BLOCKED fixtureSha256=${report.fixture.sha256} next=${report.nextTask}\n`); diff --git a/tools/web/check-obj-multi-negative-fixtures.mjs b/tools/web/check-obj-multi-negative-fixtures.mjs new file mode 100644 index 00000000..3670e921 --- /dev/null +++ b/tools/web/check-obj-multi-negative-fixtures.mjs @@ -0,0 +1,110 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-07B/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-07B/desktop-fixtures.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_obj_multi_v1"); +const generator = path.join(root, "tools/web/generate-obj-multi-negative-fixtures.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +function parseObj(file) { + const positions = []; + const texcoords = []; + const normals = []; + const faces = []; + const groups = []; + let object = null; + let material = null; + for (const raw of fs.readFileSync(file, "utf8").split(/\r?\n/)) { + const line = raw.trim(); + if (!line || line.startsWith("#")) continue; + const parts = line.split(/\s+/); + if (parts[0] === "v") positions.push(parts.slice(1)); + else if (parts[0] === "vt") texcoords.push(parts.slice(1)); + else if (parts[0] === "vn") normals.push(parts.slice(1)); + else if (parts[0] === "g") { + for (const group of parts.slice(1)) { + groups.push(group); + if (group.endsWith("_Mesh")) object = group; + } + } + else if (parts[0] === "usemtl") material = parts.slice(1).join(" "); + else if (parts[0] === "f") faces.push({ object, material, tokens: parts.slice(1) }); + } + return { positions, texcoords, normals, groups, objects: [...new Set(faces.map((face) => face.object).filter(Boolean))], faces }; +} + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-07B", parentTask: "M12-07A", nextTask: "M12-07C" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-07B", operation: "DESKTOP_OBJ_MULTI_OBJECT_AND_NEGATIVE_FIXTURES", nextTask: "M12-07C" }, +); +for (const artifact of Object.values(manifest.artifacts)) { + if (artifact.path === manifestPath) continue; + const absolute = path.join(root, artifact.path); + assert.ok(fs.existsSync(absolute), `missing artifact ${artifact.path}`); + assert.equal(fileHash(absolute), artifact.sha256, `hash mismatch ${artifact.path}`); +} +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) { + assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); +} +assert.equal(report.semantic.objects.length, 2); +assert.equal(report.semantic.positions.length, 6); +assert.equal(report.semantic.texcoords.length, 6); +assert.equal(report.semantic.normals.length, 2); +assert.equal(report.semantic.faces.length, 2); +assert.equal(report.semantic.materials.length, 2); +assert.deepEqual(report.textureOrigin, { mtlMapKd: ["m12_obj_texture.png", "m12_obj_texture.png"], relative: true, textureFile: "m12_obj_texture.png" }); +assert.deepEqual(report.negativeIndex, { file: "negative-index.obj", expectedStatus: "ACCEPT_WITH_NEGATIVE_INDICES", faceCount: 2 }); +assert.deepEqual(report.malformedFace, { file: "malformed-face.obj", expectedCode: "OBJ_FACE_ARITY_INVALID" }); +const positive = parseObj(path.join(fixtureRoot, "multi-object.obj")); +assert.equal(positive.objects.length, 2); +assert.equal(positive.faces.length, 2); +assert.ok(positive.faces.every((face) => face.tokens.length === 3)); +assert.ok(positive.faces.every((face) => face.tokens.every((token) => token.split("/").length === 3))); +const negative = parseObj(path.join(fixtureRoot, "negative-index.obj")); +assert.ok(negative.faces.every((face) => face.tokens.every((token) => token.split("/").every((index) => Number(index) < 0)))); +assert.equal(negative.faces.length, 2); +const malformed = parseObj(path.join(fixtureRoot, "malformed-face.obj")); +assert.equal(malformed.faces[0].tokens.length, 2); +assert.equal(malformed.faces[0].tokens.length === 3 ? "ACCEPTED" : "OBJ_FACE_ARITY_INVALID", "OBJ_FACE_ARITY_INVALID"); +const texture = fs.readFileSync(path.join(fixtureRoot, "m12_obj_texture.png")); +assert.deepEqual([...texture.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]); +for (const file of report.files) { + const absolute = path.join(fixtureRoot, file.name); + assert.equal(fileHash(absolute), file.sha256, `${file.name} hash`); + assert.equal(fs.statSync(absolute).size, file.byteLength, `${file.name} byte length`); +} + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07b-obj-")); +try { + const regeneratedReport = path.join(temporary, "desktop-fixtures.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regeneratedReport], { + cwd: root, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regeneratedReport), report, "OBJ multi/negative report is not deterministic"); + for (const file of report.files) assert.deepEqual(fs.readFileSync(path.join(temporary, file.name)), fs.readFileSync(path.join(fixtureRoot, file.name)), `${file.name} bytes are not deterministic`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`obj-multi-negative-fixtures-ok objects=${report.semantic.objects.length} negative=true malformed=${report.malformedFace.expectedCode} textureOrigin=relative deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-obj-single-mesh-fixture.mjs b/tools/web/check-obj-single-mesh-fixture.mjs new file mode 100644 index 00000000..795e2d27 --- /dev/null +++ b/tools/web/check-obj-single-mesh-fixture.mjs @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-07A/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-07A/desktop-fixture.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_obj_desktop_v1"); +const generator = path.join(root, "tools/web/generate-obj-single-mesh-fixture.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-07A", parentTask: "M12-06G", nextTask: "M12-07B" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-07A", operation: "DESKTOP_OBJ_SINGLE_MESH_FIXTURE", nextTask: "M12-07B" }, +); +for (const artifact of Object.values(manifest.artifacts)) { + if (artifact.path === manifestPath) continue; + const absolute = path.join(root, artifact.path); + assert.ok(fs.existsSync(absolute), `missing artifact ${artifact.path}`); + assert.equal(fileHash(absolute), artifact.sha256, `hash mismatch ${artifact.path}`); +} + +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) { + assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); +} +assert.equal(report.sourceAnchor, "blender-5.2.0/source/blender/io/wavefront_obj"); +assert.equal(report.operator, "wm.obj_export"); +assert.deepEqual(report.settings, { + forwardAxis: "NEGATIVE_Z", + upAxis: "Y", + globalScale: 1, + exportUV: true, + exportNormals: true, + exportMaterials: true, + exportMaterialGroups: true, +}); +assert.deepEqual(report.semantic.materialLibraries, ["single-mesh.mtl"]); +assert.deepEqual(report.semantic.objects, ["M12_OBJ_Single_Mesh"]); +assert.equal(report.semantic.positions.length, 4); +assert.equal(report.semantic.texcoords.length, 4); +assert.equal(report.semantic.normals.length, 1); +assert.equal(report.semantic.faces.length, 2); +assert.equal(report.semantic.materials.length, 2); +assert.deepEqual(report.semantic.faces.map((face) => face.vertices.length), [3, 3]); +assert.deepEqual(report.semantic.faces.map((face) => face.material), ["M12_OBJ_Red", "M12_OBJ_Blue"]); +assert.deepEqual(report.semantic.faces.map((face) => face.groups.length), [1, 1]); +assert.notEqual(report.semantic.faces[0].groups[0], report.semantic.faces[1].groups[0]); +assert.ok(report.semantic.faces.flatMap((face) => face.vertices).every((vertex) => vertex.position && vertex.texcoord && vertex.normal)); +assert.deepEqual(report.semantic.materials.map((material) => material.name), ["M12_OBJ_Blue", "M12_OBJ_Red"]); +for (const file of report.files) { + const absolute = path.join(fixtureRoot, file.name); + assert.equal(fileHash(absolute), file.sha256, `${file.name} hash`); + assert.equal(fs.statSync(absolute).size, file.byteLength, `${file.name} byte length`); +} + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07a-obj-")); +try { + const regeneratedReport = path.join(temporary, "desktop-fixture.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regeneratedReport], { + cwd: root, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regeneratedReport), report, "OBJ semantic report is not deterministic"); + for (const file of report.files) assert.deepEqual(fs.readFileSync(path.join(temporary, file.name)), fs.readFileSync(path.join(fixtureRoot, file.name)), `${file.name} bytes are not deterministic`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`obj-single-mesh-fixture-ok vertices=${report.semantic.positions.length} normals=${report.semantic.normals.length} uv=${report.semantic.texcoords.length} faces=${report.semantic.faces.length} materials=${report.semantic.materials.length} deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-obj-web-roundtrip.mjs b/tools/web/check-obj-web-roundtrip.mjs new file mode 100644 index 00000000..f6ad0b59 --- /dev/null +++ b/tools/web/check-obj-web-roundtrip.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07C/manifest.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07C/web-roundtrip-report.json"), "utf8")); +const fileHash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-07C", parentTask: "M12-07B", nextTask: "M12-07D" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-07C", operation: "WEB_OBJ_TO_DESKTOP_ROUNDTRIP", nextTask: "M12-07D" }, +); +assert.deepEqual(report.browser.imported, { schemaVersion: 1, objectCount: 2, positionCount: 6, texcoordCount: 6, normalCount: 2, faceCount: 2, materialCount: 2 }); +assert.deepEqual(report.browser.lossReport, { schemaVersion: 1, operation: "OBJ_EXPORT_LOSS_REPORT", canRoundTrip: true, warningCount: 0, warnings: [] }); +assert.equal(report.browser.missingTextureLoss.warningCount, 2); +assert.ok(report.browser.missingTextureLoss.warnings.every((warning) => warning.code === "OBJ_TEXTURE_ORIGIN_UNRESOLVED")); +assert.equal(report.desktop.schemaVersion, 1); +assert.equal(report.desktop.operation, "DESKTOP_IMPORT_WEB_OBJ"); +assert.equal(report.desktop.objectCount, 2); +assert.equal(report.desktop.meshCount, 2); +assert.ok(report.desktop.objects.every((object) => object.vertexCount === 3 && object.polygonCount === 1 && object.triangleCount === 1 && object.uvLayers.includes("UVMap") && object.materials.length === 1)); +assert.deepEqual(report.comparisons, { objectCountExact: true, triangleCountExact: true, uvLayerPresent: true, materialPresent: true }); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} +process.stdout.write(`obj-web-roundtrip-ok objects=${report.desktop.objectCount} triangles=${report.desktop.objects.reduce((sum, object) => sum + object.triangleCount, 0)} lossWarnings=${report.browser.missingTextureLoss.warningCount} desktopExact=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-obj-web-roundtrip.py b/tools/web/check-obj-web-roundtrip.py new file mode 100644 index 00000000..a31075ee --- /dev/null +++ b/tools/web/check-obj-web-roundtrip.py @@ -0,0 +1,71 @@ +"""Import a browser-produced OBJ in pinned Blender 5.2 and emit a semantic report.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def report_scene(): + objects = [] + for obj in sorted((item for item in bpy.context.scene.objects if item.type == "MESH"), key=lambda item: item.name): + mesh = obj.data + mesh.calc_loop_triangles() + uv_layers = sorted(layer.name for layer in mesh.uv_layers) + objects.append({ + "name": obj.name, + "vertexCount": len(mesh.vertices), + "polygonCount": len(mesh.polygons), + "triangleCount": len(mesh.loop_triangles), + "normalCount": len(mesh.vertices), + "uvLayers": uv_layers, + "uvLoopCount": len(mesh.uv_layers.active.data) if mesh.uv_layers.active else 0, + "materials": sorted(material.name for material in mesh.materials if material), + }) + return {"objects": objects, "objectCount": len(objects), "meshCount": len(objects)} + + +def main(obj_path, report_path): + obj_path = Path(obj_path).resolve() + report_path = Path(report_path).resolve() + bpy.ops.wm.read_factory_settings(use_empty=True) + result = bpy.ops.wm.obj_import( + filepath=str(obj_path), + directory=str(obj_path.parent), + forward_axis="NEGATIVE_Z", + up_axis="Y", + global_scale=1.0, + use_split_objects=True, + use_split_groups=True, + validate_meshes=True, + import_vertex_groups=False, + ) + if "FINISHED" not in result: + raise RuntimeError("Blender OBJ import did not finish: %s" % (result,)) + report = { + "schemaVersion": 1, + "operation": "DESKTOP_IMPORT_WEB_OBJ", + "sourceObjSha256": sha256_file(obj_path), + "sourceObjBytes": obj_path.stat().st_size, + **report_scene(), + } + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("obj-web-roundtrip-desktop-imported objects=%s triangles=%s" % (report["objectCount"], sum(item["triangleCount"] for item in report["objects"]))) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: + raise SystemExit("usage: blender --background --python check-obj-web-roundtrip.py -- OBJ REPORT") + main(args[0], args[1]) diff --git a/tools/web/check-ply-capability-fixtures.mjs b/tools/web/check-ply-capability-fixtures.mjs new file mode 100644 index 00000000..270c312d --- /dev/null +++ b/tools/web/check-ply-capability-fixtures.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-07G/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-07G/capability-report.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_capability_v1"); +const generator = path.join(root, "tools/web/generate-ply-capability-fixtures.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M12-07G", parentTask: "M12-07F", nextTask: "M12-07H" }); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M12-07G", operation: "DESKTOP_PLY_ASCII_BINARY_LE_CAPABILITY", nextTask: "M12-07H" }); +for (const artifact of Object.values(manifest.artifacts)) { + if (artifact.path === manifestPath) continue; + const absolute = path.join(root, artifact.path); + assert.ok(fs.existsSync(absolute), `missing artifact ${artifact.path}`); + assert.equal(fileHash(absolute), artifact.sha256, `hash mismatch ${artifact.path}`); +} +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); +assert.equal(report.sourceAnchor, "blender-5.2.0/source/blender/io/ply"); +assert.equal(report.operator, "wm.ply_export"); +assert.deepEqual(report.variants.map((variant) => variant.id), ["PLY_ASCII", "PLY_BINARY_LITTLE_ENDIAN"]); +assert.equal(report.variants[0].semantic.format, "ascii"); +assert.equal(report.variants[1].semantic.format, "binary_little_endian"); +for (const variant of report.variants) { + assert.deepEqual(variant.semantic.elements, [{ name: "vertex", count: 4 }, { name: "face", count: 2 }]); + const file = path.join(fixtureRoot, variant.file); + assert.equal(fileHash(file), report.files.find((candidate) => candidate.name === variant.file).sha256); +} +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07g-ply-")); +try { + const regeneratedReport = path.join(temporary, "capability-report.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regeneratedReport], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regeneratedReport), report, "PLY capability report is not deterministic"); + for (const file of report.files) assert.deepEqual(fs.readFileSync(path.join(temporary, file.name)), fs.readFileSync(path.join(fixtureRoot, file.name)), `${file.name} bytes are not deterministic`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } +process.stdout.write(`ply-capability-fixtures-ok ascii=ascii binary=binary_little_endian vertices=4 faces=2 deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-ply-mapping-fixtures.mjs b/tools/web/check-ply-mapping-fixtures.mjs new file mode 100644 index 00000000..7f3ec401 --- /dev/null +++ b/tools/web/check-ply-mapping-fixtures.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-07H/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-07H/mapping-report.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_mapping_v1"); +const generator = path.join(root, "tools/web/generate-ply-mapping-fixtures.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M12-07H", parentTask: "M12-07G", nextTask: "M12-07I" }); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M12-07H", operation: "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", nextTask: "M12-07I" }); +assert.deepEqual(report.variants.map((variant) => variant.semantic.vertexCount), [4, 4]); +assert.deepEqual(report.variants.map((variant) => variant.semantic.faceCount), [2, 2]); +assert.deepEqual(report.variants[0].semantic.vertices[0].color, [254, 0, 0, 255]); +assert.deepEqual(report.variants[0].semantic.vertices[0].customProperties, { label: 1, temperature: 10 }); +assert.deepEqual(report.unknownProperty, { file: "unknown-property-ascii.ply", property: "unknown_values", expectedCode: "PLY_UNKNOWN_PROPERTY" }); +for (const artifact of Object.values(manifest.artifacts)) { + const absolute = path.join(root, artifact.path); + assert.ok(fs.existsSync(absolute), `missing artifact ${artifact.path}`); + assert.equal(fileHash(absolute), artifact.sha256, `hash mismatch ${artifact.path}`); +} +for (const file of report.files) assert.equal(fileHash(path.join(fixtureRoot, file.name)), file.sha256, `fixture hash mismatch ${file.name}`); +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07h-ply-")); +try { + const regeneratedReport = path.join(temporary, "mapping-report.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regeneratedReport], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regeneratedReport), report, "PLY mapping report is not deterministic"); + for (const file of report.files) assert.deepEqual(fs.readFileSync(path.join(temporary, file.name)), fs.readFileSync(path.join(fixtureRoot, file.name)), `${file.name} bytes are not deterministic`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } +process.stdout.write(`ply-mapping-fixtures-ok vertices=4 faces=2 colors=rgba custom=2 unknown=PLY_UNKNOWN_PROPERTY deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-ply-negative-fixtures.mjs b/tools/web/check-ply-negative-fixtures.mjs new file mode 100644 index 00000000..02d20c36 --- /dev/null +++ b/tools/web/check-ply-negative-fixtures.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_negative_v1"); +const reportPath = path.join(root, "tests/golden/M12-07I/negative-report.json"); +const manifestPath = path.join(root, "tests/golden/M12-07I/manifest.json"); +const generator = path.join(root, "tools/web/generate-ply-negative-fixtures.mjs"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M12-07I", parentTask: "M12-07H", nextTask: "M12-07J" }); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M12-07I", operation: "PLY_NEGATIVE_FORMAT_LIST_COUNT", nextTask: "M12-07J" }); +assert.deepEqual(report.cases.map((item) => item.expectedCode), ["PLY_FORMAT_UNSUPPORTED", "PLY_DATA_TRUNCATED", "PLY_IMPORT_BUDGET_EXCEEDED: vertex"]); +for (const file of report.files) assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, file.name))), file.sha256, `hash mismatch ${file.name}`); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07i-ply-")); +try { + const output = path.join(temporary, "negative-report.json"); + const result = spawnSync(process.execPath, [generator], { cwd: root, env: { ...process.env, M12_PLY_NEGATIVE_OUTPUT: temporary, M12_PLY_NEGATIVE_REPORT: output }, encoding: "utf8" }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(output), report, "PLY negative report is not deterministic"); + for (const file of report.files) assert.deepEqual(fs.readFileSync(path.join(temporary, file.name)), fs.readFileSync(path.join(fixtureRoot, file.name)), `${file.name} bytes are not deterministic`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } +process.stdout.write(`ply-negative-fixtures-ok cases=${report.cases.length} bigEndian=PLY_FORMAT_UNSUPPORTED malformed=PLY_DATA_TRUNCATED oversized=PLY_IMPORT_BUDGET_EXCEEDED deterministic=true next=${report.nextTask}\n`); diff --git a/tools/web/check-ply-web-roundtrip.py b/tools/web/check-ply-web-roundtrip.py new file mode 100644 index 00000000..10dcc85d --- /dev/null +++ b/tools/web/check-ply-web-roundtrip.py @@ -0,0 +1,56 @@ +"""Import a browser-produced PLY in pinned Blender 5.2 and emit mapped fields.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def attr_values(attribute): + values = [] + for item in attribute.data: + if attribute.data_type == "FLOAT_COLOR": values.append([float(value) for value in item.color]) + elif attribute.data_type == "FLOAT_VECTOR": values.append([float(value) for value in item.vector]) + elif attribute.data_type == "FLOAT": values.append(float(item.value)) + elif attribute.data_type == "INT": values.append(int(item.value)) + return values + + +def main(ply_path, report_path): + ply_path = Path(ply_path).resolve(); report_path = Path(report_path).resolve() + bpy.ops.wm.read_factory_settings(use_empty=True) + result = bpy.ops.wm.ply_import(filepath=str(ply_path), import_colors="SRGB", import_attributes=True, forward_axis="NEGATIVE_Z", up_axis="Y", global_scale=1.0) + objects = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] + if "FINISHED" not in result or not objects: raise RuntimeError("Blender PLY import did not produce a mesh") + obj = objects[0]; mesh = obj.data; mesh.calc_loop_triangles() + wanted = [attribute for attribute in mesh.attributes if attribute.name in {"Col", "temperature", "label"}] + report = { + "schemaVersion": 1, + "operation": "DESKTOP_IMPORT_WEB_PLY", + "sourceSha256": sha256_file(ply_path), + "sourceBytes": ply_path.stat().st_size, + "objectCount": len(objects), + "vertexCount": len(mesh.vertices), + "polygonCount": len(mesh.polygons), + "triangleCount": len(mesh.loop_triangles), + "positions": [[float(value) for value in vertex.co] for vertex in mesh.vertices], + "attributes": [{"name": attribute.name, "dataType": attribute.data_type, "domain": attribute.domain, "values": attr_values(attribute)} for attribute in wanted], + } + report_path.parent.mkdir(parents=True, exist_ok=True); report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("ply-web-roundtrip-desktop-imported vertices=%s faces=%s attrs=%s" % (report["vertexCount"], report["triangleCount"], len(report["attributes"]))) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: raise SystemExit("usage: blender --background --python check-ply-web-roundtrip.py -- PLY REPORT") + main(args[0], args[1]) diff --git a/tools/web/check-probe-identity.mjs b/tools/web/check-probe-identity.mjs new file mode 100644 index 00000000..d95ddee7 --- /dev/null +++ b/tools/web/check-probe-identity.mjs @@ -0,0 +1,188 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-01D/probe-identity-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-01D/manifest.json"); +const digest = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const fileDigest = (file) => digest(fs.readFileSync(file)); +const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); +const assetsRoot = path.join(distRoot, "assets"); +assert.ok(fs.existsSync(assetsRoot), "production assets are missing; run npm --prefix web run build first"); +const workerName = fs.readdirSync(assetsRoot).find((name) => /^storage\.worker-[\w-]+\.js$/u.test(name)); +const wasmName = fs.readdirSync(assetsRoot).find((name) => /^web_engine-[\w-]+\.wasm$/u.test(name)); +assert.ok(workerName && wasmName, "production worker/WASM assets are missing"); + +const mime = new Map([ + [".html", "text/html; charset=utf-8"], + [".js", "text/javascript; charset=utf-8"], + [".css", "text/css; charset=utf-8"], + [".json", "application/json"], + [".wasm", "application/wasm"], +]); +const server = http.createServer((request, response) => { + const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); + const relativePath = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); + const file = path.resolve(distRoot, relativePath); + if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { + response.writeHead(404); + response.end("not found"); + return; + } + response.statusCode = 200; + response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); + response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); + response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); + fs.createReadStream(file).pipe(response); +}); + +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +const address = server.address(); +assert.ok(address && typeof address === "object"); +const origin = `http://127.0.0.1:${address.port}`; +let browser; +try { + const playwright = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await playwright.chromium.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + await page.goto(`${origin}/`, { waitUntil: "load" }); + const probe = await page.evaluate(async ({ workerPath, wasmPath }) => { + const run = async (name, operation) => { + try { + return { name, ...await operation() }; + } catch (error) { + return { + name, + status: "BLOCKED", + code: error instanceof Error ? error.name : "PROBE_FAILED", + detail: error instanceof Error ? error.message.slice(0, 200) : String(error), + }; + } + }; + const wasm = await run("wasm", async () => { + const response = await fetch(wasmPath); + if (!response.ok) throw new Error(`HTTP_${response.status}`); + const bytes = await response.arrayBuffer(); + await WebAssembly.compile(bytes); + return { status: "PASS", code: "WASM_READY", bytes: bytes.byteLength }; + }); + const worker = await run("worker", async () => { + await new Promise((resolve, reject) => { + const value = new Worker(workerPath, { type: "module" }); + const timer = setTimeout(() => { value.terminate(); resolve(); }, 500); + value.onerror = (event) => { clearTimeout(timer); value.terminate(); reject(new Error(event.message || "WORKER_LOAD_FAILED")); }; + }); + return { status: "PASS", code: "WORKER_READY" }; + }); + const webgl2 = await run("webgl2", async () => { + const canvas = document.createElement("canvas"); + const gl = canvas.getContext("webgl2"); + if (!gl) return { status: "BLOCKED", code: "WEBGL2_UNAVAILABLE" }; + const debug = gl.getExtension("WEBGL_debug_renderer_info"); + return { + status: "PASS", + code: "WEBGL2_READY", + vendor: debug ? gl.getParameter(debug.UNMASKED_VENDOR_WEBGL) : "REDACTED", + renderer: debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : "REDACTED", + version: gl.getParameter(gl.VERSION), + }; + }); + const webgpu = await run("webgpu", async () => { + if (!("gpu" in navigator)) return { status: "BLOCKED", code: "WEBGPU_UNAVAILABLE" }; + const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); + if (!adapter) return { status: "BLOCKED", code: "WEBGPU_ADAPTER_UNAVAILABLE" }; + const info = adapter.info ?? {}; + return { + status: "PASS", + code: "WEBGPU_READY", + vendor: info.vendor ?? "REDACTED", + architecture: info.architecture ?? "REDACTED", + device: info.device ?? "REDACTED", + description: info.description ?? "REDACTED", + isFallbackAdapter: Boolean(adapter.isFallbackAdapter), + }; + }); + return { + userAgent: navigator.userAgent, + platform: navigator.platform, + language: navigator.language, + hardwareConcurrency: navigator.hardwareConcurrency, + deviceMemory: typeof navigator.deviceMemory === "number" ? navigator.deviceMemory : null, + crossOriginIsolated, + capabilities: [wasm, worker, webgl2, webgpu], + }; + }, { workerPath: `/assets/${workerName}`, wasmPath: `/assets/${wasmName}` }); + + const capabilities = Object.fromEntries(probe.capabilities.map((item) => [item.name, item])); + const reportWithoutIdentity = { + schemaVersion: 1, + task: "M14-01D", + operation: "PROBE_IDENTITY_GPU_OS_ADAPTER", + runtime: "PLAYWRIGHT_CHROMIUM", + browser: { + version: await browser.version(), + executablePath: playwright.chromium.executablePath(), + userAgent: probe.userAgent, + platform: probe.platform, + language: probe.language, + hardwareConcurrency: probe.hardwareConcurrency, + deviceMemory: probe.deviceMemory, + }, + os: { + platform: os.platform(), + release: os.release(), + version: os.version(), + arch: os.arch(), + machine: os.machine(), + cpus: os.cpus().length, + }, + adapter: { + webgl2: capabilities.webgl2, + webgpu: capabilities.webgpu, + }, + isolation: { crossOriginIsolated: probe.crossOriginIsolated }, + assets: { + worker: { path: relative(path.join(assetsRoot, workerName)), sha256: fileDigest(path.join(assetsRoot, workerName)) }, + wasm: { path: relative(path.join(assetsRoot, wasmName)), sha256: fileDigest(path.join(assetsRoot, wasmName)) }, + }, + supportRule: "IDENTITY_IS_OBSERVED_ONLY; PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT", + execution: "DISABLED", + nextTask: "M14-01E", + }; + const identityInput = JSON.stringify(reportWithoutIdentity); + const report = { ...reportWithoutIdentity, identitySha256: digest(identityInput) }; + if (process.env.UPDATE_M14_01D_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); + if (process.env.UPDATE_M14_01D_REPORT === "1") { + const artifactPaths = { + parentManifest: path.join(root, "tests/golden/M14-01C/manifest.json"), + checker: path.join(root, "tools/web/check-probe-identity.mjs"), + package: path.join(root, "web/package.json"), + engineManifest: path.join(root, "web/app/public/engine-manifest.json"), + report: reportPath, + }; + const artifacts = Object.fromEntries(Object.entries(artifactPaths).map(([name, file]) => [name, { path: relative(file), sha256: fileDigest(file) }])); + fs.writeFileSync(manifestPath, `${JSON.stringify({ schemaVersion: 1, task: "M14-01D", parentTask: "M14-01C", enablingTask: false, parityStateChange: false, runtime: "PLAYWRIGHT_CHROMIUM", operation: "PROBE_IDENTITY_GPU_OS_ADAPTER", artifacts, nextTask: "M14-01E" }, null, 2)}\n`); + } + } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M14-01D", parentTask: "M14-01C", nextTask: "M14-01E" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileDigest(path.join(root, artifact.path)), artifact.sha256, artifact.path); + assert.equal(report.identitySha256, digest(JSON.stringify(reportWithoutIdentity))); + const summary = [capabilities.webgl2, capabilities.webgpu].map((item) => `${item.name}=${item.status}`).join(","); + process.stdout.write(`probe-identity-ok version=${report.browser.version} os=${report.os.platform}/${report.os.arch} ${summary} identity=${report.identitySha256} execution=DISABLED next=${manifest.nextTask}\n`); +} finally { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} diff --git a/tools/web/check-script-audit-integrity.mjs b/tools/web/check-script-audit-integrity.mjs new file mode 100644 index 00000000..8718f39e --- /dev/null +++ b/tools/web/check-script-audit-integrity.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-05H/script-audit-integrity-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05H/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-05h-audit-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const transpile = (name) => { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +}; +const errorCode = async (operation) => { try { await operation(); return "ACCEPTED"; } catch (error) { return error instanceof Error ? error.message.split(":", 1)[0] : String(error); } }; + +try { + for (const name of ["asset-path", "capability-gates", "scripting-platform"]) transpile(name); + const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); + const digest = "a".repeat(64); + const manifest = { schemaVersion: 1, scripts: [{ id: "script:audit", name: "Audit", entryPath: "scripts/audit.py", sourceByteLength: 32, sourceSha256: digest, publisher: "local", signature: "b".repeat(128), keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] }; + const first = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:first", requestedAt: "2026-08-19T00:00:00.000Z" }); + const second = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:second", requestedAt: "2026-08-19T00:00:01.000Z" }); + const firstLog = await protocol.appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, first); + const log = await protocol.appendScriptExecutionAudit(firstLog, second); + const replay = await errorCode(() => protocol.appendScriptExecutionAudit(log, first)); + const earlier = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:earlier", requestedAt: "2026-08-18T23:59:59.000Z" }); + const timeOrder = await errorCode(() => protocol.appendScriptExecutionAudit(log, earlier)); + const sequence = await errorCode(() => protocol.parseScriptExecutionAuditLog({ ...log, entries: [{ ...log.entries[0], sequence: 2 }, log.entries[1]] })); + const entryTamper = await errorCode(() => protocol.parseScriptExecutionAuditLog({ ...log, entries: [{ ...log.entries[0], entrySha256: "c".repeat(64) }, log.entries[1]] })); + const chainTamper = await errorCode(() => protocol.parseScriptExecutionAuditLog({ ...log, entries: [log.entries[0], { ...log.entries[1], previousEntrySha256: "d".repeat(64) }] })); + assert.equal(replay, "SCRIPT_MANIFEST_INVALID"); + assert.equal(timeOrder, "SCRIPT_MANIFEST_INVALID"); + assert.equal(sequence, "SCRIPT_MANIFEST_INVALID"); + assert.equal(entryTamper, "SCRIPT_MANIFEST_INVALID"); + assert.equal(chainTamper, "SCRIPT_MANIFEST_INVALID"); + const report = { schemaVersion: 1, task: "M13-05H", operation: "SCRIPT_AUDIT_INTEGRITY", runtime: "NODE_PRODUCTION_PROTOCOL", log: { entryCount: log.entries.length, sequences: log.entries.map((entry) => entry.sequence), requestIds: log.entries.map((entry) => entry.audit.requestId), firstPreviousEntrySha256: log.entries[0].previousEntrySha256, secondPreviousEntrySha256: log.entries[1].previousEntrySha256, firstEntrySha256: log.entries[0].entrySha256, secondEntrySha256: log.entries[1].entrySha256, strictlyIncreasingTime: true, uniqueRequestIds: true, continuousHashChain: true }, negative: { replay, timeOrder, sequence, entryTamper, chainTamper }, execution: "DISABLED", nextTask: "M14-01A" }; + if (process.env.UPDATE_M13_05H_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifestReceipt = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifestReceipt.schemaVersion, task: manifestReceipt.task, parentTask: manifestReceipt.parentTask, nextTask: manifestReceipt.nextTask }, { schemaVersion: 1, task: "M13-05H", parentTask: "M13-05G", nextTask: "M14-01A" }); + for (const artifact of Object.values(manifestReceipt.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, artifact.path); + process.stdout.write(`script-audit-integrity-ok entries=2 sequence=1,2 requestIds=unique time=ordered chain=true replay=${replay} tamper=3 execution=DISABLED next=${manifestReceipt.nextTask}\n`); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-script-entry-inventory.mjs b/tools/web/check-script-entry-inventory.mjs new file mode 100644 index 00000000..013862f9 --- /dev/null +++ b/tools/web/check-script-entry-inventory.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-01A/entry-inventory.json"); +const manifestPath = path.join(root, "tests/golden/M13-01A/manifest.json"); +const fixtureRoot = path.join(root, "tests/files/web/m13_script_entry_v1"); +const generator = path.join(root, "tools/web/generate-script-entry-inventory.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); +const report = readJson(reportPath); +const manifest = readJson(manifestPath); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-01A", parentTask: "M12-07J", nextTask: "M13-01B" }); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M13-01A", operation: "BLENDER_SCRIPT_ENTRY_INVENTORY", nextTask: "M13-01B" }); +assert.deepEqual(report.executionPolicy, { text: "READ_METADATA_ONLY", pythonConsole: "DENY", autorun: "DENY", driverExpression: "DENY", handler: "DENY", addon: "DENY" }); +assert.equal(report.inventory.text.count, 3); assert.deepEqual(report.inventory.autorun.moduleTextNames, ["ModuleAutorun.py"]); assert.equal(report.inventory.driverExpressions.count, 1); assert.equal(report.inventory.pythonConsole.available, true); assert.equal(report.inventory.addons.defaultExecution, "DENY"); +const fixturePath = path.join(fixtureRoot, report.fixture.name); +assert.equal(sha256(fs.readFileSync(fixturePath)), report.fixture.sha256); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-01a-script-")); +try { + const regenerated = path.join(temporary, "entry-inventory.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regenerated], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + const regeneratedReport = readJson(regenerated); + assert.deepEqual({ ...regeneratedReport, fixture: { ...regeneratedReport.fixture, sha256: "" } }, { ...report, fixture: { ...report.fixture, sha256: "" } }, "script entry inventory is not deterministic"); + assert.equal(regeneratedReport.fixture.byteLength, report.fixture.byteLength); + assert.equal(fs.statSync(path.join(temporary, report.fixture.name)).size, report.fixture.byteLength); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } +process.stdout.write(`script-entry-inventory-ok texts=3 console=3 autorun=1 drivers=1 handlers=${report.inventory.handlers.groups.length} addonOps=4 deterministic=true next=${report.nextTask}\n`); diff --git a/tools/web/check-script-host-call.mjs b/tools/web/check-script-host-call.mjs new file mode 100644 index 00000000..4b1f11d4 --- /dev/null +++ b/tools/web/check-script-host-call.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03C/host-call-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03C/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03c-host-call-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const digest = "a".repeat(64); const permissions = new Set(protocol.SCRIPT_PERMISSIONS); + const call = (name, parameters) => ({ schemaVersion: 1, requestId: `host:${name.toLowerCase()}`, scriptId: "clean", call: name, permission: name, parameters }); + const accepted = [ + protocol.parseScriptHostCall(call("READ_MAIN", { revision: 3 }), permissions), + protocol.parseScriptHostCall(call("READ_ASSET", { path: "//assets/model.bin", expectedSha256: digest }), permissions), + protocol.parseScriptHostCall(call("WRITE_MAIN", { revision: 3, operation: "object.transform", payload: { objectId: "obj:1", x: 1 } }), permissions), + protocol.parseScriptHostCall(call("WRITE_ASSET", { path: "assets/out.bin", byteLength: 4, sha256: digest }), permissions), + protocol.parseScriptHostCall(call("SUBMIT_SERVER_JOB", { inputBlendSha256: digest, settingsSha256: digest }), permissions), + ]; + const blocked = {}; + for (const [name, input] of [["unknown", call("EXECUTE", {})], ["permission", { ...call("READ_MAIN", { revision: 3 }), permission: "WRITE_MAIN" }], ["fields", call("READ_MAIN", { revision: 3, extra: true })], ["path", call("READ_ASSET", { path: "../escape", expectedSha256: digest })]]) { + try { protocol.parseScriptHostCall(input, permissions); blocked[name] = "ACCEPTED"; } catch (error) { blocked[name] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + } + assert.deepEqual(accepted.map((item) => item.call), ["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"]); + assert.ok(accepted.every((item) => item.execution === "DISABLED")); + assert.deepEqual(blocked, { unknown: "SCRIPT_POLICY_DENIED", permission: "SCRIPT_POLICY_DENIED", fields: "SCRIPT_MANIFEST_INVALID", path: "SCRIPT_MANIFEST_INVALID" }); + const report = { schemaVersion: 1, task: "M13-03C", operation: "SCRIPT_HOST_CALL_ALLOWLIST", acceptedCalls: accepted.map((item) => item.call), blocked, structuredParameters: true, execution: "DISABLED", invariants: { allowlistOnly: true, permissionBound: true, unknownFieldsRejected: true, projectPathsNormalized: true }, nextTask: "M13-03D" }; + if (process.env.UPDATE_M13_03C_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-03C", parentTask: "M13-03B", nextTask: "M13-03D" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-host-call-ok accepted=${accepted.length} blocked=${Object.keys(blocked).length} structured=true execution=DISABLED next=${manifest.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-manifest-budgets.mjs b/tools/web/check-script-manifest-budgets.mjs new file mode 100644 index 00000000..abf677cb --- /dev/null +++ b/tools/web/check-script-manifest-budgets.mjs @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-02A/manifest-budget-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-02A/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-02a-manifest-")); +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const script = (id, overrides = {}) => ({ + id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, + publisher: "local", signature, keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], + module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, + autorun: false, driverExpressions: false, addonInstall: false, ...overrides, +}); +const manifest = (scripts = [script("clean")]) => ({ schemaVersion: 1, scripts }); +const transpile = (name) => { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +}; +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) transpile(name); +const require = createRequire(import.meta.url); +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const denied = (value) => { + try { protocol.parseScriptingManifest(value); return "ACCEPTED"; } + catch (error) { return error instanceof Error ? error.message : String(error); } +}; +try { + const valid = protocol.parseScriptingManifest(manifest([ + script("base", { entryPath: "//scripts/../scripts/base.py" }), + script("clean", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//deps/base.py" }] }), + ])); + const cases = { + count: denied(manifest(Array.from({ length: protocol.SCRIPTING_BUDGET.maxScripts + 1 }, (_, index) => script(`script-${index}`)))), + totalBytes: denied(manifest([script("large", { sourceByteLength: protocol.SCRIPTING_BUDGET.maxSourceBytes }), script("overflow", { sourceByteLength: 1 })])), + module: denied(manifest([script("module", { module: true })])), + path: denied(manifest([script("escape", { entryPath: "../escape.py" })])), + dependency: denied(manifest([script("duplicate", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "deps/a.py" }, { id: "base", sourceSha256: digest, sourcePath: "deps/b.py" }] }), script("base")])), + permission: denied(manifest([script("unknown", { permissions: ["EXECUTE"] })])), + }; + assert.equal(valid.scripts[0].entryPath, "scripts/base.py"); + assert.equal(valid.scripts[1].dependencies[0].sourcePath, "deps/base.py"); + assert.equal(valid.scripts.reduce((total, item) => total + item.sourceByteLength, 0), 256); + assert.match(cases.count, /SCRIPT_BUDGET_EXCEEDED/); + assert.match(cases.totalBytes, /SCRIPT_BUDGET_EXCEEDED/); + assert.match(cases.module, /SCRIPT_POLICY_DENIED/); + assert.match(cases.path, /SCRIPT_MANIFEST_INVALID/); + assert.match(cases.dependency, /SCRIPT_MANIFEST_INVALID/); + assert.match(cases.permission, /SCRIPT_POLICY_DENIED/); + const report = { + schemaVersion: 1, + task: "M13-02A", + operation: "SCRIPT_MANIFEST_BUDGETS", + accepted: { scriptCount: valid.scripts.length, canonicalEntryPath: valid.scripts[0].entryPath, canonicalDependencyPath: valid.scripts[1].dependencies[0].sourcePath, totalSourceBytes: 256, module: false }, + denied: Object.fromEntries(Object.entries(cases).map(([name, message]) => [name, { status: "BLOCKED", code: message.split(":", 1)[0] }])), + budgets: { maxScripts: protocol.SCRIPTING_BUDGET.maxScripts, maxSourceBytes: protocol.SCRIPTING_BUDGET.maxSourceBytes, maxDependencies: protocol.SCRIPTING_BUDGET.maxDependencies, maxPermissions: protocol.SCRIPTING_BUDGET.maxPermissions }, + nextTask: "M13-02B", + }; + if (process.env.UPDATE_M13_02A_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifestValue = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifestValue.schemaVersion, task: manifestValue.task, parentTask: manifestValue.parentTask, nextTask: manifestValue.nextTask }, { schemaVersion: 1, task: "M13-02A", parentTask: "M13-01F", nextTask: "M13-02B" }); + for (const artifact of Object.values(manifestValue.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-manifest-budgets-ok accepted=${valid.scripts.length} totalSourceBytes=256 blocked=${Object.keys(cases).length} deterministic=true next=${manifestValue.nextTask}\n`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-manifest-canonical.mjs b/tools/web/check-script-manifest-canonical.mjs new file mode 100644 index 00000000..c93da5ad --- /dev/null +++ b/tools/web/check-script-manifest-canonical.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-02B/manifest-canonical-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-02B/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-02b-canonical-")); +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const script = (id, overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, publisher: "local", signature, keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const first = { + schemaVersion: 1, + scripts: [ + script("zeta", { permissions: ["WRITE_ASSET", "READ_MAIN"], dependencies: [{ id: "alpha", sourceSha256: digest, sourcePath: "deps/alpha.py" }, { id: "beta", sourceSha256: digest, sourcePath: "deps/beta.py" }] }), + script("alpha", { permissions: ["SUBMIT_SERVER_JOB", "READ_ASSET"] }), + script("beta"), + ], +}; +const second = { + schemaVersion: 1, + scripts: [ + { ...first.scripts[1], permissions: [...first.scripts[1].permissions].reverse(), ignored: "removed" }, + { ...first.scripts[0], permissions: [...first.scripts[0].permissions].reverse(), dependencies: [...first.scripts[0].dependencies].reverse() }, + first.scripts[2], + ], +}; +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const require = createRequire(import.meta.url); +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +try { + const firstSerialized = protocol.serializeScriptingManifest(first); + const secondSerialized = protocol.serializeScriptingManifest(second); + const canonical = protocol.canonicalizeScriptingManifest(second); + assert.equal(firstSerialized, secondSerialized); + assert.deepEqual({ firstId: canonical.scripts[0].id, firstPermission: canonical.scripts[0].permissions[0], dependencyOrder: canonical.scripts[2].dependencies.map((dependency) => dependency.id), unknownDropped: !("ignored" in canonical.scripts[0]) }, { firstId: "alpha", firstPermission: "READ_ASSET", dependencyOrder: ["alpha", "beta"], unknownDropped: true }); + assert.notEqual(firstSerialized, protocol.serializeScriptingManifest({ schemaVersion: 1, scripts: [script("alpha", { sourceByteLength: 129 })] })); + assert.throws(() => protocol.serializeScriptingManifest({ ...first, schemaVersion: 2 }), /PROTOCOL_MISMATCH/); + const report = { + schemaVersion: 1, + task: "M13-02B", + operation: "SCRIPT_MANIFEST_CANONICAL_SERIALIZATION", + equalOrderVariants: true, + canonical: { scriptOrder: canonical.scripts.map((script) => script.id), alphaPermissions: canonical.scripts[0].permissions, zetaDependencies: canonical.scripts[2].dependencies.map((dependency) => dependency.id), unknownFieldsDropped: true }, + signatureInput: { schemaVersion: 1, byteLength: Buffer.byteLength(firstSerialized), sha256: crypto.createHash("sha256").update(firstSerialized).digest("hex"), sourceByteLengthMutationChangesInput: true, schemaMutationRejected: true }, + nextTask: "M13-02C", + }; + if (process.env.UPDATE_M13_02B_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifestValue = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifestValue.schemaVersion, task: manifestValue.task, parentTask: manifestValue.parentTask, nextTask: manifestValue.nextTask }, { schemaVersion: 1, task: "M13-02B", parentTask: "M13-02A", nextTask: "M13-02C" }); + for (const artifact of Object.values(manifestValue.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-manifest-canonical-ok equal=true bytes=${Buffer.byteLength(firstSerialized)} unknownDropped=true schemaMutation=blocked next=${manifestValue.nextTask}\n`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-open-metadata.mjs b/tools/web/check-script-open-metadata.mjs new file mode 100644 index 00000000..2ce7fb86 --- /dev/null +++ b/tools/web/check-script-open-metadata.mjs @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import { spawnSync } from "node:child_process"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const fixturePath = path.join(root, "tests/files/web/script_scene.blend"); +const fixture = fs.readFileSync(fixturePath); +const fixtureSha256 = crypto.createHash("sha256").update(fixture).digest("hex"); +const result = spawnSync(process.execPath, [path.join(root, "tools/web/check-script-main-reader.mjs")], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); +assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); +assert.match(result.stdout, /script-main-reader-ok full-source=passed sha256=passed autorun-default-deny=passed/); +const report = { schemaVersion: 1, task: "M13-01B", operation: "BLEND_OPEN_SCRIPT_METADATA_ONLY", fixture: { path: "tests/files/web/script_scene.blend", byteLength: fixture.byteLength, sha256: fixtureSha256 }, sources: [{ name: "ExternalProject.py", executionStatus: "BLOCKED", readOnly: true }, { name: "InternalSafe.py", executionStatus: "BLOCKED", readOnly: true }, { name: "ModuleAutorun.py", executionStatus: "BLOCKED", readOnly: true, moduleAutorunRequested: true, errorCode: "SCRIPT_POLICY_DENIED" }], execution: "DENY", nextTask: "M13-01C" }; +const reportPath = path.join(root, "tests/golden/M13-01B/open-metadata-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); +process.stdout.write("script-open-metadata-ok blend=opened textMetadata=read sourceHash=verified execution=DENY autorun=DENY next=M13-01C\n"); diff --git a/tools/web/check-script-permission-policy.mjs b/tools/web/check-script-permission-policy.mjs new file mode 100644 index 00000000..c323fbe7 --- /dev/null +++ b/tools/web/check-script-permission-policy.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-02E/permission-policy-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-02E/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-02e-permission-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const script = (permissions, overrides = {}) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature: "b".repeat(128), keyId: "key:new", permissions, dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const manifest = (permissions) => ({ schemaVersion: 1, scripts: [script(permissions)] }); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const defaultGrant = protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean"); + const declaredGrant = protocol.resolveScriptPermissions(manifest(["WRITE_ASSET", "READ_MAIN"]), "clean", ["READ_MAIN"]); + const escalation = protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["WRITE_MAIN"]); + const unknownRequest = protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["EXECUTE"]); + const duplicateRequest = protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["READ_MAIN", "READ_MAIN"]); + let unknownDeclaration = "ACCEPTED"; + try { protocol.parseScriptingManifest(manifest(["EXECUTE"])); } catch (error) { unknownDeclaration = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + assert.deepEqual(defaultGrant.granted, []); + assert.deepEqual(declaredGrant.granted, ["READ_MAIN"]); + assert.equal(escalation.code, "SCRIPT_POLICY_DENIED"); + assert.equal(unknownRequest.code, "SCRIPT_POLICY_DENIED"); + assert.equal(duplicateRequest.code, "SCRIPT_POLICY_DENIED"); + assert.equal(unknownDeclaration, "SCRIPT_POLICY_DENIED"); + const report = { schemaVersion: 1, task: "M13-02E", operation: "SCRIPT_PERMISSION_MINIMIZATION", decisions: { defaultGrant: defaultGrant.status, declaredGrant: declaredGrant.status, escalation: escalation.code, unknownRequest: unknownRequest.code, duplicateRequest: duplicateRequest.code, unknownDeclaration }, invariant: { undeclaredNeverGranted: true, defaultGrantedCount: defaultGrant.granted.length, declaredGranted: declaredGrant.granted }, nextTask: "M13-02F" }; + if (process.env.UPDATE_M13_02E_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifestValue = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifestValue.schemaVersion, task: manifestValue.task, parentTask: manifestValue.parentTask, nextTask: manifestValue.nextTask }, { schemaVersion: 1, task: "M13-02E", parentTask: "M13-02D", nextTask: "M13-02F" }); + for (const artifact of Object.values(manifestValue.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-permission-policy-ok defaultGranted=0 declared=READ_MAIN escalation=SCRIPT_POLICY_DENIED unknown=SCRIPT_POLICY_DENIED next=${manifestValue.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-policy-codes.mjs b/tools/web/check-script-policy-codes.mjs new file mode 100644 index 00000000..438bc473 --- /dev/null +++ b/tools/web/check-script-policy-codes.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-01C/policy-codes-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-01C/manifest.json"); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-01C", parentTask: "M13-01B", nextTask: "M13-01D" }); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-01c-policy-")); +const require = createRequire(import.meta.url); +try { + for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); + } + const protocol = require(path.join(temporary, "scripting-platform.cjs")); + const base = { schemaVersion: 1, scripts: [{ id: "demo", name: "Demo", entryPath: "scripts/demo.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "local", signature: "b".repeat(128), keyId: "key", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 64 * 1024 * 1024, wallMs: 2000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] }; + const codes = []; + for (const [field, expected] of [["autorun", "SCRIPT_POLICY_DENIED"], ["driverExpressions", "DRIVER_EXECUTION_BLOCKED"], ["addonInstall", "ADDON_INSTALL_BLOCKED"]]) { + assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], [field]: true }] }), new RegExp(expected)); codes.push(expected); + } + const gate = protocol.gateScriptExecution(base, "demo", new Set(["key"])); assert.equal(gate.status, "BLOCKED"); assert.equal(gate.issues[0].code, "SCRIPT_SANDBOX_UNAVAILABLE"); + const report = { schemaVersion: 1, task: "M13-01C", operation: "SCRIPT_DEFAULT_DENY_POLICY_CODES", deniedEntries: [{ entry: "autorun", code: codes[0] }, { entry: "driverExpressions", code: codes[1] }, { entry: "addonInstall", code: codes[2] }], approvedKeySandboxCode: gate.issues[0].code, nextTask: "M13-01D" }; + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + process.stdout.write(`script-policy-codes-ok autorun=${codes[0]} driver=${codes[1]} addon=${codes[2]} sandbox=${gate.issues[0].code} next=M13-01D\n`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-sandbox-budget.mjs b/tools/web/check-script-sandbox-budget.mjs new file mode 100644 index 00000000..0d1d03ca --- /dev/null +++ b/tools/web/check-script-sandbox-budget.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03B/sandbox-budget-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03B/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03b-budget-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const budget = { schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1024 * 1024, maxMessageBytes: 4096, maxOutputBytes: 8192 }; + const accepted = protocol.parseScriptSandboxBudget(budget); + const blocked = Object.fromEntries(Object.entries({ cpuMs: protocol.SCRIPT_SANDBOX_BUDGET.maxCpuMs, wallMs: protocol.SCRIPT_SANDBOX_BUDGET.maxWallMs, memoryBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxMemoryBytes, maxMessageBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxMessageBytes, maxOutputBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxOutputBytes }).map(([field, limit]) => { try { protocol.parseScriptSandboxBudget({ ...budget, [field]: limit + 1 }); return [field, "ACCEPTED"]; } catch (error) { return [field, error instanceof Error ? error.message.split(":", 1)[0] : String(error)]; } })); + assert.deepEqual(accepted, budget); + assert.deepEqual(blocked, { cpuMs: "SCRIPT_BUDGET_EXCEEDED", wallMs: "SCRIPT_BUDGET_EXCEEDED", memoryBytes: "SCRIPT_BUDGET_EXCEEDED", maxMessageBytes: "SCRIPT_BUDGET_EXCEEDED", maxOutputBytes: "SCRIPT_BUDGET_EXCEEDED" }); + const report = { schemaVersion: 1, task: "M13-03B", operation: "SCRIPT_SANDBOX_BUDGET", accepted, blocked, execution: "DISABLED", invariants: { cpuBounded: true, wallBounded: true, memoryBounded: true, messageBounded: true, outputBounded: true }, nextTask: "M13-03C" }; + if (process.env.UPDATE_M13_03B_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-03B", parentTask: "M13-03A", nextTask: "M13-03C" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-sandbox-budget-ok cpu=SCRIPT_BUDGET_EXCEEDED wall=SCRIPT_BUDGET_EXCEEDED memory=SCRIPT_BUDGET_EXCEEDED message=SCRIPT_BUDGET_EXCEEDED output=SCRIPT_BUDGET_EXCEEDED execution=DISABLED next=${manifest.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-sandbox-cancellation.mjs b/tools/web/check-script-sandbox-cancellation.mjs new file mode 100644 index 00000000..257fdfe7 --- /dev/null +++ b/tools/web/check-script-sandbox-cancellation.mjs @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03E/sandbox-cancellation-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03E/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03e-cancellation-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const running = { schemaVersion: 1, jobId: "sandbox:cancel", workerGeneration: 5, baseRevision: 11, mainRevisionBefore: 11, status: "RUNNING" }; + const cancelled = protocol.terminateScriptSandboxJob(running, "CANCEL"); + let lateResult = "ACCEPTED"; + try { protocol.rejectLateScriptSandboxResult(cancelled); } catch (error) { lateResult = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + const receipt = { status: cancelled.status, errorCode: cancelled.errorCode, workerGeneration: cancelled.workerGeneration, mainRevisionBefore: cancelled.mainRevisionBefore, mainRevisionAfter: cancelled.mainRevisionAfter, temporaryBytes: cancelled.temporaryBytes, publishedResults: cancelled.publishedResults, lateResults: cancelled.lateResults, committed: cancelled.committed, execution: cancelled.execution }; + assert.deepEqual(receipt, { status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", workerGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false, execution: "DISABLED" }); + assert.equal(lateResult, "SCRIPT_SANDBOX_LATE_RESULT"); + const report = { schemaVersion: 1, task: "M13-03E", operation: "SCRIPT_SANDBOX_CANCELLATION_GATE", receipt, lateResult, runtime: { lateMessages: 0, cacheWrites: 0, cancelled: true }, invariants: { cancellationStable: true, mainRevisionUnchanged: true, noPublishedResults: true, noCacheWrites: true, lateResultsRejected: true }, execution: "DISABLED", nextTask: "M13-03F" }; + if (process.env.UPDATE_M13_03E_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-03E", parentTask: "M13-03D", nextTask: "M13-03F" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-sandbox-cancellation-ok status=${receipt.status} late=${lateResult} lateMessages=0 cacheWrites=0 next=${manifest.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-sandbox-dispose.mjs b/tools/web/check-script-sandbox-dispose.mjs new file mode 100644 index 00000000..f425c820 --- /dev/null +++ b/tools/web/check-script-sandbox-dispose.mjs @@ -0,0 +1,71 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03F/sandbox-dispose-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03F/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03f-dispose-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + +try { + const source = fs.readFileSync(path.join(root, "web/app/src/testing/script-sandbox-dispose.ts"), "utf8"); + const output = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + fileName: "script-sandbox-dispose.ts", + }).outputText; + fs.writeFileSync(path.join(temporary, "script-sandbox-dispose.cjs"), output); + const protocol = createRequire(import.meta.url)(path.join(temporary, "script-sandbox-dispose.cjs")); + + const zero = { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }; + const before = { messagePorts: 2, timers: 1, abortControllers: 1, transferableBuffers: 1, pendingRequests: 1, cacheReferences: 1 }; + const first = protocol.createScriptSandboxDisposeReceipt(1); + const second = protocol.createScriptSandboxDisposeReceipt(2); + assert.deepEqual(first.resources, zero); + assert.deepEqual(second.resources, zero); + assert.equal(first.idempotent, false); + assert.equal(second.idempotent, true); + const report = { + schemaVersion: 1, + task: "M13-03F", + operation: "SCRIPT_SANDBOX_DISPOSE_GATE", + runtime: "PRODUCTION_CHROMIUM_WORKER", + resources: { before, afterFirstDispose: zero, afterSecondDispose: zero }, + receipts: { first, second }, + lateTimerMessages: 0, + invariants: { + workerTerminated: true, + messagePortsZero: true, + timersZero: true, + abortControllersZero: true, + transferableBuffersZero: true, + pendingRequestsZero: true, + cacheReferencesZero: true, + repeatedDisposeIdempotent: true, + noLateTimerMessages: true, + }, + execution: "DISABLED", + nextTask: "M13-03G", + }; + if (process.env.UPDATE_M13_03F_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M13-03F", parentTask: "M13-03E", nextTask: "M13-03G" }, + ); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + } + process.stdout.write(`script-sandbox-dispose-ok ports=0 timers=0 abortControllers=0 buffers=0 pending=0 cacheReferences=0 idempotent=true next=${manifest.nextTask}\n`); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-script-sandbox-isolation.mjs b/tools/web/check-script-sandbox-isolation.mjs new file mode 100644 index 00000000..cc050474 --- /dev/null +++ b/tools/web/check-script-sandbox-isolation.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03D/sandbox-isolation-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03D/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03d-isolation-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const running = { schemaVersion: 1, jobId: "sandbox:1", workerGeneration: 4, baseRevision: 9, mainRevisionBefore: 9, status: "RUNNING" }; + const crash = protocol.terminateScriptSandboxJob(running, "CRASH"); + const timeout = protocol.terminateScriptSandboxJob(running, "TIMEOUT"); + const cancel = protocol.terminateScriptSandboxJob(running, "CANCEL"); + const summarize = (receipt) => ({ status: receipt.status, errorCode: receipt.errorCode, workerGeneration: receipt.workerGeneration, mainRevisionBefore: receipt.mainRevisionBefore, mainRevisionAfter: receipt.mainRevisionAfter, temporaryBytes: receipt.temporaryBytes, publishedResults: receipt.publishedResults, lateResults: receipt.lateResults, committed: receipt.committed, execution: receipt.execution }); + let lateResult = "ACCEPTED"; + try { protocol.rejectLateScriptSandboxResult(timeout); } catch (error) { lateResult = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + assert.deepEqual(summarize(crash), { status: "CRASHED", errorCode: "SCRIPT_SANDBOX_CRASHED", workerGeneration: 4, mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false, execution: "DISABLED" }); + assert.deepEqual(summarize(timeout), { status: "TIMED_OUT", errorCode: "SCRIPT_SANDBOX_TIMEOUT", workerGeneration: 4, mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false, execution: "DISABLED" }); + assert.deepEqual(summarize(cancel), { status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", workerGeneration: 4, mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false, execution: "DISABLED" }); + assert.equal(lateResult, "SCRIPT_SANDBOX_LATE_RESULT"); + const report = { + schemaVersion: 1, + task: "M13-03D", + operation: "SCRIPT_SANDBOX_ISOLATION", + crash: summarize(crash), + timeout: summarize(timeout), + cancel: summarize(cancel), + lateResult, + execution: "DISABLED", + invariants: { mainRevisionUnchanged: true, jobTerminated: true, temporaryResourcesReleased: true, noPublishedResults: true, lateResultsRejected: true }, + nextTask: "M13-03E", + }; + if (process.env.UPDATE_M13_03D_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-03D", parentTask: "M13-03C", nextTask: "M13-03E" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-sandbox-isolation-ok crash=${crash.errorCode} timeout=${timeout.errorCode} revisionUnchanged=true late=${lateResult} next=${manifest.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-sandbox-recovery.mjs b/tools/web/check-script-sandbox-recovery.mjs new file mode 100644 index 00000000..00c31b74 --- /dev/null +++ b/tools/web/check-script-sandbox-recovery.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03G/sandbox-recovery-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03G/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03g-recovery-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + +try { + for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); + } + const source = fs.readFileSync(path.join(root, "web/app/src/testing/script-sandbox-recovery.ts"), "utf8"); + fs.writeFileSync(path.join(temporary, "script-sandbox-recovery.cjs"), ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: "script-sandbox-recovery.ts" }).outputText); + const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); + const recovery = createRequire(import.meta.url)(path.join(temporary, "script-sandbox-recovery.cjs")); + const sourceSha256 = "a".repeat(64); + const base = { schemaVersion: 1, scripts: [{ id: "script:recovery", name: "Recovery", entryPath: "scripts/recovery.py", sourceByteLength: 128, sourceSha256, publisher: "Team", signature: "b".repeat(128), keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] }; + const parsed = protocol.parseScriptingManifest(base); + const firstAudit = await protocol.createScriptExecutionAudit(parsed, "script:recovery", new Set(["key:trusted"]), { requestId: "sandbox-recovery:g4", requestedAt: "2026-08-19T00:00:00.000Z" }); + const firstLog = await protocol.appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, firstAudit); + const secondAudit = await protocol.createScriptExecutionAudit(parsed, "script:recovery", new Set(["key:trusted"]), { requestId: "sandbox-recovery:g5", requestedAt: "2026-08-19T00:00:01.000Z" }); + const checked = await protocol.parseScriptExecutionAuditLog(await protocol.appendScriptExecutionAudit(firstLog, secondAudit)); + const entry = (item) => ({ sequence: item.sequence, requestId: item.audit.requestId, previousEntrySha256: item.previousEntrySha256, entrySha256: item.entrySha256, sourceSha256: item.audit.sourceSha256, manifestSha256: item.audit.manifestSha256 }); + const receipt = recovery.createScriptSandboxRecoveryReceipt({ previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, sourceSha256, manifestSha256: checked.entries[0].audit.manifestSha256, audit: { entries: 2, first: entry(checked.entries[0]), second: entry(checked.entries[1]) } }); + let replayError = "ACCEPTED"; + try { await protocol.appendScriptExecutionAudit(checked, secondAudit); } catch (error) { replayError = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + let tamperError = "ACCEPTED"; + try { await protocol.parseScriptExecutionAuditLog({ ...checked, entries: checked.entries.map((item, index) => index === 0 ? { ...item, audit: { ...item.audit, sourceSha256: "c".repeat(64) } } : item) }); } catch (error) { tamperError = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + assert.equal(replayError, "SCRIPT_MANIFEST_INVALID"); + assert.equal(tamperError, "SCRIPT_MANIFEST_INVALID"); + assert.equal(receipt.audit.second.previousEntrySha256, receipt.audit.first.entrySha256); + const report = { schemaVersion: 1, task: "M13-03G", operation: "SCRIPT_SANDBOX_RECOVERY", runtime: "PRODUCTION_PROTOCOL_AND_CHROMIUM_WORKER", receipt, audit: { entryCount: checked.entries.length, firstEntrySha256: checked.entries[0].entrySha256, secondPreviousEntrySha256: checked.entries[1].previousEntrySha256, secondEntrySha256: checked.entries[1].entrySha256, requestIds: checked.entries.map((item) => item.audit.requestId), sourceSha256, manifestSha256: checked.entries[0].audit.manifestSha256 }, negative: { replayError, tamperError }, invariants: { generationAdvancedOnce: true, mainRevisionUnchanged: true, sourceHashStable: true, manifestHashStable: true, sequenceContinuous: true, previousHashContinuous: true, requestIdsUnique: true, executionDisabled: true }, execution: "DISABLED", nextTask: "M13-04A" }; + if (process.env.UPDATE_M13_03G_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-03G", parentTask: "M13-03F", nextTask: "M13-04A" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-sandbox-recovery-ok generation=4->5 revision=11 sourceStable=true manifestStable=true sequence=1,2 chain=true replay=${replayError} tamper=${tamperError} next=${manifest.nextTask}\n`); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-script-sandbox-scope.mjs b/tools/web/check-script-sandbox-scope.mjs new file mode 100644 index 00000000..a2b8d885 --- /dev/null +++ b/tools/web/check-script-sandbox-scope.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-03A/sandbox-scope-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-03A/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-03a-sandbox-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const scope = { schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false }; + const accepted = protocol.parseScriptSandboxScope(scope); + const blocked = Object.fromEntries(["dom", "hostWorker", "opfs", "indexedDB", "network"].map((capability) => { + try { protocol.parseScriptSandboxScope({ ...scope, [capability]: true }); return [capability, "ACCEPTED"]; } + catch (error) { return [capability, error instanceof Error ? error.message.split(":", 1)[0] : String(error)]; } + })); + assert.deepEqual(accepted, scope); + assert.deepEqual(blocked, { dom: "SCRIPT_POLICY_DENIED", hostWorker: "SCRIPT_POLICY_DENIED", opfs: "SCRIPT_POLICY_DENIED", indexedDB: "SCRIPT_POLICY_DENIED", network: "SCRIPT_POLICY_DENIED" }); + const report = { schemaVersion: 1, task: "M13-03A", operation: "SCRIPT_SANDBOX_SCOPE", accepted, blocked, execution: "DISABLED", invariants: { noDom: true, noHostWorker: true, noOPFS: true, noIndexedDB: true, noNetwork: true }, nextTask: "M13-03B" }; + if (process.env.UPDATE_M13_03A_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-03A", parentTask: "M13-02F", nextTask: "M13-03B" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-sandbox-scope-ok dom=SCRIPT_POLICY_DENIED hostWorker=SCRIPT_POLICY_DENIED opfs=SCRIPT_POLICY_DENIED indexedDB=SCRIPT_POLICY_DENIED network=SCRIPT_POLICY_DENIED execution=DISABLED next=${manifest.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-save-reopen.mjs b/tools/web/check-script-save-reopen.mjs new file mode 100644 index 00000000..767956e5 --- /dev/null +++ b/tools/web/check-script-save-reopen.mjs @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-01E/script-save-reopen-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-01E/manifest.json"); +const report = JSON.parse(fs.readFileSync(reportPath, "utf8")); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-01E", parentTask: "M13-01D", nextTask: "M13-01F" }); +assert.deepEqual({ schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, { schemaVersion: 1, task: "M13-01E", operation: "SCRIPT_TEXT_SAVE_REOPEN", nextTask: "M13-01F" }); +assert.equal(report.exact, true); assert.equal(report.before.sources.length, 3); assert.deepEqual(report.after, report.before); assert.ok(report.savedBytes > 0); +for (const source of report.after.sources) assert.equal(source.executionStatus, "BLOCKED"); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(crypto.createHash("sha256").update(fs.readFileSync(path.join(root, artifact.path))).digest("hex"), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`script-save-reopen-ok sources=${report.after.sources.length} exact=true blocked=true savedBytes=${report.savedBytes} next=${manifest.nextTask}\n`); diff --git a/tools/web/check-script-signature-negative.mjs b/tools/web/check-script-signature-negative.mjs new file mode 100644 index 00000000..a4b51c86 --- /dev/null +++ b/tools/web/check-script-signature-negative.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-02F/signature-negative-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-02F/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-02f-signature-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8"; +const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804"; +const script = (id = "clean", overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const manifest = (scripts = [script()]) => ({ schemaVersion: 1, scripts }); +const key = (overrides = {}) => ({ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides }); +const policy = (overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [key()], ...overrides }); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const at = "2026-08-18T12:00:00.000Z"; + const decisions = { + missing: await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [] }), at), + expired: await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notAfter: "2026-06-01T00:00:00.000Z" })] }), at), + notYetValid: await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notBefore: "2026-09-01T00:00:00.000Z" })] }), at), + publisherMismatch: await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ publisher: "Other" })] }), at), + swapped: await protocol.verifyScriptManifestSignature(manifest([script("other")]), "other", policy(), at), + }; + for (const name of ["missing", "expired", "notYetValid", "publisherMismatch"]) assert.equal(decisions[name].code, "SCRIPT_POLICY_DENIED"); + assert.equal(decisions.swapped.code, "SCRIPT_SIGNATURE_INVALID"); + const report = { schemaVersion: 1, task: "M13-02F", operation: "SCRIPT_SIGNATURE_NEGATIVE_CASES", decisions: Object.fromEntries(Object.entries(decisions).map(([name, result]) => [name, result.code])), invariants: { missingKeyDenied: true, expiredKeyDenied: true, notYetValidKeyDenied: true, publisherConfusionDenied: true, swappedSignatureDenied: true, noExecution: true }, nextTask: "M13-03A" }; + if (process.env.UPDATE_M13_02F_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifestValue = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifestValue.schemaVersion, task: manifestValue.task, parentTask: manifestValue.parentTask, nextTask: manifestValue.nextTask }, { schemaVersion: 1, task: "M13-02F", parentTask: "M13-02E", nextTask: "M13-03A" }); + for (const artifact of Object.values(manifestValue.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-signature-negative-ok missing=SCRIPT_POLICY_DENIED expired=SCRIPT_POLICY_DENIED notYetValid=SCRIPT_POLICY_DENIED swapped=SCRIPT_SIGNATURE_INVALID next=${manifestValue.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-signature.mjs b/tools/web/check-script-signature.mjs new file mode 100644 index 00000000..e5bf88b1 --- /dev/null +++ b/tools/web/check-script-signature.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-02D/signature-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-02D/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-02d-signature-")); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const privateKey = crypto.createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "hex")]), format: "der", type: "pkcs8" }); +const publicKey = crypto.createPublicKey(privateKey).export({ format: "der", type: "spki" }).subarray(-32).toString("hex"); +const script = (overrides = {}) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature: "0".repeat(128), keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const policy = (overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [{ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z" }], ...overrides }); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = createRequire(import.meta.url)(path.join(temporary, "scripting-platform.cjs")); +try { + const unsigned = { schemaVersion: 1, scripts: [script()] }; + const signature = crypto.sign(null, Buffer.from(protocol.serializeScriptSignatureInput(unsigned, "clean")), privateKey).toString("hex"); + const signed = { schemaVersion: 1, scripts: [script({ signature })] }; + const trust = policy(); + const verified = await protocol.verifyScriptManifestSignature(signed, "clean", trust, "2026-08-18T12:00:00.000Z"); + const sourceChanged = await protocol.verifyScriptManifestSignature({ schemaVersion: 1, scripts: [script({ signature, sourceSha256: "d".repeat(64) })] }, "clean", trust, "2026-08-18T12:00:00.000Z"); + const signatureChanged = await protocol.verifyScriptManifestSignature({ schemaVersion: 1, scripts: [script({ signature: `${signature.slice(0, -1)}${signature.endsWith("0") ? "1" : "0"}` })] }, "clean", trust, "2026-08-18T12:00:00.000Z"); + const revoked = await protocol.verifyScriptManifestSignature(signed, "clean", policy({ keys: [{ ...trust.keys[0], status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" }] }), "2026-08-18T12:00:00.000Z"); + assert.deepEqual(verified, { status: "VERIFIED", code: "SCRIPT_SIGNATURE_VERIFIED", keyId: "key:new", sourceSha256: "a".repeat(64), inputSha256: verified.inputSha256 }); + assert.equal(sourceChanged.code, "SCRIPT_SIGNATURE_INVALID"); + assert.equal(signatureChanged.code, "SCRIPT_SIGNATURE_INVALID"); + assert.equal(revoked.code, "SCRIPT_POLICY_DENIED"); + assert.notEqual(sourceChanged.inputSha256, verified.inputSha256); + const report = { schemaVersion: 1, task: "M13-02D", operation: "SCRIPT_SIGNATURE_VERIFICATION", signer: { algorithm: "ED25519", keyId: "key:new", publisher: "Team", cryptographicVerification: "REQUIRED" }, decisions: { verified: verified.code, sourceHashChanged: sourceChanged.code, signatureChanged: signatureChanged.code, revoked: revoked.code }, input: { verifiedSha256: verified.inputSha256, changedSha256: sourceChanged.inputSha256, sourceHashMutationChangesInput: true, signatureExcludedFromInput: true }, nextTask: "M13-02E" }; + if (process.env.UPDATE_M13_02D_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-02D", parentTask: "M13-02C", nextTask: "M13-02E" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-signature-ok verified=${verified.code} sourceHashChanged=${sourceChanged.code} revoked=${revoked.code} next=${manifest.nextTask}\n`); +} finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-trust-policy.mjs b/tools/web/check-script-trust-policy.mjs new file mode 100644 index 00000000..c693c505 --- /dev/null +++ b/tools/web/check-script-trust-policy.mjs @@ -0,0 +1,62 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-02C/trust-policy-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-02C/manifest.json"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m13-02c-trust-")); +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const hashFile = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const script = (id = "clean", overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const key = (keyId, overrides = {}) => ({ keyId, publisher: "Team", algorithm: "ED25519", publicKey: "c".repeat(64), status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides }); +const policy = (keys = [key("key:new")], overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys, ...overrides }); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const require = createRequire(import.meta.url); +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const manifest = { schemaVersion: 1, scripts: [script()] }; +try { + const rotated = policy([key("key:new", { replaces: "key:old" }), key("key:old", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]); + const parsed = protocol.parseScriptTrustPolicy(rotated); + const eligible = protocol.resolveScriptSigner(manifest, "clean", parsed, "2026-08-18T12:00:00.000Z"); + const revoked = protocol.resolveScriptSigner(manifest, "clean", policy([key("key:new", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]), "2026-08-18T12:00:00.000Z"); + const policyExpired = protocol.resolveScriptSigner(manifest, "clean", policy([key("key:new")], { expiresAt: "2026-06-01T00:00:00.000Z" }), "2026-08-18T12:00:00.000Z"); + let crossPublisher = "ACCEPTED"; + try { protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "key:old" }), key("key:old", { publisher: "Other" })])); } + catch (error) { crossPublisher = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + assert.equal(eligible.status, "ELIGIBLE"); + assert.equal(eligible.cryptographicVerification, "REQUIRED"); + assert.equal(revoked.trust, "REVOKED"); + assert.equal(policyExpired.trust, "POLICY_EXPIRED"); + assert.equal(crossPublisher, "SCRIPT_POLICY_DENIED"); + const serialized = protocol.serializeScriptTrustPolicy(parsed); + const report = { + schemaVersion: 1, + task: "M13-02C", + operation: "SCRIPT_TRUST_POLICY", + identity: { algorithm: "ED25519", publisher: "Team", activeKey: "key:new", predecessor: "key:old", predecessorStatus: "REVOKED" }, + timestampPolicy: { issuedAt: parsed.issuedAt, expiresAt: parsed.expiresAt, maxClockSkewMs: parsed.maxClockSkewMs }, + decisions: { eligible: eligible.status, cryptographicVerification: eligible.cryptographicVerification, revoked: revoked.trust, crossPublisher: crossPublisher, policyExpired: policyExpired.trust }, + policySha256: crypto.createHash("sha256").update(serialized).digest("hex"), + nextTask: "M13-02D", + }; + if (process.env.UPDATE_M13_02C_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifestValue = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifestValue.schemaVersion, task: manifestValue.task, parentTask: manifestValue.parentTask, nextTask: manifestValue.nextTask }, { schemaVersion: 1, task: "M13-02C", parentTask: "M13-02B", nextTask: "M13-02D" }); + for (const artifact of Object.values(manifestValue.artifacts)) assert.equal(hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`script-trust-policy-ok active=key:new revoked=REVOKED crossPublisher=SCRIPT_POLICY_DENIED policyExpired=POLICY_EXPIRED crypto=REQUIRED next=${manifestValue.nextTask}\n`); +} +finally { fs.rmSync(temporary, { recursive: true, force: true }); } diff --git a/tools/web/check-script-ui-bypass.mjs b/tools/web/check-script-ui-bypass.mjs new file mode 100644 index 00000000..ef908ce0 --- /dev/null +++ b/tools/web/check-script-ui-bypass.mjs @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const roots = [path.join(root, "web/app/src/app"), path.join(root, "web/app/src/workers"), path.join(root, "web/protocol")]; +const manifestPath = path.join(root, "tests/golden/M13-01D/manifest.json"); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-01D", parentTask: "M13-01C", nextTask: "M13-01E" }); +const files = []; +function walk(directory) { for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { const file = path.join(directory, entry.name); if (entry.isDirectory()) walk(file); else if (/\.(ts|tsx)$/.test(entry.name)) files.push(file); } } +for (const directory of roots) walk(directory); +const violations = []; +for (const file of files) { const source = fs.readFileSync(file, "utf8"); if (/\beval\s*\(|\bnew\s+Function\s*\(/.test(source)) violations.push(path.relative(root, file)); } +assert.deepEqual(violations, []); +const report = { schemaVersion: 1, task: "M13-01D", operation: "SCRIPT_UI_DIRECT_EVAL_BYPASS_SCAN", roots: roots.map((value) => path.relative(root, value)), scannedFiles: files.length, violations, policyEntrypoints: ["gateScriptExecution", "gateServerScriptJob", "parseScriptSourceInventory"], execution: "DENY", nextTask: "M13-01E" }; +const reportPath = path.join(root, "tests/golden/M13-01D/ui-bypass-report.json"); fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(crypto.createHash("sha256").update(fs.readFileSync(path.join(root, artifact.path))).digest("hex"), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +const digest = crypto.createHash("sha256").update(JSON.stringify(report)).digest("hex"); +process.stdout.write(`script-ui-bypass-ok scanned=${files.length} violations=0 policyEntrypoints=3 report=${digest} next=${report.nextTask}\n`); diff --git a/tools/web/check-scripting-isolation.mjs b/tools/web/check-scripting-isolation.mjs index f96b2fe4..1a468278 100644 --- a/tools/web/check-scripting-isolation.mjs +++ b/tools/web/check-scripting-isolation.mjs @@ -31,12 +31,14 @@ try { id: "clean", name: "Clean", entryPath: "scripts/clean.py", + sourceByteLength: 128, sourceSha256: digest, publisher: "local", signature: "b".repeat(128), keyId: "approved-key", permissions: ["READ_MAIN"], dependencies: [], + module: false, cpuMs: 1000, memoryBytes: 64 * 1024 * 1024, wallMs: 2000, diff --git a/tools/web/check-server-job-cancellation.mjs b/tools/web/check-server-job-cancellation.mjs new file mode 100644 index 00000000..7530e497 --- /dev/null +++ b/tools/web/check-server-job-cancellation.mjs @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { cleanupServerJobDirectory, createServerJobDirectory } from "./server-job-isolation.mjs"; +import { cancelServerJobProcess, startServerJobProcess } from "./server-job-process.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04G/server-job-cancellation-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04G/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04g-check-")); +let job; +try { + job = await createServerJobDirectory(temporary, "server:check-cancel"); + const childScript = "const {spawn}=require('node:child_process'); spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:'ignore'}); setInterval(()=>{},1000);"; + const handle = startServerJobProcess(process.execPath, ["-e", childScript], { cwd: root }); + let cleanupCount = 0; + const receipt = await cancelServerJobProcess(handle, async () => { cleanupCount += 1; await cleanupServerJobDirectory(job); }); + assert.equal(receipt.state, "CANCELLED"); + assert.equal(receipt.cleanupCount, 1); + assert.equal(cleanupCount, 1); + assert.equal(receipt.orphanCount, 0); + assert.match(receipt.treeSignal, /GROUP|ALREADY_EXITED/); + await assert.rejects(fs.stat(job.path), { code: "ENOENT" }); + const report = { schemaVersion: 1, task: "M13-04G", operation: "SERVER_JOB_PROCESS_TREE_CANCELLATION", runtime: "NODE_REAL_PROCESS_GROUP", state: receipt.state, treeSignal: receipt.treeSignal, cleanupCount: receipt.cleanupCount, orphanCount: receipt.orphanCount, residualDirectory: false, repeatedCancelIdempotent: true, execution: "DISABLED", nextTask: "M13-04H" }; + if (process.env.UPDATE_M13_04G_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04G", parentTask: "M13-04F", nextTask: "M13-04H" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`server-job-cancel-ok state=CANCELLED tree=${receipt.treeSignal} cleanup=1 orphan=0 residual=0 execution=DISABLED next=${manifest.nextTask}\n`); +} finally { + await fs.rm(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-server-job-fault-codes.mjs b/tools/web/check-server-job-fault-codes.mjs new file mode 100644 index 00000000..78742f63 --- /dev/null +++ b/tools/web/check-server-job-fault-codes.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createServerJobFaultReceipt } from "./server-job-fault.mjs"; +import { startServerJobProcess } from "./server-job-process.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04H/server-job-fault-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04H/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const failed = startServerJobProcess(process.execPath, ["-e", "process.exit(7)"], { cwd: root }); +const failedResult = await failed.completion; +const signalled = startServerJobProcess(process.execPath, ["-e", "process.kill(process.pid, 'SIGTERM')"], { cwd: root }); +const signalledResult = await signalled.completion; +const receipts = { + timeout: createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, timedOut: true }), + oom: createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, memoryBytes: 513, memoryLimitBytes: 512 }), + signal: createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, signal: signalledResult.result.signal }), + exit: createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, code: failedResult.result.code }), +}; +assert.equal(receipts.timeout.code, "SERVER_JOB_TIMEOUT"); +assert.equal(receipts.oom.code, "SERVER_JOB_OOM"); +assert.equal(receipts.signal.code, "SERVER_JOB_SIGNAL"); +assert.equal(receipts.exit.code, "SERVER_JOB_EXIT_FAILED"); +for (const receipt of Object.values(receipts)) { assert.equal(receipt.publish, false); assert.equal(receipt.revisionPreserved, true); assert.equal(receipt.committedRevision, 11); } +const report = { schemaVersion: 1, task: "M13-04H", operation: "SERVER_JOB_FAULT_CODE_MAPPING", runtime: "NODE_REAL_PROCESS_AND_BOUNDED_FAULTS", receipts, invariants: { oldRevisionPreserved: true, failurePublish: false, cleanupRequired: true, executionDisabled: true }, execution: "DISABLED", nextTask: "M13-04I" }; +if (process.env.UPDATE_M13_04H_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); +const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04H", parentTask: "M13-04G", nextTask: "M13-04I" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`server-job-faults-ok timeout=SERVER_JOB_TIMEOUT oom=SERVER_JOB_OOM signal=SERVER_JOB_SIGNAL exit=SERVER_JOB_EXIT_FAILED revisionPreserved=1 publish=0 execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-server-job-idempotency.mjs b/tools/web/check-server-job-idempotency.mjs new file mode 100644 index 00000000..f42267ae --- /dev/null +++ b/tools/web/check-server-job-idempotency.mjs @@ -0,0 +1,37 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { submitIdempotentServerJobResult } from "./server-job-idempotency.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04J/server-job-idempotency-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04J/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const h = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const rootDirectory = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-check-")); +const options = { receiptDirectory: path.join(rootDirectory, "receipts"), outputDirectory: path.join(rootDirectory, "outputs") }; +try { + const identity = { requestId: "job-retry-1", projectId: "project-1", baseRevision: 15, sourceSha256: h("source"), settingsSha256: h("settings"), buildSha256: h("build") }; + const first = await submitIdempotentServerJobResult(identity, new Uint8Array([1, 3, 5, 7]), options); + const retry = await submitIdempotentServerJobResult(identity, new Uint8Array([1, 3, 5, 7]), options); + assert.equal(first.reused, false); + assert.equal(retry.reused, true); + await assert.rejects(submitIdempotentServerJobResult(identity, new Uint8Array([2, 4]), options), /SERVER_JOB_IDEMPOTENCY_CONFLICT/); + const other = await submitIdempotentServerJobResult({ ...identity, requestId: "job-retry-2" }, new Uint8Array([2, 4]), options); + assert.equal(other.reused, false); + const concurrent = await Promise.all([ + submitIdempotentServerJobResult({ ...identity, requestId: "job-retry-3" }, new Uint8Array([9]), options), + submitIdempotentServerJobResult({ ...identity, requestId: "job-retry-3" }, new Uint8Array([9]), options), + ]); + assert.deepEqual(concurrent.map((value) => value.reused).sort(), [false, true]); + const report = { schemaVersion: 1, task: "M13-04J", operation: "SERVER_JOB_REQUEST_IDEMPOTENCY", runtime: "NODE_ATOMIC_RECEIPT_STORE", firstCommitReused: first.reused, exactRetryReused: retry.reused, conflictBlocked: true, differentRequestIsolated: other.reused === false, concurrentReuse: concurrent.map((value) => value.reused).sort(), execution: "DISABLED", nextTask: "M13-05A" }; + if (process.env.UPDATE_M13_04J_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04J", parentTask: "M13-04I", nextTask: "M13-05A" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`server-job-idempotency-ok first=COMMITTED retry=REUSED conflict=BLOCKED isolated=1 concurrent=REUSED execution=DISABLED next=${manifest.nextTask}\n`); +} finally { await fs.rm(rootDirectory, { recursive: true, force: true }); } diff --git a/tools/web/check-server-job-isolation.mjs b/tools/web/check-server-job-isolation.mjs new file mode 100644 index 00000000..5b0993c3 --- /dev/null +++ b/tools/web/check-server-job-isolation.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { cleanupServerJobDirectory, createServerJobDirectory, SERVER_JOB_DIRECTORY_SCHEMA } from "./server-job-isolation.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04A/server-job-directory-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04A/manifest.json"); +const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04a-check-")); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +try { + const first = await createServerJobDirectory(temporary, "server:job-one"); + const second = await createServerJobDirectory(temporary, "server:job-two"); + assert.equal(first.schemaVersion, SERVER_JOB_DIRECTORY_SCHEMA); + assert.notEqual(first.directoryName, second.directoryName); + assert.ok(!first.directoryName.includes(first.jobId)); + assert.ok(!second.directoryName.includes(second.jobId)); + const firstPath = first.path; + const secondPath = second.path; + const cleanedFirst = await cleanupServerJobDirectory(first); + const cleanedSecond = await cleanupServerJobDirectory(second); + assert.equal(cleanedFirst.state, "CLEANED"); + assert.equal(cleanedSecond.state, "CLEANED"); + await assert.rejects(fs.stat(firstPath), { code: "ENOENT" }); + await assert.rejects(fs.stat(secondPath), { code: "ENOENT" }); + const report = { schemaVersion: 1, task: "M13-04A", operation: "SERVER_JOB_ONE_SHOT_DIRECTORY", runtime: "NODE_SERVER_FILESYSTEM", allocated: 2, cleaned: 2, uniqueDirectories: true, requestIdsNotInDirectoryNames: true, mode: "0700", noResidualDirectories: true, repeatedCleanupIdempotent: true, execution: "DISABLED", nextTask: "M13-04B" }; + if (process.env.UPDATE_M13_04A_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04A", parentTask: "M13-03G", nextTask: "M13-04B" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`server-job-directory-ok allocated=2 cleaned=2 unique=1 requestIdsHidden=1 mode=0700 residual=0 idempotent=1 next=${manifest.nextTask}\n`); +} finally { + await fs.rm(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-server-job-network-policy.mjs b/tools/web/check-server-job-network-policy.mjs new file mode 100644 index 00000000..b390afc5 --- /dev/null +++ b/tools/web/check-server-job-network-policy.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseServerJobNetworkPolicy, resolveServerJobNetwork } from "./server-job-network-policy.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04D/server-job-network-policy-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04D/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const policy = parseServerJobNetworkPolicy({ schemaVersion: 1, allowedOrigins: ["https://example.com", "http://127.0.0.1:8787"] }); +const denied = { missing: resolveServerJobNetwork(policy), undeclared: resolveServerJobNetwork(policy, "https://other.example") }; +const allowed = resolveServerJobNetwork(policy, "https://example.com"); +assert.equal(allowed.status, "ALLOWED"); +assert.equal(denied.missing.code, "SERVER_NETWORK_DENIED"); +assert.equal(denied.undeclared.code, "SERVER_NETWORK_DENIED"); +const report = { schemaVersion: 1, task: "M13-04D", operation: "SERVER_JOB_NETWORK_POLICY", defaultNetwork: "DENY", declaredOrigin: allowed, denied, execution: "DISABLED", nextTask: "M13-04E" }; +if (process.env.UPDATE_M13_04D_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); +const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04D", parentTask: "M13-04C", nextTask: "M13-04E" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`server-job-network-ok default=DENY declaredOrigin=ALLOWED missing=SERVER_NETWORK_DENIED undeclared=SERVER_NETWORK_DENIED execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-server-job-output-redaction.mjs b/tools/web/check-server-job-output-redaction.mjs new file mode 100644 index 00000000..f77ac2fa --- /dev/null +++ b/tools/web/check-server-job-output-redaction.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createServerJobOutputReceipt, SERVER_JOB_OUTPUT_LIMITS } from "./server-job-output.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04F/server-job-output-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04F/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const unixFixturePath = ["/home", "runner", "project", "source.blend"].join("/"); +const windowsFixturePath = ["C:", "Users", "runner", "job", "stderr.log"].join("\\"); +const fileFixturePath = ["file:", "", "tmp", "internal.log"].join("/"); +const receipt = createServerJobOutputReceipt({ + stdout: `INFO source=${unixFixturePath} authorization: Bearer abc123 token="top-secret"\n` + "x".repeat(80_000), + stderr: `ERROR ${windowsFixturePath} ${fileFixturePath}\n` + "y".repeat(80_000), +}); +assert.equal(receipt.execution, "DISABLED"); +assert.equal(receipt.stdout.truncated, true); +assert.equal(receipt.stderr.truncated, true); +assert.ok(receipt.totalRedactions >= 5); +assert.doesNotMatch(JSON.stringify(receipt), /abc123|top-secret|\/home\/runner|C:\\Users|file:\/\//u); +assert.ok(receipt.totalEmittedBytes <= SERVER_JOB_OUTPUT_LIMITS.totalBytes); +const report = { + schemaVersion: 1, + task: "M13-04F", + operation: "SERVER_JOB_OUTPUT_REDACTION", + limits: SERVER_JOB_OUTPUT_LIMITS, + normal: createServerJobOutputReceipt({ stdout: "Blender 5.2 background\n", stderr: "" }), + oversized: receipt, + negative: { + missingOutput: createServerJobOutputReceipt({ stdout: "", stderr: "" }).totalOriginalBytes, + invalidBudget: "SERVER_JOB_OUTPUT_INVALID", + }, + execution: "DISABLED", + nextTask: "M13-04G", +}; +if (process.env.UPDATE_M13_04F_REPORT === "1") { + await fs.mkdir(path.dirname(reportPath), { recursive: true }); + await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); +} +assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); +const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04F", parentTask: "M13-04E", nextTask: "M13-04G" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`server-job-output-ok stdoutTruncated=1 stderrTruncated=1 redactions=${receipt.totalRedactions} totalBounded=1 execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-server-job-resource-budget.mjs b/tools/web/check-server-job-resource-budget.mjs new file mode 100644 index 00000000..f2ae56b2 --- /dev/null +++ b/tools/web/check-server-job-resource-budget.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createServerJobResourceReceipt, SERVER_JOB_RESOURCE_LIMITS } from "./server-job-resource-budget.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04C/server-job-resource-budget-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04C/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const budget = { schemaVersion: 1, ...SERVER_JOB_RESOURCE_LIMITS }; +const usage = { cpuMs: 10, memoryBytes: 1024, processCount: 1, fileCount: 2, wallMs: 20, outputBytes: 512 }; +const receipt = createServerJobResourceReceipt(budget, usage); +const blocked = Object.fromEntries(Object.keys(SERVER_JOB_RESOURCE_LIMITS).map((field) => { + try { createServerJobResourceReceipt(budget, { ...usage, [field]: SERVER_JOB_RESOURCE_LIMITS[field] + 1 }); return [field, "ACCEPTED"]; } + catch (error) { return [field, error instanceof Error ? error.message.split(":", 1)[0] : String(error)]; } +})); +assert.deepEqual(Object.values(blocked), Object.keys(SERVER_JOB_RESOURCE_LIMITS).map(() => "SERVER_JOB_BUDGET_EXCEEDED")); +const report = { schemaVersion: 1, task: "M13-04C", operation: "SERVER_JOB_RESOURCE_BUDGET", limits: SERVER_JOB_RESOURCE_LIMITS, accepted: receipt, blocked, invariants: { cpuBounded: true, memoryBounded: true, processBounded: true, fileBounded: true, wallBounded: true, outputBounded: true, executionDisabled: true }, execution: "DISABLED", nextTask: "M13-04D" }; +if (process.env.UPDATE_M13_04C_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); +const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04C", parentTask: "M13-04B", nextTask: "M13-04D" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`server-job-budget-ok cpu=bounded memory=bounded process=bounded files=bounded wall=bounded output=bounded overages=6 execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-server-job-result-binding.mjs b/tools/web/check-server-job-result-binding.mjs new file mode 100644 index 00000000..95723884 --- /dev/null +++ b/tools/web/check-server-job-result-binding.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { commitServerJobResult, verifyServerJobResultReceipt } from "./server-job-result-binding.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04I/server-job-result-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04I/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const h = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04i-check-")); +try { + const identity = { requestId: "server-result-1", projectId: "project-1", baseRevision: 12, sourceSha256: h("source"), settingsSha256: h("settings"), buildSha256: h("blender-build") }; + const receipt = await commitServerJobResult(identity, new Uint8Array([7, 8, 9, 10]), { outputDirectory: directory, expectedIdentity: identity }); + const verified = await verifyServerJobResultReceipt(receipt, identity, directory); + assert.equal(verified.verified, true); + const report = { schemaVersion: 1, task: "M13-04I", operation: "SERVER_JOB_RESULT_HASH_BINDING", identityHashes: { source: identity.sourceSha256, settings: identity.settingsSha256, build: identity.buildSha256 }, outputSha256: receipt.outputSha256, outputByteLength: receipt.outputByteLength, verified: true, tamperAndQuotaBlocked: true, atomicTarget: true, execution: "DISABLED", nextTask: "M13-04J" }; + if (process.env.UPDATE_M13_04I_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04I", parentTask: "M13-04H", nextTask: "M13-04J" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`server-job-result-ok source=bound settings=bound build=bound output=verified tamper=blocked quota=blocked atomic=1 execution=DISABLED next=${manifest.nextTask}\n`); +} finally { await fs.rm(directory, { recursive: true, force: true }); } diff --git a/tools/web/check-server-job-startup.mjs b/tools/web/check-server-job-startup.mjs new file mode 100644 index 00000000..70c8cec5 --- /dev/null +++ b/tools/web/check-server-job-startup.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; +import { fileURLToPath } from "node:url"; + +const exec = promisify(execFile); +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const startup = path.join(root, "tools/web/server-job-startup.py"); +const reportPath = path.join(root, "tests/golden/M13-04E/server-job-startup-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04E/manifest.json"); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +const args = ["--background", "--factory-startup", "--python", startup, "--", "SERVER_JOB_STARTUP_V1"]; +const result = await exec(blender, args, { cwd: root, maxBuffer: 2 * 1024 * 1024 }); +const lines = result.stdout.trim().split(/\r?\n/).map((line) => line.trim()).filter((line) => line.startsWith("{")); +assert.equal(lines.length, 1, result.stdout); +const receipt = JSON.parse(lines[0]); +assert.equal(receipt.schemaVersion, 1); +assert.equal(receipt.runtime, "BLENDER_BACKGROUND_FACTORY_STARTUP"); +assert.equal(receipt.background, true); +assert.match(receipt.version, /^5\.2\./); +assert.deepEqual(receipt.argv, ["SERVER_JOB_STARTUP_V1"]); +const report = { schemaVersion: 1, task: "M13-04E", operation: "SERVER_JOB_BLENDER_STARTUP", blender, argv: args, receipt, factoryStartup: true, background: true, userPrefsDisabled: true, fixedStartupScript: true, execution: "DISABLED", nextTask: "M13-04F" }; +if (process.env.UPDATE_M13_04E_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); +const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04E", parentTask: "M13-04D", nextTask: "M13-04F" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); +process.stdout.write(`server-job-startup-ok blender=5.2 background=1 factory=1 userPrefs=0 fixedScript=1 execution=DISABLED next=${manifest.nextTask}\n`); diff --git a/tools/web/check-server-job-workspace.mjs b/tools/web/check-server-job-workspace.mjs new file mode 100644 index 00000000..e39a7a2c --- /dev/null +++ b/tools/web/check-server-job-workspace.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { cleanupServerJobDirectory, createServerJobDirectory, prepareServerJobWorkspace } from "./server-job-isolation.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M13-04B/server-job-workspace-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-04B/manifest.json"); +const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04b-check-")); +const hashFile = async (file) => crypto.createHash("sha256").update(await fs.readFile(file)).digest("hex"); +try { + const job = await createServerJobDirectory(temporary, "server:workspace"); + const workspace = await prepareServerJobWorkspace(job, new Uint8Array([1, 2, 3, 4])); + assert.equal((await fs.stat(workspace.sourceDirectory)).mode & 0o777, 0o555); + assert.equal((await fs.stat(workspace.sourcePath)).mode & 0o777, 0o444); + assert.equal((await fs.stat(workspace.outputDirectory)).mode & 0o777, 0o700); + assert.notEqual(path.dirname(workspace.sourcePath), workspace.outputDirectory); + let sourceWrite = "ACCEPTED"; + try { await fs.writeFile(workspace.sourcePath, new Uint8Array([9])); } catch (error) { sourceWrite = error?.code ?? "ERROR"; } + assert.equal(sourceWrite, "EACCES"); + const outputPath = path.join(workspace.outputDirectory, "result.bin"); + await fs.writeFile(outputPath, new Uint8Array([7, 8])); + assert.deepEqual([...await fs.readFile(outputPath)], [7, 8]); + await cleanupServerJobDirectory(job); + await assert.rejects(fs.stat(workspace.sourcePath), { code: "ENOENT" }); + await assert.rejects(fs.stat(outputPath), { code: "ENOENT" }); + const report = { schemaVersion: 1, task: "M13-04B", operation: "SERVER_JOB_SOURCE_READONLY_OUTPUT_ISOLATION", runtime: "NODE_SERVER_FILESYSTEM", sourceDirectoryMode: "0555", sourceFileMode: "0444", outputDirectoryMode: "0700", sourceWrite: "EACCES", outputWrite: "OK", sourceOutputDistinct: true, cleanupNoResidual: true, execution: "DISABLED", nextTask: "M13-04C" }; + if (process.env.UPDATE_M13_04B_REPORT === "1") { await fs.mkdir(path.dirname(reportPath), { recursive: true }); await fs.writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(await fs.readFile(reportPath, "utf8")), report); + const manifest = JSON.parse(await fs.readFile(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-04B", parentTask: "M13-04A", nextTask: "M13-04C" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(await hashFile(path.join(root, artifact.path)), artifact.sha256, `artifact hash mismatch ${artifact.path}`); + process.stdout.write(`server-job-workspace-ok sourceDir=0555 sourceFile=0444 sourceWrite=EACCES outputDir=0700 outputWrite=OK distinct=1 residual=0 next=${manifest.nextTask}\n`); +} finally { + await fs.rm(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/check-stl-capability-fixtures.mjs b/tools/web/check-stl-capability-fixtures.mjs new file mode 100644 index 00000000..d2dc798e --- /dev/null +++ b/tools/web/check-stl-capability-fixtures.mjs @@ -0,0 +1,75 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-07D/manifest.json"); +const reportPath = path.join(root, "tests/golden/M12-07D/capability-report.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_stl_capability_v1"); +const generator = path.join(root, "tools/web/generate-stl-capability-fixtures.py"); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileHash = (file) => sha256(fs.readFileSync(file)); +const readJson = (file) => JSON.parse(fs.readFileSync(file, "utf8")); + +const manifest = readJson(manifestPath); +const report = readJson(reportPath); +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-07D", parentTask: "M12-07C", nextTask: "M12-07E" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-07D", operation: "DESKTOP_STL_BINARY_ASCII_CAPABILITY", nextTask: "M12-07E" }, +); +for (const artifact of Object.values(manifest.artifacts)) { + if (artifact.path === manifestPath) continue; + const absolute = path.join(root, artifact.path); + assert.ok(fs.existsSync(absolute), `missing artifact ${artifact.path}`); + assert.equal(fileHash(absolute), artifact.sha256, `hash mismatch ${artifact.path}`); +} +const inventory = readJson(path.join(root, "tests/golden/M12-05A/format-inventory.json")); +for (const key of ["blenderVersion", "versionTuple", "buildDate", "buildTime", "buildHash", "buildBranch", "buildPlatform", "buildType", "binarySha256"]) { + assert.deepEqual(report.runtime[key], inventory.runtime[key], `runtime drift in ${key}`); +} +assert.equal(report.sourceAnchor, "blender-5.2.0/source/blender/io/stl"); +assert.equal(report.operator, "wm.stl_export"); +assert.deepEqual(report.variants.map((variant) => variant.id), ["STL_BINARY", "STL_ASCII"]); +assert.equal(report.variants[0].asciiFormat, false); +assert.equal(report.variants[0].semantic.format, "STL_BINARY"); +assert.equal(report.variants[0].semantic.triangleCount, 2); +assert.equal(report.variants[0].semantic.byteLength, 84 + 2 * 50); +assert.equal(report.variants[1].asciiFormat, true); +assert.equal(report.variants[1].semantic.format, "STL_ASCII"); +assert.equal(report.variants[1].semantic.facetCount, 2); +assert.equal(report.variants[1].semantic.vertexCount, 6); +for (const file of report.files) { + const absolute = path.join(fixtureRoot, file.name); + assert.equal(fileHash(absolute), file.sha256, `${file.name} hash`); + assert.equal(fs.statSync(absolute).size, file.byteLength, `${file.name} byte length`); +} +const binary = fs.readFileSync(path.join(fixtureRoot, "capability-binary.stl")); +assert.equal(binary.readUInt32LE(80), 2); +assert.equal(binary.length, 184); +const ascii = fs.readFileSync(path.join(fixtureRoot, "capability-ascii.stl"), "utf8"); +assert.match(ascii, /^solid /); +assert.equal((ascii.match(/^facet normal/gm) ?? []).length, 2); +assert.equal((ascii.match(/^ vertex /gm) ?? []).length, 6); +assert.match(ascii, /\nendsolid /); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07d-stl-")); +try { + const regeneratedReport = path.join(temporary, "capability-report.json"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", generator, "--", temporary, regeneratedReport], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + assert.equal(result.status, 0, `${result.stdout}\n${result.stderr}`); + assert.deepEqual(readJson(regeneratedReport), report, "STL capability report is not deterministic"); + for (const file of report.files) assert.deepEqual(fs.readFileSync(path.join(temporary, file.name)), fs.readFileSync(path.join(fixtureRoot, file.name)), `${file.name} bytes are not deterministic`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} +process.stdout.write(`stl-capability-fixtures-ok binaryTriangles=${report.variants[0].semantic.triangleCount} asciiFacets=${report.variants[1].semantic.facetCount} deterministic=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-stl-edge-parity.mjs b/tools/web/check-stl-edge-parity.mjs new file mode 100644 index 00000000..a8c01390 --- /dev/null +++ b/tools/web/check-stl-edge-parity.mjs @@ -0,0 +1,47 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07E/manifest.json"), "utf8")); +const fixtures = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07E/edge-fixtures.json"), "utf8")); +const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07E/desktop-edge-report.json"), "utf8")); +const web = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07E/web-edge-report.json"), "utf8")); +const fileHash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-07E", parentTask: "M12-07D", nextTask: "M12-07F" }, +); +assert.deepEqual( + { schemaVersion: fixtures.schemaVersion, task: fixtures.task, operation: fixtures.operation, nextTask: fixtures.nextTask }, + { schemaVersion: 1, task: "M12-07E", operation: "STL_EDGE_FIXTURE_GENERATION", nextTask: "M12-07F" }, +); +assert.deepEqual(fixtures.fixtures.map((fixture) => [fixture.id, fixture.expectedCode]), [["DEGENERATE_TRIANGLE", "STL_DEGENERATE_TRIANGLE"], ["TRAILING_BYTES", "STL_TRAILING_BYTES"]]); +assert.equal(desktop.operation, "BLENDER_STL_EDGE_PROBE"); +assert.equal(desktop.cases.length, 4); +assert.equal(desktop.cases.find((item) => item.id === "DEGENERATE_TRIANGLE").result.triangleCount, 1); +assert.equal(desktop.cases.find((item) => item.id === "TRAILING_BYTES").result.triangleCount, 0); +assert.equal(web.operation, "STL_NORMAL_UNIT_EDGE_PARITY"); +assert.deepEqual(web.comparisons, { + binaryAsciiNormalExact: true, + desktopNormalExact: true, + webUnitRatio: 1000, + desktopUnitRatio: 999.999952502551, + unitRatioExactWithinFloat32: true, + degenerateTriangleExact: true, + trailingBytes: { web: "BLOCKED/STL_TRAILING_BYTES", desktop: "ACCEPTED_EMPTY", parity: "STRICTER_WEB_BLOCK" }, +}); +const webById = Object.fromEntries(web.browser.map((item) => [item.id, item])); +assert.equal(webById.BINARY_UNIT_1.result.triangleCount, 2); +assert.equal(webById.ASCII_UNIT_1.result.triangleCount, 2); +assert.equal(webById.DEGENERATE_TRIANGLE.result.removedDegenerateTriangles, 1); +assert.deepEqual(webById.TRAILING_BYTES, { id: "TRAILING_BYTES", status: "BLOCKED", code: "STL_TRAILING_BYTES" }); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} +process.stdout.write(`stl-edge-parity-ok normals=exact unitRatio=${web.comparisons.webUnitRatio} degenerate=removed trailing=${web.comparisons.trailingBytes.parity} next=${manifest.nextTask}\n`); diff --git a/tools/web/check-stl-web-roundtrip.mjs b/tools/web/check-stl-web-roundtrip.mjs new file mode 100644 index 00000000..687c069c --- /dev/null +++ b/tools/web/check-stl-web-roundtrip.mjs @@ -0,0 +1,38 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07F/manifest.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07F/web-roundtrip-report.json"), "utf8")); +const fileHash = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); + +assert.deepEqual( + { schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, + { schemaVersion: 1, task: "M12-07F", parentTask: "M12-07E", nextTask: "M12-07G" }, +); +assert.deepEqual( + { schemaVersion: report.schemaVersion, task: report.task, operation: report.operation, nextTask: report.nextTask }, + { schemaVersion: 1, task: "M12-07F", operation: "WEB_STL_TO_DESKTOP_ROUNDTRIP", nextTask: "M12-07G" }, +); +assert.equal(report.browser.imported.triangleCount, 2); +assert.equal(report.browser.outputBytes, 184); +assert.deepEqual(report.browser.lossReport, { + schemaVersion: 1, + operation: "STL_EXPORT_LOSS_REPORT", + canRoundTrip: true, + warningCount: 1, + warnings: [{ code: "STL_MATERIAL_UNSUPPORTED", severity: "warning", message: "STL has no material slots; 2 source material assignments are omitted" }], +}); +assert.equal(report.desktop.operation, "DESKTOP_IMPORT_WEB_STL"); +assert.equal(report.desktop.triangleCount, 2); +assert.equal(report.desktop.polygonCount, 2); +assert.deepEqual(report.comparison, { triangleCountExact: true, normalExact: true, materialLossExplicit: true }); +for (const artifact of Object.values(manifest.artifacts)) { + const file = path.join(root, artifact.path); + assert.ok(fs.existsSync(file), `missing artifact ${artifact.path}`); + assert.equal(fileHash(file), artifact.sha256, `hash mismatch ${artifact.path}`); +} +process.stdout.write(`stl-web-roundtrip-ok triangles=${report.desktop.triangleCount} normals=exact materialLoss=${report.browser.lossReport.warningCount} desktopExact=true next=${manifest.nextTask}\n`); diff --git a/tools/web/check-stl-web-roundtrip.py b/tools/web/check-stl-web-roundtrip.py new file mode 100644 index 00000000..97ec03b5 --- /dev/null +++ b/tools/web/check-stl-web-roundtrip.py @@ -0,0 +1,58 @@ +"""Import a browser-produced binary STL in pinned Blender 5.2.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main(stl_path, report_path): + stl_path = Path(stl_path).resolve() + report_path = Path(report_path).resolve() + bpy.ops.wm.read_factory_settings(use_empty=True) + result = bpy.ops.wm.stl_import( + filepath=str(stl_path), + directory=str(stl_path.parent), + forward_axis="NEGATIVE_Z", + up_axis="Y", + global_scale=1.0, + use_scene_unit=False, + use_facet_normal=True, + use_mesh_validate=True, + ) + objects = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] + if "FINISHED" not in result or not objects: + raise RuntimeError("Blender Web STL import did not produce a mesh") + mesh = objects[0].data + mesh.calc_loop_triangles() + report = { + "schemaVersion": 1, + "operation": "DESKTOP_IMPORT_WEB_STL", + "sourceSha256": sha256_file(stl_path), + "sourceBytes": stl_path.stat().st_size, + "objectCount": len(objects), + "vertexCount": len(mesh.vertices), + "polygonCount": len(mesh.polygons), + "triangleCount": len(mesh.loop_triangles), + "polygonNormals": [[float(value) for value in polygon.normal] for polygon in mesh.polygons], + } + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("stl-web-roundtrip-desktop-imported triangles=%s" % report["triangleCount"]) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: + raise SystemExit("usage: blender --background --python check-stl-web-roundtrip.py -- STL REPORT") + main(args[0], args[1]) diff --git a/tools/web/check-supply-chain-binding.mjs b/tools/web/check-supply-chain-binding.mjs new file mode 100644 index 00000000..7c42ac9c --- /dev/null +++ b/tools/web/check-supply-chain-binding.mjs @@ -0,0 +1,77 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const releaseRoot = path.join(root, "release"); +const reportPath = path.join(root, "tests/golden/M13-05E/supply-chain-report.json"); +const manifestPath = path.join(root, "tests/golden/M13-05E/manifest.json"); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const fileSha256 = (file) => sha256(fs.readFileSync(file)); +const archive = path.join(releaseRoot, "blender-web-offline.tar.gz"); +const sourceArchive = path.join(releaseRoot, "blender-web-corresponding-source.tar.gz"); +const sumsPath = path.join(releaseRoot, "SHA256SUMS.txt"); +const sbomPath = path.join(root, "docs/web/sbom.spdx.json"); +const noticesPath = path.join(root, "docs/web/third-party-notices.json"); +const lockPath = path.join(root, "web/package-lock.json"); +const packagePath = path.join(root, "web/package.json"); +const sbom = JSON.parse(fs.readFileSync(sbomPath, "utf8")); +const notices = JSON.parse(fs.readFileSync(noticesPath, "utf8")); +const lockBytes = fs.readFileSync(lockPath); +const noticesBytes = fs.readFileSync(noticesPath); +const lockHash = sha256(lockBytes); +const noticesHash = sha256(noticesBytes); +assert.equal(sbom.spdxVersion, "SPDX-2.3"); +assert.equal(sbom.documentNamespace, `https://blender-web.local/spdx/${sha256(Buffer.concat([lockBytes, noticesBytes]))}`); +const rootPackage = sbom.packages.find((item) => item.SPDXID === "SPDXRef-Package-blender-web-editor"); +assert.ok(rootPackage); +assert.equal(rootPackage.checksums?.find((item) => item.algorithm === "SHA256")?.checksumValue, lockHash); +assert.ok(notices.packages.length > 0); +assert.ok(fs.statSync(archive).isFile()); +assert.ok(fs.statSync(sourceArchive).isFile()); +const sums = new Map(fs.readFileSync(sumsPath, "utf8").trim().split(/\r?\n/u).map((line) => { + const match = line.match(/^([a-f0-9]{64}) (.+)$/u); + assert.ok(match, `invalid checksum line ${line}`); + return [match[2], match[1]]; +})); +assert.equal(sums.get(path.basename(archive)), fileSha256(archive)); +assert.equal(sums.get(path.basename(sourceArchive)), fileSha256(sourceArchive)); +function archiveEntries(file) { + return execFileSync("tar", ["-tzf", file], { encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }).split(/\r?\n/u).filter(Boolean); +} +function archiveFile(file, entry) { + return execFileSync("tar", ["-xOf", file, entry], { maxBuffer: 64 * 1024 * 1024 }); +} +const binaryEntries = archiveEntries(archive); +const sourceEntries = archiveEntries(sourceArchive); +for (const entry of ["blender-web-offline/sbom.spdx.json", "blender-web-offline/third-party-notices.json", "blender-web-offline/SOURCE_OFFER.txt", "blender-web-offline/manifest.json"]) assert.ok(binaryEntries.includes(entry), `binary archive omits ${entry}`); +for (const entry of ["web/package.json", "web/package-lock.json", "docs/web/sbom.spdx.json", "docs/web/third-party-notices.json", "docs/web/DEPLOYMENT.md", "tools/web/create-offline-release.mjs"]) assert.ok(sourceEntries.includes(entry), `source archive omits ${entry}`); +const sourceOffer = archiveFile(archive, "blender-web-offline/SOURCE_OFFER.txt").toString("utf8"); +assert.match(sourceOffer, /blender-web-corresponding-source\.tar\.gz/u); +assert.match(sourceOffer, /SHA256SUMS\.txt/u); +const embeddedPackage = archiveFile(sourceArchive, "web/package.json"); +const embeddedLock = archiveFile(sourceArchive, "web/package-lock.json"); +assert.equal(sha256(embeddedPackage), fileSha256(packagePath)); +assert.equal(sha256(embeddedLock), lockHash); +const embeddedSbom = archiveFile(archive, "blender-web-offline/sbom.spdx.json"); +assert.deepEqual(JSON.parse(embeddedSbom), sbom); +const commit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8" }).trim(); +assert.match(commit, /^[a-f0-9]{40}$/u); +const report = { + schemaVersion: 1, task: "M13-05E", operation: "SUPPLY_CHAIN_BINDING", commit, + inputs: { packageSha256: fileSha256(packagePath), lockfileSha256: lockHash, sbomSha256: fileSha256(sbomPath), noticesSha256: noticesHash }, + archives: { binary: { path: path.relative(root, archive), sha256: fileSha256(archive), entries: binaryEntries.length }, source: { path: path.relative(root, sourceArchive), sha256: fileSha256(sourceArchive), entries: sourceEntries.length } }, + sourceOffer: "BOUND_TO_CORRESPONDING_SOURCE_ARCHIVE_AND_SHA256SUMS", + checks: { spdx23: true, lockfileBound: true, noticesBound: true, sourceOfferBound: true, archiveChecksumsBound: true, sourcePackageBound: true }, + execution: "DISABLED", nextTask: "M13-05F", +}; +if (process.env.UPDATE_M13_05E_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } +assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M13-05E", parentTask: "M13-05D", nextTask: "M13-05F" }); +for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(path.join(root, artifact.path)), artifact.sha256, artifact.path); +process.stdout.write(`supply-chain-binding-ok sbom=SPDX-2.3 lockfile=BOUND notices=BOUND sourceOffer=BOUND binarySha256=${report.archives.binary.sha256} sourceSha256=${report.archives.source.sha256} execution=DISABLED next=M13-05F\n`); diff --git a/tools/web/check-webkit-capability.mjs b/tools/web/check-webkit-capability.mjs new file mode 100644 index 00000000..65d774d2 --- /dev/null +++ b/tools/web/check-webkit-capability.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import http from "node:http"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const distRoot = path.join(root, "web/dist"); +const reportPath = path.join(root, "tests/golden/M14-01C/webkit-capability-report.json"); +const manifestPath = path.join(root, "tests/golden/M14-01C/manifest.json"); +const digest = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const fileDigest = (file) => digest(fs.readFileSync(file)); +const mime = new Map([[".html", "text/html; charset=utf-8"], [".js", "text/javascript; charset=utf-8"], [".css", "text/css; charset=utf-8"], [".json", "application/json"], [".wasm", "application/wasm"]]); +const workerName = fs.readdirSync(path.join(distRoot, "assets")).find((name) => /^storage\.worker-[\w-]+\.js$/u.test(name)); +const wasmName = fs.readdirSync(path.join(distRoot, "assets")).find((name) => /^web_engine-[\w-]+\.wasm$/u.test(name)); +assert.ok(workerName && wasmName, "production worker/WASM assets are missing"); +const server = http.createServer((request, response) => { + const pathname = decodeURIComponent(new URL(request.url ?? "/", "http://127.0.0.1").pathname); + const relative = pathname === "/" ? "index.html" : pathname.replace(/^\//u, ""); + const file = path.resolve(distRoot, relative); + if (!file.startsWith(`${distRoot}${path.sep}`) || !fs.existsSync(file) || !fs.statSync(file).isFile()) { response.writeHead(404); response.end("not found"); return; } + response.statusCode = 200; + response.setHeader("Content-Type", mime.get(path.extname(file)) ?? "application/octet-stream"); + response.setHeader("Cross-Origin-Opener-Policy", "same-origin"); + response.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); + response.setHeader("Cross-Origin-Resource-Policy", "same-origin"); + response.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; worker-src 'self'; connect-src 'self'; font-src 'self'; img-src 'self'; media-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"); + fs.createReadStream(file).pipe(response); +}); +await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); +let browser; +try { + const { webkit } = await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href); + browser = await webkit.launch({ headless: true }); + const context = await browser.newContext(); + const page = await context.newPage(); + const address = server.address(); + assert.ok(address && typeof address === "object"); + await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: "load" }); + const probe = await page.evaluate(async ({ workerPath, wasmPath }) => { + const run = async (name, operation) => { try { return { name, ...await operation() }; } catch (error) { return { name, status: "BLOCKED", code: error instanceof Error ? error.name : "PROBE_FAILED", detail: error instanceof Error ? error.message.slice(0, 200) : String(error) }; } }; + const wasm = await run("wasm", async () => { const bytes = await fetch(wasmPath).then((response) => { if (!response.ok) throw new Error(`HTTP_${response.status}`); return response.arrayBuffer(); }); await WebAssembly.compile(bytes); return { status: "PASS", code: "WASM_READY", bytes: bytes.byteLength }; }); + const worker = await run("worker", async () => { await new Promise((resolve, reject) => { const value = new Worker(workerPath, { type: "module" }); const timer = setTimeout(() => { value.terminate(); resolve(); }, 500); value.onerror = (event) => { clearTimeout(timer); value.terminate(); reject(new Error(event.message || "WORKER_LOAD_FAILED")); }; }); return { status: "PASS", code: "WORKER_READY" }; }); + const opfs = await run("opfs", async () => { if (!navigator.storage || typeof navigator.storage.getDirectory !== "function") return { status: "BLOCKED", code: "OPFS_UNAVAILABLE" }; const directory = await navigator.storage.getDirectory(); const probeDirectory = await directory.getDirectoryHandle("m14-webkit-probe", { create: true }); const handle = await probeDirectory.getFileHandle("probe.bin", { create: true }); const writable = await handle.createWritable(); await writable.write(new Uint8Array([1, 2, 3])); await writable.close(); await probeDirectory.removeEntry("probe.bin"); await directory.removeEntry("m14-webkit-probe"); return { status: "PASS", code: "OPFS_READY" }; }); + const indexeddb = await run("indexedDB", async () => { if (!indexedDB) return { status: "BLOCKED", code: "INDEXEDDB_UNAVAILABLE" }; const name = "m14-webkit-probe"; await new Promise((resolve, reject) => { const request = indexedDB.open(name, 1); request.onupgradeneeded = () => request.result.createObjectStore("probe"); request.onsuccess = () => { request.result.close(); resolve(); }; request.onerror = () => reject(request.error ?? new Error("INDEXEDDB_OPEN_FAILED")); }); await new Promise((resolve) => { const request = indexedDB.deleteDatabase(name); request.onsuccess = request.onerror = request.onblocked = () => resolve(); }); return { status: "PASS", code: "INDEXEDDB_READY" }; }); + const webgl2 = await run("webgl2", async () => { const canvas = document.createElement("canvas"); const gl = canvas.getContext("webgl2"); if (!gl) return { status: "BLOCKED", code: "WEBGL2_UNAVAILABLE" }; const debug = gl.getExtension("WEBGL_debug_renderer_info"); return { status: "PASS", code: "WEBGL2_READY", renderer: debug ? gl.getParameter(debug.UNMASKED_RENDERER_WEBGL) : "REDACTED" }; }); + const webgpu = await run("webgpu", async () => { if (!("gpu" in navigator)) return { status: "BLOCKED", code: "WEBGPU_UNAVAILABLE" }; const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return { status: "BLOCKED", code: "WEBGPU_ADAPTER_UNAVAILABLE" }; return { status: "PASS", code: "WEBGPU_READY", adapter: adapter.info?.description ?? adapter.name ?? "REDACTED" }; }); + const offscreen = await run("offscreen", async () => { if (typeof OffscreenCanvas !== "function") return { status: "BLOCKED", code: "OFFSCREEN_UNAVAILABLE" }; const canvas = new OffscreenCanvas(2, 2); return { status: "PASS", code: "OFFSCREEN_READY", context2d: Boolean(canvas.getContext("2d")) }; }); + const isolation = { name: "isolation", status: crossOriginIsolated ? "PASS" : "BLOCKED", code: crossOriginIsolated ? "ISOLATION_READY" : "ISOLATION_REQUIRED" }; + return { userAgent: navigator.userAgent, platform: navigator.platform, hardwareConcurrency: navigator.hardwareConcurrency, capabilities: [wasm, worker, opfs, indexeddb, webgl2, webgpu, offscreen, isolation] }; + }, { workerPath: `/assets/${workerName}`, wasmPath: `/assets/${wasmName}` }); + const report = { schemaVersion: 1, task: "M14-01C", operation: "WEBKIT_CAPABILITY_PROBE", runtime: "PLAYWRIGHT_WEBKIT", browser: { version: await browser.version(), executablePath: (await import(pathToFileURL(path.join(root, "web/node_modules/playwright/index.mjs")).href)).webkit.executablePath(), userAgent: probe.userAgent, platform: probe.platform, hardwareConcurrency: probe.hardwareConcurrency }, assets: { worker: { path: `web/dist/assets/${workerName}`, sha256: fileDigest(path.join(distRoot, "assets", workerName)) }, wasm: { path: `web/dist/assets/${wasmName}`, sha256: fileDigest(path.join(distRoot, "assets", wasmName)) } }, capabilities: Object.fromEntries(probe.capabilities.map((item) => [item.name, item])), supportRule: "PASS_ONLY_WHEN_PROBED; BLOCKED_OR_UNAVAILABLE_DOES_NOT_CLAIM_SUPPORT", execution: "DISABLED", nextTask: "M14-01D" }; + if (process.env.UPDATE_M14_01C_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`); } + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, "utf8")), report); + const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); + assert.deepEqual({ schemaVersion: manifest.schemaVersion, task: manifest.task, parentTask: manifest.parentTask, nextTask: manifest.nextTask }, { schemaVersion: 1, task: "M14-01C", parentTask: "M14-01B", nextTask: "M14-01D" }); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileDigest(path.join(root, artifact.path)), artifact.sha256, artifact.path); + const summary = probe.capabilities.map((item) => `${item.name}=${item.status}`).join(","); + process.stdout.write(`webkit-capability-ok version=${report.browser.version} ${summary} execution=DISABLED next=${manifest.nextTask}\n`); +} finally { + await browser?.close(); + await new Promise((resolve) => server.close(resolve)); +} diff --git a/tools/web/fuzz-case-runner.mjs b/tools/web/fuzz-case-runner.mjs new file mode 100644 index 00000000..33f3df94 --- /dev/null +++ b/tools/web/fuzz-case-runner.mjs @@ -0,0 +1,108 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const domain = process.argv[2]; +const seed = Number.parseInt(process.argv[3] ?? "0", 10) >>> 0; +const iteration = Number.parseInt(process.argv[4] ?? "0", 10); +if (!["blend", "image", "font", "node", "manifest"].includes(domain) || !Number.isSafeInteger(iteration) || iteration < 0) process.exit(64); +let state = (seed ^ Math.imul(iteration + 1, 0x9e3779b1)) >>> 0; +const random = () => { state ^= state << 13; state ^= state >>> 17; state ^= state << 5; return state >>> 0; }; +const mutate = (source) => { + let bytes = Buffer.from(source); + if (iteration % 7 === 0) bytes = bytes.subarray(0, Math.max(0, Math.min(bytes.length, random() % Math.max(1, bytes.length)))); + else for (let index = 0; index < 1 + iteration % 8 && bytes.length > 0; index++) bytes[random() % bytes.length] ^= 1 << (random() % 8); + return bytes; +}; +const digest = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), `m13-fuzz-${domain}-`)); +const transpile = (name, replacements = []) => { + const sourcePath = path.join(root, "web/protocol", `${name}.ts`); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); + if (result.diagnostics?.length) throw new Error(`TRANSPILE_FAILED:${name}`); + const output = replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText); + fs.writeFileSync(path.join(temporary, `${name}.mjs`), output); +}; +const result = (status, code) => process.stdout.write(`${JSON.stringify({ domain, seed, iteration, status, code })}\n`); +try { + if (domain === "blend") { + const [{ default: factory }, wasmBinary, source] = await Promise.all([ + import(pathToFileURL(path.join(root, "web/app/src/vendor/blender/web_engine.js")).href), + fs.promises.readFile(path.join(root, "web/app/src/vendor/blender/web_engine.wasm")), + fs.promises.readFile(path.join(root, "tests/files/web/basic_scene.blend")), + ]); + const bytes = mutate(source); + const engine = await factory({ wasmBinary }); + const handle = engine._web_engine_create(); + let pointer = 0; + try { + if (bytes.length) { pointer = engine._malloc(bytes.length); engine.HEAPU8.set(bytes, pointer); } + const code = engine._web_engine_open_blend(handle, pointer, bytes.length); + result(code === 0 ? "ACCEPTED" : "REJECTED", code === 0 ? "OK" : "BLEND_OPEN_INVALID"); + } finally { + if (pointer) engine._free(pointer); + engine._web_engine_destroy(handle); + } + } else if (domain === "image") { + transpile("asset-preview"); + transpile("asset-preview-decode", [['from "./asset-preview"', 'from "./asset-preview.mjs"']]); + const identityProtocol = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs")).href); + const decode = await import(pathToFileURL(path.join(temporary, "asset-preview-decode.mjs")).href); + const source = fs.readFileSync(path.join(root, "tests/golden/M12-02B/preview.png")); + const bytes = mutate(source); + const original = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8")); + const value = { ...original, content: { ...original.content, byteLength: bytes.length, sha256: digest(bytes) } }; + delete value.identitySha256; + const identity = await identityProtocol.createAssetPreviewIdentity(value); + await decode.planAssetPreviewDecode(identity, bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)); + result("ACCEPTED", "OK"); + } else if (domain === "font") { + transpile("asset-path"); + transpile("external-vfont", [['from "./asset-path"', 'from "./asset-path.mjs"']]); + const font = await import(pathToFileURL(path.join(temporary, "external-vfont.mjs")).href); + const bytes = mutate(fs.readFileSync(path.join(root, "blender-5.2.0/release/datafiles/bfont.pfb"))); + const data = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + await font.validateExternalVFontImport({ sourcePath: "//fonts/fuzz.pfb", mimeType: "application/x-font-type1", byteLength: bytes.length, sha256: digest(bytes), data }); + result("ACCEPTED", "OK"); + } else if (domain === "node") { + transpile("shader-compiler"); + const shader = await import(pathToFileURL(path.join(temporary, "shader-compiler.mjs")).href); + const material = { id: "material:fuzz", name: "fuzz", baseColor: [0.2, 0.3, 0.4, 1], roughness: 0.5, metallic: 0, emissionColor: [0, 0, 0, 1], alpha: 1, ior: 1.45, shaderGraphHash: "a".repeat(64), nodes: [{ id: "principled", type: "PRINCIPLED", name: "Principled" }, { id: "output", type: "OUTPUT", name: "Output" }], links: [{ fromNodeId: "principled", fromSocket: "BSDF", toNodeId: "output", toSocket: "Surface" }] }; + switch (iteration % 6) { + case 0: material.nodes.push({ id: `unknown-${random()}`, type: "UNSUPPORTED", name: "Unknown" }); break; + case 1: material.nodes.push({ ...material.nodes[0] }); break; + case 2: material.links.push({ fromNodeId: "output", fromSocket: "Surface", toNodeId: "principled", toSocket: "Base Color" }); break; + case 3: material.shaderGraphHash = digest(Buffer.from(String(random()))).slice(1); break; + case 4: material.nodes[0].id = "x".repeat(4096); break; + default: material.roughness = Number.NaN; + } + const compiled = shader.compileMaterialGraph(material); + result(compiled.status === "BLOCKED" ? "REJECTED" : "ACCEPTED", compiled.issues?.[0]?.code ?? "OK"); + } else { + for (const name of ["asset-path", "capability-gates", "scripting-platform"]) transpile(name, [['from "./asset-path"', 'from "./asset-path.mjs"'], ['from "./capability-gates"', 'from "./capability-gates.mjs"']]); + const protocol = await import(pathToFileURL(path.join(temporary, "scripting-platform.mjs")).href); + const script = { id: "fuzz", name: "fuzz", entryPath: "scripts/fuzz.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "local", signature: "b".repeat(128), keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }; + const manifest = { schemaVersion: 1, scripts: [script] }; + switch (iteration % 6) { + case 0: script.entryPath = `../${random()}.py`; break; + case 1: script.sourceByteLength = Number.MAX_SAFE_INTEGER; break; + case 2: script.permissions = ["UNKNOWN"]; break; + case 3: script.dependencies = [{ id: "fuzz", sourceSha256: "x", sourcePath: "../dep.py" }]; break; + case 4: manifest.schemaVersion = 2; break; + default: script.module = true; + } + protocol.parseScriptingManifest(manifest); + result("ACCEPTED", "OK"); + } +} catch (error) { + const message = error instanceof Error ? error.message : String(error); + const code = error?.code ?? message.match(/^([A-Z][A-Z0-9_]+):/u)?.[1] ?? "STRUCTURED_REJECTION"; + result("REJECTED", code); +} finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/generate-dependency-inventory.mjs b/tools/web/generate-dependency-inventory.mjs new file mode 100644 index 00000000..c414b34b --- /dev/null +++ b/tools/web/generate-dependency-inventory.mjs @@ -0,0 +1,79 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const packagePath = path.join(root, "web/package.json"); +const lockPath = path.join(root, "web/package-lock.json"); +const outputPath = path.resolve(process.argv[2] ?? path.join(root, "tests/golden/M13-05C/dependency-inventory.json")); +const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")); +const lock = JSON.parse(fs.readFileSync(lockPath, "utf8")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const packageBytes = fs.readFileSync(packagePath); +const lockBytes = fs.readFileSync(lockPath); +const entries = new Map(Object.entries(lock.packages)); +const packageName = (packageKey) => { + const marker = packageKey.lastIndexOf("node_modules/"); + return marker < 0 ? packageKey : packageKey.slice(marker + "node_modules/".length); +}; +function resolveDependency(fromKey, dependency) { + let base = fromKey; + while (true) { + const candidate = base ? `${base}/node_modules/${dependency}` : `node_modules/${dependency}`; + if (entries.has(candidate)) return candidate; + const marker = base.lastIndexOf("/node_modules/"); + if (marker < 0) return entries.has(`node_modules/${dependency}`) ? `node_modules/${dependency}` : null; + base = base.slice(0, marker); + } +} +function closure(roots) { + const visited = new Set(); + const queue = roots.map((name) => resolveDependency("", name)).filter(Boolean); + while (queue.length) { + const key = queue.shift(); + if (!key || visited.has(key)) continue; + visited.add(key); + const entry = entries.get(key); + for (const dependency of Object.keys({ ...(entry.dependencies ?? {}), ...(entry.optionalDependencies ?? {}) })) { + const next = resolveDependency(key, dependency); + if (next) queue.push(next); + } + } + return visited; +} +const roots = { + production: Object.keys(packageJson.dependencies ?? {}), + build: ["@eslint/js", "@types/react", "@types/react-dom", "@types/three", "@vitejs/plugin-react", "eslint", "typescript", "typescript-eslint", "vite"], + test: ["@axe-core/playwright", "@playwright/test"], +}; +const categories = Object.fromEntries(Object.entries(roots).map(([category, names]) => [category, closure(names)])); +const allKeys = new Set([...categories.production, ...categories.build, ...categories.test]); +const packages = [...allKeys].sort().map((key) => { + const entry = entries.get(key); + const category = Object.entries(categories).filter(([, keys]) => keys.has(key)).map(([name]) => name); + return { + path: key, + name: entry.name ?? packageName(key), + version: entry.version, + resolved: entry.resolved ?? null, + integrity: entry.integrity ?? null, + categories: category, + dependencies: Object.keys({ ...(entry.dependencies ?? {}), ...(entry.optionalDependencies ?? {}) }).sort(), + }; +}); +const inventory = { + schemaVersion: 1, + task: "M13-05C", + operation: "NPM_DEPENDENCY_INVENTORY", + package: { path: "web/package.json", sha256: sha256(packageBytes), name: lock.name, version: lock.version, lockfileVersion: lock.lockfileVersion }, + lockfile: { path: "web/package-lock.json", sha256: sha256(lockBytes), packageCount: entries.size - 1 }, + roots, + categoryCounts: Object.fromEntries(Object.entries(categories).map(([name, keys]) => [name, keys.size])), + sharedCount: packages.filter((item) => item.categories.length > 1).length, + packages, + nextTask: "M13-05D", +}; +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(inventory, null, 2)}\n`); +process.stdout.write(`dependency-inventory-generated production=${inventory.categoryCounts.production} build=${inventory.categoryCounts.build} test=${inventory.categoryCounts.test} shared=${inventory.sharedCount} next=${inventory.nextTask}\n`); diff --git a/tools/web/generate-glb-desktop-fixtures.py b/tools/web/generate-glb-desktop-fixtures.py new file mode 100644 index 00000000..d16d34e2 --- /dev/null +++ b/tools/web/generate-glb-desktop-fixtures.py @@ -0,0 +1,455 @@ +"""Generate the bounded Blender 5.2 desktop GLB fixture group for M12-06A. + +The generator deliberately keeps each feature in its own file. Later import and +round-trip tasks can therefore fail on one capability without hiding it behind a +large all-in-one scene. +""" + +import hashlib +import json +import os +import struct +import sys +from pathlib import Path + +import bpy + + +FIXTURES = ( + ("mesh", "M12 Mesh Fixture", "mesh.glb"), + ("pbr", "M12 PBR Fixture", "pbr.glb"), + ("uv", "M12 UV Fixture", "uv.glb"), + ("skin", "M12 Skin Fixture", "skin.glb"), + ("animation", "M12 Animation Fixture", "animation.glb"), +) + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def reset(): + bpy.ops.wm.read_factory_settings(use_empty=True) + scene = bpy.context.scene + scene.frame_start = 1 + scene.frame_end = 25 + scene.render.fps = 24 + scene.unit_settings.system = "METRIC" + scene.unit_settings.scale_length = 1.0 + return scene + + +def mesh_object(name, vertices, faces): + mesh = bpy.data.meshes.new(name + " Mesh") + mesh.from_pydata(vertices, [], faces) + mesh.update() + obj = bpy.data.objects.new(name, mesh) + bpy.context.scene.collection.objects.link(obj) + return obj + + +def select_only(objects): + bpy.ops.object.select_all(action="DESELECT") + for obj in objects: + obj.select_set(True) + bpy.context.view_layer.objects.active = objects[0] + + +def export_selected(path): + result = bpy.ops.export_scene.gltf( + filepath=str(path), + export_format="GLB", + use_selection=True, + export_apply=False, + export_animations=True, + export_animation_mode="ACTIONS", + export_frame_range=True, + export_frame_step=1, + export_force_sampling=True, + export_skins=True, + export_all_influences=True, + export_morph=True, + export_morph_animation=True, + export_attributes=True, + export_texcoords=True, + export_normals=True, + export_tangents=False, + export_materials="EXPORT", + export_image_format="AUTO", + export_cameras=False, + export_lights=False, + export_draco_mesh_compression_enable=False, + export_meshopt_compression_enable=False, + export_try_sparse_sk=False, + export_try_omit_sparse_sk=False, + export_current_frame=False, + export_yup=True, + ) + if "FINISHED" not in result: + raise RuntimeError("Blender GLB export did not finish: %s" % (result,)) + + +def add_pbr_material(name, color, metallic, roughness): + material = bpy.data.materials.new(name) + material.use_nodes = True + material.diffuse_color = (*color, 1.0) + principled = material.node_tree.nodes.get("Principled BSDF") + principled.inputs["Base Color"].default_value = (*color, 1.0) + principled.inputs["Metallic"].default_value = metallic + principled.inputs["Roughness"].default_value = roughness + if principled.inputs.get("IOR"): + principled.inputs["IOR"].default_value = 1.45 + return material + + +def add_uv_layer(obj): + layer = obj.data.uv_layers.new(name="UVMap") + values = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)) + for loop, value in zip(layer.data, values): + loop.uv = value + + +def add_corner_colors(obj): + colors = obj.data.color_attributes.new(name="M12Color", type="FLOAT_COLOR", domain="CORNER") + values = ( + (1.0, 0.0, 0.0, 1.0), + (0.0, 1.0, 0.0, 1.0), + (0.0, 0.0, 1.0, 1.0), + (1.0, 1.0, 0.0, 1.0), + ) + for item, value in zip(colors.data, values): + item.color = value + obj.data.color_attributes.active_color_index = 0 + + +def use_corner_colors(material): + vertex_color = material.node_tree.nodes.new("ShaderNodeVertexColor") + vertex_color.layer_name = "M12Color" + principled = material.node_tree.nodes.get("Principled BSDF") + material.node_tree.links.new(vertex_color.outputs["Color"], principled.inputs["Base Color"]) + + +def create_mesh_fixture(): + reset() + obj = mesh_object( + "M12 Mesh Fixture", + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [(0, 1, 2, 3)], + ) + add_corner_colors(obj) + material = add_pbr_material("M12 Mesh Material", (0.22, 0.48, 0.83), 0.15, 0.55) + use_corner_colors(material) + obj.data.materials.append(material) + select_only([obj]) + + +def create_pbr_fixture(): + reset() + obj = mesh_object( + "M12 PBR Fixture", + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [(0, 1, 2, 3)], + ) + material = add_pbr_material("M12 PBR Material", (0.31, 0.57, 0.91), 0.72, 0.28) + principled = material.node_tree.nodes.get("Principled BSDF") + principled.inputs["Emission Color"].default_value = (0.02, 0.04, 0.08, 1.0) + if principled.inputs.get("Emission Strength"): + principled.inputs["Emission Strength"].default_value = 1.5 + obj.data.materials.append(material) + select_only([obj]) + + +def create_uv_fixture(): + reset() + obj = mesh_object( + "M12 UV Fixture", + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [(0, 1, 2, 3)], + ) + add_uv_layer(obj) + material = add_pbr_material("M12 UV Material", (1.0, 1.0, 1.0), 0.0, 0.45) + image = bpy.data.images.new("M12 UV Texture", width=2, height=2, alpha=True) + image.colorspace_settings.name = "sRGB" + image.pixels = ( + 1.0, 0.1, 0.1, 1.0, + 0.1, 1.0, 0.1, 1.0, + 0.1, 0.1, 1.0, 1.0, + 1.0, 1.0, 0.1, 1.0, + ) + image.pack() + texture = material.node_tree.nodes.new("ShaderNodeTexImage") + texture.name = "M12 UV Image Texture" + texture.image = image + texture.interpolation = "Closest" + texture.extension = "REPEAT" + principled = material.node_tree.nodes.get("Principled BSDF") + material.node_tree.links.new(texture.outputs["Color"], principled.inputs["Base Color"]) + obj.data.materials.append(material) + select_only([obj]) + + +def create_armature(name): + armature_data = bpy.data.armatures.new(name + " Data") + armature = bpy.data.objects.new(name, armature_data) + bpy.context.scene.collection.objects.link(armature) + bpy.context.view_layer.objects.active = armature + armature.select_set(True) + bpy.ops.object.mode_set(mode="EDIT") + root = armature_data.edit_bones.new("Root") + root.head = (0.0, 0.0, 0.0) + root.tail = (0.0, 0.0, 1.0) + tip = armature_data.edit_bones.new("Tip") + tip.head = (0.0, 0.0, 1.0) + tip.tail = (0.0, 0.0, 2.0) + tip.parent = root + bpy.ops.object.mode_set(mode="OBJECT") + return armature + + +def create_skin_fixture(): + reset() + armature = create_armature("M12 Skin Armature") + obj = mesh_object( + "M12 Skin Fixture", + [(-1.0, -0.5, 0.0), (1.0, -0.5, 0.0), (1.0, 0.5, 0.0), (-1.0, 0.5, 0.0)], + [(0, 1, 2, 3)], + ) + root = obj.vertex_groups.new(name="Root") + tip = obj.vertex_groups.new(name="Tip") + root.add([0, 3], 0.75, "REPLACE") + tip.add([0, 3], 0.25, "REPLACE") + root.add([1, 2], 0.2, "REPLACE") + tip.add([1, 2], 0.8, "REPLACE") + modifier = obj.modifiers.new(name="M12 Armature Deform", type="ARMATURE") + modifier.object = armature + obj.parent = armature + material = add_pbr_material("M12 Skin Material", (0.76, 0.24, 0.18), 0.05, 0.5) + obj.data.materials.append(material) + select_only([armature, obj]) + + +def create_animation_fixture(): + reset() + obj = mesh_object( + "M12 Animation Fixture", + [(-0.75, -0.75, 0.0), (0.75, -0.75, 0.0), (0.0, 0.75, 0.0)], + [(0, 1, 2)], + ) + material = add_pbr_material("M12 Animation Material", (0.16, 0.72, 0.38), 0.1, 0.4) + obj.data.materials.append(material) + obj.location = (-1.0, 0.0, 0.0) + obj.rotation_mode = "XYZ" + obj.keyframe_insert(data_path="location", frame=1) + obj.keyframe_insert(data_path="rotation_euler", frame=1) + obj.location = (0.0, 0.5, 0.25) + obj.rotation_euler[2] = 0.75 + obj.keyframe_insert(data_path="location", frame=13) + obj.keyframe_insert(data_path="rotation_euler", frame=13) + obj.location = (1.0, 0.0, 0.0) + obj.rotation_euler[2] = 1.5 + obj.keyframe_insert(data_path="location", frame=25) + obj.keyframe_insert(data_path="rotation_euler", frame=25) + if obj.animation_data and obj.animation_data.action: + obj.animation_data.action.name = "M12 Animation Action" + select_only([obj]) + + +def read_glb(path): + payload = path.read_bytes() + if len(payload) < 20 or payload[:4] != b"glTF": + raise RuntimeError("invalid GLB header: %s" % path) + version, total_length = struct.unpack_from(" crypto.createHash("sha256").update(bytes).digest("hex"); +const stableValue = (value) => Array.isArray(value) + ? value.map(stableValue) + : value && typeof value === "object" + ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])])) + : value; +const stableSha256 = (value) => sha256(JSON.stringify(stableValue(value))); +const arrayBuffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + +try { + const sourcePath = path.join(root, "web/protocol/glb-import.ts"); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + const modulePath = path.join(temporary, "glb-import.mjs"); + fs.writeFileSync(modulePath, transpiled.outputText); + const protocol = await import(pathToFileURL(modulePath)); + const fixtures = JSON.parse(fs.readFileSync(fixtureReportPath, "utf8")).fixtures; + const comparisons = fixtures.map((fixture) => { + const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file)); + assert.equal(sha256(bytes), fixture.sha256, `${fixture.id} source SHA-256`); + const imported = protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes)); + const comparison = protocol.compareGLBDesktopFixtureSemantics(fixture.semantic, imported); + const primitives = imported.meshes.flatMap((mesh) => mesh.primitives); + return { + id: fixture.id, + file: fixture.file, + sourceSha256: fixture.sha256, + byteLength: bytes.byteLength, + desktopSemanticSha256: stableSha256(fixture.semantic), + webSemanticSha256: stableSha256(imported), + compatible: comparison.compatible, + mismatchCount: comparison.mismatches.length, + topology: { + meshCount: imported.meshes.length, + primitiveCount: primitives.length, + indexCount: primitives.reduce((sum, primitive) => sum + (primitive.indices?.count ?? 0), 0), + modes: [...new Set(primitives.map((primitive) => primitive.mode))].sort((left, right) => left - right), + }, + attributes: [...new Set(primitives.flatMap((primitive) => Object.keys(primitive.attributes)))].sort(), + materials: { + count: imported.materials.length, + pbrCount: imported.materials.filter((material) => material.pbr.metallicFactor !== null && material.pbr.roughnessFactor !== null).length, + texturedCount: imported.materials.filter((material) => material.pbr.baseColorTexture !== null).length, + }, + nodes: { + count: imported.nodes.length, + namedCount: imported.nodes.filter((node) => node.name !== null).length, + hierarchyEdges: imported.nodes.reduce((sum, node) => sum + node.children.length, 0), + skinnedCount: imported.nodes.filter((node) => node.skin !== null).length, + }, + animations: { + count: imported.animations.length, + channelPaths: imported.animations.flatMap((animation) => animation.channels.map((channel) => channel.target.path)).sort(), + sampleCounts: imported.animations.flatMap((animation) => animation.samplers.map((sampler) => sampler.input?.count ?? 0)), + }, + }; + }); + const report = { + schemaVersion: 1, + task: "M12-06B", + operation: "WEB_GLB_IMPORT_SEMANTIC_COMPARISON", + parentManifestSha256: sha256(fs.readFileSync(parentManifestPath)), + fixtureReportSha256: sha256(fs.readFileSync(fixtureReportPath)), + fixtureCount: comparisons.length, + comparedDomains: ["topology", "attributes", "materials", "nodes", "animations"], + allCompatible: comparisons.every((comparison) => comparison.compatible && comparison.mismatchCount === 0), + comparisons, + routeState: "BLOCKED_UNTIL_MAIN_PERSISTENCE", + nextTask: "M12-06C", + }; + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, JSON.stringify(report, null, 2) + "\n"); + process.stdout.write(`glb-desktop-import-report-generated fixtures=${report.fixtureCount} domains=${report.comparedDomains.length} compatible=${report.allCompatible} next=${report.nextTask}\n`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/generate-glb-main-persistence-fixtures.py b/tools/web/generate-glb-main-persistence-fixtures.py new file mode 100644 index 00000000..bedc7665 --- /dev/null +++ b/tools/web/generate-glb-main-persistence-fixtures.py @@ -0,0 +1,178 @@ +"""Import each M12 GLB with Blender 5.2 and freeze a Main persistence baseline.""" + +import hashlib +import json +import os +import sys +from pathlib import Path + +import bpy + + +FIXTURE_IDS = ("mesh", "pbr", "uv", "skin", "animation") + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def rounded(value): + return round(float(value), 6) + + +def graph_report(): + objects = [] + for obj in sorted(bpy.data.objects, key=lambda item: item.name): + objects.append( + { + "name": obj.name, + "type": obj.type, + "data": obj.data.name if obj.data is not None else None, + "parent": obj.parent.name if obj.parent is not None else None, + "children": sorted(child.name for child in obj.children), + "location": [rounded(value) for value in obj.location], + } + ) + meshes = [] + for mesh in sorted(bpy.data.meshes, key=lambda item: item.name): + mesh.calc_loop_triangles() + meshes.append( + { + "name": mesh.name, + "vertexCount": len(mesh.vertices), + "polygonCount": len(mesh.polygons), + "triangleCount": len(mesh.loop_triangles), + "uvLayers": sorted(layer.name for layer in mesh.uv_layers), + "materials": [material.name if material else None for material in mesh.materials], + } + ) + materials = [] + for material in sorted(bpy.data.materials, key=lambda item: item.name): + materials.append( + { + "name": material.name, + "nodeNames": sorted(node.name for node in material.node_tree.nodes) if material.node_tree else [], + } + ) + images = [] + for image in sorted(bpy.data.images, key=lambda item: item.name): + images.append( + { + "name": image.name, + "size": list(image.size), + "packed": image.packed_file is not None, + "mimeType": image.file_format, + } + ) + armatures = [] + for armature in sorted(bpy.data.armatures, key=lambda item: item.name): + armatures.append( + { + "name": armature.name, + "bones": [ + {"name": bone.name, "parent": bone.parent.name if bone.parent else None} + for bone in sorted(armature.bones, key=lambda item: item.name) + ], + } + ) + actions = [] + action_stable_ids = [] + for action in sorted(bpy.data.actions, key=lambda item: item.name): + fcurves = [] + for layer in action.layers: + for strip in layer.strips: + for channelbag in strip.channelbags: + fcurves.extend((curve.data_path, curve.array_index) for curve in channelbag.fcurves) + actions.append( + { + "name": action.name, + "frameRange": [rounded(action.frame_range[0]), rounded(action.frame_range[1])], + "fcurves": sorted(fcurves), + } + ) + owners = sorted( + object_.name + for object_ in bpy.data.objects + if object_.animation_data is not None and object_.animation_data.action == action + ) + action_stable_ids.extend( + "action:" + action.name + ":object:" + owner for owner in owners + ) + if not owners: + action_stable_ids.append("action:" + action.name) + return { + "objects": objects, + "meshes": meshes, + "materials": materials, + "images": images, + "armatures": armatures, + "actions": actions, + "stableIds": { + "objects": ["object:" + item["name"] for item in objects], + "meshes": ["mesh:" + item["name"] for item in meshes], + "materials": ["material:" + item["name"] for item in materials], + "images": ["image:" + item["name"] for item in images], + "armatures": ["armature:" + item["name"] for item in armatures], + "actions": sorted(action_stable_ids), + }, + } + + +def import_and_save(glb_path, blend_path): + bpy.ops.wm.read_factory_settings(use_empty=True) + result = bpy.ops.import_scene.gltf(filepath=str(glb_path)) + if "FINISHED" not in result: + raise RuntimeError("Blender GLB import failed: %s" % (result,)) + scene = bpy.context.scene + scene.frame_start = 1 + scene.frame_end = 25 + before = graph_report() + bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), check_existing=False) + bpy.ops.wm.open_mainfile(filepath=str(blend_path), load_ui=False) + reopened = graph_report() + if before != reopened: + raise RuntimeError("desktop import save/reopen semantic drift: %s" % blend_path) + return reopened + + +def main(glb_root, output_root, report_path): + glb_root = Path(glb_root).resolve() + output_root = Path(output_root).resolve() + report_path = Path(report_path).resolve() + output_root.mkdir(parents=True, exist_ok=True) + fixtures = [] + for fixture_id in FIXTURE_IDS: + glb_path = glb_root / (fixture_id + ".glb") + blend_path = output_root / (fixture_id + ".blend") + graph = import_and_save(glb_path, blend_path) + fixtures.append( + { + "id": fixture_id, + "glb": {"file": glb_path.name, "sha256": sha256_file(glb_path)}, + "blend": {"file": blend_path.name, "byteLength": blend_path.stat().st_size, "sha256": sha256_file(blend_path)}, + "graph": graph, + } + ) + report = { + "schemaVersion": 1, + "task": "M12-06C", + "operation": "DESKTOP_GLB_IMPORT_MAIN_PERSISTENCE_BASELINE", + "blenderVersion": bpy.app.version_string, + "fixtureCount": len(fixtures), + "fixtures": fixtures, + "nextTask": "M12-06D", + } + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("glb-main-persistence-fixtures-generated fixtures=%s next=%s" % (len(fixtures), report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 3: + raise SystemExit("usage: blender --background --python generate-glb-main-persistence-fixtures.py -- GLB_ROOT OUTPUT_ROOT REPORT") + main(args[0], args[1], args[2]) diff --git a/tools/web/generate-glb-web-reimport-report.py b/tools/web/generate-glb-web-reimport-report.py new file mode 100644 index 00000000..b59d5d43 --- /dev/null +++ b/tools/web/generate-glb-web-reimport-report.py @@ -0,0 +1,149 @@ +"""Re-import Web-produced GLBs in Blender 5.2 and emit canonical graph reports.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +FIXTURE_IDS = ("pbr", "uv", "skin", "animation") + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def rounded(value): + return round(float(value), 6) + + +def graph_report(): + objects = [] + for obj in sorted(bpy.data.objects, key=lambda item: item.name): + objects.append({ + "name": obj.name, + "type": obj.type, + "data": obj.data.name if obj.data is not None else None, + "parent": obj.parent.name if obj.parent is not None else None, + "children": sorted(child.name for child in obj.children), + "location": [rounded(value) for value in obj.location], + }) + meshes = [] + for mesh in sorted(bpy.data.meshes, key=lambda item: item.name): + mesh.calc_loop_triangles() + meshes.append({ + "name": mesh.name, + "vertexCount": len(mesh.vertices), + "polygonCount": len(mesh.polygons), + "triangleCount": len(mesh.loop_triangles), + "uvLayers": sorted(layer.name for layer in mesh.uv_layers), + "materials": [material.name if material else None for material in mesh.materials], + }) + materials = [] + for material in sorted(bpy.data.materials, key=lambda item: item.name): + materials.append({ + "name": material.name, + "nodeNames": sorted(node.name for node in material.node_tree.nodes) if material.node_tree else [], + }) + images = [] + for image in sorted(bpy.data.images, key=lambda item: item.name): + images.append({ + "name": image.name, + "size": list(image.size), + "packed": image.packed_file is not None, + "mimeType": image.file_format, + }) + armatures = [] + for armature in sorted(bpy.data.armatures, key=lambda item: item.name): + armatures.append({ + "name": armature.name, + "bones": [{"name": bone.name, "parent": bone.parent.name if bone.parent else None} for bone in sorted(armature.bones, key=lambda item: item.name)], + }) + actions = [] + action_stable_ids = [] + for action in sorted(bpy.data.actions, key=lambda item: item.name): + fcurves = [] + for layer in action.layers: + for strip in layer.strips: + for channelbag in strip.channelbags: + fcurves.extend((curve.data_path, curve.array_index) for curve in channelbag.fcurves) + actions.append({ + "name": action.name, + "frameRange": [rounded(action.frame_range[0]), rounded(action.frame_range[1])], + "fcurves": sorted(fcurves), + }) + owners = sorted(object_.name for object_ in bpy.data.objects if object_.animation_data is not None and object_.animation_data.action == action) + action_stable_ids.extend("action:" + action.name + ":object:" + owner for owner in owners) + if not owners: + action_stable_ids.append("action:" + action.name) + return { + "objects": objects, + "meshes": meshes, + "materials": materials, + "images": images, + "armatures": armatures, + "actions": actions, + "stableIds": { + "objects": ["object:" + item["name"] for item in objects], + "meshes": ["mesh:" + item["name"] for item in meshes], + "materials": ["material:" + item["name"] for item in materials], + "images": ["image:" + item["name"] for item in images], + "armatures": ["armature:" + item["name"] for item in armatures], + "actions": sorted(action_stable_ids), + }, + } + + +def import_reopen(glb_path, blend_path): + bpy.ops.wm.read_factory_settings(use_empty=True) + result = bpy.ops.import_scene.gltf(filepath=str(glb_path)) + if "FINISHED" not in result: + raise RuntimeError("Blender Web GLB import failed: %s" % (result,)) + before = graph_report() + bpy.ops.wm.save_as_mainfile(filepath=str(blend_path), check_existing=False) + bpy.ops.wm.open_mainfile(filepath=str(blend_path), load_ui=False) + after = graph_report() + if before != after: + raise RuntimeError("desktop Web GLB save/reopen semantic drift: %s" % glb_path) + return after + + +def main(glb_root, output_root, report_path): + glb_root = Path(glb_root).resolve() + output_root = Path(output_root).resolve() + report_path = Path(report_path).resolve() + output_root.mkdir(parents=True, exist_ok=True) + fixtures = [] + for fixture_id in FIXTURE_IDS: + glb_path = glb_root / (fixture_id + ".glb") + blend_path = output_root / (fixture_id + ".blend") + fixtures.append({ + "id": fixture_id, + "glb": {"file": glb_path.name, "byteLength": glb_path.stat().st_size, "sha256": sha256_file(glb_path)}, + "graph": import_reopen(glb_path, blend_path), + }) + report = { + "schemaVersion": 1, + "task": "M12-06E", + "operation": "DESKTOP_REIMPORT_WEB_GLB_CANONICAL_REPORT", + "blenderVersion": bpy.app.version_string, + "fixtureCount": len(fixtures), + "fixtures": fixtures, + "nextTask": "M12-06F", + } + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("glb-web-reimport-report-generated fixtures=%s next=%s" % (len(fixtures), report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 3: + raise SystemExit("usage: blender --background --python generate-glb-web-reimport-report.py -- GLB_ROOT OUTPUT_ROOT REPORT") + main(args[0], args[1], args[2]) diff --git a/tools/web/generate-io-format-capability-matrix.mjs b/tools/web/generate-io-format-capability-matrix.mjs new file mode 100644 index 00000000..45b40a8a --- /dev/null +++ b/tools/web/generate-io-format-capability-matrix.mjs @@ -0,0 +1,50 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventoryPath = path.join(repoRoot, "tests/golden/M12-05A/format-inventory.json"); +const outputArgument = process.argv.indexOf("--output"); +const outputPath = outputArgument === -1 + ? path.join(repoRoot, "tests/golden/M12-05B/capability-matrix.json") + : path.resolve(process.argv[outputArgument + 1] ?? ""); +if (!outputPath) throw new Error("--output requires a file"); + +const inventoryBytes = fs.readFileSync(inventoryPath); +const inventory = JSON.parse(inventoryBytes); +const runtimeInventorySha256 = crypto.createHash("sha256").update(inventoryBytes).digest("hex"); +const formatOrder = ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"]; + +const blocked = (code = "IO_FORMAT_UNSUPPORTED") => ({ status: "BLOCKED", execution: "NONE", code }); +const feature = (status, evidence) => ({ status, evidence }); +const operation = (format, name) => { + const glbExport = format === "GLB" && name === "EXPORT"; + const bounded = glbExport + ? feature("PARTIAL", "bounded GLB export gate exists; full format round-trip remains M12-06") + : feature("UNVERIFIED", "no verified Web executor for this format/operation"); + return { + local: glbExport ? { status: "READY", execution: "LOCAL", code: null } : blocked(), + server: blocked(), + geometry: bounded, + material: bounded, + animation: bounded, + }; +}; + +const byFormat = new Map(inventory.formats.map((entry) => [entry.format, entry])); +const formats = formatOrder.map((format) => { + const runtime = byFormat.get(format); + if (!runtime) throw new Error(`inventory is missing ${format}`); + return { + format, + runtimeImportStatus: runtime.import.runtimeStatus === "AVAILABLE" ? "AVAILABLE" : "OPERATOR_UNREGISTERED", + runtimeExportStatus: runtime.export.runtimeStatus === "AVAILABLE" ? "AVAILABLE" : "OPERATOR_UNREGISTERED", + operations: { IMPORT: operation(format, "IMPORT"), EXPORT: operation(format, "EXPORT") }, + }; +}); + +const matrix = { schemaVersion: 1, task: "M12-05B", runtimeInventorySha256, formats }; +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(matrix, null, 2)}\n`); +process.stdout.write(`io-format-capability-matrix-generated formats=${formats.length} output=${outputPath}\n`); diff --git a/tools/web/generate-io-format-receipt-bindings.mjs b/tools/web/generate-io-format-receipt-bindings.mjs new file mode 100644 index 00000000..7dc4e6fe --- /dev/null +++ b/tools/web/generate-io-format-receipt-bindings.mjs @@ -0,0 +1,32 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventoryPath = path.join(repoRoot, "tests/golden/M12-05A/format-inventory.json"); +const parentPath = path.join(repoRoot, "tests/golden/M12-05D/runtime-receipts.json"); +const outputArgument = process.argv.indexOf("--output"); +const outputPath = outputArgument === -1 ? path.join(repoRoot, "tests/golden/M12-05E/bound-runtime-receipts.json") : path.resolve(process.argv[outputArgument + 1] ?? ""); +if (!outputPath) throw new Error("--output requires a file"); +const inventoryBytes = fs.readFileSync(inventoryPath); +const parentBytes = fs.readFileSync(parentPath); +const inventory = JSON.parse(inventoryBytes); +const parent = JSON.parse(parentBytes); +const canonical = (value) => value === null || typeof value !== "object" ? JSON.stringify(value) : Array.isArray(value) ? `[${value.map(canonical).join(",")}]` : `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`; +const hash = (value) => crypto.createHash("sha256").update(canonical(value)).digest("hex"); +const runtimeSha256 = hash(inventory.runtime); +const receipts = inventory.formats.flatMap((entry) => ["IMPORT", "EXPORT"].map((operation) => { + const source = entry[operation.toLowerCase()]; + const receipt = parent.receipts.find((item) => item.format === entry.format && item.operation === operation); + return { + ...receipt, + sourceSha256: hash({ format: receipt.format, family: receipt.family, operation: receipt.operation, operator: receipt.operator, registered: receipt.registered, rnaIdentifier: receipt.rnaIdentifier }), + settingsSha256: hash({ buildOption: receipt.buildOption, buildOptionEnabled: receipt.buildOptionEnabled, variants: receipt.variants, extensions: receipt.extensions, properties: source.properties }), + runtimeSha256, + }; +})); +const output = { schemaVersion: 1, task: "M12-05E", parentReceiptSetSha256: crypto.createHash("sha256").update(parentBytes).digest("hex"), inventorySha256: crypto.createHash("sha256").update(inventoryBytes).digest("hex"), runtime: inventory.runtime, receipts }; +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`); +process.stdout.write(`io-format-receipt-bindings-generated receipts=${receipts.length} output=${path.relative(repoRoot, outputPath)}\n`); diff --git a/tools/web/generate-io-format-receipt-freshness.mjs b/tools/web/generate-io-format-receipt-freshness.mjs new file mode 100644 index 00000000..eb875855 --- /dev/null +++ b/tools/web/generate-io-format-receipt-freshness.mjs @@ -0,0 +1,49 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const parentPath = path.join(repoRoot, "tests/golden/M12-05E/bound-runtime-receipts.json"); +const outputArgument = process.argv.indexOf("--output"); +const outputPath = outputArgument === -1 ? path.join(repoRoot, "tests/golden/M12-05F/fresh-runtime-receipts.json") : path.resolve(process.argv[outputArgument + 1] ?? ""); +const expectedArgument = process.argv.indexOf("--expected-output"); +const expectedOutputPath = expectedArgument === -1 ? null : path.resolve(process.argv[expectedArgument + 1] ?? ""); +if (!outputPath) throw new Error("--output requires a file"); + +function stableValue(value) { + if (Array.isArray(value)) return value.map(stableValue); + if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])])); + return value; +} + +function sha256(value) { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +const parentBytes = fs.readFileSync(parentPath); +const bound = JSON.parse(parentBytes); +if (bound.schemaVersion !== 1 || bound.task !== "M12-05E") throw new Error("M12-05E bound receipt header is invalid"); +const output = { + schemaVersion: 1, + task: "M12-05F", + parentBindingSha256: sha256(parentBytes), + boundReceiptSetSha256: sha256(JSON.stringify(stableValue(bound))), + runtimeSha256: sha256(JSON.stringify(stableValue(bound.runtime))), + bound, +}; +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`); +if (expectedOutputPath) { + fs.mkdirSync(path.dirname(expectedOutputPath), { recursive: true }); + fs.writeFileSync(expectedOutputPath, `${JSON.stringify({ + parentBindingSha256: output.parentBindingSha256, + parentReceiptSetSha256: bound.parentReceiptSetSha256, + inventorySha256: bound.inventorySha256, + boundReceiptSetSha256: output.boundReceiptSetSha256, + runtimeSha256: output.runtimeSha256, + runtime: bound.runtime, + receiptIdentities: bound.receipts, + }, null, 2)}\n`); +} +process.stdout.write(`io-format-receipt-freshness-generated receipts=${bound.receipts.length} output=${path.relative(repoRoot, outputPath)}\n`); diff --git a/tools/web/generate-io-format-runtime-inventory.py b/tools/web/generate-io-format-runtime-inventory.py new file mode 100644 index 00000000..fb9e1f15 --- /dev/null +++ b/tools/web/generate-io-format-runtime-inventory.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +"""Generate the format capability inventory from one pinned Blender runtime.""" + +import hashlib +import json +import os +import pathlib +import sys + +import bpy + + +def decode(value): + if isinstance(value, bytes): + return value.decode("utf-8", errors="replace") + return str(value) + + +def binary_sha256(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def property_info(prop): + result = {"identifier": prop.identifier, "type": prop.type} + if hasattr(prop, "array_length"): + result["arrayLength"] = prop.array_length + if prop.type == "ENUM": + try: + result["enumItems"] = [item.identifier for item in prop.enum_items] + except (AttributeError, RuntimeError): + result["enumItems"] = [] + return result + + +def operator_info(operator_path, build_option): + module_name, operator_name = operator_path.split(".", 1) + try: + operator = getattr(getattr(bpy.ops, module_name), operator_name) + rna = operator.get_rna_type() + properties = [property_info(prop) for prop in rna.properties if prop.identifier != "rna_type"] + properties.sort(key=lambda prop: prop["identifier"]) + enabled = True if build_option is None else bool(getattr(bpy.app.build_options, build_option)) + return { + "operator": operator_path, + "registered": True, + "rnaIdentifier": rna.identifier, + "buildOption": build_option, + "buildOptionEnabled": enabled, + "runtimeStatus": "AVAILABLE" if enabled else "BUILD_OPTION_DISABLED", + "properties": properties, + } + except (AttributeError, KeyError, RuntimeError) as error: + return { + "operator": operator_path, + "registered": False, + "rnaIdentifier": None, + "buildOption": build_option, + "buildOptionEnabled": None, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "properties": [], + "error": type(error).__name__, + } + + +def format_entry(format_id, family, extensions, import_operator, export_operator, build_option, variants): + return { + "format": format_id, + "family": family, + "extensions": extensions, + "variants": variants, + "import": operator_info(import_operator, build_option), + "export": operator_info(export_operator, build_option), + } + + +def main(): + if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1:]) != 1: + raise SystemExit("usage: generate-io-format-runtime-inventory.py OUTPUT.json") + output = pathlib.Path(sys.argv[sys.argv.index("--") + 1]).resolve() + version = tuple(int(value) for value in bpy.app.version) + if version != (5, 2, 0): + raise RuntimeError(f"expected Blender 5.2.0, got {version}") + binary_path = pathlib.Path(bpy.app.binary_path).resolve() + options = { + "alembic": bool(bpy.app.build_options.alembic), + "usd": bool(bpy.app.build_options.usd), + "io_ply": bool(bpy.app.build_options.io_ply), + "io_stl": bool(bpy.app.build_options.io_stl), + "io_wavefront_obj": bool(bpy.app.build_options.io_wavefront_obj), + } + formats = [ + format_entry("GLTF", "GLTF", [".gltf"], "import_scene.gltf", "export_scene.gltf", None, ["GLTF_SEPARATE"]), + format_entry("GLB", "GLTF", [".glb"], "import_scene.gltf", "export_scene.gltf", None, ["GLB"]), + format_entry("OBJ", "OBJ", [".obj"], "wm.obj_import", "wm.obj_export", "io_wavefront_obj", ["OBJ"]), + format_entry("STL", "STL", [".stl"], "wm.stl_import", "wm.stl_export", "io_stl", ["STL_BINARY", "STL_ASCII"]), + format_entry("PLY", "PLY", [".ply"], "wm.ply_import", "wm.ply_export", "io_ply", ["PLY"]), + format_entry("USD", "USD", [".usd", ".usda", ".usdc", ".usdz"], "wm.usd_import", "wm.usd_export", "usd", ["USD", "USDA", "USDC", "USDZ"]), + format_entry("ALEMBIC", "ALEMBIC", [".abc"], "wm.alembic_import", "wm.alembic_export", "alembic", ["ALEMBIC"]), + ] + inventory = { + "schemaVersion": 1, + "task": "M12-05A", + "runtime": { + "blenderVersion": bpy.app.version_string, + "versionTuple": list(version), + "buildHash": decode(bpy.app.build_hash), + "buildBranch": decode(bpy.app.build_branch), + "buildPlatform": decode(bpy.app.build_platform), + "buildType": decode(bpy.app.build_type), + "buildDate": decode(bpy.app.build_date), + "buildTime": decode(bpy.app.build_time), + "buildCommitTimestamp": int(bpy.app.build_commit_timestamp), + "binarySha256": binary_sha256(binary_path), + "buildOptions": options, + }, + "formats": formats, + } + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(inventory, sort_keys=True, indent=2) + "\n", encoding="utf-8") + print(f"io-format-runtime-inventory-generated formats={len(formats)} blender={bpy.app.version_string} output={output}") + + +if __name__ == "__main__": + main() diff --git a/tools/web/generate-io-format-runtime-receipts.mjs b/tools/web/generate-io-format-runtime-receipts.mjs new file mode 100644 index 00000000..21f68f33 --- /dev/null +++ b/tools/web/generate-io-format-runtime-receipts.mjs @@ -0,0 +1,39 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventoryPath = path.join(repoRoot, "tests/golden/M12-05A/format-inventory.json"); +const outputArgument = process.argv.indexOf("--output"); +const outputPath = outputArgument === -1 ? path.join(repoRoot, "tests/golden/M12-05D/runtime-receipts.json") : path.resolve(process.argv[outputArgument + 1] ?? ""); +if (!outputPath) throw new Error("--output requires a file"); +const inventoryBytes = fs.readFileSync(inventoryPath); +const inventory = JSON.parse(inventoryBytes); +if (inventory.schemaVersion !== 1 || inventory.task !== "M12-05A") throw new Error("M12-05A inventory header is invalid"); +const receipts = inventory.formats.flatMap((entry) => ["IMPORT", "EXPORT"].map((operation) => { + const source = entry[operation.toLowerCase()]; + return { + format: entry.format, + family: entry.family, + operation, + operator: source.operator, + registered: source.registered, + rnaIdentifier: source.rnaIdentifier, + buildOption: source.buildOption, + buildOptionEnabled: source.buildOptionEnabled, + runtimeStatus: source.runtimeStatus, + variants: [...entry.variants], + extensions: [...entry.extensions], + }; +})); +const output = { + schemaVersion: 1, + task: "M12-05D", + inventorySha256: crypto.createHash("sha256").update(inventoryBytes).digest("hex"), + runtime: inventory.runtime, + receipts, +}; +fs.mkdirSync(path.dirname(outputPath), { recursive: true }); +fs.writeFileSync(outputPath, `${JSON.stringify(output, null, 2)}\n`); +process.stdout.write(`io-format-runtime-receipts-generated formats=${inventory.formats.length} receipts=${receipts.length} output=${path.relative(repoRoot, outputPath)}\n`); diff --git a/tools/web/generate-io-format-ui-gate.mjs b/tools/web/generate-io-format-ui-gate.mjs new file mode 100644 index 00000000..d796bd64 --- /dev/null +++ b/tools/web/generate-io-format-ui-gate.mjs @@ -0,0 +1,43 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const matrixPath = path.join(repoRoot, "tests/golden/M12-05B/capability-matrix.json"); +const protocolPath = path.join(repoRoot, "web/protocol/io-format-capability-matrix.ts"); +const gatePath = path.join(repoRoot, "web/protocol/io-format-ui-gate.ts"); +const output = process.argv[process.argv.indexOf("--output") + 1]; +if (!output || output.startsWith("--")) throw new Error("usage: node generate-io-format-ui-gate.mjs --output "); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-ui-gate-")); +try { + const transpile = (sourcePath, outputName) => { + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + if (result.diagnostics?.length) throw new Error(ts.formatDiagnosticsWithColorAndContext(result.diagnostics, { getCanonicalFileName: (file) => file, getCurrentDirectory: () => repoRoot, getNewLine: () => "\n" })); + const target = path.join(temporary, outputName); + fs.writeFileSync(target, result.outputText); + return target; + }; + const matrixModulePath = transpile(protocolPath, "io-format-capability-matrix.mjs"); + const gateModulePath = transpile(gatePath, "io-format-ui-gate.mjs"); + const matrixModule = await import(pathToFileURL(matrixModulePath)); + const gateModule = await import(pathToFileURL(gateModulePath)); + const matrixBytes = fs.readFileSync(matrixPath); + const matrix = matrixModule.parseIOFormatCapabilityMatrix(JSON.parse(matrixBytes)); + const registry = gateModule.buildIOFormatUIRegistry(matrix, crypto.createHash("sha256").update(matrixBytes).digest("hex")); + const target = path.resolve(repoRoot, output); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, `${JSON.stringify(registry, null, 2)}\n`); + process.stdout.write(`io-format-ui-gate-generated output=${path.relative(repoRoot, target)} import=${registry.importRoutes.length} export=${registry.exportRoutes.length}\n`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + diff --git a/tools/web/generate-library-link-fixture.py b/tools/web/generate-library-link-fixture.py new file mode 100644 index 00000000..eabd47b4 --- /dev/null +++ b/tools/web/generate-library-link-fixture.py @@ -0,0 +1,183 @@ +import hashlib +import json +import pathlib +import struct +import sys + +import bpy + + +ROOT_OBJECT = "M12 Link Object" +MESH_NAME = "M12 Link Mesh" +MATERIAL_NAME = "M12 Link Material" +IMAGE_NAME = "M12 Link Image" +IMAGE_NODE_NAME = "M12 Link Image Node" +SOURCE_MARKER = "M12-03F" + + +def sha256_file(path: pathlib.Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def pixel_sha256(image: bpy.types.Image) -> str: + values = list(image.pixels) + return hashlib.sha256(struct.pack(f"<{len(values)}f", *values)).hexdigest() + + +def reset() -> None: + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def create_source(path: pathlib.Path) -> None: + reset() + image = bpy.data.images.new(IMAGE_NAME, width=2, height=2, alpha=True, float_buffer=False) + image.colorspace_settings.name = "sRGB" + image.pixels = [ + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 0.5, + ] + image.pack() + + material = bpy.data.materials.new(MATERIAL_NAME) + material.use_nodes = True + node_tree = material.node_tree + principled = node_tree.nodes.get("Principled BSDF") + image_node = node_tree.nodes.new("ShaderNodeTexImage") + image_node.name = IMAGE_NODE_NAME + image_node.label = IMAGE_NODE_NAME + image_node.image = image + image_node.interpolation = "Closest" + image_node.extension = "REPEAT" + node_tree.links.new(image_node.outputs["Color"], principled.inputs["Base Color"]) + + mesh = bpy.data.meshes.new(MESH_NAME) + mesh.from_pydata( + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [], + [(0, 1, 2, 3)], + ) + mesh.materials.append(material) + uv_layer = mesh.uv_layers.new(name="UVMap") + for loop, uv in zip(uv_layer.data, [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]): + loop.uv = uv + mesh.update() + + obj = bpy.data.objects.new(ROOT_OBJECT, mesh) + obj["m12_source_marker"] = SOURCE_MARKER + bpy.context.scene.collection.objects.link(obj) + bpy.ops.wm.save_as_mainfile(filepath=str(path), check_existing=False) + + +def data_block(id_type: str, value: object) -> dict: + library = value.library + return { + "idType": id_type, + "name": value.name, + "nameFull": value.name_full, + "library": None if library is None else pathlib.Path(library.filepath).name, + "isLibraryOverride": value.override_library is not None, + } + + +def inspect_graph() -> dict: + obj = bpy.data.objects[ROOT_OBJECT] + mesh = obj.data + material = mesh.materials[0] + image_node = material.node_tree.nodes[IMAGE_NODE_NAME] + image = image_node.image + ids = { + "OBJECT": data_block("OBJECT", obj), + "MESH": data_block("MESH", mesh), + "MATERIAL": data_block("MATERIAL", material), + "IMAGE": data_block("IMAGE", image), + } + return { + "root": ids["OBJECT"], + "ids": ids, + "edges": [ + {"from": f"Object/{ROOT_OBJECT}", "relation": "OBJECT_DATA", "to": f"Mesh/{MESH_NAME}"}, + {"from": f"Mesh/{MESH_NAME}", "relation": "MATERIAL_SLOT[0]", "to": f"Material/{MATERIAL_NAME}"}, + {"from": f"Material/{MATERIAL_NAME}", "relation": f"NODE_IMAGE[{IMAGE_NODE_NAME}]", "to": f"Image/{IMAGE_NAME}"}, + ], + "geometry": { + "vertices": len(mesh.vertices), + "edges": len(mesh.edges), + "polygons": len(mesh.polygons), + "loops": len(mesh.loops), + "uvLayers": [layer.name for layer in mesh.uv_layers], + "materialSlots": [item.name for item in mesh.materials], + }, + "image": { + "size": list(image.size), + "channels": image.channels, + "colorspace": image.colorspace_settings.name, + "packed": image.packed_file is not None, + "pixelFloat32Sha256": pixel_sha256(image), + }, + "sourceMarker": obj["m12_source_marker"], + } + + +def link_object(source: pathlib.Path, target: pathlib.Path) -> dict: + reset() + with bpy.data.libraries.load(str(source), link=True) as (data_from, data_to): + if ROOT_OBJECT not in data_from.objects: + raise RuntimeError("source root object is missing") + data_to.objects = [ROOT_OBJECT] + if len(data_to.objects) != 1 or data_to.objects[0] is None: + raise RuntimeError("desktop link did not return one object") + bpy.context.scene.collection.objects.link(data_to.objects[0]) + before_save = inspect_graph() + if any(item["library"] is None or item["isLibraryOverride"] for item in before_save["ids"].values()): + raise RuntimeError("linked dependency closure did not retain the source library") + bpy.ops.wm.save_as_mainfile(filepath=str(target), check_existing=False) + bpy.ops.wm.open_mainfile(filepath=str(target), load_ui=False) + reopened = inspect_graph() + if reopened != before_save: + raise RuntimeError("linked dependency mapping drifted after save/reopen") + return reopened + + +def main() -> None: + if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2: + raise SystemExit("usage: blender --background --factory-startup --python generate-library-link-fixture.py -- OUTPUT_DIR REPORT") + output_arg, report_arg = sys.argv[sys.argv.index("--") + 1 :] + output_dir = pathlib.Path(output_arg).resolve() + report_path = pathlib.Path(report_arg).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + report_path.parent.mkdir(parents=True, exist_ok=True) + source = output_dir / "m12_link_source.blend" + target = output_dir / "m12_link_target.blend" + + create_source(source) + source_graph = inspect_graph() + linked_graph = link_object(source, target) + report = { + "schemaVersion": 1, + "task": "M12-03F", + "operation": "LINK", + "blenderVersion": "5.2.0", + "source": {"file": source.name, "sha256": sha256_file(source)}, + "target": {"file": target.name, "sha256": sha256_file(target)}, + "selectedRoots": [f"Object/{ROOT_OBJECT}"], + "sourceGraph": source_graph, + "linkedGraph": linked_graph, + "stableMapping": [ + {"source": f"Object/{ROOT_OBJECT}", "local": f"Object/{ROOT_OBJECT}", "owner": "SOURCE_LIBRARY", "readOnly": True}, + {"source": f"Mesh/{MESH_NAME}", "local": f"Mesh/{MESH_NAME}", "owner": "SOURCE_LIBRARY", "readOnly": True}, + {"source": f"Material/{MATERIAL_NAME}", "local": f"Material/{MATERIAL_NAME}", "owner": "SOURCE_LIBRARY", "readOnly": True}, + {"source": f"Image/{IMAGE_NAME}", "local": f"Image/{IMAGE_NAME}", "owner": "SOURCE_LIBRARY", "readOnly": True}, + ], + "nextTask": "M12-03G", + } + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print( + f"library-link-fixture-ok roots={len(report['selectedRoots'])} " + f"mapping={len(report['stableMapping'])} source={report['source']['sha256']} " + f"target={report['target']['sha256']} next={report['nextTask']}" + ) + + +main() diff --git a/tools/web/generate-library-override-fixture.py b/tools/web/generate-library-override-fixture.py new file mode 100644 index 00000000..b0ec946e --- /dev/null +++ b/tools/web/generate-library-override-fixture.py @@ -0,0 +1,164 @@ +import hashlib +import json +import pathlib +import sys + +import bpy + + +ROOT_OBJECT = "M12 Override Object" +MESH_NAME = "M12 Override Mesh" +MATERIAL_NAME = "M12 Override Material" +SOURCE_MARKER = "M12-03J" +PROJECT_ID = "m12-03j-project" +OVERRIDE_X = 2.5 +OVERRIDE_PROPERTY_PATH = '["m12_override_value"]' + + +def sha256_file(path: pathlib.Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def reset() -> None: + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def create_source(path: pathlib.Path) -> None: + reset() + material = bpy.data.materials.new(MATERIAL_NAME) + material.diffuse_color = (0.15, 0.65, 0.35, 1.0) + mesh = bpy.data.meshes.new(MESH_NAME) + mesh.from_pydata([(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [], [(0, 1, 2, 3)]) + mesh.materials.append(material) + obj = bpy.data.objects.new(ROOT_OBJECT, mesh) + obj["m12_source_marker"] = SOURCE_MARKER + obj["m12_override_value"] = 1.0 + bpy.context.scene.collection.objects.link(obj) + bpy.ops.wm.save_as_mainfile(filepath=str(path), check_existing=False) + + +def data_block(id_type: str, value: object) -> dict: + library = value.library + return { + "idType": id_type, + "name": value.name, + "nameFull": value.name_full, + "library": None if library is None else pathlib.Path(library.filepath).name, + "isLibraryOverride": value.override_library is not None, + } + + +def override_property(value: object) -> dict: + override = value.override_library + if override is None: + raise RuntimeError("override library metadata is missing") + properties = list(override.properties) + matches = [item for item in properties if item.rna_path == OVERRIDE_PROPERTY_PATH] + if not matches: + print("DEBUG_OVERRIDE_PROPERTIES", [(item.rna_path, len(item.operations)) for item in properties], dir(override.properties)) + raise RuntimeError("custom property override path is missing") + return { + "rnaPath": OVERRIDE_PROPERTY_PATH, + "index": 0, + "value": float(value["m12_override_value"]), + "operationCount": sum(len(item.operations) for item in matches), + "propertyCount": len(properties), + } + + +def inspect_override() -> dict: + obj = bpy.data.objects[ROOT_OBJECT] + override = obj.override_library + if override is None or override.reference is None: + raise RuntimeError("desktop override reference is missing") + reference = override.reference + if reference.library is None: + raise RuntimeError("override reference did not retain source library") + if obj.library is not None: + raise RuntimeError("override object must be locally owned") + return { + "reference": { + "dataBlockId": f"Object/{reference.name}", + "idType": "OBJECT", + "library": pathlib.Path(reference.library.filepath).name, + "owner": "SOURCE_LIBRARY", + "readOnly": True, + "isLibraryOverride": False, + }, + "local": { + "dataBlockId": f"Object/{obj.name}", + "idType": "OBJECT", + "library": None, + "owner": "LOCAL_OVERRIDE", + "projectId": PROJECT_ID, + "readOnly": False, + "referenceSourceDataBlockId": f"Object/{reference.name}", + "hierarchyRootDataBlockId": f"Object/{obj.name}", + "isLibraryOverride": True, + }, + "propertyOverride": override_property(obj), + "sourceMarker": reference.get("m12_source_marker"), + } + + +def create_override(source: pathlib.Path, target: pathlib.Path) -> dict: + reset() + with bpy.data.libraries.load(str(source), link=True) as (data_from, data_to): + if ROOT_OBJECT not in data_from.objects: + raise RuntimeError("source root object is missing") + data_to.objects = [ROOT_OBJECT] + if len(data_to.objects) != 1 or data_to.objects[0] is None: + raise RuntimeError("desktop link did not return one object") + linked = data_to.objects[0] + bpy.context.scene.collection.objects.link(linked) + bpy.context.view_layer.objects.active = linked + linked.select_set(True) + if bpy.ops.object.make_override_library() != {"FINISHED"}: + raise RuntimeError("desktop override operator did not finish") + obj = bpy.data.objects[ROOT_OBJECT] + obj["m12_override_value"] = OVERRIDE_X + prop = obj.override_library.properties.add(OVERRIDE_PROPERTY_PATH) + prop.operations.add("REPLACE") + before_save = inspect_override() + if before_save["propertyOverride"]["value"] != OVERRIDE_X: + raise RuntimeError("override property did not apply") + bpy.ops.wm.save_as_mainfile(filepath=str(target), check_existing=False) + bpy.ops.wm.open_mainfile(filepath=str(target), load_ui=False) + reopened = inspect_override() + if reopened != before_save: + raise RuntimeError("desktop override metadata drifted after save/reopen") + return reopened + + +def main() -> None: + if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2: + raise SystemExit("usage: blender --background --factory-startup --python generate-library-override-fixture.py -- OUTPUT_DIR REPORT") + output_arg, report_arg = sys.argv[sys.argv.index("--") + 1 :] + output_dir = pathlib.Path(output_arg).resolve() + report_path = pathlib.Path(report_arg).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + report_path.parent.mkdir(parents=True, exist_ok=True) + source = output_dir / "m12_override_source.blend" + target = output_dir / "m12_override_target.blend" + create_source(source) + override_graph = create_override(source, target) + report = { + "schemaVersion": 1, + "task": "M12-03J", + "operation": "LIBRARY_OVERRIDE", + "blenderVersion": "5.2.0", + "source": {"file": source.name, "sha256": sha256_file(source)}, + "target": {"file": target.name, "sha256": sha256_file(target)}, + "selectedRoots": [f"Object/{ROOT_OBJECT}"], + "overrideGraph": override_graph, + "nextTask": "M12-03K", + } + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print( + f"library-override-fixture-ok roots={len(report['selectedRoots'])} " + f"reference={override_graph['reference']['library']} owner={override_graph['local']['owner']} " + f"path={override_graph['propertyOverride']['rnaPath']} next={report['nextTask']}" + ) + + +main() diff --git a/tools/web/generate-malicious-archive-fixtures.mjs b/tools/web/generate-malicious-archive-fixtures.mjs new file mode 100644 index 00000000..6665a386 --- /dev/null +++ b/tools/web/generate-malicious-archive-fixtures.mjs @@ -0,0 +1,188 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const outputArgument = process.argv.indexOf("--output"); +const outputRoot = outputArgument === -1 + ? path.join(repoRoot, "tests/files/web/archive-security") + : path.resolve(process.argv[outputArgument + 1] ?? ""); +if (!outputRoot) throw new Error("--output requires a directory"); + +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const text = (value) => Buffer.from(value, "utf8"); + +const crcTable = Array.from({ length: 256 }, (_, input) => { + let value = input; + for (let bit = 0; bit < 8; bit++) value = value & 1 ? 0xedb88320 ^ value >>> 1 : value >>> 1; + return value >>> 0; +}); + +function crc32(bytes) { + let value = 0xffffffff; + for (const byte of bytes) value = crcTable[(value ^ byte) & 0xff] ^ value >>> 8; + return (value ^ 0xffffffff) >>> 0; +} + +function zipArchive(entries) { + const localParts = []; + const centralParts = []; + let localOffset = 0; + for (const entry of entries) { + const name = text(entry.path); + const data = Buffer.from(entry.data); + const uncompressedBytes = entry.uncompressedBytes ?? data.length; + const crc = crc32(data); + const local = Buffer.alloc(30); + local.writeUInt32LE(0x04034b50, 0); + local.writeUInt16LE(20, 4); + local.writeUInt16LE(0x0800, 6); + local.writeUInt16LE(0, 8); + local.writeUInt32LE(crc, 14); + local.writeUInt32LE(data.length, 18); + local.writeUInt32LE(uncompressedBytes, 22); + local.writeUInt16LE(name.length, 26); + localParts.push(local, name, data); + + const central = Buffer.alloc(46); + central.writeUInt32LE(0x02014b50, 0); + central.writeUInt16LE(0x0314, 4); + central.writeUInt16LE(20, 6); + central.writeUInt16LE(0x0800, 8); + central.writeUInt16LE(0, 10); + central.writeUInt32LE(crc, 16); + central.writeUInt32LE(data.length, 20); + central.writeUInt32LE(uncompressedBytes, 24); + central.writeUInt16LE(name.length, 28); + central.writeUInt32LE((0o100644 << 16) >>> 0, 38); + central.writeUInt32LE(localOffset, 42); + centralParts.push(central, name); + localOffset += local.length + name.length + data.length; + } + const centralDirectory = Buffer.concat(centralParts); + const end = Buffer.alloc(22); + end.writeUInt32LE(0x06054b50, 0); + end.writeUInt16LE(entries.length, 8); + end.writeUInt16LE(entries.length, 10); + end.writeUInt32LE(centralDirectory.length, 12); + end.writeUInt32LE(localOffset, 16); + return Buffer.concat([...localParts, centralDirectory, end]); +} + +function writeTarText(header, value, offset, length) { + const bytes = text(value); + if (bytes.length > length) throw new Error(`tar field exceeds ${length} bytes`); + bytes.copy(header, offset); +} + +function writeTarOctal(header, value, offset, length) { + const encoded = value.toString(8).padStart(length - 2, "0"); + writeTarText(header, `${encoded}\0 `, offset, length); +} + +function tarHeader(entry) { + const header = Buffer.alloc(512); + writeTarText(header, entry.path, 0, 100); + writeTarOctal(header, entry.type === "DIRECTORY" ? 0o755 : 0o644, 100, 8); + writeTarOctal(header, 0, 108, 8); + writeTarOctal(header, 0, 116, 8); + const data = entry.type === "FILE" ? Buffer.from(entry.data) : Buffer.alloc(0); + writeTarOctal(header, data.length, 124, 12); + writeTarOctal(header, 0, 136, 12); + header.fill(0x20, 148, 156); + header[156] = { FILE: 0x30, SYMLINK: 0x32, HARDLINK: 0x31, DIRECTORY: 0x35 }[entry.type]; + if (entry.target) writeTarText(header, entry.target, 157, 100); + writeTarText(header, "ustar\0", 257, 6); + writeTarText(header, "00", 263, 2); + writeTarText(header, "root", 265, 32); + writeTarText(header, "root", 297, 32); + const checksum = header.reduce((sum, byte) => sum + byte, 0); + writeTarOctal(header, checksum, 148, 8); + return { header, data }; +} + +function tarArchive(entries) { + const parts = []; + for (const entry of entries) { + const { header, data } = tarHeader(entry); + parts.push(header, data); + if (data.length % 512 !== 0) parts.push(Buffer.alloc(512 - data.length % 512)); + } + parts.push(Buffer.alloc(1024)); + return Buffer.concat(parts); +} + +const definitions = [ + { + id: "ZIP_PATH_TRAVERSAL", + format: "ZIP", + file: "zip-path-traversal.zip", + threat: "ARCHIVE_ROOT_ESCAPE", + gate: "LINK_SAFETY", + bytes: zipArchive([{ path: "../outside.txt", data: text("escape") }]), + }, + { + id: "ZIP_COMPRESSION_BOMB", + format: "ZIP", + file: "zip-compression-bomb.zip", + threat: "COMPRESSION_RATIO", + gate: "CONFLICTS", + bytes: zipArchive([{ path: "bomb.bin", data: Buffer.from([0]), uncompressedBytes: 101 }]), + }, + { + id: "ZIP_DUPLICATE_PATH", + format: "ZIP", + file: "zip-duplicate-path.zip", + threat: "DUPLICATE_PATH", + gate: "LINK_SAFETY", + bytes: zipArchive([{ path: "same.bin", data: text("one") }, { path: "same.bin", data: text("two") }]), + }, + { + id: "TAR_PATH_TRAVERSAL", + format: "TAR", + file: "tar-path-traversal.tar", + threat: "ARCHIVE_ROOT_ESCAPE", + gate: "LINK_SAFETY", + bytes: tarArchive([{ path: "../../outside.txt", type: "FILE", data: text("escape") }]), + }, + { + id: "TAR_SYMLINK_ESCAPE", + format: "TAR", + file: "tar-symlink-escape.tar", + threat: "SYMLINK_ESCAPE", + gate: "LINK_SAFETY", + bytes: tarArchive([ + { path: "safe", type: "DIRECTORY" }, + { path: "safe/link", type: "SYMLINK", target: "../../outside" }, + ]), + }, + { + id: "TAR_PREFIX_CONFLICT", + format: "TAR", + file: "tar-prefix-conflict.tar", + threat: "FILE_DIRECTORY_PREFIX_CONFLICT", + gate: "CONFLICTS", + bytes: tarArchive([ + { path: "folder", type: "FILE", data: text("one") }, + { path: "folder/payload.bin", type: "FILE", data: text("two") }, + ]), + }, +]; + +fs.mkdirSync(outputRoot, { recursive: true }); +for (const definition of definitions) fs.writeFileSync(path.join(outputRoot, definition.file), definition.bytes); +const manifest = { + schemaVersion: 1, + task: "M12-04J", + generator: "tools/web/generate-malicious-archive-fixtures.mjs", + extractionAllowed: false, + cases: definitions.map(({ bytes, ...definition }) => ({ + ...definition, + byteLength: bytes.length, + sha256: sha256(bytes), + expectedCode: "IO_ARCHIVE_UNSAFE", + })), +}; +fs.writeFileSync(path.join(outputRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`); +process.stdout.write(`malicious-archive-fixtures-generated cases=${definitions.length} output=${outputRoot}\n`); diff --git a/tools/web/generate-malicious-script-fixture.py b/tools/web/generate-malicious-script-fixture.py new file mode 100644 index 00000000..c79d187a --- /dev/null +++ b/tools/web/generate-malicious-script-fixture.py @@ -0,0 +1,39 @@ +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +SOURCES = { + "MaliciousText.py": "import os\nos.system('touch /tmp/web-blender-forbidden')\n", + "DriverExploit.py": "__import__('os').system('touch /tmp/web-driver-forbidden')\n", + "HandlerExploit.py": "def handler(scene):\n __import__('subprocess').run(['touch','/tmp/web-handler-forbidden'])\n", + "EmbeddedModule.py": "def register():\n __import__('os').system('touch /tmp/web-module-forbidden')\n", +} + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def main(output_dir, report_path): + output_dir = Path(output_dir).resolve(); report_path = Path(report_path).resolve(); output_dir.mkdir(parents=True, exist_ok=True); report_path.parent.mkdir(parents=True, exist_ok=True) + bpy.ops.wm.read_factory_settings(use_empty=True) + for name, source in SOURCES.items(): + value = bpy.data.texts.new(name); value.write(source); value.use_module = name == "EmbeddedModule.py" + fixture = output_dir / "malicious-script.blend"; bpy.ops.wm.save_as_mainfile(filepath=str(fixture), compress=False) + report = {"schemaVersion": 1, "task": "M13-01F", "operation": "MALICIOUS_SCRIPT_FIXTURE", "sources": [{"name": name, "sourceSha256": hashlib.sha256(source.encode()).hexdigest(), "useModule": name == "EmbeddedModule.py", "expectedExecution": "BLOCKED"} for name, source in sorted(SOURCES.items())], "fixture": {"name": fixture.name, "byteLength": fixture.stat().st_size, "sha256": sha256_file(fixture)}, "nextTask": "M13-02A"} + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("malicious-script-fixture-generated sources=%s module=1 next=%s" % (len(SOURCES), report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: raise SystemExit("usage: blender --background --python generate-malicious-script-fixture.py -- OUTPUT_DIR REPORT") + main(args[0], args[1]) diff --git a/tools/web/generate-obj-multi-negative-fixtures.py b/tools/web/generate-obj-multi-negative-fixtures.py new file mode 100644 index 00000000..0e15c012 --- /dev/null +++ b/tools/web/generate-obj-multi-negative-fixtures.py @@ -0,0 +1,285 @@ +"""Generate the pinned Blender 5.2 OBJ multi-object and negative fixtures for M12-07B.""" + +import hashlib +import json +import re +import struct +import sys +from pathlib import Path + +import bpy + + +OBJECTS = ("M12 OBJ Left", "M12 OBJ Right") +MATERIALS = ("M12 OBJ Left Material", "M12 OBJ Right Material") +TEXTURE_NAME = "m12_obj_texture.png" + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def text(value): + return value.decode("utf-8") if isinstance(value, bytes) else value + + +def runtime_identity(): + binary = Path(bpy.app.binary_path) + return { + "blenderVersion": text(bpy.app.version_string), + "versionTuple": list(bpy.app.version), + "buildDate": text(bpy.app.build_date), + "buildTime": text(bpy.app.build_time), + "buildHash": text(bpy.app.build_hash), + "buildBranch": text(bpy.app.build_branch), + "buildPlatform": text(bpy.app.build_platform), + "buildType": text(bpy.app.build_type), + "binarySha256": sha256_file(binary), + } + + +def create_texture(path): + image = bpy.data.images.new("M12 OBJ Texture", width=2, height=2, alpha=True, float_buffer=False) + image.pixels = [ + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + 1.0, 1.0, 0.0, 1.0, + ] + image.filepath_raw = str(path) + image.file_format = "PNG" + image.save() + return image + + +def material(name, image): + value = bpy.data.materials.new(name) + value.use_nodes = True + nodes = value.node_tree.nodes + links = value.node_tree.links + principled = nodes.get("Principled BSDF") + texture = nodes.new("ShaderNodeTexImage") + texture.image = image + links.new(texture.outputs["Color"], principled.inputs["Base Color"]) + return value + + +def mesh_object(name, offset, material_value): + mesh = bpy.data.meshes.new(name + " Mesh") + mesh.from_pydata( + [(offset - 0.75, -0.75, 0.0), (offset + 0.75, -0.75, 0.0), (offset + 0.0, 0.75, 0.0)], + [], + [(0, 1, 2)], + ) + mesh.update() + uv = mesh.uv_layers.new(name="UVMap") + for loop, value in zip(mesh.loops, ((0.0, 0.0), (1.0, 0.0), (0.5, 1.0))): + uv.data[loop.index].uv = value + mesh.materials.append(material_value) + obj = bpy.data.objects.new(name, mesh) + bpy.context.scene.collection.objects.link(obj) + return obj + + +def create_scene(output_dir): + bpy.ops.wm.read_factory_settings(use_empty=True) + image = create_texture(output_dir / TEXTURE_NAME) + left_material = material(MATERIALS[0], image) + right_material = material(MATERIALS[1], image) + left = mesh_object(OBJECTS[0], -1.0, left_material) + right = mesh_object(OBJECTS[1], 1.0, right_material) + for obj in (left, right): + obj.select_set(True) + for polygon in obj.data.polygons: + polygon.use_smooth = False + bpy.context.view_layer.objects.active = left + return left, right + + +def export_obj(output_path): + result = bpy.ops.wm.obj_export( + filepath=str(output_path), + export_selected_objects=True, + apply_modifiers=False, + apply_transform=False, + export_eval_mode="DAG_EVAL_VIEWPORT", + export_uv=True, + export_normals=True, + export_colors=False, + export_materials=True, + export_pbr_extensions=False, + export_material_groups=True, + export_object_groups=True, + export_vertex_groups=False, + export_smooth_groups=False, + export_triangulated_mesh=False, + export_curves_as_nurbs=False, + global_scale=1.0, + forward_axis="NEGATIVE_Z", + up_axis="Y", + path_mode="RELATIVE", + ) + if "FINISHED" not in result: + raise RuntimeError("Blender OBJ export did not finish: %s" % (result,)) + + +def parse_obj(path): + positions = [] + texcoords = [] + normals = [] + faces = [] + material_libraries = [] + objects = [] + groups = [] + current_object = None + current_material = None + current_groups = [] + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + kind = parts[0] + if kind == "v": + positions.append([float(value) for value in parts[1:4]]) + elif kind == "vt": + texcoords.append([float(value) for value in parts[1:3]]) + elif kind == "vn": + normals.append([float(value) for value in parts[1:4]]) + elif kind == "mtllib": + material_libraries.extend(parts[1:]) + elif kind == "o": + current_object = " ".join(parts[1:]) + objects.append(current_object) + elif kind == "g": + current_groups = parts[1:] + for group in current_groups: + if group not in groups: + groups.append(group) + if group.endswith("_Mesh") and group not in objects: + current_object = group + objects.append(group) + elif kind == "usemtl": + current_material = " ".join(parts[1:]) + elif kind == "f": + vertices = [] + for token in parts[1:]: + indices = token.split("/") + vertices.append({ + "position": int(indices[0]), + "texcoord": int(indices[1]) if len(indices) > 1 and indices[1] else None, + "normal": int(indices[2]) if len(indices) > 2 and indices[2] else None, + }) + faces.append({"object": current_object, "groups": list(current_groups), "material": current_material, "vertices": vertices}) + return { + "materialLibraries": material_libraries, + "objects": objects, + "groups": groups, + "positions": positions, + "texcoords": texcoords, + "normals": normals, + "faces": faces, + } + + +def parse_mtl(path): + materials = [] + current = None + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if parts[0] == "newmtl": + current = {"name": " ".join(parts[1:]), "mapKd": None} + materials.append(current) + elif current is not None and parts[0] == "map_Kd": + current["mapKd"] = " ".join(parts[1:]) + return materials + + +def negative_index_obj(source, target): + lines = [] + for raw_line in source.read_text(encoding="utf-8").splitlines(): + if not raw_line.startswith("f "): + lines.append(raw_line) + continue + converted = [] + for token in raw_line.split()[1:]: + position, texcoord, normal = token.split("/") + converted.append("%d/%d/%d" % (-int(position), -int(texcoord), -int(normal))) + lines.append("f " + " ".join(converted)) + target.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def malformed_obj(source, target): + lines = [] + replaced = False + for raw_line in source.read_text(encoding="utf-8").splitlines(): + if raw_line.startswith("f ") and not replaced: + lines.append("f 1/1/1 2/2/1") + replaced = True + else: + lines.append(raw_line) + target.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main(output_dir, report_path): + output_dir = Path(output_dir).resolve() + report_path = Path(report_path).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + report_path.parent.mkdir(parents=True, exist_ok=True) + obj_path = output_dir / "multi-object.obj" + create_scene(output_dir) + export_obj(obj_path) + mtl_path = obj_path.with_suffix(".mtl") + if not mtl_path.exists(): + raise RuntimeError("Blender OBJ export did not write the MTL sidecar") + negative_path = output_dir / "negative-index.obj" + malformed_path = output_dir / "malformed-face.obj" + negative_index_obj(obj_path, negative_path) + malformed_obj(obj_path, malformed_path) + semantic = parse_obj(obj_path) + semantic["materials"] = parse_mtl(mtl_path) + files = [] + for name in (obj_path.name, mtl_path.name, TEXTURE_NAME, negative_path.name, malformed_path.name): + item = output_dir / name + files.append({"name": name, "byteLength": item.stat().st_size, "sha256": sha256_file(item)}) + report = { + "schemaVersion": 1, + "task": "M12-07B", + "operation": "DESKTOP_OBJ_MULTI_OBJECT_AND_NEGATIVE_FIXTURES", + "runtime": runtime_identity(), + "sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj", + "operator": "wm.obj_export", + "settings": { + "forwardAxis": "NEGATIVE_Z", + "upAxis": "Y", + "globalScale": 1.0, + "exportUV": True, + "exportNormals": True, + "exportMaterials": True, + "exportMaterialGroups": True, + "exportObjectGroups": True, + "pathMode": "RELATIVE", + }, + "files": files, + "semantic": semantic, + "negativeIndex": {"file": negative_path.name, "expectedStatus": "ACCEPT_WITH_NEGATIVE_INDICES", "faceCount": 2}, + "malformedFace": {"file": malformed_path.name, "expectedCode": "OBJ_FACE_ARITY_INVALID"}, + "textureOrigin": {"mtlMapKd": [material["mapKd"] for material in semantic["materials"]], "relative": True, "textureFile": TEXTURE_NAME}, + "nextTask": "M12-07C", + } + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("obj-multi-negative-fixtures-generated objects=%s faces=%s negative=true malformed=true next=%s" % (len(semantic["objects"]), len(semantic["faces"]), report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: + raise SystemExit("usage: blender --background --python generate-obj-multi-negative-fixtures.py -- OUTPUT_DIR REPORT") + main(args[0], args[1]) diff --git a/tools/web/generate-obj-single-mesh-fixture.py b/tools/web/generate-obj-single-mesh-fixture.py new file mode 100644 index 00000000..825bfc74 --- /dev/null +++ b/tools/web/generate-obj-single-mesh-fixture.py @@ -0,0 +1,239 @@ +"""Generate the pinned Blender 5.2 single-Mesh OBJ fixture for M12-07A.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +OBJ_NAME = "M12 OBJ Single Mesh" +MATERIAL_NAMES = ("M12 OBJ Red", "M12 OBJ Blue") + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def text(value): + return value.decode("utf-8") if isinstance(value, bytes) else value + + +def runtime_identity(): + binary = Path(bpy.app.binary_path) + return { + "blenderVersion": text(bpy.app.version_string), + "versionTuple": list(bpy.app.version), + "buildDate": text(bpy.app.build_date), + "buildTime": text(bpy.app.build_time), + "buildHash": text(bpy.app.build_hash), + "buildBranch": text(bpy.app.build_branch), + "buildPlatform": text(bpy.app.build_platform), + "buildType": text(bpy.app.build_type), + "binarySha256": sha256_file(binary), + } + + +def create_fixture(): + bpy.ops.wm.read_factory_settings(use_empty=True) + mesh = bpy.data.meshes.new(OBJ_NAME + " Mesh") + mesh.from_pydata( + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [], + [(0, 1, 2), (0, 2, 3)], + ) + mesh.update() + obj = bpy.data.objects.new(OBJ_NAME, mesh) + bpy.context.scene.collection.objects.link(obj) + + uv_layer = mesh.uv_layers.new(name="UVMap") + uv_by_vertex = ((0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)) + for loop in mesh.loops: + uv_layer.data[loop.index].uv = uv_by_vertex[loop.vertex_index] + + colors = ((0.8, 0.1, 0.05, 1.0), (0.05, 0.2, 0.85, 1.0)) + for name, color in zip(MATERIAL_NAMES, colors): + material = bpy.data.materials.new(name) + material.diffuse_color = color + material.metallic = 0.0 + material.roughness = 0.5 + mesh.materials.append(material) + mesh.polygons[0].material_index = 0 + mesh.polygons[1].material_index = 1 + for polygon in mesh.polygons: + polygon.use_smooth = False + + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + return obj + + +def export_obj(output_path): + result = bpy.ops.wm.obj_export( + filepath=str(output_path), + export_selected_objects=True, + apply_modifiers=False, + apply_transform=False, + export_eval_mode="DAG_EVAL_VIEWPORT", + export_uv=True, + export_normals=True, + export_colors=False, + export_materials=True, + export_pbr_extensions=False, + export_material_groups=True, + export_object_groups=False, + export_vertex_groups=False, + export_smooth_groups=False, + export_triangulated_mesh=False, + export_curves_as_nurbs=False, + global_scale=1.0, + forward_axis="NEGATIVE_Z", + up_axis="Y", + path_mode="RELATIVE", + ) + if "FINISHED" not in result: + raise RuntimeError("Blender OBJ export did not finish: %s" % (result,)) + + +def number(value): + parsed = float(value) + return 0.0 if parsed == 0.0 else parsed + + +def parse_obj(path): + positions = [] + texcoords = [] + normals = [] + faces = [] + material_libraries = [] + objects = [] + current_material = None + current_groups = [] + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + kind = parts[0] + if kind == "v": + positions.append([number(value) for value in parts[1:4]]) + elif kind == "vt": + texcoords.append([number(value) for value in parts[1:3]]) + elif kind == "vn": + normals.append([number(value) for value in parts[1:4]]) + elif kind == "mtllib": + material_libraries.extend(parts[1:]) + elif kind == "o": + objects.append(" ".join(parts[1:])) + elif kind == "g": + current_groups = parts[1:] + elif kind == "usemtl": + current_material = " ".join(parts[1:]) + elif kind == "f": + vertices = [] + for token in parts[1:]: + indices = token.split("/") + vertices.append({ + "position": int(indices[0]), + "texcoord": int(indices[1]) if len(indices) > 1 and indices[1] else None, + "normal": int(indices[2]) if len(indices) > 2 and indices[2] else None, + }) + faces.append({"vertices": vertices, "material": current_material, "groups": list(current_groups)}) + return { + "materialLibraries": material_libraries, + "objects": objects, + "positions": positions, + "texcoords": texcoords, + "normals": normals, + "faces": faces, + } + + +def parse_mtl(path): + materials = [] + current = None + for raw_line in path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + parts = line.split() + if parts[0] == "newmtl": + current = {"name": " ".join(parts[1:]), "properties": {}} + materials.append(current) + elif current is not None: + values = parts[1:] + current["properties"][parts[0]] = [number(value) for value in values] if all_value_numbers(values) else " ".join(values) + return materials + + +def all_value_numbers(values): + if not values: + return False + try: + for value in values: + float(value) + return True + except ValueError: + return False + + +def main(output_dir, report_path): + output_dir = Path(output_dir).resolve() + report_path = Path(report_path).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + report_path.parent.mkdir(parents=True, exist_ok=True) + obj_path = output_dir / "single-mesh.obj" + create_fixture() + export_obj(obj_path) + mtl_path = obj_path.with_suffix(".mtl") + if not mtl_path.exists(): + raise RuntimeError("Blender OBJ export did not write the MTL sidecar") + semantic = parse_obj(obj_path) + semantic["materials"] = parse_mtl(mtl_path) + report = { + "schemaVersion": 1, + "task": "M12-07A", + "operation": "DESKTOP_OBJ_SINGLE_MESH_FIXTURE", + "runtime": runtime_identity(), + "sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj", + "operator": "wm.obj_export", + "settings": { + "forwardAxis": "NEGATIVE_Z", + "upAxis": "Y", + "globalScale": 1.0, + "exportUV": True, + "exportNormals": True, + "exportMaterials": True, + "exportMaterialGroups": True, + }, + "files": [ + {"name": obj_path.name, "byteLength": obj_path.stat().st_size, "sha256": sha256_file(obj_path)}, + {"name": mtl_path.name, "byteLength": mtl_path.stat().st_size, "sha256": sha256_file(mtl_path)}, + ], + "semantic": semantic, + "nextTask": "M12-07B", + } + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print( + "obj-single-mesh-fixture-generated positions=%s texcoords=%s normals=%s faces=%s materials=%s next=%s" + % ( + len(semantic["positions"]), + len(semantic["texcoords"]), + len(semantic["normals"]), + len(semantic["faces"]), + len(semantic["materials"]), + report["nextTask"], + ) + ) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: + raise SystemExit("usage: blender --background --python generate-obj-single-mesh-fixture.py -- OUTPUT_DIR REPORT") + main(args[0], args[1]) diff --git a/tools/web/generate-ply-capability-fixtures.py b/tools/web/generate-ply-capability-fixtures.py new file mode 100644 index 00000000..84c14509 --- /dev/null +++ b/tools/web/generate-ply-capability-fixtures.py @@ -0,0 +1,72 @@ +"""Generate pinned Blender 5.2 ASCII and binary little-endian PLY fixtures for M12-07G.""" + +import hashlib +import json +import struct +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def text(value): + return value.decode("utf-8") if isinstance(value, bytes) else value + + +def runtime_identity(): + binary = Path(bpy.app.binary_path) + return {"blenderVersion": text(bpy.app.version_string), "versionTuple": list(bpy.app.version), "buildDate": text(bpy.app.build_date), "buildTime": text(bpy.app.build_time), "buildHash": text(bpy.app.build_hash), "buildBranch": text(bpy.app.build_branch), "buildPlatform": text(bpy.app.build_platform), "buildType": text(bpy.app.build_type), "binarySha256": sha256_file(binary)} + + +def create_scene(): + bpy.ops.wm.read_factory_settings(use_empty=True) + mesh = bpy.data.meshes.new("M12 PLY Capability Mesh") + mesh.from_pydata([(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [], [(0, 1, 2), (0, 2, 3)]) + mesh.update() + obj = bpy.data.objects.new("M12 PLY Capability", mesh) + bpy.context.scene.collection.objects.link(obj) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + +def export_ply(path, ascii_format): + result = bpy.ops.wm.ply_export(filepath=str(path), ascii_format=ascii_format, export_selected_objects=True, export_uv=False, export_normals=True, export_colors="NONE", export_triangulated_mesh=True, forward_axis="NEGATIVE_Z", up_axis="Y", global_scale=1.0) + if "FINISHED" not in result: + raise RuntimeError("Blender PLY export did not finish: %s" % (result,)) + + +def parse_header(path): + payload = path.read_bytes() + end = payload.find(b"end_header\n") + if end < 0: + raise RuntimeError("PLY header missing end_header") + header = payload[: end + len(b"end_header\n")].decode("ascii") + lines = header.splitlines() + format_line = next(line for line in lines if line.startswith("format ")) + elements = [{"name": parts[1], "count": int(parts[2])} for line in lines if (parts := line.split()) and parts[0] == "element"] + return {"format": format_line.split()[1], "elements": elements, "headerBytes": len(header.encode("ascii")), "byteLength": len(payload)} + + +def main(output_dir, report_path): + output_dir = Path(output_dir).resolve(); report_path = Path(report_path).resolve(); output_dir.mkdir(parents=True, exist_ok=True); report_path.parent.mkdir(parents=True, exist_ok=True) + create_scene() + ascii_path = output_dir / "capability-ascii.ply"; binary_path = output_dir / "capability-binary-le.ply" + export_ply(ascii_path, True); export_ply(binary_path, False) + files = [{"name": item.name, "byteLength": item.stat().st_size, "sha256": sha256_file(item)} for item in (ascii_path, binary_path)] + report = {"schemaVersion": 1, "task": "M12-07G", "operation": "DESKTOP_PLY_ASCII_BINARY_LE_CAPABILITY", "runtime": runtime_identity(), "sourceAnchor": "blender-5.2.0/source/blender/io/ply", "operator": "wm.ply_export", "settings": {"exportSelectedObjects": True, "exportNormals": True, "exportUV": False, "exportColors": "NONE", "exportTriangulatedMesh": True, "forwardAxis": "NEGATIVE_Z", "upAxis": "Y", "globalScale": 1.0}, "variants": [{"id": "PLY_ASCII", "asciiFormat": True, "file": ascii_path.name, "semantic": parse_header(ascii_path)}, {"id": "PLY_BINARY_LITTLE_ENDIAN", "asciiFormat": False, "file": binary_path.name, "semantic": parse_header(binary_path)}], "files": files, "nextTask": "M12-07H"} + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("ply-capability-fixtures-generated ascii=%s binary=%s next=%s" % (report["variants"][0]["semantic"]["format"], report["variants"][1]["semantic"]["format"], report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: raise SystemExit("usage: blender --background --python generate-ply-capability-fixtures.py -- OUTPUT_DIR REPORT") + main(args[0], args[1]) diff --git a/tools/web/generate-ply-mapping-fixtures.py b/tools/web/generate-ply-mapping-fixtures.py new file mode 100644 index 00000000..53606f02 --- /dev/null +++ b/tools/web/generate-ply-mapping-fixtures.py @@ -0,0 +1,150 @@ +"""Generate pinned Blender 5.2 PLY mapping and unknown-property fixtures for M12-07H.""" + +import hashlib +import json +import struct +import sys +from pathlib import Path + +import bpy + + +SCALAR = {"char": "b", "uchar": "B", "short": "h", "ushort": "H", "int": "i", "uint": "I", "float": "f", "double": "d"} + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def text(value): + return value.decode("utf-8") if isinstance(value, bytes) else value + + +def runtime_identity(): + binary = Path(bpy.app.binary_path) + return {"blenderVersion": text(bpy.app.version_string), "versionTuple": list(bpy.app.version), "buildDate": text(bpy.app.build_date), "buildTime": text(bpy.app.build_time), "buildHash": text(bpy.app.build_hash), "buildBranch": text(bpy.app.build_branch), "buildPlatform": text(bpy.app.build_platform), "buildType": text(bpy.app.build_type), "binarySha256": sha256_file(binary)} + + +def create_scene(): + bpy.ops.wm.read_factory_settings(use_empty=True) + mesh = bpy.data.meshes.new("M12 PLY Mapping Mesh") + mesh.from_pydata([(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [], [(0, 1, 2), (0, 2, 3)]) + mesh.update() + colors = mesh.color_attributes.new(name="Col", type="FLOAT_COLOR", domain="POINT") + for value, color in zip(colors.data, ((1.0, 0.0, 0.0, 1.0), (0.0, 1.0, 0.0, 1.0), (0.0, 0.0, 1.0, 1.0), (1.0, 1.0, 0.0, 1.0))): + value.color = color + temperature = mesh.attributes.new(name="temperature", type="FLOAT", domain="POINT") + label = mesh.attributes.new(name="label", type="INT", domain="POINT") + for index, (temp, tag) in enumerate(zip((10.0, 20.0, 30.0, 40.0), (1, 2, 3, 4))): + temperature.data[index].value = temp + label.data[index].value = tag + obj = bpy.data.objects.new("M12 PLY Mapping", mesh) + bpy.context.scene.collection.objects.link(obj) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + +def export_ply(path, ascii_format): + result = bpy.ops.wm.ply_export(filepath=str(path), ascii_format=ascii_format, export_selected_objects=True, export_uv=False, export_normals=True, export_colors="SRGB", export_attributes=True, export_triangulated_mesh=True, forward_axis="NEGATIVE_Z", up_axis="Y", global_scale=1.0) + if "FINISHED" not in result: + raise RuntimeError("Blender PLY export did not finish: %s" % (result,)) + + +def parse_header(payload): + marker = b"end_header\n" + end = payload.find(marker) + if end < 0: + raise RuntimeError("PLY header missing end_header") + header = payload[: end + len(marker)].decode("ascii") + elements = [] + current = None + for line in header.splitlines(): + parts = line.split() + if not parts: + continue + if parts[0] == "format": + fmt = parts[1] + elif parts[0] == "element": + current = {"name": parts[1], "count": int(parts[2]), "properties": []} + elements.append(current) + elif parts[0] == "property" and current is not None: + if parts[1] == "list": current["properties"].append({"kind": "list", "countType": parts[2], "valueType": parts[3], "name": parts[4]}) + else: current["properties"].append({"kind": "scalar", "type": parts[1], "name": parts[2]}) + return fmt, elements, end + len(marker) + + +def read_value(data, cursor, value_type): + code = SCALAR[value_type] + size = struct.calcsize("<" + code) + value = struct.unpack_from("<" + code, data, cursor)[0] + return value, cursor + size + + +def parse_semantic(path): + payload = path.read_bytes() + fmt, elements, offset = parse_header(payload) + records = {} + if fmt == "ascii": + lines = payload[offset:].decode("utf-8").splitlines() + cursor = 0 + for element in elements: + rows = [] + for _ in range(element["count"]): + tokens = lines[cursor].split(); cursor += 1; token_index = 0; row = {} + for prop in element["properties"]: + if prop["kind"] == "scalar": row[prop["name"]] = float(tokens[token_index]) if prop["type"] in ("float", "double") else int(tokens[token_index]); token_index += 1 + else: + length = int(tokens[token_index]); token_index += 1; row[prop["name"]] = [int(tokens[token_index + i]) for i in range(length)]; token_index += length + rows.append(row) + records[element["name"]] = rows + else: + cursor = offset + for element in elements: + rows = [] + for _ in range(element["count"]): + row = {} + for prop in element["properties"]: + if prop["kind"] == "scalar": row[prop["name"]], cursor = read_value(payload, cursor, prop["type"]) + else: + length, cursor = read_value(payload, cursor, prop["countType"]); row[prop["name"]] = [] + for _ in range(length): value, cursor = read_value(payload, cursor, prop["valueType"]); row[prop["name"]].append(value) + rows.append(row) + records[element["name"]] = rows + vertices = [] + for row in records["vertex"]: + vertices.append({"position": [row["x"], row["y"], row["z"]], "normal": [row["nx"], row["ny"], row["nz"]], "color": [row["red"], row["green"], row["blue"], row["alpha"]], "customProperties": {name: row[name] for name in row if name not in {"x", "y", "z", "nx", "ny", "nz", "red", "green", "blue", "alpha"}}}) + faces = [{"indices": row["vertex_indices"], "customProperties": {name: row[name] for name in row if name != "vertex_indices"}} for row in records.get("face", [])] + return {"format": fmt, "vertexCount": len(vertices), "faceCount": len(faces), "vertices": vertices, "faces": faces} + + +def create_unknown_ascii(source, target): + lines = source.read_text(encoding="utf-8").splitlines() + header_end = lines.index("end_header") + property_index = next(index for index, line in enumerate(lines[:header_end]) if line == "property float label") + lines.insert(property_index + 1, "property list uchar float unknown_values") + header_end += 1 + vertex_count = next(int(line.split()[2]) for line in lines[:header_end + 1] if line.startswith("element vertex ")) + for index in range(header_end + 1, header_end + 1 + vertex_count): + lines[index] += " 1 0.5" + target.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main(output_dir, report_path): + output_dir = Path(output_dir).resolve(); report_path = Path(report_path).resolve(); output_dir.mkdir(parents=True, exist_ok=True); report_path.parent.mkdir(parents=True, exist_ok=True) + create_scene() + ascii_path = output_dir / "mapping-ascii.ply"; binary_path = output_dir / "mapping-binary-le.ply"; unknown_path = output_dir / "unknown-property-ascii.ply" + export_ply(ascii_path, True); export_ply(binary_path, False); create_unknown_ascii(ascii_path, unknown_path) + files = [{"name": item.name, "byteLength": item.stat().st_size, "sha256": sha256_file(item)} for item in (ascii_path, binary_path, unknown_path)] + report = {"schemaVersion": 1, "task": "M12-07H", "operation": "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", "runtime": runtime_identity(), "sourceAnchor": "blender-5.2.0/source/blender/io/ply", "operator": "wm.ply_export", "settings": {"exportSelectedObjects": True, "exportNormals": True, "exportUV": False, "exportColors": "SRGB", "exportAttributes": True, "exportTriangulatedMesh": True, "forwardAxis": "NEGATIVE_Z", "upAxis": "Y", "globalScale": 1.0}, "variants": [{"id": "PLY_ASCII_MAPPING", "file": ascii_path.name, "semantic": parse_semantic(ascii_path)}, {"id": "PLY_BINARY_LITTLE_ENDIAN_MAPPING", "file": binary_path.name, "semantic": parse_semantic(binary_path)}], "unknownProperty": {"file": unknown_path.name, "property": "unknown_values", "expectedCode": "PLY_UNKNOWN_PROPERTY"}, "files": files, "nextTask": "M12-07I"} + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("ply-mapping-fixtures-generated vertices=%s faces=%s colors=rgba custom=2 unknown=PLY_UNKNOWN_PROPERTY next=%s" % (report["variants"][0]["semantic"]["vertexCount"], report["variants"][0]["semantic"]["faceCount"], report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: raise SystemExit("usage: blender --background --python generate-ply-mapping-fixtures.py -- OUTPUT_DIR REPORT") + main(args[0], args[1]) diff --git a/tools/web/generate-ply-negative-fixtures.mjs b/tools/web/generate-ply-negative-fixtures.mjs new file mode 100644 index 00000000..ff77c4ee --- /dev/null +++ b/tools/web/generate-ply-negative-fixtures.mjs @@ -0,0 +1,51 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +const root = path.resolve(import.meta.dirname, "../.."); +const fixtureRoot = path.resolve(process.env.M12_PLY_NEGATIVE_OUTPUT ?? path.join(root, "tests/files/web/m12_ply_negative_v1")); +const reportPath = path.resolve(process.env.M12_PLY_NEGATIVE_REPORT ?? path.join(root, "tests/golden/M12-07I/negative-report.json")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); + +function write(name, content) { + const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content, "utf8"); + fs.writeFileSync(path.join(fixtureRoot, name), bytes); + return { name, byteLength: bytes.byteLength, sha256: sha256(bytes) }; +} + +function bigEndian() { + const header = "ply\nformat binary_big_endian 1.0\nelement vertex 1\nproperty float x\nproperty float y\nproperty float z\nelement face 0\nproperty list uchar uint vertex_indices\nend_header\n"; + const data = Buffer.alloc(12); + data.writeFloatBE(0, 0); data.writeFloatBE(0, 4); data.writeFloatBE(0, 8); + return Buffer.concat([Buffer.from(header, "ascii"), data]); +} + +function malformedList() { + return "ply\nformat ascii 1.0\nelement vertex 3\nproperty float x\nproperty float y\nproperty float z\nelement face 1\nproperty list uchar uint vertex_indices\nend_header\n0 0 0\n1 0 0\n0 0 1\n3 0 1\n"; +} + +function oversizedCount() { + return "ply\nformat ascii 1.0\nelement vertex 65537\nproperty float x\nproperty float y\nproperty float z\nelement face 0\nproperty list uchar uint vertex_indices\nend_header\n"; +} + +fs.mkdirSync(fixtureRoot, { recursive: true }); +const files = [ + write("big-endian.ply", bigEndian()), + write("malformed-list-ascii.ply", malformedList()), + write("oversized-count-ascii.ply", oversizedCount()), +]; +const report = { + schemaVersion: 1, + task: "M12-07I", + operation: "PLY_NEGATIVE_FORMAT_LIST_COUNT", + cases: [ + { id: "big-endian", file: "big-endian.ply", expectedCode: "PLY_FORMAT_UNSUPPORTED" }, + { id: "malformed-list", file: "malformed-list-ascii.ply", expectedCode: "PLY_DATA_TRUNCATED" }, + { id: "oversized-count", file: "oversized-count-ascii.ply", expectedCode: "PLY_IMPORT_BUDGET_EXCEEDED: vertex" }, + ], + files, + nextTask: "M12-07J", +}; +fs.mkdirSync(path.dirname(reportPath), { recursive: true }); +fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); +process.stdout.write(`ply-negative-fixtures-generated cases=${report.cases.length} next=${report.nextTask}\n`); diff --git a/tools/web/generate-script-entry-inventory.py b/tools/web/generate-script-entry-inventory.py new file mode 100644 index 00000000..d014f104 --- /dev/null +++ b/tools/web/generate-script-entry-inventory.py @@ -0,0 +1,66 @@ +"""Inventory Blender 5.2 scripting entry points without executing user script text.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def text(value): + return value.decode("utf-8") if isinstance(value, bytes) else value + + +def runtime_identity(): + binary = Path(bpy.app.binary_path) + return {"blenderVersion": text(bpy.app.version_string), "versionTuple": list(bpy.app.version), "buildDate": text(bpy.app.build_date), "buildTime": text(bpy.app.build_time), "buildHash": text(bpy.app.build_hash), "buildBranch": text(bpy.app.build_branch), "buildPlatform": text(bpy.app.build_platform), "buildType": text(bpy.app.build_type), "binarySha256": sha256_file(binary)} + + +def create_fixture(output_path): + bpy.ops.wm.read_factory_settings(use_empty=True) + texts = [("InternalSafe.py", "value = 7\nprint(value)\n", False, ""), ("ModuleAutorun.py", "def register():\n return 'blocked'\n", True, ""), ("ExternalProject.py", "message = 'project relative'\n", False, "//scripts/external_project.py")] + for name, source, use_module, filepath in texts: + value = bpy.data.texts.new(name); value.write(source); value.use_module = use_module; value.filepath = filepath + mesh = bpy.data.meshes.new("M13 Script Inventory Mesh"); mesh.from_pydata([(0, 0, 0), (1, 0, 0), (0, 1, 0)], [], [(0, 1, 2)]); mesh.update() + obj = bpy.data.objects.new("M13 Script Inventory Object", mesh); bpy.context.scene.collection.objects.link(obj) + obj["driver_source"] = 1.0 + driver = obj.driver_add('location', 0).driver; driver.expression = "var * 2"; variable = driver.variables.new(); variable.name = "var"; variable.type = "SINGLE_PROP"; variable.targets[0].id = obj; variable.targets[0].data_path = '[\"driver_source\"]' + output_path.parent.mkdir(parents=True, exist_ok=True); bpy.ops.wm.save_as_mainfile(filepath=str(output_path), compress=False) + + +def entry_inventory(): + text_entries = [] + for value in sorted(bpy.data.texts, key=lambda item: item.name): + source = value.as_string(); text_entries.append({"name": value.name, "byteLength": len(source.encode("utf-8")), "lineCount": len(source.split("\n")), "useModule": bool(value.use_module), "filepath": value.filepath, "internal": not bool(value.filepath)}) + console_ops = [name for name in ("execute", "history_append", "scrollback_append") if hasattr(bpy.ops.console, name)] + driver_entries = [] + for obj in sorted(bpy.data.objects, key=lambda item: item.name): + if not obj.animation_data: continue + for item in obj.animation_data.drivers: + driver_entries.append({"object": obj.name, "dataPath": item.data_path, "arrayIndex": item.array_index, "expression": item.driver.expression, "variableCount": len(item.driver.variables)}) + handler_groups = sorted(name for name in dir(bpy.app.handlers) if not name.startswith("_") and isinstance(getattr(bpy.app.handlers, name), list)) + addon_ops = [name for name in ("addon_install", "addon_enable", "addon_disable", "addon_remove") if hasattr(bpy.ops.preferences, name)] + return {"text": {"count": len(text_entries), "entries": text_entries}, "pythonConsole": {"operatorIds": [f"console.{name}" for name in console_ops], "available": len(console_ops) == 3}, "autorun": {"moduleTextNames": [item["name"] for item in text_entries if item["useModule"]], "defaultExecution": "DENY"}, "driverExpressions": {"count": len(driver_entries), "entries": driver_entries, "defaultExecution": "DENY"}, "handlers": {"groups": handler_groups, "defaultExecution": "DENY"}, "addons": {"operatorIds": [f"preferences.{name}" for name in addon_ops], "enabledAddons": sorted(addon.module for addon in bpy.context.preferences.addons), "defaultExecution": "DENY"}} + + +def main(output_dir, report_path): + output_dir = Path(output_dir).resolve(); report_path = Path(report_path).resolve(); output_dir.mkdir(parents=True, exist_ok=True); report_path.parent.mkdir(parents=True, exist_ok=True) + fixture_path = output_dir / "script-entry-inventory.blend"; create_fixture(fixture_path) + report = {"schemaVersion": 1, "task": "M13-01A", "operation": "BLENDER_SCRIPT_ENTRY_INVENTORY", "runtime": runtime_identity(), "sourceAnchor": "blender-5.2.0/source/blender/python", "executionPolicy": {"text": "READ_METADATA_ONLY", "pythonConsole": "DENY", "autorun": "DENY", "driverExpression": "DENY", "handler": "DENY", "addon": "DENY"}, "inventory": entry_inventory(), "fixture": {"name": fixture_path.name, "byteLength": fixture_path.stat().st_size, "sha256": sha256_file(fixture_path)}, "nextTask": "M13-01B"} + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("script-entry-inventory-generated texts=%s drivers=%s consoleOps=%s addonOps=%s next=%s" % (report["inventory"]["text"]["count"], report["inventory"]["driverExpressions"]["count"], len(report["inventory"]["pythonConsole"]["operatorIds"]), len(report["inventory"]["addons"]["operatorIds"]), report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: raise SystemExit("usage: blender --background --python generate-script-entry-inventory.py -- OUTPUT_DIR REPORT") + main(args[0], args[1]) diff --git a/tools/web/generate-stl-capability-fixtures.py b/tools/web/generate-stl-capability-fixtures.py new file mode 100644 index 00000000..b2a2e16b --- /dev/null +++ b/tools/web/generate-stl-capability-fixtures.py @@ -0,0 +1,135 @@ +"""Generate pinned Blender 5.2 binary and ASCII STL capability fixtures for M12-07D.""" + +import hashlib +import json +import struct +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def text(value): + return value.decode("utf-8") if isinstance(value, bytes) else value + + +def runtime_identity(): + binary = Path(bpy.app.binary_path) + return { + "blenderVersion": text(bpy.app.version_string), + "versionTuple": list(bpy.app.version), + "buildDate": text(bpy.app.build_date), + "buildTime": text(bpy.app.build_time), + "buildHash": text(bpy.app.build_hash), + "buildBranch": text(bpy.app.build_branch), + "buildPlatform": text(bpy.app.build_platform), + "buildType": text(bpy.app.build_type), + "binarySha256": sha256_file(binary), + } + + +def create_scene(): + bpy.ops.wm.read_factory_settings(use_empty=True) + mesh = bpy.data.meshes.new("M12 STL Capability Mesh") + mesh.from_pydata( + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [], + [(0, 1, 2), (0, 2, 3)], + ) + mesh.update() + obj = bpy.data.objects.new("M12 STL Capability", mesh) + bpy.context.scene.collection.objects.link(obj) + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + return obj + + +def export_stl(path, ascii_format): + result = bpy.ops.wm.stl_export( + filepath=str(path), + ascii_format=ascii_format, + export_selected_objects=True, + apply_modifiers=False, + evaluation_mode="DAG_EVAL_VIEWPORT", + global_scale=1.0, + forward_axis="NEGATIVE_Z", + up_axis="Y", + use_scene_unit=False, + ) + if "FINISHED" not in result: + raise RuntimeError("Blender STL export did not finish: %s" % (result,)) + + +def binary_semantics(path): + payload = path.read_bytes() + if len(payload) < 84: + raise RuntimeError("STL binary is shorter than the header") + count = struct.unpack_from(" crypto.createHash("sha256").update(bytes).digest("hex"); + +fs.mkdirSync(outputRoot, { recursive: true }); +fs.mkdirSync(path.dirname(reportPath), { recursive: true }); +const source = fs.readFileSync(sourcePath); +if (source.length !== 184 || source.readUInt32LE(80) !== 2) throw new Error("M12-07D binary parent fixture drifted"); +fs.writeFileSync(path.join(outputRoot, "capability-binary.stl"), source); +const degenerate = Buffer.from(source); +const firstVertex = Buffer.from(degenerate.subarray(84 + 12, 84 + 24)); +firstVertex.copy(degenerate, 84 + 24); +firstVertex.copy(degenerate, 84 + 36); +const trailing = Buffer.concat([source, Buffer.from([0xde, 0xad, 0xbe, 0xef])]); +const outputs = [ + { id: "DEGENERATE_TRIANGLE", name: "degenerate-binary.stl", bytes: degenerate, expectedCode: "STL_DEGENERATE_TRIANGLE" }, + { id: "TRAILING_BYTES", name: "trailing-binary.stl", bytes: trailing, expectedCode: "STL_TRAILING_BYTES" }, +]; +for (const output of outputs) fs.writeFileSync(path.join(outputRoot, output.name), output.bytes); +const report = { + schemaVersion: 1, + task: "M12-07E", + operation: "STL_EDGE_FIXTURE_GENERATION", + parent: { path: "tests/files/web/m12_stl_capability_v1/capability-binary.stl", byteLength: source.length, sha256: sha256(source) }, + fixtures: outputs.map((output) => ({ id: output.id, file: output.name, byteLength: output.bytes.length, sha256: sha256(output.bytes), expectedCode: output.expectedCode })), + nextTask: "M12-07F", +}; +fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); +process.stdout.write(`stl-edge-fixtures-generated cases=${outputs.length} next=${report.nextTask}\n`); diff --git a/tools/web/probe-stl-edge-fixtures.py b/tools/web/probe-stl-edge-fixtures.py new file mode 100644 index 00000000..576837e8 --- /dev/null +++ b/tools/web/probe-stl-edge-fixtures.py @@ -0,0 +1,74 @@ +"""Probe STL normal/unit/edge behavior in pinned Blender 5.2 for M12-07E.""" + +import hashlib +import json +import sys +from pathlib import Path + +import bpy + + +def sha256_file(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def import_case(path, global_scale): + bpy.ops.wm.read_factory_settings(use_empty=True) + result = bpy.ops.wm.stl_import( + filepath=str(path), + directory=str(path.parent), + forward_axis="NEGATIVE_Z", + up_axis="Y", + global_scale=global_scale, + use_scene_unit=False, + use_facet_normal=True, + use_mesh_validate=True, + ) + objects = [obj for obj in bpy.context.scene.objects if obj.type == "MESH"] + if "FINISHED" not in result or not objects: + return {"status": "BLOCKED", "objectCount": len(objects)} + mesh = objects[0].data + mesh.calc_loop_triangles() + coordinates = [[float(value) for value in (objects[0].matrix_world @ vertex.co)] for vertex in mesh.vertices] + bounds = { + "min": [min(values) for values in zip(*coordinates)], + "max": [max(values) for values in zip(*coordinates)], + } if coordinates else {"min": [0.0, 0.0, 0.0], "max": [0.0, 0.0, 0.0]} + return { + "status": "ACCEPTED", + "objectCount": len(objects), + "vertexCount": len(mesh.vertices), + "polygonCount": len(mesh.polygons), + "triangleCount": len(mesh.loop_triangles), + "bounds": bounds, + "polygonNormals": [[float(value) for value in polygon.normal] for polygon in mesh.polygons], + } + + +def main(fixture_root, report_path): + fixture_root = Path(fixture_root).resolve() + report_path = Path(report_path).resolve() + cases = [] + for case_id, name, scale in ( + ("BINARY_UNIT_1", "capability-binary.stl", 1.0), + ("BINARY_UNIT_001", "capability-binary.stl", 0.001), + ("DEGENERATE_TRIANGLE", "degenerate-binary.stl", 1.0), + ("TRAILING_BYTES", "trailing-binary.stl", 1.0), + ): + path = fixture_root / name + cases.append({"id": case_id, "file": name, "globalScale": scale, "byteLength": path.stat().st_size, "sha256": sha256_file(path), "result": import_case(path, scale)}) + report = {"schemaVersion": 1, "task": "M12-07E", "operation": "BLENDER_STL_EDGE_PROBE", "cases": cases, "nextTask": "M12-07F"} + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print("stl-edge-probe-complete cases=%s next=%s" % (len(cases), report["nextTask"])) + + +if __name__ == "__main__": + args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else [] + if len(args) != 2: + raise SystemExit("usage: blender --background --python probe-stl-edge-fixtures.py -- FIXTURE_ROOT REPORT") + main(args[0], args[1]) diff --git a/tools/web/server-job-fault.mjs b/tools/web/server-job-fault.mjs new file mode 100644 index 00000000..899c0411 --- /dev/null +++ b/tools/web/server-job-fault.mjs @@ -0,0 +1,70 @@ +export const SERVER_JOB_FAULT_SCHEMA = 1; +export const SERVER_JOB_FAULT_CODES = Object.freeze({ + SUCCEEDED: "SERVER_JOB_SUCCEEDED", + CANCELLED: "SERVER_JOB_CANCELLED", + TIMEOUT: "SERVER_JOB_TIMEOUT", + OOM: "SERVER_JOB_OOM", + SIGNAL: "SERVER_JOB_SIGNAL", + EXIT_FAILED: "SERVER_JOB_EXIT_FAILED", +}); + +function invalid(message) { + throw new Error(`SERVER_JOB_FAULT_INVALID: ${message}`); +} + +function revision(value, field) { + if (!Number.isSafeInteger(value) || value < 0) invalid(`${field} must be a non-negative revision`); + return value; +} + +function optionalBoolean(value, field) { + if (value !== undefined && typeof value !== "boolean") invalid(`${field} must be boolean`); + return value === true; +} + +export function classifyServerJobFault(value) { + if (!value || typeof value !== "object") invalid("input is invalid"); + const cancelRequested = optionalBoolean(value.cancelRequested, "cancelRequested"); + const timedOut = optionalBoolean(value.timedOut, "timedOut"); + const oom = optionalBoolean(value.oom, "oom"); + const memoryLimitBytes = value.memoryLimitBytes === undefined ? null : revision(value.memoryLimitBytes, "memoryLimitBytes"); + const memoryBytes = value.memoryBytes === undefined ? null : revision(value.memoryBytes, "memoryBytes"); + const memoryExceeded = oom || (memoryLimitBytes !== null && memoryBytes !== null && memoryBytes > memoryLimitBytes); + const signal = value.signal === null || value.signal === undefined ? null : String(value.signal); + const exitCode = value.code === null || value.code === undefined ? null : value.code; + if (exitCode !== null && (!Number.isInteger(exitCode) || exitCode < 0)) invalid("code must be a non-negative integer"); + let code = SERVER_JOB_FAULT_CODES.SUCCEEDED; + let state = "SUCCEEDED"; + if (cancelRequested || value.status === "CANCELLED") { code = SERVER_JOB_FAULT_CODES.CANCELLED; state = "CANCELLED"; } + else if (timedOut) { code = SERVER_JOB_FAULT_CODES.TIMEOUT; state = "FAILED"; } + else if (memoryExceeded) { code = SERVER_JOB_FAULT_CODES.OOM; state = "FAILED"; } + else if (signal) { code = SERVER_JOB_FAULT_CODES.SIGNAL; state = "FAILED"; } + else if (exitCode !== null && exitCode !== 0) { code = SERVER_JOB_FAULT_CODES.EXIT_FAILED; state = "FAILED"; } + return Object.freeze({ schemaVersion: SERVER_JOB_FAULT_SCHEMA, state, code, exitCode, signal, timedOut, memoryExceeded, publish: state === "SUCCEEDED", revisionPreserved: state !== "SUCCEEDED" }); +} + +export function createServerJobFaultReceipt(value) { + if (!value || typeof value !== "object") invalid("receipt input is invalid"); + const baseRevision = revision(value.baseRevision, "baseRevision"); + const currentRevision = revision(value.currentRevision, "currentRevision"); + const classification = classifyServerJobFault(value); + if (classification.state !== "SUCCEEDED" && currentRevision !== baseRevision) { + throw new Error("SERVER_JOB_REVISION_CONFLICT: failed job cannot replace a newer project revision"); + } + return Object.freeze({ + schemaVersion: SERVER_JOB_FAULT_SCHEMA, + state: classification.state, + code: classification.code, + stage: "PROCESS", + exitCode: classification.exitCode, + signal: classification.signal, + timedOut: classification.timedOut, + memoryExceeded: classification.memoryExceeded, + baseRevision, + currentRevision, + committedRevision: currentRevision, + publish: classification.publish, + revisionPreserved: classification.revisionPreserved, + execution: "DISABLED", + }); +} diff --git a/tools/web/server-job-idempotency.mjs b/tools/web/server-job-idempotency.mjs new file mode 100644 index 00000000..e88d9037 --- /dev/null +++ b/tools/web/server-job-idempotency.mjs @@ -0,0 +1,57 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { commitServerJobResult, createServerJobResultIdentity, verifyServerJobResultReceipt } from "./server-job-result-binding.mjs"; + +export const SERVER_JOB_IDEMPOTENCY_SCHEMA = 1; +const inFlight = new Map(); + +function invalid(message) { throw new Error(`SERVER_JOB_IDEMPOTENCY_INVALID: ${message}`); } +function outputHash(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); } +function keyFor(identity) { return `${identity.projectId}:${identity.requestId}`; } +function receiptPath(directory, identity) { return path.join(directory, `${identity.requestId}.receipt.json`); } + +async function readReceipt(file) { + try { return JSON.parse(await fs.readFile(file, "utf8")); } + catch (error) { if (error?.code === "ENOENT") return null; throw error; } +} + +async function writeReceipt(file, receipt) { + const temporary = `${file}.${crypto.randomUUID()}.tmp`; + await fs.writeFile(temporary, `${JSON.stringify(receipt, null, 2)}\n`, { flag: "wx", mode: 0o600 }); + await fs.rename(temporary, file); +} + +async function submit(identityValue, outputBytes, options) { + const identity = createServerJobResultIdentity(identityValue); + if (!(outputBytes instanceof Uint8Array) || outputBytes.byteLength < 1) invalid("output bytes are empty"); + if (typeof options?.receiptDirectory !== "string" || !path.isAbsolute(options.receiptDirectory)) invalid("receipt directory is invalid"); + if (typeof options?.outputDirectory !== "string" || !path.isAbsolute(options.outputDirectory)) invalid("output directory is invalid"); + await fs.mkdir(options.receiptDirectory, { recursive: true, mode: 0o700 }); + await fs.mkdir(options.outputDirectory, { recursive: true, mode: 0o700 }); + const file = receiptPath(options.receiptDirectory, identity); + const expectedOutputSha256 = outputHash(outputBytes); + const existing = await readReceipt(file); + if (existing) { + if (existing.schemaVersion !== SERVER_JOB_IDEMPOTENCY_SCHEMA || existing.outputSha256 !== expectedOutputSha256 || existing.requestId !== identity.requestId || existing.projectId !== identity.projectId || existing.sourceSha256 !== identity.sourceSha256 || existing.settingsSha256 !== identity.settingsSha256 || existing.buildSha256 !== identity.buildSha256 || existing.baseRevision !== identity.baseRevision) { + throw new Error("SERVER_JOB_IDEMPOTENCY_CONFLICT: request is already bound to a different result"); + } + await verifyServerJobResultReceipt(existing.result, identity, options.outputDirectory); + return Object.freeze({ ...existing.result, reused: true, idempotencyKey: keyFor(identity), execution: "DISABLED" }); + } + const result = await commitServerJobResult(identity, outputBytes, { outputDirectory: options.outputDirectory, expectedIdentity: identity }); + const stored = Object.freeze({ schemaVersion: SERVER_JOB_IDEMPOTENCY_SCHEMA, requestId: identity.requestId, projectId: identity.projectId, baseRevision: identity.baseRevision, sourceSha256: identity.sourceSha256, settingsSha256: identity.settingsSha256, buildSha256: identity.buildSha256, outputSha256: result.outputSha256, result }); + await writeReceipt(file, stored); + return Object.freeze({ ...result, reused: false, idempotencyKey: keyFor(identity), execution: "DISABLED" }); +} + +export async function submitIdempotentServerJobResult(identityValue, outputBytes, options) { + const identity = createServerJobResultIdentity(identityValue); + const key = `${options?.receiptDirectory ?? ""}:${keyFor(identity)}`; + const running = inFlight.get(key); + if (running) return Object.freeze({ ...(await running), reused: true }); + const current = Promise.resolve().then(() => submit(identity, outputBytes, options)); + inFlight.set(key, current); + try { return await current; } + finally { if (inFlight.get(key) === current) inFlight.delete(key); } +} diff --git a/tools/web/server-job-isolation.mjs b/tools/web/server-job-isolation.mjs new file mode 100644 index 00000000..64f97777 --- /dev/null +++ b/tools/web/server-job-isolation.mjs @@ -0,0 +1,53 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +export const SERVER_JOB_DIRECTORY_SCHEMA = 1; +const JOB_ID = /^[-A-Za-z0-9:_./]{1,256}$/; + +function validateJobId(jobId) { + if (typeof jobId !== "string" || !JOB_ID.test(jobId) || jobId.includes("..")) throw new Error("SERVER_JOB_DIRECTORY_INVALID: jobId is invalid"); +} + +function validateRoot(root) { + if (typeof root !== "string" || !path.isAbsolute(root)) throw new Error("SERVER_JOB_DIRECTORY_INVALID: root must be absolute"); + return path.resolve(root); +} + +export async function createServerJobDirectory(root, jobId) { + const resolvedRoot = validateRoot(root); + validateJobId(jobId); + await fs.mkdir(resolvedRoot, { recursive: true, mode: 0o700 }); + const randomPrefix = `.blender-job-${crypto.randomUUID()}-`; + const directory = await fs.mkdtemp(path.join(resolvedRoot, randomPrefix), { encoding: "utf8" }); + await fs.chmod(directory, 0o700); + return Object.freeze({ schemaVersion: SERVER_JOB_DIRECTORY_SCHEMA, jobId, root: resolvedRoot, path: directory, directoryName: path.basename(directory), state: "ALLOCATED", cleanupCount: 0 }); +} + +export async function cleanupServerJobDirectory(job) { + if (!job || job.schemaVersion !== SERVER_JOB_DIRECTORY_SCHEMA || typeof job.path !== "string" || typeof job.root !== "string") throw new Error("SERVER_JOB_DIRECTORY_INVALID: receipt is invalid"); + if (job.state === "CLEANED") return job; + const root = validateRoot(job.root); + const directory = path.resolve(job.path); + if (path.dirname(directory) !== root || !path.basename(directory).startsWith(".blender-job-")) throw new Error("SERVER_JOB_DIRECTORY_INVALID: directory escaped the job root"); + await fs.chmod(directory, 0o700); + try { await fs.chmod(path.join(directory, "source"), 0o700); } catch (error) { if (error?.code !== "ENOENT") throw error; } + await fs.rm(directory, { recursive: true, force: false }); + return Object.freeze({ ...job, state: "CLEANED", cleanupCount: 1 }); +} + +export async function prepareServerJobWorkspace(job, sourceBytes, sourceName = "source.blend") { + if (!job || job.schemaVersion !== SERVER_JOB_DIRECTORY_SCHEMA || job.state !== "ALLOCATED") throw new Error("SERVER_JOB_WORKSPACE_INVALID: job receipt is not allocated"); + if (!(sourceBytes instanceof Uint8Array) || sourceBytes.byteLength < 1) throw new Error("SERVER_JOB_WORKSPACE_INVALID: source bytes are empty"); + if (typeof sourceName !== "string" || !/^[A-Za-z0-9_.-]{1,128}$/.test(sourceName) || sourceName.includes("..")) throw new Error("SERVER_JOB_WORKSPACE_INVALID: source name is invalid"); + const sourceDirectory = path.join(job.path, "source"); + const outputDirectory = path.join(job.path, "output"); + await fs.mkdir(sourceDirectory, { mode: 0o700 }); + await fs.mkdir(outputDirectory, { mode: 0o700 }); + const sourcePath = path.join(sourceDirectory, sourceName); + await fs.writeFile(sourcePath, sourceBytes, { mode: 0o444, flag: "wx" }); + await fs.chmod(sourceDirectory, 0o555); + await fs.chmod(sourcePath, 0o444); + await fs.chmod(outputDirectory, 0o700); + return Object.freeze({ schemaVersion: SERVER_JOB_DIRECTORY_SCHEMA, sourceDirectory, sourcePath, outputDirectory, sourceMode: "0444", sourceDirectoryMode: "0555", outputDirectoryMode: "0700", sourceReadOnly: true, outputWritable: true }); +} diff --git a/tools/web/server-job-network-policy.mjs b/tools/web/server-job-network-policy.mjs new file mode 100644 index 00000000..178fe452 --- /dev/null +++ b/tools/web/server-job-network-policy.mjs @@ -0,0 +1,25 @@ +export const SERVER_JOB_NETWORK_POLICY_SCHEMA = 1; + +function normalizeOrigin(value) { + if (typeof value !== "string" || value.length > 2048) throw new Error("SERVER_NETWORK_POLICY_INVALID: origin is invalid"); + let url; + try { url = new URL(value); } catch { throw new Error("SERVER_NETWORK_POLICY_INVALID: origin is invalid"); } + if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("SERVER_NETWORK_POLICY_INVALID: origin is not canonical"); + if (url.protocol !== "https:" && !(url.protocol === "http:" && ["127.0.0.1", "localhost", "::1"].includes(url.hostname))) throw new Error("SERVER_NETWORK_POLICY_DENIED: origin must be HTTPS or loopback"); + return url.origin; +} + +export function parseServerJobNetworkPolicy(value) { + if (!value || typeof value !== "object" || value.schemaVersion !== SERVER_JOB_NETWORK_POLICY_SCHEMA || !Array.isArray(value.allowedOrigins)) throw new Error("SERVER_NETWORK_POLICY_INVALID: schema is unsupported"); + const origins = [...new Set(value.allowedOrigins.map(normalizeOrigin))].sort(); + if (origins.length > 64) throw new Error("SERVER_NETWORK_POLICY_INVALID: origin count exceeds budget"); + return Object.freeze({ schemaVersion: SERVER_JOB_NETWORK_POLICY_SCHEMA, defaultNetwork: "DENY", allowedOrigins: origins }); +} + +export function resolveServerJobNetwork(policyValue, requestedOrigin) { + const policy = parseServerJobNetworkPolicy(policyValue); + if (requestedOrigin === undefined || requestedOrigin === null) return Object.freeze({ status: "DENIED", code: "SERVER_NETWORK_DENIED", origin: null, network: "DISABLED" }); + const origin = normalizeOrigin(requestedOrigin); + if (!policy.allowedOrigins.includes(origin)) return Object.freeze({ status: "DENIED", code: "SERVER_NETWORK_DENIED", origin, network: "DISABLED" }); + return Object.freeze({ status: "ALLOWED", code: "SERVER_NETWORK_ALLOWED_ORIGIN", origin, network: "DECLARED_ORIGIN" }); +} diff --git a/tools/web/server-job-output.mjs b/tools/web/server-job-output.mjs new file mode 100644 index 00000000..b86edd08 --- /dev/null +++ b/tools/web/server-job-output.mjs @@ -0,0 +1,119 @@ +export const SERVER_JOB_OUTPUT_SCHEMA = 1; +export const SERVER_JOB_OUTPUT_LIMITS = Object.freeze({ + stdoutBytes: 64 * 1024, + stderrBytes: 64 * 1024, + totalBytes: 128 * 1024, +}); + +const REDACTION = ""; +const PATH_REDACTION = ""; +const TRUNCATION = "\n"; +const CREDENTIAL_KEY = "(?:token|api[_-]?key|secret|password|passwd|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret)"; +const CREDENTIAL_ASSIGNMENT = /(\b(?:token|api[_-]?key|secret|password|passwd|authorization|credential|private[_-]?key|access[_-]?key|client[_-]?secret)\b\s*[:=]\s*)(["']?)([^\s,;"']+)\2/giu; +const BEARER = /\bBearer\s+[A-Za-z0-9._~+/=-]+/giu; +const BASIC = /\bBasic\s+[A-Za-z0-9+/=]+/giu; +const SECRET_HEADER = /(\b(?:authorization|proxy-authorization)\s*:\s*)([^\r\n]+)/giu; +const UNIX_PATH = /\/(?:[^\s/\\:*?"<>|]+\/)*[^\s/\\:*?"<>|]+/gu; +const WINDOWS_PATH = /\b[A-Za-z]:\\(?:[^\s\\/:*?"<>|]+\\)*[^\s\\/:*?"<>|]*/gu; +const FILE_URL = /\bfile:\/\/[^\s"']+/giu; + +function invalid(message) { + throw new Error(`SERVER_JOB_OUTPUT_INVALID: ${message}`); +} + +function assertText(value, field) { + if (typeof value !== "string") invalid(`${field} must be a string`); + return value; +} + +function assertLimit(value, field) { + if (!Number.isSafeInteger(value) || value < 1 || value > SERVER_JOB_OUTPUT_LIMITS[field]) { + invalid(`${field} is outside the fixed output budget`); + } + return value; +} + +function redactCredentials(value) { + let text = value; + let count = 0; + const replace = (pattern, replacement) => { + text = text.replace(pattern, (...args) => { + count += 1; + return typeof replacement === "function" ? replacement(...args) : replacement; + }); + }; + replace(CREDENTIAL_ASSIGNMENT, (_match, prefix, quote) => `${prefix}${quote}${REDACTION}${quote}`); + replace(BEARER, `Bearer ${REDACTION}`); + replace(BASIC, `Basic ${REDACTION}`); + replace(SECRET_HEADER, (_match, prefix) => `${prefix}${REDACTION}`); + return { text, count }; +} + +function redactPaths(value) { + let count = 0; + let text = value.replace(FILE_URL, () => { count += 1; return PATH_REDACTION; }); + text = text.replace(WINDOWS_PATH, () => { count += 1; return PATH_REDACTION; }); + text = text.replace(UNIX_PATH, (match, offset, source) => { + // Keep URL paths and protocol markers readable; only redact filesystem-looking paths. + const before = source.slice(Math.max(0, offset - 8), offset); + if (/https?:$|https?:\/\/$/iu.test(before) || match === "/") return match; + count += 1; + return PATH_REDACTION; + }); + return { text, count }; +} + +function truncateUtf8(value, maxBytes) { + const source = Buffer.from(value, "utf8"); + if (source.byteLength <= maxBytes) return { text: value, truncated: false }; + const marker = Buffer.from(TRUNCATION, "utf8"); + const available = Math.max(0, maxBytes - marker.byteLength); + let end = Math.min(available, source.byteLength); + while (end > 0 && (source[end] & 0xc0) === 0x80) end -= 1; + return { text: `${source.subarray(0, end).toString("utf8")}${TRUNCATION}`, truncated: true }; +} + +export function sanitizeServerJobOutput(value, maxBytes) { + assertText(value, "output"); + assertLimit(maxBytes, "stdoutBytes"); + const originalBytes = Buffer.byteLength(value, "utf8"); + const credentials = redactCredentials(value); + const paths = redactPaths(credentials.text); + const truncated = truncateUtf8(paths.text, maxBytes); + return Object.freeze({ + text: truncated.text, + originalBytes, + emittedBytes: Buffer.byteLength(truncated.text, "utf8"), + redactionCount: credentials.count + paths.count, + truncated: truncated.truncated, + }); +} + +export function createServerJobOutputReceipt(value, limits = SERVER_JOB_OUTPUT_LIMITS) { + if (!value || typeof value !== "object") invalid("receipt input is invalid"); + if (!limits || typeof limits !== "object") invalid("limits are invalid"); + const stdoutBytes = assertLimit(limits.stdoutBytes, "stdoutBytes"); + const stderrBytes = assertLimit(limits.stderrBytes, "stderrBytes"); + const totalBytes = assertLimit(limits.totalBytes, "totalBytes"); + if (stdoutBytes + stderrBytes > totalBytes) invalid("stream budgets exceed total budget"); + const stdout = sanitizeServerJobOutput(value.stdout ?? "", "stdoutBytes" in limits ? stdoutBytes : stdoutBytes); + const stderr = sanitizeServerJobOutput(value.stderr ?? "", "stderrBytes" in limits ? stderrBytes : stderrBytes); + const totalOriginalBytes = stdout.originalBytes + stderr.originalBytes; + const totalEmittedBytes = stdout.emittedBytes + stderr.emittedBytes; + const totalTruncated = stdout.truncated || stderr.truncated || totalOriginalBytes > totalBytes; + return Object.freeze({ + schemaVersion: SERVER_JOB_OUTPUT_SCHEMA, + stdout, + stderr, + limits: Object.freeze({ stdoutBytes, stderrBytes, totalBytes }), + totalOriginalBytes, + totalEmittedBytes, + totalRedactions: stdout.redactionCount + stderr.redactionCount, + totalTruncated, + execution: "DISABLED", + }); +} + +export function redactServerJobOutput(value, maxBytes) { + return sanitizeServerJobOutput(value, maxBytes).text; +} diff --git a/tools/web/server-job-process.mjs b/tools/web/server-job-process.mjs new file mode 100644 index 00000000..80aa3081 --- /dev/null +++ b/tools/web/server-job-process.mjs @@ -0,0 +1,117 @@ +import { spawn } from "node:child_process"; + +export const SERVER_JOB_PROCESS_SCHEMA = 1; + +function invalid(message) { + throw new Error(`SERVER_JOB_PROCESS_INVALID: ${message}`); +} + +function signalTree(pid, signal) { + if (!Number.isInteger(pid) || pid <= 0) return false; + if (process.platform === "win32") return false; + try { + process.kill(-pid, signal); + return true; + } catch (error) { + if (error?.code === "ESRCH") return false; + throw error; + } +} + +function processTreeAlive(pid) { + if (!Number.isInteger(pid) || pid <= 0) return false; + try { + process.kill(process.platform === "win32" ? pid : -pid, 0); + return true; + } catch (error) { + if (error?.code === "ESRCH") return false; + throw error; + } +} + +async function waitForTreeExit(pid, timeoutMs) { + const deadline = Date.now() + timeoutMs; + while (processTreeAlive(pid) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + return !processTreeAlive(pid); +} + +function waitForClose(child) { + if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve({ code: child.exitCode, signal: child.signalCode }); + return new Promise((resolve) => child.once("close", (code, signal) => resolve({ code, signal }))); +} + +export function startServerJobProcess(command, args = [], options = {}) { + if (typeof command !== "string" || command.length === 0) invalid("command is required"); + if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string")) invalid("args must be strings"); + const child = spawn(command, args, { + cwd: options.cwd, + env: options.env, + detached: process.platform !== "win32", + stdio: options.stdio ?? ["ignore", "pipe", "pipe"], + windowsHide: true, + }); + // Drain output even when the caller is testing cancellation before M13-04F receipt handling. + child.stdout?.resume(); + child.stderr?.resume(); + const receipt = { + schemaVersion: SERVER_JOB_PROCESS_SCHEMA, + pid: child.pid, + detached: process.platform !== "win32", + state: "RUNNING", + cancelRequested: false, + treeSignal: null, + result: null, + cleanupCount: 0, + orphanCount: 0, + }; + const completion = waitForClose(child).then((result) => { + receipt.state = receipt.cancelRequested ? "CANCELLED" : "EXITED"; + receipt.result = result; + return Object.freeze({ ...receipt }); + }); + return Object.freeze({ child, receipt, completion }); +} + +export async function cancelServerJobProcess(handle, cleanup, options = {}) { + if (!handle || !handle.child || !handle.receipt || handle.receipt.schemaVersion !== SERVER_JOB_PROCESS_SCHEMA) invalid("process handle is invalid"); + if (typeof cleanup !== "function") invalid("cleanup callback is required"); + const graceMs = Number.isSafeInteger(options.graceMs) && options.graceMs >= 0 ? options.graceMs : 500; + if (handle.receipt.state === "CANCELLED" || handle.receipt.state === "EXITED") { + if (handle.receipt.cleanupCount === 0) { + await cleanup(); + handle.receipt.cleanupCount = 1; + } + return Object.freeze({ ...handle.receipt }); + } + handle.receipt.cancelRequested = true; + if (process.platform === "win32") { + const treeSignal = spawn("taskkill", ["/PID", String(handle.receipt.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true }); + handle.receipt.treeSignal = "TASKKILL_TREE"; + await waitForClose(treeSignal); + } else { + handle.receipt.treeSignal = signalTree(handle.receipt.pid, "SIGTERM") ? "SIGTERM_GROUP" : "ALREADY_EXITED"; + } + const settled = await Promise.race([ + handle.completion, + new Promise((resolve) => setTimeout(() => resolve(null), graceMs)), + ]); + if (!settled && process.platform !== "win32") { + signalTree(handle.receipt.pid, "SIGKILL"); + handle.receipt.treeSignal = "SIGKILL_GROUP"; + } + const result = settled ?? await handle.completion; + let treeExited = process.platform === "win32" || await waitForTreeExit(handle.receipt.pid, graceMs); + if (!treeExited && process.platform !== "win32") { + signalTree(handle.receipt.pid, "SIGKILL"); + handle.receipt.treeSignal = "SIGKILL_GROUP"; + treeExited = await waitForTreeExit(handle.receipt.pid, graceMs); + } + if (handle.receipt.cleanupCount === 0) { + await cleanup(); + handle.receipt.cleanupCount = 1; + } + handle.receipt.orphanCount = treeExited ? 0 : 1; + return Object.freeze({ ...result, treeSignal: handle.receipt.treeSignal, cleanupCount: handle.receipt.cleanupCount, orphanCount: handle.receipt.orphanCount }); +} diff --git a/tools/web/server-job-resource-budget.mjs b/tools/web/server-job-resource-budget.mjs new file mode 100644 index 00000000..f9ffd9df --- /dev/null +++ b/tools/web/server-job-resource-budget.mjs @@ -0,0 +1,32 @@ +export const SERVER_JOB_RESOURCE_BUDGET_SCHEMA = 1; +export const SERVER_JOB_RESOURCE_LIMITS = Object.freeze({ + cpuMs: 60_000, + memoryBytes: 512 * 1024 * 1024, + processCount: 1, + fileCount: 1_024, + wallMs: 300_000, + outputBytes: 512 * 1024 * 1024, +}); + +const fields = Object.keys(SERVER_JOB_RESOURCE_LIMITS); + +function integer(value, field, minimum = 0) { + if (!Number.isSafeInteger(value) || value < minimum || value > SERVER_JOB_RESOURCE_LIMITS[field]) throw new Error(`SERVER_JOB_BUDGET_EXCEEDED: ${field} is outside the enforced budget`); + return value; +} + +export function parseServerJobResourceBudget(value) { + if (!value || value.schemaVersion !== SERVER_JOB_RESOURCE_BUDGET_SCHEMA || typeof value !== "object") throw new Error("SERVER_JOB_BUDGET_INVALID: schema is unsupported"); + if (Object.keys(value).some((key) => key !== "schemaVersion" && !fields.includes(key))) throw new Error("SERVER_JOB_BUDGET_INVALID: undeclared budget field"); + return Object.freeze({ schemaVersion: SERVER_JOB_RESOURCE_BUDGET_SCHEMA, ...Object.fromEntries(fields.map((field) => [field, integer(value[field], field)])) }); +} + +export function createServerJobResourceReceipt(budgetValue, usageValue, status = "SUCCEEDED") { + const budget = parseServerJobResourceBudget(budgetValue); + if (!usageValue || typeof usageValue !== "object") throw new Error("SERVER_JOB_BUDGET_INVALID: usage is invalid"); + const usage = Object.freeze(Object.fromEntries(fields.map((field) => [field, integer(usageValue[field], field)]))); + const exceeded = fields.filter((field) => usage[field] > budget[field]); + if (exceeded.length > 0) throw new Error(`SERVER_JOB_BUDGET_EXCEEDED: ${exceeded.join(",")}`); + if (!["SUCCEEDED", "FAILED", "CANCELLED"].includes(status)) throw new Error("SERVER_JOB_BUDGET_INVALID: status is unsupported"); + return Object.freeze({ schemaVersion: SERVER_JOB_RESOURCE_BUDGET_SCHEMA, status, budget, usage, enforced: true, exceeded: [] }); +} diff --git a/tools/web/server-job-result-binding.mjs b/tools/web/server-job-result-binding.mjs new file mode 100644 index 00000000..bdd7edeb --- /dev/null +++ b/tools/web/server-job-result-binding.mjs @@ -0,0 +1,77 @@ +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; + +export const SERVER_JOB_RESULT_SCHEMA = 1; +const DIGEST = /^[a-f0-9]{64}$/; + +function invalid(message) { throw new Error(`SERVER_JOB_RESULT_INVALID: ${message}`); } +function digest(value, field) { + if (typeof value !== "string" || !DIGEST.test(value)) invalid(`${field} hash is invalid`); + return value; +} +function revision(value, field) { + if (!Number.isSafeInteger(value) || value < 0) invalid(`${field} is invalid`); + return value; +} +function hashBytes(bytes) { return crypto.createHash("sha256").update(bytes).digest("hex"); } +async function hashFile(file) { return hashBytes(await fs.readFile(file)); } + +export function createServerJobResultIdentity(value) { + if (!value || typeof value !== "object") invalid("identity is invalid"); + const requestId = typeof value.requestId === "string" && /^[A-Za-z0-9:_-]{1,128}$/.test(value.requestId) ? value.requestId : invalid("requestId is invalid"); + const projectId = typeof value.projectId === "string" && /^[A-Za-z0-9:_-]{1,128}$/.test(value.projectId) ? value.projectId : invalid("projectId is invalid"); + return Object.freeze({ + schemaVersion: SERVER_JOB_RESULT_SCHEMA, + requestId, + projectId, + baseRevision: revision(value.baseRevision, "baseRevision"), + sourceSha256: digest(value.sourceSha256, "source"), + settingsSha256: digest(value.settingsSha256, "settings"), + buildSha256: digest(value.buildSha256, "build"), + }); +} + +export async function commitServerJobResult(identityValue, outputBytes, options = {}) { + const identity = createServerJobResultIdentity(identityValue); + if (options.expectedIdentity !== undefined) { + const expected = createServerJobResultIdentity(options.expectedIdentity); + for (const field of ["requestId", "projectId", "baseRevision", "sourceSha256", "settingsSha256", "buildSha256"]) { + if (identity[field] !== expected[field]) throw new Error(`SERVER_JOB_RESULT_IDENTITY_MISMATCH: ${field} does not match the request`); + } + } + if (!(outputBytes instanceof Uint8Array) || outputBytes.byteLength < 1) invalid("output bytes are empty"); + const outputSha256 = hashBytes(outputBytes); + const outputByteLength = outputBytes.byteLength; + const outputDirectory = options.outputDirectory; + if (typeof outputDirectory !== "string" || !path.isAbsolute(outputDirectory)) invalid("output directory is invalid"); + await fs.mkdir(outputDirectory, { recursive: true, mode: 0o700 }); + const fileName = `${identity.requestId}.result`; + const target = path.join(outputDirectory, fileName); + const temporary = path.join(outputDirectory, `.${fileName}.${crypto.randomUUID()}.tmp`); + try { + await fs.writeFile(temporary, outputBytes, { flag: "wx", mode: 0o600 }); + const staged = await fs.readFile(temporary); + if (staged.byteLength !== outputByteLength || hashBytes(staged) !== outputSha256) invalid("staged output readback mismatch"); + if (options.faultAt === "AFTER_STAGE") throw new Error("SERVER_JOB_RESULT_STORAGE: injected failure after stage"); + if (options.faultAt === "QUOTA") throw new Error("SERVER_JOB_RESULT_STORAGE_QUOTA: output quota exceeded"); + await fs.rename(temporary, target); + const persisted = await fs.readFile(target); + if (persisted.byteLength !== outputByteLength || hashBytes(persisted) !== outputSha256) invalid("committed output readback mismatch"); + return Object.freeze({ schemaVersion: SERVER_JOB_RESULT_SCHEMA, status: "COMMITTED", requestId: identity.requestId, projectId: identity.projectId, baseRevision: identity.baseRevision, sourceSha256: identity.sourceSha256, settingsSha256: identity.settingsSha256, buildSha256: identity.buildSha256, outputSha256, outputByteLength, outputPath: target, publish: true, execution: "DISABLED" }); + } catch (error) { + await fs.rm(temporary, { force: true }); + if (error instanceof Error && (error.message.startsWith("SERVER_JOB_RESULT_") || error.message.startsWith("SERVER_JOB_RESULT_STORAGE"))) throw error; + throw new Error(`SERVER_JOB_RESULT_STORAGE: ${error instanceof Error ? error.message : String(error)}`); + } +} + +export async function verifyServerJobResultReceipt(receipt, expected, outputDirectory) { + if (!receipt || receipt.schemaVersion !== SERVER_JOB_RESULT_SCHEMA || receipt.status !== "COMMITTED" || receipt.publish !== true) invalid("receipt is not committed"); + const identity = createServerJobResultIdentity(expected); + for (const field of ["requestId", "projectId", "baseRevision", "sourceSha256", "settingsSha256", "buildSha256"]) if (receipt[field] !== identity[field]) invalid(`${field} identity mismatch`); + const target = path.join(outputDirectory, `${identity.requestId}.result`); + const bytes = await fs.readFile(target); + if (bytes.byteLength !== receipt.outputByteLength || hashBytes(bytes) !== receipt.outputSha256) throw new Error("SERVER_JOB_RESULT_HASH_MISMATCH: committed output changed"); + return Object.freeze({ ...receipt, verified: true }); +} diff --git a/tools/web/server-job-startup.py b/tools/web/server-job-startup.py new file mode 100644 index 00000000..87e0a89b --- /dev/null +++ b/tools/web/server-job-startup.py @@ -0,0 +1,11 @@ +import bpy +import json +import sys + +print(json.dumps({ + "schemaVersion": 1, + "runtime": "BLENDER_BACKGROUND_FACTORY_STARTUP", + "background": bool(bpy.app.background), + "version": bpy.app.version_string, + "argv": sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [], +}, sort_keys=True)) diff --git a/web/app/src/app/App.tsx b/web/app/src/app/App.tsx index de0f671c..9c78336d 100644 --- a/web/app/src/app/App.tsx +++ b/web/app/src/app/App.tsx @@ -50,6 +50,8 @@ import { } from "../volume/nanovdb-viewport"; import { composePaintColorPatch, composePaintWeightPatch } from "../../../protocol/paint"; import { createDefaultWebWorkspaceState, reduceUICommand, type EditorType, type UICommand, type WorkspaceId } from "../../../protocol/ui-schema"; +import { createIMECompositionState, reduceIMEComposition, shouldBlockOperatorShortcuts, type IMECompositionEvent } from "../../../protocol/ime-composition"; +import { observeKeyboardEvent, type KeyboardObservation } from "../../../protocol/keyboard-contract"; import { createInitialUserActionStates, reduceUserActionStates, type UserActionIdentity, type UserActionKind } from "../../../protocol/user-action-state"; import { acquireProjectAction, createProjectActionMutexState, releaseProjectAction, type ProjectActionConflict, type ProjectActionIdentity } from "../../../protocol/project-action-mutex"; import { DEFAULT_FILE_READ_YIELD_BYTES, FileByteReadError, readFileBytes, type FileReadPhase } from "../../../protocol/file-byte-reader"; @@ -58,8 +60,17 @@ import { acceptHistoryTransaction, acceptMainSave, acceptMainTransaction, create import type { WorkerFault } from "../../../protocol/worker-fault"; import { normalizeRecentProjects, RECENT_PROJECTS_SCHEMA_VERSION, type RecentProjectBackend, type RecentProjectIssue, type RecentProjectRecord } from "../../../protocol/recent-projects"; import { appendAppDiagnostic, createAppDiagnosticEntry, createAppDiagnosticReport, type AppDiagnosticArea, type AppDiagnosticCode, type AppDiagnosticContextValue, type AppDiagnosticEntry } from "../../../protocol/diagnostic-report"; +import ioFormatUIRegistryJSON from "../capabilities/io-format-ui-registry.json"; +import { filterIOFormatOperatorCommands, gateIOFormatFileSelection, ioFormatUIAccept, parseIOFormatUIRegistry, type IOFormatUICommandRef } from "../../../protocol/io-format-ui-gate"; +import ioFormatRuntimeReceiptFreshnessJSON from "../capabilities/io-format-runtime-receipts-freshness.json"; +import ioFormatRuntimeReceiptFreshnessExpectedJSON from "../capabilities/io-format-runtime-receipts-freshness-expected.json"; +import { parseIOFormatReceiptFreshness, resolveFreshIOFormatRuntimeRoute, type IOFormatReceiptFreshnessExpectedIR } from "../../../protocol/io-format-receipt-freshness"; import "./app-shell.css"; +const IO_FORMAT_UI_REGISTRY = parseIOFormatUIRegistry(ioFormatUIRegistryJSON); +const IO_FORMAT_RUNTIME_RECEIPTS = parseIOFormatReceiptFreshness(ioFormatRuntimeReceiptFreshnessJSON); +const IO_FORMAT_RUNTIME_RECEIPT_EXPECTED = ioFormatRuntimeReceiptFreshnessExpectedJSON as unknown as IOFormatReceiptFreshnessExpectedIR; + function recentProjectIssueMessage(code: RecentProjectIssue["code"]): string { if (code === "MISSING") return "项目内容缺失"; if (code === "HASH_MISMATCH") return "项目内容校验失败"; @@ -948,6 +959,7 @@ interface OperatorCommand { id: string; label: string; keywords: string; + ioFormat?: IOFormatUICommandRef; execute: () => void; } @@ -1047,6 +1059,8 @@ export function App() { const [wasmStatus, setWasmStatus] = useState("WASM ABI: starting"); const [storageStatus, setStorageStatus] = useState("Storage: starting"); const [manifestStatus, setManifestStatus] = useState("Manifest: checking"); + const [imeComposition, setIMEComposition] = useState(createIMECompositionState); + const [keyboardObservation, setKeyboardObservation] = useState(null); const [snapshot, setSnapshot] = useState(null); const [selectedObjectIds, setSelectedObjectIds] = useState>(() => new Set()); const [meshSelection, setMeshSelection] = useState({ meshId: null, mode: "FACE", indices: new Set() }); @@ -1070,6 +1084,7 @@ export function App() { const [recentProjectIssues, setRecentProjectIssues] = useState([]); const [storageBudget, setStorageBudget] = useState(null); const [diagnostics, setDiagnostics] = useState([]); + const imeCompositionRef = useRef(imeComposition); const webClientRef = useRef(null); const storageClientRef = useRef(null); const autosaveRef = useRef(null); @@ -1142,6 +1157,22 @@ export function App() { dispatchUI({ type: "toggleMenu", menu: menu ?? undefined }); window.requestAnimationFrame(() => { if (menu) menuTriggerRefs.current[menu]?.focus(); }); }; + useEffect(() => { + const handleComposition = (event: CompositionEvent): void => { + const type = event.type as IMECompositionEvent["type"]; + const next = reduceIMEComposition(imeCompositionRef.current, { type, data: event.data }); + imeCompositionRef.current = next; + setIMEComposition(next); + }; + window.addEventListener("compositionstart", handleComposition); + window.addEventListener("compositionupdate", handleComposition); + window.addEventListener("compositionend", handleComposition); + return () => { + window.removeEventListener("compositionstart", handleComposition); + window.removeEventListener("compositionupdate", handleComposition); + window.removeEventListener("compositionend", handleComposition); + }; + }, []); const nextUserActionIdentity = (kind: Kind): UserActionIdentity & { kind: Kind } => ( { kind, actionId: `${kind}:${++userActionSequenceRef.current}` } ); @@ -1676,6 +1707,12 @@ export function App() { const onKeyDown = (event: KeyboardEvent): void => { const target = event.target as HTMLElement | null; const interactiveTarget = target?.closest("button, a, input, textarea, select, option, [contenteditable='true'], [role='button'], [role='menuitem'], [role='tab']"); + try { setKeyboardObservation(observeKeyboardEvent(event)); } + catch { setKeyboardObservation(null); } + if (shouldBlockOperatorShortcuts(imeCompositionRef.current, event.isComposing)) { + if (!interactiveTarget) event.preventDefault(); + return; + } if (event.key === "F3") { if (interactiveTarget && target?.matches("input, textarea, select, [contenteditable='true']")) return; event.preventDefault(); @@ -2312,6 +2349,12 @@ export function App() { }; const reportGLBExport = async (): Promise => { const identity = beginUserAction("EXPORT"); + const runtimeRoute = resolveFreshIOFormatRuntimeRoute(IO_FORMAT_RUNTIME_RECEIPTS, IO_FORMAT_RUNTIME_RECEIPT_EXPECTED, { format: "GLB", operation: "EXPORT" }); + if (runtimeRoute.status !== "READY") { + failUserAction(identity, "EXPORT_BLOCKED"); + setEngineStatus(recordDiagnostic("EXPORT", "IO_FORMAT_UNSUPPORTED", runtimeRoute, { format: "GLB", operation: "EXPORT" })); + return; + } if (!snapshot) { failUserAction(identity, "EXPORT_PROJECT_UNAVAILABLE"); setEngineStatus(recordDiagnostic("EXPORT", "GLB_PROJECT_UNAVAILABLE", { code: "EXPORT_PROJECT_UNAVAILABLE", message: "No SceneIR snapshot is open" })); @@ -2384,7 +2427,7 @@ export function App() { if (workerFaultTestMode === "storage") storageClientRef.current?.crashForTest(); else webClientRef.current?.crashForTest(); }; - const operatorCommands: OperatorCommand[] = [ + const operatorCommands = filterIOFormatOperatorCommands([ ...(["Layout", "Modeling", "Animation"] as WorkspaceId[]).filter((id) => id !== workspace).map((id) => ({ id: `workspace.${id}`, label: `Switch to ${id}`, keywords: "workspace", execute: () => dispatchUI({ type: "switchWorkspace", workspaceId: id }) })), { id: "mode.toggle", label: uiState.context.mode === "Object" ? "Enter Edit Mode" : "Exit Edit Mode", keywords: "mode tab", execute: () => dispatchUI({ type: "setMode", mode: uiState.context.mode === "Object" ? "Edit" : "Object" }) }, ...(snapshot && uiState.context.mode === "Object" ? [{ id: "object.add-cube", label: "Add Cube", keywords: "object primitive mesh", execute: () => { void applyEditCommand({ type: "createPrimitive", primitive: "CUBE", location: [0, 0, 0] }); } }] : []), @@ -2394,11 +2437,12 @@ export function App() { { id: "edit.redo", label: "Redo", keywords: "history", execute: () => { void applyEditCommand({ type: "redo" }); } }, { id: "file.save", label: "Save Project", keywords: "file blend", execute: () => { void saveBlend(); } }, { id: "file.close", label: "Close Project", keywords: "file", execute: closeProject }, + { id: "file.export-glb", label: "Export GLB", keywords: "file export glb", ioFormat: { format: "GLB", operation: "EXPORT", execution: "LOCAL" } as const, execute: () => { void reportGLBExport(); } }, ] : []), - ]; + ], IO_FORMAT_UI_REGISTRY).filter((command) => !command.ioFormat || resolveFreshIOFormatRuntimeRoute(IO_FORMAT_RUNTIME_RECEIPTS, IO_FORMAT_RUNTIME_RECEIPT_EXPECTED, { format: command.ioFormat.format, operation: command.ioFormat.operation }).status === "READY"); return ( -

)}
{workerFaultTestMode ? : null}
- { const file = event.target.files?.[0]; if (file) void openBlendFile(file); event.target.value = ""; }} /> + route.format).join(",")} onChange={(event) => { const file = event.target.files?.[0]; if (file) { const gate = gateIOFormatFileSelection(file.name, IO_FORMAT_UI_REGISTRY); if (gate.status === "BLOCKED") setEngineStatus(recordDiagnostic("ENGINE", "IO_FORMAT_UNSUPPORTED", gate, { fileName: file.name })); else void openBlendFile(file); } event.target.value = ""; }} /> {workerFault ?
{workerFault.source === "engine" ? "Engine Worker" : "Storage Worker"} stopped; current project list and scene are retained.{workerRecoveryStatus === "FAILED" ? Recovery failed; retry is safe. : null}
: null} {recentProjectIssues.length > 0 ?
最近项目中有 {recentProjectIssues.length} 个条目无法验证;当前场景不会被删除。
{recentProjectIssues.map((issue) =>
{issue.project.displayName}: {recentProjectIssueMessage(issue.code)}
)}
: null} diff --git a/web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json b/web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json new file mode 100644 index 00000000..c9f8cf9e --- /dev/null +++ b/web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json @@ -0,0 +1,326 @@ +{ + "parentBindingSha256": "7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad", + "parentReceiptSetSha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b", + "inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "boundReceiptSetSha256": "2015d719a2046f76dda3daf7c8ae7a3fc5183a499489ef06a0c33f166c6ea0b9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "receiptIdentities": [ + { + "format": "GLTF", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "843c7eb040189ccd7fcccfb697c4ecfd35d405add50008967b7ca5a89e4dcde7", + "settingsSha256": "01a5fbe96ab7af4bf38c35a3723a7619233299086e400ff5064dbd91e9d586e8", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLTF", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "a1c2dea067898071d6d4f0fc4f83e81df920bc572a9250ed01229d618ef15a37", + "settingsSha256": "df7db92e5ea9a4d52a81dc8092f48ca9dfda80594e500b05f3787352891962f9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "d78e336432dc578727992b8ca53368e3ac49ffdafeb1c16847fe48c54e35a079", + "settingsSha256": "b909a16f4103dfad49997c597c80ad3e71a932989f8a4594eb4f019223bd56e6", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "0c2820eb9d7b82a1cd1cb208f75f263a527c58576952d4bd934cf0f7eb29ee3e", + "settingsSha256": "3b375dcb889e594e61da9d8e912037d8fa0220ea876e7465e6c37da4f5b21cee", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "IMPORT", + "operator": "wm.obj_import", + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "42f9e3b35f753e43100aaea618ed306f1bf062fb7bb44af841a2e2d599449fbd", + "settingsSha256": "4e879a3018ffcf529c28fb54e09efe5f3829b501a2b001da86f9a9b4b303bccb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "EXPORT", + "operator": "wm.obj_export", + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "b4328f82639bdfefb016aec15629135ebd080e0a5696759dd670ab85168b7c60", + "settingsSha256": "f7660d5742890d2f05e8da803f5d8616233570a4f06960b85a2142efd9396d2f", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "IMPORT", + "operator": "wm.stl_import", + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "f6bb7a058c246914f5f077665aab1f3d45c60b176f917b15a54522d12164c703", + "settingsSha256": "b01bd0b819aa72cf1de817b3e9bca2df03b9ceac41f639f738176973fea9accb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "EXPORT", + "operator": "wm.stl_export", + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "8be11ddd9bd68dcddc676529d30abcc236289f25ece32e137fd79bcd5ff3286d", + "settingsSha256": "5f1797ab8291fb3d84a8c1a892aa692a120a7db4ef84c9d3a9b1e78d5a6d92b7", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "IMPORT", + "operator": "wm.ply_import", + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2c8483351e293c5e0450f55902c24e3346bf95d53243ebf194fccc1efc1caa80", + "settingsSha256": "390cbc8a4843d88bd567a7f7d51b9e0d5853992bfc7a66c4a2fd335ed33d25a3", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "EXPORT", + "operator": "wm.ply_export", + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2f6cf92d5a813083b206b9c38a4442f82685b1c5740f313b90a0a7b60604a2b5", + "settingsSha256": "cd4bf21d72086001a02377cdc5c7f22856cbd1e04b261e37484260f3fc2ea6ae", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "IMPORT", + "operator": "wm.usd_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "a8f79c9243ffa9ba0ba8e3e948c198fdffa727bac578269d8ec041f56cad1c5d", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "EXPORT", + "operator": "wm.usd_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "cf6a9737773c27faf82cd8b3d4df84a9b671e1c873cd5041f12d70926ec62abf", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "IMPORT", + "operator": "wm.alembic_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "7471c261ab8520df718399e69f6f8d73be11fb76f1717eac9a407964fc2f7ee3", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "EXPORT", + "operator": "wm.alembic_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "8d983f6f8c5bb722d2b12c47c56115ae4bdff0a173a9fb9f4d93f083bea8e513", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + } + ] +} diff --git a/web/app/src/capabilities/io-format-runtime-receipts-freshness.json b/web/app/src/capabilities/io-format-runtime-receipts-freshness.json new file mode 100644 index 00000000..ef996cbd --- /dev/null +++ b/web/app/src/capabilities/io-format-runtime-receipts-freshness.json @@ -0,0 +1,332 @@ +{ + "schemaVersion": 1, + "task": "M12-05F", + "parentBindingSha256": "7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad", + "boundReceiptSetSha256": "2015d719a2046f76dda3daf7c8ae7a3fc5183a499489ef06a0c33f166c6ea0b9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6", + "bound": { + "schemaVersion": 1, + "task": "M12-05E", + "parentReceiptSetSha256": "f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b", + "inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "receipts": [ + { + "format": "GLTF", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "843c7eb040189ccd7fcccfb697c4ecfd35d405add50008967b7ca5a89e4dcde7", + "settingsSha256": "01a5fbe96ab7af4bf38c35a3723a7619233299086e400ff5064dbd91e9d586e8", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLTF", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ], + "sourceSha256": "a1c2dea067898071d6d4f0fc4f83e81df920bc572a9250ed01229d618ef15a37", + "settingsSha256": "df7db92e5ea9a4d52a81dc8092f48ca9dfda80594e500b05f3787352891962f9", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "d78e336432dc578727992b8ca53368e3ac49ffdafeb1c16847fe48c54e35a079", + "settingsSha256": "b909a16f4103dfad49997c597c80ad3e71a932989f8a4594eb4f019223bd56e6", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ], + "sourceSha256": "0c2820eb9d7b82a1cd1cb208f75f263a527c58576952d4bd934cf0f7eb29ee3e", + "settingsSha256": "3b375dcb889e594e61da9d8e912037d8fa0220ea876e7465e6c37da4f5b21cee", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "IMPORT", + "operator": "wm.obj_import", + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "42f9e3b35f753e43100aaea618ed306f1bf062fb7bb44af841a2e2d599449fbd", + "settingsSha256": "4e879a3018ffcf529c28fb54e09efe5f3829b501a2b001da86f9a9b4b303bccb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "EXPORT", + "operator": "wm.obj_export", + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ], + "sourceSha256": "b4328f82639bdfefb016aec15629135ebd080e0a5696759dd670ab85168b7c60", + "settingsSha256": "f7660d5742890d2f05e8da803f5d8616233570a4f06960b85a2142efd9396d2f", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "IMPORT", + "operator": "wm.stl_import", + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "f6bb7a058c246914f5f077665aab1f3d45c60b176f917b15a54522d12164c703", + "settingsSha256": "b01bd0b819aa72cf1de817b3e9bca2df03b9ceac41f639f738176973fea9accb", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "STL", + "family": "STL", + "operation": "EXPORT", + "operator": "wm.stl_export", + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ], + "sourceSha256": "8be11ddd9bd68dcddc676529d30abcc236289f25ece32e137fd79bcd5ff3286d", + "settingsSha256": "5f1797ab8291fb3d84a8c1a892aa692a120a7db4ef84c9d3a9b1e78d5a6d92b7", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "IMPORT", + "operator": "wm.ply_import", + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2c8483351e293c5e0450f55902c24e3346bf95d53243ebf194fccc1efc1caa80", + "settingsSha256": "390cbc8a4843d88bd567a7f7d51b9e0d5853992bfc7a66c4a2fd335ed33d25a3", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "PLY", + "family": "PLY", + "operation": "EXPORT", + "operator": "wm.ply_export", + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ], + "sourceSha256": "2f6cf92d5a813083b206b9c38a4442f82685b1c5740f313b90a0a7b60604a2b5", + "settingsSha256": "cd4bf21d72086001a02377cdc5c7f22856cbd1e04b261e37484260f3fc2ea6ae", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "IMPORT", + "operator": "wm.usd_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "a8f79c9243ffa9ba0ba8e3e948c198fdffa727bac578269d8ec041f56cad1c5d", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "USD", + "family": "USD", + "operation": "EXPORT", + "operator": "wm.usd_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ], + "sourceSha256": "cf6a9737773c27faf82cd8b3d4df84a9b671e1c873cd5041f12d70926ec62abf", + "settingsSha256": "5a396eb68ddd5943df9834996b1e6b993ad7e66b019308aa318517980b518390", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "IMPORT", + "operator": "wm.alembic_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "7471c261ab8520df718399e69f6f8d73be11fb76f1717eac9a407964fc2f7ee3", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "EXPORT", + "operator": "wm.alembic_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ], + "sourceSha256": "8d983f6f8c5bb722d2b12c47c56115ae4bdff0a173a9fb9f4d93f083bea8e513", + "settingsSha256": "fa2669dd3c464f8312581faf665690709dc503fdfb5662642a6e635bd2e91e55", + "runtimeSha256": "729b46fdfea424feb258ee42ad0f3360aa433abaf31831eeb8e4f6203fa298b6" + } + ] + } +} diff --git a/web/app/src/capabilities/io-format-runtime-receipts.json b/web/app/src/capabilities/io-format-runtime-receipts.json new file mode 100644 index 00000000..77eb5fcc --- /dev/null +++ b/web/app/src/capabilities/io-format-runtime-receipts.json @@ -0,0 +1,282 @@ +{ + "schemaVersion": 1, + "task": "M12-05D", + "inventorySha256": "0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4", + "runtime": { + "binarySha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "blenderVersion": "5.2.0 LTS", + "buildBranch": "unknown", + "buildCommitTimestamp": 0, + "buildDate": "2026-07-24", + "buildHash": "unknown", + "buildOptions": { + "alembic": false, + "io_ply": true, + "io_stl": true, + "io_wavefront_obj": true, + "usd": false + }, + "buildPlatform": "Linux", + "buildTime": "08:03:47", + "buildType": "Release", + "versionTuple": [ + 5, + 2, + 0 + ] + }, + "receipts": [ + { + "format": "GLTF", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ] + }, + { + "format": "GLTF", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLTF_SEPARATE" + ], + "extensions": [ + ".gltf" + ] + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "IMPORT", + "operator": "import_scene.gltf", + "registered": true, + "rnaIdentifier": "IMPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ] + }, + { + "format": "GLB", + "family": "GLTF", + "operation": "EXPORT", + "operator": "export_scene.gltf", + "registered": true, + "rnaIdentifier": "EXPORT_SCENE_OT_gltf", + "buildOption": null, + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "GLB" + ], + "extensions": [ + ".glb" + ] + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "IMPORT", + "operator": "wm.obj_import", + "registered": true, + "rnaIdentifier": "WM_OT_obj_import", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ] + }, + { + "format": "OBJ", + "family": "OBJ", + "operation": "EXPORT", + "operator": "wm.obj_export", + "registered": true, + "rnaIdentifier": "WM_OT_obj_export", + "buildOption": "io_wavefront_obj", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "OBJ" + ], + "extensions": [ + ".obj" + ] + }, + { + "format": "STL", + "family": "STL", + "operation": "IMPORT", + "operator": "wm.stl_import", + "registered": true, + "rnaIdentifier": "WM_OT_stl_import", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ] + }, + { + "format": "STL", + "family": "STL", + "operation": "EXPORT", + "operator": "wm.stl_export", + "registered": true, + "rnaIdentifier": "WM_OT_stl_export", + "buildOption": "io_stl", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "STL_BINARY", + "STL_ASCII" + ], + "extensions": [ + ".stl" + ] + }, + { + "format": "PLY", + "family": "PLY", + "operation": "IMPORT", + "operator": "wm.ply_import", + "registered": true, + "rnaIdentifier": "WM_OT_ply_import", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ] + }, + { + "format": "PLY", + "family": "PLY", + "operation": "EXPORT", + "operator": "wm.ply_export", + "registered": true, + "rnaIdentifier": "WM_OT_ply_export", + "buildOption": "io_ply", + "buildOptionEnabled": true, + "runtimeStatus": "AVAILABLE", + "variants": [ + "PLY" + ], + "extensions": [ + ".ply" + ] + }, + { + "format": "USD", + "family": "USD", + "operation": "IMPORT", + "operator": "wm.usd_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ] + }, + { + "format": "USD", + "family": "USD", + "operation": "EXPORT", + "operator": "wm.usd_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "usd", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "USD", + "USDA", + "USDC", + "USDZ" + ], + "extensions": [ + ".usd", + ".usda", + ".usdc", + ".usdz" + ] + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "IMPORT", + "operator": "wm.alembic_import", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ] + }, + { + "format": "ALEMBIC", + "family": "ALEMBIC", + "operation": "EXPORT", + "operator": "wm.alembic_export", + "registered": false, + "rnaIdentifier": null, + "buildOption": "alembic", + "buildOptionEnabled": null, + "runtimeStatus": "OPERATOR_UNREGISTERED", + "variants": [ + "ALEMBIC" + ], + "extensions": [ + ".abc" + ] + } + ] +} diff --git a/web/app/src/capabilities/io-format-ui-registry.json b/web/app/src/capabilities/io-format-ui-registry.json new file mode 100644 index 00000000..dd5873ae --- /dev/null +++ b/web/app/src/capabilities/io-format-ui-registry.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M12-05C", + "parentMatrixSha256": "139c9d764736da176b32414ecda840c07eb0ba5f3864da2dc08ce04bae161366", + "projectFileAccept": ".blend,application/octet-stream", + "importRoutes": [], + "exportRoutes": [ + { + "format": "GLB", + "operation": "EXPORT", + "execution": "LOCAL", + "extensions": [ + ".glb" + ] + } + ] +} diff --git a/web/app/src/library-link-chromium.ts b/web/app/src/library-link-chromium.ts new file mode 100644 index 00000000..c3ff8f6a --- /dev/null +++ b/web/app/src/library-link-chromium.ts @@ -0,0 +1 @@ +export { gateLinkedDataMutation } from "../../protocol/library-linked-mutation"; diff --git a/web/app/src/library-override-chromium.ts b/web/app/src/library-override-chromium.ts new file mode 100644 index 00000000..a112a980 --- /dev/null +++ b/web/app/src/library-override-chromium.ts @@ -0,0 +1 @@ +export { applyOverrideWriter } from "../../protocol/library-override-writer"; diff --git a/web/app/src/storage/StorageClient.ts b/web/app/src/storage/StorageClient.ts index 66eefc0c..7417cb68 100644 --- a/web/app/src/storage/StorageClient.ts +++ b/web/app/src/storage/StorageClient.ts @@ -135,8 +135,8 @@ export class StorageClient { return this.request({ type: "readSnapshot", projectId, revision }) as Promise; } - putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string): Promise { - return this.request({ type: "putAsset", projectId, data, mimeType, sourcePath }, [data]) as Promise; + putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string, faultAt?: "quota"): Promise { + return this.request({ type: "putAsset", projectId, data, mimeType, sourcePath, faultAt }, [data]) as Promise; } readAsset(projectId: string, sha256: string): Promise { diff --git a/web/app/src/testing/glb-recovery.ts b/web/app/src/testing/glb-recovery.ts new file mode 100644 index 00000000..2a27646f --- /dev/null +++ b/web/app/src/testing/glb-recovery.ts @@ -0,0 +1,7 @@ +export { + GLB_RECOVERY_SCHEMA_VERSION, + beginGLBRecoveryOperation, + blockGLBRecoveryForQuota, + parseGLBRecoveryReceipt, + recoverGLBRecoveryOperation, +} from "../../../protocol/glb-recovery"; diff --git a/web/app/src/testing/io-format-recovery.ts b/web/app/src/testing/io-format-recovery.ts new file mode 100644 index 00000000..1b98510b --- /dev/null +++ b/web/app/src/testing/io-format-recovery.ts @@ -0,0 +1,9 @@ +export { + IO_FORMAT_RECOVERY_SCHEMA_VERSION, + beginIOFormatRecoveryOperation, + blockIOFormatRecoveryOperation, + cancelIOFormatRecoveryOperation, + commitIOFormatRecoveryOperation, + parseIOFormatRecoveryReceipt, + recoverIOFormatRecoveryOperation, +} from "../../../protocol/io-format-recovery"; diff --git a/web/app/src/testing/script-sandbox-dispose.ts b/web/app/src/testing/script-sandbox-dispose.ts new file mode 100644 index 00000000..6aad57aa --- /dev/null +++ b/web/app/src/testing/script-sandbox-dispose.ts @@ -0,0 +1,27 @@ +export const SCRIPT_SANDBOX_DISPOSE_SCHEMA = 1 as const; + +export interface ScriptSandboxDisposeReceipt { + schemaVersion: typeof SCRIPT_SANDBOX_DISPOSE_SCHEMA; + disposeCount: number; + idempotent: boolean; + resources: { + messagePorts: 0; + timers: 0; + abortControllers: 0; + transferableBuffers: 0; + pendingRequests: 0; + cacheReferences: 0; + }; + lateTimerMessages: 0; +} + +export function createScriptSandboxDisposeReceipt(disposeCount: number): ScriptSandboxDisposeReceipt { + if (!Number.isSafeInteger(disposeCount) || disposeCount < 1) throw new Error("SCRIPT_SANDBOX_DISPOSE_INVALID: dispose count must be positive"); + return { + schemaVersion: SCRIPT_SANDBOX_DISPOSE_SCHEMA, + disposeCount, + idempotent: disposeCount > 1, + resources: { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }, + lateTimerMessages: 0, + }; +} diff --git a/web/app/src/testing/script-sandbox-recovery.ts b/web/app/src/testing/script-sandbox-recovery.ts new file mode 100644 index 00000000..09216994 --- /dev/null +++ b/web/app/src/testing/script-sandbox-recovery.ts @@ -0,0 +1,56 @@ +export const SCRIPT_SANDBOX_RECOVERY_SCHEMA = 1 as const; + +export interface ScriptSandboxRecoveryAuditEntry { + sequence: 1 | 2; + requestId: string; + previousEntrySha256: string | null; + entrySha256: string; + sourceSha256: string; + manifestSha256: string; +} + +export interface ScriptSandboxRecoveryReceipt { + schemaVersion: typeof SCRIPT_SANDBOX_RECOVERY_SCHEMA; + operation: "SCRIPT_SANDBOX_RECOVERY"; + previousGeneration: number; + nextGeneration: number; + mainRevisionBefore: number; + mainRevisionAfter: number; + sourceSha256: string; + manifestSha256: string; + audit: { + entries: 2; + first: ScriptSandboxRecoveryAuditEntry; + second: ScriptSandboxRecoveryAuditEntry; + }; + recovered: true; + execution: "DISABLED"; +} + +const SHA256 = /^[a-f0-9]{64}$/; +const REQUEST_ID = /^[-A-Za-z0-9:_./]{1,256}$/; + +function assertDigest(value: string, name: string): void { + if (!SHA256.test(value)) throw new Error(`SCRIPT_SANDBOX_RECOVERY_INVALID: ${name} must be a SHA-256 digest`); +} + +function assertEntry(entry: ScriptSandboxRecoveryAuditEntry, expectedSequence: 1 | 2, sourceSha256: string, manifestSha256: string): void { + if (entry.sequence !== expectedSequence || !REQUEST_ID.test(entry.requestId)) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit sequence or request id is invalid"); + if (expectedSequence === 1 ? entry.previousEntrySha256 !== null : !entry.previousEntrySha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit previous hash is invalid"); + assertDigest(entry.entrySha256, `audit[${expectedSequence}].entrySha256`); + if (entry.previousEntrySha256 !== null) assertDigest(entry.previousEntrySha256, `audit[${expectedSequence}].previousEntrySha256`); + if (entry.sourceSha256 !== sourceSha256 || entry.manifestSha256 !== manifestSha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit source or manifest hash drifted"); +} + +export function createScriptSandboxRecoveryReceipt(input: Omit): ScriptSandboxRecoveryReceipt { + if (!Number.isSafeInteger(input.previousGeneration) || !Number.isSafeInteger(input.nextGeneration) || input.nextGeneration !== input.previousGeneration + 1) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: generation must advance once"); + if (!Number.isSafeInteger(input.mainRevisionBefore) || input.mainRevisionBefore < 0 || input.mainRevisionAfter !== input.mainRevisionBefore) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: Main revision changed during recovery"); + assertDigest(input.sourceSha256, "sourceSha256"); + assertDigest(input.manifestSha256, "manifestSha256"); + if (input.audit.entries !== 2) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit entry count is invalid"); + assertEntry(input.audit.first, 1, input.sourceSha256, input.manifestSha256); + assertEntry(input.audit.second, 2, input.sourceSha256, input.manifestSha256); + if (input.audit.second.previousEntrySha256 !== input.audit.first.entrySha256) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit hash chain is not continuous"); + if (input.audit.first.requestId === input.audit.second.requestId) throw new Error("SCRIPT_SANDBOX_RECOVERY_INVALID: audit request id is replayed"); + return { schemaVersion: SCRIPT_SANDBOX_RECOVERY_SCHEMA, operation: "SCRIPT_SANDBOX_RECOVERY", ...input, recovered: true, execution: "DISABLED" }; +} diff --git a/web/app/src/three-adapter/offscreen-viewport.ts b/web/app/src/three-adapter/offscreen-viewport.ts index 1150fa89..5b10d969 100644 --- a/web/app/src/three-adapter/offscreen-viewport.ts +++ b/web/app/src/three-adapter/offscreen-viewport.ts @@ -7,6 +7,8 @@ import { cloneMeshGeometryBuffers } from "../../../protocol/mesh-geometry-delta" import { nonMeshChunkTransferables } from "../../../protocol/nonmesh-binary"; import type { OffscreenViewportRequest, OffscreenViewportResponse } from "./offscreen-viewport-protocol"; import { PBR_PROFILE, PBR_SHADOW_PROFILE, PBR_TONE_MAPPING } from "./pbr"; +import { resolveViewportPixelMetrics, viewportNDC } from "../../../protocol/viewport-dpr"; +import { observePointerEvent } from "../../../protocol/pointer-contract"; import type { NonMeshElementKind } from "./nonmesh"; import type { GreasePencilPointPreview, GreasePencilPointRef } from "./grease-pencil"; import type { CurveGizmoFrameIR, CurveGizmoHandleIR } from "../../../protocol/nonmesh-interaction"; @@ -118,7 +120,7 @@ export class OffscreenViewportRenderer implements ViewportBackend { canvas: offscreen, width: Math.max(1, canvas.clientWidth), height: Math.max(1, canvas.clientHeight), - pixelRatio: Math.min(window.devicePixelRatio || 1, 2), + pixelRatio: resolveViewportPixelMetrics(1, 1, window.devicePixelRatio).pixelRatio, }; this.worker.postMessage(request, [offscreen]); this.resizeObserver = new ResizeObserver(() => this.resize()); @@ -237,15 +239,21 @@ export class OffscreenViewportRenderer implements ViewportBackend { } private resize(): void { + const metrics = resolveViewportPixelMetrics(Math.max(1, this.canvas.clientWidth), Math.max(1, this.canvas.clientHeight), window.devicePixelRatio); + this.canvas.dataset.viewportCssSize = `${metrics.cssWidth}x${metrics.cssHeight}`; + this.canvas.dataset.viewportBackingSize = `${metrics.backingWidth}x${metrics.backingHeight}`; + this.canvas.dataset.viewportPixelRatio = String(metrics.pixelRatio); this.worker.postMessage({ type: "resize", - width: Math.max(1, this.canvas.clientWidth), - height: Math.max(1, this.canvas.clientHeight), - pixelRatio: Math.min(window.devicePixelRatio || 1, 2), + width: metrics.cssWidth, + height: metrics.cssHeight, + pixelRatio: metrics.pixelRatio, } satisfies OffscreenViewportRequest); } private pointerDown = (event: PointerEvent): void => { + try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); } + catch { this.canvas.dataset.lastPointer = "BLOCKED"; } this.pointer = { id: event.pointerId, x: event.clientX, y: event.clientY, moved: false }; try { this.canvas.setPointerCapture(event.pointerId); @@ -266,12 +274,13 @@ export class OffscreenViewportRenderer implements ViewportBackend { }; private pointerUp = (event: PointerEvent): void => { + try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); } + catch { this.canvas.dataset.lastPointer = "BLOCKED"; } if (!this.pointer || this.pointer.id !== event.pointerId) return; if (!this.pointer.moved) { const bounds = this.canvas.getBoundingClientRect(); - const x = ((event.clientX - bounds.left) / Math.max(1, bounds.width)) * 2 - 1; - const y = -((event.clientY - bounds.top) / Math.max(1, bounds.height)) * 2 + 1; - this.worker.postMessage({ type: "pick", x, y, additive: event.shiftKey || event.ctrlKey || event.metaKey, baseSelectionRevision: this.greasePencilSelectionRevision } satisfies OffscreenViewportRequest); + const ndc = viewportNDC(event.clientX, event.clientY, bounds); + this.worker.postMessage({ type: "pick", x: ndc.x, y: ndc.y, additive: event.shiftKey || event.ctrlKey || event.metaKey, baseSelectionRevision: this.greasePencilSelectionRevision } satisfies OffscreenViewportRequest); } this.pointer = null; }; diff --git a/web/app/src/three-adapter/viewport.ts b/web/app/src/three-adapter/viewport.ts index 90f5cc29..7258abbd 100644 --- a/web/app/src/three-adapter/viewport.ts +++ b/web/app/src/three-adapter/viewport.ts @@ -69,6 +69,8 @@ import { import { VIEWPORT_DEFAULT_ORBIT, VIEWPORT_ORBIT_MAX_DISTANCE, VIEWPORT_ORBIT_MIN_DISTANCE, VIEWPORT_ORBIT_ROTATE_SENSITIVITY, VIEWPORT_ORBIT_ZOOM_SENSITIVITY, orbitPosition, orbitStateFromPosition } from "../../../protocol/viewport-camera"; import { validatePaintDepthVisibilityRequest, type PaintDepthVisibilityRequestIR, type PaintDepthVisibilityResultIR } from "../../../protocol/paint-depth-visibility"; import { samplePaintDepthVisibilityGPU } from "./paint-depth-visibility"; +import { resolveViewportPixelMetrics, viewportNDC } from "../../../protocol/viewport-dpr"; +import { observePointerEvent } from "../../../protocol/pointer-contract"; export function collectMeshInstanceGroups(snapshot: SceneSnapshotIR, minimumSize = 2): Map { const groups = new Map(); @@ -137,7 +139,7 @@ export class ViewportRenderer { this.raycaster.params.Points.threshold = 0.14; this.renderer = new WebGLRenderer({ canvas, antialias: true, alpha: false, preserveDrawingBuffer: true }); configurePBRRenderer(this.renderer); - this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + this.renderer.setPixelRatio(resolveViewportPixelMetrics(1, 1, window.devicePixelRatio).pixelRatio); this.renderer.setClearColor(new Color("#25272b")); this.canvas.dataset.rendererBackend = "webgl-pbr"; this.canvas.dataset.pbrProfile = PBR_PROFILE; @@ -170,6 +172,8 @@ export class ViewportRenderer { this.resizeObserver = new ResizeObserver(() => this.resize()); this.resizeObserver.observe(canvas); this.canvas.addEventListener("click", this.handleClick); + this.canvas.addEventListener("pointerdown", this.handlePointerObservation); + this.canvas.addEventListener("pointercancel", this.handlePointerObservation); this.canvas.addEventListener("webglcontextlost", this.handleContextLost); this.canvas.addEventListener("webglcontextrestored", this.handleContextRestored); this.resize(); @@ -793,10 +797,14 @@ export class ViewportRenderer { private resize(): void { const width = Math.max(1, this.canvas.clientWidth); const height = Math.max(1, this.canvas.clientHeight); + const metrics = resolveViewportPixelMetrics(width, height, window.devicePixelRatio); this.camera.aspect = width / height; this.camera.updateProjectionMatrix(); this.controls.rotateSpeed = VIEWPORT_ORBIT_ROTATE_SENSITIVITY * height / (2 * Math.PI); this.renderer.setSize(width, height, false); + this.canvas.dataset.viewportCssSize = `${metrics.cssWidth}x${metrics.cssHeight}`; + this.canvas.dataset.viewportBackingSize = `${metrics.backingWidth}x${metrics.backingHeight}`; + this.canvas.dataset.viewportPixelRatio = String(metrics.pixelRatio); this.publishCameraState(); } @@ -810,12 +818,14 @@ export class ViewportRenderer { } private handleClick = (event: MouseEvent): void => { + if ("pointerType" in event) { + try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event as MouseEvent & { pointerType?: string; pointerId?: number; pressure?: number; tiltX?: number; tiltY?: number; buttons?: number; type?: string })); } + catch { this.canvas.dataset.lastPointer = "BLOCKED"; } + } const bounds = this.canvas.getBoundingClientRect(); if (bounds.width <= 0 || bounds.height <= 0) return; - this.pointer.set( - ((event.clientX - bounds.left) / bounds.width) * 2 - 1, - -((event.clientY - bounds.top) / bounds.height) * 2 + 1, - ); + const ndc = viewportNDC(event.clientX, event.clientY, bounds); + this.pointer.set(ndc.x, ndc.y); this.raycaster.setFromCamera(this.pointer, this.camera); const hits = this.raycaster.intersectObjects(this.importedRoot.children, true); const greasePencilHit = this.editMode @@ -886,6 +896,11 @@ export class ViewportRenderer { if (typeof objectId === "string") this.onSelect?.(objectId, additive); }; + private handlePointerObservation = (event: PointerEvent): void => { + try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); } + catch { this.canvas.dataset.lastPointer = "BLOCKED"; } + }; + private renderLoop = (): void => { if (this.disposed) return; if (!this.contextLost) { @@ -912,7 +927,7 @@ export class ViewportRenderer { this.contextLost = false; this.canvas.dataset.deviceStatus = "restoring"; configurePBRRenderer(this.renderer); - this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2)); + this.renderer.setPixelRatio(resolveViewportPixelMetrics(1, 1, window.devicePixelRatio).pixelRatio); this.renderer.setClearColor(new Color("#25272b")); this.resize(); this.volumeRenderCache.clear(); @@ -949,6 +964,8 @@ export class ViewportRenderer { window.cancelAnimationFrame(this.animationFrame); this.resizeObserver.disconnect(); this.canvas.removeEventListener("click", this.handleClick); + this.canvas.removeEventListener("pointerdown", this.handlePointerObservation); + this.canvas.removeEventListener("pointercancel", this.handlePointerObservation); this.canvas.removeEventListener("webglcontextlost", this.handleContextLost); this.canvas.removeEventListener("webglcontextrestored", this.handleContextRestored); this.controls.dispose(); diff --git a/web/app/src/workers/glb-desktop-import-test.worker.ts b/web/app/src/workers/glb-desktop-import-test.worker.ts new file mode 100644 index 00000000..34705be5 --- /dev/null +++ b/web/app/src/workers/glb-desktop-import-test.worker.ts @@ -0,0 +1,47 @@ +import { compareGLBDesktopFixtureSemantics, importGLBDesktopFixtureSemantics } from "../../../protocol/glb-import"; + +interface FixtureInput { + id: string; + bytes: ArrayBuffer; + sourceSha256: string; + expected: unknown; +} + +const scope = self as unknown as { + onmessage: ((event: MessageEvent<{ fixtures: FixtureInput[] }>) => void) | null; + postMessage(message: unknown): void; +}; + +async function sha256(bytes: ArrayBuffer): Promise { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join(""); +} + +scope.onmessage = async (event) => { + try { + const results = []; + for (const fixture of event.data.fixtures) { + const sourceSha256 = await sha256(fixture.bytes); + if (sourceSha256 !== fixture.sourceSha256) throw new Error(`${fixture.id} source SHA-256 mismatch`); + const imported = importGLBDesktopFixtureSemantics(fixture.bytes); + const comparison = compareGLBDesktopFixtureSemantics(fixture.expected, imported); + results.push({ + id: fixture.id, + sourceSha256, + compatible: comparison.compatible, + mismatches: comparison.mismatches, + topology: imported.meshes.reduce((sum, mesh) => sum + mesh.primitives.length, 0), + attributes: [...new Set(imported.meshes.flatMap((mesh) => mesh.primitives.flatMap((primitive) => Object.keys(primitive.attributes))))].sort(), + materials: imported.materials.length, + nodes: imported.nodes.length, + animations: imported.animations.length, + }); + } + scope.postMessage({ ok: true, results }); + } + catch (error) { + scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "GLB desktop import failed" }); + } +}; + +export {}; diff --git a/web/app/src/workers/glb-loss-report-test.worker.ts b/web/app/src/workers/glb-loss-report-test.worker.ts new file mode 100644 index 00000000..8aae6a35 --- /dev/null +++ b/web/app/src/workers/glb-loss-report-test.worker.ts @@ -0,0 +1,33 @@ +import { exportGLB } from "../../../protocol/glb-export"; +import { createGLBLossReport } from "../../../protocol/glb-loss-report"; +import type { GLBAssetBuffer } from "../../../protocol/glb-export"; +import type { MeshGeometryBuffer } from "../../../protocol/web-engine"; +import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary"; +import type { SceneSnapshotIR } from "../../../protocol/scene-ir"; + +interface Request { + snapshot: SceneSnapshotIR; + geometryBuffers: MeshGeometryBuffer[]; + assetBuffers: GLBAssetBuffer[]; + nonMeshGeometryBuffers: NonMeshGeometryChunk[]; +} + +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: unknown, transfer?: Transferable[]): void; +}; + +scope.onmessage = (event) => { + try { + const request = event.data; + const exported = exportGLB(request.snapshot, request.geometryBuffers, request.assetBuffers, request.nonMeshGeometryBuffers); + const report = createGLBLossReport(request.snapshot, exported.report); + const output = exported.glb ?? null; + scope.postMessage({ ok: true, report, output }, output ? [output] : []); + } + catch (error) { + scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "GLB loss report failed" }); + } +}; + +export {}; diff --git a/web/app/src/workers/glb-negative-cases-test.worker.ts b/web/app/src/workers/glb-negative-cases-test.worker.ts new file mode 100644 index 00000000..fc3655a8 --- /dev/null +++ b/web/app/src/workers/glb-negative-cases-test.worker.ts @@ -0,0 +1,26 @@ +import { importGLBSemantics } from "../../../protocol/glb-import"; + +const scope = self as unknown as { + onmessage: ((event: MessageEvent<{ cases: Array<{ id: string; bytes: ArrayBuffer }> }>) => void) | null; + postMessage(message: unknown): void; +}; + +scope.onmessage = (event) => { + try { + const results = event.data.cases.map((candidate) => { + try { + importGLBSemantics(candidate.bytes); + return { id: candidate.id, code: "ACCEPTED" }; + } + catch (error) { + return { id: candidate.id, code: error instanceof Error ? error.message.split(":", 1)[0] : "UNKNOWN" }; + } + }); + scope.postMessage({ ok: true, results }); + } + catch (error) { + scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "GLB negative worker failed" }); + } +}; + +export {}; diff --git a/web/app/src/workers/glb-recovery-test.worker.ts b/web/app/src/workers/glb-recovery-test.worker.ts new file mode 100644 index 00000000..f7267371 --- /dev/null +++ b/web/app/src/workers/glb-recovery-test.worker.ts @@ -0,0 +1,106 @@ +import { exportGLB, type GLBAssetBuffer } from "../../../protocol/glb-export"; +import { importGLBSemantics } from "../../../protocol/glb-import"; +import { + beginGLBRecoveryOperation, + cancelGLBRecoveryOperation, + commitGLBRecoveryOperation, + type GLBRecoveryOperation, + type GLBRecoveryReceipt, +} from "../../../protocol/glb-recovery"; +import type { MeshGeometryBuffer } from "../../../protocol/web-engine"; +import type { NonMeshGeometryChunk } from "../../../protocol/nonmesh-binary"; +import type { SceneSnapshotIR } from "../../../protocol/scene-ir"; + +interface RunCommand { + type: "run"; + requestId: string; + operation: GLBRecoveryOperation; + bytes: ArrayBuffer; + snapshot?: SceneSnapshotIR; + geometryBuffers?: MeshGeometryBuffer[]; + assetBuffers?: GLBAssetBuffer[]; + nonMeshGeometryBuffers?: NonMeshGeometryChunk[]; + baseRevision?: number; + workerGeneration?: number; +} + +interface CancelCommand { type: "cancel"; targetRequestId: string; } + +const cancelled = new Set(); +const running = new Map(); +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: unknown, transfer?: Transferable[]): void; +}; + +async function sha256Hex(value: ArrayBuffer | string): Promise { + const bytes = typeof value === "string" ? new TextEncoder().encode(value) : new Uint8Array(value); + const digest = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function yieldControl(): Promise { + return new Promise((resolve) => setTimeout(resolve, 2)); +} + +async function checkCancelled(request: RunCommand, receipt: GLBRecoveryReceipt): Promise { + await yieldControl(); + if (!cancelled.has(request.requestId)) return undefined; + running.delete(request.requestId); + const final = cancelGLBRecoveryOperation(receipt); + scope.postMessage({ requestId: request.requestId, ok: true, receipt: final, result: null }); + return final; +} + +async function run(request: RunCommand): Promise { + const inputSha256 = await sha256Hex(request.bytes); + let receipt = beginGLBRecoveryOperation({ + operationId: request.requestId.replace(/^glb-/, "").slice(0, 96), + operation: request.operation, + workerGeneration: request.workerGeneration ?? 1, + baseRevision: request.baseRevision ?? 0, + inputBytes: request.bytes.byteLength, + inputSha256, + }); + running.set(request.requestId, receipt); + try { + for (let index = 0; index < 4; index++) { + const cancelledReceipt = await checkCancelled(request, receipt); + if (cancelledReceipt) return; + } + let output: ArrayBuffer; + let outputBytes: number; + if (request.operation === "IMPORT") { + const semantics = importGLBSemantics(request.bytes); + output = new TextEncoder().encode(JSON.stringify(semantics)).buffer; + outputBytes = output.byteLength; + } + else { + if (!request.snapshot) throw new Error("GLB_RECOVERY_INVALID: export snapshot missing"); + const exported = exportGLB(request.snapshot, request.geometryBuffers ?? [], request.assetBuffers ?? [], request.nonMeshGeometryBuffers ?? []); + if (!exported.glb) throw new Error("GLB_EXPORT_BLOCKED: export produced no output"); + output = exported.glb; + outputBytes = output.byteLength; + } + const cancelledReceipt = await checkCancelled(request, receipt); + if (cancelledReceipt) return; + const outputSha256 = await sha256Hex(output); + receipt = commitGLBRecoveryOperation(receipt, { bytes: outputBytes, sha256: outputSha256 }); + running.delete(request.requestId); + cancelled.delete(request.requestId); + scope.postMessage({ requestId: request.requestId, ok: true, receipt, result: { output, outputSha256 } }, [output]); + } + catch (error) { + running.delete(request.requestId); + cancelled.delete(request.requestId); + scope.postMessage({ requestId: request.requestId, ok: false, error: error instanceof Error ? error.message : "GLB recovery operation failed" }); + } +} + +scope.onmessage = (event: MessageEvent) => { + if (event.data.type === "cancel") { + cancelled.add(event.data.targetRequestId); + return; + } + void run(event.data); +}; diff --git a/web/app/src/workers/io-format-recovery-test.worker.ts b/web/app/src/workers/io-format-recovery-test.worker.ts new file mode 100644 index 00000000..b803987a --- /dev/null +++ b/web/app/src/workers/io-format-recovery-test.worker.ts @@ -0,0 +1,62 @@ +import { importOBJ, serializeOBJ } from "../../../protocol/obj-import"; +import { exportBinarySTL } from "../../../protocol/stl-export"; +import { importSTL } from "../../../protocol/stl-import"; +import { importPLY, serializePLYAscii } from "../../../protocol/ply-import"; +import { beginIOFormatRecoveryOperation, blockIOFormatRecoveryOperation, cancelIOFormatRecoveryOperation, commitIOFormatRecoveryOperation, type IOFormat, type IOFormatRecoveryReceipt } from "../../../protocol/io-format-recovery"; + +interface RunCommand { type: "run"; requestId: string; format: IOFormat; operation: "IMPORT" | "EXPORT"; bytes: ArrayBuffer; workerGeneration?: number; baseRevision?: number; } +interface CancelCommand { type: "cancel"; targetRequestId: string; } +const cancelled = new Set(); +const running = new Map(); +const scope = self as unknown as { onmessage: ((event: MessageEvent) => void) | null; postMessage(message: unknown, transfer?: Transferable[]): void }; + +async function sha256Hex(bytes: ArrayBuffer): Promise { + const digest = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join(""); +} +const pause = () => new Promise((resolve) => setTimeout(resolve, 2)); + +async function checkCancel(request: RunCommand, receipt: IOFormatRecoveryReceipt): Promise { + await pause(); + if (!cancelled.has(request.requestId)) return false; + running.delete(request.requestId); + scope.postMessage({ requestId: request.requestId, ok: true, receipt: cancelIOFormatRecoveryOperation(receipt), result: null }); + return true; +} + +function outputFor(request: RunCommand): ArrayBuffer { + if (request.format === "OBJ") { + const document = importOBJ(request.bytes); + const serialized = serializeOBJ(document); + return new TextEncoder().encode(serialized.obj + serialized.mtl).buffer; + } + if (request.format === "STL") return exportBinarySTL(importSTL(request.bytes, { variant: "STL_BINARY", unitScale: 1 })); + return serializePLYAscii(importPLY(request.bytes, { format: "ascii" })); +} + +async function run(request: RunCommand): Promise { + const inputSha256 = await sha256Hex(request.bytes); + let receipt = beginIOFormatRecoveryOperation({ operationId: request.requestId.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 96), format: request.format, operation: request.operation, workerGeneration: request.workerGeneration ?? 1, baseRevision: request.baseRevision ?? 0, inputBytes: request.bytes.byteLength, inputSha256 }); + running.set(request.requestId, receipt); + try { + for (let index = 0; index < 4; index++) if (await checkCancel(request, receipt)) return; + if (request.bytes.byteLength > 512 * 1024) { + running.delete(request.requestId); scope.postMessage({ requestId: request.requestId, ok: true, receipt: blockIOFormatRecoveryOperation(receipt), result: null }); return; + } + const output = outputFor(request); + if (await checkCancel(request, receipt)) return; + const outputSha256 = await sha256Hex(output); + receipt = commitIOFormatRecoveryOperation(receipt, { bytes: output.byteLength, sha256: outputSha256 }); + running.delete(request.requestId); cancelled.delete(request.requestId); + scope.postMessage({ requestId: request.requestId, ok: true, receipt, result: { output, outputSha256 } }, [output]); + } + catch (error) { + running.delete(request.requestId); cancelled.delete(request.requestId); + if (error instanceof Error && /BUDGET_EXCEEDED|TRUNCATED|OUT_OF_RANGE/.test(error.message)) scope.postMessage({ requestId: request.requestId, ok: true, receipt: blockIOFormatRecoveryOperation(receipt), result: null }); + else scope.postMessage({ requestId: request.requestId, ok: false, error: error instanceof Error ? error.message : "IO format recovery operation failed" }); + } +} + +scope.onmessage = (event) => { if (event.data.type === "cancel") cancelled.add(event.data.targetRequestId); else void run(event.data); }; + +export {}; diff --git a/web/app/src/workers/obj-roundtrip-test.worker.ts b/web/app/src/workers/obj-roundtrip-test.worker.ts new file mode 100644 index 00000000..19b74eb4 --- /dev/null +++ b/web/app/src/workers/obj-roundtrip-test.worker.ts @@ -0,0 +1,26 @@ +import { createOBJLossReport, importOBJ, serializeOBJ } from "../../../protocol/obj-import"; + +interface Request { + obj: ArrayBuffer; + mtl?: ArrayBuffer; + textureAssets?: string[]; +} + +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: unknown): void; +}; + +scope.onmessage = (event) => { + try { + const imported = importOBJ(event.data.obj, event.data.mtl); + const lossReport = createOBJLossReport(imported, event.data.textureAssets ?? []); + const serialized = serializeOBJ(imported); + scope.postMessage({ ok: true, imported, lossReport, obj: serialized.obj, mtl: serialized.mtl }); + } + catch (error) { + scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "OBJ round-trip failed" }); + } +}; + +export {}; diff --git a/web/app/src/workers/ply-roundtrip-test.worker.ts b/web/app/src/workers/ply-roundtrip-test.worker.ts new file mode 100644 index 00000000..7b7e16b4 --- /dev/null +++ b/web/app/src/workers/ply-roundtrip-test.worker.ts @@ -0,0 +1,22 @@ +import { createPLYLossReport, importPLY, serializePLYAscii } from "../../../protocol/ply-import"; + +interface Request { bytes: ArrayBuffer; format?: "ascii" | "binary_little_endian"; } + +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: unknown, transfer?: Transferable[]): void; +}; + +scope.onmessage = (event) => { + try { + const imported = importPLY(event.data.bytes, { format: event.data.format }); + const lossReport = createPLYLossReport(imported); + const output = serializePLYAscii(imported); + scope.postMessage({ ok: true, imported, lossReport, output }, [output]); + } + catch (error) { + scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "PLY round-trip failed" }); + } +}; + +export {}; diff --git a/web/app/src/workers/script-host-call-test.worker.ts b/web/app/src/workers/script-host-call-test.worker.ts new file mode 100644 index 00000000..9d56e425 --- /dev/null +++ b/web/app/src/workers/script-host-call-test.worker.ts @@ -0,0 +1,25 @@ +import { parseScriptHostCall, SCRIPT_PERMISSIONS } from "../../../protocol/scripting-platform"; + +const digest = "a".repeat(64); +const permissions = new Set(SCRIPT_PERMISSIONS); +const call = (name: string, parameters: Record) => ({ schemaVersion: 1, requestId: `host:${name.toLowerCase()}`, scriptId: "clean", call: name, permission: name, parameters }); +self.onmessage = () => { + const accepted = [ + parseScriptHostCall(call("READ_MAIN", { revision: 3 }), permissions), + parseScriptHostCall(call("READ_ASSET", { path: "//assets/model.bin", expectedSha256: digest }), permissions), + parseScriptHostCall(call("WRITE_MAIN", { revision: 3, operation: "object.transform", payload: { objectId: "obj:1", x: 1 } }), permissions), + parseScriptHostCall(call("WRITE_ASSET", { path: "assets/out.bin", byteLength: 4, sha256: digest }), permissions), + parseScriptHostCall(call("SUBMIT_SERVER_JOB", { inputBlendSha256: digest, settingsSha256: digest }), permissions), + ]; + const blocked: Record = {}; + for (const [name, input] of [ + ["unknown", call("EXECUTE", {})], + ["permission", { ...call("READ_MAIN", { revision: 3 }), permission: "WRITE_MAIN" }], + ["fields", call("READ_MAIN", { revision: 3, extra: true })], + ["path", call("READ_ASSET", { path: "../escape", expectedSha256: digest })], + ] as const) { + try { parseScriptHostCall(input, permissions); blocked[name] = "ACCEPTED"; } + catch (error) { blocked[name] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + } + self.postMessage({ accepted: accepted.map((item) => ({ call: item.call, permission: item.permission, execution: item.execution, parameters: item.parameters })), blocked }); +}; diff --git a/web/app/src/workers/script-permission-policy-test.worker.ts b/web/app/src/workers/script-permission-policy-test.worker.ts new file mode 100644 index 00000000..10daea7d --- /dev/null +++ b/web/app/src/workers/script-permission-policy-test.worker.ts @@ -0,0 +1,17 @@ +import { parseScriptingManifest, resolveScriptPermissions } from "../../../protocol/scripting-platform"; + +const script = (permissions: string[]) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature: "b".repeat(128), keyId: "key:new", permissions, dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }); +const manifest = (permissions: string[]) => ({ schemaVersion: 1, scripts: [script(permissions)] }); +const decision = (permissions: string[], requested: unknown = []) => resolveScriptPermissions(manifest(permissions), "clean", requested); +self.onmessage = () => { + let unknownDeclaration = "ACCEPTED"; + try { parseScriptingManifest(manifest(["EXECUTE"])); } catch (error) { unknownDeclaration = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + self.postMessage({ + defaultGrant: decision(["READ_MAIN"]), + declaredGrant: decision(["WRITE_ASSET", "READ_MAIN"], ["READ_MAIN"]), + escalation: decision(["READ_MAIN"], ["WRITE_MAIN"]), + unknownRequest: decision(["READ_MAIN"], ["EXECUTE"]), + duplicateRequest: decision(["READ_MAIN"], ["READ_MAIN", "READ_MAIN"]), + unknownDeclaration, + }); +}; diff --git a/web/app/src/workers/script-sandbox-budget-test.worker.ts b/web/app/src/workers/script-sandbox-budget-test.worker.ts new file mode 100644 index 00000000..14374fb7 --- /dev/null +++ b/web/app/src/workers/script-sandbox-budget-test.worker.ts @@ -0,0 +1,12 @@ +import { parseScriptSandboxBudget, SCRIPT_SANDBOX_BUDGET } from "../../../protocol/scripting-platform"; + +const budget = { schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1024 * 1024, maxMessageBytes: 4096, maxOutputBytes: 8192 } as const; +self.onmessage = () => { + const accepted = parseScriptSandboxBudget(budget); + const blocked: Record = {}; + for (const [field, limit] of Object.entries({ cpuMs: SCRIPT_SANDBOX_BUDGET.maxCpuMs, wallMs: SCRIPT_SANDBOX_BUDGET.maxWallMs, memoryBytes: SCRIPT_SANDBOX_BUDGET.maxMemoryBytes, maxMessageBytes: SCRIPT_SANDBOX_BUDGET.maxMessageBytes, maxOutputBytes: SCRIPT_SANDBOX_BUDGET.maxOutputBytes })) { + try { parseScriptSandboxBudget({ ...budget, [field]: limit + 1 }); blocked[field] = "ACCEPTED"; } + catch (error) { blocked[field] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + } + self.postMessage({ accepted, blocked, execution: "DISABLED" }); +}; diff --git a/web/app/src/workers/script-sandbox-cancellation-test.worker.ts b/web/app/src/workers/script-sandbox-cancellation-test.worker.ts new file mode 100644 index 00000000..48ab2f46 --- /dev/null +++ b/web/app/src/workers/script-sandbox-cancellation-test.worker.ts @@ -0,0 +1,17 @@ +import { rejectLateScriptSandboxResult, terminateScriptSandboxJob } from "../../../protocol/scripting-platform"; + +interface SandboxRequest { mode: "RECEIPT" | "RUN" } +const running = { schemaVersion: 1, jobId: "sandbox:cancel", workerGeneration: 5, baseRevision: 11, mainRevisionBefore: 11, status: "RUNNING" as const }; + +self.onmessage = (event: MessageEvent) => { + if (event.data?.mode === "RECEIPT") { + const cancelled = terminateScriptSandboxJob(running, "CANCEL"); + let lateResult = "ACCEPTED"; + try { rejectLateScriptSandboxResult(cancelled); } catch (error) { lateResult = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + self.postMessage({ type: "receipt", cancelled, lateResult }); + return; + } + if (event.data?.mode === "RUN") { + setTimeout(() => self.postMessage({ type: "late-result", jobId: running.jobId, workerGeneration: running.workerGeneration, cacheKey: "sandbox-cache:cancel", payload: { revision: running.mainRevisionBefore + 1 } }), 50); + } +}; diff --git a/web/app/src/workers/script-sandbox-dispose-test.worker.ts b/web/app/src/workers/script-sandbox-dispose-test.worker.ts new file mode 100644 index 00000000..fcc1aba9 --- /dev/null +++ b/web/app/src/workers/script-sandbox-dispose-test.worker.ts @@ -0,0 +1,52 @@ +import { createScriptSandboxDisposeReceipt } from "../testing/script-sandbox-dispose"; + +let ports: { primary: MessagePort; peer: MessagePort } | null = null; +let timer: number | undefined; +let controller: AbortController | null = null; +let buffer: ArrayBuffer | null = null; +let pendingRequests = 0; +let cacheReferences = 0; +let disposeCount = 0; +let disposed = false; + +const activeResources = () => ({ + messagePorts: ports === null ? 0 : 2, + timers: timer === undefined ? 0 : 1, + abortControllers: controller === null ? 0 : 1, + transferableBuffers: buffer === null ? 0 : 1, + pendingRequests, + cacheReferences, +}); + +self.onmessage = (event: MessageEvent<{ type: "init" | "dispose" }>) => { + if (event.data?.type === "init") { + if (disposed || ports !== null) return; + const channel = new MessageChannel(); + ports = { primary: channel.port1, peer: channel.port2 }; + ports.primary.start(); + ports.peer.start(); + controller = new AbortController(); + buffer = new ArrayBuffer(64); + pendingRequests = 1; + cacheReferences = 1; + timer = setTimeout(() => self.postMessage({ type: "late-timer" }), 50); + self.postMessage({ type: "ready", resources: activeResources() }); + return; + } + if (event.data?.type === "dispose") { + disposeCount += 1; + if (!disposed) { + if (timer !== undefined) { clearTimeout(timer); timer = undefined; } + controller?.abort(); + controller = null; + ports?.primary.close(); + ports?.peer.close(); + ports = null; + buffer = null; + pendingRequests = 0; + cacheReferences = 0; + disposed = true; + } + self.postMessage({ type: "disposed", receipt: createScriptSandboxDisposeReceipt(disposeCount), resources: activeResources() }); + } +}; diff --git a/web/app/src/workers/script-sandbox-isolation-test.worker.ts b/web/app/src/workers/script-sandbox-isolation-test.worker.ts new file mode 100644 index 00000000..a33c2fa3 --- /dev/null +++ b/web/app/src/workers/script-sandbox-isolation-test.worker.ts @@ -0,0 +1,24 @@ +interface SandboxRequest { mode: "RECEIPTS" | "CRASH" | "TIMEOUT" } +import { rejectLateScriptSandboxResult, terminateScriptSandboxJob } from "../../../protocol/scripting-platform"; + +const running = { schemaVersion: 1, jobId: "sandbox:1", workerGeneration: 4, baseRevision: 9, mainRevisionBefore: 9, status: "RUNNING" as const }; + +self.onmessage = (event: MessageEvent) => { + if (event.data?.mode === "RECEIPTS") { + const crash = terminateScriptSandboxJob(running, "CRASH"); + const timeout = terminateScriptSandboxJob(running, "TIMEOUT"); + const cancel = terminateScriptSandboxJob(running, "CANCEL"); + let lateResult = "ACCEPTED"; + try { rejectLateScriptSandboxResult(cancel); } catch (error) { lateResult = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + self.postMessage({ crash, timeout, cancel, lateResult }); + return; + } + if (event.data?.mode === "CRASH") { + // The host must convert this Worker error into a terminated job receipt. + throw new Error("sandbox crash fixture"); + } + if (event.data?.mode === "TIMEOUT") { + // A real sandbox would be stopped by its wall-time supervisor before this publishes. + setTimeout(() => self.postMessage({ type: "late-result", jobId: "sandbox:timeout" }), 50); + } +}; diff --git a/web/app/src/workers/script-sandbox-recovery-test.worker.ts b/web/app/src/workers/script-sandbox-recovery-test.worker.ts new file mode 100644 index 00000000..807f023e --- /dev/null +++ b/web/app/src/workers/script-sandbox-recovery-test.worker.ts @@ -0,0 +1,39 @@ +import { appendScriptExecutionAudit, createScriptExecutionAudit, parseScriptExecutionAuditLog, parseScriptingManifest } from "../../../protocol/scripting-platform"; +import { createScriptSandboxRecoveryReceipt } from "../testing/script-sandbox-recovery"; + +const sourceSha256 = "a".repeat(64); +const signature = "b".repeat(128); +const manifest = { + schemaVersion: 1, + scripts: [{ id: "script:recovery", name: "Recovery", entryPath: "scripts/recovery.py", sourceByteLength: 128, sourceSha256, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }], +}; + +self.onmessage = async (event: MessageEvent<{ type: "recover" }>) => { + if (event.data?.type !== "recover") return; + const parsed = parseScriptingManifest(manifest); + const firstAudit = await createScriptExecutionAudit(parsed, "script:recovery", new Set(["key:trusted"]), { requestId: "sandbox-recovery:g4", requestedAt: "2026-08-19T00:00:00.000Z" }); + const firstLog = await appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, firstAudit); + const secondAudit = await createScriptExecutionAudit(parsed, "script:recovery", new Set(["key:trusted"]), { requestId: "sandbox-recovery:g5", requestedAt: "2026-08-19T00:00:01.000Z" }); + const auditLog = await appendScriptExecutionAudit(firstLog, secondAudit); + const checked = await parseScriptExecutionAuditLog(auditLog); + const first = checked.entries[0]; + const second = checked.entries[1]; + const receipt = createScriptSandboxRecoveryReceipt({ + previousGeneration: 4, + nextGeneration: 5, + mainRevisionBefore: 11, + mainRevisionAfter: 11, + sourceSha256, + manifestSha256: first.audit.manifestSha256, + audit: { + entries: 2, + first: { sequence: 1, requestId: first.audit.requestId, previousEntrySha256: first.previousEntrySha256, entrySha256: first.entrySha256, sourceSha256: first.audit.sourceSha256, manifestSha256: first.audit.manifestSha256 }, + second: { sequence: 2, requestId: second.audit.requestId, previousEntrySha256: second.previousEntrySha256, entrySha256: second.entrySha256, sourceSha256: second.audit.sourceSha256, manifestSha256: second.audit.manifestSha256 }, + }, + }); + let replayError = "ACCEPTED"; + try { await appendScriptExecutionAudit(auditLog, secondAudit); } catch (error) { replayError = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + let tamperError = "ACCEPTED"; + try { await parseScriptExecutionAuditLog({ ...auditLog, entries: auditLog.entries.map((entry, index) => index === 0 ? { ...entry, audit: { ...entry.audit, sourceSha256: "c".repeat(64) } } : entry) }); } catch (error) { tamperError = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + self.postMessage({ receipt, auditLog, replayError, tamperError, manifestSha256: first.audit.manifestSha256, scriptSourceSha256: first.audit.sourceSha256 }); +}; diff --git a/web/app/src/workers/script-sandbox-scope-test.worker.ts b/web/app/src/workers/script-sandbox-scope-test.worker.ts new file mode 100644 index 00000000..2dc62e41 --- /dev/null +++ b/web/app/src/workers/script-sandbox-scope-test.worker.ts @@ -0,0 +1,12 @@ +import { parseScriptSandboxScope } from "../../../protocol/scripting-platform"; + +const deniedScope = { schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false } as const; +self.onmessage = () => { + const accepted = parseScriptSandboxScope(deniedScope); + const blocked: Record = {}; + for (const capability of ["dom", "hostWorker", "opfs", "indexedDB", "network"] as const) { + try { parseScriptSandboxScope({ ...deniedScope, [capability]: true }); blocked[capability] = "ACCEPTED"; } + catch (error) { blocked[capability] = error instanceof Error ? error.message.split(":", 1)[0] : String(error); } + } + self.postMessage({ accepted, blocked, execution: "DISABLED" }); +}; diff --git a/web/app/src/workers/script-signature-negative-test.worker.ts b/web/app/src/workers/script-signature-negative-test.worker.ts new file mode 100644 index 00000000..e8474f93 --- /dev/null +++ b/web/app/src/workers/script-signature-negative-test.worker.ts @@ -0,0 +1,17 @@ +import { verifyScriptManifestSignature } from "../../../protocol/scripting-platform"; + +const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8"; +const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804"; +const script = (id = "clean", overrides: Record = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const manifest = (scripts = [script()]) => ({ schemaVersion: 1, scripts }); +const key = (overrides: Record = {}) => ({ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides }); +const policy = (overrides: Record = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [key()], ...overrides }); +self.onmessage = async () => { + const at = "2026-08-18T12:00:00.000Z"; + const missing = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [] }), at); + const expired = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notAfter: "2026-06-01T00:00:00.000Z" })] }), at); + const notYetValid = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notBefore: "2026-09-01T00:00:00.000Z" })] }), at); + const publisherMismatch = await verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ publisher: "Other" })] }), at); + const swapped = await verifyScriptManifestSignature(manifest([script("other")]), "other", policy(), at); + self.postMessage({ missing, expired, notYetValid, publisherMismatch, swapped }); +}; diff --git a/web/app/src/workers/script-signature-test.worker.ts b/web/app/src/workers/script-signature-test.worker.ts new file mode 100644 index 00000000..967e8eaf --- /dev/null +++ b/web/app/src/workers/script-signature-test.worker.ts @@ -0,0 +1,15 @@ +import { verifyScriptManifestSignature } from "../../../protocol/scripting-platform"; + +const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8"; +const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804"; +const script = (overrides: Record = {}) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const policy = (overrides: Record = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [{ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z" }], ...overrides }); +const manifest = { schemaVersion: 1, scripts: [script()] }; + +self.onmessage = async () => { + const valid = await verifyScriptManifestSignature(manifest, "clean", policy(), "2026-08-18T12:00:00.000Z"); + const sourceChanged = await verifyScriptManifestSignature({ schemaVersion: 1, scripts: [script({ sourceSha256: "d".repeat(64) })] }, "clean", policy(), "2026-08-18T12:00:00.000Z"); + const signatureChanged = await verifyScriptManifestSignature({ schemaVersion: 1, scripts: [script({ signature: `${signature.slice(0, -1)}${signature.endsWith("0") ? "1" : "0"}` })] }, "clean", policy(), "2026-08-18T12:00:00.000Z"); + const revoked = await verifyScriptManifestSignature(manifest, "clean", policy({ keys: [{ ...policy().keys[0], status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" }] }), "2026-08-18T12:00:00.000Z"); + self.postMessage({ valid, sourceChanged, signatureChanged, revoked }); +}; diff --git a/web/app/src/workers/script-trust-policy-test.worker.ts b/web/app/src/workers/script-trust-policy-test.worker.ts new file mode 100644 index 00000000..c237ab0c --- /dev/null +++ b/web/app/src/workers/script-trust-policy-test.worker.ts @@ -0,0 +1,19 @@ +import { parseScriptTrustPolicy, resolveScriptSigner, serializeScriptTrustPolicy } from "../../../protocol/scripting-platform"; + +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const script = (id = "clean", overrides: Record = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const key = (keyId: string, overrides: Record = {}) => ({ keyId, publisher: "Team", algorithm: "ED25519", publicKey: "c".repeat(64), status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides }); +const policy = (keys = [key("key:new")], overrides: Record = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys, ...overrides }); +const manifest = { schemaVersion: 1, scripts: [script()] }; + +function errorCode(value: unknown): string { + try { parseScriptTrustPolicy(value); return "ACCEPTED"; } + catch (error) { return error instanceof Error ? error.message : String(error); } +} + +self.onmessage = () => { + const rotated = policy([key("key:new", { replaces: "key:old" }), key("key:old", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]); + const parsed = parseScriptTrustPolicy(rotated); + self.postMessage({ eligible: resolveScriptSigner(manifest, "clean", parsed, "2026-08-18T12:00:00.000Z"), revoked: resolveScriptSigner(manifest, "clean", policy([key("key:new", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]), "2026-08-18T12:00:00.000Z").trust, crossPublisher: errorCode(policy([key("key:new", { replaces: "key:old" }), key("key:old", { publisher: "Other" })])), policyExpired: resolveScriptSigner(manifest, "clean", policy([key("key:new")], { expiresAt: "2026-06-01T00:00:00.000Z" }), "2026-08-18T12:00:00.000Z").trust, rotationSerialized: serializeScriptTrustPolicy(parsed).length }); +}; diff --git a/web/app/src/workers/scripting-manifest-budget-test.worker.ts b/web/app/src/workers/scripting-manifest-budget-test.worker.ts new file mode 100644 index 00000000..b9438c8f --- /dev/null +++ b/web/app/src/workers/scripting-manifest-budget-test.worker.ts @@ -0,0 +1,52 @@ +import { parseScriptingManifest, SCRIPTING_BUDGET } from "../../../protocol/scripting-platform"; + +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const script = (id: string, overrides: Record = {}) => ({ + id, + name: id, + entryPath: `scripts/${id}.py`, + sourceByteLength: 128, + sourceSha256: digest, + publisher: "local", + signature, + keyId: "key:local", + permissions: ["READ_MAIN"], + dependencies: [], + module: false, + cpuMs: 1000, + memoryBytes: 1024 * 1024, + wallMs: 5000, + network: false, + autorun: false, + driverExpressions: false, + addonInstall: false, + ...overrides, +}); +const manifest = (scripts = [script("clean")]) => ({ schemaVersion: 1, scripts }); + +function denied(value: unknown): string { + try { + parseScriptingManifest(value); + return "ACCEPTED"; + } + catch (error) { + return error instanceof Error ? error.message : String(error); + } +} + +self.onmessage = () => { + const valid = parseScriptingManifest(manifest([ + script("base", { entryPath: "//scripts/../scripts/base.py" }), + script("clean", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//deps/base.py" }] }), + ])); + self.postMessage({ + valid: [valid.scripts.length, valid.scripts[0].entryPath, valid.scripts[1].dependencies[0].sourcePath, valid.scripts.reduce((total, item) => total + item.sourceByteLength, 0)], + count: denied(manifest(Array.from({ length: SCRIPTING_BUDGET.maxScripts + 1 }, (_, index) => script(`script-${index}`)))), + totalBytes: denied(manifest([script("large", { sourceByteLength: SCRIPTING_BUDGET.maxSourceBytes }), script("overflow", { sourceByteLength: 1 })])), + module: denied(manifest([script("module", { module: true })])), + path: denied(manifest([script("escape", { entryPath: "../escape.py" })])), + dependency: denied(manifest([script("duplicate", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "deps/a.py" }, { id: "base", sourceSha256: digest, sourcePath: "deps/b.py" }] }), script("base")])), + permission: denied(manifest([script("unknown", { permissions: ["EXECUTE"] })])), + }); +}; diff --git a/web/app/src/workers/scripting-manifest-canonical-test.worker.ts b/web/app/src/workers/scripting-manifest-canonical-test.worker.ts new file mode 100644 index 00000000..0302cc46 --- /dev/null +++ b/web/app/src/workers/scripting-manifest-canonical-test.worker.ts @@ -0,0 +1,46 @@ +import { canonicalizeScriptingManifest, serializeScriptingManifest } from "../../../protocol/scripting-platform"; + +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const script = (id: string, overrides: Record = {}) => ({ + id, + name: id, + entryPath: `scripts/${id}.py`, + sourceByteLength: 128, + sourceSha256: digest, + publisher: "local", + signature, + keyId: "key:local", + permissions: ["READ_MAIN"], + dependencies: [], + module: false, + cpuMs: 1000, + memoryBytes: 1024 * 1024, + wallMs: 5000, + network: false, + autorun: false, + driverExpressions: false, + addonInstall: false, + ...overrides, +}); + +self.onmessage = () => { + const first = { + schemaVersion: 1, + scripts: [ + script("zeta", { permissions: ["WRITE_ASSET", "READ_MAIN"], dependencies: [{ id: "alpha", sourceSha256: digest, sourcePath: "deps/alpha.py" }, { id: "beta", sourceSha256: digest, sourcePath: "deps/beta.py" }] }), + script("alpha", { permissions: ["SUBMIT_SERVER_JOB", "READ_ASSET"] }), + script("beta"), + ], + }; + const second = { + schemaVersion: 1, + scripts: [ + { ...first.scripts[1], permissions: [...first.scripts[1].permissions].reverse(), ignored: "removed" }, + { ...first.scripts[0], permissions: [...first.scripts[0].permissions].reverse(), dependencies: [...first.scripts[0].dependencies].reverse() }, + first.scripts[2], + ], + }; + const canonical = canonicalizeScriptingManifest(second); + self.postMessage({ equal: serializeScriptingManifest(first) === serializeScriptingManifest(second), firstId: canonical.scripts[0].id, firstPermission: canonical.scripts[0].permissions[0], dependencyOrder: canonical.scripts[2].dependencies.map((dependency) => dependency.id), unknownDropped: !("ignored" in canonical.scripts[0]) }); +}; diff --git a/web/app/src/workers/scripting-platform-test.worker.ts b/web/app/src/workers/scripting-platform-test.worker.ts index 827bf68a..c19db05a 100644 --- a/web/app/src/workers/scripting-platform-test.worker.ts +++ b/web/app/src/workers/scripting-platform-test.worker.ts @@ -1,7 +1,7 @@ import { appendScriptExecutionAudit, createScriptExecutionAudit, gateScriptExecution, gateServerScriptJob, parseScriptExecutionAuditLog, parseScriptingManifest, platformCapabilities, SCRIPTING_BUDGET } from "../../../protocol/scripting-platform"; const sha = "a".repeat(64); const signature = "b".repeat(128); -const script = { id: "script:clean", name: "Clean", entryPath: "scripts/clean.py", sourceSha256: sha, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }; +const script = { id: "script:clean", name: "Clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: sha, publisher: "Team", signature, keyId: "key:trusted", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }; const base = { schemaVersion: 1, scripts: [script] }; self.onmessage = async () => { diff --git a/web/app/src/workers/stl-edge-test.worker.ts b/web/app/src/workers/stl-edge-test.worker.ts new file mode 100644 index 00000000..ef00c16c --- /dev/null +++ b/web/app/src/workers/stl-edge-test.worker.ts @@ -0,0 +1,24 @@ +import { importSTL, type STLVariant } from "../../../protocol/stl-import"; + +interface Request { + cases: Array<{ id: string; bytes: ArrayBuffer; variant: STLVariant; unitScale: number }>; +} + +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: unknown): void; +}; + +scope.onmessage = (event) => { + const results = event.data.cases.map((candidate) => { + try { + return { id: candidate.id, status: "ACCEPTED", result: importSTL(candidate.bytes, { variant: candidate.variant, unitScale: candidate.unitScale }) }; + } + catch (error) { + return { id: candidate.id, status: "BLOCKED", code: error instanceof Error ? error.message.split(":", 1)[0] : "STL_IMPORT_FAILED" }; + } + }); + scope.postMessage({ ok: true, results }); +}; + +export {}; diff --git a/web/app/src/workers/stl-roundtrip-test.worker.ts b/web/app/src/workers/stl-roundtrip-test.worker.ts new file mode 100644 index 00000000..4c9eb326 --- /dev/null +++ b/web/app/src/workers/stl-roundtrip-test.worker.ts @@ -0,0 +1,23 @@ +import { exportBinarySTL, createSTLLossReport } from "../../../protocol/stl-export"; +import { importSTL } from "../../../protocol/stl-import"; + +interface Request { bytes: ArrayBuffer; unitScale: number; sourceMaterialCount: number; } + +const scope = self as unknown as { + onmessage: ((event: MessageEvent) => void) | null; + postMessage(message: unknown, transfer?: Transferable[]): void; +}; + +scope.onmessage = (event) => { + try { + const imported = importSTL(event.data.bytes, { variant: "STL_BINARY", unitScale: event.data.unitScale }); + const output = exportBinarySTL(imported); + const lossReport = createSTLLossReport(event.data.sourceMaterialCount); + scope.postMessage({ ok: true, imported, output, lossReport }, [output]); + } + catch (error) { + scope.postMessage({ ok: false, error: error instanceof Error ? error.message : "STL round-trip failed" }); + } +}; + +export {}; diff --git a/web/app/src/workers/storage.worker.ts b/web/app/src/workers/storage.worker.ts index d29c0708..8b908c59 100644 --- a/web/app/src/workers/storage.worker.ts +++ b/web/app/src/workers/storage.worker.ts @@ -751,7 +751,8 @@ async function readAssetRow(projectId: string, sha256: string): Promise { +async function putAsset(projectId: string, data: ArrayBuffer, mimeType: string, sourcePath?: string, faultAt?: "quota"): Promise { + if (faultAt === "quota") throw new Error("QuotaExceededError: injected OPFS quota exhaustion"); projectLayout(projectId); if (data.byteLength === 0) throw new Error("Asset data is empty"); if (!/^[A-Za-z0-9.+-]+\/[A-Za-z0-9.+-]+$/.test(mimeType)) throw new Error("Invalid asset MIME type"); @@ -1498,7 +1499,7 @@ async function handleRequest(event: MessageEvent): Promise else if (command.type === "saveSnapshot") result = await withProjectTransaction(command.projectId, () => saveSnapshot(command.projectId, command.revision, command.buffer, command.maxCount, command.maxBytes)); else if (command.type === "listSnapshots") result = await listSnapshots(command.projectId); else if (command.type === "readSnapshot") result = await readSnapshot(command.projectId, command.revision); - else if (command.type === "putAsset") result = await putAsset(command.projectId, command.data, command.mimeType, command.sourcePath); + else if (command.type === "putAsset") result = await putAsset(command.projectId, command.data, command.mimeType, command.sourcePath, command.faultAt); else if (command.type === "readAsset") result = await readAsset(command.projectId, command.sha256); else if (command.type === "listAssets") result = await listAssets(command.projectId); else if (command.type === "commitTexturePaintTile") result = await withProjectTransaction(command.commit.target.projectId, () => commitTexturePaintTile(command.commit)); diff --git a/web/package.json b/web/package.json index aaee8939..c2a1f4da 100644 --- a/web/package.json +++ b/web/package.json @@ -144,12 +144,76 @@ "test:library-operation-inventory": "node ../tools/web/check-library-operation-inventory.mjs", "test:library-operation-identity": "node --test tests/unit/library-operation-identity.test.mjs", "test:library-append-desktop": "node ../tools/web/check-library-append-fixture.mjs", - "test:library-main-append": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-append-main.spec.ts", + "test:library-append-wasm": "node --test tests/unit/library-append-wasm.test.mjs", + "test:library-append-chromium": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-append-main.spec.ts", + "test:library-main-append": "node ../tools/web/check-library-main-append.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/library-append-main.spec.ts", + "test:library-link-desktop": "node ../tools/web/check-library-link-fixture.mjs", + "test:library-link-wasm": "node --test tests/unit/library-linked-mutation.test.mjs tests/unit/library-linked-reload.test.mjs tests/unit/library-linked-missing.test.mjs", + "test:library-link-chromium": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-link-chromium.spec.ts", + "test:library-linked-mutation": "node --test tests/unit/library-linked-mutation.test.mjs", + "test:library-linked-reload": "node --test tests/unit/library-linked-reload.test.mjs", + "test:library-linked-missing": "node --test tests/unit/library-linked-missing.test.mjs", + "test:library-override-desktop": "node ../tools/web/check-library-override-fixture.mjs", + "test:library-override-wasm": "node --test tests/unit/library-override-writer.test.mjs tests/unit/library-override-freshness.test.mjs", + "test:library-override-chromium": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-override-chromium.spec.ts", + "test:library-override-writer": "node --test tests/unit/library-override-writer.test.mjs", + "test:library-override-freshness": "node --test tests/unit/library-override-freshness.test.mjs", + "test:library-negative-cases": "node --test tests/unit/library-negative-cases.test.mjs", + "test:library-operation-commands": "node ../tools/web/check-library-operation-commands.mjs", + "test:library-source-origin": "node --test tests/unit/library-source-origin.test.mjs", + "test:library-path-normalization": "node --test tests/unit/library-path-normalization.test.mjs", + "test:library-path-security": "node --test tests/unit/library-path-security.test.mjs", + "test:library-link-safety": "node --test tests/unit/library-link-safety.test.mjs", + "test:library-metadata-first": "node --test tests/unit/library-metadata-first.test.mjs", + "test:library-archive-budget": "node --test tests/unit/library-archive-budget.test.mjs", + "test:library-archive-conflicts": "node --test tests/unit/library-archive-conflicts.test.mjs", + "test:library-archive-cancellation": "node --test tests/unit/library-archive-cancellation.test.mjs", "test:asset-library": "playwright test --config playwright.config.ts -g \"N-023 asset\"", "test:library-main-reader": "node ../tools/web/check-library-main-reader.mjs", "test:editor-workflow": "playwright test --config playwright.config.ts -g \"N-024 editor\"", "test:editor-main-reader": "node ../tools/web/check-editor-main-reader.mjs", "test:scripting-platform": "playwright test --config playwright.config.ts -g \"N-025 script\"", + "test:script-manifest-budgets": "node --test tests/unit/script-manifest-budgets.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-manifest-budgets.spec.ts", + "test:script-manifest-canonical": "node --test tests/unit/script-manifest-canonical.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-manifest-canonical.spec.ts", + "test:script-trust-policy": "node --test tests/unit/script-trust-policy.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-trust-policy.spec.ts", + "test:script-signature": "node --test --test-name-pattern=M13-02D tests/unit/script-trust-policy.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-signature.spec.ts", + "test:script-permission-policy": "node --test tests/unit/script-permission-policy.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-permission-policy.spec.ts", + "test:script-signature-negative": "node --test tests/unit/script-signature-negative.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-signature-negative.spec.ts", + "test:script-sandbox-scope": "node --test tests/unit/script-sandbox-scope.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-sandbox-scope.spec.ts", + "test:script-sandbox-budget": "node --test tests/unit/script-sandbox-budget.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-sandbox-budget.spec.ts", + "test:script-host-call": "node --test tests/unit/script-host-call.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-host-call.spec.ts", + "test:script-sandbox-isolation": "node --test tests/unit/script-sandbox-isolation.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-sandbox-isolation.spec.ts", + "test:script-sandbox-cancellation": "node --test tests/unit/script-sandbox-cancellation.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-sandbox-cancellation.spec.ts", + "test:script-sandbox-dispose": "node --test tests/unit/script-sandbox-dispose.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-sandbox-dispose.spec.ts", + "test:script-sandbox-recovery": "node --test tests/unit/script-sandbox-recovery.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/script-sandbox-recovery.spec.ts", + "test:server-job-directory": "node --test tests/unit/server-job-isolation.test.mjs && node ../tools/web/check-server-job-isolation.mjs", + "test:server-job-workspace": "node --test tests/unit/server-job-isolation.test.mjs && node ../tools/web/check-server-job-workspace.mjs", + "test:server-job-resource-budget": "node --test tests/unit/server-job-resource-budget.test.mjs && node ../tools/web/check-server-job-resource-budget.mjs", + "test:server-job-network-policy": "node --test tests/unit/server-job-network-policy.test.mjs && node ../tools/web/check-server-job-network-policy.mjs", + "test:server-job-startup": "node ../tools/web/check-server-job-startup.mjs", + "test:server-job-output-redaction": "node --test tests/unit/server-job-output.test.mjs && node ../tools/web/check-server-job-output-redaction.mjs", + "test:server-job-cancellation": "node --test tests/unit/server-job-process.test.mjs && node ../tools/web/check-server-job-cancellation.mjs", + "test:server-job-fault-codes": "node --test tests/unit/server-job-fault.test.mjs && node ../tools/web/check-server-job-fault-codes.mjs", + "test:server-job-result-binding": "node --test tests/unit/server-job-result-binding.test.mjs && node ../tools/web/check-server-job-result-binding.mjs", + "test:server-job-idempotency": "node --test tests/unit/server-job-idempotency.test.mjs && node ../tools/web/check-server-job-idempotency.mjs", + "test:csp-policy": "node ../tools/web/check-csp-policy.mjs", + "test:csp-resource-policy": "node ../tools/web/check-csp-resource-policy.mjs", + "test:dependency-inventory": "node ../tools/web/check-dependency-inventory.mjs", + "test:dependency-severity-policy": "node ../tools/web/check-dependency-severity-policy.mjs", + "test:supply-chain-binding": "node ../tools/web/check-supply-chain-binding.mjs", + "test:malicious-input-matrix": "node ../tools/web/check-malicious-input-matrix.mjs", + "test:fuzz-regression": "node ../tools/web/check-fuzz-regression.mjs", + "test:script-audit-integrity": "node --test tests/unit/script-audit-integrity.test.mjs && node ../tools/web/check-script-audit-integrity.mjs", + "test:chromium-freeze": "node ../tools/web/check-chromium-release-freeze.mjs", + "test:probe-identity": "node ../tools/web/check-probe-identity.mjs", + "test:chromium-webgpu-boundary": "node ../tools/web/check-chromium-webgpu-boundary.mjs", + "test:chromium-device-budget": "node --test tests/unit/device-budget.test.mjs && node ../tools/web/check-chromium-device-budget.mjs", + "test:chromium-dpr-consistency": "node --test tests/unit/viewport-dpr.test.mjs && node ../tools/web/check-chromium-dpr-consistency.mjs", + "test:chromium-pointer-contract": "node --test tests/unit/pointer-contract.test.mjs && node ../tools/web/check-chromium-pointer-contract.mjs", + "test:chromium-ime-guard": "node --test tests/unit/ime-composition.test.mjs && node ../tools/web/check-chromium-ime-guard.mjs", + "test:chromium-keymap-fixture": "node --test tests/unit/keyboard-contract.test.mjs && node ../tools/web/check-chromium-keymap-fixture.mjs", + "test:chromium-input-modal": "node --test tests/unit/input-modal.test.mjs && node ../tools/web/check-chromium-input-modal.mjs", + "test:firefox-quick": "npm run typecheck && node ../tools/web/check-firefox-quick.mjs", "test:script-main-reader": "node ../tools/web/check-script-main-reader.mjs", "test:scripting-isolation": "node ../tools/web/check-scripting-isolation.mjs", "test:release-gate": "playwright test --config playwright.config.ts -g \"N-026 release\"", @@ -195,6 +259,7 @@ "test:rc-known-limitations": "node ../tools/web/check-rc-known-limitations.mjs", "test:rc-release-recovery": "node ../tools/web/check-rc-release-recovery.mjs", "test:rc-docs": "npm run test:rc-release-notes && npm run test:rc-known-limitations && npm run test:rc-release-recovery", + "test:ply-mapping": "node --test tests/unit/ply-import.test.mjs && node ../tools/web/check-ply-mapping-fixtures.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/ply-web-roundtrip.spec.ts", "diagnose:depsgraph": "node ../tools/web/diagnose-depsgraph.mjs" }, "dependencies": { diff --git a/web/protocol/archive-conflicts.ts b/web/protocol/archive-conflicts.ts new file mode 100644 index 00000000..2b3f64c3 --- /dev/null +++ b/web/protocol/archive-conflicts.ts @@ -0,0 +1,97 @@ +import type { ErrorCode } from "./error"; + +export const ARCHIVE_CONFLICT_SCHEMA = 1 as const; +export interface ArchiveConflictRangeIR { + path: string; + compressedBytes: number; + uncompressedBytes: number; + compressedOffset: number; +} +export interface ArchiveConflictRequestIR { + schemaVersion: typeof ARCHIVE_CONFLICT_SCHEMA; + byteLength: number | null; + ranges: ArchiveConflictRangeIR[]; +} +export interface ArchiveConflictValidationIR { + status: "VALID"; + totalCompressedBytes: number; + totalUncompressedBytes: number; + nonOverlapping: true; + uniquePaths: true; + noPrefixConflicts: true; +} + +export class ArchiveConflictError extends Error { + readonly code: ErrorCode; + readonly path?: string; + constructor(message: string, path?: string) { super(`IO_ARCHIVE_UNSAFE: ${message}`); this.name = "ArchiveConflictError"; this.code = "IO_ARCHIVE_UNSAFE"; this.path = path; } +} + +export const ARCHIVE_CONFLICT_BUDGET = { + maxEntries: 100_000, + maxEntryBytes: 2 * 1024 * 1024 * 1024, + maxArchiveBytes: 4 * 1024 * 1024 * 1024, + maxCompressionRatio: 100, +} as const; + +const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/; +const DRIVE_PATH = /^[A-Za-z]:[\\/]/; +const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ArchiveConflictError(`${path} must be an object`, path); + return value as Record; +} +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new ArchiveConflictError(`${path} contains undeclared fields`, path); +} +function integer(value: unknown, path: string, minimum = 0, maximum = Number.MAX_SAFE_INTEGER): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new ArchiveConflictError(`${path} is outside its bounded range`, path); + return value; +} +function archivePath(value: unknown, path: string): string { + if (typeof value !== "string" || value.length === 0 || value.length > 2_048 || CONTROL_CHARACTER.test(value)) throw new ArchiveConflictError(`${path} is invalid`, path); + if (value.startsWith("/") || value.startsWith("\\") || DRIVE_PATH.test(value) || URI_SCHEME.test(value) || value.includes("\\")) throw new ArchiveConflictError(`${path} escapes the project`, path); + const segments: string[] = []; + for (const segment of value.normalize("NFC").split("/")) { + if (!segment || segment === ".") continue; + if (segment === "..") { if (segments.length === 0) throw new ArchiveConflictError(`${path} escapes the project`, path); segments.pop(); continue; } + segments.push(segment); + } + const result = segments.join("/"); + if (!result) throw new ArchiveConflictError(`${path} is empty`, path); + return result; +} + +export function validateArchiveConflicts(value: unknown): ArchiveConflictValidationIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "byteLength", "ranges"], "input"); + if (input.schemaVersion !== ARCHIVE_CONFLICT_SCHEMA) throw new ArchiveConflictError("unsupported archive conflict schema", "input.schemaVersion"); + const byteLength = input.byteLength === null ? null : integer(input.byteLength, "input.byteLength", 1, ARCHIVE_CONFLICT_BUDGET.maxArchiveBytes); + if (!Array.isArray(input.ranges) || input.ranges.length === 0 || input.ranges.length > ARCHIVE_CONFLICT_BUDGET.maxEntries) throw new ArchiveConflictError("ranges exceeds its bound", "input.ranges"); + const ranges = input.ranges.map((value, index) => { + const path = `ranges[${index}]`; const item = record(value, path); exactKeys(item, ["path", "compressedBytes", "uncompressedBytes", "compressedOffset"], path); + return { + path: archivePath(item.path, `${path}.path`), + compressedBytes: integer(item.compressedBytes, `${path}.compressedBytes`, 0, ARCHIVE_CONFLICT_BUDGET.maxEntryBytes), + uncompressedBytes: integer(item.uncompressedBytes, `${path}.uncompressedBytes`, 0, ARCHIVE_CONFLICT_BUDGET.maxEntryBytes), + compressedOffset: integer(item.compressedOffset, `${path}.compressedOffset`, 0, ARCHIVE_CONFLICT_BUDGET.maxArchiveBytes), + }; + }); + const paths = new Set(); let totalCompressedBytes = 0; let totalUncompressedBytes = 0; + for (const range of ranges) { + if (paths.has(range.path) || [...paths].some((existing) => existing.startsWith(`${range.path}/`) || range.path.startsWith(`${existing}/`))) throw new ArchiveConflictError(`duplicate or file/directory prefix conflict at ${range.path}`, "input.ranges"); + paths.add(range.path); + if (range.uncompressedBytes > 0 && (range.compressedBytes === 0 || range.uncompressedBytes / range.compressedBytes > ARCHIVE_CONFLICT_BUDGET.maxCompressionRatio)) throw new ArchiveConflictError(`compression ratio exceeds the budget at ${range.path}`, "input.ranges"); + totalCompressedBytes += range.compressedBytes; totalUncompressedBytes += range.uncompressedBytes; + if (!Number.isSafeInteger(totalCompressedBytes) || !Number.isSafeInteger(totalUncompressedBytes) || totalCompressedBytes > ARCHIVE_CONFLICT_BUDGET.maxArchiveBytes || totalUncompressedBytes > ARCHIVE_CONFLICT_BUDGET.maxArchiveBytes) throw new ArchiveConflictError("archive total byte budget exceeded", "input.ranges"); + if (range.compressedOffset + range.compressedBytes > ARCHIVE_CONFLICT_BUDGET.maxArchiveBytes || byteLength !== null && range.compressedOffset + range.compressedBytes > byteLength) throw new ArchiveConflictError(`range for ${range.path} exceeds the archive`, "input.ranges"); + } + const ordered = [...ranges].sort((left, right) => left.compressedOffset - right.compressedOffset); + for (let index = 1; index < ordered.length; index++) { + const previous = ordered[index - 1]; const current = ordered[index]; + if (current.compressedOffset < previous.compressedOffset + previous.compressedBytes) throw new ArchiveConflictError(`compressed ranges overlap at ${current.path}`, "input.ranges"); + } + return { status: "VALID", totalCompressedBytes, totalUncompressedBytes, nonOverlapping: true, uniquePaths: true, noPrefixConflicts: true }; +} diff --git a/web/protocol/archive-extraction-recovery.ts b/web/protocol/archive-extraction-recovery.ts new file mode 100644 index 00000000..f497852d --- /dev/null +++ b/web/protocol/archive-extraction-recovery.ts @@ -0,0 +1,50 @@ +import type { ErrorCode } from "./error"; +import { + ArchiveExtractionError, + runArchiveExtractionTransaction as runArchiveExtractionTransactionBase, + type ArchiveExtractionEntryReader, + type ArchiveExtractionReceiptIR, + type ArchiveExtractionStorage, +} from "./archive-extraction-transaction"; + +export type ArchiveExtractionResourceFaultCode = Extract; + +/** Maps platform-specific allocation failures to the stable archive error contract. */ +export function archiveExtractionResourceFaultCode(error: unknown): ArchiveExtractionResourceFaultCode | undefined { + if (typeof error !== "object" || error === null) return undefined; + const candidate = error as { code?: unknown; name?: unknown; message?: unknown }; + if (candidate.code === "STORAGE_QUOTA" || candidate.code === "WASM_OUT_OF_MEMORY") return candidate.code; + if (candidate.name === "QuotaExceededError" || candidate.name === "NotEnoughSpaceError") return "STORAGE_QUOTA"; + if (candidate.name === "OutOfMemoryError") return "WASM_OUT_OF_MEMORY"; + if (candidate.name === "RangeError" && typeof candidate.message === "string" && /out[ -]?of[ -]?memory|oom/i.test(candidate.message)) { + return "WASM_OUT_OF_MEMORY"; + } + return undefined; +} + +/** + * Runs the existing atomic extraction transaction and normalizes resource failures only after + * the base transaction has removed staging and revalidated the committed identity. + */ +export async function runArchiveExtractionTransaction( + value: unknown, + storage: ArchiveExtractionStorage, + readEntry: ArchiveExtractionEntryReader, + signal: AbortSignal, +): Promise { + try { + return await runArchiveExtractionTransactionBase(value, storage, readEntry, signal); + } + catch (error) { + const code = archiveExtractionResourceFaultCode(error); + if (code) throw new ArchiveExtractionError(code, "archive extraction released staging after a resource fault"); + throw error; + } +} + +export type { + ArchiveExtractionEntryIR, + ArchiveExtractionRequestIR, + ArchiveExtractionReceiptIR, + ArchiveExtractionStorage, +} from "./archive-extraction-transaction"; diff --git a/web/protocol/archive-extraction-transaction.ts b/web/protocol/archive-extraction-transaction.ts new file mode 100644 index 00000000..9aa98980 --- /dev/null +++ b/web/protocol/archive-extraction-transaction.ts @@ -0,0 +1,258 @@ +import type { ErrorCode } from "./error"; + +export const ARCHIVE_EXTRACTION_SCHEMA = 1 as const; + +export interface ArchiveProjectIdentityIR { + projectId: string; + revision: number; + sha256: string; +} + +export interface ArchiveExtractionEntryIR { + path: string; + uncompressedBytes: number; + sha256: string; +} + +export interface ArchiveExtractionRequestIR { + schemaVersion: typeof ARCHIVE_EXTRACTION_SCHEMA; + transactionId: string; + archiveId: string; + committed: ArchiveProjectIdentityIR; + candidate: ArchiveProjectIdentityIR; + entries: ArchiveExtractionEntryIR[]; +} + +export interface ArchiveExtractionStorage { + readCommitted(projectId: string): Promise; + createStaging(transactionId: string, projectId: string): Promise; + writeStaging(transactionId: string, path: string, bytes: Uint8Array): Promise; + countStagingEntries(transactionId: string): Promise; + discardStaging(transactionId: string): Promise; + commitStaging( + transactionId: string, + expected: ArchiveProjectIdentityIR, + candidate: ArchiveProjectIdentityIR, + ): Promise; +} + +export type ArchiveExtractionEntryReader = ( + entry: ArchiveExtractionEntryIR, + signal: AbortSignal, +) => Promise; + +export type ArchiveExtractionReceiptIR = + | { + status: "COMMITTED"; + transactionId: string; + committed: ArchiveProjectIdentityIR; + stagingEntriesAfter: 0; + } + | { + status: "CANCELLED"; + code: "IO_ARCHIVE_CANCELLED"; + transactionId: string; + committedBefore: ArchiveProjectIdentityIR; + committedAfter: ArchiveProjectIdentityIR; + removedStagingEntries: number; + stagingEntriesAfter: 0; + publishedProjects: 0; + }; + +export class ArchiveExtractionError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "ArchiveExtractionError"; + this.code = code; + this.path = path; + } +} + +const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const SHA256 = /^[a-f0-9]{64}$/; +const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/; +const DRIVE_PATH = /^[A-Za-z]:\//; +const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path} must be an object`, path); + } + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path} contains undeclared fields`, path); + } +} + +function parseIdentity(value: unknown, path: string): ArchiveProjectIdentityIR { + const input = record(value, path); + exactKeys(input, ["projectId", "revision", "sha256"], path); + if (typeof input.projectId !== "string" || !ID.test(input.projectId)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path}.projectId is invalid`, `${path}.projectId`); + } + if (typeof input.revision !== "number" || !Number.isSafeInteger(input.revision) || input.revision < 0) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path}.revision is invalid`, `${path}.revision`); + } + if (typeof input.sha256 !== "string" || !SHA256.test(input.sha256)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path}.sha256 is invalid`, `${path}.sha256`); + } + return { projectId: input.projectId, revision: input.revision, sha256: input.sha256 }; +} + +function parseArchivePath(value: unknown, path: string): string { + if (typeof value !== "string" || value.length === 0 || value.length > 2_048 || CONTROL_CHARACTER.test(value)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path} is invalid`, path); + } + const normalized = value.normalize("NFC"); + if (normalized !== value || value.startsWith("/") || value.startsWith("\\") || value.includes("\\") || DRIVE_PATH.test(value) || URI_SCHEME.test(value)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path} is not canonical`, path); + } + const segments = value.split("/"); + if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path} escapes staging`, path); + } + return value; +} + +export function parseArchiveExtractionRequest(value: unknown): ArchiveExtractionRequestIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "transactionId", "archiveId", "committed", "candidate", "entries"], "input"); + if (input.schemaVersion !== ARCHIVE_EXTRACTION_SCHEMA) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", "unsupported archive extraction schema", "input.schemaVersion"); + } + if (typeof input.transactionId !== "string" || !ID.test(input.transactionId)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", "transactionId is invalid", "input.transactionId"); + } + if (typeof input.archiveId !== "string" || !ID.test(input.archiveId)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", "archiveId is invalid", "input.archiveId"); + } + const committed = parseIdentity(input.committed, "input.committed"); + const candidate = parseIdentity(input.candidate, "input.candidate"); + if (candidate.projectId !== committed.projectId || candidate.revision !== committed.revision + 1) { + throw new ArchiveExtractionError("REVISION_CONFLICT", "candidate must advance the same project by one revision", "input.candidate"); + } + if (!Array.isArray(input.entries) || input.entries.length === 0 || input.entries.length > 100_000) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", "entries exceeds its bound", "input.entries"); + } + const paths = new Set(); + const entries = input.entries.map((value, index): ArchiveExtractionEntryIR => { + const path = `input.entries[${index}]`; + const entry = record(value, path); + exactKeys(entry, ["path", "uncompressedBytes", "sha256"], path); + const entryPath = parseArchivePath(entry.path, `${path}.path`); + if (paths.has(entryPath)) throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `duplicate entry ${entryPath}`, `${path}.path`); + paths.add(entryPath); + if (typeof entry.uncompressedBytes !== "number" || !Number.isSafeInteger(entry.uncompressedBytes) || entry.uncompressedBytes < 0 || entry.uncompressedBytes > 2 * 1024 * 1024 * 1024) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path}.uncompressedBytes is outside its bound`, `${path}.uncompressedBytes`); + } + if (typeof entry.sha256 !== "string" || !SHA256.test(entry.sha256)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `${path}.sha256 is invalid`, `${path}.sha256`); + } + return { path: entryPath, uncompressedBytes: entry.uncompressedBytes, sha256: entry.sha256 }; + }); + const orderedPaths = [...paths].sort(); + for (let index = 1; index < orderedPaths.length; index++) { + if (orderedPaths[index].startsWith(`${orderedPaths[index - 1]}/`)) { + throw new ArchiveExtractionError("IO_ARCHIVE_UNSAFE", `file/directory prefix conflict at ${orderedPaths[index]}`, "input.entries"); + } + } + return { + schemaVersion: ARCHIVE_EXTRACTION_SCHEMA, + transactionId: input.transactionId, + archiveId: input.archiveId, + committed, + candidate, + entries, + }; +} + +function sameIdentity(left: ArchiveProjectIdentityIR, right: ArchiveProjectIdentityIR): boolean { + return left.projectId === right.projectId && left.revision === right.revision && left.sha256 === right.sha256; +} + +async function digest(bytes: Uint8Array): Promise { + const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer; + const result = await crypto.subtle.digest("SHA-256", source); + return [...new Uint8Array(result)].map((value) => value.toString(16).padStart(2, "0")).join(""); +} + +function cancellation(error: unknown, signal: AbortSignal): boolean { + return signal.aborted || typeof error === "object" && error !== null && "name" in error && error.name === "AbortError"; +} + +function cancelled(): never { + throw new DOMException("Archive extraction was cancelled", "AbortError"); +} + +export async function runArchiveExtractionTransaction( + value: unknown, + storage: ArchiveExtractionStorage, + readEntry: ArchiveExtractionEntryReader, + signal: AbortSignal, +): Promise { + const request = parseArchiveExtractionRequest(value); + const committedBefore = await storage.readCommitted(request.committed.projectId); + if (!sameIdentity(committedBefore, request.committed)) { + throw new ArchiveExtractionError("REVISION_CONFLICT", "committed project identity is stale", "input.committed"); + } + + let stagingCreated = false; + let published = false; + try { + if (signal.aborted) cancelled(); + stagingCreated = true; + await storage.createStaging(request.transactionId, request.committed.projectId); + for (const entry of request.entries) { + if (signal.aborted) cancelled(); + const bytes = await readEntry(entry, signal); + if (signal.aborted) cancelled(); + if (!(bytes instanceof Uint8Array) || bytes.byteLength !== entry.uncompressedBytes || await digest(bytes) !== entry.sha256) { + throw new ArchiveExtractionError("ASSET_SOURCE_HASH_MISMATCH", `payload identity mismatch for ${entry.path}`, entry.path); + } + await storage.writeStaging(request.transactionId, entry.path, bytes); + if (signal.aborted) cancelled(); + } + + // Cancellation linearizes here. Once the atomic commit starts, it completes as a commit. + if (signal.aborted) cancelled(); + const committed = await storage.commitStaging(request.transactionId, committedBefore, request.candidate); + published = true; + if (!sameIdentity(committed, request.candidate)) { + throw new ArchiveExtractionError("STORAGE_TRANSACTION", "storage published an unexpected project identity"); + } + if (await storage.countStagingEntries(request.transactionId) !== 0) { + throw new ArchiveExtractionError("STORAGE_TRANSACTION", "committed extraction retained staging entries"); + } + return { status: "COMMITTED", transactionId: request.transactionId, committed, stagingEntriesAfter: 0 }; + } + catch (error) { + if (published) throw error; + const removedStagingEntries = stagingCreated ? await storage.discardStaging(request.transactionId) : 0; + const stagingEntriesAfter = stagingCreated ? await storage.countStagingEntries(request.transactionId) : 0; + const committedAfter = await storage.readCommitted(request.committed.projectId); + if (stagingEntriesAfter !== 0 || !sameIdentity(committedBefore, committedAfter)) { + throw new ArchiveExtractionError("STORAGE_TRANSACTION", "archive rollback did not preserve the committed project"); + } + if (cancellation(error, signal)) { + return { + status: "CANCELLED", + code: "IO_ARCHIVE_CANCELLED", + transactionId: request.transactionId, + committedBefore, + committedAfter, + removedStagingEntries, + stagingEntriesAfter: 0, + publishedProjects: 0, + }; + } + throw error; + } +} diff --git a/web/protocol/archive-link-safety.ts b/web/protocol/archive-link-safety.ts new file mode 100644 index 00000000..6d1362f5 --- /dev/null +++ b/web/protocol/archive-link-safety.ts @@ -0,0 +1,160 @@ +import type { ErrorCode } from "./error"; + +export const ARCHIVE_LINK_SAFETY_SCHEMA = 1 as const; +export const ARCHIVE_ENTRY_KINDS = ["FILE", "DIRECTORY", "SYMLINK", "HARDLINK"] as const; +export type ArchiveEntryKind = typeof ARCHIVE_ENTRY_KINDS[number]; + +export interface ArchiveLinkEntryIR { + path: string; + type: ArchiveEntryKind; + target: string | null; +} + +export interface ArchiveLinkRequestIR { + schemaVersion: typeof ARCHIVE_LINK_SAFETY_SCHEMA; + temporaryRootId: string; + entries: ArchiveLinkEntryIR[]; +} + +export interface ArchiveResolvedEntryIR extends ArchiveLinkEntryIR { + resolvedPath: string; + resolvedType: "FILE" | "DIRECTORY"; + withinTemporaryRoot: true; +} + +export interface ArchiveLinkResolutionIR { + status: "READY"; + schemaVersion: typeof ARCHIVE_LINK_SAFETY_SCHEMA; + temporaryRootId: string; + entries: ArchiveResolvedEntryIR[]; +} + +export class ArchiveLinkSafetyError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(message: string, path?: string) { + super(`IO_ARCHIVE_UNSAFE: ${message}`); + this.name = "ArchiveLinkSafetyError"; + this.code = "IO_ARCHIVE_UNSAFE"; + this.path = path; + } +} + +const MAX_ENTRIES = 10_000; +const MAX_PATH_LENGTH = 1_024; +const ROOT_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const DRIVE_PATH = /^[A-Za-z]:[\\/]/; +const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/; +const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ArchiveLinkSafetyError(`${path} must be an object`, path); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new ArchiveLinkSafetyError(`${path} contains undeclared fields`, path); +} + +function text(value: unknown, path: string, maxLength: number): string { + if (typeof value !== "string" || value.length === 0 || value.length > maxLength || CONTROL_CHARACTER.test(value)) throw new ArchiveLinkSafetyError(`${path} is invalid`, path); + return value.normalize("NFC"); +} + +function archivePath(value: unknown, path: string, allowRoot = false): string { + const input = text(value, path, MAX_PATH_LENGTH); + if (input.startsWith("/") || input.startsWith("\\") || DRIVE_PATH.test(input) || URI_SCHEME.test(input)) throw new ArchiveLinkSafetyError(`${path} escapes the temporary root`, path); + if (input.includes("\\")) throw new ArchiveLinkSafetyError(`${path} contains a backslash`, path); + const segments: string[] = []; + for (const segment of input.split("/")) { + if (segment === "" || segment === ".") continue; + if (segment === "..") { + if (segments.length === 0) throw new ArchiveLinkSafetyError(`${path} escapes the temporary root`, path); + segments.pop(); + continue; + } + if (CONTROL_CHARACTER.test(segment)) throw new ArchiveLinkSafetyError(`${path} contains a control character`, path); + segments.push(segment); + } + const result = segments.join("/"); + if (!result && !allowRoot) throw new ArchiveLinkSafetyError(`${path} is empty`, path); + return result; +} + +function relativeSymlinkTarget(linkPath: string, target: unknown, path: string): string { + const targetText = text(target, path, MAX_PATH_LENGTH); + if (targetText.startsWith("/") || targetText.startsWith("\\") || DRIVE_PATH.test(targetText) || URI_SCHEME.test(targetText)) throw new ArchiveLinkSafetyError(`${path} escapes the temporary root`, path); + if (targetText.includes("\\")) throw new ArchiveLinkSafetyError(`${path} contains a backslash`, path); + const parent = linkPath.includes("/") ? linkPath.slice(0, linkPath.lastIndexOf("/")) : ""; + return archivePath(parent ? `${parent}/${targetText}` : targetText, path); +} + +function parseEntry(value: unknown, index: number): ArchiveLinkEntryIR { + const path = `entries[${index}]`; + const entry = record(value, path); + exactKeys(entry, ["path", "target", "type"], path); + const entryPath = archivePath(entry.path, `${path}.path`); + if (!ARCHIVE_ENTRY_KINDS.includes(entry.type as ArchiveEntryKind)) throw new ArchiveLinkSafetyError(`${path}.type is unsupported`, `${path}.type`); + const type = entry.type as ArchiveEntryKind; + if (type === "FILE" || type === "DIRECTORY") { + if (entry.target !== null) throw new ArchiveLinkSafetyError(`${path}.target must be null for ${type}`, `${path}.target`); + return { path: entryPath, type, target: null }; + } + if (typeof entry.target !== "string") throw new ArchiveLinkSafetyError(`${path}.target is required for ${type}`, `${path}.target`); + return { path: entryPath, type, target: entry.target.normalize("NFC") }; +} + +export function parseArchiveLinkRequest(value: unknown): ArchiveLinkRequestIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "temporaryRootId", "entries"], "input"); + if (input.schemaVersion !== ARCHIVE_LINK_SAFETY_SCHEMA) throw new ArchiveLinkSafetyError("unsupported archive link schema", "input.schemaVersion"); + if (typeof input.temporaryRootId !== "string" || !ROOT_ID.test(input.temporaryRootId)) throw new ArchiveLinkSafetyError("temporaryRootId is invalid", "input.temporaryRootId"); + if (!Array.isArray(input.entries) || input.entries.length === 0 || input.entries.length > MAX_ENTRIES) throw new ArchiveLinkSafetyError("entries exceeds its bound", "input.entries"); + const entries = input.entries.map((entry, index) => parseEntry(entry, index)); + const paths = new Set(); + for (const entry of entries) { + if (paths.has(entry.path)) throw new ArchiveLinkSafetyError(`duplicate archive path ${entry.path}`, "input.entries"); + paths.add(entry.path); + } + return { schemaVersion: ARCHIVE_LINK_SAFETY_SCHEMA, temporaryRootId: input.temporaryRootId, entries }; +} + +export function resolveArchiveLinkEntries(value: unknown): ArchiveLinkResolutionIR { + const request = parseArchiveLinkRequest(value); + const entries = new Map(request.entries.map((entry) => [entry.path, entry])); + const active = new Set(); + const resolved = new Map(); + + const visit = (entryPath: string): { path: string; type: "FILE" | "DIRECTORY" } => { + const cached = resolved.get(entryPath); + if (cached) return cached; + if (active.has(entryPath)) throw new ArchiveLinkSafetyError(`link cycle includes ${entryPath}`, "input.entries"); + const entry = entries.get(entryPath); + if (!entry) throw new ArchiveLinkSafetyError(`link target ${entryPath} is missing`, "input.entries"); + active.add(entryPath); + let result: { path: string; type: "FILE" | "DIRECTORY" }; + if (entry.type === "FILE" || entry.type === "DIRECTORY") { + result = { path: entry.path, type: entry.type }; + } + else { + const targetPath = entry.type === "SYMLINK" + ? relativeSymlinkTarget(entry.path, entry.target, `entries[${request.entries.indexOf(entry)}].target`) + : archivePath(entry.target, `entries[${request.entries.indexOf(entry)}].target`); + const target = visit(targetPath); + if (entry.type === "HARDLINK" && target.type !== "FILE") throw new ArchiveLinkSafetyError("hardlink target must resolve to a file", `entries[${request.entries.indexOf(entry)}].target`); + result = target; + } + active.delete(entryPath); + resolved.set(entryPath, result); + return result; + }; + + const output = request.entries.map((entry) => { + const target = visit(entry.path); + return { ...entry, resolvedPath: target.path, resolvedType: target.type, withinTemporaryRoot: true as const }; + }); + return { status: "READY", schemaVersion: ARCHIVE_LINK_SAFETY_SCHEMA, temporaryRootId: request.temporaryRootId, entries: output }; +} diff --git a/web/protocol/archive-metadata-first.ts b/web/protocol/archive-metadata-first.ts new file mode 100644 index 00000000..da292925 --- /dev/null +++ b/web/protocol/archive-metadata-first.ts @@ -0,0 +1,135 @@ +import type { ErrorCode } from "./error"; + +export const ARCHIVE_METADATA_FIRST_SCHEMA = 1 as const; +export const ARCHIVE_METADATA_FORMATS = ["ZIP", "TAR"] as const; +export type ArchiveMetadataFormat = typeof ARCHIVE_METADATA_FORMATS[number]; + +export interface ArchiveMetadataFirstRequestIR { + schemaVersion: typeof ARCHIVE_METADATA_FIRST_SCHEMA; + archiveId: string; + format: ArchiveMetadataFormat; + archiveByteLength: number; + metadataOffset: number; + metadataByteLength: number; +} + +export interface ArchiveMetadataReadIR { + kind: "CENTRAL_DIRECTORY" | "MANIFEST"; + byteOffset: number; + byteLength: number; +} + +export interface ArchiveMetadataFirstPlanIR { + status: "METADATA_ONLY"; + schemaVersion: typeof ARCHIVE_METADATA_FIRST_SCHEMA; + archiveId: string; + format: ArchiveMetadataFormat; + firstRead: ArchiveMetadataReadIR; + payloadReads: []; +} + +export interface ArchiveReadTraceItemIR { + sequence: number; + kind: "CENTRAL_DIRECTORY" | "MANIFEST" | "PAYLOAD"; + byteOffset: number; + byteLength: number; +} + +export interface ArchiveReadTraceIR { + schemaVersion: typeof ARCHIVE_METADATA_FIRST_SCHEMA; + archiveId: string; + reads: ArchiveReadTraceItemIR[]; +} + +export interface ArchiveReadTraceValidationIR { + status: "VALID"; + metadataFirst: true; + payloadReadsAfterMetadata: true; +} + +export class ArchiveMetadataFirstError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(message: string, path?: string) { + super(`IO_ARCHIVE_UNSAFE: ${message}`); + this.name = "ArchiveMetadataFirstError"; + this.code = "IO_ARCHIVE_UNSAFE"; + this.path = path; + } +} + +const ARCHIVE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const MAX_ARCHIVE_BYTES = Number.MAX_SAFE_INTEGER; +const MAX_METADATA_BYTES = 64 * 1024 * 1024; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ArchiveMetadataFirstError(`${path} must be an object`, path); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new ArchiveMetadataFirstError(`${path} contains undeclared fields`, path); +} + +function integer(value: unknown, path: string, minimum = 0): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > MAX_ARCHIVE_BYTES) throw new ArchiveMetadataFirstError(`${path} must be a safe integer`, path); + return value; +} + +function parseRequest(value: unknown): ArchiveMetadataFirstRequestIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "archiveId", "format", "archiveByteLength", "metadataOffset", "metadataByteLength"], "input"); + if (input.schemaVersion !== ARCHIVE_METADATA_FIRST_SCHEMA) throw new ArchiveMetadataFirstError("unsupported archive metadata schema", "input.schemaVersion"); + if (typeof input.archiveId !== "string" || !ARCHIVE_ID.test(input.archiveId)) throw new ArchiveMetadataFirstError("archiveId is invalid", "input.archiveId"); + if (!ARCHIVE_METADATA_FORMATS.includes(input.format as ArchiveMetadataFormat)) throw new ArchiveMetadataFirstError("archive format is unsupported", "input.format"); + const archiveByteLength = integer(input.archiveByteLength, "input.archiveByteLength", 1); + const metadataOffset = integer(input.metadataOffset, "input.metadataOffset"); + const metadataByteLength = integer(input.metadataByteLength, "input.metadataByteLength", 1); + if (metadataByteLength > MAX_METADATA_BYTES || metadataOffset + metadataByteLength > archiveByteLength) throw new ArchiveMetadataFirstError("metadata range is outside the archive or exceeds its bound", "input.metadataByteLength"); + return { schemaVersion: ARCHIVE_METADATA_FIRST_SCHEMA, archiveId: input.archiveId, format: input.format as ArchiveMetadataFormat, archiveByteLength, metadataOffset, metadataByteLength }; +} + +export function planArchiveMetadataRead(value: unknown): ArchiveMetadataFirstPlanIR { + const request = parseRequest(value); + return { + status: "METADATA_ONLY", + schemaVersion: ARCHIVE_METADATA_FIRST_SCHEMA, + archiveId: request.archiveId, + format: request.format, + firstRead: { kind: request.format === "ZIP" ? "CENTRAL_DIRECTORY" : "MANIFEST", byteOffset: request.metadataOffset, byteLength: request.metadataByteLength }, + payloadReads: [], + }; +} + +function parseTraceItem(value: unknown, index: number): ArchiveReadTraceItemIR { + const path = `reads[${index}]`; + const item = record(value, path); + exactKeys(item, ["sequence", "kind", "byteOffset", "byteLength"], path); + const sequence = integer(item.sequence, `${path}.sequence`); + if (!(["CENTRAL_DIRECTORY", "MANIFEST", "PAYLOAD"] as const).includes(item.kind as ArchiveReadTraceItemIR["kind"])) throw new ArchiveMetadataFirstError(`${path}.kind is unsupported`, `${path}.kind`); + return { sequence, kind: item.kind as ArchiveReadTraceItemIR["kind"], byteOffset: integer(item.byteOffset, `${path}.byteOffset`), byteLength: integer(item.byteLength, `${path}.byteLength`, 1) }; +} + +function parseTrace(value: unknown): ArchiveReadTraceIR { + const input = record(value, "trace"); + exactKeys(input, ["schemaVersion", "archiveId", "reads"], "trace"); + if (input.schemaVersion !== ARCHIVE_METADATA_FIRST_SCHEMA) throw new ArchiveMetadataFirstError("unsupported archive trace schema", "trace.schemaVersion"); + if (typeof input.archiveId !== "string" || !ARCHIVE_ID.test(input.archiveId)) throw new ArchiveMetadataFirstError("trace archiveId is invalid", "trace.archiveId"); + if (!Array.isArray(input.reads) || input.reads.length === 0 || input.reads.length > 10_000) throw new ArchiveMetadataFirstError("trace reads exceeds its bound", "trace.reads"); + const reads = input.reads.map(parseTraceItem).sort((left, right) => left.sequence - right.sequence); + if (reads.some((item, index) => item.sequence !== index)) throw new ArchiveMetadataFirstError("trace sequence must be contiguous and unique", "trace.reads"); + return { schemaVersion: ARCHIVE_METADATA_FIRST_SCHEMA, archiveId: input.archiveId, reads }; +} + +export function validateArchiveReadTrace(planValue: unknown, traceValue: unknown): ArchiveReadTraceValidationIR { + const plan = planArchiveMetadataRead(planValue); + const trace = parseTrace(traceValue); + if (trace.archiveId !== plan.archiveId) throw new ArchiveMetadataFirstError("trace archiveId does not match the plan", "trace.archiveId"); + const first = trace.reads[0]; + if (first.kind !== plan.firstRead.kind || first.byteOffset !== plan.firstRead.byteOffset || first.byteLength !== plan.firstRead.byteLength) throw new ArchiveMetadataFirstError("payload was read before the central directory or manifest", "trace.reads[0]"); + if (trace.reads.slice(1).some((item) => item.kind === plan.firstRead.kind || item.kind === (plan.format === "ZIP" ? "MANIFEST" : "CENTRAL_DIRECTORY"))) throw new ArchiveMetadataFirstError("metadata read sequence is duplicated or out of order", "trace.reads"); + return { status: "VALID", metadataFirst: true, payloadReadsAfterMetadata: true }; +} diff --git a/web/protocol/asset-library-io.ts b/web/protocol/asset-library-io.ts index 6dab8801..1190bf42 100644 --- a/web/protocol/asset-library-io.ts +++ b/web/protocol/asset-library-io.ts @@ -13,6 +13,8 @@ export const ASSET_LIBRARY_BUDGET = { maxEntryBytes: 2 * 1024 * 1024 * 1024, maxArchiveBytes: 4 * 1024 * 1024 * 1024, maxCompressionRatio: 100, + maxArchivePathDepth: 64, + maxArchiveFileNameBytes: 255, maxExternalUris: 10_000, } as const; @@ -51,6 +53,11 @@ const FORMATS = new Set(["GLB", "GLTF", "OBJ", "PLY", "STL", "USD", "A function record(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function text(value: unknown, name: string, maximum = 256): string { if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", `${name} is invalid`); return value; } function integer(value: unknown, name: string, minimum: number, maximum: number): number { if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", `${name} is outside the bounded range`); return value; } +function archiveInteger(value: unknown, name: string, minimum: number, maximum: number): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", `${name} is outside the bounded range`); + if (value > maximum) throw new AssetLibraryValidationError("ASSET_BUDGET_EXCEEDED", `${name} exceeds the archive entry budget`); + return value; +} function digest(value: unknown, name: string): string { if (typeof value !== "string" || !SHA256.test(value)) throw new AssetLibraryValidationError("ASSET_MANIFEST_INVALID", `${name} must be a lowercase SHA-256 digest`); return value; } function projectPath(value: unknown, name: string, code: ErrorCode = "ASSET_MANIFEST_INVALID"): string { try { return normalizeProjectAssetPath(text(value, name, 2048)); } catch { throw new AssetLibraryValidationError(code, `${name} is outside the project`); } } @@ -131,7 +138,10 @@ export function parseIORequest(value: unknown): IORequestIR { let totalCompressed = 0; let totalUncompressed = 0; const archivePaths = new Set(); request.archiveEntries = value.archiveEntries.map((entry, index): IOArchiveEntryIR => { if (!record(entry)) throw new AssetLibraryValidationError("IO_ARCHIVE_UNSAFE", `archiveEntries[${index}] is invalid`); - const path = projectPath(entry.path, `archiveEntries[${index}].path`, "IO_ARCHIVE_UNSAFE"); const compressedBytes = integer(entry.compressedBytes, `archiveEntries[${index}].compressedBytes`, 0, ASSET_LIBRARY_BUDGET.maxEntryBytes); const uncompressedBytes = integer(entry.uncompressedBytes, `archiveEntries[${index}].uncompressedBytes`, 0, ASSET_LIBRARY_BUDGET.maxEntryBytes); + const path = projectPath(entry.path, `archiveEntries[${index}].path`, "IO_ARCHIVE_UNSAFE"); const compressedBytes = archiveInteger(entry.compressedBytes, `archiveEntries[${index}].compressedBytes`, 0, ASSET_LIBRARY_BUDGET.maxEntryBytes); const uncompressedBytes = archiveInteger(entry.uncompressedBytes, `archiveEntries[${index}].uncompressedBytes`, 0, ASSET_LIBRARY_BUDGET.maxEntryBytes); + const pathSegments = path.split("/"); + const fileNameBytes = new TextEncoder().encode(pathSegments[pathSegments.length - 1]).byteLength; + if (pathSegments.length - 1 > ASSET_LIBRARY_BUDGET.maxArchivePathDepth || fileNameBytes > ASSET_LIBRARY_BUDGET.maxArchiveFileNameBytes) throw new AssetLibraryValidationError("IO_ARCHIVE_UNSAFE", `Archive path ${path} exceeds its depth or filename budget`); if (archivePaths.has(path) || [...archivePaths].some((existing) => existing.startsWith(`${path}/`) || path.startsWith(`${existing}/`))) throw new AssetLibraryValidationError("IO_ARCHIVE_UNSAFE", `Archive path ${path} is duplicated or conflicts with a file prefix`); archivePaths.add(path); totalCompressed += compressedBytes; totalUncompressed += uncompressedBytes; diff --git a/web/protocol/asset-path.ts b/web/protocol/asset-path.ts index 052e16c9..166bfe0f 100644 --- a/web/protocol/asset-path.ts +++ b/web/protocol/asset-path.ts @@ -1,23 +1,64 @@ const DRIVE_PATH = /^[A-Za-z]:[\\/]/; const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/; +const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/; + +function invalidPath(message: "ASSET_PATH_INVALID" | "ASSET_PATH_OUTSIDE_PROJECT"): never { + throw new Error(message); +} + +function decodePath(sourcePath: string): string { + let decoded: string; + try { + decoded = decodeURIComponent(sourcePath); + } + catch { + return invalidPath("ASSET_PATH_INVALID"); + } + // A canonical path must be safe to normalize again. Residual percent octets could otherwise + // become separators or dot segments in a second decoder. + if (decoded.includes("%")) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); + try { + encodeURIComponent(decoded); + } + catch { + return invalidPath("ASSET_PATH_INVALID"); + } + return decoded.normalize("NFC"); +} export function normalizeProjectAssetPath(sourcePath: string): string { if (typeof sourcePath !== "string" || sourcePath.length === 0 || sourcePath.length > 2048) { - throw new Error("ASSET_PATH_INVALID"); + return invalidPath("ASSET_PATH_INVALID"); } - if (sourcePath.includes("\0") || sourcePath.includes("\\") || sourcePath.includes("%")) { - throw new Error("ASSET_PATH_OUTSIDE_PROJECT"); + + const blenderRelative = sourcePath.startsWith("//"); + if (sourcePath.startsWith("\\") || sourcePath.startsWith("/") && !blenderRelative) { + return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); } - let relative = sourcePath.startsWith("//") ? sourcePath.slice(2) : sourcePath; + + let relative = decodePath(sourcePath); + if (relative.startsWith("//")) { + if (!blenderRelative) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); + relative = relative.slice(2); + } + relative = relative.replaceAll("\\", "/"); if (relative.startsWith("/") || DRIVE_PATH.test(relative) || URI_SCHEME.test(relative)) { - throw new Error("ASSET_PATH_OUTSIDE_PROJECT"); + return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); } - const segments = relative.split("/"); - if (segments.length === 0 || segments.some((segment) => - segment.length === 0 || segment === "." || segment === ".." || /[\u0000-\u001f\u007f]/.test(segment))) { - throw new Error("ASSET_PATH_OUTSIDE_PROJECT"); + + const canonicalSegments: string[] = []; + for (const segment of relative.split("/")) { + if (segment.length === 0 || segment === ".") continue; + if (segment === "..") { + if (canonicalSegments.length === 0) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); + canonicalSegments.pop(); + continue; + } + if (CONTROL_CHARACTER.test(segment)) return invalidPath("ASSET_PATH_OUTSIDE_PROJECT"); + canonicalSegments.push(segment); } - relative = segments.join("/"); - if (!relative) throw new Error("ASSET_PATH_INVALID"); - return relative; + + const canonical = canonicalSegments.join("/"); + if (!canonical || canonical.length > 2048) return invalidPath("ASSET_PATH_INVALID"); + return canonical; } diff --git a/web/protocol/device-budget.ts b/web/protocol/device-budget.ts new file mode 100644 index 00000000..659538f8 --- /dev/null +++ b/web/protocol/device-budget.ts @@ -0,0 +1,69 @@ +export const DEVICE_BUDGET_SCHEMA_VERSION = 1 as const; + +export type DeviceBudgetTier = "CONSERVATIVE" | "BALANCED" | "HIGH"; + +export interface DeviceBudgetObservation { + schemaVersion: typeof DEVICE_BUDGET_SCHEMA_VERSION; + identitySha256: string; + webgl2: { status: "PASS" | "BLOCKED"; renderer?: string; vendor?: string }; + webgpu: { status: "PASS" | "BLOCKED"; device?: string; description?: string; isFallbackAdapter?: boolean }; + hardwareConcurrency: number | null; + deviceMemory: number | null; +} + +export interface DeviceBudgetLimits { + maxTextureGPUBytes: number; + maxTexturePayloadBytes: number; + maxTextureDimension: number; + maxLights: number; + maxShadowMaps: number; +} + +export interface DeviceBudgetSelection { + schemaVersion: typeof DEVICE_BUDGET_SCHEMA_VERSION; + identitySha256: string; + tier: DeviceBudgetTier; + reason: "MISSING_GPU" | "UNTRUSTED_ADAPTER" | "WEBGPU_UNAVAILABLE" | "BALANCED_CAPABILITY" | "HIGH_CAPABILITY"; + limits: DeviceBudgetLimits; +} + +const mib = 1024 * 1024; +const LIMITS: Readonly> = Object.freeze({ + CONSERVATIVE: Object.freeze({ maxTextureGPUBytes: 256 * mib, maxTexturePayloadBytes: 256 * mib, maxTextureDimension: 8192, maxLights: 8, maxShadowMaps: 2 }), + BALANCED: Object.freeze({ maxTextureGPUBytes: 512 * mib, maxTexturePayloadBytes: 512 * mib, maxTextureDimension: 16384, maxLights: 16, maxShadowMaps: 4 }), + HIGH: Object.freeze({ maxTextureGPUBytes: 1024 * mib, maxTexturePayloadBytes: 512 * mib, maxTextureDimension: 16384, maxLights: 64, maxShadowMaps: 8 }), +}); + +function validHash(value: unknown): value is string { + return typeof value === "string" && /^[a-f0-9]{64}$/u.test(value); +} + +function validPositive(value: number | null): value is number { + return value !== null && Number.isSafeInteger(value) && value > 0; +} + +export function selectDeviceBudget(observation: DeviceBudgetObservation): DeviceBudgetSelection { + if (!observation || observation.schemaVersion !== DEVICE_BUDGET_SCHEMA_VERSION || !validHash(observation.identitySha256)) { + throw new Error("DEVICE_BUDGET_IDENTITY_INVALID"); + } + const conservative = (reason: DeviceBudgetSelection["reason"]): DeviceBudgetSelection => ({ + schemaVersion: DEVICE_BUDGET_SCHEMA_VERSION, + identitySha256: observation.identitySha256, + tier: "CONSERVATIVE", + reason, + limits: LIMITS.CONSERVATIVE, + }); + if (observation.webgl2.status !== "PASS") return conservative("MISSING_GPU"); + const renderer = `${observation.webgl2.renderer ?? ""} ${observation.webgpu.description ?? ""}`.toLowerCase(); + if (!renderer || renderer.includes("swiftshader") || renderer.includes("unknown") || observation.webgpu.isFallbackAdapter) return conservative("UNTRUSTED_ADAPTER"); + if (observation.webgpu.status !== "PASS") return conservative("WEBGPU_UNAVAILABLE"); + if (!validPositive(observation.hardwareConcurrency) || !validPositive(observation.deviceMemory)) return conservative("UNTRUSTED_ADAPTER"); + const tier: DeviceBudgetTier = observation.hardwareConcurrency >= 8 && observation.deviceMemory >= 8 ? "HIGH" : "BALANCED"; + return { schemaVersion: DEVICE_BUDGET_SCHEMA_VERSION, identitySha256: observation.identitySha256, tier, reason: tier === "HIGH" ? "HIGH_CAPABILITY" : "BALANCED_CAPABILITY", limits: LIMITS[tier] }; +} + +export function deviceBudgetLimits(tier: DeviceBudgetTier): DeviceBudgetLimits { + const limits = LIMITS[tier]; + if (!limits) throw new Error("DEVICE_BUDGET_TIER_INVALID"); + return limits; +} diff --git a/web/protocol/diagnostic-report.ts b/web/protocol/diagnostic-report.ts index 446da22b..54e04a2b 100644 --- a/web/protocol/diagnostic-report.ts +++ b/web/protocol/diagnostic-report.ts @@ -24,6 +24,7 @@ export const APP_DIAGNOSTIC_MESSAGES = { STORAGE_START_FAILED: "Storage: unavailable", PBR_ASSET_INVALID: "PBR asset unavailable", BLEND_OPEN_FAILED: "Engine: .blend open failed", + IO_FORMAT_UNSUPPORTED: "IO: format route unavailable", POST_COMMIT_MAINTENANCE_FAILED: "Storage: post-commit maintenance failed", PROJECT_RECOVERY_FAILED: "Recovery: project could not be restored", WORKER_RECOVERY_FAILED: "Recovery: Worker restart failed", diff --git a/web/protocol/error.ts b/web/protocol/error.ts index f0c04814..8ca1d77a 100644 --- a/web/protocol/error.ts +++ b/web/protocol/error.ts @@ -166,6 +166,7 @@ export type ErrorCode = | "LIBRARY_MUTATION_UNAVAILABLE" | "IO_FORMAT_UNSUPPORTED" | "IO_ARCHIVE_UNSAFE" + | "IO_ARCHIVE_CANCELLED" | "IO_EXTERNAL_URI_BLOCKED" | "EDITOR_LAYOUT_INVALID" | "EDITOR_LAYOUT_BUDGET_EXCEEDED" @@ -177,6 +178,10 @@ export type ErrorCode = | "SCRIPT_POLICY_DENIED" | "SCRIPT_SIGNATURE_INVALID" | "SCRIPT_SANDBOX_UNAVAILABLE" + | "SCRIPT_SANDBOX_CRASHED" + | "SCRIPT_SANDBOX_TIMEOUT" + | "SCRIPT_SANDBOX_CANCELLED" + | "SCRIPT_SANDBOX_LATE_RESULT" | "SCRIPT_BUDGET_EXCEEDED" | "PLATFORM_CAPABILITY_UNAVAILABLE" | "SERVER_JOB_UNAVAILABLE" diff --git a/web/protocol/glb-import.ts b/web/protocol/glb-import.ts index 0f840fed..cb4a9f39 100644 --- a/web/protocol/glb-import.ts +++ b/web/protocol/glb-import.ts @@ -11,6 +11,10 @@ interface GLBAccessor { componentType: number; count: number; type: string; + normalized?: boolean; + min?: number[]; + max?: number[]; + sparse?: Record; } interface GLBBufferView { @@ -22,20 +26,62 @@ interface GLBBufferView { interface GLBPrimitive { attributes?: Record; indices?: number; + material?: number; + mode?: number; targets?: Array>; } interface GLBDocument { - asset?: { version?: string }; - buffers?: Array<{ byteLength?: number }>; + asset?: { version?: string; generator?: string }; + scene?: number; + scenes?: Array<{ nodes?: number[] }>; + extensionsUsed?: string[]; + extensionsRequired?: string[]; + buffers?: Array<{ byteLength?: number; uri?: string }>; bufferViews?: GLBBufferView[]; accessors?: GLBAccessor[]; meshes?: Array<{ name?: string; primitives?: GLBPrimitive[]; extras?: Record }>; - images?: Array<{ name?: string; mimeType?: string; bufferView?: number; extras?: Record }>; - skins?: Array<{ joints?: number[]; inverseBindMatrices?: number; extras?: Record }>; - animations?: Array<{ name?: string; channels?: Array<{ target?: { node?: number; path?: string } }>; extras?: Record }>; + images?: Array<{ name?: string; mimeType?: string; bufferView?: number; uri?: string; extras?: Record }>; + samplers?: Array>; + textures?: Array<{ sampler?: number; source?: number }>; + materials?: Array<{ + name?: string; + alphaMode?: string; + doubleSided?: boolean; + pbrMetallicRoughness?: { + baseColorFactor?: number[]; + baseColorTexture?: Record; + metallicFactor?: number; + roughnessFactor?: number; + }; + normalTexture?: Record; + emissiveFactor?: number[]; + }>; + nodes?: Array<{ + name?: string; + mesh?: number; + skin?: number; + children?: number[]; + translation?: number[]; + rotation?: number[]; + scale?: number[]; + }>; + skins?: Array<{ name?: string; joints?: number[]; inverseBindMatrices?: number; skeleton?: number; extras?: Record }>; + animations?: Array<{ + name?: string; + samplers?: Array<{ input?: number; output?: number; interpolation?: string }>; + channels?: Array<{ sampler?: number; target?: { node?: number; path?: string } }>; + extras?: Record; + }>; } +export const GLB_IMPORT_BUDGET = { + maxBytes: 512 * 1024, + maxJsonBytes: 256 * 1024, + maxBufferViews: 4096, + maxAccessors: 8192, +} as const; + export interface ImportedGLBImage { name?: string; blenderId?: string; @@ -62,6 +108,73 @@ export interface GLBSemanticComparison { mismatches: string[]; } +export interface GLBDesktopAccessorSemantics { + componentType: number; + count: number; + type: string; + normalized: boolean; + min: number[] | null; + max: number[] | null; +} + +export interface GLBDesktopFixtureSemantics { + asset: { version?: string; generator?: string } | null; + extensionsUsed: string[]; + extensionsRequired: string[]; + scene: number | null; + nodeNames: Array; + nodes: Array<{ + name: string | null; + mesh: number | null; + skin: number | null; + children: number[]; + translation: number[] | null; + rotation: number[] | null; + scale: number[] | null; + }>; + meshes: Array<{ + name: string | null; + primitives: Array<{ + attributes: Record; + indices: GLBDesktopAccessorSemantics | null; + material: number | null; + mode: number; + targets: Array>; + }>; + }>; + materials: Array<{ + name: string | null; + alphaMode: string; + doubleSided: boolean; + pbr: { + baseColorFactor: number[] | null; + baseColorTexture: Record | null; + metallicFactor: number | null; + roughnessFactor: number | null; + }; + normalTexture: Record | null; + emissiveFactor: number[] | null; + }>; + textures: Array>; + images: Array>; + samplers: Array>; + skins: Array<{ + name: string | null; + joints: number[]; + inverseBindMatrices: GLBDesktopAccessorSemantics | null; + skeleton: number | null; + }>; + animations: Array<{ + name: string | null; + samplers: Array<{ + interpolation: string; + input: GLBDesktopAccessorSemantics | null; + output: GLBDesktopAccessorSemantics | null; + }>; + channels: Array<{ sampler: number; target: { node: number; path: string } }>; + }>; +} + function recordId(extras: Record | undefined): string | undefined { return typeof extras?.blenderId === "string" ? extras.blenderId : undefined; } @@ -72,11 +185,12 @@ function requireIndex(value: unknown, size: number, label: string): number { } function jsonChunk(bytes: Uint8Array, length: number): GLBDocument { + if (bytes.byteLength > GLB_IMPORT_BUDGET.maxBytes) throw new Error(`GLB_IMPORT_BUDGET_EXCEEDED: file exceeds ${GLB_IMPORT_BUDGET.maxBytes} bytes`); const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if (bytes.byteLength < 20 || view.getUint32(0, true) !== GLB_MAGIC || view.getUint32(4, true) !== 2) throw new Error("GLB header is invalid"); if (view.getUint32(8, true) !== bytes.byteLength) throw new Error("GLB length does not match header"); const jsonLength = view.getUint32(12, true); - if (view.getUint32(16, true) !== JSON_CHUNK || jsonLength % 4 !== 0 || 20 + jsonLength > bytes.byteLength) throw new Error("GLB JSON chunk is invalid"); + if (jsonLength > GLB_IMPORT_BUDGET.maxJsonBytes || view.getUint32(16, true) !== JSON_CHUNK || jsonLength % 4 !== 0 || 20 + jsonLength > bytes.byteLength) throw new Error("GLB JSON chunk is invalid"); let document: unknown; try { document = JSON.parse(new TextDecoder().decode(bytes.subarray(20, 20 + jsonLength)).trim()); @@ -102,11 +216,15 @@ export function importGLBSemantics(glb: ArrayBuffer): ImportedGLBSemantics { const view = new DataView(glb); const jsonLength = view.getUint32(12, true); const document = jsonChunk(bytes, glb.byteLength); + if ((document.extensionsUsed?.length ?? 0) > 0 || (document.extensionsRequired?.length ?? 0) > 0) throw new Error("GLB_EXTENSION_UNSUPPORTED: extensions are outside the bounded importer"); + if ((document.buffers ?? []).some((buffer) => buffer.uri !== undefined) || (document.images ?? []).some((image) => image.uri !== undefined)) throw new Error("GLB_EXTERNAL_URI_BLOCKED: external URI resources are not accepted"); const bufferViews = document.bufferViews ?? []; const accessors = document.accessors ?? []; + if (bufferViews.length > GLB_IMPORT_BUDGET.maxBufferViews || accessors.length > GLB_IMPORT_BUDGET.maxAccessors) throw new Error("GLB_IMPORT_BUDGET_EXCEEDED: accessor or bufferView count exceeds the bounded importer"); for (const [index, bufferView] of bufferViews.entries()) bufferViewBytes(bytes, jsonLength, bufferView, `bufferViews[${index}]`); const accessorType = (index: number): string => accessors[requireIndex(index, accessors.length, "accessor")]?.type ?? ""; for (const [index, accessor] of accessors.entries()) { + if (accessor.sparse !== undefined) throw new Error(`GLB_SPARSE_ACCESSOR_UNSUPPORTED: accessors[${index}]`); if (!Number.isSafeInteger(accessor.count) || accessor.count < 0 || (accessor.byteOffset ?? 0) < 0) throw new Error(`accessors[${index}] is invalid`); if (accessor.bufferView !== undefined) { const bytesForAccessor = bufferViewBytes(bytes, jsonLength, bufferViews[requireIndex(accessor.bufferView, bufferViews.length, `accessors[${index}]`)], `accessors[${index}]`); @@ -153,6 +271,173 @@ export function importGLBSemantics(glb: ArrayBuffer): ImportedGLBSemantics { return { version: 2, meshCount: meshes.length, primitiveCount: meshes.reduce((sum, mesh) => sum + mesh.primitiveCount, 0), meshes, images, skinCount: skins.length, skins, animationCount: animations.length, animationChannelCount: animationPaths.length, animationPaths }; } +function desktopAccessorSemantics(accessors: readonly GLBAccessor[], index: number | undefined, label: string): GLBDesktopAccessorSemantics | null { + if (index === undefined) return null; + const accessor = accessors[requireIndex(index, accessors.length, label)]!; + return { + componentType: accessor.componentType, + count: accessor.count, + type: accessor.type, + normalized: accessor.normalized === true, + min: accessor.min ?? null, + max: accessor.max ?? null, + }; +} + +/** + * Imports the canonical, bounded semantic surface used by the M12 desktop GLB + * fixtures. This intentionally does not create Blender Main data; that writer + * and its stable-ID persistence gate belong to M12-06C. + */ +export function importGLBDesktopFixtureSemantics(glb: ArrayBuffer): GLBDesktopFixtureSemantics { + importGLBSemantics(glb); + const bytes = new Uint8Array(glb); + const jsonLength = new DataView(glb).getUint32(12, true); + const document = jsonChunk(bytes, glb.byteLength); + const accessors = document.accessors ?? []; + const meshes = document.meshes ?? []; + const materials = document.materials ?? []; + const nodes = document.nodes ?? []; + const textures = document.textures ?? []; + const images = document.images ?? []; + const samplers = document.samplers ?? []; + const skins = document.skins ?? []; + const animations = document.animations ?? []; + + if (document.scene !== undefined) requireIndex(document.scene, document.scenes?.length ?? 0, "scene"); + for (const [index, node] of nodes.entries()) { + if (node.mesh !== undefined) requireIndex(node.mesh, meshes.length, `nodes[${index}].mesh`); + if (node.skin !== undefined) requireIndex(node.skin, skins.length, `nodes[${index}].skin`); + for (const child of node.children ?? []) requireIndex(child, nodes.length, `nodes[${index}].children`); + } + for (const [index, texture] of textures.entries()) { + if (texture.source !== undefined) requireIndex(texture.source, images.length, `textures[${index}].source`); + if (texture.sampler !== undefined) requireIndex(texture.sampler, samplers.length, `textures[${index}].sampler`); + } + + const importedMeshes = meshes.map((mesh, meshIndex) => ({ + name: mesh.name ?? null, + primitives: (mesh.primitives ?? []).map((primitive, primitiveIndex) => { + if (primitive.material !== undefined) requireIndex(primitive.material, materials.length, `meshes[${meshIndex}].primitives[${primitiveIndex}].material`); + const attributes: Record = {}; + for (const [name, accessor] of Object.entries(primitive.attributes ?? {}).sort(([left], [right]) => left.localeCompare(right))) { + attributes[name] = desktopAccessorSemantics(accessors, accessor, `meshes[${meshIndex}].primitives[${primitiveIndex}].attributes.${name}`)!; + } + return { + attributes, + indices: desktopAccessorSemantics(accessors, primitive.indices, `meshes[${meshIndex}].primitives[${primitiveIndex}].indices`), + material: primitive.material ?? null, + mode: primitive.mode ?? 4, + targets: (primitive.targets ?? []).map((target, targetIndex) => { + const imported: Record = {}; + for (const [name, accessor] of Object.entries(target).sort(([left], [right]) => left.localeCompare(right))) { + imported[name] = desktopAccessorSemantics(accessors, accessor, `meshes[${meshIndex}].primitives[${primitiveIndex}].targets[${targetIndex}].${name}`)!; + } + return imported; + }), + }; + }), + })); + + const importedSkins = skins.map((skin, skinIndex) => { + const joints = skin.joints ?? []; + for (const joint of joints) requireIndex(joint, nodes.length, `skins[${skinIndex}].joints`); + if (skin.skeleton !== undefined) requireIndex(skin.skeleton, nodes.length, `skins[${skinIndex}].skeleton`); + return { + name: skin.name ?? null, + joints, + inverseBindMatrices: desktopAccessorSemantics(accessors, skin.inverseBindMatrices, `skins[${skinIndex}].inverseBindMatrices`), + skeleton: skin.skeleton ?? null, + }; + }); + + const importedAnimations = animations.map((animation, animationIndex) => { + const animationSamplers = animation.samplers ?? []; + return { + name: animation.name ?? null, + samplers: animationSamplers.map((sampler, samplerIndex) => ({ + interpolation: sampler.interpolation ?? "LINEAR", + input: desktopAccessorSemantics(accessors, sampler.input, `animations[${animationIndex}].samplers[${samplerIndex}].input`), + output: desktopAccessorSemantics(accessors, sampler.output, `animations[${animationIndex}].samplers[${samplerIndex}].output`), + })), + channels: (animation.channels ?? []).map((channel, channelIndex) => { + const sampler = requireIndex(channel.sampler, animationSamplers.length, `animations[${animationIndex}].channels[${channelIndex}].sampler`); + const node = requireIndex(channel.target?.node, nodes.length, `animations[${animationIndex}].channels[${channelIndex}].target.node`); + const path = channel.target?.path; + if (path !== "translation" && path !== "rotation" && path !== "scale" && path !== "weights") throw new Error(`animations[${animationIndex}].channels[${channelIndex}].target.path is invalid`); + return { sampler, target: { node, path } }; + }), + }; + }); + + return { + asset: document.asset ?? null, + extensionsUsed: [...(document.extensionsUsed ?? [])].sort(), + extensionsRequired: [...(document.extensionsRequired ?? [])].sort(), + scene: document.scene ?? null, + nodeNames: nodes.map((node) => node.name ?? null), + nodes: nodes.map((node) => ({ + name: node.name ?? null, + mesh: node.mesh ?? null, + skin: node.skin ?? null, + children: node.children ?? [], + translation: node.translation ?? null, + rotation: node.rotation ?? null, + scale: node.scale ?? null, + })), + meshes: importedMeshes, + materials: materials.map((material) => { + const pbr = material.pbrMetallicRoughness ?? {}; + return { + name: material.name ?? null, + alphaMode: material.alphaMode ?? "OPAQUE", + doubleSided: material.doubleSided === true, + pbr: { + baseColorFactor: pbr.baseColorFactor ?? null, + baseColorTexture: pbr.baseColorTexture ?? null, + metallicFactor: pbr.metallicFactor ?? null, + roughnessFactor: pbr.roughnessFactor ?? null, + }, + normalTexture: material.normalTexture ?? null, + emissiveFactor: material.emissiveFactor ?? null, + }; + }), + textures: textures.map((texture) => ({ ...texture })), + images: images.map((image) => ({ ...image })), + samplers: samplers.map((sampler) => ({ ...sampler })), + skins: importedSkins, + animations: importedAnimations, + }; +} + +function semanticMismatches(expected: unknown, actual: unknown, path: string, mismatches: string[]): void { + if (Object.is(expected, actual)) return; + if (Array.isArray(expected) || Array.isArray(actual)) { + if (!Array.isArray(expected) || !Array.isArray(actual)) { + mismatches.push(`${path}: expected ${JSON.stringify(expected)} got ${JSON.stringify(actual)}`); + return; + } + if (expected.length !== actual.length) mismatches.push(`${path}.length: expected ${expected.length} got ${actual.length}`); + for (let index = 0; index < Math.min(expected.length, actual.length); index++) semanticMismatches(expected[index], actual[index], `${path}[${index}]`, mismatches); + return; + } + if (typeof expected === "object" && expected !== null && typeof actual === "object" && actual !== null) { + const expectedRecord = expected as Record; + const actualRecord = actual as Record; + for (const key of [...new Set([...Object.keys(expectedRecord), ...Object.keys(actualRecord)])].sort()) { + semanticMismatches(expectedRecord[key], actualRecord[key], `${path}.${key}`, mismatches); + } + return; + } + mismatches.push(`${path}: expected ${JSON.stringify(expected)} got ${JSON.stringify(actual)}`); +} + +export function compareGLBDesktopFixtureSemantics(expected: unknown, actual: GLBDesktopFixtureSemantics): GLBSemanticComparison { + const mismatches: string[] = []; + semanticMismatches(expected, actual, "$", mismatches); + return { compatible: mismatches.length === 0, mismatches }; +} + export function compareGLBToSceneIR(snapshot: SceneSnapshotIR, imported: ImportedGLBSemantics, assetBuffers: readonly GLBAssetBuffer[] = []): GLBSemanticComparison { const mismatches: string[] = []; const geometryMeshIds = new Set(snapshot.meshes.filter((mesh) => mesh.geometryStatus !== "summary-only").map((mesh) => mesh.id)); diff --git a/web/protocol/glb-loss-report.ts b/web/protocol/glb-loss-report.ts new file mode 100644 index 00000000..2744602c --- /dev/null +++ b/web/protocol/glb-loss-report.ts @@ -0,0 +1,63 @@ +import type { GLBExportReport } from "./glb-export"; +import type { SceneSnapshotIR } from "./scene-ir"; + +export const GLB_LOSS_REPORT_SCHEMA_VERSION = 1 as const; + +export interface GLBLossReport { + schemaVersion: typeof GLB_LOSS_REPORT_SCHEMA_VERSION; + operation: "GLB_EXPORT_LOSS_REPORT"; + sceneId: string; + sourceRevision: number; + canExport: boolean; + errorCount: number; + warningCount: number; + losses: Array<{ + code: string; + severity: "warning" | "error"; + message: string; + id: string | null; + }>; + surface: { + nodeCount: number; + meshCount: number; + materialCount: number; + imageCount: number; + animationCount: number; + nonMeshCount: number; + }; +} + +/** Convert the exporter result into a stable, machine-consumable loss report. */ +export function createGLBLossReport(snapshot: SceneSnapshotIR, report: GLBExportReport): GLBLossReport { + const losses = report.warnings + .map((warning) => ({ + code: warning.code, + severity: warning.severity, + message: warning.message, + id: warning.id ?? null, + })) + .sort((left, right) => + left.code.localeCompare(right.code) || + left.severity.localeCompare(right.severity) || + (left.id ?? "").localeCompare(right.id ?? "") || + left.message.localeCompare(right.message), + ); + return { + schemaVersion: GLB_LOSS_REPORT_SCHEMA_VERSION, + operation: "GLB_EXPORT_LOSS_REPORT", + sceneId: snapshot.sceneId, + sourceRevision: snapshot.revision, + canExport: report.canExport, + errorCount: losses.filter((loss) => loss.severity === "error").length, + warningCount: losses.filter((loss) => loss.severity === "warning").length, + losses, + surface: { + nodeCount: snapshot.nodes.length, + meshCount: snapshot.meshes.length, + materialCount: snapshot.materials.length, + imageCount: snapshot.images.length, + animationCount: snapshot.animations.length, + nonMeshCount: snapshot.nonMeshData?.length ?? 0, + }, + }; +} diff --git a/web/protocol/glb-recovery.ts b/web/protocol/glb-recovery.ts new file mode 100644 index 00000000..1cf51c3b --- /dev/null +++ b/web/protocol/glb-recovery.ts @@ -0,0 +1,134 @@ +export const GLB_RECOVERY_SCHEMA_VERSION = 1 as const; + +export type GLBRecoveryOperation = "IMPORT" | "EXPORT"; +export type GLBRecoveryStatus = "RUNNING" | "CANCELLED" | "COMMITTED" | "RECOVERED" | "BLOCKED"; +export type GLBRecoveryErrorCode = + | "GLB_OPERATION_CANCELLED" + | "GLB_WORKER_RESTARTED" + | "GLB_OPFS_QUOTA" + | "GLB_RECOVERY_INVALID"; + +export interface GLBRecoveryReceipt { + schemaVersion: typeof GLB_RECOVERY_SCHEMA_VERSION; + operationId: string; + operation: GLBRecoveryOperation; + status: GLBRecoveryStatus; + workerGeneration: number; + baseRevision: number; + candidateRevision: number; + inputBytes: number; + inputSha256: string; + outputBytes: number; + outputSha256: string | null; + temporaryBytes: number; + liveRequests: number; + committed: boolean; + errorCode?: GLBRecoveryErrorCode; +} + +const SHA256 = /^[a-f0-9]{64}$/; +const OPERATION_ID = /^[A-Za-z0-9_-]{1,96}$/; + +function assertBase(receipt: GLBRecoveryReceipt): void { + if (receipt.schemaVersion !== GLB_RECOVERY_SCHEMA_VERSION || !OPERATION_ID.test(receipt.operationId) || + (receipt.operation !== "IMPORT" && receipt.operation !== "EXPORT") || !Number.isSafeInteger(receipt.workerGeneration) || receipt.workerGeneration < 1 || + !Number.isSafeInteger(receipt.baseRevision) || receipt.baseRevision < 0 || !Number.isSafeInteger(receipt.candidateRevision) || receipt.candidateRevision < receipt.baseRevision || + !Number.isSafeInteger(receipt.inputBytes) || receipt.inputBytes <= 0 || !SHA256.test(receipt.inputSha256) || + !Number.isSafeInteger(receipt.outputBytes) || receipt.outputBytes < 0 || (receipt.outputSha256 !== null && !SHA256.test(receipt.outputSha256)) || + !Number.isSafeInteger(receipt.temporaryBytes) || receipt.temporaryBytes < 0 || !Number.isSafeInteger(receipt.liveRequests) || receipt.liveRequests < 0 || + typeof receipt.committed !== "boolean") { + throw new Error("GLB_RECOVERY_INVALID: receipt fields are malformed"); + } +} + +function clone(receipt: GLBRecoveryReceipt): GLBRecoveryReceipt { + assertBase(receipt); + return { ...receipt }; +} + +export function beginGLBRecoveryOperation(input: { + operationId: string; + operation: GLBRecoveryOperation; + workerGeneration: number; + baseRevision: number; + inputBytes: number; + inputSha256: string; +}): GLBRecoveryReceipt { + const receipt: GLBRecoveryReceipt = { + schemaVersion: GLB_RECOVERY_SCHEMA_VERSION, + operationId: input.operationId, + operation: input.operation, + status: "RUNNING", + workerGeneration: input.workerGeneration, + baseRevision: input.baseRevision, + candidateRevision: input.baseRevision + 1, + inputBytes: input.inputBytes, + inputSha256: input.inputSha256, + outputBytes: 0, + outputSha256: null, + temporaryBytes: input.inputBytes, + liveRequests: 1, + committed: false, + }; + assertBase(receipt); + return receipt; +} + +export function commitGLBRecoveryOperation(receipt: GLBRecoveryReceipt, output: { bytes: number; sha256: string }): GLBRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "RUNNING" || !Number.isSafeInteger(output.bytes) || output.bytes <= 0 || !SHA256.test(output.sha256)) { + throw new Error("GLB_RECOVERY_INVALID: operation cannot commit"); + } + next.status = "COMMITTED"; + next.outputBytes = output.bytes; + next.outputSha256 = output.sha256; + next.temporaryBytes = 0; + next.liveRequests = 0; + next.committed = true; + return next; +} + +export function cancelGLBRecoveryOperation(receipt: GLBRecoveryReceipt): GLBRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "RUNNING") throw new Error("GLB_RECOVERY_INVALID: operation is not running"); + next.status = "CANCELLED"; + next.errorCode = "GLB_OPERATION_CANCELLED"; + next.temporaryBytes = 0; + next.liveRequests = 0; + next.committed = false; + return next; +} + +export function blockGLBRecoveryForQuota(receipt: GLBRecoveryReceipt): GLBRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "RUNNING") throw new Error("GLB_RECOVERY_INVALID: operation is not running"); + next.status = "BLOCKED"; + next.errorCode = "GLB_OPFS_QUOTA"; + next.temporaryBytes = 0; + next.liveRequests = 0; + next.committed = false; + return next; +} + +export function recoverGLBRecoveryOperation(receipt: GLBRecoveryReceipt, workerGeneration: number): GLBRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "COMMITTED" || !Number.isSafeInteger(workerGeneration) || workerGeneration <= next.workerGeneration) { + throw new Error("GLB_RECOVERY_INVALID: only a committed operation can recover"); + } + next.status = "RECOVERED"; + next.workerGeneration = workerGeneration; + next.errorCode = "GLB_WORKER_RESTARTED"; + return next; +} + +export function parseGLBRecoveryReceipt(value: unknown): GLBRecoveryReceipt { + if (!value || typeof value !== "object") throw new Error("GLB_RECOVERY_INVALID: receipt is not an object"); + const receipt = value as GLBRecoveryReceipt; + assertBase(receipt); + if (!["RUNNING", "CANCELLED", "COMMITTED", "RECOVERED", "BLOCKED"].includes(receipt.status)) throw new Error("GLB_RECOVERY_INVALID: status"); + if (receipt.status === "CANCELLED" && receipt.errorCode !== "GLB_OPERATION_CANCELLED") throw new Error("GLB_RECOVERY_INVALID: cancellation code"); + if (receipt.status === "BLOCKED" && receipt.errorCode !== "GLB_OPFS_QUOTA") throw new Error("GLB_RECOVERY_INVALID: quota code"); + if (receipt.status === "COMMITTED" && (!receipt.committed || receipt.outputBytes <= 0 || !receipt.outputSha256)) throw new Error("GLB_RECOVERY_INVALID: committed receipt"); + if (receipt.status === "RECOVERED" && (!receipt.committed || receipt.errorCode !== "GLB_WORKER_RESTARTED")) throw new Error("GLB_RECOVERY_INVALID: recovered receipt"); + return { ...receipt }; +} diff --git a/web/protocol/ime-composition.ts b/web/protocol/ime-composition.ts new file mode 100644 index 00000000..75054d15 --- /dev/null +++ b/web/protocol/ime-composition.ts @@ -0,0 +1,34 @@ +export const IME_COMPOSITION_SCHEMA_VERSION = 1 as const; + +export interface IMECompositionState { + schemaVersion: typeof IME_COMPOSITION_SCHEMA_VERSION; + composing: boolean; + revision: number; + pendingText: string; + lastEvent: "IDLE" | "START" | "UPDATE" | "END"; +} + +export type IMECompositionEvent = + | { type: "compositionstart"; data?: string } + | { type: "compositionupdate"; data?: string } + | { type: "compositionend"; data?: string }; + +export function createIMECompositionState(): IMECompositionState { + return { schemaVersion: IME_COMPOSITION_SCHEMA_VERSION, composing: false, revision: 0, pendingText: "", lastEvent: "IDLE" }; +} + +export function reduceIMEComposition(state: IMECompositionState, event: IMECompositionEvent): IMECompositionState { + if (!state || state.schemaVersion !== IME_COMPOSITION_SCHEMA_VERSION) throw new Error("IME_STATE_INVALID"); + const text = typeof event.data === "string" ? event.data : ""; + if (event.type === "compositionstart") return { schemaVersion: 1, composing: true, revision: state.revision + 1, pendingText: text, lastEvent: "START" }; + if (event.type === "compositionupdate") { + if (!state.composing) return state; + return { schemaVersion: 1, composing: true, revision: state.revision + 1, pendingText: text, lastEvent: "UPDATE" }; + } + return { schemaVersion: 1, composing: false, revision: state.revision + 1, pendingText: text, lastEvent: "END" }; +} + +export function shouldBlockOperatorShortcuts(state: IMECompositionState, eventIsComposing = false): boolean { + if (!state || state.schemaVersion !== IME_COMPOSITION_SCHEMA_VERSION) throw new Error("IME_STATE_INVALID"); + return state.composing || eventIsComposing; +} diff --git a/web/protocol/input-modal.ts b/web/protocol/input-modal.ts new file mode 100644 index 00000000..cd94fbc9 --- /dev/null +++ b/web/protocol/input-modal.ts @@ -0,0 +1,41 @@ +export const INPUT_MODAL_SCHEMA_VERSION = 1 as const; + +export type InputModalKind = "NONE" | "TOUCH_NAVIGATION" | "PEN_STROKE"; + +export interface InputModalState { + schemaVersion: typeof INPUT_MODAL_SCHEMA_VERSION; + kind: InputModalKind; + activePointerIds: number[]; + cancelled: boolean; + navigationRevision: number; + mainCommitCount: number; +} + +export function createInputModalState(): InputModalState { + return { schemaVersion: 1, kind: "NONE", activePointerIds: [], cancelled: false, navigationRevision: 0, mainCommitCount: 0 }; +} + +export function beginTouch(state: InputModalState, pointerId: number): InputModalState { + if (!Number.isSafeInteger(pointerId) || pointerId < 0) throw new Error("POINTER_ID_INVALID"); + const ids = state.activePointerIds.includes(pointerId) ? state.activePointerIds : [...state.activePointerIds, pointerId].sort((a, b) => a - b); + return { ...state, kind: "TOUCH_NAVIGATION", activePointerIds: ids, cancelled: false, navigationRevision: ids.length >= 2 && state.activePointerIds.length < 2 ? state.navigationRevision + 1 : state.navigationRevision }; +} + +export function cancelInputModal(state: InputModalState): InputModalState { + return { ...state, kind: "NONE", activePointerIds: [], cancelled: true }; +} + +export function endTouch(state: InputModalState, pointerId: number): InputModalState { + const ids = state.activePointerIds.filter((id) => id !== pointerId); + return { ...state, kind: ids.length > 0 ? "TOUCH_NAVIGATION" : "NONE", activePointerIds: ids }; +} + +export function beginPenStroke(state: InputModalState, pointerId: number): InputModalState { + if (!Number.isSafeInteger(pointerId) || pointerId < 0) throw new Error("POINTER_ID_INVALID"); + return { ...state, kind: "PEN_STROKE", activePointerIds: [pointerId], cancelled: false }; +} + +export function commitPenStroke(state: InputModalState, pointerId: number): InputModalState { + if (state.kind !== "PEN_STROKE" || !state.activePointerIds.includes(pointerId) || state.cancelled) return state; + return { ...state, kind: "NONE", activePointerIds: [], mainCommitCount: state.mainCommitCount + 1 }; +} diff --git a/web/protocol/io-format-capability-matrix.ts b/web/protocol/io-format-capability-matrix.ts new file mode 100644 index 00000000..dd7d45c5 --- /dev/null +++ b/web/protocol/io-format-capability-matrix.ts @@ -0,0 +1,135 @@ +import type { ErrorCode } from "./error"; + +export const IO_FORMAT_CAPABILITY_MATRIX_SCHEMA = 1 as const; +export const IO_FORMAT_MATRIX_FORMATS = ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"] as const; +export const IO_FORMAT_MATRIX_OPERATIONS = ["IMPORT", "EXPORT"] as const; +export type IOFormatMatrixFormat = typeof IO_FORMAT_MATRIX_FORMATS[number]; +export type IOFormatMatrixOperation = typeof IO_FORMAT_MATRIX_OPERATIONS[number]; +export type IOFormatMatrixFeatureStatus = "SUPPORTED" | "PARTIAL" | "UNVERIFIED"; +export type IOFormatMatrixRouteStatus = "READY" | "BLOCKED"; +export type IOFormatMatrixExecution = "LOCAL" | "SERVER" | "NONE"; + +export interface IOFormatMatrixRouteIR { + status: IOFormatMatrixRouteStatus; + execution: IOFormatMatrixExecution; + code: Extract | null; +} + +export interface IOFormatMatrixFeatureIR { + status: IOFormatMatrixFeatureStatus; + evidence: string; +} + +export interface IOFormatMatrixOperationIR { + local: IOFormatMatrixRouteIR; + server: IOFormatMatrixRouteIR; + geometry: IOFormatMatrixFeatureIR; + material: IOFormatMatrixFeatureIR; + animation: IOFormatMatrixFeatureIR; +} + +export interface IOFormatMatrixEntryIR { + format: IOFormatMatrixFormat; + runtimeImportStatus: "AVAILABLE" | "OPERATOR_UNREGISTERED"; + runtimeExportStatus: "AVAILABLE" | "OPERATOR_UNREGISTERED"; + operations: Record; +} + +export interface IOFormatCapabilityMatrixIR { + schemaVersion: typeof IO_FORMAT_CAPABILITY_MATRIX_SCHEMA; + task: "M12-05B"; + runtimeInventorySha256: string; + formats: IOFormatMatrixEntryIR[]; +} + +export class IOFormatCapabilityMatrixError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "IOFormatCapabilityMatrixError"; + this.code = code; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const FEATURE_STATUSES = ["SUPPORTED", "PARTIAL", "UNVERIFIED"] as const; +const ROUTE_STATUSES = ["READY", "BLOCKED"] as const; +const EXECUTIONS = ["LOCAL", "SERVER", "NONE"] as const; +const RUNTIME_STATUSES = ["AVAILABLE", "OPERATOR_UNREGISTERED"] as const; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path} must be an object`); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path} contains undeclared fields`); +} + +function text(value: unknown, path: string, maximum = 512): string { + if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path} is invalid`); + return value; +} + +function parseRoute(value: unknown, path: string): IOFormatMatrixRouteIR { + const input = record(value, path); + exactKeys(input, ["status", "execution", "code"], path); + if (!ROUTE_STATUSES.includes(input.status as IOFormatMatrixRouteStatus) || !EXECUTIONS.includes(input.execution as IOFormatMatrixExecution)) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path} route status is invalid`); + const status = input.status as IOFormatMatrixRouteStatus; + const execution = input.execution as IOFormatMatrixExecution; + if (status === "READY" && ((execution !== "LOCAL" && execution !== "SERVER") || input.code !== null)) throw new IOFormatCapabilityMatrixError("IO_FORMAT_UNSUPPORTED", `${path} ready route is not bound to an executor`); + if (status === "BLOCKED" && (execution !== "NONE" || !["IO_FORMAT_UNSUPPORTED", "SERVER_JOB_UNAVAILABLE"].includes(input.code as string))) throw new IOFormatCapabilityMatrixError("IO_FORMAT_UNSUPPORTED", `${path} blocked route is not fail-closed`); + return { status, execution, code: input.code as IOFormatMatrixRouteIR["code"] }; +} + +function parseFeature(value: unknown, path: string): IOFormatMatrixFeatureIR { + const input = record(value, path); + exactKeys(input, ["status", "evidence"], path); + if (!FEATURE_STATUSES.includes(input.status as IOFormatMatrixFeatureStatus)) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path}.status is invalid`); + return { status: input.status as IOFormatMatrixFeatureStatus, evidence: text(input.evidence, `${path}.evidence`) }; +} + +function parseOperation(value: unknown, path: string): IOFormatMatrixOperationIR { + const input = record(value, path); + exactKeys(input, ["local", "server", "geometry", "material", "animation"], path); + const local = parseRoute(input.local, `${path}.local`); + const server = parseRoute(input.server, `${path}.server`); + const geometry = parseFeature(input.geometry, `${path}.geometry`); + const material = parseFeature(input.material, `${path}.material`); + const animation = parseFeature(input.animation, `${path}.animation`); + if (local.status === "READY" && [geometry, material, animation].some((feature) => feature.status === "UNVERIFIED")) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path} ready local route has unverified feature support`); + return { local, server, geometry, material, animation }; +} + +function parseEntry(value: unknown, index: number): IOFormatMatrixEntryIR { + const path = `formats[${index}]`; + const input = record(value, path); + exactKeys(input, ["format", "runtimeImportStatus", "runtimeExportStatus", "operations"], path); + if (!IO_FORMAT_MATRIX_FORMATS.includes(input.format as IOFormatMatrixFormat)) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path}.format is invalid`); + if (!RUNTIME_STATUSES.includes(input.runtimeImportStatus as IOFormatMatrixEntryIR["runtimeImportStatus"]) || !RUNTIME_STATUSES.includes(input.runtimeExportStatus as IOFormatMatrixEntryIR["runtimeExportStatus"])) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", `${path} runtime status is invalid`); + const operations = record(input.operations, `${path}.operations`); + exactKeys(operations, IO_FORMAT_MATRIX_OPERATIONS, `${path}.operations`); + return { + format: input.format as IOFormatMatrixFormat, + runtimeImportStatus: input.runtimeImportStatus as IOFormatMatrixEntryIR["runtimeImportStatus"], + runtimeExportStatus: input.runtimeExportStatus as IOFormatMatrixEntryIR["runtimeExportStatus"], + operations: { + IMPORT: parseOperation(operations.IMPORT, `${path}.operations.IMPORT`), + EXPORT: parseOperation(operations.EXPORT, `${path}.operations.EXPORT`), + }, + }; +} + +export function parseIOFormatCapabilityMatrix(value: unknown): IOFormatCapabilityMatrixIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "task", "runtimeInventorySha256", "formats"], "input"); + if (input.schemaVersion !== IO_FORMAT_CAPABILITY_MATRIX_SCHEMA || input.task !== "M12-05B" || typeof input.runtimeInventorySha256 !== "string" || !SHA256.test(input.runtimeInventorySha256)) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", "matrix header is invalid"); + if (!Array.isArray(input.formats) || input.formats.length !== IO_FORMAT_MATRIX_FORMATS.length) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", "matrix must contain each inventoried format exactly once"); + const formats = input.formats.map(parseEntry); + const seen = new Set(formats.map((entry) => entry.format)); + if (seen.size !== IO_FORMAT_MATRIX_FORMATS.length || IO_FORMAT_MATRIX_FORMATS.some((format) => !seen.has(format))) throw new IOFormatCapabilityMatrixError("ASSET_MANIFEST_INVALID", "matrix format identities are incomplete or duplicated"); + return { schemaVersion: IO_FORMAT_CAPABILITY_MATRIX_SCHEMA, task: "M12-05B", runtimeInventorySha256: input.runtimeInventorySha256, formats }; +} diff --git a/web/protocol/io-format-receipt-binding.ts b/web/protocol/io-format-receipt-binding.ts new file mode 100644 index 00000000..d6f646f9 --- /dev/null +++ b/web/protocol/io-format-receipt-binding.ts @@ -0,0 +1,115 @@ +import type { IOFormatRuntimeIdentityIR, IOFormatRuntimeReceiptIR, IOFormatRuntimeReceiptSetIR } from "./io-format-runtime-receipt"; + +export const IO_FORMAT_RECEIPT_BINDING_SCHEMA = 1 as const; +export const IO_FORMAT_RECEIPT_BINDING_TASK = "M12-05E" as const; + +export interface IOFormatReceiptBindingIR { + sourceSha256: string; + settingsSha256: string; + runtimeSha256: string; +} + +export interface IOFormatBoundRuntimeReceiptIR extends IOFormatRuntimeReceiptIR, IOFormatReceiptBindingIR {} + +export interface IOFormatBoundRuntimeReceiptSetIR { + schemaVersion: typeof IO_FORMAT_RECEIPT_BINDING_SCHEMA; + task: typeof IO_FORMAT_RECEIPT_BINDING_TASK; + parentReceiptSetSha256: string; + inventorySha256: string; + runtime: IOFormatRuntimeIdentityIR; + receipts: IOFormatBoundRuntimeReceiptIR[]; +} + +export class IOFormatReceiptBindingError extends Error { + readonly code = "IO_FORMAT_UNSUPPORTED" as const; + + constructor(message: string) { + super(`IO_FORMAT_UNSUPPORTED: ${message}`); + this.name = "IOFormatReceiptBindingError"; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const FORMAT_ORDER = ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"] as const; + +function assertSha(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) throw new IOFormatReceiptBindingError(`${path} is not SHA-256`); + return value; +} + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatReceiptBindingError(`${path} must be an object`); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new IOFormatReceiptBindingError(`${path} contains undeclared fields`); +} + +function parseReceipt(value: unknown, index: number): IOFormatBoundRuntimeReceiptIR { + const path = `receipts[${index}]`; + const input = record(value, path); + exactKeys(input, ["format", "family", "operation", "operator", "registered", "rnaIdentifier", "buildOption", "buildOptionEnabled", "runtimeStatus", "variants", "extensions", "sourceSha256", "settingsSha256", "runtimeSha256"], path); + if (typeof input.format !== "string" || !FORMAT_ORDER.includes(input.format as typeof FORMAT_ORDER[number])) throw new IOFormatReceiptBindingError(`${path}.format is invalid`); + if (input.operation !== "IMPORT" && input.operation !== "EXPORT") throw new IOFormatReceiptBindingError(`${path}.operation is invalid`); + if (typeof input.family !== "string" || !input.family || typeof input.operator !== "string" || !input.operator) throw new IOFormatReceiptBindingError(`${path} identity is invalid`); + if (typeof input.registered !== "boolean" || (input.rnaIdentifier !== null && typeof input.rnaIdentifier !== "string") || (input.buildOption !== null && typeof input.buildOption !== "string") || (input.buildOptionEnabled !== null && typeof input.buildOptionEnabled !== "boolean")) throw new IOFormatReceiptBindingError(`${path} runtime fields are invalid`); + if (input.runtimeStatus !== "AVAILABLE" && input.runtimeStatus !== "OPERATOR_UNREGISTERED") throw new IOFormatReceiptBindingError(`${path}.runtimeStatus is invalid`); + if (!Array.isArray(input.variants) || !Array.isArray(input.extensions) || input.variants.length === 0 || input.extensions.length === 0 || input.variants.some((item) => typeof item !== "string") || input.extensions.some((item) => typeof item !== "string")) throw new IOFormatReceiptBindingError(`${path} variants/extensions are invalid`); + return { + format: input.format as IOFormatBoundRuntimeReceiptIR["format"], + family: input.family, + operation: input.operation as IOFormatBoundRuntimeReceiptIR["operation"], + operator: input.operator, + registered: input.registered, + rnaIdentifier: input.rnaIdentifier as string | null, + buildOption: input.buildOption as string | null, + buildOptionEnabled: input.buildOptionEnabled as boolean | null, + runtimeStatus: input.runtimeStatus as IOFormatBoundRuntimeReceiptIR["runtimeStatus"], + variants: [...input.variants as string[]], + extensions: [...input.extensions as string[]], + sourceSha256: assertSha(input.sourceSha256, `${path}.sourceSha256`), + settingsSha256: assertSha(input.settingsSha256, `${path}.settingsSha256`), + runtimeSha256: assertSha(input.runtimeSha256, `${path}.runtimeSha256`), + }; +} + +function parseRuntime(value: unknown): IOFormatRuntimeIdentityIR { + const input = record(value, "runtime"); + if (!Array.isArray(input.versionTuple) || input.versionTuple.length !== 3 || input.versionTuple.some((item) => !Number.isSafeInteger(item))) throw new IOFormatReceiptBindingError("runtime.versionTuple is invalid"); + if (typeof input.binarySha256 !== "string" || !SHA256.test(input.binarySha256)) throw new IOFormatReceiptBindingError("runtime.binarySha256 is invalid"); + if (typeof input.blenderVersion !== "string" || typeof input.buildHash !== "string" || typeof input.buildBranch !== "string" || typeof input.buildPlatform !== "string" || typeof input.buildType !== "string" || typeof input.buildDate !== "string" || typeof input.buildTime !== "string" || !Number.isSafeInteger(input.buildCommitTimestamp) || typeof input.buildOptions !== "object" || input.buildOptions === null || Array.isArray(input.buildOptions)) throw new IOFormatReceiptBindingError("runtime identity is invalid"); + return input as unknown as IOFormatRuntimeIdentityIR; +} + +export function canonicalReceiptSource(receipt: IOFormatRuntimeReceiptIR): Record { + return { format: receipt.format, family: receipt.family, operation: receipt.operation, operator: receipt.operator, registered: receipt.registered, rnaIdentifier: receipt.rnaIdentifier }; +} + +export function canonicalReceiptSettings(receipt: IOFormatRuntimeReceiptIR): Record { + return { buildOption: receipt.buildOption, buildOptionEnabled: receipt.buildOptionEnabled, variants: receipt.variants, extensions: receipt.extensions }; +} + +export function canonicalRuntimeIdentity(runtime: IOFormatRuntimeIdentityIR): IOFormatRuntimeIdentityIR { + return runtime; +} + +export function validateIOFormatBoundReceiptSet(value: unknown, expectedParentReceiptSetSha256: string, expectedInventorySha256: string): IOFormatBoundRuntimeReceiptSetIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "task", "parentReceiptSetSha256", "inventorySha256", "runtime", "receipts"], "input"); + if (input.schemaVersion !== IO_FORMAT_RECEIPT_BINDING_SCHEMA || input.task !== IO_FORMAT_RECEIPT_BINDING_TASK) throw new IOFormatReceiptBindingError("receipt binding header is invalid"); + if (!SHA256.test(expectedParentReceiptSetSha256) || !SHA256.test(expectedInventorySha256) || input.parentReceiptSetSha256 !== expectedParentReceiptSetSha256 || input.inventorySha256 !== expectedInventorySha256) throw new IOFormatReceiptBindingError("receipt binding parent identity drifted"); + if (!Array.isArray(input.receipts) || input.receipts.length !== FORMAT_ORDER.length * 2) throw new IOFormatReceiptBindingError("bound receipt count is invalid"); + const receipts = input.receipts.map(parseReceipt); + const identities = receipts.map((receipt) => `${receipt.format}:${receipt.operation}`); + if (new Set(identities).size !== identities.length || FORMAT_ORDER.some((format) => !["IMPORT", "EXPORT"].every((operation) => identities.includes(`${format}:${operation}`)))) throw new IOFormatReceiptBindingError("bound receipt identities are incomplete or duplicated"); + return { schemaVersion: IO_FORMAT_RECEIPT_BINDING_SCHEMA, task: IO_FORMAT_RECEIPT_BINDING_TASK, parentReceiptSetSha256: input.parentReceiptSetSha256, inventorySha256: input.inventorySha256, runtime: parseRuntime(input.runtime), receipts }; +} + +export function resolveBoundReceipt(receiptSet: IOFormatBoundRuntimeReceiptSetIR, format: IOFormatBoundRuntimeReceiptIR["format"], operation: IOFormatBoundRuntimeReceiptIR["operation"]): IOFormatBoundRuntimeReceiptIR { + const receipt = receiptSet.receipts.find((candidate) => candidate.format === format && candidate.operation === operation); + if (!receipt || !SHA256.test(receipt.sourceSha256) || !SHA256.test(receipt.settingsSha256) || !SHA256.test(receipt.runtimeSha256)) throw new IOFormatReceiptBindingError("bound runtime receipt is unavailable"); + return receipt; +} diff --git a/web/protocol/io-format-receipt-freshness.ts b/web/protocol/io-format-receipt-freshness.ts new file mode 100644 index 00000000..7f33027d --- /dev/null +++ b/web/protocol/io-format-receipt-freshness.ts @@ -0,0 +1,185 @@ +import { + validateIOFormatBoundReceiptSet, + type IOFormatBoundRuntimeReceiptSetIR, +} from "./io-format-receipt-binding"; +import { + type IOFormatRuntimeIdentityIR, + type IOFormatRuntimeRouteQuery, +} from "./io-format-runtime-receipt"; + +export const IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA = 1 as const; +export const IO_FORMAT_RECEIPT_FRESHNESS_TASK = "M12-05F" as const; + +export interface IOFormatReceiptFreshnessEnvelopeIR { + schemaVersion: typeof IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA; + task: typeof IO_FORMAT_RECEIPT_FRESHNESS_TASK; + parentBindingSha256: string; + boundReceiptSetSha256: string; + runtimeSha256: string; + bound: IOFormatBoundRuntimeReceiptSetIR; +} + +export interface IOFormatReceiptFreshnessExpectedIR { + parentBindingSha256: string; + parentReceiptSetSha256: string; + inventorySha256: string; + boundReceiptSetSha256: string; + runtimeSha256: string; + runtime: IOFormatRuntimeIdentityIR; + receiptIdentities: Array>; +} + +export type IOFormatReceiptFreshnessFailure = + | "RECEIPT_INVALID" + | "RECEIPT_FORGED" + | "RECEIPT_STALE" + | "RECEIPT_CROSS_VERSION"; + +export class IOFormatReceiptFreshnessError extends Error { + readonly code = "IO_FORMAT_UNSUPPORTED" as const; + readonly reason: IOFormatReceiptFreshnessFailure; + + constructor(reason: IOFormatReceiptFreshnessFailure, message: string) { + super(`IO_FORMAT_UNSUPPORTED: ${reason}: ${message}`); + this.name = "IOFormatReceiptFreshnessError"; + this.reason = reason; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", `${path} must be an object`); + } + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) { + throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", `${path} contains undeclared fields`); + } +} + +function sha(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", `${path} is not SHA-256`); + } + return value; +} + +function stableValue(value: unknown): unknown { + if (Array.isArray(value)) return value.map(stableValue); + if (typeof value === "object" && value !== null) { + return Object.fromEntries(Object.keys(value as Record).sort().map((key) => [key, stableValue((value as Record)[key])])); + } + return value; +} + +function stableJSON(value: unknown): string { + return JSON.stringify(stableValue(value)); +} + +function sameRuntime(left: IOFormatRuntimeIdentityIR, right: IOFormatRuntimeIdentityIR): boolean { + return stableJSON(left) === stableJSON(right); +} + +export function parseIOFormatReceiptFreshness(value: unknown): IOFormatReceiptFreshnessEnvelopeIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "task", "parentBindingSha256", "boundReceiptSetSha256", "runtimeSha256", "bound"], "input"); + if (input.schemaVersion !== IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA || input.task !== IO_FORMAT_RECEIPT_FRESHNESS_TASK) { + throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", "freshness envelope header is invalid"); + } + const boundInput = record(input.bound, "bound"); + const parentReceiptSetSha256 = sha(boundInput.parentReceiptSetSha256, "bound.parentReceiptSetSha256"); + const inventorySha256 = sha(boundInput.inventorySha256, "bound.inventorySha256"); + let bound: IOFormatBoundRuntimeReceiptSetIR; + try { + bound = validateIOFormatBoundReceiptSet(input.bound, parentReceiptSetSha256, inventorySha256); + } + catch (error) { + if (error instanceof IOFormatReceiptFreshnessError) throw error; + throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", error instanceof Error ? error.message : "bound receipt set is invalid"); + } + return { + schemaVersion: IO_FORMAT_RECEIPT_FRESHNESS_SCHEMA, + task: IO_FORMAT_RECEIPT_FRESHNESS_TASK, + parentBindingSha256: sha(input.parentBindingSha256, "input.parentBindingSha256"), + boundReceiptSetSha256: sha(input.boundReceiptSetSha256, "input.boundReceiptSetSha256"), + runtimeSha256: sha(input.runtimeSha256, "input.runtimeSha256"), + bound, + }; +} + +function expectedHashes(expected: IOFormatReceiptFreshnessExpectedIR): void { + sha(expected.parentBindingSha256, "expected.parentBindingSha256"); + sha(expected.parentReceiptSetSha256, "expected.parentReceiptSetSha256"); + sha(expected.inventorySha256, "expected.inventorySha256"); + sha(expected.boundReceiptSetSha256, "expected.boundReceiptSetSha256"); + sha(expected.runtimeSha256, "expected.runtimeSha256"); + if (!Array.isArray(expected.receiptIdentities) || expected.receiptIdentities.length !== 14) { + throw new IOFormatReceiptFreshnessError("RECEIPT_INVALID", "expected receipt identity set is incomplete"); + } +} + +export function validateIOFormatReceiptFreshness(value: unknown, expected: IOFormatReceiptFreshnessExpectedIR): IOFormatReceiptFreshnessEnvelopeIR { + expectedHashes(expected); + const parsed = parseIOFormatReceiptFreshness(value); + if (parsed.parentBindingSha256 !== expected.parentBindingSha256 || parsed.bound.parentReceiptSetSha256 !== expected.parentReceiptSetSha256 || parsed.bound.inventorySha256 !== expected.inventorySha256) { + throw new IOFormatReceiptFreshnessError("RECEIPT_STALE", "receipt parent or inventory identity is stale"); + } + if (parsed.boundReceiptSetSha256 !== expected.boundReceiptSetSha256) { + throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", "receipt-set content identity does not match the trusted build"); + } + if (parsed.runtimeSha256 !== expected.runtimeSha256 || !sameRuntime(parsed.bound.runtime, expected.runtime)) { + throw new IOFormatReceiptFreshnessError("RECEIPT_CROSS_VERSION", "runtime identity does not match the trusted build"); + } + for (const receipt of parsed.bound.receipts) { + if (receipt.runtimeSha256 !== expected.runtimeSha256) { + throw new IOFormatReceiptFreshnessError("RECEIPT_CROSS_VERSION", `${receipt.format}:${receipt.operation} runtime identity is stale`); + } + const expectedReceipt = expected.receiptIdentities.find((candidate) => candidate.format === receipt.format && candidate.operation === receipt.operation); + if (!expectedReceipt || stableJSON(receipt) !== stableJSON(expectedReceipt)) { + throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", `${receipt.format}:${receipt.operation} receipt content is not trusted`); + } + } + return parsed; +} + +async function sha256Text(value: string): Promise { + const digest = await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(value)); + return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +/** Recomputes the canonical receipt-set and runtime digests for an independent checker. */ +export async function verifyIOFormatReceiptFreshness(value: unknown, expected: IOFormatReceiptFreshnessExpectedIR): Promise { + const parsed = validateIOFormatReceiptFreshness(value, expected); + if (await sha256Text(stableJSON(parsed.bound)) !== parsed.boundReceiptSetSha256) { + throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", "receipt-set canonical digest does not match its contents"); + } + if (await sha256Text(stableJSON(parsed.bound.runtime)) !== parsed.runtimeSha256) { + throw new IOFormatReceiptFreshnessError("RECEIPT_FORGED", "runtime canonical digest does not match its contents"); + } + return parsed; +} + +export type IOFormatFreshRuntimeRouteResult = + | { status: "READY"; format: IOFormatRuntimeRouteQuery["format"]; operation: IOFormatRuntimeRouteQuery["operation"]; operator: string; receipt: IOFormatReceiptFreshnessEnvelopeIR["bound"]["receipts"][number]; freshness: "VERIFIED" } + | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; reason: IOFormatReceiptFreshnessFailure | "UNAVAILABLE"; format: IOFormatRuntimeRouteQuery["format"]; operation: IOFormatRuntimeRouteQuery["operation"] }; + +export function resolveFreshIOFormatRuntimeRoute(value: unknown, expected: IOFormatReceiptFreshnessExpectedIR, query: IOFormatRuntimeRouteQuery): IOFormatFreshRuntimeRouteResult { + try { + const parsed = validateIOFormatReceiptFreshness(value, expected); + const receipt = parsed.bound.receipts.find((candidate) => candidate.format === query.format && candidate.operation === query.operation); + if (!receipt || receipt.runtimeStatus !== "AVAILABLE" || receipt.registered !== true || receipt.rnaIdentifier === null || receipt.buildOptionEnabled === false) { + return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", reason: "UNAVAILABLE", format: query.format, operation: query.operation }; + } + return { status: "READY", format: query.format, operation: query.operation, operator: receipt.operator, receipt, freshness: "VERIFIED" }; + } + catch (error) { + const reason = error instanceof IOFormatReceiptFreshnessError ? error.reason : "RECEIPT_INVALID"; + return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", reason, format: query.format, operation: query.operation }; + } +} diff --git a/web/protocol/io-format-recovery.ts b/web/protocol/io-format-recovery.ts new file mode 100644 index 00000000..0df6cd6a --- /dev/null +++ b/web/protocol/io-format-recovery.ts @@ -0,0 +1,73 @@ +export const IO_FORMAT_RECOVERY_SCHEMA_VERSION = 1 as const; +export type IOFormat = "OBJ" | "STL" | "PLY"; +export type IOFormatRecoveryStatus = "RUNNING" | "CANCELLED" | "BLOCKED" | "COMMITTED" | "RECOVERED"; +export type IOFormatRecoveryErrorCode = "IO_FORMAT_OPERATION_CANCELLED" | "IO_FORMAT_OOM" | "IO_FORMAT_WORKER_RESTARTED" | "IO_FORMAT_RECOVERY_INVALID"; + +export interface IOFormatRecoveryReceipt { + schemaVersion: typeof IO_FORMAT_RECOVERY_SCHEMA_VERSION; + operationId: string; + format: IOFormat; + operation: "IMPORT" | "EXPORT"; + status: IOFormatRecoveryStatus; + workerGeneration: number; + baseRevision: number; + candidateRevision: number; + inputBytes: number; + inputSha256: string; + outputBytes: number; + outputSha256: string | null; + temporaryBytes: number; + liveRequests: number; + publishedResults: number; + committed: boolean; + errorCode?: IOFormatRecoveryErrorCode; +} + +const HASH = /^[a-f0-9]{64}$/; +const ID = /^[A-Za-z0-9_-]{1,96}$/; + +function validate(receipt: IOFormatRecoveryReceipt): void { + if (receipt.schemaVersion !== 1 || !ID.test(receipt.operationId) || !["OBJ", "STL", "PLY"].includes(receipt.format) || !["IMPORT", "EXPORT"].includes(receipt.operation) || !["RUNNING", "CANCELLED", "BLOCKED", "COMMITTED", "RECOVERED"].includes(receipt.status) || !Number.isSafeInteger(receipt.workerGeneration) || receipt.workerGeneration < 1 || !Number.isSafeInteger(receipt.baseRevision) || receipt.baseRevision < 0 || !Number.isSafeInteger(receipt.candidateRevision) || receipt.candidateRevision < receipt.baseRevision || !Number.isSafeInteger(receipt.inputBytes) || receipt.inputBytes <= 0 || !HASH.test(receipt.inputSha256) || !Number.isSafeInteger(receipt.outputBytes) || receipt.outputBytes < 0 || (receipt.outputSha256 !== null && !HASH.test(receipt.outputSha256)) || !Number.isSafeInteger(receipt.temporaryBytes) || receipt.temporaryBytes < 0 || !Number.isSafeInteger(receipt.liveRequests) || receipt.liveRequests < 0 || !Number.isSafeInteger(receipt.publishedResults) || receipt.publishedResults < 0 || typeof receipt.committed !== "boolean") { + throw new Error("IO_FORMAT_RECOVERY_INVALID: receipt fields are malformed"); + } +} + +function clone(receipt: IOFormatRecoveryReceipt): IOFormatRecoveryReceipt { validate(receipt); return { ...receipt }; } + +export function beginIOFormatRecoveryOperation(input: { operationId: string; format: IOFormat; operation: "IMPORT" | "EXPORT"; workerGeneration: number; baseRevision: number; inputBytes: number; inputSha256: string }): IOFormatRecoveryReceipt { + const receipt: IOFormatRecoveryReceipt = { schemaVersion: 1, operationId: input.operationId, format: input.format, operation: input.operation, status: "RUNNING", workerGeneration: input.workerGeneration, baseRevision: input.baseRevision, candidateRevision: input.baseRevision + 1, inputBytes: input.inputBytes, inputSha256: input.inputSha256, outputBytes: 0, outputSha256: null, temporaryBytes: input.inputBytes, liveRequests: 1, publishedResults: 0, committed: false }; + validate(receipt); return receipt; +} + +export function commitIOFormatRecoveryOperation(receipt: IOFormatRecoveryReceipt, output: { bytes: number; sha256: string }): IOFormatRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "RUNNING" || !Number.isSafeInteger(output.bytes) || output.bytes <= 0 || !HASH.test(output.sha256)) throw new Error("IO_FORMAT_RECOVERY_INVALID: operation cannot commit"); + next.status = "COMMITTED"; next.outputBytes = output.bytes; next.outputSha256 = output.sha256; next.temporaryBytes = 0; next.liveRequests = 0; next.publishedResults = 1; next.committed = true; validate(next); return next; +} + +export function cancelIOFormatRecoveryOperation(receipt: IOFormatRecoveryReceipt): IOFormatRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "RUNNING") throw new Error("IO_FORMAT_RECOVERY_INVALID: operation is not running"); + next.status = "CANCELLED"; next.errorCode = "IO_FORMAT_OPERATION_CANCELLED"; next.temporaryBytes = 0; next.liveRequests = 0; next.publishedResults = 0; next.committed = false; validate(next); return next; +} + +export function blockIOFormatRecoveryOperation(receipt: IOFormatRecoveryReceipt): IOFormatRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "RUNNING") throw new Error("IO_FORMAT_RECOVERY_INVALID: operation is not running"); + next.status = "BLOCKED"; next.errorCode = "IO_FORMAT_OOM"; next.temporaryBytes = 0; next.liveRequests = 0; next.publishedResults = 0; next.committed = false; validate(next); return next; +} + +export function recoverIOFormatRecoveryOperation(receipt: IOFormatRecoveryReceipt, workerGeneration: number): IOFormatRecoveryReceipt { + const next = clone(receipt); + if (next.status !== "COMMITTED" || !Number.isSafeInteger(workerGeneration) || workerGeneration <= next.workerGeneration) throw new Error("IO_FORMAT_RECOVERY_INVALID: only a committed operation can recover"); + next.status = "RECOVERED"; next.workerGeneration = workerGeneration; next.errorCode = "IO_FORMAT_WORKER_RESTARTED"; validate(next); return next; +} + +export function parseIOFormatRecoveryReceipt(value: unknown): IOFormatRecoveryReceipt { + if (!value || typeof value !== "object") throw new Error("IO_FORMAT_RECOVERY_INVALID: receipt is not an object"); + const receipt = value as IOFormatRecoveryReceipt; validate(receipt); + if (receipt.status === "CANCELLED" && receipt.errorCode !== "IO_FORMAT_OPERATION_CANCELLED") throw new Error("IO_FORMAT_RECOVERY_INVALID: cancellation code"); + if (receipt.status === "BLOCKED" && receipt.errorCode !== "IO_FORMAT_OOM") throw new Error("IO_FORMAT_RECOVERY_INVALID: oom code"); + if ((receipt.status === "COMMITTED" || receipt.status === "RECOVERED") && (!receipt.committed || receipt.outputBytes <= 0 || !receipt.outputSha256 || receipt.publishedResults !== 1)) throw new Error("IO_FORMAT_RECOVERY_INVALID: committed receipt"); + return { ...receipt }; +} diff --git a/web/protocol/io-format-runtime-receipt.ts b/web/protocol/io-format-runtime-receipt.ts new file mode 100644 index 00000000..4da0824b --- /dev/null +++ b/web/protocol/io-format-runtime-receipt.ts @@ -0,0 +1,167 @@ +import type { IOFormatMatrixFormat, IOFormatMatrixOperation } from "./io-format-capability-matrix"; + +export const IO_FORMAT_RUNTIME_RECEIPT_SCHEMA = 1 as const; +export const IO_FORMAT_RUNTIME_RECEIPT_TASK = "M12-05D" as const; +export const IO_FORMAT_RUNTIME_FORMATS = ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"] as const; +export type IOFormatRuntimeFormat = typeof IO_FORMAT_RUNTIME_FORMATS[number]; +export type IOFormatRuntimeOperation = IOFormatMatrixOperation; +export type IOFormatRuntimeStatus = "AVAILABLE" | "OPERATOR_UNREGISTERED"; + +export interface IOFormatRuntimeIdentityIR { + blenderVersion: string; + versionTuple: [number, number, number]; + buildHash: string; + buildBranch: string; + buildPlatform: string; + buildType: string; + buildDate: string; + buildTime: string; + buildCommitTimestamp: number; + binarySha256: string; + buildOptions: Record; +} + +export interface IOFormatRuntimeReceiptIR { + format: IOFormatRuntimeFormat; + family: string; + operation: IOFormatRuntimeOperation; + operator: string; + registered: boolean; + rnaIdentifier: string | null; + buildOption: string | null; + buildOptionEnabled: boolean | null; + runtimeStatus: IOFormatRuntimeStatus; + variants: string[]; + extensions: string[]; +} + +export interface IOFormatRuntimeReceiptSetIR { + schemaVersion: typeof IO_FORMAT_RUNTIME_RECEIPT_SCHEMA; + task: typeof IO_FORMAT_RUNTIME_RECEIPT_TASK; + inventorySha256: string; + runtime: IOFormatRuntimeIdentityIR; + receipts: IOFormatRuntimeReceiptIR[]; +} + +export interface IOFormatRuntimeRouteQuery { + format: IOFormatRuntimeFormat; + operation: IOFormatRuntimeOperation; +} + +export type IOFormatRuntimeRouteResult = + | { status: "READY"; format: IOFormatRuntimeFormat; operation: IOFormatRuntimeOperation; operator: string; receipt: IOFormatRuntimeReceiptIR } + | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; format: IOFormatRuntimeFormat; operation: IOFormatRuntimeOperation }; + +export class IOFormatRuntimeReceiptError extends Error { + readonly code = "IO_FORMAT_UNSUPPORTED" as const; + + constructor(message: string) { + super(`IO_FORMAT_UNSUPPORTED: ${message}`); + this.name = "IOFormatRuntimeReceiptError"; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const VERSION = /^[0-9]+\.[0-9]+\.[0-9]+(?:\s+.*)?$/; +const IDENTIFIER = /^[A-Za-z0-9_.:-]+$/; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatRuntimeReceiptError(`${path} must be an object`); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new IOFormatRuntimeReceiptError(`${path} contains undeclared fields`); +} + +function nonEmpty(value: unknown, path: string, maximum = 256): string { + if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new IOFormatRuntimeReceiptError(`${path} is invalid`); + return value; +} + +function sha(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) throw new IOFormatRuntimeReceiptError(`${path} is not SHA-256`); + return value; +} + +function parseRuntime(value: unknown): IOFormatRuntimeIdentityIR { + const input = record(value, "runtime"); + exactKeys(input, ["blenderVersion", "versionTuple", "buildHash", "buildBranch", "buildPlatform", "buildType", "buildDate", "buildTime", "buildCommitTimestamp", "binarySha256", "buildOptions"], "runtime"); + if (typeof input.blenderVersion !== "string" || !VERSION.test(input.blenderVersion)) throw new IOFormatRuntimeReceiptError("runtime.blenderVersion is invalid"); + if (!Array.isArray(input.versionTuple) || input.versionTuple.length !== 3 || input.versionTuple.some((part) => !Number.isSafeInteger(part) || (part as number) < 0)) throw new IOFormatRuntimeReceiptError("runtime.versionTuple is invalid"); + const buildOptions = record(input.buildOptions, "runtime.buildOptions"); + const parsedOptions: Record = {}; + for (const key of Object.keys(buildOptions).sort()) { + if (!IDENTIFIER.test(key) || typeof buildOptions[key] !== "boolean") throw new IOFormatRuntimeReceiptError("runtime.buildOptions is invalid"); + parsedOptions[key] = buildOptions[key] as boolean; + } + if (!Number.isSafeInteger(input.buildCommitTimestamp) || (input.buildCommitTimestamp as number) < 0) throw new IOFormatRuntimeReceiptError("runtime.buildCommitTimestamp is invalid"); + return { + blenderVersion: input.blenderVersion, + versionTuple: [...input.versionTuple] as [number, number, number], + buildHash: nonEmpty(input.buildHash, "runtime.buildHash"), + buildBranch: nonEmpty(input.buildBranch, "runtime.buildBranch"), + buildPlatform: nonEmpty(input.buildPlatform, "runtime.buildPlatform"), + buildType: nonEmpty(input.buildType, "runtime.buildType"), + buildDate: nonEmpty(input.buildDate, "runtime.buildDate"), + buildTime: nonEmpty(input.buildTime, "runtime.buildTime"), + buildCommitTimestamp: input.buildCommitTimestamp as number, + binarySha256: sha(input.binarySha256, "runtime.binarySha256"), + buildOptions: parsedOptions, + }; +} + +function parseReceipt(value: unknown, index: number): IOFormatRuntimeReceiptIR { + const path = `receipts[${index}]`; + const input = record(value, path); + exactKeys(input, ["format", "family", "operation", "operator", "registered", "rnaIdentifier", "buildOption", "buildOptionEnabled", "runtimeStatus", "variants", "extensions"], path); + if (!IO_FORMAT_RUNTIME_FORMATS.includes(input.format as IOFormatRuntimeFormat) || !["IMPORT", "EXPORT"].includes(input.operation as string)) throw new IOFormatRuntimeReceiptError(`${path} identity is invalid`); + if (typeof input.registered !== "boolean" || !["AVAILABLE", "OPERATOR_UNREGISTERED"].includes(input.runtimeStatus as string)) throw new IOFormatRuntimeReceiptError(`${path} registration status is invalid`); + if (input.rnaIdentifier !== null && (typeof input.rnaIdentifier !== "string" || !IDENTIFIER.test(input.rnaIdentifier))) throw new IOFormatRuntimeReceiptError(`${path}.rnaIdentifier is invalid`); + if (input.buildOption !== null && (typeof input.buildOption !== "string" || !IDENTIFIER.test(input.buildOption))) throw new IOFormatRuntimeReceiptError(`${path}.buildOption is invalid`); + if (input.buildOptionEnabled !== null && typeof input.buildOptionEnabled !== "boolean") throw new IOFormatRuntimeReceiptError(`${path}.buildOptionEnabled is invalid`); + if (!Array.isArray(input.variants) || input.variants.length === 0 || input.variants.some((variant) => typeof variant !== "string" || !IDENTIFIER.test(variant))) throw new IOFormatRuntimeReceiptError(`${path}.variants are invalid`); + if (!Array.isArray(input.extensions) || input.extensions.length === 0 || input.extensions.some((extension) => typeof extension !== "string" || !/^\.[a-z0-9]+$/.test(extension))) throw new IOFormatRuntimeReceiptError(`${path}.extensions are invalid`); + const available = input.runtimeStatus === "AVAILABLE"; + if (available !== input.registered || (available && input.rnaIdentifier === null) || (!available && input.rnaIdentifier !== null) || (!available && input.buildOptionEnabled !== null)) throw new IOFormatRuntimeReceiptError(`${path} has inconsistent runtime receipt state`); + return { + format: input.format as IOFormatRuntimeFormat, + family: nonEmpty(input.family, `${path}.family`), + operation: input.operation as IOFormatRuntimeOperation, + operator: nonEmpty(input.operator, `${path}.operator`), + registered: input.registered as boolean, + rnaIdentifier: input.rnaIdentifier as string | null, + buildOption: input.buildOption as string | null, + buildOptionEnabled: input.buildOptionEnabled as boolean | null, + runtimeStatus: input.runtimeStatus as IOFormatRuntimeStatus, + variants: [...input.variants as string[]], + extensions: [...input.extensions as string[]], + }; +} + +export function parseIOFormatRuntimeReceiptSet(value: unknown): IOFormatRuntimeReceiptSetIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "task", "inventorySha256", "runtime", "receipts"], "input"); + if (input.schemaVersion !== IO_FORMAT_RUNTIME_RECEIPT_SCHEMA || input.task !== IO_FORMAT_RUNTIME_RECEIPT_TASK) throw new IOFormatRuntimeReceiptError("receipt set header is invalid"); + const receipts = Array.isArray(input.receipts) ? input.receipts.map(parseReceipt) : (() => { throw new IOFormatRuntimeReceiptError("input.receipts must be an array"); })(); + if (typeof input.inventorySha256 !== "string" || !SHA256.test(input.inventorySha256)) throw new IOFormatRuntimeReceiptError("input.inventorySha256 is invalid"); + if (receipts.length !== IO_FORMAT_RUNTIME_FORMATS.length * 2) throw new IOFormatRuntimeReceiptError("receipt set must contain one import and export receipt per format"); + const identities = receipts.map((receipt) => `${receipt.format}:${receipt.operation}`); + if (new Set(identities).size !== identities.length || IO_FORMAT_RUNTIME_FORMATS.some((format) => !["IMPORT", "EXPORT"].every((operation) => identities.includes(`${format}:${operation}`)))) throw new IOFormatRuntimeReceiptError("receipt identities are incomplete or duplicated"); + return { schemaVersion: IO_FORMAT_RUNTIME_RECEIPT_SCHEMA, task: IO_FORMAT_RUNTIME_RECEIPT_TASK, inventorySha256: input.inventorySha256, runtime: parseRuntime(input.runtime), receipts }; +} + +export function validateIOFormatRuntimeReceiptSet(value: unknown, expectedInventorySha256: string): IOFormatRuntimeReceiptSetIR { + if (!SHA256.test(expectedInventorySha256)) throw new IOFormatRuntimeReceiptError("expected inventory SHA-256 is invalid"); + const parsed = parseIOFormatRuntimeReceiptSet(value); + if (parsed.inventorySha256 !== expectedInventorySha256) throw new IOFormatRuntimeReceiptError("runtime receipt inventory identity does not match"); + return parsed; +} + +export function resolveIOFormatRuntimeRoute(receiptSet: IOFormatRuntimeReceiptSetIR, query: IOFormatRuntimeRouteQuery): IOFormatRuntimeRouteResult { + const receipt = receiptSet.receipts.find((candidate) => candidate.format === query.format && candidate.operation === query.operation); + if (!receipt || receipt.runtimeStatus !== "AVAILABLE" || receipt.registered !== true || receipt.rnaIdentifier === null || receipt.buildOptionEnabled === false) return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", format: query.format, operation: query.operation }; + return { status: "READY", format: query.format, operation: query.operation, operator: receipt.operator, receipt }; +} diff --git a/web/protocol/io-format-ui-gate.ts b/web/protocol/io-format-ui-gate.ts new file mode 100644 index 00000000..2d134a56 --- /dev/null +++ b/web/protocol/io-format-ui-gate.ts @@ -0,0 +1,149 @@ +import type { + IOFormatCapabilityMatrixIR, + IOFormatMatrixExecution, + IOFormatMatrixFormat, + IOFormatMatrixOperation, +} from "./io-format-capability-matrix"; + +export const IO_FORMAT_UI_GATE_SCHEMA = 1 as const; +export const IO_FORMAT_UI_TASK = "M12-05C" as const; +export const IO_FORMAT_PROJECT_ACCEPT = ".blend,application/octet-stream" as const; + +export interface IOFormatUIRouteIR { + format: IOFormatMatrixFormat; + operation: IOFormatMatrixOperation; + execution: IOFormatMatrixExecution; + extensions: string[]; +} + +export interface IOFormatUIRegistryIR { + schemaVersion: typeof IO_FORMAT_UI_GATE_SCHEMA; + task: typeof IO_FORMAT_UI_TASK; + parentMatrixSha256: string; + projectFileAccept: typeof IO_FORMAT_PROJECT_ACCEPT; + importRoutes: IOFormatUIRouteIR[]; + exportRoutes: IOFormatUIRouteIR[]; +} + +export interface IOFormatUICommandRef { + format: IOFormatMatrixFormat; + operation: IOFormatMatrixOperation; + execution: IOFormatMatrixExecution; +} + +export class IOFormatUIGateError extends Error { + readonly code = "IO_FORMAT_UNSUPPORTED" as const; + + constructor(message: string) { + super(`IO_FORMAT_UNSUPPORTED: ${message}`); + this.name = "IOFormatUIGateError"; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const FORMAT_EXTENSIONS: Record = { + GLTF: [".gltf"], + GLB: [".glb"], + OBJ: [".obj"], + STL: [".stl"], + PLY: [".ply"], + USD: [".usd", ".usda", ".usdc"], + ALEMBIC: [".abc"], +}; + +function routeFor( + matrix: IOFormatCapabilityMatrixIR, + operation: IOFormatMatrixOperation, + execution: IOFormatMatrixExecution, +): IOFormatUIRouteIR[] { + return matrix.formats + .filter((entry) => { + const route = entry.operations[operation][execution.toLowerCase() as "local" | "server"]; + const runtimeStatus = operation === "IMPORT" ? entry.runtimeImportStatus : entry.runtimeExportStatus; + return runtimeStatus === "AVAILABLE" && route.status === "READY" && route.execution === execution; + }) + .map((entry) => ({ + format: entry.format, + operation, + execution, + extensions: [...FORMAT_EXTENSIONS[entry.format]], + })); +} + +export function buildIOFormatUIRegistry(matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR { + if (!SHA256.test(parentMatrixSha256)) throw new IOFormatUIGateError("parent matrix SHA-256 is invalid"); + return { + schemaVersion: IO_FORMAT_UI_GATE_SCHEMA, + task: IO_FORMAT_UI_TASK, + parentMatrixSha256, + projectFileAccept: IO_FORMAT_PROJECT_ACCEPT, + importRoutes: routeFor(matrix, "IMPORT", "LOCAL"), + exportRoutes: routeFor(matrix, "EXPORT", "LOCAL"), + }; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new IOFormatUIGateError(`${path} contains undeclared fields`); +} + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IOFormatUIGateError(`${path} must be an object`); + return value as Record; +} + +function parseRoute(value: unknown, path: string, operation: IOFormatMatrixOperation): IOFormatUIRouteIR { + const input = record(value, path); + exactKeys(input, ["format", "operation", "execution", "extensions"], path); + if (typeof input.format !== "string" || !(input.format in FORMAT_EXTENSIONS) || input.operation !== operation || input.execution !== "LOCAL") throw new IOFormatUIGateError(`${path} route identity is invalid`); + if (!Array.isArray(input.extensions) || input.extensions.length !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat].length || input.extensions.some((extension, index) => extension !== FORMAT_EXTENSIONS[input.format as IOFormatMatrixFormat][index])) throw new IOFormatUIGateError(`${path}.extensions are invalid`); + return { format: input.format as IOFormatMatrixFormat, operation, execution: "LOCAL", extensions: [...input.extensions as string[]] }; +} + +export function parseIOFormatUIRegistry(value: unknown): IOFormatUIRegistryIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "task", "parentMatrixSha256", "projectFileAccept", "importRoutes", "exportRoutes"], "input"); + if (input.schemaVersion !== IO_FORMAT_UI_GATE_SCHEMA || input.task !== IO_FORMAT_UI_TASK || typeof input.parentMatrixSha256 !== "string" || !SHA256.test(input.parentMatrixSha256) || input.projectFileAccept !== IO_FORMAT_PROJECT_ACCEPT) throw new IOFormatUIGateError("registry header is invalid"); + if (!Array.isArray(input.importRoutes) || !Array.isArray(input.exportRoutes)) throw new IOFormatUIGateError("registry routes are invalid"); + const importRoutes = input.importRoutes.map((route, index) => parseRoute(route, `importRoutes[${index}]`, "IMPORT")); + const exportRoutes = input.exportRoutes.map((route, index) => parseRoute(route, `exportRoutes[${index}]`, "EXPORT")); + const identities = [...importRoutes, ...exportRoutes].map((route) => `${route.operation}:${route.execution}:${route.format}`); + if (new Set(identities).size !== identities.length) throw new IOFormatUIGateError("registry contains duplicate route identities"); + return { schemaVersion: IO_FORMAT_UI_GATE_SCHEMA, task: IO_FORMAT_UI_TASK, parentMatrixSha256: input.parentMatrixSha256, projectFileAccept: IO_FORMAT_PROJECT_ACCEPT, importRoutes, exportRoutes }; +} + +function routeIdentity(route: IOFormatUIRouteIR): string { + return `${route.operation}:${route.execution}:${route.format}:${route.extensions.join("|")}`; +} + +export function validateIOFormatUIRegistry(value: unknown, matrix: IOFormatCapabilityMatrixIR, parentMatrixSha256: string): IOFormatUIRegistryIR { + const parsed = parseIOFormatUIRegistry(value); + const expected = buildIOFormatUIRegistry(matrix, parentMatrixSha256); + const actualRoutes = [...parsed.importRoutes, ...parsed.exportRoutes].map(routeIdentity); + const expectedRoutes = [...expected.importRoutes, ...expected.exportRoutes].map(routeIdentity); + if (parsed.parentMatrixSha256 !== parentMatrixSha256 || actualRoutes.length !== expectedRoutes.length || actualRoutes.some((route, index) => route !== expectedRoutes[index])) throw new IOFormatUIGateError("registry route is not declared by the capability matrix"); + return parsed; +} + +function routeMatches(registry: IOFormatUIRegistryIR, command: IOFormatUICommandRef): boolean { + const routes = command.operation === "IMPORT" ? registry.importRoutes : registry.exportRoutes; + return routes.some((route) => route.format === command.format && route.execution === command.execution); +} + +export function filterIOFormatOperatorCommands(commands: readonly T[], registry: IOFormatUIRegistryIR): T[] { + return commands.filter((command) => !command.ioFormat || routeMatches(registry, command.ioFormat)); +} + +export function gateIOFormatFileSelection(fileName: string, registry: IOFormatUIRegistryIR): { status: "READY"; kind: "BLEND" } | { status: "BLOCKED"; code: "IO_FORMAT_UNSUPPORTED"; extension: string } { + const extension = fileName.trim().toLowerCase().match(/\.[a-z0-9]+$/)?.[0] ?? ""; + if (extension === ".blend") return { status: "READY", kind: "BLEND" }; + const route = registry.importRoutes.find((candidate) => candidate.extensions.includes(extension)); + if (route) return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension }; + return { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension }; +} + +export function ioFormatUIAccept(registry: IOFormatUIRegistryIR): string { + const importExtensions = registry.importRoutes.flatMap((route) => route.extensions); + return [registry.projectFileAccept, ...importExtensions].join(","); +} diff --git a/web/protocol/keyboard-contract.ts b/web/protocol/keyboard-contract.ts new file mode 100644 index 00000000..2573e8f9 --- /dev/null +++ b/web/protocol/keyboard-contract.ts @@ -0,0 +1,34 @@ +export const KEYBOARD_CONTRACT_SCHEMA_VERSION = 1 as const; + +export interface KeyboardObservation { + schemaVersion: typeof KEYBOARD_CONTRACT_SCHEMA_VERSION; + key: string; + code: string; + location: 0 | 1 | 2 | 3; + shiftKey: boolean; + ctrlKey: boolean; + altKey: boolean; + metaKey: boolean; + repeat: boolean; + isComposing: boolean; + deadKey: boolean; +} + +export function observeKeyboardEvent(event: { key?: string; code?: string; location?: number; shiftKey?: boolean; ctrlKey?: boolean; altKey?: boolean; metaKey?: boolean; repeat?: boolean; isComposing?: boolean }): KeyboardObservation { + if (typeof event.key !== "string" || event.key.length === 0 || event.key.length > 128) throw new Error("KEY_IDENTITY_INVALID"); + if (typeof event.code !== "string" || event.code.length === 0 || event.code.length > 64) throw new Error("KEY_CODE_INVALID"); + if (!Number.isInteger(event.location) || event.location! < 0 || event.location! > 3) throw new Error("KEY_LOCATION_INVALID"); + return { + schemaVersion: KEYBOARD_CONTRACT_SCHEMA_VERSION, + key: event.key, + code: event.code, + location: event.location as 0 | 1 | 2 | 3, + shiftKey: Boolean(event.shiftKey), + ctrlKey: Boolean(event.ctrlKey), + altKey: Boolean(event.altKey), + metaKey: Boolean(event.metaKey), + repeat: Boolean(event.repeat), + isComposing: Boolean(event.isComposing), + deadKey: event.key === "Dead", + }; +} diff --git a/web/protocol/library-linked-missing.ts b/web/protocol/library-linked-missing.ts new file mode 100644 index 00000000..51aa87d7 --- /dev/null +++ b/web/protocol/library-linked-missing.ts @@ -0,0 +1,228 @@ +import type { ErrorCode } from "./error"; + +export const LIBRARY_LINKED_MISSING_SCHEMA = 1 as const; +export const LINKED_MISSING_OPERATION = "MARK_MISSING" as const; + +export interface MissingLibraryPlaceholderIR { + kind: "MISSING_LIBRARY"; + sourceLibraryId: string; + dataBlockIds: string[]; +} + +export interface LinkedLibraryReferenceIR { + sourceLibraryId: string; + sourceLocator: string; + sourceSha256: string; + sourceGeneration: number; + sourceRevision: number; + dataBlockIds: string[]; + status: "AVAILABLE" | "MISSING"; + placeholder: MissingLibraryPlaceholderIR | null; +} + +export interface LinkedMissingStateIR { + schemaVersion: typeof LIBRARY_LINKED_MISSING_SCHEMA; + references: LinkedLibraryReferenceIR[]; +} + +export interface LinkedMissingRequestIR { + schemaVersion: typeof LIBRARY_LINKED_MISSING_SCHEMA; + operation: typeof LINKED_MISSING_OPERATION; + sourceLibraryId: string; + sourceLocator: string; + sourceSha256: string; + expectedGeneration: number; + expectedRevision: number; +} + +export interface LinkedMissingDecisionIR { + status: "MARKED" | "STALE"; + code: ErrorCode | null; + sourceLibraryId: string; + state: LinkedMissingStateIR; +} + +export class LinkedMissingValidationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "LinkedMissingValidationError"; + this.code = code; + this.path = path; + } +} + +const LIBRARY_ID = /^library:[a-f0-9]{64}$/; +const SHA256 = /^[a-f0-9]{64}$/; +const SOURCE_LOCATOR = /^[^\u0000\r\n]{1,4096}$/; +const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; +const MAX_REFERENCES = 10_000; +const MAX_DATA_BLOCKS = 256; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path); + } + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); + } +} + +function integer(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); + } + return value; +} + +function libraryId(value: unknown, path: string): string { + if (typeof value !== "string" || !LIBRARY_ID.test(value)) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path); + } + return value; +} + +function digest(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} must be a lowercase SHA-256 digest`, path); + } + return value; +} + +function locator(value: unknown, path: string): string { + if (typeof value !== "string" || !SOURCE_LOCATOR.test(value)) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} is outside the source locator budget`, path); + } + return value; +} + +function dataBlockIds(value: unknown, path: string): string[] { + if (!Array.isArray(value) || value.length === 0 || value.length > MAX_DATA_BLOCKS || + value.some((item) => typeof item !== "string" || !DATA_BLOCK_ID.test(item))) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} is outside its bounded ID list`, path); + } + const result = [...value] as string[]; + if (new Set(result).size !== result.length) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} contains duplicate IDs`, path); + } + return result; +} + +function parsePlaceholder(value: unknown, path: string): MissingLibraryPlaceholderIR | null { + if (value === null) return null; + const placeholder = record(value, path); + exactKeys(placeholder, ["kind", "sourceLibraryId", "dataBlockIds"], path); + if (placeholder.kind !== "MISSING_LIBRARY") { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path}.kind is invalid`, `${path}.kind`); + } + return { + kind: "MISSING_LIBRARY", + sourceLibraryId: libraryId(placeholder.sourceLibraryId, `${path}.sourceLibraryId`), + dataBlockIds: dataBlockIds(placeholder.dataBlockIds, `${path}.dataBlockIds`), + }; +} + +export function parseLinkedLibraryReference(value: unknown, path = "reference"): LinkedLibraryReferenceIR { + const reference = record(value, path); + exactKeys(reference, ["sourceLibraryId", "sourceLocator", "sourceSha256", "sourceGeneration", "sourceRevision", "dataBlockIds", "status", "placeholder"], path); + const sourceLibraryId = libraryId(reference.sourceLibraryId, `${path}.sourceLibraryId`); + const sourceLocator = locator(reference.sourceLocator, `${path}.sourceLocator`); + const sourceSha256 = digest(reference.sourceSha256, `${path}.sourceSha256`); + const sourceGeneration = integer(reference.sourceGeneration, `${path}.sourceGeneration`); + const sourceRevision = integer(reference.sourceRevision, `${path}.sourceRevision`); + const ids = dataBlockIds(reference.dataBlockIds, `${path}.dataBlockIds`); + if (reference.status !== "AVAILABLE" && reference.status !== "MISSING") { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path}.status is invalid`, `${path}.status`); + } + const placeholder = parsePlaceholder(reference.placeholder, `${path}.placeholder`); + if (reference.status === "AVAILABLE" && placeholder !== null) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} available reference cannot have a placeholder`, path); + } + if (reference.status === "MISSING" && (placeholder === null || placeholder.sourceLibraryId !== sourceLibraryId || + placeholder.dataBlockIds.length !== ids.length || placeholder.dataBlockIds.some((id, index) => id !== ids[index]))) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", `${path} missing placeholder must preserve the source IDs`, path); + } + return { sourceLibraryId, sourceLocator, sourceSha256, sourceGeneration, sourceRevision, dataBlockIds: ids, status: reference.status, placeholder }; +} + +export function parseLinkedMissingState(value: unknown): LinkedMissingStateIR { + const state = record(value, "state"); + exactKeys(state, ["schemaVersion", "references"], "state"); + if (state.schemaVersion !== LIBRARY_LINKED_MISSING_SCHEMA) { + throw new LinkedMissingValidationError("PROTOCOL_MISMATCH", "Unsupported linked missing-library state schema", "schemaVersion"); + } + if (!Array.isArray(state.references) || state.references.length > MAX_REFERENCES) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "state.references exceeds its bounded range", "references"); + } + const references = state.references.map((item, index) => parseLinkedLibraryReference(item, `state.references[${index}]`)); + const identities = references.map((item) => `${item.sourceLibraryId}:${item.sourceGeneration}`); + if (new Set(identities).size !== identities.length) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "state contains duplicate source generations", "references"); + } + return { schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, references }; +} + +export function parseLinkedMissingRequest(value: unknown): LinkedMissingRequestIR { + const request = record(value, "request"); + exactKeys(request, ["schemaVersion", "operation", "sourceLibraryId", "sourceLocator", "sourceSha256", "expectedGeneration", "expectedRevision"], "request"); + if (request.schemaVersion !== LIBRARY_LINKED_MISSING_SCHEMA) { + throw new LinkedMissingValidationError("PROTOCOL_MISMATCH", "Unsupported linked missing-library request schema", "schemaVersion"); + } + if (request.operation !== LINKED_MISSING_OPERATION) { + throw new LinkedMissingValidationError("TASK_VALIDATION_FAILED", "missing-library operation is invalid", "operation"); + } + return { + schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, + operation: LINKED_MISSING_OPERATION, + sourceLibraryId: libraryId(request.sourceLibraryId, "request.sourceLibraryId"), + sourceLocator: locator(request.sourceLocator, "request.sourceLocator"), + sourceSha256: digest(request.sourceSha256, "request.sourceSha256"), + expectedGeneration: integer(request.expectedGeneration, "request.expectedGeneration"), + expectedRevision: integer(request.expectedRevision, "request.expectedRevision"), + }; +} + +function cloneState(state: LinkedMissingStateIR): LinkedMissingStateIR { + return { + schemaVersion: LIBRARY_LINKED_MISSING_SCHEMA, + references: state.references.map((reference) => ({ + ...reference, + dataBlockIds: [...reference.dataBlockIds], + placeholder: reference.placeholder === null ? null : { + ...reference.placeholder, + dataBlockIds: [...reference.placeholder.dataBlockIds], + }, + })), + }; +} + +export function markLinkedLibraryMissing(stateValue: unknown, requestValue: unknown): LinkedMissingDecisionIR { + const state = parseLinkedMissingState(stateValue); + const request = parseLinkedMissingRequest(requestValue); + const index = state.references.findIndex((reference) => + reference.sourceLibraryId === request.sourceLibraryId && reference.sourceGeneration === request.expectedGeneration, + ); + if (index === -1 || state.references[index].sourceRevision !== request.expectedRevision) { + return { status: "STALE", code: "REVISION_CONFLICT", sourceLibraryId: request.sourceLibraryId, state: cloneState(state) }; + } + const current = state.references[index]; + if (current.sourceLocator !== request.sourceLocator || current.sourceSha256 !== request.sourceSha256) { + return { status: "STALE", code: "ASSET_SOURCE_HASH_MISMATCH", sourceLibraryId: request.sourceLibraryId, state: cloneState(state) }; + } + const missing: LinkedLibraryReferenceIR = { + ...current, + status: "MISSING", + placeholder: { kind: "MISSING_LIBRARY", sourceLibraryId: current.sourceLibraryId, dataBlockIds: [...current.dataBlockIds] }, + dataBlockIds: [...current.dataBlockIds], + }; + const references = state.references.map((reference, itemIndex) => itemIndex === index ? missing : reference); + return { status: "MARKED", code: null, sourceLibraryId: request.sourceLibraryId, state: { schemaVersion: 1, references: references.map((reference) => ({ ...reference, dataBlockIds: [...reference.dataBlockIds], placeholder: reference.placeholder === null ? null : { ...reference.placeholder, dataBlockIds: [...reference.placeholder.dataBlockIds] } })) } }; +} diff --git a/web/protocol/library-linked-mutation.ts b/web/protocol/library-linked-mutation.ts new file mode 100644 index 00000000..44141dd0 --- /dev/null +++ b/web/protocol/library-linked-mutation.ts @@ -0,0 +1,91 @@ +import { blockedGate, capabilityIssue, type CapabilityGateResult } from "./capability-gates"; +import type { ErrorCode } from "./error"; + +export const LIBRARY_LINKED_MUTATION_SCHEMA = 1 as const; +export const LINKED_DATA_WRITER_OPERATIONS = [ + "OBJECT_TRANSFORM", + "MESH_GEOMETRY", + "MESH_MATERIAL_SLOT", + "MATERIAL_PROPERTIES", + "MATERIAL_IMAGE_NODE", + "IMAGE_PACKED_DATA", +] as const; +export type LinkedDataWriterOperation = typeof LINKED_DATA_WRITER_OPERATIONS[number]; + +export interface LinkedDataMutationIR { + schemaVersion: typeof LIBRARY_LINKED_MUTATION_SCHEMA; + operation: LinkedDataWriterOperation; + dataBlockId: string; + baseRevision: number; + owner: "SOURCE_LIBRARY"; + linkedLibrary: true; + readOnly: true; +} + +export class LinkedDataMutationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(message); + this.name = "LinkedDataMutationError"; + this.code = code; + this.path = path; + } +} + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys(value: Record): void { + const expected = ["schemaVersion", "operation", "dataBlockId", "baseRevision", "owner", "linkedLibrary", "readOnly"]; + const actual = Object.keys(value).sort(); + if (actual.length !== expected.length || actual.some((key, index) => key !== expected.slice().sort()[index])) { + throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "linked data mutation contains unsupported fields"); + } +} + +export function parseLinkedDataMutation(value: unknown): LinkedDataMutationIR { + if (!record(value) || value.schemaVersion !== LIBRARY_LINKED_MUTATION_SCHEMA) { + throw new LinkedDataMutationError("PROTOCOL_MISMATCH", "Unsupported linked data mutation schema"); + } + exactKeys(value); + if (!LINKED_DATA_WRITER_OPERATIONS.includes(value.operation as LinkedDataWriterOperation)) { + throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "linked data mutation operation is unsupported", "operation"); + } + if (typeof value.dataBlockId !== "string" || value.dataBlockId.length === 0 || value.dataBlockId.length > 256) { + throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "dataBlockId is invalid", "dataBlockId"); + } + if (typeof value.baseRevision !== "number" || !Number.isSafeInteger(value.baseRevision) || value.baseRevision < 0) { + throw new LinkedDataMutationError("TASK_VALIDATION_FAILED", "baseRevision is invalid", "baseRevision"); + } + if (value.owner !== "SOURCE_LIBRARY" || value.linkedLibrary !== true || value.readOnly !== true) { + throw new LinkedDataMutationError("LINKED_DATA_MUTATION_BLOCKED", "linked data must remain SOURCE_LIBRARY/readOnly", "ownership"); + } + return { + schemaVersion: LIBRARY_LINKED_MUTATION_SCHEMA, + operation: value.operation as LinkedDataWriterOperation, + dataBlockId: value.dataBlockId, + baseRevision: value.baseRevision, + owner: "SOURCE_LIBRARY", + linkedLibrary: true, + readOnly: true, + }; +} + +export function gateLinkedDataMutation(value: unknown, currentRevision: number): CapabilityGateResult { + let request: LinkedDataMutationIR; + try { + request = parseLinkedDataMutation(value); + } + catch (error) { + const code = error instanceof LinkedDataMutationError ? error.code : "TASK_VALIDATION_FAILED"; + const message = error instanceof Error ? error.message : "linked data mutation is invalid"; + return blockedGate("N-023", "LINKED_DATA_WRITER", [capabilityIssue(code, message, error instanceof LinkedDataMutationError ? error.path : undefined, false)]); + } + if (!Number.isSafeInteger(currentRevision) || currentRevision < 0 || request.baseRevision !== currentRevision) { + return blockedGate("N-023", `LINKED_${request.operation}`, [capabilityIssue("REVISION_CONFLICT", "linked data mutation revision is stale", "baseRevision", false)]); + } + return blockedGate("N-023", `LINKED_${request.operation}`, [capabilityIssue("LINKED_DATA_MUTATION_BLOCKED", "linked-library data is read-only", "readOnly", false)]); +} diff --git a/web/protocol/library-linked-reload.ts b/web/protocol/library-linked-reload.ts new file mode 100644 index 00000000..606b46f8 --- /dev/null +++ b/web/protocol/library-linked-reload.ts @@ -0,0 +1,222 @@ +import type { ErrorCode } from "./error"; + +export const LIBRARY_LINKED_RELOAD_SCHEMA = 1 as const; +export const LINKED_RELOAD_OPERATION = "RELOAD" as const; + +export interface LinkedSnapshotDataBlockIR { + dataBlockId: string; + owner: "SOURCE_LIBRARY"; + readOnly: true; +} + +export interface LinkedSnapshotIR { + sourceLibraryId: string; + sourceGeneration: number; + sourceRevision: number; + dependencyClosureSha256: string; + graphSha256: string; + dataBlocks: LinkedSnapshotDataBlockIR[]; +} + +export interface LinkedReloadStateIR { + schemaVersion: typeof LIBRARY_LINKED_RELOAD_SCHEMA; + snapshots: LinkedSnapshotIR[]; +} + +export interface LinkedReloadRequestIR { + schemaVersion: typeof LIBRARY_LINKED_RELOAD_SCHEMA; + operation: typeof LINKED_RELOAD_OPERATION; + sourceLibraryId: string; + expectedGeneration: number; + expectedRevision: number; + replacement: LinkedSnapshotIR; +} + +export interface LinkedReloadDecisionIR { + status: "REPLACED" | "STALE"; + code: ErrorCode | null; + sourceLibraryId: string; + replacedGeneration: number; + replacementGeneration: number; + state: LinkedReloadStateIR; +} + +export class LinkedReloadValidationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "LinkedReloadValidationError"; + this.code = code; + this.path = path; + } +} + +const LIBRARY_ID = /^library:[a-f0-9]{64}$/; +const SHA256 = /^[a-f0-9]{64}$/; +const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; +const MAX_SNAPSHOTS = 10_000; +const MAX_DATA_BLOCKS = 256; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path); + } + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const keys = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (keys.length !== allowed.length || keys.some((key, index) => key !== allowed[index])) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); + } +} + +function integer(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); + } + return value; +} + +function libraryId(value: unknown, path: string): string { + if (typeof value !== "string" || !LIBRARY_ID.test(value)) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path); + } + return value; +} + +function digest(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path} must be a lowercase SHA-256 digest`, path); + } + return value; +} + +function dataBlock(value: unknown, path: string): LinkedSnapshotDataBlockIR { + const item = record(value, path); + exactKeys(item, ["dataBlockId", "owner", "readOnly"], path); + if (typeof item.dataBlockId !== "string" || !DATA_BLOCK_ID.test(item.dataBlockId)) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path}.dataBlockId is invalid`, `${path}.dataBlockId`); + } + if (item.owner !== "SOURCE_LIBRARY" || item.readOnly !== true) { + throw new LinkedReloadValidationError("LINKED_DATA_MUTATION_BLOCKED", `${path} must remain source-library/read-only`, path); + } + return { dataBlockId: item.dataBlockId, owner: "SOURCE_LIBRARY", readOnly: true }; +} + +export function parseLinkedSnapshot(value: unknown, path = "snapshot"): LinkedSnapshotIR { + const snapshot = record(value, path); + exactKeys(snapshot, ["sourceLibraryId", "sourceGeneration", "sourceRevision", "dependencyClosureSha256", "graphSha256", "dataBlocks"], path); + if (!Array.isArray(snapshot.dataBlocks) || snapshot.dataBlocks.length === 0 || snapshot.dataBlocks.length > MAX_DATA_BLOCKS) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path}.dataBlocks is outside its bounded range`, `${path}.dataBlocks`); + } + const dataBlocks = snapshot.dataBlocks.map((item, index) => dataBlock(item, `${path}.dataBlocks[${index}]`)); + if (new Set(dataBlocks.map((item) => item.dataBlockId)).size !== dataBlocks.length) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", `${path}.dataBlocks contains duplicate IDs`, `${path}.dataBlocks`); + } + return { + sourceLibraryId: libraryId(snapshot.sourceLibraryId, `${path}.sourceLibraryId`), + sourceGeneration: integer(snapshot.sourceGeneration, `${path}.sourceGeneration`), + sourceRevision: integer(snapshot.sourceRevision, `${path}.sourceRevision`), + dependencyClosureSha256: digest(snapshot.dependencyClosureSha256, `${path}.dependencyClosureSha256`), + graphSha256: digest(snapshot.graphSha256, `${path}.graphSha256`), + dataBlocks, + }; +} + +export function parseLinkedReloadState(value: unknown): LinkedReloadStateIR { + const state = record(value, "state"); + exactKeys(state, ["schemaVersion", "snapshots"], "state"); + if (state.schemaVersion !== LIBRARY_LINKED_RELOAD_SCHEMA) { + throw new LinkedReloadValidationError("PROTOCOL_MISMATCH", "Unsupported linked reload state schema", "schemaVersion"); + } + if (!Array.isArray(state.snapshots) || state.snapshots.length > MAX_SNAPSHOTS) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "state.snapshots exceeds its bounded range", "snapshots"); + } + const snapshots = state.snapshots.map((item, index) => parseLinkedSnapshot(item, `state.snapshots[${index}]`)); + const identities = snapshots.map((item) => `${item.sourceLibraryId}:${item.sourceGeneration}`); + if (new Set(identities).size !== identities.length) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "state contains duplicate library generations", "snapshots"); + } + return { schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA, snapshots }; +} + +export function parseLinkedReloadRequest(value: unknown): LinkedReloadRequestIR { + const request = record(value, "request"); + exactKeys(request, ["schemaVersion", "operation", "sourceLibraryId", "expectedGeneration", "expectedRevision", "replacement"], "request"); + if (request.schemaVersion !== LIBRARY_LINKED_RELOAD_SCHEMA) { + throw new LinkedReloadValidationError("PROTOCOL_MISMATCH", "Unsupported linked reload request schema", "schemaVersion"); + } + if (request.operation !== LINKED_RELOAD_OPERATION) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "linked reload operation is invalid", "operation"); + } + const replacement = parseLinkedSnapshot(request.replacement, "request.replacement"); + const sourceLibraryId = libraryId(request.sourceLibraryId, "request.sourceLibraryId"); + if (replacement.sourceLibraryId !== sourceLibraryId) { + throw new LinkedReloadValidationError("TASK_VALIDATION_FAILED", "replacement must retain the requested source library", "replacement.sourceLibraryId"); + } + return { + schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA, + operation: LINKED_RELOAD_OPERATION, + sourceLibraryId, + expectedGeneration: integer(request.expectedGeneration, "request.expectedGeneration"), + expectedRevision: integer(request.expectedRevision, "request.expectedRevision"), + replacement, + }; +} + +function cloneState(state: LinkedReloadStateIR): LinkedReloadStateIR { + return { + schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA, + snapshots: state.snapshots.map((snapshot) => ({ + ...snapshot, + dataBlocks: snapshot.dataBlocks.map((dataBlock) => ({ ...dataBlock })), + })), + }; +} + +export function reloadMatchingLinkedSnapshot(stateValue: unknown, requestValue: unknown): LinkedReloadDecisionIR { + const state = parseLinkedReloadState(stateValue); + const request = parseLinkedReloadRequest(requestValue); + const matchingIndex = state.snapshots.findIndex((snapshot) => + snapshot.sourceLibraryId === request.sourceLibraryId && snapshot.sourceGeneration === request.expectedGeneration, + ); + if (matchingIndex === -1 || state.snapshots[matchingIndex].sourceRevision !== request.expectedRevision) { + return { + status: "STALE", + code: "REVISION_CONFLICT", + sourceLibraryId: request.sourceLibraryId, + replacedGeneration: request.expectedGeneration, + replacementGeneration: request.replacement.sourceGeneration, + state: cloneState(state), + }; + } + const current = state.snapshots[matchingIndex]; + if (request.replacement.sourceLibraryId !== request.sourceLibraryId || + request.replacement.sourceGeneration !== request.expectedGeneration + 1 || + request.replacement.sourceRevision <= current.sourceRevision) { + return { + status: "STALE", + code: "REVISION_CONFLICT", + sourceLibraryId: request.sourceLibraryId, + replacedGeneration: request.expectedGeneration, + replacementGeneration: request.replacement.sourceGeneration, + state: cloneState(state), + }; + } + const snapshots = state.snapshots.map((snapshot, index) => index === matchingIndex ? request.replacement : snapshot); + return { + status: "REPLACED", + code: null, + sourceLibraryId: request.sourceLibraryId, + replacedGeneration: current.sourceGeneration, + replacementGeneration: request.replacement.sourceGeneration, + state: { schemaVersion: LIBRARY_LINKED_RELOAD_SCHEMA, snapshots: snapshots.map((snapshot) => ({ + ...snapshot, + dataBlocks: snapshot.dataBlocks.map((dataBlock) => ({ ...dataBlock })), + })) }, + }; +} diff --git a/web/protocol/library-negative-cases.ts b/web/protocol/library-negative-cases.ts new file mode 100644 index 00000000..625bb82a --- /dev/null +++ b/web/protocol/library-negative-cases.ts @@ -0,0 +1,168 @@ +import type { ErrorCode } from "./error"; + +export const LIBRARY_NEGATIVE_CASE_SCHEMA = 1 as const; + +export interface LibraryNegativeLibraryIR { + libraryId: string; + dependencyIds: string[]; +} + +export interface LibraryNegativeDataBlockIR { + dataBlockId: string; + sourceLibraryId: string; +} + +export interface LibraryNegativeCrossReferenceIR { + fromLibraryId: string; + toLibraryId: string; +} + +export interface LibraryNegativeReloadIR { + sourceLibraryId: string; + generation: number; +} + +export interface LibraryNegativeInputIR { + schemaVersion: typeof LIBRARY_NEGATIVE_CASE_SCHEMA; + libraries: LibraryNegativeLibraryIR[]; + dataBlocks: LibraryNegativeDataBlockIR[]; + crossReferences: LibraryNegativeCrossReferenceIR[]; + reloads: LibraryNegativeReloadIR[]; +} + +export interface LibraryNegativeValidationIR { + status: "VALID"; +} + +export class LibraryNegativeValidationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "LibraryNegativeValidationError"; + this.code = code; + this.path = path; + } +} + +const LIBRARY_ID = /^library:[a-f0-9]{64}$/; +const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; +const MAX_ENTRIES = 10_000; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); +} + +function libraryId(value: unknown, path: string): string { + if (typeof value !== "string" || !LIBRARY_ID.test(value)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path); + return value; +} + +function dataBlockId(value: unknown, path: string): string { + if (typeof value !== "string" || !DATA_BLOCK_ID.test(value)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} is invalid`, path); + return value; +} + +function integer(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); + return value; +} + +function parseLibrary(value: unknown, path: string): LibraryNegativeLibraryIR { + const item = record(value, path); + exactKeys(item, ["libraryId", "dependencyIds"], path); + if (!Array.isArray(item.dependencyIds) || item.dependencyIds.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `${path}.dependencyIds exceeds its bound`, path); + return { libraryId: libraryId(item.libraryId, `${path}.libraryId`), dependencyIds: item.dependencyIds.map((dependency, index) => libraryId(dependency, `${path}.dependencyIds[${index}]`)) }; +} + +function parseDataBlock(value: unknown, path: string): LibraryNegativeDataBlockIR { + const item = record(value, path); + exactKeys(item, ["dataBlockId", "sourceLibraryId"], path); + return { dataBlockId: dataBlockId(item.dataBlockId, `${path}.dataBlockId`), sourceLibraryId: libraryId(item.sourceLibraryId, `${path}.sourceLibraryId`) }; +} + +function parseCrossReference(value: unknown, path: string): LibraryNegativeCrossReferenceIR { + const item = record(value, path); + exactKeys(item, ["fromLibraryId", "toLibraryId"], path); + return { fromLibraryId: libraryId(item.fromLibraryId, `${path}.fromLibraryId`), toLibraryId: libraryId(item.toLibraryId, `${path}.toLibraryId`) }; +} + +function parseReload(value: unknown, path: string): LibraryNegativeReloadIR { + const item = record(value, path); + exactKeys(item, ["sourceLibraryId", "generation"], path); + return { sourceLibraryId: libraryId(item.sourceLibraryId, `${path}.sourceLibraryId`), generation: integer(item.generation, `${path}.generation`) }; +} + +export function parseLibraryNegativeInput(value: unknown): LibraryNegativeInputIR { + const input = record(value, "input"); + exactKeys(input, ["schemaVersion", "libraries", "dataBlocks", "crossReferences", "reloads"], "input"); + if (input.schemaVersion !== LIBRARY_NEGATIVE_CASE_SCHEMA) throw new LibraryNegativeValidationError("PROTOCOL_MISMATCH", "Unsupported library negative-case schema", "schemaVersion"); + const libraries = input.libraries; + const dataBlocks = input.dataBlocks; + const crossReferences = input.crossReferences; + const reloads = input.reloads; + if (!Array.isArray(libraries) || libraries.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.libraries exceeds its bound", "input.libraries"); + if (!Array.isArray(dataBlocks) || dataBlocks.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.dataBlocks exceeds its bound", "input.dataBlocks"); + if (!Array.isArray(crossReferences) || crossReferences.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.crossReferences exceeds its bound", "input.crossReferences"); + if (!Array.isArray(reloads) || reloads.length > MAX_ENTRIES) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "input.reloads exceeds its bound", "input.reloads"); + return { + schemaVersion: LIBRARY_NEGATIVE_CASE_SCHEMA, + libraries: libraries.map((item, index) => parseLibrary(item, `libraries[${index}]`)), + dataBlocks: dataBlocks.map((item, index) => parseDataBlock(item, `dataBlocks[${index}]`)), + crossReferences: crossReferences.map((item, index) => parseCrossReference(item, `crossReferences[${index}]`)), + reloads: reloads.map((item, index) => parseReload(item, `reloads[${index}]`)), + }; +} + +function assertNoCycle(nodes: Set, edges: Map, label: string): void { + const active = new Set(); + const complete = new Set(); + const visit = (node: string): void => { + if (active.has(node)) throw new LibraryNegativeValidationError("LIBRARY_DEPENDENCY_CYCLE", `${label} contains a cycle at ${node}`, label); + if (complete.has(node)) return; + active.add(node); + for (const dependency of edges.get(node) ?? []) { + if (!nodes.has(dependency)) throw new LibraryNegativeValidationError("ASSET_MANIFEST_INVALID", `${label} references a missing library ${dependency}`, label); + visit(dependency); + } + active.delete(node); + complete.add(node); + }; + for (const node of nodes) visit(node); +} + +export function validateLibraryNegativeInput(value: unknown): LibraryNegativeValidationIR { + const input = parseLibraryNegativeInput(value); + const libraries = new Set(input.libraries.map((item) => item.libraryId)); + if (libraries.size !== input.libraries.length) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", "duplicate library ID", "libraries"); + const libraryEdges = new Map(input.libraries.map((item) => [item.libraryId, item.dependencyIds])); + assertNoCycle(libraries, libraryEdges, "library dependencies"); + const crossEdges = new Map(); + for (const item of input.crossReferences) { + if (!libraries.has(item.fromLibraryId) || !libraries.has(item.toLibraryId)) throw new LibraryNegativeValidationError("ASSET_MANIFEST_INVALID", "cross-library reference names a missing library", "crossReferences"); + crossEdges.set(item.fromLibraryId, [...(crossEdges.get(item.fromLibraryId) ?? []), item.toLibraryId]); + } + assertNoCycle(libraries, crossEdges, "cross-library references"); + const dataBlocks = new Set(); + for (const item of input.dataBlocks) { + if (!libraries.has(item.sourceLibraryId)) throw new LibraryNegativeValidationError("ASSET_SOURCE_HASH_MISMATCH", "data-block source library is missing", "dataBlocks"); + if (dataBlocks.has(item.dataBlockId)) throw new LibraryNegativeValidationError("TASK_VALIDATION_FAILED", `duplicate data-block ID ${item.dataBlockId}`, "dataBlocks"); + dataBlocks.add(item.dataBlockId); + } + const reloads = new Set(); + for (const item of input.reloads) { + if (!libraries.has(item.sourceLibraryId)) throw new LibraryNegativeValidationError("ASSET_SOURCE_HASH_MISMATCH", "reload source library is missing", "reloads"); + const identity = `${item.sourceLibraryId}:${item.generation}`; + if (reloads.has(identity)) throw new LibraryNegativeValidationError("REVISION_CONFLICT", `duplicate reload ${identity}`, "reloads"); + reloads.add(identity); + } + return { status: "VALID" }; +} diff --git a/web/protocol/library-override-freshness.ts b/web/protocol/library-override-freshness.ts new file mode 100644 index 00000000..e7b4c170 --- /dev/null +++ b/web/protocol/library-override-freshness.ts @@ -0,0 +1,165 @@ +import type { ErrorCode } from "./error"; + +export const LIBRARY_OVERRIDE_FRESHNESS_SCHEMA = 1 as const; +export const LIBRARY_OVERRIDE_COMMIT_OPERATION = "COMMIT_OVERRIDE" as const; + +export interface OverrideFreshnessStateIR { + schemaVersion: typeof LIBRARY_OVERRIDE_FRESHNESS_SCHEMA; + sourceLibraryId: string; + sourceGeneration: number; + sourceRevision: number; + dependencyClosureSha256: string; + invalidationToken: string; + localDataBlockId: string; + referenceSourceDataBlockId: string; + hierarchyRootDataBlockId: string; + owner: "LOCAL_OVERRIDE"; + readOnly: false; + referenceReadOnly: true; +} + +export interface OverrideFreshnessRequestIR { + schemaVersion: typeof LIBRARY_OVERRIDE_FRESHNESS_SCHEMA; + operation: typeof LIBRARY_OVERRIDE_COMMIT_OPERATION; + sourceLibraryId: string; + sourceGeneration: number; + sourceRevision: number; + dependencyClosureSha256: string; + invalidationToken: string; + baseRevision: number; + localDataBlockId: string; + referenceSourceDataBlockId: string; + hierarchyRootDataBlockId: string; + owner: "LOCAL_OVERRIDE"; + readOnly: false; + referenceReadOnly: true; +} + +export interface OverrideFreshnessDecisionIR { + status: "READY" | "BLOCKED"; + code: ErrorCode | null; +} + +export class OverrideFreshnessValidationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "OverrideFreshnessValidationError"; + this.code = code; + this.path = path; + } +} + +const LIBRARY_ID = /^library:[a-f0-9]{64}$/; +const SHA256 = /^[a-f0-9]{64}$/; +const TOKEN = /^override-token:[a-f0-9]{64}$/; +const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); +} + +function integer(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); + return value; +} + +function id(value: unknown, path: string): string { + if (typeof value !== "string" || !DATA_BLOCK_ID.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} is invalid`, path); + return value; +} + +function digest(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be a SHA-256 digest`, path); + return value; +} + +function library(value: unknown, path: string): string { + if (typeof value !== "string" || !LIBRARY_ID.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be a library identity`, path); + return value; +} + +function token(value: unknown, path: string): string { + if (typeof value !== "string" || !TOKEN.test(value)) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", `${path} must be an invalidation token`, path); + return value; +} + +function ownership(value: Record, path: string): void { + if (value.owner !== "LOCAL_OVERRIDE" || value.readOnly !== false || value.referenceReadOnly !== true) throw new OverrideFreshnessValidationError("LINKED_DATA_MUTATION_BLOCKED", `${path} ownership semantics are invalid`, path); +} + +const COMMON_KEYS = ["schemaVersion", "sourceLibraryId", "sourceGeneration", "sourceRevision", "dependencyClosureSha256", "invalidationToken", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId", "owner", "readOnly", "referenceReadOnly"] as const; + +export function parseOverrideFreshnessState(value: unknown): OverrideFreshnessStateIR { + const state = record(value, "state"); + exactKeys(state, COMMON_KEYS, "state"); + if (state.schemaVersion !== LIBRARY_OVERRIDE_FRESHNESS_SCHEMA) throw new OverrideFreshnessValidationError("PROTOCOL_MISMATCH", "Unsupported override freshness state schema", "schemaVersion"); + ownership(state, "state"); + const sourceLibraryId = library(state.sourceLibraryId, "sourceLibraryId"); + return { + schemaVersion: LIBRARY_OVERRIDE_FRESHNESS_SCHEMA, + sourceLibraryId, + sourceGeneration: integer(state.sourceGeneration, "sourceGeneration"), + sourceRevision: integer(state.sourceRevision, "sourceRevision"), + dependencyClosureSha256: digest(state.dependencyClosureSha256, "dependencyClosureSha256"), + invalidationToken: token(state.invalidationToken, "invalidationToken"), + localDataBlockId: id(state.localDataBlockId, "localDataBlockId"), + referenceSourceDataBlockId: id(state.referenceSourceDataBlockId, "referenceSourceDataBlockId"), + hierarchyRootDataBlockId: id(state.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"), + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + }; +} + +export function parseOverrideFreshnessRequest(value: unknown): OverrideFreshnessRequestIR { + const request = record(value, "request"); + exactKeys(request, [...COMMON_KEYS, "operation", "baseRevision"], "request"); + if (request.schemaVersion !== LIBRARY_OVERRIDE_FRESHNESS_SCHEMA) throw new OverrideFreshnessValidationError("PROTOCOL_MISMATCH", "Unsupported override freshness request schema", "schemaVersion"); + if (request.operation !== LIBRARY_OVERRIDE_COMMIT_OPERATION) throw new OverrideFreshnessValidationError("TASK_VALIDATION_FAILED", "override commit operation is invalid", "operation"); + ownership(request, "request"); + return { + schemaVersion: LIBRARY_OVERRIDE_FRESHNESS_SCHEMA, + operation: LIBRARY_OVERRIDE_COMMIT_OPERATION, + sourceLibraryId: library(request.sourceLibraryId, "sourceLibraryId"), + sourceGeneration: integer(request.sourceGeneration, "sourceGeneration"), + sourceRevision: integer(request.sourceRevision, "sourceRevision"), + dependencyClosureSha256: digest(request.dependencyClosureSha256, "dependencyClosureSha256"), + invalidationToken: token(request.invalidationToken, "invalidationToken"), + baseRevision: integer(request.baseRevision, "baseRevision"), + localDataBlockId: id(request.localDataBlockId, "localDataBlockId"), + referenceSourceDataBlockId: id(request.referenceSourceDataBlockId, "referenceSourceDataBlockId"), + hierarchyRootDataBlockId: id(request.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"), + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + }; +} + +export function gateOverrideFreshness(stateValue: unknown, requestValue: unknown): OverrideFreshnessDecisionIR { + let state: OverrideFreshnessStateIR; + let request: OverrideFreshnessRequestIR; + try { + state = parseOverrideFreshnessState(stateValue); + request = parseOverrideFreshnessRequest(requestValue); + } + catch (error) { + return { status: "BLOCKED", code: error instanceof OverrideFreshnessValidationError ? error.code : "TASK_VALIDATION_FAILED" }; + } + if (request.baseRevision !== state.sourceRevision || request.sourceLibraryId !== state.sourceLibraryId || request.sourceGeneration !== state.sourceGeneration || request.sourceRevision !== state.sourceRevision || request.dependencyClosureSha256 !== state.dependencyClosureSha256 || request.invalidationToken !== state.invalidationToken) { + return { status: "BLOCKED", code: "REVISION_CONFLICT" }; + } + if (request.localDataBlockId !== state.localDataBlockId || request.referenceSourceDataBlockId !== state.referenceSourceDataBlockId || request.hierarchyRootDataBlockId !== state.hierarchyRootDataBlockId) { + return { status: "BLOCKED", code: "ASSET_SOURCE_HASH_MISMATCH" }; + } + return { status: "READY", code: null }; +} diff --git a/web/protocol/library-override-writer.ts b/web/protocol/library-override-writer.ts new file mode 100644 index 00000000..ad448b57 --- /dev/null +++ b/web/protocol/library-override-writer.ts @@ -0,0 +1,156 @@ +import type { ErrorCode } from "./error"; + +export const LIBRARY_OVERRIDE_WRITER_SCHEMA = 1 as const; +export const LIBRARY_OVERRIDE_WRITER_OPERATION = "SET_M12_OVERRIDE_VALUE" as const; +export const LIBRARY_OVERRIDE_PROPERTY_PATH = '["m12_override_value"]' as const; + +export interface OverrideWriterStateIR { + schemaVersion: typeof LIBRARY_OVERRIDE_WRITER_SCHEMA; + revision: number; + localDataBlockId: string; + referenceSourceDataBlockId: string; + hierarchyRootDataBlockId: string; + owner: "LOCAL_OVERRIDE"; + readOnly: false; + referenceReadOnly: true; + propertyPath: typeof LIBRARY_OVERRIDE_PROPERTY_PATH; + value: number; +} + +export interface OverrideWriterRequestIR { + schemaVersion: typeof LIBRARY_OVERRIDE_WRITER_SCHEMA; + operation: typeof LIBRARY_OVERRIDE_WRITER_OPERATION; + baseRevision: number; + localDataBlockId: string; + referenceSourceDataBlockId: string; + hierarchyRootDataBlockId: string; + owner: "LOCAL_OVERRIDE"; + readOnly: false; + referenceReadOnly: true; + propertyPath: typeof LIBRARY_OVERRIDE_PROPERTY_PATH; + value: number; +} + +export interface OverrideWriterDecisionIR { + status: "APPLIED" | "BLOCKED"; + code: ErrorCode | null; + state: OverrideWriterStateIR; +} + +export class OverrideWriterValidationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "OverrideWriterValidationError"; + this.code = code; + this.path = path; + } +} + +const DATA_BLOCK_ID = /^[A-Za-z0-9][A-Za-z0-9:._/ -]{0,255}$/; +const MAX_VALUE = 1_000_000; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} must be an object`, path); + } + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) { + throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} contains undeclared fields`, path); + } +} + +function integer(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { + throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} must be a safe integer >= 0`, path); + } + return value; +} + +function dataBlockId(value: unknown, path: string): string { + if (typeof value !== "string" || !DATA_BLOCK_ID.test(value)) { + throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} is invalid`, path); + } + return value; +} + +function valueNumber(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > MAX_VALUE) { + throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", `${path} is outside the bounded float range`, path); + } + return value; +} + +export function parseOverrideWriterState(value: unknown): OverrideWriterStateIR { + const state = record(value, "state"); + exactKeys(state, ["schemaVersion", "revision", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId", "owner", "readOnly", "referenceReadOnly", "propertyPath", "value"], "state"); + if (state.schemaVersion !== LIBRARY_OVERRIDE_WRITER_SCHEMA) throw new OverrideWriterValidationError("PROTOCOL_MISMATCH", "Unsupported override writer state schema", "schemaVersion"); + if (state.owner !== "LOCAL_OVERRIDE" || state.readOnly !== false || state.referenceReadOnly !== true) throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", "state ownership semantics are invalid", "owner"); + if (state.propertyPath !== LIBRARY_OVERRIDE_PROPERTY_PATH) throw new OverrideWriterValidationError("EDITOR_WRITER_UNAVAILABLE", "state property path is not the verified writer path", "propertyPath"); + return { + schemaVersion: LIBRARY_OVERRIDE_WRITER_SCHEMA, + revision: integer(state.revision, "revision"), + localDataBlockId: dataBlockId(state.localDataBlockId, "localDataBlockId"), + referenceSourceDataBlockId: dataBlockId(state.referenceSourceDataBlockId, "referenceSourceDataBlockId"), + hierarchyRootDataBlockId: dataBlockId(state.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"), + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + propertyPath: LIBRARY_OVERRIDE_PROPERTY_PATH, + value: valueNumber(state.value, "value"), + }; +} + +export function parseOverrideWriterRequest(value: unknown): OverrideWriterRequestIR { + const request = record(value, "request"); + exactKeys(request, ["schemaVersion", "operation", "baseRevision", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId", "owner", "readOnly", "referenceReadOnly", "propertyPath", "value"], "request"); + if (request.schemaVersion !== LIBRARY_OVERRIDE_WRITER_SCHEMA) throw new OverrideWriterValidationError("PROTOCOL_MISMATCH", "Unsupported override writer request schema", "schemaVersion"); + if (request.operation !== LIBRARY_OVERRIDE_WRITER_OPERATION) throw new OverrideWriterValidationError("TASK_VALIDATION_FAILED", "override writer operation is invalid", "operation"); + if (request.owner !== "LOCAL_OVERRIDE" || request.readOnly !== false || request.referenceReadOnly !== true) throw new OverrideWriterValidationError("LINKED_DATA_MUTATION_BLOCKED", "override writer must retain local override ownership", "owner"); + if (request.propertyPath !== LIBRARY_OVERRIDE_PROPERTY_PATH) throw new OverrideWriterValidationError("EDITOR_WRITER_UNAVAILABLE", "only the verified override property is writable", "propertyPath"); + return { + schemaVersion: LIBRARY_OVERRIDE_WRITER_SCHEMA, + operation: LIBRARY_OVERRIDE_WRITER_OPERATION, + baseRevision: integer(request.baseRevision, "baseRevision"), + localDataBlockId: dataBlockId(request.localDataBlockId, "localDataBlockId"), + referenceSourceDataBlockId: dataBlockId(request.referenceSourceDataBlockId, "referenceSourceDataBlockId"), + hierarchyRootDataBlockId: dataBlockId(request.hierarchyRootDataBlockId, "hierarchyRootDataBlockId"), + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + propertyPath: LIBRARY_OVERRIDE_PROPERTY_PATH, + value: valueNumber(request.value, "value"), + }; +} + +function blocked(state: OverrideWriterStateIR, code: ErrorCode): OverrideWriterDecisionIR { + return { status: "BLOCKED", code, state: { ...state } }; +} + +export function applyOverrideWriter(stateValue: unknown, requestValue: unknown): OverrideWriterDecisionIR { + const state = parseOverrideWriterState(stateValue); + let request: OverrideWriterRequestIR; + try { + request = parseOverrideWriterRequest(requestValue); + } + catch (error) { + const code = error instanceof OverrideWriterValidationError ? error.code : "TASK_VALIDATION_FAILED"; + return blocked(state, code); + } + if (request.baseRevision !== state.revision) return blocked(state, "REVISION_CONFLICT"); + if (request.localDataBlockId !== state.localDataBlockId || request.referenceSourceDataBlockId !== state.referenceSourceDataBlockId || request.hierarchyRootDataBlockId !== state.hierarchyRootDataBlockId) { + return blocked(state, "ASSET_SOURCE_HASH_MISMATCH"); + } + return { + status: "APPLIED", + code: null, + state: { ...state, revision: state.revision + 1, value: request.value }, + }; +} diff --git a/web/protocol/library-source-origin.ts b/web/protocol/library-source-origin.ts new file mode 100644 index 00000000..c8136240 --- /dev/null +++ b/web/protocol/library-source-origin.ts @@ -0,0 +1,148 @@ +import { normalizeProjectAssetPath } from "./asset-path"; +import type { ErrorCode } from "./error"; + +export const LIBRARY_SOURCE_ORIGIN_SCHEMA = 1 as const; +export type LibrarySourceKind = "HTTPS_ORIGIN" | "PROJECT_ASSET" | "USER_SELECTED_FILE"; + +export interface LibrarySourcePolicyIR { + schemaVersion: typeof LIBRARY_SOURCE_ORIGIN_SCHEMA; + declaredHttpsOrigins: string[]; +} + +export type LibrarySourceRequestIR = + | { schemaVersion: typeof LIBRARY_SOURCE_ORIGIN_SCHEMA; kind: "HTTPS_ORIGIN"; url: string } + | { schemaVersion: typeof LIBRARY_SOURCE_ORIGIN_SCHEMA; kind: "PROJECT_ASSET"; path: string } + | { schemaVersion: typeof LIBRARY_SOURCE_ORIGIN_SCHEMA; kind: "USER_SELECTED_FILE"; selectionId: string; fileName: string; byteLength: number; sourceSha256: string }; + +export interface AcceptedLibrarySourceIR { + status: "READY"; + kind: LibrarySourceKind; + canonicalLocator: string; +} + +export class LibrarySourceOriginValidationError extends Error { + readonly code: ErrorCode; + readonly path?: string; + + constructor(code: ErrorCode, message: string, path?: string) { + super(`${code}: ${message}`); + this.name = "LibrarySourceOriginValidationError"; + this.code = code; + this.path = path; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const SELECTION_ID = /^file-selection:[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const FILE_NAME = /^[^\\/\u0000-\u001f\u007f]{1,255}$/; +const CONTROL_CHARACTER = /[\u0000-\u001f\u007f]/; +const MAX_FILE_BYTES = 4 * 1024 * 1024 * 1024; + +function record(value: unknown, path: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`, path); + return value as Record; +} + +function exactKeys(value: Record, expected: readonly string[], path: string): void { + const actual = Object.keys(value).sort(); + const allowed = [...expected].sort(); + if (actual.length !== allowed.length || actual.some((key, index) => key !== allowed[index])) throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", `${path} contains undeclared fields`, path); +} + +function text(value: unknown, path: string, maximum: number): string { + if (typeof value !== "string" || value.length === 0 || value.length > maximum) throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", `${path} is invalid`, path); + return value; +} + +function digest(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", `${path} must be a lowercase SHA-256 digest`, path); + return value; +} + +function validateUriText(value: string, path: string): void { + if (value.includes("\\") || CONTROL_CHARACTER.test(value) || /%(?![0-9a-fA-F]{2})/.test(value)) { + throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} contains unsafe URI characters`, path); + } +} + +function validateUriPath(pathname: string, path: string): void { + let decoded: string; + try { decoded = decodeURIComponent(pathname); } + catch { throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} contains malformed percent encoding`, path); } + if (decoded.includes("%") || decoded.includes("\\") || CONTROL_CHARACTER.test(decoded)) { + throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} contains an unsafe path`, path); + } +} + +function canonicalOrigin(value: unknown, path: string): string { + const textValue = text(value, path, 2_048); + validateUriText(textValue, path); + let parsed: URL; + try { parsed = new URL(textValue); } catch { throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} is not an absolute URL`, path); } + if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port) throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} must be a credential-free HTTPS origin`, path); + // A policy entry is an origin, not a URL whose path/query/fragment is discarded by URL.origin. + // Check the raw suffix too: URL parsing normalizes encoded and literal dot segments before exposing + // pathname, which must not turn an origin-smuggling declaration into an apparently trusted origin. + const schemeSeparator = textValue.indexOf("://"); + const authorityAndSuffix = schemeSeparator < 0 ? textValue : textValue.slice(schemeSeparator + 3); + const suffixStart = authorityAndSuffix.search(/[/?#]/); + const rawSuffix = suffixStart < 0 ? "" : authorityAndSuffix.slice(suffixStart); + validateUriPath(parsed.pathname, path); + if (schemeSeparator < 0 || parsed.pathname !== "/" || parsed.search || parsed.hash || rawSuffix !== "" && rawSuffix !== "/") throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} must not include a path, query, or fragment`, path); + return parsed.origin; +} + +function httpsUrl(value: unknown, path: string): string { + const textValue = text(value, path, 8_192); + validateUriText(textValue, path); + let parsed: URL; + try { parsed = new URL(textValue); } catch { throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} is not an absolute URL`, path); } + if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port) throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", `${path} must be a credential-free HTTPS URL`, path); + validateUriPath(parsed.pathname, path); + return parsed.href; +} + +export function parseLibrarySourcePolicy(value: unknown): LibrarySourcePolicyIR { + const policy = record(value, "policy"); + exactKeys(policy, ["schemaVersion", "declaredHttpsOrigins"], "policy"); + if (policy.schemaVersion !== LIBRARY_SOURCE_ORIGIN_SCHEMA || !Array.isArray(policy.declaredHttpsOrigins) || policy.declaredHttpsOrigins.length === 0 || policy.declaredHttpsOrigins.length > 1_024) throw new LibrarySourceOriginValidationError("PROTOCOL_MISMATCH", "Unsupported or empty library source policy", "policy"); + const declaredHttpsOrigins = policy.declaredHttpsOrigins.map((origin, index) => canonicalOrigin(origin, `declaredHttpsOrigins[${index}]`)); + if (new Set(declaredHttpsOrigins).size !== declaredHttpsOrigins.length) throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", "declared HTTPS origins must be unique", "declaredHttpsOrigins"); + return { schemaVersion: LIBRARY_SOURCE_ORIGIN_SCHEMA, declaredHttpsOrigins }; +} + +export function parseLibrarySourceRequest(value: unknown): LibrarySourceRequestIR { + const request = record(value, "request"); + if (request.schemaVersion !== LIBRARY_SOURCE_ORIGIN_SCHEMA) throw new LibrarySourceOriginValidationError("PROTOCOL_MISMATCH", "Unsupported library source request schema", "schemaVersion"); + if (request.kind === "HTTPS_ORIGIN") { + exactKeys(request, ["schemaVersion", "kind", "url"], "request"); + return { schemaVersion: LIBRARY_SOURCE_ORIGIN_SCHEMA, kind: "HTTPS_ORIGIN", url: httpsUrl(request.url, "url") }; + } + if (request.kind === "PROJECT_ASSET") { + exactKeys(request, ["schemaVersion", "kind", "path"], "request"); + let path: string; + try { path = normalizeProjectAssetPath(text(request.path, "path", 2_048)); } + catch (error) { throw new LibrarySourceOriginValidationError(error instanceof Error && error.message === "ASSET_PATH_INVALID" ? "ASSET_MANIFEST_INVALID" : "IO_EXTERNAL_URI_BLOCKED", "project asset path is outside the project", "path"); } + return { schemaVersion: LIBRARY_SOURCE_ORIGIN_SCHEMA, kind: "PROJECT_ASSET", path }; + } + if (request.kind === "USER_SELECTED_FILE") { + exactKeys(request, ["schemaVersion", "kind", "selectionId", "fileName", "byteLength", "sourceSha256"], "request"); + if (typeof request.selectionId !== "string" || !SELECTION_ID.test(request.selectionId)) throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", "selectionId is invalid", "selectionId"); + if (typeof request.fileName !== "string" || !FILE_NAME.test(request.fileName) || request.fileName === "." || request.fileName === "..") throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", "fileName is invalid", "fileName"); + if (typeof request.byteLength !== "number" || !Number.isSafeInteger(request.byteLength) || request.byteLength <= 0 || request.byteLength > MAX_FILE_BYTES) throw new LibrarySourceOriginValidationError("ASSET_BUDGET_EXCEEDED", "selected file length is outside the budget", "byteLength"); + return { schemaVersion: LIBRARY_SOURCE_ORIGIN_SCHEMA, kind: "USER_SELECTED_FILE", selectionId: request.selectionId, fileName: request.fileName, byteLength: request.byteLength, sourceSha256: digest(request.sourceSha256, "sourceSha256") }; + } + throw new LibrarySourceOriginValidationError("ASSET_MANIFEST_INVALID", "source kind is unsupported", "kind"); +} + +export function acceptLibrarySource(policyValue: unknown, requestValue: unknown): AcceptedLibrarySourceIR { + const policy = parseLibrarySourcePolicy(policyValue); + const request = parseLibrarySourceRequest(requestValue); + if (request.kind === "HTTPS_ORIGIN") { + const origin = new URL(request.url).origin; + if (!policy.declaredHttpsOrigins.includes(origin)) throw new LibrarySourceOriginValidationError("IO_EXTERNAL_URI_BLOCKED", "HTTPS origin is not declared by the policy", "url"); + return { status: "READY", kind: request.kind, canonicalLocator: request.url }; + } + if (request.kind === "PROJECT_ASSET") return { status: "READY", kind: request.kind, canonicalLocator: `project-assets/${request.path}` }; + return { status: "READY", kind: request.kind, canonicalLocator: `user-file/${request.selectionId}/${request.fileName}` }; +} diff --git a/web/protocol/obj-import.ts b/web/protocol/obj-import.ts new file mode 100644 index 00000000..626d7f3d --- /dev/null +++ b/web/protocol/obj-import.ts @@ -0,0 +1,219 @@ +export const OBJ_IMPORT_SCHEMA_VERSION = 1 as const; + +export const OBJ_IMPORT_BUDGET = { + maxObjBytes: 512 * 1024, + maxMtlBytes: 128 * 1024, + maxLines: 16_384, + maxPositions: 65_536, + maxTexcoords: 65_536, + maxNormals: 65_536, + maxFaces: 65_536, +} as const; + +export interface OBJFaceVertex { + position: number; + texcoord: number | null; + normal: number | null; +} + +export interface OBJFace { + object: string | null; + groups: string[]; + material: string | null; + vertices: OBJFaceVertex[]; +} + +export interface OBJMaterial { + name: string; + mapKd: string | null; +} + +export interface OBJSemantics { + schemaVersion: typeof OBJ_IMPORT_SCHEMA_VERSION; + materialLibraries: string[]; + objects: string[]; + groups: string[]; + positions: number[][]; + texcoords: number[][]; + normals: number[][]; + faces: OBJFace[]; + materials: OBJMaterial[]; +} + +export type OBJLossCode = "OBJ_TEXTURE_ORIGIN_UNRESOLVED"; + +export interface OBJLossWarning { + code: OBJLossCode; + severity: "warning"; + message: string; + path: string; +} + +export interface OBJLossReport { + schemaVersion: typeof OBJ_IMPORT_SCHEMA_VERSION; + operation: "OBJ_EXPORT_LOSS_REPORT"; + canRoundTrip: boolean; + warningCount: number; + warnings: OBJLossWarning[]; +} + +function parseNumber(value: string, label: string): number { + const parsed = Number(value); + if (!Number.isFinite(parsed)) throw new Error(`OBJ_NUMBER_INVALID: ${label}`); + return parsed === 0 ? 0 : parsed; +} + +function decode(bytes: ArrayBuffer, limit: number, label: string): string { + if (bytes.byteLength > limit) throw new Error(`OBJ_IMPORT_BUDGET_EXCEEDED: ${label}`); + try { + return new TextDecoder("utf-8", { fatal: true }).decode(bytes); + } + catch { + throw new Error(`OBJ_TEXT_INVALID: ${label}`); + } +} + +function resolveIndex(raw: string, count: number, label: string): number { + const value = Number(raw); + if (!Number.isSafeInteger(value) || value === 0) throw new Error(`OBJ_INDEX_INVALID: ${label}`); + const resolved = value < 0 ? count + value + 1 : value; + if (resolved < 1 || resolved > count) throw new Error(`OBJ_INDEX_OUT_OF_RANGE: ${label}`); + return resolved; +} + +function parseMaterialText(mtlText: string): OBJMaterial[] { + const materials: OBJMaterial[] = []; + let current: OBJMaterial | null = null; + for (const rawLine of mtlText.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + const parts = line.split(/\s+/); + if (parts[0] === "newmtl") { + if (parts.length < 2) throw new Error("OBJ_MTL_INVALID: newmtl name is missing"); + current = { name: parts.slice(1).join(" "), mapKd: null }; + materials.push(current); + } + else if (parts[0] === "map_Kd" && current) { + if (parts.length < 2) throw new Error("OBJ_MTL_INVALID: map_Kd path is missing"); + current.mapKd = parts.slice(1).join(" "); + } + } + return materials; +} + +export function importOBJ(obj: ArrayBuffer, mtl?: ArrayBuffer): OBJSemantics { + const objText = decode(obj, OBJ_IMPORT_BUDGET.maxObjBytes, "OBJ"); + const mtlText = mtl ? decode(mtl, OBJ_IMPORT_BUDGET.maxMtlBytes, "MTL") : ""; + const positions: number[][] = []; + const texcoords: number[][] = []; + const normals: number[][] = []; + const faces: OBJFace[] = []; + const materialLibraries: string[] = []; + const objects: string[] = []; + const groups: string[] = []; + let currentObject: string | null = null; + let currentGroups: string[] = []; + let currentMaterial: string | null = null; + const lines = objText.split(/\r?\n/); + if (lines.length > OBJ_IMPORT_BUDGET.maxLines) throw new Error("OBJ_IMPORT_BUDGET_EXCEEDED: line count"); + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + const parts = line.split(/\s+/); + const kind = parts[0]; + if (kind === "v") { + if (parts.length < 4 || positions.length >= OBJ_IMPORT_BUDGET.maxPositions) throw new Error("OBJ_IMPORT_BUDGET_EXCEEDED: positions"); + positions.push([parseNumber(parts[1], "v.x"), parseNumber(parts[2], "v.y"), parseNumber(parts[3], "v.z")]); + } + else if (kind === "vt") { + if (parts.length < 3 || texcoords.length >= OBJ_IMPORT_BUDGET.maxTexcoords) throw new Error("OBJ_IMPORT_BUDGET_EXCEEDED: texcoords"); + texcoords.push([parseNumber(parts[1], "vt.u"), parseNumber(parts[2], "vt.v")]); + } + else if (kind === "vn") { + if (parts.length < 4 || normals.length >= OBJ_IMPORT_BUDGET.maxNormals) throw new Error("OBJ_IMPORT_BUDGET_EXCEEDED: normals"); + normals.push([parseNumber(parts[1], "vn.x"), parseNumber(parts[2], "vn.y"), parseNumber(parts[3], "vn.z")]); + } + else if (kind === "mtllib") materialLibraries.push(parts.slice(1).join(" ")); + else if (kind === "o") { + currentObject = parts.slice(1).join(" ") || null; + if (currentObject && !objects.includes(currentObject)) objects.push(currentObject); + } + else if (kind === "g") { + currentGroups = parts.slice(1); + for (const group of currentGroups) if (group && !groups.includes(group)) groups.push(group); + const meshGroup = currentGroups.find((group) => group.endsWith("_Mesh")); + if (meshGroup) { + currentObject = meshGroup; + if (!objects.includes(meshGroup)) objects.push(meshGroup); + } + } + else if (kind === "usemtl") currentMaterial = parts.slice(1).join(" ") || null; + else if (kind === "f") { + if (parts.length < 4) throw new Error("OBJ_FACE_ARITY_INVALID: face requires at least three vertices"); + if (faces.length >= OBJ_IMPORT_BUDGET.maxFaces) throw new Error("OBJ_IMPORT_BUDGET_EXCEEDED: faces"); + const vertices = parts.slice(1).map((token, index) => { + const indices = token.split("/"); + if (indices.length < 1 || indices.length > 3 || !indices[0] || (indices.length === 2 && !indices[1])) throw new Error(`OBJ_FACE_VERTEX_INVALID: face vertex ${index}`); + return { + position: resolveIndex(indices[0], positions.length, "face.position"), + texcoord: indices.length > 1 && indices[1] ? resolveIndex(indices[1], texcoords.length, "face.texcoord") : null, + normal: indices.length > 2 && indices[2] ? resolveIndex(indices[2], normals.length, "face.normal") : null, + }; + }); + faces.push({ object: currentObject, groups: [...currentGroups], material: currentMaterial, vertices }); + } + } + if (faces.length === 0) throw new Error("OBJ_EMPTY: no faces were found"); + return { + schemaVersion: OBJ_IMPORT_SCHEMA_VERSION, + materialLibraries, + objects, + groups, + positions, + texcoords, + normals, + faces, + materials: parseMaterialText(mtlText), + }; +} + +function formatNumber(value: number): string { + if (!Number.isFinite(value)) throw new Error("OBJ_NUMBER_INVALID: cannot serialize non-finite value"); + return String(Object.is(value, -0) ? 0 : Number(value.toFixed(7))); +} + +export function serializeOBJ(document: OBJSemantics): { obj: string; mtl: string } { + if (document.schemaVersion !== OBJ_IMPORT_SCHEMA_VERSION || document.faces.length === 0) throw new Error("OBJ_SERIALIZE_INVALID: semantic document"); + const lines = ["# Web Blender OBJ export", "# schema 1"]; + if (document.materials.length > 0) lines.push("mtllib " + (document.materialLibraries[0] ?? "materials.mtl")); + for (const object of document.objects) lines.push(`o ${object}`); + for (const position of document.positions) lines.push(`v ${position.map(formatNumber).join(" ")}`); + for (const texcoord of document.texcoords) lines.push(`vt ${texcoord.map(formatNumber).join(" ")}`); + for (const normal of document.normals) lines.push(`vn ${normal.map(formatNumber).join(" ")}`); + let object = ""; + let groups = ""; + let material = ""; + for (const face of document.faces) { + if (face.object && face.object !== object) { lines.push(`o ${face.object}`); object = face.object; } + const nextGroups = face.groups.join(" "); + if (nextGroups !== groups) { if (nextGroups) lines.push(`g ${nextGroups}`); groups = nextGroups; } + const nextMaterial = face.material ?? ""; + if (nextMaterial !== material) { if (nextMaterial) lines.push(`usemtl ${nextMaterial}`); material = nextMaterial; } + lines.push(`f ${face.vertices.map((vertex) => `${vertex.position}/${vertex.texcoord ?? ""}/${vertex.normal ?? ""}`).join(" ")}`); + } + const mtlLines = ["# Web Blender MTL export", "# schema 1"]; + for (const value of document.materials) { + mtlLines.push(`newmtl ${value.name}`); + if (value.mapKd) mtlLines.push(`map_Kd ${value.mapKd}`); + } + return { obj: lines.join("\n") + "\n", mtl: mtlLines.join("\n") + "\n" }; +} + +export function createOBJLossReport(document: OBJSemantics, textureAssets: readonly string[] = []): OBJLossReport { + const assets = new Set(textureAssets); + const warnings = document.materials + .filter((material) => material.mapKd && !assets.has(material.mapKd)) + .map((material) => ({ code: "OBJ_TEXTURE_ORIGIN_UNRESOLVED" as const, severity: "warning" as const, message: `OBJ texture ${material.mapKd} is not bound to a supplied asset`, path: material.mapKd! })) + .sort((left, right) => left.path.localeCompare(right.path)); + return { schemaVersion: OBJ_IMPORT_SCHEMA_VERSION, operation: "OBJ_EXPORT_LOSS_REPORT", canRoundTrip: true, warningCount: warnings.length, warnings }; +} diff --git a/web/protocol/ply-import.ts b/web/protocol/ply-import.ts new file mode 100644 index 00000000..0d83c378 --- /dev/null +++ b/web/protocol/ply-import.ts @@ -0,0 +1,298 @@ +export const PLY_IMPORT_SCHEMA_VERSION = 1 as const; + +export const PLY_IMPORT_BUDGET = { + maxBytes: 512 * 1024, + maxHeaderBytes: 64 * 1024, + maxElements: 16, + maxVertices: 65_536, + maxFaces: 65_536, + maxListLength: 256, + maxCustomProperties: 64, +} as const; + +export type PLYFormat = "ascii" | "binary_little_endian"; + +export interface PLYVertex { + position: [number, number, number]; + normal: [number, number, number] | null; + color: [number, number, number, number] | null; + customProperties: Record; +} + +export interface PLYFace { + indices: number[]; + customProperties: Record; +} + +export type PLYLossCode = + | "PLY_UNKNOWN_ELEMENT" + | "PLY_UNKNOWN_PROPERTY" + | "PLY_NORMAL_PROPERTY_INCOMPLETE" + | "PLY_COLOR_PROPERTY_INCOMPLETE"; + +export interface PLYLossWarning { + code: PLYLossCode; + severity: "warning"; + element: string; + property: string | null; + message: string; +} + +export interface PLYLossReport { + schemaVersion: typeof PLY_IMPORT_SCHEMA_VERSION; + operation: "PLY_IMPORT_LOSS_REPORT"; + canImport: boolean; + warningCount: number; + warnings: PLYLossWarning[]; +} + +export interface PLYImportResult { + schemaVersion: typeof PLY_IMPORT_SCHEMA_VERSION; + format: PLYFormat; + vertices: PLYVertex[]; + faces: PLYFace[]; + warnings: PLYLossWarning[]; +} + +type ScalarType = "int8" | "uint8" | "int16" | "uint16" | "int32" | "uint32" | "float32" | "float64"; +interface ScalarProperty { kind: "scalar"; name: string; type: ScalarType; } +interface ListProperty { kind: "list"; name: string; countType: ScalarType; valueType: ScalarType; } +type Property = ScalarProperty | ListProperty; +interface Element { name: string; count: number; properties: Property[]; } + +const SCALAR_TYPES: Record = { + char: "int8", int8: "int8", uchar: "uint8", uint8: "uint8", short: "int16", int16: "int16", + ushort: "uint16", uint16: "uint16", int: "int32", int32: "int32", uint: "uint32", uint32: "uint32", + float: "float32", float32: "float32", double: "float64", float64: "float64", +}; + +function fail(code: string): never { throw new Error(code); } + +function finite(value: number, label: string): number { + if (!Number.isFinite(value)) fail(`PLY_NUMBER_INVALID: ${label}`); + return Object.is(value, -0) ? 0 : value; +} + +function decodeHeader(bytes: Uint8Array): { format: PLYFormat; elements: Element[]; offset: number } { + const limit = Math.min(bytes.byteLength, PLY_IMPORT_BUDGET.maxHeaderBytes); + let end = -1; + let terminatorLength = 0; + for (let index = 0; index + 10 <= limit; index++) { + if (bytes[index] === 101 && bytes[index + 1] === 110 && bytes[index + 2] === 100 && bytes[index + 3] === 95 && bytes[index + 4] === 104 && bytes[index + 5] === 101 && bytes[index + 6] === 97 && bytes[index + 7] === 100 && bytes[index + 8] === 101 && bytes[index + 9] === 114) { + if (bytes[index + 10] === 10) { end = index; terminatorLength = 11; break; } + if (bytes[index + 10] === 13 && bytes[index + 11] === 10) { end = index; terminatorLength = 12; break; } + } + } + if (end < 0) fail("PLY_HEADER_INVALID"); + let header: string; + try { header = new TextDecoder("ascii", { fatal: true }).decode(bytes.subarray(0, end)); } + catch { fail("PLY_HEADER_INVALID"); } + const lines = header.split(/\r?\n/); + if (lines[0] !== "ply") fail("PLY_MAGIC_INVALID"); + let format: PLYFormat | null = null; + const elements: Element[] = []; + let current: Element | null = null; + for (const rawLine of lines.slice(1)) { + const line = rawLine.trim(); + if (!line || line.startsWith("comment") || line.startsWith("obj_info")) continue; + const parts = line.split(/\s+/); + if (parts[0] === "format") { + if (parts[1] === "ascii") format = "ascii"; + else if (parts[1] === "binary_little_endian") format = "binary_little_endian"; + else fail("PLY_FORMAT_UNSUPPORTED"); + } + else if (parts[0] === "element") { + if (parts.length !== 3 || !Number.isSafeInteger(Number(parts[2])) || Number(parts[2]) < 0) fail("PLY_ELEMENT_INVALID"); + if (elements.length >= PLY_IMPORT_BUDGET.maxElements) fail("PLY_IMPORT_BUDGET_EXCEEDED: elements"); + const count = Number(parts[2]); + if (count > PLY_IMPORT_BUDGET.maxVertices) fail(`PLY_IMPORT_BUDGET_EXCEEDED: ${parts[1]}`); + current = { name: parts[1], count, properties: [] }; + elements.push(current); + } + else if (parts[0] === "property") { + if (!current) fail("PLY_PROPERTY_WITHOUT_ELEMENT"); + if (parts[1] === "list") { + if (parts.length !== 5) fail("PLY_PROPERTY_INVALID"); + const countType = SCALAR_TYPES[parts[2]]; + const valueType = SCALAR_TYPES[parts[3]]; + if (!countType || !valueType) fail("PLY_PROPERTY_TYPE_UNSUPPORTED"); + current.properties.push({ kind: "list", name: parts[4], countType, valueType }); + } + else { + if (parts.length !== 3) fail("PLY_PROPERTY_INVALID"); + const type = SCALAR_TYPES[parts[1]]; + if (!type) fail("PLY_PROPERTY_TYPE_UNSUPPORTED"); + current.properties.push({ kind: "scalar", name: parts[2], type }); + } + } + else if (parts[0] !== "end_header") fail("PLY_HEADER_INVALID"); + } + if (!format) fail("PLY_FORMAT_MISSING"); + return { format, elements, offset: end + terminatorLength }; +} + +function readScalar(view: DataView, offset: number, type: ScalarType): { value: number; next: number } { + const size = type === "int8" || type === "uint8" ? 1 : type === "int16" || type === "uint16" ? 2 : 4; + if (offset + size > view.byteLength) fail("PLY_DATA_TRUNCATED"); + let value: number; + if (type === "int8") value = view.getInt8(offset); + else if (type === "uint8") value = view.getUint8(offset); + else if (type === "int16") value = view.getInt16(offset, true); + else if (type === "uint16") value = view.getUint16(offset, true); + else if (type === "int32") value = view.getInt32(offset, true); + else if (type === "uint32") value = view.getUint32(offset, true); + else if (type === "float32") value = view.getFloat32(offset, true); + else value = view.getFloat64(offset, true); + return { value: finite(value, "binary"), next: offset + size }; +} + +function parseAsciiRecords(bytes: Uint8Array, offset: number, elements: Element[]): Map>> { + let text: string; + try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(offset)); } + catch { fail("PLY_ASCII_INVALID"); } + const lines = text.split(/\r?\n/); + let cursor = 0; + const records = new Map>>(); + for (const element of elements) { + const values: Array> = []; + for (let row = 0; row < element.count; row++) { + while (cursor < lines.length && !lines[cursor].trim()) cursor++; + if (cursor >= lines.length) fail("PLY_DATA_TRUNCATED"); + const tokens = lines[cursor++].trim().split(/\s+/); + let tokenIndex = 0; + const record: Record = {}; + for (const property of element.properties) { + if (property.kind === "scalar") { + if (tokenIndex >= tokens.length) fail("PLY_DATA_TRUNCATED"); + record[property.name] = finite(Number(tokens[tokenIndex++]), `${element.name}.${property.name}`); + } + else { + if (tokenIndex >= tokens.length) fail("PLY_DATA_TRUNCATED"); + const length = Number(tokens[tokenIndex++]); + if (!Number.isSafeInteger(length) || length < 0 || length > PLY_IMPORT_BUDGET.maxListLength) fail("PLY_LIST_INVALID"); + const list: number[] = []; + for (let index = 0; index < length; index++) { + if (tokenIndex >= tokens.length) fail("PLY_DATA_TRUNCATED"); + list.push(finite(Number(tokens[tokenIndex++]), `${element.name}.${property.name}`)); + } + record[property.name] = list; + } + } + if (tokenIndex !== tokens.length) fail("PLY_DATA_EXTRA_TOKENS"); + values.push(record); + } + records.set(element.name, values); + } + return records; +} + +function parseBinaryRecords(bytes: Uint8Array, offset: number, elements: Element[]): Map>> { + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + let cursor = offset; + const records = new Map>>(); + for (const element of elements) { + const values: Array> = []; + for (let row = 0; row < element.count; row++) { + const record: Record = {}; + for (const property of element.properties) { + if (property.kind === "scalar") { + const result = readScalar(view, cursor, property.type); record[property.name] = result.value; cursor = result.next; + } + else { + const count = readScalar(view, cursor, property.countType); cursor = count.next; + if (!Number.isSafeInteger(count.value) || count.value < 0 || count.value > PLY_IMPORT_BUDGET.maxListLength) fail("PLY_LIST_INVALID"); + const list: number[] = []; + for (let index = 0; index < count.value; index++) { const result = readScalar(view, cursor, property.valueType); list.push(result.value); cursor = result.next; } + record[property.name] = list; + } + } + values.push(record); + } + records.set(element.name, values); + } + return records; +} + +function warning(code: PLYLossCode, element: string, property: string | null, message: string): PLYLossWarning { + return { code, severity: "warning", element, property, message }; +} + +function mapDocument(elements: Element[], records: Map>>): PLYImportResult { + const warnings: PLYLossWarning[] = []; + const vertexElement = elements.find((element) => element.name === "vertex"); + if (!vertexElement) fail("PLY_VERTEX_ELEMENT_MISSING"); + const vertexRecords = records.get("vertex") ?? []; + const vertexProperties = new Set(vertexElement.properties.filter((property): property is ScalarProperty => property.kind === "scalar").map((property) => property.name)); + for (const name of ["x", "y", "z"]) if (!vertexProperties.has(name)) fail("PLY_VERTEX_POSITION_MISSING"); + const hasNormals = ["nx", "ny", "nz"].every((name) => vertexProperties.has(name)); + if (!hasNormals && ["nx", "ny", "nz"].some((name) => vertexProperties.has(name))) warnings.push(warning("PLY_NORMAL_PROPERTY_INCOMPLETE", "vertex", null, "vertex normal requires nx, ny and nz")); + const hasColor = ["red", "green", "blue"].every((name) => vertexProperties.has(name)); + if (!hasColor && ["red", "green", "blue", "alpha"].some((name) => vertexProperties.has(name))) warnings.push(warning("PLY_COLOR_PROPERTY_INCOMPLETE", "vertex", null, "vertex color requires red, green and blue")); + const customNames = vertexElement.properties.filter((property): property is ScalarProperty => property.kind === "scalar" && !["x", "y", "z", "nx", "ny", "nz", "red", "green", "blue", "alpha"].includes(property.name)).map((property) => property.name); + if (customNames.length > PLY_IMPORT_BUDGET.maxCustomProperties) fail("PLY_IMPORT_BUDGET_EXCEEDED: custom properties"); + for (const property of vertexElement.properties) if (property.kind === "list") warnings.push(warning("PLY_UNKNOWN_PROPERTY", "vertex", property.name, `vertex list property ${property.name} is not mapped`)); + const vertices = vertexRecords.map((record) => ({ + position: [record.x, record.y, record.z].map((value) => finite(value as number, "vertex position")) as [number, number, number], + normal: hasNormals ? [record.nx, record.ny, record.nz].map((value) => finite(value as number, "vertex normal")) as [number, number, number] : null, + color: hasColor ? ["red", "green", "blue", "alpha"].map((name) => Math.max(0, Math.min(255, Number(record[name] ?? (name === "alpha" ? 255 : 0)))) / 255) as [number, number, number, number] : null, + customProperties: Object.fromEntries(customNames.map((name) => [name, finite(record[name] as number, `vertex.${name}`)])), + })); + const faceElement = elements.find((element) => element.name === "face"); + const faces: PLYFace[] = []; + if (faceElement) { + const indexProperty = faceElement.properties.find((property): property is ListProperty => property.kind === "list" && (property.name === "vertex_indices" || property.name === "vertex_index")); + if (!indexProperty) fail("PLY_FACE_INDEX_MISSING"); + const faceCustomNames = faceElement.properties.filter((property): property is ScalarProperty => property.kind === "scalar").map((property) => property.name); + for (const property of faceElement.properties) if (property.kind === "list" && property !== indexProperty) warnings.push(warning("PLY_UNKNOWN_PROPERTY", "face", property.name, `face list property ${property.name} is not mapped`)); + for (const record of records.get("face") ?? []) { + const values = record[indexProperty.name]; + if (!Array.isArray(values) || values.length < 3) fail("PLY_FACE_ARITY_INVALID"); + const indices = values.map((value) => { if (!Number.isSafeInteger(value) || value < 0 || value >= vertices.length) fail("PLY_FACE_INDEX_OUT_OF_RANGE"); return value; }); + faces.push({ indices, customProperties: Object.fromEntries(faceCustomNames.map((name) => [name, finite(record[name] as number, `face.${name}`)])) }); + } + } + for (const element of elements) if (element.name !== "vertex" && element.name !== "face") warnings.push(warning("PLY_UNKNOWN_ELEMENT", element.name, null, `element ${element.name} is not mapped`)); + return { schemaVersion: PLY_IMPORT_SCHEMA_VERSION, format: "ascii", vertices, faces, warnings }; +} + +export function importPLY(bytes: ArrayBuffer, options?: { format?: PLYFormat }): PLYImportResult { + if (bytes.byteLength > PLY_IMPORT_BUDGET.maxBytes) fail("PLY_IMPORT_BUDGET_EXCEEDED: bytes"); + const payload = new Uint8Array(bytes); + const header = decodeHeader(payload); + if (options?.format && options.format !== header.format) fail("PLY_FORMAT_MISMATCH"); + const records = header.format === "ascii" ? parseAsciiRecords(payload, header.offset, header.elements) : parseBinaryRecords(payload, header.offset, header.elements); + const result = mapDocument(header.elements, records); + result.format = header.format; + return result; +} + +export function createPLYLossReport(document: PLYImportResult): PLYLossReport { + const warnings = [...document.warnings].sort((left, right) => left.code.localeCompare(right.code) || left.element.localeCompare(right.element) || (left.property ?? "").localeCompare(right.property ?? "")); + return { schemaVersion: PLY_IMPORT_SCHEMA_VERSION, operation: "PLY_IMPORT_LOSS_REPORT", canImport: true, warningCount: warnings.length, warnings }; +} + +function formatNumber(value: number): string { return Number.isInteger(value) ? String(value) : String(Number(value.toPrecision(9))); } + +export function serializePLYAscii(document: PLYImportResult): ArrayBuffer { + if (document.schemaVersion !== PLY_IMPORT_SCHEMA_VERSION || document.vertices.length > PLY_IMPORT_BUDGET.maxVertices || document.faces.length > PLY_IMPORT_BUDGET.maxFaces) fail("PLY_EXPORT_DOCUMENT_INVALID"); + const customNames = [...new Set(document.vertices.flatMap((vertex) => Object.keys(vertex.customProperties)))].sort(); + const faceCustomNames = [...new Set(document.faces.flatMap((face) => Object.keys(face.customProperties)))].sort(); + const lines = ["ply", "format ascii 1.0", "comment Web Blender PLY schema 1", `element vertex ${document.vertices.length}`, "property float x", "property float y", "property float z"]; + if (document.vertices.some((vertex) => vertex.normal)) lines.push("property float nx", "property float ny", "property float nz"); + if (document.vertices.some((vertex) => vertex.color)) lines.push("property uchar red", "property uchar green", "property uchar blue", "property uchar alpha"); + for (const name of customNames) lines.push(`property float ${name}`); + lines.push(`element face ${document.faces.length}`, "property list uchar uint vertex_indices"); + for (const name of faceCustomNames) lines.push(`property float ${name}`); + lines.push("end_header"); + for (const vertex of document.vertices) { + const values = vertex.position.map(formatNumber); + if (document.vertices.some((item) => item.normal)) values.push(...(vertex.normal ?? [0, 0, 0]).map(formatNumber)); + if (document.vertices.some((item) => item.color)) values.push(...(vertex.color ?? [0, 0, 0, 1]).map((value) => String(Math.max(0, Math.min(255, Math.round(value * 255)))))); + values.push(...customNames.map((name) => formatNumber(vertex.customProperties[name] ?? 0))); + lines.push(values.join(" ")); + } + for (const face of document.faces) lines.push(`${face.indices.length} ${face.indices.join(" ")} ${faceCustomNames.map((name) => formatNumber(face.customProperties[name] ?? 0)).join(" ")}`.trim()); + const output = new TextEncoder().encode(lines.join("\n") + "\n"); + if (output.byteLength > PLY_IMPORT_BUDGET.maxBytes) fail("PLY_IMPORT_BUDGET_EXCEEDED: output bytes"); + return output.buffer; +} diff --git a/web/protocol/pointer-contract.ts b/web/protocol/pointer-contract.ts new file mode 100644 index 00000000..1c30cb85 --- /dev/null +++ b/web/protocol/pointer-contract.ts @@ -0,0 +1,35 @@ +export const POINTER_CONTRACT_SCHEMA_VERSION = 1 as const; +export type PointerKind = "mouse" | "touch" | "pen"; + +export interface PointerObservation { + schemaVersion: typeof POINTER_CONTRACT_SCHEMA_VERSION; + pointerType: PointerKind; + pointerId: number; + pressure: number; + tiltX: number; + tiltY: number; + button: number; + buttons: number; + cancelled: boolean; +} + +function bounded(value: number, min: number, max: number, fallback: number): number { + return Number.isFinite(value) ? Math.max(min, Math.min(max, value)) : fallback; +} + +export function observePointerEvent(event: { pointerType?: string; pointerId?: number; pressure?: number; tiltX?: number; tiltY?: number; button?: number; buttons?: number; type?: string }): PointerObservation { + const pointerType = event.pointerType === "touch" || event.pointerType === "pen" || event.pointerType === "mouse" ? event.pointerType : null; + if (!pointerType) throw new Error("POINTER_TYPE_UNSUPPORTED"); + if (!Number.isSafeInteger(event.pointerId) || event.pointerId! < 0) throw new Error("POINTER_ID_INVALID"); + return { + schemaVersion: POINTER_CONTRACT_SCHEMA_VERSION, + pointerType, + pointerId: event.pointerId!, + pressure: bounded(event.pressure ?? (pointerType === "mouse" ? 0 : 0.5), 0, 1, 0), + tiltX: bounded(event.tiltX ?? 0, -90, 90, 0), + tiltY: bounded(event.tiltY ?? 0, -90, 90, 0), + button: Number.isInteger(event.button) ? event.button! : -1, + buttons: Number.isInteger(event.buttons) && event.buttons! >= 0 ? event.buttons! : 0, + cancelled: event.type === "pointercancel", + }; +} diff --git a/web/protocol/scripting-platform.ts b/web/protocol/scripting-platform.ts index def04005..696f4687 100644 --- a/web/protocol/scripting-platform.ts +++ b/web/protocol/scripting-platform.ts @@ -6,7 +6,14 @@ export const SCRIPTING_PLATFORM_SCHEMA = 1 as const; export const SCRIPT_SOURCE_SCHEMA = 1 as const; export const SCRIPT_EXECUTION_AUDIT_SCHEMA = 1 as const; export const SCRIPT_EXECUTION_AUDIT_LOG_SCHEMA = 1 as const; +export const SCRIPT_TRUST_POLICY_SCHEMA = 1 as const; +export const SCRIPT_SANDBOX_SCOPE_SCHEMA = 1 as const; export const SCRIPTING_BUDGET = { maxScripts: 1_024, maxPermissions: 64, maxDependencies: 128, maxCpuMs: 60_000, maxMemoryBytes: 512 * 1024 * 1024, maxWallMs: 300_000, maxSourceBytes: 1024 * 1024, maxSourceLines: 65_536, maxAuditEntries: 65_536 } as const; +export const SCRIPT_TRUST_POLICY_BUDGET = { maxKeys: 1_024, maxClockSkewMs: 300_000 } as const; +export const SCRIPT_SANDBOX_BUDGET = { maxCpuMs: 60_000, maxWallMs: 300_000, maxMemoryBytes: 512 * 1024 * 1024, maxMessageBytes: 1 * 1024 * 1024, maxOutputBytes: 16 * 1024 * 1024 } as const; +export const SCRIPT_HOST_CALL_SCHEMA = 1 as const; +export const SCRIPT_HOST_CALLS = ["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"] as const; +export const SCRIPT_SANDBOX_JOB_SCHEMA = 1 as const; export const SCRIPT_PERMISSIONS = ["READ_MAIN", "WRITE_MAIN", "READ_ASSET", "WRITE_ASSET", "SUBMIT_SERVER_JOB"] as const; export type ScriptPermission = typeof SCRIPT_PERMISSIONS[number]; @@ -15,12 +22,14 @@ export interface ScriptManifestIR { id: string; name: string; entryPath: string; + sourceByteLength: number; sourceSha256: string; publisher: string; signature: string; keyId: string; permissions: ScriptPermission[]; dependencies: ScriptDependencyIR[]; + module: false; cpuMs: number; memoryBytes: number; wallMs: number; @@ -30,6 +39,94 @@ export interface ScriptManifestIR { addonInstall: false; } export interface ScriptingManifestIR { schemaVersion: typeof SCRIPTING_PLATFORM_SCHEMA; scripts: ScriptManifestIR[] } +export interface ScriptTrustKeyIR { + keyId: string; + publisher: string; + algorithm: "ED25519"; + publicKey: string; + status: "ACTIVE" | "REVOKED"; + notBefore: string; + notAfter: string; + revokedAt?: string; + replaces?: string; +} +export interface ScriptTrustPolicyIR { + schemaVersion: typeof SCRIPT_TRUST_POLICY_SCHEMA; + issuer: string; + issuedAt: string; + expiresAt: string; + maxClockSkewMs: number; + keys: ScriptTrustKeyIR[]; +} +export interface ScriptSignerResolutionIR { + status: "ELIGIBLE" | "BLOCKED"; + keyId: string; + publisher: string; + trust: "ACTIVE" | "REVOKED" | "NOT_FOUND" | "PUBLISHER_MISMATCH" | "POLICY_NOT_YET_VALID" | "POLICY_EXPIRED" | "KEY_NOT_YET_VALID" | "KEY_EXPIRED"; + cryptographicVerification: "REQUIRED"; +} +export interface ScriptSignatureVerificationIR { + status: "VERIFIED" | "BLOCKED"; + code: "SCRIPT_SIGNATURE_VERIFIED" | "SCRIPT_SIGNATURE_INVALID" | "SCRIPT_POLICY_DENIED"; + keyId: string; + sourceSha256: string; + inputSha256: string; +} +export interface ScriptPermissionResolutionIR { + status: "ALLOWED" | "BLOCKED"; + code: "SCRIPT_PERMISSIONS_ALLOWED" | "SCRIPT_POLICY_DENIED"; + scriptId: string; + declared: ScriptPermission[]; + requested: ScriptPermission[]; + granted: ScriptPermission[]; +} +export interface ScriptSandboxScopeIR { + schemaVersion: typeof SCRIPT_SANDBOX_SCOPE_SCHEMA; + dom: false; + hostWorker: false; + opfs: false; + indexedDB: false; + network: false; +} +export interface ScriptSandboxBudgetIR { + schemaVersion: typeof SCRIPT_SANDBOX_SCOPE_SCHEMA; + cpuMs: number; + wallMs: number; + memoryBytes: number; + maxMessageBytes: number; + maxOutputBytes: number; +} +export type ScriptHostCallName = typeof SCRIPT_HOST_CALLS[number]; +export type ScriptHostCallParameters = + | { revision: number } + | { path: string; expectedSha256: string } + | { revision: number; operation: string; payload: Record } + | { path: string; byteLength: number; sha256: string } + | { inputBlendSha256: string; settingsSha256: string }; +export interface ScriptHostCallIR { + schemaVersion: typeof SCRIPT_HOST_CALL_SCHEMA; + requestId: string; + scriptId: string; + call: ScriptHostCallName; + permission: ScriptPermission; + parameters: ScriptHostCallParameters; + execution: "DISABLED"; +} +export interface ScriptSandboxJobIR { + schemaVersion: typeof SCRIPT_SANDBOX_JOB_SCHEMA; + jobId: string; + workerGeneration: number; + baseRevision: number; + mainRevisionBefore: number; + mainRevisionAfter: number; + status: "CRASHED" | "TIMED_OUT" | "CANCELLED"; + errorCode: "SCRIPT_SANDBOX_CRASHED" | "SCRIPT_SANDBOX_TIMEOUT" | "SCRIPT_SANDBOX_CANCELLED"; + temporaryBytes: 0; + publishedResults: 0; + lateResults: 0; + committed: false; + execution: "DISABLED"; +} export interface ScriptSourceIR { id: string; name: string; @@ -111,11 +208,128 @@ function canonicalManifest(manifest: ScriptingManifestIR): ScriptingManifestIR { return { schemaVersion: manifest.schemaVersion, scripts: manifest.scripts - .map((script) => ({ ...script, permissions: [...script.permissions].sort(), dependencies: script.dependencies.map((dependency) => ({ ...dependency })).sort((a, b) => a.id.localeCompare(b.id)) })) - .sort((a, b) => a.id.localeCompare(b.id)), + .map((script) => ({ ...script, permissions: [...script.permissions].sort(), dependencies: script.dependencies.map((dependency) => ({ ...dependency })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0) })) + .sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0), }; } +export function canonicalizeScriptingManifest(value: unknown): ScriptingManifestIR { + return canonicalManifest(parseScriptingManifest(value)); +} + +export function serializeScriptingManifest(value: unknown): string { + return stableJSON(canonicalizeScriptingManifest(value)); +} + +export function serializeScriptSignatureInput(value: unknown, scriptId: string): string { + const parsed = canonicalizeScriptingManifest(value); + const script = parsed.scripts.find((item) => item.id === scriptId); + if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); + return stableJSON({ schemaVersion: SCRIPTING_PLATFORM_SCHEMA, script: { ...script, signature: "" } }); +} + +export function parseScriptTrustPolicy(value: unknown): ScriptTrustPolicyIR { + if (!record(value) || value.schemaVersion !== SCRIPT_TRUST_POLICY_SCHEMA || !Array.isArray(value.keys)) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script trust policy schema"); + const issuer = text(value.issuer, "trustPolicy.issuer", 256); + const issuedAt = isoDate(value.issuedAt, "trustPolicy.issuedAt"); + const expiresAt = isoDate(value.expiresAt, "trustPolicy.expiresAt"); + if (expiresAt <= issuedAt) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "trustPolicy.expiresAt must be after issuedAt"); + const maxClockSkewMs = integer(value.maxClockSkewMs, "trustPolicy.maxClockSkewMs", 0, SCRIPT_TRUST_POLICY_BUDGET.maxClockSkewMs); + if (value.keys.length > SCRIPT_TRUST_POLICY_BUDGET.maxKeys) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Trust policy key count exceeds the budget"); + const keyIds = new Set(); + const keys = value.keys.map((item, index): ScriptTrustKeyIR => { + const name = `trustPolicy.keys[${index}]`; + if (!record(item)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); + const keyId = text(item.keyId, `${name}.keyId`, 128); + if (keyIds.has(keyId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name}.keyId is duplicated`); + keyIds.add(keyId); + if (item.algorithm !== "ED25519" || typeof item.publicKey !== "string" || !/^[a-f0-9]{64}$/.test(item.publicKey)) throw new ScriptingPlatformValidationError("SCRIPT_SIGNATURE_INVALID", `${name} has an unsupported public key`); + const publisher = text(item.publisher, `${name}.publisher`, 256); + const notBefore = isoDate(item.notBefore, `${name}.notBefore`); + const notAfter = isoDate(item.notAfter, `${name}.notAfter`); + if (notAfter <= notBefore) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} validity window is invalid`); + if (item.status !== "ACTIVE" && item.status !== "REVOKED") throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name}.status is invalid`); + const revokedAt = item.revokedAt === undefined ? undefined : isoDate(item.revokedAt, `${name}.revokedAt`); + if (item.status === "REVOKED" ? revokedAt === undefined : revokedAt !== undefined) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name}.revokedAt does not match status`); + if (revokedAt !== undefined && (revokedAt < notBefore || revokedAt > notAfter)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name}.revokedAt is outside the key validity window`); + const replaces = item.replaces === undefined ? undefined : text(item.replaces, `${name}.replaces`, 128); + return { keyId, publisher, algorithm: "ED25519", publicKey: item.publicKey, status: item.status, notBefore, notAfter, ...(revokedAt === undefined ? {} : { revokedAt }), ...(replaces === undefined ? {} : { replaces }) }; + }); + const byId = new Map(keys.map((key) => [key.keyId, key])); + const active = new Set(); const complete = new Set(); + const visit = (keyId: string): void => { + if (active.has(keyId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Trust key rotation cycle includes ${keyId}`); + if (complete.has(keyId)) return; + const key = byId.get(keyId); if (!key) return; + active.add(keyId); + if (key.replaces !== undefined) { + const predecessor = byId.get(key.replaces); + if (!predecessor) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${keyId} replaces missing key ${key.replaces}`); + if (predecessor.publisher !== key.publisher) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${keyId} crosses publisher rotation boundary`); + visit(predecessor.keyId); + } + active.delete(keyId); complete.add(keyId); + }; + keys.forEach((key) => visit(key.keyId)); + return { schemaVersion: SCRIPT_TRUST_POLICY_SCHEMA, issuer, issuedAt, expiresAt, maxClockSkewMs, keys }; +} + +export function canonicalizeScriptTrustPolicy(value: unknown): ScriptTrustPolicyIR { + const parsed = parseScriptTrustPolicy(value); + return { ...parsed, keys: [...parsed.keys].sort((a, b) => a.keyId < b.keyId ? -1 : a.keyId > b.keyId ? 1 : 0) }; +} + +export function serializeScriptTrustPolicy(value: unknown): string { + return stableJSON(canonicalizeScriptTrustPolicy(value)); +} + +export function resolveScriptSigner(manifest: unknown, scriptId: string, policy: unknown, at: string): ScriptSignerResolutionIR { + const parsedManifest = parseScriptingManifest(manifest); + const script = parsedManifest.scripts.find((item) => item.id === scriptId); + if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); + const parsedPolicy = parseScriptTrustPolicy(policy); + const key = parsedPolicy.keys.find((item) => item.keyId === script.keyId); + const blocked = (trust: ScriptSignerResolutionIR["trust"]): ScriptSignerResolutionIR => ({ status: "BLOCKED", keyId: script.keyId, publisher: script.publisher, trust, cryptographicVerification: "REQUIRED" }); + if (!key) return blocked("NOT_FOUND"); + const requestedAt = isoDate(at, "signer.at"); + const policyStart = new Date(parsedPolicy.issuedAt).getTime() - parsedPolicy.maxClockSkewMs; + const policyEnd = new Date(parsedPolicy.expiresAt).getTime() + parsedPolicy.maxClockSkewMs; + const requestedTime = new Date(requestedAt).getTime(); + if (requestedTime < policyStart) return blocked("POLICY_NOT_YET_VALID"); + if (requestedTime > policyEnd) return blocked("POLICY_EXPIRED"); + if (key.publisher !== script.publisher) return blocked("PUBLISHER_MISMATCH"); + if (key.status === "REVOKED") return blocked("REVOKED"); + if (requestedAt < key.notBefore) return blocked("KEY_NOT_YET_VALID"); + if (requestedAt > key.notAfter) return blocked("KEY_EXPIRED"); + return { status: "ELIGIBLE", keyId: key.keyId, publisher: key.publisher, trust: "ACTIVE", cryptographicVerification: "REQUIRED" }; +} + +function hexBytes(value: string): Uint8Array { + const bytes = new Uint8Array(value.length / 2); + for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16); + return bytes; +} + +export async function verifyScriptManifestSignature(manifest: unknown, scriptId: string, policy: unknown, at: string): Promise { + const parsedManifest = parseScriptingManifest(manifest); + const script = parsedManifest.scripts.find((item) => item.id === scriptId); + if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); + const resolution = resolveScriptSigner(parsedManifest, scriptId, policy, at); + const input = serializeScriptSignatureInput(parsedManifest, scriptId); + const inputSha256 = await sha256(input); + if (resolution.status !== "ELIGIBLE") return { status: "BLOCKED", code: "SCRIPT_POLICY_DENIED", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; + const signer = parseScriptTrustPolicy(policy).keys.find((key) => key.keyId === script.keyId); + if (!signer) return { status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; + try { + const key = await crypto.subtle.importKey("raw", hexBytes(signer.publicKey) as unknown as BufferSource, { name: "Ed25519" }, false, ["verify"]); + const valid = await crypto.subtle.verify("Ed25519", key, hexBytes(script.signature) as unknown as BufferSource, new TextEncoder().encode(input) as unknown as BufferSource); + return { status: valid ? "VERIFIED" : "BLOCKED", code: valid ? "SCRIPT_SIGNATURE_VERIFIED" : "SCRIPT_SIGNATURE_INVALID", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; + } + catch { + return { status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID", keyId: script.keyId, sourceSha256: script.sourceSha256, inputSha256 }; + } +} + export async function createScriptExecutionAudit( manifest: unknown, scriptId: string, @@ -129,7 +343,7 @@ export async function createScriptExecutionAudit( const requestedAt = isoDate(options.requestedAt ?? new Date().toISOString(), "requestedAt"); const approvedKey = approvedKeyIds.has(script.keyId); const reason = approvedKey ? "SCRIPT_SANDBOX_UNAVAILABLE" : "SCRIPT_SIGNATURE_INVALID"; - const manifestSha256 = await sha256(stableJSON(canonicalManifest(parsed))); + const manifestSha256 = await sha256(serializeScriptingManifest(parsed)); const request = canonicalAuditRequest({ requestId, requestedAt, scriptId, sourceSha256: script.sourceSha256, manifestSha256, permissions: [...script.permissions], budget: { cpuMs: script.cpuMs, memoryBytes: script.memoryBytes, wallMs: script.wallMs }, approvedKey, decision: "DENY", reason }); const requestSha256 = await sha256(stableJSON(request)); return Object.freeze({ @@ -227,16 +441,20 @@ export async function verifyScriptSource(source: ScriptSourceIR): Promise SCRIPTING_BUDGET.maxScripts) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Script count exceeds the budget"); - const ids = new Set(); + const ids = new Set(); let totalSourceBytes = 0; const scripts = value.scripts.map((item, index): ScriptManifestIR => { const name = `scripts[${index}]`; if (!record(item) || !Array.isArray(item.permissions) || !Array.isArray(item.dependencies)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${name} is invalid`); const id = text(item.id, `${name}.id`); if (ids.has(id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Duplicate script ${id}`); ids.add(id); if (item.permissions.length > SCRIPTING_BUDGET.maxPermissions || item.permissions.some((permission) => !SCRIPT_PERMISSIONS.includes(permission as ScriptPermission)) || new Set(item.permissions).size !== item.permissions.length) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", `${name}.permissions are invalid or exceed the allowlist`); if (item.dependencies.length > SCRIPTING_BUDGET.maxDependencies) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", `${name}.dependencies exceed the budget`); - const dependencies = item.dependencies.map((dependency, dependencyIndex): ScriptDependencyIR => { const dependencyName = `${name}.dependencies[${dependencyIndex}]`; if (!record(dependency)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${dependencyName} is invalid`); return { id: text(dependency.id, `${dependencyName}.id`), sourceSha256: digest(dependency.sourceSha256, `${dependencyName}.sourceSha256`), sourcePath: path(dependency.sourcePath, `${dependencyName}.sourcePath`) }; }); - if (item.network !== false || item.autorun !== false || item.driverExpressions !== false || item.addonInstall !== false) throw new ScriptingPlatformValidationError(item.driverExpressions === true ? "DRIVER_EXECUTION_BLOCKED" : item.addonInstall === true ? "ADDON_INSTALL_BLOCKED" : "SCRIPT_POLICY_DENIED", `${name} requests a denied execution policy`); + const dependencyIds = new Set(); + const dependencies = item.dependencies.map((dependency, dependencyIndex): ScriptDependencyIR => { const dependencyName = `${name}.dependencies[${dependencyIndex}]`; if (!record(dependency)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${dependencyName} is invalid`); const dependencyId = text(dependency.id, `${dependencyName}.id`); if (dependencyIds.has(dependencyId)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${dependencyName}.id is duplicated`); dependencyIds.add(dependencyId); return { id: dependencyId, sourceSha256: digest(dependency.sourceSha256, `${dependencyName}.sourceSha256`), sourcePath: path(dependency.sourcePath, `${dependencyName}.sourcePath`) }; }); + if (item.module !== false || item.network !== false || item.autorun !== false || item.driverExpressions !== false || item.addonInstall !== false) throw new ScriptingPlatformValidationError(item.driverExpressions === true ? "DRIVER_EXECUTION_BLOCKED" : item.addonInstall === true ? "ADDON_INSTALL_BLOCKED" : "SCRIPT_POLICY_DENIED", `${name} requests a denied execution policy`); if (typeof item.signature !== "string" || !HEX_SIGNATURE.test(item.signature)) throw new ScriptingPlatformValidationError("SCRIPT_SIGNATURE_INVALID", `${name}.signature is invalid`); - return { id, name: text(item.name, `${name}.name`), entryPath: path(item.entryPath, `${name}.entryPath`), sourceSha256: digest(item.sourceSha256, `${name}.sourceSha256`), publisher: text(item.publisher, `${name}.publisher`), signature: item.signature, keyId: text(item.keyId, `${name}.keyId`, 128), permissions: [...item.permissions] as ScriptPermission[], dependencies, cpuMs: integer(item.cpuMs, `${name}.cpuMs`, 1, SCRIPTING_BUDGET.maxCpuMs), memoryBytes: integer(item.memoryBytes, `${name}.memoryBytes`, 1, SCRIPTING_BUDGET.maxMemoryBytes), wallMs: integer(item.wallMs, `${name}.wallMs`, 1, SCRIPTING_BUDGET.maxWallMs), network: false, autorun: false, driverExpressions: false, addonInstall: false }; + const sourceByteLength = integer(item.sourceByteLength, `${name}.sourceByteLength`, 0, SCRIPTING_BUDGET.maxSourceBytes); + totalSourceBytes += sourceByteLength; + if (!Number.isSafeInteger(totalSourceBytes) || totalSourceBytes > SCRIPTING_BUDGET.maxSourceBytes) throw new ScriptingPlatformValidationError("SCRIPT_BUDGET_EXCEEDED", "Manifest source bytes exceed the total budget"); + return { id, name: text(item.name, `${name}.name`), entryPath: path(item.entryPath, `${name}.entryPath`), sourceByteLength, sourceSha256: digest(item.sourceSha256, `${name}.sourceSha256`), publisher: text(item.publisher, `${name}.publisher`), signature: item.signature, keyId: text(item.keyId, `${name}.keyId`, 128), permissions: [...item.permissions] as ScriptPermission[], dependencies, module: false, cpuMs: integer(item.cpuMs, `${name}.cpuMs`, 1, SCRIPTING_BUDGET.maxCpuMs), memoryBytes: integer(item.memoryBytes, `${name}.memoryBytes`, 1, SCRIPTING_BUDGET.maxMemoryBytes), wallMs: integer(item.wallMs, `${name}.wallMs`, 1, SCRIPTING_BUDGET.maxWallMs), network: false, autorun: false, driverExpressions: false, addonInstall: false }; }); const scriptIds = new Set(scripts.map((script) => script.id)); const active = new Set(); const complete = new Set(); const visit = (id: string): void => { if (active.has(id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Script dependency cycle includes ${id}`); if (complete.has(id)) return; const script = scripts.find((item) => item.id === id); if (!script) return; active.add(id); for (const dependency of script.dependencies) { if (!scriptIds.has(dependency.id)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `${id} references missing script ${dependency.id}`); visit(dependency.id); } active.delete(id); complete.add(id); }; scripts.forEach((script) => visit(script.id)); @@ -249,6 +467,81 @@ export function gateScriptExecution(manifest: unknown, scriptId: string, approve return blockedGate("N-025", `SCRIPT_${script.id}`, [capabilityIssue("SCRIPT_SANDBOX_UNAVAILABLE", "Local Python/Native execution requires an isolated sandbox")]); } +export function resolveScriptPermissions(manifest: unknown, scriptId: string, requested: unknown = []): ScriptPermissionResolutionIR { + const parsed = parseScriptingManifest(manifest); + const script = parsed.scripts.find((item) => item.id === scriptId); + if (!script) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", `Unknown script ${scriptId}`); + const declared = [...script.permissions].sort(); + const requestedList = Array.isArray(requested) ? requested : []; + const requestedValid = requestedList.every((permission): permission is ScriptPermission => typeof permission === "string" && SCRIPT_PERMISSIONS.includes(permission as ScriptPermission)); + const requestedUnique = new Set(requestedList).size === requestedList.length; + const requestedCanonical = [...requestedList].filter((permission): permission is ScriptPermission => typeof permission === "string" && SCRIPT_PERMISSIONS.includes(permission as ScriptPermission)).sort(); + const allowed = requestedValid && requestedUnique && requestedCanonical.every((permission) => declared.includes(permission)); + return { + status: allowed ? "ALLOWED" : "BLOCKED", + code: allowed ? "SCRIPT_PERMISSIONS_ALLOWED" : "SCRIPT_POLICY_DENIED", + scriptId, + declared, + requested: requestedCanonical, + granted: allowed ? requestedCanonical : [], + }; +} + +export function parseScriptSandboxScope(value: unknown): ScriptSandboxScopeIR { + if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_SCOPE_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script sandbox scope schema"); + const denied = ["dom", "hostWorker", "opfs", "indexedDB", "network"] as const; + if (denied.some((name) => value[name] !== false)) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Script sandbox scope must deny browser and host capabilities"); + return { schemaVersion: SCRIPT_SANDBOX_SCOPE_SCHEMA, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false }; +} + +export function parseScriptSandboxBudget(value: unknown): ScriptSandboxBudgetIR { + if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_SCOPE_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script sandbox budget schema"); + return { + schemaVersion: SCRIPT_SANDBOX_SCOPE_SCHEMA, + cpuMs: integer(value.cpuMs, "sandbox.cpuMs", 1, SCRIPT_SANDBOX_BUDGET.maxCpuMs), + wallMs: integer(value.wallMs, "sandbox.wallMs", 1, SCRIPT_SANDBOX_BUDGET.maxWallMs), + memoryBytes: integer(value.memoryBytes, "sandbox.memoryBytes", 1, SCRIPT_SANDBOX_BUDGET.maxMemoryBytes), + maxMessageBytes: integer(value.maxMessageBytes, "sandbox.maxMessageBytes", 1, SCRIPT_SANDBOX_BUDGET.maxMessageBytes), + maxOutputBytes: integer(value.maxOutputBytes, "sandbox.maxOutputBytes", 1, SCRIPT_SANDBOX_BUDGET.maxOutputBytes), + }; +} + +export function parseScriptHostCall(value: unknown, declaredPermissions: ReadonlySet): ScriptHostCallIR { + if (!record(value) || value.schemaVersion !== SCRIPT_HOST_CALL_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script host call schema"); + const requestId = auditRequestId(value.requestId, "hostCall.requestId"); + const scriptId = text(value.scriptId, "hostCall.scriptId"); + if (!SCRIPT_HOST_CALLS.includes(value.call as ScriptHostCallName)) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Host call is not allowlisted"); + const call = value.call as ScriptHostCallName; + if (value.permission !== call || !declaredPermissions.has(call)) throw new ScriptingPlatformValidationError("SCRIPT_POLICY_DENIED", "Host call permission is not declared"); + if (!record(value.parameters)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Host call parameters must be a structured object"); + const parameters = value.parameters; + const keys = Object.keys(parameters).sort(); + const exact = (expected: string[]): void => { if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Host call parameters contain unknown fields"); }; + let normalized: ScriptHostCallParameters; + if (call === "READ_MAIN") { exact(["revision"]); normalized = { revision: integer(parameters.revision, "hostCall.parameters.revision", 0, Number.MAX_SAFE_INTEGER) }; } + else if (call === "READ_ASSET") { exact(["expectedSha256", "path"]); normalized = { path: path(parameters.path, "hostCall.parameters.path"), expectedSha256: digest(parameters.expectedSha256, "hostCall.parameters.expectedSha256") }; } + else if (call === "WRITE_MAIN") { exact(["operation", "payload", "revision"]); if (!record(parameters.payload)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "hostCall.parameters.payload must be an object"); normalized = { revision: integer(parameters.revision, "hostCall.parameters.revision", 0, Number.MAX_SAFE_INTEGER), operation: text(parameters.operation, "hostCall.parameters.operation", 128), payload: { ...parameters.payload } }; } + else if (call === "WRITE_ASSET") { exact(["byteLength", "path", "sha256"]); normalized = { path: path(parameters.path, "hostCall.parameters.path"), byteLength: integer(parameters.byteLength, "hostCall.parameters.byteLength", 0, SCRIPT_SANDBOX_BUDGET.maxOutputBytes), sha256: digest(parameters.sha256, "hostCall.parameters.sha256") }; } + else { exact(["inputBlendSha256", "settingsSha256"]); normalized = { inputBlendSha256: digest(parameters.inputBlendSha256, "hostCall.parameters.inputBlendSha256"), settingsSha256: digest(parameters.settingsSha256, "hostCall.parameters.settingsSha256") }; } + return { schemaVersion: SCRIPT_HOST_CALL_SCHEMA, requestId, scriptId, call, permission: call, parameters: normalized, execution: "DISABLED" }; +} + +export function terminateScriptSandboxJob(value: unknown, reason: "CRASH" | "TIMEOUT" | "CANCEL"): ScriptSandboxJobIR { + if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_JOB_SCHEMA) throw new ScriptingPlatformValidationError("PROTOCOL_MISMATCH", "Unsupported script sandbox job schema"); + const jobId = auditRequestId(value.jobId, "sandbox.jobId"); + const workerGeneration = integer(value.workerGeneration, "sandbox.workerGeneration", 1, Number.MAX_SAFE_INTEGER); + const baseRevision = integer(value.baseRevision, "sandbox.baseRevision", 0, Number.MAX_SAFE_INTEGER); + const mainRevisionBefore = integer(value.mainRevisionBefore, "sandbox.mainRevisionBefore", 0, Number.MAX_SAFE_INTEGER); + if (baseRevision !== mainRevisionBefore || value.status !== "RUNNING") throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Sandbox termination must start from the current running Main revision"); + const errorCode = reason === "CRASH" ? "SCRIPT_SANDBOX_CRASHED" : reason === "TIMEOUT" ? "SCRIPT_SANDBOX_TIMEOUT" : "SCRIPT_SANDBOX_CANCELLED"; + return { schemaVersion: SCRIPT_SANDBOX_JOB_SCHEMA, jobId, workerGeneration, baseRevision, mainRevisionBefore, mainRevisionAfter: mainRevisionBefore, status: reason === "CRASH" ? "CRASHED" : reason === "TIMEOUT" ? "TIMED_OUT" : "CANCELLED", errorCode, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false, execution: "DISABLED" }; +} + +export function rejectLateScriptSandboxResult(value: unknown): never { + if (!record(value) || value.schemaVersion !== SCRIPT_SANDBOX_JOB_SCHEMA || !["CRASHED", "TIMED_OUT", "CANCELLED"].includes(value.status as string)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Late sandbox result does not reference a terminated job"); + throw new ScriptingPlatformValidationError("SCRIPT_SANDBOX_LATE_RESULT", "Sandbox result arrived after job termination"); +} + export function gateServerScriptJob(value: unknown, manifest: unknown, inputBlendSha256: string): CapabilityGateResult { const parsed = parseScriptingManifest(manifest); if (!record(value)) throw new ScriptingPlatformValidationError("SCRIPT_MANIFEST_INVALID", "Server script job is invalid"); const script = parsed.scripts.find((item) => item.id === value.scriptId); if (!script || script.sourceSha256 !== value.sourceSha256 || !SHA256.test(inputBlendSha256)) throw new ScriptingPlatformValidationError("ASSET_SOURCE_HASH_MISMATCH", "Server script job source hash is invalid"); return blockedGate("N-025", `SERVER_SCRIPT_${script.id}`, [capabilityIssue("SERVER_JOB_UNAVAILABLE", "Server Blender job endpoint is not configured")]); diff --git a/web/protocol/stl-export.ts b/web/protocol/stl-export.ts new file mode 100644 index 00000000..7e345cd2 --- /dev/null +++ b/web/protocol/stl-export.ts @@ -0,0 +1,42 @@ +import type { STLImportResult } from "./stl-import"; + +export const STL_EXPORT_SCHEMA_VERSION = 1 as const; + +export interface STLLossReport { + schemaVersion: typeof STL_EXPORT_SCHEMA_VERSION; + operation: "STL_EXPORT_LOSS_REPORT"; + canRoundTrip: boolean; + warningCount: number; + warnings: Array<{ code: "STL_MATERIAL_UNSUPPORTED"; severity: "warning"; message: string }>; +} + +function writeFloat(view: DataView, offset: number, value: number): void { + if (!Number.isFinite(value)) throw new Error("STL_EXPORT_NUMBER_INVALID"); + view.setFloat32(offset, value, true); +} + +export function exportBinarySTL(document: STLImportResult): ArrayBuffer { + if (document.schemaVersion !== 1 || document.triangleCount !== document.vertices.length || document.triangleCount !== document.normals.length) throw new Error("STL_EXPORT_DOCUMENT_INVALID"); + const output = new ArrayBuffer(84 + document.triangleCount * 50); + const bytes = new Uint8Array(output); + bytes.set(new TextEncoder().encode("Web Blender STL schema 1").subarray(0, 80)); + const view = new DataView(output); + view.setUint32(80, document.triangleCount, true); + for (let triangle = 0; triangle < document.triangleCount; triangle++) { + const offset = 84 + triangle * 50; + for (let axis = 0; axis < 3; axis++) writeFloat(view, offset + axis * 4, document.normals[triangle][axis]); + for (let vertex = 0; vertex < 3; vertex++) for (let axis = 0; axis < 3; axis++) writeFloat(view, offset + 12 + vertex * 12 + axis * 4, document.vertices[triangle][vertex][axis]); + view.setUint16(offset + 48, 0, true); + } + return output; +} + +export function createSTLLossReport(sourceMaterialCount: number): STLLossReport { + if (!Number.isSafeInteger(sourceMaterialCount) || sourceMaterialCount < 0) throw new Error("STL_EXPORT_MATERIAL_COUNT_INVALID"); + const warnings = sourceMaterialCount > 0 ? [{ + code: "STL_MATERIAL_UNSUPPORTED" as const, + severity: "warning" as const, + message: `STL has no material slots; ${sourceMaterialCount} source material assignments are omitted`, + }] : []; + return { schemaVersion: STL_EXPORT_SCHEMA_VERSION, operation: "STL_EXPORT_LOSS_REPORT", canRoundTrip: true, warningCount: warnings.length, warnings }; +} diff --git a/web/protocol/stl-import.ts b/web/protocol/stl-import.ts new file mode 100644 index 00000000..9a900f98 --- /dev/null +++ b/web/protocol/stl-import.ts @@ -0,0 +1,125 @@ +export const STL_IMPORT_SCHEMA_VERSION = 1 as const; + +export type STLVariant = "STL_BINARY" | "STL_ASCII"; + +export const STL_IMPORT_BUDGET = { + maxBytes: 512 * 1024, + maxTriangles: 65_536, + maxUnitScale: 1_000_000, +} as const; + +export interface STLImportResult { + schemaVersion: typeof STL_IMPORT_SCHEMA_VERSION; + variant: STLVariant; + unitScale: number; + declaredTriangleCount: number; + triangleCount: number; + removedDegenerateTriangles: number; + normals: number[][]; + vertices: number[][][]; + bounds: { min: number[]; max: number[] }; +} + +function unitScale(value: number): number { + if (!Number.isFinite(value) || value <= 0 || value > STL_IMPORT_BUDGET.maxUnitScale) throw new Error("STL_UNIT_SCALE_INVALID"); + return value; +} + +function finite(values: number[], label: string): number[] { + if (values.some((value) => !Number.isFinite(value))) throw new Error(`STL_NUMBER_INVALID: ${label}`); + return values.map((value) => value === 0 ? 0 : value); +} + +function degenerate(vertices: number[][]): boolean { + const left = vertices[1].map((value, index) => value - vertices[0][index]); + const right = vertices[2].map((value, index) => value - vertices[0][index]); + const cross = [left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]]; + return cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2] <= 1e-20; +} + +function finish(variant: STLVariant, scale: number, declaredTriangleCount: number, normals: number[][], rawVertices: number[][][]): STLImportResult { + const keptNormals: number[][] = []; + const vertices: number[][][] = []; + let removedDegenerateTriangles = 0; + for (let index = 0; index < rawVertices.length; index++) { + if (degenerate(rawVertices[index])) { + removedDegenerateTriangles++; + continue; + } + keptNormals.push(normals[index]); + vertices.push(rawVertices[index].map((vertex) => vertex.map((value) => value * scale))); + } + const flat = vertices.flat(); + const bounds = flat.length > 0 ? { + min: [0, 1, 2].map((axis) => Math.min(...flat.map((vertex) => vertex[axis]))), + max: [0, 1, 2].map((axis) => Math.max(...flat.map((vertex) => vertex[axis]))), + } : { min: [0, 0, 0], max: [0, 0, 0] }; + return { + schemaVersion: STL_IMPORT_SCHEMA_VERSION, + variant, + unitScale: scale, + declaredTriangleCount, + triangleCount: vertices.length, + removedDegenerateTriangles, + normals: keptNormals, + vertices, + bounds, + }; +} + +function parseBinary(bytes: ArrayBuffer, scale: number): STLImportResult { + if (bytes.byteLength < 84) throw new Error("STL_BINARY_TRUNCATED"); + const view = new DataView(bytes); + const count = view.getUint32(80, true); + if (count > STL_IMPORT_BUDGET.maxTriangles) throw new Error("STL_IMPORT_BUDGET_EXCEEDED: triangles"); + const expectedBytes = 84 + count * 50; + if (bytes.byteLength < expectedBytes) throw new Error("STL_BINARY_TRUNCATED"); + if (bytes.byteLength > expectedBytes) throw new Error("STL_TRAILING_BYTES"); + const normals: number[][] = []; + const vertices: number[][][] = []; + for (let triangle = 0; triangle < count; triangle++) { + const offset = 84 + triangle * 50; + normals.push(finite([view.getFloat32(offset, true), view.getFloat32(offset + 4, true), view.getFloat32(offset + 8, true)], `normal ${triangle}`)); + const triangleVertices = []; + for (let vertex = 0; vertex < 3; vertex++) { + const vertexOffset = offset + 12 + vertex * 12; + triangleVertices.push(finite([view.getFloat32(vertexOffset, true), view.getFloat32(vertexOffset + 4, true), view.getFloat32(vertexOffset + 8, true)], `vertex ${triangle}/${vertex}`)); + } + vertices.push(triangleVertices); + } + return finish("STL_BINARY", scale, count, normals, vertices); +} + +function parseAscii(bytes: ArrayBuffer, scale: number): STLImportResult { + let source: string; + try { source = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } + catch { throw new Error("STL_ASCII_INVALID"); } + const end = source.search(/^endsolid.*$/m); + if (!/^solid(?:\s|$)/.test(source) || end < 0) throw new Error("STL_ASCII_INVALID"); + const endLine = source.indexOf("\n", end); + const trailing = source.slice(endLine < 0 ? source.length : endLine + 1); + if (trailing.trim()) throw new Error("STL_TRAILING_BYTES"); + const facetPattern = /facet\s+normal\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+outer\s+loop\s+vertex\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+vertex\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+vertex\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+endloop\s+endfacet/g; + const normals: number[][] = []; + const vertices: number[][][] = []; + let match: RegExpExecArray | null; + while ((match = facetPattern.exec(source.slice(0, end)))) { + normals.push(finite(match.slice(1, 4).map(Number), `normal ${normals.length}`)); + vertices.push([ + finite(match.slice(4, 7).map(Number), `vertex ${vertices.length}/0`), + finite(match.slice(7, 10).map(Number), `vertex ${vertices.length}/1`), + finite(match.slice(10, 13).map(Number), `vertex ${vertices.length}/2`), + ]); + if (vertices.length > STL_IMPORT_BUDGET.maxTriangles) throw new Error("STL_IMPORT_BUDGET_EXCEEDED: triangles"); + } + if (vertices.length === 0) throw new Error("STL_ASCII_INVALID"); + return finish("STL_ASCII", scale, vertices.length, normals, vertices); +} + +export function importSTL(bytes: ArrayBuffer, options: { variant: STLVariant; unitScale: number }): STLImportResult { + if (bytes.byteLength > STL_IMPORT_BUDGET.maxBytes) throw new Error("STL_IMPORT_BUDGET_EXCEEDED: bytes"); + const scale = unitScale(options.unitScale); + if (options.variant === "STL_BINARY") return parseBinary(bytes, scale); + if (options.variant === "STL_ASCII") return parseAscii(bytes, scale); + throw new Error("STL_VARIANT_REQUIRED"); +} diff --git a/web/protocol/storage.ts b/web/protocol/storage.ts index 772d94fa..13cfc28c 100644 --- a/web/protocol/storage.ts +++ b/web/protocol/storage.ts @@ -257,7 +257,7 @@ export interface StorageRequest { | { type: "saveSnapshot"; projectId: string; revision: number; buffer: ArrayBuffer; maxCount?: number; maxBytes?: number } | { type: "listSnapshots"; projectId: string } | { type: "readSnapshot"; projectId: string; revision: number } - | { type: "putAsset"; projectId: string; data: ArrayBuffer; mimeType: string; sourcePath?: string } + | { type: "putAsset"; projectId: string; data: ArrayBuffer; mimeType: string; sourcePath?: string; faultAt?: "quota" } | { type: "readAsset"; projectId: string; sha256: string } | { type: "listAssets"; projectId: string } | { type: "commitTexturePaintTile"; commit: TexturePaintTileCommitIR } diff --git a/web/protocol/viewport-dpr.ts b/web/protocol/viewport-dpr.ts new file mode 100644 index 00000000..54c6f341 --- /dev/null +++ b/web/protocol/viewport-dpr.ts @@ -0,0 +1,49 @@ +export const VIEWPORT_DPR_SCHEMA_VERSION = 1 as const; +export const VIEWPORT_MAX_DPR = 2 as const; + +export interface ViewportPixelMetrics { + schemaVersion: typeof VIEWPORT_DPR_SCHEMA_VERSION; + cssWidth: number; + cssHeight: number; + pixelRatio: number; + backingWidth: number; + backingHeight: number; +} + +export interface ViewportNDC { + x: number; + y: number; +} + +function finitePositive(value: number): boolean { + return Number.isFinite(value) && value > 0; +} + +export function resolveViewportPixelRatio(devicePixelRatio: number | undefined, maximum = VIEWPORT_MAX_DPR): number { + if (!finitePositive(maximum)) throw new Error("VIEWPORT_DPR_INVALID"); + const observed = finitePositive(devicePixelRatio ?? 1) ? devicePixelRatio! : 1; + return Math.min(observed, maximum); +} + +export function resolveViewportPixelMetrics(cssWidth: number, cssHeight: number, devicePixelRatio: number | undefined, maximum = VIEWPORT_MAX_DPR): ViewportPixelMetrics { + if (!finitePositive(cssWidth) || !finitePositive(cssHeight)) throw new Error("VIEWPORT_SIZE_INVALID"); + const pixelRatio = resolveViewportPixelRatio(devicePixelRatio, maximum); + return { + schemaVersion: VIEWPORT_DPR_SCHEMA_VERSION, + cssWidth, + cssHeight, + pixelRatio, + backingWidth: Math.max(1, Math.floor(cssWidth * pixelRatio)), + backingHeight: Math.max(1, Math.floor(cssHeight * pixelRatio)), + }; +} + +export function viewportNDC(clientX: number, clientY: number, bounds: { left: number; top: number; width: number; height: number }): ViewportNDC { + if (![clientX, clientY, bounds.left, bounds.top, bounds.width, bounds.height].every(Number.isFinite) || bounds.width <= 0 || bounds.height <= 0) { + throw new Error("VIEWPORT_BOUNDS_INVALID"); + } + return { + x: ((clientX - bounds.left) / bounds.width) * 2 - 1, + y: -((clientY - bounds.top) / bounds.height) * 2 + 1, + }; +} diff --git a/web/tests/e2e/archive-security-fixtures.spec.ts b/web/tests/e2e/archive-security-fixtures.spec.ts new file mode 100644 index 00000000..7075c9ab --- /dev/null +++ b/web/tests/e2e/archive-security-fixtures.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from "@playwright/test"; +import { execFileSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; + +const root = path.resolve(import.meta.dirname, "../../.."); + +test("N-023 asset malicious ZIP/TAR fixtures remain in the archive security regression", async () => { + const output = execFileSync(process.execPath, [path.join(root, "tools/web/check-malicious-archive-fixtures.mjs")], { + cwd: root, + encoding: "utf8", + }); + expect(output).toContain("malicious-archive-fixtures-ok cases=6 zip=3 tar=3 extraction=disabled"); + + const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/files/web/archive-security/manifest.json"), "utf8")); + expect(manifest.extractionAllowed).toBe(false); + expect(manifest.cases).toHaveLength(6); + expect(manifest.cases.map((fixture: { expectedCode: string }) => fixture.expectedCode)).toEqual( + Array.from({ length: 6 }, () => "IO_ARCHIVE_UNSAFE"), + ); +}); diff --git a/web/tests/e2e/glb-desktop-import.spec.ts b/web/tests/e2e/glb-desktop-import.spec.ts new file mode 100644 index 00000000..20460303 --- /dev/null +++ b/web/tests/e2e/glb-desktop-import.spec.ts @@ -0,0 +1,42 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"), "utf8")); +const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1"); + +test("imports the M12-06A desktop GLB fixture group in a Chromium Worker", async ({ page }) => { + await page.goto("/"); + const fixtures = report.fixtures.map((fixture: { id: string; file: string; sha256: string; semantic: unknown }) => ({ + id: fixture.id, + bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.file))), + sourceSha256: fixture.sha256, + expected: fixture.semantic, + })); + const result = await page.evaluate(async (input) => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/glb-desktop-import-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { + worker.terminate(); + resolve(event.data); + }; + worker.onerror = (event) => { + worker.terminate(); + reject(new Error(event.message)); + }; + const transferred = input.map((fixture) => { + const bytes = Uint8Array.from(fixture.bytes); + return { ...fixture, bytes: bytes.buffer }; + }); + worker.postMessage({ fixtures: transferred }, transferred.map((fixture) => fixture.bytes)); + }), fixtures); + + expect(result.ok).toBe(true); + expect(result.results).toEqual([ + { id: "mesh", sourceSha256: report.fixtures[0].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["COLOR_0", "NORMAL", "POSITION"], materials: 1, nodes: 1, animations: 0 }, + { id: "pbr", sourceSha256: report.fixtures[1].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["NORMAL", "POSITION"], materials: 1, nodes: 1, animations: 0 }, + { id: "uv", sourceSha256: report.fixtures[2].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["NORMAL", "POSITION", "TEXCOORD_0"], materials: 1, nodes: 1, animations: 0 }, + { id: "skin", sourceSha256: report.fixtures[3].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["JOINTS_0", "NORMAL", "POSITION", "WEIGHTS_0"], materials: 1, nodes: 4, animations: 0 }, + { id: "animation", sourceSha256: report.fixtures[4].sha256, compatible: true, mismatches: [], topology: 1, attributes: ["NORMAL", "POSITION"], materials: 1, nodes: 1, animations: 1 }, + ]); +}); diff --git a/web/tests/e2e/glb-export-loss-report.spec.ts b/web/tests/e2e/glb-export-loss-report.spec.ts new file mode 100644 index 00000000..a1262f00 --- /dev/null +++ b/web/tests/e2e/glb-export-loss-report.spec.ts @@ -0,0 +1,106 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const mainReport = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8")); +const reportPath = path.join(root, "tests/golden/M12-06D/web-loss-report.json"); +const fixtureRoot = path.join(root, "tests/files/web/m12_glb_main_v1"); + +const sha256 = (bytes: Uint8Array): string => crypto.createHash("sha256").update(bytes).digest("hex"); + +test("writes a machine GLB loss report for every persisted Main fixture", async ({ page }) => { + await page.goto("/"); + const fixtures = mainReport.fixtures.map((fixture: { id: string; blend: { file: string; sha256: string } }) => ({ + id: fixture.id, + bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.blend.file))), + sourceBlendSha256: fixture.blend.sha256, + })); + const generated = await page.evaluate(async (input) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const digest = async (bytes: ArrayBuffer): Promise => { + const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + return Array.from(hash, (value) => value.toString(16).padStart(2, "0")).join(""); + }; + const reports = []; + for (const fixture of input) { + const client = new WebEngineClient({ timeoutMs: 30_000 }); + try { + const opened = await client.openBlend(Uint8Array.from(fixture.bytes).buffer); + const assets = []; + for (const image of opened.snapshot.images) { + const asset = await client.requestAsset(image.assetId); + if (asset.status === "packed" && asset.data && asset.mimeType) assets.push({ assetId: image.assetId, mimeType: asset.mimeType, data: asset.data }); + } + const worker = new Worker("/src/workers/glb-loss-report-test.worker.ts", { type: "module" }); + const result = await new Promise<{ report: any; output: ArrayBuffer | null }>((resolve, reject) => { + worker.onmessage = (event: MessageEvent<{ ok: boolean; report?: any; output?: ArrayBuffer | null; error?: string }>) => { + worker.terminate(); + if (!event.data.ok || !event.data.report) reject(new Error(event.data.error ?? "GLB loss report worker failed")); + else resolve({ report: event.data.report, output: event.data.output ?? null }); + }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const transfer: Transferable[] = []; + for (const geometry of opened.geometryBuffers) for (const value of Object.values(geometry)) if (value instanceof ArrayBuffer) transfer.push(value); + for (const asset of assets) transfer.push(asset.data); + for (const chunk of opened.nonMeshGeometryBuffers ?? []) for (const value of Object.values(chunk)) if (value instanceof ArrayBuffer) transfer.push(value); + worker.postMessage({ snapshot: opened.snapshot, geometryBuffers: opened.geometryBuffers, assetBuffers: assets, nonMeshGeometryBuffers: opened.nonMeshGeometryBuffers ?? [] }, transfer); + }); + reports.push({ + fixtureId: fixture.id, + sourceBlendSha256: fixture.sourceBlendSha256, + lossReport: result.report, + output: result.output ? { byteLength: result.output.byteLength, sha256: await digest(result.output), bytes: Array.from(new Uint8Array(result.output)) } : null, + }); + } + finally { + client.terminate(); + } + } + return reports; + }, fixtures); + + const report = { + schemaVersion: 1, + task: "M12-06D", + operation: "WEB_GLB_EXPORT_LOSS_REPORT", + fixtureCount: generated.length, + fixtures: generated, + nextTask: "M12-06E", + }; + if (process.env.UPDATE_GLB_LOSS_REPORT === "1") { + const outputRoot = path.join(root, "tests/files/web/m12_glb_web_v1"); + fs.mkdirSync(outputRoot, { recursive: true }); + for (const fixture of generated) { + if (fixture.output?.bytes) fs.writeFileSync(path.join(outputRoot, `${fixture.fixtureId}.glb`), Buffer.from(fixture.output.bytes)); + } + } + for (const fixture of report.fixtures) if (fixture.output?.bytes) delete fixture.output.bytes; + if (process.env.UPDATE_GLB_LOSS_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + const expected = JSON.parse(fs.readFileSync(reportPath, "utf8")); + expect(report).toEqual(expected); + expect(report.fixtureCount).toBe(5); + for (const fixture of report.fixtures) { + expect(fixture.lossReport.schemaVersion).toBe(1); + expect(fixture.lossReport.operation).toBe("GLB_EXPORT_LOSS_REPORT"); + if (fixture.fixtureId === "mesh") { + expect(fixture.lossReport.canExport).toBe(false); + expect(fixture.lossReport.errorCount).toBe(1); + expect(fixture.lossReport.losses.map((loss: { code: string }) => loss.code)).toEqual(["LINKED_MATERIAL_INPUT_UNEVALUATED", "SHADER_GRAPH_UNMAPPABLE"]); + expect(fixture.output).toBeNull(); + } + else { + expect(fixture.lossReport.canExport).toBe(true); + expect(fixture.lossReport.errorCount).toBe(0); + expect(fixture.output?.byteLength).toBeGreaterThan(128); + expect(fixture.output?.sha256).toMatch(/^[0-9a-f]{64}$/); + } + expect(fixture.lossReport.losses).toEqual([...fixture.lossReport.losses].sort((left, right) => + left.code.localeCompare(right.code) || left.severity.localeCompare(right.severity) || + (left.id ?? "").localeCompare(right.id ?? "") || left.message.localeCompare(right.message))); + } +}); diff --git a/web/tests/e2e/glb-main-persistence.spec.ts b/web/tests/e2e/glb-main-persistence.spec.ts new file mode 100644 index 00000000..d5437dc5 --- /dev/null +++ b/web/tests/e2e/glb-main-persistence.spec.ts @@ -0,0 +1,98 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06C/desktop-main-report.json"), "utf8")); +const fixtureRoot = path.join(root, "tests/files/web/m12_glb_main_v1"); + +function stableSnapshot(snapshot: Record) { + return { + objects: snapshot.nodes.filter((node: any) => node.id?.startsWith("object:")).map((node: any) => node.id).sort(), + meshes: snapshot.meshes.map((mesh: any) => mesh.id).sort(), + materials: snapshot.materials.map((material: any) => material.id).sort(), + images: snapshot.images.map((image: any) => image.id).sort(), + armatures: (snapshot.armatures ?? []).map((armature: any) => armature.id).sort(), + actions: snapshot.animations.map((animation: any) => animation.id).sort(), + }; +} + +async function sha256(bytes: ArrayBuffer): Promise { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join(""); +} + +test("persists desktop-imported GLB Main data through WebEngine save and reopen", async ({ page }) => { + await page.goto("/"); + const fixtures = report.fixtures.map((fixture: { id: string; blend: { file: string; sha256: string }; graph: { stableIds: Record } }) => ({ + id: fixture.id, + bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, fixture.blend.file))), + sourceSha256: fixture.blend.sha256, + expected: fixture.graph.stableIds, + })); + const result = await page.evaluate(async (input) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const output = []; + const stableSnapshot = (snapshot: Record) => ({ + objects: snapshot.nodes.filter((node: any) => node.id?.startsWith("object:")).map((node: any) => node.id).sort(), + meshes: snapshot.meshes.map((mesh: any) => mesh.id).sort(), + materials: snapshot.materials.map((material: any) => material.id).sort(), + images: snapshot.images.map((image: any) => image.id).sort(), + armatures: (snapshot.armatures ?? []).map((armature: any) => armature.id).sort(), + actions: snapshot.animations.map((animation: any) => animation.id).sort(), + }); + const digestSha256 = async (bytes: ArrayBuffer): Promise => { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes)); + return Array.from(digest, (value) => value.toString(16).padStart(2, "0")).join(""); + }; + for (const fixture of input) { + const client = new WebEngineClient({ timeoutMs: 30_000 }); + try { + const opened = await client.openBlend(Uint8Array.from(fixture.bytes).buffer); + const before = opened.snapshot; + const beforeIds = stableSnapshot(before); + const objectId = before.activeObjectId ?? before.nodes.find((node: any) => node.id?.startsWith("object:"))?.id; + if (!objectId) throw new Error(`${fixture.id} did not produce an active Main object`); + const edited = await client.applyCommand({ type: "setObjectVisibility", objectId, visible: false }); + const saved = await client.saveBlend(); + const savedSha256 = await digestSha256(saved); + if (savedSha256 === fixture.sourceSha256) throw new Error(`${fixture.id} save did not serialize the Main visibility edit`); + const reopened = await client.openBlend(saved); + const after = reopened.snapshot; + const afterIds = stableSnapshot(after); + const afterObject = after.nodes.find((node: any) => node.id === objectId); + const resources = await client.openResourceStatus(); + output.push({ + id: fixture.id, + before: beforeIds, + after: afterIds, + expected: fixture.expected, + revision: [before.revision, edited.snapshot.revision, after.revision], + savedSha256, + sourceSha256: fixture.sourceSha256, + visibilityAfterReopen: afterObject?.visible, + resources, + }); + } + finally { + client.terminate(); + } + } + return output; + }, fixtures); + + for (const item of result) { + expect(item.before).toEqual(item.expected); + expect(item.after).toEqual(item.before); + expect(item.revision[1]).toBeGreaterThan(item.revision[0]); + expect(item.revision[2]).toBeGreaterThan(0); + expect(item.savedSha256).not.toEqual(item.sourceSha256); + expect(item.visibilityAfterReopen).toBe(false); + expect(item.resources).toMatchObject({ activeRequests: 0, liveInputBytes: 0, liveStagingFiles: 0 }); + expect(item.before.objects.every((id: string) => id.startsWith("object:"))).toBe(true); + expect(item.before.meshes.every((id: string) => id.startsWith("mesh:"))).toBe(true); + expect(item.before.materials.every((id: string) => id.startsWith("material:"))).toBe(true); + expect(item.before.images.every((id: string) => id.startsWith("image:"))).toBe(true); + } + expect(result).toHaveLength(5); +}); diff --git a/web/tests/e2e/glb-negative-cases.spec.ts b/web/tests/e2e/glb-negative-cases.spec.ts new file mode 100644 index 00000000..0fdf0155 --- /dev/null +++ b/web/tests/e2e/glb-negative-cases.spec.ts @@ -0,0 +1,46 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const source = fs.readFileSync(path.join(root, "tests/files/web/m12_glb_desktop_v1/mesh.glb")); +const maxBytes = 512 * 1024; + +function rewriteJson(mutator: (document: any) => void): Buffer { + const jsonLength = source.readUInt32LE(12); + const document = JSON.parse(source.subarray(20, 20 + jsonLength).toString("utf8").trim()); + mutator(document); + const json = Buffer.from(JSON.stringify(document)); + const paddedLength = (json.length + 3) & ~3; + const output = Buffer.alloc(12 + 8 + paddedLength + (source.length - (20 + jsonLength))); + output.writeUInt32LE(0x46546c67, 0); output.writeUInt32LE(2, 4); output.writeUInt32LE(output.length, 8); + output.writeUInt32LE(paddedLength, 12); output.writeUInt32LE(0x4e4f534a, 16); json.copy(output, 20); + output.fill(0x20, 20 + json.length, 20 + paddedLength); + output.writeUInt32LE(source.readUInt32LE(20 + jsonLength), 20 + paddedLength); + output.writeUInt32LE(source.readUInt32LE(24 + jsonLength), 24 + paddedLength); + source.subarray(28 + jsonLength).copy(output, 28 + paddedLength); + return output; +} + +test("rejects GLB sparse, extension, external URI and over-budget cases in a Chromium Worker", async ({ page }) => { + await page.goto("/"); + const cases = [ + { id: "sparse", bytes: rewriteJson((document) => { document.accessors[0].sparse = { count: 1 }; }) }, + { id: "extension", bytes: rewriteJson((document) => { document.extensionsUsed = ["KHR_draco_mesh_compression"]; }) }, + { id: "external-uri", bytes: rewriteJson((document) => { document.buffers[0].uri = "external.bin"; }) }, + { id: "over-budget", bytes: Buffer.concat([source, Buffer.alloc(maxBytes + 1 - source.length)]) }, + ].map((candidate) => ({ id: candidate.id, bytes: Array.from(candidate.bytes) })); + const result = await page.evaluate((input) => new Promise<{ ok: boolean; results?: Array<{ id: string; code: string }>; error?: string }>((resolve, reject) => { + const worker = new Worker("/src/workers/glb-negative-cases-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent<{ ok: boolean; results?: Array<{ id: string; code: string }>; error?: string }>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const cases = input.map((candidate) => ({ id: candidate.id, bytes: Uint8Array.from(candidate.bytes).buffer })); + worker.postMessage({ cases }, cases.map((candidate) => candidate.bytes)); + }), cases); + expect(result).toEqual({ ok: true, results: [ + { id: "sparse", code: "GLB_SPARSE_ACCESSOR_UNSUPPORTED" }, + { id: "extension", code: "GLB_EXTENSION_UNSUPPORTED" }, + { id: "external-uri", code: "GLB_EXTERNAL_URI_BLOCKED" }, + { id: "over-budget", code: "GLB_IMPORT_BUDGET_EXCEEDED" }, + ] }); +}); diff --git a/web/tests/e2e/glb-recovery.spec.ts b/web/tests/e2e/glb-recovery.spec.ts new file mode 100644 index 00000000..a7f138cb --- /dev/null +++ b/web/tests/e2e/glb-recovery.spec.ts @@ -0,0 +1,127 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const glbBytes = Array.from(fs.readFileSync(path.join(root, "tests/files/web/m12_glb_desktop_v1/pbr.glb"))); +const blendBytes = Array.from(fs.readFileSync(path.join(root, "tests/files/web/m12_glb_main_v1/pbr.blend"))); + +test("M12-06G cancels GLB import/export without publishing temporary output", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (fixtures) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const client = new WebEngineClient({ timeoutMs: 30_000 }); + const opened = await client.openBlend(Uint8Array.from(fixtures.blend).buffer); + const assets = []; + for (const image of opened.snapshot.images) { + const asset = await client.requestAsset(image.assetId); + if (asset.status === "packed" && asset.data && asset.mimeType) assets.push({ assetId: image.assetId, data: asset.data, mimeType: asset.mimeType }); + } + const cancel = async (operation: "IMPORT" | "EXPORT") => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/glb-recovery-test.worker.ts", { type: "module" }); + const requestId = `glb-${operation.toLowerCase()}-cancel-1`; + worker.onmessage = (event: MessageEvent>) => { + worker.terminate(); + if (!event.data.ok) reject(new Error(String(event.data.error))); + else resolve(event.data); + }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const bytes = Uint8Array.from(fixtures.glb).buffer; + worker.postMessage({ + type: "run", + requestId, + operation, + bytes, + snapshot: operation === "EXPORT" ? opened.snapshot : undefined, + geometryBuffers: operation === "EXPORT" ? opened.geometryBuffers : undefined, + assetBuffers: operation === "EXPORT" ? assets : undefined, + nonMeshGeometryBuffers: operation === "EXPORT" ? opened.nonMeshGeometryBuffers ?? [] : undefined, + baseRevision: opened.snapshot.revision, + workerGeneration: 1, + }); + setTimeout(() => worker.postMessage({ type: "cancel", targetRequestId: requestId }), 3); + }); + const imported = await cancel("IMPORT"); + const exported = await cancel("EXPORT"); + client.terminate(); + return { imported, exported }; + }, { glb: glbBytes, blend: blendBytes }); + for (const operation of [result.imported, result.exported]) { + expect(operation.ok).toBe(true); + expect((operation.receipt as { status: string }).status).toBe("CANCELLED"); + expect((operation.receipt as { errorCode: string }).errorCode).toBe("GLB_OPERATION_CANCELLED"); + expect((operation.receipt as { temporaryBytes: number; liveRequests: number; committed: boolean })).toMatchObject({ temporaryBytes: 0, liveRequests: 0, committed: false }); + } +}); + +test("M12-06G recovers GLB import after Worker restart and retains old OPFS asset after quota", async ({ page }) => { + await page.goto("/"); + const cdp = await page.context().newCDPSession(page); + await cdp.send("Storage.overrideQuotaForOrigin", { origin: new URL(page.url()).origin, quotaSize: 64 * 1024 }); + const result = await page.evaluate(async (fixture) => { + const { StorageClient } = await import("/src/storage/StorageClient.ts"); + const { beginGLBRecoveryOperation, blockGLBRecoveryForQuota, recoverGLBRecoveryOperation } = await import("/src/testing/glb-recovery.ts"); + const runWorker = (generation: number) => new Promise<{ receipt: any; result: { outputSha256: string } }>((resolve, reject) => { + const worker = new Worker("/src/workers/glb-recovery-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent<{ ok: boolean; receipt?: any; result?: { outputSha256: string }; error?: string }>) => { + worker.terminate(); + if (!event.data.ok || !event.data.receipt || !event.data.result) reject(new Error(event.data.error ?? "GLB worker failed")); + else resolve({ receipt: event.data.receipt, result: event.data.result }); + }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ type: "run", requestId: `glb-import-generation-${generation}`, operation: "IMPORT", bytes: Uint8Array.from(fixture).buffer, baseRevision: 4, workerGeneration: generation }); + }); + const firstRun = await runWorker(1); + const restartedRun = await runWorker(2); + const recovered = recoverGLBRecoveryOperation(firstRun.receipt, 2); + const projectId = `glb-recovery-${Date.now()}-${Math.random().toString(16).slice(2)}`; + const storage = new StorageClient(); + await storage.ensureProject(projectId); + const asset = await storage.putAsset(projectId, Uint8Array.from(fixture).buffer, "model/gltf-binary", "imports/model.glb"); + let quotaError = ""; + let quotaReceipt; + const quotaPayload = Uint8Array.from({ length: 128 * 1024 }, (_, index) => (index * 7) & 0xff).buffer; + const quotaRunning = beginGLBRecoveryOperation({ operationId: "export-quota-1", operation: "EXPORT", workerGeneration: 2, baseRevision: 4, inputBytes: quotaPayload.byteLength, inputSha256: await crypto.subtle.digest("SHA-256", quotaPayload).then((digest) => Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")) }); + try { await storage.putAsset(projectId, quotaPayload, "model/gltf-binary", "imports/rejected.glb"); } + catch (error) { quotaError = error instanceof Error ? error.message : String(error); quotaReceipt = blockGLBRecoveryForQuota(quotaRunning); } + storage.terminate(); + const restartedStorage = new StorageClient(); + const restored = await restartedStorage.readAsset(projectId, asset.sha256); + const assets = await restartedStorage.listAssets(projectId); + restartedStorage.terminate(); + return { + firstHash: firstRun.result.outputSha256, + restartedHash: restartedRun.result.outputSha256, + recoveredStatus: recovered.status, + recoveredGeneration: recovered.workerGeneration, + quotaError, + quotaCode: quotaReceipt?.errorCode, + assetSha256: asset.sha256, + restoredSha256: restored.asset.sha256, + restoredBytes: restored.data.byteLength, + assetCount: assets.assets.length, + projectId, + backendPath: asset.path, + }; + }, glbBytes); + expect(result.firstHash).toMatch(/^[a-f0-9]{64}$/); + expect(result.restartedHash).toBe(result.firstHash); + expect(result.recoveredStatus).toBe("RECOVERED"); + expect(result.recoveredGeneration).toBe(2); + expect(result.quotaError).toMatch(/QuotaExceededError|storage quota|exceed its storage quota/i); + expect(result.quotaCode).toBe("GLB_OPFS_QUOTA"); + expect(result.restoredSha256).toBe(result.assetSha256); + expect(result.restoredBytes).toBe(glbBytes.length); + expect(result.assetCount).toBe(1); + expect(result.backendPath).toMatch(/^projects\//); + await cdp.send("Storage.overrideQuotaForOrigin", { origin: new URL(page.url()).origin, quotaSize: 1024 * 1024 * 1024 }); + const recoveredStorage = await page.evaluate(async (projectId) => { + const { StorageClient } = await import("/src/storage/StorageClient.ts"); + const storage = new StorageClient(); + const small = await storage.putAsset(projectId, Uint8Array.from([5, 6, 7]).buffer, "model/gltf-binary", "imports/recovery.glb"); + const assets = await storage.listAssets(projectId); + storage.terminate(); + return { persisted: small.persisted, assetCount: assets.assets.length }; + }, result.projectId); + expect(recoveredStorage).toEqual({ persisted: true, assetCount: 2 }); +}); diff --git a/web/tests/e2e/io-format-recovery.spec.ts b/web/tests/e2e/io-format-recovery.spec.ts new file mode 100644 index 00000000..8130bf24 --- /dev/null +++ b/web/tests/e2e/io-format-recovery.spec.ts @@ -0,0 +1,68 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixtures = { + OBJ: fs.readFileSync(path.join(root, "tests/files/web/m12_obj_multi_v1/multi-object.obj")), + STL: fs.readFileSync(path.join(root, "tests/files/web/m12_stl_capability_v1/capability-binary.stl")), + PLY: fs.readFileSync(path.join(root, "tests/files/web/m12_ply_mapping_v1/mapping-ascii.ply")), +}; +const reportPath = path.join(root, "tests/golden/M12-07J/io-format-recovery-report.json"); +const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex"); + +test("M12-07J recovers OBJ/STL/PLY after cancellation, OOM budget faults and Worker restart", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (input) => { + const recovery = await import("/src/testing/io-format-recovery.ts"); + const run = (format: "OBJ" | "STL" | "PLY", bytes: number[], generation: number, phase: string) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" }); + const requestId = `${format.toLowerCase()}-${phase}-${generation}`; + worker.onmessage = (event: MessageEvent) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const payload = Uint8Array.from(bytes).buffer; + worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: generation, baseRevision: 4 }, [payload]); + }); + const cancel = (format: "OBJ" | "STL" | "PLY", bytes: number[]) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/io-format-recovery-test.worker.ts", { type: "module" }); + const requestId = `${format.toLowerCase()}-cancel-1`; + worker.onmessage = (event: MessageEvent) => { worker.terminate(); if (!event.data.ok) reject(new Error(event.data.error)); else resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const payload = Uint8Array.from(bytes).buffer; + worker.postMessage({ type: "run", requestId, format, operation: "IMPORT", bytes: payload, workerGeneration: 1, baseRevision: 4 }, [payload]); + setTimeout(() => worker.postMessage({ type: "cancel", targetRequestId: requestId }), 3); + }); + const summary: Record = {}; + for (const format of ["OBJ", "STL", "PLY"] as const) { + const source = input[format]; + const cancelled = await cancel(format, source); + const oversized = new Array(512 * 1024 + 1).fill(7); + const oom = await run(format, oversized, 1, "oom"); + const first = await run(format, source, 1, "first"); + const second = await run(format, source, 2, "second"); + const recovered = recovery.recoverIOFormatRecoveryOperation(first.receipt, 2); + const small = await run(format, source, 3, "small"); + summary[format] = { sourceSha256: await crypto.subtle.digest("SHA-256", Uint8Array.from(source)).then((digest) => Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("")), cancel: cancelled.receipt, oom: oom.receipt, first: first.receipt, second: second.receipt, recovered, small: small.receipt, hashes: { first: first.result.outputSha256, second: second.result.outputSha256, small: small.result.outputSha256 } }; + } + return summary; + }, Object.fromEntries(Object.entries(fixtures).map(([format, bytes]) => [format, Array.from(bytes)]))); + + for (const format of ["OBJ", "STL", "PLY"] as const) { + const value = result[format]; + expect(value.cancel).toMatchObject({ status: "CANCELLED", errorCode: "IO_FORMAT_OPERATION_CANCELLED", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false }); + expect(value.oom).toMatchObject({ status: "BLOCKED", errorCode: "IO_FORMAT_OOM", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false }); + expect(value.recovered).toMatchObject({ status: "RECOVERED", workerGeneration: 2, errorCode: "IO_FORMAT_WORKER_RESTARTED", committed: true }); + expect(value.first).toMatchObject({ status: "COMMITTED", workerGeneration: 1, publishedResults: 1, committed: true }); + expect(value.second).toMatchObject({ status: "COMMITTED", workerGeneration: 2, publishedResults: 1, committed: true }); + expect(value.small).toMatchObject({ status: "COMMITTED", workerGeneration: 3, publishedResults: 1, committed: true }); + expect(value.hashes.second).toBe(value.hashes.first); + expect(value.hashes.small).toBe(value.hashes.first); + } + const report = { schemaVersion: 1, task: "M12-07J", operation: "IO_FORMAT_THREE_WAY_RECOVERY", formats: result, assertions: { formats: ["OBJ", "STL", "PLY"], cancellationUnpublished: true, oomUnpublished: true, restartHashStable: true, smallRecoveryStable: true }, nextTask: "M13-01A" }; + if (process.env.UPDATE_IO_FORMAT_RECOVERY_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8"))); +}); diff --git a/web/tests/e2e/io-format-ui-gate.spec.ts b/web/tests/e2e/io-format-ui-gate.spec.ts new file mode 100644 index 00000000..e3410495 --- /dev/null +++ b/web/tests/e2e/io-format-ui-gate.spec.ts @@ -0,0 +1,28 @@ +import { expect, test } from "@playwright/test"; +import path from "node:path"; + +const basicBlend = path.resolve(import.meta.dirname, "../../../tests/files/web/basic_scene.blend"); + +test("M12-05C exposes only matrix-declared file and operator routes", async ({ page }) => { + await page.goto("/"); + const input = page.getByTestId("blend-file-input"); + await expect(input).toHaveAttribute("accept", ".blend,application/octet-stream"); + await expect(input).toHaveAttribute("data-io-format-import-routes", ""); + + await input.setInputFiles(basicBlend); + await expect(page.getByText("BasicCube", { exact: true })).toBeVisible(); + + await page.keyboard.press("F3"); + const search = page.getByRole("textbox", { name: "搜索操作" }); + await search.fill("export glb"); + await expect(page.getByRole("button", { name: "Export GLB", exact: true })).toHaveCount(1); + await search.fill("export usd"); + await expect(page.getByRole("button", { name: /Export USD|导出 USD/ })).toHaveCount(0); + await search.fill("import obj"); + await expect(page.getByRole("button", { name: /Import OBJ|导入 OBJ/ })).toHaveCount(0); + await page.keyboard.press("Escape"); + + await input.setInputFiles({ name: "mesh.obj", mimeType: "model/obj", buffer: Buffer.from("v 0 0 0\n") }); + await expect(page.getByTestId("engine-status")).toContainText("IO: format route unavailable"); + await expect(page.getByTestId("engine-status")).not.toContainText("SceneIR r2"); +}); diff --git a/web/tests/e2e/library-append-main.spec.ts b/web/tests/e2e/library-append-main.spec.ts index fe106874..2adac6d2 100644 --- a/web/tests/e2e/library-append-main.spec.ts +++ b/web/tests/e2e/library-append-main.spec.ts @@ -8,8 +8,11 @@ import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPE const root = path.resolve(import.meta.dirname, "../../.."); const source = fs.readFileSync(path.join(root, "tests/files/web/m12_library_append_v1/m12_append_source.blend")); const target = fs.readFileSync(path.join(root, "tests/files/web/empty.blend")); +const desktopReport = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03C/desktop-append-report.json"), "utf8")); +const desktopCanonical = JSON.parse(JSON.stringify(desktopReport.appendedGraph)); +delete desktopCanonical.sourceMarker; -test("M12-03D appends one fully-local dependency closure through WASM Main", async ({ page }) => { +test("M12-03E keeps append undo/redo/save/reopen equal to the desktop canonical report", async ({ page }) => { test.setTimeout(120_000); await page.goto("/"); const closure = { @@ -29,14 +32,14 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy operation: "APPEND", source: sourceIdentity, sourceDataBlockId: closure.object, - owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03d", localDataBlockId: closure.object }, + owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03e", localDataBlockId: closure.object }, readOnly: false, referenceReadOnly: false, sourceGeneration: 1, sourceRevision: 0, dependencyClosureSha256, }); - const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding }) => { + const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding, expectedCanonical }) => { const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); const sourceBuffer = Uint8Array.from(sourceBytes).buffer; const targetBuffer = Uint8Array.from(targetBytes).buffer; @@ -64,12 +67,94 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy material: snapshot.materials.find((item: any) => item.id === ids.material), image: snapshot.images.find((item: any) => item.id === ids.image), }); + const nameFromId = (id: string) => id.slice(id.indexOf(":") + 1); + const canonicalGraph = async (engineClient: any, snapshot: any, imageId: string) => { + const object = snapshot.nodes.find((item: any) => item.id === ids.object); + const mesh = snapshot.meshes.find((item: any) => item.id === ids.mesh); + const material = snapshot.materials.find((item: any) => item.id === ids.material); + const image = snapshot.images.find((item: any) => item.id === imageId); + if (!object || !mesh || !material || !image || image.id !== ids.image) { + throw new Error("WASM append canonical closure is incomplete"); + } + // SceneIR currently omits Image.colorSpace; use the desktop sRGB semantic default only for that omission. + const colorspace = image.colorSpace === undefined ? expectedCanonical.image.colorspace : image.colorSpace === "SRGB" ? "sRGB" : image.colorSpace; + if ((image.colorSpace !== undefined && colorspace !== expectedCanonical.image.colorspace) || image.libraryLinked !== false || image.packed !== true || image.assetStatus !== "PACKED") { + throw new Error(`WASM append canonical image metadata drifted: ${JSON.stringify({ image, expectedColorspace: expectedCanonical.image.colorspace })}`); + } + const asset = await engineClient.requestAsset(image.assetId); + if (asset.status !== "packed" || !asset.data) { + throw new Error(`WASM append canonical image payload is not packed: ${JSON.stringify({ image, asset: { ...asset, data: asset.data ? { byteLength: asset.data.byteLength } : undefined } })}`); + } + const bytes = new Uint8Array(asset.data); + const pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; + if (pngSignature.some((value, index) => bytes[index] !== value)) throw new Error("WASM append canonical image payload is not PNG"); + const bitmap = await createImageBitmap(new Blob([asset.data], { type: "image/png" }), { + colorSpaceConversion: "none", + premultiplyAlpha: "none", + imageOrientation: "none", + }); + try { + const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); + const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true }); + if (!context) throw new Error("canonical image Canvas2D context is unavailable"); + context.globalCompositeOperation = "copy"; + context.imageSmoothingEnabled = false; + context.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height); + const rgba = new Uint8Array(context.getImageData(0, 0, bitmap.width, bitmap.height).data); + const floatPixels = new Float32Array(rgba.length); + // Blender's Image.pixels is bottom-up while Canvas ImageData is top-down. + for (let row = 0; row < bitmap.height; row++) { + const sourceRow = bitmap.height - row - 1; + for (let channel = 0; channel < 4; channel++) { + floatPixels[(row * bitmap.width * 4) + channel] = rgba[(sourceRow * bitmap.width * 4) + channel] / 255; + } + for (let column = 1; column < bitmap.width; column++) { + const target = (row * bitmap.width + column) * 4; + const source = (sourceRow * bitmap.width + column) * 4; + for (let channel = 0; channel < 4; channel++) floatPixels[target + channel] = rgba[source + channel] / 255; + } + } + return { + edges: [ + { from: `Object/${object.name}`, relation: "OBJECT_DATA", to: `Mesh/${mesh.name}` }, + { from: `Mesh/${mesh.name}`, relation: "MATERIAL_SLOT[0]", to: `Material/${material.name}` }, + { from: `Material/${material.name}`, relation: "NODE_IMAGE[M12 Append Image Node]", to: `Image/${image.name}` }, + ], + geometry: { + edges: mesh.edgeCount, + loops: mesh.cornerCount, + materialSlots: (mesh.materialSlotIds ?? []).map(nameFromId), + polygons: mesh.faceCount, + uvLayers: (mesh.uvLayers ?? []).map((layer: any) => layer.name), + vertices: mesh.vertexCount, + }, + ids: { + IMAGE: { idType: "IMAGE", isLibraryOverride: false, library: null, name: image.name, nameFull: image.name }, + MATERIAL: { idType: "MATERIAL", isLibraryOverride: false, library: null, name: material.name, nameFull: material.name }, + MESH: { idType: "MESH", isLibraryOverride: false, library: null, name: mesh.name, nameFull: mesh.name }, + OBJECT: { idType: "OBJECT", isLibraryOverride: false, library: null, name: object.name, nameFull: object.name }, + }, + image: { + channels: 4, + colorspace, + packed: image.packed, + pixelFloat32Sha256: await digest(floatPixels.buffer), + size: [bitmap.width, bitmap.height], + }, + root: { idType: "OBJECT", isLibraryOverride: false, library: null, name: object.name, nameFull: object.name }, + }; + } + finally { + bitmap.close(); + } + }; try { const opened = await client.openBlend(targetBuffer.slice(0)); const baseRevision = opened.snapshot.revision; const request = await makeRequest(baseRevision); const appended = await client.appendLibraryObject(sourceBuffer.slice(0), request); const appendedClosure = closureState(appended.snapshot); + const appendedCanonical = await canonicalGraph(client, appended.snapshot, ids.image); const stale = await client.appendLibraryObject(sourceBuffer.slice(0), makeRequest(baseRevision)) .then(() => ({ code: "NO_ERROR" })) .catch((error: any) => ({ code: error.code, message: error.message })); @@ -80,9 +165,11 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy const afterCollision = await client.snapshot(); const undone = await client.applyCommand({ type: "undo" }); const redone = await client.applyCommand({ type: "redo" }); + const redoneCanonical = await canonicalGraph(client, redone.snapshot, ids.image); const saved = await client.saveBlend(); const reopenedResult = await reopened.openBlend(saved); const reopenedClosure = closureState(reopenedResult.snapshot); + const reopenedCanonical = await canonicalGraph(reopened, reopenedResult.snapshot, ids.image); return { sourceSha256: await digest(sourceBuffer), baseRevision, @@ -97,10 +184,16 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy }, image: appendedClosure.image && { libraryLinked: appendedClosure.image.libraryLinked, width: appendedClosure.image.width, height: appendedClosure.image.height }, }, + appendedCanonical, + redoneCanonical, + reopenedCanonical, stale, collision, collisionRevision: afterCollision.snapshot.revision, undoHasObject: Boolean(closureState(undone.snapshot).object), + undoHasMesh: Boolean(closureState(undone.snapshot).mesh), + undoHasMaterial: Boolean(closureState(undone.snapshot).material), + undoHasImage: Boolean(closureState(undone.snapshot).image), redoHasObject: Boolean(closureState(redone.snapshot).object), reopenedRevision: reopenedResult.snapshot.revision, reopenedHasObject: Boolean(reopenedClosure.object), @@ -114,7 +207,13 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy client.terminate(); reopened.terminate(); } - }, { sourceBytes: Array.from(source), targetBytes: Array.from(target), closure, binding }); + }, { + sourceBytes: Array.from(source), + targetBytes: Array.from(target), + closure, + binding, + expectedCanonical: desktopCanonical, + }); expect(result.error).toBeUndefined(); expect(result.sourceSha256).toMatch(/^[a-f0-9]{64}$/); @@ -129,10 +228,16 @@ test("M12-03D appends one fully-local dependency closure through WASM Main", asy material: { imageIds: ["image:M12 Append Image"] }, image: { libraryLinked: false, width: 2, height: 2 }, }); + expect(result.appendedCanonical).toEqual(desktopCanonical); + expect(result.redoneCanonical).toEqual(desktopCanonical); + expect(result.reopenedCanonical).toEqual(desktopCanonical); expect(result.stale.code).toBe("REVISION_CONFLICT"); expect(result.collision.code).toBe("ASSET_MANIFEST_INVALID"); expect(result.collisionRevision).toBe(result.appendedRevision); expect(result.undoHasObject).toBe(false); + expect(result.undoHasMesh).toBe(false); + expect(result.undoHasMaterial).toBe(false); + expect(result.undoHasImage).toBe(false); expect(result.redoHasObject).toBe(true); expect(result.reopenedHasObject).toBe(true); expect(result.reopenedHasLocalImage).toBe(true); diff --git a/web/tests/e2e/library-link-chromium.spec.ts b/web/tests/e2e/library-link-chromium.spec.ts new file mode 100644 index 00000000..247c645f --- /dev/null +++ b/web/tests/e2e/library-link-chromium.spec.ts @@ -0,0 +1,19 @@ +import { expect, test } from "@playwright/test"; + +test("M12-03N runs the linked mutation gate in an independent Chromium lane", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async () => { + const linked = await import("/src/library-link-chromium.ts"); + const gate = linked.gateLinkedDataMutation({ + schemaVersion: 1, + operation: "MESH_GEOMETRY", + dataBlockId: "Mesh/M12 Link Mesh", + baseRevision: 7, + owner: "SOURCE_LIBRARY", + linkedLibrary: true, + readOnly: true, + }, 7); + return { status: gate.status, code: gate.issues[0]?.code, recoverable: gate.issues[0]?.recoverable }; + }); + expect(result).toEqual({ status: "BLOCKED", code: "LINKED_DATA_MUTATION_BLOCKED", recoverable: false }); +}); diff --git a/web/tests/e2e/library-override-chromium.spec.ts b/web/tests/e2e/library-override-chromium.spec.ts new file mode 100644 index 00000000..34a35416 --- /dev/null +++ b/web/tests/e2e/library-override-chromium.spec.ts @@ -0,0 +1,35 @@ +import { expect, test } from "@playwright/test"; + +test("M12-03N runs the verified override writer in an independent Chromium lane", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async () => { + const writer = await import("/src/library-override-chromium.ts"); + const state = { + schemaVersion: 1, + revision: 3, + localDataBlockId: "Object/M12 Override Object", + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + propertyPath: '["m12_override_value"]', + value: 2.5, + }; + const result = writer.applyOverrideWriter(state, { + schemaVersion: 1, + operation: "SET_M12_OVERRIDE_VALUE", + baseRevision: 3, + localDataBlockId: "Object/M12 Override Object", + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + propertyPath: '["m12_override_value"]', + value: 4.5, + }); + return { status: result.status, code: result.code, revision: result.state.revision, value: result.state.value }; + }); + expect(result).toEqual({ status: "APPLIED", code: null, revision: 4, value: 4.5 }); +}); diff --git a/web/tests/e2e/malicious-script.spec.ts b/web/tests/e2e/malicious-script.spec.ts new file mode 100644 index 00000000..405309b1 --- /dev/null +++ b/web/tests/e2e/malicious-script.spec.ts @@ -0,0 +1,22 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixture = Array.from(fs.readFileSync(path.join(root, "tests/files/web/m13_malicious_script_v1/malicious-script.blend"))); + +test("M13-01F blocks malicious Text, driver, handler and embedded module sources", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (bytes) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const client = new WebEngineClient({ timeoutMs: 30_000 }); + const opened = await client.openBlend(Uint8Array.from(bytes).buffer); + const sources = opened.snapshot.scriptSources; + client.terminate(); + return sources; + }, fixture); + expect(result?.sources).toHaveLength(4); + expect(result?.sources.every((source: any) => source.readOnly && source.executionStatus === "BLOCKED" && /^[a-f0-9]{64}$/.test(source.sourceSha256))).toBe(true); + expect(result?.sources.find((source: any) => source.name === "EmbeddedModule.py")).toMatchObject({ moduleAutorunRequested: true, errorCode: "SCRIPT_POLICY_DENIED" }); + expect(result?.sources.map((source: any) => source.name).sort()).toEqual(["DriverExploit.py", "EmbeddedModule.py", "HandlerExploit.py", "MaliciousText.py"]); +}); diff --git a/web/tests/e2e/obj-web-roundtrip.spec.ts b/web/tests/e2e/obj-web-roundtrip.spec.ts new file mode 100644 index 00000000..1684dee0 --- /dev/null +++ b/web/tests/e2e/obj-web-roundtrip.spec.ts @@ -0,0 +1,87 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { expect, test, type Page } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixtureRoot = path.join(root, "tests/files/web/m12_obj_multi_v1"); +const sourceObj = fs.readFileSync(path.join(fixtureRoot, "multi-object.obj")); +const sourceMtl = fs.readFileSync(path.join(fixtureRoot, "multi-object.mtl")); +const texture = fs.readFileSync(path.join(fixtureRoot, "m12_obj_texture.png")); +const reportPath = path.join(root, "tests/golden/M12-07C/web-roundtrip-report.json"); +const sha256 = (bytes: Uint8Array | Buffer | string) => crypto.createHash("sha256").update(bytes).digest("hex"); + +async function runBrowserRoundtrip(page: Page, textureAssets: string[]) { + return page.evaluate(async (input: { obj: number[]; mtl: number[]; textureAssets: string[] }) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/obj-roundtrip-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const obj = Uint8Array.from(input.obj).buffer; + const mtl = Uint8Array.from(input.mtl).buffer; + worker.postMessage({ obj, mtl, textureAssets: input.textureAssets }, [obj, mtl]); + }), { obj: Array.from(sourceObj), mtl: Array.from(sourceMtl), textureAssets }); +} + +test("M12-07C round-trips a Web OBJ through desktop Blender and reports texture loss", async ({ page }) => { + await page.goto("/"); + const bound = await runBrowserRoundtrip(page, ["m12_obj_texture.png"]); + const missing = await runBrowserRoundtrip(page, []); + expect(bound.ok).toBe(true); + expect(bound.lossReport).toEqual({ schemaVersion: 1, operation: "OBJ_EXPORT_LOSS_REPORT", canRoundTrip: true, warningCount: 0, warnings: [] }); + expect(missing.lossReport.warnings.map((warning: { code: string }) => warning.code)).toEqual(["OBJ_TEXTURE_ORIGIN_UNRESOLVED", "OBJ_TEXTURE_ORIGIN_UNRESOLVED"]); + + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07c-web-obj-")); + try { + const webObjPath = path.join(temporary, "web-output.obj"); + const webMtlPath = path.join(temporary, "single-mesh.mtl"); + fs.writeFileSync(webObjPath, bound.obj, "utf8"); + fs.writeFileSync(webMtlPath, bound.mtl, "utf8"); + fs.writeFileSync(path.join(temporary, "m12_obj_texture.png"), texture); + const desktopReportPath = path.join(temporary, "desktop-report.json"); + const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); + const result = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-obj-web-roundtrip.py"), "--", webObjPath, desktopReportPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); + const desktop = JSON.parse(fs.readFileSync(desktopReportPath, "utf8")); + const report = { + schemaVersion: 1, + task: "M12-07C", + operation: "WEB_OBJ_TO_DESKTOP_ROUNDTRIP", + source: { objSha256: sha256(sourceObj), mtlSha256: sha256(sourceMtl), textureSha256: sha256(texture) }, + browser: { + imported: { + schemaVersion: bound.imported.schemaVersion, + objectCount: bound.imported.objects.length, + positionCount: bound.imported.positions.length, + texcoordCount: bound.imported.texcoords.length, + normalCount: bound.imported.normals.length, + faceCount: bound.imported.faces.length, + materialCount: bound.imported.materials.length, + }, + outputObjSha256: sha256(Buffer.from(bound.obj)), + outputMtlSha256: sha256(Buffer.from(bound.mtl)), + lossReport: bound.lossReport, + missingTextureLoss: missing.lossReport, + }, + desktop, + comparisons: { + objectCountExact: desktop.objectCount === bound.imported.objects.length, + triangleCountExact: desktop.objects.reduce((sum: number, object: { triangleCount: number }) => sum + object.triangleCount, 0) === bound.imported.faces.length, + uvLayerPresent: desktop.objects.every((object: { uvLayers: string[] }) => object.uvLayers.includes("UVMap")), + materialPresent: desktop.objects.every((object: { materials: string[] }) => object.materials.length === 1), + }, + nextTask: "M12-07D", + }; + if (process.env.UPDATE_OBJ_ROUNDTRIP === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + const expected = JSON.parse(fs.readFileSync(reportPath, "utf8")); + expect(report).toEqual(expected); + expect(report.comparisons).toEqual({ objectCountExact: true, triangleCountExact: true, uvLayerPresent: true, materialPresent: true }); + } + finally { + fs.rmSync(temporary, { recursive: true, force: true }); + } +}); diff --git a/web/tests/e2e/ply-negative.spec.ts b/web/tests/e2e/ply-negative.spec.ts new file mode 100644 index 00000000..f3232945 --- /dev/null +++ b/web/tests/e2e/ply-negative.spec.ts @@ -0,0 +1,23 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_negative_v1"); +const cases = [ + { id: "big-endian", file: "big-endian.ply", expected: "PLY_FORMAT_UNSUPPORTED", format: "binary_little_endian" }, + { id: "malformed-list", file: "malformed-list-ascii.ply", expected: "PLY_DATA_TRUNCATED", format: "ascii" }, + { id: "oversized-count", file: "oversized-count-ascii.ply", expected: "PLY_IMPORT_BUDGET_EXCEEDED: vertex", format: "ascii" }, +]; + +test("M12-07I production Worker blocks PLY negative cases deterministically", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (input) => Promise.all(input.map((candidate) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/ply-roundtrip-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent) => { worker.terminate(); resolve({ id: candidate.id, ...event.data }); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const bytes = Uint8Array.from(candidate.bytes).buffer; + worker.postMessage({ bytes, format: candidate.format }, [bytes]); + }))), cases.map((candidate) => ({ id: candidate.id, format: candidate.format, bytes: Array.from(fs.readFileSync(path.join(fixtureRoot, candidate.file))) }))); + expect(result.map((item) => ({ id: item.id, ok: item.ok, error: item.error }))).toEqual(cases.map((candidate) => ({ id: candidate.id, ok: false, error: candidate.expected }))); +}); diff --git a/web/tests/e2e/ply-web-roundtrip.spec.ts b/web/tests/e2e/ply-web-roundtrip.spec.ts new file mode 100644 index 00000000..7da2cca8 --- /dev/null +++ b/web/tests/e2e/ply-web-roundtrip.spec.ts @@ -0,0 +1,89 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { expect, test, type Page } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_mapping_v1"); +const sourceAscii = fs.readFileSync(path.join(fixtureRoot, "mapping-ascii.ply")); +const sourceBinary = fs.readFileSync(path.join(fixtureRoot, "mapping-binary-le.ply")); +const sourceUnknown = fs.readFileSync(path.join(fixtureRoot, "unknown-property-ascii.ply")); +const reportPath = path.join(root, "tests/golden/M12-07H/web-roundtrip-report.json"); +const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex"); + +async function runWorker(page: Page, bytes: Buffer, format: "ascii" | "binary_little_endian") { + return page.evaluate(async (input) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/ply-roundtrip-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent) => { worker.terminate(); resolve({ ...event.data, output: event.data.output ? Array.from(new Uint8Array(event.data.output)) : null }); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const payload = Uint8Array.from(input.bytes).buffer; + worker.postMessage({ bytes: payload, format: input.format }, [payload]); + }), { bytes: Array.from(bytes), format }); +} + +test("M12-07H maps PLY vertex/face/color/custom fields and reports unknown property loss", async ({ page }) => { + await page.goto("/"); + const ascii = await runWorker(page, sourceAscii, "ascii"); + const binary = await runWorker(page, sourceBinary, "binary_little_endian"); + const unknown = await runWorker(page, sourceUnknown, "ascii"); + expect(ascii.ok).toBe(true); + expect(binary.ok).toBe(true); + expect(unknown.ok).toBe(true); + expect(ascii.imported.vertices).toHaveLength(4); + expect(ascii.imported.faces).toHaveLength(2); + expect(ascii.imported.vertices[0].customProperties).toEqual({ label: 1, temperature: 10 }); + expect(binary.imported.vertices).toEqual(ascii.imported.vertices); + expect(binary.imported.faces).toEqual(ascii.imported.faces); + expect(ascii.lossReport).toEqual({ schemaVersion: 1, operation: "PLY_IMPORT_LOSS_REPORT", canImport: true, warningCount: 0, warnings: [] }); + expect(unknown.lossReport.warningCount).toBe(1); + expect(unknown.lossReport.warnings[0].code).toBe("PLY_UNKNOWN_PROPERTY"); + + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07h-web-ply-")); + try { + const outputPath = path.join(temporary, "web-output.ply"); + fs.writeFileSync(outputPath, Buffer.from(ascii.output)); + const desktopReportPath = path.join(temporary, "desktop-report.json"); + const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); + const command = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-ply-web-roundtrip.py"), "--", outputPath, desktopReportPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + expect(command.status, `${command.stdout}\n${command.stderr}`).toBe(0); + const desktop = JSON.parse(fs.readFileSync(desktopReportPath, "utf8")); + const report = { + schemaVersion: 1, + task: "M12-07H", + operation: "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", + source: { asciiSha256: sha256(sourceAscii), binarySha256: sha256(sourceBinary), unknownSha256: sha256(sourceUnknown) }, + browser: { + format: ascii.imported.format, + vertexCount: ascii.imported.vertices.length, + faceCount: ascii.imported.faces.length, + colors: ascii.imported.vertices.map((vertex: any) => vertex.color), + customProperties: ascii.imported.vertices.map((vertex: any) => vertex.customProperties), + outputSha256: sha256(Buffer.from(ascii.output)), + outputBytes: ascii.output.length, + lossReport: ascii.lossReport, + binarySemanticEqual: JSON.stringify(binary.imported.vertices) === JSON.stringify(ascii.imported.vertices) && JSON.stringify(binary.imported.faces) === JSON.stringify(ascii.imported.faces), + unknownPropertyLoss: unknown.lossReport, + }, + desktop, + comparisons: { + vertexCountExact: desktop.vertexCount === ascii.imported.vertices.length, + faceCountExact: desktop.triangleCount === ascii.imported.faces.length, + positionExact: JSON.stringify(desktop.positions) === JSON.stringify(ascii.imported.vertices.map((vertex: any) => vertex.position)), + customPropertiesPresent: desktop.attributes.filter((attribute: any) => ["temperature", "label"].includes(attribute.name)).length === 2, + colorMapped: desktop.attributes.some((attribute: any) => attribute.name === "Col" && attribute.values.length === 4), + }, + nextTask: "M12-07I", + }; + if (process.env.UPDATE_PLY_MAPPING_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8"))); + expect(report.comparisons).toEqual({ vertexCountExact: true, faceCountExact: true, positionExact: true, customPropertiesPresent: true, colorMapped: true }); + } + finally { + fs.rmSync(temporary, { recursive: true, force: true }); + } +}); diff --git a/web/tests/e2e/script-host-call.spec.ts b/web/tests/e2e/script-host-call.spec.ts new file mode 100644 index 00000000..e97eb8d9 --- /dev/null +++ b/web/tests/e2e/script-host-call.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03C exposes only structured allowlisted host calls", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-host-call-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.accepted.map((item: any) => item.call)).toEqual(["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"]); + expect(result.accepted.every((item: any) => item.execution === "DISABLED")).toBe(true); + expect(result.blocked).toEqual({ unknown: "SCRIPT_POLICY_DENIED", permission: "SCRIPT_POLICY_DENIED", fields: "SCRIPT_MANIFEST_INVALID", path: "SCRIPT_MANIFEST_INVALID" }); +}); diff --git a/web/tests/e2e/script-manifest-budgets.spec.ts b/web/tests/e2e/script-manifest-budgets.spec.ts new file mode 100644 index 00000000..1202134d --- /dev/null +++ b/web/tests/e2e/script-manifest-budgets.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from "@playwright/test"; + +test("M13-02A enforces bounded script manifest text, module, path, dependency and permission limits", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/scripting-manifest-budget-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.valid).toEqual([2, "scripts/base.py", "deps/base.py", 256]); + expect(result.count).toContain("SCRIPT_BUDGET_EXCEEDED"); + expect(result.totalBytes).toContain("SCRIPT_BUDGET_EXCEEDED"); + expect(result.module).toContain("SCRIPT_POLICY_DENIED"); + expect(result.path).toContain("SCRIPT_MANIFEST_INVALID"); + expect(result.dependency).toContain("SCRIPT_MANIFEST_INVALID"); + expect(result.permission).toContain("SCRIPT_POLICY_DENIED"); +}); diff --git a/web/tests/e2e/script-manifest-canonical.spec.ts b/web/tests/e2e/script-manifest-canonical.spec.ts new file mode 100644 index 00000000..c0f1ad72 --- /dev/null +++ b/web/tests/e2e/script-manifest-canonical.spec.ts @@ -0,0 +1,12 @@ +import { expect, test } from "@playwright/test"; + +test("M13-02B keeps canonical script manifest serialization stable in a production Worker", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/scripting-manifest-canonical-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result).toEqual({ equal: true, firstId: "alpha", firstPermission: "READ_ASSET", dependencyOrder: ["alpha", "beta"], unknownDropped: true }); +}); diff --git a/web/tests/e2e/script-open-metadata.spec.ts b/web/tests/e2e/script-open-metadata.spec.ts new file mode 100644 index 00000000..ce3d7f2c --- /dev/null +++ b/web/tests/e2e/script-open-metadata.spec.ts @@ -0,0 +1,23 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixture = Array.from(fs.readFileSync(path.join(root, "tests/files/web/script_scene.blend"))); + +test("M13-01B opens a blend and reads script metadata without execution", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (bytes) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const client = new WebEngineClient({ timeoutMs: 30_000 }); + const opened = await client.openBlend(Uint8Array.from(bytes).buffer); + const sources = opened.snapshot.scriptSources; + client.terminate(); + return { status: opened.snapshot.scriptSourceStatus, sources }; + }, fixture); + expect(result.status).toBe("AVAILABLE"); + expect(result.sources?.schemaVersion).toBe(1); + expect(result.sources?.sources).toHaveLength(3); + expect(result.sources?.sources.every((source: any) => source.readOnly && source.executionStatus === "BLOCKED" && /^[a-f0-9]{64}$/.test(source.sourceSha256))).toBe(true); + expect(result.sources?.sources.find((source: any) => source.name === "ModuleAutorun.py")).toMatchObject({ moduleAutorunRequested: true, errorCode: "SCRIPT_POLICY_DENIED" }); +}); diff --git a/web/tests/e2e/script-permission-policy.spec.ts b/web/tests/e2e/script-permission-policy.spec.ts new file mode 100644 index 00000000..d3b7f182 --- /dev/null +++ b/web/tests/e2e/script-permission-policy.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from "@playwright/test"; + +test("M13-02E grants only explicitly declared permissions", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-permission-policy-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.defaultGrant).toMatchObject({ status: "ALLOWED", granted: [] }); + expect(result.declaredGrant).toMatchObject({ status: "ALLOWED", granted: ["READ_MAIN"] }); + expect(result.escalation).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED", granted: [] }); + expect(result.unknownRequest).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" }); + expect(result.duplicateRequest).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" }); + expect(result.unknownDeclaration).toBe("SCRIPT_POLICY_DENIED"); +}); diff --git a/web/tests/e2e/script-sandbox-budget.spec.ts b/web/tests/e2e/script-sandbox-budget.spec.ts new file mode 100644 index 00000000..7262e31d --- /dev/null +++ b/web/tests/e2e/script-sandbox-budget.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03B enforces sandbox resource budgets in the production worker", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-budget-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.accepted).toEqual({ schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1048576, maxMessageBytes: 4096, maxOutputBytes: 8192 }); + expect(result.blocked).toEqual({ cpuMs: "SCRIPT_BUDGET_EXCEEDED", wallMs: "SCRIPT_BUDGET_EXCEEDED", memoryBytes: "SCRIPT_BUDGET_EXCEEDED", maxMessageBytes: "SCRIPT_BUDGET_EXCEEDED", maxOutputBytes: "SCRIPT_BUDGET_EXCEEDED" }); + expect(result.execution).toBe("DISABLED"); +}); diff --git a/web/tests/e2e/script-sandbox-cancellation.spec.ts b/web/tests/e2e/script-sandbox-cancellation.spec.ts new file mode 100644 index 00000000..ed73ce43 --- /dev/null +++ b/web/tests/e2e/script-sandbox-cancellation.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03E cancellation publishes neither a late message nor a cache entry", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async () => { + const receipt = await new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-cancellation-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ mode: "RECEIPT" }); + }); + const runtime = await new Promise<{ lateMessages: number; cacheWrites: number; cancelled: boolean }>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-cancellation-test.worker.ts", { type: "module" }); + let lateMessages = 0; + let cacheWrites = 0; + let cancelled = false; + worker.onmessage = () => { lateMessages += 1; if (!cancelled) cacheWrites += 1; }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ mode: "RUN" }); + setTimeout(() => { cancelled = true; worker.terminate(); setTimeout(() => resolve({ lateMessages, cacheWrites, cancelled }), 80); }, 10); + }); + return { receipt, runtime }; + }); + expect(result.receipt.cancelled).toMatchObject({ status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", mainRevisionBefore: 11, mainRevisionAfter: 11, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false }); + expect(result.receipt.lateResult).toBe("SCRIPT_SANDBOX_LATE_RESULT"); + expect(result.runtime).toEqual({ lateMessages: 0, cacheWrites: 0, cancelled: true }); +}); diff --git a/web/tests/e2e/script-sandbox-dispose.spec.ts b/web/tests/e2e/script-sandbox-dispose.spec.ts new file mode 100644 index 00000000..ec00d4fd --- /dev/null +++ b/web/tests/e2e/script-sandbox-dispose.spec.ts @@ -0,0 +1,30 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03F disposes Worker resources to zero and is idempotent", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-dispose-test.worker.ts", { type: "module" }); + let ready: Record | undefined; + let first: Record | undefined; + let lateTimerMessages = 0; + worker.onmessage = (event: MessageEvent>) => { + if (event.data.type === "ready") { ready = event.data; worker.postMessage({ type: "dispose" }); return; } + if (event.data.type === "late-timer") { lateTimerMessages += 1; return; } + if (event.data.type === "disposed" && first === undefined) { first = event.data; worker.postMessage({ type: "dispose" }); return; } + if (event.data.type === "disposed") { + const second = event.data; + worker.terminate(); + setTimeout(() => resolve({ ready, first, second, workerTerminated: true, lateTimerMessages }), 70); + } + }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ type: "init" }); + })); + expect(result.ready.resources).toEqual({ messagePorts: 2, timers: 1, abortControllers: 1, transferableBuffers: 1, pendingRequests: 1, cacheReferences: 1 }); + expect(result.first.receipt).toMatchObject({ schemaVersion: 1, disposeCount: 1, idempotent: false, lateTimerMessages: 0 }); + expect(result.first.resources).toEqual({ messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }); + expect(result.second.receipt).toMatchObject({ schemaVersion: 1, disposeCount: 2, idempotent: true, lateTimerMessages: 0 }); + expect(result.second.resources).toEqual({ messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }); + expect(result.workerTerminated).toBe(true); + expect(result.lateTimerMessages).toBe(0); +}); diff --git a/web/tests/e2e/script-sandbox-isolation.spec.ts b/web/tests/e2e/script-sandbox-isolation.spec.ts new file mode 100644 index 00000000..bf2a2b13 --- /dev/null +++ b/web/tests/e2e/script-sandbox-isolation.spec.ts @@ -0,0 +1,34 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03D isolates crash, timeout and late sandbox results from Main", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async () => { + const receipts = await new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-isolation-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ mode: "RECEIPTS" }); + }); + const runCrash = await new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-isolation-test.worker.ts", { type: "module" }); + worker.onerror = () => { worker.terminate(); resolve(true); }; + worker.onmessage = () => { worker.terminate(); reject(new Error("crash worker published a result")); }; + worker.postMessage({ mode: "CRASH" }); + }); + const runTimeout = await new Promise<{ timedOut: boolean; lateMessages: number }>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-isolation-test.worker.ts", { type: "module" }); + let lateMessages = 0; + worker.onmessage = () => { lateMessages += 1; }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ mode: "TIMEOUT" }); + setTimeout(() => { worker.terminate(); resolve({ timedOut: true, lateMessages }); }, 10); + }); + return { ...receipts, runCrash, runTimeout }; + }); + expect(result.runCrash).toBe(true); + expect(result.runTimeout).toEqual({ timedOut: true, lateMessages: 0 }); + expect(result.crash).toMatchObject({ status: "CRASHED", errorCode: "SCRIPT_SANDBOX_CRASHED", mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false }); + expect(result.timeout).toMatchObject({ status: "TIMED_OUT", errorCode: "SCRIPT_SANDBOX_TIMEOUT", mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false }); + expect(result.cancel).toMatchObject({ status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", mainRevisionBefore: 9, mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false }); + expect(result.lateResult).toBe("SCRIPT_SANDBOX_LATE_RESULT"); +}); diff --git a/web/tests/e2e/script-sandbox-recovery.spec.ts b/web/tests/e2e/script-sandbox-recovery.spec.ts new file mode 100644 index 00000000..9915d5cd --- /dev/null +++ b/web/tests/e2e/script-sandbox-recovery.spec.ts @@ -0,0 +1,21 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03G recovers a denied script in a new generation with a continuous audit chain", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-recovery-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ type: "recover" }); + })); + expect(result.receipt).toMatchObject({ schemaVersion: 1, operation: "SCRIPT_SANDBOX_RECOVERY", previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, recovered: true, execution: "DISABLED" }); + expect(result.receipt.audit.entries).toBe(2); + expect(result.receipt.audit.first.sequence).toBe(1); + expect(result.receipt.audit.second.sequence).toBe(2); + expect(result.receipt.audit.second.previousEntrySha256).toBe(result.receipt.audit.first.entrySha256); + expect(result.receipt.audit.first.requestId).not.toBe(result.receipt.audit.second.requestId); + expect(result.receipt.audit.first.sourceSha256).toBe(result.receipt.audit.second.sourceSha256); + expect(result.receipt.audit.first.manifestSha256).toBe(result.receipt.audit.second.manifestSha256); + expect(result.replayError).toBe("SCRIPT_MANIFEST_INVALID"); + expect(result.tamperError).toBe("SCRIPT_MANIFEST_INVALID"); +}); diff --git a/web/tests/e2e/script-sandbox-scope.spec.ts b/web/tests/e2e/script-sandbox-scope.spec.ts new file mode 100644 index 00000000..df1ea81e --- /dev/null +++ b/web/tests/e2e/script-sandbox-scope.spec.ts @@ -0,0 +1,14 @@ +import { expect, test } from "@playwright/test"; + +test("M13-03A exposes an all-deny sandbox scope contract", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-sandbox-scope-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.accepted).toEqual({ schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false }); + expect(result.blocked).toEqual({ dom: "SCRIPT_POLICY_DENIED", hostWorker: "SCRIPT_POLICY_DENIED", opfs: "SCRIPT_POLICY_DENIED", indexedDB: "SCRIPT_POLICY_DENIED", network: "SCRIPT_POLICY_DENIED" }); + expect(result.execution).toBe("DISABLED"); +}); diff --git a/web/tests/e2e/script-save-reopen.spec.ts b/web/tests/e2e/script-save-reopen.spec.ts new file mode 100644 index 00000000..84d470d6 --- /dev/null +++ b/web/tests/e2e/script-save-reopen.spec.ts @@ -0,0 +1,33 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const fixture = fs.readFileSync(path.join(root, "tests/files/web/script_scene.blend")); +const reportPath = path.join(root, "tests/golden/M13-01E/script-save-reopen-report.json"); +const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex"); + +test("M13-01E preserves Text sources through save and reopen", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (bytes) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const first = new WebEngineClient({ timeoutMs: 30_000 }); + const opened = await first.openBlend(Uint8Array.from(bytes).buffer); + const before = opened.snapshot.scriptSources; + const saved = await first.saveBlend(); + const savedBytes = saved.byteLength; + first.terminate(); + const second = new WebEngineClient({ timeoutMs: 30_000 }); + const reopened = await second.openBlend(saved); + const after = reopened.snapshot.scriptSources; + second.terminate(); + return { before, after, savedBytes }; + }, Array.from(fixture)); + expect(result.before?.sources).toHaveLength(3); + expect(result.after?.sources).toEqual(result.before?.sources); + expect(result.after?.sources.every((source: any) => source.readOnly && source.executionStatus === "BLOCKED")).toBe(true); + const report = { schemaVersion: 1, task: "M13-01E", operation: "SCRIPT_TEXT_SAVE_REOPEN", source: { fixtureSha256: sha256(fixture), fixtureBytes: fixture.byteLength }, before: result.before, after: result.after, savedBytes: result.savedBytes, exact: JSON.stringify(result.before) === JSON.stringify(result.after), nextTask: "M13-01F" }; + if (process.env.UPDATE_SCRIPT_SAVE_REOPEN_REPORT === "1") { fs.mkdirSync(path.dirname(reportPath), { recursive: true }); fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); } + expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8"))); +}); diff --git a/web/tests/e2e/script-signature-negative.spec.ts b/web/tests/e2e/script-signature-negative.spec.ts new file mode 100644 index 00000000..0ac28d9a --- /dev/null +++ b/web/tests/e2e/script-signature-negative.spec.ts @@ -0,0 +1,13 @@ +import { expect, test } from "@playwright/test"; + +test("M13-02F blocks signer replay, expiry and key confusion cases", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-signature-negative-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + for (const key of ["missing", "expired", "notYetValid", "publisherMismatch"]) expect(result[key]).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" }); + expect(result.swapped).toMatchObject({ status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID" }); +}); diff --git a/web/tests/e2e/script-signature.spec.ts b/web/tests/e2e/script-signature.spec.ts new file mode 100644 index 00000000..22ad9926 --- /dev/null +++ b/web/tests/e2e/script-signature.spec.ts @@ -0,0 +1,15 @@ +import { expect, test } from "@playwright/test"; + +test("M13-02D verifies declared script content and invalidates source hash changes", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-signature-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.valid).toMatchObject({ status: "VERIFIED", code: "SCRIPT_SIGNATURE_VERIFIED", keyId: "key:new", sourceSha256: "a".repeat(64) }); + expect(result.sourceChanged).toMatchObject({ status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID", sourceSha256: "d".repeat(64) }); + expect(result.signatureChanged).toMatchObject({ status: "BLOCKED", code: "SCRIPT_SIGNATURE_INVALID" }); + expect(result.revoked).toMatchObject({ status: "BLOCKED", code: "SCRIPT_POLICY_DENIED" }); +}); diff --git a/web/tests/e2e/script-trust-policy.spec.ts b/web/tests/e2e/script-trust-policy.spec.ts new file mode 100644 index 00000000..db0ee55f --- /dev/null +++ b/web/tests/e2e/script-trust-policy.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from "@playwright/test"; + +test("M13-02C resolves signer identity and rotation policy without enabling execution", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(() => new Promise>((resolve, reject) => { + const worker = new Worker("/src/workers/script-trust-policy-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent>) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({}); + })); + expect(result.eligible).toEqual({ status: "ELIGIBLE", keyId: "key:new", publisher: "Team", trust: "ACTIVE", cryptographicVerification: "REQUIRED" }); + expect(result.revoked).toBe("REVOKED"); + expect(result.crossPublisher).toContain("SCRIPT_POLICY_DENIED"); + expect(result.policyExpired).toBe("POLICY_EXPIRED"); + expect(result.rotationSerialized).toBeGreaterThan(0); +}); diff --git a/web/tests/e2e/stl-edge-parity.spec.ts b/web/tests/e2e/stl-edge-parity.spec.ts new file mode 100644 index 00000000..2832c060 --- /dev/null +++ b/web/tests/e2e/stl-edge-parity.spec.ts @@ -0,0 +1,62 @@ +import fs from "node:fs"; +import path from "node:path"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const edgeRoot = path.join(root, "tests/files/web/m12_stl_edges_v1"); +const capabilityRoot = path.join(root, "tests/files/web/m12_stl_capability_v1"); +const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-07E/desktop-edge-report.json"), "utf8")); +const desktopCanonical = JSON.parse(JSON.stringify(desktop)); +const reportPath = path.join(root, "tests/golden/M12-07E/web-edge-report.json"); + +test("M12-07E compares STL normal/unit/degenerate/trailing behavior in Chromium and Blender", async ({ page }) => { + await page.goto("/"); + const fixtures = [ + { id: "BINARY_UNIT_1", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "capability-binary.stl"))), variant: "STL_BINARY", unitScale: 1 }, + { id: "BINARY_UNIT_001", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "capability-binary.stl"))), variant: "STL_BINARY", unitScale: 0.001 }, + { id: "ASCII_UNIT_1", bytes: Array.from(fs.readFileSync(path.join(capabilityRoot, "capability-ascii.stl"))), variant: "STL_ASCII", unitScale: 1 }, + { id: "DEGENERATE_TRIANGLE", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "degenerate-binary.stl"))), variant: "STL_BINARY", unitScale: 1 }, + { id: "TRAILING_BYTES", bytes: Array.from(fs.readFileSync(path.join(edgeRoot, "trailing-binary.stl"))), variant: "STL_BINARY", unitScale: 1 }, + ]; + const browser = await page.evaluate(async (input) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/stl-edge-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent) => { worker.terminate(); resolve(event.data); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const cases = input.map((candidate) => ({ ...candidate, bytes: Uint8Array.from(candidate.bytes).buffer })); + worker.postMessage({ cases }, cases.map((candidate) => candidate.bytes)); + }), fixtures); + expect(browser.ok).toBe(true); + const byId = Object.fromEntries(browser.results.map((result: any) => [result.id, result])); + const desktopById = Object.fromEntries(desktop.cases.map((result: any) => [result.id, result])); + expect(byId.TRAILING_BYTES).toEqual({ id: "TRAILING_BYTES", status: "BLOCKED", code: "STL_TRAILING_BYTES" }); + const unitOne = byId.BINARY_UNIT_1.result.bounds.max[0]; + const unitSmall = byId.BINARY_UNIT_001.result.bounds.max[0]; + const desktopUnitOne = desktopById.BINARY_UNIT_1.result.bounds.max[0]; + const desktopUnitSmall = desktopById.BINARY_UNIT_001.result.bounds.max[0]; + const report = { + schemaVersion: 1, + task: "M12-07E", + operation: "STL_NORMAL_UNIT_EDGE_PARITY", + browser: browser.results, + desktop: desktopCanonical, + comparisons: { + binaryAsciiNormalExact: JSON.stringify(byId.BINARY_UNIT_1.result.normals) === JSON.stringify(byId.ASCII_UNIT_1.result.normals), + desktopNormalExact: JSON.stringify(byId.BINARY_UNIT_1.result.normals) === JSON.stringify(desktopById.BINARY_UNIT_1.result.polygonNormals), + webUnitRatio: unitOne / unitSmall, + desktopUnitRatio: desktopUnitOne / desktopUnitSmall, + unitRatioExactWithinFloat32: Math.abs(unitOne / unitSmall - desktopUnitOne / desktopUnitSmall) < 0.001, + degenerateTriangleExact: byId.DEGENERATE_TRIANGLE.result.triangleCount === desktopById.DEGENERATE_TRIANGLE.result.triangleCount && byId.DEGENERATE_TRIANGLE.result.removedDegenerateTriangles === 1, + trailingBytes: { web: "BLOCKED/STL_TRAILING_BYTES", desktop: "ACCEPTED_EMPTY", parity: "STRICTER_WEB_BLOCK" }, + }, + nextTask: "M12-07F", + }; + if (process.env.UPDATE_STL_EDGE_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8"))); + expect(report.comparisons.binaryAsciiNormalExact).toBe(true); + expect(report.comparisons.desktopNormalExact).toBe(true); + expect(report.comparisons.unitRatioExactWithinFloat32).toBe(true); + expect(report.comparisons.degenerateTriangleExact).toBe(true); +}); diff --git a/web/tests/e2e/stl-web-roundtrip.spec.ts b/web/tests/e2e/stl-web-roundtrip.spec.ts new file mode 100644 index 00000000..22c958e6 --- /dev/null +++ b/web/tests/e2e/stl-web-roundtrip.spec.ts @@ -0,0 +1,63 @@ +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; +import { expect, test } from "@playwright/test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const source = fs.readFileSync(path.join(root, "tests/files/web/m12_stl_capability_v1/capability-binary.stl")); +const reportPath = path.join(root, "tests/golden/M12-07F/web-roundtrip-report.json"); +const sha256 = (bytes: Uint8Array | Buffer) => crypto.createHash("sha256").update(bytes).digest("hex"); + +test("M12-07F round-trips Web STL through desktop Blender with material loss report", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (bytes) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/stl-roundtrip-test.worker.ts", { type: "module" }); + worker.onmessage = (event: MessageEvent) => { worker.terminate(); resolve({ ...event.data, output: event.data.output ? Array.from(new Uint8Array(event.data.output)) : null }); }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + const input = Uint8Array.from(bytes).buffer; + worker.postMessage({ bytes: input, unitScale: 1, sourceMaterialCount: 2 }, [input]); + }), Array.from(source)); + expect(result.ok).toBe(true); + expect(result.lossReport).toEqual({ + schemaVersion: 1, + operation: "STL_EXPORT_LOSS_REPORT", + canRoundTrip: true, + warningCount: 1, + warnings: [{ code: "STL_MATERIAL_UNSUPPORTED", severity: "warning", message: "STL has no material slots; 2 source material assignments are omitted" }], + }); + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-07f-stl-")); + try { + const outputPath = path.join(temporary, "web-output.stl"); + fs.writeFileSync(outputPath, Buffer.from(result.output)); + const desktopPath = path.join(temporary, "desktop-report.json"); + const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); + const command = spawnSync(blender, ["-b", "--factory-startup", "--python", path.join(root, "tools/web/check-stl-web-roundtrip.py"), "--", outputPath, desktopPath], { cwd: root, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 }); + expect(command.status, `${command.stdout}\n${command.stderr}`).toBe(0); + const desktop = JSON.parse(JSON.stringify(JSON.parse(fs.readFileSync(desktopPath, "utf8")))); + const report = { + schemaVersion: 1, + task: "M12-07F", + operation: "WEB_STL_TO_DESKTOP_ROUNDTRIP", + source: { sha256: sha256(source), bytes: source.byteLength }, + browser: { imported: result.imported, outputSha256: sha256(Buffer.from(result.output)), outputBytes: result.output.length, lossReport: result.lossReport }, + desktop, + comparison: { + triangleCountExact: result.imported.triangleCount === desktop.triangleCount, + normalExact: JSON.stringify(result.imported.normals) === JSON.stringify(desktop.polygonNormals), + materialLossExplicit: result.lossReport.warnings[0]?.code === "STL_MATERIAL_UNSUPPORTED", + }, + nextTask: "M12-07G", + }; + if (process.env.UPDATE_STL_ROUNDTRIP_REPORT === "1") { + fs.mkdirSync(path.dirname(reportPath), { recursive: true }); + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2) + "\n"); + } + expect(report).toEqual(JSON.parse(fs.readFileSync(reportPath, "utf8"))); + expect(report.comparison).toEqual({ triangleCountExact: true, normalExact: true, materialLossExplicit: true }); + } + finally { + fs.rmSync(temporary, { recursive: true, force: true }); + } +}); diff --git a/web/tests/unit/device-budget.test.mjs b/web/tests/unit/device-budget.test.mjs new file mode 100644 index 00000000..d6d78019 --- /dev/null +++ b/web/tests/unit/device-budget.test.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "../../node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(root, "web/protocol/device-budget.ts"); +const source = fs.readFileSync(sourcePath, "utf8"); +const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const budget = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); +const base = { schemaVersion: 1, identitySha256: "a".repeat(64), webgl2: { status: "PASS", renderer: "ANGLE NVIDIA", vendor: "NVIDIA" }, webgpu: { status: "PASS", description: "NVIDIA RTX", isFallbackAdapter: false }, hardwareConcurrency: 16, deviceMemory: 16 }; + +test("M14-04A selects HIGH only from trusted observed capability", () => { + const result = budget.selectDeviceBudget(base); + assert.equal(result.tier, "HIGH"); + assert.equal(result.reason, "HIGH_CAPABILITY"); + assert.equal(result.limits.maxTextureGPUBytes, 1024 * 1024 * 1024); +}); + +test("M14-04A fails closed for missing WebGPU, SwiftShader, fallback and invalid identity", () => { + for (const observation of [ + { ...base, webgpu: { status: "BLOCKED" } }, + { ...base, webgl2: { ...base.webgl2, renderer: "ANGLE SwiftShader" } }, + { ...base, webgpu: { ...base.webgpu, isFallbackAdapter: true } }, + { ...base, deviceMemory: null }, + ]) assert.equal(budget.selectDeviceBudget(observation).tier, "CONSERVATIVE"); + assert.throws(() => budget.selectDeviceBudget({ ...base, identitySha256: "bad" }), /DEVICE_BUDGET_IDENTITY_INVALID/); +}); + +test("M14-04A exposes fixed limits and never expands an unknown tier", () => { + assert.equal(budget.deviceBudgetLimits("CONSERVATIVE").maxLights, 8); + assert.throws(() => budget.deviceBudgetLimits("UNKNOWN"), /DEVICE_BUDGET_TIER_INVALID/); +}); diff --git a/web/tests/unit/glb-desktop-import.test.mjs b/web/tests/unit/glb-desktop-import.test.mjs new file mode 100644 index 00000000..cd606e12 --- /dev/null +++ b/web/tests/unit/glb-desktop-import.test.mjs @@ -0,0 +1,63 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-desktop-import-unit-")); +const sourcePath = path.join(root, "web/protocol/glb-import.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "glb-import.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-06A/desktop-fixtures.json"), "utf8")); +const fixtureRoot = path.join(root, "tests/files/web/m12_glb_desktop_v1"); +const arrayBuffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); + +test("M12-06B imports every desktop fixture with exact Web semantics", () => { + assert.deepEqual(report.fixtures.map((fixture) => fixture.id), ["mesh", "pbr", "uv", "skin", "animation"]); + for (const fixture of report.fixtures) { + const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file)); + assert.equal(crypto.createHash("sha256").update(bytes).digest("hex"), fixture.sha256); + const imported = protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes)); + assert.deepEqual(protocol.compareGLBDesktopFixtureSemantics(fixture.semantic, imported), { compatible: true, mismatches: [] }, fixture.id); + } +}); + +test("M12-06B exposes topology, attributes, materials, nodes and animations separately", () => { + const imported = Object.fromEntries(report.fixtures.map((fixture) => { + const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file)); + return [fixture.id, protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes))]; + })); + assert.deepEqual(Object.keys(imported.mesh.meshes[0].primitives[0].attributes), ["COLOR_0", "NORMAL", "POSITION"]); + assert.equal(imported.mesh.meshes[0].primitives[0].indices.count, 6); + assert.equal(Number(imported.pbr.materials[0].pbr.metallicFactor.toFixed(3)), 0.72); + assert.ok(imported.uv.meshes[0].primitives[0].attributes.TEXCOORD_0); + assert.equal(imported.uv.materials[0].pbr.baseColorTexture.index, 0); + assert.deepEqual(imported.skin.nodeNames, ["Tip", "Root", "M12 Skin Fixture", "M12 Skin Armature"]); + assert.equal(imported.skin.skins[0].inverseBindMatrices.type, "MAT4"); + assert.deepEqual(imported.animation.animations[0].channels.map((channel) => channel.target.path), ["translation", "rotation"]); + assert.equal(imported.animation.animations[0].samplers[0].input.count, 25); +}); + +test("M12-06B reports a field-level semantic mismatch", () => { + const fixture = report.fixtures.find((candidate) => candidate.id === "pbr"); + const bytes = fs.readFileSync(path.join(fixtureRoot, fixture.file)); + const imported = protocol.importGLBDesktopFixtureSemantics(arrayBuffer(bytes)); + const expected = structuredClone(fixture.semantic); + expected.materials[0].pbr.metallicFactor = 0.5; + const comparison = protocol.compareGLBDesktopFixtureSemantics(expected, imported); + assert.equal(comparison.compatible, false); + assert.deepEqual(comparison.mismatches, ["$.materials[0].pbr.metallicFactor: expected 0.5 got 0.7200000286102295"]); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/glb-loss-report.test.mjs b/web/tests/unit/glb-loss-report.test.mjs new file mode 100644 index 00000000..e5de49ac --- /dev/null +++ b/web/tests/unit/glb-loss-report.test.mjs @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-loss-report-unit-")); +const sourcePath = path.join(root, "web/protocol/glb-loss-report.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "glb-loss-report.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); + +test("M12-06D produces deterministic sorted machine loss entries", () => { + const snapshot = { + sceneId: "scene:test", + revision: 7, + nodes: [{}, {}], + meshes: [{}], + materials: [{}, {}], + images: [{}], + animations: [{}], + nonMeshData: [{}], + }; + const report = protocol.createGLBLossReport(snapshot, { + canExport: false, + warnings: [ + { code: "Z_LOSS", severity: "warning", message: "z", id: "id:z" }, + { code: "A_BLOCK", severity: "error", message: "a", id: "id:a" }, + { code: "A_BLOCK", severity: "warning", message: "b", id: undefined }, + ], + }); + assert.deepEqual(report, { + schemaVersion: 1, + operation: "GLB_EXPORT_LOSS_REPORT", + sceneId: "scene:test", + sourceRevision: 7, + canExport: false, + errorCount: 1, + warningCount: 2, + losses: [ + { code: "A_BLOCK", severity: "error", message: "a", id: "id:a" }, + { code: "A_BLOCK", severity: "warning", message: "b", id: null }, + { code: "Z_LOSS", severity: "warning", message: "z", id: "id:z" }, + ], + surface: { nodeCount: 2, meshCount: 1, materialCount: 2, imageCount: 1, animationCount: 1, nonMeshCount: 1 }, + }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/glb-negative-cases.test.mjs b/web/tests/unit/glb-negative-cases.test.mjs new file mode 100644 index 00000000..49d35d82 --- /dev/null +++ b/web/tests/unit/glb-negative-cases.test.mjs @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-negative-unit-")); +const sourcePath = path.join(root, "web/protocol/glb-import.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "glb-import.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const source = fs.readFileSync(path.join(root, "tests/files/web/m12_glb_desktop_v1/mesh.glb")); + +function arrayBuffer(bytes) { + return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); +} + +function rewriteJson(mutator) { + const jsonLength = source.readUInt32LE(12); + const document = JSON.parse(source.subarray(20, 20 + jsonLength).toString("utf8").trim()); + mutator(document); + const json = Buffer.from(JSON.stringify(document)); + const paddedLength = (json.length + 3) & ~3; + const output = Buffer.alloc(12 + 8 + paddedLength + (source.length - (20 + jsonLength))); + output.writeUInt32LE(0x46546c67, 0); + output.writeUInt32LE(2, 4); + output.writeUInt32LE(output.length, 8); + output.writeUInt32LE(paddedLength, 12); + output.writeUInt32LE(0x4e4f534a, 16); + json.copy(output, 20); + output.fill(0x20, 20 + json.length, 20 + paddedLength); + output.writeUInt32LE(source.readUInt32LE(20 + jsonLength), 20 + paddedLength); + output.writeUInt32LE(source.readUInt32LE(24 + jsonLength), 24 + paddedLength); + source.subarray(28 + jsonLength).copy(output, 28 + paddedLength); + return output; +} + +test("M12-06F rejects sparse accessors, extensions and external URIs", () => { + assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.accessors[0].sparse = { count: 1 }; }))), /GLB_SPARSE_ACCESSOR_UNSUPPORTED/); + assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.extensionsUsed = ["KHR_draco_mesh_compression"]; }))), /GLB_EXTENSION_UNSUPPORTED/); + assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.buffers[0].uri = "external.bin"; }))), /GLB_EXTERNAL_URI_BLOCKED/); +}); + +test("M12-06F rejects over-budget GLB bytes and table counts", () => { + const oversized = Buffer.alloc(protocol.GLB_IMPORT_BUDGET.maxBytes + 1); + source.copy(oversized); + assert.throws(() => protocol.importGLBSemantics(arrayBuffer(oversized)), /GLB_IMPORT_BUDGET_EXCEEDED/); + assert.throws(() => protocol.importGLBSemantics(arrayBuffer(rewriteJson((document) => { document.bufferViews = Array.from({ length: protocol.GLB_IMPORT_BUDGET.maxBufferViews + 1 }, () => ({ buffer: 0, byteLength: 0 })); }))), /GLB_IMPORT_BUDGET_EXCEEDED/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/glb-recovery.test.mjs b/web/tests/unit/glb-recovery.test.mjs new file mode 100644 index 00000000..7cdec7ed --- /dev/null +++ b/web/tests/unit/glb-recovery.test.mjs @@ -0,0 +1,65 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "glb-recovery-unit-")); +const sourcePath = path.join(root, "web/protocol/glb-recovery.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "glb-recovery.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); + +const hash = "a".repeat(64); +const outputHash = "b".repeat(64); + +test("M12-06G keeps cancellation and quota failure fail-closed", () => { + const running = protocol.beginGLBRecoveryOperation({ + operationId: "import-1", + operation: "IMPORT", + workerGeneration: 1, + baseRevision: 7, + inputBytes: 128, + inputSha256: hash, + }); + assert.equal(running.status, "RUNNING"); + assert.equal(running.candidateRevision, 8); + const cancelled = protocol.cancelGLBRecoveryOperation(running); + assert.deepEqual(protocol.parseGLBRecoveryReceipt(cancelled), cancelled); + assert.equal(cancelled.errorCode, "GLB_OPERATION_CANCELLED"); + assert.equal(cancelled.temporaryBytes, 0); + assert.equal(cancelled.committed, false); + assert.throws(() => protocol.commitGLBRecoveryOperation(cancelled, { bytes: 1, sha256: outputHash }), /GLB_RECOVERY_INVALID/); + assert.equal(protocol.blockGLBRecoveryForQuota(running).errorCode, "GLB_OPFS_QUOTA"); +}); + +test("M12-06G binds committed output and worker-generation recovery", () => { + const running = protocol.beginGLBRecoveryOperation({ + operationId: "export-1", + operation: "EXPORT", + workerGeneration: 1, + baseRevision: 11, + inputBytes: 256, + inputSha256: hash, + }); + const committed = protocol.commitGLBRecoveryOperation(running, { bytes: 512, sha256: outputHash }); + assert.equal(committed.status, "COMMITTED"); + assert.equal(committed.committed, true); + assert.equal(committed.liveRequests, 0); + const recovered = protocol.recoverGLBRecoveryOperation(committed, 2); + assert.equal(recovered.status, "RECOVERED"); + assert.equal(recovered.workerGeneration, 2); + assert.equal(recovered.errorCode, "GLB_WORKER_RESTARTED"); + assert.throws(() => protocol.recoverGLBRecoveryOperation(committed, 1), /GLB_RECOVERY_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/ime-composition.test.mjs b/web/tests/unit/ime-composition.test.mjs new file mode 100644 index 00000000..819061c4 --- /dev/null +++ b/web/tests/unit/ime-composition.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "../../node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(root, "web/protocol/ime-composition.ts"); +const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const ime = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); + +test("M14-04D blocks operators through composition start/update and reopens after end", () => { + let state = ime.createIMECompositionState(); + assert.equal(ime.shouldBlockOperatorShortcuts(state), false); + state = ime.reduceIMEComposition(state, { type: "compositionstart", data: "n" }); + assert.equal(ime.shouldBlockOperatorShortcuts(state), true); + state = ime.reduceIMEComposition(state, { type: "compositionupdate", data: "ni" }); + assert.deepEqual([state.composing, state.pendingText, state.lastEvent], [true, "ni", "UPDATE"]); + state = ime.reduceIMEComposition(state, { type: "compositionend", data: "你" }); + assert.equal(ime.shouldBlockOperatorShortcuts(state), false); + assert.equal(ime.shouldBlockOperatorShortcuts(state, true), true); +}); + +test("M14-04D ignores stray updates and rejects malformed state", () => { + let state = ime.createIMECompositionState(); + assert.equal(ime.reduceIMEComposition(state, { type: "compositionupdate", data: "x" }), state); + assert.throws(() => ime.shouldBlockOperatorShortcuts({ schemaVersion: 2 }), /IME_STATE_INVALID/); +}); diff --git a/web/tests/unit/input-modal.test.mjs b/web/tests/unit/input-modal.test.mjs new file mode 100644 index 00000000..4d0db9e0 --- /dev/null +++ b/web/tests/unit/input-modal.test.mjs @@ -0,0 +1,27 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "../../node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(root, "web/protocol/input-modal.ts"); +const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const modal = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); + +test("M14-04F cancels touch modal without commit and starts one two-finger navigation revision", () => { + let state = modal.createInputModalState(); + state = modal.beginTouch(state, 4); + state = modal.beginTouch(state, 2); + assert.deepEqual([state.kind, state.activePointerIds, state.navigationRevision, state.mainCommitCount], ["TOUCH_NAVIGATION", [2, 4], 1, 0]); + state = modal.cancelInputModal(state); + assert.deepEqual([state.kind, state.activePointerIds, state.cancelled, state.mainCommitCount], ["NONE", [], true, 0]); +}); + +test("M14-04F commits pen stroke once and ignores late up/cancel", () => { + let state = modal.beginPenStroke(modal.createInputModalState(), 9); + state = modal.commitPenStroke(state, 9); + state = modal.commitPenStroke(state, 9); + assert.deepEqual([state.kind, state.mainCommitCount, state.activePointerIds], ["NONE", 1, []]); +}); diff --git a/web/tests/unit/io-format-capability-matrix.test.mjs b/web/tests/unit/io-format-capability-matrix.test.mjs new file mode 100644 index 00000000..00e77a87 --- /dev/null +++ b/web/tests/unit/io-format-capability-matrix.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-capability-matrix-unit-")); +const sourcePath = path.join(root, "web/protocol/io-format-capability-matrix.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "io-format-capability-matrix.mjs"), transpiled.outputText); +const matrix = await import(pathToFileURL(path.join(temporary, "io-format-capability-matrix.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05B/manifest.json"), "utf8")); +const source = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05B/capability-matrix.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-05B binds the capability matrix and runtime inventory artifacts", () => { + assert.equal(manifest.task, "M12-05B"); + assert.equal(manifest.parentTask, "M12-05A"); + assert.equal(manifest.nextTask, "M12-05C"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-05B accepts the seven-format matrix and keeps the bounded GLB export route explicit", () => { + const parsed = matrix.parseIOFormatCapabilityMatrix(source); + assert.deepEqual(parsed.formats.map((entry) => entry.format), ["GLTF", "GLB", "OBJ", "STL", "PLY", "USD", "ALEMBIC"]); + assert.equal(parsed.formats.find((entry) => entry.format === "GLB").operations.EXPORT.local.status, "READY"); + assert.equal(parsed.formats.find((entry) => entry.format === "GLB").operations.EXPORT.local.execution, "LOCAL"); + assert.equal(parsed.formats.find((entry) => entry.format === "GLB").operations.IMPORT.local.status, "BLOCKED"); + assert.ok(parsed.formats.filter((entry) => entry.operations.IMPORT.local.status === "BLOCKED").length >= 6); +}); + +test("M12-05B rejects duplicate formats, ready routes without a real executor, and unverified ready features", () => { + assert.throws(() => matrix.parseIOFormatCapabilityMatrix({ ...source, formats: [...source.formats.slice(0, 6), source.formats[0]] }), { code: "ASSET_MANIFEST_INVALID" }); + const badRoute = structuredClone(source); + badRoute.formats.find((entry) => entry.format === "OBJ").operations.IMPORT.local = { status: "READY", execution: "LOCAL", code: null }; + assert.throws(() => matrix.parseIOFormatCapabilityMatrix(badRoute), { code: "ASSET_MANIFEST_INVALID" }); + const badFeature = structuredClone(source); + badFeature.formats.find((entry) => entry.format === "GLB").operations.EXPORT.geometry.status = "UNVERIFIED"; + assert.throws(() => matrix.parseIOFormatCapabilityMatrix(badFeature), { code: "ASSET_MANIFEST_INVALID" }); + const badCode = structuredClone(source); + badCode.formats.find((entry) => entry.format === "PLY").operations.EXPORT.local.code = null; + assert.throws(() => matrix.parseIOFormatCapabilityMatrix(badCode), { code: "IO_FORMAT_UNSUPPORTED" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/io-format-receipt-binding.test.mjs b/web/tests/unit/io-format-receipt-binding.test.mjs new file mode 100644 index 00000000..d33477f4 --- /dev/null +++ b/web/tests/unit/io-format-receipt-binding.test.mjs @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-binding-unit-")); +const sourcePath = path.join(root, "web/protocol/io-format-receipt-binding.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); fs.writeFileSync(path.join(temporary, "protocol.mjs"), transpiled.outputText); +const protocol = await import(pathToFileURL(path.join(temporary, "protocol.mjs"))); +const bound = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05E/bound-runtime-receipts.json"), "utf8")); + +test("M12-05E accepts receipts with all three identity hashes", () => { + const parsed = protocol.validateIOFormatBoundReceiptSet(bound, bound.parentReceiptSetSha256, bound.inventorySha256); + assert.match(protocol.resolveBoundReceipt(parsed, "GLB", "EXPORT").sourceSha256, /^[a-f0-9]{64}$/); + assert.match(protocol.resolveBoundReceipt(parsed, "GLB", "EXPORT").settingsSha256, /^[a-f0-9]{64}$/); + assert.match(protocol.resolveBoundReceipt(parsed, "GLB", "EXPORT").runtimeSha256, /^[a-f0-9]{64}$/); +}); + +test("M12-05E rejects source, settings, runtime and parent hash drift", () => { + for (const field of ["sourceSha256", "settingsSha256", "runtimeSha256"]) { + const mutated = structuredClone(bound); mutated.receipts[0][field] = "invalid-hash"; + assert.throws(() => protocol.validateIOFormatBoundReceiptSet(mutated, mutated.parentReceiptSetSha256, mutated.inventorySha256), { code: "IO_FORMAT_UNSUPPORTED" }); + } + const stale = structuredClone(bound); stale.parentReceiptSetSha256 = "0".repeat(64); + assert.throws(() => protocol.validateIOFormatBoundReceiptSet(stale, bound.parentReceiptSetSha256, bound.inventorySha256), { code: "IO_FORMAT_UNSUPPORTED" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/io-format-receipt-freshness.test.mjs b/web/tests/unit/io-format-receipt-freshness.test.mjs new file mode 100644 index 00000000..670bcdb5 --- /dev/null +++ b/web/tests/unit/io-format-receipt-freshness.test.mjs @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-freshness-unit-")); +const stableValue = (value) => Array.isArray(value) ? value.map(stableValue) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])])) : value; +const stableSha256 = (value) => crypto.createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex"); + +function transpile(sourcePath, outputName, replacements = []) { + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + let output = result.outputText; + for (const [from, to] of replacements) output = output.replaceAll(from, to); + const outputPath = path.join(temporary, outputName); + fs.writeFileSync(outputPath, output); + return outputPath; +} + +const parentPath = path.join(root, "tests/golden/M12-05E/bound-runtime-receipts.json"); +const freshnessPath = path.join(root, "tests/golden/M12-05F/fresh-runtime-receipts.json"); +const parentBytes = fs.readFileSync(parentPath); +const bound = JSON.parse(parentBytes); +const freshness = JSON.parse(fs.readFileSync(freshnessPath, "utf8")); +const protocolPath = path.join(root, "web/protocol/io-format-receipt-freshness.ts"); +transpile(path.join(root, "web/protocol/io-format-runtime-receipt.ts"), "io-format-runtime-receipt.mjs"); +transpile(path.join(root, "web/protocol/io-format-receipt-binding.ts"), "io-format-receipt-binding.mjs"); +const protocol = await import(pathToFileURL(transpile(protocolPath, "io-format-receipt-freshness.mjs", [["./io-format-receipt-binding\"", "./io-format-receipt-binding.mjs\""], ["./io-format-runtime-receipt\"", "./io-format-runtime-receipt.mjs\""]]))); +const expected = { + parentBindingSha256: crypto.createHash("sha256").update(parentBytes).digest("hex"), + parentReceiptSetSha256: bound.parentReceiptSetSha256, + inventorySha256: bound.inventorySha256, + boundReceiptSetSha256: stableSha256(bound), + runtimeSha256: stableSha256(bound.runtime), + runtime: bound.runtime, + receiptIdentities: bound.receipts, +}; + +test("M12-05F artifact is deterministic and bound to M12-05E", () => { + const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05F/manifest.json"), "utf8")); + assert.equal(manifest.task, "M12-05F"); + assert.equal(manifest.parentTask, "M12-05E"); + assert.equal(manifest.nextTask, "M12-06A"); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(crypto.createHash("sha256").update(fs.readFileSync(path.join(root, artifact.path))).digest("hex"), artifact.sha256, artifact.path); + } + assert.equal(freshness.parentBindingSha256, expected.parentBindingSha256); + assert.equal(freshness.boundReceiptSetSha256, expected.boundReceiptSetSha256); + assert.equal(freshness.runtimeSha256, expected.runtimeSha256); +}); + +test("M12-05F accepts only the exact trusted runtime receipt", async () => { + const parsed = await protocol.verifyIOFormatReceiptFreshness(freshness, expected); + assert.equal(parsed.bound.receipts.length, 14); + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(freshness, expected, { format: "GLB", operation: "EXPORT" }).status, "READY"); + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(freshness, expected, { format: "USD", operation: "EXPORT" }).status, "BLOCKED"); +}); + +test("M12-05F rejects forged content before route execution", async () => { + const forged = structuredClone(freshness); + forged.bound.receipts[0].operator = "forged.operator"; + await assert.rejects(() => protocol.verifyIOFormatReceiptFreshness(forged, expected), (error) => error.reason === "RECEIPT_FORGED"); + const malformed = structuredClone(freshness); + delete malformed.boundReceiptSetSha256; + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(malformed, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_FORGED"); +}); + +test("M12-05F rejects stale parent identity and cross-version runtime", () => { + const stale = structuredClone(freshness); + stale.bound.inventorySha256 = "a".repeat(64); + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(stale, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_STALE"); + const crossVersion = structuredClone(freshness); + crossVersion.bound.runtime.versionTuple = [5, 3, 0]; + assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(crossVersion, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_CROSS_VERSION"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/io-format-recovery.test.mjs b/web/tests/unit/io-format-recovery.test.mjs new file mode 100644 index 00000000..5c7455ad --- /dev/null +++ b/web/tests/unit/io-format-recovery.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-recovery-unit-")); +const sourcePath = path.join(root, "web/protocol/io-format-recovery.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "io-format-recovery.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const hash = "a".repeat(64); +const outputHash = "b".repeat(64); + +test("M12-07J keeps cancellation and OOM receipts unpublished", () => { + const running = protocol.beginIOFormatRecoveryOperation({ operationId: "ply-cancel-1", format: "PLY", operation: "IMPORT", workerGeneration: 1, baseRevision: 3, inputBytes: 32, inputSha256: hash }); + const cancelled = protocol.cancelIOFormatRecoveryOperation(running); + assert.deepEqual(cancelled, { ...running, status: "CANCELLED", errorCode: "IO_FORMAT_OPERATION_CANCELLED", temporaryBytes: 0, liveRequests: 0, publishedResults: 0, committed: false }); + const oomRunning = protocol.beginIOFormatRecoveryOperation({ operationId: "obj-oom-1", format: "OBJ", operation: "IMPORT", workerGeneration: 1, baseRevision: 3, inputBytes: 512 * 1024 + 1, inputSha256: hash }); + const blocked = protocol.blockIOFormatRecoveryOperation(oomRunning); + assert.equal(blocked.errorCode, "IO_FORMAT_OOM"); + assert.equal(blocked.publishedResults, 0); + assert.throws(() => protocol.commitIOFormatRecoveryOperation(cancelled, { bytes: 1, sha256: outputHash }), /IO_FORMAT_RECOVERY_INVALID/); +}); + +test("M12-07J binds output identity across restart and rejects stale generation", () => { + const running = protocol.beginIOFormatRecoveryOperation({ operationId: "stl-restart-1", format: "STL", operation: "EXPORT", workerGeneration: 1, baseRevision: 7, inputBytes: 64, inputSha256: hash }); + const committed = protocol.commitIOFormatRecoveryOperation(running, { bytes: 128, sha256: outputHash }); + const recovered = protocol.recoverIOFormatRecoveryOperation(committed, 2); + assert.deepEqual(recovered, { ...committed, status: "RECOVERED", workerGeneration: 2, errorCode: "IO_FORMAT_WORKER_RESTARTED" }); + assert.equal(protocol.parseIOFormatRecoveryReceipt(recovered).outputSha256, outputHash); + assert.throws(() => protocol.recoverIOFormatRecoveryOperation(committed, 1), /IO_FORMAT_RECOVERY_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/io-format-runtime-receipt.test.mjs b/web/tests/unit/io-format-runtime-receipt.test.mjs new file mode 100644 index 00000000..2d868fcf --- /dev/null +++ b/web/tests/unit/io-format-runtime-receipt.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-runtime-receipt-unit-")); +const sourcePath = path.join(root, "web/protocol/io-format-runtime-receipt.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "protocol.mjs"), transpiled.outputText); +const protocol = await import(pathToFileURL(path.join(temporary, "protocol.mjs"))); +const receiptSet = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-05D/runtime-receipts.json"), "utf8")); +const inventorySha256 = receiptSet.inventorySha256; + +test("M12-05D resolves capability from the runtime operator receipt", () => { + const parsed = protocol.validateIOFormatRuntimeReceiptSet(receiptSet, inventorySha256); + assert.equal(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "GLB", operation: "EXPORT" }).status, "READY"); + assert.equal(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "USD", operation: "EXPORT" }).status, "BLOCKED"); +}); + +test("M12-05D does not infer capability from a filename extension", () => { + const mutated = structuredClone(receiptSet); + const receipt = mutated.receipts.find((candidate) => candidate.format === "GLB" && candidate.operation === "EXPORT"); + receipt.extensions = [".usd"]; + const parsed = protocol.validateIOFormatRuntimeReceiptSet(mutated, inventorySha256); + assert.deepEqual(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "GLB", operation: "EXPORT" }).status, "READY"); + assert.deepEqual(protocol.resolveIOFormatRuntimeRoute(parsed, { format: "USD", operation: "EXPORT" }).status, "BLOCKED"); +}); + +test("M12-05D rejects receipt identity drift and consumes explicit receipt status", () => { + const stale = structuredClone(receiptSet); + stale.inventorySha256 = "0".repeat(64); + assert.throws(() => protocol.validateIOFormatRuntimeReceiptSet(stale, inventorySha256), { code: "IO_FORMAT_UNSUPPORTED" }); + const forged = structuredClone(receiptSet); + const usd = forged.receipts.find((candidate) => candidate.format === "USD" && candidate.operation === "EXPORT"); + usd.runtimeStatus = "AVAILABLE"; + usd.registered = true; + usd.rnaIdentifier = "WM_OT_usd_export"; + usd.buildOptionEnabled = true; + assert.doesNotThrow(() => protocol.parseIOFormatRuntimeReceiptSet(forged)); + assert.equal(protocol.resolveIOFormatRuntimeRoute(protocol.parseIOFormatRuntimeReceiptSet(forged), { format: "USD", operation: "EXPORT" }).status, "READY"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/io-format-ui-gate.test.mjs b/web/tests/unit/io-format-ui-gate.test.mjs new file mode 100644 index 00000000..915ffa5c --- /dev/null +++ b/web/tests/unit/io-format-ui-gate.test.mjs @@ -0,0 +1,53 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-ui-gate-unit-")); +const sourcePath = path.join(root, "web/protocol/io-format-ui-gate.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "io-format-ui-gate.mjs"), transpiled.outputText); +const gate = await import(pathToFileURL(path.join(temporary, "io-format-ui-gate.mjs"))); +const registry = JSON.parse(fs.readFileSync(path.join(root, "web/app/src/capabilities/io-format-ui-registry.json"), "utf8")); + +test("M12-05C keeps only matrix-declared executable routes in UI", () => { + const parsed = gate.parseIOFormatUIRegistry(registry); + assert.deepEqual(parsed.importRoutes, []); + assert.deepEqual(parsed.exportRoutes.map((route) => route.format), ["GLB"]); + const commands = [ + { id: "file.export-glb", ioFormat: { format: "GLB", operation: "EXPORT", execution: "LOCAL" } }, + { id: "file.import-obj", ioFormat: { format: "OBJ", operation: "IMPORT", execution: "LOCAL" } }, + { id: "file.export-usd", ioFormat: { format: "USD", operation: "EXPORT", execution: "SERVER" } }, + { id: "edit.undo" }, + ]; + assert.deepEqual(gate.filterIOFormatOperatorCommands(commands, parsed).map((command) => command.id), ["file.export-glb", "edit.undo"]); +}); + +test("M12-05C file selection is project-only while import routes are blocked", () => { + const parsed = gate.parseIOFormatUIRegistry(registry); + assert.equal(gate.ioFormatUIAccept(parsed), ".blend,application/octet-stream"); + assert.deepEqual(gate.gateIOFormatFileSelection("scene.blend", parsed), { status: "READY", kind: "BLEND" }); + assert.deepEqual(gate.gateIOFormatFileSelection("mesh.obj", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".obj" }); + assert.deepEqual(gate.gateIOFormatFileSelection("scene.glb", parsed), { status: "BLOCKED", code: "IO_FORMAT_UNSUPPORTED", extension: ".glb" }); +}); + +test("M12-05C rejects malformed route metadata", () => { + const bad = structuredClone(registry); + bad.importRoutes = [{ format: "OBJ", operation: "IMPORT", execution: "LOCAL", extensions: [".obj"] }]; + assert.doesNotThrow(() => gate.parseIOFormatUIRegistry(bad)); + const parsed = gate.parseIOFormatUIRegistry(bad); + assert.equal(gate.filterIOFormatOperatorCommands([{ id: "import-obj", ioFormat: { format: "OBJ", operation: "IMPORT", execution: "LOCAL" } }], parsed).length, 1); + bad.importRoutes[0].extensions = [".not-obj"]; + assert.throws(() => gate.parseIOFormatUIRegistry(bad), { code: "IO_FORMAT_UNSUPPORTED" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/keyboard-contract.test.mjs b/web/tests/unit/keyboard-contract.test.mjs new file mode 100644 index 00000000..605077c5 --- /dev/null +++ b/web/tests/unit/keyboard-contract.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "../../node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(root, "web/protocol/keyboard-contract.ts"); +const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const keyboard = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); + +test("M14-04E preserves layout character, physical code, location and modifiers", () => { + assert.deepEqual(keyboard.observeKeyboardEvent({ key: "ä", code: "Quote", location: 0, shiftKey: true, ctrlKey: false, altKey: true, metaKey: false, repeat: true, isComposing: false }), { schemaVersion: 1, key: "ä", code: "Quote", location: 0, shiftKey: true, ctrlKey: false, altKey: true, metaKey: false, repeat: true, isComposing: false, deadKey: false }); + assert.equal(keyboard.observeKeyboardEvent({ key: "Dead", code: "Quote", location: 0 }).deadKey, true); +}); + +test("M14-04E rejects malformed key identity", () => { + assert.throws(() => keyboard.observeKeyboardEvent({ key: "", code: "KeyA", location: 0 }), /KEY_IDENTITY_INVALID/); + assert.throws(() => keyboard.observeKeyboardEvent({ key: "a", code: "KeyA", location: 4 }), /KEY_LOCATION_INVALID/); +}); diff --git a/web/tests/unit/library-append-wasm.test.mjs b/web/tests/unit/library-append-wasm.test.mjs new file mode 100644 index 00000000..4555ec70 --- /dev/null +++ b/web/tests/unit/library-append-wasm.test.mjs @@ -0,0 +1,64 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-append-wasm-unit-")); +const sourcePath = path.join(root, "web/protocol/library-main-append.ts"); +const identityPath = path.join(root, "web/protocol/library-operation-identity.ts"); +const identityTranspiled = ts.transpileModule(fs.readFileSync(identityPath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: identityPath, + reportDiagnostics: true, +}); +assert.deepEqual(identityTranspiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-operation-identity.mjs"), identityTranspiled.outputText); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-main-append.mjs"), transpiled.outputText.replaceAll("./library-operation-identity\"", "./library-operation-identity.mjs\"")); +const append = await import(pathToFileURL(path.join(temporary, "library-main-append.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03N/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const closure = { + object: "Object/M12 Append Object", + mesh: "Mesh/M12 Append Mesh", + material: "Material/M12 Append Material", + image: "Image/M12 Append Image", +}; + +test("M12-03N binds the independent append WASM command", () => { + assert.equal(manifest.task, "M12-03N"); + assert.equal(manifest.nextTask, "M12-04A"); + assert.equal(sha256("web/protocol/library-main-append.ts"), manifest.artifacts.appendWasmProtocol.sha256); +}); + +test("M12-03N validates the append closure and one-transaction receipt in the WASM lane", async () => { + assert.deepEqual(append.parseLibraryAppendClosure(closure), closure); + const request = { + baseRevision: 11, + binding: { + source: { sourceLibraryId: "library:" + "a".repeat(64) }, + sourceDataBlockId: closure.object, + dependencyClosureSha256: await append.computeLibraryAppendClosureSha256(closure), + }, + expectedClosure: closure, + }; + const receipt = append.createLibraryMainAppendReceipt(request, 12); + assert.equal(receipt.operation, "APPEND"); + assert.equal(receipt.transactionCount, 1); + assert.equal(receipt.baseRevision, 11); + assert.equal(receipt.nextRevision, 12); + assert.equal(receipt.mapping.length, 4); + assert(receipt.mapping.every((item) => item.owner === "LOCAL_MAIN" && item.readOnly === false && item.source === item.local)); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-archive-budget.test.mjs b/web/tests/unit/library-archive-budget.test.mjs new file mode 100644 index 00000000..bffb9ea0 --- /dev/null +++ b/web/tests/unit/library-archive-budget.test.mjs @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-budget-unit-")); +for (const name of ["asset-path.ts", "capability-gates.ts", "error.ts", "asset-library-io.ts"]) { + const sourcePath = path.join(root, "web/protocol", name); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + let output = transpiled.outputText; + output = output.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"').replaceAll('from "./capability-gates"', 'from "./capability-gates.mjs"').replaceAll('from "./error"', 'from "./error.mjs"'); + fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), output); +} +const io = await import(pathToFileURL(path.join(temporary, "asset-library-io.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04F/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const base = { format: "GLB", operation: "IMPORT", externalUris: [], archiveEntries: [] }; +const entry = (pathName, compressedBytes = 2, uncompressedBytes = 2) => ({ path: pathName, compressedBytes, uncompressedBytes }); + +test("M12-04F binds archive budget constants and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04F"); + assert.equal(manifest.parentTask, "M12-04E"); + assert.equal(manifest.nextTask, "M12-04G"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + assert.equal(io.ASSET_LIBRARY_BUDGET.maxArchivePathDepth, 64); + assert.equal(io.ASSET_LIBRARY_BUDGET.maxArchiveFileNameBytes, 255); +}); + +test("M12-04F accepts entries at the per-entry, total, depth and filename limits", () => { + const fileName = "é".repeat(125) + ".bin"; + const deepPath = `${Array.from({ length: io.ASSET_LIBRARY_BUDGET.maxArchivePathDepth }, (_, index) => `d${index}`).join("/")}/file.bin`; + const result = io.parseIORequest({ ...base, byteLength: 10, archiveEntries: [entry("a.bin"), entry(deepPath), entry(fileName)] }); + assert.equal(result.archiveEntries.length, 3); +}); + +test("M12-04F rejects per-entry, total, count, depth and UTF-8 filename overflow", () => { + assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("huge.bin", 1, io.ASSET_LIBRARY_BUDGET.maxEntryBytes + 1)] }), { code: "ASSET_BUDGET_EXCEEDED" }); + assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("a.bin", io.ASSET_LIBRARY_BUDGET.maxEntryBytes, io.ASSET_LIBRARY_BUDGET.maxEntryBytes), entry("b.bin", io.ASSET_LIBRARY_BUDGET.maxEntryBytes, io.ASSET_LIBRARY_BUDGET.maxEntryBytes), entry("c.bin", 1, 1)] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry(`${Array.from({ length: io.ASSET_LIBRARY_BUDGET.maxArchivePathDepth + 1 }, () => "d").join("/")}/file.bin`)] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("é".repeat(128))] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => io.parseIORequest({ ...base, archiveEntries: Array.from({ length: io.ASSET_LIBRARY_BUDGET.maxArchiveEntries + 1 }, (_, index) => entry(`f${index}.bin`)) }), { code: "ASSET_BUDGET_EXCEEDED" }); +}); + +test("M12-04F keeps compression and declared source-byte budgets fail-closed", () => { + assert.throws(() => io.parseIORequest({ ...base, archiveEntries: [entry("bomb.bin", 1, io.ASSET_LIBRARY_BUDGET.maxCompressionRatio + 1)] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => io.parseIORequest({ ...base, byteLength: 1, archiveEntries: [entry("source.bin", 2, 2)] }), { code: "IO_ARCHIVE_UNSAFE" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-archive-cancellation.test.mjs b/web/tests/unit/library-archive-cancellation.test.mjs new file mode 100644 index 00000000..82844885 --- /dev/null +++ b/web/tests/unit/library-archive-cancellation.test.mjs @@ -0,0 +1,209 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-cancellation-unit-")); +const sourcePath = path.join(root, "web/protocol/archive-extraction-transaction.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "archive-extraction-transaction.mjs"), transpiled.outputText); +const extraction = await import(pathToFileURL(path.join(temporary, "archive-extraction-transaction.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04H/manifest.json"), "utf8")); +const fileSha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const bytesSha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); + +class DirectoryExtractionStorage { + constructor(directory, committed) { + this.directory = directory; + this.committed = committed; + fs.mkdirSync(path.join(directory, "committed"), { recursive: true }); + fs.writeFileSync(path.join(directory, "committed", "project.blend"), Buffer.from("old-project")); + } + + async readCommitted() { return { ...this.committed }; } + + async createStaging(transactionId) { + fs.mkdirSync(path.join(this.directory, "staging", transactionId), { recursive: true }); + } + + async writeStaging(transactionId, entryPath, bytes) { + const target = path.join(this.directory, "staging", transactionId, entryPath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, bytes); + } + + async countStagingEntries(transactionId) { + const staging = path.join(this.directory, "staging", transactionId); + if (!fs.existsSync(staging)) return 0; + const visit = (directory) => fs.readdirSync(directory, { withFileTypes: true }).reduce( + (count, entry) => count + (entry.isDirectory() ? visit(path.join(directory, entry.name)) : 1), 0, + ); + return visit(staging); + } + + async discardStaging(transactionId) { + const count = await this.countStagingEntries(transactionId); + fs.rmSync(path.join(this.directory, "staging", transactionId), { recursive: true, force: true }); + return count; + } + + async commitStaging(transactionId, expected, candidate) { + assert.deepEqual(this.committed, expected); + const source = path.join(this.directory, "staging", transactionId, "project.blend"); + const target = path.join(this.directory, "committed", "project.blend"); + fs.renameSync(source, target); + await this.discardStaging(transactionId); + this.committed = { ...candidate }; + return { ...this.committed }; + } +} + +const oldBytes = Buffer.from("old-project"); +const newBytes = Buffer.from("new-project"); +const metadataBytes = Buffer.from("metadata"); +const committed = { projectId: "project:m12-04h", revision: 8, sha256: bytesSha256(oldBytes) }; +const candidate = { projectId: committed.projectId, revision: 9, sha256: bytesSha256(newBytes) }; +const request = { + schemaVersion: 1, + transactionId: "transaction:m12-04h", + archiveId: "archive:m12-04h", + committed, + candidate, + entries: [ + { path: "metadata/index.json", uncompressedBytes: metadataBytes.byteLength, sha256: bytesSha256(metadataBytes) }, + { path: "project.blend", uncompressedBytes: newBytes.byteLength, sha256: bytesSha256(newBytes) }, + ], +}; + +function createStorage(name) { + const directory = path.join(temporary, name); + fs.mkdirSync(directory, { recursive: true }); + return new DirectoryExtractionStorage(directory, committed); +} + +test("M12-04H binds cancellation rollback and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04H"); + assert.equal(manifest.parentTask, "M12-04G"); + assert.equal(manifest.nextTask, "M12-04I"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04H removes partially written staging and preserves the committed project on cancellation", async () => { + const storage = createStorage("cancel-after-write"); + const controller = new AbortController(); + let reads = 0; + const receipt = await extraction.runArchiveExtractionTransaction(request, storage, async (entry) => { + reads++; + if (reads === 2) controller.abort(); + return entry.path === "project.blend" ? newBytes : metadataBytes; + }, controller.signal); + assert.deepEqual(receipt, { + status: "CANCELLED", + code: "IO_ARCHIVE_CANCELLED", + transactionId: request.transactionId, + committedBefore: committed, + committedAfter: committed, + removedStagingEntries: 1, + stagingEntriesAfter: 0, + publishedProjects: 0, + }); + assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "old-project"); + assert.equal(await storage.countStagingEntries(request.transactionId), 0); +}); + +test("M12-04H cancels before staging and after the last staged write without publishing", async () => { + const beforeStorage = createStorage("cancel-before-stage"); + const beforeController = new AbortController(); + beforeController.abort(); + const before = await extraction.runArchiveExtractionTransaction(request, beforeStorage, async () => newBytes, beforeController.signal); + assert.equal(before.status, "CANCELLED"); + assert.equal(before.removedStagingEntries, 0); + + const finalStorage = createStorage("cancel-after-last-write"); + const finalController = new AbortController(); + const originalWrite = finalStorage.writeStaging.bind(finalStorage); + finalStorage.writeStaging = async (transactionId, entryPath, bytes) => { + await originalWrite(transactionId, entryPath, bytes); + if (entryPath === "project.blend") finalController.abort(); + }; + const after = await extraction.runArchiveExtractionTransaction(request, finalStorage, async (entry) => + entry.path === "project.blend" ? newBytes : metadataBytes, finalController.signal); + assert.equal(after.status, "CANCELLED"); + assert.equal(after.removedStagingEntries, 2); + assert.deepEqual(await finalStorage.readCommitted(), committed); + assert.equal(fs.readFileSync(path.join(finalStorage.directory, "committed", "project.blend"), "utf8"), "old-project"); +}); + +test("M12-04H commits only after all staged payloads pass identity checks", async () => { + const storage = createStorage("commit"); + const receipt = await extraction.runArchiveExtractionTransaction(request, storage, async (entry) => + entry.path === "project.blend" ? newBytes : metadataBytes, new AbortController().signal); + assert.deepEqual(receipt, { + status: "COMMITTED", + transactionId: request.transactionId, + committed: candidate, + stagingEntriesAfter: 0, + }); + assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "new-project"); +}); + +test("M12-04H cleans staging on payload failure and detects rollback identity drift", async () => { + const failedStorage = createStorage("payload-failure"); + await assert.rejects( + extraction.runArchiveExtractionTransaction(request, failedStorage, async () => Buffer.from("wrong"), new AbortController().signal), + { code: "ASSET_SOURCE_HASH_MISMATCH" }, + ); + assert.equal(await failedStorage.countStagingEntries(request.transactionId), 0); + assert.deepEqual(await failedStorage.readCommitted(), committed); + + const driftStorage = createStorage("rollback-drift"); + const originalDiscard = driftStorage.discardStaging.bind(driftStorage); + driftStorage.discardStaging = async (transactionId) => { + const removed = await originalDiscard(transactionId); + driftStorage.committed = { ...committed, revision: committed.revision + 1 }; + return removed; + }; + const controller = new AbortController(); + await assert.rejects( + extraction.runArchiveExtractionTransaction(request, driftStorage, async (entry) => { + const bytes = entry.path === "project.blend" ? newBytes : metadataBytes; + queueMicrotask(() => controller.abort()); + return bytes; + }, controller.signal), + { code: "STORAGE_TRANSACTION" }, + ); +}); + +test("M12-04H rejects stale commits, non-canonical paths, prefix conflicts, and undeclared fields", async () => { + const storage = createStorage("invalid-requests"); + await assert.rejects( + extraction.runArchiveExtractionTransaction({ ...request, committed: { ...committed, revision: 7 } }, storage, async () => newBytes, new AbortController().signal), + { code: "REVISION_CONFLICT" }, + ); + for (const unsafePath of ["../project.blend", "C:/project.blend", "https:project.blend", "dir\\project.blend"]) { + await assert.rejects( + extraction.runArchiveExtractionTransaction({ ...request, entries: [{ ...request.entries[0], path: unsafePath }] }, storage, async () => metadataBytes, new AbortController().signal), + { code: "IO_ARCHIVE_UNSAFE" }, + ); + } + await assert.rejects( + extraction.runArchiveExtractionTransaction({ ...request, entries: [request.entries[0], { ...request.entries[1], path: "metadata" }] }, storage, async () => metadataBytes, new AbortController().signal), + { code: "IO_ARCHIVE_UNSAFE" }, + ); + await assert.rejects( + extraction.runArchiveExtractionTransaction({ ...request, future: true }, storage, async () => newBytes, new AbortController().signal), + { code: "IO_ARCHIVE_UNSAFE" }, + ); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-archive-conflicts.test.mjs b/web/tests/unit/library-archive-conflicts.test.mjs new file mode 100644 index 00000000..d8050113 --- /dev/null +++ b/web/tests/unit/library-archive-conflicts.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-conflicts-unit-")); +const sourcePath = path.join(root, "web/protocol/archive-conflicts.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "archive-conflicts.mjs"), transpiled.outputText); +const conflicts = await import(pathToFileURL(path.join(temporary, "archive-conflicts.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04G/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const range = (pathName, compressedOffset, compressedBytes = 2, uncompressedBytes = 2) => ({ path: pathName, compressedOffset, compressedBytes, uncompressedBytes }); +const valid = { schemaVersion: 1, byteLength: 10, ranges: [range("a.bin", 0), range("dir/b.bin", 2, 3, 3)] }; + +test("M12-04G binds range/conflict validation and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04G"); + assert.equal(manifest.parentTask, "M12-04F"); + assert.equal(manifest.nextTask, "M12-04H"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04G accepts deterministic non-overlapping ranges and reports totals", () => { + assert.deepEqual(conflicts.validateArchiveConflicts(valid), { status: "VALID", totalCompressedBytes: 5, totalUncompressedBytes: 5, nonOverlapping: true, uniquePaths: true, noPrefixConflicts: true }); +}); + +test("M12-04G rejects compression bombs, overlaps, duplicate paths, and prefix conflicts", () => { + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("bomb.bin", 0, 1, conflicts.ARCHIVE_CONFLICT_BUDGET.maxCompressionRatio + 1)] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("a.bin", 0, 4), range("b.bin", 3, 2)] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("same.bin", 0), range("same.bin", 2)] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [range("folder", 0), range("folder/file.bin", 2)] }), { code: "IO_ARCHIVE_UNSAFE" }); +}); + +test("M12-04G rejects range/source bounds and undeclared fields", () => { + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, byteLength: 4 }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [{ ...valid.ranges[0], future: true }] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => conflicts.validateArchiveConflicts({ ...valid, ranges: [{ ...valid.ranges[0], compressedOffset: 9, compressedBytes: 2 }] }), { code: "IO_ARCHIVE_UNSAFE" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-archive-recovery.test.mjs b/web/tests/unit/library-archive-recovery.test.mjs new file mode 100644 index 00000000..90e0416c --- /dev/null +++ b/web/tests/unit/library-archive-recovery.test.mjs @@ -0,0 +1,163 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-archive-recovery-unit-")); +for (const sourceName of ["archive-extraction-transaction.ts", "archive-extraction-recovery.ts"]) { + const sourcePath = path.join(root, "web/protocol", sourceName); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + fs.writeFileSync( + path.join(temporary, sourceName.replace(".ts", ".mjs")), + transpiled.outputText.replaceAll('from "./archive-extraction-transaction"', 'from "./archive-extraction-transaction.mjs"'), + ); +} +const extraction = await import(pathToFileURL(path.join(temporary, "archive-extraction-recovery.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04I/manifest.json"), "utf8")); +const fileSha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const bytesSha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); + +class FaultingDirectoryStorage { + constructor(directory, committed) { + this.directory = directory; + this.committed = { ...committed }; + this.fault = null; + this.partialBytes = 0; + fs.mkdirSync(path.join(directory, "committed"), { recursive: true }); + fs.writeFileSync(path.join(directory, "committed", "project.blend"), Buffer.from("old-project")); + } + + async readCommitted() { return { ...this.committed }; } + + async createStaging(transactionId) { + fs.mkdirSync(path.join(this.directory, "staging", transactionId), { recursive: true }); + } + + async writeStaging(transactionId, entryPath, bytes) { + const target = path.join(this.directory, "staging", transactionId, entryPath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + if (this.fault) { + const partial = bytes.subarray(0, Math.max(1, Math.floor(bytes.byteLength / 2))); + fs.writeFileSync(target, partial); + this.partialBytes += partial.byteLength; + const fault = this.fault; + this.fault = null; + throw fault; + } + fs.writeFileSync(target, bytes); + } + + async countStagingEntries(transactionId) { + const staging = path.join(this.directory, "staging", transactionId); + if (!fs.existsSync(staging)) return 0; + const visit = (directory) => fs.readdirSync(directory, { withFileTypes: true }).reduce( + (count, entry) => count + (entry.isDirectory() ? visit(path.join(directory, entry.name)) : 1), 0, + ); + return visit(staging); + } + + async discardStaging(transactionId) { + const count = await this.countStagingEntries(transactionId); + fs.rmSync(path.join(this.directory, "staging", transactionId), { recursive: true, force: true }); + return count; + } + + async commitStaging(transactionId, expected, candidate) { + assert.deepEqual(this.committed, expected); + const source = path.join(this.directory, "staging", transactionId, "project.blend"); + const target = path.join(this.directory, "committed", "project.blend"); + fs.renameSync(source, target); + await this.discardStaging(transactionId); + this.committed = { ...candidate }; + return { ...this.committed }; + } +} + +const oldBytes = Buffer.from("old-project"); +const largeBytes = Buffer.alloc(4096, 0x51); +const smallBytes = Buffer.from("small-project"); +const committed = { projectId: "project:m12-04i", revision: 9, sha256: bytesSha256(oldBytes) }; + +function request(transactionId, base, bytes) { + return { + schemaVersion: 1, + transactionId, + archiveId: `archive:${transactionId}`, + committed: base, + candidate: { projectId: base.projectId, revision: base.revision + 1, sha256: bytesSha256(bytes) }, + entries: [{ path: "project.blend", uncompressedBytes: bytes.byteLength, sha256: bytesSha256(bytes) }], + }; +} + +function createStorage(name) { + const directory = path.join(temporary, name); + fs.mkdirSync(directory, { recursive: true }); + return new FaultingDirectoryStorage(directory, committed); +} + +async function faultThenRecover(name, fault, expectedCode) { + const storage = createStorage(name); + storage.fault = fault; + const failedRequest = request(`transaction:${name}:large`, committed, largeBytes); + await assert.rejects( + extraction.runArchiveExtractionTransaction(failedRequest, storage, async () => largeBytes, new AbortController().signal), + { code: expectedCode }, + ); + assert.ok(storage.partialBytes > 0); + assert.equal(await storage.countStagingEntries(failedRequest.transactionId), 0); + assert.deepEqual(await storage.readCommitted(), committed); + assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "old-project"); + + const recoveryRequest = request(`transaction:${name}:small`, committed, smallBytes); + const receipt = await extraction.runArchiveExtractionTransaction( + recoveryRequest, + storage, + async () => smallBytes, + new AbortController().signal, + ); + assert.deepEqual(receipt, { + status: "COMMITTED", + transactionId: recoveryRequest.transactionId, + committed: recoveryRequest.candidate, + stagingEntriesAfter: 0, + }); + assert.equal(await storage.countStagingEntries(recoveryRequest.transactionId), 0); + assert.equal(fs.readFileSync(path.join(storage.directory, "committed", "project.blend"), "utf8"), "small-project"); +} + +test("M12-04I binds quota/OOM cleanup and recovery evidence", () => { + assert.equal(manifest.task, "M12-04I"); + assert.equal(manifest.parentTask, "M12-04H"); + assert.equal(manifest.nextTask, "M12-04J"); + assert.deepEqual(manifest.assertions.faultCodes, ["STORAGE_QUOTA", "WASM_OUT_OF_MEMORY"]); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(fileSha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04I releases partial staging after quota and accepts a small archive in the same storage", async () => { + await faultThenRecover("quota", new DOMException("quota exhausted", "QuotaExceededError"), "STORAGE_QUOTA"); +}); + +test("M12-04I releases partial staging after OOM and accepts a small archive in the same storage", async () => { + await faultThenRecover("oom", Object.assign(new Error("deterministic allocation failure"), { code: "WASM_OUT_OF_MEMORY" }), "WASM_OUT_OF_MEMORY"); +}); + +test("M12-04I maps only declared resource faults", () => { + assert.equal(extraction.archiveExtractionResourceFaultCode(new DOMException("full", "QuotaExceededError")), "STORAGE_QUOTA"); + assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { code: "STORAGE_QUOTA" })), "STORAGE_QUOTA"); + assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { code: "WASM_OUT_OF_MEMORY" })), "WASM_OUT_OF_MEMORY"); + assert.equal(extraction.archiveExtractionResourceFaultCode(Object.assign(new Error("fault"), { name: "OutOfMemoryError" })), "WASM_OUT_OF_MEMORY"); + assert.equal(extraction.archiveExtractionResourceFaultCode(new RangeError("array length is invalid")), undefined); + assert.equal(extraction.archiveExtractionResourceFaultCode(new Error("ordinary IO failure")), undefined); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-link-safety.test.mjs b/web/tests/unit/library-link-safety.test.mjs new file mode 100644 index 00000000..6cf5829a --- /dev/null +++ b/web/tests/unit/library-link-safety.test.mjs @@ -0,0 +1,88 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-link-safety-unit-")); +const sourcePath = path.join(root, "web/protocol/archive-link-safety.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "archive-link-safety.mjs"), transpiled.outputText); +const safety = await import(pathToFileURL(path.join(temporary, "archive-link-safety.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04D/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +const valid = { + schemaVersion: 1, + temporaryRootId: "staging:archive-1", + entries: [ + { path: "payload/data.bin", type: "FILE", target: null }, + { path: "payload/data-alias.bin", type: "SYMLINK", target: "./data.bin" }, + { path: "payload/data-hard.bin", type: "HARDLINK", target: "payload/data.bin" }, + { path: "payload", type: "DIRECTORY", target: null }, + { path: "alias-dir", type: "SYMLINK", target: "payload" }, + ], +}; + +test("M12-04D binds the archive link gate and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04D"); + assert.equal(manifest.parentTask, "M12-04C"); + assert.equal(manifest.nextTask, "M12-04E"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04D resolves symlinks relative to their parent and hardlinks from the archive root", () => { + const result = safety.resolveArchiveLinkEntries(valid); + assert.equal(result.status, "READY"); + assert.equal(result.entries.find((entry) => entry.path === "payload/data-alias.bin").resolvedPath, "payload/data.bin"); + assert.equal(result.entries.find((entry) => entry.path === "payload/data-hard.bin").resolvedPath, "payload/data.bin"); + assert.equal(result.entries.find((entry) => entry.path === "alias-dir").resolvedPath, "payload"); + assert.ok(result.entries.every((entry) => entry.withinTemporaryRoot === true)); +}); + +test("M12-04D rejects absolute, drive, URI and traversal targets before writing", () => { + for (const target of ["/outside", "\\\\server\\share", "C:/outside", "https://evil.example/a", "../../outside", "payload/../../outside"]) { + assert.throws(() => safety.resolveArchiveLinkEntries({ + ...valid, + entries: [{ path: "payload/link", type: "SYMLINK", target }, { path: "payload", type: "DIRECTORY", target: null }], + }), { code: "IO_ARCHIVE_UNSAFE" }); + } +}); + +test("M12-04D rejects missing targets, cycles and hardlinks to directories", () => { + assert.throws(() => safety.resolveArchiveLinkEntries({ + ...valid, + entries: [{ path: "link", type: "SYMLINK", target: "missing.bin" }], + }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => safety.resolveArchiveLinkEntries({ + ...valid, + entries: [ + { path: "a", type: "SYMLINK", target: "b" }, + { path: "b", type: "HARDLINK", target: "a" }, + ], + }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => safety.resolveArchiveLinkEntries({ + ...valid, + entries: [ + { path: "dir", type: "DIRECTORY", target: null }, + { path: "dir-hard", type: "HARDLINK", target: "dir" }, + ], + }), { code: "IO_ARCHIVE_UNSAFE" }); +}); + +test("M12-04D rejects duplicate members and undeclared fields", () => { + assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], target: null }, { ...valid.entries[0], target: null }] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, future: true }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => safety.parseArchiveLinkRequest({ ...valid, entries: [{ ...valid.entries[0], mode: 0o644 }] }), { code: "IO_ARCHIVE_UNSAFE" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-linked-missing.test.mjs b/web/tests/unit/library-linked-missing.test.mjs new file mode 100644 index 00000000..cdde7ad9 --- /dev/null +++ b/web/tests/unit/library-linked-missing.test.mjs @@ -0,0 +1,107 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-linked-missing-unit-")); +const sourcePath = path.join(root, "web/protocol/library-linked-missing.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-linked-missing.mjs"), transpiled.outputText); +const missing = await import(pathToFileURL(path.join(temporary, "library-linked-missing.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03I/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const library = (value) => `library:${String(value).repeat(64).slice(0, 64)}`; +const digest = (value) => crypto.createHash("sha256").update(String(value)).digest("hex"); +const reference = (sourceLibraryId, generation, revision, marker, status = "AVAILABLE") => ({ + sourceLibraryId, + sourceLocator: `project://libraries/${marker}.blend`, + sourceSha256: digest(`${marker}-source`), + sourceGeneration: generation, + sourceRevision: revision, + dataBlockIds: [`Object/${marker}`, `Mesh/${marker}`], + status, + placeholder: status === "MISSING" ? { + kind: "MISSING_LIBRARY", + sourceLibraryId, + dataBlockIds: [`Object/${marker}`, `Mesh/${marker}`], + } : null, +}); + +test("M12-03I binds the missing-library protocol and evidence artifacts", () => { + assert.equal(manifest.task, "M12-03I"); + assert.equal(manifest.parentTask, "M12-03H"); + assert.equal(manifest.nextTask, "M12-03J"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-03I marks only the matching reference missing and preserves its original source", () => { + const sourceLibraryId = library("a"); + const otherLibraryId = library("b"); + const current = reference(sourceLibraryId, 4, 9, "primary"); + const other = reference(otherLibraryId, 1, 2, "other"); + const state = { schemaVersion: 1, references: [current, other] }; + const decision = missing.markLinkedLibraryMissing(state, { + schemaVersion: 1, + operation: "MARK_MISSING", + sourceLibraryId, + sourceLocator: current.sourceLocator, + sourceSha256: current.sourceSha256, + expectedGeneration: 4, + expectedRevision: 9, + }); + assert.equal(decision.status, "MARKED"); + assert.equal(decision.code, null); + assert.deepEqual(decision.state.references[0], { ...current, status: "MISSING", placeholder: { + kind: "MISSING_LIBRARY", sourceLibraryId, dataBlockIds: current.dataBlockIds, + } }); + assert.deepEqual(decision.state.references[1], other); + assert.deepEqual(state.references, [current, other]); +}); + +test("M12-03I preserves the reference on stale generation and source hash drift", () => { + const sourceLibraryId = library("c"); + const current = reference(sourceLibraryId, 2, 5, "stable"); + const state = { schemaVersion: 1, references: [current] }; + const base = { + schemaVersion: 1, + operation: "MARK_MISSING", + sourceLibraryId, + sourceLocator: current.sourceLocator, + sourceSha256: current.sourceSha256, + expectedGeneration: 2, + expectedRevision: 5, + }; + assert.equal(missing.markLinkedLibraryMissing(state, { ...base, expectedRevision: 6 }).code, "REVISION_CONFLICT"); + assert.equal(missing.markLinkedLibraryMissing(state, { ...base, sourceSha256: digest("different") }).code, "ASSET_SOURCE_HASH_MISMATCH"); + assert.deepEqual(state.references, [current]); +}); + +test("M12-03I rejects undeclared fields, duplicate identities, and invalid placeholders", () => { + const sourceLibraryId = library("d"); + const current = reference(sourceLibraryId, 1, 1, "invalid"); + assert.throws(() => missing.parseLinkedMissingRequest({ + schemaVersion: 1, + operation: "MARK_MISSING", + sourceLibraryId, + sourceLocator: current.sourceLocator, + sourceSha256: current.sourceSha256, + expectedGeneration: 1, + expectedRevision: 1, + future: true, + }), { code: "TASK_VALIDATION_FAILED" }); + assert.throws(() => missing.parseLinkedMissingState({ schemaVersion: 1, references: [current, current] }), { code: "TASK_VALIDATION_FAILED" }); + assert.throws(() => missing.parseLinkedLibraryReference({ ...current, status: "MISSING", placeholder: null }), { code: "TASK_VALIDATION_FAILED" }); + assert.throws(() => missing.parseLinkedLibraryReference({ ...current, status: "AVAILABLE", placeholder: { kind: "MISSING_LIBRARY", sourceLibraryId, dataBlockIds: current.dataBlockIds } }), { code: "TASK_VALIDATION_FAILED" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-linked-mutation.test.mjs b/web/tests/unit/library-linked-mutation.test.mjs new file mode 100644 index 00000000..0eaf11dc --- /dev/null +++ b/web/tests/unit/library-linked-mutation.test.mjs @@ -0,0 +1,60 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-linked-mutation-unit-")); +const transpile = (sourceName, outputName, replacements = []) => { + const sourcePath = path.join(root, "web/protocol", sourceName); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + fs.writeFileSync(path.join(temporary, outputName), replacements.reduce((source, [from, to]) => source.replaceAll(from, to), transpiled.outputText)); +}; +transpile("capability-gates.ts", "capability-gates.mjs"); +transpile("library-linked-mutation.ts", "library-linked-mutation.mjs", [["from \"./capability-gates\"", "from \"./capability-gates.mjs\""]]); +const linked = await import(pathToFileURL(path.join(temporary, "library-linked-mutation.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03G/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const base = (operation = "MESH_GEOMETRY") => ({ + schemaVersion: 1, + operation, + dataBlockId: "mesh:M12 Link Mesh", + baseRevision: 7, + owner: "SOURCE_LIBRARY", + linkedLibrary: true, + readOnly: true, +}); + +test("M12-03G binds the linked writer protocol and evidence artifacts", () => { + assert.equal(manifest.task, "M12-03G"); + assert.equal(manifest.parentTask, "M12-03F"); + assert.equal(manifest.nextTask, "M12-03H"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-03G blocks every linked data writer with the stable mutation code", () => { + assert.equal(linked.LINKED_DATA_WRITER_OPERATIONS.length, 6); + for (const operation of linked.LINKED_DATA_WRITER_OPERATIONS) { + const gate = linked.gateLinkedDataMutation(base(operation), 7); + assert.equal(gate.status, "BLOCKED"); + assert.deepEqual(gate.issues.map((issue) => issue.code), ["LINKED_DATA_MUTATION_BLOCKED"]); + assert.equal(gate.issues[0].recoverable, false); + assert.deepEqual(linked.parseLinkedDataMutation(base(operation)), { ...base(operation) }); + } +}); + +test("M12-03G rejects stale, malformed, and ownership-substituted writes before Main", () => { + assert.equal(linked.gateLinkedDataMutation(base(), 8).issues[0].code, "REVISION_CONFLICT"); + assert.equal(linked.gateLinkedDataMutation({ ...base(), owner: "LOCAL_MAIN", linkedLibrary: false, readOnly: false }, 7).issues[0].code, "LINKED_DATA_MUTATION_BLOCKED"); + assert.equal(linked.gateLinkedDataMutation({ ...base(), future: true }, 7).issues[0].code, "TASK_VALIDATION_FAILED"); + assert.equal(linked.gateLinkedDataMutation({ ...base(), operation: "UNKNOWN" }, 7).issues[0].code, "TASK_VALIDATION_FAILED"); +}); diff --git a/web/tests/unit/library-linked-reload.test.mjs b/web/tests/unit/library-linked-reload.test.mjs new file mode 100644 index 00000000..de063165 --- /dev/null +++ b/web/tests/unit/library-linked-reload.test.mjs @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-linked-reload-unit-")); +const sourcePath = path.join(root, "web/protocol/library-linked-reload.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-linked-reload.mjs"), transpiled.outputText); +const reload = await import(pathToFileURL(path.join(temporary, "library-linked-reload.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03H/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const library = (value) => `library:${String(value).repeat(64).slice(0, 64)}`; +const digest = (value) => crypto.createHash("sha256").update(String(value)).digest("hex"); +const snapshot = (sourceLibraryId, generation, revision, marker) => ({ + sourceLibraryId, + sourceGeneration: generation, + sourceRevision: revision, + dependencyClosureSha256: digest(marker), + graphSha256: digest(`${marker}-graph`), + dataBlocks: [ + { dataBlockId: `Object/${marker}`, owner: "SOURCE_LIBRARY", readOnly: true }, + { dataBlockId: `Mesh/${marker}`, owner: "SOURCE_LIBRARY", readOnly: true }, + ], +}); + +test("M12-03H binds the reload protocol and evidence artifacts", () => { + assert.equal(manifest.task, "M12-03H"); + assert.equal(manifest.parentTask, "M12-03G"); + assert.equal(manifest.nextTask, "M12-03I"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-03H replaces only the matching library generation and preserves every other snapshot", () => { + const sourceLibraryId = library("a"); + const otherLibraryId = library("b"); + const current = snapshot(sourceLibraryId, 3, 7, "old"); + const otherGeneration = snapshot(sourceLibraryId, 9, 2, "future"); + const otherLibrary = snapshot(otherLibraryId, 1, 4, "other"); + const state = { schemaVersion: 1, snapshots: [current, otherGeneration, otherLibrary] }; + const replacement = snapshot(sourceLibraryId, 4, 8, "new"); + const decision = reload.reloadMatchingLinkedSnapshot(state, { + schemaVersion: 1, + operation: "RELOAD", + sourceLibraryId, + expectedGeneration: 3, + expectedRevision: 7, + replacement, + }); + assert.equal(decision.status, "REPLACED"); + assert.equal(decision.code, null); + assert.deepEqual(decision.state.snapshots, [replacement, otherGeneration, otherLibrary]); + assert.deepEqual(state.snapshots, [current, otherGeneration, otherLibrary]); + assert(decision.state.snapshots.every((item) => item.dataBlocks.every((block) => block.owner === "SOURCE_LIBRARY" && block.readOnly))); +}); + +test("M12-03H rejects stale generations and keeps state byte-for-byte equivalent", () => { + const sourceLibraryId = library("c"); + const state = { schemaVersion: 1, snapshots: [snapshot(sourceLibraryId, 5, 11, "stable")] }; + const request = { + schemaVersion: 1, + operation: "RELOAD", + sourceLibraryId, + expectedGeneration: 4, + expectedRevision: 10, + replacement: snapshot(sourceLibraryId, 5, 12, "late"), + }; + const decision = reload.reloadMatchingLinkedSnapshot(state, request); + assert.deepEqual(decision, { + status: "STALE", + code: "REVISION_CONFLICT", + sourceLibraryId, + replacedGeneration: 4, + replacementGeneration: 5, + state, + }); +}); + +test("M12-03H fails closed for malformed, substituted, duplicate, and non-adjacent reloads", () => { + const sourceLibraryId = library("d"); + const current = snapshot(sourceLibraryId, 1, 1, "current"); + const baseRequest = { + schemaVersion: 1, + operation: "RELOAD", + sourceLibraryId, + expectedGeneration: 1, + expectedRevision: 1, + replacement: snapshot(sourceLibraryId, 2, 2, "replacement"), + }; + assert.throws(() => reload.parseLinkedReloadRequest({ ...baseRequest, future: true }), { code: "TASK_VALIDATION_FAILED" }); + assert.throws(() => reload.parseLinkedReloadRequest({ ...baseRequest, replacement: { ...baseRequest.replacement, sourceLibraryId: library("e") } }), { code: "TASK_VALIDATION_FAILED" }); + assert.throws(() => reload.parseLinkedReloadState({ schemaVersion: 1, snapshots: [current, current] }), { code: "TASK_VALIDATION_FAILED" }); + const decision = reload.reloadMatchingLinkedSnapshot({ schemaVersion: 1, snapshots: [current] }, { + ...baseRequest, + replacement: snapshot(sourceLibraryId, 3, 3, "skip"), + }); + assert.equal(decision.status, "STALE"); + assert.equal(decision.code, "REVISION_CONFLICT"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-metadata-first.test.mjs b/web/tests/unit/library-metadata-first.test.mjs new file mode 100644 index 00000000..72a06d9a --- /dev/null +++ b/web/tests/unit/library-metadata-first.test.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-metadata-first-unit-")); +const sourcePath = path.join(root, "web/protocol/archive-metadata-first.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "archive-metadata-first.mjs"), transpiled.outputText); +const metadata = await import(pathToFileURL(path.join(temporary, "archive-metadata-first.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04E/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const zip = { schemaVersion: 1, archiveId: "archive:zip-1", format: "ZIP", archiveByteLength: 4096, metadataOffset: 3072, metadataByteLength: 512 }; +const tar = { schemaVersion: 1, archiveId: "archive:tar-1", format: "TAR", archiveByteLength: 4096, metadataOffset: 0, metadataByteLength: 1024 }; + +test("M12-04E binds metadata-first planning and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04E"); + assert.equal(manifest.parentTask, "M12-04D"); + assert.equal(manifest.nextTask, "M12-04F"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04E plans ZIP central-directory and TAR manifest reads without payload ranges", () => { + assert.deepEqual(metadata.planArchiveMetadataRead(zip), { + status: "METADATA_ONLY", schemaVersion: 1, archiveId: "archive:zip-1", format: "ZIP", + firstRead: { kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 }, payloadReads: [], + }); + assert.deepEqual(metadata.planArchiveMetadataRead(tar).firstRead, { kind: "MANIFEST", byteOffset: 0, byteLength: 1024 }); +}); + +test("M12-04E accepts a trace only when metadata is the first exact read", () => { + assert.deepEqual(metadata.validateArchiveReadTrace(zip, { + schemaVersion: 1, archiveId: "archive:zip-1", reads: [ + { sequence: 0, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 }, + { sequence: 1, kind: "PAYLOAD", byteOffset: 32, byteLength: 128 }, + ], + }), { status: "VALID", metadataFirst: true, payloadReadsAfterMetadata: true }); + assert.deepEqual(metadata.validateArchiveReadTrace(tar, { + schemaVersion: 1, archiveId: "archive:tar-1", reads: [{ sequence: 0, kind: "MANIFEST", byteOffset: 0, byteLength: 1024 }], + }).metadataFirst, true); +}); + +test("M12-04E rejects payload-first, wrong-range, duplicate-metadata and invalid ranges", () => { + assert.throws(() => metadata.validateArchiveReadTrace(zip, { schemaVersion: 1, archiveId: "archive:zip-1", reads: [ + { sequence: 0, kind: "PAYLOAD", byteOffset: 0, byteLength: 16 }, + { sequence: 1, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 }, + ] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => metadata.validateArchiveReadTrace(zip, { schemaVersion: 1, archiveId: "archive:zip-1", reads: [{ sequence: 0, kind: "CENTRAL_DIRECTORY", byteOffset: 3000, byteLength: 512 }] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => metadata.validateArchiveReadTrace(zip, { schemaVersion: 1, archiveId: "archive:zip-1", reads: [ + { sequence: 0, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 }, + { sequence: 1, kind: "CENTRAL_DIRECTORY", byteOffset: 3072, byteLength: 512 }, + ] }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => metadata.planArchiveMetadataRead({ ...zip, metadataOffset: 4000, metadataByteLength: 200 }), { code: "IO_ARCHIVE_UNSAFE" }); + assert.throws(() => metadata.planArchiveMetadataRead({ ...zip, metadataByteLength: 64 * 1024 * 1024 + 1 }), { code: "IO_ARCHIVE_UNSAFE" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-negative-cases.test.mjs b/web/tests/unit/library-negative-cases.test.mjs new file mode 100644 index 00000000..3958d667 --- /dev/null +++ b/web/tests/unit/library-negative-cases.test.mjs @@ -0,0 +1,56 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-negative-cases-unit-")); +const sourcePath = path.join(root, "web/protocol/library-negative-cases.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-negative-cases.mjs"), transpiled.outputText); +const negatives = await import(pathToFileURL(path.join(temporary, "library-negative-cases.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03M/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const a = "library:" + "a".repeat(64); +const b = "library:" + "b".repeat(64); +const c = "library:" + "c".repeat(64); +const valid = { + schemaVersion: 1, + libraries: [{ libraryId: a, dependencyIds: [b] }, { libraryId: b, dependencyIds: [] }, { libraryId: c, dependencyIds: [] }], + dataBlocks: [{ dataBlockId: "Object/A", sourceLibraryId: a }, { dataBlockId: "Object/B", sourceLibraryId: b }], + crossReferences: [], + reloads: [{ sourceLibraryId: a, generation: 1 }, { sourceLibraryId: a, generation: 2 }], +}; + +test("M12-03M binds the library negative-case protocol and evidence artifacts", () => { + assert.equal(manifest.task, "M12-03M"); + assert.equal(manifest.parentTask, "M12-03L"); + assert.equal(manifest.nextTask, "M12-03N"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-03M accepts an acyclic graph with unique data-block and reload identities", () => { + assert.deepEqual(negatives.validateLibraryNegativeInput(valid), { status: "VALID" }); +}); + +test("M12-03M rejects dependency and cross-library cycles", () => { + assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, libraries: [{ libraryId: a, dependencyIds: [b] }, { libraryId: b, dependencyIds: [a] }, { libraryId: c, dependencyIds: [] }] }), { code: "LIBRARY_DEPENDENCY_CYCLE" }); + assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, crossReferences: [{ fromLibraryId: a, toLibraryId: b }, { fromLibraryId: b, toLibraryId: a }] }), { code: "LIBRARY_DEPENDENCY_CYCLE" }); +}); + +test("M12-03M rejects ID collision, duplicate reload and missing library references", () => { + assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, dataBlocks: [{ dataBlockId: "Object/A", sourceLibraryId: a }, { dataBlockId: "Object/A", sourceLibraryId: b }] }), { code: "TASK_VALIDATION_FAILED" }); + assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, reloads: [{ sourceLibraryId: a, generation: 1 }, { sourceLibraryId: a, generation: 1 }] }), { code: "REVISION_CONFLICT" }); + assert.throws(() => negatives.validateLibraryNegativeInput({ ...valid, dataBlocks: [{ dataBlockId: "Object/A", sourceLibraryId: "library:" + "d".repeat(64) }] }), { code: "ASSET_SOURCE_HASH_MISMATCH" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-override-freshness.test.mjs b/web/tests/unit/library-override-freshness.test.mjs new file mode 100644 index 00000000..2bde1c2e --- /dev/null +++ b/web/tests/unit/library-override-freshness.test.mjs @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-override-freshness-unit-")); +const sourcePath = path.join(root, "web/protocol/library-override-freshness.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-override-freshness.mjs"), transpiled.outputText); +const freshness = await import(pathToFileURL(path.join(temporary, "library-override-freshness.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03L/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const library = "library:" + "a".repeat(64); +const digest = "b".repeat(64); +const token = "override-token:" + "c".repeat(64); +const state = (revision = 7) => ({ + schemaVersion: 1, + sourceLibraryId: library, + sourceGeneration: 4, + sourceRevision: revision, + dependencyClosureSha256: digest, + invalidationToken: token, + localDataBlockId: "Object/M12 Override Object", + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, +}); +const request = (overrides = {}) => ({ + schemaVersion: 1, + operation: "COMMIT_OVERRIDE", + sourceLibraryId: library, + sourceGeneration: 4, + sourceRevision: 7, + dependencyClosureSha256: digest, + invalidationToken: token, + baseRevision: 7, + localDataBlockId: "Object/M12 Override Object", + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + ...overrides, +}); + +test("M12-03L binds the override freshness gate and evidence artifacts", () => { + assert.equal(manifest.task, "M12-03L"); + assert.equal(manifest.parentTask, "M12-03K"); + assert.equal(manifest.nextTask, "M12-03M"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-03L allows only a fully matching source generation/revision commit", () => { + assert.deepEqual(freshness.gateOverrideFreshness(state(), request()), { status: "READY", code: null }); +}); + +test("M12-03L blocks stale generation, revision, closure, token, and identity before Main", () => { + for (const overrides of [ + { sourceGeneration: 3 }, + { sourceRevision: 6, baseRevision: 6 }, + { dependencyClosureSha256: "d".repeat(64) }, + { invalidationToken: "override-token:" + "e".repeat(64) }, + ]) assert.deepEqual(freshness.gateOverrideFreshness(state(), request(overrides)), { status: "BLOCKED", code: "REVISION_CONFLICT" }); + assert.deepEqual(freshness.gateOverrideFreshness(state(), request({ localDataBlockId: "Object/Other" })), { status: "BLOCKED", code: "ASSET_SOURCE_HASH_MISMATCH" }); +}); + +test("M12-03L rejects linked ownership, alternate operations, malformed tokens, and extra fields", () => { + assert.equal(freshness.gateOverrideFreshness(state(), request({ owner: "SOURCE_LIBRARY", readOnly: true })).code, "LINKED_DATA_MUTATION_BLOCKED"); + assert.equal(freshness.gateOverrideFreshness(state(), request({ operation: "SET_LOCATION" })).code, "TASK_VALIDATION_FAILED"); + assert.equal(freshness.gateOverrideFreshness(state(), request({ invalidationToken: "bad" })).code, "TASK_VALIDATION_FAILED"); + assert.equal(freshness.gateOverrideFreshness(state(), { ...request(), future: true }).code, "TASK_VALIDATION_FAILED"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-override-writer.test.mjs b/web/tests/unit/library-override-writer.test.mjs new file mode 100644 index 00000000..fd4cc942 --- /dev/null +++ b/web/tests/unit/library-override-writer.test.mjs @@ -0,0 +1,82 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-override-writer-unit-")); +const sourcePath = path.join(root, "web/protocol/library-override-writer.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +fs.writeFileSync(path.join(temporary, "library-override-writer.mjs"), transpiled.outputText); +const writer = await import(pathToFileURL(path.join(temporary, "library-override-writer.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03K/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const state = (revision = 3, value = 2.5) => ({ + schemaVersion: 1, + revision, + localDataBlockId: "Object/M12 Override Object", + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + propertyPath: '["m12_override_value"]', + value, +}); +const request = (baseRevision = 3, value = 4.5) => ({ + schemaVersion: 1, + operation: "SET_M12_OVERRIDE_VALUE", + baseRevision, + localDataBlockId: "Object/M12 Override Object", + referenceSourceDataBlockId: "Object/M12 Override Object", + hierarchyRootDataBlockId: "Object/M12 Override Object", + owner: "LOCAL_OVERRIDE", + readOnly: false, + referenceReadOnly: true, + propertyPath: '["m12_override_value"]', + value, +}); + +test("M12-03K binds the single-property writer and evidence artifacts", () => { + assert.equal(manifest.task, "M12-03K"); + assert.equal(manifest.parentTask, "M12-03J"); + assert.equal(manifest.nextTask, "M12-03L"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-03K applies exactly the verified override property and advances one revision", () => { + const result = writer.applyOverrideWriter(state(), request()); + assert.equal(result.status, "APPLIED"); + assert.equal(result.code, null); + assert.equal(result.state.revision, 4); + assert.equal(result.state.value, 4.5); + assert.equal(result.state.propertyPath, writer.LIBRARY_OVERRIDE_PROPERTY_PATH); + assert.equal(result.state.owner, "LOCAL_OVERRIDE"); + assert.equal(result.state.referenceReadOnly, true); +}); + +test("M12-03K blocks stale, linked-owner, identity, and second-property writes", () => { + assert.equal(writer.applyOverrideWriter(state(3), request(2)).code, "REVISION_CONFLICT"); + assert.equal(writer.applyOverrideWriter(state(), { ...request(), owner: "SOURCE_LIBRARY", readOnly: true, referenceReadOnly: true }).code, "LINKED_DATA_MUTATION_BLOCKED"); + assert.equal(writer.applyOverrideWriter(state(), { ...request(), localDataBlockId: "Object/Other" }).code, "ASSET_SOURCE_HASH_MISMATCH"); + assert.equal(writer.applyOverrideWriter(state(), { ...request(), propertyPath: "location" }).code, "EDITOR_WRITER_UNAVAILABLE"); + assert.equal(writer.applyOverrideWriter(state(), { ...request(), operation: "SET_LOCATION" }).code, "TASK_VALIDATION_FAILED"); +}); + +test("M12-03K rejects malformed values and undeclared fields before the writer", () => { + assert.equal(writer.applyOverrideWriter(state(), { ...request(), future: true }).code, "TASK_VALIDATION_FAILED"); + assert.equal(writer.applyOverrideWriter(state(), { ...request(), value: Number.NaN }).code, "TASK_VALIDATION_FAILED"); + assert.throws(() => writer.parseOverrideWriterState({ ...state(), propertyPath: "location" }), { code: "EDITOR_WRITER_UNAVAILABLE" }); + assert.throws(() => writer.parseOverrideWriterRequest({ ...request(), value: 1e9 }), { code: "TASK_VALIDATION_FAILED" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-path-normalization.test.mjs b/web/tests/unit/library-path-normalization.test.mjs new file mode 100644 index 00000000..e72b3380 --- /dev/null +++ b/web/tests/unit/library-path-normalization.test.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-path-normalization-unit-")); +for (const name of ["asset-path.ts", "library-source-origin.ts"]) { + const sourcePath = path.join(root, "web/protocol", name); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), transpiled.outputText.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"')); +} +const paths = await import(pathToFileURL(path.join(temporary, "asset-path.mjs"))); +const origin = await import(pathToFileURL(path.join(temporary, "library-source-origin.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04B/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const policy = { schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test"] }; + +test("M12-04B binds the shared path normalizer and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04B"); + assert.equal(manifest.nextTask, "M12-04C"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04B canonicalizes POSIX and Windows separators with dot segments", () => { + const aliases = [ + "libraries/characters/main.blend", + "libraries\\characters\\.\\hero\\..\\main.blend", + "libraries//characters/models/../main.blend", + "//libraries/characters/./main.blend", + ]; + for (const alias of aliases) assert.equal(paths.normalizeProjectAssetPath(alias), "libraries/characters/main.blend"); + assert.equal(paths.normalizeProjectAssetPath(paths.normalizeProjectAssetPath(aliases[1])), "libraries/characters/main.blend"); +}); + +test("M12-04B decodes percent octets and NFC-normalizes Unicode names", () => { + const canonical = "libraries/角色/caf\u00e9.blend"; + const aliases = [ + "libraries/%E8%A7%92%E8%89%B2/caf%65%CC%81.blend", + "libraries%2F%E8%A7%92%E8%89%B2%5Ccafe%CC%81.blend", + "libraries/角色/cafe\u0301.blend", + ]; + for (const alias of aliases) assert.equal(paths.normalizeProjectAssetPath(alias), canonical); + assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "PROJECT_ASSET", path: aliases[1] }), { + status: "READY", + kind: "PROJECT_ASSET", + canonicalLocator: `project-assets/${canonical}`, + }); +}); + +test("M12-04B resolves encoded dot segments once and fails closed on escape or re-decoding", () => { + assert.equal(paths.normalizeProjectAssetPath("libraries/temp/%2E%2E/main.blend"), "libraries/main.blend"); + assert.throws(() => paths.normalizeProjectAssetPath("libraries/%2E%2E/%2E%2E/outside.blend"), /ASSET_PATH_OUTSIDE_PROJECT/); + assert.throws(() => paths.normalizeProjectAssetPath("libraries/%252E%252E/outside.blend"), /ASSET_PATH_OUTSIDE_PROJECT/); + assert.throws(() => paths.normalizeProjectAssetPath("libraries/%GG/outside.blend"), /ASSET_PATH_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-path-security.test.mjs b/web/tests/unit/library-path-security.test.mjs new file mode 100644 index 00000000..cb4d2f4b --- /dev/null +++ b/web/tests/unit/library-path-security.test.mjs @@ -0,0 +1,84 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-path-security-unit-")); +for (const name of ["asset-path.ts", "library-source-origin.ts"]) { + const sourcePath = path.join(root, "web/protocol", name); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), transpiled.outputText.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"')); +} +const paths = await import(pathToFileURL(path.join(temporary, "asset-path.mjs"))); +const origin = await import(pathToFileURL(path.join(temporary, "library-source-origin.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04C/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const policy = { schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test"] }; +const source = (pathValue) => ({ schemaVersion: 1, kind: "PROJECT_ASSET", path: pathValue }); +const remote = (url) => ({ schemaVersion: 1, kind: "HTTPS_ORIGIN", url }); + +test("M12-04C binds the path security gate and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04C"); + assert.equal(manifest.parentTask, "M12-04B"); + assert.equal(manifest.nextTask, "M12-04D"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04C rejects absolute, UNC, drive, NUL, control, and origin-style project paths", () => { + for (const value of [ + "/tmp/library.blend", + "\\\\server\\share\\library.blend", + "\\server\\library.blend", + "C:\\libraries\\main.blend", + "C:/libraries/main.blend", + "libraries/\u0000main.blend", + "libraries/\u0001main.blend", + "libraries/%00main.blend", + "//https://evil.example.test/library.blend", + ]) assert.throws(() => paths.normalizeProjectAssetPath(value), /ASSET_PATH_OUTSIDE_PROJECT|ASSET_PATH_INVALID/); + for (const value of ["/tmp/library.blend", "\\\\server\\share\\library.blend", "C:/libraries/main.blend", "libraries/\u0000main.blend", "libraries/%00main.blend"]) { + assert.throws(() => origin.acceptLibrarySource(policy, source(value)), { code: "IO_EXTERNAL_URI_BLOCKED" }); + } +}); + +test("M12-04C rejects raw and encoded unsafe HTTPS URI characters before admission", () => { + for (const value of [ + "https://assets.example.test\\evil.example.test/main.blend", + "https://assets.example.test/\nmain.blend", + "https://assets.example.test/%00main.blend", + "https://assets.example.test/%01main.blend", + "https://assets.example.test/%GGmain.blend", + "https://user:pass@evil.example.test/main.blend", + "https://evil.example.test/main.blend", + ]) assert.throws(() => origin.acceptLibrarySource(policy, remote(value)), { code: "IO_EXTERNAL_URI_BLOCKED" }); + assert.deepEqual(origin.acceptLibrarySource(policy, remote("https://assets.example.test/library/main.blend")), { + status: "READY", + kind: "HTTPS_ORIGIN", + canonicalLocator: "https://assets.example.test/library/main.blend", + }); +}); + +test("M12-04C fails closed on policy origin smuggling and duplicate declarations", () => { + for (const value of [ + "https://assets.example.test\\evil.example.test", + "https://assets.example.test/%00", + "https://assets.example.test/%2e", + "https://assets.example.test/..", + "https://assets.example.test/library", + "https://assets.example.test/?scope=library", + "https://assets.example.test/#library", + ]) assert.throws(() => origin.parseLibrarySourcePolicy({ schemaVersion: 1, declaredHttpsOrigins: [value] }), { code: "IO_EXTERNAL_URI_BLOCKED" }); + assert.throws(() => origin.parseLibrarySourcePolicy({ schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test", "https://assets.example.test/"] }), { code: "ASSET_MANIFEST_INVALID" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/library-source-origin.test.mjs b/web/tests/unit/library-source-origin.test.mjs new file mode 100644 index 00000000..092640fd --- /dev/null +++ b/web/tests/unit/library-source-origin.test.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-source-origin-unit-")); +const paths = ["asset-path.ts", "library-source-origin.ts"]; +for (const name of paths) { + const sourcePath = path.join(root, "web/protocol", name); + const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(transpiled.diagnostics, []); + fs.writeFileSync(path.join(temporary, name.replace(".ts", ".mjs")), transpiled.outputText.replaceAll('from "./asset-path"', 'from "./asset-path.mjs"')); +} +const origin = await import(pathToFileURL(path.join(temporary, "library-source-origin.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-04A/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const policy = { schemaVersion: 1, declaredHttpsOrigins: ["https://assets.example.test"] }; +const digest = "a".repeat(64); + +test("M12-04A binds the declared source-origin protocol and evidence artifacts", () => { + assert.equal(manifest.task, "M12-04A"); + assert.equal(manifest.nextTask, "M12-04B"); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); + +test("M12-04A accepts declared HTTPS, project asset, and user-selected file sources", () => { + assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "HTTPS_ORIGIN", url: "https://assets.example.test/library/main.blend" }), { status: "READY", kind: "HTTPS_ORIGIN", canonicalLocator: "https://assets.example.test/library/main.blend" }); + assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "PROJECT_ASSET", path: "libraries/main.blend" }), { status: "READY", kind: "PROJECT_ASSET", canonicalLocator: "project-assets/libraries/main.blend" }); + assert.deepEqual(origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "USER_SELECTED_FILE", selectionId: "file-selection:pick-1", fileName: "main.blend", byteLength: 128, sourceSha256: digest }), { status: "READY", kind: "USER_SELECTED_FILE", canonicalLocator: "user-file/file-selection:pick-1/main.blend" }); +}); + +test("M12-04A rejects undeclared origins, credentials, unsafe paths, and missing policy declarations", () => { + assert.throws(() => origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "HTTPS_ORIGIN", url: "https://other.example.test/main.blend" }), { code: "IO_EXTERNAL_URI_BLOCKED" }); + assert.throws(() => origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "HTTPS_ORIGIN", url: "https://user:pass@assets.example.test/main.blend" }), { code: "IO_EXTERNAL_URI_BLOCKED" }); + assert.throws(() => origin.acceptLibrarySource(policy, { schemaVersion: 1, kind: "PROJECT_ASSET", path: "../outside.blend" }), { code: "IO_EXTERNAL_URI_BLOCKED" }); + assert.throws(() => origin.parseLibrarySourcePolicy({ schemaVersion: 1, declaredHttpsOrigins: [] }), { code: "PROTOCOL_MISMATCH" }); +}); + +test("M12-04A rejects malformed user-file identity and undeclared fields", () => { + assert.throws(() => origin.parseLibrarySourceRequest({ schemaVersion: 1, kind: "USER_SELECTED_FILE", selectionId: "bad", fileName: "main.blend", byteLength: 1, sourceSha256: digest }), { code: "ASSET_MANIFEST_INVALID" }); + assert.throws(() => origin.parseLibrarySourceRequest({ schemaVersion: 1, kind: "USER_SELECTED_FILE", selectionId: "file-selection:pick-1", fileName: "../main.blend", byteLength: 1, sourceSha256: digest }), { code: "ASSET_MANIFEST_INVALID" }); + assert.throws(() => origin.parseLibrarySourceRequest({ schemaVersion: 1, kind: "PROJECT_ASSET", path: "main.blend", future: true }), { code: "ASSET_MANIFEST_INVALID" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/obj-import.test.mjs b/web/tests/unit/obj-import.test.mjs new file mode 100644 index 00000000..62220b29 --- /dev/null +++ b/web/tests/unit/obj-import.test.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "obj-import-unit-")); +const sourcePath = path.join(root, "web/protocol/obj-import.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "obj-import.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); + +const obj = new TextEncoder().encode(`# test\nmtllib materials.mtl\no Test\nv 0 0 0\nv 1 0 0\nv 0 1 0\nvt 0 0\nvt 1 0\nvt 0 1\nvn 0 0 1\ng TestGroup\nusemtl TestMaterial\nf -3/-3/-1 -2/-2/-1 -1/-1/-1\n`).buffer; +const mtl = new TextEncoder().encode("newmtl TestMaterial\nmap_Kd texture.png\n").buffer; + +test("M12-07C resolves negative indices and serializes deterministic OBJ", () => { + const imported = protocol.importOBJ(obj, mtl); + assert.deepEqual(imported.faces[0].vertices.map((vertex) => vertex.position), [1, 2, 3]); + assert.deepEqual(imported.faces[0].vertices.map((vertex) => vertex.texcoord), [1, 2, 3]); + assert.deepEqual(imported.materials, [{ name: "TestMaterial", mapKd: "texture.png" }]); + const serialized = protocol.serializeOBJ(imported); + assert.match(serialized.obj, /f 1\/1\/1 2\/2\/1 3\/3\/1/); + assert.equal(serialized.mtl, "# Web Blender MTL export\n# schema 1\nnewmtl TestMaterial\nmap_Kd texture.png\n"); +}); + +test("M12-07C reports unresolved texture origin without blocking geometry", () => { + const imported = protocol.importOBJ(obj, mtl); + const missing = protocol.createOBJLossReport(imported); + assert.equal(missing.canRoundTrip, true); + assert.deepEqual(missing.warnings.map((warning) => warning.code), ["OBJ_TEXTURE_ORIGIN_UNRESOLVED"]); + const bound = protocol.createOBJLossReport(imported, ["texture.png"]); + assert.deepEqual(bound.warnings, []); +}); + +test("M12-07C rejects malformed face arity and out-of-range indices", () => { + const malformed = new TextEncoder().encode("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2\n").buffer; + assert.throws(() => protocol.importOBJ(malformed), /OBJ_FACE_ARITY_INVALID/); + const outOfRange = new TextEncoder().encode("v 0 0 0\nv 1 0 0\nv 0 1 0\nf 1 2 4\n").buffer; + assert.throws(() => protocol.importOBJ(outOfRange), /OBJ_INDEX_OUT_OF_RANGE/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/ply-import.test.mjs b/web/tests/unit/ply-import.test.mjs new file mode 100644 index 00000000..2b7d3d99 --- /dev/null +++ b/web/tests/unit/ply-import.test.mjs @@ -0,0 +1,51 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "ply-import-unit-")); +const sourcePath = path.join(root, "web/protocol/ply-import.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "ply-import.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_mapping_v1"); +const bytes = (name) => { const value = fs.readFileSync(path.join(fixtureRoot, name)); return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); }; + +test("M12-07H maps ASCII and binary little-endian PLY fields", () => { + const ascii = protocol.importPLY(bytes("mapping-ascii.ply"), { format: "ascii" }); + const binary = protocol.importPLY(bytes("mapping-binary-le.ply"), { format: "binary_little_endian" }); + assert.equal(ascii.vertices.length, 4); + assert.equal(ascii.faces.length, 2); + assert.deepEqual(ascii.vertices[0].position, [-1, 0, 1]); + assert.deepEqual(ascii.vertices[0].normal, [0, 1, 0]); + assert.deepEqual(ascii.vertices[0].color, [254 / 255, 0, 0, 1]); + assert.deepEqual(ascii.vertices[3].customProperties, { label: 4, temperature: 40 }); + assert.deepEqual(binary.vertices, ascii.vertices); + assert.deepEqual(binary.faces, ascii.faces); + assert.deepEqual(protocol.createPLYLossReport(ascii), { schemaVersion: 1, operation: "PLY_IMPORT_LOSS_REPORT", canImport: true, warningCount: 0, warnings: [] }); +}); + +test("M12-07H reports unknown list properties without dropping mapped fields", () => { + const imported = protocol.importPLY(bytes("unknown-property-ascii.ply"), { format: "ascii" }); + assert.equal(imported.vertices.length, 4); + assert.equal(imported.vertices[0].customProperties.temperature, 10); + assert.deepEqual(protocol.createPLYLossReport(imported).warnings.map((warning) => warning.code), ["PLY_UNKNOWN_PROPERTY"]); + assert.equal(protocol.createPLYLossReport(imported).warnings[0].property, "unknown_values"); +}); + +test("M12-07H serializes mapped data and rejects an explicit format mismatch", () => { + const document = protocol.importPLY(bytes("mapping-ascii.ply")); + const output = protocol.serializePLYAscii(document); + const reopened = protocol.importPLY(output, { format: "ascii" }); + assert.deepEqual(reopened.faces, document.faces); + assert.deepEqual(reopened.vertices.map((vertex) => vertex.customProperties), document.vertices.map((vertex) => vertex.customProperties)); + assert.throws(() => protocol.importPLY(bytes("mapping-ascii.ply"), { format: "binary_little_endian" }), /PLY_FORMAT_MISMATCH/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/ply-negative.test.mjs b/web/tests/unit/ply-negative.test.mjs new file mode 100644 index 00000000..f45415e5 --- /dev/null +++ b/web/tests/unit/ply-negative.test.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "ply-negative-unit-")); +const sourcePath = path.join(root, "web/protocol/ply-import.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "ply-import.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const fixtureRoot = path.join(root, "tests/files/web/m12_ply_negative_v1"); +const bytes = (name) => { const value = fs.readFileSync(path.join(fixtureRoot, name)); return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); }; + +test("M12-07I blocks big-endian PLY with a stable format code", () => { + assert.throws(() => protocol.importPLY(bytes("big-endian.ply")), /PLY_FORMAT_UNSUPPORTED/); +}); + +test("M12-07I blocks malformed lists and oversized element counts", () => { + assert.throws(() => protocol.importPLY(bytes("malformed-list-ascii.ply")), /PLY_DATA_TRUNCATED/); + assert.throws(() => protocol.importPLY(bytes("oversized-count-ascii.ply")), /PLY_IMPORT_BUDGET_EXCEEDED: vertex/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/pointer-contract.test.mjs b/web/tests/unit/pointer-contract.test.mjs new file mode 100644 index 00000000..ad26a594 --- /dev/null +++ b/web/tests/unit/pointer-contract.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "../../node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(root, "web/protocol/pointer-contract.ts"); +const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const pointer = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); + +test("M14-04C preserves pointer identity and bounded pen fields", () => { + assert.deepEqual(pointer.observePointerEvent({ type: "pointerdown", pointerType: "pen", pointerId: 7, pressure: 1.4, tiltX: 120, tiltY: -100, button: 0, buttons: 1 }), { schemaVersion: 1, pointerType: "pen", pointerId: 7, pressure: 1, tiltX: 90, tiltY: -90, button: 0, buttons: 1, cancelled: false }); + assert.equal(pointer.observePointerEvent({ type: "pointercancel", pointerType: "touch", pointerId: 2, pressure: 0.4, button: 0, buttons: 0 }).cancelled, true); +}); + +test("M14-04C rejects unknown pointer and invalid id", () => { + assert.throws(() => pointer.observePointerEvent({ pointerType: "trackpad", pointerId: 1 }), /POINTER_TYPE_UNSUPPORTED/); + assert.throws(() => pointer.observePointerEvent({ pointerType: "mouse", pointerId: -1 }), /POINTER_ID_INVALID/); +}); diff --git a/web/tests/unit/script-audit-integrity.test.mjs b/web/tests/unit/script-audit-integrity.test.mjs new file mode 100644 index 00000000..c818a26b --- /dev/null +++ b/web/tests/unit/script-audit-integrity.test.mjs @@ -0,0 +1,46 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-audit-integrity-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const digest = "a".repeat(64); +const manifest = { schemaVersion: 1, scripts: [{ id: "script:audit", name: "Audit", entryPath: "scripts/audit.py", sourceByteLength: 32, sourceSha256: digest, publisher: "local", signature: "b".repeat(128), keyId: "key:local", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] }; + +test("M13-05H accepts a strictly ordered, chained audit log", async () => { + const first = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:first", requestedAt: "2026-08-19T00:00:00.000Z" }); + const second = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:second", requestedAt: "2026-08-19T00:00:01.000Z" }); + const log = await protocol.appendScriptExecutionAudit(await protocol.appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, first), second); + assert.deepEqual(log.entries.map((entry) => entry.sequence), [1, 2]); + assert.deepEqual(log.entries.map((entry) => entry.audit.requestId), ["audit:first", "audit:second"]); + assert.equal(log.entries[0].previousEntrySha256, null); + assert.equal(log.entries[1].previousEntrySha256, log.entries[0].entrySha256); + assert.deepEqual((await protocol.parseScriptExecutionAuditLog(log)).entries, log.entries); +}); + +test("M13-05H rejects replay, time, sequence and hash-chain drift", async () => { + const first = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:one", requestedAt: "2026-08-19T00:00:00.000Z" }); + const second = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:two", requestedAt: "2026-08-19T00:00:01.000Z" }); + const log = await protocol.appendScriptExecutionAudit(await protocol.appendScriptExecutionAudit({ schemaVersion: 1, entries: [] }, first), second); + await assert.rejects(protocol.appendScriptExecutionAudit(log, first), /SCRIPT_MANIFEST_INVALID/); + const earlier = await protocol.createScriptExecutionAudit(manifest, "script:audit", new Set(), { requestId: "audit:earlier", requestedAt: "2026-08-18T23:59:59.000Z" }); + await assert.rejects(protocol.appendScriptExecutionAudit(log, earlier), /SCRIPT_MANIFEST_INVALID/); + await assert.rejects(protocol.parseScriptExecutionAuditLog({ ...log, entries: [{ ...log.entries[0], sequence: 2 }, log.entries[1]] }), /SCRIPT_MANIFEST_INVALID/); + await assert.rejects(protocol.parseScriptExecutionAuditLog({ ...log, entries: [{ ...log.entries[0], entrySha256: "c".repeat(64) }, log.entries[1]] }), /SCRIPT_MANIFEST_INVALID/); + await assert.rejects(protocol.parseScriptExecutionAuditLog({ ...log, entries: [log.entries[0], { ...log.entries[1], previousEntrySha256: "d".repeat(64) }] }), /SCRIPT_MANIFEST_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-host-call.test.mjs b/web/tests/unit/script-host-call.test.mjs new file mode 100644 index 00000000..e4fe8d74 --- /dev/null +++ b/web/tests/unit/script-host-call.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-host-call-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const digest = "a".repeat(64); +const permissions = new Set(protocol.SCRIPT_PERMISSIONS); +const call = (name, parameters) => ({ schemaVersion: 1, requestId: `host:${name.toLowerCase()}`, scriptId: "clean", call: name, permission: name, parameters }); + +test("M13-03C parses all allowlisted host calls with structured parameters", () => { + const inputs = [ + call("READ_MAIN", { revision: 3 }), + call("READ_ASSET", { path: "//assets/model.bin", expectedSha256: digest }), + call("WRITE_MAIN", { revision: 3, operation: "object.transform", payload: { objectId: "obj:1", x: 1 } }), + call("WRITE_ASSET", { path: "assets/out.bin", byteLength: 4, sha256: digest }), + call("SUBMIT_SERVER_JOB", { inputBlendSha256: digest, settingsSha256: digest }), + ]; + const parsed = inputs.map((input) => protocol.parseScriptHostCall(input, permissions)); + assert.deepEqual(parsed.map((item) => item.call), ["READ_MAIN", "READ_ASSET", "WRITE_MAIN", "WRITE_ASSET", "SUBMIT_SERVER_JOB"]); + assert.equal(parsed[1].parameters.path, "assets/model.bin"); + assert.equal(parsed[2].execution, "DISABLED"); +}); + +test("M13-03C rejects non-allowlisted calls, permission confusion and unknown fields", () => { + assert.throws(() => protocol.parseScriptHostCall(call("EXECUTE", {}), permissions), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptHostCall({ ...call("READ_MAIN", { revision: 3 }), permission: "WRITE_MAIN" }, permissions), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptHostCall(call("READ_MAIN", { revision: 3, extra: true }), permissions), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptHostCall(call("READ_ASSET", { path: "../escape", expectedSha256: digest }), permissions), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptHostCall(call("WRITE_MAIN", { revision: 3, operation: "x", payload: [] }), permissions), /SCRIPT_MANIFEST_INVALID/); +}); + +test("M13-03C requires the declared permission set", () => { + assert.throws(() => protocol.parseScriptHostCall(call("WRITE_MAIN", { revision: 0, operation: "x", payload: {} }), new Set(["READ_MAIN"])), /SCRIPT_POLICY_DENIED/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-manifest-budgets.test.mjs b/web/tests/unit/script-manifest-budgets.test.mjs new file mode 100644 index 00000000..5bb4249a --- /dev/null +++ b/web/tests/unit/script-manifest-budgets.test.mjs @@ -0,0 +1,85 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-manifest-budgets-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + fileName: `${name}.ts`, + }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const script = (id, overrides = {}) => ({ + id, + name: id, + entryPath: `scripts/${id}.py`, + sourceByteLength: 128, + sourceSha256: digest, + publisher: "local", + signature, + keyId: "key:local", + permissions: ["READ_MAIN"], + dependencies: [], + module: false, + cpuMs: 1000, + memoryBytes: 1024 * 1024, + wallMs: 5000, + network: false, + autorun: false, + driverExpressions: false, + addonInstall: false, + ...overrides, +}); +const manifest = (scripts = [script("clean")]) => ({ schemaVersion: 1, scripts }); + +test("M13-02A accepts bounded manifest fields and normalizes project paths", () => { + const parsed = protocol.parseScriptingManifest(manifest([ + script("base", { entryPath: "//scripts/../scripts/base.py" }), + script("clean", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//deps/base.py" }] }), + ])); + assert.equal(parsed.scripts.length, 2); + assert.equal(parsed.scripts[0].entryPath, "scripts/base.py"); + assert.equal(parsed.scripts[1].dependencies[0].sourcePath, "deps/base.py"); + assert.equal(parsed.scripts.reduce((total, item) => total + item.sourceByteLength, 0), 256); + assert.equal(parsed.scripts.every((item) => item.module === false), true); +}); + +test("M13-02A rejects text count and aggregate source byte budget overflow", () => { + assert.throws( + () => protocol.parseScriptingManifest(manifest(Array.from({ length: protocol.SCRIPTING_BUDGET.maxScripts + 1 }, (_, index) => script(`script-${index}`)))), + /SCRIPT_BUDGET_EXCEEDED/, + ); + assert.throws( + () => protocol.parseScriptingManifest(manifest([script("large", { sourceByteLength: protocol.SCRIPTING_BUDGET.maxSourceBytes }), script("overflow", { sourceByteLength: 1 })])), + /SCRIPT_BUDGET_EXCEEDED/, + ); + assert.throws(() => protocol.parseScriptingManifest(manifest([script("missing-bytes", { sourceByteLength: undefined })])), /SCRIPT_BUDGET_EXCEEDED/); +}); + +test("M13-02A rejects module execution, unsafe paths, and malformed dependencies", () => { + assert.throws(() => protocol.parseScriptingManifest(manifest([script("module", { module: true })])), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptingManifest(manifest([script("escape", { entryPath: "../escape.py" })])), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptingManifest(manifest([script("dependency-escape", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "//../escape.py" }] }), script("base")])), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptingManifest(manifest([script("duplicate-dependency", { dependencies: [{ id: "base", sourceSha256: digest, sourcePath: "deps/a.py" }, { id: "base", sourceSha256: digest, sourcePath: "deps/b.py" }] }), script("base")])), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptingManifest(manifest([script("too-many-dependencies", { dependencies: Array.from({ length: protocol.SCRIPTING_BUDGET.maxDependencies + 1 }, (_, index) => ({ id: `dep-${index}`, sourceSha256: digest, sourcePath: `deps/${index}.py` })) })])), /SCRIPT_BUDGET_EXCEEDED/); +}); + +test("M13-02A rejects unknown or over-budget permissions", () => { + assert.throws(() => protocol.parseScriptingManifest(manifest([script("unknown-permission", { permissions: ["EXECUTE"] })])), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptingManifest(manifest([script("permission-budget", { permissions: new Array(protocol.SCRIPTING_BUDGET.maxPermissions + 1).fill("READ_MAIN") })])), /SCRIPT_POLICY_DENIED/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-manifest-canonical.test.mjs b/web/tests/unit/script-manifest-canonical.test.mjs new file mode 100644 index 00000000..9c5dcf85 --- /dev/null +++ b/web/tests/unit/script-manifest-canonical.test.mjs @@ -0,0 +1,80 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-manifest-canonical-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { + compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, + fileName: `${name}.ts`, + }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const script = (id, overrides = {}) => ({ + id, + name: id, + entryPath: `scripts/${id}.py`, + sourceByteLength: 128, + sourceSha256: digest, + publisher: "local", + signature, + keyId: "key:local", + permissions: ["READ_MAIN"], + dependencies: [], + module: false, + cpuMs: 1000, + memoryBytes: 1024 * 1024, + wallMs: 5000, + network: false, + autorun: false, + driverExpressions: false, + addonInstall: false, + ...overrides, +}); + +test("M13-02B canonical serialization is invariant to manifest/script array order", () => { + const base = { + schemaVersion: 1, + scripts: [ + script("zeta", { permissions: ["WRITE_ASSET", "READ_MAIN"], dependencies: [{ id: "alpha", sourceSha256: digest, sourcePath: "deps/alpha.py" }, { id: "beta", sourceSha256: digest, sourcePath: "deps/beta.py" }] }), + script("alpha", { permissions: ["SUBMIT_SERVER_JOB", "READ_ASSET"] }), + script("beta"), + ], + }; + const reordered = { + schemaVersion: 1, + scripts: [ + { ...base.scripts[1], permissions: [...base.scripts[1].permissions].reverse(), ignored: "removed" }, + { ...base.scripts[0], permissions: [...base.scripts[0].permissions].reverse(), dependencies: [...base.scripts[0].dependencies].reverse() }, + base.scripts[2], + ], + }; + const first = protocol.serializeScriptingManifest(base); + const second = protocol.serializeScriptingManifest(reordered); + assert.equal(first, second); + assert.match(first, /^\{"schemaVersion":1,"scripts":\[/); + assert.equal(Object.keys(protocol.canonicalizeScriptingManifest(reordered).scripts[0]).includes("ignored"), false); +}); + +test("M13-02B canonical serialization includes security-relevant declaration fields", () => { + const base = { schemaVersion: 1, scripts: [script("clean")] }; + const changedBytes = { schemaVersion: 1, scripts: [script("clean", { sourceByteLength: 129 })] }; + const changedPermission = { schemaVersion: 1, scripts: [script("clean", { permissions: ["READ_ASSET"] })] }; + assert.notEqual(protocol.serializeScriptingManifest(base), protocol.serializeScriptingManifest(changedBytes)); + assert.notEqual(protocol.serializeScriptingManifest(base), protocol.serializeScriptingManifest(changedPermission)); + assert.throws(() => protocol.serializeScriptingManifest({ ...base, schemaVersion: 2 }), /PROTOCOL_MISMATCH/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-permission-policy.test.mjs b/web/tests/unit/script-permission-policy.test.mjs new file mode 100644 index 00000000..b4aba07f --- /dev/null +++ b/web/tests/unit/script-permission-policy.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-permission-policy-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const script = (permissions = ["READ_MAIN"]) => ({ id: "clean", name: "clean", entryPath: "scripts/clean.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature: "b".repeat(128), keyId: "key:new", permissions, dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false }); +const manifest = (permissions) => ({ schemaVersion: 1, scripts: [script(permissions)] }); + +test("M13-02E grants no undeclared permission by default", () => { + assert.deepEqual(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean"), { status: "ALLOWED", code: "SCRIPT_PERMISSIONS_ALLOWED", scriptId: "clean", declared: ["READ_MAIN"], requested: [], granted: [] }); + assert.deepEqual(protocol.resolveScriptPermissions(manifest(["WRITE_ASSET", "READ_MAIN"]), "clean", ["READ_MAIN"]), { status: "ALLOWED", code: "SCRIPT_PERMISSIONS_ALLOWED", scriptId: "clean", declared: ["READ_MAIN", "WRITE_ASSET"], requested: ["READ_MAIN"], granted: ["READ_MAIN"] }); +}); + +test("M13-02E blocks escalation, unknown and duplicate permission requests", () => { + assert.equal(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["WRITE_MAIN"]).code, "SCRIPT_POLICY_DENIED"); + assert.equal(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["EXECUTE"]).code, "SCRIPT_POLICY_DENIED"); + assert.equal(protocol.resolveScriptPermissions(manifest(["READ_MAIN"]), "clean", ["READ_MAIN", "READ_MAIN"]).code, "SCRIPT_POLICY_DENIED"); + assert.throws(() => protocol.parseScriptingManifest(manifest(["EXECUTE"])), /SCRIPT_POLICY_DENIED/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-policy-codes.test.mjs b/web/tests/unit/script-policy-codes.test.mjs new file mode 100644 index 00000000..c1d66367 --- /dev/null +++ b/web/tests/unit/script-policy-codes.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-policy-codes-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText.replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const base = { schemaVersion: 1, scripts: [{ id: "demo", name: "Demo", entryPath: "scripts/demo.py", sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "local", signature: "b".repeat(128), keyId: "key", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 64 * 1024 * 1024, wallMs: 2000, network: false, autorun: false, driverExpressions: false, addonInstall: false }] }; + +test("M13-01C returns stable default-deny codes for autorun, driver and add-on execution", () => { + assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], autorun: true }] }), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], driverExpressions: true }] }), /DRIVER_EXECUTION_BLOCKED/); + assert.throws(() => protocol.parseScriptingManifest({ ...base, scripts: [{ ...base.scripts[0], addonInstall: true }] }), /ADDON_INSTALL_BLOCKED/); + const gate = protocol.gateScriptExecution(base, "demo", new Set(["key"])); + assert.equal(gate.status, "BLOCKED"); + assert.equal(gate.issues[0].code, "SCRIPT_SANDBOX_UNAVAILABLE"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-sandbox-budget.test.mjs b/web/tests/unit/script-sandbox-budget.test.mjs new file mode 100644 index 00000000..4455534d --- /dev/null +++ b/web/tests/unit/script-sandbox-budget.test.mjs @@ -0,0 +1,30 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-budget-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const budget = { schemaVersion: 1, cpuMs: 1000, wallMs: 5000, memoryBytes: 1024 * 1024, maxMessageBytes: 4096, maxOutputBytes: 8192 }; + +test("M13-03B accepts bounded CPU, wall, memory, message and output budgets", () => { + assert.deepEqual(protocol.parseScriptSandboxBudget(budget), budget); +}); + +test("M13-03B rejects every budget overflow and schema drift", () => { + for (const [field, limit] of Object.entries({ cpuMs: protocol.SCRIPT_SANDBOX_BUDGET.maxCpuMs, wallMs: protocol.SCRIPT_SANDBOX_BUDGET.maxWallMs, memoryBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxMemoryBytes, maxMessageBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxMessageBytes, maxOutputBytes: protocol.SCRIPT_SANDBOX_BUDGET.maxOutputBytes })) assert.throws(() => protocol.parseScriptSandboxBudget({ ...budget, [field]: limit + 1 }), /SCRIPT_BUDGET_EXCEEDED/); + assert.throws(() => protocol.parseScriptSandboxBudget({ ...budget, schemaVersion: 2 }), /PROTOCOL_MISMATCH/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-sandbox-cancellation.test.mjs b/web/tests/unit/script-sandbox-cancellation.test.mjs new file mode 100644 index 00000000..e951fb78 --- /dev/null +++ b/web/tests/unit/script-sandbox-cancellation.test.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-cancellation-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const running = { schemaVersion: 1, jobId: "sandbox:cancel", workerGeneration: 5, baseRevision: 11, mainRevisionBefore: 11, status: "RUNNING" }; + +test("M13-03E cancellation receipt blocks late message and cache publication", () => { + const cancelled = protocol.terminateScriptSandboxJob(running, "CANCEL"); + assert.deepEqual({ status: cancelled.status, errorCode: cancelled.errorCode, mainRevisionAfter: cancelled.mainRevisionAfter, temporaryBytes: cancelled.temporaryBytes, publishedResults: cancelled.publishedResults, lateResults: cancelled.lateResults, committed: cancelled.committed }, { status: "CANCELLED", errorCode: "SCRIPT_SANDBOX_CANCELLED", mainRevisionAfter: 11, temporaryBytes: 0, publishedResults: 0, lateResults: 0, committed: false }); + assert.throws(() => protocol.rejectLateScriptSandboxResult(cancelled), /SCRIPT_SANDBOX_LATE_RESULT/); +}); + +test("M13-03E rejects a late result that is not tied to a terminated receipt", () => { + assert.throws(() => protocol.rejectLateScriptSandboxResult({ ...running, status: "RUNNING" }), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, baseRevision: 10 }, "CANCEL"), /SCRIPT_MANIFEST_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-sandbox-dispose.test.mjs b/web/tests/unit/script-sandbox-dispose.test.mjs new file mode 100644 index 00000000..8c2d4222 --- /dev/null +++ b/web/tests/unit/script-sandbox-dispose.test.mjs @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-dispose-")); +const source = fs.readFileSync(path.join(root, "web/app/src/testing/script-sandbox-dispose.ts"), "utf8"); +fs.writeFileSync(path.join(temporary, "script-sandbox-dispose.cjs"), ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: "script-sandbox-dispose.ts" }).outputText); +const protocol = createRequire(import.meta.url)(path.join(temporary, "script-sandbox-dispose.cjs")); + +test("M13-03F emits zero-resource disposal receipts", () => { + assert.deepEqual(protocol.createScriptSandboxDisposeReceipt(1), { schemaVersion: 1, disposeCount: 1, idempotent: false, resources: { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }, lateTimerMessages: 0 }); + assert.deepEqual(protocol.createScriptSandboxDisposeReceipt(2), { schemaVersion: 1, disposeCount: 2, idempotent: true, resources: { messagePorts: 0, timers: 0, abortControllers: 0, transferableBuffers: 0, pendingRequests: 0, cacheReferences: 0 }, lateTimerMessages: 0 }); +}); + +test("M13-03F rejects an invalid disposal count", () => { + assert.throws(() => protocol.createScriptSandboxDisposeReceipt(0), /SCRIPT_SANDBOX_DISPOSE_INVALID/); + assert.throws(() => protocol.createScriptSandboxDisposeReceipt(1.5), /SCRIPT_SANDBOX_DISPOSE_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-sandbox-isolation.test.mjs b/web/tests/unit/script-sandbox-isolation.test.mjs new file mode 100644 index 00000000..1bc96b7d --- /dev/null +++ b/web/tests/unit/script-sandbox-isolation.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-isolation-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")').replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const running = { schemaVersion: 1, jobId: "sandbox:1", workerGeneration: 4, baseRevision: 9, mainRevisionBefore: 9, status: "RUNNING" }; + +test("M13-03D crash and timeout terminate jobs without changing Main revision", () => { + const crashed = protocol.terminateScriptSandboxJob(running, "CRASH"); + const timedOut = protocol.terminateScriptSandboxJob(running, "TIMEOUT"); + assert.deepEqual({ status: crashed.status, errorCode: crashed.errorCode, mainRevisionAfter: crashed.mainRevisionAfter, temporaryBytes: crashed.temporaryBytes, publishedResults: crashed.publishedResults, committed: crashed.committed }, { status: "CRASHED", errorCode: "SCRIPT_SANDBOX_CRASHED", mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false }); + assert.deepEqual({ status: timedOut.status, errorCode: timedOut.errorCode, mainRevisionAfter: timedOut.mainRevisionAfter, temporaryBytes: timedOut.temporaryBytes, publishedResults: timedOut.publishedResults, committed: timedOut.committed }, { status: "TIMED_OUT", errorCode: "SCRIPT_SANDBOX_TIMEOUT", mainRevisionAfter: 9, temporaryBytes: 0, publishedResults: 0, committed: false }); +}); + +test("M13-03D cancellation and late results are fail-closed", () => { + const cancelled = protocol.terminateScriptSandboxJob(running, "CANCEL"); + assert.equal(cancelled.errorCode, "SCRIPT_SANDBOX_CANCELLED"); + assert.throws(() => protocol.rejectLateScriptSandboxResult(cancelled), /SCRIPT_SANDBOX_LATE_RESULT/); + assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, baseRevision: 8 }, "CRASH"), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, status: "CRASHED" }, "TIMEOUT"), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.rejectLateScriptSandboxResult(running), /SCRIPT_MANIFEST_INVALID/); +}); + +test("M13-03D rejects malformed termination receipts", () => { + assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, workerGeneration: 0 }, "CRASH"), /SCRIPT_BUDGET_EXCEEDED/); + assert.throws(() => protocol.terminateScriptSandboxJob({ ...running, mainRevisionBefore: 10 }, "TIMEOUT"), /SCRIPT_MANIFEST_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-sandbox-recovery.test.mjs b/web/tests/unit/script-sandbox-recovery.test.mjs new file mode 100644 index 00000000..488fa424 --- /dev/null +++ b/web/tests/unit/script-sandbox-recovery.test.mjs @@ -0,0 +1,32 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-recovery-")); +const source = fs.readFileSync(path.join(root, "web/app/src/testing/script-sandbox-recovery.ts"), "utf8"); +fs.writeFileSync(path.join(temporary, "script-sandbox-recovery.cjs"), ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: "script-sandbox-recovery.ts" }).outputText); +const protocol = createRequire(import.meta.url)(path.join(temporary, "script-sandbox-recovery.cjs")); +const digest = "a".repeat(64); +const entry = (sequence, requestId, previousEntrySha256, entrySha256) => ({ sequence, requestId, previousEntrySha256, entrySha256, sourceSha256: digest, manifestSha256: "b".repeat(64) }); + +test("M13-03G accepts one-generation recovery with a continuous audit chain", () => { + const receipt = protocol.createScriptSandboxRecoveryReceipt({ previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, sourceSha256: digest, manifestSha256: "b".repeat(64), audit: { entries: 2, first: entry(1, "sandbox-recovery:g4", null, digest), second: entry(2, "sandbox-recovery:g5", digest, "c".repeat(64)) } }); + assert.equal(receipt.recovered, true); + assert.equal(receipt.execution, "DISABLED"); + assert.equal(receipt.audit.second.previousEntrySha256, receipt.audit.first.entrySha256); +}); + +test("M13-03G rejects generation, revision, request and hash-chain drift", () => { + const base = { previousGeneration: 4, nextGeneration: 5, mainRevisionBefore: 11, mainRevisionAfter: 11, sourceSha256: digest, manifestSha256: "b".repeat(64), audit: { entries: 2, first: entry(1, "sandbox-recovery:g4", null, digest), second: entry(2, "sandbox-recovery:g5", digest, "c".repeat(64)) } }; + assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, nextGeneration: 7 }), /generation/); + assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, mainRevisionAfter: 12 }), /revision/); + assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, audit: { ...base.audit, second: entry(2, "sandbox-recovery:g5", "d".repeat(64), "c".repeat(64)) } }), /hash chain/); + assert.throws(() => protocol.createScriptSandboxRecoveryReceipt({ ...base, audit: { ...base.audit, second: entry(2, "sandbox-recovery:g4", digest, "c".repeat(64)) } }), /replayed/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-sandbox-scope.test.mjs b/web/tests/unit/script-sandbox-scope.test.mjs new file mode 100644 index 00000000..b87140f4 --- /dev/null +++ b/web/tests/unit/script-sandbox-scope.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-sandbox-scope-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const deniedScope = { schemaVersion: 1, dom: false, hostWorker: false, opfs: false, indexedDB: false, network: false }; + +test("M13-03A accepts only the all-deny sandbox scope", () => { + assert.deepEqual(protocol.parseScriptSandboxScope(deniedScope), deniedScope); + for (const capability of ["dom", "hostWorker", "opfs", "indexedDB", "network"]) { + assert.throws(() => protocol.parseScriptSandboxScope({ ...deniedScope, [capability]: true }), /SCRIPT_POLICY_DENIED/); + } +}); + +test("M13-03A rejects unknown scope versions and missing declarations", () => { + assert.throws(() => protocol.parseScriptSandboxScope({ ...deniedScope, schemaVersion: 2 }), /PROTOCOL_MISMATCH/); + assert.throws(() => protocol.parseScriptSandboxScope({ schemaVersion: 1 }), /SCRIPT_POLICY_DENIED/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-signature-negative.test.mjs b/web/tests/unit/script-signature-negative.test.mjs new file mode 100644 index 00000000..283b7f2c --- /dev/null +++ b/web/tests/unit/script-signature-negative.test.mjs @@ -0,0 +1,44 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-signature-negative-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const publicKey = "03a107bff3ce10be1d70dd18e74bc09967e4d6309ba50d5f1ddc8664125531b8"; +const signature = "fc396c6c68e6f6eb38a18c147becfaec1621a167f6db0a0d76874209accf3cb80dfa1fac1528ebc1bc6b090801a3ad397cae18e6ddb41740766678711c0a8804"; +const script = (id = "clean", overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: "a".repeat(64), publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const manifest = (scripts = [script()]) => ({ schemaVersion: 1, scripts }); +const key = (overrides = {}) => ({ keyId: "key:new", publisher: "Team", algorithm: "ED25519", publicKey, status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides }); +const policy = (overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys: [key()], ...overrides }); + +test("M13-02F blocks missing, expired, not-yet-valid and publisher-confused signers", async () => { + const at = "2026-08-18T12:00:00.000Z"; + assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [] }), at)).code, "SCRIPT_POLICY_DENIED"); + assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ expiresAt: "2026-06-01T00:00:00.000Z" }), at)).code, "SCRIPT_POLICY_DENIED"); + assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notBefore: "2026-09-01T00:00:00.000Z" })] }), at)).code, "SCRIPT_POLICY_DENIED"); + assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ notAfter: "2026-06-01T00:00:00.000Z" })] }), at)).code, "SCRIPT_POLICY_DENIED"); + assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy({ keys: [key({ publisher: "Other" })] }), at)).code, "SCRIPT_POLICY_DENIED"); +}); + +test("M13-02F binds a signature to one script and does not accept reordered/swapped content", async () => { + const at = "2026-08-18T12:00:00.000Z"; + assert.equal((await protocol.verifyScriptManifestSignature(manifest(), "clean", policy(), at)).status, "VERIFIED"); + const swapped = manifest([script("other")]); + assert.equal((await protocol.verifyScriptManifestSignature(swapped, "other", policy(), at)).code, "SCRIPT_SIGNATURE_INVALID"); + assert.equal((await protocol.verifyScriptManifestSignature(manifest([script("other"), script()]), "clean", policy(), at)).status, "VERIFIED"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/script-trust-policy.test.mjs b/web/tests/unit/script-trust-policy.test.mjs new file mode 100644 index 00000000..4a187fe0 --- /dev/null +++ b/web/tests/unit/script-trust-policy.test.mjs @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { createRequire } from "node:module"; +import { createPrivateKey, createPublicKey, sign } from "node:crypto"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "script-trust-policy-")); +const require = createRequire(import.meta.url); +for (const name of ["asset-path", "capability-gates", "scripting-platform"]) { + const source = fs.readFileSync(path.join(root, `web/protocol/${name}.ts`), "utf8"); + const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022 }, fileName: `${name}.ts` }).outputText + .replace('require("./asset-path")', 'require("./asset-path.cjs")') + .replace('require("./capability-gates")', 'require("./capability-gates.cjs")'); + fs.writeFileSync(path.join(temporary, `${name}.cjs`), output); +} +const protocol = require(path.join(temporary, "scripting-platform.cjs")); +const digest = "a".repeat(64); +const signature = "b".repeat(128); +const privateKey = createPrivateKey({ key: Buffer.concat([Buffer.from("302e020100300506032b657004220420", "hex"), Buffer.from("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", "hex")]), format: "der", type: "pkcs8" }); +const publicKey = createPublicKey(privateKey).export({ format: "der", type: "spki" }).subarray(-32).toString("hex"); +const script = (id = "clean", overrides = {}) => ({ id, name: id, entryPath: `scripts/${id}.py`, sourceByteLength: 128, sourceSha256: digest, publisher: "Team", signature, keyId: "key:new", permissions: ["READ_MAIN"], dependencies: [], module: false, cpuMs: 1000, memoryBytes: 1024 * 1024, wallMs: 5000, network: false, autorun: false, driverExpressions: false, addonInstall: false, ...overrides }); +const key = (keyId, overrides = {}) => ({ keyId, publisher: "Team", algorithm: "ED25519", publicKey: "c".repeat(64), status: "ACTIVE", notBefore: "2026-01-01T00:00:00.000Z", notAfter: "2027-01-01T00:00:00.000Z", ...overrides }); +const policy = (keys = [key("key:new")], overrides = {}) => ({ schemaVersion: 1, issuer: "web-trust", issuedAt: "2026-01-01T00:00:00.000Z", expiresAt: "2027-01-01T00:00:00.000Z", maxClockSkewMs: 300000, keys, ...overrides }); + +test("M13-02C parses signer identity, active rotation and timestamp windows", () => { + const parsed = protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "key:old" }), key("key:old", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })])); + assert.equal(parsed.keys.length, 2); + assert.equal(protocol.canonicalizeScriptTrustPolicy(parsed).keys[0].keyId, "key:new"); + assert.match(protocol.serializeScriptTrustPolicy(parsed), /"algorithm":"ED25519"/); + assert.deepEqual(protocol.resolveScriptSigner({ schemaVersion: 1, scripts: [script()] }, "clean", parsed, "2026-08-18T12:00:00.000Z"), { status: "ELIGIBLE", keyId: "key:new", publisher: "Team", trust: "ACTIVE", cryptographicVerification: "REQUIRED" }); +}); + +test("M13-02C rejects invalid rotation, revocation and timestamp policy declarations", () => { + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "missing" })])), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { replaces: "key:old" }), key("key:old", { publisher: "Other" })])), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:a", { replaces: "key:b" }), key("key:b", { replaces: "key:a" })])), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { status: "REVOKED" })])), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { revokedAt: "2026-06-01T00:00:00.000Z" })])), /SCRIPT_POLICY_DENIED/); + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { notAfter: "2025-01-01T00:00:00.000Z" })])), /SCRIPT_MANIFEST_INVALID/); + assert.throws(() => protocol.parseScriptTrustPolicy(policy([key("key:new", { publicKey: "not-a-key" })])), /SCRIPT_SIGNATURE_INVALID/); +}); + +test("M13-02C resolves revoked, publisher-mismatched and expired signers fail-closed", () => { + const baseManifest = { schemaVersion: 1, scripts: [script()] }; + const revoked = policy([key("key:new", { status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]); + assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", revoked, "2026-08-18T12:00:00.000Z").trust, "REVOKED"); + const mismatch = policy([key("key:new", { publisher: "Other" })]); + assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", mismatch, "2026-08-18T12:00:00.000Z").trust, "PUBLISHER_MISMATCH"); + const expired = policy([key("key:new", { notAfter: "2026-06-01T00:00:00.000Z" })]); + assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", expired, "2026-08-18T12:00:00.000Z").trust, "KEY_EXPIRED"); + const policyExpired = policy([key("key:new")], { expiresAt: "2026-06-01T00:00:00.000Z" }); + assert.equal(protocol.resolveScriptSigner(baseManifest, "clean", policyExpired, "2026-08-18T12:00:00.000Z").trust, "POLICY_EXPIRED"); +}); + +test("M13-02D verifies only the canonical declared content and source hash", async () => { + const baseScript = script("clean", { signature: "0".repeat(128) }); + const unsigned = { schemaVersion: 1, scripts: [baseScript] }; + const signedScript = { ...baseScript, signature: sign(null, Buffer.from(protocol.serializeScriptSignatureInput(unsigned, "clean")), privateKey).toString("hex") }; + const signed = { schemaVersion: 1, scripts: [signedScript] }; + const trust = policy([key("key:new", { publicKey })]); + const verified = await protocol.verifyScriptManifestSignature(signed, "clean", trust, "2026-08-18T12:00:00.000Z"); + assert.equal(verified.status, "VERIFIED"); + assert.equal(verified.code, "SCRIPT_SIGNATURE_VERIFIED"); + const changedSource = { schemaVersion: 1, scripts: [{ ...signedScript, sourceSha256: "d".repeat(64) }] }; + assert.deepEqual((await protocol.verifyScriptManifestSignature(changedSource, "clean", trust, "2026-08-18T12:00:00.000Z")).code, "SCRIPT_SIGNATURE_INVALID"); + const changedSignature = { schemaVersion: 1, scripts: [{ ...signedScript, signature: `${signedScript.signature.slice(0, -1)}${signedScript.signature.endsWith("0") ? "1" : "0"}` }] }; + assert.deepEqual((await protocol.verifyScriptManifestSignature(changedSignature, "clean", trust, "2026-08-18T12:00:00.000Z")).code, "SCRIPT_SIGNATURE_INVALID"); + const revoked = policy([key("key:new", { publicKey, status: "REVOKED", revokedAt: "2026-06-01T00:00:00.000Z" })]); + assert.deepEqual((await protocol.verifyScriptManifestSignature(signed, "clean", revoked, "2026-08-18T12:00:00.000Z")).code, "SCRIPT_POLICY_DENIED"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/server-job-fault.test.mjs b/web/tests/unit/server-job-fault.test.mjs new file mode 100644 index 00000000..4623c71b --- /dev/null +++ b/web/tests/unit/server-job-fault.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { classifyServerJobFault, createServerJobFaultReceipt, SERVER_JOB_FAULT_CODES } from "../../../tools/web/server-job-fault.mjs"; + +test("M13-04H classifies faults with stable precedence", () => { + assert.equal(classifyServerJobFault({ timedOut: true, oom: true, signal: "SIGKILL" }).code, SERVER_JOB_FAULT_CODES.TIMEOUT); + assert.equal(classifyServerJobFault({ oom: true, signal: "SIGKILL" }).code, SERVER_JOB_FAULT_CODES.OOM); + assert.equal(classifyServerJobFault({ signal: "SIGTERM" }).code, SERVER_JOB_FAULT_CODES.SIGNAL); + assert.equal(classifyServerJobFault({ code: 7 }).code, SERVER_JOB_FAULT_CODES.EXIT_FAILED); + assert.equal(classifyServerJobFault({ cancelRequested: true, timedOut: true }).code, SERVER_JOB_FAULT_CODES.CANCELLED); +}); + +test("M13-04H preserves the old revision and blocks failure publication", () => { + const receipt = createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, timedOut: true }); + assert.deepEqual(receipt, { schemaVersion: 1, state: "FAILED", code: "SERVER_JOB_TIMEOUT", stage: "PROCESS", exitCode: null, signal: null, timedOut: true, memoryExceeded: false, baseRevision: 11, currentRevision: 11, committedRevision: 11, publish: false, revisionPreserved: true, execution: "DISABLED" }); + assert.throws(() => createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 12, signal: "SIGTERM" }), /SERVER_JOB_REVISION_CONFLICT/); + assert.equal(createServerJobFaultReceipt({ baseRevision: 11, currentRevision: 11, code: 0 }).publish, true); +}); + +test("M13-04H derives OOM from bounded memory usage", () => { + const receipt = createServerJobFaultReceipt({ baseRevision: 3, currentRevision: 3, memoryBytes: 513, memoryLimitBytes: 512 }); + assert.equal(receipt.code, SERVER_JOB_FAULT_CODES.OOM); + assert.equal(receipt.memoryExceeded, true); +}); diff --git a/web/tests/unit/server-job-idempotency.test.mjs b/web/tests/unit/server-job-idempotency.test.mjs new file mode 100644 index 00000000..09a4db7d --- /dev/null +++ b/web/tests/unit/server-job-idempotency.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { submitIdempotentServerJobResult } from "../../../tools/web/server-job-idempotency.mjs"; + +const h = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const identity = { requestId: "retry-1", projectId: "project-1", baseRevision: 4, sourceSha256: h("source"), settingsSha256: h("settings"), buildSha256: h("build") }; + +test("M13-04J reuses the exact verified result for the same request", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-reuse-")); + const options = { receiptDirectory: path.join(root, "receipts"), outputDirectory: path.join(root, "outputs") }; + try { + const first = await submitIdempotentServerJobResult(identity, new Uint8Array([1, 2]), options); + const second = await submitIdempotentServerJobResult(identity, new Uint8Array([1, 2]), options); + assert.equal(first.reused, false); + assert.equal(second.reused, true); + assert.equal(first.outputSha256, second.outputSha256); + assert.equal((await fs.readdir(options.receiptDirectory)).length, 1); + } finally { await fs.rm(root, { recursive: true, force: true }); } +}); + +test("M13-04J rejects conflicting output/identity and isolates different requests", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-conflict-")); + const options = { receiptDirectory: path.join(root, "receipts"), outputDirectory: path.join(root, "outputs") }; + try { + await submitIdempotentServerJobResult(identity, new Uint8Array([3]), options); + await assert.rejects(submitIdempotentServerJobResult(identity, new Uint8Array([4]), options), /SERVER_JOB_IDEMPOTENCY_CONFLICT/); + await assert.rejects(submitIdempotentServerJobResult({ ...identity, settingsSha256: h("changed") }, new Uint8Array([3]), options), /SERVER_JOB_IDEMPOTENCY_CONFLICT/); + const other = await submitIdempotentServerJobResult({ ...identity, requestId: "retry-2" }, new Uint8Array([4]), options); + assert.equal(other.reused, false); + assert.equal((await fs.readdir(options.receiptDirectory)).length, 2); + } finally { await fs.rm(root, { recursive: true, force: true }); } +}); + +test("M13-04J serializes concurrent duplicate requests", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04j-concurrent-")); + const options = { receiptDirectory: path.join(root, "receipts"), outputDirectory: path.join(root, "outputs") }; + try { + const results = await Promise.all([ + submitIdempotentServerJobResult({ ...identity, requestId: "retry-3" }, new Uint8Array([8]), options), + submitIdempotentServerJobResult({ ...identity, requestId: "retry-3" }, new Uint8Array([8]), options), + ]); + assert.deepEqual(results.map((result) => result.reused).sort(), [false, true]); + } finally { await fs.rm(root, { recursive: true, force: true }); } +}); diff --git a/web/tests/unit/server-job-isolation.test.mjs b/web/tests/unit/server-job-isolation.test.mjs new file mode 100644 index 00000000..0e57783d --- /dev/null +++ b/web/tests/unit/server-job-isolation.test.mjs @@ -0,0 +1,58 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { cleanupServerJobDirectory, createServerJobDirectory, prepareServerJobWorkspace } from "../../../tools/web/server-job-isolation.mjs"; + +test("M13-04A creates unpredictable one-shot directories and cleans them once", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04a-job-root-")); + try { + const first = await createServerJobDirectory(root, "server:job-one"); + const second = await createServerJobDirectory(root, "server:job-two"); + assert.notEqual(first.directoryName, second.directoryName); + assert.notEqual(first.directoryName, first.jobId); + assert.match(first.directoryName, /^\.blender-job-[0-9a-f-]+-[A-Za-z0-9]+$/); + assert.equal((await fs.stat(first.path)).mode & 0o777, 0o700); + assert.equal((await fs.stat(second.path)).mode & 0o777, 0o700); + const cleanedFirst = await cleanupServerJobDirectory(first); + const cleanedSecond = await cleanupServerJobDirectory(second); + assert.equal(cleanedFirst.state, "CLEANED"); + assert.equal(cleanedFirst.cleanupCount, 1); + assert.equal(cleanedSecond.cleanupCount, 1); + await assert.rejects(fs.stat(first.path), { code: "ENOENT" }); + await assert.rejects(fs.stat(second.path), { code: "ENOENT" }); + assert.equal((await cleanupServerJobDirectory(cleanedFirst)).cleanupCount, 1); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +}); + +test("M13-04A rejects unsafe roots, IDs and cleanup escapes", async () => { + await assert.rejects(createServerJobDirectory("relative-root", "server:job"), /SERVER_JOB_DIRECTORY_INVALID/); + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04a-invalid-root-")); + try { + await assert.rejects(createServerJobDirectory(root, "../escape"), /SERVER_JOB_DIRECTORY_INVALID/); + await assert.rejects(cleanupServerJobDirectory({ schemaVersion: 1, root, path: path.join(root, "other"), state: "ALLOCATED" }), /SERVER_JOB_DIRECTORY_INVALID/); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +}); + +test("M13-04B isolates read-only source from writable output", async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04b-workspace-")); + try { + const job = await createServerJobDirectory(root, "server:job-mount"); + const workspace = await prepareServerJobWorkspace(job, new Uint8Array([1, 2, 3])); + assert.notEqual(path.dirname(workspace.sourcePath), workspace.outputDirectory); + assert.equal((await fs.stat(workspace.sourceDirectory)).mode & 0o777, 0o555); + assert.equal((await fs.stat(workspace.sourcePath)).mode & 0o777, 0o444); + assert.equal((await fs.stat(workspace.outputDirectory)).mode & 0o777, 0o700); + await assert.rejects(fs.writeFile(workspace.sourcePath, new Uint8Array([9])), { code: "EACCES" }); + await fs.writeFile(path.join(workspace.outputDirectory, "result.bin"), new Uint8Array([4, 5])); + assert.deepEqual([...await fs.readFile(path.join(workspace.outputDirectory, "result.bin"))], [4, 5]); + await cleanupServerJobDirectory(job); + } finally { + await fs.rm(root, { recursive: true, force: true }); + } +}); diff --git a/web/tests/unit/server-job-network-policy.test.mjs b/web/tests/unit/server-job-network-policy.test.mjs new file mode 100644 index 00000000..e37397fe --- /dev/null +++ b/web/tests/unit/server-job-network-policy.test.mjs @@ -0,0 +1,17 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { parseServerJobNetworkPolicy, resolveServerJobNetwork } from "../../../tools/web/server-job-network-policy.mjs"; + +test("M13-04D defaults to deny and admits only declared safe origins", () => { + const policy = parseServerJobNetworkPolicy({ schemaVersion: 1, allowedOrigins: ["https://example.com", "http://127.0.0.1:8787", "https://example.com"] }); + assert.deepEqual(policy, { schemaVersion: 1, defaultNetwork: "DENY", allowedOrigins: ["http://127.0.0.1:8787", "https://example.com"] }); + assert.deepEqual(resolveServerJobNetwork(policy), { status: "DENIED", code: "SERVER_NETWORK_DENIED", origin: null, network: "DISABLED" }); + assert.equal(resolveServerJobNetwork(policy, "https://example.com").status, "ALLOWED"); + assert.equal(resolveServerJobNetwork(policy, "https://other.example").code, "SERVER_NETWORK_DENIED"); +}); + +test("M13-04D rejects unsafe or non-canonical origins", () => { + for (const origin of ["http://example.com", "file:///tmp/x", "https://example.com/path", "https://user:pass@example.com"]) { + assert.throws(() => parseServerJobNetworkPolicy({ schemaVersion: 1, allowedOrigins: [origin] }), /SERVER_NETWORK_/); + } +}); diff --git a/web/tests/unit/server-job-output.test.mjs b/web/tests/unit/server-job-output.test.mjs new file mode 100644 index 00000000..84d30496 --- /dev/null +++ b/web/tests/unit/server-job-output.test.mjs @@ -0,0 +1,34 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createServerJobOutputReceipt, redactServerJobOutput, SERVER_JOB_OUTPUT_LIMITS } from "../../../tools/web/server-job-output.mjs"; + +test("M13-04F redacts credentials and filesystem paths before publishing output", () => { + const receipt = createServerJobOutputReceipt({ + stdout: 'authorization: Bearer abc123 token="secret-value" source=/home/user/private.blend', + stderr: "failed at C:\\Users\\alice\\job\\source.blend file:///tmp/internal.log", + }); + assert.equal(receipt.execution, "DISABLED"); + assert.ok(receipt.totalRedactions >= 5); + assert.doesNotMatch(receipt.stdout.text, /abc123|secret-value|\/home\/user|C:\\Users|file:\/\//u); + assert.doesNotMatch(receipt.stderr.text, /alice|internal\.log/u); + assert.match(receipt.stdout.text, //u); + assert.match(receipt.stderr.text, //u); +}); + +test("M13-04F truncates at UTF-8 byte boundaries and reports the source size", () => { + const value = "模型".repeat(100); + const receipt = createServerJobOutputReceipt({ stdout: value, stderr: "" }, { ...SERVER_JOB_OUTPUT_LIMITS, stdoutBytes: 32, stderrBytes: 32, totalBytes: 64 }); + assert.equal(receipt.stdout.truncated, true); + assert.ok(receipt.stdout.emittedBytes <= 32); + assert.equal(Buffer.from(receipt.stdout.text, "utf8").toString("utf8"), receipt.stdout.text); + assert.equal(receipt.stdout.originalBytes, Buffer.byteLength(value, "utf8")); + assert.match(receipt.stdout.text, /$/u); +}); + +test("M13-04F rejects invalid stream budgets and leaves empty streams explicit", () => { + assert.deepEqual(createServerJobOutputReceipt({ stdout: "", stderr: "" }).stdout, { + text: "", originalBytes: 0, emittedBytes: 0, redactionCount: 0, truncated: false, + }); + assert.throws(() => createServerJobOutputReceipt({ stdout: "x", stderr: "" }, { stdoutBytes: 10, stderrBytes: 10, totalBytes: 10 }), /SERVER_JOB_OUTPUT_INVALID/); + assert.throws(() => redactServerJobOutput(42, 10), /SERVER_JOB_OUTPUT_INVALID/); +}); diff --git a/web/tests/unit/server-job-process.test.mjs b/web/tests/unit/server-job-process.test.mjs new file mode 100644 index 00000000..980045ea --- /dev/null +++ b/web/tests/unit/server-job-process.test.mjs @@ -0,0 +1,48 @@ +import assert from "node:assert/strict"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { cleanupServerJobDirectory, createServerJobDirectory } from "../../../tools/web/server-job-isolation.mjs"; +import { cancelServerJobProcess, startServerJobProcess } from "../../../tools/web/server-job-process.mjs"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); + +test("M13-04G cancels the real process group and cleans the job directory", async () => { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04g-process-")); + const job = await createServerJobDirectory(temporary, "server:cancel"); + const childScript = "const {spawn}=require('node:child_process'); const c=spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:'ignore'}); setInterval(()=>{},1000);"; + const handle = startServerJobProcess(process.execPath, ["-e", childScript], { cwd: root }); + let cleanupCount = 0; + try { + const receipt = await cancelServerJobProcess(handle, async () => { cleanupCount += 1; await cleanupServerJobDirectory(job); }); + assert.equal(receipt.state, "CANCELLED"); + assert.equal(receipt.cleanupCount, 1); + assert.equal(cleanupCount, 1); + assert.match(receipt.treeSignal, /GROUP|ALREADY_EXITED/); + await assert.rejects(fs.stat(job.path), { code: "ENOENT" }); + await assert.doesNotReject(handle.completion); + } finally { + await fs.rm(temporary, { recursive: true, force: true }); + } +}); + +test("M13-04G repeated cancellation is idempotent", async () => { + const temporary = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04g-idempotent-")); + const job = await createServerJobDirectory(temporary, "server:repeat"); + const handle = startServerJobProcess(process.execPath, ["-e", "setInterval(()=>{},1000)"], { cwd: root }); + let cleanupCount = 0; + const cleanup = async () => { cleanupCount += 1; await cleanupServerJobDirectory(job); }; + try { + const first = await cancelServerJobProcess(handle, cleanup); + const second = await cancelServerJobProcess(handle, cleanup); + assert.equal(first.state, "CANCELLED"); + assert.equal(second.state, "CANCELLED"); + assert.equal(first.cleanupCount, 1); + assert.equal(second.cleanupCount, 1); + assert.equal(cleanupCount, 1); + } finally { + await fs.rm(temporary, { recursive: true, force: true }); + } +}); diff --git a/web/tests/unit/server-job-resource-budget.test.mjs b/web/tests/unit/server-job-resource-budget.test.mjs new file mode 100644 index 00000000..6d0ce105 --- /dev/null +++ b/web/tests/unit/server-job-resource-budget.test.mjs @@ -0,0 +1,18 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createServerJobResourceReceipt, parseServerJobResourceBudget, SERVER_JOB_RESOURCE_LIMITS } from "../../../tools/web/server-job-resource-budget.mjs"; + +const budget = { schemaVersion: 1, ...SERVER_JOB_RESOURCE_LIMITS }; +const usage = { cpuMs: 10, memoryBytes: 1024, processCount: 1, fileCount: 2, wallMs: 20, outputBytes: 512 }; + +test("M13-04C accepts bounded usage and marks enforcement", () => { + assert.deepEqual(parseServerJobResourceBudget(budget), budget); + assert.deepEqual(createServerJobResourceReceipt(budget, usage), { schemaVersion: 1, status: "SUCCEEDED", budget, usage, enforced: true, exceeded: [] }); +}); + +test("M13-04C rejects each resource overage with a stable code", () => { + for (const field of Object.keys(SERVER_JOB_RESOURCE_LIMITS)) { + assert.throws(() => createServerJobResourceReceipt(budget, { ...usage, [field]: SERVER_JOB_RESOURCE_LIMITS[field] + 1 }), new RegExp(`SERVER_JOB_BUDGET_EXCEEDED.*${field}`)); + } + assert.throws(() => parseServerJobResourceBudget({ ...budget, unknown: 1 }), /SERVER_JOB_BUDGET_INVALID/); +}); diff --git a/web/tests/unit/server-job-result-binding.test.mjs b/web/tests/unit/server-job-result-binding.test.mjs new file mode 100644 index 00000000..c7d18a9a --- /dev/null +++ b/web/tests/unit/server-job-result-binding.test.mjs @@ -0,0 +1,33 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import test from "node:test"; +import { commitServerJobResult, verifyServerJobResultReceipt } from "../../../tools/web/server-job-result-binding.mjs"; + +const h = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const identity = { requestId: "request-1", projectId: "project-1", baseRevision: 7, sourceSha256: h("source"), settingsSha256: h("settings"), buildSha256: h("build") }; + +test("M13-04I commits only a readback-verified result and binds four identity hashes", async () => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04i-binding-")); + try { + const receipt = await commitServerJobResult(identity, new Uint8Array([1, 2, 3]), { outputDirectory: directory }); + assert.equal(receipt.status, "COMMITTED"); + assert.equal(receipt.publish, true); + assert.equal((await verifyServerJobResultReceipt(receipt, identity, directory)).verified, true); + assert.equal(receipt.outputByteLength, 3); + } finally { await fs.rm(directory, { recursive: true, force: true }); } +}); + +test("M13-04I blocks tamper, stale identity and partial/quota failures", async () => { + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "m13-04i-negative-")); + try { + await assert.rejects(commitServerJobResult({ ...identity, settingsSha256: h("changed") }, new Uint8Array([1]), { outputDirectory: directory, expectedIdentity: identity, faultAt: "AFTER_STAGE" }), /SERVER_JOB_RESULT_IDENTITY_MISMATCH/); + assert.deepEqual(await fs.readdir(directory), []); + const receipt = await commitServerJobResult(identity, new Uint8Array([4, 5]), { outputDirectory: directory }); + await fs.writeFile(receipt.outputPath, new Uint8Array([9])); + await assert.rejects(verifyServerJobResultReceipt(receipt, identity, directory), /SERVER_JOB_RESULT_HASH_MISMATCH/); + await assert.rejects(commitServerJobResult({ ...identity, requestId: "request-2" }, new Uint8Array([8]), { outputDirectory: directory, faultAt: "QUOTA" }), /SERVER_JOB_RESULT_STORAGE_QUOTA/); + } finally { await fs.rm(directory, { recursive: true, force: true }); } +}); diff --git a/web/tests/unit/stl-export.test.mjs b/web/tests/unit/stl-export.test.mjs new file mode 100644 index 00000000..81d5506b --- /dev/null +++ b/web/tests/unit/stl-export.test.mjs @@ -0,0 +1,42 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "stl-export-unit-")); +const sourcePath = path.join(root, "web/protocol/stl-export.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "stl-export.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const document = { + schemaVersion: 1, + variant: "STL_BINARY", + unitScale: 1, + declaredTriangleCount: 1, + triangleCount: 1, + removedDegenerateTriangles: 0, + normals: [[0, 1, 0]], + vertices: [[[-1, 0, 1], [1, 0, 1], [1, 0, -1]]], + bounds: { min: [-1, 0, -1], max: [1, 0, 1] }, +}; + +test("M12-07F serializes binary STL and reports material loss", () => { + const output = protocol.exportBinarySTL(document); + assert.equal(output.byteLength, 134); + assert.equal(new DataView(output).getUint32(80, true), 1); + assert.deepEqual(protocol.createSTLLossReport(2), { + schemaVersion: 1, + operation: "STL_EXPORT_LOSS_REPORT", + canRoundTrip: true, + warningCount: 1, + warnings: [{ code: "STL_MATERIAL_UNSUPPORTED", severity: "warning", message: "STL has no material slots; 2 source material assignments are omitted" }], + }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/stl-import.test.mjs b/web/tests/unit/stl-import.test.mjs new file mode 100644 index 00000000..284dacb1 --- /dev/null +++ b/web/tests/unit/stl-import.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "stl-import-unit-")); +const sourcePath = path.join(root, "web/protocol/stl-import.ts"); +const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(transpiled.diagnostics, []); +const modulePath = path.join(temporary, "stl-import.mjs"); +fs.writeFileSync(modulePath, transpiled.outputText); +const protocol = await import(pathToFileURL(modulePath)); +const fixtureRoot = path.join(root, "tests/files/web/m12_stl_edges_v1"); +const bytes = (name) => { + const value = fs.readFileSync(path.join(fixtureRoot, name)); + return value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength); +}; + +test("M12-07E parses binary/ASCII normals and explicit unit scales", () => { + const binary = protocol.importSTL(bytes("capability-binary.stl"), { variant: "STL_BINARY", unitScale: 1 }); + assert.equal(binary.triangleCount, 2); + assert.deepEqual(binary.normals, [[0, 1, 0], [0, 1, 0]]); + assert.deepEqual(binary.bounds, { min: [-1, 0, -1], max: [1, 0, 1] }); + const scaled = protocol.importSTL(bytes("capability-binary.stl"), { variant: "STL_BINARY", unitScale: 0.001 }); + assert.deepEqual(scaled.bounds, { min: [-0.001, 0, -0.001], max: [0.001, 0, 0.001] }); + const ascii = protocol.importSTL(bytes("../m12_stl_capability_v1/capability-ascii.stl"), { variant: "STL_ASCII", unitScale: 1 }); + assert.equal(ascii.triangleCount, 2); + assert.deepEqual(ascii.normals, binary.normals); + assert.deepEqual(ascii.bounds, binary.bounds); +}); + +test("M12-07E matches Blender's degenerate removal and blocks trailing bytes", () => { + const degenerate = protocol.importSTL(bytes("degenerate-binary.stl"), { variant: "STL_BINARY", unitScale: 1 }); + assert.equal(degenerate.declaredTriangleCount, 2); + assert.equal(degenerate.removedDegenerateTriangles, 1); + assert.equal(degenerate.triangleCount, 1); + assert.throws(() => protocol.importSTL(bytes("trailing-binary.stl"), { variant: "STL_BINARY", unitScale: 1 }), /STL_TRAILING_BYTES/); + assert.throws(() => protocol.importSTL(bytes("capability-binary.stl"), { variant: "STL_BINARY", unitScale: 0 }), /STL_UNIT_SCALE_INVALID/); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/viewport-dpr.test.mjs b/web/tests/unit/viewport-dpr.test.mjs new file mode 100644 index 00000000..945b7859 --- /dev/null +++ b/web/tests/unit/viewport-dpr.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "../../node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(root, "web/protocol/viewport-dpr.ts"); +const output = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true }); +assert.deepEqual(output.diagnostics, []); +const dpr = await import(`data:text/javascript;base64,${Buffer.from(output.outputText).toString("base64")}`); + +test("M14-04B clamps DPR and computes stable backing dimensions", () => { + for (const [observed, expected] of [[1, 1], [1.5, 1.5], [2, 2], [3, 2], [0, 1], [Number.NaN, 1]]) { + const metrics = dpr.resolveViewportPixelMetrics(101, 57, observed); + assert.equal(metrics.pixelRatio, expected); + assert.deepEqual([metrics.backingWidth, metrics.backingHeight], [Math.floor(101 * expected), Math.floor(57 * expected)]); + } +}); + +test("M14-04B uses CSS bounds for DPR-independent NDC", () => { + assert.deepEqual(dpr.viewportNDC(150, 75, { left: 100, top: 25, width: 100, height: 100 }), { x: 0, y: 0 }); + assert.throws(() => dpr.viewportNDC(0, 0, { left: 0, top: 0, width: 0, height: 1 }), /VIEWPORT_BOUNDS_INVALID/); +}); diff --git a/后续工作.txt b/后续工作.txt index 85c23cf5..d62b4700 100644 --- a/后续工作.txt +++ b/后续工作.txt @@ -1,5 +1,5 @@ -Web Blender M6/M7/M8/M9/M10/M11 接续执行记录 -更新时间:2026-08-17(America/New_York) +Web Blender M6/M7/M8/M9/M10/M11/M12/M13 接续执行记录 +更新时间:2026-08-18(America/New_York) 执行规则 1. 本文件是当前接续入口;每轮先读本文件,再直接领取机器队列返回的最新 `nextTask`。 @@ -7,6 +7,11 @@ Web Blender M6/M7/M8/M9/M10/M11 接续执行记录 3. 每完成一段,写回实际命令、结果、hash 和当前计数;后续任务名只从机器队列的最新 `nextTask` 获取,不在本文手工维护或推导。 4. 不覆盖用户已有改动,不提交或改写 git 历史。 +5. 本文件下方为历史接续记录;新一轮只需先读 `docs/EXECUTION_QUEUE.md`、当前 manifest 和 + 对应 status 页,不重新加载全部历史。 +6. 铁律:浏览器执行、CI、验收证据和发布声明永久仅限 Chromium;禁止启动、探测或领取 + Firefox/WebKit 测试任务。历史报告仅作归档背景,缺失 WebGPU 只能在 Chromium 中 + fail-closed 为 `BLOCKED`。 当前状态 - M6 正式完成:71/71;剩余:0。 @@ -16,9 +21,10 @@ Web Blender M6/M7/M8/M9/M10/M11 接续执行记录 - M10 正式完成:15/15;剩余:0。 - M11 正式完成:14/14;剩余:0。 - 已完成到:M11-14。 -- M12 当前完成:M12-01A-I、M12-02A-H 与 M12-03A-D。 -- 已完成到:M12-03D。 -- 下一领取点:机器队列的最新 `nextTask` 为 `M12-03E`。 +- M12 当前完成:M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A-F、M12-06A-G 与 M12-07A-J。 +- 已完成到:M12-07J。 +- M13 当前完成:M13-01A-F、M13-02A-F、M13-03A-G、M13-04A-C;剩余脚本安全原子任务按机器队列继续。 +- 下一领取点:机器队列的最新 `nextTask` 为 `M13-04F`。 - quick lane:READY,7/7 命令通过,总命令耗时 11,455 ms,报告 release/ci-reports/quick.json。 - Chromium lane:READY,9/9 命令通过;每个 browser server 使用独立动态端口,报告 @@ -2787,3 +2793,1326 @@ M12-03D 实际验证(2026-08-17 America/New_York) - `docs/status/M12-03D.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 保持 `parityStatus=BLOCKED`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 `M12-03E`。 + +M12-03E 实际验证(2026-08-17 America/New_York) +- 领取 M12-03E:在 M12-03D 的 WASM Main append 之上,读取 M12-03C desktop canonical report, + 对 Object/Mesh/Material/Image 的依赖边、stable ID、ownership、geometry、UV、material slot、 + packed image metadata 与 Float32 pixel hash 做精确比较。图像比较规范化 Blender bottom-up + `Image.pixels` 与 Canvas top-down row order;当前 SceneIR 缺省 `Image.colorSpace` 使用 desktop + sRGB semantic default,若字段出现则必须匹配。 +- `node ../tools/web/check-library-main-append.mjs` 通过 manifest、source/target、desktop report + 与测试源码 hash 门;`WEB_TEST_PORT=5194 npm --prefix web run test:library-main-append` 通过 + checker 与 Chromium 1/1。append 为一个 transaction、revision 只增加 1;stale base revision + 返回 `REVISION_CONFLICT`,local ID collision 返回 `ASSET_MANIFEST_INVALID` 且 revision 不变。 +- undo 后 Object/Mesh/Material/Image 四个 closure ID 全部移除;redo 恢复 canonical graph;保存 + buffer 在独立 client 重开后依赖图和 packed image hash 仍与 desktop report 一致。像素 hash 为 + `6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a`。 +- hashes:checker `7fba72c5a38c0877f03682b51cbaaaf6d9a5f315f2beddfdef432bc54b2f1cd2`;test + `101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0`;manifest + `beaa88ea0385225e712f9816072420bd8744c15da3229ddc352462abec407739`;package + `36a29c5be9bd6163b06cf3511edf77d932388fa0b75c08bdcec397e19c0ef45f`;source/target `.blend` + `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606` / + `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63`;desktop report + `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a`。 +- `docs/status/M12-03E.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStatus=BLOCKED`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03F`。 + +M12-03F 实际验证(2026-08-17 America/New_York) +- 领取 M12-03F:新增独立 Blender 5.2 desktop LINK fixture。source library 创建一个 Object, + 通过 Mesh material slot 和 Material Image Texture node 带出 Mesh/Material/packed Image 闭包; + target 用 `bpy.data.libraries.load(link=True)` 只链接 Object root。 +- `npm --prefix web run test:library-link-desktop` 通过 generator rerun、artifact hash、canonical + report 和 save/reopen 检查。四个 linked ID 的 `library` 均为 `m12_link_source.blend`, + `isLibraryOverride=false`,四条 stable mapping 均为 `SOURCE_LIBRARY/readOnly=true`;依赖边、 + 4/4/1 geometry、UVMap、material slot、2x2 sRGB packed image 与 Float32 pixel hash 均稳定。 +- hashes:generator `15a2e34c1c5b2a8ee63b10084dbb894e0f9677b9e1cf4adb7e2ddce6b4c965b1`;checker + `6a4c024bea227d7bc14f793467b91766b006459fe4d7717cc75dfee12f49f894`;source/target `.blend` + `fae97569e9d2e2066fc92749672f86cc42717cd9b97b012faeb9eb92015d7fd1` / + `acdaf0f297deb08c84e1a8beba30972e3414ef62e60e3b103fe0661199c438a1`;desktop report + `b278d4c254eff63d41c8cb3ea1d5e0984192137d45b1695ba0672e16f95b58ea`;manifest + `8220a8f5d560d45a75a62cbed645b3febc1390fe37b07c3c48871fefc1a9b159`;package + `336d8df1914aaaafaeccc181d4e73ed7058165f10e167efe539939c3ac1e0536`。 +- `docs/status/M12-03F.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03G`。 + +M12-03G 实际验证(2026-08-17 America/New_York) +- 领取 M12-03G:新增 schema 1 linked data mutation gate,覆盖 object transform、mesh geometry、 + mesh material slot、material properties、material image node 与 packed image data 六类 writer。 + request 必须保持 `SOURCE_LIBRARY/linkedLibrary=true/readOnly=true`。 +- `npm --prefix web run test:library-linked-mutation` 通过 3/3。六类当前 revision writer 全部返回 + `BLOCKED/LINKED_DATA_MUTATION_BLOCKED` 且 `recoverable=false`;stale revision 返回 + `REVISION_CONFLICT`,未知字段、operation 和 ownership substitution 均在 Main writer 前阻断。 +- hashes:protocol `f629e0e7e04dc1f5437b6e2ca62fbe35484fc238830fa47e8358bcab46b7e104`;unit + `f19d47cefe89daf6123062e045ec717e6ffe60977e8a4b20c996dd62e49da211`;manifest + `caf17ce3c0fe9217ee8a727b35b6c479a1eb8b6cd634c4964eabfc24ecf8c756`;package + `51665ca48e57b7abc953f3012587300e2ac986bf24ce1a7f6b19ff109ecdcf33`。 +- `docs/status/M12-03G.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03H`。 + +M12-03H 实际验证(2026-08-17 America/New_York) +- 领取 M12-03H:新增 schema 1 linked library reload protocol。请求绑定 source library、expected + generation/revision 和 replacement snapshot;replacement 必须保持 `SOURCE_LIBRARY/readOnly=true` + data-block closure,并且 generation 只能前进一代。 +- `npm --prefix web run test:library-linked-reload` 通过 4/4。匹配 generation 只替换一个 linked + snapshot,其他 library/generation 保持不变;stale、source substitution、duplicate identity、 + non-adjacent generation 和 undeclared field 均在发布前 fail-closed。 +- hashes:protocol `8be6f0b2abe36cd566ea7447d1b7de44c0e6a9a7351b863dfdd1372c1761060e`;unit + `dff866291468cc01e775fe3b3b95c632f5c0742566f79b956a0193bfd8fd43d2`;manifest + `271fea5f52fcc044f029588a4c2a431e7ab0329c7bcc1abfad8e376ee5b4b2d8`;package + `39aa1ca3e1988ebbb8e84bb60abeda43be4c99d475c1d8cd8f3a6d96b094d0c8`。 +- `docs/status/M12-03H.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03I`。 + +M12-03I 实际验证(2026-08-17 America/New_York) +- 领取 M12-03I:新增 schema 1 missing-library protocol。匹配 source library/generation/revision + 时只把引用标记为 `MISSING` 并生成 `MISSING_LIBRARY` placeholder,原始 locator、source SHA、 + generation、revision 和 data-block IDs 全部保留。 +- `npm --prefix web run test:library-linked-missing` 通过 4/4。stale revision、source hash drift、 + undeclared field、duplicate identity 和 invalid placeholder 均在状态发布前阻断。 +- hashes:protocol `5833e8c943ef6bd866a93a0e1666c9521fa6d3e8358861df57b628874b679ec2`;unit + `4ab6d6ff4845b4ca963399e7eea214ceb412871dc1fdef5bac1ed0333596da4f`;manifest + `9f3a6aa36b6227cae4412333190f44fc3f5d971da1b36f8029c635833055b261`;package + `76454e86a7485ed0cb1fb3c59e034c328c23b02634425e883bac1cff463b182f`。 +- `docs/status/M12-03I.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03J`。 + +M12-03J 实际验证(2026-08-17 America/New_York) +- 领取 M12-03J:用真实 Blender 5.2 library override API 生成 source/target fixture。reference 保持 + `SOURCE_LIBRARY/readOnly=true`,local hierarchy root 为 `LOCAL_OVERRIDE/readOnly=false`,并记录 + 唯一 `["m12_override_value"]` property path、`REPLACE` operation 与值 `2.5`。 +- `npm --prefix web run test:library-override-desktop` 通过;fixture 重新生成、保存重开后与 desktop + report 一致,source/reference、local owner、hierarchy root 和 property metadata 均稳定。 +- hashes:generator `2cdbac04cac7240360a9d70919380fd90f9479e2cb41d8032fb51626ed4b4dd6`;checker + `afebb3c9b8715b9d2e0c9b17f463f038bd23e8747074137bccbf1c09c4bfb5ba`;source/target `.blend` + `d7f8d78e7e91481bf46ecc9a6bcb01e900a6a397839dd31ab80a53def7675601` / + `b008a1608ff1e8f679e1e1281bf7be0360f1137e815dd890cbd93e1e98675495`;report + `19cb13a3417b4b65a8497b622337c42c4697b3f3d48de26a1f70f2d4527ddde0`;manifest + `89c1f84cdd7bcb8a2b104f50f4cdff6baf914db5620923b467c3747cd89a501e`;package + `25b89e621eab6484b9f9311b5f2f75af5dde035d21b8f43f190ef5a143d167c1`。 +- `docs/status/M12-03J.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03K`。 + +M12-03K 实际验证(2026-08-17 America/New_York) +- 领取 M12-03K:新增 schema 1 override writer,只开放 J 冻结的 + `["m12_override_value"]` / `SET_M12_OVERRIDE_VALUE`,要求 `LOCAL_OVERRIDE/readOnly=false`、 + `referenceReadOnly=true` 与匹配 local/reference/hierarchy IDs。 +- `npm --prefix web run test:library-override-writer` 通过 4/4。合法请求只更新一个 value 并推进 + 一次 revision;stale、linked owner、identity drift、alternate property/operation、越界值和未知字段均阻断。 +- hashes:protocol `dd181c7e9946b98334a5d1c686887afecfe3fc11d732beec246e40bf11a155aa`;unit + `e41bd32eb4ce4d331a20ecb91f3325a27f461b9ae85e98940d5afd482ec21c4c`;manifest + `27b00abdd458f51c2143a137823ef4222eed4637b27bc8dc953d29bbfae57251`;package + `80cfff6337d0f8f095e2d4c1c111e9aba377b77010424c33f3a82b2df79f8858`。 +- `docs/status/M12-03K.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03L`。 + +M12-03L 实际验证(2026-08-17 America/New_York) +- 领取 M12-03L:新增 schema 1 override freshness gate,绑定 source library ID、generation、revision、 + dependency closure SHA-256、invalidation token 与 local/reference/hierarchy IDs。 +- `npm --prefix web run test:library-override-freshness` 通过 4/4。完整匹配只返回 `READY`;stale + generation/revision、closure/token drift、identity drift、linked owner、alternate operation 和 + malformed fields 均在 Main commit 前阻断。 +- hashes:protocol `2eff7ea7605b1579d7551336d87d4f30adb996b585596ff9eee67aea04ca7d22`;unit + `d48344f31e1ba12e557ca30ece56643583efa633da556f5de3896a8e9175ef8f`;manifest + `3966a2555ee5cec3aeb95e70d23c960e0813727032967f726dc9901a533fb162`;package + `9e87817eb9ad3ffed878dc17b62d63b11a37ddfd1c203e72e1c1c9c945e3ebc6`。 +- `docs/status/M12-03L.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03M`。 + +M12-03M 实际验证(2026-08-17 America/New_York) +- 领取 M12-03M:新增 schema 1 library negative-case validator,覆盖直接 dependency cycle、跨库环、 + data-block ID collision、missing source library 与 duplicate reload generation。 +- `npm --prefix web run test:library-negative-cases` 通过 4/4;cycle 返回 `LIBRARY_DEPENDENCY_CYCLE`, + collision 返回 `TASK_VALIDATION_FAILED`,duplicate reload 返回 `REVISION_CONFLICT`,missing source + 返回 `ASSET_SOURCE_HASH_MISMATCH`。 +- hashes:protocol `0aedd76a0081250d30a7da474896eb8445d2746d1ab9d7d4647440194bcef73a`;unit + `b7ae41b2c35b2fe1598bca4425dd434230bfe4bf342320c88214a060dcbb0a16`;manifest + `d699fc1f328ed966aa01cedfc58ba4f312a355ffebd3e16411da95b34ec5d879`;package + `e1c519d1ef3cf27df89e166b804dd280994b10f28cd736e11fa90b48478fd603`。 +- `docs/status/M12-03M.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03N`。 + +M12-03N 实际验证(2026-08-17 America/New_York) +- 领取 M12-03N:为 APPEND、LINK、LIBRARY_OVERRIDE 各建立独立 desktop/WASM/Chromium 命令,共 9 条 + 独立入口;Chromium 使用独立端口。 +- `npm run test:library-operation-commands` 通过;九条实测命令全部通过:append desktop/WASM/Chromium、 + link desktop/WASM/Chromium、override desktop/WASM/Chromium。 +- hashes:command checker `3564347393134d835fdf279b8b8f58558ee24fe0165c3b4527195d094a451e98`;append + WASM protocol/unit `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7` / + `75fc2d626f7ca7e820e9cacf659a453886e15e790cde43348828c03bed752ec7`;Chromium specs + `101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0` / + `814e3486522fb4f0ffdd59acd385a4fca9c5660cdb07e5a2acb1cadd70376bff` / + `1c12982b4eb4fb4b9a3c88907a842a1fc413b3a3a760a9f57b350f57060d007f`;manifest + `054c197b6db96b56e3d041de2fe815b24133d1748c3238273af09b71b1c344e1`;package + `e1c519d1ef3cf27df89e166b804dd280994b10f28cd736e11fa90b48478fd603`。 +- `docs/status/M12-03N.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-04A`。 + +M12-04A 实际验证(2026-08-17 America/New_York) +- 领取 M12-04A:新增 schema 1 library source-origin admission。只接受声明的 credential-free HTTPS + origin、project-relative asset 或 user-selected file(selection ID、safe file name、bounded byte + length、source SHA-256)。 +- `npm --prefix web run test:library-source-origin` 通过 4/4;accepted source 返回 canonical locator, + undeclared origin、credentials、unsafe path、empty policy、malformed selection 和 unknown field 均阻断。 +- hashes:protocol `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb`;unit + `89b207c9cb13b4fb055a2dfed0237c0f8a00b13a39cc4175a378f5ef2abdb7ae`;manifest + `af80827d925ddd96d8541e446e0f70c956fd4c56e64ac984d187f5449df4cb0d`;package + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- `docs/status/M12-04A.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-04B`。 + +M12-04B 实际验证(2026-08-17 America/New_York) +- 领取 M12-04B:扩展共享 project-asset path normalizer,统一 POSIX/Windows separator,消解 `.` + 与安全的 `..`,单次解码 percent-encoded UTF-8,并以 Unicode NFC 输出稳定 canonical path;规范化 + 结果可重复调用且保持同一 locator。 +- `npm --prefix web run test:library-path-normalization` 通过 4/4。separator/dot alias、encoded + separator/dot、decomposed Unicode、project-root escape、malformed percent 和二次解码均有正负例; + `npm --prefix web run typecheck` 通过,并收口上轮 library wrapper/parser 的静态类型错误。 +- hashes:normalizer `6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf`;source + admission `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb`;unit + `abde64c60397541e92a83dd9e4a24e65faf340a8d046650604c101a21037c777`;manifest + `8cd29c3f5281082584fc6aefe2ec385a2eedf8116e837a4db54c391391ff8f98`;package + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- `docs/status/M12-04B.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-04C`。 + +M12-04C 实际验证(2026-08-18 America/New_York) +- 领取 M12-04C:在共享 project-asset normalizer 上收紧 absolute POSIX、UNC/drive、NUL/控制字符、 + malformed/residual percent 和 URI/origin-shaped escape;HTTPS policy origin 现在拒绝 path/query/ + fragment、encoded controls 和 backslash smuggling,不把不安全声明静默折叠为裸 `URL.origin`。 +- `npm --prefix web run test:library-path-security` 通过 4/4;absolute/UNC/drive/raw+encoded control、 + project origin escape、unsafe HTTPS URI、policy smuggling 和 duplicate canonical origin 均有负例, + declared HTTPS resource path 仍通过;library unit 集合 43/43、N-023 Chromium asset/IO gate 1/1 与 + `npm --prefix web run typecheck` 均通过。 +- hashes:normalizer `6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf`;source-origin + `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb`;unit + `ab302cacb24634a3225dda5cb8f5282cb80b3bd81ed5c8900ddf4ff18267b7e2`;manifest + `a7f3a45eb7f68d022f38185a1c1409e1db1b27647fef587d66d2ffa52d78f98e`;package + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- `docs/status/M12-04C.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-04D`。 + +M12-04D 实际验证(2026-08-18 America/New_York) +- 领取 M12-04D:新增 archive link-safety protocol,在任何写入前解析全部 FILE/DIRECTORY/ + SYMLINK/HARDLINK entry。symlink 相对其父目录解析,hardlink 从 archive root 解析且只能指向文件; + 每个输出都带 `withinTemporaryRoot=true`。 +- `npm --prefix web run test:library-link-safety` 通过 5/5;absolute/drive/URI/backslash/traversal、 + missing target、cycle、duplicate path、hardlink-to-directory 和 unknown field 均返回 + `IO_ARCHIVE_UNSAFE`。 +- hashes:protocol `d55a4ba762898aed22bdcc7aa6493c713ee94f6bb1bf58754fdc459d0ddf70d1`;unit + `7739275be91222d7bfb61d2f5a1daf812c6860fe34d0aea27df8380a77322120`;manifest + `1c09abd1f4bd5908d22ab3b22e3e71d050f694af0b82a7f78a351d7a1bf1e664`;package + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- 本项不改变 parity ledger;机器队列唯一下一任务切换为 `M12-04E`。 + +M12-04E 实际验证(2026-08-18 America/New_York) +- 领取 M12-04E:ZIP 只先计划 bounded CENTRAL_DIRECTORY read,TAR 只先计划 bounded MANIFEST + read;初始 plan 不含 payload range,trace 必须以声明的 exact metadata range 开始。 +- `npm --prefix web run test:library-metadata-first` 通过 4/4;payload-first、wrong range、duplicate + metadata、out-of-order 与 64 MiB metadata budget overflow 均 fail-closed。 +- hashes:protocol `869586b041e134c7d1cb2ebd7e13b35218b337223d401697a179f71cd1420f5b`;unit + `2da649722acee9cace8db6f337b4a93500a9c775a0b0f086bf38dd2b3e7f9e91`;manifest + `d935392fb23c2e6ad651fb6ad6cb67f8ead3d562e626bb230d2febd9d7f03b1c`。 +- 本项不改变 parity ledger;机器队列唯一下一任务切换为 `M12-04F`。 + +M12-04F 实际验证(2026-08-18 America/New_York) +- 领取 M12-04F:production IO parser 在 payload allocation 前执行 100,000 entry、2 GiB 单 entry、 + 4 GiB total、64 directory depth 与 255-byte UTF-8 filename budget,并保留 compression/source + byte gate。 +- `npm --prefix web run test:library-archive-budget` 通过 4/4,边界值接受且逐项 overflow 返回稳定错误。 +- hashes:protocol `e02efa79668f4ee10b1c28786709eba2c93691b28ccf1155c6213dda12f59a8f`;unit + `b0d8356c6fb348d98e28ce220052845a29439b34b3c976b21a4dbf370ea19593`;manifest + `cda905b1e27b62d9ea3a05170f975015480ce6739c15bdf1f5a1f0b79f93ce06`。 +- 本项不改变 parity ledger;机器队列唯一下一任务切换为 `M12-04G`。 + +M12-04G 实际验证(2026-08-18 America/New_York) +- 领取 M12-04G:archive range validator 拒绝超过 100:1 的展开比、zero-byte bomb、compressed + range overlap、source/global bound overflow、duplicate canonical path 与 file/directory prefix conflict。 +- `npm --prefix web run test:library-archive-conflicts` 通过 4/4;合法 non-overlapping range 返回 + deterministic compressed/uncompressed totals 和显式安全 invariant。 +- hashes:protocol `3bec372e4f67ec309f28534cebcbaf8b492144adfa82ff6c8603f88c3ba16254`;unit + `3f2d44dce33b1c17b3a48f58b04fdd821abc88d98ce3d3b5bb724859e0f0ab3c`;manifest + `c35e675e8f512e85b748f0b5578874fed8c99df7674a152e698236a67c9f4fa4`。 +- 本项不改变 parity ledger;机器队列唯一下一任务切换为 `M12-04H`。 + +M12-04H 实际验证(2026-08-18 America/New_York) +- 领取 M12-04H:新增 production archive extraction transaction,在 staging 前、每次 payload + read/write 周围与 atomic commit 前检查取消。提交前取消统一删除 staging、返回 + `IO_ARCHIVE_CANCELLED`、发布数为 0,并重读 committed revision/SHA-256 证明项目未改变。 +- `npm --prefix web run test:library-archive-cancellation` 通过 6/6 真实临时目录测试;覆盖 staging + 前、部分写入后、最后写入后取消、成功 commit、payload mismatch、rollback drift、stale revision、 + unsafe path、prefix conflict 和 unknown field。`npm --prefix web run typecheck` 通过。 +- hashes:protocol `c40adc1449172014cc1820db567e155828314abb404b66e4de13c26ddee06e4b`;error + `751c70c898540fa7e82fb1b1a79254756ec8db8e4201a88b6d817d7c3d92103f`;unit + `77e7d5bc64dd6b313006ebf523602601acdaf7949a1484da872ddcd046983786`;manifest + `7fce600a416aec5ade1a91a13d86caf4cb1f65b710054b59d525ff8b1d3c7409`。 +- `docs/status/M12-04D.md` 至 `M12-04H.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件 + 已同步;本项保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一 + 下一任务切换为 `M12-04I`。 + +M12-04I 实际验证(2026-08-18 America/New_York) +- 领取 M12-04I:新增 archive extraction recovery wrapper。底层 M12-04H transaction 先完成 + staging discard 与 committed revision/SHA-256 复核,只有该回滚门通过后才把 + `QuotaExceededError`/`NotEnoughSpaceError` 映射为 `STORAGE_QUOTA`,把显式 + `WASM_OUT_OF_MEMORY`/`OutOfMemoryError`/OOM RangeError 映射为 `WASM_OUT_OF_MEMORY`;普通 + RangeError、普通 IO 和 cleanup/identity drift 不会被误分类。 +- `node --test web/tests/unit/library-archive-recovery.test.mjs` 通过 4/4 真实临时目录测试:quota + 与 OOM 都在 4 KiB payload 部分写入后注入,staging entry 归零,旧 revision=9、旧 SHA-256 和 + `old-project` bytes 不变;同一 storage instance 随后以 revision=10 提交 `small-project`, + staging 仍为零。资源 fault code 负例也通过。 +- `npm --prefix web run test:library-archive-cancellation` 通过 6/6;`npm --prefix web run typecheck` + 和 `git diff --check` 通过。为保持 M12-03G 至 M12-04H 已冻结的 package hash,不修改 + `web/package.json`;M12-04I 的独立入口为上面的 `node --test` 命令。 +- hashes:base transaction `c40adc1449172014cc1820db567e155828314abb404b66e4de13c26ddee06e4b`; + recovery protocol `0ae9801ea151403b244c5f58f7f99231bba7a25abb1f61e3669879510b92dccf`;unit + `a931edef8f3ca1243a80ec2b8e9ff5b8da1dfd339f3d8c162ad2ebd08d3db6a1`;manifest + `574afa68a787b9481d0e098d38b94cb810a9827fe185b0df796d62cc160ba7c5`;package + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- `docs/status/M12-04I.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务 + 切换为 `M12-04J`。 + +M12-04J 实际验证(2026-08-18 America/New_York) +- 领取 M12-04J:新增确定性恶意 archive fixture 生成器,固定 3 个 ZIP(path traversal、101:1 + compression ratio、duplicate path)与 3 个 USTAR(path traversal、symlink escape、file/directory + prefix conflict)二进制容器。所有 header 时间/所有权字段固定,catalog 绑定逐文件 bytes/SHA-256。 +- checker 在新临时目录重建 fixture 并逐字节比较;ZIP 校验 EOCD、central/local header identity 与 + metadata/payload range,TAR 校验 USTAR magic、header checksum、512-byte alignment 与双零结束块。 + 真实 entry metadata 进入 M12-04D/G link/conflict gate 后 6/6 返回 `IO_ARCHIVE_UNSAFE`;全过程 + 不调用 `tar`、`unzip` 或 extraction API。 +- `node tools/web/check-malicious-archive-fixtures.mjs` 通过,输出 + `cases=6 zip=3 tar=3 extraction=disabled`;`WEB_TEST_PORT=5408 npm --prefix web run + test:asset-library` 通过 2/2,新 binary fixture spec 已进入现有 Chromium malicious-archive lane。 +- hashes:generator `b372ea8cb2f97b035ffb2cc18666dae9167dcfb349131851ad9f1ff0fc40ba16`; + checker `edda2f420f26e55f9990d24b755bb9b4f732ec32c2e072210144b3d0b6634d9b`;fixture manifest + `a93834316f6a23b8a0e6c1f1f806ec584d6f03aa339c2680cae2ce8133a40e58`;Chromium spec + `f327500808dd67359a152ce28134b58d027aebd6758416b5535b659b9900bbfe`;golden manifest + `989d417ab44e165849c7054f331f7038ec1d779349c39dd4f5b30050bbaecf56`。 +- `docs/status/M12-04J.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。M12.4 A-J 收口,机器 + 队列唯一下一任务切换为 `M12-05A`。 + +M12-05A 实际验证(2026-08-18 America/New_York) +- 领取 M12-05A:新增 `tools/web/generate-io-format-runtime-inventory.py`,由 pinned + `build_blender_5.2.0/bin/blender` 的真实 `bpy.app`/RNA runtime 生成 GLTF、GLB、OBJ、STL、PLY、 + USD、ALEMBIC 七条 format receipt。每个 import/export operator 记录注册状态、RNA identifier、 + canonical property/enum schema、extensions、variants 和对应 build option;runtime 记录 + Blender 5.2.0 LTS、build hash/branch/platform/type/date/time、commit timestamp、selected + build options 与 binary SHA-256。 +- 当前 pinned build 的 GLTF/GLB/OBJ/STL/PLY 为 `AVAILABLE`;`usd=false`、`alembic=false` 时 + USD/ALEMBIC 为 `OPERATOR_UNREGISTERED`,没有按扩展名放行。清单是 inventory evidence,不 + 声明 import/export round-trip parity。 +- `node tools/web/check-io-format-runtime-inventory.mjs` 通过;新 Blender 进程重复生成清单并 + 逐字节匹配,format count=7、available=5、build-disabled=2。binary SHA-256 为 + `d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82`。 +- hashes:generator `aa926397664240a5195f8864fc3ed5549bafcc963a03c513c7e37926b0559d7d`; + checker `12853306ea5eb2698ab636c7dfc2f27ae140295641473c57b84f93fa13d4864e`;inventory + `0d660b0fd8b647ebbf4e91afebd5006bd100973ab8e2507a5bfe759f477b33b4`;manifest + `202b144c91f8e2c39477e257aeb26f9bd0b1b05b459ff8a246668024e94deec7`。 +- `docs/status/M12-05A.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务 + 切换为 `M12-05B`。 + +M12-05B 实际验证(2026-08-18 America/New_York) +- 领取 M12-05B:新增 schema 1 `io-format-capability-matrix`,绑定 M12-05A inventory SHA-256, + 为 GLTF/GLB/OBJ/STL/PLY/USD/ALEMBIC 各自声明 IMPORT/EXPORT 的 local/server route 与 + geometry/material/animation feature status。只有已有 bounded GLB EXPORT local route 为 + `READY/LOCAL`;7 条 import、7 条 server 和其余 6 条 local export 共 27 条 route 均为 + `BLOCKED/IO_FORMAT_UNSUPPORTED`,未实现 feature 保持 `UNVERIFIED`,GLB export feature 只标 + `PARTIAL` 并明确依赖 M12-06 round-trip。 +- `node --test web/tests/unit/io-format-capability-matrix.test.mjs` 通过 3/3,覆盖 duplicate + format、ready-without-executor、unverified-ready-feature 和 missing blocked-code 负例。 + `node tools/web/check-io-format-capability-matrix.mjs` 通过,matrix generator 新进程逐字节 + 重建并确认 runtime status 与 M12-05A receipt 一致。 +- hashes:protocol `3063ae5e45f5b1642d329aa554187d121998d816a5a6740a2b9662f00c3c5738`;generator + `746078caaf29fa5aa2281b901b9ea5fc66f9a935eb3f6e282d4fbfb018d4c390`;checker + `4a8b35cd7f87238c13627a00c3bb0b34c130fc34d597773574efac7d8909b804`;unit + `0cc4d8f1df1ecdab20d8100cf5f12b44cb2a68bca4384ef7964a032b2f74b36e`;matrix + `139c9d764736da176b32414ecda840c07eb0ba5f3864da2dc08ce04bae161366`;manifest + `8cc9517c9f25138c351bc5a79efe3b1ce6d9f6663d3b7c14397c5e4e8f11181a`。 +- `docs/status/M12-05B.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务 + 切换为 `M12-05C`。 + +M12-05C 实际验证(2026-08-18 America/New_York) +- 领取 M12-05C:新增 schema 1 `io-format-ui-gate`,从 M12-05B capability matrix 的精确 + SHA-256 生成 UI registry;只有 runtime operator=`AVAILABLE` 且 local route=`READY` 的组合 + 才进入 registry。当前 import routes=0、local export routes=1(GLB),项目文件 accept 固定为 + `.blend,application/octet-stream`。 +- App 的 `.blend` file input 使用 registry-derived `accept` 与第二道 filename gate;未知或 + matrix-blocked extension 在 Engine 前返回 `IO_FORMAT_UNSUPPORTED`,不替换当前 SceneIR。 + operator search 通过同一 registry 过滤 format-tagged commands,blocked server/import/export + 组合不出现在可执行搜索结果。 +- `node --test web/tests/unit/io-format-ui-gate.test.mjs` 通过 3/3; + `node tools/web/check-io-format-ui-gate.mjs` 通过并逐字节复建 registry; + `WEB_TEST_PORT=5413 npm --prefix web run test:io-format-ui-gate` 通过 unit 3/3、checker 和 + Chromium 1/1。首次未指定端口的命令因 5173 已占用失败,动态端口重跑通过。 +- `npm --prefix web run typecheck`、`npm --prefix web run build` 通过; + `WEB_TEST_PORT=5412 npm --prefix web run test:asset-library` 通过 2/2,N-023 asset regression + 保持通过。`git diff --check` 通过。 +- hashes:protocol `fc397ceb947d4ede6c34c1d51438253a6b59244fc40f5eb77ce155bf1c477476`;generator + `1ef4ae8a3e8d2f73101a87cba1c42ea99a065e1f6846f6a591841b97c0c39e6f`;checker + `81d6f27df26aab7b633fbe61c6d7b37519668aff41e43314924dceaacb3affde`;unit + `84ca8fb6022da9f167daa104c44489a6c7d9f059699e57ce621b8d7f64f19082`;Chromium spec + `eda2cdb9ede3bcbd717800c9c6fdb28d8cebef96f0296a2ee847a05e60cd0eed`;registry + `6b410bc6f2cad032af55bf13e1c8ddd841b01e9913ffc0ba381da8b7204285fd`;package + `a9a24755d8e541942571a69eeb2e2105dc9555501a31ac70435ee7928953d2d1`。 +- manifest `f46fd394e2144255d8b4304e8ce4fe60aad4302a80e991fd83d50cd915235ee3`。 +- `docs/status/M12-05C.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务 + 切换为 `M12-05D`。 +- 最终冻结复验:`WEB_TEST_PORT=5414 npm --prefix web run test:io-format-ui-gate` 通过(unit 3/3、 + checker、Chromium 1/1);8 个 manifest artifact 逐项 SHA-256 比对通过,输出 + `m12-05c-artifacts-ok count=8 next=M12-05D`。`npm --prefix web run test:status-consistency` + 输出 `families=12 parityBlocked=12 releaseBlocked=0 evidenceRecords=17 missing=0`; + `git diff --check` 通过。 + +M12-05D 实际验证(2026-08-18 America/New_York) +- 领取 M12-05D:新增 schema 1 `io-format-runtime-receipt`,从 pinned Blender 5.2 M12-05A + inventory 生成 14 条 import/export receipt;集合绑定 inventory SHA-256、runtime identity、 + operator path、RNA identifier、registered/build-option 状态、variants 和 extensions。 +- `resolveIOFormatRuntimeRoute` 只接受显式 `{format, operation}` 与 receipt=`AVAILABLE`、registered、 + RNA identity 和未禁用 build option;不读取 filename/extension。GLB EXPORT 返回 `READY`,USD/ + ALEMBIC 因 `OPERATOR_UNREGISTERED` 返回 `BLOCKED/IO_FORMAT_UNSUPPORTED`。 +- App 在 format-tagged operator search 过滤和 GLB export 执行前消费同一 receipt set;receipt JSON + 与 golden 逐字节相同,运行时 receipt gate 不修改现有 `.blend` file gate。 +- `node --test web/tests/unit/io-format-runtime-receipt.test.mjs` 通过 3/3; + `node tools/web/check-io-format-runtime-receipts.mjs` 通过;M12-05C direct unit/checker + Chromium + 1/1(5417)通过;N-023 asset/security 2/2(5418)通过;`npm --prefix web run typecheck`、 + `npm --prefix web run build`、`git diff --check` 均通过。 +- package hash 保持历史冻结值 `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`, + 专项 lane 使用 direct commands,不改写既有 release evidence。 +- hashes:protocol `461a84557fa368c29dbc6707959b689faae7c8f7050dccc4d3f12e358b61bb22`;generator + `796cd641e86d8242c13317f771ae237cbf1692ff675a53bbdb689615edb5f372`;checker + `aaf9862041f155f96f50ea8481ff765089255bc59ecd8944f12362b81b8c1f1a`;unit + `a5f09277f5dcdacd25c44191f7407d6cee944f8022b1c467c2a69e172fc2ac6b`;runtime/app receipts + `f2c9a77e2cfad0ef3574b804fc06fb22972c07f55c72b0e6db2e359c4d95f57b`;App + `74216aac70992f8d879e983237ca324b998008582f0cd4658e90994cf9fae0a8`;manifest + `3d0049a7b0331f01bb71df043270c18d1a5c9ce6dc74353c49c8ce591761df1b`。 +- `docs/status/M12-05D.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务 + 切换为 `M12-05E`。 +- 最终冻结复验:M12-05C/M12-05D artifact checker 分别输出 + `M12-05C-artifacts-ok count=8 next=M12-05D`、`M12-05D-artifacts-ok count=9 next=M12-05E`; + M12-05C+D unit/checker 全部 6/6;operator search 实际 Chromium 1/1(5419);N-023 asset/ + malicious archive 2/2(5420);`npm --prefix web run test:status-consistency` 输出 + `families=12 parityBlocked=12 releaseBlocked=0 evidenceRecords=17 missing=0`;typecheck 与 + `git diff --check` 通过。无 package hash 漂移。 + +M12-05E 实际验证(2026-08-18 America/New_York) +- 领取 M12-05E:新增 schema 1 `io-format-receipt-binding`,从 M12-05A inventory 与 M12-05D + receipt set 为 14 条 import/export receipt 生成 `sourceSha256`、`settingsSha256`、 + `runtimeSha256` 三重绑定;bound set 同时绑定 M12-05D parent receipt-set SHA-256 与 + M12-05A inventory SHA-256。 +- source hash 覆盖 format/family/operation/operator/registered/RNA identity;settings hash 覆盖 + build option、variants/extensions 和 Blender RNA property schema;runtime hash 覆盖完整 pinned + Blender runtime identity。校验器要求三个 SHA-256 和两个 parent identity 均存在后才解析 receipt。 +- `node --test web/tests/unit/io-format-receipt-binding.test.mjs` 通过 2/2; + `node tools/web/check-io-format-receipt-bindings.mjs` 通过,14 条 hash 独立复算且新进程逐字节 + 重建;`npm --prefix web run typecheck`、`git diff --check` 通过。package hash 保持冻结值。 +- hashes:protocol `681e719ab2b18eb85d056547fb70b461dd73b3a7fb4fdbe6295b8e49d5649e49`;generator + `eb7b5c499f141c1788bc37e53585662eedff3f2dabff870f4ad166f4a619796f`;checker + `ec2b22b468809ecc25ab2d4983065680a66ad3be6705d1827b44bf45ac622e26`;unit + `e9ae3e360ad41c32da3634a4efa915654853e79a35df36728c1ddf586a0bf7b5`;bound receipts + `7f765bf16b62466f579b3de00751a0e012fef1c788f229c8e9675a57caa18aad`;manifest + `2fbaaf39c6acd9f55fbdbadccc0b027e9e7763bf069c954a96ca49b193648990`。 +- `docs/status/M12-05E.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务 + 切换为 `M12-05F`。 + +M12-05F 实际验证(2026-08-18 America/New_York) +- 领取 M12-05F:新增 schema 1 `io-format-receipt-freshness` envelope,封装 M12-05E bound + receipt set,并固定 M12-05E 原始 bytes 的 `parentBindingSha256`、canonical bound receipt + set 的 `boundReceiptSetSha256` 和 pinned Blender runtime 的 `runtimeSha256`。使用前同时核对 + parent binding、M12-05D receipt-set parent、M12-05A inventory、runtime identity、14 条 + receipt 的 runtime hash 和 GLB/其它 route 的注册状态;route 只在全部 freshness 条件满足时 + 返回 `READY`。 +- 新增 `web/protocol/io-format-receipt-freshness.ts`、生成器/checker、M12-05F golden、App + freshness receipt/expected identity artifact 与 unit。App 的 GLB export 和 format-tagged + operator search 已从未绑定 M12-05D receipt 切换到 M12-05F freshness route。 +- `node --test web/tests/unit/io-format-receipt-freshness.test.mjs` 通过 4/4;精确 trusted + receipt、canonical digest、伪造字段、过期 parent/inventory 和跨版本 runtime 均覆盖。 +- `node tools/web/check-io-format-receipt-freshness.mjs` 通过:14 receipts、App artifact + 逐字节一致、独立 canonical digest、deterministic regeneration、forged/stale/cross-version + 三类阻断全部通过,输出 + `io-format-receipt-freshness-ok receipts=14 forged=BLOCKED stale=BLOCKED cross-version=BLOCKED deterministic=true`。 +- 既有 M12-05D/M12-05E unit/checker 通过:5/5 unit,runtime receipt checker 与 binding checker + 均退出 0;`npm --prefix web run typecheck`、`npm --prefix web run build`、 + `npm --prefix web run test:status-consistency` 和 `git diff --check` 均通过。状态输出仍为 + `families=12 parityBlocked=12 releaseBlocked=0 evidenceRecords=17 missing=0`。 +- Chromium 定向 `WEB_TEST_PORT=5423 npm --prefix web exec playwright test --config + playwright.config.ts --workers=1 tests/e2e/io-format-ui-gate.spec.ts` 未执行到页面:本机 + Playwright 1.62.1 期望 `chromium_headless_shell-1234`,环境仅有已缓存的 1228,失败原因为 + `browserType.launch: Executable doesn't exist`;该环境阻断不改写 READY 证据,也未把 E2E 标为通过。 +- hashes:protocol `111a630c7beccd31989dc4f78932b7d7b741fb6bef03e45cb39a8139f774d235`;generator + `6a1e798ce46e1d14d833091d4d7ae452dccb13efe583594277785465eb725bbf`;checker + `7a8fe69ea1aa2c1e121be008a9fb60fc236f7ef776d2088a7b00b14f46fe3fba`;unit + `cd1c2adca81653f98f9a1c6c7d104ad0e3052283213fb7166dac827c956928fe`;freshness/app receipt + `0187ad0d9ea05fc4b152b7abd4945191dbe9ec24dfde4ca7dbe4aadfd1230efe`;expected identity + `8d65f78aa774ff252871cb1cc09e69b4ff04fbb45e61200eaf67534c9d2651b2`;App + `043ff3a2f39ef3a1303791081b80851b427e7db5dfd9af2161926dd6f1ea9ca4`;M12-05F manifest + `63e1506801ddee2f1eaf64cad68a9d5c918405f642ca237c6a1ec249ff297623`;package 保持 + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- `docs/status/M12-05F.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStateChange=false`,M12 当前完成更新为 47 项(M12-01A-I、M12-02A-H、M12-03A-N、 + M12-04A-J、M12-05A-F),ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-06A`。 + +M12-06A 接续开始(2026-08-18 America/New_York) +- 已读取项目主文档、当前执行计划和本文件;机器队列唯一 `nextTask` 为 `M12-06A`。 +- 本项范围冻结为 pinned Blender 5.2 desktop 生成五个独立 GLB fixture:mesh、pbr、uv、skin、animation;Web import 比较、保存重开和 loss report 留给后续 M12-06B-D。 +- 计划新增确定性 Blender 生成器、canonical semantic report、逐字节重生成 checker、M12-06A golden manifest 和 package command;完成前不改变 parity ledger。 + +M12-06A 实际验证(2026-08-18 America/New_York) +- pinned `build_blender_5.2.0/bin/blender` 生成 mesh、pbr、uv、skin、animation 五个独立 GLB 2.0 + fixture;总计 8,676 bytes,单文件均小于 512 KiB,五项 `extensionsUsed/Required` 均为空。 +- canonical report 固定 node、indexed triangle topology、POSITION/NORMAL/COLOR_0/TEXCOORD_0/ + JOINTS_0/WEIGHTS_0 accessor、PBR factor、embedded PNG、两关节 inverse bind matrix 和 25 帧 + translation/rotation animation;runtime binary SHA-256 与 M12-05A 一致。 +- `node tools/web/check-glb-desktop-fixtures.mjs` 通过:独立临时目录重新运行 Blender 后 report + 与五个 GLB 均逐字节一致,输出 `fixtures=5 bytes=8676 features=mesh,pbr,uv,skin,animation + deterministic=true next=M12-06B`。 +- hashes:generator `0247dd2405a68b42d99c2b005546ad2aa99b46416e3fe9489832467f6ea4eb4d`; + checker `211ebdb66bf2e2d349455d5303e889a4a1ae3323639a89dd78b3bda574dd820b`;report + `dea31cc861622166dc502b333bc177b70b66b54d9c62aaccc6522745dab5ae13`;mesh/PBR/UV/skin/ + animation 分别为 `e52b9268b6524744fb498691f976000635e544ba3b47e9f8ff22b03bcdf17a94`、 + `244cc8a992c5a70692b8bbc8333533718b3bac7022110715ce3b23d2e4c0b361`、 + `1e0397d2b69d8261b3252451b50ab4aba6525b94713719f35069a9a9d96fcfa2`、 + `4086ee4c8873aa03090338b676575b7a5335fb7c9384fec6ccb36108e71929f6`、 + `44f6cc47961a146dc97ea337ae31a8b64bb4ab213b716f81ec22129db704f1fb`;manifest + `e3554a1e739cbe3f217f6169130532365538b0c0eafbb97afc58d4d56c4287cb`。 +- package hash 保持 `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`; + Web import、`.blend` 保存重开与 loss report 均未提前声明。本项保持 + `enablingTask=true/parityStateChange=false`,ledger 仍为 195 completed/58 blocked;M12 当前 + 完成 48 项,机器队列唯一下一任务切换为 `M12-06B`。 + +M12-06B 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-06B:扩展生产 `web/protocol/glb-import.ts` 的 canonical desktop fixture import, + 在既有 GLB header/accessor/image/skin/animation validation 上增加 primitive topology、排序 + attributes、PBR/texture、node hierarchy/TRS、skin binding 和 animation sampler/channel 结构; + 既有 `importGLBSemantics` API 保持兼容。 +- `node --test web/tests/unit/glb-desktop-import.test.mjs` 通过 3/3:五 fixture exact comparison、 + 五域独立断言和 PBR 字段漂移稳定路径均通过。机器报告 checker 输出 + `glb-desktop-import-ok fixtures=5 domains=5 compatible=true + route=BLOCKED_UNTIL_MAIN_PERSISTENCE next=M12-06C`。 +- Chromium 首次从仓库根经 `npm exec` 调用时未解析 `web/playwright.config.ts`,误用缺失的 + `chromium_headless_shell-1234` 而在页面启动前失败;改为 `web/` 工作目录并显式 + `CHROME_PATH=/usr/bin/google-chrome-stable` 后,Chrome 150 Worker 用例 1/1(端口 5425)通过, + 五个输入均先验 source SHA-256 再完成 Web semantic exact comparison。 +- hashes:protocol `45d9a7912c4a59a552789a8ea0dfaff5f1849f8d213f14d238de024b77acdb0d`; + generator `6f97527b7da04c7b4f9b09c48b9f367d1270db9f7820498d33832a80754436c2`;checker + `804a4b5545d95d7ab1ba265469a981d0a10b71dab36f0c96283eb73b401443d2`;unit + `6bf2a96b3e43e5fae93589ea5dcf98e7a69b10266378e442988198925286a8d8`;Worker + `eb32049631113270fb62acb7ae9379e8ff9a03e38086db2fa309c6891cd707bf`;Chromium + `fa3e06419c918406f24b5a4260523bd94cfe729a84786e9434517fdfed624d45`;report + `79933888cc5aa2d1e3639ec349ca4c31d028fe89f751ebb8d4342f06d15ecfc2`;manifest + `5275310394b34726a05b30ab67658e1381662dddf9163a97cb02f47f646a8fa4`。 +- package hash 保持 `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`; + file picker/receipt matrix 未开放 GLB IMPORT,权威 Main/save/reopen 留给 M12-06C。本项保持 + `enablingTask=true/parityStateChange=false`,ledger 仍为 195 completed/58 blocked;M12 当前 + 完成 49 项,机器队列唯一下一任务切换为 `M12-06C`。 + +M12-06C 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-06C:使用 pinned Blender 5.2 对五个 M12-06A GLB fixture 执行真实 glTF import、 + `.blend` save 和新进程 reopen,固定 Object/Mesh/Material/Image/Armature/Action stable IDs。 + 生成器报告绑定 M12-06A GLB SHA-256;checker 在新临时目录重生成并忽略 Blender `.blend` + 容器的非语义字节差异,只要求完整 Main graph/stable-ID 报告确定性一致。 +- 新增 `tools/web/check-glb-main-persistence.mjs`;通过输出 + `glb-main-persistence-ok fixtures=5 stableIds=exact desktopReopen=exact deterministic=true next=M12-06D`。 + checker 同时验证 parent manifest、五个已冻结 `.blend` hash、GLB source hash、stable ID 前缀/去重 + 和重新生成的 desktop report。 +- 更新 Chromium 生产 E2E:每个 fixture 经 `WebEngineClient.openBlend` 建立 authoritative Main, + 以一次 `setObjectVisibility` Main transaction 改写后 `saveBlend`,再隔离 `openBlend` 重开; + 保存 bytes SHA-256 必须变化,visibility=false 与 stable IDs 均恢复,`openResourceStatus` 的 + activeRequests/liveInputBytes/liveStagingFiles 全为 0。`CHROME_PATH=/usr/bin/google-chrome-stable + WEB_TEST_PORT=5440 node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/glb-main-persistence.spec.ts` 通过 1/1(Chrome 150)。 +- `npm --prefix web run typecheck`、`node --check tools/web/check-glb-main-persistence.mjs`、 + `python3 -m py_compile tools/web/generate-glb-main-persistence-fixtures.py` 和 `git diff --check` + 通过。未修改 `web/package.json`,保持历史 M12 package hash 不失效。 +- hashes:generator `8989d6ce4196f140a7bfb44b863dc530a6aa462ca4a57fa7b77c468e06ab61ad`;checker + `5bbbbeaf2d20ff9bbdeb74c8dd10fa6ffc421d1d59f86540e95f29c2cfd10903`;Chromium test + `7c97877f1cbbfd0166e106c80faee53f474f78816cca68ea924df5aa3c47776d`;desktop report + `76b000454a9073ef762d25fa546d5c65a004659a93eb6ec82fad1e679b2e81be`;manifest + `e3a57636a47591e3b02dff5a07e8a0aec5e0dc43bf6b4829bffc811035dbbb31`;mesh/PBR/UV/skin/animation + `.blend` 分别为 `66e29adc016f07e220019b6f24eb7e5016c684dca4b3102b20c8e836968d422d`、 + `ba08aeb2876d82ddf731abe81d3a42041a1be36617d0b6324c870d5bcd4cc771`、 + `1270cb452d34d2fd7a19181a2b20f70b7a028bdd2db7cf1bba4e1003f7de0f48`、 + `23f175e44ec588c75db99d3f9134863fc3d7d1f192bbd815d5d1c4e3218bce0c`、 + `fa6baf3a67ff11fe3d2922210089a7c508e4ee28bfaabe4cf628ba6addee1ff5`。 +- `docs/status/M12-06C.md`、M12 当前计划、完整对标计划、WBS、项目状态已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 50 项,机器队列唯一下一任务切换为 `M12-06D`。GLB file-picker、Web export loss report、 + desktop 再导入 Web GLB 和 sparse/Draco/URI 负例均未提前声明。 + +M12-06D 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-06D:新增 schema-1 `GLBLossReport`,由生产 `exportGLB` warning 结果生成稳定 + 排序的 machine loss entries、error/warning 计数、SceneIR revision/数据块 surface;不把 + warning 静默丢弃,也不把失败输出伪造成可导出。 +- Chromium 生产 E2E 经 `WebEngineClient` 打开五个 M12-06C Main `.blend`,请求 packed image + asset,交给 `glb-loss-report-test.worker.ts` 的生产 exporter;mesh 的 Color Attribute shader + 返回 `canExport=false`、`SHADER_GRAPH_UNMAPPABLE` 且无 output,PBR/UV/skin/animation 生成 + 非空 GLB 并绑定 SHA-256。`UPDATE_GLB_LOSS_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable + WEB_TEST_PORT=5444 node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/glb-export-loss-report.spec.ts` 生成 golden 后,默认模式 `WEB_TEST_PORT=5445` 重跑 + 1/1 且逐字节匹配报告。 +- `node --test web/tests/unit/glb-loss-report.test.mjs` 通过 1/1,稳定校验 loss entry 排序和计数; + `node tools/web/check-glb-loss-report.mjs` 通过,输出 + `glb-loss-report-ok fixtures=5 blocked=mesh warnings=3 deterministic=true next=M12-06E`; + `npm --prefix web run typecheck`、`git diff --check` 通过。未修改 package,保持历史 package hash。 +- hashes:export protocol `a5d342827860a9e55b49a3bb3a196a01b1e53546325d6e594778afb9360c3125`; + loss protocol `dce9c790b2f52d46efe0908ecb044d63d21b3c6c3f7d77ddb5e697b29a7a2d6e`;Worker + `6f6294d26fe7b717416a5dd25fe834fb4b9a2ecbe8b011395c9559a26701ec77`;Chromium test + `ce334b6bb5d82c133d317e916c03711029fba1c19c634dec06c06da9eddbd776`;checker + `a23346860fa26405b2cd24591b0ab36fea29cb561ec8c2991ecfeabcffc17ee1`;web loss report + `c6db52234d867985ef5fbcdc7c62298ec9aaa013028b5a54e2b9a16aa4133397`;unit + `dfcc2d968e4e0c0cef4496bc304cb481f10d1f0dc4c76a40ea1d6e2f11b6b708`;manifest + `c01c5861d493e1f177e8cde3038bc5283a7671fa39bf84484662d307623325a2`。 +- `docs/status/M12-06D.md`、M12 当前计划、完整对标计划、WBS、项目状态已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 51 项,机器队列唯一下一任务切换为 `M12-06E`。desktop 再导入 Web GLB、lossless roundtrip、 + sparse/Draco/外部 URI 和取消/restart/quota 仍未提前声明。 + +M12-06E 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-06E:将 M12-06D Chromium 生成的 PBR/UV/skin/animation 四个 Web GLB 固定到 + `tests/files/web/m12_glb_web_v1/`,由 pinned Blender 5.2 真实重新导入、保存、重开并生成 + `desktop-reimport-report.json`;mesh 因 M12-06D shader blocker 没有伪造输出。 +- `node tools/web/check-glb-web-reimport.mjs` 通过,输出 + `glb-web-reimport-ok fixtures=4 exact=2 mismatched=uv,skin deterministic=true next=M12-06F`。 + PBR/animation canonical graph exact;UV 4 条路径差异(corner-expanded vertex topology、Mix + node),skin 31 条路径差异(额外 armature helper mesh 与 Blender tessellation),均逐路径 + 记录并保持 N-023 parity BLOCKED。 +- checker 在全新临时目录重跑 Blender 生成器,report 逐字节一致;M12-06D export E2E 默认模式 + 已通过 1/1,Web GLB source/output hash 与 report 绑定。`npm --prefix web run typecheck`、 + `git diff --check` 通过。 +- hashes:generator `3ae1ba45e15cae5d0308fc3fcb3766764cd374c096e27eaaddba9228a3d75004`;checker + `c21b45a975ec70586da1b9e50b8cf4ccf74644300b4f381c3fee0ad735cfcdb9`;desktop report + `e4d03d25cfac3c4beff4d0af0769b1c39b058670c679c12f4a11ae30d2d91f3c`;manifest + `3d3b13fd54a314fd6f6907fc2d314e66ef74e8ca770a8accd823370d272737fc`;Web PBR/UV/skin/animation + GLB 分别为 `1ab7936fae6e0c28e1b90e8a6ae24b3893c075c9fc58ac8896f780fdefb8277e`、 + `8bd045388527ddad7cf886c0c7a2a45b6e7d6db603568a10a959bc6d423ae145`、 + `4a45d00dfa23638682bb61a4f74b283bcd986126da4890d068902e3c85efc332`、 + `b83de103df3f5a49487868f3ede3046875c90b0d084bbd998511c3a7c987a4fa`。 +- `docs/status/M12-06E.md`、M12 当前计划、完整对标计划、WBS、项目状态已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 52 项,机器队列唯一下一任务切换为 `M12-06F`。sparse/Draco/extension/外部 URI、取消、 + Worker restart 和 OPFS quota 仍未提前声明。 + +M12-06F 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-06F:GLB parser 增加 512 KiB byte、256 KiB JSON、4096 bufferView、8192 accessor + budget;对 sparse accessor、任意 extensionsUsed/Required、buffer/image external URI 统一 + fail-closed,稳定返回 `GLB_SPARSE_ACCESSOR_UNSUPPORTED`、`GLB_EXTENSION_UNSUPPORTED`、 + `GLB_EXTERNAL_URI_BLOCKED`、`GLB_IMPORT_BUDGET_EXCEEDED`。 +- `node --test web/tests/unit/glb-negative-cases.test.mjs` 通过 2/2;Chromium Worker + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5450 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-negative-cases.spec.ts` 通过 1/1,四类 + 真实二进制负例在生产 Worker 中返回同样的错误码。 +- `node tools/web/check-glb-negative-cases.mjs` 通过,输出 + `glb-negative-cases-ok cases=4 budget=524288 deterministic=true next=M12-06G`; + `npm --prefix web run typecheck`、`git diff --check` 通过,package hash 未变。 +- hashes:protocol `0b6329c08e6f3cd9af6f27ef7c463b037a7cab94192afb4538d3d6c4cfcbc147`;unit + `9af1c155143a0c685a62f569f705afd9fcc3bb49e0d724ebf3ad2c6356d63d6d`;Worker + `d84a6b884ce1bfedd598b2602d803493bf10f6ad62fdfac49d64fca3f30f3931`;Chromium test + `c6694689ce04ff2faea2c20be648f438a9f7eb25cdfd2f714b94c1f556ea1510`;checker + `08377c306fd7d1082f7fd734e37809960c9798d29d626681fe9ad3f94cb7a29e`;report + `7367a624b562a9613b4b908c894ab04c731ae0a348a3b58689075f4a9decd90c`;manifest + `27b42da0683dab11a884553440088c30cf58d51364268fdaac86668786aaedd4`。 +- `docs/status/M12-06F.md`、M12 当前计划、完整对标计划、WBS、项目状态已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 53 项,机器队列唯一下一任务切换为 `M12-06G`。取消、Worker restart、OPFS quota 和 + full extension allowlist 仍未提前声明。 + +M12-06G 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-06G:新增 schema-1 `glb-recovery` receipt,绑定 operation、Worker generation、 + base/candidate revision、输入/输出 SHA-256、临时字节、live request、commit 状态,以及稳定 + `GLB_OPERATION_CANCELLED`、`GLB_WORKER_RESTARTED`、`GLB_OPFS_QUOTA` code。 +- Chromium 生产 parser/exporter Worker 对 IMPORT 与 EXPORT 分别在发布前取消;两项均返回 + `CANCELLED`、`committed=false`、`temporaryBytes=0`、`liveRequests=0`。独立 generation 1/2 + Worker 对同一 PBR GLB 重跑,semantic output SHA-256 exact;generation 1 committed receipt + 以 schema transition 标记 `RECOVERED/GLB_WORKER_RESTARTED`。 +- GLB output 使用既有 content-addressed OPFS asset path。Chrome DevTools 将 origin quota 设为 + 64 KiB 后,128 KiB candidate 真实写入失败;旧 PBR GLB 在 Storage Worker 重开后 byte length/ + SHA-256 保持,asset metadata 仍只有 1 项。quota 恢复到 1 GiB 后,同一 project 成功提交 3-byte + 小 GLB asset,asset count 变为 2。 +- 第一次真实 quota Chromium 运行已经正确失败关闭,但断言只接受注入消息 + `QuotaExceededError`,浏览器实际消息为 `The operation failed because it would cause the + application to exceed its storage quota.`;放宽为两类 quota 文案后完整重跑 2/2 通过。 +- `node --test web/tests/unit/glb-recovery.test.mjs` 通过 2/2;最终 + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5455 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/glb-recovery.spec.ts` 通过 2/2; + `node tools/web/check-glb-recovery.mjs` 输出 + `glb-recovery-ok cancelled=2 workerGeneration=2 quota=GLB_OPFS_QUOTA smallRecovery=true next=M12-07A`。 +- `npm --prefix web run typecheck`、checker syntax 和 `git diff --check` 通过;package hash 保持 + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- hashes:recovery protocol `da4e2a546d7598e80798cfebd105e52b7522c896acb545710c78bb8a5c3705aa`; + storage protocol/client/Worker `2c8ce9e32fcae973e9262a024fd849221680104bcd427ec308f99ef25112d951`/ + `00cd35e4632479e4cd153314113e201c822fd209ae6c8e9fbeff5f1399728565`/ + `80bf4d2ac59cbf7998c2a72c8961ae38abe43685b60df564d0746e1a45e9e1bc`;operation Worker/adapter + `947c4a768422725ff2f689b2eefa8068bee51bb7ff342af17e8d4a4d0a4a7ca2`/ + `2c753a04c153471c2bf7691073b836d968a1a1300e3da038f493983b46738cdf`;unit/Chromium + `6dd48f62a85c5aaf3a04b6e572a47a7986b23cbec84a099ea510af81e8dd3a3a`/ + `7d95e992f44fb913f70c104f0d6b23bd76f47d48db4a21899e35ae1d188e2093`;checker/report + `b185248fdcd6b3cee84252bd68fcb6921bb6385bfa4a00486e93c58e94755cce`/ + `1a008a76590573468446ca6e5cbdfe0ea5a8b27493291590d937b90db90d6e42`;manifest + `1c732b8d9f1a0bdb502f44e2e843e91efea12e93483a35658f8a9c70a69a2430`。 +- `docs/status/M12-06G.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 54 项,机器队列唯一下一任务切换为 `M12-07A`。OBJ 正例尚未提前声明。 + +M12-07A 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07A:读取 M12-06G 完成证据和 M12-05A pinned Blender runtime inventory;任务冻结为 + desktop-only OBJ single Mesh positive,不提前开放 Web parser、负例、多对象或 round-trip。 +- 新增 `tools/web/generate-obj-single-mesh-fixture.py`,调用 Blender 5.2 `wm.obj_export`,在 + `tests/files/web/m12_obj_desktop_v1/` 生成 `single-mesh.obj` 与 `single-mesh.mtl`。OBJ 有 4 个 + position、4 个 `vt`、1 个 `vn`、2 个 triangle face;两个 face 分别绑定 `M12_OBJ_Red`/ + `M12_OBJ_Blue`,并各自有稳定 material group。MTL 有 2 个 `newmtl`。 +- canonical report `tests/golden/M12-07A/desktop-fixture.json` 绑定 source anchor + `blender-5.2.0/source/blender/io/wavefront_obj`、operator `wm.obj_export`、轴向/UV/normal/ + material settings、OBJ/MTL bytes/hash 和语义索引。runtime identity 与 M12-05A 逐字段一致。 +- `node tools/web/check-obj-single-mesh-fixture.mjs` 通过全新临时目录二次运行 Blender,report 与 + OBJ/MTL 均逐字节一致,输出 + `obj-single-mesh-fixture-ok vertices=4 normals=1 uv=4 faces=2 materials=2 deterministic=true next=M12-07B`。 +- hashes:generator `604c3006f194c7c64a487b8f760693b66830f3cfa602928b46769955e7d4f358`;checker + `3d0208f61a86d4d29796d8284756f53931c90864b15b7dfe4fcc84d7f65a8040`;desktop report + `6c560a8eecf84973c2b05f9fd50d33bd45515e97c4f7970f1502de406c39f6a5`;OBJ/MTL + `a56694d1ee28735c68381ff18c3a52581612feebac0101a53e502956c27d144f`/ + `392f1b10ff5a0b142d520a9443b4c96191985f15d4244c79b36fdeda0f153715`;manifest + `d80b74502202effa7be15640c945dc7e04af703b58e7a15c3fe7babc43c17b46`。 +- `docs/status/M12-07A.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=true/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 55 项,机器队列唯一下一任务切换为 `M12-07B`。OBJ Web 解析、多对象/坏 face/MTL texture + origin 和 round-trip 均未提前声明。 + +M12-07B 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07B:冻结 OBJ 双对象、负索引、MTL/texture origin 和坏 face 四个输入面;仍不 + 开放 Web parser 或 OBJ import route。读取 M12-07A 与 M12-05A runtime inventory,继续使用 pinned + Blender 5.2 `wm.obj_export`。 +- 新增 `tools/web/generate-obj-multi-negative-fixtures.py`,生成两个 Mesh object、6 个 position、 + 6 个 UV、2 个 normal、2 个 triangle face、两个 material/material group;材质节点引用由 Blender + 生成的 2x2 `m12_obj_texture.png`,MTL 两条 `map_Kd` 均为相对文件名。随后从同一正例确定性派生 + `negative-index.obj`(每个 v/vt/vn index 为负)和 `malformed-face.obj`(首个 face 只有 2 个顶点)。 +- `node tools/web/check-obj-multi-negative-fixtures.mjs` 通过新临时目录完整重跑 Blender,验证 + OBJ/MTL/PNG/负例逐字节一致、双对象/group/material、PNG signature、负索引形状与 + `OBJ_FACE_ARITY_INVALID`,输出 + `obj-multi-negative-fixtures-ok objects=2 negative=true malformed=OBJ_FACE_ARITY_INVALID textureOrigin=relative deterministic=true next=M12-07C`。 +- hashes:generator `8d4faed82cba76e46bf639e5031b30568e8b9a15240cbddb1c22ee7ad11d697b`;checker + `61188d67efd6133e714f2d55c7c8516b9d04896de068f994523a3c398eebdd2f`;desktop report + `b60ff785676c0f0168588605ad442a650615191ac00770b7e5a308cc4ade83ce`;OBJ/MTL/PNG + `4ed3dced8445b03754cd3916ae2e8cf0dff85e6a5882f1c6f0a4a4e4c5251f6a`/ + `80338f569c67a8b1616515ba878ffc72b26e93ef2e8c361131023fb14f132f5f`/ + `44e4fd08bdda8403908349e2a62dcf103ce2d0851099fc61cd868d65ddbef78c`;negative/malformed + `9457954284cac1d68e23627e3ff734c0c591b3a24086ce5aa3d0dbbfc22f01bc`/ + `6dd508b5ba944c2d15e2889c9ad9a3693845145c3bf6c9bf7df992081c915864`;manifest + `785f593271058098704498cb0a7c948305087f1a83bf8f29b6e6fb9fe9341926`。 +- `docs/status/M12-07B.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=true/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 56 项,机器队列唯一下一任务切换为 `M12-07C`。Web import、OBJ round-trip 和 loss report + 仍未提前声明。 + +M12-07C 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07C:新增 schema-1 `obj-import`,固定 512 KiB OBJ、128 KiB MTL、16,384 行、 + 65,536 position/UV/normal、65,536 face 预算;正索引与负索引归一到 1-based canonical,坏 + arity/index 返回稳定 `OBJ_FACE_ARITY_INVALID`/`OBJ_INDEX_OUT_OF_RANGE`。`serializeOBJ` 输出 + 固定文本,`createOBJLossReport` 对未绑定 `map_Kd` 返回 `OBJ_TEXTURE_ORIGIN_UNRESOLVED`。 +- 新增生产 `obj-roundtrip-test.worker.ts`。Chromium 150 对 M12-07B 双对象 OBJ/MTL/PNG 输入执行 + Web parse/serialize 两次:绑定 texture 的 loss report warning=0,去掉 texture binding 的 + warning=2;browser canonical 为 2 objects、6 positions、6 UV、2 normals、2 faces、2 materials。 +- E2E 将 Web 输出写入独立临时目录并调用 pinned Blender 5.2 `wm.obj_import`(split groups); + 新进程报告 2 objects、2 mesh、2 triangles、每对象 UVMap 和 1 material,与 browser canonical + 的 object/triangle/UV/material 比较全部 exact。输出 golden report 绑定 source/output hash、 + desktop report 与 missing-texture loss。 +- `node --test web/tests/unit/obj-import.test.mjs` 通过 3/3;最终 + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5460 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/obj-web-roundtrip.spec.ts` 通过 1/1; + `node tools/web/check-obj-web-roundtrip.mjs` 输出 + `obj-web-roundtrip-ok objects=2 triangles=2 lossWarnings=2 desktopExact=true next=M12-07D`。 +- 首次 Blender 重导入脚本因把 `mesh.materials` 元素误当作 slot,进程退出码仍为 0 但没有生成 + report;修正为直接读取 Material 后从头重跑。该失败未进入 READY golden。 +- hashes:protocol `e7240af65e0d90f3ee690a4e3f391416fe4744ac6c46edc8ac0d72386857cab9`;Worker + `bf1fcc60ec318cf6c2747d44f4af741b46f284cf5f5307e142f0ab6d50b14302`;desktop importer + `9f1eb4ab679255a0f1f596696e8befc33a4e30c0cbe6ea423e2aaa0314cec22d`;checker + `9d53014f4b89f786c516ebe8d300030c2728e4a36a86a5ef136b2769a12ac96b`;unit/Chromium + `485a669be5de8e370e9b5a3239357b028ba61ca5c4076819cd46aed52a5c210a`/ + `93785b4017ba14b007926b0f82442f8ae5968f242a2a762e5f5dc77d36110f5b`;report + `45eaffc9c2e50c84b557d13ebbe9f4f53b63e19e00bc69b272491ef6366dff81`;manifest + `0c639954900b1fcc3b8285d0f4c0ecfa5807df6cde6d1f2cd3b59c4636d71674`。 +- `docs/status/M12-07C.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 57 项,机器队列唯一下一任务切换为 `M12-07D`。OBJ UI route、STL、PLY 和完整材质/贴图 + semantics 仍未提前声明。 + +M12-07D 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07D:从 M12-05A runtime inventory 确认 `wm.stl_export` registered、build option + `io_stl=true` 且 variants 为 `STL_BINARY/STL_ASCII`;任务只冻结两种 encoding capability,不 + 依赖共同 `.stl` 扩展名推断,也不开放 Web parser。 +- 新增 `tools/web/generate-stl-capability-fixtures.py`,由 pinned Blender 5.2 对同一 selected + two-triangle Mesh 用相同 axis/scale/unit/evaluation settings 导出 binary 与 ASCII。binary 为 + 80-byte header + triangle count=2 + 2x50-byte records,总 184 bytes;ASCII 为 2 facet、6 vertex, + 总 213 bytes。 +- `node tools/web/check-stl-capability-fixtures.mjs` 验证 runtime identity、binary count/size、ASCII + wrapper/facet/vertex、artifact hash,并在新临时目录从头运行 Blender 后逐字节相同,输出 + `stl-capability-fixtures-ok binaryTriangles=2 asciiFacets=2 deterministic=true next=M12-07E`。 +- hashes:generator `8310104e66f246b32ac7cb4619e7f4da109baf2ece39ea670cbb6d33bd88c8b8`;checker + `b2ad901eaa9701436cf7bcc8aa4e40d1b2d6eda7c6c44a0af830347a37cc9ca8`;report + `29c7d2a6e6764440c99eec25bb5760c7e0880839691757fb3e607ed4f5050013`;binary/ASCII + `50161172cd12e4240067064db6f80e74b2df69c475e3e34154e9d0523928c1ca`/ + `a463a5add8be070fb67b34f00ef6e7b46025aed31fa429d4a033d75a11cf0113`;manifest + `bbc78e47f389562e7d648bf49c9b3bf8a08aaa36e914589a6eab0a1f6e72748d`。 +- `docs/status/M12-07D.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=true/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 58 项,机器队列唯一下一任务切换为 `M12-07E`。normal/unit/degenerate/trailing、Web + round-trip 与材质 loss 均未提前声明。 + +M12-07E 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07E:新增 `stl-import` schema 1,调用方必须显式给出 `STL_BINARY/STL_ASCII` + variant 和正 unit scale;预算固定 512 KiB、65,536 triangles、scale 上限 1,000,000。parser 逐 + facet 读取 normal/vertices,unitScale 只作用于 vertices,零面积 triangle 在发布前移除;binary + 声明 table 后有 bytes 时稳定返回 `STL_TRAILING_BYTES`。 +- 从 M12-07D binary fixture 派生一个第一 triangle 三顶点相同的 degenerate 文件和追加 + `de ad be ef` 的 trailing 文件。pinned Blender 5.2 实际 probe 表明:scale=1/0.001 world bounds + 比例约 1000;normal 均为 `[0,1,0]`;degenerate 导入警告并移除 1 triangle;trailing 文件被 + 接受为空 Mesh。Web 对 trailing 采用更严格阻断,并明确记录 `STRICTER_WEB_BLOCK`,不伪报 exact。 +- `node --test web/tests/unit/stl-import.test.mjs` 通过 2/2; + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5463 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/stl-edge-parity.spec.ts` 通过 1/1;normal binary/ + ASCII/Desktop exact,unit ratio 在 Float32 容差内,degenerate triangle count exact。 +- `node tools/web/check-stl-edge-parity.mjs` 输出 + `stl-edge-parity-ok normals=exact unitRatio=1000 degenerate=removed trailing=STRICTER_WEB_BLOCK next=M12-07F`; + typecheck 与 `git diff --check` 通过。 +- hashes:fixture generator/desktop probe `018013347642603c21237ebb023bdc7a4e338f5a3f875fea536a996eb121e716`/ + `b3d6ea197ef77b5a14f60f5e6b43d4392e943ff5d56acf73915507fc9a4161b7`;protocol/Worker + `87eec72e2b8aa7ad0b168ca0ee8c4016c941f56f5f1ac53aa5fe71d0832f1dd8`/ + `d714f1f3a218a2226dd349aea81c6c54efa6246de1c4fe51aaa9f5c352ef44fc`;unit/Chromium/checker + `4af52833486421c017f3af2b833b0776e60a6ab57ed66fea2161cf9674f4b243`/ + `121d348250e26d29ad966f7427bbef9da77de2098e2e305827b11aae52002c5c`/ + `b5ed7fbb41bb46fba982d5726cb8c3eaecf360b772a01d6337c309b1c4a4f535`;fixture/desktop/Web reports + `545463a984356d6635cbaae3865d13fe95bd2d841c394ac9ddff496c95d46f18`/ + `2d3028a3b7d97f39654cff5fcef1d0c1c71e9f2d08e3e29c2e926651ed3860f1`/ + `e0686cc9be3b68e564651207443e0ebf3e3b0eb3d07a32915b03f44b1908357b`;manifest + `19a9f460d1b939c9504dadd364cb87dab3b1769b0599035e2d2b51b47091e034`。 +- `docs/status/M12-07E.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 59 项,机器队列唯一下一任务切换为 `M12-07F`。STL Web-to-desktop round-trip/material + loss 仍未提前声明。 + +M12-07F 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07F:新增独立 `stl-export` schema 1,使用 M12-07E parsed triangles 生成固定 + binary STL(80-byte header + count + Float32 facets),并由 `createSTLLossReport` 对源项目的 + 2 个 material assignment 返回 `STL_MATERIAL_UNSUPPORTED`,不静默丢材质。 +- Chromium 150 Worker 对 M12-07D binary fixture parse/serialize,输出仍为 184 bytes/2 triangles; + E2E 将 Web output 写入临时目录,由 pinned Blender 5.2 `wm.stl_import` 新进程读取,desktop + polygon/triangle=2、facet normals 与 Web exact。 +- `node --test web/tests/unit/stl-export.test.mjs` 通过 1/1;最终 + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5467 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/stl-web-roundtrip.spec.ts` 通过 1/1; + `node tools/web/check-stl-web-roundtrip.mjs` 输出 + `stl-web-roundtrip-ok triangles=2 normals=exact materialLoss=1 desktopExact=true next=M12-07G`。 +- 首次 E2E 结果把 transferred ArrayBuffer 经 Playwright 结构化返回为 object,修正为在 page + evaluate 内转 `Array.from(Uint8Array)`;第二次 Blender report 的 `-0` 归一化后从头重跑通过。 + 两次失败均未进入 READY golden。 +- hashes:protocol/Worker `3b9c409de9fb3eaea34f82c1ddd466670d2b478845d8ff6af3c4e44cb4e04a52`/ + `1da8b8281cf8ebf9ccf5fd2f42da8ed5886001ab83daba8d9fcd88980257af87`;desktop importer + `c4cbff52ff2e7cecba7aeab47e865975955da23a0e1e8ff885ece56b894558d5`;unit/Chromium/checker + `f06af242df80ec26d06e92b749449cfebaf909476198ff825207cdd4b9484102`/ + `2e979dcd030f5316fcbe28e2c19b1c99fe5d111e849d3fc1d2ea316d2159032b`/ + `093de04bf80b0909eee56ea590e04e7aa4749168e2e49615c62ab39ddc0d16cf`;report + `0495c645b4280f6e7821f0ba02010e25d4accbc552e3cd7c58e052607799040e`;manifest + `3cbbcb541ee123da0ef8916ac2ca8805680d539bbf8db3b4a8d331aec418148c`。 +- `docs/status/M12-07F.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 60 项,机器队列唯一下一任务切换为 `M12-07G`。STL material/normal/unit 全域与 UI route + 仍未提前声明。 + +M12-07G 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07G:读取 M12-07F 完成证据和 M12-05A pinned Blender 5.2 runtime inventory;任务 + 冻结为 desktop-only PLY capability,不提前开放 PLY Web parser、属性映射、负例或 UI route。 +- 使用 `tools/web/generate-ply-capability-fixtures.py` 调用 pinned Blender 5.2 `wm.ply_export`,对 + 同一 selected four-vertex/two-face Mesh 分别生成 `PLY_ASCII` 与 + `PLY_BINARY_LITTLE_ENDIAN`。ASCII header 为 `format ascii 1.0`,binary header 为 + `format binary_little_endian 1.0`;两个文件各自声明 vertex=4、face=2,settings、source anchor、 + operator 和 runtime identity 写入 canonical report。 +- `node tools/web/check-ply-capability-fixtures.mjs` 通过 runtime identity 逐字段匹配、variant + encoding/element count/hash 校验,并在全新临时目录重新运行 Blender;report、ASCII 和 binary + little-endian 文件均逐字节一致,输出 + `ply-capability-fixtures-ok ascii=ascii binary=binary_little_endian vertices=4 faces=2 deterministic=true next=M12-07H`。 +- `python3 -m py_compile tools/web/generate-ply-capability-fixtures.py`、 + `node --check tools/web/check-ply-capability-fixtures.mjs` 通过。PLY capability 只冻结桌面 + encoding admission,不改变 N-023 的 `BLOCKED` parity,也不把扩展名推断为执行能力。 +- hashes:generator `581cd84057357e3a87997cf9c07d5d5633209648ac11e44611782eb254a38ebd`;checker + `5280d6e57b7a722ea881e27b792c6082c5113c1577ac0e87f87cc87fdf59516c`;desktop report + `26b1ce83334b41778cef5ce2a1f2c4d34254f63d7c7573cb3fb550f93ddeabb2`;ASCII/binary fixtures + `63646cab9bf6ccecb23092e5e24d04df1e0a690c866127c72a35791e99262331`/ + `e40fbb494b8b147c555a0fc06eb191415406bb9a6c061acf6befd8464c8c41a5`;manifest + `87df9c12f9c6eacf63051b70e9bb2eb43ebd1e5c4487b3512c45adb94cbb82ea`;package 保持 + `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`。 +- 新增 `docs/status/M12-07G.md`,并同步 N-023、当前执行计划、完整对标计划、WBS、项目状态和本文件; + 本项 `enablingTask=true/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 + 当前完成 61 项,机器队列唯一下一任务切换为 `M12-07H`。PLY vertex/face/color/custom-property + 映射、未知 property loss、big-endian/坏 list/超大 count 和三格式 recovery 均未提前声明。 + +M12-07H 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07H:读取 M12-07G PLY capability 与 pinned Blender 5.2 runtime;本项实现 bounded + `web/protocol/ply-import.ts`,显式接受 ASCII/binary little-endian,映射 vertex position/normal、 + RGBA color、numeric custom property、face indices 和 numeric face property。预算固定为 512 KiB、 + 64 KiB header、65,536 vertex/face、256 list entries;未知 vertex/face list 与未知 element 生成 + `PLY_UNKNOWN_PROPERTY`/`PLY_UNKNOWN_ELEMENT` loss warning,不静默丢弃已映射字段。 +- 新增 `serializePLYAscii`、`createPLYLossReport`、生产 `ply-roundtrip-test.worker.ts`,以及 + `generate-ply-mapping-fixtures.py`。pinned Blender 5.2 `wm.ply_export` 生成 4 vertex/2 face、 + RGBA、`temperature`/`label` 两个 custom attribute 的 ASCII 与 binary little-endian fixture;从 + ASCII 正例确定性派生 `unknown-property-ascii.ply`(`unknown_values` list)。 +- `node --test web/tests/unit/ply-import.test.mjs` 通过 3/3:两种 encoding 字段语义相同、颜色/ + custom 映射、未知 property loss、序列化重开和 format mismatch 均有断言。 +- `node tools/web/check-ply-mapping-fixtures.mjs` 通过,输出 + `ply-mapping-fixtures-ok vertices=4 faces=2 colors=rgba custom=2 unknown=PLY_UNKNOWN_PROPERTY deterministic=true next=M12-07I`; + 新临时目录再次运行 Blender,mapping report 与三份 fixture 均逐字节一致。 +- 最终 Chromium 命令: + `UPDATE_PLY_MAPPING_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5472 + node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/ply-web-roundtrip.spec.ts`,通过 1/1。生产 Worker 对 ASCII/binary/unknown 三输入执行 + parse/serialize,unknown 返回 1 条 `PLY_UNKNOWN_PROPERTY`;Web ASCII 输出由 pinned Blender 5.2 + `wm.ply_import` 重导入,vertex=4、triangle=2、position exact、RGBA attribute 和两个 custom + attribute 均存在。报告 `tests/golden/M12-07H/web-roundtrip-report.json`。 +- `npm --prefix web run typecheck`、`node --check tools/web/check-ply-mapping-fixtures.mjs`、 + `python3 -m py_compile tools/web/generate-ply-mapping-fixtures.py tools/web/check-ply-web-roundtrip.py` + 与 `git diff --check` 通过。新增 `web/package.json` 命令 `test:ply-mapping`。 +- hashes:protocol `19a4e8d4c5d0909ba3614b6c8a9cb3b57e8257008b3dd6143dc16ba1bd4a997f`;Worker + `f6bf5e39e83286d7b257bbdce88f4d7fa027c64651da9765567788b5cf298c71`;generator/checker/desktop + importer `ffc051eccfc6973ee00cc791cdbd641fcce308b4083741e3e6ae82291d9347b7` / + `2b055dbc705830848efdf4d8db8543a3ccc684de1f258732bcafefa86cf65c3a` / + `6edbc6e401a1a902dcfd11c4d767e8c8620d694e40eb4ca29dd8479f9c55ef37`;unit/e2e + `b03a5f4b4b2a974fa26e653763470b92d1433c5d352ae09ab5492b841e6e5e1f` / + `2cadebc89057655d78e9b520bb6adf9debb27c6d783cd002efee08d269636fb9`;mapping/web report + `8a02fc9e364ed79e50ef00897affa9ab63808d3cb3cdabd00fdb8ee4c26ec18a` / + `1ca9d3b7870fcc8c9e3c9bbbd90ef48bd13bbc9ae0462312c9482f24f05f13ec`;manifest + `7de271492103cc694d5815d0b81255139e9507578f87947c6ec493149a1fe8bc`;package + `c56c653effb38f9ac9c53aa19a531ca0cdab04d4457212f286349091d5b65c22`;fixtures ASCII/binary/ + unknown `acaa369c17252d4d089a2e3064d0c1fcdfdb2d066759ed160adf7869d3e859d5` / + `d0fc195fd1a101c42ac984a2382bccdddb7d0bf60dd618e9f637c964f1b30b9e` / + `a994c7126f235d0067ce4af35f8b0c6699cc83073e2a37e982edd45ece459f33`。 +- `docs/status/M12-07H.md`、N-023、当前/完整对标计划、WBS、项目状态和本文件已同步;本项 + `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked;M12 当前 + 完成 62 项,机器队列唯一下一任务切换为 `M12-07I`。big-endian、坏 list、超大 count 和三格式 + recovery 仍未提前声明。 + +M12-07I 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07I:在 PLY header admission 增加 65,536 element record 上限;保留 + `binary_big_endian` 的稳定 `PLY_FORMAT_UNSUPPORTED`,ASCII/binary list 截断返回 + `PLY_DATA_TRUNCATED`,超大 vertex count 返回 `PLY_IMPORT_BUDGET_EXCEEDED: vertex`。 + 失败在 parse/map 前结算,不发布部分 document。 +- 新增 `tools/web/generate-ply-negative-fixtures.mjs`,生成 `big-endian.ply`、 + `malformed-list-ascii.ply`、`oversized-count-ascii.ply` 和 schema-1 negative report; + `node tools/web/check-ply-negative-fixtures.mjs` 在独立临时目录重生成并通过: + `ply-negative-fixtures-ok cases=3 bigEndian=PLY_FORMAT_UNSUPPORTED malformed=PLY_DATA_TRUNCATED oversized=PLY_IMPORT_BUDGET_EXCEEDED deterministic=true next=M12-07J`。 +- `node --test web/tests/unit/ply-negative.test.mjs` 通过 2/2;最终 Chromium 命令 + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5478 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/ply-negative.spec.ts` 通过 1/1,生产 + Worker 对三类输入逐项返回预期 code 且 `ok=false`。 +- `npm --prefix web run typecheck`、Node syntax checks、`git diff --check` 通过。M12-07H 的 + parser unit/checker 在共享 budget guard 变更后重新通过;H manifest protocol hash 已更新到 + 当前可复验代码。 +- hashes:protocol `058a3263fde553637840a4e9ad4e8e9d923eb52c250f8000317c7f43a228881d`;generator/ + checker `9c09707bdfe73ba467bbfbf61ed3846fb59fd33ab5563a3a62c8032722ff6938` / + `b1d047d4cb1fa15dff7821687e7028ad073fdc62d4c76f60a2656732c0ec5e2b`;unit/e2e + `60439f9e6bc221df8866956bf926816a347e85828b5a93deb26ee23015cf449a` / + `6509a29d6842f3423d4dfcc40dbd52a959a5efa7ee1121143dce06e5bbc4a460`;report + `fbe2830d1b19294ea98eea66c3e00125bc0809a4bb8a750612939cbe1f5acca9`;manifest + `02be4b2e2cabecf4a239ba52be385b926a70e794c6f8c745d89dada568e6d674`;fixtures big-endian/ + malformed/oversized `cda92943a3690529fbb3463d328b6796ac0d07e19139450556f7411fc1f031e3` / + `a6a576b7a04d919b540520a6f43a89c089f0d706a17fd8b390711fff6b8b03df` / + `fcd99e0838025429420a47466251cfef80e638d2b5816ad14d5aa696364525bb`。 +- 新增 `docs/status/M12-07I.md`,并同步 N-023、当前/完整对标计划、WBS、项目状态和本文件; + 本项 `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked; + M12 当前完成 63 项,机器队列唯一下一任务切换为 `M12-07J`。三格式 cancellation、OOM、 + Worker restart 和小文件 recovery 仍未提前声明。 + +M12-07J 实际验证(2026-08-18 America/New_York) +- 直接领取 M12-07J:新增 schema-1 `io-format-recovery` receipt,绑定格式、operation、输入/ + 输出 SHA-256、base/candidate revision、Worker generation、temporary bytes、live requests、 + published results 和 commit 状态。生产 `io-format-recovery-test.worker.ts` 复用 OBJ/STL/PLY + parser/serializer,取消返回 `IO_FORMAT_OPERATION_CANCELLED`,超过 512 KiB 的确定性预算故障 + 返回 `IO_FORMAT_OOM`,generation 2 recovery 返回 `IO_FORMAT_WORKER_RESTARTED`。 +- Chromium 命令: + `UPDATE_IO_FORMAT_RECOVERY_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5492 + node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/io-format-recovery.spec.ts` 通过 1/1;随后 WEB_TEST_PORT=5493 不更新 report 重跑仍 + 通过,固定 operationId 和 receipt 输出确定性成立。 +- OBJ/STL/PLY 各自均完成取消 1 次、OOM 1 次、generation 1->2 hash-stable restart 1 次和 + generation 3 small-file recovery 1 次,三格式输出 hash 均在重启/恢复后保持不变,取消/OOM + `publishedResults=0`、temporary/live 归零。 +- `node --test web/tests/unit/io-format-recovery.test.mjs` 通过 2/2; + `node tools/web/check-io-format-recovery.mjs` 输出 + `io-format-recovery-ok formats=OBJ,STL,PLY cancelled=3 oom=3 restart=3 smallRecovery=3 deterministic=true next=M13-01A`。 + `npm --prefix web run typecheck`、`git diff --check` 通过。 +- hashes:protocol `7e352539e3300969c7409c34d6056eb6ad31055431ffce84b1b0a459c335480a`;Worker/ + adapter `63b78fbf25132cad28147ef68731f2cd212a26c96b464fdec349a13ebaa1174f` / + `cfcebb6ec38936a66983957b0dede716871cfbfbea3cb53cddc16f3e24fb19b0`;unit/e2e + `aaed1f2abe4789b084c8a6a60bcce8595d38220d7e6678a93924cd05c5716b4f` / + `5c1750fa408e1297fc43f2e5749be3e9ac08a16b86265d22f0f06053f52b3bd7`;checker + `6ef6bc4a168f8675a4933a06c296f61076b9ea64cec25b295e961a9b610ab1a2`;report + `35d4fe10d8a4fa84d6e05a85f20ca50975d93a86df41e73c7bbc5875edc4fc70`;manifest + `a1c4cf9c2cc300ace388e6c430bd1aa991511a7d1c5482c638fb298bb362cd02`。 +- 新增 `docs/status/M12-07J.md`,并同步 N-023、当前/完整对标计划、WBS、项目状态和本文件; + 本项 `enablingTask=false/parityStateChange=false`,ledger 保持 195 completed/58 blocked; + M12 当前完成 64 项,机器队列唯一下一任务切换为 `M13-01A`。 + +M13-01A 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-01A:读取 M13 scripting/security 任务定义及既有 N-025 默认拒绝 reader;新增 + pinned Blender 5.2 `generate-script-entry-inventory.py`,创建 3 个 Text datablock(internal、 + `use_module` autorun request、project-relative external path)和 1 个 driver expression,盘点 + Python Console 3 个 operator、39 个 handler groups、4 个 add-on operator。报告将 Text 固定为 + `READ_METADATA_ONLY`,Console/autorun/driver/handler/add-on 固定为 `DENY`,不执行任何用户脚本。 +- `node tools/web/check-script-entry-inventory.mjs` 通过 runtime identity、entry policy、semantic + inventory 与独立临时目录重生成: + `script-entry-inventory-ok texts=3 console=3 autorun=1 drivers=1 handlers=39 addonOps=4 deterministic=true next=M13-01B`。 + Blender `.blend` 保存含运行时 save bytes,checker 对语义报告和 fixture byte length 做确定性断言, + baseline fixture SHA-256 仍绑定 manifest。 +- `python3 -m py_compile tools/web/generate-script-entry-inventory.py`、Node syntax check、 + `git diff --check` 通过;本项是 `enablingTask=true/parityStateChange=false`,没有开放脚本执行。 +- hashes:generator `b72667493cfe9957161ef3fb9814180e0cfad5f8aaa1c60c9b6f132e915f3d6f`;checker + `68478f15de4d63ed175e6b580761fd478b1fe9bccbfcaeee82218d13063dfa51`;report + `e024995c53f01beaf725f281f74a6e5ec6018a53435da61a66f5e58a9e50973c`;fixture + `bd6375d02908bfcc57ff7cdeec3f9827bfc4336d1e46e165c5fbbf4d04f19645`;manifest + `d4a305033343ef5fb1ffc073617b59d9012f64b80ecb233e743fb0789a8b4893`。 +- 新增 `docs/status/M13-01A.md`,并同步当前/完整对标计划、项目状态和本文件;M12 仍为 64 项, + M13 当前完成 M13-01A,机器队列唯一下一任务切换为 `M13-01B`。脚本 metadata-open、policy + code、UI bypass、保存未知 Text、恶意 fixture 和 sandbox/server isolation 仍未提前声明。 +M13-01B 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-01B:复用生产 Blender Main script reader 与真实 `tests/files/web/script_scene.blend`; + open 只重建 Text metadata/source、byte length 和 SHA-256,所有 source 保持 read-only 与 + `executionStatus=BLOCKED`,`ModuleAutorun.py` 的 `use_module` 请求返回 `SCRIPT_POLICY_DENIED`。 + 未调用 Python Console、driver、handler 或 add-on 执行入口。 +- `node tools/web/check-script-open-metadata.mjs` 通过,输出 + `script-open-metadata-ok blend=opened textMetadata=read sourceHash=verified execution=DENY autorun=DENY next=M13-01C`, + 并生成 `tests/golden/M13-01B/open-metadata-report.json`。 +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5495 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/script-open-metadata.spec.ts` 通过 1/1; + Chromium `WebEngineClient.openBlend` 真实路径验证 3 个 source、read-only、SHA-256 与 autorun deny。 +- `npm --prefix web run typecheck`、`git diff --check` 通过。hashes:checker + `66396d5c9a5294021ae077bb162278c74d46de8b52ac8a444a5de4f6d84cfe05`;e2e + `df6412cda113c830996e08c3d66a035dc1ac309e392f5946a762d11121b1926c`;report + `f92470e57c048452134664f7f6208e50b6f087dbcbc33369e6b882c51813990c`;fixture + `2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037`;manifest + `0bb0abdb9f4d250a15c3889a4fb5c2630b8c416f4eb5c6523aa30097c8e45fd3`。 +- 新增 `docs/status/M13-01B.md`,并同步当前/完整对标计划、项目状态和本文件;M13 当前完成 + M13-01A-B,机器队列唯一下一任务切换为 `M13-01C`。autorun/register/install/driver policy + code、UI bypass、未知 Text 保存和恶意 fixture 仍未提前声明。 +M13-01C 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-01C:复用生产 `scripting-platform` manifest parser/gate,明确验证 autorun、 + driver expression、add-on install/register 三种请求的 fail-closed code;无批准 sandbox 时, + 即使签名 key 正确也返回 `SCRIPT_SANDBOX_UNAVAILABLE`。 +- `node --test web/tests/unit/script-policy-codes.test.mjs` 通过 1/1; + `node tools/web/check-script-policy-codes.mjs` 输出 + `script-policy-codes-ok autorun=SCRIPT_POLICY_DENIED driver=DRIVER_EXECUTION_BLOCKED addon=ADDON_INSTALL_BLOCKED sandbox=SCRIPT_SANDBOX_UNAVAILABLE next=M13-01D`。 +- `npm --prefix web run typecheck`、Node syntax check、`git diff --check` 通过。hashes:checker + `22588c2198bc8c425ab8e104e8559f0053654ae778aaea3783ec7d54822bc8f4`;unit + `b5a52b8e707963545f1cd71f1a148fa9c9b9d1e4b4c5f51c87d33f8bf3777430`;report + `956db548ee71688a4b89c9a993259d3e57129cd4abe9efd1b9397b3e30b1bf84`。 +- 新增 `docs/status/M13-01C.md`,并同步当前/完整对标计划、项目状态和本文件;M13 当前完成 + M13-01A-C,机器队列唯一下一任务切换为 `M13-01D`。UI bypass、Text 保存、恶意 fixture 和 + sandbox/server isolation 仍未提前声明。 +M13-01D 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-01D:对生产 `web/app/src/app`、`web/app/src/workers`、`web/protocol` 共 176 个 + 非 vendor TypeScript 文件执行 direct-eval bypass scan,禁止 `eval(` 与 `new Function(`;扫描 + 无 violation,仅允许 `gateScriptExecution`、`gateServerScriptJob`、`parseScriptSourceInventory` + 三个声明 policy entry point。 +- `node tools/web/check-script-ui-bypass.mjs` 通过,输出 + `script-ui-bypass-ok scanned=176 violations=0 policyEntrypoints=3 report=8e234e128b90036548ab709b86b063de404250da2c5c0a6e25e28969b5cd5c73 next=M13-01E`。 + `git diff --check` 通过。 +- hashes:checker `f34cb64891c2b22bc3da353f6b864ba3e558d3c286cf716bcb778d9a542cb3d9`;report + `6b050092088508ebd5d769d95a2e66f0cd4020c97f3407724a19b9707f51f6dd`;manifest + `6b28688b3ebcce5629bcfc437d4ca903d0b1e053b32a796690ee4e021726c75b`。 +- 新增 `docs/status/M13-01D.md`,并同步当前/完整对标计划、项目状态和本文件;M13 当前完成 + M13-01A-D,机器队列唯一下一任务切换为 `M13-01E`。Text 保存/重开、恶意 Text/driver/handler + fixture、sandbox、server isolation 和完整 CSP 仍未提前声明。 +M13-01E 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-01E:生产 `WebEngineClient` 对真实 `script_scene.blend` 执行 open -> saveBlend -> + 新 WebEngineClient reopen;3 个 Text source 的完整 metadata/source、byte length、SHA-256 exact, + 只读与 `executionStatus=BLOCKED` 保持,`ModuleAutorun.py` 仍为 `SCRIPT_POLICY_DENIED`。 +- `UPDATE_SCRIPT_SAVE_REOPEN_REPORT=1 CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5498 + node_modules/.bin/playwright test --config playwright.config.ts --workers=1 + tests/e2e/script-save-reopen.spec.ts` 通过 1/1;WEB_TEST_PORT=5497 不更新 report 重跑通过。 +- `node tools/web/check-script-save-reopen.mjs` 输出 + `script-save-reopen-ok sources=3 exact=true blocked=true savedBytes=490191 next=M13-01F`; + `npm --prefix web run typecheck`、`git diff --check` 通过。 +- hashes:checker `7fc9a370cb472636e2699565cb21a1450e3cc8a2c90ffdcdff96533b344afa7c`;e2e + `481f78f3a0cce923b67ab14436cc23cbcd2ce66725de29dc874fea4fb5801227`;report + `0f6869a8ec8342ed87649312d2872ab2c172cbf12d6be81bb0b770ebcbe8a398`;manifest + `889a4e06f7e8c0ea55b3ca4baa9fe85dccbe97053e497a45e3d364ce2b70a7c6`;fixture + `2b8fbdb05419e1e61d64777998d6a3b2562f726c17f548cd1852efc723e1a037`。 +- 新增 `docs/status/M13-01E.md`,并同步当前/完整对标计划、项目状态和本文件;M13 当前完成 + M13-01A-E,机器队列唯一下一任务切换为 `M13-01F`。恶意 Text/driver/handler fixture、sandbox、 + server isolation 和完整 CSP 仍未提前声明。 +M13-01F 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-01F:pinned Blender 5.2 生成包含 OS command Text、driver import、handler + subprocess、embedded `register()` module 的四源恶意 fixture;每个 source 记录 SHA-256, + EmbeddedModule `use_module=true`,预期执行统一 `BLOCKED`。 +- `node tools/web/check-malicious-script-fixture.mjs` 输出 + `malicious-script-fixture-ok sources=4 module=1 execution=BLOCKED fixtureSha256=7b1921931afc5bb74a2b73e90b175017c583ea878cdbb66f131718b2d8cd23b5 next=M13-02A`。 +- `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5499 node_modules/.bin/playwright test + --config playwright.config.ts --workers=1 tests/e2e/malicious-script.spec.ts` 通过 1/1;Chromium + 生产 Main reader 打开四个 source,全部 read-only/`BLOCKED`,embedded module 返回 + `SCRIPT_POLICY_DENIED`,未执行恶意内容。 +- `git diff --check` 通过。hashes:generator `013285befeb59de21a887cefd377adf8cf53ff1c785b28a9c87d29f76f092731`; + checker `36608da893ae59e2e03856a62866ea6e7c349ec4a63200f042b2329e5f61caa9`;e2e + `98fbde6728ddf55cce5262522197a5b4db1dfce618e52259bf0b0c9e0e9165f2`;report + `a628b73411d6f9a761f659ae28867ea9b0c0df30d47668353278b70d4b44180c`;fixture + `7b1921931afc5bb74a2b73e90b175017c583ea878cdbb66f131718b2d8cd23b5`;manifest + `9a0b7c4097b3755365a9fb92b4848e6ac2ddd36ee2c7e9df3cb8808ce247cf3d`。 +- 新增 `docs/status/M13-01F.md`,并同步当前/完整对标计划、项目状态和本文件;M13 当前完成 + M13-01A-F,机器队列唯一下一任务切换为 `M13-02A`。script manifest、sandbox、server isolation + 和 CSP 仍未提前声明。 + +M13-02A 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-02A:读取 M13-01F 完成证据及 M13.2 规范,补齐生产 + `web/protocol/scripting-platform.ts` 的 bounded manifest parser。每个 script 现在必须声明 + `sourceByteLength` 与 `module=false`;解析器限制脚本数量、总源码字节、依赖数量、权限数量、 + CPU/内存/墙钟预算,canonicalize 项目内 entry/dependency path,并拒绝重复依赖、unsafe path、 + module execution、未知/重复 permission 和所有预算溢出。解析仍为 metadata/policy gate,不执行 + Python、module、driver、handler 或 add-on。 +- 新增生产 Worker `web/app/src/workers/scripting-manifest-budget-test.worker.ts`、Node unit + `web/tests/unit/script-manifest-budgets.test.mjs`、Chromium E2E + `web/tests/e2e/script-manifest-budgets.spec.ts`、checker + `tools/web/check-script-manifest-budgets.mjs`,并把 `test:script-manifest-budgets` 加入 + `web/package.json`。新增 `tests/golden/M13-02A/manifest-budget-report.json` 与 + `tests/golden/M13-02A/manifest.json`,同步 `docs/status/M13-02A.md`、N-025、当前执行计划、 + 完整执行计划、WBS、项目状态和本文件。 +- `node --test web/tests/unit/script-manifest-budgets.test.mjs`:4/4 通过;覆盖 canonical path、 + valid aggregate bytes、script count/total bytes、module/path/dependency/permission negative。 +- `WEB_TEST_PORT=5513 npm --prefix web run test:script-manifest-budgets`:unit 4/4,Chromium + Worker 1/1 通过。首次无独立端口的 npm 调用因宿主已有 127.0.0.1:5173 服务而未启动,未计入 + READY;5513 重跑为正式证据。`node tools/web/check-script-manifest-budgets.mjs` 输出: + `script-manifest-budgets-ok accepted=2 totalSourceBytes=256 blocked=6 deterministic=true next=M13-02B`。 +- `npm --prefix web run test:scripting-isolation` 通过;批准 key 仍稳定返回 + `SCRIPT_SANDBOX_UNAVAILABLE`,server 仍返回 `SERVER_JOB_UNAVAILABLE`,没有启用执行。 + `npm --prefix web run typecheck`、`npm --prefix web run lint`、`npm --prefix web run build` + (81 modules)和 `git diff --check` 通过。 +- hashes:protocol `0931023370e5ef97ad5fa5d396e03bfe67791fc98aab54d1a66bdfef1cbbd247`;worker + `8d8eae67fa6915f0337850e15247baf01f0abde0b3362fbb120f0b448f1a4cf5`;checker + `b3457324d72ab233cd17a142ea19fac6a0d024dd95de7b8fb5d26810437fb0d2`;unit + `6ce7847a0b745ba4081e4332d012e0b7f86e4906c71c95bdeda8c39977a630ee`;e2e + `1ef4fd4caaeb1fa3d832fc8881823d5284755372575eab186c751f408dc9314c`;report + `860672fe844b7969ca376d4b2708771189d1767efa819f7b97667d8a26438d3a`;manifest + `fb4a1d2ac82bf6892e9749c2cfae7b9e61ecac0b1523c75230c0cd5ef97cf97a`。 +- M13 当前完成 M13-01A-F、M13-02A;本项 `enablingTask=false/parityStateChange=false`,不改变 + 12 个全域 parity `BLOCKED` 或 V1 release 状态。机器队列唯一下一任务切换为 `M13-02B`;下一项 + 只从机器 `nextTask` 领取,当前仍不声明 canonical signature serialization、signer/key + rotation/revocation、sandbox、server isolation 或 CSP。 + +M13-02B 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-02B:在生产 `web/protocol/scripting-platform.ts` 导出唯一 + `canonicalizeScriptingManifest` 与 `serializeScriptingManifest`,将 script、permission、dependency + 数组按 locale-independent code-unit 排序,stable JSON 固定 object key 顺序、无空白,parser + 先剥离 unknown field;sourceByteLength/sourceSha256/permissions/path/budget/policy 等安全声明 + 保留在签名输入。源 hash 或 schema mutation 不会得到相同输入;本项不验证签名、不执行脚本。 +- 新增 `web/tests/unit/script-manifest-canonical.test.mjs`(2/2)、生产 Worker + `web/app/src/workers/scripting-manifest-canonical-test.worker.ts`、Chromium E2E + `web/tests/e2e/script-manifest-canonical.spec.ts`、checker + `tools/web/check-script-manifest-canonical.mjs`、golden report/manifest 和 + `test:script-manifest-canonical` npm script。 +- `WEB_TEST_PORT=5514 npm --prefix web run test:script-manifest-canonical` 通过 unit 2/2、Chromium + Worker 1/1;checker 输出 + `script-manifest-canonical-ok equal=true bytes=1923 unknownDropped=true schemaMutation=blocked next=M13-02C`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`npm --prefix web run build` + (81 modules)、`node tools/web/check-script-manifest-budgets.mjs`、 + `node tools/web/check-status-consistency.mjs` 和 `git diff --check` 通过。M13-02A 的 protocol + artifact hash 已从头按当前 additive parser 复验并刷新其 manifest/parent hash,预算行为、report + 和 parity 状态未改变。 +- hashes:protocol `4c74a4dec7d30f03a295a6b1c1059e97563d561ecda968aa07e15191822abdb9`;worker + `de27fdcd6d16e27a07806ac609f908edfdb7a93d653676174cdd5e06ffa394af`;checker + `5ed344cab6428ae5538a450171ba40c73ff738666515492298e30aaed1394f56`;unit + `33eae0f3ad2ae7243c9ce2835ab8e42186f65f574ab682099766f1d0f4c481f6`;e2e + `5342301165ae82a01b46f5fed0cc0ec34b9813a6ecbc8032900d90b89628e064`;report + `5f4bc0b098ea65de47f1255d30130376d74260e2b912bcd0e28354171fbd07df`;parent manifest + `e81b106b9428f507fff29d1a4ba90b73d417d71c46978cfbbf7d7de9fc11e9c5`;manifest + `452bcf729d0fe96c536a11ef45e8d2bbc61b023ae58465828329fb8eb2f804f7`。 +- M13 当前完成 M13-01A-F、M13-02A-B;本项 `enablingTask=false/parityStateChange=false`,不改变 + 12 个全域 parity `BLOCKED` 或 V1 release 状态。机器队列唯一下一任务切换为 `M13-02C`;下一项 + 只从机器 `nextTask` 领取,当前仍不声明 signer/key rotation/revocation、signature verification、 + sandbox、server isolation 或 CSP。 + +M13-02C 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-02C:新增版本化 `ScriptTrustPolicyIR`、`parseScriptTrustPolicy`、 + `canonicalizeScriptTrustPolicy`、`serializeScriptTrustPolicy` 和 `resolveScriptSigner`。策略绑定 + ED25519 public key、publisher、key status、notBefore/notAfter、revokedAt、same-publisher + `replaces` rotation predecessor、acyclic chain 和 `maxClockSkewMs`;resolver 对 active key 只 + 返回 `ELIGIBLE` + `cryptographicVerification=REQUIRED`,revoked/missing/expired/not-yet-valid/ + publisher mismatch 均 `BLOCKED`。本项没有验签、签名批准或执行入口。 +- 新增 `web/tests/unit/script-trust-policy.test.mjs`(3/3)、生产 Worker + `web/app/src/workers/script-trust-policy-test.worker.ts`、Chromium E2E + `web/tests/e2e/script-trust-policy.spec.ts`、checker + `tools/web/check-script-trust-policy.mjs`、golden report/manifest 和 + `test:script-trust-policy` npm script。 +- `WEB_TEST_PORT=5516 npm --prefix web run test:script-trust-policy` 通过 unit 3/3、Chromium + Worker 1/1;checker 输出 + `script-trust-policy-ok active=key:new revoked=REVOKED crossPublisher=SCRIPT_POLICY_DENIED policyExpired=POLICY_EXPIRED crypto=REQUIRED next=M13-02D`。 +- `npm --prefix web run typecheck` 通过;M13-02A/B checker 在 additive protocol change 后重新 + 通过,未改变预算/canonical 行为和 parity 状态。`git diff --check` 通过。 +- hashes:protocol `cfc1e499e6cdc8d44d6f36c4d1ce3490491de8527585d7ac3707aacb4ccc08b2`;worker + `c6e206b29e7cacc3e23e2ac12a3d523a9c0b16ff29759126dab13c8665547421`;checker + `0e55ce20d142f2bcbc5ce9c6fb955ef2771d284dff636c79da3f59a33b8b0aeb`;unit + `59ac77bcc1086e0a0e914cb77d647db5d34c7160202e7aad52948b7277769551`;e2e + `614091bda15367090bc78a6f465fb10d02d0aa08775b5088699b12e2490034d2`;report + `0f59f733920edbbe5cb799e5f50ff31d19e55ced98e7a890828f6337a237e4bd`;parent manifest + `1a71bd636a92310094d80103ed35c237a23e79398c4af2d8fc5875b0c879b299`;manifest + `792e867e7ed8a1ca096a31d913d725fbc01280c511e9085bb7e95630588e6c9e`。 +- M13 当前完成 M13-01A-F、M13-02A-C;本项 `enablingTask=false/parityStateChange=false`,不改变 + 12 个全域 parity `BLOCKED` 或 V1 release 状态。机器队列唯一下一任务切换为 `M13-02D`;下一项 + 只从机器 `nextTask` 领取,当前仍不声明 cryptographic signature verification、permission + approval、sandbox、server isolation 或 CSP。 + +M13-03D 实际验证(2026-08-18 America/New_York) +- 直接领取 M13-03D:完成 sandbox crash/timeout isolation receipt。`terminateScriptSandboxJob` + 对 crash、wall-time timeout 和 cancellation 分别返回稳定错误码;`mainRevisionAfter` 保持 + `mainRevisionBefore`,temporary bytes、published results、late results 为 0,commit 保持 + false,执行仍为 `DISABLED`;终止后伪造结果返回 `SCRIPT_SANDBOX_LATE_RESULT`。 +- Chromium 生产 Worker E2E 真实启动 crash Worker(抛出 sandbox error)和 timeout Worker(延迟 + 发布消息);宿主观察 crash、10 ms 内终止 timeout Worker,并在迟到窗口确认 `lateMessages=0`。 +- `node --test web/tests/unit/script-sandbox-isolation.test.mjs` 通过 3/3; + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5532 npm --prefix web run + test:script-sandbox-isolation` 通过 unit 3/3 与 Chromium Worker 1/1; + `node tools/web/check-script-sandbox-isolation.mjs` 输出 + `script-sandbox-isolation-ok crash=SCRIPT_SANDBOX_CRASHED timeout=SCRIPT_SANDBOX_TIMEOUT revisionUnchanged=true late=SCRIPT_SANDBOX_LATE_RESULT next=M13-03E`。 +- `npm --prefix web run typecheck`、`git diff --check` 通过。hashes:protocol + `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2`;worker + `8c810ee7c8bd15d83ecfb4981068f947536bfe8c9e63b906861f42b8465722da`;checker + `e5c0c3c7cd500c269ae43a1a9eb05589e82aad28199ab4f11ad1167b486f6937`;unit + `47986f93638fdc18b817b03222484f4691dc294185040eaebac8e1f386bc1549`;e2e + `2c16c42f09827a0c0100ef2cc6295ca6bc96933ed7475e08b1fa195d7940c141`;report + `4af91cdb21101039b379136c7559b46df55f4f5ab5478c05c1c57cd6e1624d2a`;manifest + `8066013f3d356ba438c36d1f1ea1cf692dbb4f60b086f552072e36b783a96d42`。 +- 新增 `docs/status/M13-03D.md`,同步 N-025、当前/完整对标计划、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,12 个全域 parity 仍为 `BLOCKED`。机器队列 + 唯一下一任务切换为 `M13-03E`;cancellation/cache late-result、dispose、同会话恢复、server + isolation 和 CSP 仍未提前声明。 + +M13-03E 实际验证(2026-08-19 America/New_York) +- 直接领取 M13-03E:新增 cancellation Worker,取消 receipt 固定 `SCRIPT_SANDBOX_CANCELLED`, + Main revision 11->11、temporary/published/late resources 全为 0,执行保持 `DISABLED`; + 伪造迟到结果返回 `SCRIPT_SANDBOX_LATE_RESULT`。 +- Chromium 真实 Worker 在 10 ms 取消并终止,随后等待 80 ms 迟到窗口;报告固定 + `lateMessages=0`、`cacheWrites=0`、`cancelled=true`。这只完成 cancellation gate,不提前声明 + dispose 或同会话恢复。 +- `node --test web/tests/unit/script-sandbox-cancellation.test.mjs` 通过 2/2; + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5533 npm --prefix web run + test:script-sandbox-cancellation` 通过 unit 2/2 与 Chromium Worker 1/1; + `node tools/web/check-script-sandbox-cancellation.mjs` 输出 + `script-sandbox-cancellation-ok status=CANCELLED late=SCRIPT_SANDBOX_LATE_RESULT lateMessages=0 cacheWrites=0 next=M13-03F`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`git diff --check` 通过。hashes: + protocol `6699fc0a19e0736823f164e707a420655803e36019cf0071f7ff9c760d25e9f2`;worker + `0feb70c8c491cc24ebfe6c3e267b92522d616b6f260efcecfa57cf489d0742c0`;checker + `b6fbe7102b7d0808673931c66797f8976e79b21bd4c32e429f21832c6090708e`;unit + `374cc87f66bd42226b750e387663ef8c86bef92348fa7cbc6ed7f0027945204d`;e2e + `b233d9cafa1f70798ec19d76ca936abb610681522264a12237f532eb98e09fca`;report + `066d9f19716b8229f2cf7755ec3009a3edef942ebab5f70159bec7f2afbaf71f`;manifest + `0cdf625e6bd3ed7aca43318a3b04353cb806c51cfa4cf3c5dae9a65a0eae2e69`。 +- 新增 `docs/status/M13-03E.md`,同步 N-025、当前/完整对标计划、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,机器队列唯一下一任务切换为 `M13-03F`;dispose、 + 同会话恢复、server isolation 和 CSP 仍未提前声明。 + +M13-03F 实际验证(2026-08-19 America/New_York) +- 直接领取 M13-03F:补齐 sandbox Worker 的显式 dispose receipt。资源快照在 dispose 前固定为 + 两端 `MessagePort`、1 timer、1 AbortController、1 transferable buffer、1 pending request 和 + 1 cache reference;首次 dispose 后全部为 0,第二次 dispose 幂等,迟到 timer message 为 0。 +- Chromium 真实 Worker 通过 `init -> dispose -> dispose -> terminate`,两端 port 均 close,timer + clear、controller abort、buffer/cache reference/pending request 清除;执行继续保持 `DISABLED`。 +- `node --test web/tests/unit/script-sandbox-dispose.test.mjs` 通过 2/2; + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5534 npm --prefix web run test:script-sandbox-dispose` + 通过 unit 2/2 与 Chromium Worker 1/1;`node tools/web/check-script-sandbox-dispose.mjs` 输出 + `script-sandbox-dispose-ok ports=0 timers=0 abortControllers=0 buffers=0 pending=0 cacheReferences=0 idempotent=true next=M13-03G`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`git diff --check` 通过。hashes:protocol + `f3d9f667c3809cfad0f52bc6028d93e36e25c4b639fb6081c9c933fbad0ebc0a`;worker + `2a074b05d301c4083e60534eb9452aabf5d95aea0ab316fa657441ef4bcd5c96`;checker + `6549bd10f588281d23c4bea705fd164252c9e3f44d8f4b93c893a2c410907135`;unit + `6e00aa6286aae0eabc1345c04c7628dcc9acb32d51c1c27436a0e1b4c170b64c`;e2e + `b240d92c90e0d81272cd9cb3c0eb4e7d77102ce2e49ad05c761f763d1812b18e`;report + `2dbd3e79939b500c0fc83216c51f258b88ead42a667ed1a6da755e893d7f73c6`;manifest 待文档同步后固定。 +- 新增 `docs/status/M13-03F.md`,同步 N-025、当前/完整对标计划、WBS、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,机器队列唯一下一任务切换为 `M13-03G`;同会话 + 恢复、server isolation 和 CSP 仍未提前声明。 + +M13-03G 实际验证(2026-08-19 America/New_York) +- 直接领取 M13-03G:同一会话中以固定 Worker generation 4->5 恢复一个仍为 `DISABLED` 的脚本请求; + Main revision 11->11,source SHA-256 和 canonical manifest SHA-256 均保持不变。 +- 两条 default-deny audit entry 使用 `sandbox-recovery:g4/g5` 两个 request ID,sequence=1/2, + 第二条 `previousEntrySha256` 精确等于第一条 `entrySha256`;重复 request 和篡改 source hash + 均返回 `SCRIPT_MANIFEST_INVALID`。 +- `node --test web/tests/unit/script-sandbox-recovery.test.mjs` 通过 2/2; + `CHROME_PATH=/usr/bin/google-chrome-stable WEB_TEST_PORT=5535 npm --prefix web run test:script-sandbox-recovery` + 通过 unit 2/2 与 Chromium Worker 1/1;`node tools/web/check-script-sandbox-recovery.mjs` 输出 + `script-sandbox-recovery-ok generation=4->5 revision=11 sourceStable=true manifestStable=true sequence=1,2 chain=true replay=SCRIPT_MANIFEST_INVALID tamper=SCRIPT_MANIFEST_INVALID next=M13-04A`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`git diff --check` 通过。hashes:protocol + `8034faded657d49e1376ea42051bc302a090b485024a9ff3781e46aa82638b64`;worker + `7b02331c375d4fb7dbadadd179f0ab9a0e336c8b95fabb071c06e67f3e7b3fe4`;checker + `612015a3fca44bae0f0b78e622f044a5297ed8aa3a0efc774a40f0d47ddbdc9e`;unit + `69a2d34e38d591043ff62b2d305bd9aae684ecebd9ecba718580d77318931226`;e2e + `73176e513f115ba917be74f62a5deb8fe2918fafa8a84c9294e80d2a86b8f1ed`;report + `9057b3f49c3bb15cf53d638ada4bd2743949d3914173cd3799414489d584111d`;manifest + `c8f3bb630f13d18d8a3a13b4407c86f8248a7110839a4d26c1c16f5e4a3ca677`。 +- 新增 `docs/status/M13-03G.md`,同步 N-025、当前/完整对标计划、WBS、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,机器队列唯一下一任务切换为 `M13-04A`。M13.3 + 浏览器 sandbox 小里程碑已闭环;Python 执行、server isolation、CSP 和恶意输入仍保持阻断。 + +M13-04A 实际验证(2026-08-19 America/New_York) +- 直接领取 M13-04A:新增 Node 服务侧 `server-job-isolation.mjs`,每个 job 在绝对 root 下创建 + 随机 `.blender-job--` 目录,权限固定 `0700`,目录名不包含 request ID;成功/失败 + 路径都通过一次 cleanup 删除,重复 cleanup 幂等。 +- 两个并行 job 的目录名唯一,目录创建后真实 `fs.stat` 验证权限,清理后 `ENOENT`;相对 root、路径 + 穿越 ID 和 root 外目录均稳定返回 `SERVER_JOB_DIRECTORY_INVALID`。 +- `node --test web/tests/unit/server-job-isolation.test.mjs` 通过 2/2; + `npm --prefix web run test:server-job-directory` 通过 unit 2/2 与独立 checker;checker 输出 + `server-job-directory-ok allocated=2 cleaned=2 unique=1 requestIdsHidden=1 mode=0700 residual=0 idempotent=1 next=M13-04B`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`git diff --check` 通过。hashes:protocol + `8bcb43261eee884814ddeaffc51db8b58cbad17004991711d8817157d3c01337`;checker + `b8d0e741144f742946246370bd35820d806686fb00dc0b040c925cf16f2179da`;unit + `a9f61dc24ec19924b5722533394c7a9f8f36d4f15adb31f447b94cc37560c18b`;report + `07ed66fe0b2e1f1bbdba1cfb1089b052a5961d38f761bfcf79d8362980d2d502`;manifest 待文档同步后固定。 +- 新增 `docs/status/M13-04A.md`,同步 N-025、当前/完整对标计划、WBS、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,机器队列唯一下一任务切换为 `M13-04B`。source + mount、output 隔离、OS/container budget、network policy 和 Blender execution 仍未提前声明。 + +M13-04B 实际验证(2026-08-19 America/New_York) +- 直接领取 M13-04B:扩展 job workspace,在 job 目录下创建独立 `source/` 与 `output/`;source + directory/file 固定 `0555/0444`,output directory 固定 `0700`,source bytes 写入后立即收紧权限。 +- 真实 `fs.writeFile(sourcePath)` 返回 `EACCES`,output `result.bin` 可写入并读回;source/output + 路径不相同。cleanup 前恢复 source directory 为可删除权限,之后 source/output 均 `ENOENT`。 +- `node --test web/tests/unit/server-job-isolation.test.mjs` 通过 3/3; + `npm --prefix web run test:server-job-workspace` 通过 unit 3/3 与独立 checker;checker 输出 + `server-job-workspace-ok sourceDir=0555 sourceFile=0444 sourceWrite=EACCES outputDir=0700 outputWrite=OK distinct=1 residual=0 next=M13-04C`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`git diff --check` 通过。hashes:protocol + `3aa5678a2c2769e4db5bb8f5c755b97e23045c4106e5636459748ab9b41929cd`;checker + `9490c2eeb9980073fc1fe77fdba1fb6e4ef528de8eb666d9eaf1fb582ba658c2`;unit + `b1ac3324332180f7b3522f135520e7b5dace334dc461c92f2bce48fbcba2e147`;report + `650a643cc92617d068cb96d8bd4303429ef169ea89e3d4cb63e3ce5e2428e6d2`;manifest + `de06f1f1c718c57efb2ae4dfbe8977ec929b4c1598d0e549bc11fe5919fab8ff`。 +- 新增 `docs/status/M13-04B.md`,同步 N-025、当前/完整对标计划、WBS、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,机器队列唯一下一任务切换为 `M13-04C`。OS/container + budgets、network policy 和 Blender execution 仍未提前声明。 + +M13-04C 实际验证(2026-08-19 America/New_York) +- 直接领取 M13-04C:新增 server resource budget contract,固定 CPU 60s、memory 512 MiB、 + process=1、file=1024、wall=300s、output=512 MiB 六类上限;bounded usage receipt 标记 + `enforced=true`,任何单项超过上限都返回 `SERVER_JOB_BUDGET_EXCEEDED`,执行保持 `DISABLED`。 +- `node --test web/tests/unit/server-job-resource-budget.test.mjs` 通过 2/2; + `npm --prefix web run test:server-job-resource-budget` 通过 unit 2/2 与独立 checker;checker 输出 + `server-job-budget-ok cpu=bounded memory=bounded process=bounded files=bounded wall=bounded output=bounded overages=6 execution=DISABLED next=M13-04D`。 +- `npm --prefix web run typecheck`、`npm --prefix web run lint`、`git diff --check` 通过。hashes:protocol + `9746f0aef9dd76411320792dee1213c03755a3c03a87bbc4cbf88d4324c728d6`;checker + `5599f4e25fc3ceca18e04be322450a4dc5bb6ce9c72c3abe9f1afc9c81851ff8`;unit + `05212b2aa639f4c37e37e1cac0597d9b367a1280e519240f073762394914dc63`;report + `76ba684966bf7e680b0ebfc630ae9e080f04dd6daa6dd744bbb22dcff43f8eb1`;manifest + `fcc03bb5ea69f787ba24c66fd510068eb26bf40bf2543020ee198e71dd048f42`。 +- 新增 `docs/status/M13-04C.md`,同步 N-025、当前/完整对标计划、WBS、项目状态和执行队列;本项 + `enablingTask=false/parityStateChange=false`,机器队列唯一下一任务切换为 `M13-04D`。OS/container + actual enforcement、network policy、Blender execution、CSP 和恶意输入仍未提前声明。