SIDQ صدق Live

Verification control for DataHub agents

DataHub says the column exists. PostgreSQL says it doesn’t.

Sidq cross-checks DataHub against its own graph, live PostgreSQL, and the transformation SQL behind stored lineage before an agent acts. A deterministic policy returns PASS, WARN, or BLOCK, proves any repair, and writes re-verifiable evidence back to DataHub.

No LLM decides. Missing evidence never becomes a pass.

  • Live PostgreSQL
  • 285 contradictions in DataHub’s own showcase catalog
  • Docs + constraints vs PostgreSQL
  • 1,180 tests · 84.20% branch
  • Official DataHub MCP

Runs a real command on this host and prints the real output. 5 proof runs per client every 10 minutes; a refusal says which limit applied and when it resets.

the sidq loop · live proof console
Compare
DataHub ↔ DataHub · live PostgreSQL · transformation SQL
Decide
one deterministic policy → PASS · WARN · BLOCK
Prove
simulate the repair, then re-run every check against the catalog it would create
Remember
native Assertion + Receipt → a separate reader verifies it again

Recorded proof — not live. Output of make gate-demo, captured at git revision f006136. Offline verdict below re-runs this exact command live on this host; the buttons beside it run different proofs.

examples/01 and every artifact quoting its hash are current

DECISION : BLOCK
RULES    : wide_blast_radius, critical_downstream
COMMIT   : 5addb753788935d4d1aa6a9483c28c6fc124e5c7
POLICY   : 66f48004804c5ce02955699710466b6d58ae7a868f876a4774e548c5c15920b8

Reproduced from examples/01-blocked-pii-dashboard/ — same policy, same commit,
byte-identical verdict. Full evidence: examples/01-blocked-pii-dashboard/verdict.json

Five fixed read-only commands run on this host. Output prints here.

Five runnable demonstrations, each a fixed read-only command from a closed table. They are what this host will execute for you — not the whole of what the page proves. Claims turns DataHub prose into bounded SELECT-only SQL against PostgreSQL; audit hunts catalog contradictions; repair proves proposed fixes without applying them; handoff re-verifies persisted state; gate-demo re-derives the committed BLOCK offline. The times beside them are measured on this host, not estimated.

Three read-only MCP tools

No hidden mutation. Their verification store is not a DataHub Receipt reader — the independent Receipt read is the proof above.

check_change · verify_context · search_verified

Use Sidq in an agent or CI

One engine and one policy hash behind every surface: an MCP server agents call directly, a DataHub Skill that fires on “can I trust this asset”, the CLI, a GitHub Action, and a PR bot that posts a single provenance-honest verdict on the diff.

sidq-mcp · skills/datahub-verify · action.yml · python -m sidq.bot.action

Nothing below is a placeholder. $PWD resolves while you stand in the checkout, so the MCP registration needs no path editing at all:

cd sidq && make install

codex mcp add sidq \
  --env DATAHUB_GMS_URL=http://localhost:8080 \
  --env SIDQ_REPO_ROOT="$PWD" \
  -- "$PWD/.venv/bin/sidq-mcp"

SIDQ_REPO_ROOT is the repository whose SQL gets gated. The Sidq checkout works for a first run; point it at your own repository afterwards.

The Skill is the one place a path is genuinely yours — it installs into the data repository, not into Sidq:

cd /the/data-repository-you-want-verified
npx skills add NexuChat/sidq --skill datahub-verify --agent codex

01 The claim

A catalog can agree with itself—and still be wrong.

Catalog-only checks can expose contradictions inside metadata. They cannot detect a catalog that is internally consistent but no longer matches the source. Sidq crosses that boundary before an agent plans or writes.

DataHub says

raw.customers.email

Ingested, indexed, and what every agent planning against this table will read.

PostgreSQL says

raw.customers.email_address

Renamed by make demo-break. Nothing was re-ingested, which is exactly how this happens in production.

