Skip to content

Repository files navigation

cfdb — code facts database

A local-first, deterministic fact base for Rust workspaces. cfdb walks a Cargo workspace, extracts structural facts (crates, modules, items, fields, call sites, entry points, concepts, visibility, cfg gates) into a typed node/edge graph, and lets you query that graph with either a Cypher subset or a fluent Rust builder API.

It is a library first and a CLI second. Every verb the cfdb binary exposes is a function call in cfdb-query / cfdb-petgraph / cfdb-extractor — the binary is a wire form, not the system of record.

What it is for

  • Architecture ban rules as declarative queries. Replace handwritten Rust architecture tests (test_no_utc_now_outside_tests, test_no_f64_in_domain, test_no_reqwest_client_new) with Cypher files checked into .cfdb/queries/. Run them in CI; any row is a violation.
  • Canonical-bypass detection. Identify call sites that go around a canonical resolver — reachable, unreachable, dead, and caller-scoped variants — without writing custom Rust each time.
  • Vertical split-brain detection. Find entry points from which two divergent resolver-shaped items reach the same concept under different names.
  • Signature divergence. Find function signatures that drift from a declared canonical shape across a workspace.
  • Unresolved and resolved call graphs. Two extractors coexist: a fast syn-based name-level extractor (v0.1) and a rust-analyzer-HIR-backed resolver extractor (v0.2, feature-gated). Both emit into the same schema; the :CallSite.resolver discriminator ("syn" vs "hir") lets queries mix or partition them.
  • Workspace inventory. List items by name pattern, group by bounded context, enumerate entry points, describe the schema.
  • Recall gating. cfdb-recall measures the extractor against cargo public-api / rustdoc --output-format json ground truth so the fact base cannot silently under-report.

If you have ever written a walkdir + regex audit script against a Rust codebase, cfdb is the structured replacement.

The cfdb / graph-specs duo

cfdb is one half of a paired toolchain:

  • cfdb — the X-ray. Detect existing drift in a Rust workspace. Answers "what is there, and what of it violates a rule?"
  • graph-specs-rust — the vaccine. Block new drift at PR time against declared specs in specs/. Vendors cfdb as a pinned git dep and consumes its fact stream.

The two are developed in lockstep (RFC-033 cross-dogfood). cfdb's SchemaVersion is the wire contract between them; a bump in cfdb requires a matching fixture bump in graph-specs (see docs/cross-fixture-bump.md). Either tool is useful on its own — the duo lets you combine retrospective (cfdb) and preventive (graph-specs) enforcement on the same codebase.

Install

cfdb is not (yet) published to crates.io. Use it as a git path dep or from a checkout.

git clone https://github.com/yannickgranger/cfdb
cd cfdb
cargo build --release -p cfdb-cli
# binary is target/release/cfdb

Minimum supported Rust version: 1.85.

The cfdb-hir-extractor crate pins ra-ap-* crates at exact versions (see docs/ra-ap-upgrade-protocol.md). HIR support is feature-gated so cfdb-cli's default build does not pull the full rust-analyzer compile tree.

CLI quickstart

# 1. Extract facts from a Cargo workspace into a keyspace.
cfdb extract --workspace /path/to/your/project --db .cfdb/db --keyspace myproj

# 2. Run a Cypher ban rule. Empty output = clean.
cfdb violations --db .cfdb/db --keyspace myproj --rule examples/queries/arch-ban-utc-now.cypher

# 3. Ad-hoc query.
cfdb query --db .cfdb/db --keyspace myproj \
  'MATCH (i:Item) WHERE i.kind = "fn" AND i.is_test = false RETURN i.qname LIMIT 20'

# 4. List all callers of a symbol.
cfdb list-callers --db .cfdb/db --keyspace myproj --qname '.*::now$'

# 5. Dump sorted canonical JSONL (determinism-checkable).
cfdb dump --db .cfdb/db --keyspace myproj > facts.jsonl

# 6. Describe the schema.
cfdb schema-describe

The full verb surface (20 verbs across INGEST / RAW / TYPED / SNAPSHOT / SCHEMA) is documented in the cfdb --help output and in the module docs of crates/cfdb-cli/src/main.rs.

