zakariaf/flutter-skills

seeded-determinism-and-golden-vectors

- Enforces reproducible derived content — output every device must compute identically from a key, with no server.

Ver código-fonte

Como usar

  1. Copie o comando e verifique o pacote antes de instalar no seu ambiente de agente.
  2. Consulte no original as ferramentas, credenciais e ambientes necessários.
  3. Descreva os dados de entrada e a entrega esperada; confira a saída com as fontes.
npx skills add https://github.com/zakariaf/flutter-skills --skill seeded-determinism-and-golden-vectors

Antes de começar

  • A instalação não inclui assinatura de modelo, franquia API ou acesso aos seus arquivos.
  • Estrelas e instalações indicam atividade, não um teste independente de qualidade ou segurança.
Documento original do Skill

Renderizado do repositório de origem, preservando títulos, exemplos, código, tabelas, links e imagens.

Seeded determinism and golden vectors

Some output must be a pure function of a key: the daily pick every user compares with their friends, a procedurally generated layout, a shareable result someone else can verify. It has to come out identical on every device, in every timezone, on the platform build you ship next year — with no server to arbitrate. That is a stronger property than "the tests pass", and it needs its own discipline plus a frozen oracle to keep it.

testing-strategy owns how you test pure logic (seeded fuzz against an independent oracle, injected Clock). This skill owns what makes the output reproducible in the first place, and the committed vector table that pins it.

A golden vector is not a golden image.* Vectors are data fingerprints for a pure generator, asserted by dart test with no widget binding; reference PNGs of rendered UI belong to widget-golden-and-a11y-testing, and re-blessing them is the separate ritual in run-goldens-rebaseline. The two share only the word "golden" and the rule that a gate must never regenerate what it checks.

Non-negotiable rules

  1. The key is injected, never read. A generator takes its key as an argument — a date string, an

id, a sequence number. Nothing reachable from it calls DateTime.now(). Wall-clock time enters the app exactly once, through the injected Clock at the composition root (value-objects-money-and-units owns that rule). WHY: a generator that reads a clock is untestable and un-golden-able, and its output cannot be reproduced from a bug report.

  1. A calendar-day key is a civil date, not an instant. "The 4th of July" is a date-only value —

store and pass it as one (an ISO YYYY-MM-DD string or a day serial), never as a DateTime instant. An event timestamp is still a UTC instant; a day identity is not a moment in time at all. WHY: an instant makes every read timezone-dependent, so two devices disagree about which content is "today's".

  1. One day definition, used everywhere it is compared. If content is derived from a UTC day while

a streak, deadline, or "already seen" check uses local midnight, the two disagree for hours every day near the boundary. Pick local or UTC deliberately, name it once, and route every comparison through that one definition.

  1. Entropy has exactly one source: a seed you derived. No ambient Random()/Random.secure()

anywhere on the generation path — not even for a "harmless" tiebreak or shuffle. Every draw is an explicit call on the seeded generator you threaded in, and the order of draws is part of the contract: reordering two draws changes every output after it.

  1. Derive the seed with a specified hash and a mix step.* Hash the key string (FNV-1a-64 is

small, specified and easy to reimplement), XOR in a per-feature and per-mode salt, then pass the result through an avalanche/mix step (SplitMix64) before drawing from it. WHY: consecutive keys differ by one byte, and without a mix step that correlation shows up as visibly similar output on consecutive days — the bug users notice first. See references/seed-derivation.md.

  1. Own the PRNG; do not depend on an unspecified one. dart:math's Random(seed) is reproducible

within a given runtime, but its algorithm is an implementation detail with no documented stability guarantee across SDK versions or platforms. Output a user will compare with another user's must not rest on that. SplitMix64 is eight lines; write it, test it against published vectors, and freeze it.

  1. Salt per feature and per mode. Two features seeded from the same date must not draw the same

sequence, or their outputs correlate visibly. One salt constant per feature, frozen forever once shipped.

  1. Regenerate derived content; never store it. Persist the key, the generatorVersion, and

the outcome (what the user did). The content itself is key → generator → content, rebuildable forever. WHY: storing it doubles the source of truth and makes history un-migratable. Derived aggregates are likewise recomputed on read — persistence-drift owns that rule.

  1. A shipped generator is frozen. The moment users have seen output for a key, that output is

what that key means. An improvement ships as a new version with a cutover key, with the old version kept in the binary to reproduce everything before it. Never edit a shipped generator in place: it silently rewrites what the past was, and every stored outcome now refers to content that no longer exists.

  1. Pin the contract with golden vectors. A committed table of (key, params) → fingerprint rows,

plus whatever derived metrics the UI shows, asserted exactly. Store a fingerprint (a stable hash of the canonical serialization), not the whole artifact, so the table stays small and a real change is a one-line diff a reviewer can actually see. == for integers and fingerprints; tolerance only for a deliberately calibrated float.

  1. Compute every expected value from an independent oracle. A fixture generated by the