BLOCK — on the context, before the agent plans against a column that no longer exists.

One context. Three independent comparisons.

DataHub compared with DataHub

Six contradiction families across stored schema, field lineage, protection metadata, documentation, deprecation, and ownership.

DataHub compared with PostgreSQL

Schema fields, documented row-level promises, and the constraints the database actually enforces.

Stored lineage compared with SQL

Edges DataHub stores but SQL no longer produces—and edges SQL produces that DataHub never recorded.

  • 5 claim types
  • 6 catalog contradiction families
  • 38 explicit policy rules
  • 3 read-only MCP tools

The public claims run is the non-mutating live-source proof: DataHub prose becomes bounded SELECT-only SQL, and only returned row counts determine the verdict. The schema-rename demonstration remains in the local runbook because it deliberately changes PostgreSQL. Any missing comparison is reported as unverifiable, never clean.

Where a model is allowed to act

Turning a documented sentence into a testable claim is the one place a model earns its keep — and the only place it is allowed to act. A 16 KB linear head over frozen third-party embeddings proposes claims at 95.8% precision and 58.0% recall across 72 proposals, against a 17.4% rule baseline. It proposes only unique and not_null; a proposal that cannot be compiled to SQL is dropped; and it never emits a verdict. It may extend WARN coverage after the deterministic rules abstain, and it can never turn a PASS into a BLOCK.

285 contradictions.
In DataHub’s own showcase catalog.

A read-only audit found 285 instances of one contradiction family: stored lineage pointing to fields the destination datasets’ own schemas say do not exist — in showcase-ecommerce, the sample catalog DataHub itself ships. Here is one, whole.

Claim A · stored lineage

order_details.billing_address_line1

Customer_Analytics_Measures.BILLING_ADDRESS_LINE1

Claim B · that asset's own schema

Fields on Customer_Analytics_Measures:

Customer LTV · Value

The column the lineage names does not exist.

Both statements live inside the catalog. No source system was consulted. They cannot both be true.

And the same read says who wrote them. Attributing each side to its ingestion run — one query parameter, no extra calls — shows that the run which wrote the lineage is the run that wrote the schema, in all five affected assets and every stored version: ten out of ten. So this is not two sources disagreeing; one writer produced both halves in the same run. A later run reproduced the shape exactly rather than repairing it, which is why the finding names a job to fix rather than a state to refresh. How that was measured ↗

That is one of six catalog-self-checks. Sidq also checks for protection markers lost across field lineage, documentation naming absent columns, orphan edges, deprecated assets feeding live BI, and consumed assets with no owner. A bounded or incomplete read is reported as unverifiable, not clean.

It also decides what to look at. The audit ranks every asset by how much damage a lie about it would do and spends its budget on the worst first — then lets what it finds change the order: an asset caught in a contradiction promotes its neighbours ahead of their static score, because contradictions cluster. Same catalog, same sequence every time; a different catalog, a different one.

285
internal contradictions
67
datasets examined
5
assets they cluster in
29
consumed assets with no owner
Real read-only audit over the official MCP server. Output prints in the console above.

02 The gate

No LLM decides whether the agent may act.

Five evidence gates feed one deterministic policy. A model may propose a bounded read-only test; only returned evidence can move the verdict.

Same agent. Same goal. Same catalog.
Only one of them is allowed to check.

Without Sidq

Reads the catalog, sees 22 columns including cust_email, and writes the query. Nothing here is careless — the catalog answered, and there was nothing else to ask.

select region_id, count(cust_email)
from order_entry.customers
group by region_id

With Sidq

Same code path, one addition: it verifies before it proposes. verify_context returns 0 findings and 24 checks it could not complete.

-- No SQL proposed.
--   not checked: schema_drift (no live source configured)
--   not checked: lineage_rot ×22 (no target-side column lineage)
--   not checked: constraint_reconciliation

