Skip to content

fresh

last synced 2026-06-17T22:39:35.514075+00:00 · coverage 85% (graph)

Validation by Beadloom doc_sync — same source as sync-check.

Graph Domain

YAML format for describing the project architecture graph, with loader, diff engine, rule engine, import resolver, linter, snapshot storage, C4 architecture model mapping, and cross-repo federation.

Features and components

Features (each with a SPEC.md):

  • Graph Diff — graph delta engine vs a git ref / snapshot.
  • Rule Engine — architecture-boundary lint (incl. module-coverage).
  • Import Resolver — code-import → edge resolution.
  • C4 Diagrams — graph → C4/Mermaid mapping.
  • Federation — cross-repo export + hub aggregation.
  • Snapshot — point-in-time graph snapshots + compare.

Components (internal building blocks, each with a DOC.md):

  • Graph Loader — YAML → nodes / edges ingestion seam.
  • Contracts — first-class cross-service contract model.
  • SDL — dependency-free GraphQL SDL surface extractor (name-level).
  • GraphQL Surface — typed GraphQL Tier-A surface (field types + nullability/list + args) via the optional graphql-core extra, honest name-level fallback.
  • GraphQL Breaking — native typed GraphQL breaking analysis (names broken consumer references).
  • AMQP Body — strict AMQP message-body JSON-Schema model + native body-diff (names broken consumer-read fields; nested + array depth).
  • AsyncAPI — source-only AsyncAPI ingestion adapter (extracts a message payload JSON-Schema into the internal AMQP body model; honest degradation).

Specification

File Location

