Compare commits

...

79 Commits

Author SHA1 Message Date
cbfb3d0f74 Draft capability entry (reuse-surface REUSE-WP-0017-T04, cohort 3)
Honest first-pass maturity vector grounded in README/docs/tests present
in this repo; no invented evidence. Flagged for human review before
publish. See reuse-surface history/2026-07-06-coverage-classification.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 19:50:53 +02:00
20316c533f Recorded decision 2026-07-04 00:31:34 +02:00
58a2cfac5c Regenerate agent instructions: workstream -> workplan terminology
Registration guidance now prescribes file-first + fix-consistency (C-06)
instead of manual create_workplan/create_workstream calls; progress-event
examples use workplan_id; legacy field names annotated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 01:47:46 +02:00
7312ed3767 Archive closed workplans to workplans/archived/ (ADR-001)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 00:25:41 +02:00
f9e661ec69 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-07-02:
  - update .custodian-brief.md for railiance-fabric
2026-07-02 00:24:01 +02:00
f57a5c1dce Repo hygiene: fill stack-and-commands, normalize workplan statuses
- Fill .claude/rules/stack-and-commands.md (was an empty TODO template)
- Normalize workplan frontmatter statuses to canonical vocabulary
  (completed/done -> finished) per ADR-001

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 00:21:49 +02:00
757770b42f Record whynot-design publish lane decisions 2026-07-01 20:07:20 +02:00
635521406f Normalize agent instructions and workplan frontmatter (STATE-WP-0067)
- Align agent files with on-disk workplan prefixes (infer from workplan ids)
- Set workplan domain to registered domain_slug; add topic_slug where applicable
- Repair frontmatter delimiter formatting; migrate legacy task status literals
- Regenerate AGENTS.md, CLAUDE.md, and .claude/rules from State Hub templates
2026-06-22 23:16:28 +02:00
2afeb86ed2 Add .repo-classification.yaml (CUST-WP-0050 T11 agent first-pass) 2026-06-22 17:47:41 +02:00
caa1e4100d Add credential routing instructions for all agent runtimes
Propagate shared credential-routing section (Codex, Claude, Grok, llm-connect)
from state-hub template via scripts/propagate_credential_routing.py.
2026-06-18 22:48:39 +02:00
dae9e3409a Add capability registry index scaffold (REUSE-WP-0014-T07 B05) 2026-06-16 01:59:28 +02:00
e23ed0c06b Document semantic attractors concept 2026-06-06 00:52:21 +02:00
fe2e98e2a4 Track generated State Hub decision log 2026-06-05 22:23:38 +02:00
7cb943ed41 Finish zone drag verification workplan 2026-06-05 21:38:03 +02:00
31841d0f1c chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-05:
  - update .custodian-brief.md for railiance-fabric
2026-06-05 21:36:10 +02:00
40b409cd92 Sync fabric workplan IDs 2026-06-05 17:12:48 +02:00
0b5d295800 Declare forge graph contracts 2026-06-05 17:10:31 +02:00
5dff7f14da chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-06-05:
  - update .custodian-brief.md for railiance-fabric
2026-06-05 17:07:16 +02:00
5e89f6a075 chore: add graph explorer make target 2026-06-03 17:24:58 +02:00
f09f110e77 feat: add zone layout selector 2026-05-25 03:17:23 +02:00
558e0dc157 feat: stabilize graph zone containers 2026-05-25 02:08:45 +02:00
0f7b7d1fed feat: add draggable graph zones 2026-05-25 01:09:05 +02:00
9b612447ca docs: finish zone entity workplan 2026-05-25 00:43:34 +02:00
a7a22a673f feat: prototype graph zone collapse 2026-05-25 00:40:13 +02:00
296ac051a7 feat: persist graph explorer zone state 2026-05-25 00:27:10 +02:00
1ad432270b feat: show zone resolver diagnostics 2026-05-25 00:09:18 +02:00
f1cd74dc7b feat: resolve graph explorer zones from definitions 2026-05-25 00:00:28 +02:00
d060b1c896 feat: add zone visualization resolver 2026-05-24 18:10:30 +02:00
fdee0b0855 chore: activate zone visualization workplan 2026-05-24 17:59:18 +02:00
f41d0da831 docs: define zone visualization engine workplan 2026-05-24 17:56:40 +02:00
5a2d987b6a feat: render graph explorer zone boundaries 2026-05-24 17:02:12 +02:00
ea81533172 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 17:00:39 +02:00
c26a725f92 docs: add zone boundary overlay workplan 2026-05-24 16:24:00 +02:00
8176d88926 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 16:22:31 +02:00
ff1c4ce05b feat: add deployment zone overlays 2026-05-24 15:55:05 +02:00
62236f6453 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 15:50:37 +02:00
d9f1f3f71a feat: canonicalize duplicate repo identities 2026-05-24 14:08:40 +02:00
5c9c2f281e chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 14:07:24 +02:00
ea2fa1203b docs: define deployment zone overlays 2026-05-24 12:58:18 +02:00
e60e8d5bb4 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 12:56:36 +02:00
735867392e feat: bootstrap accountability-root fabric snapshot 2026-05-24 10:41:29 +02:00
7956415924 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 10:39:46 +02:00
355b7be66a feat: compare accountability update deltas 2026-05-24 10:05:39 +02:00
071a4c49e3 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 10:01:39 +02:00
a55f1a45d6 feat: resolve accountability ownership reviews 2026-05-24 09:53:44 +02:00
c27d71a511 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 09:52:16 +02:00
ab7e0ccab1 feat: persist accountability evidence identities 2026-05-24 09:38:57 +02:00
26f1913d51 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 09:37:26 +02:00
999f90dcbe feat: collect accountability root evidence 2026-05-24 03:11:47 +02:00
43d3866b18 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 03:10:45 +02:00
a1bd9df8e4 feat: define accountability root manifest 2026-05-24 03:00:29 +02:00
455362153d chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 02:59:03 +02:00
f4aa4aa996 docs: finish financial fabric reset guidance 2026-05-24 01:37:05 +02:00
9717b18b02 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 01:35:01 +02:00
13188a6ae1 feat: seed railiance financial baseline 2026-05-24 01:20:07 +02:00
7d9b49764b chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 01:18:25 +02:00
63379ca329 feat: version state hub fabric export contract 2026-05-24 01:06:49 +02:00
be0299e528 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 01:05:49 +02:00
3a47a92729 feat: validate financial fabric graph exports 2026-05-24 00:57:14 +02:00
3318d2c1b9 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 00:56:00 +02:00
39762551c0 docs: audit financial fabric vnext contract 2026-05-24 00:42:39 +02:00
0874446994 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 00:38:19 +02:00
7db9c3cbf2 docs: define financial fabric architecture 2026-05-24 00:25:59 +02:00
a1e99d9b34 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-24:
  - update .custodian-brief.md for railiance-fabric
2026-05-24 00:05:05 +02:00
b70e74fde5 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-23:
  - update .custodian-brief.md for railiance-fabric
2026-05-23 21:53:28 +02:00
d3fe1e3ed3 Finish WP-0016 after State Hub graph ingest 2026-05-23 21:52:47 +02:00
fd859998c4 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-23:
  - update .custodian-brief.md for railiance-fabric
2026-05-23 16:05:52 +02:00
3e181e58ff Record WP-0016 reset reingest outcome 2026-05-23 16:05:18 +02:00
f6dbf31db1 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-23:
  - update .custodian-brief.md for railiance-fabric
2026-05-23 14:54:24 +02:00
9c22d3e0df Add canon reset and reingest guardrails 2026-05-23 14:52:57 +02:00
653411ffb8 refactoring for canon conformity 2026-05-23 14:00:59 +02:00
0193c97094 Register canon-aligned graph reset workplan 2026-05-23 07:34:13 +02:00
01bc4f3efe Refine runtime entity taxonomy 2026-05-21 21:28:34 +02:00
072fa8f7a7 Collapse duplicate repository graph nodes 2026-05-21 01:04:22 +02:00
a8e67db9e9 Discover runtime topology facts 2026-05-21 00:25:45 +02:00
af4132c182 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-21:
  - update .custodian-brief.md for railiance-fabric
2026-05-21 00:24:14 +02:00
78e8d381c2 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-21:
  - update .custodian-brief.md for railiance-fabric
2026-05-21 00:05:40 +02:00
e3440df163 Close path-scoped duplicate identity workplan 2026-05-20 23:45:51 +02:00
36d4b895f9 chore(consistency): sync task status from DB [auto]
Updated by fix-consistency on 2026-05-20:
  - update .custodian-brief.md for railiance-fabric
2026-05-20 23:44:21 +02:00
154 changed files with 26282 additions and 315 deletions

View File

@@ -0,0 +1,50 @@
# Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=railiance-fabric` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes**`warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`

View File

@@ -1,37 +1,41 @@
## First Session Protocol
Triggered when `get_domain_summary("railiance")` shows **no workstreams**.
Triggered when `get_domain_summary("financials")` shows **no workplans**.
The project is registered but work has not yet been structured.
**Step 1 — Read, don't write**
- `~/the-custodian/canon/projects/railiance/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/railiance/roadmap_v0.1.md` — planned phases
- `~/the-custodian/canon/projects/financials/project_charter_v0.1.md` — purpose, scope
- `~/the-custodian/canon/projects/financials/roadmap_v0.1.md` — planned phases
- Scan repo root: README, directory structure, existing code or docs
**Step 2 — Survey in-progress work**
Look for TODOs, open branches, half-finished files. Note done vs. started but incomplete.
**Step 3 — Propose workstreams to Bernd**
Propose 13 workstreams — each a coherent strand, weeks to months, anchored to a
**Step 3 — Propose workplans to Bernd**
Propose 13 workplans — each a coherent strand, weeks to months, anchored to a
roadmap phase. **Wait for approval before creating.**
**Step 4 — Create workplan file first, then DB record (ADR-001)**
**Step 4 — Write the workplan file; fix-consistency registers it (ADR-001)**
```
workplans/railiance-fabric-WP-NNNN-<slug>.md ← write this first
workplans/RAILIANCE-WP-NNNN-<slug>.md ← write this, commit it
```
Then register in the hub:
```
create_workstream(topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38", title="...", owner="...", description="...")
create_task(workstream_id="<id>", title="...", priority="high|medium|low")
Then register by running the consistency check — do **not** call
`create_workplan`/`create_task` (or legacy `create_workstream`) yourself;
manual registration duplicates what C-06 creates from the file:
```bash
statehub fix-consistency --repo railiance-fabric
```
C-06 creates the hub workplan + tasks and writes `state_hub_workstream_id` /
`state_hub_task_id` back into the file (legacy field names, kept for
compatibility — they hold workplan/task IDs).
**Step 5 — Record the setup**
```
add_progress_event(
summary="First session: structured railiance into N workstreams, M tasks",
summary="First session: structured financials into N workplans, M tasks",
event_type="milestone",
topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38",
detail={"workstreams": [...], "tasks_created": M}
detail={"workplans": [...], "tasks_created": M}
)
```

View File

@@ -1,5 +1,5 @@
**Purpose:** railiance-fabric - (fill in purpose)
**Domain:** railiance
**Domain:** financials
**Repo slug:** railiance-fabric
**Topic ID:** ca369340-a64e-442e-98f1-a4fa7dc74a38

View File

@@ -1,6 +1,7 @@
## Session Protocol
State Hub: http://127.0.0.1:8000
Dev Hub (State Hub API): http://127.0.0.1:8000
MCP server name in `~/.claude.json`: `dev-hub`
**Step 1 — Orient**
@@ -10,7 +11,7 @@ cat .custodian-brief.md
```
Then call the MCP tool for richer cross-domain context when MCP tools are exposed:
```
get_domain_summary("railiance")
get_domain_summary("financials")
```
If MCP tools are unavailable in the current agent session, use the REST API:
```bash
@@ -39,11 +40,11 @@ curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
ls workplans/
```
For each file with `status: ready`, `active`, or `blocked`, note pending
`todo`/`in_progress` tasks.
`wait`/`todo`/`progress` tasks.
**Step 4 — Present brief**
1. **Active workstreams** for `railiance` — title, task counts, blocking decisions
1. **Active workplans** for `financials` — title, task counts, blocking decisions
2. **Pending tasks** from `workplans/` + any `[repo:railiance-fabric]` hub tasks
3. **Goal guidance** — if `goal_guidance` in summary:
- `needs_workplan`: surface as top action — *"Repo goal '{title}' has no workplan yet"*
@@ -51,33 +52,42 @@ For each file with `status: ready`, `active`, or `blocked`, note pending
4. **Suggested next action** — highest-priority open item
5. **SBOM status** — flag if `last_sbom_at` is unset for this repo
If no workstreams: follow First Session Protocol (`first-session.md`).
If no workplans: follow First Session Protocol (`first-session.md`).
**During work:** `record_decision()` · `add_progress_event()` · `resolve_decision()`
> State Hub is a *read model*. Bootstrap tools (`create_workstream`, `create_task`)
> are First Session Protocol only. Work structure belongs in repo files (ADR-001).
> State Hub is a *read model*. **Never register workplans or tasks by hand**
> (`create_workplan`, `create_task`, or the legacy `create_workstream`) — write
> the workplan file in `workplans/` and run `fix-consistency`; its C-06 check
> registers the workplan and its tasks in the hub and writes the IDs back into
> the file. Manual registration creates duplicates the moment fix-consistency
> runs. Work structure belongs in repo files (ADR-001).
>
> Terminology: "workstream" is the legacy name for workplan. Some API/frontmatter
> field names keep it for compatibility (`state_hub_workstream_id`,
> `workstream_id` params) — treat them as workplan IDs.
**Session close:**
With MCP tools:
```
add_progress_event(summary="...", topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38", workstream_id="<uuid>")
add_progress_event(summary="...", topic_id="ca369340-a64e-442e-98f1-a4fa7dc74a38", workplan_id="<uuid>")
```
Without MCP tools:
```bash
curl -s -X POST http://127.0.0.1:8000/progress/ \
-H "Content-Type: application/json" \
-d '{"topic_id":"ca369340-a64e-442e-98f1-a4fa7dc74a38","workstream_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
-d '{"topic_id":"ca369340-a64e-442e-98f1-a4fa7dc74a38","workplan_id":"<uuid>","event_type":"note","summary":"what changed","author":"codex"}'
```
If workplan files were modified, ensure the local copy is up to date first:
If workplan files were modified, ensure the local copy is up to date first,
then sync from the repo checkout:
```bash
git -C <repo_path> pull --ff-only
cd ~/state-hub && make fix-consistency REPO=railiance-fabric
git pull --ff-only
statehub fix-consistency
```
For repos where implementation runs on a remote machine (e.g. CoulombCore),
use the combined target which pulls before fixing:
use the pull-before-fix mode from any shell with the State Hub CLI:
```bash
cd ~/state-hub && make fix-consistency-remote REPO=railiance-fabric
statehub fix-consistency --repo railiance-fabric --remote
```
**C-15** (DB task ahead of file) is normal in multi-machine workflows — writeback
will sync the file to match DB. **C-16** (repo behind remote) blocks all writes

View File

@@ -1,19 +1,14 @@
## Stack
<!-- TODO: Fill in language, frameworks, and key dependencies -->
- **Language:**
- **Key deps:**
- **Language:** Python ≥3.12 (`railiance_fabric` package)
- **Key deps:** jsonschema, PyYAML; SQLite registry at `.railiance-fabric/registry.sqlite3`
## Dev Commands
```bash
# TODO: Fill in the standard commands for this repo
# Install dependencies
# Run tests
# Lint / type check
# Build / package (if applicable)
python3 -m pytest tests/ # run the test suite
make graph-explorer # registry-backed graph explorer (HOST/PORT/REGISTRY_DB overridable)
python3 -m railiance_fabric.server --db .railiance-fabric/registry.sqlite3
```
Local-only tooling — no production deploy surface in this repo.

View File

@@ -1,28 +1,45 @@
## Workplan Convention (ADR-001)
File location: `workplans/railiance-fabric-WP-NNNN-<slug>.md`
ID prefix: `RAILIANCE-WP`
File location: `workplans/RAILIANCE-WP-NNNN-<slug>.md`
ID prefix: `RAILIANCE-WP-`
Work items originate as files in this repo **before** being registered in the hub.
Canonical workplan/workstream frontmatter statuses are:
Canonical workplan frontmatter statuses are:
`proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, `archived`.
Use `proposed` for a newly drafted plan, `ready` after review against current
repo state, and `finished` when implementation is complete. `stalled` and
`needs_review` are derived health labels, not stored statuses.
Closed workplans may be moved to `workplans/archived/` with a completion-date
prefix: `YYMMDD-railiance-fabric-WP-NNNN-<slug>.md`. The frontmatter id remains
prefix: `YYMMDD-RAILIANCE-WP-NNNN-<slug>.md`. The frontmatter id remains
unchanged; the prefix is only for quick visual reference.
Small opportunistic tasks discovered during another session use **Ad Hoc Tasks**:
`workplans/ADHOC-YYYY-MM-DD.md`, workstream slug `adhoc-YYYY-MM-DD`, and task ids
`workplans/ADHOC-YYYY-MM-DD.md`, workplan slug `adhoc-YYYY-MM-DD`, and task ids
`ADHOC-YYYY-MM-DD-T01`, `T02`, etc. Use adhocs only for low-risk work completed
directly. Promote anything requiring analysis, design, approval, dependencies, or
multiple planned phases into a normal workplan.
Ecosystem todos from other agents arrive as `[repo:railiance-fabric]` hub tasks —
visible at session start. Pick one up by creating the workplan file, then registering
the workstream.
visible at session start. Pick one up by creating the workplan file, committing,
and running `statehub fix-consistency` — C-06 registers the workplan in the hub.
Never register by hand with `create_workplan`/`create_workstream`.
Task blocks use this shape:
```task
id: RAILIANCE-WP-NNNN-T01
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
```
Status progression is `todo``progress``done`; use `wait` for waiting or
blocked work and `cancel` for stopped work.
Workplan frontmatter carries `state_hub_workstream_id` — a legacy field name
kept for compatibility ("workstream" is the old term for workplan); it holds
the hub workplan id and is written by fix-consistency. Do not edit or rename it.
<!-- Ralph Loop rules and HEUREKA sequence: ~/.claude/CLAUDE.md — do not duplicate here -->

View File

@@ -1,25 +1,18 @@
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
# Custodian Brief — railiance-fabric
**Domain:** railiance
**Last synced:** 2026-05-20 21:37 UTC
**Domain:** financials
**Last synced:** 2026-07-01 22:24 UTC
**State Hub:** http://127.0.0.1:8000 *(adjust if running on a remote machine)*
## Active Workstreams
### Path Scoped Duplicate Identity
Progress: 0/4 done | workstream_id: `bf80900b-606c-442c-aa15-d6272d73a8d6`
**Open tasks:**
- · T01 - Add Path-Scoped Reconciliation Coverage `8d9cb1c2`
- · T02 - Implement Path-Aware Duplicate Detection `7d4d439d`
- · T03 - Verify Controlled Rescan `0d6cad1d`
- · T04 - Rollout Recommendation `4be31147`
*(none — repo may need first-session setup)*
---
## MCP Orientation (when available)
If the state-hub MCP server is reachable, call:
`get_domain_summary("railiance")`
`get_domain_summary("financials")`
This provides richer cross-domain context.
If the MCP call fails, use this file as your orientation source.

17
.repo-classification.yaml Normal file
View File

@@ -0,0 +1,17 @@
repo_classification:
standard: Repo Classification Standard
version: '1.0'
classified_at: '2026-06-22'
classified_by: agent
category: project
domain: financials
secondary_domains: []
capability_tags:
- platform
- operations
business_stake:
- technology
- operations
business_mechanics:
- coordination
- operation

View File

@@ -4,7 +4,7 @@
**Purpose:** railiance-fabric - (fill in purpose)
**Domain:** railiance
**Domain:** financials
**Repo slug:** railiance-fabric
**Topic ID:** `ca369340-a64e-442e-98f1-a4fa7dc74a38`
**Workplan prefix:** `RAILIANCE-WP-`
@@ -20,6 +20,12 @@ there is no MCP server for Codex agents.
|---------|-----|
| Local workstation | `http://127.0.0.1:8000` |
| Remote via tunnel | `http://127.0.0.1:18000` |
| Optional local edge relay | http://127.0.0.1:18080 |
When an operator has enabled the edge relay, set API_BASE to the relay URL.
Queueable writes return an explicit queued receipt if the central hub is
unreachable. Treat that as pending local evidence, then ask the operator to run
statehub outbox status/replay after connectivity returns.
### Orient at session start
@@ -27,8 +33,8 @@ there is no MCP server for Codex agents.
# 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" \
# Active workplans for this domain
curl -s "http://127.0.0.1:8000/workplans/?topic_id=ca369340-a64e-442e-98f1-a4fa7dc74a38&status=active" \
| python3 -m json.tool
# Check inbox
@@ -51,20 +57,20 @@ curl -s -X POST http://127.0.0.1:8000/progress/ \
"summary": "what was done",
"event_type": "note",
"author": "codex",
"workstream_id": "<uuid>",
"workplan_id": "<uuid>",
"task_id": "<uuid>"
}'
```
Omit `workstream_id` / `task_id` when not applicable.
Omit `workplan_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
-d '{"status": "progress"}'
# values: wait | todo | progress | done | cancel
```
### Flag a task for human review
@@ -80,10 +86,10 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
## Session Protocol
**Start:**
1. `cat .custodian-brief.md` — domain goal and open workstreams (offline-safe)
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
2. Check inbox: `GET /messages/?to_agent=railiance-fabric&unread_only=true`; mark read
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
4. Check blocked tasks: `GET /tasks/?needs_human=true`
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
**During work:**
- Update task statuses in workplan files as tasks progress
@@ -92,12 +98,69 @@ curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
**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
`~/state-hub`:
3. After workplan file changes, run:
```bash
make fix-consistency REPO=railiance-fabric
statehub fix-consistency
```
This syncs task status from files into the hub DB.
Coding agents should run this directly; ask the operator only if the CLI or
State Hub API is unavailable. This syncs task status from files into the hub DB.
---
## Credential and access routing
**Audience:** Codex, Claude Code, Grok, and custodian agents that call **llm-connect**
for inference. Run this check **before** requesting secrets, API keys, SSH access,
login tokens, or database passwords — in any repo, not only `ops-warden`.
ops-warden **issues SSH certificates only** (`warden sign`, `cert_command`). Every
other credential need belongs to another subsystem. **Do not** message
`ops-warden` on State Hub expecting a secret value; the reply is a pointer, not a key.
### Lookup (do this first)
```bash
warden route find "<describe your need>" --json
warden route show <catalog-id> --json
```
Requires the `warden` CLI from `~/ops-warden` (`uv tool install .` or `uv run warden`).
| Agent runtime | How to orient |
| --- | --- |
| **Codex / Grok** (shell, HTTP State Hub) | `warden route` commands above; inbox `to_agent=railiance-fabric` is for coordination, not secret vending |
| **Claude Code** (MCP when available) | `get_domain_summary("custodian")` for workplans; **still** use `warden route` for credential ownership |
| **llm-connect** (inference service) | Never put secret retrieval in prompts; route custody to OpenBao/operator paths surfaced by `warden route` |
### Quick routing table
| I need… | Owner | ops-warden executes? |
| --- | --- | --- |
| SSH cert (`adm`/`agt`/`atm`) | ops-warden | **Yes** — `warden sign` |
| API key, DB password, provider token | OpenBao (`railiance-platform`) | No — route only |
| Login / OIDC / MFA | key-cape / Keycloak | No — route only |
| Authorization decision | flex-auth | No — route only |
| activity-core → issue-core emission | activity-core + issue-core | No — `warden route show activity-core-issue-sink` |
| SSH tunnel | ops-bridge (+ `cert_command` from warden) | No — route only |
### Anti-patterns (do not do these)
- `POST /messages/` to `ops-warden` asking for `ISSUE_CORE_API_KEY`, `OPENROUTER_API_KEY`, etc.
- Inventing `warden secret`, `warden login`, `warden bao`, `warden tunnel` — they do not exist
- Pasting secrets into Git, State Hub, workplans, logs, or chat
### Other capabilities (reuse-surface)
Non-credential capabilities are usually discovered through **reuse-surface** federation
(`reuse-surface` registry / `capability.*` indexes). Credential routing is inlined in
every repo's agent instructions because it is high-frequency, high-risk, and easy to
get wrong.
**Canon:** `~/ops-warden/wiki/CredentialRouting.md` · catalog `~/ops-warden/registry/routing/catalog.yaml`
<!-- REPO-AGENTS-EXTENSIONS -->
<!-- Append repo-specific agent instructions below this marker.
The state-hub template sync preserves content after this line. -->
---
@@ -124,7 +187,7 @@ anything needing analysis, design, approval, dependencies, or multiple phases.
id: RAILIANCE-WP-NNNN
type: workplan
title: "..."
domain: railiance
domain: financials
repo: railiance-fabric
status: proposed | ready | active | blocked | backlog | finished | archived
owner: codex
@@ -146,7 +209,7 @@ derived health labels, not frontmatter statuses.
` ` `task
id: RAILIANCE-WP-NNNN-T01
status: todo | in_progress | done | blocked
status: wait | todo | progress | done | cancel
priority: high | medium | low
state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
` ` `
@@ -154,7 +217,7 @@ state_hub_task_id: "<uuid>" # written by fix-consistency — do not edit
Task description text.
```
Status progression: `todo` → `in_progress` → `done` (or `blocked`)
Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
To create a new workplan:
1. Write the file following the format above

View File

@@ -8,4 +8,5 @@
@.claude/rules/stack-and-commands.md
@.claude/rules/architecture.md
@.claude/rules/repo-boundary.md
@.claude/rules/credential-routing.md
@.claude/rules/agents.md

93
DECISIONS.md Normal file
View File

@@ -0,0 +1,93 @@
# Decision Log
_Auto-generated by the Custodian State Hub._
## State Hub workstation Postgres migration approval
**Date:** 2026-06-03
**Decided by:** codex
Superseded by WP-0004 T09 cancellation and CUST-WP-0038 ownership of State Hub HA migration, including hostname, registry, exposure model, HA database/storage, restore drills, and production data migration approval.
---
## activity-core deployment architecture
**Date:** 2026-06-03
**Decided by:** codex
Superseded by completed WP-0004 outcome: activity-core was deployed as a K3s production service on railiance01 on 2026-05-22; the packaging architecture question is no longer blocking this workplan.
---
## Review CCR-2026-0001 whynot-design npm publish token lane
**Date:** 2026-06-27
**Decided by:** human
APPROVE: scoped path and confirmed binding are acceptable
---
## Review CCR-2026-0001 corrected whynot-design npm publish token lane
**Date:** 2026-06-27
**Decided by:** human
APPROVE: We fixed the path using coulomb as the org/tenant.
---
## Forgejo hostname and exposure model
**Date:** 2026-07-02
**Decided by:** human
the hostname shall be forgejo.coulomb.social. The exposure model is private repos by default. We will use the transition from gitea to forgejo for closing down public access and establishing credential handling to have convenience when working with the repos. Gitea can and should remain reachable during transition.
---
## Forgejo SMTP and sender identity
**Date:** 2026-07-02
**Decided by:** human
We will use forgejo@coulomb.social
---
## Forgejo package registry scope
**Date:** 2026-07-02
**Decided by:** human
We should support the full range from the start.
---
## Forgejo Actions runner isolation model
**Date:** 2026-07-02
**Decided by:** human
we will try to go with isolated host runners, with least-privilege credential boundaries.
---
## Forgejo backup target and restore cadence
**Date:** 2026-07-02
**Decided by:** human
We will need to figure out the details, but i want to use backup.coulomb.social as the hostname with a backend we need to figure out yet.
---
## Forgejo cutover and rollback strategy
**Date:** 2026-07-02
**Decided by:** human
We will do a staged migration and lock the gitea repos of transitioned repos but keep gitea until everything has been transfered and just then start a 14day trial phase and after that retire gitea to the backup.
---

21
Makefile Normal file
View File

@@ -0,0 +1,21 @@
PYTHON ?= python3
REGISTRY_DB ?= .railiance-fabric/registry.sqlite3
HOST ?= 127.0.0.1
PORT ?= 8765
.DEFAULT_GOAL := help
.PHONY: help graph-explorer registry
help:
@printf "Available targets:\n"
@printf " make graph-explorer Start the registry-backed graph explorer on HOST:PORT.\n"
@printf " Defaults: HOST=$(HOST), PORT=$(PORT), REGISTRY_DB=$(REGISTRY_DB)\n"
@printf " make registry Alias for graph-explorer.\n"
graph-explorer:
@mkdir -p $(dir $(REGISTRY_DB))
@echo "Starting Railiance Fabric graph explorer at http://$(HOST):$(PORT)/ui/graph-explorer"
$(PYTHON) -m railiance_fabric.server --db "$(REGISTRY_DB)" --host "$(HOST)" --port "$(PORT)"
registry: graph-explorer

View File

