From 73471a725638188b83195ac37f72c469c359dcaa Mon Sep 17 00:00:00 2001 From: Marco Schaeck Date: Tue, 27 Oct 2026 16:31:37 +0100 Subject: [PATCH] docs: remove exploration leftovers (handover, audit, glossary) --- CONTRIBUTING.md | 3 +- GLOSSARY.md | 167 - HANDOVER.md | 5517 ----------------- docs/.vitepress/config.ts | 2 +- docs/audit-2026-10.md | 1683 ----- docs/testing.md | 9 +- test/domain/diagram/incremental-audit.test.ts | 2 +- test/e2e/fuzz.test.ts | 3 +- 8 files changed, 6 insertions(+), 7380 deletions(-) delete mode 100644 GLOSSARY.md delete mode 100644 HANDOVER.md delete mode 100644 docs/audit-2026-10.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb4e54f7..e35bea01 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,8 +86,7 @@ them on a private corpus without copying it into the repository, are described i functions live in the one `utils/` folder of their module (`domain/document/utils/moddle.ts`, `domain/document/utils/fuzzy.ts`); a utils file holds no class and imports only other utils files and error classes. Outside the entries (`api.ts`, `index.ts`, `node.ts`, which keep the - package's public functions and delegate to the classes) there are no free functions. The - domain terms the classes are named after are in [GLOSSARY.md](GLOSSARY.md). + package's public functions and delegate to the classes) there are no free functions. - `test/`: the vitest suite. Its folders mirror the module folders of `src/` (`test/domain/batch/`, `test/adapters/in/cli/`, ...): a test sits with the module whose interface it exercises. Black-box tests through the entries and the CLI are in `test/e2e/`, diff --git a/GLOSSARY.md b/GLOSSARY.md deleted file mode 100644 index 09fbd483..00000000 --- a/GLOSSARY.md +++ /dev/null @@ -1,167 +0,0 @@ -# Glossary - -The domain terms of bpmn-cli, as the code names them. A class is named after one of these terms; -a new concept gets its line here first. - -| term | meaning | -| --- | --- | -| **Doc** | One loaded BPMN document: the moddle tree, its id index and the lookups over it (`domain/document/Doc.ts`). | -| **Edit command** | One change a caller asks for, named after what it does: a model command (`AddElement`, `ConnectElements`, `SetProperties`, `RemoveElements`, `ChangeElementType`, `MoveElements`, `ReorderElements`, `EditExtensionElements`, `SplitIntoBranches`) or a diagram command (`PlaceShapes`, `AlignShapes`, `ColorElements`, `PlaceLabel`, `RerouteFlow`, `InsertSpace`, `TidyShapes`, `CompactDiagram`); the ops JSON of `apply` spells them as ops (`"op": "add"`) (`domain/commands/types.ts`). | -| **Use case** | The contract of one command line verb or API function: an interface with its `Command` and `Result` in `application/port/in/` (`EditDocumentUseCase`, `CreateDocumentUseCase`, `RelayoutDiagramUseCase`, `ValidateDocumentUseCase`, `ReadDocumentUseCase`, `ShowModelUseCase`, `ShowConventionsUseCase`, `FindElementsUseCase`, `RenderDiagramUseCase`, `RenderSvgUseCase`, `RunCommandsUseCase`, `AnalyzeLayoutUseCase`), implemented by the service of the same name in `application/service/` (`EditDocumentService`, ...). Every public function of the library that works on a document goes through one, even where the service only forwards to the domain. | -| **Service** | A class named `...Service`: in the domain, work the commands share or run (`ElementCreationService`, `SequenceFlowService`, ...); in `application/service/`, the implementation of one use case. | -| **Port** / **adapter** | The way in (`application/port/in/`, the use cases) and out (`application/port/out/`: `LoadDocumentPort`, `SaveDocumentPort`, `LoadProjectSettingsPort`, `WriteImagePort`) / what drives the application (`adapters/in/cli/`) or provides an outbound port (`adapters/out/files/` on disk, `adapters/out/memory/` for the in-memory API, which has no project settings and passes no document ports); only the entries wire them together. | -| **Batch** | The commands of one `apply` call (or of one changing command), run on the document in memory in their order: a diagram command runs after the layout of the model commands before it. Guard, commands, validation, layout, validators, result, all or nothing (`domain/batch/EditBatchService.ts`); the use case then saves the result through `SaveDocumentPort`. | -| **Edit context** | The state of one edit: the call's settings (id conventions, platform, kept flow ids) and what the running batch recorded (`domain/document/EditContext.ts`). The editing services receive it next to the document; `new EditContext(doc)` starts a fresh one. | -| **Model command service** | Hands each model command of a batch to its domain service in an edit context; a batch runs in segments, the drawing brought up to date between them (`domain/batch/ModelCommandService.ts`). | -| **Diagram command service** | Hands each diagram command to its domain service on a document's drawing (`domain/diagram/DiagramCommandService.ts`). | -| **Drawn diagram** | The drawing the diagram commands change: its planes, one drawing per plane, the refusals for what is not drawn (`domain/diagram/DrawnDiagram.ts`). | -| **Batch drawing** | What a batch does to the drawing, the one way the batch reaches it: the layout mode, the layouts between its commands, the diagram commands, the stickies (`domain/diagram/BatchDrawing.ts`). | -| **Layout request** | A sub-process an op asked to lay out expanded or collapsed, kept with the document until a layout reads it (`domain/document/LayoutRequests.ts`). | -| **Relayout diagram** | `bpmn layout` of a document: a full redraw (expanding / collapsing sub-processes) or a tidy (`application/service/RelayoutDiagramService.ts`). | -| **Validate document** | `bpmn validate` of a document: validation, the validators and a layout dry run (`application/service/ValidateDocumentService.ts`). | -| **Reorder elements** | An `order` command on a document: the outgoing flows of a node, the lanes of a pool or lane, or the pools of a collaboration (`domain/editing/ElementOrderService.ts`). | -| **Reorder target** | What an `order` command reorders; lanes and pools are bands the drawing follows (`domain/commands/ReorderTarget.ts`). | -| **Id suggestions** | The ids of a document a mistyped id may have meant: the batch rename, another spelling, a typo, a matching name (`domain/document/IdSuggestions.ts`). | -| **Alias** | A `$name` an op gives to what it creates, so later ops of the batch can name it. | -| **Op aliases** | Where one op defines aliases and where it may name one, read from its edit command definition, and the op with its aliases resolved (`domain/commands/OpAliases.ts`). | -| **Alias table** | The aliases of a running batch: each bound to its element, so it follows renames and retypes (`domain/commands/AliasTable.ts`). | -| **Field spec** | Type, description and rules of one key of an op in the ops JSON; one table per op in its edit command definition, the shared ones (placement, flow and trigger options, split nodes and branches, selectors) in `domain/commands/utils/op-fields.ts`. | -| **Edit command definition** | Everything the ops JSON knows of one edit command: its name, description, field specs, the keys that hold element ids, its checks across keys and its schema rules; one class per command (`domain/commands/definitions/`, e.g. `SplitIntoBranchesDefinition.ts`), listed by `EditCommandDefinitions`. | -| **Command field check** | The checks across the keys of one parsed command (placement, flow options, kind and trigger, selectors) the edit command definitions call (`domain/commands/CommandFieldCheck.ts`). | -| **Command parser** | Checks the ops JSON of `apply` against the field specs and checks of each edit command definition and the batch aliases and returns typed ops (`domain/commands/CommandParser.ts`). | -| **Ops schema** | The JSON Schema of the ops JSON, built from the field specs and schema rules of the edit command definitions (`domain/commands/OpsSchemaBuilder.ts`). | -| **Change set** | What one op created, changed and removed, and the warnings it raised (`domain/document/ChangeSet.ts`). | -| **Pool service** | Creates a participant (pool): wraps the one process, binds one, creates a new or a black-box one (`domain/editing/participants/PoolService.ts`). | -| **Lane service** | Creates lanes (nested ones inherit the parent's members) and assigns flow nodes to a lane (`domain/editing/participants/LaneService.ts`). | -| **Artifact service** | Creates data objects, data stores, text annotations and groups, and names a group (`domain/editing/ArtifactService.ts`). | -| **Association service** | Creates and removes associations and data associations (`domain/editing/AssociationService.ts`). | -| **Kind** | The CLI's name for a BPMN element type, with an optional trigger (`userTask`, `boundaryEvent:timer`). | -| **Trigger** | The event definition of an event (message, timer, error, signal, ...). | -| **Decision link** | Which DMN decision a business rule task calls, in the spelling of its engine (`camunda:decisionRef`, `zeebe:calledDecision`, the unprefixed `calledDecision`): read by `DecisionLinkReader` (`domain/document/`), written by `DecisionLinkWriter` (`domain/editing/`). | -| **Flow bridge** | Reconnecting the predecessors of a removed or moved node to its successors: a single node, a join, a moved group (`domain/editing/flows/FlowBridgeService.ts`). | -| **Moved flows** | The flows a `move` takes out at the old place and puts back at the new one, and what becomes of their names, conditions and default markers (`domain/editing/flows/MovedFlows.ts`). | -| **Splice** | Inserting a node into an existing sequence flow (`SequenceFlowService.splice`). | -| **Model commands** | `add`, `connect`, `set`, `remove`, `retype`, `move`, `order`, `ext` and `split` run in `ElementCreationService`, `ConnectionService`, `PropertyService`, `ElementRemovalService`, `ElementTypeChangeService`, `ElementMoveService`, `ElementOrderService`, `ExtensionElementService` and `BranchSplitService` (`domain/editing/`). | -| **Flow rules** | What a sequence flow may connect and carry: the roles of its ends, one scope for both, a condition its source takes (`domain/document/FlowRules.ts`). | -| **Sequence flow service** | Creating, removing, redirecting and splicing a document's sequence flows, their default marker and condition (`domain/editing/flows/SequenceFlowService.ts`). | -| **Flow options** | The name, id, condition, language and default marker of the flow an op creates or splices a node into (`domain/editing/flows/FlowOptions.ts`). | -| **Node placement service** | Where a node goes (--after / --before / --flow / --in / --on) and the append, prepend, splice or attachment that puts it there (`domain/editing/NodePlacementService.ts`). | -| **Lane inheritance** | The lane a node placed in a process without --lane gets (`domain/editing/participants/LaneInheritance.ts`). | -| **Event-gateway rule** | Which targets an event-based gateway may lead to: the Camunda 7 engines' rule or BPMN 2.0's, by the file (`domain/editing/events/EventGatewayRule.ts`). | -| **Flow id follower** | A flow whose id names its ends takes the id of its new ends when they change (`domain/editing/flows/FlowIdFollower.ts`). | -| **Stale flow report** | W_FLOW_ID_STALE for the flows that kept an id naming their old ends (`domain/editing/flows/StaleFlowReport.ts`). | -| **Event trigger** | An event's trigger as the model has it: as options, as details, as a short text (`domain/editing/events/EventTrigger.ts`). | -| **Trigger change** | A change of an element's trigger or kind: merged options, a dropped non-interrupting flag, the message-flow check, the id state (`domain/editing/events/TriggerChange.ts`). | -| **Event definition service** | The event definitions of one event, updated in place or replaced when its trigger changes (`domain/editing/events/EventDefinitionService.ts`). | -| **Root element service** | The root Message, Error, Signal and Escalation elements events, tasks and message flows reference (`domain/editing/events/RootElementService.ts`). | -| **Property coercer** | The keys `set` takes and the values it writes: settable BPMN attributes, typed values, vendor attributes (`domain/editing/PropertyCoercer.ts`). | -| **Set key catalog** | Every documented `set` key in one definition: its description, where it applies, how `show ` reads it back and how `set` writes it (`domain/editing/SetKeyCatalog.ts`). | -| **Nested slots** | The `definition.` / `loop.` / `condition.` keys of the nested elements without an id of their own (`domain/editing/NestedSlots.ts`). | -| **Element properties** | The settable view of an element's properties that `show ` prints: the set key catalog's reads around the generic and vendor attributes (`domain/editing/ElementProperties.ts`). | -| **Id allocator** | The ids one edit gives out (new, explicit, with a collision suffix) and what it recorded about them (`domain/document/IdAllocator.ts`). | -| **Id rename service** | Renaming an id with every reference, and the ids a retype or a trigger change leaves a stale prefix (`domain/editing/IdRenameService.ts`). | -| **Plane** | The drawing of one BPMN diagram: its shapes, edges and labels (`domain/diagram/di/types.ts`; read by `domain/diagram/di/DiagramReader.ts`, written by `domain/diagram/di/PlaneWriter.ts`). | -| **Plane query** | Read-only queries on a plane: its leaf shapes, the content of a sub-process, boundary events, the spacing it is drawn with (`domain/diagram/di/PlaneQuery.ts`). | -| **Semantics** | The index of the semantic model a drawing is read against: elements by id, lanes, pools, scopes, sequence flows (`domain/diagram/di/Semantics.ts`). | -| **DI pruner** | Deletes DI whose element is gone, and the edges of flows whose ends changed (`domain/diagram/di/DiPruner.ts`). | -| **Checkpoint** | A saved state of a plane or of chosen shapes and edges (bounds, labels, waypoints, ends), to try a change and undo it (`domain/diagram/di/DrawingCheckpoint.ts`). | -| **Groups** | The `bpmn:Group` boxes of a document: remembered before a full redraw and drawn again around their members (`domain/diagram/di/Groups.ts`). | -| **Sticky** | A design-iq sticky note (`bpmiq:sticky`) that follows the flow node nearest to it (`domain/diagram/di/Stickies.ts`). | -| **Drawn element ids** | Ids given to the elements a full redraw draws that have none, in the file's id style (`domain/diagram/di/DrawnElementIds.ts`). | -| **DI ids** | The ids of DI elements in the file's style: new ones (`domain/diagram/di/DiIds.ts`) and the old ones kept over a full redraw (`domain/diagram/di/DiIdMemo.ts`). | -| **Drawing** | One plane of a kept drawing being edited, with what the edit touched: space tool, routing, labels, separation and frames behind one seam (`domain/diagram/layout/incremental/Drawing.ts`). | -| **Frame** | A pool, lane or expanded sub-process: a shape that holds other shapes (`domain/diagram/di/Frames.ts` finds a shape's frames and their interior; `domain/diagram/layout/incremental/FrameEditor.ts` draws, removes, collapses and expands them). | -| **Space tool** | The modeler's tool that moves everything beyond a line to make or close room, growing the frames it crosses (`domain/diagram/layout/incremental/SpaceTool.ts`). | -| **Separation** | Local overlap removal: the shapes that conflict with new or moved ones give way (`domain/diagram/layout/incremental/Separation.ts`). | -| **Edge router** | Routes and repairs single connections of a kept drawing with the BPMN side conventions (`domain/diagram/layout/incremental/EdgeRouter.ts`). | -| **Orthogonal router** | Finds the cheapest orthogonal route of one connection between two boxes: A* over a visibility grid (`domain/diagram/layout/incremental/OrthogonalRouter.ts`, `domain/diagram/layout/incremental/RouteGrid.ts`, `domain/diagram/layout/incremental/SearchHeap.ts`). | -| **Label placer** | Puts external labels on the first free spot around their shape or flow (`domain/diagram/layout/incremental/LabelPlacer.ts`). | -| **Trial** | A change that is measured and kept only when it makes the result no worse, else undone: of a kept drawing (`domain/diagram/di/TrialGuard.ts`), or of an arrangement of the clean engine, kept only when the routes then draw better (`domain/diagram/layout/clean/ArrangementTrial.ts`). | -| **Drawing snapshot** | What the planes showed before a format op: bounds, waypoints, labels, connection states (`domain/diagram/layout/incremental/DrawingSnapshot.ts`). | -| **Layout analysis** | The measurement of a drawing's layout problems, one plane analysis per BPMNPlane (`domain/diagram/di/LayoutAnalysis.ts`, `domain/diagram/di/PlaneAnalysis.ts`; kinds and weights in `domain/diagram/utils/problems.ts`). | -| **Problem locator** | Finds where a layout problem shows on its plane, for the numbered badges of `render --problems` (`domain/diagram/di/ProblemLocator.ts`). | -| **Node placer** | Gives new and relocated elements a shape in a kept drawing, next to their neighbours, making room where needed (`domain/diagram/layout/incremental/NodePlacer.ts`). | -| **Diagram commands** | `place` and `space` run in `ShapePlacementService` and `SpaceInsertionService` (`domain/diagram/format/`), `align`, `color`, `label`, `route`, `tidy` and `compact` in the drawn diagram itself (`alignShapes`, `colorElements`, `placeLabel`, `rerouteFlow`, `removeOverlaps`, `compactDiagram`); the lane bands of `order` in the `LaneBandService` (`domain/diagram/format/LaneBandService.ts`). | -| **Room service** | Keeps the shapes a format op moved on request where they are and lets the others give way, refusing a move out of its frame (`domain/diagram/format/RoomService.ts`). | -| **Room hint** | The space-tool run an `E_NO_ROOM` refusal of `place` / `align` names, found by a dry run (`domain/diagram/format/RoomHint.ts`). | -| **Compaction service** | Closing the empty rows and columns of a drawing and shrinking its frames to their content (`domain/diagram/format/CompactionService.ts`). | -| **Layout mode** | How a write updates the drawing: `full` redraws, `incremental` keeps a hand-made drawing and places only what changed. | -| **Engine** | A layout engine: the clean engine redraws, the incremental engine keeps a drawing. | -| **Snapshot** | What every DI element showed before the ops of a write, with the semantic facts the incremental layout's diff needs (`domain/diagram/layout/incremental/Snapshot.ts`). | -| **Engine-owned** | A drawing that equals what the engine draws for the same model, so `auto` redraws it in full (`domain/diagram/layout/DiagramGeometry.ts`). | -| **Engine drawing** | What the clean engine draws for the document; the incremental layout copies blocks of it for new expanded sub-processes and pools (`domain/diagram/layout/EngineDrawing.ts`). | -| **Crossing repair** | The incremental layout's last step: crossings between new and kept lines are tried away by routing again or a branch's next row, each trial measured (`domain/diagram/layout/incremental/CrossingRepair.ts`). | -| **Full layout** | A redraw of the whole model by an engine that keeps the DI ids, the colours and the groups (`domain/diagram/layout/FullLayout.ts`). | -| **Temporary lane members** | Laneless flow nodes put into a lane for one bpmn-auto-layout run only (`domain/diagram/layout/TemporaryLaneMembers.ts`). | -| **Association edges** | The association DI bpmn-auto-layout leaves out, drawn after its run (`domain/diagram/layout/AssociationEdges.ts`). | -| **Profile** | The rules of one execution platform (Camunda 7, Camunda 8) that `validate` runs on demand. | -| **Engine profile** | One engine's profile run over a document: the walk, the findings and the rules of Camunda 7 (`C7Profile`) or Camunda 8 (`C8Profile`) on the `EngineProfile` base (`platform/profiles/`). | -| **Platform profiles** | The loaded engine profiles and their loads, runs, baselines and deltas (`domain/validation/profiles/PlatformProfiles.ts`). | -| **Rule kit** | What the on-demand rule chunks use of the core, handed to a profile's constructor instead of imported (`domain/validation/profiles/rule-kit.ts`). | -| **Timer value** | The text of a Camunda 8 timer or date: ISO 8601 duration, date-time, repeating interval or cron expression (`domain/validation/profiles/TimerValue.ts`). | -| **FEEL expression** | The text after the `=` of a Camunda 8 value, checked for JUEL habits and FEEL syntax (`FeelExpression`, parsed by `FeelParser`). | -| **Schema text** | The BPMN schema rules read from a file's text (child order, IDREFs, NCName ids, booleans) that the model cannot see (`SchemaText` over `BpmnSchema`). | -| **Platform detection** | Which engine a file targets, from an explicit choice, `modeler:executionPlatform` or the vendor namespaces (`domain/document/vendor/PlatformDetection.ts`). | -| **Document namespaces** | The xmlns declarations of a document and the URIs its prefixed names resolve to (`domain/document/vendor/DocumentNamespaces.ts`). | -| **Finding** | One problem a validation reports (structural, profile, bpmnlint, a host's validator). | -| **Structural validator** | The structural errors and lint warnings of one document, with the platform profile's findings on request (`domain/validation/StructuralValidator.ts`). | -| **Flow order** | The flow nodes of one scope in flow order from its start events, with the ones no start reaches (`domain/validation/FlowOrder.ts`). | -| **Validator set** | The validators of one call, named, run on one document (`domain/validation/ValidatorSet.ts`). | -| **Baseline** | The findings a document had before an edit, so a write reports only what the edit added (`domain/validation/FindingBaseline.ts`). | -| **Id style** | The way a file spells its ids (prefixes, case, flow form), learned from the file (`domain/document/ids/IdStyle.ts`). | -| **Learned style** | What the ids of one file show by majority: the prefix per kind, the body case, the flow forms (`domain/document/ids/LearnedStyle.ts`). | -| **Tally** | The votes of a file's ids for the keys of one learned rule (`domain/document/ids/Tally.ts`). | -| **Id fitter** | Builds a flow's id from its two ends within the 64 characters of an id, cutting at whole words (`domain/document/ids/IdFitter.ts`). | -| **Element labels** | How ids name an element: as a flow end, and as the anchor of an unnamed element (`domain/document/ids/ElementLabels.ts`). | -| **Id registry** | Every id a document uses (BPMN, DI and vendor ids), so that a new id never collides (`domain/document/ids/IdRegistry.ts`). | -| **Id conventions** | The id rules a project declares in the `element-id-naming` rule of its `.bpmnlintrc`, read by the id conventions reader (`domain/document/ids/IdConventionsReader.ts`). | -| **Declared conventions** | The id conventions as the id style applies them: the usable prefixes, the qualifier, the body case (`domain/document/ids/DeclaredConventions.ts`). | -| **Conventions table** | The conventions new ids follow in one file, a row per id pattern and source (`bpmn show --conventions`, `domain/document/ids/ConventionsTable.ts`). | -| **Vendor extension** | Engine-specific content (`camunda:`, `zeebe:`) on a BPMN element. | -| **Vendor descriptor** | The read-only lookup over an engine's moddle descriptor: its attributes, extension element types, placement and the types an element holds once (`VendorDescriptor`, `CAMUNDA`, `ZEEBE`). | -| **Extension editor** | Runs an `ext` op: resolves its host, then adds (`ExtensionInserter`) or removes (`ExtensionRemover`) vendor extension elements (`domain/editing/extensions/ExtensionElementService.ts`). | -| **Extension structure** | Where vendor extension elements go: containers, single-instance and keyed types, child order (`domain/editing/extensions/ExtensionStructure.ts`). | -| **Extension level** | The top of an element's extensionElements or the children of one vendor element, where `ext` inserts, selects and removes (`domain/editing/extensions/ExtensionLevel.ts`). | -| **Extension selector** | `prefix:type[attr=value]/...`: the path `ext` names vendor elements with (`domain/editing/extensions/SelectorParser.ts`). | -| **Nested host** | An event definition, loop characteristics or condition expression that holds extension elements and is named through its owner (`domain/editing/NestedHosts.ts`). | -| **Vendor elements** | How `ext` reads, names and creates generic vendor elements: canonical type, prefix, attributes, key and label (`domain/editing/VendorElements.ts`). | -| **Extension snippet** | The `--xml` of `ext add`, read synchronously into generic elements (`domain/editing/extensions/ExtensionSnippetReader.ts`). | -| **Zeebe subscription** | The correlation key of a message, written on the bpmn:Message an event or receive task waits for (`domain/editing/events/ZeebeSubscription.ts`). | -| **Camunda 8 defaults** | What the CLI writes for elements it creates in a Camunda 8 file, like Camunda Modeler (`domain/document/vendor/Camunda8Defaults.ts`). | -| **Command catalogue** | The commands of `bpmn` with their arguments, `--help` text and guide usage, and the options of every write (`adapters/in/report/CommandCatalogue.ts`). | -| **Shot** | The image or tiles one render wrote, with size, image tokens, plane and numbered problems: `adapters/out/files/FileImageWriter.ts` writes it, `adapters/in/cli/RenderReport.ts` prints it. | -| **View request** | What `show` (and `viewXml` / `showXml`) is asked to show: the model, one element, its context, a neighbourhood or the drawing, checked like the CLI's flags (`domain/views/ViewRequest.ts`). | -| **Conventions lookup** | The id conventions the `.bpmnlintrc` that applies to a file declares, and why it declares none (`LoadProjectSettingsPort.idConventions`, read by `ShowConventionsService`). | -| **Agent guide** | `bpmn guide`: the cheat sheet for agents, whole, short or by topic (`adapters/in/report/AgentGuide.ts`). | -| **Kinds reference** | `bpmn kinds`: the tables of kinds, triggers, keys, placement, ops and error codes, as text and JSON (`adapters/in/report/KindsReference.ts`). | -| **Scope** | A process or sub-process the clean engine lays out on its own, in a fixed sequence of steps (`domain/diagram/layout/clean/ScopePlacer.ts`). | -| **Grid** | The columns (layers) and rows (bands) of a scope's nodes and where they put them (`domain/diagram/layout/clean/ScopeGrid.ts`, the rows by `RowAssigner.ts`). | -| **Arrangement** | Where the clean engine has put a scope's nodes, boundary events, flows and artifacts at one moment; an `ArrangementCheckpoint` saves it and puts it back. | -| **Join column** | The column under a node that its flows from below rise in on, kept free by moving the node right (`JoinColumnTrial`). | -| **Chain lift** | A node and the nodes after it in its lane moved up onto the row of the flow into it (`ChainLiftTrial`). | -| **Top border** | The upper border of a host, where a boundary event whose flows go up sits when that draws better (`TopBorderTrial`). | -| **Router** | Draws the lines: `EdgeRouter` the sequence flows of a scope, `LinkRouter` its associations, `MessageFlowRouter` the message flows between pools. | -| **DI writer** | Emits the BPMN DI of one plane from the laid-out scopes (`domain/diagram/layout/clean/DiWriter.ts`). | -| **Pool stack** | The pools of a collaboration, top to bottom in the order whose message flows cut the fewest shapes (`domain/diagram/layout/clean/PoolStacker.ts`). | -| **Declutter** | Moving labels that sit on a shape, a label or a line to a free spot (`domain/diagram/layout/clean/LabelDeclutter.ts`). | -| **Note** | A text annotation; the collaboration's are placed by `domain/diagram/layout/clean/NotePlacer.ts`. | -| **Model view** | The read view of a document without coordinates: processes, nodes in flow order, lanes, data, collaboration, problems (`domain/views/ModelViewBuilder.ts`). | -| **Element facts** | What the read views say about one element on its own: trigger, props, vendor values, implementation (`domain/views/ElementFacts.ts`). | -| **Element links** | How an element is tied to the rest of its document: pool, message flows, annotations, data, effective lane (`domain/views/ElementLinks.ts`). | -| **Window** | The neighbourhood `show --around` shows: the flow nodes within `depth` flow steps of an element (`domain/views/AroundWindow.ts`). | -| **Element context** | Where an element is and what surrounds it, as `show --context` prints it (`domain/views/ElementContextBuilder.ts`). | -| **Selector** | `--path`, `--kind` or `--branch`: the elements a format op works on (`domain/diagram/format/ElementSelector.ts`). | -| **Column** | The flow nodes of a diagram clustered by centre x, numbered `c0..` left to right (`domain/views/DiagramColumns.ts`). | -| **Flood** | Three or more warnings of one code (and validator) printed as one line (`adapters/in/report/WarningLines.ts`). | -| **Mutation report** | What a write reports: its changes, its warnings as a delta, the layout and the validation (`adapters/in/report/MutationReportBuilder.ts`; text `MutationText`, `--summary` `MutationSummaryBuilder` / `SummaryText`). | -| **Scene** | The drawable items of one plane a render paints, with their boxes and layers (`domain/views/render/Scene.ts`). | -| **Crop** | The part of a plane a render draws in full: around, path, scope or ids (`domain/views/render/Crop.ts`). | -| **Tile** | One image of a render split at the diagram's columns and rows (`domain/views/render/TileGrid.ts`). | -| **Config resolution** | A `.bpmnlintrc`'s effective rules and what it leaves out (`domain/validation/BpmnlintConfigResolution.ts`). | -| **Lint run** | One lint of one document with a new Linter, failing rules left out (`domain/validation/LintRun.ts`). | -| **Project bpmnlint** | The `.bpmnlintrc` that applies to a file, loaded once: validator, id conventions, settings (`adapters/out/files/ProjectBpmnlint.ts`). | -| **Resolver chain** | The tiers bundled, shipped, project that answer a `.bpmnlintrc`'s rules and configs (`adapters/out/files/ChainResolver.ts`). | -| **Repo trust** | Whether a git repository counts as the user's, as git checks it; only then does project code run (`adapters/out/files/RepoTrust.ts`). | -| **Text preservation** | Writing a changed model so the parts the change did not touch keep the original file's text (`domain/document/xml/TextPreservation.ts`). | -| **Preserving writer** | Writes the changed model with the unchanged elements and the style of the original text (`domain/document/xml/PreservingWriter.ts`). | -| **Element aligner** | Pairs the elements of two XML texts from the roots down: by id, then by name and position (`domain/document/xml/ElementAligner.ts`). | diff --git a/HANDOVER.md b/HANDOVER.md deleted file mode 100644 index 06290a9c..00000000 --- a/HANDOVER.md +++ /dev/null @@ -1,5517 +0,0 @@ -# Handover, 2026-10-20 (after step 1 of the audit fixes, the Camunda 7 step and its follow-ups, step 2: bpmn-cli as design-iq's editing engine, step 3 round 1 with its fixes and follow-ups: views, speaking ids, layout ergonomics, Camunda 8, stages 1 to 3 of `bpmn render` with the fixes after its verification (#12), the profile rules loaded on demand (#13), stages A and B of the incremental layout of edits to hand-drawn models (#18), the removal of the design profile (#35), ids up to 64 characters cut at whole words (#31), flow ids kept on request (#32), the project's bpmnlint configuration (#33), the id conventions of its element-id-naming (#34), @miragon/bpmnlint-plugin-rules 0.12.1, decision links in the file's spelling (#42), the E_VALIDATION advice that reads the refusal, flows that follow every end change, an alias through a retype with a typed split's input checked, the move fixes after #32, the note when BPMN_KEEP_FLOW_IDS kept the flow ids, a write's count of what bpmnlint leaves out, the .bpmnlintrc reloaded after later installs and found through linked directories, group boxes drawn again by a full redraw, the id forms fixed before 0.7, a file's flow form that stays put, DI ids within 64 characters that follow `set id=`, the stale edges of flows whose ends changed, `outAs`, the alias of the flow a splice creates, and the follow-ups of the id conventions: the conventions line, the stale-prefix hint, the process ids of new pools, declared DI prefixes, a join's entries from below sharing its bottom vertex, and labels kept off the titles of expanded sub-processes) - -State of `bpmn-cli` and what to do next. Everything below is verified against -the code in this repository, not from memory. - -## Where it stands - -A CLI that lets an AI agent edit BPMN 2.0 models semantically. The agent names -elements by id and changes kinds, names, flows, triggers, lanes, pools, -properties and vendor extensions; it never sees or writes diagram interchange. -Every mutating command runs the same pipeline: load, apply operations, -validate, lay out, write atomically; the write keeps the text of everything -the change did not touch, and a result equal to the file is not written. The -same pipeline runs in the browser on strings (`@miragon/bpmn-cli`: -`applyToXml` and friends; the file helpers are `@miragon/bpmn-cli/node`), with -a host's own validators inside the transaction. -`validate` (and every write) runs the engine profile of the file's platform: -Camunda 7 (`W_C7_*`) or Camunda 8 (`W_C8_*`), each rule checked on the -engines; the rules of each platform load on demand (a chunk of their own in -a host's bundle). Elements are addressed by id only; new ids speak (names, kind + -place, the ends of a flow) in the file's id style (where the project's -`.bpmnlintrc` runs `element-id-naming`: in its prefixes and case), and an `apply` batch -names what it creates with aliases (`"as": "$name"`); a flow whose id names -its ends follows them, unless flow ids are API and kept (`--keep-flow-ids`, -`BPMN_KEEP_FLOW_IDS=1`: `W_FLOW_ID_STALE` names the stale ones). An agent reads a -large model through `show --around ` and `show --context`, and a -write reports the warnings it added, not the file's old ones (`--summary` -for a few lines). A hand-made diagram is kept (new elements -are placed locally, like the modeler's space tool; a batch like the same ops -sent one by one; a new branch or boundary path takes a row that crosses no -line, opening one under its anchor or going above it, and a crossing left is -repaired once more), a new file or an -engine-owned drawing is redrawn by the built-in engine, and the agent formats -the picture with commands that name elements (`place`, `align`, `color`, -`label`, `route`, `space`, `tidy`, `compact`, `order` of lanes and pools; -selectors `--path`, `--kind`, `--branch`), and looks at it with `bpmn -render` (a PNG or SVG of the diagram, no browser: crops such as `--around`, -halos, the layout problems numbered in the image, tiles; `--render ` on -any mutating command draws its result). The format-command idea -comes from Miragon/design-iq PR #218 (`@bpmiq/bpmn-edit`, ADR 0008); no code -was copied, the concepts were re-implemented here. - -``` -npm install && npm run build -npm run gate # build, 2317 tests (+246 opt-in engine tests), isomorphism check with bundle budgets, layout-regression budget, short fuzz campaign -npm run typecheck -node tools/layout-regress.mjs # FILES 115 SCORE 370 (budget in tools/bench/regress-budget.json) -node bin/bpmn.js guide --short # the cheat sheet an agent reads first (`guide` for all of it, `guide ` for one section) -``` - -An audit in October 2026 (eight streams, about 60,000 mutations) confirmed 77 -bugs; [docs/audit-2026-10.md](docs/audit-2026-10.md) has the table with the -current status of each, and [docs/testing.md](docs/testing.md) how to run every -test layer, the benchmark and the fuzzer. - -## A join's entries share its bottom vertex; labels keep off sub-process titles (2026-10-20) - -Two layout requests of the maintainer: "In Joining Gateways dürfen Pfeile auch -von unten reingehen" (in a gateway that merges flows, of any kind, flows may -enter from below) and "Bei Event Subprozessen überlappen manchmal -Beschriftungen mit Elementen" (labels in event sub-processes overlap -elements; the screenshot showed a compensation event's label on the -sub-process's name). Three review rounds (reviewers, each finding checked -by a skeptic) found 16, 12 and 7 problems (the last only low ones); they -are fixed below or listed as open, the doc statements made exact. - -- **A join's entries from below share its bottom vertex** (from above: its - top vertex), for every gateway kind, as bpmn-js draws them (`h:v`). - - Metric (`metrics.ts` `runs`, `joinTail`, `checkSegmentOverlaps`): the - common tail of two flows into one gateway that end on the same bottom / - top vertex on a vertical last run is no `segmentOverlap`; the rest of - their overlap still counts, and so do a merge onto a line into the left - / right vertex, two flows on one line from start to end (also with a - duplicate first waypoint), shared last runs into tasks and events, and - `inOut` against an outgoing flow on the same vertex. `locate` puts the - badge of a pair still reported on the counted run. On the engine's 115 - scenario drawings: segmentOverlap into gateways 50 -> 3 (the 3 left are - merges into a left vertex). - - Incremental router (`router.ts` `targetMerge`, `crossings`; - `reroute.ts` `gatewayEnds`, `takenSides`): a flow into a gateway is - routed as before (every docked connection takes its vertex); when - another entry docks on the top / bottom vertex it is routed once more - with that vertex shared and no overlap cost on the other entries' last - vertical run, and that route is kept only when it crosses no more lines - (the first version traded a crossing for the shared vertex: - controlflow__s24 / s20 adds). Before, a second entry paid AVOID + - OVERLAP (about 400) and went over the top, into the left vertex across - the other entry, or merged onto a line; the docs/layout.md example now - gives `score 6 -> 0` without a `route`. - - Full engine (`layout/route.ts` `used()` with `out`, `offDiamond`): a - loop into a gateway pays the "used" penalty only when an outgoing flow - uses the point, so a second loop into a merge, or a loop into a join - with a forward rise, rises into the shared bottom vertex; not when the - loop would leave its gateway beside the diamond to get there (the first - version did: off-diamond gateway ends on 200 random models went 36 -> - 53, now 33; a loop still prefers a vertex, it is not forced). A gateway - label above a loop leaving through its top keeps left of the vertex - line as bpmn-js wraps it. - - Bench (764 incremental edits): measured with the 0.7 metric as the - baseline, segmentOverlap -281 (the metric alone); with one metric for - both, the routing changes 4 runs (crossings -1 each); no run worse. -- **Labels keep off the titles of expanded sub-processes** (event - sub-processes, transactions, ad-hoc included). - - One title geometry (`diagram/text.ts` `titleBox` / `titleDepth`: 12 px, - padding 7, wrapped at width - 14, 14.4 px a line, centred) for render, - the engine, the incremental layout and the metrics. - - Full engine: `declutterLabels` treats titles as obstacles and, inside - an expanded sub-process, tries a free spot below the title first, then - one beside it, the spots next to the shape before the stepped ones; - `frameSubProcess` (`layout/place.ts`, `childPad`) sizes a sub-process - for its content's labels at their default spots (events, gateways, - flows, data objects and stores, a data label that would overlap another - at the spot declutter moves it to; overhang past padding - 6) and for - its title's lines and the - labels above its first row; `labelGapFor` keeps labels on the same side - of neighbours in one row 4 px apart (the DI estimate is wide, so their - boxes touched and one label jumped onto the title). Loop channels run - below the default labels of the events they pass (a loop under a timer - pushed its label out of the frame), a loop inside a lane keeps 15 px - from its borders and its label inside the lane, above or below its run - (`engine.ts` `fitLaneLoops` grows the lane, cutting at the lane's own top so the - lanes before it never move, and moves the lane's own flows with their - labels; before, a loop could run on the lane divider; the first version - of this cut at the loop channel and detached a flow of the lane above; - a label that starts on a lane's bottom border belongs to the lane below, - so it no longer pushes the lane's event sub-processes down), - and a same-row flow uses one y for - both ends (sub-process heights of either parity gave 1 px diagonals). - This answers the open item of step 3 round 1 ("a label left of a - sub-process's content can stick out of it"). - - Incremental: titles are label obstacles and a label tries spots inside - its frame's content area first, then inside its home (sub-process or - pool; shifted spots of each side before a centred spot outside), then - anywhere (`labels.ts` `labelArea`, `homeOf`, `firstFree`); `fitInFrame` - grows a frame around a new shape's label; a new node keeps its label - clear of its neighbours' (`place.ts` `labelRoom`); `frameInterior` - keeps room for the title; a new gateway at the top of a sub-process and - `label --side above` make room below the title (`space.ts` - `roomBelowTitle`; the `label` op notes it); a renamed sub-process whose title gains - lines and now reaches what is drawn inside it makes room by at most the - lines it gained (`incremental.ts` `growForTitle`, `oldTitle`; the first - version counted a message flow's label above the sub-process and grew - it by 100+ px on every rename), and `compact` keeps that room; a label - that making room or a wider name brought onto a sub-process's centred - title is placed again inside the sub-process, or stays when no spot - there is free (`labels.ts` `clearTitles`, after the incremental layout - and every geometric format op; a label already on a title stays). - - New soft metric kind `labelOnTitle` (weight 2, not a harness kind): - a label on the title of a named expanded sub-process. -- **Harness**: SCORE 444 -> 443 (controlflow__s20: a gateway label off a - loop), budget lowered to 443. A full redraw changes 16 of the 115 - scenarios: agent__inc and its six variants (a long loop 12 px lower, - below an event's label; in agent__inc.i1 two loops, and its "retry" - label moves onto the loop's channel), agent__o2c / .e6 (a gateway label 3 px left), - artifacts__04 / 04b (the sub-process 10 px taller for the data store's - two-line label, which goes below its store), artifacts__14 / 14b (a - sub-process 1 px taller), controlflow__s19 (the end event 15 px right, - its 7-line label below it instead of above), controlflow__s20 and - subproc__s05 (a sub-process 2 px wider); every other per-file score is - unchanged. - -Evidence: gate 2264 tests + 246 opt-in, layout regression 443 (budget 443), -fuzz 12 x 15 0 / 0, edit bench no regression; the bundle grew by 10 KB -minified (whole entry 978.1 / 312.8 KB, `applyToXml` 694.8 / 222.5 KB; -budgets 979 / 313 and 695 / 223). Differential runs -against 0.7 (105 generated models, 1431 chained edit steps, identical -ops; the build the third review round checked): no crash, refusal or model -difference; summed over all steps labelOnTitle 325 -> 0, labelOutsideFrame -616 -> 55, labelOnLine 513 -> 440, crossings 910 -> 853. Private corpora -were not measured. - -Still open: ops.ts `settle()` reroutes broken flows with the stale lines of -those it reroutes later (incremental.ts `routeAll` leaves them out); place.ts -`connectionCost` does not trial-route a new chain's last flow into its join; -a split still takes one vertex per branch (the mirror of the join rule); the -engine's join with a down exit keeps `inOut` (agent__o2c); an empty branch's -reserved band can lie on a lane border (only loops grow lanes); pool and lane -header bands are no label obstacles yet (a start label can reach into a lane -header) and lanes are not sized for bottom-row labels; `labelOutsideFrame` -still measures the DI estimate, not the drawn text; a sub-process's title -over its content in a file from another tool is not reported; `label --side -below/left/right` at a frame border can still step the label out of the -frame; a rename that keeps the line count but widens the title does not move -a task drawn by hand in the title row; an event label displaced by a data -object below it, under a title spanning the whole width, still steps below -and pushes the data label out (no room is reserved for it); a label for which -no spot inside its frame is free still goes outside (incremental: the frame -does not grow for it), and the incremental placer takes a free spot inside -the frame however far out (up to two label heights, beside other elements) -before a near one outside it, so in a tight first row a new chain's labels -can crowd each other (one landing on a data object) or block a crossing -repair; a shifted spot 6 px beside a gateway's own line can count as -`labelOnLine` where it replaced a label outside the frame; lane Two's -over-the-top channel can sit higher than -needed when lane One's loop runs on the divider (`rangeTop`'s band test is -inclusive). - -### Follow-ups from a demo model (2026-10-20) - -The maintainer looked at a large demo (an order process in three lanes: -exclusive merge with a loop, parallel and inclusive split / join, a timer -boundary path, an event sub-process) and asked for four more things; each -was implemented, reviewed by reviewers with a skeptic per finding, and its -review findings fixed. - -- **A new entry follows the shared way into a join** (incremental router, - `router.ts` `runsOf`, merge tails, a third A* run state): with - `targetMerge` the whole common tail of the other entries into the top / - bottom vertex is shared (each entry's last run, a short hook with the - run before it, and every run back to where it parts from another entry, - as `metrics.ts` `joinTail` defines it), its runs are grid lines, and - running on it is free only when the route follows it into the vertex - (leaving it costs the overlap); every grid edge inside a tail counts, - a 1 px one too. A route touches its target gateway at no other vertex - than its port (`guardVertices`; sources are not guarded). Before, a new - branch into the demo's hooked join rose beside the bundle and ran along - the diamond's bottom edge through the vertex into the left vertex. -- **A join's flow to a target below leaves through its right vertex** - (`layout/route.ts` `joinFromBelow`): a gateway with two or more forward - entries, one rising into its bottom vertex, sends its flows to targets - below right, down, right (15 px right of the vertex, else mid-gap, else - the old drop); a split with one entry keeps its bottom drops. Harness - inOut 17 -> 0, SCORE 443 -> 392 (budget lowered). -- **An empty branch is drawn straight when its row is free** - (`route.ts` `openRow`, `lineFree`): in a laned scope the other branches - can run in other lanes; the empty one then runs from the split's right - vertex into the join's left vertex when nothing lies on that row and it - crosses no more lines than the band; an unused band leaves no strip. -- **A join's entries rise straight with the join exactly above** - (`layout/place.ts` `freeJoinColumns`, `columnTaken`, `gridScope`; - `route.ts` `entriesFromBelow`, `routeFaults`): when a shape or label lies - in the column under a join between it and its entries from below (the - demo: the end of the timer path), the join moves right past it with - everything after it, up to 3 columns, kept only when more entries rise - straight and the scope gets no more crossings, flows through shapes or - through labels (trial routing). In laned scopes the trials keep the band - offset the earlier passes gave the empty branches and a rejected move - restores the arranged state; the trials measure only what can change, so - a model with 120 blocked joins lays out in about 1.2x the time without - the rule (the first version took 7x). Harness hooks into a join 7 -> 0, - every per-file score unchanged (5 drawings move a join a column); - 120-model differential: hooks 37 -> 3, score 1449 -> 1384, none worse. - -Evidence: gate 2283 tests + 246 opt-in, layout regression 392 (budget 392), -fuzz 12 x 15 0 / 0, edit bench no regression; a full redraw differs from 0.7 -in 28 of the 115 scenarios; bundle 985.7 / 315.7 KB (whole entry) and 702.4 / -225.3 KB (`applyToXml`), budgets 986 / 316 and 703 / 226. - -Still open from these: a split with exactly one entry from below still drops -its branches from the vertex that entry rises into (`inOut`); in a laned scope -an empty branch's band, where its row is not free, keeps the y of the layout -without lanes (it can lie on a lane border); 3 of 120 generated models keep a -hook because moving the join would add a crossing; the incremental layout does -not move a join (a new entry into a hand-drawn join whose column is taken -follows the hook bundle); gateway sources are not guarded against routes -through their other vertices, and no metric reports such a touch. - -**A boundary path into an upper lane leaves downwards and rises straight** -(the maintainer's request on the demo's timer path: its line ran along the -bottom border of the next task and hooked into "Kunden über Lieferverzug -informieren", which sat in that task's column). - -- Router (`layout/route.ts` `runBelow`, `GEdge.runsBelow`): a boundary - event's flow to a target above leaves the event's bottom and runs at a - height that keeps RUN_CLEAR (15 px) from the shapes, node and flow labels - and earlier runs in its x range (just below the host's row, else below - everything between them in the host's lane), then rises straight into the - target's bottom (a free, unblocked bottom slot), else into its left side - from the column gap, else hooks in; the old side exits only when every - new way cuts a shape. These flows are routed after the other forward - flows (the lowest host first; of one row the rightmost event first, so its - run is the highest and no drop of an event left of it crosses it), a way - through a node's label spot or a flow label costs like a crossing, and the - scope (lane: `fitLaneLoops`; pool, expanded sub-process, process: - `layout.height`) grows to hold the run. -- Placer (`place.ts` `freeJoinColumns`, `takenColumns`; `route.ts` - `risesInto`, `riseX`): the straight-rise rule of joins now covers every - node a forward flow enters from below; such a node (with what follows it) - moves right until its column is free, joins first, a node a move would - newly block pushed along; a move is kept when it gains a straight rise or - lowers crossings / cut shapes / labels run through, and adds nothing; - chains are chosen by the columns without moves, so the happy path stays. -- Harness SCORE 392 -> 371 (budget 371): 5 drawings gain straight rises, - lanes__complaint-t1 15 -> 10 and t2 21 -> 11 lose crossings. A reviewer - corpus of 148 models (boundary events of every kind, all lane situations): - score 1974 -> 1244, crossings 237 -> 170, flows through shapes 79 -> 35; - 45 better, 5 worse (r013 14 -> 25, r045 5 -> 10, r043 8 -> 12, m-3 10 -> 12, - x-9 0 -> 5: a run that kept 3 px from an empty branch's line now keeps 15 px - and crosses it). - -Evidence: gate 2297 tests + 246 opt-in, layout regression 371, fuzz 12 x 15 -0 / 0, edit benches no regression; a full redraw differs from 0.7 in 36 of the -115 scenarios; bundle 991.1 / 318.0 KB and 707.8 / 227.7 KB, budgets 992 / 319 -and 708 / 228. The fix round after review was stopped before its own -independent verification; the integrator verified it (gate, the review -repros, the corpus above, timings, the demo). - -Still open from this: the 5 models above; a laned model with 120 moved -handlers lays out in about 2x the time without the rule (gate models -unchanged); a host's second data object under the boundary event blocks the -way down (the old side exit stays); the incremental layout neither routes -boundary paths this way nor moves a handler; drawings get wider where nodes -move (54 of 200 generated models, 193 px on average); the runs keep clear of -the labels' default spots only, so declutter can still move a sibling -boundary label onto a run (review finding 8, not fixed). - -### A way over the top, a branch on its split's row (2026-10-20, after PR #72 was opened) - -The maintainer, looking at the reviewer model r013 (a timer on the first task -of lane 0 leading to its last task; a parallel split whose happy path runs on -in lane 2): "the way over the top would have had fewer crossings" and "why is -Notify data not on the straight line: both arrows leave the AND gateway -downwards". - -- **Ways over the top** (`layout/route.ts`, `runAbove`, `GEdge.runsAbove`): - where host and target share a lane (or there are no lanes), a boundary - event's flow to a target above may leave the event downwards, take a short - stub to the column gap after (or before) its host, rise past the host's row - to a run 15 px above everything in its x range and enter the target's top - (or its left side from the gap before it). Scored with the same faults as - the ways below, a small penalty above them, and taken only when it crosses - fewer lines than the best way below crosses plus cuts; the run is a top - channel later loops keep off; lanes grow for it (`fitLaneLoops`), and - `freeJoinColumns` does not move a node for an entry it gets from above. - r013 25 -> 10. -- **A branch on its split's row** (`layout/place.ts` `liftChains`, in - `engine.ts` `applyLanes` after `freeJoinColumns`): in a laned scope a node - entered from a node of its own lane on a row above moves up onto that row, - with the nodes after it in its row and lane, where nothing of the lane lies - or runs in their cells (the happy path's line past the columns its nodes in - other lanes leave empty included); kept only when the flow into it is then - one segment on no other flow's line, more flows are straight, none bends - more and nothing gets worse (trial routing, `routesOf`, exact rollback); a - row left empty closes. Without lanes nothing moves. Harness 371 -> 370 - (subproc__s13: a parallel run gone); 7 scenario drawings change. - -Evidence: gate 2310 tests + 246 opt-in, layout regression 370 (budget 370), -fuzz 12 x 15 0 / 0, edit benches (incremental, auto) no regression; 740 -generated models (the reviewer corpus and both implementers' corpora) against -PR #72's first commit: 36 better, 0 worse (crossings e.g. 186 -> 128 on 120 -models of r013's kind); timings within 1.13x on 60 / 120-split models; a full -redraw differs from 0.7 in 39 of 115 scenarios; bundle 996.6 / 320.0 KB and -713.3 / 229.8 KB, budgets 997 / 321 and 714 / 230. Verified by the integrator -(no separate review round). - -Still open: ways over the top only within one lane; loops and compensation -links routed after the boundary flows are not seen by their scoring; the -lift tries only a node's first qualifying incoming flow and gives up gains -where the flow would share the split's right vertex with a blocked drop -(p041, p047); trials measure routes before `fitLaneLoops` and message flows; -the plugin's recipes model (skills/bpmn/reference/formatting.md) now shows a -`labelOnLine` after its incremental review batch (the incremental router -reroutes a message flow through a label), and its "place a branch" step -needs a `space` first; a shortened message flow's envelope can sit on a pool -border; the incremental layout lifts nothing and draws no way over the top. - -### A boundary event on its host's top border (2026-10-21) - -The maintainer, on r013's way over the top (it left the event downwards and -rose through the gap before the host, crossing Start -> Pick invoice): "the -boundary event would have fitted better on top when the flow runs along the -top". - -- `layout/place.ts` `boundariesOnTop` (after `freeJoinColumns`, in laned - scopes after `liftChains`): a boundary event whose flows all go forward to - targets above it in its host's lane (or without lanes) is tried on its - host's top border (`GNode.onTop`, `placeBoundaryEvents`: from the right end - of the border, the nearer target's event rightmost; never on an expanded - sub-process's title), its label to the upper right (`labelLeft`: upper - left). The router (`route.ts`, shared `overEnds` / `overWay`) takes the - flow straight up to `runAbove`, along and down into the target's top (or - its left side). Kept by trial routing (flows, compensation links, data - associations) only when every flow of the event leaves upwards, its label - is clear, no straight rise or one-segment flow of another flow is lost and - the faults drop, or stay with a shorter way and fewer bends; else rolled - back exactly. Not tried: compensation events, events with data or notes, - message-flow ends. Labels of top events are obstacles for runs, loops and - notes; message legs going up jog above them. -- A boundary event's last-resort channel to a target below in its own lane is - a run its lane holds (`runsBelow`); to a target in another lane it is not - (the integrator's fix: marking those too made the host's lane grow while the - flow stayed, a `detachedEdge` in 3 of 420 laned models). -- The incremental layout needed no change: `attachedSide` sends a top-border - event's flow out of its top, labels.ts already labels it upper right. - -Evidence: gate 2317 tests + 246 opt-in, layout regression 370 (all 115 -drawings identical), fuzz 12 x 15 0 / 0, edit benches (incremental, auto) no -regression; 988 generated models against the previous PR commit: 93 better, -0 worse (corpus of r013's kind: crossings 128 -> 73, score 789 -> 469), 104 -events on a top border; r013 10 -> 5; timings within 1.08x; bundle 1001.6 / -321.8 KB and 718.3 / 231.6 KB, budgets 1002 / 322 and 719 / 232. Verified by -the integrator. - -Still open: only events whose way goes over the top move (a target in another -lane keeps the way below); the host's row keeps its room below when all its -events moved up; on a 100 px task the second top event covers the task's type -icon, and three or more rarely pass the clear-label check; trials do not see -message flows or lane growth; a message flow into the host's top docks about -7 px from a top event. - -## Follow-ups of the id conventions (2026-10-13, `fix/conventions-followups`, follow-ups of #34) - -Four follow-ups of #34 ("Open after #34", "Left for later"). All but the -process ids change only output under a `.bpmnlintrc` that runs -element-id-naming; the process ids follow the file in both modes (the -user's decision, Q2 a). - -- **The `conventions:` line only where the file has a style of its own** - (`IdStyle.ownStyleDiffers`, `Learned.unconforming`, `passes`): `learn` - notes whether the file has an id the declared conventions check that - does not follow them (a prefix they do not accept, a body not in their - case; a sequence flow under an exact entry too; any id of a type whose - prefix is a `W_CONFIG_IGNORED`, and under `required` of an event whose - qualified prefix is too long: no id passes a prefix new ids cannot have, - `rejects`, `Declared.ignored`; not `StartEvent_1`, the process or the - definitions). `Doc.allocateId` and `recordDeclaredId` record ids for - the line only then, and `Doc.create` no longer records - the process / definitions id of a new file. A new file and a file whose - checked ids all follow the rule get no line and no `conventions.ids`; - before, `bpmn new` and the first adds said `startEvent_orderReceived - follows element-id-naming ..., not the file's own id style`, and so did - a kind the file had no id of (`gateway_approved`: `learnedOnly()` - extrapolates `exclusiveGateway_` without evidence). A file of both styles - keeps the line, also for a kind it has no id of. `conforms` became - `passes` (a head and, under a declared case, the body), which - `declaredCaseId` shares; the check stops at the first such id, so - `IdStyle.infer` costs the same. -- **W_ID_PREFIX_STALE suggests an id the rule accepts** (`stalePrefix`): it - swaps the head only of an id that passed the rule for what the element - was (`declaredCaseId`); for one that did not it suggests the id of what - the element is now (`derivedBase(now)`, an unnamed one with its id's - words as the context), as for an id the rule did not check, and keeps - `was` in the message. The context of an unnamed one is the rest after - `was`, never the whole id (`speakingStem` reads `serviceTask_1` as the - stem `serviceTask` with a collision suffix and would put the old kind's - words into the new id, `userTask_serviceTask1`). After a `was` without - separator, a rest whose own prefix completes `was` into the old type's - name (`Task_checkOrder` after `user`: `userTask`) means the id starts - with `was` by accident: that path too, under every case (also `any`, - where every body passes), with what follows that prefix as the context; - a rest like `check_order` after `ut` is a body and keeps the head swap. `serviceTask_shipGoods` under PascalCase, retyped to a - script task: `scriptTask_ShipGoods` (was `scriptTask_shipGoods`, which - the rule rejects too); `userTask_checkOrder` under a prefix `user`: - `manualTask_checkOrder` (was `manualTask_Task_checkOrder`); an unnamed - `serviceTask_1`, retyped to a user task: `userTask_userTask1` (camelCase; - PascalCase `userTask_UserTask1`; was `userTask_1`, which the rule rejects: - a body begins with a letter). The guide's fix text says so. - `recordIdState` passes generated bases, which pass the rule: unchanged. -- **A new pool's process follows the file's process ids** - (`Learned.processes` / `processBodies`, `processStyle`, `fileLevel`): - `learn` tallies the prefixes of the process ids on their own (they vote - on nothing else; participant ids never on them). Where at least two - share one (a tie: the alphabetically first), a new process takes it, - with and without a `.bpmnlintrc` (unless it names `bpmn:Process` - exactly), in the case their bodies show (a tie between two cases: the - file's where it is one of them, else the first of pascal, camel, snake, - Pascal_Snake), else the file's: the case a kind the rule does not name - gets (`caseFor()`; without a `.bpmnlintrc` the file's over every id), so - the process matches the pool made with it (Camunda Modeler's `Process_1` - and `Process_0k3x9qa` next to hashed participants give - `Process_BankAccount` with `Participant_BankAccount` under the rule, as - before, not the rule's camelCase of the ids that follow it). Single - lower-case words, `process_customer`, show no case and are no - PascalCase: camel, snake where that case is snake_case / Pascal_Snake (a - snake_case file under the camelCase rule: camel, like its pool; never - the declared prefixes' shape): `process_customer` and `process_shop` - give `process_bankAccount` next to `participant_`, `Participant_` and - `Pool_` pools alike (before: `Process_BankAccount` under the rule, and without - it `process_bankAccount` or `Process_bankAccount` after the shape of the - file's other prefixes). Files bpmn-cli made keep `Process_`: their - process ids vote for it. Where no two share a prefix nothing changes - (under the rule `Process_`, without it the learned prefix chain). - `show --conventions` gives the process row the source `file` there. -- **A prefix declared for a DI type is a W_CONFIG_IGNORED** - (`idConventionsFromLint`, `DI_WITH_IDS`): one note per DI type bpmn-cli - writes ids for (`BPMNShape`, `BPMNEdge`, `BPMNPlane`, `BPMNDiagram`) that - the rule resolves to a prefix (its exact entry, not false or null); not - `BPMNLabel` (written without an id, which the rule skips) and not a key - that names no type (`bpmndi:Nope`, no more than `bpmn:Nope`) (`prefix of - bpmndi:BPMNShape "shape_" is not followed: - bpmn-cli names DI after its element (BPMNShape_, _di), and the - rule reports new DI ids`; hint: set it to false), listed by `new`, - `validate` and `show --conventions` and counted by a write like the - other notes. DI ids do not follow the prefix: that would touch the DI id - style, the layout engine, `restoreDiIds` and the DI rename recognition, - and bpmn-js writes `_di` anyway. `src/diagram/write.ts` is untouched - (#58 changed it since: a long id is cut in a new DI id, and DI ids follow - `set id=`; the note's forms still hold). -- Docs: `src/guide.ts` (CONVENTIONS, the W_ID_PREFIX_STALE fix, the - W_CONFIG_IGNORED meaning, `ID_CONVENTIONS.prefixes` / `declared`; `guide - --short` unchanged, 4644 bytes), docs/bpmnlint.md (the process ids, the - stale hint, the paragraph on the line that said the file's own style - needs two ids per rule, the DI note), docs/reference.md, docs/library.md, - docs/migrating.md (the process ids without a `.bpmnlintrc`, the one - change against 0.6), docs/testing.md. -- Tests: `test/conventions-declared.test.ts` (a new file and five writes - without the line, also for a process id the rule names exactly; a - conforming file's first gateway without it, and with it once the file - has `Activity_Legacy` or `serviceTask_Legacy` under camelCase, a receive - task under a prefix the rule cannot use, or an escalation end event whose - required qualified prefix is too long; the two - batch-regeneration tests now on a file of both styles, and without the - line on a conforming one; `scriptTask_ShipGoods`, - `scriptTask_AfterStart`, `manualTask_checkOrder` and `manualTask_review` - for ids that did not pass, `userTask_userTask1` / `userTask_UserTask1` - for an unnamed `serviceTask_1`, `manualTask_checkOrder` and - `manualTask_reviewIt` under the case any, the head swapped for ids that - did, also `utcheck_order` under a prefix `ut` and snake_case; process ids with and without the rule after three pool prefixes, in - a snake_case file (with and without the rule, under a snake_case one), - after `Process_*` ids, a tie between prefixes and one between body cases, - Camunda Modeler's `Process_1` and a hash next to hashed participants, - processes that share no prefix and an exact `bpmn:Process` entry, the - view's process row), - `test/conventions-lint.test.ts` (the DI notes of `idConventionsFromLint`, - none for false / null, `BPMNLabel` or a key that names no type, one for - `BPMNPlane`, the real rule reporting `BPMNShape_`), - `test/bpmnlint.test.ts` (the DI notes in `new`, `validate`, `show - --conventions`, counted by a write). The 7 new or changed tests fail on - main with #57 to #60 (checked after the rebase). -- Differential against `fix/flow-style-vote` (b2de40d, merged as #59; - #57, #58 and #60 touch none of `src/idstyle.ts`, `src/conventions.ts` - and `src/document.ts`, so the classes below hold on main): the same batches (fuzz steps and an `add participant`, two - retypes, an `add xor` per file) on both builds without conventions and - under three element-id-naming settings (the defaults, PascalCase, - snake_case with the qualifier required), run again after the review - fixes. Fixtures and scenarios (163 files, 10,408 comparisons, 52 refused - by both): 24 differ, all a new pool's process in the three - `incremental/orders*.bpmn` files, whose processes are `Proc_*` - (`Proc_BankAccount`, was `Process_BankAccount`), 6 without conventions - and 18 with; the 72 `add participant` runs in files whose processes are - all `Process_*` gave `Process_*`. `show --conventions --json` differs in - the process row alone: in those three files (`Proc_{name:pascal}`, 3 - without, 9 with conventions) and, under the conventions, in 9 scenarios - whose processes are `Process_*` (the same pattern, now `file` instead of - `default`: 27). A private corpus (counts only; 351 files, 10,872 - comparisons, 112 refused by both): 745 differ: 12 process ids (1 - without conventions), 50 dropped `conventions:` lines (files whose - checked ids all follow the rule), 683 W_ID_PREFIX_STALE suggestions - (ids whose body is not in the declared case, mostly under PascalCase - and snake_case); 0 unexplained, no `conventions:` line differs in a - file of both styles, the 56 `add participant` runs in files whose - processes are all `Process_*` gave `Process_*`; `show --conventions - --json` differs in the process row of 50 of 1,404 views. A new file and - five adds under each setting: 24 of 30 steps differ, each a dropped - line. Without conventions only the 7 runs with process evidence differ. - `tools/speaking-ids.mjs --baseline` (scenarios and fixtures), `npm run - roundtrip` identical; `layout-regress --compare` 0 better, 0 worse. -- Runtime (CPU medians, interleaved rounds, 300 tasks and 299 flows, - b2de40d / this branch): `IdStyle.infer` unchanged (a mixed file 1.39 / - 1.38 ms, under the conventions 2.64 / 2.74; a conforming file 2.55 / - 2.52, under the conventions 4.52 / 4.64); 20 adds in one batch within - noise (110-127 ms on both). -- Bundle (rebased on main with #57, #58, #59 and #60): whole entry 965.5 / - 308.2 -> 968.1 / 309.2 KB, `applyToXml` only 684.1 / 218.6 -> 685.4 / - 219.0 KB (minified / gzip). The whole entry's gzip budget is raised - 309 -> 312 KB (measured + 1 %): main left 0.8 KB under it, and this - change's +1.0 KB (the guide text of the four changes, about 1 KB of text, - and the process / conformance code, with the review fixes: the ignored - types counted, the stale context, the process case, the DI types bpmn-cli - writes ids for) passes 309. The other budgets hold: 972 KB minified (3.9 - KB to spare), 689 / 220 KB for `applyToXml` only (3.6 / 1.0 KB to - spare). `docs/library.md` updated. - -Left for later: - -- The body case of a new process under a shared prefix comes from the - process ids, else the file's; a file whose processes mix cases (one - `process_Customer`, one `process_shop`) takes the case they show, a tie - between cases the file's. Nothing smarter is needed until a file shows - it. -- The DI note does not name what bpmn-cli writes for a plane or a diagram - (`BPMNPlane_`, `BPMNDiagram_`); the rule checks those only where - a prefix names their type. -- `rejects` counts every id of a type whose prefix is ignored as one the - rule rejects. That is exact for a prefix that cannot start an id or - leaves no room, and close for one with a character outside ASCII (an id - could start with it and pass the rule). An exact check needs the raw - prefix, which `IdConventions` does not carry (a change of the library - API, for a case no file has shown). -- W_ID_PREFIX_STALE for an id the rule did not check before (a type - switched off) still takes an unnamed element's context from - `speakingStem` of the whole id, which reads `scriptTask_1` as the stem - `scriptTask` with a collision suffix: retyped to a service task it - suggests `serviceTask_scriptTask1` (also before this change). Such - an id has no `was` to cut at, and `speakingStem` reads a bare - `reviewOrder_2` the same way on purpose; telling the two apart needs the - type names of the id's own prefix. - -## `outAs`: the alias of the flow a splice creates (2026-10-13, `feat/out-as-alias`) - -A gap #32 left ("Left out, for later" below): on a splice `flowAs` names the -kept flow into the new node, and the flow the splice creates (new node -> -old successor) had no alias, so a one-shot batch (design-iq sends one per -edit) that splices a gateway in and sets a condition or a default on that flow -had to guess its id. The user chose a new public key (option a of the -follow-up plan's Q4): documented in the full guide and docs/ops.md, the -short guide unchanged. - -- **`outAs` on `add`** (`src/ops/add.ts` `bindOutAs`, `src/ops/types.ts`, - `src/batch.ts` `ADD_FIELDS`, `src/ops/aliases.ts` `DEF_KEYS`). It binds the - one sequence flow the add creates out of the new node: on a splice - (`after` a node with one out-flow, `before` a node with one in-flow, - `after` + `before`, `flow`) the flow to the old successor, on a prepend - the flow to the anchor (the flow `flowAs` names there too), else the flow - to `to` (an append after a gateway, `in`, `on` or no placement with - `to`). The new node was unconnected before its placement, so its - out-flows after it are exactly those. `E_USAGE` where there is none - (`outAs $o: Activity_X gets no flow out of it from this placement - (--after Gateway_G appends it)`) or two, a splice or prepend together - with `to` (the message names both; the hint says to connect the `to` - target with a connect op that has `as`): no guessing which one was - meant. parseOps refuses it up front on a kind that is no flow node (data - object, annotation, lane, pool: `"outAs": add dataObject creates no - sequence flow out of it`, so it never suggests `to` there; `checkKind` - now returns the family, `FLOW_NODE_FAMILIES` moved to `src/kinds.ts`) - and where no key can create such a flow (no `after`, `before`, `flow` or - `to`); the add's own check stays for typed library ops. With - `--if-absent` and an existing element (`bindExistingOutAs`) it binds the - one flow out of it and refuses none, several or no flow node (`outAs - $yes: Gateway_Valid already exists and has 2 flows out of it (...)`, - the flows as candidates): the idempotent gateway batch run again after - a second branch was put first would otherwise have bound that branch - and put the condition on both, and an existing node without out-flow - left the alias unbound, so the next op said no op defines it. Split - nodes take no `outAs` (the split creates a node's - out-flow with the next node; `SplitBranch.nodes` omits it, so parseOps - says `unknown key`). The alias follows a later rename of the flow like - every alias. The `E_UNKNOWN_ALIAS` hint and its catalogue entry name it. -- Docs: the full guide's alias paragraph of OPS JSON and the CONTRACT's - alias sentence, [docs/ops.md](docs/ops.md) (Aliases: what `outAs` names, - with the gateway example; "has no alias, so take its id from the result" - is gone; `ifAbsent` with an existing node), [docs/reference.md](docs/reference.md) - (one bullet under Placement; on a prepend `flowAs` and `outAs` name the - same flow), [docs/migrating.md](docs/migrating.md) (the 0.7 sentence - that said the splice's flow has no alias), the skill's - `reference/ops.md` (keys table and the alias sentence), SKILL.md (rule 4 - and the `E_UNKNOWN_ALIAS` row, like the catalogue), - [docs/library.md](docs/library.md) (bundle table). `guide --short` is - byte-identical (4644 bytes): its generated ops key list leaves out the - keys in `FULL_GUIDE_ONLY_KEYS` (`src/guide.ts`, only `outAs`). -- Tests: `test/batch-aliases.test.ts` (a gateway spliced into a flow, its - new flow given a name and condition and the other branch made the - default in the same batch, also through `bpmn apply` with the `aliases:` - line and `--json`; `after` and `before` splices; `to` after a gateway, - with `in` and on a boundary event; a prepend; a later splice into the - aliased flow renames it and the alias follows; `--if-absent` on a node - with one out-flow; the refusals: parse time, an append, a splice with `to`, a data object, - annotation, lane and pool at parse time and as a typed op, a split node, - a typed op of `mutateDoc`, an alias used before its op; `--if-absent`: - the gateway batch run again binds the one flow, refused once a second - branch is put first, and on an existing node without out-flow or a pool; - the schema, the full guide, the catalogue, and the short guide without - it), and `test/batch.test.ts` (the allowed keys of add now list `outAs`; - guide and docs/ops.md say `"outAs"` names the splice's new flow and no - longer "has no alias"). All eight fail on main. -- Evidence against a build of main: batches through `applyToXml` on - test/fixtures and tools/scenarios (164 files, 2573 batches). Without - `outAs` (adds with `as` / `flowAs`, splits, prepends with `to`, the - `flowAs` refusal, a typo on connect) byte-identical, xml and result: 815. - A typo on add: the message differs only by `outAs` in the allowed keys - (163). `E_UNKNOWN_ALIAS`: the message is the same, the hint names - `outAs` (163). With `outAs`, main refuses the key; the branch writes the - xml that main and the branch write for the same batch with the alias - replaced by the flow's id, and the result differs only by the alias: - 830 (163 of them an `ifAbsent` add of an existing task with one - out-flow). 80 more fail on both builds with the same error (a placement - the file refuses), 2 are the new `E_USAGE` (the file's first task ends - its path, so the gateway after it appends), and 520 refusals are the new - `E_USAGE`: 260 placements without such a flow, 163 data objects at parse - time, 97 `ifAbsent` adds of an existing node with several or no - out-flows. A private corpus (635 files, 9825 batches, counts only): - 3065 / 613 / 613 / 3409 / 176 / 2 / 1027 + 613 + 307 in the same - classes, nothing else. The CLI's - `guide --short` is byte-identical, `guide` and `kinds` differ by the - alias text, the add keys line and the catalogue fix, `kinds --json` by - `outAs` in the add schema and that fix. `layout-regress --compare`: 0 - better, 0 worse; `speaking-ids --baseline` and `roundtrip --baseline` - are unchanged. -- Bundle (rebased on main with #58, #59 and #60): whole entry 962.7 -> - 965.5 KB minified (307.4 -> 308.2 KB gzip), `applyToXml` 681.8 -> 684.1 KB - (218.0 -> 218.6 KB gzip), for the key's schema text, its parse checks and - the add's refusals (the texts were shortened first). Main's budgets in - `tools/iso/budget.json` hold (972 / 309 and 689 / 220 KB), so they stay: - 6.5 KB minified and 0.8 KB gzip are left under the whole entry's, 4.9 and - 1.4 KB under `applyToXml`'s; the gzip margin of the whole entry is the - tight one. Runtime: - one `doc.outgoing` call for an add with `outAs`, none without; a splice - batch takes about 9 ms on both builds (median of 40). -- Left for later: the flow out of the last node of a split branch (to the - join or the old successor) has no alias either; a split node with - `outAs` would need the split to bind it after the join is made. A typed - `add` with `flowAs` on a data object or pool still ignores it silently - (parseOps refuses it); `outAs` there is `E_USAGE`. `flowAs` with - `--if-absent` still binds the existing node's first in-flow, and without - one leaves the alias unbound (a later use says no op defines it); a - change of `flowAs` was out of scope here. The alias list of - docs/migrating.md under "Changes from 0.3" stays without `outAs`: it - lists what 0.4 brought, and the 0.7 section names `outAs`. - -## Stale edges of flows whose ends changed (2026-10-13, `fix/stale-edges-of-re-ended-flows`, follow-ups of #32) - -Under `--no-layout` a flow whose ends an op changed (a bridge, the flows of -a move, a `set source=`) kept its BPMNEdge at the old ends, with -`--keep-flow-ids` without any note; a second BPMNDiagram of the same -process kept such edges after every write; and no later write repaired -them: `brokenEdge` tolerated the end gap an edge had before, so an edge -already off its ends never counted as broken, and no metric showed it. - -- **`--no-layout`** (`src/pipeline.ts` skip path; `changedEnds` / - `pruneReEnded` in `src/diagram/incremental.ts`; `pruneEdges` in the prune - section of `src/diagram/write.ts`, nothing else there, so the rebase onto - the DI-id PR stays local): the BPMNEdge of a sequence or message flow - whose ends differ from the snapshot goes, in every diagram and both - modes. Ends are compared by the id they have now: `set id=` and a retype - (also retype + `set id=` in one batch: `successorOf` in `src/result.ts`) - are no end change. An edge stays when its end points already reach the - shapes of the new ends (within 10 px: a node added and removed again - under `--no-layout` gives the input's DI back, and an engine drawing - stays engine-owned), and on the root's first diagram while a new end has - no shape (a splice of a node the same write added: the next write places - it and routes the flow). The note counts and names the flows: - `removed 3 stale diagram element(s), among them the edge(s) of Flow_AToB, - Flow_BToC, Flow_CToDone (their ends changed; the next model edit without - --no-layout draws them, or \`bpmn route \`)`. -- **`route` draws a missing line** (`drawnEdge(..., draw)` in - `src/diagram/ops.ts`): a sequence or message flow without a BPMNEdge - whose two ends are drawn on one plane gets one (before: `E_NO_SHAPE`, the - same for a flow added with `--no-layout`). The `E_NO_SHAPE` hint of a - flow, the catalogue entry and the note name `route` and the next model - edit before `bpmn layout `. `color` / `label` on such a flow still - refuse (`E_NO_SHAPE`); format-only writes do not draw missing edges. -- **Further views** (incremental step 1): the same edges go from a second - BPMNDiagram of a root (it is never drawn), with a layout note. -- **Detached edges heal** (`detachedEnds`, `DETACHED_GAP` = 10 in - `src/diagram/reroute.ts`): a connection whose first or last waypoint lies - more than 10 px outside its end's shape counts as broken whatever it was - before. The incremental layout reroutes it with a note (`Flow_X routed - again: it did not reach Activity_C`), and so do the format commands that - change the geometry; that also catches flows a `tidy` / `place` left 11 - to 30 px off an event or gateway (`endGaps` tolerates half a round - shape's width). Not on a plane with vertical pools or lanes: - `brokenEdge(..., { detached: false })` there, since the incremental - layout may route nothing on such a plane and a rename would otherwise be - a full horizontal redraw (auto) or `E_LAYOUT_INCREMENTAL`. -- **Metric `detachedEdge`** (`src/diagram/metrics.ts`): the same rule as - an EXTRA kind of weight 10 (hard), ids `[edge, end shape(s) it misses]`, - located at the missed end point. The harness has no such kind: SCORE - stays 444. -- Bundle budgets (`tools/iso/budget.json`, measured after the rebase onto - #58 and #59): the PR adds about 4 KB minified / 1.3 KB gzip (pruning, - `route` drawing a line, the metric, hints). The whole entry goes - 958.7 / 306.1 -> 962.7 / 307.4 KB, within #58's gzip budget of 309 but - over its 960 KB minified; `applyToXml` only goes 678.3 / 216.8 -> - 681.8 / 218.0 KB, over 679 / 217. Raised to measured + 1 %: the whole - entry's minified budget 960 -> 972 KB (gzip stays 309), `applyToXml` - only 679 / 217 -> 689 / 220 KB. - -Evidence (origin/main ef14c4d as the baseline): - -- 11 new tests; 9 fail on origin/main (the splice test only because - `detachedEnds` is new), and the retype + `set id=` and add + remove tests - pin origin/main's behaviour, which the first version of this change broke - (found in review): `test/keep-flow-ids.test.ts` - (keep-mode move under `--no-layout`, the next write), - `test/diagram-incremental.test.ts` (a bridge; `route` after and within a - `--no-layout` batch, `color`'s refusal naming route; add + remove under - `--no-layout` gives the input back; a splice to a new node; an edge - 150 px off its ends healed, tidied, kept by `--no-layout`), - `test/diagram-incremental-robustness.test.ts` (a second view; vertical - pool with a detached edge keeps the drawing on a rename; retype + `set - id=` keeps both views' edges), `test/step1-diagram.test.ts` - (`detachedEnds`, the metric). -- Differential (same edits in process on both builds, 159 repository files - with a task: rename, add, timer path, split, remove, move, tidy, retype + - `set id=`, and `--no-layout` add / remove / move / retype + `set id=`): - every difference is in one class. The 14 scenarios whose edges are already - off their ends (agent__anno*, agent__claim*, artifacts__*) heal on every - write; `--no-layout` remove / move drop re-ended edges (143 / 114 files); - 14 runs differ only in the note's ``; twoviews.bpmn loses re-ended - edges in its second view; two `tidy` runs (gallery.bpmn, - lanes__complaint-t1) reroute an edge the tidy left 14 / 18 px off an - event. Retype + `set id=` is identical on every file without a detached - edge, with and without layout. `route` on 634 drawn flows: identical. - Private corpus (325 files, counts only): identical on the 324 files - without a detached edge for every layout edit; the one with a detached - association heals; one `tidy` reroutes a flow it left 30 px off a - gateway; `--no-layout` drops re-ended edges (323 removes, 268 moves); - `route` draws the line of 3 flows that had none (origin/main: - `E_NO_SHAPE`). -- `layout-regress --compare`: SCORE 444, 0 better, 0 worse. - `speaking-ids --baseline`: 570 / 570. `roundtrip`: no-op identical - 155 -> 142 of 164 and rename (layout auto) p90 2 -> 18 lines, all on the - 14 stale scenarios (a no-op heals them, like missing DI is completed); - `--no-layout` no-ops 164 / 164. -- Bench against the baseline: `RESULT: no regression`; the 101 models - without a detached edge give the same records; the 14 stale ones heal - (mean harness delta -1.9 -> -14.1, tidy hard 55 -> 47). -- Fuzz, 80 walks x 25 steps, seed 11, auto and incremental: 0 errors; the - baseline gives 4 `hard:detachedEdge` (`place` leaving flows off a gateway - or event), this build none; the 2 `hard:frameIntrusion` are the same. -- Write time, interleaved medians: 15 incremental and `--no-layout` writes - on 5 repository models 215.7 -> 213.4 ms (a second run 219.6 -> 221.2); - generated 120 / 300 node models 691.4 -> 693.6 ms (+0.3 %): noise. -- After the rebase onto #58 and #59 (origin/main f492f7c as the baseline): - 2201 tests; the same edits on 166 repository files with a task (rename, - add, remove, move, tidy; `--no-layout` add / remove / move) fall into the - same classes: 15 files with lines off their ends heal on every layout - write (the 14 stale scenarios and #59's fixture pool-branch-7922.bpmn, - whose F5 misses G); `--no-layout` remove / move drop re-ended edges and - nothing else (159 / 143 files); twoviews.bpmn loses re-ended edges in its - second view; nan.bpmn and nobounds.bpmn differ only in the note's - ``; the two `tidy` runs reroute the one edge they left off an event. - `roundtrip`: no-op identical 159 -> 145 of 169, all 14 on those files; - `--no-layout` no-ops 169 / 169. `layout-regress --compare`: 0 better, 0 - worse. Write time, interleaved medians of 25 rounds, two runs: 15 writes - on 5 repository models 156.0 -> 152.6 and 160.5 -> 165.9 ms; 6 - incremental and `--no-layout` writes on generated 120 / 300 node models - 681.0 -> 684.3 and 683.8 -> 668.5 ms: noise (within 3.5 % either way). - -Left for later: - -- A pruned edge's colour, label position and DI id are not carried to the - line the next write draws (documented in contract.md and migrating.md); - carrying them would need the old DI kept somewhere across writes. -- `color` / `label` on a flow without a line refuse with a hint to `route` - first; a format-only write does not draw missing edges. -- An edge another tool left detached in a further view, or on a plane with - vertical pools / lanes, stays (`detachedEdge` reports it). -- `endGaps`' half-width tolerance for round ends is unchanged below 10 px. -- On the first diagram, retype + `set id=` in one batch still reroutes the - node's flows ('ends changed' in `collectEdges` compares through - `currentId`, which does not follow a retype); harmless, as on origin/main. - Following `successorOf` in `currentId` would touch every pool / lane / - host check there, so it was left out. -- The 14 stale scenario files are not redrawn: they are inputs as other - tools write them. - -## DI ids within 64 characters, and they follow `set id=` (2026-10-13, `fix/di-ids-within-64-follow-renames`) - -Follow-ups of #31 and #32. A new drawing (a new file, a file without DI, DI -whose ids show no form) and its full redraws kept both engines' -`BPMNShape_` / `BPMNEdge_` / `BPMNPlane_` / `BPMNDiagram_` -uncut (73 to 76 characters for ids of 63 to 64), although #31 promised 64 -for every id and docs/audit-2026-10.md said derived DI ids were cut; and -`bpmn set id=`, the command `W_FLOW_ID_STALE` and -`W_ID_PREFIX_STALE` suggest, left `BPMNShape_` / `BPMNEdge_` / -`_di`, which the automatic flow rename moves. - -- **`src/diagram/write.ts`**: `restoreDiIds` no longer returns early - without old DI. DI without an old id that is free gets - `styledId( ?? 'bpmn', kind, id)` (a plane or diagram - `BPMNPlane_` / `BPMNDiagram_` + the root id cut, as `diIds()` already - did), through `claimUnique`, given after every old id and with the ids - that need no cut first, so a cut id never pushes an uncut one to `_2`. - Old ids still win and are never cut. `styledId` has a `legacy` flag - (passed to `cutAt`); `diStyleOf(diId, elId, kind)` (was `edgeStyleOf`, - edges only) also compares the cut, the 0.6 cut and either with a - collision suffix `_2`. `followDiIds(defs, el, old, id, ids)` moves each - BPMNShape / BPMNEdge id of `el` derived from `old` to the one derived - from `id` in the same style, `_2`, `_3`, ... when that is taken (as new - DI gets its id); one already derived from `id` stays. A main diagram - whose root changed (a pool added) counts its old ids as derived from the - old root also when they were cut, now or as 0.6 cut them (`namesRoot`), - so `BPMNPlane_` still follows the new collaboration. - `MAX_DI` stays 61. -- **`src/layout.ts`**: the auto engine runs its second pass (parse, - `restoreDiIds`, serialize) also without old DI when an id of the model - is longer than 49 characters (`diIdsFit`); otherwise its XML is returned - untouched as before. -- **`src/ops/set.ts`** `renameId` and **`src/ops/flows.ts`** `followEnds` / - `followRenamedEnd` call `followDiIds`; the plane and diagram of a - sub-process collapsed without a drawing (`src/pipeline.ts` - `persistExpansionHints`) take `diIds()` ids. Planes and diagrams do not - follow a rename (`set id=` keeps `BPMNPlane_`). -- **`src/ops/remove.ts`**: the DI a remove detaches gives its ids back to - `doc.ids`, so in a batch `remove X` + `set Y id=X` takes `BPMNShape_X` / - `X_di` as two commands do (before, the id still counted as taken and Y - kept `BPMNShape_Y`). -- **Evidence**: 12 tests fail on origin/main and pass (test/conventions-di.test.ts: - a fresh drawing with 52 to 64 character ids, flows named after long - names, a collapsed sub-process and a 54-character process id under both - engines, `_2` for two cut ids alike, the uncut id first, a pool added to - a drawing of a 54- and a 61-character process id under both engines and - both layout modes, a plane 0.6 cut, the collapse placeholder, the 0.6 - `BPMNEdge_Flow_Pruefung10To2030` after a splice and a `set id=`; - test/keep-flow-ids.test.ts: `set id=` on a flow and a task in `_di` and - `BPMN_` files, with ids kept too, following the `W_FLOW_ID_STALE` - hint now writes the very file the default rename writes, a DI id not - derived stays and one whose new id is taken gets `_2`, a derived id with - `_2` / `_3` follows, `remove` + `set id=` in one batch writes what two - commands write (with and without `--no-layout`), and the comment inside - the renamed element's DI element is dropped with the result's note); 1 - guard passes on both (an old DI id over 64 stays). Differential against - origin/main, in process (layout, layout `--engine auto`, the file - without DI drawn by both engines, `set name=`, `set id=` of a task and - of a flow, also `--no-layout`, a splice, validate): the 164 repository - files and the 635 files of the private corpus give the same JSON - everywhere and the same XML except two classes: DI ids of ids too long - for their room in a fresh drawing (repository 1 file, corpus 433, all - cut, each new id within 64), and the renamed element's DI id after - `set id=` (that DI element is then rewritten as bpmn-moddle writes it, - as the renamed element itself already was; the text around it is - byte-identical). Redraws of files with DI, `set name=`, splices and - validate are identical (run again after the review fixes and after the - rebase onto #56, against that main: the same counts). layout-regress - 444 -> 444 (BETTER 0, WORSE 0), roundtrip identical per file, - speaking-ids identical. -- **Runtime**: in process, warm, both builds interleaved, sum of per-file - medians of 9 runs over the 115 scenarios, against main after #56: fresh - drawing clean 719.5 -> 726.8 ms, auto 2,678.3 -> 2,681.5 ms, `set id=` - 1,499.5 -> 1,498.1 ms (noise); with every id lengthened to 52 to 63 - characters the clean engine 823.2 -> 838.5 ms and the auto engine - 2,794.2 -> 2,958.1 ms (+6 %, the second pass, about 1.4 ms a file, only - without old DI and with an id over 49). -- **Bundle**: whole entry 957.7 -> 958.7 KB minified (305.7 -> 306.1 KB - gzip), `applyToXml` only 677.6 -> 678.3 KB (216.6 -> 216.8 KB), measured - after the rebase onto #59 (docs/library.md's table has the new sizes). - The whole entry's gzip budget is raised 306 -> 309 KB (measured + 1 %): - #59 brought it to 305.7 KB, and this change's +0.4 KB (`followDiIds`, the - cut of new DI ids, the auto engine's second pass) passes 306. The other - budgets stay (960 KB minified; 679 / 217 KB for `applyToXml` only). -- **Left**: DI ids a file already has over 64 characters stay (an id is - never renamed by a redraw); planes and diagrams do not follow `set id=` - of their root. - -Left for later (low finding of the review, not fixed here): - -- `set id=` writes the renamed element's DI element anew, as bpmn-moddle - writes it (attribute order, `/>`, numbers such as `240.0` -> `240`), and - drops a comment inside it (the result's note counts it), as the renamed - element itself already was on main: `src/preserve.ts` pairs elements by - id, so a renamed one counts as removed and added. docs/migrating.md and - docs/reference.md say so, a test pins the note. Pairing the one unpaired - sibling of the same tag on each side would keep both texts, but - preserve.ts sees text only: a `remove` plus an `add` of the same kind - among the same siblings would pair as well and carry the removed - element's comments into the new one. Doing it right needs the batch's - renames (element and DI ids) handed to `preserveText`, which runs through - `src/document.ts` and the pipeline; a change of its own, with a - differential over every `set id=`. - -## A file's flow form stays put (2026-10-13, `fix/flow-style-vote`, follow-ups of #31 and #32) - -Four follow-ups of #31 ("Left for later") and #32 ("Left out, for later"): -in files whose flows mix prefixes, or number them glued (`f3`, `flow12`), -the learned flow style tipped after a few edits or a move, and flows named -under the losing prefix stopped following their ends, by default and with -ids kept (where a followed `W_FLOW_ID_STALE` hint then named its ends no -more). All in `src/idstyle.ts` (`learn`, `basesOf`); `idNamesEnds` only -passes the flow id to `derivedBases`. - -- **bpmn-cli's glued flows vote** (`GLUED_NAMED`, `FlowVotes.gluedNamed`): - a flow without `_` named by its ends after a glued numbered prefix - (`fPackToWeigh` next to `f1`; the longest such prefix followed by an - upper-case letter or a digit) votes `numbered` for that prefix; with a - suffix (`GLUED_SUFFIXED`) it now votes for the prefix too, and still - counts only where the file has glued numbered flows. In a snake_case or - Pascal_Snake body bpmn-cli's glued flows have a `_` - (`fweigh_parcel_to_label_parcel`, `fWeigh_Parcel_To_Label_Parcel`): - a flow with `_to_` / `_To_` (`GLUED_JOINED`, `FlowVotes.joined`) waits - until the glued prefixes are known and votes for one where it is the id - bpmn-cli gives its ends after it (`gluedJoinedOf`: uncut, cut, as 0.6 - cut it, with `_2`, or naming its source whole, so a spliced flow whose - id was kept still votes); every other one counts as before. Before, such - flows counted against the form: polish-rename.bpmn read as `Flow_` after - five adds, and the sixth named `Flow_ArchiveTo...` and kept - `fBillToShipTheGoodsToTheCustomer` on Archive -> C (the snake_case file: - the sixth renamed `farchive_order_to_event` to - `Flow_archive_order_to_close_case`). -- **Ties between forms and prefixes never go by document order** - (`Tally.top(min, prefers)`, `FORM_ORDER`): a tie between forms goes to - the one that names the ends, between a form's prefixes to the one more - flows use (`uses`: `any` plus the glued numbered flows and the glued - flows that voted for a prefix, so `f1, f2` against `sf_1, sf_2` is a - tie), then to the alphabetically first; `any` alphabetically. Before, the - first one counted won, so a move that declared a flow elsewhere flipped - F / G (seeds 9473, 7922). A tie between the scopes of scopedTo flows and - the element prefix tallies keep the first one counted, as before (a - scope tie is a file of one prefix and one form, whose conventions must - not change). -- **Flows follow under every prefix and form the file's flows use** - (`Learned.flowStyles`, `otherStyles`, `connectionBases`, `mayBeOf`): - `derivedBases` lists the bases of the winning style and of every other - one (`named` under each flow prefix with its separator, each form built - from the ends under the prefixes that voted for it, scopedTo per scope; - replaces `namedPrefixes`). Given the flow id, it computes only the styles - the id can be of (it starts with their head, prefix and a scopedTo - flow's scope, or with `Flow_` where the head is 40+ characters), so a - file of many prefixes or scopes costs a string test per style. A renamed - flow takes the winning style's id (a flow under another scope: the scope - new flows there take). -- **0.6's scoped flows cut to an empty target word vote** (`SCOPED_CUT`, - `legacyScopedCut`): `Flow_KotO_WarenwirtschaftssystemabgleichTo` (with a - glued suffix or none) votes scopedTo where it is the base 0.6 wanted for - its ends; four of them no longer switch a file of three uncut ones to - named `Flow_`. -- Tests: `test/conventions-ids.test.ts` (polish-rename's six adds; the - same walk in a snake_case and a Pascal_Snake body, also eight adds with - ids kept; the flow12 test tightened by three more commands and a splice; - glued votes only for a matching glued prefix, also with `_`; ties both - ways, with SequenceFlow_ / Flow_ hashes, a glued against a `_` prefix and - two forms under one prefix; a flow under the outvoted prefix and an - idPair flow under an outvoted form renamed, with ids kept - `W_FLOW_ID_STALE`; the 0.6 scoped file and a look-alike that does not - vote; a scope tie that keeps the first scope, and a flow under the other - scope that follows its ends), `test/keep-flow-ids.test.ts` (the five - recorded follow-hint seeds 1003, 9456, 9473, 7392, 7922 as fixtures in - `test/fixtures/mixed-styles/`, and 9456's default-mode drift); the guide - pin in `test/step3-ids-report.test.ts`. 14 of them fail on origin/main, - and so does the guide pin; the scope-tie test pins origin/main's scope - (fixture `mixed-styles/scope-tie.bpmn`). -- Differential against origin/main (ef14c4d): `show --conventions --json` - on the 163 distinct fixtures and scenarios differs in one file - (`step1/nested.bpmn`: H1, H2, G1-G3, F1-F3, G and F tied, G declared - first: now F); no file whose flows use one prefix and one form differs. - 3,226 single-op batches (both modes): 22 differ, 16 on nested.bpmn and 6 - on scope-tie.bpmn (a flow under the scope that lost the tie is renamed - after its new ends, with ids kept `W_FLOW_ID_STALE`; main left it naming - its old ends). 648 walks of eight adds each: 26 differ, 20 in five glued - files (the three `polish-*.bpmn`, two of the new fixtures), where the - branch keeps `f` (main switched to `Flow_` after five to seven adds), 4 - on nested.bpmn and 2 on scope-tie.bpmn (as above). The classes that may - differ: mixed-prefix files with tied prefixes, glued numbered files, - files with two tied forms under one prefix (now the form that names the - ends: two hashes and two idPair flows give idPair new flows instead of - `Flow_To`; no fixture has one), and scopedTo files with several - scopes (renames only). `tools/speaking-ids.mjs --baseline`: the same 20 - renames on both builds, 2 flow prefixes differ (nested.bpmn); - `tools/roundtrip.mjs` identical; `layout-regress --compare` 0 better, 0 - worse. The recorded hint cases: 0 'NOT NAMING after follow' (5 on main). - A keep-mode campaign (4,832 random ops on fixtures and scenarios, every - hint followed): 0 followed ids that stop naming their ends (11 on main), - 2,120 hints (2,072). -- Runtime (CPU medians, interleaved rounds): `IdStyle.infer` on 300 flows - unchanged (stemTo 6.1 / 6.3 ms, named 13.5 / 13.5, glued 1.8 / 1.7, - mixed 1.9 / 2.1; a snake_case glued file 3.7 -> 1.9, snake_case - `flow_a_to_b` flows next to `f1`, each one checked, 2.0 -> 3.2, snake_case - without glued flows 2.0 / 2.1); `derivedBases` per flow on single-style - files unchanged (named 44 / 42 us, glued 41 / 40), stemTo 79 -> 87 us - (one more 0.6 base for the `named` style), three flow prefixes 44 -> 64 - us, a scopedTo chain with 3 / 12 / 30 scopes 37 -> 53, 44 -> 69, 53 -> 81 - us (each scope tested by its head: before that filter 629 us at 30); 60 - splices on a 200-flow chain 147 -> 149 ms (ids kept 155 -> 151), within - noise. -- Bundle: whole entry 955.0 / 304.7 -> 957.7 / 305.7 KB, `applyToXml` only - 675.0 / 215.5 -> 677.6 / 216.6 KB (minified / gzip); within the budgets - (960 / 306, 679 / 217), unchanged, but the gzip headroom is now 0.3 and - 0.4 KB: the next PR that adds code to the entry will likely have to raise - them; `docs/library.md` updated. - -Left for later: - -- A file whose glued numbered flows were all renamed away (no `f3` left) - has no prefix for its `fAToB` flows to vote for: they count for nothing - and the file reads as `Flow_` (the vote needs the numbered evidence; a - run of lower-case letters alone would read bare camelCase flow ids as - prefixes). -- A tie between element prefixes (per kind and family), and one between - the scopes of scopedTo flows, still goes to the first one counted, so a - move that declares flows elsewhere can change the scope of new flows - where two scopes tie (kept: such a file's conventions must not change). -- A glued flow in a snake_case / Pascal_Snake body votes only where it - names its ends or its source (`gluedJoinedOf`); one whose source a move - or a bridge changed while its id was kept counts for nothing, where a - camelCase one votes by its shape alone (`GLUED_NAMED`). - -## Id forms before 0.7 (2026-10-13, `fix/id-forms-before-0.7`, follow-ups of #31 and #34) - -Six follow-ups of #31 and #34 change the form of new ids, so they land -before 0.7.0 (release PR #43), which then ships them; #31's cuts and #34's -glued suffix were never released and need no compatibility, 0.6's ids are -recognised as before (the legacy paths and tests are unchanged). The user's -decisions: the letter word is `No`; a long head stays wherever the id fits -(Q3 a); the filler rule covers the 40-character name slugs too. - -- **`No` before a glued collision number after a digit** (`withSuffix`, - `isSuffix` in `src/ids.ts`): under a declared camelCase / PascalCase and - in learned stemTo / scopedTo flows a taken base that ends in a digit gets - `No2` (`serviceTask_step1No2`, `ServiceTask_Phase2No2`, - `flow_checkOrderToSendPdf10No2`; before `serviceTask_step12`, which - pushed the real Step 12 to `serviceTask_step122`). `IdStyle.next`, - `Doc.regeneratedId`, the `W_ID_PREFIX_STALE` hint and a split's join use - it; `idNamesEnds`, `flowForms` and `cutOfForm` accept `No` before the - digits (`idNamesEnds` also for the former ends and labels a batch - records since #46, `test/conventions-ids.test.ts`), glued digits stay - accepted (0.6's `...Pdf102` follows). The real element-id-naming rule - passes the new ids (`test/conventions-lint.test.ts`). -- **No filler word at the end of a cut** (`isFiller`, `cutName`; `endForms`): - a cut flow end gives up a filler word (und, oder, an, die, the, of, ...; - a fixed German / English list) that a given-up word leaves last or before - its Join / number tail, and the 40-character slug of a name and a body cut - to its room drop one too (`Activity_LieferantenbestellungVersenden`, was - `...VersendenUnd`). The first word stays, and so does a filler word - between a number and the number tail (`StartAfter2Und2`; without that - the cut fell back to a word cut inside, `...Lieferantenbestellungsueb2`); - whole-id cuts (DI ids, joins) are unchanged; an uncut name that ends on - one keeps it. -- **Numbers before a number tail stay** (`endForms`): a flow's end - `zahlungseingang_2_2` never shrinks to `zahlungseingang_2` (another - element's id; the suffix then skipped `_2`), so no flow skips a suffix. - An element body that the 64-character cap ends on a number still skips - the suffix its cut gives back, as on main (`_auftrag_2_2`, - `_2_3`): a guard that cut one more word instead gave the second - "Auftrag 2 2" `_auftrag_2`, the id of an element named Auftrag 2, - and three equal names a mix of cuts; the review removed it. -- **A body keeps a letter; long element prefixes per id** (`withLetter`, - `cutBody`, `leadOf`): a room that ends inside a leading number starts at - the first word with a letter (`_rechnung`, was `_10202422024`; - camelCase lower first); a file prefix of more than 52 characters (fewer - than 8 left beside a `_99`) stays where the body's words up to its first - word with a letter and no filler word fit whole, else `activity_...`, per - id (`<55>_auftrag`, `activity_auftrag_2`; a 56+ prefix now keeps - `_gh`). For an unnamed element that lead runs to the anchor's - first word (`afterCheck`, `timerOnReview`), so its id still names the - anchor: `activity_afterCheckOrder` under a 56-character prefix as on main, - not `_after`, `_after_2`, ... with `W_ID_SUFFIXED`. -- **The backing data object keeps its word** (`IdRequest.tailWord`, - `backingRequest`): where references and data objects share a prefix and - the name's 40 characters leave `Object` / `Store` no room, the word goes - after the body (`processDataArtefactItem_lieferung_vollstaendig_object`, - was `..._eingegangen_2` with a `W_ID_SUFFIXED` that named no collision), - bare ids too. ` Object` counts only where its id still ends in the - word: a name that ends on a filler word lost both to the cut - (`..._variante` for "... Variante A"), and the next reference named - "... Variante" took `_2`. -- **A long flow head stays where the ends fit** (`wholePair`, `wholeEnds`, - `wholeIdFormBase`, `namedHead().long`): a flow prefix or scope that leaves - the ends fewer than 16 characters stays where they fit at whole words - (`Flow_<41-char scope>_ValidateToPack`, was `Flow_Validate_To_Pack`), else - Flow_ as before. A suffix whose cut gives the head up gives the whole id - the `Flow_` form with its `_2` (`Flow_Validate_To_Step1_2`; before the - review `Flow__ValidateToStep1_2`, a form the learner does not read - back). `show --conventions` shows the file's head with `Flow_ where the - ends do not fit`. One file can hold both forms; `derivedBases` lists both. - -Evidence: - -- `npm run gate` after the rebase onto 74c3b50 (#46 to #55): 2158 tests - (+246 opt-in; 2149 on main), isomorphism check, layout regression 444 - (budget 444), fuzz 12 x 15 with 0 errors; `guide --short` 4,644 bytes, - unchanged. Before the rebase (on 98fa834): `npm run roundtrip` and - `tools/speaking-ids.mjs --baseline` (scenarios and fixtures) as on main. -- A differential against main (98fa834, before the rebase), classified per changed id by a - trigger read off main's and the branch's ids (a local script, not - committed): filler = main's id has a filler word right before an end - boundary (the end, `To`, a number, a suffix, the next id of an idPair - flow) where the rule drops it (not the first word of an end, not between - two numbers, `To` itself no filler) and the branch's id has none there, - or main's 40-character slug of a name in the edit was cut after one; - No = `No`; x22 = two numbers in a row; long element prefix = - the head changed and one is over 52 characters, or main's body has no - letter; data = a backing id that now ends in `Object` / `Store`, or a - reference whose name it freed; long flow head = one id starts with the - file's long head. The suffixed ids of a changed base take its class, a - flow that names a changed element id that element's. Anything else, an - id over 64 or a change of warnings alone is unexplained. The first - version attributed changes by commit and took any filler word in the - edit (`To` too) as the trigger, which held for every corpus edit; these - counts replace it. - - 512 BPMN files on disk (the fixtures, the scenarios and a private - corpus, counts only), 11,892 edits (a task, a gateway, two unnamed - tasks, a timer, long names, a splice, a leading number, two flows, - Step 1 / Step 12, Auftrag 2 2, data, data names that end on a filler - word; without and with the rule's defaults): 7,730 identical, 4,162 - different with 13,342 changed ids: filler 6,371, No 5,448, data 40, - flows naming a changed id 1,483, unexplained 0; `show --conventions` - identical on every file. - - 372,000 synthetic request sequences (three ids each) in 31 file styles - (long prefixes and scopes, bare ids, a shared data prefix, declared - cases), with filler words and numbers: 77,200 differ (207,063 ids: - filler 92,415, long flow head 80,597, long element prefix 24,750, No - 8,780, x22 521), unexplained 0; the filler trigger holds for 39 % of - all sequences, changed or not. Without filler words and numbers - (372,000): 32,707 differ, all of them long flow head (67,820 ids) or - long element prefix (24,715) in those styles. - - The first version found four slips (a bare-id data object, a number - that ended a cut end held like a tail, the glued suffix after a head - given up for Flow_, a filler first word kept a long element prefix); - the review and the stricter one five more, fixed in the last commit: - the unnamed lead, the data object after a filler word, the suffix - form after a long head, the guard in `next()`, the filler word between - two numbers. -- Runtime (in-process, CPU time medians, 98fa834 -> branch, two runs): - `derivedBase` 12-13 -> 11-12 us, `derivedBases` 48 -> 50-51 us, three - ids from `next()` 31 -> 30 us per request, `IdStyle.infer` on 300 flows - (250 cut) 3.4-4.3 vs 2.9-3.5 ms, a splice write without layout 1.9 -> - 1.8 ms median. -- Bundle, on 74c3b50: whole entry 950.6 / 303.2 -> 955.0 / 304.7 KB, - `applyToXml` only 671.8 / 214.4 -> 675.0 / 215.5 KB (minified / gzip); - both fit the budgets (960 / 306, 679 / 217), unchanged; - `docs/library.md` updated. - -Left for later: - -- The filler list is fixed and short (`ueber`, `nach`, `vor` are not in - it: `Activity_BenachrichtigungUeber`); a number left before a dropped - word (`...Und4`, c31-count-before-dropped-word) and an end that shrinks - to a leading filler word stay as in #31. -- An element body that the cap ends on a number skips a suffix (above). - A suffix that never reads as another name's id would need a word there - (`No` is for glued suffixes only, Q1). -- A long element prefix kept for a base gives way in a suffixed id that - leaves the lead no room: `<55>_auftrag`, then `activity_auftrag_2`, - although `activity_auftrag` is free (the id still names Auftrag). -- A slug the filler rule frees of its last word can end on a number that - #31's number tail then keeps at the cost of words in a small room - (`<45>_user_task_2024` for a spelled "user task after ok 2024 und ...", - main `<45>_user_task_after_ok`; `<50>_after2024` for "After Daten 2024 - zur ..."): the tail rule is #31's, the slug only shorter. -- A suffix cut that cuts a flow end inside a word does not know that the - numbers before a number tail stay, and can drop one there - (`..._kund_2024ToLieferante2` after the base `..._kunde_1_2024ToLieferan`). - -## Group boxes over a full redraw (2026-10-13, `fix/full-redraw-keeps-groups`) - -A full redraw (`bpmn layout`, `--relayout` / `--layout full`, an -engine-owned drawing in `auto` mode, the `W_LAYOUT_INCREMENTAL_FAILED` -fallback, `--engine auto`) dropped every group's BPMNShape without a word, -and the quality line called the lost box resolved overlaps (gallery.bpmn: -`score 182 -> 64; resolved: overlaps [Group_FirstTasks, Activity_UserTask], -...`). Now a redraw draws each group again around what its box held. - -- **`src/diagram/groups.ts`** (new, browser-safe): `rememberGroups` records - every drawn group before an engine replaces the DI (plane, box, label, - members: shapes whose centre lay in the box, frames and groups inside - whole, elements referencing its category value). `redrawGroups` runs after - either engine, smallest group first (an inner group counts as a member of - the outer one), appended in the old DI order; the old DI id comes back - through `restoreDiIds`, colours through `applyColors`. The box: the - members' new bounds with their boundary events and the labels that pull - in no neighbour, 20 px around (40 above for a name), each side at most - half-way to a shape that is no member or to the frame around the members; - then whatever has its centre inside joins (a boundary event with its - host), until nothing joins. Other groups' boxes play no part in the - padding or the labels, so the order the groups are drawn in never changes - a box; a box that holds a group drawn after it is measured again with it. - So a redraw of the result changes nothing, and an edit to it finds the - drawing engine-owned. A group with nothing drawn inside comes after the - others and is no member of any: it keeps its box, or its size GROUP_PAD - below the drawing when the redraw put a shape or label on the old box, - and the layout block says so (`note: group kept its box: ...` / - `moved below the drawing: ...`). A group bpmn-auto-layout drew itself - (members that reference its category value) is left alone; - `GROUP_MEMBERS_NOT_FOUND` goes for the groups drawn here. -- **Clean engine**: the parts of a scope that no flow connects follow the - first part of their group (`place.ts` `groupedOrder`, - `PlaceOptions.groups` from `membersOf`). A part's group is that of the - first of its nodes (declaration order, boundary events after their host) - a box held, and the parts from a start event and from a node without - incoming flow are one list, so groups of independent chains no longer - swallow each other, also when a box held only the tasks or a chain has no - start event; without groups the order is the old one. A - collaboration-level group no longer reports `W_LAYOUT_DI_NOT_CREATED`; an - association to a group says `an end is a group: its box is drawn after - the layout` instead of "different diagrams". -- **Metrics** (`src/diagram/metrics.ts` and `tools/layout-regress.mjs` - alike): a group is no leaf shape, so no overlaps, through or label on - shape with a group (a group's own label still counts). The bench no longer - excludes groups (`hardDelta` / `softDelta`, `addedGroup` gone); the - fuzzer keeps `group:*` as warnings (rare now: the gate campaign 4 -> 0). -- **Review fixes** (before the PR): stacked groups (test/fixtures - incremental/groups.bpmn) came out differently on a second redraw, since - the padding stopped at a group box drawn earlier; an empty inner group - kept at its old box stretched the outer group to it; a kept empty box - could land on redrawn shapes (unstable, and the next edit found the - drawing hand-made); a box around tasks only, or a chain without a start - event, still let groups swallow each other; two overlapping groups needed - a second redraw to settle; a boundary event could join a box without its - host; `docs/migrating.md`, `docs/reference.md` (`notes` of a full - redraw), `docs/commands.md` and the guide's `layout` summary did not say - what changed. Each has a test now. -- **Evidence**: 16 tests (test/layout.test.ts "groups over a full redraw", - 15, and test/diagram-metrics.test.ts) fail on origin/main and pass. - Differential against origin/main, in process (layout, layout `--engine - auto`, a rename in `auto` mode, validate): the 159 repository files - without a group and the 347 files of the private corpus without one give - identical results; the 5 + 4 with a group differ by the redrawn boxes, - the metrics and the wording of an annotation-to-group `DI_NOT_CREATED` - only (their incremental edits write the same XML; every group they had is - drawn again by both engines, a second redraw is unchanged and an edit - after it runs `full`). layout-regress 444 -> 444 (BETTER 0, WORSE 0; no - scenario has a group, the budget stays), roundtrip identical. Runtime: no - measurable change. In process, warm (`layoutXml`, sum of per-file - medians, both builds interleaved): the 115 scenarios 1,156.1 -> 1,159.2 - ms and 1,179.7 -> 1,183.1 ms in two runs (clean, +0.3 %, the noise of - swapping the order), 3,512.1 -> 3,477.1 ms (`--engine auto`), the 5 - fixtures with a group 69.2 -> 69.8 ms; on gallery.bpmn remembering and - redrawing its group take 0.08 + 0.09 ms. -- **Bundle** (measured after the rebase onto the merged follow-ups): whole - entry 942.0 -> 950.6 KB minified (300.3 -> 303.2 KB gzip), `applyToXml` - only 663.5 -> 671.8 KB (211.6 -> 214.4 KB gzip). `src/diagram/groups.ts` - and the grouped order are in every `applyToXml` (a full redraw can run in - any write), and both entries no longer fit main's budgets of 948 / 302 and - 670 / 214, so they are raised to 960 / 306 and 679 / 217, the measured - size + 1.0 % (docs/library.md's table too). -- **Left**: the engine does not keep the members of one connected flow - together, so a box around members the redraw spread out also takes in - what lies between them (and holds it from then on), and its border can - cut a shape that lies partly between them (a wide annotation; the padding - itself never crosses one); an association between a text annotation and - a group is still not drawn by a full redraw (the annotation goes above - the drawing). Groups still cannot be created. - -Left for later (low findings of the review, not fixed here): - -- Two groups that overlap without one inside the other: a part held by both - is keyed by the nested path of both, so the other group's parts do not - follow it and one box can take in the other (five chains in the rows 2, 4, - 1, 5, 3 with Group_A on 2, 4, 1 and Group_B on 1, 5: Group_B ends up - around everything). The redraw is stable now (one pass settles it), but - keeping each group's parts consecutive needs an order in which every - group's parts are adjacent (a consecutive-ones ordering) rather than a - sort key, which is more than this fix; no repository or corpus file has - such groups. -- A non-member that the members' own box overlaps (a wide annotation placed - between two members) is cut by the border. Letting such a shape join - would make the box grow over neighbours it never held, possibly in a - cascade; the docs now say that the padding never crosses a neighbour but - members spread apart can still overlap one. - -## The .bpmnlintrc in long-running hosts and through linked directories (2026-10-13, `fix/lint-reload-and-linked-dirs`) - -Follow-ups of #33. A library host (design-iq's live host) that followed the -`W_LINT_UNAVAILABLE` hint and installed the plugin kept saying "is not -installed" until it restarted; a model reached through a symbolic link from -outside its repository got no `.bpmnlintrc`, while git finds the repository; -and `bpmnlintEngine()` resolved none of the camunda-compat configs the -plugin's bundle lacks, which a `.bpmnlintrc` gets offline. - -- **Reload** (`src/node/bpmnlint.ts`, `loadBpmnlint`): a cache entry keeps - what the load depended on in the project besides the `.bpmnlintrc`'s text - (`LoadDeps`, one `Dep` each): the packages the chain left out - (`ChainResolver.reason`, not bpmnlint), the project modules that failed to - load, and the moddle module paths the project answered or left out (not - the plugin's camunda / zeebe descriptors). Each `Dep` holds its `depState` - from before the load looked it up (`ChainResolver.first` before any tier, - `boundedRequire` before the require, `moddleExtensionsOf` before the - lookup), so a package that lands while the load runs is a change for the - next call. `depState` is where Node resolves the spellings, plus the files - Node's caches do not see: the nearest `node_modules/` from the - `.bpmnlintrc`'s directory up, its `package.json` and the path inside it, - or a relative / absolute path itself, each as path, inode, size, mtime, - ctime (npm extracts with the tarball's fixed mtime, so the inode and ctime - count). Node remembers each resolution for the life of the process - (`Module._pathCache`, returned without looking at the disk), so a package - found above the repository stayed found there after the hint's install - inside it: a reload first drops those entries of the old load's deps - (`forgetResolutions`; the request and its subpaths). Node does not cache a - require that threw, so a fixed plugin loads. A hit with nothing left out - and no project descriptor checks nothing. Not seen: a fix inside a module - the failed plugin requires in turn (a reinstall is); a project plugin that - loaded stays in Node's require cache (restart, as before); a failed plugin - reinstalled as a version whose `main` moved stays "not installed" until a - restart, because Node keeps the `package.json` it read (in C++, not - reachable from JS). -- **Linked directories** (`findBpmnlintConfig`): the path as given first, so - an in-repository link to a directory outside keeps the repository's - `.bpmnlintrc`; where that finds none and the start directory is reached - through a link, the physical directory, where git finds the repository - (`mlink -> repo/models` gets `repo`'s). `physicalOf` takes the deepest - existing ancestor of a directory a write is about to create (`bpmn new - mlink/sub/x.bpmn`), so a new subdirectory behind the link gets the same - `.bpmnlintrc` and id conventions. A linked model file counts by the - directory of the link, as git counts the working directory (the guide and - docs say "a model whose directory is reached through a symbolic link"). - Trust is unchanged: the project tier decides on the physical root of the - repository of the `.bpmnlintrc` found. `shown()` puts a physical path - outside a linked `lintFrom` relative to where the link leads (`..`), not - across the two spellings; the CLI's working directory is physical, so its - paths do not change. -- **`bpmnlintEngine()`**: its resolver is the offline part of the chain - (`offlineResolver`): the plugin's bundled resolver, then bpmnlint's - `NodeResolver` inside the plugin for the packages bpmn-cli ships. It stays - synchronous, and what neither knows throws the bundled resolver's error. - Library hosts get `camunda-cloud-8-5`, `camunda-platform-7-20` and the - other camunda-compat configs with the rules and findings a `.bpmnlintrc` - gets (test). The upstream half (Miragon/bpmnlint-rules: bundle every - camunda-compat config, or say that only the latest 7.x / 8.x one is - bundled; it matters for browser hosts, which have only the bundle) is not - filed. -- **Bench** (chore): `tools/bench/lib/corpus.mjs` passes `--no-lint-config` - to the engine corpus's `layout --force` when the reference build has it - (detected once, only when a file is drawn), like `arms.mjs` and the fuzz - executor. The corpus does not change (115 of 115 files byte-identical). -- Docs: docs/bpmnlint.md (Which configuration runs, Project code, Library - use, Limits), docs/library.md, one line in the guide's BPMNLINT, the - module header. This closes the cache and lookup items of #33's "Left for - later" and the bpmn-cli half of its camunda-compat bundle item. - -Evidence: - -- `npm run gate`: 2133 tests (+246 opt-in; +7 in test/bpmnlint.test.ts, each - fails on origin/main; the two added after the review, a package found - above the repository and one that appears during the load, and the new - subdirectory checks of the two link cases also fail on the branch's first - version), isomorphism check within budget (whole entry - 941.8 / 300.2 -> 942.0 / 300.3 KB, the guide lines; `applyToXml` 663.5 / - 211.6 unchanged), layout regression 444 (budget 444), fuzz 12 x 15 0 - errors. -- Differential against origin/main: the CLI on the 164 files of - tools/scenarios and test/fixtures (`validate`, `validate --json`, `add - --dry-run`) in a repository without a `.bpmnlintrc`, one with a project - plugin, a missing plugin and moddle descriptors, a link inside it to a - directory outside, and a directory outside any repository: 1968 of 1968 - outputs identical. Through a link from outside the repository 477 of 492 - differ, all by bpmnlint now running (the 15 others: an `add` both builds - refuse, `E_AMBIGUOUS_SCOPE`, before any lint), and every `validate` equals - the output inside the repository but the paths (328 of 328). - `layout-regress --compare` 0 better / 0 worse, `speaking-ids --baseline` - 570 of 570, `roundtrip` identical. -- The review's probes: a plugin and a moddle package found above the - repository and then installed inside it, and a descriptor written while - the load runs, are used by the next call in the same process (before: the - same message until a restart); the reinstall that moves `main` gives "is - not installed" until a restart (the documented limit). -- Runtime: a cache hit with nothing left out makes the same file system - calls as main (one read, one exists, one realpath, two lstat, one stat, no - module resolution; about 30 us either way). One package left out and one - project descriptor: 11 stats (the walk to the nearest - `node_modules/` up to `/`) and two resolutions, about 60 to 85 us - a hit against 25 to 30 us on main, which checked nothing. A load takes the - state of each package and descriptor before it looks it up (a resolution - and a few stats each, once per load). A lookup that finds no - `.bpmnlintrc` adds one realpath (about 20 us; through a link it walks - twice, about 0.12 ms). The CLI's `validate` with a `.bpmnlintrc` that has - a project plugin, a missing plugin and two moddle paths: median 167 ms - against 171 ms on main (30 runs each), within the noise. - -Left for later (low, not fixed here): - -- A failed plugin reinstalled as a version whose entry module moved is - reported as "not installed" until the host restarts (Node's package.json - cache). The reason could say that the package directory is there and Node - cannot resolve it, with a restart hint; that also changes the message of a - broken install in a fresh process, so it belongs in a PR of its own. -- `test/global-setup.ts` checks only the path as given for a stray - `.bpmnlintrc` above the temporary directory. That is enough: outside a - repository only the start directory counts. -- The upstream half of the camunda-compat bundle item (Miragon/bpmnlint-rules) - is not filed; it is a public issue and waits for the user's go. - -## A write counts what bpmnlint leaves out (2026-10-13, `fix/lint-left-out-counted`) - -Two findings of #33's third review ("Left for later" in its section, the -2nd and the 3rd). On a file without incoming / outgoing lists a write's -validator line read `0 error(s)` while `bpmn validate` exited 2: the -list-rule findings a write leaves out were dropped without a count. And a -moddle descriptor BpmnModdle rejects stopped every rule, while a write's -line looked like a healthy run. Now a write's totals are validate's, and -its line says what it did not compare -([docs/bpmnlint.md](docs/bpmnlint.md#writes)): -`validator bpmnlint (.bpmnlintrc in .): 7 error(s), 0 warning(s) in the -result (0 added, 0 resolved, 0 already in the file, 7 left out (incoming / -outgoing lists the file does not keep; `bpmn validate ` lists them))`. - -- **A per-run channel** (`src/validators.ts`, additive and optional API): - `ValidatorFinding.leftOut` (a short reason). The issue keeps it hidden (a - symbol like the key, never serialised). `compareRuns` skips such issues - before and after: never added, resolved, kept or pre-existing, never - blocking. A left-out finding after also takes its unmarked twin before - out of `resolved`: a real dead end that a change gives a flow in a file - without the lists is still reported by bpmnlint in CI, so it is not - resolved (W_DEAD_END is, as itself). It counts them in `counts` and in - `ValidatorReport.leftOut` (`[{ reason, count }]`, one entry per reason, - absent when there is none). A run with no run before (the advisory - bpmnlint of a created document, `bpmn new`) ignores the mark and reports - every finding, so `new` still lists a whole-model W_LINT_FAILED with its - cause and hint. A write's left-out finding covers no lint warning; a - check reports and covers as before - ([docs/validators.md](docs/validators.md#left-out)). -- **bpmnlint** (`src/bpmnlint.ts`): in before / after, a list-rule finding - the element's flows contradict is marked `incoming / outgoing lists the - file does not keep` (it was a `continue`), and the W_LINT_FAILED of a - model bpmnlint could not lint at all `W_LINT_FAILED, no rule ran`. - validate is unchanged. `resolveBpmnlintConfig` builds a BpmnModdle with - all descriptors once; when that throws it adds them one prefix at a time - and leaves out each one BpmnModdle rejects (a prefix or uri already - defined, an extends of an unknown type) as a `moddle` W_LINT_UNAVAILABLE - (`BpmnModdle rejects it ()`, pkg the module path or - `descriptor object`). The other rules run, and the node layer's detail - counts it (`1 left out (W_LINT_UNAVAILABLE; ...)`) without a change there. - The whole-model case that is left: a descriptor BpmnModdle takes but - cannot parse the file with (a property that redefines one bpmn does not - have). -- **Text** (`src/report.ts`): `keptOf` subtracts the left-out findings, so - `already in the file` and `--summary`'s `preexistingCount` count what they - counted before on a list-less file. The line adds ``N left out (; - `bpmn validate ` lists it/them)`` inside an advisory validator's - parenthesis, and ` (N left out (...))` to a blocking one (`lintBlocks`); - a host's validator, which `bpmn validate` does not run, says `a check - lists it/them` instead. `--summary` names them in a line of its own - (``validator bpmnlint: 7 left out (...; `bpmn validate ` lists - them)``) and its JSON in `leftOut: [{ validator, reason, count }]` - (additive, optional): a list-less write with `--summary` showed nothing - while validate exited 2, and the redefines case no longer said that no - rule ran. -- Docs: docs/bpmnlint.md (Writes, Which rules are available, - moddleExtensions, Lists of incoming and outgoing flows), - docs/validators.md (`leftOut` in Findings, a Left out section at the end - of Before and after, Results), docs/library.md, docs/reference.md - (`--json`, `--summary --json`), the guide's bpmnlint topic, its JSON - shapes and the catalogue entries of W_LINT_UNAVAILABLE and W_LINT_FAILED, - SKILL.md (the left-out findings include the ones of elements the change - added). `guide --short` is unchanged (4,644 bytes). - -Evidence: - -- Tests (each fails on main): test/validators.test.ts, a host validator - that leaves its dead ends out (blocking: not refused, not resolved, the - lint warning it would cover stays; advisory: the line and `--summary`; a - check reports it), one entry per reason, a finding real before and left - out after (not resolved) and the reverse (added), and a created document - (advisory: reported; blocking: left out); test/bpmnlint.test.ts, a write - on a list-less file (the line, the JSON, `--summary`, a new element there: - the write's totals equal validate's on the written file, `lintBlocks`), a - file with lists (no `leftOut`, the line as before), `add end --after` and - `connect` on a real loose node of a list-less file (no bpmnlint finding - resolved, W_DEAD_END / W_UNREACHABLE resolved as themselves), descriptors - BpmnModdle rejects (duplicate prefix and uri, `extends: ['bpmn:Nope']`, - two of one prefix, `{}` after `[]`) in the core and on disk (validate - lists the real findings), the whole-model W_LINT_FAILED left out of a - write (line and `--summary`) and listed by a new file with its cause and - hint. The core test of a model that cannot be linted now uses the - redefines descriptor (`{ c: [], d: {} }` is no longer one). -- `npm run gate` (after the rebase on #54): 2126 tests (+246 opt-in; - 2118 on main), isomorphism check, layout regression 444 (budget 444), - fuzz 12 x 15 0 errors. The whole entry 938.9 / 299.2 -> 941.8 / 300.2 KB, - `applyToXml` only 663.0 / 211.4 -> 663.5 / 211.6 KB: within main's - budgets (948 / 302, 670 / 214), unchanged; docs/library.md has the new - sizes. -- Differential against main (in process, test/fixtures + tools/scenarios, - 164 files as they are and without the lists, configurations - recommended, recommended + automation, camunda-compat 7-24 and 8-10 with - their moddle descriptors, a duplicate-prefix and a redefines descriptor; - validate, `set name=`, `add task --after` and a new file, text, JSON, - `--summary` and the written XML; 5832 cases, 3568 identical, 0 - unexplained): every file that keeps its lists gives byte-identical - output. The differences: a write on a list-less file (650 cases, 12 of - them fixtures whose own lists are incomplete) differs only in the - validator's `counts` and `leftOut`, its line and `--summary`'s - `leftOut`, and its totals equal validate's on the result; in 4 of them a - list finding main called resolved is no longer resolved (all 6 are still - in validate's result) and the lint warning it covered is resolved as - itself. The duplicate prefix (969 cases plus `new`) equals recommended - plus one W_LINT_UNAVAILABLE in validate (328 of 328 checked). The - redefines descriptor (644 writes) moves the W_LINT_FAILED from `already - in the file` to `1 left out (W_LINT_FAILED, no rule ran; ...)`, and - `--summary` from `preexistingCount` to its `leftOut` line; its `new` is - identical to main (W_LINT_FAILED added, with cause and hint). - `tools/roundtrip.mjs` and `tools/speaking-ids.mjs --baseline` are - identical to main. -- Runtime: with nothing left out, `compareRuns` does one more test per - issue. A `.bpmnlintrc` with moddle extensions costs one BpmnModdle - construction more per load (resolveBpmnlintConfig with the camunda - descriptor 0.05 -> 0.68 ms, once per load, cached with it); without them - nothing. The CLI's validate and `set` with camunda-compat 7-24 on - orders.bpmn: within noise (119 -> 117 ms, 856 -> 828 ms, medians of 15). - -Left: - -- A whole-model failure in one run only (before lints, after cannot, or - the reverse) still lists the other run's findings as resolved / added. - With the descriptor check the remaining causes are the configuration's, - which fail both runs alike. -- `rules reading : properties` in the W_LINT_UNAVAILABLE of a rejected - descriptor names the moddleExtensions key, also where the descriptor's own - prefix is another (`x` with prefix `bpmn`). -- The W_LINT_UNAVAILABLE and W_CONFIG_IGNORED notes in the validator's - detail keep main's `bpmn validate lists it` (no ``, no backticks): - changing them would change the line of every file that keeps its lists. -- A finding left out before and real after (a node with flows loses them) - is added, like on main: the change made it real. - -## A result says when BPMN_KEEP_FLOW_IDS kept the flow ids (2026-10-13, `feat/keep-flow-ids-env-note`) - -The second half of a #32 'Left out' item. `BPMN_KEEP_FLOW_IDS=1` usually -sits in the `env` of an agent's settings, where the agent reading a result -does not see it. With it set, a result could differ from the default with -no warning at all: on a line of numbered flows, a move put the node's old -out-flow back (`changed: Flow_2, Flow_4, Flow_3, ...`) where the default -creates a new flow and removes the old one. Nothing in the result named the -variable. - -- **CLI only** (`src/cli.ts`): `keepFlowIdsOption` returns the value and, - when the variable (not a flag) turned keeping on, the variable's value. - `printMutation` (every mutating command and `apply`, with stdin / stdout - too) adds one note, `flow ids kept: BPMN_KEEP_FLOW_IDS=1 - (--no-keep-flow-ids renames flows after their new ends for one command)` - with the value as set, where the kept ids changed the result - (`keptByEnvNote`): the change gave `W_FLOW_ID_STALE`, or a move - relocated a node (`ChangeSet.relocated`, from #47) and the change lists a - sequence flow it did not create as changed. The note sits among the result's notes (text and JSON - `notes`); `--summary`, which prints no notes of its own, prints it after - its change lines (`summaryText`), and `--summary --json` gets a `notes` - key only then. An `E_NOT_FOUND` of a batch of two or more ops gets one - more hint sentence (`keptIdsHint`): the missing id may be the one the - default renames a flow to, which kept its old id here. The library and - its reports are unchanged; the browser bundle only by the guide's text. -- **When the note shows.** Kept ids change a result in two places only: - `followEnds` (`src/ops/flows.ts`) keeps a flow whose id names its old ends - and records it for `W_FLOW_ID_STALE`, and a move puts the moved node's - one out-flow back (`src/ops/move.ts`, `doc.keepsFlowId`) instead of - removing it and creating a new one. A flow whose id does not name its ends - (`Flow_10`, Camunda Modeler's `Flow_1q7m2zd`) is renamed in neither mode, - so a splice, a bridging remove or `set target=` on it is listed as changed - in both; a flow the change created follows its ends in both; `set - name=` / `id=` (also the rename `W_FLOW_ID_STALE`'s hint gives) is the - same in both. None of these gets the note. The first version of this - branch showed it for any changed sequence flow; the review's differential - found that 548 of its 636 edits came out byte-identical without the - variable. -- **No change** when a flag decides (also `--keep-flow-ids` with the - variable set), when the variable is unset, empty or off, and for `new`, - `layout` and the read commands. -- **Docs**: guide OUTPUT and the `--summary --json` shape, docs/reference.md - (Environment, `--summary --json`), docs/commands.md, docs/contract.md - (Limitations), plugins/bpmn-cli/skills/bpmn/SKILL.md. `guide --short` is - unchanged. -- **Tests**: test/cli.test.ts (the note in text, `--summary`, `--json` and - `--summary --json`, with the value as set, for a stale flow and for the - numbered-flow move that has no warning, also in `apply`; byte-identical - output where a flag decides, the variable is unset or off, or the kept ids - changed nothing: a task rename, `connect`, `remove `, a batch that - connects and then names a flow, `set name=` / `id=`, and a splice, - a bridging remove and `set target=` on numbered and on Camunda Modeler - flow ids; the `E_NOT_FOUND` hint in text and JSON, none where a flag - decides or for one op); test/batch.test.ts (guide, docs and skill name - the note). -- **Evidence**: `apply ... -o -` of the benchmark's edits E1-E6, E8, E9 - plus a move, a `set target=`, a `set name=`, a bridging remove and a batch - that splices after a task and then names the task's old out-flow, on every - scenario (1,094 edits, 26,256 runs) and again on a copy of the scenarios - whose flows got ids that name their ends (the same counts). Against a - build of origin/main: every run with the variable unset, set with either - flag, `0`, or unset with `--keep-flow-ids` is byte-identical (exit, XML, - report); with it set the exit and the XML are identical and the report - differs only by the note. On the branch, the variable set against unset - in the same output format: the note is present exactly where the two - differ. On the scenarios (numbered flow ids) that is the 88 moves and - nothing else; on the speaking copy 724 edits (E1 108, E3 112, E8 76, E9 - 76, and 88 each of the move, `set target=`, the bridging remove and the - splice-then-name batch). `set name=` on a flow, E2 and E4-E6 come out the - same and get no note. No run failed only with the variable, so the - `E_NOT_FOUND` hint has its test only. Only `dist/cli.js` and - `dist/guide.js` differ; layout-regress (SCORE 444, 0 better / 0 worse), - speaking-ids and roundtrip print the same numbers for both builds. Gate: - 2075 tests, fuzz ERRORS 0, bundle +0.4 KB minified / +0.2 KB gzip (guide - text), `applyToXml` alone unchanged. -- **Rebased onto #55, #52, #53, #46, #48 and #47.** #47 made a move in - place a no-op whose flow options (`flowName`, `condition`, `flowId`) go - to the flow into its first node, the same in both modes. The first - version of this branch keyed the move case on "the ops ran a move" - (`moves`, set by `mutateArg`), so such a batch got the note with nothing - kept (`[{op:'move', ids:['Activity_BookInvoice'], after: - 'Activity_CheckInvoice', flowName:'ok'}]`: `changed: Flow_2` in both - modes). It now asks `ChangeSet.relocated`, which a move in place leaves - empty, and `moves` is gone; test/cli.test.ts has the two cases (speaking - ids with `flowId`, numbered with `flowName`), which fail with the old - condition. docs/reference.md names the move in place among the results - without a note. Gate after the rebase: 2118 tests (+246 opt-in; 2115 on - main), fuzz ERRORS 0, layout regress SCORE 444, `guide --short` 4,644 - bytes; bundles 938.9 / 299.2 KB whole entry and 663.0 / 211.4 KB - `applyToXml` alone, within main's budgets (948 / 302, 670 / 214; - docs/library.md). - -Left for later: - -- A move that changes a flow without putting one back (a gateway with - several out-flows spliced into a flow; a node the same batch created) - still gets the note. Telling the put-back apart would need the library to - mark it; the benchmark has no such move (every move with the note - differs from the default). -- The `E_NOT_FOUND` hint is conditional ("If it is the id ..."): the CLI - does not know whether the default would have given that id, so a typo in - a later op of a batch gets the sentence too. - -## Moves anchored on their own nodes, moves in place (2026-10-13, `fix/move-anchor-and-noop`) - -Three follow-ups of #32 (two from its "Left out, for later", one found -while checking them), in both flow-id modes, with the fixes of their -review: - -- **A chain anchored on one of its own nodes is refused** - (`ops/move.ts assertChainAnchor`). `move A Join Done --after Join`, - `move T1 E --after E` or `move A B --flow Flow_BToC` placed the chain - after one of its later nodes: exit 0 with unreachable nodes, a flow - listed as created and removed, `_2` ids. The per-node check - (`assertNotInside` against each node's own placement) never saw it. Now - the node the chain starts after (the `--after` anchor, also with - `--before`; the source of the `--flow`) must not move with a later node: - be it, lie inside it, or be one of its boundary events (`isInside` checks - `attachedToRef` too: `move A T --after ` or `--flow` out of it - made a cycle T -> timer -> A -> T with exit 0). A `--before` anchor inside - a later node is refused too (`move A Sub --before `; it was - refused with a message about Sub and A). `E_INVALID_PLACEMENT` before - anything changes; the hint names the move of the first node alone, or, - when that one is already there (`move C B --after B` with B -> C), the - reorder `bpmn move C --before B` (only when B has one flow in). The - first node keeps its own check and message (`assertFirstPlacement` runs - moveNode's checks first, in their order); a single node after its own - boundary event is `E_INVALID_PLACEMENT` now (`that is one of its boundary - events, which move with it`; it was `E_NOT_FOUND` for the boundary event). - A `--before` anchor or the `--flow`'s target that is a later node stays - allowed (`move A Join --before Join`, keep-flow-ids.test.ts). -- **A move in place changes nothing** (`inPlace`). `move B --after A` with - A -> B detached B (the bridge A -> C took B -> C's label and condition) - and spliced it back: by default `Flow_AToB_2 "go"`, `Flow_BToC_2` and two - `W_ID_SUFFIXED`; with ids kept the label moved to A -> B. A node with - several out-flows (a gateway after its predecessor) or several in-flows - (a merge before its successor) lost all of them. Now a placement that - leaves every node where it is gives a note (`Activity_B is already after - Activity_A`; for a chain `..., in this order`) and no change: each later - node follows the one before through its only in-flow, `--after X` is the - first node's only predecessor through its only in-flow (X no gateway: an - append makes a new branch), `--before Y` the last node's only successor - through its only out-flow (Y no join: a prepend), `--after` + `--before` - both (also `--after --before `). Flow options go to the - first node's in-flow (`optionsOnInFlow`: `applyFlowOptions`, now - exported, and a `flowId` renames that flow through `renameId`, like `set - id=`, `E_DUPLICATE_ID` when taken; one change entry `(renamed from - Flow_CToDone)`), so they need exactly one. Before the review fix a - `flowId` was dropped without a word; origin/main made a new flow with it - where it appended the node and dropped it where it spliced. Boundary - events, a node given twice and `--flow` are never in place. Some inputs - that failed before are notes now when the nodes are in place - (`E_HAS_SUCCESSOR` from an anchor with several out-flows, - `E_HAS_PREDECESSOR` before an implicit merge, `E_NO_FLOW` for a chain - with `--after X --before Y`, `E_INVALID_BRIDGE`). -- **Nothing written.** The incremental layout placed every id of a move - with a placement again (`pipeline.ts relocatedIds`), so even a no-op - rearranged a hand-made drawing (step1/far-label.bpmn: SUB, SE and ST - moved). It now takes only the nodes a move relocated: - `ChangeSet.relocated` (new, public, additive; the ids at the time of the - move, not renamed, concatenated by `merge`, not in `toJSON`). This also - keeps `move X --in ` ("already in") from moving X's shape. -- **What the bridge took.** `detachNode` and `detachWithBridge` return the - `Bridge`: the in-flow (`flow`), the out-flow it replaced (`from`) and what - it `took` (the name string, the condition element, the associations; - `carryAssociations` returns them; the default-flow rule of - `detachWithBridge` deletes `took.condition`). With ids kept, once the - whole chain is placed (`reportBridgesKept`, after `giveBackLate`), the - last change entry of a put-back out-flow says what of its own label (as - the move started: `labelsBefore`) a bridge still holds, by the bridge's - id then: `(was -> Activity_C; its name and condition stay with the bridge - Flow_AToB)`. It follows later bridges of the chain that took the value - over, and says nothing for what the flow has again or what a later - bridge did not take (that detach warned). The first version built the - clause when the flow was put back: a bridge a later node renamed was - named by its old id (a batch flow following its ends), and a swap - (`move B A --after Start`) got two clauses pointing at each other and an - extra entry `ChangeSet.change` no longer deduplicated. A bridge a later - node removed (not bridged: an end event, a split) took the label along, - silently on origin/main too (`move A E1 --after G`: `removed F_T_A - "done"`, no "done" left): the flow now gets name and condition back when - it ends between its old ends (`giveBack`, listed once), else - `W_LABEL_DROPPED` / `W_CONDITION_DROPPED` (`the bridge F_S_A took it, and - a later node of the move removed F_S_A`). Default mode is unchanged (the - out-flow is `removed` with its label). -- One #32 test changed: "a later change that leaves a stale flow where it - was" moved X in place to run Flow_AToB through its named ends; that move - is a no-op now, so the test moves X away and back (`move X --after C`, - `move X --after A`) with the same three expectations. -- Rebased onto #46, #48, #52, #53 and #55: the change entries this branch - builds from another one (the renamed flow of a move in place, the clause - of `reportBridgesKept`) keep the element they are about - (`flowChange(flow, detail)`, `about`), as #46 made every entry of move.ts - do, so a later rename still finds them. - -Docs: commands.md (move), ops.md, reference.md (placement), migrating.md -(Changes from 0.6: the refusal, the move in place and its `flowId`, -`ChangeSet.relocated`, the clause), the guide (`E_INVALID_PLACEMENT` in -the catalogue, the move summary, a placement note, the batch text), the -`flowId` description of the op schema (batch.ts), SKILL.md (error table). -`guide --short` is unchanged (4,644 bytes). - -Evidence: 7 new tests (test/keep-flow-ids.test.ts, -test/ops-mutate.test.ts) fail on origin/main and pass here. A differential -of generated move ops (the fuzz's own move, moves in place, chains along -the flows with anchors inside and outside, flow options with `flowId`, -swaps and reversed chains, chains anchored on a boundary event of a later -node, `--before` inside a later sub-process; both modes, the whole pipeline -with its layout) on origin/main and this branch differs only on no-op and -self-anchored moves, in the clause and where a removed bridge's label is -given back or warned about: - -| | test/fixtures + tools/scenarios (164 files) | private corpus (222 files, counts only) | -| --- | --- | --- | -| runs | 14,756 | 18,804 | -| identical | 7,583 | 9,951 | -| no-op on the branch | 4,330 | 5,356 | -| ... origin/main kept the graph (new ids, label moved, `flowId` flow made) | 3,718 | 4,384 | -| ... origin/main changed the graph (a gateway's or merge's other flows cut) | 406 | 740 | -| ... origin/main refused (`E_INVALID_PLACEMENT`, `E_HAS_SUCCESSOR`, `E_INVALID_BRIDGE`, ...) | 206 | 232 | -| refused as self-anchored (a later node, inside one, its boundary event) | 2,542 | 3,206 | -| ... origin/main wrote a rewired model | 1,530 | 1,610 | -| refused as placed on its own boundary event (origin/main: mostly `E_NOT_FOUND`) | 296 | 290 | -| only the clause of the put-back flow added (keep mode) | 5 | 1 | -| a removed bridge's label given back or warned about | 0 | 0 | -| anything else | 0 | 0 | - -The fuzz walks themselves (tools/fuzz/walk.mjs, the same seeds on both -builds, 96 walks x 25 steps, seed 1, auto and incremental, 130 move steps): -82 walks identical step by step, 14 diverge, each first at a `move-after` -that is a no-op here; one error-severity violation, the same in both builds -(a walk that does not diverge). `npm run roundtrip` and `tools/speaking-ids.mjs ---baseline` are identical to origin/main, the layout regress score stays -444, and the gate's fuzz has `ERRORS 0` and 4 warnings, as on origin/main. -Runtime: a move with the same output takes the same time (median 30 / 30 -ms and 35 / 35 ms per run, the two corpora, origin/main / branch); a no-op -is about 16 % faster (no detach, nothing placed again). The differential, -the walks and the runtimes were measured before the rebase. Bundles after -it: whole entry 938.5 / 299.0 KB (origin/main 931.6 / 296.7), `applyToXml` -alone 663.0 / 211.4 KB (657.5 / 209.6). Main's budgets (935 / 298 and 660 -/ 210) had 0.3 to 3.4 KB left after #46 and #48 grew the edit path, less -than this change adds (about 7 / 2.3 and 5.5 / 1.8 KB), so they are raised -to the measured size plus about 1 %: 948 / 302 and 670 / 214 -(tools/iso/budget.json, docs/library.md). - -Left for later (low, from the work and its review): - -- With `--before` and flow options a first node without exactly one - in-flow runs the old path (there is no single flow for the options). -- A relocating move that splices drops a `flowId` without a warning, as on - origin/main (`add` says `W_OPTION_IGNORED` with the `set id=` hint); the - schema says `flowId` names only a flow the op creates. -- The refusal hint is not run: "move the first node alone" can itself be - refused (a start event spliced into a flow). - -## A batch alias through a retype, a split's input in the library (2026-10-13, `fix/batch-alias-retype-split-input`) - -Two items #34 left open ("Open after #34" and "Left for later" below). - -- **An alias follows its element through a retype** (`src/ops/retype.ts`, - `src/result.ts`, `src/ops/index.ts`, `src/ops/aliases.ts`). A retype to - another BPMN type replaces the element (`swapType` makes a new one), and - the alias table kept the old one: a later op that used the alias got - `E_NOT_FOUND` ("which an op before this one removed"), and the result's - `aliases` left the alias out. Now `ChangeSet.replaced` records - `{ from, to }` (merged like `bindings`; `runBatch` clears both before it - returns), and `runBatch` calls `AliasTable.rebind(from, to)` after each - op. The alias follows the new element, also when a retype under the - declared conventions gave it a new id (`serviceTask_checkInvoice` -> - `userTask_checkInvoice`). A retype within one type (a trigger change) - never replaced the element, so nothing changes there. Output changes only - for batches that alias an element and retype it to another type: a batch - that uses the alias afterwards now runs, and `aliases:` and the JSON - `aliases` list the retyped element's alias. -- **A split's input in the library** (`src/ops/split.ts`, - `src/ops/aliases.ts`). Typed ops skip `parseOps`. `mutateDoc` took a - branch without `nodes` as a direct gateway -> join flow. `nodes: 'abc'`, - `branches: 'oops'`, a null branch, a node that is no object and a node - without a string `kind` crashed with an uncoded TypeError, in the alias - table's `slots()` or partway through the split with the gateway already - added. `splitFlow` now checks that shape before the split changes - anything (the ops before it in the batch have run). The error is - `E_USAGE` with parseOps' message and hint, `split` where parseOps has - `ops[i] (split)`: `split branches[0]: missing required key "nodes" - (Nodes of the branch in order ...)`; the parentheses come from - `BRANCH_FIELDS` / `NODE_FIELDS` of `src/batch.ts` (imported, batch.ts - unchanged), and the test compares each message and hint with - `parseOps`. The alias helpers read the split's lists only where they are - arrays of objects. `branches` that are missing or empty stay - `E_INVALID_VALUE`. The CLI and `applyToXml` run parseOps first, so their - texts do not change. -- Docs: [docs/ops.md](docs/ops.md) (an alias follows its element "through - a rename and a retype"), [docs/library.md](docs/library.md) (`mutateDoc`: - what a split checks, and that the ops before it have run), - [docs/migrating.md](docs/migrating.md) (two bullets in "Changes from - 0.6": the alias through a retype; a typed split without `nodes` is - `E_USAGE`, 0.6 made it a direct flow), one sentence each in the full - guide's ops section and the skill's `reference/ops.md`. `guide --short` - is unchanged (4644 bytes on main with #55, #52, #53 and #46). -- Tests: `test/batch-aliases.test.ts` (create, retype, set and a format op - on the alias, also for a gateway and an event; retyped twice; removed - after the retype is still `E_NOT_FOUND`; a new id under the declared - conventions; the CLI's `aliases:` line and `--json`) and - `test/ops-create.test.ts` (`mutateDoc` with branches without `nodes`, - `nodes: 'abc'`, `branches: 'oops'`, a null branch or node, a node - without a string `kind`: `E_USAGE` in parseOps' words, the document - unchanged; `nodes: []` still a direct flow). All four fail on main. -- Evidence against a build of main: the same batches through - `applyToXml` on test/fixtures and tools/scenarios (164 files, 2216 - batches). Batches without a type-changing retype of an aliased element, - and the split errors of parseOps, are byte-identical (xml and result): - 1793. The alias used after a retype: 259 now run (main `E_NOT_FOUND`). - The retype only: 162 differ by the added alias alone. On a private - corpus (counts only) the numbers are 231 / 27 / 21 of 279, with no other - difference. The CLI's split errors are identical. A seeded fuzz campaign - `--via apply` (40 walks x 25 steps, 44 retypes) wrote byte-identical - models and logs, apart from timings. `layout-regress --compare`: 0 - better, 0 worse; `speaking-ids --baseline` and `roundtrip --baseline` - are unchanged. Bundle: whole entry 925.7 -> 927.2 KB minified (294.5 -> - 295.0 KB gzip) before the rebase; after the rebase onto main with #55, - #52, #53 and #46 930.1 -> 931.6 KB (296.1 -> 296.7 KB gzip), `applyToXml` - 656.1 -> 657.5 KB (209.0 -> 209.6 KB gzip): within the budget, which - stays as it is. Runtime: one loop over the op's - replaced elements (empty without a retype) and a check over the split's - lists; within noise. -- Left for later: a typed split node whose `kind` is a string but empty or - unknown is `E_UNKNOWN_KIND` from `addElement`, after the split gateway - was added (coded, as before; parseOps refuses it up front). Checking it - in `checkShape` would need parseOps' `checkKind` (the - `eventSubProcess:` kinds), which lives in `src/batch.ts`, which - P6b (`outAs`) edits. The other keys of a typed split node (`set`, - `flowName`) are not checked, as for a typed `add`. `split.ts` has its own - five-line `describe()` (batch.ts does not export its copy). - -## Flows follow every end change (2026-10-13, `fix/flows-follow-every-end-change`, follow-ups of #32 and #34) - -Four leftovers of #32 ("Left out, for later") and #34 ("Pre-existing"), all -in the test whether a flow id names its ends and in the change report. - -- **An end renamed earlier** (`ops/flows.ts endLabels`, `recordFormerEnd`, - `idNamesEnds`; `Doc.recordFormerEnd`): `idNamesEnds` named an end only as - it was called then (labelOf, its id's stem, formerLabels). After `set - Activity_Check id=Activity_Verify` (in the batch or a command before) or - `set name=` on an end whose id says nothing, `Flow_CheckToOk` neither - followed a later splice nor was reported with ids kept. It now also tries - (a) the end's name where its id speaks (left out where its words are a - label's, `slugify` equal, so a file whose ids are their names' slugs does - the same work as before); this covers `set id=` of a named end across - commands; and (b) the id and labels `set` recorded before it changed an - end's `id` or `name` in the running batch or op on its own - (`Doc.formerEndsOf`, forgotten where `runBatch` and `runOp` alone forget - the kept flows), with the connection request built from the recorded id - (stemTo, idPair and scopedTo are built from ids). It records on change, - only for a node with sequence flows; no snapshot of every end. A retype - that builds a new element (`swapType`) hands the record on - (`Doc.replaceElement`). followEnds renames such a flow after its current - ends (`Flow_VerifyToLog`), staleFlowWarning reports it against them. -- **A flow without id** (`followEnds`): `idNamesEnds` is false for an empty - id; `followEnds` gives a flow without id whose ends change an id in both - modes, from its new ends (`allocateId`, no Rename: there was no old id) or - the `--flow-id` of the `add` that splices into it (claimed like the id of - a flow add creates: E_INVALID_ID / E_DUPLICATE_ID alike; no - W_OPTION_IGNORED then), and returns `''`, which `renamedNote` turns into - ` (id added: it had none)` on its one change entry; `Doc.keepsFlowId` is - false for it (a move does not put it back); `createSequenceFlow` claims no - undefined id. `add`, `split`, `apply` and a bridge into it ran into - E_INTERNAL (exit 70); the incremental layout now draws it by that id. - `format.ts` prints `(no id)` for a removed one (was `undefined`), - `--summary` leaves it out (`report.ts uniqueIds`). -- **The report names a flow by its own id** (`result.ts about`, `replaced`): - a splice renamed the spliced flow, the new flow took the freed id, and - `ChangeSet.rename` renamed every entry with that id, the new flow's too: - `created sequenceFlow Flow_CheckToCheck` twice in the text and `--json`, - `--summary` without the changed one, no `renamed`. Change entries and - renames now carry their element in a WeakMap (`about`; JSON and the - entries' shape unchanged): `flowChange(flow, detail?)` and `changeOf` - record it (the six `{ ...flowChange(x), detail }` sites pass the detail - instead, so they keep it), `Doc.recordRename` / `recordRegenerated` take - it, and `ChangeSet.rename` renames an entry with the old id only when it - has no element or the renamed one (also in a batch's merge). A retype - that builds a new element records it as the old one's successor - (`Doc.replaceElement` -> `replaced`), so the entries an earlier op made - for the old element still follow its regenerated id under declared - conventions (`[add, set doc=, retype]` names `userTask_checkInvoice` - everywhere, as on main). The file was always right. -- **One command per flow for a splicing `add --flow-id` with ids kept** - (`Doc.preferFlowId`, `KeptFlow.preferred`, `flowIdIgnoredHint`): - `W_OPTION_IGNORED` named `bpmn set F id=` and - `W_FLOW_ID_STALE` `bpmn set F id=`; following both failed - (E_NOT_FOUND). Where the splice kept the flow, add records the given id - with the node the flow then ends at; `staleFlowWarning` suggests it while - the flow still ends there and the id is valid and free (else the speaking - id), and the `W_OPTION_IGNORED` hint says `W_FLOW_ID_STALE names the - command that renames F`. Where no W_FLOW_ID_STALE comes for that flow (its - id still names its new ends, a later op put it back or renamed it), - `reportStaleFlows` gives the hint its own command back, or none where the - flow no longer has that id or the `--flow-id` is taken. Default mode and a - flow that was not kept (hashed, or created in the batch): unchanged. -- Docs: guide (`W_FLOW_ID_STALE`, `W_OPTION_IGNORED`, the `--flow` rule, - `kinds --section ids` renames; `guide --short` unchanged, 4,644 bytes), - reference.md (Ids, flow options), ops.md, migrating.md. - -Evidence: - -- `npm run gate` (rebased on #55, #52 and #53): 2,104 tests (+246 opt-in; - 2,090 on main; 14 new in keep-flow-ids, drawn-ids, speaking-ids, - conventions-ids and conventions-declared, 11 of them fail on main, 3 - guards pass on both: a renamed end changes nothing else, the `--flow-id` - fallbacks, and every entry names the final id after `[add, change, - retype]` under declared conventions), isomorphism check, layout - regression 444, fuzz 12 x 15: 173 ok, 7 refused, 0 errors. -- Against main, a build of each, the same seeded batches (the fuzzer's - semantic steps, a `set id=` before a quarter of them, a second step chained - to almost a third; with and without ids kept, every third step with the - layout too) on tools/scenarios and test/fixtures: 164 files, 1,968 - batches, 5,248 comparisons. 5,017 identical, 181 refused alike, 50 differ, - each only in flow ids (by default the same file once the new ids are mapped - back, with ids kept the same file and result plus W_FLOW_ID_STALE): 32 - where an end got another id in an earlier command and its name still - names the flow (16 renamed, 16 reported), 18 where it got it earlier in - the batch. None of the 164 files as read has a flow the wider test reads - differently. `tools/speaking-ids.mjs` on the same files: identical output - (18 renamed flows on both); `layout-regress --compare`: 0 better, 0 worse; - `tools/roundtrip.mjs`: identical. -- With the conventions of a bare element-id-naming: seeded batches on - tools/scenarios (920, 166 with a retype; 1,840 comparisons) differ in 2, - both a `set name=` earlier in the batch (renamed / reported); `[add, set - name=, retype]` after the first task of each of 112 files gives the same - result as main in all of them. -- With ids kept, the same 1,968 batches (`set id=` or `name=` before 30 %): - no E_INTERNAL, no flow id changed, one rename command per flow, every - W_FLOW_ID_STALE command can be followed (295 hints; 281 on main). 4 - followed ids no longer name their ends on the next read (3 on main): the - mixed-style drift in #32's Left out (polish-*.bpmn, `f` and `Flow_`); the - fourth is a flow only this branch reports. -- Bundle (rebased on #55, #52 and #53): whole entry 927.5 / 295.2 -> 930.1 / - 296.1 KB (minified / gzip), `applyToXml` only 654.3 / 208.3 -> 656.1 / - 209.0 KB, the table in `docs/library.md` too; budgets unchanged. -- Runtime: by default `idNamesEnds` makes the same `derivedBases` calls as - before where an end's name gives its id's words (one `slugify` more per - named end); only an end with a different name, or one `set` renamed - earlier in the batch, adds a label or an id to try. Under a machine load - of 100 to 190 (other worktrees), medians of 15 interleaved runs, base -> - branch: 60 splices on a 200-flow chain 206 -> 203 ms following, 211 -> 212 - ms with ids kept, 30 `set id=` + splice pairs 147 -> 150 ms, the 60 - splices through `applyToXml` 271 -> 269 ms. - -Left out: `set name=` on an end whose id says nothing, followed by an end -change in a later command, and `set id=` on an end without a name (or whose -name gives no id words) followed by an end change in a later command: -nothing records the old name or id across commands, so such a flow keeps -its id without a word (by default too); a retype or trigger change that -changes an unnamed end's kind word (`Task` -> `UserTask`) before an end -change in the same batch (not recorded; only `set id=` / `name=` are). - -## The E_VALIDATION advice reads the refusal (2026-10-13, `fix/e-validation-hint`) - -An open point after #35, and wider than its note said there: the -advice after an `E_VALIDATION` refusal sent the reader to `bpmn validate -`. A refusal writes nothing, so validate runs on the unchanged file -and shows none of the refused errors (`bpmn new; add task; add participant -Customer --black-box`: refused with `E_EMPTY_COLLABORATION` and -`E_ORPHAN_PROCESS`, then `validate` says `valid, 3 warning(s)`, exit 0). An -agent that followed it could take the refusal for spurious. A host -validator's errors never show in the CLI at all. - -- Text only, no code, id or output other than these: the catalogue's - `E_VALIDATION` fix (`bpmn guide` COMMON ERRORS, `guide errors`, `bpmn - kinds`, `kinds --json`, `ERROR_CATALOGUE`) says to read the errors in the - refusal (the lines under it, JSON `error.errors`, library - `err.details.errors`) and that `bpmn validate ` does not show them; - the `guide --short` line (4,644 of 5,120 bytes); the hint of a structural - refusal (`src/pipeline.ts`) is now the validator refusal's without its - brackets; the plugin skill's error table. The guide's error output - (`guide commands`) and `docs/reference.md` list `errors` in the JSON - error and the lines under a refusal (the guide also the `command` / - `ops` it left out), `docs/library.md` lists it in `CliError.details`, and - `docs/validators.md` says that a check of the unchanged document does not - show them. -- Tests (`test/validators.test.ts`): the black-box refusal (text and JSON - hint, file unchanged, validate exit 0 without the refused codes, the hint - equal to `validatorRefusal`'s), and that no sentence of the catalogue fix, - the short-guide line or the skill row names `bpmn validate` except to say - that it does not show them, with `errors?` in the guide's JSON error. - Both fail on main; `docs/testing.md` describes them. -- Differential against main (1,769 CLI runs on the scenarios: guide, every - topic and `--help`, kinds, refusals, forced and plain writes): only these - texts differ, each a pure substitution (209 structural refusals in text - and in JSON, 4 catalogue texts, `kinds --json`, the short line, the - error output in `guide` and `guide commands`); written - files, exit codes, ids, speaking-ids, layout-regress and roundtrip - identical. -- `npm run gate` (rebased on #55 and #52): 2,090 tests (+246 opt-in), - isomorphism check (the whole entry 927.0 / 295.0 -> 927.5 / 295.2 KB, - `applyToXml` only 654.2 / 208.3 -> 654.3 / 208.3 KB, the table in - `docs/library.md` too, no budget raised), layout regression 444 (budget - 444), fuzz 12 x 15 0 errors (the 4 group warnings in one walk of main). - -## Decision links in the file's spelling (2026-10-13, `fix/42-decision-link-spelling`, issue #42) - -With the library's `platform: 'none'` (design-iq's live host passes it for -a document without an executable process), `set calledDecision=` -replaced a link in an engine spelling with the unprefixed attribute: -`camunda:decisionRef` became `calledDecision`, a `zeebe:calledDecision` lost -its `resultVariable`, and a new link next to engine links was unprefixed, so -one file mixed spellings. #42 settles it as written. Auto-detection never -gives none for such a file (a link in an engine spelling makes it Camunda 7 -/ 8), so the CLI's `set` is unchanged: it converts as before, and a file -without engine content gets the unprefixed attribute as before. The docs -(migrating.md, reference.md, the guide) say so. - -- **Spelling** (`src/ops/decision.ts`, `setDecisionLink`, platform none - only): a task whose link has an engine spelling keeps it - (`camunda:decisionRef` / `operaton:decisionRef` updated in place, the one - the readers read first when a task has two; a `zeebe:calledDecision` gets - the `decisionId` and keeps its `resultVariable`, no - `W_DECISION_RESULT_VARIABLE`). The own link is the one `decisionLinkOf` - reads: a `zeebe:calledDecision` without a `decisionId` is none, so such a - task follows the file (the element goes, or with a zeebe file it takes the - `decisionId` and keeps its `resultVariable`), and next to an own unprefixed - link it stays unprefixed. A task without a link takes the engine - spelling of the file's other links (every business rule task, in - sub-processes and other processes too) when they all have one and the - same, with a namespace that resolves (`fileEngineSpelling`, one walk over - the BPMN tree, only on this path; written with the file's prefix). - Otherwise, also on a task whose own link is unprefixed (`calledDecision`, - or a hand-written `calledElement`, which is still replaced; #42 does not - list it), the unprefixed `calledDecision` as before. The Camunda 7 / 8 - branches share the writers (`writeC7`, `writeZeebe`) with unchanged - output; reading (`decisionLinkOf`) is unchanged. -- **Whose spelling**: the unprefixed `calledDecision` is bpmn-cli's own - spelling for a model without an engine. The comments (decision.ts, - set.ts, c7.ts, view.ts, bpmnlint.ts), the guide (the `set` summary, the - `calledDecision` set key of `bpmn kinds`, the fix of - `W_C7_DEPLOY_SCHEMA`), the bpmnlint import-warning hint, - docs/bpmnlint.md, docs/reference.md (Set keys: the spellings), - docs/library.md (`platform`), docs/testing.md and docs/migrating.md say - so, and none names design-iq as the reason. docs/audit-2026-10.md keeps - its historical wording. -- **Tests**: `test/decision-link.test.ts` 14 -> 30 cases (each acceptance - line of #42, the follow-up probes, a `zeebe:calledDecision` without - `decisionId`, the CLI's path with platform auto converting as before, and - a check that no guide entry about the link names design-iq); 11 of them - and the bpmnlint hint case fail on main. `test/c7-followups-integration.test.ts` and `test/c8-profile.test.ts`: - a comment and a case label only. -- **Differential** against main (in-process: `set calledDecision=` and - `calledDecision=` on every business rule task, an added business rule - task with a link after the first task, platform auto and none; 109 public - files and 197 private ones, counts only): with auto (the CLI's path) all - identical; with none 8 of 798 runs differ, in 4 files with engine links, - each a kept own spelling (4) or a new link that follows the file's single - spelling (4). The 47 private files with unprefixed links: identical. -- `npm run gate`: 2,088 tests (+246 opt-in), isomorphism check (whole - entry 925.9 / 294.6 -> 927.0 / 295.0 KB, `applyToXml` only 653.3 / 208.0 - -> 654.2 / 208.3 KB, budgets unchanged), layout regression 444 (budget - 444), fuzz 12 x 15 0 errors. `guide --short` unchanged (4,638 bytes). - -Open after #42: - -- `ext add` of another platform's extension warns nothing (the other - upstream ask of design-iq's ADR 0009): an issue of its own. - -## @miragon/bpmnlint-plugin-rules 0.12.1 (2026-10-13, `fix/bpmnlint-rules-0.12.1`) - -The plugin is pinned to 0.12.1 (was 0.12.0; bpmnlint stays 11.15.0). The -lockfile changes only the plugin's own entry (version, tarball, integrity, -its peer range `bpmnlint >=11.14.0`); its dependencies are the same. - -- **`case: "toString"`**: element-id-naming no longer throws on a case named - after a key of `Object.prototype` (upstream #79; `toString`, `__proto__`); - it checks the fallback, PascalCase, like any unknown case. - `idConventionsFromLint` gives that case the note of every unknown case - (`case "toString" is unknown: the rule checks PascalCase, and new ids take - it`). A `resolveCase` answer without a known label comes only from helpers - a host injects from a plugin before 0.12.1 (Object.prototype's property: a - function for `toString`, Object.prototype itself for `__proto__`); its rule - cannot check the case (``, or W_LINT_FAILED on an id with - the prefix), so that path keeps a note of its own: `is unknown, and the - rule of a @miragon/bpmnlint-plugin-rules before 0.12.1 cannot check it: new - ids take PascalCase, the fallback for an unknown case`. The guide - (`W_CONFIG_IGNORED`) and docs/bpmnlint.md give the unknown-case wording, - docs/library.md the older plugin's limit; `test/conventions-lint.test.ts` pins both notes (the older helpers by a - stub) and lints `userTask_checkInvoice` under `toString` and `__proto__` - with the real rule (one PascalCase finding each). -- **ESM build**: it loads in plain Node now (upstream #78). bpmn-cli keeps - `createRequire` (the CJS build): `bpmnlintEngine()` is synchronous, and - Linter and NodeResolver are required from the plugin's own location, so - they are the bpmnlint the plugin's rules import, and the shipped tier - resolves from inside the plugin. The module header of - `src/node/bpmnlint.ts`, docs/library.md, docs/testing.md and the header of - `test/bpmnlint.test.ts` give that reason instead of the broken ESM build. -- **Peer range `bpmnlint >=11.14.0`** changes where npm puts the plugin: with - a project bpmnlint older than 11.14, npm no longer hoists the plugin next - to it but keeps it under bpmn-cli, next to bpmn-cli's own bpmnlint 11.15.0, - and lint runs (checked with npm 12.0.2: project `bpmnlint 11.0.0`, packed - CLI; the 0.12.0 pin gave W_LINT_UNAVAILABLE, the 0.12.1 pin `4 error(s), 14 - warning(s)`). The old hoisted bpmnlint, and its W_LINT_UNAVAILABLE, is left - for `--legacy-peer-deps` (the plugin is hoisted again) and package managers - that ignore peers. docs/bpmnlint.md ("A hoisted bpmnlint"), the guide's - `W_LINT_UNAVAILABLE` and the module header of `src/node/bpmnlint.ts` say - so; the tests that build such a layout by hand are unchanged. -- Of the upstream items listed under #33 below, 0.12.1 settles the ESM - build, the peer range, the rule name in the doc comments and the README's - `.bpmnlintrc` examples (strict JSON now, `json` blocks that `JSON.parse` - reads). The bundle with only camunda-compat 7-24 / 8-10 is now documented - upstream as intended (README and `createBundledResolver`'s doc comment: - chain bpmnlint's NodeResolver behind it for older engines), which is what - bpmn-cli's shipped tier does; nothing of that list is open. -- Version mentions: CONTRIBUTING.md, docs/bpmnlint.md, docs/migrating.md - (the 0.7 dependency line; the toString note was never released, so no - migration line of its own). The broken-plugin stub in - `test/bpmnlint.test.ts` keeps `version: '0.12.0'` (any version; it throws - on load). - -## Id conventions of the .bpmnlintrc (2026-10-13, `feat/34-id-conventions`, issue #34) - -bpmn-cli learned the id style from the file alone. A new file got bpmn-js ids, -a kind the file had never used fell back to the default prefix, and in a -repository whose `.bpmnlintrc` runs `@miragon/rules/element-id-naming` -(`recommended-for-automation` turns it on) every write added findings of that -rule, which #33 then listed. Now the ids bpmn-cli generates follow the rule -where it declares a prefix ([docs/bpmnlint.md](docs/bpmnlint.md#id-conventions)). -The user's decisions hold: the conventions come only from the resolved -setting of element-id-naming, read as the rule reads it, with the plugin's -own naming helpers; no `.bpmn-cli.json` and no other config file (no topic -or message name templates either); generated ids fill the -gap a change opens, existing ids are never renamed for the conventions; a -value new ids cannot follow never blocks a command. - -- **Core** (`src/conventions.ts`, browser-safe; the naming helpers are - injected): `idConventionsFromLint(setting, naming, { source })` merges the - options over `DEFAULT_PREFIXES` as the rule does, runs `resolvePrefix` on - an element of every concrete `bpmn:` type of the moddle registry (120 - types; 23 hits for the bare flag, so `bpmn:Definitions`, `bpmn:DataStore` - and the rest are covered when named exactly) and `acceptedPrefixes` on an - event of each kind and trigger (32 qualified prefixes), and gives - `IdConventions` (plain data: prefixes, `case`, `qualifier`, `source`, - `ignored`) plus `W_CONFIG_IGNORED` notes: a prefix that is no string, - cannot start an id, has a character outside ASCII or is longer than 56 - characters (left out: that type follows the learned style; `ignored` - names the concrete types the rule still checks with it), under - `required` a qualified prefix longer than 56 characters (`ignored` names - `bpmn:EndEvent:escalation`), an unknown case (the rule's PascalCase; also - `toString` and other names of JavaScript's Object, which the rule cannot - check) or qualifier (optional; the hint's guess is `closestKey` of - `src/batch.ts`). - DI types are out of scope. `conventionsView` / `renderConventions` are - `show --conventions`: rows grouped by pattern and source (`lint`, `file`, - `lint shape`, `default`), `_` for prefixes named after their kind, - `not checked` or `checked; prefix ignored (W_CONFIG_IGNORED)` for kinds - the rule names nothing new ids can follow, the flow head new flows really - get (`IdStyle.explainFlow`: `F_` after `F1`..`F12`, `Flow_` after a prefix - that leaves no room), one line for the qualified prefixes; where none are - declared, why (`undeclared`: `--no-lint-config`, no `.bpmnlintrc`, one - that did not load, the rule off, no usable prefix). -- **Ids** (`src/idstyle.ts`, DECLARED CONVENTIONS in the module contract): a - request whose type has a declared prefix takes it (the qualified one only - under `required`) and a body in the declared case; the kind word drops - what the prefix says (`startEvent_timer1stOfMonth`), a number first gets - the kind word under camelCase / PascalCase, where the collision suffix is - glued (`serviceTask_checkInvoice2`) and a join is `Join` (snake_case - `_join`, also in a file of bare ids; after an explicit split id the - declared prefix does not start: that prefix and the gateway's label, - `gateway_checkJoin`; after one whose body is not in the declared case, - `CASE_BODY`, the rule's patterns: that prefix and the body's words, - `gateway_approvedJoin` for `gateway_Approved`). Flows take - the declared prefix in the `named` form (the file's scope segment left - out of the ends, `From` before a number); message flows an exact - `bpmn:MessageFlow` entry, else the style the file's own message flows - show (`flowVoted`: two that agree; the ones under the borrowed prefix do - not vote, so the second message flow no longer flips to `Flow_`), else - the sequence flows' prefix and style. The learner runs twice: `l` without the ids that - already start with a prefix the conventions accept for their element - (`conforms`; the drift fix: a PascalCase file keeps `Lane_TeamA` .. - `Lane_TeamH` over eight writes, and its `Activity_CheckOrder` still - votes), `all` over every id (`learnedOnly()`: rename recognition through - `derivedBases`, and the comparison behind the `conventions:` line). - Uncovered types take the declared case only where `l` shows none (single - lower-case words show none there: `lane_sales`, then `lane_backOffice`; - under a declared PascalCase, which they cannot be, they vote camel) - and its prefixes do not begin with the other letter (`otherShape`: a - Modeler file of `Participant_` and hashed `Activity_` ids keeps - PascalCase), the declared prefixes' shape only where `l` shows none. - Process and definitions ids are bpmn-cli's (`IdStyle.DEFAULT`: - `Process_`, also a new pool's process, never learned from the - lint-shaped `participant_` ids bpmn-cli wrote) unless an exact - `bpmn:Process` / `bpmn:Definitions` entry exists (`process_main` and - `definitions_main` without a name). Under `required` an event whose - qualified prefix is too long (`IdConventions.ignored`: - `bpmn:EndEvent:escalation`, one W_CONFIG_IGNORED) follows the learned - style, and `headsOf` accepts nothing for it. `stalePrefix` for - W_ID_PREFIX_STALE: the declared prefix of `to` on the rest of the id, or, - where nothing is declared for `from` (a type switched off, an ignored or - too long qualified prefix: the rule did not check the id before), the id - the element's request wants now (its name, else its id's words as the - context). Without declared conventions none of it runs. -- **Doc, pipeline, ops**: `Doc.conventions` (setting other ones learns the - style again), `NewDocOptions` / `MutationOptions` / `EditOptions.conventions`; - `mutateDoc` sets them for the call and puts the caller's back. - `Doc.allocateId` records an id under a prefix the conventions declare - for the request (`explain(req).source === 'lint'`: not a lane, a pool or - a message flow that borrows the sequence flows' prefix) whose base - differs from what `learnedOnly()` gives (also a split's join and an exact - process or definitions prefix of `new`, each only where the rule names its - type); the result's `conventions: { ids, source }`, a - `conventions:` line in text and `--summary`. `retype` and trigger changes - record the element's state before them (`recordIdState` in `ops/set.ts`, - the first record of an id wins); `reportStaleIdPrefixes` builds - `W_ID_PREFIX_STALE` at the end of the op on its own or of the batch, like - W_FLOW_ID_STALE: once per element, from its state before the batch to the - one after, the `bpmn set id=` free after the batch and - among the hints; none for an element a later op renamed or made fit. An - id the batch generated (`Doc.generatedBase`; a split's join too, - `markGenerated`; an explicit claim, `claimId`, takes an id out again, also - one an op freed) is nobody's API yet: a retype or trigger change gives - the element the new prefix on the id it wanted (`regeneratedId`, a suffix - where taken, W_ID_SUFFIXED), a rename with `Rename.regenerated` that the - entries' details, the notes and the warnings follow (its old - W_ID_SUFFIXED goes; a later op that names the old id gets an E_NOT_FOUND - that says so), and no warning. The sequence and message flows the batch - generated from its ends follow it (`flowsNamedAfter` before the rename, - `followRenamedEnd` after: `SFGoToSTCheck` -> `SFGoToUTCheck`, - `flow_task_b_serviceTask_check` -> `flow_task_b_userTask_check`, - `flow_checkToCheck2` -> `flow_checkToCheck`; `Rename.was` keeps them out - of the retyped ones in E_NOT_FOUND). -- **Node** (`src/node/bpmnlint.ts`): `projectLint` gives the validator and - the conventions from one load (memoised per resolved configuration); - `projectIdConventions(file, opts)` for the CLI's `new` and `show` (its - notes add the configuration's W_LINT_UNAVAILABLE, and `unavailable` marks - a `.bpmnlintrc` that did not load, so `show --conventions` names it). The - notes go into the bpmnlint validator (`bpmnlintValidator(..., { notes })`), - so `validate` and `new` list them and a write only counts them, with - `N element-id-naming values ignored (W_CONFIG_IGNORED; ...)` in its - validator line. `fileContext` passes the conventions unless the host passes - `conventions` or a validator named bpmnlint; `validate -` gets them - through `checkLoadedDoc` (lookup from the working directory). - `LoadedBpmnlint.where` is new. -- **CLI**: `show --conventions` (with `--json`, `--lint-config`, - `--no-lint-config`, whose help says so and which are `E_USAGE` without - `--conventions`; a missing path shows a new model there, stdin looks up - from the working directory; an id or another view flag is `E_USAGE`); - `new` checks `--id` (`assertProcessId`) and an existing target - (`assertNewFile`, which `toFile` uses too) first, as main does without a - `.bpmnlintrc`, then reads the conventions before `Doc.create`; - `--no-lint-config` says it drops the conventions too. The view names the - triggers of an event type whose required qualified prefix is too long on - its row (`message, escalation: qualified prefix ignored, ...`) and in the - qualifier line (`not the triggers marked above`). -- **Docs and codes**: `W_ID_PREFIX_STALE`, `W_CONFIG_IGNORED`; guide section - and topic `conventions`, `ID_CONVENTIONS.declared`, one line in the short - guide (4,638 of 5,120 bytes, 4,522 before); docs/bpmnlint.md (section, the examples of #33 now - without element-id-naming findings, a `show --conventions` example, all - three pinned), reference.md (also the JSON field), commands.md (also the - guide topics), contract.md, library.md, ops.md (the join id), - validators.md (the engine defaults turn the rule on: the snippet of its - advisory validators passes `conventions` and `notes`, pinned and run for - c7 and c8), migrating.md, - testing.md, README, SKILL.md (run `bpmn show --conventions` before - the first change in a repository with a `.bpmnlintrc`; the `conventions:` - line and `W_ID_PREFIX_STALE`). -- **Tests**: `test/conventions-lint.test.ts` (the real rule on every id of a - new file under 8 settings and of 6 file styles under 8 more, also of - elements created and then retyped or given another trigger with the flows - named after them, a split's gateways retyped in the same batch, the join - after an explicit split id in another case, 0 findings; a - control with findings; the table, the notes, also for `toString`, a - prefix outside ASCII and a qualified prefix too long under `required`); - `test/conventions-declared.test.ts` (drift, what the rule does not name - in a file with one `Lane_`, hashes or only covered ids, single words, - qualifier, kind word, suffix and join (also after an explicit split id - and in a file of bare ids), message flows and new pools' processes over - many writes, an element created and retyped in one batch, the - `conventions:` line of `new`, scoped flows, renames into the - declared form (also of a flow 0.6 named in a file of another flow form, - #31's recognition), a host table, W_ID_PREFIX_STALE alone and in a batch - (also for a type switched off), the flows that follow a regenerated id, - the split's join, an id freed and claimed again, the - result line and what it leaves out, the view with its reasons, ignored - prefixes, unqualified triggers and flow heads); `test/bpmnlint.test.ts` (#33's expectations - updated; the lookup and its switches, the notes in validate / write / - `new` / stdin, every fixture byte for byte without the rule, the CLI: - `new` byte-identical in an automation repository and its own errors - before E_LINT_CONFIG, `show --conventions` with a broken `.bpmnlintrc` and - a rule name with a typo). - -Evidence: - -- `npm run gate`: 2072 tests (+246 opt-in; 2008 on main), isomorphism check, - layout regression 444 (budget 444), fuzz 12 x 15 with 0 errors. -- Against main (with #35, #31, #32 and #33), a build of each: every test - fixture (49) and `new` without one, under six setups (no `.bpmnlintrc`; - automation with element-id-naming off; the rule at severity 0 with - options; a `.bpmnlintrc` without the rule; recommended-for-modeling; - automation with the rule on and `--no-lint-config`), through `show`, - `validate`, 20 kinds of `add`, `connect`, `retype`, `set`, `remove`, - `move`, `layout`, `apply` and `new`: 16,302 command pairs, stdout, - stderr, exit code and every file in the directory identical (`bpmn show - --conventions` aside, which is new). -- The whole entry grows 899.2 / 285.2 -> 925.7 / 294.5 KB (minified / gzip; - the guide's text 7.2, `src/idstyle.ts` 7.0, `src/conventions.ts` 6.4, the - rest 5.9 KB minified); that does not fit main's budget of 910 / 289 (#33's - size + about 1 %), so it is raised to 935 / 298, the measured size + 1.0 / - 1.2 %. `applyToXml` only 641.2 / 203.8 -> 653.3 / 208.0 KB (the style 7.0, - `src/document.ts` 1.6, `src/ops/set.ts` 1.5, `src/ops/flows.ts` 0.7 KB, - the rest; the conventions module and the plugin's helpers are not in it); - that does not fit main's budget of 643 / 204 either (#35 lowered it to its - size), so it is raised to 660 / 210, the measured size + 1.0 %. -- Runtime without a `.bpmnlintrc`, or with the rule off: unchanged (the CLI's - `add` on orders.bpmn 97 -> 97 ms, gallery.bpmn 151 -> 151 ms, main -> #34, - interleaved medians of 15). With an automation `.bpmnlintrc`: the table - takes 1.6 ms (3.3 ms the first time) once per loaded configuration (the - plugin load is #33's); learning the style twice takes gallery.bpmn's 140 - ids from 0.7 to 1.5 ms (the check whether an id follows the conventions is - one prefix test per element); a write compares each new id under a - declared prefix with the file's own style once, and W_ID_PREFIX_STALE is - built once per retyped element after the batch. The CLI's `add` 133 -> 139 - ms (orders.bpmn) and 191 -> 194 ms (gallery.bpmn), `validate` 100 -> 102 ms - / 128 -> 131 ms. - -Open after #34: - -- Topic and message names: Miragon/bpmnlint-rules#68. -- DI ids (`bpmndi:*`) follow their element, also where the rule names a DI - type. A prefix declared for one is a `W_CONFIG_IGNORED` since - `fix/conventions-followups` (above). -- `mutateDoc` accepts a split's `branches` without `nodes` silently (the CLI - refuses them): a library input check, not #34. Fixed in - `fix/batch-alias-retype-split-input` (above). -- The `conventions:` line compares with the file's own style, which needs - two ids per rule: a file that follows the rule gets it for its first ids - and for a kind it has no id of yet. Leaving such ids out would need the - learned style to say where it has no evidence of its own. Fixed in - `fix/conventions-followups` (above): only a file with ids the rule - rejects gets the line. - -Left for later (low findings of the second review, not fixed in #34): - -- A glued collision suffix after a body that ends in a number reads as another number (`serviceTask_step1` + `2` = `serviceTask_step12`, which then pushes the real "Step 12" to `serviceTask_step122`); unique and passes the rule, needs a letter word before the number (`step1No2`) with derivedBases / idNamesEnds following it. -- W_ID_PREFIX_STALE keeps the old id's body as it is: an id that never passed the declared case (`serviceTask_shipGoods` under PascalCase) gets a suggestion the rule still rejects, and a prefix without separator matched by accident (`user` on `userTask_checkOrder`) a garbled one (`manualTask_Task_checkOrder`); fire only for ids that fully passed the rule, or re-case the body. Fixed in `fix/conventions-followups` (above): such an id gets the id of what the element is now. -- Pre-existing (also on main): an op that touches a sequence flow without id crashes with E_INTERNAL (`idNamesEnds` on `undefined`); the change report names a created flow by the id its spliced flow just freed (`ChangeSet.rename` renames every entry with that id); a batch alias used after `retype` of its element is E_NOT_FOUND (swapType makes a new element; fixed in `fix/batch-alias-retype-split-input`, above). Separate issue. - -Left for later (low findings of the review after the rebase onto main, not fixed in #34): - -- Ids the batch built from a regenerated element's id other than flows keep its old stem: an unnamed element after it (`task_AfterSTCheck` after `UTCheck`), a boundary event on it (`event_TimerOnSTCheck`). Only the flows named after it follow; the rest would need the requests (context, anchor) of what the batch created. -- Message flows that follow the file's own `stemTo` / `stemSnake` form copy the ends' id stems as they are, so one id can mix cases (`mf_askQuestion_to_getQuestion` in a snake file under camelCase). The rule does not check message flow ids; re-casing the stems needs the form learner (`flowForms`) to read the re-cased ids as that form too, or the file's form flips. -- A new pool's process takes `Process_` as specified, also where the file's processes show a convention of their own (`process_customer`, `process_shop`: `Process_BankAccount`, `process_bankAccount` without a `.bpmnlintrc`). Following the file's process ids where at least two share a prefix would keep such files consistent; the rule does not check processes. Fixed in `fix/conventions-followups` (above), in both modes. -- The batch alias after a type-changing `retype` (above, also on main): a fix (rebind the alias to the element `swapType` makes) changes the output without a `.bpmnlintrc`, so it belongs in a PR of its own. Fixed in `fix/batch-alias-retype-split-input` (above). - -## The project's bpmnlint configuration (2026-10-13, `feat/33-bpmnlint`, issue #33) - -A team that runs bpmnlint in CI and in a pre-commit hook (`bpmnlint:recommended`, -camunda-compat, `@miragon/rules`, a plugin of its own) got its findings only -after the agent's loop: a change could be valid for bpmn-cli and still fail -the lint gate. Now `bpmn validate` and every write run the nearest -`.bpmnlintrc` ([docs/bpmnlint.md](docs/bpmnlint.md)). The user's decisions -hold: built on Miragon/bpmnlint-rules (`bpmnlint` 11.15.0 and -`@miragon/bpmnlint-plugin-rules` 0.12.0, exact runtime dependencies), the -findings a change adds are reported and never block (library hosts may opt -into blocking, `lintBlocks`), validate exits 2 on bpmnlint errors, no -`.bpmnlintrc` loads nothing, Node 22.12+, the lookup stops at the repository -root (outside a repository it looks only in the model's directory). - -- **Core** (`src/bpmnlint.ts`, browser-safe: bpmnlint is injected as `{ - Linter, resolver }`): `resolveBpmnlintConfig` flattens `extends` like - bpmnlint's Linter but leaves out and lists what it cannot resolve (a name - bpmnlint cannot parse costs only that entry; async resolvers are awaited; - an `extends` cycle is walked once, one `W_LINT_UNAVAILABLE` names the - configs that close it, where a cycle with fan-out ran out of memory - before; a moddle value that is neither a path nor an object is listed); - `bpmnlintValidator` lints with a new Linter per run (stateful rules) on the - XML parsed with the `.bpmnlintrc`'s moddle extensions only, keys findings - without array indices (an earlier sibling removed changes nothing), turns a - throwing rule or factory into one `W_LINT_FAILED` (nothing on stderr; an - async check that rejects and a factory that returns a promise too, never - an unhandled rejection), reports the lint parse's import warnings - bpmn-cli's reader does not (`import-warning`, with a hint; design-iq's - decision link has its own), and - checks the list rules (`no-implicit-end`, `no-implicit-start`, - `no-disconnected`, `event-based-gateway`, which counts the outgoing list) - against the flows: a finding the flows contradict (a file without incoming - / outgoing lists) is left out of a write and carries a hint in validate. A - moddle module path the host resolved nothing for is `W_LINT_UNAVAILABLE`, - not dropped. `BPMNLINT_COVERS`: `W_NO_START`, `W_NO_END`, `W_DEAD_END` are - dropped where the rule reports on the same element, the finding takes - their hint. -- **Node** (`src/node/bpmnlint.ts`): the lookup is `findUp` - (`src/node/find-up.ts`, from #30's closed branch; `gitRoot` added); bpmnlint and the plugin load with `createRequire` (the plugin's ESM - build fails in plain Node), Linter and NodeResolver from the plugin's own - bpmnlint, a load failure is one `W_LINT_UNAVAILABLE` and the write goes - ahead; the chain asks the bundled resolver, then the camunda-compat 2.61.1 - the plugin installs (decision Q1: every camunda-compat config offline), - then the repository's `node_modules` (only with `lintProjectCode`, which - the CLI passes; only inside a git repository of the user running - bpmn-cli, checked by owner as git checks it (`repoOff`: lstat of the - directory, of the `.git` itself and of a `.git` file's gitdir; root's - files only for root, the SUDO_UID sudo sets under sudo; git's - `safe.directory` is not read; a `.git` file that names no git directory - is no repository, and the message says so; where there are no uids - (Windows) only a repository inside the home directory and below no - filesystem root counts, so a planted `C:\.git` with `C:\node_modules` - runs nothing), so a planted `/tmp/.bpmnlintrc` and - `/tmp/node_modules`, with or without a `/tmp/.git` (also a link to `/` or - a hard link to a file of root), run no plugin; entry modules inside the - repository root only; a package whose config came from there gives its - rules from there); moddle paths are read as JSON, never executed. The - owner gates only this tier (decision after the third review): the lookup - goes up to the root of any git repository, whoever owns it (a `.git` - file that names no git directory makes none, as for git; a `.git` that - is no regular file or is over 1 MiB is not read, so a planted FIFO - cannot block it). Root over another uid's checkout (a CI `container:` - job, `docker run -v`, `sudo`) and on Windows a repository outside the - home directory (`C:\dev`) get the repository's `.bpmnlintrc` with the - bundled and shipped rules, and a project plugin there is left out with - the visible `W_LINT_UNAVAILABLE` that names the reason and the way out; - gating the lookup had made lint vanish silently there. A planted - `/tmp/.git` with `/tmp/.bpmnlintrc` now applies to models below `/tmp` - that lie in no repository of their own, but only with bpmn-cli's rules - (advisory) and moddle JSON read from inside `/tmp`. Outside a repository - the lookup takes only the model's own directory; a library host without - `lintProjectCode` is told when the setting would not help (no repository, - another user's), and is not told to install a package it would not run. - A string `moddleExtensions` is one `W_LINT_UNAVAILABLE`, not one per - character. A write's validator line says `N left out (W_LINT_UNAVAILABLE; - ...)`, since a write only counts those. - `projectBpmnlintSettings` (for #34) uses the same lookup, flags and errors, - so `--no-lint-config` switches off what #34 reads too (decision Q5). `bpmnlintEngine()` for library hosts; the chain - itself is not exported (`loadBpmnlint(...).engine.resolver` is it). -- **Validator hook** (`src/validators.ts`, `src/pipeline.ts`): - `NamedValidator.blocks: false` (advisory: introduced errors among the - warnings with `severity: 'error'`, old errors only counted, `blocks: false` - in the report); two validators of one name compare in order; `new` lists - an advisory validator's findings of the new model; a resolved lint warning - a resolved validator finding covers is listed once (a host validator with - `covers`: `resolved:` no longer lists `W_DEAD_END` next to the finding - that covers it). docs/validators.md has all of it (Advisory validators). -- **Output** (`src/report.ts`): `warning [bpmnlint error] ` for an - error a write added, `[bpmnlint error] ` in validate; an - advisory validator's findings group by code and message (decision Q3); its - line adds `(N added, M resolved, K already in the file)`, and `--summary` - counts K in its `warnings:` line; validate folds three or more findings of - a rule with one hint into one line that lists the ids with the message it - shows (a real dead end among list findings keeps its line; - `W_LINT_UNAVAILABLE`, `W_LINT_FAILED` and `import-warning` never fold; - gallery.bpmn: 142 findings in 16 lines; `--json` lists all). -- **CLI**: `--lint-config ` / `--no-lint-config` on validate and every - mutating command; a write looks up from the file it writes (`-o`), a - model from stdin without `-o` from the working directory (a removed - working directory: no `lintFrom`, absolute paths still run). Exit codes - are set with `process.exitCode` (validate's 2 and 5, a write's `--strict` - 5, every error), and the CLI exits once stdout and stderr took what was - written (`exitWhenWritten`): `process.exit` right away cut piped output - off at 64 KB on macOS, which a `.bpmnlintrc` made common (`validate - --json` exits 2 on its errors), and waiting for the event loop let a - project plugin's timer keep the process alive. `EPIPE` on stdout or - stderr (`| head`) is ignored, so the exit code stays and no trace is - printed (the audit's open `EPIPE`, now also on the success path). -- **Pre-existing, fixed on the way** (the third review found them): stdin is - read as a stream (once commander's `node:process` import touched - `process.stdin`, a pipe was non-blocking and `readFileSync(0)` failed with - `EAGAIN` whenever its writer was slower than the start: `curl ... | bpmn - validate -`); `writeAtomic` turns a failed write into `E_IO` (`-o` naming - a directory: "it is a directory", exit 4, was `E_INTERNAL` 70) and leaves - no temp file. -- **Docs and codes**: `W_LINT_UNAVAILABLE`, `W_LINT_FAILED`, `E_LINT_CONFIG`; - guide section and topic `bpmnlint` (not `lint`, which names bpmn-cli's own - `W_*` checks); docs/bpmnlint.md, and the advisory validators with the - library example of bpmnlint in docs/validators.md; SKILL.md tells the - agent to fix added findings in the same batch and to pass - `--no-lint-config` in a repository or folder the user does not trust (a - project plugin runs under the skill's pre-approved `bpmn` command); the - short guide's CONTRACT says bpmnlint only reports. -- **Node floor**: `engines.node` `>=22.12.0` (commander 15 and vitest 5 need - it), a CI job builds and tests on 22.12, every Node 20 mention is updated. -- **Tests and tools**: `test/bpmnlint.test.ts` (54 cases: a model outside a - repository, a `.git` of another uid (its `.bpmnlintrc` applies, its plugin - does not run), also a planted link to `/`, a hard link to a gitfile and - to `/etc/hosts` (skipped as root and on Windows), a `.git` file without - its git directory, a system without uids (Windows, simulated; also a - repository outside the home directory), async rules, extends cycles with - fan-out; the CLI cases run a tsc build of `src/` under plain node, also - root over another uid's checkout (a preload makes `process.getuid()` 0: - exit 2 on a bundled rule, the plugin named), a broken install, a hoisted - old bpmnlint, JSON beyond 64 KB through a pipe, a pipe closed after the - first chunk, a plugin that leaves a timer running, a removed working - directory and the `-o` lookup); `test/cli.test.ts` a slow stdin writer - and `-o` naming a directory; `test/global-setup.ts` stops the run when a - `.bpmnlintrc` lies above the temporary directory or the repository; fuzz, - bench (feature detected) and the layout regression pass - `--no-lint-config`. - -Rebased onto main after #35 (design profile removed), #31 (id cutting) and -#32 (flow ids kept), squashed into one commit (the branch before: -`pr39-before-rebase`): - -- #35's removal is final: no `--profile`, `profile` / `contentRepo`, design - validator or `docs/design-iq.md` came back. What #33 said about the design - profile went: `--profile none` keeping bpmnlint (docs/bpmnlint.md, the - guide's BPMNLINT section), the E_VALIDATION hint and catalogue text about - `--profile none`, the design line of the validator-findings note in the - guide's OUTPUT. The resolved-once behaviour stays as a rule of the hook - (`covers`), and its test uses the host validator `gate` of - test/validators.test.ts instead of the design profile. -- The advisory validators (`blocks`, the same-name pairing, `covers` - passing its hint, `advisoryValidators` for `renderSummary`) are in - docs/validators.md, with the bpmnlint example that was in - docs/design-iq.md (test/bpmnlint.test.ts reads it there); docs/library.md - links to them. -- The decision-link hint of an `import-warning` no longer names - `W_DESIGN_DECISION_LINK` (gone with #35); it says what the link is. -- #32 stays as it is: `BPMN_KEEP_FLOW_IDS` / `envSwitch` in src/cli.ts next - to `lintOptions` / `workingDir`, `keepFlowIds` and the lint options in - `mutationOptions`; vitest has `setupFiles` (test/setup.ts) and - `globalSetup` (test/global-setup.ts); the layout regression passes - `--no-lint-config` and `CLI_ENV`, the fuzz and bench executors both. - The hints and catalogue entries #33 added name the file first or name no - command. - -Evidence: - -- `npm run gate` (on main after #35, #31 and #32): 2008 tests (+246 - opt-in), isomorphism check, layout regression 444 (budget 444), fuzz 12 x - 15 0 errors. The whole entry grows 880.9 / 278.6 (main) -> 899.2 / 285.2 - KB (+18.3 / +6.6 KB, as before the rebase: the validator about 8 KB with - its hints, the guide's bpmnlint text and codes about 8 KB, bpmn-moddle - 10.3 and the advisory output of the report the rest). Main's budget of - 883 / 279 cannot hold that; it is raised to 910 / 289, the measured size - + about 1 %, as main keeps its budgets close. `applyToXml` only 640.4 / - 203.5 -> 641.2 / 203.8 KB, within main's 643 / 204 (unchanged). The guide - adds 124 to 153 KB (38 to 47 KB gzip) to `applyToXml` (docs/library.md - said 116 to 140; main itself measures 121 to 146 with this lock file). -- Runtime without a `.bpmnlintrc`: unchanged (one stat per directory up to - the repository root, or up to `/` outside one). The repository check of a - loaded `.bpmnlintrc` (the walk to `.git` and one stat) costs under 0.01 ms. With one (recommended, `@miragon/rules` - automation, camunda-compat 7-24, the camunda moddle): the CLI's validate - 76 -> 103 ms (orders.bpmn), 92 -> 131 ms (gallery.bpmn), a write 101 -> - 138 ms / 154 -> 198 ms (loading bpmnlint and the plugin is about 25 ms of - it); in process, warm, `mutateFile` +7.5 ms, `checkFile` +4 / +12 ms. -- Findings equal bpmnlint's own Linter on orders.bpmn and gallery.bpmn - (test). - -Open after #33: - -- Upstream, for Miragon/bpmnlint-rules (not filed): the ESM build does not - load in plain Node; the bundle has only camunda-compat 7-24 / 8-10; the - peer range should be `bpmnlint >=11.14` (an older hoisted bpmnlint cannot - load the plugin); the element-id-naming doc comment names - `miragon/element-id-naming`; the README's JSONC `.bpmnlintrc` examples are - rejected by `npx bpmnlint`. -- `bpmn layout` drops a group's BPMNShape (gallery.bpmn's - `Group_FirstTasks`): a separate bug, not #33. Fixed in - `fix/full-redraw-keeps-groups` (see "Group boxes over a full redraw"). -- `src/node/find-up.ts` and `test/find-up.test.ts` came from #30's branch - (`fix/30-designiq-yml`, PR #36, closed unmerged once #35 removed the - design profile): they are new files of this branch, and only the - `.bpmnlintrc` lookup uses them. #34 builds on `projectBpmnlintSettings` - and `bpmnlintEngine().plugin.naming`. - -Left for later (low findings of the third review, not fixed in #33): - -- `loadBpmnlint` caches what a `.bpmnlintrc` could not resolve: a long-running host keeps saying "is not installed" after the install, until it restarts or the file changes (documented now; a fix keys the cache on the involved package.json / descriptor mtimes, or does not cache project-side failures). -- A whole-model `W_LINT_FAILED` (bpmnlint could not lint the model, e.g. moddle descriptors BpmnModdle rejects) is the same before and after, so a write only counts it and its validator line does not say that no rule ran (validate lists it; its hint and the catalogue are corrected); descriptor objects are not checked up front. -- List-rule findings a write leaves out on a file without incoming / outgoing lists are not counted: the write's validator line can read `0 error(s)` while `bpmn validate` exits 2 (a `N left out (incoming / outgoing lists)` note needs a per-run count from the validator). -- The lookup walks the path as written: a model reached through a symlinked directory from outside the repository (`mlink -> repo/models`, `bpmn validate mlink/x.bpmn`) gets no repository `.bpmnlintrc`, while git, which starts from the physical directory, finds the repository; a realpath start would flip the opposite case (an in-repository link to a directory outside it), so pick one rule and document it. - -## Flow ids kept on request (2026-10-13, `feat/32-keep-flow-ids`, issue #32) - -A splice, a bridge, a move or `set source=` / `target=` renames a -flow whose id names its old ends. Where flow ids are API (bpmn-to-code -writes them into the generated process API, process tests name them), that -rename breaks code far from the model. The library could keep them -(`Doc.followFlowEnds = false`), but the CLI had no switch, and nothing said -which ids no longer name their ends. The decisions: one name at every -layer; flag > `BPMN_KEEP_FLOW_IDS` > library option / `Doc` field > default -(rename); no project config file (#34 reads id conventions from the -`.bpmnlintrc` and adds none); a strict environment value; the warning only -from the change that makes a flow stale. - -- **Switch** (`src/cli.ts`): `--keep-flow-ids` / `--no-keep-flow-ids` on - every mutating command and `apply` (`withMutationOptions`), else - `BPMN_KEEP_FLOW_IDS` (`envSwitch`: unset or empty = default; 1 / true / - yes / on and 0 / false / no / off in any case; anything else is `E_USAGE` - with a hint). Only `mutationOptions` reads it, so `show` and `validate` - never fail on it, and a flag decides without reading it. `src/cli.ts` is - still the only module that reads the environment. -- **Library**: `MutationOptions.keepFlowIds` and `EditOptions.keepFlowIds`. - `mutate()` sets `Doc.followFlowEnds` for the call and puts the caller's - value back in `finally` (an explicit option wins; undefined: the field). - `Doc.followFlowEnds = false` no longer keeps every id: a flow the same - call created follows its ends (migrating.md, library.md). -- **One test for a rename and a stale id** (`ops/flows.ts idNamesEnds`, - extracted from `followEnds`). With ids kept, `followEnds` records a flow - whose id named its old ends (`Doc.recordKeptFlow`; the first ends stay - while the flow has the id it kept, so a flow the batch renamed with `set - id=` takes that id and a later splice records the ends that id named) - instead of renaming it, but only a flow the document had before the batch - (`Doc.flowsBefore`, the sequence flows as elements, with their ends, when - `runBatch`, or a `runOp` outside a batch, starts: a flow an earlier op or - the op itself created is nobody's API and still follows; - `BatchContext.created` holds ids, which a later rename changes). - `runBatch` turns the kept flows into `W_FLOW_ID_STALE` once, after - `stillHolds`, against each flow's final ends (`reportStaleFlows`, - `staleFlowWarning`): one per flow, none for a flow a later op removed or - put back between its old ends, and none for a flow that ends where it did - when the batch began (a flow that was stale before: moving a node a splice - put in away and back, `move X --after C` then `move X --after A`, runs it - through the ends its id names and back, which recorded it again). `runOp` - on its own (no batch) reports its own. The - message names the old and the new ends; the hint is `bpmn set - id=`, where the suggestion is what `IdStyle.next` - gives the new ends against the document's ids plus the hints before it in - the same report (`reportStaleFlows` keeps a copy of the registry): two - flows that end up between the same ends get the id and its `_2`, so every - hint can be followed. The document claims none. In every flow form it is - the id the default rename gives (`test/conventions-ids.test.ts`). -- **Move puts the out-flow back** (`ops/move.ts`, `Doc.keepsFlowId`, the - `reuse` of `createSequenceFlow` / `placeNode`): with ids kept, a moved - node's one out-flow, which the detach takes out, is put back as the first - flow the placement creates out of that node (a splice's node -> old - target, a prepend's node -> anchor, the append of the next node of the - chain, which a gateway always gets: a gateway followed by another node of - the chain keeps it for that append, so `move Gateway_Ok Activity_Book` - keeps Flow_OkToBook between them and the gateway's splice makes a new - flow), with its id, documentation and extensions. Its name, condition - and annotations stay with the bridge (`detachNode` carries them), and it - is no longer a default flow; a node that is not bridged (`detachWithBridge` - and `detachNode` now return whether they bridged) drops name and - condition, and once the flow is put back to another node - `W_LABEL_DROPPED` / `W_CONDITION_DROPPED` name it (before, they were gone - without a word); between its old ends once the chain is placed it gets - them back, and its default marker (`giveBack`), also when a later node of - the chain takes it back there (`giveBackLate` withdraws the warnings: - `move A B --after X` with A not bridged splices A -> B in as A -> Done, - B's splice after A ends it at B again). A default the move gives the node - (`"default"` on a prepend, batch and library only) wins over the marker - (the change entry adds `no longer the default flow of `), and a flow - it made the default flow gets no condition (`W_CONDITION_DROPPED`; it had - kept it, a default flow with a condition). A `flowId` for the flow a - prepend creates (node -> anchor, batch and library only) names a new flow - instead (`createSequenceFlow` ignores `reuse` with an id). The result - lists the put-back flow as changed (`B -> A (was -> C)`), not as removed - and created. The chain runs through these flows, so `move B C` keeps B -> C - between B and C, unless B has several out-flows (then it is removed with - B's other flows). When the detach of a node takes out the flow from the - node moved before it (the node is not bridged: an end event, a join; - `move C Done --after B` on a line, which changes nothing), that flow is - filed under the node before, and the append puts it back with its name, - condition and default marker (it used to come back as `Flow_CToDone_2`). - What nothing puts back stays removed: the out-flow of the last node - appended after a gateway or a node without successor, the other flows of - a node that is not bridged (several in- or out-flows, or none in or none - out; its single out-flow and the flow from the node before it are put - back), and with `--in` the group's out-flow. Default mode never reuses - (no output change). The incremental layout reroutes a put-back flow like - any flow whose ends changed, also into a collapsed sub-process's plane. -- **Id style read before the ops** (`ops/index.ts readIdStyle`): `runBatch` - and `runOp` on its own read `Doc.idStyle` before the first op. It was - learned lazily when the first new id was needed, from the ids, names, - types and flow ends then in the file, so whatever ran before changed it: a - first op that removed flows (also a single `move`, whose detach removes - them), `set id=`, `set name=` or `retype` could make it learn - another style (prefix `F` -> `Flow`, `idPair` -> `named`, `UserTask_` -> - `ServiceTask_`): new ids in that style, and a flow whose id named its old - ends was neither renamed nor reported. This changes default-mode output - against 0.6 in those batches and commands (migrating.md). -- **Report**: it is an op warning, so it is in `warnings.added` of the - change that made the flow stale only; it counts for `--strict` (exit 5), - `bpmn validate` never lists it (without the old ends a lint could only - guess), and it never floods (`report.ts NEVER_FLOOD`: each line carries its - own command). There is no `renamed` key when ids are kept. -- **Hints name the file**: the `W_OPTION_IGNORED` hint of `add --flow-id` - on a splice, both `E_INVALID_RETYPE` hints, the `E_CROSS_SCOPE` hint, the - hints of `E_INVALID_TRIGGER` (retype, set), `W_FLOW_DROPPED`, the lane - conflict of `add lane`, a blocked `attachedToRef`, a default flow's - condition, a call activity's `calledElement`, a boundary event's `place`, - the notes of a self loop and a split whose branches all end, and the - catalogue entries `E_DANGLING_REF`, `E_FLOW_LINKS`, `E_CROSS_SCOPE`, - `E_INVALID_HOST`, `E_LANE_CONFLICT`, `W_NOT_IN_LANE`, `W_UNREACHABLE`, - `W_C7_DEPLOY_HISTORY_TTL` named a command without ``. A test now - checks every command with arguments in the catalogue and in the strings - under `src/` (a small scanner in the test reads the string literals and - templates, so comments and regular expressions are left out and escaped - quotes hide nothing; not TypeScript's compiler API, which typescript 7, - Dependabot #28, no longer exports; a command with an option first is a - name, not one to paste). -- **Test isolation**: `test/setup.ts` (vitest `setupFiles`) deletes - `BPMN_KEEP_FLOW_IDS` before every test file, so the CLIs the tests spawn - rename as by default; `CLI_ENV` in `tools/fuzz/lib/setup.mjs` is the - environment of the fuzzer, the benchmark, `tools/layout-regress.mjs` and - the demo. -- **Docs**: the guide (catalogue `W_FLOW_ID_STALE` after `W_ID_SUFFIXED`, - the common options, CONTRACT, OUTPUT, ops JSON, the `--flow` rule, - `kinds --section ids`; `guide --short` one line more, 4,389 of 5,120 - bytes), docs/contract.md (the limitation says how to keep ids), - commands.md, reference.md (Ids and a new Environment table), library.md, - ops.md, testing.md, migrating.md (Changes from 0.6), the skill (when to - keep flow ids; `BPMN_KEEP_FLOW_IDS` in the `env` of the Claude Code - settings, because an inline variable in front of `bpmn` is not covered by - the pre-approved `Bash(bpmn *)`) and recipes.md. `E_USAGE` (catalogue, - exit code 1 in the guide and reference.md, the skill's error table) names - a bad `BPMN_KEEP_FLOW_IDS`, the first variable that can fail a command. - -Evidence: `npm run gate` passes with 1,942 tests (+246 opt-in; 28 new: -`test/keep-flow-ids.test.ts` for every op that changes the ends of a flow, -the negatives, a batch, a flow the batch renames with `set id=`, a flow an -op creates (also `runOp` on its own), a move that puts back the node's -out-flow and a group move (spliced and appended; a second node that is not -bridged: an end event, a shared end, a join; a gateway first), a moved node -that is not bridged with a labelled, conditional out-flow (also first in a -chain whose next node takes the flow back, and as a prepend made the -default flow, next to a default the prepend gave), a later change -that leaves a stale flow where it was, the library precedence and the -report; `test/conventions-ids.test.ts` for every flow form, two flows with -the same new ends (hints `Flow_OkToJoin` / `Flow_OkToJoin_2`) and the style -read before a batch that removes first; `test/cli.test.ts` for the flag, the -variable and its bad values on the built CLI; `test/isomorphic.test.ts` in -the browser bundle; the doc pins in `test/batch.test.ts`, whose string -scanner checks itself and passes with typescript 7.0.2 installed); the -layout regression stays at 444 and the gate's fuzz campaign is unchanged -(173 ok, 7 refused, 0 errors). With `BPMN_KEEP_FLOW_IDS=maybe` set in the -shell the CLI tests still pass. Default mode does the same work as before -(`idNamesEnds` is the old check); against origin/main it differs only where -the style read applies (seeded batches at the `runOps` level: 27 of 2,000 -differ, none once main's `Doc.idStyle` is read before the ops too). With ids -kept, a batch takes one set of the document's sequence flows and runs -`idNamesEnds` and `IdStyle.next` once per stale flow at its end: 60 splices -in one batch, ops alone 96.5 ms following vs 94.2 ms kept, `applyToXml` -without layout 120.7 vs 121.6 ms (noise). A `runOp` outside a batch takes -the same set once per op (only with ids kept; every CLI and pipeline path -runs a batch): 60 single splices on a 200-flow chain, 125.0 ms following vs -119.7 ms kept (noise). Bundle: whole entry 872.5 -> 880.9 KB minified (275.7 --> 278.6 gzip), `applyToXml` only 636.0 -> 640.4 KB (201.9 -> 203.5), within -main's budgets (883 / 279 and 643 / 204 KB, unchanged; the whole entry's -gzip is 0.4 KB below its budget). Seeded random batches after the third -review: with ids kept, 1,400 batches (4,901 ops, 1,919 moves, each also op -by op) keep every invariant (no flow gone without `removed:`, no file flow -renamed, hints free and followable, the same DI edge id, no new validation -error) except the chain in Left for later and one hint case older than this -round. The check that a move never replaces a moved node's out-flow with a -new flow out of it failed in 12 of them before and fails in that 1 now, and -the 2 repeated stale warnings are gone. Default mode is byte-identical to -the state before the third review in 3,258 comparisons (600 batches with and -without layout, and each op alone). - -Rebased onto main after #35 (the design profile removed) and #31 (ids cut -at whole words): `--keep-flow-ids` and `keepFlowIds` follow #35's removal -of `--profile`, `profile` and `contentRepo` (none is added back; -library.md points at validators.md); `idNamesEnds` carries #31's forms of -`derivedBases` (in a file of another flow form also the `named` id, and the -ids 0.6 cut); the catalogue keeps #31's `W_ID_SUFFIXED` with -`W_FLOW_ID_STALE` after it, and `kinds --section ids` #31's length and -collisions with the renames of both. The test count, `guide --short`, the -bundle, the layout regression and the fuzz campaign above are measured on -the rebased tree (main alone: 1,914 tests, 4,282 bytes, 872.5 / 275.7 and -636.0 / 201.9 KB, score 444, 173 ok, 7 refused, 0 errors); the timings, -the comparison with the old main and the random batches were measured -before the rebase. - -Left out, for later: - -- `bpmn set id=`, the hint's command, does not move a derived DI edge - id the way the automatic rename does (`BPMNEdge_` stays): a cosmetic - mismatch, a follow-up of its own. Fixed in - `fix/di-ids-within-64-follow-renames` (see the section at the top), for - shapes too. -- There is no repository default in a project file; the flag and the - variable set it. A result does not say why ids were kept (flag or - variable): for the variable it does since 2026-10-13 (see the section at - the top); a flag is the caller's own. -- `add --flow-id` on a splice with ids kept gives two rename hints for one - flow: `W_OPTION_IGNORED` with the given id, `W_FLOW_ID_STALE` with the - speaking one. -- An end renamed earlier in the same change (`set id=`, or `name=` on - an end without a speaking id) hides the next end change of its flows: - `idNamesEnds` labels the end as it is then, so `Flow_CheckToOk` after `set - Activity_Check id=Activity_Verify` no longer names its ends and gets - neither a rename nor `W_FLOW_ID_STALE`, the same in both modes. A fix - would record each end's id and name at batch start (`flowsNow`) and accept - those too. -- The flow a splice creates (new node -> old successor) has no alias: - `flowAs` on a splicing `add` names the kept flow into the node. The docs - say so; an `outAs` key would be new syntax. Done in `feat/out-as-alias` - (above): `outAs` on add names it. -- With ids kept and no layout (`--no-layout`, or a second `BPMNDiagram` the - incremental layout leaves alone), a put-back flow keeps its BPMNEdge with - the old waypoints and the result has no `run bpmn layout` note (default - mode removes that flow's edge and says so); the next incremental write - leaves it stale too. Default mode keeps bridged and spliced edges stale - the same way; a fix would drop the edge of a flow whose ends changed, or - reroute edges that no longer touch their ends. Both done since - 2026-10-13 ("Stale edges of flows whose ends changed" at the top). -- In a file of mixed id styles, following `W_FLOW_ID_STALE` hints can tip - the learned style (`IdStyle` takes the first prefix seen on a tie: a move - declares a put-back `F3` before `G1`), so the next read no longer sees the - followed ids as naming their ends: a later end change neither renames nor - reports them. The drift is older than #32 (origin/main switches `fX` to - `Flow_X` after a few adds to polish-rename.bpmn); a fix belongs in - `idNamesEnds`, accepting the forms of every flow prefix in use. Fixed - since 2026-10-13 (see "A file's flow form stays put" at the top). - -## Ids up to 64 characters, cut at whole words (2026-10-13, `fix/31-flow-id-cut`, issue #31) - -A splice renamed `Flow_ConsignationsSynchronisierenToConsignationsAktualisiert` -to `Flow_ConsignationSynchronisierenToConsignationsProtokollieren`: a letter -short, although the full id has 62 characters. Two defects: every generated -id was capped at 61 (`MAX_BASE = MAX_ID - 3`, room for a collision suffix -`_99` on every id, needed or not), and `fitPair` shared the room by -characters and cut the head of an end with `cutAt`, whose fallback for a -first word longer than the room cut inside a word that could go whole. - -- **The cap** (`src/idstyle.ts`): `MAX_BASE` is gone; `derived` and `wanted` - take a `max` (default `MAX_ID`), so the cap no longer cuts an id within 64 - characters (a name's 40-character slug, `MAX_SLUG`, is unchanged). - `next()` cuts a base again only when base and suffix pass 64: a flow - shares what is left between its ends (`wanted(req, 64 - suffix)`), an - anchor-qualified id keeps the anchor's kind, the suffix separator stays - (`2` for stemTo / scopedTo). `joinId` cuts at `MAX_ID - 5`; `joinIdFor` - (`src/ops/split.ts`) cuts a suffixed join so that `_join_2` stays within 64. -- **Whole words** (`fitPair`, `endForms`; `idWords`, `keptTail` in - `src/ids.ts`; `cutAt` now keeps whole `idWords` too, one definition of a - word, so a slug that begins with `_` is no longer cut to nothing): each - end lists its forms (`endForms`), giving up its last word one at a time, - never its first word, its Join / number tail, its last word with a letter - before that tail (`Flow_2LieferantenbestellungsuebeTo...`, 0.6: - `Flow_2To...`; `Flow_LieferantenbestellungenJoinTo...`, never a bare - `Flow_JoinTo...`) or a word between two numbers (no `Stufe23`, nor - `stufe_2_2` for `stufe_2_pruefung_2`, which read like a suffixed id and - made `next()` skip `_2`); a number the given-up word leaves at the end of - the id goes too where it reads like a collision suffix (`closingNumber`: - a separated `_4` where the suffix is `_2`, any number before a glued `2` - in stemTo / scopedTo; a stemSnake `..._to_lieferantenbestellungen` for - `Task_lieferantenbestellungen_4_augen_ab`, never `..._4`), and stays - elsewhere (`Flow_Phase1To...` and `Flow_Phase2To...`, which an earlier cut - of this branch made one `Flow_PhaseTo...` and a `_2`; a default target - `...ToLieferantenbestellungen4`). Ends that begin with the same word - keep their last word and give up the word before it, down to the first - one (`Flow_SynchronisierenToLieferantenbestellungenProtokollieren`). Of the - pairs of forms that fit, fitPair takes the one with the most words, then - the most characters, then the more even one, then the one where the - source gave up more: a short word goes before a long one - (`Flow_SynchronisierenToRechnungLieferantenbestellungen`, not - `Flow_SynchronisierenAktualisierenToRechnung`), and - `Flow_NeueToZahlungseingaengePruefenUndAbgleichen` keeps its five words. A - word is cut inside only when even the shortest forms do not fit: in the - longer end, where the other fits whole and it keeps 8 characters - (`Flow_LieferantenbestellungsueberToZahlungseingangsbestaetigungen`), else - in both (`sharePair`); `cutInside` keeps the Join / number tail where the - head keeps a letter and does not run into it - (`Flow_3Zahlungseingangsbest2024To...`; before: `Flow_32024To...`, the word - between the numbers dropped). A camelCase end that loses its first word - starts as camelSlug writes it, from the label's words where it has them - (`Flow_urlPDFVersendenTo...`, not `urlpdf`, also where camelSlug cut the - name at 40). `endForms` lists only the forms that can fit (and the - shortest), so ends of any length are cut fast (`derivedBases` on ends of - 1,600 words: 256 ms of CPU before, 5 ms). `cutAt` keeps a letter too: - where the whole words that fit have none or end in a number the kept one - would run into, it cuts the next word inside (`orderFulfilmentProcessTask_2URLAPI...abgleic2`, - 0.6: `..._22`; `Flow_10Warenwirtschaftssyst10To...`, 0.6: `Flow_1010To...`); - `cutAt(slug, max, true)` cuts as 0.6 did (`legacyCutWords`, 0.6's loop: - a slug that begins with `_` and whose first word does not fit is cut to - nothing), for recognising its ids. -- **Flows named by their ends' ids** (idPair `Flow__`, - as design-iq writes them, and idSnake): each end keeps its prefix, the - words after it share the room, and "ends that begin alike" compares those - words, not the prefixes - (`Flow_Task_lieferantenbestellungen_pruefen_Task_zahlungseingaenge`; 0.6: - `Flow_Task_pruefen_Task_zahlungseingaenge_abgleichen_und`). Below a room - of 16 (two long prefixes, also from a suffix) the whole ids share it, cut - by fitPair like any ends, so the words of a prefix can go too - (`Flow_intermediate_protokollieren_intermediateCatchEvent_gebucht` for two - `intermediateThrowEvent_` / `intermediateCatchEvent_` ids). -- **Scoped flows** (`Flow_KotO_ValidateToReserve`): the two end words share - the room after the scope like any ends, so the target is no longer cut - away (`Flow_KotO_WarenwirtschaftssystemToKundenstammdatenaktualisierung`; - 0.6: `Flow_KotO_WarenwirtschaftssystemabgleichTo`, which `SCOPED_TO` no - longer reads, so enough of them flipped the file's form); an end word is - the first word with a letter of the id (`Augen` for - `serviceTask_KotO_4AugenPruefen`, 0.6: `To4`, a second one `To42`). -- **Long prefixes**: a file prefix that leaves an element's body fewer than - 8 of the 64 characters gives way to the bpmn-cli prefix of the kind in - the file's case (`activity_auftrag`; 0.6 kept a body of 8 and went past - 64), `withWord` cuts body and anchor word as one where the word leaves the - body fewer than 8; a flow prefix (with a scopedTo flow's scope) that - leaves its ends fewer than 16 gives way to a named flow with `Flow_` in - every flow form (a scoped one named by its end words: - `Flow_Validate_To_Ship` for a 57-character scope; before: stemTo - `_To`, scopedTo `Flow__To`, both ends empty and past 64), - and `pairEnds` never passes fitPair a negative room. - A glued flow named by its ends with a suffix - (`flowVertragsunterlagenarchivieToKundenstammdatenaktualisierung_2`, - `GLUED_SUFFIXED`) lends no prefix in `learn`: 0.6 read it as the prefix - `flowVertrags...` and a number, and three of them made that the flow - prefix (80-character ids, splices not followed). In a file of `flow12` - ids it still counts for their form, as 0.6 counted it (an earlier cut - of this branch dropped it from the count, and such a file switched to - `Flow_` a few edits sooner, its glued flows no longer followed). -- **Cut flows vote for their form** (`cutOfForm` in `learn`): a flow whose - id matches no form uncut votes for a form built from its ends' ids - (stemTo, idPair, stemSnake, idSnake) when it is one of the cuts - `derivedBases` lists for that form (also 0.6's), with or without a - suffix; cheap tests first (the word between the ends, a stemTo source in - lower case, the start of a stem form's source). Before, enough cut flows - flipped an idPair / idSnake / stemSnake file to `named`: the 7th - long-named splice in an idPair file renamed nothing, and new flows - switched convention. In a file whose flows read as any other form than - `named`, `derivedBases` also lists the `named` bases (uncut, cut for a - suffix, and as 0.6 cut them, also again for `_100`; with the form's - prefix and the file's most used flow prefix, `namedPrefixes`, which 0.6 - took), so the `named` flows bpmn-cli gave it before still follow their - ends: 0.6's in an idPair / idSnake file it read as `named` - (`Flow_PackOrderToArchiveOrder`, renamed to - `Flow_Activity_PackOrder_Activity_WeighParcel`), 0.6's cut ones to a - suffixed end (`Flow_lieferantenbestellungsueberToLieferantenbestellungsuebe2`), - and a flow to a join, named by the join's label (`CheckOrderJoin`). An - id an author wrote in that form follows too (nothing tells it apart; - `ID_CONVENTIONS.renames` and docs/reference.md say so): of 537 files on - disk one flow changes, in a stemTo file whose start event has a - snake_case id, renamed where 0.6 kept it. Cost: `IdStyle.infer` on 300 - flows of which 250 are cut, 2 to 6 ms of CPU (idPair) and 3 to 11 ms - (idSnake); a file of named flows is unchanged. -- **Flows written by 0.6** (`derivedBases`, used by `followEnds`): the uncut - id when it has at most 60 characters, else also the ids at 64 to 60 (a - suffix of up to 4 characters, `_100`), the id 0.6 cut (`legacyFitPair`, - the old algorithm with 0.6's `cutAt` and scoped end words, kept only for - this) and that id cut again to 60, as 0.6 did for a suffix from `_100`, - so a flow 0.6 cut inside a word still follows its ends; and the id 0.6 - wanted before its cap, with the file's head also where it now gives way - to `Flow_` (a 65-character `Flow_<41-character scope>_ValidateToShipping` - follows its ends as in 0.6). -- DI ids keep `MAX_DI` 61 and go through `cutAt`, so only its letter guard - changes them: a DI id 0.6 cut to a bare number or to two numbers in a row - is not recognised by `edgeStyleOf` and keeps its id when its flow is - renamed. Fixed in `fix/di-ids-within-64-follow-renames` (`diStyleOf` - also compares the 0.6 cut; see the section at the top). - -Ids differ from 0.6 for the same edit where an id has 62 to 64 characters or -has to be cut; one pinned id changed: the long-names test's flow keeps the -most characters among its cuts of seven words, -`Flow_PruefenObDieEingereichtenUnterlagenToBenachrichtigungUeber` (0.6: -`...EingereichtenToBenachrichtigungUeberDen`, 7 characters shorter). Runtime: a splice write -with long names is unchanged (4.6 to 4.7 ms median, as in 0.6), the -layout-regression score (444) and the gate's fuzz campaign are identical; on -long name pairs `derivedBase` takes about 10 instead of 7 microseconds and -`derivedBases` about 50 instead of 12 (a few calls per renamed flow; in a -file of another flow form than `named` about three times that for its -`named` bases, while a long-named splice write in an idPair file stays at -2 to 3 ms median, as before). -Bundle (on top of #35): whole entry 865.8 / 273.2 -> 872.5 / 275.7 KB, -`applyToXml` only 630.6 / 199.9 -> 636.0 / 201.9 KB. The budgets #35 set -stay (883 / 279 and 643 / 204, `tools/iso/budget.json` unchanged): the -change fits them, `applyToXml` 7.0 / 2.1 KB below and the whole entry -10.5 / 3.3 KB below, so nothing needs raising (before #35 this branch had -raised the gzip budget of `applyToXml` from 206 to 207 KB); -`docs/library.md` updated. -Tests: `#31` cases in `test/step3-ids-report.test.ts` (the issue's ids, -camelCase, a seeded check of 2,000 name pairs whose cut ends have no room -for their next word, a cut inside a word in one end, fitPair's tie, tail -and first-word rules, the cut with the most words and characters, ends -with numbers (`Phase1To...` / `Phase2To...`, no suffix skipped after -`stufe_2_pruefung_2`), never a bare `JoinTo`, a camelCase end without its -first word, also from a name cut at 40, `cutAt`'s letter guard, 2,000 -random idPair / idSnake flows, a 0.6 flow, also with `_100` and a leading -separator, suffixes up to `_101` and none skipped, stemTo `2`, a suffix on -an id that names the anchor's kind, a 58-character file prefix, long flow -prefixes and a long scope in every flow form, an idPair file through seven -long-named splices, a stemTo file's `named` join flow, flows 0.6 named in -files that now read as idPair, idSnake and stemTo (also cut to an end with -a suffix) and an author's id in that form, a 65-character scoped flow -whose head now gives way to `Flow_`, ends of 1,600 words, -element ids of 62 to 64 characters with the `--if-absent` hint and a data -object, joins), a -design-iq splice, scoped flows with long and number-led end words and a -glued file with suffixed flows, also one they keep on `flowAToB`, in -`test/conventions-ids.test.ts`, the -issue's commands in `test/cli.test.ts`, doc pins in `test/batch.test.ts` -and `test/conventions-ids.test.ts`. Left for later: a stop word left at the end of a cut -(`Flow_Task_lieferung_versenden_und_Task_rechnung_eingegangen_und`: the -most words win), the engine's `BPMNShape_` / `BPMNEdge_` over 64 -characters in a fresh drawing, the DI cap (both fixed in -`fix/di-ids-within-64-follow-renames`, see the section at the top; `MAX_DI` -stays 61), a data object whose `Object` -word a reference id of 63 or 64 characters cuts away again (it takes `_2`, -as 0.6 did just below its cap), a cut inside a word where two one-word ends -do not fit beside each other (one end loses its last letters, -`Flow_Task_lieferantenbestellungsuebersich_Task_zahlungseingaenge`), a -suffix glued to an end that ends in a digit (stemTo / scopedTo -`...Pdf10` + `2`, as in 0.6), bpmn-cli's own glued flows without a suffix, -which count against a numbered file's form without voting for it, scoped -flows 0.6 cut to an empty end word (`Flow_KotO_...To`), which no longer -match their form and can flip a 0.6 file's form (these two fixed since -2026-10-13, "A file's flow form stays put" at the top), an end whose leading -number fills a small room (`10202422024Rechnung` in 11 characters keeps no -letter), an element prefix of 52 to 55 characters that a suffix leaves a -body under 8 (the fallback to the bpmn-cli prefix counts no suffix: -`_2024_100`, a body without a letter), a target that already ends -in a number before its number tail (`x_2_2`: its re-cut for `_2` gives the -base again, so `_2` is skipped), and a count before a dropped word kept at -the end of a target that has no separated suffix -(`...ToLieferantenbestellungen4` for `Lieferantenbestellungen 4 Augen ab`: -only a suffix's look is checked, not whether the number counts the next -word), and the fallback for a long head, which looks at the head's length -alone: a scope of 41 or more characters (a flow prefix of 46, an element -prefix of 56) gives `Flow_Pack_To_Reserve` (`activity_gh`) also where the -id with the file's head would fit within 64 (0.6: -`Flow__PackToReserve`, 60 characters). - -## The design profile removed (2026-10-13, `feat/35-remove-design-profile`, issue #35) - -The decision on #30: bpmn-cli does not couple to design-iq, and design-iq's -own validator is the only place its rules live. design-iq's live host -already embeds the core with `profile: 'none'` and passes its own -`checkModel` through the validator hook (design-iq's ADR 0009, -Miragon/design-iq#244). A checkout of a content repository has its own check -(`npx @miragon/design-iq-validator --root .`, which the repository's -`CLAUDE.md` asks for after every model edit). The copy drifted: the rename -to `designiq.yml` turned it off without a message (#30), and the analysis -for #30 found about 15 edge differences to `checkModel`. This replaces the -issue's earlier plan, a project-validator mechanism in bpmn-cli with an -adapter shipped by design-iq: that would be new coupling for one consumer -that already has its own check. - -- **Removed** (no replacement in bpmn-cli): - - the rules: `src/platform/design.ts` (`designFindings`, - `designValidator`, `undeclaredPrefixes`, `DESIGN_VALIDATOR`) and the - `E_DESIGN_*` / `W_DESIGN_*` codes; - - the profile and the repository lookup: `src/platform/repo.ts` - (`resolveProfile`, `profileValidators`, `modelsFolderOf`, - `PROFILE_CHOICES`, `CONTENT_CONFIG_FILE`, `ContentRepo`, `ProfileChoice`, - `ProfileInfo`), `src/node/repo.ts` (`findContentRepo`, - `contentModelIds`, `contentRepoOf`, `resolveFileProfile`, - `ContentRepoOnDisk`) and the lookup in `src/node/files.ts` - (`fileContext` only names the file the validators see); - - the options: `--profile` on the mutating commands and on `validate` (now - an unknown option: `E_USAGE`, exit 1, the hint any unknown option gets), - and `profile` / `contentRepo` in `MutationOptions`, `CheckOptions`, - `EditOptions`, `ValidateXmlOptions` and the file helpers; - - the output: `profile` in the `validate` result (`ValidationReport`, - `CheckResult`), `profiles` in `bpmn kinds --json` and `kinds --section - profiles`; - - the guide: the DESIGN-IQ section, the `design` topic, `PROFILE_DOCS`, - the `--profile` common option and the catalogue entries. `E_VALIDATION`, - `W_PREEXISTING_ERROR` and a line of the COMMANDS section (the result - format of the mutating commands, `bpmn guide commands`) name "a host's - validator"; a validator's `E_VALIDATION` hint no longer mentions - `--profile none`. The fix of `W_PREEXISTING_ERROR` says that a host - validator's error is listed by the host's own check or by `validateXml` - with `validators`, and the COMMANDS line shows a finding as `[] - : ` (a host's codes are its own, not `E_...`); - - the docs: `docs/design-iq.md` became `docs/validators.md` (the hook - only, with a product-neutral example run against the sources), the - verdict suite of `docs/testing.md` and the skill's line on content - repositories are gone, and `docs/migrating.md` ("Changes from 0.6") - lists every breaking change with what to do instead. -- **Stays**: the validator hook (`validators`, `NamedValidator`, - `ValidatorFinding` with `ruleId` and `key`, the before and after runs, - `W_PREEXISTING_ERROR`, `E_VALIDATOR_FAILED`, `covers`, `ctx.file`); sticky - support (a `bpmiq:sticky` follows its node: file format compatibility, the - namespace is frozen); decision links read and written in every spelling - (`src/ops/decision.ts` unchanged, also the unprefixed `calledDecision=` for - `platform: none`); the platform profiles (Camunda 7 / 8, `--platform`). -- **Instead**: in a design-iq content repository an agent runs - `npx @miragon/design-iq-validator --root .` after its edits; a library - host that wants a check inside the write passes its own validator through - `validators`. The design-iq side (drop `profile: 'none'` with the bump) is - Miragon/design-iq#248. -- **Tests**: `test/design-profile.test.ts` is deleted: its 36 rule cases and - 12 explicit cases only tested design-iq's rules or the repository lookup, - and its opt-in suite compared the copy with design-iq's validator (48 - passing tests and 1 skipped suite fewer). The generic hook coverage runs - through two host validators in `test/validators.test.ts` (15 -> 18 - cases): `gate()` reads the parsed document (unreachable and dead-end - errors that cover the lint warnings, start-event errors counted one by - one, a size warning with a `key` and no element), `shapes` reads the XML. - Ported to them: a refused change and the same change without the - validator, `--force`, a pre-existing error through a rename, one start - event per process and a node with its flows in one transaction, findings - counted one by one, warnings reported once (the `key` identity), the same - bytes with and without a validator, the check of what would be written - (`--no-layout`), `ruleId` findings, the check of `validate` (every - finding, `covers`) and the validators seeing the file's own diagram. The - CLI cases: "`--profile` is an unknown option like any other" (the same - stderr as `--bogus none`), and a structural error through the CLI: `bpmn - apply` refuses it with the error list under the message and in `--json` - `error.errors` (exit 2, nothing written), and after `--force` (no `.bak` - without `--backup`) `bpmn validate` reports it and exits 2. The deleted - design-profile tests were the only CLI runs of these. `test/step2-integration.test.ts` and - `test/isomorphic.test.ts` keep their case counts with host validators - (`ctx.file` of `--out`, of `checkFile` and of the `file` option; `bpmn - validate --json` equals `validateXml` and has no `profile`; no profile - chunk loads, and the split bundle reports a host validator's error, its - `W_PREEXISTING_ERROR` after an edit and the lint warning it covers like - Node); `test/decision-link.test.ts` drops `profile: 'none'`. - Deleted with the profile: the guide's DESIGN-IQ recipe case and the - `--profile bogus` / `profile: 'strict'` cases. -- **Closed**: #30 (decided: no coupling) and PR #36 (the `designiq.yml` - lookup fix, option a of #30), obsolete since the lookup is gone. -- **Release**: one minor release with a `BREAKING CHANGE` footer, no - deprecation release (design-iq is the only known library host and does - not use the profile; CLI scripts drop `--profile`). -- `npm run gate`: 1,888 tests (+246 opt-in), isomorphism check, - layout regression 444 (budget 444), fuzz 12 x 15 0 errors (4 group - warnings in one walk). The bundles shrink and the - budgets go down with the code (`tools/iso/budget.json`, measured + about - 2 %): `applyToXml` only 642.6 / 204.0 -> 630.6 / 199.9 KB, budget 650 / - 206 -> 643 / 204; the whole entry 887.2 / 280.3 -> 865.8 / 273.2 KB, - budget 899 / 284 -> 883 / 279 (the table in `docs/library.md` too). The - Camunda 7 / 8 chunks do not change. Importing one guide export adds 116 - to 140 KB minified (35 to 43 KB gzip) to `applyToXml`; `docs/library.md` - said 118 to 141, measured at #13 (on main it was 123 to 149). - -Open after #35: - -- Once the release is out: the design-iq PR for Miragon/design-iq#248 (the - version bump, `profile: 'none'` dropped). - -## Incremental layout of edits to hand-drawn models, stage B (2026-10-12, `feat/layout-room-making`, issue #18) - -Option B of issue #18 on top of stage A: the crossing-aware row choice with -room-making (2) and the local repair after placement (3). Stage A had made a -batch place like single commands; what was left was the single-command base -rate: a new branch or boundary path crossed a bypass or loop-back line in -about one run in five (boundary path 23 %, new branch at a split 20 %, a -decision spliced into a branch 38 % on the private corpus). The user's -decisions hold: elements by id only, new ids speak, a hand-made drawing is -kept (no full redraw as a fix). - -- **Row choice** (`diagram/place.ts` `rowCandidates`, `cheapest`, - `chainCost`, `linesUnder`, `rowsAbove`, `topCandidates`): - - the cost of a candidate row now counts the rest of the chain on that row - (`chainCost`): the line the following new nodes take crosses an existing - line (a crossing, 300) or an existing line runs through the room they - take (`LINE_HIT`, 400: it would be routed around them, mostly with a - crossing). A node without any successor yet keeps room for one event - (`chainExtent`): the next command mostly ends the branch there, so a - batch and the same ops sent one by one choose the same row; - - the first row below is opened with the space tool not only when shapes - take it, but also when it lies on a line or a line runs between the - anchor and it: everything at or below the topmost thing in the way moves - down, the horizontal segments of the lines running under the anchor - (bypass and loop-back lines) and the shapes they dock on included - (`linesUnder`; the space tool shifts interior waypoints, so the line - stays orthogonal). An opened row costs 100 + 3 per moved shape (at most - 40) + 0.2 per px: less than one crossing (before: 150 + 10 per shape, so - a crossing won over an opened row with 15 shapes below; the issue saw - 220 against a crossing of 300 in the fixture's log, and the crossing was - not counted on the chain at all); - - rows above the anchor are candidates too: for a new branch when the - anchor has no branch above it, for the first node of a boundary path when - the event sits on the bottom border with no other drawn path: the event - moves to a free spot on the host's top border (`topSpot`, `seatOnTop`; - its label is placed again at the end, `PlaceCtx.relabel`). A row above - costs `TOP_ROW` (160) more, so a clean row below wins; when the first row - above sticks out of the lane, pool or sub-process, room is made at the - top of that frame first (like `place --side top`), costed like an opened - row. Open question 2 decided: automatic placement tries the top border - on its own; - - the trial route of a candidate starts from the anchor where the room - making puts it (`shiftedBox`), and from the top spot for a path above. -- **Local repair** (`diagram/incremental.ts repairCrossings`, step 10 of - the contract): for every crossing between a connection drawn in this run - and an existing one, at most 8 trials per write: - - the new connection is routed again with the whole drawing in place (the - hand drawing stays), else the existing one with the new shapes as - obstacles; - - a crossing left: the branch or exception path the new connection belongs - to (`PlaceCtx.heads`, found back through the nodes placed in this run) - starts at its next row candidate (`PlaceCtx.skip`): steps 6 to 9 run - again from a checkpoint taken before step 6 (`checkpoint` / `restore`: - plane membership, every shape's bounds and label, every connection's - waypoints, label and ends, the placement state and the run's sets and - notes), at most twice (`CHAIN_RETRIES`); skipped when the run created a - plane or adopted an engine block. The DI the measurement created for the - new elements is handed to the redrawn ones (`takeDi` / `giveDi`), so no - element gets a second DI element; - - a trial is kept only under the guard of `compact`: it adds no hard - layout problem and lowers the score (the planes are written to the DI - and measured after each trial). An existing connection kept rerouted is - in `layout.rerouted` with a note (`Flow_X routed again: it crossed the - new Flow_Y`), a chain placed again in a note (`Activity_X placed at - another row: at the first one its path crossed a line`). -- **Associations broken during placement** are routed again at once, like - sequence flows (`routeNow`; message flows still wait for the end): a batch - saw a data association its placement broke in its old place and chose - another row than single commands (one corpus run in each of two edits). -- **Audit #63 fixed** (`diagram/space.ts borderPad`, `fitInFrame`): a frame - grows until a shape keeps 10 px from the frame's own boundary events on - that border (half the event + 10 px), and the row choice estimates that - growth: a new branch inside a transaction no longer lands on its cancel - event. #13 (a splice into a loop-back flow) and #74 (a task placed left of - its rejoin target) are the splice and prepend paths, not the row choice: - unchanged. - -Evidence (private corpora local, counts only): - -- The issue's probe on the private corpus (267 files with a diagram, 3,362 - incremental runs; the stage A build measured again), runs adding a - crossing, batch / single commands: - - | edit | runs | stage A | stage B | - | --- | ---: | --- | --- | - | decision after a task | 265 | 52 / 64 | 5 / 5 | - | the same + reminder path | 265 | 60 / 75 | 12 / 12 | - | decision into a branch of a split | 161 | 54 / 61 | 11 / 12 | - | the same + reminder path | 161 | 47 / 60 | 12 / 13 | - | "no" to the sibling branch's node | 161 | 37 / 39 | 30 / 32 | - | timer path on a task | 265 | 44 / 61 | 3 / 6 | - | `split` after a task (one op) | 265 | 53 | 5 | - | new branch at a split (one batch) | 169 | 33 | 9 | - | skip with a merge | 97 | 3 / 3 | 3 / 3 | - - The acceptance targets of the issue (single commands): boundary path 2.3 % - (target <= 10 %, before 23 %), new branch at a split 5.3 % (<= 10 %, before - 19.5 %), decision spliced into a branch 7.5 % (<= 20 %, before 38 %). A - batch adds crossings in no more runs than its single commands, for every - edit. Backward flows unchanged (15 / 15, 0, 8, ...). Hard defects without - a group: 0 of 3,362 runs. The repair kept a reroute in 49 runs and a chain - at its next row in 38; a boundary path went above its host in 383 of about - 1,380 runs with one (stage A added a crossing there in 137 of them, another - problem in 28, none in 218: there the rows below were mostly farther - away or their chain ran through a line). -- Bench on the scenarios and the 60 Modeler models (stage A -> stage B): - runs adding crossings E1 13 -> 9, E2 26 -> 11, E4 15 -> 2, E7 58 -> 39, E8 - 30 -> 13, E9 31 -> 14 (all edits 205 -> 120); runs adding backward flows 9 - -> 10 (E7 6 -> 7: one Modeler model where E4's timer path went above its - host and E6's rename then widened the host, which slides the event right of - the path's end); the median share of existing shapes moved > 5 px of E1 / - E2 / E4 unchanged (25 % / 0 % / 0 %), E8 / E9 61 % -> 63 % / 64 %; the - tail grew (0.5.0 -> stage B): E2 mean 6.2 % -> 9.8 %, p90 23 % -> 39 %, - max 61 % -> 100 %; E4 runs moving any shape 34 -> 44, p75 0 % -> 11 % - (room made at the top of a pool moves its whole content down as one - block: in one Modeler model all 23 shapes move 125 px, relative to each - other none); mean harness delta of all edits 0.1 -> -0.8. No semantic failure, run adding a - hard defect or global hard count more: `RESULT: no regression`. The soft - budget is lowered to the new counts (E8 / E9 crossings 9 / 9 of 76, backward - flows 1 / 1). -- The eval fixture: the four batches plus the timer reminder path score 0 - (stage A: 5, 0, 0, 5), batch and single commands give the same shapes for - all eleven op sets of stage A; the reminder path goes above the approval on - its own where below it crosses the "no" flow (`place --side bottom` takes - it back). -- Write time: on two CLI-built models with 120 and 300 nodes (local, the - audit's generator) a write of each probe edit takes 100 to 120 ms and 230 - to 330 ms before and after (these edits add no crossing there, so nothing - is repaired; the machine was loaded by parallel runs, the differences are - within its noise). On the corpus the 3,379 probe runs took 219 s before and - 226 s after (+3 %; median 40 -> 42 ms, max 485 -> 510 ms); a write that - repairs (about 3 % of the runs) takes up to three times as long (10 -> 28 ms - on a small model). -- Fuzz, 80 walks x 25 steps (incremental and auto, the default corpus and - the 60 Modeler models): 0 errors, the same warnings as stage A. -- `npm run gate` (on the first stage A build, before the fixes below): - 1,912 tests (+247 opt-in), isomorphism check, layout regression 444 - (budget 444), fuzz 12 x 15 0 errors. The bundle budgets - are raised for this code (`tools/iso/budget.json`, measured + about 2 %): - `applyToXml` only 627.9 / 199.1 KB (stage A) -> 637.5 / 202.4 KB, budget - 630 / 201 -> 650 / 206; the whole entry 871.5 / 275.0 -> 881.6 / 278.5 - KB, budget 878 / 277 -> 899 / 284 (the README table is updated; it still - showed the sizes before stage A). - `test/layout-room-making.test.ts` (9 tests, 8 fail on stage A; synthetic - fixtures in `test/fixtures/layout-room/`). - -Open after stage B: - -- "no" to the sibling branch's existing node still crosses in about one run - in five (30 / 161): the crossing is the new flow into a drawn node, no row - is chosen for it, and rerouting it rarely lowers the score. -- A boundary path goes above its host in about a quarter of the runs with - one (when every row below crosses a line or lies far below): the drawing's - convention changes there; `place --side bottom` takes one back. -- An opened row moves everything below the line in the pool's band (like the - space tool); the penalty caps the moved shapes at 40, so a large pool moves - for one crossing avoided. -- A host that grows to fit a new name slides its boundary events along the - border; an exception path placed right above / below the event can then - run backwards (bench E7, one run). -- Rerouting broken data associations during placement can add a crossing - between two existing associations (seen on `artifacts__05-data-pool-lanes`); - the repair looks at new-vs-existing crossings only. - -### Stage B, fixes after the verification (2026-10-12) - -The verification of stage B passed ten criteria, rated two partial and -failed one (the eval not re-run); it found one high, two medium and six low -findings. Stage B is rebased onto the fixed stage A (`4966084`). Each fix has -a test in `test/layout-room-making.test.ts` (the later blocks; synthetic -fixtures `rework-line-over-host`, `timer-on-subprocess`, `claims-two-lanes`, -`shared-reject-line` in `test/fixtures/layout-room/`) that fails on the -stage B build: - -- **High: a path added to a boundary event on its host's top border tore - the hand-drawn row.** The plugin's own recovery (`place --side top`, then - `add ... --after `) put the reminder below the host, and the room - making took the event's bottom (inside the host's row) as its floor: the - host row's flows and the events on them counted as lines under the - anchor, so the start and end events dropped about 218 px while the tasks - stayed (`place.ts placeAfter`). Now the floor is the host's bottom, and - the path of an event on the top border runs above its host (its rows - above are candidates, rows below cost `TOP_ROW` more). The eval model: - nothing moves, score 0 -> 0 (before 0 -> 5 with a crossing, also on stage - A); the verifier's 9-op batch (a second path connected later): 0 -> 0. -- **Medium: the top-border spot ignored lines.** A rework line 10 px above - the host ran through the spot: the event sat on it, the line was routed - again below the row and crossed another one (synthetic, 1 -> 6). `topSpot` - now keeps clear of every line (but the event's own) and of connection - labels; that model: 1 -> 0, the event stays below. -- **Medium: the repair traded a crossing with a new flow for one between - two drawn lines** (2 of 3,379 corpus runs; the score fell, so the guard - kept it). A trial now also may not add a crossing between two - connections drawn before the write (counted together, so a chain drawn - again may cross another pair of them, not more; hard problems still per - problem). The corpus run: 12 -> 17 with one crossing of the new flow - instead of 12 -> 11 with one between two drawn lines (stage A: 12 -> 4, - where the opened row moved the merge and its lines for nothing: the - "no" flow descends through the opened row). -- **Low: an event drawn by hand moved to the top border** when its path was - added. Only an event the CLI placed moves now: placed in this write, or - sitting where `placeBoundary` puts a new event (`boundarySpot`, the spot - computation split out of it), so the same ops as single commands still - take the top border. -- **Low: a rerouted line ran along a new end event's border** (no - `through`, also on stage A). A line touching a node placed as the - continuation of a chain (`PlaceCtx.continued`: on its anchor's row, no - row choice) is routed again, at the end and right when a batch places - the node (`incremental.ts grazes`). Not for every new node: a merge a - single command splices first and a later `connect` moves would otherwise - reroute a line in single commands that the batch never touches (the - stage A parity test of the merge batch in a pool with lanes). -- **Low: the `routeNow` association change had no test** that fails without - it: an annotation above the eval's row on "Notify requester"; the splice - breaks the association, and the batch places the reminder path like - single commands only when it is routed at once. -- **Low: writes that repair took longer on large models.** A reroute that - still crosses the line it is tried for, or (an existing connection) newly - crosses another drawn line, is not measured (each measurement writes and - measures the whole drawing; debug line `still crossing, not measured`). - The probe edits on the two 300-node models, single-threaded, one after - another: stage A 30.4 s, stage B before 34.8 s (+14 %), now 32.5 s (+7 %); - `splitBranch` 483 / 710 / 596 ms, `decisionBatch` 564 / 722 / 623 ms. -- **Low: the locality claim** ("E1 / E2 / E4 unchanged") held for the - median only; the evidence above now gives the tail. -- **Low: `fitInFrame`'s JSDoc** stood above `borderPad`'s: moved. - -Evidence on the fixed build: the issue's probe on the private corpus (3,403 -records, 3,362 incremental runs): runs adding a crossing exactly as before -the fixes for every edit (boundary path 6 / 265 = 2.3 %, new branch at a -split 9 / 169 = 5.3 %, decision spliced into a branch 12 / 161 = 7.5 %); a -batch never adds a crossing in more runs than its single commands; hard -defects without a group 0; reroutes kept 49 -> 37 and chains placed again -38 -> 30 (the guard undoes the trades). `bench --compare` against stage A -and against 0.5.0: no regression, crossings per edit as before the fixes -(all edits 205 -> 120 against stage A, 302 -> 120 against 0.5.0), mean -harness delta -0.8 -> -0.7. The `edit-hand-drawn-model` eval, 2 runs per -arm on this build (`claude plugin eval plugins/bpmn-cli --case -edit-hand-drawn-model --runs 2`, $1.03): WITH 1.00 / 1.00 (every grader -passed, both drawings score 0 in `bpmn metrics`), WITHOUT 0.64 / 0.73 (mean -0.68, delta +0.32). No WITH run edited DI by hand: both built the change -with `bpmn apply` batches of `add` ops, read the rendered PNG and -validated. Both WITHOUT runs rewrote the file with a Python script, -`dc:Bounds` and waypoints included (`no-xml-in-shell`, `validated`, -`speaking-new-ids` failed). Bundle: whole entry 887.2 / -280.3 KB, `applyToXml` only 642.6 / 204.0 KB (budgets unchanged, 899 / 284 -and 650 / 206). `npm run gate`: 1,933 tests (+247 opt-in), isomorphism -check, layout regression 444 (budget 444), fuzz 12 x 15 0 errors (4 group -warnings in one walk on one of the new fixtures, which the campaign's -seeded corpus now picks; the same campaign run against the stage B build -before the fixes and against stage A gives the same 4). - -Open after the fixes, besides the list above: - -- An opened row whose chain still crosses a line (a line descending from the - anchor's row through it) moves the shapes below for nothing; its cost - counts the crossing, so it wins only when every other row crosses too. -- With an annotation associated across the row (drawn above it, on a node - below it), a few batches on the eval's model place a node differently from - single commands: a synthetic sweep of 27 annotation spots x 5 op sets, 4 - of 135 on this build, 4 others on stage A. - -## Incremental layout of edits to hand-drawn models, stage A (2026-10-12, `feat/layout-batch-parity`, issue #18) - -Issue #18 measured that an agent's change to a hand-drawn model sent as one -`apply` batch came out far worse than the same ops sent as single commands -(on the plugin eval's model 0 -> 9..19 against 0 -> 0..3), that agents had no -command to put a boundary event on the top border, and that `E_NO_ROOM` -named no command that works. Stage A is the issue's recommended first PR: -batch parity (1), the boundary side control (4), `E_NO_ROOM` with a command -(5) and the bench edits (6). The user's decisions hold: elements by id only, -new ids speak, a hand-made drawing is kept (no full redraw as a fix). - -- **A batch places like single commands** (`diagram/place.ts`, - `diagram/incremental.ts`, `pipeline.ts`): - - the new nodes are placed in op order (the ids the ops created, passed as - `IncrementalOptions.created`; the model lists an inserted node next to - its anchor, so a merge spliced after the approval used to be placed - before it, on the main row); - - `chainReaches` follows every successor through new nodes and - `spliceTarget` continues the splice of a node placed before S in this - run (`PlaceCtx.spliced`): a decision with a new "no" branch, or a - `split`, inside the chain keeps the splice (before: the chain had to be - straight, so the task went to a new branch row below); - - exception paths (new nodes after a new boundary event) wait for the - normal flow's new nodes, and boundary events for the normal flow too, - whatever the op order; - - while more nodes follow, the sequence flows of each placed node are - routed right away, the flows the placement broke too, and the spacing is - measured again (`routeNow`, through `PlaceCtx.afterNode`): the next - placement sees them like the next single command would. At the end such - a flow is routed again only when it broke since (`ctx.routedNow`), and - an existing one counts as rerouted; - - a branch row still keeps room for the chain of new nodes after it, now - free of shapes only (labels and lines there are dealt with when the chain - is placed): with labels and lines counted, a "no" label made the - reminder path take another column than single commands; without the - room at all, a branch's end event landed on a line (bench E2, a - Modeler model with a group: 8 -> 49). -- **A connect into a merge that would enter it backwards** (open question 4, - decided yes): a join drawn before the ops (a gateway with two or more - incoming flows and one outgoing flow to S) that a new flow would enter - backwards, from a node it does not lead back to (no loop return), is - placed again right of its rightmost source, before S, like a splice - (`incremental.ts pullJoins`, `PlaceCtx.joins`); its sources and S in its - pool / sub-process, S right of the sources. The eval's merge as single - commands: 0 -> 3 (backward flow) before, 0 -> 0 now, the same shapes as the - batch. -- **`place --side top|bottom`** (op key `side`; - `diagram/ops.ts boundarySide`): the event moves to that border of its - host (the spot nearest its column that no other boundary event and no - docking connection takes; `E_NO_ROOM` when none is left), its exception - path drawn on the old side (the nodes only it leads to, beyond that - border) is mirrored about the host's centre row (above the interior of - the host's lane, pool or sub-process the space tool first makes the room - at the top of that frame), the others give way (`makeRoom`), its flows - are routed again. On the 112 scenarios with a timer path on the median - task, `--side top` succeeds on all (26 with a lane, pool or sub-process - above that needed the room), adding no hard defect. Only the boundary event, no row - or column (`E_USAGE` otherwise); `place` of a boundary event with a row / - column is still `E_WRONG_KIND`, its hint names `--side`. Open question 3 - decided for `place --side` (the issue's first option). -- **`E_NO_ROOM` carries one command** (`diagram/ops.ts withRoomHint`, - `roomCandidates`; `cli.ts withFile`): when `place` / `align` refuse, a dry - run on the drawing as the op found it tries `space --after` the column - reference by 1 to 3 columns (`before`: after the reference's drawn - predecessors), `space --below` the row reference by 1 or 2 rows, and for a - row request the space right of the shape left of the moved ones, each - followed by the op; of those that let it succeed, the one with the - fewest layout problems goes into `details.ops` (the batch op) and, when - the op saw the file's own drawing, `details.command` (`bpmn space - --after Gateway_1hd8x2m --by 2col`, the CLI fills in the file, quoted - when needed), and the hint says what it carries (see "fixes after the - verification" below); without one the hint names the shapes to move - first. Both refusals of the eval (`place Activity_ApprovePurchase - Gateway_PurchaseApproved --row-of Gateway_1hd8x2m --after - Gateway_1hd8x2m` on the drawings 0.5.0 left after the merge batch and the - "no"-to-the-existing-node batch with the reminder path, - `test/fixtures/layout-batch/`) succeed after the command (score 19 -> 7 - and 11 -> 5). -- **Bench E8 / E9** (`tools/bench/gen-edits.mjs`): E8 splices a decision - into a branch of an existing split with a new "no" branch in one batch, - E9 adds a non-interrupting timer reminder path on the new task; bpmn-cli - arms only, not part of E7. Every record carries `soft.addedKinds` - (crossings, backward flows; `lib/measure.mjs softDelta`), `bench - --compare` shows the runs adding them per edit and fails when the runs of - E8 / E9 on the scenarios exceed `tools/bench/soft-budget.json` (E8: 16 - crossings / 1 backward flow of 76 runs, E9: 17 / 1; on 0.5.0 58 / 60 each). - Open question 5 decided: gated for E8 / E9, reported for the others. - -Evidence (private corpora local, counts only): - -- The eval fixture, score before -> after, batch / single commands: - - | edit | 0.5.0 | now | - | --- | --- | --- | - | decision + "no" to the existing node | 0 -> 11 / 0 -> 0 | 0 -> 0 / 0 -> 0 | - | decision + new "no" branch | 0 -> 9 / 0 -> 0 | 0 -> 0 / 0 -> 0 | - | `split` | 0 -> 9 / 0 -> 0 | 0 -> 0 / 0 -> 0 | - | merge gateway | 0 -> 19 / 0 -> 3 | 0 -> 0 / 0 -> 0 | - | each + the timer reminder path | 11, 9, 8, 19 / 5, 0, 0, 8 | 5, 0, 0, 5 / 5, 0, 0, 5 | - - Every batch gives the same shapes as its single commands (with the - reminder path too) and adds no backward flow. The 5 left with the - reminder path ("no" to the existing node, merge) is one crossing between - the reminder path and the "no" flow, in the batch and in single commands - alike; `place Event_ApprovalOverdue --side top` takes the path above the - approval: score 0 for all four, batch and single commands. -- The issue's probe on the private corpus (267 files with a diagram, 3,362 - incremental runs), runs adding a crossing / a backward flow: - - | edit | runs | 0.5.0 batch | now batch | single commands | - | --- | ---: | --- | --- | --- | - | decision after a task | 265 | 70 / 33 | 52 / 15 | 64 / 15 | - | the same + reminder path | 265 | 223 / 33 | 60 / 15 | 75 / 15 | - | decision into a branch of a split | 161 | 116 / 129 | 54 / 0 | 61 / 0 | - | the same + reminder path | 161 | 117 / 129 | 47 / 0 | 60 / 0 | - | "no" to the sibling branch's node | 161 | 126 / 147 | 37 / 15 | 39 / 15 | - | timer path on a task | 265 | 53 / 0 | 44 / 0 | 61 / 0 | - | `split` after a task (one op) | 265 | 60 / 8 | 53 / 8 | | - | new branch at a split | 169 | 35 / 0 | 33 / 0 | | - | skip with a merge | 97 | 2 / 1 | 3 / 1 | 3 / 1 | - - A batch adds crossings in no more runs than the same ops as single - commands, a branch splice in a batch adds no backward flow (before 129 of - 161). Single commands are unchanged except the merge (backward flows into - a merge an earlier command spliced). Hard defects without a group: 0 of - 3,362 runs (every hard problem added involves a group, as before). -- Bench on the scenarios and the 60 Modeler models (0.5.0 -> now): E8 runs - adding crossings 78 -> 30, backward flows 89 -> 1, mean harness delta 8.9 -> - 2.4; E9 80 -> 31, 89 -> 1, 10.2 -> 2.3; the median share of moved shapes of - E8 / E9 0 % -> 61 % (the splice shifts the rest of the row, as single - commands do; 0.5.0 put the new nodes on a new row below), of E1 / E2 / E4 - unchanged (25 % / 0 % / 0 %); E2 mean harness delta 1.6 -> 2.0 (one Modeler - model, 8 -> 40: the new branch inside a group frame, group overlaps only), - E7 -1.1 -> -0.9. No semantic failure, run adding a hard defect or global - hard count more; `RESULT: no regression`. The one E8 / E9 backward flow - (a scenario with lanes) comes from single commands too. -- `npm run gate`: 1,900 tests (+247 opt-in), isomorphism check, layout - regression 444 (budget 444), fuzz 12 x 15 0 errors (the one hard-overlap - warning of `gallery__04-subprocess` is the same on 0.5.0). - -Open after stage A: - -- Steps 2 and 3 of the issue (stage B): a crossing-aware row choice that - opens a row below the anchor and tries the host's top border for a - boundary path on its own (open question 2: today only on request, - `--side`), and a local repair pass. The single-command base rate stays (a - new branch or boundary path crosses a bypass or loop-back flow in about - one run in five); the fixture's two reminder-path batches keep their - crossing until then (stage B: 0). -- The `edit-hand-drawn-model` eval was not re-run (2 runs per arm); the - skill now says that one batch places like single commands and names - `place --side top` for a crossing reminder path. -- A merge pulled right moves what lies right of its new column (the space - tool, like a splice); a merge whose successor lies left of its sources is - not pulled. - -### Stage A, fixes after the verification (2026-10-12) - -The verification of stage A passed six criteria and rated four partial. -Each finding below has a test in `test/layout-batch-parity.test.ts` (last -block) that fails on the stage A build: - -- **Splices into two branches of one gateway in one batch** (a task into - "yes" and a merge into "no", two tasks, the eval's merge batch in a pool - with lanes: 18 against 5 as single commands, before 8 on 0.5.0). The - first node's anchor counted as a branching node only when another branch - was drawn; in a batch that branch leads to a node a later op spliced, not - yet placed, so the node went to the anchor's row (in another lane: a new - row of its lane). Now a branch drawn before the ops counts too - (`place.ts placeAfter`). And a merge placed in this run that a later - node's flow would enter backwards is placed again right of its - predecessors (`place.ts pullPlacedJoins`, the batch form of `pullJoins`): - the merge batch with the merge added first gives the shapes of single - commands as well. On the eval model and the pool with lanes, all five - variants: same shapes and score as single commands, no backward flow. -- **`place --side` back to the bottom in a pool with lanes** refused with - `E_LEAVES_CONTAINER` whose hint named the shape's own lane as the lane it - would land in (`lanes__complaint-t1`, `-t2`, `subproc__s15`): the way - back took a node of another lane above the host as part of the exception - path. The path is now the nodes in the host's own lane / pool / - sub-process (`ops.ts exceptionPath`); below a lane with a lane underneath - the lane grows at its bottom first (like the top); the hint of a shape - that would land below its lane in no lane says so. A third boundary event - drawn over a corner (`artifacts__10-three-boundaries`) found no spot on - the way back: a spot reaching over a corner of the border is the fallback - (its centre on the border's line). The verifier's runs, there and back: - of the 146 existing boundary events of the scenarios and the Modeler - models refusals 4 -> 0 and runs adding a hard problem 1 -> 0; of the 56 - new boundary paths on a sub-process refusals 4 -> 0 (a crowded border - now takes a corner spot; the move is on request, its quality line says - what it costs). -- **`place --side` and an annotation on a flow** (`artifacts__16`): the move - rerouted a loop flow, and the association to it ran straight through two - shapes (hard `through`). An association docked on a flow now ends at the - nearest point of the flow a straight line reaches without cutting a - shape, else it is routed around the shapes to the middle of the flow's - nearest segment (`reroute.ts flowDock`); it is rerouted when its end left - the flow, after the flows (`routeRank`). That run: 37 -> 0 instead of - 37 -> 26 with two `through`. -- **`E_NO_ROOM`** took the first space-tool run that fits (`1col`; the - eval's two refusals score 8 and 7 after it, `2col` 7 and 5): now the run - with the fewest layout problems (`2col` for both). After other ops of a - batch the command for the file was checked on another drawing and could - name ids only the refused batch has (`--after Activity_NewGate`): now - `details.command` only when the refused op saw the file's own drawing - (`runFormatOps(..., asFile)`, the first op of a batch of format ops only), - else the hint names only the batch op to put before the place op. - -Not changed: the timer reminder path of the fixture's "no to the existing -node" and merge batches still crosses the "no" flow, in the batch and in -single commands alike (score 5); `--side top` takes it above (score 0), and -stage B (`feat/layout-room-making`) tries the host's top border on its own -(both 0). The bench's one Modeler model whose E2 branch lands inside a group -frame (harness 8 -> 40, group overlaps only) is unchanged. - -Evidence: the issue's probe on the private corpus gives the same records -as stage A (3,403); the verifier's adversarial runs (657, scenarios and -Modeler models, batch / single, `--side` there and back) add no hard -problem; `bench --compare` against 0.5.0: no regression (E8 / E9 crossings -78 -> 30 / 80 -> 31, backward flows 89 -> 1 each, harness 8.9 -> 2.3 / 10.2 --> 2.3; E1 / E2 / E4 moved share unchanged); 311 batches twice in two -processes give the same bytes. `npm run gate`: 1,909 tests (+247 opt-in), -layout regression 444 (budget 444), fuzz 12 x 15 0 errors (the same hard- -overlap warning of `gallery__04-subprocess` as 0.5.0). The -`applyToXml`-only bundle grew by this code from 627.9 to 631.4 KB minified: -its budget is 635 KB now (`tools/iso/budget.json`; gzip 200.2 of 201 KB). - -## `bpmn render`, fixes after the verification (2026-10-11, `feat/render`, issue #12) - -The verification of stages 1 to 3 (a fresh clone, every acceptance -criterion, hostile inputs, a side-by-side review against `tools/render.sh`) -passed eight criteria and found one high, three medium and five low -findings. All are fixed, each with a test that fails before -(`test/render-fixes.test.ts`, 11 tests); two existing tests changed with the -behaviour (the gallery's empty collapsed sub-process plane is now -`E_NO_MATCH` for `--scope`; the label test names the lines of the two-pass -rule). Where the issue left a question open, the decision taken is named. - -- **`--tiles` is bounded (high).** A drawing spanning 1e9 px aborted the - process with a V8 out-of-memory error (exit 134): the tile grid had no - cap. Now coordinates beyond ±1,000,000 px never reach it (broken geometry, - below), each axis is cut only over the stretches that hold something - (gaps of half a tile and more are skipped), and the cutting stops as soon - as either axis needs more than `MAX_TILES` (40) pieces. Decision for "too - many tiles": the drawing is written as one image with a note - (`--tiles: the drawing needs more than 40 tiles ...`, and the labels note - names `--around`, `--path`, `--scope`), not an error, so `--tiles` never - fails where the plain render works. 400 shapes spread over ±1,000,000 px - at `--max-size 50 --scale 10` finish well under a second. -- **No blank tiles (medium).** A tile is kept only when it holds a shape, a - label (a flow's, a message's, a pool's or lane's name where it is drawn) - or a numbered problem; a connection passing through no longer keeps one - (the old test was the connection's bounding box). A start event and a - task 50,000 px apart with one flow: 676 tiles (625 blank, ~3.2 M image - tokens, 27 s) before, 2 tiles now (3 when the flow has a label), and a - note says empty parts are left out. Bands are numbered among the tiles - kept, so the note counts them (`3 tiles (2500 bands)` was the highest band - number). -- **Broken geometry (medium).** A shape or connection with a coordinate - that is not a number or lies beyond ±1,000,000 px, or with a negative - size, is not drawn, nor are such label bounds; notes name them with the - repair (`bpmn layout ` draws them again; a label: - `bpmn label --side below`). A NaN waypoint gave - `width="NaN"` and a blank PNG with exit 0; now the start event and the - task are drawn and the flow is named. A plane on which nothing can be - drawn is `E_RENDER`. Non-finite shape bounds stay "no shape" as before - (`plane.ts`). Decision for the limit: 1,000,000 px (500 screens of 2000 - px); none of the 155 repository and 394 corpus files comes near it (no - such note on any of them). Every hostile file of the verification (34, - plus the two sparse 50,000 px files) renders with `--tiles --problems`, - exit 0. -- **External labels like bpmn-js 18 (medium).** bpmn-js lays an external - label out at max(90, the DI label width) in 11 px, then renders it in a - box as wide as the widest line of that (rounded up), which can break it - once more; the renderer did one pass at 90 px. It now does both. The - verification's reference renders now match: gallery - `Event_CompensationEnd` and lanes__complaint-t1 `Gateway_Kulanz` on one line, - artifacts__09 `Event_Rollback` as `Roll` / `back` / `bookings`, - controlflow__s19 `Gateway_Long` / `Flow_3` / `Flow_6` with 7 / 5 / 6 - lines. Lines that differ from before: 16 of 1,313 external labels in 8 - of the 155 repository files, 332 of 3,769 in 141 of the 394 corpus - files. **Metrics unchanged**, by decision: `metrics.ts` does not use - `text.ts`; `labelClash` judges a label by its DI label bounds and a coarse - text estimate of its own (`glyph`: 6 px a character, at most 90 px wide, - 14 px a line), which neither the old nor the new renderer rule enters, and - `text.ts` otherwise serves the incremental engine's activity sizing - (embedded labels, which bpmn-js lays out once). The regression budget - stays 444 of 444. Bringing `glyph` to the renderer's rule would change - `labelClash` counts and needs its own regression measurement: a possible - follow-up. -- **Message flows show their message (low).** With a `messageRef`: the 21 x - 14 envelope halfway along the flow, outlined (`messageVisibleKind` - defaults to "initiating" in the DI schema), filled for "non_initiating", - and the message's name in 12 px wrapped at 150 px. A probe of bpmn-js - 18.31 in Chrome showed where it puts the name on import: its left edge at - the middle, its top 10 px below (it measures the envelope and the text as - 0 x 0 then; only a redraw after an edit centres the name below the - envelope). The renderer draws the import placement, which is what a viewer - or modeler shows when it opens the file and what `tools/render.sh` shows. - 8 of 155 repository and 6 of 394 corpus files have such flows. -- **`render -o x.jpg` (low)** is `E_USAGE` (a name not ending in `.png` / - `.svg`), unless `--format` says what to write, like `--render`. -- **A file without a diagram (low).** Decision: `--problems` lists what - `bpmn metrics` reports for the file, consistent with "every metrics - problem, in metrics order": `test/fixtures/views/claims.bpmn` lists its 37 - `missing` problems (none in the image) instead of 9 problems of the - in-memory drawing, and a note says the problems are the file's. The - in-memory drawing's problems are not the file's to fix; after `bpmn layout - ` saves a drawing, `--problems` lists that drawing's. -- **`--scope` on an empty plane (low)** is `E_NO_MATCH` like the other - empty crops; a `--scope` that is a plane (a collapsed sub-process, a - process without a pool) now names that plane's flow nodes in `crop: ...: - N nodes` (it said 0 nodes). -- **Notes (low).** A scale in a note and in the `wrote` line is rounded - down to two decimals (`scaleText`): 0.7458 reads `0.74 ... (below 0.75)`, - not `0.75 ... (below 0.75)`; the band count, see above. -- **The stage-3 eval** stays as run; `plugins/bpmn-cli/evals/README.md` - now states its limitation: `tidy-after-edit` cannot separate the skill's - look step from the CLI's own guide (the baseline has the same `bpmn` and - `guide --short` names `render`). - -| | before | after | -| --- | --- | --- | -| repository, 155 files, `--tiles --problems` at 2000 px | 4 files tiled, at most 2 tiles | the same; 0 errors | -| private corpus, 394 files (counts only) | 22 files tiled, at most 3 tiles | the same; 0 errors | -| core bundle, entry plus shared chunk | 985.7 KB minified, 305.8 KB gzip | 992.0 KB (+6.3), 307.9 KB gzip; since before stage 1 945.9 -> 992.0 KB (+46.1 KB of the issue's 50); the `applyToXml`-only bundle unchanged | -| gate | 1854 tests | 1865 tests (+247 opt-in), iso check, budget 444 of 444, fuzz 0 errors | - -## `bpmn render`, stage 3 (2026-10-11, `feat/render`, issue #12) - -Adoption in the Claude Code plugin; no change to the CLI or the library. -Lead decisions that apply: the skill passes an explicit `-o` into a -`.bpmn-render/` folder of the working directory (scratch, may be deleted -or ignored; the CLI's own default, the temp directory, lies outside the -working directory, where Claude Code may ask before a Read). - -- **The look step** (`plugins/bpmn-cli/skills/bpmn/SKILL.md`, core loop - step 4 of 6): after a change that drew something, `bpmn render - --problems -o .bpmn-render/.png` or `--render .bpmn-render/.png` - on the change, Read the PNG, fix the numbered problems by their ids with - the format commands, render again at most twice, `--around` / `--path` on a - large model. The description says "render" too (for "show me the diagram"). - `reference/formatting.md` has a new section 2 "Look at it": the crops, - `--highlight`, the `wrote` line and the token budget, `--max-size`, the - 0.75 note and `--tiles`, `--problems`, planes, `--render`; its batch - example renders. `.bpmn-render/` is in this repository's `.gitignore`. -- **Size**: `claude --plugin-dir plugins/bpmn-cli plugin details bpmn-cli` - before / after: always-on ~482 / ~484 tokens, on invoke ~6k / ~6.4k - (SKILL.md 16,080 -> 16,966 bytes). `claude plugin validate --strict` - passes for the plugin and the marketplace. -- **Tests** (`test/plugin.test.ts`, 4 new, each fails before; the gate - passes with 1854 tests): the render block of - SKILL.md runs on the recipes' model and writes a real PNG of the size the - `wrote` line names, within 2000 px, into `.bpmn-render/`; every `bpmn - render` and `--render` the skill shows writes into `.bpmn-render/`; the - formatting examples (with their renders) run as before. The test's - esbuild bundle of the CLI now sits at `/pkg/cli/cli.mjs` with links - `/assets` and `/node_modules`, so it finds the font - (`../../assets` from the bundle) and `@resvg/resvg-wasm`. -- **Eval case `tidy-after-edit`** (`plugins/bpmn-cli/evals/`): a - hand-drawn invoice approval (synthetic, speaking ids) whose gateway label - sits on "Approve invoice" and whose rejection branch crosses the reminder - path of a timer (`bpmn metrics`: `labelClash`, `crossings`); the user asks - for an email step "Inform requester" in the rejection branch and to clean - up the drawing, keeping the main line. Graders: the new task between the - right two nodes; the label clear and no crossing (DI conditions, since - graders cannot run commands; `test/plugin.test.ts` asserts that they - agree with `bpmn metrics --json` on the fixture, the CLI solution and - seven other states: only the edit, only one of the two fixes, the label - below / left / right, the solution followed by `compact`); the main line - in place; 12 nodes, 10 flows; validated; no hand-written XML; - `render-read` (a Read of a `.png`) and `skill-used` as with-only - indicators. - -The eval, run once (`claude plugin eval plugins/bpmn-cli --case -tidy-after-edit --runs 2 --scaffold --allow-tools Bash Write Edit`, the -CLI of this branch on `PATH` from a copy in the user cache directory as -`evals/README.md` describes; 111 s, $1.41 in all): - -| | with the plugin | without (baseline) | -| --- | --- | --- | -| score as run (2 runs) | 0.95 (1.0, 0.9) | 0.95 (1.0, 0.9) | -| score with the corrected `no-crossing` grader | 1.0 (1.0, 1.0) | 0.95 (1.0, 0.9) | -| failed graders | `no-crossing` once: a false negative, see below | `no-xml-in-shell` once (`sed -n '/= 420 - once the reminder task had moved up to 241-340; one run left them at 300 - and 400 (metrics: no crossing). The grader now takes the reminder's row - in steps of 100 px (rows at least 100 px apart, as the CLI draws them), - and the agreement test has the solution plus `compact` as a variant; - applied to the four kept results it passes all four. Not re-run (one - eval run per stage). -- One run with the plugin prefixed its first render with `mkdir -p - .bpmn-render;`, which the render does itself and which is not - pre-approved (a user would be asked); the look step now says the - directory is created. Both runs with the plugin also chained `bpmn` - commands with `;` against the skill's "one command per line" rule. - -## `bpmn render`, stage 2 (2026-10-11, `feat/render`, issue #12) - -The agent features of the issue's phased plan, on top of stage 1 (below). -Lead decisions that apply: highlights are a halo; collapsed sub-process -planes stay separate images (`--plane`); elements are named by id only. - -- **Crops** (`src/diagram/render.ts`, `RenderOptions` `around` / `path` / - `scope` / `ids`; one per call, two are `E_USAGE`): `--around - [--depth n] [--inner]` draws exactly the nodes of `show --around` - (`aroundView` itself, not a copy), `--path [--via ...]` the - nodes and flows of `pathOf` (the format selector), `--scope ` a pool, - lane or expanded sub-process and every shape whose centre lies inside it - (a collapsed sub-process: its plane; a process: its pool, else its plane; - anything else `E_WRONG_KIND`), ids exactly these. A crop draws the - connections between its elements in full, for around / path also the data - and annotations attached to them; a connection to an element it leaves - out is drawn faded in full with its label (`data-faded`; the view fits - only its first 40 px), because a 40 px stub hid crossings and label - clashes the badges pointed at; pools, lanes, groups and the expanded - sub-processes around drawn shapes are context (`data-context`), clipped. - Without `--plane` the plane of the crop's first element is drawn; crop - elements on other planes are named in a note; nothing drawn is - `E_NO_MATCH`. `nodes` lists the crop's flow nodes drawn. -- **Halos** (`highlight`): a list (orange; unknown ids `E_NOT_FOUND`, ids - outside the image named in a note) or `{ created, changed, moved }` - (green, orange, a lighter blue; created wins over changed over moved; a - pool / lane / expanded sub-process that only moved or grew gets none: - after an incremental insert every lane "moved" and the image was all - blue). Drawn under the element; labels get a tint. -- **Numbered problems** (`problems: true` or a list): `locateProblems(defs, - problems)` in `src/diagram/metrics.ts` adds `at: { plane, x, y }` per - problem from its kind, ids and detail with the checks' own tests (the - crossing point, the overlap's centre, the middle of the segment inside the - shape, the shared run, the node outside its lane, the first waypoint - outside the frame, ...). It is a separate pass on purpose: a first version - computed locations inside `layoutProblems` (opt-in thunks) and grew the - `applyToXml`-only bundle by 2.5 KB, because every mutation runs the - metrics; now that bundle is byte-identical and `metrics` / mutation - results carry no coordinates. The render lists every metrics problem in - metrics order with the same ids (with a crop: those naming an element it - draws; a given list: exactly those, numbered in its order), each as a red - badge (constant 18 px) on its location when that is on the plane and in - the image, moved aside within the problem's bounding box when it would - cover an earlier badge; the rest is listed `(not in the image)`. -- **Tiles** (`tiles`): when the image would draw labels below 0.75, it is - cut into tiles at `scale` (default 1) within `--max-size`: between the - plane's columns (`planeColumns` in `src/diagram/view.ts`, the c of - `show --layout`), in the middle of the gaps, as even as the gaps allow, - and into bands between rows of shapes when too tall; empty tiles are left - out; a readable image stays one (a note). `tiles` / PNG `images` carry - `columns` and `band`; a badge is in one tile (`tile`). -- **`focus`** (what `--render` uses): the plane of the first focus element; - when the whole plane would be unreadable, the region around the focus - elements and their neighbours (plus 40 px), with a note. -- **The command**: `bpmn render [ids...] --around / --depth / - --inner / --path / --via / --scope / --highlight (repeatable) / - --problems / --tiles`; extra result lines `crop: around X (depth 2): 7 - nodes`, `halos: ...`, `problems (numbered in the image):` with - ` [] `, one `wrote` line per tile (`tile 2/3 - c9-c17`, files `-1.png`, ...) and the total; `--tiles` with `-o -`, - `--depth` / `--inner` without `--around`, `--via` without `--path` and a - `--path` without exactly two ids are `E_USAGE`. JSON adds `problems`, - `nodes`, `images`. -- **`--render ` on every mutating command** - (`withMutationOptions`; validated before anything runs): after the write - (also with `--dry-run`) the result is drawn with `changeHighlight(report)` - (created, changed incl. colours, moved: placed / moved / rerouted / - reshaped / labels), the problems the change added (`layout.metrics.added`, - matched to their locations by `problemKey`) and `focus` on the touched - elements. Text: a `render:` block after the report; JSON: `render`. A - failed render (`E_RENDER`, `E_IO`) is reported in that block and the - command still exits 0: the change stands. -- **Fuzz**: the walk renders every written step (`tools/fuzz/lib/walk.mjs`, - `loadRender` in `setup.mjs`: `renderPng(xml, { problems: true, tiles: true - })`); a throw, an image over 2000 px or another number of problems than the - metrics (import warnings aside) is a `render` error; `--no-render` skips - it; `minimize.mjs` replays with the render oracle and its `run.sh` ends - with the render command. The gate's campaign renders. -- **Docs**: README (`render` section, synopsis, common options, library - API, sizes, changes), the guide (section LOOKING AT THE DRAWING, topic - `render`, a line in `guide --short`, the command entry, `--render` in - the common options), `E_NO_MATCH` / `E_WRONG_KIND` in the catalogue, - docs/testing.md. -- **Tests** (`test/render-agent.test.ts`, 21 tests, about 5 s, each fails - before): locations inside the elements' box for every problem of every - scenario and fixture (`layoutProblems` unchanged), `--problems` in metrics - order with every badge inside its elements, `--around` against - `aroundView` for every 4th node of every scenario and the median of all - 1,880 depth-2 crops, `--path` / `--scope` / ids, planes and notes, the - errors, halos and `changeHighlight`, tiles (every column in exactly one - tile, bands, PNG per tile), focus, the command flags and file names, - `--render` (text, JSON, the region of a large drawing, a failed render), - the fuzz oracle. New synthetic fixture `test/fixtures/render/long.bpmn` - (28 nodes in a row, 3,940 px wide; built with the CLI). - -Measured on this build: - -| | | -| --- | --- | -| core bundle, whole entry | 966.4 -> 985.7 KB minified (+19.3 KB: renderer +11.9, `locateProblems` +3.5, guide text +4.0, `planeColumns` +0.3); since before stage 1 945.9 -> 985.7 KB (+39.8 KB of the issue's 50); gzip 298.9 -> 305.8 KB | -| core bundle, `applyToXml` only | 768,734 bytes, byte-identical to stage 1 | -| time | `bpmn render agent__o2c --problems` 0.20 s wall (an `--around` crop the same); `add ... --render x.png` 0.28 s, without 0.12 s | -| `tools/scenarios` | `--around` depth 2 for each of the 1,880 drawn flow nodes: median 368 image tokens (acceptance: at most 1,000), p90 1,518, max 3,515; every problem located inside its elements' box; the listed problems equal `metrics` for every file | -| `test/fixtures/render/long.bpmn` | whole at 2000 px: scale 0.51 (unreadable); `--tiles`: 2 tiles c0-c12 / c13-c25, 1,413 image tokens in all | -| private corpus (394 files, counts only) | 1,116 problems, 1,111 located (the 5 `missing` have no place), 0 outside their elements' box, 1,099 badges on the first plane; 5,698 `--around` depth-2 crops, 0 errors, median 257 image tokens (p90 1,069, max 3,622; 652 over 1,000); the 22 files below the 0.75 label scale all tile into 2-3 tiles (median 2), each at most 1,743 px, 3,593 image tokens per file median (max 5,997) | - -Still open after stage 2 (and the issue's acceptance list): the plugin -skill's look step with `test/plugin.test.ts` running its example and the -eval case (both done in stage 3, above), and the reviewer's 20-scenario -side-by-side with `tools/render.sh`. - -## `bpmn render`, stage 1 (2026-10-11, `feat/render`, issue #12) - -The agent can look at the diagram now. Stage 1 of the issue's phased plan: -the renderer, the command, the budget and the PNG rasteriser. Decisions of -the lead on the issue's open questions: `@resvg/resvg-wasm` is a regular -dependency loaded lazily in the node layer, with the Arimo Latin TTF -(OFL-1.1) and its licence in the package; without `-o` the image goes to -`/bpmn-cli/.png`; `--max-size` defaults to 2000 px; -collapsed sub-process planes are listed, `--plane ` draws one; -highlights (stage 2) as a halo; design-iq is out of scope. - -- **The renderer** (`src/diagram/render.ts`, core, browser-safe): - `renderSvg(xml, { plane, scale, maxSize })` draws one diagram plane from - its DI, close to bpmn-js: every element kind of the issue's acceptance - list (event triggers with interrupting / non-interrupting styles, - gateway markers, task types, loop / multi-instance / compensation / - ad-hoc markers, expanded / collapsed / event sub-processes, - transactions, call activities, pools (black box, vertical) and lanes with - their names, data objects / stores / inputs / outputs, annotations and - associations, groups, message flows, default / conditional markers, - `bioc:` / `color:` colours; colour values are checked before they go into - the SVG). The glyphs are our own, drawn from the notation (no bpmn-js - code or PathMap). External labels follow bpmn-js exactly: centred on the - DI label bounds, top at their y, 90 px wrap in 11 px Arial with the line - breaking of `src/diagram/text.ts` (which now returns the lines: - `wrapLines`; `innerLines` counts them), 13.2 px lines; without bounds - below the shape or 15 px above the middle of a flow's middle segment. - Arrow heads are paths, not SVG markers, and everything outside the view is - culled (a marker-carrying path outside the view made resvg trap in the - issue's prototype). A file without any diagram is laid out in memory (a - note says so); elements of the plane without a shape are named in a note; - the other planes are listed; the long edge is capped at `maxSize`, and a - scale below `MIN_LABEL_SCALE` (0.75) adds the note naming `--around`, - `--path` and `--tiles` (stage 2 adds them). -- **PNG** (`src/node/render.ts`, `@miragon/bpmn-cli/node`): - `renderPng(xml, opts)` -> `{ images: [{ png, width, height, tokens }], - scale, plane, planes, notes }` (the issue's API shape; one image until - `--tiles`). resvg-wasm 2.6.2 is imported on the first call, the wasm read - from its package, the font from `assets/fonts` (no system font: the - image is the same everywhere). Every rasteriser failure, including a wasm - trap, is `E_RENDER` (exit 4) and the next render still works. -- **The command**: `bpmn render [-o ] [--format png|svg] - [--plane ] [--max-size ] [--scale ] [--json]`; the result line - `wrote WxH px ~N image tokens scale S plane `, the other - planes and the notes; `-o -` writes the image to stdout (a PNG only when - stdout is no terminal) and the report to stderr. -- **Tests** (`test/render.test.ts`, each fails before): every file of - `tools/scenarios` and `test/fixtures` (and each other plane) renders to SVG - and PNG within 2000 px with the same bytes twice (4.5 s for 153 files in - one worker); text pixels with the bundled font only; `E_RENDER` for a - 488-byte SVG that makes resvg trap and for malformed SVG; the bpmn-js label - positions; colours and unsafe colour values; planes and notes; the budget - note; the in-memory layout; the renderer absent from an - `applyToXml`-only bundle; the command. `test/isomorphic.test.ts` compares - `renderSvg` in the browser bundle with Node; `tools/iso/check.mjs`'s - strict consumer uses `renderSvg` and `renderPng`. The synthetic - `test/fixtures/render/gallery.bpmn` has every element kind (for the - reviewer's side-by-side with `tools/render.sh`). - -Measured on this build: - -| | | -| --- | --- | -| core bundle, whole entry | 945.9 -> 966.4 KB minified (+20.4 KB: the renderer 18.9 KB, the guide text for `render` 1.5 KB), 291.7 -> 298.9 KB gzip (+7.2 KB) | -| core bundle, `applyToXml` only | 768,736 -> 768,734 bytes minified (the `text.ts` change is 2 bytes smaller), gzip 238,454 -> 238,443 | -| install | +2.53 MB (`@resvg/resvg-wasm`: 2.48 MB wasm), +27 KB font, licence and note in `assets/fonts`; no install scripts | -| time | `bpmn render` of `agent__o2c` 0.27 s wall (CLI start, parse, SVG, wasm load, PNG); in-process 3 ms SVG + 16 ms PNG per scenario on average | -| `tools/scenarios` (115 files) | all render; image tokens at the default 2000 px p50 820 / p90 2,291 / max 4,115; natural long edge p50 1,269 / p90 2,157 / max 2,852 px, 23 over 2000; 3 below the 0.75 label scale; 8 files with 10 other planes, all render; 2 without a diagram (laid out in memory) | -| private corpus (396 files, counts only) | 394 render (2 files of the manifest are missing on disk), 0 errors; tokens p50 634 / p90 1,896 / max 4,801; natural long edge over 2000 px: 66, below the 0.75 label scale: 22; 22 files with 30 other planes, all render; 24 without a diagram, 2 with elements without a shape | - -Stage 2 (above) added crops, halos, numbered problems, tiles, `--render` and -the fuzz step's rendering; stage 3 is open. - -## Profile rules on demand (2026-10-11, `feat/lazy-profiles`, issue #13) - -Between 0.3.0 and 0.4.0 the browser bundle grew by about a third, and every -host loaded both rule sets for every document. Option (a) of issue #13 is -implemented: the Camunda 7 and Camunda 8 rules load the first time a -document of that platform is validated. Decisions taken for the issue's -open questions: (1) the building blocks stay synchronous with a preload, as -the issue suggests; (2) it is done now; (3) `loadProfile(platform)` is -documented for hosts that prefetch when they open a C7 / C8 file; (4) a -chunk that fails to load is the new `E_PROFILE_LOAD` (category io), the -code the issue proposes. - -- **Loader** (`src/platform/profile.ts`): the only importer of `c7.ts` / - `c8.ts`, with `import()` (LOADERS); `feel.ts` and `schema-text.ts` come - with `c8.ts`. `loadProfile(platform)` loads one rule set (one promise per - platform; `'none'` resolves at once; a failed load is forgotten, so the - next call imports the chunk again: in Node that loads it, a browser may - keep the failed module fetch until the page reloads, Chrome does, and the - `import()` specifier is a static string the host's bundler rewrites, so it - cannot vary; the hint and the catalogue say so), `loadProfiles(platforms?)` - several (default both). `runProfile` and `validateDoc(doc, { platform })` - are still synchronous and throw `E_PROFILE_NOT_LOADED` (category usage, - `details.platform`) when the resolved platform's rules are not loaded. - Vendor descriptors, the zeebe lookups, detection and the design profile - stay in the core: the ops and design-iq's save gate need them on every - platform. -- **Rule kit** (`src/platform/rule-kit.ts`): the rule modules import - nothing of the core at runtime (type imports only). They declare the - helpers they use as module variables and export `useKit(kit)`; the loader - calls it with `RULE_KIT` right after the import (`c8.ts` passes it on to - `schema-text.ts`). Why: a module the core and a rule module both import - is put by Rollup / Vite into the host's entry chunk, and the rule chunk - then imports the entry chunk back; a host whose entry module awaits a - C7 / C8 call or `loadProfile` at top level waited for itself forever (an - earlier version of this change did; 0.4.0, without the split, did not). - Now the C7 chunk imports nothing and the C8 chunk (with `feel` and - `schema-text`) neither. Nothing of the kit may be used while a rule - module is evaluated (only inside functions): the bindings are assigned - after the import. A helper a new rule needs goes into `RULE_KIT`. -- **Pipeline** (`src/pipeline.ts`): `mutate()` awaits `ensureProfile` - before anything runs and again after the ops (they can turn a plain file - into a C7 / C8 file: the first `camunda:` / `zeebe:` content), `checkDoc()` - before it validates. A load failure rejects before a result exists. The - in-memory API, the file helpers and the CLI are unchanged. -- **Check** (`tools/iso/check.mjs`): names the on-demand chunks (Camunda 7 - rules, Camunda 8 rules, bpmn-auto-layout), fails when - `dist/platform/{c7,c8,feel,schema-text}.js` is in the initial chunks of - the whole entry or of the applyToXml-only entry (naming the module), fails - when a rule module imports (also through another rule module) a module - the entry loads (naming both: the hang above; esbuild alone never shows - it, it puts shared modules into chunks of their own, so the check reads - the module graph), and enforces `tools/iso/budget.json` (measured + about - 2 %: whole entry 830 / 261 KB, applyToXml only 630 / 201, C7 chunk 58 / - 18, C8 chunk 80 / 24; raising one is an explicit diff). Merged with - `bpmn render` (#12) the whole entry is 860.7 / 271.3 KB (`renderSvg` and - `locateProblems`, +46 KB; `applyToXml` only does not change), so its - budget is 878 / 277 KB. `--table` prints - the README table; `tools/iso/bundle.mjs` returns each chunk's text, the - module graph and the entry chunk: the output whose metafile `entryPoint` - is the requested entry (esbuild gives every `import()` target an - `entryPoint`, so the earlier `chunks.find((c) => c.entry)` was right - only because esbuild writes the entry first). -- **Tests**: `test/isomorphic.test.ts` bundles `src/index.ts` as an ESM - split bundle, writes a fresh copy per case, loads it with Node's own - loader (a CommonJS helper; vitest's `import()` would transform the chunks - and cache a failed load) and records which chunk is evaluated: plain, - design and `platform: 'none'` load nothing, a C7 file only the C7 rules, - a C8 file only the C8 rules; a batch that turns a plain file into a C7 / - C8 file loads them after the ops and reports 0.4.0's added findings - (pinned); `E_PROFILE_NOT_LOADED`, the cache, and a missing chunk - (`E_PROFILE_LOAD` with the page-reload hint, then a later call that - imports it again). It also checks that exactly the requested entry is the - entry chunk and that the rule modules import nothing the entry loads - (both fail on the earlier version of this change). Without the load - after the ops two of them fail. The ten test files that call - `runProfile` / `validateDoc(doc, { platform })` directly start with `await - loadProfiles()`; there is no global vitest setup, so the pipeline tests - run the on-demand path. - -Evidence (private corpus local, counts only): -- Sizes (`check.mjs`, KB = 1024 bytes, minified / gzip, initial chunks): - whole entry 945.9 / 291.7 -> 814.7 / 255.1 (-131.2 / -36.6); - `applyToXml` only 750.7 / 232.9 -> 618.2 / 195.8 (-132.5 / -37.1); on - demand: C7 rules 56.3 / 17.0, C8 rules 78.8 / 23.1, bpmn-auto-layout - 82.1 / 28.0. The rule kit costs the rule chunks about 0.5 KB minified - and saves the entry about 1.3 KB gzip (fewer names exported across - chunks). -- A Vite 7.3.6 host whose entry module awaits the calls at top level, run - in headless Chrome (scratch project, not in the repository): plain, - design and `platform: 'none'` fetch only the entry chunk; C7, a plain -> - C7 batch and `loadProfile('c7')` fetch only the C7 chunk; C8, a plain -> - C8 batch and `loadProfile('c8')` only the C8 chunk; all resolve, and the - added findings match 0.4.0. In Vite's chunk graph the C7 and C8 chunks - import nothing. The same page built against the earlier version of this - change stays pending for C7, C8 and the prefetch (the rule chunk imported - the entry chunk). -- A blocked C8 chunk: `E_PROFILE_LOAD` in Chrome; after unblocking, a call - in the same page fails again without a request (the browser keeps the - failed fetch), as the hint now says. -- The CLI's output is byte-identical to a 0.4.0 build (stdout, stderr, exit - code): `validate --json` on the 153 `.bpmn` files of `test/fixtures` and - `tools/scenarios` (112 plain, 7 C7, 34 C8); 52 steps of `new` / `apply` / - `set` / `ext` / `validate` sessions, text and `--json`, with the file - identical after every step (plain, plain -> C7 through `set - camunda:assignee`, plain -> C8 through `ext add zeebe:taskDefinition`, - C7, C8, `--platform` overrides); `validate --json` on the 394 private - corpus files (216 C7, 68 C8, 110 plain). After the rule kit, again - against 0.4.0 (stdout, stderr, exit code, written file): 1,224 of 1,224 - commands on those 153 files (`validate --json` auto / c7 / c8 / none, - `set name`, `set camunda:assignee`, `ext add zeebe:taskDefinition`, a - mixed `apply`) and 396 of 396 on 50 private corpus files (20 C7, 20 C8, - 10 plain). -- gate: 1,820 tests + 247 opt-in, isomorphism check (89 modules, within - the budgets, the rule modules import nothing the entry loads), layout - regression 444 (budget 444), fuzz 12 x 15 0 / 0. - -Still open (follow-ups): -- **bpmn-auto-layout under top-level await**: its on-demand chunk imports - `bpmn-moddle` (and `min-dash`), which Rollup / Vite put into the host's - entry chunk, so a host whose entry module awaits an `engine: 'auto'` - call at top level hangs, in 0.4.0 as on this branch (checked in Chrome). - A third-party module cannot take a kit; the README says to call it from - a function. The check does not cover it (`rules: []`). -- **`guide.ts`**: it is tree-shaken from `applyToXml`, but importing any one - of `guideShort`, `ERROR_CATALOGUE`, `kindsJson`, `kindsText` or - `guideText` adds 118 to 141 KB minified / 35 to 42 KB gzip (constants - computed at module scope keep the whole module). Split it per section or - behind a `./guide` subpath (an issue of its own). -- The descriptors (about 25 KB minified, 3 KB gzip) stay in the core: the - ops need them on every platform, and loading them on demand would put an - async step before every op batch (option c' of #13). - -## Step 3 follow-ups (2026-10-10, `step3/followups`) - -A second verifier round on `step3/round1` found six more issues, and the -Claude Code plugin evals (agents using the plugin on real tasks) four -more; all ten are fixed on `step3/followups` (built on `step3/round1`), -each with a test that fails on round 1. The user's decisions hold: ids -only, speaking ids in the file's style, an explicit `--id` wins. - -- **Anchor names with numbers** (`src/idstyle.ts speakingStem`, `hasWord`): - a stem with many digits counted as a machine id, so the flows and tasks - at the split after "Check 2024" were named by a kind word - (`Flow_Check2024ToGateway`, `Activity_AfterGateway`). A stem with a word - (a run of four letters with one that is no hex digit) speaks now: - `Flow_Check2024ToAfterCheck2024`, `Activity_ServiceTaskAfterCheck2024`; - `sid-...`, UUIDs and hex stay machine ids. -- **Data objects** (`idstyle.ts kindTypeNames`, `ops/artifacts.ts - backingRequest`): in a file with type-named prefixes the reference and its - `bpmn:DataObject` both wanted `dataObject_invoice` (the second took `_2` - and a `W_ID_SUFFIXED`). The reference's type-named prefix is its own type - now (`dataObjectReference_invoice` + `dataObject_invoice`; data stores - alike); where a file names both with one prefix the backing element says - what it is (`data_invoiceObject`), never a suffix. -- **DI ids** (`diagram/write.ts styledId`, `edgeStyleOf`): a DI id derived - from a long element id (`Edge_`, `BPMNShape_`, `_di`, - planes, diagrams, the edge of a flow renamed after its ends) is cut at a - word boundary to stay within 64 characters. -- **Camunda 8 values** (`platform/c8.ts dateTimeRangeProblem`, - `cronProblem`, `platform/schema-text.ts` kind `boolean`): a date that does - not exist (2030-02-30, 2031-02-29, 25:00, an offset over 18 hours) in a - timer, a cycle's start or a due date, a cron field out of Spring's range - (hour 25, month 13, day-of-week 8, a step of 0, a reversed range, `?` - outside the day fields, an unknown name; Quartz `L` / `W` / `#` pass) and - an xsd:boolean attribute that is not true / false / 1 / 0 - (`cancelActivity="maybe"`, `isExpanded="TRUE"` on a shape; bpmn-moddle - reads it as false) are `W_C8_DEPLOY_TIMER` / `W_C8_DEPLOY_USER_TASK` / - `W_C8_DEPLOY_SCHEMA` with the reason. 27 models deployed to Camunda - 8.9.22 (`test/step3-followups-c8.test.ts`), every verdict holds. -- **Duplicate ids** (`validate.ts checkDuplicateIds`): bpmn-moddle keeps the - first element and drops the later one with an import warning, so - `validate` said "valid". It is `E_DUPLICATE_ID` now, on every platform, - naming the element it could not read and its line. -- **split** (`ops/split.ts`): a join needs two branches that reach it. When - the other branches end in end events the one that continues runs on to - the old successor directly (no pass-through gateway; `joinAs` is a usage - error then); the note marks terminating ends `(end event)`. -- **space --by** (`ops/types.ts parseSpaceAmount`, `diagram/ops.ts - spaceOp`): `col`, `column(s)`, `row(s)`, `px` besides - `column`, `row` and pixels; the result notes the distance (`inserted 320 - px (2 columns of 160 px) right of X`), and a few pixels say how to ask - for columns (`--by 2` is 2 px). -- **E_NOT_FOUND** (`document.ts suggest`): ids with the query's prefix - first; the same prefix with one body inside the other is a candidate (the - eval's `Flow_GatewayPurchaseApprovedToOrderGoods` suggests - `Flow_PurchaseApprovedToOrderGoods`, not `Gateway_PurchaseApproved`). -- **Usage hints** (`cli.ts positionalHint`, `batch.ts shapeHint`): an - unknown option that is a positional argument or a key gets the command - rewritten (`add f.bpmn --kind userTask --name "Check it"` -> `bpmn add - f.bpmn userTask "Check it"`; `set f.bpmn X --name Y` -> `name=Y`); a - `route` op with `flowId` and a `set` op with its properties at the top - level get the op rewritten. Wrong shapes are still refused. -- **Batch splices** (`diagram/place.ts spliceTarget`, `chainReaches`): the - incremental layout placed a node as a splice only when its own flow had - ended at the successor before the ops; in one batch that splices a task - and then a gateway after it, the task's successor is the new gateway, so - both went to a new branch row below (backward flow, crossing). A straight - chain of new nodes is spliced as a whole now: the eval's model gets the - same geometry as with two commands (score 0; before 9). -- **Labels inside pools** (`layout/place.ts framedSize`, `nodeLabels`; - `layout/engine.ts` pool sizing, `declutterLabels`): a full layout made - pools and lanes as wide as the shapes, so an end event's label at the - right edge stuck out of the pool (the eval's order-to-cash model). Pools - and lanes hold the labels right of their content, the last lane the - labels below it, and a label the engine moves aside takes a free spot - inside its frame first. Sub-processes are not resized for labels (that - moved collaboration annotations on two corpus files). - -Evidence (private corpora local, counts only; round 1 -> this branch): -gate 1,786 tests + 247 opt-in, isomorphism check (88 modules), layout -regression 444 (budget 444), fuzz 12 x 15 0 / 0; Camunda 8 engine suites -519 / 519; the new rules report nothing on the 38 real Camunda 8 files (the -agreement 38 / 38 holds) nor on 394 corpus files; the verifier's id probes -(96 files still on disk, 1,513 edits): `W_ID_SUFFIXED` 193 -> 142 -(annotation + data 51 -> 0), flows with a kind word for an end 53 -> 10 -(ends at unnamed gateways with hashed ids), ends that read alike 10 -> 7, -ids over 64 characters 8 -> 0 (longest 61); roundtrip on the 291 real -files unchanged (no-op identical 262 / 278 in layout auto, 278 / 278 with -`--no-layout`, rename 2 lines, insert 56 lines median); full layouts of 509 -files: `labelOutsideFrame` 188 -> 160, score 2,653 -> 2,599; bundle 945.9 / -291.7 KB (whole entry, minified / gzip), `applyToXml` 750.7 / 232.9 KB. -Details: [docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-follow-ups-2026-10-10). - -Still open from these: a label left of a sub-process's content can stick -out of it (sizing sub-processes for labels moved other things), labels -above a gateway are not counted into a pool's height; new flows that end at -an existing unnamed gateway with a hashed id still read `...ToGateway` (the -documented fallback); two unnamed tasks after one unnamed gateway still -differ only by `_2` (documented), and in some camelCase files the flow to -the second reads `afterXToAfterX_2` (7 in the probes). - -## Step 3 round 1 fixes (2026-10-10) - -An independent verifier re-checked `step3/round1` (gate, dogfooding on large -real models, the corpora, the four engines). Its findings were fixed on -three branches built in parallel on round 1, `step3/fix-ids`, -`step3/fix-c8` and `step3/fix-views`, and merged on `step3/round1` together -with the leftover of the ids branch and the integration below. The user's -decisions hold throughout: elements are addressed by id only (names -repeat), new ids speak in the file's prefix, separator and case (never a -random, hash or numbered id), an explicit `--id` wins. With #53 fixed the -audit stands at 49 fixed, 5 partly fixed, 23 open of 77. Tables and -evidence (counts only): -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-round-1-fixes-2026-10-10). - -### Ids and report (`step3/fix-ids`) - -The verifier's findings on ids and on the report (one medium, seven low). -Every fix has a test in `test/step3-ids-report.test.ts` that fails on -round 1. - -- **Flows at unnamed elements** (`src/idstyle.ts labelOf`): an unnamed flow - end is named by the speaking part of its own id, its kind and place - (`Gateway_AfterCheck` -> `AfterCheck`; the join of the split after Check - -> `CheckJoin`, `joinLabel`), not by a kind word, so the flows at two - unnamed gateways differ without `_2` (`Flow_CheckToAfterCheck`, - `Flow_AfterCheckToCheckJoin`, `Flow_FixJoinToCheckJoin`; round 1: - `Flow_CheckToGateway`, `Flow_GatewayToJoin_2`, `Flow_JoinToJoin`). A flow - renamed after its new ends (`followEnds`) gets the same rule; a flow an - earlier build named with a kind word (`Flow_CheckToGateway`) still counts - as named after its ends (`formerLabels`). Existing ids stay. -- **The context once** (`contextOf`, used by add, split, artifacts, lanes and - the redraw's id-less elements): after / before an unnamed anchor placed - the same way, the nearest named anchor (`Event_EndAfterCheck` after - `Gateway_AfterCheck`; never `AfterAfter...`). An unnamed gateway or - activity whose id is taken, or whose body another id has (the unnamed - gateway it follows), first spells out its kind (`IdStyle.spelledBase`: - `Gateway_ParallelAfterCheck`, `Activity_ServiceTaskAfterCheck`; not where - the prefix says the kind, `Task_`, `parallelGateway_`); the merged build - then names the anchor (see "The fixes together"), and only then `_2`. -- **Length** (`src/ids.ts cutAt`, `MAX_ID` = 64): bodies are cut at a word - boundary (a name at 40 characters as before, now at a word; a join's - `Join` and a number at the end are kept), the two ends of a flow share - the room (`fitPair`), a suffix never makes an id longer than 64; - `derivedBases` still recognises a flow a file got before the cap. -- **Transliteration** (`transliterate`): the letters NFKD does not reduce to - a base letter are spelled out (`ø` -> `oe`, `å` -> `aa`, `æ` -> `ae`, `œ` - -> `oe`, `ł` -> `l`, `đ` / `ð` -> `d`, `þ` -> `th`, `ı` -> `i`); a name - without a letter (`123`, `✓✓✓`, `审批 订单`) names nothing - (`hasIdWords`): the element gets the id of an unnamed one. -- **`bpmn new`** (`Doc.create`): `--name` or a speaking `--id` names the - definitions (`Definitions_OrderToCash`, `Definitions_Billing`); without - either they stay `Process_1` / `Definitions_1`. Round 1 wrote - `Definitions_1` also with `--name` (this handover said otherwise). -- **Warnings delta** (`src/pipeline.ts validationDelta`, `src/result.ts - ChangeSet.rename`, `src/ops/index.ts runBatch`): (a) the platform - profile's findings the file had (`W_C7_*` / `W_C8_*`) count among "already - in the file", so `preexistingCount` is what `bpmn validate` lists (a lint - warning such a finding says again counts once, as there); (b) a group - warning (`W_DUPLICATE_NAME`) stays the old one while its group only - shrinks, list warnings (`W_BRANCH_NAME`, `W_IMPLICIT_SPLIT` / `_JOIN`) - while their elements are among the old ones; (c) a bridge that takes the - id of the flow the change removed is a takeover (`Rename.takeover`): the - bridged flow's old id is removed, the taken id changed, no `renamed`; - (d) op warnings about the model's state (`W_LANES_WITHOUT_POOL`, - `W_IMPLICIT_SPLIT`, `W_IMPLICIT_JOIN`) are checked against the batch's - final state (`stillHolds`); (e) a document created in memory has an empty - platform baseline, so `new --target camunda8` lists - `W_C8_DEPLOY_START_EVENT` (not `W_NO_START`), which the next edit - resolves. - -### Camunda 8 (`step3/fix-c8`) - -The independent verifier found the profile's deploy recall at 0.74 on 200 -adversarial models (W_C8_DEPLOY_* only; precision 0.94), false deploy -findings for white-space values and a lower-case date-time, no way to -address the message an op creates in a batch, a `zeebe:subscription` on the -event (where Camunda 8 ignores it), and `kinds --help` without the -zeebeElements section. Fixed, each rule checked on Camunda 8.9.22: - -- **FEEL is parsed** (`src/platform/feel.ts`, no dependency, about 23 KB / - 7 KB gzip with the schema-text rules): a tokenizer and a recursive-descent - reading of the grammar Camunda 8.9 parses at deploy, written from 766 - deployed expressions (`test/fixtures/c8/feel-verdicts.json`: 762 of them, - synthetic, with the verdict; the check agrees on every one). `feelProblem` - (c8.ts) keeps the JUEL messages and then runs it. Camunda's parser is - lenient where a plain grammar is not (`x andy` is `x and y`, `ifx then 1 - else 2` an if, `x instanceof` is `x in stanceof`); the check splits a - keyword glued to a name the same way. Messages name the FEEL spelling - (`<> (FEEL: != ...)`, `if ... then without else`). Now also checked: a - signal name and a published message name starting with `=`, the - `zeebe:adHoc` expressions, an ad-hoc sub-process's completion condition - (`set completion=` sets it). -- **Schema rules from the file's text** (`src/platform/schema-text.ts`, - W_C8_DEPLOY_SCHEMA): child elements out of the XSD's order (from - bpmn-moddle's property order, with the places the XSD has a choice or - orders differently as one slot), IDREFs that name no id (lane - `flowNodeRef`, `default`, `dataObjectRef`, data associations, IO sets), - ids that are no NCName (`a:b` in the model, with `set id=`; ones - bpmn-moddle drops on import, `F:1`, from the text). Issues of an element an - edit removed are dropped; the text is read once per document text. -- **Values**: `PT1.5H` is refused (a fraction on seconds only), a lower-case - `t` / `z` in a date-time deploys; a job type, message / signal name, error - / escalation code, process / decision id, result variable, listener type, - linked resource type or published correlation key of white space deploys - (only empty is refused: `missing()` in c8.ts), an empty priority deploys, - an empty link name is refused. -- **`refAs`** on add / connect / set / retype binds the bpmn:Message / - Error / Signal / Escalation the element references after the op - (`bindRefAs`, ops/events.ts; E_USAGE when none or several); in the alias - table, `--summary` and `aliases`. -- **The message a catching element waits for**: `ext add - zeebe:subscription` and `set - zeebe:correlationKey=` write the message's `zeebe:subscription`, with a - note naming the other elements that share the message (redirect, not a - refusal: the target is unambiguous, the result says where it went); an - element without a message is E_WRONG_HOST with the `set message=` to run - first. Camunda 7 has no such element (its descriptor has no message - correlation key), so nothing changed there. -- `kinds --help` lists the sections from the section table; a test checks - that every `kinds --section` the guide names exists. - -Evidence on the branch (outside the repository; counts only): 126 probe models of this -round agree with the engine (before: 22 disagreed); the verifier's 200 -adversarial models deployed again: W_C8_DEPLOY_* precision 0.938 -> 1.000, -recall 0.741 -> 0.951 (the 4 left get a structural error, so any finding: -recall 0.815 -> 1.000, precision 0.846 -> 0.910; the 8 left are structural -rules stricter than Camunda 8, a start event with an incoming flow and -similar, and an id with an umlaut bpmn-moddle cannot read); the 38 real -Camunda 8 files: the profile agrees with the engine on 38 / 38, 0 of 97 -distinct real FEEL values flagged; the C8 edit battery (8 edit types, 304 -edits): 0 regressions, 299 / 299 agree; its message edit with `refAs` or -the redirect instead of an id guess: 74 / 76 written (the 2 others keep an -existing message's other key: E_DUPLICATE_EXTENSION), 0 regressions, 0 -misplaced subscriptions; the C8 engine suites 460 / 460. Tests: -`test/step3-c8-fixes.test.ts`. - -### Views and layout (`step3/fix-views`) - -The verifier's findings on the reading views and the format ops, and two -items the ids package left open (#14 for `move`, #53). - -- **`show --context`** (`src/context.ts elementContext`, `format.ts - renderContext`): `caught by:` lists every boundary event of every - sub-process around the element (message, timer, signal, conditional, - error, escalation, compensation; inner first, non-interrupting marked; - the element's own boundary events stay `boundary:`, the event - sub-processes that catch it `event sub-processes:`, both of every kind); - `message:` (id, name, Camunda 8 correlation key; `ElementContext.message`) - on message events and send / receive tasks, which also list the message - flows of their message drawn to their pool (`DetailMessageFlow.at`, - `(at pool P)`); a message flow: `message:`, `in: collaboration`, `from:` / - `to:` with their pools (`ContextLink.pool`); a message, signal, error or - escalation: `used by:` (`usedBy`). `view.ts`: `correlationKeyOf`, - `messageFlowEntry`. On 394 files the median stays 430 bytes (middle - activity, p90 916; every node 441, before 424; message nodes 501). -- **Format ops never take a shape out of its sub-process** - (`diagram/ops.ts`): `checkFrames` takes the references: a sub-process - grows towards a reference inside it, never out to one outside it - (`E_LEAVES_CONTAINER`); `alignOp` leaves out (note `left out - (sub-process ): ...`) a member only a selector named - (`resolveSelection` returns `picked`); `makeRoom` refuses (`E_NO_ROOM`) - when making room would take a shape out of a sub-process, lane or pool - that held it. On 396 files: 0 runs adding `outside*` (19 before), more ops - succeed (6,412 / 7,467, before 6,057); with `--path` / `--branch` sets - (align, tidy, place; 6,295 runs) 0 add `outside*` (1 before). -- **`move` into a cross-lane flow** (#14 for move; `src/ops/lanes.ts` - `inheritedLane` / `laneInheritedWarning` / `crossLaneNote`, shared with - `add`): a node without a lane at its new place gets add's lane - (`W_LANE_INHERITED`); a node in a lane keeps it, with a note when the flow - runs between two other lanes, and is drawn on a row of that lane - (`diagram/place.ts placeAfter`: after a branching anchor, the target's row - in another lane than the node's own is replaced by a free row of its lane, - `rowInLane`; before, the anchor's lane stretched over to the target's row, - +125 px for the next lane, +265 px two lanes away; `add --lane` the same). -- **A plain `remove` of a join** (#53, `ops/remove.ts isMerge` / - `refuseSynchronisingJoin`): a merge that does not synchronise is bridged - from every predecessor like `--bridge-all`; a parallel / inclusive / - complex join is `E_AMBIGUOUS_BRIDGE` with the two exact commands. The - verifier's one Camunda 7 deploy regression (a plain remove in its - battery) deploys now; on the battery's 66 merges: 62 bridged, all deploy - on the three engines, 4 joins refused, 0 `W_UNREACHABLE` (before: 43 - refused by the engines, 379 `W_UNREACHABLE`). With #53 fixed the audit - stands at 49 fixed, 5 partly fixed, 23 open of 77. - -Tests: `test/views-context.test.ts`, `test/format-frames.test.ts`, -`test/move-lanes.test.ts`, `test/remove-join.test.ts` (each fails on -`step3/round1`); `test/diagram-format.test.ts` expects `E_LEAVES_CONTAINER` -for the column outside a sub-process (was `E_NO_ROOM`), the sub-process test -of `test/ops-mutate.test.ts` removes without bridging. - -Evidence on the branch (private corpora local, counts only): `npm run gate` passes with -1,635 tests + 159 opt-in, the isomorphism check (86 modules, 904 / 277 KB -minified / gzip), layout regression 115 files score 444 (budget 444) and -fuzz 12 x 15 (0 errors, 0 warnings); the layout bench (175 models, 935 -edits, 525 global runs) against `step3/round1`: no regression, every -figure equal (2 semantic failures in both, 0 runs adding hard defects). - -### The fixes together (`step3/round1`) - -Merged in the order ids, Camunda 8, views (merge commits). Git merged the -code of all three without conflict except one import line of -`src/ops/add.ts` (`bindRefAs` of the Camunda 8 branch next to the lane -helpers of the views branch); the docs were merged by hand. Then, each with -a test that fails on the merge before it: - -- **Unnamed elements in a row** (the ids branch's leftover; `src/idstyle.ts - anchorContext`, `IdRequest.anchorKind`, `IdStyle.qualifiedBase`, - `fitPair`). In a file whose task prefix says the kind (`Task_`, `task_`, - the older Camunda Modeler's files) a task after an unnamed gateway had - the gateway's body (`Task_AfterCheck` after `ExclusiveGateway_AfterCheck`: - `SequenceFlow_AfterCheckToAfterCheck`) and the next one `_2`. Now the - context of an element after an unnamed anchor carries the anchor's kind, - and where the id would repeat the anchor's (the base or its body taken, - the spelled-out kind taken or not possible) the element names that - anchor: `Task_AfterCheckGateway` ("after the Check gateway"), then - `Task_AfterCheckGatewayTask`; in the default style - `Activity_ServiceTaskAfterCheck`, then `Activity_AfterCheckServiceTask` - (round 1 and the ids branch: `Activity_AfterCheck`, the gateway's body). - Two unnamed tasks right after the same unnamed gateway take `_2` on the - form that names the gateway (`Task_AfterCheckGateway_2`), never the - gateway's body. The two ends of a flow that begin with the same word - (the elements after one anchor) keep their last word when the 64 - character cap cuts them (`cutKeepingLast`), so a long anchor name no - longer makes `flow_afterXToAfterX`. `W_ID_SUFFIXED` is a signal again: a - name the file already has, a second flow between the same pair, or - unnamed elements that only an index tells apart. -- **`--summary`** (`src/report.ts mutationSummary`): an element the change - created is listed once, under `created` (before, a batch in lanes listed - every new node under `changed` too, for the lane it got, and a message - `refAs` named and an `ext` op then completed). The full result keeps those - change entries. -- Checked together (`test/step3-fixes-integration.test.ts`): the message of - `refAs` in the aliases and `--summary` next to speaking ids; the - subscription redirect of a receive task in the warnings delta (the deploy - finding resolved once, nothing added, the note in the result); a plain - remove of a merge renames the bridged flows after their new ends - (`renamed`), a parallel join is refused with both commands; `move` out of - a sub-process into a cross-lane flow takes add's lane - (`W_LANE_INHERITED`) and renames the split flow after its new ends. - -### Evidence on the merged build - -Private corpora used locally, outside the repository; counts only. - -- **Gate**: `npm run typecheck` clean; `npm run gate` 1,729 tests + 219 - opt-in (engine tests), isomorphism check (88 modules), layout regression - 115 files score 444 (budget 444), fuzz 12 x 15: 0 errors, 0 warnings. -- **Engines**: the Camunda 7 live-engine suites 804 / 804 (Camunda 7.24.0, - CIB seven 2.2.0, Operaton 2.1.5); the Camunda 8 suites 460 / 460 (Camunda - 8.9.22). -- **Camunda 7 real-file battery** (50 files, 48 deployable on all three - engines before; 8 edit types: rename, an external task with an input - parameter, a plain remove, a boundary timer path, a split with a JUEL - condition, retype, compact, `remove --bridge-all` of a join): 373 edits, - all written, 0 regressions on any of the three engines (round 1: 1, the - plain remove the views branch fixed). -- **Camunda 8 corpus** (38 real files, 4 refused by Camunda 8.9.22): the - profile agrees with the engine on 38 / 38, 0 false deploy findings. -- **Roundtrip** (`tools/roundtrip.mjs`; 289 of the 291 real files are - still on disk, 276 with a rename target): a no-op is byte-identical in - 260 / 276 in layout auto (260 / 263 of the files with a diagram, 98.9 %; - the 13 without one are drawn) and 276 / 276 with `--no-layout`; a rename - changes 2 lines (median, p90, max); an insert 56 lines in the median, a - region of 83.2 %. Round 1 gives the same figures on the same files. -- **Ids** (the ids branch's probe set: 15 edits on each of 271 real files, - 4,017 edits, 21,000 new ids, 10,144 new flows; round 1 -> `fix-ids` -> - merged): `W_ID_SUFFIXED` 394 -> 542 -> 221, of it unnamed elements in a - row and names without letters 11 -> 332 -> 11 (all 11 in files with the - `scopedTo` flow form); new flows whose two ends read alike 168 -> 507 -> - 3; flows with a kind word for an end 1,879 -> 178 -> 178; ids over 64 - characters 2,533 -> 0 -> 0 (longest 110 -> 61 -> 61); - `tools/speaking-ids.mjs` on the 289 files: 1,731 / 1,731 new ids speak, - two edits with unnamed elements at two places share an id in 0 / 223 - file pairs. The rest of - `W_ID_SUFFIXED`: a data object, task or pool of a name the file already - has, a second flow between the same pair (also the bridge of a remove), - and the `AfterToAfter` flows of the `scopedTo` files (nested splits). -- **Browser bundle**: whole entry 936.5 / 288.2 KB minified / gzip, - `applyToXml` 742.3 / 229.7 KB (+82 KB bpmn-auto-layout on demand); - round 1: 897 / 275 and 710 / 219 KB, most of the growth the FEEL parser - and schema rules of the Camunda 8 fixes. -- **Docs**: `guide --short` 4,172 bytes (budget 5 KB). - -### Still open after the round 1 fixes - -- **Breaking for hosts** (design-iq): the mutation JSON / `EditReport` has - `warnings: {added, resolved, preexistingCount}` and no - `validation.warnings`; `--json` is compact; `show` puts `lane=` on each - node and writes message flows differently; new ids speak and a flow whose - id named its ends is renamed (`renamed` in the result). -- Ids: two branches adding an element of the same name (or an unnamed one - at anchors of the same name) get the same id, a merge of both a - duplicate; a flow renamed after its ends is the one exception to "ids - never change" (`Doc.followFlowEnds = false` in the library, no CLI - switch; since #32 `--keep-flow-ids`, see the section at the top); `bpmn new` without `--name` (and without a speaking `--id`) - writes `Process_1` / `Definitions_1`; `move` of a node with several - incoming flows still disconnects them (only `remove` bridges a merge), - and removing a join together with its predecessors (the join first) is - refused rather than bridged; a context is read from the anchor's id, so - a cut anchor id gives a cut context - (`Gateway_After4AugenPrinzipPruefenFreigeben2`); in files with the - learned `scopedTo` flow form (`Flow_KotO_ValidateToReserve`: the first - word of each end) the flows between unnamed elements after one anchor - read `AfterToAfter` and take `2`. -- Views: no `find --attr` / `list --fields` projections, no `vars` view, - no `render` command; the full `show` keeps vendor values but not the - extension summary; JSON is compact but not slimmer; `--strict` still - counts every warning, also the file's own. -- Layout: compaction closes strips only, pools side by side are not - compacted against each other, a full redraw orders pools by its own rule; - open bugs #13, #15, #16, #19, #25, #63–#68, #70, #76 (#40, #74 partly); - the fuzzer still finds `place` / `align` pushing a flow through a shape - and a boundary event added on a host inside a sub-process intruding into - another frame (the build before step 3 does the same); format ops with - selector sets still add overlaps or a flow through a shape on some real - files (as many as before the round 1 fixes, which removed `outsideSub`). -- Camunda 8: FEEL syntax is checked but not its meaning (types, functions), - variables are not followed, `zeebe:publishMessage` is reported as not - run, event-based gateways of a file without `` lists have - no repairing command, forms / called processes / decisions are not - checked against a deployment; some structural rules are stricter than - Camunda 8 (a start event with an incoming flow and similar: 7 of the - verifier's accepted models get an error), an id with an umlaut is - unreadable for bpmn-moddle; the Camunda 7 profile does not check timer - values (`PT2D` deploys there and fails at run time). -- `--summary` drops the notes, also the one that says where a - subscription given to a catch event went (`changed:` names the message); - the full result and `--json` keep it. -- The core grew by about 220 KB minified / 64 KB gzip since 0.3.0 (most of - it the Camunda 8 profile, its descriptor and the FEEL parser): a lazily - loaded profile per platform would be the lever. - -## What step 3 round 1 changed (2026-10-10) - -Four packages were built in parallel on `step3/views`, `step3/ids`, -`step3/layout` and `step3/camunda8` (each on 0.3.0) and merged on -`step3/round1`. Two decisions of the user hold for all of them: elements -are addressed **by id only** (names may repeat; there is no name-based -addressing anywhere, `--around ""` is `E_NOT_FOUND`), and **new ids -speak** (`Activity_CheckInvoice`, `Gateway_InvoiceOk`, -`Flow_CheckInvoiceToBookInvoice`; never a random, modeler-hash or hash id) -while following the file's prefix, separator and case; an explicit `--id` / -`"id"` always wins. 18 audit bugs are fixed and 3 partly (#12, #14, #21, -#22, #28, #39, #41, #42, #48, #58, #60, #61, #62, #69, #71, #72, #73, #77; -#40, #53, #74 partly): 48 fixed, 6 partly fixed, 23 open of 77 (after the -round 1 fixes above 49, 5 and 23). Tables and evidence: -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-round-1-2026-10-10-the-packages-together). - -### Views and output (`step3/views`) - -Goal: token-efficient reading for agents on large models and output that -does not grow with the file's old problems. Elements are addressed by id -only (names may repeat; no name-based addressing). Fixed audit bugs #21, -#22, #58, #60; details and measurements in -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-2026-10-10-views-and-output). - -- **`show --around [--depth n] [--inner]`** and **`show - --context`** (`src/context.ts`: `aroundView`, `elementContext`, - `implementationOf`; renderers `renderAround` / `renderContext` in - `src/format.ts`; `viewDoc` / `renderShown` in `src/api.ts` are what the CLI - and `viewXml` / `showXml` share, the option checks included). The window - follows sequence flows both ways (boundary event <-> host, link pairs; a - sub-process's first / last node leads out to it, `--inner` also in), lists - per node lane, vendor values and a compact extension summary (`io: in a; - out b`, `zeebe:taskDefinition type=...`, `type xN`), `<-` incoming flows - from outside, the omitted counts, and the window's message flows, - annotations and data. The context lists pool > process > sub-processes, - the effective lane (`via` a sub-process or a boundary event's host, - `within` parent lanes), from / to with names, own boundary events, every - boundary event of the sub-processes around it (`caught by`; error / - escalation only until the verifier fixes below), the event sub-processes - of every scope around it (not the one it is in), annotations, message - flows with partner and pool, data. -- **`show`**: `lane=` per node, the lanes section is a tree of names; - message-flow lines with endpoint names; collaboration annotations - (`ModelView.collaboration.annotations`). **`show `**: `messageFlows`, - `annotations`, `attachedTo`, `data` (`src/view.ts`: `messageFlowsOf`, - `annotationsOf`, `dataLinksOf`, `poolOf`). -- **Warnings as a delta** (`src/pipeline.ts validationDelta`, - `MutationResult.delta`; `src/report.ts`): a lint warning the file had - (code, element, related elements, followed through renames) and every - `W_PREEXISTING_ERROR` are counted, not listed; resolved = the lint warnings - and errors the file had that are gone, plus the platform's and validators' - resolved ones. A document created in memory (`new`) has no delta: all its - warnings are added. The report: `warnings: {added, resolved, - preexistingCount}`, `validation` without `warnings` (breaking for - consumers of the JSON / `EditReport`; `MutationResult.validation.warnings` - is unchanged, `--strict` still counts every warning). Text: added warnings - (`warningLines`: three or more of one code as one line), `resolved:`, one - count line. `--summary` (`mutationSummary` / `renderSummary`). -- **Docs cost** (`src/guide.ts`): `guideShort()` (3.8 KB; test: <= 5 KB), - `guideTopic(topic)` slices the full guide by its headings (`GUIDE_TOPICS`; - new sections READING LARGE MODELS, OUTPUT, CAMUNDA 8), `kinds --section` - (`kindsSections` / `kindsSectionText` / `kindsSectionJson`). Every - `--json` is compact, `--pretty` indents. -- **stdin / stdout** (`src/cli.ts readDocArg`, `runMutation`): `-` as the - file for every command; a mutation of stdin or `-o -` writes the XML to - stdout and the report to stderr; `apply - -` is `E_USAGE`. - `layoutDocToFile` (node layer) serves `layout -`. - -Tests: `test/views.test.ts` (views, #21, #22, the byte budget per node), -`test/report-delta.test.ts` (delta, floods, summary), `test/cli-views.test.ts` -(the CLI: flags, compact JSON, `--summary`, stdin / stdout, `guide --short` / -topics, `kinds --section`, the CAMUNDA 8 recipe run), the isomorphism test -runs `--around` / `--context` in the browser bundle; synthetic fixture -`test/fixtures/views/claims.bpmn`. Gate on the branch: 1,354 tests + 1 -opt-in, layout score 444, isomorphism and fuzz unchanged. - -Evidence (private corpora, local only): `--around` (depth 2) on 278 real -files 1.2 KB median / 2.1 KB p90 vs `show` 2.3 / 7.6 KB vs PR #218's -`outline --around` 1.4 / 2.4 KB; on the 30 models with >= 30 flow nodes -1.8 KB vs 10.6 KB; 236 bytes per shown node (median); 0 failures on 383 -files. A rename's text result on the 97 real files with old warnings: 523 --> 245 bytes median, JSON 2,597 -> 1,276. - -Still open from this package (after the integration): `find --attr` / `list --fields` projections -and a `vars` view (where is variable X used) were not built; `render` is -not a command; the full `show` -keeps vendor values but not the extension summary of `--around` (to keep -its size); JSON is compact but not slimmer (the model view still repeats -`type` / `extensions` / `extensionElements`). - -### Speaking ids and batches (`step3/ids`) - -User decisions: no name-based addressing (elements are addressed by id, -names may repeat), and every new id must speak. Branch `step3/ids`; table -and evidence in -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-2026-10-10-speaking-ids-and-batches). - -- **Speaking ids** (`src/idstyle.ts`): `next(req)` always builds the id from - words: a named element from its name, an unnamed one from a word for its - kind and its context (`IdRequest.context`, given by the ops: - `After `, `Before `, `On `, `In `; - `Gateway_AfterCheckInvoice`, `Event_TimerOnReview`, - `Event_EndAfterTimerOnReview`), a flow from its ends (`flowRequest`, - `labelOf`: the speaking part of an end's id, its name when the id says - too little, else a kind word; `Flow_CheckInvoiceToBookInvoice`), the rest - from its owner (the verifier's fixes after round 1 refined the unnamed - cases, see below). The prefixes, - the body case and the file's own flow forms are learned as in step 2; - hashed and numbered files lend their prefix and separator to the new - `named` flow form; `hash7` and running numbers are gone. Every collision - suffix is recorded by `Doc.allocateId` and reported as `W_ID_SUFFIXED` - (`Doc.reportSuffixed`, called by `runOp`, `addElement`, `connectElements`). -- **Flows follow their ends** (`ops/flows.ts followEnds`, called by - `redirectFlow` and `spliceIntoFlow`): a flow whose id is the one the - style gives a flow between its old ends (also with a suffix, also built - from an end's former name) is renamed after its new ends, its derived DI - edge id with it; the change says `renamed from ...`. `ChangeSet.rename` - rewrites the entries of earlier ops of the batch. `Doc.followFlowEnds = - false` keeps ids (the engine-ownership check simulates a remove). -- **Batch aliases** (`src/ops/aliases.ts`, `ops/index.ts runBatch`): `as` - (add, connect, split, split nodes), `flowAs` (add, split nodes: the flow - into the node), `joinAs` (split); aliases bind elements - (`ChangeSet.bind`), are resolved per op (format ops at the end of the - batch), checked by `parseOps` (`checkAliases`); `MutationResult.aliases` - / the `aliases:` line. `mutateDoc` resolves typed ops the same way. -- **E_NOT_FOUND** (`Doc.suggest`, `Doc.batch`): ranked candidates (renamed - in the batch, case / umlaut / other or no prefix, containment, typos with - `editDistance` from `src/ids.ts`, names), the batch's own ids first and in - the hint. -- **remove** (`ops/remove.ts`): `--with-branch` (`branchOf`: the nodes only - the root leads to, boundary paths and compensation handlers included; - `E_AMBIGUOUS_BRANCH` for a node several paths reach) and `--bridge-all` - (a join from every predecessor; `E_AMBIGUOUS_BRIDGE` for a split). -- **add into a cross-lane flow** (#14, `ops/add.ts inheritedLane`): the lane - of the row the incremental layout uses (the target's after a branching - source), `W_LANE_INHERITED`; decided before the placement. - -Tests: `test/speaking-ids.test.ts`, `test/batch-aliases.test.ts`, -`test/not-found.test.ts`, `test/remove-branch.test.ts`, -`test/lane-splice.test.ts`; expectations of hashed / numbered ids in the -older tests now name speaking ids. Tool: `tools/speaking-ids.mjs`. - -Evidence (291 real files, counts only): new ids that speak 1,745 / 1,745 -(0.3.0: 645 / 1,365); the prefix step 2 learned kept for 1,628 / 1,630; two -independent edits at two places share a new id in 0 / 225 file pairs (0.3.0: -9), with unnamed elements 3 / 225 (0.3.0: 9), with the same name 225 / 225 -(0.3.0: 219: a name gives the id); 380 flows renamed after their ends; an -insert changes 70 lines (median; 0.3.0: 66). Gate on the branch: 1,353 tests, layout -regression 444, fuzz 0 errors; 80 x 25 fuzz walks as 0.3.0. The README's -Camunda 7 example with speaking ids deploys on the three Camunda 7 engines. - -Still open from this package: two branches that add an element of the same -name produce the same id (git merges both: a duplicate id), the same for -unnamed elements at anchors of the same name; renaming a flow after its ends -changes a few more lines of an insert (the flow, its ends' mirror lists, its -DI edge); `Process_1` / `Definitions_1` of `bpmn new` without `--name` are -tool defaults, not speaking (round 1 wrote `Definitions_1` with `--name` too; -since the round 1 fixes `--name` or a speaking `--id` names the -definitions). (`move` into a cross-lane flow and a plain `remove` of a join, -open here, are fixed by the round 1 fixes below.) - -### Layout ergonomics (`step3/layout`) - -Formatting a kept drawing without XML, the operations the dogfooding agents -missed (branch `step3/layout`; elements are addressed by id only, names can -repeat). Details, measurements and the bug statuses: -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-2026-10-10-layout-ergonomics). - -- **`compact [...]`** (`src/diagram/compact.ts`, - op `compact`): closes empty columns / rows and shrinks expanded - sub-processes, pools and lanes to their content, inside out, keeping order - and relative positions. Each strip is closed with `space.ts closeStrip` - inside a guard (`ops.ts guard`: reroute what broke, measure, undo a strip - that adds a hard problem or raises the score, note it). Message flows do - not hold a strip open (their bends and labels are squeezed). Negative - `space --by -column|-row|-` closes empty space at one place. - Real corpus: 0 hard problems added, score never raised; total area -9 % - (hand / broader corpus), -14 % on models edited in agent sessions, up to - -47 %; 8-31 ms median. -- **Pool order**: `order ` (ops JSON - `pools`, black boxes included): participants and pool bands restacked, - message flows routed again (`E_NOT_PARTICIPANT`). -- **Selectors** for place / align / color / tidy (`src/diagram/select.ts`): - `--path [--via ]`, `--kind `, - `--branch ` (`place --branch F --below X` moves a branch up to the - join); `E_NO_MATCH` when they name nothing. -- **`show --layout`**: column indices (c0..cN) per node across the diagram, - wide empty gaps between columns, ` … ` where a wide gap splits a row (JSON - `columns`, `gaps`). -- **Metrics**: soft kinds `backwardFlow`, `segmentOverlap` (flows merging on - one line before their target), `labelOutsideFrame`, `messageLabelFar` - (`QUALITY_KEYS`, weights < 6). -- **Audit bugs fixed**: #12, #39, #41, #42, #61 (`layout.reshaped`, - `format[].reshaped`), #62, #69, #71, #72 / #73 (reported by the new kinds), - #77 (a splice pushes only by the missing room); #40 and #74 partly. -- **Fuzzer**: generators for compact, closing space, pool order, selectors. -- Tests: `test/step3-compact.test.ts`, `test/step3-order.test.ts`, - `test/step3-select.test.ts`, `test/step3-view.test.ts`, - `test/step3-audit.test.ts`, new cases in `test/diagram-metrics.test.ts`. - Gate on the branch: 1357 tests (+39), layout regression 444, fuzz 0 errors - and 0 warnings (before: 2), browser bundle 746 / 234 KB minified / gzip. - -### The Camunda 8 profile (`step3/camunda8`) - -Goal: mirror the Camunda 7 work for Camunda 8 (Zeebe), every rule checked -against Camunda 8.9.22 (REST v2). Fixes #28 (no Camunda 8 rules). Table with -the findings, the rules and the evidence: -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-3-2026-10-10-camunda-8-profile). - -- **Zeebe descriptor as data** (`src/platform/zeebe.ts`): `zeebe-bpmn-moddle` - 2.0.0 (pinned development dependency) is inlined as - `src/platform/zeebe-descriptor.ts` by `tools/gen-camunda-descriptor.mjs` - (which now writes both descriptors; `--check`; `test/isomorphic.test.ts` - compares both with the installed packages). Never registered with - bpmn-moddle. `zeebeType`, `zeebeAllowedOn`, `zeebeContainersOf`, - `zeebeNestedOnly`, `zeebeAttr`; corrections where Camunda 8.9 differs - (`zeebe:subscription` on bpmn:Message, `zeebe:properties` anywhere, - `zeebe:publishMessage`, which the descriptor does not know). -- **Camunda 8 profile** (`src/platform/c8.ts`, wired in `profile.ts`; - `report.ts` counts its findings like Camunda 7's; `validate.ts` drops the - lint warnings a C8 finding repeats): 34 codes, 22 `W_C8_DEPLOY_*` (Camunda - 8 refuses the file), 11 runtime, 1 practice; hints are exact commands - (single elements rebuilt with `--replace` and every attribute they keep). - Checks executable processes (also ad-hoc content, which Camunda 8 - validates; a file without an executable process is refused), the root - elements they use and the schema rules on the whole file. Timer values - (`validDuration`, `validDateTime`, `validCycle`) and the FEEL slips - (`feelProblem`: `&&`, `||`, `==`, `!`, `${...}`, single quotes, - brackets, a dangling operator; FEEL is not parsed) were decided by - deploying 60 timer values and 45 expressions. -- **Operations**: `new --target camunda8` writes - `modeler:executionPlatformVersion="8.9.0"`; `add` / `retype` give a user - task of a Camunda 8 file `zeebe:userTask` and a new event definition an id - after its event (`src/ops/platform.ts`; since the integration through the - id style: Camunda 8.9 refuses conditional, compensation and link catch - definitions without one); `ext - add loop.zeebe:loopCharacteristics` creates the multi-instance loop; - a zeebe element where the descriptor says it is not read is - `W_MISPLACED_EXTENSION`; `zeebe:adHoc`, `conditionalFilter` and - `publishMessage` are kept single (merged); `set zeebe:` for an - attribute of a zeebe element is `E_WRONG_HOST` with the `ext add` (also - `loop.zeebe:*`, `zeebe:correlationKey` -> the message's subscription); - `retype` names the zeebe content the new kind cannot use. - `src/ops/covers.ts` (moved out of retype.ts, re-exported there): an - operation warning the profile repeats item by item gives way to the - profile findings (retype's summary, the zeebe W_MISPLACED_EXTENSION, - `W_DECISION_RESULT_VARIABLE`). -- **View**: `show` names what a Camunda 8 node does (`job=`, - `calledElement=`, `script=`, `form=`, `assignee=` / `candidateGroups=` / - `candidateUsers=`, `inputCollection=` ...), a message its correlation key. -- **Docs and examples**: README "Camunda 8" (settings table, the profile, - the evidence, a worked example that deploys and runs), the guide's CAMUNDA - 8 recipe, `bpmn kinds` section CAMUNDA 8 (`kinds --json` -> - `zeebeElements`), every `W_C8_*` code in the catalogue. The ops example - and the worked session are valid Camunda 8 now (FEEL condition, `P2D`: - the old examples' `PT2D` is no ISO 8601 duration and Camunda 8 refuses - it; a job type for every service and send task). - -Tests: `test/c8-profile.test.ts` (151 models with Camunda 8's verdict, the -value checks, the validate output, the Modeler defaults; live engine opt-in -with `BPMN_C8_ENGINE=`, which also runs 7 runtime scenarios), -`test/c8-ops.test.ts` (set / ext / retype / show, the ops example, the -hints run through the CLI and, with the engine, deployed). Evidence (outside -the repository; counts only): the engine suites 332 / 332; 590 probe -deployments, the profile disagreeing only on two XSD element-order errors -and four cases the structural validation is stricter about; the 38 real -Camunda 8 files and 28 scenario files: 0 false deploy findings, 32 / 32 -refused files found, following the hints made all 32 deployable (306 -commands); the edit battery (7 edit types, 459 edits): 0 regressions, the -profile agrees with the engine on every result, 1,689 / 1,689 extension -blocks outside the edited elements byte for byte unchanged; the Camunda 7 -live-engine suites unchanged (804 / 804). - -Still open from this package: FEEL is not parsed (only the common slips); the -profile does not follow variables (an input mapping's local variable read -by a later gateway); `zeebe:publishMessage` is reported as not run by 8.9 -(drop the rule when Camunda runs it); a file without `` -lists cannot get them for an event-based gateway (reported, no command); -linked forms, called processes and decisions are not checked against a -deployment; the core grew by about 90 KB minified / 23 KB gzip. - -### Integration (`step3/round1`) - -Merged in the order views, ids, layout, camunda8 (merge commits with the -conflict resolutions), then integration commits with their tests in -`test/step3-integration.test.ts`: - -- **Aliases reach every op key that names an element** (`ops/aliases.ts - REF_KEYS`): the selectors `path`, `via`, `branch` of place / align / - color / tidy, the `ids` of `compact`, the `pools` of `order`. A format - op runs after the layout, outside the batch; its ids are now checked at - the end of `runBatch` while the batch context is set, so an id an earlier - op renamed or removed is `E_NOT_FOUND` naming the new id (a flow renamed - after its ends) and the op, as for the semantic ops. -- **`--summary` and the JSON** (`src/report.ts`): the batch aliases, the ids - the change renamed (`renamed`, old -> new: a flow whose id named its old - ends; chains collapsed, elements the change created left out; text - `renamed: -> `), one line per format op (`format #: - ...`, JSON `format`), so a place / compact / color result is no longer - "no changes". -- **Warnings delta and the Camunda profiles** (`validate.ts lintCoveredBy`, - used by `withProfile` and `pipeline.ts validationDelta`): a resolved - missing start is listed once, as the platform finding (it was listed as - `W_NO_START` and `W_C8_DEPLOY_START_EVENT`). -- **Reading views and Camunda 8**: `show --around` / `show --context` - print a zeebe setting once, in the extension summary (`view.ts nodeProps(el, - { zeebe: false })`: no `job=` next to `zeebe:taskDefinition type=...`); - the full `show` keeps the short facts. -- **Camunda 8 event definition ids** (`ops/platform.ts - definitionIdDefault`): through `Doc.allocateId` (`typeRequest` of the - definition type, the speaking part of the event's id, else its name or a - kind word): the file's prefix and case, never a Modeler hash - (`ConditionalEventDefinition_StockReady` for `Event_1q2w3e4` "Stock - ready"), a taken id `_2` with `W_ID_SUFFIXED`. -- **A layout defect the gate's fuzz campaign hit on the integrated build** - (walk `controlflow__s04`, seed 1491033354, step 10; every package build - gave the same defect on that walk, the walk only came up with the new - ids): removing a node closed the strip on its row and pulled an expanded - sub-process over a gateway of the row below (`di:frameIntrusion`). - `diagram/incremental.ts closeRow` now undoes the close when a moved - expanded sub-process comes within 10 px of a shape that stays. -- **Docs**: one CAMUNDA 8 guide section (the recipe of both packages; every - `ext add` / `set` line of it runs in `test/cli-views.test.ts`), `kinds - --section zeebeElements`, the short guide names speaking ids, aliases and - renamed flows; guide examples with speaking ids; the README's worked - session, `--summary` examples and Camunda 7 / 8 worked examples re-run - with the integrated build. - -### Evidence on the integrated build - -Private corpora used locally, outside the repository; counts only. - -- **Gate**: 1,615 tests + 159 opt-in (engine tests), isomorphism check (85 - modules), layout regression 115 files score 444 (budget 444), fuzz 12 x - 15: 0 errors, 0 warnings. -- **Engines**: the Camunda 7 live-engine suites 804 / 804 (Camunda 7.24.0, - CIB seven 2.2.0, Operaton 2.1.5); the Camunda 8 suites 332 / 332 - (Camunda 8.9.22). -- **Roundtrip** (`tools/roundtrip.mjs`, the 291 real files, 278 with a - rename target; 0.3.0 on the same files in brackets): a no-op is - byte-identical in 262 / 278 in layout auto (262 / 278; 262 / 265 of the - files with a diagram, 98.9 %: the 13 without one are drawn) and 278 / 278 - with `--no-layout` (278 / 278); a rename changes 2 lines (median, p90 and - max with `--no-layout`; 2); an insert changes 56 lines in the median - (54) and a region of 83.2 % (80.3 %): the flow renamed after its new - ends. -- **Camunda 7 real-file battery** (218 files, 8 edit types, 1,512 edits): - every edit succeeds, 0 regressions on any of the three engines, 0 - unexpected camunda changes (one file deploys after a remove, as in step 2). -- **Camunda 8**: the 66 corpus files 0 false deploy findings, 32 / 32 - refused files found; the edit battery 459 edits, 0 regressions, the - profile agrees with the engine on 459 / 459 results, 1,689 / 1,689 - extension blocks outside the edited elements unchanged; following the - hints (306 commands, none refused) makes 32 / 32 refused files deployable. -- **README examples**: the worked session's result deploys on Camunda 8.9; - the Camunda 8 worked example deploys and runs both paths; the Camunda 7 - worked example deploys on the three engines. -- **Browser bundle**: whole entry 897 / 275 KB minified / gzip, `applyToXml` - 710 / 219 KB (+82 KB bpmn-auto-layout on demand); 0.3.0: 716 / 224 and - 594 / 186 KB. After the Camunda 8 fixes (below): 922 / 284 and 733 / 227 KB. - -## What step 2 changed (2026-10-09): bpmn-cli as design-iq's editing engine - -Goal: bpmn-cli as the editing engine inside Miragon's design-iq (PR #218 of -Miragon/design-iq, its ADR 0008), which runs the editor in the browser, -stores BPMN in git, syncs a file live by replacing one text region per save -and refuses every save its validator finds an error in. Four packages were -built in parallel and merged on `step2/integration`; 13 audit bugs are fixed -(#27, #29–#33, #43–#47, #55, #56), 2 partly (#48, #49). Tables and evidence: -[docs/audit-2026-10.md](docs/audit-2026-10.md#step-2-2026-10-09-design-iqs-editing-engine). - -**The write pipeline, as it is now** (`src/pipeline.ts mutate`, in memory, -browser-safe): lossy-import guard -> the document as read is serialised once -(`asRead`, the baseline of the text step) -> layout snapshot and sticky -anchors -> error baseline, platform profile baseline, validators on the -document before the ops -> ops -> structural validation (`E_VALIDATION` for -introduced errors) -> layout (auto / incremental / full; a full redraw gives -the DI its ids back inside `layoutModel`) -> format ops -> stickies follow -their nodes -> text-preserving step (`outputText`: mirror lists the file's -way, unchanged elements keep their text) -> validators on that final text -(`E_VALIDATION` for introduced errors) -> `unchanged` (result equals the -input text). The node layer (`src/node/files.ts`) reads, looks up the -design-iq content repository, calls the pipeline and writes atomically, -skipping a write of an unchanged result over its own file. - -- **Browser-safe core and in-memory API** (core package, #33, #49 partly). - Two entries (`package.json` `exports`): `@miragon/bpmn-cli` (`src/index.ts`) - never touches files, `process` or Node builtins; `@miragon/bpmn-cli/node` - (`src/node/index.ts`) adds `readXml`, `readDoc`, `loadDoc`, `writeAtomic`, - `mutateFile`, `mutateDocToFile`, `layoutFile`, `checkFile` and the content - repository lookup (`src/node/repo.ts`: `findContentRepo`, `contentModelIds`, - `contentRepoOf`, `resolveFileProfile`). Only `src/node/` and `src/cli.ts` - may use Node; `tools/iso/check.mjs` in the gate fails otherwise. - `src/api.ts`: `applyToXml(xml, ops, opts)` -> `{ xml, unchanged, result }`, - `newXml`, `layoutXml` (= `bpmn layout`), `validateXml`, `viewXml` / - `showXml`, `metricsXml`, `findXml`, `extensionsXml`; ops go through - `parseOps` like `apply`; options include `profile`, `contentRepo`, - `validators` and `file`. `mutateDoc` never writes (`out`, `dryRun`, - `backup`, `mustNotExist` are node options); `layoutDoc` / `checkDoc` are - the `layout` / `validate` commands on a `Doc`. What a command reports is - `src/report.ts` (CLI and API alike). Layout debug lines: `src/debug.ts` - (`setLayoutDebug`, `debug` option; the CLI maps `BPMN_LAYOUT_DEBUG`). The - Camunda 7 descriptor is inlined (`src/platform/camunda-descriptor.ts`, - `tools/gen-camunda-descriptor.mjs`; a test fails while it is stale); - bpmn-auto-layout is imported on first use. `npm run build` also ships the - type shims (`tools/build-types.mjs`); `moddle` is a direct dependency. -- **Text-preserving writes** (roundtrip package, #30, #31, #44–#47). - `src/preserve.ts` (reader with positions: `src/xmltext.ts`) compares the - file, bpmn-moddle's serialisation of the model as read and of the changed - model: elements whose serialisation did not change keep their original - text; changed ones keep their start tag or attribute order, new ones get - the file's indentation, line breaks and `/>` style, CDATA stays CDATA. The - result must read back as exactly the changed model, else it falls back to - the plain serialisation with a note (a DOCTYPE and a forced lossy import - always do). `src/mirror.ts`: incoming / outgoing lists are complete in - memory (`Doc.fromXml` records them as read in `doc.source.mirror`) and - written the file's way. Comments next to removed elements are reported as - dropped. `MutationResult.unchanged`; `Doc.source` (`DocSource`); - `preserveText` exported; `tools/roundtrip.mjs` (`npm run roundtrip`). -- **Edits follow the file** (conventions package, #32, #43, #47, #48 partly, - #55, #56). `src/idstyle.ts` learns a document's id style once - (`Doc.idStyle`, `Doc.allocateId`): prefixes per kind / family / type name, - pascal / camel / snake / Pascal_Snake / modeler-hash / numbered bodies, flow - forms (`Flow_0k3x9qa`, `Flow_12`, `flow_aToB`, `Flow__`); flows - and unnamed elements are hashed from stable inputs (files that number - their flows keep numbering); umlauts become ae / oe / ue / ss. A full redraw - keeps every DI, plane and diagram id (`diagram/write.ts` rememberDiIds / - restoreDiIds inside `layoutModel`, both engines). design-iq stickies - (`bpmiq:sticky`) follow their nearest flow node (`src/diagram/stickies.ts`, - `layout.stickies`). Generated ids are not guessable: `apply` batches pass - explicit ids for back references. (Step 3 replaced the hashes with - speaking ids and added batch aliases, see above.) -- **Validator hook, design profile, decision link** (validation package, - #27, #29). `src/validators.ts`: `validators` (functions `(xml, ctx) => - findings` or `{ name, validate }`, design-iq's `Finding` accepted) run on - the document before the ops and on the final text; introduced errors block - (`E_VALIDATION`, each finding with `validator`), old ones are - `W_PREEXISTING_ERROR` (followed through renames), introduced warnings are - reported, a throwing validator is `E_VALIDATOR_FAILED`. - `src/platform/design.ts`: design-iq's save-gate rules (`E_DESIGN_*`, - `W_DESIGN_*`); `--profile auto|design|none`; `auto` runs it for the models - of a design-iq content repository (a `bpmiq.yml` above the file naming a - models folder that contains it; in memory: when `contentRepo` is given). - `src/platform/repo.ts` decides (browser-safe), `src/node/repo.ts` looks the - repository up. `src/ops/decision.ts`: `set calledDecision=` in the - file's spelling (design: unprefixed, C7: `camunda:decisionRef`, C8: - `zeebe:calledDecision`). - -**Verifier round** (after the integration; table in -[docs/audit-2026-10.md](docs/audit-2026-10.md#verifier-round-2026-10-09)): -`E_DESIGN_NAMESPACE` runs design-iq's namespace check on the raw text -(text, CDATA, comments and attribute values that look like `_To`; every write is UTF-8 and a changed result declares -UTF-8, the node layer reads a file in its declared encoding -(`src/encoding.ts`, `decodeXmlBytes`); the design profile counts flows per -id, checks every collaboration, reports an attribute written twice -(`E_DESIGN_XML`), and an id reference with whitespace around it resolves at -import (model.ts); in a process with two lane sets the profile stays -stricter than design-iq (documented); `camunda-bpmn-moddle` is a -development dependency. - -**Integration decisions** (`step2/integration`): the core API runs the -roundtrip post-pass and the validators (they see the post-pass result, their -baseline is the text as read); skipping an unchanged in-place write and the -`bpmiq.yml` lookup moved to the node layer (the core takes `contentRepo` and -`file`; the node layer fills them for the file it writes, the `--out` target -included); #47 was fixed by two packages and the conventions version -(inside `layoutModel`) is kept, `src/diagram/keep-ids.ts` dropped; stickies -move before the text step, so only the moved sticky elements change; the -`unchanged`, `forced` and validator lines and the `profile` / `validators` -keys of `validate --json` live in the shared report. - -Tests: `test/api.test.ts`, `test/isomorphic.test.ts`, -`test/node-files.test.ts` (encodings too), `test/drawn-ids.test.ts`, `test/roundtrip.test.ts` (fixtures -`test/fixtures/roundtrip/`), `test/conventions-ids.test.ts`, -`test/conventions-di.test.ts`, `test/conventions-stickies.test.ts`, -`test/design-profile.test.ts` (opt-in against design-iq's validator with -`BPMN_DESIGN_IQ_VALIDATOR`), `test/validators.test.ts`, -`test/decision-link.test.ts`, and `test/step2-integration.test.ts` (the -packages together). - -Evidence on the integrated build (private corpora used locally, outside the -repository; counts only): - -- **Gate**: 1,318 tests + 1 opt-in (972 before step 2), isomorphism check, - layout regression 115 files score 444 (budget 444), fuzz 12 x 15: 0 - errors (2 warnings of open bugs #61 and `route`, the same with the package - builds). -- **Roundtrip** (265 real files the CLI accepts, the audit's targets, all - with a diagram): no-op byte-identical 262 in layout auto (the rest: the - layout completes missing DI; a file without a diagram, 22 of the 289 real - files the CLI accepts, is drawn on any write) and 265 with `--no-layout` (0.2.0: 40 / 42; PR #218: 169); a rename - changes 2 lines (median, p90), region median 0 %; an insert rewrites a - region of 77 % (median; 0.2.0: 97 %, PR #218: 86 %). Every output of 12 - edit types on 396 files equals the roundtrip package's byte for byte except - where the id style names a new element differently. In memory - (`applyToXml`) a no-op is `unchanged` for 260 / 263 real files (auto) and - 263 / 263 (`layout: false`). -- **Id style** (291 real files): new task ids in the file's style 264 / 270 - (0.2.0: 4), flows 226 / 226 (0.2.0: 5), a full redraw keeps 8,345 / 8,345 - DI ids, 9 / 225 file pairs share a new id (0.2.0: 225 / 225; all in files - that number their flows, three of them without separator). -- **Design profile** vs design-iq's validator (394 files): 0 disagreements - (65 refused, 328 accepted; 540 / 540 findings matched); of 2,231 probe edits - the 1,117 written add no design-iq error and the 1,114 refused would. -- **Browser**: the bundle in a vm context without Node globals equals Node in - 3,491 / 3,491 calls on 394 files; 716 / 224 KB minified / gzip for the - whole entry, 594 / 186 KB for `applyToXml` alone (+82 KB bpmn-auto-layout on - demand). -- **Camunda 7** (Camunda 7.24.0, CIB seven 2.2.0, Operaton 2.1.5): the - 218-file battery (8 edit types, deploy before / after) 0 regressions, 0 - unexpected camunda changes; the sweep of 4,100 edits 0 regressions and - every verdict as with 0.2.0; the chained session 0 regressions; the - follow-up battery 202 / 202 edits deploy; the execution scenarios 65 / 65; - the live-engine tests 804 / 804. One file (a data object after the flow - elements, refused by Camunda 7 and Operaton) is no longer repaired as a - side effect of an edit, since a write keeps the element order. - -Still open from step 2: in layout `auto` a no-op is still written when the -layout completes missing DI (3 of the audit's 265 targets) or draws a file -without a diagram (every such file: 22 of the 289 real files the CLI -accepts, 13 of them with a node to rename; 16 of 278 files with a rename -target in all; audit P3: skip the layout when the model did not change); an insert -rewrites one text region of about 77 % of the file because it changes the -process and the DI section (a host wanting fewer conflicts needs several -regions per save); a write keeps the file's element order, so it no longer -repairs an element in a place the BPMN XSD does not allow (0.2 rewrote such a -file in bpmn-moddle's order; 1 of 218 real Camunda 7 files, refused by the -engines before and after an edit); a spliced or bridged flow keeps its id -(#48) and files that number their flows can still collide across branches (9 -of 225 file pairs) (both resolved by step 3's speaking ids); in a process with two lane sets the design profile checks -the lanes design-iq does not read (stricter, documented; moot since the design profile was removed with #35); a sticky moves by its node's centre shift only; nested lanes -(`set lane=` writes the child lane only, design-iq reads top-level lanes) and -lane inheritance of a node placed next to a node in no lane; design-iq's -degree rules make compensation handlers, link events and ad-hoc content -unsavable there (the design profile reported them; removed with #35); host validators only through the library, not the CLI; -`mutateDoc` still takes typed ops unchecked (#49; `applyToXml` checks them); -bundle size (`applyToXml` 594 / 186 KB minified / gzip; ops, diagram and the -Camunda 7 profile are the largest parts: a lazy profile or a slimmer build -would be the next lever); design-iq's code conventions (audit R17: `.ts` -import specifiers, `erasableSyntaxOnly`, `node --test`) are not addressed. - -## What the Camunda 7 follow-ups changed (2026-10-09, before step 2) - -An independent verifier re-checked the Camunda 7 step on the three engines -and reported 15 follow-ups plus profile noise; all are fixed, together with -seven more found while integrating. Table with status and engine evidence: -[docs/audit-2026-10.md](docs/audit-2026-10.md#c7-follow-ups-2026-10-09-verifier-round). - -- **Profile** (`src/platform/c7.ts`): new deploy rules - `W_C7_DEPLOY_START_EVENT`, `W_C7_DEPLOY_LINK`, `W_C7_DEPLOY_SCHEMA` - (attributes BPMN does not define, anywhere in the file), subscriptions - grouped by the engine's scope, compensation `activityRef` scope, timeout - listener id, field values; runtime `W_C7_EMPTY_CONDITION`, - `W_C7_MULTIPLE_EVENT_DEFINITIONS` and wrong-side event-definition settings - (`W_C7_MISPLACED_*`). Two false deploy findings removed (script-resource - conditions, `camunda:type="shell"`). Of several event definitions the - engines act on one and do not parse the others (`actingOrder`, - engine-checked per event type): the deploy rules look at that one only. - Hints rebuild one repeatable extension element by its selector instead of - `--replace`, and use `definition[].` where `definition.` is ambiguous. -- **One platform detector** (`detect.ts detectPlatformOf`) behind `validate` - and `Doc.platform()`; an explicit `MutationOptions.platform` reaches the - ops (`Doc.platformChoice`). Operaton's own namespace is part of the Camunda - 7 family everywhere: `isC7Uri` / `C7_URIS` / `OPERATON_URI` in - `descriptor.ts`; `ext` applies its structure rules by local name with - operaton containers (`ruleName` / `inFamily` in `ops/ext.ts`), `set`, - rename, retype and resource conditions accept `operaton:`, a new process of - an Operaton file gets `operaton:historyTimeToLive`, and the profile reads - `operaton:*` first like Operaton (`"operaton": true`). -- **Operations**: `definition[].` / `definition[].` selectors - (`E_AMBIGUOUS_NESTED` for a plain `definition.` on several definitions), - `E_CROSS_SCOPE` for `activityRef`, an empty condition is refused, keyed - `ext add` replacement reports what it drops with the `--xml` that keeps it, - multi-line bodies print line by line, `"type": "loop.0"` in ops JSON, the - event-gateway rule per file (engines' rule for Camunda 7, BPMN 2.0 - otherwise; explicit edges warn), duplicate single elements are a lossy - import, `set =` removes an attribute BPMN does not define, and a - write reports a retype's stale camunda content once (through the profile). -- **Output**: `validate` counts import warnings (`--strict` fails on them), - says "no Camunda 8 engine rules yet" for c8 files, and a boundary event on - a compensation handler is reported once (`E_INVALID_HOST`). -- **Docs**: catalogue entries for `E_AMBIGUOUS_NESTED` and the five new - `W_C7_*` codes (and the changed ones), README sections (set, remove, - retype, ext, validate, set keys, Camunda 7, ops JSON, limitations), the - guide's Camunda 7 recipe, `bpmn kinds --json` -> `nestedSelectors`. - -A second verifier round fixed two regressions and two hint / precision -issues ([table](docs/audit-2026-10.md#c7-follow-ups-second-verifier-round-2026-10-09)): -the BPMN schema rules on event definitions (`checkSchemaEventDefinitions`: -conditional without condition, link without name, timer with two time -elements) run on every definition of the file, also one the engines ignore -and non-executable processes; nested extension hints address the flow node -(`ext remove Event_Wait definition.0`, never the definition's own -id); start events follow the engines (an empty process / embedded -sub-process is `W_C7_DEPLOY_START_EVENT`, a transaction without start is -runtime `W_C7_TRANSACTION_NO_START`, a connected ad-hoc sub-process is -`W_C7_DEPLOY_AD_HOC_SUBPROCESS`, ad-hoc content is skipped); the second-start -hint never moves a flow into a node the kept start reaches -(`extraStartHint`). Tests: `test/c7-followups-verify.test.ts`. - -Tests: `test/c7-followups-profile.test.ts`, `test/c7-followups-ops.test.ts`, -`test/c7-followups-integration.test.ts`, `test/c7-followups-verify.test.ts` (live engines opt-in with -`BPMN_C7_ENGINES` like `c7-profile`). Engine evidence (outside the -repository): 145 / 145 follow-up repro checks, the step's suites unchanged -on the integrated build (108 / 108 with three checks following the new -contract, 39 / 39, 65 / 65, extension scenarios identical), profile precision -on 218 real files 0 false positives (14 / 15 refused files found) and 5,860 / -5,860 derived files, real-file battery 202 / 202 edits deploy. - -## What the Camunda 7 step changed (2026-10-09) - -A second audit checked Camunda 7 support (Camunda 7.24.0, CIB seven 2.2.0, -Operaton 2.1.5; 218 real C7 / CIB seven files from private corpora). Camunda -content was preserved perfectly, but treated as untyped; 18 findings, all -fixed now. The table with each finding, its fix and the engine evidence is in -[docs/audit-2026-10.md](docs/audit-2026-10.md#camunda-7-audit-2026-10-09). - -- **Descriptor as data** (`src/platform/descriptor.ts`): reads - `camunda-bpmn-moddle`'s `camunda.json` (pinned 8.0.1; since step 2 - inlined, the package a development dependency) to know which camunda - attributes and extension elements belong where. - It is never registered with bpmn-moddle: camunda content stays generic and - the serialisation is byte-stable. Two `allowedIn` lists are corrected to what - the engines accept. -- **`new --target camunda7`** writes `camunda:historyTimeToLive="180"` and - `modeler:executionPlatformVersion="7.24.0"` like the Modeler (the engines - refuse an executable process without TTL); a new pool's process gets the TTL - (`Doc.initProcess`). Unknown targets fail with `E_USAGE`. -- **Nested keys** (`src/ops/set.ts`): `definition.`, `loop.`, - `condition.` address the event definition, the loop characteristics and - the condition expression; a camunda attribute that belongs there is refused - on the parent (`E_WRONG_HOST`, the hint names the key), a missing nested - element is `E_NO_NESTED_ELEMENT`. Conditions are changed in place and report - a dropped script resource / language; loop changes report dropped vendor - content; renames re-point `camunda:errorEventDefinition errorRef`; - send / receive tasks take `message=`; camunda Boolean attributes are written - as exactly `true` / `false`. `bpmn kinds` lists the nested keys - ("NESTED KEYS", `kinds --json` -> `nestedKeys`). -- **`ext` structure** (`src/ops/ext.ts`): child types are filed into their - container, single-instance containers merged (`E_DUPLICATE_EXTENSION` on a - conflict), keyed items replaced, paths reach nested containers, `ext remove` - takes selectors (`'camunda:inputParameter[name=x]'`; `E_AMBIGUOUS_EXTENSION`), - `--xml` accepts `bpmn:` children inside vendor elements and must match the - positional type, `ext add` on `bpmn:definitions` is `E_WRONG_KIND`, content - the engines would not read is `W_MISPLACED_EXTENSION`. A `definition.` / - `loop.` / `condition.` prefix (or op `slot`) addresses the extension - elements of a nested element (`loop.camunda:failedJobRetryTimeCycle`). - `ext list` and `show ` print extension content as an indented tree. -- **Engine rules in the ops**: bridges next to an event-based gateway that the - engines refuse are `E_INVALID_BRIDGE` (remove, move, move --in); a boundary - event on a compensation handler is `E_INVALID_HOST` (add / move --on, and a - structural check in `validate.ts`, so `set isForCompensation=true` on a host - with boundary events is blocked); `connect` warns `W_DUPLICATE_FLOW`; - `retype` names camunda content the new kind cannot use - (`W_PROPERTY_INAPPLICABLE`). -- **Views**: `show` prints vendor values (nested ones under their set keys, - process- and flow-level content, `xN` counts), `find` matches vendor values - and prints the match. -- **Camunda 7 profile** (`src/platform/{detect,c7,profile,finding}.ts`): - `validate` detects the platform (`--platform auto|c7|c8|none`) and runs 33 - rules (`W_C7_DEPLOY_*` = the engines refuse the file; runtime and practice - findings), each with a `severity` and a hint naming the fixing command. - Every write reports only the profile findings it introduced - (`validation.platform.added/resolved`). Library: `validateDoc(doc, - { platform })`, `runProfile`, `detectPlatform`, `MutationOptions.platform`. -- **Docs**: README "Camunda 7" section with a worked example (run on all three - engines), the guide's CAMUNDA 7 recipe, every new code in the error - catalogue (the catalogue test now also sees codes with digits, `W_C7_*`). - -Tests: `test/c7-semantic.test.ts`, `test/c7-ext-structure.test.ts`, -`test/c7-profile.test.ts` (82 models with their engine verdict; live engines -opt-in with `BPMN_C7_ENGINES`, see docs/testing.md), -`test/c7-integration.test.ts`; all synthetic. Engine evidence (outside the -repository): every finding's repro re-run on the integrated build (108 / 108 -checks), the real-file battery (same numbers as before, 0 regressions, 0 -unexpected camunda changes; the 37 former sweep regressions are refused now), -the execution scenarios (65 / 65, including what the old CLI could not -express). - -## What step 1 changed - -All seven high-severity bugs and twelve medium ones are fixed (17 fully, 2 -partly; 58 remain open). Each fix has a regression test that fails on the -code before it: `test/step1-semantic.test.ts`, `test/step1-diagram.test.ts`, -`test/step1-engine.test.ts`, `test/step1-gate.test.ts`, fixtures in -`test/fixtures/step1/` and `test/fixtures/gate/` (all synthetic). - -**Semantic layer and pipeline** (`src/model.ts`, `document.ts`, -`validate.ts`, `pipeline.ts`, `ops/`): - -- Vendor extension ids (`camunda:formField id`) are not BPMN ids: no false - `E_DUPLICATE_ID`; `show` / `set` / `remove` on such an id give `E_NOT_FOUND` - naming the vendor element and its owner. -- Changing only the details of an event's trigger updates the event - definition in place (id and vendor attributes stay); a trigger kind change - reports dropped vendor content (`W_PROPERTY_DROPPED`). A new timer value is - classified again (`R/...` -> cycle). -- `mutateDoc` refuses lossy imports like the CLI (`E_IMPORT_LOSSY`, `assertLossless`). -- Only validation errors a change introduces block the write; errors the file - already had become `W_PREEXISTING_ERROR` (complexGateway files, plain starts - in event sub-processes, black-box-only collaborations are editable now). -- `retype` of a sub-process with content to a task / call activity needs - `--force` (`E_WOULD_DROP_CONTENT` lists the ids); complexGateway can be - retyped to a supported gateway. -- Associations on a bridged flow move to the bridging flow (no more - `E_DANGLING_REF` on remove / move next to an annotated flow); a removed flow - is also dropped from stale `incoming` / `outgoing` entries of other nodes. - -**Incremental layout, format ops, metrics** (`src/diagram/`): - -- "Frame mode" in the space tool (`space.ts`): a growing sub-process makes - room inside itself, ancestors grow only when something sticks out, and room - is made one level further out instead of growing over a foreign shape. Used - by placement, fitInFrame, name growth, labels and expand. -- `fitInFrame` skips an ancestor that does not hold the inner frame's centre - (a drawing with an event sub-process outside its member lane). -- Unconnected nodes go below their own container's content, not below the - lowest label of the plane. -- `place` / `align` refuse (`E_NO_ROOM`) instead of pushing the reference's - pool / lane / sub-process away, and never write a new overlap: a reference - the neighbours would be pushed onto moves along when the request still - holds, else `E_NO_ROOM` (`ops.ts` makeRoom, `newOverlaps`; fixes #11). -- Metrics: new hard kinds `frameIntrusion`, `frameOverlap`, `degenerateEdge` - (weight 10, not in the harness: `EXTRA_KEYS` / `HARNESS_KEYS` in - `metrics.ts`), and a real segment-rectangle test for `through` (also in the - harness and in `geom.segmentHits`). -- No connection is written with fewer than two distinct waypoints. - -**Full engine** (`src/layout/`): - -- Lane bands follow the content after loop routing (`growFirstLane`). -- Pools are ordered by the message-flow cost (fewest cut shapes, declared - order on ties; `messages.ts poolOrder`); message-flow legs jog into free - column gaps; notes on boundary events avoid sibling events. -- Event sub-processes go below the content of their own lane and honour - collapse; collaboration-level annotations get DI (`notes.ts`); cross-scope - associations are drawn when both ends are on one plane, else reported. -- Side effect: a drawing made by the previous engine version is no longer - recognised as engine-owned on laned and collaboration models, so `auto` - keeps it (incremental) instead of redrawing it. - -**Tooling** (new in the repository): `tools/bench` (edit benchmark with a -baseline arm), `tools/fuzz` (seeded random walks with invariants and ddmin -minimisation), `test/fuzz.test.ts` (fixed walks in-process), -`tools/bench/regress-gate.mjs` with the budget, `npm run gate`, -`test/repo-hygiene.test.ts` (no local paths or control characters in the -repository), docs/testing.md, docs/audit-2026-10.md. - -## Evidence - -Numbers from the gate on 2026-10-08; baseline = the build before step 1, both -measured with the new metrics. The real models come from private corpora -plugged in through `BPMN_BENCH_CORPUS` / `BPMN_FUZZ_CORPUS`; they never enter -the repository. - -- **Layout regression**: the original 111 scenarios 472 -> 402 (464 for the - old engine with the corrected `through` test), 5 files better, none worse; - 4 new scenarios 42 (old engine 245); total 115 files SCORE 444. -- **Benchmark**, auto mode, edits E1–E6 pooled (new / baseline): on the 115 - scenarios 0 / 3 runs add a hard defect, median share of other nodes moved - 11 % / 12 %, a full layout leaves 3 / 33 hard defects; on the scenarios - redrawn by the previous engine 5 / 11 runs (5 / 20 defects); on 60 - modeler-drawn models 100 % / 95 % of the runs succeed without `--force`, - 0 / 0 runs add a hard defect, a full layout leaves 18 / 46 hard defects. - Semantic success 100 % in every corpus and arm. -- **Fuzzing**, 2 x 200 walks x 40 steps (scenarios, perturbed variants, - modeler models; auto / incremental): steps adding a hard defect 0.14 % / - 0.70 %, further steps adding only a frame defect 0.02 % / 0.41 % (the audit - had 0.64 % + 0.77 %); `E_VALIDATION` refusals 14 / 216. Every hard step of - the new build was replayed on the same input with the baseline, which adds - the same defects: none is a regression. -- **Performance** unchanged: an insert on 56 modeler models, median 79 ms - (77 ms before). -- Earlier evidence for the incremental layout (blind visual judging of - rendered samples): on edits of hand-drawn models this CLI ranked first in - 43 of 52 judgments against the old full redraw and the PR #218 tool; for - full redraws the clean engine was judged the best global layouter (3.77 vs - bpmn-auto-layout 3.56 vs PR #218 2.96). - -## How to work on it - -``` -npm run gate # before every hand-over -npx vitest run test/.test.ts # one layer -node tools/layout-regress.mjs --save /tmp/base.json; ...; node tools/layout-regress.mjs --compare /tmp/base.json -BPMN_LAYOUT_DEBUG=1 node bin/bpmn.js add ... # placement candidates, reroute reasons, [strip] lines (library: setLayoutDebug / debug option) -npm run check:iso # after a build: the core still bundles for the browser; the profile rules load on demand and import nothing the entry loads; sizes within tools/iso/budget.json (--table: the README table) -npm run roundtrip [] # after a build: how much of a file a no-op, a rename, an insert rewrite -node bin/bpmn.js metrics # problems with ids -node bin/bpmn.js render --problems -o /tmp/png/x.png # look at the result, the problems numbered (no browser; --around / --path / --scope / ids crop, --tiles, --plane ) -node bin/bpmn.js add ... --render /tmp/png/after.png # any mutating command: its result with what changed in a halo -tools/render.sh /tmp/png # the same with real bpmn-js (the reference for reviews) -BASELINE_BIN=/bin/bpmn.js npm run bench # benchmark against an older build -npm run fuzz # random walks; see docs/testing.md -BPMN_C7_ENGINES=[,] npx vitest run --no-file-parallelism test/c7-*.test.ts # Camunda 7 profile vs live engines -BPMN_C8_ENGINE= npx vitest run test/c8-profile.test.ts test/c8-ops.test.ts # Camunda 8 profile vs a live engine -node tools/gen-camunda-descriptor.mjs # after updating camunda-bpmn-moddle or zeebe-bpmn-moddle -``` - -Rule of thumb: render and look at what you touched; the metrics do not see -everything (labels, aesthetics). Lower the -regression budget when the engine improves; raise it only with a reviewed -reason. Keep fixtures synthetic: rebuild a private model's defect with a -handful of elements (the fuzzer's minimiser tells you which ops matter). - -## What to build next, most valuable first - -(Camunda 7 / 8 leftovers, smaller than the items below: the XSD element -order in the C7 profile, the `activiti:` fallback namespace of Camunda 7 / -CIB seven, a FEEL parser for the Camunda 8 profile (today: the common slips -only), checking linked forms / called processes / decisions against a -deployment. See the "Still open" lists of the C7 audit and of step 3.) - -1. **Issue #18 after stage B**: what stage B left (see "Incremental layout - of edits to hand-drawn models, stage B" and its fixes): a `connect` to - the sibling branch's node that crosses, the convention of exception - paths above their host, a grown host sliding its boundary events, an - opened row that cannot help (a line descending through it), room at the - top of a pool moving its whole content. The `edit-hand-drawn-model` eval - passed (WITH 1.00, no DI edited by hand). -2. **The remaining layout bugs the fuzzer still hits**: data objects and - annotations placed onto shapes or into a foreign sub-process, boundary - events added on a host inside a sub-process intruding into another - frame, `place` / `align` pushing a - flow through a shape, `route` and expand / collapse through shapes, - associations on the wrong plane after a collapse (#15), data - associations not rerouted after `split` / `move --lane`, flows left - detached from gateways (#16). -3. **CI**: run `npm run gate` on every change; run the bench and a longer fuzz - campaign on the private corpus on a schedule, outside the repository. -4. **An MCP server.** Thin wrapper over show/find/add/connect/apply/validate - plus `show --layout`, `metrics` and the format ops. Removes the shell - quoting trap (`${...}` in conditions). -5. **Embedding in design-iq, the rest** (audit P1 and the step 2 leftovers - above): skip the layout in `auto` mode when the model did not change (a - no-op that completes missing DI is still written); hunks or several text - regions per save for inserts; lane conventions for nested lanes (a node - in a child lane also listed by the parent lanes, as bpmn-js and design-iq - expect; today `set lane=` writes the child lane only and `E_LANE_CONFLICT` - reports bpmn-js files that list both) and lane inheritance for nodes - placed next to a node in no lane; the bundle size if design-iq needs it - (the profile rules load on demand since #13; the next lever is splitting - `guide.ts`, see "Profile rules on demand"). -6. **Let the agent see the result**: issue #12. Stages 1 (the renderer, - `bpmn render`), 2 (crops, halos, numbered problems, tiles, `--render`) - and 3 (the plugin skill's look step and its eval case) are done, see - "`bpmn render`, stage 3", "stage 2" and "stage 1"; the verification's - side-by-side with `tools/render.sh` (35 files) and its fixes are in - "`bpmn render`, fixes after the verification". Open: the whole eval suite - on the new skill, and offering `renderSvg` to design-iq (out of scope - here). -7. **Persistent layout intent**: pins / "main path" hints in the DI that the - clean engine honours, so formatting survives a full redraw. - -## Known residuals - -- Validators: the CLI cannot pass host validators (only the library can); - a host validator that needs repository context (design-iq's `checkModel` - with `modelIds`) gets it from the host. bpmn-cli has no copy of - design-iq's rules since #35: in a content repository, run design-iq's own - validator after edits. A new node next to a node in no lane inherits no - lane (`--lane` in the same command). - -- Camunda 7: an Operaton-namespace file is checked the way Operaton reads it - (operaton first, camunda as the fallback), so `operaton:historyTimeToLive` - satisfies the TTL rule although Camunda 7 and CIB seven ignore it; the - platform line says so. The form-field type rule reports custom form types; - `activiti:` is not modelled; a text-less field child cannot be told apart - from a blank one; message start events of two executable pools with one - message name are not reported (the engines refuse them). - -- Camunda 8: FEEL syntax is checked, not its meaning (unknown functions, - types); variables are not followed (an input mapping's local variable read by a later gateway passes - the profile and fails at run time); `zeebe:publishMessage` is reported as - accepted but not run by 8.9; an event-based gateway of a file without - `` lists is reported but cannot be repaired with a command; - forms, called processes and decisions are not checked against a - deployment. The old examples' `PT2D` duration is no ISO 8601 duration: - Camunda 8 refuses it at deploy; Camunda 7.24, CIB seven 2.2 and Operaton - 2.1 deploy it, and every instance fails when it reaches the timer - (ENGINE-09027, checked on all three); the docs say `P2D` now, the Camunda - 7 profile does not check timer values yet (a follow-up for it). - -- See the open bugs in docs/audit-2026-10.md and its "Open findings from the - gate". -- `labelOnLine` and `msgThroughPool` still use a segment's bounding box, so - diagonal associations produce false positives (in the metrics and the - harness alike). -- A drawing of the previous engine version with event sub-processes outside - their lanes stays as it is in incremental edits; `bpmn layout` moves them. -- `tidy` considers shapes, not labels. Frames grow on demand and shrink only - with `compact` / `space --by -`, which close strips (rows / columns - empty across a pool or lane band): content spread over many rows shrinks - less than with a full redraw; pools side by side are not compacted - against each other. -- Known residuals of the clean engine: complexGateway and choreographies - unsupported, groups cannot be created (a full redraw draws an existing - group box again around its members; members of one connected flow and - two overlapping groups are not kept apart, and an annotation's - association to a group is not drawn, see "Group boxes over a full - redraw"); cross-scope - associations are straight lines; boundary-handler paths can run through - nested expanded sub-processes (#19); `order ` sets - the pool order of a kept drawing, a full redraw orders pools by its own - rule (message-flow cost, declared order on ties). diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index b7e359ff..dd899614 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -15,7 +15,7 @@ export default miragonDocs({ footerMessage: "Released under the MIT License.", // Notes for maintainers, read on GitHub: not pages of the site. - srcExclude: ["releasing.md", "testing.md", "audit-2026-10.md"], + srcExclude: ["releasing.md", "testing.md"], nav: [ { diff --git a/docs/audit-2026-10.md b/docs/audit-2026-10.md deleted file mode 100644 index aa5004c5..00000000 --- a/docs/audit-2026-10.md +++ /dev/null @@ -1,1683 +0,0 @@ -# Audit, October 2026 - -A broad audit of `bpmn-cli` after the incremental layout and the format -operations landed (see HANDOVER.md). Eight independent streams tried to break -the CLI from different angles; a ninth pass re-ran every reported bug and kept -only what it could reproduce. This document summarises what was tested, what -held, and the 77 confirmed bugs. It contains aggregate numbers only: the real -models used come from private sources and stay outside this repository. - -How to run the same kinds of checks yourself: [testing.md](testing.md). - -## Verdict - -- **The semantic layer and the write pipeline are robust.** Across about - 60,000 mutations (CLI calls and in-process dry runs) there was no crash, no - write on a failed command, and every written file re-imported cleanly. - Vendor extensions an edit did not touch survived every write in auto and - incremental mode (#2 is the exception: an edited event definition). -- **The CLI is deterministic and fast.** Same input, same bytes, regardless of - working directory, time zone or locale. Models of 300 flow nodes take about - 0.2–0.3 s per edit. -- **Layout quality is where the bugs are.** The incremental engine adds a hard - defect (overlap, flow through a shape, node outside its lane / pool / - sub-process) in roughly 0.4–1.2 % of steps, more with nested sub-processes, - and the metrics did not see a whole class of defects (a frame covering foreign - shapes). -- **Integration gaps for an embedding host** (Miragon/design-iq, PR #218 tool - `@bpmiq/bpmn-edit`): formatting churn on write, no platform awareness, the - library is not browser-safe. Addressed in - [step 2](#step-2-2026-10-09-design-iqs-editing-engine): text-preserving - writes, a browser-safe core with an in-memory API, the file's id style, - the host's save gate in the transaction; a Camunda 8 profile followed in - [step 3](#step-3-2026-10-10-camunda-8-profile). - -## What was tested - -| stream | method | volume | -| --- | --- | --- | -| roundtrip | no-op, rename and insert writes compared byte-wise, canonically and as git / single-region diffs; serialisers of the host editor and the desktop modeler as references | 396 unique files (291 real, 105 scenarios) | -| integration | the host's requirements (ADR 0008 of the PR #218 tool) checked one by one; probe insert on real models validated by the host's validator | 176 real models probed; 189 files for parse / serialise identity | -| coverage | inventory of unit tests and fixtures; construct x operation matrix; 49 probe kinds; seeded 30-step agent sessions | 6,011 probe runs on 208 files; 7,660 session steps | -| complex structures | adversarial hunt on nested / event / collapsed sub-processes, transactions, compensation, links, multi-instance; structure-aware fuzzer | about 2,300 mutations; 47 full redraws | -| collaborations and platforms | collaborations, Camunda 7 / Camunda 8 extensions, lanes, message flows; 26 scenarios per file in auto / incremental / full | 4,365 single-op runs on 212 files; about 3,000 fuzz steps | -| scale, fuzzing, determinism | seeded random walks (34 op types, invariants after every step), determinism pairs, fixpoints, performance on models up to 317 flow nodes | 576 walks, about 37,400 CLI calls | -| dogfooding, new models | an agent building three models from a brief with the guide only | about 60 CLI calls, 8 renders | -| dogfooding, large models | an agent navigating and editing five large real models; validation of a real corpus | 96 logged calls; 213 files validated | - -### Corpora (aggregate) - -| corpus | files | flow nodes p50 / p90 / max | notes | -| --- | --- | --- | --- | -| modeler-drawn real models (private) | 60 | 15 / 28 / 57 | 10 with lanes, 10 with >= 2 pools, 18 with event sub-processes, 8 with sub-process depth >= 2 | -| broader real corpus (private) | about 290 | median 11 shapes, p90 32, max 62 | 209 Camunda 7, 38 Camunda 8, 44 without vendor namespace; 58 collaborations, 96 with sub-processes, 93 with boundary events, 14 with several diagrams | -| tools/scenarios (public, in this repo) | 111 at audit time | 14 / 29 / 39 | engine-drawn, stale DI from an earlier engine version | -| test/fixtures/incremental (public) | 22 | 7 / 17 / 18 | synthetic, nesting depth <= 1 | -| generated large models | 15 + 115 perturbed variants | 12–317 | PR #218 generator and CLI-built S/M/L/XL models; space-tool style DI perturbation so that incremental mode runs | - -No local file reached the size of the target platform's models (105–125 flow -nodes per plane, 324–676 KB according to the public PR #218 documentation); -the largest real file had 62 flow nodes and 90 KB. Engine-drawn synthetic large -models are too regular to show defects, so they are no substitute. - -## Results - -### Robustness - -| invariant (scale / fuzz stream, 37,400 calls) | result | -| --- | --- | -| crash (exit 70, signal, timeout) | 0 | -| usage error on a generated op | 0 | -| file modified by a failed command | 0 of 1,660 refusals | -| new import warnings after a write | 0 | -| duplicate, dangling, stale or missing DI | 0 | -| NaN, negative or zero sizes | 0 | -| BPMNEdge with fewer than 2 waypoints | 1 (bug #10) | -| DI on the wrong plane | 18, all associations after a collapse (bug #15) | -| shapes moved without being reported | 0 | -| connections reshaped without being reported | 91 (bug #61) | -| result metrics vs a standalone `metrics` run | 0 mismatches | - -The other streams agree: the complex-structure hunt found every written file -re-importable with DI on the right plane (including drill-down planes) and the -guards (cross-scope, invalid trigger, lane membership, moving a sub-process into -its own child, cancel / compensation / error-start checks) correct in every -case tried. In auto and incremental mode no run lost or changed extension -elements, vendor attributes, documentation or namespace declarations; DI losses -came from full redraws only. - -### Determinism - -- 10 walk pairs of 40 ops: all 370 written step files byte-identical, JSON - results identical apart from timing. -- The same batch from another working directory, time zone and locale: - byte-identical. -- Fixpoints: `layout(layout(x)) == layout(x)` and `tidy(tidy(x)) == tidy(x)` on - 309 of 309 files. -- A second no-op write on the CLI's own output was byte-identical for every - real and scenario file. -- No `Math.random` or date use in `src/`. - -### Performance - -Measured under heavy host load (upper bounds), median of 3 runs: - -| model size | incremental | full | `--no-layout` | -| --- | --- | --- | --- | -| 120–128 flow nodes | 118 ms (max 140) | 141 ms (max 173) | 103 ms | -| 300–317 flow nodes | 213 ms (max 352) | 268 ms (max 402) | 180 ms | - -Node start-up is about 45 ms. The PR #218 layouter needed 14–15 s for the -300-node models. Output size, not speed, is the scaling problem for agents: -`show` on 317 nodes is about 39 KB (about 10k tokens), `show --json` about -159 KB, and a plain `add` on such a model returns about 3.7k tokens, mostly -repeated pre-existing lint warnings (bug #60). - -### Layout quality - -- Scale fuzzing: 225 of 35,260 successful steps (0.64 %) added a hard defect - the metrics report, in 143 of 568 walks. A further 0.77 % of steps (one - campaign) produced frame intrusions or overlapping frames the metrics could - not see (bug #8). -- Worst operations by hard-defect rate: `place --below --column-of` 5.3 %, - expand / collapse 3.7 %, lane order 3.0 %, `align` 2.5 %, `split` 1.4 %, - add after 1.0 %, add into a flow 0.8 %. 0 % for rename, retype, color, label, - remove flow, space after, tidy and lane moves. -- Probes (208 files, real and public): 73 of 6,011 runs (1.2 %) added a - reported hard defect; 7 added a silent frame intrusion. Agent sessions on real models: 0.41 % of - steps added a reported hard defect, 0.81 % a silent one, and 15 % of 30-step - sessions ended with at least one layout defect. -- Nested sub-processes: 29 of 246 inserts (12 %) into nested sub-processes of - real models let a growing frame cover a neighbour; the metrics reported - "score 0 -> 0" in 34 of the 35 affected runs (bugs #4, #8). -- Full redraws: on lane models with loops the clean engine can place nodes - outside their lanes and score far worse than `--engine auto` (bug #7); event - sub-processes, collaboration annotations and cross-scope associations have - dedicated full-engine bugs (#17–#20, #23, #24). -- Locality of incremental edits (share of existing shapes moved, median / p90): - add after 9 % / 69 %, add into a flow 20 % / 76 %, split 31 % / 80 %, lane - move 2 % / 7 %, remove 0 % / 37 %. - -### Roundtrip fidelity - -- No vendor content lost in any write (camunda, zeebe, modeler, bioc / color, - unknown attributes, host stickies). -- Relative to a no-op write, a rename changes exactly 2 lines in 257 of 259 - files. -- Formatting churn: namespace declarations reordered (206 of 259 files), vendor - attributes moved behind typed ones (57), XML comments dropped (61), CDATA - rewritten (2), derived `incoming` / `outgoing` lists added (bugs #30, #31, - #44, #45). A no-op write is byte-identical in only 15 % of real files; a - text-preserving post-pass prototype raised that to 92 % and cut the median - rename diff from 5 to 2 lines. -- 6 of 265 writable real files were refused with E_VALIDATION, 4 of them false - positives (vendor form-field ids counted as BPMN ids, bug #1). -- Fixed since: see [step 2](#roundtrip-fidelity-of-writes). - -### Agent ergonomics (dogfooding) - -- Building three models from a brief: 58 calls, 6 errors (id guesses, pool - order, a wrong key), no XML fallback; renders were essential to see problems - the metrics missed. -- Five large real models: navigation questions took 2–24 calls; 3 XML - fallbacks were needed; 150–350 ms per call on a 92 KB model. - -## Confirmed bugs - -113 bug reports from the eight streams were merged into 80 candidates; the -verification pass reproduced 77 and rejected 3 as intended behaviour. - -| severity | count | -| --- | --- | -| high | 7 | -| medium | 36 | -| low | 34 | - -| area | count | -| --- | --- | -| incremental layout | 20 | -| semantic operations | 12 | -| CLI usability | 11 | -| validation | 9 | -| format operations | 9 | -| full layout | 8 | -| roundtrip | 6 | -| integration | 2 | - -Status after step 1 (2026-10-08, see [Step 1](#step-1-2026-10-08)): 17 fixed, -2 partly fixed, 58 open. After the Camunda 7 audit (#28 partly fixed) and -step 2 (2026-10-09, see [Step 2](#step-2-2026-10-09-design-iqs-editing-engine)): -30 fixed, 5 partly fixed, 42 open. After step 3 round 1 (2026-10-10, see -[Step 3 round 1](#step-3-round-1-2026-10-10-the-packages-together): the -packages [views](#step-3-2026-10-10-views-and-output), -[ids](#step-3-2026-10-10-speaking-ids-and-batches), -[layout](#step-3-2026-10-10-layout-ergonomics) and -[Camunda 8](#step-3-2026-10-10-camunda-8-profile)): 48 fixed, 6 partly -fixed, 23 open. After the -[round 1 fixes](#step-3-round-1-fixes-2026-10-10) -(#53 fixed): 49 fixed, 5 partly fixed, 23 open. After -[issue #18 stage B](#issue-18-stage-b-2026-10-12-room-making-row-choice-and-local-repair) -(#63 fixed): 50 fixed, 5 partly fixed, 22 open. - -| # | severity | area | bug | status | -| --- | --- | --- | --- | --- | -| 1 | high | validation | Vendor form-field / property ids are treated as BPMN ids: false E_DUPLICATE_ID blocks writes, `show` crashes, remove / set act inside the extension | fixed (step 1) | -| 2 | high | semantic-op | Changing an event's trigger details drops the vendor attributes and the id of its eventDefinition | fixed (step 1) | -| 3 | high | roundtrip | The library path (Doc.fromXml + mutateDoc) skips the lossy-import guard and silently drops content | fixed (step 1) | -| 4 | high | incremental-layout | A growing nested / expanded sub-process grows its ancestor frames over neighbours outside the space-tool band | fixed (step 1) | -| 5 | high | incremental-layout | An unconnected node added or moved into a sub-process or lane lands below the lowest edge label of the whole plane | fixed (step 1) | -| 6 | high | format-op | place / align with a reference in another pool or frame pushes the reference's frame away and leaves the reference outside it | fixed (step 1) | -| 7 | high | full-layout | The clean engine places nodes outside their lane on lane models with loops and scores far worse than `--engine auto` | fixed (step 1) | -| 8 | medium | validation | metrics, `show --layout` and `layout.metrics` cannot see frame intrusions, overlapping frames or degenerate edges | fixed (step 1) | -| 9 | medium | validation | metrics reports `through` false positives for diagonal segments (bounding-box test) | partly fixed (step 1): `through` uses a real segment test; `labelOnLine` and `msgThroughPool` still use the bounding box | -| 10 | medium | incremental-layout | The incremental layout can write a BPMNEdge with a single waypoint, unreported | fixed (step 1) | -| 11 | medium | format-op | align / place push shapes that give way onto the pinned reference or other shapes; the overlap is written | fixed (step 1 gate): a new overlap is never written; the references move along or the op is refused | -| 12 | medium | format-op | place of a node two levels deep after its grandparent stretches the inner sub-process out of the outer one | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): a reference that is one of the moved node's own frames is E_LEAVES_CONTAINER with a hint (since step 1 nothing stretched any more, but the refusal was a generic E_NO_ROOM) | -| 13 | medium | incremental-layout | Splicing into a loop-back flow puts the new node on the main row and reroutes the happy path over it | open | -| 14 | medium | incremental-layout | add into a cross-lane flow takes the source's lane but the target's row; the source lane grows by about 470 px | fixed ([step 3](#step-3-2026-10-10-speaking-ids-and-batches)): the lane of the row the node is drawn on (the target's after a branching source), `W_LANE_INHERITED` names the other lane; since the [round 1 fixes](#step-3-round-1-fixes-2026-10-10) also `move` of a node without a lane at its new place, and a node that keeps its lane (`move`, `add --lane`) goes on a row of that lane instead of stretching it over to the target's row | -| 15 | medium | incremental-layout | Collapsing a sub-process leaves border-crossing connections on the wrong plane | open | -| 16 | medium | incremental-layout | Flows into gateways or events stay detached when the shape moves by up to half its width | partly fixed (`fix/stale-edges-of-re-ended-flows`): an end more than 10 px outside its shape's box is routed again by the incremental layout and the format commands (metric `detachedEdge`); up to 10 px is still tolerated (`endGaps` lets a round end lie up to half its width outside the box) | -| 17 | medium | full-layout | The full engine draws event sub-processes in the last lane regardless of lane membership | fixed (step 1) | -| 18 | medium | full-layout | The full engine always expands event sub-processes; collapse requests are reported as done but ignored | fixed (step 1) | -| 19 | medium | full-layout | The full engine routes a boundary-handler path through nested expanded sub-processes | open | -| 20 | medium | full-layout | A full layout drops the DI of collaboration-level annotations and associations | fixed (step 1); group boxes are drawn again around their members since `fix/full-redraw-keeps-groups` | -| 21 | medium | cli-ux | `show` omits text annotations owned by the collaboration | fixed ([step 3, views](#step-3-2026-10-10-views-and-output)): listed under the collaboration with what they are attached to | -| 22 | medium | cli-ux | `show ` lists no message flows, data associations or associations | fixed ([step 3, views](#step-3-2026-10-10-views-and-output)): message flows with the partner and its pool, associated annotations, data read / written | -| 23 | medium | full-layout | The full engine gives cross-scope associations no DI and draws cross-scope data associations as diagonals | partly fixed (step 1): drawn when both ends are on one plane, else reported; still straight lines | -| 24 | medium | full-layout | Full-engine message flows run through shapes of lower lanes; the pool order is fixed | fixed (step 1): automatic pool order; a manual pool order since [step 3](#step-3-2026-10-10-layout-ergonomics) (`order `) | -| 25 | medium | full-layout | The clean engine produces through-shape defects on large generated lane models | open | -| 26 | medium | validation | Pre-existing validation errors (e.g. a complexGateway, black-box-only collaborations) block every write | fixed (step 1) | -| 27 | medium | validation | Validation is laxer than the host's save gate: the CLI writes models the host would reject | fixed ([step 2](#validation-hook-and-design-profile)): validator hook in the transaction and a design profile mirroring the save gate. The design profile was removed with issue #35 (2026-10-13): the host passes its own validator through the hook | -| 28 | medium | validation | No vendor schema or platform awareness: wrong-platform extensions and typos are accepted | fixed: platform detection and a Camunda 7 profile ([C7 audit](#camunda-7-audit-2026-10-09)), a Camunda 8 profile and the Zeebe descriptor ([step 3](#step-3-2026-10-10-camunda-8-profile)) | -| 29 | medium | semantic-op | The host's unprefixed `calledDecision` link cannot be set | fixed ([step 2](#validation-hook-and-design-profile)): `set calledDecision=` in the file's spelling | -| 30 | medium | roundtrip | Namespace declarations and vendor attributes are reordered on every write | fixed ([step 2](#roundtrip-fidelity-of-writes)) | -| 31 | medium | roundtrip | XML comments are dropped on every write, silently | fixed ([step 2](#roundtrip-fidelity-of-writes)) | -| 32 | medium | semantic-op | Sequential ids for unnamed elements (Flow_n) collide between independent edits | fixed ([step 2](#edits-follow-the-file)): flows and unnamed elements get a hash of stable inputs; files that number their flows keep numbering. Since [step 3](#step-3-2026-10-10-speaking-ids-and-batches) speaking ids instead (kind + place, the ends of a flow), numbered files included | -| 33 | medium | integration | The library is not browser-safe (node:fs / node:path imports, process.env at runtime) | fixed ([step 2](#isomorphic-core)) | -| 34 | medium | semantic-op | retype of a sub-process with content deletes the whole subtree with only warnings | fixed (step 1) | -| 35 | medium | semantic-op | remove / move of a node next to an annotated sequence flow fails with E_DANGLING_REF | fixed (step 1) | -| 36 | medium | validation | Validation gaps on complex constructs (boundary on an event sub-process, second none start, event-gateway conditions, split link pairs) | open | -| 37 | medium | validation | Moving a compensation handler or its host splits the pair across scopes without an error | open | -| 38 | medium | semantic-op | Lane membership and lane geometry diverge (first lane, remove lane, move --in) | open | -| 39 | medium | incremental-layout | Removing the only (empty) lane, or the only child lane, collapses the pool or lane to 1 px | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): the only lane of a pool / parent lane leaves its frame as it is | -| 40 | medium | format-op | Lane reorder leaves members drawn outside their band, or overhanging boundary events, outside the pool | partly fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): a band whose members hang out at its bottom (a boundary event on the border) grows with the pool; a member drawn in another band still moves with its semantic lane | -| 41 | medium | incremental-layout | Data objects and annotations do not follow a lane the space tool moves | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): a shape no lane lists moves with the lane it is drawn in | -| 42 | medium | incremental-layout | Boundary events on a shrunk or moved sub-process border are not collision-checked | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): a strip whose closing would put a moved shape (a boundary event riding the border) onto another one stays open | -| 43 | medium | integration | Host stickies (`bpmiq:sticky`) do not follow nodes the incremental layout moves | fixed ([step 2](#edits-follow-the-file)): they follow the nearest flow node after incremental layout, format ops and redraws | -| 44 | low | roundtrip | CDATA sections are rewritten as entity-escaped text | fixed ([step 2](#roundtrip-fidelity-of-writes)) | -| 45 | low | roundtrip | Every write adds `incoming` / `outgoing` entries to files that omit them | fixed ([step 2](#roundtrip-fidelity-of-writes)) | -| 46 | low | cli-ux | No-op writes still rewrite the file and report a change | fixed ([step 2](#roundtrip-fidelity-of-writes)) | -| 47 | low | roundtrip | A full redraw renames every DI id | fixed ([step 2](#edits-follow-the-file)): every DI, plane and diagram id stays; new DI in the file's DI id style | -| 48 | low | cli-ux | Generated ids ignore the file's id style; a bridged flow keeps an id naming the removed node | fixed: ids follow the file's style ([step 2](#edits-follow-the-file)); a bridged, spliced or moved flow whose id names its ends is renamed after its new ends ([step 3](#step-3-2026-10-10-speaking-ids-and-batches)) | -| 49 | low | format-op | The library's mutateDoc does not validate ops: `space` with by:"80" moves content by about 29,000 px | partly fixed ([step 2](#isomorphic-core)): `applyToXml` checks and normalises ops like `apply`; `mutateDoc` still takes typed ops unchecked | -| 50 | low | semantic-op | A foreign prefix for the Camunda namespace breaks `--unset`, `ext add --replace` and `ext remove` | open | -| 51 | low | semantic-op | Rename does not follow id references inside vendor extensions; removing a process does not warn about callers | open | -| 52 | low | semantic-op | retype keeps vendor attributes and template bindings that no longer apply, without a warning | open | -| 53 | low | semantic-op | Removing a join with N incoming flows disconnects the rest of the scope and floods the output with warnings | fixed ([step 3](#step-3-2026-10-10-speaking-ids-and-batches), [round 1 fixes](#step-3-round-1-fixes-2026-10-10)): `remove --bridge-all` reconnects every predecessor, `--with-branch` removes a branch without orphans; a plain remove bridges a merge from every predecessor and refuses a parallel / inclusive join with the two exact commands (`E_AMBIGUOUS_BRIDGE`) | -| 54 | low | semantic-op | connect twice silently creates a duplicate flow | open | -| 55 | low | cli-ux | E_NOT_FOUND suggests no candidates for umlaut-transliteration variants of an id | fixed ([step 2](#edits-follow-the-file)); since [step 3](#step-3-2026-10-10-speaking-ids-and-batches) also typos, other / no prefixes, the ids and renames of the running batch | -| 56 | low | cli-ux | Id slugs transliterate German inconsistently (ä -> a but ß -> ss) | fixed ([step 2](#edits-follow-the-file)): ä -> ae, ö -> oe, ü -> ue, ß -> ss | -| 57 | low | cli-ux | `order ` gives a misleading E_WRONG_KIND | open | -| 58 | low | cli-ux | `-o -` writes a file named `-`; `show -` does not read stdin | fixed ([step 3, views](#step-3-2026-10-10-views-and-output)): `-` reads the model from stdin, `-o -` writes to stdout (the report to stderr) | -| 59 | low | cli-ux | `--scope` prints the whole file's root elements and problems and does not accept lanes | open | -| 60 | low | cli-ux | Per-call output does not scale: pre-existing lint warnings are repeated on every mutation | fixed ([step 3, views](#step-3-2026-10-10-views-and-output)): warnings as a delta (added / resolved / a count), floods as one line, `--summary`, compact JSON | -| 61 | low | cli-ux | `layout.rerouted` omits connections reshaped by space-tool passes | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): `layout.reshaped` and `format[].reshaped` list the connections the space tool stretched without routing them again | -| 62 | low | format-op | `route --exit` accepts a side pointing into a boundary event's host | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): E_INVALID_VALUE for an exit / entry side that points into the host | -| 63 | low | incremental-layout | A new branch inside a transaction lands on the transaction's own bottom boundary event | fixed ([issue #18 stage B](#issue-18-stage-b-2026-10-12-room-making-row-choice-and-local-repair)): a frame grows until a shape stays 10 px clear of the frame's own boundary events on that border (space.ts borderPad), and the row choice estimates that growth | -| 64 | low | incremental-layout | Collapsing a nested sub-process does not close the freed gap | open | -| 65 | low | incremental-layout | Expanding a collapsed sub-process leaves a flow routed outside the new frame | open | -| 66 | low | incremental-layout | Collapse or retype reseats boundary events from two sides into the same corner | open | -| 67 | low | incremental-layout | 1 px diagonals are emitted, and a flow already 1 px off orthogonal is never rerouted | open | -| 68 | low | incremental-layout | Wrapping a laned process in a new pool leaves 30 px gaps around the lanes | open | -| 69 | low | format-op | place / align refuse a column alignment for a 2 px overlap with a lane header | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): up to 10 px into the header of a lane or pool are accepted (a sub-process's border stays the limit) | -| 70 | low | incremental-layout | A new empty lane gets the median sibling height (460 px and more) | open | -| 71 | low | format-op | align by column pushes external event labels outside the pool; metrics do not check labels against pools | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): the frame grows for a label at its right too; `labelOutsideFrame` reports labels outside their pool / sub-process | -| 72 | low | format-op | `place --before` can create a backward flow, and no metric reports it | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): reported as `backwardFlow` (soft metric kind, in `layout.metrics.added` of the place); `show --layout` prints columns | -| 73 | low | validation | Two flows into one target sharing a final segment are not flagged | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): reported as `segmentOverlap` (later, by the maintainer's decision, not for a join's entries sharing a gateway's bottom / top vertex) | -| 74 | low | incremental-layout | A new task is placed 35 px left of its rejoin target; two flows share the final segment | partly fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): the shared final segment is reported (`segmentOverlap`, see #73); the placement is unchanged | -| 75 | low | semantic-op | An annotation connected to a node inside a collapsed sub-process gets an association without DI, and validate passes | open | -| 76 | low | incremental-layout | The first participant on a plane without bpmnElement is placed beside the content instead of wrapping it | open | -| 77 | low | incremental-layout | The space tool shifts the whole pool for a local insert, and gaps cannot be closed again | fixed ([step 3](#step-3-2026-10-10-layout-ergonomics)): a splice pushes only by the room that is missing; `compact` and `space --by -column` close gaps | - -## Step 1 (2026-10-08) - -The first fix round took all seven high-severity bugs and twelve medium -ones (statuses in the table above), plus problems the gate itself found: - -- **Semantic layer** (#1, #2, #3, #26, #34, #35): vendor extension ids are no - longer BPMN ids; changing trigger details keeps the event definition; - `mutateDoc` refuses lossy imports; only errors a change introduces block a - write (`W_PREEXISTING_ERROR` for the others); retyping a sub-process with - content needs `--force` (`E_WOULD_DROP_CONTENT`); associations follow a - bridged flow. -- **Incremental layout and format ops** (#4, #5, #6, #8, #9 partly, #10): a - growing frame makes room around it instead of growing over foreign shapes - ("frame mode" of the space tool); unconnected nodes go below their own - container's content; `place` / `align` refuse to push a reference's frame - away (`E_NO_ROOM`); the metrics gained `frameIntrusion`, `frameOverlap` and - `degenerateEdge` (hard, weight 10) and a real segment test for `through`; - no connection is written with fewer than two waypoints. -- **Full engine** (#7, #17, #18, #20, #23 partly, #24): lane bands follow the - content after loop routing; event sub-processes go into their own lane and - honour collapse; collaboration annotations are laid out; pools are ordered - by message-flow cost and message-flow legs avoid shapes. -- **Found by the gate, fixed:** (a) growing a task inside an event - sub-process that the drawing has outside its member lane moved the - sub-process 600 px down without the task (`outsideSub`; drawings of the - previous engine version, which put event sub-processes into the last lane; - the pre-step-1 build does the same); (b) removing a node next to a flow - with a stale `incoming` entry on another node (a modeler file) failed with - `E_DANGLING_REF`; (c) a raw NUL byte in a source file, which makes git treat - it as binary; (d) a regression of the new frame mode: `place --row-of R` - could make the shapes around a pinned reference R room and push them onto - it (one step in 16,000 fuzz steps). The fix is general: `place` / `align` - never write a new overlap (the references move along when the request - still holds, else `E_NO_ROOM`), which also fixes #11. Regression tests: - `test/step1-gate.test.ts`, `test/repo-hygiene.test.ts`, and the two #11 - walks of `test/fuzz.test.ts` now pass. - -### Gate results - -Baseline is the build before step 1; both builds are measured with the new -metrics library, so the new hard kinds count for both. - -- **Unit and property tests**: all green, `npm run typecheck` clean, `npm run - gate` passes. -- **Layout regression** (`tools/layout-regress.mjs`): the original 111 - scenarios score 402 (472 at audit time; 464 for the old engine with the - corrected `through` test), 5 files better, none worse. The 4 new scenarios - score 42 (old engine 245). Budget: 115 files, SCORE 444. -- **Benchmark**, auto mode, edits E1–E6 pooled (new / baseline): - -| corpus | runs | succeeded without `--force` | semantic success | runs adding a hard defect | hard defects added | other nodes moved > 5 px (median share) | score change (mean) | full layout: hard defects after | -| --- | --- | --- | --- | --- | --- | --- | --- | --- | -| tools/scenarios | 502 | 100 % / 100 % | 100 % / 100 % | 0 / 3 | 0 / 7 | 11 % / 12 % | -0.7 / -0.8 | 3 / 33 | -| scenarios redrawn by the previous engine | 509 | 100 % / 100 % | 100 % / 100 % | 5 / 11 | 5 / 20 | 21 % / 21 % | -0.8 / -0.4 | – | -| modeler-drawn models (private, 60) | 263 | 100 % / 95 % | 100 % / 100 % | 0 / 0 | 0 / 0 | 16 % / 16 % | +0.4 / +0.6 | 18 / 46 | - - On the redrawn scenarios 49 of 509 runs are now incremental instead of a - full redraw: a drawing of the previous engine version is no longer - recognised as engine-owned on laned and collaboration models. All five - remaining hard defects there are `through` problems of full redraws, in - runs where the baseline adds one as well (four of them identical). -- **Fuzzing**: two campaigns of 200 walks of 40 steps (seeds 7 and 8), auto - and incremental mode alternating, on tools/scenarios, 115 perturbed - variants and the private modeler corpus; the same walks for both builds: - -| | this build | baseline | -| --- | --- | --- | -| successful steps | 15,588 | 15,399 | -| refused with `E_VALIDATION` | 14 | 216 | -| steps adding a hard defect of the audit's kinds (overlaps, through, missing, outside*) | 22 (0.14 %) | 108 (0.70 %) | -| further steps adding only a frame defect (frameIntrusion, frameOverlap, degenerateEdge) | 3 (0.02 %) | 63 (0.41 %) | - - The audit measured 0.64 % and 0.77 % for the same two rates. Every step of - this build that added a hard defect was replayed on the same input with - the baseline build, and the baseline adds exactly the same defects: none of - them is a regression. They belong to open bugs: a data object placed onto - a shape or into a foreign sub-process, `route` and expand / collapse - toggles through shapes, boundary events and annotations colliding (#42, - #63), `place --below` stretching a nested sub-process (#12-like). The DI - invariants found one more open bug, associations on the wrong plane after a - collapse (#15), in both builds. -- **Performance**: an insert (E1) on 56 modeler models, run one after the - other: median 79 ms against 77 ms before. - -### Open findings from the gate - -- A data object or annotation that finds no free spot falls back to its - first candidate and can land inside a foreign sub-process (reported as - `frameIntrusion`). -- Data associations are not rerouted after `split` or `move --lane` and can - run through gateways. -- `labelOnLine` still uses the bounding box of a segment, so a diagonal - association counts every label in its box (#9); most `labelOnLine` - additions in the benchmark are this artefact. -- An edit on a drawing of the previous engine version that has event - sub-processes outside their lanes keeps that drawing (incremental mode); - only a full redraw moves them into their lanes. - -## Camunda 7 audit (2026-10-09) - -A second audit asked one question: does `bpmn-cli` handle Camunda 7 -properties correctly? Four streams (breadth of the Camunda descriptor, -execution on the engines, real files, descriptor coverage and agent -ergonomics) and a verification pass ran against Camunda 7.24.0, CIB seven -2.2.0 and Operaton 2.1.5 over REST; the three engines gave the same verdict on -every model. The real files were 218 Camunda 7 / CIB seven models from -private corpora (2,651 `camunda:` attributes, 4,849 `camunda:` elements); -only aggregate numbers appear here. - -**Verdict before the fixes.** Camunda content was preserved perfectly: across -the 218 real files and eight edit types (rename, insert, remove with -bridging, lane move, retype, boundary event, colour, full redraw), about -4,600 camunda items per edit type were compared element by element and none -changed outside the edited element, and no edit turned a deployable file into -an undeployable one in the main battery. But the CLI treated camunda content -as untyped: attributes of nested elements (event definitions, multi-instance -loops, condition expressions) could not be set and landed silently on the -parent, `ext add` duplicated single-instance containers and dropped child -types loose into `extensionElements`, `new --target camunda7` wrote files no -engine deploys, `validate` knew nothing of Camunda 7, and some bridges next -to event-based gateways produced files the engines refuse (37 deploy -regressions in an exhaustive sweep of 4,100 edits). - -The verification pass confirmed 18 findings (one more was intended behaviour: -a timer expression without `--timer-kind` becomes `timeDate`, which the -engines resolve for durations and dates alike). - -| # | severity | finding | status | -| --- | --- | --- | --- | -| C1 | critical | `new --target camunda7` writes no `camunda:historyTimeToLive`; the minimal model is refused by all three engines (ENGINE-12018) and `validate` says it is valid | fixed: `new --target camunda7` writes TTL 180 and the platform version like Camunda Modeler; a new pool's process gets the TTL; `W_C7_DEPLOY_HISTORY_TTL` | -| C2 | critical | renaming a `bpmn:Error` leaves `camunda:errorEventDefinition errorRef` dangling; the external-task error mapping silently stops working | fixed: rename re-points the id-valued attributes the descriptor knows; `W_C7_DANGLING_REF` for loaded files | -| C3 | high | attributes of event definitions, multi-instance loops and condition expressions cannot be set; `set` writes them on the parent, where the engines ignore them (or refuse the file for a collection) | fixed: `definition.` / `loop.` / `condition.` keys, `E_WRONG_HOST` on the parent with the right key in the hint, the same prefixes on `ext` for their extension elements, `W_C7_MISPLACED_ATTRIBUTE` for loaded files | -| C4 | high | `ext add` appends a second `camunda:inputOutput` / `formData` (engines refuse: ENGINE-01009); child types land loose in `extensionElements` and are ignored | fixed: child types are filed into their container, single-instance containers merged (`E_DUPLICATE_EXTENSION` on a conflict), keyed items replaced, selectors for `ext remove`; `W_C7_DEPLOY_DUPLICATE_EXTENSION`, `W_C7_MISPLACED_EXTENSION` | -| C5 | high | `remove` bridges an event-based gateway branch to a non-catch element; the engines refuse the file | fixed: `E_INVALID_BRIDGE` (remove, move, move --in); `W_C7_DEPLOY_EVENT_GATEWAY` (also for a receive task after the gateway) | -| C6 | medium | `ext add --xml` refuses `bpmn:` children inside a vendor element (timeout task listener, `camunda:potentialStarter`) | fixed (accepted nested, refused at the top level; no second prefix declared) | -| C7 | medium | `set condition=` drops `camunda:resource` silently and keeps the script language (the engines then fail at run time) | fixed: changed in place, `W_PROPERTY_DROPPED`; a `${...}` body does not inherit a script language | -| C8 | medium | `show` / `find` hide vendor values and nested / process / flow-level camunda content | fixed: values, nested keys, process and flow content, `xN` counts; `find` matches vendor values | -| C9 | medium | `validate` has no Camunda 7 awareness (0 of 35 engine-relevant variants flagged) | fixed: the Camunda 7 profile (33 codes, severity deploy / runtime / practice), auto-detected; writes report the findings they introduce; `camunda:` Boolean attributes are written as exactly true / false | -| C10 | medium | `retype` keeps camunda attributes and extension elements the new kind cannot use, silently | fixed: `W_PROPERTY_INAPPLICABLE` names them with the commands that remove them (content kept on purpose), plus the profile findings (e.g. a service task without implementation) | -| C11 | medium | no `message=` for receive / send tasks; `add --message` ignored | fixed: `set message=`, `add --message`, `"message"` in apply | -| C12 | medium | a bridge can give two branches of one event-based gateway the same message | fixed: `E_INVALID_BRIDGE`; `W_C7_DEPLOY_EVENT_GATEWAY` / `W_C7_DEPLOY_MESSAGE` | -| C13 | medium | `ext add` on `bpmn:definitions` writes XSD-invalid `extensionElements` | fixed: `E_WRONG_KIND`; `W_C7_DEPLOY_DEFINITIONS_EXTENSION` | -| C14 | low | a boundary event can be attached to a compensation handler | fixed: `E_INVALID_HOST` in `add --on` / `move --on` and as a structural check (so `set isForCompensation=true` on a host with boundary events is blocked and loaded files are reported); `W_C7_DEPLOY_BOUNDARY_HOST` | -| C15 | low | `set loop=none` (or a loop type switch) drops multi-instance vendor content without a warning | fixed: `W_PROPERTY_DROPPED` | -| C16 | low | text views flatten nested extension content (`show`) or hide it (`ext list`) | fixed: an indented tree in both | -| C17 | low | `new --target` accepts unknown platforms silently | fixed: `E_USAGE` (camunda7 covers CIB seven and Operaton) | -| C18 | low | `ext add --xml` ignores the positional type | fixed: `E_INVALID_VALUE` on a mismatch, nothing written | - -**Re-verification after the fixes** (integrated build, all three engines): - -- The repro of every finding, re-run with the new CLI: 108 of 108 checks - pass, among them the controls (the old CLI's outputs are still refused by - the engines and now flagged by `validate`). The fix rounds' own engine - suites pass unchanged on the integrated build (39 / 39 semantic checks; the - extension-structure scenarios with identical deploy and run verdicts). -- The profile's 82 synthetic models with live engines - (`BPMN_C7_ENGINES=... npx vitest run test/c7-profile.test.ts`): 246 of 246 - engine verdicts as stated, a deploy finding exactly where the engines - refuse. Measured while the profile was written: on the real corpus the - file-level deploy findings have precision 1.000 and recall 0.867 (13 of 15 - refused files; both misses are XSD-level errors the profile does not - check), the Camunda 8 and plain files of the corpus get no C7 finding, and - 430 unrelated edits of real files added none. -- The real-file battery (218 files, eight edit types, deploy before and after - on all three engines): the same numbers as before the fixes, 0 regressions, - 0 unexpected camunda changes, 0 new typed-moddle warnings. The exhaustive - sweep: the 37 former deploy regressions (30 bridges next to event-based - gateways, 7 boundary events on compensation handlers) are now refused - (`E_INVALID_BRIDGE`, `E_INVALID_HOST`), no refusal hit an edit whose result - had deployed, and no other result changed. The chained session (all eight - edits on one copy): unchanged, 0 failed steps, 0 regressions. -- The execution scenarios, rebuilt with the new CLI and run on the engines: - 65 of 65 checks, including what the old CLI could not express: error code - variables on the error definition, a message throw and a message end event - as external tasks, a conditional event filtered by `variableName`, a - collection multi-instance, a retry cycle on an async multi-instance body - (`ext add loop.camunda:failedJobRetryTimeCycle`), `camunda:in` on a - signal definition, an escalation code variable, generated forms built from - child types and paths, and the README's worked example. - -**Still open** (none blocks a deploy the engines would accept; status after -the follow-up round below in brackets): - -- No Camunda 8 rules: detection reports `c8`, but camunda content in a - Camunda 8 file is not flagged. [fixed in step 3: the Camunda 8 profile, - `W_C8_FOREIGN_CONTENT` among its rules] -- The profile does not check XSD-level errors (element order, unprefixed - attributes such as `calledDecision`), garbage `dueDate` / retry cycle - values, or `class` + `expression` precedence; the form-field type rule - reports custom form types a plugin may register. [unprefixed attributes: - fixed, `W_C7_DEPLOY_SCHEMA`; the rest open] -- `operaton:historyTimeToLive` satisfies the TTL rule, although Camunda 7 and - CIB seven refuse such a file. [decided: a file that uses the operaton - namespace targets Operaton and is read the way Operaton reads it; the - platform line and `"operaton": true` say that only Operaton reads it] -- `connect` and `add --after` can still build an event-based gateway branch - the engines refuse (only bridges are refused); in a Camunda 7 file the - write reports `W_C7_DEPLOY_EVENT_GATEWAY`. The generic lint - `W_EVENT_GATEWAY_TARGET` still accepts a receive task (BPMN allows it). - [explicit edges warn by design; plain files follow the BPMN 2.0 rule now, - `connect` / `add` / `move` warn `W_EVENT_GATEWAY_TARGET`] -- `W_MISPLACED_EXTENSION` is a warning, not an error. The Zeebe structure - rules (single-instance elements, containers, keys) follow the Zeebe - descriptor and the Modeler's convention; no Zeebe engine checked them. - [Zeebe rules engine-checked in step 3 (Camunda 8.9); the warning stays a - warning] - -### C7 follow-ups (2026-10-09, verifier round) - -An independent verifier re-checked the Camunda 7 step on the same three -engines and the 218 real files and reported 15 follow-ups (each with a -repro) plus profile noise. Two fix rounds (the profile and the operations) -and an integration pass fixed them; the integration pass found and fixed -seven more. Every fix has a regression test that fails on the code before -it (`test/c7-followups-profile.test.ts`, `test/c7-followups-ops.test.ts`, -`test/c7-followups-integration.test.ts`; the profile tests carry the engine -verdict of each model and re-check it with `BPMN_C7_ENGINES`). - -| # | follow-up | status | -| --- | --- | --- | -| F1 | false `W_C7_DEPLOY_EVENT_DEFINITION` on a conditional event whose condition is a `camunda:resource` script (the engines deploy and run it) | fixed: a resource condition is a condition; only a missing `` element is a deploy finding | -| F2 | `set condition.camunda:resource=` left an empty script condition: no warning, valid, every start fails | fixed: refused (`E_INVALID_VALUE`) unless `condition=` / `when=` comes in the same command; loaded files: new runtime `W_C7_EMPTY_CONDITION` (flows the engines evaluate, conditional events) | -| F3 | two none / timer start events in a process, two start events in a sub-process: valid, all engines refuse | fixed: new `W_C7_DEPLOY_START_EVENT` (also a (sub-)process with content but no start event); lint `W_NO_START` gives way to it | -| F4 | duplicate signal start, receive task + boundary on one message, link throw without catch / duplicate link catch names, timeout task listener without id: valid, all engines refuse | fixed: subscriptions grouped by the engine's scope (`W_C7_DEPLOY_MESSAGE` / `W_C7_DEPLOY_SIGNAL`), new `W_C7_DEPLOY_LINK`, `W_C7_DEPLOY_LISTENER` for the id | -| F5 | `definition.activityRef` outside the throw event's scope accepted; all engines refuse | fixed: `E_CROSS_SCOPE` in `set`; loaded files `W_C7_DEPLOY_EVENT_DEFINITION` (an event sub-process may name its parent scope) | -| F6 | Operaton-namespace files validated as C7, but `ext` / `set` / rename handled only `camunda:` (loose parameters, duplicate containers, dangling errorRef, `yes` booleans, no `E_WRONG_HOST`) | fixed: the operaton namespace is part of the Camunda 7 family everywhere (ext structure rules by local name with their own containers, rename, booleans, `E_WRONG_HOST`, retype, resource conditions, TTL of new processes); `validate` reads operaton first like Operaton and flags `"operaton": true` | -| F7 | `definition.` on an event with two event definitions edited the first one silently (the one the engines ignore) | fixed: `E_AMBIGUOUS_NESTED`, `definition[].` / `definition[].` in `set`, `ext`, ops JSON, `show`, `ext list`; `set trigger=` keeps one; new runtime `W_C7_MULTIPLE_EVENT_DEFINITIONS` (integration) | -| F8 | validate hints rebuilt repeatable extension elements with `ext add --replace`, deleting their siblings; "take" text for other events | fixed: remove that one by its selector, add it again (hints run through the CLI in the tests) | -| F9 | `camunda:type="shell"` and an empty conditional-event condition reported as deploy refusals; the engines deploy both | fixed: shell tasks accepted (command field, fixed values, true/false flags checked as the engines do); empty condition is runtime `W_C7_EMPTY_CONDITION` | -| F10 | keyed replacement in `ext add` dropped the old item's children silently | fixed: `W_PROPERTY_DROPPED` naming them, the hint holds the `--xml` that keeps them | -| F11 | text views flattened multi-line bodies | fixed: `body:` plus one `\| ` line per line | -| F12 | ops JSON `ext remove` with `"type": "loop.0"` failed | fixed (`"0"`, `"loop.0"`, `"definition[1].0"`) | -| F13 | `E_INVALID_BRIDGE` applied the C7 rule to plain BPMN files | fixed: plain files follow BPMN 2.0 (receive tasks allowed unless mixed, with boundary events or other incoming flows), the message names the rule; explicit edges warn | -| F14 | two loop characteristics on one task: the first dropped silently on any write | fixed: a duplicate single element is a lossy import (`E_IMPORT_LOSSY`; `--force` keeps the last); `show` / `validate` print it | -| F15 | throw-only / catch-only settings on the wrong side of an event definition not flagged | fixed: runtime `W_C7_MISPLACED_ATTRIBUTE` / `W_C7_MISPLACED_EXTENSION` with remove commands | -| N1 | noise: a boundary event on a compensation handler reported twice (`E_INVALID_HOST` + `W_C7_DEPLOY_BOUNDARY_HOST`) | fixed (integration): `validate` and writes show the structural error only | -| N2 | noise: one retype printed `W_PROPERTY_INAPPLICABLE` plus one `W_C7_MISPLACED_*` per attribute | fixed: a write drops the summary when the profile names every item | -| N3 | noise: `0 refused at deploy` for Camunda 8 files | fixed (integration): "no Camunda 8 engine rules yet" | -| N4 | noise: both camunda and zeebe declared, no content: "no vendor namespace" | fixed: the detail names both declarations; one platform detector for the profile and `Doc.platform()` | -| I1 | integration: of several event definitions the engines act on one and do not parse the others (engine-checked order per event type); the profile flagged ignored definitions (false deploy findings) and a multi-definition catch event behind an event-based gateway | fixed: the deploy rules look at the acting definition only; new runtime `W_C7_MULTIPLE_EVENT_DEFINITIONS`; hints say `definition[].` where `definition.` would be ambiguous | -| I2 | integration: `W_C7_DEPLOY_SCHEMA` could only say "edit the XML" | fixed: `set =` (also `definition.=`) removes an attribute BPMN does not define; the hint names it (the diagram: edit the XML or `bpmn layout`) | -| I3 | integration: `validate` ignored import warnings in its summary and in `--strict` | fixed: counted (`n import warning(s)`), `--strict` exits 5 | -| I4 | integration: a write hid an op `W_EVENT_GATEWAY_TARGET` about a new branch when lint reported another branch of the gateway | fixed: deduplicated per branch | -| I5 | integration: `MutationOptions.platform` did not reach the ops (bridge rule followed the detected platform) | fixed | -| I6 | integration: hints with `definition[].` keys were not shell-quoted | fixed | -| I7 | integration: a new process of an Operaton-only file got `camunda:historyTimeToLive` | fixed: `operaton:historyTimeToLive` with the file's prefix | - -**Engine evidence** (integrated build, Camunda 7.24.0, CIB seven 2.2.0, -Operaton 2.1.5; all models synthetic, every deployment deleted): - -- Every follow-up repro re-run with the new CLI: 145 of 145 checks (deploy - verdicts, run-time behaviour such as the script condition firing, inline - conditions routing, the timer-only behaviour of a message + timer catch - event, Operaton reading the operaton input mapping, and controls: the old - outputs are refused or fail as the new findings say). -- Which definition the engines act on: 36 catch-event pairs (intermediate, - process start, boundary; both XML orders), 12 event sub-process pairs, 4 - throw-event pairs and 7 event-gateway combinations, identical on all three; - an ignored definition is not even parsed (a broken one deploys, a broken - acting one is refused). -- The suites of the Camunda 7 step on the integrated build: the audit repros - 108 / 108 (three checks follow the new contract: a retype in a C7 file - names each stale item once through the profile, a boundary event on a - compensation handler is reported once as `E_INVALID_HOST`), the semantic - suite 39 / 39, the execution scenarios 65 / 65, the extension-structure - scenarios unchanged (same deploy and run verdicts). -- Real files: the profile on the 218 originals has 0 deploy false positives - and finds 14 of the 15 refused files (the miss is an XSD element-order - error); on 5,860 derived files (edit chains) every verdict agrees. The - battery on 49 deployable real files (rename, error rename, insert, remove, - retype; 202 edits): every edit succeeds, every result deploys on all three - engines, no camunda content changes outside the edited element, the same - findings as before the round. - -**Still open after the follow-ups:** - -- The `activiti:` namespace, which Camunda 7 and CIB seven still read as a - fallback (Operaton does not), is not modelled. -- ~~Empty executable processes and empty sub-processes are left to the - lint; `adHocSubProcess` is not modelled.~~ Fixed in the second verifier - round (below). -- Model limits: a text-less `camunda:string` / `camunda:expression` field - child cannot be told apart from one holding blanks (refused vs accepted); - bpmn-moddle turns a blank-only body into an empty one on a rewrite. The XSD - element order is invisible in the model. -- The precedence of other event-definition kinds (error, escalation, - compensation, cancel, terminate) among several definitions is not checked; - such events are reported with "one of them" and all their definitions are - checked. - -### C7 follow-ups, second verifier round (2026-10-09) - -A second verifier re-checked the follow-ups on the same engines and files: -the 15 follow-ups hold, with two regressions and two hint / precision issues -that this round fixes. Regression tests that fail on the code before: -`test/c7-followups-verify.test.ts` (models with engine verdicts, re-checked -with `BPMN_C7_ENGINES`, and every hint run through the real CLI). - -| # | finding | status | -| --- | --- | --- | -| V1 | regression: a conditional event definition without condition element (or a link definition without name) next to a timer was no longer reported, although the engines refuse the file (XSD validation runs before they pick the acting definition) | fixed: the schema rules run on every event definition of the file, ignored ones and non-executable processes included (`checkSchemaEventDefinitions`); only the semantic rules follow the acting definition. New schema rule: a timer definition with two of timeDate / timeCycle / timeDuration (`W_C7_DEPLOY_EVENT_DEFINITION`). The hint names both ways out: `set trigger=` (drops the ignored one) or fix it and keep only it | -| V2 | regression: the `W_C7_MISPLACED_EXTENSION` / `W_C7_UNKNOWN_ELEMENT` hint for content of an event definition or loop with an id of its own said `ext remove Def_T definition.0` (`E_WRONG_KIND`) | fixed: hints address nested content through the flow node (`ext remove Event_Wait definition.0`); the duplicate-merge hint of a nested element gets its `loop.` / `definition.` prefix; content the flow node reads is moved there (`..., then add it to `) | -| V3 | false deploy finding: a transaction without start event deploys on all three engines | fixed: runtime `W_C7_TRANSACTION_NO_START` (every instance that reaches it fails: "No initial activity found for subprocess"); an empty embedded sub-process or empty process is now `W_C7_DEPLOY_START_EVENT` (the engines refuse both: "must define a startEvent element"; it replaces `W_NO_START` / `W_EMPTY_SUBPROCESS` for that scope); new `W_C7_DEPLOY_AD_HOC_SUBPROCESS` for an ad-hoc sub-process with a sequence flow or boundary event (or, Camunda 7 / CIB seven, a multi-instance loop), and the rest of every ad-hoc sub-process is skipped like the engines skip it (its loop rules hold) | -| V4 | following the `W_C7_DEPLOY_START_EVENT` hint could double a branch (two flows from the kept start into one node: it ran twice) | fixed: a flow into a node the kept start already reaches goes with the removed start; a branch of its own is moved (named flow ids, no placeholder) only when it does not run into the kept start's path; a process start can keep its way in with another trigger; an event sub-process keeps the trigger in an event sub-process of its own | - -**Engine evidence** (Camunda 7.24.0, CIB seven 2.2.0, Operaton 2.1.5; all -models synthetic, every deployment deleted): - -- V1: the four verifier repros and a link repro are refused by all three - (cvc-complex-type.2.4.b / cvc-complex-type.4) and now flagged; so are the - same problems in a non-executable process (also in a file with only a - non-executable process) and a timer with timeDate + timeDuration, also as - the ignored definition of an event sub-process start - (cvc-complex-type.2.4.d). Both ways out of every hint deploy (12 files). -- V3, what the engines do: an embedded sub-process without start event is - refused, empty or not, connected or not; a process without start event is - refused, also an empty one; a transaction without start event (with a - task, empty, or with only an event sub-process) deploys, and starting the - process fails when it reaches the transaction (an unconnected one never - runs); an ad-hoc sub-process with an incoming or outgoing flow is refused - ("Invalid destination / source 'X' of sequence flow"), one with a boundary - event too ("Invalid reference in boundary event"); an unconnected ad-hoc - sub-process deploys (empty, with tasks, with flows and two start events - inside, with a service task without implementation, a script task without - script or a sub-process without start inside, or with broken settings on - it such as two failedJobRetryTimeCycle with asyncBefore, a jobPriority - "abc", a listener without implementation), also inside a sub-process; its - multi-instance loop is parsed: an invalid loop is refused by all three, any - loop by Camunda 7 and CIB seven (ENGINE-01009, "activity" is null), a valid - one deploys on Operaton. 39 probes, the profile agrees on all; the hints - (add a start event, remove, retype the ad-hoc sub-process, drop its loop) - lead to files that deploy and run. -- V4: following the hint on 8 two-start models (same target, reachable - target, separate branches, branches that meet, mixed flows, sub-process, - event sub-process) gives files that deploy, and each node runs once (task - lists `[U]`, `[U, V]`, `[X]`); the event sub-process's other trigger is - kept in an event sub-process of its own. -- The suites with live engines (`c7-profile`, `c7-followups-profile`, - `c7-followups-integration`, `c7-followups-verify`): 784 / 784. -- Real files: on the 218 originals 0 deploy false positives (14 of 15 refused - files found, the miss is the XSD element-order error), findings identical - to the previous round; the 12 TTL-fixed variants agree 12 / 12; on the - 5,860 derived files 0 false positives and 0 misses (5,524 deploy, 336 - refused), findings identical to the previous round. - -**Still open after the second round** (reported by the verifier, not part -of this round): message start events of two executable pools that share a -message name (all engines refuse, ENGINE-01011); a `camunda:inputOutput` next -to an `operaton:inputOutput` (Operaton reads only its own, Camunda 7 and CIB -seven only theirs); the `W_PROPERTY_DROPPED` restore hint after replacing an -input parameter's script with `--body` keeps both; `EPIPE` when stdout closes -early (fixed with #33: the CLI ends without a trace and keeps its exit code). - -## Step 2 (2026-10-09): design-iq's editing engine - -Goal: bpmn-cli as the editing engine inside Miragon's design-iq (PR #218 of -Miragon/design-iq, its ADR 0008): a core that runs in the browser, writes -that change only what the edit changed, edits that follow the file's -conventions, and the host's save gate inside the write transaction. Four -packages were built in parallel and merged on one branch (see -[Integration and re-measurement](#integration-and-re-measurement)): 13 bugs -of the table fixed (#27, #29, #30, #31, #32, #33, #43, #44, #45, #46, #47, -#55, #56), 2 partly (#48, #49). - -### Isomorphic core - -The engine for an embedding host (design-iq, ADR 0008 decision 1 and its -dependency rule `bpmn-edit-stays-isomorphic`): #33 fixed, #49 partly. - -- **Browser-safe core.** Nothing reachable from the package's main entry - imports a Node builtin or reads `process`: file I/O (read, atomic write, - backup, the existing-file guard of `new`) moved to `src/node/files.ts` - (`@miragon/bpmn-cli/node`), `mutateDoc` never writes, the layout debug lines - go to a hook (`setLayoutDebug`, a `debug` option; the CLI maps - `BPMN_LAYOUT_DEBUG` to it), the Camunda 7 descriptor is inlined - (`src/platform/camunda-descriptor.ts`, generated, a test keeps it equal to - the package) instead of read with `createRequire`, and bpmn-auto-layout is - imported on first use (`engine: 'auto'` only). -- **In-memory API** (`src/api.ts`): `applyToXml`, `newXml`, `layoutXml`, - `validateXml`, `viewXml` / `showXml`, `metricsXml`, `findXml`, - `extensionsXml`: strings in, data out, the CLI's code path and guards - (lossy import, validation, dropped content, platform profile), ops checked - like `apply`; `{ xml, unchanged, result }` with the CLI's `--json` report. - The CLI's report and text rendering moved to `src/report.ts`, shared. -- **Package**: `exports` `.` (core) and `./node`, type shims shipped with - `dist/types` (a strict consumer compiled against the audit build failed - with TS7016 on every declaration naming a bpmn-moddle type), `moddle` a - direct dependency (its types are named), `sideEffects` only the CLI files. -- **Evidence**: `tools/iso/check.mjs` in the gate (esbuild browser bundle of - `dist/index.js`: no builtin, no Node global, no node layer; both entries and - their types resolve for a strict consumer); `test/isomorphic.test.ts` runs - the bundle in a vm context without Node globals and gets byte-identical - results to Node; `test/api.test.ts` compares every function with the CLI - command; `test/node-files.test.ts` the node layer. Bundle (minified / - gzip): the whole entry 648 / 200 KB, `applyToXml` alone 541 / 167 KB, plus - 82 KB bpmn-auto-layout on demand (the audit measured 472 KB for the whole - entry before the Camunda 7 work, with the layouter and with Node shims). - On the 394 files of the roundtrip corpus the browser bundle in a vm context - gave byte-identical results to Node in 2,331 / 2,331 calls (validate, show, - metrics, a rename, an insert and a colour op per file; 1,146 of 1,149 edits - succeeded, 3 regular refusals). - -### Roundtrip fidelity of writes - -Goal: bpmn-cli as the editing engine of a host that stores BPMN in git and -syncs a file live by replacing one text region per save (design-iq, PR #218 -of Miragon/design-iq): every byte changed outside the edit is churn and a -co-editing conflict. Fixed: #30, #31, #44, #45, #46, #47. - -- **Text-preserving output** (`src/preserve.ts`, reader `src/xmltext.ts`): - the original text, bpmn-moddle's serialisation of the model as read and - the one after the change are paired element by element (ids among - siblings, else name and position); an element whose serialisation did not - change keeps its original text, a changed one its start tag or attribute - order, new ones follow the file's indentation, line breaks and `/>` style; - CDATA stays CDATA, prolog and epilog stay. The result must read back with - bpmn-moddle as exactly the changed model, else the plain serialisation is - written with a note (no such fall-back in any measured run). -- **No-op writes** (#46): a result equal to the file is not written - (`written: false`, `unchanged: true`, text `unchanged: `). -- **Comments** (#31): kept everywhere the change did not remove an element; - a comment next to a removed element is reported (`n XML comment(s) - dropped`), never silently. -- **Derived lists** (#45, `src/mirror.ts`): `incoming` / `outgoing` are - completed in memory when a file is read and written the way the file keeps - them (entries for new and changed flows where the file lists entries). -- **DI ids of a full redraw** (#47): the redrawn DI elements get the ids - they had, new ones the file's id style. (The package did this after the - pipeline's redraw, `src/diagram/keep-ids.ts`; the integration keeps the - conventions package's fix of the same bug inside `layoutModel`, see - [Edits follow the file](#edits-follow-the-file).) - -Measured with the audit's roundtrip harness (same targets, adapted to the -new build) on the private corpus, aggregate numbers; PR #218 from the audit. -"Region" is the share of the file one region replace rewrites (common -prefix and suffix trimmed, as the host's `diffRegion`). - -| measure (real files the CLI accepts: 265) | before (0.2.0) | now | PR #218 | -| --- | --- | --- | --- | -| no-op byte-identical, layout auto | 40 (15 %) | 262 (98.9 %) | 169 (64 %) | -| no-op byte-identical, `--no-layout` | 42 (16 %) | 265 (100 %) | | -| rename, changed lines median / p90 / max (`--no-layout`) | 5 / 16 / 200 | 2 / 2 / 2 | 2 / 10 / - | -| rename, region median / p90 / runs >= 50 % (`--no-layout`) | 16.3 % / 56 % / 47 | 0.0 % / 0.0 % / 0 | 0 % / - / 18 | -| rename, region median / runs >= 50 % (layout auto) | 16.5 % / 50 | 0.0 % / 4 | | -| rename, files with changes outside the element and its DI (`--no-layout` / auto) | 0 / 2 (vs its own no-op) | 0 / 5 | 0 | -| insert after a task, changed lines median / p90 | 56 / 146 | 50 / 140 | 56 / 159 | -| insert, region median / p90 | 97.4 % / 99.4 % | 77.2 % / 89.8 % | 86 % | - -- The three no-ops that are not byte-identical in layout auto come from the - layout, not from the writer: it completes missing DI (three edges and a - data store shape in two files, the plane's `bpmnElement` in the third). - The 265 targets are all files with a diagram: a file without one (22 of - the 289 real files the CLI accepts) is drawn from scratch on any write in - layout auto, so a no-op is written there too (13 of 13 such files with a - named node to rename; see the verifier round). - The same files account for 3 of the 5 renames with collateral in auto - mode; the other 2 grow the renamed task (its name no longer fits) and - reroute one edge. -- Further edit types on all 396 files (real and scenarios), changed lines - median before -> now: remove with bridging 47 -> 42, retype 6 -> 4, a - vendor attribute 4 -> 2 (12 runs now unchanged: the value was already - set), a timer boundary event 13 -> 11, documentation 3 -> 1, `tidy` 2 -> 0 - (355 of 387 runs now unchanged), full redraw 114 -> 88 (p90 358 -> 252, - region 95 % -> 43 %: the semantic part and the DI start tags stay). -- Semantics: in all 4,475 runs of the twelve edit types, the result reads - (bpmn-moddle) exactly as the previous build's result, apart from the - incoming / outgoing entries and the DI ids a redraw keeps; no original - entry was lost and no entry was written that the previous build did not - write too. No fall-back to the plain serialisation and no dropped comment - in any run. The tools/scenarios layout regression (115 files, score 444) - and the Camunda 7 profile tests are unchanged. -- Cost: the comparison and the read-back check add 15–20 ms to a write of - the largest real files (90 KB), 3 ms to a 9 KB file. -- In-repository checks: `test/roundtrip.test.ts` (synthetic fixtures with - exact expected texts; every fixture and scenario: a no-op is unchanged, a - rename changes one line) and `tools/roundtrip.mjs` (`npm run roundtrip`, - the same measures on any corpus, optionally against a baseline build). On - the 152 public files: no-op byte-identical 144 (layout auto; the rest is - the layout drawing or completing DI) and 152 (`--no-layout`), before 113 - and 114. - -### Edits follow the file - -New ids, DI ids and design-iq stickies follow the file (#32, #43, #47, -#48 partly, #55, #56). - -- **Id style inference** (`src/idstyle.ts`, #48): prefixes per kind / family - / type name, bodies (pascal, camel, snake, Pascal_Snake, modeler hash, - numbered) and flow forms (modeler hash, numbered, `flow_aToB`, - `Flow__`, `*_to_*`) are learned from the file; a rule needs two - ids (one in a mostly prefixed file). README "Ids", `kinds --json` -> `ids`. -- **Collision-resistant ids** (#32): flows and unnamed elements get - `_<7 base-36 chars>` hashed from stable inputs instead of - `_`. -- **German names** (#55, #56): ae / oe / ue / ss; E_NOT_FOUND suggests the - other spellings. -- **Full redraw keeps DI ids** (#47): shapes, edges, planes and diagrams keep - their ids; new DI follows the file's DI id style. -- **Stickies** (#43): `bpmiq:sticky` follows its nearest flow node. - -Measured on the private corpus of the roundtrip stream (291 real files, 105 -scenarios; aggregate numbers only), with a classifier independent of the -implementation, inserting a named user task (and, separately, an unnamed -parallel gateway) after a task: - -| measure | before | after | -| --- | --- | --- | -| new task id matches the file's prefix and body style | 4 / 270 (1 %) | 264 / 270 (98 %) | -| prefix only / body only | 6 % / 29 % | 99 % / 99 % | -| new gateway prefix matches | 40 % | 98 % | -| new flow id has the file's flow form (226 files with one) | 5 / 226 (2 %) | 226 / 226 (100 %) | -| new DI id has the file's DI style | 99 % | 99 % | -| everything known matches | 1 / 129 | 125 / 129 (97 %) | -| DI ids kept by `bpmn layout` | 310 / 8,345 (4 %), 1 / 267 files | 8,345 / 8,345, 267 / 267 files | -| two edits at two places of one file share a new id | 225 / 225 file pairs | 6 / 225 (all in files that number their flows) | - -On the 105 scenarios (bpmn-cli's own drawings, numbered flows) every -measure stays at 100 %. No real file has stickies; they are covered by -synthetic tests (`test/conventions-stickies.test.ts`). - -Still open: a spliced or bridged flow keeps its id even where the id names -its old ends (`flow_aToB` now from a new node; ids of existing elements -never change); a sticky follows the shift of its node's centre only (a -growing node does not push it aside). - -### Validation hook and design profile - -The integration analysis for Miragon's design-iq (PR #218, requirement R13) -found that bpmn-cli validated with its own severities: it wrote models the -host's save gate (`@bpmiq/validator`, HTTP 422 on any ERROR) refuses (a -second start event, a node in no lane, unreachable nodes, dead ends), and a -host could not plug its validator into the transaction. It also could not -set design-iq's decision link (#29). - -| # | finding | status | -| --- | --- | --- | -| 27 | Validation is laxer than the host's save gate | fixed: a validator hook in the pipeline (`MutationOptions.validators`, `checkFile(file, { validators })`, `src/validators.ts`) runs on the document before the ops and on the candidate XML after the layout; errors the change introduces block the write (`E_VALIDATION`), errors the file had are `W_PREEXISTING_ERROR`, warnings the change introduced are reported, each finding with its validator. A built-in design profile (`src/platform/design.ts`, `--profile design`, auto for the models of a design-iq content repository with a `bpmiq.yml`) mirrors the save gate's rules | -| 29 | The host's unprefixed `calledDecision` link cannot be set | fixed: `set calledDecision=` writes the link in the file's spelling (design model: `calledDecision`, Camunda 7: `camunda:decisionRef`, Camunda 8: `zeebe:calledDecision`) and removes the others; `show` / `find` read every spelling; the `W_C7_DEPLOY_SCHEMA` hint for an unprefixed one converts it; a design model's `calledDecision` is no import warning | - -**Decision: when the design profile runs.** By default (`--profile auto`) -for the models of a design-iq content repository only (a `bpmiq.yml` in the -file's directory or above names a models folder that contains the file); -`--profile design` anywhere, `--profile none` never. Not for every file -without an engine namespace (PR #218's "design" platform, bpmn-cli's `none`; -the two detectors agree on all 394 real files): the gate refuses every -intermediate state of a model built command by command, which is right for -design-iq and wrong for plain BPMN editing. - -**Evidence** (outside the repository; private files used locally only, -counts only): - -- Rules: 28 synthetic models in `test/design-profile.test.ts` with - design-iq's verdict; run against design-iq's own validator - (`BPMN_DESIGN_IQ_VALIDATOR`) all 28 verdicts and error counts agree. -- The roundtrip stream's corpus, 394 files (289 real models from private - sources, Camunda 7 / 8, CIB seven, design-iq samples and plain BPMN, plus - the 105 scenarios): design-iq's validator crashes on 1 (a DOCTYPE with an - external entity; bpmn-cli reads it, no design error). On the other - 393 the design profile's verdict equals design-iq's: 65 refused, 328 - accepted, 0 disagreements; all 540 findings match by rule and element - (404 missing DI, 43 dead ends, 39 nodes in no lane, 27 unreachable, 24 - start-event counts, 3 files without process). -- bpmn-cli's structural validation refuses 2 of the 328 files design-iq - accepts: a plain start event in an event sub-process - (`E_EVENT_SUBPROCESS_PLAIN_START`) and an incoming / outgoing entry that - contradicts its flow (`E_FLOW_LINKS`). Both stay editable (pre-existing - errors never block). -- The gate on edits: 2,231 probe edits on the real files (insert a task into - a flow, rename a task, add a loose task, a boundary event without and with - its path, a second start event with a path), each run through - `mutateDoc(..., { profile: 'design' })` and the result (forced where - refused) checked by design-iq's validator: every edit the gate let through - (1,117) added no design-iq error, every edit it refused (1,114) would have - added one. The first measurement found 18 writes that added a third start - event to a process that already had two (the process-level finding counted - as pre-existing); since then several start events are reported on each - start event, so adding one is always new. -- 7 of the refusals are a new node next to a node that is in no lane (an - insert after a laneless receive task, an end event after a boundary event - whose host is in no lane): the new node inherits no lane. design-iq refuses - those saves too; assigning a lane in the same command (`--lane`) passes. - -**Open after step 2:** bpmn-cli's lane rules still differ from bpmn-js / -design-iq for nested lanes (`set lane=` puts a node into the child lane -only, bpmn-js also lists it in the parent lanes, which `E_LANE_CONFLICT` -reports; design-iq reads the top-level lanes only); design-iq's degree rules -make compensation handlers, link events and ad-hoc sub-process content -unsavable there (reported with that explanation); the design profile parses -the candidate XML once more per write: a rename on the 377 corpus models -with a task takes 1.9 ms more with the profile (median; p90 3.8 ms, max -9.5 ms; 7.2 ms without). - -### Integration and re-measurement - -The four packages were merged on `step2/integration` (core, roundtrip, -conventions, validation, in that order) and made to work as one pipeline: - -- **One write pipeline, in memory.** `mutateDoc` runs the guards, the ops, - the structural validation, the layout, the format ops, the stickies, the - text-preserving step and then the validators, and reports `unchanged`. - The node layer (`src/node/files.ts`) reads, writes atomically and skips - writing an unchanged result over its own file. So the in-memory API - (`applyToXml` and the others) keeps the input's text, its `unchanged` is - meaningful (the input string itself on a no-op), new ids follow the - file's id style, and it takes `profile`, `contentRepo`, `validators` and - `file`. -- **Validators see exactly what is written**: the final text after the - text-preserving step; their baseline run sees the document's text as read. -- **Content repository**: the `bpmiq.yml` lookup and the listing of the - repository's models read the file system, so they moved to the node layer - (`src/node/repo.ts`); the core decides from `contentRepo` (a host passes - it, the node layer fills it for the file it writes or checks, the `--out` - target included). -- **#47 was fixed twice** (the roundtrip package after the pipeline's full - redraw, `src/diagram/keep-ids.ts`; the conventions package inside - `layoutModel`). The conventions version is kept (both engines, `bpmn - layout` and auto redraws, plane and diagram ids by root), the other is - dropped; the roundtrip package's tests pass on it. -- **One report**: `unchanged`, the validator lines and the `forced` lines of - a mutation and the `profile` / `validators` of `validate --json` come from - `src/report.ts`, so `applyToXml` and `validateXml` report them like the CLI. -- **Tests**: expectations that assumed another package's old behaviour were - adapted (numbered or `Activity_` ids on `Task_`-styled fixtures and in the - README's library example; incoming / outgoing lists written into list-less - fixtures, where an element without children now stays self-closing). - New: `test/step2-integration.test.ts` (the packages together) and the - design profile and host validators in the browser bundle - (`test/isomorphic.test.ts`). - -Re-measured on the integrated build (private corpora used locally, counts -only; "package" = the package's own build and numbers): - -| check | result | -| --- | --- | -| `npm run gate` | 45 test files, 1,292 tests + 1 opt-in (972 before step 2); isomorphism check; layout regression 115 files, score 444 <= budget 444; fuzz 12 walks x 15 steps: 0 errors, 2 warnings (an unreported reroute after `split`, #61; a `route` through a shape), the same with the roundtrip and the conventions builds on this corpus | -| roundtrip, 265 real files, the audit's targets | no-op byte-identical 262 (auto) and 265 (`--no-layout`); rename: 2 changed lines (median and p90), region median 0 %; insert: region median 77.2 %, p90 89.8 % (identical to the package) | -| roundtrip, 12 edit types on 396 files | every output byte-identical to the roundtrip package's, except where the id style names a new element differently (265 of 371 inserts, 205 of 369 boundary paths, new DI ids of 2 of 394 full redraws); full redraw changes median 88 lines (0.2.0: 114) | -| in-memory no-op (`applyToXml`, rename to the current name) | `unchanged` for 260 of 263 real files in layout auto, 263 of 263 with `layout: false` | -| id style, 291 real files | new task id in the file's style 264 / 270, gateway prefix 188 / 191, flow form 226 / 226, all known 125 / 129; a full redraw keeps 8,345 / 8,345 DI ids; 6 of 225 file pairs share a new id (identical to the package) | -| design profile vs design-iq's validator, 394 files | 65 refused and 328 accepted by both, 0 disagreements (design-iq crashes on 1); 540 / 540 findings matched; 2,231 probe edits: the 1,117 written add no design-iq error, each of the 1,114 refused would add one (identical to the package) | -| browser bundle vs Node, 394 files | 3,491 / 3,491 calls byte-identical (validate, validate with the design profile, show, metrics; per file a rename, an insert, an insert with the design profile, a no-op and a colour op) | -| bundle, minified / gzip | whole entry 707 / 221 KB, `applyToXml` alone 586 / 183 KB, plus 82 KB bpmn-auto-layout on demand (core package alone: 648 / 200 and 541 / 167 KB) | -| Camunda 7 battery, 218 real files, 8 edit types (rename, insert, remove, lane, retype, boundary path, colour, full redraw), deployed before and after on Camunda 7.24.0, CIB seven 2.2.0, Operaton 2.1.5 | every edit written; 0 regressions (no file that deployed before an edit fails after it); 0 unexpected camunda changes; 0 new typed-moddle warnings. Against 0.2.0: 3,966 deploy verdicts compared, 16 differ, all of one file (see below) | -| exhaustive sweep, 4,100 edits (remove, retype, insert, boundary path on every eligible element) | 4,056 written, 44 refused exactly as by 0.2.0 (30 `E_INVALID_BRIDGE`, 7 `E_INVALID_HOST`, 7 `E_VALIDATION`); 3,873 Camunda 7 and 2,658 CIB seven deployments, 0 regressions, every verdict as with 0.2.0; 0 unexpected camunda changes | -| chained session (all eight edits on one copy), 218 files | 0 failed steps, 0 unexpected camunda changes, 0 regressions (Camunda 7 206 / 206, CIB seven 136 / 136, Operaton 206 / 206) | -| follow-up battery (49 deployable files: rename, error rename, insert, remove, retype) | 202 / 202 edits written and deployed on all three engines; the same 7 profile findings as before | -| execution scenarios (models built by the CLI, run over REST) | 65 / 65 checks on all three engines, the README's worked example included (the script now names the one flow id it uses: generated flow ids are hashes) | -| live-engine tests (`BPMN_C7_ENGINES`, the four profile test files) | 804 / 804 | - -One behaviour change shows in the Camunda 7 battery: a write keeps the -order of the file's elements, so it no longer repairs a file whose element -order the BPMN XSD refuses. 0.2.0 rewrote every file in bpmn-moddle's order, -which repaired 1 of the 218 real files as a side effect of any edit (a data -object after the flow elements: refused by Camunda 7 and Operaton before the -edit, deployable after it). Now that file stays refused before and after an -edit, as it was; no file that deployed before an edit fails after it. - -### Verifier round (2026-10-09) - -An independent verifier re-checked the integrated build (its own scripts: -browser parity, roundtrip and semantics, id style, design profile against -design-iq's validator, Camunda 7 deploys, the node layer) and confirmed the -step's claims, with six findings. All are fixed or documented; each fix has -a regression test that fails on the build before it. - -| finding | severity | status | -| --- | --- | --- | -| The design profile let through writes design-iq's save gate refuses: design-iq's namespace check runs two regular expressions on the raw text (``; whitespace + `p:name="`), so documentation, annotation text, CDATA, comments and attribute values count; bpmn-cli read bpmn-moddle's import warnings (`set A doc='Set key app:mode="prod"'` was written, design-iq refused it) | medium | fixed: `E_DESIGN_NAMESPACE` runs design-iq's check on the text it reads (as read before a change, the candidate after it); the message says where the match is and how to reword it (`undeclaredPrefixes`; `test/design-profile.test.ts`) | -| DI drawn for an id-less collaboration, participant, process or message flow got `bpmnElement="undefined"` and repeated `_undefined` DI ids (0.2.0 too) | medium | fixed: a full redraw first gives every element it draws that has no id one in the file's id style, through the document's registry, reported as changed elements and a note (`src/diagram/drawn-ids.ts`; `test/drawn-ids.test.ts`). The incremental layout never draws an id-less element. On the 8 real files with id-less elements, `bpmn layout` re-imports without new warnings (before 1 to 14 warnings and up to 6 duplicate ids), and `tidy` works on the 7 of them without a diagram (it refused them with `E_NO_DIAGRAM`) | -| The id style was not learned from ids without prefix (`checkOrder`) or from `Flow__To` flows | low | fixed: an unprefixed id that spells its element's name is evidence for "no prefix" (a named element gets a bare camelCase / PascalCase id, an unnamed one keeps a prefix; a kind without own evidence goes bare when its family has no prefixed id and the file's flow nodes mostly have none); flows numbered without separator (`flow5`); a new flow form `scopedTo` (`test/conventions-ids.test.ts`). 15 of the 273 real files with an insert target get other ids, all in these forms | -| A non-UTF-8 encoding declaration was kept while the new text was written as UTF-8 | low | fixed, the safe option: every write is UTF-8, and a changed result declares UTF-8 (with a note; `src/encoding.ts`). Re-encoding in the declared encoding was rejected: it cannot write every character (no character references in names or comments). The node layer reads a file in the encoding it declares, so the characters of an ISO-8859-1 file survive (it decoded every file as UTF-8 before); a no-op is not written (`test/node-files.test.ts`). No real file declares another encoding | -| "3 of 265 written no-ops" left out the files without a diagram | low | corrected: 3 of the audit's 265 targets (all with a diagram; the layout completes missing DI), and every file without a diagram (22 of the 289 real files the CLI accepts; a rename to the current name is written for each of the 13 of them with a named node). 16 of 278 files with a rename target | -| 8 synthetic edge cases where the profile and design-iq disagreed | low | 7 aligned: the three raw-text namespace cases; flows counted per id like design-iq (flows without id or sharing an id count once, nodes without id not at all); the pools and message flows of every collaboration; an attribute written twice is not well-formed (`E_DESIGN_XML`, nothing else checked, as in design-iq); an id reference with whitespace around it resolves by the trimmed id at import (the XSD collapses it; it was a lossy import that refused every write). Documented: in a process with two lane sets design-iq checks no lanes (its parser makes the repeated element a list), the profile checks every lane set (stricter) | - -Also: `camunda-bpmn-moddle` is a development dependency (only the -descriptor generator and its test read it; the core uses the inlined copy). -The packed package, installed into an empty project without it, runs `new ---target camunda7`, `set` / `ext add` with camunda content, `validate` -(platform c7, the descriptor rules) and both library entries. - -Re-measured on the final build (private corpora used locally, counts only): - -| check | result | -| --- | --- | -| `npm run gate` | 46 test files, 1,318 tests + 1 opt-in (1,292 at the integration); isomorphism check; layout regression 115 files, score 444 <= budget 444; fuzz 12 walks x 15 steps: 0 errors, the same 2 warnings (#61; a `route` through a shape); `npm run typecheck` clean. Bundle (minified / gzip): whole entry 716 / 224 KB, `applyToXml` alone 594 / 186 KB (+8 / +3 KB: the drawn-ids step, the encoding step and the namespace check's locator) | -| roundtrip, 265 real files, the audit's targets | unchanged: no-op byte-identical 262 (auto) and 265 (`--no-layout`); rename: 2 changed lines (median and p90), region median 0 %; insert: region median 77.2 %, p90 89.8 % | -| roundtrip, 12 edit types on 396 files, against the integrated build | 33 of 4,486 outputs differ, all from these fixes: 16 inserts and 1 boundary path get ids in the file's style (bare ids, flows numbered without separator, `Flow__To`), and `layout` and `tidy` on the 8 real files with id-less elements give them ids first (7 `tidy` runs that were refused with `E_NO_DIAGRAM`, because the drawing pointed at `undefined`, now succeed). Every changed output re-imports without a new warning | -| in-memory no-op (`applyToXml`) | unchanged: `unchanged` for 260 of 263 real files in layout auto, 263 of 263 with `layout: false` | -| id style, 291 real files | unchanged: new task id in the file's style 264 / 270, gateway prefix 188 / 191, flow form 226 / 226, all known 125 / 129; 8,345 / 8,345 DI ids kept. Two edits on two branches share a new id in 9 of 225 file pairs (6 before): the three new ones number their flows without separator (`f5`), which the id style now continues, as it does `Flow_12` | -| design profile vs design-iq's validator, 394 files | unchanged: 65 refused and 328 accepted by both, 0 disagreements; 540 / 540 findings matched; of 2,231 probe edits the 1,117 written add no design-iq error and the 1,114 refused would add one. The verifier's 30 synthetic edge cases: 29 agree, 1 documented difference (two lane sets). The opt-in test (`BPMN_DESIGN_IQ_VALIDATOR`): all 36 synthetic cases (7 new) agree with design-iq's validator in verdict and error count | -| browser bundle vs Node, 394 files | 3,491 / 3,491 calls byte-identical | -| live-engine tests (`BPMN_C7_ENGINES`, Camunda 7.24.0, CIB seven 2.2.0, Operaton 2.1.5) | 804 / 804 | - -## Step 3 (2026-10-10): views and output - -Token-efficient reading and output for agents on large models (the -dogfooding streams' friction: `show` of a large model is 15–40 KB, a write -repeated every pre-existing warning, the guide was 30–43 KB). Elements are -addressed by id only (a name may repeat). Fixed: #21, #22, #58, #60. - -- **`show --around [--depth n] [--inner]`** (`src/context.ts - aroundView`): the neighbourhood along sequence flows in both directions - (boundary event and host, link pairs; a sub-process's first and last node - lead out to it, `--inner` also into it), per node its lane, vendor values - and a compact implementation summary (`io: in a; out b`, - `zeebe:taskDefinition type=...`), incoming flows from outside the window - (`<-`), the counts of what was left out, the window's message flows, - annotations and data. -- **`show --context`** (`elementContext`): pool > process > - sub-processes, the effective lane (inherited through sub-processes, nested - lanes), from / to with names, own boundary events, the error / escalation - boundary events of the sub-processes around it, the event sub-processes of - every scope around it, annotations, message flows with the partner and its - pool, data, implementation. -- **`show`**: `lane=` per node (the lanes section is a tree of names), - message flows with endpoint names, collaboration annotations (#21); `show - `: message flows, annotations, data (#22). -- **Mutation output** (`src/report.ts`, `pipeline.ts validationDelta`): the - warnings a change added, the findings it resolved (lint, errors, platform, - validators; identity through renames) and one line counting the file's - own; three or more of one code as one line; `--summary`; JSON - `warnings: {added, resolved, preexistingCount}` (#60). -- **Docs cost**: `guide --short` (3.8 KB; the full guide is 43 KB), `guide - ` (15 topics, including a new CAMUNDA 8 section), `kinds --section`, - compact JSON for every `--json` (`--pretty` indents). -- **stdin / stdout** (#58): `-` as the file for every command, `-o -`. - -Measured on the audit corpus (383 unique files; 278 real ones from private -corpora, the rest the public scenarios), centred on the middle activity of -each model, against the full `show` and PR #218's `outline --around` (depth 2 -both): - -| bytes | median | p90 | max | -| --- | --- | --- | --- | -| `show` (whole model), real files | 2,337 | 7,641 | 18,769 | -| `show --around` (depth 2), real files | 1,203 | 2,058 | 2,972 | -| PR #218 `outline --around`, real files | 1,447 | 2,431 | 3,707 | -| `show --around --depth 1`, real files | 894 | 1,332 | 1,812 | -| `show --context`, real files | 514 | 1,079 | 2,395 | -| `show --around` per shown node, real files | 236 | 358 | 495 | - -`--around` / `show`: 0.58 in the median (p90 1.0: small models fit into -the window); on the 30 real models with 30 flow nodes or more 1.8 KB against -10.6 KB (0.17, p90 0.24). `--around` / PR #218: 0.80 in the median (p90 -0.99; on the large models 0.88). 0 failures on the 383 files. A budget test -(`test/views.test.ts`) keeps the window under 320 bytes per node and a tenth -of `show` on a synthetic 120-task model, and its size independent of the -model's. - -Mutation output, a rename (`--no-layout`) on each of the 278 real files, -the build before step 3 against this one: on the 97 files with warnings of -their own the text result shrinks from 523 to 245 bytes in the median (p90 -1,304 -> 319, max 4,304 -> 1,389), `--summary` is 122 bytes, and `--json` -from 2,597 (indented, with every warning) to 1,276 bytes; over all 278 -files the text results sum to 61 KB instead of 108 KB and the JSON results -to 379 KB instead of 698 KB. - -## Step 3 (2026-10-10): speaking ids and batches - -The user decided: elements are addressed by id only (names may repeat, no -name-based addressing), and new ids must speak. Package "ids & batches" -(branch `step3/ids`): - -- **Speaking ids** (`src/idstyle.ts`): a named element's body is its name, - an unnamed one's a word for its kind and its place (`Gateway_AfterCheckInvoice`, - `Event_TimerOnReview`, `Event_StartInPayment`), a flow's its ends - (`Flow_CheckInvoiceToBookInvoice`; an end is named by the speaking part of - its id, by its name when the id says too little), the rest what it - belongs to (`Collaboration_OrderHandling`, `LaneSet_OrderHandling`, - `Association_To`). Prefixes, case and the file's own flow forms - stay as step 2 learned them; hashed and numbered files lend their prefix - and separator (`Activity_CheckInvoice` next to `Activity_0k3x9qa`, - `SF_ThirdToFinish`, `flowCheckOrderToShip`). A taken id gets `_2` and - `W_ID_SUFFIXED` (now for every generated id). A flow whose id names its - ends is renamed after its new ends when a splice, a bridge, a move or - `set target=` changes them (DI edge id along; #48). -- **Batch aliases** (`src/ops/OpAliases.ts`): `"as": "$name"` (`flowAs`, - `joinAs`) names what an op creates; later ops use the alias in every id - field; checked before anything runs (`E_UNKNOWN_ALIAS`, - `E_DUPLICATE_ALIAS`); `aliases` in the result. -- **`E_NOT_FOUND`** ranks its candidates: a flow renamed earlier in the - batch, case / umlaut / prefix variants, containment, typos, names; the ids - the batch created first, and listed in the hint. -- **`remove --with-branch` / `--bridge-all`** (#53 partly): - `E_AMBIGUOUS_BRANCH`, `E_AMBIGUOUS_BRIDGE` where it is not clear what goes. -- **#14**: a node added into a flow between two lanes takes the lane of the - row it is drawn on; `W_LANE_INHERITED`. - -Evidence (291 real files of the private corpus, counts only; probes are dry -runs of a named task, an unnamed gateway and an unnamed boundary event at -the first task with one outgoing flow, `tools/speaking-ids.mjs`): - -| measure | 0.3.0 (step 2) | step 3 | -| --- | --- | --- | -| new ids that speak (no hash, no number) | 645 / 1,365 (47 %) | 1,745 / 1,745 (100 %) | -| new ids with the prefix step 2 learned from the file | (reference) | 1,628 / 1,630 (the two others: `F_To` in files of glued `F12` flows) | -| flows renamed because their ids named their old ends | 0 | 380 (in files whose flows name their ends) | -| two edits, different names at two places, share a new id | 9 / 225 file pairs | 0 / 225 | -| two edits, unnamed gateway + boundary event at two places | 9 / 225 | 3 / 225 (the two anchors have the same name) | -| two edits, the same name at two places | 219 / 225 | 225 / 225 (an id comes from the name: a merge needs one of them renamed) | -| an insert (`tools/roundtrip.mjs`): changed lines, median / region | 66 / 84.4 % | 70 / 85.9 % (the renamed flow) | - -Gate: 1,353 tests + 1 opt-in, layout regression 444 (budget 444), fuzz -12 x 15 0 errors; a longer campaign (80 walks x 25 steps, seed 7) gives -the same refusals and hard defects as 0.3.0. The README's Camunda 7 -example, rebuilt with speaking ids, deploys on Camunda 7.24.0, CIB seven -2.2.0 and Operaton 2.1.5. - -## Step 3 (2026-10-10): layout ergonomics - -The layout package of step 3: formatting a kept drawing without XML got the -operations the dogfooding agents missed, and the medium layout bugs of this -area were fixed where that was safe (table above: #12, #39, #41, #42, #61, -#62, #69, #71, #72, #73, #77 fixed, #40 and #74 partly). Elements are -addressed by id only (no name-based addressing; names can repeat). - -- **`compact [...]`** (`src/diagram/compact.ts`): - closes empty columns and rows and shrinks expanded sub-processes (deepest - first, everything outside kept), the columns of each pool (all lanes at - once), the rows of each leaf lane (the pool and the lanes below follow), - the rows between pools; right-aligned pools stay aligned. Every strip goes - through a guard (`src/diagram/ops.ts guard`): after it closed, broken - connections are routed again and the drawing is measured; a strip that adds - a hard problem or raises the score is undone and reported in a note. - Message flows do not hold a strip open: their bends and labels inside it - are scaled into the room that stays. Negative `space --by -column|-row|-` - closes empty space at one place (as far as it is empty; E_NO_ROOM when it - would add a problem). -- **Pool order** `order ` (ops JSON - `pools`): `collaboration.participants` and the pool bands (black boxes - included) restacked in the slots of the old order; message flows routed - again; E_NOT_PARTICIPANT. -- **Selectors** of place / align / color / tidy (`src/diagram/select.ts`): - `--path [--via ]` (shortest sequence-flow path, - default flows first), `--kind `, `--branch ` (what only that - branch reaches up to the join, following forward-drawn flows); E_NO_MATCH. -- **`show --layout`**: every node with its column across the diagram (c0..cN), - the wide empty gaps between columns, ` … ` where a wide gap splits a row. -- **Metrics**: soft kinds (weight < 6, never hard for the bench and the - fuzzer) `backwardFlow`, `segmentOverlap`, `labelOutsideFrame`, - `messageLabelFar` (`QUALITY_KEYS`). On the private real corpus (289 files) - they report 3 backward flows, 105 merging final segments in 43 files, 2 - labels outside their frame and no far message label; the clean engine's - own drawings of the 115 scenarios show merging segments in 37 (loop returns - entering one side of their target), nothing else. Later, by the - maintainer's decision, the common run of a join's entries into a gateway's - bottom / top vertex is not counted any more (bpmn-js draws joins so; a - merge into a side vertex still is). -- **Fuzzer**: generators for compact, closing space, pool order and the - selectors; the move oracle accepts a NaN coordinate that stays NaN. - -Measured on copies of the private corpora (outside the repository, counts -only), `compact` with layout off (in memory, `applyToXml`): - -| corpus | files | shrunk | area after / before (median, p10, all files) | hard problems added | score raised | time (median / max) | -| --- | --- | --- | --- | --- | --- | --- | -| modeler-drawn real models (hand) | 60 | 38 | 0.968, 0.826, 0.912 | 0 | 0 | 16 / 84 ms | -| broader real corpus | 265 | 121 | 1.000, 0.844, 0.912 | 0 | 0 | 8 / 64 ms | -| models edited in the dogfooding sessions | 53 | 48 | 0.918, 0.706, 0.856 | 0 | 0 | 31 / 102 ms | - -The guard left strips open in 3 files (two pools side by side would overlap, -or a rerouted connection would cross another). Renders of before / after -(the 2107 px high pool of a dogfooding session: 1530 px, score unchanged; -the largest reductions of the hand corpus, 47 % and 25 %) show the order and -the relative positions kept and the message flows unchanged in shape. - -Fuzzing (same seeds, private hand corpus, scenarios and fixtures): 120 walks -x 25 format steps and 100 walks x 30 mixed steps: 0 errors; the hard -warnings left (`place` / `align` through a shape, `add` of a boundary event -intruding into a frame) are added identically by the build before step 3; -`unreportedReroute` went from 20-52 warnings per campaign to 0 (#61). A -semantic-only campaign gives the same hard counts as the build before (2 / -2). Gate: 1357 tests (1318 before), layout regression 444 (unchanged, the full engine is -untouched), fuzz 0 errors and 0 warnings (before: 2). - -Still open in this area: #13, #15, #16, #19, #25, #63-#68, #70, #76 (#14 is fixed by the ids package); -compaction closes strips only (rows / columns empty across a whole pool or -lane band), so content spread over many rows shrinks less than a redraw; -pools side by side are not compacted against each other; a full redraw -orders pools by its own rule (the manual order is kept only by the format -phase of the same write). - -## Step 3 (2026-10-10): Camunda 8 profile - -The Camunda 7 work had a profile, nested keys and engine evidence; Camunda 8 -had detection only (`validate` said "no Camunda 8 engine rules yet", #28). -This step mirrors the Camunda 7 work for Camunda 8 (Zeebe) and checks every -rule against Camunda 8.9.22 (REST v2: `POST /v2/deployments`, process -instances, job activation, user tasks, message publication, incidents). The -real files are the 38 Camunda 8 files of the audit's corpus (four of them -the design-iq PR #218 fixtures), plus the 28 synthetic scenario files that -declare Camunda 8; only aggregate numbers appear here. - -| # | finding | status | -| --- | --- | --- | -| 28 | no Camunda 8 rules: camunda content in a Camunda 8 file, a service task without job type, a JUEL condition ... are not flagged | fixed: the Camunda 8 profile (`src/platform/c8.ts`, 34 codes: 22 `W_C8_DEPLOY_*` deploy, 11 runtime, 1 practice), detected like the Camunda 7 one; every write reports the findings it introduced | -| C8-1 | the Zeebe structure rules (single elements, containers, keys) followed the Modeler's convention, unchecked | fixed: `zeebe-bpmn-moddle` 2.0.0 inlined as data (`src/platform/zeebe.ts`, never registered); single elements engine-checked (Camunda 8.9 refuses a second `taskDefinition`, `ioMapping`, `taskHeaders`, `userTask`, `formDefinition`, ..., also `adHoc`, `conditionalFilter`, `publishMessage`, which `ext add` now merges too; two `jobPriorityDefinition` deploy) | -| C8-2 | `new --target camunda8` wrote no platform version; new user tasks were job worker user tasks (not in the v2 user task API) | fixed: `modeler:executionPlatformVersion="8.9.0"`; `add` / `retype` give a user task of a Camunda 8 file `zeebe:userTask` (`src/ops/platform.ts`) | -| C8-3 | event definitions are written without id; Camunda 8.9 refuses conditional, compensation and link catch definitions without one (an internal NullPointerException) | fixed: in a Camunda 8 file a new definition gets an id after its event (`ConditionalEventDefinition_StockReady`; since [round 1](#step-3-round-1-2026-10-10-the-packages-together) through the id style: the file's prefix and case, never a Modeler hash, `_2` with `W_ID_SUFFIXED`); loaded files: `W_C8_DEPLOY_EVENT_DEFINITION` with `set definition.id=...` | -| C8-4 | `ext add loop.zeebe:loopCharacteristics` failed without a loop (E_NO_NESTED_ELEMENT, the hint named `camunda:collection`) | fixed: it creates the parallel multi-instance loop it configures; the hint for other `loop.` types is platform-aware | -| C8-5 | `set zeebe:assignee=...` wrote an attribute Camunda 8 ignores | fixed: `E_WRONG_HOST`, the hint is the `ext add` of the zeebe element that reads it (by the descriptor; also `loop.zeebe:*`, `zeebe:correlationKey` -> the message's subscription) | -| C8-6 | `ext add` of a zeebe element on a host that does not read it, `retype` leaving zeebe content the new kind cannot use: silent | fixed: `W_MISPLACED_EXTENSION` / `W_PROPERTY_INAPPLICABLE` by the descriptor (in a Camunda 8 write replaced by the profile's `W_C8_MISPLACED_EXTENSION`, one report per item: `src/ops/utils/covers.ts`) | -| C8-7 | `show` printed only `ext: zeebe:...` for Camunda 8 nodes | fixed: `job=`, `calledElement=`, `script=`, `form=`, `assignee=` / `candidateGroups=`, `inputCollection=` ...; a message its `correlationKey` | -| C8-8 | the README's examples used the duration `PT2D` (Camunda 8 refuses it, ISO 8601 is `P2D`), a JUEL condition and a send task without job in a Camunda 8 file | fixed: the ops example and the worked session are valid Camunda 8 (the result deploys) | -| C8-9 | `set calledDecision=` in a Camunda 8 file reported the missing resultVariable twice once the profile ran | fixed: the operation warning gives way to the profile finding | - -**What Camunda 8.9 refuses at deploy** (the deploy rules; each a synthetic -model in `test/c8-profile.test.ts`): a service / send / script / business -rule task or message throw / end event without job type (a script task may -have `zeebe:script`, a business rule task `zeebe:calledDecision`; not both), -`zeebe:script` without expression or resultVariable, `zeebe:calledDecision` -without decisionId or resultVariable, a call activity without -`zeebe:calledElement processId` (the BPMN `calledElement` is not read), a -catching message (not a process start) whose message has no -`zeebe:subscription correlationKey`, a nameless message or signal, a FEEL -message / signal name on a start event, one message name caught twice in a -scope (boundary events with their receive task, event-based gateway -branches, event sub-process starts, process starts), error codes twice (or -two catch-alls) in a scope, a catching error code that is an expression, a -non-interrupting error event, an escalation boundary event on anything but a -sub-process or call activity, static values where FEEL is required -(conditions, script expressions, inputCollection / outputElement, -correlation keys) and FEEL syntax errors, timer values that are no ISO 8601 -duration / date-time with offset / repeating interval or 6-field cron -(`PT2D`, a date without offset, a 5-field cron), cycles on intermediate or -interrupting timers, a multi-instance without `zeebe:loopCharacteristics` or -inputCollection, outputCollection without outputElement, two of a zeebe -element read once, form definitions that are not exactly one of formId / -externalReference (Camunda user task) or formId / formKey (job worker user -task), task listeners on a job worker user task, priority outside 0..100, -due dates that are no date-time, listeners without type or event type, -start listeners on start / boundary events, end listeners on gateways, -listeners on sequence flows, io mappings without target (or source for -outputs), duplicate header keys, invalid binding types, linked resources -without resourceType, events without or with several event definitions, -triggered starts in embedded sub-processes, none / compensation starts in -event sub-processes, conditional / compensation / link-catch definitions -without id, compensation activityRefs outside the scope, compensation -boundary events without handler, event-based gateways with fewer than two -(listed) outgoing flows or a branch that is not a message / timer / signal / -conditional catch event, a process without start event or with two none -starts, sub-processes without exactly one none start, ad-hoc sub-processes -with start / end events or without activity, link throws without catch, -duplicate link catch names, transactions, cancel events, unprefixed -attributes BPMN does not define (the whole file), extension elements on -`bpmn:definitions`, and a file without an executable process. Camunda 8 -validates only executable processes (non-executable ones may have any -defect) but also the content of ad-hoc sub-processes. - -**What deploys but misbehaves** (runtime rules; each run on the engine): a -flow without condition out of an exclusive or inclusive gateway with other -outgoing flows is never taken (no condition true: incident "Expected at -least one condition to evaluate to true, or to have a default flow"); a -condition on a flow out of a task or parallel gateway is ignored (both -branches run); a standard loop runs once; a JUEL completion condition and -non-numeric static job retries end in incidents; `camunda:*` content is -ignored (`camunda:assignee` does not assign a Camunda user task); a -`zeebe:taskDefinition` on a user task, `zeebe:userTask` or -`zeebe:calledDecision` on a service task, an io mapping on a gateway, input -mappings on start / boundary / none throw / none end events have no effect -(output mappings there work; input mappings on catch events and receive -tasks work, unlike the descriptor says); task headers without key or value -are dropped; `zeebe:publishMessage` (not in the descriptor) is accepted but -not run (a send task gets an incident, a message end event sends nothing); a -user task without `zeebe:userTask` is a job of type -`io.camunda.zeebe:userTask`, not listed by `/v2/user-tasks` (practice). - -**Evidence** (all with the final build): - -- Deploy rules: the 151 models of `test/c8-profile.test.ts` with - `BPMN_C8_ENGINE`: a deploy finding (or the structural error standing for - it) exactly for the 106 refused ones (45 deploy); the runtime block (7 scenarios: job - worker with mapping and headers, correlation, FEEL routing, decision and - call activity, Camunda user task vs job worker user task, multi-instance - and standard loop, the ignored / failing settings) and the hint models of - `test/c8-ops.test.ts`: 332 / 332. Exploration before the rules: 590 probe - deployments (300 refused); the profile agrees on all but six, two - XSD element-order errors the model cannot see (the CLI writes the right - order) and four the CLI's structural validation is stricter than the - engine about (two root processes without pools, an event sub-process with - an incoming flow, an intermediate error throw). 60 timer values and 45 - FEEL expressions decide the value checks (every valid one passes). -- Real files: the 38 real Camunda 8 files (34 deploy, 4 refused) and the 28 - scenario files (all refused: design models without zeebe content): 0 - false deploy findings, 32 / 32 refused files found, every element the - engine names in a refusal is in a deploy finding (definitions by their - event). Following the hints (the first command of each, placeholders - filled in; 306 commands through the CLI, none refused) leaves no deploy - finding in 32 / 32 files, and all 32 deploy. -- Edit battery: rename, id rename, insert (service task with job), remove - (bridged), retype (service -> send task; task -> user task), timer - boundary event with an end event, full redraw on the 66 files: 459 edits - (236 on files that deploy), every edit succeeds, 0 regressions (deployable - -> refused), the profile agrees with the engine on all 459 results, and - 1,689 / 1,689 `` blocks outside the edited - elements are byte for byte unchanged. -- The README's Camunda 8 worked example (two files) deploys and runs both - paths; the worked session's result deploys. -- Camunda 7 unchanged: the live-engine suites (Camunda 7.24, CIB seven 2.2, - Operaton 2.1) 804 / 804. - -**Still open:** - -- FEEL is not parsed: the profile reports static values and the common - slips (`&&`, `||`, `==`, `!`, `${...}`, single quotes, unbalanced - brackets, a dangling operator); other syntax errors only the engine finds. -- A variable an input mapping makes local (the worked example's `amount`) - is not visible to a later gateway; the profile does not follow variables. -- `zeebe:publishMessage` is known to the profile only (not in the - descriptor), and the profile reports it as not run by 8.9; when Camunda - runs it, the rule goes. -- Event-based gateways in files without `` lists: the - profile reports them (Camunda 8 counts the listed flows); the CLI keeps a - file's way of listing them and has no command to add the lists. -- Linked forms (`formId`) are not checked against deployed forms, called - processes and decisions not against the deployment. -- Found on the way, for the Camunda 7 profile: `PT2D` (no ISO 8601 - duration) deploys on Camunda 7.24, CIB seven 2.2 and Operaton 2.1, and - every instance fails when it reaches the timer (ENGINE-09027); the C7 - profile does not check timer values. -- Bundle size: the Camunda 8 profile and descriptor add about 90 KB - minified / 23 KB gzip to the core (whole entry 810 / 247 KB, `applyToXml` - 665 / 204 KB). - -## Step 3 round 1 (2026-10-10): the packages together - -The four step-3 packages above were built in parallel on 0.3.0 and merged on -`step3/round1` (views, ids, layout, Camunda 8). Two decisions of the user -hold for all of them: elements are addressed by id only (names may repeat, -no name-based addressing), and new ids speak in the file's conventions -(never a random, modeler-hash or hash id; an explicit id always wins). - -**Audit items addressed in round 1:** - -| # | status | package | -| --- | --- | --- | -| 12 | fixed | layout | -| 14 | fixed | ids | -| 21, 22 | fixed | views | -| 28 | fixed (partly since the Camunda 7 audit) | Camunda 8 | -| 39, 41, 42 | fixed | layout | -| 48 | fixed (partly since step 2) | ids | -| 53 | partly fixed (`remove --bridge-all`, `--with-branch`) | ids | -| 58, 60 | fixed | views | -| 61, 62, 69, 71, 72, 73, 77 | fixed | layout | -| 40, 74 | partly fixed | layout | - -That is 18 fixed and 3 partly fixed; the table above has 48 fixed, 6 partly -fixed and 23 open of the 77 bugs. From the dogfooding wishlist: `show ---around` / `show --context`, the warnings delta and `--summary`, -`guide --short` / topics, stdin / stdout, speaking ids, batch aliases, -ranked `E_NOT_FOUND` candidates, `compact`, pool order, the selectors, -columns in `show --layout` and the Camunda 8 profile are done; `find ---attr` / `list --fields`, a `vars` view and a `render` command are not. - -**Integration** (`test/step3-integration.test.ts`; each fix with a test that -fails on the merge before it): - -| what | resolution | -| --- | --- | -| aliases and the layout package's new op keys | `"as"` aliases work in the selectors `path` / `via` / `branch`, the `ids` of `compact` and the `pools` of `order` | -| a format op naming an id an earlier op renamed | checked at the end of the batch, while its context is set: `E_NOT_FOUND` names the new id and the op (before: a bare "No element with id" from the format phase) | -| `--summary` with aliases, renames and format ops | `aliases:`, `renamed: -> ` (also `renamed` in the JSON: a summary reader learns the new id of a flow renamed after its ends) and one `format #` line per format op (before: "no changes" after a `place`) | -| warnings delta and the platform profiles | a resolved missing start is listed once, as the platform finding (before: `W_NO_START` and `W_C8_DEPLOY_START_EVENT`) | -| `show --around` / `--context` in Camunda 8 files | each zeebe setting once, in the extension summary (before: `job=charge-card, zeebe:taskDefinition type=charge-card retries=3`) | -| event definition ids of Camunda 8 files | through the id style: the file's prefix and case, the speaking part of the event's id or its name (before: the hash of a Modeler event id was copied, `ConditionalEventDefinition_1q2w3e4`), `_2` with `W_ID_SUFFIXED` | -| the gate's fuzz campaign (walk `controlflow__s04`, seed 1491033354, step 10) | an open layout defect, not a regression: every package build gives it on that walk, which only comes up with the integrated ids. Removing a node closed the strip on its row and pulled an expanded sub-process over a gateway of the row below (`di:frameIntrusion`); `closeRow` now keeps the strip open then | -| guide, kinds, README | one CAMUNDA 8 guide section (every `ext add` / `set` line of its recipe runs in a test), `kinds --section zeebeElements`, speaking ids in the guide's examples, the README's worked session and examples re-run | - -**Evidence on the integrated build** (private corpora outside the -repository, counts only): - -| check | result | -| --- | --- | -| gate | 1,615 tests + 159 opt-in, isomorphism check (85 modules), layout regression 444 (budget 444), fuzz 12 x 15: 0 errors, 0 warnings | -| Camunda 7 live-engine suites (Camunda 7.24.0, CIB seven 2.2.0, Operaton 2.1.5) | 804 / 804 | -| Camunda 8 engine suites (Camunda 8.9.22) | 332 / 332 | -| roundtrip, 291 real files (278 with a rename target), 0.3.0 in brackets | no-op byte-identical 262 / 278 in layout auto (262; 262 / 265 = 98.9 % of the files with a diagram, the 13 without one are drawn), 278 / 278 with `--no-layout` (278); rename 2 changed lines (median, p90, max; 2); insert 56 lines median (54), region 83.2 % (80.3 %): the flow renamed after its new ends | -| Camunda 7 real-file battery, 218 files, 8 edit types | 1,512 edits, all succeed, 0 regressions on the three engines, 0 unexpected camunda changes (one file deploys after a remove, as in step 2) | -| Camunda 8 corpus, 66 files | 0 false deploy findings, 32 / 32 refused files found; following the hints (306 commands, none refused) makes all 32 deployable | -| Camunda 8 edit battery | 459 edits, 0 regressions, the profile agrees with the engine on 459 / 459, 1,689 / 1,689 extension blocks outside the edited elements unchanged | -| README examples | the worked session's result deploys on Camunda 8.9; the Camunda 8 example deploys and runs both paths; the Camunda 7 example deploys on the three engines | -| browser bundle | whole entry 897 / 275 KB minified / gzip, `applyToXml` 710 / 219 KB (0.3.0: 716 / 224, 594 / 186) | - -**Still open after round 1:** the "still open" lists of the four package -sections, without what the others did (`show --layout` has columns now, #14 -is fixed); for hosts the JSON of a mutation changed (`warnings` is -`{added, resolved, preexistingCount}`, `validation.warnings` is gone, -`--json` is compact) and new ids speak, a flow whose id named its ends is -renamed (`renamed` in the result); two branches adding an element of the -same name get the same id; the core grew by about 180 KB minified / 51 KB -gzip since 0.3.0. - -## Step 3 round 1 fixes (2026-10-10) - -An independent verifier re-checked `step3/round1` with its own scripts -(gate, dogfooding on large real models, the corpora, Camunda 7 and 8). Its -findings were fixed on three branches built in parallel on round 1 -(`step3/fix-ids`, `step3/fix-c8`, `step3/fix-views`) and merged on -`step3/round1` with the leftover of the ids branch. Elements are addressed -by id only; new ids speak in the file's conventions. Audit status after the -fixes (#53 fixed): 49 fixed, 5 partly fixed, 23 open of 77. - -### Ids and report (`step3/fix-ids`) - -Each fix has a test that fails on round 1 (`test/step3-ids-report.test.ts`). - -| finding | severity | status | -| --- | --- | --- | -| Flows at unnamed gateways / tasks got kind words that collide (`Flow_CheckToGateway`, `Flow_FixToGateway` for two gateways, `Flow_GatewayToJoin_2` with `W_ID_SUFFIXED` in a 9-node file, `Flow_JoinToJoin`, `Flow_TaskToTask`), although the gateway's id speaks (`Gateway_AfterCheck`) | medium | fixed: an unnamed end is named by the speaking part of its own id (`Flow_CheckToAfterCheck`, `Flow_AfterCheckToCheckJoin`, `Flow_FixJoinToCheckJoin`); flows renamed after their ends use the same rule, flows named by the old rule still follow their ends, existing ids stay | -| Id quality: flow ids over 100 characters, `Ø` dropped, `AfterAfter...` chains | low | fixed: at most 64 characters, cut at a word boundary, the same every time; `ø` / `å` / `æ` / `œ` / `ł` / `þ` spelled out, other accents dropped; after an unnamed anchor the nearest named anchor, once; a taken id of an unnamed gateway or activity spells out its kind before `_2` (`Gateway_ParallelAfterCheck`); a name without a letter names nothing | -| `bpmn new --name` wrote `Definitions_1` (the handover said only without `--name`) | low | fixed: `Definitions_` (also from a speaking `--id`); the handover corrected | -| "N warnings already in the file" left out the platform profile's findings (`W_C7_*` / `W_C8_*`) | low | fixed: counted; the count is what `bpmn validate` lists | -| Removing one of three equally named elements re-reported `W_DUPLICATE_NAME` as added (and 1 resolved) | low | fixed: a group warning that only shrank is the old one (compared by code and element ids); one that takes in another element is added | -| The remove summary listed one id as removed and as changed / renamed when the bridged flow took over the removed flow's id | low | fixed: a takeover lists the bridged flow's old id as removed and the taken id as changed, no rename | -| A stale `W_LANES_WITHOUT_POOL` when lanes come before the participant in the same batch | low | fixed: op warnings about the model's state (`W_LANES_WITHOUT_POOL`, `W_IMPLICIT_SPLIT`, `W_IMPLICIT_JOIN`) are checked against the batch's final state | -| `new --target camunda8` reported `W_NO_START`, the next edit "resolved: W_C8_DEPLOY_START_EVENT", never shown | low | fixed: `new` reports the platform findings an edit would (`W_C8_DEPLOY_START_EVENT`, which the next edit resolves) | -| (left open by the branch) in a file whose task prefix says the kind (`Task_`, `task_`) an unnamed task after an unnamed gateway had the gateway's body (`Flow_AfterCheckToAfterCheck`), the next one `_2` | low | fixed on the merged build: where the id would repeat its unnamed anchor's, it names the anchor (`Task_AfterCheckGateway`, then `Task_AfterCheckGatewayTask`; `Activity_AfterCheckServiceTask` after `Activity_ServiceTaskAfterCheck`); flow ends that begin alike keep their last word when cut | - -Measured on the private corpus (local only, counts only; round 1 -> -`step3/fix-ids` -> the merged build). Probe set: 15 edits at the first, middle and last task -with one outgoing flow of each of 271 real files (a named task, an unnamed -gateway, a boundary timer path, an error boundary, a message catch, signal -and conditional events, a split, nested splits, three unnamed elements in a -row, annotation and data, names with digits / CJK / symbols / accents, long -names, a remove, a connect, a black-box pool): 4,017 edits (59 refused -`E_CROSS_SCOPE` in both), 21,000 new ids, 10,144 new flows. - -| measure | round 1 | fix-ids | merged | -| --- | --- | --- | --- | -| new flows with a kind word for an end (`Gateway`, `Join`, `Task`, `End`, ...) | 1,879 | 178 (ends whose id is a hash and that have no name: the documented fallback) | 178 | -| new flows whose two ends read alike (`Flow_AfterCheckToAfterCheck`, also with a suffix) | 168 | 507 | 3 (two named the same, `AfterToAfter` of the `scopedTo` form) | -| new ids over 64 characters / over 100 | 2,533 / 12 | 0 / 0 | 0 / 0 | -| longest new id / p99 / median | 110 / 91 / 36 | 61 / 61 / 40 | 61 / 61 / 41 | -| `W_ID_SUFFIXED`, all probes | 394 | 542 | 221 | -| ... nested splits / signal + conditional / connect / remove | 106 / 84 / 9 / 14 | 22 / 0 / 5 / 13 | 22 / 0 / 5 / 13 | -| ... unnamed elements in a row, names without letters | 11 | 332 (files whose task prefix says the kind, `Task_` / `task_`: the second unnamed task after one named anchor took `_2`) | 11 (all in files with the `scopedTo` flow form: `Flow__AfterToAfter2`) | -| ... the rest: a data object / pool of a name the file has (145 / 5), a second flow between the same pair (connect, bridge of a remove), a named task the file has and the `scopedTo` files' `AfterToAfter` flows (splits, nested splits) | | | as in fix-ids | -| `tools/speaking-ids.mjs`, 291 files (merged: the 289 still on disk): new ids that speak; two edits with unnamed elements at two places share an id | 1,745 / 1,745; 3 / 225 | 1,745 / 1,745; 0 / 225 | 1,731 / 1,731; 0 / 223 | -| a rename: "already in the file" + added = what `validate` lists afterwards | 238 / 274 files | 274 / 274 (counting a pre-existing error as `validate` lists it, among the errors, and validate's DI warnings, which a write without layout does not report) | | -| remove one of 3+ equally named tasks: `W_DUPLICATE_NAME` added | 5 / 5 files | 0 / 5 | | -| remove a task with a bridge: an id listed as removed and as changed / renamed | 3 / 684 | 0 / 684 | | -| gate | 1,615 tests + 159 opt-in | 1,631 tests + 159 opt-in; isomorphism check (85 modules), layout regression 444 (budget 444), fuzz 12 x 15: 0 errors, 0 warnings | see "The fixes together" | - -Still open: a context is read from the anchor's id, so a cut id gives a cut -context (`Gateway_After4AugenPrinzipPruefenFreigeben2`); the `scopedTo` -flow form names an end by its first word, so the flows between unnamed -elements after one anchor read `AfterToAfter` and take `2`. - -### Camunda 8 (`step3/fix-c8`) - -Every new rule was deployed to Camunda 8.9.22 (it refuses exactly what is -flagged). This closes the round 1 item "FEEL is not parsed" (its syntax; -not its meaning). - -| finding | fix | -| --- | --- | -| deploy recall 0.74 (W_C8_DEPLOY_* on 200 adversarial models): FEEL syntax errors (`&`, `\|`, `<>`, `>>`, `%`, if without else, `AND`, `:=`), `= if a then 1` in a script, a JUEL ad-hoc completion condition, `PT1.5H`, an id `a:b`, a lane `flowNodeRef` to a missing id, the XSD's element order | a FEEL syntax check without dependency (`src/platform/feel.ts`, written from 766 deployed expressions, agreeing with the engine on all; 762 are in `test/fixtures/c8/feel-verdicts.json`), also for signal / published message names, `zeebe:adHoc` and an ad-hoc completion condition; fractions on seconds only; schema rules read from the file's text (`src/platform/schema-text.ts`: child order, IDREFs, ids that are no NCName, also those bpmn-moddle drops) | -| false deploy findings: white-space values, a lower-case date-time | white space deploys as a name, job type, code, id or result variable (only empty is refused), `t` / `z` in either case, an empty priority deploys; an empty link name is refused (added) | -| the message an op creates cannot be addressed in a batch; `ext add zeebe:subscription` lands on the event | `"refAs": "$msg"` on add / connect / set / retype; a subscription or `zeebe:correlationKey` given to a catch event or receive task goes to its message, with a note (an element without a message: E_WRONG_HOST with the commands) | -| `kinds --help` lacks zeebeElements | the help lists the section table; a test checks every section the guide names | - -**Evidence on the branch** (counts only): - -| check | result | -| --- | --- | -| the verifier's 200 adversarial models, deployed again (81 refused) | W_C8_DEPLOY_*: precision 0.938 -> 1.000, recall 0.741 -> 0.951; any finding (W_C8_DEPLOY_* or a structural error): precision 0.846 -> 0.910, recall 0.815 -> 1.000 (the 8 left: structural rules stricter than Camunda 8, a start event with an incoming flow and similar, and an id with an umlaut bpmn-moddle cannot read) | -| 126 probe models of this round | 126 / 126 agree with the engine (before: 104) | -| 38 real Camunda 8 files | the profile agrees on 38 / 38; 0 of 97 distinct real FEEL values flagged | -| Camunda 8 edit battery (8 edit types) | 304 edits, 299 written, 0 regressions, 299 / 299 agree; the message edit with `refAs` or the redirect instead of an id guess: 74 / 76 written (2 keep an existing message's other key), 0 regressions, 0 misplaced subscriptions | -| Camunda 8 engine suites | 460 / 460 | -| browser bundle | whole entry 922 / 284 KB, `applyToXml` 733 / 227 KB (+25 / 8 and +23 / 7 KB) | - -### Views and layout (`step3/fix-views`) - -The verifier's findings about the reading views and the format ops, and two -items the ids package left open. Each fix has a test that fails on -`step3/round1`. - -| finding | fix | test | -| --- | --- | --- | -| `show --context`: `caught by` named only error / escalation boundary events of the sub-processes around the element (the interrupting message boundary events that cancel it took a third call); no message for message events / receive tasks; a message flow printed its header only; a message did not name its users | `caught by:` every boundary event of every sub-process around it, inner first, non-interrupting ones marked; `message:` (id, name, Camunda 8 correlation key) on message events and send / receive tasks, which also list the message flows of their message drawn to their pool (`(at pool P)`; a flow without `messageRef` by the message's name); a message flow: `message:`, `in: collaboration`, `from:` / `to:` with their pools; a message / signal / error / escalation: `used by:` | `test/views-context.test.ts` | -| `align --kind endEvent --axis column` (and explicit ids with `--to`) moved the end events of a sub-process to a column outside it: the sub-process grew towards it, was pushed aside by what it then covered and left them behind (hard `outsideSub`; the build before step 3 did the same) | a sub-process grows towards a reference inside it, never out to one outside it (`E_LEAVES_CONTAINER`, the hint says the reference lies outside); `align` leaves out a member only a selector (`--kind`, `--path`, `--branch`) named, with a note; `place` moves the selection as one group and refuses; making room never takes a shape out of a sub-process, lane or pool that held it (`E_NO_ROOM`) | `test/format-frames.test.ts` (synthetic model, the two scenarios the fuzzer and the verifier hit) | -| #14 for `move`: a node moved out of a sub-process (or from another pool) into a flow between two lanes took the flow source's lane, on the target's row after a branching source; a node that kept the source's lane (and `add --lane` with it) was drawn on the target's row too, its lane stretched over to it (+125 px for the next lane, +265 px two lanes away) | the node without a lane gets one by `add`'s rule (`ops/lanes.ts`, shared): the lane of the row the layout draws it on, `W_LANE_INHERITED`; a node in a lane keeps it (a note when the flow runs between two other lanes) and the incremental layout puts it on a free row of that lane (`diagram/place.ts`, a new row only when none is free); `--lane` decides up front | `test/move-lanes.test.ts` | -| #53: a plain `remove` of a join disconnected the rest | a merge that does not synchronise (exclusive / event-based gateway, a task two paths flow into) is bridged from every predecessor like `--bridge-all`; a parallel / inclusive / complex join is refused with `E_AMBIGUOUS_BRIDGE`, the hint gives `--bridge-all` and `--no-bridge` for it (bridging ends the synchronisation, not bridging cuts off the successor) | `test/remove-join.test.ts` | - -Decision for #53: bridging a merge keeps what runs (each path already ran -on alone), so it happens without asking, like the bridge of a node with one -incoming and one outgoing flow; for a synchronising join both answers -change the process, so the agent says which, with the exact command it is -given. - -**Evidence on the branch** (private corpora used locally, outside the -repository; counts only): - -| check | before (`step3/round1`) | after | -| --- | --- | --- | -| `show --context` bytes, 394 files of the audit corpus, centred on the middle activity (median / p90 / max) | 430 / 875 / 2,108 | 430 / 916 / 2,108 | -| the same for every flow node (5,863) | 424 / 1,044 / 2,483 | 441 / 1,233 / 2,608 | -| message events and send / receive tasks (778) | 439 / 1,188 / 2,305 | 501 / 1,359 / 2,608 | -| nodes inside sub-processes (1,558) | 707 / 1,659 / 2,305 | 830 / 1,741 / 2,608 | -| format ops with selector sets on 396 files (`align --kind` row and column and `tidy --kind` for every kind of a file, `align` of all end events onto the last node, `place --kind endEvent`; 7,467 runs) | 6,057 written, 19 add `outside*` (45 `outsideSub`) | 6,412 written, 0 add `outside*`; the other hard kinds identical (`overlaps` 71, `through` 91: the open "`align` pushes a flow through a shape") | -| the same on the 115 scenarios (2,495 runs) | 5 add `outsideSub` | 0 | -| `--path` / `--branch` sets (up to 3 branches and 3 paths per file; `align` row and column, `tidy`, `place` below the first and after the last node), 396 files, 6,295 runs | 4,512 written, 1 adds `outsideSub` (4), `overlaps` 8, `through` 23 | 4,503 written, 0 add `outside*`, `overlaps` 8, `through` 23 | -| a node of the gateway's lane spliced into the flow to another lane after the branching gateway (`move`, `add --lane`) | the lane grows by 125 px (next lane) / 265 px (two lanes away) | 0 px / 137 px (one new row, when the lane has no free one) | -| layout bench (115 scenarios + 60 hand models; 935 edits E1-E7, 525 global runs) | 2 semantic failures, 0 runs adding hard defects, mean Δ score -0.5; global hard after: tidy 194, layout 21, engine-auto 80 (1 failed) | the same, no edit better or worse | -| `npm run gate` | 1,615 tests, score 444 | 1,635 tests + 159 opt-in, isomorphism 86 modules, score 444 (budget 444), fuzz 12 x 15: 0 errors, 0 warnings | -| plain `remove` of every merge / join (66) in the verifier's 50 Camunda 7 battery files, deployed on Camunda 7.24.0, CIB seven 2.2.0 and Operaton 2.1.5 | 66 written, 43 refused by the engines, 379 `W_UNREACHABLE` added | 62 bridged, all deploy on the three engines, 4 parallel / inclusive joins refused with the commands, 0 `W_UNREACHABLE` | - -The verifier's one Camunda 7 deploy regression (a plain remove of a node -with two incoming flows left a gateway with one conditional outgoing flow) -deploys on the three engines now. - -### The fixes together - -Merged in the order ids, Camunda 8, views; each fix of this step with a test -that fails on the merge before it (`test/step3-ids-report.test.ts`, -`test/step3-fixes-integration.test.ts`). - -| what | resolution | -| --- | --- | -| unnamed elements in a row where the prefix says the kind (left open by the ids branch) | an element whose id would repeat its unnamed anchor's names the anchor's kind (`Task_AfterCheckGateway`), before a suffix; flow ends that begin alike keep their last word when cut (see the ids table) | -| `--summary` listed elements the batch created under `changed` too (the lane a new node got, the message `refAs` named that an `ext` op completed) | listed once, under `created`; the full result keeps the change entries | -| `refAs` aliases, renamed flows and speaking ids in one `--summary`; the subscription redirect in the warnings delta; a plain remove of a merge and `move` into a cross-lane flow with flows renamed after their ends | work together (checked in `test/step3-fixes-integration.test.ts`, no change needed) | - -**Evidence on the merged build** (private corpora used locally, outside the -repository; counts only): - -| check | result | -| --- | --- | -| gate | `npm run typecheck` clean; 1,729 tests + 219 opt-in, isomorphism check (88 modules), layout regression 444 (budget 444), fuzz 12 x 15: 0 errors, 0 warnings | -| Camunda 7 live-engine suites (Camunda 7.24.0, CIB seven 2.2.0, Operaton 2.1.5) | 804 / 804 | -| Camunda 8 engine suites (Camunda 8.9.22) | 460 / 460 | -| Camunda 7 real-file battery: 50 files (48 deployable on the three engines), 8 edit types, three engines | 373 edits, all written, 0 regressions (round 1: 1, a plain remove of a node with two incoming flows) | -| Camunda 8 corpus: 38 real files (4 refused by the engine) | the profile agrees on 38 / 38, 0 false deploy findings | -| `tools/roundtrip.mjs`: 289 of the 291 real files still on disk, 276 with a rename target | no-op identical 260 / 276 in layout auto (260 / 263 with a diagram, 98.9 %), 276 / 276 with `--no-layout`; rename 2 lines (median, p90, max); insert 56 lines median, region 83.2 %; round 1 the same on the same files | -| id probes (the ids table above) | `W_ID_SUFFIXED` 394 -> 542 -> 221; unnamed elements in a row 11 -> 332 -> 11; flows whose ends read alike 168 -> 507 -> 3 | -| browser bundle | whole entry 936.5 / 288.2 KB minified / gzip, `applyToXml` 742.3 / 229.7 KB (round 1: 897 / 275 and 710 / 219) | -| `guide --short` | 4,172 bytes (budget 5 KB) | - -## Step 3 follow-ups (2026-10-10) - -The second verifier round on `step3/round1` (six new findings) and what -agents ran into in the Claude Code plugin evals (four), fixed on -`step3/followups`; each with a test that fails on round 1 -(`test/step3-followups-ids.test.ts`, `test/step3-followups-c8.test.ts`, -`test/step3-followups-ops.test.ts`, `test/step3-followups-layout.test.ts`). -The audit's 77 bugs are unchanged: 49 fixed, 5 partly fixed, 23 open. - -| finding | severity | status | -| --- | --- | --- | -| an unnamed element whose anchor name has a multi-digit number was labelled by its kind word (`Flow_Check2024ToGateway`, `Activity_AfterGateway`): a stem with many digits counted as a machine id | medium | fixed: a stem with a word (four letters, one no hex digit) speaks (`Flow_Check2024ToAfterCheck2024`); machine ids still do not | -| a data object reference in a file with type-named prefixes: its `bpmn:DataObject` got `dataObject_invoice_2` and a `W_ID_SUFFIXED` | medium | fixed: the reference's type-named prefix is its own type (`dataObjectReference_invoice` + `dataObject_invoice`); where a file names both with one prefix the object says what it is (`data_invoiceObject`), never a suffix | -| Camunda 8: impossible timer dates / times, out-of-range cron fields and non-boolean xsd:booleans passed | low | fixed: `W_C8_DEPLOY_TIMER` (also a due date: `W_C8_DEPLOY_USER_TASK`) with the reason, `W_C8_DEPLOY_SCHEMA` for a boolean (from the file's text); 27 models deployed to Camunda 8.9.22, each verdict holds | -| a duplicate element id was "valid" with an import warning | low | fixed: `E_DUPLICATE_ID` in validate on every platform (both engines refuse it) | -| split created a join only one branch reaches when the other ends in an end event | low | fixed: a join needs two branches that reach it; one continuing branch runs on to the successor (no pass-through gateway, `joinAs` refused), the note marks terminating ends | -| DI ids derived from new element ids could exceed 64 characters (`Edge_` / `Shape_` prefixes) | low | fixed: derived DI ids (also planes, diagrams, a renamed flow's edge) are cut at a word boundary; since 2026-10-13 also the engine's `BPMNShape_` / `BPMNEdge_` / `BPMNPlane_` / `BPMNDiagram_` ids in a new drawing and its redraws, which kept them uncut until then, and `set id=` takes a derived DI id along (`test/conventions-di.test.ts`, `test/keep-flow-ids.test.ts`) | -| eval: `space --by 2` meant 2 px, agents meant two columns | | fixed: `col`, `column(s)`, `row(s)`, `px`; the result notes the distance, a few pixels say how to ask for columns | -| eval: `E_NOT_FOUND` for a mistyped flow id suggested a gateway | | fixed: ids with the query's prefix first; the same prefix with one body inside the other is a candidate (`Flow_GatewayXToY` -> `Flow_XToY`) | -| eval: `add --kind userTask --name X`, a `route` op with `flowId`, a `set` op with top-level properties | | fixed: hints with the command / op rewritten (still refused) | -| eval: one batch splicing a task and then a gateway into one flow of a hand-drawn model put both below the line; a full layout with lanes left an end event's label outside the pool | | fixed: a chain of new nodes in one flow is spliced like separate commands (the eval's model: score 0 -> 0, the same geometry as two commands; before 0 -> 9); pools and lanes hold the labels right of their content, a moved label prefers a spot inside its frame | - -**Evidence** (private corpora used locally, outside the repository; counts -only; round 1 -> this branch): - -| check | result | -| --- | --- | -| gate | `npm run typecheck` clean; 1,786 tests + 247 opt-in, isomorphism check (88 modules), layout regression 444 (budget 444), fuzz 12 x 15: 0 errors, 0 warnings | -| Camunda 8 engine suites (Camunda 8.9.22) | 519 / 519 (460 + the 27 follow-up models and the duplicate id, plus their checks without engine) | -| Camunda 8 corpus: 38 real files (and 28 scenarios) | none of the new rules reports anything on them (nor on 394 corpus files): the agreement 38 / 38 holds, 0 false deploy findings | -| id probes of the verifier (15 edits on each of the 96 of its 100 files still on disk, 1,513 edits) | `W_ID_SUFFIXED` 193 -> 142 (annotation + data 51 -> 0); new flows with a kind word for an end 53 -> 10 (ends at existing unnamed gateways with hashed ids: the documented fallback); ends that read alike 10 -> 7; ids over 64 characters 8 -> 0, longest 66 -> 61 | -| `tools/roundtrip.mjs`, 291 real files (278 run) | no-op identical 262 / 278 in layout auto, 278 / 278 with `--no-layout`; rename 2 lines; insert 56 lines median, region 83.2 %; round 1 the same | -| full layout of 509 files (115 scenarios, 394 corpus files) | `labelOutsideFrame` 188 -> 160, score 2,653 -> 2,599; crossings 229 -> 228, flows through a shape 45 -> 46 (a diagonal collaboration association next to a wider pool), labels on a line 82 -> 81 | -| browser bundle | whole entry 945.9 / 291.7 KB minified / gzip, `applyToXml` 750.7 / 232.9 KB (round 1: 936.5 / 288.2 and 742.3 / 229.7) | -| `guide --short` | 4,172 bytes (budget 5 KB) | - -## Issue #18, stage A (2026-10-12): edits to hand-drawn models - -The plugin eval's item 2 (new branches and boundary paths in a drawn model -add crossings; agents fight `place` and `E_NO_ROOM`), measured in issue #18 -on the eval model and a private corpus. Stage A (`feat/layout-batch-parity`, -`test/layout-batch-parity.test.ts`; details in HANDOVER, "Incremental layout -of edits to hand-drawn models, stage A"). The audit's 77 bugs are unchanged. - -| finding | severity | status | -| --- | --- | --- | -| eval: one batch with a decision that gets a second outgoing flow (a "no" branch, `split`) put the spliced task and gateway on a new row below; a merge was placed before the nodes it follows | | fixed: new nodes in op order, the splice continues through new nodes with branches; the eval's batches give the shapes of single commands (score 0 -> 0, before 0 -> 9..19) | -| eval: a boundary path placed before the normal flow's new nodes crossed them | | fixed: exception paths wait for the normal flow; each placed node's flows are routed before the next one is placed, as single commands see them | -| eval: a `connect` into a merge an earlier command spliced ran backwards | | fixed: the merge is placed again right of its sources (no loop return) | -| eval: no command puts a boundary event on its host's top border (agents edited `dc:Bounds` by hand) | | fixed: `place --side top\|bottom`, its exception path follows | -| eval: `E_NO_ROOM` named no command that works | | fixed: a dry-run-checked `space` command in the refusal (`details.command`, `details.ops`) | -| bench: no edit hit batches; `--compare` gated hard kinds only | | fixed: E8 / E9 (batches), soft kinds per edit, a soft budget for E8 / E9 | -| verification: a batch splicing into two branches of one gateway (a task into "yes", a merge into "no"; the merge batch in a pool with lanes: 18 against 5 as single commands) put the first node on a new row | | fixed: an anchor whose other branch leads to a node a later op spliced counts as branching; a merge placed in this run is placed again when a later node's flow would enter it backwards | -| verification: `place --side bottom` after `--side top` in a pool with lanes refused (`E_LEAVES_CONTAINER` with a hint naming the shape's own lane); back to a border two other events take refused | | fixed: the path is the nodes in the host's own frame; a lane with a lane underneath grows at its bottom; a corner spot as a fallback; the hint says "below lane X, in no lane of its pool" | -| verification: `place --side` rerouted a loop flow, and the association of an annotation on it ran through two shapes (`through`) | | fixed: an association docked on a flow takes the nearest point of the flow a straight line reaches, else it is routed around the shapes; it follows the flow's final line (routed after the flows) | -| verification: `E_NO_ROOM` took the first space-tool run that fits (`1col`, score 8 / 7 after it; `2col`: 7 / 5), and in a batch after other ops named a command for the file that was checked on another drawing or named ids only the batch has | | fixed: the run with the fewest layout problems; `details.command` only when the op saw the file's drawing, else only the batch op (`details.ops`) | - -Open: the crossing-aware row choice and the local repair (issue #18 stage -B); the single-command rate of new branches and boundary paths crossing a -bypass or loop-back flow (about one run in five) is unchanged. - -## Issue #18, stage B (2026-10-12): room-making row choice and local repair - -Steps 2 and 3 of issue #18 (`feat/layout-room-making`, -`test/layout-room-making.test.ts`; details in HANDOVER, "Incremental layout -of edits to hand-drawn models, stage B"). Of the audit's items in the same -code, #63 (a branch row in a frame) is fixed; #13 (a splice into a loop-back -flow) and #74 (a task placed left of its rejoin target) are splice and -prepend placements, not the row choice: unchanged. Audit: 50 fixed, 5 -partly fixed, 22 open. - -| finding | severity | status | -| --- | --- | --- | -| issue #18 (2): a new branch or boundary path in a hand-made drawing crosses a bypass or loop-back line in about one run in five (single commands, private corpus: boundary path 23 %, new branch at a split 20 %, decision spliced into a branch 38 %) | | fixed: the row choice counts the chain's line on its row, opens the row right under the anchor (the lines under it move down with the shapes), tries rows above the anchor and, for a boundary path, the host's top border; 2 % / 5 % / 7 % | -| issue #18 (3): no repair after placement | | fixed: a crossing between a new and an existing connection is routed away (the new one, else the existing one), else the branch starts at its next row candidate; kept only under the guard of `compact`, at most 8 trials per write | -| #63: a new branch inside a transaction lands on its bottom boundary event | low | fixed: frames grow clear of their own boundary events | -| a batch saw an association its placement broke in its old place (rerouted only at the end), single commands saw it rerouted | | fixed: the placement routes broken associations and data associations at once, like sequence flows | -| verification: a path added to a boundary event on its host's top border (`place --side top`, then the reminder) went below the host, and the room-making cut line fell inside the host's row: the start and end events of the hand-drawn row dropped about 218 px | high | fixed: the path of an event on the top border runs above its host (rows below cost more); room below the host is made under the host's whole row | -| verification: the top-border spot ignored a line running through it: the event sat on the line, the line was routed again and crossed another one | medium | fixed: the spot keeps clear of lines and connection labels | -| verification: an opened row plus the repair traded a crossing with a new flow for one between two drawn lines (score lower, 2 of 3,379 corpus runs) | medium | fixed: a repair trial that adds a crossing between two connections drawn before the write is undone | -| verification: a boundary event drawn by hand on the bottom border went to the top border when its path was added | low | fixed: only an event the CLI placed (in this write, or where a command put it) moves | -| verification: a rerouted line ran along the border of a new end event (no `through`) | low | fixed: a line touching a node placed at the end of a chain is routed again | -| verification: writes that run the repair on 300-node models took up to 60 % longer | low | partly fixed: a reroute that still crosses the line it is tried for (or, for an existing line, crosses another drawn line) is not measured; 300-node probe edits +7 % over stage A in total (before +14 %) | - -## Coverage gaps - -What the audit found untested or only weakly tested before this step: - -1. **Edits inside sub-processes at depth >= 2** (insert, move, split, boundary), - including event sub-processes nested in sub-processes. Found broken (#4). -2. **Target-size real geometry**: 100+ flow nodes per plane, 300–700 KB, - Camunda 8. Nothing local came close, and generated stand-ins are too regular. -3. **Long agent sessions**: sequences of 20–50 edits on one model. Unit tests - are single-op; 15 % of real-model sessions ended with a defect. -4. **Incremental layout with parallel, inclusive and event-based gateways, - transactions, ad-hoc sub-processes, call activities, intermediate and link - events**: no unit test at the time. -5. **Artifacts on removed, moved, collapsed or other-plane elements** - (associations, data associations). Found broken (#15, #35, #75). -6. **Files containing unsupported kinds** (complexGateway): every write blocked (#26). -7. **Format operations on nested or multi-plane drawings and on message-flow labels.** -8. **Group-heavy drawings**: groups count as overlaps, so the hard-defect - oracle produces false positives on such files (since - `fix/full-redraw-keeps-groups` a group is no leaf shape for the metrics). -9. **Not covered at all**: vertical pools beyond the refusal path, - choreographies, second diagram views beyond collapsed planes, deployment of - the outputs to a real engine. -10. **Infrastructure**: no CI, no property-based tests, and the benchmark and - fuzzer lived outside the repository. This step adds `tools/bench`, - `tools/fuzz`, `test/fuzz.test.ts` and `npm run gate` (see testing.md); - a CI job and a scheduled run on a private corpus are still to be set up. - -## Recommended roadmap - -**P0, correctness and the oracles** - -- Make the metrics see frame intrusions, overlapping frames and degenerate - edges (#8) and keep them hard; the bench, the fuzzer and the property test - pick new kinds up automatically. -- Fix nested-container growth (#4, #5, #6, #11) with regression fixtures for - nested sub-processes (depth 2–3, event sub-process inside a sub-process, - siblings in other row bands). -- Stop treating vendor extension ids as BPMN ids (#1); move the lossy-import - guard into the library path (#3); keep trigger-detail vendor attributes (#2). -- Run `npm run gate` in CI on every change; run the bench and a longer fuzz - campaign on the private corpus on a schedule, outside the repository. -- Obtain two or three anonymised target-size models (ids, names and docs - renamed, geometry kept) and add synthetic fixtures that reproduce their - defects. - -**P1, embedding and roundtrip** - -- Text-preserving output (root tag, attribute order, prolog, comments, CDATA) - and true no-op writes (#30, #31, #44–#47): done in step 2. -- An isomorphic core without fs / process access (#33) and op validation in - the library entry points (#49). Done in step 2 (`applyToXml` and the other - in-memory functions; `mutateDoc` still takes typed ops unchecked). -- Platform detection and typed vendor adapters (#28, #29; the decision link is - done, the Camunda 8 profile in step 3), a pluggable validation profile - matching the host's save gate (#27, #26; done in step 2; the design profile - was removed with issue #35, the validator hook stays). -- Collision-resistant and file-style ids (#32, #48); stickies follow nodes - (#43): done in step 2; speaking ids and flows renamed after their new - ends in step 3 (#48 done). -- Agent output: a neighbourhood view (`show --around `), diffed and capped - per-call output (#60): done in step 3; an MCP server (HANDOVER, "What to - build next"). - -**P2, layout depth** - -- Full-engine fixes for event sub-processes, collaboration annotations and - message flows (#17–#20, #23–#25) and the lane / loop placement (#7). -- Groups as frames in placement and metrics; cross-plane artifacts (#15, #75). -- Format operations on nested and multi-plane drawings; a `render` command and - periodic blind visual comparison of rendered samples. - -**P3** - -- Mutation testing of the metrics and oracles, so that a blind spot like #8 is - found by the test suite rather than by an audit. -- Comment preservation inside the model, element templates beyond the - reference, licensing and ownership for a contribution to the host. diff --git a/docs/testing.md b/docs/testing.md index 36bde5e9..eec664ac 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -31,9 +31,6 @@ rename flows as by default, and the fuzzer, the benchmark, the layout regression and the demo run the CLI without it (`CLI_ENV` in `tools/fuzz/lib/setup.mjs`). A test that needs it sets it for its own spawn. -The audit that motivated this tooling is summarised in -[audit-2026-10.md](audit-2026-10.md). - ## Invariants The fuzzer and the property test check after every step @@ -408,8 +405,7 @@ npx vitest run test/e2e/isomorphic.test.ts entry chunk is the output whose metafile `entryPoint` is the requested entry (esbuild gives every `import()` target an `entryPoint` too). esbuild itself never shows the hang (it puts shared modules into chunks - of their own); the check models what Rollup does, and the Vite run in - HANDOVER.md ("Profile rules on demand") proved it in Chrome. + of their own); the check models what Rollup does. - `test/e2e/isomorphic.test.ts` bundles `src/index.ts` the same way (no build needed), checks that the check sees a planted `node:fs` import and `process` read, and runs the bundle in a `vm` context without any Node @@ -696,8 +692,7 @@ one message name at the same time. The other C7 regression tests (`test/domain/editing/c7-semantic.test.ts`, `test/domain/editing/c7-ext-structure.test.ts`, `test/e2e/camunda7.test.ts`, `test/domain/editing/c7-ops.test.ts`) run without engines; their engine evidence (deploy and run the CLI-built models, real -customer files before and after edits) was collected outside the repository, -see [audit-2026-10.md](audit-2026-10.md#camunda-7-audit-2026-10-09). When you +customer files before and after edits) was collected outside the repository. When you change how vendor content is written, build a small model with the CLI, deploy it to a Camunda 7 compatible engine and run it (start, fetch-and-lock / complete, correlate), and run the real-file battery on your private corpus: diff --git a/test/domain/diagram/incremental-audit.test.ts b/test/domain/diagram/incremental-audit.test.ts index f00bf15d..acbd859f 100644 --- a/test/domain/diagram/incremental-audit.test.ts +++ b/test/domain/diagram/incremental-audit.test.ts @@ -1,6 +1,6 @@ /** * Audit bugs of the incremental layout fixed with the layout - * ergonomics package (docs/audit-2026-10.md): + * ergonomics package: * - #41 a data object or annotation drawn in a lane follows the lane when * the space tool moves it (it has no lane membership of its own) * - #42 closing a strip inside a sub-process never puts a boundary event diff --git a/test/e2e/fuzz.test.ts b/test/e2e/fuzz.test.ts index d8f9c3ea..39c0986f 100644 --- a/test/e2e/fuzz.test.ts +++ b/test/e2e/fuzz.test.ts @@ -63,8 +63,7 @@ const WALKS = [ ]; /** - * Walks that currently hit open bugs of the 2026-10 audit (`bug` = the number - * in the table of docs/audit-2026-10.md). They are expected to fail; when a fix + * Walks that currently hit open layout bugs (`bug` = the issue they reproduce). They are expected to fail; when a fix * makes one pass, vitest reports it and the walk moves into WALKS. */ const KNOWN: Array<{ file: string; mode: Mode; seed: number; steps?: number; bug: string }> = [];