Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a02b90-83bf-75c2-81c8-aa705414e4d4 |
||
|---|---|---|
| .. | ||
| rail.schema.json | ||
| rapp.schema.json | ||
| README.md | ||
| reef.schema.json | ||
Railiance family declaration schemas
Normative schemas for the repo-family declaration files. docs/repo-family-bootstrap-contract.md
names the minimum fields; these schemas define their shapes and are the
authority where the two disagree.
| Family | Declaration path | Schema |
|---|---|---|
rapp-* |
declarations/rapp.yaml |
rapp.schema.json |
rail-* |
declarations/rail.yaml |
rail.schema.json |
reef-* |
declarations/reef.yaml |
reef.schema.json |
Written under RMASTER-WP-0021 from the drift survey routed by
railiance-platform under its RAILIANCE-WP-0015 (State Hub messages
04c776c4, f88f938d).
The two cardinalities
These are the load-bearing rules. Everything the validator can usefully check follows from keeping them distinct.
- Repos to rapps is many-to-many. A repo may contribute to more than one rapp — a shared library, or a service that is a member of one bundle and a dependency of another.
- Deployables to rapps is one-to-one. Every running deployable has exactly one rapp that owns its rollout.
The second is what makes the coverage question well-formed: does every live deployable belong to exactly one rapp? That single check catches stale reef bindings, unpackaged live workloads, and the workload-count gap at once. It is what turns this schema from a one-shot lint into a standing control loop.
Coverage boundary
Every managed running deployable needs exactly one authoritative rapp. The rule applies equally to application services and to operational or tooling runtimes that participate in controls. A long-lived controller, scheduled tool, or managed one-shot Job is still a deployable; it is not exempt because its purpose is operational.
Not every operational subject is a workload. Human access, credential patterns, broker actions, approval records, one-off commands, and infrastructure resources retain their native actor, lane, activity, decision, or resource identity when they are not themselves managed deployables. Do not create a fake rapp merely to make a consumer join total.
Consumers must preserve three distinct outcomes:
| Outcome | Meaning | Required handling |
|---|---|---|
resolved |
Exactly one authoritative rapp claims the managed deployable. | Use its explicit rapp and workload reference. |
unknown |
The subject should be a workload, but no unique declaration resolves. | Report migration debt; do not infer identity. |
not-applicable |
The subject is explicitly not a workload. | Retain its native non-workload identity. |
A pre-rapp running deployable is therefore migration debt and remains
unknown to workload-based controls until declared. Repository names,
namespaces, paths, labels, and apparent ownership are evidence inputs, not
fallback identity rules.
Railiance Master owns this vocabulary and the normative family schemas. Package owners author their declarations; integration owners may discover, validate, index, project, and resolve explicit references. They must not create a parallel generic workload declaration or copy rapp metadata into a second source of truth.
Rapp bounded context is therefore its own dimension, derived neither from
Forgejo organizations nor from State Hub domains. A repo lives in exactly one
Forgejo org, so org:repo is one-to-many, and a many-to-many grouping cannot
be derived from a one-to-many one. Domains fail in both directions. See
docs/adr/ADR-0007-rapp-declaration-contract.md and decision d07ee5f9.
Normative choices, and what they cost
The three live rapp declarations expressed their rollout, smoke and rollback contracts three mutually unreadable ways. The schema picks one shape. Where it picks, it says so and it says who pays.
default_mode is kept — all three declarations already carried it. No cost.
supported_commands becomes commands in all three contracts. Mechanical
rename for rapp-openbao and rapp-postgres.
smoke_contract.required is required, and commands is optional. Taken
from rapp-qonto. This is the one migration with real cost: the platform rapps
declare only commands, so their owners must name the outcomes those commands
establish. That cost is the point. A command list records what was run; a
required list records what must be true. Only the second can be checked by
anything other than the person who ran it.
rollback_contract.order is required. Taken from rapp-qonto. Recoverable
from the command sequences the platform rapps already document, so the cost is
low. direct_kubernetes_fallback generalizes to fallback.
workload_identity.name is the workload, never the repo, and the schema
forbids the rapp- prefix on it outright. rapp-qonto currently sets it to
rapp-qonto; the correct value is qonto.
contract_version, readiness_state, data_classification and
criticality are required. All four exist only in rapp-qonto today. The
first two are promoted because the rail family has carried them consistently in
both rails and has not drifted — that contrast is the whole argument for
schema. The second two are promoted because a package whose classification and
criticality are unstated cannot be admitted to a reef on evidence.
consumer_contract is schema-defined but optional. Only rapp-postgres
provisions isolated units to other services. Requiring it everywhere would be
false precision.
Open question: what may ownership_repo name?
Currently rapp-openbao and rapp-postgres name railiance-platform, a layer
repo, and rapp-qonto names qonto-assistant, an application repo.
The schema admits both, requiring only that there be exactly one and that it not be the rapp repo itself. This is deliberately the permissive reading, taken so the schema does not silently invalidate two of three live declarations on a question the survey cannot settle.
The tighter alternative is that ownership_repo must name the repo whose team
is accountable for the workload in production, which would likely disqualify a
layer repo that owns dozens of unrelated workloads. That is a modelling call
about where accountability sits, not a lint, and it belongs to the architecture
owner. Until it is made, treat the permissive rule as provisional.
Validator-only constraints
JSON Schema cannot express these; tools/validate-family-declarations.py does:
primary_railmust appear insupported_rails(rapp) or, when set, inhosted_rails(reef). A reef may omit both when it hosts no rail.- reef
bound_rapps, if present, must match the projection ofrapp.bound_reefs - a rapp
ownership_repomust not be the rapp repo itself - declared member repos, named rails, and named reefs must resolve on disk
- a deployable name may belong to at most one rapp
exposure.posture: publicon a rapp requiresexposure.grant,binding_admission: production-approved, and a bound reef that has granted a public surface (ADR-0008)exposure.posture: publicon a rail requiresexposure.grantandreadiness_state: production-approvedexposure.posture: publicon a reef requiresexposure.grants; a provider-delegated reef (no rail) cannot be public- grant port
6443is never legal - omitted
exposuremeansprivate; the field is additive and must not be confused with rappdata_classification: public
Rail schema (T04)
rail.schema.json codifies the two live rails. It does not tighten the family
beyond current practice.
Required on every rail: kind, rail_id, repo, ownership_repo,
contract_version, composition_kind, execution_architecture,
readiness_state, required_substrate_capabilities, supported_rollout_modes.
composition_kind: derived additionally requires base_rail,
base_rail_contract, inherited_semantics, overridden_semantics, and
compatibility_constraints. A base rail must not carry those fields.
required_substrate_capabilities is the live name. The bootstrap contract's
older substrate_prerequisites label is retired; do not reintroduce it.
Rail readiness_state uses the composition-contract vocabulary (declared,
installed, verified, production-approved, deprecated). That is
deliberately not the rapp vocabulary and not the reef lifecycle_state.
Validated 2026-08-13 against both live rails with jsonschema 4.10.3: both
conform. That is the intended contrast with the rapp family.
Reef schema (T04)
reef.schema.json codifies the live reef-railiance declaration, with one
normative change: bound_rapps is no longer a hand-maintained required field.
Required on every reef: kind, reef_id, repo, ownership_repo,
substrate_kind, lifecycle_state, criticality, current_members.
primary_rail and hosted_rails are required only when the reef hosts
rails. current_members may be empty on a planned provider-delegated
reef.
bound_rapps is optional. Its source of truth is rapp.bound_reefs. The
validator inverts that field. If a reef still lists bound_rapps, the list
must match the projection exactly.
Migration for reef-railiance. The current list is [rapp-qonto]. That
is already stale: rapp-openbao and rapp-postgres are live on Railiance01.
Until those three declarations name reef-railiance in bound_reefs, the
derived projection is empty and a hand-list of anything fails. The owning
repo should delete bound_rapps now, or replace it with the derived set once
the rapps name the reef. Either is a one-line change; leaving the stale list
is the failure mode this check exists to catch.
rapp.bound_reefs was added as a required field in the same task. An empty
list is legal and means the package is declared but not yet placed.
Validated 2026-08-13 against the live reef: it conforms as a document. The
bound_rapps value does not survive the derivation check, which is the
intended result.
Runtime dependencies name capabilities, not resources
runtime_dependencies is a list of capability names (s3-backup-target,
openbao-database-secrets-engine). The schema does not, and must not, say
who provisions the thing that satisfies a name.
The postgres backup object store is the worked example. rapp-postgres
declares s3-backup-target as a consumed capability. The bucket is a
resource-control resource (RESOURCE-WP-0002). The credential is
railiance-platform OpenBao custody (Secret platform-pg-backup-s3).
Purchase itself is the escalated human decision 9c21c0e0. Until
resource-control hands an endpoint to railiance-platform,
make postgres-backup-deploy stays fail-closed on purpose. That is not a
hole in rapp-postgres ownership and not a hole in this schema.
Running the validator
tools/validate-family-declarations.py
tools/validate-family-declarations.py --root ..
tools/validate-family-declarations.py --inventory path/to/live-deployables.json
tools/validate-family-declarations.py --self-test
--inventory is the T06 coverage check. This repo does not query a cluster.
The file is produced by an implementation repo and must be a JSON mapping
with a deployables list of {name, namespace?, kind?} (or bare name
strings). Live names that no rapp claims are reported as wave-2 worklist
items, not suppressed.
The validator is tools/validate-family-declarations.py (RMASTER-WP-0021-T05).
It is runnable standalone now. Calling it from fix-consistency waits on
the-custodian admitting the family prefixes; that sequencing is not ours.