Skip to content

feat(emf): model the Resolved Deployment target metamodel in Ecore - #136

Merged
ExtraToast merged 3 commits into
mainfrom
87-resolved-deployment-ecore
Sep 16, 2026
Merged

ExtraToast merged 3 commits into
mainfrom
87-resolved-deployment-ecore

Conversation

@ExtraToast

Copy link
Copy Markdown
Contributor

Closes #87. Part of #78.

What this branch does

Task 2 transforms into the Resolved Deployment metamodel and Task 3 generates from it, so it is due for Task 1 alongside the source metamodel it is the other half of. Nothing existed: bundles/resolve and bundles/render each held a placeholder notes.ecore, and the module table named a target .ecore that was not in the tree.

  • The metamodel, bundles/metamodel/model/resolved-deployment.ecore, 35 classifiers in one package. Its Java is generated into target/ by a second MWE2 workflow, the way the source metamodel's already is, and is left out of formatting, coverage and mutation on the same rule. plugin.xml registers the package, so an examiner's Eclipse import resolves it.
  • models/minimal.resolveddeployment, the hand-written target model. Every value of it is derivable from notes.project.yml and the pinned Platform document and node contract beside it. It is test input and not an oracle: it is what the Acceleo templates are first run against (Tracer bullet for Task 3: render the minimal case with Acceleo from its hand-written target model #94) and what the QVT-Operational transformation must produce (Tracer bullet for Task 2: resolve the minimal case with QVT-Operational #90), so it is the target half of both tracers before either exists.
  • DependencyEdges, the export of the parity contract's dependencies.json: the consumer Process, the provider Application and Surface, the address the consumer is given, and the peers the policy allows.

The decisions

The package holds two halves, and that is the point. The first is every decision layer 2 takes, in the words CONTEXT.md gives them. The second is one class per resource family of the project proposal's generated-resources table, each carrying the path it is written to. The production implementation keeps those apart because nothing there reads them together; a model-to-text template reads one model, so here they are one package. That difference is exactly why the parity contract compares the two implementations through dependency edges and generated files rather than through resolved.json.

No name crosses between the halves. A WorkloadFile carries the controller kind and the strategy that spell a Process's cutover, and the cutover itself stays on the Process. The template decides nothing, which is the layer rule holding one level further down than the chapter states it.

The target package stays out of the descriptor, and that is now checked rather than asserted. A target class reaching the descriptor would put this implementation's shape into an oracle the other one has to match. The check allows exactly three names through: Cutover, Match and DurabilityClass are the source metamodel's own vocabularies, carried over because they mean the same thing.

What the tests hold

Eight, and two of them tie the model to the tree rather than to itself: the eight paths it assigns are compared with the files actually on disk under spec/v1/examples/minimal/rendered/, so a template that writes somewhere else fails here rather than at review.

minimal holds no dependency edge, which is the point of that case, so the edge's own shape is proved on a model built in the test: one edge the policy names a peer for, and one that resolves to an address with no peer of its own.

The pending witnesses move

REQ-033's reason said there was no Ecore metamodel here to validate against. This change makes that false, so it would have become a lie on merge. What is still missing is a model this implementation derives, which is #90. REQ-034 waits on placement resolving against the node contract (#91), and REQ-035 on something rendering (#94). All three stay owed; none is quietly discharged.

Verification

./mvnw -B -ntp verify from emf/ exits 0 locally: 53 tests, the jacoco floor at 1.00 and pitest's mutation threshold both met. Root npm run verify green at 782 tests, 35 ledger rows, 99.2% statements.

Two failures were caught locally before anything was pushed, which is worth recording because the previous branch learned it the hard way: spotless formatting, and the jacoco floor, because the edge-building path never ran against a case that has no edges.

Task 2 transforms into this metamodel and Task 3 generates from it, so it is
due for Task 1 with the source metamodel it is the other half of. Nothing
existed: `bundles/resolve` and `bundles/render` each held a placeholder
package, and the architecture's module table named a target `.ecore` that was
not there.

The package holds two halves, and that is the point. The first is every
decision layer 2 takes, in the words CONTEXT.md gives them. The second is one
class per resource family of the project proposal's generated-resources table,
each carrying the path it is written to. The production implementation keeps
those apart because nothing there reads them together; a model-to-text
template reads one model, so here they are one package. That difference is
exactly why the parity contract compares the two implementations through
dependency edges and generated files rather than through `resolved.json`.

No name crosses between the halves. A `WorkloadFile` carries the controller
kind and the strategy that spell a Process's `cutover`, and the `cutover`
itself stays on the Process, so the template decides nothing.

`models/minimal.resolveddeployment` is the hand-written target model, every
value of it derivable from `notes.project.yml` and the pinned Platform
document and node contract beside it. It is test input and not an oracle: it
is what the Acceleo templates are first run against (#94) and what the
QVT-Operational transformation must produce (#90), so it is the target half of
both tracers before either exists.

`DependencyEdges` exports the parity contract's `dependencies.json`, which is
the half of resolution both implementations must agree on before either
renders. `minimal` holds no edge, so the edge's own shape is proved on a model
built in the test: one edge the policy names a peer for, one without.

Two of the seven tests tie the model to the tree rather than to itself: the
eight paths it assigns are compared with the files actually under
`spec/v1/examples/minimal/rendered/`, so a template that writes somewhere else
fails here rather than at review.

The three pending witnesses move to where they can actually be discharged.
REQ-033's reason said there was no Ecore metamodel here to validate against,
which this change makes false; what is still missing is a model this
implementation *derives*, which is #90. REQ-034 waits on placement resolving
against the node contract (#91) and REQ-035 on something rendering (#94).
#87 says the target package is not part of the descriptor, which covers the
source metamodel only, and nothing checked it. A target class reaching the
descriptor would put this implementation's shape into an oracle the other one
has to match, and the two shape layer 2 differently on purpose.

The check is by name against the committed descriptor, and it allows exactly
three: `Cutover`, `Match` and `DurabilityClass` are the source metamodel's own
vocabularies, carried into the target package under the same names because
they mean the same thing.
@ExtraToast ExtraToast added type: feature New user-facing or operator-facing capability. area: deploy homelab-deploy, deploy-v2, manifests, and rollout flow. component: config Configuration schema, defaults, or repo settings. priority: P1 High; important and should be handled in the current iteration. labels Sep 16, 2026
@ExtraToast ExtraToast self-assigned this Sep 16, 2026
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

Shape

bucket files additions deletions
production code 4 +453 -0
tests 1 +226 -0
documentation 2 +18 -3
tooling 4 +156 -7

Coverage

Baseline: main @ 1061212.

metric this branch main diff
statements 99.20% 99.20% 0.00
branches 96.56% 96.56% 0.00
functions 100.00% 100.00% 0.00
lines 99.13% 99.13% 0.00

This pull request changed no covered line.

CodeQL reported three `java/local-variable-is-never-read` findings against
`ResolvedDeploymentTest`, on locals every one of which is passed to
`assertThat`. The cause is in the same log: buildless extraction failed to get
a dependency graph from Maven, and `.github/codeql/codeql-config.yml` excludes
`**/target/**`, where the metamodel's generated Java lives. A local whose only
read sits inside an expression over an unresolved type reads as never read.
`DescriptorTest` never trips this because it works off the committed JSON
rather than a generated package.

Filtering the rule was the other way out and it is worse: the rule is right in
general, and a repository-wide exclusion to accommodate one file's shape hides
the unused locals it exists to find. The three locals carried no meaning the
assertion did not already carry, so they are inlined, which is the shape the
Java test beside this one already uses. The descriptor is the exception: it is
read once per classifier inside a lambda, so it is a field rather than a local,
with a comment naming why.
@ExtraToast
ExtraToast merged commit 21cba92 into main Sep 16, 2026
21 checks passed
@ExtraToast
ExtraToast deleted the 87-resolved-deployment-ecore branch September 16, 2026 17:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: deploy homelab-deploy, deploy-v2, manifests, and rollout flow. component: config Configuration schema, defaults, or repo settings. priority: P1 High; important and should be handled in the current iteration. type: feature New user-facing or operator-facing capability.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Model the Resolved Deployment target metamodel in Ecore

2 participants