vantage 0.2.0
vantage: ^0.2.0 copied to clipboard
An in-app QA & developer-tools HUD for Flutter — device simulation, widget inspection, and an extensible plugin architecture for building QA tooling.
Changelog #
0.2.0 #
Added #
VantageMemoryStorageis exported. It is the defaultVantage.storageand takesseedandenabled, but it was not in the barrel — so the only way to pre-seed QA state, or to force storage on in an internal release-mode QA build, was to write an entireVantageStoragefrom scratch.- RTL characterisation tests for both shipped shell styles.
hudTextDirectionmade RTL reachable and nothing exercised it. The docked bar mirrors correctly (itsRowis direction-aware); the bubble's geometry is deliberately physical, so a bubble the user dragged somewhere stays there regardless of locale. That asymmetry is now pinned rather than merely true.
Fixed after review — enabled, and the resize handle #
enabledwas a construction-time constant that crashed when it changed.initStatereturned early when disabled, leaving the controller, scope and registry — alllate final— unassigned, sofalse -> truethrewLateInitializationError,true -> falsethrew on the scope, and a host disabled after mounting never detached its plugins (leaking everyEventSourcesubscription).disposecompounded it by consulting the current flag. The trio is now nullable, built by_activate, released by_deactivate, driven fromdidUpdateWidget, with teardown keyed off the scope's existence rather than the flag. Pre-existing; found reviewing the work above.- The resize handle was a 24pt touch target — the paint was the hit area, half the Material minimum. The band still paints 24; the box is now 48.
- The resize handle was drag-only. It is now a semantics slider with increase/decrease actions and a focusable node with arrow-key handling, so switch control and keyboards can resize. Drag, semantics and keyboard share one clamp/persist path.
VantageResizablePanelandVantageResizeEdgewere exported by mistake — neither appears in a shell style's public signature. Retracted before release.- The panel's floor now accounts for the handle's extent; a 48pt handle
overflowed a short viewport by squeezing the panel's
TabBarbelow its intrinsic height. - A resize no longer loses the first ~18pt of a drag. Tap-to-focus had put a tap recognizer in the same gesture arena, so the drag had to win it; focus is taken on drag start instead.
- Shell storage keys are versioned (
vantage.shell.v1) — a durable store outlives the code that wrote it and the storage contract has no migration hook. redactorandeventBufferCapacityare now applied live on rebuild; lowering the capacity drops the oldest events immediately.storage,plugins,initiallyOpenandinitialSimulationare documented as mount-only.- Mounting a
Vantageinside anotherVantagenow asserts in debug.Vantage.appBuilderbinds to the nearest host, so the outer one reported its builder missing forever and silently never applied its simulation.
The configuration surface #
The HUD was full of decisions a host could not change. The panel opened at
exactly half the screen, the bubble was a 48pt bug icon, the bar said "Vantage",
the activation gesture was two fingers for 500ms, and the chrome rendered in a
stock ThemeData() in en_US. None of that was configurable without
reimplementing the chrome.
Added — panel geometry ([api-change], approved by the maintainer) #
-
VantagePanelSize—.fraction(f),.pixels(n),.available(), with validation and value equality. -
VantageShellData.panelSize, so a third-partyVantageShellStylereceives the host's choice too, andVantage(panelSize:)to set it. It travels through the sameInheritedWidgetasshellStyle, so a new value takes effect live rather than freezing in the shell route's builder closure..available()is named for what it can promise: on the docked style the panel shares the screen with a 48pt bar, which is subtracted before sizing. Sizing resolves against the safe-area box via aLayoutBuilderinside theSafeArea— resolving againstMediaQuery.sizeovershoots by exactly the safe-area padding, which shows up only on a notched device.
Added — shell appearance #
VantageBubbleShellStyle:bubbleSize,margin,icon,activeIcon,initialAlignment,panelAlignment(the panel can dock to the bottom) andresizable.VantageDockedShellStyle:label,icon,barHeightandresizable. An emptylabelomits the text and its spacing.- Both styles now implement
==/hashCode. The parameterlessconstclasses were getting that from const canonicalisation; adding fields would have quietly taken it away and rebuilt the whole shell on every host build.
Added — drag to resize, and state that survives #
resizable(default off — it adds visible chrome and a new gesture surface) puts a drag handle on the panel's free edge. The dragged height is hydrated synchronously on the first frame and written on drag end only; one write per drag frame would queue dozens of unawaited futures with no ordering between them. The bubble and docked styles remember separate heights, because they negotiate against different boxes.- The bubble's position is now persisted.
Vantage.storage's own dartdoc has always named bubble position as its use case, and nothing had ever written it. - Core-owned shell state is namespaced under
vantage.shell, the same discipline the storage contract asks of plugins.
Added — host options that already existed one layer down #
VantageActivationDetector, VantageEventBusImpl and VantageControllerImpl
all took these; Vantage simply never passed them on, so they were unreachable
defaults rather than choices.
activationPointers,activationHoldDuration,activationEnabled. Disabling the gesture does not disable Vantage — the bubble still opens the HUD.eventBufferCapacity.initiallyOpen,initialSimulation— read once at mount, and documented as such.
Added — HUD chrome environment #
-
hudTheme,hudLocale,hudTextDirection,hudLocalizationsDelegates. The HUD sits above the host'sMaterialAppand so cannot inherit any of these.hudLocaleandhudLocalizationsDelegatesare a pair: the default delegates are English-only, so an unsupported locale leaves the HUD with noMaterialLocalizationsand the panel'sTabBarasserts.
Fixed #
DefaultCupertinoLocalizationsis now among the HUD's delegates. Without it any Cupertino widget in a plugin panel asserted, and a panel cannot borrow the app's delegates from above the app.hudTextDirectionis applied insideLocalizations, which publishes its ownDirectionalityderived from the resolved locale. An override above it is silently discarded.
Compatibility #
Every default is exactly the previous hard-coded value, so a host that passes none of the new options sees no change. No existing test needed editing.
0.1.3 #
Documentation only — no code change.
- The icon had a grey matte and border baked into it. It was rasterised with
macOS
qlmanage -t, which emits a Quick Look thumbnail composited onto an opaque card — every one of its 262,144 pixels was fully opaque and the corners were solid white, so pub.dev's dark theme showed a light square behind the rounded artwork. Replaced withtool/render_icon.dart, a dependency-free rasteriser that evaluates signed-distance coverage natively at each output size and verifies the corner alpha is 0 rather than assuming it. - New mark: two opposed brackets framing a V — an inspection frame around a vantage point.
0.1.2 #
Documentation only — no code change.
- A proper icon, and a README that actually sells the thing: a scannable feature list instead of a wall of prose. The claims are the same ones the tests back; nothing was added to the list that the package does not do.
- Platform badge now states all six, which
0.1.1earned but never advertised.
0.1.1 #
Two defects found by the first integrator to compose every Vantage package at once. Both were in the host widget; no public symbol changed.
The "you forgot appBuilder" banner could accuse a correctly-wired app #
The check was a single post-frame sample that latched: if Vantage.appBuilder
had not run by the end of the first frame, the banner went up and nothing ever
took it down. An async LocalizationsDelegate is enough to trigger that — until
every delegate resolves, Localizations renders an empty Container, so there is
no Navigator and no MaterialApp.builder on the first frame. vantage_a11y's
VantagePseudoLocalizationDelegate declares Future<T> load(Locale) async, which
can never return a SynchronousFuture, so using vantage_a11y made core accuse
the app of a misconfiguration that did not exist.
- The check is now bound by wall clock (a two-second grace), not by a frame or loop count — an app that renders one frame and then idles produces no further frames to count.
- The diagnostic no longer latches:
appBuildermounting at any later point retracts the banner. - The banner is now wrapped in
IgnorePointer. It is aMaterialpinned across the top of the app whoseTexthit-tests opaquely, so it silently swallowed every tap in that band — the integrator lost app-bar buttons to it. It has nothing to tap, and the same message still goes todebugPrint. - It still fires, unchanged, for an app that genuinely never adds the hook.
Vantage.shellStyle did nothing after the first build #
The HUD shell's onGenerateRoute runs once, so the route's builder closed over
whatever shellStyle existed then. Passing a different style to Vantage later
changed nothing on screen, which made bubble-vs-docked — a headline feature — a
construction-time constant. The one-off quickActions list went stale through the
identical closure and is fixed with it.
Both values now travel down the tree in an inherited widget above the HUD's
Navigator instead of into the closure; the route's overlay entries are ordinary
descendants, so they depend on it and rebuild when it changes. The shell route is
still an OverlayRoute — never a ModalRoute, which would contribute a
full-screen ModalBarrier and make the hosted app uninteractable. The tap-through
and single-ModalBarrier regression tests are unchanged and still pass.
0.1.0 #
Redaction keeps the document parseable — approved api-change, 2026-08-01 #
VantageDefaultRedactor now emits a quoted mask where it replaced a quoted
value, so {"password": "hunter2"} redacts to {"password": "[REDACTED]"}
instead of the unparseable {"password": [REDACTED]}. Every text artifact in a
bug bundle goes through this redactor, so the old behaviour handed a developer a
jsonDecode failure on exactly the artifacts that had carried something
sensitive. No public symbol changed — behaviour only.
- Unquoted values are unchanged: form-encoded bodies, query strings and plain
log lines have no quotes to preserve, so
token=abc&page=2still redacts totoken=[REDACTED]&page=2. Pinned by a test, so the fix cannot over-reach. redact()is still idempotent, and the shared corpus is unaffected — it asserts a secret's absence, never the mask's shape.- This closes the item the entry below recorded as "Known and unchanged".
Redaction — approved api-change, 2026-08-01 #
VantageDefaultRedactor guards every contributor's text artifact on the way
into a bug bundle, and a shared corpus found it leaking 13 credential shapes. All
are now covered. No public symbol changed — behaviour only.
- Keys match separator-insensitively, so
accessToken,access_tokenandaccess-tokenare one rule. Dart JSON is overwhelmingly camelCase, so this was the common case, not an edge case. - New key coverage:
sessionId,cvv,cvc,pin,ssn,creditCard,cardNumber,accountNumber,routingNumber,iban,otp,passphrase,credentials,privateKey, and the usual auth headers (Authentication,X-Csrf-Token,X-Amz-Security-Token,X-Firebase-AppCheckand friends). - New shape coverage, for secrets with no key beside them: JWTs (self-
identifying — the header segment always begins
eyJ), credentials embedded in a URL (https://user:pw@host, masking the whole pair), card numbers written in groups (spaces defeat the plain 13–19 digit rule), and US SSNs. - Fixed: the unquoted-value branch swallowed the closing quote of an enclosing
JSON string literal, so redacting a
key=valueshape inside a literal produced unparseable JSON — on exactly the failed-request payloads a tester files a bug about.
Known and unchanged at the time of this entry — superseded by the entry above,
same day: the mask was emitted unquoted, so masking a quoted JSON value yielded
technically invalid JSON. Still best-effort, not a guarantee — the
class dartdoc states what it does and does not catch, and
vantageRedactionCorpus in vantage_network_core pins both.
Core 1.1 — capability batch #
EventSource+ a host-owned merged, timestamped, ring-buffered event bus (VantageScope.eventBus, default capacity 500). RegisteredEventSourceplugins are relayed automatically and stamped with their pluginid.ExportContributor+ an export pipeline with mandatory redaction of text artifacts (VantageScope.buildExport).VantageSensitivity.sensitiveartifacts are dropped unless explicitly requested; text artifacts always pass throughVantageRedactor; plugin ids and filenames are sanitized to one safe path segment each (no traversal); a throwing contributor is isolated and recorded in the manifest.- Scope is stated in the manifest rather than overclaimed:
redacted: truecovers text only, alongsideredactionScope: 'text-artifacts-only'andunredactedBinaryArtifacts.VantageDefaultRedactoris pattern-based best-effort — a safety net over data the plugin already redacted, never the only line of defence. - Email masking is opt-in (
VantageDefaultRedactor(maskEmails: true)) and the pipeline enables it forVantageSensitivity.sensitiveartifacts only, so an ordinary log keeps the addresses a tester needs. - Breaking (experimental API):
ExportContributor.buildExportnow takes aVantageExportContext. Nothing implemented the mixin before, so no plugin is affected.
- Scope is stated in the manifest rather than overclaimed:
VantageScope.captureApp({pixelRatio})— PNG bytes of the app subtree from a core-ownedRepaintBoundaryplaced above the app and inside the simulation. Returns null (never throws) where rasterization is unavailable, before the first paint, or for apixelRatiooutside(0, 8].- The host now detaches every plugin (
onDetach) whenVantageis disposed, so anEventSource's stream controller is released with its plugin. VantageStorage+VantageMemoryStorage: namespaced, debug-only key-value persistence forString/bool/int/double, inert underkReleaseMode. An invalid namespace throwsArgumentError(not an assert), and views nest (a/b/). Core still depends on the Flutter SDK only; a persistent adapter is a later federated package.VantageScope.appRootElementis now populated by the host (same boundary ascaptureApp). No signature change — the getter existed since 1.0; its doc comment now states exactly which element it is (the boundary wrapping the app, whose single child is the app's own root).- Contract-test kit:
expectVantageEventSourceIsBroadcast,expectVantageExportArtifactsSafe, plus four new tests insiderunVantagePluginContractTests(lifecycle-with-scope, broadcast events, bundle-safe artifacts, export-survives-null-capture). src/api/experimental.dartis now a compatibility re-export ofvantage_event.dart+vantage_export.dart.
Slice 1 #
- Initial repository scaffold: pub workspace + melos, strict lints
(
very_good_analysis), CI (format · analyze · api-guard · test), the frozen-contract guard, and the Slice 1 (core shell + plugin API) design spec. - Core shell:
Vantage+Vantage.appBuilder, controller, registry, scope, activation gesture, bubble + docked shell styles, quick actions, contract kit.