From bdf77da86b7c46b3660a070099dcfb7cf417aedb Mon Sep 17 00:00:00 2001 From: nejimakibird <23066051+nejimakibird@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:00:36 +0900 Subject: [PATCH] feat: prefer target-first mapping format --- Templates/mapping/mapping.template.md | 6 +- docs/formats/FORMAT-mapping.md | 42 ++++++----- docs/ja/formats/FORMAT-mapping.md | 42 ++++++----- main.js | 73 +++++++++++++++---- .../MAP-INVENTORY-SEARCH-SCREEN-TO-DATA.md | 12 +-- src/core/diagnostic-section-guidance.ts | 2 +- src/editor/model-weave-editor-suggest.ts | 48 +++++++++--- src/parsers/mapping-parser.ts | 29 +++++++- src/templates/model-weave-templates.ts | 2 +- test/diagnostic-section-guidance.test.mjs | 2 +- test/domains-model.test.mjs | 59 +++++++++++++++ 11 files changed, 238 insertions(+), 79 deletions(-) diff --git a/Templates/mapping/mapping.template.md b/Templates/mapping/mapping.template.md index d05ff71..07fb27b 100644 --- a/Templates/mapping/mapping.template.md +++ b/Templates/mapping/mapping.template.md @@ -24,10 +24,10 @@ Describe the mapping purpose and scope. ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-SAMPLE-SOURCE]].sample_id | [[DATA-SAMPLE-TARGET]].sample_id | Direct copy | | Y | | -| [[DATA-SAMPLE-SOURCE]].sample_name | [[DATA-SAMPLE-TARGET]].sample_name | Direct copy | | N | | +| [[DATA-SAMPLE-TARGET]].sample_id | [[DATA-SAMPLE-SOURCE]].sample_id | Direct copy | | Y | | +| [[DATA-SAMPLE-TARGET]].sample_name | [[DATA-SAMPLE-SOURCE]].sample_name | Direct copy | | N | | ## Rules diff --git a/docs/formats/FORMAT-mapping.md b/docs/formats/FORMAT-mapping.md index 40d98f0..3bbe0d4 100644 --- a/docs/formats/FORMAT-mapping.md +++ b/docs/formats/FORMAT-mapping.md @@ -93,10 +93,10 @@ Maps order request fields to the order entity. ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | | -| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | | +| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | | +| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | | ``` ## Full example @@ -128,14 +128,14 @@ Maps an external inventory CSV file to inventory-related database entities. ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | [[ENT-INVENTORY]].shipper_id | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion | -| [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | [[ENT-INVENTORY]].warehouse_id | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion | -| [[DATA-INVENTORY-IMPORT-FILE]].item_code | [[ENT-INVENTORY]].item_id | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion | -| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-INVENTORY]].quantity | parse decimal | | Y | Convert text to numeric | -| [[DATA-INVENTORY-IMPORT-FILE]].status | [[ENT-INVENTORY]].inventory_status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value | -| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-STOCK-MOVEMENT]].movement_quantity | copy when quantity is positive | | N | Create movement row only when quantity exists | +| [[ENT-INVENTORY]].shipper_id | [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion | +| [[ENT-INVENTORY]].warehouse_id | [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion | +| [[ENT-INVENTORY]].item_id | [[DATA-INVENTORY-IMPORT-FILE]].item_code | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion | +| [[ENT-INVENTORY]].quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | parse decimal | | Y | Convert text to numeric | +| [[ENT-INVENTORY]].inventory_status | [[DATA-INVENTORY-IMPORT-FILE]].status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value | +| [[ENT-STOCK-MOVEMENT]].movement_quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | copy when quantity is positive | | N | Create movement row only when quantity exists | ## Source Links @@ -245,16 +245,18 @@ Use `## Mappings` to define field-level mapping rows. Expected header: ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| ``` +The legacy source-first header `source_ref | target_ref | transform | rule | required | notes` remains supported without diagnostics for existing files, but new files should use the target-first order above. + Columns: | column | meaning | | ------------ | --------------------------------------------------------------------------------------------------------- | -| `source_ref` | Source field, value, expression, or model reference. May point to members such as `[[DATA-ID]].fieldName`. | | `target_ref` | Target field, value, expression, or model reference. May point to members such as `[[DATA-ID]].fieldName`. | +| `source_ref` | Source field, value, expression, or model reference. May point to members such as `[[DATA-ID]].fieldName`. | | `transform` | Transformation description. Use simple text such as `copy`, `format date`, `lookup`, or `convert status`. | | `rule` | Optional related `rule` reference when transformation is governed by a rule model. | | `required` | Whether the mapping is mandatory. Use `Y` or `N`. | @@ -265,11 +267,11 @@ Example: ```markdown ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | | -| [[DATA-ORDER-REQUEST]].status | [[ENT-ORDER]].order_status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion | -| current_user.user_id | [[ENT-ORDER]].created_by | copy | | N | Audit field | +| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | | +| [[ENT-ORDER]].order_status | [[DATA-ORDER-REQUEST]].status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion | +| [[ENT-ORDER]].created_by | current_user.user_id | copy | | N | Audit field | ``` Notes: @@ -325,7 +327,7 @@ Do not add unsupported columns to structured tables just to store extra informat ### Mappings table ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| ``` @@ -452,7 +454,7 @@ Avoid using display labels as mapping endpoints. Risky: ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| | Customer ID | Customer ID | copy | | Y | ambiguous | ``` @@ -460,9 +462,9 @@ Risky: Prefer stable qualified references: ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | | +| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | | ``` ### Confusing mapping with app_process diff --git a/docs/ja/formats/FORMAT-mapping.md b/docs/ja/formats/FORMAT-mapping.md index d2ae679..64094cb 100644 --- a/docs/ja/formats/FORMAT-mapping.md +++ b/docs/ja/formats/FORMAT-mapping.md @@ -93,10 +93,10 @@ Maps order request fields to the order entity. ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | | -| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | | +| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | | +| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | | ``` ## 詳細例 @@ -128,14 +128,14 @@ Maps an external inventory CSV file to inventory-related database entities. ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | [[ENT-INVENTORY]].shipper_id | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion | -| [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | [[ENT-INVENTORY]].warehouse_id | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion | -| [[DATA-INVENTORY-IMPORT-FILE]].item_code | [[ENT-INVENTORY]].item_id | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion | -| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-INVENTORY]].quantity | parse decimal | | Y | Convert text to numeric | -| [[DATA-INVENTORY-IMPORT-FILE]].status | [[ENT-INVENTORY]].inventory_status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value | -| [[DATA-INVENTORY-IMPORT-FILE]].quantity | [[ENT-STOCK-MOVEMENT]].movement_quantity | copy when quantity is positive | | N | Create movement row only when quantity exists | +| [[ENT-INVENTORY]].shipper_id | [[DATA-INVENTORY-IMPORT-FILE]].shipper_code | lookup shipper_id by code | [[RULE-SHIPPER-LOOKUP]] | Y | External code conversion | +| [[ENT-INVENTORY]].warehouse_id | [[DATA-INVENTORY-IMPORT-FILE]].warehouse_code | lookup warehouse_id by code | [[RULE-WAREHOUSE-LOOKUP]] | Y | External code conversion | +| [[ENT-INVENTORY]].item_id | [[DATA-INVENTORY-IMPORT-FILE]].item_code | lookup item_id by code | [[RULE-ITEM-LOOKUP]] | Y | External code conversion | +| [[ENT-INVENTORY]].quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | parse decimal | | Y | Convert text to numeric | +| [[ENT-INVENTORY]].inventory_status | [[DATA-INVENTORY-IMPORT-FILE]].status | convert status | [[RULE-INVENTORY-STATUS-CONVERT]] | Y | Maps external status to CodeSet value | +| [[ENT-STOCK-MOVEMENT]].movement_quantity | [[DATA-INVENTORY-IMPORT-FILE]].quantity | copy when quantity is positive | | N | Create movement row only when quantity exists | ## Source Links @@ -245,16 +245,18 @@ tags: 期待されるヘッダー: ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| ``` +旧形式の source-first header `source_ref | target_ref | transform | rule | required | notes` は既存ファイル互換のため診断なしで読み取れます。ただし新規ファイルでは上記の target-first order を使ってください。 + 列の意味: | column | meaning | | ------------ | ----------------------------------------------------------------------- | -| `source_ref` | source field、値、式、model referenceです。`[[DATA-ID]].fieldName` のようなmember参照を使えます。 | | `target_ref` | target field、値、式、model referenceです。`[[DATA-ID]].fieldName` のようなmember参照を使えます。 | +| `source_ref` | source field、値、式、model referenceです。`[[DATA-ID]].fieldName` のようなmember参照を使えます。 | | `transform` | 変換内容です。`copy`, `format date`, `lookup`, `convert status` などの短い説明を使います。 | | `rule` | 変換がrule modelに従う場合の任意の関連 `rule` 参照です。 | | `required` | mappingが必須かどうかを示します。`Y` または `N` を使います。 | @@ -265,11 +267,11 @@ tags: ```markdown ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-ORDER-REQUEST]].order_id | [[ENT-ORDER]].order_id | copy | | Y | | -| [[DATA-ORDER-REQUEST]].status | [[ENT-ORDER]].order_status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion | -| current_user.user_id | [[ENT-ORDER]].created_by | copy | | N | Audit field | +| [[ENT-ORDER]].order_id | [[DATA-ORDER-REQUEST]].order_id | copy | | Y | | +| [[ENT-ORDER]].order_status | [[DATA-ORDER-REQUEST]].status | convert status | [[RULE-ORDER-STATUS-CONVERT]] | Y | Code conversion | +| [[ENT-ORDER]].created_by | current_user.user_id | copy | | N | Audit field | ``` 注意: @@ -326,7 +328,7 @@ mappingを、実装ファイル、インターフェース仕様、ETLスクリ ### Mappings table ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| ``` @@ -453,7 +455,7 @@ FORMATが明示的に定義していない限り、`id`, `source`, `target`, `co 危険な例: ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| | Customer ID | Customer ID | copy | | Y | ambiguous | ``` @@ -461,9 +463,9 @@ FORMATが明示的に定義していない限り、`id`, `source`, `target`, `co 推奨: ```markdown -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[DATA-ORDER-REQUEST]].customer_id | [[ENT-ORDER]].customer_id | copy | | Y | | +| [[ENT-ORDER]].customer_id | [[DATA-ORDER-REQUEST]].customer_id | copy | | Y | | ``` ### mappingとapp_processを混同する diff --git a/main.js b/main.js index 13c6dad..4e4a66e 100644 --- a/main.js +++ b/main.js @@ -1633,7 +1633,7 @@ var SECTION_HEADERS = { }, mapping: { scope: "role | ref | notes", - mappings: "source_ref | target_ref | transform | rule | required | notes", + mappings: "target_ref | source_ref | transform | rule | required | notes", "source links": SOURCE_LINKS_HEADER }, message: { @@ -7637,32 +7637,36 @@ function getMappingCompletion(lines, cursor, line, index) { } } if (section === "Mappings") { - if (!hasTableHeader(lines, cursor.line, ["source_ref", "target_ref", "transform", "rule", "required", "notes"])) { + const mappingHeader = getNearestSupportedMappingHeader(lines, cursor.line); + if (!mappingHeader) { return null; } - if (cell.columnIndex === 0 || cell.columnIndex === 1) { + const columnName = mappingHeader[cell.columnIndex]; + if (columnName === "source_ref" || columnName === "target_ref") { const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch); + const isSourceRef = columnName === "source_ref"; + const placeholder = isSourceRef ? "Complete mapping source_ref" : "Complete mapping target_ref"; const qualifiedMemberRequest = getQualifiedMemberCompletionRequest( cursor, cell, cellValue, index, - cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref" + placeholder ); if (qualifiedMemberRequest) { return qualifiedMemberRequest; } return { - kind: cell.columnIndex === 0 ? "mapping-source-ref" : "mapping-target-ref", + kind: isSourceRef ? "mapping-source-ref" : "mapping-target-ref", replaceFrom: cell.replaceFrom, replaceTo: cell.replaceTo, suggestions: buildStructuredReferenceSuggestions(index), - placeholder: cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref", + placeholder, initialQuery: normalizeCompletionQuery(cellValue), tableColumnIndex: cell.columnIndex }; } - if (cell.columnIndex === 3) { + if (columnName === "rule") { return { kind: "mapping-rule-ref", replaceFrom: cell.replaceFrom, @@ -7675,17 +7679,17 @@ function getMappingCompletion(lines, cursor, line, index) { initialQuery: normalizeCompletionQuery( extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch) ), - tableColumnIndex: 3 + tableColumnIndex: cell.columnIndex }; } - if (cell.columnIndex === 4) { + if (columnName === "required") { return buildOptionCompletionRequest( "mapping-rule-ref", cell, line, ["Y", "N"], "Complete mapping required", - 4 + cell.columnIndex ); } } @@ -7827,6 +7831,25 @@ function getScreenFieldTargetSuggestions(lines) { kind: "reference" })); } +function getNearestSupportedMappingHeader(lines, cursorLine) { + const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => { + const row = parseMarkdownTableRow(candidate); + return isSupportedMappingHeader(row); + }); + if (tableHeaderIndex < 0 || cursorLine <= tableHeaderIndex + 1) { + return null; + } + return parseMarkdownTableRow(lines[tableHeaderIndex] ?? ""); +} +function isSupportedMappingHeader(header) { + if (!header) { + return false; + } + return sameOrderedHeaders(header, ["target_ref", "source_ref", "transform", "rule", "required", "notes"]) || sameOrderedHeaders(header, ["source_ref", "target_ref", "transform", "rule", "required", "notes"]); +} +function sameOrderedHeaders(actual, expected) { + return actual.length === expected.length && expected.every((header, index) => actual[index] === header); +} function hasTableHeader(lines, cursorLine, expectedHeader) { const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => { const row = parseMarkdownTableRow(candidate); @@ -10584,7 +10607,7 @@ tags: ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| ## Rules @@ -13512,7 +13535,8 @@ function createWarning16(path2, field, message) { // src/parsers/mapping-parser.ts var SCOPE_HEADERS = ["role", "ref", "notes"]; -var MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"]; +var MAPPING_HEADERS = ["target_ref", "source_ref", "transform", "rule", "required", "notes"]; +var LEGACY_MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"]; function parseMappingFile(markdown, path2) { const frontmatterResult = parseFrontmatter(markdown); const frontmatter = frontmatterResult.file.frontmatter ?? {}; @@ -13536,7 +13560,12 @@ function parseMappingFile(markdown, path2) { warnings.push(createWarning17(path2, "name", 'required frontmatter "name" is missing')); } const scopeTable = parseMarkdownTable(sections.Scope, SCOPE_HEADERS, path2, "Scope"); - const mappingsTable = parseMarkdownTable(sections.Mappings, MAPPING_HEADERS, path2, "Mappings"); + const mappingsTable = parseMarkdownTable( + sections.Mappings, + getAcceptedMappingHeaders(sections.Mappings), + path2, + "Mappings" + ); warnings.push(...scopeTable.warnings, ...mappingsTable.warnings); const fallbackName = name || id || getFileStem13(path2) || "Untitled Mapping"; return { @@ -13572,6 +13601,20 @@ function parseMappingFile(markdown, path2) { warnings }; } +function getAcceptedMappingHeaders(lines) { + const actualHeader = getMarkdownTableHeader(lines); + if (actualHeader && sameHeaders7(actualHeader, LEGACY_MAPPING_HEADERS)) { + return [...LEGACY_MAPPING_HEADERS]; + } + return [...MAPPING_HEADERS]; +} +function getMarkdownTableHeader(lines) { + const headerLine = lines?.map((line) => line.trim()).find((line) => line.startsWith("|")); + return headerLine ? splitMarkdownTableRow(headerLine) : null; +} +function sameHeaders7(headers, expectedHeaders) { + return headers.length === expectedHeaders.length && expectedHeaders.every((header, index) => headers[index] === header); +} function getFileStem13(path2) { return path2.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? ""; } @@ -22084,7 +22127,7 @@ function updateColorSchemeColorCell(markdown, request) { return unchanged2(markdown, "table-missing"); } const headers = splitMarkdownTableRow(lines[tableStart]) ?? []; - if (!sameHeaders7(headers, [...COLOR_HEADERS2])) { + if (!sameHeaders8(headers, [...COLOR_HEADERS2])) { return unchanged2(markdown, "header-mismatch"); } const separatorCells = splitMarkdownTableRow(lines[tableStart + 1]); @@ -22171,7 +22214,7 @@ function findDataRowLineIndex(lines, start, end, targetRowIndex) { } return null; } -function sameHeaders7(actual, expected) { +function sameHeaders8(actual, expected) { return actual.length === expected.length && actual.every((header, index) => header === expected[index]); } function normalizeHeadingName(value) { diff --git a/samples/mapping/MAP-INVENTORY-SEARCH-SCREEN-TO-DATA.md b/samples/mapping/MAP-INVENTORY-SEARCH-SCREEN-TO-DATA.md index e49082f..9932321 100644 --- a/samples/mapping/MAP-INVENTORY-SEARCH-SCREEN-TO-DATA.md +++ b/samples/mapping/MAP-INVENTORY-SEARCH-SCREEN-TO-DATA.md @@ -26,13 +26,13 @@ Maps inventory search screen fields to inventory search condition data object. ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| -| [[../screen/SCR-INVENTORY-SEARCH]].shipper_id | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].shipper_id | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | Y | | -| [[../screen/SCR-INVENTORY-SEARCH]].warehouse_id | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].warehouse_id | Direct copy | | N | | -| [[../screen/SCR-INVENTORY-SEARCH]].item_id | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].item_id | Direct copy | | N | | -| [[../screen/SCR-INVENTORY-SEARCH]].lot_no | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].lot_no | Direct copy | | N | | -| [[../screen/SCR-INVENTORY-SEARCH]].inventory_status | [[../data/DATA-INVENTORY-SEARCH-CONDITION]].inventory_status | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | N | CodeSet value | +| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].shipper_id | [[../screen/SCR-INVENTORY-SEARCH]].shipper_id | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | Y | | +| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].warehouse_id | [[../screen/SCR-INVENTORY-SEARCH]].warehouse_id | Direct copy | | N | | +| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].item_id | [[../screen/SCR-INVENTORY-SEARCH]].item_id | Direct copy | | N | | +| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].lot_no | [[../screen/SCR-INVENTORY-SEARCH]].lot_no | Direct copy | | N | | +| [[../data/DATA-INVENTORY-SEARCH-CONDITION]].inventory_status | [[../screen/SCR-INVENTORY-SEARCH]].inventory_status | Direct copy | [[../rule/RULE-INVENTORY-SEARCH-CONDITION]] | N | CodeSet value | ## Rules diff --git a/src/core/diagnostic-section-guidance.ts b/src/core/diagnostic-section-guidance.ts index 61bd759..f745d2e 100644 --- a/src/core/diagnostic-section-guidance.ts +++ b/src/core/diagnostic-section-guidance.ts @@ -79,7 +79,7 @@ const SECTION_HEADERS: Partial>> = { }, mapping: { scope: "role | ref | notes", - mappings: "source_ref | target_ref | transform | rule | required | notes", + mappings: "target_ref | source_ref | transform | rule | required | notes", "source links": SOURCE_LINKS_HEADER }, message: { diff --git a/src/editor/model-weave-editor-suggest.ts b/src/editor/model-weave-editor-suggest.ts index 8933ef1..a50d68d 100644 --- a/src/editor/model-weave-editor-suggest.ts +++ b/src/editor/model-weave-editor-suggest.ts @@ -1691,32 +1691,36 @@ function getMappingCompletion( } if (section === "Mappings") { - if (!hasTableHeader(lines, cursor.line, ["source_ref", "target_ref", "transform", "rule", "required", "notes"])) { + const mappingHeader = getNearestSupportedMappingHeader(lines, cursor.line); + if (!mappingHeader) { return null; } - if (cell.columnIndex === 0 || cell.columnIndex === 1) { + const columnName = mappingHeader[cell.columnIndex]; + if (columnName === "source_ref" || columnName === "target_ref") { const cellValue = extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch); + const isSourceRef = columnName === "source_ref"; + const placeholder = isSourceRef ? "Complete mapping source_ref" : "Complete mapping target_ref"; const qualifiedMemberRequest = getQualifiedMemberCompletionRequest( cursor, cell, cellValue, index, - cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref" + placeholder ); if (qualifiedMemberRequest) { return qualifiedMemberRequest; } return { - kind: cell.columnIndex === 0 ? "mapping-source-ref" : "mapping-target-ref", + kind: isSourceRef ? "mapping-source-ref" : "mapping-target-ref", replaceFrom: cell.replaceFrom, replaceTo: cell.replaceTo, suggestions: buildStructuredReferenceSuggestions(index), - placeholder: cell.columnIndex === 0 ? "Complete mapping source_ref" : "Complete mapping target_ref", + placeholder, initialQuery: normalizeCompletionQuery(cellValue), tableColumnIndex: cell.columnIndex }; } - if (cell.columnIndex === 3) { + if (columnName === "rule") { return { kind: "mapping-rule-ref", replaceFrom: cell.replaceFrom, @@ -1731,17 +1735,17 @@ function getMappingCompletion( initialQuery: normalizeCompletionQuery( extractLineText(line, cell.replaceFrom.ch, cell.replaceTo.ch) ), - tableColumnIndex: 3 + tableColumnIndex: cell.columnIndex }; } - if (cell.columnIndex === 4) { + if (columnName === "required") { return buildOptionCompletionRequest( "mapping-rule-ref", cell, line, ["Y", "N"], "Complete mapping required", - 4 + cell.columnIndex ); } } @@ -1958,6 +1962,32 @@ function getScreenFieldTargetSuggestions(lines: string[]): CompletionSuggestion[ })); } +function getNearestSupportedMappingHeader(lines: string[], cursorLine: number): string[] | null { + const tableHeaderIndex = findNearestLine(lines, cursorLine, (candidate) => { + const row = parseMarkdownTableRow(candidate); + return isSupportedMappingHeader(row); + }); + if (tableHeaderIndex < 0 || cursorLine <= tableHeaderIndex + 1) { + return null; + } + + return parseMarkdownTableRow(lines[tableHeaderIndex] ?? ""); +} + +function isSupportedMappingHeader(header: string[] | null): header is string[] { + if (!header) { + return false; + } + return ( + sameOrderedHeaders(header, ["target_ref", "source_ref", "transform", "rule", "required", "notes"]) || + sameOrderedHeaders(header, ["source_ref", "target_ref", "transform", "rule", "required", "notes"]) + ); +} + +function sameOrderedHeaders(actual: readonly string[], expected: readonly string[]): boolean { + return actual.length === expected.length && expected.every((header, index) => actual[index] === header); +} + function hasTableHeader( lines: string[], cursorLine: number, diff --git a/src/parsers/mapping-parser.ts b/src/parsers/mapping-parser.ts index 40fd0e4..1f211c5 100644 --- a/src/parsers/mapping-parser.ts +++ b/src/parsers/mapping-parser.ts @@ -1,11 +1,12 @@ import { parseFrontmatter } from "./frontmatter-parser"; -import { parseMarkdownTable } from "./markdown-table"; +import { parseMarkdownTable, splitMarkdownTableRow } from "./markdown-table"; import { extractMarkdownSections } from "./markdown-sections"; import { parseSourceLinks } from "./source-links-parser"; import type { MappingModel, ValidationWarning } from "../types/models"; const SCOPE_HEADERS = ["role", "ref", "notes"]; -const MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"]; +const MAPPING_HEADERS = ["target_ref", "source_ref", "transform", "rule", "required", "notes"] as const; +const LEGACY_MAPPING_HEADERS = ["source_ref", "target_ref", "transform", "rule", "required", "notes"] as const; export function parseMappingFile( markdown: string, @@ -36,7 +37,12 @@ export function parseMappingFile( } const scopeTable = parseMarkdownTable(sections.Scope, SCOPE_HEADERS, path, "Scope"); - const mappingsTable = parseMarkdownTable(sections.Mappings, MAPPING_HEADERS, path, "Mappings"); + const mappingsTable = parseMarkdownTable( + sections.Mappings, + getAcceptedMappingHeaders(sections.Mappings), + path, + "Mappings" + ); warnings.push(...scopeTable.warnings, ...mappingsTable.warnings); @@ -80,6 +86,23 @@ export function parseMappingFile( }; } +function getAcceptedMappingHeaders(lines: string[] | undefined): string[] { + const actualHeader = getMarkdownTableHeader(lines); + if (actualHeader && sameHeaders(actualHeader, LEGACY_MAPPING_HEADERS)) { + return [...LEGACY_MAPPING_HEADERS]; + } + return [...MAPPING_HEADERS]; +} + +function getMarkdownTableHeader(lines: string[] | undefined): string[] | null { + const headerLine = lines?.map((line) => line.trim()).find((line) => line.startsWith("|")); + return headerLine ? splitMarkdownTableRow(headerLine) : null; +} + +function sameHeaders(headers: readonly string[], expectedHeaders: readonly string[]): boolean { + return headers.length === expectedHeaders.length && expectedHeaders.every((header, index) => headers[index] === header); +} + function getFileStem(path: string): string { return path.replace(/\\/g, "/").split("/").pop()?.replace(/\.md$/i, "") ?? ""; } diff --git a/src/templates/model-weave-templates.ts b/src/templates/model-weave-templates.ts index bf52933..850a3a4 100644 --- a/src/templates/model-weave-templates.ts +++ b/src/templates/model-weave-templates.ts @@ -516,7 +516,7 @@ tags: ## Mappings -| source_ref | target_ref | transform | rule | required | notes | +| target_ref | source_ref | transform | rule | required | notes | |---|---|---|---|---|---| ## Rules diff --git a/test/diagnostic-section-guidance.test.mjs b/test/diagnostic-section-guidance.test.mjs index 74a371e..a42bb27 100644 --- a/test/diagnostic-section-guidance.test.mjs +++ b/test/diagnostic-section-guidance.test.mjs @@ -196,7 +196,7 @@ test("section guidance resolves stable headers across supported model types", () ["data_object", "Format", "key | value | notes"], ["app_process", "Steps", "id | domain | label | kind | input | output | rule | invoke | screen | notes"], ["screen", "Actions", "id | label | kind | target | event | condition | invoke | transition | rule | notes"], - ["mapping", "Mappings", "source_ref | target_ref | transform | rule | required | notes"], + ["mapping", "Mappings", "target_ref | source_ref | transform | rule | required | notes"], ["codeset", "Values", "code | label | sort_order | active | notes"], ["domains", "Domains", "id | name | kind | parent | description"], ["domain_diagram", "Domain Sources", "ref | notes"], diff --git a/test/domains-model.test.mjs b/test/domains-model.test.mjs index 7c28c58..8b52263 100644 --- a/test/domains-model.test.mjs +++ b/test/domains-model.test.mjs @@ -12,6 +12,7 @@ await build({ '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";', @@ -92,6 +93,7 @@ const { parseDomainDiagramFile, parseColorSchemeFile, parseDfdDiagramFile, + parseMappingFile, parseDiagramFile, MODEL_WEAVE_TEMPLATES, DEFAULT_MODEL_WEAVE_SETTINGS, @@ -2376,6 +2378,63 @@ name: Menu actions + +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: `---