comparison_report library

What a comparison wrote down, typed — the reader for index.json.

Every fw compare writes its whole verdict to index.json: every preview entry and every scenario step, each with what the four channels found — pixels, widget tree, visible texts, app events — and where its two frames are. This library reads that back so a project's tool/ script — a pull-request gate, a screenshot uploader, a custom comment — is a few lines over typed classes rather than a map walk written once per project:

  • ComparisonReport.read takes an exported page's directory and hands back the ComparisonIndex with a way to open the frames it names.
  • ComparisonIndex.fromJson takes the file, or the object fw compare --json prints, when only the verdict is wanted.
  • ComparisonIndex.findings is the verdict in the order a reader wants it: worst first, both halves merged, the rows that are neither same nor skipped. ComparisonIndex.ok is the gate.
  • ComparisonIndex.verdictGap says when there is no verdict to gate on — a harness that would not build, a half of nothing but both-sides failures — which ok alone cannot tell apart from real regressions. fw compare exits 1 on exactly this sentence.
var report = await ComparisonReport.read('build/comparison/report/web');
if (report.index.ok) return;
for (var finding in report.index.findings) {
  print('${finding.state.name} ${finding.half.name} ${finding.id}');
  if (finding.preview?.shots case var shots?) {
    if (report.frame(shots.head) case var png?) {
      await service.upload(png, finding.id);
    }
  }
}
exitCode = 1;

This is published API. A field renamed here breaks somebody's script, which is why comparisonReportVersion exists and why ComparisonIndex.fromJson refuses a major it does not know rather than handing back a half-decoded object. Added fields do not bump the version — an older reader ignoring a new key is the behaviour that makes adding one cheap.

One vocabulary, several careers, exactly as scenarios_report.dart argues: fw compare builds these classes, writes them, the studio's own panel renders them, the exported page parses them in a browser, and a consumer's script reads them here. What is not here is what never reaches the file — the runner's live frames and the step aligner — because a shape that cannot be written cannot be read, and publishing it would freeze a runner internal.

Plain Dart on purpose — nothing in the model may import package:flutter, and nothing may import dart:io: a consumer's script runs under a bare dart run, and the exported comparison page parses this very model in a browser. The disk-facing reader lives in report_io.dart, and test/comparison/purity_test.dart fails the build if either rule is broken.

Classes

BranchDelta
A whole branch that exists on one side only.
ChannelDelta
One difference, on whichever channel found it.
ComparedFinding
One row worth attention, with the half it came from.
ComparedHalf
What one half of a comparison did.
ComparedItem
One thing compared, on every channel that had something to say.
ComparisonHost
The machine a comparison ran on — what a reader needs before deciding a run that took three times its usual length, or a picture drawn by a software rasterizer, is about the branch.
ComparisonIndex
A whole index.json, read back.
ComparisonPhase
One timed phase of a comparison.
ComparisonReport
A written comparison, read back.
ComparisonTimings
Where one comparison's time went — index.json's timings.
DiffRect
One region that changed.
EventChannel
What the app did on the way to a step, compared.
EventDelta
One field of one event that moved.
FoldedDelta
One shape of difference, and how much of a comparison wore it.
FrameRef
Where a frame is, and how to read it.
PixelChannel
PixelDiff
Two frames compared as pixels.
ScenarioComparison
One scenario's two runs, compared.
StepIds
Hands out a flow's step ids, each of them once.
TextChannel
The visible text, which diffs exactly and costs nothing.
TreeChannel
TreeDelta
One thing that is not the same about the two trees.
TreeDiff
Why the pixels differ, in words.

Enums

ComparedHalfKind
Which half a row came from.
ComparedState
Declared in the order a report ranks them: the top row should be the thing most likely to be a mistake.
ComparisonFrames
Where the frames a report names actually are.
EventDeltaKind
What kind of disagreement one delta is.
ExportedFrames
Which of a report's frames were written beside it.
TreeDeltaKind
Declared in the order a report ranks them.

Constants

comparisonReportFile → const String
What a comparison writes its whole verdict to, in the cache and inside an export alike.
comparisonReportVersion → const int
The format index.json is written in.
maxEventDeltas → const int
How many field deltas one item's events channel keeps.

Functions

comparedIdIn(String package, String id) → String
How a row is addressed when a comparison spans several packages.
foldChannelDeltas(Iterable<List<ChannelDelta>> perItem) → List<FoldedDelta>
Groups deltas by their shape, keeping the order they were built in.
isComparedFinding(ComparedState state) → bool
Whether state is worth a reader's attention.
rankComparedFindings({List<ComparedItem> previews = const [], List<ScenarioComparison> scenarios = const []}) → List<ComparedFinding>
Every row worth attention, worst first, both halves merged.
verdictGapOf({String? scenariosNote, String? previewsNote, Iterable<ComparedState> scenarioStates = const [], Iterable<ComparedState> previewStates = const [], int inconclusiveScenarios = 0, bool narrowed = false}) → String?
Why a comparison's verdict is incomplete, or null when it is whole.