@@ -1,11 +1,20 @@
# Railiance Fabric
Railiance Fabric defines the repo-owned declaration model for the Railiance
ecosystem graph.
Railiance Fabric models the durable infrastructure-responsibility graph of the
Railiance netkingdom.
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.
Fabric is bounded by financial and operational accountability: who pays for the
infrastructure, who is accountable for keeping it alive, and which durable
interfaces create value across fabric and subfabric boundaries.
It holds schemas, discovery tools, registry services, graph queries, and State
Hub export contracts for services, machines, repositories, deployables,
endpoints, ownership, dependencies, and bindings across Railiance deployment
realities.
See `docs/FabricDiscoveryAndUpdate.md` for the current architecture direction
for fabric boundaries, king/lord/tenant ownership, discovery, rebuilds, and
update loops.
## Validate Declarations
@@ -36,14 +45,35 @@ railiance-fabric blast-radius openbao-kv-v2-mount
railiance-fabric export --format json
railiance-fabric export --format mermaid
railiance-fabric export --format graph-explorer
railiance-fabric export --format financial
```
See `docs/discovery-queries.md` for command details.
## Financial Fabric Baseline
The current financial Fabric model starts from
`fabric/financial/railiance-netkingdom.yaml`. It defines the Railiance
netkingdom, the king, the current lord, the one active Railiance fabric, and
the default ownership rules used while Railiance still has one effective payer.
`railiance-fabric export --format financial` projects the current accepted
legacy graph into the `railiance.fabric/v1alpha2` / `financial-fabric-v1`
contract. Repo-local `fabric/` declarations remain useful evidence and
bootstrap data; they are not the long-term authority for external deployment,
ownership, tenant, or utility relations.
See `docs/financial-fabric-operator-guide.md` for the reset/rebuild refresh
loop and State Hub handoff.
## Adopt In Another Repo
See `docs/adoption-guide.md` for the declaration workflow and
`docs/first-rollout.md` for the initial Railiance repo rollout.
See `docs/adoption-guide.md` for the legacy declaration workflow and
`docs/first-rollout.md` for the initial Railiance repo rollout. Treat
repo-local declarations as self-description and discovery evidence. Financial
fabric membership, ownership, and cross-boundary utility relations are owned by
the accountability-root discovery model described in
`docs/FabricDiscoveryAndUpdate.md`.
## Next: Ecosystem Registry Service
@@ -51,7 +81,22 @@ See `docs/ecosystem-registry-service.md` for the standards comparison and
service direction for registering repos and interacting with the combined
ecosystem model. See `docs/registry-api.md` for the current registry HTTP API.
Start the first registry service slice with:
List available Make targets from the repo root:
```bash
make
```
Start the first registry service slice and graph explorer explicitly:
```bash
make graph-explorer
```
The target serves `http://127.0.0.1:8765/ui/graph-explorer` by default. Override
`PORT`, `HOST`, or `REGISTRY_DB` if the local port or database path differs.
Equivalent raw command:
```bash
railiance-fabric-registry --db .railiance-fabric/registry.sqlite3 --port 8765
@@ -106,5 +151,6 @@ loop.
The graph explorer export is the first executable slice of the interactive
Fabric map. See `docs/graph-explorer-transfer-review.md` for the repo-scoping
transfer review, `docs/graph-explorer-contract.md` for the shared manifest and
payload contract, and `docs/graph-explorer-operations.md` for launch, refresh,
verification, and extraction guidance.
payload contract, `docs/semantic-attractors.md` for the attractor-based layout
orientation concept, and `docs/graph-explorer-operations.md` for launch,
refresh, verification, and extraction guidance.

View File

@@ -87,6 +87,80 @@ spec:
- sts-token
tags: [storage, credentials, security]
- id: kubernetes-runtime
name: Kubernetes runtime
lifecycle: active
description: Provides the Kubernetes API, namespaces, workloads, Services, Ingresses, and runtime primitives consumed by Railiance services.
default_criticality: critical
default_data_classification: restricted
expected_interface_types:
- kubernetes-api
- kubernetes-crd
tags: [kubernetes, cluster, runtime]
- id: ci-cd-template-catalog
name: CI/CD template catalog
lifecycle: planned
description: Provides reusable workflow templates, release gates, and delivery conventions for Railiance workloads.
default_criticality: medium
default_data_classification: internal
expected_interface_types:
- workflow-template-contract
- cli
tags: [ci, cd, gitops, enablement]
- id: source-hosting
name: Source hosting
lifecycle: active
description: Hosts Git repositories, repository metadata, review surfaces, and source-forge web/API access.
default_criticality: high
default_data_classification: confidential
expected_interface_types:
- web-ui
- http-api
- git-ssh
tags: [forge, git, source]
- id: container-registry
name: Container registry
lifecycle: active
description: Publishes and serves OCI container images for Railiance workloads.
default_criticality: high
default_data_classification: confidential
expected_interface_types:
- oci-registry
tags: [forge, registry, container-image]
- id: python-package-registry
name: Python package registry
lifecycle: active
description: Publishes and serves Python package artifacts for Railiance source and app builds.
default_criticality: high
default_data_classification: confidential
expected_interface_types:
- python-package-index
tags: [forge, registry, python, package]
- id: workflow-runner-substrate
name: Workflow runner substrate
lifecycle: planned
description: Provides forge-backed runner infrastructure, labels, placement, and credential boundaries for workflows.
default_criticality: high
default_data_classification: restricted
expected_interface_types:
- workflow-runner-label-contract
tags: [forge, runner, actions, automation]
- id: artifact-promotion-evidence
name: Artifact promotion evidence
lifecycle: active
description: Provides release artifact identity, provenance, publish, restore, and readiness evidence for consumers.
default_criticality: high
default_data_classification: internal
expected_interface_types:
- evidence-contract
tags: [forge, evidence, provenance, release]
- id: audit-event-sink
name: Audit/event sink
lifecycle: planned
@@ -117,6 +191,7 @@ spec:
default_data_classification: internal
expected_interface_types:
- http-api
- mcp-api
- event-stream
tags: [coordination, state-hub, planning]

View File

@@ -25,6 +25,14 @@ spec:
typical_auth_methods: [none, oidc, jwt, mtls, api_key]
versioning: URL path, static asset version, and documented user-facing workflow compatibility.
- id: mcp-api
name: MCP API
lifecycle: active
description: Model Context Protocol interface consumed by agents or agent runtimes.
category: api
typical_auth_methods: [none, oidc, jwt, mtls, api_key]
versioning: MCP protocol version, tool schema version, and documented capability compatibility.
- id: oidc-discovery
name: OIDC discovery
lifecycle: active
@@ -49,6 +57,14 @@ spec:
typical_auth_methods: [kubernetes_service_account]
versioning: group, version, and kind.
- id: kubernetes-api
name: Kubernetes API
lifecycle: active
description: Kubernetes API server surface consumed by operators, controllers, and automation.
category: kubernetes
typical_auth_methods: [kubernetes_service_account, oidc, static_secret]
versioning: Kubernetes version, API groups, RBAC contract, and kubeconfig delivery path.
- id: helm-release
name: Helm release
lifecycle: active
@@ -73,6 +89,54 @@ spec:
typical_auth_methods: [database_role, static_secret, openbao_token]
versioning: engine version, connection contract, and migration compatibility.
- id: git-ssh
name: Git SSH
lifecycle: active
description: Git-over-SSH repository access endpoint.
category: source-control
typical_auth_methods: [static_secret, unknown]
versioning: hostname, port, SSH host key, authorized key scope, and Git server compatibility.
- id: oci-registry
name: OCI registry
lifecycle: active
description: OCI distribution-compatible container image registry endpoint.
category: registry
typical_auth_methods: [api_key, static_secret, none]
versioning: registry host, API behavior, package visibility, and tag/digest semantics.
- id: python-package-index
name: Python package index
lifecycle: active
description: Python package index endpoint compatible with pip/uv simple API consumption.
category: registry
typical_auth_methods: [api_key, static_secret, none]
versioning: package index URL, package visibility, token scope, and package version semantics.
- id: workflow-runner-label-contract
name: Workflow runner label contract
lifecycle: planned
description: Published runner label, placement, and trust contract consumed by CI/CD workflows.
category: automation
typical_auth_methods: [none, kubernetes_service_account, static_secret]
versioning: semantic label names, trust level, credential purpose, and runner replacement rules.
- id: workflow-template-contract
name: Workflow template contract
lifecycle: planned
description: Reusable CI/CD workflow template or template catalog contract.
category: automation
typical_auth_methods: [none]
versioning: template id, input schema, runner labels, and release gate semantics.
- id: evidence-contract
name: Evidence contract
lifecycle: active
description: Documented evidence bundle or machine-readable evidence contract for release, restore, or readiness decisions.
category: evidence
typical_auth_methods: [none, api_key]
versioning: evidence schema version, required fields, source links, and retention policy.
- id: object-storage-bucket
name: Object-storage bucket
lifecycle: planned

View File

@@ -0,0 +1,367 @@
# Fabric Discovery And Update
## Intent
`railiance-fabric` models the durable infrastructure-responsibility graph of the
Railiance netkingdom.
A fabric is not a repository, environment, cluster, deployment scenario, or
security zone. A fabric is the financial and operational responsibility boundary
around infrastructure: who pays for it, who is accountable for it, and who has
the authority to keep it alive.
The graph exists to answer:
- what infrastructure, services, repositories, deployables, endpoints, and
machines exist;
- who owns or pays for each node;
- which fabrics and subfabrics contain those nodes;
- which durable interfaces cross ownership boundaries;
- which cross-boundary interfaces provide utility that may carry business value.
Fabric intentionally stays out of live operational telemetry. It may represent
that a service, host, endpoint, or deployment exists because durable automation
or configuration proves it. It should not represent whether that thing is
currently healthy, loaded, failing, or profitable in the accounting sense.
## Financial Boundary Terms
Fabric uses financial responsibility terms for fabric boundaries.
### King
The king is responsible for the whole netkingdom.
The king has the key to the netkingdom. Literally, this means the master key or
at least a relevant key component for secrets, backups, and recovery paths across
the infrastructure under the netkingdom.
The king also holds the legal and contractual relationship with all lords and
tenants. Even when responsibility is delegated, the king remains the actor that
can ultimately restore, recover, govern, or terminate the netkingdom.
### Lord
A lord pays for a fabric.
A lord is accountable for a durable infrastructure-responsibility boundary. In
the current Railiance setup there is one effective fabric because one actor pays
for all infrastructure. Additional fabrics appear when the responsibility to pay
for a coherent infrastructure boundary changes.
### Tenant
A tenant pays for restricted use of a subfabric.
A tenant is not the root payer for the whole fabric. A tenant receives bounded
access to utility inside a subfabric and may consume services or interfaces
provided by a lord, by the king, or by another tenant-facing utility.
## Fabrics, Subfabrics, And Views
### Fabric
A fabric is a durable responsibility boundary. Membership changes when financial
or operational responsibility changes, not when a service is redeployed, a repo
is refactored, a host is replaced, or an environment name changes.
Criteria for establishing a fabric:
- there is a lord who pays for the infrastructure in that boundary;
- the boundary can be discovered from durable infrastructure, deployment, or
contractual evidence;
- the boundary remains stable across ordinary deployment and development churn;
- ownership changes imply a meaningful change to who is accountable for cost,
continuity, recovery, and utility delivery.
### Subfabric
A subfabric is a delegated responsibility or tenancy partition within a fabric.
Subfabrics are useful when a tenant receives restricted access to infrastructure
or utility while the parent fabric remains under a lord or king. A subfabric may
eventually become a separate fabric if payment, control, recovery, and legal
responsibility move to another lord.
### View
A view is a diagnostic or informational slice through the graph.
Views can filter by repository, service, environment, machine, endpoint, tenant,
workstream, deployment stack, or business capability. Views do not define fabric
membership and should not mutate fabric boundaries.
Zone views are a special kind of view. They group the subgraph by deployment
environment, deployment scenario, routing authority, or access zone. They are
useful for questions such as:
- which services are currently visible in private dev, shared test, or
production;
- which control surfaces appear in user-facing zones;
- which production surfaces still have unrestricted developer access;
- which routes exist in production but not in test;
- which early-access interfaces lack an explicit policy authority.
### Environment
An environment such as local, dev, staging, production, or lab is a deployment
classification inside a fabric. It is not a fabric by itself.
Railiance uses these baseline environment terms:
| Environment | Meaning |
|-------------|---------|
| `dev` | Private developer-local execution. Useful for debugging and discovery, but not shared truth for other developers unless explicitly published. |
| `test` | Shared central test stage for collaborators and friendly early-access users. |
| `prod` | Production execution. Alpha-stage developer access may exist now, but production must be able to move toward restricted operator access and applicable security and data-privacy controls. |
### Deployment Scenario
A deployment scenario is a realized or planned arrangement of services,
machines, endpoints, automation, and configuration. It belongs inside a fabric
or subfabric but does not define the financial boundary.
Current Railiance scenario names:
| Scenario | Environment | Meaning |
|----------|-------------|---------|
| `bernd-laptop` | `dev` | Private local workstation scenario. It may discover useful loopback services and ports, but it is not a shared developer environment. |
| `coulombcore` | `test` | Central shared test-stage server for developers and friendly early-access users. |
| `railiance01` | `prod` | Production host. It is still alpha-accessible to developers, but the intended direction is restricted production access. |
### Routing Authority
A routing authority is the deployment component that maps an external name,
local URL, ingress host, or port to a backend service. Fabric discovers routing
authority output; it does not allocate ports or author routes.
Examples:
- local loopback process launch configuration for developer tools;
- Makefile or script defaults for local-only bootstrap surfaces;
- Docker Compose published ports;
- Kubernetes `Service`, `Ingress`, or controller-specific route manifests;
- Traefik, nginx, Caddy, HAProxy, or other reverse-proxy configuration;
- DNS records and TLS/certificate automation when they prove reachable names.
### Access Zone
An access zone is a visualization and discovery overlay that describes who a
surface is intended to be reachable by in a deployment scenario. It is not a
fabric boundary and does not define the policy itself.
Useful initial access zones:
| Access zone | Intended reach |
|-------------|----------------|
| `private-dev` | One developer's local machine. |
| `collaborator-test` | Collaborating developers in the shared test stage. |
| `early-access` | Friendly users participating in early access. |
| `production-public` | Production user-facing surfaces. |
| `production-admin` | Production administrative or control surfaces. |
### Policy Authority
A policy authority is the system expected to enforce access rules for a surface.
Fabric may record which authority is in play, such as NetKingdom IAM, an ingress
policy, network policy, or local-only loopback binding, but Fabric does not
define or enforce those rules.
These terms let the graph say: "this service belongs to the Railiance primary
fabric, runs in the `coulombcore` test scenario, is routed by Traefik, and is
intended for the `early-access` zone." That is a deployment and access overlay,
not a change to fabric membership.
## Ownership Rule
Every node in the Fabric graph must carry owner information.
Ownership may be explicit on the node or inherited from the containing fabric or
subfabric, but it must be resolvable. Nodes without resolvable ownership are
incomplete because Fabric cannot explain who is accountable for their cost,
continuity, recovery, or utility.
Ownership should identify the relevant king, lord, tenant, or delegated operator
where applicable. Operators and maintainers may be represented as supporting
actors, but they do not replace the financial boundary terms unless they also
pay for or legally own the responsibility boundary.
## Cross-Boundary Edges
Edges may cross fabric and subfabric boundaries.
These edges are especially important. They are where one owned boundary consumes
utility from another owned boundary. They are also where business value can
appear: paid access, cost recovery, shared platform utility, tenant-facing
interfaces, or contractual service delivery.
Fabric should treat cross-boundary interfaces as first-class graph facts. A
cross-boundary utility edge should be able to identify:
- provider owner;
- consumer owner;
- provider fabric or subfabric;
- consumer fabric or subfabric;
- exposed endpoint, contract, service, or utility;
- durable evidence for the relationship;
- expected payment or business model when known;
- metering basis when known.
The existence of a cross-boundary interface belongs in Fabric. Live usage,
current health, latency, revenue events, and incident state belong to telemetry,
accounting, or operational systems outside the Fabric core.
## Cost And Profit Center Attribution
Cost centers and profit centers are accounting attributions. They are not fabric
boundaries by themselves.
A cost center may be evidence that a responsibility boundary exists, especially
when it corresponds to a lord who pays for infrastructure. However, the fabric
boundary is still established by financial and operational accountability, not
by the accounting label alone.
Cost and profit center attribution may overlap with fabrics and subfabrics:
- one fabric may contain many cost centers;
- one tenant may span several cost centers;
- one cost center may include nodes from multiple subfabrics;
- one shared service may allocate cost across several tenants or cost centers;
- one cross-boundary utility may be cost-bearing for one actor and
profit-bearing for another.
Fabric should therefore support optional accounting attribution on nodes and
edges without allowing that attribution to redefine containment:
- node cost center;
- node profit center;
- edge cost allocation model;
- edge profit attribution;
- payment schema;
- metering or allocation basis when known;
- attribution validity period when known.
This lets Fabric build cost-center and profit-center views while preserving the
durability of fabric membership. A node moving from one cost center to another
does not necessarily move fabrics. A node moves fabrics only when financial or
operational responsibility for the infrastructure boundary changes.
Useful accounting views include:
- all infrastructure attributed to a cost center;
- all cross-boundary utility edges attributed to a profit center;
- tenant-facing utilities without a payment schema;
- shared services with unresolved allocation;
- owned nodes without cost-center attribution;
- expensive or critical infrastructure with no visible value interface.
## Discovery Approach
Fabric discovery should start from accountability roots, not from arbitrary repo
ownership declarations.
Useful roots include:
- the king and current netkingdom inventory;
- lords and their fabrics;
- tenants and their subfabrics;
- State Hub attached repositories and known host paths;
- Gitea organizations and repository URLs;
- deployment automation repositories;
- Docker, Compose, Kubernetes, systemd, reverse proxy, and infrastructure
manifests;
- backup, recovery, and secret-management evidence;
- service endpoint and API specifications;
- CI/CD and release automation that proves deployables and deployment targets.
The scanner follows these roots outward to discover durable evidence of nodes
and edges. Repository-local facts can still help identify deployables, APIs, and
artifacts, but repository-owned declarations should not be the default source of
truth for external fabric relations.
## Rebuild From Scratch
A full rebuild should be deterministic enough to repeat and provenance-rich
enough to review.
Recommended phases:
1. Establish the netkingdom root and current king.
2. Register known lords, fabrics, tenants, and subfabrics.
3. Register durable discovery roots for each fabric and subfabric.
4. Crawl deployment automation, infrastructure manifests, repo inventories,
service configs, and endpoint contracts.
5. Store raw evidence with source paths, URLs, timestamps, scanner versions, and
content hashes.
6. Normalize identities across host paths, repository URLs, image names, service
names, endpoint names, and deployment names.
7. Build candidate graph nodes and edges.
8. Resolve or flag ownership for every node.
9. Promote accepted candidates into a versioned fabric snapshot.
10. Export the accepted snapshot to State Hub as a read model.
## Keeping The Model Up To Date
Fabric freshness should be based on durable configuration and automation changes,
with scheduled rescans as a safety net.
Rescan triggers include:
- deployment automation changes;
- infrastructure manifest changes;
- State Hub attached repository inventory changes;
- repository changes that affect deployables, APIs, images, service names,
endpoint contracts, ports, or deployment configuration;
- tenant or lord changes;
- backup, recovery, or secret-root changes;
- manual operator requests;
- scheduled periodic rebuilds.
Normal source-code edits do not necessarily change the Fabric graph. Fabric
should focus on changes that alter durable topology, ownership, deployment,
interfaces, or cross-boundary utility.
Each update should compare new discovery output with the previous accepted
snapshot and produce a delta. The delta should distinguish:
- added, changed, and removed nodes;
- added, changed, and removed edges;
- changed ownership;
- changed fabric or subfabric membership;
- new or removed cross-boundary utility interfaces;
- ambiguous identity merges that need review;
- discovered nodes without resolvable ownership.
## Boundary With Security Terms
Fabric should avoid using security-zone language for its core concepts.
Terms such as realm, domain, tenant realm, identity realm, and security domain
are useful in systems such as Microsoft environments or Keycloak, but they carry
security and identity-management semantics. Fabric may later relate to those
systems as discovered evidence, but the core Fabric concept is financial and
operational responsibility, not identity or access-control topology.
For now, use:
- king, lord, and tenant for financial responsibility actors;
- fabric and subfabric for durable responsibility boundaries;
- view for diagnostic graph slices;
- environment for deployment classification;
- deployment scenario for a concrete place where services run;
- routing authority for the component that maps names or ports to services;
- access zone for the visualization overlay that groups intended reachability;
- policy authority for the external system expected to enforce access rules.
Access zones may be security-relevant, but they are not Fabric security policy.
They are discovered and visualized evidence that helps operators spot misplaced
surfaces, missing policy authorities, or accidental exposure.
## State Hub Role
State Hub coordinates work and stores a readable projection of the Fabric graph.
It should not be the primary author of Fabric topology.
`railiance-fabric` discovers, normalizes, versions, and exports Fabric snapshots.
State Hub imports those snapshots so agents and operators can ask practical
questions about work, ownership, dependencies, and cross-boundary utility.

View File

@@ -0,0 +1,337 @@
# Zone Entity Visualization
## Intent
The graph explorer currently treats zones as visual overlays derived from node
metadata. That is useful for first orientation, but it keeps zones as a
decoration around an already-laid-out graph. The next step is to make a zone a
first-class visualization entity beside nodes and edges.
A zone is a bounded drawing surface for a subgraph. It is similar to a sheet of
paper or a layer in a graphics application: it decides which graph elements are
drawn inside it, how those elements are laid out, how it stacks with other
zones, and whether it can be collapsed into a representative node.
Zones are view entities. They help the operator inspect the fabric without
changing the underlying fabric graph. A zone can be based on deployment
environment, ownership, tenant boundaries, cost center, access zone, or any
other declared view rule, but those concepts remain distinct from the zone
mechanism itself.
## Core Model
A graph view contains three visible entity classes:
- nodes: fabric entities such as services, hosts, repos, scenarios, or owners
- edges: fabric relationships such as deployment, routing, ownership, policy,
dependency, or evidence links
- zones: visual drawing surfaces that contain a selected subgraph
A zone definition describes how to derive and render one zone. A zone instance
is the result of applying a definition to the current graph, filter state, and
view profile.
Zone definitions should be serializable so they can later live in saved graph
profiles, operator presets, or a dedicated view registry.
## Zone Definition
A zone definition should contain at least these fields:
```yaml
id: prod
label: Production
enabled: true
membership:
mode: include
rules:
- field: deploymentEnvironment
op: equals
value: prod
attraction:
rules:
- edge_type: fabric:serves
direction: both
depth: 1
node_filter:
field: kind
op: in
value: [service, endpoint]
layout:
algorithm: cose
options: {}
presentation:
height: 30
color: "#0f766e"
opacity: 0.16
blur_below: true
collapse:
enabled: true
label: Production Zone
```
The exact schema can evolve, but the responsibilities should stay separate:
- membership chooses seed nodes
- attraction optionally expands the zone from those seeds through selected edge
types and depths
- layout chooses how the zone subgraph is arranged
- presentation chooses how the zone itself is drawn and stacked
- collapse chooses how the zone can become a representative node
## Membership Rules
Membership rules decide which nodes initially belong to a zone. They should be
declarative and inspect node data rather than hard-code UI concepts.
Useful operators:
- `equals`
- `not_equals`
- `in`
- `not_in`
- `exists`
- `missing`
- `matches`
Useful rule composition:
- `all`: every nested rule must match
- `any`: at least one nested rule must match
- `none`: no nested rule may match
The first implementation can support a small subset such as `equals`, `in`,
`exists`, `all`, and `any`, then widen the rule language once saved zone
profiles need it.
## Attraction Rules
Attraction rules allow a zone to pull in additional nodes connected to the seed
set. This is useful when the operator wants a zone to include the services,
routes, machines, or contracts that make the zone understandable, even when
those nodes do not carry the seed attribute themselves.
An attraction rule should define:
- edge type or edge-type pattern
- direction: `out`, `in`, or `both`
- maximum depth
- optional node filter
- optional edge filter
Attraction must be deterministic. Given the same graph and zone definitions, the
same node should land in the same zone every time.
Attraction also needs a conflict policy because a node must not belong to more
than one zone. The recommended first policy is priority by zone height and then
definition order:
1. Seed membership wins over attracted membership.
2. If two zones seed the same node, the conflict is reported and the node is
assigned to the higher zone.
3. If two zones attract the same node, the node is assigned to the higher zone.
4. If height ties, the earlier definition wins.
5. Every conflict is recorded as a view diagnostic.
This keeps the single-zone invariant while making ambiguous rules visible.
## Single-Zone Invariant
A visible node must be assigned to zero or one zone in a given graph view. A node
that belongs to no zone remains on the base canvas.
The invariant is important because zones are meant to behave like drawing
surfaces, not like arbitrary tags. Allowing the same node to appear in multiple
zones would make edge routing, selection, collapse behavior, and interaction
state ambiguous.
The model can still support analytical overlap by reporting diagnostics and by
offering alternative view presets. It should not solve overlap by duplicating
nodes in the same rendered view.
## Zone Layout
Each zone may choose its own layout algorithm for its subgraph. For example:
- deployment zones may use force-directed layout
- ownership zones may use concentric layout
- stage zones may use grid layout
- collapsed or nearly empty zones may use compact deterministic placement
The outer graph layout and the zone subgraph layouts are separate concerns. A
first implementation can keep the current Cytoscape layout for all nodes and
only render zone surfaces around assigned nodes. A later implementation should
introduce a two-phase layout:
1. Place zones and unzoned nodes on the main canvas.
2. Place each zone's assigned subgraph inside that zone using the zone's layout.
This will likely require an internal view model that separates fabric graph data
from rendered graph coordinates.
### Per-Zone Layout Preparation
The current graph explorer should not immediately run independent Cytoscape
layouts inside each zone rectangle. Cytoscape layouts operate on collections in
one coordinate space, while the current zone overlay is a view layer drawn over
the already-laid-out graph. Running nested layouts directly against visible
nodes would make pan/zoom, filters, collapse state, and edge routing fragile.
The safer path is a two-phase view layout:
1. Resolve zones from the current graph and view filters.
2. Place zone containers and unzoned nodes in the global canvas.
3. For each zone, compute local coordinates for its assigned nodes using the
zone's configured layout algorithm.
4. Project local zone coordinates into global graph coordinates.
5. Route internal edges inside the zone and boundary edges through the zone
perimeter or collapsed zone node.
The implementation needs these pieces before per-zone layouts become safe:
- a resolved zone view model that survives filtering and saved profiles;
- a stable assignment invariant so a visible node belongs to no more than one
zone;
- a zone container model with size, position, padding, and height;
- a local coordinate projection layer from zone space to Cytoscape space;
- explicit boundary-edge routing rules;
- collapse state that can replace a zone subgraph with a representative node;
- diagnostics when a configured zone layout cannot be applied.
The present implementation already establishes the first, second, and sixth
pieces. A follow-up should introduce a zone container placement phase before
attempting per-zone node layout. That follow-up can keep Cytoscape as the final
renderer while moving layout decisions into a Fabric-owned view model.
### Stable Zone Containers
The first container implementation keeps zone surfaces as view state keyed by
stable zone id. When a zone first appears, the global graph layout supplies its
initial center. Once created, the container owns the zone surface position while
the global layout continues to arrange the base canvas and unzoned nodes.
Dragging a zone moves the container and its currently assigned visible member
nodes together. Rerunning layout or switching the layout algorithm should keep
the container in its stored graph coordinates and then project the zone's
visible subgraph back into that container.
Container state belongs in saved or copied graph view state, not in the Fabric
payload. It is an operator workspace preference, similar to manual visibility
overrides.
Zone-local layout is also view state. The first selectable algorithms are a
compact grid and a circle layout. Switching between them should rearrange the
assigned nodes inside each stable container without moving the container itself
or changing the underlying Fabric relationships.
### Context Edges
Display-only context edges are not zone connectivity. Repository `declares`
edges, for example, show which repository declared a node, but they should not
create boundary diagnostics, attraction paths, or collapsed-zone boundary
edges. A host can still show them as explanatory evidence in details, but a
zone boundary should only react to canonical or host-promoted graph
relationships.
## Layer Height And Overlap
Zone presentation includes a height. Height is a visual stacking concept, not a
security or ownership concept.
When zones overlap:
- higher zones should draw above lower zones
- lower zones may be desaturated when covered
- lower zones may be blurred or dimmed when covered
- labels should remain readable where practical
- pointer interaction should prefer the highest visible zone
The implementation should avoid implying that a higher zone is more important.
Height is about rendering order and view ergonomics only.
## Collapse To Zone Node
A zone can optionally collapse into a representative node. Collapsing is a view
operation that preserves graph connectivity:
- nodes inside the zone become hidden from the current view
- the zone node represents the collapsed zone
- edges between inside and outside nodes are reconnected to the zone node
- edges fully inside the zone are hidden or summarized
- expanding restores the original nodes and edges
The collapsed zone node should expose useful summary data:
- contained node count
- contained edge count
- dominant node kinds
- inbound and outbound boundary edge counts
- diagnostics such as membership conflicts
This enables drill-down exploration without destroying the operator's context.
## Hierarchical Zones
The collapse model naturally leads to hierarchical zones. A tenant subfabric may
sit inside a landlord fabric, an access zone may sit inside a deployment
environment, and a cost-center view may cut across both as an alternate profile.
Hierarchy should be introduced carefully:
- parent-child containment must be explicit in the zone definition or derived
from a deterministic rule
- a node still belongs to exactly one visible leaf zone
- collapsing a parent collapses all visible descendants
- cross-boundary edges must remain visible at the collapsed boundary
The first implementation should support flat zones. The data model should avoid
choices that would make hierarchy impossible later.
## Diagnostics
Zones should produce diagnostics because rule-based view construction can hide
important ambiguity.
Useful diagnostics:
- node matched by more than one membership rule
- node attracted by more than one zone
- edge crosses zone boundaries
- zone has no seed nodes
- attraction depth reached limit
- collapsed zone has hidden internal boundary evidence
Diagnostics belong to the visualization layer. They should not mutate the
fabric graph.
## Relationship To Fabric Concepts
Zones are not fabrics, subfabrics, realms, domains, environments, cost centers,
or tenants. A zone is a view mechanism that can visualize those concepts.
Examples:
- a deployment-environment zone groups nodes by `dev-tegwick`, `test`, or `prod`
- a financial fabric zone groups nodes by lord or tenant responsibility
- a cost-center zone groups nodes for accounting analysis
- an access-zone view groups nodes by operational access policy once that
metadata exists
This keeps the graph explorer flexible while preserving the stricter meaning of
fabric boundaries: who pays for, owns, and is responsible for infrastructure.
## Implementation Direction
The recommended path is incremental:
1. Introduce a pure zone resolver that converts graph data and zone definitions
into zone instances, node assignments, edge summaries, and diagnostics.
2. Replace the current hard-coded environment/access overlay derivation with
resolver-backed default zone definitions.
3. Add zone-aware rendering state to the graph explorer UI.
4. Add saved zone profiles once the model is stable.
5. Add collapse-to-zone-node behavior.
6. Add per-zone layout only after the resolver and rendering model are stable.
This order keeps the current useful overlay behavior while moving the engine
toward zones as first-class view entities.

View File

@@ -0,0 +1,172 @@
# Accountability Root Manifest
The accountability root manifest is the handoff between the financial Fabric
model and the discovery/update loop.
It answers where discovery starts. A manifest names the netkingdom, actors,
fabric boundaries, and durable roots that can prove repositories, deployment
realities, service configuration, endpoint contracts, backup/recovery evidence,
and secret-root metadata. It does not collect live telemetry and it does not
make State Hub the authoring surface for topology.
Schema:
```text
schemas/accountability-root-manifest.schema.yaml
```
Current Railiance manifest:
```text
fabric/discovery/railiance-accountability-roots.yaml
```
Tenant/subfabric example:
```text
examples/discovery/accountability-root-manifest.yaml
```
Raw evidence run schema:
```text
schemas/accountability-root-evidence.schema.yaml
```
Identity projection schema:
```text
schemas/accountability-identity-projection.schema.yaml
```
Ownership review schema:
```text
schemas/accountability-ownership-review.schema.yaml
```
Update delta schema:
```text
schemas/accountability-update-delta.schema.yaml
```
## Required Sections
- `netkingdom`: root id, name, and king actor.
- `actors`: king, lord, tenant, operator, or steward actors.
- `fabrics`: fabric and subfabric boundaries.
- `discovery_roots`: durable roots such as State Hub repo inventory, Gitea
organizations, registry manifests, host paths, repo checkouts, deployment
automation, endpoint contracts, backup/recovery evidence, and secret-root
metadata.
- `refresh`: cadence and trigger hints for the future update loop.
## Boundary Rules
The current Railiance manifest has one active fabric:
`fabric.railiance.primary`. Future tenant subfabrics are added under that
fabric by adding a tenant actor, a `Subfabric`, and subfabric-scoped discovery
roots. This does not change the root fabric criterion: the fabric boundary
still rests on financial and operational accountability.
`owner_actor_id` on a discovery root describes the default owner to attach to
identity candidates discovered through that root. For ordinary repositories,
deployment files, and host-path evidence this should be the lord who pays for
the fabric. King authority remains modeled on the netkingdom and on roots that
represent recovery, secret, backup, or termination authority.
Discovery roots should state `safe_discovery` explicitly. Secret and backup
roots should use `metadata_only` or `explicit_review`; adapters must never read
secret values or operational telemetry while building Fabric graph evidence.
## Collecting Root Evidence
The first adapter slice emits raw evidence without promoting it into accepted
graph snapshots:
```bash
railiance-fabric discover-roots \
--manifest fabric/discovery/railiance-accountability-roots.yaml \
--max-items-per-root 200
```
The command covers manifest-backed repository inventory, repository checkout
identity, host-path evidence, deployment automation and infrastructure files,
State Hub/Gitea metadata roots, endpoint/service-config roots, and safe
metadata-only backup or secret roots. Remote HTTP reads are disabled by default;
pass `--include-remote` only when the operator intentionally wants configured
remote roots such as State Hub inventory endpoints to be fetched.
The output is an `AccountabilityRootEvidenceRun`. Every evidence item carries
provenance, source, fingerprint, `durable: true`, and
`live_telemetry: false`, preserving the boundary between Fabric evidence and
operational telemetry.
To normalize raw evidence into reviewable identity candidates:
```bash
railiance-fabric discover-roots \
--identity-projection \
--max-items-per-root 200
```
To persist raw evidence and identity candidates in a local SQLite store:
```bash
railiance-fabric discover-roots \
--store-db .railiance-fabric/accountability-evidence.sqlite3 \
--identity-projection
```
The store is intentionally separate from accepted registry graph snapshots. It
keeps raw evidence runs, evidence items, and identity candidates available for
inspection before any candidate is promoted.
## Ownership Review
To resolve ownership and containment from the normalized identities:
```bash
railiance-fabric discover-roots \
--ownership-review \
--store-db .railiance-fabric/accountability-evidence.sqlite3
```
The ownership review inherits owners from fabric/subfabric containment when
possible, applies explicit owner evidence from discovery roots, and marks
unresolved or ambiguous candidates as `needs_review`. Accepted candidates must
have a resolved owner and containment unless they are actors or the netkingdom
root.
To persist a reviewer decision for a stable identity candidate:
```bash
railiance-fabric review-identity identity:repository:example-repo \
--store-db .railiance-fabric/accountability-evidence.sqlite3 \
--decision accept \
--owner-actor-id actor.railiance.primary-lord \
--fabric-id fabric.railiance.primary \
--reviewer operator \
--note "accepted from reviewed checkout evidence"
```
Reviewer decisions are keyed by stable identity key. Later rescans apply the
latest decision for that key, so ordinary evidence refreshes do not lose
reviewed ownership choices.
## Update Deltas
To compare the current run with previous identity and ownership-review outputs:
```bash
railiance-fabric discover-roots \
--delta \
--previous-identity-projection previous-identities.json \
--previous-ownership-review previous-ownership.json
```
The delta separates candidate graph node changes, candidate graph edge changes,
ownership changes, containment changes, review-state changes, and blocker
changes. When `summary.promotion_needed` is `false`, the update loop can skip
promotion because the durable evidence produced no meaningful Fabric change.

View File

@@ -0,0 +1,140 @@
# Canon Alignment Review For RAIL-FAB-WP-0016
Date: 2026-05-23
## Review Packet
This review follows the InfoTechCanon consumer alignment workflow for
`railiance-fabric`.
Reviewed Fabric sources:
- `INTENT.md`, `SCOPE.md`, and `README.md`
- `railiance_fabric/graph.py`, `scanner.py`, `registry.py`, and `graph_explorer.py`
- `schemas/discovery-snapshot.schema.yaml` and `schemas/state-hub-export.schema.yaml`
- workplans `RAIL-FAB-WP-0010` through `RAIL-FAB-WP-0016`
Reviewed canon sources:
- `infospace/agent/review-kit/review-kit.yaml`
- `infospace/agent/review-kit/review-workflow.yaml`
- `infospace/evaluations/railiance-fabric/conformance-pack.yaml`
- `infospace/evaluations/railiance-fabric/entity-edge-capture-criteria.yaml`
- `infospace/evaluations/railiance-fabric/mapping-expectations.yaml`
- `infospace/evaluations/railiance-fabric/visualization-examples.yaml`
The workplan's `review-kit/alignment` reference is the canon artifact id; the
physical files are under `infospace/agent/review-kit/`.
## Repository Context
Producer intent: Railiance Fabric owns repo-authored graph declarations,
scanner output, registry projection, validation, query tooling, and State Hub
export contracts for the Railiance ecosystem graph.
Current scope: source-controlled declarations model services, capabilities,
interfaces, dependencies, and binding assertions. Deterministic scanning adds
candidate repositories, libraries, manifests, runtime endpoints, domains,
ports, and source evidence. The graph explorer adds visual projection edges and
inferred runtime views for usability.
Consumer purposes:
- make cross-repo dependencies reviewable,
- make provider/consumer and blast-radius queries reliable,
- provide State Hub with a read model rather than an authoring surface,
- support reingest after a canon-aligned model reset,
- prevent visualization metadata from becoming graph truth.
## Selected Canon Surfaces
| Surface | Why selected |
| --- | --- |
| `model/landscape` | Services, software systems, runtime resources, environments, ownership, and dependency claims. |
| `model/devsecops` | Source repositories, packages, lockfiles, pipelines, artifacts, deployments, and attestations. |
| `model/network` | Endpoints, ports, DNS, routes, reachability, and future flow nodes. |
| `model/data` | Datastore and reads/writes relationships are expected gaps in current Fabric capture. |
| `model/observability` | Evidence, telemetry signals, scanner provenance, and validation support. |
| `model/governance` | Policies, reviews, decisions, exceptions, and reset guardrails. |
| `model/security` | Controls and findings that should not be flattened into generic dependency edges. |
| `model/task` | Review and remediation work created from graph gaps. |
| `model/purpose-demand-extension` | Consumer purpose, scope pressure, and canon evolution feedback. |
| `standard/tagging` | Non-relationship classification without using display edges as semantics. |
## Target Node Taxonomy
| Canon category | Current Fabric source | Fit | Target action |
| --- | --- | --- | --- |
| source-repository | `Repository` candidates and registered repos | direct | Keep as source evidence for repo-local declarations and scans. |
| service | `ServiceDeclaration` | direct | Keep as logical/deployable service boundary. |
| software-system | `CapabilityDeclaration`, `Library`, `ExternalLibrary` | partial | Keep as transitional mapping; later split capability semantics from system/component boundaries. |
| endpoint | `InterfaceDeclaration`, `ApplicationEndpoint`, `NetworkPort`, `DomainName` | partial/direct | Prefer explicit endpoint nodes for network reachability; preserve interface contract attributes. |
| deployment | `DeploymentService`, `ScoreWorkload`, `ContainerBuild` | partial/direct | Add deployment nodes for actual release/deploy evidence before destructive reset. |
| runtime-resource | `RuntimeService`, `Server`, `Kubernetes*` candidates | partial/direct | Capture workload, service DNS, namespace, VM/server, and cluster objects as runtime reality. |
| datastore | none first-class | gap | Add explicit datastore extraction from declarations, manifests, and config before reingest acceptance. |
| flow | route/resolve/port edges only | gap | Add first-class Flow nodes when observed traffic or declared communication needs protocol/evidence context. |
| policy | none first-class | gap | Add policy nodes for governing declarations, reset gates, and validation controls. |
| control | none first-class | gap | Add control nodes only when preventive/detective/corrective controls have evidence. |
| evidence | `BindingAssertion`, `Lockfile`, `ServiceConfig`, contracts, source anchors | partial | Make evidence explicit instead of hiding it only in attributes. |
| task | workplans and review gaps | gap | Capture review/remediation tasks after State Hub readiness work is agreed. |
| consumer-purpose | interface card and purpose-fit review | gap | Keep in docs now; model as graph nodes only if State Hub needs purpose-driven filtering. |
| telemetry-signal | none first-class | gap | Add metrics/logs/alerts/dashboards as observed signals when connectors exist. |
## Target Edge Taxonomy
| Current Fabric edge | Canon relationship | Fit | Notes |
| --- | --- | --- | --- |
| `exposes`, `exposes_port`, `listens_on` | `exposes` | direct/partial | Good first canonical family for service/endpoint reachability. |
| `consumes`, `depends_on_library`, `binds:*`, `uses_interface` | `depends_on` | partial | Preserve helper nodes until a reingest can project direct service relationships safely. |
| `provides` | `implements` | partial | Service-to-capability is a Fabric helper relation; needs stronger canon treatment or collapse. |
| `available_via`, `names_endpoint` | `exposes` | partial | Useful transitional mappings from capability/interface/domain to endpoint. |
| `defines_deployment`, `builds_container`, `declares_package` | `built_from` | partial | Direction and artifact semantics need cleanup before being canonical claims. |
| `defines_runtime_object`, `defines_workload`, `runs_on`, `deployed_as` | `deploys` | partial | Current scanner/UI edges mix source definitions and deployment reality. |
| `routes_to_port`, `routes_to_service`, `resolves_to` | `flows_to` | partial | These are reachability hints, not logical dependencies. |
| `uses_lockfile`, `uses_config`, `documents_interface`, `cataloged_as` | `evidenced_by` | partial | Evidence should become first-class where it supports a specific claim. |
| `declares`, `owns_deployment`, canon display examples | display-only | direct | These are view/cluster edges and must not be conformance claims. |
## Mapping Findings
Direct mappings are strong for repositories, services, runtime resources,
application endpoints, network ports, and explicit `exposes` relationships.
Partial mappings are expected for capability, interface, dependency, binding,
package, manifest, and route concepts. They should keep legacy names during the
transition but carry `canon_category`, `canonical_type`, `mapping_fit`, and
`evidence_state` metadata.
Conflicts: network flow must not be treated as logical dependency, and
graph-explorer layout edges must not be treated as canonical ownership,
dependency, reachability, policy, or evidence.
Gaps: datastores, flows, policy, control, telemetry signals, tasks, and
consumer-purpose nodes are not first-class scanner outputs yet. These should be
implemented as explicit gaps or candidate mappings, not forced into nearby
legacy Fabric semantics.
## Execution Direction
The first implementation slice adds canon metadata beside existing node and
edge names. That keeps registry and graph explorer behavior stable while making
the renewed model inspectable and testable.
The destructive reset phase remains blocked until the following exist:
- export and archive command for current registry graph data,
- reset command that requires an explicit operator flag,
- rollback note that explains restore limits,
- validation that the new scanner/projection can reingest registered repos,
- before/after counts and sample graph review.
No destructive reset was executed during this T01/T02 start.
## Canon Feedback
- Capability and dependency helper nodes are useful Fabric authoring concepts
but do not map cleanly to canonical graph entity categories.
- Edge direction needs explicit treatment for source repo to package/deployment
evidence, because canon `built_from` points from deployment/artifact context
back to source.
- Evidence state should remain independent from review state: accepted inferred
claims and candidate declared claims are different situations.

View File

@@ -0,0 +1,100 @@
schema: info-tech-canon.interface-card.v1
id: railiance-fabric/interface-card
title: Railiance Fabric Canon Interface Card
consumer:
repo: railiance-fabric
domain: railiance
owner: codex
intent: Make the Railiance ecosystem understandable, discoverable, and evolvable through accepted Fabric snapshots, accountability-root discovery, and repo-local evidence.
scope: Shared schemas, validation, graph construction, registry projections, scanner output, and State Hub export contracts for repository, service, capability, interface, dependency, binding, and runtime discovery data.
purposes:
- id: purpose/graph-refactor
use_case: Canon-aligned graph model reset and reingest.
consumer_need: Separate canonical graph semantics from registry, scanner, and visualization convenience edges before broad adoption.
demand_signals:
- RAIL-FAB-WP-0016
- Registry graph explorer needs clearer canonical/display boundary.
- Scanner output needs evidence state and canon mapping metadata.
canon_surfaces:
implemented_profiles: []
consumed_artifacts:
- model/landscape
- model/network
- model/data
- model/devsecops
- model/observability
- model/governance
- model/security
- model/task
- model/purpose-demand-extension
- standard/tagging
- conformance/railiance-fabric
owned_concepts:
- FabricGraphExport
- FabricDiscoverySnapshot
- GraphExplorerPayload
- RegistryOnboardingManifest
produced_concepts:
- FabricEntity
- FabricEdge
- CaptureSource
- DisplayEdge
- CanonicalEdgeCandidate
- VisualizationView
consumed_concepts:
- Repository
- Service
- SoftwareSystem
- RuntimeResource
- Endpoint
- Deployment
- Flow
- Evidence
- Task
mappings:
- source: Repository
target: source-repository
fit: direct
- source: ServiceDeclaration
target: service
fit: direct
- source: InterfaceDeclaration
target: endpoint
fit: partial
- source: CapabilityDeclaration
target: software-system
fit: partial
- source: DependencyDeclaration
target: depends_on edge
fit: gap
validation_expectations:
commands:
- python3 -m pytest
evidence_required:
- Nodes carry canon_category, canon_anchor, mapping_fit, and evidence_state.
- Edges carry canonical_type, display_only, mapping_fit, and evidence_state.
- Display-only edges do not act as conformance claims.
- Reset commands are guarded and documented before graph data is dropped.
known_gaps:
- Datastore, flow, policy, control, telemetry-signal, task, and consumer-purpose are not first-class scanner outputs yet.
- Legacy declaration nodes still preserve capability/dependency/binding helper nodes until reingest rules collapse or replace them safely.
purpose_fit:
state: partial-fit
matched_capabilities:
- entity and edge capture criteria
- mapping expectations
- visualization boundary examples
scope_pressure: Fabric needs a stable edge vocabulary and evidence-state vocabulary for mixed declared, observed, inferred, and proposed graph claims.
recommended_disposition: Continue consumer-side refactor; record canon gaps as explicit feedback rather than silently extending Fabric semantics.
consumer_needs:
current:
- Canon-aligned graph metadata in scanner, registry, export, and graph explorer payloads.
- Safe destructive reset guardrails before dropping previous graph snapshots.
- Reingest validation across registered and local repositories.
requested_extensions:
- Stable relationship vocabulary for graph capture.
- Evidence-state vocabulary for captured edges.
- Visualization boundary guidance for display-only edges.
feedback:
- CapabilityDeclaration and DependencyDeclaration are useful Fabric authoring helpers but do not map cleanly to canon node categories.
- Network flow and logical dependency must remain separate even when both are inferred from the same source artifact.

View File

@@ -1,12 +1,18 @@
# 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 declarations are small YAML documents that capture repo-local
self-description evidence. The first schema version is
`railiance.fabric/v1alpha1`.
These declarations are still supported for bootstrap and compatibility, but
they are not the long-term authority for external deployment realities,
financial fabric boundaries, tenant subfabrics, or cross-boundary utility
relations. The financial Fabric model starts from accountability roots as
described in `docs/FabricDiscoveryAndUpdate.md`.
## File Layout
Participating repositories should use this layout:
Participating repositories that publish local evidence should use this layout:
```text
fabric/
@@ -56,7 +62,7 @@ begin with the owning repo slug when possible:
| `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.owner` | Local evidence owner for the declaration; financial Fabric ownership is resolved separately. |
| `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. |

View File

@@ -1,7 +1,7 @@
# Discovery Queries And Exports
Railiance Fabric includes a first CLI surface for inspecting local declaration
graphs.
graphs and projecting them into the current financial Fabric baseline.
All commands accept a repo root, `fabric/` directory, or declaration files. When
paths are omitted, commands read `./fabric`.
@@ -84,13 +84,30 @@ Export the graph as the manifest-compatible graph explorer payload:
railiance-fabric export --format graph-explorer
```
Export the graph as the financial Fabric baseline projection:
```bash
railiance-fabric export --format financial
```
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`
Canon-aligned exports also carry mapping metadata beside the existing Fabric
terms: nodes include `canon_category`, `canon_anchor`, `mapping_fit`, and
`evidence_state`; edges include `canonical_type`, `display_only`,
`mapping_fit`, and `evidence_state`.
The graph explorer payload wraps those nodes and edges as Cytoscape-compatible
elements with stable keys, layers, display state, visual facets, source
references, and deep links. The registry service exposes the same projection at
`GET /exports/graph-explorer`.
The financial export emits `railiance.fabric/v1alpha2` /
`financial-fabric-v1`. It combines the current legacy graph evidence with
`fabric/financial/railiance-netkingdom.yaml`, assigning inherited ownership and
fabric containment for the current single Railiance fabric. Use it as the reset
contract and State Hub vNext handoff artifact.

View File

@@ -7,12 +7,16 @@ 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.
model over accepted Fabric graph snapshots, discovery evidence, and supporting
inventory.
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 registry should not become the authoring surface for Fabric ownership or
boundaries. Repo-local `fabric/` files remain useful evidence, while financial
fabric membership, tenant boundaries, ownership, and cross-boundary utility
relations are resolved from accountability roots and accepted snapshots. 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.

View File

@@ -0,0 +1,421 @@
# Financial Fabric Model Audit
Date: 2026-05-24
Workplan: `RAIL-FAB-WP-0017`
## Purpose
This audit identifies the current `railiance-fabric` assumptions that must
change to implement the financial Fabric architecture in
`docs/FabricDiscoveryAndUpdate.md`.
The current implementation is still centered on repo-owned declarations:
```text
repo -> service -> capability -> interface -> dependency -> binding
```
That model remains useful as legacy evidence and as a self-description layer,
but it is no longer the correct top-level source of truth for external fabric
relations. The vNext model must start from accountability roots: netkingdom,
king, lords, tenants, fabrics, subfabrics, deployment automation, durable
infrastructure evidence, and cross-boundary utility interfaces.
## Current Model Assumptions
### Repo-Owned Declarations Are The Primary Graph Source
Affected files:
- `INTENT.md`
- `SCOPE.md`
- `docs/declaration-schema.md`
- `docs/adoption-guide.md`
- `docs/first-rollout.md`
- `docs/repo-reality-scanner.md`
- `fabric/README.md`
- `examples/declarations/**`
- `fabric/services/**`
- `fabric/capabilities/**`
- `fabric/interfaces/**`
- `fabric/dependencies/**`
- `fabric/bindings/**`
Current behavior:
- Repositories are expected to own declarations for services, capabilities,
interfaces, dependencies, and binding assertions.
- The scanner treats repo-owned Fabric declarations as the highest-trust source
for accepted graph data.
- Existing examples and docs teach adoption by adding `fabric/` files to
participating repositories.
Required change:
- Repo-owned declarations should become one evidence source, mostly useful for
self-description, not the default authority for external relations.
- External relations, tenancy, deployment membership, and cross-boundary utility
edges should come from fabric-scoped discovery roots and deployment evidence.
### Declaration Kinds Encode The Graph Shape
Affected files:
- `schemas/service.schema.yaml`
- `schemas/capability.schema.yaml`
- `schemas/interface.schema.yaml`
- `schemas/dependency.schema.yaml`
- `schemas/binding.schema.yaml`
- `schemas/common.schema.yaml`
- `railiance_fabric/model.py`
- `railiance_fabric/loader.py`
- `railiance_fabric/validation.py`
- `railiance_fabric/graph.py`
- `railiance_fabric/canon.py`
- `tests/test_canon.py`
- `tests/test_registry.py`
- `tests/test_scanner.py`
- `tests/test_discovery.py`
- `tests/test_discovery_registry.py`
- `tests/test_reconciliation.py`
Current behavior:
- `loader.py` hardcodes declaration directories:
`services`, `capabilities`, `interfaces`, `dependencies`, and `bindings`.
- `validation.py` only accepts the legacy declaration kinds through
`SCHEMA_BY_KIND`.
- `graph.py` builds provider/consumer/binding behavior directly from those
declaration kinds.
- `canon.py` maps those declaration kinds into canonical categories, with
several partial or gap mappings.
- Tests assert the legacy kinds and edge types as the normal graph shape.
Required change:
- Add vNext semantic graph concepts for netkingdom, actor, fabric, subfabric,
owned node, utility interface, accounting attribution, and provenance.
- Keep legacy declaration kinds as a compatibility/evidence layer until they
can be migrated or intentionally retired.
- Stop treating environment tags as possible fabric boundaries.
### Metadata Owner Is Not Financial Ownership
Affected files:
- `schemas/common.schema.yaml`
- `railiance_fabric/graph.py`
- `railiance_fabric/scanner.py`
- `railiance_fabric/registry.py`
- `schemas/state-hub-export.schema.yaml`
- existing declaration files under `fabric/**`
Current behavior:
- `metadata.owner` is required for declarations, but it is just a string.
- Graph exports place owner inside node `attributes.owner`, not as a first-class
field.
- Discovery candidates can carry owner-like attributes, but there is no
enforced owner actor, role, inheritance rule, or unresolved-owner state.
- State Hub export nodes require `repo`, `domain`, and `lifecycle`, but not
financial owner, fabric, or subfabric.
Required change:
- Introduce actor identity and role: king, lord, tenant, and supporting
operator/steward where needed.
- Every accepted node must resolve to an owner actor, either explicit or
inherited from containing fabric/subfabric.
- Missing or ambiguous ownership must be represented as review state before
promotion.
### Fabric Membership Is Missing
Affected files:
- `schemas/state-hub-export.schema.yaml`
- `schemas/discovery-snapshot.schema.yaml`
- `railiance_fabric/scanner.py`
- `railiance_fabric/registry.py`
- `railiance_fabric/graph_explorer.py`
- `railiance_fabric/graph_explorer_ui.py`
- `docs/graph-explorer-contract.md`
- `docs/state-hub-integration.md`
Current behavior:
- Accepted graph nodes have repo/domain/lifecycle fields, but no `fabric_id`,
`subfabric_id`, containment path, or owning lord/tenant relation.
- Discovery snapshots are scoped to one `repo_slug`, one commit, and one scan
profile.
- Registry snapshots are stored per repository and combined by merging the
latest snapshot for each repo.
Required change:
- Add first-class fabric/subfabric identity and containment.
- Preserve repo scoping for repository evidence, but do not let repo scope
define fabric membership.
- Add a root netkingdom/fabric baseline for the current one-fabric Railiance
setup.
### Cross-Boundary Utility Is Flattened Into Dependency Edges
Affected files:
- `schemas/dependency.schema.yaml`
- `schemas/binding.schema.yaml`
- `railiance_fabric/graph.py`
- `railiance_fabric/registry.py`
- `railiance_fabric/canon.py`
- `railiance_fabric/graph_explorer.py`
- `railiance_fabric/graph_explorer_ui.py`
- `tests/test_registry.py`
- `tests/test_graph_explorer.py`
Current behavior:
- Consumers and providers are expressed as dependencies and binding assertions.
- Edges such as `consumes`, `binds:*`, and `uses_interface` map mostly to
canonical `depends_on`.
- There is no explicit provider-owner/consumer-owner boundary, payment schema,
metering basis, or business model on the edge.
Required change:
- Add a first-class cross-boundary utility edge concept.
- Preserve technical dependency edges, but distinguish them from economic or
tenancy value interfaces.
- Allow cross-boundary edges across fabric and subfabric boundaries without
treating the boundary crossing as an error.
### Cost/Profit Centers Are Not Represented
Affected files:
- all current graph schemas and exports
- `railiance_fabric/registry.py`
- `railiance_fabric/graph_explorer.py`
- `railiance_fabric/graph_explorer_ui.py`
- State Hub import contract in `schemas/state-hub-export.schema.yaml`
Current behavior:
- There are no node or edge fields for cost center, profit center, allocation
model, payment schema, metering basis, or attribution validity.
Required change:
- Add optional accounting attribution on nodes and edges.
- Ensure accounting attribution can drive views without changing fabric or
subfabric membership.
### Discovery Has Good Mechanics But Wrong Roots
Affected files:
- `railiance_fabric/scanner.py`
- `railiance_fabric/discovery.py`
- `railiance_fabric/reconciliation.py`
- `railiance_fabric/connectors.py`
- `schemas/discovery-snapshot.schema.yaml`
- `docs/repo-reality-scanner.md`
- `docs/operational-rescan-loops.md`
- `docs/registry-onboarding.md`
Current behavior:
- Discovery snapshots are provenance-rich and already separate candidates from
accepted graph state.
- Replacement scopes, stable keys, tombstones, review states, reconciliation
diffs, and connector runs are already present.
- Discovery is still repo-scoped and gives `repo_declaration` precedence.
- `local-fabric-registry` reads repository onboarding manifests, not
accountability roots or deployment responsibility boundaries.
Required change:
- Reuse the raw-evidence/candidate/accepted mechanics.
- Add accountability-root manifests and adapters for deployment automation,
infrastructure evidence, recovery/secrets/backup evidence, and ownership
boundaries.
- Change precedence so repo declarations can be strong self-description
evidence without overriding fabric-scoped deployment evidence.
### State Hub Export Is Too Thin For The New Model
Affected files:
- `schemas/state-hub-export.schema.yaml`
- `railiance_fabric/graph.py`
- `railiance_fabric/registry.py`
- `railiance_fabric/server.py`
- `docs/state-hub-integration.md`
- `docs/registry-api.md`
Current behavior:
- `/exports/state-hub` returns a `FabricGraphExport` with nodes and edges.
- Nodes require `id`, `kind`, `name`, `repo`, `domain`, and `lifecycle`.
- Edges require `from`, `to`, and `type`.
- Canon metadata and evidence state are present, but ownership, containment,
accounting, and utility metadata are not first-class.
Required change:
- Version the export contract.
- Add fabric/subfabric containment, owner actor identity, owner role,
cross-boundary utility metadata, and optional cost/profit attribution.
- Keep enough compatibility metadata for State Hub to support old imports until
`STATE-WP-0051` replaces or migrates the read model.
## Affected APIs And Commands
HTTP surfaces:
- `GET /exports/state-hub`
- `GET /exports/graph-explorer`
- `GET /exports/backstage`
- `GET /exports/xregistry`
- `GET /graph/providers`
- `GET /graph/consumers`
- `GET /graph/unresolved`
- `GET /graph/blast-radius`
- `GET /graph/dependency-path`
- `POST /repositories/{repo_slug}/snapshots`
- `POST /repositories/{repo_slug}/discovery-snapshots`
- `POST /repositories/{repo_slug}/discovery-snapshots/{id}/accept`
- `GET /repositories/{repo_slug}/inventory`
CLI surfaces:
- `railiance-fabric validate`
- `railiance-fabric export`
- `railiance-fabric providers`
- `railiance-fabric consumers`
- `railiance-fabric dependency-path`
- `railiance-fabric unresolved`
- `railiance-fabric blast-radius`
- `railiance-fabric scan`
- `railiance-fabric registry sync`
- `railiance-fabric registry sync-manifest`
- `railiance-fabric registry scan-manifest`
- `railiance-fabric registry ingest-discovery`
- `railiance-fabric registry accept-discovery`
- `railiance-fabric registry rescan-status`
Projection surfaces:
- Graph Explorer payload and manifest.
- Backstage projection.
- xRegistry projection.
- State Hub Fabric read-model import.
## Compatibility Risks
1. State Hub import compatibility:
`STATE-WP-0050` expects the current `FabricGraphExport` shape. Adding
required fields or changing node/edge semantics can break ingest unless the
export is versioned or State Hub is updated in lockstep.
2. Graph Explorer compatibility:
Graph Explorer filters and detail panes use current node kinds, layers,
dependency chains, unresolved states, and canon metadata. New actor/fabric
nodes and utility edges need UI mapping before the explorer remains useful.
3. Query command compatibility:
Provider/consumer/dependency-path commands are built on capability and
dependency declarations. They may remain useful as legacy views, but should
not be presented as the whole Fabric model.
4. Registry storage compatibility:
The registry stores snapshots as JSON, so schema migration is flexible, but
combined graph behavior currently deduplicates by node id across latest
per-repo snapshots. Fabric-scoped snapshots may need a root snapshot or
scenario/fabric identity instead of only repo latest snapshots.
5. Test fixture blast radius:
Most tests assert legacy declaration kinds, repo-scoped discovery keys, and
current export payload shape. The migration should add vNext tests before
replacing old assertions.
6. Canon mapping ambiguity:
Current canonical categories do not include financial actors, fabrics,
subfabrics, accounting attributions, or utility value interfaces. These
either need canonical mappings or explicit `mapping_fit: gap` handling.
7. Existing accepted graph baseline:
The current accepted graph contains 49 nodes and 58 edges from the
post-`RAIL-FAB-WP-0016` reset. It should be treated as a legacy baseline
and re-exported or archived before a controlled vNext reset.
## Migration Approach
1. Preserve legacy declarations as `v1alpha1` evidence.
Do not delete `fabric/` declarations or examples immediately. Reclassify
them as self-description and bootstrap evidence.
2. Introduce a vNext contract alongside the current export.
Add schema version metadata before making breaking changes. Prefer either
`railiance.fabric/v1alpha2` or an explicit export `schema_version`.
3. Add first-class model concepts before changing scanner roots.
Implement netkingdom, actor, fabric, subfabric, ownership, utility edge, and
accounting attribution semantics in schemas/tests first.
4. Extend discovery rather than replacing it.
Reuse `FabricDiscoverySnapshot`, replacement scopes, candidates,
provenance, reconciliation, and acceptance. Add accountability-root
manifest evidence and deployment automation adapters.
5. Seed the current one-fabric baseline.
Create the Railiance netkingdom root, current king, current lord/fabric, and
inherited ownership defaults. This lets existing nodes resolve ownership
before tenant subfabrics exist.
6. Version State Hub import.
Coordinate with `STATE-WP-0051` so State Hub can ingest both the old and
vNext export during the transition, or explicitly perform a read-model reset.
7. Rebuild and compare.
Produce a vNext export from the current baseline, compare it against the
legacy 49-node/58-edge export, and document intentional semantic changes.
## Follow-On Implementation Notes
For `RAIL-FAB-WP-0017-T02`:
- Define schema names and versioning for vNext graph exports.
- Decide whether netkingdom/fabric/actor are node kinds, top-level export
sections, or both.
- Define ownership inheritance and unresolved-owner representation.
- Define utility edge fields:
provider owner, consumer owner, provider boundary, consumer boundary,
payment schema, metering basis, and evidence.
- Define accounting fields:
cost center, profit center, allocation model, attribution validity.
For `RAIL-FAB-WP-0017-T03`:
- Add model classes or typed helpers beyond the generic `Declaration` wrapper.
- Extend validation to enforce resolvable ownership on accepted nodes.
- Update registry projection to preserve ownership, containment, accounting,
and utility metadata.
- Keep old declaration graph validation available as a legacy command or
compatibility path.
For `RAIL-FAB-WP-0017-T04`:
- Update `schemas/state-hub-export.schema.yaml`.
- Add sample vNext export payloads and tests.
- Coordinate the import fields with `STATE-WP-0051`.
For `RAIL-FAB-WP-0018`:
- Build an accountability-root manifest instead of extending
`registry/local-repos.yaml` into too many meanings.
- Treat State Hub attached repos as one discovery root, not as the Fabric
boundary itself.
- Add deployment automation and infrastructure evidence adapters before trying
to infer cross-boundary value edges.

View File

@@ -0,0 +1,226 @@
# Financial Fabric Operator Guide
This guide is the operator path for the financial Fabric reset described in
`docs/FabricDiscoveryAndUpdate.md`.
## Current Baseline
Railiance currently has one effective fabric because one actor pays for the
infrastructure. The accepted baseline lives at:
```text
fabric/financial/railiance-netkingdom.yaml
```
It defines:
- `railiance.netkingdom` as the netkingdom root;
- `actor.railiance.king` as the king with recovery, secrets, backup, and
termination authority;
- `actor.railiance.primary-lord` as the lord who pays for the current fabric;
- `fabric.railiance.primary` as the current active fabric;
- inherited ownership for accepted nodes until a more specific owner is
discovered or approved;
- a future subfabric template for tenant modeling.
The baseline is not a security-zone model. Do not introduce realm, domain, or
identity-zone language into the Fabric core when updating it.
## Deployment Overlay Baseline
Fabric membership is not the same as deployment environment. Use these current
Railiance deployment overlays when interpreting discovery evidence:
| Scenario | Environment | Access zone baseline | Notes |
|----------|-------------|----------------------|-------|
| `bernd-laptop` | `dev` | `private-dev` | Local machine used by one developer. Its loopback ports and local tools are useful evidence for debugging, but are not shared truth for other developers. |
| `coulombcore` | `test` | `collaborator-test`, `early-access` | Central test-stage server for collaborating developers and friendly early-access users. |
| `railiance01` | `prod` | `production-public`, `production-admin` | Production host. Developer access exists during alpha, but production should be able to move toward restricted operator access and applicable security and data-privacy controls. |
The routing authority is the source that maps names or ports to services in a
scenario. For local dev this may be a Makefile, script, process launcher, or
loopback binding. For test and production this should usually come from
Kubernetes `Service` and `Ingress`, Traefik/nginx/Caddy/HAProxy configuration,
DNS, and TLS automation.
Fabric should discover those routes and visualize the resulting zones. It
should not allocate ports, author reverse-proxy config, or define the access
policy. The policy authority is external evidence, such as NetKingdom IAM,
ingress policy, network policy, or local-only loopback binding.
## Refresh Loop
Run these checks from the `railiance-fabric` repo after changing declarations,
baseline ownership, discovery inputs, or export code:
```bash
railiance-fabric validate .
railiance-fabric export --format json
railiance-fabric export --format financial
python3 -m pytest tests/test_registry.py -q
```
Use the legacy JSON export for compatibility with existing `STATE-WP-0050`
State Hub behavior. Use the financial export to verify the vNext contract and
the ownership/fabric projection.
For accountability-root discovery, start from the current root manifest:
```text
fabric/discovery/railiance-accountability-roots.yaml
```
The manifest schema is documented in `docs/accountability-root-manifest.md`.
To collect raw evidence from those roots without promoting graph state:
```bash
railiance-fabric discover-roots --max-items-per-root 200
```
To inspect normalized identity candidates or persist a local evidence run:
```bash
railiance-fabric discover-roots --identity-projection
railiance-fabric discover-roots --store-db .railiance-fabric/accountability-evidence.sqlite3
```
To inspect ownership blockers and apply review decisions:
```bash
railiance-fabric discover-roots --ownership-review \
--store-db .railiance-fabric/accountability-evidence.sqlite3
railiance-fabric review-identity <stable-key> \
--store-db .railiance-fabric/accountability-evidence.sqlite3 \
--decision accept \
--owner-actor-id actor.railiance.primary-lord \
--fabric-id fabric.railiance.primary
```
To compare a new run with saved review outputs:
```bash
railiance-fabric discover-roots --delta \
--previous-identity-projection previous-identities.json \
--previous-ownership-review previous-ownership.json
```
The current bootstrap artifacts live at:
```text
fabric/discovery/snapshots/2026-05-24-railiance-bootstrap-identities.json
fabric/discovery/snapshots/2026-05-24-railiance-bootstrap-ownership-review.json
fabric/discovery/snapshots/2026-05-24-railiance-bootstrap-update-delta.json
exports/state-hub/2026-05-24-railiance-financial-fabric-v1.json
```
To refresh the same artifact set:
```bash
railiance-fabric discover-roots --include-remote --max-items-per-root 200 \
--identity-projection > fabric/discovery/snapshots/YYYY-MM-DD-railiance-bootstrap-identities.json
railiance-fabric discover-roots --include-remote --max-items-per-root 200 \
--ownership-review > fabric/discovery/snapshots/YYYY-MM-DD-railiance-bootstrap-ownership-review.json
railiance-fabric discover-roots --include-remote --max-items-per-root 200 \
--delta > fabric/discovery/snapshots/YYYY-MM-DD-railiance-bootstrap-update-delta.json
railiance-fabric export --format financial . \
> exports/state-hub/YYYY-MM-DD-railiance-financial-fabric-v1.json
```
The financial export must satisfy these invariants:
- every accepted node has resolvable ownership;
- every accepted node has fabric containment;
- cost/profit center fields are optional accounting attribution, not fabric
boundaries;
- cross-boundary utility edges carry provider and consumer owner context when
known;
- unresolved ownership or containment remains explicit rather than invented by
State Hub.
## Registry Reset Rebuild
After a guarded registry reset, rebuild from durable roots in this order:
1. Confirm the baseline still reflects who pays for the infrastructure.
2. Validate the repo evidence with `railiance-fabric validate .`.
3. Emit `railiance-fabric export --format financial` and review the ownership
projection.
4. Start the registry and sync this repo:
```bash
railiance-fabric-registry --db .railiance-fabric/registry.sqlite3 --port 8765
railiance-fabric registry sync --repo-slug railiance-fabric .
```
5. Sync broader known repos with `registry/local-repos.yaml` or
`registry/railiance-repos.yaml` when their local checkouts exist.
6. Compare snapshot diffs before promoting new discovery output.
Repo-local `fabric/` declarations are evidence during this rebuild. The
replacement discovery loop should start from the netkingdom, king, lords,
tenants, deployment automation, service manifests, endpoints, backups, and
secret roots rather than asking each repo to own all external relations.
## State Hub Handoff
State Hub is a read model for Fabric topology. It should ingest Fabric exports,
link them to repos/workstreams/tasks, and expose dashboard/search views. It
should not author ownership, fabric membership, or utility boundaries.
Until `STATE-WP-0051` is implemented:
- continue importing or syncing the legacy `v1alpha1` graph for existing State
Hub graph views;
- keep the `v1alpha2` financial export as the contract artifact for State Hub
implementation and review;
- update file-backed workplan state through State Hub consistency after
workplan edits:
```bash
cd ~/state-hub
make fix-consistency REPO=railiance-fabric
```
After `STATE-WP-0051`, the financial export should become the preferred State
Hub graph import. The importer must preserve netkingdom, actors, fabrics,
containment, ownership, accounting attribution, cross-boundary utility context,
and unresolved gaps.
To import a saved financial export into State Hub:
```bash
curl -s -X POST \
"http://127.0.0.1:8000/fabric/graph-exports?source_repo_slug=railiance-fabric" \
-H "Content-Type: application/json" \
--data-binary @exports/state-hub/2026-05-24-railiance-financial-fabric-v1.json
```
If the `/fabric/graph-exports` endpoints return `500` while ordinary State Hub
routes work, run the State Hub migrations and retry:
```bash
cd ~/state-hub
make migrate
# or, when uv is not on PATH:
.venv/bin/alembic upgrade head
```
## Discovery Work Handoff
The next discovery/update-loop work should replace the baseline projection with
durable evidence collection:
- discover repos from State Hub and Gitea;
- discover deployment realities from automation and infrastructure manifests;
- discover services, machines, endpoints, backup roots, and secret roots;
- normalize identities across repo URLs, host paths, image names, service
names, and endpoint contracts;
- resolve owner and containment from the baseline, explicit evidence, or
review decisions;
- emit deltas for ownership, containment, nodes, edges, and cross-boundary
utility changes.
The baseline projection is therefore a safe bridge, not the final discovery
engine.

View File

@@ -0,0 +1,493 @@
# Financial Fabric VNext Contract
Date: 2026-05-24
Workplan: `RAIL-FAB-WP-0017`
Status: contract draft for implementation
## Intent
The vNext Fabric contract models the Railiance netkingdom as a durable
infrastructure-responsibility graph.
The contract separates:
- financial responsibility boundaries from environments and views;
- ownership from repository metadata;
- cross-boundary utility interfaces from ordinary technical dependencies;
- accounting attribution from fabric membership;
- durable topology from live telemetry.
The contract should be implemented as a versioned export shape before replacing
the existing `railiance.fabric/v1alpha1` declaration-centered graph.
## Versioning
Recommended contract identity:
```yaml
apiVersion: railiance.fabric/v1alpha2
kind: FabricGraphExport
schema_version: financial-fabric-v1
```
`v1alpha1` remains the compatibility layer for legacy declarations and the
current State Hub import. `v1alpha2` introduces first-class netkingdom,
actor, fabric, subfabric, ownership, utility, and accounting semantics.
## Top-Level Export Shape
```yaml
apiVersion: railiance.fabric/v1alpha2
kind: FabricGraphExport
schema_version: financial-fabric-v1
generated_at: "2026-05-24T00:00:00Z"
source:
producer: railiance-fabric
registry: registry
commit: ""
generation_reason: rebuild | rescan | operator_refresh
compatibility:
legacy_v1alpha1_supported: true
breaking_reset: false
netkingdom:
id: railiance.netkingdom
name: Railiance Netkingdom
king_actor_id: actor.railiance.king
actors: []
fabrics: []
nodes: []
edges: []
unresolved: []
```
The top-level sections are intentionally explicit. `nodes` and `edges` remain
the primary graph payload for State Hub and visualizers, but `netkingdom`,
`actors`, and `fabrics` define the responsibility boundary context used to
resolve ownership and containment.
## Actors
Actors are accountable parties. They are graph addressable and may also be
materialized as nodes when a consumer needs a pure node/edge projection.
```yaml
id: actor.railiance.king
kind: FabricActor
role: king
name: Railiance King
legal_name: ""
description: Responsible for the Railiance netkingdom.
authority:
recovery_authority: true
secrets_authority: true
backup_authority: true
termination_authority: true
relationships:
contracts_with:
- actor.railiance.primary-lord
```
Actor roles:
- `king`: responsible for the netkingdom and relevant recovery/secrets/backup
authority.
- `lord`: pays for a fabric.
- `tenant`: pays for restricted use of a subfabric.
- `operator`: operates infrastructure on behalf of a king, lord, or tenant.
- `steward`: maintains a repo, service, or capability but does not necessarily
pay for it.
Only king, lord, and tenant define financial boundary semantics. Operator and
steward roles are supporting roles.
## Fabrics And Subfabrics
Fabrics and subfabrics are responsibility boundaries.
```yaml
id: fabric.railiance.primary
kind: Fabric
name: Railiance Primary Fabric
netkingdom_id: railiance.netkingdom
lord_actor_id: actor.railiance.primary-lord
parent_fabric_id: null
boundary:
boundary_type: fabric
criterion: financial_and_operational_accountability
payment_responsibility: actor.railiance.primary-lord
operational_responsibility: actor.railiance.king
recovery_responsibility: actor.railiance.king
status: active
evidence_refs: []
```
```yaml
id: subfabric.railiance.tenant.coulomb
kind: Subfabric
name: Coulomb Tenant Subfabric
netkingdom_id: railiance.netkingdom
parent_fabric_id: fabric.railiance.primary
tenant_actor_id: actor.coulomb.tenant
boundary:
boundary_type: subfabric
criterion: restricted_paid_tenant_utility
payment_responsibility: actor.coulomb.tenant
operational_responsibility: actor.railiance.primary-lord
recovery_responsibility: actor.railiance.king
status: planned
evidence_refs: []
```
Fabric membership changes when payment, operational responsibility, recovery
responsibility, or legal accountability changes. It does not change because a
service moves environments, a host is replaced, a repo is refactored, or a
diagnostic view changes.
## Nodes
Every accepted node must resolve ownership and containment.
```yaml
id: state-hub.api
kind: Service
name: State Hub API
repo: state-hub
domain: custodian
lifecycle: active
containment:
netkingdom_id: railiance.netkingdom
fabric_id: fabric.railiance.primary
subfabric_id: null
environment: local
deployment_scenario_id: null
ownership:
owner_actor_id: actor.railiance.primary-lord
owner_role: lord
resolution: inherited
inherited_from: fabric.railiance.primary
supporting_actor_ids:
- actor.state-hub.steward
accounting:
cost_center_id: null
profit_center_id: null
allocation_model: null
evidence:
state: observed
review_state: accepted
confidence: 0.95
refs: []
attributes: {}
```
Required accepted-node semantics:
- `containment.netkingdom_id`
- `containment.fabric_id`
- `ownership.owner_actor_id`
- `ownership.owner_role`
- `ownership.resolution`
- `evidence.state`
- `evidence.review_state`
Recommended compatibility fields:
- `repo`
- `domain`
- `lifecycle`
- `canon_category`
- `canon_anchor`
- `mapping_fit`
- `attributes`
## Ownership Resolution
Ownership resolution values:
- `explicit`: evidence directly names the owner actor.
- `inherited`: ownership is inherited from containing fabric or subfabric.
- `unresolved`: no acceptable owner could be resolved.
- `ambiguous`: multiple plausible owners exist and review is needed.
Unresolved or ambiguous nodes may appear as candidates or importable read-model
gaps, but they must not silently become accepted graph truth.
```yaml
id: unresolved:node:fixture
target_id: fixture.unknown-service
kind: unresolved_ownership
severity: needs_review
message: Node has no explicit owner and no resolvable fabric/subfabric owner.
evidence_refs: []
```
## Edges
Edges keep technical topology while adding boundary and value semantics.
```yaml
id: edge:state-hub.api:exposes:state-hub.http
from: state-hub.api
to: state-hub.http
type: exposes
relationship_category: technical
canonical_type: exposes
boundary:
crosses_fabric_boundary: false
crosses_subfabric_boundary: false
evidence:
state: observed
review_state: accepted
confidence: 0.95
refs: []
attributes: {}
```
Relationship categories:
- `containment`: netkingdom, fabric, subfabric, actor, or node containment.
- `ownership`: owner/steward/operator relationships.
- `technical`: deployment, exposure, dependency, flow, build, or evidence edge.
- `utility`: value-bearing interface from provider to consumer.
- `accounting`: cost/profit attribution or allocation edge.
- `evidence`: source artifact or provenance relationship.
## Cross-Boundary Utility Edges
Cross-boundary utility edges are first-class because they represent value moving
across financial responsibility boundaries.
```yaml
id: utility:state-hub-api:coulomb-tenant
from: state-hub.http
to: tenant.coulomb.automation-client
type: provides_utility_to
relationship_category: utility
canonical_type: depends_on
provider:
owner_actor_id: actor.railiance.primary-lord
fabric_id: fabric.railiance.primary
subfabric_id: null
consumer:
owner_actor_id: actor.coulomb.tenant
fabric_id: fabric.railiance.primary
subfabric_id: subfabric.railiance.tenant.coulomb
boundary:
crosses_fabric_boundary: false
crosses_subfabric_boundary: true
utility:
utility_type: coordination_api
contract_id: state-hub.http
payment_schema_id: payment.internal-tenant-access
metering_basis: request | seat | flat | allocation | unknown
business_model: tenant_utility
accounting:
provider_profit_center_id: null
consumer_cost_center_id: null
allocation_model: null
evidence:
state: declared
review_state: accepted
confidence: 0.8
refs: []
attributes: {}
```
The edge may cross a subfabric boundary without crossing the parent fabric
boundary. This is expected for tenant utility inside the current Railiance
fabric.
## Cost And Profit Center Attribution
Accounting attribution is optional and must not redefine containment.
Node-level attribution:
```yaml
accounting:
cost_center_id: cc.platform.shared
profit_center_id: null
allocation_model: direct
valid_from: "2026-05-24"
valid_until: null
```
Edge-level attribution:
```yaml
accounting:
provider_profit_center_id: pc.tenant-utilities
consumer_cost_center_id: cc.coulomb.automation
allocation_model: usage_weighted
payment_schema_id: payment.internal-tenant-access
metering_basis: request
valid_from: "2026-05-24"
valid_until: null
```
Cost/profit center changes should produce accounting deltas and views, not
fabric moves.
## Evidence And Provenance
Every discovered or accepted fact should carry evidence.
```yaml
evidence:
state: observed | declared | inferred | proposed | gap
review_state: accepted | candidate | needs_review | rejected
confidence: 0.0
refs:
- id: evidence:state-hub:compose:api
source_kind: deployment_manifest
source_path: compose.yaml
source_url: null
source_repo: state-hub
source_commit: abc123
scanner: compose
scanner_version: "0.1.0"
content_hash: sha256:...
observed_at: "2026-05-24T00:00:00Z"
```
The contract continues to distinguish durable discovered configuration from
live operational telemetry. Health, current running state, latency, incident
status, and usage volume are not Fabric core fields.
## Legacy Canon Mapping
Existing canonical metadata remains useful for compatibility, but some legacy
terms should be demoted to evidence.
| Existing kind/type | vNext disposition |
| --- | --- |
| `Repository` | Keep as source-repository node evidence. |
| `ServiceDeclaration` | Legacy self-description evidence for `Service`. |
| `CapabilityDeclaration` | Legacy self-description evidence for utility or service capability. |
| `InterfaceDeclaration` | Legacy self-description evidence for `Endpoint` or `Contract`. |
| `DependencyDeclaration` | Legacy consumer-purpose or dependency evidence; not sufficient for value edge alone. |
| `BindingAssertion` | Legacy binding evidence; may support utility edge when boundary actors are known. |
| `RuntimeService`, `DeploymentService`, `Kubernetes*`, `Server` | Durable deployment/runtime-resource evidence. |
| `provides`, `exposes`, `runs_on`, `deployed_as`, `flows_to` | Keep as technical/topology edges. |
| `consumes`, `binds:*`, `uses_interface` | Keep as technical dependency edges; promote to utility edges only when provider/consumer ownership and boundary context are known. |
| `declares` | Display/evidence edge only. |
New vNext node kinds to map:
| vNext kind | Suggested canonical category |
| --- | --- |
| `Netkingdom` | `software-system` until canon has a better category. |
| `FabricActor` | `consumer-purpose` or `control` depending role; likely canon gap. |
| `Fabric` | `software-system` or `control`; likely canon gap. |
| `Subfabric` | `software-system` or `control`; likely canon gap. |
| `UtilityInterface` | `endpoint` when technical, `software-system` when abstract. |
| `CostCenter` | `control`; likely canon gap. |
| `ProfitCenter` | `control`; likely canon gap. |
New vNext edge types to map:
| vNext edge type | Suggested canonical type |
| --- | --- |
| `contains` | `part_of` |
| `owned_by` | `governed_by` |
| `operated_by` | `governed_by` |
| `provides_utility_to` | `depends_on` with `relationship_category: utility` |
| `attributed_to_cost_center` | `governed_by` or canon gap |
| `attributed_to_profit_center` | `governed_by` or canon gap |
| `evidenced_by` | `evidenced_by` |
## Examples
### Current Single Railiance Fabric
```yaml
netkingdom:
id: railiance.netkingdom
name: Railiance Netkingdom
king_actor_id: actor.railiance.king
actors:
- id: actor.railiance.king
kind: FabricActor
role: king
name: Railiance King
- id: actor.railiance.primary-lord
kind: FabricActor
role: lord
name: Railiance Primary Lord
fabrics:
- id: fabric.railiance.primary
kind: Fabric
name: Railiance Primary Fabric
netkingdom_id: railiance.netkingdom
lord_actor_id: actor.railiance.primary-lord
parent_fabric_id: null
status: active
```
### Future Tenant Subfabric
```yaml
actors:
- id: actor.coulomb.tenant
kind: FabricActor
role: tenant
name: Coulomb Tenant
fabrics:
- id: subfabric.railiance.tenant.coulomb
kind: Subfabric
name: Coulomb Tenant Subfabric
netkingdom_id: railiance.netkingdom
parent_fabric_id: fabric.railiance.primary
tenant_actor_id: actor.coulomb.tenant
status: planned
```
### Cross-Subfabric Utility
```yaml
edges:
- id: utility:state-hub-http:coulomb-client
from: state-hub.http
to: coulomb.automation-client
type: provides_utility_to
relationship_category: utility
provider:
owner_actor_id: actor.railiance.primary-lord
fabric_id: fabric.railiance.primary
subfabric_id: null
consumer:
owner_actor_id: actor.coulomb.tenant
fabric_id: fabric.railiance.primary
subfabric_id: subfabric.railiance.tenant.coulomb
boundary:
crosses_fabric_boundary: false
crosses_subfabric_boundary: true
utility:
utility_type: coordination_api
contract_id: state-hub.http
payment_schema_id: payment.internal-tenant-access
metering_basis: unknown
business_model: tenant_utility
```
## State Hub Import Requirements
State Hub should materialize at least:
- export version and schema version;
- netkingdom id;
- fabric/subfabric ids and names;
- actor ids, roles, and names;
- node containment fields;
- node owner actor and owner role;
- ownership resolution state;
- edge relationship category;
- utility provider/consumer ownership context;
- boundary crossing booleans;
- cost/profit center attribution;
- unresolved ownership and containment gaps;
- retained canon metadata and evidence state.
State Hub must not invent missing ownership or redefine fabric membership. It
may display unresolved gaps and expose filters/views for operator review.

View File

@@ -2,8 +2,9 @@
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.
the long-term target is for each owning repo to contribute local evidence while
financial Fabric ownership and boundary decisions come from accountability-root
discovery.
## Seeded Repos
@@ -28,8 +29,9 @@ For each owning repo:
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.
6. Once accountability-root discovery can reproduce the graph, mark the
central seed declarations as bootstrap evidence and keep only the repo-local
facts that remain useful self-description.
## Suggested Order

View File

@@ -89,6 +89,13 @@ Hosts should also include useful optional fields when available: `label`,
`freshnessState`, `confidence`, `visualSize`, `ownership`, `unresolved`,
`sourceReferences`, and `deepLinks`.
Fabric hosts should also include deployment overlay fields when available:
`deploymentEnvironment`, `deploymentScenario`, `routingAuthority`,
`accessZone`, `policyAuthority`, `exposure`, and `host`. These fields describe
where an element runs or is reachable in a concrete deployment scenario. They
do not define fabric membership and must remain filter/grouping metadata unless
the host explicitly promotes a separate graph relationship with evidence.
Edges may include layout hints used by the client-side layout engine:
`sameRepo`, `sourceRepo`, `targetRepo`, `layoutAffinity`,
`layoutIdealLength`, and `layoutElasticity`. Fabric uses these hints to keep
@@ -97,6 +104,32 @@ edges are intentionally shortest and most elastic; deployment-to-repo edges are
longer and looser so infrastructure placement does not collapse into the repo
node.
## Semantic Attractor Modes
Semantic attractors are view-only topic poles that can pull graph entities
toward conceptual neighborhoods in spring-based layouts. For repository maps,
an operator might choose attractors such as `security`, `development`, and
`operations`; Fabric can then score each repository's semantic closeness to
those attractors from repo-owned `SCOPE.md` evidence and map the score to
layout strength.
Attractors are not domain edges and do not change Fabric graph data. They may
be materialized as synthetic display-only nodes and `semantic_attraction`
edges, or carried as top-level view metadata that the renderer turns into
layout forces. Attraction scores should remain inspectable, with source
references and confidence, so the operator can understand why a repository was
pulled toward a topic.
Unlike zones, attractors may overlap. A repository can be close to both
`development` and `operations`, and the layout should place it between those
poles. Zone resolvers, boundary diagnostics, dependency queries, blast-radius
queries, and collapsed-zone boundary edges should ignore semantic attraction
edges unless a host explicitly promotes an attractor relation into canonical
graph data.
See `docs/semantic-attractors.md` for the concept model, scoring semantics,
payload direction, and implementation path.
## Display State Ownership
The contract allows either the host service or the engine to evaluate display
@@ -132,6 +165,94 @@ The first Fabric manifest declares:
| `binding` | Binding assertions between consumer dependencies and providers. |
| `library` | Future library/SBOM inventory nodes. |
## Zone Overlay Modes
The graph explorer should support zone-oriented modes for Fabric payloads:
| Mode | Purpose |
|------|---------|
| `fabric` | Group by financial responsibility: fabric, subfabric, owner. |
| `environment` | Group by `dev`, `test`, `prod`, or other deployment environment. |
| `deployment-scenario` | Group by concrete scenario such as `bernd-laptop`, `coulombcore`, or `railiance01`. |
| `routing-authority` | Group by loopback launcher, Compose port mapping, ingress controller, reverse proxy, DNS, or equivalent route authority. |
| `access-zone` | Group by intended reachability such as `private-dev`, `collaborator-test`, `early-access`, `production-public`, or `production-admin`. |
Zone modes are diagnostic views. They answer "where does this run or who can
reach it here?" without mutating the underlying Fabric responsibility boundary.
Zone boundary overlays are a visual layer over the graph canvas. They should be
computed from visible node positions after layout/filtering rather than modeled
as graph parent nodes. The overlay should be backed by explicit zone definitions
and a resolver so visible nodes are assigned to at most one rendered zone. The
default boundary grouping is `deploymentEnvironment`:
| Overlay label | Matching nodes |
|---------------|----------------|
| `dev-tegwick` | visible nodes with `deploymentEnvironment: dev` in the private local development overlay |
| `test` | visible nodes with `deploymentEnvironment: test` or legacy `staging` |
| `prod` | visible nodes with `deploymentEnvironment: prod` |
Nodes without deployment overlay data are not enclosed. Edge-only deployment
overlay evidence may contribute to zone summaries and warnings, but it should
not create a boundary unless at least one visible node belongs to the same zone.
When access-zone grouping is selected, boundaries use `accessZone` values such
as `private-dev`, `collaborator-test`, `production-public`, or
`production-admin`.
Zone labels should be rendered as plain text in the upper-left corner of the
zone rectangle, without badge frames or white backgrounds. The label may still
act as the zone's focus/drag handle as long as it visually reads as text drawn
on the zone surface.
Useful warnings for the graph explorer include:
- control surfaces in user-facing access zones;
- production nodes with unrestricted developer access;
- early-access routes without a policy authority;
- services present in production but missing from test;
- local-only surfaces that appear in shared or production scenarios;
- conflicting port or host claims within the same deployment scenario.
Zone resolvers should also expose scoped diagnostics in zone detail panels. The
initial diagnostic set includes empty zone seed sets, visible nodes matched by
multiple zone definitions, and edges crossing zone boundaries. Attraction
diagnostics such as multiple attraction candidates or depth-limit stops belong
to the same resolver diagnostic channel when attraction rules are enabled.
Display-only context edges, such as repository `declares` edges, are evidence
for where declarations came from. They must not count as zone boundary
connectivity, attraction paths, or collapsed-zone boundary edges unless a host
explicitly promotes them to canonical graph relationships.
Saved graph profiles should persist zone view state as an explicit nested
`zone` object. The initial fields are `visible`, `grouping`, `definitionSet`,
`presentation`, and `containers`. URL parameters may continue to expose
compatibility aliases such as `zoneBoundaries`, `zoneGrouping`, and
`zoneDefinitionSet`, but saved profiles should prefer the nested object so
future zone definition sets, presentation preferences, and operator-placed zone
surfaces can be restored without another state migration.
Zone containers are view state, not fabric data. A container stores a stable
zone surface position and size in graph coordinates. Global graph layout may
place unzoned nodes and provide an initial center for new zones, but existing
zone containers should keep their operator-chosen positions when the layout
algorithm changes. After the global layout pass, each zone may project its
assigned visible nodes into local coordinates inside its container. The current
local layout choices are compact grid and circle. The selected zone-local
layout algorithm belongs in the nested `zone.layout.algorithm` view state so it
can be restored by saved or copied views without changing the Fabric payload.
Zone collapse is a view-only operation. A collapsed zone should hide its visible
member nodes, replace them with a synthetic zone node, and draw synthetic
boundary edges from that zone node to visible external neighbors. Internal edges
are summarized on the zone node rather than rendered. Expanding the zone removes
the synthetic elements and restores the original graph elements without
changing the underlying payload.
Zone dragging is also view-only. Dragging a zone handle should translate the
currently assigned visible member nodes by the same delta and then recompute the
overlay bounds from the new node positions. The operation updates Cytoscape view
coordinates only; it does not change Fabric graph data.
## Repo-Scoping Compatibility
Repo-scoping can adapt without a rewrite because its current graph payload
@@ -167,3 +288,5 @@ Host repos should own:
- authentication and authorization
- domain-specific graph modes
- deep links back to source systems
- deployment overlay extraction from the route/proxy/deployment authority they
control

View File

@@ -6,18 +6,38 @@ verified, and how to extract the shared engine once the second adapter is ready.
## Launch
Start the registry against the local SQLite database:
List available Make targets from the repo root:
```bash
railiance-fabric-registry --db .railiance-fabric/registry.sqlite3 --port 8765
make
```
Open the map:
Start the registry-backed graph explorer explicitly:
```bash
make graph-explorer
```
This serves the graph explorer at:
```text
http://127.0.0.1:8765/ui/graph-explorer
```
The target defaults to `.railiance-fabric/registry.sqlite3`, host
`127.0.0.1`, and port `8765`. Override those when needed:
```bash
make graph-explorer PORT=8876
make graph-explorer HOST=0.0.0.0 PORT=8765 REGISTRY_DB=/tmp/railiance-fabric.sqlite3
```
Equivalent raw command:
```bash
railiance-fabric-registry --db .railiance-fabric/registry.sqlite3 --port 8765
```
Useful supporting endpoints:
```text

View File

@@ -1,7 +1,10 @@
# Registry API
The Railiance Fabric registry is a small HTTP API over repo-owned Fabric
declarations and supporting inventory.
The Railiance Fabric registry is a small HTTP API over accepted Fabric graph
snapshots, discovery evidence, reset archives, and supporting inventory.
Legacy repo-local declarations can still produce snapshots, but they are
evidence for the graph rather than the long-term authority for external
deployment, ownership, tenant, or utility relations.
## Health And Status
@@ -27,7 +30,9 @@ GET /repositories/{repo_slug}/snapshots/diff
```
Snapshot ingestion accepts a `FabricGraphExport` under `graph` plus `commit`
and optional `generated_at`.
and optional `generated_at`. The registry accepts both legacy
`railiance.fabric/v1alpha1` graph exports and the financial
`railiance.fabric/v1alpha2` / `financial-fabric-v1` export contract.
## Discovery Snapshots
@@ -99,4 +104,23 @@ GET /exports/libraries/xregistry
GET /ui/graph-explorer
GET /exports/graph-explorer/manifest
GET /exports/graph-explorer
GET /exports/reset-archive
```
`GET /exports/state-hub` currently serves the accepted combined graph for the
State Hub read model. During the financial reset, keep using the legacy shape
for existing State Hub views until `STATE-WP-0051` materializes v1alpha2
fields. Use `railiance-fabric export --format financial` and
`examples/exports/financial-fabric-v1.json` as the vNext contract references.
## Guarded Reset
```text
POST /admin/reset-graph-data
```
The reset endpoint requires `confirm`,
`reason`, and `archive_sha256`. `confirm` must be
`RESET-RAILIANCE-FABRIC-GRAPH-DATA`. Operators should prefer the CLI wrapper
documented in `docs/registry-reset-operations.md`, because it exports the
archive and computes the checksum before calling the destructive endpoint.

View File

@@ -1,9 +1,15 @@
# Registry Onboarding
Multi-repo onboarding uses a repo-owned manifest to register ecosystem
Multi-repo onboarding uses an operator-owned manifest to register ecosystem
repositories with a running Railiance Fabric registry and to push graph and
library inventory when the local checkout has the required inputs.
Repo-local declarations remain useful self-description evidence. They should
not be treated as the default authority for external fabric membership,
ownership, tenant boundaries, or cross-boundary utility relations. Those belong
to the accountability-root discovery model described in
`docs/FabricDiscoveryAndUpdate.md`.
## Run The Railiance Manifest
Start the registry:
@@ -57,7 +63,7 @@ registration.
Each repository entry is registered first. If the checkout is unavailable or has
no Fabric declarations yet, the command leaves the repo registered and reports a
warning. This lets the registry represent known repos before every repo has
adopted local declarations.
published local evidence.
When declarations exist, the command validates them, builds a graph snapshot,
and posts it to:
@@ -67,11 +73,18 @@ POST /repositories/{repo_slug}/snapshots
```
The first Railiance manifest keeps the seed ecosystem graph in
`railiance-fabric` and registers adjacent repos as known sources. As those repos
adopt repo-local `fabric/` declarations, they can be synced without changing the
registry API.
`railiance-fabric` and registers adjacent repos as known sources. As discovery
roots mature, adjacent repos can contribute local evidence without making each
repo responsible for the whole external fabric relation model.
`registry/local-repos.yaml` is a broader local-host manifest. It is generated
from active State Hub repo records whose local path exists on the current WSL
host. It registers cross-domain local repos into the Fabric registry while only
syncing graph snapshots for repos that already have Fabric declarations.
When two active repo records point at the same checkout path, accountability
root discovery treats them as aliases of one repository identity. The canonical
identity can be declared with `canonical_slug`; otherwise discovery prefers the
slug matching the checkout directory name and then the entry with a remote URL.
Use `split_identity: true` only when shared paths are intentional and should
remain separate review candidates.

View File

@@ -0,0 +1,61 @@
# Registry Reset Operations
RAIL-FAB-WP-0016 allows a destructive registry graph reset, but only after an
archive has been produced and the operator uses an explicit confirmation token.
## Export Archive Only
```bash
railiance-fabric registry export-reset-archive \
.railiance-fabric/reset-archive.json \
--registry-url http://127.0.0.1:8765
```
The archive contains:
- repository registrations,
- current combined graph export,
- stored graph snapshots,
- discovery snapshots and accepted graph snapshot links,
- artifacts,
- library inventory,
- prior reset events,
- rollback notes.
## Guarded Reset
```bash
railiance-fabric registry reset-graph-data \
--registry-url http://127.0.0.1:8765 \
--archive .railiance-fabric/reset-archive.json \
--confirm RESET-RAILIANCE-FABRIC-GRAPH-DATA \
--reason "canon-aligned graph reset before full reingest"
```
The command first writes the archive, computes its SHA-256, then calls the
registry reset endpoint. The registry records a reset event with the archive
path, archive checksum, reason, and dropped row counts.
The reset deletes graph snapshots, discovery snapshots, artifacts, and library
inventory. Repository registration rows are preserved so reingest can start
from the known repo list.
## Rollback Limits
The archive is a JSON evidence bundle, not an automatic SQLite restore. Use it
to inspect or manually reinsert prior registry data if needed. After reset, the
intended source of truth is a fresh scan and acceptance pass from accountability
roots using the financial Fabric model. The current bridge is the Railiance
baseline projection:
```bash
railiance-fabric validate .
railiance-fabric export --format financial
railiance-fabric registry sync --repo-slug railiance-fabric .
```
Use repo-local declarations as evidence during reingest, not as the authority
for all external ownership, tenant, or utility relations.
Do not run the reset until the replacement scanner/projection path has passed
validation and a sample reingest review.

View File

@@ -2,8 +2,10 @@
The repo reality scanner discovers Fabric entities from repository evidence and
turns them into candidate graph facts. It is a discovery layer, not a new
authoring surface. Repo-owned declarations remain the highest-trust source for
accepted Fabric graph data.
authoring surface. Repo-owned declarations remain high-trust self-description
evidence, but financial Fabric ownership, tenant boundaries, and cross-boundary
utility relations must be resolved from accountability roots or review
decisions before they become accepted graph data.
## Contract
@@ -76,9 +78,10 @@ returns malformed JSON, the scanner records a `review_artifacts` entry and keeps
the discovery snapshot schema-valid.
The LLM never receives the whole repository. The scanner first builds a compact
evidence bundle from deterministic candidates, prioritizing repo-owned Fabric
declarations, services, capabilities, interfaces, libraries, deployments, and
small README/INTENT/SCOPE signals. The prompt asks for strict JSON:
evidence bundle from deterministic candidates, prioritizing durable local
evidence such as Fabric declarations, services, capabilities, interfaces,
libraries, deployments, and small README/INTENT/SCOPE signals. The prompt asks
for strict JSON:
```json
{"nodes": [], "edges": [], "attributes": []}
@@ -162,7 +165,9 @@ candidate stable keys. Existing accepted graph nodes win over discovery nodes
with the same graph id, so repo-owned declarations are preserved. Projected
nodes carry discovery stable key, origin, review state, confidence, provenance,
and source anchors in graph attributes; the graph explorer payload exposes
those fields for review.
those fields for review. For financial Fabric fields, accepted discovery still
needs an accountability-root, baseline inheritance rule, or explicit review
decision.
## Connector Follow-Up
@@ -198,8 +203,9 @@ with status, source, message, and candidate counts.
Connector-derived facts should be treated this way:
- accepted: only when the connector reads explicit repo-owned declarations or a
catalog already governed as authoritative for that field
- accepted: only when the connector reads explicit repo-owned evidence,
accountability-root evidence, or a catalog already governed as authoritative
for that field
- candidate: stable local registry facts such as onboarding manifest entries,
declared remote URLs, State Hub ids, and declaration paths
- review-only: missing catalogs, rate limits, connector failures, ambiguous
@@ -408,9 +414,11 @@ precedence:
6. `manual`
Manual review can override local candidate state, but it should not silently
rewrite repo-owned declarations. If accepted discoveries should become
authoritative, the safer next step is to generate a repo-owned declaration patch
for human review.
rewrite repo-owned declarations. If accepted discoveries should become durable
repo-local evidence, generate a repo-owned declaration patch for human review.
If they affect financial ownership, fabric containment, tenancy, or utility
value boundaries, generate a baseline or accountability-root review item
instead.
## Duplicate Handling

340
docs/semantic-attractors.md Normal file
View File

@@ -0,0 +1,340 @@
# Semantic Attractors
## Intent
Semantic attractors are view entities that help an operator orient inside a
medium or large graph. An attractor represents a topic, concern, capability
area, operating mode, or other conceptual pole such as `security`,
`development`, `operations`, `identity`, `data`, or `delivery`.
The graph explorer can place attractors on the canvas and connect graph
entities to them with view-only relationship strength. The stronger an
entity's semantic closeness to an attractor, the more that attractor should
pull the entity in force-directed or spring-based layouts.
The first motivating use case is repository orientation. Given a set of
repositories, the operator defines attractors such as `security`,
`development`, and `operations`. Railiance Fabric reads each repository's
`SCOPE.md`, estimates semantic closeness to those attractors, and maps that
score to layout force. The resulting map becomes a navigational surface: repos
with similar purpose drift toward the same conceptual pole without replacing
the underlying dependency or responsibility graph.
## What Attractors Are
An attractor is not a fabric node in the source graph. It is a graph-view
artifact with these responsibilities:
- name a topic or concern that is useful for orientation;
- define how closeness to that topic is measured;
- expose a score for each eligible entity;
- translate that score into layout hints and optional visual edges;
- keep the scoring evidence inspectable so the map does not become mysterious.
Attractors should be saved as view/profile configuration, operator presets, or
host-provided explorer configuration. They should not mutate repo-owned Fabric
declarations, and they should not imply that a repository provides or consumes
a capability.
## Why This Helps
Dependency edges answer "what depends on what?" Ownership and deployment
metadata answer "who owns this?" and "where does this run?" Those questions are
necessary, but they can still leave a large repo collection hard to scan.
Attractors answer a softer question: "what is this near, conceptually?"
This gives operators a fast way to discover clusters such as:
- repos that are security-heavy but not obvious from their names;
- operations tooling that depends on development systems;
- application repos that are unexpectedly close to platform/runtime concerns;
- thin adapter repos that sit between two conceptual poles;
- orphaned or ambiguous repos that have weak attraction to every known topic.
## Core Model
An attractor definition should be serializable and stable:
```yaml
id: security
label: Security
description: Identity, authorization, secrets, MFA, audit, policy, and trust boundaries.
applies_to:
layers: [repository]
evidence:
sources:
- type: scope_markdown
path: SCOPE.md
scoring:
method: lexical_semantic_profile
anchors:
- security
- identity
- authorization
- secrets
- audit
- policy
- mfa
negative_anchors:
- unrelated
normalization:
mode: per_entity_softmax
layout:
min_score: 0.15
max_score: 1.0
strength_scale: 0.8
ideal_length:
min: 80
max: 420
presentation:
color: "#be123c"
edge_style: dashed
```
The exact schema can evolve, but the responsibilities should remain separate:
- `applies_to` chooses which graph elements can be scored.
- `evidence` declares which text or metadata is used.
- `scoring` defines the semantic metric.
- `normalization` turns raw scores into comparable view weights.
- `layout` maps weights to graph layout hints.
- `presentation` controls the optional visual attractor node and edges.
## Scoring From SCOPE.md
`SCOPE.md` is a useful first evidence source because it is intentionally short,
repo-owned, and written to explain when a repository is relevant. For repository
attraction, the scorer should use sections such as:
- `One-liner`
- `Core Idea`
- `In Scope`
- `Relevant When`
- `Provided Capabilities`
- `Related / Overlapping Repositories`
- `Terminology`
Sections such as `Out of Scope` and `Not Relevant When` should be used
carefully. They can reduce false positives, but they should not erase a topic
just because the repo mentions a boundary. For example, a repo can say it is
not an authorization engine while still being semantically near security
because it models secrets, policy, or trust boundaries.
The first implementation can use a transparent lexical profile:
1. Parse `SCOPE.md` into sections.
2. Tokenize section text and provided capability keywords.
3. Weight section matches, with `One-liner`, `Core Idea`, `In Scope`, and
capability keywords carrying more weight than incidental notes.
4. Score each attractor by matching configured anchors and related terms.
5. Normalize scores per entity so one verbose `SCOPE.md` does not dominate.
6. Store the score, confidence, and top evidence snippets in the view payload.
Later implementations can replace or augment lexical scoring with embeddings,
LLM-assisted classification, or operator-reviewed labels. The contract should
not depend on a particular scorer.
## Score Semantics
Attractor scores should be continuous values in `[0, 1]`.
Suggested interpretation:
| Score | Meaning |
|-------|---------|
| `0.00` | no useful evidence of semantic closeness |
| `0.10` to `0.30` | weak signal; useful only as a faint layout hint |
| `0.30` to `0.60` | moderate closeness; entity should visibly lean toward the attractor |
| `0.60` to `0.85` | strong closeness; entity likely belongs near the attractor cluster |
| `0.85` to `1.00` | primary semantic identity or explicit operator label |
Every score should carry a confidence separate from closeness. A repo with a
thin or missing `SCOPE.md` may have low confidence even if a few terms match.
Attractors should also support multi-attraction. A repository can be close to
both `development` and `operations`; the layout should then place it between
those poles instead of forcing a single category. This is the main difference
from zones: zones preserve a single-surface invariant, while attractors are
allowed to overlap because they are layout forces, not containers.
## Layout Mapping
Attraction scores become layout hints. They should not become domain edges.
A graph explorer can map scores to synthetic view edges:
```json
{
"data": {
"id": "attractor:security->repo:flex-auth",
"source": "attractor:security",
"target": "repo:flex-auth",
"edgeType": "semantic_attraction",
"displayOnly": true,
"score": 0.82,
"confidence": 0.74,
"strength": "strong",
"layoutAffinity": 0.82,
"layoutIdealLength": 110,
"layoutElasticity": 0.9,
"sourceReferences": [
{
"type": "scope_markdown",
"path": "SCOPE.md",
"section": "In Scope"
}
]
},
"classes": "semantic-attraction"
}
```
For force-directed layouts:
- stronger scores should increase spring strength or edge weight;
- stronger scores should shorten ideal length;
- weak scores may be hidden visually while still applying a small force;
- edges below a configured threshold should not affect layout;
- display-only attraction edges should be excluded from dependency, boundary,
blast-radius, and zone-connectivity diagnostics.
Attractor nodes can be pinned, arranged on a ring, placed by the operator, or
computed from the current profile. For first use, a stable radial placement is
usually enough: place three to eight attractors around the graph, then let
repositories find their balance.
## View Payload Shape
The graph explorer payload should be able to carry attractor metadata without
changing the canonical Fabric graph.
Recommended top-level view extension:
```json
{
"view": {
"attractors": {
"enabled": true,
"definitionSet": "repo-concerns-v1",
"definitions": [
{
"id": "security",
"label": "Security",
"description": "Identity, authorization, secrets, audit, and policy.",
"color": "#be123c"
}
],
"scores": [
{
"attractor_id": "security",
"element_id": "repo:flex-auth",
"score": 0.82,
"confidence": 0.74,
"method": "lexical_semantic_profile",
"evidence": [
{
"source": "SCOPE.md",
"section": "Core Idea",
"terms": ["authorization", "policy"]
}
]
}
]
}
}
}
```
The renderer may choose to materialize these into synthetic nodes and edges at
runtime. A host may also emit synthetic display-only elements directly if that
is easier for the current engine.
## Operator Workflow
A useful attractor workflow should feel like mapmaking:
1. Choose a preset such as `Security / Development / Operations`.
2. Review the generated scores and evidence for a few known repos.
3. Hide or pin attractors that are not useful for the current question.
4. Save the attractor definition set in the graph profile.
5. Use the resulting layout to discover ambiguous, central, or misplaced repos.
The UI should expose:
- a toggle for semantic attractors;
- a definition-set selector;
- score threshold controls;
- optional visual attraction edges;
- pinned/unpinned attractor placement;
- detail panels explaining why a repo is close to an attractor;
- diagnostics for missing evidence, low confidence, and overly broad
attractors.
## Relationship To Zones
Zones and attractors solve different orientation problems.
Zones are bounded drawing surfaces. A visible node belongs to zero or one zone
in a given view. They are useful for deployment environments, access zones,
ownership surfaces, and other container-like questions.
Attractors are semantic force points. A visible node can be pulled by multiple
attractors at once. They are useful for topical orientation, concern mapping,
and discovering conceptual neighborhoods.
The two concepts can combine cleanly:
- zones can show where entities run;
- attractors can pull repos inside or outside those zones based on semantic
concern;
- zone diagnostics should ignore semantic attraction edges unless explicitly
configured otherwise;
- attractor scores can be summarized inside zone details.
## Initial Presets
A first repository-orientation preset should keep the set small:
| Attractor | Topic Signal |
|-----------|--------------|
| `security` | identity, secrets, authorization, policy, audit, MFA, trust boundaries |
| `development` | source code, build, CI/CD, package publishing, scaffolding, developer workflows |
| `operations` | deployment, runtime, monitoring, backups, incidents, infrastructure lifecycle |
Useful follow-up presets:
- `data`, `identity`, `delivery`, `governance`
- `platform`, `application`, `tooling`
- `financial`, `runtime`, `coordination`
Attractors should start as operator-chosen presets rather than global truth.
The same repository can be viewed through different conceptual lenses.
## Implementation Path
The concept can be implemented incrementally:
1. Add an attractor definition format for graph explorer profiles.
2. Parse repo `SCOPE.md` files during registry sync or graph export.
3. Compute transparent lexical scores for repositories.
4. Include attractor scores and evidence in the graph explorer payload.
5. Add synthetic attractor nodes and display-only attraction edges in the UI.
6. Map attraction scores to layout hints for the force-directed layout.
7. Add detail-panel evidence and low-confidence diagnostics.
8. Support saved attractor presets and operator score overrides.
This keeps attractors as a view concern until the scoring model proves useful.
If a semantic relation becomes durable domain knowledge, it can later be
promoted into a proper Fabric declaration with separate evidence and review.
## Open Questions
- Should attractor definitions live in graph profiles, repo config, or a shared
registry preset file?
- Should scoring run during registry sync, export, or entirely in the browser?
- How much operator override should be allowed before scores become maintained
labels rather than computed evidence?
- What is the right default for missing or stale `SCOPE.md` evidence?
- Should the first implementation use only lexical scoring, or should it also
prepare a pluggable embedding scorer interface?

View File

@@ -1,26 +1,27 @@
# 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.
Railiance Fabric is the discovery, validation, and versioning layer for the
Railiance infrastructure-responsibility graph. 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 Fabric topology,
ownership, fabric membership, or cross-boundary utility relations.
## 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 |
| Deployment/accountability roots | Durable evidence of infrastructure, deployment, ownership, utility, and payment responsibility | State Hub task state |
| Participating repos | Self-description evidence such as code, manifests, API contracts, package metadata, and legacy `fabric/` declarations | All external deployment/fabric relations |
| Railiance Fabric | Schemas, discovery, validation, graph construction, accepted snapshots, exports | State Hub tasks/progress/decisions |
| State Hub | Read-model storage, links to repos/workstreams/tasks/progress, dashboard/search views | Editing Fabric topology or inventing ownership |
The flow is:
```text
repo-local fabric/*.yaml
accountability roots + durable deployment evidence + repo evidence
|
v
railiance-fabric validate/export
railiance-fabric scan/validate/export
|
v
State Hub graph read model
@@ -31,15 +32,25 @@ dashboard, search, planning, progress links
## Export Shape
The CLI emits `FabricGraphExport` JSON:
The CLI and registry emit `FabricGraphExport` JSON:
```bash
railiance-fabric export --format json
railiance-fabric export --format financial
```
Schema: `schemas/state-hub-export.schema.yaml`
Top-level shape:
The schema now accepts two export families:
- `railiance.fabric/v1alpha1`: legacy declaration-centered graph export.
- `railiance.fabric/v1alpha2` with `schema_version:
financial-fabric-v1`: financial Fabric graph export.
### Legacy v1alpha1 Shape
The legacy top-level shape remains supported for compatibility with
`STATE-WP-0050`:
```yaml
apiVersion: railiance.fabric/v1alpha1
@@ -58,6 +69,10 @@ Node fields:
| `repo` | Owning repo slug. |
| `domain` | Owning domain slug. |
| `lifecycle` | Declaration lifecycle. |
| `canon_category` | Canon-aligned entity category when known. |
| `canon_anchor` | Canon surface that owns the selected category. |
| `mapping_fit` | Mapping confidence bucket: `direct`, `partial`, `conflict`, `gap`, or `unknown`. |
| `evidence_state` | Evidence state for the node claim: `observed`, `declared`, `inferred`, `proposed`, or `gap`. |
Edge fields:
@@ -65,7 +80,87 @@ Edge fields:
|-------|---------|
| `from` | Source node id. |
| `to` | Target node id. |
| `type` | Relationship type, such as `provides`, `exposes`, `available_via`, `consumes`, `binds:exact`, or `uses_interface`. |
| `type` | Fabric relationship type, such as `provides`, `exposes`, `available_via`, `consumes`, `binds:exact`, or `uses_interface`. |
| `canonical_type` | Canon-aligned relationship family when known, such as `exposes`, `depends_on`, `deploys`, or `flows_to`. |
| `display_only` | `true` when the edge is a visualization/layout relationship rather than a canonical graph claim. |
| `evidence_state` | Evidence state for the claim: `observed`, `declared`, `inferred`, `proposed`, or `gap`. |
### Financial v1alpha2 Shape
The financial Fabric export adds first-class responsibility and value
semantics:
```yaml
apiVersion: railiance.fabric/v1alpha2
kind: FabricGraphExport
schema_version: financial-fabric-v1
netkingdom:
id: railiance.netkingdom
king_actor_id: actor.railiance.king
actors: []
fabrics: []
nodes: []
edges: []
unresolved: []
```
Additional top-level sections:
| Field | Meaning |
|-------|---------|
| `schema_version` | Financial Fabric export schema identity. |
| `netkingdom` | Root responsibility context and king actor. |
| `actors` | King, lord, tenant, operator, and steward actors. |
| `fabrics` | Fabric and subfabric boundaries. |
| `unresolved` | Ownership, containment, or import gaps to display for review. |
Additional node fields:
| Field | Meaning |
|-------|---------|
| `containment` | Netkingdom, fabric, optional subfabric, environment, and optional deployment scenario. |
| `ownership` | Owner actor, owner role, and ownership resolution state. |
| `accounting` | Optional cost/profit center and allocation metadata. |
| `evidence` | Evidence state, review state, confidence, and evidence references. |
Additional edge fields:
| Field | Meaning |
|-------|---------|
| `relationship_category` | `containment`, `ownership`, `technical`, `utility`, `accounting`, or `evidence`. |
| `provider` / `consumer` | Owner and boundary context for utility edges. |
| `boundary` | Whether the edge crosses fabric or subfabric boundaries. |
| `utility` | Utility type, contract, payment schema, metering basis, and business model. |
| `accounting` | Optional cost/profit attribution for the relationship. |
| `evidence` | Evidence state, review state, confidence, and evidence references. |
Example payload: `examples/exports/financial-fabric-v1.json`.
## Operator Refresh During Reset
For the current reset, operators should produce both export families:
```bash
railiance-fabric validate .
railiance-fabric export --format json
railiance-fabric export --format financial
```
The `json` export remains the compatibility payload for State Hub graph views
implemented by `STATE-WP-0050`. The `financial` export is the vNext contract
artifact for `STATE-WP-0051` and should be reviewed whenever the baseline,
discovery inputs, ownership model, or registry materialization changes.
After workplan file changes, refresh State Hub's file-backed index from the
State Hub repo:
```bash
make fix-consistency REPO=railiance-fabric
```
Graph import and workplan consistency are separate. The consistency command
does not author Fabric graph topology; it only keeps State Hub's workplan cache
aligned with repo files.
## Proposed State Hub Read Model
@@ -119,9 +214,20 @@ fabric_graph_edges
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.
The `STATE-WP-0050` implementation already stores a v1alpha1 read model. The
`STATE-WP-0051` follow-up should extend that storage with:
- import/export schema version;
- netkingdom id;
- fabric and subfabric ids;
- actor ids and roles;
- node containment;
- node owner actor, owner role, and ownership resolution;
- edge relationship category;
- utility provider/consumer ownership context;
- fabric/subfabric boundary crossing flags;
- node and edge accounting attribution;
- unresolved ownership or containment gaps.
## Linking To Existing State Hub Entities
@@ -133,7 +239,7 @@ State Hub should enrich graph nodes by matching:
- 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.
Fabric export or source evidence.
## Ingestion Rules
@@ -143,7 +249,22 @@ repo-owned declaration files.
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.
but do not auto-edit Fabric topology, ownership, or source evidence.
6. For `v1alpha2`, preserve unresolved ownership/containment gaps rather than
inventing State Hub-owned values.
7. For `v1alpha2`, expose cost/profit center fields as accounting views, not as
fabric membership changes.
## Current State Hub Limitations
As of `STATE-WP-0050`, State Hub imports the legacy `v1alpha1` export as a
read model. It does not yet materialize the `v1alpha2` financial Fabric fields.
Until `STATE-WP-0051` is implemented, a `v1alpha2` export is a contract and
registry capability in `railiance-fabric`, not a fully queryable State Hub read
model. Operators should keep importing the current `v1alpha1` export for
existing dashboard/query behavior and use `v1alpha2` payloads for contract
verification and follow-on implementation.
## Initial Dashboard Queries

View File

@@ -36,6 +36,7 @@ Machine-readable catalog files:
|------|-----------|----------|--------------|
| `http-api` | active | api | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
| `web-ui` | active | ui | `none`, `oidc`, `jwt`, `mtls`, `api_key` |
| `mcp-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` |

View File

@@ -1,7 +1,7 @@
# Declaration Fixtures
These fixtures support the T02 schema baseline and give the future validator
real inputs to exercise.
These fixtures support the legacy `v1alpha1` declaration schema and give the
validator real inputs to exercise.
`valid/` contains a coherent mini-graph:
@@ -11,5 +11,10 @@ real inputs to exercise.
- 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.
`invalid/` contains schema-level failures. The validator should report clear
errors for these before it attempts graph-level checks.
For the financial Fabric contract, use
`examples/exports/financial-fabric-v1.json`. That fixture exercises
king/lord/tenant actors, fabric/subfabric containment, ownership, cost/profit
attribution, and a cross-boundary utility edge.

View File

@@ -0,0 +1,104 @@
apiVersion: railiance.fabric/v1alpha2
kind: AccountabilityRootManifest
metadata:
id: example.accountability-roots
name: Example Accountability Roots
description: Minimal example showing how a tenant subfabric can be added without changing the parent fabric criterion.
netkingdom:
id: example.netkingdom
name: Example Netkingdom
king_actor_id: actor.example.king
actors:
- id: actor.example.king
role: king
name: Example King
authority:
recovery_authority: true
secrets_authority: true
backup_authority: true
termination_authority: true
- id: actor.example.lord
role: lord
name: Example Lord
- id: actor.example.tenant
role: tenant
name: Example Tenant
fabrics:
- id: fabric.example.primary
kind: Fabric
name: Example Primary Fabric
netkingdom_id: example.netkingdom
lord_actor_id: actor.example.lord
parent_fabric_id: null
status: active
boundary:
boundary_type: fabric
criterion: financial_and_operational_accountability
payment_responsibility: actor.example.lord
operational_responsibility: actor.example.king
recovery_responsibility: actor.example.king
- id: subfabric.example.tenant
kind: Subfabric
name: Example Tenant Subfabric
netkingdom_id: example.netkingdom
tenant_actor_id: actor.example.tenant
parent_fabric_id: fabric.example.primary
status: planned
boundary:
boundary_type: subfabric
criterion: restricted_paid_tenant_utility
payment_responsibility: actor.example.tenant
operational_responsibility: actor.example.lord
recovery_responsibility: actor.example.king
discovery_roots:
- id: root.example.state-hub
type: state_hub_repo_inventory
status: active
fabric_id: fabric.example.primary
owner_actor_id: actor.example.king
source:
base_url: http://127.0.0.1:8000
api_paths:
- /managed-repos/
safe_discovery: metadata_only
evidence_scope:
- repo_inventory
- repository_identity
refresh:
cadence: on_change
triggers:
- state_hub_repo_inventory_change
- operator_request
- id: root.example.tenant-api-contracts
type: endpoint_contract
status: planned
fabric_id: fabric.example.primary
subfabric_id: subfabric.example.tenant
owner_actor_id: actor.example.tenant
source:
repo_slug: example-tenant
path: contracts/openapi
safe_discovery: local_files
evidence_scope:
- endpoint_contract
- tenant_boundary
refresh:
cadence: on_change
triggers:
- endpoint_contract_change
- operator_request
refresh:
cadence: manual
triggers:
- operator_request
- state_hub_repo_inventory_change
- endpoint_contract_change
- lord_or_tenant_change
templates:
future_subfabric:
parent_fabric_id: fabric.example.primary
tenant_actor_role: tenant
required_updates:
- Add tenant actor.
- Add Subfabric with tenant_actor_id.
- Add subfabric-scoped discovery roots.

View File

@@ -0,0 +1,192 @@
{
"apiVersion": "railiance.fabric/v1alpha2",
"kind": "FabricGraphExport",
"schema_version": "financial-fabric-v1",
"generated_at": "2026-05-24T00:00:00Z",
"source": {
"producer": "railiance-fabric",
"registry": "registry",
"commit": "example",
"generation_reason": "operator_refresh"
},
"compatibility": {
"legacy_v1alpha1_supported": true,
"breaking_reset": false
},
"netkingdom": {
"id": "railiance.netkingdom",
"name": "Railiance Netkingdom",
"king_actor_id": "actor.railiance.king"
},
"actors": [
{
"id": "actor.railiance.king",
"kind": "FabricActor",
"role": "king",
"name": "Railiance King"
},
{
"id": "actor.railiance.primary-lord",
"kind": "FabricActor",
"role": "lord",
"name": "Railiance Primary Lord"
},
{
"id": "actor.coulomb.tenant",
"kind": "FabricActor",
"role": "tenant",
"name": "Coulomb Tenant"
}
],
"fabrics": [
{
"id": "fabric.railiance.primary",
"kind": "Fabric",
"name": "Railiance Primary Fabric",
"netkingdom_id": "railiance.netkingdom",
"lord_actor_id": "actor.railiance.primary-lord",
"parent_fabric_id": null,
"status": "active",
"boundary": {
"boundary_type": "fabric",
"criterion": "financial_and_operational_accountability"
},
"evidence_refs": []
},
{
"id": "subfabric.railiance.tenant.coulomb",
"kind": "Subfabric",
"name": "Coulomb Tenant Subfabric",
"netkingdom_id": "railiance.netkingdom",
"parent_fabric_id": "fabric.railiance.primary",
"tenant_actor_id": "actor.coulomb.tenant",
"status": "planned",
"boundary": {
"boundary_type": "subfabric",
"criterion": "restricted_paid_tenant_utility"
},
"evidence_refs": []
}
],
"nodes": [
{
"id": "state-hub.http",
"kind": "UtilityInterface",
"name": "State Hub HTTP API",
"repo": "state-hub",
"domain": "custodian",
"lifecycle": "active",
"containment": {
"netkingdom_id": "railiance.netkingdom",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": null,
"environment": "local",
"deployment_scenario_id": null
},
"ownership": {
"owner_actor_id": "actor.railiance.primary-lord",
"owner_role": "lord",
"resolution": "inherited",
"inherited_from": "fabric.railiance.primary",
"supporting_actor_ids": []
},
"accounting": {
"cost_center_id": "cc.platform.shared",
"allocation_model": "direct"
},
"evidence": {
"state": "declared",
"review_state": "accepted",
"confidence": 0.9,
"refs": []
},
"canon_category": "endpoint",
"canon_anchor": "model/network",
"mapping_fit": "partial",
"evidence_state": "declared",
"attributes": {
"description": "Example tenant-facing State Hub API utility."
}
},
{
"id": "coulomb.automation-client",
"kind": "Service",
"name": "Coulomb Automation Client",
"repo": "coulomb-automation",
"domain": "railiance",
"lifecycle": "planned",
"containment": {
"netkingdom_id": "railiance.netkingdom",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": "subfabric.railiance.tenant.coulomb",
"environment": "local",
"deployment_scenario_id": null
},
"ownership": {
"owner_actor_id": "actor.coulomb.tenant",
"owner_role": "tenant",
"resolution": "explicit",
"supporting_actor_ids": []
},
"accounting": {
"cost_center_id": "cc.coulomb.automation",
"allocation_model": "direct"
},
"evidence": {
"state": "declared",
"review_state": "accepted",
"confidence": 0.8,
"refs": []
},
"attributes": {}
}
],
"edges": [
{
"id": "utility:state-hub-http:coulomb-client",
"from": "state-hub.http",
"to": "coulomb.automation-client",
"type": "provides_utility_to",
"relationship_category": "utility",
"canonical_type": "depends_on",
"canon_anchor": "model/landscape",
"mapping_fit": "partial",
"display_only": false,
"evidence_state": "declared",
"provider": {
"owner_actor_id": "actor.railiance.primary-lord",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": null
},
"consumer": {
"owner_actor_id": "actor.coulomb.tenant",
"fabric_id": "fabric.railiance.primary",
"subfabric_id": "subfabric.railiance.tenant.coulomb"
},
"boundary": {
"crosses_fabric_boundary": false,
"crosses_subfabric_boundary": true
},
"utility": {
"utility_type": "coordination_api",
"contract_id": "state-hub.http",
"payment_schema_id": "payment.internal-tenant-access",
"metering_basis": "unknown",
"business_model": "tenant_utility"
},
"accounting": {
"provider_profit_center_id": "pc.tenant-utilities",
"consumer_cost_center_id": "cc.coulomb.automation",
"allocation_model": "usage_weighted"
},
"evidence": {
"state": "declared",
"review_state": "accepted",
"confidence": 0.8,
"refs": []
},
"attributes": {}
}
],
"unresolved": []
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,8 @@
# Seed Declarations
These declarations are the first repo-local seed graph for Railiance Fabric.
They are legacy `v1alpha1` evidence used for validation, registry snapshots,
and compatibility exports.
They model current and planned Railiance ecosystem providers:
@@ -17,4 +19,9 @@ They model current and planned Railiance ecosystem providers:
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.
already published Fabric evidence.
The current financial responsibility baseline lives in
`fabric/financial/railiance-netkingdom.yaml`. It defines the Railiance
netkingdom, king, current lord, one active fabric, inherited node ownership,
and the template for future tenant subfabrics.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-apps.s5-releases.artifact-evidence-to-forge
name: S5 artifact evidence binding
owner: railiance-apps
repo: railiance-apps
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: railiance-apps.s5-releases.needs-artifact-evidence
provider_capability_id: railiance-forge.source-forge.artifact-promotion-evidence
provider_interface_id: railiance-forge.source-forge.evidence-contract
status: compatible
rationale: S5 release readiness should cite forge-owned artifact publish, restore, and operating evidence.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-apps.s5-releases.container-registry-to-forge
name: S5 container registry binding
owner: railiance-apps
repo: railiance-apps
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: railiance-apps.s5-releases.needs-container-registry
provider_capability_id: railiance-forge.source-forge.container-registry
provider_interface_id: railiance-forge.source-forge.oci-registry
status: compatible
rationale: S5 releases consume already-published app images from the forge-owned OCI registry.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-enablement.delivery-templates.runner-substrate-to-forge
name: Enablement runner substrate binding
owner: railiance-enablement
repo: railiance-enablement
domain: railiance
spec:
lifecycle: planned
environments: [dev, staging, prod]
dependency_id: railiance-enablement.delivery-templates.needs-runner-substrate
provider_capability_id: railiance-forge.source-forge.workflow-runner-substrate
provider_interface_id: railiance-forge.source-forge.runner-label-contract
status: compatible
rationale: S4 reusable templates should consume forge-owned runner labels, trust posture, and runner evidence.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-forge.source-forge.kubernetes-runtime-to-cluster
name: Forge Kubernetes runtime binding
owner: railiance-forge
repo: railiance-forge
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: railiance-forge.source-forge.needs-kubernetes-runtime
provider_capability_id: railiance-cluster.kubernetes.runtime
provider_interface_id: railiance-cluster.kubernetes.api
status: compatible
rationale: The forge runtime is deployed on the Railiance Kubernetes runtime provided by railiance-cluster.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-forge.source-forge.object-storage-to-artifact-store
name: Forge object storage binding
owner: railiance-forge
repo: railiance-forge
domain: railiance
spec:
lifecycle: planned
environments: [dev, staging, prod]
dependency_id: railiance-forge.source-forge.needs-object-storage
provider_capability_id: artifact-store.object-storage
provider_interface_id: artifact-store.object-storage.bucket
status: compatible
rationale: Durable forge artifact/blob preservation should use the planned Railiance object-storage provider rather than ad hoc forge-local storage.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-forge.source-forge.postgresql-to-cnpg
name: Forge PostgreSQL binding
owner: railiance-forge
repo: railiance-forge
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: railiance-forge.source-forge.needs-postgresql
provider_capability_id: railiance-platform.cnpg.postgresql
provider_interface_id: railiance-platform.cnpg.database-connection
status: compatible
rationale: Current Gitea database state is backed by the Railiance platform CNPG PostgreSQL service.

View File

@@ -0,0 +1,16 @@
apiVersion: railiance.fabric/v1alpha1
kind: BindingAssertion
metadata:
id: railiance-forge.source-forge.runtime-secrets-to-openbao
name: Forge runtime secrets binding
owner: railiance-forge
repo: railiance-forge
domain: railiance
spec:
lifecycle: active
environments: [dev, staging, prod]
dependency_id: railiance-forge.source-forge.needs-runtime-secrets
provider_capability_id: railiance-platform.openbao.runtime-secrets
provider_interface_id: railiance-platform.openbao.kv-v2
status: compatible
rationale: Runtime secret custody for forge workloads belongs to the platform OpenBao path; SOPS/age remains bootstrap only.

View File

@@ -0,0 +1,21 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-cluster.kubernetes.runtime
name: Kubernetes runtime
owner: railiance-cluster
repo: railiance-cluster
domain: railiance
source_links:
- label: Cluster scope
path: /home/worsch/railiance-cluster/SCOPE.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Provides Kubernetes runtime primitives and API access consumed by Railiance platform, forge, and app workloads.
capability_type: kubernetes-runtime
service_id: railiance-cluster.kubernetes
interface_ids:
- railiance-cluster.kubernetes.api
criticality: critical
data_classification: restricted

View File

@@ -0,0 +1,23 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-enablement.delivery-templates.ci-cd-templates
name: CI/CD workflow templates
owner: railiance-enablement
repo: railiance-enablement
domain: railiance
source_links:
- label: Enablement scope
path: /home/worsch/railiance-enablement/SCOPE.md
- label: Enablement intent
path: /home/worsch/railiance-enablement/INTENT.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Reusable Railiance workflow templates, promotion conventions, and delivery gates that consume forge runner labels and artifact evidence.
capability_type: ci-cd-template-catalog
service_id: railiance-enablement.delivery-templates
interface_ids:
- railiance-enablement.delivery-templates.workflow-template-contract
criticality: medium
data_classification: internal

View File

@@ -0,0 +1,23 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-forge.source-forge.artifact-promotion-evidence
name: Artifact promotion evidence
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Observability and evidence contract
path: /home/worsch/railiance-forge/docs/observability-operating-evidence.md
- label: Backup and restore handoff
path: /home/worsch/railiance-forge/docs/backup-restore-secret-handoff.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Provides artifact identity, provenance, publish, restore, and release-readiness evidence that downstream releases can cite.
capability_type: artifact-promotion-evidence
service_id: railiance-forge.source-forge
interface_ids:
- railiance-forge.source-forge.evidence-contract
criticality: high
data_classification: internal

View File

@@ -0,0 +1,21 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-forge.source-forge.container-registry
name: Container registry
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Container registry docs
path: /home/worsch/railiance-forge/docs/gitea-container-registry.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Provides the Gitea OCI container registry endpoint used by Railiance workloads.
capability_type: container-registry
service_id: railiance-forge.source-forge
interface_ids:
- railiance-forge.source-forge.oci-registry
criticality: high
data_classification: confidential

View File

@@ -0,0 +1,21 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-forge.source-forge.python-package-registry
name: Python package registry
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Package registry docs
path: /home/worsch/railiance-forge/docs/gitea-package-registry.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Provides the Gitea Python package registry endpoint used by Railiance source and app builds.
capability_type: python-package-registry
service_id: railiance-forge.source-forge
interface_ids:
- railiance-forge.source-forge.python-package-index
criticality: high
data_classification: confidential

View File

@@ -0,0 +1,22 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-forge.source-forge.source-hosting
name: Source hosting
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Forge scope
path: /home/worsch/railiance-forge/SCOPE.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Hosts Railiance Git repositories, review surfaces, repository metadata, and source-forge access paths.
capability_type: source-hosting
service_id: railiance-forge.source-forge
interface_ids:
- railiance-forge.source-forge.web-ui
- railiance-forge.source-forge.git-ssh
criticality: high
data_classification: confidential

View File

@@ -0,0 +1,21 @@
apiVersion: railiance.fabric/v1alpha1
kind: CapabilityDeclaration
metadata:
id: railiance-forge.source-forge.workflow-runner-substrate
name: Workflow runner substrate
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Runner ownership contract
path: /home/worsch/railiance-forge/docs/ci-runner-actions-gitops-ownership.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Provides forge-backed runner labels, placement, credential boundaries, and runner health evidence consumed by workflow templates and release checks.
capability_type: workflow-runner-substrate
service_id: railiance-forge.source-forge
interface_ids:
- railiance-forge.source-forge.runner-label-contract
criticality: high
data_classification: restricted

View File

@@ -0,0 +1,30 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-apps.s5-releases.needs-artifact-evidence
name: S5 artifact evidence dependency
owner: railiance-apps
repo: railiance-apps
domain: railiance
source_links:
- label: Apps scope
path: /home/worsch/railiance-apps/SCOPE.md
- label: Observability and evidence contract
path: /home/worsch/railiance-forge/docs/observability-operating-evidence.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: railiance-apps.s5-releases
requires:
capability_type: artifact-promotion-evidence
capability_id: railiance-forge.source-forge.artifact-promotion-evidence
interface:
type: evidence-contract
version_constraint: ">=v1"
auth:
method: none
criticality: high
data_classification: internal
fallback:
mode: manual
description: App operators can record manual evidence, but S5 should cite forge-owned artifact readiness when promoting releases.

View File

@@ -0,0 +1,30 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-apps.s5-releases.needs-container-registry
name: S5 container registry dependency
owner: railiance-apps
repo: railiance-apps
domain: railiance
source_links:
- label: Apps scope
path: /home/worsch/railiance-apps/SCOPE.md
- label: Container registry docs
path: /home/worsch/railiance-forge/docs/gitea-container-registry.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: railiance-apps.s5-releases
requires:
capability_type: container-registry
capability_id: railiance-forge.source-forge.container-registry
interface:
type: oci-registry
version_constraint: ">=registry-v2"
auth:
method: api_key
criticality: high
data_classification: confidential
fallback:
mode: none
description: S5 releases require a reachable container registry for private or internal app images.

View File

@@ -0,0 +1,30 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-enablement.delivery-templates.needs-runner-substrate
name: Enablement runner substrate dependency
owner: railiance-enablement
repo: railiance-enablement
domain: railiance
source_links:
- label: Enablement scope
path: /home/worsch/railiance-enablement/SCOPE.md
- label: Runner ownership contract
path: /home/worsch/railiance-forge/docs/ci-runner-actions-gitops-ownership.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
consumer_service_id: railiance-enablement.delivery-templates
requires:
capability_type: workflow-runner-substrate
capability_id: railiance-forge.source-forge.workflow-runner-substrate
interface:
type: workflow-runner-label-contract
version_constraint: ">=v1"
auth:
method: none
criticality: high
data_classification: internal
fallback:
mode: manual
description: Reusable templates can remain draft-only until forge publishes runner labels and trust evidence.

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-forge.source-forge.needs-kubernetes-runtime
name: Forge Kubernetes runtime dependency
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Forge scope
path: /home/worsch/railiance-forge/SCOPE.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: railiance-forge.source-forge
requires:
capability_type: kubernetes-runtime
capability_id: railiance-cluster.kubernetes.runtime
interface:
type: kubernetes-api
version_constraint: ">=v1"
auth:
method: kubernetes_service_account
criticality: critical
data_classification: restricted
fallback:
mode: none
description: The forge runtime cannot operate without the Railiance Kubernetes runtime.

View File

@@ -0,0 +1,30 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-forge.source-forge.needs-object-storage
name: Forge object storage dependency
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Backup and restore handoff
path: /home/worsch/railiance-forge/docs/backup-restore-secret-handoff.md
- label: Platform OpenBao object-storage handoff
path: /home/worsch/railiance-platform/docs/openbao.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
consumer_service_id: railiance-forge.source-forge
requires:
capability_type: object-storage
capability_id: artifact-store.object-storage
interface:
type: object-storage-bucket
version_constraint: ">=v1"
auth:
method: sts_token
criticality: high
data_classification: confidential
fallback:
mode: manual
description: Current Gitea package blobs remain on PVC until durable object-storage backup or artifact preservation is proven.

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-forge.source-forge.needs-postgresql
name: Forge PostgreSQL dependency
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Backup and restore handoff
path: /home/worsch/railiance-forge/docs/backup-restore-secret-handoff.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: railiance-forge.source-forge
requires:
capability_type: postgresql-database-service
capability_id: railiance-platform.cnpg.postgresql
interface:
type: database-connection
version_constraint: ">=v16"
auth:
method: database_role
criticality: critical
data_classification: confidential
fallback:
mode: none
description: The forge runtime requires the Gitea database state and cannot degrade safely without it.

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: DependencyDeclaration
metadata:
id: railiance-forge.source-forge.needs-runtime-secrets
name: Forge runtime secrets dependency
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Backup and restore handoff
path: /home/worsch/railiance-forge/docs/backup-restore-secret-handoff.md
spec:
lifecycle: active
environments: [dev, staging, prod]
consumer_service_id: railiance-forge.source-forge
requires:
capability_type: runtime-secrets
capability_id: railiance-platform.openbao.runtime-secrets
interface:
type: openbao-kv-v2-mount
version_constraint: ">=v1 <v2"
auth:
method: kubernetes_service_account
criticality: critical
data_classification: secret
fallback:
mode: manual
description: SOPS/age bootstrap can carry encrypted deploy input, but runtime secret custody belongs to the platform path.

View File

@@ -0,0 +1,227 @@
apiVersion: railiance.fabric/v1alpha2
kind: AccountabilityRootManifest
metadata:
id: railiance.accountability-roots
name: Railiance Accountability Roots
description: Current discovery roots for rebuilding the Railiance Fabric graph from durable accountability evidence.
source_links:
- label: Financial Fabric architecture
path: docs/FabricDiscoveryAndUpdate.md
- label: Financial baseline
path: fabric/financial/railiance-netkingdom.yaml
netkingdom:
id: railiance.netkingdom
name: Railiance Netkingdom
king_actor_id: actor.railiance.king
baseline_ref:
label: Railiance financial baseline
path: fabric/financial/railiance-netkingdom.yaml
actors:
- id: actor.railiance.king
role: king
name: Railiance King
description: Responsible for the Railiance netkingdom and recovery authority.
authority:
recovery_authority: true
secrets_authority: true
backup_authority: true
termination_authority: true
- id: actor.railiance.primary-lord
role: lord
name: Railiance Primary Lord
description: Pays for the current Railiance infrastructure boundary.
fabrics:
- id: fabric.railiance.primary
kind: Fabric
name: Railiance Primary Fabric
netkingdom_id: railiance.netkingdom
lord_actor_id: actor.railiance.primary-lord
parent_fabric_id: null
status: active
boundary:
boundary_type: fabric
criterion: financial_and_operational_accountability
payment_responsibility: actor.railiance.primary-lord
operational_responsibility: actor.railiance.king
recovery_responsibility: actor.railiance.king
evidence_refs:
- label: Railiance financial baseline
path: fabric/financial/railiance-netkingdom.yaml
discovery_roots:
- id: root.state-hub.attached-repos
type: state_hub_repo_inventory
status: active
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.primary-lord
source:
base_url: http://127.0.0.1:8000
api_paths:
- /managed-repos/
safe_discovery: metadata_only
evidence_scope:
- repo_inventory
- repository_identity
refresh:
cadence: on_change
triggers:
- state_hub_repo_inventory_change
- operator_request
notes: Read State Hub as repo inventory evidence only; State Hub does not author Fabric ownership or topology.
- id: root.gitea.coulomb
type: gitea_organization
status: active
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.primary-lord
source:
url: ssh://git@92.205.130.254:30022/coulomb
organization: coulomb
safe_discovery: metadata_only
evidence_scope:
- repo_inventory
- repository_identity
refresh:
cadence: on_change
triggers:
- git_commit
- operator_request
- id: root.registry.local-repos
type: registry_manifest
status: active
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.primary-lord
source:
manifest_path: registry/local-repos.yaml
safe_discovery: local_files
evidence_scope:
- repo_inventory
- repository_identity
- local_checkout
refresh:
cadence: on_change
triggers:
- state_hub_repo_inventory_change
- operator_request
- id: root.workspace.home-worsch
type: host_path
status: active
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.primary-lord
source:
path: /home/worsch
patterns:
- "*/.git"
- "*/fabric"
safe_discovery: local_files
evidence_scope:
- local_checkout
- repository_identity
refresh:
cadence: manual
triggers:
- operator_request
- id: root.railiance-fabric.checkout
type: repository_checkout
status: active
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.primary-lord
source:
repo_slug: railiance-fabric
path: /home/worsch/railiance-fabric
remote_url: gitea-remote:coulomb/railiance-fabric.git
safe_discovery: local_files
evidence_scope:
- repository_identity
- local_checkout
- service_configuration
- endpoint_contract
- deployment_topology
refresh:
cadence: on_change
triggers:
- git_commit
- deployment_manifest_change
- endpoint_contract_change
- operator_request
- id: root.deployment.local-manifests
type: deployment_automation
status: active
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.primary-lord
source:
path: /home/worsch
patterns:
- "**/compose.yaml"
- "**/compose.yml"
- "**/docker-compose.yaml"
- "**/Dockerfile"
- "**/*.service"
- "**/k8s/*.yaml"
- "**/deploy*.sh"
safe_discovery: local_files
evidence_scope:
- deployment_topology
- infrastructure
- service_configuration
refresh:
cadence: on_change
triggers:
- deployment_manifest_change
- infrastructure_manifest_change
- operator_request
- id: root.openbao.secret-metadata
type: secret_root
status: planned
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.king
source:
repo_slug: railiance-fabric
path: fabric/services/railiance-platform-openbao.yaml
safe_discovery: metadata_only
evidence_scope:
- secret_metadata
- infrastructure
refresh:
cadence: manual
triggers:
- secret_root_change
- operator_request
notes: Discover only existence and metadata for secret roots; never extract secret values.
- id: root.backup-recovery.metadata
type: backup_recovery
status: planned
fabric_id: fabric.railiance.primary
owner_actor_id: actor.railiance.king
source:
path: docs/financial-fabric-operator-guide.md
safe_discovery: explicit_review
evidence_scope:
- backup_recovery
- manual_review
refresh:
cadence: manual
triggers:
- backup_recovery_change
- operator_request
refresh:
cadence: manual
triggers:
- operator_request
- state_hub_repo_inventory_change
- git_commit
- deployment_manifest_change
- infrastructure_manifest_change
- endpoint_contract_change
- secret_root_change
- backup_recovery_change
- lord_or_tenant_change
notes: Manual rebuild is the default until snapshot deltas and freshness triggers are implemented.
templates:
future_subfabric:
parent_fabric_id: fabric.railiance.primary
tenant_actor_role: tenant
required_updates:
- Add tenant actor with role tenant.
- Add Subfabric under fabric.railiance.primary with tenant_actor_id.
- Add tenant-specific discovery roots with subfabric_id.
- Add cross-boundary utility edges with provider and consumer owner context.
note: Tenant subfabrics do not change the current root fabric criterion.

