Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,20 @@ jobs:
with:
melos-version: 7.8.1

- uses: invertase/github-action-dart-analyzer@v1
with:
fatal-infos: false
fatal-warnings: false

- name: Install dependencies
run: fvm flutter pub get

- name: Build Runner
run: melos run build_runner:build --no-select
timeout-minutes: 5

# Runs what maintainers run: `dart analyze --fatal-infos` over every
# package, after code generation so generated files exist. DCM is the
# other half of `melos run analyze` and is not run here yet; see
# docs/maintainers/analysis-parity.md.
- name: Analyze
run: melos run analyze:dart --no-select

- name: Check Contract Schemas
run: melos run contracts:check --no-select

Expand Down
23 changes: 18 additions & 5 deletions docs/maintainers/adr/0001-runtime-mermaid-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,20 @@ Implemented on `feat/superdeck-mermaid-core`, not as a spike:
- **No Node or mermaid.js in the product.** mermaid.js 12.0.0 was used once,
locally, to record the reference behaviour in §4.2. The export-time
JavaScript fallback (option 3) is closed for this delivery.
- **No web payload measurement yet.** The release web bundle delta is still
unmeasured; see the blockers list in the delivery report.
- **Web payload, measured.** Demo release web build, same machine and SDK,
before and after the swap:

| | before | after | delta |
| --- | --- | --- | --- |
| `main.dart.js` | 3.36 MB | 5.10 MB | **+1.74 MB** |
| `main.dart.js` gzipped | 1.01 MB | 1.63 MB | **+0.62 MB** |
| whole `build/web` | 47.7 MB | 49.5 MB | +1.7 MB |

The tree shaker cannot drop unused diagram families, because
`Mermaid.render()` switches over all 28 — as predicted in §4.5. At
**+0.62 MB compressed** this is inside the 1.5 MB exit criterion agreed
below, so the decision stands; it is the price of the restored families and
it is worth re-measuring on every `mermaid_core` bump.

## 7. Reconsideration triggers

Expand Down Expand Up @@ -337,8 +349,9 @@ Implemented on `feat/superdeck-mermaid-core`, not as a spike:

## 9. Still open

- The release web bundle delta from retaining 28 diagram implementations is
unmeasured. Measure before the next web release; the exit threshold agreed
above was 1.5 MB compressed.
- The vendored parser is a fork of one file. Re-check it on every
`mermaid_core` bump, and delete it when upstream rejects both inputs.
- Web payload is +0.62 MB compressed (§6). Inside the agreed threshold, but
re-measure on every bump; if a future release pushes it past 1.5 MB, the
options are deferred loading of the diagram code or dropping the dependency
for web only.
75 changes: 75 additions & 0 deletions docs/maintainers/analysis-parity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Static analysis: what runs where

Maintainer notes, current as of 2026-09-21.

## The commands

| Command | What it runs |
| --- | --- |
| `melos run analyze` | `analyze:dart` then `analyze:dcm` |
| `melos run analyze:dart` | `dart analyze --fatal-infos` in every package |
| `melos run analyze:dcm` | `dcm analyze . --fatal-style --fatal-warnings` in every package that depends on `dart_code_metrics_presets` |
| `melos run analyze:all` | the above plus `dcm check-unused-files` and `dcm check-unused-code` |

Run `melos run <script> --no-select` in scripts and CI. Without the flag melos
prompts for a package and a non-interactive runner hangs on that prompt.

## What CI runs

The Test workflow ran `invertase/github-action-dart-analyzer` with
`fatal-infos: false` and `fatal-warnings: false`, before `pub get` and before
code generation. That reported strictly less than the local command, on a tree
whose generated files did not exist yet.

It now runs `melos run analyze:dart --no-select` after code generation, which
is exactly the first half of `melos run analyze`.

## The gap that remains: DCM in CI

`analyze:dcm` is not in CI. Running it needs the `dcm` binary on the runner
(`CQLabs/setup-dcm`) and a licence key in repository secrets. Until that is
configured, DCM findings are caught only on developer machines.

Closing it is one job step and one secret; it is not done here because this
change cannot verify a secret it cannot see.

## The other gap: the playground's DCM baseline

`packages/playground/dcm_baseline.json` (written 2026-07-15 with DCM 1.38.0)
suppresses findings that `melos run analyze` would otherwise fail on.

Removing the editor emptied a third of it: 187 suppressed findings across 21
files named code that no longer exists. Those entries were deleted — a stale
entry cannot hide a current finding, so pruning them is safe and it stops the
file from looking like more debt than it is. **468 suppressed findings remain**,
all naming live files.

