feat: add screen flow view for flow diagrams

This commit is contained in:
nejimakibird 2026-07-10 13:48:28 +09:00
parent 3e5f0a5b5c
commit 54a4287360
13 changed files with 736 additions and 55 deletions

View file

@ -93,7 +93,8 @@ Current Color Scheme application:
| `domain_diagram` Area / Tree | `target=domain`, `kind=<Domain.kind>` |
| `dfd_diagram` objects | `target=dfd`, `kind=<Objects.kind>` |
| `dfd_diagram` Domain subgraphs | `target=domain`, `kind=<Domain.kind>` |
| `flow_diagram` Domain subgraphs | `target=domain`, `kind=<Objects.domain>` matching Domain kind rows |
| low_diagram nodes | arget=flow_diagram, kind=<Objects.kind> |
| low_diagram Domain subgraphs | arget=domain, kind=<Objects.domain> matching Domain kind rows |
| `app_process` Business Flow | `target=app_process`, `kind=<Steps.kind>` |
| Weave Map layer subgraphs | `target=weave_map`, `kind=ui / process / rule / rule_state / ui_message / data / mapping / implementation / data_flow / relationship / source / warning / other` |

View file

@ -40,6 +40,7 @@ kind: screen_communication
| `id` | yes | Unique model id |
| `name` | yes | Human-readable name |
| `kind` | yes | `screen_communication` for the MVP |
| `flow_view` | no | `detail` (default) or `screen` |
## Domain Sources / Domains
@ -89,6 +90,13 @@ Supported MVP object kinds:
| `store` | `lin-cyl` | `store` |
| `datastore` | `lin-cyl` | `store` |
| `external` | `rect` | `external` |
| `actor` | `rect` | `external` |
| `user` | `rect` | `external` |
| `message` | `rect` | `context` |
| `data` | `rect` | `process` |
| `api` | `rect` | `process` |
| `service` | `rect` | `process` |
| `handler` | `rect` | `process` |
| unknown values | `rect` | fallback |
`Objects.ref` may point to `screen`, `app_process`, `data_object`, `dfd_object`, or another model asset. Unknown object kinds should not block rendering.
@ -109,9 +117,17 @@ Mermaid edge labels are assembled compactly from `trigger`, `kind`, and `data`.
## Rendering
The MVP uses Internal Detail View only and renders the raw Objects / Flows graph without projection, folding, or view selectors.
Flow Diagram supports two MVP view modes.
Surface View, Communication View, projection, context/process/store folding, transition coverage, and automatic generation from screen or app_process models are future work.
`detail` is the default view and renders the raw Objects / Flows graph. It keeps internal processes, contexts, stores, external systems, and all declared flow edges visible.
`screen` is a Screen Flow projection. It keeps user-visible objects with kind `screen`, `external`, `actor`, `user`, `context`, or `message`, and folds internal objects such as `work_object`, `app_process`, `process`, `session`, `datastore`, `data`, `store`, `api`, `service`, and `handler`. Edges are projected as reachability between visible objects. If a diagram has only folded/internal objects, rendering falls back to the Detail view so the graph is not empty.
Screen Flow edge labels are taken from the incoming flow to the visible target, using this priority: `condition`, then `trigger`, then `kind`. When multiple projected paths produce the same visible source/target pair, labels are merged with ` / `.
Screen Flow is a derived rendering projection. It does not change the source Markdown, does not create data nodes from `Flows.data`, and does not replace the detailed communication model.
Toolbar switching is not part of the MVP; use `flow_view` frontmatter to choose the default view for a file.
## AI Generation Notes
@ -120,4 +136,5 @@ Surface View, Communication View, projection, context/process/store folding, tra
- Keep table headers exactly as documented.
- Use local `Objects.id` values in `Flows.from` and `Flows.to`.
- Put edge semantics in `Flows.kind`, user/system events in `Flows.trigger`, payloads or data references in `Flows.data`, and guards in `Flows.condition`.
- Use `flow_view: screen` only when the same detailed graph should default to Screen Flow projection.
- Do not add generated state matrices, folding rules, or automatic screen/process derivations.

View file