Read that refusal precisely. Offline, the agent did not catch the catalog lying — it produced zero findings. It stopped because three checks could not run at all, and it names each one instead of dropping it. That is the exact substitution this project exists to refuse: “we did not check” is not “we checked and it passed.” Both transcripts ↗

  • Adaptive auditor
  • Sidq MCP
  • Agent Skill
  • CLI
  • GitHub Action
  • PR bot

03 The refusal

BLOCK

An agent proposed removing cust_email.
Sidq refused — with evidence.

BLOCK critical_downstream Verified downstream ownership crosses team boundaries, including ORG_BACKEND_ENG and eight additional owners.
WARN wide_blast_radius 16 downstream consumers recorded, including a live Looker dashboard.
CONTEXT PII_Data The path carries PII metadata — supporting context, not the blocking cause.

Column-level lineage, followed hop by hop from the changed field to the thing that breaks:

  1. dbtorder_entry.customers / cust_email
  2. Snowflakeanalytics.order_details
  3. Lookerexplore.order_details
  4. Dashboarddashboards.53owned by another team
Byte-identical replay — no DataHub, no credentials. Output prints in the console above.

The same refusal, on real pull requests.

Four public pull requests on this repository carry the PR bot’s own verdicts, each read against a live DataHub, not a fixture. Two pass, two are refused with the rules named. One sticky comment per PR, rewritten in place rather than repeated.

  1. #1 harmless column ↗PASSno blocking or warning rule fired · verdict.json ↗
  2. #2 remove cust_emailBLOCKEDpii_exposure · critical_downstream · verdict.json ↗
  3. #3 stale catalog context ↗BLOCKEDunknown_field · catalog_reality_mismatch · verdict.json ↗
  4. #4 compatible migration ↗PASSproven against the live graph, with a receipt · verdict.json ↗

The checks on these four pull requests are red. The reason is published here rather than left to be found.

Both defects are in the bot that posts, not in the engine that decides. The workflow grants issues: write, but GitHub governs commenting on a pull request under pull-requests — so the bot is refused with 403 and the run fails. On #3 ↗ a second defect fires first: one graph recording is absent from the committed fixtures, and the doc-rot gate turned that unavailable catalog into a KeyError instead of a refusal to certify.

The verdicts above were read against a live catalog, and each is linked in full so it stands without the bot. Both fixes are written and held by the same rule as everything else here — the submission commit is frozen, so nothing has been pushed. A tool whose whole claim is that an unperformed check must never be reported as a clean one owes that standard to its own CI first.

04 The repair

Sidq rejects fixes that only move the failure.

A proposal is not a repair until the same deterministic engine re-runs against the catalog it would create.

A repair must survive its own audit.

Sidq repairs only what the catalog already proves. It can propagate a protection marker across the full downstream field-lineage closure, or assign an owner only when every owned upstream agrees. The other four contradiction families remain open with an explicit reason — because plausible metadata is still invented metadata.

A two-hop leak makes the point. source carries the PII marker; middle and sink do not.

Rejected

Tag middle#email only. The named finding is resolved — and Sidq still refuses it, because re-running every check against the catalog it would create surfaces a new pii_leak_untagged on sink. It resolved the ticket and moved the leak.

Proven

Tag the full field-lineage closure — middle and sink together, in one call. Every check re-runs, no new finding appears, and the set is verified jointly rather than one at a time.

This is a pinned regression, not an illustration: the one-hop refusal is the exact failure a live catalog produced.

  1. Propose
  2. Simulate
  3. Re-run every check
  4. Reject collateral
  5. Prove the surviving set together
  6. Write only with --apply
  7. Re-read live state

An acknowledged mutation stays open until direct readback proves the original finding is gone and no new one appeared.

05 The receipt

The next agent verifies for itself.

A receipt is not authority. A separate reader re-reads semantic metadata and complete one-hop lineage, then checks policy hash and age. Applicability, verdict, authorization, and coverage stay separate: a current BLOCK means “checked and refused” and still authorizes only STOP; absent, stale, or unreadable means RECHECK.

