--- id: RMASTER-WP-0021 type: workplan title: "Rapp Declaration Schema and Family Declaration Validator" domain: financials repo: railiance-master status: finished owner: codex topic_slug: railiance planning_priority: high planning_order: 21 created: "2026-08-11" updated: "2026-08-13" 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" --- # RMASTER-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 `RMASTER-WP-0017` through `RMASTER-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: RMASTER-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: RMASTER-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: RMASTER-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: member_repos: # first-party, each sharing rollout and rollback fate - repo: role: deployables: [] upstream_components: # third-party, pinned - name: source: version: ``` 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: RMASTER-WP-0021-T04 status: done 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. **Done 2026-08-13.** `schemas/rail.schema.json` and `schemas/reef.schema.json` written from the live declarations and the composition/substrate contracts, not from the bootstrap field lists (those lists used an older `substrate_prerequisites` name the live rails do not carry). Both live rails conform. The live reef conforms as a document. `bound_rapps` is optional and derived from a new required `rapp.bound_reefs` field (empty list means declared but not placed). The live `reef-railiance` hand-list `[rapp-qonto]` fails the derivation check, which is the intended result. Migration for that repo: delete the field now, or replace it with the derived set once the three live rapps name the reef. Adjacent ownership, recorded so it is not mistaken for a schema hole: the postgres backup object store is a consumed capability (`s3-backup-target`), not a rapp-owned resource. Bucket = `resource-control` / `RESOURCE-WP-0002`. Credential = `railiance-platform` OpenBao `Secret platform-pg-backup-s3`. Purchase = decision `9c21c0e0`. `make postgres-backup-deploy` stays fail-closed until that handoff lands. ## T05 - Build the family declaration validator ```task id: RMASTER-WP-0021-T05 status: done 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. **Done 2026-08-13.** `tools/validate-family-declarations.py` walks `rail-*` / `rapp-*` / `reef-*` siblings of `--root` (default: parent of this repo). `--self-test` passes against `tools/testdata/family-declarations`. First live scan of `/home/worsch` (no inventory): 6 declarations, 31 errors. The two rails are clean. The three declared rapps fail on the already-known shape drift plus the new `bound_reefs` field. The reef fails only on stale `bound_rapps`. The three undeclared engine repos fail as prefix-without-declaration. That is exactly the drift the schema exists to make loud. ## T06 - Add the deployable coverage check ```task id: RMASTER-WP-0021-T06 status: done 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. **Done 2026-08-13.** The validator takes `--inventory PATH`. This repo does not query a cluster. The file is a JSON mapping with `source`, `captured_at`, optional `reef`, and `deployables` as a list of `{name, namespace?, kind?}` (bare name strings also accepted). Live names that no rapp claims are errors labelled as wave-2 worklist items. Declared names missing from the inventory are warnings. The first live run is not in this repo: it needs `railiance-platform` to emit the inventory T01 asked for. The check itself is implemented and covered by `--self-test`. ## T07 - Update the bootstrap contract to point at the schemas ```task id: RMASTER-WP-0021-T07 status: done 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. **Done 2026-08-13.** Bootstrap contract now points at the three schema files and refuses to restate their fields. `docs/adr/ADR-0007-rapp-declaration-contract.md` records the dimension, the two cardinalities, grouped-by-bounded-context granularity, and that the schemas are normative. Decision `d07ee5f9` remains the platform-side record; this ADR is the framework record, not a second decision. ## T08 - Refresh rapp-first-wave-candidates.md ```task id: RMASTER-WP-0021-T08 status: done 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`. **Done 2026-08-13.** The document now leads with the plan-versus-outcome table, states that the user-facing wrapper is unproven, keeps the 2026-07-25 assessment as history, and carries the wave-2 inventory in composition shape. The three undeclared engine repos are named so they are not invented again as separate rapps. `rapp-forgejo` placement cites `d151d817`. ## T09 - Release the schema to downstream repos ```task id: RMASTER-WP-0021-T09 status: done 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. **Done 2026-08-13.** Release messages sent. Downstream now has the shape and the per-repo change list; migration itself is not this task. | To | Message | What they received | |---|---|---| | `railiance-platform` | `072c5820` | WP-0015-T02 unblocked; inventory shape for T06; S3 split restated | | `rapp-openbao` | `093f2f73` | 11-error migration (metadata, composition, `bound_reefs`, contract rename) | | `rapp-postgres` | `7b1cb9b7` | same 11-error set; S3 is a consumed destination, not a hole | | `rapp-qonto` | `16742192` | 5-error migration (`composition`, `bound_reefs`, `package_type`, name, `fallback`) | | `reef-railiance` | `6b395e26` | delete or replace stale `bound_rapps` | | `resource-control` | `aa8393e3` | coordination only: bucket is theirs (`RESOURCE-WP-0002`) |