diff --git a/docs/atlas/catalog.json b/docs/atlas/catalog.json index 3226337a..4463f260 100644 --- a/docs/atlas/catalog.json +++ b/docs/atlas/catalog.json @@ -1 +1 @@ -{"records":["0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67"],"schema":"fsgg.atlas/1"} +{"records":["0863093e69a16355fa9b90f8e7e29156ea4ad88c4523bc4883568bde831bdf35","2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248","231df3792931ad2333d00970a5993b3e13fa9dc38f687899f9fa2c966ed9fbfa","24fdc3980640ba58bedcae6f16d5b467eb7383c31161fb9b6ff10fd2e667b0e5","264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4","2b7dfb3a2f76368cb603161cdb6f8fa8c2b8afc3e96659dd37c975921ba5ba66","33df463263533529cca24a0b09937573f836d197cc1a739e9960b1cdd4deb1fd","343077d02ddd266b21d8677f7c256854134f7e3e2d18137b55ebe3f32564c5c0","34391bda3fb56135a2bddf74ab790958c175bc6a69c38175e6f8db792cfd50a0","36a19fefd7024f54f3391ffed19019c10222a514719edd7a44f0cde2a1c48258","3d3a0a302c02cd0bfee86715d09b0211baa8dad3e47402c638fc95c7cfa02e87","3de8718fb3ca9b15212e8bd84fba205995d24636087090a61c63c28a96a745b1","3df4f31fda99ad137c651322ad29809e36380400516399274870b4a4332b4a92","3f09ee199e3c2e808e0f6e74f197f2fdd8158ae099a7195c498ce9466bb78b03","44bda82d6f9a9625a83283bf56a15a1831f287037214dc134eeeae87f1faa869","6c5b38ee260d1f15f8029d699ec1cebceafb5e1407a6341dc7a4b96f4d9d3521","72742cbaa561237f10251e94aa5c61153ab5c7f14fc5771b7a5ebdece1e1cb11","87d6de6f3730cecd75d9a4411bf4dbac4e058127a3ce9f46b07f6de110bc26de","8b1cf51c5f03c0ae19c46ca3ea4a1158acbb6e47bf109ca8c506c5417b51cf9f","95115e1052b5ea4fcd393f5c9b9c6f0298657f0028eee84e16215ede97576cfa","ab4502eff76606ac8e58b623878f3d76e1e0b18f30796aec7c446e2aab8b57c8","c01c2c98f13a65b49113916a2f295e46a03fc4c185ece880267dd86c3f3dee8b","d557525e3ebfad227c8d6db9bb6350d42506fb02b1de8058cb443f4f008c0f67"],"schema":"fsgg.atlas/1"} diff --git a/docs/atlas/records/2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248.json b/docs/atlas/records/2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248.json new file mode 100644 index 00000000..60785cab --- /dev/null +++ b/docs/atlas/records/2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248.json @@ -0,0 +1 @@ +{"citations":[{"path":"scripts/wasm-release/installed-evidence.py","repository":"FS-GG/FS.GG.Game","revision":"d971032fa6607cb81931f2e1e52d61f34a8620b1","sha256":"d7f74acf8a6b1502e7537055e021c5c998f87e700562bd0805c7248bddc82709"},{"path":"tests/release/wasm/test-installed-evidence.py","repository":"FS-GG/FS.GG.Game","revision":"d971032fa6607cb81931f2e1e52d61f34a8620b1","sha256":"6aa6c9027d2645cd554ed86fbc8e48df445c3c66c977e423151cfe13ffd563b5"},{"path":"scripts/wasm-release/bind-installed-release.py","repository":"FS-GG/FS.GG.Game","revision":"d971032fa6607cb81931f2e1e52d61f34a8620b1","sha256":"dd785951b1f0505533358b180f27b2382e99eea17a8bb7855bcadb5ac85a69a1"},{"path":".github/workflows/wasm-installed-org.yml","repository":"FS-GG/FS.GG.Game","revision":"d971032fa6607cb81931f2e1e52d61f34a8620b1","sha256":"5d9a89a860e6af539ce4c5e99add2a2d5cc50e217ae36b720a2b8f90a9434221"},{"path":"docs/roadmaps/wasm-shared-01.md","repository":"FS-GG/FS.GG.Game","revision":"d971032fa6607cb81931f2e1e52d61f34a8620b1","sha256":"543cb23b9b7cf03cce5f4baeb580b930cd4673838f633fc20177f9f67cd626fe"}],"conflictsWith":[],"id":"game/current-wasm-custody-source-boundary","kind":"boundary","recordedAt":"2026-10-17T09:33:18Z","repository":"FS-GG/FS.GG.Game","schema":"fsgg.atlas/1","status":"recorded","summary":"Publication 0.4.0 remains accepted at immutable producer/publisher P=E 6732d2371d142d5ccc5d2d127f20e4f179800892, original 39015233527/attempt1. PR715 delivered the reviewed read-only P/Q binding and bounded courier at qualifier 5104205393d45fcc1ce5eacbb40f9a237403634d with complete same-tree PR qualification. Original installed 39016307973 remains failed with zero artifacts and public unexecuted. Fresh installed 39020851715/attempt1 ran both feeds independently: each general consumer runtime passed its 32 browser and Rust2/C4 checks, but first evidence collection refused 21 actual Fable source/support paths and returned 1. Both selected supervisor suites were unreached. Final collectors were ineligible/failing; portable diagnostic uploads succeeded and were independently reviewed. This is not full installed acceptance. Runtime 0, collection 1, cleanup 0 and wrapper command 1/tee 0 remain distinct; descendant settlement remains unproved.\nThe connected source correction classifies finite pinned Fable module/context roles across general consumer output, selected compiler output and its browser copy, and both restored Worker policy trees. Copied FS.GG source/primitives must equal retained original package members. Actual compiled .fs.js and transformed .fableproj outputs must have their corresponding original source/project input; they are not claimed byte-identical to that input. Fable-library runtime JavaScript must equal the published Browser package static-library member. Known pinned-library documentation/declarations/metadata and generated bookkeeping are bounded data, not substitutes for executable provenance. Consumer archive admission separately restricts these roles to its own two actual contexts; forged selected-context support refuses during actual merge. Foreign modules/members, library/source byte drift, unexpected executable paths and links refuse; mandatory Core, lock and Worker evidence remains required. Original package/member/signature/P-Q bindings and 512 MiB/256 MiB per file/32 MiB logs/1 MiB metadata/1024 member caps remain unchanged.\nThe complete P-to-current-Q audit still admits only finite reviewed installed-binding/courier/tests/owning-documentation paths. Package/runtime/Worker/model/trace/project/lock/tool/semantic-harness inputs remain unchanged from P; publisher writer predicates retain same-current-source requirements. This functional citation precedes one normal metadata append adding exactly the catalog and this content-addressed record to that finite whitelist, with final exact review; no broad descendant or documentation exemption is implied. Focused inert role/topology/provenance/merge controls and clean static preflight passed. Actual 143 retained dependency files per feed replayed the new data-only provenance predicates, but 21 unretained support-file bytes remain unavailable and were not replayed. Repaired complete native selected topology, coherent CI, actual fit and a separately selected fresh both-feed installed operation remain owed before consumer adoption. Publication and all failed original identities stay unchanged.","supersedes":["264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4"],"tags":["wasm","installed","publication","qualification","source","evidence","fable"],"title":"Installed proof classifies Fable dependencies by role and original package provenance"} diff --git a/docs/roadmaps/wasm-shared-01.md b/docs/roadmaps/wasm-shared-01.md index b0daa11f..40540171 100644 --- a/docs/roadmaps/wasm-shared-01.md +++ b/docs/roadmaps/wasm-shared-01.md @@ -17,10 +17,20 @@ Fresh installed operation [39016307973](https://github.com/FS-GG/FS.GG.Game/actions/runs/39016307973), attempt 1, remains failed. Org's actual consumer qualification passed, then evidence upload failed on NuGet HTTP-cache filenames and produced zero artifacts. Public -was cancelled before execution. The current follow-up separates reviewed qualifier -Q from this immutable published P, repairs bounded evidence retention, and requires -complete new coherent qualification plus a separately selected both-feed installed -operation. Both-feed acceptance and consumer adoption remain open. Earlier source +was cancelled before execution. PR715 delivered the reviewed P/Q binding and +bounded evidence collector at qualifier +`5104205393d45fcc1ce5eacbb40f9a237403634d`, with complete same-tree PR qualification. +Its fresh both-feed installed operation +[39020851715](https://github.com/FS-GG/FS.GG.Game/actions/runs/39020851715), attempt 1, +also remains failed: each feed passed the general consumer's 32 browser tests and +Rust2/C4 checks, then its first evidence projection refused Fable dependency +source/support files. Both selected-supervisor phases were unreached. Independent +review verified both bounded diagnostic archives; final collectors failed as +ineligible while both uploads succeeded. No both-feed acceptance follows. +The current correction classifies those dependency roles against original package +members across the general and selected Fable contexts. Complete new coherent +qualification and a separately selected installed operation remain required. +Publication remains accepted at P; consumer adoption remains open. Earlier source and operation frontiers below retain their historical scopes. ## Protected qualification and checkout-reader correction (completed source delivery) @@ -727,3 +737,28 @@ paths. Complete coherent CI and a separately selected new both-feed installed ru remain required; neither local source controls nor the original org-only runtime success authorize consumer adoption. Publication bytes and both failed/successful original identities remain unchanged. + + +### Fable dependency projection after original installed refusal + +Original installed operation 39020851715/attempt1 preserved the precise first +cause: general consumer runtime 0, collection 1, cleanup 0, command 1 and log-copy +0 on each feed. Its 21 refused Fable source/support paths prevented both selected +supervisor suites from starting. Both always-run final collectors returned +ineligible diagnostics; their two portable uploads succeeded and were independently +reviewed. This corrects evidence retention, not installed acceptance or descendant +cleanup proof. The earlier zero-artifact operation remains a separate failure. + +Projection now distinguishes finite Fable dependency modules and their roles in +the actual compiler output, selected output/copy and restored Worker policy +contexts. Copied FS.GG sources and primitive inputs must match the retained +original package member; generated JavaScript and transformed Fable projects must +have the corresponding source/project member in that same original archive. +Fable-library runtime JavaScript must match the original Browser package's static +library member. Known pinned-library documentation/declarations/metadata and +Fable bookkeeping remain bounded data, not runtime or provenance substitutes. +Foreign modules/members, unexpected executable files, paths, links, changed copied +sources or library runtime refuse; missing Core, lock and Worker proof still +refuses. Producer caps and feed isolation do not change. Inert controls cover all +caller contexts and actual merge provenance, while repaired native execution and +complete projection fit remain unproved until a separately selected original run. diff --git a/scripts/wasm-release/bind-installed-release.py b/scripts/wasm-release/bind-installed-release.py index 74ff49a6..e59ec5fe 100644 --- a/scripts/wasm-release/bind-installed-release.py +++ b/scripts/wasm-release/bind-installed-release.py @@ -30,6 +30,7 @@ 'docs/roadmaps/wasm-shared-01.md', 'docs/atlas/catalog.json', 'docs/atlas/records/264a2f7887a9b6473cbc24102e1bf9be70e57f6d7ba224138140efa094d541f4.json', + 'docs/atlas/records/2056fa8b653c2240afdad2a16f572a07019109254847829610a9468bb2f95248.json', }) diff --git a/scripts/wasm-release/installed-evidence.py b/scripts/wasm-release/installed-evidence.py index d28f24bb..dc7c5a65 100644 --- a/scripts/wasm-release/installed-evidence.py +++ b/scripts/wasm-release/installed-evidence.py @@ -8,6 +8,7 @@ import pathlib import stat import tarfile +import zipfile ROOT = pathlib.Path(__file__).resolve().parents[2] MIB = 1024 * 1024 @@ -28,6 +29,44 @@ def safe_name(name): return name +def fable_dependency(name): + """Finite roles in the actual pinned Fable dependency layout, not suffix grants.""" + if '/fable_modules/' not in name: return None + prefix, relative = name.split('/fable_modules/', 1) + contexts = {'consumer/public/sub/app/fable', 'selected/installed-fable', + 'selected/browser/fable', 'selected/installed-fable/Installed', + 'selected/browser/fable/Installed', + 'consumer/public/sub/app/_content/FS.GG.Wasm.Browser/policy', + 'selected/browser/_content/FS.GG.Wasm.Browser/policy'} + if prefix not in contexts: raise ValueError('unreviewed Fable dependency context') + if relative == '.gitignore': return ('bookkeeping', None, None) + module, separator, leaf = relative.partition('/') + if not separator: raise ValueError('unreviewed Fable dependency module') + if module == 'fable-library-js.5.18.0': + if leaf in ('README.md', 'CHANGELOG.md', 'big.d.ts', 'package.json'): + return ('library-support', None, None) + # The exact runtime member must also occur byte-identically in the + # published Browser package; .js alone never establishes provenance. + return ('library-runtime', 'fs.gg.wasm.browser', + 'contentFiles/any/any/_content/FS.GG.Wasm.Browser/policy/fable_modules/'+relative) + modules = { + 'FS.GG.Wasm.Browser.0.4.0': ('fs.gg.wasm.browser', + ('Admission', 'Host', 'Invocation', 'Lifecycle', 'RuntimeProtocol', 'WorkerEntry'), + 'FS.GG.Wasm.Browser'), + 'FS.GG.Wasm.Contracts.0.4.0': ('fs.gg.wasm.contracts', ('Contracts',), 'FS.GG.Wasm.Contracts')} + if module not in modules: raise ValueError('unreviewed Fable dependency module') + package, stems, project = modules[module] + if leaf in {stem+suffix for stem in stems for suffix in ('.fs', '.fsi')}: + return ('package-source', package, 'fable/'+leaf) + if leaf in {stem+'.fs.js' for stem in stems}: + return ('compiled-package-source', package, 'fable/'+leaf[:-3]) + if leaf == project+'.fableproj': + return ('derived-project', package, 'fable/'+project+'.fsproj') + if package == 'fs.gg.wasm.browser' and leaf in ('eng/wasm-shared/version.props', 'js/wasm-primitives.js'): + return ('package-source', package, 'fable/'+leaf) + raise ValueError('unreviewed Fable package dependency member') + + def consumer_member(name): safe_name(name) fixed={'consumer/release-manifest.json','consumer/NuGet.Config','consumer/consumer/packages.lock.json', @@ -36,6 +75,11 @@ def consumer_member(name): for ident in PACKAGES: if name in {'consumer/packages/'+ident+'/'+leaf for leaf in (ident+'.0.4.0.nupkg',ident+'.0.4.0.nupkg.sha512','.nupkg.metadata')}:return True if name in {'consumer/core/'+leaf for leaf in ('fsharp.core.10.1.302.nupkg','fsharp.core.10.1.302.nupkg.sha512','.nupkg.metadata','reference-netstandard2.0.dll','reference-netstandard2.1.dll')}:return True + if '/fable_modules/' in name: + context = name.split('/fable_modules/', 1)[0] + if context not in {'consumer/public/sub/app/fable', 'consumer/public/sub/app/_content/FS.GG.Wasm.Browser/policy'}: return False + try: return fable_dependency(name) is not None + except ValueError: return False return name.startswith('consumer/public/sub/app/') and pathlib.PurePosixPath(name).suffix in SUFFIXES and not set(pathlib.PurePosixPath(name).parts)&{'node_modules','http-cache','lock-http-cache','.git'} @@ -69,6 +113,36 @@ def add(self, path, name, required=True): if category: setattr(self,category,getattr(self,category)+len(body)) except (OSError,ValueError) as error: self.errors.append({'member':name,'status':'absent' if not path.exists() else 'refused','reason':str(error)}) + def dependency(self, name): + """Join actual dependency roles to the already retained original archive.""" + try: + role = fable_dependency(name) + if role is None: return + kind, package, member = role + body = self.files[name] + if kind in ('bookkeeping', 'library-support'): + body.decode('utf-8') + if b'\0' in body: raise ValueError('binary Fable support material') + if name.endswith('/package.json'): + metadata = json.loads(body) + if not isinstance(metadata, dict): raise ValueError('invalid Fable library metadata') + if (metadata.get('name'), metadata.get('version'), metadata.get('type')) != ('@fable-org/fable-library-js', '2.8.0', 'module'): + raise ValueError('pinned Fable library metadata drift') + return + context = 'consumer' if name.startswith('consumer/') else 'selected' + original = self.files[context+'/packages/'+package+'/'+package+'.0.4.0.nupkg'] + with zipfile.ZipFile(io.BytesIO(original)) as archive: + rows = [row for row in archive.infolist() if row.filename == member] + if len(rows) != 1 or rows[0].is_dir() or rows[0].file_size > LIMITS['file']: + raise ValueError('Fable dependency absent/ambiguous in original package') + source = archive.read(rows[0]) + if kind in ('library-runtime', 'package-source') and source != body: + raise ValueError('Fable dependency differs from original package member') + # Compiled .fs.js and transformed .fableproj are actual compiler + # outputs, not claimed byte-identical copies of their source. + except (OSError, ValueError, KeyError, TypeError, UnicodeError, RuntimeError, EOFError, zipfile.BadZipFile) as error: + self.errors.append({'member': name, 'status': 'refused', 'reason': str(error)}) + def tree(self, path, prefix): path=pathlib.Path(path) if not path.exists(): @@ -87,9 +161,15 @@ def tree(self, path, prefix): count+=1 if count>LIMITS['members']: raise ValueError('compiled output census cap') file=pathlib.Path(directory,name) - if file.suffix not in SUFFIXES and name!='http.log' and not (prefix=='consumer/public' and file.relative_to(path).as_posix()=='sub/app/Consumer'): - self.errors.append({'member':prefix+'/'+file.relative_to(path).as_posix(),'status':'refused','reason':'unreviewed compiled output suffix'}); continue - self.add(file,prefix+'/'+file.relative_to(path).as_posix()) + member = prefix+'/'+file.relative_to(path).as_posix() + try: + dependency = fable_dependency(member) + except ValueError as error: + self.errors.append({'member':member,'status':'refused','reason':str(error)}); continue + if dependency is None and file.suffix not in SUFFIXES and name!='http.log' and not (prefix=='consumer/public' and file.relative_to(path).as_posix()=='sub/app/Consumer'): + self.errors.append({'member':member,'status':'refused','reason':'unreviewed compiled output suffix'}); continue + self.add(file,member) + if member in self.files: self.dependency(member) except (OSError,ValueError) as error: self.errors.append({'member':prefix,'status':'refused','reason':str(error)}) def packages(self, cache, prefix): @@ -224,6 +304,9 @@ def merge(projection, source): projection.files[member.name]=body;projection.total+=len(body) if category: setattr(projection,category,getattr(projection,category)+len(body)) if seen != set(rows)|{'evidence-manifest.json'}: raise ValueError('consumer archive roster drift') + # Archive order is not authority: originals may follow their copied + # dependencies, so join all roles only after the complete merge. + for name in rows: projection.dependency(name) projection.errors.extend(manifest['errors']) if manifest.get('runtimeStatus')!=0 or manifest.get('collectorStatus')!='passed' or manifest.get('eligible') is not True: projection.errors.append({'member':'consumer','status':'failed','reason':'consumer runtime/collection not eligible'}) diff --git a/tests/release/wasm/test-installed-evidence.py b/tests/release/wasm/test-installed-evidence.py index 9254bef4..0f58aacd 100644 --- a/tests/release/wasm/test-installed-evidence.py +++ b/tests/release/wasm/test-installed-evidence.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 """Inert actual collector controls: no compiler, restore, browser or network.""" import importlib.util +import io +import zipfile import json import os import pathlib @@ -50,6 +52,97 @@ def test_hidden_metadata_and_colon_cache_exclusion(self): joined=e.Projection();e.merge(joined,root/'output') self.assertFalse(joined.errors);self.assertEqual(joined.files,p.files) + def test_complete_fable_dependency_roles_and_original_archive_joins(self): + # Inert topology mirrors all three compiler/adapter outputs plus both + # restored Worker policy trees. Source/support is distinct from runtime. + members = { + 'fs.gg.wasm.browser': {'fable/'+stem+suffix: (stem+suffix).encode() + for stem in ('Admission','Host','Invocation','Lifecycle','RuntimeProtocol','WorkerEntry') + for suffix in ('.fs','.fsi')}, + 'fs.gg.wasm.contracts': {'fable/Contracts.fs':b'Contracts.fs','fable/Contracts.fsi':b'Contracts.fsi'}} + for package,project in (('fs.gg.wasm.browser','FS.GG.Wasm.Browser'),('fs.gg.wasm.contracts','FS.GG.Wasm.Contracts')): + members[package]['fable/'+project+'.fsproj']=b'' + members['fs.gg.wasm.browser'].update({ + 'fable/eng/wasm-shared/version.props':b'', + 'fable/js/wasm-primitives.js':b'actual primitives', + 'contentFiles/any/any/_content/FS.GG.Wasm.Browser/policy/fable_modules/fable-library-js.5.18.0/Array.js':b'actual runtime', + 'contentFiles/any/any/_content/FS.GG.Wasm.Browser/policy/fable_modules/fable-library-js.5.18.0/lib/big.js':b'actual nested runtime'}) + archives={} + for package,rows in members.items(): + output=io.BytesIO() + with zipfile.ZipFile(output,'w') as archive: + for name,body in rows.items():archive.writestr(name,body) + archives[package]=output.getvalue() + contexts=('consumer/public/sub/app/fable','selected/installed-fable','selected/browser/fable', 'selected/installed-fable/Installed','selected/browser/fable/Installed', + 'consumer/public/sub/app/_content/FS.GG.Wasm.Browser/policy', + 'selected/browser/_content/FS.GG.Wasm.Browser/policy') + for context in contexts: + with self.subTest(context=context),tempfile.TemporaryDirectory() as raw: + root=pathlib.Path(raw);p=e.Projection();owner=context.split('/')[0] + for package,body in archives.items(): + p.add(put(root/package,body),owner+'/packages/'+package+'/'+package+'.0.4.0.nupkg') + dependency=root/'compiled/fable_modules' + put(dependency/'.gitignore',b'*') + library=dependency/'fable-library-js.5.18.0' + for leaf,body in (('README.md',b'readme'),('CHANGELOG.md',b'changelog'),('big.d.ts',b'declare type'), + ('package.json',b'{"name":"@fable-org/fable-library-js","version":"2.8.0","type":"module"}'), + ('Array.js',b'actual runtime'),('lib/big.js',b'actual nested runtime')):put(library/leaf,body) + for package,module in (('fs.gg.wasm.browser','FS.GG.Wasm.Browser'),('fs.gg.wasm.contracts','FS.GG.Wasm.Contracts')): + folder=dependency/(module+'.0.4.0') + for member,body in members[package].items(): + if not member.startswith('fable/'):continue + leaf=member[len('fable/'):] + if leaf.endswith('.fsproj'):put(folder/(module+'.fableproj'),b'') + else:put(folder/leaf,body) + if leaf.endswith('.fs'):put(folder/(leaf+'.js'),b'generated current source') + p.tree(root/'compiled',context) + self.assertFalse(p.errors,p.errors) + self.assertIn(context+'/fable_modules/FS.GG.Wasm.Browser.0.4.0/Host.fs',p.files) + self.assertIn(context+'/fable_modules/FS.GG.Wasm.Browser.0.4.0/Host.fs.js',p.files) + if owner=='consumer': + self.assertEqual(p.finish(root/'out',0),0) + joined=e.Projection();e.merge(joined,root/'out') + self.assertFalse(joined.errors,joined.errors);self.assertEqual(joined.files,p.files) + copied=context+'/fable_modules/FS.GG.Wasm.Browser.0.4.0/Host.fs' + original=p.files[copied];p.files[copied]=b'changed copied source' + p.finish(root/'forged',0) + refused=e.Projection();e.merge(refused,root/'forged') + self.assertTrue(any(x['member']==copied and x['reason']=='Fable dependency differs from original package member' for x in refused.errors),refused.errors) + p.files[copied]=original + # Same named runtime drift and same-suffix foreign executables + # must reach their actual provenance/role refusal predicates. + put(library/'Array.js',b'changed runtime') + bad=e.Projection();bad.files.update(p.files) + # dependency validates the collected bytes, not the path again. + bad.files[context+'/fable_modules/fable-library-js.5.18.0/Array.js']=b'changed runtime' + bad.dependency(context+'/fable_modules/fable-library-js.5.18.0/Array.js') + self.assertTrue(any(x['reason']=='Fable dependency differs from original package member' for x in bad.errors),bad.errors) + for leaf in ('fable-library-js.5.18.0/unexpected.exe','fable-library-js.5.18.0/foreign.js', + 'Other.0.4.0/Host.fs','FS.GG.Wasm.Browser.0.4.0/Foreign.fs', + 'FS.GG.Wasm.Browser.0.4.0/Foreign.fs.js'): + name=context+'/fable_modules/'+leaf + if leaf.endswith('foreign.js'): + bad.files[name]=b'foreign runtime';bad.dependency(name) + self.assertTrue(any(x['member']==name and 'absent/ambiguous' in x['reason'] for x in bad.errors)) + elif leaf.endswith('unexpected.exe'): + bad.files[name]=b'foreign executable';bad.dependency(name) + self.assertTrue(any(x['member']==name and 'absent/ambiguous' in x['reason'] for x in bad.errors)) + else: + with self.assertRaisesRegex(ValueError,'unreviewed Fable'):e.fable_dependency(name) + for name in ('consumer/public/sub/app/arbitrary/fable_modules/fable-library-js.5.18.0/Array.js', + 'consumer/public/sub/app/fable/fable_modules/Unknown/runtime.js'): + self.assertFalse(e.consumer_member(name)) + + def test_consumer_merge_refuses_selected_dependency_context(self): + for name in ('selected/installed-fable/fable_modules/.gitignore', + 'selected/browser/fable/fable_modules/fable-library-js.5.18.0/README.md'): + with self.subTest(name=name),tempfile.TemporaryDirectory() as raw: + root=pathlib.Path(raw);p=e.Projection();p.add(put(root/'data',b'foreign support'),name) + self.assertEqual(p.finish(root/'forged-consumer',0),0) + joined=e.Projection();e.merge(joined,root/'forged-consumer') + self.assertTrue(any(x['reason']=='foreign consumer evidence member' for x in joined.errors),joined.errors) + self.assertNotIn(name,joined.files) + def test_refusals_retain_independent_evidence_and_runtime_cause(self): for defect in ('symlink','hardlink','traversal','duplicate','oversize','missing'): with self.subTest(defect=defect),tempfile.TemporaryDirectory() as raw: