Releases: flyersworder/agentic-data-contracts
Release list
v0.30.0 — verified-examples contract validation
Verified-examples contract validation
validate_examples(...) re-validates an external corpus of question → SQL examples against a DataContract using the same two-layer Validator that gates live agent queries. The corpus stays entirely yours (repo, YAML, human-reviewed MR flow); the framework contributes exactly one verb — validate.
Highlights
- Two uses of one call: an authoring-time MR gate (
if not report.ok: sys.exit(1)) and a contract-change drift sweep (report.violationsare what a schema/contract change just broke). - Each example gets one status —
valid/violation/unverified/unchecked— pluscontract_checked/engine_checkedflags.report.okis a safe gate: True only when every example isvalid. - Decision B: for SQL an engine parses but sqlglot cannot (e.g. Denodo/VDP), the engine's own planner renders the verdict —
unverified(plannable, policy not statically checked), never countedok. - Never executes the SQL; confirms allowed, well-formed, plannable against the current schema, not result-correctness.
- Runnable demo:
examples/revenue_agent/verify_examples.py(live DuckDB EXPLAIN, incl. a schema-drift catch).
Compatibility — backward compatible: one new defaulted field (ValidationResult.parse_error), a net-new module, four new exports, no new dependencies.
See the CHANGELOG and the "Validating a verified-examples corpus" README section for details.
v0.29.0 — metric decomposition reconciliation check
Added
Metric decomposition reconciliation check. New reconcile_decomposition(...) (in agentic_data_contracts.validation) executes a metric's declared decompositions against a live database and asserts the arithmetic identity holds within tolerance — the reconciliation half of Spec B. It catches an identity that has gone false in the data (ETL drift, a child metric SQL that diverged, a join that skews a population) which the per-query validators (sqlglot, EXPLAIN) never see because the SQL is still authorized.
The check is keyed off the declared decomposition — the contract owns what the identity is (operator + operand names), while the caller supplies scalar SQL for the parent and each declared operand, owning how to measure each over its chosen slice; no metric executor is assumed. Malformed input raises ValueError; data conditions are findings (reconciles=False with a mechanical reason) — a NULL / empty / non-finite measurement, or a ratio zero denominator. ReconciliationResult.reason reports only the mechanical condition and never infers the cause — diagnosis stays agent-owned. Default rel_tol=1e-4 is tight because decompositions are exact identities. Intended primary home is CI: a hermetic per-PR regression guard plus a live-warehouse nightly drift detector.
Compatibility
Fully backward compatible — a net-new module plus two new exports (reconcile_decomposition, ReconciliationResult); nothing existing changes behavior, no new dependencies.
Full changelog: https://github.com/flyersworder/agentic-data-contracts/blob/main/CHANGELOG.md
v0.28.1 — dump omit-empty fix + example/doc coverage
Patch release following v0.28.0 (metric identity decomposition).
Fixed
dump_semantic_sourcenow omits emptydecompositions/drill_by. 0.28.0 emitted both keys on every metric even when empty, which diverged from the tools layer's omit-when-empty convention and changed a frozen contract'scontract_digestfor a source that declares no decompositions — re-freezing a pre-0.28 contract produced a different content address than under 0.27.x. A leaf metric now dumps byte-identically to the pre-0.28 format, socontract_digestis stable across the upgrade for contracts that don't use the new fields. Metrics that do declare decompositions/drill_by are unaffected and still round-trip.
Docs
- The
revenue_agentandgrowth_agentexamples now demonstrate the feature —revenue_agentaproductidentity (total_revenue = active_customers × revenue_per_customer) plus aregiondrill,growth_agentaratioidentity onconversion_ratethat exercises a mixed identity + influence graph fortrace_metric_impacts'skindsfilter. - Completed decomposition coverage in
docs/architecture.md(tool signature,lookup_metricfields,MetricDefinitionfield list).
Full suite green (724 tests); ruff / ruff format / ty clean.
v0.28.0 — metric identity decomposition + drill dimensions
Added
- Metric identity decomposition. A metric can declare
decompositions— arithmetic identities describing how its value is exactly reconstructed from other metrics viasum,product,ratio, ordifference(e.g.total_revenue = product(paying_customers, arpu)). Unlike the causalmetric_impactsgraph (evidential, non-exhaustive), an identity decomposition is exact and exhaustive, so an agent doing root-cause analysis can walk the arithmetic skeleton deterministically before reaching for speculative drivers. Validated loudly at load (on both file load and frozen-contractfrom_rawrehydration): unknown operator, wrong operand arity (ratio/differencebinary;sum/product≥2), unresolved operand, and any cycle — identity edges must form a DAG. - Dimensional drill hints via
drill_by. A priority-ordered list of dimensional slice hints (dimension+schema.table.column) naming the exhaustive cuts (revenue GROUP BY region) that dominate weekly-review diagnosis. Columns are soft-validated (malformed shape raises; undeclared table is skipped). trace_metric_impactswalks both edge kinds. Decomposition operands becomeIdentityEdges sharing the metric graph withMetricImpact(influence) edges; the tool tags each edge with itskindand takes a newkindsargument (all|identity|influence, defaultall) so an agent can walk the deterministic identity skeleton first, then the causal drivers.lookup_metricsurfacesdecompositionsanddrill_bydirectly.
Compatibility
Fully backward compatible — both fields are new, optional, and default empty; existing contracts and dbt/Cube-sourced metrics behave identically. Extraction from dbt/Cube, an execution-based reconciliation check, and a variance-diagnosis tool are deferred; today the fields are YamlSource-only.
Full suite green (722 tests); ruff / ruff format / ty clean.
v0.27.0 — portable contracts + ARD publish path
Portable, self-contained contracts + Agentic Resource Discovery (ARD) publish path
Added
DataContract.freeze_semantic_source()— snapshots a contract's semantic source inline (metrics, relationships, metric-impacts, table column-schemas) so a serialized contract enforces identically on any machine with no filesystem access to the original dbt/Cube/YAML source. Freezing clears the machine-specificpathand normalizestype, so the content address is reproducible across machines and leaks no local paths. NewYamlSource.from_raw()↔dump_semantic_source()inverse pair — frozen snapshots are source-type-agnostic (dbt/Cube normalize to the canonical YAML-source shape).- New
agentic_data_contracts.ardmodule —build_catalog_entry()/build_ai_catalog()emit a spec-valid Agentic Resource Discoveryai-catalog.jsonentry for a contract-governed MCP server, with the frozen contract pinned as a digest-addresseddata-contractattestation in the trust manifest.contract_canonical_bytes()/contract_digest()produce the content-addressable artifact a consumer independently recomputes — so the publish→verify loop closes with no trust in the publisher's assertion.
Breaking (fail-loud)
- A declared-but-unavailable semantic source now raises
SemanticSourceUnavailableError(a new top-level export, deliberately not aFileNotFoundErrorsubclass) instead of a bareFileNotFoundError— covering missing file, a directory path, a permission error, malformed YAML, and malformed dbt JSON — so enforcement never silently degrades off the authoring box. Migration: code that caughtFileNotFoundErroraround contract/tool construction should catchSemanticSourceUnavailableError. - The
SemanticSourceprotocol gainsget_table_schemas()(affects only third-party custom sources that are frozen).SemanticSource.pathis now optional, gated by a validator requiring path-or-inline. - Existing contracts and the common path (source present, or letting errors propagate) are unaffected.
Built TDD red-first and hardened by two independent code-review passes. 688 tests green; ruff + ty clean. Full details in CHANGELOG.md.
v0.26.0 — metric-first domain membership
Metric-first domain membership
Domain↔metric membership is now declared in one place: each metric self-declares its domains (domains: [...], read from meta.domains by the yaml/dbt/cube adapters). The contract's Domain carries catalog metadata only (summary, description, owners, review cadence) and no longer lists its metrics — the old Domain.metrics field and the union shim that reconciled it are gone. The catalog is authoritative for which domains exist; list_metrics(domain=), lookup_domain, and the system-prompt domain index all agree on that universe.
⚠️ Breaking changes
Domain.metricsremoved;Domainnow setsextra="forbid". A pre-0.26 contract that still listsmetrics:under a domain raises aValidationErrorat load time (rather than silently dropping it and leaving the domain empty). Migration is mechanical: delete thosemetrics:lines — each metric already declares its domain in the semantic source.lookup_domainmembership is reverse-looked-up frommetric.domains, so it returns every metric that declares the domain. With no semantic source configured it returns an empty member list.- The
[pydantic-ai]extra now requirespydantic-ai-slim[anthropic]>=2.0.0(was>=1.107.0); the adapter is verified against 2.x.
Other
- Restored a metric-first membership-validation warning (a metric declaring an uncataloged domain now warns at startup).
- Per-domain
metric_counttallied via a dedupe-safedomain_metric_countshelper (O(metrics), not O(domains×metrics)). - Dependencies refreshed (
uv lock --upgrade); pre-commit hooks bumped (prek autoupdate). - Hardened across two code-review passes. 663 tests; ruff/format/ty clean.
See CHANGELOG.md for full detail.
v0.25.0 — dual-role ownership + per-metric freshness
Governance metadata mined from Lyft's Metric Semantic Layer article (lessons #1 dual-role ownership, #3 freshness surfacing).
Added
- Dual-role ownership — optional
business_owner/operational_owner(teams, not individuals) onMetricDefinitionandDomain. Business owner owns the definition + review cadence; operational owner owns data health. - Per-metric
last_reviewed+ metric staleness —find_stale_reviews()/DataContract.find_stale()now audit metrics as a third artefact kind (domain/metric/metric_impact), with owners carried in findingcontextso the report says who to nag. - Owners + freshness in the agent-facing tools —
lookup_metric/lookup_domainsurface owners +last_reviewed+ astaleflag;list_metricscarries a leanstaleflag.create_tools(..., staleness_threshold_days=90). - Two audiences, two policies —
find_stale()is the strict governance/CI audit (missinglast_reviewed= stale); the agent-facing tools are lenient (emitstale/last_reviewedonly when set).
Compatibility
- Additive API (all new fields optional; new params keyword-only). Behavior change:
DataContract.find_stale()now also flags un-reviewed metrics — grandfather withf.age_days is not Noneor back-filllast_reviewed. dbt/Cube sources leave the new fields unset for now.
Full notes in CHANGELOG.md.
v0.24.0 — deps-aware Pydantic AI toolset
Deps-aware Pydantic AI toolset — one shared Agent for many users
create_pydantic_ai_toolset(contract, ...) returns a Pydantic AI ToolsetFunc you register on a single shared agent via agent.toolset(per_run_step=False)(...). On each run it reads a per-user ContractDeps (session + caller_principal) from RunContext.deps and rebuilds the contract's tools bound to that user — so you build the Agent once and each user is just a message_history + a small ContractDeps, instead of a separate per-user tools list.
caller_principalpassthrough added tocreate_pydantic_ai_tools, so per-principal table/rule gating applies in the baked-in path too.- Enforcement unchanged: validation block →
ModelRetry; session-budget breach → terminalContractSessionLimitError. - Built on Pydantic AI's dynamic-toolset mechanism (not a hand-rolled
AbstractToolset). Purely additive — base installs and existing adapters are unaffected. - End-to-end isolation proven through
agent.run()on one shared agent (user A exhausting their budget doesn't affect user B).
CI / security
uv lock --upgrade clears 3 transitive advisories (langchain → 1.3.10, langsmith → 0.8.18, pydantic-settings → 2.14.2); uv-secure reports no vulnerabilities. Full suite 635 green; ruff + ty clean.
See CHANGELOG.md for full details.
v0.21.1 (data-plugin skills overlay + dependency refresh)
Added
- Reference template for layering Anthropic's
dataknowledge-work plugin on top of contract-governed tools. All three example agents (revenue_agent,growth_agent,ops_agent) carry an opt-in overlay (off by default, enabled viaDATA_PLUGIN_PATH). The agent gains the plugin's analyst skills (validate-data,statistical-analysis,explore-data,sql-queries) while every query stays contract-enforced.growth_agent/agent.pyis the canonical template; README documents the pattern. - Security guard:
strict_mcp_config=Truerestricts the session to the governed in-process server only, so the plugin's bundled.mcp.jsonwarehouse servers stay inert and the agent has no ungoverned path around the contract. Skill list deliberately omitsdata-context-extractorand viz/dashboard skills.
Compatibility
- No library API change — touches only
examples/, docs, lockfile, and tooling. Patch bump. - Opt-in and degrades gracefully — feature-detects
plugins/skills/strict_mcp_configonClaudeAgentOptions, so examples run unchanged on older SDKs and with zero setup.
Internal
uv lock --upgrade:claude-agent-sdk 0.1.81 → 0.2.87(overlay API verified intact across the jump), pluslangchain 1.3.0→1.3.2,langgraph 1.2.0→1.2.2,duckdb 1.5.2→1.5.3,snowflake-connector-python 4.5.0→4.6.0,mcp 1.27.1→1.27.2,ruff 0.15.13→0.15.15, and others. Full 602-test suite + ruff + ty green.prek autoupdate:ruff-pre-commit v0.15.13 → v0.15.15.
v0.21.0 — describe_table emits column descriptions
Fixed
describe_table now emits column descriptions to the agent. Since the tool factory's first commit, the tool serialised columns as {name, type, nullable} only — Column.description was silently dropped on the way out, even when populated by the adapter (e.g., a Denodo deployment carrying authored catalog comments) or available in the contract's semantic source. This is the single largest context improvement a data-contract library can make: per the Datacult "boring work" benchmark, adding column descriptions moved an agent's SQL accuracy from 0% to 15% and SQL generation from 38.5% to 100% — the largest jump in their six-layer experiment.
The fix overlays descriptions onto the tool response with this precedence:
- Semantic source via
SemanticSource.get_table_schema(schema, table)— the canonical agent-facing authority Column.descriptionfrom the adapter — captures warehouse catalog comments- Field omitted entirely when both are empty, keeping responses tight
The SemanticSource.get_table_schema protocol method is no longer dead code from the tool layer's perspective. All three built-in semantic sources (YamlSource, DbtSource, CubeSource) already populated TableSchema.columns[*].description from their respective inputs; the tool just never consulted them. Now it does.
Added
- 3 new tests in
tests/test_tools/test_factory.pypinning the merge behaviour: semantic-source descriptions reach the agent, adapter-supplied descriptions surface when the semantic source has no entry (with the field omitted when both are empty), and semantic source wins when both have descriptions for the same column.
Compatibility
- Backward-compatible response shape. The new `description` field is additive only — consumers that ignore unknown keys see no behaviour change. The field is omitted (not set to `""`) when no description exists, so JSON payload size is unchanged for description-less columns.
- No new failure modes. The merge guards `semantic_source is None`, `get_table_schema(...)` returning `None`, columns appearing in one source but not the other, and empty-string descriptions. A column described in the semantic source but absent from the warehouse is silently dropped — the adapter's column list is the source of truth for which columns exist; the semantic source only adorns them.
- No new dependencies. The fix uses interfaces that already existed in the codebase.
Internal
- `uv lock --upgrade` refreshed transitive dependencies (notable bumps: `sqlglot 30.6.0 → 30.8.0`, `langchain 1.2.17 → 1.3.0`, `langgraph 1.1.10 → 1.2.0`, `pydantic 2.13.3 → 2.13.4`, `cryptography 47.0.0 → 48.0.0`). Full 602-test suite + ruff + ty all green against the new versions.
- `.pre-commit-config.yaml`: `ruff-pre-commit` rev bumped to `v0.15.13` to match the lockfile-pinned `ruff` binary.