ADR 0004: Target plugin API
Status: Accepted (P0)
Context
Section titled “Context”Studio must ship Core/Angular/React targets at 0.1 and add React-dynamic and Java later, without the canvas, command engine, or diagnostics ever knowing those targets exist. If a target were hardcoded into the editor, adding Java would mean forking or branching the canvas — which R5/R12 explicitly forbid.
Decision
Section titled “Decision”- A target is
StudioTarget<T>(plan section 10):id,displayName,version,capabilities,defaults(),analyze(project, options),generate(project, options). It receivesMdyStudioProject(or itsContractderivation) and returns anArtifact(targetId,files[]withpath/language/content/role,diagnostics, optionalentryFile). - Targets are discovered through a
TargetManifest { id, displayName, load(): Promise<StudioTarget> }registry. The editor holds manifests, not target implementations, until a target is actually invoked —load()is called lazily (R5). No target package is imported eagerly by editor code. - Generation pipeline is fixed and shared: normalize → resolve IDs to paths →
capability analysis → target-specific IR → AST/structured writer → print →
format → syntax check → optional compile →
Artifact. Complex source is never produced by ad hoc string templates (R10); a shared TS IR factory coverscreateForm/mdyForm/useMdyForm(plan section 10). - Explicit forbidden dependency edges (plan section 4), restated here as the
authoritative list for this ADR’s scope:
model -> DOM/React/Angular/Astro/target— forbidden.- runtime package (
packages/core, adapters, etc.)-> Studio— forbidden in either direction of authority: runtime never imports Studio (R12). target -> Studio UI— forbidden; a target only depends onstudio-model/studio-codegen/studio-target-core, never onstudio-ui-reactorapps/studio.
- Target incompatibility with a given model shape is a diagnostic the target
reports via
analyze, not a canonical-model constraint (ADR 0003) — the canvas stays valid even if the currently-selected target can’t yet support every feature used. - Every target must pass the conformance suite (plan section 10): deterministic, no project mutation, safe paths, stable diagnostics, unsupported-feature reporting, stale/cancel handling, fixture compiles.
Consequences
Section titled “Consequences”- Adding Java (P14) means adding one more
TargetManifestentry and onepackages/studio-target-javapackage; zero edits tostudio-model/studio-editor/studio-ui-react. - Generation must run off the main thread (workers, plan section 11) with a generation ID so a stale in-flight generation from a since-changed project is discarded — this is a target-plugin-API concern (result identity), not a UI concern.
- A target that needs a model feature that doesn’t exist yet is a model change proposed through ADR 0001/0002, not a special case bolted onto the target API.
Verification
Section titled “Verification”npm run test:studio— each target’s conformance fixture, including the stub signatures everyImplementationRefinmode: "stub"obliges it to emit.scripts/audit-package-independence.mjs— no target package is imported eagerly by editor code, and the forbidden dependency edges are enforced against the real import graph rather than asserted in prose.
Security and privacy
Section titled “Security and privacy”Generated code is written through a structured IR and printed, never assembled from ad hoc string templates. That is a correctness decision first and an injection decision second: a project-supplied label, name or pattern reaching a template by concatenation is how attacker-authored content becomes attacker-authored source in someone’s build. Targets load lazily, so untrusted target code is not executed until a user chooses that target.
Rejection-test answers
Section titled “Rejection-test answers”- Java addable without canvas model change? Yes — this ADR’s entire
point is that
StudioTarget<T>/Artifact/manifest registry are the only surface a target touches; nothing here is JS-shaped (files[]content is opaque text,languageis just a string like"java"). - Target loads lazily, no hardcoded UI import? Yes:
TargetManifest.load()is the only load path; the editor never statically imports a target package. - Rename/move preserve all references? N/A to this ADR directly — see ADR 0002 (targets consume the already-resolved model/Contract, so they never see raw path drift).
- Same normalized project → byte-identical output? Yes:
generateis specified as deterministic and non-mutating per the conformance suite; given../checkout-example.mdunchanged, any target run twice produces identicalArtifact.files[].content.
Satisfies
Section titled “Satisfies”R5, R10, R11 (no eval/remote plugins — load() is a static import
resolved by the manifest, not dynamic code fetched at runtime), R12.