Skip to content

GraphBridge Doctor

Doctor answers two questions: does my EF model finalize and do my registered queries compile, and what does the selected database's schema actually provide? Run it offline in a build or code review, and in connected read-only mode before a deployment. It is a C# API (GraphBridge.Tooling.Diagnostics.GraphDoctor in the optional GraphBridge.Tooling package) that you call from your own application host.

using GraphBridge.Tooling.Diagnostics;

// Register the queries to check. The factory (here from workshop lesson 16) only builds a Fluent query; Doctor never runs it.
var budget = new GraphQueryBudget(maxRows: 100, deadline: TimeSpan.FromSeconds(10));
var catalog = new GraphQueryCatalog([
    GraphQueryContract.Fluent(new GraphQueryDefinition("workshop.coactors.v1",
        "Distinct co-actor names ordered by name.", "Lessons/16-FluentPatterns#Coactors",
        [new("name", GraphResultKind.String)], GraphResultComparison.Ordered, GraphDuplicatePolicy.Eliminate, budget),
        db => Lesson16FluentPatterns.Coactors(db, "Keanu Reeves"))
]);

// db is your existing DbContext configured with UseNeo4j. Offline: no connection is opened.
var report = GraphDoctor.InspectOffline(db, catalog);
Console.WriteLine(report.ToText());   // or report.ToJson()
return report.Status == GraphEvidenceStatus.Pass ? 0 : 1;

The text report starts like this (hashes, versions and counts shortened):

GraphBridge evidence / format 1
Mode: offline | Status: pass
Database observation: not-run
Parameter values: excluded
Statement and schema text: excluded
Loaded provider: 0.2.0-preview.1…
…
Model fingerprint: …
Mapped nodes: … | relationships: …
GBD-MODEL-000 | Model | pass
…
workshop.coactors.v1 | compilation: pass | execution: not-run

Doctor is the first of three tools that share one query catalog and one evidence format. GraphBridge Verify turns the same catalog into pass/fail gates with EXPLAIN plans and result comparisons, and Query Lab renders the captured evidence as HTML or in VS Code.

Registered queries and offline Doctor

A query contract pairs a public declaration with a trusted factory that builds the query. GraphQueryDefinition takes:

Argument Meaning
id Stable public identifier: lowercase ASCII segments separated by . or -, at most 100 characters, for example movies.coactors.v1.
description, sourceReference Public text (1–1,024 characters, no control characters). Never put secrets in them.
columns 1–64 distinct output fields, each a GraphResultColumn(name, GraphResultKind, nullable). Kinds: String, Integer, FloatingPoint, Boolean.
comparison Ordered or Multiset: how Verify V2 compares rows.
duplicates Preserve or Eliminate: whether duplicate rows are part of the contract.
budget GraphQueryBudget(maxRows, deadline, maxPathHops): 1–10,000 rows, a deadline up to 5 minutes, optional 0–32 path hops.
requiredFeatures Optional, up to 32 identifiers that Verify V1 checks against the server (see Verify).
referenceId, fixtureId Optional, declared together: the independent native reference and fixture that Verify V2 uses.

Register the definition with GraphQueryContract.Fluent(definition, factory). A second overload, Fluent(definition, factory, rowProjection), adds the scalar-row projection that semantic verification needs; Doctor never invokes it. GraphQueryCatalog holds at most 256 contracts with distinct IDs and orders them by ID. The catalog accepts Fluent Cypher factories only; it does not discover assemblies or evaluate source strings. The sample registrations register three real movie queries with projections.

GraphDoctor.InspectOffline(context, catalog, includeStatements: false, cancellationToken) then:

  1. Finalizes the model through GetGraphSchemaManifest() and records its storage fingerprint and node/relationship counts (GBD-MODEL-000, or GBD-MODEL-001 when the model is invalid).
  2. Compiles every contract with the provider's own Fluent compiler. A failure stays attached to its contract ID with a stable diagnosis.

If the model fails, no query is compiled and every contract stays not-run. Doctor creates no session, runs no DDL, executes nothing and leaves the context, its driver and its change tracker to you. Cancellation is checked between steps; it cannot interrupt your factory code. A catalog with no contracts still reports the model, which says nothing about queries or the database.

Factories are trusted application delegates, not sandboxed code. Register only functions that construct queries: do not execute database work inside them and never register untrusted delegates.

