Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@ It is also the naming authority for code. A type, a folder or a diagnostic that
names one of these concepts uses the word below, unchanged, so that a reviewer
moving between a chapter and a module never translates.

## The three layers

Deployment configuration is split into three layers, and the middle one is a
contract ([0003](docs/adr/model/0003-three-layer-meta-model.md), normative in
[chapter 00](spec/v1/00-overview.md#the-meta-model)). **Layer 1 contains no
mechanisms; layer 3 contains no decisions.**
## The three-model pipeline

**Three-model pipeline**: the arrangement of the whole model. Deployment
configuration is split into three layers, and the middle one is a contract
([0003](docs/adr/model/0003-three-model-pipeline.md), normative in
[chapter 00](spec/v1/00-overview.md#the-three-model-pipeline)). **Layer 1
contains no mechanisms; layer 3 contains no decisions.** Each layer is a
**model**, and each is joined to the next by a transformation. The layers are
stages of one pipeline, not metalevels: no layer is a type model of the one
below it. So the arrangement is not a "meta-model", a name this record used to
carry and has retired; say *three-model pipeline*, and say *layer 1*, *layer 2*
and *layer 3* for its members.

**Metamodel**: the definition of the language a model is written in, and
nothing else. A metamodel is a model whose instances are models, so the word is
never used for a layer, a document, an abstraction level or the pipeline as a
whole. The production implementation declares its languages as wire schemas;
the model-driven implementation under `emf/` has two hand-written Ecore
metamodels, a source and a target, and generates the Deliverable Set as files
with no metamodel of its own. One word, `metamodel`, unhyphenated.

**Project Intent**: layer 1. What a project's repository authors by hand:
requirements, never mechanisms. Two kinds of file, a project file and one env file
Expand Down Expand Up @@ -101,15 +115,18 @@ bin-packing ([0061](docs/adr/model/0061-placement-is-hard-dimensions.md)).

**Capability**: a named node property a Process may require.

**Hardening Class**: the pod security posture a Process takes, with named
exceptions each carrying a reason
([0016](docs/adr/model/0016-pod-hardening.md)).
**Hardening Class**: the pod security posture every Process takes, declared
once by the platform and authored by none of them. It has no exception surface:
a Process states the paths it must write, and an image that cannot meet the
class is refused ([0016](docs/adr/model/0016-pod-hardening.md)).

**Runtime Profile**: the profile selected by `runtime`, from which observability
and runtime environment variables are derived. Writing one of its keys by hand
is a build error.

**Alert Class**: how an alert on this Application should be delivered
**Alert Class**: how urgently a signal about this Application should wake
someone. Urgency only: which receiver, which channel and which severity mapping
belong to the monitoring stack that reads the projection, never to this model
([0021](docs/adr/model/0021-observability-scrape-and-alert-class.md)).

**Grant**: declared access to a Secret Store path, its keys, its access tier
Expand Down Expand Up @@ -238,7 +255,7 @@ described observed context and the content is authored intent

**Deployment.** Ambiguous between the Kubernetes kind and the estate's old
`deployment.jorisjonkers.dev` documents, which is the confusion
[0003](docs/adr/model/0003-three-layer-meta-model.md) exists to end. Say
[0003](docs/adr/model/0003-three-model-pipeline.md) exists to end. Say
Resolved Deployment, or say the Kubernetes kind.

**Reload.** A secret's `rotation.tolerates: reload`, and nothing else: the
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ lands, the **compiler** that turns that model into deployable artifacts.

## The shape of the model

Three layers, and the middle one is a contract
([0003](docs/adr/model/0003-three-layer-meta-model.md)):
**The three-model pipeline**: three models, each joined to the next by a
transformation, with the middle one as a contract
([0003](docs/adr/model/0003-three-model-pipeline.md)). They are stages of a
pipeline, not metalevels, which is why "metamodel" here means a language
definition and nothing else ([`CONTEXT.md`](CONTEXT.md#the-three-model-pipeline)):

1. **Project Intent**: hand-authored, requirements only. What an application owner
knows and nobody else does: its cold-start budget, what its data is worth,
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ check both registers.
|---|---|---|---|
| [0001](model/0001-estate-scale-and-ownership.md) | The estate is one maintainer, one cluster, about thirty Applications | open | 00-overview.md#the-estate |
| [0002](model/0002-kubernetes-as-substrate.md) | Kubernetes stays, for two properties that must be made real | open | 00-overview.md#substrate |
| [0003](model/0003-three-layer-meta-model.md) | Three layers, with the middle layer as a contract | settled | 00-overview.md#the-meta-model |
| [0003](model/0003-three-model-pipeline.md) | Three layers, with the middle layer as a contract | settled | 00-overview.md#the-three-model-pipeline |
| [0004](model/0004-contention-decides-authority.md) | Contention decides who declares a value | open | 20-resolved-deployment.md#authority |
| [0005](model/0005-derivation-is-total.md) | Derivation from declared intent covers the live estate | open | 20-resolved-deployment.md#derived-mechanics |
| [0006](model/0006-pinned-inputs.md) | Every assignment is a function of pinned, digested inputs | open | 20-resolved-deployment.md#pinned-inputs |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ The model already names the layers, and the code that renders it kept its own
unrelated shape. The generation being replaced put resolution inside the
renderer, and the estate paid for it in vocabulary: three mutually incompatible
documents all claiming `deployment.jorisjonkers.dev/v2`, with no word for which
one was wrong ([0003](../model/0003-three-layer-meta-model.md)). Naming the
one was wrong ([0003](../model/0003-three-model-pipeline.md)). Naming the
folders after the layers is not decoration. It means a reviewer reading
[chapter 20](../../../spec/v1/20-resolved-deployment.md) and a reviewer reading
`src/domain/resolved/` are looking at the same thing, and a rule that lands in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ difference to enter. One serializer makes determinism one module's
responsibility, and the double-render check then has one place to fail.

The typed object model earns its place for a second reason, which is stronger.
Layer 3 contains no decisions ([0003](../model/0003-three-layer-meta-model.md)),
Layer 3 contains no decisions ([0003](../model/0003-three-model-pipeline.md)),
and a string builder cannot be held to that: any field can be written, including
one no chapter assigns. A narrow object model declaring only the fields this
estate sets turns the rule into a compile error: an adapter cannot set what the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ make one side a derivative the other cannot be checked against. Two hand-written
implementations can drift, so equality has to be proven.

It is proven at the layer boundaries the model already has
([0003](../model/0003-three-layer-meta-model.md)). A parsed intent, a Resolved
([0003](../model/0003-three-model-pipeline.md)). A parsed intent, a Resolved
Deployment and a Deliverable Set are each a whole document, and
[0006](../model/0006-pinned-inputs.md) makes each a function of pinned inputs,
so a committed canonical copy of each is an oracle both sides can be held to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ tier: premise
status: proposed
claim: settled
date: 2026-08-31
normative: spec/v1/00-overview.md#the-meta-model
normative: spec/v1/00-overview.md#the-three-model-pipeline
---

# Three layers, with the middle layer as a contract
Expand Down Expand Up @@ -52,6 +52,14 @@ the Intent plus the pinned inputs, the cluster-state snapshot among them
([0034](0034-cluster-state-pinned-input.md)), and it is the sole input the
deliverable layer's adapters accept ([0053](0053-adapter-port-contract.md)).

**The name, amended 2026-09-15.** This decision was filed as "the three-layer
meta-model", and that name claims something the decision does not. The three
layers are three **models**, each written in its own language and joined to
the next by a transformation; no layer is a type model of the layer below it,
which is what a metamodel requires. The arrangement is the **three-model
pipeline**, "metamodel" is kept for a language definition, and the decision
itself, its H1 and everything else in this record are unchanged.

## Alternatives

| option | cost if taken | why rejected |
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0055-bidirectional-ledgers.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ rests-on: ["0003"]

## Rests on

Under [0003](0003-three-layer-meta-model.md) layer 3 is a total function of the
Under [0003](0003-three-model-pipeline.md) layer 3 is a total function of the
Resolved Deployment, so every live object the render does not produce can be
named as a predicate over the cluster inventory (kind, namespace, and a name or
name pattern) and so matched or found unmatched by machine. False if: an
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0062-application-is-the-release-unit.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ is derived and the other is authored.
|---|---|---|
| A `releaseWith` field, valid only within the project file | The deleted field worn narrower: the schema keeps a coupling name plus a rule that its target is in the same file, and two Applications in one file get a second way to say "together" | The file-scope rule is the Application boundary spelled out longhand; it adds a record without adding a fact, and the record can disagree with the boundary |
| Co-location implies atomicity: every Application in a project file releases as one | Adding an unrelated Application to `media.yml` silently couples its rollout to nine others, and nothing in the file says so | The trap: coupling acquired by editing an unrelated line, discoverable only when a held release blocks an Application its owner never coupled |
| A `components` level between Application and Process | A third authoring level whose only job is grouping Processes, and every field must then be assigned to Application, component or Process | Fails [0003](0003-three-layer-meta-model.md)'s two deciding questions, the level records no decision, and Application is already that grouping |
| A `components` level between Application and Process | A third authoring level whose only job is grouping Processes, and every field must then be assigned to Application, component or Process | Fails [0003](0003-three-model-pipeline.md)'s two deciding questions, the level records no decision, and Application is already that grouping |

## Reversibility
Undo cost today: reintroduce an Application-level coupling field and split the merged
Expand Down
4 changes: 2 additions & 2 deletions docs/adr/model/0070-path-authority-is-layer-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ projects with `E_PATH_COLLISION` evaluated on the assembled plan, before any
adapter runs, and no adapter holding an API that returns a path.

## Why
Layer 3 contains no decisions ([0003](0003-three-layer-meta-model.md)), and a
Layer 3 contains no decisions ([0003](0003-three-model-pipeline.md)), and a
path is a decision. It says which directory owns an object, and therefore which
kustomization includes it, which Reconcile Unit applies it, and who is
answerable for a field. The earlier rule, a path is a pure function of its
Expand Down Expand Up @@ -59,7 +59,7 @@ not let one Adapter write into another's.
| option | cost if taken | why rejected |
|---|---|---|
| Keep the path a function of the adapter and the object | No spec edit; matches the sentence chapter 30 already carries | Leaves the per-project object and the estate-scoped Deliverable unresolvable, and keeps collision detection at the writer, where it has never existed |
| A layout policy module consulted by both layers | An explicit seam, testable alone | A component holding decisions while sitting outside the three layers is the unnamed middle the meta-model exists to prevent, and it would own authority no chapter assigns it |
| A layout policy module consulted by both layers | An explicit seam, testable alone | A component holding decisions while sitting outside the three layers is the unnamed middle the three-model pipeline exists to prevent, and it would own authority no chapter assigns it |
| Let the writer resolve collisions by precedence | Nothing to design; deterministic given an order | Encodes authority as evaluation order, which is invisible in every artifact a reviewer reads, and makes adding an adapter a change to what an existing one emits |

## Reversibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ it was a digest nobody compared to a clock.
Two things deliberately stay out. Foundation components are Applications in project
files the platform owns ([0096](0096-the-foundation-is-declared.md)), because a
second way to declare an Application is the duplicate vocabulary
[0003](0003-three-layer-meta-model.md) exists to end. The node contract stays
[0003](0003-three-model-pipeline.md) exists to end. The node contract stays
its own pinned input ([0056](0056-node-facts-single-source.md)), because nix
reads it and would otherwise read a deployment-model document.

Expand Down
2 changes: 1 addition & 1 deletion docs/adr/model/0116-project-application-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ rests-on: ["0003"]

## Rests on
The three levels of layer 1 are fixed by the model rather than by any
substrate ([0003](0003-three-layer-meta-model.md)): one authored file with one
substrate ([0003](0003-three-model-pipeline.md)): one authored file with one
owner, a set of runnable parts that switch version together, and one runnable
part. Their names therefore describe those roles and nothing the substrate
spells. False if: a level's new name is also a name the rendered output uses
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ Adapters build **typed objects**, not text. The object model
([`src/objects/`](#layers)) declares only the fields this estate sets, so a
field the model cannot express cannot be set by an adapter: which is how
"layer 3 contains no decisions"
([0003](adr/model/0003-three-layer-meta-model.md)) becomes a compile-time
([0003](adr/model/0003-three-model-pipeline.md)) becomes a compile-time
property rather than a review question.

One serializer in the infrastructure ring turns those objects into bytes. It
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ written by hand because the course grades authored metamodels and because
generating it from Zod would make its structure unfalsifiable against Zod.

The proposal defines two metamodels, a source and a target, and the course
grades them as such. The model's three layers
([0003](../../../../docs/adr/model/0003-three-layer-meta-model.md)) still hold:
grades them as such. The three layers of the three-model pipeline
([0003](../../../../docs/adr/model/0003-three-model-pipeline.md)) still hold:
the Deliverable Set is the generated files, so it has no metamodel, and the
target metamodel carries layer 2 together with the typed resources the files
are written from, because an Acceleo template reads one model. The Platform
Expand Down
28 changes: 19 additions & 9 deletions spec/v1/00-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,20 @@ registered adapters do not: five of the six emit Kubernetes kinds and the sixth
emits Vault configuration, which is why the swap is a v2 migration rather than
an undo once repositories author against a shipped v1.

## The meta-model
## The three-model pipeline

Deployment configuration is split into three layers, and the middle one is a
contract ([0003](../../docs/adr/model/0003-three-layer-meta-model.md)).
contract ([0003](../../docs/adr/model/0003-three-model-pipeline.md)).

The three layers are **three models**, each written in its own language and
joined to the next by a transformation. They are **not metalevels**: no layer is
a type model of the layer below it, and calling the arrangement a "meta-model"
claims a relation the layers do not have. A metamodel needs the type-model-of
relation applied twice, which is what separates it from a model of a model; the
layers here are stages of one modelling pipeline, and a modelling layer is a
different thing from an abstraction layer. This specification therefore keeps
**metamodel** for a language definition and calls the arrangement itself the
three-model pipeline.

| Layer | Name | Authored | Owns |
|---|---|---|---|
Expand Down Expand Up @@ -111,9 +121,9 @@ time reads live cluster state. Reproducibility is therefore conditional and
true: identical inputs *including* `clusterStateDigest` produce a byte-identical
tree, so a differing render with identical digests is a defect, never weather.

![The meta-model](diagrams/00-overview-meta-model.drawio.svg)
![The three-model pipeline](diagrams/00-overview-three-model-pipeline.drawio.svg)

<sub>[Diagram source](#the-meta-model) · edit by opening the SVG in draw.io</sub>
<sub>[Diagram source](#the-three-model-pipeline) · edit by opening the SVG in draw.io</sub>

## Programme scope

Expand Down Expand Up @@ -235,8 +245,8 @@ parse-checked in CI.

| path | what it shows |
|---|---|
| `examples/projects/{auth,knowledge,data}.yml` | Project Intent, one file per project: two-level secret grants, `probes: none` stated explicitly, TCP probes, `placement` dimensions, declared `writablePaths`, `durability` per volume, and the `auth` pair as two Processes of one Application |
| `examples/{knowledge-api,knowledge-ingest-worker,auth-api,platform-postgres}.base.env` | env files, one set **per Process**, threaded with `${dependency:…}` and `${secret:<granted-path>#<key>}` placeholders whose paths byte-match a granted path |
| `examples/{auth,knowledge,data,minimal}/<project>.project.yml` | Project Intent, one file per project: two-level secret grants, `probes: none` stated explicitly, TCP probes, `placement` dimensions, declared `writablePaths`, `durability` per volume, and the `auth` pair as two Processes of one Application |
| `examples/{auth,knowledge,data,minimal}/env/<process>/base.env` | env files, one set **per Process** in a directory named for it, threaded with `${dependency:…}` and `${secret:<granted-path>#<key>}` placeholders whose paths byte-match a granted path |
| `examples/workflows/project-publish-fragment.yml` | publish on merge, `oras push` then `oras resolve`, read back |
| `examples/workflows/compose.yml` | pull participants, assert the estate-wide invariants, **prove the gate can fail** |
| `examples/negative/duplicate-application-id/` | a negative fixture, so an invariant that stops running is detectable |
Expand Down Expand Up @@ -413,15 +423,15 @@ a plain diff. **Where the two disagree the SVG is the diagram and the mermaid is
what gets fixed**, the same precedence this repository uses between a chapter and
an ADR.

### The meta-model
### The three-model pipeline

```mermaid
flowchart TB
subgraph AUTH["layer 1, hand-authored: Project Intent in each owning repository, Platform Intent in the platform's"]
a1["projects/&lt;project&gt;.yml<br/>applications, processes, placement, hardening,<br/>durability, probes, exposure, secrets"]
a1["platform/&lt;project&gt;.project.yml<br/>applications, processes, placement,<br/>writable paths, durability, probes,<br/>exposure, observability, secrets"]
a2["env/&lt;process&gt;/*.env<br/>one set per Process"]
a3["assets<br/>declarative, never executable"]
a5["platform.yml<br/>tiers, durability policy, engines,<br/>receivers, cadences, providers, bootstrap set"]
a5["platform.intent.yml<br/>substrate facts, bootstrap set,<br/>tiers, durability policy, hardening,<br/>cadences, engines, providers"]
end

a1 --> FR["Intent Fragments<br/>every authored document, published by digest"]
Expand Down
Loading
Loading