Current PASSContinue.
Current WARNReview or escalate.
Current BLOCKStop. Checked and refused — never shown as unverified.
NOT VERIFIEDMissing, stale, or unreadable. Check again.

The verdict lands in DataHub’s own Quality tab.

Not a dashboard of Sidq's own. Each policy rule becomes a native DataHub Assertion through the documented custom-assertion API, reported by that rule's own severity, sitting beside the dataset's real owners and glossary terms — with sidq as the assertion platform.

DataHub's Quality tab for the products dataset: one passing assertion named Sidq policy rule sidq.verdict, platform sidq, category SIDQ.POLICY_RULE, from an External source.
DataHub OSS v1.5.0.6, captured in a logged-in browser session. The filter row reads Passing (1), SIDQ.POLICY_RULE (1), External (1). A rule that stops firing is retired once; an assertion an operator soft-deleted is never resurrected. The reproducible half — the GraphQL response DataHub's own UI issues — is in examples/06-native-assertion ↗.

One loop, through DataHub, end to end.

  1. Readgraph context through the official DataHub MCP server.
  2. Decideone deterministic policy returns PASS, WARN, or BLOCK. No model participates.
  3. Writeeach rule mirrors into DataHub’s own Quality tab as a native Assertion; a Receipt is persisted through official MCP only when asked.
  4. Read it backthe exact structured body, or the write stays write_unconfirmed.
  5. Re-verifya separate process re-reads the current context and judges again. An asset nobody examined stays NOT VERIFIED.

A mutation acknowledgement is not success. Sidq trusts a Receipt only after a direct read returns its exact structured body; a mismatch remains write_unconfirmed and triggers bounded compensation instead of becoming trusted state.

How the decision is reached — one picture
A change or agent question passes five evidence gates, one policy engine emits PASS, WARN or BLOCK, an opted-in audit can write a receipt into DataHub, and a later reader re-verifies it.

Gates collect evidence. One engine decides. No model participates in a blocking decision. Open full size ↗ · ARCHITECTURE.md ↗

06 The swarm

Four agents. No coordinator.
DataHub is the shared state.

Four concurrent auditors coordinate only through DataHub-native receipts—no IPC, message bus, lock service, or shared filesystem. Kill one mid-run; the others keep working, because nothing was ever assigned to it. A fifth, independent process reconstructs the ledger from DataHub alone.

The same receipts are also how Sidq remembers across time. An audit runs on a bounded budget, so a catalog larger than one budget used to re-examine its worst assets forever and defer the tail forever. sidq audit --resume re-reads the receipts earlier runs wrote back, re-derives whether each still holds under the current policy hash, and spends the budget on assets no run has reached — so repeated runs converge on coverage instead of circling. The memory lives in the catalog, not in a file beside the agent.

Recorded, not runnable here — and that is the point. Every proof button on this page runs a command that cannot write to a catalog. The swarm coordinates by writing, so it can never be one of them; a read-only swarm would demonstrate nothing. This is the verbatim output of make swarm-demo against a DataHub OSS v1.5.0.6 quickstart, run swarm-1786323944. Run it yourself with the five-command runbook.

  • 4 workers
  • 1 killed mid-run
  • 14 distinct assets in 18 examinations
  • 9 Receipts read by a process that examined nothing
The full recorded run
== four workers start together — no coordinator, no IPC ==

Swarm worker alpha
  examined          6
  receipts written  6
  vouched by peers  1 (beta) — their receipts held, so this worker moved on
Swarm worker beta
  examined          6
  receipts written  5
  vouched by peers  1 (alpha) — their receipts held, so this worker moved on
  write failures    1
Swarm worker gamma
  examined          6
  receipts written  3
  vouched by peers  4 (alpha, beta) — their receipts held, so this worker moved on

  >> killed delta mid-run — its unfinished assets were never assigned to it

