|
|
@@ -166,9 +166,10 @@ class DataRuleRepository:
|
|
|
),
|
|
|
}
|
|
|
|
|
|
- def consume_dataflow_draft(
|
|
|
- self, receipt: dict[str, Any], *, actor_uid: str
|
|
|
- ) -> str:
|
|
|
+ @staticmethod
|
|
|
+ def _draft_receipt(
|
|
|
+ receipt: dict[str, Any], *, actor_uid: str
|
|
|
+ ) -> dict[str, str]:
|
|
|
if not isinstance(receipt, dict) or set(receipt) != {
|
|
|
"reservation_id",
|
|
|
"dataflow_uid",
|
|
|
@@ -182,31 +183,205 @@ class DataRuleRepository:
|
|
|
actor = _uid(actor_uid, "actor_uid")
|
|
|
nonce = _text(receipt.get("nonce"), "draft nonce", 200)
|
|
|
nonce_hash = hashlib.sha256(nonce.encode("utf-8")).hexdigest()
|
|
|
- consumed = self.session.execute(
|
|
|
+ return {
|
|
|
+ "id": reservation_id,
|
|
|
+ "dataflow_uid": dataflow_uid,
|
|
|
+ "actor_uid": actor,
|
|
|
+ "nonce_hash": nonce_hash,
|
|
|
+ }
|
|
|
+
|
|
|
+ def begin_dataflow_create(
|
|
|
+ self,
|
|
|
+ receipt: dict[str, Any],
|
|
|
+ *,
|
|
|
+ actor_uid: str,
|
|
|
+ lease_seconds: int = 60,
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ """Atomically claim a create attempt or replay its committed result."""
|
|
|
+ if (
|
|
|
+ isinstance(lease_seconds, bool)
|
|
|
+ or not isinstance(lease_seconds, int)
|
|
|
+ or lease_seconds < 10
|
|
|
+ or lease_seconds > 300
|
|
|
+ ):
|
|
|
+ raise ValueError("dataflow create lease is invalid")
|
|
|
+ values = self._draft_receipt(receipt, actor_uid=actor_uid)
|
|
|
+ lease_token = new_governance_uid()
|
|
|
+ claimed = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ "UPDATE public.dataflow_draft_reservations "
|
|
|
+ "SET state = 'creating', lease_token = CAST(:lease_token AS uuid), "
|
|
|
+ "lease_expires_at = CURRENT_TIMESTAMP + "
|
|
|
+ "(:lease_seconds * INTERVAL '1 second'), "
|
|
|
+ "attempt_count = attempt_count + 1, "
|
|
|
+ "create_started_at = COALESCE("
|
|
|
+ "create_started_at, CURRENT_TIMESTAMP), "
|
|
|
+ "error_code = NULL, updated_at = CURRENT_TIMESTAMP "
|
|
|
+ "WHERE id = CAST(:id AS uuid) "
|
|
|
+ "AND dataflow_uid = CAST(:dataflow_uid AS uuid) "
|
|
|
+ "AND actor_uid = CAST(:actor_uid AS uuid) "
|
|
|
+ "AND nonce_hash = :nonce_hash "
|
|
|
+ "AND consumed_at IS NULL "
|
|
|
+ "AND (expires_at > CURRENT_TIMESTAMP OR attempt_count > 0) "
|
|
|
+ "AND (state IN ('reserved', 'failed') OR "
|
|
|
+ "(state = 'creating' AND lease_expires_at <= CURRENT_TIMESTAMP)) "
|
|
|
+ "RETURNING dataflow_uid::text, attempt_count "
|
|
|
+ "/* begin_dataflow_create */"
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ **values,
|
|
|
+ "lease_token": lease_token,
|
|
|
+ "lease_seconds": lease_seconds,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ if claimed is not None:
|
|
|
+ return {
|
|
|
+ "status": "claimed",
|
|
|
+ "dataflow_uid": str(claimed["dataflow_uid"]),
|
|
|
+ "lease_token": lease_token,
|
|
|
+ "attempt": int(claimed["attempt_count"]),
|
|
|
+ }
|
|
|
+
|
|
|
+ row = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ "SELECT state, dataflow_uid::text, result, "
|
|
|
+ "result_digest, lease_expires_at, expires_at "
|
|
|
+ "FROM public.dataflow_draft_reservations "
|
|
|
+ "WHERE id = CAST(:id AS uuid) "
|
|
|
+ "AND dataflow_uid = CAST(:dataflow_uid AS uuid) "
|
|
|
+ "AND actor_uid = CAST(:actor_uid AS uuid) "
|
|
|
+ "AND nonce_hash = :nonce_hash "
|
|
|
+ "/* inspect_dataflow_create */"
|
|
|
+ ),
|
|
|
+ values,
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ if row is not None and row["state"] == "completed":
|
|
|
+ result = _object(row["result"], "dataflow create result")
|
|
|
+ if _canonical_hash(result) != str(row["result_digest"]):
|
|
|
+ raise RuntimeError("dataflow create result integrity check failed")
|
|
|
+ return {
|
|
|
+ "status": "completed",
|
|
|
+ "dataflow_uid": str(row["dataflow_uid"]),
|
|
|
+ "result": copy.deepcopy(result),
|
|
|
+ }
|
|
|
+ if row is not None and row["state"] == "creating":
|
|
|
+ raise ValueError("dataflow_create_in_progress")
|
|
|
+ raise ValueError("draft_reservation_refresh_required")
|
|
|
+
|
|
|
+ def complete_dataflow_create(
|
|
|
+ self,
|
|
|
+ *,
|
|
|
+ reservation_id: str,
|
|
|
+ dataflow_uid: str,
|
|
|
+ lease_token: str,
|
|
|
+ dataflow_node_id: int,
|
|
|
+ result: dict[str, Any],
|
|
|
+ ) -> dict[str, Any]:
|
|
|
+ reservation = _uid(reservation_id, "reservation_id")
|
|
|
+ flow_uid = _uid(dataflow_uid, "dataflow_uid")
|
|
|
+ lease = _uid(lease_token, "lease_token")
|
|
|
+ if (
|
|
|
+ isinstance(dataflow_node_id, bool)
|
|
|
+ or not isinstance(dataflow_node_id, int)
|
|
|
+ or dataflow_node_id < 0
|
|
|
+ ):
|
|
|
+ raise ValueError("dataflow_node_id is invalid")
|
|
|
+ normalized_result = _object(result, "dataflow create result")
|
|
|
+ result_digest = _canonical_hash(normalized_result)
|
|
|
+ completed = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ "UPDATE public.dataflow_draft_reservations "
|
|
|
+ "SET state = 'completed', result = CAST(:result AS jsonb), "
|
|
|
+ "result_digest = :result_digest, "
|
|
|
+ "dataflow_node_id = :dataflow_node_id, "
|
|
|
+ "completed_at = CURRENT_TIMESTAMP, "
|
|
|
+ "consumed_at = CURRENT_TIMESTAMP, lease_token = NULL, "
|
|
|
+ "lease_expires_at = NULL, updated_at = CURRENT_TIMESTAMP "
|
|
|
+ "WHERE id = CAST(:id AS uuid) "
|
|
|
+ "AND dataflow_uid = CAST(:dataflow_uid AS uuid) "
|
|
|
+ "AND state = 'creating' "
|
|
|
+ "AND lease_token = CAST(:lease_token AS uuid) "
|
|
|
+ "RETURNING result /* complete_dataflow_create */"
|
|
|
+ ),
|
|
|
+ {
|
|
|
+ "id": reservation,
|
|
|
+ "dataflow_uid": flow_uid,
|
|
|
+ "lease_token": lease,
|
|
|
+ "result": _json(normalized_result),
|
|
|
+ "result_digest": result_digest,
|
|
|
+ "dataflow_node_id": dataflow_node_id,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ if completed is None:
|
|
|
+ replay = (
|
|
|
+ self.session.execute(
|
|
|
+ text(
|
|
|
+ "SELECT result, result_digest "
|
|
|
+ "FROM public.dataflow_draft_reservations "
|
|
|
+ "WHERE id = CAST(:id AS uuid) "
|
|
|
+ "AND dataflow_uid = CAST(:dataflow_uid AS uuid) "
|
|
|
+ "AND state = 'completed' "
|
|
|
+ "/* replay_completed_dataflow_create */"
|
|
|
+ ),
|
|
|
+ {"id": reservation, "dataflow_uid": flow_uid},
|
|
|
+ )
|
|
|
+ .mappings()
|
|
|
+ .one_or_none()
|
|
|
+ )
|
|
|
+ if replay is None:
|
|
|
+ raise RuntimeError("dataflow create lease was lost")
|
|
|
+ replay_result = _object(replay["result"], "dataflow create result")
|
|
|
+ if _canonical_hash(replay_result) != str(replay["result_digest"]):
|
|
|
+ raise RuntimeError("dataflow create result integrity check failed")
|
|
|
+ return copy.deepcopy(replay_result)
|
|
|
+ return copy.deepcopy(normalized_result)
|
|
|
+
|
|
|
+ def fail_dataflow_create(
|
|
|
+ self, *, reservation_id: str, lease_token: str, error_code: str
|
|
|
+ ) -> None:
|
|
|
+ self.session.execute(
|
|
|
text(
|
|
|
"UPDATE public.dataflow_draft_reservations "
|
|
|
- "SET consumed_at = CURRENT_TIMESTAMP "
|
|
|
+ "SET state = 'failed', error_code = :error_code, "
|
|
|
+ "failed_at = CURRENT_TIMESTAMP, lease_token = NULL, "
|
|
|
+ "lease_expires_at = NULL, updated_at = CURRENT_TIMESTAMP "
|
|
|
"WHERE id = CAST(:id AS uuid) "
|
|
|
- "AND dataflow_uid = CAST(:dataflow_uid AS uuid) "
|
|
|
- "AND actor_uid = CAST(:actor_uid AS uuid) "
|
|
|
- "AND nonce_hash = :nonce_hash "
|
|
|
- "AND consumed_at IS NULL "
|
|
|
- "AND expires_at > CURRENT_TIMESTAMP "
|
|
|
- "RETURNING dataflow_uid::text "
|
|
|
- "/* consume_dataflow_draft */"
|
|
|
+ "AND state = 'creating' "
|
|
|
+ "AND lease_token = CAST(:lease_token AS uuid) "
|
|
|
+ "/* fail_dataflow_create */"
|
|
|
),
|
|
|
{
|
|
|
- "id": reservation_id,
|
|
|
- "dataflow_uid": dataflow_uid,
|
|
|
- "actor_uid": actor,
|
|
|
- "nonce_hash": nonce_hash,
|
|
|
+ "id": _uid(reservation_id, "reservation_id"),
|
|
|
+ "lease_token": _uid(lease_token, "lease_token"),
|
|
|
+ "error_code": _text(error_code, "error_code", 100),
|
|
|
},
|
|
|
- ).scalar_one_or_none()
|
|
|
- if consumed is None or str(consumed) != dataflow_uid:
|
|
|
- raise ValueError(
|
|
|
- "dataflow draft receipt is invalid, expired, or already used"
|
|
|
- )
|
|
|
- return dataflow_uid
|
|
|
+ )
|
|
|
+
|
|
|
+ def commit_dataflow_create_claim(self) -> None:
|
|
|
+ """Durably publish the lease before the Neo4j side effect starts."""
|
|
|
+ self.session.commit()
|
|
|
+
|
|
|
+ def commit_dataflow_create_failure(
|
|
|
+ self, *, reservation_id: str, lease_token: str, error_code: str
|
|
|
+ ) -> None:
|
|
|
+ self.fail_dataflow_create(
|
|
|
+ reservation_id=reservation_id,
|
|
|
+ lease_token=lease_token,
|
|
|
+ error_code=error_code,
|
|
|
+ )
|
|
|
+ self.session.commit()
|
|
|
|
|
|
@staticmethod
|
|
|
def candidate_hash(candidate: dict[str, Any]) -> str:
|
|
|
@@ -1667,6 +1842,7 @@ class DataRuleRepository:
|
|
|
input_schema_refs: list[str] | None = None,
|
|
|
output_schema_ref: str | None = None,
|
|
|
version_id: str | None = None,
|
|
|
+ schema_resolver: Any | None = None,
|
|
|
) -> dict[str, Any]:
|
|
|
"""Return only immutable, trusted rule and standard versions."""
|
|
|
if not isinstance(query, str) or len(query) > 200:
|
|
|
@@ -1722,6 +1898,8 @@ class DataRuleRepository:
|
|
|
"version_id, rv.rule_uid::text AS asset_uid, r.name, "
|
|
|
"rv.version_no, r.owner_uid::text AS owner_uid, "
|
|
|
"rv.status, rv.rule_spec AS schema_compatibility, "
|
|
|
+ "lp.id::text AS logical_plan_id, lp.schema_hashes, "
|
|
|
+ "'[]'::jsonb AS bound_rules, 1::integer AS binding_count, "
|
|
|
"((SELECT COUNT(*) FROM "
|
|
|
"public.dataflow_component_bindings cb "
|
|
|
"WHERE cb.rule_version_id = rv.id) + "
|
|
|
@@ -1768,6 +1946,43 @@ class DataRuleRepository:
|
|
|
"sv.standard_uid::text AS asset_uid, s.name, "
|
|
|
"sv.version_no, s.owner_uid::text AS owner_uid, "
|
|
|
"sv.status, sv.scope AS schema_compatibility, "
|
|
|
+ "NULL::text AS logical_plan_id, NULL::jsonb AS schema_hashes, "
|
|
|
+ "COALESCE((SELECT jsonb_agg(jsonb_build_object("
|
|
|
+ "'rule_version_id', brv.id::text, "
|
|
|
+ "'rule_spec', brv.rule_spec, "
|
|
|
+ "'logical_plan_id', blp.id::text, "
|
|
|
+ "'schema_hashes', blp.schema_hashes, "
|
|
|
+ "'compile_evidence_id', bce.id::text, "
|
|
|
+ "'test_evidence_id', bte.id::text) ORDER BY srb.clause_id) "
|
|
|
+ "FROM public.standard_rule_bindings srb "
|
|
|
+ "JOIN public.data_rule_versions brv "
|
|
|
+ "ON brv.id = srb.rule_version_id "
|
|
|
+ "JOIN LATERAL (SELECT value.* FROM "
|
|
|
+ "public.rule_logical_plans value "
|
|
|
+ "WHERE value.rule_version_id = brv.id "
|
|
|
+ "AND value.status = 'published' "
|
|
|
+ "ORDER BY value.created_at DESC LIMIT 1) blp ON TRUE "
|
|
|
+ "JOIN LATERAL (SELECT value.id FROM "
|
|
|
+ "public.rule_logical_compile_evidence value "
|
|
|
+ "WHERE value.logical_plan_id = blp.id "
|
|
|
+ "AND value.status = 'success' "
|
|
|
+ "AND value.compiler_version = blp.compiler_version "
|
|
|
+ "AND value.plan_hash = blp.plan_hash "
|
|
|
+ "AND value.schema_hashes = blp.schema_hashes "
|
|
|
+ "AND value.capabilities = blp.capabilities "
|
|
|
+ "ORDER BY value.created_at DESC LIMIT 1) bce ON TRUE "
|
|
|
+ "JOIN LATERAL (SELECT value.id FROM "
|
|
|
+ "public.rule_logical_test_evidence value "
|
|
|
+ "WHERE value.logical_plan_id = blp.id "
|
|
|
+ "AND value.status = 'success' "
|
|
|
+ "AND value.plan_hash = blp.plan_hash "
|
|
|
+ "AND value.schema_hashes = blp.schema_hashes "
|
|
|
+ "ORDER BY value.created_at DESC LIMIT 1) bte ON TRUE "
|
|
|
+ "WHERE srb.standard_version_id = sv.id "
|
|
|
+ "AND brv.status = 'published'), '[]'::jsonb) AS bound_rules, "
|
|
|
+ "(SELECT COUNT(*) FROM public.standard_rule_bindings srb "
|
|
|
+ "WHERE srb.standard_version_id = sv.id)::integer "
|
|
|
+ "AS binding_count, "
|
|
|
"(SELECT COUNT(*) FROM "
|
|
|
"public.dataflow_component_bindings cb "
|
|
|
"WHERE cb.standard_version_id = sv.id)::integer "
|
|
|
@@ -1830,9 +2045,96 @@ class DataRuleRepository:
|
|
|
result["id"] = str(evidence_id)
|
|
|
return result
|
|
|
|
|
|
- def compatibility_summary(
|
|
|
- value: Any, row_asset_type: str
|
|
|
+ def resolve_ref(schema_ref: Any) -> dict[str, Any] | None:
|
|
|
+ if schema_resolver is None or not isinstance(schema_ref, str):
|
|
|
+ return None
|
|
|
+ try:
|
|
|
+ snapshot = schema_resolver.resolve(schema_ref)
|
|
|
+ except Exception:
|
|
|
+ return None
|
|
|
+ if not isinstance(snapshot, dict):
|
|
|
+ return None
|
|
|
+ schema_hash = snapshot.get("schema_hash")
|
|
|
+ if not isinstance(schema_hash, str) or not re.fullmatch(
|
|
|
+ r"[0-9a-f]{64}", schema_hash
|
|
|
+ ):
|
|
|
+ return None
|
|
|
+ return {
|
|
|
+ "snapshot_id": (
|
|
|
+ str(snapshot["id"]) if snapshot.get("id") is not None else None
|
|
|
+ ),
|
|
|
+ "schema_hash": schema_hash,
|
|
|
+ "source_revision": (
|
|
|
+ str(snapshot["source_revision"])
|
|
|
+ if snapshot.get("source_revision") is not None
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ }
|
|
|
+
|
|
|
+ def rule_compatibility(
|
|
|
+ binding: dict[str, Any],
|
|
|
+ published_hashes: Any,
|
|
|
+ logical_plan_id: Any,
|
|
|
+ compile_evidence_id: Any,
|
|
|
+ test_evidence_id: Any,
|
|
|
) -> dict[str, Any]:
|
|
|
+ asset_input = binding.get("input_schema_ref")
|
|
|
+ asset_output = binding.get("output_schema_ref")
|
|
|
+ hashes = (
|
|
|
+ _object(published_hashes, "schema hashes")
|
|
|
+ if published_hashes is not None
|
|
|
+ else {}
|
|
|
+ )
|
|
|
+ input_snapshot = resolve_ref(asset_input)
|
|
|
+ output_snapshot = resolve_ref(asset_output)
|
|
|
+ refs_match = (
|
|
|
+ asset_input in input_schema_refs
|
|
|
+ and asset_output == output_schema_ref
|
|
|
+ )
|
|
|
+ if input_snapshot is None or output_snapshot is None:
|
|
|
+ status, reason = "unknown", "schema_resolution_unavailable"
|
|
|
+ else:
|
|
|
+ hashes_match = (
|
|
|
+ input_snapshot["schema_hash"] == hashes.get("input")
|
|
|
+ and output_snapshot["schema_hash"] == hashes.get("output")
|
|
|
+ )
|
|
|
+ status = (
|
|
|
+ "compatible" if refs_match and hashes_match else "incompatible"
|
|
|
+ )
|
|
|
+ reason = (
|
|
|
+ "trusted_schema_hashes_match"
|
|
|
+ if status == "compatible"
|
|
|
+ else "schema_ref_or_hash_drift"
|
|
|
+ )
|
|
|
+ return {
|
|
|
+ "status": status,
|
|
|
+ "reason": reason,
|
|
|
+ "evidence": {
|
|
|
+ "logical_plan_id": (
|
|
|
+ str(logical_plan_id)
|
|
|
+ if logical_plan_id is not None
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ "compile_evidence_id": (
|
|
|
+ str(compile_evidence_id)
|
|
|
+ if compile_evidence_id is not None
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ "test_evidence_id": (
|
|
|
+ str(test_evidence_id)
|
|
|
+ if test_evidence_id is not None
|
|
|
+ else None
|
|
|
+ ),
|
|
|
+ "input": input_snapshot,
|
|
|
+ "output": output_snapshot,
|
|
|
+ "published_schema_hashes": {
|
|
|
+ "input": hashes.get("input"),
|
|
|
+ "output": hashes.get("output"),
|
|
|
+ },
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ def compatibility_summary(value: Any, row: Any) -> dict[str, Any]:
|
|
|
binding = _object(value, "schema compatibility")
|
|
|
if not has_context:
|
|
|
return {
|
|
|
@@ -1840,38 +2142,61 @@ class DataRuleRepository:
|
|
|
"reason": "production_line_context_required",
|
|
|
"evidence": None,
|
|
|
}
|
|
|
- if row_asset_type == "rule":
|
|
|
- asset_input = binding.get("input_schema_ref")
|
|
|
- asset_output = binding.get("output_schema_ref")
|
|
|
- compatible = (
|
|
|
- asset_input in input_schema_refs
|
|
|
- and asset_output == output_schema_ref
|
|
|
+ if str(row["asset_type"]) == "rule":
|
|
|
+ return rule_compatibility(
|
|
|
+ binding,
|
|
|
+ row.get("schema_hashes"),
|
|
|
+ row.get("logical_plan_id"),
|
|
|
+ row.get("compile_evidence_id"),
|
|
|
+ row.get("test_evidence_id"),
|
|
|
)
|
|
|
- evidence = {
|
|
|
- "asset_input_schema_ref": asset_input,
|
|
|
- "asset_output_schema_ref": asset_output,
|
|
|
- "flow_input_schema_refs": input_schema_refs,
|
|
|
- "flow_output_schema_ref": output_schema_ref,
|
|
|
- }
|
|
|
- else:
|
|
|
- scope = binding.get("schema_ref")
|
|
|
- compatible = scope in {
|
|
|
- *input_schema_refs,
|
|
|
- output_schema_ref,
|
|
|
- }
|
|
|
- evidence = {
|
|
|
- "asset_scope_schema_ref": scope,
|
|
|
- "flow_input_schema_refs": input_schema_refs,
|
|
|
- "flow_output_schema_ref": output_schema_ref,
|
|
|
+ bound_rules = _array(
|
|
|
+ row.get("bound_rules", []), "standard bound rules"
|
|
|
+ )
|
|
|
+ if not bound_rules or len(bound_rules) != int(
|
|
|
+ row.get("binding_count", 0)
|
|
|
+ ):
|
|
|
+ return {
|
|
|
+ "status": "unknown",
|
|
|
+ "reason": "standard_trusted_rule_evidence_incomplete",
|
|
|
+ "evidence": {"bound_rules": []},
|
|
|
}
|
|
|
+ summaries = [
|
|
|
+ rule_compatibility(
|
|
|
+ _object(item.get("rule_spec"), "bound rule spec"),
|
|
|
+ item.get("schema_hashes"),
|
|
|
+ item.get("logical_plan_id"),
|
|
|
+ item.get("compile_evidence_id"),
|
|
|
+ item.get("test_evidence_id"),
|
|
|
+ )
|
|
|
+ for item in bound_rules
|
|
|
+ ]
|
|
|
+ statuses = {summary["status"] for summary in summaries}
|
|
|
+ status = (
|
|
|
+ "unknown"
|
|
|
+ if "unknown" in statuses
|
|
|
+ else "incompatible"
|
|
|
+ if "incompatible" in statuses
|
|
|
+ else "compatible"
|
|
|
+ )
|
|
|
return {
|
|
|
- "status": "compatible" if compatible else "incompatible",
|
|
|
+ "status": status,
|
|
|
"reason": (
|
|
|
- "exact_schema_refs_match"
|
|
|
- if compatible
|
|
|
- else "exact_schema_refs_do_not_match"
|
|
|
+ "all_standard_rule_hashes_match"
|
|
|
+ if status == "compatible"
|
|
|
+ else "standard_bound_rule_schema_drift"
|
|
|
),
|
|
|
- "evidence": evidence,
|
|
|
+ "evidence": {
|
|
|
+ "bound_rules": [
|
|
|
+ {
|
|
|
+ "rule_version_id": str(item["rule_version_id"]),
|
|
|
+ **summary["evidence"],
|
|
|
+ }
|
|
|
+ for item, summary in zip(
|
|
|
+ bound_rules, summaries, strict=True
|
|
|
+ )
|
|
|
+ ]
|
|
|
+ },
|
|
|
}
|
|
|
|
|
|
items = []
|
|
|
@@ -1893,7 +2218,7 @@ class DataRuleRepository:
|
|
|
"status": str(row["status"]),
|
|
|
"schema_compatibility": compatibility_summary(
|
|
|
row["schema_compatibility"],
|
|
|
- str(row["asset_type"]),
|
|
|
+ row,
|
|
|
),
|
|
|
"impact_count": int(row["impact_count"]),
|
|
|
"backend": str(row["backend"]),
|
|
|
@@ -1918,12 +2243,14 @@ class DataRuleRepository:
|
|
|
version_id: str,
|
|
|
input_schema_refs: list[str] | None = None,
|
|
|
output_schema_ref: str | None = None,
|
|
|
+ schema_resolver: Any | None = None,
|
|
|
) -> dict[str, Any]:
|
|
|
page = self.search_published_assets(
|
|
|
asset_type=asset_type,
|
|
|
version_id=version_id,
|
|
|
input_schema_refs=input_schema_refs,
|
|
|
output_schema_ref=output_schema_ref,
|
|
|
+ schema_resolver=schema_resolver,
|
|
|
limit=1,
|
|
|
offset=0,
|
|
|
)
|