Finish RAILIANCE-WP-0021: family schemas, validator, and ADR-0007

Add rail and reef schemas, derive reef bound_rapps from rapp.bound_reefs,
and ship a standalone family-declaration validator with an inventory-fed
coverage check. Point the bootstrap contract at the schemas, record the
dimension and cardinality decisions in ADR-0007, correct the first-wave
candidates document, and release the shape to downstream repos.
This commit is contained in:
codex 2026-08-13 15:29:07 +02:00
parent d3b3512a60
commit 3cc0dc31d6
20 changed files with 1509 additions and 187 deletions

View file

@ -0,0 +1,76 @@
# ADR-0007: Rapp Declaration Contract
Date: 2026-08-13
Status: Accepted
## Context
`RAILIANCE-WP-0017` through `RAILIANCE-WP-0019` established the four-axis
model and materialized the first family repos. The model held up. Its
enforcement did not.
A 2026-08-11 survey by `railiance-platform` found that the three live
`rapp.yaml` files were mutually unreadable: rollout, smoke, and rollback
contracts used different shapes; metadata that both rails carry consistently
appeared in only one rapp; `reef-railiance` `bound_rapps` listed `rapp-qonto`
only, while `rapp-openbao` and `rapp-postgres` were already live on the same
reef. `docs/repo-family-bootstrap-contract.md` named fields in prose and
could not catch any of this.
The same survey treated rapp grouping as something that might be derived from
Forgejo organizations or from State Hub domains. Neither works. A repo lives
in exactly one Forgejo org, so `org:repo` is one-to-many. A repo may
legitimately contribute to more than one rapp, so `rapp:repo` is
many-to-many. A many-to-many grouping cannot be derived from a one-to-many
one. Domains fail in both directions. The three dimensions also change at
different speeds.
Canon OAS P1 requires independent perspectives to be separate orthogonal
dimensions. Decision `d07ee5f9` already recorded this from the
`railiance-platform` side; this ADR is the framework record.
## Decision
1. **Bounded rapp context is its own dimension.** It is derived neither from
Forgejo organizations nor from State Hub domains. Grouping is a first-class
declaration, not a projection of some other axis.
2. **Two cardinalities stay distinct.** Repos to rapps is many-to-many: a
repo may appear in the `composition.member_repos` of more than one rapp.
Deployables to rapps is one-to-one: every running deployable has exactly
one rapp that owns its rollout. The coverage question — *does every live
deployable belong to exactly one rapp?* — is well-formed only if these
stay distinct.
3. **Granularity is grouped-by-bounded-context.** One rapp per cohesive group
that deploys, versions, and rolls back together, not one rapp per
deployable. Grouping is legitimate only where members share rollout and
rollback fate. A single-repo rapp is the one-member case of the same
`composition` block, not a second shape.
4. **The schema is normative.** `schemas/rapp.schema.json`,
`schemas/rail.schema.json`, and `schemas/reef.schema.json` define the
shapes. Framework prose cites those files. It does not restate their
fields. Reef `bound_rapps` is a derived projection of `rapp.bound_reefs`,
not a hand-maintained registry.
The detailed shapes, including the single normative form of the rollout,
smoke, and rollback contracts, live in the schema files and
`schemas/README.md`.
## Consequences
- Drift across family declarations fails in
`tools/validate-family-declarations.py` instead of accumulating in prose.
- `railiance-platform` `RAILIANCE-WP-0015-T02` can converge `rapp-openbao`
and `rapp-postgres` onto one shape. Migration belongs to the owning repos;
this ADR does not move any declaration.
- `reef-railiance` must stop treating `bound_rapps: [rapp-qonto]` as source
of truth. The list is already stale.
- Three further `rapp-*` repos (`rapp-secrets-engine`, `rapp-tenant-engine`,
`rapp-user-engine`) carry the family prefix and no declaration. They are
visible to the validator as undeclared and must be declared, renamed, or
retired by their owners.
- Calling the validator from `fix-consistency` still waits on
`the-custodian` admitting the family prefixes into the classification
standard. That sequencing is not this repo's.