== overlap, from the surviving workers' own reports ==
  worker reports found     3 of 4 expected
  total examinations       18
  distinct assets examined 14
  duplicated examinations  4

== the ledger, read from DataHub by a process that audited nothing ==
  current valid receipts   9 of 50
  alpha 4 · beta 2 · gamma 3

The overlap and ledger answer different questions. Each worker records a digest for every examination; when two workers touch the same asset, Sidq checks whether their independent conclusions agree and surfaces divergence without choosing a winner. That overlap comes from worker reports — DataHub stores only the latest receipt and cannot count collisions after the fact. The ledger comes from a fifth process that trusts no worker and reads current receipts from DataHub alone. The first measurement of this run failed, at 6 distinct assets of 18 examinations, and two defects were found and fixed because the number existed.

07 Reproduce

Re-derive the refusal on your machine.

git clone https://github.com/NexuChat/sidq.git cd sidq make gate-demo

All commands and integrations

Nine subcommands, one engine. Read-only unless a flag says otherwise; sidq --help is the source of truth for flags.

sidq check
Gate one SQL file or a Git diff. Local read; no catalog mutation.
sidq explain
Explain one deterministic rule.
sidq audit
Hunt catalog contradictions. Read-only; --write-receipts and --write-assertions are explicit opt-in, --resume converges coverage.
sidq claims
Compile documented promises into bounded read-only SQL against the source.
sidq verify
Independently re-check a Receipt and its context.
sidq repair
Propose and prove repairs. Dry run by default; only --apply mutates.
sidq swarm
Run one swarm worker. Writes current Receipts — an operator flow, never a public button.
sidq swarm-overlap
Measure worker overlap from local reports only.
sidq swarm-ledger
Read the swarm back out of DataHub as an independent observer.

The repository wraps the ones a reader is most likely to want. Two of the buttons above are literally these targets — Claims runs make claims-demo and the offline verdict runs make gate-demo; the other three call the sidq binary directly.

make doctor
Report what is present and what is missing before anything is run. Changes nothing.
make check
ruff, ruff format, mypy, then the whole suite with coverage — where the test count and coverage figure on this page come from.
make regen-check
Re-derive every generated artifact and fail if a committed copy has drifted from what the engine now produces.
make gate-demo
Re-derive the published verdict from the committed recording. Offline.
make claims-demo
Compile the documented promises and test them against live PostgreSQL.
make mcp-smoke
Prove the MCP server starts and exposes exactly the three read-only tools named above.

In CI, the same engine runs as a GitHub Action that publishes one sticky verdict comment and one check:

- uses: NexuChat/sidq@02969cb46a86c44a7b411ff98d9e05c4f6fd3c93
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    mode: live          # or: fixture
    repo-root: .

The bot writes through the issue-comments endpoint, but its target is a pull request, so the workflow needs pull-requests: write. issues: write reads as sufficient from that endpoint path and is not — GitHub answers 403. The committed workflow and docs/PR-BOT.md ↗ still carry the narrower scope; that is the red check on the demo pull requests above, and the corrected permission is the one written here. Full setup in docs/SETUP.md ↗.

صِدق — Sidq — means truthfulness: claim only what the evidence can support.

Upstream evidence: a DataHub packaging defect was reported before the deadline in datahub#19017 ↗ and later fixed by an equivalent merged change; the datahub-verify Skill PR datahub-skills#81 ↗ is open and unmerged.

No DataHub or credentials — the replay bootstraps a hash-locked environment and re-derives the published verdict byte-for-byte. Then open examples/01-blocked-pii-dashboard/verdict.json and compare hashes.

Signing in to the live DataHub: sidq-judge@local.invalid / sidq-judge-2026. Published deliberately, and deliberately powerless — the account holds DataHub's Reader role and nothing else. It reads every asset, lineage graph and Sidq receipt; every write is refused with 403 UNAUTHORIZED, rechecked before submission. This form has no private field, so any credential a judge needs is public by necessity, which is exactly why the published one cannot change anything.