The graph is stored in .beadloom/_graph/*.yml. All files with the .yml extension in this directory are loaded during reindex, sorted by name.

YAML Structure

yaml
nodes:
  - ref_id: my-service        # Unique identifier (required)
    kind: service              # Node type (required)
    summary: "Description"     # Brief description (required)
    source: src/my_service/    # Path to source code (optional)
    lifecycle: active          # active|planned|deprecated|dead (optional, default active)
    docs:                      # Linked documents (optional)
      - docs/my-service.md
    # Any additional fields go into extra (JSON)

edges:
  - src: my-service            # Source ref_id (required)
    dst: core                  # Destination ref_id (required)
    kind: part_of              # Edge type (required)
    lifecycle: active          # active|planned|deprecated|dead (optional, default active)
  # A cross-repo edge endpoint uses @<repo>:<ref_id>, e.g. dst: @integration-service:plans

Node Types (node kind)

KindDescription
domainDomain area
featureFeature
serviceService / module
entityData entity
adrArchitecture Decision Record

Edge Types (edge kind)

KindDescriptionBFS Priority
part_ofA is part of B1
touches_entityA touches entity B2
usesA uses B3
implementsA implements B3
depends_onA depends on B4
touches_codeA touches code of B5

The docs Field

An array of paths to documents linked to the node. Paths are specified relative to the project root (e.g., docs/spec.md). During reindex, a doc_path -> ref_id mapping is built to link chunks to graph nodes.

The lifecycle Field (federation)

An optional lifecycle status on each node and edge — one of active (default), planned, deprecated, dead, or external (BDL-038 G7). It is a first-class SQLite column (not stored in extra), so it is type-checked, SQL-queryable, and visible to the rule engine: only active edges count as "live" for the no-dependency-cycles and architecture-layers rules. The federation hub reconciles each edge's lifecycle against reality to produce a three-valued intent-vs-reality verdict. Absent → active; an invalid value is recorded in GraphLoadResult.errors and falls back to active. See the federation SPEC.

external marks a node the author declares as present-but-not-ours — e.g. a native Swift/Kotlin/ObjC++/C++ bridge in modules/. A dependent whose target is external (or an edge that itself declares lifecycle: external) resolves to EdgeVerdict.EXTERNAL / ContractVerdict.EXTERNAL at the hub — never DRIFT. external is the most-significant lifecycle (external > dead > deprecated > planned > active), so a single external endpoint marks the whole contract external. VALID_LIFECYCLES in loader.py accepts it; the DB lifecycle CHECK admits it (SCHEMA v4).

Cross-repo references (federation)

A graph ref may name a node in another repository as @<repo>:<ref_id> (e.g. @integration-service:plans). A plain ref (no leading @) stays local exactly as before. Cross-repo edges are persisted in a dedicated foreign_edges table and resolve at a federation hub via beadloom export / beadloom federate. See the federation SPEC.

GraphQL contracts (federation)

A cross-service contract may declare protocol: graphql (alongside amqp). The producer edge (kind: produces) carries contract: {protocol: graphql, schema: <Name>, source_file: <path-to-schema.graphql>}; at load time the loader parses the referenced SDL (relative to the project root) with the dependency-free sdl.extract_surface extractor and folds the exposed surface — top-level Query/Mutation/Subscription field names plus type/input/enum/interface type names — into the stored contract payload as exposed: [...sorted...]. A missing/unreadable file records exposed: [] plus a GraphLoadResult.warnings entry (honest, never faked). The consumer edge (kind: consumes, often @backend:<Schema>) declares contract: {protocol: graphql, schema: <Name>, references: [op/type names]}, carried through verbatim. Both sides reconcile by contract_key = graphql:<schema> (a name, not a code symbol), so a TS/FSD client and a backend resolve across the language boundary (G3). See the federation SPEC.

Typed Tier-A surface + native breaking verdict (BDL-060 / S2)

When the optional graphql-core extra (beadloom[graphql]) is installed, the loader additionally folds a producer's typed Tier-A surface into the contract payload as a sorted/deduped fields block — each operation field (across queries, mutations, AND subscriptions) carries its return type with nullability (!) and list ([]) wrapping preserved plus its args ({name, type}). The graphql_surface component owns this extraction; absent the extra (or on a malformed SDL) it degrades honestly to the name-level surface (no fields block, no fabricated types). A consumer edge may likewise declare a typed fields block (carried verbatim). The federation export emits contract.fields (sorted via _normalize_contract_surface); older readers tolerate a missing block (additive schema). When both sides are typed, contracts.Contract computes a native breaking verdict (the graphql_breaking component): a consumer-referenced field/arg that is absent, type-narrowed, or nullability-broken (e.g. a non-null arg the consumer doesn't supply, a Plan!Plan narrowing, a dropped/retyped subscription field) → BREAKING, naming the offending field/arg; a purely additive producer change (new optional field, new nullable arg, nullability widening) is benign. Absent typed depth on either side the verdict honestly degrades to the BDL-038 name-presence check. Beadloom computes this verdict itself (native rigor); it does not delegate to an external GraphQL registry/tool.

AMQP message body + native body-diff (BDL-060 / S3)

An AMQP contract edge may declare an optional body — a minimal JSON-Schema (type, properties, required, enum, nested objects, array items) describing the message payload — on the producer (direction: produces) and the consumer (direction: consumes). The loader carries it verbatim; the federation hub normalizes it (properties + required sorted recursively via amqp_body.serialize_body) in _normalize_contract_surface, so an equivalent body declared in a different order is byte-stable on the wire (the determinism invariant). The export schema bump is additive — older readers tolerate a missing body. When both sides carry a body, contracts.Contract computes a native body-diff verdict (the amqp_body component): a consumer-read field that is absent, type-incompatible, required-by-consumer-but-now-optional/removed, enum-narrowed, or nested/array-broken in the producer body → BREAKING, naming the field path (field, parent.child, field[], field[].child); additive producer fields and widened requiredness are benign. Absent a body on either side the AMQP verdict degrades honestly to the name-level both-sides presence check (BDL-038 parity). Teams using AsyncAPI can ingest the message payload JSON-Schema via the source-only asyncapi.extract_payload_body adapter (AsyncAPI 2.x/3.x; honest degradation to None — never a fabricated body); the internal model stays the minimal JSON-Schema body, AsyncAPI is just a source.

Nested landscapes — product vs company (federation, BDL-038 / U5)

An optional landscape provenance names the product a satellite belongs to. It is resolved like repo (resolve_landscape: .beadloom/config.yml landscape: key > the resolved repo name) and beadloom export emits it only when explicitly configured (an undeclared-landscape export keeps the F1 wire shape — no landscape key). beadloom federate then composes either one product-landscape (all satellites share, or none declare, a landscape) or a company-landscape (several). Implicit same-contract_key reconciliation is scoped within a landscape — reconcile_contracts groups by (landscape, contract_key) — so two unrelated products that happen to share a coincidental message_type / schema name reconcile in separate groups and produce zero mutual DRIFT / UNDECLARED / false-CONFIRMED. A genuine cross-product contract is declared with an explicit @otherrepo:<ref> consumer edge: such a key is promoted cross-landscape (one shared group) and still resolves with a both-sides verdict regardless of landscape. An export with no landscape (or one equal to its repo) shares a single default group, so a single-product run is byte-identical to F1. See the federation SPEC.

Landscape gate — federate --fail-on (federation, BDL-039 / F3)

gate_failures(fed, fail_on) -> list[GateFailure] is a pure function over a FederatedGraph: it scans every edge EdgeVerdict and every contract ContractVerdict (matched case-insensitively against the enum value) and returns deterministically-sorted GateFailures (each with a kind edge/contract, an identity, the matched verdict, and for BREAKING the missing GraphQL names). beadloom federate --fail-on <csv> wires it as a CI gate: it writes federated.json/.txt and prints the report first, then exits 1 on any failure (so CI always has the artifact). A bare --fail-on / default uses SAFE_DEFAULT_FAIL_ON (breaking,drift,orphaned_consumer,undeclared_producer + the edge-level undeclared). No false gates (principle 3): NEVER_FAIL_VERDICTS (external/expected/dead/unmapped/confirmed/ok/cleanup_candidate) is disjoint from the default set and is rejected if passed to --fail-on. gate_failure_remediation(failure) -> str | None derives an agent-actionable "how to fix" hint per verdict (BREAKING names the missing surface; ORPHANED_CONSUMER / UNDECLARED_PRODUCER / DRIFT name the contract and the corrective action), printed as a fix: line under each failing verdict. See the federation SPEC.

Agent-actionable violations — Violation.remediation (rule engine, BDL-039 / F3)

Each Violation carries an additive remediation: str | None (default None so existing constructions/tests are unaffected). evaluate_all populates it as a deterministic post-pass via _remediation_for(rule_type, violation), which templates a "how to fix" hint per rule kind: deny/forbid-import → remove/reroute the named import; forbid → remove/reroute the named edge; cycle → break the cycle at a named edge; layer → invert the dependency or extract a shared abstraction; cardinality → split the over-large node; require → add the required edge. The linter surfaces this through --format json (additive remediation key on each violations entry, plus a stable findings array {kind, rule, severity, locations, why, remediation}) and --format github (GitHub Actions ::error/::warning annotations). rich/porcelain are unchanged.

Modules

  • loader.py -- YAML graph parser and SQLite loader. Parses .beadloom/_graph/*.yml files and populates nodes and edges tables. Validates ref_id uniqueness and edge integrity. Supports in-place YAML node updates, written through the atomic-io primitive (write_yaml_atomic: temp file + fsync + atomic os.replace) so an interrupted edit never truncates the source-of-truth *.yml. Cross-repo edge endpoints (@<repo>:<ref_id>) are recorded as ForeignEdges into a dedicated foreign_edges table (surfaced on GraphLoadResult.foreign_edges) for hub resolution rather than treated as dangling-edge errors (F1). For a GraphQL produces contract with a source_file, the loader folds the parsed SDL exposed surface into the stored contract payload (F2 / BDL-038); a missing file records exposed: [] + a warning.
  • diff.py -- Graph delta engine. Compares current on-disk graph YAML against state at a given git ref, or compares a saved snapshot against the current DB state. Detects added, removed, and changed nodes and edges, including source path changes, tag changes, and symbol count deltas. Provides Rich rendering and JSON serialization. See graph-diff SPEC.
  • rules/ -- Architecture rule engine (decomposed by responsibility, BDL-059 S3): types.py (model), loader.py (YAML → typed rules + DB validation), evaluators.py (per-rule-type evaluation), cycles.py (colored-DFS cycle detection + edge-liveness helpers), __init__.py (evaluate_all orchestration + remediation + stable re-exports). Parses rules.yml (schema v1/v2/v3), validates rules against the graph DB, and evaluates deny, require, cycle, import-boundary, forbid-edge, layer, and cardinality rules against code imports, edges, file paths, and node metrics. Supports severity levels (error, warn), tag-based node matching via NodeMatcher, and bulk tag assignments (v3). rule_engine.py remains a thin re-export shim for the prior import path.
  • import_resolver.py -- Multi-language import analysis. Extracts imports via tree-sitter for Python, TypeScript/JavaScript, Go, Rust, Kotlin, Java, Swift, Objective-C, and C/C++. Resolves imports to graph node ref_ids. Generates depends_on edges from resolved imports.
  • linter.py -- Linter orchestrator. Loads rules, optionally runs incremental reindex, evaluates all rules, and returns structured LintResult with violations, counts, and timing. Provides Rich, JSON, and porcelain output formatters.
  • snapshot.py -- Architecture snapshot storage. Saves the current graph state (nodes, edges, symbol counts) to the graph_snapshots table, lists saved snapshots, and compares two snapshots to produce a SnapshotDiff with added, removed, and changed nodes and edges.
  • c4.py -- C4 architecture model mapping. Maps graph nodes and edges to the C4 model (System / Container / Component levels) using part_of depth heuristics or explicit c4_level in node extras. Renders diagrams in Mermaid C4 syntax and C4-PlantUML syntax. Supports level-based filtering (context, container, component) and scoped component views.
  • federation/ -- Cross-repo federation (BDL-037 / F1, BDL-038 / F2), decomposed by responsibility (BDL-059 S3): refs.py (the @<repo>:<ref_id> FederatedRef identity model + parse_ref), export.py (the deterministic satellite exportbuild_export / serialize_export with repo/commit_sha/exported_at provenance), reconcile.py (the hub aggregation aggregate_exportsFederatedGraph with three-valued intent-vs-reality EdgeVerdicts, first-class AMQP + GraphQL contract reconciliation carrying a contract-level ContractVerdict, and per-satellite staleness), gate.py (the landscape gategate_failures / GateFailure / SAFE_DEFAULT_FAIL_ON / NEVER_FAIL_VERDICTS, BDL-039 / F3, that gives those verdicts teeth in CI), and __init__.py (re-exports the full public surface, so from beadloom.graph.federation import X is unchanged). Contract reconciliation + classification is delegated to contracts.py. See federation SPEC.
  • contracts.py -- First-class cross-service contract model (BDL-038 / F2). Owns the Contract / ContractEndpoint model, the protocol-prefixed language-neutral contract_key derivation (AMQP amqp:<exchange>/<routing>:<message_type>, GraphQL graphql:<schema>), the ContractVerdict enum (contract-level intent-vs-reality), and reconcile_contracts (groups AMQP and GraphQL contract-bearing edges into first-class Contracts by key; attaches the producer exposed surface and consumer references onto the Contract — plus, when present, the typed exposed_fields / referenced_fields from the GraphQL fields block and the exposed_body / referenced_body from the AMQP body JSON-Schema; the federation/reconcile.py hub delegates here and projects back to the F1 flat shape via Contract.to_report_dict). The BREAKING verdict is typed when both sides carry depth — for GraphQL the graphql_breaking component (absence / type-narrowing / nullability / arg breaks, subscriptions first-class), for AMQP the amqp_body component (absence / type-incompatible / required-narrowed / nested+array body breaks) — else the BDL-038 name-presence check. See federation SPEC.
  • sdl.py -- Minimal, dependency-free GraphQL SDL surface extractor (BDL-038 / F2). extract_surface(sdl_text) returns the producer's exposed names — top-level Query/Mutation/Subscription field names plus type/input/enum/interface type names — as a set[str]; operation_field_names(sdl_text) returns only the operation field names (the name-level fallback substrate for graphql_surface). Name-presence only (no schema validation); malformed/empty SDL yields set() (recorded honestly as exposed: []).
  • graphql_surface.py -- Typed GraphQL Tier-A surface extraction (BDL-060 / S2, G1a). extract_typed_surface(sdl_text) returns a TypedSurface mapping each operation field (queries/mutations/subscriptions) to its return type (with !/[] wrapping) + args, parsed via the OPTIONAL graphql-core extra; absent the extra (or on a malformed SDL) it degrades honestly to the name-level surface (typed=False, empty types — never fabricated). serialize_typed_surface / parse_typed_surface give a deterministic sorted/deduped wire shape for the federation contract.fields block.
  • graphql_breaking.py -- Native typed GraphQL breaking analysis (BDL-060 / S2, G1a). breaking_field_descriptors(exposed_fields, referenced_fields) names the consumer references the producer breaks (absent / type-narrowed / nullability-broken / arg-broken), "<field>" or "<field>(<arg>)"; additive producer changes are benign. Beadloom computes the verdict itself — no external GraphQL tool. Invoked by contracts.Contract.breaking_fields only when both sides carry a real typed surface.
  • amqp_body.py -- Strict AMQP message-body JSON-Schema model + native body-diff (BDL-060 / S3, G1b). BodySchema / parse_body / serialize_body model the minimal JSON-Schema body (type/properties/required/enum/nested objects/array items) deterministically (sorted, recursively); breaking_body_descriptors(producer_body, consumer_body) names the consumer-read fields the producer body breaks (absent / type-incompatible / required-by-consumer-now-optional / enum-narrowed / nested + array), pathing field / parent.child / field[] / field[].child; additive producer fields + widened requiredness are benign. Invoked by contracts.Contract.body_breaking_fields only when both sides declared a body.
  • asyncapi.py -- Source-only AsyncAPI ingestion adapter (BDL-060 / S3, G1b). extract_payload_body(document, channel=None) extracts a message payload JSON-Schema (AsyncAPI 2.x channels.<ch>.{publish,subscribe}.message.payload / 3.x channels.<ch>.messages.<m>.payload with one-hop $ref into components.messages) into the internal amqp_body model; AsyncAPI is a source only. Reads YAML or JSON via the core yaml.safe_load (no optional extra); an unparseable / payload-less / remote-$ref doc degrades honestly to None (never a fabricated body).

Invariants

  • ref_id must be unique across all YAML files
  • kind (nodes and edges) is a free-form string — Beadloom is paradigm-agnostic, not DDD-only (BDL-038 / U1). The loader and the SQLite schema accept any kind verbatim (no enum, no CHECK), so an FSD project can use page / widget / entity / repository (and FSD-style edge kinds) and they survive export → federate with zero loss or coercion, exactly like DDD domain / service. The federation + contract path (the federation/ package, contracts.py) is fully kind-agnostic and never branches on a DDD kind.
    • The conventional DDD preset kinds — nodes domain / feature / service / entity / adr; edges part_of / depends_on / uses / implements / touches_entity / touches_code (plus the contract kinds produces / consumes) — are the vocabulary the rule engine recognizes in rules.yml matchers (rule_engine.VALID_NODE_KINDS / VALID_EDGE_KINDS): a rule that matches on kind must name one of these (else a rules-load error). This is a constraint on the rule preset's vocabulary, not on the graph: a graph node/edge with a non-preset kind is stored, exported, and federated faithfully — it is never rejected, and beadloom lint does not flag it (only a rule referencing an unknown kind is an error).
  • Edges referencing non-existent local nodes are skipped with a warning
  • Duplicate ref_id values are skipped with an error
  • Rules file supports schema versions 1, 2, and 3
  • Schema v3 adds optional top-level tags: block for bulk tag assignments and tag-based matching in NodeMatcher
  • Rule names must be unique within rules.yml
  • Each rule must have exactly one of: deny, require, forbid_cycles, forbid_import, forbid, layers, or check
  • Rule severity must be one of: error, warn

API

Module src/beadloom/graph/loader.py

  • parse_graph_file(path: Path) -> ParsedFile -- Parse a single YAML graph file into nodes and edges.
  • load_graph(graph_dir: Path, conn: sqlite3.Connection, *, project_root: Path | None = None) -> GraphLoadResult -- Load all *.yml files from a directory into SQLite (two-pass: nodes then edges). Returns GraphLoadResult with nodes_loaded, edges_loaded, errors, warnings. A contract-bearing edge's persisted contract_key is the full protocol-prefixed identity from contracts.contract_key (e.g. amqp:<exchange>/<routing>:<message_type>), so same-name / different-exchange contracts on one node pair stay distinct (BDL-038 / G4); plain edges keep '' (identity (src,dst,kind)). project_root (default: graph_dir's grandparent) anchors a GraphQL produces contract's relative source_file, whose parsed SDL exposed surface is folded into the edge's contract payload.
  • update_node_in_yaml(graph_dir: Path, conn: sqlite3.Connection, ref_id: str, *, summary: str | None = None, source: str | None = None) -> bool -- Update a node's fields in YAML source and SQLite. Returns True if node was found and updated. The YAML writeback is atomic (write_yaml_atomic).
  • get_node_tags(conn: sqlite3.Connection, ref_id: str) -> set[str] -- Extract tags from a node's extra JSON column. Returns an empty set when the node does not exist or has no tags key in its extra data.

Module src/beadloom/graph/diff.py

  • compute_diff(project_root: Path, since: str = "HEAD") -> GraphDiff -- Compare current graph YAML with state at a git ref. Raises ValueError on invalid ref.
  • compute_diff_from_snapshot(conn: sqlite3.Connection, snapshot_id: int) -> GraphDiff -- Compare a saved snapshot (from graph_snapshots table) with the current live state in the nodes and edges tables. Returns a GraphDiff with since_ref set to "snapshot:<id>". Raises ValueError if the snapshot ID is not found.
  • render_diff(diff: GraphDiff, console: Console) -> None -- Render a GraphDiff using Rich console output. Displays source path changes, tag changes, and symbol count deltas for changed nodes.
  • diff_to_dict(diff: GraphDiff) -> dict[str, object] -- Serialize a GraphDiff to a JSON-compatible dict.

Package src/beadloom/graph/rules/

Decomposed by responsibility (BDL-059 S3); see the rule-engine SPEC for the per-module map. src/beadloom/graph/rule_engine.py is a thin backwards-compatible re-export shim for the prior import path. The full public surface is re-exported from beadloom.graph.rules.

  • load_rules(rules_path: Path) -> list[Rule] -- Parse rules.yml and return validated Rule objects (union of DenyRule | RequireRule | CycleRule | ImportBoundaryRule | ForbidEdgeRule | LayerRule | CardinalityRule). Raises ValueError on schema errors.
  • load_rules_with_tags(rules_path: Path) -> tuple[list[Rule], dict[str, list[str]]] -- Parse rules.yml returning both rules and tag assignments from the optional top-level tags: block (schema v3). Returns (rules, tag_assignments) tuple.
  • validate_rules(rules: list[Rule], conn: sqlite3.Connection) -> list[str] -- Validate rules against the database. Returns warning messages for ref_ids not found in nodes.
  • evaluate_deny_rules(conn: sqlite3.Connection, rules: list[DenyRule]) -> list[Violation] -- Evaluate deny rules against code_imports. Supports tag-based matching via get_node_tags().
  • evaluate_require_rules(conn: sqlite3.Connection, rules: list[RequireRule]) -> list[Violation] -- Evaluate require rules against nodes and edges. Supports tag-based matching.
  • evaluate_cycle_rules(conn: sqlite3.Connection, rules: list[CycleRule]) -> list[Violation] -- Evaluate cycle rules using iterative DFS over edges of specified kind(s). Reports each unique cycle once with the full path.
  • evaluate_import_boundary_rules(conn: sqlite3.Connection, rules: list[ImportBoundaryRule]) -> list[Violation] -- Evaluate import boundary rules against code_imports using fnmatch glob patterns on file paths.
  • evaluate_forbid_edge_rules(conn: sqlite3.Connection, rules: list[ForbidEdgeRule]) -> list[Violation] -- Evaluate forbid edge rules against the edges table. Checks source and destination nodes against from_matcher and to_matcher, optionally restricted by edge_kind. Supports tag-based matching.
  • evaluate_layer_rules(conn: sqlite3.Connection, rules: list[LayerRule]) -> list[Violation] -- Evaluate layer rules against the edges table. For enforce: top-down, detects lower-to-upper layer dependencies and optional layer-skip violations when allow_skip=False.
  • evaluate_cardinality_rules(conn: sqlite3.Connection, rules: list[CardinalityRule]) -> list[Violation] -- Evaluate cardinality rules against nodes, code_symbols, file_index, and sync_state. Checks max_symbols, max_files, and min_doc_coverage thresholds for matched nodes.
  • evaluate_all(conn: sqlite3.Connection, rules: list[Rule], *, project_root: Path | None = None) -> list[Violation] -- Evaluate all rules (deny + require + cycle + import boundary + forbid edge + layer + cardinality + unregistered-feature-candidate + module-coverage), enrich each Violation with a deterministic remediation hint, and return violations sorted by (rule_name, file_path or ""). project_root (default: cwd) roots the on-disk module enumeration the module-coverage rule uses.

Module src/beadloom/graph/import_resolver.py

  • extract_imports(file_path: Path) -> list[ImportInfo] -- Extract import statements from a source file using tree-sitter. Supports Python, TS/JS, Go, Rust, Kotlin, Java, Swift, Objective-C, C/C++.
  • resolve_import_to_node(import_path: str, file_path: Path, conn: sqlite3.Connection, scan_paths: list[str] | None = None, *, is_ts: bool = False) -> str | None -- Map an import path to a graph node ref_id via code_symbols annotations or hierarchical source-prefix matching.
  • index_imports(project_root: Path, conn: sqlite3.Connection) -> int -- Scan all source files, index imports into code_imports table, and create depends_on edges. Returns count of imports indexed.
  • create_import_edges(conn: sqlite3.Connection) -> int -- Create depends_on edges from resolved code imports. Returns number of edges created.

Module src/beadloom/graph/linter.py

  • lint(project_root: Path, *, rules_path: Path | None = None, reindex: Callable[[Path], object] | None = None) -> LintResult -- Run the full lint process: load rules, evaluate, return results. reindex is an optional callback (e.g. application.reindex.incremental_reindex) invoked before evaluation so the graph-layer linter stays pure; the CLI injects the application reindex as an orchestration concern. Raises LintError on invalid configuration.
  • format_rich(result: LintResult) -> str -- Format a LintResult as human-readable text with violation markers.
  • format_json(result: LintResult) -> str -- Format a LintResult as structured JSON with violations array and summary.
  • format_porcelain(result: LintResult) -> str -- Format a LintResult as machine-readable one-line-per-violation output.

Module src/beadloom/graph/snapshot.py

  • save_snapshot(conn: sqlite3.Connection, label: str | None = None) -> int -- Save current graph state (nodes, edges, symbol counts) as a snapshot in the graph_snapshots table. Returns the new snapshot ID.
  • list_snapshots(conn: sqlite3.Connection) -> list[SnapshotInfo] -- List all saved snapshots, newest first. Returns a list of SnapshotInfo objects.
  • compare_snapshots(conn: sqlite3.Connection, old_id: int, new_id: int) -> SnapshotDiff -- Compare two snapshots and return a SnapshotDiff with added, removed, and changed nodes and edges. Raises ValueError if either snapshot ID is not found.

Module src/beadloom/graph/c4.py

  • map_to_c4(conn: sqlite3.Connection) -> tuple[list[C4Node], list[C4Relationship]] -- Map architecture graph to C4 model elements. Reads all nodes and edges from the database. Assigns C4 levels using explicit c4_level in node extras (priority) or part_of depth heuristic (depth 0=System, 1=Container, 2+=Component). Returns a tuple of C4 nodes and relationships.
  • render_c4_mermaid(nodes: list[C4Node], relationships: list[C4Relationship]) -> str -- Render C4 model as Mermaid C4 diagram syntax (C4Container). Produces System(), Container(), Component() elements with _Ext/Db variants for external/database nodes. Groups children in System_Boundary() blocks.
  • render_c4_plantuml(nodes: list[C4Node], relationships: list[C4Relationship]) -> str -- Render C4 model as C4-PlantUML syntax. Produces a complete @startuml/@enduml block with !include for the C4-PlantUML stdlib. Uses standard macros: System(), Container(), Component(), Rel() with _Ext/Db variants.
  • filter_c4_nodes(nodes: list[C4Node], relationships: list[C4Relationship], *, level: str = "container", scope: str | None = None) -> tuple[list[C4Node], list[C4Relationship]] -- Filter C4 nodes by diagram level. "context" keeps only System-level and external nodes. "container" keeps System and Container nodes. "component" requires scope and keeps children of the scoped container. Raises ValueError if level="component" without scope, or if scope ref_id is not found.

Package src/beadloom/graph/federation/

Cross-repo identity, satellite export, and hub aggregation — decomposed by responsibility (BDL-059 S3): refs.py (identity model), export.py (satellite export), reconcile.py (hub aggregation → FederatedGraph + verdicts), gate.py (landscape gate). The package __init__.py re-exports the full public surface, so from beadloom.graph.federation import X is unchanged. See the federation SPEC for full detail.

  • parse_ref(raw: str) -> FederatedRef -- Parse a graph ref: plain → local FederatedRef(None, raw); @repo:id → foreign FederatedRef("repo", "id"); malformed @...FederationRefError. Only the first : after @ splits repo from ref_id.
  • is_foreign_ref(raw: str) -> bool -- Cheap leading-@ check (does not validate shape).
  • build_export(conn, *, repo, commit_sha, exported_at, generator, landscape=None) -> dict -- Build the deterministic satellite export artifact (schema v2) from the indexed graph; unions the edges and foreign_edges tables; nodes sorted by ref_id, edges by (src, dst, kind). landscape (BDL-038 / U5) is emitted only when provided (an undeclared-landscape export omits the key — F1 back-compat).
  • resolve_landscape(project_root: Path) -> str -- Resolve the landscape (product) name: .beadloom/config.yml landscape: key > the resolved repo name. The CLI omits it from the export when it equals the repo default.
  • serialize_export(export: dict) -> str -- Serialize an export dict to deterministic JSON (sorted keys, 2-space indent).
  • resolve_repo_name(project_root: Path) -> str -- Resolve the repo name: .beadloom/config.yml repo: > git origin remote basename > directory name.
  • current_commit_sha(project_root: Path) -> str | None -- git HEAD sha, or None when project_root is not the git toplevel (honest "unknown HEAD").
  • aggregate_exports(exports: list[dict], *, now: str | None = None) -> FederatedGraph -- Compose ≥2 satellite exports into one namespaced federated graph: resolve @repo: endpoints, tag each edge with its satellite's landscape, assign an EdgeVerdict per edge, reconcile AMQP + GraphQL contracts into first-class Contracts scoped by (landscape, contract_key) with a contract-level ContractVerdict (sorted by contract_key), record per-satellite staleness + landscape provenance. now injectable for deterministic age. Edge-verdict reconciliation also honours external and unmapped (BDL-038 G7/U4): an edge whose target node is lifecycle: external (or which itself declares external) → EdgeVerdict.EXTERNAL; an edge whose target resolves in the union but is present-without-a-usable-surface (empty summary) → EdgeVerdict.UNMAPPED. Both suppress DRIFT and are kept distinct from unresolved_refs (genuinely-absent foreign targets).
  • serialize_federation(fed: FederatedGraph) -> str -- Serialize a FederatedGraph to deterministic JSON: { schema_version, repos, nodes, edges, contracts, unresolved_refs }.
  • render_federation_report(fed: FederatedGraph) -> str -- Human-readable text report (satellites grouped by landscape with a product/company-landscape label + sha/age, edge-verdict counts, DRIFT list, contract-verdict counts + explicit BREAKING / DRIFT / ORPHANED_CONSUMER / UNDECLARED_PRODUCER call-outs with the missing GraphQL names, unresolved refs).

Constants: EXPORT_SCHEMA_VERSION = 2, FEDERATION_SCHEMA_VERSION = 2 (independent). Export schema v2 (BDL-038 / F2) adds the protocol: graphql contract wire — a producer edge carries contract.exposed (parsed SDL surface), a consumer edge carries contract.references; aggregate_exports / federate still read v1 exports (missing GraphQL fields default to empty). Federation schema v2 (BDL-038 / BEAD-04) enriches each contracts entry with a contract-level verdict (ContractVerdict) plus protocol / contract_key / lifecycle and, for GraphQL, exposed / references / the missing names that triggered BREAKING; F1's flat keys (message_type / directions / repos / confirmed) are KEPT as a subset, and contracts is sorted by contract_key. The bump is on the hub OUTPUT only — the two version bumps are independent.

Module src/beadloom/graph/contracts.py

First-class cross-service contract model (F2). The federation/reconcile.py hub delegates contract reconciliation here. See the federation SPEC.

  • contract_key(payload: dict) -> str -- Derive a protocol-prefixed, language-neutral contract identity: AMQP → amqp:<exchange>/<routing_key>:<message_type> (missing exchange/routing fall back to *, so a v1 message-type-only payload yields amqp:*/*:<message_type> and still reconciles); GraphQL → graphql:<schema>; other → <protocol>:<message_type-or-name>.
  • reconcile_contracts(edges: list[dict]) -> list[Contract] -- Group AMQP and GraphQL contract-bearing edges by (landscape, contract_key) into first-class Contracts (BDL-038 / U5: implicit same-key matching is landscape-scoped so unrelated products never cross-pollute; an explicit @otherrepo: key is promoted cross-landscape into one shared group); accumulates the producer exposed surface and consumer references (sorted + deduped) — plus, for AMQP, the producer's exposed_body and consumer's referenced_body JSON-Schema (normalized) — and folds the most-significant edge lifecycle (external > dead > deprecated > planned > active) onto each Contract, then assigns its verdict via classify. Insertion order preserved (the hub sorts the projected dicts by contract_key). Helpers cross_landscape_keys(edges) / edge_group_key(edge, keys) expose the grouping so the hub's UNDECLARED sweep stays landscape-consistent.
  • classify(contract: Contract) -> ContractVerdict -- Assign the contract-level intent-vs-reality verdict (RFC §5, G5), lifecycle intent first: externalEXTERNAL (BDL-038 G7: a contract whose folded edge lifecycle is external resolves here — never DRIFT), deadDEAD, planned/deprecatedEXPECTED; then shape: GraphQL or AMQP with both sides and a non-empty missing_references (a GraphQL surface break — references ⊄ exposed or the typed verdict — or an AMQP body break) → BREAKING, both sides compatible → CONFIRMED, consumers-only → ORPHANED_CONSUMER, producers-only → UNDECLARED_PRODUCER. Complementary to the edge-level EdgeVerdict.UNDECLARED (an additional projection, not a replacement).

