diff --git a/main.js b/main.js index 93c5127..7d14f86 100644 --- a/main.js +++ b/main.js @@ -12890,6 +12890,7 @@ function validateStandaloneDomains(index, warnings) { }); } } + validateStandaloneDomainParents(entriesByDomainId, warnings); for (const entries of entriesByDomainId.values()) { if (entries.length < 2) { continue; @@ -12918,6 +12919,24 @@ function validateStandaloneDomains(index, warnings) { compareStandaloneDomainFields(sortedEntries, warnings); } } +function validateStandaloneDomainParents(entriesByDomainId, warnings) { + for (const entries of entriesByDomainId.values()) { + for (const entry of entries) { + const parent = entry.domain.parent?.trim(); + if (!parent || parent === entry.domain.id || entriesByDomainId.has(parent)) { + continue; + } + warnings.push({ + code: "unresolved-reference", + message: formatDomainParentUnknownMessage(parent), + severity: "warning", + path: entry.path, + field: "Domains.parent", + context: { rowIndex: entry.domain.rowIndex + 1 } + }); + } + } +} function compareStandaloneDomainFields(entries, warnings) { for (const field of STANDALONE_DOMAIN_CANONICAL_FIELDS) { const values = new Set(entries.map((entry) => entry.domain[field]?.trim() ?? "")); @@ -13326,6 +13345,7 @@ function ensureVaultValidation(index) { return; } clearVaultValidationWarnings(index); + clearDomainParentNotDefinedWarnings(index); for (const warning of validateVaultIndex(index)) { pushWarning(index.warningsByFilePath, warning.path ?? "vault", { ...warning, @@ -13676,6 +13696,24 @@ function clearVaultValidationWarnings(index) { ); } } +function clearDomainParentNotDefinedWarnings(index) { + for (const [path2, warnings] of Object.entries(index.warningsByFilePath)) { + const model = index.modelsByFilePath[path2]; + if (model?.fileType !== "domains") { + continue; + } + index.warningsByFilePath[path2] = warnings.filter( + (warning) => !getDomainParentNotDefinedValue(warning) + ); + } +} +function getDomainParentNotDefinedValue(warning) { + if (warning.code !== "unresolved-reference" || warning.field !== "Domains.parent") { + return null; + } + const match = warning.message.match(/^Domain parent "([^"]+)" is not defined\.$/); + return match?.[1] ?? null; +} function getDuplicateModelKey(model) { if (model.fileType === "markdown") { return null; diff --git a/src/core/validator.ts b/src/core/validator.ts index 64c9e18..4f27fa6 100644 --- a/src/core/validator.ts +++ b/src/core/validator.ts @@ -14,6 +14,7 @@ import { formatDfdObjectUnknownLocalDomainMessage, formatDfdLocalDomainFieldMismatchMessage, formatDfdLocalDomainMissingSharedMessage, + formatDomainParentUnknownMessage, formatStandaloneDomainDuplicateMessage, formatStandaloneDomainFieldConflictMessage } from "./domain-diagnostics"; @@ -127,6 +128,8 @@ function validateStandaloneDomains( } } + validateStandaloneDomainParents(entriesByDomainId, warnings); + for (const entries of entriesByDomainId.values()) { if (entries.length < 2) { continue; @@ -159,6 +162,28 @@ function validateStandaloneDomains( } } +function validateStandaloneDomainParents( + entriesByDomainId: Map>, + warnings: ValidationWarning[] +): void { + for (const entries of entriesByDomainId.values()) { + for (const entry of entries) { + const parent = entry.domain.parent?.trim(); + if (!parent || parent === entry.domain.id || entriesByDomainId.has(parent)) { + continue; + } + warnings.push({ + code: "unresolved-reference", + message: formatDomainParentUnknownMessage(parent), + severity: "warning", + path: entry.path, + field: "Domains.parent", + context: { rowIndex: entry.domain.rowIndex + 1 } + }); + } + } +} + function compareStandaloneDomainFields( entries: Array<{ domain: DomainEntry; path: string }>, warnings: ValidationWarning[] diff --git a/src/core/vault-index.ts b/src/core/vault-index.ts index 5edefa9..05fa3f3 100644 --- a/src/core/vault-index.ts +++ b/src/core/vault-index.ts @@ -182,6 +182,7 @@ export function ensureVaultValidation(index: ModelingVaultIndex): void { } clearVaultValidationWarnings(index); + clearDomainParentNotDefinedWarnings(index); for (const warning of validateVaultIndex(index)) { pushWarning(index.warningsByFilePath, warning.path ?? "vault", { ...warning, @@ -599,6 +600,27 @@ function clearVaultValidationWarnings(index: ModelingVaultIndex): void { } } +function clearDomainParentNotDefinedWarnings(index: ModelingVaultIndex): void { + for (const [path, warnings] of Object.entries(index.warningsByFilePath)) { + const model = index.modelsByFilePath[path]; + if (model?.fileType !== "domains") { + continue; + } + + index.warningsByFilePath[path] = warnings.filter( + (warning) => !getDomainParentNotDefinedValue(warning) + ); + } +} + +function getDomainParentNotDefinedValue(warning: ValidationWarning): string | null { + if (warning.code !== "unresolved-reference" || warning.field !== "Domains.parent") { + return null; + } + const match = warning.message.match(/^Domain parent "([^"]+)" is not defined\.$/); + return match?.[1] ?? null; +} + function getDuplicateModelKey( model: ParsedFileModel ): { key: string; id: string } | null { diff --git a/test/domains-model.test.mjs b/test/domains-model.test.mjs index b22ab0f..be370dd 100644 --- a/test/domains-model.test.mjs +++ b/test/domains-model.test.mjs @@ -534,6 +534,24 @@ test("localizes standalone Domain diagnostics", () => { ); }); +test("allows standalone Domains parent from another Domains file", () => { + const index = buildVaultIndex([ + domainsFile("COUNTRY_EXAMPLE.md", "COUNTRY_EXAMPLE", "| software_develop | Software develop | business | | Shared parent |"), + domainsFile("COUNTRY_EXAMPLE_1.md", "COUNTRY_EXAMPLE_1", [ + "| software_develop_eigyo_1 | Sales 1 | team | software_develop | External parent |", + "| software_develop_kaihatsu_1 | Development 1 | team | software_develop | External parent |" + ].join("\n")) + ]); + + const messages = (index.warningsByFilePath["COUNTRY_EXAMPLE_1.md"] ?? []).map( + (warning) => warning.message + ); + assert.equal( + messages.includes("Domain parent \"software_develop\" is not defined."), + false + ); +}); + test("validates duplicate standalone Domains across files", () => { const index = buildVaultIndex([ { @@ -1498,6 +1516,10 @@ id: DOMAIN-DIAGRAM-LOGISTICS ["company", "logistics", "warehouse", "team_1"] ); assert.equal(resolved.warnings.length, 0); + const sourceMessages = (index.warningsByFilePath["model/domains/DOMAINS-WAREHOUSE.md"] ?? []).map( + (warning) => warning.message + ); + assert.equal(sourceMessages.includes("Domain parent \"logistics\" is not defined."), false); assert.match(buildDomainMindmapMermaid(resolved.domains), /会社全体(organization)/); assert.match(buildDomainHierarchyMermaid(resolved.domains), /subgraph domain_company/); assert.match(