@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,1391 @@
1
+ // ─── Designer Engine Facade ──────────────────────────
2
+ // 来源: docs/04-module-api-and-call-graph.md §2
3
+ //
4
+ // DesignerEngine 是唯一语义中枢。
5
+ // 所有 "can-edit / how-to-edit / what-happens-after" 决策只在 engine 中发生。
6
+ import { OperationErrorCode } from "../protocol.js";
7
+ import { buildMultiFileRenderGraph } from "../graph/project-graph-builder.js";
8
+ import { buildEditableTopology } from "../topology/topology.js";
9
+ import { resolveAllCapabilities } from "../capability/capability-resolver.js";
10
+ import { projectDetailWriteTarget, projectRenderDocument, projectRenderObjectDetail, } from "../render/render-projection.js";
11
+ import { buildComponentSemanticDescriptor } from "../render/component-semantic.js";
12
+ import { buildContentModel } from "../render/content-model.js";
13
+ import { buildMediaModel } from "../render/media-model.js";
14
+ import { buildStyleSource } from "../render/tailwind-style-model.js";
15
+ import { applyClassNameSourceWrite, extractClassNameSource, } from "../classname-source.js";
16
+ import { OperationPlanner } from "../planner/planner.js";
17
+ import { resolveProvenance, resolveTextProvenance, } from "../provenance/resolve-text-provenance.js";
18
+ import { createWritebackService } from "../writeback/writeback-service.js";
19
+ import { createEventBus } from "../events/event-bus.js";
20
+ const nextRouteEntryPattern = /^(?:src\/)?app\/.*\/?page\.[cm]?[jt]sx?$/;
21
+ const nextLayoutBasenames = [
22
+ "layout.tsx",
23
+ "layout.ts",
24
+ "layout.jsx",
25
+ "layout.js",
26
+ "layout.mjs",
27
+ "layout.cjs",
28
+ ];
29
+ const repeatItemInstanceKeyMarker = "::repeat-item#";
30
+ const richTextBlockKeyMarker = "::rich-text-block#";
31
+ // ── Default in-memory file system ──
32
+ function createDefaultFileSystem() {
33
+ const versions = new Map();
34
+ return {
35
+ async readFile(path) {
36
+ const fs = await import("node:fs/promises");
37
+ return fs.readFile(path, "utf-8");
38
+ },
39
+ async writeFile(path, content) {
40
+ const fs = await import("node:fs/promises");
41
+ await fs.writeFile(path, content, "utf-8");
42
+ },
43
+ async exists(path) {
44
+ const fs = await import("node:fs/promises");
45
+ try {
46
+ await fs.access(path);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ },
53
+ getVersion(file) {
54
+ return versions.get(file) ?? 0;
55
+ },
56
+ bumpVersion(file) {
57
+ const v = (versions.get(file) ?? 0) + 1;
58
+ versions.set(file, v);
59
+ return v;
60
+ },
61
+ };
62
+ }
63
+ function toTextSourceView(result) {
64
+ if (!result || result.kind !== "value-binding") {
65
+ return null;
66
+ }
67
+ const path = "path" in result ? result.path : undefined;
68
+ const root = result.root;
69
+ const bindingName = root && (root.kind === "binding" || root.kind === "named-export")
70
+ ? root.name
71
+ : undefined;
72
+ return {
73
+ kind: result.sourceKind,
74
+ file: result.file,
75
+ expression: result.displayExpression,
76
+ bindingName,
77
+ path: path?.map((segment) => segment.kind === "property" ? segment.name : segment.index),
78
+ };
79
+ }
80
+ function toWriteTargetView(writeTarget) {
81
+ return {
82
+ kind: writeTarget.isProxy ? "component-proxy" : "jsx-node",
83
+ file: writeTarget.file,
84
+ componentName: writeTarget.componentName,
85
+ isProxy: writeTarget.isProxy,
86
+ };
87
+ }
88
+ /**
89
+ * 创建 DesignerEngine 实例。
90
+ */
91
+ export function createInternalEditEngine(config) {
92
+ let _status = "idle";
93
+ const _fileSystem = config.fileSystem ?? createDefaultFileSystem();
94
+ const _eventBus = createEventBus();
95
+ const _routes = new Map();
96
+ let _activeRouteId = null;
97
+ const _pendingRebuildFiles = new Set();
98
+ let _rebuildLoop = null;
99
+ const _writebackService = createWritebackService(_fileSystem, {
100
+ externalAdapters: config.externalAdapters,
101
+ });
102
+ const _fileContentCache = new Map();
103
+ let _documentVersion = 0;
104
+ let _mutationsInFlight = 0;
105
+ // Register onEvent callback
106
+ if (config.onEvent) {
107
+ _eventBus.subscribe(config.onEvent);
108
+ }
109
+ // Register initial routes
110
+ if (config.routes) {
111
+ for (const route of config.routes) {
112
+ _routes.set(route.routeId, {
113
+ routeId: route.routeId,
114
+ entryFile: route.entryFile,
115
+ graph: null,
116
+ topology: null,
117
+ capabilities: null,
118
+ document: null,
119
+ detailProjections: null,
120
+ fileVersions: new Map(),
121
+ });
122
+ }
123
+ }
124
+ // ── Build route graph (async, multi-file aware) ──
125
+ async function buildDetailProjections(graph, topology, allSources) {
126
+ const detailProjections = new Map();
127
+ const provenanceFileCache = new Map();
128
+ const exists = typeof _fileSystem.exists === "function"
129
+ ? (path) => _fileSystem.exists(path)
130
+ : async () => false;
131
+ for (const [key, element] of graph.elements) {
132
+ const topNode = topology.nodes.get(key);
133
+ if (!topNode) {
134
+ continue;
135
+ }
136
+ const source = allSources.get(element.sourceFile) ??
137
+ _fileContentCache.get(element.sourceFile);
138
+ const textSources = new Map();
139
+ const textTargets = new Map();
140
+ const provenances = new Map();
141
+ const segmentResolutions = [];
142
+ for (const segment of element.textSegments) {
143
+ if (!source) {
144
+ textSources.set(segment.index, null);
145
+ continue;
146
+ }
147
+ const resolved = await resolveTextProvenance({
148
+ file: element.sourceFile,
149
+ source,
150
+ segment,
151
+ readFile: (path) => _fileSystem.readFile(path),
152
+ exists,
153
+ graph,
154
+ externalSourceResolvers: config.externalSourceResolvers,
155
+ externalAdapters: config.externalAdapters?.map(({ sourceType, adapterId, capabilities }) => ({
156
+ sourceType,
157
+ adapterId,
158
+ capabilities,
159
+ })),
160
+ functionCallContracts: config.functionCallContracts,
161
+ fileCache: provenanceFileCache,
162
+ });
163
+ const provenance = await resolveProvenance({
164
+ file: element.sourceFile,
165
+ source,
166
+ target: { kind: "text-segment", segment },
167
+ readFile: (path) => _fileSystem.readFile(path),
168
+ exists,
169
+ graph,
170
+ externalSourceResolvers: config.externalSourceResolvers,
171
+ externalAdapters: config.externalAdapters?.map(({ sourceType, adapterId, capabilities }) => ({
172
+ sourceType,
173
+ adapterId,
174
+ capabilities,
175
+ })),
176
+ functionCallContracts: config.functionCallContracts,
177
+ fileCache: provenanceFileCache,
178
+ });
179
+ const textSource = toTextSourceView(resolved);
180
+ textSources.set(segment.index, textSource);
181
+ textTargets.set(segment.index, resolved);
182
+ provenances.set(segment.index, provenance);
183
+ segmentResolutions.push({
184
+ editable: segment.editable,
185
+ source: textSource,
186
+ });
187
+ }
188
+ const writeTarget = projectDetailWriteTarget(toWriteTargetView(topNode.writeTarget), segmentResolutions);
189
+ const componentSemantic = buildComponentSemanticDescriptor({
190
+ key,
191
+ element,
192
+ graph,
193
+ provenances,
194
+ });
195
+ const contentModel = buildContentModel(element);
196
+ const mediaModel = buildMediaModel(element);
197
+ const styleSource = buildStyleSource(element);
198
+ const conditionalRegion = graph.regions.find((region) => region.kind === "conditional-region" &&
199
+ region.childKeys.includes(key));
200
+ let conditional;
201
+ const candidate = conditionalRegion?.sourceCandidates?.[0];
202
+ if (source &&
203
+ conditionalRegion &&
204
+ candidate?.kind === "conditional-branch") {
205
+ const provenance = await resolveProvenance({
206
+ file: element.sourceFile,
207
+ source,
208
+ target: { kind: "conditional-branch", candidate },
209
+ readFile: (path) => _fileSystem.readFile(path),
210
+ exists,
211
+ graph,
212
+ externalSourceResolvers: config.externalSourceResolvers,
213
+ externalAdapters: config.externalAdapters?.map(({ sourceType, adapterId, capabilities }) => ({
214
+ sourceType,
215
+ adapterId,
216
+ capabilities,
217
+ })),
218
+ functionCallContracts: config.functionCallContracts,
219
+ fileCache: provenanceFileCache,
220
+ });
221
+ const branchIndex = conditionalRegion.childKeys.indexOf(key);
222
+ conditional = {
223
+ currentBranch: branchIndex <= 0 ? "then" : "else",
224
+ availableBranches: conditionalRegion.childKeys.length > 1
225
+ ? ["then", "else"]
226
+ : ["then"],
227
+ conditionExpression: candidate.conditionExpression,
228
+ provenance: {
229
+ finalSource: provenance.finalSource
230
+ ? {
231
+ kind: provenance.finalSource.kind,
232
+ file: provenance.finalSource.file,
233
+ displayPath: provenance.finalSource.displayPath,
234
+ }
235
+ : null,
236
+ chain: provenance.chain.map((hop) => ({
237
+ kind: hop.kind,
238
+ file: hop.file,
239
+ displayName: hop.displayName,
240
+ canEditHere: hop.canEditHere,
241
+ })),
242
+ confidence: provenance.confidence,
243
+ editMode: provenance.editMode,
244
+ diagnostics: provenance.diagnostics.map((diagnostic) => ({
245
+ code: diagnostic.code,
246
+ message: diagnostic.message,
247
+ })),
248
+ },
249
+ };
250
+ }
251
+ detailProjections.set(key, {
252
+ textSources,
253
+ textTargets,
254
+ provenances,
255
+ writeTarget,
256
+ componentSemantic,
257
+ contentModel,
258
+ mediaModel,
259
+ styleSource,
260
+ conditional,
261
+ });
262
+ }
263
+ return detailProjections;
264
+ }
265
+ function getTrackedFileVersion(file) {
266
+ return typeof _fileSystem.getVersion === "function"
267
+ ? _fileSystem.getVersion(file)
268
+ : 0;
269
+ }
270
+ function getRouteVersionFloor(routeState, fileVersions) {
271
+ let versionFloor = 1;
272
+ for (const version of fileVersions.values()) {
273
+ versionFloor = Math.max(versionFloor, version);
274
+ }
275
+ return Math.max(versionFloor, routeState.graph?.version ?? 0);
276
+ }
277
+ async function buildRoute(routeId, source, options = {}) {
278
+ const routeState = _routes.get(routeId);
279
+ if (!routeState)
280
+ return;
281
+ const additionalEntries = await loadNextRouteLayoutEntries(routeState.entryFile);
282
+ // Use multi-file builder to load + merge all locally-imported files
283
+ const { graph, allSources } = await buildMultiFileRenderGraph(routeState.entryFile, source, (path) => _fileSystem.readFile(path), typeof _fileSystem.exists === "function"
284
+ ? (path) => _fileSystem.exists(path)
285
+ : async () => false, {
286
+ additionalEntries,
287
+ version: routeState.graph?.version ?? 1,
288
+ });
289
+ const fileVersions = new Map();
290
+ for (const file of allSources.keys()) {
291
+ fileVersions.set(file, getTrackedFileVersion(file));
292
+ }
293
+ let routeVersion = Math.max(getRouteVersionFloor(routeState, fileVersions), options.minimumVersion ?? 0);
294
+ if (options.changed && routeVersion <= (routeState.graph?.version ?? 0)) {
295
+ routeVersion = (routeState.graph?.version ?? 0) + 1;
296
+ }
297
+ graph.version = routeVersion;
298
+ _documentVersion = Math.max(_documentVersion, routeVersion);
299
+ // Cache all loaded source files for planner use
300
+ for (const [file, src] of allSources) {
301
+ _fileContentCache.set(file, src);
302
+ }
303
+ graph.routeId = routeId;
304
+ const topology = buildEditableTopology(graph);
305
+ const capabilities = resolveAllCapabilities(topology, graph);
306
+ const detailProjections = await buildDetailProjections(graph, topology, allSources);
307
+ const canUpdateTextByKey = resolveEffectiveUpdateTextCapabilities(graph, capabilities, detailProjections);
308
+ const document = projectRichTextBlockEntries(projectRepeatConcreteEntries(projectRenderDocument(graph, topology, capabilities, {
309
+ canUpdateTextByKey,
310
+ }), graph, detailProjections), graph, detailProjections);
311
+ routeState.graph = graph;
312
+ routeState.topology = topology;
313
+ routeState.capabilities = capabilities;
314
+ routeState.document = document;
315
+ routeState.detailProjections = detailProjections;
316
+ routeState.fileVersions = fileVersions;
317
+ }
318
+ async function loadNextRouteLayoutEntries(entryFile) {
319
+ const entries = [];
320
+ for (const file of getNextRouteLayoutCandidates(entryFile)) {
321
+ const exists = typeof _fileSystem.exists === "function"
322
+ ? await _fileSystem.exists(file)
323
+ : false;
324
+ if (!exists) {
325
+ continue;
326
+ }
327
+ entries.push({
328
+ file,
329
+ source: await _fileSystem.readFile(file),
330
+ });
331
+ }
332
+ return entries;
333
+ }
334
+ function resolveEffectiveUpdateTextCapabilities(graph, capabilities, detailProjections) {
335
+ const result = new Map();
336
+ for (const [key, element] of graph.elements) {
337
+ const capability = capabilities.get(key);
338
+ if (!capability?.canUpdateText) {
339
+ result.set(key, false);
340
+ continue;
341
+ }
342
+ const detail = detailProjections.get(key);
343
+ const hasWritableSegment = element.textSegments.some((segment) => {
344
+ if (!segment.editable) {
345
+ return false;
346
+ }
347
+ const provenance = detail?.provenances.get(segment.index) ?? null;
348
+ return !provenance || isWritableUpdateTextProvenance(provenance);
349
+ });
350
+ result.set(key, hasWritableSegment);
351
+ }
352
+ return result;
353
+ }
354
+ function isWritableUpdateTextProvenance(provenance) {
355
+ return (provenance.editMode === "direct" ||
356
+ provenance.editMode === "proxy" ||
357
+ provenance.editMode === "upstream");
358
+ }
359
+ async function rebuildRoutesForFiles(files, options = {}) {
360
+ const uniqueFiles = new Set(files);
361
+ const result = {
362
+ rebuiltRoutes: [],
363
+ failedRoutes: [],
364
+ };
365
+ for (const routeState of _routes.values()) {
366
+ const isAffected = [...uniqueFiles].some((file) => routeState.entryFile === file ||
367
+ routeState.graph?.fileIndex.has(file));
368
+ if (!isAffected) {
369
+ continue;
370
+ }
371
+ try {
372
+ const entrySource = await _fileSystem.readFile(routeState.entryFile);
373
+ await buildRoute(routeState.routeId, entrySource, options);
374
+ if (routeState.graph) {
375
+ result.rebuiltRoutes.push({
376
+ routeId: routeState.routeId,
377
+ version: routeState.graph.version,
378
+ });
379
+ }
380
+ }
381
+ catch (error) {
382
+ result.failedRoutes.push({
383
+ routeId: routeState.routeId,
384
+ message: getErrorMessage(error),
385
+ });
386
+ }
387
+ }
388
+ return result;
389
+ }
390
+ function getAffectedRouteVersion(files, fallbackVersion = 0) {
391
+ const uniqueFiles = new Set(files);
392
+ let version = fallbackVersion;
393
+ for (const routeState of _routes.values()) {
394
+ const isAffected = [...uniqueFiles].some((file) => routeState.entryFile === file ||
395
+ routeState.graph?.fileIndex.has(file));
396
+ if (isAffected && routeState.graph) {
397
+ version = Math.max(version, routeState.graph.version);
398
+ }
399
+ }
400
+ return version;
401
+ }
402
+ function getAffectedRouteIds(files) {
403
+ const uniqueFiles = new Set(files);
404
+ const routeIds = new Set();
405
+ for (const routeState of _routes.values()) {
406
+ const isAffected = [...uniqueFiles].some((file) => routeState.entryFile === file ||
407
+ routeState.graph?.fileIndex.has(file));
408
+ if (isAffected) {
409
+ routeIds.add(routeState.routeId);
410
+ }
411
+ }
412
+ return [...routeIds];
413
+ }
414
+ function scheduleRouteRebuild(files) {
415
+ for (const file of files) {
416
+ _pendingRebuildFiles.add(file);
417
+ }
418
+ if (_rebuildLoop || _status === "disposed") {
419
+ return;
420
+ }
421
+ _rebuildLoop = (async () => {
422
+ _status = "building";
423
+ try {
424
+ while (_pendingRebuildFiles.size > 0) {
425
+ await waitForMutationRebuilds();
426
+ const filesToRebuild = [..._pendingRebuildFiles];
427
+ _pendingRebuildFiles.clear();
428
+ const changedFiles = await filterChangedSourceFiles(filesToRebuild);
429
+ if (changedFiles.length === 0) {
430
+ continue;
431
+ }
432
+ const affectedRouteIds = getAffectedRouteIds(changedFiles);
433
+ for (const routeId of affectedRouteIds) {
434
+ _eventBus.emit({ type: "graph-invalidated", routeId });
435
+ }
436
+ const { rebuiltRoutes, failedRoutes } = await rebuildRoutesForFiles(changedFiles, { changed: true });
437
+ for (const { routeId, version } of rebuiltRoutes) {
438
+ _eventBus.emit({ type: "graph-ready", routeId, version });
439
+ }
440
+ for (const { routeId, message } of failedRoutes) {
441
+ _eventBus.emit({ type: "graph-error", routeId, message });
442
+ }
443
+ }
444
+ }
445
+ finally {
446
+ _rebuildLoop = null;
447
+ if (_status !== "disposed") {
448
+ _status = "ready";
449
+ }
450
+ if (_pendingRebuildFiles.size > 0) {
451
+ scheduleRouteRebuild([]);
452
+ }
453
+ }
454
+ })();
455
+ }
456
+ async function waitForMutationRebuilds() {
457
+ while (_mutationsInFlight > 0) {
458
+ await new Promise((resolve) => setTimeout(resolve, 0));
459
+ }
460
+ }
461
+ async function filterChangedSourceFiles(files) {
462
+ const changedFiles = [];
463
+ for (const file of new Set(files)) {
464
+ const cachedSource = _fileContentCache.get(file);
465
+ if (cachedSource === undefined) {
466
+ changedFiles.push(file);
467
+ continue;
468
+ }
469
+ try {
470
+ const currentSource = await _fileSystem.readFile(file);
471
+ if (currentSource !== cachedSource) {
472
+ changedFiles.push(file);
473
+ }
474
+ }
475
+ catch {
476
+ changedFiles.push(file);
477
+ }
478
+ }
479
+ return changedFiles;
480
+ }
481
+ // ── Ensure route is built ──
482
+ async function ensureRouteBuilt(routeId) {
483
+ const routeState = _routes.get(routeId);
484
+ if (!routeState)
485
+ throw new Error(`Route not found: ${routeId}`);
486
+ if (routeState.graph)
487
+ return; // already built
488
+ const source = await _fileSystem.readFile(routeState.entryFile);
489
+ await buildRoute(routeId, source);
490
+ }
491
+ function getRouteStateForKey(key, routeId) {
492
+ if (routeId) {
493
+ const routeState = _routes.get(routeId) ?? null;
494
+ if (!routeState || !routeStateHasKey(routeState, key)) {
495
+ return null;
496
+ }
497
+ return routeState;
498
+ }
499
+ for (const routeState of _routes.values()) {
500
+ if (routeStateHasKey(routeState, key)) {
501
+ return routeState;
502
+ }
503
+ }
504
+ return null;
505
+ }
506
+ function routeStateHasKey(routeState, key) {
507
+ if (routeState.graph?.elements.has(key)) {
508
+ return true;
509
+ }
510
+ const repeatInstance = parseRepeatItemInstanceKey(key);
511
+ if (repeatInstance) {
512
+ return routeState.graph?.elements.has(repeatInstance.baseKey) === true;
513
+ }
514
+ const richTextBlock = parseRichTextBlockKey(key);
515
+ return richTextBlock
516
+ ? routeState.graph?.elements.has(richTextBlock.ownerKey) === true
517
+ : false;
518
+ }
519
+ const engine = {
520
+ get status() {
521
+ return _status;
522
+ },
523
+ dispose() {
524
+ _status = "disposed";
525
+ },
526
+ registerRoute(routeId, entryFile) {
527
+ _routes.set(routeId, {
528
+ routeId,
529
+ entryFile,
530
+ graph: null,
531
+ topology: null,
532
+ capabilities: null,
533
+ document: null,
534
+ detailProjections: null,
535
+ fileVersions: new Map(),
536
+ });
537
+ },
538
+ async setActiveRoute(routeId) {
539
+ const routeState = _routes.get(routeId);
540
+ if (!routeState)
541
+ throw new Error(`Route not found: ${routeId}`);
542
+ const wasAlreadyActive = _activeRouteId === routeId;
543
+ _activeRouteId = routeId;
544
+ if (!routeState.graph) {
545
+ _status = "building";
546
+ try {
547
+ const source = await _fileSystem.readFile(routeState.entryFile);
548
+ await buildRoute(routeId, source);
549
+ const nextVersion = _routes.get(routeId)?.graph?.version ?? 1;
550
+ _status = "ready";
551
+ _eventBus.emit({
552
+ type: "graph-ready",
553
+ routeId,
554
+ version: nextVersion,
555
+ });
556
+ }
557
+ catch {
558
+ _status = "ready"; // degrade gracefully with empty state
559
+ }
560
+ return;
561
+ }
562
+ _status = "ready";
563
+ if (!wasAlreadyActive) {
564
+ _eventBus.emit({
565
+ type: "graph-ready",
566
+ routeId,
567
+ version: routeState.graph.version,
568
+ });
569
+ }
570
+ },
571
+ getDocument(routeId) {
572
+ const routeState = _routes.get(routeId);
573
+ if (!routeState)
574
+ throw new Error(`Route not found: ${routeId}`);
575
+ if (!routeState.document)
576
+ throw new Error(`Route not built: ${routeId}`);
577
+ return routeState.document;
578
+ },
579
+ getObjectCapabilities(key, routeId) {
580
+ const routeState = getRouteStateForKey(key, routeId);
581
+ const richTextBlock = parseRichTextBlockKey(key);
582
+ if (richTextBlock) {
583
+ return {
584
+ key,
585
+ canUpdateText: false,
586
+ canInsertChild: false,
587
+ canMove: true,
588
+ canRemove: true,
589
+ boundaryKind: null,
590
+ isOpaque: false,
591
+ };
592
+ }
593
+ const baseKey = parseRepeatItemInstanceKey(key)?.baseKey ?? key;
594
+ const element = routeState?.graph?.elements.get(baseKey);
595
+ const capability = routeState?.capabilities?.get(baseKey);
596
+ if (element && capability) {
597
+ return {
598
+ key,
599
+ canUpdateText: capability.canUpdateText,
600
+ canInsertChild: capability.canInsertChild,
601
+ canMove: capability.canMove,
602
+ canRemove: capability.canRemove,
603
+ boundaryKind: element.boundaryKind,
604
+ isOpaque: element.isOpaque,
605
+ };
606
+ }
607
+ throw new Error(`Key not found: ${key}`);
608
+ },
609
+ getRenderDetail(key, routeId) {
610
+ const routeState = getRouteStateForKey(key, routeId);
611
+ const richTextBlock = parseRichTextBlockKey(key);
612
+ const baseKey = richTextBlock?.ownerKey ??
613
+ parseRepeatItemInstanceKey(key)?.baseKey ??
614
+ key;
615
+ if (routeState?.graph &&
616
+ routeState.topology &&
617
+ routeState.capabilities &&
618
+ routeState.detailProjections) {
619
+ const detailState = routeState.detailProjections.get(baseKey);
620
+ if (detailState) {
621
+ const detail = projectRenderObjectDetail(baseKey, routeState.graph, routeState.topology, routeState.capabilities, detailState.textSources, detailState.writeTarget, detailState.provenances, detailState.conditional, detailState.componentSemantic, detailState.contentModel, detailState.mediaModel, detailState.styleSource, detailState.styleModel);
622
+ return key === baseKey ? detail : { ...detail, key };
623
+ }
624
+ }
625
+ throw new Error(`Key not found: ${key}`);
626
+ },
627
+ getClassNameSource(key, routeId) {
628
+ const routeState = getRouteStateForKey(key, routeId);
629
+ const baseKey = parseRepeatItemInstanceKey(key)?.baseKey ?? key;
630
+ if (!routeState?.graph) {
631
+ throw new Error(`Key not found: ${key}`);
632
+ }
633
+ const element = routeState.graph.elements.get(baseKey);
634
+ if (!element) {
635
+ throw new Error(`Key not found: ${key}`);
636
+ }
637
+ const source = _fileContentCache.get(element.sourceFile);
638
+ if (source === undefined) {
639
+ throw new Error(`Source not loaded: ${element.sourceFile}`);
640
+ }
641
+ return extractClassNameSource({
642
+ key: baseKey,
643
+ route: {
644
+ routeId: routeState.routeId,
645
+ graph: routeState.graph,
646
+ source,
647
+ },
648
+ });
649
+ },
650
+ async setClassNameSource(request) {
651
+ const routeState = getRouteStateForKey(request.key, request.routeId);
652
+ const baseKey = parseRepeatItemInstanceKey(request.key)?.baseKey ?? request.key;
653
+ if (!routeState?.graph) {
654
+ return {
655
+ ok: false,
656
+ reason: "node_not_found",
657
+ diagnostics: [
658
+ {
659
+ code: "source-locator-warning",
660
+ message: `Key not found: ${request.key}`,
661
+ },
662
+ ],
663
+ };
664
+ }
665
+ const element = routeState.graph.elements.get(baseKey);
666
+ if (!element) {
667
+ return {
668
+ ok: false,
669
+ reason: "node_not_found",
670
+ diagnostics: [
671
+ {
672
+ code: "source-locator-warning",
673
+ message: `Key not found: ${request.key}`,
674
+ },
675
+ ],
676
+ };
677
+ }
678
+ const source = _fileContentCache.get(element.sourceFile) ??
679
+ (await _fileSystem.readFile(element.sourceFile));
680
+ const current = extractClassNameSource({
681
+ key: baseKey,
682
+ route: {
683
+ routeId: routeState.routeId,
684
+ graph: routeState.graph,
685
+ source,
686
+ },
687
+ });
688
+ const result = await applyClassNameSourceWrite({
689
+ request,
690
+ current,
691
+ source,
692
+ writeSource: async (nextSource) => {
693
+ await _fileSystem.writeFile(element.sourceFile, nextSource);
694
+ _fileSystem.bumpVersion?.(element.sourceFile);
695
+ _fileContentCache.set(element.sourceFile, nextSource);
696
+ },
697
+ });
698
+ if (!result.ok) {
699
+ return result;
700
+ }
701
+ await rebuildRoutesForFiles([element.sourceFile], {
702
+ changed: true,
703
+ minimumVersion: request.expectedVersion.documentVersion + 1,
704
+ });
705
+ return {
706
+ ok: true,
707
+ source: this.getClassNameSource(baseKey, routeState.routeId),
708
+ };
709
+ },
710
+ async execute(request) {
711
+ // 1. Find route
712
+ let routeState = null;
713
+ if (request.target.kind === "node") {
714
+ routeState = getRouteStateForKey(request.target.key, request.routeId);
715
+ }
716
+ else {
717
+ routeState = _routes.get(request.target.routeId) ?? null;
718
+ if (routeState && !routeState.graph) {
719
+ await ensureRouteBuilt(routeState.routeId);
720
+ }
721
+ }
722
+ if (!routeState) {
723
+ return {
724
+ requestId: request.id,
725
+ kind: request.kind,
726
+ target: request.target,
727
+ ok: false,
728
+ resultVersion: _documentVersion,
729
+ error: {
730
+ code: request.target.kind === "route"
731
+ ? OperationErrorCode.ROUTE_NOT_FOUND
732
+ : OperationErrorCode.NODE_NOT_FOUND,
733
+ message: request.target.kind === "route"
734
+ ? `Route not found: ${request.target.routeId}`
735
+ : `Target key not found: ${request.target.key}`,
736
+ },
737
+ };
738
+ }
739
+ if (!routeState.graph ||
740
+ !routeState.topology ||
741
+ !routeState.capabilities) {
742
+ return {
743
+ requestId: request.id,
744
+ kind: request.kind,
745
+ target: request.target,
746
+ ok: false,
747
+ resultVersion: _documentVersion,
748
+ error: {
749
+ code: request.target.kind === "route"
750
+ ? OperationErrorCode.ROUTE_NOT_FOUND
751
+ : OperationErrorCode.NODE_NOT_FOUND,
752
+ message: request.target.kind === "route"
753
+ ? `Route not ready: ${request.target.routeId}`
754
+ : `Target key not found: ${request.target.key}`,
755
+ },
756
+ };
757
+ }
758
+ // 2. Plan via OperationPlanner
759
+ const graphAccess = createPlannerGraphAccess(routeState, routeState.graph, routeState.capabilities, _fileContentCache, _fileSystem);
760
+ const planResult = await new OperationPlanner(graphAccess).plan(request);
761
+ if (!planResult.ok) {
762
+ return {
763
+ requestId: request.id,
764
+ kind: request.kind,
765
+ target: request.target,
766
+ ok: false,
767
+ resultVersion: _documentVersion,
768
+ error: {
769
+ code: mapInternalErrorCode(planResult.errorCode),
770
+ message: planResult.reason,
771
+ },
772
+ };
773
+ }
774
+ const rewritePlan = planResult.plan;
775
+ if (rewritePlan.steps.length === 0) {
776
+ return {
777
+ requestId: request.id,
778
+ kind: request.kind,
779
+ target: request.target,
780
+ ok: false,
781
+ resultVersion: _documentVersion,
782
+ error: {
783
+ code: OperationErrorCode.PLAN_FAILED,
784
+ message: "Planner could not generate a valid execution plan",
785
+ },
786
+ };
787
+ }
788
+ // 3. Delegate execute + snapshot + undo-stack to writeback-service
789
+ _mutationsInFlight += 1;
790
+ const internal = await _writebackService.execute(rewritePlan);
791
+ if (!internal.ok) {
792
+ _mutationsInFlight -= 1;
793
+ return {
794
+ requestId: request.id,
795
+ kind: request.kind,
796
+ target: request.target,
797
+ ok: false,
798
+ resultVersion: _documentVersion,
799
+ error: {
800
+ code: mapInternalErrorCode(internal.errorCode),
801
+ message: internal.reason,
802
+ },
803
+ };
804
+ }
805
+ // 4. Rebuild graph for affected files
806
+ try {
807
+ await rebuildRoutesForFiles(internal.affectedFiles, {
808
+ changed: true,
809
+ minimumVersion: internal.resultVersion,
810
+ });
811
+ }
812
+ finally {
813
+ _mutationsInFlight -= 1;
814
+ }
815
+ const resultVersion = getAffectedRouteVersion(internal.affectedFiles, internal.resultVersion);
816
+ _documentVersion = Math.max(_documentVersion, resultVersion);
817
+ // 5. Emit events
818
+ const result = {
819
+ requestId: request.id,
820
+ kind: request.kind,
821
+ target: request.target,
822
+ ok: true,
823
+ resultVersion,
824
+ };
825
+ _eventBus.emit({ type: "operation-completed", result });
826
+ if (routeState.routeId) {
827
+ _eventBus.emit({
828
+ type: "document-changed",
829
+ routeId: routeState.routeId,
830
+ version: resultVersion,
831
+ });
832
+ }
833
+ return result;
834
+ },
835
+ async undo() {
836
+ _mutationsInFlight += 1;
837
+ const internal = await _writebackService.undo();
838
+ if (!internal.ok) {
839
+ _mutationsInFlight -= 1;
840
+ return {
841
+ requestId: "undo",
842
+ kind: "update-text",
843
+ target: { kind: "node", key: "" },
844
+ ok: false,
845
+ resultVersion: _documentVersion,
846
+ error: {
847
+ code: OperationErrorCode.INTERNAL_ERROR,
848
+ message: internal.reason,
849
+ },
850
+ };
851
+ }
852
+ try {
853
+ await rebuildRoutesForFiles(internal.affectedFiles, {
854
+ changed: true,
855
+ minimumVersion: internal.resultVersion,
856
+ });
857
+ }
858
+ finally {
859
+ _mutationsInFlight -= 1;
860
+ }
861
+ const resultVersion = getAffectedRouteVersion(internal.affectedFiles, internal.resultVersion);
862
+ _documentVersion = Math.max(_documentVersion, resultVersion);
863
+ return {
864
+ requestId: "undo",
865
+ kind: "update-text",
866
+ target: { kind: "node", key: "" },
867
+ ok: true,
868
+ resultVersion,
869
+ };
870
+ },
871
+ async redo() {
872
+ _mutationsInFlight += 1;
873
+ const internal = await _writebackService.redo();
874
+ if (!internal.ok) {
875
+ _mutationsInFlight -= 1;
876
+ return {
877
+ requestId: "redo",
878
+ kind: "update-text",
879
+ target: { kind: "node", key: "" },
880
+ ok: false,
881
+ resultVersion: _documentVersion,
882
+ error: {
883
+ code: OperationErrorCode.INTERNAL_ERROR,
884
+ message: internal.reason,
885
+ },
886
+ };
887
+ }
888
+ try {
889
+ await rebuildRoutesForFiles(internal.affectedFiles, {
890
+ changed: true,
891
+ minimumVersion: internal.resultVersion,
892
+ });
893
+ }
894
+ finally {
895
+ _mutationsInFlight -= 1;
896
+ }
897
+ const resultVersion = getAffectedRouteVersion(internal.affectedFiles, internal.resultVersion);
898
+ _documentVersion = Math.max(_documentVersion, resultVersion);
899
+ return {
900
+ requestId: "redo",
901
+ kind: "update-text",
902
+ target: { kind: "node", key: "" },
903
+ ok: true,
904
+ resultVersion,
905
+ };
906
+ },
907
+ notifyFileChanged(file) {
908
+ const affectedFiles = new Set();
909
+ // Find routes affected by this file change
910
+ for (const routeState of _routes.values()) {
911
+ if (routeState.entryFile === file ||
912
+ routeState.graph?.fileIndex.has(file)) {
913
+ affectedFiles.add(file);
914
+ }
915
+ }
916
+ scheduleRouteRebuild(affectedFiles);
917
+ },
918
+ subscribe(listener) {
919
+ return _eventBus.subscribe(listener);
920
+ },
921
+ };
922
+ return engine;
923
+ }
924
+ // ── PlannerGraphAccess adapter ──────────────────────────────────────
925
+ /**
926
+ * Creates a PlannerGraphAccess from the engine's route state + file system.
927
+ * Maps protocol/engine types to the shapes planner.ts expects.
928
+ */
929
+ function createPlannerGraphAccess(routeState, graph, capabilities, fileContentCache, fs) {
930
+ const repeatRootOwners = new Map();
931
+ const repeatTemplateOwners = new Map();
932
+ const visitRepeatTemplateNode = (key, ownerKey) => {
933
+ if (repeatTemplateOwners.has(key)) {
934
+ return;
935
+ }
936
+ repeatTemplateOwners.set(key, ownerKey);
937
+ const element = graph.elements.get(key);
938
+ for (const childKey of element?.childKeys ?? []) {
939
+ visitRepeatTemplateNode(childKey, ownerKey);
940
+ }
941
+ };
942
+ for (const region of graph.regions) {
943
+ if (region.kind !== "repeat-region")
944
+ continue;
945
+ for (const childKey of region.childKeys) {
946
+ repeatRootOwners.set(childKey, region.ownerKey);
947
+ visitRepeatTemplateNode(childKey, region.ownerKey);
948
+ }
949
+ }
950
+ return {
951
+ getNode(key) {
952
+ const resolved = resolvePlannerKey(key);
953
+ const element = graph.elements.get(resolved.baseKey);
954
+ if (!element)
955
+ return null;
956
+ return toNodeInfo(key, element, resolved.repeatIndex, resolved.richTextBlock);
957
+ },
958
+ getChildNodes(key) {
959
+ const resolved = resolvePlannerKey(key);
960
+ if (resolved.richTextBlock) {
961
+ return [];
962
+ }
963
+ const element = graph.elements.get(resolved.baseKey);
964
+ if (!element)
965
+ return [];
966
+ return element.childKeys
967
+ .map((childKey) => {
968
+ const child = graph.elements.get(childKey);
969
+ return child ? toNodeInfo(childKey, child) : null;
970
+ })
971
+ .filter((child) => child !== null);
972
+ },
973
+ getCapability(key) {
974
+ const resolved = resolvePlannerKey(key);
975
+ if (resolved.richTextBlock) {
976
+ return {
977
+ key,
978
+ canUpdateText: false,
979
+ canRemove: true,
980
+ canInsertChild: false,
981
+ canMove: true,
982
+ constraints: [],
983
+ };
984
+ }
985
+ const cap = capabilities.get(resolved.baseKey);
986
+ if (!cap) {
987
+ return {
988
+ key,
989
+ canUpdateText: false,
990
+ canRemove: false,
991
+ canInsertChild: false,
992
+ canMove: false,
993
+ constraints: [],
994
+ };
995
+ }
996
+ return {
997
+ key,
998
+ canUpdateText: cap.canUpdateText,
999
+ canRemove: cap.canRemove,
1000
+ canInsertChild: cap.canInsertChild,
1001
+ canMove: cap.canMove,
1002
+ constraints: cap.constraints.map((c) => c.message),
1003
+ };
1004
+ },
1005
+ getFileContent(file) {
1006
+ return fileContentCache.get(file) ?? "";
1007
+ },
1008
+ getFileVersion(file) {
1009
+ return Math.max(routeState.fileVersions.get(file) ?? 0, graph.version);
1010
+ },
1011
+ getRouteEntryFile(routeId) {
1012
+ return routeState.routeId === routeId ? routeState.entryFile : null;
1013
+ },
1014
+ getComponentSemantic(key) {
1015
+ const resolved = resolvePlannerKey(key);
1016
+ return (routeState.detailProjections?.get(resolved.baseKey)
1017
+ ?.componentSemantic ?? null);
1018
+ },
1019
+ getEditableTextSegmentIndex(key) {
1020
+ const resolved = resolvePlannerKey(key);
1021
+ const iterator = routeState.detailProjections
1022
+ ?.get(resolved.baseKey)
1023
+ ?.textTargets.keys();
1024
+ const first = iterator?.next();
1025
+ return first && !first.done ? first.value : null;
1026
+ },
1027
+ async getTextWriteTarget(key, segmentIndex) {
1028
+ const resolved = resolvePlannerKey(key);
1029
+ return retargetRepeatTextWriteTarget(routeState.detailProjections
1030
+ ?.get(resolved.baseKey)
1031
+ ?.textTargets.get(segmentIndex) ?? null, resolved.repeatIndex);
1032
+ },
1033
+ getTextProvenance(key, segmentIndex) {
1034
+ const resolved = resolvePlannerKey(key);
1035
+ return (routeState.detailProjections
1036
+ ?.get(resolved.baseKey)
1037
+ ?.provenances.get(segmentIndex) ?? null);
1038
+ },
1039
+ };
1040
+ function resolvePlannerKey(key) {
1041
+ const richTextBlock = parseRichTextBlockKey(key);
1042
+ if (richTextBlock) {
1043
+ return {
1044
+ baseKey: richTextBlock.ownerKey,
1045
+ repeatIndex: null,
1046
+ richTextBlock,
1047
+ };
1048
+ }
1049
+ const repeatInstance = parseRepeatItemInstanceKey(key);
1050
+ if (repeatInstance) {
1051
+ return {
1052
+ baseKey: repeatInstance.baseKey,
1053
+ repeatIndex: repeatInstance.repeatIndex,
1054
+ richTextBlock: null,
1055
+ };
1056
+ }
1057
+ return { baseKey: key, repeatIndex: null, richTextBlock: null };
1058
+ }
1059
+ function toNodeInfo(key, element, repeatIndex = null, richTextBlock = null) {
1060
+ const baseKey = richTextBlock?.ownerKey ??
1061
+ parseRepeatItemInstanceKey(key)?.baseKey ??
1062
+ key;
1063
+ const detail = routeState.detailProjections?.get(baseKey);
1064
+ return {
1065
+ key,
1066
+ file: element.sourceFile,
1067
+ component: element.componentName,
1068
+ tag: element.tag,
1069
+ structuralPath: element.identity.structuralPath,
1070
+ sourceRange: element.sourceRange,
1071
+ parentKey: element.parentKey ?? undefined,
1072
+ isRepeatRegion: element.boundaryKind === "repeat-region" ||
1073
+ repeatTemplateOwners.has(baseKey),
1074
+ isBoundary: false,
1075
+ importedFrom: element.importedFrom,
1076
+ exportName: element.exportName,
1077
+ isRepeatItemRoot: repeatIndex !== null || repeatRootOwners.has(baseKey),
1078
+ repeatOwnerKey: repeatRootOwners.get(baseKey) ?? repeatTemplateOwners.get(baseKey),
1079
+ contentModel: detail?.contentModel,
1080
+ richTextBlock: richTextBlock
1081
+ ? {
1082
+ field: richTextBlock.field,
1083
+ index: richTextBlock.index,
1084
+ }
1085
+ : undefined,
1086
+ attributes: element.attributes?.map((attribute) => ({
1087
+ name: attribute.name,
1088
+ value: attribute.value,
1089
+ source: attribute.source
1090
+ ? {
1091
+ expression: attribute.source.expression,
1092
+ base: attribute.source.base,
1093
+ path: attribute.source.path.map((segment) => ({ ...segment })),
1094
+ }
1095
+ : null,
1096
+ resolvedValue: attribute.resolvedValue,
1097
+ })),
1098
+ };
1099
+ }
1100
+ }
1101
+ /** Maps writeback-service internal error codes to protocol OperationErrorCode. */
1102
+ function mapInternalErrorCode(code) {
1103
+ switch (code) {
1104
+ case "stale-version":
1105
+ return OperationErrorCode.VERSION_STALE;
1106
+ case "capability-denied":
1107
+ return OperationErrorCode.CAPABILITY_REJECTED;
1108
+ case "route-not-found":
1109
+ return OperationErrorCode.ROUTE_NOT_FOUND;
1110
+ case "unsupported-operation":
1111
+ return OperationErrorCode.UNSUPPORTED_OPERATION;
1112
+ case "invalid-target":
1113
+ return OperationErrorCode.INVALID_TARGET;
1114
+ case "invalid-params":
1115
+ return OperationErrorCode.INVALID_PARAMS;
1116
+ case "locator-miss":
1117
+ return OperationErrorCode.LOCATOR_FAILED;
1118
+ case "ast-error":
1119
+ return OperationErrorCode.AST_PARSE_ERROR;
1120
+ case "writeback-failed":
1121
+ return OperationErrorCode.WRITE_IO_ERROR;
1122
+ case "rollback-failed":
1123
+ return OperationErrorCode.ROLLBACK_FAILED;
1124
+ default:
1125
+ return OperationErrorCode.INTERNAL_ERROR;
1126
+ }
1127
+ }
1128
+ function getErrorMessage(error) {
1129
+ if (error instanceof Error && error.message) {
1130
+ return error.message;
1131
+ }
1132
+ return String(error);
1133
+ }
1134
+ function getNextRouteLayoutCandidates(entryFile) {
1135
+ const normalized = normalizeRouteFile(entryFile);
1136
+ if (!nextRouteEntryPattern.test(normalized)) {
1137
+ return [];
1138
+ }
1139
+ const appRoot = normalized.startsWith("src/app/") ? "src/app" : "app";
1140
+ const routeDirectory = normalized.slice(0, normalized.lastIndexOf("/"));
1141
+ const directories = [];
1142
+ let current = routeDirectory;
1143
+ while (current === appRoot || current.startsWith(`${appRoot}/`)) {
1144
+ directories.unshift(current);
1145
+ if (current === appRoot) {
1146
+ break;
1147
+ }
1148
+ current = current.slice(0, current.lastIndexOf("/"));
1149
+ }
1150
+ return directories.flatMap((directory) => nextLayoutBasenames.map((basename) => `${directory}/${basename}`));
1151
+ }
1152
+ function normalizeRouteFile(file) {
1153
+ return file.replace(/\\/g, "/").replace(/^\.\//, "");
1154
+ }
1155
+ function makeRepeatItemInstanceKey(baseKey, index) {
1156
+ return `${baseKey}${repeatItemInstanceKeyMarker}${index}`;
1157
+ }
1158
+ function makeRichTextBlockKey(ownerKey, field, index) {
1159
+ return `${ownerKey}${richTextBlockKeyMarker}${encodeURIComponent(field)}#${index}`;
1160
+ }
1161
+ function parseRichTextBlockKey(key) {
1162
+ if (typeof key !== "string") {
1163
+ return null;
1164
+ }
1165
+ const markerIndex = key.lastIndexOf(richTextBlockKeyMarker);
1166
+ if (markerIndex < 0) {
1167
+ return null;
1168
+ }
1169
+ const ownerKey = key.slice(0, markerIndex);
1170
+ const rest = key.slice(markerIndex + richTextBlockKeyMarker.length);
1171
+ const separatorIndex = rest.lastIndexOf("#");
1172
+ if (!ownerKey || separatorIndex <= 0) {
1173
+ return null;
1174
+ }
1175
+ const field = decodeURIComponent(rest.slice(0, separatorIndex));
1176
+ const index = Number(rest.slice(separatorIndex + 1));
1177
+ if (!field || !Number.isInteger(index) || index < 0) {
1178
+ return null;
1179
+ }
1180
+ return { ownerKey, field, index };
1181
+ }
1182
+ function parseRepeatItemInstanceKey(key) {
1183
+ if (typeof key !== "string") {
1184
+ return null;
1185
+ }
1186
+ const markerIndex = key.lastIndexOf(repeatItemInstanceKeyMarker);
1187
+ if (markerIndex < 0) {
1188
+ return null;
1189
+ }
1190
+ const baseKey = key.slice(0, markerIndex);
1191
+ const repeatIndex = Number(key.slice(markerIndex + repeatItemInstanceKeyMarker.length));
1192
+ if (!baseKey || !Number.isInteger(repeatIndex) || repeatIndex < 0) {
1193
+ return null;
1194
+ }
1195
+ return { baseKey, repeatIndex };
1196
+ }
1197
+ function getRepeatItemCount(detail) {
1198
+ let count = 1;
1199
+ for (const target of detail?.textTargets.values() ?? []) {
1200
+ if (target?.kind !== "value-binding") {
1201
+ continue;
1202
+ }
1203
+ if (target.sourceKind !== "repeat-template") {
1204
+ continue;
1205
+ }
1206
+ count = Math.max(count, target.collectionIndexMap?.length ?? 0, target.collectionPathMap?.length ?? 0);
1207
+ }
1208
+ return count;
1209
+ }
1210
+ function getRichTextBlocks(value) {
1211
+ if (typeof value === "string") {
1212
+ return value === "" ? [] : value.split(/\n{2,}/);
1213
+ }
1214
+ if (Array.isArray(value)) {
1215
+ return value.filter(isOperationValue);
1216
+ }
1217
+ if (isOperationObject(value) &&
1218
+ Array.isArray(value.content) &&
1219
+ value.content.every(isOperationValue)) {
1220
+ return value.content;
1221
+ }
1222
+ return [];
1223
+ }
1224
+ function isOperationObject(value) {
1225
+ return !Array.isArray(value) && typeof value === "object" && value !== null;
1226
+ }
1227
+ function isOperationValue(value) {
1228
+ if (value === null ||
1229
+ typeof value === "string" ||
1230
+ typeof value === "number" ||
1231
+ typeof value === "boolean") {
1232
+ return true;
1233
+ }
1234
+ if (Array.isArray(value)) {
1235
+ return value.every(isOperationValue);
1236
+ }
1237
+ if (typeof value === "object" && value !== null) {
1238
+ return Object.values(value).every(isOperationValue);
1239
+ }
1240
+ return false;
1241
+ }
1242
+ function getAttributeValue(element, field) {
1243
+ const attribute = element.attributes?.find((candidate) => candidate.name === field);
1244
+ if (!attribute) {
1245
+ return undefined;
1246
+ }
1247
+ if (attribute.resolvedValue !== undefined) {
1248
+ return attribute.resolvedValue;
1249
+ }
1250
+ return isOperationValue(attribute.value) ? attribute.value : undefined;
1251
+ }
1252
+ function cloneValuePath(path) {
1253
+ return path?.map((segment) => ({ ...segment }));
1254
+ }
1255
+ function retargetRepeatTextWriteTarget(target, repeatIndex) {
1256
+ if (repeatIndex === null ||
1257
+ target?.kind !== "value-binding" ||
1258
+ target.sourceKind !== "repeat-template") {
1259
+ return target;
1260
+ }
1261
+ const mappedIndex = target.collectionIndexMap?.[repeatIndex] ?? repeatIndex;
1262
+ const mappedPath = cloneValuePath(target.collectionPathMap?.[repeatIndex]) ??
1263
+ cloneValuePath(target.locator?.path);
1264
+ if (mappedPath) {
1265
+ for (let index = mappedPath.length - 1; index >= 0; index -= 1) {
1266
+ const segment = mappedPath[index];
1267
+ if (segment.kind === "index") {
1268
+ segment.index = mappedIndex;
1269
+ break;
1270
+ }
1271
+ }
1272
+ }
1273
+ return {
1274
+ ...target,
1275
+ path: cloneValuePath(target.path),
1276
+ locator: target.locator
1277
+ ? {
1278
+ ...target.locator,
1279
+ path: mappedPath,
1280
+ }
1281
+ : target.locator,
1282
+ collectionIndexMap: target.collectionIndexMap
1283
+ ? [...target.collectionIndexMap]
1284
+ : undefined,
1285
+ collectionPathMap: target.collectionPathMap?.map((path) => cloneValuePath(path) ?? []),
1286
+ };
1287
+ }
1288
+ function projectRepeatConcreteEntries(document, graph, detailProjections) {
1289
+ const repeatItemRoots = new Set();
1290
+ for (const region of graph.regions) {
1291
+ if (region.kind !== "repeat-region") {
1292
+ continue;
1293
+ }
1294
+ for (const childKey of region.childKeys) {
1295
+ repeatItemRoots.add(childKey);
1296
+ }
1297
+ }
1298
+ if (repeatItemRoots.size === 0) {
1299
+ return document;
1300
+ }
1301
+ const entryByKey = new Map(document.entries.map((entry) => [
1302
+ entry.key,
1303
+ { ...entry, childKeys: [...entry.childKeys] },
1304
+ ]));
1305
+ const entries = [];
1306
+ for (const originalEntry of document.entries) {
1307
+ const entry = entryByKey.get(originalEntry.key) ?? {
1308
+ ...originalEntry,
1309
+ childKeys: [...originalEntry.childKeys],
1310
+ };
1311
+ entries.push(entry);
1312
+ if (!repeatItemRoots.has(entry.key)) {
1313
+ continue;
1314
+ }
1315
+ const repeatItemCount = getRepeatItemCount(detailProjections.get(entry.key));
1316
+ if (repeatItemCount <= 1) {
1317
+ continue;
1318
+ }
1319
+ const parentEntry = entry.parentKey
1320
+ ? entryByKey.get(entry.parentKey)
1321
+ : null;
1322
+ const baseChildIndex = parentEntry?.childKeys.indexOf(entry.key) ?? -1;
1323
+ const virtualKeys = [];
1324
+ for (let index = 1; index < repeatItemCount; index += 1) {
1325
+ const virtualKey = makeRepeatItemInstanceKey(entry.key, index);
1326
+ virtualKeys.push(virtualKey);
1327
+ entries.push({
1328
+ ...entry,
1329
+ key: virtualKey,
1330
+ parentKey: entry.parentKey,
1331
+ childKeys: [...entry.childKeys],
1332
+ });
1333
+ }
1334
+ if (parentEntry && baseChildIndex >= 0) {
1335
+ parentEntry.childKeys.splice(baseChildIndex + 1, 0, ...virtualKeys);
1336
+ }
1337
+ }
1338
+ return {
1339
+ ...document,
1340
+ rootKeys: [...document.rootKeys],
1341
+ entries,
1342
+ };
1343
+ }
1344
+ function projectRichTextBlockEntries(document, graph, detailProjections) {
1345
+ const entryByKey = new Map(document.entries.map((entry) => [
1346
+ entry.key,
1347
+ { ...entry, childKeys: [...entry.childKeys] },
1348
+ ]));
1349
+ const entries = [];
1350
+ let hasRichTextBlocks = false;
1351
+ for (const originalEntry of document.entries) {
1352
+ const entry = entryByKey.get(originalEntry.key) ?? {
1353
+ ...originalEntry,
1354
+ childKeys: [...originalEntry.childKeys],
1355
+ };
1356
+ entries.push(entry);
1357
+ const contentModel = detailProjections.get(entry.key)?.contentModel;
1358
+ if (!contentModel?.operationBoundary.includes("remove-rich-text-block")) {
1359
+ continue;
1360
+ }
1361
+ const element = graph.elements.get(entry.key);
1362
+ if (!element) {
1363
+ continue;
1364
+ }
1365
+ const blocks = getRichTextBlocks(getAttributeValue(element, contentModel.field));
1366
+ if (blocks.length === 0) {
1367
+ continue;
1368
+ }
1369
+ const blockKeys = blocks.map((_, index) => makeRichTextBlockKey(entry.key, contentModel.field, index));
1370
+ entry.childKeys.push(...blockKeys);
1371
+ for (let index = 0; index < blockKeys.length; index += 1) {
1372
+ entries.push({
1373
+ key: blockKeys[index],
1374
+ tag: "rich-text-block",
1375
+ label: `${contentModel.field} block ${index + 1}`,
1376
+ parentKey: entry.key,
1377
+ childKeys: [],
1378
+ operationSummary: {
1379
+ canUpdateText: false,
1380
+ canInsertChild: false,
1381
+ canMove: true,
1382
+ canRemove: true,
1383
+ },
1384
+ });
1385
+ }
1386
+ hasRichTextBlocks = true;
1387
+ }
1388
+ return hasRichTextBlocks
1389
+ ? { ...document, rootKeys: [...document.rootKeys], entries }
1390
+ : document;
1391
+ }