schemas/rapp.schema.json defines one normative shape for the rollout, smoke and rollback contracts in place of the three mutually unreadable variants found across the live rapps, promotes contract_version, readiness_state, data_classification and criticality to required, and forbids the rapp- prefix on workload_identity.name. composition replaces the flat members list per amendment f88f938d: purpose, member_repos with deployables, and pinned upstream_components. Repos are many:many with rapps; deployables are 1:1, which is what makes the T06 coverage check well-formed. ownership_repo left permissive pending an architecture-owner call; the tighter alternative is written up in schemas/README.md. Validated against all three live declarations: openbao 10 errors, postgres 10, qonto 4 — precisely the reported drift and nothing else. Also found: three further rapp-* repos (secrets-engine, tenant-engine, user-engine) carry no declarations at all, which the routed survey missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
428 lines
18 KiB
Markdown
428 lines
18 KiB
Markdown
---
|
|
id: RAILIANCE-WP-0021
|
|
type: workplan
|
|
title: "Rapp Declaration Schema and Family Declaration Validator"
|
|
domain: financials
|
|
repo: railiance-master
|
|
status: ready
|
|
owner: codex
|
|
topic_slug: railiance
|
|
planning_priority: high
|
|
planning_order: 21
|
|
created: "2026-08-11"
|
|
updated: "2026-08-11"
|
|
related_repos:
|
|
- railiance-master
|
|
- railiance-platform
|
|
- rapp-openbao
|
|
- rapp-postgres
|
|
- rapp-qonto
|
|
- rail-kubernetes
|
|
- rail-knative
|
|
- reef-railiance
|
|
- the-custodian
|
|
state_hub_workstream_id: "37667c23-1fa9-430e-8d80-d67d98f90e38"
|
|
---
|
|
|
|
# RAILIANCE-WP-0021 - Rapp Declaration Schema and Family Declaration Validator
|
|
|
|
## Goal
|
|
|
|
Give the `rapp-*` family a machine-checkable declaration contract, and stand up
|
|
a validator over all three family declaration files (`rail.yaml`, `rapp.yaml`,
|
|
`reef.yaml`) so that architectural drift fails loudly instead of accumulating
|
|
quietly.
|
|
|
|
The deliverable is not more prose. `docs/repo-family-bootstrap-contract.md`
|
|
already names the required `rapp.yaml` fields; naming them in prose is exactly
|
|
what failed. The deliverable is a schema plus a check that runs.
|
|
|
|
## Why This Exists
|
|
|
|
`RAILIANCE-WP-0017` through `RAILIANCE-WP-0019` established the four-axis model
|
|
and materialized the first concrete family repos. The model held up. Its
|
|
enforcement did not.
|
|
|
|
`railiance-platform` surveyed all six family repos and the live Railiance01
|
|
cluster on 2026-08-11 under its `RAILIANCE-WP-0015` and routed the findings
|
|
here, on the grounds that `railiance-master` owns the model and an
|
|
implementation repo should not author it. State Hub messages
|
|
`04c776c4-52fa-494a-9761-942123f28a6e` (proposal) and
|
|
`f88f938d-1ebc-4ba0-91ee-9395e44a7cb3` (amendment) carry the detail.
|
|
|
|
Reported drift across the three live rapps:
|
|
|
|
- rollout, smoke, and rollback contracts are mutually unreadable —
|
|
`rapp-openbao` and `rapp-postgres` use `supported_commands` lists,
|
|
`rapp-qonto` uses `default_mode` / `production_gate`, a `required:` smoke
|
|
list and an `order:` rollback list
|
|
- `contract_version`, `readiness_state`, `data_classification` and
|
|
`criticality` appear only in `rapp-qonto`, though both rails carry
|
|
`contract_version` / `readiness_state` consistently
|
|
- `consumer_contract` / `consumers` exist only in `rapp-postgres`
|
|
- `workload_identity.name` is the workload name in two rapps and the repo name
|
|
in the third
|
|
- `ownership_repo` points at a layer repo twice and at an app repo once
|
|
- `reef-railiance` `bound_rapps` lists `rapp-qonto` only, while `rapp-openbao`
|
|
and `rapp-postgres` are both live on Railiance01 — a hand-maintained registry
|
|
with nothing checking it
|
|
- roughly 17 live non-system workloads in-cluster against 3 rapps
|
|
|
|
The `rail-*` family received a tighter contract in wave 1 and has stayed clean.
|
|
That contrast is the argument for schema rather than for more documentation.
|
|
|
|
## Current Starting Point
|
|
|
|
- no `schemas/` directory exists in this repo
|
|
- `docs/repo-family-bootstrap-contract.md` lists eight required `rapp.yaml`
|
|
fields with no shapes, no types, and no cardinality rules
|
|
- `docs/rapp-first-wave-candidates.md` still reads as though the planned first
|
|
wave (`rapp-openbao`, `rapp-vergabe-teilnahme`, `rapp-forgejo`) happened; the
|
|
actual population is `rapp-openbao`, `rapp-postgres`, `rapp-qonto`, the
|
|
latter two created ad hoc from immediate need
|
|
- `rapp-vergabe-teilnahme` was never built, so the user-facing wrapper pattern
|
|
it existed to prove remains unproven while the platform-service pattern has
|
|
been proven twice
|
|
- canon does not yet admit the family prefixes, so `fix-consistency` cannot
|
|
call a family validator until `the-custodian` lands its side
|
|
- `railiance-platform` is holding `RAILIANCE-WP-0015-T02` (bringing
|
|
`rapp-openbao` and `rapp-postgres` onto one shape) until this schema exists
|
|
|
|
## Target Outcome
|
|
|
|
When this workplan is complete:
|
|
|
|
1. `schemas/rapp.schema.json` defines every required and optional `rapp.yaml`
|
|
field with one normative shape per contract.
|
|
2. A `composition:` block expresses what a grouped rapp actually is: a stated
|
|
purpose, first-party member repos, and pinned third-party components.
|
|
3. The two cardinalities are written down and enforced separately — repos are
|
|
many-to-many with rapps, deployables are one-to-one.
|
|
4. A validator runs over `rail.yaml`, `rapp.yaml`, and `reef.yaml` and fails on
|
|
drift.
|
|
5. Reef `bound_rapps` is derived from rapp declarations rather than
|
|
hand-listed.
|
|
6. `docs/rapp-first-wave-candidates.md` records what was actually built versus
|
|
planned, and carries the grouped wave-2 inventory.
|
|
7. Downstream repos have a single shape to converge on, and know it.
|
|
|
|
## Boundaries
|
|
|
|
This workplan owns the schema, the validator, the cardinality rules, and the
|
|
framework documents that describe them.
|
|
|
|
It does not migrate any rapp repo onto the new shape — that stays with each
|
|
owning repo, coordinated through `railiance-platform` `RAILIANCE-WP-0015-T02`.
|
|
|
|
It does not touch canon. Admitting the family prefixes into
|
|
`repo-classification-standard_v1.0` is `the-custodian`'s call, routed there
|
|
separately as `cc64764a`.
|
|
|
|
It does not address the Forgejo organization split. All repos currently sit
|
|
under a single `coulomb/` org and a split into stewardship orgs is a
|
|
URL-breaking change across every remote, CI config and ArgoCD source. It is an
|
|
independent dimension from rapp grouping and needs its own work record.
|
|
|
|
## Tasks
|
|
|
|
## T01 - Accept the routed proposal and record the ownership decision
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T01
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "af45d82d-c1e3-47b3-81ae-2974bc65cc5d"
|
|
```
|
|
|
|
Read and acknowledge State Hub messages `04c776c4` and `f88f938d`, mark them
|
|
read, and reply to `railiance-platform` confirming that `railiance-master`
|
|
accepts ownership of the schema and that this workplan is the vehicle.
|
|
|
|
Record the framework decision that rapp bounded context is a grouping dimension
|
|
in its own right, derived neither from Forgejo organizations nor from State Hub
|
|
domains, with the cardinality argument as rationale: a repo lives in exactly
|
|
one Forgejo org, so `org:repo` is one-to-many, while a repo may legitimately
|
|
contribute to more than one rapp, so `rapp:repo` is many-to-many — and 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.
|
|
|
|
Reference OAS P1 in `canon/standards/orthogonal-architecture_v1.0.md`: separate
|
|
independent perspectives into orthogonal dimensions.
|
|
|
|
**Done 2026-08-11.** Both messages marked read; reply `a9a8109f` sent to
|
|
`railiance-platform` accepting ownership and confirming this workplan as the
|
|
vehicle. No decision record was created here: `d07ee5f9` already carries the
|
|
dimension decision from the `railiance-platform` side, and duplicating it would
|
|
create two records of one decision. The rationale lands instead in `ADR-0007`
|
|
under T07. One question was returned to `railiance-platform` — whether they can
|
|
emit the live deployable inventory that T06 needs, so this repo does not
|
|
acquire a cluster dependency.
|
|
|
|
## T02 - Author schemas/rapp.schema.json
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "87ae4551-1fac-4613-b258-28c8b22bbb82"
|
|
```
|
|
|
|
Create `schemas/` and write `rapp.schema.json` covering:
|
|
|
|
- the eight fields already required by the bootstrap contract: `rapp_id`,
|
|
`workload_identity`, `ownership_repo`, `supported_rails`,
|
|
`runtime_dependencies`, `rollout_contract`, `smoke_contract`,
|
|
`rollback_contract`
|
|
- `contract_version` and `readiness_state`, matching the shape the rail family
|
|
already carries
|
|
- `data_classification` and `criticality`, promoted from `rapp-qonto`-only to
|
|
required
|
|
- `consumer_contract`, promoted from `rapp-postgres`-only to optional but
|
|
schema-defined
|
|
|
|
Settle the two definitional questions the survey exposed. `workload_identity.name`
|
|
is the workload, never the repo. `ownership_repo` names the repo that owns the
|
|
workload's lifecycle; state explicitly whether a layer repo is a legal value,
|
|
since current declarations disagree.
|
|
|
|
Pick one normative shape for `rollout_contract`, `smoke_contract` and
|
|
`rollback_contract` rather than admitting all three observed variants. Note in
|
|
the schema which existing declaration the chosen shape most resembles, so
|
|
migration cost is visible.
|
|
|
|
**Done 2026-08-11.** `schemas/rapp.schema.json` (draft 2020-12) and
|
|
`schemas/README.md` written from the three live declarations rather than from
|
|
the survey summary, which turned out to understate the drift — see the extra
|
|
finding below.
|
|
|
|
Normative choices, with who pays:
|
|
|
|
- `default_mode` kept; all three declarations already carried it. No cost.
|
|
- `supported_commands` → `commands` in all three contracts. Mechanical.
|
|
- `smoke_contract.required` is required, `commands` optional — taken from
|
|
`rapp-qonto`. This is the one migration with real cost: the platform rapps
|
|
declare only commands and their owners must now name the outcomes those
|
|
commands establish. A command list records what was run; a required list
|
|
records what must be true, and only the second is checkable by anyone other
|
|
than the person who ran it.
|
|
- `rollback_contract.order` required, also from `rapp-qonto`, recoverable from
|
|
the command sequences already documented. `direct_kubernetes_fallback`
|
|
generalizes to `fallback`.
|
|
- `workload_identity.name` is the workload, never the repo; the schema forbids
|
|
the `rapp-` prefix outright.
|
|
- `contract_version`, `readiness_state`, `data_classification`, `criticality`
|
|
all promoted to required.
|
|
- `consumer_contract` schema-defined but optional.
|
|
|
|
`ownership_repo` is left permissive — exactly one repo, not the rapp repo
|
|
itself, layer and application repos both legal — so the schema does not
|
|
silently invalidate two of three live declarations on a question the survey
|
|
cannot settle. The tighter alternative and its consequence are written up in
|
|
`schemas/README.md` for the architecture owner. Treat the permissive rule as
|
|
provisional.
|
|
|
|
Validated against all three live declarations with `jsonschema` 4.10.3. The
|
|
schema is well-formed and reproduces precisely the drift the survey reported,
|
|
which is the intended result — it fails on real drift and not on anything else:
|
|
|
|
| Declaration | Errors | Character |
|
|
|---|---|---|
|
|
| `rapp-openbao` | 10 | four missing metadata fields, no composition, three contracts on the old shape |
|
|
| `rapp-postgres` | 10 | identical set — the two platform rapps drifted together |
|
|
| `rapp-qonto` | 4 | composition, `direct_kubernetes_fallback`, missing `package_type`, and `workload_identity.name` = `rapp-qonto` |
|
|
|
|
**Extra finding, not in the routed survey.** There are six `rapp-*` repos, not
|
|
three: `rapp-secrets-engine`, `rapp-tenant-engine` and `rapp-user-engine` exist
|
|
and carry no `declarations/` directory at all. The survey counted only declared
|
|
rapps, so the real gap is wider than the roughly 17-workloads-versus-3-rapps
|
|
figure suggests — three further repos claim the family prefix while declaring
|
|
nothing. Raised to `railiance-platform`; whether these are stubs, abandoned, or
|
|
undeclared live workloads should be settled before T06 draws its coverage
|
|
baseline, since undeclared repos are invisible to a coverage check that reads
|
|
declarations.
|
|
|
|
## T03 - Define the composition block and its cardinality rules
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T03
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "8b130ff8-0601-40bb-8539-d6f5847c2f41"
|
|
```
|
|
|
|
Supersede the flat `members:` list from the original proposal with the
|
|
composition block from the amendment:
|
|
|
|
```yaml
|
|
composition:
|
|
purpose: <what this coherent workload does>
|
|
member_repos: # first-party, each sharing rollout and rollback fate
|
|
- repo: <slug>
|
|
role: <what it contributes>
|
|
deployables: [<names>]
|
|
upstream_components: # third-party, pinned
|
|
- name: <chart|image|operator>
|
|
source: <repo/chart or registry ref>
|
|
version: <pin>
|
|
```
|
|
|
|
Encode both cardinalities. A repo may appear in the `member_repos` of more than
|
|
one rapp. A deployable must belong to exactly one rapp — each running
|
|
deployable has exactly one rapp owning its rollout. Keeping these distinct is
|
|
what makes the coverage check in T06 well-defined.
|
|
|
|
Carry the operator decision of 2026-08-11 into the schema: rapp granularity is
|
|
grouped-by-bounded-context, one rapp per cohesive group that deploys, versions
|
|
and rolls back together, not one per deployable. Grouping is legitimate only
|
|
where members share rollout and rollback fate; make that testable rather than
|
|
advisory where possible.
|
|
|
|
**Done 2026-08-11.** `composition` is required on every rapp, in
|
|
`schemas/rapp.schema.json` under `$defs/composition`. A single-repo rapp
|
|
declares exactly one member, so there is no second shape for the ungrouped
|
|
case — the composition block is the only way to state membership.
|
|
|
|
`purpose` carries a `minLength` and is documented as the bounded-context
|
|
statement: if it cannot be written without "and also", the grouping is probably
|
|
wrong. That is the closest this schema gets to testing shared rollout fate;
|
|
the substantive check is structural, in that `deployables` is required on every
|
|
member, which is what T06 consumes.
|
|
|
|
Both cardinalities are encoded where they bite. `member_repos[].repo` is
|
|
unconstrained across rapps, so a repo may appear in several — many-to-many.
|
|
`member_repos[].deployables` is documented as one-to-one and enforced by the
|
|
validator's coverage check rather than by the schema, since no single-file
|
|
schema can see across declarations. `upstream_components` requires an exact
|
|
`version` pin, with the schema stating that ranges and floating tags are not
|
|
pins.
|
|
|
|
`workload_identity.package_type` gains a `grouped-composition` value for rapps
|
|
whose members carry their own package types.
|
|
|
|
## T04 - Schematize rail.yaml and reef.yaml, and derive bound_rapps
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T04
|
|
status: todo
|
|
priority: medium
|
|
state_hub_task_id: "50ffbff9-f8f0-4085-975a-6c5123d9c2f0"
|
|
```
|
|
|
|
Write `schemas/rail.schema.json` and `schemas/reef.schema.json` from the
|
|
minimum field sets in `docs/repo-family-bootstrap-contract.md`. The rail family
|
|
has stayed clean, so this is largely codifying what already works; do not
|
|
tighten it beyond current practice without cause.
|
|
|
|
Change `reef.yaml` `bound_rapps` from a hand-maintained list to a derived
|
|
projection of the rapp declarations that name the reef, and record the
|
|
migration path for `reef-railiance`, whose current list is already known stale.
|
|
|
|
## T05 - Build the family declaration validator
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T05
|
|
status: todo
|
|
priority: high
|
|
state_hub_task_id: "0a212d33-2169-4472-80d0-69ec4edba5cb"
|
|
```
|
|
|
|
Add a validator under `tools/` that walks the family repos, locates each
|
|
`declarations/*.yaml`, and validates it against the matching schema.
|
|
|
|
Checks in priority order, per the routed proposal:
|
|
|
|
1. every declaration conforms to its schema
|
|
2. every declared member repo and upstream pin resolves
|
|
3. reef `bound_rapps` is derivable from rapp declarations rather than
|
|
hand-listed
|
|
4. deployable coverage — deferred to T06, which needs cluster state
|
|
|
|
Exit non-zero on any failure and name the offending file and field. Make it
|
|
runnable standalone now, and callable from `fix-consistency` later; do not
|
|
block on canon admitting the prefixes, since that is `the-custodian`'s
|
|
sequencing, not ours.
|
|
|
|
## T06 - Add the deployable coverage check
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T06
|
|
status: todo
|
|
priority: medium
|
|
state_hub_task_id: "519c2838-c4af-46e9-81fa-612fb09e91e3"
|
|
```
|
|
|
|
Extend the validator with the standing control-loop question: does every live
|
|
deployable belong to exactly one rapp?
|
|
|
|
This is the check that turns the schema from a one-shot lint into an ongoing
|
|
control loop. Per the amendment it catches all three drift findings at once —
|
|
stale reef bindings, unpackaged live workloads, and the roughly
|
|
17-workloads-versus-3-rapps gap.
|
|
|
|
Decide how live cluster state reaches the check without `railiance-master`
|
|
acquiring a cluster dependency it should not have. Consuming an inventory
|
|
produced by an implementation repo is preferable to querying Railiance01 from
|
|
here.
|
|
|
|
Expect the first run to report a large uncovered set. That output is the wave-2
|
|
worklist, not a failure to suppress.
|
|
|
|
## T07 - Update the bootstrap contract to point at the schemas
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T07
|
|
status: todo
|
|
priority: medium
|
|
state_hub_task_id: "aa71bc2f-7d9e-4351-8130-8b6e23b8ee36"
|
|
```
|
|
|
|
Revise `docs/repo-family-bootstrap-contract.md` so the minimum-field lists cite
|
|
the schema files as normative rather than restating fields in prose. Prose that
|
|
duplicates a schema will drift from it.
|
|
|
|
Record an ADR (next free number, `ADR-0007`) for the framework decisions this
|
|
workplan settles: bounded rapp context as an independent dimension, the two
|
|
cardinalities, and grouped-by-bounded-context granularity.
|
|
|
|
## T08 - Refresh rapp-first-wave-candidates.md
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T08
|
|
status: todo
|
|
priority: medium
|
|
state_hub_task_id: "28e4ef82-62ea-41d2-a059-05c047292240"
|
|
```
|
|
|
|
Correct the document to record what was actually built against what was
|
|
planned: `rapp-openbao` as planned, then `rapp-postgres` and `rapp-qonto` ad
|
|
hoc from need, and `rapp-vergabe-teilnahme` never built.
|
|
|
|
State the consequence plainly — the platform-service pattern is proven twice
|
|
and the user-facing wrapper pattern is unproven — and carry the operator
|
|
decision to build `rapp-vergabe-teilnahme` to prove the user-facing shape
|
|
before mass extraction.
|
|
|
|
Add the grouped wave-2 inventory in the new composition shape. Indicative
|
|
candidates: `rapp-netkingdom-identity` (sso, mfa, user-engine, tenant-engine,
|
|
flex-auth), `rapp-forgejo` (forgejo, runner, registry), `rapp-coulomb-social`,
|
|
`rapp-helix-forge`.
|
|
|
|
## T09 - Release the schema to downstream repos
|
|
|
|
```task
|
|
id: RAILIANCE-WP-0021-T09
|
|
status: wait
|
|
priority: high
|
|
state_hub_task_id: "b8dc2452-8ec0-4301-98b7-4ec96411f428"
|
|
```
|
|
|
|
Waiting on T02, T03 and T05.
|
|
|
|
Notify `railiance-platform` that the shape is settled and
|
|
`RAILIANCE-WP-0015-T02` is unblocked, and notify `rapp-openbao`,
|
|
`rapp-postgres`, `rapp-qonto` and `reef-railiance` of the migration they each
|
|
own. Include the normative shape chosen for the rollout, smoke and rollback
|
|
contracts and what each repo must change.
|
|
|
|
Migration itself belongs to the owning repos. This task ends when they have
|
|
what they need, not when they are done.
|