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

89 lines
4.2 KiB
Markdown

# 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.