diff --git a/.agents/skills/adr/SKILL.md b/.agents/skills/adr/SKILL.md
index 7671438fe..ef49e527d 100644
--- a/.agents/skills/adr/SKILL.md
+++ b/.agents/skills/adr/SKILL.md
@@ -18,13 +18,11 @@ pointing at it.
`docs/adr/architecture/` for a decision about the compiler's own structure
(its `normative:` pointer names a `docs/architecture.md` or
`docs/architecture-rules.md` heading); `docs/adr/deferred/` only for
- delivery or co-testing direction work, never for a v1 decision. While
- `emf/` exists, a Java-side decision goes to `emf/docs/adr/` instead, using
- the same numbering sequence.
-2. **Take the next number.** Check `docs/adr/README.md`'s register **and**
- `emf/docs/adr/README.md`'s, even when writing to the root register: the
- sequence is estate-wide and a number already used in either register is
- the fastest way to fail the register-integrity check.
+ delivery or co-testing direction work, never for a v1 decision.
+2. **Take the next number.** Check `docs/adr/README.md`'s register: the
+ sequence is estate-wide, a number already used in it is never taken again,
+ and the next number is the highest the register holds plus one (which is
+ the fastest way to fail the register-integrity check).
3. **Write the frontmatter.** `tier: premise` (a falsifiable claim, no
`rests-on`) or `tier: decision` (names only premises in `rests-on`, never
another decision: a decision-to-decision dependency is prose, in `## Why`,
@@ -38,7 +36,7 @@ pointing at it.
here: that belongs in the chapter the `normative:` pointer names, and this
record links to it instead.
5. **Add the register row**, in the numeric position its section of
- `docs/adr/README.md` (or `emf/docs/adr/README.md`) already sorts by, with
+ `docs/adr/README.md` already sorts by, with
the title and claim matching the file exactly.
6. **Cite it correctly everywhere else you touch.** Never a bare `ADR-NNNN`
or bare number: always a Markdown link. A decision that changes is
diff --git a/.agents/skills/spec-change/SKILL.md b/.agents/skills/spec-change/SKILL.md
index f83e23e1c..2c2ecc643 100644
--- a/.agents/skills/spec-change/SKILL.md
+++ b/.agents/skills/spec-change/SKILL.md
@@ -32,14 +32,14 @@ next.
`expected/dependencies.json`, `expected/resolved.json`,
`.diagnostics.json`, `rendered/`, `expected/descriptor.json`.
Written and reviewed by hand, never regenerated by CI
- ([`docs/architecture.md#the-parity-contract`](../../../docs/architecture.md#the-parity-contract)).
+ ([`docs/architecture.md#the-oracles`](../../../docs/architecture.md#the-oracles)).
A tool may propose a candidate; the committed file is the reviewed copy of
it, canonical JSON (RFC 8785), byte for byte.
- **The diagrams**, under `spec/v1/diagrams/`: one drawn SVG per chapter, the
editable draw.io source embedded, one palette, colour carrying the layer.
- **The TypeScript declarations.** A model change lands in the TypeScript
- production implementation first; `emf/` catches up in a pull request of its
- own ([`AGENTS.md`](../../../AGENTS.md#boundaries)). Nothing regenerates an
+ implementation and its oracle files together
+ ([`AGENTS.md`](../../../AGENTS.md#boundaries)). Nothing regenerates an
oracle.
## Steps
@@ -50,8 +50,7 @@ next.
needs, or updating the pointer in the same commit if a heading moves.
3. Update or add the worked example(s) the change bears on, and hand-write
the oracle files it touches.
-4. Update the TypeScript declarations against the same oracle files; leave
- `emf/` as it is.
+4. Update the TypeScript declarations against the same oracle files.
5. Run `npm run lint:adrs` and `npm run lint:links`.
## Claiming a ticket
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 96a8a6198..067d35e57 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -7,6 +7,3 @@
# change that lands without its ADR is the drift this repository exists to stop.
/docs/adr/ @JorisJonkers-dev/maintainers
/spec/ @JorisJonkers-dev/maintainers
-
-# The model-driven implementation, deleted at its sunset.
-/emf/ @JorisJonkers-dev/maintainers
diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
index 938f760ec..4a23c9718 100644
--- a/.github/codeql/codeql-config.yml
+++ b/.github/codeql/codeql-config.yml
@@ -1,6 +1,6 @@
# Paths CodeQL does not analyse. Build output and generated sources are not
-# code anyone wrote here; the model-driven build (emf/) generates Java from its
-# genmodel and Xtext grammar into target/ and src-gen/.
+# code anyone wrote here; generated code lands in target/, src-gen/ and
+# xtend-gen/.
'name': 'deploy-kit CodeQL'
'paths-ignore':
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 59018ef51..73a979981 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -114,11 +114,6 @@
- '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 Markdown file git does not ignore.
# The ADR domain move rewrote about a hundred references outside the
# decision set, and nothing checked them.
@@ -496,79 +491,6 @@
'security-events': 'write'
'uses': './.github/workflows/codeql.yml'
- # The model-driven implementation under emf/ (docs/adr/emf/0107): its own
- # Maven build, JDK and gates, deleted with the directory at its sunset. Every gate
- # it runs is listed in emf/docs/rules.md; the thresholds are in emf/pom.xml.
- # test/emf-wiring.test.ts proves this step names a real build.
- #
- # The model-driven build under emf/ (Maven and Tycho) runs its tests and its
- # 100% JaCoCo gate on every change to its sources or to this workflow. Any
- # other change skips its steps: a model change lands in TypeScript first and
- # moves the oracles, and the model-driven build catches up in its own change. Its 100% PIT gate takes too long for every change, so it runs
- # periodically: on the nightly run and on a dispatch. It is not one of
- # Pipeline Complete's needs.
- 'emf':
- 'name': 'Model-driven build'
- 'if': "!startsWith(github.head_ref, 'release-please--')"
- 'runs-on': 'ubuntu-latest'
- 'timeout-minutes': 60
- 'permissions':
- 'contents': 'read'
- 'steps':
- # A pull request checks out its merge commit and a push to main the
- # squashed commit; either way the first parent is main before it.
- - 'uses': 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' # v7.0.1
- 'with':
- 'fetch-depth': '2'
- - 'name': 'Decide whether the change reaches the model-driven build'
- 'id': 'scope'
- 'shell': 'bash'
- 'env':
- 'EVENT': '${{ github.event_name }}'
- 'run': |
- set -euo pipefail
- mutation='false'
- if [ "${EVENT}" = 'workflow_dispatch' ] || [ "${EVENT}" = 'schedule' ]; then
- needed='true'
- mutation='true'
- elif git diff --name-only HEAD^1 HEAD | grep -qE '^(emf/|\.github/workflows/ci\.yml$)'; then
- needed='true'
- else
- needed='false'
- fi
- echo "needed=${needed}" >> "$GITHUB_OUTPUT"
- echo "mutation=${mutation}" >> "$GITHUB_OUTPUT"
- - 'name': 'Nothing the build reads changed'
- 'if': "steps.scope.outputs.needed != 'true'"
- 'run': 'echo "No model-driven build: this change reaches neither emf/ nor this workflow."'
- - 'uses': 'actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6' # v6.0.1
- 'if': "steps.scope.outputs.needed == 'true'"
- 'with':
- 'distribution': 'temurin'
- 'java-version-file': 'emf/.java-version'
- 'cache': 'maven'
- # Tycho caches p2 bundles in the Maven repository too, so the target
- # platform is part of the key.
- 'cache-dependency-path': |
- emf/**/pom.xml
- emf/emf.target
- - 'name': 'Verify'
- 'if': "steps.scope.outputs.needed == 'true'"
- 'working-directory': 'emf'
- 'shell': 'bash'
- 'env':
- 'MUTATION': '${{ steps.scope.outputs.mutation }}'
- 'run': |
- set -euo pipefail
- if [ "${MUTATION}" = 'true' ]; then
- ./mvnw -B -ntp verify
- else
- ./mvnw -B -ntp verify -DskipPitest=true
- fi
- - 'name': 'Summary'
- 'if': "always() && steps.scope.outputs.needed == 'true'"
- 'run': 'emf/scripts/summary.sh >> "$GITHUB_STEP_SUMMARY"'
-
# Two jobs land here together, for issue #33. Neither gates: neither joins
# `pipeline-complete`'s `needs` below, per
# docs/adr/architecture/0064-a-gate-is-a-script-or-a-named-job.md's own rule
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 60eee5f33..956ee1f5b 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -1,8 +1,7 @@
-# CodeQL static analysis: JavaScript, TypeScript, the workflow logic, and the
-# model-driven implementation's Java under emf/.
+# CodeQL static analysis: JavaScript, TypeScript, and the workflow logic.
#
-# Every language is analysed without a build (`build-mode: none`), so Tycho and
-# p2 resolution can never break the scan. Generated sources and build output
+# Every language is analysed without a build (`build-mode: none`), so builds
+# can never break the scan. Generated sources and build output
# are ignored through .github/codeql/codeql-config.yml.
#
# A gate, not a report. ci.yml calls this workflow on every pull request and on
@@ -37,7 +36,6 @@
'include':
- 'language': 'javascript-typescript'
- 'language': 'actions'
- - 'language': 'java-kotlin'
'steps':
- 'uses': 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1' # v7.0.1
diff --git a/.prettierignore b/.prettierignore
index be0cd95fe..89077b69f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -21,10 +21,10 @@ spec/
.github/actions/
.github/codeql/
-# The model-driven implementation's stage goldens are canonical JSON its own
-# writer produces, compared byte for byte, so prettier must not reflow them.
-emf/bundles/cli/src/test/resources/stages/
-
# Scratch. `.tmp/` is a working directory, not a deliverable, and its contents
# are whatever a tool last dropped there.
.tmp/
+
+# Local working artifacts, not deliverables.
+.playwright-mcp/
+.scratch/
diff --git a/AGENTS.md b/AGENTS.md
index 04f65d602..a410f1cc6 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -23,9 +23,8 @@ this file stays a pointer. Everything below is repository-specific.
## What this repository is
`deploy-kit` holds the deployment model (`spec/v1`), its decision record
-(`docs/adr/`), and the compiler that renders it: a TypeScript production
-implementation under `src/` and, for the length of a
-university course, a model-driven implementation under `emf/`. Start at
+(`docs/adr/`), and the compiler that renders it: a TypeScript implementation
+under `src/`. Start at
[`README.md`](README.md) for the shape of the model, or
[`docs/adr/README.md`](docs/adr/README.md) for why each rule is what it is.
@@ -46,10 +45,6 @@ every rule a tool enforces on it.
- Vitest for the suite, ESLint (`strictTypeChecked`, with type information)
and Prettier for style, dependency-cruiser for the module graph, Stryker for
mutation testing.
-- `emf/` is a second, separate stack: Maven and Tycho on JDK 21, building Ecore, Xtext, OCL, QVT-Operational and
- Acceleo. It has its own `AGENTS.md` equivalent in
- [`emf/docs/architecture.md`](emf/docs/architecture.md); this file only
- routes an agent into it, per [the parity contract](#the-model-driven-implementation-under-emf).
## Setup
@@ -107,7 +102,7 @@ Coverage is a ratchet
([0063](docs/adr/architecture/0063-coverage-and-mutation-are-ratchets.md)): the thresholds
in `vitest.config.ts` sit on what the suite reaches, over an explicit include
list, and only ever rise. Mutation testing holds the same shape with a break
-score in `stryker.config.json`. Neither covers `emf/`, which measures itself.
+score in `stryker.config.json`.
## Code style
@@ -138,11 +133,10 @@ tool runs it.
enforcement. Changing what `spec/v1` says the model means. Anything that
touches release, publishing or rulesets.
- **Always:** land a model change in TypeScript and in the oracle files it
- affects, in one pull request. `emf/` follows in a pull request of its own,
- held to the same oracle files by its own build. Check both ADR registers
- before taking a number.
-- **Never:** generate one implementation from the other, regenerate an oracle
- file in CI, or add a root dependency on anything under `emf/`. Push to
+ affects, in one pull request. Check the ADR register before taking a
+ number.
+- **Never:** regenerate an oracle
+ file in CI. Push to
`main` or force-push a shared branch. Edit a version or `CHANGELOG.md` by
hand. Supersede an ADR or annotate it with an amendment note: a decision
that changes is rewritten, in its one record, in the pull request that
@@ -193,7 +187,7 @@ above instead of restating them.
| skill | what it does |
|---|---|
-| [`adr`](.agents/skills/adr/SKILL.md) | Writes a decision record to this repository's own ADR contract: the right domain directory, the next estate-wide number (checking both registers while `emf/` exists), the frontmatter, premises only in `rests-on`, an Alternatives table, the register row, and an anchor that resolves. |
+| [`adr`](.agents/skills/adr/SKILL.md) | Writes a decision record to this repository's own ADR contract: the right domain directory, the next estate-wide number, the frontmatter, premises only in `rests-on`, an Alternatives table, the register row, and an anchor that resolves. |
| [`spec-change`](.agents/skills/spec-change/SKILL.md) | Edits a spec chapter as one change: its anchors, the ADRs that point at it, `CONTEXT.md`'s vocabulary, the worked examples, the rendered trees, the oracle files, the diagrams, and both implementations' declarations. |
| [`gate-triage`](.agents/skills/gate-triage/SKILL.md) | Goes from a failing CI job or a quoted rule id to its ledger row and the ADR that decided it, so a red gate resolves to the document that explains it rather than a guess. |
| [`new-rule`](.agents/skills/new-rule/SKILL.md) | Adds a rule ledger row, its enforcement and a failing fixture that proves it, in one change, per [0066](docs/adr/architecture/0066-every-enforced-rule-has-an-id-a-row-and-a-fixture.md). |
@@ -252,48 +246,18 @@ both. See [`docs/agents/domain.md`](docs/agents/domain.md).
structure, normative for code.
- [`docs/architecture-rules.md`](docs/architecture-rules.md) and
[`docs/requirements.md`](docs/requirements.md): the rule ledger and the
- behaviour ledger, two different questions kept in two different documents.
-
-## The model-driven implementation under `emf/`
-
-The MDE course requires the compiler to be built with Ecore, Xtext, OCL,
-QVT-Operational and Acceleo, so `emf/` holds the model-driven implementation:
-a second, hand-written Java implementation beside the TypeScript production
-implementation (Maven and Tycho, JDK 21, no Eclipse IDE to build, but
-loadable in Eclipse for the course's examiners). A model change lands in
-TypeScript first, and `emf/` catches up in a pull request of its own, held to
-the same oracle files:
-
-- **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, which runs the build with its tests and coverage gate
- on every change to `emf/`, its mutation gate nightly, and is needed by no
- gate; the `emf` ADR lint step; and CodeQL's `java-kotlin` entry), `test/emf-wiring.test.ts`
- with its ledger rows, the `emf` domain in `scripts/lint-adrs.ts` and its test, the release-please and Renovate
- configuration, 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 TypeScript first.** `emf/` may trail `main` between
- catch-ups, and its `OracleDistanceTest` shows how far; the links, meaning
- and codes lints skip it.
-- 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`.
+ behaviour ledger, two different questions kept in two different documents.## MDE coursework material
+
+`docs/mde/` holds a university course's reports, frozen as delivered: one
+directory per task, plus [`docs/mde/README.md`](docs/mde/README.md), which
+says which directory mirrors which task. The course's own assignment text
+(`assignment.md`, `practicals.md`) and the course-material submodule are
+retired later, on #354, once the grade is recorded.
`deploy-config-schema` is the repository this one replaced. `deploy-kit`
renders and delivers the whole estate, so it is archived: read it for
history, never as a source of truth.
-## MDE coursework material
-
-`docs/mde/` holds a university course's reports: one directory per task, plus
-[`docs/mde/assignment.md`](docs/mde/assignment.md), the course's own task text,
-which says what each task requires and hands in, and
-[`docs/mde/practicals.md`](docs/mde/practicals.md), what each practical session
-asks. [`docs/mde/README.md`](docs/mde/README.md) says which directory mirrors
-which task.
-
The course's own hand-outs (the lecture decks, the background reading and the
index over them) are **not in this repository**. They are third-party works, so
they live in the private repository `JorisJonkers-dev/mde-course-material`,
diff --git a/CONTEXT.md b/CONTEXT.md
index 568bf83bf..ea28e0c25 100644
--- a/CONTEXT.md
+++ b/CONTEXT.md
@@ -27,11 +27,8 @@ 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 Zod schemas in
-`src/model/`;
-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.
+whole. The implementation declares its languages as Zod schemas in
+`src/model/`. 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
@@ -789,10 +786,6 @@ for a DNS name, an ADR decision domain, and the core ring of the compiler's
hexagon.
**Bootstrap.** The bootstrap set and the bootstrap order, and nothing else.
-The first build of the model-driven implementation under `emf/` is the **EMF
-scaffold** (its Maven build, gates and CI job, with no EMF dependency), and its
-first change that depends on the modelling tools is the **walking skeleton**,
-which proves each tool runs headless.
**Config.** Avoid. Env files carry *configuration*; the platform's facts and
policies are *Platform Intent*; an Application's authored document is *Project Intent*.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 9c50ed324..2cf8ec322 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -34,8 +34,7 @@ document or one gate. A change is bigger than that shape when it:
- adds a gate (its npm script and its CI job, in the same pull request;
[0064](docs/adr/architecture/0064-a-gate-is-a-script-or-a-named-job.md)), or
- changes what `spec/v1` says the model means, in which case it lands with
- the ADR that decides it, in TypeScript first (`emf/` catches up in a pull
- request of its own), and with every oracle file the change touches.
+ the ADR that decides it and with every oracle file the change touches.
`AGENTS.md`'s Boundaries section states which of these an agent may do on its
own and which need to be asked about first.
diff --git a/README.md b/README.md
index 870c00114..aacebea95 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,6 @@ lands, the **compiler** that turns that model into deployable artifacts.
| [`spec/v1/diagrams/`](spec/v1/diagrams/README.md) | One drawn diagram per chapter, as an SVG with the editable draw.io diagram embedded. One palette; colour carries the layer. |
| [`spec/v1/examples/minimal/`](spec/v1/examples/minimal/README.md) | The smallest complete Application: one project, one Application, one Process, 31 authored lines reaching 12 objects. |
| [`spec/v1/examples/`](spec/v1/examples) | Worked examples: real Applications from this estate, written in the model. |
-| [`emf/`](emf/README.md) | The model-driven implementation: a Java build on Ecore, Xtext, OCL, QVT-Operational and Acceleo, held to parity with the production implementation and deleted after the course. |
| [`scripts/`](scripts/) | The gates: the ADR contract, links, manifests and layer boundaries. TypeScript that Node runs directly ([tooling](docs/architecture.md#tooling)). |
## The shape of the model
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 5baf047f7..3d65fb693 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -35,10 +35,6 @@ resolves without knowing which domain it lands in.
| [`architecture/`](architecture/README.md) | the compiler's own structure: layering, the trace, the error model, gates | `docs/architecture.md`, `docs/architecture-rules.md` | yes |
| [`deferred/`](deferred/README.md) | co-testing and multi-cluster estates, parked | sections the chapters deliberately lack | no |
-The model-driven implementation keeps its records at
-[`emf/docs/adr/`](../../emf/docs/adr/README.md), numbered from the same sequence
-and linted by the same script with `emf` as its root.
-
## Premises
@@ -209,7 +205,6 @@ Decisions about the compiler's own structure, not about the model; see
| [0065](architecture/0065-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 |
| [0066](architecture/0066-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, and a rule not enforced yet says so with a reason | settled |
| [0067](architecture/0067-every-spec-error-code-is-proved-by-a-test.md) | Every error code the specification defines is proved by a test, or pending on the ticket that will prove it | settled |
-| [0068](architecture/0068-two-implementations-meet-at-the-parity-table.md) | Two hand-written implementations meet at the oracles the parity table lists, each tree keeps its own rule for generated files, and constraint parity is checked by code | open |
| [0088](architecture/0088-the-committed-schemas-and-their-corpus-are-the-contract.md) | The committed JSON Schemas and their accept/refuse corpus are the contract, and byte equality only proves the generator's output is committed | settled |
| [0089](architecture/0089-the-package-ships-the-command-built-when-it-is-packed.md) | The package ships the command, built to JavaScript when it is packed, and the repository still runs its TypeScript directly | settled |
diff --git a/docs/adr/architecture/0056-the-compiler-is-a-chain-of-typed-models.md b/docs/adr/architecture/0056-the-compiler-is-a-chain-of-typed-models.md
index b869c1df8..4a0705c62 100644
--- a/docs/adr/architecture/0056-the-compiler-is-a-chain-of-typed-models.md
+++ b/docs/adr/architecture/0056-the-compiler-is-a-chain-of-typed-models.md
@@ -92,5 +92,3 @@ move: a day. Becomes irreversible once: an out-of-tree adapter imports a
fixture, and one new row forbids a step importing another step.
- `docs/architecture.md#layers` and `#ports` are rewritten to the chain in the
same slice.
-- The model-driven implementation shares the mapping names and nothing else
- ([0068](0068-two-implementations-meet-at-the-parity-table.md)).
diff --git a/docs/adr/architecture/0062-tests-run-in-process-on-vitest.md b/docs/adr/architecture/0062-tests-run-in-process-on-vitest.md
index 076605546..8ec5194f0 100644
--- a/docs/adr/architecture/0062-tests-run-in-process-on-vitest.md
+++ b/docs/adr/architecture/0062-tests-run-in-process-on-vitest.md
@@ -9,9 +9,7 @@ rests-on: ["0001"]
# Tests run in-process on Vitest, and the tooling is TypeScript that Node runs directly
-This decision covers the repository root. The model-driven implementation
-under `emf/` is tested with JUnit through Maven, inside `emf/`
-([0068](0068-two-implementations-meet-at-the-parity-table.md)).
+This decision covers the repository root.
## Rests on
Every gate this repository runs can be exercised in-process by one test runner
diff --git a/docs/adr/architecture/0063-coverage-and-mutation-are-ratchets.md b/docs/adr/architecture/0063-coverage-and-mutation-are-ratchets.md
index 70d87165e..e17200c39 100644
--- a/docs/adr/architecture/0063-coverage-and-mutation-are-ratchets.md
+++ b/docs/adr/architecture/0063-coverage-and-mutation-are-ratchets.md
@@ -178,6 +178,6 @@ Undo cost today: numbers in two config files. Becomes irreversible once: never.
- One top-level statement is the smallest share a shard takes, so a change that
re-tests every mutant of one large statement waits on that statement.
- A pull request passes the gate without a shard running; so does a push that
- changes only documents, the release files or `emf/`.
+ changes only documents or the release files.
- A mutant can sit undetected on `main` for up to a week when only a fixture,
a test helper or a dependency changed. The weekly issue is where it surfaces.
diff --git a/docs/adr/architecture/0067-every-spec-error-code-is-proved-by-a-test.md b/docs/adr/architecture/0067-every-spec-error-code-is-proved-by-a-test.md
index 003e8bd6c..0e5fb2f88 100644
--- a/docs/adr/architecture/0067-every-spec-error-code-is-proved-by-a-test.md
+++ b/docs/adr/architecture/0067-every-spec-error-code-is-proved-by-a-test.md
@@ -44,9 +44,7 @@ exercising, so the two implementations meet this rule on the same codes.
## Reversibility
Undo cost today: deleting one gate and its test. Becomes irreversible once:
-never; the constraint parity test
-([0068](0068-two-implementations-meet-at-the-parity-table.md)) reads the same
-codes.
+never; the tests that name a code read the same codes.
## Consequences
- A change that adds a code to a chapter adds a test or a pending entry. Paid by
diff --git a/docs/adr/architecture/0068-two-implementations-meet-at-the-parity-table.md b/docs/adr/architecture/0068-two-implementations-meet-at-the-parity-table.md
deleted file mode 100644
index 884157656..000000000
--- a/docs/adr/architecture/0068-two-implementations-meet-at-the-parity-table.md
+++ /dev/null
@@ -1,97 +0,0 @@
----
-tier: decision
-status: accepted
-claim: open
-owner: joris
-date: 2026-09-29
-normative: docs/architecture.md#the-parity-contract
-rests-on: ["0003", "0006"]
----
-
-# Two hand-written implementations meet at the oracles the parity table lists, each tree keeps its own rule for generated files, and constraint parity is checked by code
-
-The TypeScript production implementation and the model-driven implementation
-under `emf/` are both written by hand. Neither is generated from the other, and
-neither is tested against the other. Each is tested on its own against the
-committed oracle files the table in
-[`docs/architecture.md#the-parity-contract`](../../architecture.md#the-parity-contract)
-lists, with the implementation each binds; `resolved.json` binds the production
-implementation only. An oracle file is reviewed by hand and never written by CI;
-a tool may write a candidate. A generated file in the root tree is committed and
-diff-checked in CI; the model-driven tree generates its Java and Xtext sources at
-build time and commits neither. Constraint parity is a derived check: a test
-lists every specification code three ways (the Complete OCL invariant named by
-it, the TypeScript check named by it, the refusal fixture whose diagnostics name
-it) and fails on any gap. A behaviour row that proves a production-only oracle is
-marked production-only and needs no model-driven witness. A model change lands
-in TypeScript and its oracle files first, and the model-driven implementation
-catches up in a pull request of its own, held to the same oracles by its own
-build: its tests and coverage gate run on every change to `emf/`, its mutation
-gate nightly, and it is not in `Pipeline Complete`, so it may trail `main`
-between catch-ups.
-
-## Rests on
-
-The layer boundaries give points where a run's state is a complete document
-([0003](../model/0003-three-model-pipeline.md)), and pinned inputs make each a
-function of the input ([0006](../model/0006-pinned-inputs.md)), so two
-independent implementations can be held equal through committed files without
-running beside each other.
-
-**False if:** the two disagree on a case every oracle binding both covers and no
-oracle fails, or a constraint is enforced by one implementation and caught by no
-check. **Settled by:** both implementations green against every oracle the table
-marks as binding both, and the code parity test failing on today's gap:
-`E_UNKNOWN_ENV_SCOPE` has a TypeScript check and no invariant or fixture.
-
-## Why
-
-**The course requires a second implementation.** The graded work uses Ecore,
-Xtext, OCL, QVT-Operational and Acceleo, and grades the modelling artifacts as
-authored work, so neither side may be generated from the other. Two
-hand-written implementations can drift, so equality is proven.
-
-**Oracles name the side that is wrong.** A diff between two runs never does. A
-committed canonical copy at each boundary lets each suite run alone.
-
-**The table is the contract.** A record that restated the oracle list went stale
-when `resolved.json` became production-only and `effective.json` was added.
-Pointing at the table keeps one list.
-
-**Generated files follow their tree.** A derived file that is not committed is
-invisible to review; one committed but unchecked drifts. The model-driven build
-regenerates its Java from the metamodel on every build, where a committed copy
-would be a second, staler one
-([0073](../../../emf/docs/adr/emf/0073-source-and-target-metamodels-are-hand-written.md)).
-
-**Names are the key for constraint parity.** The OCL names every invariant after
-its code, and the TypeScript checks are one function per code with the same
-name, so the three-way comparison needs no hand-kept ledger of constraints.
-
-## Alternatives
-
-| option | cost if taken | why rejected |
-|---|---|---|
-| Generate one implementation from the other | no structural parity tests | the course grades both as authored work, and a generated side cannot fail on its own |
-| Diff the two implementations' outputs in CI | no committed intermediate files | a red diff does not say which side is wrong |
-| Only the rendered tree as oracle | uses what exists | a resolution bug and a rendering bug look the same |
-| A hand-written `CONS-NNN` constraint ledger | a readable register with stable ids | a third ledger duplicating what the names already say |
-| Let CI refresh oracle files | no hand edits | an implementation becomes its own oracle |
-
-## Reversibility
-
-Undo cost today: the parity test and a ledger marker: an hour. Becomes
-irreversible once: the model-driven implementation is deleted at its sunset,
-when this record's two-implementation half goes with it
-([0071](../../../emf/docs/adr/emf/0071-emf-is-coursework-scoped-and-self-contained.md)).
-
-## Consequences
-
-- An oracle file changes in the pull request that changes the behaviour it
- records, the production implementation goes red until it is fixed, and the
- model-driven implementation's `OracleDistanceTest` shows the gap until a
- catch-up closes it.
-- The behaviour ledger gains a production-only marker, and the model-driven
- witness check skips rows carrying it.
-- The constraint parity test is to be written, owned by joris, and fails first
- on `E_UNKNOWN_ENV_SCOPE`.
diff --git a/docs/adr/architecture/0088-the-committed-schemas-and-their-corpus-are-the-contract.md b/docs/adr/architecture/0088-the-committed-schemas-and-their-corpus-are-the-contract.md
index 6db861552..c4a6ff049 100644
--- a/docs/adr/architecture/0088-the-committed-schemas-and-their-corpus-are-the-contract.md
+++ b/docs/adr/architecture/0088-the-committed-schemas-and-their-corpus-are-the-contract.md
@@ -49,9 +49,10 @@ JorisJonkers-dev/deploy-kit#223 the published Resolved Deployment schema
accepted documents the model refused, and the byte check stayed green. Only a
test that puts the same document to both catches that.
-**Committed and hand-written, like every oracle.** A corpus regenerated by an
-implementation would agree with that implementation by construction
-([0068](0068-two-implementations-meet-at-the-parity-table.md)).
+**Committed and hand-written, like every oracle.** An oracle file is reviewed
+by hand and never written by CI; a tool may write a candidate. A corpus
+regenerated by an implementation would agree with that implementation by
+construction.
## Alternatives
diff --git a/docs/adr/architecture/README.md b/docs/adr/architecture/README.md
index 24477f2d8..fb9f917fd 100644
--- a/docs/adr/architecture/README.md
+++ b/docs/adr/architecture/README.md
@@ -16,8 +16,6 @@ Numbers come from the one estate-wide sequence; the register is
[`../README.md`](../README.md). The scope boundary is stated in
[chapter 00](../../../spec/v1/00-overview.md#programme-scope).
-The records here bind the TypeScript tree. The model-driven implementation
-under `emf/` decides its own structure in
-[`emf/docs/adr/`](../../../emf/docs/adr/README.md), and
-[0068](0068-two-implementations-meet-at-the-parity-table.md) is the contract
-between the two.
+The records here bind the TypeScript tree:
+[`docs/architecture.md#the-oracles`](../../architecture.md#the-oracles) is the
+contract between them.
diff --git a/docs/adr/model/0012-shared-intent-descends-and-is-lowered.md b/docs/adr/model/0012-shared-intent-descends-and-is-lowered.md
index bb8363e4a..38c062b76 100644
--- a/docs/adr/model/0012-shared-intent-descends-and-is-lowered.md
+++ b/docs/adr/model/0012-shared-intent-descends-and-is-lowered.md
@@ -116,5 +116,3 @@ exist under paths only some Processes hold.
touching one, paid by the lowering, which grows one mapping per family.
- The Effective Intent has no schema version and no digest; the pinned-input
chain runs from the authored documents ([0006](0006-pinned-inputs.md)).
-- Both implementations carry a lowering, held equal by the Effective Intent
- oracle ([0068](../architecture/0068-two-implementations-meet-at-the-parity-table.md)).
diff --git a/docs/adr/model/0113-a-privileged-role-names-the-project-that-holds-it.md b/docs/adr/model/0113-a-privileged-role-names-the-project-that-holds-it.md
index 7f2346b9d..7282fde3f 100644
--- a/docs/adr/model/0113-a-privileged-role-names-the-project-that-holds-it.md
+++ b/docs/adr/model/0113-a-privileged-role-names-the-project-that-holds-it.md
@@ -28,7 +28,7 @@ Application under a role's id and that Application receives the role: its
Process admitted to the API, its surface asked by a Canary, or its address
handed out as the collector or the Secret Store.
**Settled by:** the negative fixture per role in `test/model/intent-set.test.ts`
-and the matching linking test in `emf/`, each refusing the reference as
+and the matching linking test, each refusing the reference as
unlinked when the named Project does not declare the Application.
## Why
diff --git a/docs/agents/domain.md b/docs/agents/domain.md
index 98c763889..2ccbef452 100644
--- a/docs/agents/domain.md
+++ b/docs/agents/domain.md
@@ -38,8 +38,6 @@ documentation when exploring it. Single context, one layout, no
│ ├── model/ ← v1 model decisions, point at spec/v1
│ ├── architecture/ ← compiler decisions, point at docs/architecture.md
│ └── deferred/ ← co-testing and retired delivery, not linted
-└── emf/ ← a second implementation, with its own
- docs/adr/ numbered from the same sequence
```
## Use the glossary's vocabulary
diff --git a/docs/architecture-rules/gates.md b/docs/architecture-rules/gates.md
index a45b74b1a..786235f6e 100644
--- a/docs/architecture-rules/gates.md
+++ b/docs/architecture-rules/gates.md
@@ -5,7 +5,7 @@ Rules on this repository's own documents and pipeline.
| id | family | the rule | enforced by | proved by |
|---|---|---|---|---|
| RULE-049 | gates | Every decision record satisfies the frontmatter, register, citation and normative-anchor contract | `npm:lint:adrs` | [test/adr-lint-negative.test.ts](../../test/adr-lint-negative.test.ts) `missing frontmatter block` |
-| RULE-050 | gates | Every relative link and heading anchor in Markdown git does not ignore, outside `emf/`, resolves | `npm:lint:links` | [test/link-contract.test.ts](../../test/link-contract.test.ts) `anchor #the-old-name not found` |
+| RULE-050 | gates | Every relative link and heading anchor in Markdown git does not ignore resolves | `npm:lint:links` | [test/link-contract.test.ts](../../test/link-contract.test.ts) `anchor #the-old-name not found` |
| RULE-051 | gates | Every rendered example object validates against its pinned Kubernetes or CRD schema | `npm:lint:manifests` | [test/manifest-contract.test.ts](../../test/manifest-contract.test.ts) `no rendered examples found` |
| RULE-052 | gates | Every behaviour ledger row sits in an area file, parses, carries an id no other row carries, names a real non-empty test, and is cited by no dangling id | `npm:lint:requirements` | [test/requirements-lint-negative.test.ts](../../test/requirements-lint-negative.test.ts) `holds a row, which belongs in an area file` |
| RULE-053 | gates | Every script, path, coverage number and Node version README.md and CONTRIBUTING.md name matches the repository | `npm:lint:docs` | [test/docs-contract.test.ts](../../test/docs-contract.test.ts) `is not a script in package.json` |
@@ -16,8 +16,6 @@ Rules on this repository's own documents and pipeline.
| RULE-058 | gates | No em-dash enters tracked text outside `docs/mde/` and `CHANGELOG.md` | `file:test/emdash.test.ts` | [test/emdash.test.ts](../../test/emdash.test.ts) `contains an em-dash` |
| RULE-059 | gates | A gate's npm script and the CI job that runs it land together, or the script is listed pending with a reason | `file:test/pipeline-wiring.test.ts` | [test/pipeline-wiring.test.ts](../../test/pipeline-wiring.test.ts) `every script either runs in some workflow` |
| RULE-060 | gates | No committed secret matching the default gitleaks ruleset or this repository's own allowlist, checked locally by the same command CI runs | `npm:lint:secrets` | [test/secret-scan-contract.test.ts](../../test/secret-scan-contract.test.ts) `secret scan: could not run` |
-| RULE-061 | gates | A workflow step that runs the Maven wrapper runs it in a directory holding a POM and the wrapper, and the model-driven reactor names only modules on disk | `file:test/emf-wiring.test.ts` | [test/emf-wiring.test.ts](../../test/emf-wiring.test.ts) `which names a POM that does not exist` |
-| RULE-062 | gates | CodeQL analyses the model-driven implementation's Java without a build, ignoring build output and generated sources | `file:.github/codeql/codeql-config.yml` | [test/emf-wiring.test.ts](../../test/emf-wiring.test.ts) `'language': 'java-kotlin'` |
| RULE-063 | gates | A CodeQL finding of any severity fails `Pipeline Complete`, unless the finding is filtered in the CodeQL configuration | `file:.github/workflows/codeql.yml` | [test/pipeline-wiring.test.ts](../../test/pipeline-wiring.test.ts) `'name': 'Fail on any finding'` |
| RULE-064 | gates | A push to `main` re-tests each mutant under `src/` whose module or killing test changed since the last green `main` run, and a pull request runs none, in parallel shards, and a mutation score below the measured break threshold fails the build | `file:.github/workflows/ci.yml` | [test/mutation-contract.test.ts](../../test/mutation-contract.test.ts) `npm run test:mutation -- --incremental` |
| RULE-065 | gates | Every file a pull request changes falls into a named shape bucket; a path no rule claims fails the fallback test rather than joining one silently | `file:scripts/lib/change-buckets.ts` | [test/change-buckets.test.ts](../../test/change-buckets.test.ts) `keeps every file this repository tracks inside a named bucket` |
@@ -28,4 +26,3 @@ Rules on this repository's own documents and pipeline.
| RULE-070 | gates | A decision record whose claim is settled carries `status: accepted`, in both registers, so the field distinguishes something | `npm:lint:adrs` | [test/adr-lint-negative.test.ts](../../test/adr-lint-negative.test.ts) `requires status accepted, got 'proposed'` |
| RULE-075 | gates | `Pipeline Complete` needs the merging `mutation` job, which never skips and fails when the scope job or any mutation shard did not succeed; the shards run only where the scope job says the change reaches a mutant | `file:.github/workflows/ci.yml` | [test/pipeline-wiring.test.ts](../../test/pipeline-wiring.test.ts) `'name': 'Fail: the scope or a shard did not succeed'` |
| RULE-076 | gates | Every rendered pod template meets Pod Security's `restricted` profile and the posture's read-only root filesystem, save the platform's VPN native sidecar, which is outside the posture and may add only `NET_ADMIN` and `NET_RAW`; a rendered file that does not parse fails rather than passing unread; the check runs in-process, so a machine without kubeconform still runs it | `npm:lint:manifests` | [test/manifest-contract.test.ts](../../test/manifest-contract.test.ts) `refuses a pod that breaks the restricted profile` |
-| RULE-077 | gates | No workflow, and not the committed `emf/.mvn/maven.config`, passes the model-driven build's `quick` profile, which skips the coverage floor, PIT and the Spotless check for a local edit, so CI keeps every gate | `file:test/emf-wiring.test.ts` | [test/emf-wiring.test.ts](../../test/emf-wiring.test.ts) `passes the quick profile to the model-driven build` |
diff --git a/docs/architecture.md b/docs/architecture.md
index 62054c627..9c9b1d4f9 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -183,7 +183,7 @@ that will exercise it, and no code a chapter does not define is used in the
tree; `npm run lint:codes` holds both
([0067](adr/architecture/0067-every-spec-error-code-is-proved-by-a-test.md)). A
test exercises a code by naming it, in a TypeScript test or a refused case's
-committed `diagnostics.json`; `emf/` is out of its scope.
+committed `diagnostics.json`.
The CLI renders diagnostics for a human by default, emits the array verbatim
under `--json`, and maps failure classes to distinct exit statuses.
@@ -194,8 +194,7 @@ A file a tool derives from another committed file is committed too, and CI
regenerates it and fails on a diff, from the pull request that brings its
generator: the JSON Schema derived from the metamodel, and the diagnostic
catalogue. A reader sees the artifact without running anything, and a change
-to the source cannot land without the artifact that depends on it
-([0068](adr/architecture/0068-two-implementations-meet-at-the-parity-table.md)).
+to the source cannot land without the artifact that depends on it.
An oracle file is the opposite. The rendered example trees, `intent.json`,
`resolved.json`, `dependencies.json`, `diagnostics.json` and the descriptor are
@@ -302,59 +301,38 @@ 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 two implementations. The TypeScript code under `src/` is the
-**production implementation**. The model-driven engineering course this
-repository is coursework for requires Ecore, Xtext, OCL, QVT-Operational and
-Acceleo, so a **model-driven implementation** in Java lives under
-[`emf/`](../emf/README.md)
-until its sunset condition holds
-([0068](adr/architecture/0068-two-implementations-meet-at-the-parity-table.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 model-driven implementation.
-
-**The contract holds with a lag.** A model change lands in TypeScript and its
-oracle files first, and `emf/` catches up in a pull request of its own. Its
-build runs its tests and coverage gate on every change to `emf/`, and its
-mutation gate nightly; it is not in `Pipeline Complete`, so `emf/` may trail
-`main` between catch-ups, and its `OracleDistanceTest` reports how far.
-
-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.
-
-The two are compared on what the project proposal names: validation, dependency
-resolution, errors and generated resources. They are not required to agree on
-anything else. In particular their intermediate models differ: the production
-implementation keeps the Resolved Deployment and the typed object model its
-adapters build as two things, while the model-driven implementation has one
-target metamodel holding both, because a model-to-text template reads one model.
-
-| oracle | where | compared as | binds |
-|---|---|---|---|
-| the parsed Project Intent (validation) | `spec/v1/examples//expected/intent.json` | canonical JSON, byte for byte | both |
-| the diagnostics of a refused case (errors) | `.diagnostics.json` beside the refused input, or the refused set of documents, in `refusals/` or `negative/` | a set of `(code, document, path)` triples | both |
-| the resolved dependency edges (dependency resolution) | `spec/v1/examples//expected/dependencies.json` | canonical JSON, byte for byte | both |
-| the Deliverable Set (generated resources) | `spec/v1/examples//rendered/` | the existing golden tree, byte for byte | both |
-| the source metamodel's **authored** structure | `spec/v1/examples/expected/descriptor.json` | canonical JSON, byte for byte | both |
-| the Effective Intent (the lowering) | `spec/v1/examples//expected/effective.json` | canonical JSON, byte for byte | both |
-| the Resolved Deployment | `spec/v1/examples//expected/resolved.json` | canonical JSON, byte for byte | production only |
-| the diagnostics of a file the reader refuses (code `schema`) | `.diagnostics.json` beside the refused input, in `schema-refusals/` | a set of `(code, document, path)` triples | production only |
-
-**A schema refusal binds the production implementation only.** The
-model-driven implementation reads a file through a grammar, which refuses a
-token where the production reader names the field the token belongs to, so the
-two agree on the code and not on the place
+## The oracles
+
+The compiler is tested against committed oracle files, one per boundary a run
+crosses. The implementation is never its own oracle: a test compares a run
+against the committed file, and a change to the source cannot land quietly
+redesigning what a stage writes.
+
+The project proposal names what an oracle covers: validation, dependency
+resolution, errors and generated resources.
+
+| oracle | where | compared as |
+|---|---|---|
+| the parsed Project Intent (validation) | `spec/v1/examples//expected/intent.json` | canonical JSON, byte for byte |
+| the diagnostics of a refused case (errors) | `.diagnostics.json` beside the refused input, or the refused set of documents, in `refusals/` or `negative/` | a set of `(code, document, path)` triples |
+| the resolved dependency edges (dependency resolution) | `spec/v1/examples//expected/dependencies.json` | canonical JSON, byte for byte |
+| the Deliverable Set (generated resources) | `spec/v1/examples//rendered/` | the existing golden tree, byte for byte |
+| the source metamodel's **authored** structure | `spec/v1/examples/expected/descriptor.json` | canonical JSON, byte for byte |
+| the Effective Intent (the lowering) | `spec/v1/examples//expected/effective.json` | canonical JSON, byte for byte |
+| the Resolved Deployment | `spec/v1/examples//expected/resolved.json` | canonical JSON, byte for byte |
+| the diagnostics of a file the reader refuses (code `schema`) | `.diagnostics.json` beside the refused input, in `schema-refusals/` | a set of `(code, document, path)` triples |
+
+**A schema refusal reports a different place than a model refusal would.** The
+reader names the field the token belongs to, where a refusing grammar would
+point at the token
([chapter 10](../spec/v1/10-project-intent.md#what-a-schema-refusal-reports)
fixes the place). The refused cases a rule answers, in `refusals/` and
-`negative/`, bind both.
+`negative/`, bind the model's own behaviour exactly.
**The dependency edges** are, per Application, every dependency edge after
resolution: the consumer, the provider Application and Surface, the address the
consumer is given, and the policy peers that allow the connection. It is the
-part of resolution both implementations must agree on before either renders.
+part of resolution that must settle before the render runs.
The file is an object with one `applications` entry per Application, each an
`id` and its `edges`; an Application with no dependency carries an empty list.
The edge's own fields are fixed by the first case that has one.
@@ -363,23 +341,21 @@ The edge's own fields are fixed by the first case that has one.
([chapter 10](../spec/v1/10-project-intent.md#the-effective-intent)): the
authored document and the env files beside it, merged onto the Processes that
hold them. It is the one oracle that covers the env files at all, because they
-are not part of the document `intent.json` fixes, and it is where the two
-implementations' readers meet.
+are not part of the document `intent.json` fixes.
**The descriptor fixes the authored shape**, which is every class a document
holds. `env` is the one feature it leaves out of a class: an env file is a
directory beside the document rather than a key in it, so its own classes are
-listed and the feature that would carry them is not. A shape a transformation writes and nobody authors is left out of it: the
-Effective Intent's Project and Application are the two today
-([0012](adr/model/0012-shared-intent-descends-and-is-lowered.md)), and what holds
-them equal is the same thing that holds every other stage equal, the oracles
-downstream of the lowering. An implementation that declared a lowered class where
-the other did not would still render the same tree or fail the rendered oracle.
-
-**The canonical writers** are `src/infrastructure/canonical-json.ts` and
-`emf/bundles/metamodel`'s `CanonicalJson`, held to the same cases. An oracle file is exactly
-its canonical text, with no final newline, and a test fails any committed oracle
-that is not byte-identical to its own canonicalisation.
+listed and the feature that would carry them is not. A shape a transformation
+writes and nobody authors is left out of it: the Effective Intent's Project and
+Application are the two today
+([0012](adr/model/0012-shared-intent-descends-and-is-lowered.md)), and what
+holds them equal is the same thing that holds every other stage equal, the
+oracles downstream of the lowering.
+
+**The canonical writer** is `src/infrastructure/canonical-json.ts`. An oracle
+file is exactly its canonical text, with no final newline, and a test fails any
+committed oracle that is not byte-identical to its own canonicalisation.
**Canonical JSON** is RFC 8785 (JSON Canonicalization Scheme): keys sorted,
numbers in their shortest form, no insignificant whitespace. An absent
@@ -399,44 +375,35 @@ refused single file names itself.
**The descriptor** lists every class of the source metamodel (Project Intent
and the Platform Intent it is resolved against) with its features, each
feature's type and multiplicity, and every closed vocabulary with its literals.
-Target structures are compared through what they generate, not structurally.
-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 descriptor's shape is fixed here, not by the format that produces it:
- `classes`, sorted by name. Each carries its `name` and its `features`, sorted
by name; a class the language writes as one word carries that word as
`scalar`.
- A **feature** carries its `name`, the `types` it admits sorted by name,
whether it is `required`, whether it holds `many` values, whether it is a
- `map` keyed by string, and whether it is a `reference`: a name the document
- writes that links to a model element, whose one type is the element's class.
- A map also names what one of its entries is, as `entry`, so a reference can
- point at a map's entries. A type is a class name, a vocabulary name, or one of
- `string`, `int` and `boolean`.
+ `map` keyed by string, and whether it is a
+ `reference`: a name the document writes that links to a model element, whose
+ one type is the element's class. A map also names what one of its entries is,
+ as `entry`, so a reference can point at a map's entries. A type is a class
+ name, a vocabulary name, or one of `string`, `int` and `boolean`.
- A union is not a class: a feature whose value may be one of several classes
- names them all, so an abstract class on one side and a union on the other
- describe the same model.
+ names them all, so an abstract class and a union on one side describe the
+ same model.
- `vocabularies`, sorted by name, each with its `literals` in the order the
model declares them.
-**Constraint parity is checked by code, not kept in a ledger.** Every model
-constraint is named by the diagnostic code it emits: the Complete OCL invariant
-carries the code as its name, and the TypeScript check is one function per code
-with the same name. A parity test lists every code three ways (the invariant,
-the check, and the refused fixture whose diagnostics name it) and fails on any
-gap ([0068](adr/architecture/0068-two-implementations-meet-at-the-parity-table.md)).
+**Every model constraint is named by the diagnostic code it emits**, and the
+check for a code is one function carrying the code's name, so a code's
+enforcement is findable from the code alone.
**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 model-driven witness
-for the same id is listed inside `emf/`, and `emf/`'s own gate fails when a model
-row has no witness there.
+behaviour is the model's own (parse, validate, resolve, render) is proved by
+the test the row names.
-**Whichever implementation lands a case first commits its reviewed oracle,
-and the other matches it.** The first oracles are written by hand: `minimal`'s
-parsed intent and dependency edges, and `minimal`'s and `knowledge`'s
-`resolved.json` beside the metamodel that gives them a shape.
+A case arrives with its reviewed oracle, written by hand: `minimal`'s parsed
+intent and dependency edges, and `minimal`'s and `knowledge`'s `resolved.json`
+beside the metamodel that gives them a shape.
A `resolved.json` is held to the chapter as well as to the metamodel.
[Chapter 20](../spec/v1/20-resolved-deployment.md#worked-example-knowledges-projection)'s
@@ -446,15 +413,15 @@ value. The chapter is the readable half and the oracle is the complete one;
neither may say something the other contradicts.
An oracle file changes in the pull request that changes the behaviour it
-records, and the TypeScript implementation goes red until it is 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.
+records, and the implementation goes red until it is fixed. CI never
+regenerates an oracle file: a tool may write a candidate, and the committed
+file is the reviewed copy of it. A case without every oracle file it needs is
+not yet a complete case, and the ledger that lists cases says so rather than
+skipping it silently.
## Gates
-Twenty gates hold the structure, and each exists because its absence has already
+Nineteen gates hold the structure, and each exists because its absence has already
cost something in the generation this compiler replaces. Gates are grouped
into CI jobs, aggregated by one required check that fails when any gate job fails,
is cancelled, or is skipped
@@ -483,18 +450,13 @@ proves the two never drift apart.
| package contents | `node scripts/check-package-contents.ts` | `npm pack` shipping a file outside `docs/adr/`, `spec/` and the JavaScript under `dist/`, the boundary the package's `files` field states but does not enforce on its own |
| actionlint | a pinned `actionlint` binary | invalid workflow syntax, an undefined `${{ }}` expression, a shellcheck finding inside a `run:` step |
| secret scan | `npm run lint:secrets` | a committed secret matching the default ruleset, or this repository's own allowlist entries |
-| code scanning | CodeQL, called from `ci.yml` as the `codeql` job | any finding, of any severity, in JavaScript, TypeScript, workflow logic or the Java under `emf/`; a wrong one is filtered in `.github/codeql/codeql-config.yml` with its reason |
-| model-driven build | `./mvnw -B -ntp verify` in `emf/` | every gate the [model-driven implementation](../emf/docs/architecture.md#gates) holds itself to: toolchain versions, compiler warnings, tests, coverage and mutation floors, formatting |
+| code scanning | CodeQL, called from `ci.yml` as the `codeql` job | any finding, of any severity, in JavaScript, TypeScript or the workflow logic; a wrong one is filtered in `.github/codeql/codeql-config.yml` with its reason |
Decisions, links, manifests, requirements, rules, codes, docs, agents and
meaning share one CI job, `contracts`: all nine check a document against a
rule rather than code against a graph.
Boundaries runs alone as `architecture`, because it is the one gate that
speaks for `docs/architecture.md` itself rather than for a document beside it.
-The model-driven build runs alone as `emf`, on its own JDK and Maven: its
-tests and coverage gate on every change to `emf/` or the workflow, and its
-mutation gate nightly and on a dispatch. It is not in `Pipeline Complete`, so
-it gates nothing, and it is deleted with `emf/`.
Every rule these gates enforce is written down once, with a greppable id, in
the [rule ledger](architecture-rules.md)
diff --git a/docs/mde/README.md b/docs/mde/README.md
index 389302afb..f82c08695 100644
--- a/docs/mde/README.md
+++ b/docs/mde/README.md
@@ -15,13 +15,13 @@ One directory per deadline, named after the course's own task numbering.
| Directory | Due | Deliverable | Overleaf project | State |
|---|---|---|---|---|
| [`task-0-proposal/`](task-0-proposal/) | approved | Task 0 — project description, the proposal that must be approved before work starts | `6aa2ba0c3920baa59ec20901` | re-mirrored 2026-09-16, after the delivery and output-layout corrections in #56 |
-| [`task-1-metamodelling/`](task-1-metamodelling/) | 2026-09-25 | Task 1 — domain analysis and modelling decisions for the two metamodels, **Project Intent** (source) and **Resolved Deployment** (target), their diagrams and example models | `6aaa9f1d3920baa59ec267f7` | drafted; both metamodels exist as Ecore under [`emf/bundles/metamodel/model/`](../../emf/bundles/metamodel/model/) |
-| [`task-2-transformations/`](task-2-transformations/) | 2026-10-16 | Task 2 — the model-to-model transformation from **Project Intent** (resolved against the Platform document and the pinned inputs) to **Resolved Deployment**, its rules and limitations, evidence it runs | `6ad0ce4e3920baa59ec3d535` | written 2026-10-15; the transformations were [`resolution.qvto`](../../emf/bundles/resolve/model/resolution.qvto) and `estate.qvto`, which [#547](https://github.com/JorisJonkers-dev/deploy-kit/issues/547) has since removed |
-| [`task-3-code-generation/`](task-3-code-generation/) | 2026-10-30 | Task 3 — the model-to-text transformation from **Resolved Deployment** to the Deliverable Set, the generated code, evidence it loads | `6ae33bae3afd0f952d331ed6` | finished 2026-11-03, after the deadline; the module is [`render.mtl`](../../emf/bundles/render/model/render.mtl) |
+| [`task-1-metamodelling/`](task-1-metamodelling/) | 2026-09-25 | Task 1 — domain analysis and modelling decisions for the two metamodels, **Project Intent** (source) and **Resolved Deployment** (target), their diagrams and example models | `6aaa9f1d3920baa59ec267f7` | drafted; both metamodels exist as Ecore under [`emf/bundles/metamodel/model/`](https://github.com/JorisJonkers-dev/mde-project/tree/main/bundles/metamodel/model) |
+| [`task-2-transformations/`](task-2-transformations/) | 2026-10-16 | Task 2 — the model-to-model transformation from **Project Intent** (resolved against the Platform document and the pinned inputs) to **Resolved Deployment**, its rules and limitations, evidence it runs | `6ad0ce4e3920baa59ec3d535` | written 2026-10-15; the transformations were [`resolution.qvto`](https://github.com/JorisJonkers-dev/mde-project/blob/main/bundles/resolve/model/resolution.qvto) and `estate.qvto`, which [#547](https://github.com/JorisJonkers-dev/deploy-kit/issues/547) has since removed |
+| [`task-3-code-generation/`](task-3-code-generation/) | 2026-10-30 | Task 3 — the model-to-text transformation from **Resolved Deployment** to the Deliverable Set, the generated code, evidence it loads | `6ae33bae3afd0f952d331ed6` | finished 2026-11-03, after the deadline; the module is [`render.mtl`](https://github.com/JorisJonkers-dev/mde-project/blob/main/bundles/render/model/render.mtl) |
Each task delivers an EMF artefact, and the table above says which directory
documents it. What the artefact *is* lives in
-[`emf/docs/architecture.md`](../../emf/docs/architecture.md#modules), whose
+[`emf/docs/architecture.md`](https://github.com/JorisJonkers-dev/mde-project/blob/main/docs/architecture.md#modules), whose
module table carries the same task numbering, so the two are read together and
neither restates the other.
diff --git a/docs/mde/assignment.md b/docs/mde/assignment.md
index ced5e31ab..adea9567d 100644
--- a/docs/mde/assignment.md
+++ b/docs/mde/assignment.md
@@ -120,7 +120,7 @@ settle doubts. The report must stand on its own, carry the four parts above in
roughly those proportions, and discuss limitations and reflect on the choices.
Mirror: [`task-1-metamodelling/`](task-1-metamodelling/). The Ecore files are
-under [`emf/bundles/metamodel/model/`](../../emf/bundles/metamodel/model/); the
+under [`emf/bundles/metamodel/model/`](https://github.com/JorisJonkers-dev/mde-project/tree/main/bundles/metamodel/model); the
hand-in zip is built by
[`task-1-metamodelling/handin/build.py`](task-1-metamodelling/handin/build.py).
diff --git a/docs/mde/practicals.md b/docs/mde/practicals.md
index 19547b514..93b106580 100644
--- a/docs/mde/practicals.md
+++ b/docs/mde/practicals.md
@@ -5,7 +5,7 @@ assignment page states it. They are exercises and are separate from the
project: each is a one-point `.zip` upload. They matter here because each one
walks through a tool the project uses. Session 5 is the QVT Operational tooling
that Task 2 is built with, session 6 the Acceleo 4 tooling of Task 3, and
-sessions 1 and 3 are the Ecore and Xtext workflow behind [`emf/`](../../emf/).
+sessions 1 and 3 are the Ecore and Xtext workflow behind [`emf/`](https://github.com/JorisJonkers-dev/mde-project).
The project's own tasks are in [`assignment.md`](assignment.md). The lecture
each session follows, and its slides, recording and reading, are indexed in
diff --git a/docs/requirements.md b/docs/requirements.md
index 9c993db17..d4f3aec86 100644
--- a/docs/requirements.md
+++ b/docs/requirements.md
@@ -28,7 +28,7 @@ files. Each file holds the same three-column table.
| file | what goes there |
|---|---|
-| [`repository.md`](requirements/repository.md) | this repository's own tree: decision records, links, documents, module boundaries, the test harness, coverage, the ledgers, specification codes, the `emf/` wiring |
+| [`repository.md`](requirements/repository.md) | this repository's own tree: decision records, links, documents, module boundaries, the test harness, coverage, the ledgers, specification codes |
| [`pipeline.md`](requirements/pipeline.md) | a pull request, a push to `main` or a release: titles and commits, the pull request report, release candidates, the published package, mutation testing |
| [`intent.md`](requirements/intent.md) | the authored documents before resolution: parsing, the metamodel and its JSON Schemas, refusals, env files, Shared Intent, canonical oracle JSON |
| [`resolution.md`](requirements/resolution.md) | layer 2 as a whole: the Resolved Deployment, Application revisions, Aliases and Stable Addresses, the foundation, the render of the composed union |
diff --git a/docs/requirements/repository.md b/docs/requirements/repository.md
index 14e2ba812..1612fe685 100644
--- a/docs/requirements/repository.md
+++ b/docs/requirements/repository.md
@@ -1,11 +1,11 @@
# Repository contracts
-A behaviour of this repository's own tree: its decision records, links, documents, module boundaries, test harness, coverage, ledgers, specification codes and the `emf/` wiring.
+A behaviour of this repository's own tree: its decision records, links, documents, module boundaries, test harness, coverage, ledgers and specification codes.
| id | a contributor or a consumer can rely on | proved by |
|---|---|---|
-| 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 Markdown git does not ignore, outside `emf/`, resolves to a real target | [test/link-contract.test.ts](../../test/link-contract.test.ts) |
+| 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-002 | Every relative link and heading anchor in Markdown git does not ignore 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) |
| REQ-006 | No em-dash enters tracked text outside `docs/mde/` and `CHANGELOG.md` | [test/emdash.test.ts](../../test/emdash.test.ts) |
@@ -15,7 +15,6 @@ A behaviour of this repository's own tree: its decision records, links, document
| REQ-011 | Every script, path, coverage number and Node version README.md and CONTRIBUTING.md name matches the repository they describe | [test/docs-contract.test.ts](../../test/docs-contract.test.ts) |
| REQ-013 | `npm run verify` runs the same secret scan CI runs, failing on a committed secret rather than only after a push | [test/secret-scan-contract.test.ts](../../test/secret-scan-contract.test.ts) |
| REQ-014 | Every rule this repository enforces has a ledger row with a greppable id and a fixture that proves it fires, and a rule not enforced yet is listed as pending with a ticket and a reason rather than dropped | [test/rules-contract.test.ts](../../test/rules-contract.test.ts) |
-| REQ-015 | The model-driven build CI runs is the build in the tree: no workflow runs the Maven wrapper where there is no build, the reactor names no missing module, and CodeQL scans its Java | [test/emf-wiring.test.ts](../../test/emf-wiring.test.ts) |
| REQ-016 | A code scanning finding of any severity fails `Pipeline Complete`, so it blocks the merge rather than only landing in the Security tab | [test/pipeline-wiring.test.ts](../../test/pipeline-wiring.test.ts) |
| REQ-017 | Every error code the specification defines is exercised by a test, or pending on the ticket that will exercise it, and no code the specification does not define is used in the tree | [test/codes-lint.test.ts](../../test/codes-lint.test.ts) |
| REQ-018 | The compiler's inner rings cannot read the environment, the clock, randomness, a child process or the filesystem synchronously, and only `src/cli/boundary.ts` exits the process or writes output | [test/seams.test.ts](../../test/seams.test.ts) |
diff --git a/emf/.gitignore b/emf/.gitignore
deleted file mode 100644
index becc17471..000000000
--- a/emf/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-# Eclipse metadata m2e writes when the modules are imported; the Maven build is the source of truth.
-.project
-.classpath
-.settings/
diff --git a/emf/.java-version b/emf/.java-version
deleted file mode 100644
index aabe6ec39..000000000
--- a/emf/.java-version
+++ /dev/null
@@ -1 +0,0 @@
-21
diff --git a/emf/.mvn/maven.config b/emf/.mvn/maven.config
deleted file mode 100644
index 318d96975..000000000
--- a/emf/.mvn/maven.config
+++ /dev/null
@@ -1,2 +0,0 @@
--Daether.syncContext.named.factory=file-lock
--Daether.syncContext.named.nameMapper=file-gav
diff --git a/emf/.mvn/wrapper/maven-wrapper.properties b/emf/.mvn/wrapper/maven-wrapper.properties
deleted file mode 100644
index a3c407ffc..000000000
--- a/emf/.mvn/wrapper/maven-wrapper.properties
+++ /dev/null
@@ -1,4 +0,0 @@
-wrapperVersion=3.3.4
-distributionType=only-script
-distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
-distributionSha256Sum=5af3b743dd8b876b5c45da33b676251e5f1687712644abb4ee519ca56e1d89ce
diff --git a/emf/README.md b/emf/README.md
deleted file mode 100644
index e324241b2..000000000
--- a/emf/README.md
+++ /dev/null
@@ -1,135 +0,0 @@
-# emf: the model-driven implementation
-
-Everything under this directory is the model-driven implementation of the
-deploy-kit compiler, the second implementation beside the TypeScript production
-implementation, 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
-([0071](docs/adr/emf/0071-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 production implementation 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 |
-| [docs/benchmarks.md](docs/benchmarks.md) | how long each stage takes, and the lowering in QVTo against an ATL port of it |
-
-## Where each artefact lives
-
-This table says which artefact lives where and which course task grades
-it, so the split tree
-([0081](docs/adr/emf/0081-bundles-and-tests-are-separate-tiers.md))
-answers the navigation question without a search.
-
-| artefact | lives in | graded in | how an examiner opens it |
-|---|---|---|---|
-| the Ecore metamodels (source, target and pinned inputs) and their OCL | `emf/bundles/metamodel` | Task 1 | imported in step 2; `model/project-intent.ocl` validates `model/cache.xmi` clean and refuses `model/cache-continuous.xmi`, as step 3 describes |
-| the Xtext grammars and generated editor | `emf/bundles/syntax` | Task 1 | imported in step 2; the generated editor reports OCL constraint violations while a source file is edited in it |
-| the QVTo transformations | `emf/bundles/resolve` | Task 2 | imported in step 2; `resolution.launch` runs the resolution on `minimal`, as step 4 describes |
-| the Acceleo templates | `emf/bundles/render` | Task 3 | imported in step 2; `render.launch` runs them, as step 4 describes |
-| the pipeline entry point | `emf/bundles/cli` | Task 1 onward | imported in step 2, alongside the rest |
-| the parity suite | `emf/tests/parity` | no task grades it | it is Maven-only; no examiner opens it |
-
-## Building
-
-```sh
-cd emf
-./mvnw verify
-```
-
-JDK 21 is required; the wrapper downloads the pinned Maven, and Tycho the
-bundles `emf.target` pins.
-
-## A fast local loop
-
-The full build runs every gate, so a one-line change to a transformation
-waits minutes. While editing, build the bundles without the parity tier,
-offline, run only the test that covers the change, and skip the gates. This
-takes about half a minute:
-
-```sh
-cd emf
-./mvnw -o -Pquick verify -pl '!tests/parity' -Dtest=StageTest -Dsurefire.failIfNoSpecifiedTests=false
-```
-
-- `-Pquick` skips the JaCoCo agent and its coverage floor, PIT and the
- Spotless check. It runs the tests. CI never passes it, so every gate still
- runs on the pull request.
-- `-pl '!tests/parity'` builds every bundle and leaves out the parity tier;
- the full build runs it. `bundles/cli` holds the pipeline tests, including
- the stage cases. Maven's `-am` cannot stand in here: a bundle names the bundles
- it needs in its `META-INF/MANIFEST.MF`, not its POM, so `-pl bundles/cli -am`
- builds `cli` alone and Tycho cannot resolve the bundles it requires.
-- `-o` works offline once one full build has filled the local repository.
-- `-Dtest=` runs one test class, or `#` one method.
- `-Dsurefire.failIfNoSpecifiedTests=false` lets the modules that have no
- such test pass.
-- `-Dgolden.update=true` rewrites the stage goldens from what the stages now
- write, for review in `git diff`.
-
-[`mvnd`](https://github.com/apache/maven-mvnd), the Maven daemon, keeps a warm
-JVM between builds and takes the same arguments. It is optional: nothing here
-needs it.
-
-On a managed machine an antivirus scanner can slow every file Maven and Tycho
-read and write. Excluding the local Maven repository and the `target/`
-directories from scanning is a request to the machine's IT department, not a
-setting in this repository.
-
-## Opening in Eclipse
-
-For examiners, in Eclipse Modeling Tools 2026-06 with the OCL, QVT-Operational
-and Acceleo 4 SDKs installed from the same release:
-
-0. Run `./mvnw verify` once, from `emf/`, before importing anything. Each
- metamodel's typed Java is generated into `target/generated-sources/emf/`,
- which `build.properties` names as a source folder, and on a clean clone that
- directory does not exist yet: import first and the workspace opens with
- every reference to a generated package unresolved. One terminal build is
- what makes the import land clean.
-1. Open `emf/emf.target` and choose **Set as Active Target Platform**.
-2. **File > Import > Maven > Existing Maven Projects**, with `emf/` as the
- root directory. The importer walks the whole tree, so that one root
- still finds the five bundles nested under `emf/bundles/` and the parity
- suite nested under `emf/tests/parity`; import every module. Five of the six
- are `eclipse-plugin` packaging, which m2e maps through the Tycho
- configurator: accept the connector if the importer offers to install one,
- and restart when it asks.
-3. In `dev.jorisjonkers.deploykit.emf.metamodel`, open
- `model/project-intent.ecore`, the Project Intent metamodel. Open
- `model/cache.xmi` with the Sample Reflective Ecore Model Editor. It holds a
- small Project, as the parser reads it from YAML. `model/cache-continuous.xmi`
- is the same Project with one edit: its Process asks for a `continuous`
- cutover over a ReadWriteOnce volume. Load `model/project-intent.ocl` into
- each model's editor through **OCL > Load Document** (from the OCL Examples
- and Editors SDK), one editor at a time. An invariant across documents ranges
- over its editor's resources, so two Projects in one editor would each refuse
- the other's name. Validated against the document, `cache.xmi` breaks
- nothing, and `cache-continuous.xmi` breaks `E_CUTOVER_UNHONOURABLE` on its
- Process and nothing else. `ProjectIntentOclTest`, which step 0's build runs,
- holds both results. In Eclipse 2026-06 with OCL 6.24, the editor's
- **Validate** after the load reports neither. The command's job disposes the
- document it loaded before **Validate** runs, while the same loader kept open
- over the editor's resources reports exactly that one invariant.
-4. Run `resolution.launch` in `dev.jorisjonkers.deploykit.emf.resolve` and
- `render.launch` in `dev.jorisjonkers.deploykit.emf.render` from **Run > Run
- Configurations**. They write under each project's `target/`. The resolution
- reads the two extents step 0's build left under
- `dev.jorisjonkers.deploykit.emf.cli/target/parity/minimal/`, and its output
- equals `models/minimal.resolveddeployment`. `render.launch` is a Java
- application launch of `RenderModel`, which reads that committed model with
- its package registered in its own resource set and renders it through
- `Rendering.render`, the entry the pipeline uses: the files it writes under
- `target/render/apps/notes/` equal
- `spec/v1/examples/minimal/rendered/apps/notes/`, as `RenderModelTest` holds.
diff --git a/emf/bundles/cli/META-INF/MANIFEST.MF b/emf/bundles/cli/META-INF/MANIFEST.MF
deleted file mode 100644
index b3a3f9e87..000000000
--- a/emf/bundles/cli/META-INF/MANIFEST.MF
+++ /dev/null
@@ -1,17 +0,0 @@
-Manifest-Version: 1.0
-Bundle-ManifestVersion: 2
-Bundle-Name: deploy-kit model-driven pipeline
-Bundle-SymbolicName: dev.jorisjonkers.deploykit.emf.cli;singleton:=true
-Bundle-Version: 0.1.0.qualifier
-Bundle-RequiredExecutionEnvironment: JavaSE-21
-Automatic-Module-Name: dev.jorisjonkers.deploykit.emf.cli
-Export-Package: dev.jorisjonkers.deploykit.emf.cli
-Require-Bundle: dev.jorisjonkers.deploykit.emf.metamodel,
- dev.jorisjonkers.deploykit.emf.syntax,
- dev.jorisjonkers.deploykit.emf.resolve,
- dev.jorisjonkers.deploykit.emf.render,
- org.eclipse.emf.ecore,
- org.eclipse.emf.ecore.xmi,
- org.eclipse.xtext,
- org.eclipse.ocl.pivot,
- org.eclipse.ocl.xtext.completeocl
diff --git a/emf/bundles/cli/build.properties b/emf/bundles/cli/build.properties
deleted file mode 100644
index fe08435df..000000000
--- a/emf/bundles/cli/build.properties
+++ /dev/null
@@ -1,11 +0,0 @@
-source.. = src/main/java/
-bin.includes = META-INF/,\
- .
-# Test-only: EMF Compare decides every model comparison in the tests (docs/adr/emf/0152); EMF-021 keeps it out of shipped classes.
-# Test-only too: the benchmark's ATL port of the lowering runs on EMFTVM, beside the QVTo lowering it is
-# measured against (docs/benchmarks.md).
-additional.bundles = org.eclipse.emf.compare,\
- com.google.guava,\
- org.eclipse.m2m.qvt.oml,\
- org.eclipse.m2m.atl.emftvm,\
- org.eclipse.m2m.atl.emftvm.compiler
diff --git a/emf/bundles/cli/pom.xml b/emf/bundles/cli/pom.xml
deleted file mode 100644
index 620f6d027..000000000
--- a/emf/bundles/cli/pom.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- 4.0.0
-
-
- dev.jorisjonkers.deploykit.emf
- emf-parent
- 0.1.0-SNAPSHOT
- ../../pom.xml
-
-
- dev.jorisjonkers.deploykit.emf.cli
- eclipse-plugin
- deploy-kit model-driven cli
-
-
-
-
- org.pitest
- pitest-maven
-
-
-
- dev.jorisjonkers.deploykit.emf.cli.*
-
-
-
- org.eclipse.ocl.xtext.completeocl.CompleteOCLStandaloneSetup
-
-
-
- dev.jorisjonkers.deploykit.emf.cli.ComparisonTest
- dev.jorisjonkers.deploykit.emf.cli.ModelDiffTest
- dev.jorisjonkers.deploykit.emf.cli.StageTest
- dev.jorisjonkers.deploykit.emf.cli.FieldCoverageTest
- dev.jorisjonkers.deploykit.emf.cli.OracleDistanceTest
-
-
-
-
-
-
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Authored.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Authored.java
deleted file mode 100644
index 8b1a77b05..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Authored.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-import java.util.List;
-import org.eclipse.emf.ecore.EObject;
-
-/**
- * A set of documents read together as the resolution reads them, before any project is lowered: the
- * authored Projects with their env files and the Platform document, and the pinned inputs, or the
- * diagnostics that refused the set. A refused set holds no document.
- */
-public record Authored(List intent, List pinned, List diagnostics) {
-
- public Authored {
- intent = List.copyOf(intent);
- pinned = List.copyOf(pinned);
- diagnostics = List.copyOf(diagnostics);
- }
-
- public static Authored of(List intent, List pinned) {
- return new Authored(intent, pinned, List.of());
- }
-
- public static Authored refused(List diagnostics) {
- return new Authored(List.of(), List.of(), diagnostics);
- }
-
- public boolean ok() {
- return diagnostics.isEmpty();
- }
-}
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Collisions.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Collisions.java
deleted file mode 100644
index f2b7ccbc2..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Collisions.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-import dev.jorisjonkers.deploykit.emf.metamodel.resolveddeployment.Deliverable;
-import dev.jorisjonkers.deploykit.emf.metamodel.resolveddeployment.ResolvedDeployment;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.stream.Stream;
-
-/**
- * One owner per path (spec/v1/30-deliverables.md#path-allocation): two Deliverables may never claim
- * one path, no two documents one Vault name, and no two objects one ServiceAccount, since a Vault
- * role is bound to the account's name and namespace alone. Every collision is refused before
- * anything is written.
- */
-final class Collisions {
-
- static final String PATH_COLLISION = "E_PATH_COLLISION";
-
- /**
- * The estate-scoped share: each Project's resolution plans the files of it its own model names,
- * so a render of several Projects plans one such file once for each of them.
- */
- private static final String ESTATE = "estate/";
-
- /** Where a Vault document lands: a policies directory, under the name Vault holds it by. */
- private static final Pattern VAULT_DOCUMENT = Pattern.compile("/policies/([^/]+\\.(?:policy|role)\\.json)$");
-
- /** A ServiceAccount as the templates write one: its kind, then its metadata's name and namespace. */
- private static final Pattern ACCOUNT =
- Pattern.compile("(?m)^ *kind: ServiceAccount\\n( *)metadata:\\n((?:\\1 .*\\n)+)");
-
- private static final Pattern NAME = Pattern.compile("(?m)^ *name: (.+)$");
- private static final Pattern NAMESPACE = Pattern.compile("(?m)^ *namespace: (.+)$");
-
- private Collisions() {}
-
- /** Every path and every Vault name of the Deliverables {@code deployments} plan, owned twice. */
- static List ofPlan(List deployments) {
- Map> owners = new TreeMap<>();
- Map> names = new TreeMap<>();
- for (ResolvedDeployment deployment : deployments) {
- for (Deliverable deliverable : deployment.getDeliverables()) {
- String path = deliverable.getPath();
- owners.computeIfAbsent(path, key -> key.startsWith(ESTATE) ? new LinkedHashSet<>() : new ArrayList<>())
- .add(deliverable.getAdapter().getLiteral());
- Matcher vault = VAULT_DOCUMENT.matcher(path);
- if (vault.find()) {
- names.computeIfAbsent(vault.group(1), key -> new LinkedHashSet<>())
- .add(path);
- }
- }
- }
- List refused = new ArrayList<>();
- names.forEach((name, paths) -> {
- if (paths.size() > 1) {
- refused.add(refusal("the Vault name " + name + " is claimed by " + String.join(" and ", paths)));
- }
- });
- owners.forEach((path, adapters) -> {
- if (adapters.size() > 1) {
- refused.add(refusal(path + " is claimed by " + String.join(" and ", adapters)));
- }
- });
- return refused;
- }
-
- /** Every ServiceAccount the files written under {@code root} hold, held by two of them. */
- static List ofAccounts(Path root) {
- Map> accounts = new TreeMap<>();
- try (Stream files = Files.walk(root)) {
- for (Path file : files.filter(Files::isRegularFile).sorted().toList()) {
- String text = Files.readString(file, StandardCharsets.UTF_8);
- Matcher account = ACCOUNT.matcher(text);
- while (account.find()) {
- String metadata = account.group(2);
- accounts.computeIfAbsent(
- value(NAMESPACE, metadata) + "/" + value(NAME, metadata), key -> new ArrayList<>())
- .add(root.relativize(file).toString());
- }
- }
- } catch (IOException e) {
- throw new UncheckedIOException(e);
- }
- List refused = new ArrayList<>();
- accounts.forEach((account, paths) -> {
- if (paths.size() > 1) {
- refused.add(refusal("the ServiceAccount " + account + " is claimed by " + String.join(" and ", paths)));
- }
- });
- return refused;
- }
-
- private static String value(Pattern key, String metadata) {
- Matcher matcher = key.matcher(metadata);
- return matcher.find() ? matcher.group(1).strip() : "";
- }
-
- private static Diagnostic refusal(String message) {
- return new Diagnostic(PATH_COLLISION, "", "", message);
- }
-}
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Constraints.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Constraints.java
deleted file mode 100644
index cc7a80807..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Constraints.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.eclipse.emf.common.util.Diagnostic;
-import org.eclipse.emf.common.util.URI;
-import org.eclipse.emf.ecore.EObject;
-import org.eclipse.emf.ecore.EPackage;
-import org.eclipse.emf.ecore.EValidator;
-import org.eclipse.emf.ecore.resource.Resource;
-import org.eclipse.emf.ecore.resource.impl.ResourceImpl;
-import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
-import org.eclipse.emf.ecore.util.Diagnostician;
-import org.eclipse.ocl.xtext.completeocl.CompleteOCLStandaloneSetup;
-import org.eclipse.ocl.xtext.completeocl.validation.CompleteOCLEObjectValidator;
-
-/**
- * The Complete OCL constraints of a metamodel, evaluated over one parsed document. An invariant is
- * named by the diagnostic code it emits and its context is the object the diagnostic points at, so a
- * violation becomes a {@link dev.jorisjonkers.deploykit.emf.cli.Diagnostic} without a lookup table
- * (emf/docs/architecture.md#constraints). Every failed invariant is reported, never only the first.
- */
-public final class Constraints {
-
- /** The invariant name inside the message Eclipse OCL builds for a violation. */
- private static final Pattern VIOLATED = Pattern.compile("'[^']*::([A-Za-z0-9_]+)' constraint is violated");
-
- /** The constraints the metamodel carries, as the build puts them beside its classes. */
- public static URI beside(Class> metamodel, String file) {
- return URI.createURI(metamodel.getResource("/" + file).toString());
- }
-
- private Constraints() {}
-
- /** One failed invariant: its code, the object it refused, the feature it names, and what Eclipse said. */
- record Violation(String code, EObject object, String feature, String message) {}
-
- /**
- * Evaluates {@code document} against the constraints at {@code constraints}, in document order. An
- * invariant may read the other documents of the resource set the document was read into.
- */
- public static List check(EObject document, URI constraints) {
- String file = document.eResource().getURI().lastSegment();
- List refusals = new ArrayList<>();
- for (Violation violation : violations(document, constraints)) {
- // An invariant named CODE__feature refuses one feature of the object, and its path says so.
- refusals.add(new dev.jorisjonkers.deploykit.emf.cli.Diagnostic(
- violation.code(),
- file,
- Pointer.of(violation.object()) + (violation.feature().isEmpty() ? "" : "/" + violation.feature()),
- violation.message()));
- }
- return refusals;
- }
-
- /**
- * Every invariant of {@code constraints} that {@code document} and what it contains fail, in
- * document order, each with the object it refused. The document needs no resource of its own.
- */
- static List violations(EObject document, URI constraints) {
- CompleteOCLStandaloneSetup.doSetup();
- EPackage metamodel = document.eClass().getEPackage();
- EValidator previous = EValidator.Registry.INSTANCE.getEValidator(metamodel);
- EValidator.Registry.INSTANCE.put(metamodel, new CompleteOCLEObjectValidator(metamodel, constraints));
- // An invariant is evaluated in the resource set of its object, so a model a transformation
- // built is held in one of its own for as long as it is asked.
- Resource held = document.eResource() == null ? new ResourceImpl() : null;
- if (held != null) {
- new ResourceSetImpl().getResources().add(held);
- held.getContents().add(document);
- }
- try {
- // Validation walks the document's own containment, so every refusal points into its file.
- return violations(Diagnostician.INSTANCE.validate(document));
- } finally {
- EValidator.Registry.INSTANCE.put(metamodel, previous);
- if (held != null) {
- held.getContents().remove(document);
- }
- }
- }
-
- private static List violations(Diagnostic diagnostic) {
- List violations = new ArrayList<>();
- Matcher violated = VIOLATED.matcher(diagnostic.getMessage());
- if (violated.find()) {
- // A violation carries the object it refused as its first datum.
- String[] named = violated.group(1).split("__", 2);
- violations.add(new Violation(
- named[0],
- (EObject) diagnostic.getData().get(0),
- named.length == 1 ? "" : named[1],
- diagnostic.getMessage()));
- }
- for (Diagnostic child : diagnostic.getChildren()) {
- violations.addAll(violations(child));
- }
- return violations;
- }
-}
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/DataFile.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/DataFile.java
deleted file mode 100644
index 884948582..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/DataFile.java
+++ /dev/null
@@ -1,498 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.Datum;
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.DatumEntry;
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.DatumKind;
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.DatumList;
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.DatumMap;
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.DatumScalar;
-import dev.jorisjonkers.deploykit.emf.metamodel.pinnedinputs.PinnedInputsFactory;
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.util.HashSet;
-import java.util.Optional;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-/**
- * A data file an Application ships, read as YAML reads it, into the data it holds: a rule-group
- * file, which the monitoring stack's rules are written from as the file writes them
- * (spec/v1/10-project-intent.md#alert-rules). It reads the YAML such a file is written in: block
- * mappings and sequences, flow collections over any number of lines, JSON among them, plain scalars
- * over one line or several, single- and double-quoted scalars on one line, literal and folded block
- * scalars, and comments. A plain scalar is typed as YAML's core schema types it. A file that is not
- * YAML, or that uses what this reader has no meaning for (an anchor, an alias, a tag, a directive, a
- * second document, an indentation indicator, a duplicated key), holds nothing.
- */
-final class DataFile {
-
- private static final PinnedInputsFactory MODEL = PinnedInputsFactory.eINSTANCE;
- private static final String KEY = "groups";
-
- /** A line that opens or ends a document: after the first token, a second document. */
- private static final Pattern MARKER = Pattern.compile("(?m)^(---|\\.\\.\\.)([ \\t].*)?$");
-
- private final String text;
- private int at;
-
- private DataFile(String text) {
- this.text = text.replace("\r\n", "\n");
- }
-
- /** What {@code text} holds, or nothing where it is not YAML this reader reads. */
- static Optional read(String text) {
- DataFile file = new DataFile(text);
- try {
- file.blank();
- if (file.text.startsWith("---", file.at) && file.ends(file.at + 3)) {
- file.at += 3;
- file.blank();
- }
- if (MARKER.matcher(file.text).region(file.at, file.text.length()).find()) {
- throw new Malformed();
- }
- Datum datum = file.more() ? file.node(-1) : scalar("null", DatumKind.EMPTY);
- file.blank();
- if (file.more()) {
- throw new Malformed();
- }
- return Optional.of(datum);
- } catch (Malformed e) {
- return Optional.empty();
- }
- }
-
- /** The rule groups {@code text} holds: the list a top-level mapping holds under `groups`, where it holds one. */
- static Optional groups(String text) {
- return read(text)
- .filter(DatumMap.class::isInstance)
- .flatMap(root -> ((DatumMap) root)
- .getEntries().stream()
- .filter(entry -> entry.getKey().equals(KEY))
- .map(DatumEntry::getValue)
- .findFirst())
- .filter(DatumList.class::isInstance)
- .map(DatumList.class::cast)
- .filter(list -> !list.getItems().isEmpty());
- }
-
- /** What YAML has no meaning for here, or no meaning for at all. */
- private static final class Malformed extends RuntimeException {
- private static final long serialVersionUID = 1L;
- }
-
- // ------------------------------------------------------------------ positions
-
- /**
- * The character at {@code index}, where the end of the file reads as a line break. No index before
- * the file is asked for: a comment's {@code #} is only looked behind past a token's first character.
- */
- private char peek(int index) {
- return index < text.length() ? text.charAt(index) : '\n';
- }
-
- /** The column {@code index} sits at on its line. */
- private int column(int index) {
- return index - (text.lastIndexOf('\n', index - 1) + 1);
- }
-
- /** Whether {@code index} ends a token: the end of the file, a space or a line break. */
- private boolean ends(int index) {
- char c = peek(index);
- return c == ' ' || c == '\t' || c == '\n';
- }
-
- /** Whether a block sequence's dash opens at {@code at}. */
- private boolean dash() {
- return peek(at) == '-' && ends(at + 1);
- }
-
- private boolean more() {
- return at < text.length();
- }
-
- /** Spaces and tabs on the line. */
- private void spaces() {
- while (peek(at) == ' ' || peek(at) == '\t') {
- at++;
- }
- }
-
- /** Whether nothing but a comment is left on the line; the rest of the line is skipped if so. */
- private boolean lineEnds() {
- spaces();
- if (peek(at) == '#') {
- while (peek(at) != '\n') {
- at++;
- }
- }
- return peek(at) == '\n';
- }
-
- /** Every blank line and comment, up to the next token of the file. */
- private void blank() {
- while (more() && lineEnds()) {
- at++;
- }
- }
-
- /** The token on the line at {@code at} ends its value: nothing but a comment follows it. */
- private void endOfValue() {
- if (!lineEnds()) {
- throw new Malformed();
- }
- }
-
- // ------------------------------------------------------------------ block structure
-
- /** The node at {@code at}, inside a parent at column {@code parent}. */
- private Datum node(int parent) {
- int column = column(at);
- if (dash()) {
- return sequence(column);
- }
- if (keyAhead()) {
- return mapping(column);
- }
- return value(parent);
- }
-
- private DatumList sequence(int column) {
- DatumList list = MODEL.createDatumList();
- while (more() && column(at) == column && dash()) {
- at++;
- list.getItems().add(lineEnds() ? below(column) : node(column));
- blank();
- }
- if (more() && column(at) > column) {
- throw new Malformed();
- }
- return list;
- }
-
- private DatumMap mapping(int column) {
- DatumMap map = MODEL.createDatumMap();
- Set keys = new HashSet<>();
- do {
- DatumEntry entry = MODEL.createDatumEntry();
- entry.setKey(key());
- if (!keys.add(entry.getKey())) {
- throw new Malformed();
- }
- if (lineEnds()) {
- blank();
- entry.setValue(more() && column(at) == column && dash() ? sequence(column) : below(column));
- } else {
- entry.setValue(value(column));
- }
- map.getEntries().add(entry);
- blank();
- } while (more() && column(at) == column && !dash() && keyAhead());
- if (more() && column(at) > column) {
- throw new Malformed();
- }
- return map;
- }
-
- /** The node on the lines below a key or a dash at {@code column}, or an empty scalar where there is none. */
- private Datum below(int column) {
- blank();
- return more() && column(at) > column ? node(column) : scalar("null", DatumKind.EMPTY);
- }
-
- /** Whether a mapping key opens at {@code at}: a key, then a colon a space or the line's end follows. */
- private boolean keyAhead() {
- int start = at;
- try {
- key();
- return true;
- } catch (Malformed e) {
- return false;
- } finally {
- at = start;
- }
- }
-
- /** The key at {@code at}, with the colon after it. */
- private String key() {
- String key;
- char c = peek(at);
- if (c == '"' || c == '\'') {
- key = quoted();
- spaces();
- } else {
- int start = at;
- while (!(peek(at) == ':' && ends(at + 1)) && peek(at) != '\n' && !(peek(at) == '#' && ends(at - 1))) {
- at++;
- }
- key = text.substring(start, at).strip();
- // A key opens on no indicator: an anchor or a tag is outside the subset.
- if (!key.isEmpty() && "[]{},#&*!|>'\"%@`".indexOf(key.charAt(0)) >= 0) {
- throw new Malformed();
- }
- }
- if (peek(at) != ':' || !ends(at + 1)) {
- throw new Malformed();
- }
- at++;
- return key;
- }
-
- /** A value that is no collection of the block: a flow collection, a block scalar, or a scalar. */
- private Datum value(int parent) {
- char c = peek(at);
- if (c == '[' || c == '{') {
- Datum flow = flow();
- endOfValue();
- return flow;
- }
- if (c == '|' || c == '>') {
- return block(parent);
- }
- if (c == '"' || c == '\'') {
- String quoted = quoted();
- endOfValue();
- return scalar(quoted, DatumKind.STRING);
- }
- if ("&*!%@`".indexOf(c) >= 0) {
- throw new Malformed();
- }
- return plain(parent);
- }
-
- /**
- * A plain scalar, to the end of its line or a comment, and every line after it indented further than
- * its parent, folded: a line break is a space, and each blank line between two is one line break.
- */
- private Datum plain(int parent) {
- StringBuilder value = new StringBuilder(rest());
- int breaks = 0;
- int resume = at;
- while (more()) {
- at++;
- spaces();
- // The end of the file reads as a line break, so it is counted with the blank lines.
- if (peek(at) == '\n') {
- breaks++;
- } else if (column(at) > parent && peek(at) != '#') {
- // A key there would open a mapping inside a scalar, which YAML refuses.
- if (keyAhead()) {
- throw new Malformed();
- }
- value.append(breaks == 0 ? " " : "\n".repeat(breaks)).append(rest());
- breaks = 0;
- resume = at;
- } else {
- break;
- }
- }
- at = resume;
- return typed(value.toString());
- }
-
- /** The rest of the line from {@code at}, short of a comment, trimmed; {@code at} is left at the line's end. */
- private String rest() {
- int start = at;
- while (peek(at) != '\n' && !(peek(at) == '#' && ends(at - 1))) {
- at++;
- }
- String line = text.substring(start, at).strip();
- lineEnds();
- return line;
- }
-
- /**
- * A literal or folded block scalar: every line under the indicator indented at least as far as its
- * first, with that indentation removed, joined by a line break under {@code |} and folded under
- * {@code >}, and its trailing line breaks clipped to one, stripped under {@code -} or kept under
- * {@code +}.
- */
- private Datum block(int parent) {
- boolean literal = peek(at) == '|';
- at++;
- char chomp = peek(at) == '-' || peek(at) == '+' ? text.charAt(at++) : ' ';
- endOfValue();
- StringBuilder value = new StringBuilder();
- int indent = -1;
- int breaks = 0;
- boolean previousMore = false;
- while (at + 1 < text.length()) {
- int start = at + 1;
- int end = text.indexOf('\n', start);
- String line = text.substring(start, end < 0 ? text.length() : end);
- int width = line.length() - line.stripLeading().length();
- if (line.isBlank()) {
- breaks++;
- } else if (width > parent && (indent < 0 || width >= indent)) {
- indent = indent < 0 ? width : indent;
- String content = line.substring(indent).stripTrailing();
- boolean more = content.startsWith(" ") || content.startsWith("\t");
- // A line break is kept under `|`, around a line indented further and before the first
- // line; elsewhere under `>` it folds to a space, or into the blank lines after it.
- boolean kept = literal || more || previousMore || value.length() == 0;
- value.append(value.length() == 0 ? "" : kept || breaks > 0 ? "\n" : " ")
- .append("\n".repeat(kept ? breaks : Math.max(breaks - 1, 0)))
- .append(content);
- previousMore = more;
- breaks = 0;
- } else {
- break;
- }
- at = end < 0 ? text.length() : end;
- }
- String content = value.toString();
- String trailing = content.isEmpty() ? "" : "\n";
- return scalar(
- content + (chomp == '-' ? "" : chomp == '+' ? trailing + "\n".repeat(breaks) : trailing),
- DatumKind.STRING);
- }
-
- // ------------------------------------------------------------------ flow structure
-
- /** Spaces, line breaks and comments inside a flow collection. */
- private void flowBlank() {
- while (more() && lineEnds()) {
- at++;
- }
- spaces();
- }
-
- private Datum flow() {
- boolean map = text.charAt(at++) == '{';
- char close = map ? '}' : ']';
- DatumMap entries = MODEL.createDatumMap();
- DatumList items = MODEL.createDatumList();
- Set keys = new HashSet<>();
- flowBlank();
- while (peek(at) != close) {
- if (map) {
- DatumEntry entry = MODEL.createDatumEntry();
- entry.setKey(peek(at) == '"' || peek(at) == '\'' ? quoted() : flowPlain(true));
- if (!keys.add(entry.getKey())) {
- throw new Malformed();
- }
- flowBlank();
- if (peek(at) != ':') {
- throw new Malformed();
- }
- at++;
- flowBlank();
- entry.setValue(peek(at) == ',' || peek(at) == close ? scalar("null", DatumKind.EMPTY) : flowNode());
- entries.getEntries().add(entry);
- } else {
- items.getItems().add(flowNode());
- }
- flowBlank();
- if (peek(at) == ',') {
- at++;
- flowBlank();
- } else if (peek(at) != close) {
- throw new Malformed();
- }
- }
- at++;
- return map ? entries : items;
- }
-
- private Datum flowNode() {
- char c = peek(at);
- if (c == '[' || c == '{') {
- return flow();
- }
- if (c == '"' || c == '\'') {
- return scalar(quoted(), DatumKind.STRING);
- }
- return typed(flowPlain(false));
- }
-
- /** A plain scalar in a flow collection, to the next indicator; a key's also stops at its colon. */
- private String flowPlain(boolean key) {
- int start = at;
- while (more()
- && ",[]{}".indexOf(peek(at)) < 0
- && !(peek(at) == ':' && (key || ends(at + 1)))
- && !(peek(at) == '#' && ends(at - 1))) {
- at++;
- }
- String plain = text.substring(start, at).strip().replaceAll("\\s*\\n\\s*", " ");
- if (plain.isEmpty() || "&*!%@`|>".indexOf(plain.charAt(0)) >= 0) {
- throw new Malformed();
- }
- return plain;
- }
-
- // ------------------------------------------------------------------ scalars
-
- /** A single- or double-quoted scalar on one line, its escapes read. */
- private String quoted() {
- char quote = text.charAt(at++);
- StringBuilder value = new StringBuilder();
- while (true) {
- char c = peek(at++);
- if (c == '\n') {
- throw new Malformed();
- } else if (c == quote && quote == '\'' && peek(at) == '\'') {
- value.append('\'');
- at++;
- } else if (c == quote) {
- return value.toString();
- } else if (c == '\\' && quote == '"') {
- value.append(escaped());
- } else {
- value.append(c);
- }
- }
- }
-
- /** The character a double-quoted scalar's escape after the backslash stands for. */
- private String escaped() {
- char c = peek(at++);
- return switch (c) {
- case 'n' -> "\n";
- case 't' -> "\t";
- case 'r' -> "\r";
- case 'b' -> "\b";
- case 'f' -> "\f";
- case '0' -> "\0";
- case '"', '\\', '/', ' ' -> String.valueOf(c);
- case 'u' -> {
- try {
- String hex = text.substring(at, at + 4);
- at += 4;
- yield String.valueOf((char) Integer.parseInt(hex, 16));
- } catch (NumberFormatException | StringIndexOutOfBoundsException e) {
- throw new Malformed();
- }
- }
- default -> throw new Malformed();
- };
- }
-
- /** A plain scalar as YAML's core schema types it: null, a boolean, an integer, a float, or a string. */
- private static DatumScalar typed(String plain) {
- if (plain.matches("~|null|Null|NULL")) {
- return scalar("null", DatumKind.EMPTY);
- }
- if (plain.matches("true|True|TRUE|false|False|FALSE")) {
- return scalar(plain.toLowerCase(java.util.Locale.ROOT), DatumKind.BOOLEAN);
- }
- if (plain.matches("[-+]?[0-9]+")) {
- return scalar(new BigInteger(plain).toString(), DatumKind.NUMBER);
- }
- if (plain.matches("0o[0-7]+|0x[0-9a-fA-F]+")) {
- return scalar(
- new BigInteger(plain.substring(2), plain.charAt(1) == 'o' ? 8 : 16).toString(), DatumKind.NUMBER);
- }
- if (plain.matches("[-+]?(\\.[0-9]+|[0-9]+(\\.[0-9]*)?)([eE][-+]?[0-9]+)?")) {
- return scalar(new BigDecimal(plain).stripTrailingZeros().toPlainString(), DatumKind.NUMBER);
- }
- return scalar(plain, DatumKind.STRING);
- }
-
- private static DatumScalar scalar(String text, DatumKind kind) {
- DatumScalar scalar = MODEL.createDatumScalar();
- scalar.setText(text);
- scalar.setKind(kind);
- return scalar;
- }
-}
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java
deleted file mode 100644
index 172aaffab..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/Diagnostic.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-/**
- * A refusal: the code the specification gives it, the file name of the authored document it points
- * into, the JSON Pointer of the authored value it concerns, and a message for a human. The code, the
- * document and the path are the parity contract's; the message is this implementation's own.
- */
-public record Diagnostic(String code, String document, String path, String message) {
-
- /** The code every refusal of a document's shape carries, until a rule gives it its own. */
- public static final String SCHEMA = "schema";
-}
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/EnvFiles.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/EnvFiles.java
deleted file mode 100644
index fae0858bf..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/EnvFiles.java
+++ /dev/null
@@ -1,187 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.EnvFile;
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.EnvLiteral;
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.EnvValue;
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.EnvVariable;
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.Placeholder;
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.PlaceholderKind;
-import dev.jorisjonkers.deploykit.emf.metamodel.projectintent.ProjectIntentFactory;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Optional;
-import java.util.Set;
-import java.util.regex.Pattern;
-
-/**
- * The dotenv subset chapter 10 fixes, read into the model
- * (spec/v1/10-project-intent.md#the-dotenv-subset-that-is-read). A reader rather than a grammar, for
- * the reason emf/docs/adr/emf/0082-the-env-files-are-read-not-parsed-by-xtext.md records.
- */
-public final class EnvFiles {
-
- /** Which level a scope directory names, and the name it names it by. */
- public record Scope(Level level, String name) {
- public enum Level {
- PROJECT,
- APPLICATION,
- PROCESS
- }
- }
-
- /** An authored env file, by the path that says which level it reaches. */
- public record Source(String path, String text) {}
-
- /** One file read, with the scope its directory put it in. */
- public record Scoped(String path, Scope scope, EnvFile file) {}
-
- /** What reading gave: the scoped files, or the diagnostics that refused them. */
- public record Read(List files, List diagnostics) {
-
- public Read {
- files = List.copyOf(files);
- diagnostics = List.copyOf(diagnostics);
- }
-
- public boolean ok() {
- return diagnostics.isEmpty();
- }
- }
-
- private static final ProjectIntentFactory MODEL = ProjectIntentFactory.eINSTANCE;
- private static final Pattern NAME = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
- private static final String OPENS = "${";
- private static final String BASE = "base.env";
- private static final String PROJECT_SCOPE = "_project";
- private static final String APPLICATION_SCOPE = "_applications";
-
- private EnvFiles() {}
-
- /** Every source read and scoped, or every line they break the subset on. */
- public static Read read(List sources) {
- List files = new ArrayList<>();
- List refusals = new ArrayList<>();
- for (Source source : sources) {
- Optional scope = scopeOf(source.path());
- if (scope.isEmpty()) {
- refusals.add(schema(source.path(), source.path(), "this path names no env scope directory"));
- continue;
- }
- // A scope holds one file, and no file varies by cluster.
- if (!source.path().endsWith("/" + BASE)) {
- refusals.add(schema(source.path(), source.path(), "a scope's one env file is " + BASE));
- continue;
- }
- List broke = new ArrayList<>();
- EnvFile file = readOne(source, broke);
- if (broke.isEmpty()) {
- files.add(new Scoped(source.path(), scope.get(), file));
- } else {
- refusals.addAll(broke);
- }
- }
- return new Read(files, refusals);
- }
-
- /** The level {@code path}'s scope directory names, or nothing where it names none. */
- public static Optional scopeOf(String path) {
- String[] segments = path.split("/");
- int env = List.of(segments).lastIndexOf("env");
- if (env < 0 || !path.endsWith(".env")) {
- return Optional.empty();
- }
- // A scope is a directory, so the file is one segment deeper than it, and one deeper
- // again where the Application scope names which Application.
- // The `.env` file is itself a segment after `env`, so `rest` is never empty here.
- String[] rest = Arrays.copyOfRange(segments, env + 1, segments.length);
- if (APPLICATION_SCOPE.equals(rest[0])) {
- return rest.length == 3 ? Optional.of(new Scope(Scope.Level.APPLICATION, rest[1])) : Optional.empty();
- }
- if (rest.length != 2) {
- return Optional.empty();
- }
- return Optional.of(
- PROJECT_SCOPE.equals(rest[0])
- ? new Scope(Scope.Level.PROJECT, rest[0])
- : new Scope(Scope.Level.PROCESS, rest[0]));
- }
-
- private static EnvFile readOne(Source source, List refusals) {
- EnvFile file = MODEL.createEnvFile();
- Set seen = new LinkedHashSet<>();
- String[] lines = source.text().split("\n", -1);
- for (int i = 0; i < lines.length; i++) {
- String content = lines[i].trim();
- if (content.isEmpty() || content.startsWith("#")) {
- continue;
- }
- String at = source.path() + ":" + (i + 1);
- int equals = content.indexOf('=');
- String name = equals < 1 ? "" : content.substring(0, equals);
- if (equals < 1 || !NAME.matcher(name).matches()) {
- refusals.add(schema(source.path(), at, "a line is not a NAME=value assignment"));
- continue;
- }
- Optional value = value(content.substring(equals + 1));
- if (value.isEmpty()) {
- refusals.add(schema(source.path(), at, "the value of " + name + " is outside the subset"));
- continue;
- }
- if (!seen.add(name)) {
- refusals.add(new Diagnostic(
- "E_SHARED_DECLARATION_DUPLICATED", source.path(), at, name + " is set twice in one file"));
- }
- EnvVariable variable = MODEL.createEnvVariable();
- variable.setName(name);
- variable.setValue(value.get());
- file.getEntries().add(variable);
- }
- return file;
- }
-
- /**
- * A literal, or one placeholder with literal text after it, and never a secret with text after
- * it: a secret becomes one key of a Secret, and half a key is nothing.
- */
- private static Optional value(String raw) {
- if (raw.startsWith(OPENS)) {
- int close = raw.indexOf('}');
- if (close == -1) {
- return Optional.empty();
- }
- // Split once: a source may hold colons, and the kind may hold none.
- String[] parts = raw.substring(OPENS.length(), close).split(":", 2);
- PlaceholderKind kind = parts.length == 2 ? PlaceholderKind.get(parts[0]) : null;
- String source = parts.length == 2 ? parts[1] : "";
- String suffix = raw.substring(close + 1);
- boolean whole = kind != PlaceholderKind.SECRET || suffix.isEmpty();
- if (kind == null || source.isEmpty() || !literal(suffix) || !whole) {
- return Optional.empty();
- }
- Placeholder placeholder = MODEL.createPlaceholder();
- placeholder.setKind(kind);
- placeholder.setSource(source);
- if (!suffix.isEmpty()) {
- placeholder.setSuffix(suffix);
- }
- return Optional.of(placeholder);
- }
- if (raw.isEmpty() || !literal(raw)) {
- return Optional.empty();
- }
- EnvLiteral literal = MODEL.createEnvLiteral();
- literal.setText(raw);
- return Optional.of(literal);
- }
-
- /** Literal text: no {@code #}, which is a comment wherever it appears, and no placeholder. */
- private static boolean literal(String text) {
- return !text.contains("#") && !text.contains(OPENS);
- }
-
- private static Diagnostic schema(String document, String at, String message) {
- return new Diagnostic(Diagnostic.SCHEMA, document, at, message);
- }
-}
diff --git a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java b/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java
deleted file mode 100644
index 4e081024e..000000000
--- a/emf/bundles/cli/src/main/java/dev/jorisjonkers/deploykit/emf/cli/IntentJson.java
+++ /dev/null
@@ -1,134 +0,0 @@
-package dev.jorisjonkers.deploykit.emf.cli;
-
-import dev.jorisjonkers.deploykit.emf.metamodel.descriptor.Descriptor;
-import dev.jorisjonkers.deploykit.emf.syntax.values.ProjectIntentValueConverters;
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import org.eclipse.emf.common.util.Enumerator;
-import org.eclipse.emf.ecore.EAnnotation;
-import org.eclipse.emf.ecore.EObject;
-import org.eclipse.emf.ecore.EReference;
-import org.eclipse.emf.ecore.EStructuralFeature;
-import org.eclipse.emf.ecore.util.EcoreUtil;
-import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
-
-/**
- * Reads a parsed model as the JSON value the parity contract compares: every feature named as the
- * authored key it holds, a map entry as an object, an enumeration as its literal, and an optional
- * feature absent when the document left it out. A class the metamodel annotates as a scalar is
- * written as that scalar, which is how {@code probes: none} stays the word it was authored as, and
- * one it annotates with a value is written as that feature's value, which is how a plain key stays
- * its name. The
- * metamodel is walked reflectively, so a feature added to the {@code .ecore} reaches the intent
- * without a line here.
- */
-public final class IntentJson {
-
- private IntentJson() {}
-
- /** The annotation a class carries when the language writes it as one word rather than a block. */
- private static final String JSON = "https://jorisjonkers.dev/deploy-kit/json";
-
- /** The JSON value of {@code root}, as a map of authored key to value. */
- public static Map of(EObject root) {
- Map json = new LinkedHashMap<>();
- for (EStructuralFeature feature : root.eClass().getEAllStructuralFeatures()) {
- if (isSet(root, feature)) {
- json.put(Descriptor.keyOf(feature), value(root, feature));
- }
- }
- return json;
- }
-
- /** Whether the document carries {@code feature}: a required feature always, an optional one when set. */
- private static boolean isSet(EObject owner, EStructuralFeature feature) {
- return feature.isRequired() || owner.eIsSet(feature);
- }
-
- /**
- * The name a reference links by: an Application's ID, a Process's name, or a port's key. A Process
- * and a port are named only within what holds them, so neither is an ID of the document.
- */
- private static String nameOf(EObject target) {
- String id = EcoreUtil.getID(target);
- return id != null
- ? id
- : String.valueOf(target.eGet(
- target.eClass().getEStructuralFeature(target instanceof Map.Entry, ?> ? "key" : "name")));
- }
-
- private static Object value(EObject owner, EStructuralFeature feature) {
- Object value = owner.eGet(feature);
- if (feature instanceof EReference reference && !reference.isContainment()) {
- // A reference is written as the name that linked it: the identifier of what it points at, or,
- // where it points into a document that was not read with this one, the name as written.
- if (reference.isMany()) {
- // A list of names, each the one its own position wrote.
- List