@rejot-dev/thalo 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +396 -0
- package/dist/ast/ast-types.d.ts +469 -0
- package/dist/ast/ast-types.d.ts.map +1 -0
- package/dist/ast/ast-types.js +11 -0
- package/dist/ast/ast-types.js.map +1 -0
- package/dist/ast/builder.js +158 -0
- package/dist/ast/builder.js.map +1 -0
- package/dist/ast/extract.js +748 -0
- package/dist/ast/extract.js.map +1 -0
- package/dist/ast/node-at-position.d.ts +147 -0
- package/dist/ast/node-at-position.d.ts.map +1 -0
- package/dist/ast/node-at-position.js +382 -0
- package/dist/ast/node-at-position.js.map +1 -0
- package/dist/ast/visitor.js +232 -0
- package/dist/ast/visitor.js.map +1 -0
- package/dist/checker/check.d.ts +53 -0
- package/dist/checker/check.d.ts.map +1 -0
- package/dist/checker/check.js +105 -0
- package/dist/checker/check.js.map +1 -0
- package/dist/checker/rules/actualize-missing-updated.js +34 -0
- package/dist/checker/rules/actualize-missing-updated.js.map +1 -0
- package/dist/checker/rules/actualize-unresolved-target.js +42 -0
- package/dist/checker/rules/actualize-unresolved-target.js.map +1 -0
- package/dist/checker/rules/alter-before-define.js +53 -0
- package/dist/checker/rules/alter-before-define.js.map +1 -0
- package/dist/checker/rules/alter-undefined-entity.js +32 -0
- package/dist/checker/rules/alter-undefined-entity.js.map +1 -0
- package/dist/checker/rules/create-requires-section.js +34 -0
- package/dist/checker/rules/create-requires-section.js.map +1 -0
- package/dist/checker/rules/define-entity-requires-section.js +31 -0
- package/dist/checker/rules/define-entity-requires-section.js.map +1 -0
- package/dist/checker/rules/duplicate-entity-definition.js +37 -0
- package/dist/checker/rules/duplicate-entity-definition.js.map +1 -0
- package/dist/checker/rules/duplicate-field-in-schema.js +38 -0
- package/dist/checker/rules/duplicate-field-in-schema.js.map +1 -0
- package/dist/checker/rules/duplicate-link-id.js +52 -0
- package/dist/checker/rules/duplicate-link-id.js.map +1 -0
- package/dist/checker/rules/duplicate-metadata-key.js +21 -0
- package/dist/checker/rules/duplicate-metadata-key.js.map +1 -0
- package/dist/checker/rules/duplicate-section-heading.js +41 -0
- package/dist/checker/rules/duplicate-section-heading.js.map +1 -0
- package/dist/checker/rules/duplicate-section-in-schema.js +38 -0
- package/dist/checker/rules/duplicate-section-in-schema.js.map +1 -0
- package/dist/checker/rules/duplicate-timestamp.js +104 -0
- package/dist/checker/rules/duplicate-timestamp.js.map +1 -0
- package/dist/checker/rules/empty-required-value.js +45 -0
- package/dist/checker/rules/empty-required-value.js.map +1 -0
- package/dist/checker/rules/empty-section.js +21 -0
- package/dist/checker/rules/empty-section.js.map +1 -0
- package/dist/checker/rules/invalid-date-range-value.js +56 -0
- package/dist/checker/rules/invalid-date-range-value.js.map +1 -0
- package/dist/checker/rules/invalid-default-value.js +86 -0
- package/dist/checker/rules/invalid-default-value.js.map +1 -0
- package/dist/checker/rules/invalid-field-type.js +45 -0
- package/dist/checker/rules/invalid-field-type.js.map +1 -0
- package/dist/checker/rules/missing-required-field.js +48 -0
- package/dist/checker/rules/missing-required-field.js.map +1 -0
- package/dist/checker/rules/missing-required-section.js +51 -0
- package/dist/checker/rules/missing-required-section.js.map +1 -0
- package/dist/checker/rules/missing-title.js +56 -0
- package/dist/checker/rules/missing-title.js.map +1 -0
- package/dist/checker/rules/remove-undefined-field.js +42 -0
- package/dist/checker/rules/remove-undefined-field.js.map +1 -0
- package/dist/checker/rules/remove-undefined-section.js +42 -0
- package/dist/checker/rules/remove-undefined-section.js.map +1 -0
- package/dist/checker/rules/rules.d.ts +71 -0
- package/dist/checker/rules/rules.d.ts.map +1 -0
- package/dist/checker/rules/rules.js +102 -0
- package/dist/checker/rules/rules.js.map +1 -0
- package/dist/checker/rules/synthesis-empty-query.js +35 -0
- package/dist/checker/rules/synthesis-empty-query.js.map +1 -0
- package/dist/checker/rules/synthesis-missing-prompt.js +42 -0
- package/dist/checker/rules/synthesis-missing-prompt.js.map +1 -0
- package/dist/checker/rules/synthesis-missing-sources.js +32 -0
- package/dist/checker/rules/synthesis-missing-sources.js.map +1 -0
- package/dist/checker/rules/synthesis-unknown-query-entity.js +39 -0
- package/dist/checker/rules/synthesis-unknown-query-entity.js.map +1 -0
- package/dist/checker/rules/timestamp-out-of-order.js +55 -0
- package/dist/checker/rules/timestamp-out-of-order.js.map +1 -0
- package/dist/checker/rules/unknown-entity.js +32 -0
- package/dist/checker/rules/unknown-entity.js.map +1 -0
- package/dist/checker/rules/unknown-field.js +40 -0
- package/dist/checker/rules/unknown-field.js.map +1 -0
- package/dist/checker/rules/unknown-section.js +47 -0
- package/dist/checker/rules/unknown-section.js.map +1 -0
- package/dist/checker/rules/unresolved-link.js +34 -0
- package/dist/checker/rules/unresolved-link.js.map +1 -0
- package/dist/checker/rules/update-without-create.js +65 -0
- package/dist/checker/rules/update-without-create.js.map +1 -0
- package/dist/checker/visitor.d.ts +69 -0
- package/dist/checker/visitor.d.ts.map +1 -0
- package/dist/checker/visitor.js +67 -0
- package/dist/checker/visitor.js.map +1 -0
- package/dist/checker/workspace-index.d.ts +50 -0
- package/dist/checker/workspace-index.d.ts.map +1 -0
- package/dist/checker/workspace-index.js +108 -0
- package/dist/checker/workspace-index.js.map +1 -0
- package/dist/commands/actualize.d.ts +113 -0
- package/dist/commands/actualize.d.ts.map +1 -0
- package/dist/commands/actualize.js +111 -0
- package/dist/commands/actualize.js.map +1 -0
- package/dist/commands/check.d.ts +65 -0
- package/dist/commands/check.d.ts.map +1 -0
- package/dist/commands/check.js +61 -0
- package/dist/commands/check.js.map +1 -0
- package/dist/commands/format.d.ts +90 -0
- package/dist/commands/format.d.ts.map +1 -0
- package/dist/commands/format.js +80 -0
- package/dist/commands/format.js.map +1 -0
- package/dist/commands/query.d.ts +152 -0
- package/dist/commands/query.d.ts.map +1 -0
- package/dist/commands/query.js +151 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/constants.d.ts +31 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +51 -0
- package/dist/constants.js.map +1 -0
- package/dist/files.d.ts +58 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +103 -0
- package/dist/files.js.map +1 -0
- package/dist/formatters.d.ts +39 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +200 -0
- package/dist/formatters.js.map +1 -0
- package/dist/fragment.d.ts +22 -0
- package/dist/fragment.d.ts.map +1 -0
- package/dist/git/git.js +240 -0
- package/dist/git/git.js.map +1 -0
- package/dist/merge/conflict-detector.d.ts +89 -0
- package/dist/merge/conflict-detector.d.ts.map +1 -0
- package/dist/merge/conflict-detector.js +352 -0
- package/dist/merge/conflict-detector.js.map +1 -0
- package/dist/merge/conflict-formatter.js +143 -0
- package/dist/merge/conflict-formatter.js.map +1 -0
- package/dist/merge/driver.d.ts +54 -0
- package/dist/merge/driver.d.ts.map +1 -0
- package/dist/merge/driver.js +112 -0
- package/dist/merge/driver.js.map +1 -0
- package/dist/merge/entry-matcher.d.ts +50 -0
- package/dist/merge/entry-matcher.d.ts.map +1 -0
- package/dist/merge/entry-matcher.js +141 -0
- package/dist/merge/entry-matcher.js.map +1 -0
- package/dist/merge/entry-merger.js +194 -0
- package/dist/merge/entry-merger.js.map +1 -0
- package/dist/merge/merge-result-builder.d.ts +62 -0
- package/dist/merge/merge-result-builder.d.ts.map +1 -0
- package/dist/merge/merge-result-builder.js +89 -0
- package/dist/merge/merge-result-builder.js.map +1 -0
- package/dist/mod.d.ts +31 -0
- package/dist/mod.js +23 -0
- package/dist/model/document.d.ts +134 -0
- package/dist/model/document.d.ts.map +1 -0
- package/dist/model/document.js +275 -0
- package/dist/model/document.js.map +1 -0
- package/dist/model/line-index.d.ts +85 -0
- package/dist/model/line-index.d.ts.map +1 -0
- package/dist/model/line-index.js +159 -0
- package/dist/model/line-index.js.map +1 -0
- package/dist/model/workspace.d.ts +296 -0
- package/dist/model/workspace.d.ts.map +1 -0
- package/dist/model/workspace.js +562 -0
- package/dist/model/workspace.js.map +1 -0
- package/dist/parser.js +27 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.native.d.ts +51 -0
- package/dist/parser.native.d.ts.map +1 -0
- package/dist/parser.native.js +62 -0
- package/dist/parser.native.js.map +1 -0
- package/dist/parser.shared.d.ts +99 -0
- package/dist/parser.shared.d.ts.map +1 -0
- package/dist/parser.shared.js +124 -0
- package/dist/parser.shared.js.map +1 -0
- package/dist/parser.web.d.ts +67 -0
- package/dist/parser.web.d.ts.map +1 -0
- package/dist/parser.web.js +49 -0
- package/dist/parser.web.js.map +1 -0
- package/dist/schema/registry.d.ts +108 -0
- package/dist/schema/registry.d.ts.map +1 -0
- package/dist/schema/registry.js +281 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/semantic/analyzer.d.ts +107 -0
- package/dist/semantic/analyzer.d.ts.map +1 -0
- package/dist/semantic/analyzer.js +261 -0
- package/dist/semantic/analyzer.js.map +1 -0
- package/dist/services/change-tracker/change-tracker.d.ts +111 -0
- package/dist/services/change-tracker/change-tracker.d.ts.map +1 -0
- package/dist/services/change-tracker/change-tracker.js +62 -0
- package/dist/services/change-tracker/change-tracker.js.map +1 -0
- package/dist/services/change-tracker/create-tracker.d.ts +42 -0
- package/dist/services/change-tracker/create-tracker.d.ts.map +1 -0
- package/dist/services/change-tracker/create-tracker.js +53 -0
- package/dist/services/change-tracker/create-tracker.js.map +1 -0
- package/dist/services/change-tracker/git-tracker.d.ts +59 -0
- package/dist/services/change-tracker/git-tracker.d.ts.map +1 -0
- package/dist/services/change-tracker/git-tracker.js +218 -0
- package/dist/services/change-tracker/git-tracker.js.map +1 -0
- package/dist/services/change-tracker/timestamp-tracker.d.ts +22 -0
- package/dist/services/change-tracker/timestamp-tracker.d.ts.map +1 -0
- package/dist/services/change-tracker/timestamp-tracker.js +74 -0
- package/dist/services/change-tracker/timestamp-tracker.js.map +1 -0
- package/dist/services/definition.d.ts +37 -0
- package/dist/services/definition.d.ts.map +1 -0
- package/dist/services/definition.js +43 -0
- package/dist/services/definition.js.map +1 -0
- package/dist/services/entity-navigation.d.ts +200 -0
- package/dist/services/entity-navigation.d.ts.map +1 -0
- package/dist/services/entity-navigation.js +211 -0
- package/dist/services/entity-navigation.js.map +1 -0
- package/dist/services/hover.d.ts +81 -0
- package/dist/services/hover.d.ts.map +1 -0
- package/dist/services/hover.js +669 -0
- package/dist/services/hover.js.map +1 -0
- package/dist/services/query.d.ts +116 -0
- package/dist/services/query.d.ts.map +1 -0
- package/dist/services/query.js +225 -0
- package/dist/services/query.js.map +1 -0
- package/dist/services/references.d.ts +52 -0
- package/dist/services/references.d.ts.map +1 -0
- package/dist/services/references.js +66 -0
- package/dist/services/references.js.map +1 -0
- package/dist/services/semantic-tokens.d.ts +54 -0
- package/dist/services/semantic-tokens.d.ts.map +1 -0
- package/dist/services/semantic-tokens.js +213 -0
- package/dist/services/semantic-tokens.js.map +1 -0
- package/dist/services/synthesis.d.ts +90 -0
- package/dist/services/synthesis.d.ts.map +1 -0
- package/dist/services/synthesis.js +113 -0
- package/dist/services/synthesis.js.map +1 -0
- package/dist/source-map.d.ts +42 -0
- package/dist/source-map.d.ts.map +1 -0
- package/dist/source-map.js +170 -0
- package/dist/source-map.js.map +1 -0
- package/package.json +128 -0
- package/tree-sitter-thalo.wasm +0 -0
- package/web-tree-sitter.wasm +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { extractSourceFile } from "../ast/extract.js";
|
|
2
|
+
import { parseDocument } from "../parser.js";
|
|
3
|
+
import { matchEntries } from "./entry-matcher.js";
|
|
4
|
+
import { detectConflicts } from "./conflict-detector.js";
|
|
5
|
+
import { buildMergedResult } from "./merge-result-builder.js";
|
|
6
|
+
|
|
7
|
+
//#region src/merge/driver.ts
|
|
8
|
+
/**
|
|
9
|
+
* Perform three-way merge of thalo files
|
|
10
|
+
*
|
|
11
|
+
* This is the main entry point for the merge driver.
|
|
12
|
+
* It parses all three versions, matches entries, detects conflicts,
|
|
13
|
+
* and produces a merged result.
|
|
14
|
+
*
|
|
15
|
+
* @param base - Base version content (common ancestor)
|
|
16
|
+
* @param ours - Our version content (local changes)
|
|
17
|
+
* @param theirs - Their version content (incoming changes)
|
|
18
|
+
* @param options - Merge options
|
|
19
|
+
* @returns MergeResult with merged content and conflict information
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const base = '2026-01-01T00:00Z define-entity lore "Lore"';
|
|
24
|
+
* const ours = base + '\n2026-01-02T00:00Z create lore "My entry" ^entry1';
|
|
25
|
+
* const theirs = base + '\n2026-01-03T00:00Z create lore "Their entry" ^entry2';
|
|
26
|
+
*
|
|
27
|
+
* const result = mergeThaloFiles(base, ours, theirs);
|
|
28
|
+
* console.log(result.success); // true
|
|
29
|
+
* console.log(result.content); // Merged content with both entries
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
function mergeThaloFiles(base, ours, theirs, options = {}) {
|
|
33
|
+
try {
|
|
34
|
+
const baseDoc = parseDocument(base, { fileType: "thalo" });
|
|
35
|
+
const oursDoc = parseDocument(ours, { fileType: "thalo" });
|
|
36
|
+
const theirsDoc = parseDocument(theirs, { fileType: "thalo" });
|
|
37
|
+
const baseAst = baseDoc.blocks.length > 0 ? extractSourceFile(baseDoc.blocks[0].tree.rootNode) : {
|
|
38
|
+
entries: [],
|
|
39
|
+
syntaxErrors: []
|
|
40
|
+
};
|
|
41
|
+
const oursAst = oursDoc.blocks.length > 0 ? extractSourceFile(oursDoc.blocks[0].tree.rootNode) : {
|
|
42
|
+
entries: [],
|
|
43
|
+
syntaxErrors: []
|
|
44
|
+
};
|
|
45
|
+
const theirsAst = theirsDoc.blocks.length > 0 ? extractSourceFile(theirsDoc.blocks[0].tree.rootNode) : {
|
|
46
|
+
entries: [],
|
|
47
|
+
syntaxErrors: []
|
|
48
|
+
};
|
|
49
|
+
const syntaxErrorConflicts = [];
|
|
50
|
+
if (baseAst.syntaxErrors.length > 0) syntaxErrorConflicts.push({
|
|
51
|
+
type: "parse-error",
|
|
52
|
+
message: `Parse error in base: ${baseAst.syntaxErrors.map((e) => e.message || "syntax error").join(", ")}`,
|
|
53
|
+
location: 0,
|
|
54
|
+
identity: { entryType: "parse-error" },
|
|
55
|
+
context: { errorMessage: "base version has syntax errors" }
|
|
56
|
+
});
|
|
57
|
+
if (oursAst.syntaxErrors.length > 0) syntaxErrorConflicts.push({
|
|
58
|
+
type: "parse-error",
|
|
59
|
+
message: `Parse error in ours: ${oursAst.syntaxErrors.map((e) => e.message || "syntax error").join(", ")}`,
|
|
60
|
+
location: 0,
|
|
61
|
+
identity: { entryType: "parse-error" },
|
|
62
|
+
context: { errorMessage: "ours version has syntax errors" }
|
|
63
|
+
});
|
|
64
|
+
if (theirsAst.syntaxErrors.length > 0) syntaxErrorConflicts.push({
|
|
65
|
+
type: "parse-error",
|
|
66
|
+
message: `Parse error in theirs: ${theirsAst.syntaxErrors.map((e) => e.message || "syntax error").join(", ")}`,
|
|
67
|
+
location: 0,
|
|
68
|
+
identity: { entryType: "parse-error" },
|
|
69
|
+
context: { errorMessage: "theirs version has syntax errors" }
|
|
70
|
+
});
|
|
71
|
+
if (syntaxErrorConflicts.length > 0) return {
|
|
72
|
+
success: false,
|
|
73
|
+
content: ours,
|
|
74
|
+
conflicts: syntaxErrorConflicts,
|
|
75
|
+
stats: {
|
|
76
|
+
totalEntries: 0,
|
|
77
|
+
oursOnly: 0,
|
|
78
|
+
theirsOnly: 0,
|
|
79
|
+
common: 0,
|
|
80
|
+
autoMerged: 0,
|
|
81
|
+
conflicts: syntaxErrorConflicts.length
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const matches = matchEntries(baseAst.entries, oursAst.entries, theirsAst.entries);
|
|
85
|
+
return buildMergedResult(matches, detectConflicts(matches, options), options);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
content: ours,
|
|
91
|
+
conflicts: [{
|
|
92
|
+
type: "merge-error",
|
|
93
|
+
message: `Merge failed: ${errorMessage}`,
|
|
94
|
+
location: 0,
|
|
95
|
+
identity: { entryType: "error" },
|
|
96
|
+
context: { errorMessage }
|
|
97
|
+
}],
|
|
98
|
+
stats: {
|
|
99
|
+
totalEntries: 0,
|
|
100
|
+
oursOnly: 0,
|
|
101
|
+
theirsOnly: 0,
|
|
102
|
+
common: 0,
|
|
103
|
+
autoMerged: 0,
|
|
104
|
+
conflicts: 1
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
//#endregion
|
|
111
|
+
export { mergeThaloFiles };
|
|
112
|
+
//# sourceMappingURL=driver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.js","names":["syntaxErrorConflicts: MergeConflict[]"],"sources":["../../src/merge/driver.ts"],"sourcesContent":["import { parseDocument } from \"../parser.js\";\nimport { extractSourceFile } from \"../ast/extract.js\";\nimport type { MergeResult } from \"./merge-result-builder.js\";\nimport type { MergeConflict, ConflictRule } from \"./conflict-detector.js\";\nimport { matchEntries } from \"./entry-matcher.js\";\nimport { detectConflicts } from \"./conflict-detector.js\";\nimport { buildMergedResult } from \"./merge-result-builder.js\";\n\n/**\n * Options for the merge driver\n */\nexport interface MergeOptions {\n /**\n * Conflict marker style\n * - \"git\": Standard Git style (ours/theirs)\n * - \"diff3\": Include base section\n */\n markerStyle?: \"git\" | \"diff3\";\n\n /**\n * Whether to include base in markers (diff3 style)\n * Deprecated: Use markerStyle: \"diff3\" instead\n */\n showBase?: boolean;\n\n /**\n * Custom conflict detection rules\n * Applied after default rules\n */\n conflictRules?: ConflictRule[];\n}\n\n/**\n * Perform three-way merge of thalo files\n *\n * This is the main entry point for the merge driver.\n * It parses all three versions, matches entries, detects conflicts,\n * and produces a merged result.\n *\n * @param base - Base version content (common ancestor)\n * @param ours - Our version content (local changes)\n * @param theirs - Their version content (incoming changes)\n * @param options - Merge options\n * @returns MergeResult with merged content and conflict information\n *\n * @example\n * ```typescript\n * const base = '2026-01-01T00:00Z define-entity lore \"Lore\"';\n * const ours = base + '\\n2026-01-02T00:00Z create lore \"My entry\" ^entry1';\n * const theirs = base + '\\n2026-01-03T00:00Z create lore \"Their entry\" ^entry2';\n *\n * const result = mergeThaloFiles(base, ours, theirs);\n * console.log(result.success); // true\n * console.log(result.content); // Merged content with both entries\n * ```\n */\nexport function mergeThaloFiles(\n base: string,\n ours: string,\n theirs: string,\n options: MergeOptions = {},\n): MergeResult {\n try {\n const baseDoc = parseDocument(base, { fileType: \"thalo\" });\n const oursDoc = parseDocument(ours, { fileType: \"thalo\" });\n const theirsDoc = parseDocument(theirs, { fileType: \"thalo\" });\n\n const baseAst =\n baseDoc.blocks.length > 0\n ? extractSourceFile(baseDoc.blocks[0].tree.rootNode)\n : { entries: [], syntaxErrors: [] };\n const oursAst =\n oursDoc.blocks.length > 0\n ? extractSourceFile(oursDoc.blocks[0].tree.rootNode)\n : { entries: [], syntaxErrors: [] };\n const theirsAst =\n theirsDoc.blocks.length > 0\n ? extractSourceFile(theirsDoc.blocks[0].tree.rootNode)\n : { entries: [], syntaxErrors: [] };\n\n // Surface syntax errors as parse-error conflicts\n const syntaxErrorConflicts: MergeConflict[] = [];\n if (baseAst.syntaxErrors.length > 0) {\n syntaxErrorConflicts.push({\n type: \"parse-error\",\n message: `Parse error in base: ${baseAst.syntaxErrors.map((e) => e.message || \"syntax error\").join(\", \")}`,\n location: 0,\n identity: { entryType: \"parse-error\" },\n context: { errorMessage: \"base version has syntax errors\" },\n });\n }\n if (oursAst.syntaxErrors.length > 0) {\n syntaxErrorConflicts.push({\n type: \"parse-error\",\n message: `Parse error in ours: ${oursAst.syntaxErrors.map((e) => e.message || \"syntax error\").join(\", \")}`,\n location: 0,\n identity: { entryType: \"parse-error\" },\n context: { errorMessage: \"ours version has syntax errors\" },\n });\n }\n if (theirsAst.syntaxErrors.length > 0) {\n syntaxErrorConflicts.push({\n type: \"parse-error\",\n message: `Parse error in theirs: ${theirsAst.syntaxErrors.map((e) => e.message || \"syntax error\").join(\", \")}`,\n location: 0,\n identity: { entryType: \"parse-error\" },\n context: { errorMessage: \"theirs version has syntax errors\" },\n });\n }\n\n if (syntaxErrorConflicts.length > 0) {\n return {\n success: false,\n content: ours,\n conflicts: syntaxErrorConflicts,\n stats: {\n totalEntries: 0,\n oursOnly: 0,\n theirsOnly: 0,\n common: 0,\n autoMerged: 0,\n conflicts: syntaxErrorConflicts.length,\n },\n };\n }\n\n const matches = matchEntries(baseAst.entries, oursAst.entries, theirsAst.entries);\n\n const conflicts = detectConflicts(matches, options);\n\n const result = buildMergedResult(matches, conflicts, options);\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n content: ours,\n conflicts: [\n {\n type: \"merge-error\",\n message: `Merge failed: ${errorMessage}`,\n location: 0,\n identity: { entryType: \"error\" },\n context: { errorMessage },\n },\n ],\n stats: {\n totalEntries: 0,\n oursOnly: 0,\n theirsOnly: 0,\n common: 0,\n autoMerged: 0,\n conflicts: 1,\n },\n };\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,SAAgB,gBACd,MACA,MACA,QACA,UAAwB,EAAE,EACb;AACb,KAAI;EACF,MAAM,UAAU,cAAc,MAAM,EAAE,UAAU,SAAS,CAAC;EAC1D,MAAM,UAAU,cAAc,MAAM,EAAE,UAAU,SAAS,CAAC;EAC1D,MAAM,YAAY,cAAc,QAAQ,EAAE,UAAU,SAAS,CAAC;EAE9D,MAAM,UACJ,QAAQ,OAAO,SAAS,IACpB,kBAAkB,QAAQ,OAAO,GAAG,KAAK,SAAS,GAClD;GAAE,SAAS,EAAE;GAAE,cAAc,EAAE;GAAE;EACvC,MAAM,UACJ,QAAQ,OAAO,SAAS,IACpB,kBAAkB,QAAQ,OAAO,GAAG,KAAK,SAAS,GAClD;GAAE,SAAS,EAAE;GAAE,cAAc,EAAE;GAAE;EACvC,MAAM,YACJ,UAAU,OAAO,SAAS,IACtB,kBAAkB,UAAU,OAAO,GAAG,KAAK,SAAS,GACpD;GAAE,SAAS,EAAE;GAAE,cAAc,EAAE;GAAE;EAGvC,MAAMA,uBAAwC,EAAE;AAChD,MAAI,QAAQ,aAAa,SAAS,EAChC,sBAAqB,KAAK;GACxB,MAAM;GACN,SAAS,wBAAwB,QAAQ,aAAa,KAAK,MAAM,EAAE,WAAW,eAAe,CAAC,KAAK,KAAK;GACxG,UAAU;GACV,UAAU,EAAE,WAAW,eAAe;GACtC,SAAS,EAAE,cAAc,kCAAkC;GAC5D,CAAC;AAEJ,MAAI,QAAQ,aAAa,SAAS,EAChC,sBAAqB,KAAK;GACxB,MAAM;GACN,SAAS,wBAAwB,QAAQ,aAAa,KAAK,MAAM,EAAE,WAAW,eAAe,CAAC,KAAK,KAAK;GACxG,UAAU;GACV,UAAU,EAAE,WAAW,eAAe;GACtC,SAAS,EAAE,cAAc,kCAAkC;GAC5D,CAAC;AAEJ,MAAI,UAAU,aAAa,SAAS,EAClC,sBAAqB,KAAK;GACxB,MAAM;GACN,SAAS,0BAA0B,UAAU,aAAa,KAAK,MAAM,EAAE,WAAW,eAAe,CAAC,KAAK,KAAK;GAC5G,UAAU;GACV,UAAU,EAAE,WAAW,eAAe;GACtC,SAAS,EAAE,cAAc,oCAAoC;GAC9D,CAAC;AAGJ,MAAI,qBAAqB,SAAS,EAChC,QAAO;GACL,SAAS;GACT,SAAS;GACT,WAAW;GACX,OAAO;IACL,cAAc;IACd,UAAU;IACV,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,WAAW,qBAAqB;IACjC;GACF;EAGH,MAAM,UAAU,aAAa,QAAQ,SAAS,QAAQ,SAAS,UAAU,QAAQ;AAMjF,SAFe,kBAAkB,SAFf,gBAAgB,SAAS,QAAQ,EAEE,QAAQ;UAGtD,OAAO;EACd,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AAC3E,SAAO;GACL,SAAS;GACT,SAAS;GACT,WAAW,CACT;IACE,MAAM;IACN,SAAS,iBAAiB;IAC1B,UAAU;IACV,UAAU,EAAE,WAAW,SAAS;IAChC,SAAS,EAAE,cAAc;IAC1B,CACF;GACD,OAAO;IACL,cAAc;IACd,UAAU;IACV,YAAY;IACZ,QAAQ;IACR,YAAY;IACZ,WAAW;IACZ;GACF"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Entry } from "../ast/ast-types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/merge/entry-matcher.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Identity used to match entries across versions
|
|
7
|
+
*/
|
|
8
|
+
interface EntryIdentity {
|
|
9
|
+
/**
|
|
10
|
+
* Primary identity: Explicit link ID (^link-id)
|
|
11
|
+
* Used when available for matching
|
|
12
|
+
*/
|
|
13
|
+
linkId?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Fallback identity: Timestamp for entries without explicit links
|
|
16
|
+
* Combined with entryType for uniqueness
|
|
17
|
+
*/
|
|
18
|
+
timestamp?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Entry type (for validation and fallback matching)
|
|
21
|
+
*/
|
|
22
|
+
entryType: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A matched set of entries across three versions
|
|
26
|
+
*/
|
|
27
|
+
interface EntryMatch {
|
|
28
|
+
/**
|
|
29
|
+
* Identity used to match this entry across versions
|
|
30
|
+
*/
|
|
31
|
+
identity: EntryIdentity;
|
|
32
|
+
/**
|
|
33
|
+
* Entry from base version (common ancestor)
|
|
34
|
+
* null if entry was added in ours or theirs
|
|
35
|
+
*/
|
|
36
|
+
base: Entry | null;
|
|
37
|
+
/**
|
|
38
|
+
* Entry from ours version (local/current)
|
|
39
|
+
* null if entry was deleted in ours
|
|
40
|
+
*/
|
|
41
|
+
ours: Entry | null;
|
|
42
|
+
/**
|
|
43
|
+
* Entry from theirs version (incoming)
|
|
44
|
+
* null if entry was deleted in theirs
|
|
45
|
+
*/
|
|
46
|
+
theirs: Entry | null;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { EntryIdentity, EntryMatch };
|
|
50
|
+
//# sourceMappingURL=entry-matcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-matcher.d.ts","names":[],"sources":["../../src/merge/entry-matcher.ts"],"sourcesContent":[],"mappings":";;;;;;AAKA;AAsBiB,UAtBA,aAAA,CAsBU;EAIf;;;;EAkBG,MAAA,CAAA,EAAA,MAAA;;;;;;;;;;;;;;UAtBE,UAAA;;;;YAIL;;;;;QAMJ;;;;;QAMA;;;;;UAME"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
//#region src/merge/entry-matcher.ts
|
|
2
|
+
/**
|
|
3
|
+
* Match entries across three versions based on identity
|
|
4
|
+
*
|
|
5
|
+
* Algorithm:
|
|
6
|
+
* 1. Build identity map for each version (link ID or timestamp)
|
|
7
|
+
* 2. Compute union of all identities
|
|
8
|
+
* 3. For each identity, find corresponding entry in each version
|
|
9
|
+
* 4. Return matched triplets
|
|
10
|
+
*
|
|
11
|
+
* @param base - Entries from base version (common ancestor)
|
|
12
|
+
* @param ours - Entries from ours version (local/current)
|
|
13
|
+
* @param theirs - Entries from theirs version (incoming)
|
|
14
|
+
* @returns Array of matched entry triplets
|
|
15
|
+
*/
|
|
16
|
+
function matchEntries(base, ours, theirs) {
|
|
17
|
+
const baseMap = buildIdentityMap(base, "base");
|
|
18
|
+
const oursMap = buildIdentityMap(ours, "ours");
|
|
19
|
+
const theirsMap = buildIdentityMap(theirs, "theirs");
|
|
20
|
+
const allIdentities = /* @__PURE__ */ new Set();
|
|
21
|
+
for (const id of baseMap.keys()) allIdentities.add(id);
|
|
22
|
+
for (const id of oursMap.keys()) allIdentities.add(id);
|
|
23
|
+
for (const id of theirsMap.keys()) allIdentities.add(id);
|
|
24
|
+
const matches = [];
|
|
25
|
+
for (const idKey of allIdentities) {
|
|
26
|
+
const identity = parseIdentityKey(idKey);
|
|
27
|
+
matches.push({
|
|
28
|
+
identity,
|
|
29
|
+
base: baseMap.get(idKey) || null,
|
|
30
|
+
ours: oursMap.get(idKey) || null,
|
|
31
|
+
theirs: theirsMap.get(idKey) || null
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return matches;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a map from identity key to entry
|
|
38
|
+
*
|
|
39
|
+
* Precondition: Each version must not contain duplicate identities.
|
|
40
|
+
* Callers should validate inputs via the checker module's duplicate-link-id
|
|
41
|
+
* and duplicate-timestamp rules before invoking the merge driver.
|
|
42
|
+
*
|
|
43
|
+
* @param entries - Array of entries to map
|
|
44
|
+
* @param versionName - Name of version for error messages (e.g., "base", "ours", "theirs")
|
|
45
|
+
* @returns Map from serialized identity to entry
|
|
46
|
+
* @throws Error if duplicate identity is found within the version
|
|
47
|
+
*/
|
|
48
|
+
function buildIdentityMap(entries, versionName) {
|
|
49
|
+
const map = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
const key = serializeIdentity(getEntryIdentity(entry));
|
|
52
|
+
if (map.has(key)) throw new Error(`Duplicate identity '${key}' found in ${versionName} version. Ensure inputs are validated via checker rules before merging.`);
|
|
53
|
+
map.set(key, entry);
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extract identity from an entry
|
|
59
|
+
*
|
|
60
|
+
* Priority:
|
|
61
|
+
* 1. Explicit link ID (^link-id) from header
|
|
62
|
+
* 2. Timestamp for entries without link IDs
|
|
63
|
+
*
|
|
64
|
+
* @param entry - Entry to extract identity from
|
|
65
|
+
* @returns Entry identity
|
|
66
|
+
*/
|
|
67
|
+
function getEntryIdentity(entry) {
|
|
68
|
+
if (entry.type === "instance_entry" && entry.header.link) return {
|
|
69
|
+
linkId: entry.header.link.id,
|
|
70
|
+
entryType: entry.type
|
|
71
|
+
};
|
|
72
|
+
if (entry.type === "schema_entry" && entry.header.link) return {
|
|
73
|
+
linkId: entry.header.link.id,
|
|
74
|
+
entryType: entry.type
|
|
75
|
+
};
|
|
76
|
+
if (entry.type === "synthesis_entry") return {
|
|
77
|
+
linkId: entry.header.linkId.id,
|
|
78
|
+
entryType: entry.type
|
|
79
|
+
};
|
|
80
|
+
if (entry.type === "actualize_entry") return {
|
|
81
|
+
linkId: entry.header.target.id,
|
|
82
|
+
entryType: entry.type
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
timestamp: getEntryTimestamp(entry) || void 0,
|
|
86
|
+
entryType: entry.type
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Get timestamp string from an entry
|
|
91
|
+
*
|
|
92
|
+
* @param entry - Entry to extract timestamp from
|
|
93
|
+
* @returns Timestamp string or null
|
|
94
|
+
*/
|
|
95
|
+
function getEntryTimestamp(entry) {
|
|
96
|
+
switch (entry.type) {
|
|
97
|
+
case "instance_entry":
|
|
98
|
+
case "schema_entry":
|
|
99
|
+
case "synthesis_entry":
|
|
100
|
+
case "actualize_entry": return entry.header.timestamp.value;
|
|
101
|
+
default: return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Serialize identity to a unique key string
|
|
106
|
+
*
|
|
107
|
+
* @param identity - Identity to serialize
|
|
108
|
+
* @returns Unique key string
|
|
109
|
+
*/
|
|
110
|
+
function serializeIdentity(identity) {
|
|
111
|
+
if (identity.linkId) return `link:${identity.linkId}`;
|
|
112
|
+
if (identity.timestamp) return `ts:${identity.timestamp}:${identity.entryType}`;
|
|
113
|
+
return `type:${identity.entryType}`;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Parse identity key back to EntryIdentity
|
|
117
|
+
*
|
|
118
|
+
* @param key - Serialized identity key
|
|
119
|
+
* @returns Parsed identity
|
|
120
|
+
*/
|
|
121
|
+
function parseIdentityKey(key) {
|
|
122
|
+
if (key.startsWith("link:")) return {
|
|
123
|
+
linkId: key.slice(5),
|
|
124
|
+
entryType: "unknown"
|
|
125
|
+
};
|
|
126
|
+
if (key.startsWith("ts:")) {
|
|
127
|
+
const withoutPrefix = key.slice(3);
|
|
128
|
+
const lastColonIndex = withoutPrefix.lastIndexOf(":");
|
|
129
|
+
if (lastColonIndex === -1) throw new Error(`Invalid timestamp identity key: ${key}`);
|
|
130
|
+
return {
|
|
131
|
+
timestamp: withoutPrefix.slice(0, lastColonIndex),
|
|
132
|
+
entryType: withoutPrefix.slice(lastColonIndex + 1)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
if (key.startsWith("type:")) return { entryType: key.slice(5) };
|
|
136
|
+
throw new Error(`Invalid identity key: ${key}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//#endregion
|
|
140
|
+
export { getEntryIdentity, matchEntries, serializeIdentity };
|
|
141
|
+
//# sourceMappingURL=entry-matcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-matcher.js","names":["matches: EntryMatch[]"],"sources":["../../src/merge/entry-matcher.ts"],"sourcesContent":["import type { Entry } from \"../ast/ast-types.js\";\n\n/**\n * Identity used to match entries across versions\n */\nexport interface EntryIdentity {\n /**\n * Primary identity: Explicit link ID (^link-id)\n * Used when available for matching\n */\n linkId?: string;\n\n /**\n * Fallback identity: Timestamp for entries without explicit links\n * Combined with entryType for uniqueness\n */\n timestamp?: string;\n\n /**\n * Entry type (for validation and fallback matching)\n */\n entryType: string;\n}\n\n/**\n * A matched set of entries across three versions\n */\nexport interface EntryMatch {\n /**\n * Identity used to match this entry across versions\n */\n identity: EntryIdentity;\n\n /**\n * Entry from base version (common ancestor)\n * null if entry was added in ours or theirs\n */\n base: Entry | null;\n\n /**\n * Entry from ours version (local/current)\n * null if entry was deleted in ours\n */\n ours: Entry | null;\n\n /**\n * Entry from theirs version (incoming)\n * null if entry was deleted in theirs\n */\n theirs: Entry | null;\n}\n\n/**\n * Match entries across three versions based on identity\n *\n * Algorithm:\n * 1. Build identity map for each version (link ID or timestamp)\n * 2. Compute union of all identities\n * 3. For each identity, find corresponding entry in each version\n * 4. Return matched triplets\n *\n * @param base - Entries from base version (common ancestor)\n * @param ours - Entries from ours version (local/current)\n * @param theirs - Entries from theirs version (incoming)\n * @returns Array of matched entry triplets\n */\nexport function matchEntries(base: Entry[], ours: Entry[], theirs: Entry[]): EntryMatch[] {\n const baseMap = buildIdentityMap(base, \"base\");\n const oursMap = buildIdentityMap(ours, \"ours\");\n const theirsMap = buildIdentityMap(theirs, \"theirs\");\n\n const allIdentities = new Set<string>();\n for (const id of baseMap.keys()) {\n allIdentities.add(id);\n }\n for (const id of oursMap.keys()) {\n allIdentities.add(id);\n }\n for (const id of theirsMap.keys()) {\n allIdentities.add(id);\n }\n\n const matches: EntryMatch[] = [];\n for (const idKey of allIdentities) {\n const identity = parseIdentityKey(idKey);\n matches.push({\n identity,\n base: baseMap.get(idKey) || null,\n ours: oursMap.get(idKey) || null,\n theirs: theirsMap.get(idKey) || null,\n });\n }\n\n return matches;\n}\n\n/**\n * Build a map from identity key to entry\n *\n * Precondition: Each version must not contain duplicate identities.\n * Callers should validate inputs via the checker module's duplicate-link-id\n * and duplicate-timestamp rules before invoking the merge driver.\n *\n * @param entries - Array of entries to map\n * @param versionName - Name of version for error messages (e.g., \"base\", \"ours\", \"theirs\")\n * @returns Map from serialized identity to entry\n * @throws Error if duplicate identity is found within the version\n */\nfunction buildIdentityMap(entries: Entry[], versionName: string): Map<string, Entry> {\n const map = new Map<string, Entry>();\n\n for (const entry of entries) {\n const identity = getEntryIdentity(entry);\n const key = serializeIdentity(identity);\n\n if (map.has(key)) {\n throw new Error(\n `Duplicate identity '${key}' found in ${versionName} version. ` +\n `Ensure inputs are validated via checker rules before merging.`,\n );\n }\n\n map.set(key, entry);\n }\n\n return map;\n}\n\n/**\n * Extract identity from an entry\n *\n * Priority:\n * 1. Explicit link ID (^link-id) from header\n * 2. Timestamp for entries without link IDs\n *\n * @param entry - Entry to extract identity from\n * @returns Entry identity\n */\nexport function getEntryIdentity(entry: Entry): EntryIdentity {\n if (entry.type === \"instance_entry\" && entry.header.link) {\n return {\n linkId: entry.header.link.id,\n entryType: entry.type,\n };\n }\n\n if (entry.type === \"schema_entry\" && entry.header.link) {\n return {\n linkId: entry.header.link.id,\n entryType: entry.type,\n };\n }\n\n if (entry.type === \"synthesis_entry\") {\n return {\n linkId: entry.header.linkId.id,\n entryType: entry.type,\n };\n }\n\n if (entry.type === \"actualize_entry\") {\n return {\n linkId: entry.header.target.id,\n entryType: entry.type,\n };\n }\n\n const timestamp = getEntryTimestamp(entry);\n return {\n timestamp: timestamp || undefined,\n entryType: entry.type,\n };\n}\n\n/**\n * Get timestamp string from an entry\n *\n * @param entry - Entry to extract timestamp from\n * @returns Timestamp string or null\n */\nfunction getEntryTimestamp(entry: Entry): string | null {\n switch (entry.type) {\n case \"instance_entry\":\n case \"schema_entry\":\n case \"synthesis_entry\":\n case \"actualize_entry\":\n return entry.header.timestamp.value;\n default:\n return null;\n }\n}\n\n/**\n * Serialize identity to a unique key string\n *\n * @param identity - Identity to serialize\n * @returns Unique key string\n */\nexport function serializeIdentity(identity: EntryIdentity): string {\n if (identity.linkId) {\n return `link:${identity.linkId}`;\n }\n\n if (identity.timestamp) {\n return `ts:${identity.timestamp}:${identity.entryType}`;\n }\n\n // Fallback for synthetic identities (e.g., error conflicts)\n return `type:${identity.entryType}`;\n}\n\n/**\n * Parse identity key back to EntryIdentity\n *\n * @param key - Serialized identity key\n * @returns Parsed identity\n */\nfunction parseIdentityKey(key: string): EntryIdentity {\n if (key.startsWith(\"link:\")) {\n const linkId = key.slice(5);\n return {\n linkId,\n entryType: \"unknown\",\n };\n }\n\n if (key.startsWith(\"ts:\")) {\n const withoutPrefix = key.slice(3);\n const lastColonIndex = withoutPrefix.lastIndexOf(\":\");\n if (lastColonIndex === -1) {\n throw new Error(`Invalid timestamp identity key: ${key}`);\n }\n const timestamp = withoutPrefix.slice(0, lastColonIndex);\n const entryType = withoutPrefix.slice(lastColonIndex + 1);\n return {\n timestamp,\n entryType,\n };\n }\n\n if (key.startsWith(\"type:\")) {\n const entryType = key.slice(5);\n return {\n entryType,\n };\n }\n\n throw new Error(`Invalid identity key: ${key}`);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAkEA,SAAgB,aAAa,MAAe,MAAe,QAA+B;CACxF,MAAM,UAAU,iBAAiB,MAAM,OAAO;CAC9C,MAAM,UAAU,iBAAiB,MAAM,OAAO;CAC9C,MAAM,YAAY,iBAAiB,QAAQ,SAAS;CAEpD,MAAM,gCAAgB,IAAI,KAAa;AACvC,MAAK,MAAM,MAAM,QAAQ,MAAM,CAC7B,eAAc,IAAI,GAAG;AAEvB,MAAK,MAAM,MAAM,QAAQ,MAAM,CAC7B,eAAc,IAAI,GAAG;AAEvB,MAAK,MAAM,MAAM,UAAU,MAAM,CAC/B,eAAc,IAAI,GAAG;CAGvB,MAAMA,UAAwB,EAAE;AAChC,MAAK,MAAM,SAAS,eAAe;EACjC,MAAM,WAAW,iBAAiB,MAAM;AACxC,UAAQ,KAAK;GACX;GACA,MAAM,QAAQ,IAAI,MAAM,IAAI;GAC5B,MAAM,QAAQ,IAAI,MAAM,IAAI;GAC5B,QAAQ,UAAU,IAAI,MAAM,IAAI;GACjC,CAAC;;AAGJ,QAAO;;;;;;;;;;;;;;AAeT,SAAS,iBAAiB,SAAkB,aAAyC;CACnF,MAAM,sBAAM,IAAI,KAAoB;AAEpC,MAAK,MAAM,SAAS,SAAS;EAE3B,MAAM,MAAM,kBADK,iBAAiB,MAAM,CACD;AAEvC,MAAI,IAAI,IAAI,IAAI,CACd,OAAM,IAAI,MACR,uBAAuB,IAAI,aAAa,YAAY,yEAErD;AAGH,MAAI,IAAI,KAAK,MAAM;;AAGrB,QAAO;;;;;;;;;;;;AAaT,SAAgB,iBAAiB,OAA6B;AAC5D,KAAI,MAAM,SAAS,oBAAoB,MAAM,OAAO,KAClD,QAAO;EACL,QAAQ,MAAM,OAAO,KAAK;EAC1B,WAAW,MAAM;EAClB;AAGH,KAAI,MAAM,SAAS,kBAAkB,MAAM,OAAO,KAChD,QAAO;EACL,QAAQ,MAAM,OAAO,KAAK;EAC1B,WAAW,MAAM;EAClB;AAGH,KAAI,MAAM,SAAS,kBACjB,QAAO;EACL,QAAQ,MAAM,OAAO,OAAO;EAC5B,WAAW,MAAM;EAClB;AAGH,KAAI,MAAM,SAAS,kBACjB,QAAO;EACL,QAAQ,MAAM,OAAO,OAAO;EAC5B,WAAW,MAAM;EAClB;AAIH,QAAO;EACL,WAFgB,kBAAkB,MAAM,IAEhB;EACxB,WAAW,MAAM;EAClB;;;;;;;;AASH,SAAS,kBAAkB,OAA6B;AACtD,SAAQ,MAAM,MAAd;EACE,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,kBACH,QAAO,MAAM,OAAO,UAAU;EAChC,QACE,QAAO;;;;;;;;;AAUb,SAAgB,kBAAkB,UAAiC;AACjE,KAAI,SAAS,OACX,QAAO,QAAQ,SAAS;AAG1B,KAAI,SAAS,UACX,QAAO,MAAM,SAAS,UAAU,GAAG,SAAS;AAI9C,QAAO,QAAQ,SAAS;;;;;;;;AAS1B,SAAS,iBAAiB,KAA4B;AACpD,KAAI,IAAI,WAAW,QAAQ,CAEzB,QAAO;EACL,QAFa,IAAI,MAAM,EAAE;EAGzB,WAAW;EACZ;AAGH,KAAI,IAAI,WAAW,MAAM,EAAE;EACzB,MAAM,gBAAgB,IAAI,MAAM,EAAE;EAClC,MAAM,iBAAiB,cAAc,YAAY,IAAI;AACrD,MAAI,mBAAmB,GACrB,OAAM,IAAI,MAAM,mCAAmC,MAAM;AAI3D,SAAO;GACL,WAHgB,cAAc,MAAM,GAAG,eAAe;GAItD,WAHgB,cAAc,MAAM,iBAAiB,EAAE;GAIxD;;AAGH,KAAI,IAAI,WAAW,QAAQ,CAEzB,QAAO,EACL,WAFgB,IAAI,MAAM,EAAE,EAG7B;AAGH,OAAM,IAAI,MAAM,yBAAyB,MAAM"}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
//#region src/merge/entry-merger.ts
|
|
2
|
+
/**
|
|
3
|
+
* Merge a single entry match (assumes no conflicts)
|
|
4
|
+
*
|
|
5
|
+
* Handles various merge scenarios:
|
|
6
|
+
* - Additions (one side only)
|
|
7
|
+
* - Deletions (missing on one side)
|
|
8
|
+
* - Single-side modifications
|
|
9
|
+
* - Both-side modifications with fine-grained merge
|
|
10
|
+
*
|
|
11
|
+
* @param match - The entry match to merge
|
|
12
|
+
* @returns Merged entry, or null if deleted
|
|
13
|
+
*/
|
|
14
|
+
function mergeEntry(match) {
|
|
15
|
+
const { base, ours, theirs } = match;
|
|
16
|
+
if (!base && ours && !theirs) return ours;
|
|
17
|
+
if (!base && !ours && theirs) return theirs;
|
|
18
|
+
if (base && !ours && theirs) return null;
|
|
19
|
+
if (base && ours && !theirs) return null;
|
|
20
|
+
if (base && ours && theirs) {
|
|
21
|
+
const oursChanged = !entriesEqual(base, ours);
|
|
22
|
+
const theirsChanged = !entriesEqual(base, theirs);
|
|
23
|
+
if (oursChanged && !theirsChanged) return ours;
|
|
24
|
+
if (!oursChanged && theirsChanged) return theirs;
|
|
25
|
+
if (oursChanged && theirsChanged) return mergeEntryChanges(base, ours, theirs);
|
|
26
|
+
return base;
|
|
27
|
+
}
|
|
28
|
+
return ours || theirs || base;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Fine-grained merge of entry changes
|
|
32
|
+
*
|
|
33
|
+
* For instance/synthesis entries, merge metadata and content independently
|
|
34
|
+
*/
|
|
35
|
+
function mergeEntryChanges(base, ours, theirs) {
|
|
36
|
+
if (ours.type === "instance_entry" && theirs.type === "instance_entry") {
|
|
37
|
+
const baseMetadata = base.type === "instance_entry" || base.type === "synthesis_entry" || base.type === "actualize_entry" ? base.metadata : [];
|
|
38
|
+
const baseContent = base.type === "instance_entry" || base.type === "synthesis_entry" ? base.content : null;
|
|
39
|
+
return {
|
|
40
|
+
...ours,
|
|
41
|
+
metadata: mergeMetadata(baseMetadata, ours.metadata, theirs.metadata),
|
|
42
|
+
content: mergeContent(baseContent, ours.content, theirs.content)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
if (ours.type === "synthesis_entry" && theirs.type === "synthesis_entry") {
|
|
46
|
+
const baseMetadata = base.type === "instance_entry" || base.type === "synthesis_entry" || base.type === "actualize_entry" ? base.metadata : [];
|
|
47
|
+
const baseContent = base.type === "instance_entry" || base.type === "synthesis_entry" ? base.content : null;
|
|
48
|
+
return {
|
|
49
|
+
...ours,
|
|
50
|
+
metadata: mergeMetadata(baseMetadata, ours.metadata, theirs.metadata),
|
|
51
|
+
content: mergeContent(baseContent, ours.content, theirs.content)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (ours.type === "actualize_entry" && theirs.type === "actualize_entry") {
|
|
55
|
+
const baseMetadata = base.type === "instance_entry" || base.type === "synthesis_entry" || base.type === "actualize_entry" ? base.metadata : [];
|
|
56
|
+
return {
|
|
57
|
+
...ours,
|
|
58
|
+
metadata: mergeMetadata(baseMetadata, ours.metadata, theirs.metadata)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return ours;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge metadata lists (non-conflicting keys)
|
|
65
|
+
*
|
|
66
|
+
* Strategy: Take the changed version for each key
|
|
67
|
+
*/
|
|
68
|
+
function mergeMetadata(base, ours, theirs) {
|
|
69
|
+
const baseMap = new Map(base.map((m) => [m.key.value, m]));
|
|
70
|
+
const oursMap = new Map(ours.map((m) => [m.key.value, m]));
|
|
71
|
+
const theirsMap = new Map(theirs.map((m) => [m.key.value, m]));
|
|
72
|
+
const allKeys = new Set([
|
|
73
|
+
...baseMap.keys(),
|
|
74
|
+
...oursMap.keys(),
|
|
75
|
+
...theirsMap.keys()
|
|
76
|
+
]);
|
|
77
|
+
const merged = [];
|
|
78
|
+
for (const key of allKeys) {
|
|
79
|
+
const baseVal = baseMap.get(key);
|
|
80
|
+
const oursVal = oursMap.get(key);
|
|
81
|
+
const theirsVal = theirsMap.get(key);
|
|
82
|
+
if (!oursVal && !theirsVal) continue;
|
|
83
|
+
if (oursVal && (!baseVal || !metadataEquals(baseVal, oursVal))) merged.push(oursVal);
|
|
84
|
+
else if (theirsVal && (!baseVal || !metadataEquals(baseVal, theirsVal))) merged.push(theirsVal);
|
|
85
|
+
else if (oursVal && theirsVal) merged.push(oursVal);
|
|
86
|
+
}
|
|
87
|
+
return merged;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Merge content (best effort)
|
|
91
|
+
*
|
|
92
|
+
* If both sides modified content, take ours for now
|
|
93
|
+
* More sophisticated merging could be added later
|
|
94
|
+
*/
|
|
95
|
+
function mergeContent(base, ours, theirs) {
|
|
96
|
+
if (base === null && ours === null && theirs === null) return null;
|
|
97
|
+
if (ours === null && theirs === null) return null;
|
|
98
|
+
if (ours && theirs) {
|
|
99
|
+
const oursChanged = !contentEquals(base, ours);
|
|
100
|
+
const theirsChanged = !contentEquals(base, theirs);
|
|
101
|
+
if (oursChanged && !theirsChanged) return ours;
|
|
102
|
+
if (!oursChanged && theirsChanged) return theirs;
|
|
103
|
+
return ours;
|
|
104
|
+
}
|
|
105
|
+
return ours || theirs || base;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if two entries are deeply equal
|
|
109
|
+
*/
|
|
110
|
+
function entriesEqual(a, b) {
|
|
111
|
+
if (a.type !== b.type) return false;
|
|
112
|
+
switch (a.type) {
|
|
113
|
+
case "instance_entry":
|
|
114
|
+
if (b.type !== "instance_entry") return false;
|
|
115
|
+
return headersEqual(a.header, b.header) && metadataListsEqual(a.metadata, b.metadata) && contentEquals(a.content, b.content);
|
|
116
|
+
case "schema_entry":
|
|
117
|
+
if (b.type !== "schema_entry") return false;
|
|
118
|
+
return headersEqual(a.header, b.header) && schemaBlocksEqual(a, b);
|
|
119
|
+
case "synthesis_entry":
|
|
120
|
+
if (b.type !== "synthesis_entry") return false;
|
|
121
|
+
return headersEqual(a.header, b.header) && metadataListsEqual(a.metadata, b.metadata) && contentEquals(a.content, b.content);
|
|
122
|
+
case "actualize_entry":
|
|
123
|
+
if (b.type !== "actualize_entry") return false;
|
|
124
|
+
return headersEqual(a.header, b.header) && metadataListsEqual(a.metadata, b.metadata);
|
|
125
|
+
default: return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Strip non-semantic fields (syntaxNode, location) for comparison.
|
|
130
|
+
* This avoids circular reference issues with tree-sitter nodes.
|
|
131
|
+
*/
|
|
132
|
+
function stripForComparison(obj) {
|
|
133
|
+
if (obj === null || obj === void 0) return obj;
|
|
134
|
+
if (Array.isArray(obj)) return obj.map(stripForComparison);
|
|
135
|
+
if (typeof obj === "object") {
|
|
136
|
+
const result = {};
|
|
137
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
138
|
+
if (key === "syntaxNode" || key === "location") continue;
|
|
139
|
+
result[key] = stripForComparison(value);
|
|
140
|
+
}
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
return obj;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check if two headers are equal (comparing semantic fields only)
|
|
147
|
+
*/
|
|
148
|
+
function headersEqual(a, b) {
|
|
149
|
+
return JSON.stringify(stripForComparison(a)) === JSON.stringify(stripForComparison(b));
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Check if two metadata lists are equal
|
|
153
|
+
*/
|
|
154
|
+
function metadataListsEqual(a, b) {
|
|
155
|
+
if (a.length !== b.length) return false;
|
|
156
|
+
const aMap = new Map(a.map((m) => [m.key.value, m.value.raw]));
|
|
157
|
+
const bMap = new Map(b.map((m) => [m.key.value, m.value.raw]));
|
|
158
|
+
if (aMap.size !== bMap.size) return false;
|
|
159
|
+
for (const [key, value] of aMap) if (bMap.get(key) !== value) return false;
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Check if two content nodes are equal
|
|
164
|
+
*/
|
|
165
|
+
function contentEquals(a, b) {
|
|
166
|
+
if (a === null && b === null) return true;
|
|
167
|
+
if (a === null || b === null) return false;
|
|
168
|
+
if (a.children.length !== b.children.length) return false;
|
|
169
|
+
for (let i = 0; i < a.children.length; i++) {
|
|
170
|
+
const childA = a.children[i];
|
|
171
|
+
const childB = b.children[i];
|
|
172
|
+
if (childA.type !== childB.type) return false;
|
|
173
|
+
if (childA.text !== childB.text) return false;
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if schema blocks are equal (comparing semantic fields only)
|
|
179
|
+
*/
|
|
180
|
+
function schemaBlocksEqual(a, b) {
|
|
181
|
+
const aObj = a;
|
|
182
|
+
const bObj = b;
|
|
183
|
+
return JSON.stringify(stripForComparison(aObj.metadataBlock)) === JSON.stringify(stripForComparison(bObj.metadataBlock)) && JSON.stringify(stripForComparison(aObj.sectionsBlock)) === JSON.stringify(stripForComparison(bObj.sectionsBlock)) && JSON.stringify(stripForComparison(aObj.removeMetadataBlock)) === JSON.stringify(stripForComparison(bObj.removeMetadataBlock)) && JSON.stringify(stripForComparison(aObj.removeSectionsBlock)) === JSON.stringify(stripForComparison(bObj.removeSectionsBlock));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Check if two metadata items are equal
|
|
187
|
+
*/
|
|
188
|
+
function metadataEquals(a, b) {
|
|
189
|
+
return a.key.value === b.key.value && a.value.raw === b.value.raw;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
//#endregion
|
|
193
|
+
export { entriesEqual, mergeEntry };
|
|
194
|
+
//# sourceMappingURL=entry-merger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry-merger.js","names":["merged: Metadata[]","result: Record<string, unknown>"],"sources":["../../src/merge/entry-merger.ts"],"sourcesContent":["import type { Entry, Metadata, Content } from \"../ast/ast-types.js\";\nimport type { EntryMatch } from \"./entry-matcher.js\";\n\n/**\n * Merge a single entry match (assumes no conflicts)\n *\n * Handles various merge scenarios:\n * - Additions (one side only)\n * - Deletions (missing on one side)\n * - Single-side modifications\n * - Both-side modifications with fine-grained merge\n *\n * @param match - The entry match to merge\n * @returns Merged entry, or null if deleted\n */\nexport function mergeEntry(match: EntryMatch): Entry | null {\n const { base, ours, theirs } = match;\n\n if (!base && ours && !theirs) {\n return ours;\n }\n if (!base && !ours && theirs) {\n return theirs;\n }\n\n if (base && !ours && theirs) {\n return null;\n }\n if (base && ours && !theirs) {\n return null;\n }\n\n if (base && ours && theirs) {\n const oursChanged = !entriesEqual(base, ours);\n const theirsChanged = !entriesEqual(base, theirs);\n\n if (oursChanged && !theirsChanged) {\n return ours;\n }\n if (!oursChanged && theirsChanged) {\n return theirs;\n }\n\n if (oursChanged && theirsChanged) {\n return mergeEntryChanges(base, ours, theirs);\n }\n\n return base;\n }\n\n return ours || theirs || base;\n}\n\n/**\n * Fine-grained merge of entry changes\n *\n * For instance/synthesis entries, merge metadata and content independently\n */\nfunction mergeEntryChanges(base: Entry, ours: Entry, theirs: Entry): Entry {\n if (ours.type === \"instance_entry\" && theirs.type === \"instance_entry\") {\n const baseMetadata =\n base.type === \"instance_entry\" ||\n base.type === \"synthesis_entry\" ||\n base.type === \"actualize_entry\"\n ? base.metadata\n : [];\n const baseContent =\n base.type === \"instance_entry\" || base.type === \"synthesis_entry\" ? base.content : null;\n\n return {\n ...ours,\n metadata: mergeMetadata(baseMetadata, ours.metadata, theirs.metadata),\n content: mergeContent(baseContent, ours.content, theirs.content),\n };\n }\n\n if (ours.type === \"synthesis_entry\" && theirs.type === \"synthesis_entry\") {\n const baseMetadata =\n base.type === \"instance_entry\" ||\n base.type === \"synthesis_entry\" ||\n base.type === \"actualize_entry\"\n ? base.metadata\n : [];\n const baseContent =\n base.type === \"instance_entry\" || base.type === \"synthesis_entry\" ? base.content : null;\n\n return {\n ...ours,\n metadata: mergeMetadata(baseMetadata, ours.metadata, theirs.metadata),\n content: mergeContent(baseContent, ours.content, theirs.content),\n };\n }\n\n if (ours.type === \"actualize_entry\" && theirs.type === \"actualize_entry\") {\n const baseMetadata =\n base.type === \"instance_entry\" ||\n base.type === \"synthesis_entry\" ||\n base.type === \"actualize_entry\"\n ? base.metadata\n : [];\n\n return {\n ...ours,\n metadata: mergeMetadata(baseMetadata, ours.metadata, theirs.metadata),\n };\n }\n\n return ours;\n}\n\n/**\n * Merge metadata lists (non-conflicting keys)\n *\n * Strategy: Take the changed version for each key\n */\nfunction mergeMetadata(base: Metadata[], ours: Metadata[], theirs: Metadata[]): Metadata[] {\n const baseMap = new Map(base.map((m) => [m.key.value, m]));\n const oursMap = new Map(ours.map((m) => [m.key.value, m]));\n const theirsMap = new Map(theirs.map((m) => [m.key.value, m]));\n\n const allKeys = new Set([...baseMap.keys(), ...oursMap.keys(), ...theirsMap.keys()]);\n const merged: Metadata[] = [];\n\n for (const key of allKeys) {\n const baseVal = baseMap.get(key);\n const oursVal = oursMap.get(key);\n const theirsVal = theirsMap.get(key);\n\n // If key was deleted on both sides, don't resurrect it\n if (!oursVal && !theirsVal) {\n continue;\n }\n\n // If ours changed/added the key, use ours\n if (oursVal && (!baseVal || !metadataEquals(baseVal, oursVal))) {\n merged.push(oursVal);\n }\n // If theirs changed/added the key, and ours didn't change it, use theirs\n else if (theirsVal && (!baseVal || !metadataEquals(baseVal, theirsVal))) {\n merged.push(theirsVal);\n }\n // If both sides have the key and neither changed it, use either one\n else if (oursVal && theirsVal) {\n merged.push(oursVal);\n }\n }\n\n return merged;\n}\n\n/**\n * Merge content (best effort)\n *\n * If both sides modified content, take ours for now\n * More sophisticated merging could be added later\n */\nfunction mergeContent(\n base: Content | null,\n ours: Content | null,\n theirs: Content | null,\n): Content | null {\n if (base === null && ours === null && theirs === null) {\n return null;\n }\n\n // If both sides deleted content, don't resurrect base\n if (ours === null && theirs === null) {\n return null;\n }\n\n if (ours && theirs) {\n const oursChanged = !contentEquals(base, ours);\n const theirsChanged = !contentEquals(base, theirs);\n\n if (oursChanged && !theirsChanged) {\n return ours;\n }\n if (!oursChanged && theirsChanged) {\n return theirs;\n }\n\n return ours;\n }\n\n return ours || theirs || base;\n}\n\n/**\n * Check if two entries are deeply equal\n */\nexport function entriesEqual(a: Entry, b: Entry): boolean {\n if (a.type !== b.type) {\n return false;\n }\n\n switch (a.type) {\n case \"instance_entry\":\n if (b.type !== \"instance_entry\") {\n return false;\n }\n return (\n headersEqual(a.header, b.header) &&\n metadataListsEqual(a.metadata, b.metadata) &&\n contentEquals(a.content, b.content)\n );\n\n case \"schema_entry\":\n if (b.type !== \"schema_entry\") {\n return false;\n }\n return headersEqual(a.header, b.header) && schemaBlocksEqual(a, b);\n\n case \"synthesis_entry\":\n if (b.type !== \"synthesis_entry\") {\n return false;\n }\n return (\n headersEqual(a.header, b.header) &&\n metadataListsEqual(a.metadata, b.metadata) &&\n contentEquals(a.content, b.content)\n );\n\n case \"actualize_entry\":\n if (b.type !== \"actualize_entry\") {\n return false;\n }\n return headersEqual(a.header, b.header) && metadataListsEqual(a.metadata, b.metadata);\n\n default:\n return false;\n }\n}\n\n/**\n * Strip non-semantic fields (syntaxNode, location) for comparison.\n * This avoids circular reference issues with tree-sitter nodes.\n */\nfunction stripForComparison(obj: unknown): unknown {\n if (obj === null || obj === undefined) {\n return obj;\n }\n if (Array.isArray(obj)) {\n return obj.map(stripForComparison);\n }\n if (typeof obj === \"object\") {\n const result: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(obj)) {\n // Skip non-semantic fields that may have circular refs\n if (key === \"syntaxNode\" || key === \"location\") {\n continue;\n }\n result[key] = stripForComparison(value);\n }\n return result;\n }\n return obj;\n}\n\n/**\n * Check if two headers are equal (comparing semantic fields only)\n */\nfunction headersEqual(a: unknown, b: unknown): boolean {\n return JSON.stringify(stripForComparison(a)) === JSON.stringify(stripForComparison(b));\n}\n\n/**\n * Check if two metadata lists are equal\n */\nfunction metadataListsEqual(a: Metadata[], b: Metadata[]): boolean {\n if (a.length !== b.length) {\n return false;\n }\n\n const aMap = new Map(a.map((m) => [m.key.value, m.value.raw]));\n const bMap = new Map(b.map((m) => [m.key.value, m.value.raw]));\n\n if (aMap.size !== bMap.size) {\n return false;\n }\n\n for (const [key, value] of aMap) {\n if (bMap.get(key) !== value) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Check if two content nodes are equal\n */\nfunction contentEquals(a: Content | null, b: Content | null): boolean {\n if (a === null && b === null) {\n return true;\n }\n if (a === null || b === null) {\n return false;\n }\n\n if (a.children.length !== b.children.length) {\n return false;\n }\n\n for (let i = 0; i < a.children.length; i++) {\n const childA = a.children[i];\n const childB = b.children[i];\n\n if (childA.type !== childB.type) {\n return false;\n }\n if (childA.text !== childB.text) {\n return false;\n }\n }\n\n return true;\n}\n\n/**\n * Check if schema blocks are equal (comparing semantic fields only)\n */\nfunction schemaBlocksEqual(a: unknown, b: unknown): boolean {\n const aObj = a as {\n metadataBlock?: unknown;\n sectionsBlock?: unknown;\n removeMetadataBlock?: unknown;\n removeSectionsBlock?: unknown;\n };\n const bObj = b as {\n metadataBlock?: unknown;\n sectionsBlock?: unknown;\n removeMetadataBlock?: unknown;\n removeSectionsBlock?: unknown;\n };\n\n return (\n JSON.stringify(stripForComparison(aObj.metadataBlock)) ===\n JSON.stringify(stripForComparison(bObj.metadataBlock)) &&\n JSON.stringify(stripForComparison(aObj.sectionsBlock)) ===\n JSON.stringify(stripForComparison(bObj.sectionsBlock)) &&\n JSON.stringify(stripForComparison(aObj.removeMetadataBlock)) ===\n JSON.stringify(stripForComparison(bObj.removeMetadataBlock)) &&\n JSON.stringify(stripForComparison(aObj.removeSectionsBlock)) ===\n JSON.stringify(stripForComparison(bObj.removeSectionsBlock))\n );\n}\n\n/**\n * Check if two metadata items are equal\n */\nfunction metadataEquals(a: Metadata, b: Metadata): boolean {\n return a.key.value === b.key.value && a.value.raw === b.value.raw;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { MergeConflict } from "./conflict-detector.js";
|
|
2
|
+
|
|
3
|
+
//#region src/merge/merge-result-builder.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Statistics about a merge operation
|
|
7
|
+
*/
|
|
8
|
+
interface MergeStats {
|
|
9
|
+
/**
|
|
10
|
+
* Total entries in merged result
|
|
11
|
+
*/
|
|
12
|
+
totalEntries: number;
|
|
13
|
+
/**
|
|
14
|
+
* Entries present only in ours (additions)
|
|
15
|
+
*/
|
|
16
|
+
oursOnly: number;
|
|
17
|
+
/**
|
|
18
|
+
* Entries present only in theirs (additions)
|
|
19
|
+
*/
|
|
20
|
+
theirsOnly: number;
|
|
21
|
+
/**
|
|
22
|
+
* Entries present in all three versions unchanged
|
|
23
|
+
*/
|
|
24
|
+
common: number;
|
|
25
|
+
/**
|
|
26
|
+
* Entries successfully auto-merged
|
|
27
|
+
*/
|
|
28
|
+
autoMerged: number;
|
|
29
|
+
/**
|
|
30
|
+
* Number of conflicts detected
|
|
31
|
+
*/
|
|
32
|
+
conflicts: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Result of a three-way merge operation
|
|
36
|
+
*/
|
|
37
|
+
interface MergeResult {
|
|
38
|
+
/**
|
|
39
|
+
* Whether the merge completed without conflicts
|
|
40
|
+
* - `true`: Clean merge, all changes reconciled
|
|
41
|
+
* - `false`: Conflicts detected, manual resolution required
|
|
42
|
+
*/
|
|
43
|
+
success: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The merged content as a string
|
|
46
|
+
* - On success: Clean merged entries
|
|
47
|
+
* - On failure: Includes conflict markers
|
|
48
|
+
*/
|
|
49
|
+
content: string;
|
|
50
|
+
/**
|
|
51
|
+
* List of conflicts detected during merge
|
|
52
|
+
* Empty array if success is true
|
|
53
|
+
*/
|
|
54
|
+
conflicts: MergeConflict[];
|
|
55
|
+
/**
|
|
56
|
+
* Statistics about the merge operation
|
|
57
|
+
*/
|
|
58
|
+
stats: MergeStats;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { MergeResult, MergeStats };
|
|
62
|
+
//# sourceMappingURL=merge-result-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-result-builder.d.ts","names":[],"sources":["../../src/merge/merge-result-builder.ts"],"sourcesContent":[],"mappings":";;;;AAUA;AAmCA;;UAnCiB,UAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmCA,WAAA;;;;;;;;;;;;;;;;;aAmBJ;;;;SAKJ"}
|