View file

@ -1,15 +1,48 @@
# First-Wave `rapp-*` Candidates
Date: 2026-07-25
Updated: 2026-08-13 (RAILIANCE-WP-0021-T08)
## Purpose
Choose and order the first `rapp-*` candidates for Railiance.
Choose and order the first `rapp-*` candidates for Railiance, and record
what was actually built against that plan.
`rapp-*` repos are for managed workload packaging and scaffolding. They are not
new ownership homes. The decision here is therefore not "which repos are
important?" but "which workloads already have a strong enough package boundary
to become first-class managed wrappers without weakening ownership?"
`rapp-*` repos are for managed workload packaging and scaffolding. They are
not new ownership homes. The decision is not "which repos are important?"
but "which workloads already have a strong enough package boundary to become
first-class managed wrappers without weakening ownership?"
## What Was Actually Built
The 2026-07-25 plan was:
1. `rapp-openbao`
2. `rapp-vergabe-teilnahme`
3. `rapp-forgejo` after the forge runtime target stabilized
What exists on 2026-08-13:
| Repo | Status | How it arrived |
|---|---|---|
| `rapp-openbao` | live, declared | as planned |
| `rapp-postgres` | live, declared | ad hoc, from the need for a shared platform database |
| `rapp-qonto` | live, declared | ad hoc, from the need to run Qonto on `rail-knative` |
| `rapp-vergabe-teilnahme` | never built | the user-facing wrapper it existed to prove is unproven |
| `rapp-forgejo` | not built | forge placement is now decided (`d151d817`): workload to `rapp-forgejo`, layer to S4 |
| `rapp-secrets-engine` | exists, undeclared | family prefix, no `declarations/` |
| `rapp-tenant-engine` | exists, undeclared | family prefix, no `declarations/` |
| `rapp-user-engine` | exists, undeclared | family prefix, no `declarations/` |
The platform-service pattern is proven twice (`rapp-openbao`,
`rapp-postgres`). The user-facing wrapper pattern is unproven. That is the
consequence of building to immediate need instead of to the planned second
slot.
The operator decision that follows is: build `rapp-vergabe-teilnahme` to
prove the user-facing shape before mass extraction. Do not treat the three
engine repos, or any other live undeclared workload, as a substitute for
that proof.
## Selection Criteria
@ -23,147 +56,159 @@ The first-wave candidates should satisfy most of the following:
- low ambiguity between packaging ownership and domain ownership
- good demonstration value for later `rapp-*` extractions
## Assessed Candidates
## Planned First-Wave Assessment (2026-07-25)
The original assessment is kept below so the gap between plan and outcome
stays visible. It is not the current worklist.
### 1. OpenBao from `railiance-platform`
Assessment: **choose first**
Assessment: **choose first** — and this one landed.
Why it fits:
Why it fitted:
- It is a clear third-party upstream product with durable identity.
- The workload already has a substantial Railiance packaging surface:
`helm/openbao-values.yaml`, middleware, UI overlay assets, deploy/verify
scripts, and operator runbooks.
- The repo already distinguishes between the OpenBao workload itself and the
wider S3 platform policy that consumes it.
- It is important enough to prove that `rapp-*` is not only for user-facing
apps; platform services can also be managed workloads.
- The workload already had a substantial Railiance packaging surface.
- The repo already distinguished the OpenBao workload from the wider S3
platform policy that consumes it.
- It proved that `rapp-*` is not only for user-facing apps.
What should move into `rapp-openbao`:
- Helm values and Kubernetes-facing package assets for the OpenBao workload
- UI overlay wrapper assets
- generic deploy, verify, and workload-health runbooks
- workload-specific smoke and recovery expectations
What should remain in `railiance-platform`:
- the S3 ownership of secrets custody as a platform capability
- workload lane policy, credential approval, and platform-wide access models
- cross-workload secret-delivery conventions consumed by many workloads
Conclusion:
`rapp-openbao` should be the first `rapp-*` extraction.
What stayed in `railiance-platform`: secrets custody as a platform
capability, lane policy, credential approval, and cross-workload
secret-delivery conventions.
### 2. Forge workload from `railiance-forge`
Assessment: **defer from first wave**
Assessment then: **defer from first wave**. That deferral still holds as a
first-wave fact, but the placement question is now closed. Decision
`d151d817` puts the Forgejo workload in `rapp-forgejo` and the layer
responsibility (runner placement and labels, registry retention, artifact
lifecycle, package credentials) in S4 `railiance-enablement`.
`railiance-forge` is placed, not retired. Gitea is confirmed replaced.
Why it is not first:
- The current live workload is still Gitea on CoulombCore.
- The intended production direction is Forgejo on Railiance01.
- Packaging identity is therefore not stable enough yet: extracting now would
either enshrine a temporary Gitea compatibility package or pretend the
Forgejo cutover is already complete.
- The runtime and migration story still spans current Gitea operation, future
Forgejo deployment, runner substrate, registries, and cutover sequencing.
Decision:
- Do **not** make the forge workload the first `rapp-*` extraction.
- Prefer `rapp-forgejo` as the eventual target package once the production
direction is the real operating target.
- Create `rapp-gitea` only if Railiance discovers it needs a long-lived
compatibility wrapper for the current workload rather than a short migration
bridge.
What this means:
- `railiance-forge` keeps runtime ownership for now.
- The forge wrapper should follow after the current Gitea-versus-Forgejo
packaging identity stops moving.
Conclusion:
The forge workload is packaging-worthy, but it should not be first-wave
`rapp-*`.
`rapp-forgejo` is therefore a wave-2 package with a decided home, not an
open placement debate.
### 3. User-facing S5 workload from `railiance-apps`
Assessment: **choose `vergabe-teilnahme` for wave 1**
Assessment then: **choose `vergabe-teilnahme` for wave 1**. It was never
built. The reasons it was the right user-facing slot remain: one named
workload, explicit release surface, secret handling that is not entangled
with shared platform semantics. That is why it is still the package that
must prove the user-facing shape before mass extraction.
Why `vergabe-teilnahme` wins the S5 slot:
`reuse-surface` remains the strongest follow-on self-built service
candidate. `core-hub` should still wait.
- It is clearly user-facing and already operates as one named workload package.
- Its release surface is explicit: chart, values, ingress, deployment targets,
migration command, smoke checks, and operator runbook.
- Secret handling is clear without being entangled with shared platform
semantics: app credentials are consumed from Kubernetes Secrets and the app
env secret is locally rebuilt by operator procedure.
- It is simpler and more stable than the current Core Hub / Inter-Hub history
and more obviously user-facing than service-style workloads such as
`reuse-surface`.
## Wave-2 Inventory
What should move into `rapp-vergabe-teilnahme`:
Grouped by bounded context, in the composition shape from ADR-0007. These
are indicative packages, not created repos. A package is listed only where
members share rollout and rollback fate. Names that already exist as
undeclared `rapp-*` repos are marked so they are not invented twice.
- the chart and workload values
- the ingress and app-specific release runbook
- workload-specific rollout, migration, smoke, and rollback guidance
- workload-specific secret consumption contract
### `rapp-vergabe-teilnahme` — prove the user-facing wrapper
What should remain in `railiance-apps`:
```yaml
composition:
purpose: Package the Vergabe Teilnahme application as a managed user-facing workload.
member_repos:
- repo: vergabe-teilnahme
role: application source and chart
deployables: [vergabe-teilnahme]
```
- S5 ownership of generic application release patterns
- reusable onboarding and operator recipes
- cross-app S5 guardrails that should not be duplicated per workload package
Build this before any further user-facing extraction.
Second-wave S5 note:
### `rapp-forgejo` — decided placement
- `reuse-surface` is the strongest follow-on self-built service candidate after
`vergabe-teilnahme`, because it already shows explicit OpenBao-backed runtime
secret lanes and a tidy single-workload chart surface.
- `core-hub` and retired `inter-hub` should wait because their packaging
history is still entangled with cutover and legacy service evolution.
```yaml
composition:
purpose: Package Forgejo, its runner, and the package registry as one managed forge.
member_repos:
- repo: railiance-forge
role: operational helm, manifests, runner, and registry assets
deployables: [forgejo, forgejo-runner, forgejo-registry]
```
Conclusion:
Layer concerns stay in `railiance-enablement`. Do not create `rapp-gitea`.
`rapp-vergabe-teilnahme` should be the first user-facing S5 `rapp-*`.
### `rapp-netkingdom-identity` — identity bounded context
## First-Wave Order
```yaml
composition:
purpose: Package NetKingdom identity — users, tenants, SSO, MFA, and authorization — as one rollback unit.
member_repos:
- repo: user-engine
role: user identity
deployables: [user-engine]
- repo: tenant-engine
role: tenant identity
deployables: [tenant-engine]
- repo: key-cape
role: SSO and MFA
deployables: [keycloak]
- repo: flex-auth
role: authorization decisions
deployables: [flex-auth]
```
The recommended order is:
`rapp-user-engine` and `rapp-tenant-engine` already exist as undeclared
prefix claims. They are not separate rapps under grouped-by-bounded-context
granularity. Their owners should either fold them into this package or
justify an independent rollback fate and then declare them. `rapp-secrets-engine`
is the same question against `rapp-openbao` / `railiance-platform`, not a
fourth identity member.
1. `rapp-openbao`
2. `rapp-vergabe-teilnahme`
3. `rapp-forgejo` after the forge runtime target is stable enough to avoid
immediate rename or migration churn
### `rapp-coulomb-social`
This order is intentional.
```yaml
composition:
purpose: Package the Coulomb Social application as a managed user-facing workload.
member_repos:
- repo: coulomb-social
role: application
deployables: [coulomb-social]
```
- `rapp-openbao` proves the third-party platform-service wrapper model.
- `rapp-vergabe-teilnahme` proves the self-built user-facing app wrapper model.
- The forge wrapper then follows with a clearer target identity and after the
first two wrappers have established the pattern.
Do not start this until `rapp-vergabe-teilnahme` has proven the user-facing
shape.
### `rapp-helix-forge`
```yaml
composition:
purpose: Package the Helix Forge methodological workspace as a managed workload.
member_repos:
- repo: helix-forge
role: capability-first development ecosystem
deployables: [helix-forge]
```
Helix Forge is a different lineage from `railiance-forge`. It is not a
rename of `rapp-forgejo`.
## Risks To Watch
- If `rapp-openbao` absorbs platform-wide policy and credential-governance
logic, the wrapper will become a shadow S3 repo.
- If `rapp-vergabe-teilnahme` absorbs generic S5 onboarding or app recipes, it
will weaken `railiance-apps` as the shared S5 release home.
- If the forge wrapper is extracted too early, Railiance will immediately face
a Gitea-versus-Forgejo package rename or parallel-wrapper problem.
logic, the wrapper becomes a shadow S3 repo.
- If `rapp-postgres` is asked to mint the backup account, key, or bucket, a
consumed destination is being mistaken for package ownership. The bucket
is `resource-control` (`RESOURCE-WP-0002`); the credential is
`railiance-platform` OpenBao custody; purchase is decision `9c21c0e0`.
- If `rapp-vergabe-teilnahme` absorbs generic S5 onboarding, it weakens
`railiance-apps`.
- If the three undeclared engine repos are treated as rapps without a
declaration, the coverage check cannot see them and will list their live
deployables as uncovered.
- If wave-2 extraction starts before the user-facing wrapper is proven,
Railiance will mass-produce the platform-service shape and still not know
whether the other shape works.
## Outcome
Railiance now has a concrete first-wave `rapp-*` sequence:
- first `rapp-openbao`
- then `rapp-vergabe-teilnahme`
- then the forge wrapper once the production package identity is stable
That is enough to start repo-local extraction planning without pretending every
workload needs its own `rapp` immediately.
Wave 1 proved the platform-service wrapper, twice, and did not prove the
user-facing wrapper. The next package that must exist is
`rapp-vergabe-teilnahme`. Everything else in the wave-2 inventory waits on
that proof, on owner-side declaration of the three undeclared prefix
claims, and on the schema migration owned by `RAILIANCE-WP-0015-T02`.