Library quickstart

cfdb's crates are usable as a library without the CLI. Three patterns:

A. Fluent builder → in-process evaluator

use cfdb_core::{ItemKind, StoreBackend};
use cfdb_petgraph::PetgraphStore;
use cfdb_query::builder::Query;

let mut store = PetgraphStore::new();
cfdb_extractor::extract_workspace("/path/to/proj", &mut store, "myproj")?;

let query = Query::match_node("i", "Item")
    .where_eq("i.kind", ItemKind::Fn)
    .where_eq("i.is_test", false)
    .return_prop("i", "qname")
    .limit(20)
    .build();

let result = store.execute(&query, "myproj")?;
for row in result.rows {
    println!("{}", row.get("i.qname").unwrap());
}

B. Cypher parser → same AST → same evaluator

use cfdb_query::parser::parse_query;

let query = parse_query(r#"
    MATCH (i:Item)
    WHERE i.kind = "fn" AND i.is_test = false
    RETURN i.qname LIMIT 20
"#)?;

let result = store.execute(&query, "myproj")?;

Both surfaces produce identical cfdb_core::Query values — there is exactly one evaluator, which is an architectural invariant.

C. Custom backend

cfdb_core::StoreBackend is a trait. cfdb-petgraph is the reference in-process implementation; alternative backends (SQLite, Kùzu, a remote RPC) can be written by implementing the trait. cfdb-core has zero dependencies on the parser, the store, the extractor, or any wire form.

Schema overview

Facts land in a typed graph. The current schema (covered by cfdb schema-describe):

Nodes: Crate, Module, File, Item (fn / struct / enum / trait / const / type / …), Field, CallSite, EntryPoint, Concept, BoundedContext.

Edges: IN_CRATE, IN_MODULE, HAS_FIELD, INVOKES_AT (Item → CallSite), CALLS (resolver-emitted, HIR), CANONICAL_FOR, RESOLVES_TO (concept-level), IMPORTS, CONTAINS.

Every node/edge carries provenance (source_file, line, resolver where relevant) and an is_test flag so rules can scope to prod-only, test-only, or all.

cfdb_core::SchemaVersion is the wire contract. Downstream consumers (graph-specs-rust, custom backends) pin this version; breaking changes bump it in a reviewed PR.

Languages: v0.1 ships Rust (the reference LanguageProducer per RFC-041) — cfdb-extractor walks Cargo.toml workspaces via syn. PHP (#264) and TypeScript (#265) plug in behind the same trait via the META multi-language roadmap (#266). The :Item.kind enum is a schema-governed closed set; new languages introducing new kind values require a separate schema RFC + SchemaVersion patch + lockstep PR on graph-specs-rust per RFC-033 §4 I2.

Example queries

See examples/queries/ for runnable queries, each with a header comment explaining the pattern:

File Pattern
arch-ban-utc-now.cypher Ban rule — forbid Utc::now() in inner-ring prod code
arch-ban-f64-in-domain.cypher Ban rule — forbid f64 in domain types
arch-ban-reqwest-client-new.cypher Ban rule — forbid direct reqwest::Client::new()
list-callers.cypher Find every call site of a symbol matched by regex
hsb-by-name.cypher Horizontal split-brain by name
vertical-split-brain.cypher Vertical split-brain — two resolvers reachable from one entry point (fork kind)
vertical-split-brain-drop.cypher Vertical split-brain — entry point registers wire key K, reachable resolver reads divergent key K' (drop kind, #297 Phase B)
canonical-bypass-reachable.cypher Bypass rule with live user-reachable verdict
canonical-bypass-caller.cypher Bypass rule scoped to caller regex
canonical-bypass-dead.cypher Bypass rule with dead-code verdict
canonical-unreachable.cypher Canonical resolver is unreachable from any entry point
signature-divergent.cypher Function signature drifts from declared canonical shape
const-table-overlap.cypher Const-literal tables overlap across crates — verdict ladder: CONST_TABLE_DUPLICATE (entries_hash equality) → CONST_TABLE_SUBSET (one set ⊂ other) → CONST_TABLE_INTERSECTION_HIGH (jaccard ≥ 0.5)

All queries in this table are smoke-tested in CI against cfdb-self (#339, RFC-030 §3.2 liveness) — a parser regression or schema drift that breaks any of them blocks merge. Parameterized queries opt out via a // smoke-skip: <reason> header on line 1.

All examples are plain text — copy, adapt parameters, run.

Cypher subset

The parser implements a deliberate subset of openCypher:

Supported: MATCH (var:Label), edge patterns with direction and labels, WHERE with =, <>, <, <=, >, >=, AND, OR, NOT, IN, regex =~, string functions (starts_with, ends_with, contains), RETURN with property projection, ORDER BY, LIMIT, SKIP, WITH pipelining, basic aggregations (count, collect), parameters ($name).

Not supported (v0.1): CREATE, MERGE, DELETE, SET, CALL procedures, variable-length path patterns *, shortest-path, graph mutation in general. cfdb is read-only by design — writes happen through extract and enrich-* verbs, not Cypher.

See crates/cfdb-query/src/parser/ for the full grammar and studies/001-graph-store-selection.md §8 for the subset rationale.

User-defined functions

The evaluator exposes a small stable set of UDFs callable from Cypher — path filters, callee-name extraction, reachability tests, signature hashing. See docs/udfs.md for the full list and semantics.

Determinism

cfdb extract is byte-stable on an unchanged tree: two consecutive extracts hash-identically (ci/determinism-check.sh). The graph store is treated as a cache; the canonical fact format is sorted JSONL keyed by (node_label, qname) / (edge_label, src_qname, dst_qname). This is what determinism, diffing, and cross-machine reproducibility test against — not the on-disk backend file.

Recall

cfdb-recall compares the extractor's view of a crate's public API against rustdoc --output-format json as ground truth, reports ratios per crate, and fails CI if recall falls below threshold. This is the guard against the extractor silently missing items (macro-expanded types, re-exports, nested pub mod). Requires nightly for the rustdoc JSON emitter.

Nightly cadence (#340, Phase C of EPIC #338). A dedicated workflow (.gitea/workflows/recall-nightly.yml) runs cfdb-recall against develop HEAD at 03:00 UTC daily and on workflow_dispatch. Per-crate ratios + an aggregate are emitted to stdout AND written to recall-ratios.json (uploaded as the recall-ratios workflow artifact). The workflow then posts Gitea commit statuses recall/<crate> (one per measured crate) and recall/total (aggregate) on the develop SHA via the Gitea Statuses API. PR-time CI is unchanged — the slim build at .gitea/workflows/ci.yml:140 (cargo check -p cfdb-recall --no-default-features) stays as the synchronous gate; the nightly is the asynchronous deep gate that exercises the runner feature against rustdoc-json without extending PR latency.

Const-threshold rule. Per CLAUDE.md §6 row 5 and the project §3 no-ratchet rule, recall thresholds live as const declarations in crates/cfdb-recall/src/thresholds.rsRECALL_THRESHOLD_PER_CRATE (initial: 0.85) and RECALL_THRESHOLD_TOTAL (initial: 0.90). There is no .recall-baseline.json, no allowlist file, no --update-baseline flag. Per-crate dispatch goes through threshold_for_crate(name)'s match arms — overrides are explicit and reviewed. Tightening either threshold is a reviewed PR that edits the constant; the threshold-pin unit test in thresholds.rs catches careless edits.

Soft-warning → hard transition. AC-5 of #340 makes the first nightly cycle after merge soft-warning by definition: the workflow always posts recall/<crate> and recall/total statuses on develop HEAD, but the Gitea project's required-checks list is admin-set and lives outside this workflow. After at least one successful nightly baseline (typically the second cycle), the operator promotes recall/total to a required check via the Gitea project settings. From that point on, a regression in aggregate recall blocks new merges to develop. Per-crate recall/<crate> contexts remain advisory — the aggregate is the gate. Promoting an individual crate to required is an explicit operator decision, not implicit.

Reading recall-ratios.json. The artifact follows a versioned schema (see the doc comment on --json-out in crates/cfdb-recall/src/bin/cfdb-recall.rs). Each entry under crates[] carries name, recall (ratio in [0.0, 1.0], or null for a vacuous empty-surface crate), threshold, passes, matched, adjusted_denominator, total_public, missing_count, audited_count. The total object carries the aggregate (matched-sum / adjusted_denominator-sum) plus its threshold and pass/fail. AC-7 maps recall/<crate> = error (yellow) to a rustdoc-json or extractor failure during the run — the workflow distinguishes "infra problem" (no JSON produced → single yellow recall/total = error) from "real recall regression" (JSON produced, ratios below threshold → red per affected crate).

Dogfood enforcement

Every PR runs cfdb against itself + against the companion at a pinned SHA. The gates:

Gate Tool Question Failure
Self-hosted ban rules cfdb violations against examples/queries/arch-ban-*.cypher "Does cfdb's own code use forbidden patterns?" Any new row under a ban rule
Enrichment-pass postconditions tools/dogfood-enrich against .cfdb/queries/self-enrich-*.cypher (per RFC-039) "Did each enrichment pass write the attrs/edges its contract requires?" Any non-zero violation row
enrich-deprecation (#343) Source-grep #[deprecated] count vs :Item.is_deprecated = true count "Did the extractor see every #[deprecated] annotation in the workspace?" Extracted count < source-grep count → exit 30
enrich-rfc-docs (#344) FS scan of docs/RFC-*.md count vs count(:RfcDoc) + count(:Item)-[:REFERENCED_BY]->(:RfcDoc) > 0 "Did the extractor see every shipped RFC and wire its REFERENCED_BY edges?" :RfcDoc count < FS count, OR zero REFERENCED_BY edges → exit 30
enrich-bounded-context (#345) Keyspace count(:Item) vs count(:Item WHERE bounded_context = "") against the BC_COVERAGE_THRESHOLD = 95 const; ratio computed harness-side via {{ total_items }} + {{ nulls_threshold }} substitutions (Path B from #355) "Are at least 95% of :Item nodes assigned a non-empty bounded_context after the combined extract+enrich pipeline?" count(empty bounded_context) > total * (100 - threshold) / 100 → exit 30
enrich-concepts (#346) TOML scan of .cfdb/concepts/*.toml distinct names + canonical_crate count vs count(:Concept) + count(:LABELED_AS) > 0 + (conditional) count(:CANONICAL_FOR) > 0 "Did the enrichment pipeline materialize every declared bounded context?" Any of the three sentinels fires → exit 30
enrich-reachability (#347, nightly, --features hir) Keyspace count(:Item{kind:"fn"}) vs count(:Item{kind:"fn"} WHERE reachable_from_entry = false) against REACHABILITY_THRESHOLD = 80 const; Path B substitution shape "Are at least 80% of :Item{kind:'fn'} nodes reachable from a :EntryPoint over CALLS*?" unreachable count > total * (100 - threshold) / 100 → exit 30
enrich-metrics (#348, nightly, --features quality-metrics) Keyspace count(:Item{kind:"fn"}) vs count(:Item{kind:"fn"}) with cyclomatic and unwrap_count set, against METRICS_COVERAGE_THRESHOLD = 95 const; Path B substitution shape "Are both cyclomatic and unwrap_count emitted on at least 95% of :Item{kind:'fn'} nodes after the metrics pass?" missing-attr count > total * (100 - threshold) / 100 → exit 30
enrich-git-history (#349, nightly, --features git-enrich) Keyspace count(:Item) vs count(:Item WHERE git_last_commit_unix_ts = null) against GIT_COVERAGE_THRESHOLD = 95 const; Path B substitution shape "Did the git-enrich pass write git_last_commit_unix_ts on at least 95% of :Item nodes?" missing-attr count > total * (100 - threshold) / 100 → exit 30
Determinism ci/determinism-check.sh + ci/dogfood-determinism.sh "Is cfdb extract byte-stable across two runs?" sha256 / stdout mismatch
Cross-dogfood ci/cross-dogfood.sh against graph-specs-rust at pinned SHA "Does cfdb produce zero findings on the companion?" Any rule row → exit 30
Extractor recall cfdb-recall (extractor vs rustdoc --output-format=json) "Does the syn-based extractor see everything rustdoc sees?" Recall ratio below per-crate threshold
No metric ratchets Repo rule — thresholds are const in tool source, raised only by reviewed PR "Does this PR add a baseline / ceiling / allowlist file?" PR rejected on sight

Crates

Crate Role
cfdb-core Node/Edge fact types, Query AST, StoreBackend + EnrichBackend traits, schema vocabulary, SchemaVersion. Zero deps on parser / store / extractor — the dependency rule points inward.
cfdb-query Cypher-subset parser (chumsky) + Rust builder API. Both produce the same cfdb_core::Query AST. Includes shape-level lints.
cfdb-petgraph Reference StoreBackend on petgraph::StableDiGraph. Hosts the query evaluator.
cfdb-extractor syn + cargo_metadata workspace walker. Emits Nodes, Edges, and name-level CallSites.
cfdb-hir-extractor rust-analyzer HIR-backed resolver extractor. Emits resolved CALLS, INVOKES_AT, EntryPoint. Feature-gated to isolate its compile cost.
cfdb-hir-petgraph-adapter Glue between cfdb-hir-extractor and cfdb-petgraph that keeps the ra-ap-* crates out of cfdb-cli's default build.
cfdb-concepts Bounded-context resolver — reads .cfdb/concepts/<context>.toml and maps crate names to contexts.
cfdb-recall Recall gate — extractor vs. rustdoc ground truth.
cfdb-cli The cfdb binary. Thin wrapper over the library crates.

Layout

.
├── crates/           # library + CLI
├── examples/queries/ # runnable Cypher examples
├── specs/            # canonical-concept docs, one file per crate (dogfood contract)
├── docs/             # RFCs, protocols, pattern reference
├── studies/          # design spikes and backend selection
├── ci/               # determinism, recall, cross-dogfood scripts
└── tools/            # small helpers (prelude-trigger checker, etc.)

specs/ — docs that are also the contract

specs/concepts/ holds one markdown file per crate (cfdb-core.md, cfdb-query.md, …). Each file is a human-readable concept dictionary: every public type, trait, and top-level function gets a ## Name heading and a one-paragraph description of what it means and what invariants it carries. Read them first to orient — they are the shortest path to understanding cfdb's vocabulary without reading source.

They are also the dogfood contract. The companion graph-specs-rust tool consumes specs/concepts/*.md as the source of truth for what the code is supposed to contain; on every PR it diffs the specs against cfdb's own extracted fact graph and blocks any drift (a spec heading with no matching item, an item with no matching heading, a signature change not reflected in spec). cfdb's own codebase is the first customer of this discipline — the specs are both its documentation and the proof that the toolchain works on its authors' own tree before being pointed at anyone else's.

Adding a new public type to a cfdb crate requires adding its spec heading in the same PR. That is how the specs stay honest.

Status

Under active development. v0.1 (syn extractor + petgraph store + Cypher subset + 10+ example queries + recall gate) is feature-complete on develop. v0.2 (HIR extractor, enrichment verbs, concept resolution) lands incrementally — see the RFC-cfdb.md in docs/ for the roadmap.

The wire schema (SchemaVersion) is versioned. Breaking changes bump it and are called out in release notes; non-breaking additions are documented in SchemaDescribe output.

Documentation

Contributing

New capabilities (new verb, new fact type, new schema field, new --flag, new sub-backend) are RFC-first — see CLAUDE.md §2. Bug fixes and mechanical refactors go straight to an issue + PR. Every PR passes the dogfood gate: cfdb's own ban rules run against cfdb itself, and cross-dogfood runs cfdb against graph-specs-rust at a pinned SHA. No metric ratchets, no baseline files — violations are fixed, not accumulated.

License

Dual-licensed under MIT or Apache-2.0, at your option.

About

code fact database

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages