Finish RAILIANCE-WP-0021: family schemas, validator, and ADR-0007
Add rail and reef schemas, derive reef bound_rapps from rapp.bound_reefs, and ship a standalone family-declaration validator with an inventory-fed coverage check. Point the bootstrap contract at the schemas, record the dimension and cardinality decisions in ADR-0007, correct the first-wave candidates document, and release the shape to downstream repos.
This commit is contained in:
parent
d3b3512a60
commit
3cc0dc31d6
20 changed files with 1509 additions and 187 deletions
|
|
@ -7,8 +7,8 @@ authority where the two disagree.
|
|||
| Family | Declaration path | Schema |
|
||||
|---|---|---|
|
||||
| `rapp-*` | `declarations/rapp.yaml` | `rapp.schema.json` |
|
||||
| `rail-*` | `declarations/rail.yaml` | `rail.schema.json` (RAILIANCE-WP-0021-T04) |
|
||||
| `reef-*` | `declarations/reef.yaml` | `reef.schema.json` (RAILIANCE-WP-0021-T04) |
|
||||
| `rail-*` | `declarations/rail.yaml` | `rail.schema.json` |
|
||||
| `reef-*` | `declarations/reef.yaml` | `reef.schema.json` |
|
||||
|
||||
Written under `RAILIANCE-WP-0021` from the drift survey routed by
|
||||
`railiance-platform` under its `RAILIANCE-WP-0015` (State Hub messages
|
||||
|
|
@ -34,7 +34,7 @@ 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` (T07) and decision `d07ee5f9`.
|
||||
`docs/adr/ADR-0007-rapp-declaration-contract.md` and decision `d07ee5f9`.
|
||||
|
||||
## Normative choices, and what they cost
|
||||
|
||||
|
|
@ -89,12 +89,97 @@ 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.
|
||||
|
||||
## Validating
|
||||
## Validator-only constraints
|
||||
|
||||
The validator lands under `tools/` in `RAILIANCE-WP-0021-T05`. Until then these
|
||||
schemas are normative but unenforced — which is precisely the condition that
|
||||
produced the drift, so T05 should not lag T02/T03 by long.
|
||||
JSON Schema cannot express these; `tools/validate-family-declarations.py` does:
|
||||
|
||||
Two constraints are validator-only, because JSON Schema cannot express them:
|
||||
`primary_rail` must appear in `supported_rails`, and reef `bound_rapps` must be
|
||||
derivable from the rapp declarations rather than hand-listed.
|
||||
- `primary_rail` must appear in `supported_rails` (rapp) or `hosted_rails` (reef)
|
||||
- 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
|
||||
|
||||
## 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`, `primary_rail`,
|
||||
`hosted_rails`, `current_members`.
|
||||
|
||||
`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` (RAILIANCE-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.
|
||||
|
|
|
|||
178
schemas/rail.schema.json
Normal file
178
schemas/rail.schema.json
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://railiance.dev/schemas/rail.schema.json",
|
||||
"title": "Railiance rail declaration",
|
||||
"description": "Normative shape for declarations/rail.yaml in every rail-* repo. Authored by railiance-master under RAILIANCE-WP-0021-T04 from the two live rails and docs/rail-composition-contract.md. Codifies current practice; it does not tighten the rail family beyond what both live declarations already carry.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"kind",
|
||||
"rail_id",
|
||||
"repo",
|
||||
"ownership_repo",
|
||||
"contract_version",
|
||||
"composition_kind",
|
||||
"execution_architecture",
|
||||
"readiness_state",
|
||||
"required_substrate_capabilities",
|
||||
"supported_rollout_modes"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "execution-rail",
|
||||
"description": "Fixed discriminator. Both live rails already agree on this value."
|
||||
},
|
||||
"rail_id": {
|
||||
"$ref": "#/$defs/railSlug",
|
||||
"description": "Canonical identifier for the rail. Must equal the repo slug."
|
||||
},
|
||||
"repo": {
|
||||
"$ref": "#/$defs/repoSlug",
|
||||
"description": "The rail-* repo holding this declaration."
|
||||
},
|
||||
"ownership_repo": {
|
||||
"$ref": "#/$defs/repoSlug",
|
||||
"description": "The ownership-axis repo accountable for this execution contract. Both live rails name railiance-cluster."
|
||||
},
|
||||
"contract_version": {
|
||||
"$ref": "#/$defs/semver",
|
||||
"description": "Version of this rail contract. A derived rail pins the base contract with base_rail_contract, which is a separate field."
|
||||
},
|
||||
"composition_kind": {
|
||||
"enum": ["base", "derived"],
|
||||
"description": "base owns a complete workload execution contract for a substrate class. derived inherits a versioned base-rail contract and owns only its specialization. See docs/rail-composition-contract.md and ADR-0005."
|
||||
},
|
||||
"execution_architecture": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"description": "The operations architecture this rail names, e.g. kubernetes, knative-serving-on-kubernetes. Free-form on purpose: a new rail is justified by new semantics, not by an enum update."
|
||||
},
|
||||
"readiness_state": {
|
||||
"enum": ["declared", "installed", "verified", "production-approved", "deprecated"],
|
||||
"description": "Readiness of the rail contract itself, matching docs/rail-composition-contract.md. Distinct from rapp readiness_state (draft/declared/deployed/verified/retired) and from reef lifecycle_state. Do not unify the three vocabularies without a separate decision."
|
||||
},
|
||||
"required_substrate_capabilities": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Capabilities the substrate must publish before this rail is usable. This is the live name; the bootstrap contract's older substrate_prerequisites label is retired in favour of this field."
|
||||
},
|
||||
"supported_rollout_modes": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Rollout modes this rail supports, e.g. stage1-run, revision-canary."
|
||||
},
|
||||
"default_for": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Workload classes this rail is the default for. Present on the base rail today; optional so a derived rail is not forced to claim default status."
|
||||
},
|
||||
"compatibility_notes": {
|
||||
"$ref": "#/$defs/notes",
|
||||
"description": "Human-readable caveats. Never load-bearing."
|
||||
},
|
||||
"base_rail": {
|
||||
"$ref": "#/$defs/railSlug",
|
||||
"description": "Required on derived rails. The rail whose contract is inherited."
|
||||
},
|
||||
"base_rail_contract": {
|
||||
"$ref": "#/$defs/semver",
|
||||
"description": "Required on derived rails. Exact contract_version of the base rail this derivation is written against. Ranges are not pins."
|
||||
},
|
||||
"inherited_semantics": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Required on derived rails. Semantics taken from the base contract rather than restated."
|
||||
},
|
||||
"overridden_semantics": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Required on derived rails. The only semantics this rail owns. If this list is empty the derived rail has no reason to exist."
|
||||
},
|
||||
"compatibility_constraints": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Required on derived rails. Constraints a substrate or binding must satisfy to use this derivation."
|
||||
},
|
||||
"source_documents": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/sourceDocument" }
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"description": "A derived rail must name its base contract and the semantics it inherits or overrides.",
|
||||
"if": {
|
||||
"properties": { "composition_kind": { "const": "derived" } },
|
||||
"required": ["composition_kind"]
|
||||
},
|
||||
"then": {
|
||||
"required": [
|
||||
"base_rail",
|
||||
"base_rail_contract",
|
||||
"inherited_semantics",
|
||||
"overridden_semantics",
|
||||
"compatibility_constraints"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"description": "A base rail must not carry derived-only fields. Those fields would imply an inheritance it does not have.",
|
||||
"if": {
|
||||
"properties": { "composition_kind": { "const": "base" } },
|
||||
"required": ["composition_kind"]
|
||||
},
|
||||
"then": {
|
||||
"not": {
|
||||
"anyOf": [
|
||||
{ "required": ["base_rail"] },
|
||||
{ "required": ["base_rail_contract"] },
|
||||
{ "required": ["inherited_semantics"] },
|
||||
{ "required": ["overridden_semantics"] },
|
||||
{ "required": ["compatibility_constraints"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"repoSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 2
|
||||
},
|
||||
"railSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^rail-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"semver": {
|
||||
"type": "string",
|
||||
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$"
|
||||
},
|
||||
"notes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Human-readable caveats. Never load-bearing: nothing may depend on parsing a note."
|
||||
},
|
||||
"sourceDocument": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repo", "path"],
|
||||
"properties": {
|
||||
"repo": { "$ref": "#/$defs/repoSlug" },
|
||||
"path": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"criticality",
|
||||
"primary_rail",
|
||||
"supported_rails",
|
||||
"bound_reefs",
|
||||
"runtime_dependencies",
|
||||
"composition",
|
||||
"rollout_contract",
|
||||
|
|
@ -73,6 +74,12 @@
|
|||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/railSlug" }
|
||||
},
|
||||
"bound_reefs": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/reefSlug" },
|
||||
"description": "Reefs this package is bound to. Added by RAILIANCE-WP-0021-T04 as the source of reef.yaml bound_rapps. Required so the derivation is always defined: an empty list means the package is declared but not yet placed. The validator inverts this field to produce each reef's bound_rapps."
|
||||
},
|
||||
"runtime_dependencies": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
|
@ -128,6 +135,10 @@
|
|||
"type": "string",
|
||||
"pattern": "^rail-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"reefSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^reef-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"semver": {
|
||||
"type": "string",
|
||||
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$"
|
||||
|
|
|
|||
118
schemas/reef.schema.json
Normal file
118
schemas/reef.schema.json
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://railiance.dev/schemas/reef.schema.json",
|
||||
"title": "Railiance reef declaration",
|
||||
"description": "Normative shape for declarations/reef.yaml in every reef-* repo. Authored by railiance-master under RAILIANCE-WP-0021-T04 from the live reef-railiance declaration and docs/reef-substrate-model.md. bound_rapps is a derived projection of rapp.bound_reefs, not a hand-maintained registry.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"kind",
|
||||
"reef_id",
|
||||
"repo",
|
||||
"ownership_repo",
|
||||
"substrate_kind",
|
||||
"lifecycle_state",
|
||||
"criticality",
|
||||
"primary_rail",
|
||||
"hosted_rails",
|
||||
"current_members"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "substrate-reef",
|
||||
"description": "Fixed discriminator. The live reef already uses this value."
|
||||
},
|
||||
"reef_id": {
|
||||
"$ref": "#/$defs/reefSlug",
|
||||
"description": "Canonical identifier for the reef. Must equal the repo slug."
|
||||
},
|
||||
"repo": {
|
||||
"$ref": "#/$defs/repoSlug",
|
||||
"description": "The reef-* repo holding this declaration."
|
||||
},
|
||||
"ownership_repo": {
|
||||
"$ref": "#/$defs/repoSlug",
|
||||
"description": "The ownership-axis repo accountable for this substrate boundary. The live reef names railiance-infra."
|
||||
},
|
||||
"substrate_kind": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 3,
|
||||
"description": "What kind of substrate this reef is. Live value is server-group. Kept as a slug rather than a closed enum so a new substrate shape does not require a schema change. See docs/reef-substrate-model.md."
|
||||
},
|
||||
"lifecycle_state": {
|
||||
"enum": ["planned", "active", "retired"],
|
||||
"description": "Lifecycle of the reef as a substrate boundary. Distinct from rail readiness_state and from the per-binding admission states in docs/reef-production-readiness-contract.md (declared/installed/verified/production-approved/deprecated)."
|
||||
},
|
||||
"criticality": {
|
||||
"enum": ["low", "medium", "high", "critical"],
|
||||
"description": "Operational criticality of the substrate. Same vocabulary as rapp.yaml. The live reef is high."
|
||||
},
|
||||
"primary_rail": {
|
||||
"$ref": "#/$defs/railSlug",
|
||||
"description": "The default rail on this reef. Must appear in hosted_rails. The validator enforces that sibling check."
|
||||
},
|
||||
"hosted_rails": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/railSlug" },
|
||||
"description": "Rails this reef hosts or is prepared to host. Topology, not readiness: hosting a rail does not make it production-approved. See ADR-0006."
|
||||
},
|
||||
"bound_rapps": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/rappSlug" },
|
||||
"description": "DERIVED. Projection of every rapp declaration whose bound_reefs lists this reef. Optional so a reef may omit the field and let the validator compute it. If present, the validator requires an exact match against that projection. reef-railiance currently hand-lists rapp-qonto only; that list is already stale (rapp-openbao and rapp-postgres are live on the same reef). Migration: either delete the field, or replace it with the derived set once those rapps name this reef."
|
||||
},
|
||||
"current_members": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Named machines or units that currently compose this reef. Not slugs: live value is Railiance01. A reef is a substrate boundary, not a hostname list, but the members must be named so the boundary is inspectable."
|
||||
},
|
||||
"compatibility_notes": {
|
||||
"$ref": "#/$defs/notes",
|
||||
"description": "Human-readable caveats. Never load-bearing."
|
||||
},
|
||||
"source_documents": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/sourceDocument" }
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"repoSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 2
|
||||
},
|
||||
"railSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^rail-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"rappSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^rapp-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"reefSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^reef-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"notes": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Human-readable caveats. Never load-bearing: nothing may depend on parsing a note."
|
||||
},
|
||||
"sourceDocument": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repo", "path"],
|
||||
"properties": {
|
||||
"repo": { "$ref": "#/$defs/repoSlug" },
|
||||
"path": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue