diff --git a/main.js b/main.js index 40be6b4..639d37e 100644 --- a/main.js +++ b/main.js @@ -13179,7 +13179,7 @@ function validateDiagram(diagram, index, warnings) { if (diagram.schema === "dfd_diagram") { const dfdDiagram = diagram; validateDfdLocalDomains(dfdDiagram, index, warnings); - validateDfdObjectDomains(dfdDiagram, warnings); + validateDfdObjectDomains(dfdDiagram, index, warnings); const objectEntries = dfdDiagram.objectEntries.length > 0 ? dfdDiagram.objectEntries : dfdDiagram.objectRefs.map((objectRef, rowIndex) => ({ ref: objectRef, rowIndex, @@ -13272,16 +13272,16 @@ function validateDiagram(diagram, index, warnings) { } } } -function validateDfdObjectDomains(diagram, warnings) { - const localDomains = diagram.domains ?? []; - const localDomainIds = new Set(localDomains.map((domain) => domain.id)); +function validateDfdObjectDomains(diagram, index, warnings) { + const hasDomainSources = diagram.domainSources.length > 0; + const mergedDomainIds = buildDfdMergedDomainIdSet(diagram, index, warnings); for (const entry of diagram.objectEntries) { const domain = entry.domain?.trim(); if (!domain) { continue; } const objectId = entry.id?.trim() || entry.ref?.trim() || String(entry.rowIndex + 1); - if (localDomainIds.size === 0) { + if (mergedDomainIds.size === 0 && !hasDomainSources) { warnings.push({ code: "unresolved-reference", message: formatDfdObjectDomainWithoutLocalDomainsMessage(objectId, domain), @@ -13290,10 +13290,10 @@ function validateDfdObjectDomains(diagram, warnings) { field: "Objects.domain", context: { rowIndex: entry.rowIndex + 1 } }); - } else if (!localDomainIds.has(domain)) { + } else if (!mergedDomainIds.has(domain)) { warnings.push({ code: "unresolved-reference", - message: formatDfdObjectUnknownLocalDomainMessage(objectId, domain), + message: hasDomainSources ? formatDfdObjectUnknownDomainMessage(objectId, domain) : formatDfdObjectUnknownLocalDomainMessage(objectId, domain), severity: "warning", path: diagram.path, field: "Objects.domain", @@ -13302,6 +13302,47 @@ function validateDfdObjectDomains(diagram, warnings) { } } } +function buildDfdMergedDomainIdSet(diagram, index, warnings) { + const domainsById = /* @__PURE__ */ new Map(); + if (diagram.domainSources.length > 0) { + const resolvedSources = resolveDomainSources( + diagram.path, + diagram.domainSources, + index + ); + warnings.push(...resolvedSources.warnings); + for (const domain of resolvedSources.domains) { + domainsById.set(domain.id, domain); + } + } + for (const localDomain of diagram.domains ?? []) { + const externalDomain = domainsById.get(localDomain.id); + if (externalDomain) { + for (const field of STANDALONE_DOMAIN_CANONICAL_FIELDS) { + const localValue = localDomain[field]?.trim() ?? ""; + const sourceValue = externalDomain[field]?.trim() ?? ""; + if (!localValue || !sourceValue || localValue === sourceValue) { + continue; + } + warnings.push({ + code: "invalid-structure", + message: formatDfdLocalDomainOverridesSourceMessage( + localDomain.id, + field, + localValue, + sourceValue + ), + severity: "warning", + path: diagram.path, + field: `Domains.${field}`, + context: { rowIndex: localDomain.rowIndex + 1 } + }); + } + } + domainsById.set(localDomain.id, localDomain); + } + return new Set(domainsById.keys()); +} function validateDfdLocalDomains(diagram, index, warnings) { const localDomains = diagram.domains ?? []; if (localDomains.length === 0) { diff --git a/src/core/validator.ts b/src/core/validator.ts index 4f27fa6..ea7f9b7 100644 --- a/src/core/validator.ts +++ b/src/core/validator.ts @@ -11,13 +11,16 @@ import type { } from "../types/models"; import { formatDfdObjectDomainWithoutLocalDomainsMessage, + formatDfdObjectUnknownDomainMessage, formatDfdObjectUnknownLocalDomainMessage, + formatDfdLocalDomainOverridesSourceMessage, formatDfdLocalDomainFieldMismatchMessage, formatDfdLocalDomainMissingSharedMessage, formatDomainParentUnknownMessage, formatStandaloneDomainDuplicateMessage, formatStandaloneDomainFieldConflictMessage } from "./domain-diagnostics"; +import { resolveDomainSources } from "./domain-diagram-resolver"; import { buildReferenceIdentityKeys, parseReferenceValue, @@ -228,7 +231,7 @@ function validateDiagram( if (diagram.schema === "dfd_diagram") { const dfdDiagram = diagram; validateDfdLocalDomains(dfdDiagram, index, warnings); - validateDfdObjectDomains(dfdDiagram, warnings); + validateDfdObjectDomains(dfdDiagram, index, warnings); const objectEntries: DfdDiagramObjectEntry[] = dfdDiagram.objectEntries.length > 0 @@ -351,10 +354,11 @@ function validateDiagram( function validateDfdObjectDomains( diagram: DfdDiagramModel, + index: ModelingVaultIndex, warnings: ValidationWarning[] ): void { - const localDomains = diagram.domains ?? []; - const localDomainIds = new Set(localDomains.map((domain) => domain.id)); + const hasDomainSources = diagram.domainSources.length > 0; + const mergedDomainIds = buildDfdMergedDomainIdSet(diagram, index, warnings); for (const entry of diagram.objectEntries) { const domain = entry.domain?.trim(); @@ -363,7 +367,7 @@ function validateDfdObjectDomains( } const objectId = entry.id?.trim() || entry.ref?.trim() || String(entry.rowIndex + 1); - if (localDomainIds.size === 0) { + if (mergedDomainIds.size === 0 && !hasDomainSources) { warnings.push({ code: "unresolved-reference", message: formatDfdObjectDomainWithoutLocalDomainsMessage(objectId, domain), @@ -372,10 +376,12 @@ function validateDfdObjectDomains( field: "Objects.domain", context: { rowIndex: entry.rowIndex + 1 } }); - } else if (!localDomainIds.has(domain)) { + } else if (!mergedDomainIds.has(domain)) { warnings.push({ code: "unresolved-reference", - message: formatDfdObjectUnknownLocalDomainMessage(objectId, domain), + message: hasDomainSources + ? formatDfdObjectUnknownDomainMessage(objectId, domain) + : formatDfdObjectUnknownLocalDomainMessage(objectId, domain), severity: "warning", path: diagram.path, field: "Objects.domain", @@ -385,6 +391,58 @@ function validateDfdObjectDomains( } } +function buildDfdMergedDomainIdSet( + diagram: DfdDiagramModel, + index: ModelingVaultIndex, + warnings: ValidationWarning[] +): Set { + const domainsById = new Map(); + + if (diagram.domainSources.length > 0) { + const resolvedSources = resolveDomainSources( + diagram.path, + diagram.domainSources, + index + ); + warnings.push(...resolvedSources.warnings); + + for (const domain of resolvedSources.domains) { + domainsById.set(domain.id, domain); + } + } + + for (const localDomain of diagram.domains ?? []) { + const externalDomain = domainsById.get(localDomain.id); + if (externalDomain) { + for (const field of STANDALONE_DOMAIN_CANONICAL_FIELDS) { + const localValue = localDomain[field]?.trim() ?? ""; + const sourceValue = externalDomain[field]?.trim() ?? ""; + if (!localValue || !sourceValue || localValue === sourceValue) { + continue; + } + + warnings.push({ + code: "invalid-structure", + message: formatDfdLocalDomainOverridesSourceMessage( + localDomain.id, + field, + localValue, + sourceValue + ), + severity: "warning", + path: diagram.path, + field: `Domains.${field}`, + context: { rowIndex: localDomain.rowIndex + 1 } + }); + } + } + + domainsById.set(localDomain.id, localDomain); + } + + return new Set(domainsById.keys()); +} + function validateDfdLocalDomains( diagram: DfdDiagramModel, index: ModelingVaultIndex, diff --git a/test/domains-model.test.mjs b/test/domains-model.test.mjs index f6191b5..6f25b4a 100644 --- a/test/domains-model.test.mjs +++ b/test/domains-model.test.mjs @@ -482,6 +482,57 @@ function resolveDfdWithFiles(files) { }; } +function buildDfdValidationIndex({ + domainSources = "", + domains = "", + objectDomain = "wms", + extraFiles = [] +} = {}) { + const domainSourcesSection = domainSources + ? `## Domain Sources + +| ref | notes | +|---|---| +${domainSources} + +` + : ""; + const domainsSection = domains + ? `## Domains + +| id | name | kind | parent | description | +|---|---|---|---|---| +${domains} + +` + : ""; + + return buildVaultIndex([ + ...extraFiles, + { + path: "DFD-SHIPPING.md", + content: `${dfdFrontmatter}${domainSourcesSection}${domainsSection}## Objects + +| id | label | kind | ref | domain | notes | +|---|---|---|---|---|---| +| receive_order | Receive order | process | | ${objectDomain} | | + +## Flows + +| id | from | to | data | notes | +|---|---|---|---|---| +` + } + ]); +} + +function dfdValidationMessages(options) { + const index = buildDfdValidationIndex(options); + return (index.warningsByFilePath["DFD-SHIPPING.md"] ?? []).map( + (warning) => warning.message + ); +} + test("parses standalone Domains documents", () => { const { file, warnings } = parseDomains(`${baseFrontmatter} ## Domains @@ -2903,6 +2954,96 @@ test("localizes DFD object Domain diagnostics", () => { ); }); +test("vault validation accepts DFD object domain from Domain Sources", () => { + const messages = dfdValidationMessages({ + domainSources: "| [[DOMAINS-COMPANY]] | Company domains |", + extraFiles: [ + domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [ + "| logistics | Logistics | department | | Logistics group |", + "| wms | WMS | system | logistics | WMS group |" + ].join("\n")) + ] + }); + + assert.equal( + messages.includes('DFD object "receive_order" references unknown Domain "wms".'), + false + ); + assert.equal( + messages.includes('DFD object "receive_order" references Domain "wms", but this DFD has no local Domains.'), + false + ); +}); + +test("vault validation keeps unknown DFD object domain warning with Domain Sources", () => { + const messages = dfdValidationMessages({ + domainSources: "| [[DOMAINS-COMPANY]] | Company domains |", + objectDomain: "missing", + extraFiles: [ + domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [ + "| logistics | Logistics | department | | Logistics group |", + "| wms | WMS | system | logistics | WMS group |" + ].join("\n")) + ] + }); + + assert.ok(messages.includes('DFD object "receive_order" references unknown Domain "missing".')); + assert.equal( + messages.includes('DFD object "receive_order" references Domain "missing", but this DFD has no local Domains.'), + false + ); +}); + +test("vault validation accepts DFD object domain from local Domains", () => { + const messages = dfdValidationMessages({ + domains: "| wms | WMS | system | | Local WMS |" + }); + + assert.equal( + messages.includes('DFD object "receive_order" references unknown local Domain "wms".'), + false + ); + assert.equal( + messages.includes('DFD object "receive_order" references Domain "wms", but this DFD has no local Domains.'), + false + ); +}); + +test("vault validation keeps unknown local DFD object domain warning", () => { + const messages = dfdValidationMessages({ + domains: "| warehouse | Warehouse | location | | Local warehouse |", + objectDomain: "missing" + }); + + assert.ok(messages.includes('DFD object "receive_order" references unknown local Domain "missing".')); +}); + +test("vault validation keeps DFD local override warning with Domain Sources", () => { + const messages = dfdValidationMessages({ + domainSources: "| [[DOMAINS-COMPANY]] | Company domains |", + domains: "| wms | Local WMS | application | logistics | Local override |", + extraFiles: [ + domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [ + "| logistics | Logistics | department | | Logistics group |", + "| wms | WMS | system | logistics | WMS group |" + ].join("\n")) + ] + }); + + assert.ok(messages.includes('DFD-local Domain "wms" overrides Domain Source name "WMS" with "Local WMS".')); + assert.ok(messages.includes('DFD-local Domain "wms" overrides Domain Source kind "system" with "application".')); + assert.equal( + messages.includes('DFD object "receive_order" references unknown Domain "wms".'), + false + ); +}); + +test("vault validation preserves DFD domain compatibility warning without Domains", () => { + const messages = dfdValidationMessages(); + + assert.ok(messages.includes('DFD object "receive_order" references Domain "wms", but this DFD has no local Domains.')); +}); + test("DFD-local Domains reuse in-file Domain diagnostics", () => { const { warnings } = parseDfd(dfdBody([ "| warehouse | Warehouse | location | | Warehouse operation |",