brick/docs/architecture/structural-diff.md
trvon 050b1b105b
Some checks failed
build / build-linux (push) Successful in 1m2s
build / build-macos (push) Has been cancelled
build / release (push) Has been cancelled
feat: support explicit cross-worktree structural diffs
2026-09-05 12:34:35 -06:00

4.2 KiB

Structural diff and gates

Brick compares two canonical building models by stable node and edge ID. The comparison is directional: baseline -> current. JSON remains the product boundary; the renderer and layout are not involved.

Compatibility

A diff is computed only after both buildings pass schema validation. By default, Building::ensure_compatible_with requires the following to match exactly:

  • Brick building schema version (currently 4);
  • absolute scan root;
  • stable repository node identity;
  • complete scan scope, including exclusions and selected P4 frontend.

Duplicate node or edge IDs in either input fail closed. Brick does not guess that two different roots, scopes, or frontend selections describe equivalent repositories.

For saved models from corresponding checkout paths, opt in with --allow-root-relocation (requires --baseline). This is a caller assertion of root equivalence, not automatic Git verification. Only root equality is relaxed; all other validation and compatibility checks remain. Source trees need not still exist. The library exposes diff::between_with_policy with ComparisonPolicy::ExplicitRootEquivalence; diff::between remains strict.

Versioned JSON contract

Structural diff JSON has its own diff_schema_version, currently 2, for both strict and relocated comparisons. It records the source building_schema_version, baseline_root, current_root, comparison_policy (strict or explicit-root-equivalence), repository graph identity, and scope. root is retained as an alias of current_root. Consumers must handle version 2 explicitly; the building schema remains 4. It contains exact counts plus six deterministically ID-sorted collections:

  • added_nodes and removed_nodes, containing complete node records;
  • changed_nodes, containing the stable ID and complete before/after nodes;
  • added_edges and removed_edges, containing complete edge records;
  • changed_edges, containing the stable ID and complete before/after edges.

A shared ID is changed when any serialized node or edge field differs. Edge comparison therefore retains evidence lane, backend, producer-local confidence, attributes, and provenance. Evidence changes are not promoted, averaged, or collapsed into a generic changed flag. Top-level node/edge array order does not affect the result.

Checkout directory names remain repository display names, so different basenames produce a changed repository node even for identical source. Absolute imported report provenance also remains visible in changed edges. No input records or IDs are rewritten, and these changes count toward the existing gates.

Metrics and diagnostics are not duplicated into the structural diff. Existing brick check metric gates continue to evaluate the current building directly.

CLI integration

The existing command accepts an optional compatible baseline:

brick check current.json --min-score 70 \
  --baseline baseline.json \
  --diff-json structural-diff.json \
  --max-added-nodes 20 \
  --max-removed-nodes 0 \
  --max-changed-edges 5

Available limits are --max-{added,removed,changed}-{nodes,edges}. Each defaults to unlimited, so adding --baseline alone is inspection-only and does not alter existing score/cycle/unresolved gate behavior. Diff output and diff limits without --baseline are rejected. When requested, complete diff JSON is written before a threshold failure is reported, so the failed gate remains inspectable.

The gate reports all current metric and diff-count failures together. Limits are absolute counts, not percentages or architectural quality claims.

Measurement and limitations

tests/fixtures/diff/ contains a positive stable-ID change and an identical clean negative. tests/benchmarks/diff_runner.py rebuilds baseline and current models at one canonical root and at separate roots, runs all four cases at least three times, checks strict rejection for relocated cases, and retains raw models, diffs, command logs, latency, RSS, Git provenance, and the resolved plan under build/benchmarks/structural_diff/.

This slice does not infer renames: a changed stable identity is one removal and one addition. Declarative architectural policy is a later consumer of these facts and must not retroactively change diff identity semantics.