@formicoidea/labre-core 0.32.0 → 0.34.1
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/_pkgs/affine-block-attachment/attachment-block.js +3 -3
- package/dist/_pkgs/affine-block-attachment/configs/toolbar.d.ts +2 -2
- package/dist/_pkgs/affine-block-attachment/configs/toolbar.js +9 -7
- package/dist/_pkgs/affine-block-bookmark/configs/toolbar.js +12 -12
- package/dist/_pkgs/affine-block-callout/callout-block.js +1 -1
- package/dist/_pkgs/affine-block-code/code-block-inline.js +1 -1
- package/dist/_pkgs/affine-block-code/code-block.js +2 -2
- package/dist/_pkgs/affine-block-code/code-toolbar/config.js +9 -1
- package/dist/_pkgs/affine-block-code/code-toolbar/index.js +1 -1
- package/dist/_pkgs/affine-block-database/data-source.js +1 -2
- package/dist/_pkgs/affine-block-database/database-block.js +2 -2
- package/dist/_pkgs/affine-block-database/external-data-source.js +1 -1
- package/dist/_pkgs/affine-block-edgeless-text/edgeless-text-block.js +1 -1
- package/dist/_pkgs/affine-block-edgeless-text/edgeless-toolbar/config.js +1 -1
- package/dist/_pkgs/affine-block-embed/common/render-linked-doc.js +3 -3
- package/dist/_pkgs/affine-block-embed/configs/toolbar.js +11 -11
- package/dist/_pkgs/affine-block-embed/embed-figma-block/embed-figma-service.js +1 -1
- package/dist/_pkgs/affine-block-embed/embed-html-block/configs/toolbar.js +5 -5
- package/dist/_pkgs/affine-block-embed/embed-iframe-block/configs/toolbar.d.ts +7 -7
- package/dist/_pkgs/affine-block-embed/embed-iframe-block/configs/toolbar.js +14 -14
- package/dist/_pkgs/affine-block-embed/embed-iframe-block/embed-iframe-block.js +2 -2
- package/dist/_pkgs/affine-block-embed-doc/embed-linked-doc-block/configs/toolbar.js +10 -10
- package/dist/_pkgs/affine-block-embed-doc/embed-linked-doc-block/embed-linked-doc-block.js +4 -4
- package/dist/_pkgs/affine-block-embed-doc/embed-linked-doc-block/utils.js +1 -1
- package/dist/_pkgs/affine-block-embed-doc/embed-synced-doc-block/components/embed-synced-doc-card.js +5 -4
- package/dist/_pkgs/affine-block-embed-doc/embed-synced-doc-block/configs/toolbar.js +10 -10
- package/dist/_pkgs/affine-block-embed-doc/embed-synced-doc-block/embed-synced-doc-block.js +1 -1
- package/dist/_pkgs/affine-block-frame/frame-manager.js +3 -1
- package/dist/_pkgs/affine-block-frame/frame-tool.js +1 -1
- package/dist/_pkgs/affine-block-frame/frame-toolbar.js +2 -2
- package/dist/_pkgs/affine-block-image/components/page-image-block.js +1 -1
- package/dist/_pkgs/affine-block-image/configs/toolbar.js +4 -4
- package/dist/_pkgs/affine-block-image/image-edgeless-block.js +1 -1
- package/dist/_pkgs/affine-block-list/utils/forward-delete.js +1 -1
- package/dist/_pkgs/affine-block-note/commands/select-block.js +1 -1
- package/dist/_pkgs/affine-block-note/commands/select-blocks-between.js +1 -1
- package/dist/_pkgs/affine-block-note/configs/toolbar.js +2 -2
- package/dist/_pkgs/affine-block-note/move-block.js +1 -1
- package/dist/_pkgs/affine-block-paragraph/utils/forward-delete.js +2 -2
- package/dist/_pkgs/affine-block-paragraph/utils/merge-with-prev.js +1 -1
- package/dist/_pkgs/affine-block-root/configs/toolbar.d.ts +6 -4
- package/dist/_pkgs/affine-block-root/configs/toolbar.js +6 -6
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/misc.d.ts +10 -10
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/misc.js +4 -4
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/more.d.ts +7 -7
- package/dist/_pkgs/affine-block-root/edgeless/configs/toolbar/more.js +9 -9
- package/dist/_pkgs/affine-block-root/edgeless/edgeless-keyboard.js +1 -1
- package/dist/_pkgs/affine-block-root/edgeless/edgeless-root-block.js +3 -1
- package/dist/_pkgs/affine-block-root/edgeless/utils/clipboard-utils.js +1 -1
- package/dist/_pkgs/affine-block-surface/adapters/markdown/element-adapter/type.js +1 -1
- package/dist/_pkgs/affine-block-surface/adapters/plain-text/element-adapter/type.js +1 -1
- package/dist/_pkgs/affine-block-surface/element-model/index.d.ts +18 -3
- package/dist/_pkgs/affine-block-surface/element-model/index.js +12 -2
- package/dist/_pkgs/affine-block-surface/extensions/audit.js +23 -6
- package/dist/_pkgs/affine-block-surface/extensions/crud-extension.js +10 -5
- package/dist/_pkgs/affine-block-surface/extensions/editor-anchored-panel.js +2 -1
- package/dist/_pkgs/affine-block-surface/extensions/element-renderer.js +1 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +1 -1
- package/dist/_pkgs/affine-block-surface/extensions/index.d.ts +4 -0
- package/dist/_pkgs/affine-block-surface/extensions/index.js +4 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-import.d.ts +172 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-import.js +404 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange.d.ts +323 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange.js +64 -0
- package/dist/_pkgs/affine-block-surface/extensions/map-quality-widget.js +61 -61
- package/dist/_pkgs/affine-block-surface/extensions/morph-toolbar.d.ts +248 -0
- package/dist/_pkgs/affine-block-surface/extensions/morph-toolbar.js +248 -0
- package/dist/_pkgs/affine-block-surface/extensions/reading-widget.js +78 -80
- package/dist/_pkgs/affine-block-surface/extensions/reading.js +2 -2
- package/dist/_pkgs/affine-block-surface/extensions/spotlight.d.ts +21 -0
- package/dist/_pkgs/affine-block-surface/extensions/spotlight.js +17 -3
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.d.ts +107 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.js +958 -0
- package/dist/_pkgs/affine-block-surface/extensions/validation.d.ts +809 -20
- package/dist/_pkgs/affine-block-surface/extensions/validation.js +1661 -50
- package/dist/_pkgs/affine-block-surface/extensions/violation-detail-widget.d.ts +50 -6
- package/dist/_pkgs/affine-block-surface/extensions/violation-detail-widget.js +129 -32
- package/dist/_pkgs/affine-block-surface/framework-background/def.d.ts +289 -0
- package/dist/_pkgs/affine-block-surface/framework-background/def.js +126 -1
- package/dist/_pkgs/affine-block-surface/framework-background/labels.d.ts +42 -2
- package/dist/_pkgs/affine-block-surface/framework-background/labels.js +80 -4
- package/dist/_pkgs/affine-block-surface/framework-background/renderer.js +172 -11
- package/dist/_pkgs/affine-block-surface/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/index.js +1 -0
- package/dist/_pkgs/affine-block-surface/renderer/element-highlight-overlay.d.ts +17 -0
- package/dist/_pkgs/affine-block-surface/renderer/element-highlight-overlay.js +66 -0
- package/dist/_pkgs/affine-block-surface/renderer/overlay.js +1 -1
- package/dist/_pkgs/affine-block-surface/view.js +5 -0
- package/dist/_pkgs/affine-block-surface-ref/configs/toolbar.js +4 -4
- package/dist/_pkgs/affine-block-surface-ref/portal/note.js +2 -2
- package/dist/_pkgs/affine-block-table/commands.js +1 -1
- package/dist/_pkgs/affine-components/edgeless-shape-color-picker/color-picker.js +1 -1
- package/dist/_pkgs/affine-components/embed-card-modal/embed-card-edit-modal.js +1 -1
- package/dist/_pkgs/affine-components/filterable-list/styles.js +1 -1
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/highlight-duotone-icon.js +8 -8
- package/dist/_pkgs/affine-components/highlight-dropdown-menu/text-duotone-icon.js +11 -11
- package/dist/_pkgs/affine-components/icons/text.js +426 -426
- package/dist/_pkgs/affine-components/open-doc-dropdown-menu/dropdown-menu.d.ts +2 -0
- package/dist/_pkgs/affine-components/peek/peekable.js +1 -1
- package/dist/_pkgs/affine-components/resource/resource.js +2 -0
- package/dist/_pkgs/affine-components/toolbar/index.d.ts +1 -0
- package/dist/_pkgs/affine-components/toolbar/index.js +1 -0
- package/dist/_pkgs/affine-components/toolbar/menu-button.js +1 -1
- package/dist/_pkgs/affine-components/toolbar/picker-menu.d.ts +55 -0
- package/dist/_pkgs/affine-components/toolbar/picker-menu.js +47 -0
- package/dist/_pkgs/affine-components/view-dropdown-menu/dropdown-menu.js +27 -14
- package/dist/_pkgs/affine-fragment-frame-panel/body/frame-panel-body.js +2 -2
- package/dist/_pkgs/affine-fragment-frame-panel/header/frame-panel-header.js +2 -2
- package/dist/_pkgs/affine-fragment-frame-panel/header/frames-setting-menu.js +1 -1
- package/dist/_pkgs/affine-fragment-outline/mobile-outline-panel.js +1 -1
- package/dist/_pkgs/affine-gfx-brush/toolbar/components/pen/pen-tool-button.js +1 -1
- package/dist/_pkgs/affine-gfx-connector/connector-manager.js +7 -10
- package/dist/_pkgs/affine-gfx-connector/connector-tool.d.ts +19 -1
- package/dist/_pkgs/affine-gfx-connector/connector-tool.js +10 -2
- package/dist/_pkgs/affine-gfx-connector/connector-watcher.js +1 -4
- package/dist/_pkgs/affine-gfx-connector/direction/direction-reveal.js +2 -1
- package/dist/_pkgs/affine-gfx-connector/direction/invert-direction.js +1 -3
- package/dist/_pkgs/affine-gfx-connector/element-renderer/index.js +1 -1
- package/dist/_pkgs/affine-gfx-connector/element-renderer/utils.js +1 -1
- package/dist/_pkgs/affine-gfx-connector/toolbar/config.js +1 -1
- package/dist/_pkgs/affine-gfx-mindmap/adapter/markdown/index.js +1 -1
- package/dist/_pkgs/affine-gfx-mindmap/adapter/utils.js +1 -1
- package/dist/_pkgs/affine-gfx-mindmap/templates/index.js +583 -4
- package/dist/_pkgs/affine-gfx-mindmap/toolbar/mindmap-menu.js +23 -23
- package/dist/_pkgs/affine-gfx-mindmap/toolbar/mindmap-tool-button.js +5 -1
- package/dist/_pkgs/affine-gfx-mindmap/view/utils.js +1 -1
- package/dist/_pkgs/affine-gfx-note/overlay/overlay.js +1 -1
- package/dist/_pkgs/affine-gfx-note/toolbar/note-senior-button.js +1 -1
- package/dist/_pkgs/affine-gfx-shape/consts.js +1 -1
- package/dist/_pkgs/affine-gfx-shape/draggable/shape-draggable.js +1 -1
- package/dist/_pkgs/affine-gfx-shape/element-renderer/shape/polygon.js +17 -9
- package/dist/_pkgs/affine-gfx-shape/element-renderer/shape/utils.js +8 -3
- package/dist/_pkgs/affine-gfx-shape/element-view.js +19 -12
- package/dist/_pkgs/affine-gfx-shape/overlay/polygon-drawing-overlay.js +1 -1
- package/dist/_pkgs/affine-gfx-shape/overlay/polygon-vertex-editing-overlay.js +12 -7
- package/dist/_pkgs/affine-gfx-shape/overlay/shape-overlay.js +1 -1
- package/dist/_pkgs/affine-gfx-shape/polygon-tool.js +4 -2
- package/dist/_pkgs/affine-gfx-shape/shape-tool.js +1 -1
- package/dist/_pkgs/affine-gfx-shape/text/edgeless-shape-text-editor.js +1 -3
- package/dist/_pkgs/affine-gfx-template/builtin/other.js +66 -17
- package/dist/_pkgs/affine-gfx-template/toolbar/overlay-scrollbar.js +1 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +1 -1
- package/dist/_pkgs/affine-gfx-text/toolbar/actions.js +1 -1
- package/dist/_pkgs/affine-inline-link/command.js +1 -1
- package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.d.ts +4 -4
- package/dist/_pkgs/affine-inline-link/link-node/configs/toolbar.js +6 -6
- package/dist/_pkgs/affine-inline-preset/command/format-block.js +1 -1
- package/dist/_pkgs/affine-inline-preset/command/format-native.js +2 -2
- package/dist/_pkgs/affine-inline-preset/command/format-text.js +1 -1
- package/dist/_pkgs/affine-inline-preset/inline-spec.js +1 -1
- package/dist/_pkgs/affine-inline-reference/reference-node/configs/toolbar.d.ts +4 -4
- package/dist/_pkgs/affine-inline-reference/reference-node/configs/toolbar.js +6 -6
- package/dist/_pkgs/affine-inline-reference/reference-node/reference-node.js +1 -1
- package/dist/_pkgs/affine-model/elements/bpmn/node.d.ts +40 -7
- package/dist/_pkgs/affine-model/elements/bpmn/node.js +4 -2
- package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +135 -17
- package/dist/_pkgs/affine-model/elements/bpmn/pool.js +148 -26
- package/dist/_pkgs/affine-model/elements/c4/board.d.ts +88 -0
- package/dist/_pkgs/affine-model/elements/c4/board.js +128 -0
- package/dist/_pkgs/affine-model/elements/c4/boundary.d.ts +63 -0
- package/dist/_pkgs/affine-model/elements/c4/boundary.js +134 -0
- package/dist/_pkgs/affine-model/elements/c4/index.d.ts +3 -0
- package/dist/_pkgs/affine-model/elements/c4/index.js +3 -0
- package/dist/_pkgs/affine-model/elements/c4/node.d.ts +97 -0
- package/dist/_pkgs/affine-model/elements/c4/node.js +130 -0
- package/dist/_pkgs/affine-model/elements/connector/connector.js +1 -1
- package/dist/_pkgs/affine-model/elements/context-map/context-map.d.ts +27 -0
- package/dist/_pkgs/affine-model/elements/context-map/context-map.js +94 -0
- package/dist/_pkgs/affine-model/elements/context-map/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/context-map/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/core-domain/core-domain.d.ts +34 -17
- package/dist/_pkgs/affine-model/elements/core-domain/core-domain.js +28 -26
- package/dist/_pkgs/affine-model/elements/cynefin/cynefin.d.ts +7 -2
- package/dist/_pkgs/affine-model/elements/cynefin/cynefin.js +9 -4
- package/dist/_pkgs/affine-model/elements/edgy/board.d.ts +7 -2
- package/dist/_pkgs/affine-model/elements/edgy/board.js +9 -4
- package/dist/_pkgs/affine-model/elements/edgy/facets.d.ts +20 -3
- package/dist/_pkgs/affine-model/elements/edgy/facets.js +13 -4
- package/dist/_pkgs/affine-model/elements/estuarine/estuarine.d.ts +7 -2
- package/dist/_pkgs/affine-model/elements/estuarine/estuarine.js +9 -4
- package/dist/_pkgs/affine-model/elements/event-storming/event-storming.d.ts +36 -0
- package/dist/_pkgs/affine-model/elements/event-storming/event-storming.js +103 -0
- package/dist/_pkgs/affine-model/elements/event-storming/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/event-storming/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/framework-background/background.d.ts +7 -2
- package/dist/_pkgs/affine-model/elements/framework-background/background.js +9 -4
- package/dist/_pkgs/affine-model/elements/framework-background/hit-test.d.ts +48 -0
- package/dist/_pkgs/affine-model/elements/framework-background/hit-test.js +55 -0
- package/dist/_pkgs/affine-model/elements/framework-background/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/framework-background/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/index.d.ts +11 -0
- package/dist/_pkgs/affine-model/elements/index.js +3 -0
- package/dist/_pkgs/affine-model/elements/shape/api/polygon.js +2 -8
- package/dist/_pkgs/affine-shared/adapters/html/block-adapter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/markdown/block-adapter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/markdown/delta-converter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/notion-html/block-adapter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/notion-html/delta-converter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/plain-text/block-adapter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/plain-text/delta-converter.js +1 -1
- package/dist/_pkgs/affine-shared/adapters/utils/color.js +5 -2
- package/dist/_pkgs/affine-shared/commands/model-crud/draft-selected-models.js +1 -1
- package/dist/_pkgs/affine-shared/commands/selection/focus-block-end.js +1 -1
- package/dist/_pkgs/affine-shared/commands/selection/focus-block-start.js +1 -1
- package/dist/_pkgs/affine-shared/consts/index.d.ts +10 -0
- package/dist/_pkgs/affine-shared/consts/index.js +10 -0
- package/dist/_pkgs/affine-shared/services/artefact-catalogue-service.d.ts +55 -0
- package/dist/_pkgs/affine-shared/services/artefact-catalogue-service.js +35 -0
- package/dist/_pkgs/affine-shared/services/audit-service.d.ts +11 -0
- package/dist/_pkgs/affine-shared/services/command-usage-service.d.ts +31 -0
- package/dist/_pkgs/affine-shared/services/command-usage-service.js +109 -0
- package/dist/_pkgs/affine-shared/services/comment-service/block-comment-manager.js +1 -1
- package/dist/_pkgs/affine-shared/services/doc-display-meta-service.js +1 -1
- package/dist/_pkgs/affine-shared/services/edit-props-store.d.ts +11 -0
- package/dist/_pkgs/affine-shared/services/edit-props-store.js +15 -0
- package/dist/_pkgs/affine-shared/services/embed-iframe/embed-iframe-config.js +1 -1
- package/dist/_pkgs/affine-shared/services/index.d.ts +2 -0
- package/dist/_pkgs/affine-shared/services/index.js +2 -0
- package/dist/_pkgs/affine-shared/services/pivot-materiality-publisher.js +2 -1
- package/dist/_pkgs/affine-shared/services/telemetry-service/lifecycle.d.ts +73 -1
- package/dist/_pkgs/affine-shared/services/toolbar-service/action.d.ts +32 -0
- package/dist/_pkgs/affine-shared/services/toolbar-service/action.js +16 -0
- package/dist/_pkgs/affine-shared/services/toolbar-service/flags.d.ts +10 -0
- package/dist/_pkgs/affine-shared/services/toolbar-service/flags.js +32 -1
- package/dist/_pkgs/affine-shared/services/toolbar-service/registry.d.ts +44 -0
- package/dist/_pkgs/affine-shared/services/toolbar-service/registry.js +161 -3
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +106 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +201 -0
- package/dist/_pkgs/affine-shared/services/translation-service/index.d.ts +1 -0
- package/dist/_pkgs/affine-shared/services/translation-service/index.js +1 -0
- package/dist/_pkgs/affine-shared/services/universe-tag-defs-service.js +5 -3
- package/dist/_pkgs/affine-shared/test-utils/affine-template.js +1 -1
- package/dist/_pkgs/affine-shared/utils/edgeless.js +1 -1
- package/dist/_pkgs/affine-shared/utils/file/filesys.d.ts +34 -0
- package/dist/_pkgs/affine-shared/utils/file/filesys.js +48 -7
- package/dist/_pkgs/affine-shared/utils/zod-schema.js +1 -3
- package/dist/_pkgs/affine-widget-drag-handle/utils.js +1 -1
- package/dist/_pkgs/affine-widget-edgeless-auto-connect/index.js +1 -1
- package/dist/_pkgs/affine-widget-edgeless-dragging-area/edgeless-dragging-area-rect.js +1 -1
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/artefact-catalogue-default.d.ts +14 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/artefact-catalogue-default.js +40 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/artefact-catalogue-widget.d.ts +158 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/artefact-catalogue-widget.js +572 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/catalogue-groups.d.ts +41 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/catalogue-groups.js +49 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/chord-format.d.ts +13 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/catalogue/chord-format.js +50 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/edgeless-toolbar.d.ts +6 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/edgeless-toolbar.js +15 -5
- package/dist/_pkgs/affine-widget-edgeless-toolbar/effects.d.ts +2 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/effects.js +2 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/extension/index.d.ts +7 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/index.d.ts +4 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/index.js +4 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/menu/command-menu.d.ts +19 -0
- package/dist/_pkgs/affine-widget-edgeless-toolbar/menu/command-menu.js +51 -10
- package/dist/_pkgs/affine-widget-edgeless-toolbar/view.js +9 -0
- package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +2 -1
- package/dist/_pkgs/affine-widget-keyboard-toolbar/icons.js +98 -98
- package/dist/_pkgs/affine-widget-keyboard-toolbar/keyboard-tool-panel.js +1 -1
- package/dist/_pkgs/affine-widget-linked-doc/import-doc/import-doc.js +1 -1
- package/dist/_pkgs/affine-widget-linked-doc/linked-doc-popover.js +1 -1
- package/dist/_pkgs/affine-widget-page-dragging-area/index.js +1 -1
- package/dist/_pkgs/affine-widget-remote-selection/doc/doc-remote-selection.js +1 -1
- package/dist/_pkgs/affine-widget-slash-menu/config.js +2 -1
- package/dist/_pkgs/affine-widget-toolbar/toolbar.js +11 -2
- package/dist/_pkgs/affine-widget-toolbar/utils.d.ts +19 -2
- package/dist/_pkgs/affine-widget-toolbar/utils.js +28 -9
- package/dist/_pkgs/data-view/core/group-by/renderer/boolean-group.js +11 -11
- package/dist/_pkgs/data-view/core/utils/menu-title.js +17 -17
- package/dist/_pkgs/data-view/view-presets/calendar/pc/actions.js +8 -12
- package/dist/_pkgs/data-view/view-presets/calendar/pc/view.js +52 -40
- package/dist/_pkgs/data-view/view-presets/table/mobile/group.js +3 -1
- package/dist/_pkgs/data-view/view-presets/table/pc/group.js +3 -1
- package/dist/_pkgs/data-view/view-presets/table/pc-virtual/group/top/group-title.js +18 -18
- package/dist/_pkgs/std/clipboard/utils.js +4 -1
- package/dist/_pkgs/std/extension/command-registry.d.ts +195 -5
- package/dist/_pkgs/std/extension/command-registry.js +218 -4
- package/dist/_pkgs/std/extension/shortcut.js +2 -1
- package/dist/_pkgs/std/extension/translation-manifest.d.ts +46 -1
- package/dist/_pkgs/std/extension/translation-manifest.js +58 -0
- package/dist/_pkgs/std/gfx/grid.js +1 -1
- package/dist/_pkgs/std/gfx/highlight.d.ts +84 -0
- package/dist/_pkgs/std/gfx/highlight.js +112 -0
- package/dist/_pkgs/std/gfx/index.d.ts +2 -1
- package/dist/_pkgs/std/gfx/index.js +1 -0
- package/dist/_pkgs/std/gfx/interactivity/gfx-view-event-handler.js +17 -3
- package/dist/_pkgs/std/gfx/model/surface/element-model.d.ts +160 -0
- package/dist/_pkgs/std/gfx/model/surface/element-model.js +75 -1
- package/dist/_pkgs/std/gfx/model/surface/role.js +5 -3
- package/dist/_pkgs/std/gfx/model/surface/surface-model.js +2 -1
- package/dist/_pkgs/std/gfx/selection.js +1 -1
- package/dist/_pkgs/std/gfx/viewport.js +1 -1
- package/dist/_pkgs/std/inline/extensions/inline-manager.js +1 -1
- package/dist/_pkgs/std/inline/extensions/markdown-matcher.js +1 -1
- package/dist/_pkgs/std/scope/std-scope.d.ts +2 -1
- package/dist/_pkgs/std/scope/std-scope.js +2 -0
- package/dist/_pkgs/std/utils/gfx.js +2 -0
- package/dist/_pkgs/store/model/store/crud.js +4 -1
- package/dist/commands.js +1 -1
- package/dist/extensions/command-usage.d.ts +23 -0
- package/dist/extensions/command-usage.js +37 -0
- package/dist/extensions/view.js +12 -0
- package/dist/frameworks.js +32 -0
- package/dist/shortcuts.d.ts +20 -6
- package/dist/shortcuts.js +7 -9
- package/dist/translations.js +98 -3
- package/package.json +13 -1
|
@@ -2,7 +2,9 @@ import { createIdentifier } from '../../global/di/index.js';
|
|
|
2
2
|
import { Bound, lineIntersects } from '../../global/gfx/index.js';
|
|
3
3
|
import { GfxGroupLikeElementModel, GfxPrimitiveElementModel, InteractivityExtension, roleIsA, } from '../../std/gfx/index.js';
|
|
4
4
|
import { effect, signal } from '@preact/signals-core';
|
|
5
|
+
import { backgroundPlot } from '../framework-background/def.js';
|
|
5
6
|
import { backgroundAxisFact, backgroundTransitionBands, backgroundTransitionVisibleProps, } from '../framework-background/facts.js';
|
|
7
|
+
import { backgroundInVariant } from '../framework-background/labels.js';
|
|
6
8
|
import { Overlay, OverlayIdentifier } from '../renderer/overlay.js';
|
|
7
9
|
import { QualityNudgeIdentifier } from './map-quality.js';
|
|
8
10
|
import { ViolationTimeline } from './violation-timeline.js';
|
|
@@ -62,7 +64,8 @@ function resolveProfile(rule, chosenId, index) {
|
|
|
62
64
|
return index.defaults.get(rule.framework);
|
|
63
65
|
}
|
|
64
66
|
/**
|
|
65
|
-
* The profile id each role-carrying element names, in one pass
|
|
67
|
+
* The profile id each role-carrying element names, in one pass — then the same
|
|
68
|
+
* for the ones that name none but SIT INSIDE one that does.
|
|
66
69
|
*
|
|
67
70
|
* Role-carrying only: a profile is a decision about a framework's background,
|
|
68
71
|
* and a neutral element cannot be one. That keeps the pass on exactly the same
|
|
@@ -70,17 +73,98 @@ function resolveProfile(rule, chosenId, index) {
|
|
|
70
73
|
*/
|
|
71
74
|
function readChosenProfiles(elements) {
|
|
72
75
|
const chosen = new Map();
|
|
76
|
+
const roleCarrying = [];
|
|
73
77
|
for (const el of elements) {
|
|
74
78
|
if (el.role === undefined)
|
|
75
79
|
continue;
|
|
80
|
+
roleCarrying.push(el);
|
|
76
81
|
const id = el.validationProfile;
|
|
77
82
|
// Whatever a peer wrote: a client that got it wrong must not break
|
|
78
83
|
// evaluation on this one.
|
|
79
84
|
if (typeof id === 'string')
|
|
80
85
|
chosen.set(el.id, id);
|
|
81
86
|
}
|
|
87
|
+
// Nobody chose anything => no inheritance to compute, and not one bound read
|
|
88
|
+
// on the whole surface. The overwhelmingly common case: choosing the default
|
|
89
|
+
// profile back WRITES NOTHING (`setProfile` clears the field), so `chosen` is
|
|
90
|
+
// empty on every document whose author never left the default.
|
|
91
|
+
if (chosen.size > 0)
|
|
92
|
+
inheritChosenProfiles(roleCarrying, chosen);
|
|
82
93
|
return chosen;
|
|
83
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* A frame that named no profile inherits the one chosen on the frame it is
|
|
97
|
+
* DRAWN INSIDE — the innermost of them.
|
|
98
|
+
*
|
|
99
|
+
* ## The question this answers
|
|
100
|
+
*
|
|
101
|
+
* A profile is chosen per root instance (PF9.1), and a finding is judged by the
|
|
102
|
+
* profile of the instance it is ATTRIBUTED to ({@link applyProfiles}). Those two
|
|
103
|
+
* sentences only describe the same element while every attribution target is
|
|
104
|
+
* something the author can reach a picker on. They stop describing the same
|
|
105
|
+
* element the moment a framework frames a question against an INNER frame — a
|
|
106
|
+
* C4 boundary, a BPMN lane — and the picker is offered on the outer one alone,
|
|
107
|
+
* which is the arbitration C4's recette settled (PO, 28/08/2026: the board
|
|
108
|
+
* arbitrates the checklist, the boundary does not). Without this, raising a
|
|
109
|
+
* board to its review level would harden the eleven rules framed against the
|
|
110
|
+
* board and silently leave the two framed against a boundary at the default
|
|
111
|
+
* for ever.
|
|
112
|
+
*
|
|
113
|
+
* Inheritance is the general form of that fix and carries no framework name: an
|
|
114
|
+
* instance with no choice of its own is governed by the choice made on the
|
|
115
|
+
* frame it sits in, which is what a reader looking at the two nested rectangles
|
|
116
|
+
* already assumes. An instance that DOES name a profile keeps it — a nested
|
|
117
|
+
* frame can still be arbitrated separately the day a framework offers a picker
|
|
118
|
+
* on one.
|
|
119
|
+
*
|
|
120
|
+
* ## The arithmetic
|
|
121
|
+
*
|
|
122
|
+
* Centre-in-frame, innermost first, ties to the smaller id — the same reading
|
|
123
|
+
* {@link plotContaining}, the audit and the C4 export use to answer "which frame
|
|
124
|
+
* is this drawn on", so a board can never read two different answers to that
|
|
125
|
+
* question. The element BOX rather than the plot: a plot needs the frame's
|
|
126
|
+
* geometry declaration, which is a per-RULE datum, and this map is built once
|
|
127
|
+
* for every rule at once. The margin only matters to an artefact lying on a
|
|
128
|
+
* title band, and inheriting a level of requirement there is harmless where
|
|
129
|
+
* counting it as a member would not be.
|
|
130
|
+
*
|
|
131
|
+
* Cross-framework nesting resolves itself downstream and needs no test here:
|
|
132
|
+
* {@link resolveProfile} ignores a profile id belonging to another framework, so
|
|
133
|
+
* a frame that inherits a foreign one falls back to its own framework's default
|
|
134
|
+
* — exactly what it got before inheriting anything.
|
|
135
|
+
*/
|
|
136
|
+
function inheritChosenProfiles(elements, chosen) {
|
|
137
|
+
const frames = elements
|
|
138
|
+
.filter(el => chosen.has(el.id))
|
|
139
|
+
.map(el => ({ id: el.id, bound: el.elementBound }));
|
|
140
|
+
// Read off the map BEFORE writing to it: an inherited id must never be
|
|
141
|
+
// inherited on again, or a chain of frames would depend on the walk order.
|
|
142
|
+
const inherited = [];
|
|
143
|
+
for (const el of elements) {
|
|
144
|
+
if (chosen.has(el.id))
|
|
145
|
+
continue;
|
|
146
|
+
const centre = el.elementBound.center;
|
|
147
|
+
let inner = null;
|
|
148
|
+
for (const frame of frames) {
|
|
149
|
+
if (!frame.bound.containsPoint(centre))
|
|
150
|
+
continue;
|
|
151
|
+
if (inner === null ||
|
|
152
|
+
isInside(frame.bound, inner.bound, frame.id, inner.id)) {
|
|
153
|
+
inner = frame;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (inner !== null)
|
|
157
|
+
inherited.push([el.id, chosen.get(inner.id)]);
|
|
158
|
+
}
|
|
159
|
+
for (const [id, profileId] of inherited)
|
|
160
|
+
chosen.set(id, profileId);
|
|
161
|
+
}
|
|
162
|
+
/** Whether `a` is the tighter frame of the two — smaller area, then smaller id. */
|
|
163
|
+
function isInside(a, b, aId, bId) {
|
|
164
|
+
const areaA = a.w * a.h;
|
|
165
|
+
const areaB = b.w * b.h;
|
|
166
|
+
return areaA === areaB ? aId < bId : areaA < areaB;
|
|
167
|
+
}
|
|
84
168
|
/**
|
|
85
169
|
* Whether `rule` is `'off'` under EVERY profile in play on this surface — in
|
|
86
170
|
* which case it is not evaluated at all, which is what `'off'` is for.
|
|
@@ -207,14 +291,24 @@ export function backgroundElementIds(rules, elements) {
|
|
|
207
291
|
* Same question `evaluateElementInBackground` answers inline, asked by the
|
|
208
292
|
* families whose subjects normally ARE on the map — an inertia bar, an arrow, a
|
|
209
293
|
* pair of overlapping labels. Ties go to the smaller id so a persisted
|
|
210
|
-
* arbitration never depends on the order a `Y.Map` was rebuilt in
|
|
294
|
+
* arbitration never depends on the order a `Y.Map` was rebuilt in — on BOTH
|
|
295
|
+
* halves of the question: a small chart dropped on a big one contains the same
|
|
296
|
+
* artefact twice, and returning whichever of the two the walk met first would
|
|
297
|
+
* have made the promise above true only of the near miss. Same walk and same
|
|
298
|
+
* tie-break as {@link containingBackground}, which asks the containment half
|
|
299
|
+
* alone.
|
|
211
300
|
*/
|
|
212
301
|
function attributeBackground(bound, backgrounds) {
|
|
302
|
+
let containing = null;
|
|
213
303
|
let nearest = null;
|
|
214
304
|
let nearestDistance = Infinity;
|
|
215
305
|
for (const background of backgrounds) {
|
|
216
|
-
if (background.bound.contains(bound))
|
|
217
|
-
|
|
306
|
+
if (background.bound.contains(bound)) {
|
|
307
|
+
if (containing === null || background.id < containing.id) {
|
|
308
|
+
containing = background;
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
218
312
|
const distance = gapSquared(background.bound, bound);
|
|
219
313
|
if (distance < nearestDistance ||
|
|
220
314
|
(distance === nearestDistance &&
|
|
@@ -224,7 +318,116 @@ function attributeBackground(bound, backgrounds) {
|
|
|
224
318
|
nearest = background;
|
|
225
319
|
}
|
|
226
320
|
}
|
|
227
|
-
return nearest;
|
|
321
|
+
return containing ?? nearest;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* The PLOT of one instance, in model coordinates — the element box minus the
|
|
325
|
+
* declared margin, i.e. the region the frame actually draws its content in.
|
|
326
|
+
*
|
|
327
|
+
* A rule that carries no {@link ValidationRule.background} declaration has no
|
|
328
|
+
* margin to subtract and the whole element box is its plot. That is the honest
|
|
329
|
+
* answer rather than a refusal: `backgroundRole` alone is a complete declaration
|
|
330
|
+
* for a family that only needs to know WHICH frame something is on, and
|
|
331
|
+
* demanding a geometry declaration to count elements inside a box would be
|
|
332
|
+
* asking a framework to declare axes its rule never reads (the same argument
|
|
333
|
+
* `relation-endpoints` makes about its own optional frame).
|
|
334
|
+
*
|
|
335
|
+
* ## The degenerate plot
|
|
336
|
+
*
|
|
337
|
+
* `backgroundPlot` subtracts the declared margins with no guard, so a frame
|
|
338
|
+
* dragged narrower than `margin.left + margin.right` yields a NEGATIVE extent —
|
|
339
|
+
* and a `Bound` of negative width contains no point at all, anywhere. Left
|
|
340
|
+
* alone, a membership family would then tally zero for a frame the user can see
|
|
341
|
+
* holding something, and a `min` rule would answer "this frame holds none" while
|
|
342
|
+
* pointing at one that visibly does. A false accusation whose only remedy —
|
|
343
|
+
* widen the frame — is nowhere in the sentence.
|
|
344
|
+
*
|
|
345
|
+
* So a plot that has collapsed falls back to the element box: the same answer
|
|
346
|
+
* this function already gives a rule that declares no geometry at all, and the
|
|
347
|
+
* one reading under which the artefact the user is looking at still counts.
|
|
348
|
+
* Silence would be defensible too; being wrong in the accusing direction is not.
|
|
349
|
+
*/
|
|
350
|
+
function plotOf(def, frame) {
|
|
351
|
+
const bound = frame.bound;
|
|
352
|
+
if (def === undefined)
|
|
353
|
+
return bound;
|
|
354
|
+
const plot = backgroundPlot(def, bound.w, bound.h);
|
|
355
|
+
if (!(plot.width > 0) || !(plot.height > 0))
|
|
356
|
+
return bound;
|
|
357
|
+
return new Bound(bound.x + plot.x0, bound.y + plot.y0, plot.width, plot.height);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* The instance an element BELONGS TO: the one whose plot contains its centre,
|
|
361
|
+
* and nothing else.
|
|
362
|
+
*
|
|
363
|
+
* ## The deliberate divergence from {@link attributeBackground}
|
|
364
|
+
*
|
|
365
|
+
* `attributeBackground` answers "which map is this finding about", and falls
|
|
366
|
+
* back to the NEAREST frame when none contains the subject — which is right for
|
|
367
|
+
* a finding that has already been raised and needs somewhere to be waived.
|
|
368
|
+
*
|
|
369
|
+
* This answers a different question: "is this artefact IN that frame". A
|
|
370
|
+
* nearest-fallback here would be a bug with a plausible face — a start event
|
|
371
|
+
* floating just outside a pool would satisfy "this pool has a start event", and
|
|
372
|
+
* a sequence flow from inside the pool to that floating task would read as
|
|
373
|
+
* staying inside it. Membership has to mean membership, or the two families
|
|
374
|
+
* built on it certify drawings that show the opposite of what they claim.
|
|
375
|
+
*
|
|
376
|
+
* The CENTRE, and the plot rather than the element box: the centre is where the
|
|
377
|
+
* eye puts an artefact (the reading {@link ElementInZoneDef.measure} already
|
|
378
|
+
* defaults to) and the plot is the region the frame draws in, so an artefact
|
|
379
|
+
* lying on the pool's own title band is not counted as being in the pool. Same
|
|
380
|
+
* resolution the audit uses to place an element, so a board never reads two
|
|
381
|
+
* different answers to "which frame is this on".
|
|
382
|
+
*
|
|
383
|
+
* Ties go to the smaller id, never to the order a `Y.Map` was rebuilt in — the
|
|
384
|
+
* invariant every attribution in this file holds, and it matters here for the
|
|
385
|
+
* same reason: the answer decides which frame a persisted arbitration is
|
|
386
|
+
* written on.
|
|
387
|
+
*/
|
|
388
|
+
function plotContaining(centre, frames) {
|
|
389
|
+
let found = null;
|
|
390
|
+
for (const frame of frames) {
|
|
391
|
+
if (!frame.plot.containsPoint(centre))
|
|
392
|
+
continue;
|
|
393
|
+
if (found === null || frame.instance.id < found.id)
|
|
394
|
+
found = frame.instance;
|
|
395
|
+
}
|
|
396
|
+
return found;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Every instance of the rule's frame, each with its plot resolved once.
|
|
400
|
+
*
|
|
401
|
+
* A board carries units of frames and hundreds of subjects, so the margin
|
|
402
|
+
* arithmetic is done per FRAME and never per subject.
|
|
403
|
+
*/
|
|
404
|
+
function framePlots(rule, elements) {
|
|
405
|
+
return backgroundsOf(rule, elements).map(instance => ({
|
|
406
|
+
instance,
|
|
407
|
+
plot: plotOf(rule.background, instance),
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Whether a rule whose whole question is asked ABOUT a frame forgot to name one
|
|
412
|
+
* — and, if so, says so once.
|
|
413
|
+
*
|
|
414
|
+
* `backgroundsOf` answers `[]` both for "this rule names no frame role" and for
|
|
415
|
+
* "this board carries no such frame", and the second is a legitimate silence
|
|
416
|
+
* every family in this file honours: a sketch drawn before the frame is a
|
|
417
|
+
* sketch. The first is a typo in the data, and left undistinguished it produces
|
|
418
|
+
* a rule that never fires and never says why — the failure the missing-bound
|
|
419
|
+
* warnings a few lines below exist to prevent, under a comment saying it is the
|
|
420
|
+
* worst thing declarative data can do.
|
|
421
|
+
*
|
|
422
|
+
* The two are distinguishable statically, so they are distinguished here, before
|
|
423
|
+
* a single element is walked.
|
|
424
|
+
*/
|
|
425
|
+
function missingFrameRole(rule, family) {
|
|
426
|
+
if (rule.backgroundRole !== undefined)
|
|
427
|
+
return false;
|
|
428
|
+
warnOnce(`${family} rule "${rule.id}" names no frame role, and its whole question ` +
|
|
429
|
+
`is asked about a frame — the rule is not evaluated.`);
|
|
430
|
+
return true;
|
|
228
431
|
}
|
|
229
432
|
/**
|
|
230
433
|
* Build the finding of `rule` against `elementIds`. One place, so every family
|
|
@@ -357,7 +560,8 @@ function cubicAt(p0, c0, c1, p1, t) {
|
|
|
357
560
|
/** Anything but a rounding difference away from `p`. */
|
|
358
561
|
function departsFrom(candidate, p) {
|
|
359
562
|
return (isPoint(candidate) &&
|
|
360
|
-
(Math.abs(candidate[0] - p[0]) > 1e-6 ||
|
|
563
|
+
(Math.abs(candidate[0] - p[0]) > 1e-6 ||
|
|
564
|
+
Math.abs(candidate[1] - p[1]) > 1e-6));
|
|
361
565
|
}
|
|
362
566
|
/**
|
|
363
567
|
* The POLYLINE a directional element exposes, in absolute model coordinates —
|
|
@@ -593,7 +797,8 @@ function evaluateAttachment(rule, elements) {
|
|
|
593
797
|
for (const el of elements) {
|
|
594
798
|
if (el.role === undefined)
|
|
595
799
|
continue;
|
|
596
|
-
if (carrierRole !== undefined &&
|
|
800
|
+
if (carrierRole !== undefined &&
|
|
801
|
+
roleIsA(el.role, carrierRole, rule.roles)) {
|
|
597
802
|
const path = elementPath(el);
|
|
598
803
|
if (path !== null)
|
|
599
804
|
carriers.push(path);
|
|
@@ -745,7 +950,8 @@ function subjectsCollide(a, b, minPenetration) {
|
|
|
745
950
|
if (!boundsOverlap(a.bound, b.bound))
|
|
746
951
|
return false;
|
|
747
952
|
if (a.path === null && b.path === null) {
|
|
748
|
-
return (minPenetration <= 0 ||
|
|
953
|
+
return (minPenetration <= 0 ||
|
|
954
|
+
boundsPenetration(a.bound, b.bound) > minPenetration);
|
|
749
955
|
}
|
|
750
956
|
// Two paths: zero-width lines, so there is no depth to measure and a declared
|
|
751
957
|
// crossing is reported as it always was.
|
|
@@ -755,7 +961,7 @@ function subjectsCollide(a, b, minPenetration) {
|
|
|
755
961
|
const bound = a.path === null ? a.bound : b.bound;
|
|
756
962
|
if (!pathHitsBound(path, bound))
|
|
757
963
|
return false;
|
|
758
|
-
return
|
|
964
|
+
return minPenetration <= 0 || pathPenetration(path, bound) > minPenetration;
|
|
759
965
|
}
|
|
760
966
|
/**
|
|
761
967
|
* How far two overlapping boxes reach INTO each other: the smaller of the two
|
|
@@ -1421,7 +1627,8 @@ function evaluateMajorityFact(rule, elements) {
|
|
|
1421
1627
|
return violations;
|
|
1422
1628
|
}
|
|
1423
1629
|
/**
|
|
1424
|
-
* The two ids a typed edge
|
|
1630
|
+
* The two ids a typed edge points at, or `null` — the EXTRACTION alone, with no
|
|
1631
|
+
* opinion on what the pair means.
|
|
1425
1632
|
*
|
|
1426
1633
|
* Duck-typed exactly like {@link elementPath}, and for the same reason: the
|
|
1427
1634
|
* engine knows roles and geometry, and must not import a connector model. An
|
|
@@ -1431,19 +1638,31 @@ function evaluateMajorityFact(rule, elements) {
|
|
|
1431
1638
|
* tool over empty canvas produces such an edge at any time, and a palette
|
|
1432
1639
|
* sample of a stroke style is one by construction. An edge that relates nothing
|
|
1433
1640
|
* is never evaluated.
|
|
1641
|
+
*
|
|
1642
|
+
* Whether a SELF-LOOP is one of the answers is a policy, and each family states
|
|
1643
|
+
* its own on top of this: see {@link boundEnds} and {@link endpointIds}.
|
|
1434
1644
|
*/
|
|
1435
|
-
function
|
|
1645
|
+
function rawEndpointIds(el) {
|
|
1436
1646
|
const edge = el;
|
|
1437
1647
|
const source = edge.source?.id;
|
|
1438
1648
|
const target = edge.target?.id;
|
|
1439
1649
|
if (typeof source !== 'string' || typeof target !== 'string')
|
|
1440
1650
|
return null;
|
|
1441
|
-
// A self-loop states nothing about an order: it is one element, compared with
|
|
1442
|
-
// itself, and no move can ever resolve it.
|
|
1443
|
-
if (source === target)
|
|
1444
|
-
return null;
|
|
1445
1651
|
return [source, target];
|
|
1446
1652
|
}
|
|
1653
|
+
/**
|
|
1654
|
+
* The two ids a typed edge BINDS, self-loops excluded.
|
|
1655
|
+
*
|
|
1656
|
+
* A self-loop states nothing about an ORDER: it is one element, compared with
|
|
1657
|
+
* itself, and no move can ever resolve the finding — so the families that read
|
|
1658
|
+
* a layout treat it exactly like an edge that relates nothing.
|
|
1659
|
+
*/
|
|
1660
|
+
function boundEnds(el) {
|
|
1661
|
+
const ends = rawEndpointIds(el);
|
|
1662
|
+
if (ends === null || ends[0] === ends[1])
|
|
1663
|
+
return null;
|
|
1664
|
+
return ends;
|
|
1665
|
+
}
|
|
1447
1666
|
/**
|
|
1448
1667
|
* "Are these two in the order their relation states?" — W4 (`docs/adr/0010`).
|
|
1449
1668
|
*
|
|
@@ -1541,6 +1760,1296 @@ function evaluateRelativeOrder(rule, elements) {
|
|
|
1541
1760
|
}
|
|
1542
1761
|
return violations;
|
|
1543
1762
|
}
|
|
1763
|
+
/**
|
|
1764
|
+
* The two ids a typed edge points at, SELF-LOOP INCLUDED — the one difference
|
|
1765
|
+
* with {@link boundEnds}, and the reason this family reads the raw pair.
|
|
1766
|
+
*
|
|
1767
|
+
* `boundEnds` answers `null` for an edge whose two ends are the same element,
|
|
1768
|
+
* because an order rule genuinely has nothing to say about one: it would be
|
|
1769
|
+
* comparing an element with itself and no move could ever resolve the finding.
|
|
1770
|
+
* A GRAMMAR rule is the opposite case — a loop is a sentence about the same
|
|
1771
|
+
* artefact twice, which is exactly what {@link RelationEndpointsDef.forbidSelfLoop}
|
|
1772
|
+
* exists to arbitrate. Folding the loop into the null here would make that flag
|
|
1773
|
+
* unreachable, silently.
|
|
1774
|
+
*/
|
|
1775
|
+
const endpointIds = rawEndpointIds;
|
|
1776
|
+
/**
|
|
1777
|
+
* The roles the sanctioned sentences speak of — the ALPHABET of the rule. See
|
|
1778
|
+
* {@link RelationEndpointsDef.allowed} for why an end outside it is silence.
|
|
1779
|
+
*/
|
|
1780
|
+
function endpointAlphabet(allowed) {
|
|
1781
|
+
const roles = [];
|
|
1782
|
+
for (const triplet of allowed) {
|
|
1783
|
+
for (const role of [triplet.source, triplet.target]) {
|
|
1784
|
+
if (!roles.includes(role))
|
|
1785
|
+
roles.push(role);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return roles;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Whether `role` is one the rule's own framework DECLARES.
|
|
1792
|
+
*
|
|
1793
|
+
* The FALLBACK half of {@link RelationEndpointsDef.flagNeutral}'s vocabulary
|
|
1794
|
+
* test — used on its own only by a rule that declares no matrix, and composed
|
|
1795
|
+
* with the grammar's own alphabet when there is one (see the call site).
|
|
1796
|
+
*
|
|
1797
|
+
* A membership test on the vocabulary the rule already carries, and deliberately
|
|
1798
|
+
* not a prefix match on the `framework:` half of the id: the namespace is a
|
|
1799
|
+
* naming convention (`role.ts` types a role id as a bare string and says as
|
|
1800
|
+
* much), while the vocabulary is data the framework hands the engine. Reading
|
|
1801
|
+
* the data cannot be wrong about a framework that spells its own prefix
|
|
1802
|
+
* differently — `ddd-context-map` roles are `context-map:` — and it keeps this
|
|
1803
|
+
* file free of string surgery on ids it does not own.
|
|
1804
|
+
*
|
|
1805
|
+
* `roleIsA` against every declared role would give the same answer at a higher
|
|
1806
|
+
* price: a role outside the vocabulary has no parent chain to walk here, since
|
|
1807
|
+
* the walk reads `rule.roles`, so it can only ever match itself.
|
|
1808
|
+
*
|
|
1809
|
+
* `Object.hasOwn`, and never a bare index: `role` comes off a `Y.Map`, so it is
|
|
1810
|
+
* whatever a peer or an importer wrote, and an element persisted with
|
|
1811
|
+
* `role: 'toString'` would answer "declared" against a bag containing no such
|
|
1812
|
+
* entry. The same threat model {@link elementExceptions} guards with its
|
|
1813
|
+
* `Array.isArray`. Every other read of a role bag in this file goes through
|
|
1814
|
+
* `roleIsA`, which writes `defs[current]?.parent` and terminates harmlessly on
|
|
1815
|
+
* a prototype hit; this is the only place that tests for the PRESENCE of a key.
|
|
1816
|
+
*/
|
|
1817
|
+
function declaresRole(rule, role) {
|
|
1818
|
+
return role !== undefined && Object.hasOwn(rule.roles, role);
|
|
1819
|
+
}
|
|
1820
|
+
/** The elements a finding names, deduplicated and sorted. */
|
|
1821
|
+
function namedElements(ids) {
|
|
1822
|
+
return Array.from(new Set(ids)).sort();
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* "Does this relation say something the framework's grammar allows?"
|
|
1826
|
+
*
|
|
1827
|
+
* The second family whose subject is a RELATION, and the first that reads
|
|
1828
|
+
* nothing but MEANING: no axis, no coordinate, no distance. For every element
|
|
1829
|
+
* carrying the declared edge role it reads the persisted `source → target` pair
|
|
1830
|
+
* and the roles at the two ends, and confronts the sentence with the ones the
|
|
1831
|
+
* framework sanctions — an actor issues a command, a command lands on an
|
|
1832
|
+
* aggregate. A relation drawn upside-down on a Wardley map is a layout mistake;
|
|
1833
|
+
* a relation drawn between the wrong two artefacts is a modelling one, and no
|
|
1834
|
+
* amount of dragging fixes it.
|
|
1835
|
+
*
|
|
1836
|
+
* ## What it stays silent about, and why each one matters
|
|
1837
|
+
*
|
|
1838
|
+
* - **an edge carrying no role, or another role** — proportionality (PRD
|
|
1839
|
+
* principle 8): a free connector is not a claim. Unless the rule declares
|
|
1840
|
+
* {@link RelationEndpointsDef.flagNeutral}, which asks for exactly one kind of
|
|
1841
|
+
* role-less link back: one drawn between two artefacts of this framework's own
|
|
1842
|
+
* vocabulary, i.e. a relation the user drew and the model does not contain;
|
|
1843
|
+
* - **an edge with a free end** — it relates nothing;
|
|
1844
|
+
* - **an end whose element is gone** — a dangling id says nothing about a model;
|
|
1845
|
+
* - **an end OUTSIDE the alphabet of {@link RelationEndpointsDef.allowed}** —
|
|
1846
|
+
* the hard requirement of this family. A flow onto a hotspot, onto a neutral
|
|
1847
|
+
* shape, onto an artefact of another framework: the user is sketching, and a
|
|
1848
|
+
* grammar that indicted the sketch would be switched off within a day (see
|
|
1849
|
+
* {@link RelationEndpointsDef.allowed});
|
|
1850
|
+
* - **a self-loop, unless the framework asked** — see
|
|
1851
|
+
* {@link RelationEndpointsDef.forbidSelfLoop}. Silence here is TOTAL: a loop
|
|
1852
|
+
* the framework tolerates is not then judged against the matrix, where it
|
|
1853
|
+
* would fail for a reason nobody meant.
|
|
1854
|
+
*
|
|
1855
|
+
* ## One finding per edge, at most
|
|
1856
|
+
*
|
|
1857
|
+
* An edge already indicted for the sentence it says is not indicted again for
|
|
1858
|
+
* saying it twice: the duplicate and exclusivity passes only see relations that
|
|
1859
|
+
* got through the matrix. Two badges on one link, for one gesture to fix, is
|
|
1860
|
+
* pedantry the canvas cannot afford.
|
|
1861
|
+
*
|
|
1862
|
+
* ## The frame
|
|
1863
|
+
*
|
|
1864
|
+
* {@link ValidationRule.backgroundRole} is optional here and buys ATTRIBUTION
|
|
1865
|
+
* only — the map a finding is written against, so the map-wide arbitration has
|
|
1866
|
+
* somewhere to live (the `no-overlap` pattern). No background DECLARATION is
|
|
1867
|
+
* required: this family reads no fact off the frame, and demanding one would be
|
|
1868
|
+
* asking a framework to declare geometry its grammar never consults.
|
|
1869
|
+
*
|
|
1870
|
+
* ## Cost
|
|
1871
|
+
*
|
|
1872
|
+
* Linear in the elements plus linear in the EDGES: one indexing pass, one pass
|
|
1873
|
+
* over the edges, then one pass over the node pairs that actually carry more
|
|
1874
|
+
* than one relation — which on any real board is a handful. There is no
|
|
1875
|
+
* pair-wise sweep over the nodes: the rule is about relations somebody drew,
|
|
1876
|
+
* never about every couple that could have had one.
|
|
1877
|
+
*
|
|
1878
|
+
* An {@link IncrementalContext} is deliberately NOT honoured, exactly like
|
|
1879
|
+
* `relative-order-along-axis`: a dirty set names the elements that moved, and
|
|
1880
|
+
* this family's verdict does not depend on where anything is. Re-reading every
|
|
1881
|
+
* edge is the cost of the linear pass it already was.
|
|
1882
|
+
*/
|
|
1883
|
+
function evaluateRelationEndpoints(rule, elements) {
|
|
1884
|
+
const endpoints = rule.endpoints;
|
|
1885
|
+
if (endpoints === undefined)
|
|
1886
|
+
return [];
|
|
1887
|
+
const byId = new Map();
|
|
1888
|
+
const edges = [];
|
|
1889
|
+
// Role-less connectors, kept only when the rule asked to hear about them —
|
|
1890
|
+
// and only the ones that are connectors at all, which the persisted pair is
|
|
1891
|
+
// the duck-typed proof of. A board is mostly neutral elements, and a rule
|
|
1892
|
+
// declaring no `flagNeutral` must not allocate one entry per rectangle.
|
|
1893
|
+
const neutral = [];
|
|
1894
|
+
for (const el of elements) {
|
|
1895
|
+
byId.set(el.id, el);
|
|
1896
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation…
|
|
1897
|
+
if (el.role === undefined) {
|
|
1898
|
+
// …unless the rule is precisely about the links that carry none.
|
|
1899
|
+
if (endpoints.flagNeutral !== undefined && rawEndpointIds(el) !== null) {
|
|
1900
|
+
neutral.push(el);
|
|
1901
|
+
}
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
if (roleIsA(el.role, endpoints.edgeRole, rule.roles)) {
|
|
1905
|
+
edges.push({ el, role: el.role });
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
if (edges.length === 0 && neutral.length === 0)
|
|
1909
|
+
return [];
|
|
1910
|
+
const backgrounds = backgroundsOf(rule, elements);
|
|
1911
|
+
// An EMPTY matrix is a matrix that says nothing, which is the same claim as
|
|
1912
|
+
// declaring none — the reading `allowed`'s own header gives it — so it is
|
|
1913
|
+
// normalised to `undefined` here rather than left to mean an empty alphabet.
|
|
1914
|
+
// Left as one it would silence the rule WHOLE, self-loops and duplicates
|
|
1915
|
+
// included, since every end is outside an alphabet of no roles: exactly the
|
|
1916
|
+
// opposite of what a framework that lists no sentence and forbids loops
|
|
1917
|
+
// asked for. A matrix says nothing about the other three verdicts; it never
|
|
1918
|
+
// switches them off.
|
|
1919
|
+
const allowed = endpoints.allowed === undefined || endpoints.allowed.length === 0
|
|
1920
|
+
? undefined
|
|
1921
|
+
: endpoints.allowed;
|
|
1922
|
+
const alphabet = allowed === undefined ? null : endpointAlphabet(allowed);
|
|
1923
|
+
// `roleIsA` walks a parent chain, and the two questions below ask it about
|
|
1924
|
+
// the same handful of roles once per edge on the board — a Context Mapping
|
|
1925
|
+
// board draws one vocabulary a hundred times over. Memoised for the duration
|
|
1926
|
+
// of THIS pass and no longer: the precedent is `evaluateNoOverlap`, which
|
|
1927
|
+
// precomputes each subject's slot memberships once instead of re-asking per
|
|
1928
|
+
// couple.
|
|
1929
|
+
const spoken = new Map();
|
|
1930
|
+
const speaks = (role) => {
|
|
1931
|
+
if (alphabet === null)
|
|
1932
|
+
return true;
|
|
1933
|
+
if (role === undefined)
|
|
1934
|
+
return false;
|
|
1935
|
+
const memo = spoken.get(role);
|
|
1936
|
+
if (memo !== undefined)
|
|
1937
|
+
return memo;
|
|
1938
|
+
const answer = alphabet.some(known => roleIsA(role, known, rule.roles));
|
|
1939
|
+
spoken.set(role, answer);
|
|
1940
|
+
return answer;
|
|
1941
|
+
};
|
|
1942
|
+
// Keyed by the whole triplet of roles, joined by NUL — which no vocabulary
|
|
1943
|
+
// contains — so no two sentences can ever fold into one answer.
|
|
1944
|
+
const sanctioned = new Map();
|
|
1945
|
+
const inMatrix = (edgeRole, sourceRole, targetRole) => {
|
|
1946
|
+
const key = `${edgeRole}\u0000${sourceRole}\u0000${targetRole}`;
|
|
1947
|
+
const memo = sanctioned.get(key);
|
|
1948
|
+
if (memo !== undefined)
|
|
1949
|
+
return memo;
|
|
1950
|
+
const answer = allowed.some(triplet => roleIsA(edgeRole, triplet.edge, rule.roles) &&
|
|
1951
|
+
roleIsA(sourceRole, triplet.source, rule.roles) &&
|
|
1952
|
+
roleIsA(targetRole, triplet.target, rule.roles));
|
|
1953
|
+
sanctioned.set(key, answer);
|
|
1954
|
+
return answer;
|
|
1955
|
+
};
|
|
1956
|
+
const exclusivePairs = endpoints.exclusivePairs ?? [];
|
|
1957
|
+
// Hoisted above the walk: the pair bookkeeping is a `DrawnRelation`, a sorted
|
|
1958
|
+
// id list and a map entry PER EDGE, and a rule that judges sentences only
|
|
1959
|
+
// would have allocated all three just to drop them. This runs on the gesture
|
|
1960
|
+
// path, inside the 16 ms the drawing has.
|
|
1961
|
+
const needsPairs = endpoints.forbidDuplicate === true || exclusivePairs.length > 0;
|
|
1962
|
+
const violations = [];
|
|
1963
|
+
// Every relation drawn between one UNORDERED pair of elements, which is the
|
|
1964
|
+
// grain both the duplicate and the exclusivity questions are asked at. Filled
|
|
1965
|
+
// in the same pass that judges the sentences, walked once afterwards.
|
|
1966
|
+
const between = new Map();
|
|
1967
|
+
for (const { el: edge, role } of edges) {
|
|
1968
|
+
const ends = endpointIds(edge);
|
|
1969
|
+
if (ends === null)
|
|
1970
|
+
continue;
|
|
1971
|
+
const [sourceId, targetId] = ends;
|
|
1972
|
+
const source = byId.get(sourceId);
|
|
1973
|
+
const target = byId.get(targetId);
|
|
1974
|
+
if (source === undefined || target === undefined)
|
|
1975
|
+
continue;
|
|
1976
|
+
// Outside the grammar entirely: the user is sketching.
|
|
1977
|
+
if (!speaks(source.role) || !speaks(target.role))
|
|
1978
|
+
continue;
|
|
1979
|
+
// A node, not the edge's own box: the box of a diagonal link spans both
|
|
1980
|
+
// ends and would attribute a relation to whichever map its corner grazes.
|
|
1981
|
+
const frameId = attributeBackground(source.elementBound, backgrounds)?.id;
|
|
1982
|
+
if (sourceId === targetId) {
|
|
1983
|
+
if (endpoints.forbidSelfLoop) {
|
|
1984
|
+
violations.push(raise(rule, namedElements([edge.id, sourceId, targetId]), frameId, endpoints.selfLoop ?? rule));
|
|
1985
|
+
}
|
|
1986
|
+
continue;
|
|
1987
|
+
}
|
|
1988
|
+
if (allowed !== undefined && !inMatrix(role, source.role, target.role)) {
|
|
1989
|
+
violations.push(raise(rule, namedElements([edge.id, sourceId, targetId]), frameId, endpoints.offMatrix ?? rule));
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
if (!needsPairs)
|
|
1993
|
+
continue;
|
|
1994
|
+
const key = sourceId < targetId
|
|
1995
|
+
? `${sourceId} ${targetId}`
|
|
1996
|
+
: `${targetId} ${sourceId}`;
|
|
1997
|
+
const drawn = between.get(key);
|
|
1998
|
+
const relation = {
|
|
1999
|
+
id: edge.id,
|
|
2000
|
+
role,
|
|
2001
|
+
sourceId,
|
|
2002
|
+
targetId,
|
|
2003
|
+
ids: namedElements([edge.id, sourceId, targetId]),
|
|
2004
|
+
...(frameId !== undefined ? { frameId } : {}),
|
|
2005
|
+
};
|
|
2006
|
+
if (drawn === undefined)
|
|
2007
|
+
between.set(key, [relation]);
|
|
2008
|
+
else
|
|
2009
|
+
drawn.push(relation);
|
|
2010
|
+
}
|
|
2011
|
+
// Which roles a role-less link may be flagged BETWEEN.
|
|
2012
|
+
//
|
|
2013
|
+
// The rule's own ALPHABET when it declares a matrix — the roles its grammar
|
|
2014
|
+
// actually has endpoints for, which is the tightest honest reading and gets
|
|
2015
|
+
// `roleIsA` inheritance and the memo above for free. It also excludes, by
|
|
2016
|
+
// construction and with nothing for an author to remember, the roles a
|
|
2017
|
+
// framework declares for things that are not endpoints of anything: a
|
|
2018
|
+
// background role, a legend glyph, a lane. Presuming somebody meant to draw a
|
|
2019
|
+
// typed relation onto a POOL is exactly the over-firing this test exists to
|
|
2020
|
+
// prevent.
|
|
2021
|
+
//
|
|
2022
|
+
// `speaks` cannot do the job alone, and the reason is worth stating: with no
|
|
2023
|
+
// matrix it answers `true` for everything, `undefined` included, because
|
|
2024
|
+
// "outside the alphabet" has no meaning when there is no alphabet. A
|
|
2025
|
+
// matrix-less rule therefore falls back to membership in the vocabulary
|
|
2026
|
+
// ({@link declaresRole}), which stays purely the dictionary `roleIsA` walks —
|
|
2027
|
+
// the bag is NOT overloaded into a filter, so an author never has to shrink
|
|
2028
|
+
// their inheritance dictionary in order to change what gets flagged.
|
|
2029
|
+
const inVocabulary = (role) => alphabet === null ? declaresRole(rule, role) : speaks(role);
|
|
2030
|
+
// The NEUTRAL pass: a link carrying no role at all, drawn between two
|
|
2031
|
+
// artefacts this rule's grammar could have related. See
|
|
2032
|
+
// {@link RelationEndpointsDef.flagNeutral} — the gesture that produces one is
|
|
2033
|
+
// quick-connect, which stamps nothing, so the user has drawn a relation the
|
|
2034
|
+
// model does not contain.
|
|
2035
|
+
//
|
|
2036
|
+
// The edge is judged on WHAT it joins and never against the matrix: the matrix
|
|
2037
|
+
// says which sentences are legal, and this edge says no sentence at all. A
|
|
2038
|
+
// self-loop is skipped for the same reason `boundEnds` skips one everywhere
|
|
2039
|
+
// else — a link from an artefact to itself is not evidence that a typed
|
|
2040
|
+
// relation was meant.
|
|
2041
|
+
for (const el of neutral) {
|
|
2042
|
+
const ends = boundEnds(el);
|
|
2043
|
+
if (ends === null)
|
|
2044
|
+
continue;
|
|
2045
|
+
const [sourceId, targetId] = ends;
|
|
2046
|
+
const source = byId.get(sourceId);
|
|
2047
|
+
const target = byId.get(targetId);
|
|
2048
|
+
if (source === undefined || target === undefined)
|
|
2049
|
+
continue;
|
|
2050
|
+
if (!inVocabulary(source.role) || !inVocabulary(target.role))
|
|
2051
|
+
continue;
|
|
2052
|
+
violations.push(raise(rule, namedElements([el.id, sourceId, targetId]), attributeBackground(source.elementBound, backgrounds)?.id, endpoints.flagNeutral));
|
|
2053
|
+
}
|
|
2054
|
+
if (!needsPairs)
|
|
2055
|
+
return violations;
|
|
2056
|
+
// Sorted, so a board holding several offending pairs always reports them the
|
|
2057
|
+
// same way whichever order the surface happened to be walked in.
|
|
2058
|
+
for (const key of [...between.keys()].sort()) {
|
|
2059
|
+
const drawn = between.get(key);
|
|
2060
|
+
if (drawn.length < 2)
|
|
2061
|
+
continue;
|
|
2062
|
+
// By edge id, so the relation that keeps its innocence is the same one on
|
|
2063
|
+
// every peer and after every reload — never the one a Y.Map rebuild
|
|
2064
|
+
// happened to yield first.
|
|
2065
|
+
const relations = [...drawn].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
2066
|
+
if (endpoints.forbidDuplicate) {
|
|
2067
|
+
const seen = new Set();
|
|
2068
|
+
for (const relation of relations) {
|
|
2069
|
+
// Same role AND same ordered pair: A → B and B → A are two different
|
|
2070
|
+
// sentences, and a framework that wants them exclusive says so in
|
|
2071
|
+
// `exclusivePairs`.
|
|
2072
|
+
const sentence = `${relation.role} ${relation.sourceId} ${relation.targetId}`;
|
|
2073
|
+
// The FIRST one is not a duplicate of anything; every copy after it is.
|
|
2074
|
+
if (seen.has(sentence)) {
|
|
2075
|
+
violations.push(raise(rule, relation.ids, relation.frameId, endpoints.duplicate ?? rule));
|
|
2076
|
+
}
|
|
2077
|
+
seen.add(sentence);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
// Two declared pairs can both cover the same two relations (a pair written
|
|
2081
|
+
// on a parent role and one on its specialisation): one situation, one
|
|
2082
|
+
// finding.
|
|
2083
|
+
const reported = new Set();
|
|
2084
|
+
for (const [first, second] of exclusivePairs) {
|
|
2085
|
+
for (let i = 0; i < relations.length; i++) {
|
|
2086
|
+
for (let j = i + 1; j < relations.length; j++) {
|
|
2087
|
+
const a = relations[i];
|
|
2088
|
+
const b = relations[j];
|
|
2089
|
+
const matched = (roleIsA(a.role, first, rule.roles) &&
|
|
2090
|
+
roleIsA(b.role, second, rule.roles)) ||
|
|
2091
|
+
(roleIsA(a.role, second, rule.roles) &&
|
|
2092
|
+
roleIsA(b.role, first, rule.roles));
|
|
2093
|
+
if (!matched)
|
|
2094
|
+
continue;
|
|
2095
|
+
const couple = `${a.id} ${b.id}`;
|
|
2096
|
+
if (reported.has(couple))
|
|
2097
|
+
continue;
|
|
2098
|
+
reported.add(couple);
|
|
2099
|
+
// BOTH relations and both ends: either link is a legitimate
|
|
2100
|
+
// resolution, so neither may be left out of the finding, and the pair
|
|
2101
|
+
// of artefacts is what makes the two of them one situation.
|
|
2102
|
+
//
|
|
2103
|
+
// The frame is the SMALLER of the two the relations were attributed
|
|
2104
|
+
// to — the house tie-break, and it has to be one: each relation is
|
|
2105
|
+
// attributed by its OWN source node, so two links drawn from opposite
|
|
2106
|
+
// ends can name two frames, and taking the first one's would write a
|
|
2107
|
+
// map-wide arbitration wherever the surface happened to be walked.
|
|
2108
|
+
const frameId = b.frameId !== undefined &&
|
|
2109
|
+
(a.frameId === undefined || b.frameId < a.frameId)
|
|
2110
|
+
? b.frameId
|
|
2111
|
+
: a.frameId;
|
|
2112
|
+
violations.push(raise(rule, namedElements([a.id, b.id, a.sourceId, a.targetId]), frameId, endpoints.exclusive ?? rule));
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
return violations;
|
|
2118
|
+
}
|
|
2119
|
+
/**
|
|
2120
|
+
* The cited zones of one instance, as model-space rectangles.
|
|
2121
|
+
*
|
|
2122
|
+
* Ratios are of the PLOT — the element box minus the declared margin — so this
|
|
2123
|
+
* is the same resolution `backgroundBoundaryCoords` does for transitions, and
|
|
2124
|
+
* the reason a rule never restates a coordinate the declaration already owns.
|
|
2125
|
+
* In the rule's OWN citation order, so `boundaryId` names the first zone the
|
|
2126
|
+
* rule itself mentions rather than the first one the declaration happens to
|
|
2127
|
+
* list.
|
|
2128
|
+
*
|
|
2129
|
+
* ## Resolved against the INSTANCE, variants included
|
|
2130
|
+
*
|
|
2131
|
+
* A variant is a second reading of the same frame, and a zone that belongs to
|
|
2132
|
+
* the other reading is not drawn on this instance at all: it is not a region of
|
|
2133
|
+
* it either, and measuring against a quadrant nobody can see would indict an
|
|
2134
|
+
* artefact for sitting where the chart shows nothing. Same gate the renderer and
|
|
2135
|
+
* the label walk pass through, read off the same props.
|
|
2136
|
+
*
|
|
2137
|
+
* Silence, not {@link warnOnce}: a rule citing a zone the declaration does not
|
|
2138
|
+
* declare is a broken RULE and says so, but a rule citing a zone this INSTANCE
|
|
2139
|
+
* does not read is a perfectly good rule looking at a frame turned to its other
|
|
2140
|
+
* page.
|
|
2141
|
+
*/
|
|
2142
|
+
function zoneRegions(def, zoneIds, frame) {
|
|
2143
|
+
const bound = frame.bound;
|
|
2144
|
+
const plot = backgroundPlot(def, bound.w, bound.h);
|
|
2145
|
+
const regions = [];
|
|
2146
|
+
for (const id of zoneIds) {
|
|
2147
|
+
const zone = (def.zones ?? []).find(candidate => candidate.id === id);
|
|
2148
|
+
if (zone === undefined)
|
|
2149
|
+
continue;
|
|
2150
|
+
if (!backgroundInVariant(def, zone.variants, frame.props))
|
|
2151
|
+
continue;
|
|
2152
|
+
regions.push({
|
|
2153
|
+
id,
|
|
2154
|
+
bound: new Bound(bound.x + plot.x0 + zone.rect.x * plot.width, bound.y + plot.y0 + zone.rect.y * plot.height, zone.rect.w * plot.width, zone.rect.h * plot.height),
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
return regions;
|
|
2158
|
+
}
|
|
2159
|
+
/**
|
|
2160
|
+
* The instance a subject is judged against: the one that CONTAINS it.
|
|
2161
|
+
*
|
|
2162
|
+
* Not `attributeBackground`, and the difference is the point: "which zone is
|
|
2163
|
+
* this in" only means something for an artefact that is ON the map. An element
|
|
2164
|
+
* next to the card is outside every zone of it, which under `expect: 'inside'`
|
|
2165
|
+
* would make it a finding of THIS rule — a second, worse-worded version of
|
|
2166
|
+
* `element-in-background`, raised on artefacts the framework may perfectly well
|
|
2167
|
+
* allow off the frame. Off every frame is silence here, and whether that is
|
|
2168
|
+
* itself a mistake is a different rule's question.
|
|
2169
|
+
*
|
|
2170
|
+
* Ties go to the smaller id, never to the order a `Y.Map` was rebuilt in: the
|
|
2171
|
+
* answer decides where a persisted arbitration is written.
|
|
2172
|
+
*/
|
|
2173
|
+
function containingBackground(bound, backgrounds) {
|
|
2174
|
+
let found = null;
|
|
2175
|
+
for (const background of backgrounds) {
|
|
2176
|
+
if (!background.bound.contains(bound))
|
|
2177
|
+
continue;
|
|
2178
|
+
if (found === null || background.id < found.id)
|
|
2179
|
+
found = background;
|
|
2180
|
+
}
|
|
2181
|
+
return found;
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* "Is this artefact in the region it belongs in?" — the zone half of
|
|
2185
|
+
* `element-in-background`.
|
|
2186
|
+
*
|
|
2187
|
+
* The zones are the frame's own declared ones, named by id: a rule says
|
|
2188
|
+
* `zoneIds: ['core'], expect: 'outside'` and the geometry comes from the
|
|
2189
|
+
* declaration the renderer paints from. Nothing is restated, so a framework
|
|
2190
|
+
* that redraws its quadrants moves its rules with them.
|
|
2191
|
+
*
|
|
2192
|
+
* ## Not gated on the tint
|
|
2193
|
+
*
|
|
2194
|
+
* A zone is SEMANTIC; its `fillVisibleProp` only decides whether it is
|
|
2195
|
+
* coloured in. A user who switched the tints off to print a clean chart has not
|
|
2196
|
+
* told the tool that the Core quadrant stopped being the Core quadrant — the
|
|
2197
|
+
* labels, the axes and the whole point of the chart are still there.
|
|
2198
|
+
*
|
|
2199
|
+
* A deliberate contrast with `attachment`, which DOES read the instance's props
|
|
2200
|
+
* before judging a transition, and the two are not in tension: a transition is a
|
|
2201
|
+
* LINE, and a line the user has hidden is a line they cannot aim at, so the
|
|
2202
|
+
* gesture the finding asks for would point at nothing. A quadrant is a REGION of
|
|
2203
|
+
* a frame of reference that is still fully on screen. Judge the drawing that is
|
|
2204
|
+
* on the canvas — and a hidden tint does not take a quadrant off it.
|
|
2205
|
+
*/
|
|
2206
|
+
function evaluateElementInZone(rule, elements) {
|
|
2207
|
+
const subjectRole = rule.appliesTo;
|
|
2208
|
+
const inZone = rule.inZone;
|
|
2209
|
+
const def = rule.background;
|
|
2210
|
+
if (subjectRole === undefined || inZone === undefined || def === undefined) {
|
|
2211
|
+
return [];
|
|
2212
|
+
}
|
|
2213
|
+
if (inZone.zoneIds.length === 0)
|
|
2214
|
+
return [];
|
|
2215
|
+
const declared = new Set((def.zones ?? []).map(zone => zone.id));
|
|
2216
|
+
const unknown = inZone.zoneIds.filter(id => !declared.has(id));
|
|
2217
|
+
if (unknown.length > 0) {
|
|
2218
|
+
// Measuring against the zones it happened to recognise would be a DIFFERENT
|
|
2219
|
+
// rule — a narrower union, a shorter list of forbidden ground — and nothing
|
|
2220
|
+
// on the canvas would show the difference.
|
|
2221
|
+
warnOnce(`element-in-zone rule "${rule.id}" names zone(s) its background does ` +
|
|
2222
|
+
`not declare (${unknown.join(', ')}) — the rule is not evaluated.`);
|
|
2223
|
+
return [];
|
|
2224
|
+
}
|
|
2225
|
+
const backgrounds = backgroundsOf(rule, elements);
|
|
2226
|
+
// No frame on the board, no zones to be in: an artefact on blank canvas is a
|
|
2227
|
+
// sketch.
|
|
2228
|
+
if (backgrounds.length === 0)
|
|
2229
|
+
return [];
|
|
2230
|
+
// Zones are a function of the frame's bounds AND of the reading it is turned
|
|
2231
|
+
// to, and a board carries units of frames against hundreds of subjects:
|
|
2232
|
+
// resolved once per frame, per variant. Both halves are in the key, so the
|
|
2233
|
+
// entry can never outlive the reading it was built for — an id alone would be
|
|
2234
|
+
// right only for as long as nobody moved this map out of one evaluation.
|
|
2235
|
+
const regionsOf = new Map();
|
|
2236
|
+
const extent = inZone.measure === 'extent';
|
|
2237
|
+
const violations = [];
|
|
2238
|
+
for (const el of elements) {
|
|
2239
|
+
if (el.role === undefined)
|
|
2240
|
+
continue;
|
|
2241
|
+
if (!roleIsA(el.role, subjectRole, rule.roles))
|
|
2242
|
+
continue;
|
|
2243
|
+
const bound = el.elementBound;
|
|
2244
|
+
const frame = containingBackground(bound, backgrounds);
|
|
2245
|
+
if (frame === null)
|
|
2246
|
+
continue;
|
|
2247
|
+
// Keyed by the frame AND the reading it is turned to, joined by NUL — which
|
|
2248
|
+
// no id and no variant name contains — so the two halves can never fold
|
|
2249
|
+
// into one entry.
|
|
2250
|
+
const variant = def.variantProp === undefined ? '' : String(frame.props[def.variantProp]);
|
|
2251
|
+
const key = `${frame.id}\u0000${variant}`;
|
|
2252
|
+
let regions = regionsOf.get(key);
|
|
2253
|
+
if (regions === undefined) {
|
|
2254
|
+
regions = zoneRegions(def, inZone.zoneIds, frame);
|
|
2255
|
+
regionsOf.set(key, regions);
|
|
2256
|
+
}
|
|
2257
|
+
// Every cited zone belongs to the frame's OTHER reading: this instance has
|
|
2258
|
+
// no such region, so there is nothing to be inside or outside of. Under
|
|
2259
|
+
// `expect: 'inside'` the alternative would be indicting every subject on a
|
|
2260
|
+
// frame that simply shows another page.
|
|
2261
|
+
if (regions.length === 0)
|
|
2262
|
+
continue;
|
|
2263
|
+
const centre = centreOf(bound);
|
|
2264
|
+
// `inside` asks for the subject to be WHOLLY in one zone; `outside` for it
|
|
2265
|
+
// to share no area with any. The border belongs to the zone in both
|
|
2266
|
+
// readings, so two neighbours never disagree about an artefact on their
|
|
2267
|
+
// frontier.
|
|
2268
|
+
const occupies = (region) => extent
|
|
2269
|
+
? region.bound.contains(bound)
|
|
2270
|
+
: region.bound.containsPoint(centre);
|
|
2271
|
+
const touches = (region) => extent
|
|
2272
|
+
? boundsOverlap(region.bound, bound)
|
|
2273
|
+
: region.bound.containsPoint(centre);
|
|
2274
|
+
if (inZone.expect === 'inside') {
|
|
2275
|
+
if (regions.some(occupies))
|
|
2276
|
+
continue;
|
|
2277
|
+
// The zone the subject missed: the nearest one, which is the one the user
|
|
2278
|
+
// was aiming at and the only one a suggestion can point back to.
|
|
2279
|
+
let nearest;
|
|
2280
|
+
let distance = Infinity;
|
|
2281
|
+
for (const region of regions) {
|
|
2282
|
+
const gap = gapSquared(region.bound, bound);
|
|
2283
|
+
if (gap < distance) {
|
|
2284
|
+
distance = gap;
|
|
2285
|
+
nearest = region;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
const violation = raise(rule, [el.id], frame.id);
|
|
2289
|
+
if (nearest !== undefined)
|
|
2290
|
+
violation.boundaryId = nearest.id;
|
|
2291
|
+
violations.push(violation);
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
const trespassed = regions.find(touches);
|
|
2295
|
+
if (trespassed === undefined)
|
|
2296
|
+
continue;
|
|
2297
|
+
const violation = raise(rule, [el.id], frame.id);
|
|
2298
|
+
violation.boundaryId = trespassed.id;
|
|
2299
|
+
violations.push(violation);
|
|
2300
|
+
}
|
|
2301
|
+
return violations;
|
|
2302
|
+
}
|
|
2303
|
+
/**
|
|
2304
|
+
* "Does this artefact belong on THIS view?"
|
|
2305
|
+
*
|
|
2306
|
+
* The first family whose subject is the SHEET. Every other one starts from an
|
|
2307
|
+
* artefact — a node, a relation, a pair — and asks something about where it is
|
|
2308
|
+
* or what touches it. This one starts from a frame that has DECLARED what kind
|
|
2309
|
+
* of view it is, and judges what has been drawn on it against that declaration.
|
|
2310
|
+
*
|
|
2311
|
+
* The level is read off the instance, from the prop the rule names
|
|
2312
|
+
* ({@link ViewAdmissibilityDef.levelProp}) — the very idiom the background
|
|
2313
|
+
* primitives already use for labels and variants — so the engine learns which
|
|
2314
|
+
* views exist from the document rather than from a registry of its own.
|
|
2315
|
+
*
|
|
2316
|
+
* ## What it stays silent about, and why each one matters
|
|
2317
|
+
*
|
|
2318
|
+
* - **a frame that declares NO level** — the overwhelming majority, and every
|
|
2319
|
+
* frame in every document written before the prop existed. No fact, no rule
|
|
2320
|
+
* (PRD principle 8): a sheet whose author never said which view it is has
|
|
2321
|
+
* made no claim for anything to contradict, and a family that guessed would
|
|
2322
|
+
* light up every diagram already on disk.
|
|
2323
|
+
* - **a level the rule's table does not name**, or names with an empty list —
|
|
2324
|
+
* the rule has nothing to say about that view. That is what lets a framework
|
|
2325
|
+
* ship the levels that forbid something and stay quiet on the one that
|
|
2326
|
+
* forbids nothing, instead of a rule that can never fire.
|
|
2327
|
+
* - **an element carrying no role** — a rectangle somebody dropped on the sheet
|
|
2328
|
+
* to think with belongs to no notation, so no view can refuse it.
|
|
2329
|
+
* - **an element off every declared view** — it is on the canvas beside the
|
|
2330
|
+
* sheet, not on it, and a view judges what is drawn ON it.
|
|
2331
|
+
* - **the frame itself**, and any other frame of the same role: a view is not
|
|
2332
|
+
* an artefact drawn on itself.
|
|
2333
|
+
*
|
|
2334
|
+
* ## Which view judges an element
|
|
2335
|
+
*
|
|
2336
|
+
* The one whose plot contains its CENTRE ({@link plotContaining}) — membership,
|
|
2337
|
+
* with the same tie-break every attribution in this file uses, and never the
|
|
2338
|
+
* nearest-frame fallback: "this artefact is on that view" has to mean it, or a
|
|
2339
|
+
* box floating beside a context diagram would be indicted by it.
|
|
2340
|
+
*
|
|
2341
|
+
* A subject on two nested views is judged by ONE of them, the smaller id, which
|
|
2342
|
+
* is the same answer `role-count` and `edge-locality` give. Two views drawn on
|
|
2343
|
+
* top of each other is not a case C4 has, and inventing a second verdict for it
|
|
2344
|
+
* would mean inventing which of the two the author meant.
|
|
2345
|
+
*
|
|
2346
|
+
* ## Cost, and why it is realtime-safe
|
|
2347
|
+
*
|
|
2348
|
+
* One pass to collect the views (units per board), one plot resolved per view,
|
|
2349
|
+
* then ONE pass over the elements with one containment walk and at most
|
|
2350
|
+
* `forbidden.length` role tests each. Linear in the surface, no graph, no pair
|
|
2351
|
+
* walk, nothing quadratic — the same budget `element-in-background` and
|
|
2352
|
+
* `role-count` are measured on.
|
|
2353
|
+
*/
|
|
2354
|
+
function evaluateViewAdmissibility(rule, elements) {
|
|
2355
|
+
const def = rule.admissibility;
|
|
2356
|
+
if (def === undefined)
|
|
2357
|
+
return [];
|
|
2358
|
+
if (missingFrameRole(rule, 'view-admissibility'))
|
|
2359
|
+
return [];
|
|
2360
|
+
// The views that have both DECLARED a level and given this rule something to
|
|
2361
|
+
// say about it. Resolved once, per frame — the plot arithmetic and the table
|
|
2362
|
+
// lookup must not be paid per subject.
|
|
2363
|
+
const views = [];
|
|
2364
|
+
const forbiddenOf = new Map();
|
|
2365
|
+
for (const frame of framePlots(rule, elements)) {
|
|
2366
|
+
const stated = frame.instance.props[def.levelProp];
|
|
2367
|
+
// A level is a non-empty string or it is nothing. The value comes off a
|
|
2368
|
+
// Y.Map, so it is whatever a peer, an importer or an older client wrote.
|
|
2369
|
+
if (typeof stated !== 'string' || stated === '')
|
|
2370
|
+
continue;
|
|
2371
|
+
// Own keys only: `forbidden` is an object literal shipped by a framework.
|
|
2372
|
+
if (!Object.hasOwn(def.forbidden, stated))
|
|
2373
|
+
continue;
|
|
2374
|
+
const forbidden = def.forbidden[stated];
|
|
2375
|
+
if (forbidden === undefined || forbidden.length === 0)
|
|
2376
|
+
continue;
|
|
2377
|
+
views.push(frame);
|
|
2378
|
+
forbiddenOf.set(frame.instance.id, forbidden);
|
|
2379
|
+
}
|
|
2380
|
+
// No view on the board has stated a level this rule judges: not one element
|
|
2381
|
+
// is walked.
|
|
2382
|
+
if (views.length === 0)
|
|
2383
|
+
return [];
|
|
2384
|
+
const violations = [];
|
|
2385
|
+
for (const el of elements) {
|
|
2386
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation.
|
|
2387
|
+
const role = el.role;
|
|
2388
|
+
if (role === undefined)
|
|
2389
|
+
continue;
|
|
2390
|
+
// A view is never an artefact drawn on ITSELF — its own centre is inside
|
|
2391
|
+
// its own plot, so without this a rule forbidding the frame's own role
|
|
2392
|
+
// would indict every frame that declared a level.
|
|
2393
|
+
if (forbiddenOf.has(el.id))
|
|
2394
|
+
continue;
|
|
2395
|
+
const view = plotContaining(centreOf(el.elementBound), views);
|
|
2396
|
+
if (view === null)
|
|
2397
|
+
continue;
|
|
2398
|
+
const forbidden = forbiddenOf.get(view.id);
|
|
2399
|
+
if (forbidden === undefined)
|
|
2400
|
+
continue;
|
|
2401
|
+
// One finding per element, whichever forbidden role matched: the author has
|
|
2402
|
+
// drawn one box on the wrong sheet, and telling them twice about it would
|
|
2403
|
+
// be describing our table rather than their diagram.
|
|
2404
|
+
if (forbidden.some(entry => roleIsA(role, entry, rule.roles))) {
|
|
2405
|
+
violations.push(raise(rule, [el.id], view.id));
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
return violations;
|
|
2409
|
+
}
|
|
2410
|
+
/**
|
|
2411
|
+
* "How many relations reach this node?" — the cardinality half of a grammar.
|
|
2412
|
+
*
|
|
2413
|
+
* One pass to count, one pass to judge. For every element carrying the declared
|
|
2414
|
+
* edge role, the persisted `source → target` pair adds one OUT to its source and
|
|
2415
|
+
* one IN to its target; then every subject node is confronted with the bounds
|
|
2416
|
+
* its rule declares.
|
|
2417
|
+
*
|
|
2418
|
+
* ## What it stays silent about, and why each one matters
|
|
2419
|
+
*
|
|
2420
|
+
* - **an edge with a free end** — it relates nothing, so it is not evidence of a
|
|
2421
|
+
* node being connected. A link the user has grabbed and not yet dropped must
|
|
2422
|
+
* not make a start event legal for the length of the gesture;
|
|
2423
|
+
* - **an end that is not a subject of this rule** — nothing to say about it;
|
|
2424
|
+
* - **a rule declaring no bound at all** — it asks nothing. It warns once, since
|
|
2425
|
+
* data that can never fire and never says why is the worst thing declarative
|
|
2426
|
+
* data can do.
|
|
2427
|
+
*
|
|
2428
|
+
* ## An end whose element is GONE, and why this family keeps the other half
|
|
2429
|
+
*
|
|
2430
|
+
* A dangling id counts against nobody, but the edge is not dropped: the node at
|
|
2431
|
+
* the surviving end really does have one relation leaving it, and no gesture the
|
|
2432
|
+
* user makes on THAT node changes the fact. This family counts the ends, never
|
|
2433
|
+
* the survivors.
|
|
2434
|
+
*
|
|
2435
|
+
* A deliberate divergence from the three families whose subject is the RELATION
|
|
2436
|
+
* — `relative-order-along-axis`, `relation-endpoints`, `edge-locality` — which
|
|
2437
|
+
* drop such an edge whole, because a sentence with a missing half is not a
|
|
2438
|
+
* sentence anybody can be told about. A COUNT has no such problem, and neither
|
|
2439
|
+
* does the traversal in {@link evaluateReachability}, which follows the arc to
|
|
2440
|
+
* an element that is not there and simply never reaches anything by it. Two
|
|
2441
|
+
* different questions, two different readings of the same broken pair.
|
|
2442
|
+
*
|
|
2443
|
+
* A SELF-LOOP counts on both sides: the node genuinely has one edge leaving and
|
|
2444
|
+
* one arriving, and that is what the document says. Whether a loop is legitimate
|
|
2445
|
+
* at all is `relation-endpoints`' question, asked with its own flag, and a
|
|
2446
|
+
* count that quietly ignored loops would let one launder a node past a `maxOut`.
|
|
2447
|
+
*
|
|
2448
|
+
* ## One finding per node
|
|
2449
|
+
*
|
|
2450
|
+
* The bounds are tested in a fixed order — in, then out; fewest, then most; and
|
|
2451
|
+
* {@link EdgeDegreeDef.eitherMin} LAST — and the first one that fails is the
|
|
2452
|
+
* finding. Five badges on one symbol for one situation is pedantry the canvas
|
|
2453
|
+
* cannot afford; the same argument `relation-endpoints` makes about not
|
|
2454
|
+
* indicting an edge twice.
|
|
2455
|
+
*
|
|
2456
|
+
* The disjunction goes last on purpose. A per-direction bound names the side the
|
|
2457
|
+
* user has to act on, and "nothing arrives here" is a more useful sentence than
|
|
2458
|
+
* "neither side has enough"; the disjunction only speaks when the four
|
|
2459
|
+
* conjunctive bounds have nothing to say. It is orthogonal to them, not a
|
|
2460
|
+
* substitute: a rule may cap one side and still ask for two on either.
|
|
2461
|
+
*
|
|
2462
|
+
* ## Cost
|
|
2463
|
+
*
|
|
2464
|
+
* Linear in the elements plus linear in the EDGES, with one hash lookup per end.
|
|
2465
|
+
* No pair-wise sweep and no closure: the rule is about the relations somebody
|
|
2466
|
+
* drew, never about the ones they could have drawn.
|
|
2467
|
+
*/
|
|
2468
|
+
function evaluateEdgeDegree(rule, elements) {
|
|
2469
|
+
const subjectRole = rule.appliesTo;
|
|
2470
|
+
const degree = rule.degree;
|
|
2471
|
+
if (subjectRole === undefined || degree === undefined)
|
|
2472
|
+
return [];
|
|
2473
|
+
const { minIn, maxIn, minOut, maxOut, eitherMin } = degree;
|
|
2474
|
+
// A pattern satisfied by every node — one supplying no bound at all — would
|
|
2475
|
+
// indict the whole board, which is the loudest possible way for a typo in the
|
|
2476
|
+
// data to be wrong. Dropped rather than honoured, and said once.
|
|
2477
|
+
const pattern = degree.forbidPattern;
|
|
2478
|
+
const patternBounds = pattern === undefined
|
|
2479
|
+
? null
|
|
2480
|
+
: [
|
|
2481
|
+
['in', '>=', pattern.minIn],
|
|
2482
|
+
['in', '<=', pattern.maxIn],
|
|
2483
|
+
['out', '>=', pattern.minOut],
|
|
2484
|
+
['out', '<=', pattern.maxOut],
|
|
2485
|
+
].filter(([, , value]) => value !== undefined);
|
|
2486
|
+
if (patternBounds !== null && patternBounds.length === 0) {
|
|
2487
|
+
warnOnce(`edge-degree rule "${rule.id}" declares a forbidden pattern with no ` +
|
|
2488
|
+
`bound in it, which every node matches — the pattern is dropped.`);
|
|
2489
|
+
}
|
|
2490
|
+
const forbidden = patternBounds !== null && patternBounds.length > 0 ? patternBounds : null;
|
|
2491
|
+
if (minIn === undefined &&
|
|
2492
|
+
maxIn === undefined &&
|
|
2493
|
+
minOut === undefined &&
|
|
2494
|
+
maxOut === undefined &&
|
|
2495
|
+
eitherMin === undefined &&
|
|
2496
|
+
forbidden === null) {
|
|
2497
|
+
warnOnce(`edge-degree rule "${rule.id}" declares no bound at all — the rule is ` +
|
|
2498
|
+
`not evaluated.`);
|
|
2499
|
+
return [];
|
|
2500
|
+
}
|
|
2501
|
+
// In the surface's own walk order, so a board reports its offending nodes the
|
|
2502
|
+
// same way every other element-local family does.
|
|
2503
|
+
const subjects = [];
|
|
2504
|
+
const bySubjectId = new Map();
|
|
2505
|
+
const edges = [];
|
|
2506
|
+
for (const el of elements) {
|
|
2507
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation.
|
|
2508
|
+
if (el.role === undefined)
|
|
2509
|
+
continue;
|
|
2510
|
+
if (roleIsA(el.role, subjectRole, rule.roles)) {
|
|
2511
|
+
const subject = { el, in: 0, out: 0 };
|
|
2512
|
+
subjects.push(subject);
|
|
2513
|
+
bySubjectId.set(el.id, subject);
|
|
2514
|
+
}
|
|
2515
|
+
if (roleIsA(el.role, degree.edgeRole, rule.roles))
|
|
2516
|
+
edges.push(el);
|
|
2517
|
+
}
|
|
2518
|
+
if (subjects.length === 0)
|
|
2519
|
+
return [];
|
|
2520
|
+
for (const edge of edges) {
|
|
2521
|
+
const ends = rawEndpointIds(edge);
|
|
2522
|
+
if (ends === null)
|
|
2523
|
+
continue;
|
|
2524
|
+
const source = bySubjectId.get(ends[0]);
|
|
2525
|
+
if (source !== undefined)
|
|
2526
|
+
source.out += 1;
|
|
2527
|
+
const target = bySubjectId.get(ends[1]);
|
|
2528
|
+
if (target !== undefined)
|
|
2529
|
+
target.in += 1;
|
|
2530
|
+
}
|
|
2531
|
+
// Only resolved when something is actually wrong: a conformant board is the
|
|
2532
|
+
// common case and must not pay for the frames it never names.
|
|
2533
|
+
let backgrounds = null;
|
|
2534
|
+
const violations = [];
|
|
2535
|
+
for (const subject of subjects) {
|
|
2536
|
+
const words = minIn !== undefined && subject.in < minIn
|
|
2537
|
+
? (degree.tooFewIn ?? rule)
|
|
2538
|
+
: maxIn !== undefined && subject.in > maxIn
|
|
2539
|
+
? (degree.tooManyIn ?? rule)
|
|
2540
|
+
: minOut !== undefined && subject.out < minOut
|
|
2541
|
+
? (degree.tooFewOut ?? rule)
|
|
2542
|
+
: maxOut !== undefined && subject.out > maxOut
|
|
2543
|
+
? (degree.tooManyOut ?? rule)
|
|
2544
|
+
: // LAST, and deliberately: a per-direction bound names the side
|
|
2545
|
+
// the user has to act on, which is a more useful sentence than
|
|
2546
|
+
// "neither side". The disjunction only speaks when the four
|
|
2547
|
+
// above have nothing to say.
|
|
2548
|
+
eitherMin !== undefined &&
|
|
2549
|
+
subject.in < eitherMin &&
|
|
2550
|
+
subject.out < eitherMin
|
|
2551
|
+
? (degree.neither ?? rule)
|
|
2552
|
+
: // LAST of all, for the same reason: a bound that failed names
|
|
2553
|
+
// what is missing, and a forbidden zone names a shape. When
|
|
2554
|
+
// both have something to say, the missing thing is the more
|
|
2555
|
+
// actionable half.
|
|
2556
|
+
forbidden !== null &&
|
|
2557
|
+
forbidden.every(([side, test, value]) => test === '>='
|
|
2558
|
+
? subject[side] >= value
|
|
2559
|
+
: subject[side] <= value)
|
|
2560
|
+
? (pattern ?? rule)
|
|
2561
|
+
: null;
|
|
2562
|
+
if (words === null)
|
|
2563
|
+
continue;
|
|
2564
|
+
backgrounds ??= backgroundsOf(rule, elements);
|
|
2565
|
+
const frameId = attributeBackground(subject.el.elementBound, backgrounds)?.id;
|
|
2566
|
+
violations.push(raise(rule, [subject.el.id], frameId, words));
|
|
2567
|
+
}
|
|
2568
|
+
return violations;
|
|
2569
|
+
}
|
|
2570
|
+
/**
|
|
2571
|
+
* "How many of these does one frame hold?" — existence and uniqueness.
|
|
2572
|
+
*
|
|
2573
|
+
* The tally is per INSTANCE and anchored ON it, exactly like
|
|
2574
|
+
* {@link evaluateMajorityFact}: a board carrying three pools holds three
|
|
2575
|
+
* independent answers, the bracket lands on the pool the answer is about, and
|
|
2576
|
+
* the `map` exemption scope therefore covers that pool and no other.
|
|
2577
|
+
*
|
|
2578
|
+
* ## Containment, and only containment
|
|
2579
|
+
*
|
|
2580
|
+
* A subject belongs to the frame whose plot contains its CENTRE
|
|
2581
|
+
* ({@link plotContaining}) — deliberately not `attributeBackground`'s
|
|
2582
|
+
* nearest-fallback, which would let a start event floating beside a pool satisfy
|
|
2583
|
+
* "this pool has a start event". A subject inside no frame at all is counted by
|
|
2584
|
+
* nobody, and whether that is itself a mistake is `element-in-background`'s
|
|
2585
|
+
* question.
|
|
2586
|
+
*
|
|
2587
|
+
* ## Silence
|
|
2588
|
+
*
|
|
2589
|
+
* - **no frame on the board** — nothing to count into. An artefact on blank
|
|
2590
|
+
* canvas is a sketch, and a process sketched before its pool was drawn must
|
|
2591
|
+
* not light up;
|
|
2592
|
+
* - **a rule declaring neither bound** — it asks nothing, and warns once;
|
|
2593
|
+
* - **a frame the GUARD has not armed** — see {@link RoleCountDef.ifPresent}.
|
|
2594
|
+
* Not judged, rather than judged and found compliant: a conditional
|
|
2595
|
+
* requirement says nothing at all about the case its condition excludes.
|
|
2596
|
+
*
|
|
2597
|
+
* An EMPTY frame is not silence: a pool holding no start event is precisely what
|
|
2598
|
+
* `min` is for, so every instance gets an entry before the walk, not when a
|
|
2599
|
+
* subject happens to land on it. The guard is what turns that back into silence
|
|
2600
|
+
* for a notation where the artefact is optional on its own.
|
|
2601
|
+
*
|
|
2602
|
+
* ## Cost
|
|
2603
|
+
*
|
|
2604
|
+
* One pass over the elements, one plot per frame, one containment test per
|
|
2605
|
+
* subject per frame — the frames are counted in units, so this is constant per
|
|
2606
|
+
* element in practice, like every other attribution in this file.
|
|
2607
|
+
*/
|
|
2608
|
+
function evaluateRoleCount(rule, elements) {
|
|
2609
|
+
const count = rule.roleCount;
|
|
2610
|
+
if (count === undefined)
|
|
2611
|
+
return [];
|
|
2612
|
+
if (missingFrameRole(rule, 'role-count'))
|
|
2613
|
+
return [];
|
|
2614
|
+
const { min, max } = count;
|
|
2615
|
+
if (min === undefined && max === undefined) {
|
|
2616
|
+
warnOnce(`role-count rule "${rule.id}" declares neither a minimum nor a maximum ` +
|
|
2617
|
+
`— the rule is not evaluated.`);
|
|
2618
|
+
return [];
|
|
2619
|
+
}
|
|
2620
|
+
const frames = framePlots(rule, elements);
|
|
2621
|
+
// No frame on the board: nothing for the count to be about.
|
|
2622
|
+
if (frames.length === 0)
|
|
2623
|
+
return [];
|
|
2624
|
+
const tally = new Map();
|
|
2625
|
+
for (const frame of frames)
|
|
2626
|
+
tally.set(frame.instance.id, 0);
|
|
2627
|
+
// The frames the GUARD has armed. Left null by a rule that declares none, so
|
|
2628
|
+
// an unguarded rule allocates nothing and reads nothing extra per element.
|
|
2629
|
+
const guard = count.ifPresent;
|
|
2630
|
+
const armed = guard === undefined ? null : new Set();
|
|
2631
|
+
for (const el of elements) {
|
|
2632
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation.
|
|
2633
|
+
if (el.role === undefined)
|
|
2634
|
+
continue;
|
|
2635
|
+
const counts = count.exact === true
|
|
2636
|
+
? el.role === count.subject
|
|
2637
|
+
: roleIsA(el.role, count.subject, rule.roles);
|
|
2638
|
+
// Not `else if`: nothing forbids a vocabulary where the guard role and the
|
|
2639
|
+
// counted role overlap, and an element that is both must arm the frame AND
|
|
2640
|
+
// be counted in it. Two independent questions about one element.
|
|
2641
|
+
const arms = guard !== undefined && roleIsA(el.role, guard, rule.roles);
|
|
2642
|
+
if (!counts && !arms)
|
|
2643
|
+
continue;
|
|
2644
|
+
// Resolved once for both questions: the containment walk is the expensive
|
|
2645
|
+
// half, and an element that is subject and guard at once must not pay twice.
|
|
2646
|
+
const frame = plotContaining(centreOf(el.elementBound), frames);
|
|
2647
|
+
// Off every frame: it neither counts towards a requirement nor arms one.
|
|
2648
|
+
// An artefact floating beside a pool is a draft, on both readings.
|
|
2649
|
+
if (frame === null)
|
|
2650
|
+
continue;
|
|
2651
|
+
if (counts)
|
|
2652
|
+
tally.set(frame.id, (tally.get(frame.id) ?? 0) + 1);
|
|
2653
|
+
if (arms)
|
|
2654
|
+
armed.add(frame.id);
|
|
2655
|
+
}
|
|
2656
|
+
const violations = [];
|
|
2657
|
+
// Sorted, so a board with several offending frames always reports them the
|
|
2658
|
+
// same way whichever order the surface happened to be walked in.
|
|
2659
|
+
for (const id of [...tally.keys()].sort()) {
|
|
2660
|
+
// A frame the guard has not armed is not judged at all — not walked, not
|
|
2661
|
+
// found compliant. That is the difference between "this notation requires
|
|
2662
|
+
// one" and "this notation requires one WHEN the other is there".
|
|
2663
|
+
if (armed !== null && !armed.has(id))
|
|
2664
|
+
continue;
|
|
2665
|
+
const held = tally.get(id);
|
|
2666
|
+
const words = min !== undefined && held < min
|
|
2667
|
+
? (count.tooFew ?? rule)
|
|
2668
|
+
: max !== undefined && held > max
|
|
2669
|
+
? (count.tooMany ?? rule)
|
|
2670
|
+
: null;
|
|
2671
|
+
if (words === null)
|
|
2672
|
+
continue;
|
|
2673
|
+
violations.push(raise(rule, [id], id, words));
|
|
2674
|
+
}
|
|
2675
|
+
return violations;
|
|
2676
|
+
}
|
|
2677
|
+
/**
|
|
2678
|
+
* "Does this relation stay inside one frame, or cross between two?"
|
|
2679
|
+
*
|
|
2680
|
+
* The third family whose subject is a RELATION, and the only one that reads the
|
|
2681
|
+
* frames rather than the roles: a sequence flow belongs to one pool, a message
|
|
2682
|
+
* flow exists only between two. The two ends carry the same roles in the legal
|
|
2683
|
+
* case and the illegal one, so nothing but the attribution can tell them apart.
|
|
2684
|
+
*
|
|
2685
|
+
* Each END is attributed on its own, by the centre of ITS element and never by
|
|
2686
|
+
* the edge's box — the box of a link between two pools spans both of them and
|
|
2687
|
+
* would answer whichever question was asked of it.
|
|
2688
|
+
*
|
|
2689
|
+
* ## What it stays silent about, and why each one matters
|
|
2690
|
+
*
|
|
2691
|
+
* - **no frame on the board** — a process sketched before anybody drew a pool is
|
|
2692
|
+
* a sketch, and this is the family a poolless board would otherwise light up
|
|
2693
|
+
* from end to end;
|
|
2694
|
+
* - **an edge with a free end, or an end whose element is gone** — it relates
|
|
2695
|
+
* nothing;
|
|
2696
|
+
* - **a self-loop** — one element compared with itself is always in its own
|
|
2697
|
+
* frame, so it says nothing about locality either way; whether a loop is legal
|
|
2698
|
+
* at all is `relation-endpoints`' question ({@link boundEnds});
|
|
2699
|
+
* - **an end inside NO frame** — the decisive one, and it is the same
|
|
2700
|
+
* proportionality {@link RelationEndpointsDef.allowed}'s alphabet applies: a
|
|
2701
|
+
* task dropped beside the pool is a draft, and a tool that answered "that flow
|
|
2702
|
+
* leaves the pool" would be indicting the act of sketching.
|
|
2703
|
+
*
|
|
2704
|
+
* ## The finding names the edge AND its two ends
|
|
2705
|
+
*
|
|
2706
|
+
* The `relative-order-along-axis` and `relation-endpoints` precedent, for the
|
|
2707
|
+
* same reason: the finding has two honest resolutions — move the artefact into
|
|
2708
|
+
* the frame it belongs to, or re-point the link — and the second is only
|
|
2709
|
+
* reachable from the edge. It is attributed to the SOURCE's frame, like every
|
|
2710
|
+
* other relation finding in this file.
|
|
2711
|
+
*
|
|
2712
|
+
* ## Cost
|
|
2713
|
+
*
|
|
2714
|
+
* Linear in the elements plus linear in the edges, with the plots resolved once
|
|
2715
|
+
* per frame.
|
|
2716
|
+
*/
|
|
2717
|
+
function evaluateEdgeLocality(rule, elements) {
|
|
2718
|
+
const locality = rule.locality;
|
|
2719
|
+
if (locality === undefined)
|
|
2720
|
+
return [];
|
|
2721
|
+
if (missingFrameRole(rule, 'edge-locality'))
|
|
2722
|
+
return [];
|
|
2723
|
+
const frames = framePlots(rule, elements);
|
|
2724
|
+
// No frame on the board, no locality to be in or out of.
|
|
2725
|
+
if (frames.length === 0)
|
|
2726
|
+
return [];
|
|
2727
|
+
const byId = new Map();
|
|
2728
|
+
const edges = [];
|
|
2729
|
+
for (const el of elements) {
|
|
2730
|
+
byId.set(el.id, el);
|
|
2731
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation.
|
|
2732
|
+
if (el.role === undefined)
|
|
2733
|
+
continue;
|
|
2734
|
+
if (roleIsA(el.role, locality.edgeRole, rule.roles))
|
|
2735
|
+
edges.push(el);
|
|
2736
|
+
}
|
|
2737
|
+
if (edges.length === 0)
|
|
2738
|
+
return [];
|
|
2739
|
+
const same = locality.mode === 'same-background';
|
|
2740
|
+
const violations = [];
|
|
2741
|
+
for (const edge of edges) {
|
|
2742
|
+
const ends = boundEnds(edge);
|
|
2743
|
+
if (ends === null)
|
|
2744
|
+
continue;
|
|
2745
|
+
const [sourceId, targetId] = ends;
|
|
2746
|
+
const source = byId.get(sourceId);
|
|
2747
|
+
const target = byId.get(targetId);
|
|
2748
|
+
if (source === undefined || target === undefined)
|
|
2749
|
+
continue;
|
|
2750
|
+
const sourceFrame = plotContaining(centreOf(source.elementBound), frames);
|
|
2751
|
+
if (sourceFrame === null)
|
|
2752
|
+
continue;
|
|
2753
|
+
const targetFrame = plotContaining(centreOf(target.elementBound), frames);
|
|
2754
|
+
if (targetFrame === null)
|
|
2755
|
+
continue;
|
|
2756
|
+
// Conformant when the two ends agree with the mode: one instance for
|
|
2757
|
+
// `same-background`, two for `cross-background`.
|
|
2758
|
+
const together = sourceFrame.id === targetFrame.id;
|
|
2759
|
+
if (together === same)
|
|
2760
|
+
continue;
|
|
2761
|
+
violations.push(raise(rule, namedElements([edge.id, sourceId, targetId]), sourceFrame.id));
|
|
2762
|
+
}
|
|
2763
|
+
return violations;
|
|
2764
|
+
}
|
|
2765
|
+
/**
|
|
2766
|
+
* "Can you get here from the start?" — the orphan question.
|
|
2767
|
+
*
|
|
2768
|
+
* The first family that builds a GRAPH. Every element carrying the declared edge
|
|
2769
|
+
* role contributes one directed arc, `source → target`, read off the persisted
|
|
2770
|
+
* pair and never off the geometry; a breadth-first walk leaves from every root
|
|
2771
|
+
* at once; and every subject the walk never reached is a finding.
|
|
2772
|
+
*
|
|
2773
|
+
* ## Silence when there is no root
|
|
2774
|
+
*
|
|
2775
|
+
* TOTAL, and it is the load-bearing decision of this family. A process with no
|
|
2776
|
+
* start event has every one of its tasks unreachable, so the rule would answer
|
|
2777
|
+
* an empty board with one finding per artefact on it — a wall of brackets whose
|
|
2778
|
+
* single real cause is a missing start event, which is `role-count`'s question
|
|
2779
|
+
* and is already asked once, on the frame. One mistake, one sentence.
|
|
2780
|
+
*
|
|
2781
|
+
* A root is seeded into the visited set, which is also what makes "a root is
|
|
2782
|
+
* never its own orphan" fall out rather than needing a second test.
|
|
2783
|
+
*
|
|
2784
|
+
* ## Two readings of "root"
|
|
2785
|
+
*
|
|
2786
|
+
* {@link ReachabilityDef.implicitRoots} adds the subjects nothing points at to
|
|
2787
|
+
* the set the walk leaves from — the second, silent way a notation with an
|
|
2788
|
+
* optional start marker says "the work begins here". Under it the gate above
|
|
2789
|
+
* means "no root of EITHER kind", and what is left is every subject that has an
|
|
2790
|
+
* incoming counted edge and that no root reaches: a ring entered from nowhere,
|
|
2791
|
+
* and a subject fed only by an edge whose own source the walk never leaves from
|
|
2792
|
+
* (a dangling id, or an element outside both roles). See that field for why the
|
|
2793
|
+
* second one is reported here and fixed elsewhere.
|
|
2794
|
+
*
|
|
2795
|
+
* The two readings share every line but the seeding, deliberately: the answer to
|
|
2796
|
+
* "can you get here" must not depend on which one a framework picked — including
|
|
2797
|
+
* for that second shape, which behaves identically either way.
|
|
2798
|
+
*
|
|
2799
|
+
* ## Direction, and the arcs it does not add
|
|
2800
|
+
*
|
|
2801
|
+
* The walk follows arcs FORWARD only. "Reachable from the start" is a statement
|
|
2802
|
+
* about the direction the process runs in; a task that only points BACK at the
|
|
2803
|
+
* chain is exactly the mistake this exists to find, and an undirected walk would
|
|
2804
|
+
* certify it. An edge with a free end, or an end whose element is gone, adds no
|
|
2805
|
+
* arc — it connects nothing.
|
|
2806
|
+
*
|
|
2807
|
+
* ## Cost, and the incremental story
|
|
2808
|
+
*
|
|
2809
|
+
* O(V + E): one indexing pass, one adjacency build, one traversal that visits
|
|
2810
|
+
* each node and each arc at most once.
|
|
2811
|
+
*
|
|
2812
|
+
* An {@link IncrementalContext} is deliberately NOT honoured, and could not
|
|
2813
|
+
* usefully be: reachability is a GLOBAL property, so re-pointing a single edge
|
|
2814
|
+
* can orphan or rescue an arbitrary number of nodes nowhere near it, and any
|
|
2815
|
+
* dirty edge invalidates the whole answer. A framework that finds the traversal
|
|
2816
|
+
* too expensive for the drawing path declares its rule `'on-demand'` — the
|
|
2817
|
+
* moment is the rule's to choose ({@link ValidationMoment}), and this family
|
|
2818
|
+
* stays agnostic about which one it is evaluated at.
|
|
2819
|
+
*/
|
|
2820
|
+
function evaluateReachability(rule, elements) {
|
|
2821
|
+
const def = rule.reachability;
|
|
2822
|
+
if (def === undefined)
|
|
2823
|
+
return [];
|
|
2824
|
+
const subjects = [];
|
|
2825
|
+
const edges = [];
|
|
2826
|
+
// Seeded with the roots, which is what makes a root that is also a subject
|
|
2827
|
+
// reachable by definition rather than by a second test.
|
|
2828
|
+
const visited = new Set();
|
|
2829
|
+
const queue = [];
|
|
2830
|
+
for (const el of elements) {
|
|
2831
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation.
|
|
2832
|
+
if (el.role === undefined)
|
|
2833
|
+
continue;
|
|
2834
|
+
if (roleIsA(el.role, def.rootRole, rule.roles)) {
|
|
2835
|
+
if (!visited.has(el.id)) {
|
|
2836
|
+
visited.add(el.id);
|
|
2837
|
+
queue.push(el.id);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
if (roleIsA(el.role, def.subjectRole, rule.roles))
|
|
2841
|
+
subjects.push(el);
|
|
2842
|
+
if (roleIsA(el.role, def.edgeRole, rule.roles))
|
|
2843
|
+
edges.push(el);
|
|
2844
|
+
}
|
|
2845
|
+
if (subjects.length === 0)
|
|
2846
|
+
return [];
|
|
2847
|
+
const implicit = def.implicitRoots === true;
|
|
2848
|
+
// Under the declared-roots-only reading the gate is answerable before a
|
|
2849
|
+
// single arc is built, which is the cheapest exit this family has. The
|
|
2850
|
+
// implicit reading cannot take it: whether there is a root at all is a fact
|
|
2851
|
+
// about the ARCS, so it has to wait for them.
|
|
2852
|
+
if (!implicit && queue.length === 0)
|
|
2853
|
+
return [];
|
|
2854
|
+
const next = new Map();
|
|
2855
|
+
// Every element some arc points AT — tracked only for the implicit reading,
|
|
2856
|
+
// which is the only one that needs an in-degree. A board on the declared
|
|
2857
|
+
// reading allocates nothing.
|
|
2858
|
+
const pointedAt = implicit ? new Set() : null;
|
|
2859
|
+
for (const edge of edges) {
|
|
2860
|
+
const ends = rawEndpointIds(edge);
|
|
2861
|
+
if (ends === null)
|
|
2862
|
+
continue;
|
|
2863
|
+
const outgoing = next.get(ends[0]);
|
|
2864
|
+
if (outgoing === undefined)
|
|
2865
|
+
next.set(ends[0], [ends[1]]);
|
|
2866
|
+
else
|
|
2867
|
+
outgoing.push(ends[1]);
|
|
2868
|
+
pointedAt?.add(ends[1]);
|
|
2869
|
+
}
|
|
2870
|
+
if (pointedAt !== null) {
|
|
2871
|
+
// A subject nothing points at IS a beginning — see
|
|
2872
|
+
// {@link ReachabilityDef.implicitRoots}. In the surface's walk order, and
|
|
2873
|
+
// through the same `visited` guard the declared roots use, so a subject
|
|
2874
|
+
// that is also a declared root is seeded exactly once.
|
|
2875
|
+
//
|
|
2876
|
+
// A SELF-LOOP disqualifies its own node, and rightly: something does point
|
|
2877
|
+
// at it. A ring of one is the smallest instance of the only defect this
|
|
2878
|
+
// reading still reports.
|
|
2879
|
+
for (const el of subjects) {
|
|
2880
|
+
if (pointedAt.has(el.id) || visited.has(el.id))
|
|
2881
|
+
continue;
|
|
2882
|
+
visited.add(el.id);
|
|
2883
|
+
queue.push(el.id);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
// THE GATE: no root of EITHER kind on the board, so nothing is unreachable —
|
|
2887
|
+
// see above. Under the implicit reading a board reaches this only when every
|
|
2888
|
+
// one of its subjects is pointed at, i.e. when they all sit in rings entered
|
|
2889
|
+
// from nowhere, and the question genuinely does not arise.
|
|
2890
|
+
if (queue.length === 0)
|
|
2891
|
+
return [];
|
|
2892
|
+
// Breadth-first from every root at once. `head` walks the queue instead of
|
|
2893
|
+
// `shift()`ing it: a shift is O(n) on a real array, which would turn an
|
|
2894
|
+
// O(V + E) traversal quadratic on the one board big enough to notice.
|
|
2895
|
+
for (let head = 0; head < queue.length; head++) {
|
|
2896
|
+
const outgoing = next.get(queue[head]);
|
|
2897
|
+
if (outgoing === undefined)
|
|
2898
|
+
continue;
|
|
2899
|
+
for (const id of outgoing) {
|
|
2900
|
+
if (visited.has(id))
|
|
2901
|
+
continue;
|
|
2902
|
+
visited.add(id);
|
|
2903
|
+
queue.push(id);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
// Only resolved when something is actually wrong — the conformant board pays
|
|
2907
|
+
// nothing for the frames it never names.
|
|
2908
|
+
let backgrounds = null;
|
|
2909
|
+
const violations = [];
|
|
2910
|
+
for (const el of subjects) {
|
|
2911
|
+
if (visited.has(el.id))
|
|
2912
|
+
continue;
|
|
2913
|
+
backgrounds ??= backgroundsOf(rule, elements);
|
|
2914
|
+
const frameId = attributeBackground(el.elementBound, backgrounds)?.id;
|
|
2915
|
+
violations.push(raise(rule, [el.id], frameId));
|
|
2916
|
+
}
|
|
2917
|
+
return violations;
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* Zero-width characters: present in the string, absent from the page.
|
|
2921
|
+
*
|
|
2922
|
+
* A name made of one of these is a name nobody can read, and a user who pasted
|
|
2923
|
+
* one has an artefact that LOOKS unnamed and validates as named — the worst of
|
|
2924
|
+
* both, and impossible to debug from the canvas. `trim()` does not remove them
|
|
2925
|
+
* (they are not `WhiteSpace` in the spec), so they are stripped first.
|
|
2926
|
+
*
|
|
2927
|
+
* ESCAPED, never written as the characters themselves. A class holding the raw
|
|
2928
|
+
* code points is invisible in every diff, review UI and editor — which makes it
|
|
2929
|
+
* the one constant in this file that can be silently changed by a linter, a
|
|
2930
|
+
* paste or a codemod that strips zero-width characters, with nothing on screen
|
|
2931
|
+
* to show it happened. The escapes are: zero-width space, zero-width
|
|
2932
|
+
* non-joiner, zero-width joiner, word joiner, and the byte-order mark.
|
|
2933
|
+
*/
|
|
2934
|
+
const INVISIBLE_CHARS = /[\u200B-\u200D\u2060\uFEFF]/g;
|
|
2935
|
+
/**
|
|
2936
|
+
* Whether a non-string value knows how to render ITSELF as text, as opposed to
|
|
2937
|
+
* inheriting `Object.prototype.toString` and answering `'[object Object]'`.
|
|
2938
|
+
*
|
|
2939
|
+
* The duck-typed test for "this is a text-bearing model": a `Y.Text` overrides
|
|
2940
|
+
* `toString`, and so does every wrapper the canvas could plausibly store in that
|
|
2941
|
+
* slot. A plain object does not, and neither does a number.
|
|
2942
|
+
*
|
|
2943
|
+
* An ARRAY is excluded even though it overrides `toString`, and that is the case
|
|
2944
|
+
* worth spelling out: a raw delta list is the shape a text most plausibly
|
|
2945
|
+
* degrades into, and `String([{ insert: 'x' }])` is `'[object Object]'` all over
|
|
2946
|
+
* again. A list of runs is not a name; a model that can render one is.
|
|
2947
|
+
*/
|
|
2948
|
+
function rendersAsText(value) {
|
|
2949
|
+
if (typeof value !== 'object' || Array.isArray(value))
|
|
2950
|
+
return false;
|
|
2951
|
+
const own = value.toString;
|
|
2952
|
+
return typeof own === 'function' && own !== Object.prototype.toString;
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* The words an element carries, or `''` — the EXTRACTION alone, with no opinion
|
|
2956
|
+
* on whether having none is a mistake.
|
|
2957
|
+
*
|
|
2958
|
+
* Duck-typed on `text`, exactly like {@link elementPath} and
|
|
2959
|
+
* {@link rawEndpointIds} are on their own properties, and for the same reason:
|
|
2960
|
+
* the engine knows roles and geometry and must not import an element model. That
|
|
2961
|
+
* one property is what a shape's `Y.Text`, a connector's label and a free text
|
|
2962
|
+
* element all expose, and `String()` is what turns any of them into the content
|
|
2963
|
+
* — the same conversion {@link textInkBound} has always relied on.
|
|
2964
|
+
*
|
|
2965
|
+
* NFC first, for the reason that function gives: the same name typed here and
|
|
2966
|
+
* pasted from elsewhere can arrive as the same glyphs in a different number of
|
|
2967
|
+
* code points. Then the invisibles, then the whitespace. What is left is what a
|
|
2968
|
+
* reader would see.
|
|
2969
|
+
*
|
|
2970
|
+
* ## What is not a name
|
|
2971
|
+
*
|
|
2972
|
+
* A value that is neither a string nor something that knows how to render itself
|
|
2973
|
+
* as one. `String({})` is `'[object Object]'` — eleven visible characters, which
|
|
2974
|
+
* would make an artefact carrying a malformed `text` read as NAMED, and named
|
|
2975
|
+
* with a word the user can neither see nor delete.
|
|
2976
|
+
*
|
|
2977
|
+
* `text` comes off a `Y.Map`, so it is whatever a peer or an importer wrote —
|
|
2978
|
+
* the same threat model {@link elementExceptions} guards with its `Array.isArray`
|
|
2979
|
+
* and {@link declaresRole} with its `Object.hasOwn`. The test is for an own
|
|
2980
|
+
* `toString`, which a `Y.Text` has and a plain object does not: it accepts every
|
|
2981
|
+
* shape the canvas actually stores without this file importing one of them.
|
|
2982
|
+
*/
|
|
2983
|
+
function elementLabel(el) {
|
|
2984
|
+
const text = el.text;
|
|
2985
|
+
if (text == null)
|
|
2986
|
+
return '';
|
|
2987
|
+
if (typeof text !== 'string' && !rendersAsText(text))
|
|
2988
|
+
return '';
|
|
2989
|
+
return String(text).normalize('NFC').replace(INVISIBLE_CHARS, '').trim();
|
|
2990
|
+
}
|
|
2991
|
+
/**
|
|
2992
|
+
* "Does this artefact say what it is?"
|
|
2993
|
+
*
|
|
2994
|
+
* The first family that reads an element's WORDS. Every other one asks about
|
|
2995
|
+
* geometry, roles or relations, all of which a reader can partly infer from the
|
|
2996
|
+
* drawing; a box with nothing written in it is the one defect nothing recovers.
|
|
2997
|
+
*
|
|
2998
|
+
* ## What counts as absent
|
|
2999
|
+
*
|
|
3000
|
+
* No `text` at all, an empty one, one that is only whitespace, and one that is
|
|
3001
|
+
* only invisible code points — see {@link elementLabel}. Anything a reader would
|
|
3002
|
+
* see is a label, however short: judging the QUALITY of a name is not this
|
|
3003
|
+
* family's business and probably nobody's.
|
|
3004
|
+
*
|
|
3005
|
+
* ## Silence
|
|
3006
|
+
*
|
|
3007
|
+
* The usual proportionality: an element with no role is not a subject, and a
|
|
3008
|
+
* board carrying no subject raises nothing. There is no frame requirement —
|
|
3009
|
+
* {@link ValidationRule.backgroundRole} buys attribution only, the `no-overlap`
|
|
3010
|
+
* pattern, so a rule may name a frame to give its findings somewhere to be
|
|
3011
|
+
* waived and need not.
|
|
3012
|
+
*
|
|
3013
|
+
* ## The moment, and why a framework should think about it
|
|
3014
|
+
*
|
|
3015
|
+
* Naming is the one property a user changes by TYPING, one character at a time.
|
|
3016
|
+
* `text` is verdict-bearing only for a framework that registers a real-time rule
|
|
3017
|
+
* of this family — {@link verdictPropsOf} adds it then and only then — so a rule
|
|
3018
|
+
* declared `'on-demand'` costs the drawing path nothing at all, and a real-time
|
|
3019
|
+
* one wakes the debounced evaluation on every keystroke in every shape on the
|
|
3020
|
+
* board. Both are supported; the second is a choice a framework should make
|
|
3021
|
+
* deliberately.
|
|
3022
|
+
*
|
|
3023
|
+
* ## Cost
|
|
3024
|
+
*
|
|
3025
|
+
* One pass, one `text` read per subject. Linear, and paid only by the elements
|
|
3026
|
+
* carrying the subject role.
|
|
3027
|
+
*/
|
|
3028
|
+
function evaluateLabelPresence(rule, elements) {
|
|
3029
|
+
const subjectRole = rule.appliesTo;
|
|
3030
|
+
const label = rule.label;
|
|
3031
|
+
if (subjectRole === undefined || label === undefined)
|
|
3032
|
+
return [];
|
|
3033
|
+
if (label.present !== true)
|
|
3034
|
+
return [];
|
|
3035
|
+
// Only resolved when something is actually wrong — a named board pays nothing
|
|
3036
|
+
// for the frames it never has to name.
|
|
3037
|
+
let backgrounds = null;
|
|
3038
|
+
const violations = [];
|
|
3039
|
+
for (const el of elements) {
|
|
3040
|
+
// Cheapest possible exit for a neutral element: no role, no evaluation.
|
|
3041
|
+
if (el.role === undefined)
|
|
3042
|
+
continue;
|
|
3043
|
+
if (!roleIsA(el.role, subjectRole, rule.roles))
|
|
3044
|
+
continue;
|
|
3045
|
+
if (elementLabel(el) !== '')
|
|
3046
|
+
continue;
|
|
3047
|
+
backgrounds ??= backgroundsOf(rule, elements);
|
|
3048
|
+
const frameId = attributeBackground(el.elementBound, backgrounds)?.id;
|
|
3049
|
+
violations.push(raise(rule, [el.id], frameId));
|
|
3050
|
+
}
|
|
3051
|
+
return violations;
|
|
3052
|
+
}
|
|
1544
3053
|
const RULE_FAMILIES = {
|
|
1545
3054
|
'element-in-background': evaluateElementInBackground,
|
|
1546
3055
|
'orientation-against-axis': evaluateOrientationAgainstAxis,
|
|
@@ -1549,6 +3058,14 @@ const RULE_FAMILIES = {
|
|
|
1549
3058
|
'tone-convention': evaluateToneConvention,
|
|
1550
3059
|
'majority-fact': evaluateMajorityFact,
|
|
1551
3060
|
'relative-order-along-axis': evaluateRelativeOrder,
|
|
3061
|
+
'relation-endpoints': evaluateRelationEndpoints,
|
|
3062
|
+
'element-in-zone': evaluateElementInZone,
|
|
3063
|
+
'edge-degree': evaluateEdgeDegree,
|
|
3064
|
+
'role-count': evaluateRoleCount,
|
|
3065
|
+
'edge-locality': evaluateEdgeLocality,
|
|
3066
|
+
reachability: evaluateReachability,
|
|
3067
|
+
'label-presence': evaluateLabelPresence,
|
|
3068
|
+
'view-admissibility': evaluateViewAdmissibility,
|
|
1552
3069
|
};
|
|
1553
3070
|
/** A rule the drawing path evaluates. `moment` absent means `'realtime'`. */
|
|
1554
3071
|
function isRealtime(rule) {
|
|
@@ -1818,6 +3335,73 @@ export function ValidationProfileExtension(profiles) {
|
|
|
1818
3335
|
},
|
|
1819
3336
|
};
|
|
1820
3337
|
}
|
|
3338
|
+
/** A framework declares its root instances here, when no rule already does. */
|
|
3339
|
+
export const ValidationFrameworkIdentifier = createIdentifier('ValidationFramework');
|
|
3340
|
+
/**
|
|
3341
|
+
* Register a framework's root-instance declaration.
|
|
3342
|
+
*
|
|
3343
|
+
* ```ts
|
|
3344
|
+
* context.register(
|
|
3345
|
+
* ValidationFrameworkExtension([
|
|
3346
|
+
* { framework: 'estuarine', backgroundRole: 'estuarine:map', roles: ESTUARINE_ROLES },
|
|
3347
|
+
* ])
|
|
3348
|
+
* );
|
|
3349
|
+
* ```
|
|
3350
|
+
*
|
|
3351
|
+
* Keyed by framework AND role, so a framework with two kinds of board declares
|
|
3352
|
+
* both and neither silently replaces the other.
|
|
3353
|
+
*/
|
|
3354
|
+
export function ValidationFrameworkExtension(defs) {
|
|
3355
|
+
return {
|
|
3356
|
+
setup: di => {
|
|
3357
|
+
for (const def of defs) {
|
|
3358
|
+
di.addImpl(ValidationFrameworkIdentifier(`${def.framework}|${def.backgroundRole}`), () => def);
|
|
3359
|
+
}
|
|
3360
|
+
},
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
/** Shared answer for "this element is nobody's frame", allocated once. */
|
|
3364
|
+
const EMPTY_FRAMEWORKS = new Set();
|
|
3365
|
+
/**
|
|
3366
|
+
* The frameworks `role` is a ROOT INSTANCE of — the one test that decides
|
|
3367
|
+
* whether an element is somebody's frame, and the only one.
|
|
3368
|
+
*
|
|
3369
|
+
* TWO sources, and they answer the same question:
|
|
3370
|
+
*
|
|
3371
|
+
* - a registered RULE naming this role as the background it measures against.
|
|
3372
|
+
* A rule that frames against a role is a framework saying "this is my map",
|
|
3373
|
+
* with no second registry to keep in step;
|
|
3374
|
+
* - a registered {@link ValidationFrameworkDef}, for the framework whose whole
|
|
3375
|
+
* contribution is nudges and which therefore has no rule to be derived from.
|
|
3376
|
+
*
|
|
3377
|
+
* Pure, and exported, because every per-instance surface downstream — the
|
|
3378
|
+
* profile picker, the nudge checklist, the check-up — is built on this answer,
|
|
3379
|
+
* and a function taking its inputs is testable without a DI container, a
|
|
3380
|
+
* surface or an editor.
|
|
3381
|
+
*
|
|
3382
|
+
* Empty for a neutral element, and for a background authored before its role
|
|
3383
|
+
* existed: no role, no framework, no tooling.
|
|
3384
|
+
*/
|
|
3385
|
+
export function frameworksOfRole(role, rules, frameworkDefs = []) {
|
|
3386
|
+
if (role === undefined)
|
|
3387
|
+
return EMPTY_FRAMEWORKS;
|
|
3388
|
+
const frameworks = new Set();
|
|
3389
|
+
for (const rule of rules) {
|
|
3390
|
+
if (rule.backgroundRole === undefined)
|
|
3391
|
+
continue;
|
|
3392
|
+
if (roleIsA(role, rule.backgroundRole, rule.roles)) {
|
|
3393
|
+
frameworks.add(rule.framework);
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
for (const def of frameworkDefs) {
|
|
3397
|
+
if (roleIsA(role, def.backgroundRole, def.roles)) {
|
|
3398
|
+
frameworks.add(def.framework);
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
// One shared answer for "this element is nobody's frame": the common case by
|
|
3402
|
+
// far, on every element of every board that carries no framework.
|
|
3403
|
+
return frameworks.size === 0 ? EMPTY_FRAMEWORKS : frameworks;
|
|
3404
|
+
}
|
|
1821
3405
|
/** Recompute delay, so a drag re-evaluates once instead of once per frame. */
|
|
1822
3406
|
const VALIDATION_DELAY_MS = 120;
|
|
1823
3407
|
/**
|
|
@@ -1866,10 +3450,34 @@ export const VERDICT_PROPS = [
|
|
|
1866
3450
|
* knows: `backgroundTransitionVisibleProps` reads the prop each framework
|
|
1867
3451
|
* writes beside the line it gates. A framework whose background declares no
|
|
1868
3452
|
* toggle contributes nothing and the set is the constant.
|
|
3453
|
+
*
|
|
3454
|
+
* ## `text`, and why it is not in the constant
|
|
3455
|
+
*
|
|
3456
|
+
* `label-presence` is the one family whose verdict turns on an element's words,
|
|
3457
|
+
* and words are what a user changes by TYPING. Watched unconditionally, every
|
|
3458
|
+
* keystroke in every shape on the board would wake the debounced evaluation —
|
|
3459
|
+
* a cost paid by every document, for a question most of them never ask.
|
|
3460
|
+
*
|
|
3461
|
+
* So it is added exactly when a REAL-TIME rule of that family is registered.
|
|
3462
|
+
* The moment filter is deliberate and mirrors {@link backgroundElementIds}: an
|
|
3463
|
+
* on-demand rule is never evaluated on the gesture path, so waking that path for
|
|
3464
|
+
* it would hand back precisely what declaring the second moment bought — and a
|
|
3465
|
+
* framework that wants its naming checked while the user types says so by
|
|
3466
|
+
* declaring the rule real-time, which is the one place that decision belongs.
|
|
1869
3467
|
*/
|
|
1870
3468
|
export function verdictPropsOf(rules) {
|
|
1871
3469
|
const props = new Set(VERDICT_PROPS);
|
|
1872
3470
|
for (const rule of rules) {
|
|
3471
|
+
if (rule.family === 'label-presence' && isRealtime(rule))
|
|
3472
|
+
props.add('text');
|
|
3473
|
+
// The level a VIEW declares decides which roles it admits, so changing it —
|
|
3474
|
+
// or clearing it back to "free sketch", which DELETES the key, which is why
|
|
3475
|
+
// `touchesVerdict` reads `oldValues` too — re-judges everything drawn on
|
|
3476
|
+
// that frame. Derived from the rule's own declaration and never from a name
|
|
3477
|
+
// the engine knows, exactly like the transition toggles below.
|
|
3478
|
+
if (rule.admissibility !== undefined) {
|
|
3479
|
+
props.add(rule.admissibility.levelProp);
|
|
3480
|
+
}
|
|
1873
3481
|
if (rule.background === undefined)
|
|
1874
3482
|
continue;
|
|
1875
3483
|
for (const prop of backgroundTransitionVisibleProps(rule.background)) {
|
|
@@ -1906,8 +3514,6 @@ export function touchesVerdict(payload, watched = VERDICT_PROPS) {
|
|
|
1906
3514
|
}
|
|
1907
3515
|
return false;
|
|
1908
3516
|
}
|
|
1909
|
-
/** Shared answer for "this element is nobody's frame", allocated once. */
|
|
1910
|
-
const EMPTY_FRAMEWORKS = new Set();
|
|
1911
3517
|
/**
|
|
1912
3518
|
* Owns the evaluation and the reactive violation list. No-op until a framework
|
|
1913
3519
|
* registers a rule.
|
|
@@ -2029,6 +3635,7 @@ export class ValidationManager extends InteractivityExtension {
|
|
|
2029
3635
|
this._rules = null;
|
|
2030
3636
|
this._verdictProps = null;
|
|
2031
3637
|
this._nudges = null;
|
|
3638
|
+
this._frameworks = null;
|
|
2032
3639
|
this._profiles = null;
|
|
2033
3640
|
this._subscriptions = [];
|
|
2034
3641
|
this._disposeSurfaceEffect = null;
|
|
@@ -2066,6 +3673,15 @@ export class ValidationManager extends InteractivityExtension {
|
|
|
2066
3673
|
this._nudges ??= Array.from(this.std.provider.getAll(QualityNudgeIdentifier).values());
|
|
2067
3674
|
return this._nudges;
|
|
2068
3675
|
}
|
|
3676
|
+
/**
|
|
3677
|
+
* Registered root-instance declarations, resolved once. Empty when every
|
|
3678
|
+
* framework derives its frames from its rules, which is all of them but the
|
|
3679
|
+
* ones that ship no rule at all — see {@link ValidationFrameworkDef}.
|
|
3680
|
+
*/
|
|
3681
|
+
get _activeFrameworks() {
|
|
3682
|
+
this._frameworks ??= Array.from(this.std.provider.getAll(ValidationFrameworkIdentifier).values());
|
|
3683
|
+
return this._frameworks;
|
|
3684
|
+
}
|
|
2069
3685
|
/**
|
|
2070
3686
|
* Registered profiles, resolved once. Empty when no framework ships one, in
|
|
2071
3687
|
* which case every rule is judged by its own declared severity.
|
|
@@ -2078,9 +3694,18 @@ export class ValidationManager extends InteractivityExtension {
|
|
|
2078
3694
|
return this.std.getOptional(OverlayIdentifier(ValidationOverlay.overlayName));
|
|
2079
3695
|
}
|
|
2080
3696
|
mounted() {
|
|
2081
|
-
//
|
|
2082
|
-
//
|
|
2083
|
-
//
|
|
3697
|
+
// No rule registered => never subscribe, never evaluate. The cost of
|
|
3698
|
+
// validation on a board where nothing can be evaluated is this single
|
|
3699
|
+
// length check, once.
|
|
3700
|
+
//
|
|
3701
|
+
// Deliberately about the RULES alone, and not about the root-instance
|
|
3702
|
+
// declarations that also feed `frameworksOf`: what the subscriptions drive
|
|
3703
|
+
// is the evaluation, and a framework that registers no rule has nothing to
|
|
3704
|
+
// evaluate however many frames it declares. Its tooling reaches the user
|
|
3705
|
+
// through getters nobody subscribes to — `nudgesFor` for the Map quality
|
|
3706
|
+
// popup, `profilesFor` for the toolbar dropdown — which are called on the
|
|
3707
|
+
// element in hand at the moment of the gesture, and answer the same thing
|
|
3708
|
+
// whether or not this method ever ran.
|
|
2084
3709
|
if (this._activeRules.length === 0)
|
|
2085
3710
|
return;
|
|
2086
3711
|
// The surface is a SIGNAL, not a fact: it can legitimately be null at
|
|
@@ -2199,15 +3824,13 @@ export class ValidationManager extends InteractivityExtension {
|
|
|
2199
3824
|
return this._activeRules.find(rule => rule.id === ruleId);
|
|
2200
3825
|
}
|
|
2201
3826
|
/**
|
|
2202
|
-
* The profiles selectable on `element`, i.e. those of every framework
|
|
2203
|
-
*
|
|
3827
|
+
* The profiles selectable on `element`, i.e. those of every framework it is a
|
|
3828
|
+
* ROOT INSTANCE of ({@link frameworksOf}).
|
|
2204
3829
|
*
|
|
2205
|
-
*
|
|
2206
|
-
*
|
|
2207
|
-
*
|
|
2208
|
-
*
|
|
2209
|
-
* no rule, no profile offered — and a framework that ships profiles but no
|
|
2210
|
-
* rule offers nothing to choose between, correctly.
|
|
3830
|
+
* Gated for free: flag off, nothing registered, no framework, no profile
|
|
3831
|
+
* offered. A framework that ships profiles and is a root instance of nothing —
|
|
3832
|
+
* neither a rule framing against the role nor a declaration naming it —
|
|
3833
|
+
* offers nothing to choose between, correctly.
|
|
2211
3834
|
*
|
|
2212
3835
|
* Empty for a neutral element, and for a background authored before its role
|
|
2213
3836
|
* existed: no role, no framework, no profile.
|
|
@@ -2219,32 +3842,22 @@ export class ValidationManager extends InteractivityExtension {
|
|
|
2219
3842
|
return this._activeProfiles.filter(profile => frameworks.has(profile.framework));
|
|
2220
3843
|
}
|
|
2221
3844
|
/**
|
|
2222
|
-
* The frameworks `element` is a ROOT INSTANCE of
|
|
2223
|
-
* rule measuring against a background carrying this element's role
|
|
3845
|
+
* The frameworks `element` is a ROOT INSTANCE of — those with a registered
|
|
3846
|
+
* rule measuring against a background carrying this element's role, and those
|
|
3847
|
+
* that declared the role outright ({@link ValidationFrameworkDef}).
|
|
2224
3848
|
*
|
|
2225
3849
|
* The one test that decides whether an element is a framework's frame, and the
|
|
2226
|
-
* only one:
|
|
2227
|
-
*
|
|
2228
|
-
*
|
|
2229
|
-
*
|
|
2230
|
-
*
|
|
3850
|
+
* only one: every per-instance surface (the profile picker, the nudge
|
|
3851
|
+
* checklist, the check-up) agrees about what a root instance is without any of
|
|
3852
|
+
* them saying so twice. Both sources are registered from the flag-gated view
|
|
3853
|
+
* extension, so it stays gated for free — flag off, nothing registered, no
|
|
3854
|
+
* framework.
|
|
2231
3855
|
*
|
|
2232
3856
|
* Empty for a neutral element, and for a background authored before its role
|
|
2233
3857
|
* existed.
|
|
2234
3858
|
*/
|
|
2235
3859
|
frameworksOf(element) {
|
|
2236
|
-
|
|
2237
|
-
if (role === undefined)
|
|
2238
|
-
return EMPTY_FRAMEWORKS;
|
|
2239
|
-
const frameworks = new Set();
|
|
2240
|
-
for (const rule of this._activeRules) {
|
|
2241
|
-
if (rule.backgroundRole === undefined)
|
|
2242
|
-
continue;
|
|
2243
|
-
if (roleIsA(role, rule.backgroundRole, rule.roles)) {
|
|
2244
|
-
frameworks.add(rule.framework);
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
return frameworks;
|
|
3860
|
+
return frameworksOfRole(element.role, this._activeRules, this._activeFrameworks);
|
|
2248
3861
|
}
|
|
2249
3862
|
/**
|
|
2250
3863
|
* The nudges offered on `element` (PF7.10), in declared order.
|
|
@@ -2419,9 +4032,7 @@ export class ValidationManager extends InteractivityExtension {
|
|
|
2419
4032
|
sliceStart = performance.now();
|
|
2420
4033
|
}
|
|
2421
4034
|
}
|
|
2422
|
-
return generation === this._checkupGeneration
|
|
2423
|
-
? this.checkup$.peek()
|
|
2424
|
-
: null;
|
|
4035
|
+
return generation === this._checkupGeneration ? this.checkup$.peek() : null;
|
|
2425
4036
|
}
|
|
2426
4037
|
/**
|
|
2427
4038
|
* The profile `element` is actually checked against — the one it names, or
|