View File

@@ -0,0 +1,404 @@
apiVersion: railiance.fabric/v1alpha1
kind: DeploymentZoneInventory
generated_at: "2026-05-24T00:00:00+02:00"
source:
repo: railiance-fabric
workplan: RAIL-FAB-WP-0020
method: source-search-and-declared-surfaces
scope:
note: >
This inventory captures deployment-zone overlay evidence. It does not
define fabric membership, port ownership, live health, or access policy.
deployment_environments:
- id: dev
scenario: bernd-laptop
intended_reachability: private operator workstation
- id: test
scenario: coulombcore
intended_reachability: shared collaborator and early-access test stage
- id: prod
scenario: railiance01
intended_reachability: production stage, currently alpha-accessible to developers
surfaces:
- id: dev.bernd-laptop.railiance-fabric.registry-api
name: Railiance Fabric registry HTTP API
repo: railiance-fabric
service_id: railiance-fabric.registry
deployment_environment: dev
deployment_scenario: bernd-laptop
access_zone: private-dev
exposure_class: local-only
routing_authority: local-loopback-binding
policy_authority: local-loopback-binding
route_evidence:
route: http://127.0.0.1:8765
host: 127.0.0.1
port: 8765
protocol: http
evidence:
- path: fabric/interfaces/railiance-fabric-registry-http-api.yaml
kind: fabric-interface-declaration
- id: dev.bernd-laptop.railiance-fabric.graph-explorer
name: Railiance Fabric graph explorer UI
repo: railiance-fabric
service_id: railiance-fabric.registry
deployment_environment: dev
deployment_scenario: bernd-laptop
access_zone: private-dev
exposure_class: local-only
routing_authority: local-loopback-binding
policy_authority: local-loopback-binding
route_evidence:
route: http://127.0.0.1:8765/ui/graph-explorer
host: 127.0.0.1
port: 8765
protocol: http
path: /ui/graph-explorer
evidence:
- path: fabric/interfaces/railiance-fabric-registry-graph-explorer-ui.yaml
kind: fabric-interface-declaration
- id: dev.bernd-laptop.state-hub.api
name: State Hub HTTP API
repo: the-custodian
service_id: the-custodian.state-hub
deployment_environment: dev
deployment_scenario: bernd-laptop
access_zone: private-dev
exposure_class: local-only
routing_authority: local-loopback-binding
policy_authority: local-loopback-binding
route_evidence:
route: http://127.0.0.1:8000
host: 127.0.0.1
port: 8000
protocol: http
evidence:
- path: fabric/interfaces/the-custodian-state-hub-http-api.yaml
kind: fabric-interface-declaration
- id: dev.bernd-laptop.state-hub.mcp
name: State Hub MCP API
repo: the-custodian
service_id: the-custodian.state-hub
deployment_environment: dev
deployment_scenario: bernd-laptop
access_zone: private-dev
exposure_class: local-only
routing_authority: local-loopback-binding
policy_authority: local-loopback-binding
route_evidence:
route: http://127.0.0.1:8001
host: 127.0.0.1
port: 8001
protocol: http
evidence:
- path: fabric/interfaces/the-custodian-state-hub-mcp-api.yaml
kind: fabric-interface-declaration
- id: dev.bernd-laptop.state-hub.dashboard
name: State Hub dashboard
repo: the-custodian
service_id: the-custodian.state-hub
deployment_environment: dev
deployment_scenario: bernd-laptop
access_zone: private-dev
exposure_class: local-only
routing_authority: local-loopback-binding
policy_authority: local-loopback-binding
route_evidence:
route: http://127.0.0.1:3000
host: 127.0.0.1
port: 3000
protocol: http
evidence:
- path: fabric/interfaces/the-custodian-state-hub-dashboard.yaml
kind: fabric-interface-declaration
- id: dev.bernd-laptop.net-kingdom.control-surface
name: NetKingdom control surface
repo: net-kingdom
service_id: net-kingdom.iam-profile
deployment_environment: dev
deployment_scenario: bernd-laptop
access_zone: private-dev
exposure_class: local-only
routing_authority: local-loopback-binding
policy_authority: local-loopback-binding
route_evidence:
route: http://127.0.0.1:8876
host: 127.0.0.1
port: 8876
protocol: http
evidence:
- path: fabric/interfaces/net-kingdom-control-surface-ui.yaml
kind: fabric-interface-declaration
- path: ../net-kingdom/sso-mfa/k8s/keycape/README.md
kind: source-search-hit
note: local OIDC callback lists localhost port 8876
- id: test.coulombcore.state-hub.http-tunnel
name: State Hub HTTP API tunnel to coulombcore
repo: railiance-infra
service_id: the-custodian.state-hub
deployment_environment: test
deployment_scenario: coulombcore
access_zone: collaborator-test
exposure_class: collaborator-test
routing_authority: ops-bridge
policy_authority: ops-bridge-ssh
route_evidence:
route: http://127.0.0.1:18000
host: 127.0.0.1
port: 18000
protocol: http
tunnel_target: coulombcore
evidence:
- path: ../railiance-infra/docs/deploy-stack.md
lines: "127"
kind: source-search-hit
- id: test.coulombcore.state-hub.mcp-tunnel
name: State Hub MCP tunnel to coulombcore
repo: railiance-infra
service_id: the-custodian.state-hub
deployment_environment: test
deployment_scenario: coulombcore
access_zone: collaborator-test
exposure_class: collaborator-test
routing_authority: ops-bridge
policy_authority: ops-bridge-ssh
route_evidence:
route: http://127.0.0.1:18001
host: 127.0.0.1
port: 18001
protocol: http
tunnel_target: coulombcore
evidence:
- path: ../railiance-infra/docs/deploy-stack.md
lines: "128"
kind: source-search-hit
- id: test.coulombcore.k3s-api-tunnel
name: k3s API tunnel to coulombcore
repo: railiance-infra
deployment_environment: test
deployment_scenario: coulombcore
access_zone: collaborator-test
exposure_class: collaborator-test
routing_authority: ops-bridge
policy_authority: ops-bridge-ssh
route_evidence:
route: https://127.0.0.1:16443
host: 127.0.0.1
port: 16443
protocol: https
tunnel_target: coulombcore
evidence:
- path: ../railiance-infra/docs/deploy-stack.md
lines: "129"
kind: source-search-hit
- path: ../railiance-cluster/SCOPE.md
lines: "127"
kind: source-search-hit
note: cluster scope states it runs on COULOMBCORE
- id: prod.railiance01.gitea
name: Gitea ingress
repo: railiance-apps
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-public
exposure_class: production-public
routing_authority: traefik
policy_authority: null
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://gitea.coulomb.social
hostname: gitea.coulomb.social
port: 443
protocol: https
review:
status: candidate
note: access zone and policy authority require operator review
evidence:
- path: ../railiance-apps/manifests/gitea-ingress.yaml
lines: "2,12,14,16,27"
kind: kubernetes-ingress
- path: ../railiance-apps/workplans/railiance-apps-WP-0002-vergabe-teilnahme-on-railiance01.md
lines: "612,613"
kind: source-search-hit
note: places Gitea before vergabe-teilnahme on railiance01
- id: prod.railiance01.vergabe-teilnahme
name: Vergabe Teilnahme ingress
repo: railiance-apps
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-public
exposure_class: production-public
routing_authority: traefik
policy_authority: null
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://vergabe-teilnahme.whywhynot.de
hostname: vergabe-teilnahme.whywhynot.de
port: 443
protocol: https
review:
status: candidate
note: production public classification is inferred from ingress host and workplan
evidence:
- path: ../railiance-apps/manifests/vergabe-teilnahme-ingress.yaml
lines: "2,11,13,15,26"
kind: kubernetes-ingress
- path: ../railiance-apps/workplans/railiance-apps-WP-0002-vergabe-teilnahme-on-railiance01.md
lines: "22,40,68,69,163,612,613"
kind: source-search-hit
- id: prod.railiance01.authelia
name: Authelia ingress
repo: net-kingdom
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-public
exposure_class: production-public
routing_authority: traefik
policy_authority: authelia
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://auth.coulomb.social
hostname: auth.coulomb.social
port: 443
protocol: https
review:
status: candidate
note: railiance01 attribution comes from NetKingdom deployment workplan
evidence:
- path: ../net-kingdom/sso-mfa/k8s/authelia/ingress.yaml
lines: "13,22,24,26,38"
kind: kubernetes-ingress
- path: ../net-kingdom/workplans/NK-WP-0003-keycape-privacyidea-cluster-deployment.md
lines: "29,47,88,101"
kind: source-search-hit
- id: prod.railiance01.keycape
name: Keycape ingress
repo: net-kingdom
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-public
exposure_class: production-public
routing_authority: traefik
policy_authority: traefik-middleware
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://kc.coulomb.social
hostname: kc.coulomb.social
port: 443
protocol: https
review:
status: candidate
note: middleware is present, but intended audience still needs operator review
evidence:
- path: ../net-kingdom/sso-mfa/k8s/keycape/ingress.yaml
lines: "13,22,23,27,29,41"
kind: kubernetes-ingress
- path: ../net-kingdom/sso-mfa/k8s/keycape/middleware.yaml
lines: "9,24"
kind: traefik-middleware
- id: prod.railiance01.privacyidea
name: privacyIDEA ingress
repo: net-kingdom
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-admin
exposure_class: production-admin
routing_authority: traefik
policy_authority: traefik-middleware
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://pink.coulomb.social
hostname: pink.coulomb.social
port: 443
protocol: https
review:
status: candidate
note: admin classification inferred from privacyIDEA role and middleware
evidence:
- path: ../net-kingdom/sso-mfa/k8s/privacyidea/ingress.yaml
lines: "25,34,36,38,40,52,60,69,71,75,77,89"
kind: kubernetes-ingress
- path: ../net-kingdom/sso-mfa/k8s/privacyidea/middleware.yaml
lines: "19,41"
kind: traefik-middleware
- id: prod.railiance01.privacyidea-account
name: privacyIDEA account self-service ingress
repo: net-kingdom
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-public
exposure_class: production-public
routing_authority: traefik
policy_authority: traefik-middleware
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://pink-account.coulomb.social
hostname: pink-account.coulomb.social
port: 443
protocol: https
review:
status: candidate
note: self-service classification inferred from host name and middleware
evidence:
- path: ../net-kingdom/sso-mfa/k8s/privacyidea/ingress.yaml
lines: "94,103,104,106,108,120"
kind: kubernetes-ingress
- id: prod.railiance01.lldap
name: LLDAP ingress
repo: net-kingdom
deployment_environment: prod
deployment_scenario: railiance01
access_zone: production-admin
exposure_class: production-admin
routing_authority: traefik
policy_authority: traefik-admin-allowlist
tls_authority: cert-manager:letsencrypt-prod
route_evidence:
route: https://lldap.coulomb.social
hostname: lldap.coulomb.social
port: 443
protocol: https
review:
status: candidate
note: admin allowlist middleware indicates intended restricted access
evidence:
- path: ../net-kingdom/sso-mfa/k8s/lldap/ingress.yaml
lines: "12,21,22,24,26,38"
kind: kubernetes-ingress
- path: ../net-kingdom/sso-mfa/k8s/lldap/middleware.yaml
lines: "11"
kind: traefik-middleware
ambiguities:
- id: railiance01-coulombcore-ip-conflict
severity: high
summary: Source documents disagree on which host owns 92.205.130.254.
evidence:
- path: ../railiance-apps/workplans/railiance-apps-WP-0002-vergabe-teilnahme-on-railiance01.md
lines: "22,163"
note: says railiance01 and Traefik LoadBalancer use 92.205.130.254
- path: ../railiance-infra/SCOPE.md
lines: "126"
note: says COULOMBCORE is 92.205.130.254 and Railiance01 is 92.205.62.239
next: reconcile host inventory before treating IP evidence as authoritative
- id: prod-access-zone-review
severity: medium
summary: Production access zones are candidate classifications.
evidence:
- path: ../railiance-apps/manifests
note: app ingress manifests show routing and TLS but not business audience
- path: ../net-kingdom/sso-mfa/k8s
note: middleware and network policy hint at access intent but do not replace operator review
next: confirm each production host as public, admin, or early-access
- id: test-reachability-is-tunneled
severity: medium
summary: Current coulombcore routes are ops-bridge tunnel evidence, not public ingress evidence.
evidence:
- path: ../railiance-infra/docs/deploy-stack.md
lines: "127,128,129"
note: state-hub and k3s API access are tunnel commands
next: add executable test-stage ingress/service discovery when coulombcore manifests exist
missing_policy_authority:
- surface_id: prod.railiance01.gitea
reason: route and TLS are discovered, but access policy authority is not evident in the ingress artifact
- surface_id: prod.railiance01.vergabe-teilnahme
reason: route and TLS are discovered, but access policy authority is not evident in the ingress artifact

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
{
"apiVersion": "railiance.fabric/v1alpha2",
"change_sets": {
"blockers": [],
"containment": [],
"ownership": [],
"review_state": []
},
"current": {
"generated_at": "2026-05-24T12:04:20Z",
"manifest_fingerprint": "dd279b655d68222a99671c1c875afdaa70ae2f907fd526e6673ddf756bc90dae",
"manifest_id": "railiance.accountability-roots"
},
"edge_delta": {
"added": [
"candidate-edge:d448183c64bba5c0"
],
"changed": [],
"removed": [],
"unchanged": []
},
"generated_at": "2026-05-24T12:04:59Z",
"kind": "AccountabilityUpdateDelta",
"node_delta": {
"added": [
"identity:actor:actor.railiance.king",
"identity:actor:actor.railiance.primary-lord",
"identity:backup-recovery-root:docs-financial-fabric-operator-guide.md",
"identity:catalog-root:gitea_organization",
"identity:catalog-root:registry_manifest",
"identity:deployable:home-worsch-.config-systemd-user-custodian-sync.service",
"identity:deployable:home-worsch-activity-core-dockerfile",
"identity:deployable:home-worsch-go-src-crypto-internal-boring-dockerfile",
"identity:deployable:home-worsch-go-src-crypto-internal-nistec-fiat-dockerfile",
"identity:deployable:home-worsch-key-cape-dockerfile",
"identity:deployable:home-worsch-repo-scoping-var-checkouts-key-cape-a01db9828dd4-dockerfile",
"identity:deployable:home-worsch-state-hub-dockerfile",
"identity:deployable:home-worsch-the-custodian-infra-build-machines-haskell-files-build-agent.service",
"identity:deployable:home-worsch-vergabe-teilnahme-dockerfile",
"identity:fabric:fabric.railiance.primary",
"identity:host-path:fabric",
"identity:host-path:git",
"identity:host-path:home-worsch-.nvm-.git",
"identity:host-path:home-worsch-activity-core-.git",
"identity:host-path:home-worsch-artifact-store-.git",
"identity:host-path:home-worsch-can-you-assist-.git",
"identity:host-path:home-worsch-domain-tree-.git",
"identity:host-path:home-worsch-flex-auth-.git",
"identity:host-path:home-worsch-guide-board-.git",
"identity:host-path:home-worsch-helix-forge-.git",
"identity:host-path:home-worsch-ihp-railiance-probe-.git",
"identity:host-path:home-worsch-info-tech-canon-.git",
"identity:host-path:home-worsch-infospace-bench-.git",
"identity:host-path:home-worsch-inter-hub-.git",
"identity:host-path:home-worsch-issue-core-.git",
"identity:host-path:home-worsch-key-cape-.git",
"identity:host-path:home-worsch-kontextual-engine-.git",
"identity:host-path:home-worsch-llm-connect-.git",
"identity:host-path:home-worsch-markitect-filter-.git",
"identity:host-path:home-worsch-markitect-main-.git",
"identity:host-path:home-worsch-markitect-quarkdown-.git",
"identity:host-path:home-worsch-markitect-tool-.git",
"identity:host-path:home-worsch-net-kingdom-.git",
"identity:host-path:home-worsch-open-cmis-tck-.git",
"identity:host-path:home-worsch-open-reuse-.git",
"identity:host-path:home-worsch-ops-bridge-.git",
"identity:host-path:home-worsch-ops-warden-.git",
"identity:host-path:home-worsch-phase-memory-.git",
"identity:host-path:home-worsch-railiance-apps-.git",
"identity:host-path:home-worsch-railiance-cluster-.git",
"identity:host-path:home-worsch-railiance-enablement-.git",
"identity:host-path:home-worsch-railiance-infra-.git",
"identity:host-path:home-worsch-railiance-platform-.git",
"identity:host-path:home-worsch-repo-scoping-.git",
"identity:host-path:home-worsch-repo-seed-.git",
"identity:host-path:home-worsch-shard-wiki-.git",
"identity:host-path:home-worsch-state-hub-.git",
"identity:host-path:home-worsch-tegwick-control-.git",
"identity:host-path:home-worsch-the-custodian-.git",
"identity:host-path:home-worsch-user-engine-.git",
"identity:host-path:home-worsch-vantage-point-.git",
"identity:host-path:home-worsch-vergabe-teilnahme-.git",
"identity:host-path:home-worsch-whynot-control-.git",
"identity:host-path:home-worsch-whynot-design-.git",
"identity:netkingdom:railiance.netkingdom",
"identity:repository:activity-core",
"identity:repository:artifact-store",
"identity:repository:domain-tree",
"identity:repository:flex-auth",
"identity:repository:guide-board",
"identity:repository:helix-forge",
"identity:repository:ihp-railiance-probe",
"identity:repository:infospace-bench",
"identity:repository:inter-hub",
"identity:repository:issue-core",
"identity:repository:key-cape",
"identity:repository:kontextual-engine",
"identity:repository:llm-connect",
"identity:repository:markitect-filter",
"identity:repository:markitect-project",
"identity:repository:markitect-quarkdown",
"identity:repository:markitect-tool",
"identity:repository:net-kingdom",
"identity:repository:open-cmis-tck",
"identity:repository:open-reuse",
"identity:repository:ops-bridge",
"identity:repository:ops-warden",
"identity:repository:phase-memory",
"identity:repository:railiance-apps",
"identity:repository:railiance-cluster",
"identity:repository:railiance-enablement",
"identity:repository:railiance-fabric",
"identity:repository:railiance-infra",
"identity:repository:railiance-platform",
"identity:repository:repo-scoping",
"identity:repository:state-hub",
"identity:repository:the-custodian",
"identity:repository:vergabe-teilnahme",
"identity:secret-root:fabric-services-railiance-platform-openbao.yaml"
],
"changed": [],
"removed": [],
"unchanged": []
},
"previous": {},
"summary": {
"edges_added": 1,
"edges_changed": 0,
"edges_removed": 0,
"edges_unchanged": 0,
"meaningful_change_count": 95,
"nodes_added": 94,
"nodes_changed": 0,
"nodes_removed": 0,
"nodes_unchanged": 0,
"promotion_needed": true
}
}

View File

@@ -0,0 +1,65 @@
apiVersion: railiance.fabric/v1alpha2
kind: FinancialFabricBaseline
metadata:
id: railiance.netkingdom-baseline
name: Railiance Netkingdom Baseline
spec:
netkingdom:
id: railiance.netkingdom
name: Railiance Netkingdom
king_actor_id: actor.railiance.king
actors:
- id: actor.railiance.king
kind: FabricActor
role: king
name: Railiance King
description: Responsible for the Railiance netkingdom and recovery authority.
authority:
recovery_authority: true
secrets_authority: true
backup_authority: true
termination_authority: true
- id: actor.railiance.primary-lord
kind: FabricActor
role: lord
name: Railiance Primary Lord
description: Pays for the current Railiance infrastructure boundary.
fabrics:
- id: fabric.railiance.primary
kind: Fabric
name: Railiance Primary Fabric
netkingdom_id: railiance.netkingdom
lord_actor_id: actor.railiance.primary-lord
parent_fabric_id: null
status: active
boundary:
boundary_type: fabric
criterion: financial_and_operational_accountability
payment_responsibility: actor.railiance.primary-lord
operational_responsibility: actor.railiance.king
recovery_responsibility: actor.railiance.king
evidence_refs: []
defaults:
containment:
netkingdom_id: railiance.netkingdom
fabric_id: fabric.railiance.primary
subfabric_id: null
environment: local
deployment_scenario_id: null
ownership:
owner_actor_id: actor.railiance.primary-lord
owner_role: lord
resolution: inherited
inherited_from: fabric.railiance.primary
supporting_actor_ids:
- actor.railiance.king
accounting:
cost_center_id: null
profit_center_id: null
allocation_model: null
future_subfabric_template:
kind: Subfabric
parent_fabric_id: fabric.railiance.primary
netkingdom_id: railiance.netkingdom
tenant_actor_role: tenant
note: Add a tenant actor and subfabric without changing the root fabric criterion.

View File

@@ -0,0 +1,35 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: net-kingdom.control-surface.ui
name: NetKingdom Control Surface
owner: net-kingdom
repo: net-kingdom
domain: railiance
spec:
lifecycle: active
environments: [dev]
description: Local NetKingdom control surface on the operator workstation.
interface_type: web-ui
version: v1
service_id: net-kingdom.iam-profile
capability_ids:
- net-kingdom.iam-profile.issuer
endpoint:
url: http://127.0.0.1:8876
notes: Local workstation endpoint after moving away from the Fabric registry port.
deployment_overlay:
deployment_environment: dev
deployment_scenario: bernd-laptop
routing_authority: net-kingdom-local-process
access_zone: private-dev
policy_authority: local-loopback-binding
exposure_class: local-only
route_evidence:
host: 127.0.0.1
port: 8876
protocol: tcp
route: http://127.0.0.1:8876
auth:
method: none
data_classification: internal

View File

@@ -0,0 +1,23 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-cluster.kubernetes.api
name: Kubernetes API
owner: railiance-cluster
repo: railiance-cluster
domain: railiance
source_links:
- label: Cluster scope
path: /home/worsch/railiance-cluster/SCOPE.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Kubernetes API surface and RBAC-controlled runtime contract consumed by Railiance workloads and operators.
interface_type: kubernetes-api
version: v1
service_id: railiance-cluster.kubernetes
capability_ids:
- railiance-cluster.kubernetes.runtime
auth:
method: kubernetes_service_account
data_classification: restricted

