ACE-073: CrossDatasourceMetric schema + deployment validation - #163
ACE-073: CrossDatasourceMetric schema + deployment validation#163ashwin-agami wants to merge 7 commits into
Conversation
Add the deployment-level cross-datasource metric schema: SubMeasure (one
per-datasource piece) and CrossDatasourceMetric (>=2 pieces across >=2
datasources, reconciled on a shared key, combined by a {alias} formula).
Add a cross_datasource_metrics bucket to OrgRecord. Add 'federated' to the
Executable literal (metric-only) and reject it on the join types
(Relationship, CrossDatasourceRelationship) for defense-in-depth.
Merge the metrics bucket from inline organization.yaml + an optional root sidecar cross_datasource_metrics.yaml, deduped by name (metrics carry a required name, unlike anonymous bridges). Lenient read-only load: a corrupt file or malformed entry is skipped, matching the bridge loader.
…(ACE-073) Add a metric loop after the bridge loop: reject a sub-measure pointing at an unattached datasource or an unresolved grain column, a reconcile_on key with no declared bridge linking the pieces' datasources (fail-closed), a combine formula referencing an unknown alias, and a metric spanning < 2 datasources. Fix the fail-open bug: bail only when there are neither bridges nor metrics, so metric checks run even with zero bridges.
…-073) New self-contained test suite (synthetic acme_crm/acme_erp on account_key): model shape validator, deployment validation (valid metric; missing bridge, unknown datasource/grain column, bad combine alias, single-datasource, wrong reconcile key, unreadable datasource piece all rejected), federated accept-on-metric/reject-on-join-types, and loader inline+sidecar merge, leniency, dedup-by-name. 100% patch coverage on touched lines.
… combine, grain (ACE-073)
There was a problem hiding this comment.
Pull request overview
This PR implements the ACE-073 “cross-datasource metric” representation + validation slice of F16 by adding a first-class CrossDatasourceMetric (with SubMeasure) to the semantic model, loading/merging it into the deployment OrgRecord, and enforcing deployment-level fail-closed validation rules (bridge connectivity, piece resolution, and combine-formula correctness).
Changes:
- Adds
SubMeasure+CrossDatasourceMetricmodels, introducesexecutable="federated"as a metric-only value, and rejects it on join edge types. - Extends
OrgRecordloading to mergecross_datasource_metricsfrom inline + optional sidecar YAML, with leniency and dedup-by-name. - Extends
validate_deployment()to validate cross-datasource metrics (pieces, bridge connectivity via connected-component, andcombineplaceholder refs) even when there are zero bridges.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
packages/agami-core/src/semantic_model/models.py |
Adds the new metric schema (SubMeasure/CrossDatasourceMetric), updates Executable, and extends OrgRecord to include metrics. |
packages/agami-core/src/semantic_model/org_record.py |
Loads/merges cross-datasource metrics from a new sidecar file, with leniency + dedup behavior. |
packages/agami-core/src/semantic_model/validator.py |
Adds deployment-level validation for cross-datasource metrics and adjusts early-return behavior to ensure metrics are always validated when present. |
tests/test_cross_datasource_metric.py |
Adds model + deployment validation coverage, including loader leniency and dedup scenarios. |
Comments suppressed due to low confidence (1)
packages/agami-core/src/semantic_model/models.py:816
- CrossDatasourceMetric._shape() checks grain is non-empty and has the same arity as reconcile_on, but it doesn’t reject duplicate grain columns (e.g. ["account_key","account_key"]). Since grain defines the grouping key for each piece, duplicates are almost certainly a modeling error and will later complicate reconciliation/execution.
# Each piece groups by its LOCAL version of the composite key, so its grain must be non-empty
# and carry exactly one column per reconcile_on component — a 2-part key needs a 2-column grain.
for sm in self.sub_measures:
if not sm.grain:
raise ValueError(
f"cross-datasource metric {self.name!r}: sub_measure {sm.alias!r} has an empty grain "
"— each piece must group by its local key column(s)"
)
if len(sm.grain) != len(self.reconcile_on):
raise ValueError(
f"cross-datasource metric {self.name!r}: sub_measure {sm.alias!r} grain {sm.grain} "
f"has {len(sm.grain)} column(s) but reconcile_on {self.reconcile_on} has "
f"{len(self.reconcile_on)} — each piece groups by its local version of the composite key"
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if isinstance(doc, dict): | ||
| return doc.get("relationships") or [] | ||
| return doc.get(key) or [] | ||
| if isinstance(doc, list): | ||
| return doc | ||
| return [] |
| if not self.reconcile_on: | ||
| raise ValueError( | ||
| f"cross-datasource metric {self.name!r}: reconcile_on (the shared key) must be non-empty" | ||
| ) | ||
| # `combine` is the formula that stitches the pieces together — a blank one is a metric that |
…st bridge entries (Copilot review) (ACE-073) - _shape now rejects a reconcile_on or grain with duplicate columns (a dup would set-match a shorter bridge key and skew the arity check). - _bridge_entries returns [] for a non-list value under the key, instead of iterating a scalar string char-by-char. Tests for both.
|
Thanks @copilot — both valid, fixed in 5a7cfaa.
Both locked by tests: |
Spec: ACE-073
Stacked on #156 (ACE-072). Base is the
ACE-072-bridge-as-model-typebranch, so this PR shows only the ACE-073 delta. Retarget tomainonce #156 merges. (Both branches carry a merge ofmain, incl. Sandeep'smcp<2pin #159 + the sql-guard fix #153.)Summary
Second slice of F16 — cross-datasource metrics: the metric itself. Adds a first-class
CrossDatasourceMetric(+SubMeasure) — per-datasource "pieces" (native aggregate bindings) reconciled on a shared key via a{alias}combine formula — plus deployment-level validation. Representation + validation only; execution is ACE-074.Changes
models.py—SubMeasure(datasource/binding/grain/alias) +CrossDatasourceMetric. Model validator enforces:executable == "federated", ≥2 pieces across ≥2 datasources, non-emptyreconcile_on, non-empty + unique aliases, eachgrainnon-empty with arity ==reconcile_on, non-emptycombine. Adds"federated"to theExecutableliteral and rejects it on the join types.cross_datasource_metricsbucket onOrgRecord.org_record.py— load/merge the metrics bucket (inline + optional sidecar), dedup byname, per-entry + corrupt-file leniency (mirrors the bridge loader).validator.py— metric checks folded intovalidate_deployment, all fail-closed: unattached datasource / unresolvedgraincolumn / no bridge connecting the pieces onreconcile_on(connected-component check) /combinenot referencing exactly the pieces / single-source. Plus the fail-open fix: run metric checks even when there are zero bridges.tests/test_cross_datasource_metric.py(30): model rejections, every deployment reject path, the disconnected-datasources repro, a composite-key happy path,federated-rejected-on-joins, dedup, leniency.Test plan
uv run dev.py checkgreen (1636 passed); 100% patch coverage on touched lines.combinemust reference exactly the pieces,grainnon-empty/arity).Checklist
Spec: ACE-073), scope-contained — no execution (ACE-074), no proposal (ACE-075), no UI (ACE-076);model_store.pyuntouched (persistence rode ACE-072's lossless fix)acme/account_keyfixtures onlybinding/combineare inert strings)