Merge branch 'fix/dfd-domain-sources-validation' into develop

This commit is contained in:
nejimakibird 2026-06-30 10:45:25 +09:00
commit 57e6a656d9
3 changed files with 253 additions and 13 deletions

55
main.js
View file

@ -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) {

View file

@ -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<string> {
const domainsById = new Map<string, DomainEntry>();
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,

View file

@ -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 |",