Add repo-family metadata to Fabric registry
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 3s

This commit is contained in:
codex 2026-07-25 18:08:57 +02:00
parent 3240b78185
commit 0ea99a73d8
15 changed files with 189 additions and 32 deletions

View file

@ -68,7 +68,7 @@ read model.
## Current State ## Current State
- Status: active implementation - Status: operational / evolving
- Implementation: registry service, graph validation and query tooling, graph - Implementation: registry service, graph validation and query tooling, graph
explorer, State Hub export contract, financial-fabric reset flow, and local explorer, State Hub export contract, financial-fabric reset flow, and local
onboarding manifests are all present onboarding manifests are all present

View file

@ -8,8 +8,8 @@
| Kind | ID | Status | Lane | Source | | Kind | ID | Status | Lane | Source |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| workplan | RAIL-FAB-WP-0025 | active | — | 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 |
| 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 |
| task | RAIL-FAB-WP-0025-T04 | wait | — | workplans/RAIL-FAB-WP-0025-rail-rapp-reef-fabric-adaptation.md | | task | RAIL-FAB-WP-0025-T04 | done | — | workplans/RAIL-FAB-WP-0025-rail-rapp-reef-fabric-adaptation.md |

View file

@ -19,6 +19,7 @@ This document is intentionally narrower than the framework architecture in
The first adaptation wave is deliberately small: The first adaptation wave is deliberately small:
- onboard the missing current Railiance repos in the seed manifest - onboard the missing current Railiance repos in the seed manifest
- persist repo-family metadata in the registry and repository projections
- add the first rail-specific capability type - add the first rail-specific capability type
- define canonical `service_type` values for future declarations - define canonical `service_type` values for future declarations
- avoid inventing full repo-family metadata or new export relations before the - avoid inventing full repo-family metadata or new export relations before the
@ -63,6 +64,21 @@ Initial Fabric shape:
The first declarations should make the substrate boundary visible before Fabric The first declarations should make the substrate boundary visible before Fabric
tries to model every machine or placement relation in detail. tries to model every machine or placement relation in detail.
## Registry Metadata Path
The registry now carries the first repo-family-aware metadata fields on
repository records:
- `repo_family`
- `ownership_repo`
- `primary_rail`
- `supported_rails`
- `substrate_kind`
This metadata is available through repository registration, manifest sync, and
repository projections such as the graph explorer's synthesized repository
nodes.
## First New Capability Type ## First New Capability Type
Fabric now reserves: Fabric now reserves:
@ -83,12 +99,13 @@ service-level visibility before they need their own dedicated capability types.
The following are not done by this adaptation slice: The following are not done by this adaptation slice:
- repo-level `repo_family` metadata in the registry
- `ownership_repo`, `supported_rails`, `primary_rail`, or `substrate_kind`
registry metadata
- export relations such as `supports_rail`, `hosts_rail`, `binds_rapp`, or - export relations such as `supports_rail`, `hosts_rail`, `binds_rapp`, or
`governed_by` `governed_by`
- validator rules for repo-family-specific declaration completeness - validator rules for repo-family-specific declaration completeness
- graph queries that resolve placement or governance directly from the new
repo-family metadata
- State Hub read-model fields that surface repo family directly in generated
repo briefs or inventory views
Those require a second wave after the first real `rail-*`, `rapp-*`, and Those require a second wave after the first real `rail-*`, `rapp-*`, and
`reef-*` repos exist. `reef-*` repos exist.

View file

@ -45,6 +45,7 @@ repositories:
path: .. path: ..
default_branch: main default_branch: main
state_hub_repo_id: 2c0de614-e468-4eb6-8157-470649ac8c05 state_hub_repo_id: 2c0de614-e468-4eb6-8157-470649ac8c05
repo_family: ownership
declaration_paths: declaration_paths:
- .. - ..
sbom: bom.json sbom: bom.json
@ -58,6 +59,17 @@ repositories:
link back to State Hub repo views, but do not block onboarding when a repo is link back to State Hub repo views, but do not block onboarding when a repo is
registered in Fabric before that ID is wired in here. registered in Fabric before that ID is wired in here.
The following repo-family metadata fields are also optional:
- `repo_family`: `ownership`, `rail`, `rapp`, or `reef`
- `ownership_repo`: canonical owning `railiance-*` repo when the registered repo
is not itself an ownership repo
- `primary_rail`: default rail for a reef or package when one is explicitly
declared
- `supported_rails`: array of supported rail ids for `rapp-*` repos
- `substrate_kind`: grouped substrate category such as `server`,
`workstation-group`, or `cluster`
`sbom` or `sboms` may point to CycloneDX JSON/YAML files. When present, the `sbom` or `sboms` may point to CycloneDX JSON/YAML files. When present, the
command ingests them as queryable library inventory after repository command ingests them as queryable library inventory after repository
registration. registration.

View file

@ -1078,6 +1078,11 @@ def _registry_manifest_evidence(root: dict[str, Any], *, max_items: int) -> list
"domain": repo.get("domain", ""), "domain": repo.get("domain", ""),
"default_branch": repo.get("default_branch", ""), "default_branch": repo.get("default_branch", ""),
"state_hub_repo_id": repo.get("state_hub_repo_id", ""), "state_hub_repo_id": repo.get("state_hub_repo_id", ""),
"repo_family": repo.get("repo_family", ""),
"ownership_repo": repo.get("ownership_repo", ""),
"primary_rail": repo.get("primary_rail", ""),
"supported_rails": repo.get("supported_rails", []) if isinstance(repo.get("supported_rails"), list) else [],
"substrate_kind": repo.get("substrate_kind", ""),
"has_local_path": bool(repo.get("path")), "has_local_path": bool(repo.get("path")),
"has_remote_url": bool(repo.get("remote_url")), "has_remote_url": bool(repo.get("remote_url")),
"canonical_slug": identity_slug if identity_slug != repo_slug else "", "canonical_slug": identity_slug if identity_slug != repo_slug else "",

View file

@ -155,6 +155,11 @@ def build_parser() -> argparse.ArgumentParser:
sync.add_argument("--remote-url", default=None) sync.add_argument("--remote-url", default=None)
sync.add_argument("--default-branch", default="main") sync.add_argument("--default-branch", default="main")
sync.add_argument("--state-hub-repo-id", default=None) sync.add_argument("--state-hub-repo-id", default=None)
sync.add_argument("--repo-family", default=None)
sync.add_argument("--ownership-repo", default=None)
sync.add_argument("--primary-rail", default=None)
sync.add_argument("--supported-rail", action="append", default=[], help="Declare a supported rail. May be repeated.")
sync.add_argument("--substrate-kind", default=None)
sync.add_argument("--commit", default=None) sync.add_argument("--commit", default=None)
sync.add_argument("--json", action="store_true", help="Print the raw snapshot response.") sync.add_argument("--json", action="store_true", help="Print the raw snapshot response.")
@ -448,6 +453,11 @@ def _registry_sync(args: argparse.Namespace) -> int:
"remote_url": args.remote_url or _git_value(repo_path, "config", "--get", "remote.origin.url"), "remote_url": args.remote_url or _git_value(repo_path, "config", "--get", "remote.origin.url"),
"default_branch": args.default_branch, "default_branch": args.default_branch,
"state_hub_repo_id": args.state_hub_repo_id, "state_hub_repo_id": args.state_hub_repo_id,
"repo_family": args.repo_family,
"ownership_repo": args.ownership_repo,
"primary_rail": args.primary_rail,
"supported_rails": args.supported_rail,
"substrate_kind": args.substrate_kind,
}, },
) )
snapshot = _registry_post( snapshot = _registry_post(
@ -824,13 +834,7 @@ def _scan_manifest_repo(
repository = _registry_post_checked( repository = _registry_post_checked(
registry_url, registry_url,
"/repositories", "/repositories",
{ _manifest_repository_payload(item, repo_path, slug),
"slug": slug,
"name": item.get("name") or repo_path.name,
"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"),
},
) )
stored = _registry_post_checked( stored = _registry_post_checked(
registry_url, registry_url,
@ -1230,13 +1234,7 @@ def _sync_manifest_repo(registry_url: str, manifest_dir: Path, item: object) ->
repository = _registry_post_checked( repository = _registry_post_checked(
registry_url, registry_url,
"/repositories", "/repositories",
{ _manifest_repository_payload(item, repo_path, slug),
"slug": slug,
"name": item.get("name") or (repo_path.name if repo_path else slug),
"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"),
},
) )
result["repository"] = repository result["repository"] = repository
except RegistryRequestError as exc: except RegistryRequestError as exc:
@ -1597,6 +1595,21 @@ def _manifest_optional_path(value: object, manifest_dir: Path) -> Path | None:
return path if path.is_absolute() else (manifest_dir / path).resolve() 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]:
return {
"slug": slug,
"name": item.get("name") or (repo_path.name if repo_path else slug),
"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"),
"repo_family": item.get("repo_family"),
"ownership_repo": item.get("ownership_repo"),
"primary_rail": item.get("primary_rail"),
"supported_rails": item.get("supported_rails"),
"substrate_kind": item.get("substrate_kind"),
}
def _manifest_paths(value: object, manifest_dir: Path) -> list[Path]: def _manifest_paths(value: object, manifest_dir: Path) -> list[Path]:
if value is None: if value is None:
return [] return []

View file

@ -209,6 +209,11 @@ class LocalFabricRegistryConnector:
"registry_remote_url": match.get("remote_url") or "", "registry_remote_url": match.get("remote_url") or "",
"registry_default_branch": match.get("default_branch") or "", "registry_default_branch": match.get("default_branch") or "",
"state_hub_repo_id": match.get("state_hub_repo_id") or "", "state_hub_repo_id": match.get("state_hub_repo_id") or "",
"repo_family": match.get("repo_family") or "",
"ownership_repo": match.get("ownership_repo") or "",
"primary_rail": match.get("primary_rail") or "",
"supported_rails": match.get("supported_rails") if isinstance(match.get("supported_rails"), list) else [],
"substrate_kind": match.get("substrate_kind") or "",
"declaration_paths": match.get("declaration_paths") if isinstance(match.get("declaration_paths"), list) else [], "declaration_paths": match.get("declaration_paths") if isinstance(match.get("declaration_paths"), list) else [],
}, },
"origin": "registry", "origin": "registry",

View file

@ -375,6 +375,11 @@ def fabric_graph_explorer_payload(
"confidence": 1.0 if has_snapshot else 0.3, "confidence": 1.0 if has_snapshot else 0.3,
"visualSize": 56 if has_snapshot else 42, "visualSize": 56 if has_snapshot else 42,
"ownership": "registry", "ownership": "registry",
"repoFamily": str(repo.get("repo_family") or ""),
"ownershipRepo": str(repo.get("ownership_repo") or ""),
"primaryRail": str(repo.get("primary_rail") or ""),
"supportedRails": repo.get("supported_rails") if isinstance(repo.get("supported_rails"), list) else [],
"substrateKind": str(repo.get("substrate_kind") or ""),
"displayState": "show", "displayState": "show",
"visibilitySource": "default", "visibilitySource": "default",
"visibilityReason": "default", "visibilityReason": "default",

View file

@ -45,6 +45,11 @@ class RegistryStore:
remote_url text, remote_url text,
default_branch text, default_branch text,
state_hub_repo_id text, state_hub_repo_id text,
repo_family text,
ownership_repo text,
primary_rail text,
supported_rails_json text not null default '[]',
substrate_kind text,
created_at text not null, created_at text not null,
updated_at text not null updated_at text not null
); );
@ -127,6 +132,7 @@ class RegistryStore:
); );
""" """
) )
_ensure_repository_columns(db)
def upsert_repository(self, payload: dict[str, Any]) -> dict[str, Any]: def upsert_repository(self, payload: dict[str, Any]) -> dict[str, Any]:
slug = _required_text(payload, "slug") slug = _required_text(payload, "slug")
@ -135,22 +141,46 @@ class RegistryStore:
remote_url = _optional_text(payload, "remote_url") remote_url = _optional_text(payload, "remote_url")
default_branch = str(payload.get("default_branch") or "main") default_branch = str(payload.get("default_branch") or "main")
state_hub_repo_id = _optional_text(payload, "state_hub_repo_id") state_hub_repo_id = _optional_text(payload, "state_hub_repo_id")
repo_family = _optional_text(payload, "repo_family")
ownership_repo = _optional_text(payload, "ownership_repo")
primary_rail = _optional_text(payload, "primary_rail")
supported_rails = _optional_string_list(payload, "supported_rails")
substrate_kind = _optional_text(payload, "substrate_kind")
with self._connect() as db: with self._connect() as db:
db.execute( db.execute(
""" """
insert into repositories ( insert into repositories (
slug, name, remote_url, default_branch, state_hub_repo_id, slug, name, remote_url, default_branch, state_hub_repo_id,
created_at, updated_at repo_family, ownership_repo, primary_rail, supported_rails_json,
substrate_kind, created_at, updated_at
) )
values (?, ?, ?, ?, ?, ?, ?) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
on conflict(slug) do update set on conflict(slug) do update set
name = excluded.name, name = excluded.name,
remote_url = excluded.remote_url, remote_url = excluded.remote_url,
default_branch = excluded.default_branch, default_branch = excluded.default_branch,
state_hub_repo_id = excluded.state_hub_repo_id, state_hub_repo_id = excluded.state_hub_repo_id,
repo_family = excluded.repo_family,
ownership_repo = excluded.ownership_repo,
primary_rail = excluded.primary_rail,
supported_rails_json = excluded.supported_rails_json,
substrate_kind = excluded.substrate_kind,
updated_at = excluded.updated_at updated_at = excluded.updated_at
""", """,
(slug, name, remote_url, default_branch, state_hub_repo_id, now, now), (
slug,
name,
remote_url,
default_branch,
state_hub_repo_id,
repo_family,
ownership_repo,
primary_rail,
json.dumps(supported_rails, sort_keys=True),
substrate_kind,
now,
now,
),
) )
return self.get_repository(slug) return self.get_repository(slug)
@ -159,19 +189,21 @@ class RegistryStore:
rows = db.execute( rows = db.execute(
""" """
select slug, name, remote_url, default_branch, state_hub_repo_id, select slug, name, remote_url, default_branch, state_hub_repo_id,
created_at, updated_at repo_family, ownership_repo, primary_rail,
supported_rails_json, substrate_kind, created_at, updated_at
from repositories from repositories
order by slug order by slug
""" """
).fetchall() ).fetchall()
return [_row_dict(row) for row in rows] return [_repository_dict(row) for row in rows]
def get_repository(self, slug: str) -> dict[str, Any]: def get_repository(self, slug: str) -> dict[str, Any]:
with self._connect() as db: with self._connect() as db:
row = db.execute( row = db.execute(
""" """
select slug, name, remote_url, default_branch, state_hub_repo_id, select slug, name, remote_url, default_branch, state_hub_repo_id,
created_at, updated_at repo_family, ownership_repo, primary_rail,
supported_rails_json, substrate_kind, created_at, updated_at
from repositories from repositories
where slug = ? where slug = ?
""", """,
@ -179,7 +211,7 @@ class RegistryStore:
).fetchone() ).fetchone()
if row is None: if row is None:
raise RegistryError(f"repository not found: {slug}", 404) raise RegistryError(f"repository not found: {slug}", 404)
return _row_dict(row) return _repository_dict(row)
def add_snapshot(self, repo_slug: str, payload: dict[str, Any]) -> dict[str, Any]: def add_snapshot(self, repo_slug: str, payload: dict[str, Any]) -> dict[str, Any]:
self.get_repository(repo_slug) self.get_repository(repo_slug)
@ -1503,6 +1535,34 @@ def _row_dict(row: sqlite3.Row) -> dict[str, Any]:
return {key: row[key] for key in row.keys()} return {key: row[key] for key in row.keys()}
def _repository_dict(row: sqlite3.Row) -> dict[str, Any]:
data = _row_dict(row)
raw_supported_rails = data.pop("supported_rails_json", "[]")
try:
decoded = json.loads(raw_supported_rails or "[]")
except json.JSONDecodeError:
decoded = []
data["supported_rails"] = decoded if isinstance(decoded, list) else []
return data
def _ensure_repository_columns(db: sqlite3.Connection) -> None:
existing = {
str(row[1])
for row in db.execute("pragma table_info(repositories)").fetchall()
}
additions = {
"repo_family": "text",
"ownership_repo": "text",
"primary_rail": "text",
"supported_rails_json": "text not null default '[]'",
"substrate_kind": "text",
}
for name, ddl in additions.items():
if name not in existing:
db.execute(f"alter table repositories add column {name} {ddl}")
def _resettable_counts(db: sqlite3.Connection) -> dict[str, int]: def _resettable_counts(db: sqlite3.Connection) -> dict[str, int]:
return { return {
"snapshots": int(db.execute("select count(*) from snapshots").fetchone()[0]), "snapshots": int(db.execute("select count(*) from snapshots").fetchone()[0]),
@ -1911,6 +1971,24 @@ def _optional_text(payload: dict[str, Any], key: str) -> str | None:
return value return value
def _optional_string_list(payload: dict[str, Any], key: str) -> list[str]:
value = payload.get(key)
if value is None:
return []
if not isinstance(value, list):
raise RegistryError(f"field '{key}' must be an array of strings")
result: list[str] = []
seen: set[str] = set()
for item in value:
if not isinstance(item, str) or not item.strip():
raise RegistryError(f"field '{key}' must be an array of non-empty strings")
cleaned = item.strip()
if cleaned not in seen:
seen.add(cleaned)
result.append(cleaned)
return result
def _utc_now() -> str: def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z") return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")

View file

@ -7,6 +7,7 @@ repositories:
path: .. path: ..
default_branch: main default_branch: main
state_hub_repo_id: 2c0de614-e468-4eb6-8157-470649ac8c05 state_hub_repo_id: 2c0de614-e468-4eb6-8157-470649ac8c05
repo_family: ownership
declaration_paths: declaration_paths:
- .. - ..
@ -15,12 +16,14 @@ repositories:
path: /home/worsch/railiance-master path: /home/worsch/railiance-master
remote_url: forgejo-remote:coulomb/railiance-master.git remote_url: forgejo-remote:coulomb/railiance-master.git
default_branch: main default_branch: main
repo_family: ownership
- slug: railiance-forge - slug: railiance-forge
name: railiance-forge name: railiance-forge
path: /home/worsch/railiance-forge path: /home/worsch/railiance-forge
remote_url: forgejo-remote:coulomb/railiance-forge.git remote_url: forgejo-remote:coulomb/railiance-forge.git
default_branch: main default_branch: main
repo_family: ownership
- slug: railiance-infra - slug: railiance-infra
name: railiance-infra name: railiance-infra
@ -28,6 +31,7 @@ repositories:
remote_url: http://92.205.130.254:32166/coulomb/railiance-infra.git remote_url: http://92.205.130.254:32166/coulomb/railiance-infra.git
default_branch: main default_branch: main
state_hub_repo_id: 485187c0-4fad-42f7-984c-5e317a66c5de state_hub_repo_id: 485187c0-4fad-42f7-984c-5e317a66c5de
repo_family: ownership
- slug: railiance-cluster - slug: railiance-cluster
name: railiance-cluster name: railiance-cluster
@ -35,6 +39,7 @@ repositories:
remote_url: http://92.205.130.254:32166/coulomb/railiance-cluster.git remote_url: http://92.205.130.254:32166/coulomb/railiance-cluster.git
default_branch: main default_branch: main
state_hub_repo_id: c3fd0dd2-a0de-415c-8e3a-e37406f4b8f8 state_hub_repo_id: c3fd0dd2-a0de-415c-8e3a-e37406f4b8f8
repo_family: ownership
- slug: railiance-platform - slug: railiance-platform
name: railiance-platform name: railiance-platform
@ -42,6 +47,7 @@ repositories:
remote_url: http://92.205.130.254:32166/coulomb/railiance-platform.git remote_url: http://92.205.130.254:32166/coulomb/railiance-platform.git
default_branch: main default_branch: main
state_hub_repo_id: 5115e0c5-009f-4168-b155-9943fe2ab9a7 state_hub_repo_id: 5115e0c5-009f-4168-b155-9943fe2ab9a7
repo_family: ownership
- slug: railiance-apps - slug: railiance-apps
name: railiance-apps name: railiance-apps
@ -49,6 +55,7 @@ repositories:
remote_url: http://92.205.130.254:32166/coulomb/railiance-apps.git remote_url: http://92.205.130.254:32166/coulomb/railiance-apps.git
default_branch: main default_branch: main
state_hub_repo_id: cb06310e-4381-428b-b8f3-d3ea8ac2f70d state_hub_repo_id: cb06310e-4381-428b-b8f3-d3ea8ac2f70d
repo_family: ownership
- slug: railiance-enablement - slug: railiance-enablement
name: railiance-enablement name: railiance-enablement
@ -56,6 +63,7 @@ repositories:
remote_url: http://92.205.130.254:32166/coulomb/railiance-enablement.git remote_url: http://92.205.130.254:32166/coulomb/railiance-enablement.git
default_branch: main default_branch: main
state_hub_repo_id: bc978be9-ca72-42bb-a451-d4737b779c5b state_hub_repo_id: bc978be9-ca72-42bb-a451-d4737b779c5b
repo_family: ownership
- slug: flex-auth - slug: flex-auth
name: flex-auth name: flex-auth

View file

@ -34,6 +34,7 @@ def test_collect_accountability_root_evidence_from_manifest(tmp_path: Path) -> N
registered_repo = next(item for item in evidence if item["evidence_type"] == "registered_repository") registered_repo = next(item for item in evidence if item["evidence_type"] == "registered_repository")
assert registered_repo["attributes"]["state_hub_repo_id"] == "fixture-state-hub-id" assert registered_repo["attributes"]["state_hub_repo_id"] == "fixture-state-hub-id"
assert registered_repo["attributes"]["repo_family"] == "ownership"
secret_root = next(item for item in evidence if item["evidence_type"] == "secret_root") secret_root = next(item for item in evidence if item["evidence_type"] == "secret_root")
assert "secret-value" not in json.dumps(secret_root) assert "secret-value" not in json.dumps(secret_root)
@ -420,6 +421,7 @@ repositories:
default_branch: main default_branch: main
state_hub_repo_id: fixture-state-hub-id state_hub_repo_id: fixture-state-hub-id
remote_url: gitea-remote:coulomb/fixture-repo.git remote_url: gitea-remote:coulomb/fixture-repo.git
repo_family: ownership
""".format(repo=repo), """.format(repo=repo),
encoding="utf-8", encoding="utf-8",
) )

View file

@ -42,7 +42,7 @@ def test_local_fabric_registry_connector_adds_separate_registry_evidence(tmp_pat
_validate_schema("discovery-snapshot.schema.yaml", snapshot) _validate_schema("discovery-snapshot.schema.yaml", snapshot)
assert snapshot["connector_runs"][0]["status"] == "success" assert snapshot["connector_runs"][0]["status"] == "success"
assert snapshot["connector_runs"][0]["candidate_counts"] == {"nodes": 1, "edges": 1, "attributes": 8} assert snapshot["connector_runs"][0]["candidate_counts"] == {"nodes": 1, "edges": 1, "attributes": 9}
assert any(scope["source_kind"] == "fabric_registry" for scope in snapshot["replacement_scopes"]) assert any(scope["source_kind"] == "fabric_registry" for scope in snapshot["replacement_scopes"])
registry_node = next(node for node in snapshot["candidates"]["nodes"] if node["kind"] == "FabricRegistryEntry") registry_node = next(node for node in snapshot["candidates"]["nodes"] if node["kind"] == "FabricRegistryEntry")
@ -50,6 +50,7 @@ def test_local_fabric_registry_connector_adds_separate_registry_evidence(tmp_pat
assert registry_node["review_state"] == "candidate" assert registry_node["review_state"] == "candidate"
assert registry_node["replacement_scope"].startswith("scope:fixture-repo:local-fabric-registry:fabric_registry") assert registry_node["replacement_scope"].startswith("scope:fixture-repo:local-fabric-registry:fabric_registry")
assert registry_node["attributes"]["state_hub_repo_id"] == "state-hub-id" assert registry_node["attributes"]["state_hub_repo_id"] == "state-hub-id"
assert registry_node["attributes"]["repo_family"] == "ownership"
assert any(edge["edge_type"] == "cataloged_as" for edge in snapshot["candidates"]["edges"]) assert any(edge["edge_type"] == "cataloged_as" for edge in snapshot["candidates"]["edges"])
registry_attributes = { registry_attributes = {
attribute["name"]: attribute attribute["name"]: attribute
@ -136,6 +137,7 @@ repositories:
default_branch: main default_branch: main
state_hub_repo_id: state-hub-id state_hub_repo_id: state-hub-id
remote_url: gitea-remote:coulomb/fixture-repo.git remote_url: gitea-remote:coulomb/fixture-repo.git
repo_family: ownership
declaration_paths: declaration_paths:
- /tmp/fixture-repo - /tmp/fixture-repo
""".lstrip(), """.lstrip(),

View file

@ -27,6 +27,7 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
"slug": "railiance-fabric", "slug": "railiance-fabric",
"name": "Railiance Fabric", "name": "Railiance Fabric",
"state_hub_repo_id": "2c0de614-e468-4eb6-8157-470649ac8c05", "state_hub_repo_id": "2c0de614-e468-4eb6-8157-470649ac8c05",
"repo_family": "ownership",
}, },
{ {
"slug": "registered-only", "slug": "registered-only",
@ -73,6 +74,7 @@ def test_graph_explorer_manifest_and_payload_validate() -> None:
assert registered_only["data"]["reviewState"] == "candidate" assert registered_only["data"]["reviewState"] == "candidate"
assert registered_only["data"]["unresolved"] is True assert registered_only["data"]["unresolved"] is True
assert nodes_by_id["repo:railiance-fabric"]["data"]["repoFamily"] == "ownership"
assert deployment["data"]["layer"] == "deployment" assert deployment["data"]["layer"] == "deployment"
assert server["data"]["layer"] == "server" assert server["data"]["layer"] == "server"
assert ":" not in server["data"]["label"] assert ":" not in server["data"]["label"]
@ -358,6 +360,7 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
"slug": "railiance-fabric", "slug": "railiance-fabric",
"name": "Railiance Fabric", "name": "Railiance Fabric",
"state_hub_repo_id": "2c0de614-e468-4eb6-8157-470649ac8c05", "state_hub_repo_id": "2c0de614-e468-4eb6-8157-470649ac8c05",
"repo_family": "ownership",
} }
) )
store.upsert_repository({"slug": "registered-only", "name": "Registered Only"}) store.upsert_repository({"slug": "registered-only", "name": "Registered Only"})

View file

@ -35,6 +35,7 @@ def test_registry_accepts_snapshot_and_queries_graph(tmp_path: Path) -> None:
"slug": "railiance-fabric", "slug": "railiance-fabric",
"name": "Railiance Fabric", "name": "Railiance Fabric",
"remote_url": "https://example.invalid/railiance-fabric.git", "remote_url": "https://example.invalid/railiance-fabric.git",
"repo_family": "ownership",
} }
) )
@ -78,6 +79,7 @@ def test_registry_accepts_snapshot_and_queries_graph(tmp_path: Path) -> None:
libraries = store.ingest_cyclonedx("railiance-fabric", _cyclonedx_bom()) libraries = store.ingest_cyclonedx("railiance-fabric", _cyclonedx_bom())
assert snapshot["repo_slug"] == "railiance-fabric" assert snapshot["repo_slug"] == "railiance-fabric"
assert store.get_repository("railiance-fabric")["repo_family"] == "ownership"
assert changed_snapshot["commit"] == "test-commit-2" assert changed_snapshot["commit"] == "test-commit-2"
assert artifact["artifact_type"] == "openapi" assert artifact["artifact_type"] == "openapi"
assert libraries["component_count"] == 2 assert libraries["component_count"] == 2
@ -469,6 +471,7 @@ def test_registry_sync_manifest_registers_multiple_repos(tmp_path: Path) -> None
" name: Railiance Fabric", " name: Railiance Fabric",
f" path: {Path('.').resolve()}", f" path: {Path('.').resolve()}",
" commit: manifest-commit", " commit: manifest-commit",
" repo_family: ownership",
f" sbom: {sbom_path.name}", f" sbom: {sbom_path.name}",
" - slug: missing-repo", " - slug: missing-repo",
" name: Missing Repo", " name: Missing Repo",
@ -491,6 +494,7 @@ def test_registry_sync_manifest_registers_multiple_repos(tmp_path: Path) -> None
repositories = {repo["slug"]: repo for repo in store.list_repositories()} repositories = {repo["slug"]: repo for repo in store.list_repositories()}
assert set(repositories) == {"missing-repo", "railiance-fabric"} assert set(repositories) == {"missing-repo", "railiance-fabric"}
assert repositories["railiance-fabric"]["repo_family"] == "ownership"
assert store.latest_snapshot("railiance-fabric")["commit"] == "manifest-commit" assert store.latest_snapshot("railiance-fabric")["commit"] == "manifest-commit"
assert store.list_snapshots("missing-repo") == [] assert store.list_snapshots("missing-repo") == []
assert store.list_libraries(repo_slug="railiance-fabric")[0]["name"] == "jsonschema" assert store.list_libraries(repo_slug="railiance-fabric")[0]["name"] == "jsonschema"

View file

@ -4,7 +4,7 @@ type: workplan
title: "Rail Rapp Reef Fabric Adaptation" title: "Rail Rapp Reef Fabric Adaptation"
domain: financials domain: financials
repo: railiance-fabric repo: railiance-fabric
status: active status: finished
owner: codex owner: codex
topic_slug: railiance topic_slug: railiance
created: "2026-07-25" created: "2026-07-25"
@ -107,13 +107,11 @@ documented the initial declaration guidance in
```task ```task
id: RAIL-FAB-WP-0025-T04 id: RAIL-FAB-WP-0025-T04
status: wait status: done
priority: medium priority: medium
state_hub_task_id: "dbfd3000-0204-473e-b1b8-eeb44d3fcd06" state_hub_task_id: "dbfd3000-0204-473e-b1b8-eeb44d3fcd06"
``` ```
Blocked on T01-T03.
Define the next implementation slice for: Define the next implementation slice for:
- repo-family-aware registry metadata - repo-family-aware registry metadata
@ -122,3 +120,8 @@ Define the next implementation slice for:
Done when the next repo-local implementation wave can start without reopening Done when the next repo-local implementation wave can start without reopening
the compatibility-first vocabulary decisions above. the compatibility-first vocabulary decisions above.
2026-07-25: Implemented the first repo-family-aware registry metadata path in
the registry store, manifest sync flow, and graph-explorer repository
projection. Remaining relation and State Hub projection gaps are now recorded
explicitly in `docs/rail-rapp-reef-adaptation.md`.