To capture one command without a catalog, call GraphQueryEvidence.Capture(contractId, query.ToCommand(), includeStatement: false) and wrap the result in new GraphEvidenceReport(GraphEvidenceMode.Offline, [snapshot], []).

Explicit connected, read-only Doctor

var report = await GraphDoctor.InspectAsync(db, catalog, "myapp.staging.v1",
    maxSchemaRows: 1000, cancellationToken: cancellationToken);
Console.WriteLine(report.ToJson());

Connected mode runs the offline checks, then calls InspectGraphSchemaAsync() to read server components, constraints and indexes (plus the current graph type on a recognized Enterprise profile) and compares them with the finalized model. It performs no DDL and no registered query execution, and it reads no data except the bounded sample for declared search indexes. If the context already has an EF transaction, Doctor borrows it unchanged; otherwise the inspector rolls back and disposes its own read transaction.

Parameter Meaning
profileId Your public name for the environment the evidence describes, such as myapp.staging.v1. Same format as a contract ID. It is not a hostname or connection string.
maxSchemaRows 1–10,000. Bounds metadata rows read, not field size or server work.
includeStatements Discloses compiled Cypher and typed snapshots; see output.
cancellationToken Checked between inspector operations; it is not a server deadline.

The report keeps the observed numeric server version, recognized edition (community or enterprise, otherwise unrecognized), advertised Cypher versions, inferred schema capabilities and finding codes with counts. It omits database and schema names, original messages, graph-type text and proposed DDL. When node-uniqueness or range-index support cannot be established, the database observation is blocked (GBD-CAP-001). Missing required schema fails (GBD-SCHEMA-001). Authentication or metadata denial, unreachable transport, exceeded row limits, cancellation and unknown failures keep no partial database success.

Warnings stay visible even when required expectations pass: database.schemaStatus is warn, the findings list them and the report carries GBD-SCHEMA-002. Typical warnings are application-enforced properties on Community and optional missing indexes. A pass does not prove data integrity, a consistent metadata snapshot, query planability or result correctness. For full schema names and proposed DDL, call InspectGraphSchemaAsync() yourself in a private context; Doctor does not keep them.

Use read-only credentials and select the intended database explicitly. Do not attach Doctor to every application request or run connected inspection on ordinary startup. Troubleshooting order: Doctor decision tree.

Search indexes

Graph search and the VectorData adapter check their index at every call, but only at the first call. Before that, a missing index, a different embedding model or a wrong property name can cost hours: searches fail, or quietly return nothing. Declare the indexes your application searches, and connected Doctor checks them before you deploy:

var catalog = new GraphQueryCatalog(contracts,
[
    GraphSearchIndexExpectation.Vector("movie-taste", indexName: "movie_taste", labelOrType: "Movie",
        property: "taste", dimensions: 1024, similarity: "cosine"),
    GraphSearchIndexExpectation.FullText("movie-text", indexName: "movie_text", ["Movie"], ["title", "tagline"])
]);
var report = await GraphDoctor.InspectAsync(db, catalog, "myapp.staging.v1");

For each declared index, Doctor reads SHOW INDEXES and a bounded sample of stored values in its own read transaction, which it always rolls back. It never creates, repairs or drops an index; a migration step can create it. It catches these mistakes:

Code What it means Typical cause
GBD-SEARCH-001 The index doesn't exist Not created in this deployment, a typo in the name, or the wrong database selected
GBD-SEARCH-002 The index isn't ONLINE yet Still populating after creation; searches fail until it's done
GBD-SEARCH-003 The index is FAILED Population failed; read the Neo4j log, then drop and recreate it
GBD-SEARCH-004 Wrong kind A vector index where full-text was expected, or a relationship index where a node index was expected
GBD-SEARCH-005 Different labels, types or properties The index covers embeddings but the application writes embedding
GBD-SEARCH-006 Different vector dimensions The embedding model changed (for example 1536 to 1024) but the index didn't; every search fails
GBD-SEARCH-007 Different similarity function (warning) Scores mean something else than the application assumes
GBD-SEARCH-008 Stored values the index silently skips (warning) Wrong dimensions, strings or other non-numeric values, or zero vectors on a cosine index
GBD-SEARCH-009 Nothing to search (warning) No sampled entity has the property: ingestion or embedding hasn't run, or the name differs
GBD-SEARCH-010 Typed search unsupported on this server Typed search needs Neo4j 2026 with Cypher 25 (full-text 2026.09+); native Cypher still works
GBD-SEARCH-011 Eventually consistent full-text index (information) Recent writes appear after a delay; fine for bulk loads
GBD-SEARCH-012 The declaration's dimensions differ from the model's The property is mapped with IsGraphVector(1024) but the index is declared with 1536, so the application writes vectors the index can't use
GBD-SEARCH-000 Every declared index passes

