{ "$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 RMASTER-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", "bound_reefs", "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" } }, "bound_reefs": { "type": "array", "uniqueItems": true, "items": { "$ref": "#/$defs/reefSlug" }, "description": "Reefs this package is bound to. Added by RMASTER-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, "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." }, "exposure": { "type": "object", "additionalProperties": false, "required": ["posture"], "description": "Who may reach this rapp's listener. Optional; omitted means private (ADR-0008). Distinct from data_classification.", "properties": { "posture": { "enum": ["private", "operator", "public"], "description": "Intended consumer-facing listener. Default if exposure is omitted: private." }, "grant": { "$ref": "#/$defs/exposureGrant" }, "tunnel": { "$ref": "#/$defs/exposureTunnel" }, "binding_admission": { "enum": ["declared", "installed", "verified", "production-approved", "deprecated"], "description": "ADR-0006 admission state of the reef binding this public listener sits on. Not rapp readiness_state. Required by the validator when posture is public." } } } }, "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"] } }, { "description": "public rapp exposure requires a grant object.", "if": { "required": ["exposure"], "properties": { "exposure": { "required": ["posture"], "properties": { "posture": { "const": "public" } } } } }, "then": { "properties": { "exposure": { "required": ["grant"] } } } } ], "$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]+)*$" }, "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*)$" }, "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::, 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 } } }, "exposureGrant": { "type": "object", "additionalProperties": false, "required": ["reason", "approved_on", "residual_risk_owner"], "description": "Accepted residual risk for a public listener. Must name a hostname or a port. Port 6443 is rejected by the validator.", "properties": { "hostname": { "type": "string", "minLength": 1 }, "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, "reason": { "type": "string", "minLength": 1 }, "approved_on": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" }, "residual_risk_owner": { "type": "string", "minLength": 1 } }, "anyOf": [ { "required": ["hostname"] }, { "required": ["port"] } ] }, "exposureTunnel": { "type": "object", "additionalProperties": false, "required": ["name"], "description": "Named ops-bridge / SSH tunnel. Does not change packet posture.", "properties": { "name": { "type": "string", "minLength": 1 }, "local_port": { "type": "integer", "minimum": 1, "maximum": 65535 } } } } }