@trojanbox-vcp-test/site-edit-engine 0.1.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.
Files changed (174) hide show
  1. package/README.md +85 -0
  2. package/dist/execute-integration/execute-fixture-harness.d.ts +25 -0
  3. package/dist/execute-integration/execute-fixture-harness.js +37 -0
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.js +2 -0
  6. package/dist/internal/ast/diagnostics/index.d.ts +5 -0
  7. package/dist/internal/ast/diagnostics/index.js +25 -0
  8. package/dist/internal/ast/history/index.d.ts +15 -0
  9. package/dist/internal/ast/history/index.js +62 -0
  10. package/dist/internal/ast/index.d.ts +8 -0
  11. package/dist/internal/ast/index.js +5 -0
  12. package/dist/internal/ast/locators/index.d.ts +1 -0
  13. package/dist/internal/ast/locators/index.js +1 -0
  14. package/dist/internal/ast/locators/resolve-locator.d.ts +16 -0
  15. package/dist/internal/ast/locators/resolve-locator.js +920 -0
  16. package/dist/internal/ast/parser/SourceParser.d.ts +30 -0
  17. package/dist/internal/ast/parser/SourceParser.js +49 -0
  18. package/dist/internal/ast/parser/index.d.ts +21 -0
  19. package/dist/internal/ast/parser/index.js +64 -0
  20. package/dist/internal/ast/primitives/conditional/conditional-primitives.d.ts +18 -0
  21. package/dist/internal/ast/primitives/conditional/conditional-primitives.js +237 -0
  22. package/dist/internal/ast/primitives/conditional/index.d.ts +1 -0
  23. package/dist/internal/ast/primitives/conditional/index.js +1 -0
  24. package/dist/internal/ast/primitives/imports/add-import.d.ts +18 -0
  25. package/dist/internal/ast/primitives/imports/add-import.js +111 -0
  26. package/dist/internal/ast/primitives/imports/index.d.ts +2 -0
  27. package/dist/internal/ast/primitives/imports/index.js +2 -0
  28. package/dist/internal/ast/primitives/imports/remove-import.d.ts +15 -0
  29. package/dist/internal/ast/primitives/imports/remove-import.js +72 -0
  30. package/dist/internal/ast/primitives/index.d.ts +10 -0
  31. package/dist/internal/ast/primitives/index.js +10 -0
  32. package/dist/internal/ast/primitives/jsx/index.d.ts +4 -0
  33. package/dist/internal/ast/primitives/jsx/index.js +4 -0
  34. package/dist/internal/ast/primitives/jsx/insert-child.d.ts +11 -0
  35. package/dist/internal/ast/primitives/jsx/insert-child.js +69 -0
  36. package/dist/internal/ast/primitives/jsx/move-node.d.ts +9 -0
  37. package/dist/internal/ast/primitives/jsx/move-node.js +76 -0
  38. package/dist/internal/ast/primitives/jsx/remove-node.d.ts +7 -0
  39. package/dist/internal/ast/primitives/jsx/remove-node.js +36 -0
  40. package/dist/internal/ast/primitives/jsx/update-text.d.ts +8 -0
  41. package/dist/internal/ast/primitives/jsx/update-text.js +81 -0
  42. package/dist/internal/ast/primitives/next/index.d.ts +1 -0
  43. package/dist/internal/ast/primitives/next/index.js +1 -0
  44. package/dist/internal/ast/primitives/next/next-primitives.d.ts +43 -0
  45. package/dist/internal/ast/primitives/next/next-primitives.js +211 -0
  46. package/dist/internal/ast/primitives/shared.d.ts +60 -0
  47. package/dist/internal/ast/primitives/shared.js +176 -0
  48. package/dist/internal/ast/primitives/style/class-expression.d.ts +23 -0
  49. package/dist/internal/ast/primitives/style/class-expression.js +174 -0
  50. package/dist/internal/ast/primitives/style/index.d.ts +1 -0
  51. package/dist/internal/ast/primitives/style/index.js +1 -0
  52. package/dist/internal/ast/primitives/style/style-primitives.d.ts +49 -0
  53. package/dist/internal/ast/primitives/style/style-primitives.js +555 -0
  54. package/dist/internal/ast/primitives/values/index.d.ts +1 -0
  55. package/dist/internal/ast/primitives/values/index.js +1 -0
  56. package/dist/internal/ast/primitives/values/value-primitives.d.ts +42 -0
  57. package/dist/internal/ast/primitives/values/value-primitives.js +158 -0
  58. package/dist/internal/ast/printer/SourcePrinter.d.ts +21 -0
  59. package/dist/internal/ast/printer/SourcePrinter.js +76 -0
  60. package/dist/internal/ast/printer/index.d.ts +6 -0
  61. package/dist/internal/ast/printer/index.js +126 -0
  62. package/dist/internal/ast/types.d.ts +190 -0
  63. package/dist/internal/ast/types.js +1 -0
  64. package/dist/internal/capability/capability-resolver.d.ts +16 -0
  65. package/dist/internal/capability/capability-resolver.js +127 -0
  66. package/dist/internal/classname-source.d.ts +24 -0
  67. package/dist/internal/classname-source.js +220 -0
  68. package/dist/internal/contracts/IEditEngineRuntime.d.ts +18 -0
  69. package/dist/internal/contracts/IEditEngineRuntime.js +1 -0
  70. package/dist/internal/domain/EditDiagnostic.d.ts +38 -0
  71. package/dist/internal/domain/EditDiagnostic.js +43 -0
  72. package/dist/internal/events/event-bus.d.ts +14 -0
  73. package/dist/internal/events/event-bus.js +21 -0
  74. package/dist/internal/graph/graph-builder.d.ts +12 -0
  75. package/dist/internal/graph/graph-builder.js +1371 -0
  76. package/dist/internal/graph/import-resolver.d.ts +31 -0
  77. package/dist/internal/graph/import-resolver.js +109 -0
  78. package/dist/internal/graph/project-graph-builder.d.ts +32 -0
  79. package/dist/internal/graph/project-graph-builder.js +133 -0
  80. package/dist/internal/graph/types.d.ts +114 -0
  81. package/dist/internal/graph/types.js +6 -0
  82. package/dist/internal/history/undo-redo.d.ts +28 -0
  83. package/dist/internal/history/undo-redo.js +42 -0
  84. package/dist/internal/index.d.ts +2 -0
  85. package/dist/internal/index.js +1 -0
  86. package/dist/internal/planner/planner.d.ts +104 -0
  87. package/dist/internal/planner/planner.js +2533 -0
  88. package/dist/internal/planner/types.d.ts +275 -0
  89. package/dist/internal/planner/types.js +6 -0
  90. package/dist/internal/protocol/boundary.d.ts +10 -0
  91. package/dist/internal/protocol/boundary.js +3 -0
  92. package/dist/internal/protocol/capability.d.ts +47 -0
  93. package/dist/internal/protocol/capability.js +8 -0
  94. package/dist/internal/protocol/error.d.ts +43 -0
  95. package/dist/internal/protocol/error.js +38 -0
  96. package/dist/internal/protocol/event.d.ts +39 -0
  97. package/dist/internal/protocol/event.js +3 -0
  98. package/dist/internal/protocol/identity.d.ts +26 -0
  99. package/dist/internal/protocol/identity.js +30 -0
  100. package/dist/internal/protocol/operation.d.ts +224 -0
  101. package/dist/internal/protocol/operation.js +8 -0
  102. package/dist/internal/protocol/render.d.ts +212 -0
  103. package/dist/internal/protocol/render.js +3 -0
  104. package/dist/internal/protocol.d.ts +9 -0
  105. package/dist/internal/protocol.js +2 -0
  106. package/dist/internal/provenance/binding-graph.d.ts +39 -0
  107. package/dist/internal/provenance/binding-graph.js +184 -0
  108. package/dist/internal/provenance/capability-policy.d.ts +15 -0
  109. package/dist/internal/provenance/capability-policy.js +96 -0
  110. package/dist/internal/provenance/data-source-classifier.d.ts +14 -0
  111. package/dist/internal/provenance/data-source-classifier.js +281 -0
  112. package/dist/internal/provenance/resolve-text-provenance.d.ts +45 -0
  113. package/dist/internal/provenance/resolve-text-provenance.js +3090 -0
  114. package/dist/internal/provenance/types.d.ts +89 -0
  115. package/dist/internal/provenance/types.js +1 -0
  116. package/dist/internal/render/component-semantic.d.ts +11 -0
  117. package/dist/internal/render/component-semantic.js +141 -0
  118. package/dist/internal/render/content-model.d.ts +3 -0
  119. package/dist/internal/render/content-model.js +89 -0
  120. package/dist/internal/render/media-model.d.ts +3 -0
  121. package/dist/internal/render/media-model.js +45 -0
  122. package/dist/internal/render/provenance-types.d.ts +33 -0
  123. package/dist/internal/render/provenance-types.js +1 -0
  124. package/dist/internal/render/render-projection.d.ts +24 -0
  125. package/dist/internal/render/render-projection.js +281 -0
  126. package/dist/internal/render/tailwind-style-model.d.ts +19 -0
  127. package/dist/internal/render/tailwind-style-model.js +1187 -0
  128. package/dist/internal/runtime/EditEngineRuntime.d.ts +25 -0
  129. package/dist/internal/runtime/EditEngineRuntime.js +89 -0
  130. package/dist/internal/runtime/EditEngineRuntimeSnapshot.d.ts +31 -0
  131. package/dist/internal/runtime/EditEngineRuntimeSnapshot.js +15 -0
  132. package/dist/internal/runtime/InternalEditEngine.d.ts +44 -0
  133. package/dist/internal/runtime/InternalEditEngine.js +1391 -0
  134. package/dist/internal/runtime.d.ts +3 -0
  135. package/dist/internal/runtime.js +1 -0
  136. package/dist/internal/topology/topology.d.ts +6 -0
  137. package/dist/internal/topology/topology.js +98 -0
  138. package/dist/internal/topology/types.d.ts +35 -0
  139. package/dist/internal/topology/types.js +5 -0
  140. package/dist/internal/types.d.ts +1 -0
  141. package/dist/internal/types.js +1 -0
  142. package/dist/internal/writeback/in-memory-fs.d.ts +7 -0
  143. package/dist/internal/writeback/in-memory-fs.js +44 -0
  144. package/dist/internal/writeback/types.d.ts +45 -0
  145. package/dist/internal/writeback/types.js +7 -0
  146. package/dist/internal/writeback/writeback-service.d.ts +7 -0
  147. package/dist/internal/writeback/writeback-service.js +568 -0
  148. package/dist/internal-adapter.d.ts +18 -0
  149. package/dist/internal-adapter.js +350 -0
  150. package/dist/next-app-router-fs.d.ts +2 -0
  151. package/dist/next-app-router-fs.js +64 -0
  152. package/dist/next-app-router.d.ts +11 -0
  153. package/dist/next-app-router.js +140 -0
  154. package/dist/preview-runtime.d.ts +394 -0
  155. package/dist/preview-runtime.js +102 -0
  156. package/dist/public-file-system.d.ts +7 -0
  157. package/dist/public-file-system.js +1 -0
  158. package/dist/runtime-sync.d.ts +95 -0
  159. package/dist/runtime-sync.js +321 -0
  160. package/dist/runtime.d.ts +340 -0
  161. package/dist/runtime.js +134 -0
  162. package/dist/site-edit-instrumentation.d.ts +19 -0
  163. package/dist/site-edit-instrumentation.js +322 -0
  164. package/dist/snapshot-file-system.d.ts +19 -0
  165. package/dist/snapshot-file-system.js +49 -0
  166. package/dist/source-watcher.d.ts +20 -0
  167. package/dist/source-watcher.js +150 -0
  168. package/dist/source-writeback-test-harness.d.ts +244 -0
  169. package/dist/source-writeback-test-harness.js +119 -0
  170. package/dist/types.d.ts +68 -0
  171. package/dist/types.js +1 -0
  172. package/dist/webpack-loader.cjs +592 -0
  173. package/dist/webpack-loader.d.ts +27 -0
  174. package/package.json +66 -0
