EKOSdocs
Docs / Guides Build

Checking Reference Integrity

Find the names that point at nothing across code, SQL, ETL and dbt — missing tables, misspelled routine calls, broken imports — with evidence and a stable fingerprint.

EKOS already resolves names across systems to build lineage: the tables a routine or view reads and writes, a Pentaho step's source and sink, the SQL a Java method builds, a dbt source(). ekos check refs (RFC 0233) reports the names that resolve to nothing (UnresolvedReference) or to several objects (AmbiguousReference). Each finding carries the referring file and line, the closest names, the evidence, a confidence, and a fingerprint that survives recompiles and edits above it. No model is called.

These are the defects no single-language linter sees: a Java query for a table no schema has, a routine deleting from a table a migration dropped.

Run it

The check runs on the compiled ledger, so compile first. ekos commit writes the same report to <state>/integrity/current.json (never into the ledger).

ekos check refs                                  # metrics + findings, most severe first
ekos check refs --scope cross-system             # only code, ETL and dbt into a schema
ekos check refs --from-index --format json       # the last commit's report, no recompute

--scope is cross-system, code (SQL routines and views to tables) or all (the default).

What is not a finding

The rules come from false positives on real repositories:

  • relations a routine creates at run time (CREATE TEMP TABLE, EXECUTE 'CREATE VIEW …'), table functions and system catalogs;
  • a name in a schema EKOS compiled nothing from: a coverage gap (info), not an error. A table the migration scripts alter but no compiled DDL defines is a coverage gap; one they drop is an error naming the script;
  • references inside migration scripts and test code: info;
  • a code model's table (ModelTable) counts only when no schema defines the name.

Calls, imports, cycles and reachability

The same check covers references inside one language:

Check Reported when
SQL routine calls a call no file defines and no engine provides; per-dialect built-in lists (coalesce, nvl, dbms_*, pg_catalog, ClickHouse functions)
Perl use, Elixir alias a package under the workspace's own namespace that no file declares
JavaScript, Python imports a relative JS import with no file behind it; a Python import under an in-tree package
cycles one finding per module dependency cycle
orphans tables nothing uses
reachability (SQL routines only) a routine no view, trigger, Java SQL text or [xref] entry-points glob reaches: UnreachableSymbol, or DynamicReferenceUnknown when code that may call it by name exists

An import into a directory ignore-patterns excludes is a coverage gap naming the file. Reachability findings are hidden unless you pass --reachability; the dynamic share is always printed beside the coverage, and languages without a call graph are reported as unsupported, with the reason. Rust and Java imports are not checked: their compilers already refuse them.

In CI

ekos check refs --fail-on error --format sarif > xref.sarif     # SARIF 2.1.0 for code scanning
ekos check refs --format json > baseline.json                   # save a baseline once …
ekos check refs --baseline baseline.json --fail-on error        # … then fail only on new findings
ekos check refs --baseline 2026-10-01T00:00:00Z                 # or new since a ledger time

--fail-on exits 1 when a shown finding is at or above error or warning. A ledger-time baseline uses the ledger's own time travel; a report file suits a CI job that rebuilds the ledger every run.

Tune and suppress

[xref]
enabled = true                 # computed at commit; default on
min-confidence = 0.5           # hide findings below this
entry-points = ["api_*", "public.report__*"]   # routines an external system calls
[xref.severity]
AmbiguousReference = "info"

Suppressions live in <state>/xref-ignore.toml (.ekos/xref-ignore.toml in a plain workspace), and every rule needs a reason:

[[ignore]]
fingerprint = "3f1c…"          # or target = "tmp_*", or source = "sql/old/*"
reason = "temporary table created in the same routine"

A suppressed finding is still counted, and --show-suppressed lists it with its reason.

Agents, console and triage

  • Agents: the read-only MCP tool ekos_xref_check returns the findings, and its name argument checks one table or routine name before the agent writes SQL against it.
  • Console: the Integrity tab and dashboard tiles show the commit-time report. The estate documentation's Findings page has a Reference integrity section.
  • Triage: ekos check explain --limit 5 asks the configured model for the likely target of an ambiguous reference, or the caller of an unreached routine. An answer is kept only if it names and cites an object it was shown. Suggestions are printed, never stored, and a model that is not a local Ollama one needs --allow-metered.

Measured

LedgerSMB iDempiere (code + scripts, no database)
References 2,018, 98.0% resolved 12,836, 95.2% resolved
Errors 4, all real: two tables no file creates, a misspelled function call (lsmb__next_businss_day), a call to a function that exists only under another name 0
SQL routines reachable 22 of 517, 95.7% dynamic 85 of 130, 34.6% dynamic

Because most LedgerSMB routines are called dynamically, the check makes no dead-code claim there. iDempiere's base schema ships as a database dump rather than DDL, so its missing tables can only be decided with the live catalog connected.