Rename RAILIANCE-WP-0017..0021 to RMASTER-WP-* so railiance-master IDs no longer collide with railiance-platform's RAILIANCE-WP series. Hub UUIDs are unchanged.
22 KiB
| id | type | title | domain | repo | status | owner | topic_slug | planning_priority | planning_order | created | updated | related_repos | state_hub_workstream_id | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| RMASTER-WP-0021 | workplan | Rapp Declaration Schema and Family Declaration Validator | financials | railiance-master | finished | codex | railiance | high | 21 | 2026-08-11 | 2026-08-13 |
|
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-openbaoandrapp-postgresusesupported_commandslists,rapp-qontousesdefault_mode/production_gate, arequired:smoke list and anorder:rollback list contract_version,readiness_state,data_classificationandcriticalityappear only inrapp-qonto, though both rails carrycontract_version/readiness_stateconsistentlyconsumer_contract/consumersexist only inrapp-postgresworkload_identity.nameis the workload name in two rapps and the repo name in the thirdownership_repopoints at a layer repo twice and at an app repo oncereef-railiancebound_rappslistsrapp-qontoonly, whilerapp-openbaoandrapp-postgresare 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.mdlists eight requiredrapp.yamlfields with no shapes, no types, and no cardinality rulesdocs/rapp-first-wave-candidates.mdstill reads as though the planned first wave (rapp-openbao,rapp-vergabe-teilnahme,rapp-forgejo) happened; the actual population israpp-openbao,rapp-postgres,rapp-qonto, the latter two created ad hoc from immediate needrapp-vergabe-teilnahmewas 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-consistencycannot call a family validator untilthe-custodianlands its side railiance-platformis holdingRAILIANCE-WP-0015-T02(bringingrapp-openbaoandrapp-postgresonto one shape) until this schema exists
Target Outcome
When this workplan is complete:
schemas/rapp.schema.jsondefines every required and optionalrapp.yamlfield with one normative shape per contract.- A
composition:block expresses what a grouped rapp actually is: a stated purpose, first-party member repos, and pinned third-party components. - The two cardinalities are written down and enforced separately — repos are many-to-many with rapps, deployables are one-to-one.
- A validator runs over
rail.yaml,rapp.yaml, andreef.yamland fails on drift. - Reef
bound_rappsis derived from rapp declarations rather than hand-listed. docs/rapp-first-wave-candidates.mdrecords what was actually built versus planned, and carries the grouped wave-2 inventory.- 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
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
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_versionandreadiness_state, matching the shape the rail family already carriesdata_classificationandcriticality, promoted fromrapp-qonto-only to requiredconsumer_contract, promoted fromrapp-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_modekept; all three declarations already carried it. No cost.supported_commands→commandsin all three contracts. Mechanical.smoke_contract.requiredis required,commandsoptional — taken fromrapp-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.orderrequired, also fromrapp-qonto, recoverable from the command sequences already documented.direct_kubernetes_fallbackgeneralizes tofallback.workload_identity.nameis the workload, never the repo; the schema forbids therapp-prefix outright.contract_version,readiness_state,data_classification,criticalityall promoted to required.consumer_contractschema-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
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:
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
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
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:
- every declaration conforms to its schema
- every declared member repo and upstream pin resolves
- reef
bound_rappsis derivable from rapp declarations rather than hand-listed - 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
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
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
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
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) |