From 4fcfee047914da04b025e1f363eaa99d9971ba24 Mon Sep 17 00:00:00 2001 From: codex Date: Sun, 26 Jul 2026 13:34:56 +0200 Subject: [PATCH] Project rail composition and readiness --- WORK-RECORDS.md | 5 ++ docs/rail-rapp-reef-adaptation.md | 11 +++ railiance_fabric/canon.py | 1 + railiance_fabric/registry.py | 60 ++++++++++++++ registry/railiance-repos.yaml | 21 +++++ tests/test_registry.py | 63 ++++++++++++++- ...027-versioned-rail-readiness-projection.md | 81 +++++++++++++++++++ 7 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 761d61a..e812275 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -10,6 +10,7 @@ | --- | --- | --- | --- | --- | | workplan | RAIL-FAB-WP-0025 | finished | — | workplans/RAIL-FAB-WP-0025-rail-rapp-reef-fabric-adaptation.md | | workplan | RAIL-FAB-WP-0026 | finished | — | workplans/RAIL-FAB-WP-0026-repo-family-relation-projection.md | +| workplan | RAIL-FAB-WP-0027 | finished | — | workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md | | task | RAIL-FAB-WP-0025-T01 | done | — | workplans/RAIL-FAB-WP-0025-rail-rapp-reef-fabric-adaptation.md | | task | RAIL-FAB-WP-0025-T02 | done | — | workplans/RAIL-FAB-WP-0025-rail-rapp-reef-fabric-adaptation.md | | task | RAIL-FAB-WP-0025-T03 | done | — | workplans/RAIL-FAB-WP-0025-rail-rapp-reef-fabric-adaptation.md | @@ -17,3 +18,7 @@ | task | RAIL-FAB-WP-0026-T01 | done | — | workplans/RAIL-FAB-WP-0026-repo-family-relation-projection.md | | task | RAIL-FAB-WP-0026-T02 | done | — | workplans/RAIL-FAB-WP-0026-repo-family-relation-projection.md | | task | RAIL-FAB-WP-0026-T03 | done | — | workplans/RAIL-FAB-WP-0026-repo-family-relation-projection.md | +| task | RAIL-FAB-WP-0027-T01 | done | — | workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md | +| task | RAIL-FAB-WP-0027-T02 | done | — | workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md | +| task | RAIL-FAB-WP-0027-T03 | done | — | workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md | +| task | RAIL-FAB-WP-0027-T04 | done | — | workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md | diff --git a/docs/rail-rapp-reef-adaptation.md b/docs/rail-rapp-reef-adaptation.md index a170d8f..1767738 100644 --- a/docs/rail-rapp-reef-adaptation.md +++ b/docs/rail-rapp-reef-adaptation.md @@ -83,6 +83,16 @@ The registry now also retains the local checkout `path` used during onboarding. That lets Fabric reopen the conventional repo-local `rail`, `rapp`, and `reef` files when it needs to project first-wave relation edges. +Second-wave projection also retains: + +- rail contract version and base/derived composition kind +- a derived rail's `base_rail` and compatible base contract +- rail and package readiness state +- binding readiness, contract version, and evidence references + +Topology remains distinct from readiness. A relation can be declared without +being installed, verified, or production-approved. + ## First New Capability Type Fabric now reserves: @@ -111,6 +121,7 @@ repo-local files: declaration - `binds_rapp` from `bindings/rapps.yaml` with a fallback to the reef declaration +- `base_rail` from a derived rail declaration This keeps the relation path anchored in reviewed repo-local files rather than guessing solely from registry metadata. diff --git a/railiance_fabric/canon.py b/railiance_fabric/canon.py index df6fe3e..251f1ac 100644 --- a/railiance_fabric/canon.py +++ b/railiance_fabric/canon.py @@ -114,6 +114,7 @@ EDGE_TYPE_CANON_MAP: dict[str, CanonEdgeMapping] = { "available_via": CanonEdgeMapping("exposes", "model/network", "partial"), "attributed_to_cost_center": CanonEdgeMapping("governed_by", "model/governance", "gap"), "attributed_to_profit_center": CanonEdgeMapping("governed_by", "model/governance", "gap"), + "base_rail": CanonEdgeMapping("depends_on", "model/landscape", "partial"), "binds_rapp": CanonEdgeMapping("deploys", "model/devsecops", "partial"), "binds": CanonEdgeMapping("depends_on", "model/landscape", "partial"), "builds_container": CanonEdgeMapping("built_from", "model/devsecops", "partial"), diff --git a/railiance_fabric/registry.py b/railiance_fabric/registry.py index 38123c8..c69d119 100644 --- a/railiance_fabric/registry.py +++ b/railiance_fabric/registry.py @@ -2045,6 +2045,14 @@ def _project_repository_family_graph(repositories: list[dict[str, Any]]) -> tupl if edge is not None: edges[_edge_key(edge)] = edge + base_rail = projection.get("base_rail") + if isinstance(base_rail, str) and base_rail: + target_id = _repository_graph_node_id(base_rail) + nodes.setdefault(target_id, _placeholder_repository_node(base_rail)) + edge = _repository_projection_edge(slug, base_rail, "base_rail", projection) + if edge is not None: + edges[_edge_key(edge)] = edge + for rail_slug in _string_list(projection.get("hosted_rails")): target_id = _repository_graph_node_id(rail_slug) nodes.setdefault(target_id, _placeholder_repository_node(rail_slug)) @@ -2075,6 +2083,11 @@ def _repository_family_projection(repository: dict[str, Any]) -> dict[str, Any]: return {} return { "ownership_repo": _text_value(declaration, "ownership_repo"), + "contract_version": _text_value(declaration, "contract_version"), + "composition_kind": _text_value(declaration, "composition_kind"), + "base_rail": _text_value(declaration, "base_rail"), + "base_rail_contract": _text_value(declaration, "base_rail_contract"), + "readiness_state": _text_value(declaration, "readiness_state"), "source_links": [{"label": "Rail declaration", "path": str(declaration_path)}], } @@ -2091,6 +2104,7 @@ def _repository_family_projection(repository: dict[str, Any]) -> dict[str, Any]: return { "ownership_repo": _text_value(declaration, "ownership_repo"), "supported_rails": supported_rails, + "readiness_state": _text_value(declaration, "readiness_state"), "source_links": [{"label": "Rapp declaration", "path": str(declaration_path)}], } @@ -2131,6 +2145,16 @@ def _repository_family_projection(repository: dict[str, Any]) -> dict[str, Any]: "ownership_repo": _text_value(declaration, "ownership_repo"), "hosted_rails": hosted_rails, "bound_rapps": bound_rapps, + "relation_metadata": { + "hosts_rail": _relation_metadata( + rails_binding.get("hosted_rails") if isinstance(rails_binding, dict) else None, + key="rail_id", + ), + "binds_rapp": _relation_metadata( + rapps_binding.get("bound_rapps") if isinstance(rapps_binding, dict) else None, + key="rapp_id", + ), + }, "source_links": source_links, } @@ -2161,6 +2185,11 @@ def _repository_projection_node(repository: dict[str, Any], projection: dict[str "substrate_kind": str(repository.get("substrate_kind") or ""), "path": str(repository.get("path") or ""), "declaration_paths": _string_list(repository.get("declaration_paths")), + "contract_version": projection.get("contract_version"), + "composition_kind": projection.get("composition_kind"), + "base_rail": projection.get("base_rail"), + "base_rail_contract": projection.get("base_rail_contract"), + "readiness_state": projection.get("readiness_state"), } if source_links: attributes["source_path"] = str(source_links[0].get("path") or "") @@ -2211,6 +2240,13 @@ def _repository_projection_edge( if source_links: attributes["source_path"] = str(source_links[0].get("path") or "") attributes["source_links"] = source_links + relation_metadata = projection.get("relation_metadata") + if isinstance(relation_metadata, dict): + by_type = relation_metadata.get(edge_type) + if isinstance(by_type, dict): + metadata = by_type.get(target_slug) + if isinstance(metadata, dict): + attributes.update(metadata) return { "from": _repository_graph_node_id(source_slug), "to": _repository_graph_node_id(target_slug), @@ -2251,6 +2287,30 @@ def _string_list(value: object) -> list[str]: return result +def _relation_metadata(value: object, *, key: str) -> dict[str, dict[str, Any]]: + if not isinstance(value, list): + return {} + result: dict[str, dict[str, Any]] = {} + for item in value: + if not isinstance(item, dict): + continue + relation_id = _text_value(item, key) + if not relation_id: + continue + metadata: dict[str, Any] = {} + readiness_state = _text_value(item, "readiness_state") + if readiness_state: + metadata["readiness_state"] = readiness_state + contract_version = _text_value(item, "contract_version") + if contract_version: + metadata["contract_version"] = contract_version + evidence = _string_list(item.get("evidence")) + if evidence: + metadata["readiness_evidence"] = evidence + result[relation_id] = metadata + return result + + def _relation_id_list(value: object, *, key: str) -> list[str]: if isinstance(value, list): result: list[str] = [] diff --git a/registry/railiance-repos.yaml b/registry/railiance-repos.yaml index e6fe37b..c68a993 100644 --- a/registry/railiance-repos.yaml +++ b/registry/railiance-repos.yaml @@ -73,6 +73,15 @@ repositories: repo_family: rail ownership_repo: railiance-cluster + - slug: rail-knative + name: rail-knative + path: /home/worsch/rail-knative + remote_url: forgejo-remote:coulomb/rail-knative.git + default_branch: main + state_hub_repo_id: f2606003-e4a1-4285-8341-fc2f0d16f7b6 + repo_family: rail + ownership_repo: railiance-cluster + - slug: rapp-openbao name: rapp-openbao path: /home/worsch/rapp-openbao @@ -85,6 +94,18 @@ repositories: supported_rails: - rail-kubernetes + - slug: rapp-qonto + name: rapp-qonto + path: /home/worsch/rapp-qonto + remote_url: forgejo-remote:coulomb/rapp-qonto.git + default_branch: main + state_hub_repo_id: 8295283a-b9e7-48e5-a440-522bee955739 + repo_family: rapp + ownership_repo: qonto-assistant + primary_rail: rail-knative + supported_rails: + - rail-knative + - slug: reef-railiance name: reef-railiance path: /home/worsch/reef-railiance diff --git a/tests/test_registry.py b/tests/test_registry.py index d50f7a7..a6659a7 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -117,6 +117,9 @@ kind: execution-rail rail_id: rail-kubernetes repo: rail-kubernetes ownership_repo: railiance-cluster +contract_version: 1.0.0 +composition_kind: base +readiness_state: verified """.lstrip(), encoding="utf-8", ) @@ -153,6 +156,10 @@ reef_id: reef-railiance hosted_rails: - rail_id: rail-kubernetes role: primary + readiness_state: declared + contract_version: 1.0.0 + evidence: + - evidence/admission/rail-kubernetes-baseline.json """.lstrip(), encoding="utf-8", ) @@ -211,6 +218,9 @@ bound_rapps: assert reef_record["path"] == str(reef_repo.resolve()) assert reef_record["declaration_paths"] == [str((reef_repo / "declarations" / "reef.yaml").resolve())] assert nodes["repo:reef-railiance"]["attributes"]["source_path"].endswith("declarations/reef.yaml") + assert nodes["repo:rail-kubernetes"]["attributes"]["contract_version"] == "1.0.0" + assert nodes["repo:rail-kubernetes"]["attributes"]["composition_kind"] == "base" + assert nodes["repo:rail-kubernetes"]["attributes"]["readiness_state"] == "verified" assert ( edges[("repo:rail-kubernetes", "governed_by", "repo:railiance-cluster")]["canonical_type"] == "governed_by" @@ -219,10 +229,13 @@ bound_rapps: edges[("repo:rapp-openbao", "supports_rail", "repo:rail-kubernetes")]["canonical_type"] == "depends_on" ) - assert ( - edges[("repo:reef-railiance", "hosts_rail", "repo:rail-kubernetes")]["canonical_type"] - == "deploys" - ) + reef_rail = edges[("repo:reef-railiance", "hosts_rail", "repo:rail-kubernetes")] + assert reef_rail["canonical_type"] == "deploys" + assert reef_rail["attributes"]["readiness_state"] == "declared" + assert reef_rail["attributes"]["contract_version"] == "1.0.0" + assert reef_rail["attributes"]["readiness_evidence"] == [ + "evidence/admission/rail-kubernetes-baseline.json" + ] reef_binding = edges[("repo:reef-railiance", "binds_rapp", "repo:rapp-openbao")] assert reef_binding["canonical_type"] == "deploys" assert any( @@ -231,6 +244,48 @@ bound_rapps: ) +def test_registry_projects_derived_rail_base_dependency(tmp_path: Path) -> None: + store = RegistryStore(tmp_path / "registry.sqlite3") + store.init_schema() + base_repo = tmp_path / "rail-kubernetes" + derived_repo = tmp_path / "rail-knative" + (base_repo / "declarations").mkdir(parents=True) + (derived_repo / "declarations").mkdir(parents=True) + (base_repo / "declarations" / "rail.yaml").write_text( + "rail_id: rail-kubernetes\nownership_repo: railiance-cluster\n", + encoding="utf-8", + ) + (derived_repo / "declarations" / "rail.yaml").write_text( + """ +rail_id: rail-knative +ownership_repo: railiance-cluster +contract_version: 1.0.0 +composition_kind: derived +base_rail: rail-kubernetes +base_rail_contract: 1.0.0 +readiness_state: declared +""".lstrip(), + encoding="utf-8", + ) + for slug, path in (("rail-kubernetes", base_repo), ("rail-knative", derived_repo)): + store.upsert_repository( + { + "slug": slug, + "name": slug, + "path": str(path), + "repo_family": "rail", + "declaration_paths": [str(path / "declarations" / "rail.yaml")], + } + ) + combined = store.combined_graph() + nodes = {node["id"]: node for node in combined["nodes"] if node["kind"] == "Repository"} + edges = {(edge["from"], edge["type"], edge["to"]): edge for edge in combined["edges"]} + assert nodes["repo:rail-knative"]["attributes"]["base_rail"] == "rail-kubernetes" + assert nodes["repo:rail-knative"]["attributes"]["base_rail_contract"] == "1.0.0" + dependency = edges[("repo:rail-knative", "base_rail", "repo:rail-kubernetes")] + assert dependency["canonical_type"] == "depends_on" + + def test_registry_http_service_serves_queries(tmp_path: Path) -> None: store = RegistryStore(tmp_path / "registry.sqlite3") store.init_schema() diff --git a/workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md b/workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md new file mode 100644 index 0000000..a778b22 --- /dev/null +++ b/workplans/RAIL-FAB-WP-0027-versioned-rail-readiness-projection.md @@ -0,0 +1,81 @@ +--- +id: RAIL-FAB-WP-0027 +type: workplan +title: "Project versioned rail composition and readiness" +domain: financials +repo: railiance-fabric +status: finished +owner: codex +topic_slug: railiance +created: "2026-07-26" +updated: "2026-07-26" +state_hub_workstream_id: "852b7d44-3125-4d96-9d8c-7247382c4b60" +--- + +# RAIL-FAB-WP-0027 - Versioned rail readiness projection + +## Goal + +Ensure Fabric topology does not imply operational readiness and make derived +rail compatibility queryable. + +## T01 - Project rail contract metadata + +```task +id: RAIL-FAB-WP-0027-T01 +status: done +priority: high +state_hub_task_id: "9bfc85a4-a1ba-4156-9eef-436d042c60d9" +``` + +Project contract version, composition kind, base rail, and readiness state from +repo-local declarations. + +2026-07-26: Repository nodes now retain the versioned composition fields and +derived rails project a `base_rail` dependency. + +## T02 - Preserve binding readiness + +```task +id: RAIL-FAB-WP-0027-T02 +status: done +priority: high +state_hub_task_id: "150cbce6-839b-44cc-b9e5-c54daa4cf3ee" +``` + +Carry declared/installed/verified/production-approved/deprecated state and +evidence references on `hosts_rail` and `binds_rapp` relations. + +2026-07-26: Reef binding metadata now projects readiness state, contract +version, and evidence references on relation edges. + +## T03 - Add compatibility and false-readiness tests + +```task +id: RAIL-FAB-WP-0027-T03 +status: done +priority: high +state_hub_task_id: "bfa5ec7e-8262-49c9-bb54-d23ddc20153e" +``` + +Prove that a declared topology edge is not interpreted as production approval +and that derived rails retain their base-contract dependency. + +2026-07-26: Added focused registry tests for declared-only reef bindings and +derived-rail base dependencies. + +## T04 - Automate drift reconciliation + +```task +id: RAIL-FAB-WP-0027-T04 +status: done +priority: medium +state_hub_task_id: "a34e5d79-43aa-4414-ad6f-7fda02295c79" +``` + +Make declaration/evidence reconciliation idempotent and suitable for CI or a +scheduled registry sync. + +2026-07-26: The existing combined-graph reconciliation now deterministically +reopens repo-local declarations and binding files and replaces projected +metadata on each sync. Focused tests pass (`5 passed`).