mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
3622 lines
116 KiB
JavaScript
3622 lines
116 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { readFileSync } from "node:fs";
|
||
import { test } from "node:test";
|
||
import { build } from "esbuild";
|
||
|
||
const outputFile = "dist/test-domains-model.mjs";
|
||
|
||
await build({
|
||
stdin: {
|
||
contents: [
|
||
'export { parseDomainsFile } from "./src/parsers/domains-parser";',
|
||
'export { parseDomainDiagramFile } from "./src/parsers/domain-diagram-parser";',
|
||
'export { parseColorSchemeFile } from "./src/parsers/color-scheme-parser";',
|
||
'export { parseDfdDiagramFile } from "./src/parsers/dfd-diagram-parser";',
|
||
'export { parseMappingFile } from "./src/parsers/mapping-parser";',
|
||
'export { parseDiagramFile } from "./src/parsers/diagram-parser";',
|
||
'export { MODEL_WEAVE_TEMPLATES } from "./src/templates/model-weave-templates";',
|
||
'export { DEFAULT_MODEL_WEAVE_SETTINGS, DOMAIN_VIEW_MODE_SETTING_OPTIONS, normalizeModelWeaveSettings } from "./src/settings/model-weave-settings";',
|
||
'export { detectFileType } from "./src/core/schema-detector";',
|
||
'export { isModelWeavePreviewSupportedFileType, SUPPORTED_MODEL_WEAVE_FORMAT_LIST } from "./src/core/supported-formats";',
|
||
'export { BUILT_IN_COLOR_SCHEME, getAppliedColorSchemeRowsForTargets, getEffectiveColorSchemeEntriesForTarget, resolveColorStyle, resolveDefaultColorScheme } from "./src/core/color-scheme";',
|
||
'export { buildDomainTree } from "./src/core/domain-tree";',
|
||
'export { buildDomainRelationshipSummaries } from "./src/core/domain-relationships";',
|
||
'export { mergeDomainDiagramSources, resolveDomainDiagram } from "./src/core/domain-diagram-resolver";',
|
||
'export { getSupportedRenderModes, resolveRenderMode } from "./src/core/render-mode";',
|
||
'export { buildImpactSummary } from "./src/core/impact-analyzer";',
|
||
'export { buildWeaveMapModel } from "./src/core/weave-map";',
|
||
'export { createModelWeaveTranslator } from "./src/i18n/messages";',
|
||
'export { buildDomainHierarchyMermaid, buildDomainMindmapMermaid, buildDomainTreeViewMermaid } from "./src/renderers/domains-mermaid";',
|
||
'export { resolveDiagramRelations } from "./src/core/relation-resolver";',
|
||
'export { buildDfdMermaidSource, renderDfdMermaidDiagram } from "./src/renderers/dfd-mermaid";',
|
||
'export { renderObjectContext } from "./src/renderers/object-context-renderer";',
|
||
'export { renderClassDiagram } from "./src/renderers/class-renderer";',
|
||
'export { buildVaultIndex, ensureVaultValidation, replaceVaultIndexFile } from "./src/core/vault-index";',
|
||
'export { resolveObjectContext } from "./src/core/object-context-resolver";',
|
||
'export { buildCurrentObjectDiagnostics, localizeDiagnosticMessage } from "./src/core/current-file-diagnostics";',
|
||
'export { getExpectedHeaderForDiagnostic } from "./src/core/diagnostic-section-guidance";'
|
||
].join("\n"),
|
||
resolveDir: ".",
|
||
sourcefile: "test-domains-model-entry.ts",
|
||
loader: "ts"
|
||
},
|
||
bundle: true,
|
||
format: "esm",
|
||
platform: "browser",
|
||
plugins: [
|
||
{
|
||
name: "stub-obsidian",
|
||
setup(buildApi) {
|
||
buildApi.onResolve({ filter: /^obsidian$/ }, () => ({
|
||
path: "obsidian",
|
||
namespace: "stub"
|
||
}));
|
||
buildApi.onLoad({ filter: /^obsidian$/, namespace: "stub" }, () => ({
|
||
contents: [
|
||
"export const Platform = {};",
|
||
"export class Notice {};",
|
||
"export class TFile {};",
|
||
"export class ItemView {};",
|
||
"export class WorkspaceLeaf {};",
|
||
"export const MarkdownRenderer = {};",
|
||
"export const normalizePath = (value) => value;",
|
||
"export const getLanguage = () => 'en';",
|
||
"export const loadMermaid = async () => ({ render: async () => ({ svg: '' }) });"
|
||
].join("\n"),
|
||
loader: "js"
|
||
}));
|
||
}
|
||
},
|
||
{
|
||
name: "stub-node-builtins",
|
||
setup(buildApi) {
|
||
buildApi.onResolve({ filter: /^(fs|path|electron)$/ }, (args) => ({
|
||
path: args.path,
|
||
namespace: "stub"
|
||
}));
|
||
buildApi.onLoad({ filter: /.*/, namespace: "stub" }, (args) => ({
|
||
contents:
|
||
args.path === "path"
|
||
? "export default {}; export const win32 = {}; export const posix = {};"
|
||
: "export const shell = { openPath: async () => '' }; export default {};",
|
||
loader: "js"
|
||
}));
|
||
}
|
||
}
|
||
],
|
||
outfile: outputFile,
|
||
logLevel: "silent"
|
||
});
|
||
|
||
const {
|
||
parseDomainsFile,
|
||
parseDomainDiagramFile,
|
||
parseColorSchemeFile,
|
||
parseDfdDiagramFile,
|
||
parseMappingFile,
|
||
parseDiagramFile,
|
||
MODEL_WEAVE_TEMPLATES,
|
||
DEFAULT_MODEL_WEAVE_SETTINGS,
|
||
DOMAIN_VIEW_MODE_SETTING_OPTIONS,
|
||
normalizeModelWeaveSettings,
|
||
detectFileType,
|
||
isModelWeavePreviewSupportedFileType,
|
||
SUPPORTED_MODEL_WEAVE_FORMAT_LIST,
|
||
BUILT_IN_COLOR_SCHEME,
|
||
getAppliedColorSchemeRowsForTargets,
|
||
getEffectiveColorSchemeEntriesForTarget,
|
||
resolveColorStyle,
|
||
resolveDefaultColorScheme,
|
||
buildImpactSummary,
|
||
buildWeaveMapModel,
|
||
buildDomainRelationshipSummaries,
|
||
buildDomainTree,
|
||
buildDomainHierarchyMermaid,
|
||
buildDomainMindmapMermaid,
|
||
buildDomainTreeViewMermaid,
|
||
createModelWeaveTranslator,
|
||
buildDfdMermaidSource,
|
||
renderDfdMermaidDiagram,
|
||
renderObjectContext,
|
||
renderClassDiagram,
|
||
buildVaultIndex,
|
||
resolveObjectContext,
|
||
mergeDomainDiagramSources,
|
||
getSupportedRenderModes,
|
||
resolveRenderMode,
|
||
resolveDomainDiagram,
|
||
buildCurrentObjectDiagnostics,
|
||
ensureVaultValidation,
|
||
localizeDiagnosticMessage,
|
||
getExpectedHeaderForDiagnostic,
|
||
resolveDiagramRelations,
|
||
replaceVaultIndexFile
|
||
} = await import(
|
||
`../${outputFile}?t=${Date.now()}`
|
||
);
|
||
|
||
class TestElement {
|
||
constructor(tagName, ownerDocument) {
|
||
this.tagName = tagName;
|
||
this.ownerDocument = ownerDocument;
|
||
this.children = [];
|
||
this.dataset = {};
|
||
this.style = {};
|
||
this.attributes = {};
|
||
this.clientWidth = 0;
|
||
this.clientHeight = 0;
|
||
this.className = "";
|
||
this._textContent = "";
|
||
this.classList = {
|
||
contains: (className) => this.className.split(/\s+/).includes(className),
|
||
add: (...classNames) => {
|
||
const current = new Set(this.className.split(/\s+/).filter(Boolean));
|
||
for (const className of classNames) {
|
||
current.add(className);
|
||
}
|
||
this.className = [...current].join(" ");
|
||
}
|
||
};
|
||
}
|
||
|
||
set textContent(value) {
|
||
this._textContent = String(value ?? "");
|
||
this.children = [];
|
||
}
|
||
|
||
get textContent() {
|
||
return [
|
||
this._textContent,
|
||
...this.children.map((child) => child.textContent ?? "")
|
||
].join("");
|
||
}
|
||
|
||
addClass(className) {
|
||
this.classList.add(className);
|
||
}
|
||
|
||
toggleClass(className, enabled) {
|
||
if (enabled) {
|
||
this.classList.add(className);
|
||
}
|
||
}
|
||
|
||
appendChild(child) {
|
||
this.children.push(child);
|
||
return child;
|
||
}
|
||
|
||
append(...children) {
|
||
for (const child of children) {
|
||
this.appendChild(child);
|
||
}
|
||
}
|
||
|
||
prepend(child) {
|
||
this.children.unshift(child);
|
||
}
|
||
|
||
replaceChildren(...children) {
|
||
this.children = [];
|
||
this.append(...children);
|
||
}
|
||
|
||
setAttribute(name, value) {
|
||
this.attributes[name] = String(value);
|
||
}
|
||
|
||
setCssProps(props) {
|
||
Object.assign(this.style, props);
|
||
}
|
||
|
||
getBoundingClientRect() {
|
||
return { left: 0, top: 0, width: this.clientWidth, height: this.clientHeight };
|
||
}
|
||
|
||
setPointerCapture() {}
|
||
|
||
releasePointerCapture() {}
|
||
|
||
hasPointerCapture() {
|
||
return false;
|
||
}
|
||
|
||
addEventListener() {}
|
||
}
|
||
|
||
function createTestDocument() {
|
||
const document = {
|
||
defaultView: {
|
||
requestAnimationFrame(callback) {
|
||
callback();
|
||
return 1;
|
||
}
|
||
},
|
||
createElement(tagName) {
|
||
return new TestElement(tagName, document);
|
||
},
|
||
createElementNS(_namespace, tagName) {
|
||
return new TestElement(tagName, document);
|
||
}
|
||
};
|
||
document.body = document.createElement("body");
|
||
return document;
|
||
}
|
||
|
||
globalThis.activeDocument = createTestDocument();
|
||
globalThis.Element = TestElement;
|
||
globalThis.window = globalThis.activeDocument.defaultView;
|
||
globalThis.ResizeObserver = class {
|
||
observe() {}
|
||
disconnect() {}
|
||
};
|
||
|
||
function parseDomains(markdown) {
|
||
const result = parseDomainsFile(markdown, "model/domains/core.md");
|
||
assert.ok(result.file);
|
||
return result;
|
||
}
|
||
|
||
function parseDomainDiagram(markdown) {
|
||
const result = parseDomainDiagramFile(markdown, "model/domains/DOMAIN-DIAGRAM-LOGISTICS.md");
|
||
assert.ok(result.file);
|
||
return result;
|
||
}
|
||
|
||
function parseColorScheme(markdown) {
|
||
const result = parseColorSchemeFile(markdown, "model/config/COLOR-SCHEME-DEFAULT.md");
|
||
assert.ok(result.file);
|
||
return result;
|
||
}
|
||
|
||
function createResolvedDfdForDetails() {
|
||
const diagram = {
|
||
fileType: "diagram",
|
||
schema: "dfd_diagram",
|
||
path: "model/dfd/DFD-DETAILS.md",
|
||
title: "DFD Details",
|
||
frontmatter: { type: "dfd_diagram" },
|
||
sections: {},
|
||
sourceLinks: [],
|
||
id: "DFD-DETAILS",
|
||
name: "DFD Details",
|
||
kind: "dfd",
|
||
objectRefs: [],
|
||
objectEntries: [],
|
||
domains: [
|
||
{ id: "warehouse", name: "Warehouse", kind: "location", parent: "", rowIndex: 0 }
|
||
],
|
||
nodes: [],
|
||
edges: [],
|
||
flows: []
|
||
};
|
||
const nodes = [
|
||
{ id: "external", label: "External", kind: "external", metadata: { domain: "warehouse" } },
|
||
{ id: "process", label: "Process", kind: "process" },
|
||
{ id: "store", label: "Store", kind: "datastore" }
|
||
];
|
||
return {
|
||
diagram,
|
||
nodes,
|
||
edges: [],
|
||
missingObjects: [],
|
||
warnings: []
|
||
};
|
||
}
|
||
|
||
function createObjectContextForDetails() {
|
||
return {
|
||
object: {
|
||
fileType: "object",
|
||
path: "model/02_class/CLS-SAMPLE.md",
|
||
frontmatter: { type: "class" },
|
||
sections: {},
|
||
sourceLinks: [],
|
||
schema: "class",
|
||
id: "CLS-SAMPLE",
|
||
name: "Sample",
|
||
kind: "entity",
|
||
responsibilities: [],
|
||
attributes: [],
|
||
methods: [],
|
||
relations: []
|
||
},
|
||
relatedObjects: [],
|
||
warnings: []
|
||
};
|
||
}
|
||
|
||
function createResolvedClassForDetails() {
|
||
const diagram = {
|
||
fileType: "diagram",
|
||
schema: "class_diagram",
|
||
path: "model/02_class/CLS-DIAGRAM.md",
|
||
title: "Class Details",
|
||
frontmatter: { type: "class_diagram" },
|
||
sections: {},
|
||
sourceLinks: [],
|
||
id: "CLS-DIAGRAM",
|
||
name: "Class Details",
|
||
kind: "class",
|
||
objectRefs: [],
|
||
nodes: [],
|
||
edges: []
|
||
};
|
||
const nodes = [
|
||
{
|
||
id: "CLS-SAMPLE",
|
||
label: "Sample",
|
||
kind: "class",
|
||
object: {
|
||
fileType: "object",
|
||
path: "model/02_class/CLS-SAMPLE.md",
|
||
frontmatter: { type: "class" },
|
||
sections: {},
|
||
sourceLinks: [],
|
||
schema: "class",
|
||
id: "CLS-SAMPLE",
|
||
name: "Sample",
|
||
kind: "class",
|
||
responsibilities: [],
|
||
attributes: [],
|
||
methods: [],
|
||
relations: []
|
||
}
|
||
}
|
||
];
|
||
return {
|
||
diagram,
|
||
nodes,
|
||
edges: [],
|
||
missingObjects: [],
|
||
warnings: []
|
||
};
|
||
}
|
||
|
||
const baseFrontmatter = `---
|
||
type: domains
|
||
id: DOMAINS-CORE
|
||
name: Core Domains
|
||
---
|
||
|
||
# Core Domains
|
||
`;
|
||
|
||
const dfdFrontmatter = `---
|
||
type: dfd_diagram
|
||
id: DFD-SHIPPING
|
||
name: Shipping DFD
|
||
---
|
||
|
||
# Shipping DFD
|
||
`;
|
||
|
||
function parseDfd(markdown) {
|
||
const result = parseDfdDiagramFile(markdown, "DFD-SHIPPING.md");
|
||
assert.ok(result.file);
|
||
return result;
|
||
}
|
||
function parseClassDiagram(markdown) {
|
||
const result = parseDiagramFile(markdown, "CLS-DIAGRAM.md");
|
||
assert.ok(result.file);
|
||
return result;
|
||
}
|
||
|
||
test("fully empty Domains rows are ignored", () => {
|
||
const { file, warnings } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| office | 事務所 | organization | | Office |
|
||
| | | | | |
|
||
`);
|
||
|
||
assert.deepEqual(warnings.map((warning) => warning.message), []);
|
||
assert.equal(file.domains.length, 1);
|
||
assert.equal(file.domains[0].id, "office");
|
||
});
|
||
|
||
test("fully empty class diagram Objects rows are ignored", () => {
|
||
const { file, warnings } = parseClassDiagram(`---
|
||
type: class_diagram
|
||
id: CLS-DIAGRAM
|
||
name: Class Diagram
|
||
---
|
||
|
||
## Objects
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| CLS-A | |
|
||
| | |
|
||
`);
|
||
|
||
assert.deepEqual(warnings.map((warning) => warning.message), []);
|
||
assert.equal(file.objectRefs.length, 1);
|
||
assert.equal(file.objectRefs[0], "CLS-A");
|
||
});
|
||
|
||
test("partially filled DFD rows still report required field diagnostics", () => {
|
||
const { file, warnings } = parseDfd(`${dfdFrontmatter}
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| | core_system | receive_order | | | |
|
||
`);
|
||
|
||
assert.equal(file.objectEntries.length, 0);
|
||
assert.ok(warnings.some((warning) => warning.message === 'DFD Objects row must have "id" or "ref".'));
|
||
});
|
||
function dfdBody(domainsRows) {
|
||
return `${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
${domainsRows}
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | notes |
|
||
|---|---|---|---|---|
|
||
| user | User | external | | User |
|
||
| pick | Pick items | process | | Pick |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| request | user | pick | Request | User request |
|
||
`;
|
||
}
|
||
|
||
function domainsFile(path, id, rows) {
|
||
return {
|
||
path,
|
||
content: `---
|
||
type: domains
|
||
id: ${id}
|
||
name: ${id}
|
||
---
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
${rows}
|
||
`
|
||
};
|
||
}
|
||
|
||
function buildDomainsIndex(dfdDomainsRows) {
|
||
const shared = `${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Shared logistics |
|
||
| warehouse | Warehouse | location | logistics | Shared warehouse |
|
||
| wms | WMS | system | logistics | Shared WMS |
|
||
`;
|
||
|
||
return buildVaultIndex([
|
||
{
|
||
path: "DOMAINS-CORE.md",
|
||
content: shared
|
||
},
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content: dfdBody(dfdDomainsRows)
|
||
}
|
||
]);
|
||
}
|
||
|
||
function resolveDfdFromContent(content) {
|
||
const index = buildVaultIndex([
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content
|
||
}
|
||
], { parseMode: "full", validate: false });
|
||
const model = index.modelsByFilePath["DFD-SHIPPING.md"];
|
||
assert.equal(model.fileType, "dfd-diagram");
|
||
return {
|
||
index,
|
||
model,
|
||
resolved: resolveDiagramRelations(model, index)
|
||
};
|
||
}
|
||
|
||
function resolveDfdWithFiles(files) {
|
||
const index = buildVaultIndex(files, { parseMode: "full", validate: false });
|
||
const model = index.modelsByFilePath["DFD-SHIPPING.md"];
|
||
assert.equal(model.fileType, "dfd-diagram");
|
||
return {
|
||
index,
|
||
model,
|
||
resolved: resolveDiagramRelations(model, index)
|
||
};
|
||
}
|
||
|
||
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
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | organization | | Logistics area |
|
||
| warehouse | Warehouse | location | logistics | Warehouse operation |
|
||
| fallback_name | | system | logistics | Uses id as display fallback |
|
||
`);
|
||
|
||
assert.equal(file.fileType, "domains");
|
||
assert.equal(file.schema, "domains");
|
||
assert.equal(file.domains.length, 3);
|
||
assert.equal(file.domains[1].parent, "logistics");
|
||
assert.equal(file.domains[2].name, undefined);
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("diagnoses invalid standalone Domain definitions", () => {
|
||
const { warnings } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| warehouse | Warehouse | location | | Warehouse operation |
|
||
| warehouse | Duplicate | location | | Duplicate id |
|
||
| orphan | Orphan | system | missing_parent | Unknown parent |
|
||
| self | Self | system | self | Self parent |
|
||
| cycle_a | Cycle A | system | cycle_b | Cycle A |
|
||
| cycle_b | Cycle B | system | cycle_a | Cycle B |
|
||
| | Missing | system | | Missing id |
|
||
`);
|
||
|
||
const messages = warnings.map((warning) => warning.message);
|
||
assert.ok(messages.includes('duplicate Domain id "warehouse"'));
|
||
assert.ok(messages.includes('Domain parent "missing_parent" is not defined.'));
|
||
assert.ok(messages.includes('Domain "self" cannot use itself as parent.'));
|
||
assert.ok(messages.some((message) => message.includes("Domain parent cycle detected")));
|
||
assert.ok(messages.includes("Domain id is required."));
|
||
});
|
||
|
||
test("localizes standalone Domain diagnostics", () => {
|
||
assert.equal(
|
||
localizeDiagnosticMessage('Domain parent "missing_parent" is not defined.', "ja"),
|
||
'Domain parent "missing_parent" が定義されていません。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage('Domain "self" cannot use itself as parent.', "ja"),
|
||
'Domain "self" は自分自身を parent にできません。'
|
||
);
|
||
});
|
||
|
||
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([
|
||
{
|
||
path: "DOMAINS-CORE.md",
|
||
content: `${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Shared logistics |
|
||
| wms | WMS | system | logistics | Shared WMS |
|
||
`
|
||
},
|
||
{
|
||
path: "DOMAINS-WAREHOUSE.md",
|
||
content: `---
|
||
type: domains
|
||
id: DOMAINS-WAREHOUSE
|
||
name: Warehouse Domains
|
||
---
|
||
|
||
# Warehouse Domains
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| warehouse | Warehouse | location | logistics | Shared warehouse |
|
||
| wms | WMS | system | logistics | Shared WMS |
|
||
`
|
||
}
|
||
]);
|
||
|
||
const coreMessages = (index.warningsByFilePath["DOMAINS-CORE.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
const warehouseMessages = (index.warningsByFilePath["DOMAINS-WAREHOUSE.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
|
||
assert.ok(coreMessages.includes('Domain "wms" is defined in multiple Domains files.'));
|
||
assert.ok(warehouseMessages.includes('Domain "wms" is defined in multiple Domains files.'));
|
||
assert.equal(
|
||
warehouseMessages.some((message) => message.includes("conflicting")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("validates conflicting standalone Domains across files", () => {
|
||
const index = buildVaultIndex([
|
||
{
|
||
path: "DOMAINS-CORE.md",
|
||
content: `${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Shared logistics |
|
||
| warehouse | Warehouse | location | logistics | Shared warehouse |
|
||
| wms | WMS | system | warehouse | Shared WMS |
|
||
`
|
||
},
|
||
{
|
||
path: "DOMAINS-WAREHOUSE.md",
|
||
content: `---
|
||
type: domains
|
||
id: DOMAINS-WAREHOUSE
|
||
name: Warehouse Domains
|
||
---
|
||
|
||
# Warehouse Domains
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Shared logistics |
|
||
| wms | WMS local | application | logistics | Local WMS |
|
||
`
|
||
}
|
||
]);
|
||
|
||
const messages = (index.warningsByFilePath["DOMAINS-WAREHOUSE.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
|
||
assert.ok(messages.includes('Domain "wms" is defined in multiple Domains files.'));
|
||
assert.ok(messages.includes('Domain "wms" has conflicting name values across Domains files.'));
|
||
assert.ok(messages.includes('Domain "wms" has conflicting kind values across Domains files.'));
|
||
assert.ok(messages.includes('Domain "wms" has conflicting parent values across Domains files.'));
|
||
assert.equal(
|
||
messages.some((message) => message.includes("description")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("shows standalone Domain duplicate diagnostics through current preview path", () => {
|
||
const fileA = `${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | Company | organization | | Company |
|
||
| logistics | Logistics | department | company | Logistics |
|
||
| warehouse | Warehouse | location | logistics | Warehouse |
|
||
| wms | WMS | system | logistics | WMS |
|
||
`;
|
||
const fileB = `---
|
||
type: domains
|
||
id: DOMAINS-WAREHOUSE
|
||
name: Warehouse Domains
|
||
---
|
||
|
||
# Warehouse Domains
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Logistics |
|
||
| warehouse | Warehouse | location | logistics | Warehouse |
|
||
| wms | WMS | system | logistics | WMS |
|
||
`;
|
||
const fileC = `---
|
||
type: domains
|
||
id: DOMAINS-UNIQUE
|
||
name: Unique Domains
|
||
---
|
||
|
||
# Unique Domains
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| billing | Billing | department | | Billing |
|
||
`;
|
||
const index = buildVaultIndex([
|
||
{
|
||
path: "DOMAINS-CORE.md",
|
||
frontmatter: { type: "domains", id: "DOMAINS-CORE", name: "Core Domains" }
|
||
},
|
||
{
|
||
path: "DOMAINS-WAREHOUSE.md",
|
||
frontmatter: {
|
||
type: "domains",
|
||
id: "DOMAINS-WAREHOUSE",
|
||
name: "Warehouse Domains"
|
||
}
|
||
},
|
||
{
|
||
path: "DOMAINS-UNIQUE.md",
|
||
frontmatter: {
|
||
type: "domains",
|
||
id: "DOMAINS-UNIQUE",
|
||
name: "Unique Domains"
|
||
}
|
||
}
|
||
], { parseMode: "shallow", validate: false });
|
||
|
||
replaceVaultIndexFile(index, { path: "DOMAINS-CORE.md", content: fileA }, "full");
|
||
replaceVaultIndexFile(index, { path: "DOMAINS-WAREHOUSE.md", content: fileB }, "full");
|
||
replaceVaultIndexFile(index, { path: "DOMAINS-UNIQUE.md", content: fileC }, "full");
|
||
ensureVaultValidation(index);
|
||
|
||
const currentModel = index.modelsByFilePath["DOMAINS-CORE.md"];
|
||
assert.equal(currentModel.fileType, "domains");
|
||
const diagnostics = buildCurrentObjectDiagnostics(
|
||
currentModel,
|
||
index,
|
||
null,
|
||
index.warningsByFilePath["DOMAINS-CORE.md"] ?? []
|
||
);
|
||
const messages = diagnostics.map((warning) => warning.message);
|
||
const uniqueMessages = (index.warningsByFilePath["DOMAINS-UNIQUE.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
|
||
assert.ok(messages.includes('Domain "logistics" is defined in multiple Domains files.'));
|
||
assert.ok(messages.includes('Domain "warehouse" is defined in multiple Domains files.'));
|
||
assert.ok(messages.includes('Domain "wms" is defined in multiple Domains files.'));
|
||
assert.ok(messages.includes('Domain "logistics" has conflicting parent values across Domains files.'));
|
||
assert.equal(
|
||
uniqueMessages.some((message) => message.includes("multiple Domains files")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("localizes vault-wide standalone Domain diagnostics", () => {
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'Domain "wms" is defined in multiple Domains files.',
|
||
"ja"
|
||
),
|
||
'Domain "wms" が複数の Domains ファイルで定義されています。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'Domain "wms" has conflicting kind values across Domains files.',
|
||
"ja"
|
||
),
|
||
'Domain "wms" の kind が複数の Domains ファイルで一致していません。'
|
||
);
|
||
});
|
||
|
||
test("parses standalone Domain Diagram source refs", () => {
|
||
const { file, warnings } = parseDomainDiagram(`---
|
||
type: domain_diagram
|
||
title: 物流Domain統合図
|
||
id: DOMAIN-DIAGRAM-LOGISTICS
|
||
name: 物流Domain統合図
|
||
---
|
||
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | 会社・部門 |
|
||
| [[DOMAINS-WAREHOUSE]] | 倉庫・係 |
|
||
`);
|
||
|
||
assert.equal(file.fileType, "domain-diagram");
|
||
assert.equal(file.id, "DOMAIN-DIAGRAM-LOGISTICS");
|
||
assert.equal(file.domainSources.length, 2);
|
||
assert.equal(file.domainSources[0].ref, "[[DOMAINS-COMPANY]]");
|
||
assert.equal(file.domainSources[1].notes, "倉庫・係");
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("parses standalone Domain Diagram source refs without notes", () => {
|
||
const { file, warnings } = parseDomainDiagram(`---
|
||
type: domain_diagram
|
||
title: 物流Domain統合図
|
||
id: DOMAIN-DIAGRAM-LOGISTICS
|
||
name: 物流Domain統合図
|
||
---
|
||
|
||
## Domain Sources
|
||
|
||
| ref |
|
||
|---|
|
||
| [[DOMAINS-COMPANY]] |
|
||
| [[DOMAINS-WAREHOUSE]] |
|
||
`);
|
||
|
||
assert.equal(file.domainSources.length, 2);
|
||
assert.equal(file.domainSources[0].ref, "[[DOMAINS-COMPANY]]");
|
||
assert.equal(file.domainSources[0].notes, undefined);
|
||
assert.equal(file.domainSources[1].ref, "[[DOMAINS-WAREHOUSE]]");
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("registers domain_diagram as a supported Model Weave type", () => {
|
||
assert.equal(detectFileType({ type: "domain_diagram" }), "domain-diagram");
|
||
assert.equal(isModelWeavePreviewSupportedFileType("domain-diagram"), true);
|
||
assert.match(SUPPORTED_MODEL_WEAVE_FORMAT_LIST, /domain_diagram/);
|
||
assert.equal(detectFileType({ type: "color_scheme" }), "color-scheme");
|
||
assert.equal(isModelWeavePreviewSupportedFileType("color-scheme"), true);
|
||
assert.match(SUPPORTED_MODEL_WEAVE_FORMAT_LIST, /color_scheme/);
|
||
});
|
||
|
||
test("defines Domain and Color Scheme insertion templates", () => {
|
||
assert.match(MODEL_WEAVE_TEMPLATES.domains, /type: domains/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.domains, /\| id \| name \| kind \| parent \| description \|/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.domains, /business_domain/);
|
||
|
||
assert.match(MODEL_WEAVE_TEMPLATES.domainDiagram, /type: domain_diagram/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.domainDiagram, /## Domain Sources/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.domainDiagram, /\| ref \|/);
|
||
|
||
assert.match(MODEL_WEAVE_TEMPLATES.colorScheme, /type: color_scheme/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.colorScheme, /\| target \| kind \| fill \| stroke \| text \| notes \|/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.colorScheme, /\| \| business \| #4f81bd \| #2f5597 \| #ffffff \| Global business color \|/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.colorScheme, /\| domain \| business \| #4f81bd \| #2f5597 \| #ffffff \| Domain-specific business color \|/);
|
||
|
||
assert.match(MODEL_WEAVE_TEMPLATES.flowDiagram, /type: flow_diagram/);
|
||
assert.match(MODEL_WEAVE_TEMPLATES.flowDiagram, /\| id \| from \| to \| kind \| trigger \| data \| condition \| notes \|/);
|
||
assert.doesNotMatch(MODEL_WEAVE_TEMPLATES.flowDiagram, /\| id \| from \| to \| data \| notes \|/);
|
||
});
|
||
|
||
test("diagram templates avoid deprecated render mode and unresolved Domain Sources", () => {
|
||
const externalTemplates = [
|
||
"Templates/dfd/dfd_diagram.template.md",
|
||
"Templates/class/class_diagram.template.md",
|
||
"Templates/er/er_diagram.template.md"
|
||
];
|
||
|
||
for (const templatePath of externalTemplates) {
|
||
const template = readFileSync(templatePath, "utf8");
|
||
assert.doesNotMatch(template, /render_mode:\s*auto\b/);
|
||
}
|
||
|
||
const dfdTemplate = readFileSync("Templates/dfd/dfd_diagram.template.md", "utf8");
|
||
assert.match(dfdTemplate, /render_mode:\s*mermaid\b/);
|
||
assert.doesNotMatch(dfdTemplate, /DOMAINS-EXAMPLE/);
|
||
|
||
assert.doesNotMatch(MODEL_WEAVE_TEMPLATES.classDiagram, /render_mode:\s*auto\b/);
|
||
assert.doesNotMatch(MODEL_WEAVE_TEMPLATES.erDiagram, /render_mode:\s*auto\b/);
|
||
assert.doesNotMatch(MODEL_WEAVE_TEMPLATES.dfdDiagram, /render_mode:\s*auto\b/);
|
||
assert.doesNotMatch(MODEL_WEAVE_TEMPLATES.dfdDiagram, /DOMAINS-EXAMPLE/);
|
||
});
|
||
|
||
test("normalizes default Domain view mode settings", () => {
|
||
assert.equal(DEFAULT_MODEL_WEAVE_SETTINGS.defaultDomainsViewMode, "mindmap");
|
||
assert.equal(DEFAULT_MODEL_WEAVE_SETTINGS.defaultDomainDiagramViewMode, "mindmap");
|
||
|
||
const configured = normalizeModelWeaveSettings({
|
||
defaultDomainsViewMode: "area",
|
||
defaultDomainDiagramViewMode: "tree"
|
||
});
|
||
assert.equal(configured.defaultDomainsViewMode, "area");
|
||
assert.equal(configured.defaultDomainDiagramViewMode, "tree");
|
||
|
||
const fallback = normalizeModelWeaveSettings({
|
||
defaultDomainsViewMode: "unknown",
|
||
defaultDomainDiagramViewMode: "mermaid"
|
||
});
|
||
assert.equal(fallback.defaultDomainsViewMode, "mindmap");
|
||
assert.equal(fallback.defaultDomainDiagramViewMode, "mindmap");
|
||
|
||
for (const value of ["mindmap", "area", "tree"]) {
|
||
const settings = normalizeModelWeaveSettings({
|
||
defaultDomainsViewMode: value,
|
||
defaultDomainDiagramViewMode: value
|
||
});
|
||
assert.equal(settings.defaultDomainsViewMode, value);
|
||
assert.equal(settings.defaultDomainDiagramViewMode, value);
|
||
}
|
||
|
||
const mixedCase = normalizeModelWeaveSettings({
|
||
defaultDomainsViewMode: "Tree",
|
||
defaultDomainDiagramViewMode: "AREA"
|
||
});
|
||
assert.equal(mixedCase.defaultDomainsViewMode, "tree");
|
||
assert.equal(mixedCase.defaultDomainDiagramViewMode, "area");
|
||
});
|
||
|
||
test("Domain view mode setting options use stable frontmatter labels", () => {
|
||
assert.deepEqual(
|
||
DOMAIN_VIEW_MODE_SETTING_OPTIONS,
|
||
[
|
||
{ value: "mindmap", label: "Mindmap" },
|
||
{ value: "area", label: "Area" },
|
||
{ value: "tree", label: "Tree" }
|
||
]
|
||
);
|
||
|
||
const labels = DOMAIN_VIEW_MODE_SETTING_OPTIONS.map((option) => option.label);
|
||
assert.equal(/領域|ツリー|マインドマップ/.test(labels.join(" ")), false);
|
||
|
||
const values = DOMAIN_VIEW_MODE_SETTING_OPTIONS.map((option) => option.value);
|
||
assert.deepEqual(values, ["mindmap", "area", "tree"]);
|
||
});
|
||
|
||
test("English settings labels for Domain view modes do not contain Japanese fixed labels", () => {
|
||
const en = createModelWeaveTranslator("en");
|
||
const labels = [
|
||
en("settings.defaultDomainsViewMode.name"),
|
||
en("settings.defaultDomainsViewMode.desc"),
|
||
en("settings.defaultDomainDiagramViewMode.name"),
|
||
en("settings.defaultDomainDiagramViewMode.desc"),
|
||
...DOMAIN_VIEW_MODE_SETTING_OPTIONS.map((option) => option.label)
|
||
];
|
||
|
||
assert.deepEqual(
|
||
labels.slice(0, 4),
|
||
[
|
||
"Default Domains view mode",
|
||
"Used when frontmatter.render_mode is not set for domains files.",
|
||
"Default Domain Diagram view mode",
|
||
"Used when frontmatter.render_mode is not set for domain_diagram files."
|
||
]
|
||
);
|
||
assert.equal(/領域|ツリー|マインドマップ|初期表示モード|表示モードです/.test(labels.join(" ")), false);
|
||
});
|
||
|
||
test("Domains render_mode supports domain-specific view modes", () => {
|
||
assert.deepEqual(getSupportedRenderModes("domains"), ["mindmap", "area", "tree"]);
|
||
assert.deepEqual(getSupportedRenderModes("domain-diagram"), ["mindmap", "area", "tree"]);
|
||
});
|
||
|
||
test("resolves Domains render_mode values without warnings", () => {
|
||
for (const [value, expected] of [
|
||
["tree", "tree"],
|
||
["Tree", "tree"],
|
||
["area", "area"],
|
||
["mindmap", "mindmap"]
|
||
]) {
|
||
const resolved = resolveRenderMode({
|
||
filePath: "model/domains/DOMAINS.md",
|
||
formatType: "domains",
|
||
frontmatterRenderMode: value,
|
||
settingsDefaultRenderMode: "area"
|
||
});
|
||
|
||
assert.equal(resolved.selectedMode, expected);
|
||
assert.equal(resolved.effectiveMode, expected);
|
||
assert.equal(resolved.source, "frontmatter");
|
||
assert.equal(resolved.diagnostics.length, 0);
|
||
}
|
||
});
|
||
|
||
test("resolves Domain Diagram render_mode values without warnings", () => {
|
||
for (const value of ["tree", "Tree"]) {
|
||
const resolved = resolveRenderMode({
|
||
filePath: "model/domains/DOMAIN-DIAGRAM.md",
|
||
formatType: "domain-diagram",
|
||
frontmatterRenderMode: value,
|
||
settingsDefaultRenderMode: "area"
|
||
});
|
||
|
||
assert.equal(resolved.selectedMode, "tree");
|
||
assert.equal(resolved.effectiveMode, "tree");
|
||
assert.equal(resolved.source, "frontmatter");
|
||
assert.equal(resolved.diagnostics.length, 0);
|
||
}
|
||
});
|
||
|
||
test("invalid Domain render_mode values still warn and fall back", () => {
|
||
const domains = resolveRenderMode({
|
||
filePath: "model/domains/DOMAINS.md",
|
||
formatType: "domains",
|
||
frontmatterRenderMode: "mermaid",
|
||
settingsDefaultRenderMode: "tree"
|
||
});
|
||
assert.equal(domains.selectedMode, "tree");
|
||
assert.equal(domains.source, "settings");
|
||
assert.equal(domains.diagnostics.length, 1);
|
||
assert.match(domains.diagnostics[0].message, /Unknown render_mode value "mermaid"/);
|
||
|
||
const diagram = resolveRenderMode({
|
||
filePath: "model/domains/DOMAIN-DIAGRAM.md",
|
||
formatType: "domain-diagram",
|
||
frontmatterRenderMode: "custom",
|
||
settingsDefaultRenderMode: "area"
|
||
});
|
||
assert.equal(diagram.selectedMode, "area");
|
||
assert.equal(diagram.source, "settings");
|
||
assert.equal(diagram.diagnostics.length, 1);
|
||
assert.match(diagram.diagnostics[0].message, /Unknown render_mode value "custom"/);
|
||
});
|
||
|
||
test("Domain UI labels are localized while values remain internal", () => {
|
||
const en = createModelWeaveTranslator("en");
|
||
const ja = createModelWeaveTranslator("ja");
|
||
|
||
const englishLabels = [
|
||
en("mermaid.source.title"),
|
||
en("domains.preview.mindmap"),
|
||
en("domains.preview.area"),
|
||
en("domains.preview.treeMode")
|
||
];
|
||
assert.deepEqual(englishLabels, ["Mermaid source", "Mindmap", "Area", "Tree"]);
|
||
for (const label of englishLabels) {
|
||
assert.equal(/Mermaid ソース|領域|ツリー|マインドマップ/.test(label), false);
|
||
}
|
||
|
||
assert.deepEqual(
|
||
[
|
||
ja("mermaid.source.title"),
|
||
ja("domains.preview.mindmap"),
|
||
ja("domains.preview.area"),
|
||
ja("domains.preview.treeMode")
|
||
],
|
||
["Mermaid ソース", "マインドマップ", "領域", "ツリー"]
|
||
);
|
||
|
||
const settings = normalizeModelWeaveSettings({
|
||
defaultDomainsViewMode: "Tree",
|
||
defaultDomainDiagramViewMode: "Area"
|
||
});
|
||
assert.equal(settings.defaultDomainsViewMode, "tree");
|
||
assert.equal(settings.defaultDomainDiagramViewMode, "area");
|
||
});
|
||
|
||
test("DFD detail labels use explicit English UI labels", () => {
|
||
const root = renderDfdMermaidDiagram(createResolvedDfdForDetails(), {
|
||
hideTitle: true,
|
||
forExport: true,
|
||
dfdDetailLabels: {
|
||
displayedObjects: "Displayed objects",
|
||
displayedFlows: "Displayed flows",
|
||
noObjects: "No objects are used for rendering.",
|
||
noFlows: "No flows are used for rendering.",
|
||
domainPlacement: "Domain placement",
|
||
resolved: "resolved",
|
||
unresolved: "unresolved"
|
||
}
|
||
});
|
||
const text = root.textContent;
|
||
|
||
assert.match(text, /Displayed objects \(3\)/);
|
||
assert.match(text, /Displayed flows \(0\)/);
|
||
assert.match(text, /No flows are used for rendering\./);
|
||
assert.match(text, /Domain placement \(1\/1 resolved\)/);
|
||
assert.doesNotMatch(text, /表示中の|描画に使われている|ありません/);
|
||
});
|
||
|
||
test("DFD detail labels use explicit Japanese UI labels", () => {
|
||
const root = renderDfdMermaidDiagram(createResolvedDfdForDetails(), {
|
||
hideTitle: true,
|
||
forExport: true,
|
||
dfdDetailLabels: {
|
||
displayedObjects: "表示中のオブジェクト",
|
||
displayedFlows: "表示中のフロー",
|
||
noObjects: "描画に使われているオブジェクトはありません。",
|
||
noFlows: "描画に使われているフローはありません。",
|
||
domainPlacement: "Domain 配置",
|
||
resolved: "解決済み",
|
||
unresolved: "未解決"
|
||
}
|
||
});
|
||
const text = root.textContent;
|
||
|
||
assert.match(text, /表示中のオブジェクト \(3\)/);
|
||
assert.match(text, /表示中のフロー \(0\)/);
|
||
assert.match(text, /描画に使われているフローはありません。/);
|
||
assert.match(text, /Domain 配置 \(1\/1 解決済み\)/);
|
||
assert.doesNotMatch(text, /表示中の objects|表示中の flows|flow はありません/);
|
||
});
|
||
|
||
test("class object context details use explicit English UI labels", () => {
|
||
const root = renderObjectContext(createObjectContextForDetails(), {
|
||
labels: {
|
||
title: "Related objects",
|
||
linked: (count) => `${count} linked`,
|
||
connectionDetails: "Connection details",
|
||
relationDetails: "Relation details",
|
||
noDirectlyRelated: "No directly related objects."
|
||
}
|
||
});
|
||
const text = root.textContent;
|
||
|
||
assert.match(text, /Connection details \(0\)/);
|
||
assert.match(text, /No directly related objects\./);
|
||
assert.doesNotMatch(text, /直接関係するオブジェクトはありません。/);
|
||
});
|
||
|
||
test("class object context details use explicit Japanese UI labels", () => {
|
||
const root = renderObjectContext(createObjectContextForDetails(), {
|
||
labels: {
|
||
title: "関連オブジェクト",
|
||
linked: (count) => `${count} 件の関連`,
|
||
connectionDetails: "接続詳細",
|
||
relationDetails: "Relation 詳細",
|
||
noDirectlyRelated: "直接関係するオブジェクトはありません。"
|
||
}
|
||
});
|
||
const text = root.textContent;
|
||
|
||
assert.match(text, /接続詳細 \(0\)/);
|
||
assert.match(text, /直接関係するオブジェクトはありません。/);
|
||
assert.doesNotMatch(text, /Connection details \(0\)|No directly related objects\./);
|
||
});
|
||
|
||
test("class diagram detail panel uses explicit English UI labels", () => {
|
||
const root = renderClassDiagram(createResolvedClassForDetails(), {
|
||
forExport: true,
|
||
classDetailLabels: {
|
||
displayedRelations: "Displayed relations",
|
||
noRelationsUsed: "No relations are currently used for rendering."
|
||
}
|
||
});
|
||
const text = root.textContent;
|
||
|
||
assert.match(text, /Displayed relations \(0\)/);
|
||
assert.match(text, /No relations are currently used for rendering\./);
|
||
assert.doesNotMatch(text, /描画に使われている関係はありません。/);
|
||
});
|
||
|
||
test("class diagram detail panel uses explicit Japanese UI labels", () => {
|
||
const root = renderClassDiagram(createResolvedClassForDetails(), {
|
||
forExport: true,
|
||
classDetailLabels: {
|
||
displayedRelations: "表示中の関係",
|
||
noRelationsUsed: "描画に使われている関係はありません。"
|
||
}
|
||
});
|
||
const text = root.textContent;
|
||
|
||
assert.match(text, /表示中の関係 \(0\)/);
|
||
assert.match(text, /描画に使われている関係はありません。/);
|
||
assert.doesNotMatch(text, /Displayed relations \(0\)|No relations are currently used/);
|
||
});
|
||
|
||
test("parses Color Scheme colors", () => {
|
||
const { file, warnings } = parseColorScheme(`---
|
||
type: color_scheme
|
||
title: Default color scheme
|
||
id: COLOR-SCHEME-DEFAULT
|
||
---
|
||
|
||
## Colors
|
||
|
||
| target | kind | fill | stroke | text | notes |
|
||
|---|---|---|---|---|---|
|
||
| domain | organization | #e3f2fd | #1976d2 | #111111 | 組織 |
|
||
| | default | #eee | #999 | #111 | default |
|
||
`);
|
||
|
||
assert.equal(file.fileType, "color-scheme");
|
||
assert.equal(file.colors.length, 2);
|
||
assert.equal(file.colors[0].target, "domain");
|
||
assert.equal(file.colors[0].kind, "organization");
|
||
assert.equal(file.colors[1].target, undefined);
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("diagnoses invalid Color Scheme rows", () => {
|
||
const { warnings } = parseColorScheme(`---
|
||
type: color_scheme
|
||
id: COLOR-SCHEME-DEFAULT
|
||
---
|
||
|
||
## Colors
|
||
|
||
| target | kind | fill | stroke | text | notes |
|
||
|---|---|---|---|---|---|
|
||
| domain | | #e3f2fd | #1976d2 | #111111 | missing kind |
|
||
| domain | organization | blue | #1976d2 | #111111 | invalid fill |
|
||
| domain | organization | #e3f2fd | #1976d2 | #111111 | duplicate |
|
||
`);
|
||
|
||
const messages = warnings.map((warning) => warning.message);
|
||
assert.ok(messages.includes("Color Scheme kind is required."));
|
||
assert.ok(messages.includes('Color Scheme fill "blue" is not a supported hex color.'));
|
||
assert.ok(messages.includes('duplicate Color Scheme entry for target "domain" and kind "organization"'));
|
||
});
|
||
|
||
test("allows global Color Scheme kind override rows", () => {
|
||
const { warnings } = parseColorScheme(`---
|
||
type: color_scheme
|
||
id: COLOR-SCHEME-DEFAULT
|
||
---
|
||
|
||
## Colors
|
||
|
||
| target | kind | fill | stroke | text | notes |
|
||
|---|---|---|---|---|---|
|
||
| | business | #4f81bd | #2f5597 | #ffffff | Global business color |
|
||
| domain | business | #70ad47 | #548235 | #ffffff | Domain-specific business color |
|
||
`);
|
||
|
||
assert.equal(
|
||
warnings.some((warning) => warning.message.includes("duplicate Color Scheme entry")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("diagnoses duplicate global Color Scheme kind rows", () => {
|
||
const { warnings } = parseColorScheme(`---
|
||
type: color_scheme
|
||
id: COLOR-SCHEME-DEFAULT
|
||
---
|
||
|
||
## Colors
|
||
|
||
| target | kind | fill | stroke | text | notes |
|
||
|---|---|---|---|---|---|
|
||
| | business | #4f81bd | #2f5597 | #ffffff | Global business color |
|
||
| | business | #70ad47 | #548235 | #ffffff | Duplicate global business |
|
||
`);
|
||
|
||
assert.ok(warnings.some((warning) =>
|
||
warning.message === 'duplicate Color Scheme entry for target "(default target)" and kind "business"'
|
||
));
|
||
});
|
||
|
||
test("resolves Color Scheme fallback and target-kind matches", () => {
|
||
const scheme = {
|
||
...BUILT_IN_COLOR_SCHEME,
|
||
entries: [
|
||
{
|
||
target: "domain",
|
||
kind: "organization",
|
||
fill: "#000",
|
||
stroke: "#111",
|
||
text: "#222",
|
||
rowIndex: 0
|
||
},
|
||
{
|
||
kind: "default",
|
||
fill: "#abc",
|
||
stroke: "#def",
|
||
text: "#123",
|
||
rowIndex: 1
|
||
}
|
||
]
|
||
};
|
||
|
||
assert.deepEqual(resolveColorStyle(scheme, "domain", "organization"), {
|
||
fill: "#000",
|
||
stroke: "#111",
|
||
text: "#222"
|
||
});
|
||
assert.deepEqual(resolveColorStyle(scheme, "domain", "department"), {
|
||
fill: "#e8f5e9",
|
||
stroke: "#388e3c",
|
||
text: "#111111"
|
||
});
|
||
assert.deepEqual(resolveColorStyle(scheme, "domain", "unknown"), {
|
||
fill: "#abc",
|
||
stroke: "#def",
|
||
text: "#123"
|
||
});
|
||
});
|
||
|
||
test("resolves global Color Scheme rules with target-specific override", () => {
|
||
const scheme = {
|
||
...BUILT_IN_COLOR_SCHEME,
|
||
entries: [
|
||
{
|
||
kind: "business",
|
||
fill: "#4f81bd",
|
||
stroke: "#2f5597",
|
||
text: "#ffffff",
|
||
rowIndex: 0
|
||
},
|
||
{
|
||
target: "domain",
|
||
kind: "business",
|
||
fill: "#70ad47",
|
||
stroke: "#548235",
|
||
text: "#ffffff",
|
||
rowIndex: 1
|
||
},
|
||
{
|
||
kind: "application",
|
||
fill: "#8064a2",
|
||
stroke: "#5f497a",
|
||
text: "#ffffff",
|
||
rowIndex: 2
|
||
}
|
||
]
|
||
};
|
||
|
||
assert.deepEqual(resolveColorStyle(scheme, "domain", "business"), {
|
||
fill: "#70ad47",
|
||
stroke: "#548235",
|
||
text: "#ffffff"
|
||
});
|
||
assert.deepEqual(resolveColorStyle(scheme, "domain", "application"), {
|
||
fill: "#8064a2",
|
||
stroke: "#5f497a",
|
||
text: "#ffffff"
|
||
});
|
||
|
||
const effective = getEffectiveColorSchemeEntriesForTarget(scheme, "domain");
|
||
const businessRows = effective.filter((entry) => entry.kind === "business");
|
||
assert.equal(businessRows.length, 1);
|
||
assert.equal(businessRows[0].target, "domain");
|
||
assert.ok(effective.some((entry) =>
|
||
entry.kind === "application" && entry.target === undefined
|
||
));
|
||
});
|
||
|
||
test("filters Applied Color Scheme rows for diagram targets", () => {
|
||
const scheme = {
|
||
...BUILT_IN_COLOR_SCHEME,
|
||
id: "custom",
|
||
entries: [
|
||
{ kind: "default", fill: "#111111", stroke: "#222222", text: "#ffffff", rowIndex: 0 },
|
||
{ kind: "business", fill: "#4f81bd", stroke: "#2f5597", text: "#ffffff", rowIndex: 1 },
|
||
{ target: "domain", kind: "business", fill: "#70ad47", stroke: "#548235", text: "#ffffff", rowIndex: 2 },
|
||
{ target: "dfd", kind: "process", fill: "#9bbb59", stroke: "#6f8a3f", text: "#000000", rowIndex: 3 },
|
||
{ target: "app_process", kind: "start", fill: "#e3f2fd", stroke: "#1976d2", text: "#111111", rowIndex: 4 },
|
||
{ target: "class", kind: "entity", fill: "#000000", stroke: "#000000", text: "#ffffff", rowIndex: 5 }
|
||
]
|
||
};
|
||
|
||
const domainRows = getAppliedColorSchemeRowsForTargets(scheme, ["domain"]);
|
||
assert.ok(domainRows.some((row) => row.entry.kind === "business" && row.entry.target === undefined));
|
||
assert.ok(domainRows.some((row) => row.entry.kind === "business" && row.entry.target === "domain"));
|
||
assert.ok(domainRows.some((row) => row.entry.kind === "organization" && row.entry.target === "domain" && row.source === "built-in"));
|
||
assert.equal(domainRows.some((row) => row.entry.target === "dfd"), false);
|
||
assert.equal(domainRows.some((row) => row.entry.target === "class"), false);
|
||
|
||
const dfdRows = getAppliedColorSchemeRowsForTargets(scheme, ["dfd", "domain"]);
|
||
assert.ok(dfdRows.some((row) => row.entry.kind === "process" && row.entry.target === "dfd"));
|
||
assert.ok(dfdRows.some((row) => row.entry.kind === "business" && row.entry.target === "domain"));
|
||
assert.equal(dfdRows.some((row) => row.entry.target === "app_process"), false);
|
||
assert.equal(dfdRows.some((row) => row.entry.target === "class"), false);
|
||
|
||
const appProcessRows = getAppliedColorSchemeRowsForTargets(scheme, ["app_process"]);
|
||
assert.ok(appProcessRows.some((row) => row.entry.kind === "start" && row.entry.target === "app_process"));
|
||
assert.ok(appProcessRows.some((row) => row.entry.kind === "default" && row.entry.target === undefined));
|
||
assert.equal(appProcessRows.some((row) => row.entry.target === "domain"), false);
|
||
assert.equal(appProcessRows.some((row) => row.entry.target === "dfd"), false);
|
||
|
||
const builtInRows = getAppliedColorSchemeRowsForTargets(undefined, ["domain"]);
|
||
assert.ok(builtInRows.some((row) => row.entry.kind === "organization" && row.source === "built-in"));
|
||
assert.ok(builtInRows.some((row) => row.entry.kind === "default" && row.source === "built-in"));
|
||
});
|
||
|
||
test("resolves configured default Color Scheme from vault index", () => {
|
||
const index = buildVaultIndex([
|
||
{
|
||
path: "model/config/COLOR-SCHEME-DEFAULT.md",
|
||
content: `---
|
||
type: color_scheme
|
||
id: COLOR-SCHEME-DEFAULT
|
||
---
|
||
|
||
## Colors
|
||
|
||
| target | kind | fill | stroke | text | notes |
|
||
|---|---|---|---|---|---|
|
||
| domain | organization | #000 | #111 | #222 | custom |
|
||
`
|
||
}
|
||
]);
|
||
|
||
const resolved = resolveDefaultColorScheme(index, "[[COLOR-SCHEME-DEFAULT]]");
|
||
assert.equal(resolved.colorScheme.id, "COLOR-SCHEME-DEFAULT");
|
||
assert.equal(resolved.warnings.length, 0);
|
||
assert.deepEqual(resolveColorStyle(resolved.colorScheme, "domain", "organization"), {
|
||
fill: "#000",
|
||
stroke: "#111",
|
||
text: "#222"
|
||
});
|
||
|
||
const fallback = resolveDefaultColorScheme(index, "[[MISSING]]");
|
||
assert.equal(fallback.colorScheme.id, "built-in-default");
|
||
assert.ok(fallback.warnings.some((warning) =>
|
||
warning.message.includes('Default Color Scheme ref "[[MISSING]]" could not be resolved')
|
||
));
|
||
});
|
||
|
||
test("diagnoses missing Domain Diagram source ref", () => {
|
||
const { warnings } = parseDomainDiagram(`---
|
||
type: domain_diagram
|
||
id: DOMAIN-DIAGRAM-LOGISTICS
|
||
---
|
||
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| | missing |
|
||
`);
|
||
|
||
assert.ok(warnings.some((warning) =>
|
||
warning.message === "Domain Source ref is required."
|
||
));
|
||
});
|
||
|
||
test("merges Domain Diagram sources with later source override", () => {
|
||
const company = parseDomainsFile(`---
|
||
type: domains
|
||
id: DOMAINS-COMPANY
|
||
---
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | |
|
||
| logistics | 物流部 | department | company | earlier |
|
||
`, "DOMAINS-COMPANY.md").file;
|
||
const warehouse = parseDomainsFile(`---
|
||
type: domains
|
||
id: DOMAINS-WAREHOUSE
|
||
---
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | 物流 | department | | later description ignored |
|
||
| warehouse | 倉庫 | location | logistics | |
|
||
`, "DOMAINS-WAREHOUSE.md").file;
|
||
|
||
assert.ok(company);
|
||
assert.ok(warehouse);
|
||
const result = mergeDomainDiagramSources([
|
||
{ source: company },
|
||
{ source: warehouse }
|
||
], "DOMAIN-DIAGRAM-LOGISTICS.md");
|
||
|
||
const logistics = result.domains.find((domain) => domain.id === "logistics");
|
||
assert.equal(logistics?.name, "物流");
|
||
assert.equal(logistics?.parent, undefined);
|
||
assert.ok(result.conflicts.some((conflict) =>
|
||
conflict.domainId === "logistics" && conflict.field === "duplicate"
|
||
));
|
||
assert.ok(result.conflicts.some((conflict) =>
|
||
conflict.domainId === "logistics" && conflict.field === "parent"
|
||
));
|
||
assert.ok(result.conflicts.some((conflict) =>
|
||
conflict.domainId === "logistics" && conflict.field === "name"
|
||
));
|
||
assert.equal(
|
||
result.conflicts.some((conflict) => conflict.field === "description"),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("diagnoses Domain Diagram kind conflicts", () => {
|
||
const first = parseDomainsFile(`---
|
||
type: domains
|
||
id: DOMAINS-A
|
||
---
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| wms | WMS | system | | |
|
||
`, "DOMAINS-A.md").file;
|
||
const second = parseDomainsFile(`---
|
||
type: domains
|
||
id: DOMAINS-B
|
||
---
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| wms | WMS | service | | |
|
||
`, "DOMAINS-B.md").file;
|
||
|
||
assert.ok(first);
|
||
assert.ok(second);
|
||
const result = mergeDomainDiagramSources([
|
||
{ source: first },
|
||
{ source: second }
|
||
]);
|
||
|
||
assert.ok(result.conflicts.some((conflict) =>
|
||
conflict.domainId === "wms" &&
|
||
conflict.field === "kind" &&
|
||
conflict.severity === "error"
|
||
));
|
||
});
|
||
|
||
test("resolves Domain Diagram sources through vault index", () => {
|
||
const index = buildVaultIndex([
|
||
domainsFile(
|
||
"model/domains/DOMAINS-COMPANY.md",
|
||
"DOMAINS-COMPANY",
|
||
"| company | 会社全体 | organization | | |\n| logistics | 物流部 | department | company | |"
|
||
),
|
||
domainsFile(
|
||
"model/domains/DOMAINS-WAREHOUSE.md",
|
||
"DOMAINS-WAREHOUSE",
|
||
"| warehouse | 倉庫 | location | logistics | |\n| team_1 | 係1 | team | warehouse | |"
|
||
),
|
||
{
|
||
path: "model/domains/DOMAIN-DIAGRAM-LOGISTICS.md",
|
||
content: `---
|
||
type: domain_diagram
|
||
id: DOMAIN-DIAGRAM-LOGISTICS
|
||
---
|
||
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | 会社・部門 |
|
||
| [[DOMAINS-WAREHOUSE]] | 倉庫・係 |
|
||
`
|
||
}
|
||
]);
|
||
const diagram = index.modelsByFilePath["model/domains/DOMAIN-DIAGRAM-LOGISTICS.md"];
|
||
assert.equal(diagram.fileType, "domain-diagram");
|
||
|
||
const resolved = resolveDomainDiagram(diagram, index);
|
||
assert.equal(resolved.sourceSummaries.length, 2);
|
||
assert.deepEqual(
|
||
resolved.domains.map((domain) => domain.id),
|
||
["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(
|
||
buildDomainHierarchyMermaid(resolved.domains, BUILT_IN_COLOR_SCHEME),
|
||
/style domain_company fill:#e3f2fd,stroke:#1976d2,color:#111111/
|
||
);
|
||
assert.match(buildDomainTreeViewMermaid(resolved.domains), /domain_company --> domain_logistics/);
|
||
assert.match(
|
||
buildDomainTreeViewMermaid(resolved.domains, BUILT_IN_COLOR_SCHEME),
|
||
/class domain_company kind_domain_organization/
|
||
);
|
||
});
|
||
|
||
test("diagnoses unresolved and non-Domains Domain Diagram sources", () => {
|
||
const index = buildVaultIndex([
|
||
{
|
||
path: "model/rules/RULE-ORDER.md",
|
||
content: `---
|
||
type: rule
|
||
id: RULE-ORDER
|
||
name: Order rule
|
||
---
|
||
`
|
||
},
|
||
{
|
||
path: "model/domains/DOMAIN-DIAGRAM-LOGISTICS.md",
|
||
content: `---
|
||
type: domain_diagram
|
||
id: DOMAIN-DIAGRAM-LOGISTICS
|
||
---
|
||
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[MISSING-DOMAINS]] | missing |
|
||
| [[RULE-ORDER]] | wrong type |
|
||
`
|
||
}
|
||
]);
|
||
const diagram = index.modelsByFilePath["model/domains/DOMAIN-DIAGRAM-LOGISTICS.md"];
|
||
assert.equal(diagram.fileType, "domain-diagram");
|
||
|
||
const resolved = resolveDomainDiagram(diagram, index);
|
||
assert.ok(resolved.warnings.some((warning) =>
|
||
warning.message.includes('Domain Source ref "[[MISSING-DOMAINS]]" could not be resolved')
|
||
));
|
||
assert.ok(resolved.warnings.some((warning) =>
|
||
warning.message.includes('expected type "domains"')
|
||
));
|
||
assert.ok(resolved.warnings.some((warning) =>
|
||
warning.message === "Domain Diagram has no valid Domain Sources."
|
||
));
|
||
});
|
||
|
||
test("localizes Domain Diagram diagnostics", () => {
|
||
const messages = [
|
||
"Domain Source ref is required.",
|
||
'Domain Source ref "[[MISSING]]" could not be resolved. Check the ID or file name.',
|
||
'Domain Source ref "[[RULE]]" resolves to type "rule", but expected type "domains".',
|
||
"Domain Diagram has no valid Domain Sources.",
|
||
'Domain Source ref "[[EMPTY]]" has no Domain rows.',
|
||
'Domain "logistics" is defined by multiple Domain Diagram sources: "A.md" and "B.md".',
|
||
'Domain "logistics" has conflicting parent values between Domain Diagram sources "A.md" and "B.md".'
|
||
].map((message) => localizeDiagnosticMessage(message, "ja"));
|
||
|
||
assert.ok(messages[0].includes("ref が必要です"));
|
||
assert.ok(messages[1].includes("参照先が見つかりません"));
|
||
assert.ok(messages[2].includes('type "domains" が必要です'));
|
||
assert.ok(messages[3].includes("有効な Domain Sources がありません"));
|
||
assert.ok(messages[4].includes("Domain 行がありません"));
|
||
assert.ok(messages[5].includes("複数の Domain Diagram source"));
|
||
assert.ok(messages[6].includes("parent が Domain Diagram source"));
|
||
});
|
||
|
||
test("localizes Color Scheme diagnostics", () => {
|
||
const messages = [
|
||
"Color Scheme kind is required.",
|
||
'Color Scheme fill "blue" is not a supported hex color.',
|
||
'duplicate Color Scheme entry for target "domain" and kind "system"',
|
||
'Default Color Scheme ref "[[MISSING]]" could not be resolved. Built-in colors will be used.',
|
||
'Default Color Scheme ref "[[RULE]]" resolves to type "rule", but expected type "color_scheme". Built-in colors will be used.'
|
||
].map((message) => localizeDiagnosticMessage(message, "ja"));
|
||
|
||
assert.ok(messages[0].includes("kind が必要です"));
|
||
assert.ok(messages[1].includes("hex color ではありません"));
|
||
assert.ok(messages[2].includes("重複しています"));
|
||
assert.ok(messages[3].includes("組み込み色を使います"));
|
||
assert.ok(messages[4].includes('type "color_scheme" が必要です'));
|
||
});
|
||
|
||
test("builds standalone Domain relationship summaries", () => {
|
||
const core = `${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | company | Logistics area |
|
||
| warehouse | Warehouse | location | logistics | Warehouse |
|
||
| wms | WMS | system | logistics | Warehouse management |
|
||
`;
|
||
const duplicate = `---
|
||
type: domains
|
||
id: DOMAINS-ALT
|
||
name: Alternate Domains
|
||
---
|
||
|
||
# Alternate Domains
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Alternate logistics |
|
||
| wms | WMS local | application | logistics | Different description |
|
||
`;
|
||
const dfd = `${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | department | | Local logistics |
|
||
| wms | WMS | system | logistics | Local WMS |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | 出荷依頼受付 | process | | wms | |
|
||
| inventory_db | 在庫DB | datastore | | wms | |
|
||
| operator | Operator | external | | logistics | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| f1 | operator | receive_order | Request | |
|
||
`;
|
||
const index = buildVaultIndex([
|
||
{ path: "DOMAINS-CORE.md", content: core },
|
||
{ path: "DOMAINS-ALT.md", content: duplicate },
|
||
{ path: "DFD-SHIPPING.md", content: dfd }
|
||
], { parseMode: "full" });
|
||
const model = index.modelsByFilePath["DOMAINS-CORE.md"];
|
||
assert.equal(model.fileType, "domains");
|
||
|
||
const summaries = buildDomainRelationshipSummaries(model, index);
|
||
const logistics = summaries.find((entry) => entry.domain.id === "logistics");
|
||
const wms = summaries.find((entry) => entry.domain.id === "wms");
|
||
const warehouse = summaries.find((entry) => entry.domain.id === "warehouse");
|
||
|
||
assert.ok(logistics);
|
||
assert.deepEqual(logistics.childIds, ["warehouse", "wms"]);
|
||
assert.equal(logistics.parentId, "company");
|
||
assert.deepEqual(logistics.definedIn.map((entry) => entry.path), [
|
||
"DOMAINS-ALT.md",
|
||
"DOMAINS-CORE.md"
|
||
]);
|
||
assert.deepEqual(logistics.conflicts, ["parent"]);
|
||
assert.deepEqual(logistics.dfdLocalDomainReferences.map((entry) => entry.path), [
|
||
"DFD-SHIPPING.md"
|
||
]);
|
||
assert.deepEqual(logistics.dfdObjectReferences.map((entry) => entry.objectId), [
|
||
"operator"
|
||
]);
|
||
|
||
assert.ok(wms);
|
||
assert.deepEqual(wms.conflicts, ["name", "kind"]);
|
||
assert.deepEqual(wms.dfdObjectReferences.map((entry) => `${entry.objectId}: ${entry.label}`), [
|
||
"inventory_db: 在庫DB",
|
||
"receive_order: 出荷依頼受付"
|
||
]);
|
||
|
||
assert.ok(warehouse);
|
||
assert.deepEqual(warehouse.definedIn.map((entry) => entry.path), ["DOMAINS-CORE.md"]);
|
||
assert.deepEqual(warehouse.dfdLocalDomainReferences, []);
|
||
assert.deepEqual(warehouse.dfdObjectReferences, []);
|
||
});
|
||
|
||
test("builds a simple standalone Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| logistics | Logistics | organization | | Logistics area |
|
||
| warehouse | Warehouse | location | logistics | Warehouse operation |
|
||
| wms | WMS | system | warehouse | Warehouse management |
|
||
`);
|
||
|
||
const tree = buildDomainTree(file.domains);
|
||
assert.equal(tree.length, 1);
|
||
assert.equal(tree[0].domain.id, "logistics");
|
||
assert.equal(tree[0].children[0].domain.id, "warehouse");
|
||
assert.equal(tree[0].children[0].children[0].domain.id, "wms");
|
||
});
|
||
|
||
test("DFD files without local Domains remain compatible", () => {
|
||
const { file, warnings } = parseDfd(`${dfdFrontmatter}
|
||
## Objects
|
||
|
||
| id | label | kind | ref | notes |
|
||
|---|---|---|---|---|
|
||
| user | User | external | | User |
|
||
| pick | Pick items | process | | Pick |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| request | user | pick | Request | User request |
|
||
`);
|
||
|
||
assert.deepEqual(file.domains, []);
|
||
assert.equal(file.nodes.length, 2);
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
function dfdDataWarningMarkdown(dataValue) {
|
||
return `${dfdFrontmatter}
|
||
## Objects
|
||
|
||
| id | label | kind | ref | notes |
|
||
|---|---|---|---|---|
|
||
| user | User | external | | User |
|
||
| pick | Pick items | process | | Pick |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| request | user | pick | ${dataValue} | User request |
|
||
`;
|
||
}
|
||
|
||
function getDfdFlowDataWarnings(dataValue, extraFiles = []) {
|
||
const { resolved } = resolveDfdWithFiles([
|
||
{ path: "DFD-SHIPPING.md", content: dfdDataWarningMarkdown(dataValue) },
|
||
...extraFiles
|
||
]);
|
||
return resolved.warnings.filter((warning) =>
|
||
warning.code === "unresolved-reference" && /flow data reference/.test(warning.message)
|
||
);
|
||
}
|
||
|
||
test("dfd_diagram Flows.data plain text does not warn", () => {
|
||
assert.equal(getDfdFlowDataWarnings("DATA-ORDER-DRAFT").length, 0);
|
||
assert.equal(getDfdFlowDataWarnings("Search condition").length, 0);
|
||
});
|
||
|
||
test("dfd_diagram Flows.data empty value does not warn", () => {
|
||
assert.equal(getDfdFlowDataWarnings("").length, 0);
|
||
});
|
||
|
||
test("dfd_diagram Flows.data resolved Wikilink does not warn", () => {
|
||
const warnings = getDfdFlowDataWarnings("[[DATA-ORDER-DRAFT]]", [
|
||
{ path: "DATA-ORDER-DRAFT.md", content: "---\ntype: data_object\nid: DATA-ORDER-DRAFT\nname: Order Draft\n---\n" }
|
||
]);
|
||
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("dfd_diagram Flows.data unresolved Wikilink emits unresolved-reference warning", () => {
|
||
const warnings = getDfdFlowDataWarnings("[[DATA-NOT-EXISTS]]");
|
||
|
||
assert.equal(warnings.length, 1);
|
||
assert.equal(warnings[0].message, 'DFD flow data reference "[[DATA-NOT-EXISTS]]" could not be resolved. Check the data/model id or file name.');
|
||
assert.equal(warnings[0].severity, "warning");
|
||
assert.equal(warnings[0].field, "data");
|
||
assert.equal(warnings[0].context.section, "Flows");
|
||
assert.equal(warnings[0].context.rowIndex, 1);
|
||
assert.equal(warnings[0].context.referenceValue, "[[DATA-NOT-EXISTS]]");
|
||
assert.equal(getExpectedHeaderForDiagnostic(warnings[0]), null);
|
||
assert.notEqual(warnings[0].context.referenceKind, "local-object-id");
|
||
});
|
||
|
||
test("dfd_diagram Flows.data multiple Wikilinks warn only for unresolved links", () => {
|
||
const warnings = getDfdFlowDataWarnings("[[DATA-A]], [[DATA-B]]", [
|
||
{ path: "DATA-A.md", content: "---\ntype: data_object\nid: DATA-A\nname: Data A\n---\n" }
|
||
]);
|
||
|
||
assert.deepEqual(warnings.map((warning) => warning.context.referenceValue), ["[[DATA-B]]"]);
|
||
});
|
||
|
||
test("localizes DFD flow data reference diagnostics", () => {
|
||
assert.equal(
|
||
localizeDiagnosticMessage('DFD flow data reference "[[DATA-NOT-EXISTS]]" could not be resolved. Check the data/model id or file name.', "ja"),
|
||
'DFD flow data reference "[[DATA-NOT-EXISTS]]" の参照先が見つかりません。data/model の id またはファイル名を確認してください。'
|
||
);
|
||
});
|
||
|
||
test("DFD Objects refs accept non-DFD Model Weave assets without unresolved warnings", () => {
|
||
const dfd = `---
|
||
type: dfd_diagram
|
||
id: DFD-MIXED-ASSETS
|
||
name: Mixed Asset DFD
|
||
---
|
||
|
||
# Mixed Asset DFD
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | notes |
|
||
|---|---|---|---|---|
|
||
| screen | Edit form | external | [[SCR-MIXED-ASSET]] | |
|
||
| process | Update issue | process | [[PROC-MIXED-ASSET]] | |
|
||
| rule | Safe attrs | process | [[RULE-MIXED-ASSET]] | |
|
||
| mapping | Notification trace | process | [[MAP-MIXED-ASSET]] | |
|
||
| data | Issue data | datastore | [[DATA-MIXED-ASSET]] | |
|
||
| entity | Issue entity | datastore | [[ENT-MIXED-ASSET]] | |
|
||
| erd | Issue ERD | other | [[ERD-MIXED-ASSET]] | |
|
||
| classd | Class diagram | other | [[CLD-MIXED-ASSET]] | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| f1 | screen | process | Issue update request | |
|
||
| f2 | process | data | Issue update result | |
|
||
`;
|
||
const index = buildVaultIndex([
|
||
{ path: "DFD-MIXED-ASSETS.md", content: dfd },
|
||
{ path: "SCR-MIXED-ASSET.md", content: `---
|
||
type: screen
|
||
id: SCR-MIXED-ASSET
|
||
name: Edit form
|
||
---
|
||
|
||
# Edit form
|
||
` },
|
||
{ path: "PROC-MIXED-ASSET.md", content: `---
|
||
type: app_process
|
||
id: PROC-MIXED-ASSET
|
||
name: Update issue
|
||
---
|
||
|
||
# Update issue
|
||
` },
|
||
{ path: "RULE-MIXED-ASSET.md", content: `---
|
||
type: rule
|
||
id: RULE-MIXED-ASSET
|
||
name: Safe attrs
|
||
kind: validation
|
||
---
|
||
|
||
# Safe attrs
|
||
` },
|
||
{ path: "MAP-MIXED-ASSET.md", content: `---
|
||
type: mapping
|
||
id: MAP-MIXED-ASSET
|
||
name: Notification trace
|
||
source: [[DATA-MIXED-ASSET]]
|
||
target: [[DATA-MIXED-ASSET]]
|
||
---
|
||
|
||
# Notification trace
|
||
` },
|
||
{ path: "DATA-MIXED-ASSET.md", content: `---
|
||
type: data_object
|
||
id: DATA-MIXED-ASSET
|
||
name: Issue data
|
||
---
|
||
|
||
# Issue data
|
||
` },
|
||
{ path: "ENT-MIXED-ASSET.md", content: `---
|
||
type: er_entity
|
||
id: ENT-MIXED-ASSET
|
||
logical_name: Issue
|
||
physical_name: issues
|
||
---
|
||
|
||
# Issue
|
||
` },
|
||
{ path: "ERD-MIXED-ASSET.md", content: `---
|
||
type: er_diagram
|
||
id: ERD-MIXED-ASSET
|
||
name: Issue ERD
|
||
---
|
||
|
||
# Issue ERD
|
||
` },
|
||
{ path: "CLD-MIXED-ASSET.md", content: `---
|
||
type: class_diagram
|
||
id: CLD-MIXED-ASSET
|
||
name: Issue classes
|
||
---
|
||
|
||
# Issue classes
|
||
` }
|
||
], { parseMode: "full" });
|
||
|
||
const model = index.modelsByFilePath["DFD-MIXED-ASSETS.md"];
|
||
assert.equal(model.fileType, "dfd-diagram");
|
||
const resolved = resolveDiagramRelations(model, index);
|
||
const messages = resolved.warnings.map((warning) => warning.message);
|
||
const validationMessages = (index.warningsByFilePath["DFD-MIXED-ASSETS.md"] ?? [])
|
||
.map((warning) => warning.message);
|
||
|
||
assert.deepEqual(resolved.missingObjects, []);
|
||
assert.equal(messages.some((message) => message.includes("unresolved DFD object ref")), false);
|
||
assert.equal(validationMessages.some((message) => message.includes("unresolved object ref")), false);
|
||
assert.equal(resolved.nodes.find((node) => node.id === "screen")?.label, "Edit form");
|
||
assert.equal(resolved.nodes.find((node) => node.id === "screen")?.kind, "external");
|
||
assert.equal(resolved.nodes.find((node) => node.id === "process")?.label, "Update issue");
|
||
assert.equal(resolved.nodes.find((node) => node.id === "process")?.kind, "process");
|
||
});
|
||
|
||
test("class Relations.to accepts non-class Model Weave assets without unresolved warnings", () => {
|
||
const index = buildVaultIndex([
|
||
{ path: "CLS-MIXED-RELATIONS.md", content: `---
|
||
type: class
|
||
id: CLS-MIXED-RELATIONS
|
||
name: IssueClass
|
||
kind: class
|
||
---
|
||
|
||
# IssueClass
|
||
|
||
## Relations
|
||
|
||
| id | to | kind | label | from_multiplicity | to_multiplicity | notes |
|
||
|---|---|---|---|---|---|---|
|
||
| rel-class | [[CLS-RELATED-CLASS]] | dependency | related class | | | |
|
||
| rel-entity | [[ENT-MIXED-RELATION]] | dependency | entity | | | |
|
||
| rel-process | [[PROC-MIXED-RELATION]] | dependency | process | | | |
|
||
| rel-rule | [[RULE-MIXED-RELATION]] | dependency | rule | | | |
|
||
| rel-data | [[DATA-MIXED-RELATION]] | dependency | data | | | |
|
||
| rel-missing | [[MISSING-MIXED-RELATION]] | dependency | missing | | | |
|
||
` },
|
||
{ path: "CLS-RELATED-CLASS.md", content: `---
|
||
type: class
|
||
id: CLS-RELATED-CLASS
|
||
name: RelatedClass
|
||
kind: class
|
||
---
|
||
|
||
# RelatedClass
|
||
` },
|
||
{ path: "CLD-MIXED-RELATIONS.md", content: `---
|
||
type: class_diagram
|
||
id: CLD-MIXED-RELATIONS
|
||
name: Mixed relation diagram
|
||
---
|
||
|
||
# Mixed relation diagram
|
||
|
||
## Objects
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[CLS-MIXED-RELATIONS]] | Focus |
|
||
| [[CLS-RELATED-CLASS]] | Related |
|
||
` },
|
||
{ path: "CLD-MIXED-EXPLICIT-RELATIONS.md", content: `---
|
||
type: class_diagram
|
||
id: CLD-MIXED-EXPLICIT-RELATIONS
|
||
name: Explicit mixed relation diagram
|
||
---
|
||
|
||
# Explicit mixed relation diagram
|
||
|
||
## Objects
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[CLS-MIXED-RELATIONS]] | Focus |
|
||
| [[CLS-RELATED-CLASS]] | Related |
|
||
|
||
## Relations
|
||
|
||
| id | from | to | kind | label | from_multiplicity | to_multiplicity | notes |
|
||
|---|---|---|---|---|---|---|
|
||
| diagram-class | [[CLS-MIXED-RELATIONS]] | [[CLS-RELATED-CLASS]] | dependency | related class | | | |
|
||
| diagram-entity | [[CLS-MIXED-RELATIONS]] | [[ENT-MIXED-RELATION]] | dependency | entity | | | |
|
||
` },
|
||
{ path: "ENT-MIXED-RELATION.md", content: `---
|
||
type: er_entity
|
||
id: ENT-MIXED-RELATION
|
||
logical_name: Issue
|
||
physical_name: issues
|
||
---
|
||
|
||
# Issue
|
||
` },
|
||
{ path: "PROC-MIXED-RELATION.md", content: `---
|
||
type: app_process
|
||
id: PROC-MIXED-RELATION
|
||
name: Update issue
|
||
---
|
||
|
||
# Update issue
|
||
` },
|
||
{ path: "RULE-MIXED-RELATION.md", content: `---
|
||
type: rule
|
||
id: RULE-MIXED-RELATION
|
||
name: Journal rule
|
||
---
|
||
|
||
# Journal rule
|
||
` },
|
||
{ path: "DATA-MIXED-RELATION.md", content: `---
|
||
type: data_object
|
||
id: DATA-MIXED-RELATION
|
||
name: Safe attributes
|
||
---
|
||
|
||
# Safe attributes
|
||
` }
|
||
], { parseMode: "full" });
|
||
|
||
const model = index.modelsByFilePath["CLS-MIXED-RELATIONS.md"];
|
||
assert.equal(model.fileType, "object");
|
||
|
||
const context = resolveObjectContext(model, index);
|
||
const diagnostics = buildCurrentObjectDiagnostics(
|
||
model,
|
||
index,
|
||
context,
|
||
index.warningsByFilePath["CLS-MIXED-RELATIONS.md"] ?? []
|
||
);
|
||
const messages = diagnostics.map((warning) => warning.message);
|
||
|
||
assert.equal(messages.some((message) => message.includes('unresolved related object "ENT-MIXED-RELATION"')), false);
|
||
assert.equal(messages.some((message) => message.includes('unresolved class relation target "ENT-MIXED-RELATION"')), false);
|
||
assert.equal(messages.some((message) => message.includes('unresolved class relation target "MISSING-MIXED-RELATION"')), true);
|
||
assert.equal(
|
||
diagnostics.filter((warning) => warning.code === "class-relation-target-not-diagram-compatible").length,
|
||
4
|
||
);
|
||
assert.ok(
|
||
messages.some((message) =>
|
||
message.includes('class relation target "ENT-MIXED-RELATION" exists, but is not compatible with Class Diagram rendering and was excluded. Consider representing non-structural cross-model relationships with Mapping.')
|
||
)
|
||
);
|
||
assert.ok(context.relatedObjects.some((entry) => entry.relatedObjectId === "ENT-MIXED-RELATION"));
|
||
assert.ok(context.relatedObjects.some((entry) => entry.relatedObjectId === "PROC-MIXED-RELATION"));
|
||
|
||
const diagram = index.modelsByFilePath["CLD-MIXED-RELATIONS.md"];
|
||
assert.equal(diagram.fileType, "diagram");
|
||
const resolved = resolveDiagramRelations(diagram, index);
|
||
const relationMessages = resolved.warnings.map((warning) => warning.message);
|
||
|
||
assert.equal(
|
||
relationMessages.some((message) => message.includes('unresolved class relation endpoint in relation "rel-entity"')),
|
||
false
|
||
);
|
||
assert.equal(
|
||
relationMessages.some((message) => message.includes('unresolved class relation endpoint in relation "rel-missing"')),
|
||
true
|
||
);
|
||
assert.equal(
|
||
resolved.warnings.filter((warning) => warning.code === "class-relation-target-not-diagram-compatible").length,
|
||
4
|
||
);
|
||
assert.equal(resolved.edges.length, 1);
|
||
assert.equal(resolved.edges[0]?.target, "CLS-RELATED-CLASS");
|
||
|
||
const explicitDiagram = index.modelsByFilePath["CLD-MIXED-EXPLICIT-RELATIONS.md"];
|
||
assert.equal(explicitDiagram.fileType, "diagram");
|
||
const explicitResolved = resolveDiagramRelations(explicitDiagram, index);
|
||
const explicitMessages = explicitResolved.warnings.map((warning) => warning.message);
|
||
|
||
assert.equal(
|
||
explicitMessages.some((message) => message.includes('unresolved relation endpoint in relation "diagram-entity"')),
|
||
false
|
||
);
|
||
assert.equal(
|
||
explicitResolved.warnings.filter((warning) => warning.code === "class-relation-target-not-diagram-compatible").length,
|
||
1
|
||
);
|
||
assert.equal(explicitResolved.edges.length, 1);
|
||
assert.equal(explicitResolved.edges[0]?.target, "CLS-RELATED-CLASS");
|
||
});
|
||
|
||
test("reference diagnostics resolve multiple wikilinks in one cell individually", () => {
|
||
const index = buildVaultIndex([
|
||
{ path: "RULE-MULTI-WIKILINK-SOURCES.md", content: `---
|
||
type: rule
|
||
id: RULE-MULTI-WIKILINK-SOURCES
|
||
name: Multi wikilink sources
|
||
kind: validation
|
||
---
|
||
|
||
# Multi wikilink sources
|
||
|
||
## Summary
|
||
|
||
Validate source references.
|
||
|
||
## Inputs
|
||
|
||
| id | data | source | required | notes |
|
||
|---|---|---|---|---|
|
||
| IN-AND | [[ENT-MULTI-WIKILINK]] | [[PROC-A]] and [[PROC-B]] | Y | Both process refs resolve |
|
||
| IN-SLASH | [[ENT-MULTI-WIKILINK]] | [[PROC-A]] / [[PROC-B]] | Y | Slash separated refs resolve |
|
||
| IN-MISSING | [[ENT-MULTI-WIKILINK]] | [[PROC-A]] and [[PROC-MISSING]] | Y | One missing process |
|
||
| IN-NATURAL | [[ENT-MULTI-WIKILINK]] | Issue current_journal | Y | Natural language source |
|
||
|
||
## Conditions
|
||
|
||
- Inputs are checked.
|
||
` },
|
||
{ path: "ENT-MULTI-WIKILINK.md", content: `---
|
||
type: er_entity
|
||
id: ENT-MULTI-WIKILINK
|
||
logical_name: Issue
|
||
physical_name: issues
|
||
---
|
||
|
||
# Issue
|
||
` },
|
||
{ path: "PROC-A.md", content: `---
|
||
type: app_process
|
||
id: PROC-A
|
||
name: Process A
|
||
---
|
||
|
||
# Process A
|
||
` },
|
||
{ path: "PROC-B.md", content: `---
|
||
type: app_process
|
||
id: PROC-B
|
||
name: Process B
|
||
---
|
||
|
||
# Process B
|
||
` }
|
||
], { parseMode: "full" });
|
||
|
||
const model = index.modelsByFilePath["RULE-MULTI-WIKILINK-SOURCES.md"];
|
||
assert.equal(model.fileType, "rule");
|
||
const diagnostics = buildCurrentObjectDiagnostics(
|
||
model,
|
||
index,
|
||
null,
|
||
index.warningsByFilePath["RULE-MULTI-WIKILINK-SOURCES.md"] ?? []
|
||
);
|
||
const sourceMessages = diagnostics
|
||
.map((warning) => warning.message)
|
||
.filter((message) => message.includes("rule input source reference"));
|
||
|
||
assert.equal(
|
||
sourceMessages.some((message) => message.includes("[[PROC-A]] and [[PROC-B]]")),
|
||
false
|
||
);
|
||
assert.equal(
|
||
sourceMessages.some((message) => message.includes("[[PROC-A]] / [[PROC-B]]")),
|
||
false
|
||
);
|
||
assert.deepEqual(sourceMessages, ['unresolved rule input source reference "[[PROC-MISSING]]"']);
|
||
|
||
const summary = buildImpactSummary(model, index);
|
||
assert.deepEqual(
|
||
summary.unresolvedOutbound.map((reference) => reference.targetRaw),
|
||
["[[PROC-MISSING]]"]
|
||
);
|
||
assert.ok(
|
||
summary.outboundRelationships.some((relationship) => relationship.modelId === "PROC-A")
|
||
);
|
||
assert.ok(
|
||
summary.outboundRelationships.some((relationship) => relationship.modelId === "PROC-B")
|
||
);
|
||
|
||
const weaveMap = buildWeaveMapModel(summary);
|
||
const warningNodes = weaveMap.nodes.filter((node) => node.status === "unresolved");
|
||
assert.deepEqual(warningNodes.map((node) => node.label), ["PROC-MISSING"]);
|
||
assert.equal(
|
||
warningNodes.some((node) => node.label.includes("[[") || node.label.includes("&[&[")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("screen diagnostics allow menu actions sharing target and event when conditions differ", () => {
|
||
const index = buildVaultIndex([
|
||
{ path: "SCR-MENU-ACTIONS.md", content: `---
|
||
type: screen
|
||
id: SCR-MENU-ACTIONS
|
||
name: Menu actions
|
||
---
|
||
|
||
# Menu actions
|
||
|
||
## Layout
|
||
|
||
| id | label | kind | purpose | notes |
|
||
|---|---|---|---|---|
|
||
| main | Main | main | Main area | |
|
||
|
||
## Fields
|
||
|
||
| id | label | kind | layout | data_type | required | ref | rule | notes |
|
||
|---|---|---|---|---|---|---|---|---|
|
||
| journal_actions | Journal actions | menu | main | action_menu | N | | | |
|
||
|
||
## Actions
|
||
|
||
| id | label | kind | target | event | condition | invoke | transition | rule | notes |
|
||
|---|---|---|---|---|---|---|---|---|---|
|
||
| ACT-COPY | Copy journal link | screen_event | journal_actions | click | journal visible | | | | |
|
||
| ACT-REACT | React | screen_event | journal_actions | click | reaction action available | | | | |
|
||
| ACT-QUOTE | Quote | screen_event | journal_actions | click | notes present and reply allowed | | | | |
|
||
| ACT-DUP-1 | Duplicate one | screen_event | journal_actions | click | same condition | | | | |
|
||
| ACT-DUP-2 | Duplicate two | screen_event | journal_actions | click | same condition | | | | |
|
||
|
||
## Transitions
|
||
|
||
| id | event | to | condition | notes |
|
||
|---|---|---|---|---|
|
||
| TRN-DONE | done | [[SCR-MENU-ACTIONS]] | saved | Self transition sample |
|
||
` }
|
||
], { parseMode: "full" });
|
||
|
||
const model = index.modelsByFilePath["SCR-MENU-ACTIONS.md"];
|
||
assert.equal(model.fileType, "screen");
|
||
const diagnostics = buildCurrentObjectDiagnostics(
|
||
model,
|
||
index,
|
||
null,
|
||
index.warningsByFilePath["SCR-MENU-ACTIONS.md"] ?? []
|
||
);
|
||
const messages = diagnostics.map((warning) => warning.message);
|
||
|
||
assert.equal(
|
||
messages.some((message) => message.includes('duplicate action target/event pair "journal_actions" + "click"')),
|
||
false
|
||
);
|
||
assert.equal(
|
||
messages.filter((message) => message.includes('duplicate action definition "journal_actions" + "click"')).length,
|
||
1
|
||
);
|
||
assert.equal(
|
||
messages.some((message) => message.includes('legacy "Transitions" section detected')),
|
||
false
|
||
);
|
||
});
|
||
|
||
|
||
|
||
|
||
test("mapping parser accepts target-first Mappings header as canonical", () => {
|
||
const { file, warnings } = parseMappingFile(`---
|
||
type: mapping
|
||
id: MAP-TARGET-FIRST
|
||
name: Target first
|
||
---
|
||
|
||
## Mappings
|
||
|
||
| target_ref | source_ref | transform | rule | required | notes |
|
||
|---|---|---|---|---|---|
|
||
| [[DATA-TARGET]].status_id | [[DATA-SOURCE]].issue | copy | [[RULE-MAP]] | Y | target first row |
|
||
`, "MAP-TARGET-FIRST.md");
|
||
|
||
assert.equal(file?.fileType, "mapping");
|
||
assert.equal(warnings.some((warning) => warning.code === "invalid-table-column"), false);
|
||
assert.deepEqual(file?.mappings, [
|
||
{
|
||
sourceRef: "[[DATA-SOURCE]].issue",
|
||
targetRef: "[[DATA-TARGET]].status_id",
|
||
transform: "copy",
|
||
rule: "[[RULE-MAP]]",
|
||
required: "Y",
|
||
notes: "target first row"
|
||
}
|
||
]);
|
||
});
|
||
|
||
test("mapping parser accepts legacy source-first Mappings header without warning", () => {
|
||
const { file, warnings } = parseMappingFile(`---
|
||
type: mapping
|
||
id: MAP-SOURCE-FIRST
|
||
name: Source first
|
||
---
|
||
|
||
## Mappings
|
||
|
||
| source_ref | target_ref | transform | rule | required | notes |
|
||
|---|---|---|---|---|---|
|
||
| [[DATA-SOURCE]].issue | [[DATA-TARGET]].status_id | copy | [[RULE-MAP]] | Y | source first row |
|
||
`, "MAP-SOURCE-FIRST.md");
|
||
|
||
assert.equal(file?.fileType, "mapping");
|
||
assert.equal(warnings.some((warning) => warning.code === "invalid-table-column"), false);
|
||
assert.deepEqual(file?.mappings, [
|
||
{
|
||
sourceRef: "[[DATA-SOURCE]].issue",
|
||
targetRef: "[[DATA-TARGET]].status_id",
|
||
transform: "copy",
|
||
rule: "[[RULE-MAP]]",
|
||
required: "Y",
|
||
notes: "source first row"
|
||
}
|
||
]);
|
||
});
|
||
|
||
test("mapping diagnostics treat member references and mapping rows as duplicate keys", () => {
|
||
const index = buildVaultIndex([
|
||
{ path: "DATA-MAP-SOURCE.md", content: `---
|
||
type: data_object
|
||
id: DATA-MAP-SOURCE
|
||
name: Source data
|
||
---
|
||
|
||
# Source data
|
||
|
||
## Fields
|
||
|
||
| name | label | type | length | required | path | ref | notes |
|
||
|---|---|---|---|---|---|---|---|
|
||
| issue | Issue | string | | N | | | |
|
||
| notes | Notes | string | | N | | | |
|
||
` },
|
||
{ path: "DATA-MAP-TARGET.md", content: `---
|
||
type: data_object
|
||
id: DATA-MAP-TARGET
|
||
name: Target data
|
||
---
|
||
|
||
# Target data
|
||
|
||
## Fields
|
||
|
||
| name | label | type | length | required | path | ref | notes |
|
||
|---|---|---|---|---|---|---|---|
|
||
| status_id | Status | string | | N | | | |
|
||
| subject | Subject | string | | N | | | |
|
||
` },
|
||
{ path: "MAP-MEMBER-DUPLICATES.md", content: `---
|
||
type: mapping
|
||
id: MAP-MEMBER-DUPLICATES
|
||
name: Member duplicates
|
||
source: [[DATA-MAP-SOURCE]]
|
||
target: [[DATA-MAP-TARGET]]
|
||
---
|
||
|
||
# Member duplicates
|
||
|
||
## Mappings
|
||
|
||
| source_ref | target_ref | transform | rule | required | notes |
|
||
|---|---|---|---|---|---|
|
||
| [[DATA-MAP-SOURCE]].issue | [[DATA-MAP-TARGET]].status_id | copy | | Y | field mapping |
|
||
| [[DATA-MAP-SOURCE]].notes | [[DATA-MAP-TARGET]].status_id | copy | | Y | same target allowed |
|
||
| [[DATA-MAP-SOURCE]].issue | [[DATA-MAP-TARGET]].subject | copy | | Y | different member target |
|
||
| [[DATA-MAP-SOURCE]].issue | [[DATA-MAP-TARGET]].subject | copy | | Y | duplicate row |
|
||
` }
|
||
], { parseMode: "full" });
|
||
|
||
const model = index.modelsByFilePath["MAP-MEMBER-DUPLICATES.md"];
|
||
assert.equal(model.fileType, "mapping");
|
||
const diagnostics = buildCurrentObjectDiagnostics(
|
||
model,
|
||
index,
|
||
null,
|
||
index.warningsByFilePath["MAP-MEMBER-DUPLICATES.md"] ?? []
|
||
);
|
||
const duplicateMessages = diagnostics
|
||
.map((warning) => warning.message)
|
||
.filter((message) => message.includes("duplicate"));
|
||
const targetMemberWarnings = diagnostics.filter(
|
||
(warning) => warning.code === "duplicate-mapping-target-member"
|
||
);
|
||
|
||
assert.deepEqual(duplicateMessages, [
|
||
'duplicate mapping row "DATA-MAP-SOURCE.issue -> DATA-MAP-TARGET.subject"'
|
||
]);
|
||
assert.deepEqual(
|
||
targetMemberWarnings.map((warning) => warning.message),
|
||
[
|
||
'mapping target member "DATA-MAP-TARGET.status_id" is mapped from multiple sources.'
|
||
]
|
||
);
|
||
assert.equal(
|
||
diagnostics.some((warning) => warning.message.includes("[[")),
|
||
false
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(duplicateMessages[0], "ja"),
|
||
'mapping row "DATA-MAP-SOURCE.issue -> DATA-MAP-TARGET.subject" が重複しています。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(targetMemberWarnings[0].message, "ja"),
|
||
'mapping target member "DATA-MAP-TARGET.status_id" が複数の source_ref から対応付けられています。'
|
||
);
|
||
});
|
||
|
||
test("mapping diagnostics allow repeated target refs when mapping rows differ", () => {
|
||
const index = buildVaultIndex([
|
||
{ path: "DATA-MAP-SOURCE-A.md", content: `---
|
||
type: data_object
|
||
id: DATA-MAP-SOURCE-A
|
||
name: Source A
|
||
---
|
||
|
||
# Source A
|
||
` },
|
||
{ path: "DATA-MAP-SOURCE-B.md", content: `---
|
||
type: data_object
|
||
id: DATA-MAP-SOURCE-B
|
||
name: Source B
|
||
---
|
||
|
||
# Source B
|
||
` },
|
||
{ path: "ENT-MAP-TARGET.md", content: `---
|
||
type: er_entity
|
||
id: ENT-MAP-TARGET
|
||
logical_name: Target entity
|
||
physical_name: target_entities
|
||
---
|
||
|
||
# Target entity
|
||
` },
|
||
{ path: "MAP-REPEATED-TARGET.md", content: `---
|
||
type: mapping
|
||
id: MAP-REPEATED-TARGET
|
||
name: Repeated target
|
||
source: [[DATA-MAP-SOURCE-A]]
|
||
target: [[ENT-MAP-TARGET]]
|
||
---
|
||
|
||
# Repeated target
|
||
|
||
## Mappings
|
||
|
||
| source_ref | target_ref | transform | rule | required | notes |
|
||
|---|---|---|---|---|---|
|
||
| [[DATA-MAP-SOURCE-A]] | [[ENT-MAP-TARGET]] | copy | | Y | first source |
|
||
| [[DATA-MAP-SOURCE-B]] | [[ENT-MAP-TARGET]] | copy | | Y | second source |
|
||
| [[DATA-MAP-SOURCE-A]] | [[ENT-MAP-TARGET]] | normalize | | Y | different transform |
|
||
` }
|
||
], { parseMode: "full" });
|
||
|
||
const model = index.modelsByFilePath["MAP-REPEATED-TARGET.md"];
|
||
assert.equal(model.fileType, "mapping");
|
||
const diagnostics = buildCurrentObjectDiagnostics(
|
||
model,
|
||
index,
|
||
null,
|
||
index.warningsByFilePath["MAP-REPEATED-TARGET.md"] ?? []
|
||
);
|
||
|
||
assert.equal(
|
||
diagnostics.some((warning) => warning.message.includes("duplicate target_ref")),
|
||
false
|
||
);
|
||
assert.equal(
|
||
diagnostics.some((warning) => warning.message.includes("duplicate mapping row")),
|
||
false
|
||
);
|
||
assert.equal(
|
||
diagnostics.some((warning) => warning.code === "duplicate-mapping-target-member"),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("DFD-local Domains parse without becoming DFD objects", () => {
|
||
const { file, warnings } = parseDfd(dfdBody([
|
||
"| logistics | Logistics | department | | Local logistics |",
|
||
"| warehouse | Warehouse | location | logistics | Local warehouse |"
|
||
].join("\n")));
|
||
|
||
assert.equal(file.domains.length, 2);
|
||
assert.equal(file.nodes.length, 2);
|
||
assert.deepEqual(file.nodes.map((node) => node.id), ["user", "pick"]);
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("parses optional DFD object domain column", () => {
|
||
const { file, warnings } = parseDfd(`${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| wms | WMS | system | | Warehouse system |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | Local process |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`);
|
||
|
||
assert.equal(file.objectEntries[0].domain, "wms");
|
||
assert.equal(file.nodes[0].metadata.domain, "wms");
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("parses optional DFD Domain Sources", () => {
|
||
const { file, warnings } = parseDfd(`${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | Company domains |
|
||
| [[DOMAINS-WMS]] | WMS domains |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | Local process |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`);
|
||
|
||
assert.equal(file.domainSources.length, 2);
|
||
assert.equal(file.domainSources[0].ref, "[[DOMAINS-COMPANY]]");
|
||
assert.equal(file.domainSources[1].notes, "WMS domains");
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("parses optional DFD Domain Sources without notes", () => {
|
||
const { file, warnings } = parseDfd(`${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref |
|
||
|---|
|
||
| [[DOMAINS-COMPANY]] |
|
||
| [[DOMAINS-WMS]] |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | Local process |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`);
|
||
|
||
assert.equal(file.domainSources.length, 2);
|
||
assert.equal(file.domainSources[0].ref, "[[DOMAINS-COMPANY]]");
|
||
assert.equal(file.domainSources[0].notes, undefined);
|
||
assert.equal(file.domainSources[1].ref, "[[DOMAINS-WMS]]");
|
||
assert.equal(warnings.length, 0);
|
||
});
|
||
|
||
test("renders DFD-local Domains as Mermaid subgraphs", () => {
|
||
const { resolved } = resolveDfdFromContent(`${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| warehouse | 倉庫 | location | | 実作業場所 |
|
||
| wms | WMS | system | | 倉庫管理システム |
|
||
| core | 基幹 | system | | 基幹システム |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | 出荷依頼受付 | other | | wms | |
|
||
| pick_items | ピッキング | process | | warehouse | |
|
||
| inventory_db | 在庫DB | datastore | | wms | |
|
||
| core_system | 基幹 | external | | core | |
|
||
| operator | 作業者 | external | | | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| f1 | core_system | receive_order | 出荷指示 | |
|
||
| f2 | receive_order | pick_items | ピッキング指示 | |
|
||
| f3 | pick_items | inventory_db | 在庫引当 | |
|
||
| f4 | operator | pick_items | 作業指示 | |
|
||
`);
|
||
|
||
const source = buildDfdMermaidSource(resolved);
|
||
assert.match(source, /subgraph domain_wms\["WMS \[system\]"\]/);
|
||
assert.match(source, /subgraph domain_warehouse\["倉庫 \[location\]"\]/);
|
||
assert.match(source, /subgraph domain_core\["基幹 \[system\]"\]/);
|
||
assert.match(source, /receive_order\["出荷依頼受付"\]:::dfdOther/);
|
||
assert.match(source, /inventory_db\[\("在庫DB"\)\]:::dfdDatastore/);
|
||
assert.match(source, /operator\["作業者"\]:::dfdExternal/);
|
||
assert.match(source, /core_system -->\|出荷指示\| receive_order/);
|
||
assert.match(source, /receive_order -->\|ピッキング指示\| pick_items/);
|
||
assert.doesNotMatch(source, /domain_wms -->/);
|
||
});
|
||
|
||
test("resolves DFD Domain Sources for nested Mermaid subgraphs", () => {
|
||
const { resolved } = resolveDfdWithFiles([
|
||
domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [
|
||
"| company | Company | organization | | Company group |",
|
||
"| logistics | Logistics | department | company | Logistics group |"
|
||
].join("\n")),
|
||
domainsFile("DOMAINS-WMS.md", "DOMAINS-WMS", [
|
||
"| warehouse | Warehouse | location | logistics | Warehouse group |",
|
||
"| wms | WMS | system | logistics | WMS group |"
|
||
].join("\n")),
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content: `${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | Company domains |
|
||
| [[DOMAINS-WMS]] | WMS domains |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | |
|
||
| pick_items | Pick items | process | | warehouse | |
|
||
| user | User | external | | | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| f1 | user | receive_order | Request | |
|
||
| f2 | receive_order | pick_items | Work | |
|
||
`
|
||
}
|
||
]);
|
||
|
||
const source = buildDfdMermaidSource(resolved);
|
||
const companyIndex = source.indexOf('subgraph domain_company["Company [organization]"]');
|
||
const logisticsIndex = source.indexOf('subgraph domain_logistics["Logistics [department]"]');
|
||
const wmsIndex = source.indexOf('subgraph domain_wms["WMS [system]"]');
|
||
const warehouseIndex = source.indexOf('subgraph domain_warehouse["Warehouse [location]"]');
|
||
|
||
assert.ok(companyIndex >= 0);
|
||
assert.ok(logisticsIndex > companyIndex);
|
||
assert.ok(wmsIndex > logisticsIndex);
|
||
assert.ok(warehouseIndex > logisticsIndex);
|
||
assert.match(source, /receive_order\["Receive order"\]:::dfdProcess/);
|
||
assert.match(source, /pick_items\["Pick items"\]:::dfdProcess/);
|
||
assert.match(source, /user\["User"\]:::dfdExternal/);
|
||
});
|
||
|
||
test("generates colored DFD Mermaid source from Color Scheme rows", () => {
|
||
const { resolved } = resolveDfdFromContent(`${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| wms | WMS | application | | 倉庫管理システム |
|
||
| warehouse | 倉庫 | operations | | 実作業場所 |
|
||
| core | 基幹 | external | | 基幹システム |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | 出荷依頼受付 | process | | wms | |
|
||
| inventory_db | 在庫DB | datastore | | wms | |
|
||
| core_system | 基幹 | external | | core | |
|
||
| manual_check | 手動確認 | other | | warehouse | |
|
||
| unknown_kind | 未分類 | | | | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| f1 | core_system | receive_order | 出荷指示 | |
|
||
| f2 | receive_order | inventory_db | 在庫引当 | |
|
||
| f3 | manual_check | receive_order | 確認結果 | |
|
||
`);
|
||
const scheme = {
|
||
...BUILT_IN_COLOR_SCHEME,
|
||
entries: [
|
||
{ target: "dfd", kind: "process", fill: "#9bbb59", stroke: "#6f8a3f", text: "#000000", rowIndex: 0 },
|
||
{ target: "dfd", kind: "datastore", fill: "#8064a2", stroke: "#60497a", text: "#ffffff", rowIndex: 1 },
|
||
{ target: "dfd", kind: "external", fill: "#a6a6a6", stroke: "#7f7f7f", text: "#000000", rowIndex: 2 },
|
||
{ target: "dfd", kind: "other", fill: "#f79646", stroke: "#c55a11", text: "#000000", rowIndex: 3 },
|
||
{ kind: "default", fill: "#f5f5f5", stroke: "#9e9e9e", text: "#111111", rowIndex: 4 },
|
||
{ target: "domain", kind: "application", fill: "#9bbb59", stroke: "#6f8a3f", text: "#000000", rowIndex: 5 },
|
||
{ target: "domain", kind: "operations", fill: "#7f7f7f", stroke: "#595959", text: "#ffffff", rowIndex: 6 },
|
||
{ target: "domain", kind: "external", fill: "#a6a6a6", stroke: "#7f7f7f", text: "#000000", rowIndex: 7 }
|
||
]
|
||
};
|
||
|
||
const source = buildDfdMermaidSource(resolved, scheme);
|
||
assert.match(source, /receive_order\["出荷依頼受付"\]:::kind_dfd_process/);
|
||
assert.match(source, /inventory_db\[\("在庫DB"\)\]:::kind_dfd_datastore/);
|
||
assert.match(source, /core_system\["基幹"\]:::kind_dfd_external/);
|
||
assert.match(source, /manual_check\["手動確認"\]:::kind_dfd_other/);
|
||
assert.match(source, /unknown_kind\["未分類"\]:::kind_dfd_other/);
|
||
assert.match(source, /classDef kind_dfd_process fill:#9bbb59,stroke:#6f8a3f,color:#000000/);
|
||
assert.match(source, /classDef kind_dfd_datastore fill:#8064a2,stroke:#60497a,color:#ffffff/);
|
||
assert.match(source, /classDef kind_dfd_external fill:#a6a6a6,stroke:#7f7f7f,color:#000000/);
|
||
assert.match(source, /classDef kind_dfd_other fill:#f79646,stroke:#c55a11,color:#000000/);
|
||
assert.match(source, /style domain_wms fill:#9bbb59,stroke:#6f8a3f,color:#000000/);
|
||
assert.match(source, /style domain_warehouse fill:#7f7f7f,stroke:#595959,color:#ffffff/);
|
||
assert.match(source, /style domain_core fill:#a6a6a6,stroke:#7f7f7f,color:#000000/);
|
||
assert.doesNotMatch(source, /:::dfdProcess/);
|
||
assert.match(source, /core_system -->\|出荷指示\| receive_order/);
|
||
});
|
||
|
||
test("uses DFD Domain Sources for domain group colors but keeps DFD node colors", () => {
|
||
const { resolved } = resolveDfdWithFiles([
|
||
domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [
|
||
"| wms | WMS | application | | WMS group |"
|
||
].join("\n")),
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content: `${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | Company domains |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`
|
||
}
|
||
]);
|
||
const scheme = {
|
||
...BUILT_IN_COLOR_SCHEME,
|
||
entries: [
|
||
{ target: "dfd", kind: "process", fill: "#9bbb59", stroke: "#6f8a3f", text: "#000000", rowIndex: 0 },
|
||
{ target: "domain", kind: "application", fill: "#8064a2", stroke: "#60497a", text: "#ffffff", rowIndex: 1 }
|
||
]
|
||
};
|
||
|
||
const source = buildDfdMermaidSource(resolved, scheme);
|
||
assert.match(source, /receive_order\["Receive order"\]:::kind_dfd_process/);
|
||
assert.match(source, /classDef kind_dfd_process fill:#9bbb59,stroke:#6f8a3f,color:#000000/);
|
||
assert.match(source, /style domain_wms fill:#8064a2,stroke:#60497a,color:#ffffff/);
|
||
assert.doesNotMatch(source, /kind_domain_/);
|
||
});
|
||
|
||
test("renders nested DFD-local Domain subgraphs", () => {
|
||
const { resolved } = resolveDfdFromContent(`${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | |
|
||
| logistics | 物流部 | department | company | |
|
||
| warehouse | 倉庫 | location | logistics | |
|
||
| wms | WMS | system | logistics | |
|
||
| core | 基幹 | system | company | |
|
||
| unused | 未使用 | system | company | |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | 出荷依頼受付 | other | | wms | |
|
||
| pick_items | ピッキング | process | | warehouse | |
|
||
| inventory_db | 在庫DB | datastore | | wms | |
|
||
| core_system | 基幹 | external | | core | |
|
||
| manual_check | 手動確認 | other | | | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
| f1 | core_system | receive_order | 出荷指示 | |
|
||
| f2 | receive_order | pick_items | ピッキング指示 | |
|
||
| f3 | pick_items | inventory_db | 在庫引当 | |
|
||
| f4 | manual_check | receive_order | 確認結果 | |
|
||
`);
|
||
|
||
const source = buildDfdMermaidSource(resolved);
|
||
const companyIndex = source.indexOf('subgraph domain_company["会社全体 [organization]"]');
|
||
const logisticsIndex = source.indexOf('subgraph domain_logistics["物流部 [department]"]');
|
||
const warehouseIndex = source.indexOf('subgraph domain_warehouse["倉庫 [location]"]');
|
||
const wmsIndex = source.indexOf('subgraph domain_wms["WMS [system]"]');
|
||
const coreIndex = source.indexOf('subgraph domain_core["基幹 [system]"]');
|
||
const manualCheckIndex = source.indexOf('manual_check["手動確認"]:::dfdOther');
|
||
|
||
assert.ok(companyIndex >= 0);
|
||
assert.ok(logisticsIndex > companyIndex);
|
||
assert.ok(warehouseIndex > logisticsIndex);
|
||
assert.ok(wmsIndex > logisticsIndex);
|
||
assert.ok(coreIndex > companyIndex);
|
||
assert.ok(manualCheckIndex > coreIndex);
|
||
assert.match(source, /pick_items\["ピッキング"\]:::dfdProcess/);
|
||
assert.match(source, /receive_order\["出荷依頼受付"\]:::dfdOther/);
|
||
assert.match(source, /inventory_db\[\("在庫DB"\)\]:::dfdDatastore/);
|
||
assert.doesNotMatch(source, /domain_unused/);
|
||
assert.match(source, /core_system -->\|出荷指示\| receive_order/);
|
||
assert.match(source, /manual_check -->\|確認結果\| receive_order/);
|
||
assert.doesNotMatch(source, /domain_company -->/);
|
||
});
|
||
|
||
test("DFD files without object domain render without Domain subgraphs", () => {
|
||
const { resolved } = resolveDfdFromContent(dfdBody([
|
||
"| logistics | Logistics | department | | Local logistics |"
|
||
].join("\n")));
|
||
const source = buildDfdMermaidSource(resolved);
|
||
|
||
assert.doesNotMatch(source, /subgraph domain_/);
|
||
assert.match(source, /user\["User"\]:::dfdExternal/);
|
||
assert.match(source, /pick\["Pick items"\]:::dfdProcess/);
|
||
});
|
||
|
||
test("diagnoses DFD object domain without local Domains", () => {
|
||
const { resolved } = resolveDfdFromContent(`${dfdFrontmatter}
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`);
|
||
const messages = resolved.warnings.map((warning) => warning.message);
|
||
|
||
assert.ok(messages.includes('DFD object "receive_order" references Domain "wms", but this DFD has no local Domains.'));
|
||
});
|
||
|
||
test("diagnoses DFD object unknown local Domain", () => {
|
||
const { resolved } = resolveDfdFromContent(`${dfdFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| warehouse | Warehouse | location | | Warehouse |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | missing | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`);
|
||
const messages = resolved.warnings.map((warning) => warning.message);
|
||
|
||
assert.ok(messages.includes('DFD object "receive_order" references unknown local Domain "missing".'));
|
||
});
|
||
|
||
test("diagnoses unknown DFD object Domain when Domain Sources are present", () => {
|
||
const { resolved } = resolveDfdWithFiles([
|
||
domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [
|
||
"| warehouse | Warehouse | location | | Warehouse group |"
|
||
].join("\n")),
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content: `${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | Company domains |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | missing | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`
|
||
}
|
||
]);
|
||
const messages = resolved.warnings.map((warning) => warning.message);
|
||
|
||
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("diagnoses unresolved and non-domains DFD Domain Source refs", () => {
|
||
const { resolved } = resolveDfdWithFiles([
|
||
{
|
||
path: "RULE-SHIPPING.md",
|
||
content: `---
|
||
type: rule
|
||
id: RULE-SHIPPING
|
||
name: Shipping Rule
|
||
---
|
||
|
||
# Shipping Rule
|
||
`
|
||
},
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content: `${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-MISSING]] | Missing |
|
||
| [[RULE-SHIPPING]] | Wrong type |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`
|
||
}
|
||
]);
|
||
const messages = resolved.warnings.map((warning) => warning.message);
|
||
|
||
assert.ok(messages.includes('Domain Source ref "[[DOMAINS-MISSING]]" could not be resolved. Check the ID or file name.'));
|
||
assert.ok(messages.includes('Domain Source ref "[[RULE-SHIPPING]]" resolves to type "rule", but expected type "domains".'));
|
||
assert.ok(messages.includes('DFD object "receive_order" references unknown Domain "wms".'));
|
||
});
|
||
|
||
test("DFD-local Domains override Domain Sources after merge", () => {
|
||
const { resolved } = resolveDfdWithFiles([
|
||
domainsFile("DOMAINS-COMPANY.md", "DOMAINS-COMPANY", [
|
||
"| logistics | Logistics | department | | Logistics group |",
|
||
"| wms | WMS | system | logistics | WMS group |"
|
||
].join("\n")),
|
||
{
|
||
path: "DFD-SHIPPING.md",
|
||
content: `${dfdFrontmatter}
|
||
## Domain Sources
|
||
|
||
| ref | notes |
|
||
|---|---|
|
||
| [[DOMAINS-COMPANY]] | Company domains |
|
||
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| wms | Local WMS | application | logistics | Local override |
|
||
|
||
## Objects
|
||
|
||
| id | label | kind | ref | domain | notes |
|
||
|---|---|---|---|---|---|
|
||
| receive_order | Receive order | process | | wms | |
|
||
|
||
## Flows
|
||
|
||
| id | from | to | data | notes |
|
||
|---|---|---|---|---|
|
||
`
|
||
}
|
||
]);
|
||
const source = buildDfdMermaidSource(resolved);
|
||
const messages = resolved.warnings.map((warning) => warning.message);
|
||
|
||
assert.match(source, /subgraph domain_wms\["Local WMS \[application\]"\]/);
|
||
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('Domain parent "logistics" is not defined.'), false);
|
||
});
|
||
|
||
test("localizes DFD object Domain diagnostics", () => {
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'DFD object "receive_order" references unknown local Domain "missing".',
|
||
"ja"
|
||
),
|
||
'DFD object "receive_order" が未定義のローカル Domain "missing" を参照しています。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'DFD object "receive_order" references Domain "wms", but this DFD has no local Domains.',
|
||
"ja"
|
||
),
|
||
'DFD object "receive_order" が Domain "wms" を参照していますが、この DFD にはローカル Domains が定義されていません。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'DFD object "receive_order" references unknown Domain "missing".',
|
||
"ja"
|
||
),
|
||
'DFD object "receive_order" が未定義の Domain "missing" を参照しています。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'DFD-local Domain "wms" overrides Domain Source kind "system" with "application".',
|
||
"ja"
|
||
),
|
||
'DFD内の Domain "wms" は Domain Source の kind "system" を "application" で上書きしています。'
|
||
);
|
||
});
|
||
|
||
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 |",
|
||
"| warehouse | Duplicate | location | | Duplicate id |",
|
||
"| orphan | Orphan | system | missing_parent | Unknown parent |",
|
||
"| self | Self | system | self | Self parent |",
|
||
"| cycle_a | Cycle A | system | cycle_b | Cycle A |",
|
||
"| cycle_b | Cycle B | system | cycle_a | Cycle B |",
|
||
"| | Missing | system | | Missing id |"
|
||
].join("\n")));
|
||
|
||
const messages = warnings.map((warning) => warning.message);
|
||
assert.ok(messages.includes('duplicate Domain id "warehouse"'));
|
||
assert.ok(messages.includes('Domain parent "missing_parent" is not defined.'));
|
||
assert.ok(messages.includes('Domain "self" cannot use itself as parent.'));
|
||
assert.ok(messages.some((message) => message.includes("Domain parent cycle detected")));
|
||
assert.ok(messages.includes("Domain id is required."));
|
||
});
|
||
|
||
test("validates DFD-local Domains against shared standalone Domains", () => {
|
||
const index = buildDomainsIndex([
|
||
"| warehouse | Warehouse | location | logistics | Description may differ |",
|
||
"| wms | WMS | application | logistics | Description may differ |",
|
||
"| unknown | Unknown | system | logistics | Local only |"
|
||
].join("\n"));
|
||
const messages = (index.warningsByFilePath["DFD-SHIPPING.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
|
||
assert.ok(messages.includes('DFD-local Domain "unknown" is not defined in shared Domains.'));
|
||
assert.ok(messages.includes('DFD-local Domain "wms" has kind "application", but shared Domains define kind "system".'));
|
||
assert.equal(
|
||
messages.some((message) => message.includes("Description may differ")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("validates DFD-local Domain parent and name mismatches", () => {
|
||
const index = buildDomainsIndex([
|
||
"| warehouse | 倉庫 | location | wms | Local parent mismatch |",
|
||
"| wms | WMS local | system | logistics | Local name mismatch |"
|
||
].join("\n"));
|
||
const messages = (index.warningsByFilePath["DFD-SHIPPING.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
|
||
assert.ok(messages.includes('DFD-local Domain "warehouse" has name "倉庫", but shared Domains define name "Warehouse".'));
|
||
assert.ok(messages.includes('DFD-local Domain "warehouse" has parent "wms", but shared Domains define parent "logistics".'));
|
||
assert.ok(messages.includes('DFD-local Domain "wms" has name "WMS local", but shared Domains define name "WMS".'));
|
||
});
|
||
|
||
test("empty DFD-local Domain fields do not conflict with shared Domains", () => {
|
||
const index = buildDomainsIndex([
|
||
"| warehouse | | | | Description may differ |",
|
||
"| wms | WMS | system | logistics | Different description |"
|
||
].join("\n"));
|
||
const messages = (index.warningsByFilePath["DFD-SHIPPING.md"] ?? []).map(
|
||
(warning) => warning.message
|
||
);
|
||
|
||
assert.equal(
|
||
messages.some((message) => message.startsWith("DFD-local Domain")),
|
||
false
|
||
);
|
||
});
|
||
|
||
test("localizes DFD-local Domain consistency diagnostics", () => {
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'DFD-local Domain "warehouse" is not defined in shared Domains.',
|
||
"ja"
|
||
),
|
||
'DFD内の Domain "warehouse" は共通 Domains に定義されていません。'
|
||
);
|
||
assert.equal(
|
||
localizeDiagnosticMessage(
|
||
'DFD-local Domain "wms" has kind "application", but shared Domains define kind "system".',
|
||
"ja"
|
||
),
|
||
'DFD内の Domain "wms" の kind は "application" ですが、共通 Domains では "system" と定義されています。'
|
||
);
|
||
});
|
||
|
||
test("generates Mermaid source for nested Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| warehouse | 倉庫 | location | logistics | 倉庫 |
|
||
| handheld | ハンディ端末 | device | warehouse | 端末 |
|
||
| wms | WMS | system | logistics | WMS |
|
||
| core | 基幹システム | system | company | 基幹 |
|
||
`);
|
||
|
||
const source = buildDomainHierarchyMermaid(file.domains);
|
||
assert.match(source, /^flowchart TB/);
|
||
assert.match(source, /subgraph domain_company\["会社全体 \[organization\]"\]/);
|
||
assert.match(source, /subgraph domain_logistics\["物流部 \[department\]"\]/);
|
||
assert.match(source, /subgraph domain_warehouse\["倉庫 \[location\]"\]/);
|
||
assert.match(source, /domain_handheld\["ハンディ端末 \[device\]"\]/);
|
||
assert.match(source, /domain_wms\["WMS \[system\]"\]/);
|
||
assert.match(source, /domain_core\["基幹システム \[system\]"\]/);
|
||
assert.doesNotMatch(source, /classDef/);
|
||
assert.doesNotMatch(source, /style domain_/);
|
||
});
|
||
|
||
test("generates colored Area source for Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| unknown | 不明 | mystery | company | unknown kind |
|
||
`);
|
||
|
||
const source = buildDomainHierarchyMermaid(file.domains, BUILT_IN_COLOR_SCHEME);
|
||
assert.match(source, /subgraph domain_company\["会社全体 \[organization\]"\]/);
|
||
assert.match(source, /style domain_company fill:#e3f2fd,stroke:#1976d2,color:#111111/);
|
||
assert.match(source, /style domain_logistics fill:#e8f5e9,stroke:#388e3c,color:#111111/);
|
||
assert.match(source, /style domain_unknown fill:#f5f5f5,stroke:#9e9e9e,color:#111111/);
|
||
assert.doesNotMatch(source, /classDef/);
|
||
});
|
||
|
||
test("generates TreeView source for single-root Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| warehouse | 倉庫 | location | logistics | 倉庫 |
|
||
| handheld | ハンディ端末 | device | warehouse | 端末 |
|
||
| office | 事務所 | location | logistics | 事務所 |
|
||
| wms | WMS | system | logistics | WMS |
|
||
| core | 基幹システム | system | company | 基幹 |
|
||
`);
|
||
|
||
const source = buildDomainTreeViewMermaid(file.domains);
|
||
assert.match(source, /^flowchart TB/);
|
||
assert.match(source, /domain_company\["会社全体 \[organization\]"\]/);
|
||
assert.match(source, /domain_logistics\["物流部 \[department\]"\]/);
|
||
assert.match(source, /domain_warehouse\["倉庫 \[location\]"\]/);
|
||
assert.match(source, /domain_handheld\["ハンディ端末 \[device\]"\]/);
|
||
assert.match(source, /domain_office\["事務所 \[location\]"\]/);
|
||
assert.match(source, /domain_wms\["WMS \[system\]"\]/);
|
||
assert.match(source, /domain_core\["基幹システム \[system\]"\]/);
|
||
assert.match(source, /domain_company --> domain_logistics/);
|
||
assert.match(source, /domain_logistics --> domain_warehouse/);
|
||
assert.match(source, /domain_warehouse --> domain_handheld/);
|
||
assert.match(source, /domain_logistics --> domain_office/);
|
||
assert.match(source, /domain_logistics --> domain_wms/);
|
||
assert.match(source, /domain_company --> domain_core/);
|
||
});
|
||
|
||
test("generates colored TreeView source for Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| unknown | 不明 | mystery | company | unknown kind |
|
||
`);
|
||
|
||
const source = buildDomainTreeViewMermaid(file.domains, BUILT_IN_COLOR_SCHEME);
|
||
assert.match(source, /classDef kind_domain_organization fill:#e3f2fd,stroke:#1976d2,color:#111111/);
|
||
assert.match(source, /classDef kind_domain_department fill:#e8f5e9,stroke:#388e3c,color:#111111/);
|
||
assert.match(source, /classDef kind_domain_mystery fill:#f5f5f5,stroke:#9e9e9e,color:#111111/);
|
||
assert.match(source, /class domain_company kind_domain_organization/);
|
||
assert.match(source, /class domain_logistics kind_domain_department/);
|
||
assert.match(source, /class domain_unknown kind_domain_mystery/);
|
||
});
|
||
|
||
test("generates colored TreeView source from global Color Scheme rows", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| business | 事業 | business | | Business |
|
||
| application | アプリ | application | business | Application |
|
||
`);
|
||
const scheme = {
|
||
...BUILT_IN_COLOR_SCHEME,
|
||
entries: [
|
||
{
|
||
kind: "business",
|
||
fill: "#4f81bd",
|
||
stroke: "#2f5597",
|
||
text: "#ffffff",
|
||
rowIndex: 0
|
||
},
|
||
{
|
||
target: "domain",
|
||
kind: "business",
|
||
fill: "#70ad47",
|
||
stroke: "#548235",
|
||
text: "#ffffff",
|
||
rowIndex: 1
|
||
},
|
||
{
|
||
kind: "application",
|
||
fill: "#8064a2",
|
||
stroke: "#5f497a",
|
||
text: "#ffffff",
|
||
rowIndex: 2
|
||
}
|
||
]
|
||
};
|
||
|
||
const source = buildDomainTreeViewMermaid(file.domains, scheme);
|
||
assert.match(source, /classDef kind_domain_business fill:#70ad47,stroke:#548235,color:#ffffff/);
|
||
assert.match(source, /classDef kind_domain_application fill:#8064a2,stroke:#5f497a,color:#ffffff/);
|
||
});
|
||
|
||
test("generates TreeView source for multiple root Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| external | 外部 | external | | 外部 |
|
||
| carrier | 配送会社 | partner | external | 配送 |
|
||
`);
|
||
|
||
const source = buildDomainTreeViewMermaid(file.domains);
|
||
assert.match(source, /^flowchart TB/);
|
||
assert.match(source, /domain_company\["会社全体 \[organization\]"\]/);
|
||
assert.match(source, /domain_external\["外部 \[external\]"\]/);
|
||
assert.match(source, /domain_company --> domain_logistics/);
|
||
assert.match(source, /domain_external --> domain_carrier/);
|
||
assert.doesNotMatch(source, /root\(\(Domains\)\)/);
|
||
});
|
||
|
||
test("generates TreeView label fallback from empty Domain name", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| fallback_name | | system | | Uses id |
|
||
`);
|
||
|
||
const source = buildDomainTreeViewMermaid(file.domains);
|
||
assert.match(source, /domain_fallback_name\["fallback_name \[system\]"\]/);
|
||
});
|
||
|
||
test("generates Mindmap source for single-root Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| warehouse | 倉庫 | location | logistics | 倉庫 |
|
||
| handheld | ハンディ端末 | device | warehouse | 端末 |
|
||
| wms | WMS | system | logistics | WMS |
|
||
| core | 基幹システム | system | company | 基幹 |
|
||
`);
|
||
|
||
const source = buildDomainMindmapMermaid(file.domains);
|
||
assert.match(source, /^mindmap/);
|
||
assert.match(source, / root\(\(会社全体(organization)\)\)/);
|
||
assert.match(source, / 物流部(department)/);
|
||
assert.match(source, / 倉庫(location)/);
|
||
assert.match(source, / ハンディ端末(device)/);
|
||
assert.match(source, / WMS(system)/);
|
||
assert.match(source, / 基幹システム(system)/);
|
||
assert.doesNotMatch(source, /\[[^\]]+\]/);
|
||
assert.doesNotMatch(source, /classDef/);
|
||
});
|
||
|
||
test("generates Mindmap source with synthetic root for multiple roots", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| company | 会社全体 | organization | | 業務全体 |
|
||
| logistics | 物流部 | department | company | 物流 |
|
||
| external | 外部 | organization | | 外部 |
|
||
| carrier | 配送会社 | partner | external | 配送 |
|
||
`);
|
||
|
||
const source = buildDomainMindmapMermaid(file.domains);
|
||
assert.match(source, /^mindmap/);
|
||
assert.match(source, / root\(\(Domains\)\)/);
|
||
assert.match(source, / 会社全体(organization)/);
|
||
assert.match(source, / 物流部(department)/);
|
||
assert.match(source, / 外部(organization)/);
|
||
assert.match(source, / 配送会社(partner)/);
|
||
assert.doesNotMatch(source, /\[[^\]]+\]/);
|
||
});
|
||
|
||
test("generates Mindmap label fallback from empty Domain name", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| fallback_name | | system | | Uses id |
|
||
`);
|
||
|
||
const source = buildDomainMindmapMermaid(file.domains);
|
||
assert.match(source, /root\(\(fallback_name(system)\)\)/);
|
||
assert.doesNotMatch(source, /\[[^\]]+\]/);
|
||
});
|
||
|
||
test("generates Mermaid label fallback from empty Domain name", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| fallback_name | | system | | Uses id |
|
||
`);
|
||
|
||
const source = buildDomainHierarchyMermaid(file.domains);
|
||
assert.match(source, /domain_fallback_name\["fallback_name \[system\]"\]/);
|
||
});
|
||
|
||
test("generates safe TreeView source for circular Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| cycle_a | Cycle A | system | cycle_b | Cycle A |
|
||
| cycle_b | Cycle B | system | cycle_a | Cycle B |
|
||
`);
|
||
|
||
const source = buildDomainTreeViewMermaid(file.domains);
|
||
assert.match(source, /^flowchart TB/);
|
||
assert.match(source, /domain_cycle_a\["Cycle A \[system\]"\]/);
|
||
assert.match(source, /domain_cycle_b\["Cycle B \[system\]"\]/);
|
||
assert.doesNotMatch(source, /domain_cycle_a --> domain_cycle_b/);
|
||
assert.doesNotMatch(source, /domain_cycle_b --> domain_cycle_a/);
|
||
assert.doesNotMatch(source, /Maximum call stack/i);
|
||
});
|
||
|
||
test("generates safe Mermaid source for circular Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| cycle_a | Cycle A | system | cycle_b | Cycle A |
|
||
| cycle_b | Cycle B | system | cycle_a | Cycle B |
|
||
`);
|
||
|
||
const source = buildDomainHierarchyMermaid(file.domains);
|
||
assert.match(source, /^flowchart TB/);
|
||
assert.match(source, /domain_cycle_a\["Cycle A \[system\]"\]/);
|
||
assert.match(source, /domain_cycle_b\["Cycle B \[system\]"\]/);
|
||
assert.doesNotMatch(source, /Maximum call stack/i);
|
||
});
|
||
|
||
test("generates safe Mindmap source for circular Domain hierarchy", () => {
|
||
const { file } = parseDomains(`${baseFrontmatter}
|
||
## Domains
|
||
|
||
| id | name | kind | parent | description |
|
||
|---|---|---|---|---|
|
||
| cycle_a | Cycle A | system | cycle_b | Cycle A |
|
||
| cycle_b | Cycle B | system | cycle_a | Cycle B |
|
||
`);
|
||
|
||
const source = buildDomainMindmapMermaid(file.domains);
|
||
assert.match(source, /^mindmap/);
|
||
assert.match(source, /Cycle A(system)/);
|
||
assert.match(source, /Cycle B(system)/);
|
||
assert.doesNotMatch(source, /\[[^\]]+\]/);
|
||
assert.doesNotMatch(source, /Maximum call stack/i);
|
||
});
|