Module src/beadloom/graph/sdl.py

Minimal, dependency-free GraphQL SDL surface extractor (F2). See the federation SPEC.

  • extract_surface(sdl_text: str) -> set[str] -- Return the producer's exposed names: top-level Query/Mutation/Subscription field names plus type/input/enum/interface type names. Name-presence only (no schema validation); empty/whitespace-only/malformed SDL yields an empty set (callers sort it for determinism and record it honestly as exposed: []).

Public Data Classes

ClassModuleDescription
ParsedFileloaderResult of parsing a single YAML file: nodes, edges
GraphLoadResultloaderSummary: nodes_loaded, edges_loaded, errors, warnings, foreign_edges (list of ForeignEdge)
ForeignEdgeloaderFrozen dataclass: src, dst, kind. A cross-repo edge endpoint (@repo:ref_id) recorded at single-repo load time (not inserted, not a dangling error) for hub resolution (F1)
GraphParseErrorloaderException raised when a graph YAML file cannot be parsed; carries the offending path and (when available) source line
NodeChangediffFrozen dataclass: ref_id, kind, change_type, old_summary, new_summary, old_source, new_source, old_tags, new_tags, symbols_added, symbols_removed
EdgeChangediffFrozen dataclass: src, dst, kind, change_type
GraphDiffdiffFrozen dataclass: since_ref, nodes, edges, property has_changes
NodeMatcherrulesFrozen dataclass: ref_id, kind, tag, exclude, method matches(node_ref_id, node_kind, *, tags=None)
DenyRulerulesFrozen dataclass: name, description, from_matcher, to_matcher, unless_edge, severity
RequireRulerulesFrozen dataclass: name, description, for_matcher, has_edge_to, edge_kind, severity
CycleRulerulesFrozen dataclass: name, description, edge_kind (str or tuple), max_depth (default 10), severity
ImportBoundaryRulerulesFrozen dataclass: name, description, from_glob, to_glob, severity. Matches file paths via fnmatch globs against code_imports
ForbidEdgeRulerulesFrozen dataclass: name, description, from_matcher, to_matcher, edge_kind (optional), severity. Forbids graph edges between matched nodes (operates on edges table, unlike DenyRule which checks code_imports)
LayerDefrulesFrozen dataclass: name, tag. Defines a single architecture layer for use in LayerRule
LayerRulerulesFrozen dataclass: name, description, layers (tuple of LayerDef), enforce ("top-down"), allow_skip (default True), edge_kind (default "uses"), severity. Enforces dependency direction between ordered layers
CardinalityRulerulesFrozen dataclass: name, description, for_matcher, max_symbols, max_files, min_doc_coverage, severity (default "warn"). Detects architectural smells via node-level cardinality checks
ViolationrulesFrozen dataclass: rule_name, rule_description, rule_type (deny/require/cycle/forbid_import/forbid/layer/cardinality), severity, file_path, line_number, from_ref_id, to_ref_id, message
SnapshotInfosnapshotFrozen dataclass: id, label, created_at, node_count, edge_count, symbols_count
SnapshotDiffsnapshotFrozen dataclass: old_id, new_id, added_nodes, removed_nodes, changed_nodes, added_edges, removed_edges, property has_changes
ImportInfoimport_resolverFrozen dataclass: file_path, line_number, import_path, resolved_ref_id
LintResultlinterDataclass: violations, rules_evaluated, files_scanned, imports_resolved, elapsed_ms, properties error_count, warning_count, has_errors
LintErrorlinterException raised on invalid lint configuration
C4Nodec4Frozen dataclass: ref_id, label, c4_level ("System" / "Container" / "Component"), description, boundary (parent ref_id or None), is_external, is_database
C4Relationshipc4Frozen dataclass: src, dst, label (edge kind: "uses" / "depends_on")
FederatedReffederationFrozen dataclass: repo (str | None), ref_id; properties is_foreign, qualified (@repo:ref_id or ref_id)
FederationRefErrorfederationValueError raised on a malformed @... foreign ref
EdgeVerdictfederationEnum: OK / DRIFT / EXPECTED / CLEANUP_CANDIDATE / UNDECLARED / DEAD / EXTERNAL / UNMAPPED (intent-vs-reality verdict; EXTERNAL/UNMAPPED suppress DRIFT — BDL-038 G7/U4)
FederatedGraphfederationDataclass: nodes, edges, repos, unresolved_refs, contracts — the composed result of aggregating ≥2 satellite exports
ContractEndpointcontractsFrozen dataclass: repo, ref_id, direction, source_file — one side of a contract (F2)
ContractcontractsDataclass: contract_key, protocol, name, endpoints, lifecycle, verdict, exposed (producer SDL surface), references (consumer-referenced names), and the AMQP exposed_body / referenced_body (the producer's / consumer's declared body JSON-Schema, BDL-060 S3); properties producers / consumers / body_breaking_fields (AMQP body break paths when both sides declared a body) / missing_references (the BREAKING signal — GraphQL consumer references absent from exposed, or the AMQP body break paths); to_report_dict() keeps F1's flat {message_type, directions, repos, confirmed} subset and adds verdict / protocol / contract_key / lifecycle (+ exposed / references / missing for GraphQL, the body break paths under missing for AMQP)
ContractVerdictcontractsEnum: CONFIRMED / DRIFT / ORPHANED_CONSUMER / UNDECLARED_PRODUCER / BREAKING / EXPECTED / EXTERNAL / DEAD (contract-level intent-vs-reality; assigned by classify)

Constraints

  • Files must be valid YAML
  • UTF-8 encoding
  • Only files with the .yml extension (not .yaml)

Testing

Tests: tests/test_graph_loader.py, tests/test_cli_graph.py, tests/test_diff.py, tests/test_diff_enhanced.py, tests/test_cli_diff.py, tests/test_rule_engine.py, tests/test_rule_severity.py, tests/test_cycle_rule.py, tests/test_import_boundary_rule.py, tests/test_linter.py, tests/test_cli_lint.py, tests/test_import_resolver.py, tests/test_import_scan.py, tests/test_symbol_diff_polish.py, tests/test_snapshot.py, tests/test_cli_snapshot.py, tests/test_c4.py, tests/test_graph_federation.py, tests/test_graph_contracts.py, tests/test_graph_sdl.py, tests/test_lifecycle_rules.py, tests/test_export.py, tests/test_federate.py, tests/test_federate_roundtrip_db.py