diff --git a/docs/formats/FORMAT-app_process.md b/docs/formats/FORMAT-app_process.md index 3efd46d..d2f15ce 100644 --- a/docs/formats/FORMAT-app_process.md +++ b/docs/formats/FORMAT-app_process.md @@ -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=` 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=` 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 diff --git a/docs/ja/formats/FORMAT-app_process.md b/docs/ja/formats/FORMAT-app_process.md index 670f5b7..c7c88af 100644 --- a/docs/ja/formats/FORMAT-app_process.md +++ b/docs/ja/formats/FORMAT-app_process.md @@ -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=` を使って色付けされます。 +* ローカルDomainが外部Domainの `name`, `kind`, `parent` を上書きする場合、警告を出し、ローカル値を使います。 +* Color Scheme が有効な場合、解決されたDomain group は `target=domain` と `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を基本フローとして使う diff --git a/main.js b/main.js index 4710c97..a085d37 100644 --- a/main.js +++ b/main.js @@ -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, diff --git a/src/core/app-process-domain-resolver.ts b/src/core/app-process-domain-resolver.ts index a603f64..be7a202 100644 --- a/src/core/app-process-domain-resolver.ts +++ b/src/core/app-process-domain-resolver.ts @@ -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(); + 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 + }; +} diff --git a/src/core/current-file-diagnostics.ts b/src/core/current-file-diagnostics.ts index ae96228..3eb9288 100644 --- a/src/core/current-file-diagnostics.ts +++ b/src/core/current-file-diagnostics.ts @@ -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}" 内で見つかりません。`], diff --git a/src/core/domain-diagnostics.ts b/src/core/domain-diagnostics.ts index 3e5846c..a9a4792 100644 --- a/src/core/domain-diagnostics.ts +++ b/src/core/domain-diagnostics.ts @@ -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}.`; +} diff --git a/src/main.ts b/src/main.ts index 1a2c090..0c6e989 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, diff --git a/src/parsers/app-process-parser.ts b/src/parsers/app-process-parser.ts index a5a7cee..da47c7c 100644 --- a/src/parsers/app-process-parser.ts +++ b/src/parsers/app-process-parser.ts @@ -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 diff --git a/src/parsers/domains-parser.ts b/src/parsers/domains-parser.ts index a80af85..8013450 100644 --- a/src/parsers/domains-parser.ts +++ b/src/parsers/domains-parser.ts @@ -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), diff --git a/test/app-process-domain-placement.test.mjs b/test/app-process-domain-placement.test.mjs index af806b1..13b633d 100644 --- a/test/app-process-domain-placement.test.mjs +++ b/test/app-process-domain-placement.test.mjs @@ -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(