View file

@ -65,54 +65,51 @@ described.
The first wave should standardize one obvious declaration path per repo family.
The field lists below are an index, not a specification. The JSON Schema for
each family is normative. If this document and a schema disagree, the schema
wins. Shapes, types, enums, and cardinality live in the schema files; do not
restate them here.
### `rail-*`
Path:
Path: `declarations/rail.yaml`
`declarations/rail.yaml`
Schema: [`schemas/rail.schema.json`](../schemas/rail.schema.json)
Minimum fields:
- `rail_id`
- `ownership_repo`
- `execution_architecture`
- `substrate_prerequisites`
- `supported_rollout_modes`
- `compatibility_notes`
A rail is either `composition_kind: base` or `composition_kind: derived`.
A derived rail names its base contract rather than copying it. See
[`docs/rail-composition-contract.md`](rail-composition-contract.md) and
ADR-0005. The live name for substrate needs is
`required_substrate_capabilities`; the earlier `substrate_prerequisites`
label is retired.
### `rapp-*`
Path:
Path: `declarations/rapp.yaml`
`declarations/rapp.yaml`
Schema: [`schemas/rapp.schema.json`](../schemas/rapp.schema.json)
Minimum fields:
A rapp is a composition: a stated purpose, first-party member repos, and
pinned third-party components. Repos to rapps is many-to-many; deployables
to rapps is one-to-one. See ADR-0007. `bound_reefs` is how a rapp names the
reefs it is placed on; that field is the source of a reef's `bound_rapps`.
- `rapp_id`
- `workload_identity`
- `ownership_repo`
- `supported_rails`
- `runtime_dependencies`
- `rollout_contract`
- `smoke_contract`
- `rollback_contract`
`runtime_dependencies` names capabilities, not the resources that satisfy
them. Who provisions a capability (for example the postgres backup object
store) is owned outside the rapp: the bucket by `resource-control`, the
credential by `railiance-platform`. The rapp consumes a reviewed destination.
### `reef-*`
Path:
Path: `declarations/reef.yaml`
`declarations/reef.yaml`
Schema: [`schemas/reef.schema.json`](../schemas/reef.schema.json)
Minimum fields:
- `reef_id`
- `substrate_kind`
- `ownership_repo`
- `lifecycle_state`
- `criticality`
- `hosted_rails`
- `bound_rapps`
- `primary_rail`
`bound_rapps` is a derived projection of every rapp whose `bound_reefs`
lists this reef. It is not a hand-maintained registry. A reef may omit the
field; if it keeps the field, the validator requires an exact match against
the projection. `primary_rail` must appear in `hosted_rails`. Hosting a rail
or binding a rapp is topology, not readiness — see ADR-0006.
## Required Boundary Rules
@ -145,20 +142,11 @@ defined in `railiance-master`.
## First-Wave Candidates
The first materialization wave should target:
- `rail-kubernetes`
- `rapp-openbao`
- `reef-railiance`
The follow-on first-wave candidates after those anchors are stable:
- `rapp-vergabe-teilnahme`
- `reef-coulombcore`
- `reef-ops-workstations`
This keeps the first concrete rollout narrow while still covering all three new
repo families.
The planned first materialization wave was `rail-kubernetes`, `rapp-openbao`,
`reef-railiance`, then `rapp-vergabe-teilnahme`. What was actually built, and
the wave-2 inventory, is recorded in
[`docs/rapp-first-wave-candidates.md`](rapp-first-wave-candidates.md). Do not
restate that list here.
## Integration Expectations