camunda / camunda/api-test-generator
Design: collapse the per-emitter build-script explosion behind the emitter registry
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 3
- Avg merge
- 13h 41m
- Merged PRs (30d)
- 23
Description
Design: collapse the per-emitter build-script explosion behind the emitter registry
Problem
The EmitterStrategy registry encapsulates the transform (scaffold/emit,
resolved via getEmitter/listEmitters). But the lifecycle around each
transform — fetch op-map → codegen → validate output → CI toolchain — is
hand-replicated in package.json scripts and test/CI config, once per target.
Adding an emitter therefore is not "register a strategy"; it's "register a
strategy and edit a half-dozen unrelated files":
| Lifecycle phase | Per-emitter surface today | Where it leaks |
|---|---|---|
| fetch op-map | fetch-js-sdk-map, fetch-python-sdk-map, … |
pipeline chain hardcodes each (package.json:45) |
| codegen | codegen:js-sdk:all, codegen:python-sdk:all, codegen:csharp-sdk:all |
testsuite:generate chain hardcodes each (package.json:40) |
| validate output | hardcoded SUITES[] array |
tests/regression/generated-suites-typecheck.test.ts (SDK suites not even listed) |
| CI toolchain | per-language steps | .github/workflows/ci.yml grows per target |
The framework advertises emitters as pluggable; the build system contradicts it.
This is the same drift vector AGENTS.md already calls out (the #286/#288
"parallel implementations silently diverge" rule): every hand-copied
codegen:<lang> / fetch-<lang>-map line is a place where one target quietly
gains or loses a step relative to its siblings (exactly what already happened —
the SDK suites are wired into testsuite:generate but have no validation
gate, so a non-compiling suite passes CI silently; see #354/#355).
What is and isn't leaking
The leak is operational/lifecycle metadata, not the pure transform. The
transform is correctly encapsulated and correctly pure (EmitterStrategy.emit
is contractually "no filesystem, no network, no global state",
emitter-sdk/src/types.ts:146-148). The fix must preserve that purity:
emitters should declare their lifecycle as data; the (already-impure)
orchestrator/test layer runs it. We do not want to run tsc/dotnet
inside emit().
There is already a precedent for declarative emitter wiring in this repo:
per-config configs/<name>/codegen/emitters.json (loadEnabledEmitters,
materializer/src/index.ts) lets a config declare which emitters it authorises.
This proposal extends the same "declare, don't hand-wire" philosophy to the
build lifecycle.
Proposed design
1. Declarative lifecycle metadata on EmitterStrategy
interface EmitterStrategy {
// ... existing id/name/supportedConfigs/scaffold/emit ...
/** Upstream op-map source. Replaces the per-target fetch-<lang>-map scripts. */
readonly sdkMap?: {
repo: string; // e.g. 'camunda/orchestration-cluster-api-js'
path: string; // e.g. 'examples/operation-map.json'
refEnv?: string; // e.g. 'JS_SDK_REF' (defaults documented)
out: string; // e.g. 'spec/js-sdk/operation-map.json'
};
/** How to typecheck/compile the emitted suite. Drives the validation gate. */
readonly validate?:
| { kind: 'tsc'; projectRelPath: string } // tsconfig.json under outDir
| { kind: 'dotnet'; projectRelPath: string } // .csproj / .sln
| { kind: 'pyright'; configRelPath?: string }
| { kind: 'command'; argv: string[] } // escape hatch
| { kind: 'none' }; // explicit opt-out
}
Emitters declare these; nothing in the strategy executes them.
2. One generic driver, registry-iterated
- codegen: add
--all-targetstomaterializer/src/index.ts, iterating
listEmitters()(intersected with the config'semitters.json). The three
codegen:*-sdk:allscripts collapse into one.testsuite:generatestops
hardcoding a per-target chain. - fetch-map: a single
fetch-mapsstep iterates registeredsdkMap
declarations (the per-languagefetch-*-mapscripts become one generic
fetcher parameterised by{repo, path, refEnv, out}—fetch-js-sdk-map.js
is already exactly this shape, just hardcoded). - validate:
generated-suites-typecheck.test.tsiterates the registry's
validatedescriptors instead of a hardcodedSUITES[]. A new emitter needs
zero test edits to get a compile gate.
3. Cross-process seam: list-targets --json
npm/CI cannot read an in-process JS registry. Add a
tsx materializer/src/index.ts list-targets --json command that prints
[{ id, name, sdkMap, validate }]. CI matrices and any shell glue consume
that projection, keeping the registry the single source of truth and the build
system a thin reader. (This also lets CI gate toolchain availability —
e.g. only run the dotnet job when some target declares validate.kind === 'dotnet'.)
Non-goals
- Do not move compilation into
emit(); the purity contract stays. - Do not build a plugin-discovery/auto-
importmechanism in this iteration;
emitters are stillregisterEmitter'd explicitly inindex.ts. The win is
eliminating the script/test/CI duplication, not the one-line registration.
Tradeoffs / open questions
- Escape hatch. Genuinely bespoke targets use
validate: { kind: 'command', argv }
or{ kind: 'none' }with a justification. Avoid over-abstracting rare cases. - CI toolchain availability.
.NET/pyrightmust be installed in CI when a
target declares them; driven by thelist-targetsprojection rather than
hand-wired steps. - Contract change is ask-first. This touches the experimental
EmitterStrategycontract (AGENTS.md "Ask first: adding/changing an emitter
target"). Land the contract addition as its own reviewed change. - SDK-map determinism.
fetch-js-sdk-map.jsalready notes a future pin file
"analogous to spec-pin.json". A declaredsdkMapis the natural home for a
pinned ref/hash; out of scope here but worth keeping the shape pin-friendly.
Suggested sequencing (lowest-risk first)
- Collapse codegen into a registry-iterated
--all-targetsrun. Pure
refactor of existing scripts, no contract change — immediate win. - Add
list-targets --json+ move op-map fetch onto a declaredsdkMap
consumed by one generic fetch step. - Make the typecheck gate registry-driven via
validatedescriptors. Lands
naturally with #354 (Python) and #355 (JS), where the SDK suites first become
compilable, plus adotnetgate for the already-complete C# suite.
Relationship to other issues
- #354 / #355 — the SDK suites' missing compile gate is a symptom of phase 3
above; the registry-drivenvalidatedescriptor is where their typecheck
gates should land. - #286 / #288 — this is the same parallel-implementation-drift failure mode,
applied to the build lifecycle rather than to a pipeline stage.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with package.json, materializer/src/index.ts, tests/regression/generated-suites-typecheck.test.ts, and .github/workflows/ci.yml; read AGENTS.md before changing the EmitterStrategy contract. Trace the existing per-target codegen, fetch, validation, and CI paths, then confirm the registry-driven lifecycle and list-targets projection cover the declared targets without moving work into emit().
Written by the indexing model from the issue text.
Assessment
- Tech stack
- github-actions, node.js, typescript
- Domain
- build-system, ci-cd, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100