From 19f9fddc35a27fd2e1df08aa31ee67ab9cee79be Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 17 May 2026 19:47:37 +0200 Subject: [PATCH] Establish Railiance Fabric graph model --- .custodian-brief.md | 40 +++ AGENTS.md | 158 +++++++++ INTENT.md | 232 ++++++++++++ README.md | 52 ++- SCOPE.md | 139 ++++++++ catalog/README.md | 11 + catalog/capability-types.yaml | 121 +++++++ catalog/interface-types.yaml | 114 ++++++ docs/adoption-guide.md | 207 +++++++++++ docs/declaration-schema.md | 252 +++++++++++++ docs/discovery-queries.md | 85 +++++ docs/ecosystem-registry-service.md | 200 +++++++++++ docs/first-rollout.md | 54 +++ docs/state-hub-integration.md | 159 +++++++++ docs/type-catalog.md | 68 ++++ docs/validator.md | 40 +++ examples/declarations/README.md | 15 + .../invalid/binding-bad-status.yaml | 15 + .../invalid/capability-bad-lifecycle.yaml | 16 + .../invalid/dependency-bad-environment.yaml | 16 + .../invalid/interface-bad-auth.yaml | 18 + .../invalid/service-missing-id.yaml | 11 + .../valid/binding-flex-auth-openbao.yaml | 23 ++ .../valid/capability-runtime-secrets.yaml | 26 ++ .../dependency-flex-auth-runtime-secrets.yaml | 31 ++ .../valid/interface-openbao-kv-v2.yaml | 29 ++ .../declarations/valid/service-openbao.yaml | 20 ++ fabric/README.md | 20 ++ ...rtifact-store-runtime-secrets-openbao.yaml | 16 + .../flex-auth-iam-profile-key-cape.yaml | 16 + .../flex-auth-runtime-secrets-openbao.yaml | 16 + fabric/bindings/flex-auth-topaz-runtime.yaml | 16 + .../bindings/state-hub-postgresql-cnpg.yaml | 16 + ...fact-store-object-storage-credentials.yaml | 18 + .../artifact-store-object-storage.yaml | 18 + ...flex-auth-api-authorization-decisions.yaml | 19 + ...flex-auth-topaz-authorization-runtime.yaml | 18 + .../key-cape-iam-profile-issuer.yaml | 19 + .../net-kingdom-iam-profile-issuer.yaml | 18 + .../railiance-platform-cnpg-postgresql.yaml | 18 + ...ance-platform-openbao-runtime-secrets.yaml | 18 + .../railiance-platform-valkey-cache.yaml | 18 + .../repo-scoping-scope-generation.yaml | 18 + .../the-custodian-state-hub-coordination.yaml | 18 + ...-store-object-storage-runtime-secrets.yaml | 24 ++ .../flex-auth-api-iam-profile.yaml | 27 ++ .../flex-auth-api-runtime-secrets.yaml | 28 ++ .../flex-auth-api-topaz-runtime.yaml | 28 ++ .../the-custodian-state-hub-postgresql.yaml | 28 ++ .../artifact-store-object-storage-bucket.yaml | 20 ++ .../artifact-store-object-storage-sts.yaml | 20 ++ fabric/interfaces/flex-auth-api-http-api.yaml | 20 ++ .../flex-auth-api-policy-package.yaml | 20 ++ .../interfaces/flex-auth-topaz-http-api.yaml | 20 ++ .../key-cape-iam-profile-http-api.yaml | 20 ++ .../key-cape-iam-profile-oidc-discovery.yaml | 20 ++ ...et-kingdom-iam-profile-oidc-discovery.yaml | 20 ++ ...nce-platform-cnpg-database-connection.yaml | 20 ++ ...iance-platform-openbao-database-roles.yaml | 21 ++ .../railiance-platform-openbao-kv-v2.yaml | 23 ++ ...e-platform-valkey-database-connection.yaml | 20 ++ .../repo-scoping-scope-generator-cli.yaml | 20 ++ .../the-custodian-state-hub-http-api.yaml | 20 ++ .../artifact-store-object-storage.yaml | 19 + fabric/services/flex-auth-api.yaml | 18 + fabric/services/flex-auth-topaz.yaml | 17 + fabric/services/key-cape-iam-profile.yaml | 18 + fabric/services/net-kingdom-iam-profile.yaml | 17 + fabric/services/railiance-platform-cnpg.yaml | 17 + .../services/railiance-platform-openbao.yaml | 18 + .../services/railiance-platform-valkey.yaml | 17 + .../repo-scoping-scope-generator.yaml | 17 + fabric/services/the-custodian-state-hub.yaml | 17 + pyproject.toml | 20 ++ railiance_fabric/__init__.py | 5 + railiance_fabric/cli.py | 183 ++++++++++ railiance_fabric/graph.py | 264 ++++++++++++++ railiance_fabric/loader.py | 56 +++ railiance_fabric/model.py | 65 ++++ railiance_fabric/validation.py | 333 ++++++++++++++++++ schemas/binding.schema.yaml | 52 +++ schemas/capability.schema.yaml | 52 +++ schemas/common.schema.yaml | 164 +++++++++ schemas/dependency.schema.yaml | 82 +++++ schemas/interface.schema.yaml | 69 ++++ schemas/service.schema.yaml | 44 +++ schemas/state-hub-export.schema.yaml | 71 ++++ .../RAIL-FAB-WP-0001-ecosystem-graph-model.md | 319 +++++++++++++++++ ...-FAB-WP-0002-ecosystem-registry-service.md | 184 ++++++++++ 89 files changed, 5007 insertions(+), 2 deletions(-) create mode 100644 .custodian-brief.md create mode 100644 AGENTS.md create mode 100644 INTENT.md create mode 100644 SCOPE.md create mode 100644 catalog/README.md create mode 100644 catalog/capability-types.yaml create mode 100644 catalog/interface-types.yaml create mode 100644 docs/adoption-guide.md create mode 100644 docs/declaration-schema.md create mode 100644 docs/discovery-queries.md create mode 100644 docs/ecosystem-registry-service.md create mode 100644 docs/first-rollout.md create mode 100644 docs/state-hub-integration.md create mode 100644 docs/type-catalog.md create mode 100644 docs/validator.md create mode 100644 examples/declarations/README.md create mode 100644 examples/declarations/invalid/binding-bad-status.yaml create mode 100644 examples/declarations/invalid/capability-bad-lifecycle.yaml create mode 100644 examples/declarations/invalid/dependency-bad-environment.yaml create mode 100644 examples/declarations/invalid/interface-bad-auth.yaml create mode 100644 examples/declarations/invalid/service-missing-id.yaml create mode 100644 examples/declarations/valid/binding-flex-auth-openbao.yaml create mode 100644 examples/declarations/valid/capability-runtime-secrets.yaml create mode 100644 examples/declarations/valid/dependency-flex-auth-runtime-secrets.yaml create mode 100644 examples/declarations/valid/interface-openbao-kv-v2.yaml create mode 100644 examples/declarations/valid/service-openbao.yaml create mode 100644 fabric/README.md create mode 100644 fabric/bindings/artifact-store-runtime-secrets-openbao.yaml create mode 100644 fabric/bindings/flex-auth-iam-profile-key-cape.yaml create mode 100644 fabric/bindings/flex-auth-runtime-secrets-openbao.yaml create mode 100644 fabric/bindings/flex-auth-topaz-runtime.yaml create mode 100644 fabric/bindings/state-hub-postgresql-cnpg.yaml create mode 100644 fabric/capabilities/artifact-store-object-storage-credentials.yaml create mode 100644 fabric/capabilities/artifact-store-object-storage.yaml create mode 100644 fabric/capabilities/flex-auth-api-authorization-decisions.yaml create mode 100644 fabric/capabilities/flex-auth-topaz-authorization-runtime.yaml create mode 100644 fabric/capabilities/key-cape-iam-profile-issuer.yaml create mode 100644 fabric/capabilities/net-kingdom-iam-profile-issuer.yaml create mode 100644 fabric/capabilities/railiance-platform-cnpg-postgresql.yaml create mode 100644 fabric/capabilities/railiance-platform-openbao-runtime-secrets.yaml create mode 100644 fabric/capabilities/railiance-platform-valkey-cache.yaml create mode 100644 fabric/capabilities/repo-scoping-scope-generation.yaml create mode 100644 fabric/capabilities/the-custodian-state-hub-coordination.yaml create mode 100644 fabric/dependencies/artifact-store-object-storage-runtime-secrets.yaml create mode 100644 fabric/dependencies/flex-auth-api-iam-profile.yaml create mode 100644 fabric/dependencies/flex-auth-api-runtime-secrets.yaml create mode 100644 fabric/dependencies/flex-auth-api-topaz-runtime.yaml create mode 100644 fabric/dependencies/the-custodian-state-hub-postgresql.yaml create mode 100644 fabric/interfaces/artifact-store-object-storage-bucket.yaml create mode 100644 fabric/interfaces/artifact-store-object-storage-sts.yaml create mode 100644 fabric/interfaces/flex-auth-api-http-api.yaml create mode 100644 fabric/interfaces/flex-auth-api-policy-package.yaml create mode 100644 fabric/interfaces/flex-auth-topaz-http-api.yaml create mode 100644 fabric/interfaces/key-cape-iam-profile-http-api.yaml create mode 100644 fabric/interfaces/key-cape-iam-profile-oidc-discovery.yaml create mode 100644 fabric/interfaces/net-kingdom-iam-profile-oidc-discovery.yaml create mode 100644 fabric/interfaces/railiance-platform-cnpg-database-connection.yaml create mode 100644 fabric/interfaces/railiance-platform-openbao-database-roles.yaml create mode 100644 fabric/interfaces/railiance-platform-openbao-kv-v2.yaml create mode 100644 fabric/interfaces/railiance-platform-valkey-database-connection.yaml create mode 100644 fabric/interfaces/repo-scoping-scope-generator-cli.yaml create mode 100644 fabric/interfaces/the-custodian-state-hub-http-api.yaml create mode 100644 fabric/services/artifact-store-object-storage.yaml create mode 100644 fabric/services/flex-auth-api.yaml create mode 100644 fabric/services/flex-auth-topaz.yaml create mode 100644 fabric/services/key-cape-iam-profile.yaml create mode 100644 fabric/services/net-kingdom-iam-profile.yaml create mode 100644 fabric/services/railiance-platform-cnpg.yaml create mode 100644 fabric/services/railiance-platform-openbao.yaml create mode 100644 fabric/services/railiance-platform-valkey.yaml create mode 100644 fabric/services/repo-scoping-scope-generator.yaml create mode 100644 fabric/services/the-custodian-state-hub.yaml create mode 100644 pyproject.toml create mode 100644 railiance_fabric/__init__.py create mode 100644 railiance_fabric/cli.py create mode 100644 railiance_fabric/graph.py create mode 100644 railiance_fabric/loader.py create mode 100644 railiance_fabric/model.py create mode 100644 railiance_fabric/validation.py create mode 100644 schemas/binding.schema.yaml create mode 100644 schemas/capability.schema.yaml create mode 100644 schemas/common.schema.yaml create mode 100644 schemas/dependency.schema.yaml create mode 100644 schemas/interface.schema.yaml create mode 100644 schemas/service.schema.yaml create mode 100644 schemas/state-hub-export.schema.yaml create mode 100644 workplans/RAIL-FAB-WP-0001-ecosystem-graph-model.md create mode 100644 workplans/RAIL-FAB-WP-0002-ecosystem-registry-service.md diff --git a/.custodian-brief.md b/.custodian-brief.md new file mode 100644 index 0000000..a6c0423 --- /dev/null +++ b/.custodian-brief.md @@ -0,0 +1,40 @@ +# Railiance Fabric Brief + +Domain: railiance +Repo slug: railiance-fabric +State Hub topic ID: ca369340-a64e-442e-98f1-a4fa7dc74a38 +State Hub workstream ID: bd190990-8e68-49a3-9ce4-0ba89103ea54 + +## Purpose + +Railiance Fabric defines the repo-owned declaration model for the Railiance +ecosystem graph: services, capabilities, interfaces, dependencies, bindings, +validation, discovery queries, and State Hub export contracts. + +## Current Work + +- `RAIL-FAB-WP-0001` is active and establishes the first ecosystem graph model. +- `T01` is done: `INTENT.md` defines the vocabulary and source-of-truth + boundary. +- `T02` is done: `docs/declaration-schema.md`, `schemas/`, and + `examples/declarations/` define the first declaration schema baseline. +- `T03` is done: `catalog/` and `docs/type-catalog.md` define the first + capability/interface type catalog. +- `T04` is done: `fabric/` contains seed declarations for the first Railiance + ecosystem provider/consumer graph. +- `T05` is done: `railiance-fabric validate` loads and validates schema, + catalog, reference, provider, source-link, and cycle checks. +- `T06` is done: discovery queries and JSON/Mermaid exports are available from + the CLI. +- `T07` is done: `docs/state-hub-integration.md` defines the graph export and + proposed hub read-model ingestion path. +- `T08` is done: `docs/adoption-guide.md` and `docs/first-rollout.md` define + how other repos should adopt and promote the seed declarations. +- All tasks in `RAIL-FAB-WP-0001` are done. + +## State Hub + +- Local API: `http://127.0.0.1:8000` +- Remote tunnel: `http://127.0.0.1:18000` +- After changing workplan files, sync from `~/the-custodian/state-hub` with: + `make fix-consistency REPO=railiance-fabric` diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..458f988 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,158 @@ +# railiance-fabric — Agent Instructions + +## Repo Identity + +**Purpose:** Railiance Fabric defines the repo-owned declaration model, validation tooling, graph queries, and State Hub export contract for the Railiance ecosystem graph. + +**Domain:** railiance +**Repo slug:** railiance-fabric +**Topic ID:** `ca369340-a64e-442e-98f1-a4fa7dc74a38` +**Workplan prefix:** `RAIL-FAB-WP-` + +--- + +## State Hub Integration + +The Custodian State Hub tracks work across all domains. Interact via HTTP REST — +there is no MCP server for Codex agents. + +| Context | URL | +|---------|-----| +| Local workstation | `http://127.0.0.1:8000` | +| Remote via tunnel | `http://127.0.0.1:18000` | + +### Orient at session start + +```bash +# Offline brief — works without hub connection +cat .custodian-brief.md + +# Active workstreams for this domain +curl -s "http://127.0.0.1:8000/workstreams/?topic_id=ca369340-a64e-442e-98f1-a4fa7dc74a38&status=active" \ + | python3 -m json.tool + +# Check inbox +curl -s "http://127.0.0.1:8000/messages/?to_agent=railiance-fabric&unread_only=true" \ + | python3 -m json.tool +``` + +Mark a message read: +```bash +curl -s -X PATCH "http://127.0.0.1:8000/messages//read" \ + -H "Content-Type: application/json" -d '{}' +``` + +### Log progress (required at session close) + +```bash +curl -s -X POST http://127.0.0.1:8000/progress/ \ + -H "Content-Type: application/json" \ + -d '{ + "summary": "what was done", + "event_type": "note", + "author": "codex", + "workstream_id": "", + "task_id": "" + }' +``` + +Omit `workstream_id` / `task_id` when not applicable. + +### Update task status + +```bash +curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ + -H "Content-Type: application/json" \ + -d '{"status": "in_progress"}' +# values: todo | in_progress | done | blocked +``` + +### Flag a task for human review + +```bash +curl -s -X PATCH "http://127.0.0.1:8000/tasks/" \ + -H "Content-Type: application/json" \ + -d '{"needs_human": true, "intervention_note": "reason"}' +``` + +--- + +## Session Protocol + +**Start:** +1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe) +2. Check inbox: `GET /messages/?to_agent=railiance-fabric&unread_only=true`; mark read +3. Scan workplans: `ls workplans/` — note `status: active` files and open tasks +4. Check blocked tasks: `GET /tasks/?needs_human=true` + +**During work:** +- Update task statuses in workplan files as tasks progress +- Record significant decisions via `POST /decisions/` + +**Close:** +1. Update workplan file task statuses to reflect progress +2. Log: `POST /progress/` with a summary of what changed +3. Note for the custodian operator: after workplan file changes, run from + `~/the-custodian/state-hub`: + ```bash + make fix-consistency REPO=railiance-fabric + ``` + This syncs task status from files into the hub DB. + +--- + +## Workplan Convention (ADR-001) + +Work items originate as files in this repo — not in the hub. The hub is a +read/cache/index layer that rebuilds from files. + +**File location:** `workplans/RAIL-FAB-WP-NNNN-.md` + +**Archived location:** completed workplans may move to +`workplans/archived/YYMMDD-RAIL-FAB-WP-NNNN-.md`. The `YYMMDD` prefix is +the completion/archive date; the frontmatter `id` does not change. + +**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use +`workplans/ADHOC-YYYY-MM-DD.md` with task ids `ADHOC-YYYY-MM-DD-T01`, etc. Use +this only for low-risk work completed directly; create a normal workplan for +anything needing analysis, design, approval, dependencies, or multiple phases. + +**Frontmatter:** + +```yaml +--- +id: RAIL-FAB-WP-NNNN +type: workplan +title: "..." +domain: railiance +repo: railiance-fabric +status: active | done +owner: codex +topic_slug: ... +created: "YYYY-MM-DD" +updated: "YYYY-MM-DD" +state_hub_workstream_id: "" # written by fix-consistency — do not edit +--- +``` + +**Task block format** (one per `##` section): + +``` +## Task Title + +` ` `task +id: RAIL-FAB-WP-NNNN-T01 +status: todo | in_progress | done | blocked +priority: high | medium | low +state_hub_task_id: "" # written by fix-consistency — do not edit +` ` ` + +Task description text. +``` + +Status progression: `todo` → `in_progress` → `done` (or `blocked`) + +To create a new workplan: +1. Write the file following the format above +2. Notify the custodian operator to run `make fix-consistency REPO=railiance-fabric` + (or send a message to the hub agent via `POST /messages/`) diff --git a/INTENT.md b/INTENT.md new file mode 100644 index 0000000..ea906d8 --- /dev/null +++ b/INTENT.md @@ -0,0 +1,232 @@ +# Railiance Fabric Intent + +Date: 2026-05-17 + +## Intent + +Railiance Fabric exists to make the Railiance ecosystem understandable, +discoverable, and evolvable as services begin to depend on one another. + +It models the living fabric of repositories, services, capabilities, +interfaces, and dependencies so humans and agents can answer questions like: + +- Which service provides runtime secrets? +- Which repos consume the NetKingdom IAM Profile? +- What breaks if the flex-auth decision envelope changes? +- Which workloads require OpenBao KV, dynamic database credentials, or + object-storage credential vending? +- Which dependencies are declared, missing, stale, or boundary-violating? + +The core idea is simple: + +```text +repo -> service -> capability -> interface -> dependency +``` + +Repos remain the source of truth for what they provide and require. Railiance +Fabric gives those declarations a shared schema, validation model, graph view, +and discovery surface. + +## Why This Exists + +Railiance is entering the phase where platform services, identity services, +application workloads, automation, policy engines, storage, and observability +will interact continuously. + +Without an explicit ecosystem graph, those interactions become folklore: +implicit dependencies, stale mental maps, fragile deployment order, and unclear +ownership when interfaces change. + +Railiance Fabric turns that implicit web into a reviewable graph: + +- capabilities are discoverable by name and semantics +- interfaces are typed and versioned +- consumers declare their requirements +- providers declare what they actually offer +- State Hub can ingest the graph as a read model +- agents can reason about blast radius, missing providers, and safe sequencing + +## Responsibility Boundary + +### Repositories Own Declarations + +Each repo owns file-backed declarations for its provided capabilities, +consumed capabilities, services, and interface contracts. + +Examples: + +```text +fabric/capabilities/*.yaml +fabric/dependencies/*.yaml +fabric/interfaces/*.yaml +fabric/services/*.yaml +``` + +These files are reviewable, versioned with the repo, and changed through normal +pull-request or workplan flow. + +### Railiance Fabric Owns The Graph Model + +Railiance Fabric owns: + +- declaration schemas +- validation rules +- graph construction +- local inspection tools +- provider/consumer matching +- compatibility and drift checks +- example declarations for core Railiance services +- export formats for State Hub, docs, and dashboards + +### State Hub Owns The Read Model + +State Hub should ingest the ecosystem graph and expose it for coordination, +but it should not become the primary authoring surface for capability and +dependency declarations. + +This keeps ADR-001 intact: formal work and declarations originate in repos; +the hub reads, visualizes, and coordinates. + +## First-Class Concepts + +### Repository + +A source-controlled project with ownership, workplans, implementation, and +local declarations. + +### Service + +A deployable or callable unit produced by a repository. A repo may produce zero +or more services. + +Examples: OpenBao, key-cape, flex-auth API, Topaz deployment, artifact-store. + +### Capability + +A stable semantic ability that consumers can depend on without hard-coding the +current implementation. + +Examples: + +- runtime secrets +- IAM Profile issuer +- authorization decision service +- PostgreSQL database service +- object-storage credential vending +- scope generation + +### Interface + +The concrete contract through which a capability is consumed. + +Examples: + +- HTTP API +- OIDC discovery +- Kubernetes Secret +- Kubernetes CRD +- Helm release +- CLI +- database connection +- object-storage bucket +- event stream +- policy package +- OpenBao KV v2 mount +- OpenBao database dynamic credential role + +### Dependency + +A consumer's declared requirement for a capability or interface, including +version, environment, auth, data classification, criticality, and fallback +expectations. + +### Binding + +A resolved edge between a consumer dependency and a provider capability. +Bindings may be exact, compatible, degraded, missing, or disputed. + +## Design Principles + +- Source of truth lives in repos. +- Capabilities are stable; implementations may move. +- Interfaces are typed, versioned, and testable. +- Dependencies are explicit requirements, not accidental imports. +- Discovery is graph search, not tribal memory. +- Validation should catch missing providers before deployment time. +- Compatibility should be machine-checkable where possible. +- Human-readable files matter; agents and humans must both be able to inspect + declarations without a running service. +- The model must support partial adoption. A repo can begin with one declared + capability or dependency and mature over time. +- The graph should reveal boundary violations without pretending to own every + domain's decisions. + +## Strategic Role + +Railiance Fabric sits between repository scoping, State Hub, and the Railiance +deployment stack. + +```text +repo-local declarations + | + v +Railiance Fabric schema and graph tools + | + +-- State Hub ingestion and coordination + +-- documentation and topology maps + +-- agent planning and blast-radius analysis + +-- deployment readiness checks +``` + +It complements: + +- `repo-scoping`, which explains what a repo is useful for. +- `the-custodian/state-hub`, which coordinates domains, workstreams, tasks, and + progress. +- `railiance-platform`, which deploys shared S3 services such as OpenBao, + PostgreSQL, Valkey, and object storage. +- `net-kingdom`, which owns identity, credential, and security architecture. +- `flex-auth`, which owns authorization policy and decision semantics. + +## Non-Goals + +Railiance Fabric is not: + +- a deployment orchestrator +- a replacement for State Hub +- a replacement for SCOPE.md +- a service mesh +- a CMDB that must manually mirror everything +- an authorization engine +- a secret manager +- a package registry + +It may inform those systems, but its job is the ecosystem graph and declaration +model. + +## Early Questions + +- What is the smallest declaration schema that is useful without becoming + ceremony? +- Which capability and interface types must exist on day one? +- How should provider/consumer matching handle environment, version, auth, + tenant, and data-class constraints? +- Which graph checks are advisory, and which should block deployment? +- How does State Hub ingest the graph without becoming the authoring source? +- How do we represent capabilities that are planned but not deployed yet? + +## Maturity Target + +A mature Railiance Fabric should let a human or agent ask: + +```text +Show all consumers of OpenBao. +Show missing providers for production Railiance. +Show every service that depends on NetKingdom identity claims. +Show all interfaces crossing from S3 platform services into S5 applications. +Show blast radius for changing flex-auth decision envelope v1. +Show runtime readiness for tenant:coulomb onboarding. +``` + +and receive source-linked, repo-owned answers. + diff --git a/README.md b/README.md index fcd7b8f..dfd8556 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,51 @@ -# repo-seed +# Railiance Fabric -A git repository template to bootstrap coulomb projects from. \ No newline at end of file +Railiance Fabric defines the repo-owned declaration model for the Railiance +ecosystem graph. + +It will hold schemas, seed declarations, validation tools, graph queries, and +State Hub export contracts for services, capabilities, interfaces, +dependencies, and bindings across Railiance repositories. + +## Validate Declarations + +From a checkout with the Python dependencies installed: + +```bash +railiance-fabric validate . +``` + +During early bootstrapping, the local module entry point works too: + +```bash +PYTHONPATH=. python -m railiance_fabric.cli validate . +``` + +The validator loads `fabric/` declarations, checks schema conformance, verifies +catalog type names, catches missing references/providers, checks active +production dependency source links, and warns about dependency cycles. + +## Query The Graph + +```bash +railiance-fabric providers runtime-secrets +railiance-fabric consumers railiance-platform.openbao.kv-v2 +railiance-fabric dependency-path flex-auth.api +railiance-fabric unresolved +railiance-fabric blast-radius openbao-kv-v2-mount +railiance-fabric export --format json +railiance-fabric export --format mermaid +``` + +See `docs/discovery-queries.md` for command details. + +## Adopt In Another Repo + +See `docs/adoption-guide.md` for the declaration workflow and +`docs/first-rollout.md` for the initial Railiance repo rollout. + +## Next: Ecosystem Registry Service + +See `docs/ecosystem-registry-service.md` for the standards comparison and +service direction for registering repos and interacting with the combined +ecosystem model. diff --git a/SCOPE.md b/SCOPE.md new file mode 100644 index 0000000..636c3ee --- /dev/null +++ b/SCOPE.md @@ -0,0 +1,139 @@ +# SCOPE + +> This file helps you quickly understand what this repository is about, +> when it is relevant, and when it is not. +> It is intentionally lightweight and may be incomplete. + +--- + +## One-liner + +Defines the Railiance ecosystem graph model so repos can declare services, +capabilities, interfaces, dependencies, and bindings in source-controlled files. + +--- + +## Core Idea + +Railiance Fabric turns implicit cross-repo dependencies into a reviewable graph. +Participating repos remain the source of truth for what they provide and +consume; this repo owns the shared schema, validation rules, graph construction, +query tooling, seed examples, and export format that State Hub can ingest as a +read model. + +--- + +## In Scope + +- YAML declaration schemas for services, capabilities, interfaces, + dependencies, and binding assertions. +- Capability and interface type catalogs for the Railiance ecosystem. +- Seed declarations for core Railiance providers and consumers. +- Local graph loading, validation, discovery queries, and export tooling. +- State Hub ingestion contract for graph exports. +- Adoption guidance for adding declarations to other repos. + +--- + +## Out of Scope + +- Deployment orchestration or GitOps ownership. +- Replacing State Hub workstreams, tasks, decisions, or progress events. +- Replacing repo-scoping, SCOPE.md, ADRs, service meshes, secret managers, or + authorization engines. +- Making State Hub the authoring surface for capability declarations. +- Runtime traffic discovery that bypasses repo-owned declarations. + +--- + +## Relevant When + +- You need to declare what a Railiance repo provides or consumes. +- You need to ask which services depend on a capability or interface. +- You need to validate missing providers, stale interfaces, or compatibility + issues before sequencing work. +- You need a graph export that State Hub can display without owning the source + declarations. + +--- + +## Not Relevant When + +- You are deploying infrastructure rather than modeling its ecosystem contract. +- You need to manage State Hub tasks, decisions, messages, or progress events. +- You need service-specific implementation details owned by another repo. +- You need an authorization, secret-management, or package-registry runtime. + +--- + +## Current State + +- Status: active planning +- Implementation: intent and first workplan present +- Stability: evolving +- Usage: internal Railiance ecosystem modeling + +The first workplan is `RAIL-FAB-WP-0001`, which establishes vocabulary, schema, +seed examples, validator/query tooling, and State Hub integration. + +--- + +## How It Fits + +- Upstream dependencies: repo-owned declarations in participating Railiance + repos. +- Downstream consumers: State Hub read model, documentation, dashboards, and + agent planning flows. +- Often used with: `repo-scoping`, `the-custodian/state-hub`, + `railiance-platform`, `net-kingdom`, `flex-auth`, and `artifact-store`. + +--- + +## Terminology + +- Preferred terms: repository, service, capability, interface, dependency, + binding. +- Also known as: ecosystem graph, capability graph, dependency graph. +- Potentially confusing terms: State Hub is the read/cache/index layer here, + not the authoring source for declarations. + +--- + +## Related / Overlapping Repositories + +- `repo-scoping` — explains what a repo is useful for; Railiance Fabric models + what repos provide and consume. +- `the-custodian/state-hub` — coordinates domains, workstreams, tasks, and + progress; it should ingest Fabric graph exports as a read model. +- `railiance-platform` — deploys shared platform services that should become + graph provider nodes. +- `net-kingdom` — owns identity, credential, and security architecture that + appears in capability/interface declarations. +- `flex-auth` — owns authorization policy and decision semantics that should be + represented as graph capabilities and interfaces. + +--- + +## Getting Oriented + +- Start with: `INTENT.md` +- Key files / directories: `workplans/` +- Entry points: `workplans/RAIL-FAB-WP-0001-ecosystem-graph-model.md` + +--- + +## Provided Capabilities + +```capability +type: tooling +title: Railiance ecosystem graph model +description: Shared declaration schema, validation model, query tooling, and State Hub export contract for Railiance service/capability/dependency graphs. +keywords: [railiance, graph, capabilities, dependencies, interfaces, state-hub] +``` + +--- + +## Notes + +Repos own declarations. Railiance Fabric owns graph semantics and validation. +State Hub owns coordination and display of ingested read models. diff --git a/catalog/README.md b/catalog/README.md new file mode 100644 index 0000000..e278106 --- /dev/null +++ b/catalog/README.md @@ -0,0 +1,11 @@ +# Type Catalog + +This directory contains the first Railiance Fabric type catalog. + +- `capability-types.yaml` defines stable semantic capabilities. +- `interface-types.yaml` defines concrete integration surfaces. + +Declaration schemas keep `capability_type` and `interface_type` as strings so +the core document shape stays decoupled from catalog evolution. The validator +planned in `RAIL-FAB-WP-0001-T05` should load these catalogs and reject unknown +types unless an explicit experimental override is added later. diff --git a/catalog/capability-types.yaml b/catalog/capability-types.yaml new file mode 100644 index 0000000..280f233 --- /dev/null +++ b/catalog/capability-types.yaml @@ -0,0 +1,121 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: CapabilityTypeCatalog +metadata: + id: railiance-fabric.capability-types + name: Railiance capability type catalog + owner: railiance-fabric + repo: railiance-fabric + domain: railiance +spec: + version: v1alpha1 + types: + - id: runtime-secrets + name: Runtime secrets + lifecycle: active + description: Stores or vends runtime secrets needed by workloads after deployment. + default_criticality: critical + default_data_classification: secret + expected_interface_types: + - openbao-kv-v2-mount + - kubernetes-secret + tags: [security, platform, secrets] + + - id: iam-profile-issuer + name: IAM Profile issuer + lifecycle: active + description: Issues or serves identity profile claims used by Railiance services. + default_criticality: critical + default_data_classification: restricted + expected_interface_types: + - oidc-discovery + - http-api + tags: [identity, security] + + - id: authorization-decision-service + name: Authorization decision service + lifecycle: active + description: Evaluates authorization policy and returns allow/deny decisions with context. + default_criticality: critical + default_data_classification: restricted + expected_interface_types: + - http-api + - policy-package + tags: [authorization, policy, security] + + - id: postgresql-database-service + name: PostgreSQL database service + lifecycle: active + description: Provides PostgreSQL databases, roles, and connection endpoints for workloads. + default_criticality: high + default_data_classification: confidential + expected_interface_types: + - database-connection + - openbao-dynamic-credential-role + tags: [database, platform] + + - id: redis-compatible-cache + name: Redis-compatible cache + lifecycle: active + description: Provides Redis protocol compatible caching or ephemeral data storage. + default_criticality: medium + default_data_classification: internal + expected_interface_types: + - database-connection + - kubernetes-secret + tags: [cache, platform] + + - id: object-storage + name: Object storage + lifecycle: planned + description: Provides bucket-style durable object storage for Railiance workloads. + default_criticality: high + default_data_classification: confidential + expected_interface_types: + - object-storage-bucket + - http-api + tags: [storage, platform] + + - id: object-storage-credential-vending + name: Object-storage credential vending + lifecycle: planned + description: Issues scoped temporary credentials for object-storage access. + default_criticality: high + default_data_classification: secret + expected_interface_types: + - http-api + - openbao-dynamic-credential-role + - sts-token + tags: [storage, credentials, security] + + - id: audit-event-sink + name: Audit/event sink + lifecycle: planned + description: Accepts audit, operational, or domain events for durable recording or routing. + default_criticality: high + default_data_classification: confidential + expected_interface_types: + - event-stream + - http-api + tags: [events, audit, observability] + + - id: scope-generation + name: Scope generation + lifecycle: active + description: Produces repo or project scope descriptions used by humans and agents. + default_criticality: medium + default_data_classification: internal + expected_interface_types: + - cli + - http-api + tags: [planning, agents, documentation] + + - id: coordination-read-model + name: Coordination read model + lifecycle: active + description: Exposes coordination state for repos, workstreams, tasks, decisions, and progress. + default_criticality: high + default_data_classification: internal + expected_interface_types: + - http-api + - event-stream + tags: [coordination, state-hub, planning] diff --git a/catalog/interface-types.yaml b/catalog/interface-types.yaml new file mode 100644 index 0000000..a446d50 --- /dev/null +++ b/catalog/interface-types.yaml @@ -0,0 +1,114 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceTypeCatalog +metadata: + id: railiance-fabric.interface-types + name: Railiance interface type catalog + owner: railiance-fabric + repo: railiance-fabric + domain: railiance +spec: + version: v1alpha1 + types: + - id: http-api + name: HTTP API + lifecycle: active + description: Request/response HTTP interface, usually JSON over HTTPS. + category: api + typical_auth_methods: [none, oidc, jwt, mtls, api_key] + versioning: path, header, media-type, or documented semantic version. + + - id: oidc-discovery + name: OIDC discovery + lifecycle: active + description: OpenID Connect discovery metadata and JWKS endpoints. + category: identity + typical_auth_methods: [none] + versioning: issuer URL and advertised metadata. + + - id: kubernetes-secret + name: Kubernetes Secret + lifecycle: active + description: Kubernetes Secret object consumed by workloads in a namespace. + category: kubernetes + typical_auth_methods: [kubernetes_service_account] + versioning: object name, key schema, and owner annotations. + + - id: kubernetes-crd + name: Kubernetes CRD + lifecycle: active + description: Kubernetes custom resource definition and versioned resource schema. + category: kubernetes + typical_auth_methods: [kubernetes_service_account] + versioning: group, version, and kind. + + - id: helm-release + name: Helm release + lifecycle: active + description: Helm chart/release interface used to install or configure a service. + category: deployment + typical_auth_methods: [kubernetes_service_account] + versioning: chart version and values schema. + + - id: cli + name: CLI + lifecycle: active + description: Command-line interface consumed by humans, agents, or automation. + category: tooling + typical_auth_methods: [none, oidc, api_key, unknown] + versioning: command version and documented flags. + + - id: database-connection + name: Database connection + lifecycle: active + description: Network database endpoint plus credentials and connection parameters. + category: data + typical_auth_methods: [database_role, static_secret, openbao_token] + versioning: engine version, connection contract, and migration compatibility. + + - id: object-storage-bucket + name: Object-storage bucket + lifecycle: planned + description: Bucket, prefix, policy, and endpoint contract for object storage. + category: storage + typical_auth_methods: [sts_token, static_secret, openbao_token] + versioning: bucket policy version and object layout contract. + + - id: event-stream + name: Event stream + lifecycle: planned + description: Pub/sub or streaming interface for audit, operational, or domain events. + category: events + typical_auth_methods: [jwt, mtls, api_key, unknown] + versioning: subject/topic names and event envelope schema. + + - id: policy-package + name: Policy package + lifecycle: active + description: Versioned policy bundle consumed by an authorization runtime. + category: policy + typical_auth_methods: [none, oidc, jwt] + versioning: package version and policy input/output schema. + + - id: openbao-kv-v2-mount + name: OpenBao KV v2 mount + lifecycle: active + description: OpenBao KV v2 mount path and secret layout contract. + category: secrets + typical_auth_methods: [kubernetes_service_account, openbao_token] + versioning: mount path, key layout, and policy version. + + - id: openbao-dynamic-credential-role + name: OpenBao dynamic credential role + lifecycle: active + description: OpenBao role that issues dynamic credentials for another service. + category: credentials + typical_auth_methods: [kubernetes_service_account, openbao_token] + versioning: role name, policy, lease semantics, and backend version. + + - id: sts-token + name: STS token + lifecycle: planned + description: Temporary scoped credential issued for object storage or similar services. + category: credentials + typical_auth_methods: [oidc, jwt, mtls] + versioning: token claim schema, audience, and lease semantics. diff --git a/docs/adoption-guide.md b/docs/adoption-guide.md new file mode 100644 index 0000000..fa69868 --- /dev/null +++ b/docs/adoption-guide.md @@ -0,0 +1,207 @@ +# Adoption Guide + +This guide shows another repo how to adopt Railiance Fabric declarations without +reading Railiance Fabric source code. + +## 1. Add The Directory Layout + +Create the declaration directories in your repo: + +```text +fabric/ + services/ + capabilities/ + interfaces/ + dependencies/ + bindings/ +``` + +Start with only the files you need. A repo can adopt Fabric with one service +and one capability, or with one dependency on a capability provided elsewhere. + +## 2. Declare A Service + +Create `fabric/services/-.yaml`: + +```yaml +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: your-repo.your-service + name: Your Service + owner: your-repo + repo: your-repo + domain: railiance + source_links: + - label: Service README + path: README.md +spec: + lifecycle: active + environments: [dev, staging, prod] + description: What this service does. + service_type: app-service + provides_capabilities: [] + exposes_interfaces: [] +``` + +Use lower-case dotted IDs. Prefer IDs that begin with the owning repo slug. + +## 3. Declare A Provided Capability + +Create `fabric/capabilities/-.yaml`: + +```yaml +apiVersion: railiance.fabric/v1alpha1 +kind: CapabilityDeclaration +metadata: + id: your-repo.your-service.runtime-secrets + name: Runtime secrets + owner: your-repo + repo: your-repo + domain: railiance + source_links: + - label: Capability docs + path: docs/runtime-secrets.md +spec: + lifecycle: active + environments: [dev, staging, prod] + description: What this capability provides. + capability_type: runtime-secrets + service_id: your-repo.your-service + interface_ids: + - your-repo.your-service.kv-v2 + criticality: high + data_classification: secret +``` + +Pick `capability_type` from `catalog/capability-types.yaml`. + +## 4. Declare An Interface + +Create `fabric/interfaces/-.yaml`: + +```yaml +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: your-repo.your-service.http-api + name: Your Service HTTP API + owner: your-repo + repo: your-repo + domain: railiance + source_links: + - label: API docs + path: docs/api.md +spec: + lifecycle: active + environments: [dev, staging, prod] + description: How consumers call this interface. + interface_type: http-api + version: v1 + service_id: your-repo.your-service + capability_ids: + - your-repo.your-service.some-capability + auth: + method: oidc + data_classification: internal +``` + +Pick `interface_type` from `catalog/interface-types.yaml`. + +## 5. Declare A Dependency + +Create `fabric/dependencies/-.yaml`: + +```yaml +apiVersion: railiance.fabric/v1alpha1 +kind: DependencyDeclaration +metadata: + id: your-repo.your-service.needs-runtime-secrets + name: Runtime secrets dependency + owner: your-repo + repo: your-repo + domain: railiance + source_links: + - label: Deployment values + path: deploy/values.yaml +spec: + lifecycle: active + environments: [dev, staging, prod] + consumer_service_id: your-repo.your-service + requires: + capability_type: runtime-secrets + interface: + type: openbao-kv-v2-mount + version_constraint: ">=v1 . +.. +.. +.. +``` + +## Shared Fields + +| Field | Meaning | +|-------|---------| +| `apiVersion` | Schema API version. Currently `railiance.fabric/v1alpha1`. | +| `kind` | Declaration kind: service, capability, interface, dependency, or binding assertion. | +| `metadata.id` | Stable graph identifier used for references and bindings. | +| `metadata.name` | Human-readable display name. | +| `metadata.owner` | Owning team, repo, or domain owner. | +| `metadata.repo` | Repo slug that owns the declaration. | +| `metadata.domain` | Domain slug, such as `railiance` or `custodian`. | +| `metadata.source_links` | Optional source pointers to docs, code, manifests, ADRs, or workplans. | +| `spec.lifecycle` | `planned`, `active`, `deprecated`, or `retired`. | +| `spec.environments` | One or more of `dev`, `staging`, `prod`, or `all`. | + +## Declaration Kinds + +### ServiceDeclaration + +A deployable or callable unit produced by a repo. + +Required type-specific fields: + +- `spec.description` + +Optional relationship fields: + +- `spec.service_type` +- `spec.provides_capabilities` +- `spec.exposes_interfaces` + +Schema: `schemas/service.schema.yaml` + +### CapabilityDeclaration + +A stable semantic ability that consumers depend on. + +Required type-specific fields: + +- `spec.description` +- `spec.capability_type` +- `spec.service_id` +- `spec.criticality` +- `spec.data_classification` + +Optional relationship fields: + +- `spec.interface_ids` +- `spec.compatibility` + +Schema: `schemas/capability.schema.yaml` + +`spec.capability_type` should match a type in +`catalog/capability-types.yaml`. Unknown types are allowed by the document +schema but should fail graph validation. + +### InterfaceDeclaration + +A concrete integration surface through which a capability is consumed. + +Required type-specific fields: + +- `spec.description` +- `spec.interface_type` +- `spec.version` +- `spec.service_id` +- `spec.auth.method` +- `spec.data_classification` + +Optional relationship fields: + +- `spec.capability_ids` +- `spec.endpoint` +- `spec.auth.audience` +- `spec.auth.scopes` +- `spec.compatibility` + +Schema: `schemas/interface.schema.yaml` + +`spec.interface_type` should match a type in `catalog/interface-types.yaml`. +Unknown types are allowed by the document schema but should fail graph +validation. + +### DependencyDeclaration + +A consumer's declared requirement for a capability or interface. + +Required type-specific fields: + +- `spec.consumer_service_id` +- `spec.requires.capability_type` +- `spec.criticality` +- `spec.data_classification` + +Optional constraint fields: + +- `spec.requires.capability_id` +- `spec.interface.type` +- `spec.interface.version_constraint` +- `spec.auth.method` +- `spec.fallback` +- `spec.compatibility` + +Schema: `schemas/dependency.schema.yaml` + +`spec.requires.capability_type` and `spec.interface.type` should match the +type catalogs. Unknown types are allowed by the document schema but should fail +graph validation. + +### BindingAssertion + +A source-controlled assertion that resolves a dependency to a provider +capability and, optionally, a provider interface. Most bindings should be +computed by the graph loader; binding assertions are for overrides, disputes, +or planned relationships that need an explicit record. + +Required type-specific fields: + +- `spec.dependency_id` +- `spec.provider_capability_id` +- `spec.status` +- `spec.rationale` + +Optional relationship fields: + +- `spec.provider_interface_id` +- `spec.compatibility` + +Schema: `schemas/binding.schema.yaml` + +## Shared Value Sets + +### Lifecycle + +```text +planned, active, deprecated, retired +``` + +### Environment + +```text +dev, staging, prod, all +``` + +Use `all` only when the declaration truly applies across every environment. + +### Data Classification + +```text +public, internal, confidential, restricted, secret +``` + +### Criticality + +```text +low, medium, high, critical +``` + +### Auth Method + +```text +none, oidc, jwt, mtls, kubernetes_service_account, openbao_token, +static_secret, database_role, sts_token, api_key, unknown +``` + +`unknown` is allowed for discovery-stage declarations but should not remain on +active production dependencies. + +## Compatibility + +The optional `compatibility` object records machine-checkable or human-reviewed +constraints: + +```yaml +compatibility: + version: "v1" + requires: + - "decision-envelope >=1.0 <2.0" + compatible_with: + - "flex-auth.decision-api.v1" + breaks: + - "decision-envelope v0" + notes: "Envelope v1 is required for tenant-scoped decisions." +``` + +T05 will decide which compatibility fields are advisory and which should fail +validation. + +## Source Links + +Use `metadata.source_links` when a declaration is based on a concrete source: + +```yaml +source_links: + - label: OpenBao Helm values + path: charts/openbao/values.yaml + - label: Runtime secrets workplan + url: https://example.invalid/workplans/openbao-runtime-secrets +``` + +At least one source link is recommended for `active` declarations. T05 will +make source-link requirements stricter for active production dependencies. diff --git a/docs/discovery-queries.md b/docs/discovery-queries.md new file mode 100644 index 0000000..32cbc28 --- /dev/null +++ b/docs/discovery-queries.md @@ -0,0 +1,85 @@ +# Discovery Queries And Exports + +Railiance Fabric includes a first CLI surface for inspecting local declaration +graphs. + +All commands accept a repo root, `fabric/` directory, or declaration files. When +paths are omitted, commands read `./fabric`. + +## Providers + +List providers for a capability type or capability id: + +```bash +railiance-fabric providers runtime-secrets +railiance-fabric providers railiance-platform.openbao.runtime-secrets +``` + +Output columns: + +```text +provider_id service_id lifecycle environments interfaces +``` + +## Consumers + +List consumers of a capability type/id or interface type/id: + +```bash +railiance-fabric consumers runtime-secrets +railiance-fabric consumers railiance-platform.openbao.kv-v2 +``` + +Output columns: + +```text +consumer_service_id dependency_id requires provider_capability_id provider_interface_id status +``` + +## Dependency Path + +Show dependency paths for a service: + +```bash +railiance-fabric dependency-path flex-auth.api +``` + +This walks declared dependencies and binding assertions recursively through +provider services. + +## Unresolved Dependencies + +Show dependencies with no matching provider or a `missing`/`disputed` binding: + +```bash +railiance-fabric unresolved +``` + +## Blast Radius + +Show consumers affected by an interface type or interface id: + +```bash +railiance-fabric blast-radius openbao-kv-v2-mount +railiance-fabric blast-radius railiance-platform.openbao.kv-v2 +``` + +## Exports + +Export the graph as JSON: + +```bash +railiance-fabric export --format json +``` + +Export the graph as Mermaid: + +```bash +railiance-fabric export --format mermaid +``` + +The JSON export has two top-level arrays: + +- `nodes`: service, capability, interface, dependency, and binding nodes +- `edges`: graph relationships such as `provides`, `exposes`, + `available_via`, `consumes`, `binds:`, and `uses_interface` diff --git a/docs/ecosystem-registry-service.md b/docs/ecosystem-registry-service.md new file mode 100644 index 0000000..ec8e3e1 --- /dev/null +++ b/docs/ecosystem-registry-service.md @@ -0,0 +1,200 @@ +# Ecosystem Registry Service Direction + +This note compares Railiance Fabric with adjacent projects and standards before +starting a service implementation for registering repositories, libraries, +services, capabilities, interfaces, and dependencies. + +## Recommendation + +Build a small Railiance Ecosystem Registry service as the API and indexed read +model over repo-owned Fabric declarations. + +The registry should not replace the `fabric/` files in each repo. Repositories +remain the source of truth. The service validates, snapshots, queries, and +projects that model so agents, humans, and State Hub can interact with the +ecosystem graph without cloning every repo or rerunning the local CLI. + +The closest external model to compare against is CNCF xRegistry. xRegistry is +specifically about metadata registries, with both file/document and API views. +Railiance should borrow that shape where useful, especially for versioned +resources, import/export, filtering, and contract registries. Railiance should +not begin by claiming xRegistry compliance; it should keep a compatible path. + +## Standards And Projects To Compare + +| Project or standard | What it is good at | What Railiance should borrow | What not to copy as the core | +|---------------------|--------------------|-------------------------------|------------------------------| +| [CNCF xRegistry](https://xregistry.io/) | Vendor-neutral metadata registries with REST APIs, document views, versioned resources, endpoint/schema/message extensions. | Use as the primary comparison for registry API shape, versioned metadata, import/export, filtering, document/API symmetry, and future endpoint/message/schema projections. | Do not make every Fabric concept an xRegistry resource on day one; keep the Railiance graph model readable and repo-native first. | +| [Backstage Software Catalog](https://github.com/backstage/backstage/blob/master/docs/features/software-catalog/descriptor-format.md) | Developer portal catalog entities such as Component, API, Resource, System, Domain, ownership, relations, and `catalog-info.yaml`. | Support Backstage export/import projections for teams that want a portal later. Borrow the ownership and domain/system vocabulary where it aligns. | Do not make Backstage the authoritative store or require its plugin/runtime model before Railiance needs a portal. | +| [CycloneDX](https://cyclonedx.org/specification/overview/) | Supply-chain inventory for components, services, dependencies, relationships, and vulnerability/security context. | Use CycloneDX SBOM/SaaSBOM imports for libraries, packages, third-party services, component dependency graphs, and provenance facets. | Do not stretch CycloneDX into the whole ecosystem model; it is strongest for bill-of-materials and supply-chain evidence. | +| [OpenAPI](https://spec.openapis.org/oas/latest) | Machine-readable HTTP API contracts. | Attach OpenAPI documents to Fabric `InterfaceDeclaration` records for HTTP APIs and expose them through the registry. | Do not use OpenAPI to describe non-HTTP dependencies or ownership relationships. | +| [AsyncAPI](https://www.asyncapi.com/docs/reference/specification/v3.0.0) | Machine-readable event/message-driven API contracts with channels, messages, operations, and protocol bindings. | Attach AsyncAPI documents to event-stream interfaces and use its vocabulary for channel/message contracts. | Do not use AsyncAPI for general service inventory. | +| [CloudEvents](https://github.com/cloudevents/spec/blob/main/cloudevents/spec.md) | Common event envelope metadata across services, platforms, and systems. | Use CloudEvents as the preferred event envelope for registry events and for Fabric event interfaces when the ecosystem needs portable event metadata. | Do not use CloudEvents as a catalog model; it is an event envelope. | +| [Open Service Broker API](https://www.openservicebrokerapi.org/) | Lifecycle commands for service catalogs, provisioning, binding, unbinding, and deprovisioning. | Borrow the clear distinction between service catalog, service instance, and binding if Railiance later adds self-service provisioning. | Do not implement provisioning as part of the initial registry; registration and discovery come first. | +| [Score](https://developer.humanitec.com/app-humanitec-io/docs/score/overview/) | Platform-agnostic workload intent for container workloads, with runtime requirements resolved by a platform. | Optionally import workload requirements into Fabric dependencies when repos already use Score. | Do not make Score mandatory; it is workload runtime intent, not an ecosystem graph. | +| [OpenLineage](https://openlineage.io/docs/spec/object-model/) | Job, run, dataset, and facet model for observing data movement and transformation. | Use its facet idea for extensible metadata and consider data-lineage projections later. | Do not use it as the general service/capability/dependency model. | + +## Service Boundary + +The registry service should own: + +- repository registration and repository metadata snapshots +- ingestion of validated Fabric graph exports +- validation results per repo and commit +- indexed graph queries across all registered repos +- version history and drift comparisons between snapshots +- optional ingestion of supporting artifacts such as CycloneDX SBOMs, + OpenAPI documents, AsyncAPI documents, and Score workload files +- State Hub export or event emission for coordination views + +The registry service should not own: + +- hand-editing repo declarations through a central UI +- deployment orchestration +- service provisioning +- policy enforcement gates before the model has adoption +- replacing State Hub workstreams, tasks, progress, or planning state +- replacing a developer portal + +## Initial Data Model + +Railiance Fabric already has first-class declarations for: + +- `ServiceDeclaration` +- `CapabilityDeclaration` +- `InterfaceDeclaration` +- `DependencyDeclaration` +- `BindingAssertion` + +The registry service should add service-level records around those declarations: + +| Entity | Purpose | +|--------|---------| +| Repository | Registered source repo, URL, default branch, State Hub repo id, scan config, last accepted snapshot. | +| Snapshot | Immutable ingest result for a repo at a commit. | +| Graph Node | Indexed projection of a service, capability, interface, dependency, or binding. | +| Graph Edge | Indexed relationship such as provides, exposes, consumes, binds, or uses interface. | +| Artifact | Supporting document such as CycloneDX SBOM, OpenAPI, AsyncAPI, Score, README, or source link. | +| Validation Result | Errors, warnings, schema versions, catalog versions, and unresolved references. | +| Registry Event | Change event emitted when a repo, snapshot, node, edge, or validation result changes. | + +## API Shape + +Start with a small HTTP API that mirrors the local CLI answers: + +```text +POST /repositories +GET /repositories +GET /repositories/{repo_slug} + +POST /repositories/{repo_slug}/snapshots +GET /repositories/{repo_slug}/snapshots +GET /repositories/{repo_slug}/snapshots/latest + +GET /graph/nodes +GET /graph/nodes/{graph_id} +GET /graph/providers?capability_type=runtime-secrets +GET /graph/consumers?target=railiance-platform.openbao.kv-v2 +GET /graph/unresolved +GET /graph/blast-radius?interface_id=openbao-kv-v2-mount + +POST /artifacts +GET /artifacts/{artifact_id} + +GET /exports/state-hub +GET /exports/backstage +GET /exports/xregistry +``` + +`POST /repositories/{repo_slug}/snapshots` should accept the current +`FabricGraphExport` plus source metadata: + +```json +{ + "repo_slug": "railiance-fabric", + "commit": "git-sha", + "generated_at": "2026-05-17T00:00:00Z", + "graph": { + "apiVersion": "railiance.fabric/v1alpha1", + "kind": "FabricGraphExport", + "nodes": [], + "edges": [] + } +} +``` + +## Interoperability Direction + +The first implementation should be Railiance-native but deliberately +projection-friendly: + +- Backstage projection: export `Component`, `API`, `Resource`, `System`, and + `Domain` entities from Fabric nodes where possible. +- xRegistry projection: expose schemas, messages, endpoints, and possibly + Fabric-specific registry groups once the internal model settles. +- CycloneDX import: attach SBOM components, services, and dependencies to repo + and service nodes. +- OpenAPI/AsyncAPI attachment: connect contract documents to interface nodes + and validate that declared interface type/version metadata is consistent. +- CloudEvents events: emit registry changes such as + `railiance.fabric.repository.registered`, + `railiance.fabric.snapshot.accepted`, and + `railiance.fabric.validation.failed`. +- Score import: map workload resources and dependencies into draft Fabric + dependency declarations only when a repo opts in. + +## Suggested Architecture + +```text +repo-local fabric/*.yaml + | + v +railiance-fabric validate/export + | + v +Ecosystem Registry ingest API + | + +--> snapshot store + +--> graph index + +--> artifact index + +--> validation result store + | + +--> State Hub export/events + +--> Backstage/xRegistry projections + +--> query API for agents and humans +``` + +Keep the first service boring: the existing Python loader and validator should +be reused. A lightweight Python HTTP service with a local relational store is +enough for the first useful version. Once ingestion and query semantics are +stable, the backing store can be replaced or expanded. + +## First Implementation Slice + +1. Service scaffold using the existing loader, validator, and graph export + model. +2. Repository registration endpoint with repo slug, URL, default branch, and + optional State Hub repo id. +3. Snapshot ingest endpoint that validates a `FabricGraphExport` and stores it + atomically. +4. Query endpoints for providers, consumers, unresolved dependencies, dependency + paths, and blast radius. +5. State Hub export endpoint matching `docs/state-hub-integration.md`. +6. Contract attachment for OpenAPI and AsyncAPI documents. +7. CycloneDX SBOM attachment for library/package inventory. +8. CloudEvents-style registry events once mutation endpoints exist. + +## Open Design Questions + +- Should the registry pull repos itself, or should repos/agents push validated + exports from CI? Push is simpler and keeps credentials narrower. +- Should repository registration live first in State Hub and sync into Fabric + Registry, or should Fabric Registry own its own repo registry and annotate + State Hub ids? The current boundary suggests Fabric Registry owns graph + registration, State Hub owns planning/coordination. +- Should the first storage backend be SQLite for local operations or Postgres + from the start? SQLite is enough for proving semantics; Postgres is better + once multiple agents write concurrently. +- Should xRegistry compatibility be a projection only, or should the internal + registry model follow xRegistry group/resource/version terminology? Start as + projection; revisit after the first API is exercised. diff --git a/docs/first-rollout.md b/docs/first-rollout.md new file mode 100644 index 0000000..6e96d6f --- /dev/null +++ b/docs/first-rollout.md @@ -0,0 +1,54 @@ +# First Rollout + +The first rollout is represented by the seed declarations under `fabric/`. +Those files are intentionally centralized in Railiance Fabric for bootstrap; +the long-term target is for each owning repo to carry its own `fabric/` +declarations. + +## Seeded Repos + +| Repo | Seeded Service(s) | First Capability | +|------|-------------------|------------------| +| `railiance-platform` | OpenBao, CNPG, Valkey | runtime secrets, PostgreSQL, Redis-compatible cache | +| `net-kingdom` | IAM Profile contract | IAM Profile issuer | +| `key-cape` | IAM Profile API | IAM Profile issuer implementation | +| `flex-auth` | flex-auth API, Topaz | authorization decisions | +| `artifact-store` | object storage service | object storage, credential vending | +| `repo-scoping` | scope generator | scope generation | +| `the-custodian` | State Hub | coordination read model | + +## Promotion Path + +For each owning repo: + +1. Copy the matching seed files from `railiance-fabric/fabric/` into the owning + repo's own `fabric/` directory. +2. Replace seed source links with repo-local source links. +3. Validate the owning repo by itself. +4. Validate the owning repo together with `railiance-fabric` and other + providers/consumers it depends on. +5. Export the multi-repo graph for State Hub ingestion. +6. Once repo-local declarations are authoritative, remove or mark the central + seed declarations as bootstrap-only. + +## Suggested Order + +1. `railiance-platform`: owns OpenBao, CNPG, and Valkey provider declarations. +2. `key-cape`: owns the first concrete IAM Profile implementation. +3. `flex-auth`: owns authorization decisions and concrete consumers of OpenBao + and IAM Profile capabilities. +4. `the-custodian/state-hub`: owns coordination read-model declarations and is + the first export consumer. +5. `repo-scoping`: owns scope-generation provider declarations. +6. `artifact-store`: can promote planned object-storage declarations when its + interfaces stabilize. + +## Completion Signal + +The rollout is good enough for the next phase when: + +- each repo can validate its own declarations +- the combined graph has no unresolved dependencies +- State Hub can ingest a `FabricGraphExport` +- dashboard/search views can answer provider, consumer, unresolved, and blast + radius questions from the ingested graph diff --git a/docs/state-hub-integration.md b/docs/state-hub-integration.md new file mode 100644 index 0000000..5f8e8a7 --- /dev/null +++ b/docs/state-hub-integration.md @@ -0,0 +1,159 @@ +# State Hub Integration Contract + +Railiance Fabric is the authoring and validation layer for ecosystem graph +declarations. State Hub should ingest Fabric outputs as a read model for +coordination, search, dashboards, and planning. It should not become the +primary authoring surface for services, capabilities, interfaces, dependencies, +or bindings. + +## Source-Of-Truth Boundary + +| Layer | Owns | Does Not Own | +|-------|------|--------------| +| Participating repos | Declaration files under `fabric/` | Global graph interpretation | +| Railiance Fabric | Schemas, type catalogs, validation, graph construction, exports | State Hub tasks/progress/decisions | +| State Hub | Read-model storage, links to repos/workstreams/tasks/progress, dashboard/search views | Editing Fabric declarations | + +The flow is: + +```text +repo-local fabric/*.yaml + | + v +railiance-fabric validate/export + | + v +State Hub graph read model + | + v +dashboard, search, planning, progress links +``` + +## Export Shape + +The CLI emits `FabricGraphExport` JSON: + +```bash +railiance-fabric export --format json +``` + +Schema: `schemas/state-hub-export.schema.yaml` + +Top-level shape: + +```yaml +apiVersion: railiance.fabric/v1alpha1 +kind: FabricGraphExport +nodes: [] +edges: [] +``` + +Node fields: + +| Field | Meaning | +|-------|---------| +| `id` | Stable graph id from declaration metadata. | +| `kind` | Declaration kind: service, capability, interface, dependency, or binding. | +| `name` | Human-readable name. | +| `repo` | Owning repo slug. | +| `domain` | Owning domain slug. | +| `lifecycle` | Declaration lifecycle. | + +Edge fields: + +| Field | Meaning | +|-------|---------| +| `from` | Source node id. | +| `to` | Target node id. | +| `type` | Relationship type, such as `provides`, `exposes`, `available_via`, `consumes`, `binds:exact`, or `uses_interface`. | + +## Proposed State Hub Read Model + +Add a State Hub ingestion endpoint or job that stores the latest graph export +per source repo: + +```text +POST /fabric/graph-exports +``` + +Suggested payload: + +```json +{ + "repo_slug": "railiance-fabric", + "commit": "", + "generated_at": "2026-05-17T00:00:00Z", + "graph": { + "apiVersion": "railiance.fabric/v1alpha1", + "kind": "FabricGraphExport", + "nodes": [], + "edges": [] + } +} +``` + +Suggested storage: + +```text +fabric_graph_exports + id + repo_id + commit + generated_at + graph_json + created_at + +fabric_graph_nodes + export_id + graph_id + kind + name + repo_slug + domain_slug + lifecycle + +fabric_graph_edges + export_id + from_graph_id + to_graph_id + edge_type +``` + +The normalized node/edge tables are optional at first. State Hub can begin with +`fabric_graph_exports.graph_json` and materialize node/edge tables once query +needs harden. + +## Linking To Existing State Hub Entities + +State Hub should enrich graph nodes by matching: + +- `node.repo` -> `managed_repos.slug` +- `node.domain` -> `domains.slug` +- workplan source links -> `workstreams.slug` or file-backed workplan index +- progress events -> `repo_id` and related workstream/task when available + +These links are annotations on the read model. They should never overwrite the +repo-owned declaration files. + +## Ingestion Rules + +1. Reject exports that fail `schemas/state-hub-export.schema.yaml`. +2. Record the source repo and commit for every accepted export. +3. Replace the previous latest export for the same repo only after the new + export validates. +4. Preserve historical exports long enough to compare graph drift. +5. Surface validation errors as State Hub progress events or human-review tasks, + but do not auto-edit declaration files. + +## Initial Dashboard Queries + +State Hub should be able to answer: + +- providers for a capability type +- consumers of a capability or interface +- unresolved dependencies +- blast radius for an interface id or type +- graph nodes by repo/domain/lifecycle + +These are the same query families exposed locally by Railiance Fabric. The hub +read model should match local answers for the same export. diff --git a/docs/type-catalog.md b/docs/type-catalog.md new file mode 100644 index 0000000..4f3c788 --- /dev/null +++ b/docs/type-catalog.md @@ -0,0 +1,68 @@ +# Core Type Catalog + +The type catalog names the stable semantic capabilities and concrete +integration surfaces used by Railiance Fabric declarations. + +The catalog has two jobs: + +- prevent ad hoc strings in repo-owned declaration files +- give the future validator enough metadata to warn about mismatched + capability/interface combinations + +Machine-readable catalog files: + +- `catalog/capability-types.yaml` +- `catalog/interface-types.yaml` + +## Capability Types + +| Type | Lifecycle | Default Criticality | Default Data | Expected Interfaces | +|------|-----------|---------------------|--------------|---------------------| +| `runtime-secrets` | active | critical | secret | `openbao-kv-v2-mount`, `kubernetes-secret` | +| `iam-profile-issuer` | active | critical | restricted | `oidc-discovery`, `http-api` | +| `authorization-decision-service` | active | critical | restricted | `http-api`, `policy-package` | +| `postgresql-database-service` | active | high | confidential | `database-connection`, `openbao-dynamic-credential-role` | +| `redis-compatible-cache` | active | medium | internal | `database-connection`, `kubernetes-secret` | +| `object-storage` | planned | high | confidential | `object-storage-bucket`, `http-api` | +| `object-storage-credential-vending` | planned | high | secret | `http-api`, `openbao-dynamic-credential-role`, `sts-token` | +| `audit-event-sink` | planned | high | confidential | `event-stream`, `http-api` | +| `scope-generation` | active | medium | internal | `cli`, `http-api` | +| `coordination-read-model` | active | high | internal | `http-api`, `event-stream` | + +## Interface Types + +| Type | Lifecycle | Category | Typical Auth | +|------|-----------|----------|--------------| +| `http-api` | active | api | `none`, `oidc`, `jwt`, `mtls`, `api_key` | +| `oidc-discovery` | active | identity | `none` | +| `kubernetes-secret` | active | kubernetes | `kubernetes_service_account` | +| `kubernetes-crd` | active | kubernetes | `kubernetes_service_account` | +| `helm-release` | active | deployment | `kubernetes_service_account` | +| `cli` | active | tooling | `none`, `oidc`, `api_key`, `unknown` | +| `database-connection` | active | data | `database_role`, `static_secret`, `openbao_token` | +| `object-storage-bucket` | planned | storage | `sts_token`, `static_secret`, `openbao_token` | +| `event-stream` | planned | events | `jwt`, `mtls`, `api_key`, `unknown` | +| `policy-package` | active | policy | `none`, `oidc`, `jwt` | +| `openbao-kv-v2-mount` | active | secrets | `kubernetes_service_account`, `openbao_token` | +| `openbao-dynamic-credential-role` | active | credentials | `kubernetes_service_account`, `openbao_token` | +| `sts-token` | planned | credentials | `oidc`, `jwt`, `mtls` | + +## Validation Rules For T05 + +The graph validator should initially enforce: + +- every `CapabilityDeclaration.spec.capability_type` exists in + `capability-types.yaml` +- every `InterfaceDeclaration.spec.interface_type` exists in + `interface-types.yaml` +- every `DependencyDeclaration.spec.requires.capability_type` exists in + `capability-types.yaml` +- every `DependencyDeclaration.spec.interface.type`, when present, exists in + `interface-types.yaml` +- provider interface types should be among the capability type's + `expected_interface_types`, unless a declaration includes a documented + compatibility note + +The validator should warn, not fail, when a declaration uses a planned type in +an active production dependency. That keeps early adoption possible while still +surfacing rollout risk. diff --git a/docs/validator.md b/docs/validator.md new file mode 100644 index 0000000..de9049e --- /dev/null +++ b/docs/validator.md @@ -0,0 +1,40 @@ +# Validator + +The first validator entry point is: + +```bash +railiance-fabric validate ... +``` + +It accepts: + +- a repo root containing `fabric/` +- a `fabric/` directory +- one or more declaration YAML files + +The validator currently checks: + +- YAML load errors +- declaration schema conformance +- duplicate graph IDs +- unknown capability and interface types +- missing service, capability, interface, dependency, and binding references +- missing provider capabilities for dependencies +- binding provider/dependency capability type mismatches +- active production dependencies without `metadata.source_links` +- active production dependencies whose providers do not cover the environment +- service dependency cycles from binding assertions + +Exit behavior: + +- exits `0` when there are no errors +- exits `1` when errors are present +- exits `1` for warnings only when `--warnings-as-errors` is used + +Examples: + +```bash +PYTHONPATH=. python -m railiance_fabric.cli validate . +PYTHONPATH=. python -m railiance_fabric.cli validate fabric/ +PYTHONPATH=. python -m railiance_fabric.cli validate examples/declarations/invalid/*.yaml +``` diff --git a/examples/declarations/README.md b/examples/declarations/README.md new file mode 100644 index 0000000..d556385 --- /dev/null +++ b/examples/declarations/README.md @@ -0,0 +1,15 @@ +# Declaration Fixtures + +These fixtures support the T02 schema baseline and give the future validator +real inputs to exercise. + +`valid/` contains a coherent mini-graph: + +- OpenBao service +- OpenBao runtime-secrets capability +- OpenBao KV v2 interface +- flex-auth runtime-secrets dependency +- binding assertion from flex-auth to OpenBao + +`invalid/` contains schema-level failures. The future validator should report +clear errors for these before it attempts graph-level checks. diff --git a/examples/declarations/invalid/binding-bad-status.yaml b/examples/declarations/invalid/binding-bad-status.yaml new file mode 100644 index 0000000..9d99bc4 --- /dev/null +++ b/examples/declarations/invalid/binding-bad-status.yaml @@ -0,0 +1,15 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: BindingAssertion +metadata: + id: flex-auth.api.bad-binding-status + name: Bad binding status + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [dev] + dependency_id: flex-auth.api.needs-runtime-secrets + provider_capability_id: railiance-platform.openbao.runtime-secrets + status: accepted + rationale: Invalid because accepted is not a binding status. diff --git a/examples/declarations/invalid/capability-bad-lifecycle.yaml b/examples/declarations/invalid/capability-bad-lifecycle.yaml new file mode 100644 index 0000000..68b013d --- /dev/null +++ b/examples/declarations/invalid/capability-bad-lifecycle.yaml @@ -0,0 +1,16 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: CapabilityDeclaration +metadata: + id: railiance-platform.openbao.bad-lifecycle + name: Bad lifecycle capability + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: started + environments: [dev] + description: Invalid because lifecycle must use the shared enum. + capability_type: runtime-secrets + service_id: railiance-platform.openbao + criticality: high + data_classification: secret diff --git a/examples/declarations/invalid/dependency-bad-environment.yaml b/examples/declarations/invalid/dependency-bad-environment.yaml new file mode 100644 index 0000000..b3e8ad0 --- /dev/null +++ b/examples/declarations/invalid/dependency-bad-environment.yaml @@ -0,0 +1,16 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: DependencyDeclaration +metadata: + id: flex-auth.api.bad-environment + name: Bad environment dependency + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [production] + consumer_service_id: flex-auth.api + requires: + capability_type: runtime-secrets + criticality: high + data_classification: secret diff --git a/examples/declarations/invalid/interface-bad-auth.yaml b/examples/declarations/invalid/interface-bad-auth.yaml new file mode 100644 index 0000000..11b70a8 --- /dev/null +++ b/examples/declarations/invalid/interface-bad-auth.yaml @@ -0,0 +1,18 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: railiance-platform.openbao.bad-auth + name: Bad auth interface + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev] + description: Invalid because oauth2 is not in the initial auth-method enum. + interface_type: http-api + version: v1 + service_id: railiance-platform.openbao + auth: + method: oauth2 + data_classification: internal diff --git a/examples/declarations/invalid/service-missing-id.yaml b/examples/declarations/invalid/service-missing-id.yaml new file mode 100644 index 0000000..53bc6e5 --- /dev/null +++ b/examples/declarations/invalid/service-missing-id.yaml @@ -0,0 +1,11 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + name: Missing ID Service + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev] + description: Invalid because metadata.id is required. diff --git a/examples/declarations/valid/binding-flex-auth-openbao.yaml b/examples/declarations/valid/binding-flex-auth-openbao.yaml new file mode 100644 index 0000000..223a341 --- /dev/null +++ b/examples/declarations/valid/binding-flex-auth-openbao.yaml @@ -0,0 +1,23 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: BindingAssertion +metadata: + id: flex-auth.api.runtime-secrets-to-openbao + name: flex-auth runtime secrets binding + owner: flex-auth + repo: flex-auth + domain: railiance + source_links: + - label: Runtime secrets binding note + path: docs/runtime-secrets.md +spec: + lifecycle: active + environments: [dev, staging, prod] + dependency_id: flex-auth.api.needs-runtime-secrets + provider_capability_id: railiance-platform.openbao.runtime-secrets + provider_interface_id: railiance-platform.openbao.kv-v2 + status: exact + rationale: flex-auth uses the OpenBao KV v2 mount as its runtime secrets provider. + compatibility: + version: v1 + compatible_with: + - railiance-platform.openbao.kv-v2 diff --git a/examples/declarations/valid/capability-runtime-secrets.yaml b/examples/declarations/valid/capability-runtime-secrets.yaml new file mode 100644 index 0000000..6f4c9e8 --- /dev/null +++ b/examples/declarations/valid/capability-runtime-secrets.yaml @@ -0,0 +1,26 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: CapabilityDeclaration +metadata: + id: railiance-platform.openbao.runtime-secrets + name: Runtime secrets + owner: railiance-platform + repo: railiance-platform + domain: railiance + source_links: + - label: Runtime secrets workplan + path: workplans/RAIL-PLAT-WP-openbao-runtime-secrets.md +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Stores and serves workload runtime secrets through OpenBao. + capability_type: runtime-secrets + service_id: railiance-platform.openbao + interface_ids: + - railiance-platform.openbao.kv-v2 + criticality: critical + data_classification: secret + compatibility: + version: v1 + compatible_with: + - railiance-platform.openbao.kv-v2 + notes: Initial runtime secrets capability for Railiance workloads. diff --git a/examples/declarations/valid/dependency-flex-auth-runtime-secrets.yaml b/examples/declarations/valid/dependency-flex-auth-runtime-secrets.yaml new file mode 100644 index 0000000..9d4050a --- /dev/null +++ b/examples/declarations/valid/dependency-flex-auth-runtime-secrets.yaml @@ -0,0 +1,31 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: DependencyDeclaration +metadata: + id: flex-auth.api.needs-runtime-secrets + name: flex-auth runtime secrets dependency + owner: flex-auth + repo: flex-auth + domain: railiance + source_links: + - label: flex-auth deployment values + path: deploy/values.yaml +spec: + lifecycle: active + environments: [dev, staging, prod] + consumer_service_id: flex-auth.api + requires: + capability_type: runtime-secrets + interface: + type: openbao-kv-v2-mount + version_constraint: ">=v1 =v1 =v1 =v1 =v1 =16 <17" + auth: + method: database_role + criticality: critical + data_classification: confidential + fallback: + mode: none + description: State Hub cannot persist coordination state without PostgreSQL. diff --git a/fabric/interfaces/artifact-store-object-storage-bucket.yaml b/fabric/interfaces/artifact-store-object-storage-bucket.yaml new file mode 100644 index 0000000..d79b977 --- /dev/null +++ b/fabric/interfaces/artifact-store-object-storage-bucket.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: artifact-store.object-storage.bucket + name: artifact-store object bucket + owner: artifact-store + repo: artifact-store + domain: railiance +spec: + lifecycle: planned + environments: [dev, staging, prod] + description: Bucket and object layout contract for artifact storage. + interface_type: object-storage-bucket + version: v1 + service_id: artifact-store.storage-service + capability_ids: + - artifact-store.object-storage + auth: + method: sts_token + data_classification: confidential diff --git a/fabric/interfaces/artifact-store-object-storage-sts.yaml b/fabric/interfaces/artifact-store-object-storage-sts.yaml new file mode 100644 index 0000000..4c69cb4 --- /dev/null +++ b/fabric/interfaces/artifact-store-object-storage-sts.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: artifact-store.object-storage.sts + name: artifact-store STS credential endpoint + owner: artifact-store + repo: artifact-store + domain: railiance +spec: + lifecycle: planned + environments: [dev, staging, prod] + description: Temporary scoped credential vending interface for object storage. + interface_type: sts-token + version: v1 + service_id: artifact-store.storage-service + capability_ids: + - artifact-store.object-storage.credentials + auth: + method: oidc + data_classification: secret diff --git a/fabric/interfaces/flex-auth-api-http-api.yaml b/fabric/interfaces/flex-auth-api-http-api.yaml new file mode 100644 index 0000000..9ee6010 --- /dev/null +++ b/fabric/interfaces/flex-auth-api-http-api.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: flex-auth.api.http-api + name: flex-auth decision HTTP API + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: HTTP API for authorization decision requests. + interface_type: http-api + version: v1 + service_id: flex-auth.api + capability_ids: + - flex-auth.api.authorization-decisions + auth: + method: oidc + data_classification: restricted diff --git a/fabric/interfaces/flex-auth-api-policy-package.yaml b/fabric/interfaces/flex-auth-api-policy-package.yaml new file mode 100644 index 0000000..5ea7336 --- /dev/null +++ b/fabric/interfaces/flex-auth-api-policy-package.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: flex-auth.api.policy-package + name: flex-auth policy package + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Versioned authorization policy package consumed by PDP runtimes. + interface_type: policy-package + version: v1 + service_id: flex-auth.api + capability_ids: + - flex-auth.api.authorization-decisions + auth: + method: oidc + data_classification: restricted diff --git a/fabric/interfaces/flex-auth-topaz-http-api.yaml b/fabric/interfaces/flex-auth-topaz-http-api.yaml new file mode 100644 index 0000000..15209b3 --- /dev/null +++ b/fabric/interfaces/flex-auth-topaz-http-api.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: flex-auth.topaz.http-api + name: Topaz decision HTTP API + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: HTTP interface for delegated Topaz authorization decisions. + interface_type: http-api + version: v1 + service_id: flex-auth.topaz + capability_ids: + - flex-auth.topaz.authorization-runtime + auth: + method: oidc + data_classification: restricted diff --git a/fabric/interfaces/key-cape-iam-profile-http-api.yaml b/fabric/interfaces/key-cape-iam-profile-http-api.yaml new file mode 100644 index 0000000..fd3b83e --- /dev/null +++ b/fabric/interfaces/key-cape-iam-profile-http-api.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: key-cape.iam-profile.http-api + name: key-cape IAM Profile HTTP API + owner: key-cape + repo: key-cape + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: HTTP API for IAM Profile lookup and claim serving. + interface_type: http-api + version: v1 + service_id: key-cape.iam-profile + capability_ids: + - key-cape.iam-profile.issuer + auth: + method: oidc + data_classification: restricted diff --git a/fabric/interfaces/key-cape-iam-profile-oidc-discovery.yaml b/fabric/interfaces/key-cape-iam-profile-oidc-discovery.yaml new file mode 100644 index 0000000..8349613 --- /dev/null +++ b/fabric/interfaces/key-cape-iam-profile-oidc-discovery.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: key-cape.iam-profile.oidc-discovery + name: key-cape OIDC discovery + owner: key-cape + repo: key-cape + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: OIDC discovery metadata for IAM Profile claims. + interface_type: oidc-discovery + version: v1 + service_id: key-cape.iam-profile + capability_ids: + - key-cape.iam-profile.issuer + auth: + method: none + data_classification: public diff --git a/fabric/interfaces/net-kingdom-iam-profile-oidc-discovery.yaml b/fabric/interfaces/net-kingdom-iam-profile-oidc-discovery.yaml new file mode 100644 index 0000000..768c5fe --- /dev/null +++ b/fabric/interfaces/net-kingdom-iam-profile-oidc-discovery.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: net-kingdom.iam-profile.oidc-discovery + name: NetKingdom IAM Profile discovery + owner: net-kingdom + repo: net-kingdom + domain: railiance +spec: + lifecycle: active + environments: [all] + description: OIDC discovery contract for IAM Profile identity architecture. + interface_type: oidc-discovery + version: v1 + service_id: net-kingdom.iam-profile + capability_ids: + - net-kingdom.iam-profile.issuer + auth: + method: none + data_classification: public diff --git a/fabric/interfaces/railiance-platform-cnpg-database-connection.yaml b/fabric/interfaces/railiance-platform-cnpg-database-connection.yaml new file mode 100644 index 0000000..69b5216 --- /dev/null +++ b/fabric/interfaces/railiance-platform-cnpg-database-connection.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: railiance-platform.cnpg.database-connection + name: CloudNativePG database connection + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: PostgreSQL network endpoint and credential contract. + interface_type: database-connection + version: "16" + service_id: railiance-platform.cnpg + capability_ids: + - railiance-platform.cnpg.postgresql + auth: + method: database_role + data_classification: confidential diff --git a/fabric/interfaces/railiance-platform-openbao-database-roles.yaml b/fabric/interfaces/railiance-platform-openbao-database-roles.yaml new file mode 100644 index 0000000..347fbde --- /dev/null +++ b/fabric/interfaces/railiance-platform-openbao-database-roles.yaml @@ -0,0 +1,21 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: railiance-platform.openbao.database-roles + name: OpenBao database dynamic credential roles + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Dynamic credential role interface for database access. + interface_type: openbao-dynamic-credential-role + version: v1 + service_id: railiance-platform.openbao + endpoint: + path: database/creds + auth: + method: kubernetes_service_account + audience: openbao + data_classification: secret diff --git a/fabric/interfaces/railiance-platform-openbao-kv-v2.yaml b/fabric/interfaces/railiance-platform-openbao-kv-v2.yaml new file mode 100644 index 0000000..3b7bb53 --- /dev/null +++ b/fabric/interfaces/railiance-platform-openbao-kv-v2.yaml @@ -0,0 +1,23 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: railiance-platform.openbao.kv-v2 + name: OpenBao KV v2 mount + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: KV v2 secret mount for approved Railiance workload secrets. + interface_type: openbao-kv-v2-mount + version: v1 + service_id: railiance-platform.openbao + capability_ids: + - railiance-platform.openbao.runtime-secrets + endpoint: + path: secret/data/railiance + auth: + method: kubernetes_service_account + audience: openbao + data_classification: secret diff --git a/fabric/interfaces/railiance-platform-valkey-database-connection.yaml b/fabric/interfaces/railiance-platform-valkey-database-connection.yaml new file mode 100644 index 0000000..b074670 --- /dev/null +++ b/fabric/interfaces/railiance-platform-valkey-database-connection.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: railiance-platform.valkey.database-connection + name: Valkey Redis-compatible connection + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Redis protocol compatible cache endpoint and credential contract. + interface_type: database-connection + version: v1 + service_id: railiance-platform.valkey + capability_ids: + - railiance-platform.valkey.cache + auth: + method: static_secret + data_classification: internal diff --git a/fabric/interfaces/repo-scoping-scope-generator-cli.yaml b/fabric/interfaces/repo-scoping-scope-generator-cli.yaml new file mode 100644 index 0000000..2a8bb83 --- /dev/null +++ b/fabric/interfaces/repo-scoping-scope-generator-cli.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: repo-scoping.scope-generator.cli + name: repo-scoping CLI + owner: repo-scoping + repo: repo-scoping + domain: custodian +spec: + lifecycle: active + environments: [all] + description: CLI interface for producing scope descriptions. + interface_type: cli + version: v1 + service_id: repo-scoping.scope-generator + capability_ids: + - repo-scoping.scope-generation + auth: + method: none + data_classification: internal diff --git a/fabric/interfaces/the-custodian-state-hub-http-api.yaml b/fabric/interfaces/the-custodian-state-hub-http-api.yaml new file mode 100644 index 0000000..fbcec5f --- /dev/null +++ b/fabric/interfaces/the-custodian-state-hub-http-api.yaml @@ -0,0 +1,20 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: InterfaceDeclaration +metadata: + id: the-custodian.state-hub.http-api + name: State Hub HTTP API + owner: the-custodian + repo: the-custodian + domain: custodian +spec: + lifecycle: active + environments: [all] + description: HTTP API for coordination state and progress tracking. + interface_type: http-api + version: v1 + service_id: the-custodian.state-hub + capability_ids: + - the-custodian.state-hub.coordination + auth: + method: none + data_classification: internal diff --git a/fabric/services/artifact-store-object-storage.yaml b/fabric/services/artifact-store-object-storage.yaml new file mode 100644 index 0000000..d4e960e --- /dev/null +++ b/fabric/services/artifact-store-object-storage.yaml @@ -0,0 +1,19 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: artifact-store.storage-service + name: artifact-store object storage + owner: artifact-store + repo: artifact-store + domain: railiance +spec: + lifecycle: planned + environments: [dev, staging, prod] + description: Planned object storage and scoped credential vending service. + service_type: storage-service + provides_capabilities: + - artifact-store.object-storage + - artifact-store.object-storage.credentials + exposes_interfaces: + - artifact-store.object-storage.bucket + - artifact-store.object-storage.sts diff --git a/fabric/services/flex-auth-api.yaml b/fabric/services/flex-auth-api.yaml new file mode 100644 index 0000000..3849242 --- /dev/null +++ b/fabric/services/flex-auth-api.yaml @@ -0,0 +1,18 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: flex-auth.api + name: flex-auth API + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Authorization policy and decision control plane. + service_type: authorization-service + provides_capabilities: + - flex-auth.api.authorization-decisions + exposes_interfaces: + - flex-auth.api.http-api + - flex-auth.api.policy-package diff --git a/fabric/services/flex-auth-topaz.yaml b/fabric/services/flex-auth-topaz.yaml new file mode 100644 index 0000000..f1c9c3e --- /dev/null +++ b/fabric/services/flex-auth-topaz.yaml @@ -0,0 +1,17 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: flex-auth.topaz + name: Topaz delegated PDP + owner: flex-auth + repo: flex-auth + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Delegated policy decision runtime used by flex-auth. + service_type: authorization-runtime + provides_capabilities: + - flex-auth.topaz.authorization-runtime + exposes_interfaces: + - flex-auth.topaz.http-api diff --git a/fabric/services/key-cape-iam-profile.yaml b/fabric/services/key-cape-iam-profile.yaml new file mode 100644 index 0000000..5383ec0 --- /dev/null +++ b/fabric/services/key-cape-iam-profile.yaml @@ -0,0 +1,18 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: key-cape.iam-profile + name: key-cape IAM Profile API + owner: key-cape + repo: key-cape + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Lightweight IAM Profile implementation for Railiance workloads. + service_type: identity-service + provides_capabilities: + - key-cape.iam-profile.issuer + exposes_interfaces: + - key-cape.iam-profile.http-api + - key-cape.iam-profile.oidc-discovery diff --git a/fabric/services/net-kingdom-iam-profile.yaml b/fabric/services/net-kingdom-iam-profile.yaml new file mode 100644 index 0000000..33cac2f --- /dev/null +++ b/fabric/services/net-kingdom-iam-profile.yaml @@ -0,0 +1,17 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: net-kingdom.iam-profile + name: NetKingdom IAM Profile + owner: net-kingdom + repo: net-kingdom + domain: railiance +spec: + lifecycle: active + environments: [all] + description: Identity and security architecture contract for IAM Profile claims. + service_type: identity-contract + provides_capabilities: + - net-kingdom.iam-profile.issuer + exposes_interfaces: + - net-kingdom.iam-profile.oidc-discovery diff --git a/fabric/services/railiance-platform-cnpg.yaml b/fabric/services/railiance-platform-cnpg.yaml new file mode 100644 index 0000000..c0ec72b --- /dev/null +++ b/fabric/services/railiance-platform-cnpg.yaml @@ -0,0 +1,17 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: railiance-platform.cnpg + name: CloudNativePG + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: PostgreSQL database service for Railiance platform and app workloads. + service_type: database-service + provides_capabilities: + - railiance-platform.cnpg.postgresql + exposes_interfaces: + - railiance-platform.cnpg.database-connection diff --git a/fabric/services/railiance-platform-openbao.yaml b/fabric/services/railiance-platform-openbao.yaml new file mode 100644 index 0000000..bf7dbeb --- /dev/null +++ b/fabric/services/railiance-platform-openbao.yaml @@ -0,0 +1,18 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: railiance-platform.openbao + name: OpenBao + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: OpenBao service used by Railiance workloads for runtime secrets. + service_type: platform-service + provides_capabilities: + - railiance-platform.openbao.runtime-secrets + exposes_interfaces: + - railiance-platform.openbao.kv-v2 + - railiance-platform.openbao.database-roles diff --git a/fabric/services/railiance-platform-valkey.yaml b/fabric/services/railiance-platform-valkey.yaml new file mode 100644 index 0000000..65c8fce --- /dev/null +++ b/fabric/services/railiance-platform-valkey.yaml @@ -0,0 +1,17 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: railiance-platform.valkey + name: Valkey + owner: railiance-platform + repo: railiance-platform + domain: railiance +spec: + lifecycle: active + environments: [dev, staging, prod] + description: Redis-compatible cache for Railiance workloads. + service_type: cache-service + provides_capabilities: + - railiance-platform.valkey.cache + exposes_interfaces: + - railiance-platform.valkey.database-connection diff --git a/fabric/services/repo-scoping-scope-generator.yaml b/fabric/services/repo-scoping-scope-generator.yaml new file mode 100644 index 0000000..b22fadd --- /dev/null +++ b/fabric/services/repo-scoping-scope-generator.yaml @@ -0,0 +1,17 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: repo-scoping.scope-generator + name: repo-scoping scope generator + owner: repo-scoping + repo: repo-scoping + domain: custodian +spec: + lifecycle: active + environments: [all] + description: Generates repo scope and usefulness descriptions for humans and agents. + service_type: planning-tool + provides_capabilities: + - repo-scoping.scope-generation + exposes_interfaces: + - repo-scoping.scope-generator.cli diff --git a/fabric/services/the-custodian-state-hub.yaml b/fabric/services/the-custodian-state-hub.yaml new file mode 100644 index 0000000..9044854 --- /dev/null +++ b/fabric/services/the-custodian-state-hub.yaml @@ -0,0 +1,17 @@ +apiVersion: railiance.fabric/v1alpha1 +kind: ServiceDeclaration +metadata: + id: the-custodian.state-hub + name: State Hub + owner: the-custodian + repo: the-custodian + domain: custodian +spec: + lifecycle: active + environments: [all] + description: Coordination read model for repos, workstreams, tasks, decisions, and progress. + service_type: coordination-service + provides_capabilities: + - the-custodian.state-hub.coordination + exposes_interfaces: + - the-custodian.state-hub.http-api diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..38d51be --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "railiance-fabric" +version = "0.1.0" +description = "Railiance ecosystem graph declaration loader and validator" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "jsonschema>=4.18", + "PyYAML>=6.0", +] + +[project.scripts] +railiance-fabric = "railiance_fabric.cli:main" + +[tool.setuptools.packages.find] +include = ["railiance_fabric*"] diff --git a/railiance_fabric/__init__.py b/railiance_fabric/__init__.py new file mode 100644 index 0000000..690dbf4 --- /dev/null +++ b/railiance_fabric/__init__.py @@ -0,0 +1,5 @@ +"""Railiance Fabric graph tooling.""" + +__all__ = ["__version__"] + +__version__ = "0.1.0" diff --git a/railiance_fabric/cli.py b/railiance_fabric/cli.py new file mode 100644 index 0000000..82e29d1 --- /dev/null +++ b/railiance_fabric/cli.py @@ -0,0 +1,183 @@ +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from .graph import FabricGraph, build_graph +from .validation import validate_roots + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="railiance-fabric", + description="Load and validate Railiance Fabric declarations.", + ) + sub = parser.add_subparsers(dest="command", required=True) + + validate = sub.add_parser( + "validate", + help="Validate one or more repo roots or declaration files.", + ) + validate.add_argument( + "paths", + nargs="+", + type=Path, + help="Repo root, fabric directory, or declaration YAML file.", + ) + validate.add_argument( + "--warnings-as-errors", + action="store_true", + help="Exit non-zero when warnings are present.", + ) + + providers = sub.add_parser("providers", help="List providers for a capability type or id.") + providers.add_argument("capability", help="Capability type or capability id.") + providers.add_argument("paths", nargs="*", type=Path, default=[Path(".")]) + + consumers = sub.add_parser("consumers", help="List consumers of a capability or interface.") + consumers.add_argument("target", help="Capability/interface type or declaration id.") + consumers.add_argument("paths", nargs="*", type=Path, default=[Path(".")]) + + dependency_path = sub.add_parser("dependency-path", help="Show dependency path for a service.") + dependency_path.add_argument("service_id", help="Service declaration id.") + dependency_path.add_argument("paths", nargs="*", type=Path, default=[Path(".")]) + + unresolved = sub.add_parser("unresolved", help="Show missing or unresolved dependencies.") + unresolved.add_argument("paths", nargs="*", type=Path, default=[Path(".")]) + + blast = sub.add_parser("blast-radius", help="Show consumers affected by an interface change.") + blast.add_argument("interface", help="Interface type or interface declaration id.") + blast.add_argument("paths", nargs="*", type=Path, default=[Path(".")]) + + export = sub.add_parser("export", help="Export graph as JSON or Mermaid.") + export.add_argument("paths", nargs="*", type=Path, default=[Path(".")]) + export.add_argument("--format", choices=["json", "mermaid"], default="json") + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + if args.command == "validate": + report = validate_roots(args.paths) + for diagnostic in report.diagnostics: + print(diagnostic.format()) + print(report.summary()) + if report.errors: + return 1 + if args.warnings_as_errors and report.warnings: + return 1 + return 0 + + if args.command == "providers": + graph = _load_graph_or_exit(args.paths) + _print_providers(graph, args.capability) + return 0 + + if args.command == "consumers": + graph = _load_graph_or_exit(args.paths) + _print_consumers(graph, args.target) + return 0 + + if args.command == "dependency-path": + graph = _load_graph_or_exit(args.paths) + print("\n".join(graph.dependency_path_lines(args.service_id))) + return 0 + + if args.command == "unresolved": + graph = _load_graph_or_exit(args.paths) + _print_unresolved(graph) + return 0 + + if args.command == "blast-radius": + graph = _load_graph_or_exit(args.paths) + _print_consumers(graph, args.interface, matches=graph.blast_radius(args.interface)) + return 0 + + if args.command == "export": + graph = _load_graph_or_exit(args.paths) + print(graph.to_mermaid() if args.format == "mermaid" else graph.to_json()) + return 0 + + parser.error(f"unknown command {args.command!r}") + return 2 + +def _load_graph_or_exit(paths: list[Path]) -> FabricGraph: + graph = build_graph(paths) + if graph.load_errors: + for path, message in graph.load_errors: + print(f"ERROR {path}: {message}", file=sys.stderr) + raise SystemExit(1) + return graph + + +def _print_providers(graph: FabricGraph, capability: str) -> None: + providers = graph.providers(capability) + if not providers: + print(f"no providers found for {capability}") + return + print("provider_id\tservice_id\tlifecycle\tenvironments\tinterfaces") + for provider in providers: + spec = provider.spec + print( + "\t".join( + [ + provider.id, + str(spec.get("service_id", "")), + str(spec.get("lifecycle", "")), + ",".join(spec.get("environments", [])), + ",".join(spec.get("interface_ids", [])), + ] + ) + ) + + +def _print_consumers( + graph: FabricGraph, + target: str, + matches: object | None = None, +) -> None: + consumer_matches = graph.consumers(target) if matches is None else list(matches) + if not consumer_matches: + print(f"no consumers found for {target}") + return + print("consumer_service_id\tdependency_id\trequires\tprovider_capability_id\tprovider_interface_id\tstatus") + for match in consumer_matches: + print( + "\t".join( + [ + match.consumer_service_id, + match.dependency_id, + match.required_capability_type, + match.provider_capability_id, + match.provider_interface_id, + match.status, + ] + ) + ) + + +def _print_unresolved(graph: FabricGraph) -> None: + unresolved = graph.unresolved_dependencies() + if not unresolved: + print("no unresolved dependencies") + return + print("dependency_id\tconsumer_service_id\trequires") + for dependency in unresolved: + spec = dependency.spec + requires = spec.get("requires", {}) + print( + "\t".join( + [ + dependency.id, + str(spec.get("consumer_service_id", "")), + str(requires.get("capability_id") or requires.get("capability_type", "")), + ] + ) + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/railiance_fabric/graph.py b/railiance_fabric/graph.py new file mode 100644 index 0000000..12a21c7 --- /dev/null +++ b/railiance_fabric/graph.py @@ -0,0 +1,264 @@ +from __future__ import annotations + +import json +import re +from collections import defaultdict +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from .loader import load_declarations +from .model import Declaration + + +@dataclass(frozen=True) +class ConsumerMatch: + consumer_service_id: str + dependency_id: str + required_capability_type: str + provider_capability_id: str = "" + provider_interface_id: str = "" + status: str = "" + + +@dataclass +class FabricGraph: + declarations: list[Declaration] + load_errors: list[tuple[Path, str]] = field(default_factory=list) + + def __post_init__(self) -> None: + self.by_id: dict[str, Declaration] = {} + self.by_kind: dict[str, list[Declaration]] = defaultdict(list) + for declaration in self.declarations: + if declaration.id and declaration.id not in self.by_id: + self.by_id[declaration.id] = declaration + self.by_kind[declaration.kind].append(declaration) + + self.services = {d.id: d for d in self.by_kind["ServiceDeclaration"]} + self.capabilities = {d.id: d for d in self.by_kind["CapabilityDeclaration"]} + self.interfaces = {d.id: d for d in self.by_kind["InterfaceDeclaration"]} + self.dependencies = {d.id: d for d in self.by_kind["DependencyDeclaration"]} + self.bindings = {d.id: d for d in self.by_kind["BindingAssertion"]} + self.bindings_by_dependency: dict[str, list[Declaration]] = defaultdict(list) + for binding in self.bindings.values(): + dependency_id = str(binding.spec.get("dependency_id", "")) + if dependency_id: + self.bindings_by_dependency[dependency_id].append(binding) + + def providers(self, capability: str) -> list[Declaration]: + if capability in self.capabilities: + return [self.capabilities[capability]] + return [ + declaration + for declaration in self.capabilities.values() + if declaration.spec.get("capability_type") == capability + ] + + def consumers(self, target: str) -> list[ConsumerMatch]: + matches: list[ConsumerMatch] = [] + target_interface_type = self.interfaces.get(target, Declaration(Path(), {})).spec.get("interface_type") + + for dependency in self.dependencies.values(): + spec = dependency.spec + requires = spec.get("requires", {}) + dependency_matches = ( + target == dependency.id + or target == requires.get("capability_id") + or target == requires.get("capability_type") + or target == spec.get("interface", {}).get("type") + or (target_interface_type and target_interface_type == spec.get("interface", {}).get("type")) + ) + + dependency_bindings = self.bindings_by_dependency.get(dependency.id, []) + binding_matches = [ + binding + for binding in dependency_bindings + if target in { + binding.spec.get("provider_capability_id"), + binding.spec.get("provider_interface_id"), + } + ] + + if dependency_matches and not dependency_bindings: + matches.append( + ConsumerMatch( + consumer_service_id=str(spec.get("consumer_service_id", "")), + dependency_id=dependency.id, + required_capability_type=str(requires.get("capability_type", "")), + ) + ) + for binding in dependency_bindings: + if dependency_matches or binding in binding_matches: + matches.append( + ConsumerMatch( + consumer_service_id=str(spec.get("consumer_service_id", "")), + dependency_id=dependency.id, + required_capability_type=str(requires.get("capability_type", "")), + provider_capability_id=str(binding.spec.get("provider_capability_id", "")), + provider_interface_id=str(binding.spec.get("provider_interface_id", "")), + status=str(binding.spec.get("status", "")), + ) + ) + + return sorted(matches, key=lambda item: (item.consumer_service_id, item.dependency_id)) + + def unresolved_dependencies(self) -> list[Declaration]: + unresolved: list[Declaration] = [] + for dependency in self.dependencies.values(): + providers = self.matching_providers(dependency) + bindings = self.bindings_by_dependency.get(dependency.id, []) + has_missing_binding = any(binding.spec.get("status") in {"missing", "disputed"} for binding in bindings) + if not providers or has_missing_binding: + unresolved.append(dependency) + return sorted(unresolved, key=lambda item: item.id) + + def matching_providers(self, dependency: Declaration) -> list[Declaration]: + requires = dependency.spec.get("requires", {}) + capability_id = requires.get("capability_id") + if capability_id: + provider = self.capabilities.get(str(capability_id)) + return [provider] if provider is not None else [] + capability_type = requires.get("capability_type") + return [ + provider + for provider in self.capabilities.values() + if provider.spec.get("capability_type") == capability_type + ] + + def dependency_path_lines(self, service_id: str) -> list[str]: + if service_id not in self.services: + return [f"unknown service: {service_id}"] + + lines: list[str] = [] + def walk(current: str, indent: int, stack: list[str]) -> None: + prefix = " " * indent + if current in stack: + lines.append(f"{prefix}{current} (cycle)") + return + lines.append(f"{prefix}{current}") + deps = [ + dep + for dep in self.dependencies.values() + if dep.spec.get("consumer_service_id") == current + ] + if not deps: + lines.append(f"{prefix} no declared dependencies") + return + for dep in sorted(deps, key=lambda item: item.id): + required = dep.spec.get("requires", {}).get("capability_type", "") + lines.append(f"{prefix} requires {required}: {dep.id}") + bindings = self.bindings_by_dependency.get(dep.id, []) + if not bindings: + providers = self.matching_providers(dep) + if providers: + for provider in providers: + lines.append(f"{prefix} candidate {provider.id}") + else: + lines.append(f"{prefix} unresolved") + continue + for binding in sorted(bindings, key=lambda item: item.id): + provider_id = str(binding.spec.get("provider_capability_id", "")) + provider = self.capabilities.get(provider_id) + provider_service = provider.spec.get("service_id") if provider else "" + status = binding.spec.get("status", "") + lines.append(f"{prefix} {status} -> {provider_id}") + if provider_service and provider_service != current: + walk(str(provider_service), indent + 3, stack + [current]) + + walk(service_id, 0, []) + return lines + + def blast_radius(self, interface: str) -> list[ConsumerMatch]: + if interface in self.interfaces: + return [ + match + for match in self.consumers(interface) + if match.provider_interface_id == interface + ] + return [ + match + for match in self.consumers(interface) + if self.dependencies[match.dependency_id].spec.get("interface", {}).get("type") == interface + ] + + def to_export(self) -> dict[str, Any]: + nodes: list[dict[str, Any]] = [] + edges: list[dict[str, str]] = [] + + for declaration in sorted(self.declarations, key=lambda item: (item.kind, item.id)): + nodes.append( + { + "id": declaration.id, + "kind": declaration.kind, + "name": declaration.metadata.get("name", declaration.id), + "repo": declaration.metadata.get("repo", ""), + "domain": declaration.metadata.get("domain", ""), + "lifecycle": declaration.spec.get("lifecycle", ""), + } + ) + + for service in self.services.values(): + for capability_id in service.spec.get("provides_capabilities", []): + edges.append({"from": service.id, "to": capability_id, "type": "provides"}) + for interface_id in service.spec.get("exposes_interfaces", []): + edges.append({"from": service.id, "to": interface_id, "type": "exposes"}) + + for capability in self.capabilities.values(): + for interface_id in capability.spec.get("interface_ids", []): + edges.append({"from": capability.id, "to": interface_id, "type": "available_via"}) + + for dependency in self.dependencies.values(): + consumer = str(dependency.spec.get("consumer_service_id", "")) + if consumer: + edges.append({"from": consumer, "to": dependency.id, "type": "consumes"}) + for binding in self.bindings_by_dependency.get(dependency.id, []): + edges.append( + { + "from": dependency.id, + "to": str(binding.spec.get("provider_capability_id", "")), + "type": f"binds:{binding.spec.get('status', '')}", + } + ) + interface_id = str(binding.spec.get("provider_interface_id", "")) + if interface_id: + edges.append({"from": dependency.id, "to": interface_id, "type": "uses_interface"}) + + return { + "apiVersion": "railiance.fabric/v1alpha1", + "kind": "FabricGraphExport", + "nodes": nodes, + "edges": edges, + } + + def to_json(self) -> str: + return json.dumps(self.to_export(), indent=2, sort_keys=True) + + def to_mermaid(self) -> str: + export = self.to_export() + lines = ["graph TD"] + for node in export["nodes"]: + node_id = _mermaid_id(node["id"]) + label = f"{node['name']}\\n{node['kind']}" + lines.append(f' {node_id}["{_escape_mermaid(label)}"]') + for edge in export["edges"]: + source = _mermaid_id(edge["from"]) + target = _mermaid_id(edge["to"]) + label = _escape_mermaid(edge["type"]) + lines.append(f" {source} -- {label} --> {target}") + return "\n".join(lines) + + +def build_graph(paths: list[Path]) -> FabricGraph: + declarations, load_errors = load_declarations(paths) + return FabricGraph(declarations=declarations, load_errors=load_errors) + + +def _mermaid_id(value: str) -> str: + cleaned = re.sub(r"[^A-Za-z0-9_]", "_", value) + if cleaned and cleaned[0].isdigit(): + cleaned = "_" + cleaned + return cleaned or "unknown" + + +def _escape_mermaid(value: str) -> str: + return value.replace('"', '\\"') diff --git a/railiance_fabric/loader.py b/railiance_fabric/loader.py new file mode 100644 index 0000000..6d0db98 --- /dev/null +++ b/railiance_fabric/loader.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +import yaml + +from .model import Declaration + +DECLARATION_DIRS = ("services", "capabilities", "interfaces", "dependencies", "bindings") + + +def repo_root() -> Path: + return Path(__file__).resolve().parents[1] + + +def declaration_files(path: Path) -> list[Path]: + path = path.resolve() + if path.is_file(): + return [path] + + fabric = path if path.name == "fabric" else path / "fabric" + files: list[Path] = [] + for directory in DECLARATION_DIRS: + root = fabric / directory + if root.is_dir(): + files.extend(sorted(root.glob("*.yaml"))) + files.extend(sorted(root.glob("*.yml"))) + return files + + +def load_yaml(path: Path) -> Any: + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def load_declarations(paths: list[Path]) -> tuple[list[Declaration], list[tuple[Path, str]]]: + declarations: list[Declaration] = [] + errors: list[tuple[Path, str]] = [] + seen_files: set[Path] = set() + + for raw in paths: + for path in declaration_files(raw): + if path in seen_files: + continue + seen_files.add(path) + try: + data = load_yaml(path) + except Exception as exc: + errors.append((path, str(exc))) + continue + if not isinstance(data, dict): + errors.append((path, "declaration must be a YAML mapping")) + continue + declarations.append(Declaration(path=path, data=data)) + + return declarations, errors diff --git a/railiance_fabric/model.py b/railiance_fabric/model.py new file mode 100644 index 0000000..598fb1d --- /dev/null +++ b/railiance_fabric/model.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class Declaration: + path: Path + data: dict[str, Any] + + @property + def kind(self) -> str: + return str(self.data.get("kind", "")) + + @property + def id(self) -> str: + return str(self.data.get("metadata", {}).get("id", "")) + + @property + def spec(self) -> dict[str, Any]: + spec = self.data.get("spec", {}) + return spec if isinstance(spec, dict) else {} + + @property + def metadata(self) -> dict[str, Any]: + meta = self.data.get("metadata", {}) + return meta if isinstance(meta, dict) else {} + + +@dataclass(frozen=True) +class Diagnostic: + severity: str + code: str + message: str + path: Path | None = None + + def format(self) -> str: + prefix = f"{self.severity} {self.code}" + if self.path is not None: + return f"{prefix} {self.path}: {self.message}" + return f"{prefix}: {self.message}" + + +@dataclass +class ValidationReport: + diagnostics: list[Diagnostic] = field(default_factory=list) + + @property + def errors(self) -> list[Diagnostic]: + return [d for d in self.diagnostics if d.severity == "ERROR"] + + @property + def warnings(self) -> list[Diagnostic]: + return [d for d in self.diagnostics if d.severity == "WARN"] + + def add(self, severity: str, code: str, message: str, path: Path | None = None) -> None: + self.diagnostics.append(Diagnostic(severity, code, message, path)) + + def summary(self) -> str: + return ( + f"Validation complete: {len(self.errors)} error(s), " + f"{len(self.warnings)} warning(s)" + ) diff --git a/railiance_fabric/validation.py b/railiance_fabric/validation.py new file mode 100644 index 0000000..1d5be72 --- /dev/null +++ b/railiance_fabric/validation.py @@ -0,0 +1,333 @@ +from __future__ import annotations + +from collections import defaultdict +from pathlib import Path +from typing import Any + +import jsonschema + +from .loader import load_declarations, load_yaml, repo_root +from .model import Declaration, ValidationReport + +SCHEMA_BY_KIND = { + "ServiceDeclaration": "service.schema.yaml", + "CapabilityDeclaration": "capability.schema.yaml", + "InterfaceDeclaration": "interface.schema.yaml", + "DependencyDeclaration": "dependency.schema.yaml", + "BindingAssertion": "binding.schema.yaml", +} + + +def validate_roots(paths: list[Path]) -> ValidationReport: + root = repo_root() + report = ValidationReport() + declarations, load_errors = load_declarations(paths) + for path, message in load_errors: + report.add("ERROR", "load.yaml", message, path) + + _validate_schema(root, declarations, report) + _validate_graph(root, declarations, report) + return report + + +def _validate_schema(root: Path, declarations: list[Declaration], report: ValidationReport) -> None: + schemas_dir = root / "schemas" + store = { + path.resolve().as_uri(): load_yaml(path) + for path in sorted(schemas_dir.glob("*.schema.yaml")) + } + + for declaration in declarations: + schema_name = SCHEMA_BY_KIND.get(declaration.kind) + if schema_name is None: + report.add( + "ERROR", + "schema.kind", + f"unknown declaration kind {declaration.kind!r}", + declaration.path, + ) + continue + + schema_path = schemas_dir / schema_name + schema = load_yaml(schema_path) + resolver = jsonschema.RefResolver( + base_uri=schema_path.resolve().as_uri(), + referrer=schema, + store=store, + ) + validator = jsonschema.Draft202012Validator(schema, resolver=resolver) + for error in sorted(validator.iter_errors(declaration.data), key=lambda e: list(e.path)): + location = ".".join(str(part) for part in error.path) or "" + report.add( + "ERROR", + "schema.invalid", + f"{location}: {error.message}", + declaration.path, + ) + + +def _validate_graph(root: Path, declarations: list[Declaration], report: ValidationReport) -> None: + by_id: dict[str, Declaration] = {} + by_kind: dict[str, list[Declaration]] = defaultdict(list) + + for declaration in declarations: + if not declaration.id: + continue + if declaration.id in by_id: + report.add( + "ERROR", + "graph.duplicate_id", + f"duplicate declaration id {declaration.id!r}", + declaration.path, + ) + else: + by_id[declaration.id] = declaration + by_kind[declaration.kind].append(declaration) + + cap_types, iface_types, expected_ifaces = _load_type_catalog(root, report) + services = {d.id: d for d in by_kind["ServiceDeclaration"]} + capabilities = {d.id: d for d in by_kind["CapabilityDeclaration"]} + interfaces = {d.id: d for d in by_kind["InterfaceDeclaration"]} + dependencies = {d.id: d for d in by_kind["DependencyDeclaration"]} + + for declaration in by_kind["CapabilityDeclaration"]: + spec = declaration.spec + capability_type = spec.get("capability_type") + if capability_type not in cap_types: + report.add("ERROR", "catalog.unknown_capability_type", f"unknown capability type {capability_type!r}", declaration.path) + _require_ref(report, declaration, "service_id", services, spec.get("service_id")) + for interface_id in spec.get("interface_ids", []): + _require_ref(report, declaration, "interface_ids", interfaces, interface_id) + + for declaration in by_kind["InterfaceDeclaration"]: + spec = declaration.spec + interface_type = spec.get("interface_type") + if interface_type not in iface_types: + report.add("ERROR", "catalog.unknown_interface_type", f"unknown interface type {interface_type!r}", declaration.path) + _require_ref(report, declaration, "service_id", services, spec.get("service_id")) + for capability_id in spec.get("capability_ids", []): + _require_ref(report, declaration, "capability_ids", capabilities, capability_id) + + for declaration in by_kind["ServiceDeclaration"]: + spec = declaration.spec + for capability_id in spec.get("provides_capabilities", []): + _require_ref(report, declaration, "provides_capabilities", capabilities, capability_id) + for interface_id in spec.get("exposes_interfaces", []): + _require_ref(report, declaration, "exposes_interfaces", interfaces, interface_id) + + for declaration in by_kind["DependencyDeclaration"]: + _validate_dependency( + declaration, + report, + services, + capabilities, + interfaces, + cap_types, + iface_types, + expected_ifaces, + ) + + for declaration in by_kind["BindingAssertion"]: + _validate_binding(declaration, report, dependencies, capabilities, interfaces) + + _detect_cycles(by_kind["DependencyDeclaration"], by_kind["BindingAssertion"], services, capabilities, report) + + +def _load_type_catalog(root: Path, report: ValidationReport) -> tuple[set[str], set[str], dict[str, set[str]]]: + cap_types: set[str] = set() + iface_types: set[str] = set() + expected_ifaces: dict[str, set[str]] = {} + + try: + cap_catalog = load_yaml(root / "catalog/capability-types.yaml") + for item in cap_catalog["spec"]["types"]: + cap_types.add(item["id"]) + expected_ifaces[item["id"]] = set(item.get("expected_interface_types", [])) + except Exception as exc: + report.add("ERROR", "catalog.load", f"cannot load capability catalog: {exc}") + + try: + iface_catalog = load_yaml(root / "catalog/interface-types.yaml") + for item in iface_catalog["spec"]["types"]: + iface_types.add(item["id"]) + except Exception as exc: + report.add("ERROR", "catalog.load", f"cannot load interface catalog: {exc}") + + return cap_types, iface_types, expected_ifaces + + +def _require_ref( + report: ValidationReport, + declaration: Declaration, + field: str, + collection: dict[str, Declaration], + value: Any, +) -> None: + if isinstance(value, str) and value in collection: + return + report.add("ERROR", "graph.missing_ref", f"{field} references unknown id {value!r}", declaration.path) + + +def _validate_dependency( + declaration: Declaration, + report: ValidationReport, + services: dict[str, Declaration], + capabilities: dict[str, Declaration], + interfaces: dict[str, Declaration], + cap_types: set[str], + iface_types: set[str], + expected_ifaces: dict[str, set[str]], +) -> None: + spec = declaration.spec + _require_ref(report, declaration, "consumer_service_id", services, spec.get("consumer_service_id")) + + requires = spec.get("requires", {}) + capability_type = requires.get("capability_type") + capability_id = requires.get("capability_id") + if capability_type not in cap_types: + report.add("ERROR", "catalog.unknown_capability_type", f"unknown required capability type {capability_type!r}", declaration.path) + if capability_id: + _require_ref(report, declaration, "requires.capability_id", capabilities, capability_id) + + interface_type = spec.get("interface", {}).get("type") + if interface_type: + if interface_type not in iface_types: + report.add("ERROR", "catalog.unknown_interface_type", f"unknown dependency interface type {interface_type!r}", declaration.path) + expected = expected_ifaces.get(str(capability_type), set()) + if expected and interface_type not in expected: + report.add( + "WARN", + "graph.unexpected_interface_type", + f"interface type {interface_type!r} is not expected for capability type {capability_type!r}", + declaration.path, + ) + + providers = _matching_providers(requires, capabilities) + if not providers: + report.add( + "ERROR", + "graph.missing_provider", + f"no provider capability found for {requires!r}", + declaration.path, + ) + + if _is_active_production(spec) and not declaration.metadata.get("source_links"): + report.add( + "ERROR", + "graph.missing_source_links", + "active production dependency requires metadata.source_links", + declaration.path, + ) + + if _is_active_production(spec): + viable = [provider for provider in providers if _provider_covers_dependency(provider.spec, spec)] + if providers and not viable: + report.add( + "ERROR", + "graph.incompatible_provider", + "no matching provider is active in the dependency environment", + declaration.path, + ) + + +def _validate_binding( + declaration: Declaration, + report: ValidationReport, + dependencies: dict[str, Declaration], + capabilities: dict[str, Declaration], + interfaces: dict[str, Declaration], +) -> None: + spec = declaration.spec + dependency = dependencies.get(str(spec.get("dependency_id"))) + provider = capabilities.get(str(spec.get("provider_capability_id"))) + _require_ref(report, declaration, "dependency_id", dependencies, spec.get("dependency_id")) + _require_ref(report, declaration, "provider_capability_id", capabilities, spec.get("provider_capability_id")) + if spec.get("provider_interface_id"): + _require_ref(report, declaration, "provider_interface_id", interfaces, spec.get("provider_interface_id")) + + if dependency and provider: + required_type = dependency.spec.get("requires", {}).get("capability_type") + provider_type = provider.spec.get("capability_type") + if required_type != provider_type: + report.add( + "ERROR", + "graph.binding_type_mismatch", + f"binding provider type {provider_type!r} does not satisfy dependency type {required_type!r}", + declaration.path, + ) + + +def _matching_providers(requires: dict[str, Any], capabilities: dict[str, Declaration]) -> list[Declaration]: + capability_id = requires.get("capability_id") + if capability_id: + provider = capabilities.get(str(capability_id)) + return [provider] if provider is not None else [] + capability_type = requires.get("capability_type") + return [ + declaration + for declaration in capabilities.values() + if declaration.spec.get("capability_type") == capability_type + ] + + +def _is_active_production(spec: dict[str, Any]) -> bool: + environments = set(spec.get("environments", [])) + return spec.get("lifecycle") == "active" and bool(environments & {"prod", "all"}) + + +def _provider_covers_dependency(provider_spec: dict[str, Any], dependency_spec: dict[str, Any]) -> bool: + if provider_spec.get("lifecycle") != "active": + return False + provider_envs = set(provider_spec.get("environments", [])) + dependency_envs = set(dependency_spec.get("environments", [])) + if "all" in provider_envs: + return True + if "all" in dependency_envs: + return {"dev", "staging", "prod"}.issubset(provider_envs) + return bool(provider_envs & dependency_envs) + + +def _detect_cycles( + dependencies: list[Declaration], + bindings: list[Declaration], + services: dict[str, Declaration], + capabilities: dict[str, Declaration], + report: ValidationReport, +) -> None: + dependency_by_id = {d.id: d for d in dependencies} + provider_by_dependency: dict[str, Declaration] = {} + for binding in bindings: + dep = str(binding.spec.get("dependency_id")) + provider_capability_id = str(binding.spec.get("provider_capability_id")) + provider = capabilities.get(provider_capability_id) + if dep and provider: + provider_by_dependency[dep] = provider + + edges: dict[str, set[str]] = defaultdict(set) + for dep_id, provider in provider_by_dependency.items(): + dependency = dependency_by_id.get(dep_id) + if dependency is None: + continue + consumer_service = dependency.spec.get("consumer_service_id") + provider_service = provider.spec.get("service_id") + if consumer_service in services and provider_service in services and consumer_service != provider_service: + edges[str(consumer_service)].add(str(provider_service)) + + visiting: set[str] = set() + visited: set[str] = set() + + def visit(node: str, stack: list[str]) -> None: + if node in visiting: + cycle = stack[stack.index(node):] + [node] + report.add("WARN", "graph.cycle", "service dependency cycle: " + " -> ".join(cycle)) + return + if node in visited: + return + visiting.add(node) + for target in edges.get(node, set()): + visit(target, stack + [target]) + visiting.remove(node) + visited.add(node) + + for node in sorted(edges): + visit(node, [node]) diff --git a/schemas/binding.schema.yaml b/schemas/binding.schema.yaml new file mode 100644 index 0000000..df27a9c --- /dev/null +++ b/schemas/binding.schema.yaml @@ -0,0 +1,52 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/binding.schema.yaml" +title: "BindingAssertion" +type: object +additionalProperties: false +required: + - apiVersion + - kind + - metadata + - spec +properties: + apiVersion: + $ref: "./common.schema.yaml#/$defs/apiVersion" + kind: + type: string + const: BindingAssertion + metadata: + $ref: "./common.schema.yaml#/$defs/metadata" + spec: + type: object + additionalProperties: false + required: + - lifecycle + - environments + - dependency_id + - provider_capability_id + - status + - rationale + properties: + lifecycle: + $ref: "./common.schema.yaml#/$defs/lifecycle" + environments: + $ref: "./common.schema.yaml#/$defs/environments" + dependency_id: + $ref: "./common.schema.yaml#/$defs/graphId" + provider_capability_id: + $ref: "./common.schema.yaml#/$defs/graphId" + provider_interface_id: + $ref: "./common.schema.yaml#/$defs/graphId" + status: + type: string + enum: + - exact + - compatible + - degraded + - missing + - disputed + rationale: + type: string + minLength: 1 + compatibility: + $ref: "./common.schema.yaml#/$defs/compatibility" diff --git a/schemas/capability.schema.yaml b/schemas/capability.schema.yaml new file mode 100644 index 0000000..044a3ed --- /dev/null +++ b/schemas/capability.schema.yaml @@ -0,0 +1,52 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/capability.schema.yaml" +title: "CapabilityDeclaration" +type: object +additionalProperties: false +required: + - apiVersion + - kind + - metadata + - spec +properties: + apiVersion: + $ref: "./common.schema.yaml#/$defs/apiVersion" + kind: + type: string + const: CapabilityDeclaration + metadata: + $ref: "./common.schema.yaml#/$defs/metadata" + spec: + type: object + additionalProperties: false + required: + - lifecycle + - environments + - description + - capability_type + - service_id + - criticality + - data_classification + properties: + lifecycle: + $ref: "./common.schema.yaml#/$defs/lifecycle" + environments: + $ref: "./common.schema.yaml#/$defs/environments" + description: + type: string + minLength: 1 + capability_type: + type: string + minLength: 1 + service_id: + $ref: "./common.schema.yaml#/$defs/graphId" + interface_ids: + type: array + items: + $ref: "./common.schema.yaml#/$defs/graphId" + criticality: + $ref: "./common.schema.yaml#/$defs/criticality" + data_classification: + $ref: "./common.schema.yaml#/$defs/dataClassification" + compatibility: + $ref: "./common.schema.yaml#/$defs/compatibility" diff --git a/schemas/common.schema.yaml b/schemas/common.schema.yaml new file mode 100644 index 0000000..4b6eef3 --- /dev/null +++ b/schemas/common.schema.yaml @@ -0,0 +1,164 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/common.schema.yaml" +title: "Railiance Fabric Common Definitions" +type: object + +$defs: + apiVersion: + type: string + const: "railiance.fabric/v1alpha1" + + graphId: + type: string + pattern: "^[a-z0-9][a-z0-9.-]*[a-z0-9]$" + minLength: 3 + maxLength: 160 + + lifecycle: + type: string + enum: + - planned + - active + - deprecated + - retired + + environment: + type: string + enum: + - dev + - staging + - prod + - all + + environments: + type: array + minItems: 1 + uniqueItems: true + items: + $ref: "#/$defs/environment" + + criticality: + type: string + enum: + - low + - medium + - high + - critical + + dataClassification: + type: string + enum: + - public + - internal + - confidential + - restricted + - secret + + authMethod: + type: string + enum: + - none + - oidc + - jwt + - mtls + - kubernetes_service_account + - openbao_token + - static_secret + - database_role + - sts_token + - api_key + - unknown + + sourceLink: + type: object + additionalProperties: false + required: + - label + properties: + label: + type: string + minLength: 1 + path: + type: string + minLength: 1 + url: + type: string + format: uri + ref: + type: string + minLength: 1 + anyOf: + - required: [path] + - required: [url] + - required: [ref] + + metadata: + type: object + additionalProperties: false + required: + - id + - name + - owner + - repo + - domain + properties: + id: + $ref: "#/$defs/graphId" + name: + type: string + minLength: 1 + owner: + type: string + minLength: 1 + repo: + type: string + minLength: 1 + domain: + type: string + minLength: 1 + source_links: + type: array + items: + $ref: "#/$defs/sourceLink" + + compatibility: + type: object + additionalProperties: false + properties: + version: + type: string + minLength: 1 + requires: + type: array + items: + type: string + minLength: 1 + compatible_with: + type: array + items: + $ref: "#/$defs/graphId" + breaks: + type: array + items: + type: string + minLength: 1 + notes: + type: string + minLength: 1 + + auth: + type: object + additionalProperties: false + required: + - method + properties: + method: + $ref: "#/$defs/authMethod" + audience: + type: string + minLength: 1 + scopes: + type: array + items: + type: string + minLength: 1 diff --git a/schemas/dependency.schema.yaml b/schemas/dependency.schema.yaml new file mode 100644 index 0000000..f51c2ca --- /dev/null +++ b/schemas/dependency.schema.yaml @@ -0,0 +1,82 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/dependency.schema.yaml" +title: "DependencyDeclaration" +type: object +additionalProperties: false +required: + - apiVersion + - kind + - metadata + - spec +properties: + apiVersion: + $ref: "./common.schema.yaml#/$defs/apiVersion" + kind: + type: string + const: DependencyDeclaration + metadata: + $ref: "./common.schema.yaml#/$defs/metadata" + spec: + type: object + additionalProperties: false + required: + - lifecycle + - environments + - consumer_service_id + - requires + - criticality + - data_classification + properties: + lifecycle: + $ref: "./common.schema.yaml#/$defs/lifecycle" + environments: + $ref: "./common.schema.yaml#/$defs/environments" + consumer_service_id: + $ref: "./common.schema.yaml#/$defs/graphId" + requires: + type: object + additionalProperties: false + required: + - capability_type + properties: + capability_type: + type: string + minLength: 1 + capability_id: + $ref: "./common.schema.yaml#/$defs/graphId" + interface: + type: object + additionalProperties: false + properties: + type: + type: string + minLength: 1 + version_constraint: + type: string + minLength: 1 + auth: + $ref: "./common.schema.yaml#/$defs/auth" + criticality: + $ref: "./common.schema.yaml#/$defs/criticality" + data_classification: + $ref: "./common.schema.yaml#/$defs/dataClassification" + fallback: + type: object + additionalProperties: false + required: + - mode + - description + properties: + mode: + type: string + enum: + - none + - manual + - degraded + - cached + - alternate_provider + description: + type: string + minLength: 1 + compatibility: + $ref: "./common.schema.yaml#/$defs/compatibility" diff --git a/schemas/interface.schema.yaml b/schemas/interface.schema.yaml new file mode 100644 index 0000000..4298b87 --- /dev/null +++ b/schemas/interface.schema.yaml @@ -0,0 +1,69 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/interface.schema.yaml" +title: "InterfaceDeclaration" +type: object +additionalProperties: false +required: + - apiVersion + - kind + - metadata + - spec +properties: + apiVersion: + $ref: "./common.schema.yaml#/$defs/apiVersion" + kind: + type: string + const: InterfaceDeclaration + metadata: + $ref: "./common.schema.yaml#/$defs/metadata" + spec: + type: object + additionalProperties: false + required: + - lifecycle + - environments + - description + - interface_type + - version + - service_id + - auth + - data_classification + properties: + lifecycle: + $ref: "./common.schema.yaml#/$defs/lifecycle" + environments: + $ref: "./common.schema.yaml#/$defs/environments" + description: + type: string + minLength: 1 + interface_type: + type: string + minLength: 1 + version: + type: string + minLength: 1 + service_id: + $ref: "./common.schema.yaml#/$defs/graphId" + capability_ids: + type: array + items: + $ref: "./common.schema.yaml#/$defs/graphId" + endpoint: + type: object + additionalProperties: false + properties: + url: + type: string + minLength: 1 + path: + type: string + minLength: 1 + notes: + type: string + minLength: 1 + auth: + $ref: "./common.schema.yaml#/$defs/auth" + data_classification: + $ref: "./common.schema.yaml#/$defs/dataClassification" + compatibility: + $ref: "./common.schema.yaml#/$defs/compatibility" diff --git a/schemas/service.schema.yaml b/schemas/service.schema.yaml new file mode 100644 index 0000000..accbfdf --- /dev/null +++ b/schemas/service.schema.yaml @@ -0,0 +1,44 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/service.schema.yaml" +title: "ServiceDeclaration" +type: object +additionalProperties: false +required: + - apiVersion + - kind + - metadata + - spec +properties: + apiVersion: + $ref: "./common.schema.yaml#/$defs/apiVersion" + kind: + type: string + const: ServiceDeclaration + metadata: + $ref: "./common.schema.yaml#/$defs/metadata" + spec: + type: object + additionalProperties: false + required: + - lifecycle + - environments + - description + properties: + lifecycle: + $ref: "./common.schema.yaml#/$defs/lifecycle" + environments: + $ref: "./common.schema.yaml#/$defs/environments" + description: + type: string + minLength: 1 + service_type: + type: string + minLength: 1 + provides_capabilities: + type: array + items: + $ref: "./common.schema.yaml#/$defs/graphId" + exposes_interfaces: + type: array + items: + $ref: "./common.schema.yaml#/$defs/graphId" diff --git a/schemas/state-hub-export.schema.yaml b/schemas/state-hub-export.schema.yaml new file mode 100644 index 0000000..5753b93 --- /dev/null +++ b/schemas/state-hub-export.schema.yaml @@ -0,0 +1,71 @@ +$schema: "https://json-schema.org/draft/2020-12/schema" +$id: "https://railiance.local/fabric/schemas/state-hub-export.schema.yaml" +title: "FabricGraphExport" +type: object +additionalProperties: false +required: + - apiVersion + - kind + - nodes + - edges +properties: + apiVersion: + type: string + const: railiance.fabric/v1alpha1 + kind: + type: string + const: FabricGraphExport + generated_at: + type: string + format: date-time + source: + type: object + additionalProperties: false + properties: + repo: + type: string + commit: + type: string + path: + type: string + nodes: + type: array + items: + type: object + additionalProperties: false + required: + - id + - kind + - name + - repo + - domain + - lifecycle + properties: + id: + $ref: "./common.schema.yaml#/$defs/graphId" + kind: + type: string + name: + type: string + repo: + type: string + domain: + type: string + lifecycle: + type: string + edges: + type: array + items: + type: object + additionalProperties: false + required: + - from + - to + - type + properties: + from: + $ref: "./common.schema.yaml#/$defs/graphId" + to: + $ref: "./common.schema.yaml#/$defs/graphId" + type: + type: string diff --git a/workplans/RAIL-FAB-WP-0001-ecosystem-graph-model.md b/workplans/RAIL-FAB-WP-0001-ecosystem-graph-model.md new file mode 100644 index 0000000..e53c189 --- /dev/null +++ b/workplans/RAIL-FAB-WP-0001-ecosystem-graph-model.md @@ -0,0 +1,319 @@ +--- +id: RAIL-FAB-WP-0001 +type: workplan +title: "Railiance Ecosystem Graph Model" +domain: railiance +repo: railiance-fabric +status: completed +owner: codex +topic_slug: railiance +planning_priority: high +planning_order: 1 +state_hub_workstream_id: "bd190990-8e68-49a3-9ce4-0ba89103ea54" +created: "2026-05-17" +updated: "2026-05-17" +--- + +# RAIL-FAB-WP-0001 - Railiance Ecosystem Graph Model + +## Goal + +Define and implement the first useful version of the Railiance ecosystem +graph: a repo-owned declaration model for services, capabilities, interfaces, +and dependencies, plus tooling to validate, inspect, and export that graph. + +The first outcome should be small but real: Railiance can declare that +OpenBao provides runtime secrets, NetKingdom provides identity/security +architecture, flex-auth provides authorization decisions, CNPG provides +PostgreSQL, and consumers can declare the exact capability/interface +requirements they rely on. + +## Context + +Railiance is moving from independent service setup into a connected ecosystem. +Platform services, identity, authorization, object storage, app workloads, +automation, and observability are starting to interact. + +The current risk is that dependencies remain implicit: in docs, deployment +scripts, assumptions, and agent memory. That makes blast radius hard to see and +capability discovery fragile. + +This workplan creates the foundation for a flexible ecosystem graph where: + +- repos declare what they provide and consume +- capabilities are stable semantic contracts +- interfaces describe concrete integration surfaces +- dependencies capture consumer requirements +- bindings connect consumers to providers +- State Hub can ingest the result as a read model + +## Scope + +In scope: + +- define vocabulary for repo, service, capability, interface, dependency, and + binding +- define YAML schemas for repo-owned declarations +- create examples for current Railiance services and cross-domain integrations +- implement a local graph loader and validator +- implement basic discovery queries and graph export +- define State Hub ingestion shape without making State Hub the authoring + surface +- document adoption guidance for other repos + +Out of scope: + +- replacing State Hub workstream/task/capability catalog ownership +- deployment orchestration or GitOps +- service mesh runtime traffic discovery +- enforcing production rollout gates before the model is proven +- complete static code analysis across all repos + +## Initial Model + +Suggested declaration layout inside each participating repo: + +```text +fabric/ + services/*.yaml + capabilities/*.yaml + interfaces/*.yaml + dependencies/*.yaml +``` + +Suggested edge model: + +```text +Repository + produces Service + provides Capability + exposes Interface + consumes Dependency + +Dependency + requires Capability + optionally constrains Interface, version, environment, auth, tenant, + data_class, criticality, and fallback + +Binding + resolves Dependency -> Capability/Interface +``` + +## Tasks + +### T01 - Intent And Vocabulary Baseline + +```task +id: RAIL-FAB-WP-0001-T01 +status: done +priority: high +state_hub_task_id: "7a74b4cf-6e0e-468c-8426-7dd85c027a21" +``` + +Create `INTENT.md` and establish the initial vocabulary: repository, service, +capability, interface, dependency, and binding. + +Done when the repo has a clear intent document that distinguishes repo-owned +declarations from State Hub's read-model role. + +### T02 - Declaration Schema Design + +```task +id: RAIL-FAB-WP-0001-T02 +status: done +priority: high +state_hub_task_id: "eb4be5b3-7440-43ce-8af3-8da371cab8a5" +``` + +Define the first YAML schema set: + +- `ServiceDeclaration` +- `CapabilityDeclaration` +- `InterfaceDeclaration` +- `DependencyDeclaration` +- `BindingOverride` or `BindingAssertion` + +Include fields for: + +- id, name, owner, repo, domain +- lifecycle status: planned, active, deprecated, retired +- environment: dev, staging, prod, all +- interface type and version +- auth method +- data classification +- criticality +- compatibility constraints +- source links + +Done when schemas are documented and have example valid/invalid fixtures. + +### T03 - Core Capability Type Catalog + +```task +id: RAIL-FAB-WP-0001-T03 +status: done +priority: high +state_hub_task_id: "070098b9-53b6-42d2-a041-e98b5c5ddcc7" +``` + +Define the first catalog of capability and interface types. + +Initial capabilities: + +- runtime secrets +- IAM Profile issuer +- authorization decision service +- PostgreSQL database service +- Redis-compatible cache +- object storage +- object-storage credential vending +- audit/event sink +- scope generation + +Initial interface types: + +- HTTP API +- OIDC discovery +- Kubernetes Secret +- Kubernetes CRD +- Helm release +- CLI +- database connection +- object-storage bucket +- event stream +- policy package +- OpenBao KV v2 mount +- OpenBao dynamic credential role + +Done when the type catalog is explicit enough for examples to avoid ad hoc +strings. + +### T04 - Example Declarations For Core Services + +```task +id: RAIL-FAB-WP-0001-T04 +status: done +priority: high +state_hub_task_id: "cfbc8d6e-58a3-4359-baf4-369d3357f6f5" +``` + +Create seed declarations for known Railiance ecosystem capabilities: + +- OpenBao as `runtime-secrets` +- NetKingdom IAM Profile as identity contract +- key-cape as lightweight IAM Profile implementation +- flex-auth as authorization decision control plane +- Topaz as delegated PDP runtime +- CloudNativePG as PostgreSQL service +- Valkey as Redis-compatible cache +- object storage and STS credential vending as planned capabilities +- State Hub as coordination/read-model service +- repo-scoping as scope-generation provider + +Done when the first graph has real provider nodes and at least a few consumer +requirements. + +### T05 - Graph Loader And Validator + +```task +id: RAIL-FAB-WP-0001-T05 +status: done +priority: high +state_hub_task_id: "59e46f73-04ba-464c-acbb-767169525d43" +``` + +Implement a local tool that loads declarations from one or more repo roots, +validates schema conformance, and builds an in-memory graph. + +Validation should catch: + +- duplicate ids +- missing provider capability for required dependencies +- unknown interface or capability types +- incompatible environment/status constraints +- missing source links for active production dependencies +- circular dependency warnings where relevant + +Done when `railiance-fabric validate ` can run against the seed +examples and produce useful diagnostics. + +### T06 - Discovery Queries And Exports + +```task +id: RAIL-FAB-WP-0001-T06 +status: done +priority: medium +state_hub_task_id: "ee50217c-8cdc-4c18-bb15-9fed35599d6c" +``` + +Add initial query commands: + +- list providers for a capability +- list consumers of a capability/interface +- show dependency path for a service +- show missing or unresolved dependencies +- show blast radius for an interface version change +- export graph as JSON +- export graph as Mermaid for documentation + +Done when a human or agent can answer "who consumes OpenBao?" and "what +capabilities are missing?" from local files. + +### T07 - State Hub Integration Contract + +```task +id: RAIL-FAB-WP-0001-T07 +status: done +priority: medium +state_hub_task_id: "32587d7a-565d-4457-a7ed-61e6f16f781a" +``` + +Define how State Hub ingests Railiance Fabric graph exports. + +The integration contract must preserve the source-of-truth boundary: + +- repo declarations are authoritative +- Railiance Fabric validates and exports +- State Hub stores or displays the read model +- State Hub links graph nodes to topics, repos, workstreams, tasks, and + progress events where useful + +Done when there is a documented export shape and a proposed ingestion path for +State Hub. + +### T08 - Adoption Guide And First Repo Rollout + +```task +id: RAIL-FAB-WP-0001-T08 +status: done +priority: medium +state_hub_task_id: "c13b29a7-053c-4f16-ab71-75fba966897e" +``` + +Write an adoption guide and apply the first declarations to a small set of +repos. + +Suggested first rollout: + +- `railiance-platform` +- `net-kingdom` +- `flex-auth` +- `artifact-store` +- `repo-scoping` +- `the-custodian/state-hub` + +Done when another repo can add declarations without reading Railiance Fabric +source code. + +## Acceptance Criteria + +- `INTENT.md` clearly defines why Railiance Fabric exists. +- The repo contains a documented schema for services, capabilities, + interfaces, dependencies, and bindings. +- Seed declarations represent at least OpenBao, NetKingdom IAM, flex-auth, + CNPG, object storage, and State Hub. +- A local validation command can detect missing providers and type mistakes. +- A local query can list providers and consumers for a capability. +- A graph export can be consumed by State Hub without making State Hub the + authoring surface. +- Adoption guidance exists for adding declarations to other repos. diff --git a/workplans/RAIL-FAB-WP-0002-ecosystem-registry-service.md b/workplans/RAIL-FAB-WP-0002-ecosystem-registry-service.md new file mode 100644 index 0000000..0d0a8e6 --- /dev/null +++ b/workplans/RAIL-FAB-WP-0002-ecosystem-registry-service.md @@ -0,0 +1,184 @@ +--- +id: RAIL-FAB-WP-0002 +type: workplan +title: "Railiance Ecosystem Registry Service" +domain: railiance +repo: railiance-fabric +status: proposed +owner: codex +topic_slug: railiance +planning_priority: high +planning_order: 2 +created: "2026-05-17" +updated: "2026-05-17" +--- + +# RAIL-FAB-WP-0002 - Railiance Ecosystem Registry Service + +## Goal + +Create the first service interface for registering repositories and interacting +with the Railiance ecosystem model across repos. + +The service should make Fabric declarations queryable without requiring every +agent or dashboard to clone every repo and run the local CLI. It should remain +an indexed read model over repo-owned declarations, not a central editor for +those declarations. + +## Context + +RAIL-FAB-WP-0001 created the repo-owned graph declaration model, validator, +query CLI, and State Hub export contract. + +The next useful step is a service that can: + +- register participating repos +- ingest validated graph snapshots by repo and commit +- expose providers, consumers, unresolved dependencies, dependency paths, and + blast-radius queries over the combined ecosystem graph +- attach supporting artifacts such as CycloneDX SBOMs, OpenAPI contracts, + AsyncAPI contracts, and Score workload intent +- project graph data to State Hub and, later, Backstage or xRegistry-compatible + views + +## Direction + +Use `docs/ecosystem-registry-service.md` as the design baseline. + +The closest external comparison point is CNCF xRegistry because it defines an +extensible metadata registry model with document/API views, versioned resources, +filtering, import/export, and endpoint/schema/message extensions. Railiance +should keep an xRegistry-compatible projection path, while preserving the +Fabric graph model as the repo-native source of truth. + +## Scope + +In scope: + +- define the registry service API and storage model +- implement repository registration +- implement snapshot ingestion for `FabricGraphExport` +- reuse the existing Fabric loader, validator, graph builder, and query logic +- store validation results per repo and commit +- expose initial HTTP query endpoints matching current CLI queries +- expose State Hub export data from the latest accepted snapshots +- support artifact attachment metadata for OpenAPI, AsyncAPI, and CycloneDX +- document Backstage and xRegistry projection strategy + +Out of scope: + +- editing repo-owned `fabric/` declarations through the service +- provisioning or binding live infrastructure +- replacing State Hub planning, task, progress, or workplan state +- building a full developer portal +- runtime service mesh discovery +- mandatory Score adoption + +## Tasks + +### T01 - Service API And Storage Design + +```task +id: RAIL-FAB-WP-0002-T01 +status: proposed +priority: high +``` + +Define the API surface, storage tables, validation semantics, and snapshot +replacement rules. + +Done when the repo contains an implementation-ready service design that +identifies request/response shapes and storage ownership. + +### T02 - Service Scaffold + +```task +id: RAIL-FAB-WP-0002-T02 +status: proposed +priority: high +``` + +Create a lightweight HTTP service that reuses the existing Python loader, +validator, graph builder, and export model. + +Done when the service can start locally and expose a health endpoint. + +### T03 - Repository Registration + +```task +id: RAIL-FAB-WP-0002-T03 +status: proposed +priority: high +``` + +Add endpoints and storage for repository slug, repo URL, default branch, +optional State Hub repo id, and ingest configuration. + +Done when repos can be registered, listed, and fetched by slug. + +### T04 - Snapshot Ingestion + +```task +id: RAIL-FAB-WP-0002-T04 +status: proposed +priority: high +``` + +Add atomic ingestion for `FabricGraphExport` payloads keyed by repo and commit. + +Done when a valid export is accepted, invalid exports are rejected with useful +errors, and the latest accepted snapshot is queryable. + +### T05 - Ecosystem Query Endpoints + +```task +id: RAIL-FAB-WP-0002-T05 +status: proposed +priority: high +``` + +Expose providers, consumers, unresolved dependencies, dependency paths, and +blast-radius queries over the latest accepted snapshots. + +Done when HTTP responses match the local CLI answers for the same graph. + +### T06 - Artifact Attachment + +```task +id: RAIL-FAB-WP-0002-T06 +status: proposed +priority: medium +``` + +Support artifact metadata for CycloneDX SBOMs, OpenAPI contracts, AsyncAPI +contracts, and Score workload files. + +Done when artifacts can be linked to repos, services, or interfaces and surfaced +in graph node details. + +### T07 - State Hub Export + +```task +id: RAIL-FAB-WP-0002-T07 +status: proposed +priority: high +``` + +Expose State Hub export data from the registry's latest accepted snapshots. + +Done when State Hub can fetch the same graph shape documented in +`docs/state-hub-integration.md`. + +### T08 - Projection Strategy + +```task +id: RAIL-FAB-WP-0002-T08 +status: proposed +priority: medium +``` + +Document and, if small enough, prototype Backstage and xRegistry projections. + +Done when it is clear which Fabric nodes map to Backstage entities and which +parts of the registry can be exposed through xRegistry-style groups, resources, +and versions.