mirror of
https://github.com/nejimakibird/model-weave.git
synced 2026-07-22 06:51:24 +00:00
feat: prefer target-first mapping format
This commit is contained in:
parent
278ae64e4c
commit
bdf77da86b
11 changed files with 238 additions and 79 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
73
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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, "") ?? "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -516,7 +516,7 @@ tags:
|
|||
|
||||
## Mappings
|
||||
|
||||
| source_ref | target_ref | transform | rule | required | notes |
|
||||
| target_ref | source_ref | transform | rule | required | notes |
|
||||
|---|---|---|---|---|---|
|
||||
|
||||
## Rules
|
||||
|
|
|
|||
|
|
@ -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"],
|
||||
|
|
|
|||
|
|
@ -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: `---
|
||||
|
|
|
|||
Loading…
Reference in a new issue