Establish Railiance Fabric graph model

This commit is contained in:
2026-05-17 19:47:37 +02:00
parent 9c1f4d1381
commit 19f9fddc35
89 changed files with 5007 additions and 2 deletions

40
.custodian-brief.md Normal file
View File

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

158
AGENTS.md Normal file
View File

@@ -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/<id>/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": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workstream_id` / `task_id` when not applicable.
### Update task status
```bash
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
-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/<task_id>" \
-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-<slug>.md`
**Archived location:** completed workplans may move to
`workplans/archived/YYMMDD-RAIL-FAB-WP-NNNN-<slug>.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: "<uuid>" # 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: "<uuid>" # 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/`)

232
INTENT.md Normal file
View File

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

View File

@@ -1,3 +1,51 @@
# repo-seed
# Railiance Fabric
A git repository template to bootstrap coulomb projects from.
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.

139
SCOPE.md Normal file
View File

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

11
catalog/README.md Normal file
View File

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

View File

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

View File

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

207
docs/adoption-guide.md Normal file
View File

@@ -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/<repo>-<service>.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/<repo>-<capability>.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/<repo>-<interface>.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/<repo>-<dependency>.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 <v2"
auth:
method: kubernetes_service_account
criticality: high
data_classification: secret
fallback:
mode: none
description: Service cannot start without runtime secrets.
```
Active production dependencies should include at least one source link.
## 6. Add A Binding Only When Needed
Most bindings can be computed by the graph loader from dependency requirements
and provider capabilities. Add a `BindingAssertion` when you need to pin,
override, dispute, or document a planned provider:
```yaml
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: your-repo.your-service.runtime-secrets-to-openbao
name: Runtime secrets binding
owner: your-repo
repo: your-repo
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: your-repo.your-service.needs-runtime-secrets
provider_capability_id: railiance-platform.openbao.runtime-secrets
provider_interface_id: railiance-platform.openbao.kv-v2
status: compatible
rationale: This service uses OpenBao KV v2 for runtime secrets.
```
## 7. Validate Locally
From the Railiance Fabric repo:
```bash
railiance-fabric validate /path/to/your-repo
```
During early bootstrapping:
```bash
PYTHONPATH=. python -m railiance_fabric.cli validate /path/to/your-repo
```
Useful discovery checks:
```bash
railiance-fabric providers runtime-secrets /path/to/your-repo
railiance-fabric consumers runtime-secrets /path/to/your-repo
railiance-fabric unresolved /path/to/your-repo
```
For multi-repo validation, pass multiple roots:
```bash
railiance-fabric validate /path/to/repo-a /path/to/repo-b
```
## 8. Export For State Hub
```bash
railiance-fabric export --format json /path/to/your-repo
```
State Hub should ingest that export as a read model. Do not edit declarations in
State Hub; change them in the owning repo and re-export.

252
docs/declaration-schema.md Normal file
View File

@@ -0,0 +1,252 @@
# Declaration Schema
Railiance Fabric declarations are small YAML documents owned by the repository
that provides or consumes the declared thing. The first schema version is
`railiance.fabric/v1alpha1`.
## File Layout
Participating repositories should use this layout:
```text
fabric/
services/*.yaml
capabilities/*.yaml
interfaces/*.yaml
dependencies/*.yaml
bindings/*.yaml
```
Railiance Fabric itself keeps reusable schemas in `schemas/` and examples in
`examples/declarations/`.
## Shared Shape
Every declaration has:
```yaml
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]
```
`metadata.id` is the stable graph identifier. Use lower-case dotted IDs that
begin with the owning repo slug when possible:
```text
<repo>.<service>
<repo>.<service>.<capability>
<repo>.<service>.<interface>
<repo>.<consumer>.<dependency>
```
## 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.

85
docs/discovery-queries.md Normal file
View File

@@ -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:<status>`, and `uses_interface`

View File

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

54
docs/first-rollout.md Normal file
View File

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

View File

@@ -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": "<git-sha>",
"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.

68
docs/type-catalog.md Normal file
View File

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

40
docs/validator.md Normal file
View File

@@ -0,0 +1,40 @@
# Validator
The first validator entry point is:
```bash
railiance-fabric validate <repo-or-file>...
```
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
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <v2"
auth:
method: kubernetes_service_account
audience: openbao
criticality: critical
data_classification: secret
fallback:
mode: none
description: flex-auth cannot start without runtime secrets.
compatibility:
requires:
- openbao-kv-v2-mount v1

View File

@@ -0,0 +1,29 @@
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
source_links:
- label: OpenBao KV mount manifest
path: manifests/openbao/kv-v2.yaml
spec:
lifecycle: active
environments: [dev, staging, prod]
description: KV v2 secret mount exposed to approved Railiance workloads.
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
compatibility:
version: v1
notes: Consumers must use workload service-account authentication.

View File

@@ -0,0 +1,20 @@
apiVersion: railiance.fabric/v1alpha1
kind: ServiceDeclaration
metadata:
id: railiance-platform.openbao
name: OpenBao
owner: railiance-platform
repo: railiance-platform
domain: railiance
source_links:
- label: OpenBao platform chart
path: charts/openbao/values.yaml
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

20
fabric/README.md Normal file
View File

@@ -0,0 +1,20 @@
# Seed Declarations
These declarations are the first repo-local seed graph for Railiance Fabric.
They model current and planned Railiance ecosystem providers:
- OpenBao runtime secrets
- NetKingdom IAM Profile contract
- key-cape IAM Profile implementation
- flex-auth authorization decisions
- Topaz delegated PDP runtime
- CloudNativePG PostgreSQL
- Valkey Redis-compatible cache
- artifact-store object storage and credential vending
- State Hub coordination read model
- repo-scoping scope generation
The files are intentionally small. They are seed data for graph loading,
validation, and State Hub export work, not a claim that every upstream repo has
already adopted Fabric declarations as source of truth.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: artifact-store.object-storage.runtime-secrets-to-openbao
name: artifact-store runtime secrets binding
owner: artifact-store
repo: artifact-store
domain: railiance
spec:
lifecycle: planned
environments: [dev, staging, prod]
dependency_id: artifact-store.object-storage.needs-runtime-secrets
provider_capability_id: railiance-platform.openbao.runtime-secrets
provider_interface_id: railiance-platform.openbao.kv-v2
status: compatible
rationale: Planned object-storage credential vending should use OpenBao for protected runtime secrets.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: flex-auth.api.iam-profile-to-key-cape
name: flex-auth IAM Profile binding
owner: flex-auth
repo: flex-auth
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: flex-auth.api.needs-iam-profile
provider_capability_id: key-cape.iam-profile.issuer
provider_interface_id: key-cape.iam-profile.oidc-discovery
status: compatible
rationale: key-cape is the lightweight IAM Profile implementation for Railiance workloads.

View File

@@ -0,0 +1,16 @@
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
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 OpenBao KV v2 as the runtime secrets provider.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: flex-auth.api.topaz-runtime-binding
name: flex-auth Topaz runtime binding
owner: flex-auth
repo: flex-auth
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: flex-auth.api.needs-topaz-runtime
provider_capability_id: flex-auth.topaz.authorization-runtime
provider_interface_id: flex-auth.topaz.http-api
status: exact
rationale: flex-auth delegates PDP execution to its Topaz runtime.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: the-custodian.state-hub.postgresql-to-cnpg
name: State Hub PostgreSQL binding
owner: the-custodian
repo: the-custodian
domain: custodian
spec:
lifecycle: active
environments: [all]
dependency_id: the-custodian.state-hub.needs-postgresql
provider_capability_id: railiance-platform.cnpg.postgresql
provider_interface_id: railiance-platform.cnpg.database-connection
status: compatible
rationale: State Hub persists coordination data in PostgreSQL, with CNPG as the Railiance database provider.

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: artifact-store.object-storage.credentials
name: Object-storage credential vending
owner: artifact-store
repo: artifact-store
domain: railiance
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Planned scoped credential vending for object-storage access.
capability_type: object-storage-credential-vending
service_id: artifact-store.storage-service
interface_ids:
- artifact-store.object-storage.sts
criticality: high
data_classification: secret

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: artifact-store.object-storage
name: Object storage
owner: artifact-store
repo: artifact-store
domain: railiance
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Planned object storage for artifacts and workload data.
capability_type: object-storage
service_id: artifact-store.storage-service
interface_ids:
- artifact-store.object-storage.bucket
criticality: high
data_classification: confidential

View File

@@ -0,0 +1,19 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: flex-auth.api.authorization-decisions
name: flex-auth authorization decisions
owner: flex-auth
repo: flex-auth
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Evaluates Railiance authorization requests and returns decision envelopes.
capability_type: authorization-decision-service
service_id: flex-auth.api
interface_ids:
- flex-auth.api.http-api
- flex-auth.api.policy-package
criticality: critical
data_classification: restricted

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: flex-auth.topaz.authorization-runtime
name: Topaz authorization runtime
owner: flex-auth
repo: flex-auth
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Delegated PDP runtime used for authorization policy evaluation.
capability_type: authorization-decision-service
service_id: flex-auth.topaz
interface_ids:
- flex-auth.topaz.http-api
criticality: critical
data_classification: restricted

View File

@@ -0,0 +1,19 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: key-cape.iam-profile.issuer
name: key-cape IAM Profile issuer
owner: key-cape
repo: key-cape
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Implements IAM Profile claim serving for Railiance workloads.
capability_type: iam-profile-issuer
service_id: key-cape.iam-profile
interface_ids:
- key-cape.iam-profile.http-api
- key-cape.iam-profile.oidc-discovery
criticality: critical
data_classification: restricted

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: net-kingdom.iam-profile.issuer
name: NetKingdom IAM Profile issuer
owner: net-kingdom
repo: net-kingdom
domain: railiance
spec:
lifecycle: active
environments: [all]
description: Defines the IAM Profile identity claims contract for Railiance.
capability_type: iam-profile-issuer
service_id: net-kingdom.iam-profile
interface_ids:
- net-kingdom.iam-profile.oidc-discovery
criticality: critical
data_classification: restricted

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-platform.cnpg.postgresql
name: CloudNativePG PostgreSQL
owner: railiance-platform
repo: railiance-platform
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Provides PostgreSQL databases and connection endpoints.
capability_type: postgresql-database-service
service_id: railiance-platform.cnpg
interface_ids:
- railiance-platform.cnpg.database-connection
criticality: high
data_classification: confidential

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-platform.openbao.runtime-secrets
name: Runtime secrets
owner: railiance-platform
repo: railiance-platform
domain: railiance
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

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-platform.valkey.cache
name: Valkey cache
owner: railiance-platform
repo: railiance-platform
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Provides Redis-compatible cache storage.
capability_type: redis-compatible-cache
service_id: railiance-platform.valkey
interface_ids:
- railiance-platform.valkey.database-connection
criticality: medium
data_classification: internal

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: repo-scoping.scope-generation
name: Repo scope generation
owner: repo-scoping
repo: repo-scoping
domain: custodian
spec:
lifecycle: active
environments: [all]
description: Generates repo scope, boundary, and usefulness descriptions.
capability_type: scope-generation
service_id: repo-scoping.scope-generator
interface_ids:
- repo-scoping.scope-generator.cli
criticality: medium
data_classification: internal

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: the-custodian.state-hub.coordination
name: State Hub coordination read model
owner: the-custodian
repo: the-custodian
domain: custodian
spec:
lifecycle: active
environments: [all]
description: Exposes repo, workstream, task, decision, and progress state for coordination.
capability_type: coordination-read-model
service_id: the-custodian.state-hub
interface_ids:
- the-custodian.state-hub.http-api
criticality: high
data_classification: internal

View File

@@ -0,0 +1,24 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: artifact-store.object-storage.needs-runtime-secrets
name: artifact-store runtime secrets dependency
owner: artifact-store
repo: artifact-store
domain: railiance
spec:
lifecycle: planned
environments: [dev, staging, prod]
consumer_service_id: artifact-store.storage-service
requires:
capability_type: runtime-secrets
interface:
type: openbao-kv-v2-mount
version_constraint: ">=v1 <v2"
auth:
method: kubernetes_service_account
criticality: high
data_classification: secret
fallback:
mode: none
description: Credential vending needs a protected source for signing and backend secrets.

View File

@@ -0,0 +1,27 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: flex-auth.api.needs-iam-profile
name: flex-auth IAM Profile dependency
owner: flex-auth
repo: flex-auth
domain: railiance
source_links:
- label: Seed dependency declaration
path: fabric/README.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: flex-auth.api
requires:
capability_type: iam-profile-issuer
interface:
type: oidc-discovery
version_constraint: ">=v1 <v2"
auth:
method: none
criticality: critical
data_classification: restricted
fallback:
mode: none
description: Authorization decisions require trusted IAM Profile claims.

View File

@@ -0,0 +1,28 @@
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: Seed dependency declaration
path: fabric/README.md
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 <v2"
auth:
method: kubernetes_service_account
audience: openbao
criticality: critical
data_classification: secret
fallback:
mode: none
description: flex-auth cannot start without runtime secrets.

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: flex-auth.api.needs-topaz-runtime
name: flex-auth Topaz runtime dependency
owner: flex-auth
repo: flex-auth
domain: railiance
source_links:
- label: Seed dependency declaration
path: fabric/README.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: flex-auth.api
requires:
capability_type: authorization-decision-service
capability_id: flex-auth.topaz.authorization-runtime
interface:
type: http-api
version_constraint: ">=v1 <v2"
auth:
method: oidc
criticality: high
data_classification: restricted
fallback:
mode: degraded
description: flex-auth can fall back to limited local policy checks only for non-critical paths.

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: the-custodian.state-hub.needs-postgresql
name: State Hub PostgreSQL dependency
owner: the-custodian
repo: the-custodian
domain: custodian
source_links:
- label: Seed dependency declaration
path: fabric/README.md
spec:
lifecycle: active
environments: [all]
consumer_service_id: the-custodian.state-hub
requires:
capability_type: postgresql-database-service
capability_id: railiance-platform.cnpg.postgresql
interface:
type: database-connection
version_constraint: ">=16 <17"
auth:
method: database_role
criticality: critical
data_classification: confidential
fallback:
mode: none
description: State Hub cannot persist coordination state without PostgreSQL.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

20
pyproject.toml Normal file
View File

@@ -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*"]

View File

@@ -0,0 +1,5 @@
"""Railiance Fabric graph tooling."""
__all__ = ["__version__"]
__version__ = "0.1.0"

183
railiance_fabric/cli.py Normal file
View File

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

264
railiance_fabric/graph.py Normal file
View File

@@ -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('"', '\\"')

View File

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

65
railiance_fabric/model.py Normal file
View File

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

View File

@@ -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 "<root>"
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])

View File

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

View File

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

164
schemas/common.schema.yaml Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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