ADR 0006. Notation plugins: capability slots, a reference meta-model, and the structured doc shape
- Status: accepted (2026-08-20)
- Context: synthesized from three fully drafted alternatives (evolutionary slot extension, versioned plugin SDK, meta-model-first) that were adversarially judged against the codebase; every load-bearing claim below was verified against the code
- Related: 0003 (hexagonal backends, CI-enforced boundaries: extended, not changed), 0004 (the published
bpmiq-validatebinary constrains where checkers may live), 0005 (the /mcp surface this ADR generalizes per notation) - Tracking: epic #118, steps #119–#126
Context
bpmiq is growing from a BPM platform into a general architecture tool: one plugin per notation: BPMN, DMN, Markdown, Wardley Maps, Team Topologies, Event Storming, Domain Storytelling, and notations we have not named yet.
@bpmiq/notations is already declared as "the ONE place that knows what a modeling notation is", and wardley/team-topology/value-chain descriptors exist. But today a descriptor buys only file identity and a Monaco language. Everything behavioral is hard-coded to BPMN/DMN:
- Discovery knows exactly two shapes:
discoverProcesses(.bpmn) anddiscoverDecisions(.dmn);bpmiq.yml's schema is{ processes: string }. The three non-BPMN extractors inextract.tsare therefore dead code: no discovery path ever reaches an.owm/.tt/.vc.jsonfile, and a syntactically broken.ttpassespnpm validateuntouched. derive.tsis two hand-written functions, not a mechanism; every caller gates ongraph.notation === "bpmn" | "dmn".- The web client mounts editors via
isBpmn/isDmnbranches; the Analyse-with-AI contract carries a closed"bpmn" | "dmn"union. - Link integrity (callActivity→subprocess, decisionRef→decision) lives as bespoke
Set<string>parameters inside the validator; impact analysis re-implements traversal per feature. - The live
/mcpregisters 19 individual hand-written tools; the generic graph tools inpackages/mcppinsequenceFlow/startEvent. - The live contract is one Y.Text per file, and the minimal-diff writer computes a single contiguous edit region per save, correct for text notations, structurally wrong for boards (below).
Two of the target notations (Event Storming, Domain Storytelling) are canvas notations whose defining scenario is many simultaneous writers.
Decision
The descriptor becomes the capability contract; registration stays static.
NotationDescriptorgains optional, pure, isomorphic slots:noun,docShape(replacing the deadprocessModelflag),template,extract(absorbing the module-privateEXTRACTORSmap),derive(absorbing thederiveProcess/deriveDecisiondispatch behind a genericDerivedViewwhose rich payloads live indetail),refs, andgraphHints(unpinning the BPMN vocabulary in generic graph analyses). A descriptor with no slots still gets everything generic: live room, Monaco, discovery, listing, release, history. Notations are compiled in: adding one is a PR, not a runtime load.One boundary rule, CI-enforced. The light descriptor data stays in
packages/notations/index.ts, browser-safe and zero-dep; thenotations-index-and-derive-stay-browser-safedependency-cruiser rule survives verbatim. Behavior slots attach in Node-safe capability modules composed at the composition roots (live-hostserver.ts, validator CLI, web bootstrap), never as function values on the eagerly exported array: two of the three drafted designs failed exactly here (fast-xml-parserin the web bundle,pnpm archred). Checkers stay inpackages/validatorbehind onecheckModeldispatch shared by the validator CLI and live-hostlintModel, protecting the publishedbpmiq-validatebinary, its near-zero-dep budget, and the never-executes-content-repo-code invariant. Nothing yjs-typed enterspackages/notations.A cross-notation reference meta-model is the linking substrate. Typed
Reference { from, rel, to, strength: required | informative }emitted by arefsslot; a repo-wideRepoIndexwith incoming/outgoing traversal; one generic dangling-ref rule replacing the bespoke id-set plumbing behavior-identically. This single mechanism powers link validation,which_models_use, backlink navigation, and a notation-agnostic reference-diff section in release PRs. It is what turns seven notations into one architecture tool.ModelGraphgains additiveparent(containment),layout, andedge.orderchannels; it remains the floor for generic analyses, not the validation substrate (checkers keep parsing raw content with their rich models).Two doc shapes; boards never ride Y.Text.
docShape: "text"(today's contract, byte-identical: BPMN, DMN, Markdown, Wardley OWM, Team Topologies JSON) anddocShape: "structured": a Y.Map-of-Y.Maps element store plus a per-notation deterministic codec to canonical line-per-element text as the at-rest format, so git diff/PR review, sha256 compare-and-set, per-file history, validator input, and VS Code bytes all survive unchanged. Agent/REST writes reconcile element-wise; ordering uses fractional indices. The Y.Text route was rejected for boards on verified mechanics: the minimal-diff writer's single contiguous region makes any multi-element save conflict with every concurrent remote edit inside the span: last-writer-wins precisely in the workshop scenario the notations exist for. The structured contract ships dark (test-only descriptor through seed→edit→persist→history→release in CI) before the first board notation uses it.Editors are external modeler libraries behind a lazy browser registry. A browser-only
WebNotationPluginregistry (mountEditor,panels,historyDiff,assistTool; absent editor ⇒ collaborative Monaco) replaces theisBpmn/isDmnbranches; bpmn-js/dmn-js become the first two plugins. Visual editors for further notations come from standalone Miragon modeler repos sharing one proven template (DOM-free schema-model with deterministic serialization, diagram-js renderer with anadditionalModuleshook, thin webapp/vscode shells):wardley-maps-modelerandteam-topologies-modeler(whose formats already match the registry) and the Event Storming modeler built in the same manner (#116). Sync reuses their battle-tested webview recipe (echo suppression, serialized import chain, viewport preservation, last-good-diagram on invalid text) on top of the minimal-diff writer.Third-party runtime plugin loading is deferred, with a named trigger. No versioned plugin-API semver gating, no boot-time npm loading, no browser
import()of foreign editor bundles, no worker sandboxing, for now. The browser path has an unsolved correctness hazard (a runtime-loaded bundle carries its own yjs; yjs is single-instance by design, so a foreign editor creating Y types against host docs corrupts them), and the server path would trade the validator's compiled-in never-executes-content guarantee for machinery with zero current consumers. Trigger to revisit: a concrete external team wanting to ship a notation without a PR against this repo. Until then, the manifest idea survives only as cheap introspection: capabilities are listable without loading heavy code.
Consequences
- Migration is eight independently shippable steps (epic #118): hygiene incl. the compound-extension stem fix (#119) → registry-driven discovery + generalized
bpmiq.ymlwith per-mediaKind baseline validation (#120) → capability slots +checkModel(#121) → reference meta-model +Finding.ruleId(#122) → MCP factories + registry-driven tools (#123) → web plugin registry (#124) → mount the two existing modelers (#125) → structured doc shape dark launch (#126). 1→2→3 are sequential; 4/5/6 parallelize after 3; 7 needs 2+6; 8 gates the Event Storming integration. - Wire and tool compatibility is preserved by aliasing, never breaking:
processes:stays valid inbpmiq.yml, existing MCP tool names/schemas are reproduced by the noun mechanism, existing link warnings keep their texts. @bpmiq/decisionsis untouched and becomes the template for future per-notation semantics modules (variables/analyze/simulate/tests); the.tests.yamlsidecar framework is NOT generalized until a second executable-semantics notation exists.- Carve-out discipline: when BPMN/DMN code eventually moves into per-notation packages, the shared view types (
DerivedDecisionet al.) must be extracted into a types-only module first:@bpmiq/decisionsimportsnotations/derive, so a naive carve-out creates a workspace package cycle. Theone-bpmn-readerandbpmn-kinds-has-zero-importsrules survive until that day because no BPMN code moves packages in steps 1–8. - Costs accepted: the descriptor/capability split adds one indirection (composition-root wiring) per notation; the structured shape adds a second persistence branch in
collab.ts(bounded by the codec contract and property-tested round-trips); per-notation editor packages pin exact versions of external libs. - Rejected alternatives: a full plugin SDK now (speculative surface for zero consumers, the yjs single-instance flaw, biggest file-move churn; see Decision 6); boards on Y.Text with validity-gated imports (verified conflict mechanics, degrades to silent lost edits; see Decision 4); centralizing checkers and yjs-typed codecs inside
packages/notations(drags the rule corpus and a yjs runtime dep into the browser-reachable core and the published validator's graph; see Decision 2);ModelGraphas the universal validation substrate (too lossy; the BPMN checker needs its rich reader; see Decision 3).