Add rapp declaration schema with composition block (WP-0021 T02, T03)
schemas/rapp.schema.json defines one normative shape for the rollout, smoke and rollback contracts in place of the three mutually unreadable variants found across the live rapps, promotes contract_version, readiness_state, data_classification and criticality to required, and forbids the rapp- prefix on workload_identity.name. composition replaces the flat members list per amendment f88f938d: purpose, member_repos with deployables, and pinned upstream_components. Repos are many:many with rapps; deployables are 1:1, which is what makes the T06 coverage check well-formed. ownership_repo left permissive pending an architecture-owner call; the tighter alternative is written up in schemas/README.md. Validated against all three live declarations: openbao 10 errors, postgres 10, qonto 4 — precisely the reported drift and nothing else. Also found: three further rapp-* repos (secrets-engine, tenant-engine, user-engine) carry no declarations at all, which the routed survey missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8fc92bd7d2
commit
28cbba1025
3 changed files with 525 additions and 2 deletions
100
schemas/README.md
Normal file
100
schemas/README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# 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` (RAILIANCE-WP-0021-T04) |
|
||||
| `reef-*` | `declarations/reef.yaml` | `reef.schema.json` (RAILIANCE-WP-0021-T04) |
|
||||
|
||||
Written under `RAILIANCE-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` (T07) 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.
|
||||
|
||||
## Validating
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
350
schemas/rapp.schema.json
Normal file
350
schemas/rapp.schema.json
Normal file
|
|
@ -0,0 +1,350 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://railiance.dev/schemas/rapp.schema.json",
|
||||
"title": "Railiance rapp declaration",
|
||||
"description": "Normative shape for declarations/rapp.yaml in every rapp-* repo. Authored by railiance-master under RAILIANCE-WP-0021 from the drift survey routed by railiance-platform (RAILIANCE-WP-0015). This schema is normative; docs/repo-family-bootstrap-contract.md cites it rather than restating it.",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"kind",
|
||||
"repo_family",
|
||||
"rapp_id",
|
||||
"repo",
|
||||
"ownership_repo",
|
||||
"contract_version",
|
||||
"readiness_state",
|
||||
"workload_identity",
|
||||
"data_classification",
|
||||
"criticality",
|
||||
"primary_rail",
|
||||
"supported_rails",
|
||||
"runtime_dependencies",
|
||||
"composition",
|
||||
"rollout_contract",
|
||||
"smoke_contract",
|
||||
"rollback_contract"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"const": "managed-workload-package",
|
||||
"description": "Fixed discriminator. All three live declarations already agree on this value."
|
||||
},
|
||||
"repo_family": {
|
||||
"const": "rapp"
|
||||
},
|
||||
"rapp_id": {
|
||||
"$ref": "#/$defs/rappSlug",
|
||||
"description": "Canonical identifier for the rapp. Must equal the repo slug for a single-repo rapp; for a grouped rapp it names the bounded context and need not match any member repo."
|
||||
},
|
||||
"repo": {
|
||||
"$ref": "#/$defs/repoSlug",
|
||||
"description": "The rapp-* repo holding this declaration."
|
||||
},
|
||||
"ownership_repo": {
|
||||
"$ref": "#/$defs/repoSlug",
|
||||
"description": "The single repo accountable for this workload's lifecycle. Both layer repos (railiance-platform) and application repos (qonto-assistant) are legal values; what the schema requires is exactly one, and that it not be the rapp repo itself. See schemas/README.md for the open question on tightening this."
|
||||
},
|
||||
"contract_version": {
|
||||
"$ref": "#/$defs/semver",
|
||||
"description": "Version of this declaration contract. Promoted from rapp-qonto-only to required, matching the rail family, which has carried it consistently in both rails and has not drifted."
|
||||
},
|
||||
"readiness_state": {
|
||||
"enum": ["draft", "declared", "deployed", "verified", "retired"],
|
||||
"description": "Lifecycle state of the package. Promoted from rapp-qonto-only to required, matching rail.yaml."
|
||||
},
|
||||
"workload_identity": {
|
||||
"$ref": "#/$defs/workloadIdentity"
|
||||
},
|
||||
"data_classification": {
|
||||
"enum": ["public", "internal", "confidential", "restricted"],
|
||||
"description": "Highest classification of data the workload handles. Promoted from rapp-qonto-only to required: a package whose classification is unstated cannot be admitted to a reef on evidence."
|
||||
},
|
||||
"criticality": {
|
||||
"enum": ["low", "medium", "high", "critical"],
|
||||
"description": "Operational criticality. Promoted from rapp-qonto-only to required. reef.yaml already carries the same vocabulary."
|
||||
},
|
||||
"primary_rail": {
|
||||
"$ref": "#/$defs/railSlug",
|
||||
"description": "The rail this package is deployed on by default. Must appear in supported_rails."
|
||||
},
|
||||
"supported_rails": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "$ref": "#/$defs/railSlug" }
|
||||
},
|
||||
"runtime_dependencies": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Capabilities the workload requires at runtime. Free-form strings today; a controlled vocabulary shared with rail.required_substrate_capabilities is deferred, not rejected."
|
||||
},
|
||||
"composition": { "$ref": "#/$defs/composition" },
|
||||
"secret_references": {
|
||||
"type": "array",
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "OpenBao paths the workload reads. Paths only — never values. Optional, since not every package holds secrets."
|
||||
},
|
||||
"consumer_contract": { "$ref": "#/$defs/consumerContract" },
|
||||
"consumers": {
|
||||
"type": "array",
|
||||
"items": { "$ref": "#/$defs/consumer" },
|
||||
"description": "Present only where consumer_contract is present."
|
||||
},
|
||||
"rollout_contract": { "$ref": "#/$defs/rolloutContract" },
|
||||
"smoke_contract": { "$ref": "#/$defs/smokeContract" },
|
||||
"rollback_contract": { "$ref": "#/$defs/rollbackContract" },
|
||||
"source_documents": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "$ref": "#/$defs/sourceDocument" },
|
||||
"description": "Documents this declaration was derived from. All three live rapps already carry this consistently."
|
||||
}
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
"description": "primary_rail must be one of supported_rails. Expressed as a validator check rather than in schema, since JSON Schema cannot compare a value against a sibling array; the validator enforces it.",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "consumers requires consumer_contract.",
|
||||
"if": { "required": ["consumers"] },
|
||||
"then": { "required": ["consumer_contract"] }
|
||||
}
|
||||
],
|
||||
"$defs": {
|
||||
"repoSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"minLength": 2
|
||||
},
|
||||
"rappSlug": {
|
||||
"type": "string",
|
||||
"pattern": "^rapp-[a-z0-9]+(-[a-z0-9]+)*$"
|
||||
},
|
||||
"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."
|
||||
},
|
||||
|
||||
"workloadIdentity": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "package_type"],
|
||||
"description": "Identity of the WORKLOAD, never of the repo. rapp-qonto currently sets name to the repo slug (rapp-qonto); that is the drift this definition closes. The correct value there is qonto.",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]+(-[a-z0-9]+)*$",
|
||||
"not": { "pattern": "^rapp-" },
|
||||
"description": "The workload's own name. Must not carry the rapp- prefix: that prefix names a repo, and this field does not name a repo."
|
||||
},
|
||||
"package_type": {
|
||||
"enum": [
|
||||
"helm-managed-platform-service",
|
||||
"manifest-managed-platform-service",
|
||||
"knative-managed-service",
|
||||
"grouped-composition"
|
||||
],
|
||||
"description": "grouped-composition is for rapps whose members carry their own package types."
|
||||
},
|
||||
"principal": { "type": "string" },
|
||||
"service_account": { "type": "string" },
|
||||
"tenant": {
|
||||
"type": "string",
|
||||
"description": "Opaque NetKingdom tenant identifier (tenant:<grouping>:<name>, net-kingdom ADR-0013). A rapp never mints, parses, or authorizes on this."
|
||||
},
|
||||
"chart": { "type": "string" },
|
||||
"chart_version": { "type": "string" },
|
||||
"app_version": { "type": "string" },
|
||||
"notes": { "$ref": "#/$defs/notes" },
|
||||
"note": { "type": "string" }
|
||||
}
|
||||
},
|
||||
|
||||
"composition": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["purpose", "member_repos"],
|
||||
"description": "What this rapp is composed of. Supersedes the flat members: list from proposal 04c776c4 per amendment f88f938d. A rapp is a composition of first-party repos AND pinned third-party components, serving a stated purpose.",
|
||||
"properties": {
|
||||
"purpose": {
|
||||
"type": "string",
|
||||
"minLength": 12,
|
||||
"description": "What this coherent workload does. One sentence. This is the bounded-context statement; if it cannot be written without 'and also', the grouping is probably wrong."
|
||||
},
|
||||
"member_repos": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"description": "First-party repos, each sharing rollout and rollback fate with the others. A single-repo rapp declares exactly one member. Grouping is legitimate ONLY where members share rollout and rollback fate; members that can be rolled back independently belong in separate rapps.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repo", "role", "deployables"],
|
||||
"properties": {
|
||||
"repo": { "$ref": "#/$defs/repoSlug" },
|
||||
"role": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"description": "What this repo contributes to the composition."
|
||||
},
|
||||
"deployables": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Names of the running units this repo contributes. CARDINALITY: a repo may appear in the member_repos of MORE THAN ONE rapp (many-to-many), but each deployable belongs to EXACTLY ONE rapp (one-to-one) — exactly one rapp owns its rollout. The validator's coverage check depends on this asymmetry; see schemas/README.md."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"upstream_components": {
|
||||
"type": "array",
|
||||
"description": "Third-party components, pinned. An unpinned upstream is not a composition member, it is a liability.",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "source", "version"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"source": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Chart repo/chart or registry reference."
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "An exact pin. Ranges and floating tags such as latest are not pins."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"rolloutContract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["default_mode"],
|
||||
"description": "NORMATIVE SHAPE. Resembles rapp-openbao and rapp-postgres for the imperative fields (commands) and rapp-qonto for the gate. default_mode was already common to all three live declarations and is kept.",
|
||||
"properties": {
|
||||
"default_mode": {
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"description": "How this package is rolled out by default, e.g. helm-upgrade-install, kubectl-server-side-apply, revision-canary."
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Renamed from supported_commands. The imperative execution path where one exists; absent for packages rolled out declaratively."
|
||||
},
|
||||
"production_gate": {
|
||||
"type": "string",
|
||||
"description": "Named gate that must be satisfied before production rollout, e.g. reef-production-approved."
|
||||
},
|
||||
"notes": { "$ref": "#/$defs/notes" }
|
||||
}
|
||||
},
|
||||
|
||||
"smokeContract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["required"],
|
||||
"description": "NORMATIVE SHAPE, taken from rapp-qonto. required: names the OUTCOMES that must hold; commands: is how they are exercised. The platform rapps currently declare only commands, so migrating them means naming their checks — that is real migration cost and it is the point: a command list says what was run, not what must be true.",
|
||||
"properties": {
|
||||
"required": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Named checks that must pass, e.g. authenticated-activation-from-zero, wrong-tenant-denied."
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Renamed from supported_commands."
|
||||
},
|
||||
"notes": { "$ref": "#/$defs/notes" }
|
||||
}
|
||||
},
|
||||
|
||||
"rollbackContract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["order"],
|
||||
"description": "NORMATIVE SHAPE, taken from rapp-qonto. order: is the ordered preference of rollback strategies, most-preferred first. The platform rapps currently declare only commands; their order is recoverable from the command sequence they already document.",
|
||||
"properties": {
|
||||
"order": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Ordered rollback strategies, most preferred first, e.g. previous-verified-knative-revision, temporary-minimum-scale, unavailable."
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": { "type": "string", "minLength": 1 },
|
||||
"description": "Renamed from supported_commands."
|
||||
},
|
||||
"fallback": {
|
||||
"type": "string",
|
||||
"description": "Generalized from rapp-qonto's direct_kubernetes_fallback. Names the exceptional path and its expiry condition."
|
||||
},
|
||||
"notes": { "$ref": "#/$defs/notes" }
|
||||
}
|
||||
},
|
||||
|
||||
"consumerContract": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["boundary_unit", "credential_lane", "credential_type"],
|
||||
"description": "Present only on packages that provision isolated units to other services. Schema-defined but optional: today only rapp-postgres carries one, and requiring it everywhere would be false precision.",
|
||||
"properties": {
|
||||
"boundary_unit": { "type": "string", "minLength": 1 },
|
||||
"credential_lane": { "type": "string", "minLength": 1 },
|
||||
"credential_type": { "type": "string", "minLength": 1 },
|
||||
"tenant_keying_required": { "type": "boolean" },
|
||||
"notes": { "$ref": "#/$defs/notes" }
|
||||
}
|
||||
},
|
||||
|
||||
"consumer": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["name", "declaration"],
|
||||
"properties": {
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"declaration": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Repo-relative path to the consumer declaration."
|
||||
},
|
||||
"cost_attribution_key": {
|
||||
"type": "string",
|
||||
"description": "Ties consumption to the cost evidence contract (fin-hub FIN-WP-0004)."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"sourceDocument": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["repo", "path"],
|
||||
"properties": {
|
||||
"repo": { "$ref": "#/$defs/repoSlug" },
|
||||
"path": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -162,7 +162,7 @@ acquire a cluster dependency.
|
|||
|
||||
```task
|
||||
id: RAILIANCE-WP-0021-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "87ae4551-1fac-4613-b258-28c8b22bbb82"
|
||||
```
|
||||
|
|
@ -190,11 +190,62 @@ Pick one normative shape for `rollout_contract`, `smoke_contract` and
|
|||
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: RAILIANCE-WP-0021-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "8b130ff8-0601-40bb-8539-d6f5847c2f41"
|
||||
```
|
||||
|
|
@ -226,6 +277,28 @@ 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue