Skip to content

Commit 30e6564

Browse files
authored
feat: verified-examples contract validation (v0.30.0) (#40)
validate_examples re-checks an external question->SQL corpus against a DataContract via the same two-layer Validator that gates live queries. Corpus stays external; framework contributes one verb, validate. Adds a decision-B engine fallback for sqlglot-unparseable SQL (surfaced as unverified, never counted ok), a strict safe-gate report.ok, and a runnable DuckDB demo. Sole core change: ValidationResult.parse_error. Backward compatible; no new dependencies. Two workflow reviews + full SDD review chain.
1 parent 2ff5a46 commit 30e6564

14 files changed

Lines changed: 1012 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.30.0] - 2026-07-19
6+
7+
### Added
8+
9+
- **Verified-examples contract validation.** New `validate_examples(...)` (in `agentic_data_contracts.validation`) re-validates an external corpus of `question → SQL` examples against a `DataContract` using the **same two-layer `Validator`** that gates live agent queries — Layer 1 (sqlglot static analysis: allowed tables, forbidden ops, required filters, `SELECT *`) always, plus Layer 2 (a live `EXPLAIN` dry-run) when a database adapter is supplied. The examples database stays **entirely external** — your repo, your YAML, your human-reviewed MR flow; the framework never stores, loads, retrieves, or executes the corpus, contributing exactly one verb: *validate*. The interchange is a plain `VerifiedExample` dataclass (`sql` is the only load-bearing field; `VerifiedExample.from_dict(...)` is a shape adapter that preserves unknown keys under `.metadata` and never interprets them). The result is an `ExampleValidationReport` of `ExampleResult`s, each with exactly one `status` — `valid` (statically contract-checked *and* passed), `violation` (a check rejected it), `unverified` (**decision B**, below), or `unchecked` (no verdict possible) — and two flags, `contract_checked` and `engine_checked`, recording *what* was verified, plus `unverified_compliance`, a markdown `summary()`, and `ok`. `report.ok` is a **safe CI gate**: True only when *every* example is `valid`, so `if not report.ok: sys.exit(1)` fails on violations, unchecked, *and* unverified rows alike (test `report.violations` directly for a laxer gate). Two uses of the same call: an **MR gate** (validate the corpus in CI before a human reviews it) and a **drift sweep** (re-run against a changed contract; `report.violations` are the examples the change — or a dropped/renamed column caught by the live EXPLAIN — just broke). The verdict is honest about its own reach: it confirms an example is still *allowed, well-formed, and plannable against the current schema* — never that it still returns the right answer, because it **never executes** the SQL (result correctness stays with the human review). For SQL an engine parses but sqlglot cannot (e.g. Denodo/VDP), a parse failure falls back to the engine's own planner (decision B): the engine vouches for plannability but contract policy is never statically checked, so the example is `unverified`, not `valid`. A per-example guard degrades any example whose adapter raises to `unchecked` rather than aborting the batch.
10+
11+
### Compatibility
12+
13+
- **Backward compatible.** One new field on `ValidationResult``parse_error: bool` (defaults `False`, appended after existing fields) — is the only change to core validation; it lets callers distinguish a parse failure from a policy block and drives the decision-B fallback. Otherwise a net-new module plus four new exports (`VerifiedExample`, `ExampleResult`, `ExampleValidationReport`, `validate_examples`) from `agentic_data_contracts.validation`; nothing existing changes behavior, and no new dependencies are added.
14+
15+
### Docs
16+
17+
- New runnable demo `examples/revenue_agent/verify_examples.py` + `verified_examples.yml`: validates an external corpus against the revenue contract with a **live DuckDB EXPLAIN**, showing a valid example, static violations, a **schema-drift catch only the dry-run finds**, and the same SQL diverging `valid` / `violation` by principal. New README section "Validating a verified-examples corpus", and the previously-undocumented `reconcile_decomposition` (0.29.0) is now cross-referenced there.
18+
19+
### Internal
20+
21+
- New `validation/examples.py`, built across 6 TDD tasks (red-first) with a per-task spec+quality review, a three-lens plan review *before* execution, and a final whole-branch review. `engine_checked` is reconstructed from `schema_valid` / `estimated_*` rather than a second core field, so the only `validator.py` change stays `parse_error`. The `SemanticSource` type import is guarded under `TYPE_CHECKING` (annotation-only, cycle-safe, matching the existing `validator.py` pattern). Full suite green (785 tests); `ruff` / `ruff format` / `ty` clean.
22+
523
## [0.29.0] - 2026-07-19
624

