Skip to content

refactor(materializer): decouple template emission + coverage from hardcoded template registry #333

@jwulf

Description

@jwulf

Problem

materializer/src/index.ts open-codes each scenario template at four sites:

  1. The template emission loop (one block per template, ~10 lines each, around lines 562–599) wiring scenariosDir → output dir → emitter constructor by hand.
  2. The templateOutputDirs map (added in feat: suppress feature specs covered by scenario-template ontology #332 for coverage collection, around line 485) duplicating the template-name → output-dir mapping.
  3. Imports of each per-template emitter at the top of the file.
  4. Comments that enumerate the supported templates.

Adding a 5th ScenarioTemplate to configs/<config>/ontology/scenario-templates.json therefore requires:

  • An ABox entry (ontology — correct).
  • A new emitter implementation under materializer/src/templateEmitters/ (correct — emitters render Playwright code, not data, so a new template shape may need new emission logic).
  • Three separate edits in index.ts to wire it in (wrong — this is mechanical glue that should not require touching the orchestrator).

Surfaced during review of #332 / #331.

Proposal

Step 1 — hoist a single registry

Create materializer/src/templateRegistry.ts:

export interface TemplateBinding {
  name: string;              // 'EdgeLifecycle'
  outputDir: string;         // 'edges'
  scenariosDir(repoRoot: string): string;
  emitter: TemplateEmitter;
}
export const TEMPLATE_REGISTRY: readonly TemplateBinding[] = [
  { name: 'EdgeLifecycle', outputDir: 'edges', scenariosDir: r => getTemplateScenariosDir(r, 'EdgeLifecycle'), emitter: edgeLifecycleEmitter },
  { name: 'EntityLifecycle', outputDir: 'entities', scenariosDir: r => getTemplateScenariosDir(r, 'EntityLifecycle'), emitter: entityLifecycleEmitter },
  { name: 'UpdatedFieldVisibleOnReadBack', outputDir: 'runtime-entities', scenariosDir: r => getTemplateScenariosDir(r, 'UpdatedFieldVisibleOnReadBack'), emitter: runtimeEntityEmitter },
  { name: 'StateTransitionVisibleAfterAction', outputDir: 'state-transitions', scenariosDir: r => getTemplateScenariosDir(r, 'StateTransitionVisibleAfterAction'), emitter: stateTransitionEmitter },
];

Then:

  • The emission loop iterates TEMPLATE_REGISTRY.
  • buildCoverage() takes TEMPLATE_REGISTRY directly — the templateOutputDirs argument disappears.
  • Adding a template is one entry, one place.

Step 2 (optional, larger) — derive the registry from the ontology

Read configs/<config>/ontology/scenario-templates.json and look up each template's emitter by name from a small materializer/src/templateEmitters/registry.ts:

export const TEMPLATE_EMITTERS: Record<string, { outputDir: string; emitter: TemplateEmitter }> = {
  EdgeLifecycle: { outputDir: 'edges', emitter: edgeLifecycleEmitter },
  // ...
};

Then adding a template is:

  • An ontology edit (one JSON block).
  • A new emitter file (only if the template shape needs new code) + one entry in TEMPLATE_EMITTERS.

The orchestrator stays untouched.

Out of scope

  • Removing the per-template emitter implementations themselves (edgeLifecycleEmitter, etc.). Those genuinely encode template-shape-specific Playwright rendering.
  • Changing how coverage.json is consumed by L3 invariants.

Acceptance

  • materializer/src/index.ts no longer references any template name.
  • Adding a { name, outputDir, emitter } triple to one registry file is sufficient to wire a new template into both emission and coverage.
  • All existing L2 / L3 invariants pass unchanged.
  • No behaviour change in generated output (byte-identical against the current camunda-oca config).

Refs

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions