Skip to content

Commands validate

albertoodev edited this page Jul 31, 2026 · 3 revisions

validate: structural-mutation gate

spm validate checks whether a mutation differs from its base only in widget-tree structure. Imports, state seeds, rendered content, and compilability must remain unchanged.

This gives generators and benchmark pipelines a static gate before they run a structural variant:

delta(AST structure)  →  delta(buildSpan)

If a mutation changed a dependency, a seed value, or a displayed string at the same time as the widget structure, any measured buildSpan difference would be tangled with a hidden data/logic change. spm validate rejects such pairs before runtime measurement. It is static-only: it never runs the mutation, and everything operates on the resolved AST.

Status: The current command supports single-pair validation and JSON output. It implements import, seed/member, rendered-content, compile, forbidden-construct, and no-op checks. Value-drift checks, batch validation, and feature-delta audits are planned but not yet implemented.

For a code-level walkthrough of how each check inspects the AST, see validate: AST Internals.

Usage

spm validate --base base.dart --mutation mutation.dart

Full surface:

spm validate --base <base.dart> --mutation <mutation.dart>
             [--deps <dependencies.dart>] [--directive <name>]
             [--json] [--strict] [-v]
Flag Meaning
--base, -b Base .dart file (required, must exist).
--mutation, -m Mutation .dart file (required, must exist).
--deps, -d Frozen dependency file. Defaults to dependencies.dart next to --base if present; otherwise no deps file is used.
--directive Mutation-operator name (e.g. all_const). Accepted for future feature-delta audits; currently informational.
--json Emit one machine-readable JSON object to stdout.
--strict Soft violations also fail the pair. No soft checks are emitted by the current implementation.
--verbose, -v Extra progress logging (suppressed in --json mode).

Exit codes

Code Meaning
0 The pair is valid.
1 At least one failing violation, or an internal failure.
64 Usage error (missing/nonexistent file, unknown flag).

What is valid vs. rejected

Valid: structure changed and rendered values stayed the same.

// base                    // mutation
Column(                    Row(
  children: [                children: [
    Text("Total"),             Text("Total"),
    Text("42"),                Text("42"),
  ],                         ],
)                          )

The pair is rejected if a visible value changed (Text("42")Text("43")), or an existing label was duplicated/removed (occurrence count is compared, not just the distinct set).

The checks

Each check appends zero or more Violation{code, severity, detail} records. Current checks are hard (always fail the pair). The detail string is written for a human and for the mutation generator's retry or repair loop.

# Code What it enforces
1 importsChanged The resolved imported-library set (canonical URIs) must match. Reordering or adding an as alias is fine; a genuinely new library is caught even when unreferenced.
2 seedsChanged On the located State class, the initState / dispose / didUpdateWidget bodies and the full field region must be byte-frozen (compared via toSource(), so comments/formatting are invisible).
3 contentDrift Rendered-leaf vocabulary must match: Text / SelectableText / Tooltip(message:) strings (multiset), Icons.* references (set), and AssetImage / Image.asset paths (set).
5 doesNotCompile The mutation must resolve without analyzer error diagnostics. Warnings and lints (e.g. an unused import) do not fail this check.
6 forbiddenConstruct The mutation must not use timing-dependent constructs: async/await, Future/Stream/Timer/Random/Animation*/Ticker*/HttpClient, DateTime.now(), or dart:io/dart:async/dart:isolate/http/dio imports.
7 noOp base.toSource() == mutation.toSource() fails: a mutation must contain a real structural change (comment/reformat-only edits still count as a no-op).

The valueDrift, featureDeltaTangled, and derivationDrift checks are not implemented yet. Their codes already exist in the ViolationCode enum so the JSON schema is stable, but no check emits them yet; featureDelta in the report is currently {}.

Output

Human mode logs each violation through SpmLogger ([severity] code: detail, hard ones to stderr) and ends with a PASS / FAIL summary line.

--json mode writes exactly one JSON object to raw stdout (bypassing SpmLogger, whose [spm]: prefix would break json.loads):

{
  "base": "/abs/path/base.dart",
  "mutation": "/abs/path/mutation.dart",
  "baseInstanceId": "494195be",
  "mutationInstanceId": "655cc792",
  "valid": false,
  "violations": [
    { "code": "contentDrift", "severity": "hard",
      "detail": "rendered text differs: +[Banner] -[Header]" }
  ],
  "featureDelta": {}
}

baseInstanceId / mutationInstanceId are omitted when unknown (e.g. the mutation didn't resolve).

Dependency scope

--deps provides a shared dependency environment for analyzer resolution, so base and mutation resolve against the same mocks, helper classes, and dependency values. The validator does not audit the internals of dependencies.dart; mock values there are assumed externally frozen, and editing that file between runs is outside the validator's current responsibility.

Where this fits

spm validate is useful before benchmarking or profiling a structural variant. A generator, script, or CI job can call spm validate --json --strict, read the violation details, and reject or repair invalid mutations before runtime measurement. Runtime validity checks such as crashes, missing frames, or noisy timing distributions remain outside this static validator.

Clone this wiki locally