diff --git a/emf/docs/adr/README.md b/emf/docs/adr/README.md index ec5ef0b..9bb2751 100644 --- a/emf/docs/adr/README.md +++ b/emf/docs/adr/README.md @@ -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 | diff --git a/emf/docs/adr/emf/0121-parity-crosses-the-cli-file-interface.md b/emf/docs/adr/emf/0121-parity-crosses-the-cli-file-interface.md new file mode 100644 index 0000000..0e6b489 --- /dev/null +++ b/emf/docs/adr/emf/0121-parity-crosses-the-cli-file-interface.md @@ -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. diff --git a/emf/docs/adr/emf/0122-bundles-and-tests-are-separate-tiers.md b/emf/docs/adr/emf/0122-bundles-and-tests-are-separate-tiers.md new file mode 100644 index 0000000..5c2ec55 --- /dev/null +++ b/emf/docs/adr/emf/0122-bundles-and-tests-are-separate-tiers.md @@ -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 `` 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. diff --git a/emf/docs/architecture.md b/emf/docs/architecture.md index 7c207dd..0bcbb60 100644 --- a/emf/docs/architecture.md +++ b/emf/docs/architecture.md @@ -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, @@ -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 @@ -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