Add the exposure contract, additive family schema fields, validator checks and fixtures, the reef-railiance exception snapshot, and routed intakes. Enforcement stays in the owning repos.
197 lines
9.5 KiB
Markdown
197 lines
9.5 KiB
Markdown
# 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.
|
|
|
|
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_rail` must appear in `supported_rails` (rapp) or, when set, in `hosted_rails` (reef). A reef may omit both when it hosts no rail.
|
|
- reef `bound_rapps`, if present, must match the projection of `rapp.bound_reefs`
|
|
- a rapp `ownership_repo` must 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: public` on a rapp requires `exposure.grant`,
|
|
`binding_admission: production-approved`, and a bound reef that has
|
|
granted a public surface (ADR-0008)
|
|
- `exposure.posture: public` on a rail requires `exposure.grant` and
|
|
`readiness_state: production-approved`
|
|
- `exposure.posture: public` on a reef requires `exposure.grants`; a
|
|
provider-delegated reef (no rail) cannot be public
|
|
- grant port `6443` is never legal
|
|
- omitted `exposure` means `private`; the field is additive and must
|
|
not be confused with rapp `data_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
|
|
|
|
```bash
|
|
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.
|