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: