Add EveFlutter provider picker proof

This commit is contained in:
Meta Vens 2026-07-09 01:24:21 +01:00
commit e2e9b21e2f
6 changed files with 235 additions and 4 deletions

View file

@ -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

View file

@ -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

View file

@ -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`.

View file

@ -122,6 +122,126 @@ class EveSurfaceState {
}
}
class EveProviderCatalog {
const EveProviderCatalog({required this.providers});
final List<EveProviderEntry> providers;
factory EveProviderCatalog.fromConformanceExport(Map<String, dynamic> json) {
final providers = ((json['providers'] as List<dynamic>?) ?? const [])
.map((provider) {
return EveProviderEntry.fromJson(provider as Map<String, dynamic>);
})
.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<String> surfaces;
final List<String> commands;
final List<EvePluginRequirement> pluginRequirements;
factory EveProviderEntry.fromJson(Map<String, dynamic> 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<dynamic>?) ?? const [])
.map(
(requirement) => EvePluginRequirement.fromJson(
requirement as Map<String, dynamic>,
),
)
.toList(growable: false),
);
}
}
class EvePluginRequirement {
const EvePluginRequirement({
required this.surfaceId,
required this.pluginId,
required this.requiredCapabilities,
});
final String surfaceId;
final String pluginId;
final List<String> requiredCapabilities;
factory EvePluginRequirement.fromJson(Map<String, dynamic> json) {
return EvePluginRequirement(
surfaceId: (json['surfaceId'] ?? '').toString(),
pluginId: (json['pluginId'] ?? '').toString(),
requiredCapabilities: _stringList(json['requiredCapabilities']),
);
}
}
List<String> _stringList(Object? value) {
return ((value as List<dynamic>?) ?? 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<String> onSelected;
@override
Widget build(BuildContext context) {
return DropdownButton<String>(
value: catalog.findProvider(selectedProviderId) == null
? null
: selectedProviderId,
hint: const Text('Select provider'),
items: [
for (final provider in catalog.providers)
DropdownMenuItem<String>(
value: provider.providerId,
child: Text(provider.providerId),
),
],
onChanged: (value) {
if (value != null) onSelected(value);
},
);
}
}
class EveNode {
EveNode({
required this.id,

View file

@ -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<String>));
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'],
},
],
},
],
};

View file

@ -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": {