diff --git a/docs/atlas/catalog.json b/docs/atlas/catalog.json index 5797001b..e223299c 100644 --- a/docs/atlas/catalog.json +++ b/docs/atlas/catalog.json @@ -1 +1 @@ -{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} +{"records":["049f1d0d8155f65b6f7675ebdb200d4595570cc7d585dd74876e0285eceffed6","0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","85f06991da74fa4e59ba73c7034dc53a35a4e90c5b64d83cc8956d3afd61d376","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa","a38737476856236f33e0aef1b97e2b900677cf99f1dbcfd381c708ceeca8b7f3","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","ab69ebe00eac40c68aec6131ef0559967b399266aae89f9673d3c30ce12f36b0","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67","d9718eb12e9ae9ea3a29a9904f131f53e99c584ef1b297aa05f1f8f6cdc6ab2a","fa8cd29fe962d7e853457e829c2d5927685f0ff374e6450fff5529f785db442d"],"schema":"fsgg.atlas/1"} diff --git a/docs/atlas/records/8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1.json b/docs/atlas/records/8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1.json new file mode 100644 index 00000000..f1e48924 --- /dev/null +++ b/docs/atlas/records/8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1.json @@ -0,0 +1 @@ +{"citations":[{"path":"src/Wasm.Browser/Admission.fs","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"0bb3799d2ee00ddbbbfa413db6b616a692e3228943d1fb43cf3c370f1e0475dc"},{"path":"tests/Wasm.Invocation.Tests/Program.fs","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"a355d817198bdd1cc00a80b0f616773c7567638b841d71e359dd9d4c16b0c6b3"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.mjs","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"656dab9d7ad8bd551761db05f4d43a5810abc13d466b3bd046992fa7af783559"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"54dee8bb9c32ab0fc009b930f465371ee3d6b3819ae3e62fb00d6950828a4db3"},{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"76c4cb3f5ce448b83099dc8de209a31273d5f2cb60f26e06b0dc1800ebed3fd5"},{"path":"eng/wasm-shared/version.props","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"6b50423f8f30da7d52305ee527baaddd91f77cc422f7585cf61c063bcad209e1"},{"path":"scripts/wasm-release/promotion.py","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"d2ea88920b22e30df973765085c92116685dda689616045bb3b01080bd5d848d"},{"path":"tests/release/wasm/test-release-promotion.py","repository":"FS-GG/FS.GG.Game","revision":"576898054e91c5231c0c78cee656cbc9eb40e5af","sha256":"3b66a4dbf32d2334237c7301582fb4e1f2b96dd26ed188d39e4fe2d94e3795b9"}],"conflictsWith":[],"id":"game/closed-worker-build-boundary","kind":"architecture","recordedAt":"2026-10-17T23:27:47Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Selected current-only source successor0.6.0 prepares one semantic change in the bounded instruction walker: exact matching of the same eight one-u32 opcodes replaces a literal eight-element F# list and List.contains inside every instruction iteration. Published0.5 generated JavaScript materializes the array/list and comparer on each iteration; this is an avoidable O(8N) allocation/traversal path, not an established quadratic scan or proof of any prior timeout's CPU cause. The surrounding byte/u32 Result representation and export/type checks outside the instruction loop remain unchanged. Unsupported opcode refusal, u32 consumption and first-error offsets retain their current semantics.\nPublic test preparation uses the existing owned module for all eight opcode syntax controls: one/two/five-byte operands and exact truncated, overflow and unterminated errors. Syntax admission is not native WebAssembly validity. A separate owned control module receives65536 genuine nop instructions in its first function while preserving other bytes. Actual installed fresh initial/candidate-abort/new-candidate Workers must admit and execute with unchanged pre-create250ms budgets, complete bounded stage observations and final retirement. No private consumer module or result is copied. Existing cold-expiry and late-message controls remain distinct; absent diagnostic packets establish last observed boundaries, not CPU attribution or guaranteed delivery.\nThe pair/SDK, current producer guards and Project dependency lock advance coherently to0.6.0 without changing unrelated lock hashes, runtime ABI, validator set, clocks, capacity, build tools or workflows. Existing pure/static production-caller preflight passed on clean committed source; an earlier invocation correctly refused dirty executor source. F#/Fable/compiler/bundle/native syntax/browser/cleanup and actual35-minute profile fit remain unrun for this candidate. One added three-Worker browser history stays under its existing15-second per-case cap; predecessor duration is only headroom evidence.\nPublished0.5 producer805400, original publisher39073209865/release416092764/transactionc567d141277004186ec6f4b24df63defb103c2fc1b811d1f4c3bcd83588cacb7 and accepted finite Qc938 served-feed39076218708 remain immutable. The installed binder still names that exact0.5 subject and explicitly refuses unpublished0.6; no compatibility route or arbitrary overlay is introduced. A future genuine publication-specific Q and separately selected publication/installed operations are required. Source qualification does not grant consumer, product, image/native or future timing acceptance.","supersedes":["71bc29a175940d22dd1d85148ee49f47aed714395fbd51d07b8751147815a55f"],"tags":["wasm","admission","parser","allocation","worker","current-only","qualification"],"title":"Prepare allocation-free opcode dispatch without changing admission or deadline authority"} diff --git a/docs/atlas/records/9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa.json b/docs/atlas/records/9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa.json new file mode 100644 index 00000000..3013df2f --- /dev/null +++ b/docs/atlas/records/9b6c05923a43ea37b10504396105d97ba51c0430e82fb3bb4953ee4256cdc1aa.json @@ -0,0 +1 @@ +{"citations":[{"path":"src/Wasm.Browser/Admission.fs","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"0bb3799d2ee00ddbbbfa413db6b616a692e3228943d1fb43cf3c370f1e0475dc"},{"path":"tests/Wasm.Invocation.Tests/Program.fs","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"a355d817198bdd1cc00a80b0f616773c7567638b841d71e359dd9d4c16b0c6b3"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.mjs","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"656dab9d7ad8bd551761db05f4d43a5810abc13d466b3bd046992fa7af783559"},{"path":"tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"54dee8bb9c32ab0fc009b930f465371ee3d6b3819ae3e62fb00d6950828a4db3"},{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"128a8386d5fd92e3587d169be34703384b5d1a16d05993be7274dae7f87cd6fb"},{"path":"eng/wasm-shared/version.props","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"6b50423f8f30da7d52305ee527baaddd91f77cc422f7585cf61c063bcad209e1"},{"path":"scripts/wasm-release/promotion.py","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"d2ea88920b22e30df973765085c92116685dda689616045bb3b01080bd5d848d"},{"path":"tests/release/wasm/test-release-promotion.py","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"3b66a4dbf32d2334237c7301582fb4e1f2b96dd26ed188d39e4fe2d94e3795b9"},{"path":"scripts/wasm-release/api-baseline-policy.json","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"d8c13049d0c042d88b6c0038bb51ef89350d54184136065e46cbd1b744db21b3"},{"path":"scripts/wasm-release/api-baseline-policy-0.5.0.json","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"aa8c1d997d58bf4203dc4584ffcc5f2201cab76f87aa29cf406d1f1aeda408d4"},{"path":"scripts/wasm-release/release_manifest.py","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"0a75aab0213be7bdaa4521a9a75a8228d0d3860b8e83ab51c2446c36c88891cd"},{"path":"tests/release/wasm/test-api-baseline-policy.py","repository":"FS-GG/FS.GG.Game","revision":"25e32ae2a118bcb30c74b13fe056ccd6063c10c6","sha256":"bfe0b46bd85d4be2ea0fb8dee6a6454e3c33c5f21c03feb21f2de9657febf9f1"}],"conflictsWith":[],"id":"game/closed-worker-build-boundary","kind":"architecture","recordedAt":"2026-10-17T23:28:41Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Selected current-only source successor0.6.0 prepares one semantic change in the bounded instruction walker: exact matching of the same eight one-u32 opcodes replaces a literal eight-element F# list and List.contains inside every instruction iteration. Published0.5 generated JavaScript materializes the array/list and comparer on each iteration; this is an avoidable O(8N) allocation/traversal path, not an established quadratic scan or proof of any prior timeout's CPU cause. The surrounding byte/u32 Result representation and export/type checks outside the instruction loop remain unchanged. Unsupported opcode refusal, u32 consumption and first-error offsets retain their current semantics.\nPublic test preparation uses the existing owned module for all eight opcode syntax controls: one/two/five-byte operands and exact truncated, overflow and unterminated errors. Syntax admission is not native WebAssembly validity. A separate owned control module receives65536 genuine nop instructions in its first function while preserving other bytes. Actual installed fresh initial/candidate-abort/new-candidate Workers must admit and execute with unchanged pre-create250ms budgets, complete bounded stage observations and final retirement. No private consumer module or result is copied. Existing cold-expiry and late-message controls remain distinct; absent diagnostic packets establish last observed boundaries, not CPU attribution or guaranteed delivery.\nThe pair/SDK, current producer guards and Project dependency lock advance coherently to0.6.0 without changing unrelated lock hashes, runtime ABI, validator set, clocks, capacity, build tools or workflows. Existing pure/static production-caller preflight passed on clean committed source; an earlier invocation correctly refused dirty executor source. F#/Fable/compiler/bundle/native syntax/browser/cleanup and actual35-minute profile fit remain unrun for this candidate. One added three-Worker browser history stays under its existing15-second per-case cap; predecessor duration is only headroom evidence.\nPublished0.5 producer805400, original publisher39073209865/release416092764/transactionc567d141277004186ec6f4b24df63defb103c2fc1b811d1f4c3bcd83588cacb7 and accepted finite Qc938 served-feed39076218708 remain immutable. The installed binder still names that exact0.5 subject and explicitly refuses unpublished0.6; no compatibility route or arbitrary overlay is introduced. A future genuine publication-specific Q and separately selected publication/installed operations are required. Source qualification does not grant consumer, product, image/native or future timing acceptance.\nIndependent source review caught a stale0.4 published ApiCompat baseline and missing immutable0.5 candidate policy reader during the version transition. The corrected mandatory current0.6 comparison binds actual published0.5 P805400/treefa2/manifest4874c07d6c6b4f1cd920e2a1379de2e5778529f94d211b2c8d138ed8d719483c and its authentic signed served archive hashes. The archived0.5 policy is byte-identical to P and only validates immutable evidence; it is not a consumer compatibility wrapper. Actual receipt-validator controls exercise both versioned policies and refuse changed valid-looking source/tree/manifest/served bytes. The required ApiCompat rail, native comparison and unavailability refusal remain intact.","supersedes":["8c514796f190fe6d4307be57983c84f92208f5d2f00fcbe3093b72618f2d7bc1"],"tags":["wasm","admission","parser","allocation","worker","current-only","qualification"],"title":"Prepare allocation-free opcode dispatch without changing admission or deadline authority"} diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index 49d27976..a5ed4478 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -1,8 +1,46 @@ # WASM-SHARED-01 — producer qualification successor -## Current 0.5 closed Worker and installed qualifier - -The current source successor selects coherent `0.5.0` for the pair and SDK. +## Current 0.6 admission dispatch preparation + +The selected current-only successor is coherent `0.6.0` for the pair and SDK. +The instruction walker previously constructed an eight-element F# list and +performed `List.contains` for every instruction; the generated JavaScript also +constructed the array/list and equality comparer inside that loop. The selected +change uses an exact opcode pattern for the same eight one-u32 instructions. +The byte reader, u32 LEB parsing, first-error offsets, refusal set, memory bounds +and native compiler's independent syntax check remain unchanged. The scan is a +bounded linear walk; this avoids repeated allocation and traversal, without +claiming a quadratic algorithm or proving the cause of any earlier timeout. +Export/type checks outside the instruction loop retain their existing bounded +list operations. The byte/LEB Result representation is outside this correction. + +Public regression preparation checks all eight opcodes with one-, two- and +five-byte u32 operands and exact truncated/overflow/unterminated first errors. +A public owned module gains 65,536 genuine nop instructions without changing its +other function bytes. Genuine fresh installed Workers must admit, initialize, +prepare/abort and prepare again with the existing 250 ms request budgets and +stage observations. These controls do not copy private consumer modules, prime +Workers, reset phase budgets or infer time spent from absent observations. +The existing linear static preflight precedes the unchanged 35-minute hosted +qualification; one additional public browser history has three fresh Workers, +with the existing 15-second per-case cap. Actual compilation, generated-code +correspondence, admission results, cleanup and whole-job fit remain unqualified +until the exact candidate runs. Original failures and future scheduling remain +separate from the source optimization. + +The mandatory ApiCompat rail compares against actual published 0.5 P805400, its +manifest and signed served archive hashes. The exact original 0.5 candidate policy +remains a versioned historical evidence reader, not an execution fallback. + +Accepted 0.5 publication/installed identities below remain immutable. The finite +installed binder still selects that exact publication and refuses the unpublished +0.6 successor; a later publication-specific qualifier requires genuine receipts. +This source assignment selects no publication, installed operation, consumer +activation, image/native workload or product outcome. + +## Accepted 0.5 closed Worker and installed qualifier + +The accepted source successor selected coherent `0.5.0` for the pair and SDK. Published `0.4.0` and its historical P/Q installed qualification below remain unchanged. The previous generated Worker policy formed an ESM import graph that had to evaluate before the native entry installed its command handler. Request admission still diff --git a/eng/wasm-shared/version.props b/eng/wasm-shared/version.props index 040cac38..2a242a63 100644 --- a/eng/wasm-shared/version.props +++ b/eng/wasm-shared/version.props @@ -1,6 +1,6 @@ - 0.5.0 + 0.6.0 wasm/v$(WasmSharedVersion) diff --git a/examples/wasm/rust/bar-guest/Cargo.lock b/examples/wasm/rust/bar-guest/Cargo.lock index 7e2afa94..17de6f9f 100644 --- a/examples/wasm/rust/bar-guest/Cargo.lock +++ b/examples/wasm/rust/bar-guest/Cargo.lock @@ -3,11 +3,11 @@ version = 4 [[package]] name = "fsgg-wasm-bar-example" -version = "0.5.0" +version = "0.6.0" dependencies = [ "fsgg-wasm-guest", ] [[package]] name = "fsgg-wasm-guest" -version = "0.5.0" +version = "0.6.0" diff --git a/examples/wasm/rust/bar-guest/Cargo.toml b/examples/wasm/rust/bar-guest/Cargo.toml index bfa8ef31..04ac06d3 100644 --- a/examples/wasm/rust/bar-guest/Cargo.toml +++ b/examples/wasm/rust/bar-guest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsgg-wasm-bar-example" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT" publish = false diff --git a/examples/wasm/rust/sc2-guest/Cargo.lock b/examples/wasm/rust/sc2-guest/Cargo.lock index 3bdb48ab..5ae51d68 100644 --- a/examples/wasm/rust/sc2-guest/Cargo.lock +++ b/examples/wasm/rust/sc2-guest/Cargo.lock @@ -3,11 +3,11 @@ version = 4 [[package]] name = "fsgg-wasm-sc2-example" -version = "0.5.0" +version = "0.6.0" dependencies = [ "fsgg-wasm-guest", ] [[package]] name = "fsgg-wasm-guest" -version = "0.5.0" +version = "0.6.0" diff --git a/examples/wasm/rust/sc2-guest/Cargo.toml b/examples/wasm/rust/sc2-guest/Cargo.toml index 9316395c..9f3aa96a 100644 --- a/examples/wasm/rust/sc2-guest/Cargo.toml +++ b/examples/wasm/rust/sc2-guest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsgg-wasm-sc2-example" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT" publish = false diff --git a/scripts/wasm-release/api-baseline-policy-0.5.0.json b/scripts/wasm-release/api-baseline-policy-0.5.0.json new file mode 100644 index 00000000..8258c5d1 --- /dev/null +++ b/scripts/wasm-release/api-baseline-policy-0.5.0.json @@ -0,0 +1,20 @@ +{ + "baselineVersion": "0.4.0", + "baselineTag": "wasm/v0.4.0", + "baselineSource": "6732d2371d142d5ccc5d2d127f20e4f179800892", + "baselineTree": "cc71e7cfe73449919376acf775940ff9e8bdab56", + "baselineManifestSha256": "f8f47c2f5ae6fd8b5aef474dfc83b096aa0a6ad0265eb16dc4f0df7ade7f01b7", + "candidateVersion": "0.5.0", + "packages": { + "FS.GG.Wasm.Contracts": { + "servedArchiveSha256": "2c3ed82724fc41bb86141369d297b393e847a172475709254456e9ffd42a6992", + "expectedStatus": "compatible", + "removedConstructorTypes": [] + }, + "FS.GG.Wasm.Browser": { + "servedArchiveSha256": "8231d666b5001d334b1591c71795c800b87e0bb58336b55826ec6486f787de69", + "expectedStatus": "compatible", + "removedConstructorTypes": [] + } + } +} diff --git a/scripts/wasm-release/api-baseline-policy.json b/scripts/wasm-release/api-baseline-policy.json index 8258c5d1..123d4bfb 100644 --- a/scripts/wasm-release/api-baseline-policy.json +++ b/scripts/wasm-release/api-baseline-policy.json @@ -1,18 +1,18 @@ { - "baselineVersion": "0.4.0", - "baselineTag": "wasm/v0.4.0", - "baselineSource": "6732d2371d142d5ccc5d2d127f20e4f179800892", - "baselineTree": "cc71e7cfe73449919376acf775940ff9e8bdab56", - "baselineManifestSha256": "f8f47c2f5ae6fd8b5aef474dfc83b096aa0a6ad0265eb16dc4f0df7ade7f01b7", - "candidateVersion": "0.5.0", + "baselineVersion": "0.5.0", + "baselineTag": "wasm/v0.5.0", + "baselineSource": "80540027623d9b1a8aa7715d0591de48455403b7", + "baselineTree": "fa2cd0160f9a7c5bb0a1ad85fd8f1eff68b867d3", + "baselineManifestSha256": "4874c07d6c6b4f1cd920e2a1379de2e5778529f94d211b2c8d138ed8d719483c", + "candidateVersion": "0.6.0", "packages": { "FS.GG.Wasm.Contracts": { - "servedArchiveSha256": "2c3ed82724fc41bb86141369d297b393e847a172475709254456e9ffd42a6992", + "servedArchiveSha256": "8cd706fd6e982c48977934015bdd34dcf05434dc78e9b290deb237424fecf8b4", "expectedStatus": "compatible", "removedConstructorTypes": [] }, "FS.GG.Wasm.Browser": { - "servedArchiveSha256": "8231d666b5001d334b1591c71795c800b87e0bb58336b55826ec6486f787de69", + "servedArchiveSha256": "498878e8fa7660fbeeb2883cfbc2385277f8ef743ae9a79ad0377bfbffcbad93", "expectedStatus": "compatible", "removedConstructorTypes": [] } diff --git a/scripts/wasm-release/promotion.py b/scripts/wasm-release/promotion.py index e27112fd..807a749f 100644 --- a/scripts/wasm-release/promotion.py +++ b/scripts/wasm-release/promotion.py @@ -31,7 +31,7 @@ sys.dont_write_bytecode = True ROOT = pathlib.Path(__file__).resolve().parents[2] REPO = 'FS-GG/FS.GG.Game' -# This adapter is selected only for the coherent current 0.4 release. Historical +# This adapter is selected only for the coherent current 0.6 release. Historical # consumed transactions remain in immutable history; no executor overlay is admitted. P = subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD'],text=True,timeout=10).strip() TREE = subprocess.check_output(['git','-C',str(ROOT),'rev-parse','HEAD^{tree}'],text=True,timeout=10).strip() @@ -95,7 +95,7 @@ def manifest_tool(): def publication_identity(subject): need(type(subject) is PublicationSubject, 'explicit publication subject required') sha(subject.producer, 40); sha(subject.tree, 40) - need(subject.version == VERSION == '0.5.0', 'wrong published subject version') + need(subject.version == VERSION == '0.6.0', 'wrong published subject version') return subject.producer, subject.tree @@ -133,7 +133,7 @@ def validate_binding(binding, *, subject=None): 'workflowSha','job','event','evidence'} need(set(binding) == fields, 'unreviewed preparation fields') need(binding['schema'] == SCHEMA and binding['repository'] == REPO and binding['workflow'] == '.github/workflows/wasm-shared.yml' and binding['mode'] == 'qualify', 'unrelated preparation route') - need(VERSION == '0.5.0' and binding['producer'] == binding['executor'] == binding['workflowSha'] == producer and binding['producerTree'] == binding['executorTree'] == tree and binding['version'] == VERSION, 'wrong selected producer/executor/tree/version') + need(VERSION == '0.6.0' and binding['producer'] == binding['executor'] == binding['workflowSha'] == producer and binding['producerTree'] == binding['executorTree'] == tree and binding['version'] == VERSION, 'wrong selected producer/executor/tree/version') need(binding['job'] == 'qualify' and binding['event'] in ('push','workflow_dispatch'), 'premerge/generation-only custody refuses') need(binding['runAttempt'] == 1, 'rerun custody refuses') need(set(binding['evidence']) == OUTER_FILES - {'preparation-binding.json'} - set(FILES), 'wrong closed evidence roster') @@ -641,7 +641,7 @@ def custody_plan(): def freeze(custody,output): custody_plan() - need(VERSION=='0.5.0', 'only selected current release can freeze') + need(VERSION=='0.6.0', 'only selected current release can freeze') need(os.environ['GITHUB_REPOSITORY']==REPO and os.environ['GITHUB_REF']=='refs/heads/main' and os.environ['GITHUB_SHA']==os.environ['GITHUB_WORKFLOW_SHA']==P, 'premerge/source/workflow substitution') event=os.environ['GITHUB_EVENT_NAME'] need(event=='push' or (event=='workflow_dispatch' and os.environ.get('WASM_QUALIFY_EXPECTED_HEAD')==P), 'qualification dispatch needs selected protected head') diff --git a/scripts/wasm-release/qualify-supervisor-installed.sh b/scripts/wasm-release/qualify-supervisor-installed.sh index 9c13e705..b9606743 100755 --- a/scripts/wasm-release/qualify-supervisor-installed.sh +++ b/scripts/wasm-release/qualify-supervisor-installed.sh @@ -12,7 +12,7 @@ feed="${WASM_PACKAGE_FEED:?}" receipts="${RUNNER_TEMP:?}/wasm-installed-receipts" [[ -f "$receipts/release-manifest.json" && -f "$receipts/promotion-binding.json" ]] || { echo 'requires genuine binder-downloaded immutable public receipts' >&2; exit 2; } version="$(sed -n 's:.*\([^<]*\).*:\1:p' "$repo/eng/wasm-shared/version.props")" -[[ "$version" == 0.5.0 && "${RELEASE_VERSION:?}" == "$version" ]] || { echo 'requires selected current installed version' >&2; exit 2; } +[[ "$version" == 0.6.0 && "${RELEASE_VERSION:?}" == "$version" ]] || { echo 'requires selected current installed version' >&2; exit 2; } q="${output}-baseline" "$repo/scripts/wasm-release/qualify-supervisor-custody.sh" --bootstrap "$q" config="${output}-NuGet.Config" diff --git a/scripts/wasm-release/release_manifest.py b/scripts/wasm-release/release_manifest.py index a42a9e8e..0834d2ec 100755 --- a/scripts/wasm-release/release_manifest.py +++ b/scripts/wasm-release/release_manifest.py @@ -18,7 +18,7 @@ API_POLICY = json.loads((Path(__file__).parent / "api-baseline-policy.json").read_text()) # Explicit historical policies verify immutable evidence; they select no current execution. HISTORICAL_API_POLICIES = [json.loads((Path(__file__).parent / name).read_text()) for name in - ("api-baseline-policy-0.2.0.json", "api-baseline-policy-0.3.0.json", "api-baseline-policy-0.4.0.json")] + ("api-baseline-policy-0.2.0.json", "api-baseline-policy-0.3.0.json", "api-baseline-policy-0.4.0.json", "api-baseline-policy-0.5.0.json")] def api_policy(version): if version == API_POLICY["candidateVersion"]: diff --git a/scripts/wasm-release/worker-bundle/package-lock.json b/scripts/wasm-release/worker-bundle/package-lock.json index 6e4e460b..13244dcb 100644 --- a/scripts/wasm-release/worker-bundle/package-lock.json +++ b/scripts/wasm-release/worker-bundle/package-lock.json @@ -1,12 +1,12 @@ { "name": "fsgg-wasm-worker-build", - "version": "0.5.0", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "fsgg-wasm-worker-build", - "version": "0.5.0", + "version": "0.6.0", "devDependencies": { "acorn": "8.19.0", "esbuild": "0.28.2", diff --git a/scripts/wasm-release/worker-bundle/package.json b/scripts/wasm-release/worker-bundle/package.json index 92fc904d..ffdd34c5 100644 --- a/scripts/wasm-release/worker-bundle/package.json +++ b/scripts/wasm-release/worker-bundle/package.json @@ -2,7 +2,7 @@ "name": "fsgg-wasm-worker-build", "private": true, "type": "module", - "version": "0.5.0", + "version": "0.6.0", "devDependencies": { "acorn": "8.19.0", "esbuild": "0.28.2", diff --git a/sdk/wasm/VERSION b/sdk/wasm/VERSION index 8f0916f7..a918a2aa 100644 --- a/sdk/wasm/VERSION +++ b/sdk/wasm/VERSION @@ -1 +1 @@ -0.5.0 +0.6.0 diff --git a/sdk/wasm/rust/Cargo.lock b/sdk/wasm/rust/Cargo.lock index 82dd79f0..9c3e5bb9 100644 --- a/sdk/wasm/rust/Cargo.lock +++ b/sdk/wasm/rust/Cargo.lock @@ -3,4 +3,4 @@ version = 4 [[package]] name = "fsgg-wasm-guest" -version = "0.5.0" +version = "0.6.0" diff --git a/sdk/wasm/rust/fsgg-wasm-guest/Cargo.toml b/sdk/wasm/rust/fsgg-wasm-guest/Cargo.toml index 7c5f17f2..5dea4179 100644 --- a/sdk/wasm/rust/fsgg-wasm-guest/Cargo.toml +++ b/sdk/wasm/rust/fsgg-wasm-guest/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fsgg-wasm-guest" -version = "0.5.0" +version = "0.6.0" edition = "2021" license = "MIT" publish = false diff --git a/src/Wasm.Browser/Admission.fs b/src/Wasm.Browser/Admission.fs index 819b7dd5..7b727175 100644 --- a/src/Wasm.Browser/Admission.fs +++ b/src/Wasm.Browser/Admission.fs @@ -326,7 +326,7 @@ module private Parser = | Error e -> issue <- Some e | Ok opcode when (opcode >= 0x06uy && opcode <= 0x0auy) || opcode=0x12uy || opcode=0x13uy || opcode=0x1cuy || opcode>=0xd0uy || opcode=0xfbuy || opcode=0xfcuy || opcode=0xfduy || opcode=0xfeuy -> issue <- Some(UnsupportedFeature($"instruction opcode 0x{opcode:x2}")) - | Ok opcode when [0x0cuy;0x0duy;0x10uy;0x20uy;0x21uy;0x22uy;0x23uy;0x24uy] |> List.contains opcode -> + | Ok (0x0cuy | 0x0duy | 0x10uy | 0x20uy | 0x21uy | 0x22uy | 0x23uy | 0x24uy) -> match u32 body with Error e -> issue <- Some e | Ok _ -> () | Ok 0x0euy -> match vectorCount body "branch table" with diff --git a/src/Wasm.Browser/README.md b/src/Wasm.Browser/README.md index ec37240b..d32cf6f8 100644 --- a/src/Wasm.Browser/README.md +++ b/src/Wasm.Browser/README.md @@ -1,9 +1,11 @@ # FS.GG.Wasm.Browser -The selected `0.5.0` source candidate prepares a closed module Worker from the -generated F# policy. Published `0.4.0` and its accepted installed evidence retain -their original identities; the new candidate still requires qualification and -coherent publication. +The selected `0.6.0` source candidate preserves the closed module Worker from the +generated F# policy and replaces the instruction walker's repeated eight-opcode +list allocation with equivalent pattern matching. Published `0.5.0` and its +accepted installed evidence retain their original identities. The new candidate +still requires actual qualification and coherent publication; this source change +does not establish an earlier timeout's cause or future deadline fit. The build-transitive package boundary copies those assets under `_content/FS.GG.Wasm.Browser/`; `worker-client.mjs` resolves its sibling Worker from the deployed asset base, including non-root applications. F# retains admission, diff --git a/src/Wasm.Contracts/compatibility-profile.v1.json b/src/Wasm.Contracts/compatibility-profile.v1.json index f2e20c5b..5e1ae97a 100644 --- a/src/Wasm.Contracts/compatibility-profile.v1.json +++ b/src/Wasm.Contracts/compatibility-profile.v1.json @@ -1,7 +1,7 @@ { "schema": "fsgg.wasm.compatibility-profile/v1", "packageId": "FS.GG.Wasm.Contracts", - "sourceVersion": "0.5.0", + "sourceVersion": "0.6.0", "published": false, "abiProfiles": [ { "id": "bar-abi-1", "version": 1, "prefix": "barc", "memory": "memory" }, diff --git a/tests/Wasm.Invocation.Tests/Program.fs b/tests/Wasm.Invocation.Tests/Program.fs index 348c27d3..299b7b32 100644 --- a/tests/Wasm.Invocation.Tests/Program.fs +++ b/tests/Wasm.Invocation.Tests/Program.fs @@ -105,6 +105,77 @@ let markerOffset = renamedExport[markerOffset + marker.Length - 1] <- byte 'x' check (Admission.inspect cfg renamedExport |> Result.isError) "exact exports must refuse renamed ABI" +// Public test-only rewrite of the first body in the existing owned fixture. +// This checks Admission syntax independently; native validity remains a second gate. +let uleb (number: int) = + let mutable value = uint32 number + let result = ResizeArray() + while value >= 128u do + result.Add(byte (value &&& 127u) ||| 128uy) + value <- value >>> 7 + result.Add(byte value) + result.ToArray() + +let readUleb (data: byte array) (position: int ref) = + let mutable value = 0 + let mutable shift = 0 + let mutable more = true + while more do + let part = data[position.Value] + position.Value <- position.Value + 1 + value <- value ||| ((int part &&& 127) <<< shift) + shift <- shift + 7 + more <- part &&& 128uy <> 0uy + value + +let rewriteFirstBody (change: byte array -> byte array) = + let position = ref 8 + let mutable changed = None + while position.Value < barBytes.Length && changed.IsNone do + let sectionStart = position.Value + let kind = barBytes[position.Value] + position.Value <- position.Value + 1 + let length = readUleb barBytes position + let payloadStart = position.Value + let finish = payloadStart + length + if kind = 10uy then + let count = readUleb barBytes position + check (count > 0) "owned fixture must contain functions" + let sizeStart = position.Value + let bodySize = readUleb barBytes position + let bodyStart = position.Value + let body = change barBytes[bodyStart .. bodyStart + bodySize - 1] + let payload = Array.concat [barBytes[payloadStart .. sizeStart - 1]; uleb body.Length; body; barBytes[bodyStart + bodySize .. finish - 1]] + let newHeader = Array.concat [[|kind|]; uleb payload.Length] + let newBodyStart = sectionStart + newHeader.Length + (sizeStart - payloadStart) + (uleb body.Length).Length + changed <- Some(Array.concat [barBytes[0 .. sectionStart - 1]; newHeader; payload; barBytes[finish ..]], newBodyStart) + position.Value <- finish + changed |> Option.defaultWith (fun () -> failwith "owned fixture has no code section") + +let dense, _ = rewriteFirstBody (fun body -> + let locals = ref 0 + let groups = readUleb body locals + for _ in 1..groups do + readUleb body locals |> ignore + locals.Value <- locals.Value + 1 + Array.concat [body[0 .. locals.Value - 1]; Array.create 65536 0x01uy; body[locals.Value ..]]) +check (Admission.inspect cfg dense |> Result.isOk) "public instruction-dense body remains admissible" + +// Each selected opcode consumes exactly one u32, preserving first-error offsets. +for opcode in [0x0cuy;0x0duy;0x10uy;0x20uy;0x21uy;0x22uy;0x23uy;0x24uy] do + for immediate in [[|0uy|]; [|0x80uy;1uy|]; [|0xffuy;0xffuy;0xffuy;0xffuy;0x0fuy|]] do + let syntax, _ = rewriteFirstBody (fun _ -> Array.concat [[|0uy;opcode|]; immediate; [|0x0buy|]]) + check (Admission.inspect cfg syntax |> Result.isOk) (sprintf "opcode 0x%x consumes u32 syntax" opcode) + for immediate, relative, detail in + [ [|0x80uy|], 3, "unexpected end of input" + [|0x80uy;0x80uy;0x80uy;0x80uy;0x10uy|], 6, "u32 LEB overflow" + [|0x80uy;0x80uy;0x80uy;0x80uy;0x80uy|], 7, "unterminated u32 LEB" ] do + let malformed, bodyStart = rewriteFirstBody (fun _ -> Array.concat [[|0uy;opcode|]; immediate]) + match Admission.inspect cfg malformed with + | Error [MalformedModule(offset, actual)] -> + check (offset = bodyStart + relative && actual = detail) (sprintf "opcode 0x%x retains first error offset/detail: %d %s" opcode offset actual) + | other -> failwithf "expected exact immediate refusal, got %A" other + let mutable mechanicsCalled = false let mechanics = { Sha256Hex = fun _ -> mechanicsCalled <- true; async.Return(Ok(String.replicate 64 "a")) diff --git a/tests/Wasm.Lifecycle.Tests/packages.lock.json b/tests/Wasm.Lifecycle.Tests/packages.lock.json index f0b64e23..11b2dae3 100644 --- a/tests/Wasm.Lifecycle.Tests/packages.lock.json +++ b/tests/Wasm.Lifecycle.Tests/packages.lock.json @@ -112,7 +112,7 @@ "fs.gg.wasm.browser": { "type": "Project", "dependencies": { - "FS.GG.Wasm.Contracts": "[0.5.0, )", + "FS.GG.Wasm.Contracts": "[0.6.0, )", "FSharp.Core": "[10.1.302, )" } }, diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs index df669980..0ac890b6 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.mjs @@ -58,8 +58,31 @@ async function session(artifact, prefix, outputCap = 65536, deadline = 250, inje // One declared history: fresh initial Worker, candidate abort, then a fresh // same-name candidate. No priming, retry, synthetic clock or renewed budget. -window.runObservedFreshWorkers = async () => { - const trace=[],artifact=await bytes("control-ordinary.wasm"),current=await session(artifact,"sc2c",65536,250,false,row=>trace.push(row)); +// Own public control module plus 65,536 genuine nop instructions in its first +// function. No private guest, clock, synthetic result or priming Worker is used. +function instructionDense(artifact) { + const leb=value=>{const out=[];do{const part=value&127;value>>>=7;out.push(part|(value?128:0));}while(value);return out;}; + const read=cursor=>{let value=0,shift=0,part;do{part=artifact[cursor.offset++];value|=(part&127)< { + const trace=[],original=await bytes("control-ordinary.wasm"),artifact=dense?instructionDense(original):original,current=await session(artifact,"sc2c",65536,250,false,row=>trace.push(row)); let result={fixtureFailure:null}; try{ result.loaded=await current.loadBounded(); @@ -76,7 +99,7 @@ window.runObservedFreshWorkers = async () => { } }catch(error){result.fixtureFailure=String(error).slice(0,256);} finally{current.dispose();} - return {...result,trace,status:current.transport.DiagnosticStatus(),retired:current.terminations,final:current.projection()}; + return {...result,artifactBytes:artifact.length,trace,status:current.transport.DiagnosticStatus(),retired:current.terminations,final:current.projection()}; }; window.runObservedColdExpiry = async () => { diff --git a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs index be4d1310..e51f70c5 100644 --- a/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs +++ b/tests/Wasm.PackageConsumer/browser/package-consumer.spec.mjs @@ -113,6 +113,22 @@ test("closed installed Worker starts fresh initially and after candidate abort u }finally{page.context().off("request",onRequest);} }); +test("public instruction-dense module admits in fresh installed Workers under the original budget",async({page})=>{ + const value=await page.evaluate(()=>window.runObservedFreshWorkers(true)); + const states=[value.loaded?.State,value.initialized?.State,value.first?.State,value.second?.State]; + console.log(JSON.stringify({stage:"public dense admission",artifactBytes:value.artifactBytes,states,status:value.status,trace:value.trace})); + expect(value.fixtureFailure).toBeNull(); + expect(value.artifactBytes).toBeGreaterThan(65536); + expect(states).toEqual(["succeeded","succeeded","succeeded","succeeded"]); + expect(value.status).toMatchObject({incomplete:false,truncated:false,readFailed:false}); + for(const physical of [1,2,3]){ + const stages=value.trace.filter(row=>row.stage==="worker-stage"&&row.physical===physical).map(row=>row.workerStage); + expect(stages.slice(0,9)).toEqual(["handler-ready","load-received","admission-begin","admission-end","sha256-begin","sha256-end","instantiate-begin","instantiate-end","load-completed"]); + } + expect(value.final).toMatchObject({Disposed:true,ActiveWorker:"",CandidateWorker:""}); + await expect.poll(()=>page.workers().length).toBe(0); +}); + test("observed cold expiry keeps its one-millisecond pre-create deadline and retirement",async({page})=>{ const value=await page.evaluate(()=>window.runObservedColdExpiry()); console.log(JSON.stringify({stage:"observed cold expiry",state:value.loaded.State,status:value.status,trace:value.trace})); diff --git a/tests/release/wasm/test-api-baseline-policy.py b/tests/release/wasm/test-api-baseline-policy.py index f53538d2..4b39ac53 100644 --- a/tests/release/wasm/test-api-baseline-policy.py +++ b/tests/release/wasm/test-api-baseline-policy.py @@ -71,10 +71,30 @@ def module(name, file): else: raise SystemExit("additive successor accepted unavailable/breaking native verdict") assert manifest.api_policy(current_policy["candidateVersion"]) == current_policy assert current_policy["candidateVersion"] == ET.parse(ROOT / "eng/wasm-shared/version.props").findtext("./PropertyGroup/WasmSharedVersion") -assert current_policy["baselineVersion"] == manifest.api_policy("0.4.0")["candidateVersion"] +assert current_policy["baselineVersion"] == manifest.api_policy("0.5.0")["candidateVersion"] +assert manifest.api_policy("0.5.0")["baselineVersion"] == "0.4.0" assert manifest.api_policy("0.4.0")["baselineVersion"] == "0.3.0" assert manifest.api_policy("0.3.0")["baselineVersion"] == "0.2.0" assert manifest.api_policy("0.2.0")["baselineVersion"] == "0.1.1" +# Exercise both exact current and immutable predecessor policy readers through +# the actual receipt validator, including changed valid-looking source/hash data. +for selected in (current_policy, manifest.api_policy("0.5.0")): + selected_rows = [{"id": identity, "baselineArchiveSha256": expected["servedArchiveSha256"], + "candidateArchiveSha256": "e" * 64, "baselineApiSurface": {"api-surface/test.fsi": "f" * 64}, + "candidateApiSurface": {"api-surface/test.fsi": "f" * 64}, "nativeLogSha256": "a" * 64, + "status": "compatible", "diagnostics": []} for identity, expected in selected["packages"].items()] + current_receipt = {"schema": "fsgg.wasm.published-api-comparison/v1", "tool": "SDK10.0.401.ApiCompat", + "packages": selected_rows, **{key: selected[key] for key in + ("baselineVersion", "baselineSource", "baselineTree", "baselineManifestSha256", "candidateVersion")}} + current_artifacts = [{"file": f"{identity}.{selected['candidateVersion']}.nupkg", "sha256": "e" * 64} for identity in selected["packages"]] + manifest.verify_api_comparison(current_receipt, current_artifacts) + for field in ("baselineSource", "baselineTree", "baselineManifestSha256", "served-bytes"): + broken = copy.deepcopy(current_receipt) + if field == "served-bytes": broken["packages"][0]["baselineArchiveSha256"] = "0" * 64 + else: broken[field] = "0" * len(broken[field]) + try: manifest.verify_api_comparison(broken, current_artifacts) + except SystemExit: pass + else: raise SystemExit("changed current/predecessor baseline identity admitted: " + field) try: manifest.api_policy("0.2.1") except SystemExit: pass else: raise SystemExit("unselected patch version accepted") diff --git a/tests/release/wasm/test-release-promotion.py b/tests/release/wasm/test-release-promotion.py index 2b65aaf5..3a164003 100644 --- a/tests/release/wasm/test-release-promotion.py +++ b/tests/release/wasm/test-release-promotion.py @@ -84,8 +84,8 @@ class Occupancy: def __init__(self,mutant=None): self.mutant=mutant; self.writes=[] def pages(self,path,key=None): if self.mutant=='denied': raise p.Refusal('403 Unknown') - if self.mutant=='deleted' and 'state=deleted' in path: return [{'name':'0.5.0','id':1}] - if self.mutant=='active' and 'state=active' in path: return [{'name':'0.5.0','id':1}] + if self.mutant=='deleted' and 'state=deleted' in path: return [{'name':'0.6.0','id':1}] + if self.mutant=='active' and 'state=active' in path: return [{'name':'0.6.0','id':1}] if self.mutant=='malformed-org': return [{'id':1}] return [{'name':'0.2.0','id':1}] def request(self,url,**kwargs): @@ -94,7 +94,7 @@ def request(self,url,**kwargs): if url.endswith('/index.json'): if self.mutant=='missing-index': return 404,None,{} if self.mutant=='malformed-index': return 200,{'versions':[{}]},{} - return 200,{'versions':['0.1.1','0.2.0']+(['0.5.0'] if self.mutant=='unlisted' else [])},{} + return 200,{'versions':['0.1.1','0.2.0']+(['0.6.0'] if self.mutant=='unlisted' else [])},{} return (200 if self.mutant in ('contradiction','unlisted') else 404),b'',{} class Tests(unittest.TestCase): @@ -424,20 +424,20 @@ def test_installed_cache_member_lock_worker_substitution(self): root=pathlib.Path(raw);cache=root/'packages';record=root/'promotion-binding.json';lock=root/'packages.lock.json' r=reviewed();dependencies={} for ident in p.PACKAGES: - folder=cache/ident.lower()/'0.5.0';folder.mkdir(parents=True) - archive=folder/(ident.lower()+'.0.5.0.nupkg') + folder=cache/ident.lower()/'0.6.0';folder.mkdir(parents=True) + archive=folder/(ident.lower()+'.0.6.0.nupkg') with zipfile.ZipFile(archive,'w') as z:z.writestr('ordinary.dll',b'real-payload');z.writestr('module-worker.mjs',b'real-worker') - r['binding']['archives'][ident+'.0.5.0.nupkg']=p.digest(archive.read_bytes()) - r['binding']['inventories'][ident+'.0.5.0.nupkg']={'ordinary.dll':p.digest(b'real-payload'),'module-worker.mjs':p.digest(b'real-worker')} + r['binding']['archives'][ident+'.0.6.0.nupkg']=p.digest(archive.read_bytes()) + r['binding']['inventories'][ident+'.0.6.0.nupkg']={'ordinary.dll':p.digest(b'real-payload'),'module-worker.mjs':p.digest(b'real-worker')} content=base64.b64encode(hashlib.sha512(archive.read_bytes()).digest()).decode() - (folder/(ident.lower()+'.0.5.0.nupkg.sha512')).write_text(content) + (folder/(ident.lower()+'.0.6.0.nupkg.sha512')).write_text(content) (folder/'.nupkg.metadata').write_text(json.dumps({'version':2,'contentHash':content,'source':'https://nuget.pkg.github.com/FS-GG/index.json'})) - dependencies[ident]={'resolved':'0.5.0','contentHash':content} + dependencies[ident]={'resolved':'0.6.0','contentHash':content} record.write_text(json.dumps({'preparation':r}));lock.write_text(json.dumps({'dependencies':{'net10.0':dependencies}})) with patch.dict(os.environ,{'ACCEPTED_EXECUTOR':p.P}): p.check_installed(record,cache,'https://nuget.pkg.github.com/FS-GG/index.json',lock) - ident=p.PACKAGES[1];folder=cache/ident.lower()/'0.5.0';archive=folder/(ident.lower()+'.0.5.0.nupkg');original=archive.read_bytes() - sidecar=folder/(ident.lower()+'.0.5.0.nupkg.sha512');metadata=folder/'.nupkg.metadata';original_sidecar=sidecar.read_text();original_metadata=metadata.read_text() + ident=p.PACKAGES[1];folder=cache/ident.lower()/'0.6.0';archive=folder/(ident.lower()+'.0.6.0.nupkg');original=archive.read_bytes() + sidecar=folder/(ident.lower()+'.0.6.0.nupkg.sha512');metadata=folder/'.nupkg.metadata';original_sidecar=sidecar.read_text();original_metadata=metadata.read_text() for mutation in ('worker','ordinary','signature-disguised-worker','lock','version','missing-cache'): saved=copy.deepcopy(dependencies) if mutation in ('worker','ordinary','signature-disguised-worker'): @@ -553,17 +553,17 @@ def test_installed_signed_hash_roles_are_independent_and_payload_locked(self): with tempfile.TemporaryDirectory() as raw,patch.dict(os.environ,{'ACCEPTED_EXECUTOR':p.P}): root=pathlib.Path(raw);cache=root/'packages';record=root/'promotion-binding.json';lock=root/'packages.lock.json';r=reviewed();deps={};originals={} for ident in p.PACKAGES: - folder=cache/ident.lower()/'0.5.0';folder.mkdir(parents=True);archive=folder/(ident.lower()+'.0.5.0.nupkg') + folder=cache/ident.lower()/'0.6.0';folder.mkdir(parents=True);archive=folder/(ident.lower()+'.0.6.0.nupkg') unsigned=self.package_fixture();signed=self.package_fixture(1);archive.write_bytes(signed);originals[ident]=signed - r['binding']['archives'][ident+'.0.5.0.nupkg']=p.digest(unsigned) - with p.zipfile.ZipFile(io.BytesIO(unsigned))as z:r['binding']['inventories'][ident+'.0.5.0.nupkg']={name:p.digest(z.read(name))for name in z.namelist()} - content=base64.b64encode(hashlib.sha512(unsigned).digest()).decode();self.assertNotEqual(base64.b64encode(hashlib.sha512(signed).digest()).decode(),content);deps[ident]={'resolved':'0.5.0','contentHash':content} - (folder/(ident.lower()+'.0.5.0.nupkg.sha512')).write_text(base64.b64encode(hashlib.sha512(signed).digest()).decode()) + r['binding']['archives'][ident+'.0.6.0.nupkg']=p.digest(unsigned) + with p.zipfile.ZipFile(io.BytesIO(unsigned))as z:r['binding']['inventories'][ident+'.0.6.0.nupkg']={name:p.digest(z.read(name))for name in z.namelist()} + content=base64.b64encode(hashlib.sha512(unsigned).digest()).decode();self.assertNotEqual(base64.b64encode(hashlib.sha512(signed).digest()).decode(),content);deps[ident]={'resolved':'0.6.0','contentHash':content} + (folder/(ident.lower()+'.0.6.0.nupkg.sha512')).write_text(base64.b64encode(hashlib.sha512(signed).digest()).decode()) (folder/'.nupkg.metadata').write_text(json.dumps({'version':2,'contentHash':content,'source':feed})) record.write_text(json.dumps({'preparation':r}));lock.write_text(json.dumps({'dependencies':{'net10.0':deps}})) with patch.object(p,'repository_signature',return_value='source-controlled verification fixture')as verify: p.check_installed(record,cache,feed,lock);self.assertEqual(verify.call_count,2) - ident=p.PACKAGES[0];folder=cache/ident.lower()/'0.5.0';archive=folder/(ident.lower()+'.0.5.0.nupkg');sidecar=folder/(ident.lower()+'.0.5.0.nupkg.sha512');metadata=folder/'.nupkg.metadata' + ident=p.PACKAGES[0];folder=cache/ident.lower()/'0.6.0';archive=folder/(ident.lower()+'.0.6.0.nupkg');sidecar=folder/(ident.lower()+'.0.6.0.nupkg.sha512');metadata=folder/'.nupkg.metadata' baseline_sidecar=sidecar.read_text();baseline_metadata=metadata.read_text();baseline_deps=copy.deepcopy(deps) for mutation in ('sidecar','metadata-hash','metadata-source','metadata-version','metadata-malformed','metadata-missing','metadata-duplicate','lock','unsigned-public','ordinary','payload','corrupt-compression','cache-drift','signature-trust','foreign-feed'): if mutation=='sidecar':sidecar.write_text(deps[ident]['contentHash']) @@ -651,7 +651,7 @@ def pages(self,*args,**kwargs):return jobs def test_foreign_occupied_second_member_blocks_first_writer(self): class Transport(Occupancy): def pages(self,path,key=None): - return [{'name':'0.5.0','id':1}] if 'browser' in path and 'state=active' in path else [] + return [{'name':'0.6.0','id':1}] if 'browser' in path and 'state=active' in path else [] calls=[] def reject(args,**kwargs): calls.append(args[0]) @@ -669,11 +669,11 @@ def test_recovery_begin_checks_occupied_payload_before_any_writer(self): class Transport(Occupancy): def pages(self,path,key=None): if outcome=='unknown':raise p.Refusal('403 occupancy Unknown') - return [{'name':'0.5.0','id':1}] if feed=='org' and 'browser' in path and 'state=active' in path else [] + return [{'name':'0.6.0','id':1}] if feed=='org' and 'browser' in path and 'state=active' in path else [] def request(self,url,**kwargs): if feed!='public' or url.endswith('/v3/index.json'):return super().request(url,**kwargs) exists='browser' in url - return (200,{'versions':['0.2.0']+(['0.5.0'] if exists else [])},{}) if url.endswith('/index.json') else (200 if exists else 404,b'',{}) + return (200,{'versions':['0.2.0']+(['0.6.0'] if exists else [])},{}) if url.endswith('/index.json') else (200 if exists else 404,b'',{}) def checked(self,path,method='GET',data=None): if method=='POST':events.append(('writer','draft')) return {'id':1,'draft':True,'assets':[]} @@ -696,7 +696,7 @@ def test_recovery_admission_requires_occupied_payload_readback(self): class Transport(Occupancy): def pages(self,path,key=None): if outcome=='unknown':raise p.Refusal('403 occupancy Unknown') - return [{'name':'0.5.0','id':1}] if 'browser' in path and 'state=active' in path else [] + return [{'name':'0.6.0','id':1}] if 'browser' in path and 'state=active' in path else [] def cmd(args,**kwargs): self.assertTrue(str(args[0]).endswith('readback.sh')) events.append(('readback',args[1],args[-1])) @@ -741,48 +741,15 @@ def test_actual_clean_executor_and_finite_glue_delta(self): with self.assertRaises(AssertionError):binder.bind_executor(binder.PUBLISHED,executor,version) with patch.dict(os.environ,{'EXECUTION_BINDING':'{"schema":"historical"}'}),self.assertRaises(p.Refusal):p.execution_input() - def test_installed_binder_authenticates_original_publication_and_current_qualifier(self): + def test_historical_installed_binder_refuses_the_unpublished_current_successor(self): spec=importlib.util.spec_from_file_location('installed_binding',ROOT/'scripts/wasm-release/bind-installed-release.py') binder=importlib.util.module_from_spec(spec);spec.loader.exec_module(binder) - producer=binder.PUBLISHED;tree=binder.PUBLISHED_TREE;qualifier='9'*40 - with patch.object(p,'P',producer),patch.object(p,'TREE',tree): - r=reviewed();run,jobs,artifact=native_data() - journal={'schema':'fsgg.wasm.promotion/v1','preparation':r,'firstPromotionRun':201,'firstPromotionAttempt':1,'producer':producer,'tag':'wasm/v0.5.0'} - values={'PUBLISHED_SOURCE':producer,'QUALIFIER_SOURCE':qualifier,'ACCEPTED_EXECUTOR':qualifier,'RELEASE_VERSION':'0.5.0'} - class Transport: - def __init__(self,mutation=None): - self.mutation=mutation;self.journal=copy.deepcopy(journal) - if mutation=='journal-producer':self.journal['producer']=qualifier - body=json.dumps(self.journal).encode();self.body=body - self.asset={'id':401,'name':'promotion-binding.json','url':f'https://api.github.com/repos/{p.REPO}/releases/assets/401','size':len(body),'digest':'sha256:'+p.digest(body)} - self.release={'id':300,'tag_name':'wasm/v0.5.0','target_commitish':qualifier if mutation=='release-source' else producer,'draft':False,'prerelease':False,'assets':[self.asset]} - def checked(self,path): - if path.endswith('/git/ref/heads/main'):return {'object':{'sha':'8'*40 if self.mutation=='main' else qualifier}} - if path.endswith('/releases/300'):return self.release - if path.endswith('/artifacts/200'):return artifact - if '/runs/100/' in path: - result=copy.deepcopy(run) - if self.mutation=='qualification-failed':result['conclusion']='failure' - return result - if '/runs/201/' in path:return {'id':201,'head_sha':producer,'repository':{'full_name':p.REPO},'event':'workflow_dispatch','path':'.github/workflows/release-wasm.yml','run_attempt':1,'status':'completed','conclusion':'failure' if self.mutation=='publication-failed' else 'success'} - raise AssertionError('unexpected native read '+path) - def pages(self,path,key=None): - if '/runs/100/' in path:return jobs - if '/runs/201/' in path:return [{'name':name,'conclusion':'success'} for name in ('preflight','admission','begin','org','public','assets','complete')] - if path.endswith('/assets'):return [self.asset] - if path.endswith('/releases'):return [self.release] - raise AssertionError('unexpected native page '+path) - def api(self,path):return (200,{'object':{'type':'commit','sha':producer}}, {}) if '/git/ref/tags/' in path else (200,self.release,{}) - def request(self,url,**kwargs):return 200,self.body,{} - for mutation in (None,'main','journal-producer','release-source','qualification-failed','publication-failed'): - native=Transport(mutation) - with self.subTest(mutation=mutation),tempfile.TemporaryDirectory() as raw,patch.object(p,'Native',return_value=native),patch.object(p,'verify_admission') as original_admission: - if mutation: - with self.assertRaises((AssertionError,p.Refusal)):binder.bind_promotion_execution(p,journal,values,native.release,pathlib.Path(raw)) - else: - self.assertEqual(binder.bind_promotion_execution(p,journal,values,native.release,pathlib.Path(raw)),r) - original_admission.assert_called_once_with(native,r,201,1) - self.assertTrue((pathlib.Path(raw)/'published-native.json').exists()) + # The finite installed binder still selects immutable published 0.5. + # A later publication-specific Q must be reviewed before 0.6 can run. + self.assertEqual(binder.PUBLISHED,'80540027623d9b1a8aa7715d0591de48455403b7') + with self.assertRaises(AssertionError): + binder.bind_executor(binder.PUBLISHED,'9'*40,p.VERSION) + def test_explicit_read_only_publication_subject_does_not_relax_writer(self): original=reviewed(); producer=p.P; tree=p.TREE @@ -815,12 +782,12 @@ def checked(self,path,method='GET',data=None): return release def pages(self,path,key=None): ident=p.PACKAGES[0] if 'contracts' in path else p.PACKAGES[1] - return [{'name':'0.5.0','id':1}] if 'state=active' in path and state.get(('org',ident)) else [] + return [{'name':'0.6.0','id':1}] if 'state=active' in path and state.get(('org',ident)) else [] def request(self,url,**kw): if url.endswith('/v3/index.json'):return super().request(url,**kw) ident=p.PACKAGES[0] if 'contracts' in url else p.PACKAGES[1] exists=state.get(('public',ident)) - return (200,{'versions':['0.2.0']+(['0.5.0'] if exists else [])},{}) if url.endswith('/index.json') else (200 if exists else 404,b'',{}) + return (200,{'versions':['0.2.0']+(['0.6.0'] if exists else [])},{}) if url.endswith('/index.json') else (200 if exists else 404,b'',{}) def rb(kind,custody,output): if kind in ('org','public'): if not all(state.get((kind,x)) for x in p.PACKAGES):raise p.Refusal('pair absent') @@ -856,7 +823,7 @@ def cmd(args,**kw): self.assertEqual(events.count('public-contracts'),1);self.assertEqual(events.count('public-browser'),1) def test_draft_resolver_actual_transport_pagination_and_unknowns(self): - release={'id':402763234,'tag_name':'wasm/v0.5.0','target_commitish':p.P,'draft':True,'prerelease':False,'assets':[]} + release={'id':402763234,'tag_name':'wasm/v0.6.0','target_commitish':p.P,'draft':True,'prerelease':False,'assets':[]} class Transport(p.Native): def __init__(self,mutation=None):super().__init__();self.mutation=mutation;self.calls=[] def api(self,path,method='GET',data=None): @@ -910,8 +877,8 @@ def test_asset_shell_callers_use_actual_shared_resolver(self): # are covered separately; no product or provider process runs here. with tempfile.TemporaryDirectory(prefix='wasm-asset-caller-control.')as raw: root=pathlib.Path(raw);tools=root/'bin';tools.mkdir();custody=root/'custody';custody.mkdir() - for name in ('fsgg-wasm-sdk-0.5.0.tar.gz','release-manifest.json','SHA256SUMS'):(custody/name).write_bytes(name.encode()) - (custody/'release-manifest.json').write_text(json.dumps({'version':'0.5.0'})) + for name in ('fsgg-wasm-sdk-0.6.0.tar.gz','release-manifest.json','SHA256SUMS'):(custody/name).write_bytes(name.encode()) + (custody/'release-manifest.json').write_text(json.dumps({'version':'0.6.0'})) (root/'reviewed.json').write_text(json.dumps(reviewed())) wrapper=tools/'python3';wrapper.write_text('#!'+sys.executable+'\n'+'''import importlib.util,sys,os,json,pathlib,hashlib,io,zipfile sys.dont_write_bytecode=True @@ -921,14 +888,14 @@ def test_asset_shell_callers_use_actual_shared_resolver(self): sys.exit(subprocess.call([sys.executable]+args)) s=importlib.util.spec_from_file_location('promotion',args[0]);p=importlib.util.module_from_spec(s);s.loader.exec_module(p) root=pathlib.Path(os.environ['FIXTURE_ROOT']);c=root/'custody' -r=json.loads((root/'reviewed.json').read_text());journal={'schema':'fsgg.wasm.promotion/v1','preparation':r,'firstPromotionRun':200,'firstPromotionAttempt':1,'producer':p.P,'tag':'wasm/v0.5.0'} +r=json.loads((root/'reviewed.json').read_text());journal={'schema':'fsgg.wasm.promotion/v1','preparation':r,'firstPromotionRun':200,'firstPromotionAttempt':1,'producer':p.P,'tag':'wasm/v0.6.0'} (c/'promotion-binding.json').write_text(json.dumps(journal)) record={'schema':'fsgg.wasm.admission/v1','reviewed':r,'mode':'promote','run':200,'attempt':1,'exchangeVerified':True,'occupancy':{k:False for k in p.OCCUPANCY_KEYS},'observedAt':'now','nugetAccountSha256':p.digest(b'offline'),'publicModeratorRemovalHistory':'Unknown','atomicReservation':'Unknown'} raw=io.BytesIO() with zipfile.ZipFile(raw,'w')as z:z.writestr('admission-binding.json',json.dumps(record)) admission=raw.getvalue() -rows=[dict(id=i+1,name=n,url=f'https://api.github.com/repos/{p.REPO}/releases/assets/{i+1}',size=len((c/n).read_bytes()),digest='sha256:'+p.digest((c/n).read_bytes()))for i,n in enumerate(('fsgg-wasm-sdk-0.5.0.tar.gz','release-manifest.json','SHA256SUMS','promotion-binding.json'))] -release=dict(id=402763234,tag_name='wasm/v0.5.0',target_commitish=p.P,draft=True,prerelease=False,assets=rows) +rows=[dict(id=i+1,name=n,url=f'https://api.github.com/repos/{p.REPO}/releases/assets/{i+1}',size=len((c/n).read_bytes()),digest='sha256:'+p.digest((c/n).read_bytes()))for i,n in enumerate(('fsgg-wasm-sdk-0.6.0.tar.gz','release-manifest.json','SHA256SUMS','promotion-binding.json'))] +release=dict(id=402763234,tag_name='wasm/v0.6.0',target_commitish=p.P,draft=True,prerelease=False,assets=rows) class N(p.Native): def api(self,path,method='GET',data=None): assert method=='GET' diff --git a/tests/release/wasm/test-release-wasm.py b/tests/release/wasm/test-release-wasm.py index 7f996d2a..eaa41ceb 100755 --- a/tests/release/wasm/test-release-wasm.py +++ b/tests/release/wasm/test-release-wasm.py @@ -123,7 +123,7 @@ def source_checks() -> None: props = (ROOT / "eng/wasm-shared/version.props").read_text() policy=json.loads((ROOT / "scripts/wasm-release/api-baseline-policy.json").read_text()) version=policy["candidateVersion"] - require(version == "0.5.0" and f"{version}" in props, "selected version origin mismatch") + require(version == "0.6.0" and f"{version}" in props, "selected version origin mismatch") for path in (ROOT / "src/Wasm.Contracts/FS.GG.Wasm.Contracts.fsproj", ROOT / "src/Wasm.Browser/FS.GG.Wasm.Browser.fsproj"): require("$(WasmSharedVersion)" in path.read_text(), f"{path} bypasses shared version origin") require((ROOT / "sdk/wasm/VERSION").read_text().strip() == version, "SDK version mismatch")