From cf40c7bb4e1fe4639488d98ec9684a8066d63f3f Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 31 Aug 2026 23:00:39 +0200 Subject: [PATCH] fix Fabric authority import compatibility Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c --- api/models/fabric_graph.py | 2 +- api/schemas/fabric_graph.py | 19 +++++++++- api/services/fabric_graph.py | 12 ++++++- ...e6f7a8c9d0_fabric_snapshot_set_revision.py | 35 +++++++++++++++++++ tests/test_routers_core.py | 23 +++++++++++- 5 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/b5e6f7a8c9d0_fabric_snapshot_set_revision.py diff --git a/api/models/fabric_graph.py b/api/models/fabric_graph.py index 850616c..746447b 100644 --- a/api/models/fabric_graph.py +++ b/api/models/fabric_graph.py @@ -23,7 +23,7 @@ class FabricGraphImport(Base, TimestampMixin): ) source_repo_slug: Mapped[str] = mapped_column(String(100), nullable=False, index=True) source_url: Mapped[str | None] = mapped_column(Text, nullable=True) - source_commit: Mapped[str | None] = mapped_column(String(80), nullable=True, index=True) + source_commit: Mapped[str | None] = mapped_column(String(255), nullable=True, index=True) source_path: Mapped[str | None] = mapped_column(Text, nullable=True) api_version: Mapped[str | None] = mapped_column(String(100), nullable=True) schema_version: Mapped[str | None] = mapped_column(String(100), nullable=True, index=True) diff --git a/api/schemas/fabric_graph.py b/api/schemas/fabric_graph.py index b1ea79e..3c9e98e 100644 --- a/api/schemas/fabric_graph.py +++ b/api/schemas/fabric_graph.py @@ -27,7 +27,10 @@ class FabricGraphSource(BaseModel): repo: str | None = None producer: str | None = None registry: str | None = None - commit: str | None = None + # Accepted-snapshot-set revisions include the algorithm and digest, for + # example ``snapshot-set:sha256:<64 hex chars>``. They are intentionally + # longer than a Git SHA but still bounded before persistence. + commit: str | None = Field(default=None, max_length=255) path: str | None = None generation_reason: str | None = None @@ -98,6 +101,18 @@ class FabricGraphAccountingPayload(BaseModel): valid_until: str | None = None +class FabricGraphDeploymentOverlayPayload(BaseModel): + model_config = ConfigDict(extra="forbid") + + deployment_environment: str | None = None + deployment_scenario: str | None = None + routing_authority: str | None = None + access_zone: str | None = None + policy_authority: str | None = None + exposure_class: str | None = None + route_evidence: dict[str, str | int | float | bool | None] = Field(default_factory=dict) + + class FabricGraphEvidencePayload(BaseModel): model_config = ConfigDict(extra="forbid") @@ -154,6 +169,7 @@ class FabricGraphNodePayload(BaseModel): containment: FabricGraphContainmentPayload | None = None ownership: FabricGraphOwnershipPayload | None = None accounting: FabricGraphAccountingPayload | None = None + deployment_overlay: FabricGraphDeploymentOverlayPayload | None = None evidence: FabricGraphEvidencePayload | None = None canon_category: str | None = None canon_anchor: str | None = None @@ -180,6 +196,7 @@ class FabricGraphEdgePayload(BaseModel): boundary: FabricGraphBoundaryPayload | None = None utility: FabricGraphUtilityPayload | None = None accounting: FabricGraphAccountingPayload | None = None + deployment_overlay: FabricGraphDeploymentOverlayPayload | None = None evidence: FabricGraphEvidencePayload | None = None attributes: dict[str, Any] = Field(default_factory=dict) diff --git a/api/services/fabric_graph.py b/api/services/fabric_graph.py index d8a5f56..8a98bf3 100644 --- a/api/services/fabric_graph.py +++ b/api/services/fabric_graph.py @@ -324,7 +324,10 @@ async def _record_invalid_import( import_run = FabricGraphImport( source_repo_slug=source_repo_slug, source_url=source_url, - source_commit=_source_value(payload, "commit"), + # Invalid payloads are retained for diagnostics, but their + # denormalized provenance must not make the rejection path fail. + # The complete input remains available in ``graph_json``. + source_commit=_bounded_source_value(payload, "commit", max_length=255), source_path=_source_value(payload, "path"), api_version=str(payload.get("apiVersion")) if payload.get("apiVersion") else None, export_kind=str(payload.get("kind")) if payload.get("kind") else None, @@ -620,6 +623,13 @@ def _source_value(payload: dict[str, Any], field: str) -> str | None: return str(value) if value else None +def _bounded_source_value( + payload: dict[str, Any], field: str, *, max_length: int +) -> str | None: + value = _source_value(payload, field) + return value[:max_length] if value is not None else None + + def _parse_datetime(value: Any) -> datetime | None: if not isinstance(value, str) or not value: return None diff --git a/migrations/versions/b5e6f7a8c9d0_fabric_snapshot_set_revision.py b/migrations/versions/b5e6f7a8c9d0_fabric_snapshot_set_revision.py new file mode 100644 index 0000000..d6c1235 --- /dev/null +++ b/migrations/versions/b5e6f7a8c9d0_fabric_snapshot_set_revision.py @@ -0,0 +1,35 @@ +"""accept deterministic Fabric snapshot-set revisions + +Revision ID: b5e6f7a8c9d0 +Revises: a4d5e6f7b8c9 +""" + +from alembic import op +import sqlalchemy as sa + + +revision = "b5e6f7a8c9d0" +down_revision = "a4d5e6f7b8c9" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.alter_column( + "fabric_graph_imports", + "source_commit", + existing_type=sa.String(length=80), + type_=sa.String(length=255), + existing_nullable=True, + ) + + +def downgrade() -> None: + op.alter_column( + "fabric_graph_imports", + "source_commit", + existing_type=sa.String(length=255), + type_=sa.String(length=80), + existing_nullable=True, + postgresql_using="left(source_commit, 80)", + ) diff --git a/tests/test_routers_core.py b/tests/test_routers_core.py index da96bd3..49ed399 100644 --- a/tests/test_routers_core.py +++ b/tests/test_routers_core.py @@ -1291,7 +1291,8 @@ def _financial_fabric_graph_export(generated_at="2026-05-24T00:00:00Z"): "source": { "producer": "railiance-fabric", "registry": "registry", - "commit": "financial-example", + "commit": "snapshot-set:sha256:" + ("a" * 64), + "path": "registry://accepted-snapshots", "generation_reason": "operator_refresh", }, "compatibility": { @@ -1373,6 +1374,11 @@ def _financial_fabric_graph_export(generated_at="2026-05-24T00:00:00Z"): "cost_center_id": "cc.platform.shared", "allocation_model": "direct", }, + "deployment_overlay": { + "deployment_environment": "local", + "routing_authority": "ops-hub", + "route_evidence": {"observed": True, "port": 8000}, + }, "evidence": { "state": "declared", "review_state": "accepted", @@ -1581,6 +1587,8 @@ class TestFabricGraphReadModel: body = r.json() assert body["import_run"]["api_version"] == "railiance.fabric/v1alpha2" assert body["import_run"]["schema_version"] == "financial-fabric-v1" + assert body["import_run"]["source_commit"] == "snapshot-set:sha256:" + ("a" * 64) + assert body["import_run"]["source_path"] == "registry://accepted-snapshots" assert body["import_run"]["netkingdom_id"] == "railiance.netkingdom" assert body["import_run"]["actor_count"] == 3 assert body["import_run"]["fabric_count"] == 2 @@ -1646,6 +1654,19 @@ class TestFabricGraphReadModel: r = await client.get("/fabric/graph/nodes") assert r.status_code == 404 + async def test_invalid_export_with_oversized_revision_is_recorded_not_500(self, client): + payload = _financial_fabric_graph_export() + payload["source"]["commit"] = "x" * 300 + + r = await client.post("/fabric/graph-exports", json=payload) + + assert r.status_code == 422, r.text + import_id = r.json()["detail"]["import_id"] + r = await client.get("/fabric/graph-exports?validation_status=invalid") + invalid = next(item for item in r.json() if item["id"] == import_id) + assert invalid["source_commit"] == "x" * 255 + assert "at most 255 characters" in invalid["error_details"]["error"] + async def test_legacy_fabric_exports_remain_compatible_with_null_financial_fields(self, client): r = await client.post("/fabric/graph-exports", json=_fabric_graph_export()) assert r.status_code == 200, r.text