From 351c5a82bc4c8e1586c9b08b31ea659ae617b54b Mon Sep 17 00:00:00 2001 From: flemming-it Date: Wed, 22 Jul 2026 22:06:18 +0200 Subject: [PATCH] feat(store): show module maintainers (0.79.0) The detail sheet always answers 'who maintains this?': one selectable line per maintainer from the store index's new maintainers list, or an honest 'not specified' when the index names nobody (StoreMaintainersSection, public for the widget tests). The install trust gate carries the same fact when present and stays terse otherwise. DE+EN; dialog-harness proof captured with the maintainer row. Signed-off-by: flemming-it --- CHANGELOG.md | 8 ++ integration_test/dialog_shots_test.dart | 1 + lib/data/about_info.dart | 2 +- lib/data/hub.dart | 7 ++ lib/l10n/app_de.arb | 3 + lib/l10n/app_en.arb | 3 + lib/l10n/app_localizations.dart | 18 +++++ lib/l10n/app_localizations_de.dart | 9 +++ lib/l10n/app_localizations_en.dart | 9 +++ lib/pages/store.dart | 39 ++++++++++ lib/widgets/chain_install_confirm.dart | 2 + pubspec.yaml | 2 +- test/store_maintainers_test.dart | 97 +++++++++++++++++++++++++ 13 files changed, 198 insertions(+), 2 deletions(-) create mode 100644 test/store_maintainers_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 20c287c..4d95a3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,14 @@ lockstep. ## Unreleased +### Added (0.79.0) + +- **Maintainers in the store.** The detail sheet always answers "who + maintains this?" — one selectable line per maintainer from the + store index, or an honest "not specified" when the index names + nobody. The install trust gate carries the same fact when present. + All bundled seed entries name their maintainer (guarded hub-side). + ### Fixed — honest install badge in the flow editor (0.78.0) - **Install is only offered when the store can deliver.** The diff --git a/integration_test/dialog_shots_test.dart b/integration_test/dialog_shots_test.dart index 25646f8..3fa17a0 100644 --- a/integration_test/dialog_shots_test.dart +++ b/integration_test/dialog_shots_test.dart @@ -69,6 +69,7 @@ StoreItem _storeItem(String verification) => StoreItem( provider: '', source: 'bundled', installVerification: verification, + maintainers: const ['Dr. Stefan Flemming (Flemming.AI) '], ); void main() { diff --git a/lib/data/about_info.dart b/lib/data/about_info.dart index b241df1..71f20cb 100644 --- a/lib/data/about_info.dart +++ b/lib/data/about_info.dart @@ -4,7 +4,7 @@ /// Studio's own build version. Bump on every UI release so the /// running app self-identifies. -const String kStudioVersion = '0.78.0'; +const String kStudioVersion = '0.79.0'; const String kProductName = 'Ch∆In Studio'; const String kVendorName = 'Flemming.AI (F∆I)'; diff --git a/lib/data/hub.dart b/lib/data/hub.dart index 11a0cac..aa04481 100644 --- a/lib/data/hub.dart +++ b/lib/data/hub.dart @@ -938,6 +938,7 @@ class HubService { canonicalCategory: e.canonicalCategory, canonicalCategoryLabel: e.canonicalCategoryLabel, installVerification: e.installVerification, + maintainers: e.maintainers, ), ) .toList(); @@ -2245,6 +2246,11 @@ class StoreItem { /// Human-readable label for [canonicalCategory] (e.g. "Data & Formats"). final String canonicalCategoryLabel; + /// Maintainer display strings from the store index (one per + /// person/organization). Empty when the index does not specify + /// any — the detail sheet renders an honest "not specified". + final List maintainers; + /// How an install of this entry would be verified under the /// hub's CURRENT policy — computed hub-side with the same /// resolvers the install gate enforces, so this can never @@ -2286,5 +2292,6 @@ class StoreItem { this.canonicalCategory = '', this.canonicalCategoryLabel = '', this.installVerification = '', + this.maintainers = const [], }); } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index f976bfc..858dbdb 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -1630,6 +1630,9 @@ "installConfirmNeedsCapabilities": "Benötigte Fähigkeiten", "installConfirmTrustTitle": "Vertrauen & Sicherheit", "installConfirmTrustBody": "Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.", + "storeSectionMaintainers": "Maintainer", + "storeMaintainersNone": "nicht angegeben", + "installConfirmMaintainers": "Maintainer", "storePolicyUnverifiedNotice": "Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.", "verifPillBlocked": "blockiert", "verifPinnedKey": "Signatur wird geprüft — hinterlegter Store-Schlüssel", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 13864d3..c2c3a96 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1654,6 +1654,9 @@ "installConfirmNeedsCapabilities": "Required capabilities", "installConfirmTrustTitle": "Trust & security", "installConfirmTrustBody": "The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.", + "storeSectionMaintainers": "Maintainers", + "storeMaintainersNone": "not specified", + "installConfirmMaintainers": "Maintainers", "storePolicyUnverifiedNotice": "Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.", "verifPillBlocked": "blocked", "verifPinnedKey": "Signature checked — pinned store key", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 9adc9af..ff78b39 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4946,6 +4946,24 @@ abstract class AppLocalizations { /// **'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.'** String get installConfirmTrustBody; + /// No description provided for @storeSectionMaintainers. + /// + /// In en, this message translates to: + /// **'Maintainers'** + String get storeSectionMaintainers; + + /// No description provided for @storeMaintainersNone. + /// + /// In en, this message translates to: + /// **'not specified'** + String get storeMaintainersNone; + + /// No description provided for @installConfirmMaintainers. + /// + /// In en, this message translates to: + /// **'Maintainers'** + String get installConfirmMaintainers; + /// No description provided for @storePolicyUnverifiedNotice. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 2246e2a..8c792fe 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2914,6 +2914,15 @@ class AppLocalizationsDe extends AppLocalizations { String get installConfirmTrustBody => 'Das Modul läuft in einer Sandbox: Es darf nur auf die Netzwerk-Endpunkte, Dateien und Umgebungsvariablen zugreifen, die es selbst deklariert — der Hub setzt diese Liste durch. Die vollständige Berechtigungsliste sehen Sie nach der Installation in den Modul-Details.'; + @override + String get storeSectionMaintainers => 'Maintainer'; + + @override + String get storeMaintainersNone => 'nicht angegeben'; + + @override + String get installConfirmMaintainers => 'Maintainer'; + @override String get storePolicyUnverifiedNotice => 'Die Signaturpflicht ist in der Hub-Richtlinie ausgeschaltet — Installationen werden nicht kryptografisch geprüft. Der Installations-Dialog zeigt den Status je Modul; für geprüfte Installationen security.require_signatures aktivieren.'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index f2edce7..e9a515f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2909,6 +2909,15 @@ class AppLocalizationsEn extends AppLocalizations { String get installConfirmTrustBody => 'The module runs in a sandbox: it may only touch the network endpoints, files, and environment variables it declares itself — the hub enforces that list. The full permission list is visible in the module details after installation.'; + @override + String get storeSectionMaintainers => 'Maintainers'; + + @override + String get storeMaintainersNone => 'not specified'; + + @override + String get installConfirmMaintainers => 'Maintainers'; + @override String get storePolicyUnverifiedNotice => 'Signature enforcement is switched off in the hub policy — installs are not cryptographically verified. The install dialog shows the per-module status; enable security.require_signatures for verified installs.'; diff --git a/lib/pages/store.dart b/lib/pages/store.dart index 14523ee..48f00ab 100644 --- a/lib/pages/store.dart +++ b/lib/pages/store.dart @@ -2686,6 +2686,7 @@ class _StoreDetailSheetState extends State<_StoreDetailSheet> { _DocsPanel(text: _docsResult!.text), const SizedBox(height: ChainSpace.lg), ], + StoreMaintainersSection(maintainers: item.maintainers), if (item.repository.isNotEmpty) ...[ _SectionHeader(l.storeSectionSource), const SizedBox(height: ChainSpace.sm), @@ -3835,6 +3836,44 @@ class _ProvenancePill extends StatelessWidget { } } +/// Who maintains this module — always shown in the detail sheet +/// (decision-maker transparency, same motivation as the About +/// page): one selectable line per maintainer, and an honest +/// "not specified" when the store index names nobody. Public so +/// the widget test pumps both states directly. +class StoreMaintainersSection extends StatelessWidget { + final List maintainers; + + const StoreMaintainersSection({super.key, required this.maintainers}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l = AppLocalizations.of(context)!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SectionHeader(l.storeSectionMaintainers), + const SizedBox(height: ChainSpace.sm), + if (maintainers.isEmpty) + Text( + l.storeMaintainersNone, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ) + else + for (final m in maintainers) + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: SelectableText(m, style: theme.textTheme.bodySmall), + ), + const SizedBox(height: ChainSpace.lg), + ], + ); + } +} + /// Signature-verification warning pill. Same philosophy as the /// provenance pill: the GOOD path (signature checked at install) /// stays quiet, and so does the policy-off case — that one is a diff --git a/lib/widgets/chain_install_confirm.dart b/lib/widgets/chain_install_confirm.dart index 12c0c15..9cca1f2 100644 --- a/lib/widgets/chain_install_confirm.dart +++ b/lib/widgets/chain_install_confirm.dart @@ -112,6 +112,8 @@ class ChainInstallConfirmDialog extends StatelessWidget { : item.source, ), if (item.license.isNotEmpty) (l.installConfirmLicense, item.license), + if (item.maintainers.isNotEmpty) + (l.installConfirmMaintainers, item.maintainers.join(', ')), if (item.status.isNotEmpty) (l.installConfirmStatus, _statusLabel(l)), if (item.requiresServices.isNotEmpty) (l.installConfirmNeedsServices, item.requiresServices.join(', ')), diff --git a/pubspec.yaml b/pubspec.yaml index 2b8c955..2937cb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: chain_studio description: "Ch∆In Studio — desktop GUI for the Ch∆In hub" publish_to: 'none' -version: 0.78.0 +version: 0.79.0 environment: sdk: ^3.11.0-200.1.beta diff --git a/test/store_maintainers_test.dart b/test/store_maintainers_test.dart new file mode 100644 index 0000000..3b7840b --- /dev/null +++ b/test/store_maintainers_test.dart @@ -0,0 +1,97 @@ +// Maintainer transparency in the store (decision-maker finding +// class): the detail sheet always answers "who maintains this?" — +// with the index's list, or an honest "not specified" — and the +// install trust gate carries the same fact when present. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:chain_studio/data/hub.dart'; +import 'package:chain_studio/l10n/app_localizations.dart'; +import 'package:chain_studio/pages/store.dart' show StoreMaintainersSection; +import 'package:chain_studio/widgets/chain_install_confirm.dart'; + +Widget _host(Widget child) => MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale('de'), + home: Scaffold(body: SingleChildScrollView(child: child)), +); + +StoreItem _item({List maintainers = const []}) => StoreItem( + name: 'text.extract', + taglineEn: '', + taglineDe: '', + descriptionEn: '', + descriptionDe: '', + category: 'text', + tags: const [], + requiresCapabilities: const [], + requiresServices: const [], + license: 'Apache-2.0', + repository: '', + bestVersion: '0.1.0', + status: 'alpha', + installed: false, + featured: false, + iconUrl: '', + screenshotUrls: const [], + docsUrl: '', + kind: 'native', + provider: '', + source: 'bundled', + maintainers: maintainers, +); + +void main() { + testWidgets('lists every maintainer on its own selectable line', ( + tester, + ) async { + await tester.pumpWidget( + _host( + const StoreMaintainersSection( + maintainers: [ + 'Jane Doe (Example Org) ', + 'John Roe ', + ], + ), + ), + ); + expect( + find.text('Jane Doe (Example Org) '), + findsOneWidget, + ); + expect(find.text('John Roe '), findsOneWidget); + expect(find.text('nicht angegeben'), findsNothing); + }); + + testWidgets('an empty list renders the honest fallback', (tester) async { + await tester.pumpWidget( + _host(const StoreMaintainersSection(maintainers: [])), + ); + expect(find.text('MAINTAINER'), findsOneWidget); + expect(find.text('nicht angegeben'), findsOneWidget); + }); + + testWidgets('the trust gate names the maintainers when present', ( + tester, + ) async { + await tester.pumpWidget( + _host( + ChainInstallConfirmDialog( + item: _item(maintainers: ['Jane Doe ']), + ), + ), + ); + await tester.pumpAndSettle(); + expect(find.text('Jane Doe '), findsOneWidget); + }); + + testWidgets('the trust gate stays terse without maintainer data', ( + tester, + ) async { + await tester.pumpWidget(_host(ChainInstallConfirmDialog(item: _item()))); + await tester.pumpAndSettle(); + expect(find.text('Maintainer'), findsNothing); + }); +}