From e7c57023e87b49e2acbc3aa4dabe5bfa3a8aca4d Mon Sep 17 00:00:00 2001 From: nejimakibird <23066051+nejimakibird@users.noreply.github.com> Date: Fri, 19 Jun 2026 22:26:46 +0900 Subject: [PATCH] feat: add diagnostic detail guidance --- main.js | 261 +++++++++++++++++++++++++++ src/i18n/en.ts | 19 ++ src/i18n/ja.ts | 19 ++ src/views/modeling-preview-view.ts | 269 ++++++++++++++++++++++++++++ styles.css | 15 ++ test/localization-language.test.mjs | 8 + 6 files changed, 591 insertions(+) diff --git a/main.js b/main.js index 6f01108..5c19fbe 100644 --- a/main.js +++ b/main.js @@ -17982,6 +17982,8 @@ var EN_MESSAGES = { "diagnostics.copyMessage": "Copy message", "diagnostics.copyMarkdown": "Copy diagnostic as Markdown", "diagnostics.copyReference": "Copy reference", + "diagnostics.copyExpectedHeader": "Copy expected header", + "diagnostics.copyFrontmatterExample": "Copy frontmatter example", "diagnostics.severity.error": "Error", "diagnostics.severity.warning": "Warning", "diagnostics.severity.note": "Note", @@ -17990,9 +17992,26 @@ var EN_MESSAGES = { "diagnostics.meta.line": "Line", "diagnostics.meta.row": "Row", "diagnostics.meta.reference": "Reference", + "diagnostics.meta.field": "Field", "diagnostics.meta.severity": "Severity", "diagnostics.meta.code": "Code", "diagnostics.meta.message": "Message", + "diagnostics.details.title": "Diagnostic details", + "diagnostics.details.expectedHeader": "Expected header", + "diagnostics.details.actualHeader": "Actual header", + "diagnostics.details.missingColumns": "Missing columns", + "diagnostics.details.extraColumns": "Extra columns", + "diagnostics.details.unresolvedReference": "Unresolved reference", + "diagnostics.details.duplicateTarget": "Duplicate target", + "diagnostics.details.mappingRow": "Mapping row", + "diagnostics.details.frontmatterKey": "Missing frontmatter key", + "diagnostics.details.frontmatterExample": "Frontmatter example", + "diagnostics.details.requestedRenderMode": "Requested render mode", + "diagnostics.details.effectiveRenderMode": "Effective render mode", + "diagnostics.details.formatDefault": "Format default", + "diagnostics.details.diagramCompatibility": "Diagram compatibility", + "diagnostics.details.notClassDiagramCompatible": "Exists, but is excluded from class diagram rendering", + "diagnostics.details.targetType": "Target type", "objectContext.title": "Related objects", "objectContext.linked": "{count} linked", "objectContext.connectionDetails": "Connection details", @@ -18319,6 +18338,8 @@ var JA_MESSAGES = { "diagnostics.copyMessage": "\u30E1\u30C3\u30BB\u30FC\u30B8\u3092\u30B3\u30D4\u30FC", "diagnostics.copyMarkdown": "\u8A3A\u65AD\u3092Markdown\u3067\u30B3\u30D4\u30FC", "diagnostics.copyReference": "\u53C2\u7167\u5024\u3092\u30B3\u30D4\u30FC", + "diagnostics.copyExpectedHeader": "\u671F\u5F85\u30D8\u30C3\u30C0\u30FC\u3092\u30B3\u30D4\u30FC", + "diagnostics.copyFrontmatterExample": "frontmatter \u4F8B\u3092\u30B3\u30D4\u30FC", "diagnostics.severity.error": "\u30A8\u30E9\u30FC", "diagnostics.severity.warning": "\u8B66\u544A", "diagnostics.severity.note": "\u30CE\u30FC\u30C8", @@ -18327,9 +18348,26 @@ var JA_MESSAGES = { "diagnostics.meta.line": "\u884C", "diagnostics.meta.row": "\u884C\u756A\u53F7", "diagnostics.meta.reference": "\u53C2\u7167\u5024", + "diagnostics.meta.field": "\u30D5\u30A3\u30FC\u30EB\u30C9", "diagnostics.meta.severity": "\u91CD\u8981\u5EA6", "diagnostics.meta.code": "\u30B3\u30FC\u30C9", "diagnostics.meta.message": "\u30E1\u30C3\u30BB\u30FC\u30B8", + "diagnostics.details.title": "\u8A3A\u65AD\u8A73\u7D30", + "diagnostics.details.expectedHeader": "\u671F\u5F85\u3055\u308C\u308B\u30D8\u30C3\u30C0\u30FC", + "diagnostics.details.actualHeader": "\u5B9F\u969B\u306E\u30D8\u30C3\u30C0\u30FC", + "diagnostics.details.missingColumns": "\u4E0D\u8DB3\u3057\u3066\u3044\u308B\u5217", + "diagnostics.details.extraColumns": "\u4F59\u5206\u306A\u5217", + "diagnostics.details.unresolvedReference": "\u672A\u89E3\u6C7A\u306E\u53C2\u7167", + "diagnostics.details.duplicateTarget": "\u91CD\u8907\u3057\u3066\u3044\u308B\u5BFE\u8C61", + "diagnostics.details.mappingRow": "Mapping row", + "diagnostics.details.frontmatterKey": "\u4E0D\u8DB3\u3057\u3066\u3044\u308B frontmatter key", + "diagnostics.details.frontmatterExample": "frontmatter \u4F8B", + "diagnostics.details.requestedRenderMode": "\u8981\u6C42\u3055\u308C\u305F\u63CF\u753B\u30E2\u30FC\u30C9", + "diagnostics.details.effectiveRenderMode": "\u5B9F\u969B\u306E\u63CF\u753B\u30E2\u30FC\u30C9", + "diagnostics.details.formatDefault": "format \u65E2\u5B9A", + "diagnostics.details.diagramCompatibility": "\u56F3\u3068\u306E\u4E92\u63DB\u6027", + "diagnostics.details.notClassDiagramCompatible": "\u5B58\u5728\u3057\u307E\u3059\u304C\u3001Class Diagram \u306E\u63CF\u753B\u5BFE\u8C61\u5916\u3067\u3059", + "diagnostics.details.targetType": "\u53C2\u7167\u5148 type", "objectContext.title": "\u95A2\u9023\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8", "objectContext.linked": "{count} \u4EF6\u306E\u95A2\u9023", "objectContext.connectionDetails": "\u63A5\u7D9A\u8A73\u7D30", @@ -23028,6 +23066,20 @@ function renderDiagnosticCard(container, diagnostic, onOpenDiagnostic, t, langua item.createSpan({ text: entry.value, cls: "model-weave-diagnostic-meta-value" }); } } + const details = getDiagnosticDetailEntries(diagnostic, t); + if (details.length > 0) { + const detailBox = card.createDiv({ cls: "model-weave-diagnostic-detail-box" }); + detailBox.createDiv({ + text: t("diagnostics.details.title"), + cls: "model-weave-diagnostic-detail-title" + }); + const detailList = detailBox.createDiv({ cls: "model-weave-diagnostic-meta-list" }); + for (const entry of details) { + const item = detailList.createDiv({ cls: "model-weave-diagnostic-meta" }); + item.createSpan({ text: entry.label, cls: "model-weave-diagnostic-meta-label" }); + item.createSpan({ text: entry.value, cls: "model-weave-diagnostic-meta-value" }); + } + } const actions = card.createDiv({ cls: "model-weave-diagnostic-actions" }); if (onOpenDiagnostic) { const openButton = actions.createEl("button", { @@ -23062,6 +23114,24 @@ function renderDiagnosticCard(container, diagnostic, onOpenDiagnostic, t, langua "model-weave-diagnostic-action" ); } + const expectedHeader = getExpectedHeaderForDiagnostic(diagnostic); + if (expectedHeader) { + renderDiagnosticCopyButton( + actions, + t("diagnostics.copyExpectedHeader"), + expectedHeader, + "model-weave-diagnostic-action" + ); + } + const frontmatterExample = getFrontmatterExampleForDiagnostic(diagnostic); + if (frontmatterExample) { + renderDiagnosticCopyButton( + actions, + t("diagnostics.copyFrontmatterExample"), + frontmatterExample, + "model-weave-diagnostic-action" + ); + } } function renderDiagnosticCopyButton(container, label, value, className) { const button = container.createEl("button", { @@ -23083,6 +23153,190 @@ function getDiagnosticSeverityLabel(severity, t) { } return t("diagnostics.severity.note"); } +function getDiagnosticDetailEntries(diagnostic, t) { + const details = []; + const expectedHeader = getExpectedHeaderForDiagnostic(diagnostic); + if (expectedHeader) { + details.push({ label: t("diagnostics.details.expectedHeader"), value: expectedHeader }); + } + const actualHeader = findDiagnosticContextValue(diagnostic.context, [ + "actualHeader", + "actualHeaders", + "header", + "headers" + ]); + if (actualHeader) { + details.push({ label: t("diagnostics.details.actualHeader"), value: actualHeader }); + } + const missingColumns = findDiagnosticContextValue(diagnostic.context, ["missingColumns", "missing"]); + if (missingColumns) { + details.push({ label: t("diagnostics.details.missingColumns"), value: missingColumns }); + } + const extraColumns = findDiagnosticContextValue(diagnostic.context, ["extraColumns", "extra"]); + if (extraColumns) { + details.push({ label: t("diagnostics.details.extraColumns"), value: extraColumns }); + } + const reference = getDiagnosticReferenceValue(diagnostic); + if (reference) { + const referenceLabel = diagnostic.code === "duplicate-mapping-target-member" ? t("diagnostics.details.duplicateTarget") : diagnostic.code === "unresolved-reference" ? t("diagnostics.details.unresolvedReference") : t("diagnostics.meta.reference"); + details.push({ label: referenceLabel, value: reference }); + } + const duplicateMappingRow = getDuplicateMappingRowValue(diagnostic); + if (duplicateMappingRow) { + details.push({ label: t("diagnostics.details.mappingRow"), value: duplicateMappingRow }); + } + const missingFrontmatterKey = getMissingFrontmatterKey(diagnostic); + if (missingFrontmatterKey) { + details.push({ label: t("diagnostics.details.frontmatterKey"), value: missingFrontmatterKey }); + } + const frontmatterExample = getFrontmatterExampleForDiagnostic(diagnostic); + if (frontmatterExample) { + details.push({ label: t("diagnostics.details.frontmatterExample"), value: frontmatterExample }); + } + const requestedRenderMode = getRequestedRenderMode(diagnostic); + if (requestedRenderMode) { + details.push({ label: t("diagnostics.details.requestedRenderMode"), value: requestedRenderMode }); + details.push({ + label: t("diagnostics.details.effectiveRenderMode"), + value: t("diagnostics.details.formatDefault") + }); + } + if (diagnostic.code === "class-relation-target-not-diagram-compatible") { + details.push({ + label: t("diagnostics.details.diagramCompatibility"), + value: t("diagnostics.details.notClassDiagramCompatible") + }); + const targetType = findDiagnosticContextValue(diagnostic.context, [ + "targetType", + "resolvedType", + "fileType", + "modelType" + ]); + if (targetType) { + details.push({ label: t("diagnostics.details.targetType"), value: targetType }); + } + } + const section = getDiagnosticSectionName(diagnostic); + if (section && /row|reference|mapping|frontmatter|render_mode|class relation/i.test(diagnostic.message)) { + details.push({ label: t("diagnostics.meta.section"), value: section }); + } + const row = getDiagnosticStringValue(diagnostic.context?.rowIndex); + if (row && /row|reference|mapping/i.test(diagnostic.message)) { + details.push({ label: t("diagnostics.meta.row"), value: row }); + } + const field = getDiagnosticStringValue(diagnostic.context?.field) ?? diagnostic.field; + if (field && /reference|field/i.test(diagnostic.message)) { + details.push({ label: t("diagnostics.meta.field"), value: field }); + } + return dedupeDiagnosticDetailEntries(details); +} +function dedupeDiagnosticDetailEntries(entries) { + const seen = /* @__PURE__ */ new Set(); + const result = []; + for (const entry of entries) { + const key = entry.label + "\0" + entry.value; + if (seen.has(key)) { + continue; + } + seen.add(key); + result.push(entry); + } + return result; +} +function getExpectedHeaderForDiagnostic(diagnostic) { + const contextHeader = findDiagnosticContextValue(diagnostic.context, [ + "expectedHeader", + "expectedHeaders", + "expected" + ]); + if (contextHeader) { + return contextHeader; + } + if (!/table columns in section/i.test(diagnostic.message)) { + return null; + } + const message = diagnostic.message; + if (/screen field headers/i.test(message)) { + return "id | label | kind | layout | rule | ref | default | required | notes"; + } + if (/legacy headers/i.test(message) && /Transitions/i.test(message)) { + return "id | event | to | condition | notes"; + } + if (/supported DFD object headers/i.test(message)) { + return "id | label | kind | ref | domain | notes"; + } + if (/supported class relation headers/i.test(message)) { + return "id | from | to | kind | label | notes"; + } + if (/Domain Sources headers/i.test(message)) { + return "ref | notes"; + } + if (/app_process step headers/i.test(message)) { + return "id | domain | label | kind | input | output | rule | invoke | screen | notes"; + } + const section = getDiagnosticSectionName(diagnostic); + return section ? getGenericExpectedHeaderForSection(section) : null; +} +function getGenericExpectedHeaderForSection(section) { + const normalized = section.trim().toLowerCase(); + if (normalized === "mappings") { + return "source_ref | target_ref | transform | rule | required | notes"; + } + if (normalized === "layout") { + return "id | label | kind | parent | order | notes"; + } + if (normalized === "actions") { + return "id | target | event | kind | invoke | transition | rule | condition | notes"; + } + if (normalized === "messages") { + return "id | timing | severity | audience | text | notes"; + } + if (normalized === "objects") { + return "id | label | kind | ref | domain | notes"; + } + if (normalized === "flows") { + return "id | from | to | label | kind | notes"; + } + if (normalized === "domain sources") { + return "ref | notes"; + } + return null; +} +function getDiagnosticSectionName(diagnostic) { + const contextSection = getDiagnosticStringValue(diagnostic.context?.section); + if (contextSection) { + return contextSection; + } + const quoted = diagnostic.message.match(/section "([^"]+)"/i)?.[1]; + return quoted ?? diagnostic.section ?? null; +} +function getDuplicateMappingRowValue(diagnostic) { + if (!/duplicate mapping row/i.test(diagnostic.message)) { + return null; + } + return getFirstQuotedDiagnosticValue(diagnostic.message); +} +function getMissingFrontmatterKey(diagnostic) { + if (!/required frontmatter/i.test(diagnostic.message)) { + return null; + } + return getFirstQuotedDiagnosticValue(diagnostic.message); +} +function getFrontmatterExampleForDiagnostic(diagnostic) { + const missingKey = getMissingFrontmatterKey(diagnostic); + if (!missingKey) { + return null; + } + const modelType = findDiagnosticContextValue(diagnostic.context, ["type", "modelType", "fileType"]) ?? "model_type"; + const modelId = missingKey === "id" ? "MODEL-ID" : findDiagnosticContextValue(diagnostic.context, ["id", "modelId"]) ?? "MODEL-ID"; + return ["---", "type: " + modelType, "id: " + modelId, "---"].join("\n"); +} +function getRequestedRenderMode(diagnostic) { + if (!/render_mode/i.test(diagnostic.message)) { + return null; + } + return getFirstQuotedDiagnosticValue(diagnostic.message); +} function getDiagnosticMetadata(diagnostic, t) { const metadata = []; const file = diagnostic.filePath ?? diagnostic.path; @@ -23144,6 +23398,10 @@ function getDiagnosticStringValue(value) { if (typeof value === "number" || typeof value === "boolean") { return String(value); } + if (Array.isArray(value)) { + const parts = value.map((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean" ? String(entry).trim() : "").filter((entry) => entry.length > 0); + return parts.length > 0 ? parts.join(" | ") : null; + } return null; } function getFirstQuotedDiagnosticValue(message) { @@ -23159,6 +23417,9 @@ function formatDiagnosticAsMarkdown(diagnostic, localizedMessage, t) { for (const entry of getDiagnosticMetadata(diagnostic, t)) { lines.push("- " + entry.label + ": " + entry.value); } + for (const entry of getDiagnosticDetailEntries(diagnostic, t)) { + lines.push("- " + entry.label + ": " + entry.value); + } return lines.join("\n"); } function toModelWeaveUiLanguage(language) { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 75a5e6e..d47de98 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -101,6 +101,8 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = { "diagnostics.copyMessage": "Copy message", "diagnostics.copyMarkdown": "Copy diagnostic as Markdown", "diagnostics.copyReference": "Copy reference", + "diagnostics.copyExpectedHeader": "Copy expected header", + "diagnostics.copyFrontmatterExample": "Copy frontmatter example", "diagnostics.severity.error": "Error", "diagnostics.severity.warning": "Warning", "diagnostics.severity.note": "Note", @@ -109,9 +111,26 @@ export const EN_MESSAGES: ModelWeaveMessageDictionary = { "diagnostics.meta.line": "Line", "diagnostics.meta.row": "Row", "diagnostics.meta.reference": "Reference", + "diagnostics.meta.field": "Field", "diagnostics.meta.severity": "Severity", "diagnostics.meta.code": "Code", "diagnostics.meta.message": "Message", + "diagnostics.details.title": "Diagnostic details", + "diagnostics.details.expectedHeader": "Expected header", + "diagnostics.details.actualHeader": "Actual header", + "diagnostics.details.missingColumns": "Missing columns", + "diagnostics.details.extraColumns": "Extra columns", + "diagnostics.details.unresolvedReference": "Unresolved reference", + "diagnostics.details.duplicateTarget": "Duplicate target", + "diagnostics.details.mappingRow": "Mapping row", + "diagnostics.details.frontmatterKey": "Missing frontmatter key", + "diagnostics.details.frontmatterExample": "Frontmatter example", + "diagnostics.details.requestedRenderMode": "Requested render mode", + "diagnostics.details.effectiveRenderMode": "Effective render mode", + "diagnostics.details.formatDefault": "Format default", + "diagnostics.details.diagramCompatibility": "Diagram compatibility", + "diagnostics.details.notClassDiagramCompatible": "Exists, but is excluded from class diagram rendering", + "diagnostics.details.targetType": "Target type", "objectContext.title": "Related objects", "objectContext.linked": "{count} linked", "objectContext.connectionDetails": "Connection details", diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index efb8602..dfa229a 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -95,6 +95,8 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = { "diagnostics.copyMessage": "メッセージをコピー", "diagnostics.copyMarkdown": "診断をMarkdownでコピー", "diagnostics.copyReference": "参照値をコピー", + "diagnostics.copyExpectedHeader": "期待ヘッダーをコピー", + "diagnostics.copyFrontmatterExample": "frontmatter 例をコピー", "diagnostics.severity.error": "エラー", "diagnostics.severity.warning": "警告", "diagnostics.severity.note": "ノート", @@ -103,9 +105,26 @@ export const JA_MESSAGES: ModelWeaveMessageDictionary = { "diagnostics.meta.line": "行", "diagnostics.meta.row": "行番号", "diagnostics.meta.reference": "参照値", + "diagnostics.meta.field": "フィールド", "diagnostics.meta.severity": "重要度", "diagnostics.meta.code": "コード", "diagnostics.meta.message": "メッセージ", + "diagnostics.details.title": "診断詳細", + "diagnostics.details.expectedHeader": "期待されるヘッダー", + "diagnostics.details.actualHeader": "実際のヘッダー", + "diagnostics.details.missingColumns": "不足している列", + "diagnostics.details.extraColumns": "余分な列", + "diagnostics.details.unresolvedReference": "未解決の参照", + "diagnostics.details.duplicateTarget": "重複している対象", + "diagnostics.details.mappingRow": "Mapping row", + "diagnostics.details.frontmatterKey": "不足している frontmatter key", + "diagnostics.details.frontmatterExample": "frontmatter 例", + "diagnostics.details.requestedRenderMode": "要求された描画モード", + "diagnostics.details.effectiveRenderMode": "実際の描画モード", + "diagnostics.details.formatDefault": "format 既定", + "diagnostics.details.diagramCompatibility": "図との互換性", + "diagnostics.details.notClassDiagramCompatible": "存在しますが、Class Diagram の描画対象外です", + "diagnostics.details.targetType": "参照先 type", "objectContext.title": "関連オブジェクト", "objectContext.linked": "{count} 件の関連", "objectContext.connectionDetails": "接続詳細", diff --git a/src/views/modeling-preview-view.ts b/src/views/modeling-preview-view.ts index a5d0247..4451579 100644 --- a/src/views/modeling-preview-view.ts +++ b/src/views/modeling-preview-view.ts @@ -4903,6 +4903,21 @@ function renderDiagnosticCard( } } + const details = getDiagnosticDetailEntries(diagnostic, t); + if (details.length > 0) { + const detailBox = card.createDiv({ cls: "model-weave-diagnostic-detail-box" }); + detailBox.createDiv({ + text: t("diagnostics.details.title"), + cls: "model-weave-diagnostic-detail-title" + }); + const detailList = detailBox.createDiv({ cls: "model-weave-diagnostic-meta-list" }); + for (const entry of details) { + const item = detailList.createDiv({ cls: "model-weave-diagnostic-meta" }); + item.createSpan({ text: entry.label, cls: "model-weave-diagnostic-meta-label" }); + item.createSpan({ text: entry.value, cls: "model-weave-diagnostic-meta-value" }); + } + } + const actions = card.createDiv({ cls: "model-weave-diagnostic-actions" }); if (onOpenDiagnostic) { const openButton = actions.createEl("button", { @@ -4939,6 +4954,26 @@ function renderDiagnosticCard( "model-weave-diagnostic-action" ); } + + const expectedHeader = getExpectedHeaderForDiagnostic(diagnostic); + if (expectedHeader) { + renderDiagnosticCopyButton( + actions, + t("diagnostics.copyExpectedHeader"), + expectedHeader, + "model-weave-diagnostic-action" + ); + } + + const frontmatterExample = getFrontmatterExampleForDiagnostic(diagnostic); + if (frontmatterExample) { + renderDiagnosticCopyButton( + actions, + t("diagnostics.copyFrontmatterExample"), + frontmatterExample, + "model-weave-diagnostic-action" + ); + } } function renderDiagnosticCopyButton( @@ -4971,6 +5006,229 @@ function getDiagnosticSeverityLabel( return t("diagnostics.severity.note"); } +interface DiagnosticDetailEntry { + label: string; + value: string; +} + +function getDiagnosticDetailEntries( + diagnostic: ValidationWarning, + t: ModelWeaveTranslator +): DiagnosticDetailEntry[] { + const details: DiagnosticDetailEntry[] = []; + const expectedHeader = getExpectedHeaderForDiagnostic(diagnostic); + if (expectedHeader) { + details.push({ label: t("diagnostics.details.expectedHeader"), value: expectedHeader }); + } + + const actualHeader = findDiagnosticContextValue(diagnostic.context, [ + "actualHeader", + "actualHeaders", + "header", + "headers" + ]); + if (actualHeader) { + details.push({ label: t("diagnostics.details.actualHeader"), value: actualHeader }); + } + + const missingColumns = findDiagnosticContextValue(diagnostic.context, ["missingColumns", "missing"]); + if (missingColumns) { + details.push({ label: t("diagnostics.details.missingColumns"), value: missingColumns }); + } + + const extraColumns = findDiagnosticContextValue(diagnostic.context, ["extraColumns", "extra"]); + if (extraColumns) { + details.push({ label: t("diagnostics.details.extraColumns"), value: extraColumns }); + } + + const reference = getDiagnosticReferenceValue(diagnostic); + if (reference) { + const referenceLabel = diagnostic.code === "duplicate-mapping-target-member" + ? t("diagnostics.details.duplicateTarget") + : diagnostic.code === "unresolved-reference" + ? t("diagnostics.details.unresolvedReference") + : t("diagnostics.meta.reference"); + details.push({ label: referenceLabel, value: reference }); + } + + const duplicateMappingRow = getDuplicateMappingRowValue(diagnostic); + if (duplicateMappingRow) { + details.push({ label: t("diagnostics.details.mappingRow"), value: duplicateMappingRow }); + } + + const missingFrontmatterKey = getMissingFrontmatterKey(diagnostic); + if (missingFrontmatterKey) { + details.push({ label: t("diagnostics.details.frontmatterKey"), value: missingFrontmatterKey }); + } + + const frontmatterExample = getFrontmatterExampleForDiagnostic(diagnostic); + if (frontmatterExample) { + details.push({ label: t("diagnostics.details.frontmatterExample"), value: frontmatterExample }); + } + + const requestedRenderMode = getRequestedRenderMode(diagnostic); + if (requestedRenderMode) { + details.push({ label: t("diagnostics.details.requestedRenderMode"), value: requestedRenderMode }); + details.push({ + label: t("diagnostics.details.effectiveRenderMode"), + value: t("diagnostics.details.formatDefault") + }); + } + + if (diagnostic.code === "class-relation-target-not-diagram-compatible") { + details.push({ + label: t("diagnostics.details.diagramCompatibility"), + value: t("diagnostics.details.notClassDiagramCompatible") + }); + const targetType = findDiagnosticContextValue(diagnostic.context, [ + "targetType", + "resolvedType", + "fileType", + "modelType" + ]); + if (targetType) { + details.push({ label: t("diagnostics.details.targetType"), value: targetType }); + } + } + + const section = getDiagnosticSectionName(diagnostic); + if (section && /row|reference|mapping|frontmatter|render_mode|class relation/i.test(diagnostic.message)) { + details.push({ label: t("diagnostics.meta.section"), value: section }); + } + const row = getDiagnosticStringValue(diagnostic.context?.rowIndex); + if (row && /row|reference|mapping/i.test(diagnostic.message)) { + details.push({ label: t("diagnostics.meta.row"), value: row }); + } + const field = getDiagnosticStringValue(diagnostic.context?.field) ?? diagnostic.field; + if (field && /reference|field/i.test(diagnostic.message)) { + details.push({ label: t("diagnostics.meta.field"), value: field }); + } + + return dedupeDiagnosticDetailEntries(details); +} + +function dedupeDiagnosticDetailEntries(entries: DiagnosticDetailEntry[]): DiagnosticDetailEntry[] { + const seen = new Set(); + const result: DiagnosticDetailEntry[] = []; + for (const entry of entries) { + const key = entry.label + "\u0000" + entry.value; + if (seen.has(key)) { + continue; + } + seen.add(key); + result.push(entry); + } + return result; +} + +function getExpectedHeaderForDiagnostic(diagnostic: ValidationWarning): string | null { + const contextHeader = findDiagnosticContextValue(diagnostic.context, [ + "expectedHeader", + "expectedHeaders", + "expected" + ]); + if (contextHeader) { + return contextHeader; + } + + if (!/table columns in section/i.test(diagnostic.message)) { + return null; + } + + const message = diagnostic.message; + if (/screen field headers/i.test(message)) { + return "id | label | kind | layout | rule | ref | default | required | notes"; + } + if (/legacy headers/i.test(message) && /Transitions/i.test(message)) { + return "id | event | to | condition | notes"; + } + if (/supported DFD object headers/i.test(message)) { + return "id | label | kind | ref | domain | notes"; + } + if (/supported class relation headers/i.test(message)) { + return "id | from | to | kind | label | notes"; + } + if (/Domain Sources headers/i.test(message)) { + return "ref | notes"; + } + if (/app_process step headers/i.test(message)) { + return "id | domain | label | kind | input | output | rule | invoke | screen | notes"; + } + + const section = getDiagnosticSectionName(diagnostic); + return section ? getGenericExpectedHeaderForSection(section) : null; +} + +function getGenericExpectedHeaderForSection(section: string): string | null { + const normalized = section.trim().toLowerCase(); + if (normalized === "mappings") { + return "source_ref | target_ref | transform | rule | required | notes"; + } + if (normalized === "layout") { + return "id | label | kind | parent | order | notes"; + } + if (normalized === "actions") { + return "id | target | event | kind | invoke | transition | rule | condition | notes"; + } + if (normalized === "messages") { + return "id | timing | severity | audience | text | notes"; + } + if (normalized === "objects") { + return "id | label | kind | ref | domain | notes"; + } + if (normalized === "flows") { + return "id | from | to | label | kind | notes"; + } + if (normalized === "domain sources") { + return "ref | notes"; + } + return null; +} + +function getDiagnosticSectionName(diagnostic: ValidationWarning): string | null { + const contextSection = getDiagnosticStringValue(diagnostic.context?.section); + if (contextSection) { + return contextSection; + } + const quoted = diagnostic.message.match(/section "([^"]+)"/i)?.[1]; + return quoted ?? diagnostic.section ?? null; +} + +function getDuplicateMappingRowValue(diagnostic: ValidationWarning): string | null { + if (!/duplicate mapping row/i.test(diagnostic.message)) { + return null; + } + return getFirstQuotedDiagnosticValue(diagnostic.message); +} + +function getMissingFrontmatterKey(diagnostic: ValidationWarning): string | null { + if (!/required frontmatter/i.test(diagnostic.message)) { + return null; + } + return getFirstQuotedDiagnosticValue(diagnostic.message); +} + +function getFrontmatterExampleForDiagnostic(diagnostic: ValidationWarning): string | null { + const missingKey = getMissingFrontmatterKey(diagnostic); + if (!missingKey) { + return null; + } + const modelType = findDiagnosticContextValue(diagnostic.context, ["type", "modelType", "fileType"]) + ?? "model_type"; + const modelId = missingKey === "id" + ? "MODEL-ID" + : findDiagnosticContextValue(diagnostic.context, ["id", "modelId"]) + ?? "MODEL-ID"; + return ["---", "type: " + modelType, "id: " + modelId, "---"].join("\n"); +} + +function getRequestedRenderMode(diagnostic: ValidationWarning): string | null { + if (!/render_mode/i.test(diagnostic.message)) { + return null; + } + return getFirstQuotedDiagnosticValue(diagnostic.message); +} + function getDiagnosticMetadata( diagnostic: ValidationWarning, t: ModelWeaveTranslator @@ -5041,6 +5299,14 @@ function getDiagnosticStringValue(value: unknown): string | null { if (typeof value === "number" || typeof value === "boolean") { return String(value); } + if (Array.isArray(value)) { + const parts = value + .map((entry) => typeof entry === "string" || typeof entry === "number" || typeof entry === "boolean" + ? String(entry).trim() + : "") + .filter((entry) => entry.length > 0); + return parts.length > 0 ? parts.join(" | ") : null; + } return null; } @@ -5062,6 +5328,9 @@ function formatDiagnosticAsMarkdown( for (const entry of getDiagnosticMetadata(diagnostic, t)) { lines.push("- " + entry.label + ": " + entry.value); } + for (const entry of getDiagnosticDetailEntries(diagnostic, t)) { + lines.push("- " + entry.label + ": " + entry.value); + } return lines.join("\n"); } diff --git a/styles.css b/styles.css index 54ec04c..fb5c4a0 100644 --- a/styles.css +++ b/styles.css @@ -1250,6 +1250,21 @@ body.model-weave-focus-mode-active { line-height: 1.45; } +.model-weave-diagnostic-detail-box { + margin-top: 8px; + padding: 8px; + border: 1px solid var(--background-modifier-border); + border-radius: 6px; + background: var(--background-secondary); +} + +.model-weave-diagnostic-detail-title { + margin-bottom: 6px; + color: var(--text-muted); + font-size: var(--model-weave-font-size-small); + font-weight: 600; +} + .model-weave-diagnostic-meta-list { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); diff --git a/test/localization-language.test.mjs b/test/localization-language.test.mjs index 7682372..227187a 100644 --- a/test/localization-language.test.mjs +++ b/test/localization-language.test.mjs @@ -214,6 +214,14 @@ test("preview and related-view labels respect UI language dictionaries", () => { en("screen.preview.layoutMissing"), "Layout is missing or undefined" ); + assert.equal(en("diagnostics.details.title"), "Diagnostic details"); + assert.equal(en("diagnostics.details.expectedHeader"), "Expected header"); + assert.equal(en("diagnostics.details.unresolvedReference"), "Unresolved reference"); + assert.equal(en("diagnostics.copyExpectedHeader"), "Copy expected header"); + assert.equal(ja("diagnostics.details.title"), "診断詳細"); + assert.equal(ja("diagnostics.details.expectedHeader"), "期待されるヘッダー"); + assert.equal(ja("diagnostics.details.unresolvedReference"), "未解決の参照"); + assert.equal(ja("diagnostics.copyExpectedHeader"), "期待ヘッダーをコピー"); assert.equal(en("sourceLinks.title"), "Source links"); assert.equal(en("sourceLinks.path"), "Path"); assert.equal(en("sourceLinks.status"), "Status");