feat: prefer target-first mapping format

This commit is contained in:
nejimakibird 2026-07-09 10:00:36 +09:00
parent 278ae64e4c
commit bdf77da86b
11 changed files with 238 additions and 79 deletions

View file

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

View file

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

View file

@ -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を混同する

73
main.js
View file

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

View file

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

View file

@ -79,7 +79,7 @@ const SECTION_HEADERS: Partial<Record<FileType, Record<string, string>>> = {
},
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: {

View file

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

View file

@ -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, "") ?? "";
}

View file

@ -516,7 +516,7 @@ tags:
## Mappings
| source_ref | target_ref | transform | rule | required | notes |
| target_ref | source_ref | transform | rule | required | notes |
|---|---|---|---|---|---|
## Rules

View file

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

View file

@ -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: `---