From ced282f8b6f636eccef7b7e3349dc920274aaf48 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 10:01:42 +0100 Subject: [PATCH] Type EveElectron pending lifecycle --- docs/eve-migration-roadmap.md | 7 + .../eve-electron/eve-runtime-capability.json | 108 +++++++++++++++ ...cult.eve.runtime_capability.v1.schema.json | 1 - scripts/run-eveelectron-lifecycle-smoke.ps1 | 123 ++++++++++++++++++ .../run-eveelectron-provider-shell-smoke.ps1 | 27 ++++ scripts/run-parity-harness.ps1 | 1 + ...ntime-owner-conformance-consumer-smoke.ps1 | 16 ++- ...plit-target-conformance-consumer-smoke.ps1 | 1 + tools/parity/parity-manifest.json | 83 +++++++++++- 9 files changed, 364 insertions(+), 3 deletions(-) create mode 100644 runtimes/incubating/eve-electron/eve-runtime-capability.json create mode 100644 scripts/run-eveelectron-lifecycle-smoke.ps1 create mode 100644 scripts/run-eveelectron-provider-shell-smoke.ps1 diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index 0908e29..6ba3125 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -387,6 +387,13 @@ Recently cut: observed Aetheria Electron/client paths that must be replaced, the required Eve contract inputs, and the pending external proofs without treating Aetheria's product client as the generic Electron runtime. +- EveElectron now has a pending runtime capability manifest at + `runtimes/incubating/eve-electron/eve-runtime-capability.json` and a + lifecycle smoke at `scripts/run-eveelectron-lifecycle-smoke.ps1`. The + manifest declares no supported runtime features or plugin projection yet; it + only types the pending command boundary, package release, provider-shell + smoke, and Electron window capture contracts so EveElectron can consume the + conformance export without pretending the generic shell already exists. - EveTui now has a pending split target and handoff at `runtimes/incubating/eve-tui/evetui-split-handoff.json`. It does not claim the advertised `tui` world-surface lowering target yet. The handoff draws the diff --git a/runtimes/incubating/eve-electron/eve-runtime-capability.json b/runtimes/incubating/eve-electron/eve-runtime-capability.json new file mode 100644 index 0000000..79ddc2c --- /dev/null +++ b/runtimes/incubating/eve-electron/eve-runtime-capability.json @@ -0,0 +1,108 @@ +{ + "schema": "gamecult.eve.runtime_capability.v1", + "runtimeId": "electron-shell", + "title": "Electron shell", + "owner": "EveElectron", + "incubation": { + "ownerRepo": "EveElectron", + "currentHostRepo": "Eve", + "splitTarget": "EveElectron", + "graduationTrigger": "Split when Electron has a provider-agnostic shell body, provider advertisement consumption, command transport, packaged app lifecycle, and capture evidence independent of Aetheria product code.", + "splitHandoff": { + "manifestPath": "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", + "smokeCommand": "powershell -ExecutionPolicy Bypass -File .\\scripts\\run-eveelectron-split-handoff-smoke.ps1" + } + }, + "supportedFeatures": [], + "supportedPlugins": [], + "unsupportedPlugins": [ + { + "pluginId": "sai.vn", + "reason": "Electron has no generic runtime body yet; Sai projection belongs behind provider/plugin advertisements once EveElectron exists." + }, + { + "pluginId": "norn.graph", + "reason": "Electron has no generic runtime body yet; Norn graph projection belongs behind provider/plugin advertisements once EveElectron exists." + }, + { + "pluginId": "tex.math", + "reason": "Electron has no generic runtime body yet; TeX projection belongs behind provider/plugin advertisements once EveElectron exists." + } + ], + "commandTransport": { + "schema": "gamecult.eve.command.v1", + "status": "pending-runtime-body", + "emitter": "EveElectronShell.EmitCommand", + "clientId": "electron-shell", + "authority": "Electron emits command intents through provider-advertised command boundaries; providers accept, deny, and publish receipts." + }, + "lifecycle": { + "release": { + "ownerRepo": "EveElectron", + "status": "pending-electron-package", + "releaseContract": { + "ownerRepo": "EveElectron", + "packageName": "eve-electron", + "packageRoot": "runtimes/incubating/eve-electron", + "versionSource": "runtimes/incubating/eve-electron/eve-runtime-capability.json", + "tagPattern": "eveelectron-v{version}", + "artifactKind": "electron-app", + "publishProof": "Packaged Electron release is created from the EveElectron repo and references the provider-agnostic shell package root." + }, + "evidencePaths": [ + "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", + "scripts/run-eveelectron-split-handoff-smoke.ps1" + ], + "pendingProofs": [ + "Electron packaged app release is cut from EveElectron rather than Eve incubation." + ] + }, + "test": { + "ownerRepo": "EveElectron", + "status": "pending-provider-advertisement-shell-smoke", + "testContract": { + "ownerRepo": "EveElectron", + "runnerKind": "electron-provider-shell-smoke", + "runnerScript": "scripts/run-eveelectron-provider-shell-smoke.ps1", + "consumerProject": "E:\\Projects\\Aetheria", + "packageName": "eve-electron", + "testAssembly": "EveElectron.ProviderShell", + "testPlatform": "Electron", + "resultsArtifact": "artifacts/eveelectron-provider-shell/{stamp}/provider-shell-results.json", + "logArtifact": "artifacts/eveelectron-provider-shell/{stamp}/provider-shell.log", + "manifestMutation": "None; the shell consumes provider advertisements and must not mutate provider product manifests." + }, + "evidencePaths": [ + "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", + "scripts/run-eveelectron-split-handoff-smoke.ps1" + ], + "pendingProofs": [ + "Electron provider-advertisement shell smoke runs from EveElectron without importing Aetheria source layout." + ] + }, + "capture": { + "ownerRepo": "EveElectron", + "status": "pending-electron-window-capture", + "captureContract": { + "ownerRepo": "EveElectron", + "runtimeId": "electron-shell", + "captureKind": "electron-window-png", + "artifactKind": "png", + "artifactPattern": "artifacts/eveelectron-capture/{stamp}/electron-shell.png", + "conformanceAttachment": "runtime.captureArtifacts[]", + "requiredProvider": "aetheria", + "requiredSurface": "aetheria.daemon.game", + "authority": "Capture observes Electron projection only; provider state, command acceptance, and receipts stay provider-owned.", + "publishProof": "Electron window capture artifact is attached to the runtime conformance report from EveElectron." + }, + "evidencePaths": [], + "pendingProofs": [ + "Electron window capture artifact is attached to the runtime conformance report." + ] + } + }, + "capture": { + "status": "missing", + "note": "Electron capture is a split blocker until EveElectron owns a provider-agnostic shell." + } +} diff --git a/schemas/gamecult.eve.runtime_capability.v1.schema.json b/schemas/gamecult.eve.runtime_capability.v1.schema.json index eeafb8e..7a852ef 100644 --- a/schemas/gamecult.eve.runtime_capability.v1.schema.json +++ b/schemas/gamecult.eve.runtime_capability.v1.schema.json @@ -286,7 +286,6 @@ "runnerKind", "runnerScript", "consumerProject", - "unityExeDefault", "packageName", "testAssembly", "testPlatform", diff --git a/scripts/run-eveelectron-lifecycle-smoke.ps1 b/scripts/run-eveelectron-lifecycle-smoke.ps1 new file mode 100644 index 0000000..9da74fb --- /dev/null +++ b/scripts/run-eveelectron-lifecycle-smoke.ps1 @@ -0,0 +1,123 @@ +param( + [string] $CapabilityManifestPath = "runtimes\incubating\eve-electron\eve-runtime-capability.json" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$absoluteManifestPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) { + $CapabilityManifestPath +} else { + Join-Path $projectRoot $CapabilityManifestPath +} + +if (-not (Test-Path -LiteralPath $absoluteManifestPath)) { + throw "EveElectron runtime capability manifest not found: $absoluteManifestPath" +} + +$manifest = Get-Content -LiteralPath $absoluteManifestPath -Raw | ConvertFrom-Json + +if ($manifest.schema -ne "gamecult.eve.runtime_capability.v1") { + throw "Unexpected EveElectron manifest schema: $($manifest.schema)" +} +if ($manifest.runtimeId -ne "electron-shell") { + throw "Unexpected EveElectron runtime id: $($manifest.runtimeId)" +} +if ($manifest.owner -ne "EveElectron") { + throw "Unexpected EveElectron owner: $($manifest.owner)" +} +if ($manifest.incubation.ownerRepo -ne "EveElectron") { + throw "Unexpected EveElectron incubation owner: $($manifest.incubation.ownerRepo)" +} +if ($manifest.incubation.currentHostRepo -ne "Eve") { + throw "Unexpected EveElectron current host repo: $($manifest.incubation.currentHostRepo)" +} +if ($manifest.incubation.splitTarget -ne "EveElectron") { + throw "Unexpected EveElectron split target: $($manifest.incubation.splitTarget)" +} +if (-not $manifest.incubation.splitHandoff.manifestPath) { + throw "EveElectron manifest missing split handoff path" +} + +if (@($manifest.supportedFeatures).Count -ne 0) { + throw "EveElectron must not claim supported runtime features before the generic shell body exists" +} +if (@($manifest.supportedPlugins).Count -ne 0) { + throw "EveElectron must not claim plugin projection before the generic shell body exists" +} + +foreach ($pluginId in @("sai.vn", "norn.graph", "tex.math")) { + $unsupported = @($manifest.unsupportedPlugins) | Where-Object { $_.pluginId -eq $pluginId } | Select-Object -First 1 + if (-not $unsupported) { + throw "EveElectron manifest missing unsupported plugin declaration: $pluginId" + } +} + +if ($manifest.commandTransport.schema -ne "gamecult.eve.command.v1") { + throw "Unexpected EveElectron command schema: $($manifest.commandTransport.schema)" +} +if ($manifest.commandTransport.status -ne "pending-runtime-body") { + throw "Unexpected EveElectron command transport status: $($manifest.commandTransport.status)" +} + +foreach ($stage in @("release", "test", "capture")) { + $stageDocument = $manifest.lifecycle.$stage + if ($null -eq $stageDocument) { + throw "EveElectron lifecycle missing stage: $stage" + } + if ($stageDocument.ownerRepo -ne "EveElectron") { + throw "EveElectron lifecycle stage $stage has unexpected owner: $($stageDocument.ownerRepo)" + } + if (-not $stageDocument.status) { + throw "EveElectron lifecycle stage $stage missing status" + } + foreach ($evidencePath in @($stageDocument.evidencePaths)) { + $absoluteEvidencePath = Join-Path $projectRoot $evidencePath + if (-not (Test-Path -LiteralPath $absoluteEvidencePath)) { + throw "EveElectron lifecycle stage $stage missing evidence path: $evidencePath" + } + } +} + +$releaseContract = $manifest.lifecycle.release.releaseContract +foreach ($field in @("ownerRepo", "packageName", "packageRoot", "versionSource", "tagPattern", "artifactKind", "publishProof")) { + if (-not $releaseContract.$field) { + throw "EveElectron release contract missing $field" + } +} +foreach ($pathProperty in @("packageRoot", "versionSource")) { + $relativePath = $releaseContract.$pathProperty + $absolutePath = Join-Path $projectRoot $relativePath + if (-not (Test-Path -LiteralPath $absolutePath)) { + throw "EveElectron release contract $pathProperty does not exist: $relativePath" + } +} + +$testContract = $manifest.lifecycle.test.testContract +foreach ($field in @("ownerRepo", "runnerKind", "runnerScript", "consumerProject", "packageName", "testAssembly", "testPlatform", "resultsArtifact", "logArtifact", "manifestMutation")) { + if (-not $testContract.$field) { + throw "EveElectron test contract missing $field" + } +} +foreach ($pathProperty in @("runnerScript", "consumerProject")) { + $candidate = $testContract.$pathProperty + $absolutePath = if ([System.IO.Path]::IsPathRooted($candidate)) { + $candidate + } else { + Join-Path $projectRoot $candidate + } + if (-not (Test-Path -LiteralPath $absolutePath)) { + throw "EveElectron test contract $pathProperty does not exist: $candidate" + } +} + +$captureContract = $manifest.lifecycle.capture.captureContract +foreach ($field in @("ownerRepo", "runtimeId", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) { + if (-not $captureContract.$field) { + throw "EveElectron capture contract missing $field" + } +} + +& (Join-Path $projectRoot "scripts\run-eveelectron-split-handoff-smoke.ps1") + +Write-Host "EveElectron lifecycle smoke passed: $absoluteManifestPath" diff --git a/scripts/run-eveelectron-provider-shell-smoke.ps1 b/scripts/run-eveelectron-provider-shell-smoke.ps1 new file mode 100644 index 0000000..4418e11 --- /dev/null +++ b/scripts/run-eveelectron-provider-shell-smoke.ps1 @@ -0,0 +1,27 @@ +param( + [string] $CapabilityManifestPath = "runtimes\incubating\eve-electron\eve-runtime-capability.json" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$absoluteManifestPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) { + $CapabilityManifestPath +} else { + Join-Path $projectRoot $CapabilityManifestPath +} + +if (-not (Test-Path -LiteralPath $absoluteManifestPath)) { + throw "EveElectron runtime capability manifest not found: $absoluteManifestPath" +} + +$manifest = Get-Content -LiteralPath $absoluteManifestPath -Raw | ConvertFrom-Json + +if ($manifest.runtimeId -ne "electron-shell") { + throw "Unexpected EveElectron runtime id: $($manifest.runtimeId)" +} +if ($manifest.lifecycle.test.status -ne "pending-provider-advertisement-shell-smoke") { + throw "EveElectron provider shell smoke should remain pending until a generic shell body exists: $($manifest.lifecycle.test.status)" +} + +throw "EveElectron provider shell smoke is a pending contract, not a runnable proof, until EveElectron owns the provider-agnostic Electron shell body." diff --git a/scripts/run-parity-harness.ps1 b/scripts/run-parity-harness.ps1 index 85f7071..7e1fd49 100644 --- a/scripts/run-parity-harness.ps1 +++ b/scripts/run-parity-harness.ps1 @@ -20,6 +20,7 @@ try { & (Join-Path $PSScriptRoot "run-eveunity-split-handoff-smoke.ps1") & (Join-Path $PSScriptRoot "run-eveunity-scene-split-handoff-smoke.ps1") & (Join-Path $PSScriptRoot "run-eveelectron-split-handoff-smoke.ps1") + & (Join-Path $PSScriptRoot "run-eveelectron-lifecycle-smoke.ps1") & (Join-Path $PSScriptRoot "run-conformance-consumer-smoke.ps1") & (Join-Path $PSScriptRoot "run-plugin-owner-conformance-consumer-smoke.ps1") & (Join-Path $PSScriptRoot "run-runtime-owner-conformance-consumer-smoke.ps1") diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index 32c8507..5f8035f 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -37,12 +37,14 @@ node $consumerScript $consumerExport ` --expect-runtime linux-flutter ` --expect-runtime android-flutter ` --expect-runtime unity-uitoolkit ` + --expect-runtime electron-shell ` --expect-runtime direct2d ` --expect-runtime-status web:active ` --expect-runtime-status windows-flutter:active ` --expect-runtime-status linux-flutter:active ` --expect-runtime-status android-flutter:active ` --expect-runtime-status unity-uitoolkit:active ` + --expect-runtime-status electron-shell:pending ` --expect-runtime-status direct2d:external-adapter-spike ` --expect-runtime-feature web:providerAdvertisements ` --expect-runtime-feature windows-flutter:embeddedDocuments ` @@ -80,6 +82,7 @@ node $consumerScript $consumerExport ` --expect-runtime-capture-status linux-flutter:ssh-golden ` --expect-runtime-capture-status android-flutter:adb-png ` --expect-runtime-capture-status unity-uitoolkit:semantic ` + --expect-runtime-capture-status electron-shell:missing ` --expect-runtime-capture-status direct2d:missing ` --expect-runtime-lifecycle-status unity-uitoolkit:release:incubating-upm-package ` --expect-runtime-lifecycle-status unity-uitoolkit:test:batchmode-editmode-tests-and-consumer-build-smoke ` @@ -100,7 +103,18 @@ node $consumerScript $consumerExport ` --expect-runtime-lifecycle-field "unity-uitoolkit:capture:captureContract.artifactKind:png" ` --expect-runtime-lifecycle-field "unity-uitoolkit:capture:captureContract.conformanceAttachment:runtime.captureArtifacts[]" ` --expect-runtime-lifecycle-field "unity-uitoolkit:capture:captureContract.requiredProvider:aetheria" ` - --expect-runtime-lifecycle-field "unity-uitoolkit:capture:captureContract.requiredSurface:aetheria.daemon.game" + --expect-runtime-lifecycle-field "unity-uitoolkit:capture:captureContract.requiredSurface:aetheria.daemon.game" ` + --expect-runtime-lifecycle-status electron-shell:release:pending-electron-package ` + --expect-runtime-lifecycle-status electron-shell:test:pending-provider-advertisement-shell-smoke ` + --expect-runtime-lifecycle-status electron-shell:capture:pending-electron-window-capture ` + --expect-runtime-lifecycle-pending "electron-shell:release:Electron packaged app release" ` + --expect-runtime-lifecycle-pending "electron-shell:test:Electron provider-advertisement shell smoke" ` + --expect-runtime-lifecycle-pending "electron-shell:capture:Electron window capture artifact" ` + --expect-runtime-lifecycle-field "electron-shell:release:releaseContract.artifactKind:electron-app" ` + --expect-runtime-lifecycle-field "electron-shell:test:testContract.runnerKind:electron-provider-shell-smoke" ` + --expect-runtime-lifecycle-field "electron-shell:test:testContract.runnerScript:scripts/run-eveelectron-provider-shell-smoke.ps1" ` + --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.captureKind:electron-window-png" ` + --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requiredSurface:aetheria.daemon.game" if ($LASTEXITCODE -ne 0) { throw "Runtime owner conformance consumer smoke failed with exit code $LASTEXITCODE" diff --git a/scripts/run-split-target-conformance-consumer-smoke.ps1 b/scripts/run-split-target-conformance-consumer-smoke.ps1 index a412c05..d7b19c7 100644 --- a/scripts/run-split-target-conformance-consumer-smoke.ps1 +++ b/scripts/run-split-target-conformance-consumer-smoke.ps1 @@ -55,6 +55,7 @@ try { --expect-split-target EveElectron ` --expect-split-target-status EveElectron:incubating ` --expect-split-target-proof "EveElectron:EveElectron split handoff is machine-readable" ` + --expect-split-target-proof "EveElectron:EveElectron lifecycle contract is machine-readable" ` --expect-split-target-blocker "EveElectron:runtime:electron-shell:status:pending" ` --expect-split-target-blocker-record EveElectron:runtime-status:electron-shell ` --expect-split-target-blocker-record EveElectron:runtime-feature:electron-shell:providerAdvertisements ` diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 736b9b1..96eee47 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -610,6 +610,15 @@ "docs/eve-migration-roadmap.md", "docs/parity-testing-harness.md" ] + }, + { + "description": "EveElectron lifecycle contract is machine-readable and smoke-checked.", + "status": "passed", + "evidencePaths": [ + "runtimes/incubating/eve-electron/eve-runtime-capability.json", + "scripts/run-eveelectron-lifecycle-smoke.ps1", + "scripts/run-eveelectron-provider-shell-smoke.ps1" + ] } ], "pendingProofs": [ @@ -657,6 +666,10 @@ "repoRole": "runtime-target", "splitTarget": "EveElectron", "graduationTrigger": "Split when Electron has a provider-agnostic shell body, provider advertisement consumption, command transport, packaged app lifecycle, and capture evidence independent of Aetheria product code.", + "capabilityManifest": { + "schemaPath": "schemas/gamecult.eve.runtime_capability.v1.schema.json", + "manifestPath": "runtimes/incubating/eve-electron/eve-runtime-capability.json" + }, "splitHandoffPath": "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", "requiredFixtures": ["embedded-surface", "aetheria-world"], "pluginFixtures": [], @@ -677,8 +690,76 @@ } ], "expectedPaths": [ - "runtimes/incubating/eve-electron/eveelectron-split-handoff.json" + "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", + "runtimes/incubating/eve-electron/eve-runtime-capability.json", + "scripts/run-eveelectron-lifecycle-smoke.ps1", + "scripts/run-eveelectron-provider-shell-smoke.ps1" ], + "lifecycle": { + "release": { + "ownerRepo": "EveElectron", + "status": "pending-electron-package", + "releaseContract": { + "ownerRepo": "EveElectron", + "packageName": "eve-electron", + "packageRoot": "runtimes/incubating/eve-electron", + "versionSource": "runtimes/incubating/eve-electron/eve-runtime-capability.json", + "tagPattern": "eveelectron-v{version}", + "artifactKind": "electron-app", + "publishProof": "Packaged Electron release is created from the EveElectron repo and references the provider-agnostic shell package root." + }, + "evidencePaths": [ + "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", + "scripts/run-eveelectron-split-handoff-smoke.ps1" + ], + "pendingProofs": [ + "Electron packaged app release is cut from EveElectron rather than Eve incubation." + ] + }, + "test": { + "ownerRepo": "EveElectron", + "status": "pending-provider-advertisement-shell-smoke", + "testContract": { + "ownerRepo": "EveElectron", + "runnerKind": "electron-provider-shell-smoke", + "runnerScript": "scripts/run-eveelectron-provider-shell-smoke.ps1", + "consumerProject": "E:\\Projects\\Aetheria", + "packageName": "eve-electron", + "testAssembly": "EveElectron.ProviderShell", + "testPlatform": "Electron", + "resultsArtifact": "artifacts/eveelectron-provider-shell/{stamp}/provider-shell-results.json", + "logArtifact": "artifacts/eveelectron-provider-shell/{stamp}/provider-shell.log", + "manifestMutation": "None; the shell consumes provider advertisements and must not mutate provider product manifests." + }, + "evidencePaths": [ + "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", + "scripts/run-eveelectron-split-handoff-smoke.ps1" + ], + "pendingProofs": [ + "Electron provider-advertisement shell smoke runs from EveElectron without importing Aetheria source layout." + ] + }, + "capture": { + "ownerRepo": "EveElectron", + "status": "pending-electron-window-capture", + "captureContract": { + "ownerRepo": "EveElectron", + "runtimeId": "electron-shell", + "captureKind": "electron-window-png", + "artifactKind": "png", + "artifactPattern": "artifacts/eveelectron-capture/{stamp}/electron-shell.png", + "conformanceAttachment": "runtime.captureArtifacts[]", + "requiredProvider": "aetheria", + "requiredSurface": "aetheria.daemon.game", + "authority": "Capture observes Electron projection only; provider state, command acceptance, and receipts stay provider-owned.", + "publishProof": "Electron window capture artifact is attached to the runtime conformance report from EveElectron." + }, + "evidencePaths": [], + "pendingProofs": [ + "Electron window capture artifact is attached to the runtime conformance report." + ] + } + }, "capture": { "status": "missing", "note": "Electron capture is a split blocker until EveElectron owns a provider-agnostic shell."