implementation under test enshrines that implementation's bugs — including the one you are about to introduce. Derive vector rows from a separate, simpler, slower implementation, or by hand. (testing-strategy owns the independent-oracle principle for fuzz loops; this is its frozen-table half.)

  1. Regeneration is explicit, reviewed, and never automatic. Vectors are refreshed only by a

deliberate local command whose diff a human reads in the pull request. CI verifies and never blesses — a pipeline that regenerates its own expectations is a gate that checks nothing, the same failure as running --update-goldens in CI.

  1. Measured, then labeled — never marketed as a proof. Where a generated artifact carries a

derived label (a difficulty, a quality tier, a size class), the metric is computed and the label is a frozen threshold table over it. Assert that the mapping is monotonic and that the thresholds have not moved; do not describe a calibration as proven, and never re-label content that already shipped.

Deriving the seed

dart
/// FNV-1a-64 over the key's UTF-8 bytes. Small, specified, trivially portable.
int fnv1a64(String key) {
  var hash = 0xcbf29ce484222325;
  for (final byte in utf8.encode(key)) {
    hash = (hash ^ byte) * 0x100000001b3;   // 64-bit wrap-around is intended
  }
  return hash;
}

/// The one entropy source. SplitMix64 — eight lines, published test vectors,
/// and yours forever, unlike an SDK-internal generator.
class SeededRng {
  SeededRng(this._state);
  int _state;

  int nextInt64() {
    _state += 0x9e3779b97f4a7c15;
    var z = _state;
    z = (z ^ (z >>> 30)) * 0xbf58476d1ce4e5b9;
    z = (z ^ (z >>> 27)) * 0x94d049bb133111eb;
    return z ^ (z >>> 31);
  }

  /// Uniform in [0, max). Draw order is part of the contract — never reorder.
  int nextInt(int max) => nextInt64().toUnsigned(63) % max;
}

/// The only place the calendar meets a generator. `isoDay` is a civil date
/// ("2026-07-04") supplied by the caller — the generator never reads a clock.
SeededRng seedFor(String isoDay, {required int featureSalt, int modeSalt = 0}) =>
    SeededRng(fnv1a64(isoDay) ^ featureSalt ^ modeSalt);

⚠️ If the app also builds for web, 64-bit integer arithmetic does not behave this way — Dart integers compile to JavaScript doubles there. Either restrict this code to native targets or implement the hash and PRNG in 32-bit halves. See references/seed-derivation.md.

The golden vector table

dart
// One row per pinned key. Computed ONCE from an independent oracle (rule 11) and
// refreshed only by `dart run tool/update_vectors.dart` (rule 12).
const dailyPickVectors = <DailyPickVector>[
  DailyPickVector('2026-01-01', 0x9f3ac1e0, 12, 'year boundary'),
  DailyPickVector('2026-07-04', 0x2b7708d4, 3, 'mid-year'),
  DailyPickVector('2026-12-31', 0x84ce11a9, 7, 'year end'),
];

test('the generator reproduces every frozen vector', () {
  for (final v in dailyPickVectors) {
    final pick = generateDailyPick(v.isoDay);
    expect(pick.fingerprint(), equals(v.fingerprint), reason: v.note);
    expect(pick.items.length, equals(v.itemCount), reason: v.note);
  }
});

Cover the boundaries a bug would hide behind: the first and last key you support, a leap day, a month/year rollover, and at least one historical key so a regression that rewrites the past fails loudly.

Anti-patterns

  • `DateTime.now()` inside a generator or a seed helper — the output stops being reproducible and

cannot be regenerated from a bug report.

  • A `DateTime` instant used as a day identity — every read becomes timezone-dependent.
  • Deriving content from a UTC day while a boundary check uses local midnight — a daily off-by-one

for hours a day, and the hardest kind of bug to see from your own timezone.

  • An ambient `Random()` for a "harmless" tiebreak — one call and the whole contract is gone.
  • Reordering draws during a refactor — every output after the swap changes; draw order is a contract.
  • Seeding straight from the raw hash with no mix step — consecutive days come out visibly similar.
  • Reusing one salt for two features — their outputs correlate.
  • Depending on `Random(seed)`, `identityHashCode`, or hash-set iteration order for anything a user

sees — no documented stability guarantee; sort by an explicit key before drawing.

  • Storing the generated artifact — persist (key, generatorVersion, outcome) and regenerate.
  • Editing a shipped generator in place — it rewrites what past keys meant; ship a versioned cutover.
  • Vectors generated by the implementation under test — they enshrine its bugs.
  • Storing whole artifacts in the vector table — unreviewable diffs; fingerprint them.
  • Auto-regenerating vectors in CI — the gate then verifies nothing.
  • Calling a calibrated label "proven" — the metric is measured; the label is a frozen threshold.

