From 6bcd0efef119813d360ad5ab79ec511eb5cfc86d Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 8 Sep 2026 11:20:52 -0400 Subject: [PATCH 1/3] refactor: give the build status one owner and repair the tooling DeckBuilder now publishes the failure status for a direct build and for a watch build, so the CLI and the watch stream no longer write it themselves. The CLI keeps reporting failures raised before a builder starts. A failed status write no longer replaces the original build error. FileDeckLoader processes the status snapshot it already read, so one cycle cannot emit events for content that arrived after the comparison started. The `brb` and `brbc` aliases call the existing build-runner scripts again, and the unused `mix_lint` command is gone. The test scripts and AGENTS.md now state which command covers the package, desktop, browser, and live generation layers. --- AGENTS.md | 34 ++++- .../builder/lib/src/build/deck_builder.dart | 36 +++++- .../test/src/build/deck_builder_test.dart | 120 ++++++++++++++++++ .../src/build/deck_builder_watch_test.dart | 20 +++ .../cli/lib/src/commands/build_command.dart | 20 +-- .../src/deck/loaders/file_deck_loader.dart | 12 +- .../file_deck_loader_snapshot_test.dart | 89 +++++++++++++ pubspec.yaml | 32 +++-- 8 files changed, 322 insertions(+), 41 deletions(-) create mode 100644 packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart diff --git a/AGENTS.md b/AGENTS.md index cf85268b..f41aecc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,6 @@ Always work inside the FVM-provided SDK (`.fvm/flutter_sdk`) to avoid toolchain fvm dart run melos run analyze # Run dart analyze + DCM analysis fvm dart run melos run analyze:all # Full analysis including unused code/files fvm dart run melos run fix # Apply dart fix + DCM autofixes -fvm dart run melos run custom_lint_analyze # Run custom lint rules ``` ### Code Generation @@ -60,13 +59,36 @@ fvm dart run melos run custom_lint_analyze # Run custom lint rules fvm dart run melos run build_runner:build # Generate code (run before tests) fvm dart run melos run build_runner:watch # Watch mode for development fvm dart run melos run build_runner:clean # Clean generated files +fvm dart run melos run brb # Alias for build_runner:build +fvm dart run melos run brbc # Alias for build_runner:clean ``` ### Testing + +Each command covers one layer. No single command covers every layer. + ```bash -fvm dart run melos run test # Run all tests -fvm dart run melos run test:coverage # Run tests with coverage -fvm flutter test # Run specific test file +fvm dart run melos run test # Package unit and widget tests +fvm dart run melos run test:integration # Desktop integration tests (Linux) +fvm dart run melos run test:integration:macos # Desktop integration tests (macOS) +fvm dart run melos run test:e2e:web # Browser smoke tests (Chromium, WebKit) +fvm dart run melos run test:e2e # Desktop integration + browser smoke +fvm dart run melos run test:all # Package tests + Linux integration tests +fvm dart run melos run test:coverage # Package tests with coverage +fvm flutter test # One test file +``` + +`melos run test` excludes `ci-excluded` suites, and no melos command runs the +live generation tests. Run those from `packages/playground`: + +```bash +# Deterministic checkpoint, no provider call. +fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \ + --dart-define=LIVE_FAKE_CHECKPOINT=true --reporter expanded + +# Live 10-slide smoke test. Skips when GOOGLE_AI_API_KEY is absent. +fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \ + --dart-define-from-file=../../.env --reporter expanded ``` ### Running Apps & Live Debugging @@ -219,6 +241,8 @@ Styles are defined in Dart through `SlideStyler`, `DeckOptions.baseStyle`, and ` | Bootstrap workspace | `fvm dart run melos bootstrap` | | Run all analysis | `fvm dart run melos run analyze` | | Generate code | `fvm dart run melos run build_runner:build` | -| Run tests | `fvm dart run melos run test` | +| Run package tests | `fvm dart run melos run test` | +| Run desktop integration tests | `fvm dart run melos run test:integration:macos` | +| Run browser smoke tests | `fvm dart run melos run test:e2e:web` | | Apply fixes | `fvm dart run melos run fix` | | Clean workspace | `fvm dart run melos run clean` | diff --git a/packages/builder/lib/src/build/deck_builder.dart b/packages/builder/lib/src/build/deck_builder.dart index 09bcbba5..5f4b1b0c 100644 --- a/packages/builder/lib/src/build/deck_builder.dart +++ b/packages/builder/lib/src/build/deck_builder.dart @@ -81,11 +81,7 @@ class DeckBuilder { final slides = await build(); yield BuildCompleted(slides.toList()); } catch (e, stackTrace) { - await store.saveBuildStatus( - phase: DeckBuildPhase.failure, - error: e, - stackTrace: stackTrace, - ); + // [build] already published the failure status for this build. yield BuildFailed(e, stackTrace); } } @@ -103,7 +99,37 @@ class DeckBuilder { return buildFuture; } + /// Runs one build and publishes its status. + /// + /// This is the single owner of the build status, so a direct build and a + /// watch build report a failure the same way. The original build error + /// always reaches the caller, even when the status write fails too. Future> _build() async { + try { + return await _runBuild(); + } catch (error, stackTrace) { + await _publishBuildFailure(error, stackTrace); + rethrow; + } + } + + Future _publishBuildFailure(Object error, StackTrace stackTrace) async { + try { + await store.saveBuildStatus( + phase: DeckBuildPhase.failure, + error: error, + stackTrace: stackTrace, + ); + } catch (statusError, statusStackTrace) { + _logger.warning( + 'Could not record the failed build status.', + statusError, + statusStackTrace, + ); + } + } + + Future> _runBuild() async { _logger.info('Starting build...'); await store.initialize(); await store.saveBuildStatus(phase: DeckBuildPhase.building); diff --git a/packages/builder/test/src/build/deck_builder_test.dart b/packages/builder/test/src/build/deck_builder_test.dart index c38cc108..d2a68faf 100644 --- a/packages/builder/test/src/build/deck_builder_test.dart +++ b/packages/builder/test/src/build/deck_builder_test.dart @@ -496,6 +496,90 @@ Discuss release plan. ); }); + test('publishes failure status for a direct build', () async { + const markdown = '# First Slide\n\nOriginal content'; + final builder = DeckBuilder( + workspace: workspace, + store: store, + plugins: [ + _TransformPlugin( + id: 'test.direct-failure', + transform: (_, _) => throw StateError('transform failed'), + ), + ], + ); + + await workspace.slidesFile.writeAsString(markdown); + await expectLater(() => builder.build(), throwsA(isA())); + + final status = await _readBuildStatus(workspace); + expect(status.phase, DeckBuildPhase.failure); + expect(status.error?.message, contains('transform failed')); + }); + + test('a later successful build replaces the failure status', () async { + const markdown = '# First Slide\n\nOriginal content'; + var shouldFail = true; + final builder = DeckBuilder( + workspace: workspace, + store: store, + plugins: [ + _TransformPlugin( + id: 'test.recovering', + transform: (block, _) { + if (shouldFail) throw StateError('transform failed'); + + return block; + }, + ), + ], + ); + + await workspace.slidesFile.writeAsString(markdown); + await expectLater(() => builder.build(), throwsA(isA())); + expect((await _readBuildStatus(workspace)).phase, DeckBuildPhase.failure); + + shouldFail = false; + await builder.build(); + + final status = await _readBuildStatus(workspace); + expect(status.phase, DeckBuildPhase.success); + expect(status.slideCount, 1); + expect(status.error, isNull); + }); + + test('keeps the build error when the status write also fails', () async { + const markdown = '# First Slide\n\nOriginal content'; + final failingStore = _FailingStatusStore( + workspace: workspace, + failOn: DeckBuildPhase.failure, + ); + final builder = DeckBuilder( + workspace: workspace, + store: failingStore, + plugins: [ + _TransformPlugin( + id: 'test.status-write-failure', + transform: (_, _) => throw StateError('transform failed'), + ), + ], + ); + + await workspace.slidesFile.writeAsString(markdown); + + await expectLater( + () => builder.build(), + throwsA( + isA().having( + (error) => error.toString(), + 'message', + contains('transform failed'), + ), + ), + ); + expect(failingStore.failureWriteAttempts, 1); + }); + test('preserves DeckFormatException thrown by plugins', () async { const markdown = '# First Slide\n\nOriginal content'; final builder = DeckBuilder( @@ -643,3 +727,39 @@ final class _FailingReferenceStore extends DeckBuildStore { await onSaveReferences(); } } + +Future _readBuildStatus(DeckWorkspace workspace) async { + final decoded = + jsonDecode(await workspace.buildStatusJson.readAsString()) + as Map; + + return DeckBuildStatus.fromJson(decoded); +} + +/// Store that refuses to record one build phase. +final class _FailingStatusStore extends DeckBuildStore { + _FailingStatusStore({required super.workspace, required this.failOn}); + + final DeckBuildPhase failOn; + var failureWriteAttempts = 0; + + @override + Future saveBuildStatus({ + required DeckBuildPhase phase, + int? slideCount, + Object? error, + StackTrace? stackTrace, + }) async { + if (phase == failOn) { + failureWriteAttempts++; + throw const FileSystemException('Cannot write the build status.'); + } + + return super.saveBuildStatus( + phase: phase, + slideCount: slideCount, + error: error, + stackTrace: stackTrace, + ); + } +} diff --git a/packages/builder/test/src/build/deck_builder_watch_test.dart b/packages/builder/test/src/build/deck_builder_watch_test.dart index a27a1a5e..50916431 100644 --- a/packages/builder/test/src/build/deck_builder_watch_test.dart +++ b/packages/builder/test/src/build/deck_builder_watch_test.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:superdeck_builder/src/build/build_event.dart'; @@ -73,6 +74,25 @@ void main() { ); }); + test('publishes failure status for a watch build', () async { + await workspace.slidesFile.writeAsString('# Slide\n\n@column\n'); + + final builder = DeckBuilder(workspace: workspace, store: store); + final iterator = StreamIterator(builder.watchAndBuild()); + addTearDown(iterator.cancel); + + await _nextEvent(iterator); + expect(await _nextEvent(iterator), isA()); + + final decoded = + jsonDecode(await workspace.buildStatusJson.readAsString()) + as Map; + final status = DeckBuildStatus.fromJson(decoded); + + expect(status.phase, DeckBuildPhase.failure); + expect(status.error, isNotNull); + }); + test('emits started and completed for a rebuild cycle', () async { final builder = DeckBuilder(workspace: workspace, store: store); final iterator = StreamIterator(builder.watchAndBuild()); diff --git a/packages/cli/lib/src/commands/build_command.dart b/packages/cli/lib/src/commands/build_command.dart index 8c3177f1..d5317be3 100644 --- a/packages/cli/lib/src/commands/build_command.dart +++ b/packages/cli/lib/src/commands/build_command.dart @@ -72,7 +72,8 @@ class BuildCommand extends SuperDeckCommand { /// Runs the build process with proper error handling and progress reporting. /// /// Uses the provided [builder] for the build, or creates a new one if not - /// provided. + /// provided. The builder owns the build status, so this method only reports + /// the failure to the console. Future _runBuild( DeckBuildStore store, DeckWorkspace workspace, { @@ -113,31 +114,16 @@ class BuildCommand extends SuperDeckCommand { progress.fail('Build failed'); logger.err('File system error: ${e.message}'); logger.err('Path: ${e.path ?? 'Unknown'}'); - await store.saveBuildStatus( - phase: .failure, - error: e, - stackTrace: .current, - ); return false; } on FormatException catch (e) { progress.fail('Format error'); logger.err(e.message); - await store.saveBuildStatus( - phase: .failure, - error: e, - stackTrace: .current, - ); return false; } catch (e, stackTrace) { progress.fail('Build failed'); _logBuildFailure(e, stackTrace); - await store.saveBuildStatus( - phase: .failure, - error: e, - stackTrace: stackTrace, - ); return false; } finally { @@ -284,6 +270,8 @@ class BuildCommand extends SuperDeckCommand { return ExitCode.success.code; } catch (e, stackTrace) { + // Only failures raised before a builder runs are reported here. Once a + // builder exists it publishes its own failure status. logger.err('Build failed before the deck could be generated.'); _logBuildFailure(e, stackTrace); await store?.saveBuildStatus( diff --git a/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart b/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart index fac26f30..4ec67cb3 100644 --- a/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart +++ b/packages/superdeck/lib/src/deck/loaders/file_deck_loader.dart @@ -62,14 +62,18 @@ class FileDeckLoader extends DeckLoader { return _statusFile.readAsString(); } - Future _processStatus(Completer cancel) async { - if (!await _statusFile.exists()) { + /// Emits the events for one already-read status snapshot. + /// + /// The caller passes the snapshot it compares later, so the loader cannot + /// process content that the build replaced after that comparison started. + Future _processStatus(String? snapshot, Completer cancel) async { + if (snapshot == null) { _emitMissingBuildOutput(cancel); return; } try { - final decoded = jsonDecode(await _statusFile.readAsString()); + final decoded = jsonDecode(snapshot); if (decoded is! Map) { _emit( SlidesErrorEvent( @@ -187,7 +191,7 @@ class FileDeckLoader extends DeckLoader { } final statusAtStart = await _readStatusSnapshot(); - await _processStatus(cancel); + await _processStatus(statusAtStart, cancel); if (!_isCycleActive(cancel)) return; final changed = await _waitForStatusChange( diff --git a/packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart b/packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart new file mode 100644 index 00000000..5a8ab567 --- /dev/null +++ b/packages/superdeck/test/src/deck/loaders/file_deck_loader_snapshot_test.dart @@ -0,0 +1,89 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:superdeck/superdeck.dart'; +import 'package:superdeck_core/superdeck_core.dart'; + +/// Status file whose reads follow a script, so the loader can be observed +/// while the build replaces the status between two reads. +class _ScriptedStatusFile implements File { + _ScriptedStatusFile(this._delegate, this._contents); + + final File _delegate; + final List _contents; + var reads = 0; + + @override + String get path => _delegate.path; + + @override + Directory get parent => _delegate.parent; + + @override + Future exists() => _delegate.exists(); + + @override + Future readAsString({Encoding encoding = utf8}) async { + final index = reads < _contents.length ? reads : _contents.length - 1; + reads++; + + return _contents[index]; + } + + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +String _statusJson(String status, int seq) => + '{"status":"$status","timestamp":"2026-03-10T10:00:0$seq.000Z"}'; + +void main() { + test('processes the status snapshot it compares against', () async { + final tempDir = await Directory.systemTemp.createTemp( + 'superdeck_loader_snapshot_', + ); + addTearDown(() async { + if (await tempDir.exists()) await tempDir.delete(recursive: true); + }); + + final workspace = DeckWorkspace(projectDir: tempDir.path); + await workspace.superdeckDir.create(recursive: true); + await workspace.deckJson.writeAsString('[]'); + final realStatusFile = workspace.buildStatusJson; + await realStatusFile.writeAsString(_statusJson('building', 1)); + + // The second read returns the finished build, which is what an + // unprotected second read inside one cycle would observe. + final scriptedStatusFile = _ScriptedStatusFile(realStatusFile, [ + _statusJson('building', 1), + _statusJson('success', 2), + ]); + + final events = []; + await IOOverrides.runZoned( + () async { + final deckLoader = FileDeckLoader(workspace: workspace); + final subscription = deckLoader.load().listen(events.add); + final deadline = DateTime.now().add(const Duration(seconds: 3)); + while (events.length < 3 && DateTime.now().isBefore(deadline)) { + await Future.delayed(const Duration(milliseconds: 20)); + } + await subscription.cancel(); + await deckLoader.dispose(); + }, + // `File(path)` inside this callback would consult the same override + // again, so unrelated paths resolve in the root zone. + createFile: (path) => path == realStatusFile.path + ? scriptedStatusFile + : Zone.root.run(() => File(path)), + ); + + expect(events, hasLength(greaterThanOrEqualTo(3))); + expect(events[0], isA()); + // The first cycle must emit the snapshot it read, not a later one. + expect(events[1], isA()); + expect(events[2], isA()); + }); +} diff --git a/pubspec.yaml b/pubspec.yaml index 1e5a034e..145982ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,17 +115,24 @@ melos: test: run: fvm dart run melos exec -c 1 -- fvm flutter test --exclude-tags ci-excluded - description: Run flutter tests excluding explicit golden and flaky suites. + description: >- + Run the package unit and widget tests. Excludes `ci-excluded` suites, + desktop integration tests, browser smoke tests, and live generation + tests. packageFilters: dirExists: test test:integration: run: cd demo && fvm dart run superdeck_cli:main build && fvm flutter test integration_test/all_tests.dart -d linux --fail-fast --timeout 2m - description: Run flutter integration tests on Linux (CI default) + description: >- + Run the demo desktop integration tests on Linux, which is the CI + default device. test:integration:macos: run: cd demo && fvm dart run superdeck_cli:main build && fvm flutter test integration_test/all_tests.dart -d macos --fail-fast --timeout 2m - description: Run flutter integration tests on macOS (local) + description: >- + Run the demo desktop integration tests on macOS, which is the local + default device. test:e2e:web:prepare: run: cd demo && fvm dart run superdeck_cli:main build && fvm flutter build web --release @@ -133,15 +140,19 @@ melos: test:e2e:web: run: fvm dart run melos run test:e2e:web:prepare && cd demo/e2e && npm ci && npx playwright install --with-deps chromium webkit && npm run test:smoke - description: Run Playwright smoke tests for the demo web build on Chromium and WebKit. + description: >- + Run the browser smoke tests for the demo web build on Chromium and + WebKit. test:e2e: run: fvm dart run melos run test:integration && fvm dart run melos run test:e2e:web - description: Run full desktop integration and web smoke E2E coverage. + description: Run the desktop integration tests and the browser smoke tests. test:all: run: fvm dart run melos run test && fvm dart run melos run test:integration - description: Run all tests (unit + integration) + description: >- + Run the package tests and the Linux desktop integration tests. Does not + run the browser smoke tests or the live generation tests. test:coverage: run: fvm dart run melos exec -- fvm flutter test --coverage @@ -154,10 +165,9 @@ melos: description: Clean all packages brb: - run: fvm dart run melos run gen:build:superdeck + run: fvm dart run melos run build_runner:build + description: Short alias for `build_runner:build`. brbc: - run: fvm dart run melos run gen:clean - - custom_lint_analyze: - run: fvm dart run melos exec --depends-on=mix_lint -- fvm dart run custom_lint + run: fvm dart run melos run build_runner:clean + description: Short alias for `build_runner:clean`. From cc55ed31a965ebbae90d53969e3cec46419e1bbb Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 8 Sep 2026 14:57:43 -0400 Subject: [PATCH 2/3] docs: retain only unfinished Markdown review work --- docs/generation_and_rendering_review_plan.md | 90 ++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 docs/generation_and_rendering_review_plan.md diff --git a/docs/generation_and_rendering_review_plan.md b/docs/generation_and_rendering_review_plan.md new file mode 100644 index 00000000..01a7e9c6 --- /dev/null +++ b/docs/generation_and_rendering_review_plan.md @@ -0,0 +1,90 @@ +# Remaining Markdown review work + +Updated September 8, 2026. The generation and runtime cleanup is implemented +in the PR stack below. The Markdown renderer comparison and parser assembly +review remain outstanding in this workspace, so this plan is still needed. + +This replaces the original broad review proposal with its remaining work. +The separate renderer handoff contains the detailed architecture and source +context. No Markdown implementation change is part of the cleanup stack. + +## Completed cleanup + +| Area | Implementation | +| --- | --- | +| Capture teardown, thumbnail pruning, transition ownership, and CI for stacked PRs | [PR #108](https://github.com/conceptadev/superdeck/pull/108) | +| Shared generation application, cancellation and document/file revision guards, service setup, and editor notices | [PR #109](https://github.com/conceptadev/superdeck/pull/109) | +| Build-status ownership, loader snapshots, script aliases, and test-command documentation | [PR #110](https://github.com/conceptadev/superdeck/pull/110) | + +These changes retain the existing package boundaries and generation models. +Generation has passed the deterministic checkpoint and a live 10-slide run +with three artworks, 18.778 seconds of generation time, and ten captured slides. +That validates one generation run; it does not establish renderer parity or +replace the separate interactive authoring checks. + +## 1. Establish the Markdown behavior contract + +Inventory the behavior supplied by the existing renderer and each custom +builder. Start with the +[Markdown showcase](../packages/core/test/fixtures/markdown/github_web_markdown_showcase.md) +and existing parser, codec, widget, and capture tests. + +Cover headings, paragraphs, inline formatting, nested lists, tasks, tables, +alerts, blockquotes, fenced code, Mermaid, images, custom widgets, Unicode, +text scaling, and overflow. Define intended link activation, selection, and +inline-image behavior before changing those features. + +Deliver a feature-to-test matrix that distinguishes current behavior from +approved changes. Reuse existing fixtures and assertions. + +## 2. Compare rendering the existing AST with ordinary widgets + +Keep the Dart `markdown` parser and the current `flutter_markdown_plus` +implementation as the working baseline. Begin the separate comparison with +paragraphs, headings, and nested alerts rendered from existing parsed nodes. +Preserve Mix styling, inline formatting, Hero behavior, asset resolution, and +syntax configuration. Do not introduce another AST or a permanent renderer +selection framework for the experiment. + +Compare the maintained code each approach needs, parser invocations during +style changes, cold/warm render cost, transition frames, capture completion, +and memory for representative 10- and 20-slide decks. Use matching SDK, fonts, +assets, devices, build modes, and cache conditions. + +A successful text prototype is not a complete renderer. Before selecting or +extending it, evaluate lists, tables, code, blockquotes, images, semantics, +links, and selection on macOS and web. Investigate another widget package only +if the comparison leaves a specific requirement unresolved. + +Deliver a keep, simplify, or replace decision supported by screenshots, +interaction results, measurements, and maintenance cost. A replacement needs +an identifiable benefit and coverage of every affected contract. + +## 3. Review shared deck assembly + +The CLI builder and Playground codec still assemble slide models separately +from the same deck parsers. Compare both paths before plugins run, using the +same inputs: options, sections, comments, widget arguments, slide identity, +fenced directives, malformed edits, and error diagnostics. + +If parity supports consolidation, extract one small pure assembly function in +`builder`. Keep filesystem access, plugins, status reporting, and each caller's +recovery behavior with their current owners. Preserve serializer normalization +and slide-identity contracts. This is a separate parser change from selecting +a content renderer. + +## Validation and retirement + +Use the layer-specific commands in [AGENTS.md](../AGENTS.md). Regenerate code, +run analysis and contract checks, then run affected package, macOS integration, +and Chromium/WebKit tests. Capture output is part of renderer acceptance. + +For each Markdown change, verify the complete authoring path: CLI build, +opening a deck, editing, invalid-edit recovery, saving/reloading, navigation, +and thumbnail/PDF capture. Keep `fvm flutter run` attached during manual checks +and inspect its logs after interactions. Run live generation again when its +behavior or model configuration changes. + +Retire this plan when the renderer decision and parser consolidation have +been implemented or explicitly declined, with the decisions and validation +recorded in their PRs or maintained architecture documentation. From c2eb121f4ab5b6deb9110011a2734cb8fcdb0622 Mon Sep 17 00:00:00 2001 From: Leo Farias Date: Tue, 8 Sep 2026 15:21:27 -0400 Subject: [PATCH 3/3] docs: align agent guidance with workspace commands --- AGENTS.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index f41aecc5..c3edb7e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,7 +9,7 @@ This file provides guidance to Claude Code and other AI assistants working on th SuperDeck is a Flutter presentation framework that renders slides written in Markdown. Users write slides in a `slides.md` file using Markdown syntax with custom block annotations, and SuperDeck renders them as a Flutter application. - **Live demo**: https://superdeck-dev.web.app -- **Repository**: https://github.com/btwld/superdeck +- **Repository**: https://github.com/conceptadev/superdeck ## Project Structure @@ -17,20 +17,24 @@ This is a Melos monorepo with the following packages: ``` packages/ - core/ # Rendering primitives, Markdown parsing, schema validation (Dart-only) + core/ # Shared deck models, schemas, Markdown utilities, storage contracts (Dart-only) superdeck/ # Flutter widgets and presentation components - cli/ # superdeck CLI tool (setup, build, publish, version) - builder/ # Code generators and build_runner integration + cli/ # superdeck CLI tool (setup, build/watch, version) + builder/ # Deck parsing, serialization, and build/watch pipeline (Dart-only) + playground/ # Flutter authoring app, editor, and AI generation + plugins/pdf/ # PDF export plugin demo/ # Sample presentation app docs/ # User-facing documentation (MDX format) ``` ### Key Package Responsibilities -- **core**: Markdown processing, slide/block configuration, shared model/schema validation, YAML utilities (no Flutter dependency) +- **core**: Shared Markdown utilities, slide/block configuration, model/schema validation, storage contracts, and YAML utilities (no Flutter dependency) - **superdeck**: Flutter widgets, DeckController, navigation, thumbnail/capture services, theme system - **cli**: CLI commands for project setup and building slides -- **builder**: build_runner generators for code generation +- **builder**: Markdown deck parsing, serialization, build/watch orchestration, and build plugins +- **playground**: Deck authoring, editor/file sessions, AI generation, and theme customization +- **plugins/pdf**: PDF capture and export UI ## Environment Setup @@ -72,25 +76,31 @@ fvm dart run melos run test # Package unit and widget tests fvm dart run melos run test:integration # Desktop integration tests (Linux) fvm dart run melos run test:integration:macos # Desktop integration tests (macOS) fvm dart run melos run test:e2e:web # Browser smoke tests (Chromium, WebKit) -fvm dart run melos run test:e2e # Desktop integration + browser smoke +fvm dart run melos run test:e2e # Linux integration + browser smoke fvm dart run melos run test:all # Package tests + Linux integration tests fvm dart run melos run test:coverage # Package tests with coverage fvm flutter test # One test file ``` -`melos run test` excludes `ci-excluded` suites, and no melos command runs the -live generation tests. Run those from `packages/playground`: +`melos run test` excludes `ci-excluded` suites; `test:coverage` includes them. +No melos command runs the live generation tests. Run those from +`packages/playground`: ```bash # Deterministic checkpoint, no provider call. fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \ --dart-define=LIVE_FAKE_CHECKPOINT=true --reporter expanded -# Live 10-slide smoke test. Skips when GOOGLE_AI_API_KEY is absent. +# Live 10-slide smoke test; requires the repository-root .env file. fvm flutter test test_live/ai_generation/ai_generation_smoke_test.dart \ + --dart-define=LIVE_FIXTURE=superdeck_demo_10 \ --dart-define-from-file=../../.env --reporter expanded ``` +The live cases skip when the supplied defines contain no `GOOGLE_AI_API_KEY`. +A missing define file fails before the tests start. Omitting `LIVE_FIXTURE` +selects the default small-fixture suite rather than the 10-slide checkpoint. + ### Running Apps & Live Debugging ```bash cd packages/playground @@ -126,7 +136,7 @@ fvm dart run melos run clean # Clean all Flutter build artifacts - Two-space Dart indentation - `snake_case.dart` filenames - Prefer relative imports over package imports -- Avoid exporting from entry-point files +- Avoid importing the package's own entry-point file internally; import the defining file - Keep widgets focused; colocate private helpers with their widget - Run `melos run fix` before committing @@ -181,7 +191,7 @@ lib/src/ - Unit tests live under each package's `test/` directory - Always regenerate code before running tests - Add regression tests with bug fixes -- CI blocks merges on failing analyze/test jobs +- Require passing analysis and relevant test checks before merging ## Commit Guidelines