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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions emf/docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,5 @@ taking a number, check both registers.
| [0113](emf/0113-acceleo-4-renders-the-deliverable-set.md) | Acceleo 4 renders the Deliverable Set, byte for byte against the committed tree | open |
| [0114](emf/0114-model-behaviours-have-a-java-witness.md) | Every model behaviour in the behaviour ledger has a Java witness, listed inside `emf/` | open |
| [0115](emf/0115-the-emf-gates-are-estate-shaped.md) | The model-driven build carries the gates the estate's JVM repositories enforce, plus Java-shaped equivalents, and measures its thresholds | open |
| [0121](emf/0121-parity-crosses-the-cli-file-interface.md) | The parity suite reaches the pipeline through its file interface, and holds no EMF type | open |
| [0122](emf/0122-bundles-and-tests-are-separate-tiers.md) | The tree splits into an Eclipse bundle tier and a test tier, and only the bundle tier is Java | open |
112 changes: 112 additions & 0 deletions emf/docs/adr/emf/0121-parity-crosses-the-cli-file-interface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
tier: decision
status: proposed
claim: open
owner: joris
date: 2026-09-16
normative: docs/architecture.md#parity
rests-on: ["0106"]
---

# The parity suite reaches the pipeline through its file interface, and holds no EMF type

## Rests on
Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the
claim is that everything the parity contract compares is already a file, so a
suite that calls no EMF type can still prove every case under
`spec/v1/examples/`. False if: a parity case exists that cannot be decided from
the pipeline's own output files and its exit code, and can only be decided by
reading an in-memory model. Settled by: the parity module building as a plain
jar with no dependency on the target platform, green on every case the three
course tasks land.

## Why
Today `parity/` imports `Pipeline`, `Parsed`, `Diagnostic` from the `cli`
module and `Descriptor`, `ProjectIntentPackage` from `metamodel`. Those are EMF
types, resolved from p2, so the test module has to be an Eclipse bundle to see
them. It is one: `parity/pom.xml` declares `eclipse-plugin` packaging, while
[the architecture](../../architecture.md#toolchain) says modules that need no p2
bundle, like `parity/`, stay plain jars. The drift is not a typo. One suite
reaching past the pipeline's own interface pulled the whole Eclipse toolchain
into the module whose job is to be independent of it.

The pipeline's real interface is narrower than its Java types: input files and
arguments in; an exit code, diagnostics and a rendered tree out. That is the
interface [0105](../../../../docs/adr/architecture/0105-two-implementations-meet-at-committed-oracles.md)
already compares the two implementations across, because an oracle file is a
file and nothing else. Two things are compared today through Java instead: the
metamodel descriptor and the parsed intent. Both are values the pipeline can
write, so both can cross the same interface as everything else.

Moving the suite onto that interface pays three times. The test module loses
every p2 dependency and becomes a plain jar. A parity failure then names an
output a person can open, not a Java field a person has to reconstruct. And the
suite exercises what a run of the pipeline leaves behind, which is what a grader
sees from `mvn verify` and what an examiner sees from a launch configuration, so
a case that passes in CI is a case either of them can reproduce.

Losing every p2 dependency is also what lets the module be written in a language
the target platform does not carry, which is
[0122](0122-bundles-and-tests-are-separate-tiers.md)'s subject. This decision
stands on its own (the drift it closes is live today), but that is the order the
two are taken in.

The module keeps its `src/main`: `CanonicalJson` and `Ledgers` are helpers the
suites call, not tests, and neither touches EMF. Only the test sources change.

It costs the ability to assert on anything the pipeline does not write. That is
the point: a value worth asserting on is a value worth emitting, and one that
is not emitted is not evidence of parity.

## Alternatives
| option | cost if taken | why rejected |
|---|---|---|
| Leave `parity/` an Eclipse bundle and fix the architecture's sentence instead | Nothing to build; the drift closes by rewriting the doc | Keeps the toolchain inside the module that exists to be free of it, and keeps the widest interface in the tree as the test surface |
| Keep the Java calls, but add a second, file-level suite beside them | Both surfaces proven | Two suites decide the same cases, and the narrow one is never trusted while the wide one exists |
| Make the pipeline a separate process the suite runs | The strictest seam, exit code included | Costs a packaged, runnable artifact and process plumbing for no case the in-process file interface cannot decide; revisit if one appears |
| Export only the descriptor, keep `Parsed` in Java | One less thing to serialise | The parsed intent is the Task 1 evidence, so it is exactly the thing that must be readable as a file |

## Reversibility
Undo cost today: restoring the Java calls and the `eclipse-plugin` packaging,
half a day, and the serialisers stay useful either way. Becomes irreversible
once: never; deleted with `emf/`.

## Consequences
- The pipeline writes the parsed intent and the diagnostics as canonical JSON,
and the metamodel writes its descriptor as a build artifact. Paid by the
pull request that moves the suite.
- `parity/pom.xml` becomes `jar` packaging and loses its `META-INF/MANIFEST.MF`
and its `build.properties`, the two PDE artifacts, so it builds without the
target platform. Paid in the same pull request.
- **The module rules stop enforcing unless they are given their classes and a
non-empty guard.** `ArchitectureTest` is `@AnalyzeClasses(packages =
"dev.jorisjonkers.deploykit.emf")`, so it sees the other modules only because
`parity/META-INF/MANIFEST.MF` requires them. Dropping that dependency, which
this decision mandates, leaves the importer with parity's own classes; every
other layer is then empty, `withOptionalLayers(true)` excuses it, and
`EMF-010` and `EMF-011` pass while proving nothing. The suite reads the
class directories the build wrote, by path, **and** asserts the imported set
is non-empty, so an empty import fails loudly instead of passing. Paid in the
same pull request, and shown by a fixture that empties the import.
- A module that declares no dependency is ordered by its position in the
reactor alone, so `-T`, `-pl` or an IDE can run the suite before the bundles
wrote their classes and output files. Each case asserts its expected output
file exists before comparing it, and a missing one fails as a missing file
rather than as a skipped case. That is a different absence from a case with no
committed oracle, which is still listed as not yet a parity case: one is the
pipeline failing to write what it owes, the other is a case nobody has pinned
yet. Paid in the same pull request.
- `EMF-010` and `EMF-011` keep their rows but gain the fixture
[0104](../../../../docs/adr/architecture/0104-every-enforced-rule-has-an-id-a-row-and-a-fixture.md)
requires, since a rule that has only ever run clean is untested and these two
would be the first to pass vacuously. Paid in the same pull request.
- The module table's dependency rule changes shape: the parity module depends on
no module, only on the files the build writes. That is a row in
[the architecture](../../architecture.md#modules), a row in
[the rule ledger](../../rules.md) for `EMF-010`, **and** the rule's own
enforcement: `MODULES_DEPEND_ONLY_ON_MODULES_ABOVE_THEM` in
`ArchitectureTest` currently names `parity` in five `mayOnlyBeAccessedByLayers`
chains, so it would keep permitting exactly what this decision forbids. The
layer and those five entries go together with the imports.
- A stage that wants a new parity assertion must first make the pipeline emit
the value. Paid by Task 2 and Task 3.
121 changes: 121 additions & 0 deletions emf/docs/adr/emf/0122-bundles-and-tests-are-separate-tiers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
tier: decision
status: proposed
claim: open
owner: joris
date: 2026-09-16
normative: docs/architecture.md#modules
rests-on: ["0106"]
---

# The tree splits into an Eclipse bundle tier and a test tier, and only the bundle tier is Java

## Rests on
Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the
claim is that the modules an examiner opens in Eclipse and the modules that
only ever run from Maven are two disjoint sets, and that the second set needs
nothing from the Eclipse toolchain. False if: a module has to be both, or a
test tier written outside Java cannot hold a gate this build already enforces.
Settled by: `./mvnw -B -ntp verify` green with `bundles/` built by Tycho, the
test tier built as a plain Kotlin jar, and every rule in
[the rule ledger](../../rules.md) still enforced.

## Why
Every module under `emf/` is an Eclipse bundle today, and the directory listing
says nothing about which of them a course examiner ever opens. Five of them are
the graded artefacts: the metamodels, the grammar, the transformation, the
templates and the pipeline that runs them. One, `parity/`, is this
repository's own evidence, built and run only by Maven, and by
[0121](0121-parity-crosses-the-cli-file-interface.md) it holds no EMF type at
all. Two audiences, two toolchains, one flat list.

The split names them: `bundles/` for what Tycho builds against `emf.target`
and Eclipse imports, `tests/` for what Maven builds as a plain jar. The boundary
answers a question every reader currently has to work out from a `pom.xml`: is
this file graded, and will a teacher open it. It says nothing about the
documents: `emf/docs/` keeps its architecture, its rule ledger, its witness list
and its register where they are, and a reader who finds them spread out is
finding a different problem than this one.

The boundary is also a language boundary. Ecore generates Java, Xtext's runtime
hooks are Java, and a bundle compiles through Tycho's JDT compiler against a
target platform that carries no Kotlin: putting Kotlin in a bundle means a
`kotlin-stdlib` unit in `emf.target`, a second compiler ahead of JDT, and an
examiner who needs the Kotlin Eclipse plugin to read the source. So `bundles/`
is Java, without exception.

The `cli` module is the one worth naming, because it looks like the exception.
It is not run from Eclipse the way the metamodels are read there, so it reads
like a candidate for Kotlin. It is not one: it calls the EMF, OCL, QVTo and
Acceleo standalone APIs, every one of them resolved from p2, so it is a bundle
by its dependencies whatever language it is written in, and it carries the
launch configurations an examiner runs. Kotlin reaches it only as a launcher
wrapping a Java pipeline, which buys argument parsing and pays a module for it.
The part with no teacher, no Eclipse and no p2 dependency is the test tier, and
that is the part this decision moves. `tests/` has none of those constraints and every
other JVM repository in the estate is Kotlin with a mature suite, so it is
Kotlin, and the estate's test idiom applies here rather than being reinvented
in Java.

The graded modules keep their short directory names. Tycho's convention is a
directory named for the bundle's symbolic name, but Eclipse names an imported
Maven project from its `artifactId` regardless, so the long names buy nothing
an import already gives and cost every path in the tree.

## Alternatives
| option | cost if taken | why rejected |
|---|---|---|
| Keep the flat module list, document the split in prose | No moves, no path updates | The listing is what a reader sees first; a convention only a document knows is a convention that gets broken |
| Split by course task (`task1/`, `task2/`, `task3/`) instead | Matches the grading schedule exactly | The `cli` and the metamodels span every task, so the split would have to cut them; the deadline is a property of a row in the module table, not of a directory |
| Directories named for bundle symbolic names | Tycho and PDE convention, directory equals Eclipse project | Eclipse takes the project name from the `artifactId` anyway, so it buys a naming match nobody reads and costs 40-character paths |
| Kotlin in the bundles too, one language for the tree | One toolchain to learn | A p2 target platform with no Kotlin, a second compiler before JDT, and generated EMF Java that stays Java regardless |
| Kotlin for `cli` alone, the module no teacher runs as a project | The pipeline entry in the estate's language | `cli` calls four standalone APIs resolved from p2, so it is a bundle by its dependencies; Kotlin would reach only a launcher around the Java pipeline |
| Keep the test tier in Java | No second language in the build | Every other JVM repository in the estate is Kotlin, so a Java suite here is the one suite nobody has a pattern for |

## Reversibility
Undo cost today: moving six directories back and updating the paths that name
them. `grep -rIl -E 'emf/(metamodel|syntax|cli|resolve|render|parity)'` over the
tracked tree finds them; an afternoon. Becomes
irreversible once: never; deleted with `emf/`.

## Consequences
- The reactor's `<module>` entries become `bundles/…` and `tests/…`, and the
root's `test/emf-wiring.test.ts` keeps holding them, since it reads the
entries rather than assuming a depth. Paid by the moving pull request.
- The `bundle` profile in `emf/pom.xml`, which activates on a module's
`META-INF/MANIFEST.MF`, now agrees with the directory it sits in. The file
test stays: the directory is documentation, the manifest is the fact.
- Spotless with palantir-java-format covers `bundles/`; the test tier needs
the formatter the estate's Kotlin repositories run. Paid by the pull request
that ports the suite.
- PIT reports many equivalent mutants on Kotlin, so the mutation threshold of
[0115](0115-the-emf-gates-are-estate-shaped.md) may not hold over the test
tier at the number it holds over Java. Lowering it is a decision of its own,
with its own record, and until one exists the threshold stands.
- `emf/README.md` gains the one table that says which artefact lives where and
which task grades it, so the layout answers the navigation question without
a search. Paid by the moving pull request.
- [0114](0114-model-behaviours-have-a-java-witness.md) says every model
behaviour has a **Java** witness, and three of its six witnesses are
`ParityTest` methods in the module this decision ports. The other three sit in
bundles and stay Java. The wording that has to change is "Java", not the rule:
a witness is a JUnit test, in whichever language its module is written. The
pull request that ports the suite amends that decision to say so and carries
the same edit into [the witness list](../../witnesses.md), `EMF-013`'s row in
[the rule ledger](../../rules.md) and the sentence under
[the gates](../../architecture.md#gates) that calls a witness a Java proof.
`emf/docs/architecture.md`'s own
[Witnesses](../../architecture.md#witnesses) section already says "the JUnit
test", so it needs no edit. Nothing here reverses
[0114](0114-model-behaviours-have-a-java-witness.md); if review reads the
change as a reversal rather than an amendment, it supersedes it instead, with
every citation naming the successor.
- Every document that names a module by its current path moves with it, in the
same commit, `emf/docs/architecture.md` and `emf/README.md` included: [the rule ledger](../../rules.md), whose `EMF-010`, `EMF-011`,
`EMF-013`, `EMF-014` and `EMF-016` rows each name a file under `parity/` and
whose `EMF-014` check fails the build the moment a named file is not where its
row says; [the witness list](../../witnesses.md);
[the root architecture](../../../../docs/architecture.md); the refusal
examples' README; and the two root tests that name a parity path in a comment
and in a fixture string. The ledger is the one that breaks the build if it is
forgotten.
38 changes: 36 additions & 2 deletions emf/docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ A module that needs a p2 bundle is an Eclipse bundle: a `META-INF/MANIFEST.MF`
naming the bundles it requires, and `eclipse-plugin` packaging. A manifest
switches on the parent's `bundle` profile, which runs the module's tests
through Maven Surefire on a plain classpath, outside OSGi, so every tool is
exercised through the standalone API a command-line run uses. Modules that
need no p2 bundle, like `parity/`, stay plain jars.
exercised through the standalone API a command-line run uses. A module that
needs no p2 bundle stays a plain jar. Every module is a bundle today, `parity/`
included, for the reason [Parity](#parity) records.

The first change to this tree is the **EMF scaffold**: the Maven reactor, the
wrapper, the gates and the `emf` CI job, with no EMF dependency and one module,
Expand Down Expand Up @@ -89,6 +90,18 @@ Tycho configuration and the target platform.
A module may depend on the modules above it in this table and on nothing
below.

Every module is Java. Ecore generates Java, Xtext's runtime hooks are Java, and
a bundle compiles through Tycho's JDT compiler against a target platform holding
no Kotlin unit, so a language other than Java is a bundle's problem before it is
anything else.

> **Proposed, not landed:**
> [0122](adr/emf/0122-bundles-and-tests-are-separate-tiers.md) splits these six
> into `bundles/` for what Tycho builds and Eclipse imports and `tests/` for what
> only Maven runs, and makes the second tier Kotlin. Until the moving pull
> request lands, the flat list above is the tree, and the Java sentence above
> holds over all of it.

## Metamodels

Two hand-written Ecore metamodels, as the project proposal defines them. Both
Expand Down Expand Up @@ -232,6 +245,27 @@ Deployment model kept inside `emf/`; it is test input, never an oracle. Output i
committed `rendered/` tree, so whitespace, key order and the `GENERATED` header
are template decisions made to match the oracle, not presentation.

## Parity

`parity/` holds this repository's own evidence: the suites that assert each stage
against the committed oracles, the ledger checks, and the module rules. It is
built and run by Maven only, and no examiner opens it.

Today it reaches the pipeline through Java: `ParityTest` calls `Pipeline`,
`Parsed` and `Diagnostic` from `cli/` and `Descriptor` and `ProjectIntentPackage`
from `metamodel/`. Those types resolve from p2, so the module carries a
`META-INF/MANIFEST.MF` and `eclipse-plugin` packaging, and the sentence in
[Toolchain](#toolchain) about a module that needs no p2 bundle does not reach it.
Its `src/main` holds `CanonicalJson` and `Ledgers`, neither of which touches EMF.

> **Proposed, not landed:**
> [0121](adr/emf/0121-parity-crosses-the-cli-file-interface.md) moves the suites
> onto the pipeline's file interface (arguments and input files in; an exit
> code, diagnostics, the parsed intent, the descriptor and the rendered tree
> out), so the module holds no EMF type and builds as a plain jar. What follows once
> it lands: a value a parity case asserts on is a value the pipeline writes, and
> a derivation with no output file is not evidence.

## Witnesses

A behaviour ledger row whose behaviour is the model's own is proved in both
Expand Down
Loading