Project repo-family relations from local declarations
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

This commit is contained in:
codex 2026-07-26 08:51:29 +02:00
parent 1d8f59f1de
commit 9af9ee3e72
13 changed files with 671 additions and 28 deletions

View file

@ -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"),
"binds_rapp": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"binds": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"builds_container": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"cataloged_as": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
@ -135,12 +136,14 @@ EDGE_TYPE_CANON_MAP: dict[str, CanonEdgeMapping] = {
"operated_by": CanonEdgeMapping("governed_by", "model/governance", "partial"),
"owned_by": CanonEdgeMapping("governed_by", "model/governance", "partial"),
"owns_deployment": CanonEdgeMapping("part_of", "model/devsecops", "partial", display_only=True),
"hosts_rail": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"provides": CanonEdgeMapping("implements", "model/landscape", "partial"),
"provides_utility_to": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"resolves_to": CanonEdgeMapping("flows_to", "model/network", "partial"),
"routes_to_port": CanonEdgeMapping("flows_to", "model/network", "partial"),
"routes_to_service": CanonEdgeMapping("flows_to", "model/network", "partial"),
"runs_on": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"supports_rail": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"suggests_capability": CanonEdgeMapping("creates_task", "model/task", "partial"),
"uses_config": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"uses_interface": CanonEdgeMapping("depends_on", "model/landscape", "partial"),

View file

@ -450,6 +450,7 @@ def _registry_sync(args: argparse.Namespace) -> int:
{
"slug": repo_slug,
"name": args.name or repo_path.name,
"path": str(repo_path),
"remote_url": args.remote_url or _git_value(repo_path, "config", "--get", "remote.origin.url"),
"default_branch": args.default_branch,
"state_hub_repo_id": args.state_hub_repo_id,
@ -457,6 +458,7 @@ def _registry_sync(args: argparse.Namespace) -> int:
"ownership_repo": args.ownership_repo,
"primary_rail": args.primary_rail,
"supported_rails": args.supported_rail,
"declaration_paths": [str(path.resolve()) for path in args.paths],
"substrate_kind": args.substrate_kind,
},
)
@ -834,7 +836,7 @@ def _scan_manifest_repo(
repository = _registry_post_checked(
registry_url,
"/repositories",
_manifest_repository_payload(item, repo_path, slug),
_manifest_repository_payload(item, repo_path, slug, manifest_dir),
)
stored = _registry_post_checked(
registry_url,
@ -1234,7 +1236,7 @@ def _sync_manifest_repo(registry_url: str, manifest_dir: Path, item: object) ->
repository = _registry_post_checked(
registry_url,
"/repositories",
_manifest_repository_payload(item, repo_path, slug),
_manifest_repository_payload(item, repo_path, slug, manifest_dir),
)
result["repository"] = repository
except RegistryRequestError as exc:
@ -1595,10 +1597,21 @@ def _manifest_optional_path(value: object, manifest_dir: Path) -> Path | None:
return path if path.is_absolute() else (manifest_dir / path).resolve()
def _manifest_repository_payload(item: dict[str, Any], repo_path: Path | None, slug: str) -> dict[str, Any]:
def _manifest_repository_payload(
item: dict[str, Any],
repo_path: Path | None,
slug: str,
manifest_dir: Path | None = None,
) -> dict[str, Any]:
declaration_paths = (
[str(path) for path in _manifest_paths(item.get("declaration_paths"), manifest_dir)]
if manifest_dir is not None
else []
)
return {
"slug": slug,
"name": item.get("name") or (repo_path.name if repo_path else slug),
"path": str(repo_path) if repo_path is not None else None,
"remote_url": item.get("remote_url") or _git_value(repo_path, "config", "--get", "remote.origin.url"),
"default_branch": item.get("default_branch") or "main",
"state_hub_repo_id": item.get("state_hub_repo_id"),
@ -1606,6 +1619,7 @@ def _manifest_repository_payload(item: dict[str, Any], repo_path: Path | None, s
"ownership_repo": item.get("ownership_repo"),
"primary_rail": item.get("primary_rail"),
"supported_rails": item.get("supported_rails"),
"declaration_paths": declaration_paths,
"substrate_kind": item.get("substrate_kind"),
}

View file

@ -61,12 +61,14 @@ _LAYER_COLORS = {
_EDGE_STRENGTH = {
"built_from": "medium",
"binds_rapp": "strong",
"depends_on": "medium",
"deploys": "strong",
"evidenced_by": "medium",
"exposes": "strong",
"flows_to": "medium",
"governed_by": "medium",
"hosts_rail": "strong",
"implements": "medium",
"observed_by": "medium",
"part_of": "weak",
@ -86,6 +88,7 @@ _EDGE_STRENGTH = {
"resolves_to": "medium",
"runs_on": "strong",
"owns_deployment": "weak",
"supports_rail": "medium",
}
@ -325,6 +328,7 @@ def fabric_graph_explorer_payload(
source_repo_slugs = {
str(node.get("repo", "")).strip()
for node in source_nodes
if str(node.get("kind", "")) != "Repository"
if str(node.get("repo", "")).strip()
}
registered_repo_slugs = {
@ -347,6 +351,12 @@ def fabric_graph_explorer_payload(
and str(node.get("id", ""))
and str(node.get("repo", "")).strip()
}
repository_source_references = {
str(node.get("repo", "")).strip(): _source_references(node)
for node in source_nodes
if str(node.get("kind", "")) == "Repository"
and str(node.get("repo", "")).strip()
}
for slug in sorted(repo_slugs):
repo = repository_index.get(slug, {})
has_snapshot = slug in snapshot_repo_slugs or slug in source_repo_slugs
@ -383,7 +393,7 @@ def fabric_graph_explorer_payload(
"displayState": "show",
"visibilitySource": "default",
"visibilityReason": "default",
"sourceReferences": [],
"sourceReferences": repository_source_references.get(slug, []),
"deepLinks": _repository_links(repo),
},
"classes": "repository accepted" if has_snapshot else "repository candidate unresolved",

View file

@ -16,7 +16,7 @@ from .financial import (
materialize_financial_graph_export,
merge_financial_graph_exports,
)
from .loader import repo_root
from .loader import load_yaml, repo_root
from .schema_validation import draft202012_validator
RESET_CONFIRMATION_TOKEN = "RESET-RAILIANCE-FABRIC-GRAPH-DATA"
@ -42,6 +42,7 @@ class RegistryStore:
create table if not exists repositories (
slug text primary key,
name text not null,
path text,
remote_url text,
default_branch text,
state_hub_repo_id text,
@ -49,6 +50,7 @@ class RegistryStore:
ownership_repo text,
primary_rail text,
supported_rails_json text not null default '[]',
declaration_paths_json text not null default '[]',
substrate_kind text,
created_at text not null,
updated_at text not null
@ -138,6 +140,7 @@ class RegistryStore:
slug = _required_text(payload, "slug")
now = _utc_now()
name = str(payload.get("name") or slug)
path = _optional_text(payload, "path")
remote_url = _optional_text(payload, "remote_url")
default_branch = str(payload.get("default_branch") or "main")
state_hub_repo_id = _optional_text(payload, "state_hub_repo_id")
@ -145,18 +148,20 @@ class RegistryStore:
ownership_repo = _optional_text(payload, "ownership_repo")
primary_rail = _optional_text(payload, "primary_rail")
supported_rails = _optional_string_list(payload, "supported_rails")
declaration_paths = _optional_string_list(payload, "declaration_paths")
substrate_kind = _optional_text(payload, "substrate_kind")
with self._connect() as db:
db.execute(
"""
insert into repositories (
slug, name, remote_url, default_branch, state_hub_repo_id,
slug, name, path, remote_url, default_branch, state_hub_repo_id,
repo_family, ownership_repo, primary_rail, supported_rails_json,
substrate_kind, created_at, updated_at
declaration_paths_json, substrate_kind, created_at, updated_at
)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict(slug) do update set
name = excluded.name,
path = excluded.path,
remote_url = excluded.remote_url,
default_branch = excluded.default_branch,
state_hub_repo_id = excluded.state_hub_repo_id,
@ -164,12 +169,14 @@ class RegistryStore:
ownership_repo = excluded.ownership_repo,
primary_rail = excluded.primary_rail,
supported_rails_json = excluded.supported_rails_json,
declaration_paths_json = excluded.declaration_paths_json,
substrate_kind = excluded.substrate_kind,
updated_at = excluded.updated_at
""",
(
slug,
name,
path,
remote_url,
default_branch,
state_hub_repo_id,
@ -177,6 +184,7 @@ class RegistryStore:
ownership_repo,
primary_rail,
json.dumps(supported_rails, sort_keys=True),
json.dumps(declaration_paths, sort_keys=True),
substrate_kind,
now,
now,
@ -188,9 +196,10 @@ class RegistryStore:
with self._connect() as db:
rows = db.execute(
"""
select slug, name, remote_url, default_branch, state_hub_repo_id,
select slug, name, path, remote_url, default_branch, state_hub_repo_id,
repo_family, ownership_repo, primary_rail,
supported_rails_json, substrate_kind, created_at, updated_at
supported_rails_json, declaration_paths_json, substrate_kind,
created_at, updated_at
from repositories
order by slug
"""
@ -201,9 +210,10 @@ class RegistryStore:
with self._connect() as db:
row = db.execute(
"""
select slug, name, remote_url, default_branch, state_hub_repo_id,
select slug, name, path, remote_url, default_branch, state_hub_repo_id,
repo_family, ownership_repo, primary_rail,
supported_rails_json, substrate_kind, created_at, updated_at
supported_rails_json, declaration_paths_json, substrate_kind,
created_at, updated_at
from repositories
where slug = ?
""",
@ -474,6 +484,13 @@ class RegistryStore:
for edge in graph.get("edges", []):
if isinstance(edge, dict):
edges.append(_edge_with_canon_metadata(edge))
projected_nodes, projected_edges = _project_repository_family_graph(self.list_repositories())
for node in projected_nodes:
node_id = str(node.get("id", ""))
if node_id:
nodes[node_id] = node
for edge in projected_edges:
edges.append(_edge_with_canon_metadata(edge))
return {
"apiVersion": "railiance.fabric/v1alpha1",
"kind": "FabricGraphExport",
@ -1538,11 +1555,17 @@ def _row_dict(row: sqlite3.Row) -> dict[str, Any]:
def _repository_dict(row: sqlite3.Row) -> dict[str, Any]:
data = _row_dict(row)
raw_supported_rails = data.pop("supported_rails_json", "[]")
raw_declaration_paths = data.pop("declaration_paths_json", "[]")
try:
decoded = json.loads(raw_supported_rails or "[]")
except json.JSONDecodeError:
decoded = []
data["supported_rails"] = decoded if isinstance(decoded, list) else []
try:
declaration_paths = json.loads(raw_declaration_paths or "[]")
except json.JSONDecodeError:
declaration_paths = []
data["declaration_paths"] = declaration_paths if isinstance(declaration_paths, list) else []
return data
@ -1552,10 +1575,12 @@ def _ensure_repository_columns(db: sqlite3.Connection) -> None:
for row in db.execute("pragma table_info(repositories)").fetchall()
}
additions = {
"path": "text",
"repo_family": "text",
"ownership_repo": "text",
"primary_rail": "text",
"supported_rails_json": "text not null default '[]'",
"declaration_paths_json": "text not null default '[]'",
"substrate_kind": "text",
}
for name, ddl in additions.items():
@ -1989,6 +2014,262 @@ def _optional_string_list(payload: dict[str, Any], key: str) -> list[str]:
return result
def _project_repository_family_graph(repositories: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
nodes: dict[str, dict[str, Any]] = {}
edges: dict[tuple[str, str, str], dict[str, Any]] = {}
for repository in repositories:
slug = str(repository.get("slug") or "").strip()
if not slug:
continue
projection = _repository_family_projection(repository)
nodes[_repository_graph_node_id(slug)] = _repository_projection_node(repository, projection)
ownership_repo = projection.get("ownership_repo")
if isinstance(ownership_repo, str) and ownership_repo:
target_id = _repository_graph_node_id(ownership_repo)
nodes.setdefault(target_id, _placeholder_repository_node(ownership_repo))
edge = _repository_projection_edge(
slug,
ownership_repo,
"governed_by",
projection,
)
if edge is not None:
edges[_edge_key(edge)] = edge
for rail_slug in _string_list(projection.get("supported_rails")):
target_id = _repository_graph_node_id(rail_slug)
nodes.setdefault(target_id, _placeholder_repository_node(rail_slug))
edge = _repository_projection_edge(slug, rail_slug, "supports_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))
edge = _repository_projection_edge(slug, rail_slug, "hosts_rail", projection)
if edge is not None:
edges[_edge_key(edge)] = edge
for rapp_slug in _string_list(projection.get("bound_rapps")):
target_id = _repository_graph_node_id(rapp_slug)
nodes.setdefault(target_id, _placeholder_repository_node(rapp_slug))
edge = _repository_projection_edge(slug, rapp_slug, "binds_rapp", projection)
if edge is not None:
edges[_edge_key(edge)] = edge
return [nodes[key] for key in sorted(nodes)], [edges[key] for key in sorted(edges)]
def _repository_family_projection(repository: dict[str, Any]) -> dict[str, Any]:
family = str(repository.get("repo_family") or "").strip()
repo_path = _repository_checkout_path(repository)
if repo_path is None or not repo_path.is_dir():
return {}
if family == "rail":
declaration_path = repo_path / "declarations" / "rail.yaml"
declaration = _load_yaml_mapping(declaration_path)
if declaration is None:
return {}
return {
"ownership_repo": _text_value(declaration, "ownership_repo"),
"source_links": [{"label": "Rail declaration", "path": str(declaration_path)}],
}
if family == "rapp":
declaration_path = repo_path / "declarations" / "rapp.yaml"
declaration = _load_yaml_mapping(declaration_path)
if declaration is None:
return {}
supported_rails = _string_list(declaration.get("supported_rails"))
if not supported_rails:
primary_rail = _text_value(declaration, "primary_rail")
if primary_rail:
supported_rails = [primary_rail]
return {
"ownership_repo": _text_value(declaration, "ownership_repo"),
"supported_rails": supported_rails,
"source_links": [{"label": "Rapp declaration", "path": str(declaration_path)}],
}
if family == "reef":
declaration_path = repo_path / "declarations" / "reef.yaml"
declaration = _load_yaml_mapping(declaration_path)
if declaration is None:
return {}
rails_path = repo_path / "bindings" / "rails.yaml"
rails_binding = _load_yaml_mapping(rails_path)
hosted_rails = _relation_id_list(
rails_binding.get("hosted_rails") if isinstance(rails_binding, dict) else None,
key="rail_id",
)
if not hosted_rails:
hosted_rails = _relation_id_list(declaration.get("hosted_rails"), key="rail_id")
if not hosted_rails:
primary_rail = _text_value(declaration, "primary_rail")
if primary_rail:
hosted_rails = [primary_rail]
rapps_path = repo_path / "bindings" / "rapps.yaml"
rapps_binding = _load_yaml_mapping(rapps_path)
bound_rapps = _relation_id_list(
rapps_binding.get("bound_rapps") if isinstance(rapps_binding, dict) else None,
key="rapp_id",
)
if not bound_rapps:
bound_rapps = _relation_id_list(declaration.get("bound_rapps"), key="rapp_id")
source_links = [{"label": "Reef declaration", "path": str(declaration_path)}]
if rails_binding is not None:
source_links.append({"label": "Reef rail bindings", "path": str(rails_path)})
if rapps_binding is not None:
source_links.append({"label": "Reef rapp bindings", "path": str(rapps_path)})
return {
"ownership_repo": _text_value(declaration, "ownership_repo"),
"hosted_rails": hosted_rails,
"bound_rapps": bound_rapps,
"source_links": source_links,
}
return {}
def _repository_checkout_path(repository: dict[str, Any]) -> Path | None:
raw_path = repository.get("path")
if not isinstance(raw_path, str) or not raw_path.strip():
return None
return Path(raw_path).expanduser().resolve()
def _repository_graph_node_id(slug: str) -> str:
return f"repo:{slug}"
def _repository_projection_node(repository: dict[str, Any], projection: dict[str, Any]) -> dict[str, Any]:
slug = str(repository.get("slug") or "")
repo_id = _repository_graph_node_id(slug)
repo_name = str(repository.get("name") or slug)
source_links = projection.get("source_links") if isinstance(projection.get("source_links"), list) else []
attributes: dict[str, Any] = {
"repo_family": str(repository.get("repo_family") or ""),
"ownership_repo": str(repository.get("ownership_repo") or ""),
"primary_rail": str(repository.get("primary_rail") or ""),
"supported_rails": _string_list(repository.get("supported_rails")),
"substrate_kind": str(repository.get("substrate_kind") or ""),
"path": str(repository.get("path") or ""),
"declaration_paths": _string_list(repository.get("declaration_paths")),
}
if source_links:
attributes["source_path"] = str(source_links[0].get("path") or "")
attributes["source_links"] = source_links
canon_mapping = node_canon_mapping("Repository")
return {
"id": repo_id,
"kind": "Repository",
"name": repo_name,
"repo": slug,
"domain": "railiance",
"lifecycle": "active",
"canon_category": canon_mapping.category,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"evidence_state": "declared",
"attributes": {key: value for key, value in attributes.items() if value not in ("", [], None)},
}
def _placeholder_repository_node(slug: str) -> dict[str, Any]:
canon_mapping = node_canon_mapping("Repository")
return {
"id": _repository_graph_node_id(slug),
"kind": "Repository",
"name": slug,
"repo": slug,
"domain": "railiance",
"lifecycle": "registered-only",
"canon_category": canon_mapping.category,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"evidence_state": "inferred",
"attributes": {},
}
def _repository_projection_edge(
source_slug: str,
target_slug: str,
edge_type: str,
projection: dict[str, Any],
) -> dict[str, Any] | None:
if not source_slug or not target_slug:
return None
source_links = projection.get("source_links") if isinstance(projection.get("source_links"), list) else []
attributes: dict[str, Any] = {}
if source_links:
attributes["source_path"] = str(source_links[0].get("path") or "")
attributes["source_links"] = source_links
return {
"from": _repository_graph_node_id(source_slug),
"to": _repository_graph_node_id(target_slug),
"type": edge_type,
"evidence_state": "declared",
"attributes": attributes,
}
def _load_yaml_mapping(path: Path) -> dict[str, Any] | None:
if not path.is_file():
return None
try:
data = load_yaml(path)
except Exception:
return None
return data if isinstance(data, dict) else None
def _text_value(payload: dict[str, Any], key: str) -> str:
value = payload.get(key)
return value.strip() if isinstance(value, str) else ""
def _string_list(value: object) -> list[str]:
if not isinstance(value, list):
return []
result: list[str] = []
seen: set[str] = set()
for item in value:
if not isinstance(item, str):
continue
cleaned = item.strip()
if not cleaned or cleaned in seen:
continue
seen.add(cleaned)
result.append(cleaned)
return result
def _relation_id_list(value: object, *, key: str) -> list[str]:
if isinstance(value, list):
result: list[str] = []
seen: set[str] = set()
for item in value:
relation_id = ""
if isinstance(item, str):
relation_id = item.strip()
elif isinstance(item, dict):
raw = item.get(key)
relation_id = raw.strip() if isinstance(raw, str) else ""
if not relation_id or relation_id in seen:
continue
seen.add(relation_id)
result.append(relation_id)
return result
return []
def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")