Definition of done

  • [ ] The generator takes its key as an argument; nothing on its path reads a clock or ambient random.
  • [ ] A day key is a civil date, and one day definition is used by every comparison that touches it.
  • [ ] The seed is hash(key) ^ featureSalt ^ modeSalt through a mix step, into a PRNG the repo owns.
  • [ ] Salts are frozen constants, one per feature/mode.
  • [ ] Content is regenerated from (key, generatorVersion); only the key, version and outcome persist.
  • [ ] generatorVersion is stored with every outcome; a generator change ships as a new version with

a cutover, and the previous version stays able to reproduce history.

  • [ ] A committed vector table pins fingerprints plus any user-visible derived metric, covering the

range boundaries, a leap day, and a historical key.

  • [ ] Every expected value came from an independent oracle, not from the implementation under test.
  • [ ] Regeneration is a reviewed local command; CI only verifies.
  • [ ] A determinism test asserts two runs from the same key are byte-identical, so any reintroduced

ambient randomness fails the build.

  • [ ] scripts/check-determinism-bans.sh is green over the generator's package/directory.
  • [ ] Any derived label is a frozen threshold table with a monotonicity test; shipped content is never

re-labeled.

Related skills

  • See testing-strategy for seeded fuzz loops, the independent-oracle principle, and injecting

Clock — this skill's tests are that shape.

  • See value-objects-money-and-units for canonical storage and the injected Clock seam.
  • See persistence-drift for storing the key/version/outcome and recomputing derived aggregates.
  • See project-structure-and-packages for keeping the generator in a Flutter-free package where these

bans are mechanically enforceable.

  • See widget-golden-and-a11y-testing for reference images of rendered UI, and

run-goldens-rebaseline for the ritual that re-blesses them — a different tier from these vectors.

  • See ci-pipeline-and-gates for running the vector check and the ban grep as gates.
  • See dart3-idioms-and-coding-standards for the immutable value types the generator returns.

References

  • Dart API — dart:math Random (seeded construction): https://api.dart.dev/stable/dart-math/Random-class.html
  • Dart — numbers on the web (int as JS double, >>>, overflow): https://dart.dev/guides/language/numbers
  • Dart API — identityHashCode: https://api.dart.dev/stable/dart-core/identityHashCode.html
  • FNV hash specification: https://datatracker.ietf.org/doc/html/draft-eastlake-fnv
  • SplitMix64 (Steele, Lea, Flood — "Fast splittable pseudorandom number generators"): https://doi.org/10.1145/2660193.2660195
  • package:test: https://pub.dev/packages/test

Relacionados por tarefa

do mesmo repositório

Mais Skills

Todos os Skills
zakariaf
Comunidade

accessibility-as-code

Enforces accessibility as a correctness property authored into each widget — Semantics(button/label) or ExcludeSemantics on every node, a11y state read from MediaQuery not app state, never MediaQuery.withClampedTextScaling / textScaleFactor / FittedBox / TextOverflow.ellipsis to fit a label, non-color redundant channels (icon+label+shape+text) for every state, contrast against composited backgrounds (4.5:1 body / 3:1 large), 44px single-tap targets, OrdinalSortKey traversal, and honoring boldTex

instalações
1
GitHub Stars
15
Atualizado
23 de set.
zakariaf
Comunidade

adaptive-layout

Enforces adapting layout by available CONSTRAINTS/size, never device or platform checks — LayoutBuilder + MediaQuery.sizeOf/paddingOf/viewInsetsOf (not .of) for narrow rebuilds, Material 3 window size classes (compact 1200) as the breakpoint vocabulary, navigation affordance chosen by width (NavigationBar → NavigationRail → NavigationDrawer), list-detail single-pane-vs-two-pane, readable max-width via ConstrainedBox, Flexible/Expanded/FractionallySizedBox over fixed widths, SafeArea + display cu

instalações
1
GitHub Stars
15
Atualizado
23 de set.
zakariaf
Comunidade

ads-and-iap-monetization

- Enforces opt-in, rewarded-first monetization over the service seam — a rewarded view grants exactly one benefit and only on a genuine reward outcome (a sealed Rewarded/Dismissed/NoFill), earn loops capped per day against an injected Clock, the earn control preloaded and hidden unless an ad is loaded so a fresh account never dead-ends on "no ads available" (a Guideline 2.1 risk), interstitials only at a natural break under a count-and-elapsed cap owned by the service so callers stay dumb, no ba

instalações
1
GitHub Stars
15
Atualizado
23 de set.
zakariaf
Comunidade

custom-canvas-and-gestures

Enforces CustomPainter/Canvas discipline — the View/Painter/Scene split with a dumb painter fed one immutable Scene value type, shouldRepaint as a single value compare kept strictly separate from the AnimationController-as-repaint animation path, one shared affine transform read by BOTH painter and hit-tester (toCanvas/toLogical exact inverses, never re-derive scale), geometry hit-testing (integer lattice or rasterized region-ID buffer, never Path.contains), zero-allocation paint(), gesture-as-p

instalações
1
GitHub Stars
15
Atualizado
23 de set.