Project rail composition and readiness
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 4s

This commit is contained in:
codex 2026-07-26 13:34:56 +02:00
parent 9af9ee3e72
commit 4fcfee0479
7 changed files with 238 additions and 4 deletions

View file

@ -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-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-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-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-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 | | 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-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-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-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 |

View file

@ -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` That lets Fabric reopen the conventional repo-local `rail`, `rapp`, and `reef`
files when it needs to project first-wave relation edges. 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 ## First New Capability Type
Fabric now reserves: Fabric now reserves:
@ -111,6 +121,7 @@ repo-local files:
declaration declaration
- `binds_rapp` from `bindings/rapps.yaml` with a fallback to the reef - `binds_rapp` from `bindings/rapps.yaml` with a fallback to the reef
declaration declaration
- `base_rail` from a derived rail declaration
This keeps the relation path anchored in reviewed repo-local files rather than This keeps the relation path anchored in reviewed repo-local files rather than
guessing solely from registry metadata. guessing solely from registry metadata.

View file

@ -114,6 +114,7 @@ EDGE_TYPE_CANON_MAP: dict[str, CanonEdgeMapping] = {
"available_via": CanonEdgeMapping("exposes", "model/network", "partial"), "available_via": CanonEdgeMapping("exposes", "model/network", "partial"),
"attributed_to_cost_center": CanonEdgeMapping("governed_by", "model/governance", "gap"), "attributed_to_cost_center": CanonEdgeMapping("governed_by", "model/governance", "gap"),
"attributed_to_profit_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_rapp": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"binds": CanonEdgeMapping("depends_on", "model/landscape", "partial"), "binds": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"builds_container": CanonEdgeMapping("built_from", "model/devsecops", "partial"), "builds_container": CanonEdgeMapping("built_from", "model/devsecops", "partial"),

View file

@ -2045,6 +2045,14 @@ def _project_repository_family_graph(repositories: list[dict[str, Any]]) -> tupl
if edge is not None: if edge is not None:
edges[_edge_key(edge)] = edge 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")): for rail_slug in _string_list(projection.get("hosted_rails")):
target_id = _repository_graph_node_id(rail_slug) target_id = _repository_graph_node_id(rail_slug)
nodes.setdefault(target_id, _placeholder_repository_node(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 {}
return { return {
"ownership_repo": _text_value(declaration, "ownership_repo"), "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)}], "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 { return {
"ownership_repo": _text_value(declaration, "ownership_repo"), "ownership_repo": _text_value(declaration, "ownership_repo"),
"supported_rails": supported_rails, "supported_rails": supported_rails,
"readiness_state": _text_value(declaration, "readiness_state"),
"source_links": [{"label": "Rapp declaration", "path": str(declaration_path)}], "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"), "ownership_repo": _text_value(declaration, "ownership_repo"),
"hosted_rails": hosted_rails, "hosted_rails": hosted_rails,
"bound_rapps": bound_rapps, "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, "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 ""), "substrate_kind": str(repository.get("substrate_kind") or ""),
"path": str(repository.get("path") or ""), "path": str(repository.get("path") or ""),
"declaration_paths": _string_list(repository.get("declaration_paths")), "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: if source_links:
attributes["source_path"] = str(source_links[0].get("path") or "") attributes["source_path"] = str(source_links[0].get("path") or "")
@ -2211,6 +2240,13 @@ def _repository_projection_edge(
if source_links: if source_links:
attributes["source_path"] = str(source_links[0].get("path") or "") attributes["source_path"] = str(source_links[0].get("path") or "")
attributes["source_links"] = source_links 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 { return {
"from": _repository_graph_node_id(source_slug), "from": _repository_graph_node_id(source_slug),
"to": _repository_graph_node_id(target_slug), "to": _repository_graph_node_id(target_slug),
@ -2251,6 +2287,30 @@ def _string_list(value: object) -> list[str]:
return result 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]: def _relation_id_list(value: object, *, key: str) -> list[str]:
if isinstance(value, list): if isinstance(value, list):
result: list[str] = [] result: list[str] = []

View file

@ -73,6 +73,15 @@ repositories:
repo_family: rail repo_family: rail
ownership_repo: railiance-cluster 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 - slug: rapp-openbao
name: rapp-openbao name: rapp-openbao
path: /home/worsch/rapp-openbao path: /home/worsch/rapp-openbao
@ -85,6 +94,18 @@ repositories:
supported_rails: supported_rails:
- rail-kubernetes - 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 - slug: reef-railiance
name: reef-railiance name: reef-railiance
path: /home/worsch/reef-railiance path: /home/worsch/reef-railiance

View file

@ -117,6 +117,9 @@ kind: execution-rail
rail_id: rail-kubernetes rail_id: rail-kubernetes
repo: rail-kubernetes repo: rail-kubernetes
ownership_repo: railiance-cluster ownership_repo: railiance-cluster
contract_version: 1.0.0
composition_kind: base
readiness_state: verified
""".lstrip(), """.lstrip(),
encoding="utf-8", encoding="utf-8",
) )
@ -153,6 +156,10 @@ reef_id: reef-railiance
hosted_rails: hosted_rails:
- rail_id: rail-kubernetes - rail_id: rail-kubernetes
role: primary role: primary
readiness_state: declared
contract_version: 1.0.0
evidence:
- evidence/admission/rail-kubernetes-baseline.json
""".lstrip(), """.lstrip(),
encoding="utf-8", encoding="utf-8",
) )
@ -211,6 +218,9 @@ bound_rapps:
assert reef_record["path"] == str(reef_repo.resolve()) assert reef_record["path"] == str(reef_repo.resolve())
assert reef_record["declaration_paths"] == [str((reef_repo / "declarations" / "reef.yaml").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: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 ( assert (
edges[("repo:rail-kubernetes", "governed_by", "repo:railiance-cluster")]["canonical_type"] edges[("repo:rail-kubernetes", "governed_by", "repo:railiance-cluster")]["canonical_type"]
== "governed_by" == "governed_by"
@ -219,10 +229,13 @@ bound_rapps:
edges[("repo:rapp-openbao", "supports_rail", "repo:rail-kubernetes")]["canonical_type"] edges[("repo:rapp-openbao", "supports_rail", "repo:rail-kubernetes")]["canonical_type"]
== "depends_on" == "depends_on"
) )
assert ( reef_rail = edges[("repo:reef-railiance", "hosts_rail", "repo:rail-kubernetes")]
edges[("repo:reef-railiance", "hosts_rail", "repo:rail-kubernetes")]["canonical_type"] assert reef_rail["canonical_type"] == "deploys"
== "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")] reef_binding = edges[("repo:reef-railiance", "binds_rapp", "repo:rapp-openbao")]
assert reef_binding["canonical_type"] == "deploys" assert reef_binding["canonical_type"] == "deploys"
assert any( 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: def test_registry_http_service_serves_queries(tmp_path: Path) -> None:
store = RegistryStore(tmp_path / "registry.sqlite3") store = RegistryStore(tmp_path / "registry.sqlite3")
store.init_schema() store.init_schema()

View file

@ -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`).