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.
- 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 arust-analyzer-HIR-backed resolver extractor (v0.2, feature-gated). Both emit into the same schema; the:CallSite.resolverdiscriminator ("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-recallmeasures the extractor againstcargo public-api/rustdoc --output-format jsonground 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.
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.
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/cfdbMinimum 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.
# 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-describeThe 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.
cfdb's crates are usable as a library without the CLI. Three patterns:
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());
}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.
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.
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.
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.
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.
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.
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.
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.rs — RECALL_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).
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 |
| 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. |
.
├── 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/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.
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.
- Core RFC:
docs/RFC-cfdb.md— rationale, schema, 9 motivating patterns. - v0.2 addendum:
docs/RFC-cfdb.md— HIR extractor, enrichment pipeline. - Anti-drift gate:
docs/RFC-030-anti-drift-gate.md— how cfdb integrates into PR-time gating. - Cross-dogfood with graph-specs:
docs/RFC-033-cross-dogfood.md, runbook indocs/cross-fixture-bump.md. - Pattern references:
docs/cfdb-pattern-b.md,docs/cfdb-pattern-c.md. - UDF reference:
docs/udfs.md. ra-ap-*upgrade protocol:docs/ra-ap-upgrade-protocol.md.
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.
Dual-licensed under MIT or Apache-2.0, at your option.