@formicoidea/labre-framework-bpmn 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/consts.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
2
- import { PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
2
+ import { PointStyle, StrokeStyle } from '@formicoidea/labre-core/model/headless';
3
3
  /**
4
4
  * Visual constants for the BPMN basics. Style "C" (hybrid): spec-accurate
5
5
  * shapes and line weights, with accent colour only on the event rings — the
@@ -240,7 +240,7 @@ export declare const POOL_LANE_NAME_FONT_SIZE = 13;
240
240
  * so this file stays the one place a reader looks for a pool's metrics, and so
241
241
  * the declaration keeps reading them from where it always did.
242
242
  */
243
- export { POOL_BAND_WIDTH, POOL_LANE_BAND_WIDTH } from '@formicoidea/labre-core/model';
243
+ export { POOL_BAND_WIDTH, POOL_LANE_BAND_WIDTH, } from '@formicoidea/labre-core/model/headless';
244
244
  /**
245
245
  * How close to an internal lane boundary a pointer has to be, in MODEL units,
246
246
  * for the gesture to be a separator drag rather than a click on the pool.
package/dist/consts.js CHANGED
@@ -1,5 +1,5 @@
1
- import { PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
2
- import { NOTATION_NEUTRALS } from '@formicoidea/labre-core/shared/consts';
1
+ import { PointStyle, StrokeStyle } from '@formicoidea/labre-core/model/headless';
2
+ import { NOTATION_NEUTRALS } from '@formicoidea/labre-core/shared/headless';
3
3
  /**
4
4
  * Visual constants for the BPMN basics. Style "C" (hybrid): spec-accurate
5
5
  * shapes and line weights, with accent colour only on the event rings — the
@@ -296,7 +296,7 @@ export const POOL_LANE_NAME_FONT_SIZE = 13;
296
296
  * so this file stays the one place a reader looks for a pool's metrics, and so
297
297
  * the declaration keeps reading them from where it always did.
298
298
  */
299
- export { POOL_BAND_WIDTH, POOL_LANE_BAND_WIDTH } from '@formicoidea/labre-core/model';
299
+ export { POOL_BAND_WIDTH, POOL_LANE_BAND_WIDTH, } from '@formicoidea/labre-core/model/headless';
300
300
  /**
301
301
  * How close to an internal lane boundary a pointer has to be, in MODEL units,
302
302
  * for the gesture to be a separator drag rather than a click on the pool.
package/dist/export.js CHANGED
@@ -1,4 +1,4 @@
1
- import { backgroundInstanceZones, backgroundPlot, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundInstanceZones, backgroundPlot, } from '@formicoidea/labre-core/interchange';
2
2
  import { BPMN_POOL_BACKGROUND } from './background.js';
3
3
  import { bpmnLaneOf, bpmnPoolOf } from './facts.js';
4
4
  import { BPMN_ROLE } from './roles.js';
package/dist/facts.js CHANGED
@@ -1,4 +1,4 @@
1
- import { backgroundInstanceZones, backgroundPlot, containingFrame, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { backgroundInstanceZones, backgroundPlot, containingFrame, } from '@formicoidea/labre-core/interchange';
2
2
  import { BPMN_POOL_BACKGROUND } from './background.js';
3
3
  /**
4
4
  * Where a BPMN artefact SITS — the facts, answerable without a `BlockStdScope`.
package/dist/import.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type InterchangeImportContext, type InterchangeImportResult } from '@formicoidea/labre-core/blocks/surface';
1
+ import type { InterchangeImportContext, InterchangeImportResult } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
3
3
  /**
4
4
  * The remarks whose wording is FIXED, as `[key, English]` pairs — one entry
package/dist/import.js CHANGED
@@ -1,5 +1,5 @@
1
- import { InterchangeImportError, } from '@formicoidea/labre-core/blocks/surface';
2
- import { ConnectorMode, PointStyle, StrokeStyle } from '@formicoidea/labre-core/model';
1
+ import { InterchangeImportError, readXmlElement, } from '@formicoidea/labre-core/interchange';
2
+ import { ConnectorMode, PointStyle, StrokeStyle, } from '@formicoidea/labre-core/model/headless';
3
3
  import { Bound } from '@formicoidea/labre-core/global/gfx';
4
4
  import { ASSOCIATION_STROKE, ASSOCIATION_WIDTH, bpmnLabelMode, MESSAGE_STROKE, MESSAGE_WIDTH, NODE_SIZE, POOL_BAND_WIDTH, POOL_REF_HEIGHT, POOL_REF_WIDTH, SEQUENCE_STROKE, SEQUENCE_WIDTH, } from './consts.js';
5
5
  import { BPMN_FORMAT_ID, BPMN_NS, BPMN_OWN_DECLARATIONS, BPMN_SCOPE, BPMN_XML_OF_KIND, escapeAttr, escapeText, } from './export.js';
@@ -191,15 +191,13 @@ export const BPMN_IMPORT_ERRORS = {
191
191
  *
192
192
  * ## Reading XML by hand, and why
193
193
  *
194
- * The tree is walked child by child rather than through
195
- * `getElementsByTagNameNS`, which happy-dom does not implement for a parsed XML
196
- * document: it answers an empty list for every namespaced query, so a unit
197
- * suite built on it would pass by asserting nothing. The same environment
198
- * decodes neither numeric character references nor `'` in an attribute
199
- * value, which is why the escaped-label round trip is pinned in chromium
200
- * (`integration-test/src/__tests__/edgeless/bpmn.spec.ts`) and not in the unit
201
- * spec. Both traps are `export.unit.spec.ts`'s findings, inherited rather than
202
- * rediscovered.
194
+ * The file is read by the platform's `readXml` and walked through its
195
+ * DOM-shaped view (`xml-element.ts` in the surface package), child by child,
196
+ * namespaces resolved. Not `DOMParser`: the P0 import spike measured this
197
+ * reader on a server, where it needed a DOM installed (jsdom) and grew
198
+ * quadratically with the file — 5.1 s of CPU at 2 000 tasks. Without a DOM it
199
+ * runs in a server function, in labre-mcp and in a unit test alike (ADR 0012,
200
+ * P3 as amended 2026-10-14), in time linear in the file.
203
201
  *
204
202
  * ## Failure is an exception, not a report
205
203
  *
@@ -293,11 +291,13 @@ export const BPMN_KIND_OF_XML = new Map(Object.entries(BPMN_XML_OF_KIND).map(([k
293
291
  * there is no such thing in BPMN, where every connecting object is one of
294
292
  * these three.
295
293
  */
296
- const EDGE_ROLE_OF_ELEMENT = {
294
+ // No prototype: it is read by a name the file chose, and `<constructor>` is
295
+ // one (security review, 2026-10-14).
296
+ const EDGE_ROLE_OF_ELEMENT = Object.assign(Object.create(null), {
297
297
  sequenceFlow: BPMN_ROLE.sequenceFlow,
298
298
  messageFlow: BPMN_ROLE.messageFlow,
299
299
  association: BPMN_ROLE.association,
300
- };
300
+ });
301
301
  /**
302
302
  * The colour extensions D5 quarantines (case 1), by namespace and by prefix.
303
303
  *
@@ -368,20 +368,14 @@ const NOT_A_BODY = new Set([
368
368
  'multiInstanceLoopCharacteristics',
369
369
  'standardLoopCharacteristics',
370
370
  ]);
371
- /* ── Reading the DOM by hand ──────────────────────────────────────────── */
372
- /** The `Node.nodeType`s a fragment can be made of, spelled rather than numbered. */
373
- const ELEMENT_NODE = 1;
374
- const TEXT_NODE = 3;
375
- const CDATA_NODE = 4;
376
- const PI_NODE = 7;
377
- const COMMENT_NODE = 8;
371
+ /* ── Reading the tree by hand ─────────────────────────────────────────── */
378
372
  /** The element children of `node`, in document order. */
379
373
  function childrenOf(node) {
380
- return Array.from(node.children);
374
+ return node.children;
381
375
  }
382
376
  /** Every element under `node`, at any depth, in document order. */
383
377
  function descendantsOf(node) {
384
- return childrenOf(node).flatMap(child => [child, ...descendantsOf(child)]);
378
+ return node.descendants();
385
379
  }
386
380
  /** Is this element in the BPMN MODEL namespace, whatever prefix it wears? */
387
381
  function isModel(element) {
@@ -428,53 +422,71 @@ function isColourAttr(attr) {
428
422
  * (XML 1.0 §3.3.3 — only a character reference survives normalization).
429
423
  */
430
424
  function fragmentOf(element) {
431
- const attrs = Array.from(element.attributes)
432
- .map(attr => ` ${attr.name}="${escapeAttr(attr.value)}"`)
433
- .join('');
434
- const parts = [];
435
- for (const child of Array.from(element.childNodes)) {
436
- if (child.nodeType === ELEMENT_NODE) {
437
- parts.push(fragmentOf(child));
438
- }
439
- else if (child.nodeType === TEXT_NODE || child.nodeType === CDATA_NODE) {
440
- parts.push(escapeText(child.nodeValue ?? ''));
441
- }
442
- else if (child.nodeType === COMMENT_NODE) {
443
- // A comment inside a vendor extension is documentation somebody wrote by
444
- // hand, and dropping it while promising the fragment back "character for
445
- // character" would make that promise false in the one place a human
446
- // would notice.
447
- parts.push(`<!--${child.nodeValue ?? ''}-->`);
448
- }
449
- else if (child.nodeType === PI_NODE) {
450
- const instruction = child;
451
- parts.push(`<?${instruction.target} ${instruction.data}?>`);
425
+ const head = (node) => `<${node.name}${Object.entries(node.attrs)
426
+ .map(([name, value]) => ` ${name}="${escapeAttr(value)}"`)
427
+ .join('')}`;
428
+ // An explicit stack: a quarantined fragment is as deep as the file made it.
429
+ const stack = [
430
+ { node: element.node, parts: [], next: 0 },
431
+ ];
432
+ let written = '';
433
+ while (stack.length > 0) {
434
+ const frame = stack[stack.length - 1];
435
+ const content = frame.node.content ?? frame.node.children;
436
+ if (frame.next < content.length) {
437
+ const entry = content[frame.next];
438
+ frame.next += 1;
439
+ if (!('kind' in entry)) {
440
+ stack.push({ node: entry, parts: [], next: 0 });
441
+ }
442
+ else if (entry.kind === 'text') {
443
+ frame.parts.push(escapeText(entry.value));
444
+ }
445
+ else if (entry.kind === 'comment') {
446
+ // A comment inside a vendor extension is documentation somebody wrote
447
+ // by hand, and dropping it while promising the fragment back
448
+ // "character for character" would make that promise false in the one
449
+ // place a human would notice.
450
+ frame.parts.push(`<!--${entry.value}-->`);
451
+ }
452
+ else {
453
+ frame.parts.push(`<?${entry.target} ${entry.data}?>`);
454
+ }
455
+ continue;
452
456
  }
457
+ stack.pop();
458
+ const { node, parts } = frame;
459
+ const text = parts.length === 0
460
+ ? `${head(node)} />`
461
+ : `${head(node)}>${parts.join('')}</${node.name}>`;
462
+ const parent = stack[stack.length - 1];
463
+ if (parent)
464
+ parent.parts.push(text);
465
+ else
466
+ written = text;
453
467
  }
454
- const name = element.nodeName;
455
- if (parts.length === 0)
456
- return `<${name}${attrs} />`;
457
- return `<${name}${attrs}>${parts.join('')}</${name}>`;
468
+ return written;
458
469
  }
459
470
  /**
460
471
  * The document, or an exception naming what is wrong with it.
461
472
  *
462
- * `DOMParser` reports a malformed document as a `parsererror` element rather
463
- * than by throwing, which is a well-formedness answer nobody asked for in the
464
- * shape of a document — so it is turned into the exception the caller can
465
- * actually act on.
473
+ * `readXml` never throws: it reads what it can and says what it had to
474
+ * recover from. Anything it recovered from is refused here, with its first
475
+ * remark as the detail, as `DOMParser` refused it before. A `DOCTYPE` is not
476
+ * an error: it is skipped, and no entity it declares is ever expanded, so a
477
+ * `&name;` it declares stays those characters (the spike's billion-laughs and
478
+ * XXE files, `__tests__/import-hostile.unit.spec.ts`).
466
479
  */
467
480
  function parseDefinitions(source) {
468
- const doc = new DOMParser().parseFromString(source, 'application/xml');
469
- const error = doc.querySelector('parsererror');
470
- if (error) {
471
- const detail = (error.textContent ?? '').trim().slice(0, 200);
481
+ const { document, root } = readXmlElement(source, { conformant: true });
482
+ const [complaint] = document.notes;
483
+ if (complaint !== undefined) {
484
+ const detail = complaint.slice(0, 200);
472
485
  throw new InterchangeImportError(`This file is not well-formed XML, so no BPMN can be read out of it: ${detail}`, {
473
486
  messageKey: BPMN_IMPORT_ERRORS.malformedXml[0],
474
487
  messageParams: { detail },
475
488
  });
476
489
  }
477
- const root = doc.documentElement;
478
490
  if (!root || root.localName !== 'definitions') {
479
491
  const tag = root?.localName ?? 'nothing';
480
492
  throw new InterchangeImportError(`A BPMN file opens on <definitions>; this one opens on <${tag}>.`, {
@@ -581,8 +593,16 @@ function diagramIndex(definitions) {
581
593
  * everywhere in this format, and everything else on an element Labre draws is
582
594
  * something Labre does not model and must not lose.
583
595
  */
584
- const READ_ATTRS = {
585
- definitions: ['id', 'name', 'targetNamespace', 'exporter', 'exporterVersion'],
596
+ // No prototype, like `EDGE_ROLE_OF_ELEMENT`: `READ_ATTRS.constructor` was a
597
+ // function, and `.includes` on it the untyped TypeError the review found.
598
+ const READ_ATTRS = Object.assign(Object.create(null), {
599
+ definitions: [
600
+ 'id',
601
+ 'name',
602
+ 'targetNamespace',
603
+ 'exporter',
604
+ 'exporterVersion',
605
+ ],
586
606
  participant: ['id', 'name', 'processRef'],
587
607
  process: ['id', 'name', 'isExecutable'],
588
608
  lane: ['id', 'name'],
@@ -594,9 +614,15 @@ const READ_ATTRS = {
594
614
  messageFlow: ['id', 'name', 'sourceRef', 'targetRef'],
595
615
  // `associationDirection` is read only when it says what the exporter would
596
616
  // write anyway; see the edge reader for the other values.
597
- association: ['id', 'name', 'sourceRef', 'targetRef', 'associationDirection'],
617
+ association: [
618
+ 'id',
619
+ 'name',
620
+ 'sourceRef',
621
+ 'targetRef',
622
+ 'associationDirection',
623
+ ],
598
624
  '': ['id', 'name'],
599
- };
625
+ });
600
626
  /** What a `BPMNShape` says that the drawing already carries. */
601
627
  const READ_SHAPE_ATTRS = [
602
628
  'id',
@@ -685,8 +711,14 @@ function layOutTheUndrawn(drafts, minted) {
685
711
  draft.kind !== 'edge' &&
686
712
  draft.kind !== 'group');
687
713
  const boxes = placed.map(boundOf);
688
- const maxX = boxes.length > 0 ? Math.max(...boxes.map(box => box.x + box.w)) : 0;
689
- const minY = boxes.length > 0 ? Math.min(...boxes.map(box => box.y)) : 0;
714
+ const maxX = boxes.length > 0
715
+ ? boxes
716
+ .map(box => box.x + box.w)
717
+ .reduce((a, b) => Math.max(a, b), -Infinity)
718
+ : 0;
719
+ const minY = boxes.length > 0
720
+ ? boxes.map(box => box.y).reduce((a, b) => Math.min(a, b), Infinity)
721
+ : 0;
690
722
  let seat = 0;
691
723
  for (const draft of drafts) {
692
724
  if (!draft.needsLayout)
@@ -718,10 +750,16 @@ function layOutTheUndrawn(drafts, minted) {
718
750
  .map(boundOf);
719
751
  if (inside.length === 0)
720
752
  return;
721
- const left = Math.min(...inside.map(box => box.x)) - MINTED_POOL_PADDING;
722
- const top = Math.min(...inside.map(box => box.y)) - MINTED_POOL_PADDING;
723
- const right = Math.max(...inside.map(box => box.x + box.w)) + MINTED_POOL_PADDING;
724
- const bottom = Math.max(...inside.map(box => box.y + box.h)) + MINTED_POOL_PADDING;
753
+ const left = inside.map(box => box.x).reduce((a, b) => Math.min(a, b), Infinity) -
754
+ MINTED_POOL_PADDING;
755
+ const top = inside.map(box => box.y).reduce((a, b) => Math.min(a, b), Infinity) -
756
+ MINTED_POOL_PADDING;
757
+ const right = inside
758
+ .map(box => box.x + box.w)
759
+ .reduce((a, b) => Math.max(a, b), -Infinity) + MINTED_POOL_PADDING;
760
+ const bottom = inside
761
+ .map(box => box.y + box.h)
762
+ .reduce((a, b) => Math.max(a, b), -Infinity) + MINTED_POOL_PADDING;
725
763
  minted.props.xywh = new Bound(
726
764
  // The name band is drawn INSIDE the frame and is not part of the plot, so
727
765
  // the frame starts a band's width further left than the work does.
@@ -814,12 +852,17 @@ export function importBpmnXml(source, context = {}) {
814
852
  }
815
853
  seenSourceIds.add(sourceId);
816
854
  };
855
+ /*
856
+ * The four keep-helpers fill their bags IN PLACE: a copy of the bag per entry
857
+ * made 20 000 carried children 7.4 s and 20 000 unknown attributes over a
858
+ * minute (security review, 2026-10-14). The bags have no prototype, because
859
+ * their keys are names and ids the file chose (`__proto__` among them).
860
+ */
817
861
  /** One attribute, kept under the scope of the element that carried it. */
818
862
  const carryAttr = (payload, scope, name, value) => {
819
- payload.attrs = {
820
- ...payload.attrs,
821
- [scope]: { ...payload.attrs?.[scope], [name]: value },
822
- };
863
+ const attrs = (payload.attrs ??= Object.create(null));
864
+ (attrs[scope] ??= Object.create(null))[name] =
865
+ value;
823
866
  carried++;
824
867
  };
825
868
  /**
@@ -830,10 +873,8 @@ export function importBpmnXml(source, context = {}) {
830
873
  * one flow, one of them generic, is a worse report than one that is precise.
831
874
  */
832
875
  const carryChild = (payload, scope, child, sourceId, announce = true) => {
833
- payload.children = {
834
- ...payload.children,
835
- [scope]: [...(payload.children?.[scope] ?? []), fragmentOf(child)],
836
- };
876
+ const children = (payload.children ??= Object.create(null));
877
+ (children[scope] ??= []).push(fragmentOf(child));
837
878
  carried++;
838
879
  if (!announce)
839
880
  return;
@@ -848,10 +889,8 @@ export function importBpmnXml(source, context = {}) {
848
889
  };
849
890
  /** One diagram fragment, kept under the scope of what it DRAWS. */
850
891
  const carryDi = (payload, scope, fragment) => {
851
- payload.di = {
852
- ...payload.di,
853
- [scope]: [...(payload.di?.[scope] ?? []), fragment],
854
- };
892
+ const di = (payload.di ??= Object.create(null));
893
+ (di[scope] ??= []).push(fragment);
855
894
  carried++;
856
895
  };
857
896
  const quarantine = (payload, fragment, reason, entry,
@@ -860,10 +899,7 @@ export function importBpmnXml(source, context = {}) {
860
899
  // `payload.quarantined[].reason` (data, never translated); only the
861
900
  // REPORT note below carries the key.
862
901
  reasonKey) => {
863
- payload.quarantined = [
864
- ...(payload.quarantined ?? []),
865
- { fragment, reason },
866
- ];
902
+ (payload.quarantined ??= []).push({ fragment, reason });
867
903
  quarantined++;
868
904
  note({
869
905
  kind: 'quarantined',
@@ -1298,13 +1334,10 @@ export function importBpmnXml(source, context = {}) {
1298
1334
  const drawn = shapes.get(inner)?.element ?? diEdges.get(inner)?.element;
1299
1335
  if (!drawn)
1300
1336
  continue;
1301
- payload.quarantined = [
1302
- ...(payload.quarantined ?? []),
1303
- {
1304
- fragment: fragmentOf(drawn),
1305
- reason: BPMN_QUARANTINE_REASON.expanded,
1306
- },
1307
- ];
1337
+ (payload.quarantined ??= []).push({
1338
+ fragment: fragmentOf(drawn),
1339
+ reason: BPMN_QUARANTINE_REASON.expanded,
1340
+ });
1308
1341
  }
1309
1342
  continue;
1310
1343
  }
@@ -0,0 +1,74 @@
1
+ import type { InterchangeFormat, InterchangeImportCapability } from '@formicoidea/labre-core/interchange';
2
+ /**
3
+ * `@labre/affine-gfx-bpmn/interchange` — BPMN's formats and its two READERS,
4
+ * with no editor behind them (ADR 0012, P3 as amended 2026-10-14).
5
+ *
6
+ * What a server needs to read a `.bpmn` or an `.svg` as a BPMN board: the
7
+ * format declarations and the import capabilities, whose `run` is a pure
8
+ * function of text. The EXPORT capability is not here and cannot be: it picks
9
+ * the artefacts out of live element models (`bpmnBoardFrom`, by `instanceof`),
10
+ * which is the editor graph this entry exists to leave out. It stays in
11
+ * `interchange.ts`, which re-exports everything below.
12
+ *
13
+ * Held by `interchange-headless.unit.spec.ts` in `packages/affine/all`.
14
+ */
15
+ /**
16
+ * BPMN 2.0 XML. **Semantic** — the file carries a model, not a picture, so it
17
+ * takes the whole preservation contract the day an importer is written.
18
+ *
19
+ * `application/xml` and not `text/xml`: the file is not meant to be read as
20
+ * text by whatever opens it, and `.bpmn` is the extension every BPMN tool
21
+ * watches for.
22
+ *
23
+ * `.xml` rides along BEHIND it, and the order is the whole of the difference:
24
+ * the first extension is the one a download is given, so Labre still writes
25
+ * `.bpmn`, and the rest are what a picker offers and what an auto-detecting
26
+ * host indexes on. Half the tools in the wild write the same bytes under the
27
+ * generic extension, and a filter that refused them would refuse a valid
28
+ * process for the sake of a filename — the same argument the shared `FileTypes`
29
+ * table made when this filter lived there, kept now that the filter is built
30
+ * from the format itself. What the file actually IS is decided by the reader,
31
+ * which throws on anything that is not a BPMN `<definitions>`.
32
+ */
33
+ export declare const BPMN_XML_EXTENSION = ".bpmn";
34
+ export declare const BPMN_XML_MIME = "application/xml";
35
+ export declare const BPMN_XML_FORMAT: InterchangeFormat;
36
+ /**
37
+ * `bpmn:bpmn:import` — a `.bpmn` file as a board.
38
+ *
39
+ * A thin adapter over {@link importBpmnXml} and nothing else, which is the
40
+ * whole of what a capability is: the parser is a pure function of a string, so
41
+ * the registry entry has nothing to add to it and deliberately adds nothing.
42
+ * A reader that threw its arms around a surface here would be a reader
43
+ * labre-mcp could not call.
44
+ */
45
+ export declare const BPMN_XML_IMPORT: InterchangeImportCapability;
46
+ /**
47
+ * SVG. **Visual** — the file carries a rendering, not a model.
48
+ *
49
+ * A separate format object from `BPMN_XML_FORMAT` and deliberately not a shared
50
+ * singleton with Wardley's: ADR 0012's unit is the TRIPLE, and the ADR
51
+ * explicitly rejects "one capability per format, with the framework inferred
52
+ * from the file" — a `.svg` is read by several frameworks and the inference is
53
+ * exactly the guess the ADR forbids everywhere else. The three constants come
54
+ * from the parser's own package so the two declarations cannot drift into
55
+ * disagreeing about the extension or the mime a picker filters on.
56
+ */
57
+ export declare const BPMN_SVG_FORMAT: InterchangeFormat;
58
+ /**
59
+ * `bpmn:svg:import` — an SVG as a sketch, best effort.
60
+ *
61
+ * **The heuristics statement and the known failure modes this capability owes
62
+ * (ADR 0012, open question 2) are the module documentation of
63
+ * `packages/affine/blocks/surface/src/extensions/svg-sketch.ts`.** They are
64
+ * written once, there, because BPMN and Wardley wrap the SAME parser and
65
+ * therefore make the same guesses; a framework that ever wants narrower or
66
+ * wider recognition writes its own parser and its own paragraph beside it.
67
+ *
68
+ * What lands is a level-1 sketch (ADR 0007) — plain shapes, brush strokes and
69
+ * editable free text — that the author then PROMOTES into BPMN artefacts.
70
+ * Nothing here decides that a rounded rectangle in somebody's picture was a
71
+ * task, and no `interchange` payload is written, because a visual round-trip
72
+ * would be a re-render (P2).
73
+ */
74
+ export declare const BPMN_SVG_IMPORT: InterchangeImportCapability;
@@ -0,0 +1,100 @@
1
+ import { interchangeCapabilityId, parseSvgSketch, SVG_SKETCH_EXTENSION, SVG_SKETCH_FORMAT_ID, SVG_SKETCH_MIME, } from '@formicoidea/labre-core/interchange';
2
+ import { BPMN_FORMAT_ID } from './export.js';
3
+ import { importBpmnXml } from './import.js';
4
+ /**
5
+ * `@labre/affine-gfx-bpmn/interchange` — BPMN's formats and its two READERS,
6
+ * with no editor behind them (ADR 0012, P3 as amended 2026-10-14).
7
+ *
8
+ * What a server needs to read a `.bpmn` or an `.svg` as a BPMN board: the
9
+ * format declarations and the import capabilities, whose `run` is a pure
10
+ * function of text. The EXPORT capability is not here and cannot be: it picks
11
+ * the artefacts out of live element models (`bpmnBoardFrom`, by `instanceof`),
12
+ * which is the editor graph this entry exists to leave out. It stays in
13
+ * `interchange.ts`, which re-exports everything below.
14
+ *
15
+ * Held by `interchange-headless.unit.spec.ts` in `packages/affine/all`.
16
+ */
17
+ /* ── The format ───────────────────────────────────────────────────────── */
18
+ /**
19
+ * BPMN 2.0 XML. **Semantic** — the file carries a model, not a picture, so it
20
+ * takes the whole preservation contract the day an importer is written.
21
+ *
22
+ * `application/xml` and not `text/xml`: the file is not meant to be read as
23
+ * text by whatever opens it, and `.bpmn` is the extension every BPMN tool
24
+ * watches for.
25
+ *
26
+ * `.xml` rides along BEHIND it, and the order is the whole of the difference:
27
+ * the first extension is the one a download is given, so Labre still writes
28
+ * `.bpmn`, and the rest are what a picker offers and what an auto-detecting
29
+ * host indexes on. Half the tools in the wild write the same bytes under the
30
+ * generic extension, and a filter that refused them would refuse a valid
31
+ * process for the sake of a filename — the same argument the shared `FileTypes`
32
+ * table made when this filter lived there, kept now that the filter is built
33
+ * from the format itself. What the file actually IS is decided by the reader,
34
+ * which throws on anything that is not a BPMN `<definitions>`.
35
+ */
36
+ export const BPMN_XML_EXTENSION = '.bpmn';
37
+ export const BPMN_XML_MIME = 'application/xml';
38
+ export const BPMN_XML_FORMAT = {
39
+ id: BPMN_FORMAT_ID,
40
+ tier: 'semantic',
41
+ extensions: [BPMN_XML_EXTENSION, '.xml'],
42
+ mime: BPMN_XML_MIME,
43
+ };
44
+ /**
45
+ * `bpmn:bpmn:import` — a `.bpmn` file as a board.
46
+ *
47
+ * A thin adapter over {@link importBpmnXml} and nothing else, which is the
48
+ * whole of what a capability is: the parser is a pure function of a string, so
49
+ * the registry entry has nothing to add to it and deliberately adds nothing.
50
+ * A reader that threw its arms around a surface here would be a reader
51
+ * labre-mcp could not call.
52
+ */
53
+ export const BPMN_XML_IMPORT = {
54
+ id: interchangeCapabilityId('bpmn', BPMN_XML_FORMAT.id, 'import'),
55
+ framework: 'bpmn',
56
+ format: BPMN_XML_FORMAT,
57
+ direction: 'import',
58
+ run: importBpmnXml,
59
+ };
60
+ /* ── The visual tier ──────────────────────────────────────────────────── */
61
+ /**
62
+ * SVG. **Visual** — the file carries a rendering, not a model.
63
+ *
64
+ * A separate format object from `BPMN_XML_FORMAT` and deliberately not a shared
65
+ * singleton with Wardley's: ADR 0012's unit is the TRIPLE, and the ADR
66
+ * explicitly rejects "one capability per format, with the framework inferred
67
+ * from the file" — a `.svg` is read by several frameworks and the inference is
68
+ * exactly the guess the ADR forbids everywhere else. The three constants come
69
+ * from the parser's own package so the two declarations cannot drift into
70
+ * disagreeing about the extension or the mime a picker filters on.
71
+ */
72
+ export const BPMN_SVG_FORMAT = {
73
+ id: SVG_SKETCH_FORMAT_ID,
74
+ tier: 'visual',
75
+ extensions: [SVG_SKETCH_EXTENSION],
76
+ mime: SVG_SKETCH_MIME,
77
+ };
78
+ /**
79
+ * `bpmn:svg:import` — an SVG as a sketch, best effort.
80
+ *
81
+ * **The heuristics statement and the known failure modes this capability owes
82
+ * (ADR 0012, open question 2) are the module documentation of
83
+ * `packages/affine/blocks/surface/src/extensions/svg-sketch.ts`.** They are
84
+ * written once, there, because BPMN and Wardley wrap the SAME parser and
85
+ * therefore make the same guesses; a framework that ever wants narrower or
86
+ * wider recognition writes its own parser and its own paragraph beside it.
87
+ *
88
+ * What lands is a level-1 sketch (ADR 0007) — plain shapes, brush strokes and
89
+ * editable free text — that the author then PROMOTES into BPMN artefacts.
90
+ * Nothing here decides that a rounded rectangle in somebody's picture was a
91
+ * task, and no `interchange` payload is written, because a visual round-trip
92
+ * would be a re-render (P2).
93
+ */
94
+ export const BPMN_SVG_IMPORT = {
95
+ id: interchangeCapabilityId('bpmn', BPMN_SVG_FORMAT.id, 'import'),
96
+ framework: 'bpmn',
97
+ format: BPMN_SVG_FORMAT,
98
+ direction: 'import',
99
+ run: parseSvgSketch,
100
+ };
@@ -1,45 +1,7 @@
1
- import type { InterchangeCapability, InterchangeExportCapability, InterchangeFormat, InterchangeImportCapability } from '@formicoidea/labre-core/blocks/surface';
1
+ import type { InterchangeCapability, InterchangeExportCapability } from '@formicoidea/labre-core/blocks/surface';
2
2
  import type { GfxPrimitiveElementModel } from '@formicoidea/labre-core/std/gfx';
3
3
  import { type BpmnExportBoard } from './export.js';
4
- /**
5
- * BPMN's entries in the interchange registry (`docs/adr/0012`, P1).
6
- *
7
- * Both directions of one format, declared as two capabilities and not as one
8
- * symmetric thing: `.bpmn` OUT shipped in #149, `.bpmn` IN is the worked
9
- * example D1–D6 specifies, and each makes its own promises. The registry says
10
- * so in the only way it can — a row per triple.
11
- *
12
- * They share the FORMAT object, deliberately. `bpmn` is the id under which
13
- * foreign matter rides on an element (D2), so a reader and a writer that
14
- * disagreed about it would write payloads the other could not find; there is
15
- * one `BPMN_XML_FORMAT` and both point at it.
16
- *
17
- * Everything here is pure. Neither serializer nor parser has ever had a `std`
18
- * in sight, and this file adds no editor to either: it picks the artefacts the
19
- * writer speaks about out of a surface's elements, and hands the reader's
20
- * output straight back to whoever asked for it.
21
- */
22
- /**
23
- * BPMN 2.0 XML. **Semantic** — the file carries a model, not a picture, so it
24
- * takes the whole preservation contract the day an importer is written.
25
- *
26
- * `application/xml` and not `text/xml`: the file is not meant to be read as
27
- * text by whatever opens it, and `.bpmn` is the extension every BPMN tool
28
- * watches for.
29
- *
30
- * `.xml` rides along BEHIND it, and the order is the whole of the difference:
31
- * the first extension is the one a download is given, so Labre still writes
32
- * `.bpmn`, and the rest are what a picker offers and what an auto-detecting
33
- * host indexes on. Half the tools in the wild write the same bytes under the
34
- * generic extension, and a filter that refused them would refuse a valid
35
- * process for the sake of a filename — the same argument the shared `FileTypes`
36
- * table made when this filter lived there, kept now that the filter is built
37
- * from the format itself. What the file actually IS is decided by the reader,
38
- * which throws on anything that is not a BPMN `<definitions>`.
39
- */
40
- export declare const BPMN_XML_EXTENSION = ".bpmn";
41
- export declare const BPMN_XML_MIME = "application/xml";
42
- export declare const BPMN_XML_FORMAT: InterchangeFormat;
4
+ export { BPMN_SVG_FORMAT, BPMN_SVG_IMPORT, BPMN_XML_EXTENSION, BPMN_XML_FORMAT, BPMN_XML_IMPORT, BPMN_XML_MIME, } from './interchange-headless.js';
43
5
  /**
44
6
  * The artefacts the exporter speaks about, picked out of a surface's elements
45
7
  * and kept in the order they were given.
@@ -95,44 +57,5 @@ export declare function bpmnBoardFrom(elements: readonly GfxPrimitiveElementMode
95
57
  export declare function bpmnSafeFilename(raw: string | undefined): string;
96
58
  /** `bpmn:bpmn:export` — the registry's first capability. */
97
59
  export declare const BPMN_XML_EXPORT: InterchangeExportCapability;
98
- /**
99
- * `bpmn:bpmn:import` — a `.bpmn` file as a board.
100
- *
101
- * A thin adapter over {@link importBpmnXml} and nothing else, which is the
102
- * whole of what a capability is: the parser is a pure function of a string, so
103
- * the registry entry has nothing to add to it and deliberately adds nothing.
104
- * A reader that threw its arms around a surface here would be a reader
105
- * labre-mcp could not call.
106
- */
107
- export declare const BPMN_XML_IMPORT: InterchangeImportCapability;
108
- /**
109
- * SVG. **Visual** — the file carries a rendering, not a model.
110
- *
111
- * A separate format object from `BPMN_XML_FORMAT` and deliberately not a shared
112
- * singleton with Wardley's: ADR 0012's unit is the TRIPLE, and the ADR
113
- * explicitly rejects "one capability per format, with the framework inferred
114
- * from the file" — a `.svg` is read by several frameworks and the inference is
115
- * exactly the guess the ADR forbids everywhere else. The three constants come
116
- * from the parser's own package so the two declarations cannot drift into
117
- * disagreeing about the extension or the mime a picker filters on.
118
- */
119
- export declare const BPMN_SVG_FORMAT: InterchangeFormat;
120
- /**
121
- * `bpmn:svg:import` — an SVG as a sketch, best effort.
122
- *
123
- * **The heuristics statement and the known failure modes this capability owes
124
- * (ADR 0012, open question 2) are the module documentation of
125
- * `packages/affine/blocks/surface/src/extensions/svg-sketch.ts`.** They are
126
- * written once, there, because BPMN and Wardley wrap the SAME parser and
127
- * therefore make the same guesses; a framework that ever wants narrower or
128
- * wider recognition writes its own parser and its own paragraph beside it.
129
- *
130
- * What lands is a level-1 sketch (ADR 0007) — plain shapes, brush strokes and
131
- * editable free text — that the author then PROMOTES into BPMN artefacts.
132
- * Nothing here decides that a rounded rectangle in somebody's picture was a
133
- * task, and no `interchange` payload is written, because a visual round-trip
134
- * would be a re-render (P2).
135
- */
136
- export declare const BPMN_SVG_IMPORT: InterchangeImportCapability;
137
60
  /** Everything BPMN registers, in one list the view extension can hand over. */
138
61
  export declare const BPMN_INTERCHANGE: readonly InterchangeCapability[];
@@ -1,11 +1,12 @@
1
- import { interchangeCapabilityId, LEGEND_ROLE, parseSvgSketch, SVG_SKETCH_EXTENSION, SVG_SKETCH_FORMAT_ID, SVG_SKETCH_MIME, } from '@formicoidea/labre-core/blocks/surface';
1
+ import { interchangeCapabilityId, LEGEND_ROLE, } from '@formicoidea/labre-core/blocks/surface';
2
2
  import { BpmnNodeElementModel, BpmnPoolElementModel, ConnectorElementModel, GroupElementModel, TextElementModel, } from '@formicoidea/labre-core/model';
3
3
  import { fillPlaceholders } from '@formicoidea/labre-core/shared/services';
4
4
  import { safeFilename } from '@formicoidea/labre-core/shared/utils';
5
- import { BPMN_FORMAT_ID, exportBpmnXmlWithWarnings, } from './export.js';
5
+ import { exportBpmnXmlWithWarnings } from './export.js';
6
6
  import { bpmnPoolOf } from './facts.js';
7
- import { importBpmnXml } from './import.js';
7
+ import { BPMN_SVG_IMPORT, BPMN_XML_EXTENSION, BPMN_XML_FORMAT, BPMN_XML_IMPORT, BPMN_XML_MIME, } from './interchange-headless.js';
8
8
  import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from './roles.js';
9
+ export { BPMN_SVG_FORMAT, BPMN_SVG_IMPORT, BPMN_XML_EXTENSION, BPMN_XML_FORMAT, BPMN_XML_IMPORT, BPMN_XML_MIME, } from './interchange-headless.js';
9
10
  /**
10
11
  * BPMN's entries in the interchange registry (`docs/adr/0012`, P1).
11
12
  *
@@ -24,33 +25,6 @@ import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from './roles.js';
24
25
  * writer speaks about out of a surface's elements, and hands the reader's
25
26
  * output straight back to whoever asked for it.
26
27
  */
27
- /* ── The format ───────────────────────────────────────────────────────── */
28
- /**
29
- * BPMN 2.0 XML. **Semantic** — the file carries a model, not a picture, so it
30
- * takes the whole preservation contract the day an importer is written.
31
- *
32
- * `application/xml` and not `text/xml`: the file is not meant to be read as
33
- * text by whatever opens it, and `.bpmn` is the extension every BPMN tool
34
- * watches for.
35
- *
36
- * `.xml` rides along BEHIND it, and the order is the whole of the difference:
37
- * the first extension is the one a download is given, so Labre still writes
38
- * `.bpmn`, and the rest are what a picker offers and what an auto-detecting
39
- * host indexes on. Half the tools in the wild write the same bytes under the
40
- * generic extension, and a filter that refused them would refuse a valid
41
- * process for the sake of a filename — the same argument the shared `FileTypes`
42
- * table made when this filter lived there, kept now that the filter is built
43
- * from the format itself. What the file actually IS is decided by the reader,
44
- * which throws on anything that is not a BPMN `<definitions>`.
45
- */
46
- export const BPMN_XML_EXTENSION = '.bpmn';
47
- export const BPMN_XML_MIME = 'application/xml';
48
- export const BPMN_XML_FORMAT = {
49
- id: BPMN_FORMAT_ID,
50
- tier: 'semantic',
51
- extensions: [BPMN_XML_EXTENSION, '.xml'],
52
- mime: BPMN_XML_MIME,
53
- };
54
28
  /* ── Pure board helpers ───────────────────────────────────────────────── */
55
29
  /**
56
30
  * Everything a generated legend is made of: the group the legend gesture stamps
@@ -269,63 +243,6 @@ export const BPMN_XML_EXPORT = {
269
243
  direction: 'export',
270
244
  run: runBpmnXmlExport,
271
245
  };
272
- /**
273
- * `bpmn:bpmn:import` — a `.bpmn` file as a board.
274
- *
275
- * A thin adapter over {@link importBpmnXml} and nothing else, which is the
276
- * whole of what a capability is: the parser is a pure function of a string, so
277
- * the registry entry has nothing to add to it and deliberately adds nothing.
278
- * A reader that threw its arms around a surface here would be a reader
279
- * labre-mcp could not call.
280
- */
281
- export const BPMN_XML_IMPORT = {
282
- id: interchangeCapabilityId('bpmn', BPMN_XML_FORMAT.id, 'import'),
283
- framework: 'bpmn',
284
- format: BPMN_XML_FORMAT,
285
- direction: 'import',
286
- run: importBpmnXml,
287
- };
288
- /* ── The visual tier ──────────────────────────────────────────────────── */
289
- /**
290
- * SVG. **Visual** — the file carries a rendering, not a model.
291
- *
292
- * A separate format object from `BPMN_XML_FORMAT` and deliberately not a shared
293
- * singleton with Wardley's: ADR 0012's unit is the TRIPLE, and the ADR
294
- * explicitly rejects "one capability per format, with the framework inferred
295
- * from the file" — a `.svg` is read by several frameworks and the inference is
296
- * exactly the guess the ADR forbids everywhere else. The three constants come
297
- * from the parser's own package so the two declarations cannot drift into
298
- * disagreeing about the extension or the mime a picker filters on.
299
- */
300
- export const BPMN_SVG_FORMAT = {
301
- id: SVG_SKETCH_FORMAT_ID,
302
- tier: 'visual',
303
- extensions: [SVG_SKETCH_EXTENSION],
304
- mime: SVG_SKETCH_MIME,
305
- };
306
- /**
307
- * `bpmn:svg:import` — an SVG as a sketch, best effort.
308
- *
309
- * **The heuristics statement and the known failure modes this capability owes
310
- * (ADR 0012, open question 2) are the module documentation of
311
- * `packages/affine/blocks/surface/src/extensions/svg-sketch.ts`.** They are
312
- * written once, there, because BPMN and Wardley wrap the SAME parser and
313
- * therefore make the same guesses; a framework that ever wants narrower or
314
- * wider recognition writes its own parser and its own paragraph beside it.
315
- *
316
- * What lands is a level-1 sketch (ADR 0007) — plain shapes, brush strokes and
317
- * editable free text — that the author then PROMOTES into BPMN artefacts.
318
- * Nothing here decides that a rounded rectangle in somebody's picture was a
319
- * task, and no `interchange` payload is written, because a visual round-trip
320
- * would be a re-render (P2).
321
- */
322
- export const BPMN_SVG_IMPORT = {
323
- id: interchangeCapabilityId('bpmn', BPMN_SVG_FORMAT.id, 'import'),
324
- framework: 'bpmn',
325
- format: BPMN_SVG_FORMAT,
326
- direction: 'import',
327
- run: parseSvgSketch,
328
- };
329
246
  /** Everything BPMN registers, in one list the view extension can hand over. */
330
247
  export const BPMN_INTERCHANGE = [
331
248
  BPMN_XML_EXPORT,
package/dist/presets.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { BpmnNodeKind } from '@formicoidea/labre-core/model';
2
- import { StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign } from '@formicoidea/labre-core/model';
2
+ import { StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign } from '@formicoidea/labre-core/model/headless';
3
3
  /**
4
4
  * What a BPMN artefact is BORN as — the one description of a node's props, read
5
5
  * by every site that creates one.
package/dist/presets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { FontFamily, SHAPE_TEXT_PADDING, SHAPE_TEXT_VERTICAL_PADDING, ShapeStyle, StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign, } from '@formicoidea/labre-core/model';
1
+ import { FontFamily, SHAPE_TEXT_PADDING, SHAPE_TEXT_VERTICAL_PADDING, ShapeStyle, StrokeStyle, TextAlign, TextFitMode, TextVerticalAlign, } from '@formicoidea/labre-core/model/headless';
2
2
  import { Bound } from '@formicoidea/labre-core/global/gfx';
3
3
  import { ACTIVITY_FIT_REF, BPMN_LABEL_GAP, BPMN_LABEL_H, BPMN_LABEL_W, CALL_ACTIVITY_WIDTH, END_WIDTH, EVENT_END, EVENT_START, GROUP_RADIUS, GROUP_STROKE, INNER_FONT_SIZE, isBpmnActivityKind, LABEL_FONT_SIZE, LABEL_INSET_RATIO, LABEL_MIN_FONT_SIZE, NEUTRAL_STROKE, NODE_FILL, NODE_SIZE, NODE_STROKE_WIDTH, START_WIDTH, TASK_RADIUS, } from './consts.js';
4
4
  import { BPMN_ROLE, BPMN_ROLE_OF_KIND } from './roles.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@formicoidea/labre-framework-bpmn",
3
3
  "description": "Labre bpmn framework for @formicoidea/labre-core.",
4
- "version": "0.50.0",
4
+ "version": "0.51.0",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "author": "lajola",
@@ -25,13 +25,17 @@
25
25
  "./commands-manifest": {
26
26
  "types": "./dist/commands-manifest.d.ts",
27
27
  "import": "./dist/commands-manifest.js"
28
+ },
29
+ "./interchange": {
30
+ "types": "./dist/interchange-headless.d.ts",
31
+ "import": "./dist/interchange-headless.js"
28
32
  }
29
33
  },
30
34
  "files": [
31
35
  "dist"
32
36
  ],
33
37
  "dependencies": {
34
- "@formicoidea/labre-core": "0.50.0",
38
+ "@formicoidea/labre-core": "0.51.0",
35
39
  "lit": "^3.2.0"
36
40
  }
37
41
  }