Measured on 2026-09-21 by moving the baseline aside (630 findings before the
prune, on the tree that still had the editor):

| Finding | Count |
| --- | --- |
| Prefer dot shorthands (enum prefixes, class instantiations, class prefixes) | 272 |
| Missing a blank line before `return` | 161 |
| Member ordering (fields, constructors, private/public methods) | 92 |
| Named arguments do not match declaration order | 33 |
| Avoid inferrable type arguments | 18 |
| Everything else, including 30 warning-level findings | 54 |

`dcm fix` resolves 601 of the 630 automatically. **Do not run it unreviewed:**
on this tree it also rewrote `Provider.of<DeckDocumentStore>(context)` to
`Provider.of(context)` at dependency-injection call sites, and produced one
file that no longer compiled (`genui_conversation_session.dart`, a
"move declaration closer to use" fix leaving a `final` potentially unassigned).
That sweep was tried during this work and reverted.

The 29 findings `dcm fix` cannot fix include real ones worth reading: a field
that is never disposed in `main.dart`, a loop that always exits after one
iteration in `superdeck_a2ui_transport.dart`, a function that always returns
null in `google_schema_adapter.dart`, and a missing `unknown` map entry in
`error_classifier.dart`.

**Recommended order when this is picked up:** fix the 29 by hand first, because
they are the ones that can be defects; then run `dcm fix` package by package
with the diff reviewed; then delete the baseline so the debt cannot grow back
unnoticed.
72 changes: 72 additions & 0 deletions docs/maintainers/first-principles-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# First-principles report: what was done with each finding

Source: the September 2026 first-principles review of `main` at `e719d66b`,
findings F1–F6 and the hardening questions in its §6. Status as of 2026-09-21.

Two of the six describe defects in a component that no longer exists. The
playground's file-backed editor was removed in favour of the Wizard, which
generates a deck, presents it, and writes it only when the reader asks. Those
findings are recorded as retired, with what replaced them, rather than quietly
dropped.

## F1–F6

| ID | The report's finding | Status | Where |
| --- | --- | --- | --- |
| **F1** | Saved Markdown does not retain generated image bytes across restart. | **Applicable — addressed** | A save writes `<name>.md`, `<name>.assets/` and `<name>.deck.json` together. Reopening in a fresh run with an empty memory cache resolves the artwork off disk. `deck_library_durability_test.dart` |
| **F2** | Generation cleanup is not scoped to the saved deck that owns the artwork. | **Applicable — structurally prevented** | Nothing evicts artwork by key any more. Every save creates a new deck ("My talk", then "My talk 2"), so no save can touch an earlier deck's files. A→B→A ownership is asserted over a real filesystem. `deck_library_durability_test.dart`, `mac_os_deck_library_test.dart` |
| **F3** | Undo during an active autosave can leave the latest document unsaved. | **Retired** | Nothing auto-saves. The deck is written from the in-memory document when the reader saves it. The fix and its regression tests remain on `fix/playground-save-safety`, which is not in the review stack. |
| **F4** | A bullet-only slide can be consumed as frontmatter. | **Applicable — fixed** | `fix/front-matter-recognition`: a `- ` line is no longer YAML evidence on its own, and a recognized block that is not a YAML map is reported instead of silently emptied. |
| **F5** | PDF capture does not wait for asynchronous content readiness. | **Applicable — fixed** | `fix/pdf-export-readiness`: one readiness scope per slide, a bounded wait, one more frame, and a failure that names the slide and the asset. Mermaid diagrams participate: a diagram that cannot render fails the wait. |
| **F6** | Route-backed rendering and controller navigation can diverge after deck resizing. | **Applicable — fixed** | `fix/slide-route-authority`: the route is the single authority; `pageBuilder` no longer repairs state; mounted tests assert route, rendered slide, counter and actions together. |

## The report's §6 hardening questions

| Question | Status |
| --- | --- |
| **Crash-safe saving** | Addressed for the only writer left. The deck library writes Markdown, artwork and manifest each through a temporary sibling and a rename, and a failed save removes only what that save created. |
| **External-edit conflicts** | **Retired.** Nothing watches a file, and no file outside the SuperDeck folder is opened, so there is no conflict to have a policy about. |
| **Theme persistence** | Addressed. `<name>.deck.json` carries the theme's catalog id, version and density; reopening resolves it through the catalog and restores it. A selection the catalog no longer offers is reported rather than silently replaced. |
| **Platform support and CI** | Partly. CI now runs the same Dart analysis maintainers run. DCM in CI and the macOS native checks are blocked; see `analysis-parity.md` and "Platform evidence" below. |
| **Resource limits and PDF scope** | Open. Unchanged by this work: the PDF controller still holds every captured image in memory, and raster output is still the only format. |
| **AI release model and security** | Open. Unchanged: compile-time key injection, image generation on for debug and opt-in for release. |

## Platform evidence

**What ran here:** package tests on the pinned SDK (macOS host), the
deterministic generation checkpoint, and the browser smoke suite on Chromium.

**What could not run, and why — not "not run", but blocked:**

- **Any macOS app build.** `packages/playground/macos` and `demo/macos` pin
`MACOSX_DEPLOYMENT_TARGET = 10.15` (`Podfile`: `platform :osx, '10.15'`)
while the Xcode on this machine supports 12.0–27.0. Every macOS target fails
the same way:

> The macOS deployment target 'MACOSX_DEPLOYMENT_TARGET' is set to 10.15,
> but the range of supported deployment target versions is 12.0 to 27.0.x.

This blocks `melos run test:integration:macos` and any manual run of the
Wizard, so the **sandbox and security-scoped bookmark behaviour of saving
and reopening is unproven**. Everything below that layer is proven: the deck
library tests write, list, reopen and resolve artwork on the real macOS
filesystem, faking only the picker and bookmark calls, which cannot run
headless anyway.

Raising the deployment target is a supported-OS decision, deliberately left
as an isolated follow-up rather than changed inside this work.

- **WebKit browser smoke.** `npx playwright install webkit` downloads
`webkit-mac-15-arm64` (Playwright 1.51's build for macOS 15) and fails to
install on this macOS 27.0 arm64 host. Chromium runs and passes; WebKit
needs either a newer Playwright whose WebKit build supports this OS, or a
CI runner on a supported macOS version.

- **Known intermittent check.** `the address bar follows the active slide`
failed once in four Chromium runs at this head (and once before, on the
previous stack, with identical routing code). When it fails, the browser URL
never leaves `/?enable-flutter-web-semantics=true` even though the deck
advanced, which points at Flutter web's route reporting not being installed
yet at the moment of the first navigation, rather than at SuperDeck's route
authority. It is not caused by the Mermaid change, and the test was left
strict. Worth a deliberate wait-for-first-report in the harness.
84 changes: 84 additions & 0 deletions docs/maintainers/renderer-and-assembly-evaluation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Deferred evaluations: shared assembly and the Markdown renderer

Maintainer notes, 2026-09-21. These are the two items the first-principles
report left open after the correctness work (report §4, "Recheck assumptions
where the product has changed").

## 1. Shared slide assembly — evaluated, consolidated

**Question.** The CLI build and the playground's deck codec each assembled
slides from `MarkdownParser`, `SlideOptions`, `SectionParser` and
`CommentParser`. Extract one function, or leave them apart?

**Evidence.** The two were not merely similar, they were the same four calls in
the same order:

| | `DeckBuilder._runBuild` | `DeckMarkdownCodec.decode` |
| --- | --- | --- |
| split | `MarkdownParser().parse(markdown)` | `const MarkdownParser().parse(markdown)` |
| options | `SlideOptions.parse(raw.frontmatter)` | identical |
| sections | `SectionParser().parse(raw.content)` | identical |
| comments | `CommentParser().parse(raw.content)` | identical |
| around it | applies build plugins | wraps failures in `DeckFormatException` |

Empirically, the front-matter work already ran every recognition fixture
through both paths and asserted the same slide count, keys and errors
(`deck_markdown_codec_test.dart`, "front matter recognition parity"), and
rebuilding `demo/slides.md` after that change produced a byte-identical
`superdeck.json`.

**Decision.** Parity is proven, so consolidate: `assembleSlides(String)` now
lives in `packages/builder/lib/src/parsers/slide_assembler.dart` and both
callers use it. The differences above stayed with their callers, which is
where they belong — plugins are a build concern, and the playground's
`DeckFormatException` is an application-facing concern.

**Deliberately not done.** No pipeline framework, no shared "deck service", no
move of filesystem work or build status. The report asked for one small pure
function; that is what exists.

## 2. The Markdown renderer — evaluated, kept

**Question.** `flutter_markdown_plus` renders slide content. Keep, or replace?

**Decision: keep.** Nothing in this delivery depends on changing it, and no
evidence was produced that a replacement is better for SuperDeck's mix of
content. The correctness contracts the report asked for are now protected
around it, which is the condition the report set before reopening this.

**What a replacement would have to prove**, if anyone reopens it — this is the
method, not a result, because the comparison has not been run:

1. Render the same corpus on both renderers: the demo deck, the three fixture
decks under `packages/builder/test/fixtures/real_decks/`, and the docs
examples. Cover lists, tables, code blocks with the repo's syntax
highlighter, images resolved through `AssetCacheStore`, links, blockquotes,
GitHub alerts, selection and semantics.
2. Prove capture parity: thumbnails, `SlideCaptureReadiness` participation and
a PDF export of the same deck, not just an on-screen frame.
3. Measure first-frame and rebuild cost per slide on macOS and web release
builds, on the same decks.
4. Show a concrete defect or missing capability the current renderer cannot
address. A working prototype is not a reason on its own.

**Related, already decided elsewhere.** Diagram rendering is not part of this
question: `docs/maintainers/adr/0001-runtime-mermaid-rendering.md` covers it,
and it is settled.

## 3. Work retired rather than finished

The file-backed editor was removed, and with it the reasons for several items
the report and its remediation plan tracked. Recorded so they are not
rediscovered as gaps:

| Retired | Why it no longer applies |
| --- | --- |
| Auto-save completion invariant (F3) | Nothing auto-saves. A deck is written when the reader saves it, from the document in memory. |
| Save-a-copy of an externally opened file | No file is opened from outside the SuperDeck folder. |
| Crash-safe writes for external files | Same: the only writer is the deck library, which writes new files through a temporary sibling and a rename. |
| External-edit conflict policy | Nothing watches a file, so nothing can conflict with an external edit. |
| Per-deck asset eviction on rebind | A save writes a new deck; it never replaces one, so there is nothing to evict. |

The autosave work still has value as evidence of the defect and the fix; it is
preserved on `fix/playground-save-safety` and `feat/playground-deck-assets`,
which are not part of the review stack.
17 changes: 2 additions & 15 deletions packages/builder/lib/src/build/deck_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import 'dart:async';
import 'package:logging/logging.dart';
import 'package:superdeck_core/superdeck_core.dart';

import '../parsers/comment_parser.dart';
import '../parsers/markdown_parser.dart';
import '../parsers/section_parser.dart';
import '../parsers/slide_assembler.dart';
import 'build_event.dart';
import 'deck_build_plugin.dart';

Expand Down Expand Up @@ -136,18 +134,7 @@ class DeckBuilder {
await _beginBuildPlugins();

final markdownRaw = await store.readDeckMarkdown();
final rawSlides = MarkdownParser().parse(markdownRaw);

final parsedSlides = [
for (final raw in rawSlides)
Slide(
key: raw.key,
options: SlideOptions.parse(raw.frontmatter),
sections: SectionParser().parse(raw.content),
comments: CommentParser().parse(raw.content),
),
];
final slides = await _applyBuildPlugins(parsedSlides);
final slides = await _applyBuildPlugins(assembleSlides(markdownRaw));

await store.saveReferences(slides);
await _finishBuildPlugins();
Expand Down
28 changes: 28 additions & 0 deletions packages/builder/lib/src/parsers/slide_assembler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:superdeck_core/superdeck_core.dart';

import 'comment_parser.dart';
import 'markdown_parser.dart';
import 'section_parser.dart';

/// Builds slide models from deck Markdown.
///
/// This is the whole of stage 1 and 2 parsing: split the document into slides,
/// read each slide's front matter into options, and parse its content into
/// sections and comments. The CLI build and the playground's deck codec both
/// call it, so a deck cannot parse one way on disk and another way on screen.
///
/// Callers add their own concerns around it: the build applies its plugins,
/// and the playground wraps failures in a [DeckFormatException] it can show.
///
/// Throws [FormatException] for front matter that is recognized but invalid.
List<Slide> assembleSlides(String markdown) {
return [
for (final raw in const MarkdownParser().parse(markdown))
Slide(
key: raw.key,
options: SlideOptions.parse(raw.frontmatter),
sections: const SectionParser().parse(raw.content),
comments: const CommentParser().parse(raw.content),
),
];
}
1 change: 1 addition & 0 deletions packages/builder/lib/superdeck_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export 'src/build/deck_builder.dart';

export 'src/parsers/comment_parser.dart';
export 'src/parsers/markdown_parser.dart';
export 'src/parsers/slide_assembler.dart';
export 'src/parsers/section_parser.dart';
export 'src/parsers/slide_serializer.dart';
Loading
Loading