@@ -0,0 +1,2533 @@
1
+ // ── OperationPlanner 骨架 ──────────────────────────────────────
2
+ // 职责:把 OperationRequest 展开成 RewritePlan。
3
+ // 根文档 03 §4 / 04 §4 定义:
4
+ // - planner 展开 MoveTarget / InsertNodeSpec 为可执行步骤
5
+ // - plan 阶段就解析好 locator / jsx / import 变更
6
+ // - cross-file move = remove + import + insert (+ remove-import)
7
+ // - component insert = import + insert
8
+ import { resolveLocator, } from "../ast/index.js";
9
+ import { resolveLocatorPath } from "../ast/locators/index.js";
10
+ import * as parser from "@babel/parser";
11
+ import _traverse from "@babel/traverse";
12
+ import * as t from "@babel/types";
13
+ import * as nodePath from "node:path";
14
+ import { updateStylePropertyInClassName } from "../render/tailwind-style-model.js";
15
+ import { evaluateCrossFileMovePolicy, evaluateInsertChildPolicy, } from "../provenance/capability-policy.js";
16
+ const traverse = (typeof _traverse === "function" ? _traverse : _traverse.default);
17
+ /* ------------------------------------------------------------------ */
18
+ /* Locator helpers */
19
+ /* ------------------------------------------------------------------ */
20
+ /**
21
+ * 创建 structural-path 定位器 — 对行号偏移(如 add-import 追加 import 行)
22
+ * 有更好的鲁棒性;source-range 作为提示(expectedRange)辅助验证。
23
+ */
24
+ function makeLocator(node) {
25
+ if (node.structuralPath.includes("#repeat:") ||
26
+ node.isRepeatItemRoot ||
27
+ node.isRepeatRegion) {
28
+ return {
29
+ kind: "jsx-node",
30
+ strategy: "source-range",
31
+ sourceRange: node.sourceRange,
32
+ };
33
+ }
34
+ return {
35
+ kind: "jsx-node",
36
+ strategy: "structural-path",
37
+ file: node.file,
38
+ component: node.component,
39
+ structuralPath: node.structuralPath,
40
+ };
41
+ }
42
+ function makeAttributeLocator(node, name) {
43
+ return {
44
+ kind: "jsx-attribute",
45
+ element: makeLocator(node),
46
+ name,
47
+ };
48
+ }
49
+ function getNodeTargetKey(request) {
50
+ return request.target.kind === "node" ? request.target.key : null;
51
+ }
52
+ function getRouteTargetId(request) {
53
+ return request.target.kind === "route" ? request.target.routeId : null;
54
+ }
55
+ function computeRelativeSpecifier(fromFile, toFile) {
56
+ const fromDir = nodePath.dirname(fromFile);
57
+ const toFileNoExt = toFile.replace(/\.(tsx|ts|jsx|js)$/, "");
58
+ let rel = nodePath.relative(fromDir, toFileNoExt);
59
+ rel = rel.replace(/\\/g, "/");
60
+ if (!rel.startsWith("."))
61
+ rel = `./${rel}`;
62
+ return rel;
63
+ }
64
+ function findKnownDirective(source) {
65
+ const clientMatch = source.match(/^\s*['"]use client['"];?/m);
66
+ if (clientMatch) {
67
+ return "use client";
68
+ }
69
+ const serverMatch = source.match(/^\s*['"]use server['"];?/m);
70
+ if (serverMatch) {
71
+ return "use server";
72
+ }
73
+ return null;
74
+ }
75
+ function extractNodeJsx(graph, node) {
76
+ const source = graph.getFileContent(node.file);
77
+ if (!source) {
78
+ return undefined;
79
+ }
80
+ const match = resolveLocator(source, makeLocator(node));
81
+ if (!match.ok) {
82
+ return undefined;
83
+ }
84
+ return source.slice(match.match.start, match.match.end);
85
+ }
86
+ function resolveInsertComponentLocalName(node) {
87
+ if (node.component.exportName !== "default") {
88
+ return node.component.exportName;
89
+ }
90
+ const normalized = node.component.source
91
+ .replace(/\\/g, "/")
92
+ .replace(/\/$/, "");
93
+ const rawSegment = normalized.split("/").pop() || "Component";
94
+ const baseName = rawSegment === "index"
95
+ ? normalized.split("/").slice(-2, -1)[0] || "Component"
96
+ : rawSegment;
97
+ const sanitized = baseName.replace(/\.[^.]+$/, "");
98
+ const tokens = sanitized.split(/[^A-Za-z0-9]+/).filter(Boolean);
99
+ if (tokens.length === 0) {
100
+ return "Component";
101
+ }
102
+ return tokens
103
+ .map((token) => token.charAt(0).toUpperCase() + token.slice(1))
104
+ .join("");
105
+ }
106
+ function parseModuleSource(source) {
107
+ try {
108
+ return parser.parse(source, {
109
+ sourceType: "module",
110
+ plugins: ["jsx", "typescript"],
111
+ });
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ function getNodeRange(node) {
118
+ return typeof node.start === "number" && typeof node.end === "number"
119
+ ? { start: node.start, end: node.end }
120
+ : null;
121
+ }
122
+ function isNodeInsideRange(node, range) {
123
+ const nodeRange = getNodeRange(node);
124
+ return Boolean(nodeRange && nodeRange.start >= range.start && nodeRange.end <= range.end);
125
+ }
126
+ function getJsxTagBindingName(name) {
127
+ if (t.isJSXIdentifier(name)) {
128
+ return /^[A-Z]/.test(name.name) ? name.name : null;
129
+ }
130
+ if (t.isJSXMemberExpression(name)) {
131
+ let current = name;
132
+ while (t.isJSXMemberExpression(current)) {
133
+ current = current.object;
134
+ }
135
+ return current.name;
136
+ }
137
+ return null;
138
+ }
139
+ function collectEscapingBindingNames(sourcePath, targetScopePath) {
140
+ const sourceRange = getNodeRange(sourcePath.node);
141
+ if (!sourceRange) {
142
+ return [];
143
+ }
144
+ const escapingNames = new Set();
145
+ const checkBinding = (name, referencePath) => {
146
+ const binding = referencePath.scope.getBinding(name);
147
+ if (!binding || isNodeInsideRange(binding.path.node, sourceRange)) {
148
+ return;
149
+ }
150
+ const targetBinding = targetScopePath.scope.getBinding(name);
151
+ if (targetBinding !== binding) {
152
+ escapingNames.add(name);
153
+ }
154
+ };
155
+ sourcePath.traverse({
156
+ ReferencedIdentifier(path) {
157
+ checkBinding(path.node.name, path);
158
+ },
159
+ JSXOpeningElement(path) {
160
+ const tagBinding = getJsxTagBindingName(path.node.name);
161
+ if (tagBinding) {
162
+ checkBinding(tagBinding, path);
163
+ }
164
+ },
165
+ });
166
+ return [...escapingNames].sort();
167
+ }
168
+ function checkSameFileMovePreservesLexicalBindings(source, sourceNode, targetParent) {
169
+ const ast = parseModuleSource(source);
170
+ if (!ast) {
171
+ return { ok: true };
172
+ }
173
+ const sourceMatch = resolveLocatorPath(ast, makeLocator(sourceNode));
174
+ if (!sourceMatch.ok || !sourceMatch.match.path.isJSXElement()) {
175
+ return { ok: true };
176
+ }
177
+ const targetParentMatch = resolveLocatorPath(ast, makeLocator(targetParent));
178
+ if (!targetParentMatch.ok || !targetParentMatch.match.path.isJSXElement()) {
179
+ return { ok: true };
180
+ }
181
+ if (crossesConditionalBoundary(sourceMatch.match.path, targetParentMatch.match.path)) {
182
+ return {
183
+ ok: false,
184
+ bindingNames: [],
185
+ reason: "Cannot move node out of its conditional branch",
186
+ };
187
+ }
188
+ const bindingNames = collectEscapingBindingNames(sourceMatch.match.path, targetParentMatch.match.path);
189
+ return bindingNames.length > 0 ? { ok: false, bindingNames } : { ok: true };
190
+ }
191
+ function crossesConditionalBoundary(sourcePath, targetParentPath) {
192
+ const conditionalPath = sourcePath.findParent((path) => path.isConditionalExpression());
193
+ if (!conditionalPath?.isConditionalExpression()) {
194
+ return false;
195
+ }
196
+ return !targetParentPath.findParent((path) => path.node === conditionalPath.node);
197
+ }
198
+ const STRICT_NATIVE_CHILDREN = {
199
+ table: new Set(["caption", "colgroup", "thead", "tbody", "tfoot", "tr"]),
200
+ thead: new Set(["tr"]),
201
+ tbody: new Set(["tr"]),
202
+ tfoot: new Set(["tr"]),
203
+ tr: new Set(["th", "td"]),
204
+ ul: new Set(["li"]),
205
+ ol: new Set(["li"]),
206
+ select: new Set(["option", "optgroup"]),
207
+ optgroup: new Set(["option"]),
208
+ };
209
+ function validateNativeInsertNesting(parent, node) {
210
+ if (node.kind !== "native-tag") {
211
+ return null;
212
+ }
213
+ const parentTag = parent.tag?.toLowerCase();
214
+ if (!parentTag ||
215
+ parentTag !== parent.tag ||
216
+ parentTag.startsWith("component:")) {
217
+ return null;
218
+ }
219
+ const allowedChildren = STRICT_NATIVE_CHILDREN[parentTag];
220
+ if (!allowedChildren) {
221
+ return null;
222
+ }
223
+ const childTag = node.tag.toLowerCase();
224
+ return allowedChildren.has(childTag)
225
+ ? null
226
+ : `Cannot insert <${node.tag}> into <${parent.tag}>; expected ${[...allowedChildren].map((tag) => `<${tag}>`).join(", ")}`;
227
+ }
228
+ function findExistingDefaultImportLocal(ast, moduleSpecifier) {
229
+ for (const statement of ast.program.body) {
230
+ if (!t.isImportDeclaration(statement) ||
231
+ statement.source.value !== moduleSpecifier) {
232
+ continue;
233
+ }
234
+ const defaultImport = statement.specifiers.find((specifier) => t.isImportDefaultSpecifier(specifier));
235
+ if (defaultImport && t.isImportDefaultSpecifier(defaultImport)) {
236
+ return defaultImport.local.name;
237
+ }
238
+ }
239
+ return null;
240
+ }
241
+ function collectProgramBindingNames(ast) {
242
+ const names = new Set();
243
+ traverse(ast, {
244
+ Program(path) {
245
+ for (const bindingName of Object.keys(path.scope.bindings)) {
246
+ names.add(bindingName);
247
+ }
248
+ path.stop();
249
+ },
250
+ });
251
+ return names;
252
+ }
253
+ function chooseAvailableComponentLocalName(preferred, usedNames) {
254
+ if (!usedNames.has(preferred)) {
255
+ return preferred;
256
+ }
257
+ let suffix = 2;
258
+ while (usedNames.has(`${preferred}${suffix}`)) {
259
+ suffix += 1;
260
+ }
261
+ return `${preferred}${suffix}`;
262
+ }
263
+ function planComponentInsert(node, fileSource) {
264
+ const preferredLocalName = resolveInsertComponentLocalName(node);
265
+ if (node.component.exportName !== "default") {
266
+ return {
267
+ localName: preferredLocalName,
268
+ importStep: buildComponentImport(node, preferredLocalName),
269
+ };
270
+ }
271
+ const ast = parseModuleSource(fileSource);
272
+ if (!ast) {
273
+ return {
274
+ localName: preferredLocalName,
275
+ importStep: buildComponentImport(node, preferredLocalName),
276
+ };
277
+ }
278
+ const existingLocal = findExistingDefaultImportLocal(ast, node.component.source);
279
+ if (existingLocal) {
280
+ return { localName: existingLocal };
281
+ }
282
+ const localName = chooseAvailableComponentLocalName(preferredLocalName, collectProgramBindingNames(ast));
283
+ return {
284
+ localName,
285
+ importStep: buildComponentImport(node, localName),
286
+ };
287
+ }
288
+ function buildJsxFromSpec(node, componentLocalName) {
289
+ if (node.kind === "native-tag") {
290
+ return node.text
291
+ ? `<${node.tag}>${node.text}</${node.tag}>`
292
+ : `<${node.tag} />`;
293
+ }
294
+ const tag = componentLocalName ?? resolveInsertComponentLocalName(node);
295
+ if (node.props && Object.keys(node.props).length > 0) {
296
+ const propsStr = Object.entries(node.props)
297
+ .map(([key, value]) => typeof value === "string"
298
+ ? `${key}="${value}"`
299
+ : `${key}={${JSON.stringify(value)}}`)
300
+ .join(" ");
301
+ return `<${tag} ${propsStr} />`;
302
+ }
303
+ return `<${tag} />`;
304
+ }
305
+ function insertNodeSpecToOperationValue(node) {
306
+ if (node.kind === "native-tag") {
307
+ return node.text === undefined
308
+ ? { kind: node.kind, tag: node.tag }
309
+ : { kind: node.kind, tag: node.tag, text: node.text };
310
+ }
311
+ const props = node.props ? toOperationValueRecord(node.props) : undefined;
312
+ return {
313
+ kind: node.kind,
314
+ component: {
315
+ source: node.component.source,
316
+ exportName: node.component.exportName,
317
+ },
318
+ ...(props ? { props } : {}),
319
+ };
320
+ }
321
+ function toOperationValueRecord(value) {
322
+ return Object.fromEntries(Object.entries(value).filter((entry) => isOperationValue(entry[1])));
323
+ }
324
+ function isOperationValue(value) {
325
+ if (value === null ||
326
+ typeof value === "string" ||
327
+ typeof value === "number" ||
328
+ typeof value === "boolean") {
329
+ return true;
330
+ }
331
+ if (Array.isArray(value)) {
332
+ return value.every(isOperationValue);
333
+ }
334
+ if (typeof value === "object") {
335
+ return Object.values(value).every(isOperationValue);
336
+ }
337
+ return false;
338
+ }
339
+ function countJsxTagReferences(source, tag) {
340
+ const ast = parseModuleSource(source);
341
+ if (!ast) {
342
+ return Number.POSITIVE_INFINITY;
343
+ }
344
+ let count = 0;
345
+ traverse(ast, {
346
+ JSXOpeningElement(path) {
347
+ const name = path.node.name;
348
+ if (t.isJSXIdentifier(name) && name.name === tag) {
349
+ count += 1;
350
+ }
351
+ },
352
+ });
353
+ return count;
354
+ }
355
+ function buildComponentImport(node, localName = resolveInsertComponentLocalName(node)) {
356
+ return {
357
+ moduleSpecifier: node.component.source,
358
+ imports: node.component.exportName === "default"
359
+ ? [{ kind: "default", local: localName }]
360
+ : [{ kind: "named", imported: node.component.exportName }],
361
+ };
362
+ }
363
+ function resolveTargetRelation(target, graph) {
364
+ if ("parentKey" in target) {
365
+ return {
366
+ parentNode: graph.getNode(target.parentKey),
367
+ anchorNode: null,
368
+ placement: target.relation,
369
+ };
370
+ }
371
+ const anchorNode = graph.getNode(target.anchorKey);
372
+ const parentNode = anchorNode?.parentKey
373
+ ? (graph.getNode(anchorNode.parentKey) ?? anchorNode)
374
+ : anchorNode;
375
+ return {
376
+ parentNode,
377
+ anchorNode,
378
+ placement: target.relation,
379
+ };
380
+ }
381
+ function isSameOrDescendant(key, ancestorKey, graph) {
382
+ let currentKey = key;
383
+ while (currentKey) {
384
+ if (currentKey === ancestorKey) {
385
+ return true;
386
+ }
387
+ currentKey = graph.getNode(currentKey)?.parentKey;
388
+ }
389
+ return false;
390
+ }
391
+ function invalidParamsReason(expectedKind, actualKind) {
392
+ return `Operation kind '${expectedKind}' does not match params.kind '${actualKind}'`;
393
+ }
394
+ function cloneValueLocator(locator, path = locator.path) {
395
+ return {
396
+ kind: "value",
397
+ root: locator.root,
398
+ path: path && path.length > 0 ? path : undefined,
399
+ expectedRange: locator.expectedRange,
400
+ };
401
+ }
402
+ function getLastPathSegment(locator) {
403
+ const path = locator.path ?? [];
404
+ return path.length > 0 ? path[path.length - 1] : null;
405
+ }
406
+ function getParentObjectLocator(locator) {
407
+ const last = getLastPathSegment(locator);
408
+ if (!last || last.kind !== "property") {
409
+ return null;
410
+ }
411
+ return cloneValueLocator(locator, locator.path?.slice(0, -1));
412
+ }
413
+ function getNearestCollectionLocator(locator) {
414
+ const path = locator.path ?? [];
415
+ for (let i = path.length - 1; i >= 0; i -= 1) {
416
+ const segment = path[i];
417
+ if (segment.kind === "index") {
418
+ return {
419
+ locator: cloneValueLocator(locator, path.slice(0, i)),
420
+ index: segment.index,
421
+ };
422
+ }
423
+ }
424
+ return null;
425
+ }
426
+ function buildRepeatInsertValueFromLocator(locator, text) {
427
+ const path = locator.path ?? [];
428
+ const collectionIndex = path.findLastIndex((segment) => segment.kind === "index");
429
+ if (collectionIndex === -1) {
430
+ return undefined;
431
+ }
432
+ const itemPath = path.slice(collectionIndex + 1);
433
+ if (itemPath.length === 0) {
434
+ return text;
435
+ }
436
+ return buildObjectValueFromPath(itemPath, text);
437
+ }
438
+ function buildObjectValueFromPath(path, value) {
439
+ const [head, ...rest] = path;
440
+ if (!head) {
441
+ return value;
442
+ }
443
+ if (head.kind !== "property") {
444
+ return undefined;
445
+ }
446
+ const nested = buildObjectValueFromPath(rest, value);
447
+ return nested === undefined ? undefined : { [head.name]: nested };
448
+ }
449
+ function getUpstreamNodeEditReason(node, textTarget, provenance) {
450
+ const finalKind = provenance?.finalSource?.kind;
451
+ if (finalKind === "component-prop") {
452
+ return "Cannot edit prop-backed node structure with a generic node operation";
453
+ }
454
+ if (textTarget?.kind === "value-binding" &&
455
+ textTarget.sourceKind === "repeat-template" &&
456
+ !node.isRepeatItemRoot) {
457
+ return "Cannot edit repeat-backed child node structure with a generic node operation";
458
+ }
459
+ if (textTarget?.kind === "value-binding" &&
460
+ textTarget.file !== node.file &&
461
+ textTarget.sourceKind !== "repeat-template") {
462
+ return "Cannot edit upstream-bound node structure with a generic node operation";
463
+ }
464
+ return null;
465
+ }
466
+ function replaceNearestCollectionIndex(locator, index) {
467
+ const path = locator.path ?? [];
468
+ for (let i = path.length - 1; i >= 0; i -= 1) {
469
+ const segment = path[i];
470
+ if (segment.kind === "index") {
471
+ const nextPath = [...path];
472
+ nextPath[i] = { kind: "index", index };
473
+ return cloneValueLocator(locator, nextPath);
474
+ }
475
+ }
476
+ return null;
477
+ }
478
+ function mapCollectionItemIndex(index, indexMap) {
479
+ return indexMap?.[index] ?? index;
480
+ }
481
+ function mapCollectionInsertionIndex(index, indexMap) {
482
+ if (!indexMap || indexMap.length === 0) {
483
+ return index;
484
+ }
485
+ if (index <= 0) {
486
+ return indexMap[0] ?? 0;
487
+ }
488
+ if (index >= indexMap.length) {
489
+ return (indexMap[indexMap.length - 1] ?? indexMap.length - 1) + 1;
490
+ }
491
+ return indexMap[index] ?? index;
492
+ }
493
+ function sameValueLocator(left, right) {
494
+ return (sameValueRoot(left.root, right.root) &&
495
+ sameValuePath(left.path ?? [], right.path ?? []));
496
+ }
497
+ function sameValueRoot(left, right) {
498
+ if (left.kind !== right.kind) {
499
+ return false;
500
+ }
501
+ switch (left.kind) {
502
+ case "binding":
503
+ case "hook-state":
504
+ case "named-export":
505
+ return "name" in right && left.name === right.name;
506
+ case "default-export":
507
+ return true;
508
+ case "expression":
509
+ return ("sourceRange" in right &&
510
+ sameSourceRange(left.sourceRange, right.sourceRange));
511
+ }
512
+ }
513
+ function sameSourceRange(left, right) {
514
+ return (left.startLine === right.startLine &&
515
+ left.startColumn === right.startColumn &&
516
+ left.endLine === right.endLine &&
517
+ left.endColumn === right.endColumn);
518
+ }
519
+ function findJsxElementBySourceRange(ast, node) {
520
+ let found = null;
521
+ traverse(ast, {
522
+ JSXElement(path) {
523
+ const loc = path.node.loc;
524
+ if (!loc) {
525
+ return;
526
+ }
527
+ if (loc.start.line === node.sourceRange.startLine &&
528
+ loc.start.column === node.sourceRange.startColumn &&
529
+ loc.end.line === node.sourceRange.endLine &&
530
+ loc.end.column === node.sourceRange.endColumn) {
531
+ found = path;
532
+ path.stop();
533
+ }
534
+ },
535
+ });
536
+ return found;
537
+ }
538
+ function sameValuePath(left, right) {
539
+ if (left.length !== right.length) {
540
+ return false;
541
+ }
542
+ for (let index = 0; index < left.length; index += 1) {
543
+ const leftSegment = left[index];
544
+ const rightSegment = right[index];
545
+ if (leftSegment.kind !== rightSegment.kind) {
546
+ return false;
547
+ }
548
+ if (leftSegment.kind === "property") {
549
+ if (rightSegment.kind !== "property" ||
550
+ leftSegment.name !== rightSegment.name) {
551
+ return false;
552
+ }
553
+ continue;
554
+ }
555
+ if (rightSegment.kind !== "index" ||
556
+ leftSegment.index !== rightSegment.index) {
557
+ return false;
558
+ }
559
+ }
560
+ return true;
561
+ }
562
+ const MEDIA_FIELD_NAMES = new Set([
563
+ "src",
564
+ "alt",
565
+ "poster",
566
+ "caption",
567
+ "width",
568
+ "height",
569
+ ]);
570
+ function resolveAttributeTarget(node, field) {
571
+ const attribute = node.attributes?.find((candidate) => candidate.name === field) ?? null;
572
+ const source = attribute?.source;
573
+ return {
574
+ attribute,
575
+ attributeLocator: makeAttributeLocator(node, field),
576
+ valueLocator: source &&
577
+ (attribute?.resolvedValue !== undefined ||
578
+ attribute.value !== source.expression)
579
+ ? {
580
+ kind: "value",
581
+ root: { kind: "binding", name: source.base },
582
+ path: source.path.length > 0 ? source.path : [],
583
+ }
584
+ : null,
585
+ };
586
+ }
587
+ function isPlainObjectRecord(value) {
588
+ return !Array.isArray(value) && typeof value === "object" && value !== null;
589
+ }
590
+ function detectContentSchema(value) {
591
+ if (typeof value === "string") {
592
+ return "markdown";
593
+ }
594
+ if (Array.isArray(value) &&
595
+ value.every((item) => isPlainObjectRecord(item))) {
596
+ return "node-array";
597
+ }
598
+ if (isPlainObjectRecord(value) &&
599
+ value.type === "doc" &&
600
+ Array.isArray(value.content)) {
601
+ return "prosemirror-doc";
602
+ }
603
+ return null;
604
+ }
605
+ function cloneOperationValue(value) {
606
+ return JSON.parse(JSON.stringify(value));
607
+ }
608
+ function splitMarkdownBlocks(markdown) {
609
+ return markdown === "" ? [] : markdown.split(/\n{2,}/);
610
+ }
611
+ function applyMarkdownBlockInsert(current, index, block) {
612
+ if (typeof block !== "string") {
613
+ return null;
614
+ }
615
+ const blocks = splitMarkdownBlocks(current);
616
+ if (index < 0 || index > blocks.length) {
617
+ return null;
618
+ }
619
+ blocks.splice(index, 0, block);
620
+ return blocks.join("\n\n");
621
+ }
622
+ function applyMarkdownBlockRemoval(current, index) {
623
+ const blocks = splitMarkdownBlocks(current);
624
+ if (index < 0 || index >= blocks.length) {
625
+ return null;
626
+ }
627
+ blocks.splice(index, 1);
628
+ return blocks.join("\n\n");
629
+ }
630
+ function applyMarkdownBlockMove(current, fromIndex, toIndex) {
631
+ const blocks = splitMarkdownBlocks(current);
632
+ if (fromIndex < 0 ||
633
+ fromIndex >= blocks.length ||
634
+ toIndex < 0 ||
635
+ toIndex >= blocks.length) {
636
+ return null;
637
+ }
638
+ const [block] = blocks.splice(fromIndex, 1);
639
+ blocks.splice(toIndex, 0, block);
640
+ return blocks.join("\n\n");
641
+ }
642
+ function applyRichTextBlockInsert(schema, current, index, block) {
643
+ if (!isPlainObjectRecord(block)) {
644
+ return null;
645
+ }
646
+ if (schema === "node-array") {
647
+ if (!Array.isArray(current)) {
648
+ return null;
649
+ }
650
+ const next = cloneOperationValue(current);
651
+ if (index < 0 || index > next.length) {
652
+ return null;
653
+ }
654
+ next.splice(index, 0, cloneOperationValue(block));
655
+ return next;
656
+ }
657
+ if (!isPlainObjectRecord(current) || !Array.isArray(current.content)) {
658
+ return null;
659
+ }
660
+ const next = cloneOperationValue(current);
661
+ if (!isPlainObjectRecord(next) || !Array.isArray(next.content)) {
662
+ return null;
663
+ }
664
+ if (index < 0 || index > next.content.length) {
665
+ return null;
666
+ }
667
+ next.content.splice(index, 0, cloneOperationValue(block));
668
+ return next;
669
+ }
670
+ function applyRichTextBlockRemoval(schema, current, index) {
671
+ if (schema === "node-array") {
672
+ if (!Array.isArray(current) || index < 0 || index >= current.length) {
673
+ return null;
674
+ }
675
+ const next = cloneOperationValue(current);
676
+ next.splice(index, 1);
677
+ return next;
678
+ }
679
+ if (!isPlainObjectRecord(current) ||
680
+ !Array.isArray(current.content) ||
681
+ index < 0 ||
682
+ index >= current.content.length) {
683
+ return null;
684
+ }
685
+ const next = cloneOperationValue(current);
686
+ if (!isPlainObjectRecord(next) || !Array.isArray(next.content)) {
687
+ return null;
688
+ }
689
+ next.content.splice(index, 1);
690
+ return next;
691
+ }
692
+ function applyRichTextBlockMove(schema, current, fromIndex, toIndex) {
693
+ const blocks = schema === "node-array"
694
+ ? Array.isArray(current)
695
+ ? cloneOperationValue(current)
696
+ : null
697
+ : isPlainObjectRecord(current) && Array.isArray(current.content)
698
+ ? cloneOperationValue(current.content)
699
+ : null;
700
+ if (!blocks ||
701
+ fromIndex < 0 ||
702
+ fromIndex >= blocks.length ||
703
+ toIndex < 0 ||
704
+ toIndex >= blocks.length) {
705
+ return null;
706
+ }
707
+ const [block] = blocks.splice(fromIndex, 1);
708
+ blocks.splice(toIndex, 0, block);
709
+ if (schema === "node-array") {
710
+ return blocks;
711
+ }
712
+ const next = cloneOperationValue(current);
713
+ next.content = blocks;
714
+ return next;
715
+ }
716
+ function getRichTextBlockCount(schema, current) {
717
+ if (schema === "markdown") {
718
+ return typeof current === "string"
719
+ ? splitMarkdownBlocks(current).length
720
+ : 0;
721
+ }
722
+ if (schema === "node-array") {
723
+ return Array.isArray(current) ? current.length : 0;
724
+ }
725
+ return isPlainObjectRecord(current) && Array.isArray(current.content)
726
+ ? current.content.length
727
+ : 0;
728
+ }
729
+ function getRichTextInsertionIndex(target, anchorNode, field, blockCount) {
730
+ if (target.relation === "prepend") {
731
+ return 0;
732
+ }
733
+ if (target.relation === "append") {
734
+ return blockCount;
735
+ }
736
+ if (!anchorNode?.richTextBlock || anchorNode.richTextBlock.field !== field) {
737
+ return null;
738
+ }
739
+ return target.relation === "before"
740
+ ? anchorNode.richTextBlock.index
741
+ : anchorNode.richTextBlock.index + 1;
742
+ }
743
+ function getRichTextMoveTargetIndex(target, resolved, source, blockCount) {
744
+ if (target.relation === "prepend") {
745
+ return 0;
746
+ }
747
+ if (target.relation === "append") {
748
+ return blockCount - 1;
749
+ }
750
+ const anchor = resolved.anchorNode?.richTextBlock;
751
+ if (!anchor || anchor.field !== source.field) {
752
+ return null;
753
+ }
754
+ return target.relation === "before"
755
+ ? anchor.index - (source.index < anchor.index ? 1 : 0)
756
+ : anchor.index + (source.index < anchor.index ? 0 : 1);
757
+ }
758
+ function getCurrentAttributeValue(attribute) {
759
+ if (attribute.resolvedValue !== undefined) {
760
+ return attribute.resolvedValue;
761
+ }
762
+ if (attribute.source && attribute.value === attribute.source.expression) {
763
+ return undefined;
764
+ }
765
+ return attribute.value;
766
+ }
767
+ function resolveEditableStyleClassTarget(node) {
768
+ const target = resolveAttributeTarget(node, "className");
769
+ if (!target.attribute) {
770
+ return {
771
+ ok: true,
772
+ target,
773
+ value: "",
774
+ };
775
+ }
776
+ if (target.attribute.source) {
777
+ return {
778
+ ok: false,
779
+ result: {
780
+ ok: false,
781
+ errorCode: "unsupported-operation",
782
+ reason: "Style editing requires a static className string",
783
+ },
784
+ };
785
+ }
786
+ const currentValue = getCurrentAttributeValue(target.attribute);
787
+ if (typeof currentValue !== "string") {
788
+ return {
789
+ ok: false,
790
+ result: {
791
+ ok: false,
792
+ errorCode: "unsupported-operation",
793
+ reason: target.attribute.source
794
+ ? "Style binding must resolve to a static className string before it can be edited"
795
+ : "Style editing currently only supports string-backed className values",
796
+ },
797
+ };
798
+ }
799
+ return {
800
+ ok: true,
801
+ target,
802
+ value: currentValue,
803
+ };
804
+ }
805
+ /* ------------------------------------------------------------------ */
806
+ /* OperationPlanner */
807
+ /* ------------------------------------------------------------------ */
808
+ let operationCounter = 0;
809
+ export class OperationPlanner {
810
+ constructor(graph) {
811
+ this.graph = graph;
812
+ }
813
+ async plan(request) {
814
+ const operationId = `op-${++operationCounter}-${Date.now()}`;
815
+ switch (request.kind) {
816
+ case "update-text": {
817
+ if (request.params.kind !== "update-text") {
818
+ return {
819
+ ok: false,
820
+ errorCode: "unknown",
821
+ reason: invalidParamsReason(request.kind, request.params.kind),
822
+ };
823
+ }
824
+ const typedRequest = request;
825
+ const resolvedNode = this.resolveNodeRequest(request);
826
+ if (!resolvedNode.ok) {
827
+ return resolvedNode.result;
828
+ }
829
+ const targetNode = resolvedNode.node;
830
+ const currentVersion = this.graph.getFileVersion(targetNode.file);
831
+ if (request.documentVersion < currentVersion) {
832
+ return {
833
+ ok: false,
834
+ errorCode: "stale-version",
835
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
836
+ };
837
+ }
838
+ return this.planUpdateText(operationId, typedRequest, targetNode, this.graph.getCapability(targetNode.key), await this.graph.getTextWriteTarget(targetNode.key, typedRequest.params.segmentIndex));
839
+ }
840
+ case "remove-node": {
841
+ if (request.params.kind !== "remove-node") {
842
+ return {
843
+ ok: false,
844
+ errorCode: "unknown",
845
+ reason: invalidParamsReason(request.kind, request.params.kind),
846
+ };
847
+ }
848
+ const typedRequest = request;
849
+ const resolvedNode = this.resolveNodeRequest(request);
850
+ if (!resolvedNode.ok) {
851
+ return resolvedNode.result;
852
+ }
853
+ const targetNode = resolvedNode.node;
854
+ const currentVersion = this.graph.getFileVersion(targetNode.file);
855
+ if (request.documentVersion < currentVersion) {
856
+ return {
857
+ ok: false,
858
+ errorCode: "stale-version",
859
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
860
+ };
861
+ }
862
+ return this.planRemoveNode(operationId, typedRequest, targetNode, this.graph.getCapability(targetNode.key));
863
+ }
864
+ case "insert-child": {
865
+ if (request.params.kind !== "insert-child") {
866
+ return {
867
+ ok: false,
868
+ errorCode: "unknown",
869
+ reason: invalidParamsReason(request.kind, request.params.kind),
870
+ };
871
+ }
872
+ const typedRequest = request;
873
+ const resolved = resolveTargetRelation(typedRequest.params.position, this.graph);
874
+ if (!resolved.parentNode) {
875
+ return {
876
+ ok: false,
877
+ errorCode: "locator-miss",
878
+ reason: `Insert target parent for request '${request.id}' not found in graph`,
879
+ };
880
+ }
881
+ const currentVersion = this.graph.getFileVersion(resolved.parentNode.file);
882
+ if (request.documentVersion < currentVersion) {
883
+ return {
884
+ ok: false,
885
+ errorCode: "stale-version",
886
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
887
+ };
888
+ }
889
+ return this.planInsertChild(operationId, typedRequest, resolved.parentNode, this.graph.getCapability(resolved.parentNode.key), resolved.anchorNode);
890
+ }
891
+ case "move-node": {
892
+ if (request.params.kind !== "move-node") {
893
+ return {
894
+ ok: false,
895
+ errorCode: "unknown",
896
+ reason: invalidParamsReason(request.kind, request.params.kind),
897
+ };
898
+ }
899
+ const typedRequest = request;
900
+ const resolvedNode = this.resolveNodeRequest(request);
901
+ if (!resolvedNode.ok) {
902
+ return resolvedNode.result;
903
+ }
904
+ const targetNode = resolvedNode.node;
905
+ const currentVersion = this.graph.getFileVersion(targetNode.file);
906
+ if (request.documentVersion < currentVersion) {
907
+ return {
908
+ ok: false,
909
+ errorCode: "stale-version",
910
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
911
+ };
912
+ }
913
+ return this.planMoveNode(operationId, typedRequest, targetNode, this.graph.getCapability(targetNode.key));
914
+ }
915
+ case "replace-rich-text-content":
916
+ case "insert-rich-text-block":
917
+ case "remove-rich-text-block":
918
+ case "set-media-field": {
919
+ const resolvedNode = this.resolveNodeRequest(request);
920
+ if (!resolvedNode.ok) {
921
+ return resolvedNode.result;
922
+ }
923
+ const targetNode = resolvedNode.node;
924
+ const currentVersion = this.graph.getFileVersion(targetNode.file);
925
+ if (request.documentVersion < currentVersion) {
926
+ return {
927
+ ok: false,
928
+ errorCode: "stale-version",
929
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
930
+ };
931
+ }
932
+ switch (request.kind) {
933
+ case "replace-rich-text-content":
934
+ if (request.params.kind !== "replace-rich-text-content") {
935
+ return {
936
+ ok: false,
937
+ errorCode: "unknown",
938
+ reason: invalidParamsReason(request.kind, request.params.kind),
939
+ };
940
+ }
941
+ return this.planRichTextOperation(operationId, request, targetNode);
942
+ case "insert-rich-text-block":
943
+ if (request.params.kind !== "insert-rich-text-block") {
944
+ return {
945
+ ok: false,
946
+ errorCode: "unknown",
947
+ reason: invalidParamsReason(request.kind, request.params.kind),
948
+ };
949
+ }
950
+ return this.planRichTextOperation(operationId, request, targetNode);
951
+ case "remove-rich-text-block":
952
+ if (request.params.kind !== "remove-rich-text-block") {
953
+ return {
954
+ ok: false,
955
+ errorCode: "unknown",
956
+ reason: invalidParamsReason(request.kind, request.params.kind),
957
+ };
958
+ }
959
+ return this.planRichTextOperation(operationId, request, targetNode);
960
+ case "set-media-field":
961
+ if (request.params.kind !== "set-media-field") {
962
+ return {
963
+ ok: false,
964
+ errorCode: "unknown",
965
+ reason: invalidParamsReason(request.kind, request.params.kind),
966
+ };
967
+ }
968
+ return this.planMediaFieldOperation(operationId, request, targetNode);
969
+ }
970
+ }
971
+ case "set-component-slot-content": {
972
+ const resolvedNode = this.resolveNodeRequest(request);
973
+ if (!resolvedNode.ok) {
974
+ return resolvedNode.result;
975
+ }
976
+ switch (request.kind) {
977
+ case "set-component-slot-content":
978
+ if (request.params.kind !== "set-component-slot-content") {
979
+ return {
980
+ ok: false,
981
+ errorCode: "unknown",
982
+ reason: invalidParamsReason(request.kind, request.params.kind),
983
+ };
984
+ }
985
+ return this.planComponentSlotOperation(operationId, request, resolvedNode.node);
986
+ }
987
+ }
988
+ case "set-object-field":
989
+ case "insert-object-field":
990
+ case "remove-object-field":
991
+ case "update-array-item":
992
+ case "insert-array-item":
993
+ case "remove-array-item":
994
+ case "move-array-item": {
995
+ const resolvedNode = this.resolveNodeRequest(request);
996
+ if (!resolvedNode.ok) {
997
+ return resolvedNode.result;
998
+ }
999
+ const targetNode = resolvedNode.node;
1000
+ const currentVersion = this.graph.getFileVersion(targetNode.file);
1001
+ if (request.documentVersion < currentVersion) {
1002
+ return {
1003
+ ok: false,
1004
+ errorCode: "stale-version",
1005
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
1006
+ };
1007
+ }
1008
+ return this.planValueCollectionOperation(operationId, request, targetNode);
1009
+ }
1010
+ case "replace-conditional-expression":
1011
+ case "set-conditional-branch-content": {
1012
+ const resolvedNode = this.resolveNodeRequest(request);
1013
+ if (!resolvedNode.ok) {
1014
+ return resolvedNode.result;
1015
+ }
1016
+ const targetNode = resolvedNode.node;
1017
+ const currentVersion = this.graph.getFileVersion(targetNode.file);
1018
+ if (request.documentVersion < currentVersion) {
1019
+ return {
1020
+ ok: false,
1021
+ errorCode: "stale-version",
1022
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
1023
+ };
1024
+ }
1025
+ return this.planConditionalOperation(operationId, request, targetNode);
1026
+ }
1027
+ case "set-jsx-prop":
1028
+ case "remove-jsx-prop":
1029
+ case "set-class-name":
1030
+ case "add-class-token":
1031
+ case "remove-class-token":
1032
+ case "set-style-property":
1033
+ case "set-style-properties":
1034
+ case "set-css-module-class": {
1035
+ const resolvedNode = this.resolveNodeRequest(request);
1036
+ if (!resolvedNode.ok) {
1037
+ return resolvedNode.result;
1038
+ }
1039
+ const currentVersion = this.graph.getFileVersion(resolvedNode.node.file);
1040
+ if (request.documentVersion < currentVersion) {
1041
+ return {
1042
+ ok: false,
1043
+ errorCode: "stale-version",
1044
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
1045
+ };
1046
+ }
1047
+ return this.planNodeAttributeOperation(operationId, request, resolvedNode.node);
1048
+ }
1049
+ case "set-directive":
1050
+ case "remove-directive":
1051
+ case "set-route-export":
1052
+ case "set-metadata-field":
1053
+ case "set-generate-metadata": {
1054
+ const resolvedRoute = this.resolveRouteRequest(request);
1055
+ if (!resolvedRoute.ok) {
1056
+ return resolvedRoute.result;
1057
+ }
1058
+ const currentVersion = this.graph.getFileVersion(resolvedRoute.file);
1059
+ if (request.documentVersion < currentVersion) {
1060
+ return {
1061
+ ok: false,
1062
+ errorCode: "stale-version",
1063
+ reason: `Document version ${request.documentVersion} is stale (current: ${currentVersion})`,
1064
+ };
1065
+ }
1066
+ return this.planRouteOperation(operationId, request, resolvedRoute.file);
1067
+ }
1068
+ default:
1069
+ return {
1070
+ ok: false,
1071
+ errorCode: "unknown",
1072
+ reason: `Unknown operation kind: ${request.kind}`,
1073
+ };
1074
+ }
1075
+ }
1076
+ planConditionalOperation(operationId, request, node) {
1077
+ const capability = this.graph.getCapability(node.key);
1078
+ const locator = makeLocator(node);
1079
+ switch (request.kind) {
1080
+ case "replace-conditional-expression":
1081
+ return this.buildPlan(operationId, request, [
1082
+ {
1083
+ order: 0,
1084
+ file: node.file,
1085
+ type: "replace-conditional-expression",
1086
+ locator,
1087
+ nextExpression: request.params.nextExpression,
1088
+ },
1089
+ ], capability);
1090
+ case "set-conditional-branch-content":
1091
+ return this.buildPlan(operationId, request, [
1092
+ {
1093
+ order: 0,
1094
+ file: node.file,
1095
+ type: "set-conditional-branch-content",
1096
+ locator,
1097
+ branch: request.params.branch,
1098
+ content: request.params.content,
1099
+ },
1100
+ ], capability);
1101
+ }
1102
+ }
1103
+ planRichTextOperation(operationId, request, node) {
1104
+ const capability = this.graph.getCapability(node.key);
1105
+ const target = resolveAttributeTarget(node, request.params.field);
1106
+ if (!target.attribute) {
1107
+ return {
1108
+ ok: false,
1109
+ errorCode: "invalid-params",
1110
+ reason: `Rich text field '${request.params.field}' was not found on the target node`,
1111
+ };
1112
+ }
1113
+ const currentValue = getCurrentAttributeValue(target.attribute);
1114
+ const schema = detectContentSchema(currentValue);
1115
+ if (!schema) {
1116
+ return {
1117
+ ok: false,
1118
+ errorCode: "unsupported-operation",
1119
+ reason: `Rich text field '${request.params.field}' is only editable for markdown strings, top-level node arrays, or { type: 'doc', content: [] } documents`,
1120
+ };
1121
+ }
1122
+ let nextValue = null;
1123
+ switch (request.kind) {
1124
+ case "replace-rich-text-content":
1125
+ if (schema === "markdown" && typeof request.params.value !== "string") {
1126
+ return {
1127
+ ok: false,
1128
+ errorCode: "invalid-params",
1129
+ reason: "Markdown rich text replacement requires a string value",
1130
+ };
1131
+ }
1132
+ if (schema !== "markdown" &&
1133
+ detectContentSchema(request.params.value) !== schema) {
1134
+ return {
1135
+ ok: false,
1136
+ errorCode: "invalid-params",
1137
+ reason: `Rich text replacement must preserve the '${schema}' schema`,
1138
+ };
1139
+ }
1140
+ nextValue = request.params.value;
1141
+ break;
1142
+ case "insert-rich-text-block":
1143
+ nextValue =
1144
+ schema === "markdown"
1145
+ ? applyMarkdownBlockInsert(currentValue, request.params.index, request.params.block)
1146
+ : applyRichTextBlockInsert(schema, currentValue, request.params.index, request.params.block);
1147
+ if (nextValue === null) {
1148
+ return {
1149
+ ok: false,
1150
+ errorCode: "invalid-params",
1151
+ reason: schema === "markdown"
1152
+ ? "Markdown insert-rich-text-block requires a string block and an in-range block index"
1153
+ : "Rich text insert-rich-text-block requires a supported block payload and an in-range block index",
1154
+ };
1155
+ }
1156
+ break;
1157
+ case "remove-rich-text-block":
1158
+ nextValue =
1159
+ schema === "markdown"
1160
+ ? applyMarkdownBlockRemoval(currentValue, request.params.index)
1161
+ : applyRichTextBlockRemoval(schema, currentValue, request.params.index);
1162
+ if (nextValue === null) {
1163
+ return {
1164
+ ok: false,
1165
+ errorCode: "invalid-params",
1166
+ reason: "remove-rich-text-block requires an in-range block index",
1167
+ };
1168
+ }
1169
+ break;
1170
+ }
1171
+ return this.buildPlan(operationId, request, [this.buildAttributeRewriteStep(node, target, nextValue)], capability);
1172
+ }
1173
+ planRichTextBlockInsert(operationId, request, node, capability, anchorNode) {
1174
+ const contentModel = node.contentModel;
1175
+ if (!contentModel?.operationBoundary.includes("insert-rich-text-block") ||
1176
+ request.params.node.kind !== "native-tag" ||
1177
+ request.params.node.text === undefined) {
1178
+ return null;
1179
+ }
1180
+ const target = resolveAttributeTarget(node, contentModel.field);
1181
+ if (!target.attribute) {
1182
+ return null;
1183
+ }
1184
+ const currentValue = getCurrentAttributeValue(target.attribute);
1185
+ const schema = detectContentSchema(currentValue);
1186
+ if (!schema) {
1187
+ return null;
1188
+ }
1189
+ const block = schema === "markdown"
1190
+ ? request.params.node.text
1191
+ : { type: "paragraph", text: request.params.node.text };
1192
+ const blockCount = getRichTextBlockCount(schema, currentValue);
1193
+ const insertionIndex = getRichTextInsertionIndex(request.params.position, anchorNode, contentModel.field, blockCount);
1194
+ if (insertionIndex === null) {
1195
+ return null;
1196
+ }
1197
+ const nextValue = schema === "markdown"
1198
+ ? applyMarkdownBlockInsert(currentValue, insertionIndex, block)
1199
+ : applyRichTextBlockInsert(schema, currentValue, insertionIndex, block);
1200
+ if (nextValue === null) {
1201
+ return {
1202
+ ok: false,
1203
+ errorCode: "invalid-params",
1204
+ reason: "insert-child could not insert into the rich text field",
1205
+ };
1206
+ }
1207
+ return this.buildPlan(operationId, request, [this.buildAttributeRewriteStep(node, target, nextValue)], capability);
1208
+ }
1209
+ planRichTextBlockRemoval(operationId, request, node, capability) {
1210
+ const block = node.richTextBlock;
1211
+ if (!block) {
1212
+ return {
1213
+ ok: false,
1214
+ errorCode: "invalid-target",
1215
+ reason: "remove-node target is not a rich text block",
1216
+ };
1217
+ }
1218
+ const target = resolveAttributeTarget(node, block.field);
1219
+ if (!target.attribute) {
1220
+ return {
1221
+ ok: false,
1222
+ errorCode: "invalid-params",
1223
+ reason: `Rich text field '${block.field}' was not found on the target node`,
1224
+ };
1225
+ }
1226
+ const currentValue = getCurrentAttributeValue(target.attribute);
1227
+ const schema = detectContentSchema(currentValue);
1228
+ if (!schema) {
1229
+ return {
1230
+ ok: false,
1231
+ errorCode: "unsupported-operation",
1232
+ reason: `Rich text field '${block.field}' is not editable`,
1233
+ };
1234
+ }
1235
+ const nextValue = schema === "markdown"
1236
+ ? applyMarkdownBlockRemoval(currentValue, block.index)
1237
+ : applyRichTextBlockRemoval(schema, currentValue, block.index);
1238
+ if (nextValue === null) {
1239
+ return {
1240
+ ok: false,
1241
+ errorCode: "invalid-params",
1242
+ reason: "remove-node requires an in-range rich text block index",
1243
+ };
1244
+ }
1245
+ return this.buildPlan(operationId, request, [this.buildAttributeRewriteStep(node, target, nextValue)], capability);
1246
+ }
1247
+ planRichTextBlockMove(operationId, request, node, capability, resolved) {
1248
+ const block = node.richTextBlock;
1249
+ if (!block) {
1250
+ return {
1251
+ ok: false,
1252
+ errorCode: "invalid-target",
1253
+ reason: "move-node target is not a rich text block",
1254
+ };
1255
+ }
1256
+ const target = resolveAttributeTarget(node, block.field);
1257
+ if (!target.attribute) {
1258
+ return {
1259
+ ok: false,
1260
+ errorCode: "invalid-params",
1261
+ reason: `Rich text field '${block.field}' was not found on the target node`,
1262
+ };
1263
+ }
1264
+ const currentValue = getCurrentAttributeValue(target.attribute);
1265
+ const schema = detectContentSchema(currentValue);
1266
+ if (!schema) {
1267
+ return {
1268
+ ok: false,
1269
+ errorCode: "unsupported-operation",
1270
+ reason: `Rich text field '${block.field}' is not editable`,
1271
+ };
1272
+ }
1273
+ const toIndex = getRichTextMoveTargetIndex(request.params.target, resolved, block, getRichTextBlockCount(schema, currentValue));
1274
+ if (toIndex === null) {
1275
+ return {
1276
+ ok: false,
1277
+ errorCode: "unsupported-operation",
1278
+ reason: "move-node requires a target in the same rich text field",
1279
+ };
1280
+ }
1281
+ const nextValue = schema === "markdown"
1282
+ ? applyMarkdownBlockMove(currentValue, block.index, toIndex)
1283
+ : applyRichTextBlockMove(schema, currentValue, block.index, toIndex);
1284
+ if (nextValue === null) {
1285
+ return {
1286
+ ok: false,
1287
+ errorCode: "invalid-params",
1288
+ reason: "move-node requires in-range rich text block indexes",
1289
+ };
1290
+ }
1291
+ return this.buildPlan(operationId, request, [this.buildAttributeRewriteStep(node, target, nextValue)], capability);
1292
+ }
1293
+ planMediaFieldOperation(operationId, request, node) {
1294
+ const capability = this.graph.getCapability(node.key);
1295
+ if (!MEDIA_FIELD_NAMES.has(request.params.field)) {
1296
+ return {
1297
+ ok: false,
1298
+ errorCode: "unsupported-operation",
1299
+ reason: `Media field '${request.params.field}' is not supported`,
1300
+ };
1301
+ }
1302
+ const target = resolveAttributeTarget(node, request.params.field);
1303
+ return this.buildPlan(operationId, request, [this.buildAttributeRewriteStep(node, target, request.params.value)], capability);
1304
+ }
1305
+ buildAttributeRewriteStep(node, target, value) {
1306
+ if (target.valueLocator) {
1307
+ return {
1308
+ order: 0,
1309
+ file: node.file,
1310
+ type: "update-value",
1311
+ locator: target.valueLocator,
1312
+ value,
1313
+ };
1314
+ }
1315
+ return {
1316
+ order: 0,
1317
+ file: node.file,
1318
+ type: "set-jsx-prop",
1319
+ locator: target.attributeLocator,
1320
+ value,
1321
+ };
1322
+ }
1323
+ async planComponentSlotOperation(operationId, request, node) {
1324
+ const semantic = this.graph.getComponentSemantic(node.key);
1325
+ const slotKeys = semantic?.slotKeys ?? [];
1326
+ if (!slotKeys.includes(request.params.slotName)) {
1327
+ return {
1328
+ ok: false,
1329
+ errorCode: "unsupported-operation",
1330
+ reason: `Slot '${request.params.slotName}' is not declared for this component`,
1331
+ };
1332
+ }
1333
+ const capability = this.graph.getCapability(node.key);
1334
+ const segmentIndex = this.graph.getEditableTextSegmentIndex(node.key);
1335
+ const textTarget = segmentIndex === null
1336
+ ? null
1337
+ : await this.graph.getTextWriteTarget(node.key, segmentIndex);
1338
+ if (request.params.slotName === "children") {
1339
+ if (!capability.canUpdateText || segmentIndex === null) {
1340
+ return {
1341
+ ok: false,
1342
+ errorCode: "capability-denied",
1343
+ reason: "Children slot content requires an editable text segment",
1344
+ };
1345
+ }
1346
+ const textValue = this.toTextContent(request.params.value);
1347
+ if (textValue === null) {
1348
+ return {
1349
+ ok: false,
1350
+ errorCode: "unsupported-operation",
1351
+ reason: "Children slot content currently only supports primitive text values",
1352
+ };
1353
+ }
1354
+ const files = textTarget?.kind === "value-binding"
1355
+ ? [node.file, textTarget.file]
1356
+ : [node.file];
1357
+ const stale = this.checkDocumentVersion(request.documentVersion, files);
1358
+ if (stale) {
1359
+ return stale;
1360
+ }
1361
+ return this.buildPlan(operationId, request, this.buildTextRewriteSteps(node, segmentIndex, textValue, textTarget), capability);
1362
+ }
1363
+ if (semantic?.editableStrategy === "upstream" &&
1364
+ capability.canUpdateText &&
1365
+ segmentIndex !== null) {
1366
+ const textValue = this.toTextContent(request.params.value);
1367
+ if (textValue === null) {
1368
+ return {
1369
+ ok: false,
1370
+ errorCode: "unsupported-operation",
1371
+ reason: "Prop-backed slot content currently only supports primitive text values",
1372
+ };
1373
+ }
1374
+ const files = textTarget?.kind === "value-binding"
1375
+ ? [node.file, textTarget.file]
1376
+ : [node.file];
1377
+ const stale = this.checkDocumentVersion(request.documentVersion, files);
1378
+ if (stale) {
1379
+ return stale;
1380
+ }
1381
+ return this.buildPlan(operationId, request, this.buildTextRewriteSteps(node, segmentIndex, textValue, textTarget), capability);
1382
+ }
1383
+ const instanceNode = semantic?.instanceKey
1384
+ ? this.graph.getNode(semantic.instanceKey)
1385
+ : node;
1386
+ if (!instanceNode) {
1387
+ return {
1388
+ ok: false,
1389
+ errorCode: "locator-miss",
1390
+ reason: "Component instance for semantic slot edit was not found",
1391
+ };
1392
+ }
1393
+ const stale = this.checkDocumentVersion(request.documentVersion, [
1394
+ node.file,
1395
+ instanceNode.file,
1396
+ ]);
1397
+ if (stale) {
1398
+ return stale;
1399
+ }
1400
+ return this.buildPlan(operationId, request, [
1401
+ {
1402
+ order: 0,
1403
+ file: instanceNode.file,
1404
+ type: "set-jsx-prop",
1405
+ locator: makeAttributeLocator(instanceNode, request.params.slotName),
1406
+ value: request.params.value,
1407
+ },
1408
+ ], capability);
1409
+ }
1410
+ async planValueCollectionOperation(operationId, request, node) {
1411
+ const capability = this.graph.getCapability(node.key);
1412
+ if (!capability.canUpdateText) {
1413
+ return {
1414
+ ok: false,
1415
+ errorCode: "capability-denied",
1416
+ reason: "Cannot update bound value on this node",
1417
+ };
1418
+ }
1419
+ const valueTarget = await this.graph.getTextWriteTarget(node.key, request.params.segmentIndex);
1420
+ if (!valueTarget ||
1421
+ valueTarget.kind !== "value-binding" ||
1422
+ !valueTarget.locator) {
1423
+ return {
1424
+ ok: false,
1425
+ errorCode: "unsupported-operation",
1426
+ reason: "Node does not resolve to a writable value path",
1427
+ };
1428
+ }
1429
+ const locator = valueTarget.locator;
1430
+ switch (request.kind) {
1431
+ case "set-object-field": {
1432
+ const last = getLastPathSegment(locator);
1433
+ if (!last || last.kind !== "property") {
1434
+ return {
1435
+ ok: false,
1436
+ errorCode: "invalid-params",
1437
+ reason: "set-object-field requires an object-field value target",
1438
+ };
1439
+ }
1440
+ return this.buildPlan(operationId, request, [
1441
+ {
1442
+ order: 0,
1443
+ file: valueTarget.file,
1444
+ type: "set-object-field",
1445
+ locator,
1446
+ value: request.params.value,
1447
+ },
1448
+ ], capability);
1449
+ }
1450
+ case "insert-object-field": {
1451
+ const parentLocator = getParentObjectLocator(locator);
1452
+ if (!parentLocator) {
1453
+ return {
1454
+ ok: false,
1455
+ errorCode: "invalid-params",
1456
+ reason: "insert-object-field requires an object-field value target",
1457
+ };
1458
+ }
1459
+ return this.buildPlan(operationId, request, [
1460
+ {
1461
+ order: 0,
1462
+ file: valueTarget.file,
1463
+ type: "insert-object-field",
1464
+ locator: parentLocator,
1465
+ field: request.params.field,
1466
+ value: request.params.value,
1467
+ },
1468
+ ], capability);
1469
+ }
1470
+ case "remove-object-field": {
1471
+ const last = getLastPathSegment(locator);
1472
+ const field = request.params.field ??
1473
+ (last?.kind === "property" ? last.name : null);
1474
+ const parentLocator = getParentObjectLocator(locator);
1475
+ if (!field || !parentLocator) {
1476
+ return {
1477
+ ok: false,
1478
+ errorCode: "invalid-params",
1479
+ reason: "remove-object-field requires an object-field value target",
1480
+ };
1481
+ }
1482
+ return this.buildPlan(operationId, request, [
1483
+ {
1484
+ order: 0,
1485
+ file: valueTarget.file,
1486
+ type: "remove-object-field",
1487
+ locator: parentLocator,
1488
+ field,
1489
+ },
1490
+ ], capability);
1491
+ }
1492
+ case "update-array-item": {
1493
+ const collection = getNearestCollectionLocator(locator);
1494
+ if (!collection) {
1495
+ return {
1496
+ ok: false,
1497
+ errorCode: "invalid-params",
1498
+ reason: "update-array-item requires an array-backed value target",
1499
+ };
1500
+ }
1501
+ const itemLocator = request.params.index === undefined
1502
+ ? locator
1503
+ : valueTarget.collectionPathMap?.[request.params.index]
1504
+ ? cloneValueLocator(locator, valueTarget.collectionPathMap[request.params.index])
1505
+ : replaceNearestCollectionIndex(locator, mapCollectionItemIndex(request.params.index, valueTarget.collectionIndexMap));
1506
+ if (!itemLocator) {
1507
+ return {
1508
+ ok: false,
1509
+ errorCode: "invalid-params",
1510
+ reason: "update-array-item requires an array-backed value target",
1511
+ };
1512
+ }
1513
+ return this.buildPlan(operationId, request, [
1514
+ {
1515
+ order: 0,
1516
+ file: valueTarget.file,
1517
+ type: "update-array-item",
1518
+ locator: itemLocator,
1519
+ value: request.params.value,
1520
+ },
1521
+ ], capability);
1522
+ }
1523
+ case "insert-array-item": {
1524
+ const collection = getNearestCollectionLocator(locator);
1525
+ if (!collection) {
1526
+ return {
1527
+ ok: false,
1528
+ errorCode: "invalid-params",
1529
+ reason: "insert-array-item requires an array-backed value target",
1530
+ };
1531
+ }
1532
+ return this.buildPlan(operationId, request, [
1533
+ {
1534
+ order: 0,
1535
+ file: valueTarget.file,
1536
+ type: "insert-array-item",
1537
+ locator: collection.locator,
1538
+ index: mapCollectionInsertionIndex(request.params.index, valueTarget.collectionIndexMap),
1539
+ value: request.params.value,
1540
+ },
1541
+ ], capability);
1542
+ }
1543
+ case "remove-array-item": {
1544
+ const collection = getNearestCollectionLocator(locator);
1545
+ if (!collection) {
1546
+ return {
1547
+ ok: false,
1548
+ errorCode: "invalid-params",
1549
+ reason: "remove-array-item requires an array-backed value target",
1550
+ };
1551
+ }
1552
+ return this.buildPlan(operationId, request, [
1553
+ {
1554
+ order: 0,
1555
+ file: valueTarget.file,
1556
+ type: "remove-array-item",
1557
+ locator: collection.locator,
1558
+ index: request.params.index === undefined
1559
+ ? collection.index
1560
+ : mapCollectionItemIndex(request.params.index, valueTarget.collectionIndexMap),
1561
+ },
1562
+ ], capability);
1563
+ }
1564
+ case "move-array-item": {
1565
+ const collection = getNearestCollectionLocator(locator);
1566
+ if (!collection) {
1567
+ return {
1568
+ ok: false,
1569
+ errorCode: "invalid-params",
1570
+ reason: "move-array-item requires an array-backed value target",
1571
+ };
1572
+ }
1573
+ return this.buildPlan(operationId, request, [
1574
+ {
1575
+ order: 0,
1576
+ file: valueTarget.file,
1577
+ type: "move-array-item",
1578
+ locator: collection.locator,
1579
+ fromIndex: request.params.fromIndex === undefined
1580
+ ? collection.index
1581
+ : mapCollectionItemIndex(request.params.fromIndex, valueTarget.collectionIndexMap),
1582
+ toIndex: mapCollectionInsertionIndex(request.params.toIndex, valueTarget.collectionIndexMap),
1583
+ },
1584
+ ], capability);
1585
+ }
1586
+ }
1587
+ }
1588
+ resolveNodeRequest(request) {
1589
+ const targetKey = getNodeTargetKey(request);
1590
+ if (!targetKey) {
1591
+ return {
1592
+ ok: false,
1593
+ result: {
1594
+ ok: false,
1595
+ errorCode: "invalid-target",
1596
+ reason: `Operation '${request.kind}' requires a node target`,
1597
+ },
1598
+ };
1599
+ }
1600
+ const node = this.graph.getNode(targetKey);
1601
+ if (!node) {
1602
+ return {
1603
+ ok: false,
1604
+ result: {
1605
+ ok: false,
1606
+ errorCode: "locator-miss",
1607
+ reason: `Target key '${targetKey}' not found in graph`,
1608
+ },
1609
+ };
1610
+ }
1611
+ return { ok: true, node };
1612
+ }
1613
+ resolveRouteRequest(request) {
1614
+ const routeId = getRouteTargetId(request);
1615
+ if (!routeId) {
1616
+ return {
1617
+ ok: false,
1618
+ result: {
1619
+ ok: false,
1620
+ errorCode: "invalid-target",
1621
+ reason: `Operation '${request.kind}' requires a route target`,
1622
+ },
1623
+ };
1624
+ }
1625
+ const file = this.graph.getRouteEntryFile(routeId);
1626
+ if (!file) {
1627
+ return {
1628
+ ok: false,
1629
+ result: {
1630
+ ok: false,
1631
+ errorCode: "route-not-found",
1632
+ reason: `Route '${routeId}' is not registered`,
1633
+ },
1634
+ };
1635
+ }
1636
+ return { ok: true, file };
1637
+ }
1638
+ planNodeAttributeOperation(operationId, request, node) {
1639
+ const file = node.file;
1640
+ const capability = this.graph.getCapability(node.key);
1641
+ switch (request.kind) {
1642
+ case "set-jsx-prop":
1643
+ return this.buildPlan(operationId, request, [
1644
+ {
1645
+ order: 0,
1646
+ file,
1647
+ type: "set-jsx-prop",
1648
+ locator: makeAttributeLocator(node, request.params.name),
1649
+ value: request.params.value,
1650
+ },
1651
+ ], capability);
1652
+ case "remove-jsx-prop":
1653
+ return this.buildPlan(operationId, request, [
1654
+ {
1655
+ order: 0,
1656
+ file,
1657
+ type: "remove-jsx-prop",
1658
+ locator: makeAttributeLocator(node, request.params.name),
1659
+ },
1660
+ ], capability);
1661
+ case "set-class-name":
1662
+ return this.buildPlan(operationId, request, [
1663
+ {
1664
+ order: 0,
1665
+ file,
1666
+ type: "set-class-name",
1667
+ locator: makeAttributeLocator(node, "className"),
1668
+ value: request.params.value,
1669
+ },
1670
+ ], capability);
1671
+ case "add-class-token":
1672
+ return this.buildPlan(operationId, request, [
1673
+ {
1674
+ order: 0,
1675
+ file,
1676
+ type: "add-class-token",
1677
+ locator: makeAttributeLocator(node, "className"),
1678
+ token: request.params.token,
1679
+ },
1680
+ ], capability);
1681
+ case "remove-class-token":
1682
+ return this.buildPlan(operationId, request, [
1683
+ {
1684
+ order: 0,
1685
+ file,
1686
+ type: "remove-class-token",
1687
+ locator: makeAttributeLocator(node, "className"),
1688
+ token: request.params.token,
1689
+ },
1690
+ ], capability);
1691
+ case "set-style-property":
1692
+ return this.planStyleClassNameOperation(operationId, request, node, {
1693
+ [request.params.property]: request.params.value,
1694
+ });
1695
+ case "set-style-properties":
1696
+ return this.planStyleClassNameOperation(operationId, request, node, request.params.properties);
1697
+ case "set-css-module-class":
1698
+ return this.buildPlan(operationId, request, [
1699
+ {
1700
+ order: 0,
1701
+ file,
1702
+ type: "set-css-module-class",
1703
+ locator: makeAttributeLocator(node, "className"),
1704
+ from: request.params.from,
1705
+ to: request.params.to,
1706
+ },
1707
+ ], capability);
1708
+ }
1709
+ }
1710
+ planStyleClassNameOperation(operationId, request, node, properties) {
1711
+ const resolvedTarget = resolveEditableStyleClassTarget(node);
1712
+ if (!resolvedTarget.ok) {
1713
+ return resolvedTarget.result;
1714
+ }
1715
+ let nextClassName = resolvedTarget.value;
1716
+ for (const [property, value] of Object.entries(properties)) {
1717
+ const updated = updateStylePropertyInClassName(nextClassName, property, value);
1718
+ if (updated === null) {
1719
+ return {
1720
+ ok: false,
1721
+ errorCode: "invalid-params",
1722
+ reason: `CSS style property '${property}' received a value that cannot be mapped to Tailwind`,
1723
+ };
1724
+ }
1725
+ nextClassName = updated;
1726
+ }
1727
+ return this.buildPlan(operationId, request, [
1728
+ {
1729
+ order: 0,
1730
+ file: node.file,
1731
+ type: "set-class-name",
1732
+ locator: makeAttributeLocator(node, "className"),
1733
+ value: nextClassName,
1734
+ },
1735
+ ], this.graph.getCapability(node.key));
1736
+ }
1737
+ planRouteOperation(operationId, request, file) {
1738
+ const source = this.graph.getFileContent(file);
1739
+ switch (request.kind) {
1740
+ case "set-directive": {
1741
+ const currentValue = findKnownDirective(source);
1742
+ return this.buildPlan(operationId, request, [
1743
+ {
1744
+ order: 0,
1745
+ file,
1746
+ type: "set-directive",
1747
+ mode: currentValue ? "update" : "add",
1748
+ currentValue: currentValue ?? undefined,
1749
+ value: request.params.value,
1750
+ },
1751
+ ]);
1752
+ }
1753
+ case "remove-directive":
1754
+ return this.buildPlan(operationId, request, [
1755
+ {
1756
+ order: 0,
1757
+ file,
1758
+ type: "remove-directive",
1759
+ value: request.params.value,
1760
+ },
1761
+ ]);
1762
+ case "set-route-export":
1763
+ return this.buildPlan(operationId, request, [
1764
+ {
1765
+ order: 0,
1766
+ file,
1767
+ type: "set-route-export",
1768
+ name: request.params.name,
1769
+ value: request.params.value,
1770
+ },
1771
+ ]);
1772
+ case "set-metadata-field":
1773
+ return this.buildPlan(operationId, request, [
1774
+ {
1775
+ order: 0,
1776
+ file,
1777
+ type: "set-metadata-field",
1778
+ field: request.params.field,
1779
+ value: request.params.value,
1780
+ },
1781
+ ]);
1782
+ case "set-generate-metadata":
1783
+ return this.buildPlan(operationId, request, [
1784
+ {
1785
+ order: 0,
1786
+ file,
1787
+ type: "set-generate-metadata",
1788
+ field: request.params.field,
1789
+ value: request.params.value,
1790
+ },
1791
+ ]);
1792
+ }
1793
+ }
1794
+ buildTextRewriteSteps(node, segmentIndex, value, textTarget) {
1795
+ return textTarget?.kind === "value-binding" && textTarget.locator
1796
+ ? [
1797
+ {
1798
+ order: 0,
1799
+ file: textTarget.file,
1800
+ type: "update-value",
1801
+ locator: textTarget.locator,
1802
+ value,
1803
+ },
1804
+ ]
1805
+ : textTarget?.kind === "external-entry"
1806
+ ? [
1807
+ {
1808
+ order: 0,
1809
+ file: `external:${textTarget.sourceId}`,
1810
+ type: "invoke-external-adapter",
1811
+ sourceType: textTarget.sourceType,
1812
+ sourceId: textTarget.sourceId,
1813
+ adapterId: textTarget.adapterId,
1814
+ capability: "write-text",
1815
+ value,
1816
+ },
1817
+ ]
1818
+ : [
1819
+ {
1820
+ order: 0,
1821
+ file: node.file,
1822
+ type: "update-text",
1823
+ locator: makeLocator(node),
1824
+ segmentIndex,
1825
+ newText: value,
1826
+ },
1827
+ ];
1828
+ }
1829
+ checkDocumentVersion(documentVersion, files) {
1830
+ for (const file of new Set(files)) {
1831
+ const currentVersion = this.graph.getFileVersion(file);
1832
+ if (documentVersion < currentVersion) {
1833
+ return {
1834
+ ok: false,
1835
+ errorCode: "stale-version",
1836
+ reason: `Document version ${documentVersion} is stale (current: ${currentVersion})`,
1837
+ };
1838
+ }
1839
+ }
1840
+ return null;
1841
+ }
1842
+ toTextContent(value) {
1843
+ if (typeof value === "string") {
1844
+ return value;
1845
+ }
1846
+ if (typeof value === "number" || typeof value === "boolean") {
1847
+ return String(value);
1848
+ }
1849
+ return null;
1850
+ }
1851
+ /* ---------------------------------------------------------------- */
1852
+ /* update-text */
1853
+ /* ---------------------------------------------------------------- */
1854
+ planUpdateText(operationId, request, node, capability, textTarget) {
1855
+ if (!capability.canUpdateText) {
1856
+ return {
1857
+ ok: false,
1858
+ errorCode: "capability-denied",
1859
+ reason: "Cannot update text on this node",
1860
+ };
1861
+ }
1862
+ return this.buildPlan(operationId, request, this.buildTextRewriteSteps(node, request.params.segmentIndex, request.params.value, textTarget), capability);
1863
+ }
1864
+ /* ---------------------------------------------------------------- */
1865
+ /* remove-node */
1866
+ /* ---------------------------------------------------------------- */
1867
+ async planRemoveNode(operationId, request, node, capability) {
1868
+ if (!capability.canRemove) {
1869
+ return {
1870
+ ok: false,
1871
+ errorCode: "capability-denied",
1872
+ reason: "Cannot remove this node",
1873
+ };
1874
+ }
1875
+ if (node.richTextBlock) {
1876
+ return this.planRichTextBlockRemoval(operationId, request, node, capability);
1877
+ }
1878
+ const primaryTextTarget = await this.graph.getTextWriteTarget(node.key, 0);
1879
+ if (primaryTextTarget?.kind === "external-entry") {
1880
+ return this.buildPlan(operationId, request, [
1881
+ {
1882
+ order: 0,
1883
+ file: `external:${primaryTextTarget.sourceId}`,
1884
+ type: "invoke-external-adapter",
1885
+ sourceType: primaryTextTarget.sourceType,
1886
+ sourceId: primaryTextTarget.sourceId,
1887
+ adapterId: primaryTextTarget.adapterId,
1888
+ capability: "remove-node",
1889
+ value: { operation: "remove-node" },
1890
+ },
1891
+ ], capability);
1892
+ }
1893
+ if (node.isRepeatItemRoot) {
1894
+ const valueTarget = primaryTextTarget;
1895
+ if (valueTarget?.kind === "value-binding" && valueTarget.locator) {
1896
+ const collection = getNearestCollectionLocator(valueTarget.locator);
1897
+ if (collection) {
1898
+ return this.buildPlan(operationId, request, [
1899
+ {
1900
+ order: 0,
1901
+ file: valueTarget.file,
1902
+ type: "remove-array-item",
1903
+ locator: collection.locator,
1904
+ index: collection.index,
1905
+ },
1906
+ ], capability);
1907
+ }
1908
+ }
1909
+ return {
1910
+ ok: false,
1911
+ errorCode: "unsupported-operation",
1912
+ reason: "Repeat-backed node removal requires a writable array target",
1913
+ };
1914
+ }
1915
+ const upstreamReason = getUpstreamNodeEditReason(node, primaryTextTarget, this.graph.getTextProvenance?.(node.key, 0) ?? null);
1916
+ if (upstreamReason) {
1917
+ return {
1918
+ ok: false,
1919
+ errorCode: "unsupported-operation",
1920
+ reason: upstreamReason,
1921
+ };
1922
+ }
1923
+ const repeatTemplateReason = this.getRepeatBackedTemplateStructureReason(node);
1924
+ if (repeatTemplateReason) {
1925
+ return {
1926
+ ok: false,
1927
+ errorCode: "unsupported-operation",
1928
+ reason: repeatTemplateReason,
1929
+ };
1930
+ }
1931
+ const upstreamDescendantReason = await this.getUpstreamBackedDescendantReason(node);
1932
+ if (upstreamDescendantReason) {
1933
+ return {
1934
+ ok: false,
1935
+ errorCode: "unsupported-operation",
1936
+ reason: upstreamDescendantReason,
1937
+ };
1938
+ }
1939
+ const steps = [
1940
+ {
1941
+ order: 0,
1942
+ file: node.file,
1943
+ type: "remove-node",
1944
+ locator: makeLocator(node),
1945
+ removeWhitespace: true,
1946
+ },
1947
+ ];
1948
+ const removeImportStep = this.buildRemoveComponentImportStep(node);
1949
+ if (removeImportStep) {
1950
+ steps.push({ ...removeImportStep, order: steps.length });
1951
+ }
1952
+ return this.buildPlan(operationId, request, steps, capability);
1953
+ }
1954
+ buildRemoveComponentImportStep(node) {
1955
+ if (!node.importedFrom || !node.exportName || !node.tag) {
1956
+ return null;
1957
+ }
1958
+ if (countJsxTagReferences(this.graph.getFileContent(node.file), node.tag) > 1) {
1959
+ return null;
1960
+ }
1961
+ const moduleSpecifier = computeRelativeSpecifier(node.file, node.importedFrom);
1962
+ return {
1963
+ order: 0,
1964
+ file: node.file,
1965
+ type: "remove-import",
1966
+ moduleSpecifier,
1967
+ imports: node.exportName === "default"
1968
+ ? [{ kind: "default" }]
1969
+ : [{ kind: "named", imported: node.exportName }],
1970
+ };
1971
+ }
1972
+ /* ---------------------------------------------------------------- */
1973
+ /* insert-child */
1974
+ /* ---------------------------------------------------------------- */
1975
+ async planInsertChild(operationId, request, node, capability, anchorNode) {
1976
+ const richTextInsertPlan = this.planRichTextBlockInsert(operationId, request, node, capability, anchorNode);
1977
+ if (richTextInsertPlan) {
1978
+ return richTextInsertPlan;
1979
+ }
1980
+ if (!capability.canInsertChild) {
1981
+ return {
1982
+ ok: false,
1983
+ errorCode: "capability-denied",
1984
+ reason: "Cannot insert child into this node",
1985
+ };
1986
+ }
1987
+ const parentTextSegmentIndex = this.graph.getEditableTextSegmentIndex(node.key);
1988
+ const parentTextTarget = parentTextSegmentIndex !== null
1989
+ ? await this.graph.getTextWriteTarget(node.key, parentTextSegmentIndex)
1990
+ : null;
1991
+ const upstreamReason = getUpstreamNodeEditReason(node, parentTextTarget, parentTextSegmentIndex !== null
1992
+ ? (this.graph.getTextProvenance?.(node.key, parentTextSegmentIndex) ??
1993
+ null)
1994
+ : null);
1995
+ if (upstreamReason) {
1996
+ return {
1997
+ ok: false,
1998
+ errorCode: "unsupported-operation",
1999
+ reason: upstreamReason,
2000
+ };
2001
+ }
2002
+ const upstreamDescendantReason = await this.getUpstreamBackedDescendantReason(node);
2003
+ if (upstreamDescendantReason) {
2004
+ return {
2005
+ ok: false,
2006
+ errorCode: "unsupported-operation",
2007
+ reason: upstreamDescendantReason,
2008
+ };
2009
+ }
2010
+ const externalInsertPlan = await this.planExternalBackedInsertChild(operationId, request, node, capability, anchorNode, parentTextTarget);
2011
+ if (externalInsertPlan) {
2012
+ return externalInsertPlan;
2013
+ }
2014
+ const repeatInsertPlan = await this.planRepeatBackedInsertChild(operationId, request, node, capability);
2015
+ if (repeatInsertPlan) {
2016
+ return repeatInsertPlan;
2017
+ }
2018
+ const repeatTemplateReason = this.getRepeatBackedTemplateStructureReason(node);
2019
+ if (repeatTemplateReason) {
2020
+ return {
2021
+ ok: false,
2022
+ errorCode: "unsupported-operation",
2023
+ reason: repeatTemplateReason,
2024
+ };
2025
+ }
2026
+ const insertPolicy = evaluateInsertChildPolicy(this.graph.getFileContent(node.file), node);
2027
+ if (!insertPolicy.allowed) {
2028
+ return {
2029
+ ok: false,
2030
+ errorCode: "unsupported-operation",
2031
+ reason: insertPolicy.reason ?? "Cannot insert child into this node",
2032
+ };
2033
+ }
2034
+ const invalidNestingReason = validateNativeInsertNesting(node, request.params.node);
2035
+ if (invalidNestingReason) {
2036
+ return {
2037
+ ok: false,
2038
+ errorCode: "unsupported-operation",
2039
+ reason: invalidNestingReason,
2040
+ };
2041
+ }
2042
+ const steps = [];
2043
+ let order = 0;
2044
+ const plannedComponentInsert = request.params.node.kind === "component-call"
2045
+ ? planComponentInsert(request.params.node, this.graph.getFileContent(node.file))
2046
+ : undefined;
2047
+ const componentImport = plannedComponentInsert?.importStep;
2048
+ if (componentImport) {
2049
+ steps.push({
2050
+ order: order++,
2051
+ file: node.file,
2052
+ type: "add-import",
2053
+ moduleSpecifier: componentImport.moduleSpecifier,
2054
+ imports: componentImport.imports,
2055
+ });
2056
+ }
2057
+ steps.push({
2058
+ order: order++,
2059
+ file: node.file,
2060
+ type: "insert-child",
2061
+ parentLocator: makeLocator(node),
2062
+ node: {
2063
+ jsx: buildJsxFromSpec(request.params.node, plannedComponentInsert?.localName),
2064
+ },
2065
+ placement: request.params.position.relation,
2066
+ anchorLocator: anchorNode ? makeLocator(anchorNode) : undefined,
2067
+ });
2068
+ return this.buildPlan(operationId, request, steps, capability);
2069
+ }
2070
+ async planExternalBackedInsertChild(operationId, request, node, capability, anchorNode, parentTextTarget) {
2071
+ const externalTarget = parentTextTarget?.kind === "external-entry"
2072
+ ? parentTextTarget
2073
+ : await this.findExternalInsertContext(node, anchorNode);
2074
+ if (!externalTarget) {
2075
+ return null;
2076
+ }
2077
+ const anchorTarget = anchorNode
2078
+ ? await this.graph.getTextWriteTarget(anchorNode.key, 0)
2079
+ : null;
2080
+ return this.buildPlan(operationId, request, [
2081
+ {
2082
+ order: 0,
2083
+ file: `external:${externalTarget.sourceId}`,
2084
+ type: "invoke-external-adapter",
2085
+ sourceType: externalTarget.sourceType,
2086
+ sourceId: externalTarget.sourceId,
2087
+ adapterId: externalTarget.adapterId,
2088
+ capability: "insert-child",
2089
+ value: {
2090
+ operation: "insert-child",
2091
+ relation: request.params.position.relation,
2092
+ parentKey: node.key,
2093
+ anchorSourceId: anchorTarget?.kind === "external-entry"
2094
+ ? anchorTarget.sourceId
2095
+ : null,
2096
+ node: insertNodeSpecToOperationValue(request.params.node),
2097
+ },
2098
+ },
2099
+ ], capability);
2100
+ }
2101
+ async findExternalInsertContext(node, anchorNode) {
2102
+ if (anchorNode) {
2103
+ const anchorTarget = await this.graph.getTextWriteTarget(anchorNode.key, 0);
2104
+ if (anchorTarget?.kind === "external-entry") {
2105
+ return anchorTarget;
2106
+ }
2107
+ }
2108
+ if (!this.graph.getChildNodes) {
2109
+ return null;
2110
+ }
2111
+ const stack = [...this.graph.getChildNodes(node.key)];
2112
+ while (stack.length > 0) {
2113
+ const child = stack.shift();
2114
+ if (!child) {
2115
+ continue;
2116
+ }
2117
+ const target = await this.graph.getTextWriteTarget(child.key, 0);
2118
+ if (target?.kind === "external-entry") {
2119
+ return target;
2120
+ }
2121
+ const externalSource = this.graph.getTextProvenance?.(child.key, 0)
2122
+ ?.finalSource?.externalSource;
2123
+ if (externalSource) {
2124
+ return {
2125
+ kind: "external-entry",
2126
+ file: `external:${externalSource.sourceId}`,
2127
+ sourceType: externalSource.sourceType,
2128
+ sourceId: externalSource.sourceId,
2129
+ adapterId: externalSource.adapterId,
2130
+ displayExpression: externalSource.sourceId,
2131
+ };
2132
+ }
2133
+ stack.push(...this.graph.getChildNodes(child.key));
2134
+ }
2135
+ return null;
2136
+ }
2137
+ async planRepeatBackedInsertChild(operationId, request, node, capability) {
2138
+ if (request.params.position.relation !== "append" &&
2139
+ request.params.position.relation !== "prepend") {
2140
+ return null;
2141
+ }
2142
+ const insertNode = request.params.node;
2143
+ if (insertNode.kind !== "native-tag" ||
2144
+ insertNode.text === undefined ||
2145
+ !this.graph.getChildNodes) {
2146
+ return null;
2147
+ }
2148
+ const repeatChild = this.graph
2149
+ .getChildNodes(node.key)
2150
+ .find((child) => child.isRepeatItemRoot &&
2151
+ child.tag?.toLowerCase() === insertNode.tag.toLowerCase());
2152
+ if (!repeatChild) {
2153
+ return null;
2154
+ }
2155
+ const valueTarget = await this.graph.getTextWriteTarget(repeatChild.key, 0);
2156
+ if (!valueTarget ||
2157
+ valueTarget.kind !== "value-binding" ||
2158
+ !valueTarget.locator) {
2159
+ return {
2160
+ ok: false,
2161
+ errorCode: "unsupported-operation",
2162
+ reason: "Repeat-backed insert-child requires a writable value target",
2163
+ };
2164
+ }
2165
+ const collection = getNearestCollectionLocator(valueTarget.locator);
2166
+ if (!collection) {
2167
+ return {
2168
+ ok: false,
2169
+ errorCode: "unsupported-operation",
2170
+ reason: "Repeat-backed insert-child requires an array-backed target",
2171
+ };
2172
+ }
2173
+ const value = buildRepeatInsertValueFromLocator(valueTarget.locator, insertNode.text);
2174
+ if (value === undefined) {
2175
+ return {
2176
+ ok: false,
2177
+ errorCode: "unsupported-operation",
2178
+ reason: "Repeat-backed insert-child could not infer an item shape from the template",
2179
+ };
2180
+ }
2181
+ const insertionIndex = request.params.position.relation === "prepend"
2182
+ ? mapCollectionInsertionIndex(0, valueTarget.collectionIndexMap)
2183
+ : mapCollectionInsertionIndex(valueTarget.collectionIndexMap?.length ?? collection.index + 1, valueTarget.collectionIndexMap);
2184
+ return this.buildPlan(operationId, request, [
2185
+ {
2186
+ order: 0,
2187
+ file: valueTarget.file,
2188
+ type: "insert-array-item",
2189
+ locator: collection.locator,
2190
+ index: insertionIndex,
2191
+ value,
2192
+ },
2193
+ ], capability);
2194
+ }
2195
+ async getUpstreamBackedDescendantReason(node) {
2196
+ if (!this.graph.getChildNodes) {
2197
+ return null;
2198
+ }
2199
+ const stack = [...this.graph.getChildNodes(node.key)];
2200
+ while (stack.length > 0) {
2201
+ const child = stack.shift();
2202
+ if (!child) {
2203
+ continue;
2204
+ }
2205
+ const segmentIndex = this.graph.getEditableTextSegmentIndex(child.key);
2206
+ const textTarget = segmentIndex !== null
2207
+ ? await this.graph.getTextWriteTarget(child.key, segmentIndex)
2208
+ : null;
2209
+ const reason = getUpstreamNodeEditReason(child, textTarget, segmentIndex !== null
2210
+ ? (this.graph.getTextProvenance?.(child.key, segmentIndex) ?? null)
2211
+ : null);
2212
+ if (reason) {
2213
+ return reason;
2214
+ }
2215
+ stack.push(...this.graph.getChildNodes(child.key));
2216
+ }
2217
+ return null;
2218
+ }
2219
+ getRepeatBackedTemplateStructureReason(node) {
2220
+ if (node.isRepeatItemRoot) {
2221
+ return "Cannot edit repeat-backed item template structure with a generic node operation";
2222
+ }
2223
+ if (node.structuralPath.includes("#repeat:")) {
2224
+ return "Cannot edit repeat-backed child node structure with a generic node operation";
2225
+ }
2226
+ if (this.isInsideMapCallback(node)) {
2227
+ return "Cannot edit repeat-backed child node structure with a generic node operation";
2228
+ }
2229
+ let parentKey = node.parentKey;
2230
+ while (parentKey) {
2231
+ const parent = this.graph.getNode(parentKey);
2232
+ if (!parent) {
2233
+ return null;
2234
+ }
2235
+ if (parent.isRepeatItemRoot || parent.isRepeatRegion) {
2236
+ return "Cannot edit repeat-backed child node structure with a generic node operation";
2237
+ }
2238
+ parentKey = parent.parentKey;
2239
+ }
2240
+ return null;
2241
+ }
2242
+ isInsideMapCallback(node) {
2243
+ const ast = parseModuleSource(this.graph.getFileContent(node.file));
2244
+ if (!ast) {
2245
+ return false;
2246
+ }
2247
+ const match = resolveLocatorPath(ast, makeLocator(node));
2248
+ const nodePath = match.ok && match.match.path.isJSXElement()
2249
+ ? match.match.path
2250
+ : findJsxElementBySourceRange(ast, node);
2251
+ if (!nodePath) {
2252
+ return false;
2253
+ }
2254
+ return Boolean(nodePath.findParent((path) => {
2255
+ if (!path.isCallExpression()) {
2256
+ return false;
2257
+ }
2258
+ const callee = path.node.callee;
2259
+ return (t.isMemberExpression(callee) &&
2260
+ t.isIdentifier(callee.property) &&
2261
+ callee.property.name === "map");
2262
+ }));
2263
+ }
2264
+ /* ---------------------------------------------------------------- */
2265
+ /* move-node */
2266
+ /* ---------------------------------------------------------------- */
2267
+ async planMoveNode(operationId, request, node, capability) {
2268
+ if (!capability.canMove) {
2269
+ return {
2270
+ ok: false,
2271
+ errorCode: "capability-denied",
2272
+ reason: "Cannot move this node",
2273
+ };
2274
+ }
2275
+ const resolved = resolveTargetRelation(request.params.target, this.graph);
2276
+ const targetParent = resolved.parentNode;
2277
+ if (!targetParent) {
2278
+ return {
2279
+ ok: false,
2280
+ errorCode: "locator-miss",
2281
+ reason: `Target parent for request '${request.id}' not found in graph`,
2282
+ };
2283
+ }
2284
+ if (node.richTextBlock) {
2285
+ return this.planRichTextBlockMove(operationId, request, node, capability, resolved);
2286
+ }
2287
+ if (isSameOrDescendant(targetParent.key, node.key, this.graph)) {
2288
+ return {
2289
+ ok: false,
2290
+ errorCode: "capability-denied",
2291
+ reason: "Cannot move node into its own subtree",
2292
+ };
2293
+ }
2294
+ if (node.isRepeatRegion && targetParent.isBoundary) {
2295
+ return {
2296
+ ok: false,
2297
+ errorCode: "capability-denied",
2298
+ reason: "Cannot move repeat-region node across boundary",
2299
+ };
2300
+ }
2301
+ if (!this.graph.getCapability(targetParent.key).canInsertChild) {
2302
+ return {
2303
+ ok: false,
2304
+ errorCode: "capability-denied",
2305
+ reason: "Cannot move node into this parent",
2306
+ };
2307
+ }
2308
+ const primaryTextTarget = await this.graph.getTextWriteTarget(node.key, 0);
2309
+ if (primaryTextTarget?.kind === "external-entry") {
2310
+ const placement = request.params.target.relation;
2311
+ const anchorTarget = resolved.anchorNode && (placement === "before" || placement === "after")
2312
+ ? await this.graph.getTextWriteTarget(resolved.anchorNode.key, 0)
2313
+ : null;
2314
+ return this.buildPlan(operationId, request, [
2315
+ {
2316
+ order: 0,
2317
+ file: `external:${primaryTextTarget.sourceId}`,
2318
+ type: "invoke-external-adapter",
2319
+ sourceType: primaryTextTarget.sourceType,
2320
+ sourceId: primaryTextTarget.sourceId,
2321
+ adapterId: primaryTextTarget.adapterId,
2322
+ capability: "move-node",
2323
+ value: {
2324
+ operation: "move-node",
2325
+ relation: placement,
2326
+ anchorSourceId: anchorTarget?.kind === "external-entry"
2327
+ ? anchorTarget.sourceId
2328
+ : null,
2329
+ parentKey: "parentKey" in request.params.target
2330
+ ? request.params.target.parentKey
2331
+ : null,
2332
+ },
2333
+ },
2334
+ ], capability);
2335
+ }
2336
+ const upstreamReason = getUpstreamNodeEditReason(node, primaryTextTarget, this.graph.getTextProvenance?.(node.key, 0) ?? null);
2337
+ if (upstreamReason) {
2338
+ return {
2339
+ ok: false,
2340
+ errorCode: "unsupported-operation",
2341
+ reason: upstreamReason,
2342
+ };
2343
+ }
2344
+ const isCrossFile = node.file !== targetParent.file;
2345
+ const placement = request.params.target.relation;
2346
+ const anchorNode = resolved.anchorNode;
2347
+ if (node.isRepeatItemRoot &&
2348
+ anchorNode?.isRepeatItemRoot &&
2349
+ node.repeatOwnerKey &&
2350
+ node.repeatOwnerKey === anchorNode.repeatOwnerKey &&
2351
+ (placement === "before" || placement === "after")) {
2352
+ const sourceTarget = primaryTextTarget;
2353
+ const anchorTarget = await this.graph.getTextWriteTarget(anchorNode.key, 0);
2354
+ if (sourceTarget?.kind === "value-binding" &&
2355
+ anchorTarget?.kind === "value-binding" &&
2356
+ sourceTarget.locator &&
2357
+ anchorTarget.locator) {
2358
+ const sourceCollection = getNearestCollectionLocator(sourceTarget.locator);
2359
+ const anchorCollection = getNearestCollectionLocator(anchorTarget.locator);
2360
+ if (sourceCollection &&
2361
+ anchorCollection &&
2362
+ sourceTarget.file === anchorTarget.file &&
2363
+ sameValueLocator(sourceCollection.locator, anchorCollection.locator)) {
2364
+ const fromIndex = sourceCollection.index;
2365
+ const anchorIndex = anchorCollection.index;
2366
+ const toIndex = placement === "before"
2367
+ ? anchorIndex - (fromIndex < anchorIndex ? 1 : 0)
2368
+ : anchorIndex + (fromIndex < anchorIndex ? 0 : 1);
2369
+ return this.buildPlan(operationId, request, [
2370
+ {
2371
+ order: 0,
2372
+ file: sourceTarget.file,
2373
+ type: "move-array-item",
2374
+ locator: sourceCollection.locator,
2375
+ fromIndex,
2376
+ toIndex,
2377
+ },
2378
+ ], capability);
2379
+ }
2380
+ }
2381
+ }
2382
+ if (node.isRepeatItemRoot) {
2383
+ return {
2384
+ ok: false,
2385
+ errorCode: "unsupported-operation",
2386
+ reason: "Cannot move repeat-backed item outside its collection",
2387
+ };
2388
+ }
2389
+ const repeatTemplateReason = this.getRepeatBackedTemplateStructureReason(node);
2390
+ if (repeatTemplateReason) {
2391
+ return {
2392
+ ok: false,
2393
+ errorCode: "unsupported-operation",
2394
+ reason: repeatTemplateReason,
2395
+ };
2396
+ }
2397
+ const upstreamDescendantReason = await this.getUpstreamBackedDescendantReason(node);
2398
+ if (upstreamDescendantReason) {
2399
+ return {
2400
+ ok: false,
2401
+ errorCode: "unsupported-operation",
2402
+ reason: upstreamDescendantReason,
2403
+ };
2404
+ }
2405
+ if (isCrossFile) {
2406
+ return this.planCrossFileMove(operationId, request, node, targetParent, capability, placement, anchorNode);
2407
+ }
2408
+ const scopeCheck = checkSameFileMovePreservesLexicalBindings(this.graph.getFileContent(node.file), node, targetParent);
2409
+ if (!scopeCheck.ok) {
2410
+ return {
2411
+ ok: false,
2412
+ errorCode: "unsupported-operation",
2413
+ reason: scopeCheck.reason ??
2414
+ `Cannot move node because it depends on bindings outside the target scope: ${scopeCheck.bindingNames.join(", ")}`,
2415
+ };
2416
+ }
2417
+ const steps = [
2418
+ {
2419
+ order: 0,
2420
+ file: node.file,
2421
+ type: "move-node",
2422
+ locator: makeLocator(node),
2423
+ targetParentLocator: makeLocator(targetParent),
2424
+ placement,
2425
+ anchorLocator: anchorNode ? makeLocator(anchorNode) : undefined,
2426
+ },
2427
+ ];
2428
+ return this.buildPlan(operationId, request, steps, capability);
2429
+ }
2430
+ /* ---------------------------------------------------------------- */
2431
+ /* cross-file move 展开 */
2432
+ /* ---------------------------------------------------------------- */
2433
+ planCrossFileMove(operationId, request, sourceNode, targetParent, capability, placement, anchorNode) {
2434
+ const steps = [];
2435
+ let order = 0;
2436
+ const sourceContent = this.graph.getFileContent(sourceNode.file);
2437
+ const sourceJsx = extractNodeJsx(this.graph, sourceNode) ?? `<${sourceNode.component} />`;
2438
+ const movePolicy = evaluateCrossFileMovePolicy(sourceContent, sourceNode);
2439
+ if (!movePolicy.allowed) {
2440
+ return {
2441
+ ok: false,
2442
+ errorCode: "unsupported-operation",
2443
+ reason: movePolicy.reason ?? "Cannot move subtree across files",
2444
+ };
2445
+ }
2446
+ steps.push({
2447
+ order: order++,
2448
+ file: sourceNode.file,
2449
+ type: "remove-node",
2450
+ locator: makeLocator(sourceNode),
2451
+ removeWhitespace: true,
2452
+ });
2453
+ if (sourceNode.importedFrom && sourceNode.exportName) {
2454
+ const localComponentName = sourceNode.tag ?? sourceNode.component;
2455
+ const targetModuleSpecifier = computeRelativeSpecifier(targetParent.file, sourceNode.importedFrom);
2456
+ const sourceModuleSpecifier = computeRelativeSpecifier(sourceNode.file, sourceNode.importedFrom);
2457
+ const componentImport = {
2458
+ moduleSpecifier: targetModuleSpecifier,
2459
+ imports: sourceNode.exportName === "default"
2460
+ ? [{ kind: "default", local: localComponentName }]
2461
+ : [
2462
+ {
2463
+ kind: "named",
2464
+ imported: sourceNode.exportName,
2465
+ local: localComponentName === sourceNode.exportName
2466
+ ? undefined
2467
+ : localComponentName,
2468
+ },
2469
+ ],
2470
+ };
2471
+ steps.push({
2472
+ order: order++,
2473
+ file: targetParent.file,
2474
+ type: "add-import",
2475
+ moduleSpecifier: componentImport.moduleSpecifier,
2476
+ imports: componentImport.imports,
2477
+ });
2478
+ steps.push({
2479
+ order: order++,
2480
+ file: targetParent.file,
2481
+ type: "insert-child",
2482
+ parentLocator: makeLocator(targetParent),
2483
+ node: { jsx: sourceJsx },
2484
+ placement,
2485
+ anchorLocator: anchorNode ? makeLocator(anchorNode) : undefined,
2486
+ });
2487
+ steps.push({
2488
+ order: order++,
2489
+ file: sourceNode.file,
2490
+ type: "remove-import",
2491
+ moduleSpecifier: sourceModuleSpecifier,
2492
+ imports: componentImport.imports.map((item) => item.kind === "default"
2493
+ ? { kind: "default" }
2494
+ : { kind: "named", imported: item.imported }),
2495
+ });
2496
+ }
2497
+ else {
2498
+ steps.push({
2499
+ order: order++,
2500
+ file: targetParent.file,
2501
+ type: "insert-child",
2502
+ parentLocator: makeLocator(targetParent),
2503
+ node: { jsx: sourceJsx },
2504
+ placement,
2505
+ anchorLocator: anchorNode ? makeLocator(anchorNode) : undefined,
2506
+ });
2507
+ }
2508
+ return this.buildPlan(operationId, request, steps, capability);
2509
+ }
2510
+ /* ---------------------------------------------------------------- */
2511
+ /* 通用 plan 构建 */
2512
+ /* ---------------------------------------------------------------- */
2513
+ buildPlan(operationId, request, steps, capability) {
2514
+ const files = new Set(steps.map((step) => step.file));
2515
+ const preSnapshots = [...files].map((file) => ({
2516
+ file,
2517
+ content: this.graph.getFileContent(file),
2518
+ version: this.graph.getFileVersion(file),
2519
+ }));
2520
+ return {
2521
+ ok: true,
2522
+ plan: {
2523
+ operationId,
2524
+ kind: request.kind,
2525
+ target: request.target,
2526
+ documentVersion: request.documentVersion,
2527
+ steps,
2528
+ preSnapshots,
2529
+ validatedCapability: capability,
2530
+ },
2531
+ };
2532
+ }
2533
+ }