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
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@
- 'name': 'ADR contract'
'run': 'npm run lint:adrs'

# The coursework implementation's register, linted as its own root.
# Deleted with emf/ (emf/docs/adr/emf/0107).
- 'name': 'ADR contract (emf)'
'run': 'node scripts/lint-adrs.ts emf'

# Relative links and heading anchors across every tracked Markdown file.
# The ADR domain move rewrote about a hundred references outside the
# decision set, and nothing checked them.
Expand Down
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,26 @@ Delivery mechanics and co-testing are **defined separately**: see
applier, a prune pass, a field manager or a co-test gate in `spec/v1`; state the
model-level rule and point at the deferred set.

### The coursework implementation under `emf/`

The MDE course requires the compiler to be built with Ecore, Xtext, OCL,
QVT-Operational and Acceleo, so `emf/` holds a second, hand-written Java
implementation (Maven and Tycho, JDK 21, no Eclipse IDE). It is deprecated
from the day it lands and deleted at its sunset:

- **The root stays TypeScript.** Every pom, module, check, ledger and decision
of the Java side lives under `emf/`. The root references it only from CI
(the `emf` job and the `emf` ADR lint step), the `emf` domain in
`scripts/lint-adrs.ts` and its test, and
[`docs/architecture.md#the-parity-contract`](docs/architecture.md#the-parity-contract).
- **Never generate one implementation from the other.** Both are tested,
separately, against committed oracle files under `spec/v1/examples/`.
- **A model change lands in both implementations** and in the oracle files,
in one pull request.
- EMF decisions live in [`emf/docs/adr/`](emf/docs/adr/README.md), numbered
from the root's sequence. Check both registers before taking a number, and
lint with `node scripts/lint-adrs.ts emf`.

`deploy-config-schema` is the repository this one replaces. It stays alive and
authoritative until `deploy-kit` can render the estate; do not treat it as dead.

Expand Down
16 changes: 16 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ severity and receivers left to the stack that reads the projection (0021,
resembled (0016, 0083). The worked examples now declare every applicable
authored field and annotate its effect.

The set was amended on 2026-09-14 for the coursework implementation: the
model-driven engineering course requires the compiler to be built with Ecore,
Xtext, OCL, QVT-Operational and Acceleo, so a second, hand-written Java
implementation lives under `emf/` until its sunset condition holds. The two are
held equal by committed oracle files and nothing else (0105); 0065, 0066, 0100
and 0102 were amended in place to scope them to the TypeScript tree. The Java
implementation's own decisions live in
[`emf/docs/adr/`](../../emf/docs/adr/README.md), numbered from the same
sequence, and are deleted with it. The model is unchanged.

Tier-0 **premises** carry one falsifiable claim each; tier-1 **decisions** name
the premises they stand on in `rests-on`. A `claim: open` means decided in
direction, untested: its owner and settling test are in the file.
Expand All @@ -54,6 +64,11 @@ which domain a decision lives in.
| [`architecture/`](architecture/) | the compiler's own structure: layering, ports, error model, gates | `docs/architecture.md`, `docs/architecture-rules.md` | yes |
| [`deferred/`](deferred/README.md) | delivery mechanics and co-testing, defined separately | sections these chapters deliberately lack | no |

The coursework implementation keeps a register of its own at
[`emf/docs/adr/`](../../emf/docs/adr/README.md), linted by the same script with
`emf` as its root. Its numbers come from this sequence: before taking a number,
check both registers.

## Premises

| # | title | claim | normative |
Expand Down Expand Up @@ -205,3 +220,4 @@ Decisions about the compiler's own structure, not about the model. Their
| [0102](architecture/0102-the-gate-grows-with-the-code.md) | A new gate's script and its CI job land in the same pull request, and a test proves the two stay matched | settled |
| [0103](architecture/0103-a-behaviour-ledger-names-what-a-test-proves.md) | A behaviour ledger names every guarantee and the test that proves it, and a meta test holds the two together | settled |
| [0104](architecture/0104-every-enforced-rule-has-an-id-a-row-and-a-fixture.md) | Every enforced rule has an id, a ledger row and a fixture that proves it fires | settled |
| [0105](architecture/0105-two-implementations-meet-at-committed-oracles.md) | Two hand-written implementations meet at committed oracle files, and neither is generated from or tested against the other | open |
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ rests-on: ["0003"]

# One hexagon, two use-cases, and a domain whose folders are the three layers

> **Amended 2026-09-14.** Scoped to the TypeScript tree under `src/`. For the
> length of the model-driven engineering course a second, Java implementation
> lives under `emf/`, bound to this one only by the parity contract
> ([0105](0105-two-implementations-meet-at-committed-oracles.md)). The rings
> below describe `src/`; nothing in `emf/` imports from it or is held to them.

> **Amended 2026-09-08.** Still two use-cases, one core, but `publish` no
> longer renders anything. A repository validates its Intent Fragment and pushes
> it by digest; every adapter runs in `compose`
Expand Down
7 changes: 7 additions & 0 deletions docs/adr/architecture/0066-wire-shape-is-not-the-domain.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ rests-on: ["0007"]

# Zod declares the authoring shape, and a mapper turns it into the domain

> **Amended 2026-09-14.** "The one validator" means the one validator in
> `src/`. The Java implementation under `emf/` validates with OCL, and the two
> are held to the same constraints through the constraint ledger and the
> refused cases' committed diagnostics
> ([0105](0105-two-implementations-meet-at-committed-oracles.md)), never by
> generating one from the other.

## Rests on
The shape a human writes and the shape the core reasons about differ enough that
one type cannot be both, and the difference is exactly what a mapper per document
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/architecture/0100-tests-run-in-process-on-vitest.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ rests-on: ["0001"]

# Tests run in-process on Vitest, and the tooling is TypeScript that Node runs directly

> **Amended 2026-09-14.** Scoped to the repository root. The Java
> implementation under `emf/` is tested with JUnit through Maven, inside `emf/`
> ([0105](0105-two-implementations-meet-at-committed-oracles.md)); the root
> stays TypeScript, and this decision is unchanged for everything outside
> `emf/`.

## Rests on
Every gate this repository runs can be exercised in-process by one test runner
that also measures coverage and hosts mutation testing, so no gate needs a
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/architecture/0102-the-gate-grows-with-the-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ rests-on: ["0001"]

# A new gate's script and its CI job land in the same pull request, and a test proves the two stay matched

> **Amended 2026-09-14.** A gate can also be a CI job with no npm script:
> the `emf` job runs Maven inside `emf/`
> ([0105](0105-two-implementations-meet-at-committed-oracles.md)). It lands in
> the pull request that adds the Maven build, joins `Pipeline Complete`, and is
> deleted with `emf/`.

## Rests on
The repository's own npm scripts are the whole list of gates, so the list a
pull request's checks show and the list `package.json` declares can be kept
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
tier: decision
status: proposed
claim: open
owner: joris
date: 2026-09-14
normative: docs/architecture.md#the-parity-contract
rests-on: ["0003", "0006"]
---

# Two hand-written implementations meet at committed oracle files, and neither is generated from or tested against the other

## Rests on
The three layers give three points where a run's state is a complete, closed
document, and pinned inputs make each of those documents a function of the
input, so two independent implementations can be compared through a committed
file at each layer without either running beside the other. False if: some
layer's content depends on how an implementation orders or identifies things
in a way canonical JSON cannot normalise, so two correct implementations
disagree byte for byte. Settled by: both implementations green against every
oracle file of every case under `spec/v1/examples/`, with no oracle carrying an
implementation-specific field.

## Why
The course this repository is coursework for rejected the proposal to build
the compiler in TypeScript alone (`docs/mde/task-0-proposal/Sections/07-tooling.tex`
asked for that substitution and flagged that it needed approval). The graded
work has to use Ecore, Xtext, OCL, QVT-Operational and Acceleo. The TypeScript
compiler stays the long-lived implementation, so for the length of the course
there are two.

Generating one from the other was the obvious way to keep them equal, and it
was ruled out: the course grades the modelling artifacts as authored work, and
the TypeScript types generated from Ecore, or Ecore generated from Zod, would
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
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
separately. A failure then names the side that is wrong, which a diff between
two runs never does. `spec/v1/examples/` already carries hand-rendered
Deliverable Sets and refused cases; the contract adds the two intermediate
documents, the diagnostics of each refused case and the metamodel's structure.

## Alternatives
| option | cost if taken | why rejected |
|---|---|---|
| Generate TypeScript from Ecore, or Ecore from TypeScript | No parity tests for structure at all | The course grades the modelling artifacts as authored work, and a generated side cannot fail independently of its source |
| Run both implementations and diff their outputs in CI | No committed intermediate files | A red diff does not say which side is wrong, and neither suite can run alone |
| Only the rendered tree as oracle | Uses what already exists | A resolution bug and a rendering bug look the same, and the metamodel and validation parities have nothing to compare |

## Reversibility
Undo cost today: deleting the contract section and the oracle files nobody has
written yet. Becomes irreversible once: never; at the second implementation's
sunset the oracle files stay as the TypeScript compiler's own golden files and
only the Java side of each check is deleted.

## Consequences
- Every behaviour change touches an oracle file, and both implementations stay
red until both are fixed. Paid by whoever changes the model, in two
implementations, for as long as there are two.
- Diagnostics carry a stable code and a JSON Pointer in both implementations,
so a refusal's location becomes contract rather than presentation. Paid once
per constraint, in the constraint ledger.
- Canonical JSON is a new serialisation both sides must produce exactly. Paid
once per implementation, and kept afterwards as the TypeScript golden format.
- The architecture decisions that assume one implementation
([0065](0065-one-hexagon-domain-mirrors-the-layers.md),
[0066](0066-wire-shape-is-not-the-domain.md),
[0100](0100-tests-run-in-process-on-vitest.md),
[0102](0102-the-gate-grows-with-the-code.md)) are scoped to the TypeScript
tree; the Java tree's decisions live in `emf/docs/adr/` and are deleted with
it.
6 changes: 6 additions & 0 deletions docs/adr/architecture/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ knowing which domain it lands in. The register is

The scope boundary is stated in
[chapter 00](../../../spec/v1/00-overview.md#programme-scope).

Four of these decisions are scoped to the TypeScript tree for as long as the
coursework implementation under `emf/` exists, and
[0105](0105-two-implementations-meet-at-committed-oracles.md) is the contract
between the two. The Java implementation's own structure is decided in
[`emf/docs/adr/`](../../../emf/docs/adr/README.md), not here.
56 changes: 56 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,62 @@ CRD schemas, so a file that parses but cannot apply fails the build.
Every gate carries negative fixtures. A check that has only ever run against a
clean tree is untested: nothing proves it would fail.

## The parity contract

This compiler has a second implementation. The model-driven engineering course
this repository is coursework for requires Ecore, Xtext, OCL, QVT-Operational
and Acceleo, so a Java implementation lives under [`emf/`](../emf/README.md)
until its sunset condition holds
([0105](adr/architecture/0105-two-implementations-meet-at-committed-oracles.md)).
Everything about that implementation, its build, its checks and its decisions,
lives inside `emf/`. What lives here is the contract both implementations
answer to, because the contract outlives the second implementation.

Neither implementation is generated from the other, and neither is the oracle
for the other. Each is tested on its own against committed oracle files, and
agreement between the two follows from both agreeing with the oracle.

| oracle | where | compared as |
|---|---|---|
| the parsed Service Intent | `spec/v1/examples/<case>/expected/intent.json` | canonical JSON, byte for byte |
| the Resolved Deployment | `spec/v1/examples/<case>/expected/resolved.json` | canonical JSON, byte for byte |
| the Deliverable Set | `spec/v1/examples/<case>/rendered/` | the existing golden tree, byte for byte |
| the diagnostics of a refused case | `<input>.diagnostics.json` beside the refused input in `refusals/` or `negative/` | a set of `(code, path)` pairs |
| the metamodel's structure | `spec/v1/examples/expected/descriptor.json` | canonical JSON, byte for byte |

**Canonical JSON** is RFC 8785 (JSON Canonicalization Scheme): keys sorted,
numbers in their shortest form, no insignificant whitespace. An absent
optional field is absent, never `null`.

**A path** is an RFC 6901 JSON Pointer into the canonical intent document,
`/services/0/observability/alertClass`. A diagnostic about a derived value
points at the authored value it derives from. Messages and hints are free per
implementation; the code and the path are the contract.

**The descriptor** lists every class of every layer with its features, each
feature's type and multiplicity, and every closed vocabulary with its literals.
The TypeScript side builds it from the Zod schemas with Zod's native
`z.toJSONSchema()` and a normaliser; the descriptor's shape is fixed here, not
by either source format.

**The constraint ledger** gives every model constraint a `CONS-NNN` id, the
diagnostic code it emits, the check that enforces it in `src/`, and a refused
fixture that proves it fires. A constraint the ledger does not list is not part
of the model's validation, whichever implementation happens to enforce it.

**Behaviour rows.** A row of the [behaviour ledger](requirements.md) whose
behaviour is the model's own (parse, validate, resolve, render) is proved in
both implementations. The row names the TypeScript test; the Java witness for
the same id is listed inside `emf/`, and `emf/`'s own gate fails when a model
row has no witness there.

An oracle file changes in the pull request that changes the behaviour it
records, and both implementations go red together until both are fixed. CI
never regenerates an oracle file from either implementation: a tool may write a
candidate, and the committed file is the reviewed copy of it. A
case without every oracle file is not yet a parity case, and the ledger that
lists cases says so rather than skipping it silently.

## Gates

Fourteen gates hold the structure, and each exists because its absence has already
Expand Down
2 changes: 1 addition & 1 deletion docs/requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ This ledger holds **14** rows. The compiler's behaviours join it as they land.

| id | a contributor or a consumer can rely on | proved by |
|---|---|---|
| REQ-001 | Every decision record under `docs/adr/` satisfies its frontmatter, register and citation contract | [test/adr-contract.test.ts](../test/adr-contract.test.ts) |
| REQ-001 | Every decision record under `docs/adr/` and `emf/docs/adr/` satisfies its frontmatter, register and citation contract, and no number is used in both | [test/adr-contract.test.ts](../test/adr-contract.test.ts) |
| REQ-002 | Every relative link and heading anchor in tracked Markdown resolves to a real target | [test/link-contract.test.ts](../test/link-contract.test.ts) |
| REQ-003 | Every rendered Kubernetes manifest in the worked examples validates against its pinned schema | [test/manifest-contract.test.ts](../test/manifest-contract.test.ts) |
| REQ-004 | The compiler's layer boundaries and module reachability are enforced on the dependency graph, not on review alone | [test/boundary-contract.test.ts](../test/boundary-contract.test.ts) |
Expand Down
24 changes: 24 additions & 0 deletions emf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# emf: the coursework implementation

Everything under this directory is the second implementation of the
deploy-kit compiler, built with the Eclipse Modeling Framework toolchain the
model-driven engineering course at the University of Twente requires: Ecore,
Xtext, OCL, QVT-Operational and Acceleo. It is **deprecated from the day it
lands**: it exists for the length of the course and is deleted when its sunset
condition holds
([0107](docs/adr/emf/0107-emf-is-coursework-scoped-and-self-contained.md)).

The repository root stays TypeScript. Nothing outside `emf/` builds, imports or
depends on anything inside it. The Maven build, its modules, its checks, its
ledgers and its decisions all live here, so the sunset is deleting this
directory and the few root lines that name it, listed in
[the architecture](docs/architecture.md#scope-and-sunset).

The one thing this implementation shares with the TypeScript compiler is the
[parity contract](../docs/architecture.md#the-parity-contract): the committed
oracle files under `spec/v1/examples/` that both are tested against, separately.

| read | for |
|---|---|
| [docs/architecture.md](docs/architecture.md) | the structure: toolchain, modules, how each stage meets the contract |
| [docs/adr/README.md](docs/adr/README.md) | the decisions that shaped it, and the register |
Loading
Loading