brick/docs/architecture/graph-model.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

11 KiB

Graph model

Brick separates analysis from presentation:

analyzers                  canonical model                consumers
grammar / lexical -\
YAMS graph ----------> nodes + typed edges ----------> JSON
Fold source/binary --/    evidence + metrics --------> Cytoscape
future traces ---------------------------------------> Fold/MCP

Compatibility

Schema version 4 makes evidence multiplicity, the selected P4 frontend in scan scope, and the nested P4 metric contract reader-visible. Readers must reject unsupported schema versions instead of guessing. Two models are structurally comparable only when they have the same schema version, canonical scan root, repository identity, and scan scope. The top-level root is absolute execution provenance; comparing artifacts from different clones or worktrees can opt into --allow-root-relocation for structural diffs. This caller assertion relaxes only root equality and is recorded in diff schema 2 alongside both original roots. It does not verify Git identity; the repository node ID is graph-local, not a repository-origin fingerprint. See structural diff.

scope.path is normalized and repository-relative. Each scope.excluded_directories value is one non-empty path component; the array is sorted and deduplicated before serialization. Readers reject absolute, drive-qualified, traversing, unsorted, duplicate, or empty scope values. scope.p4_frontend is lexical or the explicit tree-sitter-p4-pilot; models from those frontends are not comparable because their graph coverage differs. Future scope fields must become part of the compatibility check when they can change graph coverage. The top-level root must be a normalized portable absolute path (/repo, C:/repo, or a normalized UNC root); empty, relative, drive-relative, backslash, trailing separator, traversal, and unnormalized Windows verbatim forms are rejected. The scanner converts \\?\C:\repo to C:/repo and \\?\UNC\server\share to //server/share before serialization.

Compatible buildings can be compared through the separate versioned structural diff contract. It preserves complete added/removed facts and before/after records for shared IDs whose content or evidence changed. See docs/architecture/structural-diff.md. Versioned declarative boundary policy is a separate consumer of canonical dependency edges and never changes their kind or evidence; see docs/architecture/policy.md. The read-only MCP server is another consumer: it validates schema-v4 JSON once, then exposes versioned impact, cycle, bridge, and evidence queries without scanning or renderer coupling; see docs/architecture/mcp.md.

Nodes and identity

Exactly one Repository node is required; missing or ambiguous repository identity fails schema validation. Duplicate node/edge IDs, cross-kind ID collisions, dangling edge endpoints, and edges without evidence also fail schema validation. Repository and directory nodes provide compound layout. File, class, and function nodes represent source structure. External nodes make unresolved dependencies visible instead of dropping them. Finding nodes attach analyzer evidence without changing structural identities.

IDs derive from kind plus repository-relative semantic identity. The repository node uses . as its identity rather than the checkout directory name. Source line is provenance, not identity. Extracted symbols use:

path + semantic owner ID + coarse kind + name + same-name ordinal

The ordinal distinguishes overload-like declarations within one owner. This keeps IDs stable under unrelated line insertions, though inserting a same-name declaration before another remains an explicit lexical limitation.

The default C++/JS/TS/Dart/Rust grammar extractor preserves this identity format for unchanged declarations. Namespace/module scopes additionally contribute to identity and appear in attributes["source.scope"]; ordinals are local to that scope. Rust impl methods use their resolvable type as parent. Fixing a previously incorrect owner intentionally changes the affected method ID. Unresolved targets in these languages include source, language, and scope in identity so unrelated unknowns cannot merge. Legacy Python/Swift/P4 identities remain unchanged.

source.exports records syntax-level export names; it does not assert that a module is reachable under a particular build configuration. Parameter-bound calls retain source.unresolved-reason instead of resolving to a same-named function. Grammar version and extraction revision live in evidence backend strings. Schema 4 remains sufficient: consumers use the same node/edge kinds and evidence contract, with optional opaque attributes. Rebuild baselines when adopting the new extractor so corrected coverage is not mistaken for a source-code change.

P4 support initially retains the coarse display kind only when the exact domain kind is also present in node attributes:

domain = "p4"
p4.kind = "header" | "header-union" | "struct" | "parser" |
          "control" | "table" | "action" | "state" | "extern" |
          "terminal-state" | "unresolved"
p4.dialect = "p4-16"

An architecture attribute is added only when an explicit source or imported contract supplies it; the lexical scanner does not infer v1model from familiar shapes. Parser states and other nested entities use their semantic owner as parent. A consumer must not infer P4 semantics from class or function alone.

Edges and evidence

Contains and defines describe hierarchy. Calls, imports, and inherits describe dependencies and participate in the current SCC/coupling metrics. Flags attaches analysis evidence and does not affect the structural score.

An edge has a stable relationship identity, optional attributes, and an ordered evidence array. The relationship is deduplicated by (source, target, kind); independent call sites and backends add evidence records rather than replacing one another. Evidence ordering is deterministic by lane, backend, provenance, and confidence.

P4 relationships retain their exact domain meaning in edge attributes even when a coarse edge kind is used:

domain = "p4"
p4.relation = "direct-transition" | "select-transition" |
              "select-default" | "apply" | "action-call" |
              "table-action" | "include" | "compiler-diagnostic"

Every evidence record has:

  • lane: syntactic, symbolic, imported, or heuristic;
  • backend: producer and mode;
  • confidence: a producer-local numeric score, categorical label, or null when the producer did not supply one;
  • provenance: source location or report artifact.

Confidence labels from different producers are not calibrated probabilities. Missing or unknown imported values remain unknown or produce diagnostics; they must not be mapped to a convenient backend, lane, or scalar.

Lane classification

Fact Lane
Filesystem containment or declaration syntax Syntactic
Lexical/tree-sitter target resolution without compiler proof Heuristic
External analyzer diagnostic Imported
Result proved within a named bounded model Symbolic

A syntactic call expression, heuristic target resolution, imported diagnostic, and bounded symbolic fact may all support the same relationship. Name resolution never upgrades existing evidence confidence or lane.

Health indicator

The v0.1 score remains deliberately transparent:

Component Weight Meaning
Acyclicity 35% Fraction of symbols outside cyclic SCCs
Resolution 25% Dependency edges not ending at external nodes
Connectedness 25% Symbols participating in dependencies
Concentration 15% Penalty for one target dominating fan-in

The score is experimental. It is a navigation aid, not an architectural quality oracle. Future weights require labeled multi-project evidence and an ablation; they must not be tuned against one repository.

The top-level score remains the original coarse, language-agnostic indicator; its SCC inputs still include calls/imports/inherits, including coarse P4 edges. Consumers that need P4 meaning must use metrics.p4, whose fields are:

Field Canonical observation
parser_state_count / terminal_state_count P4 state and implicit terminal-state nodes
parser_transition_count Transition edges sourced at a parser state
parser_cycle_count / cyclic_parser_state_count SCCs over same-parser direct-transition, select-transition, and select-default edges
unreachable_state_count Non-terminal states not reachable from a recognized start state within the same parser
control_apply_node_count P4 control, table, and action nodes
control_apply_relationship_count apply and action-call edges sourced at those nodes
control_apply_cycle_count / cyclic_control_apply_node_count SCCs over resolved control/table/action apply and action-call edges
unresolved_dependency_count P4 include, transition, apply, action-call, or table-action edges targeting an External node

A repeated apply or action call merges call-site evidence on one directed relationship and is not a cycle without a return path. Accept/reject terminals have no outgoing transition by construction. Unreachable-state diagnostics are prefixed P4 structural observation (not verifier proof) and are recomputed idempotently. Compiler/verifier Flags edges never enter these structural metrics.

P4 compiler evidence

The qualified p4c adapter accepts only Brick envelope schema 1 for p4c v1.2.5.16 p4test-regression diagnostics. It creates Finding nodes and Flags edges with Imported evidence, never Symbolic evidence. Tool version, command mode, result category/code, exit status, raw result identity, artifact identity/provenance, explicit model bounds/confidence category, and source position remain inspectable attributes or provenance.

The adapter first resolves the exact path/kind/name/owner/line P4 entity and falls back to the matching file with a diagnostic. Unknown categories, escaping paths, unsupported tool versions/modes, non-null symbolic bounds, and unqualified symbolic tools fail closed. The complete envelope and real-fixture contract is in docs/architecture/p4-evidence.md.

Fold integration

The current adapter imports SARIF and native findings JSON. A deeper adapter should map Fold function/block/operation/value fact IDs directly to Brick identities and retain source/binary lanes. Source proved, latent, heuristic, and concrete-witness confidence must remain distinguishable. Binary evidence must not be promoted to symbolic evidence without corroboration.

A Fold finding with no backend is diagnosed and not imported. Missing confidence is preserved as unknown with a diagnostic. Duplicate finding IDs merge report provenance into the existing Flags relationship.