View File

@@ -0,0 +1,23 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-enablement.delivery-templates.workflow-template-contract
name: Workflow template contract
owner: railiance-enablement
repo: railiance-enablement
domain: railiance
source_links:
- label: Enablement scope
path: /home/worsch/railiance-enablement/SCOPE.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Template contract for reusable Railiance CI/CD and GitOps workflow patterns.
interface_type: workflow-template-contract
version: v1
service_id: railiance-enablement.delivery-templates
capability_ids:
- railiance-enablement.delivery-templates.ci-cd-templates
auth:
method: none
data_classification: internal

View File

@@ -23,6 +23,18 @@ spec:
endpoint:
url: http://127.0.0.1:8765/ui/graph-explorer
notes: Local workstation UI when the registry service is running.
deployment_overlay:
deployment_environment: dev
deployment_scenario: bernd-laptop
routing_authority: railiance-fabric-registry
access_zone: private-dev
policy_authority: local-loopback-binding
exposure_class: local-only
route_evidence:
host: 127.0.0.1
port: 8765
protocol: tcp
route: http://127.0.0.1:8765/ui/graph-explorer
auth:
method: none
data_classification: internal

View File

@@ -23,6 +23,18 @@ spec:
endpoint:
url: http://127.0.0.1:8765
notes: Local workstation endpoint when the registry service is running.
deployment_overlay:
deployment_environment: dev
deployment_scenario: bernd-laptop
routing_authority: local-process
access_zone: private-dev
policy_authority: local-loopback-binding
exposure_class: local-only
route_evidence:
host: 127.0.0.1
port: 8765
protocol: tcp
route: http://127.0.0.1:8765
auth:
method: none
data_classification: internal

View File

@@ -0,0 +1,23 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-forge.source-forge.evidence-contract
name: Forge evidence contract
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Observability and evidence contract
path: /home/worsch/railiance-forge/docs/observability-operating-evidence.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Release-readiness, artifact promotion, restore, storage, and operating evidence contract for forge consumers.
interface_type: evidence-contract
version: v1
service_id: railiance-forge.source-forge
capability_ids:
- railiance-forge.source-forge.artifact-promotion-evidence
auth:
method: none
data_classification: internal

View File

@@ -0,0 +1,25 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-forge.source-forge.git-ssh
name: Git SSH endpoint
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Observability and evidence contract
path: /home/worsch/railiance-forge/docs/observability-operating-evidence.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Git-over-SSH endpoint contract for repository clone, fetch, and push operations when exposed.
interface_type: git-ssh
version: gitea-current
service_id: railiance-forge.source-forge
capability_ids:
- railiance-forge.source-forge.source-hosting
endpoint:
notes: Record the published SSH host and port once the endpoint is verified.
auth:
method: static_secret
data_classification: confidential

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-forge.source-forge.oci-registry
name: Gitea OCI registry
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Container registry docs
path: /home/worsch/railiance-forge/docs/gitea-container-registry.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: OCI registry endpoint served by current Gitea for Railiance container images.
interface_type: oci-registry
version: registry-v2
service_id: railiance-forge.source-forge
capability_ids:
- railiance-forge.source-forge.container-registry
endpoint:
url: https://gitea.coulomb.social/v2/
auth:
method: api_key
scopes:
- package:read
- package:write
data_classification: confidential

View File

@@ -0,0 +1,28 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-forge.source-forge.python-package-index
name: Gitea Python package index
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Package registry docs
path: /home/worsch/railiance-forge/docs/gitea-package-registry.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Python package index endpoint served by current Gitea for internal Railiance packages.
interface_type: python-package-index
version: simple-api
service_id: railiance-forge.source-forge
capability_ids:
- railiance-forge.source-forge.python-package-registry
endpoint:
url: https://gitea.coulomb.social/api/packages/coulomb/pypi/simple/
auth:
method: api_key
scopes:
- package:read
- package:write
data_classification: confidential

View File

@@ -0,0 +1,23 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-forge.source-forge.runner-label-contract
name: Runner label contract
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Runner ownership contract
path: /home/worsch/railiance-forge/docs/ci-runner-actions-gitops-ownership.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Semantic runner labels, placement, trust levels, and credential boundaries consumed by workflow templates and release checks.
interface_type: workflow-runner-label-contract
version: v1
service_id: railiance-forge.source-forge
capability_ids:
- railiance-forge.source-forge.workflow-runner-substrate
auth:
method: none
data_classification: internal

View File

@@ -0,0 +1,25 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: railiance-forge.source-forge.web-ui
name: Source forge web UI
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Observability and evidence contract
path: /home/worsch/railiance-forge/docs/observability-operating-evidence.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Current Gitea web UI and HTTP endpoint for source hosting and package workflows.
interface_type: web-ui
version: gitea-current
service_id: railiance-forge.source-forge
capability_ids:
- railiance-forge.source-forge.source-hosting
endpoint:
url: https://gitea.coulomb.social/
auth:
method: unknown
data_classification: confidential

View File

@@ -0,0 +1,35 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: the-custodian.state-hub.dashboard
name: State Hub Dashboard
owner: the-custodian
repo: the-custodian
domain: custodian
spec:
lifecycle: active
environments: [dev]
description: Local browser dashboard for State Hub coordination views.
interface_type: web-ui
version: v1
service_id: the-custodian.state-hub
capability_ids:
- the-custodian.state-hub.coordination
endpoint:
url: http://127.0.0.1:3000
notes: Local workstation dashboard endpoint when the State Hub frontend is running.
deployment_overlay:
deployment_environment: dev
deployment_scenario: bernd-laptop
routing_authority: vite-dev-server
access_zone: private-dev
policy_authority: local-loopback-binding
exposure_class: local-only
route_evidence:
host: 127.0.0.1
port: 3000
protocol: tcp
route: http://127.0.0.1:3000
auth:
method: none
data_classification: internal

View File

@@ -15,6 +15,21 @@ spec:
service_id: the-custodian.state-hub
capability_ids:
- the-custodian.state-hub.coordination
endpoint:
url: http://127.0.0.1:8000
notes: Local workstation State Hub REST API.
deployment_overlay:
deployment_environment: dev
deployment_scenario: bernd-laptop
routing_authority: state-hub-local-process
access_zone: private-dev
policy_authority: local-loopback-binding
exposure_class: local-only
route_evidence:
host: 127.0.0.1
port: 8000
protocol: tcp
route: http://127.0.0.1:8000
auth:
method: none
data_classification: internal

View File

@@ -0,0 +1,35 @@
apiVersion: railiance.fabric/v1alpha1
kind: InterfaceDeclaration
metadata:
id: the-custodian.state-hub.mcp-api
name: State Hub MCP API
owner: the-custodian
repo: the-custodian
domain: custodian
spec:
lifecycle: active
environments: [dev]
description: Local MCP surface for State Hub coordination tools.
interface_type: mcp-api
version: v1
service_id: the-custodian.state-hub
capability_ids:
- the-custodian.state-hub.coordination
endpoint:
url: http://127.0.0.1:8001
notes: Local workstation MCP endpoint when State Hub is running.
deployment_overlay:
deployment_environment: dev
deployment_scenario: bernd-laptop
routing_authority: state-hub-local-process
access_zone: private-dev
policy_authority: local-loopback-binding
exposure_class: local-only
route_evidence:
host: 127.0.0.1
port: 8001
protocol: tcp
route: http://127.0.0.1:8001
auth:
method: none
data_classification: internal

View File

@@ -15,3 +15,4 @@ spec:
- net-kingdom.iam-profile.issuer
exposes_interfaces:
- net-kingdom.iam-profile.oidc-discovery
- net-kingdom.control-surface.ui

View File

@@ -0,0 +1,18 @@
apiVersion: railiance.fabric/v1alpha1
kind: ServiceDeclaration
metadata:
id: railiance-apps.s5-releases
name: Railiance S5 app releases
owner: railiance-apps
repo: railiance-apps
domain: railiance
source_links:
- label: Apps scope
path: /home/worsch/railiance-apps/SCOPE.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: S5 application release surface that consumes forge artifacts, app manifests, runbooks, dry-runs, and smoke evidence.
service_type: app-release-surface
provides_capabilities: []
exposes_interfaces: []

View File

@@ -0,0 +1,20 @@
apiVersion: railiance.fabric/v1alpha1
kind: ServiceDeclaration
metadata:
id: railiance-cluster.kubernetes
name: Railiance Kubernetes runtime
owner: railiance-cluster
repo: railiance-cluster
domain: railiance
source_links:
- label: Cluster scope
path: /home/worsch/railiance-cluster/SCOPE.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Kubernetes runtime layer that provides the API server, namespaces, workloads, Services, Ingresses, and controller substrate for Railiance services.
service_type: cluster-runtime
provides_capabilities:
- railiance-cluster.kubernetes.runtime
exposes_interfaces:
- railiance-cluster.kubernetes.api

View File

@@ -0,0 +1,22 @@
apiVersion: railiance.fabric/v1alpha1
kind: ServiceDeclaration
metadata:
id: railiance-enablement.delivery-templates
name: Railiance delivery templates
owner: railiance-enablement
repo: railiance-enablement
domain: railiance
source_links:
- label: Enablement scope
path: /home/worsch/railiance-enablement/SCOPE.md
- label: Enablement intent
path: /home/worsch/railiance-enablement/INTENT.md
spec:
lifecycle: planned
environments: [dev, staging, prod]
description: Reusable CI/CD and GitOps workflow template surface for Railiance workload delivery.
service_type: enablement-template-surface
provides_capabilities:
- railiance-enablement.delivery-templates.ci-cd-templates
exposes_interfaces:
- railiance-enablement.delivery-templates.workflow-template-contract

View File

@@ -0,0 +1,31 @@
apiVersion: railiance.fabric/v1alpha1
kind: ServiceDeclaration
metadata:
id: railiance-forge.source-forge
name: Railiance source forge
owner: railiance-forge
repo: railiance-forge
domain: railiance
source_links:
- label: Forge scope
path: /home/worsch/railiance-forge/SCOPE.md
- label: Forge intent
path: /home/worsch/railiance-forge/INTENT.md
spec:
lifecycle: active
environments: [dev, staging, prod]
description: Current Gitea source forge and future Forgejo migration surface for source hosting, registries, runner substrate, and release artifact evidence.
service_type: forge-runtime
provides_capabilities:
- railiance-forge.source-forge.source-hosting
- railiance-forge.source-forge.container-registry
- railiance-forge.source-forge.python-package-registry
- railiance-forge.source-forge.workflow-runner-substrate
- railiance-forge.source-forge.artifact-promotion-evidence
exposes_interfaces:
- railiance-forge.source-forge.web-ui
- railiance-forge.source-forge.git-ssh
- railiance-forge.source-forge.oci-registry
- railiance-forge.source-forge.python-package-index
- railiance-forge.source-forge.runner-label-contract
- railiance-forge.source-forge.evidence-contract

View File

@@ -15,3 +15,5 @@ spec:
- the-custodian.state-hub.coordination
exposes_interfaces:
- the-custodian.state-hub.http-api
- the-custodian.state-hub.mcp-api
- the-custodian.state-hub.dashboard

File diff suppressed because it is too large Load Diff

212
railiance_fabric/canon.py Normal file
View File

@@ -0,0 +1,212 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
CANONICAL_NODE_CATEGORIES = (
"source-repository",
"software-system",
"service",
"endpoint",
"deployment",
"runtime-resource",
"datastore",
"flow",
"policy",
"control",
"evidence",
"task",
"consumer-purpose",
"telemetry-signal",
)
CANONICAL_EDGE_TYPES = (
"built_from",
"implements",
"exposes",
"depends_on",
"deploys",
"flows_to",
"governed_by",
"evidenced_by",
"observed_by",
"part_of",
"reads_or_writes",
"creates_task",
)
DISPLAY_ONLY_EDGE_TYPES = (
"collapsed_into",
"declares",
"grouped_with",
"highlight_path",
"near",
"owns_deployment",
"same_color_group",
)
EVIDENCE_STATES = ("observed", "declared", "inferred", "proposed", "gap")
MAPPING_FITS = ("direct", "partial", "conflict", "gap", "unknown")
@dataclass(frozen=True)
class CanonNodeMapping:
category: str
canon_anchor: str
fit: str
notes: str = ""
@dataclass(frozen=True)
class CanonEdgeMapping:
canonical_type: str
canon_anchor: str
fit: str
display_only: bool = False
notes: str = ""
UNKNOWN_NODE_MAPPING = CanonNodeMapping(
category="unknown",
canon_anchor="",
fit="gap",
notes="No canon mapping has been selected for this Fabric node kind yet.",
)
UNKNOWN_EDGE_MAPPING = CanonEdgeMapping(
canonical_type="",
canon_anchor="",
fit="gap",
notes="No canon mapping has been selected for this Fabric edge type yet.",
)
NODE_KIND_CANON_MAP: dict[str, CanonNodeMapping] = {
"ApplicationEndpoint": CanonNodeMapping("endpoint", "model/network", "direct"),
"BindingAssertion": CanonNodeMapping("evidence", "model/observability", "partial"),
"CapabilityDeclaration": CanonNodeMapping("software-system", "model/landscape", "partial"),
"ContainerBuild": CanonNodeMapping("deployment", "model/devsecops", "partial"),
"DependencyDeclaration": CanonNodeMapping("service", "model/landscape", "gap"),
"DeploymentService": CanonNodeMapping("deployment", "model/devsecops", "direct"),
"DomainName": CanonNodeMapping("endpoint", "model/network", "partial"),
"ExternalLibrary": CanonNodeMapping("software-system", "model/landscape", "partial"),
"FabricRegistryEntry": CanonNodeMapping("source-repository", "model/devsecops", "partial"),
"Fabric": CanonNodeMapping("control", "model/governance", "gap"),
"FabricActor": CanonNodeMapping("control", "model/governance", "gap"),
"InterfaceDeclaration": CanonNodeMapping("endpoint", "model/network", "partial"),
"Library": CanonNodeMapping("software-system", "model/landscape", "partial"),
"Lockfile": CanonNodeMapping("evidence", "model/observability", "partial"),
"Netkingdom": CanonNodeMapping("software-system", "model/landscape", "gap"),
"NetworkPort": CanonNodeMapping("endpoint", "model/network", "direct"),
"ProfitCenter": CanonNodeMapping("control", "model/governance", "gap"),
"Repository": CanonNodeMapping("source-repository", "model/devsecops", "direct"),
"RuntimeService": CanonNodeMapping("runtime-resource", "model/landscape", "direct"),
"ScoreWorkload": CanonNodeMapping("deployment", "model/devsecops", "direct"),
"Server": CanonNodeMapping("runtime-resource", "model/landscape", "partial"),
"ServiceConfig": CanonNodeMapping("evidence", "model/observability", "partial"),
"ServiceDeclaration": CanonNodeMapping("service", "model/landscape", "direct"),
"Subfabric": CanonNodeMapping("control", "model/governance", "gap"),
"UtilityInterface": CanonNodeMapping("endpoint", "model/network", "partial"),
"CostCenter": CanonNodeMapping("control", "model/governance", "gap"),
}
EDGE_TYPE_CANON_MAP: dict[str, CanonEdgeMapping] = {
"available_via": CanonEdgeMapping("exposes", "model/network", "partial"),
"attributed_to_cost_center": CanonEdgeMapping("governed_by", "model/governance", "gap"),
"attributed_to_profit_center": CanonEdgeMapping("governed_by", "model/governance", "gap"),
"binds": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"builds_container": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"cataloged_as": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"consumes": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"contains": CanonEdgeMapping("part_of", "model/landscape", "partial"),
"declares": CanonEdgeMapping("part_of", "model/devsecops", "partial", display_only=True),
"declares_package": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"defines_deployment": CanonEdgeMapping("built_from", "model/devsecops", "partial"),
"defines_runtime_object": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"defines_workload": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"deployed_as": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"depends_on_library": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"documents_interface": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"exposes": CanonEdgeMapping("exposes", "model/network", "direct"),
"exposes_port": CanonEdgeMapping("exposes", "model/network", "direct"),
"listens_on": CanonEdgeMapping("exposes", "model/network", "direct"),
"names_endpoint": CanonEdgeMapping("exposes", "model/network", "partial"),
"opens_port": CanonEdgeMapping("exposes", "model/network", "partial"),
"operated_by": CanonEdgeMapping("governed_by", "model/governance", "partial"),
"owned_by": CanonEdgeMapping("governed_by", "model/governance", "partial"),
"owns_deployment": CanonEdgeMapping("part_of", "model/devsecops", "partial", display_only=True),
"provides": CanonEdgeMapping("implements", "model/landscape", "partial"),
"provides_utility_to": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"resolves_to": CanonEdgeMapping("flows_to", "model/network", "partial"),
"routes_to_port": CanonEdgeMapping("flows_to", "model/network", "partial"),
"routes_to_service": CanonEdgeMapping("flows_to", "model/network", "partial"),
"runs_on": CanonEdgeMapping("deploys", "model/devsecops", "partial"),
"suggests_capability": CanonEdgeMapping("creates_task", "model/task", "partial"),
"uses_config": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
"uses_interface": CanonEdgeMapping("depends_on", "model/landscape", "partial"),
"uses_lockfile": CanonEdgeMapping("evidenced_by", "model/observability", "partial"),
}
def node_canon_mapping(kind: str) -> CanonNodeMapping:
if kind in NODE_KIND_CANON_MAP:
return NODE_KIND_CANON_MAP[kind]
if kind.startswith("Kubernetes"):
return CanonNodeMapping("runtime-resource", "model/landscape", "direct")
return UNKNOWN_NODE_MAPPING
def edge_canon_mapping(edge_type: str) -> CanonEdgeMapping:
normalized = str(edge_type or "").strip()
if normalized.startswith("binds:"):
return EDGE_TYPE_CANON_MAP["binds"]
if normalized in EDGE_TYPE_CANON_MAP:
return EDGE_TYPE_CANON_MAP[normalized]
if normalized in CANONICAL_EDGE_TYPES:
return CanonEdgeMapping(normalized, _anchor_for_canonical_edge(normalized), "direct")
if normalized in DISPLAY_ONLY_EDGE_TYPES:
return CanonEdgeMapping("", "", "gap", display_only=True)
return UNKNOWN_EDGE_MAPPING
def evidence_state_for(
*,
origin: str = "",
source_kind: str = "",
review_state: str = "",
confidence: float | None = None,
) -> str:
if review_state == "rejected":
return "gap"
if origin == "llm":
return "proposed"
if confidence is not None and confidence < 0.5:
return "inferred"
if source_kind in {"package_registry", "container_registry", "service_catalog", "fabric_registry"}:
return "observed"
if source_kind in {"llm"}:
return "proposed"
if not source_kind and origin == "deterministic":
return "inferred"
return "declared"
def source_kind_from_anchor(source_anchor: dict[str, Any]) -> str:
return str(source_anchor.get("source_kind") or "")
def _anchor_for_canonical_edge(edge_type: str) -> str:
return {
"built_from": "model/devsecops",
"implements": "model/security",
"exposes": "model/network",
"depends_on": "model/landscape",
"deploys": "model/devsecops",
"flows_to": "model/network",
"governed_by": "model/governance",
"evidenced_by": "model/observability",
"observed_by": "model/observability",
"part_of": "model/landscape",
"reads_or_writes": "model/data",
"creates_task": "model/task",
}.get(edge_type, "")