@ -93,7 +93,8 @@ ref が解決できない場合、または対象ファイルが `type: color_sc
| `domain_diagram` Area / Tree | `target=domain`, `kind=<Domain.kind>` |
| `dfd_diagram` objects | `target=dfd`, `kind=<Objects.kind>` |
| `dfd_diagram` Domain subgraphs | `target=domain`, `kind=<Domain.kind>` |
| `flow_diagram` Domain subgraphs | `target=domain`, `kind=<Objects.domain>` に対応する Domain kind rows |
| low_diagram nodes | arget=flow_diagram, kind=<Objects.kind> |
| low_diagram Domain subgraphs | arget=domain, kind=<Objects.domain> に対応する Domain kind rows |
| `app_process` Business Flow | `target=app_process`, `kind=<Steps.kind>` |
| Weave Map layer subgraphs | `target=weave_map`, `kind=ui / process / rule / rule_state / ui_message / data / mapping / implementation / data_flow / relationship / source / warning / other` |

View file

@ -40,6 +40,7 @@ kind: screen_communication
| `id` | yes | 一意な model id |
| `name` | yes | 表示名 |
| `kind` | yes | MVPでは `screen_communication` |
| `flow_view` | no | `detail` (default) または `screen` |
## Domain Sources / Domains
@ -89,6 +90,13 @@ MVPで対応する object kind:
| `store` | `lin-cyl` | `store` |
| `datastore` | `lin-cyl` | `store` |
| `external` | `rect` | `external` |
| `actor` | `rect` | `external` |
| `user` | `rect` | `external` |
| `message` | `rect` | `context` |
| `data` | `rect` | `process` |
| `api` | `rect` | `process` |
| `service` | `rect` | `process` |
| `handler` | `rect` | `process` |
| unknown values | `rect` | fallback |
`Objects.ref``screen`, `app_process`, `data_object`, `dfd_object` または他の model asset を参照できます。未知の object kind はレンダリングを止めません。
@ -109,9 +117,17 @@ Mermaid edge label は `trigger`、`kind`、`data` から compact に組み立
## Rendering
MVPでは Internal Detail View のみを使います。Objects / Flows の raw graph を表示し、projection、folding、view selector は実装しません
Flow Diagram は MVP として2つの view mode を持ちます
Surface View、Communication View、projection、context/process/store folding、transition coverage、screen や app_process からの自動生成は将来対応です。
`detail` は既定の view です。Objects / Flows の raw graph を表示し、内部 process、context、store、external system、宣言された flow edge をそのまま表示します。
`screen` は Screen Flow projection です。`screen`, `external`, `actor`, `user`, `context`, `message` のような user-visible object を残し、`work_object`, `app_process`, `process`, `session`, `datastore`, `data`, `store`, `api`, `service`, `handler` などの内部 object を折りたたみます。Edge は表示対象 object 間の到達関係として投影されます。折りたたみ対象の内部 object しか無い diagram では、空表示にせず Detail view に fallback します。
Screen Flow の edge label は、表示対象 target へ入る flow から `condition`、`trigger`、`kind` の優先順で作ります。同じ visible source / target に複数 path が畳み込まれる場合、label は ` / ` で結合されます。
Screen Flow は派生レンダリング projection です。source Markdown は変更せず、`Flows.data` から data node を作らず、詳細な communication model を置き換えるものではありません。
Toolbar 切り替えは MVP には含めません。ファイルの既定 view を選ぶ場合は frontmatter の `flow_view` を使います。
## AI生成時の注意
@ -120,4 +136,5 @@ Surface View、Communication View、projection、context/process/store folding
- テーブルヘッダーを仕様通りに保つ。
- `Flows.from``Flows.to` にはローカル `Objects.id` を使う。
- edge semantics は `Flows.kind`、user/system event は `Flows.trigger`、payload や data reference は `Flows.data`、guard は `Flows.condition` に書く。
- 同じ詳細 graph を Screen Flow projection で既定表示したい場合だけ `flow_view: screen` を使う。
- state matrix、folding rule、自動生成された screen/process derivation は追加しない。

224
main.js
View file

@ -3062,6 +3062,7 @@ function localizeDiagnosticMessage(message, language) {
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `\u4E88\u7D04\u6E08\u307F kind "${kind}" \u304C\u4F7F\u308F\u308C\u3066\u3044\u307E\u3059\u3002`],
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} \u3067\u306F ${renderer} renderer \u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" \u306F\u4E0D\u660E\u3067\u3059\u3002format \u306E\u65E2\u5B9A renderer \u3092\u4F7F\u3044\u307E\u3059\u3002`],
[/^unknown flow_view; expected "detail" or "screen"$/, 'flow_view \u306F "detail" \u307E\u305F\u306F "screen" \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002'],
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" \u306F\u901A\u5E38\u3068\u7570\u306A\u308B\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`],
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" \u306F\u81EA\u5DF1\u30EB\u30FC\u30D7\u3067\u3059\u3002`],
[/^Domain id is required\.$/, "Domain \u306E id \u304C\u5FC5\u8981\u3067\u3059\u3002"],
@ -11721,6 +11722,13 @@ var DFD_OBJECT_HEADERS_WITHOUT_DOMAIN = ["id", "label", "kind", "ref", "notes"];
var LEGACY_OBJECT_HEADERS = ["ref", "notes"];
var FLOW_OBJECT_KINDS = /* @__PURE__ */ new Set([
"screen",
"actor",
"user",
"message",
"data",
"api",
"service",
"handler",
"process",
"app_process",
"context",
@ -11765,6 +11773,7 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
const name = typeof frontmatter.name === "string" ? frontmatter.name.trim() : "";
const level = typeof frontmatter.level === "string" || typeof frontmatter.level === "number" ? String(frontmatter.level).trim() : void 0;
const kind = typeof frontmatter.kind === "string" && frontmatter.kind.trim() ? frontmatter.kind.trim() : options.defaultKind;
const flowView = parseFlowDiagramViewMode(frontmatter.flow_view);
const rawType = typeof frontmatter.type === "string" ? frontmatter.type.trim() : "";
const isAcceptedType = rawType === options.type || options.type === "flow_diagram" && rawType === "flow-diagram";
if (!isAcceptedType) {
@ -11779,6 +11788,9 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
if (options.schema === "flow_diagram" && kind !== "screen_communication") {
warnings.push(createWarning8(path2, "kind", 'expected kind "screen_communication"'));
}
if (options.schema === "flow_diagram" && isUnknownFlowDiagramViewMode(frontmatter.flow_view)) {
warnings.push(createWarning8(path2, "flow_view", 'unknown flow_view; expected "detail" or "screen"'));
}
const objectsTable = parseDfdObjectsTable(sections.Objects, path2, {
schema: options.schema,
allowLegacyObjects: options.allowLegacyObjects,
@ -11898,6 +11910,7 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
name: name || fallbackTitle,
kind: "screen_communication",
description: joinSectionLines2(sections.Summary),
flowView,
domainSources: domainSourcesTable.rows,
domains: domainsTable.rows,
objectRefs,
@ -11934,6 +11947,12 @@ function parseDfdLikeDiagramFile(markdown, path2, options) {
warnings
};
}
function parseFlowDiagramViewMode(value) {
return typeof value === "string" && value.trim() === "screen" ? "screen" : "detail";
}
function isUnknownFlowDiagramViewMode(value) {
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "detail" && value.trim() !== "screen";
}
function getFileStem4(path2) {
return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
}
@ -15023,6 +15042,7 @@ function createShallowModel(path2, fileType, frontmatter) {
id,
name,
kind: "screen_communication",
flowView: "detail",
domainSources: [],
domains: [],
objectRefs: [],
@ -18731,22 +18751,23 @@ function renderDfdMermaidDiagram(diagram, options) {
exportAndOpenPngLabel: options?.exportAndOpenPngLabel,
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
});
const renderedDiagram = getFlowDiagramRenderedDiagram(diagram);
if (!options?.hideDetails) {
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
const domainDetails = createDomainPlacementDetails(renderedDiagram, options?.dfdDetailLabels);
if (domainDetails) {
shell3.root.appendChild(domainDetails);
}
shell3.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
shell3.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
shell3.root.appendChild(createObjectDetails(renderedDiagram, options?.dfdDetailLabels));
shell3.root.appendChild(createFlowDetails(renderedDiagram.edges, options?.dfdDetailLabels));
}
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram) ? buildFlowDiagramHoverMetadata(diagram, sourcePath) : null;
const flowHoverMetadata = isFlowDiagramModel(renderedDiagram.diagram) ? buildFlowDiagramHoverMetadata(renderedDiagram, sourcePath) : null;
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
diagram,
renderedDiagram,
sourcePath
);
const ready = renderMermaidSourceIntoShell(shell3, {
source: buildDfdMermaidSource(diagram, options?.colorScheme),
source: buildDfdMermaidSource(renderedDiagram, options?.colorScheme),
renderIdPrefix: "model_weave_dfd",
fitVerticalAlign: options?.fitVerticalAlign,
viewportState: options?.viewportState,
@ -18775,7 +18796,7 @@ function renderDfdMermaidDiagram(diagram, options) {
nodeClassName: "model-weave-mermaid-interactive-node",
dragThreshold: 6,
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
debugName: isFlowDiagramModel(renderedDiagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
formatTitle: (target) => target.label ? `${target.label} (${target.targetType ?? "model"})` : target.linktext,
openLinkText: isFlowDiagramModel(diagram.diagram) ? (target, event) => {
const linktext = getFlowDiagramOpenLinkText(target);
@ -19010,7 +19031,7 @@ function buildDfdMermaidInteractionTargets(diagram, sourcePath) {
}
function buildDfdMermaidSource(diagram, colorScheme) {
if (isFlowDiagramModel(diagram.diagram)) {
return buildFlowDiagramMermaidSource(diagram, colorScheme);
return buildFlowDiagramMermaidSource(getFlowDiagramRenderedDiagram(diagram), colorScheme);
}
const palette = getModelWeaveMermaidPalette();
const lines = ["flowchart LR"];
@ -19081,21 +19102,146 @@ function buildDfdMermaidSource(diagram, colorScheme) {
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
}
}
if (colorClasses.size > 0) {
lines.push("");
for (const [className, style] of colorClasses) {
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
}
}
if (domainStyles.length > 0) {
lines.push("", ...domainStyles);
}
return lines.join("\n");
}
function getFlowDiagramRenderedDiagram(diagram) {
if (!isFlowDiagramModel(diagram.diagram) || resolveFlowDiagramViewMode(diagram.diagram) !== "screen") {
return diagram;
}
return buildFlowDiagramScreenFlowProjection(diagram);
}
function resolveFlowDiagramViewMode(diagram, toolbarOverride) {
return toolbarOverride ?? diagram.flowView ?? "detail";
}
function buildFlowDiagramScreenFlowProjection(diagram) {
if (!isFlowDiagramModel(diagram.diagram)) {
return diagram;
}
const visibleNodes = diagram.nodes.filter(isScreenFlowVisibleNode);
if (visibleNodes.length === 0) {
return diagram;
}
const visibleIds = new Set(visibleNodes.map((node) => node.id));
const outgoing = /* @__PURE__ */ new Map();
for (const edge of diagram.edges) {
const edges = outgoing.get(edge.source) ?? [];
edges.push(edge);
outgoing.set(edge.source, edges);
}
const projectedByPair = /* @__PURE__ */ new Map();
for (const sourceNode of visibleNodes) {
const queue = (outgoing.get(sourceNode.id) ?? []).map((edge) => ({
edge,
visitedEdges: /* @__PURE__ */ new Set([getProjectionEdgeVisitKey(edge)])
}));
while (queue.length > 0) {
const current = queue.shift();
if (!current) {
continue;
}
const targetId = current.edge.target;
if (visibleIds.has(targetId)) {
if (targetId !== sourceNode.id) {
addProjectedScreenFlowEdge(projectedByPair, sourceNode.id, targetId, current.edge);
}
continue;
}
for (const nextEdge of outgoing.get(targetId) ?? []) {
const visitKey = getProjectionEdgeVisitKey(nextEdge);
if (current.visitedEdges.has(visitKey)) {
continue;
}
const nextVisitedEdges = new Set(current.visitedEdges);
nextVisitedEdges.add(visitKey);
queue.push({ edge: nextEdge, visitedEdges: nextVisitedEdges });
}
}
}
return {
...diagram,
diagram: {
...diagram.diagram,
flowView: "detail"
},
nodes: visibleNodes,
edges: [...projectedByPair.values()]
};
}
function isScreenFlowVisibleNode(node) {
switch (node.kind) {
case "screen":
case "external":
case "actor":
case "user":
case "context":
case "message":
return true;
default:
return false;
}
}
function addProjectedScreenFlowEdge(projectedByPair, source, target, incomingEdge) {
const key = `${source}->${target}`;
const label = buildScreenFlowProjectionEdgeLabel(incomingEdge);
const existing = projectedByPair.get(key);
if (!existing) {
projectedByPair.set(key, {
id: `screen_flow:${source}->${target}`,
source,
target,
kind: "flow",
label,
metadata: {
...incomingEdge.metadata,
projected: true,
sourceFlowId: incomingEdge.id,
dataRaw: void 0
}
});
return;
}
existing.label = mergeScreenFlowLabels(existing.label, label);
}
function buildScreenFlowProjectionEdgeLabel(edge) {
return getStringMetadata(edge.metadata, "condition")?.trim() || getStringMetadata(edge.metadata, "trigger")?.trim() || getStringMetadata(edge.metadata, "flowKind")?.trim() || void 0;
}
function mergeScreenFlowLabels(left, right) {
const values = [
...(left ?? "").split(" / "),
...(right ?? "").split(" / ")
].map((value) => value.trim()).filter(Boolean);
const unique = [...new Set(values)];
if (unique.length === 0) {
return void 0;
}
const merged = unique.join(" / ");
return merged.length > 80 ? `${merged.slice(0, 77)}...` : merged;
}
function getProjectionEdgeVisitKey(edge) {
return `${edge.id ?? ""}:${edge.source}->${edge.target}`;
}
function buildFlowDiagramMermaidSource(diagram, colorScheme) {
const palette = getModelWeaveMermaidPalette();
const lines = [
"flowchart LR",
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
];
const lines = ["flowchart LR"];
const colorClasses = /* @__PURE__ */ new Map();
if (!colorScheme) {
lines.push(
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
);
}
const nodeIds = /* @__PURE__ */ new Map();
const domainStyles = [];
const flowDomains = getFlowDiagramDomains(diagram);
@ -19126,11 +19272,12 @@ function buildFlowDiagramMermaidSource(diagram, colorScheme) {
nodeIds,
1,
colorScheme,
colorClasses,
domainStyles
);
}
for (const node of ungroupedNodes) {
appendFlowDiagramNode(lines, node, nodeIds, 1);
appendFlowDiagramNode(lines, node, nodeIds, 1, colorScheme, colorClasses);
}
for (const edge of diagram.edges) {
const from = nodeIds.get(edge.source);
@ -19145,12 +19292,18 @@ function buildFlowDiagramMermaidSource(diagram, colorScheme) {
lines.push(` ${from} --> ${to}`);
}
}
if (colorClasses.size > 0) {
lines.push("");
for (const [className, style] of colorClasses) {
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
}
}
if (domainStyles.length > 0) {
lines.push("", ...domainStyles);
}
return lines.join("\n");
}
function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, domainStyles) {
function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeIds, depth, colorScheme, colorClasses, domainStyles) {
const childLines = [];
for (const child of domainNode.children) {
appendFlowDiagramDomainSubgraph(
@ -19160,6 +19313,7 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
nodeIds,
depth + 1,
colorScheme,
colorClasses,
domainStyles
);
}
@ -19172,7 +19326,7 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
lines.push(`${indent}subgraph ${domainMermaidId}["${buildFlowDomainLabel(domainNode.domain)}"]`);
lines.push(...childLines);
for (const node of nodes) {
appendFlowDiagramNode(lines, node, nodeIds, depth + 1);
appendFlowDiagramNode(lines, node, nodeIds, depth + 1, colorScheme, colorClasses);
}
lines.push(`${indent}end`);
if (colorScheme) {
@ -19184,10 +19338,10 @@ function appendFlowDiagramDomainSubgraph(lines, domainNode, groupedNodes, nodeId
}
return true;
}
function appendFlowDiagramNode(lines, node, nodeIds, depth) {
function appendFlowDiagramNode(lines, node, nodeIds, depth, colorScheme, colorClasses) {
const mermaidId = toMermaidNodeId(node.id);
const shape = toFlowDiagramMermaidShape(node.kind);
const className = toFlowDiagramClassName(node.kind);
const className = colorScheme ? registerFlowDiagramColorClass(node.kind, colorScheme, colorClasses) : toFlowDiagramClassName(node.kind);
const indent = " ".repeat(depth);
nodeIds.set(node.id, mermaidId);
lines.push(`${indent}${mermaidId}@{ shape: ${shape}, label: "${escapeMermaidLabel2(node.label ?? node.ref ?? node.id)}" }`);
@ -19217,7 +19371,7 @@ function getFlowDomainColorKind(domain) {
}
function getDfdMermaidColorSchemeTargets(diagram) {
if (isFlowDiagramModel(diagram.diagram)) {
return hasNodesWithDomain(diagram) ? ["domain"] : [];
return hasNodesWithDomain(diagram) ? ["flow_diagram", "domain"] : ["flow_diagram"];
}
if (isDfdDiagramModel(diagram.diagram)) {
return hasNodesWithDomain(diagram) || (diagram.diagram.domains?.length ?? 0) > 0 ? ["dfd", "domain"] : ["dfd"];
@ -19239,12 +19393,31 @@ function toFlowDiagramMermaidShape(kind) {
case "app_process":
case "context":
case "work_object":
case "actor":
case "user":
case "message":
case "data":
case "api":
case "service":
case "handler":
case "external":
case "unknown":
default:
return "rect";
}
}
function registerFlowDiagramColorClass(kind, colorScheme, colorClasses) {
const className = toFlowDiagramColorClassName(kind);
colorClasses?.set(
className,
resolveColorStyle(colorScheme, "flow_diagram", typeof kind === "string" ? kind : void 0)
);
return className;
}
function toFlowDiagramColorClassName(kind) {
const suffix = typeof kind === "string" && kind.trim() ? kind.trim() : "default";
return `kind_flow_diagram_${sanitizeMermaidId(suffix)}`;
}
function toFlowDiagramClassName(kind) {
switch (kind) {
case "screen":
@ -19255,11 +19428,18 @@ function toFlowDiagramClassName(kind) {
return "store";
case "context":
case "work_object":
case "message":
return "context";
case "process":
case "app_process":
case "data":
case "api":
case "service":
case "handler":
return "process";
case "external":
case "actor":
case "user":
return "external";
case "unknown":
default:

View file

@ -53,4 +53,5 @@ The samples are organized by format and purpose rather than by historical versio
- [relations/REL-WMS-CORE.md](relations/REL-WMS-CORE.md)
- [domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md](domains/DOMAINS-SAMPLE-CHECKOUT-FLOW.md)
- [flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION.md](flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-COMMUNICATION.md)
- [flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md](flow_diagram/FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW.md)
- [color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md](color_scheme/COLOR-SCHEME-SAMPLE-FLOW-DOMAIN.md)

View file

@ -0,0 +1,53 @@
---
type: flow_diagram
id: FLOW-SAMPLE-CHECKOUT-SCREEN-FLOW
name: Sample Checkout Screen Flow
kind: screen_communication
flow_view: screen
tags:
- Sample
- FlowDiagram
- DomainSources
---
# Sample Checkout Screen Flow
## Summary
Checkout screen submitから、内部処理を折りたたんだScreen Flow projectionを確認するFlow Diagramサンプルです。
このサンプルでは、同じ詳細Objects / Flowsから、user / screen / external / message中心の簡略表示、Domain Sources、parent-child Domain group、投影edge labelを確認します。
## Domain Sources
| ref | notes |
|---|---|
| [[DOMAINS-SAMPLE-CHECKOUT-FLOW]] | Sample Domains for Flow Diagram group rendering |
## Objects
| id | label | kind | ref | domain | notes |
|---|---|---|---|---|---|
| user | Customer | external | | actor_area | User who submits the checkout form |
| checkout_screen | Checkout Screen | screen | | frontend_area | Checkout screen |
| submit_action | Submit Order Action | work_object | | frontend_area | UI action on the checkout screen |
| checkout_process | Checkout Process | app_process | | application_area | Server-side checkout handling |
| session_store | Session Store | session | | data_area | Temporary checkout session |
| order_store | Order Store | datastore | | data_area | Persisted order data |
| payment_gateway | Payment Gateway | external | | external_area | External payment service |
| completion_screen | Completion Screen | screen | | frontend_area | Final screen after successful checkout |
| error_message | Error Message | context | | frontend_area | Validation or payment error display |
## Flows
| id | from | to | kind | trigger | data | condition | notes |
|---|---|---|---|---|---|---|---|
| F01 | user | checkout_screen | event | open | Checkout page request | | User opens checkout screen |
| F02 | checkout_screen | submit_action | event | click submit | Checkout form values | | UI submit action |
| F03 | submit_action | checkout_process | command | submit | DATA-CHECKOUT-SUBMIT-REQUEST | | Submit request from screen to process |
| F04 | checkout_process | session_store | read | load session | Checkout session | | Plain text data label |
| F05 | checkout_process | payment_gateway | call | authorize payment | DATA-PAYMENT-AUTHORIZATION-REQUEST | payment required | External payment authorization |
| F06 | payment_gateway | checkout_process | response | authorization result | DATA-PAYMENT-AUTHORIZATION-RESULT | | Payment result response |
| F07 | checkout_process | order_store | command | save order | DATA-ORDER-CREATE-COMMAND | authorized | Persist order |
| F08 | checkout_process | completion_screen | navigation | show complete | DATA-CHECKOUT-RESULT | success | Navigate to completion screen |
| F09 | checkout_process | error_message | event | show error | Checkout error | failed | Error branch |

View file

@ -1522,6 +1522,7 @@ export function localizeDiagnosticMessage(message: string, language?: string): s
[/^reserved kind used: "([^"]+)"$/, (_match, kind) => `予約済み kind "${kind}" が使われています。`],
[/^(.+) renderer is not supported for (.+)\. Using the format default renderer\.$/, (_match, renderer, format) => `${format} では ${renderer} renderer はサポートされていません。format の既定 renderer を使います。`],
[/^Unknown render_mode value "([^"]+)"\. Using the format default renderer\.$/, (_match, value) => `render_mode "${value}" は不明です。format の既定 renderer を使います。`],
[/^unknown flow_view; expected "detail" or "screen"$/, 'flow_view は "detail" または "screen" を指定してください。'],
[/^DFD flow shape "([^"]+)" may be unusual$/, (_match, shape) => `DFD flow shape "${shape}" は通常と異なる可能性があります。`],
[/^DFD flow "([^"]+)" is a self-loop$/, (_match, flow) => `DFD flow "${flow}" は自己ループです。`],
[/^Domain id is required\.$/, "Domain の id が必要です。"],

View file

@ -943,6 +943,7 @@ function createShallowModel(
id,
name,
kind: "screen_communication",
flowView: "detail",
domainSources: [],
domains: [],
objectRefs: [],

View file

@ -23,6 +23,13 @@ const DFD_OBJECT_HEADERS_WITHOUT_DOMAIN = ["id", "label", "kind", "ref", "notes"
const LEGACY_OBJECT_HEADERS = ["ref", "notes"];
const FLOW_OBJECT_KINDS = new Set<string>([
"screen",
"actor",
"user",
"message",
"data",
"api",
"service",
"handler",
"process",
"app_process",
"context",
@ -103,6 +110,7 @@ function parseDfdLikeDiagramFile(
const kind = typeof frontmatter.kind === "string" && frontmatter.kind.trim()
? frontmatter.kind.trim()
: options.defaultKind;
const flowView = parseFlowDiagramViewMode(frontmatter.flow_view);
const rawType = typeof frontmatter.type === "string" ? frontmatter.type.trim() : "";
const isAcceptedType = rawType === options.type ||
@ -119,6 +127,9 @@ function parseDfdLikeDiagramFile(
if (options.schema === "flow_diagram" && kind !== "screen_communication") {
warnings.push(createWarning(path, "kind", 'expected kind "screen_communication"'));
}
if (options.schema === "flow_diagram" && isUnknownFlowDiagramViewMode(frontmatter.flow_view)) {
warnings.push(createWarning(path, "flow_view", 'unknown flow_view; expected "detail" or "screen"'));
}
const objectsTable = parseDfdObjectsTable(sections.Objects, path, {
schema: options.schema,
@ -248,6 +259,7 @@ function parseDfdLikeDiagramFile(
name: name || fallbackTitle,
kind: "screen_communication",
description: joinSectionLines(sections.Summary),
flowView,
domainSources: domainSourcesTable.rows,
domains: domainsTable.rows,
objectRefs,
@ -286,6 +298,14 @@ function parseDfdLikeDiagramFile(
};
}
function parseFlowDiagramViewMode(value: unknown): "detail" | "screen" {
return typeof value === "string" && value.trim() === "screen" ? "screen" : "detail";
}
function isUnknownFlowDiagramViewMode(value: unknown): boolean {
return typeof value === "string" && value.trim().length > 0 && value.trim() !== "detail" && value.trim() !== "screen";
}
function getFileStem(path: string): string {
return path.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? "";
}

View file

@ -5,6 +5,8 @@ import type {
DiagramEdge,
DfdDiagramModel,
DomainEntry,
FlowDiagramModel,
FlowDiagramViewMode,
ResolvedColorScheme,
ResolvedColorStyle,
ResolvedDiagram
@ -106,26 +108,28 @@ export function renderDfdMermaidDiagram(
exportAndOpenPngTitle: options?.exportAndOpenPngTitle
});
const renderedDiagram = getFlowDiagramRenderedDiagram(diagram);
if (!options?.hideDetails) {
const domainDetails = createDomainPlacementDetails(diagram, options?.dfdDetailLabels);
const domainDetails = createDomainPlacementDetails(renderedDiagram, options?.dfdDetailLabels);
if (domainDetails) {
shell.root.appendChild(domainDetails);
}
shell.root.appendChild(createObjectDetails(diagram, options?.dfdDetailLabels));
shell.root.appendChild(createFlowDetails(diagram.edges, options?.dfdDetailLabels));
shell.root.appendChild(createObjectDetails(renderedDiagram, options?.dfdDetailLabels));
shell.root.appendChild(createFlowDetails(renderedDiagram.edges, options?.dfdDetailLabels));
}
const sourcePath = options?.interactionSourcePath ?? diagram.diagram.path;
const flowHoverMetadata = isFlowDiagramModel(diagram.diagram)
? buildFlowDiagramHoverMetadata(diagram, sourcePath)
const flowHoverMetadata = isFlowDiagramModel(renderedDiagram.diagram)
? buildFlowDiagramHoverMetadata(renderedDiagram, sourcePath)
: null;
const interactionTargets = flowHoverMetadata?.objects ?? buildDfdMermaidInteractionTargets(
diagram,
renderedDiagram,
sourcePath
);
const ready = renderMermaidSourceIntoShell(shell, {
source: buildDfdMermaidSource(diagram, options?.colorScheme),
source: buildDfdMermaidSource(renderedDiagram, options?.colorScheme),
renderIdPrefix: "model_weave_dfd",
fitVerticalAlign: options?.fitVerticalAlign,
viewportState: options?.viewportState,
@ -155,7 +159,7 @@ export function renderDfdMermaidDiagram(
nodeClassName: "model-weave-mermaid-interactive-node",
dragThreshold: 6,
isDebugEnabled: () => options?.showMermaidRenderDebug === true,
debugName: isFlowDiagramModel(diagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
debugName: isFlowDiagramModel(renderedDiagram.diagram) ? "Flow Diagram Mermaid" : "DFD Mermaid",
formatTitle: (target) => target.label
? `${target.label} (${target.targetType ?? "model"})`
: target.linktext,
@ -440,7 +444,7 @@ export function buildDfdMermaidSource(
colorScheme?: ResolvedColorScheme
): string {
if (isFlowDiagramModel(diagram.diagram)) {
return buildFlowDiagramMermaidSource(diagram, colorScheme);
return buildFlowDiagramMermaidSource(getFlowDiagramRenderedDiagram(diagram), colorScheme);
}
const palette = getModelWeaveMermaidPalette();
@ -520,6 +524,13 @@ export function buildDfdMermaidSource(
}
}
if (colorClasses.size > 0) {
lines.push("");
for (const [className, style] of colorClasses) {
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
}
}
if (domainStyles.length > 0) {
lines.push("", ...domainStyles);
}
@ -528,19 +539,168 @@ export function buildDfdMermaidSource(
}
function getFlowDiagramRenderedDiagram(diagram: ResolvedDiagram): ResolvedDiagram {
if (!isFlowDiagramModel(diagram.diagram) || resolveFlowDiagramViewMode(diagram.diagram) !== "screen") {
return diagram;
}
return buildFlowDiagramScreenFlowProjection(diagram);
}
export function resolveFlowDiagramViewMode(
diagram: FlowDiagramModel,
toolbarOverride?: FlowDiagramViewMode
): FlowDiagramViewMode {
return toolbarOverride ?? diagram.flowView ?? "detail";
}
export function buildFlowDiagramScreenFlowProjection(diagram: ResolvedDiagram): ResolvedDiagram {
if (!isFlowDiagramModel(diagram.diagram)) {
return diagram;
}
const visibleNodes = diagram.nodes.filter(isScreenFlowVisibleNode);
if (visibleNodes.length === 0) {
return diagram;
}
const visibleIds = new Set(visibleNodes.map((node) => node.id));
const outgoing = new Map<string, DiagramEdge[]>();
for (const edge of diagram.edges) {
const edges = outgoing.get(edge.source) ?? [];
edges.push(edge);
outgoing.set(edge.source, edges);
}
const projectedByPair = new Map<string, DiagramEdge>();
for (const sourceNode of visibleNodes) {
const queue = (outgoing.get(sourceNode.id) ?? []).map((edge) => ({
edge,
visitedEdges: new Set<string>([getProjectionEdgeVisitKey(edge)])
}));
while (queue.length > 0) {
const current = queue.shift();
if (!current) {
continue;
}
const targetId = current.edge.target;
if (visibleIds.has(targetId)) {
if (targetId !== sourceNode.id) {
addProjectedScreenFlowEdge(projectedByPair, sourceNode.id, targetId, current.edge);
}
continue;
}
for (const nextEdge of outgoing.get(targetId) ?? []) {
const visitKey = getProjectionEdgeVisitKey(nextEdge);
if (current.visitedEdges.has(visitKey)) {
continue;
}
const nextVisitedEdges = new Set(current.visitedEdges);
nextVisitedEdges.add(visitKey);
queue.push({ edge: nextEdge, visitedEdges: nextVisitedEdges });
}
}
}
return {
...diagram,
diagram: {
...diagram.diagram,
flowView: "detail"
},
nodes: visibleNodes,
edges: [...projectedByPair.values()]
};
}
function isScreenFlowVisibleNode(node: DiagramNode): boolean {
switch (node.kind) {
case "screen":
case "external":
case "actor":
case "user":
case "context":
case "message":
return true;
default:
return false;
}
}
function addProjectedScreenFlowEdge(
projectedByPair: Map<string, DiagramEdge>,
source: string,
target: string,
incomingEdge: DiagramEdge
): void {
const key = `${source}->${target}`;
const label = buildScreenFlowProjectionEdgeLabel(incomingEdge);
const existing = projectedByPair.get(key);
if (!existing) {
projectedByPair.set(key, {
id: `screen_flow:${source}->${target}`,
source,
target,
kind: "flow",
label,
metadata: {
...incomingEdge.metadata,
projected: true,
sourceFlowId: incomingEdge.id,
dataRaw: undefined
}
});
return;
}
existing.label = mergeScreenFlowLabels(existing.label, label);
}
function buildScreenFlowProjectionEdgeLabel(edge: DiagramEdge): string | undefined {
return getStringMetadata(edge.metadata, "condition")?.trim() ||
getStringMetadata(edge.metadata, "trigger")?.trim() ||
getStringMetadata(edge.metadata, "flowKind")?.trim() ||
undefined;
}
function mergeScreenFlowLabels(left: string | undefined, right: string | undefined): string | undefined {
const values = [
...(left ?? "").split(" / "),
...(right ?? "").split(" / ")
]
.map((value) => value.trim())
.filter(Boolean);
const unique = [...new Set(values)];
if (unique.length === 0) {
return undefined;
}
const merged = unique.join(" / ");
return merged.length > 80 ? `${merged.slice(0, 77)}...` : merged;
}
function getProjectionEdgeVisitKey(edge: DiagramEdge): string {
return `${edge.id ?? ""}:${edge.source}->${edge.target}`;
}
function buildFlowDiagramMermaidSource(
diagram: ResolvedDiagram,
colorScheme?: ResolvedColorScheme
): string {
const palette = getModelWeaveMermaidPalette();
const lines: string[] = [
"flowchart LR",
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
];
const lines: string[] = ["flowchart LR"];
const colorClasses = new Map<string, ResolvedColorStyle>();
if (!colorScheme) {
lines.push(
` ${buildModelWeaveMermaidClassDef("screen", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("process", palette.dfdProcessFill, palette.dfdProcessBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("context", palette.dfdOtherFill, palette.dfdOtherBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("store", palette.dfdDatastoreFill, palette.dfdDatastoreBorder, { strokeWidth: 1.5 })}`,
` ${buildModelWeaveMermaidClassDef("external", palette.dfdExternalFill, palette.dfdExternalBorder, { strokeWidth: 1.5 })}`
);
}
const nodeIds = new Map<string, string>();
const domainStyles: string[] = [];
const flowDomains = getFlowDiagramDomains(diagram);
@ -573,12 +733,13 @@ function buildFlowDiagramMermaidSource(
nodeIds,
1,
colorScheme,
colorClasses,
domainStyles
);
}
for (const node of ungroupedNodes) {
appendFlowDiagramNode(lines, node, nodeIds, 1);
appendFlowDiagramNode(lines, node, nodeIds, 1, colorScheme, colorClasses);
}
for (const edge of diagram.edges) {
@ -596,6 +757,13 @@ function buildFlowDiagramMermaidSource(
}
}
if (colorClasses.size > 0) {
lines.push("");
for (const [className, style] of colorClasses) {
lines.push(` classDef ${className} ${formatMermaidClassDefStyle(style)}`);
}
}
if (domainStyles.length > 0) {
lines.push("", ...domainStyles);
}
@ -610,6 +778,7 @@ function appendFlowDiagramDomainSubgraph(
nodeIds: Map<string, string>,
depth: number,
colorScheme: ResolvedColorScheme | undefined,
colorClasses: Map<string, ResolvedColorStyle>,
domainStyles: string[]
): boolean {
const childLines: string[] = [];
@ -621,6 +790,7 @@ function appendFlowDiagramDomainSubgraph(
nodeIds,
depth + 1,
colorScheme,
colorClasses,
domainStyles
);
}
@ -635,7 +805,7 @@ function appendFlowDiagramDomainSubgraph(
lines.push(`${indent}subgraph ${domainMermaidId}["${buildFlowDomainLabel(domainNode.domain)}"]`);
lines.push(...childLines);
for (const node of nodes) {
appendFlowDiagramNode(lines, node, nodeIds, depth + 1);
appendFlowDiagramNode(lines, node, nodeIds, depth + 1, colorScheme, colorClasses);
}
lines.push(`${indent}end`);
@ -653,11 +823,15 @@ function appendFlowDiagramNode(
lines: string[],
node: DiagramNode,
nodeIds: Map<string, string>,
depth: number
depth: number,
colorScheme?: ResolvedColorScheme,
colorClasses?: Map<string, ResolvedColorStyle>
): void {
const mermaidId = toMermaidNodeId(node.id);
const shape = toFlowDiagramMermaidShape(node.kind);
const className = toFlowDiagramClassName(node.kind);
const className = colorScheme
? registerFlowDiagramColorClass(node.kind, colorScheme, colorClasses)
: toFlowDiagramClassName(node.kind);
const indent = " ".repeat(depth);
nodeIds.set(node.id, mermaidId);
lines.push(`${indent}${mermaidId}@{ shape: ${shape}, label: "${escapeMermaidLabel(node.label ?? node.ref ?? node.id)}" }`);
@ -695,7 +869,7 @@ function getFlowDomainColorKind(domain: DomainEntry): string {
export function getDfdMermaidColorSchemeTargets(diagram: ResolvedDiagram): string[] {
if (isFlowDiagramModel(diagram.diagram)) {
return hasNodesWithDomain(diagram) ? ["domain"] : [];
return hasNodesWithDomain(diagram) ? ["flow_diagram", "domain"] : ["flow_diagram"];
}
if (isDfdDiagramModel(diagram.diagram)) {
return hasNodesWithDomain(diagram) || (diagram.diagram.domains?.length ?? 0) > 0
@ -721,6 +895,13 @@ function toFlowDiagramMermaidShape(kind: unknown): string {
case "app_process":
case "context":
case "work_object":
case "actor":
case "user":
case "message":
case "data":
case "api":
case "service":
case "handler":
case "external":
case "unknown":
default:
@ -728,6 +909,24 @@ function toFlowDiagramMermaidShape(kind: unknown): string {
}
}
function registerFlowDiagramColorClass(
kind: unknown,
colorScheme: ResolvedColorScheme,
colorClasses: Map<string, ResolvedColorStyle> | undefined
): string {
const className = toFlowDiagramColorClassName(kind);
colorClasses?.set(
className,
resolveColorStyle(colorScheme, "flow_diagram", typeof kind === "string" ? kind : undefined)
);
return className;
}
function toFlowDiagramColorClassName(kind: unknown): string {
const suffix = typeof kind === "string" && kind.trim() ? kind.trim() : "default";
return `kind_flow_diagram_${sanitizeMermaidId(suffix)}`;
}
function toFlowDiagramClassName(kind: unknown): string {
switch (kind) {
case "screen":
@ -738,11 +937,18 @@ function toFlowDiagramClassName(kind: unknown): string {
return "store";
case "context":
case "work_object":
case "message":
return "context";
case "process":
case "app_process":
case "data":
case "api":
case "service":
case "handler":
return "process";
case "external":
case "actor":
case "user":
return "external";
case "unknown":
default:
@ -812,7 +1018,7 @@ function isDfdDiagramModel(diagram: ResolvedDiagram["diagram"]): diagram is DfdD
return diagram.schema === "dfd_diagram";
}
function isFlowDiagramModel(diagram: ResolvedDiagram["diagram"]): boolean {
function isFlowDiagramModel(diagram: ResolvedDiagram["diagram"]): diagram is FlowDiagramModel {
return diagram.schema === "flow_diagram";
}

View file

@ -17,8 +17,17 @@ import type { AppProcessBusinessFlowDirection } from "../core/app-process-busine
export type FileType = (typeof FILE_TYPES)[number];
export type DfdObjectKind = (typeof DFD_OBJECT_KINDS)[number];
export type DfdDiagramObjectKind = DfdObjectKind | "other";
export type FlowDiagramViewMode = "detail" | "screen";
export type FlowDiagramObjectKind =
| "screen"
| "actor"
| "user"
| "message"
| "data"
| "api"
| "service"
| "handler"
| "process"
| "app_process"
| "context"
@ -694,6 +703,7 @@ export interface FlowDiagramModel extends BaseFileModel<"flow-diagram"> {
name: string;
kind: "screen_communication";
description?: string;
flowView: FlowDiagramViewMode;
domainSources: DomainSourceRef[];
domainSourceSummaries?: DomainDiagramSourceSummary[];
domains?: DomainEntry[];

View file

@ -13,7 +13,7 @@ await build({
'export { isDiagramPreviewRouteFileType, isDfdLikeDiagramPreviewFileType } from "./src/core/preview-routing";',
'export { isModelWeavePreviewSupportedFileType } from "./src/core/supported-formats";',
'export { resolveDiagramRelations } from "./src/core/relation-resolver";',
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, getDfdMermaidColorSchemeTargets } from "./src/renderers/dfd-mermaid";',
'export { buildDfdMermaidSource, buildFlowDiagramHoverMetadata, buildFlowDiagramScreenFlowProjection, getDfdMermaidColorSchemeTargets, resolveFlowDiagramViewMode } from "./src/renderers/dfd-mermaid";',
'export { getAppliedColorSchemeRowsForTargets } from "./src/core/color-scheme";',
'export { buildCurrentDiagramDiagnostics } from "./src/core/current-file-diagnostics";',
'export { createModelWeaveTranslator } from "./src/i18n/messages";',
@ -82,6 +82,7 @@ const {
buildCurrentDiagramDiagnostics,
buildDfdMermaidSource,
buildFlowDiagramHoverMetadata,
buildFlowDiagramScreenFlowProjection,
createModelWeaveTranslator,
getAppliedColorSchemeLowerPaneSlot,
getAppliedColorSchemeRowsForTargets,
@ -95,7 +96,8 @@ const {
isDiagramPreviewRouteFileType,
isModelWeavePreviewSupportedFileType,
parseFlowDiagramFile,
resolveDiagramRelations
resolveDiagramRelations,
resolveFlowDiagramViewMode
} = await import(`../${outputFile}?t=${Date.now()}`);
globalThis.activeDocument = {
@ -162,6 +164,34 @@ test("flow_diagram is detected and parsed as a distinct model type", () => {
});
test("flow_diagram defaults flow_view to detail", () => {
const { file } = parseFlow();
assert.equal(file.flowView, "detail");
assert.equal(resolveFlowDiagramViewMode(file), "detail");
});
test("flow_diagram parses and resolves screen flow_view", () => {
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen");
const { file, warnings } = parseFlow(markdown);
assert.equal(file.flowView, "screen");
assert.equal(resolveFlowDiagramViewMode(file), "screen");
assert.equal(resolveFlowDiagramViewMode(file, "detail"), "detail");
assert.equal(warnings.some((warning) => warning.field === "flow_view"), false);
});
test("flow_diagram unknown flow_view warns and falls back to detail", () => {
const markdown = flowMarkdown.replace("kind: screen_communication", "kind: screen_communication\nflow_view: storyboard");
const { file, warnings } = parseFlow(markdown);
assert.equal(file.flowView, "detail");
assert.equal(resolveFlowDiagramViewMode(file), "detail");
assert.equal(
warnings.some((warning) => warning.field === "flow_view" && /unknown flow_view/.test(warning.message)),
true
);
});
test("flow_diagram follows the same preview support and diagram route checks", () => {
const fileType = detectFileType({ type: "flow_diagram" });
@ -258,12 +288,117 @@ test("flow_diagram Mermaid edge labels use trigger, data, and kind", () => {
assert.match(source, /mystery -->\|fallback \/ Unknown handoff\| order_screen/);
});
const screenFlowMarkdown = `---
type: flow_diagram
id: FLOW-CHECKOUT-SCREEN-FLOW
name: Checkout Screen Flow
kind: screen_communication
flow_view: screen
---
## Objects
| id | label | kind | ref | domain | notes |
|---|---|---|---|---|---|
| checkout_screen | Checkout Screen | screen | | frontend_area | Start screen |
| submit_action | Submit Action | work_object | | frontend_area | User action detail |
| checkout_process | Checkout Process | app_process | | application_area | Hidden submit process |
| completion_screen | Completion Screen | screen | | frontend_area | Success screen |
| error_message | Error Message | message | | frontend_area | User-visible error |
| session_store | Session Store | session | | data_area | Hidden session store |
| retry_process | Retry Process | process | | application_area | Alternate hidden route |
## Flows
| id | from | to | kind | trigger | data | condition | notes |
|---|---|---|---|---|---|---|---|
| SF01 | checkout_screen | submit_action | action | click:Submit | Checkout input | | Submit action |
| SF02 | submit_action | checkout_process | command | | Checkout request | | Invoke process |
| SF03 | checkout_process | completion_screen | navigate | showComplete | Completion payload | success | Show completion |
| SF04 | checkout_process | error_message | message | showError | Error details | failed | Show error |
| SF05 | checkout_process | session_store | store | save | Session data | | Hidden persistence |
| SF06 | checkout_screen | retry_process | action | click:Retry | Retry input | | Retry action |
| SF07 | retry_process | completion_screen | navigate | showComplete | Retry payload | alternate | Alternate success |
`;
function getFlowDiagramDataWarnings(markdown, extraFiles = []) {
const { model, resolved } = resolveFlow(markdown, extraFiles);
return buildCurrentDiagramDiagnostics({ diagram: model }, resolved.warnings)
.filter((warning) => warning.code === "unresolved-reference" && /flow data reference/.test(warning.message));
}
test("flow_diagram screen view projects internal paths into visible nodes", () => {
const { resolved } = resolveFlow(screenFlowMarkdown);
const projected = buildFlowDiagramScreenFlowProjection(resolved);
assert.deepEqual(projected.nodes.map((node) => node.id), [
"checkout_screen",
"completion_screen",
"error_message"
]);
assert.equal(projected.edges.length, 2);
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "session_store"), false);
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "completion_screen"), true);
assert.equal(projected.edges.some((edge) => edge.source === "checkout_screen" && edge.target === "error_message"), true);
});
test("flow_diagram screen view deduplicates projected edges and merges labels", () => {
const { resolved } = resolveFlow(screenFlowMarkdown);
const projected = buildFlowDiagramScreenFlowProjection(resolved);
const completionEdges = projected.edges.filter((edge) =>
edge.source === "checkout_screen" && edge.target === "completion_screen"
);
assert.equal(completionEdges.length, 1);
assert.match(completionEdges[0].label ?? "", /^(success \/ alternate|alternate \/ success)$/);
assert.equal(completionEdges[0].metadata.projected, true);
});
test("flow_diagram screen flow Mermaid hides collapsed nodes and uses projected labels", () => {
const { resolved } = resolveFlow(screenFlowMarkdown);
const source = buildDfdMermaidSource(resolved);
assert.match(source, /checkout_screen@\{ shape: curv-trap, label: "Checkout Screen" \}/);
assert.match(source, /completion_screen@\{ shape: curv-trap, label: "Completion Screen" \}/);
assert.match(source, /error_message@\{ shape: rect, label: "Error Message" \}/);
assert.doesNotMatch(source, /submit_action@/);
assert.doesNotMatch(source, /checkout_process@/);
assert.doesNotMatch(source, /session_store@/);
assert.match(source, /checkout_screen -->\|(success \/ alternate|alternate \/ success)\| completion_screen/);
assert.match(source, /checkout_screen -->\|failed\| error_message/);
assert.doesNotMatch(source, /Checkout input|Session data/);
});
test("flow_diagram screen view falls back to detail when no visible nodes exist", () => {
const markdown = `---
type: flow_diagram
id: FLOW-INTERNAL-ONLY
name: Internal Only Flow
kind: screen_communication
flow_view: screen
---
## Objects
| id | label | kind | ref | domain | notes |
|---|---|---|---|---|---|
| process_a | Process A | app_process | | application | Internal process |
| store_a | Store A | session | | data | Internal store |
## Flows
| id | from | to | kind | trigger | data | condition | notes |
|---|---|---|---|---|---|---|---|
| F01 | process_a | store_a | store | save | Session data | | Store state |
`;
const { resolved } = resolveFlow(markdown);
const projected = buildFlowDiagramScreenFlowProjection(resolved);
const source = buildDfdMermaidSource(resolved);
assert.deepEqual(projected.nodes.map((node) => node.id), ["process_a", "store_a"]);
assert.equal(projected.edges.length, 1);
assert.match(source, /process_a@\{ shape: rect, label: "Process A" \}/);
assert.match(source, /store_a@\{ shape: lin-cyl, label: "Store A" \}/);
assert.match(source, /process_a -->\|save \/ Session data\| store_a/);
});
test("flow_diagram Flows.data plain text does not warn", () => {
const warnings = getFlowDiagramDataWarnings(flowMarkdown.replace("[[DATA-ORDER-REQUEST]]", "DATA-ORDER-DRAFT"));
@ -768,6 +903,41 @@ test("flow_diagram Domain Sources resolve Domain name, kind, and parent for nest
assert.match(source, /style DOMAIN_application_area fill:#e9fbe9,stroke:#2f8f46,color:#123d1b/);
});
test("flow_diagram screen view preserves resolved Domain Sources for visible screens", () => {
const markdown = flowDomainSourceDiagramMarkdown
.replace("kind: screen_communication", "kind: screen_communication\nflow_view: screen")
.replace(
"| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |",
"| SESSION_STORE | Session Store | session | | data_area | Temporary persistence |\n| CONFIRM_SCREEN | Confirmation Screen | screen | | frontend_area | Completion screen |"
)
.replace(
"| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |",
"| F02 | ORDER_SUBMIT | SESSION_STORE | store | save | Session data | | Save session |\n| F03 | ORDER_SUBMIT | CONFIRM_SCREEN | navigate | showComplete | Confirmation data | success | Show completion |"
);
const { resolved } = resolveFlow(markdown, [
{ path: "DOMAINS-FLOW-TEST.md", content: flowDomainSourceMarkdown }
]);
const source = buildDfdMermaidSource(resolved, {
id: "COLOR-FLOW-DOMAIN-TEST",
name: "Flow Domain Test Colors",
entries: [
{ target: "domain", kind: "system", fill: "#f7f7f7", stroke: "#999999", text: "#222222", rowIndex: 0 },
{ target: "domain", kind: "ui", fill: "#e8f1ff", stroke: "#3b73d9", text: "#102a5c", rowIndex: 1 },
{ target: "domain", kind: "application", fill: "#e9fbe9", stroke: "#2f8f46", text: "#123d1b", rowIndex: 2 },
{ target: "domain", kind: "data", fill: "#f3e8ff", stroke: "#7c3bbd", text: "#2d124c", rowIndex: 3 }
],
defaultStyle: { fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111" }
});
assert.match(source, /subgraph DOMAIN_checkout_system\["Checkout System"\]/);
assert.match(source, /subgraph DOMAIN_frontend_area\["Frontend Area"\]/);
assert.match(source, /subgraph DOMAIN_checkout_system[\s\S]*subgraph DOMAIN_frontend_area[\s\S]*ORDER_ENTRY/);
assert.match(source, /subgraph DOMAIN_frontend_area[\s\S]*CONFIRM_SCREEN/);
assert.doesNotMatch(source, /ORDER_SUBMIT@/);
assert.doesNotMatch(source, /SESSION_STORE@/);
assert.match(source, /ORDER_ENTRY -->\|success\| CONFIRM_SCREEN/);
assert.match(source, /style DOMAIN_frontend_area fill:#e8f1ff,stroke:#3b73d9,color:#102a5c/);
});
test("flow_diagram local Domains resolve names, kinds, and nested parents", () => {
const markdown = flowDomainSourceDiagramMarkdown
.replace("## Domain Sources\n\n| ref | notes |\n|---|---|\n| [[DOMAINS-FLOW-TEST]] | test domains |\n\n", "")
@ -861,7 +1031,9 @@ const flowDomainColorScheme = {
name: "Flow Domain Colors",
entries: [
{ target: "domain", kind: "sales", fill: "#7ddea7", stroke: "#000000", text: "#ffffff", rowIndex: 0 },
{ target: "domain", kind: "application", fill: "#DDEBFF", stroke: "#4F81BD", text: "#111111", rowIndex: 1 }
{ target: "domain", kind: "application", fill: "#DDEBFF", stroke: "#4F81BD", text: "#111111", rowIndex: 1 },
{ target: "flow_diagram", kind: "screen", fill: "#fff4cc", stroke: "#bf9000", text: "#111111", rowIndex: 2 },
{ target: "flow_diagram", kind: "app_process", fill: "#e2f0d9", stroke: "#70ad47", text: "#111111", rowIndex: 3 }
],
defaultStyle: {
fill: "#f5f5f5",
@ -887,7 +1059,8 @@ test("flow_diagram Applied Color Scheme targets include used domain rows", () =>
const targets = getDfdMermaidColorSchemeTargets(resolved);
const rows = getAppliedColorSchemeRowsForTargets(flowDomainColorScheme, targets);
assert.deepEqual(targets, ["domain"]);
assert.deepEqual(targets, ["flow_diagram", "domain"]);
assert.equal(rows.some((row) => row.entry.target === "flow_diagram" && row.entry.kind === "screen"), true);
assert.equal(rows.some((row) => row.entry.target === "domain" && row.entry.kind === "sales"), true);
assert.equal(rows.some((row) => row.entry.target === "domain" && row.entry.kind === "application"), true);
assert.equal(rows.some((row) => row.entry.target === "app_process"), false);