fix: merge app process domain sources for rendering

This commit is contained in:
nejimakibird 2026-06-11 17:39:21 +09:00
parent fb1477a0b1
commit da7f0acb54
10 changed files with 557 additions and 38 deletions

View file

@ -641,13 +641,15 @@ Behavior:
* If local `## Domains` is absent, `Steps.domain` still groups Business Flow steps, but Model Weave does not warn about unknown domain values unless `## Domain Sources` is present.
* If local `## Domains` is present, `Steps.domain` is resolved by local Domain `id`.
* Resolved local Domains are rendered as nested Business Flow groups using `Domains.parent`.
* If `## Domain Sources` is also present, external Domains are loaded first and local `## Domains` override external definitions for the same `id`.
* Resolved Domains are rendered as nested Business Flow groups using `Domains.parent`.
* Domain `name` is used as the visible group label. If `name` is empty, `id` is used.
* Domains that have no direct steps are still rendered when they are ancestors of a Domain that has steps.
* Duplicate local Domain ids produce diagnostics.
* Local Domain `parent` values must reference another local Domain id.
* Local Domain `parent` values must reference another resolved Domain id. With `## Domain Sources`, this may be an imported external Domain id.
* If a local Domain has an unknown parent, it is rendered as a root-level group and the parent diagnostic remains visible.
* When a Color Scheme is active, resolved local Domain groups use `target=domain` and `kind=<Domains.kind>` for group colors.
* If a local Domain overrides an external Domain `name`, `kind`, or `parent`, Model Weave reports a warning and uses the local value.
* When a Color Scheme is active, resolved Domain groups use `target=domain` and `kind=<Domains.kind>` for group colors.
* If `Steps.domain` is present but not found, Model Weave does not fall back to `Steps.lane`.
* `Steps.lane` remains legacy-compatible layout-only placement and is only used when `Steps.domain` is empty.
@ -670,10 +672,10 @@ Behavior:
* If `## Domain Sources` is present, sources are loaded in table order and merged using the same Domain Source behavior as `domain_diagram` where practical.
* If a source cannot be resolved, or resolves to a non-`domains` file, Model Weave reports a warning.
* If `Steps.domain` does not match any merged Domain id, Model Weave reports a warning.
* Domain Sources currently validate placement metadata, but Business Flow hierarchy rendering and Domain group coloring are driven by local `## Domains`.
* If local `## Domains` is also present, local rows are applied after external sources and win for the same Domain `id`.
* The merged Domain set drives Business Flow hierarchy rendering and Domain group coloring.
* If `Steps.domain` is present but unresolved, Model Weave does not fall back to `Steps.lane`.
* `Steps.lane` remains legacy-compatible layout-only placement and is only used when `Steps.domain` is empty.
* Domain group coloring is applied only for resolved local `## Domains`. External Domain Source group coloring is planned for a later step.
* Business Flow step node colors continue to use `target=app_process` and `Steps.kind`.
#### Steps as the default flow

View file

@ -641,13 +641,15 @@ Business Flow preview には table-based steps を使います。
* ローカル `## Domains` がない場合でも、`Steps.domain` は Business Flow のStep groupingに使われます。ただし `## Domain Sources` がない限り、不明なdomain値の警告は出しません。
* ローカル `## Domains` がある場合、`Steps.domain` はローカルDomain `id` で解決されます。
* 解決されたローカルDomainは、`Domains.parent` を使って入れ子のBusiness Flow groupとして描画されます。
* `## Domain Sources` もある場合、外部Domainsを先に読み込み、同じ `id` の定義はローカル `## Domains` が上書きします。
* 解決されたDomainは、`Domains.parent` を使って入れ子のBusiness Flow groupとして描画されます。
* 表示されるgroup labelにはDomain `name` が使われます。`name` が空の場合は `id` が使われます。
* 直接Stepを持たないDomainでも、Stepを持つ子Domainの祖先であれば描画されます。
* 重複したローカルDomain idは診断になります。
* ローカルDomainの `parent` は、別のローカルDomain idを参照する必要があります。
* ローカルDomainの `parent` は、解決済みDomain idを参照する必要があります。`## Domain Sources` がある場合は、importされた外部Domain idも参照できます。
* ローカルDomainの `parent` が不明な場合、そのDomainはroot-level groupとして描画され、parent診断は表示され続けます。
* Color Scheme が有効な場合、解決されたローカルDomain group は `target=domain``kind=<Domains.kind>` を使って色付けされます。
* ローカルDomainが外部Domainの `name`, `kind`, `parent` を上書きする場合、警告を出し、ローカル値を使います。
* Color Scheme が有効な場合、解決されたDomain group は `target=domain``kind=<Domains.kind>` を使って色付けされます。
* `Steps.domain` が存在して解決できない場合でも、`Steps.lane` へはfallbackしません。
* `Steps.lane` は legacy-compatible な layout-only 配置であり、`Steps.domain` が空の場合だけ使われます。
@ -670,10 +672,10 @@ Business Flow preview には table-based steps を使います。
* `## Domain Sources` がある場合、sourceはテーブル順に読み込まれ、可能な範囲で `domain_diagram` と同じDomain Source動作でマージされます。
* sourceが解決できない場合、または `domains` 以外のファイルを指す場合、警告が出ます。
* `Steps.domain` がマージ済みDomain idに一致しない場合、警告が出ます。
* Domain Sources は配置metadataの検証に使われますが、Business Flow hierarchy rendering と Domain group coloring はローカル `## Domains` によって行われます。
* ローカル `## Domains` もある場合、ローカル行は外部sourceの後に適用され、同じDomain `id` ではローカル定義が優先されます。
* マージ済みDomain setが Business Flow hierarchy rendering と Domain group coloring に使われます。
* `Steps.domain` が存在して解決できない場合でも、`Steps.lane` へはfallbackしません。
* `Steps.lane` は legacy-compatible な layout-only 配置であり、`Steps.domain` が空の場合だけ使われます。
* Domain group coloring は解決されたローカル `## Domains` にのみ適用されます。外部 Domain Source に基づく group coloring は後続stepです。
* Business Flow のStep node色は引き続き `target=app_process``Steps.kind` を使います。
#### Stepsを基本フローとして使う

111
main.js
View file

@ -249,6 +249,9 @@ function formatAppProcessUnknownDomainMessage(stepId, domainId) {
function formatAppProcessUnknownLocalDomainMessage(stepId, domainId) {
return `app_process Step "${stepId}" references unknown local Domain "${domainId}".`;
}
function formatAppProcessLocalDomainFieldOverrideMessage(id, field) {
return `app_process local Domain "${id}" overrides external Domain ${field}.`;
}
// src/core/reference-resolver.ts
function normalizeReferenceTarget(reference) {
@ -824,6 +827,7 @@ function createMergeWarning(path2, message, severity) {
}
// src/core/app-process-domain-resolver.ts
var APP_PROCESS_DOMAIN_CONFLICT_FIELDS = ["parent", "kind", "name"];
function resolveAppProcessDomainPlacement(process, index) {
const localDomains = process.domains ?? [];
const hasLocalDomains = localDomains.length > 0;
@ -843,13 +847,21 @@ function resolveAppProcessDomainPlacement(process, index) {
conflicts: [],
warnings: []
};
const domainsById = new Map(localDomains.map((domain) => [domain.id, domain]));
for (const domain of resolvedSources.domains) {
if (!domainsById.has(domain.id)) {
domainsById.set(domain.id, domain);
}
}
const warnings = [...resolvedSources.warnings];
const mergeResult = mergeAppProcessDomains(
resolvedSources.domains,
localDomains,
process.path
);
const domainsById = new Map(mergeResult.domains.map((domain) => [domain.id, domain]));
const warnings = [
...resolvedSources.warnings,
...mergeResult.warnings,
...validateMergedAppProcessDomainParents(process.path, mergeResult.domains)
];
const conflicts = [
...resolvedSources.conflicts,
...mergeResult.conflicts
];
const placements = (process.steps ?? []).map((step) => {
const domainId = step.domain?.trim() ?? "";
if (!domainId) {
@ -859,7 +871,7 @@ function resolveAppProcessDomainPlacement(process, index) {
if (!domain) {
warnings.push({
code: "unresolved-reference",
message: hasLocalDomains ? formatAppProcessUnknownLocalDomainMessage(step.id, domainId) : formatAppProcessUnknownDomainMessage(step.id, domainId),
message: hasLocalDomains && process.domainSources.length === 0 ? formatAppProcessUnknownLocalDomainMessage(step.id, domainId) : formatAppProcessUnknownDomainMessage(step.id, domainId),
severity: "warning",
path: process.path,
field: "Steps.domain",
@ -879,13 +891,81 @@ function resolveAppProcessDomainPlacement(process, index) {
);
return {
process,
domains: [...domainsById.values()],
domains: mergeResult.domains,
sourceSummaries: resolvedSources.sourceSummaries,
conflicts: resolvedSources.conflicts,
conflicts,
placements,
warnings
};
}
function validateMergedAppProcessDomainParents(processPath, domains) {
const domainIds = new Set(domains.map((domain) => domain.id));
const warnings = [];
for (const domain of domains) {
if (!domain.parent || domain.parent === domain.id || domainIds.has(domain.parent)) {
continue;
}
warnings.push({
code: "unresolved-reference",
message: formatDomainParentUnknownMessage(domain.parent),
severity: "warning",
path: processPath,
field: "Domains.parent",
context: { rowIndex: domain.rowIndex + 1 }
});
}
return warnings;
}
function mergeAppProcessDomains(externalDomains, localDomains, processPath) {
const domainsById = /* @__PURE__ */ new Map();
const order = [];
const conflicts = [];
const warnings = [];
for (const domain of externalDomains) {
if (!domainsById.has(domain.id)) {
order.push(domain.id);
}
domainsById.set(domain.id, { ...domain });
}
for (const localDomain of localDomains) {
const externalDomain = domainsById.get(localDomain.id);
if (!externalDomain) {
order.push(localDomain.id);
domainsById.set(localDomain.id, { ...localDomain });
continue;
}
for (const field of APP_PROCESS_DOMAIN_CONFLICT_FIELDS) {
const externalValue = externalDomain[field]?.trim() ?? "";
const localValue = localDomain[field]?.trim() ?? "";
if (externalValue === localValue) {
continue;
}
conflicts.push({
domainId: localDomain.id,
field,
earlierSourcePath: "Domain Sources",
laterSourcePath: processPath,
earlierValue: externalValue,
laterValue: localValue,
effectiveSourcePath: processPath,
severity: "warning"
});
warnings.push({
code: "invalid-structure",
message: formatAppProcessLocalDomainFieldOverrideMessage(localDomain.id, field),
severity: "warning",
path: processPath,
field: "Domains"
});
}
domainsById.set(localDomain.id, { ...localDomain });
}
return {
domains: order.map((id) => domainsById.get(id)).filter((domain) => Boolean(domain)),
conflicts,
warnings
};
}
// src/core/color-scheme.ts
var BUILT_IN_COLOR_ENTRIES = [
@ -2204,6 +2284,7 @@ function localizeDiagnosticMessage(message, language) {
[/^Step "([^"]+)" has both domain and lane\. domain is used and lane is ignored\.$/, (_match, step) => `Step "${step}" \u306B\u306F domain \u3068 lane \u306E\u4E21\u65B9\u304C\u3042\u308A\u307E\u3059\u3002domain \u304C\u4F7F\u308F\u308C\u3001lane \u306F\u7121\u8996\u3055\u308C\u307E\u3059\u3002`],
[/^app_process Step "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, step, domain) => `app_process Step "${step}" \u304C\u672A\u5B9A\u7FA9\u306E Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
[/^app_process Step "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, step, domain) => `app_process Step "${step}" \u304C\u672A\u5B9A\u7FA9\u306E\u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u3092\u53C2\u7167\u3057\u3066\u3044\u307E\u3059\u3002`],
[/^app_process local Domain "([^"]+)" overrides external Domain (name|kind|parent)\.$/, (_match, domain, field) => `app_process \u30ED\u30FC\u30AB\u30EB Domain "${domain}" \u304C\u5916\u90E8 Domain \u306E ${field} \u3092\u4E0A\u66F8\u304D\u3057\u3066\u3044\u307E\u3059\u3002`],
[/^(.+) "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_match, target, value) => `${target} "${value}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002ID\u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
[/^unresolved (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" \u306E\u53C2\u7167\u5148\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002ID\u307E\u305F\u306F\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`],
[/^unresolved member ref: (.+) in (.+)$/, (_match, member, owner) => `member ref "${member}" \u304C "${owner}" \u5185\u3067\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002`],
@ -9938,7 +10019,7 @@ function parseDomainEntries(lines, path2) {
});
return { rows, warnings };
}
function validateDomainEntries(path2, domains) {
function validateDomainEntries(path2, domains, options = {}) {
const warnings = [];
const domainIds = new Set(domains.map((domain) => domain.id));
for (const domain of domains) {
@ -9956,7 +10037,7 @@ function validateDomainEntries(path2, domains) {
});
continue;
}
if (!domainIds.has(domain.parent)) {
if (!domainIds.has(domain.parent) && !options.skipUnknownParents) {
warnings.push({
code: "unresolved-reference",
message: formatDomainParentUnknownMessage(domain.parent),
@ -10780,7 +10861,9 @@ function parseAppProcessFile(markdown, path2) {
...triggersTable.warnings,
...transitionsTable.warnings,
...domainsTable.warnings,
...validateDomainEntries(path2, domainsTable.rows),
...validateDomainEntries(path2, domainsTable.rows, {
skipUnknownParents: domainSourcesTable.rows.length > 0
}),
...stepsTable.warnings,
...flowsTable.warnings,
...domainSourcesTable.warnings
@ -21294,7 +21377,7 @@ var ModelWeavePlugin = class extends import_obsidian8.Plugin {
steps: model.steps ?? [],
flows: model.flows ?? [],
hasExplicitFlows: Boolean(model.hasExplicitFlows),
domains: model.domains
domains: domainPlacement.domains.length > 0 ? domainPlacement.domains : model.domains
} : void 0,
colorScheme: colorSchemeResult.colorScheme,
warnings: diagnostics,

View file

@ -1,4 +1,6 @@
import {
formatDomainParentUnknownMessage,
formatAppProcessLocalDomainFieldOverrideMessage,
formatAppProcessUnknownDomainMessage,
formatAppProcessUnknownLocalDomainMessage
} from "./domain-diagnostics";
@ -6,10 +8,14 @@ import { resolveDomainSources } from "./domain-diagram-resolver";
import type { ModelingVaultIndex } from "./vault-index";
import type {
AppProcessModel,
DomainEntry,
DomainMergeConflict,
ResolvedAppProcessDomainPlacement,
ValidationWarning
} from "../types/models";
const APP_PROCESS_DOMAIN_CONFLICT_FIELDS = ["parent", "kind", "name"] as const;
export function resolveAppProcessDomainPlacement(
process: AppProcessModel,
index: ModelingVaultIndex
@ -35,13 +41,21 @@ export function resolveAppProcessDomainPlacement(
conflicts: [],
warnings: []
};
const domainsById = new Map(localDomains.map((domain) => [domain.id, domain]));
for (const domain of resolvedSources.domains) {
if (!domainsById.has(domain.id)) {
domainsById.set(domain.id, domain);
}
}
const warnings: ValidationWarning[] = [...resolvedSources.warnings];
const mergeResult = mergeAppProcessDomains(
resolvedSources.domains,
localDomains,
process.path
);
const domainsById = new Map(mergeResult.domains.map((domain) => [domain.id, domain]));
const warnings: ValidationWarning[] = [
...resolvedSources.warnings,
...mergeResult.warnings,
...validateMergedAppProcessDomainParents(process.path, mergeResult.domains)
];
const conflicts = [
...resolvedSources.conflicts,
...mergeResult.conflicts
];
const placements = (process.steps ?? [])
.map((step) => {
const domainId = step.domain?.trim() ?? "";
@ -53,7 +67,7 @@ export function resolveAppProcessDomainPlacement(
if (!domain) {
warnings.push({
code: "unresolved-reference",
message: hasLocalDomains
message: hasLocalDomains && process.domainSources.length === 0
? formatAppProcessUnknownLocalDomainMessage(step.id, domainId)
: formatAppProcessUnknownDomainMessage(step.id, domainId),
severity: "warning",
@ -78,10 +92,102 @@ export function resolveAppProcessDomainPlacement(
return {
process,
domains: [...domainsById.values()],
domains: mergeResult.domains,
sourceSummaries: resolvedSources.sourceSummaries,
conflicts: resolvedSources.conflicts,
conflicts,
placements,
warnings
};
}
function validateMergedAppProcessDomainParents(
processPath: string,
domains: DomainEntry[]
): ValidationWarning[] {
const domainIds = new Set(domains.map((domain) => domain.id));
const warnings: ValidationWarning[] = [];
for (const domain of domains) {
if (!domain.parent || domain.parent === domain.id || domainIds.has(domain.parent)) {
continue;
}
warnings.push({
code: "unresolved-reference",
message: formatDomainParentUnknownMessage(domain.parent),
severity: "warning",
path: processPath,
field: "Domains.parent",
context: { rowIndex: domain.rowIndex + 1 }
});
}
return warnings;
}
function mergeAppProcessDomains(
externalDomains: DomainEntry[],
localDomains: DomainEntry[],
processPath: string
): {
domains: DomainEntry[];
conflicts: DomainMergeConflict[];
warnings: ValidationWarning[];
} {
const domainsById = new Map<string, DomainEntry>();
const order: string[] = [];
const conflicts: DomainMergeConflict[] = [];
const warnings: ValidationWarning[] = [];
for (const domain of externalDomains) {
if (!domainsById.has(domain.id)) {
order.push(domain.id);
}
domainsById.set(domain.id, { ...domain });
}
for (const localDomain of localDomains) {
const externalDomain = domainsById.get(localDomain.id);
if (!externalDomain) {
order.push(localDomain.id);
domainsById.set(localDomain.id, { ...localDomain });
continue;
}
for (const field of APP_PROCESS_DOMAIN_CONFLICT_FIELDS) {
const externalValue = externalDomain[field]?.trim() ?? "";
const localValue = localDomain[field]?.trim() ?? "";
if (externalValue === localValue) {
continue;
}
conflicts.push({
domainId: localDomain.id,
field,
earlierSourcePath: "Domain Sources",
laterSourcePath: processPath,
earlierValue: externalValue,
laterValue: localValue,
effectiveSourcePath: processPath,
severity: "warning"
});
warnings.push({
code: "invalid-structure",
message: formatAppProcessLocalDomainFieldOverrideMessage(localDomain.id, field),
severity: "warning",
path: processPath,
field: "Domains"
});
}
domainsById.set(localDomain.id, { ...localDomain });
}
return {
domains: order
.map((id) => domainsById.get(id))
.filter((domain): domain is DomainEntry => Boolean(domain)),
conflicts,
warnings
};
}

View file

@ -1204,6 +1204,7 @@ export function localizeDiagnosticMessage(message: string, language?: string): s
[/^Step "([^"]+)" has both domain and lane\. domain is used and lane is ignored\.$/, (_match, step) => `Step "${step}" には domain と lane の両方があります。domain が使われ、lane は無視されます。`],
[/^app_process Step "([^"]+)" references unknown Domain "([^"]+)"\.$/, (_match, step, domain) => `app_process Step "${step}" が未定義の Domain "${domain}" を参照しています。`],
[/^app_process Step "([^"]+)" references unknown local Domain "([^"]+)"\.$/, (_match, step, domain) => `app_process Step "${step}" が未定義のローカル Domain "${domain}" を参照しています。`],
[/^app_process local Domain "([^"]+)" overrides external Domain (name|kind|parent)\.$/, (_match, domain, field) => `app_process ローカル Domain "${domain}" が外部 Domain の ${field} を上書きしています。`],
[/^(.+) "([^"]+)" could not be resolved\. Check the ID or file name\.$/, (_match, target, value) => `${target} "${value}" の参照先が見つかりません。IDまたはファイル名を確認してください。`],
[/^unresolved (.+) "([^"]+)"$/, (_match, target, value) => `${target} "${value}" の参照先が見つかりません。IDまたはファイル名を確認してください。`],
[/^unresolved member ref: (.+) in (.+)$/, (_match, member, owner) => `member ref "${member}" が "${owner}" 内で見つかりません。`],

View file

@ -109,3 +109,10 @@ export function formatAppProcessUnknownLocalDomainMessage(
): string {
return `app_process Step "${stepId}" references unknown local Domain "${domainId}".`;
}
export function formatAppProcessLocalDomainFieldOverrideMessage(
id: string,
field: "name" | "kind" | "parent"
): string {
return `app_process local Domain "${id}" overrides external Domain ${field}.`;
}

View file

@ -1332,7 +1332,9 @@ export default class ModelWeavePlugin extends Plugin {
steps: model.steps ?? [],
flows: model.flows ?? [],
hasExplicitFlows: Boolean(model.hasExplicitFlows),
domains: model.domains
domains: domainPlacement.domains.length > 0
? domainPlacement.domains
: model.domains
}
: undefined,
colorScheme: colorSchemeResult.colorScheme,

View file

@ -107,7 +107,9 @@ export function parseAppProcessFile(
...triggersTable.warnings,
...transitionsTable.warnings,
...domainsTable.warnings,
...validateDomainEntries(path, domainsTable.rows),
...validateDomainEntries(path, domainsTable.rows, {
skipUnknownParents: domainSourcesTable.rows.length > 0
}),
...stepsTable.warnings,
...flowsTable.warnings,
...domainSourcesTable.warnings

View file

@ -123,7 +123,8 @@ export function parseDomainEntries(
export function validateDomainEntries(
path: string,
domains: DomainEntry[]
domains: DomainEntry[],
options: { skipUnknownParents?: boolean } = {}
): ValidationWarning[] {
const warnings: ValidationWarning[] = [];
const domainIds = new Set(domains.map((domain) => domain.id));
@ -145,7 +146,7 @@ export function validateDomainEntries(
continue;
}
if (!domainIds.has(domain.parent)) {
if (!domainIds.has(domain.parent) && !options.skipUnknownParents) {
warnings.push({
code: "unresolved-reference",
message: formatDomainParentUnknownMessage(domain.parent),

View file

@ -699,9 +699,322 @@ test("app_process Domain Sources resolve known step domains", () => {
assert.equal(resolved.placements[0].status, "resolved");
assert.equal(resolved.placements[0].domain.name, "Fulfillment");
assert.equal(resolved.placements[0].domain.kind, "business_domain");
assert.equal(resolved.domains[0].id, "fulfillment");
assert.equal(resolved.warnings.length, 0);
});
test("app_process Domain Sources render merged hierarchy", () => {
const { index, model } = buildIndexWithProcess(
processMarkdown(
domainStepsHeader,
[
"| receive | office | Receive order | process | | | | | | external child |",
"| open | wms | Open WMS | screen | | | | | | external grandchild |"
].join("\n"),
[
"## Domain Sources",
"",
"| ref |",
"|---|",
"| [[DOMAINS-COMPANY]] |"
].join("\n")
),
[
{
path: "DOMAINS-COMPANY.md",
content: domainsMarkdown([
"| warehouse | Warehouse | operations | | Warehouse area |",
"| office | Office | organization | warehouse | Office area |",
"| wms | WMS | system | office | Warehouse management system |"
].join("\n"))
}
]
);
const resolved = resolveAppProcessDomainPlacement(model, index);
const source = buildAppProcessBusinessFlowMermaidSource({
title: model.name,
hasExplicitFlows: false,
domains: resolved.domains,
steps: model.steps,
flows: []
});
assert.equal(resolved.warnings.length, 0);
assert.match(source, /subgraph domain_warehouse\["Warehouse"\]/);
assert.match(source, /subgraph domain_office\["Office"\]/);
assert.match(source, /subgraph domain_wms\["WMS"\]/);
assert.match(source, /S1\["Receive order"\]/);
assert.match(source, /S2\[\/"Open WMS"\/\]/);
});
test("app_process mixed local and external Domain Sources render full Business Flow hierarchy", () => {
const processContent = `---
type: app_process
id: PROC-DOMAIN-PLACEMENT
name: Domain Placement
---
# Domain Placement
## Domain Sources
| ref |
|---|
| [[DOMAINS-COMPANY]] |
${localDomainsSection([
"| office | 事務所 | organization | warehouse | local definition override |",
"| qa | 検品 | operations | floor | local child of external floor |"
].join("\n"))}
## Steps
${domainStepsHeader}
| start | office | 在庫照会開始 | start | | | | | | |
| open | wms | 在庫照会画面を開く | screen | | | | | | |
| search | wms | 在庫検索 | input | | | | | | |
| judge | office | 在庫有無を判定 | decision | | | | | | |
| available | floor | 在庫ありの場合受注 | subflow | | | | | | |
| inspect | qa | 検品対象を確認 | process | | | | | | local Domainsで追加した領域 |
| unavailable | external | 在庫なしの場合は連絡 | subflow | | | | | | |
| end | | 終了 | end | | | | | | |
## Flows
| from | to | condition | label | notes |
|---|---|---|---|---|
| start | open | | | |
| open | search | | | |
| search | judge | | | |
| judge | available | | 在庫あり | |
| judge | unavailable | | 在庫なし | |
| available | inspect | | | |
| inspect | end | | | |
| unavailable | end | | | |
`;
const parsed = parseAppProcessFile(processContent, "PROC-DOMAIN-PLACEMENT.md");
assert.ok(parsed.file);
assert.equal(
parsed.warnings.some((warning) =>
warning.message === 'Domain parent "floor" is not defined.'
),
false
);
const { index, model } = buildIndexWithProcess(
processContent,
[
{
path: "DOMAINS-COMPANY.md",
content: domainsMarkdown([
"| warehouse | 倉庫 | operations | | |",
"| office | 事務所 | organization | warehouse | |",
"| wms | WMS | system | office | |",
"| floor | フロア | location | warehouse | |",
"| external | 外部 | external | | |"
].join("\n"))
}
]
);
const resolved = resolveAppProcessDomainPlacement(model, index);
const scheme = {
id: "test",
name: "Test",
entries: [
{ target: "domain", kind: "operations", fill: "#111111", stroke: "#222222", text: "#ffffff", rowIndex: 0 },
{ target: "domain", kind: "organization", fill: "#333333", stroke: "#444444", text: "#eeeeee", rowIndex: 1 },
{ target: "domain", kind: "system", fill: "#555555", stroke: "#666666", text: "#ffffff", rowIndex: 2 },
{ target: "domain", kind: "location", fill: "#777777", stroke: "#888888", text: "#ffffff", rowIndex: 3 },
{ target: "domain", kind: "external", fill: "#999999", stroke: "#aaaaaa", text: "#111111", rowIndex: 4 },
{ target: "app_process", kind: "process", fill: "#e8f5e9", stroke: "#388e3c", text: "#111111", rowIndex: 5 },
{ kind: "default", fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111", rowIndex: 6 }
],
defaultStyle: {
fill: "#f5f5f5",
stroke: "#9e9e9e",
text: "#111111"
}
};
const source = buildAppProcessBusinessFlowMermaidSource({
title: model.name,
hasExplicitFlows: Boolean(model.hasExplicitFlows),
domains: resolved.domains,
steps: model.steps,
flows: model.flows
}, scheme);
assert.equal(
resolved.warnings.some((warning) =>
warning.message === 'Domain parent "floor" is not defined.'
),
false
);
assert.equal(resolved.placements.filter((placement) => placement.status === "resolved").length, 7);
assert.match(source, /subgraph domain_warehouse\["倉庫"\]/);
assert.match(source, /subgraph domain_office\["事務所"\]/);
assert.match(source, /subgraph domain_wms\["WMS"\]/);
assert.match(source, /subgraph domain_floor\["フロア"\]/);
assert.match(source, /subgraph domain_qa\["検品"\]/);
assert.match(source, /subgraph domain_external\["外部"\]/);
assert.ok(source.indexOf('subgraph domain_wms["WMS"]') > source.indexOf('subgraph domain_office["事務所"]'));
assert.ok(source.indexOf('subgraph domain_qa["検品"]') > source.indexOf('subgraph domain_floor["フロア"]'));
assert.match(source, /style domain_warehouse fill:#111111,stroke:#222222,color:#ffffff/);
assert.match(source, /style domain_wms fill:#555555,stroke:#666666,color:#ffffff/);
assert.match(source, /style domain_floor fill:#777777,stroke:#888888,color:#ffffff/);
assert.match(source, /style domain_external fill:#999999,stroke:#aaaaaa,color:#111111/);
assert.match(source, /classDef kind_app_process_process fill:#e8f5e9,stroke:#388e3c,color:#111111/);
assert.match(source, /S4 -->\|"在庫あり"\| S5/);
assert.match(source, /S4 -->\|"在庫なし"\| S7/);
});
test("app_process Domain group Color Scheme uses merged external Domain kind", () => {
const { index, model } = buildIndexWithProcess(
processMarkdown(
domainStepsHeader,
"| receive | fulfillment | Receive order | process | | | | | | external color |",
[
"## Domain Sources",
"",
"| ref |",
"|---|",
"| [[DOMAINS-COMPANY]] |"
].join("\n")
),
[
{
path: "DOMAINS-COMPANY.md",
content: domainsMarkdown(
"| fulfillment | Fulfillment | business_domain | | Fulfillment process area |"
)
}
]
);
const resolved = resolveAppProcessDomainPlacement(model, index);
const scheme = {
id: "test",
name: "Test",
entries: [
{ target: "domain", kind: "business_domain", fill: "#123456", stroke: "#234567", text: "#ffffff", rowIndex: 0 },
{ target: "app_process", kind: "process", fill: "#e8f5e9", stroke: "#388e3c", text: "#111111", rowIndex: 1 },
{ kind: "default", fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111", rowIndex: 2 }
],
defaultStyle: {
fill: "#f5f5f5",
stroke: "#9e9e9e",
text: "#111111"
}
};
const source = buildAppProcessBusinessFlowMermaidSource({
title: model.name,
hasExplicitFlows: false,
domains: resolved.domains,
steps: model.steps,
flows: []
}, scheme);
assert.match(source, /style domain_fulfillment fill:#123456,stroke:#234567,color:#ffffff/);
assert.match(source, /classDef kind_app_process_process fill:#e8f5e9,stroke:#388e3c,color:#111111/);
});
test("app_process local Domains override external source definitions", () => {
const { index, model } = buildIndexWithProcess(
processMarkdown(
domainStepsHeader,
"| receive | fulfillment | Receive order | process | | | | | | local override |",
[
"## Domain Sources",
"",
"| ref |",
"|---|",
"| [[DOMAINS-COMPANY]] |",
"",
localDomainsSection(
"| fulfillment | Local Fulfillment | local_kind | local_parent | Local definition |"
)
].join("\n")
),
[
{
path: "DOMAINS-COMPANY.md",
content: domainsMarkdown(
"| fulfillment | External Fulfillment | external_kind | external_parent | External definition |"
)
}
]
);
const resolved = resolveAppProcessDomainPlacement(model, index);
const domain = resolved.placements[0].domain;
const messages = resolved.warnings.map((warning) => warning.message);
assert.equal(domain.name, "Local Fulfillment");
assert.equal(domain.kind, "local_kind");
assert.equal(domain.parent, "local_parent");
assert.ok(messages.includes('app_process local Domain "fulfillment" overrides external Domain name.'));
assert.ok(messages.includes('app_process local Domain "fulfillment" overrides external Domain kind.'));
assert.ok(messages.includes('app_process local Domain "fulfillment" overrides external Domain parent.'));
assert.equal(resolved.conflicts.length, 3);
assert.equal(
localizeDiagnosticMessage(messages[0], "ja"),
'app_process ローカル Domain "fulfillment" が外部 Domain の parent を上書きしています。'
);
});
test("app_process local override controls rendered Domain color", () => {
const { index, model } = buildIndexWithProcess(
processMarkdown(
domainStepsHeader,
"| receive | fulfillment | Receive order | process | | | | | | local color override |",
[
"## Domain Sources",
"",
"| ref |",
"|---|",
"| [[DOMAINS-COMPANY]] |",
"",
localDomainsSection(
"| fulfillment | Local Fulfillment | local_kind | | Local definition |"
)
].join("\n")
),
[
{
path: "DOMAINS-COMPANY.md",
content: domainsMarkdown(
"| fulfillment | External Fulfillment | external_kind | | External definition |"
)
}
]
);
const resolved = resolveAppProcessDomainPlacement(model, index);
const scheme = {
id: "test",
name: "Test",
entries: [
{ target: "domain", kind: "external_kind", fill: "#000000", stroke: "#000000", text: "#ffffff", rowIndex: 0 },
{ target: "domain", kind: "local_kind", fill: "#abcdef", stroke: "#123456", text: "#111111", rowIndex: 1 },
{ kind: "default", fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111", rowIndex: 2 }
],
defaultStyle: {
fill: "#f5f5f5",
stroke: "#9e9e9e",
text: "#111111"
}
};
const source = buildAppProcessBusinessFlowMermaidSource({
title: model.name,
hasExplicitFlows: false,
domains: resolved.domains,
steps: model.steps,
flows: []
}, scheme);
assert.match(source, /subgraph domain_fulfillment\["Local Fulfillment"\]/);
assert.match(source, /style domain_fulfillment fill:#abcdef,stroke:#123456,color:#111111/);
assert.doesNotMatch(source, /fill:#000000,stroke:#000000,color:#ffffff/);
});
test("app_process unknown step domain warns when Domain Sources are present", () => {
const { index, model } = buildIndexWithProcess(
processMarkdown(