GBD-SEARCH-008 matters most because Neo4j raises no error: values it can't index are simply left out, and a search just returns fewer results. The live test for this feature checks it against the server: a real search returns exactly the values Doctor counted as usable.

Each index appears in searchIndexes[] with its declared ID, kind, status, finding codes, how many values were sampled and how many are unusable. The report never contains the index name, labels, properties or any stored value. Sampling looks at no more than 10 times sampleSize entities (default 1,000 values) and reports counts only. Pass sampleSize: 0 to check metadata only. Native VECTOR values are not dimension-checked. Offline Doctor lists the declarations as not-run.

Additional labels

When the model gives a type additional labels, connected Doctor counts the nodes that carry the type's label but not all of its additional labels. Queries of the type skip those nodes, keyed writes don't touch them, and reading one through a native reference or an Include fails.

Code Meaning First action
GBD-LABEL-001 Some nodes lack the labels (warning) Upsert those entities by key, which adds the labels, or label them in a reviewed data step
GBD-LABEL-000 None do

The check reads only, in one transaction it rolls back, with a 60-second timeout. When every node is labelled, it reads every node of each such label; the count stops at maxSchemaRows. It runs only when the model has additional labels. Like every Doctor report, the warning names no label; this query finds the nodes for a type, here Person with __Entity__ and __KGBuilder__:

MATCH (n:Person) WHERE NOT (n:__Entity__ AND n:__KGBuilder__) RETURN elementId(n) LIMIT 100

Report output

GraphEvidenceReport.ToJson() writes version 1 evidence with deterministic ordering (queries by contract ID, diagnostics by code). Enum values use kebab case, such as "not-run". ToText() writes the same evidence as readable text. Neither call compiles or connects again. An abbreviated offline report (… marks omitted values):

{
  "formatVersion": 1,
  "mode": "offline",
  "versions": { "provider": "…", "efCore": "…", "driver": "…", "tooling": "…" },
  "model": { "fingerprint": "…", "nodes": "…", "relationships": "…" },
  "queries": [ { "contractId": "workshop.coactors.v1", "statementFingerprint": "…", "parameters": [ { "position": 0, "kind": "string" } ], "compilation": "pass", "execution": "not-run", "…": "…" } ],
  "diagnostics": [ { "code": "GBD-MODEL-000", "status": "pass", "severity": "information", "phase": "model", "…": "…" } ],
  "parameterValuesIncluded": false,
  "sensitiveTextIncluded": false,
  "databaseObservation": "not-run",
  "status": "pass"
}
Field Content
versions Informational versions of the loaded provider, EF Core, driver and tooling assemblies.
model Storage fingerprint and counts; absent when the model failed.
database Connected mode only: profile ID, server version, edition, capabilities, schemaStatus, grouped findings and its own status.
searchIndexes[] Only when the catalog declares search indexes: ID, kind, entity kind, status, finding codes, sampledValues and unusableValues. See search indexes.
queries[] Contract ID, SHA-256 statement fingerprint, parameter positions and broad kinds (string, integer, floating-point, boolean, bytes, array, null, opaque), the declared definition, compilation, an optional diagnostic, and execution: not-run.
diagnostics[] Report-level diagnoses with explanation, suggested action and documentation link.

By default a report contains no Cypher text, parameter names or values, value lengths or exception messages. With includeStatements: true (or includeStatement: true on Capture) each query also carries its parameterized statement and typed semantics snapshot, and sensitiveTextIncluded becomes true: labels and property names can reveal domain information. Parameter values are never included. A statement fingerprint proves equality, not protection against guessing.

Status and exit codes

Every observation is pass, fail, unsupported, blocked or not-run. The report status is the worst of them, in that order of precedence: fail, then blocked, then unsupported, then not-run, otherwise pass. A blocked or unsupported observation can never become an overall pass. Connected mode without a completed database observation is blocked. A report you build yourself with GraphEvidenceMode.Semantic is always blocked: result comparisons come from Verify V2, which produces its own report.

Doctor has no exit code of its own; your host decides. The usual mapping, used by the sample, is 0 for Pass and 1 otherwise, with 2 for invalid usage or configuration.

Stable diagnoses

Every diagnosis has a fixed code, status, severity, phase, explanation, suggested action and documentation link. GraphDiagnosticCatalog.Classify(exception, phase, fluentQuery: true) maps a failure to a diagnosis without wrapping, retrying or changing the original exception; keep that exception locally for investigation, because reports never include its text. GraphDiagnosticCatalog.Get(code) returns a fixed diagnosis and GraphDiagnosticCatalog.Codes lists them all.

Code Status Meaning / first action
GBD-MODEL-000 pass Finalized-model validation passed; the database was not observed.
GBD-MODEL-001 fail The model could not be finalized for graph storage. Check node labels, identity, scalar mappings and relationship endpoint roles.
GBD-QUERY-001 unsupported An EF LINQ query could not be translated. The subcode names the LINQ refusal (GBQ###, see refusal codes).
GBD-QUERY-002 unsupported A Fluent query could not be compiled. Check alias scope, direction, projection and finite bounds. The subcode names the Fluent code (GBFC###, see Fluent Cypher).
GBD-SCHEMA-000 pass The completed metadata comparison satisfies the schema expectations.
GBD-SCHEMA-001 fail A required constraint or index is missing. Provision the reviewed change explicitly; Doctor never repairs.
GBD-SCHEMA-002 pass (warning) Required expectations pass with warnings: application-enforced properties, optional indexes or unmanaged schema.
GBD-SEARCH-000 … 011 see search indexes Declared search indexes: missing (001), not ONLINE (002, blocked), failed (003), wrong kind (004), wrong labels or properties (005), wrong dimensions (006) fail; similarity (007), unusable values (008) and nothing to search (009) warn; 010 is unsupported; 011 is information.
GBD-LABEL-000, 001 see additional labels Every node with a type's label carries its additional labels (000), or some don't (001, warning): queries skip them, and reading one through a reference or an Include fails.
GBD-CAP-001 blocked A required capability is unknown. Check server version, edition and metadata permissions.
GBD-CONN-001 blocked Connection or session failure. Check endpoint, database, TLS and server readiness.
GBD-CONN-002 blocked Authentication or authorization prevented observation. Review credentials and grants privately.
GBD-EXEC-001 blocked The observation was canceled. Review the deadline before an explicit rerun.
GBD-EXEC-002 blocked Unclassified failure. Inspect the original exception locally.

The subcode field appears only when the failure carried a GraphBridge.QueryDiagnostic or GraphBridge.FluentDiagnostic value in Exception.Data; it is omitted otherwise. phase: Query means local compilation, so server execution errors are never labeled as unsupported LINQ. Permission classification recognizes the driver's AuthenticationException and Neo4j's Neo.ClientError.Security.* status codes. Schema, capability and model-pass diagnoses are added by the observing host; their presence in the catalog is not evidence that an observation ran.

To report an unsupported query, use the unsupported-query form.

Limits

  • Doctor observes. It never repairs schema, creates search indexes, runs migrations, executes registered queries, retries or certifies production readiness.
  • Budgets in a definition are declarations; Doctor does not enforce them.
  • There is no standalone doctor command and no assembly discovery: Doctor runs inside a host that knows how to build your context. GraphBridge.Tool does not include it.
  • GraphBridge.Tooling brings its authoring dependencies (RDF, YAML, Roslyn) with it. The core provider does not depend on Tooling.
  • To browse findings interactively, open the JSON report in the Query Lab VS Code extension, which has a Doctor view.

Try it

  • Workshop lesson 26 · GraphBridge Doctor registers two queries and runs Doctor offline and connected.
  • Sample 05 is a runnable host. Offline modes need no database:
dotnet run --project samples/05-FluentCypher/FluentCypher.Sample.csproj -- --doctor        # JSON
dotnet run --project samples/05-FluentCypher/FluentCypher.Sample.csproj -- --doctor-text   # text
dotnet run --project samples/05-FluentCypher/FluentCypher.Sample.csproj -- --doctor-inspect

--doctor-inspect reads GRAPHBRIDGE_NEO4J_URI, GRAPHBRIDGE_NEO4J_DATABASE, GRAPHBRIDGE_NEO4J_USERNAME and GRAPHBRIDGE_NEO4J_PASSWORD from the process environment. The URI must not embed credentials, and reports never print them. It passes only when the database has the movie model's constraints. Exit codes: 0 pass, 1 non-passing report, 2 invalid arguments or missing configuration.

Design decision: ADR-0042. Evidence: verification summary.