725
### Added

README.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ You teach agents your business domains, metrics, and governance rules upfront
1414

1515
- **Governed, not guessed** — the agent uses *your* metric definitions (`SUM(amount) FILTER (WHERE status = 'completed')`), not an ad-hoc query it invented.
1616
- **Bad SQL blocked before execution** — forbidden operations, disallowed tables, missing tenant filters, `SELECT *`, unbounded scans — caught by static analysis plus an optional EXPLAIN dry-run.
17+
- **Validate a whole corpus, not just live queries** — re-check a verified-examples database (or a metric's arithmetic identity) against the contract in CI, and catch drift when the contract or the warehouse schema changes.
1718
- **Business context first** — domain descriptions, metric ownership, freshness, and a metric graph (causal *and* arithmetic) guide the agent before it writes a line of SQL.
1819
- **Resource governance built in** — per-session cost, retry, row, and token budgets, and wall-clock limits.
1920
- **Per-caller row/column security** — allow/deny tables and filter values by principal, for multi-user bots.
@@ -777,7 +778,33 @@ await trace.callable({
777778
# "kind": "identity", "operator": "product"}
778779
```
779780

780-
Today `decompositions` / `drill_by` are declared directly in YAML contracts; dbt/Cube extraction, an execution-based reconciliation check, and a variance-diagnosis tool are deferred.
781+
Today `decompositions` / `drill_by` are declared directly in YAML contracts; dbt/Cube extraction and a variance-diagnosis tool are deferred.
782+
783+
## Validating a verified-examples corpus
784+
785+
If you keep a corpus of known-good `question → SQL` examples — the kind an analytics agent accumulates from real sessions and promotes through review — `validate_examples` re-checks each example's SQL against a contract using the *same* two-layer `Validator` that gates live queries. The corpus stays entirely yours (your repo, your format, your review flow); the library never stores, loads, or executes it — it contributes exactly one verb, *validate*.
786+
787+
```python
788+
from agentic_data_contracts import DataContract
789+
from agentic_data_contracts.validation import VerifiedExample, validate_examples
790+
791+
contract = DataContract.from_yaml("contract.yml")
792+
examples = [VerifiedExample.from_dict(row) for row in load_your_yaml()] # you own the load step
793+
794+
report = validate_examples(examples, contract, explain_adapter=adapter) # adapter → live EXPLAIN
795+
if not report.ok:
796+
print(report.summary()) # markdown, ready to post as an MR comment
797+
# in CI: sys.exit(1)
798+
```
799+
800+
Each example lands in exactly one `status` — `valid` (statically contract-checked and passed), `violation` (a check rejected it), `unverified` (the engine planned it but policy couldn't be statically checked — see below), or `unchecked` (no verdict possible) — with two flags, `contract_checked` and `engine_checked`, recording *what* was verified. `report.ok` is a **safe gate**: it is True only when *every* example is `valid`, so `if not report.ok: sys.exit(1)` fails on violations, unchecked, *and* unverified rows (test `report.violations` directly for a laxer gate). Two uses of the same call:
801+
802+
- **MR gate** — validate the corpus in CI *before* a human reviews it; fail on `not report.ok`, so the human is no longer the only check.
803+
- **Drift sweep** — re-run against a *changed* contract; `report.violations` are the examples the change just broke. With an `explain_adapter`, the live EXPLAIN also catches a dropped or renamed column that static checks can't see.
804+
805+
It confirms an example is still *allowed, well-formed, and plannable against the current schema* — never that it still returns the right answer, because it **never executes** the SQL (result correctness stays with your review). For SQL an engine parses but sqlglot cannot (e.g. Denodo/VDP), a parse failure falls back to the engine's own planner; those pass as plannable but policy-unverified, flagged in `report.unverified_compliance`. See [`examples/revenue_agent/verify_examples.py`](examples/revenue_agent/verify_examples.py) for a runnable, DuckDB-backed demo.
806+
807+
Its sibling `reconcile_decomposition(...)` applies the same CI-first, contract-relative spirit to a metric's declared arithmetic identity, executing the `decompositions` above against live data to assert the identity still holds within tolerance.
781808

782809
## Custom Prompt Rendering
783810

@@ -889,6 +916,12 @@ Each example directory contains four files:
889916
- `setup_db.py` — sample DuckDB data (auto-created on first run)
890917
- `agent.py` — runnable demo with a Claude Agent SDK path plus a fallback that exercises the tools directly
891918

919+
`revenue_agent` additionally ships a **verified-examples validation** demo — `verified_examples.yml` (an external corpus) and `verify_examples.py`, which re-checks it against the contract with a live DuckDB EXPLAIN (valid, static violations, a schema-drift catch only the dry-run finds, and the same SQL diverging by principal):
920+
921+
```bash
922+
uv run python examples/revenue_agent/verify_examples.py
923+
```
924+
892925
Reading all three gives you a complete tour of the library's design space: different enforcement levels (`block` / `warn` / `log`), different impact confidences and directions, and resource profiles tuned for very different user-latency expectations.
893926

894927
All three `agent.py` files also carry the [`data`-plugin skills overlay](#layer-anthropics-data-plugin-on-top-governed-analyst-skills) behind an opt-in `DATA_PLUGIN_PATH` env var (off by default, so the examples run with zero external setup):

docs/architecture.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,12 @@ SQL string
341341
→ return results
342342
```
343343
344+
### Batch validation: verified-examples corpus
345+
346+
`validate_examples(examples, contract, ...)` (in `agentic_data_contracts.validation`) runs an **external** corpus of `question → SQL` examples through the *same* `Validator` used for live queries — no parallel checking path. The corpus (a human-reviewed examples database) lives outside the library; the framework only re-validates each `sql`. One `Validator` is built per distinct `example.principal`, so per-principal rules are checked under the right identity, and input order is preserved.
347+
348+
Each example maps to an `ExampleResult` with exactly one `status` — `valid` (Phase 1 static checks ran *and* passed), `violation` (a check rejected it), `unverified` (decision B, below), or `unchecked` (no verdict) — and two flags: `contract_checked` (Phase 1 static checks ran; requires a successful sqlglot parse) and `engine_checked` (Phase 2 EXPLAIN ran). `engine_checked` is reconstructed from the returned `ValidationResult` (`schema_valid` / `estimated_*`) — the only core addition this feature makes to `ValidationResult` is a `parse_error` flag. When sqlglot cannot parse the SQL but an `ExplainAdapter` is present (**decision B**, for engines sqlglot does not model, e.g. Denodo/VDP), the engine is asked to plan it directly; such a pass verifies *plannability* but not *contract policy* (no AST for the static checkers), so it takes status `unverified` (`contract_checked=False`), surfaced in `report.unverified_compliance`. `report.ok` is a safe CI gate: True only when every example is `valid`, so violations, unchecked, *and* unverified rows all fail it. A per-example guard degrades any example whose adapter raises to `unchecked` rather than aborting the batch. Two intended triggers, one call: an authoring-time **MR gate** (Layer 1 only, no warehouse needed) and a contract-change **drift sweep** (with an adapter, so the live EXPLAIN catches schema drift). It never executes the SQL — the sibling `reconcile_decomposition(...)` covers execution-based identity checks.
349+
344350
## Tools Layer (Claude Agent SDK Integration)
345351
346352
Two modes: tool factory for quick starts, middleware for BYO tools.

examples/revenue_agent/agent.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,11 @@ def main() -> None:
9595
semantic = YamlSource(EXAMPLE_DIR / "semantic.yml")
9696

9797
db_path = EXAMPLE_DIR / "sample_data.duckdb"
98-
if not db_path.exists():
99-
sys.path.insert(0, str(EXAMPLE_DIR))
100-
from setup_db import setup # type: ignore[import]
98+
sys.path.insert(0, str(EXAMPLE_DIR))
99+
from setup_db import ensure_sample_db # type: ignore[import]
101100

102-
setup(str(db_path))
103-
sys.path.pop(0)
101+
ensure_sample_db(str(db_path))
102+
sys.path.pop(0)
104103
adapter = DuckDBAdapter(str(db_path))
105104

106105
tools = create_tools(dc, adapter=adapter, semantic_source=semantic)

examples/revenue_agent/setup_db.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,13 @@ def setup(db_path: str = "sample_data.duckdb") -> None:
5151
print(f"Sample database created at {db_path}")
5252

5353

54+
def ensure_sample_db(db_path: str = "sample_data.duckdb") -> None:
55+
"""Create the sample database only if it does not already exist."""
56+
from pathlib import Path
57+
58+
if not Path(db_path).exists():
59+
setup(db_path)
60+
61+
5462
if __name__ == "__main__":
5563
setup()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# An EXTERNAL "verified examples" corpus — the kind your analytics agent's
2+
# lessons-learned MR flow produces (human-reviewed question -> SQL pairs). The
3+
# framework never owns or stores this file; `validate_examples` only re-checks
4+
# each `sql` against the contract. Non-`sql` keys (type, verified_by,
5+
# last_verified) are preserved untouched in each example's `.metadata`.
6+
7+
# 1. Valid — has tenant_id, explicit columns, and the real columns exist, so it
8+
# passes static checks AND the live DuckDB EXPLAIN.
9+
- id: revenue-by-region
10+
question: "total revenue by region"
11+
sql: >
12+
SELECT c.region, SUM(o.amount) AS revenue
13+
FROM analytics.orders o
14+
JOIN analytics.customers c ON o.customer_id = c.id
15+
WHERE o.tenant_id = 'acme' AND o.status = 'completed'
16+
GROUP BY c.region
17+
type: sql
18+
verified_by: data-eng-finance
19+
last_verified: 2026-06-01
20+
21+
# 2. Contract violation — missing the required tenant_id filter. Blocked
22+
# statically (Layer 1), so the dry run is skipped (engine_checked stays False).
23+
- id: orders-missing-tenant-filter
24+
question: "list recent order amounts"
25+
sql: "SELECT id, amount FROM analytics.orders"
26+
type: sql
27+
28+
# 3. Contract violation — SELECT * is forbidden by the no_select_star rule.
29+
- id: customers-select-star
30+
question: "show all customer fields"
31+
sql: "SELECT * FROM analytics.customers WHERE tenant_id = 'acme'"
32+
type: sql
33+
34+
# 4. SCHEMA DRIFT — passes every static check (allowed table, tenant filter,
35+
# explicit columns), but `discount_pct` no longer exists in the warehouse.
36+
# Only the live EXPLAIN catches it: status violation, engine_checked True.
37+
# This is the payoff a static-only gate cannot deliver.
38+
- id: orders-dropped-column
39+
question: "orders with discount percentage"
40+
sql: "SELECT o.id, o.discount_pct FROM analytics.orders o WHERE o.tenant_id = 'acme'"
41+
type: sql
42+
verified_by: data-eng-finance
43+
last_verified: 2025-11-01
44+
45+
# 5 & 6. Principal-scoped — the SAME SQL, validated under two different
46+
# principals. customer_id 101 is in partner-a's allowlist [101,102,103] so it
47+
# is VALID for them, but not in partner-b's [201,202] so it is a VIOLATION for
48+
# them. Each example is checked as its own identity — this is why the corpus
49+
# can carry a `principal` per row.
50+
- id: partner-a-own-orders
51+
question: "partner A's orders for customer 101"
52+
sql: "SELECT id, amount FROM analytics.orders WHERE tenant_id = 'acme' AND customer_id = 101"
53+
principal: partner-a@external.com
54+
type: sql
55+
verified_by: partner-success
56+
last_verified: 2026-05-20
57+
58+
- id: partner-b-foreign-customer
59+
question: "partner B querying customer 101 (not theirs)"
60+
sql: "SELECT id, amount FROM analytics.orders WHERE tenant_id = 'acme' AND customer_id = 101"
61+
principal: partner-b@external.com
62+
type: sql
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Validate an external verified-examples corpus against the revenue contract.
2+
3+
Run:
4+
uv run python examples/revenue_agent/setup_db.py # once — builds the DuckDB
5+
uv run python examples/revenue_agent/verify_examples.py
6+
7+
What this shows
8+
---------------
9+
``verified_examples.yml`` is an *external* corpus of question -> SQL pairs — the
10+
kind your analytics agent's lessons-learned MR flow produces. The framework never
11+
owns that file; ``validate_examples`` only re-checks each ``sql`` against the
12+
contract, using the SAME two-layer Validator that gates live agent queries:
13+
14+
* Layer 1 (static) — allowed tables, forbidden ops, required filters, no SELECT *
15+
* Layer 2 (dry run) — a live DuckDB ``EXPLAIN``, so schema drift (a dropped or
16+
renamed column) is caught even when the static contract
17+
cannot see it
18+
19+
Each example lands in exactly one status — ``valid`` (contract-checked and
20+
passed), ``violation`` (a check rejected it), ``unverified`` (engine-planned but
21+
policy not statically checked; see the note below), or ``unchecked`` (no verdict)
22+
— with two flags, ``contract_checked`` and ``engine_checked``, recording *what*
23+
was verified. ``report.ok`` is True only when every example is ``valid``. Extra
24+
YAML keys (``type``, ``verified_by``, ``last_verified``) are preserved untouched
25+
in ``.metadata``.
26+
27+
Two real uses of the same call:
28+
* MR gate — validate the corpus in CI before a human reviews; ``sys.exit(1)``
29+
when ``not report.ok``.
30+
* Drift sweep — re-run against a changed contract; ``report.violations`` are the
31+
examples the change (or schema drift) just broke.
32+
33+
Note: the decision-B engine fallback (asking the engine to parse SQL sqlglot
34+
cannot) never triggers here — DuckDB and sqlglot both parse standard SQL. It
35+
earns its keep on engines sqlglot does not model, e.g. Denodo/VDP.
36+
"""
37+
38+
from __future__ import annotations
39+
40+
import sys
41+
from pathlib import Path
42+
43+
import yaml
44+
45+
from agentic_data_contracts import DataContract
46+
from agentic_data_contracts.adapters.duckdb import DuckDBAdapter
47+
from agentic_data_contracts.semantic.yaml_source import YamlSource
48+
from agentic_data_contracts.validation import VerifiedExample, validate_examples
49+
50+
EXAMPLE_DIR = Path(__file__).parent
51+
52+
53+
def main() -> None:
54+
contract = DataContract.from_yaml(EXAMPLE_DIR / "contract.yml")
55+
semantic = YamlSource(EXAMPLE_DIR / "semantic.yml")
56+
57+
db_path = EXAMPLE_DIR / "sample_data.duckdb"
58+
sys.path.insert(0, str(EXAMPLE_DIR))
59+
from setup_db import ensure_sample_db # type: ignore[import]
60+
61+
ensure_sample_db(str(db_path))
62+
sys.path.pop(0)
63+
adapter = DuckDBAdapter(str(db_path))
64+
65+
# You own this load step — the framework never reads your corpus for you.
66+
raw = yaml.safe_load((EXAMPLE_DIR / "verified_examples.yml").read_text())
67+
examples = [VerifiedExample.from_dict(row) for row in raw]
68+
69+
report = validate_examples(
70+
examples,
71+
contract,
72+
dialect=adapter.dialect, # so Layer 1 parses in the engine's dialect
73+
explain_adapter=adapter, # enables the live DuckDB EXPLAIN dry run
74+
semantic_source=semantic,
75+
)
76+
77+
print("=== Verified-examples validation (live, DuckDB EXPLAIN) ===\n")
78+
for r in report.results:
79+
label = r.example.id or r.example.question or "<unnamed>"
80+
flags = (
81+
f"contract_checked={r.contract_checked} engine_checked={r.engine_checked}"
82+
)
83+
print(f"[{r.status.upper():9}] {label} ({flags})")
84+
for reason in r.reasons:
85+
print(f" reason: {reason}")
86+
for warning in r.warnings:
87+
print(f" warning: {warning}")
88+
89+
print("\n--- report.summary() (ready to post as an MR comment) ---")
90+
print(report.summary())
91+
92+
print(
93+
f"\nGate: ok={report.ok} "
94+
f"({len(report.valid)} valid, {len(report.violations)} violation(s), "
95+
f"{len(report.unchecked)} unchecked, "
96+
f"{len(report.unverified_compliance)} plannable-but-unverified)"
97+
)
98+
# In CI you would gate the merge on this: if not report.ok: sys.exit(1)
99+
100+
101+
if __name__ == "__main__":
102+
main()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "agentic-data-contracts"
3-
version = "0.29.0"
3+
version = "0.30.0"
44
description = "YAML-first, domain-driven data governance for AI agents"
55
readme = "README.md"
66
requires-python = ">=3.12"

0 commit comments

Comments
 (0)