From e2e9b21e2f4cbc914fcc6fd71fb4dead6eb80a96 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 9 Jul 2026 01:24:21 +0100 Subject: [PATCH] Add EveFlutter provider picker proof --- docs/eve-migration-roadmap.md | 7 + docs/parity-testing-harness.md | 5 + docs/runtime-app-parity-roadmap.md | 3 + flutter/eve_parity/lib/main.dart | 120 ++++++++++++++++++ .../eve_parity/test/parity_smoke_test.dart | 88 +++++++++++++ tools/parity/parity-manifest.json | 16 ++- 6 files changed, 235 insertions(+), 4 deletions(-) diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index f2865fd..9b5c5f6 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -280,6 +280,13 @@ Exit criteria: Recently cut: +- EveFlutter now has a provider-catalog picker primitive. `EveProviderCatalog` + parses conformance-export provider entries, including Sai/Norn/TeX plugin + requirements, and `EveProviderPicker` selects those provider ids without + reading web fixture paths. The Flutter smoke test proves provider selection + from an export-shaped provider list, so the split-readiness report records the + provider-picker proof as passed while leaving release/capture lifecycle + ownership as the remaining EveFlutter blocker. - EveFlutter now has a conformance consumer smoke at `scripts/run-eveflutter-conformance-consumer-smoke.ps1`. It runs from `flutter/eve_parity`, consumes the exported conformance pack, and asserts the diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index e639d2a..3fdff48 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -120,6 +120,11 @@ That script runs the export consumer from `flutter/eve_parity` and asserts the runtime pack, Sai provider fixture, Flutter runtime targets, and `EveFlutter` split target without reading `tools/parity/parity-manifest.json`. +The Flutter widget smoke also exercises `EveProviderCatalog` and +`EveProviderPicker` against a conformance-export-shaped provider list. That is +the runtime-side provider picker proof; it consumes provider entries and plugin +requirements without reading web fixture paths. + Aetheria Unity package consumer smoke: ```powershell diff --git a/docs/runtime-app-parity-roadmap.md b/docs/runtime-app-parity-roadmap.md index f0200a2..07de4dd 100644 --- a/docs/runtime-app-parity-roadmap.md +++ b/docs/runtime-app-parity-roadmap.md @@ -155,6 +155,9 @@ First cuts: - Scaffold a generic Eve Flutter app with the same provider picker as web. - Load the same local provider advertisements and local fixture surfaces during development. +- Consume conformance-export provider entries through `EveProviderCatalog` and + select them through `EveProviderPicker`; this is the split-ready provider + picker input shape, distinct from web-local fallback fixture discovery. - Lower `surface.root` recursively into Flutter widgets/custom painters. - Lower CultUI partitions, `fieldRow`, labels, cards, buttons, metrics, and slider anatomy from `control.box`, `control.part`, and `control.hitArea`. diff --git a/flutter/eve_parity/lib/main.dart b/flutter/eve_parity/lib/main.dart index e9c1e6d..5229eec 100644 --- a/flutter/eve_parity/lib/main.dart +++ b/flutter/eve_parity/lib/main.dart @@ -122,6 +122,126 @@ class EveSurfaceState { } } +class EveProviderCatalog { + const EveProviderCatalog({required this.providers}); + + final List providers; + + factory EveProviderCatalog.fromConformanceExport(Map json) { + final providers = ((json['providers'] as List?) ?? const []) + .map((provider) { + return EveProviderEntry.fromJson(provider as Map); + }) + .toList(growable: false); + return EveProviderCatalog(providers: providers); + } + + EveProviderEntry? findProvider(String providerId) { + for (final provider in providers) { + if (provider.providerId == providerId) return provider; + } + return null; + } +} + +class EveProviderEntry { + const EveProviderEntry({ + required this.providerId, + required this.status, + required this.ownerRepo, + required this.advertisementPath, + required this.surfaces, + required this.commands, + required this.pluginRequirements, + }); + + final String providerId; + final String status; + final String ownerRepo; + final String advertisementPath; + final List surfaces; + final List commands; + final List pluginRequirements; + + factory EveProviderEntry.fromJson(Map json) { + return EveProviderEntry( + providerId: (json['providerId'] ?? '').toString(), + status: (json['status'] ?? '').toString(), + ownerRepo: (json['ownerRepo'] ?? '').toString(), + advertisementPath: (json['advertisementPath'] ?? '').toString(), + surfaces: _stringList(json['surfaces']), + commands: _stringList(json['commands']), + pluginRequirements: + ((json['pluginRequirements'] as List?) ?? const []) + .map( + (requirement) => EvePluginRequirement.fromJson( + requirement as Map, + ), + ) + .toList(growable: false), + ); + } +} + +class EvePluginRequirement { + const EvePluginRequirement({ + required this.surfaceId, + required this.pluginId, + required this.requiredCapabilities, + }); + + final String surfaceId; + final String pluginId; + final List requiredCapabilities; + + factory EvePluginRequirement.fromJson(Map json) { + return EvePluginRequirement( + surfaceId: (json['surfaceId'] ?? '').toString(), + pluginId: (json['pluginId'] ?? '').toString(), + requiredCapabilities: _stringList(json['requiredCapabilities']), + ); + } +} + +List _stringList(Object? value) { + return ((value as List?) ?? const []) + .map((item) => item.toString()) + .toList(growable: false); +} + +class EveProviderPicker extends StatelessWidget { + const EveProviderPicker({ + required this.catalog, + required this.selectedProviderId, + required this.onSelected, + super.key, + }); + + final EveProviderCatalog catalog; + final String selectedProviderId; + final ValueChanged onSelected; + + @override + Widget build(BuildContext context) { + return DropdownButton( + value: catalog.findProvider(selectedProviderId) == null + ? null + : selectedProviderId, + hint: const Text('Select provider'), + items: [ + for (final provider in catalog.providers) + DropdownMenuItem( + value: provider.providerId, + child: Text(provider.providerId), + ), + ], + onChanged: (value) { + if (value != null) onSelected(value); + }, + ); + } +} + class EveNode { EveNode({ required this.id, diff --git a/flutter/eve_parity/test/parity_smoke_test.dart b/flutter/eve_parity/test/parity_smoke_test.dart index 7a8aecd..3ece18c 100644 --- a/flutter/eve_parity/test/parity_smoke_test.dart +++ b/flutter/eve_parity/test/parity_smoke_test.dart @@ -122,6 +122,49 @@ void main() { expect(intents.single.clientId, 'flutter-parity'); expect(intents.single.payload['action']['operation'], 'focus_entity'); }); + + testWidgets('provider picker consumes conformance export providers', ( + tester, + ) async { + final catalog = EveProviderCatalog.fromConformanceExport( + _providerCatalogExport, + ); + var selected = 'aetheria'; + + await tester.pumpWidget( + MaterialApp( + home: StatefulBuilder( + builder: (context, setState) { + return EveProviderPicker( + catalog: catalog, + selectedProviderId: selected, + onSelected: (providerId) { + setState(() => selected = providerId); + }, + ); + }, + ), + ), + ); + + expect(catalog.providers, hasLength(2)); + expect(catalog.findProvider('gamecult.home.vn')?.ownerRepo, 'Sai'); + expect( + catalog + .findProvider('gamecult.home.vn') + ?.pluginRequirements + .map((requirement) => requirement.pluginId), + containsAll(['sai.vn', 'norn.graph', 'tex.math']), + ); + expect(find.text('aetheria'), findsOneWidget); + + await tester.tap(find.byType(DropdownButton)); + await tester.pumpAndSettle(); + await tester.tap(find.text('gamecult.home.vn').last); + await tester.pumpAndSettle(); + + expect(selected, 'gamecult.home.vn'); + }); } const fixtureId = String.fromEnvironment( @@ -142,3 +185,48 @@ class _Viewport { final double width; final double height; } + +const _providerCatalogExport = { + 'schema': 'gamecult.eve.conformance_export.v1', + 'providers': [ + { + 'providerId': 'aetheria', + 'status': 'advertised', + 'ownerRepo': 'Aetheria', + 'advertisementPath': 'web/fixtures/aetheria.provider-advertisement.json', + 'surfaces': ['aetheria.daemon.game'], + 'commands': ['aetheria.daemon.commands'], + 'pluginRequirements': [], + }, + { + 'providerId': 'gamecult.home.vn', + 'status': 'advertised', + 'ownerRepo': 'Sai', + 'advertisementPath': 'web/fixtures/sai-vn.provider-advertisement.json', + 'surfaces': ['sai.visual_novel.surface'], + 'commands': ['story.choose', 'story.continue', 'story.jump'], + 'pluginRequirements': [ + { + 'surfaceId': 'sai.visual_novel.surface', + 'pluginId': 'sai.vn', + 'requiredCapabilities': [ + 'vn.stage', + 'story.choose', + 'story.continue', + 'story.jump', + ], + }, + { + 'surfaceId': 'sai.visual_novel.surface', + 'pluginId': 'norn.graph', + 'requiredCapabilities': ['embed.norn'], + }, + { + 'surfaceId': 'sai.visual_novel.surface', + 'pluginId': 'tex.math', + 'requiredCapabilities': ['embed.tex'], + }, + ], + }, + ], +}; diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index 6553537..19d3bef 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -441,6 +441,15 @@ } ], "proofs": [ + { + "description": "Provider picker consumes provider advertisements without Eve source-layout knowledge.", + "status": "passed", + "evidencePaths": [ + "flutter/eve_parity/lib/main.dart", + "flutter/eve_parity/test/parity_smoke_test.dart", + "tools/parity/run-parity.mjs" + ] + }, { "description": "Conformance pack can be consumed without depending on Eve's local harness layout.", "status": "passed", @@ -452,7 +461,6 @@ } ], "pendingProofs": [ - "Provider picker consumes provider advertisements without Eve source-layout knowledge.", "Flutter build, release, and capture lifecycle can run from EveFlutter." ] }, @@ -617,7 +625,7 @@ "expectedSourceSymbols": [ { "path": "flutter/eve_parity/lib/main.dart", - "contains": ["embeddedDocuments", "surface.slot", "EveEmbeddedDocumentSlot"] + "contains": ["embeddedDocuments", "surface.slot", "EveEmbeddedDocumentSlot", "EveProviderCatalog", "EveProviderPicker"] } ], "commandTransportSmoke": { @@ -668,7 +676,7 @@ "expectedSourceSymbols": [ { "path": "flutter/eve_parity/lib/main.dart", - "contains": ["embeddedDocuments", "surface.slot", "EveEmbeddedDocumentSlot"] + "contains": ["embeddedDocuments", "surface.slot", "EveEmbeddedDocumentSlot", "EveProviderCatalog", "EveProviderPicker"] } ], "commandTransportSmoke": { @@ -756,7 +764,7 @@ "expectedSourceSymbols": [ { "path": "flutter/eve_parity/lib/main.dart", - "contains": ["embeddedDocuments", "surface.slot", "EveEmbeddedDocumentSlot"] + "contains": ["embeddedDocuments", "surface.slot", "EveEmbeddedDocumentSlot", "EveProviderCatalog", "EveProviderPicker"] } ], "commandTransportSmoke": {