# 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: ```text 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.