View File

@@ -14,12 +14,23 @@ from pathlib import Path
from typing import Any
from urllib.parse import quote
from .accountability_roots import (
DEFAULT_ROOT_MANIFEST_PATH,
AccountabilityEvidenceStore,
build_identity_projection,
build_ownership_review,
build_update_delta,
collect_accountability_root_evidence,
load_accountability_root_manifest,
)
from .connectors import ConnectorConfig
from .financial_baseline import financial_export_from_legacy
from .loader import declaration_files, load_yaml
from .graph import FabricGraph, build_graph
from .graph_explorer import fabric_graph_explorer_payload
from .llm_extraction import LLMExtractionConfig
from .reconciliation import reconcile_discovery_snapshots
from .registry import RESET_CONFIRMATION_TOKEN
from .scanner import EXTRACTOR_VERSION, ScanOptions, scan_repo
from .validation import validate_roots
@@ -71,9 +82,9 @@ def build_parser() -> argparse.ArgumentParser:
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, Mermaid, or graph-explorer payload.")
export = sub.add_parser("export", help="Export graph as JSON, Mermaid, graph-explorer, or financial payload.")
export.add_argument("paths", nargs="*", type=Path, default=[Path(".")])
export.add_argument("--format", choices=["json", "mermaid", "graph-explorer"], default="json")
export.add_argument("--format", choices=["json", "mermaid", "graph-explorer", "financial"], default="json")
scan = sub.add_parser("scan", help="Scan a repo for deterministic discovery candidates.")
scan.add_argument("path", nargs="?", type=Path, default=Path("."))
@@ -106,6 +117,33 @@ def build_parser() -> argparse.ArgumentParser:
help="Manifest path for the local-fabric-registry connector.",
)
discover_roots = sub.add_parser(
"discover-roots",
help="Collect raw evidence from accountability root manifest entries.",
)
discover_roots.add_argument("--manifest", type=Path, default=DEFAULT_ROOT_MANIFEST_PATH)
discover_roots.add_argument("--include-remote", action="store_true", help="Allow HTTP reads from configured remote roots.")
discover_roots.add_argument("--max-items-per-root", type=int, default=200)
discover_roots.add_argument("--identity-projection", action="store_true", help="Print normalized identity candidates instead of raw evidence.")
discover_roots.add_argument("--ownership-review", action="store_true", help="Print ownership resolution and review blockers.")
discover_roots.add_argument("--delta", action="store_true", help="Print a delta against previous identity/ownership review files.")
discover_roots.add_argument("--previous-identity-projection", type=Path, default=None)
discover_roots.add_argument("--previous-ownership-review", type=Path, default=None)
discover_roots.add_argument("--store-db", type=Path, default=None, help="Persist evidence and identity candidates in a SQLite store.")
review_identity = sub.add_parser(
"review-identity",
help="Persist a review decision for a stable accountability identity candidate.",
)
review_identity.add_argument("stable_key")
review_identity.add_argument("--store-db", type=Path, required=True)
review_identity.add_argument("--decision", choices=["accept", "needs_review", "reject"], required=True)
review_identity.add_argument("--owner-actor-id", default="")
review_identity.add_argument("--fabric-id", default="")
review_identity.add_argument("--subfabric-id", default="")
review_identity.add_argument("--reviewer", default="operator")
review_identity.add_argument("--note", default="")
registry = sub.add_parser("registry", help="Feed a running Railiance Fabric registry service.")
registry_sub = registry.add_subparsers(dest="registry_command", required=True)
@@ -240,6 +278,26 @@ def build_parser() -> argparse.ArgumentParser:
accept_discovery.add_argument("--accept-review-state", action="append", default=None)
accept_discovery.add_argument("--commit", default=None)
accept_discovery.add_argument("--json", action="store_true", help="Print the raw accept response.")
export_archive = registry_sub.add_parser(
"export-reset-archive",
help="Export registry graph/discovery data before a guarded reset.",
)
export_archive.add_argument("output", type=Path)
export_archive.add_argument("--registry-url", default="http://127.0.0.1:8765")
export_archive.add_argument("--overwrite", action="store_true", help="Overwrite an existing archive file.")
export_archive.add_argument("--json", action="store_true", help="Print archive metadata.")
reset_graph = registry_sub.add_parser(
"reset-graph-data",
help="Export an archive, then reset registry graph/discovery data with an explicit confirmation token.",
)
reset_graph.add_argument("--registry-url", default="http://127.0.0.1:8765")
reset_graph.add_argument("--archive", type=Path, required=True, help="Archive JSON file to write before reset.")
reset_graph.add_argument("--overwrite-archive", action="store_true", help="Overwrite an existing archive file.")
reset_graph.add_argument("--confirm", required=True, help=f"Must equal {RESET_CONFIRMATION_TOKEN}.")
reset_graph.add_argument("--reason", required=True)
reset_graph.add_argument("--json", action="store_true", help="Print the raw reset response.")
return parser
@@ -289,6 +347,8 @@ def main(argv: list[str] | None = None) -> int:
print(graph.to_mermaid())
elif args.format == "graph-explorer":
print(json.dumps(fabric_graph_explorer_payload(graph.to_export()), indent=2, sort_keys=True))
elif args.format == "financial":
print(json.dumps(financial_export_from_legacy(graph.to_export()), indent=2, sort_keys=True))
else:
print(graph.to_json())
return 0
@@ -296,6 +356,54 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "scan":
return _scan_repo(args)
if args.command == "discover-roots":
manifest = load_accountability_root_manifest(args.manifest)
payload = collect_accountability_root_evidence(
args.manifest,
include_remote=args.include_remote,
max_items_per_root=args.max_items_per_root,
)
projection = build_identity_projection(payload, manifest)
store = AccountabilityEvidenceStore(args.store_db) if args.store_db else None
decisions = store.latest_review_decisions() if store else {}
ownership_review = build_ownership_review(projection, manifest, review_decisions=decisions)
update_delta = build_update_delta(
projection,
ownership_review,
previous_identity_projection=_load_json_file(args.previous_identity_projection)
if args.previous_identity_projection
else None,
previous_ownership_review=_load_json_file(args.previous_ownership_review)
if args.previous_ownership_review
else None,
)
if args.store_db:
store.add_evidence_run(payload, projection)
output = (
update_delta
if args.delta
else ownership_review
if args.ownership_review
else projection
if args.identity_projection
else payload
)
print(json.dumps(output, indent=2, sort_keys=True))
return 0
if args.command == "review-identity":
decision = AccountabilityEvidenceStore(args.store_db).add_review_decision(
stable_key=args.stable_key,
decision=args.decision,
reviewer=args.reviewer,
owner_actor_id=args.owner_actor_id,
fabric_id=args.fabric_id,
subfabric_id=args.subfabric_id,
note=args.note,
)
print(json.dumps(decision, indent=2, sort_keys=True))
return 0
if args.command == "registry":
if args.registry_command == "sync":
return _registry_sync(args)
@@ -311,6 +419,10 @@ def main(argv: list[str] | None = None) -> int:
return _registry_ingest_discovery(args)
if args.registry_command == "accept-discovery":
return _registry_accept_discovery(args)
if args.registry_command == "export-reset-archive":
return _registry_export_reset_archive(args)
if args.registry_command == "reset-graph-data":
return _registry_reset_graph_data(args)
parser.error(f"unknown command {args.command!r}")
return 2
@@ -1011,7 +1123,12 @@ def _scan_manifest_exit_code(summary: dict[str, Any], args: argparse.Namespace)
def _manifest_discovery_snapshot_path(base_dir: Path, slug: str, profile: str) -> Path:
return base_dir.resolve() / f"{_slugify(slug)}-{_slugify(profile)}.discovery.json"
slug_part = _slugify(slug)
raw_slug_part = slug.strip().lower()
if slug_part != raw_slug_part:
fingerprint = hashlib.sha256(slug.encode("utf-8")).hexdigest()[:8]
slug_part = f"{slug_part}-{fingerprint}"
return base_dir.resolve() / f"{slug_part}-{_slugify(profile)}.discovery.json"
def _candidate_counts(snapshot: dict[str, Any]) -> dict[str, int]:
@@ -1285,6 +1402,57 @@ def _registry_accept_discovery(args: argparse.Namespace) -> int:
return 0
def _registry_export_reset_archive(args: argparse.Namespace) -> int:
try:
archive = _registry_get_checked(args.registry_url, "/exports/reset-archive")
archive_sha256 = _write_json_archive(args.output, archive, overwrite=args.overwrite)
except (RegistryRequestError, OSError) as exc:
print(f"ERROR {exc}", file=sys.stderr)
return 1
metadata = {
"archive": str(args.output),
"archive_sha256": archive_sha256,
"counts": archive.get("counts", {}),
}
if args.json:
print(json.dumps(metadata, indent=2, sort_keys=True))
else:
print(f"wrote reset archive {args.output}")
print(f"archive sha256 {archive_sha256}")
return 0
def _registry_reset_graph_data(args: argparse.Namespace) -> int:
if args.confirm != RESET_CONFIRMATION_TOKEN:
print(f"ERROR --confirm must equal {RESET_CONFIRMATION_TOKEN}", file=sys.stderr)
return 1
try:
archive = _registry_get_checked(args.registry_url, "/exports/reset-archive")
archive_sha256 = _write_json_archive(args.archive, archive, overwrite=args.overwrite_archive)
result = _registry_post_checked(
args.registry_url,
"/admin/reset-graph-data",
{
"confirm": args.confirm,
"reason": args.reason,
"archive_path": str(args.archive),
"archive_sha256": archive_sha256,
},
)
except (RegistryRequestError, OSError) as exc:
print(f"ERROR {exc}", file=sys.stderr)
return 1
if args.json:
print(json.dumps(result, indent=2, sort_keys=True))
else:
print(f"wrote reset archive {args.archive}")
print(f"archive sha256 {archive_sha256}")
print(f"reset event {result['id']} recorded")
print(f"dropped {json.dumps(result.get('dropped_counts', {}), sort_keys=True)}")
print(f"preserved {result.get('repositories_preserved', 0)} repository registration(s)")
return 0
def _scan_repo(args: argparse.Namespace) -> int:
snapshot = scan_repo(
ScanOptions(
@@ -1369,6 +1537,15 @@ class RegistryRequestError(Exception):
self.status_code = status_code
def _write_json_archive(path: Path, archive: dict[str, object], *, overwrite: bool) -> str:
if path.exists() and not overwrite:
raise OSError(f"archive already exists: {path} (use --overwrite or --overwrite-archive)")
path.parent.mkdir(parents=True, exist_ok=True)
data = json.dumps(archive, indent=2, sort_keys=True).encode("utf-8")
path.write_bytes(data)
return hashlib.sha256(data).hexdigest()
def _registry_post(registry_url: str, path: str, payload: dict[str, object]) -> dict[str, object]:
try:
return _registry_post_checked(registry_url, path, payload)
@@ -1493,6 +1670,13 @@ def _load_graph_or_exit(paths: list[Path]) -> FabricGraph:
return graph
def _load_json_file(path: Path) -> dict[str, Any]:
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise ValueError(f"JSON file must contain an object: {path}")
return payload
def _print_providers(graph: FabricGraph, capability: str) -> None:
providers = graph.providers(capability)
if not providers:

View File

@@ -6,6 +6,7 @@ from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Protocol
from .canon import edge_canon_mapping, evidence_state_for, node_canon_mapping
from .discovery import (
attribute_stable_key,
discovery_stable_key,
@@ -183,12 +184,22 @@ class LocalFabricRegistryConnector:
}
repo_key = _repository_key(context.snapshot, context.repo_slug)
entry_key = discovery_stable_key(context.repo_slug, "FabricRegistryEntry", context.repo_slug)
node_mapping = node_canon_mapping("FabricRegistryEntry")
node = {
"stable_key": entry_key,
"kind": "FabricRegistryEntry",
"label": str(match.get("name") or context.repo_slug),
"repo": context.repo_slug,
"domain": str(match.get("domain") or ""),
"canon_category": node_mapping.category,
"canon_anchor": node_mapping.canon_anchor,
"mapping_fit": node_mapping.fit,
"evidence_state": evidence_state_for(
origin="registry",
source_kind="fabric_registry",
review_state="candidate",
confidence=0.9,
),
"aliases": _unique_strings([context.repo_slug, match.get("name")]),
"attributes": {
"registry_slug": context.repo_slug,
@@ -208,9 +219,20 @@ class LocalFabricRegistryConnector:
"provenance": [provenance],
"source_anchors": [anchor],
}
edge_mapping = edge_canon_mapping("cataloged_as")
edge = {
"stable_key": relationship_stable_key(repo_key, "cataloged_as", entry_key, evidence_scope=scope["id"]),
"edge_type": "cataloged_as",
"canonical_type": edge_mapping.canonical_type,
"canon_anchor": edge_mapping.canon_anchor,
"mapping_fit": edge_mapping.fit,
"display_only": edge_mapping.display_only,
"evidence_state": evidence_state_for(
origin="registry",
source_kind="fabric_registry",
review_state="candidate",
confidence=0.9,
),
"source_key": repo_key,
"target_key": entry_key,
"origin": "registry",

View File

@@ -0,0 +1,107 @@
from __future__ import annotations
from typing import Any
OVERLAY_FIELDS = (
"deployment_environment",
"deployment_scenario",
"routing_authority",
"access_zone",
"policy_authority",
"exposure_class",
)
ROUTE_EVIDENCE_FIELDS = (
"host",
"hostname",
"port",
"protocol",
"route",
"route_url",
"path",
"scheme",
)
_ALIASES = {
"deployment_environment": ("deployment_environment", "environment"),
"deployment_scenario": ("deployment_scenario", "deployment_scenario_id", "scenario"),
"routing_authority": ("routing_authority",),
"access_zone": ("access_zone",),
"policy_authority": ("policy_authority",),
"exposure_class": ("exposure_class", "exposure"),
}
def normalize_deployment_overlay(*sources: object) -> dict[str, Any]:
"""Collect deployment-zone overlay fields without changing fabric membership."""
overlay: dict[str, Any] = {}
route_evidence: dict[str, Any] = {}
for source in sources:
if not isinstance(source, dict):
continue
nested = source.get("deployment_overlay")
if isinstance(nested, dict):
_merge_overlay_source(overlay, route_evidence, nested)
_merge_overlay_source(overlay, route_evidence, source)
if route_evidence:
overlay["route_evidence"] = route_evidence
return {key: value for key, value in overlay.items() if _has_value(value)}
def graph_explorer_overlay_fields(overlay: dict[str, Any]) -> dict[str, Any]:
route = overlay.get("route_evidence") if isinstance(overlay.get("route_evidence"), dict) else {}
return {
"deploymentOverlay": overlay,
"deploymentEnvironment": str(overlay.get("deployment_environment") or ""),
"deploymentScenario": str(overlay.get("deployment_scenario") or ""),
"routingAuthority": str(overlay.get("routing_authority") or ""),
"accessZone": str(overlay.get("access_zone") or ""),
"policyAuthority": str(overlay.get("policy_authority") or ""),
"exposureClass": str(overlay.get("exposure_class") or ""),
"routeHost": str(route.get("host") or ""),
"routeHostname": str(route.get("hostname") or ""),
"routePort": route.get("port") if route.get("port") not in (None, "") else "",
"routeProtocol": str(route.get("protocol") or ""),
"route": str(route.get("route") or route.get("route_url") or ""),
}
def _merge_overlay_source(overlay: dict[str, Any], route_evidence: dict[str, Any], source: dict[str, Any]) -> None:
for field in OVERLAY_FIELDS:
if field in overlay:
continue
value = _first_value(source, _ALIASES[field])
if _has_value(value):
overlay[field] = value
nested_route = source.get("route_evidence")
if isinstance(nested_route, dict):
for field in ROUTE_EVIDENCE_FIELDS:
value = nested_route.get(field)
if _has_value(value) and field not in route_evidence:
route_evidence[field] = value
for field in ROUTE_EVIDENCE_FIELDS:
value = source.get(field)
if _has_value(value) and field not in route_evidence:
route_evidence[field] = value
def _first_value(source: dict[str, Any], keys: tuple[str, ...]) -> Any:
for key in keys:
value = source.get(key)
if _has_value(value):
return value
return ""
def _has_value(value: Any) -> bool:
if isinstance(value, dict):
return any(_has_value(item) for item in value.values())
if isinstance(value, list):
return any(_has_value(item) for item in value)
return value not in (None, "")

View File

@@ -0,0 +1,429 @@
from __future__ import annotations
import json
from typing import Any
from .deployment_overlay import normalize_deployment_overlay
FINANCIAL_API_VERSION = "railiance.fabric/v1alpha2"
FINANCIAL_SCHEMA_VERSION = "financial-fabric-v1"
ACTOR_ROLES = {"king", "lord", "tenant", "operator", "steward"}
OWNERSHIP_RESOLUTIONS = {"explicit", "inherited", "unresolved", "ambiguous"}
EVIDENCE_STATES = {"observed", "declared", "inferred", "proposed", "gap"}
REVIEW_STATES = {"accepted", "candidate", "needs_review", "rejected"}
RELATIONSHIP_CATEGORIES = {
"containment",
"ownership",
"technical",
"utility",
"accounting",
"evidence",
}
FABRIC_KINDS = {"Fabric", "Subfabric"}
ENVIRONMENT_LABELS = {"local", "dev", "test", "staging", "prod", "lab", "all"}
def is_financial_graph_export(graph: dict[str, Any]) -> bool:
return (
graph.get("apiVersion") == FINANCIAL_API_VERSION
or graph.get("schema_version") == FINANCIAL_SCHEMA_VERSION
)
def materialize_financial_graph_export(graph: dict[str, Any]) -> dict[str, Any]:
result = json.loads(json.dumps(graph))
if not is_financial_graph_export(result):
return result
result["apiVersion"] = FINANCIAL_API_VERSION
result.setdefault("kind", "FabricGraphExport")
result.setdefault("schema_version", FINANCIAL_SCHEMA_VERSION)
result.setdefault("compatibility", {})
result.setdefault("actors", [])
result.setdefault("fabrics", [])
result.setdefault("nodes", [])
result.setdefault("edges", [])
result.setdefault("unresolved", [])
for node in _items(result, "nodes"):
_materialize_node(node)
for edge in _items(result, "edges"):
_materialize_edge(edge)
return result
def financial_graph_errors(graph: dict[str, Any]) -> list[str]:
if not is_financial_graph_export(graph):
return []
errors: list[str] = []
if graph.get("apiVersion") != FINANCIAL_API_VERSION:
errors.append(f"apiVersion must be {FINANCIAL_API_VERSION!r}")
if graph.get("kind") != "FabricGraphExport":
errors.append("kind must be 'FabricGraphExport'")
if graph.get("schema_version") != FINANCIAL_SCHEMA_VERSION:
errors.append(f"schema_version must be {FINANCIAL_SCHEMA_VERSION!r}")
netkingdom = graph.get("netkingdom")
if not isinstance(netkingdom, dict):
errors.append("netkingdom must be an object")
netkingdom = {}
netkingdom_id = _required_text(errors, "netkingdom.id", netkingdom, "id")
king_actor_id = _required_text(errors, "netkingdom.king_actor_id", netkingdom, "king_actor_id")
actors = _indexed_items(errors, graph, "actors")
actor_roles: dict[str, str] = {}
for index, actor in actors:
actor_id = _required_text(errors, f"actors[{index}].id", actor, "id")
role = _required_text(errors, f"actors[{index}].role", actor, "role")
if role and role not in ACTOR_ROLES:
errors.append(f"actors[{index}].role {role!r} is not valid")
if actor_id:
if actor_id in actor_roles:
errors.append(f"actors[{index}].id {actor_id!r} is duplicated")
actor_roles[actor_id] = role
if king_actor_id and actor_roles.get(king_actor_id) != "king":
errors.append("netkingdom.king_actor_id must reference an actor with role 'king'")
fabrics = _indexed_items(errors, graph, "fabrics")
fabric_kinds: dict[str, str] = {}
for index, fabric in fabrics:
fabric_id = _required_text(errors, f"fabrics[{index}].id", fabric, "id")
kind = _required_text(errors, f"fabrics[{index}].kind", fabric, "kind")
if kind and kind not in FABRIC_KINDS:
errors.append(f"fabrics[{index}].kind {kind!r} is not valid")
if fabric_id:
if fabric_id in ENVIRONMENT_LABELS:
errors.append(f"fabrics[{index}].id must not be an environment label")
if fabric_id in fabric_kinds:
errors.append(f"fabrics[{index}].id {fabric_id!r} is duplicated")
fabric_kinds[fabric_id] = kind
if _text(fabric.get("netkingdom_id")) != netkingdom_id:
errors.append(f"fabrics[{index}].netkingdom_id must match netkingdom.id")
if kind == "Fabric":
lord_actor_id = _required_text(errors, f"fabrics[{index}].lord_actor_id", fabric, "lord_actor_id")
if lord_actor_id and actor_roles.get(lord_actor_id) not in {"lord", "king"}:
errors.append(f"fabrics[{index}].lord_actor_id must reference a lord or king actor")
if kind == "Subfabric":
parent_id = _required_text(errors, f"fabrics[{index}].parent_fabric_id", fabric, "parent_fabric_id")
tenant_actor_id = _required_text(errors, f"fabrics[{index}].tenant_actor_id", fabric, "tenant_actor_id")
if parent_id and parent_id not in fabric_kinds:
errors.append(f"fabrics[{index}].parent_fabric_id references unknown fabric {parent_id!r}")
if tenant_actor_id and actor_roles.get(tenant_actor_id) != "tenant":
errors.append(f"fabrics[{index}].tenant_actor_id must reference a tenant actor")
node_ids = set()
nodes = _indexed_items(errors, graph, "nodes")
for index, node in nodes:
node_id = _required_text(errors, f"nodes[{index}].id", node, "id")
_required_text(errors, f"nodes[{index}].kind", node, "kind")
_required_text(errors, f"nodes[{index}].name", node, "name")
if node_id:
if node_id in node_ids:
errors.append(f"nodes[{index}].id {node_id!r} is duplicated")
node_ids.add(node_id)
review_state = _validate_evidence(errors, f"nodes[{index}]", node)
_validate_containment(errors, f"nodes[{index}]", node, netkingdom_id, fabric_kinds, accepted=review_state == "accepted")
_validate_ownership(errors, f"nodes[{index}]", node, actor_roles, accepted=review_state == "accepted")
_validate_optional_object(errors, f"nodes[{index}].accounting", node, "accounting")
_validate_overlay(errors, f"nodes[{index}].deployment_overlay", node)
edges = _indexed_items(errors, graph, "edges")
for index, edge in edges:
path = f"edges[{index}]"
source = _required_text(errors, f"{path}.from", edge, "from")
target = _required_text(errors, f"{path}.to", edge, "to")
edge_type = _required_text(errors, f"{path}.type", edge, "type")
if source and source not in node_ids:
errors.append(f"{path}.from references unknown node {source!r}")
if target and target not in node_ids:
errors.append(f"{path}.to references unknown node {target!r}")
category = _required_text(errors, f"{path}.relationship_category", edge, "relationship_category")
if category and category not in RELATIONSHIP_CATEGORIES:
errors.append(f"{path}.relationship_category {category!r} is not valid")
_validate_evidence(errors, path, edge)
_validate_optional_object(errors, f"{path}.accounting", edge, "accounting")
_validate_overlay(errors, f"{path}.deployment_overlay", edge)
if edge_type == "provides_utility_to" and category != "utility":
errors.append(f"{path}.relationship_category must be 'utility' for provides_utility_to edges")
if category == "utility":
_validate_utility_edge(errors, path, edge, actor_roles, fabric_kinds)
for index, item in _indexed_items(errors, graph, "unresolved"):
_required_text(errors, f"unresolved[{index}].target_id", item, "target_id")
_required_text(errors, f"unresolved[{index}].kind", item, "kind")
_required_text(errors, f"unresolved[{index}].message", item, "message")
return errors
def merge_financial_graph_exports(graphs: list[dict[str, Any]], *, generated_at: str) -> dict[str, Any]:
merged: dict[str, Any] = {
"apiVersion": FINANCIAL_API_VERSION,
"kind": "FabricGraphExport",
"schema_version": FINANCIAL_SCHEMA_VERSION,
"generated_at": generated_at,
"source": {"producer": "railiance-fabric", "registry": "registry", "commit": ""},
"compatibility": {"legacy_v1alpha1_supported": True, "breaking_reset": False},
"actors": [],
"fabrics": [],
"nodes": [],
"edges": [],
"unresolved": [],
}
actors: dict[str, dict[str, Any]] = {}
fabrics: dict[str, dict[str, Any]] = {}
nodes: dict[str, dict[str, Any]] = {}
edges: dict[str, dict[str, Any]] = {}
unresolved: dict[str, dict[str, Any]] = {}
for graph in graphs:
graph = materialize_financial_graph_export(graph)
if "netkingdom" in graph and "netkingdom" not in merged:
merged["netkingdom"] = graph["netkingdom"]
for actor in _items(graph, "actors"):
actor_id = str(actor.get("id") or "")
if actor_id:
actors[actor_id] = actor
for fabric in _items(graph, "fabrics"):
fabric_id = str(fabric.get("id") or "")
if fabric_id:
fabrics[fabric_id] = fabric
for node in _items(graph, "nodes"):
node_id = str(node.get("id") or "")
if node_id:
nodes[node_id] = node
for edge in _items(graph, "edges"):
edges[_edge_identity(edge)] = edge
for item in _items(graph, "unresolved"):
target = str(item.get("target_id") or "")
kind = str(item.get("kind") or "")
if target or kind:
unresolved[f"{target}:{kind}"] = item
merged["actors"] = [actors[key] for key in sorted(actors)]
merged["fabrics"] = [fabrics[key] for key in sorted(fabrics)]
merged["nodes"] = [nodes[key] for key in sorted(nodes)]
merged["edges"] = [edges[key] for key in sorted(edges)]
merged["unresolved"] = [unresolved[key] for key in sorted(unresolved)]
return materialize_financial_graph_export(merged)
def _materialize_node(node: dict[str, Any]) -> None:
attrs = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
for key in ("containment", "ownership", "accounting"):
if key not in node and isinstance(attrs.get(key), dict):
node[key] = attrs[key]
overlay = normalize_deployment_overlay(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
attrs,
node.get("containment") if isinstance(node.get("containment"), dict) else {},
)
if overlay:
node["deployment_overlay"] = overlay
node.setdefault("evidence", _legacy_evidence(node, attrs))
def _materialize_edge(edge: dict[str, Any]) -> None:
attrs = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
if "accounting" not in edge and isinstance(attrs.get("accounting"), dict):
edge["accounting"] = attrs["accounting"]
overlay = normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
attrs,
)
if overlay:
edge["deployment_overlay"] = overlay
edge.setdefault("relationship_category", _relationship_category(edge))
edge.setdefault("evidence", _legacy_evidence(edge, attrs))
if edge.get("relationship_category") == "utility":
provider = edge.get("provider") if isinstance(edge.get("provider"), dict) else {}
consumer = edge.get("consumer") if isinstance(edge.get("consumer"), dict) else {}
boundary = edge.setdefault("boundary", {})
if isinstance(boundary, dict) and provider and consumer:
boundary.setdefault("crosses_fabric_boundary", provider.get("fabric_id") != consumer.get("fabric_id"))
boundary.setdefault("crosses_subfabric_boundary", provider.get("subfabric_id") != consumer.get("subfabric_id"))
def _legacy_evidence(item: dict[str, Any], attrs: dict[str, Any]) -> dict[str, Any]:
confidence = attrs.get("discovery_confidence")
evidence: dict[str, Any] = {
"state": item.get("evidence_state") or "declared",
"review_state": attrs.get("discovery_review_state") or "accepted",
"refs": attrs.get("discovery_source_anchors") if isinstance(attrs.get("discovery_source_anchors"), list) else [],
}
if isinstance(confidence, (int, float)):
evidence["confidence"] = float(confidence)
return evidence
def _relationship_category(edge: dict[str, Any]) -> str:
edge_type = str(edge.get("type") or "")
if edge_type in {"contains", "part_of"}:
return "containment"
if edge_type in {"owned_by", "operated_by"}:
return "ownership"
if edge_type == "provides_utility_to":
return "utility"
if edge_type in {"attributed_to_cost_center", "attributed_to_profit_center"}:
return "accounting"
if edge_type == "evidenced_by":
return "evidence"
return "technical"
def _validate_evidence(errors: list[str], path: str, item: dict[str, Any]) -> str:
evidence = item.get("evidence")
if not isinstance(evidence, dict):
errors.append(f"{path}.evidence must be an object")
return ""
state = _required_text(errors, f"{path}.evidence.state", evidence, "state")
review_state = _required_text(errors, f"{path}.evidence.review_state", evidence, "review_state")
if state and state not in EVIDENCE_STATES:
errors.append(f"{path}.evidence.state {state!r} is not valid")
if review_state and review_state not in REVIEW_STATES:
errors.append(f"{path}.evidence.review_state {review_state!r} is not valid")
return review_state
def _validate_containment(
errors: list[str],
path: str,
node: dict[str, Any],
netkingdom_id: str,
fabric_kinds: dict[str, str],
*,
accepted: bool,
) -> None:
containment = node.get("containment")
if not isinstance(containment, dict):
if accepted:
errors.append(f"{path}.containment must be an object for accepted nodes")
return
if accepted:
if _text(containment.get("netkingdom_id")) != netkingdom_id:
errors.append(f"{path}.containment.netkingdom_id must match netkingdom.id")
fabric_id = _required_text(errors, f"{path}.containment.fabric_id", containment, "fabric_id")
if fabric_id:
if fabric_id in ENVIRONMENT_LABELS:
errors.append(f"{path}.containment.fabric_id must not be an environment label")
if fabric_id not in fabric_kinds:
errors.append(f"{path}.containment.fabric_id references unknown fabric {fabric_id!r}")
subfabric_id = _text(containment.get("subfabric_id"))
if subfabric_id:
if fabric_kinds.get(subfabric_id) != "Subfabric":
errors.append(f"{path}.containment.subfabric_id references unknown subfabric {subfabric_id!r}")
environment = _text(containment.get("environment"))
if environment and environment in fabric_kinds:
errors.append(f"{path}.containment.environment must not reference a fabric id")
def _validate_ownership(
errors: list[str],
path: str,
node: dict[str, Any],
actor_roles: dict[str, str],
*,
accepted: bool,
) -> None:
ownership = node.get("ownership")
if not isinstance(ownership, dict):
if accepted:
errors.append(f"{path}.ownership must be an object for accepted nodes")
return
owner_actor_id = _required_text(errors, f"{path}.ownership.owner_actor_id", ownership, "owner_actor_id")
owner_role = _required_text(errors, f"{path}.ownership.owner_role", ownership, "owner_role")
resolution = _required_text(errors, f"{path}.ownership.resolution", ownership, "resolution")
if owner_role and owner_role not in ACTOR_ROLES:
errors.append(f"{path}.ownership.owner_role {owner_role!r} is not valid")
if resolution and resolution not in OWNERSHIP_RESOLUTIONS:
errors.append(f"{path}.ownership.resolution {resolution!r} is not valid")
if owner_actor_id and owner_actor_id not in actor_roles:
errors.append(f"{path}.ownership.owner_actor_id references unknown actor {owner_actor_id!r}")
if owner_actor_id and owner_role and actor_roles.get(owner_actor_id) not in {owner_role, ""}:
errors.append(f"{path}.ownership.owner_role does not match referenced actor role")
if accepted and resolution in {"unresolved", "ambiguous"}:
errors.append(f"{path}.ownership.resolution must be explicit or inherited for accepted nodes")
def _validate_utility_edge(
errors: list[str],
path: str,
edge: dict[str, Any],
actor_roles: dict[str, str],
fabric_kinds: dict[str, str],
) -> None:
provider = _required_object(errors, f"{path}.provider", edge, "provider")
consumer = _required_object(errors, f"{path}.consumer", edge, "consumer")
boundary = _required_object(errors, f"{path}.boundary", edge, "boundary")
utility = _required_object(errors, f"{path}.utility", edge, "utility")
for side_name, side in (("provider", provider), ("consumer", consumer)):
actor_id = _required_text(errors, f"{path}.{side_name}.owner_actor_id", side, "owner_actor_id")
fabric_id = _required_text(errors, f"{path}.{side_name}.fabric_id", side, "fabric_id")
if actor_id and actor_id not in actor_roles:
errors.append(f"{path}.{side_name}.owner_actor_id references unknown actor {actor_id!r}")
if fabric_id and fabric_id not in fabric_kinds:
errors.append(f"{path}.{side_name}.fabric_id references unknown fabric {fabric_id!r}")
subfabric_id = _text(side.get("subfabric_id"))
if subfabric_id and fabric_kinds.get(subfabric_id) != "Subfabric":
errors.append(f"{path}.{side_name}.subfabric_id references unknown subfabric {subfabric_id!r}")
_required_text(errors, f"{path}.utility.utility_type", utility, "utility_type")
for key in ("crosses_fabric_boundary", "crosses_subfabric_boundary"):
if not isinstance(boundary.get(key), bool):
errors.append(f"{path}.boundary.{key} must be a boolean")
def _validate_optional_object(errors: list[str], path: str, item: dict[str, Any], key: str) -> None:
if key in item and not isinstance(item[key], dict):
errors.append(f"{path} must be an object")
def _validate_overlay(errors: list[str], path: str, item: dict[str, Any]) -> None:
overlay = item.get("deployment_overlay")
if overlay is None:
return
if not isinstance(overlay, dict):
errors.append(f"{path} must be an object")
return
route = overlay.get("route_evidence")
if route is not None and not isinstance(route, dict):
errors.append(f"{path}.route_evidence must be an object")
def _required_object(errors: list[str], path: str, item: dict[str, Any], key: str) -> dict[str, Any]:
value = item.get(key)
if not isinstance(value, dict):
errors.append(f"{path} must be an object")
return {}
return value
def _required_text(errors: list[str], path: str, item: dict[str, Any], key: str) -> str:
value = _text(item.get(key))
if not value:
errors.append(f"{path} is required")
return value
def _text(value: Any) -> str:
return value.strip() if isinstance(value, str) else ""
def _indexed_items(errors: list[str], graph: dict[str, Any], key: str) -> list[tuple[int, dict[str, Any]]]:
value = graph.get(key, [])
if not isinstance(value, list):
errors.append(f"{key} must be an array")
return []
return [(index, item) for index, item in enumerate(value) if isinstance(item, dict)]
def _items(graph: dict[str, Any], key: str) -> list[dict[str, Any]]:
value = graph.get(key, [])
return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
def _edge_identity(edge: dict[str, Any]) -> str:
if edge.get("id"):
return str(edge["id"])
return f"{edge.get('from', '')}:{edge.get('type', '')}:{edge.get('to', '')}"

View File

@@ -0,0 +1,155 @@
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from .deployment_overlay import normalize_deployment_overlay
from .financial import (
FINANCIAL_API_VERSION,
FINANCIAL_SCHEMA_VERSION,
financial_graph_errors,
materialize_financial_graph_export,
)
from .loader import load_yaml, repo_root
DEFAULT_BASELINE_PATH = repo_root() / "fabric" / "financial" / "railiance-netkingdom.yaml"
def load_financial_baseline(path: Path | None = None) -> dict[str, Any]:
baseline_path = path or DEFAULT_BASELINE_PATH
data = load_yaml(baseline_path)
if not isinstance(data, dict):
raise ValueError(f"financial baseline must be a mapping: {baseline_path}")
if data.get("kind") != "FinancialFabricBaseline":
raise ValueError(f"financial baseline kind must be FinancialFabricBaseline: {baseline_path}")
spec = data.get("spec")
if not isinstance(spec, dict):
raise ValueError(f"financial baseline spec must be a mapping: {baseline_path}")
return spec
def financial_export_from_legacy(
legacy_graph: dict[str, Any],
baseline: dict[str, Any] | None = None,
) -> dict[str, Any]:
baseline = json.loads(json.dumps(baseline or load_financial_baseline()))
defaults = baseline.get("defaults") if isinstance(baseline.get("defaults"), dict) else {}
containment_default = defaults.get("containment") if isinstance(defaults.get("containment"), dict) else {}
ownership_default = defaults.get("ownership") if isinstance(defaults.get("ownership"), dict) else {}
accounting_default = defaults.get("accounting") if isinstance(defaults.get("accounting"), dict) else {}
graph = {
"apiVersion": FINANCIAL_API_VERSION,
"kind": "FabricGraphExport",
"schema_version": FINANCIAL_SCHEMA_VERSION,
"source": {
**_object(legacy_graph.get("source")),
"producer": "railiance-fabric",
"generation_reason": "baseline_projection",
},
"compatibility": {
"legacy_v1alpha1_supported": True,
"breaking_reset": False,
"projected_from_apiVersion": legacy_graph.get("apiVersion"),
},
"netkingdom": baseline.get("netkingdom", {}),
"actors": baseline.get("actors", []),
"fabrics": baseline.get("fabrics", []),
"nodes": [
_financial_node_from_legacy(node, containment_default, ownership_default, accounting_default)
for node in legacy_graph.get("nodes", [])
if isinstance(node, dict)
],
"edges": [
_financial_edge_from_legacy(edge)
for edge in legacy_graph.get("edges", [])
if isinstance(edge, dict)
],
"unresolved": [],
}
graph["generated_at"] = legacy_graph.get("generated_at") or _utc_now()
materialized = materialize_financial_graph_export(graph)
errors = financial_graph_errors(materialized)
if errors:
raise ValueError(f"invalid financial baseline projection: {errors[0]}")
return materialized
def _financial_node_from_legacy(
node: dict[str, Any],
containment_default: dict[str, Any],
ownership_default: dict[str, Any],
accounting_default: dict[str, Any],
) -> dict[str, Any]:
result: dict[str, Any] = {
"id": node.get("id", ""),
"kind": _node_kind(node),
"name": node.get("name") or node.get("id", ""),
"repo": node.get("repo", ""),
"domain": node.get("domain", ""),
"lifecycle": node.get("lifecycle", ""),
"containment": json.loads(json.dumps(containment_default)),
"ownership": json.loads(json.dumps(ownership_default)),
"evidence_state": node.get("evidence_state", "declared"),
"attributes": _object(node.get("attributes")),
}
accounting = _object(result["attributes"].get("accounting")) or accounting_default
if _has_value(accounting):
result["accounting"] = json.loads(json.dumps(accounting))
overlay = normalize_deployment_overlay(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
result["attributes"],
result["containment"],
)
if _has_value(overlay):
result["deployment_overlay"] = overlay
for key in ("canon_category", "canon_anchor", "mapping_fit"):
if node.get(key):
result[key] = node[key]
return result
def _financial_edge_from_legacy(edge: dict[str, Any]) -> dict[str, Any]:
result = {
"from": edge.get("from", ""),
"to": edge.get("to", ""),
"type": edge.get("type", ""),
"attributes": _object(edge.get("attributes")),
}
for key in ("canonical_type", "canon_anchor", "mapping_fit", "display_only", "evidence_state"):
if key in edge:
result[key] = edge[key]
overlay = normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
result["attributes"],
)
if _has_value(overlay):
result["deployment_overlay"] = overlay
return result
def _node_kind(node: dict[str, Any]) -> str:
kind = str(node.get("kind") or "")
if kind == "ServiceDeclaration":
return "Service"
if kind == "InterfaceDeclaration":
return "UtilityInterface"
return kind or "DiscoveredEntity"
def _object(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _has_value(value: Any) -> bool:
if isinstance(value, dict):
return any(_has_value(item) for item in value.values())
if isinstance(value, list):
return any(_has_value(item) for item in value)
return value not in (None, "")
def _utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")

View File

@@ -8,6 +8,7 @@ from pathlib import Path
from typing import Any
from .loader import load_declarations
from .canon import edge_canon_mapping, node_canon_mapping
from .model import Declaration
@@ -186,6 +187,7 @@ class FabricGraph:
edges: list[dict[str, str]] = []
for declaration in sorted(self.declarations, key=lambda item: (item.kind, item.id)):
canon_mapping = node_canon_mapping(declaration.kind)
nodes.append(
{
"id": declaration.id,
@@ -194,35 +196,39 @@ class FabricGraph:
"repo": declaration.metadata.get("repo", ""),
"domain": declaration.metadata.get("domain", ""),
"lifecycle": declaration.spec.get("lifecycle", ""),
"canon_category": canon_mapping.category,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"evidence_state": "declared",
"attributes": _export_attributes(declaration),
}
)
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"})
edges.append(_export_edge(service.id, capability_id, "provides"))
for interface_id in service.spec.get("exposes_interfaces", []):
edges.append({"from": service.id, "to": interface_id, "type": "exposes"})
edges.append(_export_edge(service.id, interface_id, "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"})
edges.append(_export_edge(capability.id, interface_id, "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"})
edges.append(_export_edge(consumer, dependency.id, "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', '')}",
}
_export_edge(
dependency.id,
str(binding.spec.get("provider_capability_id", "")),
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"})
edges.append(_export_edge(dependency.id, interface_id, "uses_interface"))
return {
"apiVersion": "railiance.fabric/v1alpha1",
@@ -265,6 +271,20 @@ def _escape_mermaid(value: str) -> str:
return value.replace('"', '\\"')
def _export_edge(source: str, target: str, edge_type: str) -> dict[str, Any]:
canon_mapping = edge_canon_mapping(edge_type)
return {
"from": source,
"to": target,
"type": edge_type,
"canonical_type": canon_mapping.canonical_type,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"display_only": canon_mapping.display_only,
"evidence_state": "declared",
}
def _export_attributes(declaration: Declaration) -> dict[str, Any]:
spec = declaration.spec
base = _base_export_attributes(declaration)
@@ -320,9 +340,12 @@ def _export_attributes(declaration: Declaration) -> dict[str, Any]:
def _base_export_attributes(declaration: Declaration) -> dict[str, Any]:
source_links = declaration.metadata.get("source_links", [])
return {
attributes = {
"owner": declaration.metadata.get("owner", ""),
"description": declaration.spec.get("description", ""),
"source_path": str(declaration.path),
"source_links": source_links if isinstance(source_links, list) else [],
}
if isinstance(declaration.spec.get("deployment_overlay"), dict):
attributes["deployment_overlay"] = declaration.spec["deployment_overlay"]
return attributes

View File

@@ -1,15 +1,23 @@
from __future__ import annotations
import ipaddress
from datetime import datetime, timezone
from re import sub
from typing import Any
from urllib.parse import urlparse
from .canon import edge_canon_mapping
from .deployment_overlay import graph_explorer_overlay_fields, normalize_deployment_overlay
DISPLAY_STATES = ("show", "blur", "hide", "highlight", "remove")
LAYER_ORDER = (
"repository",
"server",
"runtime_service",
"application",
"network",
"domain",
"deployment",
"service",
"capability",
@@ -22,6 +30,10 @@ LAYER_ORDER = (
_KIND_LAYER = {
"Repository": "repository",
"Server": "server",
"RuntimeService": "runtime_service",
"ApplicationEndpoint": "application",
"NetworkPort": "network",
"DomainName": "domain",
"Deployment": "deployment",
"ServiceDeclaration": "service",
"CapabilityDeclaration": "capability",
@@ -34,6 +46,10 @@ _KIND_LAYER = {
_LAYER_COLORS = {
"repository": "#475569",
"server": "#334155",
"runtime_service": "#0369a1",
"application": "#0f766e",
"network": "#64748b",
"domain": "#9333ea",
"deployment": "#16a34a",
"service": "#0f766e",
"capability": "#2563eb",
@@ -44,13 +60,30 @@ _LAYER_COLORS = {
}
_EDGE_STRENGTH = {
"provides": "strong",
"built_from": "medium",
"depends_on": "medium",
"deploys": "strong",
"evidenced_by": "medium",
"exposes": "strong",
"flows_to": "medium",
"governed_by": "medium",
"implements": "medium",
"observed_by": "medium",
"part_of": "weak",
"reads_or_writes": "medium",
"provides": "strong",
"available_via": "medium",
"consumes": "medium",
"uses_interface": "medium",
"declares": "weak",
"deployed_as": "medium",
"exposes_port": "strong",
"listens_on": "strong",
"names_endpoint": "medium",
"opens_port": "strong",
"routes_to_port": "medium",
"routes_to_service": "strong",
"resolves_to": "medium",
"runs_on": "strong",
"owns_deployment": "weak",
}
@@ -102,6 +135,12 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"layer",
"kind",
"environment",
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"serverHost",
"lifecycle",
"unresolved",
@@ -115,10 +154,23 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"repo",
"domain",
"environment",
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"serverHost",
"routeHost",
"routeHostname",
"routePort",
"routeProtocol",
"kind",
"layer",
"edgeType",
"canonicalType",
"canonCategory",
"evidenceState",
],
"filter": {
"actions": list(DISPLAY_STATES),
@@ -128,11 +180,23 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
{"id": "repo", "label": "Repo", "type": "string"},
{"id": "domain", "label": "Domain", "type": "string"},
{"id": "environment", "label": "Environment", "type": "string"},
{"id": "deploymentEnvironment", "label": "Deployment Environment", "type": "string"},
{"id": "deploymentScenario", "label": "Deployment Scenario", "type": "string"},
{"id": "routingAuthority", "label": "Routing Authority", "type": "string"},
{"id": "accessZone", "label": "Access Zone", "type": "string"},
{"id": "policyAuthority", "label": "Policy Authority", "type": "string"},
{"id": "exposureClass", "label": "Exposure Class", "type": "string"},
{"id": "serverHost", "label": "Server Host", "type": "string"},
{"id": "routeHost", "label": "Route Host", "type": "string"},
{"id": "routePort", "label": "Route Port", "type": "number"},
{"id": "lifecycle", "label": "Lifecycle", "type": "string"},
{"id": "reviewState", "label": "Review State", "type": "string"},
{"id": "unresolved", "label": "Unresolved", "type": "boolean"},
{"id": "edgeType", "label": "Edge Type", "type": "string"},
{"id": "canonicalType", "label": "Canonical Edge", "type": "string"},
{"id": "canonCategory", "label": "Canon Category", "type": "string"},
{"id": "evidenceState", "label": "Evidence State", "type": "string"},
{"id": "displayOnly", "label": "Display Only", "type": "boolean"},
{"id": "strength", "label": "Strength", "type": "string"},
{"id": "sameLayer", "label": "Same Layer", "type": "boolean"},
{"id": "text", "label": "Text", "type": "string"},
@@ -154,6 +218,16 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
"repo",
"domain",
"lifecycle",
"deploymentEnvironment",
"deploymentScenario",
"routingAuthority",
"accessZone",
"policyAuthority",
"exposureClass",
"routeHost",
"routePort",
"canonCategory",
"evidenceState",
"unresolved",
"description",
"sourceReferences",
@@ -161,6 +235,9 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
],
"edge_fields": [
"edgeType",
"canonicalType",
"displayOnly",
"evidenceState",
"strength",
"source",
"target",
@@ -171,6 +248,31 @@ def fabric_graph_explorer_manifest(base_url: str = "") -> dict[str, Any]:
],
},
"modes": [
{
"id": "by-fabric",
"label": "Fabric",
"description": "Group and filter by fabric/accountability fields.",
},
{
"id": "by-deployment-environment",
"label": "Environment",
"description": "Group and filter by deployment environment.",
},
{
"id": "by-deployment-scenario",
"label": "Scenario",
"description": "Group and filter by deployment scenario.",
},
{
"id": "by-routing-authority",
"label": "Routing",
"description": "Group and filter by routing authority.",
},
{
"id": "by-access-zone",
"label": "Access Zone",
"description": "Group and filter by intended access zone.",
},
{
"id": "full",
"label": "Full",
@@ -238,6 +340,13 @@ def fabric_graph_explorer_payload(
elements: list[dict[str, Any]] = []
repository_index = {str(repo.get("slug", "")): repo for repo in repositories}
source_repository_node_ids = {
str(node.get("id", "")): f"repo:{str(node.get('repo', '')).strip()}"
for node in source_nodes
if str(node.get("kind", "")) == "Repository"
and str(node.get("id", ""))
and str(node.get("repo", "")).strip()
}
for slug in sorted(repo_slugs):
repo = repository_index.get(slug, {})
has_snapshot = slug in snapshot_repo_slugs or slug in source_repo_slugs
@@ -280,10 +389,17 @@ def fabric_graph_explorer_payload(
node_id = str(node.get("id", ""))
if not node_id:
continue
kind = str(node.get("kind", ""))
source_kind = str(node.get("kind", ""))
if source_kind == "Repository":
continue
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
overlay_data = _overlay_data(
node.get("deployment_overlay") if isinstance(node.get("deployment_overlay"), dict) else {},
attributes,
)
kind = _presentation_kind(source_kind, attributes)
layer = _layer_for_kind(kind)
is_unresolved = node_id in unresolved
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
review_state = str(attributes.get("discovery_review_state") or "accepted")
confidence = attributes.get("discovery_confidence")
elements.append(
@@ -299,6 +415,16 @@ def fabric_graph_explorer_payload(
"repo": str(node.get("repo", "")),
"domain": str(node.get("domain", "")),
"lifecycle": str(node.get("lifecycle", "")),
"environment": overlay_data["deploymentEnvironment"],
**overlay_data,
"canonCategory": str(
node.get("canon_category") or attributes.get("canon_category") or ""
),
"canonAnchor": str(node.get("canon_anchor") or attributes.get("canon_anchor") or ""),
"mappingFit": str(node.get("mapping_fit") or attributes.get("mapping_fit") or ""),
"evidenceState": str(
node.get("evidence_state") or attributes.get("evidence_state") or ""
),
"reviewState": review_state,
"freshnessState": "current",
"unresolved": is_unresolved,
@@ -309,7 +435,7 @@ def fabric_graph_explorer_payload(
),
"visualSize": 34 if layer == "binding" else 46 if is_unresolved else 50,
"ownership": str(attributes.get("owner") or attributes.get("discovery_origin") or "repo"),
"attributes": attributes,
"attributes": {**attributes, "source_kind": source_kind} if source_kind != kind else attributes,
"discovery": {
"stableKey": attributes.get("discovery_stable_key", ""),
"origin": attributes.get("discovery_origin", ""),
@@ -333,6 +459,7 @@ def fabric_graph_explorer_payload(
node_layers = _node_data_index(elements, "layer")
node_repos = _node_data_index(elements, "repo")
node_kinds = _node_data_index(elements, "kind")
edge_index = _append_infrastructure_elements(
source_nodes,
elements,
@@ -342,21 +469,43 @@ def fabric_graph_explorer_payload(
)
for edge in source_edges:
source = str(edge.get("from", ""))
target = str(edge.get("to", ""))
edge_type = str(edge.get("type", ""))
source = source_repository_node_ids.get(str(edge.get("from", "")), str(edge.get("from", "")))
target = source_repository_node_ids.get(str(edge.get("to", "")), str(edge.get("to", "")))
edge_type = _presentation_edge_type(str(edge.get("type", "")), source, target, node_kinds)
if not source or not target:
continue
elements.append(_edge_element(edge_index, source, target, edge_type, node_layers, node_repos))
elements.append(
_edge_element(
edge_index,
source,
target,
edge_type,
node_layers,
node_repos,
**_edge_metadata(edge, edge_type),
)
)
edge_index += 1
for slug in sorted(repo_slugs):
repo_id = f"repo:{slug}"
for node in source_nodes:
node_id = str(node.get("id", ""))
if str(node.get("kind", "")) == "Repository":
continue
if str(node.get("repo", "")) != slug or not node_id:
continue
elements.append(_edge_element(edge_index, repo_id, node_id, "declares", node_layers, node_repos))
elements.append(
_edge_element(
edge_index,
repo_id,
node_id,
"declares",
node_layers,
node_repos,
display_only=True,
)
)
edge_index += 1
visible_nodes = [element for element in elements if "source" not in element["data"]]
@@ -411,6 +560,46 @@ def _layer_for_kind(kind: str) -> str:
return _KIND_LAYER.get(kind, kind.lower() or "unknown")
def _presentation_kind(kind: str, attributes: dict[str, Any]) -> str:
if kind != "Server":
return kind
host = str(attributes.get("host") or "").strip().lower()
runtime_type = str(attributes.get("runtime_target_type") or attributes.get("server_type") or "")
if runtime_type == "kubernetes-service-dns" or host.endswith(".svc.cluster.local"):
return "RuntimeService"
if runtime_type in {"declared-endpoint", "ingress-host"} and _looks_like_domain(host):
return "ApplicationEndpoint"
return kind
def _presentation_edge_type(edge_type: str, source: str, target: str, node_kinds: dict[str, str]) -> str:
if edge_type == "resolves_to":
target_kind = node_kinds.get(target, "")
if target_kind == "ApplicationEndpoint":
return "names_endpoint"
if target_kind == "RuntimeService":
return "routes_to_service"
if edge_type == "opens_port" and node_kinds.get(source, "") in {"ApplicationEndpoint", "RuntimeService"}:
return "listens_on"
return edge_type
def _edge_metadata(edge: dict[str, Any], edge_type: str) -> dict[str, Any]:
canon_mapping = edge_canon_mapping(edge_type)
attributes = edge.get("attributes") if isinstance(edge.get("attributes"), dict) else {}
return {
"canonical_type": str(edge.get("canonical_type") or canon_mapping.canonical_type),
"canon_anchor": str(edge.get("canon_anchor") or canon_mapping.canon_anchor),
"mapping_fit": str(edge.get("mapping_fit") or canon_mapping.fit),
"display_only": bool(edge.get("display_only", canon_mapping.display_only)),
"evidence_state": str(edge.get("evidence_state") or "declared"),
"deployment_overlay": normalize_deployment_overlay(
edge.get("deployment_overlay") if isinstance(edge.get("deployment_overlay"), dict) else {},
attributes,
),
}
def _edge_strength(edge_type: str) -> str:
if edge_type.startswith("binds:"):
status = edge_type.split(":", 1)[1]
@@ -437,6 +626,26 @@ def _node_data_index(elements: list[dict[str, Any]], field: str) -> dict[str, st
return index
def _runtime_node_indexes(source_nodes: list[dict[str, Any]]) -> tuple[dict[str, str], dict[str, str]]:
servers_by_host: dict[str, str] = {}
ports_by_endpoint: dict[str, str] = {}
for node in source_nodes:
node_id = str(node.get("id") or "")
if not node_id:
continue
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
kind = _presentation_kind(str(node.get("kind") or ""), attributes)
host = _normalize_endpoint_host(str(attributes.get("host") or ""))
if kind == "Server" and host:
servers_by_host.setdefault(host, node_id)
if kind == "NetworkPort" and host:
port = _int_value(attributes.get("port"))
protocol = _normalize_protocol(str(attributes.get("protocol") or "tcp"))
if port is not None:
ports_by_endpoint.setdefault(_endpoint_key(host, port, protocol), node_id)
return servers_by_host, ports_by_endpoint
def _append_infrastructure_elements(
source_nodes: list[dict[str, Any]],
elements: list[dict[str, Any]],
@@ -446,7 +655,29 @@ def _append_infrastructure_elements(
) -> int:
edge_index = 0
endpoints_by_service = _endpoints_by_service(source_nodes)
server_ids_by_host: dict[str, str] = {}
server_ids_by_host, port_ids_by_endpoint = _runtime_node_indexes(source_nodes)
generated_edge_keys: set[tuple[str, str, str]] = set()
def append_edge(source: str, target: str, edge_type: str, *, display_only: bool = True) -> None:
nonlocal edge_index
if not source or not target:
return
key = (source, edge_type, target)
if key in generated_edge_keys:
return
generated_edge_keys.add(key)
elements.append(
_edge_element(
edge_index,
source,
target,
edge_type,
node_layers,
node_repos,
display_only=display_only,
)
)
edge_index += 1
service_nodes = sorted(
(node for node in source_nodes if node.get("kind") == "ServiceDeclaration"),
@@ -468,6 +699,12 @@ def _append_infrastructure_elements(
for endpoint in service_endpoints
if _environment_matches(environment, endpoint["environments"])
]
deployment_overlay = normalize_deployment_overlay(
attributes,
{"deployment_environment": environment},
*[endpoint["deployment_overlay"] for endpoint in matching_endpoints],
)
overlay_data = _overlay_data(deployment_overlay)
server_hosts = sorted({endpoint["host"] for endpoint in matching_endpoints})
deployment_data = {
"id": deployment_id,
@@ -481,6 +718,7 @@ def _append_infrastructure_elements(
"domain": str(service.get("domain") or ""),
"lifecycle": str(service.get("lifecycle") or ""),
"environment": environment,
**overlay_data,
"serviceId": service_id,
"serverHosts": server_hosts,
"reviewState": "accepted",
@@ -505,31 +743,40 @@ def _append_infrastructure_elements(
node_layers[deployment_id] = "deployment"
node_repos[deployment_id] = repo
elements.append(_edge_element(edge_index, service_id, deployment_id, "deployed_as", node_layers, node_repos))
edge_index += 1
append_edge(service_id, deployment_id, "deployed_as")
if repo and repo in repo_slugs:
repo_id = f"repo:{repo}"
elements.append(_edge_element(edge_index, repo_id, deployment_id, "owns_deployment", node_layers, node_repos))
edge_index += 1
append_edge(repo_id, deployment_id, "owns_deployment")
for endpoint in matching_endpoints:
server_id = server_ids_by_host.get(endpoint["host"])
if server_id is None:
server_id = _server_id(endpoint["host"])
server_ids_by_host[endpoint["host"]] = server_id
host = endpoint["host"]
port = endpoint["port"]
protocol = endpoint["protocol"]
endpoint_overlay_data = _overlay_data(
endpoint["deployment_overlay"],
{"deployment_environment": environment},
)
server_id = server_ids_by_host.get(host)
endpoint_key = _endpoint_key(host, port, protocol)
port_id = port_ids_by_endpoint.get(endpoint_key)
port_was_generated = port_id is None
if server_id is None and _looks_like_machine_address(host):
server_id = _server_id(host)
server_ids_by_host[host] = server_id
server_data = {
"id": server_id,
"stableKey": server_id,
"kind": "Server",
"layer": "server",
"label": endpoint["host"],
"name": endpoint["host"],
"label": host,
"name": host,
"description": f"Server inferred from endpoint {endpoint['url']}.",
"repo": "",
"domain": str(service.get("domain") or ""),
"lifecycle": "active",
"environment": environment,
"serverHost": endpoint["host"],
**endpoint_overlay_data,
"serverHost": host,
"reviewState": "accepted",
"freshnessState": "current",
"unresolved": False,
@@ -537,9 +784,10 @@ def _append_infrastructure_elements(
"visualSize": 48,
"ownership": "inferred",
"attributes": {
"host": endpoint["host"],
"host": host,
"source_interface_id": endpoint["interface_id"],
"endpoint_url": endpoint["url"],
"deployment_overlay": endpoint_overlay_data["deploymentOverlay"],
},
"displayState": "show",
"visibilitySource": "default",
@@ -550,8 +798,51 @@ def _append_infrastructure_elements(
elements.append({"data": server_data, "classes": "server accepted inferred"})
node_layers[server_id] = "server"
node_repos[server_id] = ""
elements.append(_edge_element(edge_index, deployment_id, server_id, "runs_on", node_layers, node_repos))
edge_index += 1
if port_id is None:
port_id = _port_id(host, port, protocol)
port_ids_by_endpoint[endpoint_key] = port_id
port_data = {
"id": port_id,
"stableKey": port_id,
"kind": "NetworkPort",
"layer": "network",
"label": f"{host}:{port}/{protocol}",
"name": f"{host}:{port}/{protocol}",
"description": f"Port inferred from endpoint {endpoint['url']}.",
"repo": "",
"domain": str(service.get("domain") or ""),
"lifecycle": "active",
"environment": environment,
**endpoint_overlay_data,
"serverHost": host,
"reviewState": "accepted",
"freshnessState": "current",
"unresolved": False,
"confidence": 0.7,
"visualSize": 42,
"ownership": "inferred",
"attributes": {
"host": host,
"port": port,
"protocol": protocol,
"source_interface_id": endpoint["interface_id"],
"endpoint_url": endpoint["url"],
"deployment_overlay": endpoint_overlay_data["deploymentOverlay"],
},
"displayState": "show",
"visibilitySource": "default",
"visibilityReason": "default",
"sourceReferences": [{"label": "Endpoint interface", "ref": endpoint["interface_id"]}],
"deepLinks": {"interface": f"/graph/nodes/{endpoint['interface_id']}"},
}
elements.append({"data": port_data, "classes": "network accepted inferred"})
node_layers[port_id] = "network"
node_repos[port_id] = ""
if server_id:
append_edge(deployment_id, server_id, "runs_on")
if port_was_generated:
append_edge(server_id, port_id, "opens_port")
append_edge(deployment_id, port_id, "exposes_port")
return edge_index
@@ -563,16 +854,29 @@ def _endpoints_by_service(source_nodes: list[dict[str, Any]]) -> dict[str, list[
attributes = node.get("attributes") if isinstance(node.get("attributes"), dict) else {}
endpoint = attributes.get("endpoint") if isinstance(attributes.get("endpoint"), dict) else {}
url = str(endpoint.get("url") or "").strip()
host = _endpoint_host(url)
parsed = _parse_endpoint_url(url)
service_id = str(attributes.get("service_id") or "")
if not service_id or not host:
if not service_id or not parsed:
continue
host, port, protocol = parsed
endpoints.setdefault(service_id, []).append(
{
"host": host,
"port": port,
"protocol": protocol,
"url": url,
"interface_id": str(node.get("id") or ""),
"environments": _environments(attributes),
"deployment_overlay": normalize_deployment_overlay(
attributes,
endpoint,
{
"host": host,
"port": port,
"protocol": protocol,
"route": url,
},
),
}
)
return endpoints
@@ -596,11 +900,70 @@ def _environment_matches(deployment_environment: str, endpoint_environments: lis
def _endpoint_host(url: str) -> str:
parsed = _parse_endpoint_url(url)
return parsed[0] if parsed else ""
def _parse_endpoint_url(url: str) -> tuple[str, int, str] | None:
if not url:
return ""
return None
parsed = urlparse(url)
host = parsed.netloc or parsed.path.split("/", 1)[0]
return host.strip().lower()
host = _normalize_endpoint_host(parsed.hostname or parsed.netloc or parsed.path.split("/", 1)[0])
try:
port = parsed.port
except ValueError:
port = None
scheme = str(parsed.scheme or "").lower()
port = port or {"http": 80, "https": 443, "postgres": 5432}.get(scheme)
if not host or port is None:
return None
return host, port, "tcp"
def _normalize_endpoint_host(host: str) -> str:
value = str(host or "").strip().lower().strip("[]")
if value in {"0.0.0.0", "::"}:
return "localhost"
return value
def _endpoint_key(host: str, port: int, protocol: str) -> str:
return f"{_normalize_endpoint_host(host)}:{port}/{_normalize_protocol(protocol)}"
def _normalize_protocol(protocol: str) -> str:
return str(protocol or "tcp").strip().lower() or "tcp"
def _int_value(value: object) -> int | None:
if isinstance(value, bool):
return None
if isinstance(value, int):
return value
text = str(value or "").strip()
if text.isdecimal():
return int(text)
return None
def _looks_like_domain(host: str) -> bool:
value = _normalize_endpoint_host(host)
if not value or value == "localhost":
return False
if all(part.isdecimal() for part in value.split(".") if part):
return False
return "." in value
def _looks_like_machine_address(host: str) -> bool:
value = _normalize_endpoint_host(host)
if value in {"localhost", "127.0.0.1"}:
return True
try:
ipaddress.ip_address(value)
except ValueError:
return False
return True
def _server_id(host: str) -> str:
@@ -608,6 +971,11 @@ def _server_id(host: str) -> str:
return f"server:{key or 'unknown'}"
def _port_id(host: str, port: int, protocol: str) -> str:
key = sub(r"[^A-Za-z0-9._:+-]+", "-", _endpoint_key(host, port, protocol)).strip("-")
return f"port:{key or 'unknown'}"
def _edge_element(
edge_index: int,
source: str,
@@ -615,15 +983,29 @@ def _edge_element(
edge_type: str,
node_layers: dict[str, str],
node_repos: dict[str, str],
*,
canonical_type: str = "",
canon_anchor: str = "",
mapping_fit: str = "",
display_only: bool = False,
evidence_state: str = "",
deployment_overlay: dict[str, Any] | None = None,
) -> dict[str, Any]:
source_layer = node_layers.get(source, "unknown")
target_layer = node_layers.get(target, "unknown")
source_repo = node_repos.get(source, "")
target_repo = node_repos.get(target, "")
same_repo = bool(source_repo and source_repo == target_repo)
canon_mapping = edge_canon_mapping(edge_type)
canonical_type = canonical_type or canon_mapping.canonical_type
canon_anchor = canon_anchor or canon_mapping.canon_anchor
mapping_fit = mapping_fit or canon_mapping.fit
display_only = display_only or canon_mapping.display_only
evidence_state = evidence_state or "declared"
strength = _edge_strength(edge_type)
layout = _layout_hints(edge_type, source_layer, target_layer, same_repo)
edge_id = f"edge:{edge_index}:{source}:{edge_type}:{target}"
overlay_data = _overlay_data(deployment_overlay or {})
return {
"data": {
"id": edge_id,
@@ -639,6 +1021,12 @@ def _edge_element(
"targetRepo": target_repo,
"edgeType": edge_type,
"dependencyType": edge_type,
"canonicalType": canonical_type,
"canonAnchor": canon_anchor,
"mappingFit": mapping_fit,
"displayOnly": display_only,
"evidenceState": evidence_state,
**overlay_data,
"strength": strength,
"edgeWidth": _edge_width(strength),
"sameLayer": source_layer == target_layer,
@@ -659,6 +1047,7 @@ def _edge_element(
edge_type.replace(":", "-"),
strength,
str(layout["affinity"]),
"display-only" if display_only else "canonical",
"same-layer" if source_layer == target_layer else "",
"same-repo" if same_repo else "",
)
@@ -667,6 +1056,10 @@ def _edge_element(
}
def _overlay_data(*sources: object) -> dict[str, Any]:
return graph_explorer_overlay_fields(normalize_deployment_overlay(*sources))
def _layout_hints(
edge_type: str,
source_layer: str,
@@ -733,6 +1126,17 @@ def _node_description(kind: str, attributes: object) -> str:
)
if kind == "BindingAssertion":
return str(attributes.get("status", ""))
if kind == "RuntimeService":
return str(attributes.get("runtime_target_type") or attributes.get("service_type") or "runtime service")
if kind == "ApplicationEndpoint":
return str(attributes.get("endpoint_url") or attributes.get("domain") or "application endpoint")
if kind == "NetworkPort":
host = str(attributes.get("host") or "")
port = str(attributes.get("port") or "")
protocol = str(attributes.get("protocol") or "")
return " ".join(part for part in (host, port, protocol) if part)
if kind == "DomainName":
return str(attributes.get("domain") or "")
return ""

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ from typing import Any, Iterable
from jsonschema import ValidationError
from .canon import edge_canon_mapping, evidence_state_for, node_canon_mapping
from .discovery import (
attribute_stable_key,
discovery_stable_key,
@@ -270,12 +271,22 @@ def project_llm_output(
rationale = str(raw_node.get("rationale") or "").strip()
if rationale:
provenance["rationale"] = rationale
canon_mapping = node_canon_mapping(kind)
candidates["nodes"].append(
{
"stable_key": stable_key,
"kind": kind,
"label": label,
"repo": repo_slug,
"canon_category": canon_mapping.category,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"evidence_state": evidence_state_for(
origin="llm",
source_kind="llm",
review_state="needs_review",
confidence=confidence,
),
"aliases": _strings(raw_node.get("aliases")) + [label],
"attributes": _json_object(raw_node.get("attributes")) if isinstance(raw_node.get("attributes"), dict) else {},
"origin": "llm",
@@ -304,10 +315,21 @@ def project_llm_output(
rationale = str(raw_edge.get("rationale") or "").strip()
if rationale:
provenance["rationale"] = rationale
canon_mapping = edge_canon_mapping(edge_type)
candidates["edges"].append(
{
"stable_key": relationship_stable_key(source_key, edge_type, target_key, evidence_scope=scope["id"]),
"edge_type": edge_type,
"canonical_type": canon_mapping.canonical_type,
"canon_anchor": canon_mapping.canon_anchor,
"mapping_fit": canon_mapping.fit,
"display_only": canon_mapping.display_only,
"evidence_state": evidence_state_for(
origin="llm",
source_kind="llm",
review_state="needs_review",
confidence=confidence,
),
"source_key": source_key,
"target_key": target_key,
"attributes": _json_object(raw_edge.get("attributes")) if isinstance(raw_edge.get("attributes"), dict) else {},

View File

@@ -16,7 +16,35 @@ ORIGIN_PRECEDENCE = {
"manual": 5,
}
PATH_SCOPED_NODE_KINDS = {"lockfile"}
PATH_SCOPED_NODE_KINDS = {
"container-build",
"deployment-service",
"domain-name",
"kubernetes-config-map",
"kubernetes-cron-job",
"kubernetes-daemon-set",
"kubernetes-deployment",
"kubernetes-horizontal-pod-autoscaler",
"kubernetes-ingress",
"kubernetes-job",
"kubernetes-namespace",
"kubernetes-secret",
"kubernetes-service",
"kubernetes-stateful-set",
"lockfile",
"network-port",
"score-workload",
"service-config",
}
EVIDENCE_AGGREGATE_EDGE_TYPES = {
"exposes_port",
"listens_on",
"names_endpoint",
"opens_port",
"resolves_to",
"routes_to_port",
"routes_to_service",
}
def reconcile_discovery_snapshots(
@@ -186,9 +214,12 @@ def _edge_conflicts(by_key: dict[str, dict[str, Any]]) -> list[dict[str, object]
conflicts: list[dict[str, object]] = []
seen: dict[tuple[str, str, str], str] = {}
for key, edge in sorted(by_key.items()):
edge_type = str(edge.get("edge_type") or "")
if edge_type in EVIDENCE_AGGREGATE_EDGE_TYPES:
continue
match_key = (
str(edge.get("source_key") or ""),
str(edge.get("edge_type") or ""),
edge_type,
str(edge.get("target_key") or ""),
)
other = seen.get(match_key)
@@ -292,11 +323,24 @@ def _path_scoped_nodes_are_distinct(left: dict[str, Any] | None, right: dict[str
right_kind = normalize_identity_part(str(right.get("kind") or ""))
if left_kind != right_kind or left_kind not in PATH_SCOPED_NODE_KINDS:
return False
left_identities = _source_anchor_identities(left)
right_identities = _source_anchor_identities(right)
if left_identities and right_identities:
return left_identities.isdisjoint(right_identities)
left_paths = _source_anchor_paths(left)
right_paths = _source_anchor_paths(right)
return bool(left_paths and right_paths and left_paths.isdisjoint(right_paths))
def _source_anchor_identities(candidate: dict[str, Any]) -> set[str]:
anchors = candidate.get("source_anchors") if isinstance(candidate.get("source_anchors"), list) else []
return {
str(anchor.get("fingerprint") or "")
for anchor in anchors
if isinstance(anchor, dict) and anchor.get("fingerprint")
}
def _source_anchor_paths(candidate: dict[str, Any]) -> set[str]:
anchors = candidate.get("source_anchors") if isinstance(candidate.get("source_anchors"), list) else []
return {

Some files were not shown because too many files have changed in this diff Show More