Project rail composition and readiness
This commit is contained in:
parent
9af9ee3e72
commit
4fcfee0479
7 changed files with 238 additions and 4 deletions
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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] = []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
Loading…
Add table
Add a link
Reference in a new issue