From 61ee709d211025274a867abc4d993e2b0522717a Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers Date: Mon, 14 Sep 2026 13:05:32 +0200 Subject: [PATCH] docs: record the coursework EMF implementation and the parity contract that binds it --- .github/workflows/ci.yml | 5 + CLAUDE.md | 20 +++ docs/adr/README.md | 16 ++ ...5-one-hexagon-domain-mirrors-the-layers.md | 6 + .../0066-wire-shape-is-not-the-domain.md | 7 + .../0100-tests-run-in-process-on-vitest.md | 6 + .../0102-the-gate-grows-with-the-code.md | 6 + ...plementations-meet-at-committed-oracles.md | 76 ++++++++++ docs/adr/architecture/README.md | 6 + docs/architecture.md | 56 +++++++ docs/requirements.md | 2 +- emf/README.md | 24 +++ emf/docs/adr/README.md | 43 ++++++ ...del-is-expressible-in-the-emf-toolchain.md | 50 +++++++ ...is-coursework-scoped-and-self-contained.md | 57 +++++++ ...-tycho-against-a-pinned-target-platform.md | 54 +++++++ ...etamodels-are-hand-written-per-document.md | 55 +++++++ ...straints-are-complete-ocl-named-by-code.md | 52 +++++++ ...es-the-authored-yaml-into-the-metamodel.md | 50 +++++++ ...12-qvto-derives-the-resolved-deployment.md | 45 ++++++ ...3-acceleo-4-renders-the-deliverable-set.md | 46 ++++++ ...14-model-behaviours-have-a-java-witness.md | 48 ++++++ emf/docs/architecture.md | 140 ++++++++++++++++++ scripts/lint-adrs.ts | 3 + test/adr-contract.test.ts | 23 +++ 25 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 docs/adr/architecture/0105-two-implementations-meet-at-committed-oracles.md create mode 100644 emf/README.md create mode 100644 emf/docs/adr/README.md create mode 100644 emf/docs/adr/emf/0106-the-model-is-expressible-in-the-emf-toolchain.md create mode 100644 emf/docs/adr/emf/0107-emf-is-coursework-scoped-and-self-contained.md create mode 100644 emf/docs/adr/emf/0108-maven-and-tycho-against-a-pinned-target-platform.md create mode 100644 emf/docs/adr/emf/0109-ecore-metamodels-are-hand-written-per-document.md create mode 100644 emf/docs/adr/emf/0110-constraints-are-complete-ocl-named-by-code.md create mode 100644 emf/docs/adr/emf/0111-xtext-parses-the-authored-yaml-into-the-metamodel.md create mode 100644 emf/docs/adr/emf/0112-qvto-derives-the-resolved-deployment.md create mode 100644 emf/docs/adr/emf/0113-acceleo-4-renders-the-deliverable-set.md create mode 100644 emf/docs/adr/emf/0114-model-behaviours-have-a-java-witness.md create mode 100644 emf/docs/architecture.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7243875..f41ad6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -100,6 +100,11 @@ - 'name': 'ADR contract' 'run': 'npm run lint:adrs' + # The coursework implementation's register, linted as its own root. + # Deleted with emf/ (emf/docs/adr/emf/0107). + - 'name': 'ADR contract (emf)' + 'run': 'node scripts/lint-adrs.ts emf' + # Relative links and heading anchors across every tracked Markdown file. # The ADR domain move rewrote about a hundred references outside the # decision set, and nothing checked them. diff --git a/CLAUDE.md b/CLAUDE.md index 05fff43..c3e9fc2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,6 +55,26 @@ Delivery mechanics and co-testing are **defined separately**: see applier, a prune pass, a field manager or a co-test gate in `spec/v1`; state the model-level rule and point at the deferred set. +### The coursework implementation under `emf/` + +The MDE course requires the compiler to be built with Ecore, Xtext, OCL, +QVT-Operational and Acceleo, so `emf/` holds a second, hand-written Java +implementation (Maven and Tycho, JDK 21, no Eclipse IDE). It is deprecated +from the day it lands and deleted at its sunset: + +- **The root stays TypeScript.** Every pom, module, check, ledger and decision + of the Java side lives under `emf/`. The root references it only from CI + (the `emf` job and the `emf` ADR lint step), the `emf` domain in + `scripts/lint-adrs.ts` and its test, and + [`docs/architecture.md#the-parity-contract`](docs/architecture.md#the-parity-contract). +- **Never generate one implementation from the other.** Both are tested, + separately, against committed oracle files under `spec/v1/examples/`. +- **A model change lands in both implementations** and in the oracle files, + in one pull request. +- EMF decisions live in [`emf/docs/adr/`](emf/docs/adr/README.md), numbered + from the root's sequence. Check both registers before taking a number, and + lint with `node scripts/lint-adrs.ts emf`. + `deploy-config-schema` is the repository this one replaces. It stays alive and authoritative until `deploy-kit` can render the estate; do not treat it as dead. diff --git a/docs/adr/README.md b/docs/adr/README.md index 43a80f3..674fbe8 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -33,6 +33,16 @@ severity and receivers left to the stack that reads the projection (0021, resembled (0016, 0083). The worked examples now declare every applicable authored field and annotate its effect. +The set was amended on 2026-09-14 for the coursework implementation: the +model-driven engineering course requires the compiler to be built with Ecore, +Xtext, OCL, QVT-Operational and Acceleo, so a second, hand-written Java +implementation lives under `emf/` until its sunset condition holds. The two are +held equal by committed oracle files and nothing else (0105); 0065, 0066, 0100 +and 0102 were amended in place to scope them to the TypeScript tree. The Java +implementation's own decisions live in +[`emf/docs/adr/`](../../emf/docs/adr/README.md), numbered from the same +sequence, and are deleted with it. The model is unchanged. + Tier-0 **premises** carry one falsifiable claim each; tier-1 **decisions** name the premises they stand on in `rests-on`. A `claim: open` means decided in direction, untested: its owner and settling test are in the file. @@ -54,6 +64,11 @@ which domain a decision lives in. | [`architecture/`](architecture/) | the compiler's own structure: layering, ports, error model, gates | `docs/architecture.md`, `docs/architecture-rules.md` | yes | | [`deferred/`](deferred/README.md) | delivery mechanics and co-testing, defined separately | sections these chapters deliberately lack | no | +The coursework implementation keeps a register of its own at +[`emf/docs/adr/`](../../emf/docs/adr/README.md), linted by the same script with +`emf` as its root. Its numbers come from this sequence: before taking a number, +check both registers. + ## Premises | # | title | claim | normative | @@ -205,3 +220,4 @@ Decisions about the compiler's own structure, not about the model. Their | [0102](architecture/0102-the-gate-grows-with-the-code.md) | A new gate's script and its CI job land in the same pull request, and a test proves the two stay matched | settled | | [0103](architecture/0103-a-behaviour-ledger-names-what-a-test-proves.md) | A behaviour ledger names every guarantee and the test that proves it, and a meta test holds the two together | settled | | [0104](architecture/0104-every-enforced-rule-has-an-id-a-row-and-a-fixture.md) | Every enforced rule has an id, a ledger row and a fixture that proves it fires | settled | +| [0105](architecture/0105-two-implementations-meet-at-committed-oracles.md) | Two hand-written implementations meet at committed oracle files, and neither is generated from or tested against the other | open | diff --git a/docs/adr/architecture/0065-one-hexagon-domain-mirrors-the-layers.md b/docs/adr/architecture/0065-one-hexagon-domain-mirrors-the-layers.md index da3b7c1..361e0af 100644 --- a/docs/adr/architecture/0065-one-hexagon-domain-mirrors-the-layers.md +++ b/docs/adr/architecture/0065-one-hexagon-domain-mirrors-the-layers.md @@ -9,6 +9,12 @@ rests-on: ["0003"] # One hexagon, two use-cases, and a domain whose folders are the three layers +> **Amended 2026-09-14.** Scoped to the TypeScript tree under `src/`. For the +> length of the model-driven engineering course a second, Java implementation +> lives under `emf/`, bound to this one only by the parity contract +> ([0105](0105-two-implementations-meet-at-committed-oracles.md)). The rings +> below describe `src/`; nothing in `emf/` imports from it or is held to them. + > **Amended 2026-09-08.** Still two use-cases, one core, but `publish` no > longer renders anything. A repository validates its Intent Fragment and pushes > it by digest; every adapter runs in `compose` diff --git a/docs/adr/architecture/0066-wire-shape-is-not-the-domain.md b/docs/adr/architecture/0066-wire-shape-is-not-the-domain.md index 8590a56..5f9eff7 100644 --- a/docs/adr/architecture/0066-wire-shape-is-not-the-domain.md +++ b/docs/adr/architecture/0066-wire-shape-is-not-the-domain.md @@ -9,6 +9,13 @@ rests-on: ["0007"] # Zod declares the authoring shape, and a mapper turns it into the domain +> **Amended 2026-09-14.** "The one validator" means the one validator in +> `src/`. The Java implementation under `emf/` validates with OCL, and the two +> are held to the same constraints through the constraint ledger and the +> refused cases' committed diagnostics +> ([0105](0105-two-implementations-meet-at-committed-oracles.md)), never by +> generating one from the other. + ## Rests on The shape a human writes and the shape the core reasons about differ enough that one type cannot be both, and the difference is exactly what a mapper per document diff --git a/docs/adr/architecture/0100-tests-run-in-process-on-vitest.md b/docs/adr/architecture/0100-tests-run-in-process-on-vitest.md index 9ce6eba..18cc4d1 100644 --- a/docs/adr/architecture/0100-tests-run-in-process-on-vitest.md +++ b/docs/adr/architecture/0100-tests-run-in-process-on-vitest.md @@ -9,6 +9,12 @@ rests-on: ["0001"] # Tests run in-process on Vitest, and the tooling is TypeScript that Node runs directly +> **Amended 2026-09-14.** Scoped to the repository root. The Java +> implementation under `emf/` is tested with JUnit through Maven, inside `emf/` +> ([0105](0105-two-implementations-meet-at-committed-oracles.md)); the root +> stays TypeScript, and this decision is unchanged for everything outside +> `emf/`. + ## Rests on Every gate this repository runs can be exercised in-process by one test runner that also measures coverage and hosts mutation testing, so no gate needs a diff --git a/docs/adr/architecture/0102-the-gate-grows-with-the-code.md b/docs/adr/architecture/0102-the-gate-grows-with-the-code.md index 81fc360..8bc83cc 100644 --- a/docs/adr/architecture/0102-the-gate-grows-with-the-code.md +++ b/docs/adr/architecture/0102-the-gate-grows-with-the-code.md @@ -9,6 +9,12 @@ rests-on: ["0001"] # A new gate's script and its CI job land in the same pull request, and a test proves the two stay matched +> **Amended 2026-09-14.** A gate can also be a CI job with no npm script: +> the `emf` job runs Maven inside `emf/` +> ([0105](0105-two-implementations-meet-at-committed-oracles.md)). It lands in +> the pull request that adds the Maven build, joins `Pipeline Complete`, and is +> deleted with `emf/`. + ## Rests on The repository's own npm scripts are the whole list of gates, so the list a pull request's checks show and the list `package.json` declares can be kept diff --git a/docs/adr/architecture/0105-two-implementations-meet-at-committed-oracles.md b/docs/adr/architecture/0105-two-implementations-meet-at-committed-oracles.md new file mode 100644 index 0000000..ab9167b --- /dev/null +++ b/docs/adr/architecture/0105-two-implementations-meet-at-committed-oracles.md @@ -0,0 +1,76 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#the-parity-contract +rests-on: ["0003", "0006"] +--- + +# Two hand-written implementations meet at committed oracle files, and neither is generated from or tested against the other + +## Rests on +The three layers give three points where a run's state is a complete, closed +document, and pinned inputs make each of those documents a function of the +input, so two independent implementations can be compared through a committed +file at each layer without either running beside the other. False if: some +layer's content depends on how an implementation orders or identifies things +in a way canonical JSON cannot normalise, so two correct implementations +disagree byte for byte. Settled by: both implementations green against every +oracle file of every case under `spec/v1/examples/`, with no oracle carrying an +implementation-specific field. + +## Why +The course this repository is coursework for rejected the proposal to build +the compiler in TypeScript alone (`docs/mde/task-0-proposal/Sections/07-tooling.tex` +asked for that substitution and flagged that it needed approval). The graded +work has to use Ecore, Xtext, OCL, QVT-Operational and Acceleo. The TypeScript +compiler stays the long-lived implementation, so for the length of the course +there are two. + +Generating one from the other was the obvious way to keep them equal, and it +was ruled out: the course grades the modelling artifacts as authored work, and +the TypeScript types generated from Ecore, or Ecore generated from Zod, would +make one side a derivative the other cannot be checked against. Two hand-written +implementations can drift, so equality has to be proven. + +It is proven at the layer boundaries the model already has +([0003](../model/0003-three-layer-meta-model.md)). A parsed intent, a Resolved +Deployment and a Deliverable Set are each a whole document, and +[0006](../model/0006-pinned-inputs.md) makes each a function of pinned inputs, +so a committed canonical copy of each is an oracle both sides can be held to +separately. A failure then names the side that is wrong, which a diff between +two runs never does. `spec/v1/examples/` already carries hand-rendered +Deliverable Sets and refused cases; the contract adds the two intermediate +documents, the diagnostics of each refused case and the metamodel's structure. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| Generate TypeScript from Ecore, or Ecore from TypeScript | No parity tests for structure at all | The course grades the modelling artifacts as authored work, and a generated side cannot fail independently of its source | +| Run both implementations and diff their outputs in CI | No committed intermediate files | A red diff does not say which side is wrong, and neither suite can run alone | +| Only the rendered tree as oracle | Uses what already exists | A resolution bug and a rendering bug look the same, and the metamodel and validation parities have nothing to compare | + +## Reversibility +Undo cost today: deleting the contract section and the oracle files nobody has +written yet. Becomes irreversible once: never; at the second implementation's +sunset the oracle files stay as the TypeScript compiler's own golden files and +only the Java side of each check is deleted. + +## Consequences +- Every behaviour change touches an oracle file, and both implementations stay + red until both are fixed. Paid by whoever changes the model, in two + implementations, for as long as there are two. +- Diagnostics carry a stable code and a JSON Pointer in both implementations, + so a refusal's location becomes contract rather than presentation. Paid once + per constraint, in the constraint ledger. +- Canonical JSON is a new serialisation both sides must produce exactly. Paid + once per implementation, and kept afterwards as the TypeScript golden format. +- The architecture decisions that assume one implementation + ([0065](0065-one-hexagon-domain-mirrors-the-layers.md), + [0066](0066-wire-shape-is-not-the-domain.md), + [0100](0100-tests-run-in-process-on-vitest.md), + [0102](0102-the-gate-grows-with-the-code.md)) are scoped to the TypeScript + tree; the Java tree's decisions live in `emf/docs/adr/` and are deleted with + it. diff --git a/docs/adr/architecture/README.md b/docs/adr/architecture/README.md index 558d32d..0df133a 100644 --- a/docs/adr/architecture/README.md +++ b/docs/adr/architecture/README.md @@ -21,3 +21,9 @@ knowing which domain it lands in. The register is The scope boundary is stated in [chapter 00](../../../spec/v1/00-overview.md#programme-scope). + +Four of these decisions are scoped to the TypeScript tree for as long as the +coursework implementation under `emf/` exists, and +[0105](0105-two-implementations-meet-at-committed-oracles.md) is the contract +between the two. The Java implementation's own structure is decided in +[`emf/docs/adr/`](../../../emf/docs/adr/README.md), not here. diff --git a/docs/architecture.md b/docs/architecture.md index 89dfc61..5d795ba 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -186,6 +186,62 @@ CRD schemas, so a file that parses but cannot apply fails the build. Every gate carries negative fixtures. A check that has only ever run against a clean tree is untested: nothing proves it would fail. +## The parity contract + +This compiler has a second implementation. The model-driven engineering course +this repository is coursework for requires Ecore, Xtext, OCL, QVT-Operational +and Acceleo, so a Java implementation lives under [`emf/`](../emf/README.md) +until its sunset condition holds +([0105](adr/architecture/0105-two-implementations-meet-at-committed-oracles.md)). +Everything about that implementation, its build, its checks and its decisions, +lives inside `emf/`. What lives here is the contract both implementations +answer to, because the contract outlives the second implementation. + +Neither implementation is generated from the other, and neither is the oracle +for the other. Each is tested on its own against committed oracle files, and +agreement between the two follows from both agreeing with the oracle. + +| oracle | where | compared as | +|---|---|---| +| the parsed Service Intent | `spec/v1/examples//expected/intent.json` | canonical JSON, byte for byte | +| the Resolved Deployment | `spec/v1/examples//expected/resolved.json` | canonical JSON, byte for byte | +| the Deliverable Set | `spec/v1/examples//rendered/` | the existing golden tree, byte for byte | +| the diagnostics of a refused case | `.diagnostics.json` beside the refused input in `refusals/` or `negative/` | a set of `(code, path)` pairs | +| the metamodel's structure | `spec/v1/examples/expected/descriptor.json` | canonical JSON, byte for byte | + +**Canonical JSON** is RFC 8785 (JSON Canonicalization Scheme): keys sorted, +numbers in their shortest form, no insignificant whitespace. An absent +optional field is absent, never `null`. + +**A path** is an RFC 6901 JSON Pointer into the canonical intent document, +`/services/0/observability/alertClass`. A diagnostic about a derived value +points at the authored value it derives from. Messages and hints are free per +implementation; the code and the path are the contract. + +**The descriptor** lists every class of every layer with its features, each +feature's type and multiplicity, and every closed vocabulary with its literals. +The TypeScript side builds it from the Zod schemas with Zod's native +`z.toJSONSchema()` and a normaliser; the descriptor's shape is fixed here, not +by either source format. + +**The constraint ledger** gives every model constraint a `CONS-NNN` id, the +diagnostic code it emits, the check that enforces it in `src/`, and a refused +fixture that proves it fires. A constraint the ledger does not list is not part +of the model's validation, whichever implementation happens to enforce it. + +**Behaviour rows.** A row of the [behaviour ledger](requirements.md) whose +behaviour is the model's own (parse, validate, resolve, render) is proved in +both implementations. The row names the TypeScript test; the Java witness for +the same id is listed inside `emf/`, and `emf/`'s own gate fails when a model +row has no witness there. + +An oracle file changes in the pull request that changes the behaviour it +records, and both implementations go red together until both are fixed. CI +never regenerates an oracle file from either implementation: a tool may write a +candidate, and the committed file is the reviewed copy of it. A +case without every oracle file is not yet a parity case, and the ledger that +lists cases says so rather than skipping it silently. + ## Gates Fourteen gates hold the structure, and each exists because its absence has already diff --git a/docs/requirements.md b/docs/requirements.md index 4a1fbe8..711627b 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -23,7 +23,7 @@ This ledger holds **14** rows. The compiler's behaviours join it as they land. | id | a contributor or a consumer can rely on | proved by | |---|---|---| -| REQ-001 | Every decision record under `docs/adr/` satisfies its frontmatter, register and citation contract | [test/adr-contract.test.ts](../test/adr-contract.test.ts) | +| REQ-001 | Every decision record under `docs/adr/` and `emf/docs/adr/` satisfies its frontmatter, register and citation contract, and no number is used in both | [test/adr-contract.test.ts](../test/adr-contract.test.ts) | | REQ-002 | Every relative link and heading anchor in tracked Markdown resolves to a real target | [test/link-contract.test.ts](../test/link-contract.test.ts) | | REQ-003 | Every rendered Kubernetes manifest in the worked examples validates against its pinned schema | [test/manifest-contract.test.ts](../test/manifest-contract.test.ts) | | REQ-004 | The compiler's layer boundaries and module reachability are enforced on the dependency graph, not on review alone | [test/boundary-contract.test.ts](../test/boundary-contract.test.ts) | diff --git a/emf/README.md b/emf/README.md new file mode 100644 index 0000000..451b194 --- /dev/null +++ b/emf/README.md @@ -0,0 +1,24 @@ +# emf: the coursework implementation + +Everything under this directory is the second implementation of the +deploy-kit compiler, built with the Eclipse Modeling Framework toolchain the +model-driven engineering course at the University of Twente requires: Ecore, +Xtext, OCL, QVT-Operational and Acceleo. It is **deprecated from the day it +lands**: it exists for the length of the course and is deleted when its sunset +condition holds +([0107](docs/adr/emf/0107-emf-is-coursework-scoped-and-self-contained.md)). + +The repository root stays TypeScript. Nothing outside `emf/` builds, imports or +depends on anything inside it. The Maven build, its modules, its checks, its +ledgers and its decisions all live here, so the sunset is deleting this +directory and the few root lines that name it, listed in +[the architecture](docs/architecture.md#scope-and-sunset). + +The one thing this implementation shares with the TypeScript compiler is the +[parity contract](../docs/architecture.md#the-parity-contract): the committed +oracle files under `spec/v1/examples/` that both are tested against, separately. + +| read | for | +|---|---| +| [docs/architecture.md](docs/architecture.md) | the structure: toolchain, modules, how each stage meets the contract | +| [docs/adr/README.md](docs/adr/README.md) | the decisions that shaped it, and the register | diff --git a/emf/docs/adr/README.md b/emf/docs/adr/README.md new file mode 100644 index 0000000..635f210 --- /dev/null +++ b/emf/docs/adr/README.md @@ -0,0 +1,43 @@ +# Decision register: the coursework implementation + +The decisions that shape the EMF implementation under `emf/`. Every one is +deleted with this directory +([0107](emf/0107-emf-is-coursework-scoped-and-self-contained.md)), and none of +them can change what the model means: the model's decisions are in +[`docs/adr/`](../../../docs/adr/README.md), and the contract this +implementation shares with the TypeScript compiler is +[0105](../../../docs/adr/architecture/0105-two-implementations-meet-at-committed-oracles.md). + +The contract every ADR here satisfies is the root register's, unchanged: +frontmatter schema, one falsifiable claim per premise, `rests-on` naming +premises only, the Alternatives table and the citation rule. `normative:` +pointers name sections of [`emf/docs/architecture.md`](../architecture.md). +`node scripts/lint-adrs.ts emf` enforces all of it. + +Numbers come from the one estate-wide sequence shared with `docs/adr/`. Before +taking a number, check both registers. + +## Domains + +| directory | holds | `normative:` pointers resolve against | linted | +|---|---|---|---| +| [`emf/`](emf/) | the EMF implementation: toolchain, metamodels, constraints, grammar, transformation, templates, witnesses | `emf/docs/architecture.md` | yes | + +## Premises + +| # | title | claim | normative | +|---|---|---|---| +| [0106](emf/0106-the-model-is-expressible-in-the-emf-toolchain.md) | The v1 model is expressible in the EMF toolchain without changing the model | open | architecture.md#scope-and-sunset | + +## Decisions + +| # | title | claim | +|---|---|---| +| [0107](emf/0107-emf-is-coursework-scoped-and-self-contained.md) | The EMF implementation is coursework-scoped, lives entirely under `emf/`, and is deleted when two conditions hold | open | +| [0108](emf/0108-maven-and-tycho-against-a-pinned-target-platform.md) | Maven builds `emf/`, with Tycho resolving p2-only bundles against a pinned target platform, on JDK 21, with no Eclipse IDE | open | +| [0109](emf/0109-ecore-metamodels-are-hand-written-per-document.md) | Each model document has a hand-written Ecore metamodel, and its Java is generated at build time | open | +| [0110](emf/0110-constraints-are-complete-ocl-named-by-code.md) | Constraints are Complete OCL invariants named by the diagnostic code they emit | open | +| [0111](emf/0111-xtext-parses-the-authored-yaml-into-the-metamodel.md) | The Xtext grammar parses the authored YAML files themselves, into the imported metamodel | open | +| [0112](emf/0112-qvto-derives-the-resolved-deployment.md) | QVT-Operational derives the Resolved Deployment | open | +| [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 | diff --git a/emf/docs/adr/emf/0106-the-model-is-expressible-in-the-emf-toolchain.md b/emf/docs/adr/emf/0106-the-model-is-expressible-in-the-emf-toolchain.md new file mode 100644 index 0000000..edfc14c --- /dev/null +++ b/emf/docs/adr/emf/0106-the-model-is-expressible-in-the-emf-toolchain.md @@ -0,0 +1,50 @@ +--- +tier: premise +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#scope-and-sunset +--- + +# The v1 model is expressible in the EMF toolchain without changing the model + +## Rests on +Every structure, constraint, derivation and rendered byte `spec/v1` defines can +be carried by Ecore, Complete OCL, an Xtext grammar, a QVT-Operational +transformation and Acceleo templates, so the second implementation needs no +change to `spec/v1` and no field the TypeScript compiler lacks. False if: a +parity case can only be made green by editing a chapter, an oracle file, or +the parity contract to suit a tool. Settled by: the `emf` job green on every +parity case under `spec/v1/examples/` with `git diff --stat spec/v1 +docs/architecture.md` over the whole `emf/` history touching only oracle files +both implementations were already failing. + +## Why +Every decision in this domain assumes the course's toolchain can carry the +model as it stands. If it cannot, the choice is between bending the model to a +tool that will be deleted and failing a parity case on purpose, and that choice +has to be visible rather than made quietly in a template. The claim is open +because nothing has been built: the likeliest counterexamples are the YAML +subset Xtext has to parse, byte-exact output from Acceleo, and derivations over +the whole composed union in QVTo. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| Assume it and say nothing | No premise to maintain | A model change made to suit a coursework tool would look like a model decision | +| Allow the EMF side a documented subset of the model | Less work in October | The course project is graded on the whole pipeline, and a subset makes parity a claim about part of the model | + +## Reversibility +Undo cost today: deleting this file. Becomes irreversible once: never; the +premise describes a test, and a counterexample is recorded as a gap, not built +around. + +## Consequences +- A counterexample is written into this file as evidence and raised as a model + question in its own pull request, never absorbed into `emf/`. Paid by whoever + finds it, in one issue. +- The model stays tool-neutral: nothing in `spec/v1` depends on Ecore, Eclipse + OCL, Xtext, QVTo or Acceleo. The Essential OCL statements in the chapters' + rule tables are OMG notation, not tool input, and the Complete OCL invariants + under `emf/` are held to them. Paid by nobody while this holds. diff --git a/emf/docs/adr/emf/0107-emf-is-coursework-scoped-and-self-contained.md b/emf/docs/adr/emf/0107-emf-is-coursework-scoped-and-self-contained.md new file mode 100644 index 0000000..3bf5bb8 --- /dev/null +++ b/emf/docs/adr/emf/0107-emf-is-coursework-scoped-and-self-contained.md @@ -0,0 +1,57 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#scope-and-sunset +rests-on: ["0106"] +--- + +# The EMF implementation is coursework-scoped, lives entirely under `emf/`, and is deleted when two conditions hold + +## Rests on +The premise here is +[0106](0106-the-model-is-expressible-in-the-emf-toolchain.md): the toolchain +can carry the model unchanged. False if: deleting `emf/` and the root lines the +architecture lists as naming it leaves anything in the repository broken or +changes any behaviour of `src/`. Settled by: the sunset pull request's CI green +with exactly those removals and no other change outside `spec/v1/examples/`. + +## Why +The course requires the EMF toolchain; the estate does not. The TypeScript +compiler is the implementation the estate will run, and the proposal +(`docs/mde/task-0-proposal/Sections/07-tooling.tex`) already argued that a JVM +and Eclipse toolchain buys nothing for the deployment workflow. The Java tree is +therefore built to be removed. + +Removal is cheap only if nothing outside `emf/` knows it exists. So the Maven +build, its modules, its checks, its ledgers and its decisions live under +`emf/`, and the root names it only where it must: CI, the ADR lint's domain +list and its test, and the parity contract's pointer. + +"Temporary" alone names no moment, so the sunset is two conditions. The grade +alone is not enough: if the TypeScript compiler still lags when the grade is +recorded, deleting `emf/` would delete the only complete implementation. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| Remove after the course ends, by date | Simplest to state | A date says nothing about whether the TypeScript compiler can stand alone yet | +| Keep it permanently as a second implementation | No sunset work | Every model change in two languages, forever, for a toolchain the estate does not use | +| EMF decisions and checks in the root `docs/` and `scripts/` | One register, one place to look | Sunset becomes picking files out of shared directories, and the root stops being TypeScript | + +## Reversibility +Undo cost today: moving files out of `emf/`, an hour. Becomes irreversible +once: never; the point of the layout is that deleting it is always one pull +request. + +## Consequences +- The root gains a CI job, a lint step, a lint domain entry with its test, and + one architecture section, and loses all but the section at sunset. Paid in + that pull request. +- EMF ADR numbers come from the estate-wide sequence, so a number used under + `emf/docs/adr/` is never reused under `docs/adr/`, and the reverse. Paid by + whoever picks the next number, by checking both registers. +- `emf/` cannot import TypeScript tooling: its own checks are JUnit through + Maven. Paid in duplicated check code, deleted with it. diff --git a/emf/docs/adr/emf/0108-maven-and-tycho-against-a-pinned-target-platform.md b/emf/docs/adr/emf/0108-maven-and-tycho-against-a-pinned-target-platform.md new file mode 100644 index 0000000..7c5143a --- /dev/null +++ b/emf/docs/adr/emf/0108-maven-and-tycho-against-a-pinned-target-platform.md @@ -0,0 +1,54 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#toolchain +rests-on: ["0106"] +--- + +# Maven builds `emf/`, with Tycho resolving p2-only bundles against a pinned target platform, on JDK 21, with no Eclipse IDE + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim here is that every tool the course requires runs headless from one Maven +build: EMF, Xtext, Eclipse OCL, QVT-Operational and Acceleo 4 all resolve from +Maven Central or a pinned p2 site and run without an OSGi workspace. False if: +any of them needs the Eclipse IDE, a launch configuration or a manual step to +produce what CI checks. Settled by: the walking skeleton's `mvn verify` green +in CI from a clean runner cache. + +## Why +The course teaches these tools inside the Eclipse IDE. The estate works in +IntelliJ and CI, and a build a grader cannot reproduce with one command is a +build nobody can check. So the IDE is out, and the question is which build tool +reaches the tools. + +Gradle was the preference and was dropped: the EMF-family tools that publish +only to p2 have no maintained Gradle route, and the Eclipse projects themselves +build with Maven and Tycho. Tycho exists to resolve p2 repositories from Maven, +with the target platform as one file pinned to exact versions, which is the +same pinning discipline the rest of the repository applies to CI binaries. +JDK 21 is the current LTS the Eclipse releases target. + +The claim is open because resolution has not been tried. The walking skeleton +is the first change to `emf/` for that reason. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| Gradle | The estate's preferred build tool, existing templates | p2-only bundles need unmaintained plugins or hand-vendored jars, and every tool's documented headless route is Maven | +| Plain Maven with vendored jars | No Tycho conventions | Transitive bundles resolved by hand and upgraded by hand | +| The Eclipse IDE, as taught | Matches the lectures | Not reproducible in CI, not the estate's editor | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; the build is +deleted with `emf/`. + +## Consequences +- CI gains a JDK and a Maven cache for one job. Paid in pipeline minutes. +- Modules that consume p2 bundles follow Tycho's packaging conventions. Paid + once per module. +- A tool that turns out not to run headless stops Task 1 early rather than in + October. Paid by the skeleton, in a day. diff --git a/emf/docs/adr/emf/0109-ecore-metamodels-are-hand-written-per-document.md b/emf/docs/adr/emf/0109-ecore-metamodels-are-hand-written-per-document.md new file mode 100644 index 0000000..f316b0f --- /dev/null +++ b/emf/docs/adr/emf/0109-ecore-metamodels-are-hand-written-per-document.md @@ -0,0 +1,55 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#metamodels +rests-on: ["0106"] +--- + +# Each model document has a hand-written Ecore metamodel, and its Java is generated at build time + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim is that Service Intent, Platform Intent, the Resolved Deployment and the +Deliverable Set each fit one Ecore package whose structure, exported as the +parity descriptor, equals the structure the Zod schemas in `src/` declare. +False if: the descriptor exported from Ecore cannot equal the committed +descriptor without a type, multiplicity or vocabulary that the TypeScript side +does not have. Settled by: the descriptor parity suite green in both +implementations. + +## Why +The course grades metamodelling as its first task, and the metamodel is what +every later tool reads: the grammar imports it, the constraints annotate it, +the transformation maps between its packages, and the templates walk it. It is +written by hand because the course grades authored metamodels and because +generating it from Zod would make its structure unfalsifiable against Zod. + +One package per document follows the model's own layering +([0003](../../../../docs/adr/model/0003-three-layer-meta-model.md)), with +Platform Intent separate because it is a separately authored document. +Cross-document references are Ecore references, so the transformation navigates +them rather than joining strings. + +Java is generated from the `.genmodel` during the build rather than committed: +the tools work on typed models most smoothly, and committed generated code is a +second copy that goes stale. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| Dynamic EMF, no generated Java | No code generation step | Every test and helper works through reflection, and the course's material assumes generated model code | +| Commit the generated Java | Navigable without a build | Large diffs on every metamodel change and a stale copy the moment someone forgets to regenerate | +| OCLinEcore or Xcore as the source | One textual file per layer | The `.ecore` is what every tool and the grader reads, so a second source format adds a translation | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; deleted with +`emf/`. + +## Consequences +- A model change is an `.ecore` edit and a Zod edit, and the descriptor suite + fails until both match. Paid by whoever changes the model. +- Class diagrams for the course report are drawn from the `.ecore` files, not + maintained separately. Paid by nobody. diff --git a/emf/docs/adr/emf/0110-constraints-are-complete-ocl-named-by-code.md b/emf/docs/adr/emf/0110-constraints-are-complete-ocl-named-by-code.md new file mode 100644 index 0000000..563ec4d --- /dev/null +++ b/emf/docs/adr/emf/0110-constraints-are-complete-ocl-named-by-code.md @@ -0,0 +1,52 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#constraints +rests-on: ["0106"] +--- + +# Constraints are Complete OCL invariants named by the diagnostic code they emit + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim is that every constraint in the constraint ledger is expressible as an +OCL invariant evaluated standalone, and that each refused case yields the same +set of `(code, path)` pairs from OCL as from Zod. False if: a ledger constraint +needs Java code outside OCL, or the containment chain cannot produce the JSON +Pointer the TypeScript side reports. Settled by: every refused case's +diagnostics parity green in `emf/parity`, and the ledger check finding an +invariant for every `CONS-NNN` row. + +## Why +OCL is the course's constraint language, and the TypeScript side's constraints +are Zod refinements. The two are held together by the constraint ledger and by +committed diagnostics, not by translation. + +Complete OCL files sit beside the metamodel rather than inside it, which keeps +structure and rules apart the way the wire schemas and the domain invariants +are apart in `src/`. Naming each invariant by its code makes the mapping from a +failed invariant to a diagnostic a name read, not a table to keep in step. + +The path is part of the contract, so it is computed, not described: the +containment chain gives each containing feature and index, which is what a +JSON Pointer into the canonical intent document is. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| OCLinEcore annotations inside the `.ecore` | One file per layer | Mixes structure and rules in the file the descriptor exporter reads | +| Java validators via EMF's validation framework | No OCL learning curve | The course grades OCL, and Java validators duplicate the TypeScript code in a third form | +| Compare codes only, not paths | No path computation | A diagnostic at the wrong object would pass | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; deleted with +`emf/`. + +## Consequences +- Every new constraint is a ledger row, a Zod check, an OCL invariant and a + refused fixture in one pull request. Paid by its author. +- OCL evaluation order is unspecified, so diagnostics compare as a set, never a + list. Paid by nobody; it is the contract. diff --git a/emf/docs/adr/emf/0111-xtext-parses-the-authored-yaml-into-the-metamodel.md b/emf/docs/adr/emf/0111-xtext-parses-the-authored-yaml-into-the-metamodel.md new file mode 100644 index 0000000..2a8ba01 --- /dev/null +++ b/emf/docs/adr/emf/0111-xtext-parses-the-authored-yaml-into-the-metamodel.md @@ -0,0 +1,50 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#concrete-syntax +rests-on: ["0106"] +--- + +# The Xtext grammar parses the authored YAML files themselves, into the imported metamodel + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim is that the YAML subset every authored example uses can be parsed by an +Xtext grammar with synthetic indentation tokens, producing instances of the +hand-written Service Intent and Platform Intent metamodels. False if: an +authored example needs YAML the grammar cannot parse without ambiguity, or the +parsed model serialises to an `intent.json` different from the committed one. +Settled by: the parsed-intent parity suite green for every authored file under +`spec/v1/examples/`. + +## Why +Parse parity is only direct when both implementations read the same bytes. A +new textual syntax for the Java side would double every fixture and add a +third failure mode, drift between a YAML file and its twin. + +Importing the metamodel instead of letting Xtext infer one keeps a single +layer-1 metamodel: the parser's output is what OCL validates and QVTo reads. +The price is that the grammar has to fit the metamodel's shape, so YAML keys +become keywords, and YAML the examples do not use is refused rather than +supported. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| A new DSL with paired YAML and DSL fixtures | A cleaner grammar to present | Every fixture written twice, and pairs drift | +| An inferred metamodel mapped to the hand-written one | The grammar is free of the metamodel's shape | A third metamodel and an extra transformation to keep in parity | +| A plain YAML loader instead of Xtext | Full YAML support | The course requires Xtext | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; deleted with +`emf/`. + +## Consequences +- A YAML construct new to the examples needs a grammar change before the Java + side can read it. Paid by whoever introduces it, and visible as a failing + parse parity case. +- The grammar refuses flow sequences, anchors and multi-document files unless + an example needs them. Paid by nobody while examples avoid them. diff --git a/emf/docs/adr/emf/0112-qvto-derives-the-resolved-deployment.md b/emf/docs/adr/emf/0112-qvto-derives-the-resolved-deployment.md new file mode 100644 index 0000000..f0ca7fb --- /dev/null +++ b/emf/docs/adr/emf/0112-qvto-derives-the-resolved-deployment.md @@ -0,0 +1,45 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#transformation +rests-on: ["0106"] +--- + +# QVT-Operational derives the Resolved Deployment + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim is that every derivation and assignment chapter 20 defines, including the +ones over the whole composed union, is expressible as QVTo mappings and helpers +run by the standalone executor. False if: a derivation needs Java black-box +code for anything but hashing, or the transformation's output serialises to a +`resolved.json` different from the committed one. Settled by: the resolved +parity suite green for every case under `spec/v1/examples/`. + +## Why +The course teaches ATL and QVT-Operational for model-to-model transformation. +The Resolved Deployment is derived mostly by computation over the whole input, +not by matching one element to one element: the path plan, collision detection, +the Reconcile Unit graph and the release gate inputs all read the composed +union. QVTo is imperative, which fits that; ATL's rule matching fits a +structural mapping and pushes the global derivations into helpers anyway. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| ATL | The most course examples | Global derivations end up as helper code outside the rule model | +| ATL for mapping, QVTo for derivations | Shows both taught tools | Two transformation runtimes to build and keep in parity | +| Java over the generated model | Fastest to write | The course grades a transformation language | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; deleted with +`emf/`. + +## Consequences +- Hashing for `renderHash` and content-hashed Assets is a black-box Java helper, + the one allowed. Paid once. +- A derivation the transformation lacks shows up as a resolved parity failure, + not a silent default. Paid by the author of the derivation. diff --git a/emf/docs/adr/emf/0113-acceleo-4-renders-the-deliverable-set.md b/emf/docs/adr/emf/0113-acceleo-4-renders-the-deliverable-set.md new file mode 100644 index 0000000..9fed94a --- /dev/null +++ b/emf/docs/adr/emf/0113-acceleo-4-renders-the-deliverable-set.md @@ -0,0 +1,46 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#text-generation +rests-on: ["0106"] +--- + +# Acceleo 4 renders the Deliverable Set, byte for byte against the committed tree + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim is that Acceleo 4 templates run standalone can reproduce every committed +`rendered/` file exactly, whitespace and key order included. False if: a +rendered file cannot be matched without post-processing the template output +outside Acceleo. Settled by: the rendered parity suite green for every case +under `spec/v1/examples/`, with no formatter between the templates and the +comparison. + +## Why +The course requires Acceleo for model-to-text, and code generation carries the +largest share of the project grade. Acceleo 4 is the maintained line, with AQL +queries and a standalone API; Acceleo 3 is in maintenance. + +Byte equality is strict for a template language, and it is the contract the +TypeScript serializer already meets. Relaxing it for one implementation would +make the rendered oracle mean two things. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| Acceleo 3 | Most tutorials | Maintenance mode, and its standalone launch goes through generated launchers | +| Compare parsed YAML, not bytes | Whitespace stops mattering | The TypeScript side is held to bytes, and determinism is part of the model | +| Templates plus a YAML formatter pass | Easy whitespace | The formatter, not the templates, would be doing the generation being graded | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; deleted with +`emf/`. + +## Consequences +- Whitespace control is a template concern throughout. Paid in template + complexity. +- A rendered file changed in `spec/v1/examples/` fails here until the template + matches. Paid by whoever changes it. diff --git a/emf/docs/adr/emf/0114-model-behaviours-have-a-java-witness.md b/emf/docs/adr/emf/0114-model-behaviours-have-a-java-witness.md new file mode 100644 index 0000000..f6accd9 --- /dev/null +++ b/emf/docs/adr/emf/0114-model-behaviours-have-a-java-witness.md @@ -0,0 +1,48 @@ +--- +tier: decision +status: proposed +claim: open +owner: joris +date: 2026-09-14 +normative: docs/architecture.md#witnesses +rests-on: ["0106"] +--- + +# Every model behaviour in the behaviour ledger has a Java witness, listed inside `emf/` + +## Rests on +Resting on [0106](0106-the-model-is-expressible-in-the-emf-toolchain.md), the +claim is that the behaviours a unit test proves in `src/` about parsing, +validation, resolution and rendering can each be proved by a JUnit test here, +and that a list inside `emf/` can be checked against the root ledger without +the root knowing about it. False if: a model behaviour row can only be proved +in one implementation, or keeping the list requires a column in the root +ledger. Settled by: the witness check in `emf/parity` green, and failing on a +fixture that removes one witness. + +## Why +The oracle files prove agreement on what the examples exercise. A unit test +proves a behaviour the examples may not reach, such as an error raised for an +input no worked example contains. Without a rule, one implementation tests it +and the other never does, and the parity contract says nothing. + +The list lives in `emf/` so the root ledger stays the TypeScript ledger it is, +and the sunset deletes the list with nothing to edit in the root. + +## Alternatives +| option | cost if taken | why rejected | +|---|---|---| +| A witness column in the root ledger | One table | The root gains an EMF dependency, and the sunset edits every row | +| Parity only through the examples | No list to maintain | Unit-level behaviour can diverge wherever no example reaches | +| A separate EMF requirements ledger with its own ids | Clean separation | Two ledgers can disagree about which behaviours exist | + +## Reversibility +Undo cost today: nothing exists. Becomes irreversible once: never; deleted with +`emf/`. + +## Consequences +- A model behaviour row lands with a Java witness or the `emf` job fails. Paid + by its author, in the same pull request. +- The check needs to know which rows are model behaviours; the TypeScript tests + that prove them live under `test/model/`, and that path is the marker. Paid + once, when the first model test lands. diff --git a/emf/docs/architecture.md b/emf/docs/architecture.md new file mode 100644 index 0000000..7d5ac34 --- /dev/null +++ b/emf/docs/architecture.md @@ -0,0 +1,140 @@ +# The coursework implementation's structure + +Normative for the code under `emf/`, the way +[`docs/architecture.md`](../../docs/architecture.md) is normative for `src/`. +Decisions in [`docs/adr/emf/`](adr/README.md) point their `normative:` field at +sections of this document, and the ADR lint checks that the anchor exists. + +This implementation answers to the model in [`spec/v1`](../../spec/v1/00-overview.md) +and to the [parity contract](../../docs/architecture.md#the-parity-contract), +and to nothing in `src/`. Where this document and either of those disagree, +they win and this document is what gets fixed. + +## Scope and sunset + +This tree implements the whole compiler: parse, validate, resolve and render, +for every case under `spec/v1/examples/`. It is built in the order the course +grades it. + +| course task | due | lands here | +|---|---|---| +| Task 1: metamodelling | 2026-09-25 | Ecore metamodels for all three layers and Platform Intent, the Xtext grammar, the OCL constraints; parity on the parsed intent, the descriptor and refused cases | +| Task 2: transformations | 2026-10-16 | the QVT-Operational transformation into the Resolved Deployment; parity on `resolved.json` | +| Task 3: code generation | 2026-10-30 | the Acceleo templates rendering the Deliverable Set; parity on `rendered/` | + +The tree is deleted in one pull request when both of these hold: + +1. the final grade for the course project is recorded; +2. the TypeScript suite is green on every parity case with the `emf` CI job + removed from `Pipeline Complete`. + +What that pull request keeps: every oracle file under `spec/v1/examples/`, the +constraint ledger without its OCL column, and the descriptor check. What it +deletes: this directory; the `emf` CI job and the `ADR contract (emf)` step in +`.github/workflows/ci.yml`; the `emf` domain entry in `scripts/lint-adrs.ts` +and the two `emf/` cases in `test/adr-contract.test.ts`; and the `emf/` +mentions in `docs/requirements.md`, `docs/adr/README.md` and `CLAUDE.md`. + +## Toolchain + +Maven, with Eclipse Tycho resolving the bundles that are published only to p2 +update sites, against one target platform file pinned to exact versions. Plain +Maven Central coordinates are used wherever a tool publishes there. JDK 21. +No Eclipse IDE, workspace or launch configuration is part of the build: every +step a grader or CI runs is `mvn verify` from `emf/`. + +The first change to this tree is a walking skeleton that proves each tool runs +headless in CI before any model work depends on it: an `.ecore` loads, an OCL +invariant fires, the Xtext parser reads a three-line document, a QVTo identity +transformation runs, and an Acceleo template writes one file. + +## Modules + +One Maven module per pipeline stage, under one parent `pom.xml` that owns the +Tycho configuration and the target platform. + +| module | holds | graded in | +|---|---|---| +| `metamodel/` | `.ecore` and `.genmodel` per layer, Complete OCL `.ocl` per layer, the descriptor exporter | Task 1 | +| `syntax/` | the Xtext grammar for the authored YAML subset | Task 1 | +| `resolve/` | the QVTo transformation from Service Intent and Platform Intent to the Resolved Deployment | Task 2 | +| `render/` | the Acceleo 4 templates from the Resolved Deployment to the Deliverable Set | Task 3 | +| `cli/` | the pipeline entry point: files in, canonical JSON, diagnostics and rendered files out | Task 1 onward | +| `parity/` | JUnit suites asserting each stage against the committed oracles, and the witness ledger check | Task 1 onward | + +A module may depend on the modules above it in this table and on nothing +below. + +## Metamodels + +One Ecore metamodel per document the model defines: Service Intent, Platform +Intent, Resolved Deployment and Deliverable Set. Each is hand-written `.ecore` +XMI, committed, with names taken unchanged from +[`CONTEXT.md`](../../CONTEXT.md). Cross-document references are Ecore +references across packages, not strings. + +Typed Java for each metamodel is generated from its `.genmodel` during the +Maven build into `target/`, and never committed. + +The descriptor exporter walks each `EPackage` reflectively and writes the +descriptor the parity contract fixes. It is the only place the Ecore structure +is compared with anything. + +## Constraints + +Every constraint lives in a Complete OCL file beside the metamodel it +constrains, one file per layer, evaluated by Eclipse OCL standalone. An +invariant's name is the diagnostic code it emits, so a failed invariant maps to +a diagnostic without a lookup table. + +An invariant failure becomes a diagnostic whose path is the JSON Pointer of the +offending object, computed from its containment chain: each containing feature's +name, and the index for a many-valued feature. Validation reports every failed +invariant, never only the first. + +The constraint ledger's OCL column lives in `emf/`: a table mapping each +`CONS-NNN` id to the OCL invariant that enforces it. `parity/` fails when a +ledger constraint has no invariant, or an invariant names a code no ledger row +carries. + +## Concrete syntax + +The Xtext grammar parses the same authored `.domain.yml` and +`platform.intent.yml` files the TypeScript compiler reads. It covers the YAML +subset those files use, with indentation handled by synthetic block tokens, and +refuses anything outside the subset with a diagnostic rather than a guess. + +The grammar imports the hand-written Service Intent and Platform Intent +metamodels, so the parser produces instances of the graded metamodel directly. +There is no inferred syntax metamodel and no mapping step between parsing and +validation. + +## Transformation + +A QVT-Operational transformation derives the Resolved Deployment from the +parsed Service Intent and Platform Intent, run through the standalone +transformation executor. Every derivation `spec/v1/20-resolved-deployment.md` +names is a mapping or a helper in it; a derived value with no mapping is a gap +the parity case for it exposes. + +## Text generation + +Acceleo 4 templates render the Deliverable Set from the Resolved Deployment, +run through Acceleo's standalone API. Output is compared byte for byte with the +committed `rendered/` tree, so whitespace, key order and the `GENERATED` header +are template decisions made to match the oracle, not presentation. + +## Witnesses + +A behaviour ledger row whose behaviour is the model's own is proved in both +implementations. `emf/docs/witnesses.md` lists, for each such `REQ-NNN` id, the +JUnit test that proves it here. `parity/` fails when a model row in +`docs/requirements.md` has no witness in that file, or a witness names a test +that does not exist or an id that no row carries. + +## Gates + +One CI job, `emf`, runs `mvn verify` in `emf/` on JDK 21 and is required by +`Pipeline Complete`. It runs every JUnit suite, the parity suites and the +ledger checks above. The ADR lint for `emf/docs/adr/` runs in the existing +`contracts` job, as `node scripts/lint-adrs.ts emf`. diff --git a/scripts/lint-adrs.ts b/scripts/lint-adrs.ts index ff13d26..b8aaa9e 100644 --- a/scripts/lint-adrs.ts +++ b/scripts/lint-adrs.ts @@ -38,6 +38,9 @@ const DOMAINS = [ domain: "architecture", normativeRoots: ["docs/architecture.md", "docs/architecture-rules.md"], }, + // The coursework implementation's register, linted with `emf` as the root, + // so its pointers name emf/docs/architecture.md. Deleted with emf/ (0107). + { domain: "emf", normativeRoots: ["docs/architecture.md"] }, ] as const; const SECTIONS = [ diff --git a/test/adr-contract.test.ts b/test/adr-contract.test.ts index 8340a35..c5c9efb 100644 --- a/test/adr-contract.test.ts +++ b/test/adr-contract.test.ts @@ -32,6 +32,29 @@ test("the ADR lint, run as a command, passes over the committed decision set", ( expect(out).toMatch(/^ADR lint: \d+ files clean$/m); }); +test("the coursework register under emf/, linted as its own root, passes", () => { + const out = execFileSync( + process.execPath, + [join(REPOSITORY, "scripts", "lint-adrs.ts"), join(REPOSITORY, "emf")], + { encoding: "utf8" }, + ); + expect(out).toMatch(/^ADR lint: \d+ files clean$/m); +}); + +test("no number is used in both the root register and the coursework register", () => { + const emfDir = join(REPOSITORY, "emf", "docs", "adr", "emf"); + const numbers = (dir: string): string[] => + (existsSync(dir) ? readdirSync(dir) : []) + .filter((file) => /^\d{4}-.+\.md$/.test(file)) + .map((file) => file.slice(0, 4)); + const root = new Set( + ["model", "architecture", "deferred"].flatMap((domain) => + numbers(join(ADR_DIR, domain)), + ), + ); + expect(numbers(emfDir).filter((n) => root.has(n))).toEqual([]); +}); + test("the decision set is non-empty, and one number is used once", () => { expect(adrFiles.length).toBeGreaterThanOrEqual(40); const numbers = adrFiles.map((file) => Number(basename(file).slice(0, 4)));