generated from coulomb/repo-seed
Compare commits
29 Commits
f41d0da831
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cbfb3d0f74 | |||
| 20316c533f | |||
| 58a2cfac5c | |||
| 7312ed3767 | |||
| f9e661ec69 | |||
| f57a5c1dce | |||
| 757770b42f | |||
| 635521406f | |||
| 2afeb86ed2 | |||
| caa1e4100d | |||
| dae9e3409a | |||
| e23ed0c06b | |||
| fe2e98e2a4 | |||
| 7cb943ed41 | |||
| 31841d0f1c | |||
| 40b409cd92 | |||
| 0b5d295800 | |||
| 5dff7f14da | |||
| 5e89f6a075 | |||
| f09f110e77 | |||
| 558e0dc157 | |||
| 0f7b7d1fed | |||
| 9b612447ca | |||
| a7a22a673f | |||
| 296ac051a7 | |||
| 1ad432270b | |||
| f1cd74dc7b | |||
| d060b1c896 | |||
| fdee0b0855 |
50
.claude/rules/credential-routing.md
Normal file
50
.claude/rules/credential-routing.md
Normal 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`
|
||||
@@ -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 1–3 workstreams — each a coherent strand, weeks to months, anchored to a
|
||||
**Step 3 — Propose workplans to Bernd**
|
||||
Propose 1–3 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}
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<!-- custodian-brief: generated by fix-consistency — do not edit manually -->
|
||||
# Custodian Brief — railiance-fabric
|
||||
|
||||
**Domain:** railiance
|
||||
**Last synced:** 2026-05-24 15:00 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
|
||||
@@ -13,6 +13,6 @@
|
||||
## 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
17
.repo-classification.yaml
Normal 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
|
||||
95
AGENTS.md
95
AGENTS.md
@@ -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
|
||||
|
||||
@@ -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
93
DECISIONS.md
Normal 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
21
Makefile
Normal 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
|
||||
22
README.md
22
README.md
@@ -81,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
|
||||
@@ -136,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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -57,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
|
||||
@@ -81,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
|
||||
|
||||
@@ -169,6 +169,70 @@ introduce a two-phase 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
|
||||
|
||||
@@ -104,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
|
||||
@@ -156,7 +182,9 @@ 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 default boundary grouping is `deploymentEnvironment`:
|
||||
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 |
|
||||
|---------------|----------------|
|
||||
@@ -171,6 +199,11 @@ 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;
|
||||
@@ -180,6 +213,46 @@ Useful warnings for the graph explorer include:
|
||||
- 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
|
||||
|
||||
@@ -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
|
||||
|
||||
340
docs/semantic-attractors.md
Normal file
340
docs/semantic-attractors.md
Normal 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?
|
||||
16
fabric/bindings/railiance-apps-artifact-evidence-forge.yaml
Normal file
16
fabric/bindings/railiance-apps-artifact-evidence-forge.yaml
Normal 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.
|
||||
16
fabric/bindings/railiance-apps-container-registry-forge.yaml
Normal file
16
fabric/bindings/railiance-apps-container-registry-forge.yaml
Normal 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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
16
fabric/bindings/railiance-forge-postgresql-cnpg.yaml
Normal file
16
fabric/bindings/railiance-forge-postgresql-cnpg.yaml
Normal 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.
|
||||
16
fabric/bindings/railiance-forge-runtime-secrets-openbao.yaml
Normal file
16
fabric/bindings/railiance-forge-runtime-secrets-openbao.yaml
Normal 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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
21
fabric/capabilities/railiance-forge-container-registry.yaml
Normal file
21
fabric/capabilities/railiance-forge-container-registry.yaml
Normal 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
|
||||
@@ -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
|
||||
22
fabric/capabilities/railiance-forge-source-hosting.yaml
Normal file
22
fabric/capabilities/railiance-forge-source-hosting.yaml
Normal 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
|
||||
@@ -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
|
||||
30
fabric/dependencies/railiance-apps-artifact-evidence.yaml
Normal file
30
fabric/dependencies/railiance-apps-artifact-evidence.yaml
Normal 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.
|
||||
30
fabric/dependencies/railiance-apps-container-registry.yaml
Normal file
30
fabric/dependencies/railiance-apps-container-registry.yaml
Normal 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.
|
||||
@@ -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.
|
||||
28
fabric/dependencies/railiance-forge-kubernetes-runtime.yaml
Normal file
28
fabric/dependencies/railiance-forge-kubernetes-runtime.yaml
Normal 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.
|
||||
30
fabric/dependencies/railiance-forge-object-storage.yaml
Normal file
30
fabric/dependencies/railiance-forge-object-storage.yaml
Normal 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.
|
||||
28
fabric/dependencies/railiance-forge-postgresql.yaml
Normal file
28
fabric/dependencies/railiance-forge-postgresql.yaml
Normal 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.
|
||||
28
fabric/dependencies/railiance-forge-runtime-secrets.yaml
Normal file
28
fabric/dependencies/railiance-forge-runtime-secrets.yaml
Normal 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.
|
||||
23
fabric/interfaces/railiance-cluster-kubernetes-api.yaml
Normal file
23
fabric/interfaces/railiance-cluster-kubernetes-api.yaml
Normal 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
|
||||
@@ -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
|
||||
23
fabric/interfaces/railiance-forge-evidence-contract.yaml
Normal file
23
fabric/interfaces/railiance-forge-evidence-contract.yaml
Normal 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
|
||||
25
fabric/interfaces/railiance-forge-git-ssh.yaml
Normal file
25
fabric/interfaces/railiance-forge-git-ssh.yaml
Normal 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
|
||||
28
fabric/interfaces/railiance-forge-oci-registry.yaml
Normal file
28
fabric/interfaces/railiance-forge-oci-registry.yaml
Normal 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
|
||||
28
fabric/interfaces/railiance-forge-python-package-index.yaml
Normal file
28
fabric/interfaces/railiance-forge-python-package-index.yaml
Normal 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
|
||||
23
fabric/interfaces/railiance-forge-runner-label-contract.yaml
Normal file
23
fabric/interfaces/railiance-forge-runner-label-contract.yaml
Normal 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
|
||||
25
fabric/interfaces/railiance-forge-web-ui.yaml
Normal file
25
fabric/interfaces/railiance-forge-web-ui.yaml
Normal 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
|
||||
18
fabric/services/railiance-apps-s5-releases.yaml
Normal file
18
fabric/services/railiance-apps-s5-releases.yaml
Normal 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: []
|
||||
20
fabric/services/railiance-cluster-kubernetes.yaml
Normal file
20
fabric/services/railiance-cluster-kubernetes.yaml
Normal 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
|
||||
22
fabric/services/railiance-enablement-delivery-templates.yaml
Normal file
22
fabric/services/railiance-enablement-delivery-templates.yaml
Normal 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
|
||||
31
fabric/services/railiance-forge-source-forge.yaml
Normal file
31
fabric/services/railiance-forge-source-forge.yaml
Normal 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
|
||||
File diff suppressed because it is too large
Load Diff
725
railiance_fabric/zone_view.py
Normal file
725
railiance_fabric/zone_view.py
Normal file
@@ -0,0 +1,725 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict, deque
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneAttractionRule:
|
||||
edge_type: str | None = None
|
||||
direction: str = "both"
|
||||
depth: int = 1
|
||||
node_filter: Mapping[str, Any] = field(default_factory=dict)
|
||||
edge_filter: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.direction not in {"out", "in", "both"}:
|
||||
raise ValueError(f"Unsupported attraction direction: {self.direction}")
|
||||
if self.depth < 0:
|
||||
raise ValueError("Attraction depth must be zero or greater")
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any]) -> ZoneAttractionRule:
|
||||
return cls(
|
||||
edge_type=_optional_string(data.get("edge_type", data.get("edgeType"))),
|
||||
direction=str(data.get("direction") or "both"),
|
||||
depth=int(data.get("depth", 1)),
|
||||
node_filter=_mapping_or_empty(data.get("node_filter", data.get("nodeFilter"))),
|
||||
edge_filter=_mapping_or_empty(data.get("edge_filter", data.get("edgeFilter"))),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {
|
||||
"direction": self.direction,
|
||||
"depth": self.depth,
|
||||
}
|
||||
if self.edge_type:
|
||||
result["edge_type"] = self.edge_type
|
||||
if self.node_filter:
|
||||
result["node_filter"] = dict(self.node_filter)
|
||||
if self.edge_filter:
|
||||
result["edge_filter"] = dict(self.edge_filter)
|
||||
return result
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneLayout:
|
||||
algorithm: str = "preset"
|
||||
options: Mapping[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any] | None) -> ZoneLayout:
|
||||
if not data:
|
||||
return cls()
|
||||
return cls(
|
||||
algorithm=str(data.get("algorithm") or "preset"),
|
||||
options=_mapping_or_empty(data.get("options")),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {"algorithm": self.algorithm, "options": dict(self.options)}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZonePresentation:
|
||||
height: int = 0
|
||||
color: str = ""
|
||||
opacity: float = 0.16
|
||||
blur_below: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any] | None) -> ZonePresentation:
|
||||
if not data:
|
||||
return cls()
|
||||
return cls(
|
||||
height=int(data.get("height", 0)),
|
||||
color=str(data.get("color") or ""),
|
||||
opacity=float(data.get("opacity", 0.16)),
|
||||
blur_below=bool(data.get("blur_below", data.get("blurBelow", False))),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"height": self.height,
|
||||
"color": self.color,
|
||||
"opacity": self.opacity,
|
||||
"blur_below": self.blur_below,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneCollapse:
|
||||
enabled: bool = False
|
||||
label: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any] | None) -> ZoneCollapse:
|
||||
if not data:
|
||||
return cls()
|
||||
return cls(
|
||||
enabled=bool(data.get("enabled", False)),
|
||||
label=str(data.get("label") or ""),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {"enabled": self.enabled, "label": self.label}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneDefinition:
|
||||
id: str
|
||||
label: str = ""
|
||||
enabled: bool = True
|
||||
membership: Mapping[str, Any] = field(default_factory=dict)
|
||||
attraction_rules: tuple[ZoneAttractionRule, ...] = ()
|
||||
layout: ZoneLayout = field(default_factory=ZoneLayout)
|
||||
presentation: ZonePresentation = field(default_factory=ZonePresentation)
|
||||
collapse: ZoneCollapse = field(default_factory=ZoneCollapse)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any]) -> ZoneDefinition:
|
||||
attraction = data.get("attraction", ())
|
||||
if isinstance(attraction, Mapping):
|
||||
raw_attraction_rules = attraction.get("rules", ())
|
||||
else:
|
||||
raw_attraction_rules = attraction
|
||||
rules = tuple(
|
||||
rule if isinstance(rule, ZoneAttractionRule) else ZoneAttractionRule.from_dict(rule)
|
||||
for rule in raw_attraction_rules
|
||||
if isinstance(rule, Mapping) or isinstance(rule, ZoneAttractionRule)
|
||||
)
|
||||
zone_id = str(data["id"])
|
||||
return cls(
|
||||
id=zone_id,
|
||||
label=str(data.get("label") or zone_id),
|
||||
enabled=bool(data.get("enabled", True)),
|
||||
membership=_mapping_or_empty(data.get("membership")),
|
||||
attraction_rules=rules,
|
||||
layout=ZoneLayout.from_dict(_mapping_or_none(data.get("layout"))),
|
||||
presentation=ZonePresentation.from_dict(_mapping_or_none(data.get("presentation"))),
|
||||
collapse=ZoneCollapse.from_dict(_mapping_or_none(data.get("collapse"))),
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"label": self.label or self.id,
|
||||
"enabled": self.enabled,
|
||||
"membership": dict(self.membership),
|
||||
"attraction": {"rules": [rule.to_dict() for rule in self.attraction_rules]},
|
||||
"layout": self.layout.to_dict(),
|
||||
"presentation": self.presentation.to_dict(),
|
||||
"collapse": self.collapse.to_dict(),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneNodeAssignment:
|
||||
node_id: str
|
||||
zone_id: str
|
||||
reason: str
|
||||
depth: int = 0
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"node_id": self.node_id,
|
||||
"zone_id": self.zone_id,
|
||||
"reason": self.reason,
|
||||
"depth": self.depth,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneBoundaryEdge:
|
||||
edge_id: str
|
||||
source: str
|
||||
target: str
|
||||
edge_type: str
|
||||
source_zone_id: str | None
|
||||
target_zone_id: str | None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"edge_id": self.edge_id,
|
||||
"source": self.source,
|
||||
"target": self.target,
|
||||
"edge_type": self.edge_type,
|
||||
"source_zone_id": self.source_zone_id,
|
||||
"target_zone_id": self.target_zone_id,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneDiagnostic:
|
||||
severity: str
|
||||
code: str
|
||||
message: str
|
||||
node_id: str | None = None
|
||||
edge_id: str | None = None
|
||||
zone_ids: tuple[str, ...] = ()
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {
|
||||
"severity": self.severity,
|
||||
"code": self.code,
|
||||
"message": self.message,
|
||||
"zone_ids": list(self.zone_ids),
|
||||
}
|
||||
if self.node_id:
|
||||
result["node_id"] = self.node_id
|
||||
if self.edge_id:
|
||||
result["edge_id"] = self.edge_id
|
||||
return result
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneInstance:
|
||||
id: str
|
||||
label: str
|
||||
definition: ZoneDefinition
|
||||
node_ids: tuple[str, ...]
|
||||
seed_node_ids: tuple[str, ...]
|
||||
attracted_node_ids: tuple[str, ...]
|
||||
internal_edge_ids: tuple[str, ...]
|
||||
boundary_edge_ids: tuple[str, ...]
|
||||
diagnostics: tuple[ZoneDiagnostic, ...]
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"label": self.label,
|
||||
"definition": self.definition.to_dict(),
|
||||
"node_ids": list(self.node_ids),
|
||||
"seed_node_ids": list(self.seed_node_ids),
|
||||
"attracted_node_ids": list(self.attracted_node_ids),
|
||||
"internal_edge_ids": list(self.internal_edge_ids),
|
||||
"boundary_edge_ids": list(self.boundary_edge_ids),
|
||||
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ZoneResolution:
|
||||
zones: tuple[ZoneInstance, ...]
|
||||
node_assignments: Mapping[str, ZoneNodeAssignment]
|
||||
boundary_edges: tuple[ZoneBoundaryEdge, ...]
|
||||
diagnostics: tuple[ZoneDiagnostic, ...]
|
||||
|
||||
def zone_by_id(self, zone_id: str) -> ZoneInstance | None:
|
||||
return next((zone for zone in self.zones if zone.id == zone_id), None)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"zones": [zone.to_dict() for zone in self.zones],
|
||||
"node_assignments": {
|
||||
node_id: assignment.to_dict()
|
||||
for node_id, assignment in self.node_assignments.items()
|
||||
},
|
||||
"boundary_edges": [edge.to_dict() for edge in self.boundary_edges],
|
||||
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _NodeRecord:
|
||||
id: str
|
||||
data: Mapping[str, Any]
|
||||
order: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _EdgeRecord:
|
||||
id: str
|
||||
source: str
|
||||
target: str
|
||||
edge_type: str
|
||||
data: Mapping[str, Any]
|
||||
order: int
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class _Candidate:
|
||||
node_id: str
|
||||
zone_id: str
|
||||
reason: str
|
||||
depth: int
|
||||
definition_order: int
|
||||
|
||||
|
||||
def resolve_zones(
|
||||
nodes: Iterable[Mapping[str, Any]],
|
||||
edges: Iterable[Mapping[str, Any]],
|
||||
zone_definitions: Iterable[ZoneDefinition | Mapping[str, Any]],
|
||||
) -> ZoneResolution:
|
||||
"""Resolve zone definitions against graph data without mutating the graph."""
|
||||
|
||||
node_records = _node_records(nodes)
|
||||
edge_records = _edge_records(edges)
|
||||
definitions = tuple(
|
||||
definition if isinstance(definition, ZoneDefinition) else ZoneDefinition.from_dict(definition)
|
||||
for definition in zone_definitions
|
||||
)
|
||||
enabled_definitions = tuple(
|
||||
(index, definition)
|
||||
for index, definition in enumerate(definitions)
|
||||
if definition.enabled
|
||||
)
|
||||
diagnostics: list[ZoneDiagnostic] = []
|
||||
candidates_by_node_id: dict[str, list[_Candidate]] = defaultdict(list)
|
||||
seed_node_ids_by_zone_id: dict[str, set[str]] = defaultdict(set)
|
||||
|
||||
for definition_order, definition in enabled_definitions:
|
||||
for node in node_records:
|
||||
if _rule_matches(node.data, definition.membership, empty_matches=False):
|
||||
seed_node_ids_by_zone_id[definition.id].add(node.id)
|
||||
candidates_by_node_id[node.id].append(
|
||||
_Candidate(node.id, definition.id, "seed", 0, definition_order)
|
||||
)
|
||||
if not seed_node_ids_by_zone_id[definition.id]:
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="WARN",
|
||||
code="ZONE_EMPTY_SEED_SET",
|
||||
message=f"Zone {definition.id} did not match any seed nodes.",
|
||||
zone_ids=(definition.id,),
|
||||
)
|
||||
)
|
||||
|
||||
attraction_candidates = _attraction_candidates(
|
||||
node_records,
|
||||
edge_records,
|
||||
enabled_definitions,
|
||||
seed_node_ids_by_zone_id,
|
||||
diagnostics,
|
||||
)
|
||||
for candidate in attraction_candidates:
|
||||
candidates_by_node_id[candidate.node_id].append(candidate)
|
||||
|
||||
height_by_zone_id = {
|
||||
definition.id: definition.presentation.height
|
||||
for _, definition in enabled_definitions
|
||||
}
|
||||
assignments: dict[str, ZoneNodeAssignment] = {}
|
||||
for node in node_records:
|
||||
candidates = candidates_by_node_id.get(node.id, [])
|
||||
if not candidates:
|
||||
continue
|
||||
seed_candidates = _unique_zone_candidates(
|
||||
candidate for candidate in candidates if candidate.reason == "seed"
|
||||
)
|
||||
attraction_candidates_for_node = _unique_zone_candidates(
|
||||
candidate for candidate in candidates if candidate.reason == "attraction"
|
||||
)
|
||||
if len(seed_candidates) > 1:
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="WARN",
|
||||
code="ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES",
|
||||
message=f"Node {node.id} matched multiple zone membership rules.",
|
||||
node_id=node.id,
|
||||
zone_ids=tuple(candidate.zone_id for candidate in seed_candidates),
|
||||
)
|
||||
)
|
||||
if len(attraction_candidates_for_node) > 1:
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="WARN",
|
||||
code="ZONE_NODE_ATTRACTED_BY_MULTIPLE_ZONES",
|
||||
message=f"Node {node.id} was attracted by multiple zones.",
|
||||
node_id=node.id,
|
||||
zone_ids=tuple(candidate.zone_id for candidate in attraction_candidates_for_node),
|
||||
)
|
||||
)
|
||||
if seed_candidates and any(
|
||||
candidate.zone_id not in {seed.zone_id for seed in seed_candidates}
|
||||
for candidate in attraction_candidates_for_node
|
||||
):
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="INFO",
|
||||
code="ZONE_SEED_OVERRIDES_ATTRACTION",
|
||||
message=f"Node {node.id} kept seed membership over attraction.",
|
||||
node_id=node.id,
|
||||
zone_ids=tuple(
|
||||
candidate.zone_id
|
||||
for candidate in [*seed_candidates, *attraction_candidates_for_node]
|
||||
),
|
||||
)
|
||||
)
|
||||
pool = seed_candidates or attraction_candidates_for_node
|
||||
chosen = _choose_candidate(pool, height_by_zone_id)
|
||||
assignments[node.id] = ZoneNodeAssignment(
|
||||
node_id=node.id,
|
||||
zone_id=chosen.zone_id,
|
||||
reason=chosen.reason,
|
||||
depth=chosen.depth,
|
||||
)
|
||||
|
||||
boundary_edges: list[ZoneBoundaryEdge] = []
|
||||
internal_edge_ids_by_zone_id: dict[str, list[str]] = defaultdict(list)
|
||||
boundary_edge_ids_by_zone_id: dict[str, list[str]] = defaultdict(list)
|
||||
for edge in edge_records:
|
||||
if _is_context_only_edge(edge):
|
||||
continue
|
||||
source_zone_id = assignments.get(edge.source).zone_id if edge.source in assignments else None
|
||||
target_zone_id = assignments.get(edge.target).zone_id if edge.target in assignments else None
|
||||
if source_zone_id and source_zone_id == target_zone_id:
|
||||
internal_edge_ids_by_zone_id[source_zone_id].append(edge.id)
|
||||
continue
|
||||
if source_zone_id or target_zone_id:
|
||||
zone_ids = tuple(sorted(str(zone_id) for zone_id in {source_zone_id, target_zone_id} - {None}))
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="INFO",
|
||||
code="ZONE_EDGE_CROSSES_ZONE_BOUNDARY",
|
||||
message=f"Edge {edge.id} crosses a zone boundary.",
|
||||
edge_id=edge.id,
|
||||
zone_ids=zone_ids,
|
||||
)
|
||||
)
|
||||
boundary_edges.append(
|
||||
ZoneBoundaryEdge(
|
||||
edge_id=edge.id,
|
||||
source=edge.source,
|
||||
target=edge.target,
|
||||
edge_type=edge.edge_type,
|
||||
source_zone_id=source_zone_id,
|
||||
target_zone_id=target_zone_id,
|
||||
)
|
||||
)
|
||||
for zone_id in {source_zone_id, target_zone_id} - {None}:
|
||||
boundary_edge_ids_by_zone_id[str(zone_id)].append(edge.id)
|
||||
|
||||
zones: list[ZoneInstance] = []
|
||||
for _, definition in enabled_definitions:
|
||||
node_ids = tuple(
|
||||
node.id
|
||||
for node in node_records
|
||||
if node.id in assignments and assignments[node.id].zone_id == definition.id
|
||||
)
|
||||
seed_node_ids = tuple(
|
||||
node_id
|
||||
for node_id in node_ids
|
||||
if assignments[node_id].reason == "seed"
|
||||
)
|
||||
attracted_node_ids = tuple(
|
||||
node_id
|
||||
for node_id in node_ids
|
||||
if assignments[node_id].reason == "attraction"
|
||||
)
|
||||
zone_diagnostics = tuple(
|
||||
diagnostic
|
||||
for diagnostic in diagnostics
|
||||
if definition.id in diagnostic.zone_ids
|
||||
)
|
||||
zones.append(
|
||||
ZoneInstance(
|
||||
id=definition.id,
|
||||
label=definition.label or definition.id,
|
||||
definition=definition,
|
||||
node_ids=node_ids,
|
||||
seed_node_ids=seed_node_ids,
|
||||
attracted_node_ids=attracted_node_ids,
|
||||
internal_edge_ids=tuple(internal_edge_ids_by_zone_id[definition.id]),
|
||||
boundary_edge_ids=tuple(boundary_edge_ids_by_zone_id[definition.id]),
|
||||
diagnostics=zone_diagnostics,
|
||||
)
|
||||
)
|
||||
|
||||
return ZoneResolution(
|
||||
zones=tuple(zones),
|
||||
node_assignments=assignments,
|
||||
boundary_edges=tuple(boundary_edges),
|
||||
diagnostics=tuple(diagnostics),
|
||||
)
|
||||
|
||||
|
||||
def _attraction_candidates(
|
||||
node_records: tuple[_NodeRecord, ...],
|
||||
edge_records: tuple[_EdgeRecord, ...],
|
||||
enabled_definitions: tuple[tuple[int, ZoneDefinition], ...],
|
||||
seed_node_ids_by_zone_id: Mapping[str, set[str]],
|
||||
diagnostics: list[ZoneDiagnostic],
|
||||
) -> list[_Candidate]:
|
||||
nodes_by_id = {node.id: node for node in node_records}
|
||||
adjacency: dict[str, list[_EdgeRecord]] = defaultdict(list)
|
||||
for edge in edge_records:
|
||||
if _is_context_only_edge(edge):
|
||||
continue
|
||||
adjacency[edge.source].append(edge)
|
||||
adjacency[edge.target].append(edge)
|
||||
|
||||
candidates: dict[tuple[str, str], _Candidate] = {}
|
||||
depth_limit_diagnostics: set[tuple[str, str]] = set()
|
||||
for definition_order, definition in enabled_definitions:
|
||||
seed_node_ids = seed_node_ids_by_zone_id.get(definition.id, set())
|
||||
if not seed_node_ids:
|
||||
continue
|
||||
for rule in definition.attraction_rules:
|
||||
queue: deque[tuple[str, int]] = deque((node_id, 0) for node_id in seed_node_ids)
|
||||
seen_depth_by_node_id = {node_id: 0 for node_id in seed_node_ids}
|
||||
while queue:
|
||||
node_id, depth = queue.popleft()
|
||||
if depth >= rule.depth:
|
||||
if _has_matching_attraction_neighbor(
|
||||
node_id,
|
||||
adjacency.get(node_id, []),
|
||||
nodes_by_id,
|
||||
rule,
|
||||
):
|
||||
key = (definition.id, node_id)
|
||||
if key not in depth_limit_diagnostics:
|
||||
depth_limit_diagnostics.add(key)
|
||||
diagnostics.append(
|
||||
ZoneDiagnostic(
|
||||
severity="INFO",
|
||||
code="ZONE_ATTRACTION_DEPTH_LIMIT_REACHED",
|
||||
message=(
|
||||
f"Zone {definition.id} reached attraction depth "
|
||||
f"{rule.depth} at node {node_id}."
|
||||
),
|
||||
node_id=node_id,
|
||||
zone_ids=(definition.id,),
|
||||
)
|
||||
)
|
||||
continue
|
||||
for edge in adjacency.get(node_id, []):
|
||||
if not _edge_matches_attraction_rule(edge, rule):
|
||||
continue
|
||||
neighbor_id = _neighbor_for_direction(node_id, edge, rule.direction)
|
||||
if not neighbor_id:
|
||||
continue
|
||||
next_depth = depth + 1
|
||||
if seen_depth_by_node_id.get(neighbor_id, next_depth + 1) <= next_depth:
|
||||
continue
|
||||
neighbor = nodes_by_id.get(neighbor_id)
|
||||
if not neighbor or not _rule_matches(
|
||||
neighbor.data,
|
||||
rule.node_filter,
|
||||
empty_matches=True,
|
||||
):
|
||||
continue
|
||||
seen_depth_by_node_id[neighbor_id] = next_depth
|
||||
queue.append((neighbor_id, next_depth))
|
||||
if neighbor_id in seed_node_ids:
|
||||
continue
|
||||
key = (neighbor_id, definition.id)
|
||||
existing = candidates.get(key)
|
||||
candidate = _Candidate(
|
||||
node_id=neighbor_id,
|
||||
zone_id=definition.id,
|
||||
reason="attraction",
|
||||
depth=next_depth,
|
||||
definition_order=definition_order,
|
||||
)
|
||||
if existing is None or candidate.depth < existing.depth:
|
||||
candidates[key] = candidate
|
||||
return sorted(candidates.values(), key=lambda candidate: (candidate.node_id, candidate.zone_id))
|
||||
|
||||
|
||||
def _has_matching_attraction_neighbor(
|
||||
node_id: str,
|
||||
edges: Iterable[_EdgeRecord],
|
||||
nodes_by_id: Mapping[str, _NodeRecord],
|
||||
rule: ZoneAttractionRule,
|
||||
) -> bool:
|
||||
for edge in edges:
|
||||
if not _edge_matches_attraction_rule(edge, rule):
|
||||
continue
|
||||
neighbor_id = _neighbor_for_direction(node_id, edge, rule.direction)
|
||||
if not neighbor_id:
|
||||
continue
|
||||
neighbor = nodes_by_id.get(neighbor_id)
|
||||
if neighbor and _rule_matches(neighbor.data, rule.node_filter, empty_matches=True):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _node_records(nodes: Iterable[Mapping[str, Any]]) -> tuple[_NodeRecord, ...]:
|
||||
records: list[_NodeRecord] = []
|
||||
for order, element in enumerate(nodes):
|
||||
data = _element_data(element)
|
||||
node_id = str(data.get("id") or data.get("stableKey") or "")
|
||||
if node_id:
|
||||
records.append(_NodeRecord(id=node_id, data=data, order=order))
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def _edge_records(edges: Iterable[Mapping[str, Any]]) -> tuple[_EdgeRecord, ...]:
|
||||
records: list[_EdgeRecord] = []
|
||||
for order, element in enumerate(edges):
|
||||
data = _element_data(element)
|
||||
source = str(data.get("source") or "")
|
||||
target = str(data.get("target") or "")
|
||||
if not source or not target:
|
||||
continue
|
||||
edge_type = str(data.get("edgeType") or data.get("type") or data.get("canonicalType") or "")
|
||||
edge_id = str(data.get("id") or data.get("stableKey") or f"edge:{source}:{target}:{order}")
|
||||
records.append(
|
||||
_EdgeRecord(
|
||||
id=edge_id,
|
||||
source=source,
|
||||
target=target,
|
||||
edge_type=edge_type,
|
||||
data=data,
|
||||
order=order,
|
||||
)
|
||||
)
|
||||
return tuple(records)
|
||||
|
||||
|
||||
def _element_data(element: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
data = element.get("data")
|
||||
if isinstance(data, Mapping):
|
||||
return data
|
||||
return element
|
||||
|
||||
|
||||
def _edge_matches_attraction_rule(edge: _EdgeRecord, rule: ZoneAttractionRule) -> bool:
|
||||
if rule.edge_type and rule.edge_type != "*" and edge.edge_type != rule.edge_type:
|
||||
return False
|
||||
return _rule_matches(edge.data, rule.edge_filter, empty_matches=True)
|
||||
|
||||
|
||||
def _is_context_only_edge(edge: _EdgeRecord) -> bool:
|
||||
return _trueish(edge.data.get("displayOnly", edge.data.get("display_only"))) or edge.edge_type == "declares"
|
||||
|
||||
|
||||
def _trueish(value: Any) -> bool:
|
||||
return value is True or str(value).lower() == "true"
|
||||
|
||||
|
||||
def _neighbor_for_direction(node_id: str, edge: _EdgeRecord, direction: str) -> str:
|
||||
if direction in {"out", "both"} and edge.source == node_id:
|
||||
return edge.target
|
||||
if direction in {"in", "both"} and edge.target == node_id:
|
||||
return edge.source
|
||||
return ""
|
||||
|
||||
|
||||
def _rule_matches(data: Mapping[str, Any], rule: Mapping[str, Any], *, empty_matches: bool) -> bool:
|
||||
if not rule:
|
||||
return empty_matches
|
||||
if "all" in rule:
|
||||
nested = rule.get("all")
|
||||
return isinstance(nested, Iterable) and all(
|
||||
isinstance(child, Mapping) and _rule_matches(data, child, empty_matches=False)
|
||||
for child in nested
|
||||
)
|
||||
if "any" in rule:
|
||||
nested = rule.get("any")
|
||||
return isinstance(nested, Iterable) and any(
|
||||
isinstance(child, Mapping) and _rule_matches(data, child, empty_matches=False)
|
||||
for child in nested
|
||||
)
|
||||
if "rules" in rule:
|
||||
nested = rule.get("rules")
|
||||
return isinstance(nested, Iterable) and all(
|
||||
isinstance(child, Mapping) and _rule_matches(data, child, empty_matches=False)
|
||||
for child in nested
|
||||
)
|
||||
|
||||
field = str(rule.get("field") or "")
|
||||
op = str(rule.get("op") or "equals")
|
||||
expected = rule.get("value")
|
||||
actual = _field_value(data, field)
|
||||
if op == "equals":
|
||||
return actual == expected
|
||||
if op == "in":
|
||||
if isinstance(expected, str) or not isinstance(expected, Iterable):
|
||||
return actual == expected
|
||||
return actual in expected
|
||||
if op == "exists":
|
||||
return actual is not None and actual != "" and actual != []
|
||||
raise ValueError(f"Unsupported zone rule operator: {op}")
|
||||
|
||||
|
||||
def _field_value(data: Mapping[str, Any], field: str) -> Any:
|
||||
if not field:
|
||||
return None
|
||||
current: Any = data
|
||||
for part in field.split("."):
|
||||
if not isinstance(current, Mapping) or part not in current:
|
||||
return None
|
||||
current = current[part]
|
||||
return current
|
||||
|
||||
|
||||
def _unique_zone_candidates(candidates: Iterable[_Candidate]) -> list[_Candidate]:
|
||||
by_zone_id: dict[str, _Candidate] = {}
|
||||
for candidate in candidates:
|
||||
existing = by_zone_id.get(candidate.zone_id)
|
||||
if existing is None or (candidate.depth, candidate.definition_order) < (
|
||||
existing.depth,
|
||||
existing.definition_order,
|
||||
):
|
||||
by_zone_id[candidate.zone_id] = candidate
|
||||
return list(by_zone_id.values())
|
||||
|
||||
|
||||
def _choose_candidate(candidates: Iterable[_Candidate], height_by_zone_id: Mapping[str, int]) -> _Candidate:
|
||||
return sorted(
|
||||
candidates,
|
||||
key=lambda candidate: (
|
||||
-height_by_zone_id.get(candidate.zone_id, 0),
|
||||
candidate.definition_order,
|
||||
candidate.zone_id,
|
||||
candidate.depth,
|
||||
),
|
||||
)[0]
|
||||
|
||||
|
||||
def _mapping_or_empty(value: Any) -> Mapping[str, Any]:
|
||||
return value if isinstance(value, Mapping) else {}
|
||||
|
||||
|
||||
def _mapping_or_none(value: Any) -> Mapping[str, Any] | None:
|
||||
return value if isinstance(value, Mapping) else None
|
||||
|
||||
|
||||
def _optional_string(value: Any) -> str | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
return str(value)
|
||||
0
registry/capabilities/.gitkeep
Normal file
0
registry/capabilities/.gitkeep
Normal file
122
registry/capabilities/capability.railiance.fabric-graph.md
Normal file
122
registry/capabilities/capability.railiance.fabric-graph.md
Normal file
@@ -0,0 +1,122 @@
|
||||
---
|
||||
id: capability.railiance.fabric-graph
|
||||
name: Railiance Fabric Ecosystem Graph
|
||||
summary: 'Models the durable infrastructure-responsibility graph of the Railiance netkingdom: schemas,
|
||||
discovery tools, registry services, graph queries, and State Hub export contracts for services, machines,
|
||||
repos, deployables, endpoints, ownership, dependencies, and bindings.'
|
||||
owner: railiance-fabric
|
||||
status: draft
|
||||
domain: financials
|
||||
tags:
|
||||
- railiance
|
||||
- graph
|
||||
- ownership
|
||||
- discovery
|
||||
maturity:
|
||||
discovery:
|
||||
current: D3
|
||||
target: D5
|
||||
confidence: medium
|
||||
rationale: README and SCOPE.md document the ecosystem graph model bounded by financial/operational
|
||||
accountability (who pays, who is accountable), with king/lord/tenant ownership concepts referenced
|
||||
in docs/FabricDiscoveryAndUpdate.md.
|
||||
availability:
|
||||
current: A1
|
||||
target: A3
|
||||
confidence: medium
|
||||
rationale: Python package (`railiance-fabric`) providing a declaration loader and validator; consumed
|
||||
as a library, no hosted service documented yet.
|
||||
external_evidence:
|
||||
completeness:
|
||||
level: C1
|
||||
confidence: low
|
||||
basis: scope_vs_intent_and_consumer_expectations
|
||||
satisfied_expectations:
|
||||
- ecosystem graph declaration loader and validator
|
||||
- schemas for services/machines/repos/deployables/endpoints/ownership/dependencies/bindings
|
||||
broken_expectations: []
|
||||
out_of_scope_expectations: []
|
||||
reliability:
|
||||
level: R0
|
||||
confidence: low
|
||||
basis: consumer_quality_signals
|
||||
known_reliability_risks:
|
||||
- discovery/rebuild/update-loop architecture documented as still evolving per docs/FabricDiscoveryAndUpdate.md
|
||||
discovery:
|
||||
intent: Let repos declare services, capabilities, interfaces, dependencies, and bindings in source-controlled
|
||||
files, and model the resulting durable infrastructure-responsibility graph across the Railiance ecosystem.
|
||||
includes:
|
||||
- ecosystem graph schema and declaration loader/validator
|
||||
- State Hub export contracts for the graph
|
||||
excludes:
|
||||
- actual infrastructure provisioning (see railiance-infra, railiance-cluster)
|
||||
assumptions: []
|
||||
use_cases: []
|
||||
research_memos: []
|
||||
availability:
|
||||
current_level: A1
|
||||
target_level: A3
|
||||
current_artifacts:
|
||||
- Python package (`railiance-fabric`)
|
||||
target_artifacts: []
|
||||
consumption_modes:
|
||||
- library import
|
||||
- cli (validation)
|
||||
relations:
|
||||
depends_on: []
|
||||
supports: []
|
||||
related_to: []
|
||||
evidence:
|
||||
documentation:
|
||||
- README.md
|
||||
- SCOPE.md
|
||||
- docs/FabricDiscoveryAndUpdate.md
|
||||
tests:
|
||||
- tests/
|
||||
consumer_feedback: []
|
||||
bug_reports: []
|
||||
incidents: []
|
||||
consumer_guidance:
|
||||
recommended_for:
|
||||
- Railiance repos wanting to declare their place in the ecosystem ownership/dependency graph
|
||||
not_recommended_for:
|
||||
- needs for actual infrastructure provisioning (see railiance-infra/-cluster)
|
||||
known_limitations:
|
||||
- discovery/rebuild/update-loop architecture still evolving
|
||||
promotion_history: []
|
||||
---
|
||||
|
||||
# Railiance Fabric Ecosystem Graph
|
||||
|
||||
## Overview
|
||||
|
||||
`railiance-fabric` models the durable infrastructure-responsibility graph of the Railiance netkingdom — who pays for infrastructure, who is accountable for it, and which durable interfaces create value across boundaries — via schemas, a declaration loader/validator, and State Hub export contracts.
|
||||
|
||||
## Assessment notes
|
||||
|
||||
### Discovery
|
||||
|
||||
README and SCOPE.md document the ecosystem graph model bounded by financial/operational accountability (who pays, who is accountable), with king/lord/tenant ownership concepts referenced in docs/FabricDiscoveryAndUpdate.md.
|
||||
|
||||
### Availability
|
||||
|
||||
Python package (`railiance-fabric`) providing a declaration loader and validator; consumed as a library, no hosted service documented yet.
|
||||
|
||||
### Completeness
|
||||
|
||||
First-pass honest assessment from the REUSE-WP-0017 coverage campaign
|
||||
(reuse-surface). No external consumer feedback exists yet; levels reflect
|
||||
scope-vs-intent documentation quality, not internal code quality.
|
||||
|
||||
### Reliability
|
||||
|
||||
No production consumer telemetry exists yet; reliability level is
|
||||
intentionally conservative pending REUSE-WP-0019 reuse-telemetry evidence.
|
||||
|
||||
## Promotion checklist
|
||||
|
||||
- [x] ID follows `capability.<domain>.<name>` pattern
|
||||
- [x] Maturity enums match `specs/CapabilityMaturityStandard.md`
|
||||
- [x] `external_evidence` is populated separately from `maturity`
|
||||
- [ ] Relations reference valid capability IDs (none yet)
|
||||
- [x] Index entry added in `registry/indexes/capabilities.yaml`
|
||||
22
registry/indexes/capabilities.yaml
Normal file
22
registry/indexes/capabilities.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: 1
|
||||
updated: '2026-07-06'
|
||||
domain: helix_forge
|
||||
capabilities:
|
||||
- id: capability.railiance.fabric-graph
|
||||
name: Railiance Fabric Ecosystem Graph
|
||||
summary: 'Models the durable infrastructure-responsibility graph of the Railiance netkingdom: schemas,
|
||||
discovery tools, registry services, graph queries, and State Hub export contracts for services, machines,
|
||||
repos, deployables, endpoints, ownership, dependencies, and bindings.'
|
||||
vector: D3 / A1 / C1 / R0
|
||||
domain: financials
|
||||
status: draft
|
||||
owner: railiance-fabric
|
||||
path: registry/capabilities/capability.railiance.fabric-graph.md
|
||||
tags:
|
||||
- railiance
|
||||
- graph
|
||||
- ownership
|
||||
- discovery
|
||||
consumption_modes:
|
||||
- library import
|
||||
- cli (validation)
|
||||
@@ -402,6 +402,7 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert 'id="zone-overlay"' in page
|
||||
assert 'id="zone-boundary-toggle"' in page
|
||||
assert 'id="zone-group-select"' in page
|
||||
assert 'id="zone-layout-select"' in page
|
||||
assert 'id="node-type-filter"' in page
|
||||
assert 'id="edge-type-filter"' in page
|
||||
assert 'id="rule-panel"' in page
|
||||
@@ -415,6 +416,49 @@ def test_registry_serves_graph_explorer_exports(tmp_path: Path) -> None:
|
||||
assert "ruleRemovalSignature" in page
|
||||
assert "zoneModeFields" in page
|
||||
assert "zoneForData" in page
|
||||
assert "defaultZoneDefinitions" in page
|
||||
assert "resolveZoneInstances" in page
|
||||
assert "zoneDiagnosticCodes" in page
|
||||
assert "ZONE_EMPTY_SEED_SET" in page
|
||||
assert "ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES" in page
|
||||
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" in page
|
||||
assert "zone.diagnostics" in page
|
||||
assert "isZoneContextOnlyEdge" in page
|
||||
assert "isZoneConnectivityEdge" in page
|
||||
assert "currentZoneViewState" in page
|
||||
assert "normalizeZoneViewState" in page
|
||||
assert "zoneDefinitionSet" in page
|
||||
assert "zoneDefinitionSets" in page
|
||||
assert "fabric-default" in page
|
||||
assert "activeZoneLayoutAlgorithm" in page
|
||||
assert "zoneLayoutAlgorithms" in page
|
||||
assert "normalizeZoneLayoutAlgorithm" in page
|
||||
assert "zoneLayoutAlgorithm" in page
|
||||
assert "zoneLayout" in page
|
||||
assert "compact-grid" in page
|
||||
assert "circle" in page
|
||||
assert "zoneContainerState" in page
|
||||
assert "ensureZoneContainer" in page
|
||||
assert "packZoneContainers" in page
|
||||
assert "applyZoneContainerLayout" in page
|
||||
assert "layoutZoneSubgraph" in page
|
||||
assert "serializedZoneContainers" in page
|
||||
assert "normalizedZoneContainers" in page
|
||||
assert "collapsedZoneSnapshots" in page
|
||||
assert "zoneDragState" in page
|
||||
assert "startZoneDrag" in page
|
||||
assert "moveZoneDrag" in page
|
||||
assert "finishZoneDrag" in page
|
||||
assert "Moved zone" in page
|
||||
assert "cursor: grab" in page
|
||||
assert "background: transparent" in page
|
||||
assert "box-shadow: none" in page
|
||||
assert "collapseZone" in page
|
||||
assert "expandZone" in page
|
||||
assert "zoneCollapseNodeId" in page
|
||||
assert "Collapse Zone" in page
|
||||
assert "Expand Zone" in page
|
||||
assert 'normalize: "deploymentEnvironment"' in page
|
||||
assert "zoneBoundsForNodes" in page
|
||||
assert "renderZoneOverlay" in page
|
||||
assert "renderZoneDetails" in page
|
||||
|
||||
290
tests/test_zone_view.py
Normal file
290
tests/test_zone_view.py
Normal file
@@ -0,0 +1,290 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from railiance_fabric.zone_view import ZoneDefinition, resolve_zones
|
||||
|
||||
|
||||
def _node(node_id: str, **data: object) -> dict[str, dict[str, object]]:
|
||||
return {"data": {"id": node_id, **data}}
|
||||
|
||||
|
||||
def _edge(edge_id: str, source: str, target: str, edge_type: str, **data: object) -> dict[str, dict[str, object]]:
|
||||
return {
|
||||
"data": {
|
||||
"id": edge_id,
|
||||
"source": source,
|
||||
"target": target,
|
||||
"edgeType": edge_type,
|
||||
**data,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_zone_definition_round_trips() -> None:
|
||||
definition = ZoneDefinition.from_dict(
|
||||
{
|
||||
"id": "prod",
|
||||
"label": "Production",
|
||||
"membership": {
|
||||
"rules": [
|
||||
{"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
{"field": "kind", "op": "in", "value": ["service", "endpoint"]},
|
||||
]
|
||||
},
|
||||
"attraction": {
|
||||
"rules": [
|
||||
{
|
||||
"edge_type": "routes_to_service",
|
||||
"direction": "out",
|
||||
"depth": 2,
|
||||
"node_filter": {"field": "kind", "op": "exists"},
|
||||
}
|
||||
]
|
||||
},
|
||||
"layout": {"algorithm": "cose", "options": {"animate": False}},
|
||||
"presentation": {"height": 30, "color": "#be123c", "opacity": 0.2, "blur_below": True},
|
||||
"collapse": {"enabled": True, "label": "Production Zone"},
|
||||
}
|
||||
)
|
||||
|
||||
assert definition.id == "prod"
|
||||
assert definition.label == "Production"
|
||||
assert definition.attraction_rules[0].edge_type == "routes_to_service"
|
||||
assert definition.attraction_rules[0].depth == 2
|
||||
assert definition.layout.algorithm == "cose"
|
||||
assert definition.presentation.height == 30
|
||||
assert definition.collapse.enabled is True
|
||||
|
||||
round_tripped = ZoneDefinition.from_dict(definition.to_dict())
|
||||
|
||||
assert round_tripped == definition
|
||||
|
||||
|
||||
def test_resolver_assigns_seed_nodes_and_boundary_edges() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[
|
||||
_node("svc.prod", deploymentEnvironment="prod"),
|
||||
_node("svc.test", deploymentEnvironment="test"),
|
||||
],
|
||||
edges=[
|
||||
_edge("edge.prod-test", "svc.prod", "svc.test", "routes_to_service"),
|
||||
],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"label": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
"presentation": {"height": 20},
|
||||
},
|
||||
{
|
||||
"id": "test",
|
||||
"label": "test",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "test"},
|
||||
"presentation": {"height": 10},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert resolution.node_assignments["svc.prod"].zone_id == "prod"
|
||||
assert resolution.node_assignments["svc.prod"].reason == "seed"
|
||||
assert resolution.zone_by_id("prod").seed_node_ids == ("svc.prod",)
|
||||
assert resolution.zone_by_id("test").seed_node_ids == ("svc.test",)
|
||||
assert resolution.boundary_edges[0].edge_id == "edge.prod-test"
|
||||
assert resolution.boundary_edges[0].source_zone_id == "prod"
|
||||
assert resolution.boundary_edges[0].target_zone_id == "test"
|
||||
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" in {
|
||||
diagnostic.code for diagnostic in resolution.diagnostics
|
||||
}
|
||||
|
||||
|
||||
def test_resolver_ignores_context_only_edges_for_boundaries_and_attraction() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[
|
||||
_node("repo", kind="Repository"),
|
||||
_node("svc.prod", deploymentEnvironment="prod"),
|
||||
_node("svc.context", kind="service"),
|
||||
],
|
||||
edges=[
|
||||
_edge("edge.repo-prod", "repo", "svc.prod", "declares", displayOnly=True),
|
||||
_edge("edge.prod-context", "svc.prod", "svc.context", "declares", displayOnly=True),
|
||||
],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"label": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
"attraction": {
|
||||
"rules": [
|
||||
{
|
||||
"edge_type": "*",
|
||||
"direction": "both",
|
||||
"depth": 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert resolution.zone_by_id("prod").boundary_edge_ids == ()
|
||||
assert resolution.zone_by_id("prod").internal_edge_ids == ()
|
||||
assert "svc.context" not in resolution.node_assignments
|
||||
assert "ZONE_EDGE_CROSSES_ZONE_BOUNDARY" not in {
|
||||
diagnostic.code for diagnostic in resolution.diagnostics
|
||||
}
|
||||
|
||||
|
||||
def test_resolver_attracts_nodes_by_edge_type_direction_and_depth() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[
|
||||
_node("seed", deploymentEnvironment="prod", kind="service"),
|
||||
_node("near", kind="endpoint"),
|
||||
_node("far", kind="endpoint"),
|
||||
],
|
||||
edges=[
|
||||
_edge("edge.seed-near", "seed", "near", "routes_to_service"),
|
||||
_edge("edge.near-far", "near", "far", "routes_to_service"),
|
||||
],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
"attraction": {
|
||||
"rules": [
|
||||
{
|
||||
"edge_type": "routes_to_service",
|
||||
"direction": "out",
|
||||
"depth": 1,
|
||||
"node_filter": {"field": "kind", "op": "exists"},
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert resolution.node_assignments["near"].zone_id == "prod"
|
||||
assert resolution.node_assignments["near"].reason == "attraction"
|
||||
assert resolution.node_assignments["near"].depth == 1
|
||||
assert "far" not in resolution.node_assignments
|
||||
assert resolution.zone_by_id("prod").internal_edge_ids == ("edge.seed-near",)
|
||||
assert resolution.zone_by_id("prod").boundary_edge_ids == ("edge.near-far",)
|
||||
assert "ZONE_ATTRACTION_DEPTH_LIMIT_REACHED" in {
|
||||
diagnostic.code for diagnostic in resolution.diagnostics
|
||||
}
|
||||
|
||||
|
||||
def test_resolver_keeps_seed_membership_over_attraction() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[
|
||||
_node("seed", deploymentEnvironment="prod", kind="service"),
|
||||
_node("tenant", deploymentEnvironment="tenant", kind="endpoint"),
|
||||
],
|
||||
edges=[
|
||||
_edge("edge.seed-tenant", "seed", "tenant", "routes_to_service"),
|
||||
],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
"attraction": {
|
||||
"rules": [
|
||||
{
|
||||
"edge_type": "routes_to_service",
|
||||
"direction": "out",
|
||||
"depth": 1,
|
||||
}
|
||||
]
|
||||
},
|
||||
"presentation": {"height": 100},
|
||||
},
|
||||
{
|
||||
"id": "tenant",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "tenant"},
|
||||
"presentation": {"height": 0},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert resolution.node_assignments["tenant"].zone_id == "tenant"
|
||||
assert resolution.node_assignments["tenant"].reason == "seed"
|
||||
assert "ZONE_SEED_OVERRIDES_ATTRACTION" in {
|
||||
diagnostic.code for diagnostic in resolution.diagnostics
|
||||
}
|
||||
|
||||
|
||||
def test_resolver_uses_height_then_definition_order_for_overlapping_membership() -> None:
|
||||
height_resolution = resolve_zones(
|
||||
nodes=[_node("shared", deploymentEnvironment="prod", owner="lord")],
|
||||
edges=[],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "lower",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
"presentation": {"height": 10},
|
||||
},
|
||||
{
|
||||
"id": "higher",
|
||||
"membership": {"field": "owner", "op": "equals", "value": "lord"},
|
||||
"presentation": {"height": 20},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert height_resolution.node_assignments["shared"].zone_id == "higher"
|
||||
assert "ZONE_NODE_SEEDED_BY_MULTIPLE_ZONES" in {
|
||||
diagnostic.code for diagnostic in height_resolution.diagnostics
|
||||
}
|
||||
|
||||
order_resolution = resolve_zones(
|
||||
nodes=[_node("shared", deploymentEnvironment="prod", owner="lord")],
|
||||
edges=[],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "first",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
"presentation": {"height": 10},
|
||||
},
|
||||
{
|
||||
"id": "second",
|
||||
"membership": {"field": "owner", "op": "equals", "value": "lord"},
|
||||
"presentation": {"height": 10},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
assert order_resolution.node_assignments["shared"].zone_id == "first"
|
||||
|
||||
|
||||
def test_resolver_serializes_resolution() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[_node("svc", deploymentEnvironment="prod")],
|
||||
edges=[],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
serialized = resolution.to_dict()
|
||||
|
||||
assert serialized["zones"][0]["id"] == "prod"
|
||||
assert serialized["node_assignments"]["svc"]["zone_id"] == "prod"
|
||||
assert serialized["node_assignments"]["svc"]["reason"] == "seed"
|
||||
|
||||
|
||||
def test_resolver_reports_empty_zone_seed_set() -> None:
|
||||
resolution = resolve_zones(
|
||||
nodes=[_node("svc", deploymentEnvironment="dev")],
|
||||
edges=[],
|
||||
zone_definitions=[
|
||||
{
|
||||
"id": "prod",
|
||||
"membership": {"field": "deploymentEnvironment", "op": "equals", "value": "prod"},
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
assert resolution.zone_by_id("prod").node_ids == ()
|
||||
assert "ZONE_EMPTY_SEED_SET" in {diagnostic.code for diagnostic in resolution.diagnostics}
|
||||
46
workplans/archived/260702-ADHOC-2026-05-25.md
Normal file
46
workplans/archived/260702-ADHOC-2026-05-25.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: ADHOC-2026-05-25
|
||||
type: workplan
|
||||
title: "Ad Hoc Fixes 2026-05-25"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-05-25"
|
||||
updated: "2026-05-25"
|
||||
state_hub_workstream_id: "77e80432-9548-4ac8-9654-7beb931741e4"
|
||||
---
|
||||
|
||||
# ADHOC-2026-05-25 - Ad Hoc Fixes
|
||||
|
||||
## Add Zone Layout Algorithm Control
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-05-25-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "f0b4fdfa-9c26-4f8a-9372-90242eeec5d8"
|
||||
```
|
||||
|
||||
The graph explorer now lays zone subgraphs out as a grid inside each zone
|
||||
container. Add an operator-facing control that can switch the zone-local layout
|
||||
algorithm while keeping stable zone containers intact.
|
||||
|
||||
Expected result: the map controls expose a zone layout selector, the selected
|
||||
algorithm applies to each zone subgraph, and the setting persists in saved or
|
||||
copied view state.
|
||||
|
||||
Result: Added a `Zone Layout` selector with Grid and Circle algorithms. The
|
||||
selected algorithm is stored in nested zone view state, reflected by the
|
||||
`zoneLayout` URL alias for non-default layout, and reapplies zone-local node
|
||||
placement without moving stable zone containers.
|
||||
|
||||
Verification:
|
||||
|
||||
- `python3 -m pytest tests/test_graph_explorer.py tests/test_zone_view.py -q`
|
||||
passed.
|
||||
- Generated graph explorer JavaScript passed `node --check`.
|
||||
- `python3 -m railiance_fabric.cli validate .` passed.
|
||||
- `python3 -m pytest -q` passed with 72 tests.
|
||||
- Headless Edge screenshots confirmed Grid and Circle zone layouts render.
|
||||
46
workplans/archived/260702-ADHOC-2026-06-03.md
Normal file
46
workplans/archived/260702-ADHOC-2026-06-03.md
Normal file
@@ -0,0 +1,46 @@
|
||||
---
|
||||
id: ADHOC-2026-06-03
|
||||
type: workplan
|
||||
title: "Ad Hoc Fixes 2026-06-03"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-06-03"
|
||||
updated: "2026-06-03"
|
||||
state_hub_workstream_id: "c39b02dd-8074-4efd-ad18-add5595bf646"
|
||||
---
|
||||
|
||||
# ADHOC-2026-06-03 - Ad Hoc Fixes
|
||||
|
||||
## Add Graph Explorer Make Target
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-06-03-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "5605e418-95f4-4658-8021-9ee6f00de537"
|
||||
```
|
||||
|
||||
The graph explorer is served by the registry HTTP service, but starting it
|
||||
currently requires remembering the full registry command and database path.
|
||||
|
||||
Add a Makefile target that starts the registry-backed graph explorer from the
|
||||
repo root with defaults for the local database, host, and port. Calling `make`
|
||||
without an explicit target should list available targets rather than start a
|
||||
long-running service. Document the target and available overrides.
|
||||
|
||||
Result: Added `make graph-explorer`, with `registry` as an alias, defaulting to
|
||||
`.railiance-fabric/registry.sqlite3`, `127.0.0.1`, and port `8765`. Plain
|
||||
`make` now prints the available targets instead of starting the service. The
|
||||
target uses `python3 -m railiance_fabric.server`, so it works from the checkout
|
||||
without requiring an installed console script. Updated the README and graph
|
||||
explorer operations guide with the target and override examples.
|
||||
|
||||
Verification:
|
||||
|
||||
- Confirmed plain `make` lists available targets.
|
||||
- Started `make graph-explorer PORT=9877 REGISTRY_DB=/tmp/railiance-fabric-make-test.sqlite3`.
|
||||
- Confirmed `GET http://127.0.0.1:9877/status` returned `status: ok`.
|
||||
- Stopped the temporary verification server on port `9877`.
|
||||
34
workplans/archived/260702-ADHOC-2026-06-06.md
Normal file
34
workplans/archived/260702-ADHOC-2026-06-06.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
id: ADHOC-2026-06-06
|
||||
type: workplan
|
||||
title: "Ad Hoc Fixes 2026-06-06"
|
||||
domain: railiance
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-06-06"
|
||||
updated: "2026-06-06"
|
||||
state_hub_workstream_id: "02426929-e247-4fbf-8072-ea05cac41e93"
|
||||
---
|
||||
|
||||
# ADHOC-2026-06-06 - Ad Hoc Fixes
|
||||
|
||||
## Document Semantic Attractors
|
||||
|
||||
```task
|
||||
id: ADHOC-2026-06-06-T01
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "982d1571-128c-40de-bedf-5d70b2ffd586"
|
||||
```
|
||||
|
||||
Refine the concept of semantic attractors for the graph explorer: topic-like
|
||||
view entities such as `security`, `development`, and `operations` that pull
|
||||
repositories or other entities based on semantic closeness, initially scored
|
||||
from repo `SCOPE.md` files and mapped into spring-layout strength.
|
||||
|
||||
Result: Added `docs/semantic-attractors.md` with the concept model, SCOPE.md
|
||||
scoring approach, score semantics, layout mapping, payload shape, operator
|
||||
workflow, relationship to zones, initial presets, implementation path, and open
|
||||
questions. Linked the concept from the README and graph explorer contract.
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0001
|
||||
type: workplan
|
||||
title: "Railiance Ecosystem Graph Model"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0002
|
||||
type: workplan
|
||||
title: "Railiance Ecosystem Registry Service"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0003
|
||||
type: workplan
|
||||
title: "Registry Feed And Library Inventory"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0004
|
||||
type: workplan
|
||||
title: "Registry Inventory And Drift Views"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0005
|
||||
type: workplan
|
||||
title: "Registry Hardening"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: medium
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0006
|
||||
type: workplan
|
||||
title: "Multi-Repo Registry Onboarding"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: high
|
||||
@@ -2,9 +2,9 @@
|
||||
id: RAIL-FAB-WP-0007
|
||||
type: workplan
|
||||
title: "All Local Repo Onboarding"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: completed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
planning_priority: medium
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0008
|
||||
type: workplan
|
||||
title: "Interactive Fabric Map"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0009
|
||||
type: workplan
|
||||
title: "Graph Explorer UI Refinement"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0010
|
||||
type: workplan
|
||||
title: "Repo Reality Scanner"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0011
|
||||
type: workplan
|
||||
title: "Operational Rescan Loops"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0012
|
||||
type: workplan
|
||||
title: "Baseline Rollout And Conflict Review"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0013
|
||||
type: workplan
|
||||
title: "Path Scoped Duplicate Identity"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0014
|
||||
type: workplan
|
||||
title: "Runtime Topology Discovery"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0015
|
||||
type: workplan
|
||||
title: "Runtime Entity Taxonomy Refinement"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0016
|
||||
type: workplan
|
||||
title: "Canon-Aligned Graph Model Reset And Reingest"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0017
|
||||
type: workplan
|
||||
title: "Financial Fabric Model Reset"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0018
|
||||
type: workplan
|
||||
title: "Accountability Root Discovery And Update Loop"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0019
|
||||
type: workplan
|
||||
title: "Duplicate Repository Identity Review"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0020
|
||||
type: workplan
|
||||
title: "Deployment Zone Discovery And Visualization"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,7 +2,7 @@
|
||||
id: RAIL-FAB-WP-0021
|
||||
type: workplan
|
||||
title: "Zone Boundary Overlays"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
@@ -2,13 +2,14 @@
|
||||
id: RAIL-FAB-WP-0022
|
||||
type: workplan
|
||||
title: "Promote graph zones to first-class visualization entities"
|
||||
domain: railiance
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance-fabric
|
||||
topic_slug: railiance
|
||||
created: "2026-05-24"
|
||||
updated: "2026-05-24"
|
||||
state_hub_workstream_id: "343f8383-ba5e-4d60-b55e-81611954d9b9"
|
||||
---
|
||||
|
||||
# Promote Graph Zones To First-Class Visualization Entities
|
||||
@@ -33,8 +34,9 @@ step.
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "636b14f7-e5c7-4d5d-acda-40f0c270cd29"
|
||||
```
|
||||
|
||||
Define an internal zone view model that can represent:
|
||||
@@ -53,12 +55,19 @@ but it does not need a persistence store in this task.
|
||||
Expected result: a small typed model or schema with tests for construction and
|
||||
basic serialization.
|
||||
|
||||
Result: Added `railiance_fabric.zone_view` dataclasses for zone definitions,
|
||||
attraction rules, layout, presentation, collapse settings, resolved zone
|
||||
instances, node assignments, boundary edges, diagnostics, and serializable
|
||||
resolution output. Covered definition round-trip and resolution serialization in
|
||||
`tests/test_zone_view.py`.
|
||||
|
||||
## Task 2: Implement A Pure Zone Resolver
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "d0ca41d2-f7c4-4409-8799-4c0099192af5"
|
||||
```
|
||||
|
||||
Implement a pure resolver that accepts graph nodes, graph edges, and zone
|
||||
@@ -75,12 +84,20 @@ The first resolver should support a conservative rule subset:
|
||||
Expected result: resolver unit tests that prove deterministic node assignment,
|
||||
seed-vs-attraction precedence, conflict reporting, and depth-limited attraction.
|
||||
|
||||
Result: Implemented `resolve_zones()` as a pure resolver over Cytoscape-style or
|
||||
raw node/edge mappings. It supports `equals`, `in`, `exists`, `all`, `any`,
|
||||
membership `rules`, edge-type/direction/depth attraction, seed precedence,
|
||||
height/order conflict resolution, single-zone node assignments, boundary edge
|
||||
summaries, and conflict diagnostics. Covered the core behavior in
|
||||
`tests/test_zone_view.py`.
|
||||
|
||||
## Task 3: Back The Existing Overlays With Zone Definitions
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "c89d8d53-72a4-4590-a0e5-67b012c3550c"
|
||||
```
|
||||
|
||||
Replace the graph explorer's hard-coded environment/access overlay grouping
|
||||
@@ -97,12 +114,22 @@ The current operator-facing behavior should remain available:
|
||||
Expected result: existing graph explorer tests continue to pass, with new tests
|
||||
showing that the UI obtains zone rectangles from resolved zone instances.
|
||||
|
||||
Result: Refactored the graph explorer overlay to use explicit default zone
|
||||
definitions and a client-side `resolveZoneInstances()` path. Deployment
|
||||
environment zones now resolve through declarative membership rules with
|
||||
`deploymentEnvironment` normalization for `dev` -> `dev-tegwick`,
|
||||
`test`/`staging` -> `test`, `prod` -> `prod`, and ignored `all` values. Access
|
||||
zone overlays are generated as dynamic definitions from visible graph evidence.
|
||||
The overlay keeps the single-zone visible-node assignment behavior while
|
||||
preserving edge evidence in zone details.
|
||||
|
||||
## Task 4: Add Zone Diagnostics To The Explorer
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "d140cb5b-6a35-4cb0-ab68-e39e708c08e9"
|
||||
```
|
||||
|
||||
Expose zone resolver diagnostics in the graph explorer without overwhelming the
|
||||
@@ -119,12 +146,19 @@ Diagnostics should include at least:
|
||||
Expected result: zone detail panels show scoped diagnostics, and tests verify
|
||||
that diagnostics are generated by the resolver rather than ad hoc UI checks.
|
||||
|
||||
Result: Added resolver diagnostics for empty seed sets, overlapping zone
|
||||
membership, attraction depth-limit stops, and boundary-crossing edges. The graph
|
||||
explorer now surfaces scoped zone diagnostics in the selected zone detail panel
|
||||
and orientation context, with assertions proving diagnostics come from the zone
|
||||
resolver path.
|
||||
|
||||
## Task 5: Persist Zone View Settings In Profiles
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "765caa50-f372-4ab4-adb4-87660e684c54"
|
||||
```
|
||||
|
||||
Extend saved graph profile state so profiles can remember zone configuration.
|
||||
@@ -139,12 +173,20 @@ At minimum, preserve:
|
||||
Expected result: saved views restore the same zone mode and default definition
|
||||
set after reload.
|
||||
|
||||
Result: Added explicit nested zone state to graph explorer profiles with
|
||||
`visible`, `grouping`, `definitionSet`, and `presentation` fields while keeping
|
||||
legacy URL aliases for `zoneBoundaries`, `zoneGrouping`, and
|
||||
`zoneDefinitionSet`. Saved and copied views now preserve the active zone
|
||||
definition set and presentation state, and profile restore normalizes unknown
|
||||
future definition sets back to the Fabric default.
|
||||
|
||||
## Task 6: Prototype Collapse-To-Zone-Node
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "7f3676cb-3d2e-417c-a385-f95545bcd738"
|
||||
```
|
||||
|
||||
Prototype collapsing a resolved zone into a representative node while preserving
|
||||
@@ -161,12 +203,20 @@ The prototype should:
|
||||
Expected result: collapse behavior works for one zone at a time and is covered
|
||||
by focused tests. Multi-zone hierarchy can remain future work.
|
||||
|
||||
Result: Added a view-only zone collapse prototype to the graph explorer. Zone
|
||||
detail panels now offer `Collapse Zone`; collapsed zones hide member nodes,
|
||||
render a synthetic zone node with node/internal-edge/boundary-edge summaries,
|
||||
draw synthetic boundary edges to visible external neighbors, and expose
|
||||
`Expand Zone` from the collapsed zone node. Expanding removes synthetic elements
|
||||
and restores the original graph view without changing the underlying payload.
|
||||
|
||||
## Task 7: Prepare For Per-Zone Layout
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0022-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: low
|
||||
state_hub_task_id: "4b6f0b7e-a066-490d-8160-ba23b03cf820"
|
||||
```
|
||||
|
||||
Identify the UI and Cytoscape integration changes needed for each zone to use a
|
||||
@@ -179,6 +229,12 @@ a follow-up workplan for two-phase layout.
|
||||
Expected result: the codebase has a documented path toward per-zone layouts
|
||||
without destabilizing the current graph explorer.
|
||||
|
||||
Result: Documented the per-zone layout preparation path in
|
||||
`docs/ZoneEntityVisualization.md`. The recommended direction is a two-phase
|
||||
layout: resolve zones and containers first, then compute local zone coordinates
|
||||
and project them into Cytoscape space. The note identifies the prerequisites
|
||||
already established by WP-0022 and avoids a premature nested-layout rewrite.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Zone entity behavior is documented in `docs/ZoneEntityVisualization.md`.
|
||||
@@ -0,0 +1,115 @@
|
||||
---
|
||||
id: RAIL-FAB-WP-0023
|
||||
type: workplan
|
||||
title: "Improve zone labels and dragging"
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-05-25"
|
||||
updated: "2026-06-05"
|
||||
state_hub_workstream_id: "f02e14c5-e60f-4950-b1a2-682c38b30431"
|
||||
---
|
||||
|
||||
# Improve Zone Labels And Dragging
|
||||
|
||||
## Context
|
||||
|
||||
RAIL-FAB-WP-0021 introduced zone boundary rectangles and RAIL-FAB-WP-0022
|
||||
promoted zones to first-class graph explorer view entities. The current zone
|
||||
labels still look like small framed buttons with a white background. That makes
|
||||
the overlay read more like controls than like labels printed on a drawing
|
||||
surface.
|
||||
|
||||
The next interaction step is to let the operator grab a zone and move it around,
|
||||
dragging all currently attached visible nodes with it. This makes zones behave
|
||||
more like the intended "piece of paper" view entity while keeping the underlying
|
||||
Fabric graph unchanged.
|
||||
|
||||
## Task 1: Simplify Zone Labels
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0023-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "cb31f964-9e25-483d-b2b3-c4acc7a1033a"
|
||||
```
|
||||
|
||||
Change zone labels from framed button badges to plain text in the upper-left
|
||||
corner of the zone rectangle.
|
||||
|
||||
The label should:
|
||||
|
||||
- have no frame;
|
||||
- have no white background;
|
||||
- remain readable against the zone fill;
|
||||
- still be keyboard-focusable/clickable enough to open zone details;
|
||||
- avoid adding visual clutter to dense graph views.
|
||||
|
||||
Expected result: graph explorer zone labels look like text drawn on the zone
|
||||
surface rather than separate UI controls.
|
||||
|
||||
Result: Updated graph explorer zone labels to render as transparent, unframed
|
||||
text in the upper-left corner of each zone rectangle. Labels remain
|
||||
keyboard-focusable and clickable, and the visual smoke screenshot confirms the
|
||||
labels read as text on the zone surface.
|
||||
|
||||
## Task 2: Drag Zones With Member Nodes
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0023-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "84de3a0d-2ab9-4775-b026-87646ea14176"
|
||||
```
|
||||
|
||||
Add a zone drag interaction that moves all currently attached visible member
|
||||
nodes with the zone.
|
||||
|
||||
The interaction should:
|
||||
|
||||
- start from the zone label or zone boundary affordance;
|
||||
- move assigned visible nodes by the drag delta;
|
||||
- keep Cytoscape node positions and overlay bounds in sync;
|
||||
- update zone details and labels during/after the drag;
|
||||
- avoid mutating the underlying Fabric payload;
|
||||
- not interfere with normal node dragging more than necessary.
|
||||
|
||||
Expected result: grabbing a zone lets the operator reposition the visible zone
|
||||
subgraph as a unit.
|
||||
|
||||
Result: Added a view-only zone drag interaction using the plain zone label as
|
||||
the grab handle. Dragging translates the currently assigned visible zone member
|
||||
nodes by the pointer delta, recomputes overlay bounds, refreshes zone details,
|
||||
and leaves the Fabric graph payload unchanged.
|
||||
|
||||
## Task 3: Verify Interaction Behavior
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0023-T03
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "8793e982-8991-4cda-8259-3f981bbb9201"
|
||||
```
|
||||
|
||||
Verify the visual and interaction behavior with focused tests and a browser
|
||||
smoke check.
|
||||
|
||||
The verification should cover:
|
||||
|
||||
- static UI test assertions for the new label/drag helpers;
|
||||
- JavaScript syntax validation;
|
||||
- graph explorer focused tests;
|
||||
- a visual smoke screenshot showing plain labels;
|
||||
- manual or scripted confirmation that zone dragging moves member nodes.
|
||||
|
||||
Expected result: the UI renders clean labels and the zone drag interaction works
|
||||
without breaking existing graph explorer behavior.
|
||||
|
||||
Result: Static UI assertions, JavaScript syntax validation, focused graph tests,
|
||||
full test suite, Fabric CLI validation, and a headless Edge visual smoke pass.
|
||||
The scripted drag smoke ran against the registry-backed graph explorer in
|
||||
disposable Edge via CDP, dragged the `dev-tegwick` zone label, confirmed 25
|
||||
rendered nodes moved together, and observed the UI status text
|
||||
`Moved zone "dev-tegwick".`
|
||||
@@ -0,0 +1,134 @@
|
||||
---
|
||||
id: RAIL-FAB-WP-0024
|
||||
type: workplan
|
||||
title: "Stabilize zone containers and layout zone subgraphs"
|
||||
domain: financials
|
||||
repo: railiance-fabric
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-05-25"
|
||||
updated: "2026-05-25"
|
||||
state_hub_workstream_id: "63202459-2f73-409a-8881-307a5fc1835a"
|
||||
---
|
||||
|
||||
# Stabilize Zone Containers And Layout Zone Subgraphs
|
||||
|
||||
## Context
|
||||
|
||||
RAIL-FAB-WP-0022 made zones first-class visualization entities and
|
||||
RAIL-FAB-WP-0023 added plain labels plus view-only zone dragging. The current
|
||||
prototype still derives a zone rectangle directly from the current positions of
|
||||
its member nodes. When the operator changes the layout algorithm or reruns
|
||||
layout, the zone surface moves with the global graph layout instead of staying
|
||||
where the operator placed it.
|
||||
|
||||
The current resolver also treats every edge attached to a zoned node as zone
|
||||
context. That includes display-only repository declaration edges. Those edges
|
||||
help explain where graph declarations came from, but they should not be read as
|
||||
direct deployment or operational connections across a zone boundary.
|
||||
|
||||
This workplan moves the graph explorer toward the intended model: a zone is a
|
||||
stable drawing surface, and the assigned subgraph is laid out inside that
|
||||
surface as a separate view concern.
|
||||
|
||||
## Task 1: Separate Context Edges From Zone Boundary Connectivity
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0024-T01
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5e018a70-5aff-41bb-b0f9-0f59302c58ae"
|
||||
```
|
||||
|
||||
Exclude display-only/context edges, especially repository `declares` edges,
|
||||
from zone boundary diagnostics and collapsed-zone boundary edges.
|
||||
|
||||
Expected result: zone detail may still mention contextual evidence when useful,
|
||||
but display-only declaration edges do not make it look as if every zoned node is
|
||||
directly connected to an unzoned repository.
|
||||
|
||||
Result: Display-only edges and repository `declares` edges are now treated as
|
||||
context-only in both the shared zone resolver and the browser view. They do not
|
||||
create boundary diagnostics, attraction paths, or collapsed-zone boundary
|
||||
edges.
|
||||
|
||||
## Task 2: Persist Stable Zone Container Positions
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0024-T02
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ffa3f7b8-ee85-461a-89b8-007e4879171c"
|
||||
```
|
||||
|
||||
Introduce a view-level zone container state keyed by stable zone id.
|
||||
|
||||
The implementation should:
|
||||
|
||||
- remember zone container center and size after layout and drag;
|
||||
- keep user-moved zone positions stable when the global layout algorithm
|
||||
changes;
|
||||
- keep saved/copied view state compatible with the nested `zone` object;
|
||||
- avoid mutating the Fabric graph payload.
|
||||
|
||||
Expected result: switching between layout algorithms keeps zone papers in the
|
||||
same operator-chosen positions.
|
||||
|
||||
Result: The graph explorer now stores stable zone containers in view state.
|
||||
Containers remember graph-coordinate position and size, survive global layout
|
||||
reruns, can be saved/copied through the nested `zone` state, and remain separate
|
||||
from Fabric graph payload data.
|
||||
|
||||
## Task 3: Layout Zone Subgraphs Inside Containers
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0024-T03
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "f2f5adfa-4de4-4606-aa9f-51dbf10c6443"
|
||||
```
|
||||
|
||||
Add a per-zone layout pass that positions assigned visible nodes inside their
|
||||
zone container after the global layout places the surrounding graph.
|
||||
|
||||
The first implementation may use a deterministic compact layout rather than a
|
||||
full nested Cytoscape layout, as long as it:
|
||||
|
||||
- only moves visible nodes assigned to the zone;
|
||||
- keeps each node inside the zone's drawing surface;
|
||||
- treats unzoned nodes as part of the base canvas;
|
||||
- keeps edge routing intact through Cytoscape's normal renderer.
|
||||
|
||||
Expected result: the visible subgraph inside each zone is arranged by the zone
|
||||
view model, not merely enclosed after the global layout.
|
||||
|
||||
Result: After global layout, each visible zone projects its assigned nodes into
|
||||
a local compact layout inside its stable container. New, not-yet-moved zones are
|
||||
packed into readable rows so the default deployment-environment papers start as
|
||||
separate drawing surfaces.
|
||||
|
||||
## Task 4: Verify And Document Zone Layout Semantics
|
||||
|
||||
```task
|
||||
id: RAIL-FAB-WP-0024-T04
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "cd418134-cacc-42f6-a6ea-0bfd1cda9965"
|
||||
```
|
||||
|
||||
Update the graph explorer contract and run focused validation.
|
||||
|
||||
Verification should cover:
|
||||
|
||||
- static UI assertions for context-edge filtering and zone container helpers;
|
||||
- JavaScript syntax validation;
|
||||
- focused graph explorer and zone resolver tests;
|
||||
- a visual smoke check of the graph explorer.
|
||||
|
||||
Expected result: documentation and tests describe the new semantics, and the
|
||||
running UI shows stable zone containers with locally arranged member nodes.
|
||||
|
||||
Result: Updated the graph explorer contract and zone entity documentation.
|
||||
Focused tests, generated JavaScript syntax validation, full tests, CLI
|
||||
validation, and a headless Edge visual smoke check all passed.
|
||||
Reference in New Issue
Block a user